diff options
| author | Astounds <kirito@disroot.org> | 2026-04-05 14:56:51 -0500 |
|---|---|---|
| committer | Astounds <kirito@disroot.org> | 2026-04-05 14:56:51 -0500 |
| commit | f0649be5dec84ce06a3164a2d9ee90f5385ac92f (patch) | |
| tree | 6dcae30ff3e0d66c895033aab9e92a4c9e4ed513 /youtube/static/js/plyr.hls.start.js | |
| parent | 62a028968e6d9b4e821b6014d6658b8317328fcf (diff) | |
| download | yt-local-f0649be5dec84ce06a3164a2d9ee90f5385ac92f.tar.lz yt-local-f0649be5dec84ce06a3164a2d9ee90f5385ac92f.tar.xz yt-local-f0649be5dec84ce06a3164a2d9ee90f5385ac92f.zip | |
Add HLS support to multi-audio
Diffstat (limited to 'youtube/static/js/plyr.hls.start.js')
| -rw-r--r-- | youtube/static/js/plyr.hls.start.js | 536 |
1 files changed, 536 insertions, 0 deletions
diff --git a/youtube/static/js/plyr.hls.start.js b/youtube/static/js/plyr.hls.start.js new file mode 100644 index 0000000..91e0221 --- /dev/null +++ b/youtube/static/js/plyr.hls.start.js @@ -0,0 +1,536 @@ +(function main() { + 'use strict'; + + console.log('Plyr start script loaded'); + + // Captions + let captionsActive = false; + if (typeof data !== 'undefined' && (data.settings.subtitles_mode === 2 || (data.settings.subtitles_mode === 1 && data.has_manual_captions))) { + captionsActive = true; + } + + // AutoPlay + let autoplayActive = typeof data !== 'undefined' && data.settings.autoplay_videos || false; + + // Quality map: label -> hls level index + window.hlsQualityMap = {}; + + let plyrInstance = null; + let currentQuality = 'auto'; + let hls = null; + window.hls = null; + + /** + * Get start level from settings (highest quality <= target) + */ + function getStartLevel(levels) { + if (typeof data === 'undefined' || !data.settings) return -1; + const defaultRes = data.settings.default_resolution; + if (defaultRes === 'auto' || !defaultRes) return -1; + const target = parseInt(defaultRes); + + // Find the level with the highest height that is still <= target + let bestLevel = -1; + let bestHeight = 0; + for (let i = 0; i < levels.length; i++) { + const h = levels[i].height; + if (h <= target && h > bestHeight) { + bestHeight = h; + bestLevel = i; + } + } + return bestLevel; + } + + /** + * Initialize HLS + */ + function initHLS(manifestUrl) { + return new Promise((resolve, reject) => { + if (!manifestUrl) { + reject('No HLS manifest URL provided'); + return; + } + + console.log('Initializing HLS for Plyr:', manifestUrl); + + if (hls) { + hls.destroy(); + hls = null; + } + + hls = new Hls({ + enableWorker: true, + lowLatencyMode: false, + maxBufferLength: 30, + maxMaxBufferLength: 60, + startLevel: -1, + }); + + window.hls = hls; + + const video = document.getElementById('js-video-player'); + if (!video) { + reject('Video element not found'); + return; + } + + hls.loadSource(manifestUrl); + hls.attachMedia(video); + + hls.on(Hls.Events.MANIFEST_PARSED, function(event, data) { + console.log('HLS manifest parsed, levels:', hls.levels?.length); + + // Set initial quality from settings + const startLevel = getStartLevel(hls.levels); + if (startLevel !== -1) { + hls.currentLevel = startLevel; + const level = hls.levels[startLevel]; + currentQuality = level.height + 'p'; + console.log('Starting at resolution:', currentQuality); + } + + resolve(hls); + }); + + hls.on(Hls.Events.ERROR, function(_, data) { + if (data.fatal) { + console.error('HLS fatal error:', data.type, data.details); + switch (data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + hls.startLoad(); + break; + case Hls.ErrorTypes.MEDIA_ERROR: + hls.recoverMediaError(); + break; + default: + reject(data); + break; + } + } + }); + }); + } + + /** + * Change HLS quality + */ + function changeHLSQuality(quality) { + if (!hls) { + console.error('HLS not available'); + return; + } + + console.log('Changing HLS quality to:', quality); + + if (quality === 'auto') { + hls.currentLevel = -1; + currentQuality = 'auto'; + console.log('HLS quality set to Auto'); + const qualityBtnText = document.getElementById('plyr-quality-text'); + if (qualityBtnText) { + qualityBtnText.textContent = 'Auto'; + } + } else { + const levelIndex = window.hlsQualityMap[quality]; + if (levelIndex !== undefined) { + hls.currentLevel = levelIndex; + currentQuality = quality; + console.log('HLS quality set to:', quality); + + const qualityBtnText = document.getElementById('plyr-quality-text'); + if (qualityBtnText) { + qualityBtnText.textContent = quality; + } + } + } + } + + /** + * Create custom quality control in Plyr controls + */ + function addCustomQualityControl(player, qualityLabels) { + player.on('ready', () => { + console.log('Adding custom quality control...'); + + const controls = player.elements.container.querySelector('.plyr__controls'); + if (!controls) { + console.error('Controls not found'); + return; + } + + if (document.getElementById('plyr-quality-container')) { + console.log('Quality control already exists'); + return; + } + + const qualityContainer = document.createElement('div'); + qualityContainer.id = 'plyr-quality-container'; + qualityContainer.className = 'plyr__control plyr__control--custom'; + + const qualityButton = document.createElement('button'); + qualityButton.type = 'button'; + qualityButton.className = 'plyr__control'; + qualityButton.setAttribute('data-plyr', 'quality-custom'); + qualityButton.setAttribute('aria-label', 'Quality'); + qualityButton.innerHTML = ` + <svg class="plyr__icon hls_quality_icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"> + <rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect> + <line x1="8" y1="12" x2="16" y2="12"></line> + <line x1="12" y1="8" x2="12" y2="16"></line> + </svg> + <span id="plyr-quality-text">${currentQuality === 'auto' ? 'Auto' : currentQuality}</span> + <svg class="plyr__icon" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"> + <polyline points="6 9 12 15 18 9"></polyline> + </svg> + `; + + const dropdown = document.createElement('div'); + dropdown.className = 'plyr-quality-dropdown'; + + qualityLabels.forEach(label => { + const option = document.createElement('div'); + option.className = 'plyr-quality-option'; + option.textContent = label === 'auto' ? 'Auto' : label; + + if (label === currentQuality) { + option.setAttribute('data-active', 'true'); + } + + option.addEventListener('click', (e) => { + e.stopPropagation(); + changeHLSQuality(label); + + dropdown.querySelectorAll('.plyr-quality-option').forEach(opt => { + opt.removeAttribute('data-active'); + }); + option.setAttribute('data-active', 'true'); + + dropdown.style.display = 'none'; + }); + + dropdown.appendChild(option); + }); + + qualityButton.addEventListener('click', (e) => { + e.stopPropagation(); + const isVisible = dropdown.style.display === 'block'; + document.querySelectorAll('.plyr-quality-dropdown, .plyr-audio-dropdown').forEach(d => { + d.style.display = 'none'; + }); + dropdown.style.display = isVisible ? 'none' : 'block'; + }); + + document.addEventListener('click', (e) => { + if (!qualityContainer.contains(e.target)) { + dropdown.style.display = 'none'; + } + }); + + qualityContainer.appendChild(qualityButton); + qualityContainer.appendChild(dropdown); + + const settingsBtn = controls.querySelector('[data-plyr="settings"]'); + if (settingsBtn) { + settingsBtn.insertAdjacentElement('beforebegin', qualityContainer); + } else { + controls.appendChild(qualityContainer); + } + + console.log('Custom quality control added'); + }); + } + + /** + * Create custom audio tracks control in Plyr controls + */ + function addCustomAudioTracksControl(player, hlsInstance) { + player.on('ready', () => { + console.log('Adding custom audio tracks control...'); + + const controls = player.elements.container.querySelector('.plyr__controls'); + if (!controls) { + console.error('Controls not found'); + return; + } + + if (document.getElementById('plyr-audio-container')) { + console.log('Audio tracks control already exists'); + return; + } + + const audioContainer = document.createElement('div'); + audioContainer.id = 'plyr-audio-container'; + audioContainer.className = 'plyr__control plyr__control--custom'; + + const audioButton = document.createElement('button'); + audioButton.type = 'button'; + audioButton.className = 'plyr__control'; + audioButton.setAttribute('data-plyr', 'audio-custom'); + audioButton.setAttribute('aria-label', 'Audio Track'); + audioButton.innerHTML = ` + <svg class="plyr__icon hls_audio_icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"> + <path d="M3 18v-6a9 9 0 0 1 18 0v6"></path> + <path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3z"></path> + <path d="M3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path> + </svg> + <span id="plyr-audio-text">Audio</span> + <svg class="plyr__icon" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"> + <polyline points="6 9 12 15 18 9"></polyline> + </svg> + `; + + const audioDropdown = document.createElement('div'); + audioDropdown.className = 'plyr-audio-dropdown'; + + function updateAudioDropdown() { + if (!hlsInstance || !hlsInstance.audioTracks) return; + + audioDropdown.innerHTML = ''; + + if (hlsInstance.audioTracks.length === 0) { + const noTrackMsg = document.createElement('div'); + noTrackMsg.className = 'plyr-audio-no-tracks'; + noTrackMsg.textContent = 'No audio tracks'; + audioDropdown.appendChild(noTrackMsg); + return; + } + + hlsInstance.audioTracks.forEach((track, idx) => { + const option = document.createElement('div'); + option.className = 'plyr-audio-option'; + option.textContent = track.name || track.lang || `Track ${idx + 1}`; + + if (hlsInstance.audioTrack === idx) { + option.setAttribute('data-active', 'true'); + } + + option.addEventListener('click', (e) => { + e.stopPropagation(); + hlsInstance.audioTrack = idx; + console.log('Audio track changed to:', track.name || track.lang || idx); + + const audioText = document.getElementById('plyr-audio-text'); + if (audioText) { + const trackName = track.name || track.lang || `Track ${idx + 1}`; + audioText.textContent = trackName.length > 8 ? trackName.substring(0, 6) + '...' : trackName; + } + + audioDropdown.querySelectorAll('.plyr-audio-option').forEach(opt => { + opt.removeAttribute('data-active'); + }); + option.setAttribute('data-active', 'true'); + + audioDropdown.style.display = 'none'; + }); + + audioDropdown.appendChild(option); + }); + } + + audioButton.addEventListener('click', (e) => { + e.stopPropagation(); + updateAudioDropdown(); + const isVisible = audioDropdown.style.display === 'block'; + document.querySelectorAll('.plyr-quality-dropdown, .plyr-audio-dropdown').forEach(d => { + d.style.display = 'none'; + }); + audioDropdown.style.display = isVisible ? 'none' : 'block'; + }); + + document.addEventListener('click', (e) => { + if (!audioContainer.contains(e.target)) { + audioDropdown.style.display = 'none'; + } + }); + + audioContainer.appendChild(audioButton); + audioContainer.appendChild(audioDropdown); + + const qualityContainer = document.getElementById('plyr-quality-container'); + if (qualityContainer) { + qualityContainer.insertAdjacentElement('beforebegin', audioContainer); + } else { + const settingsBtn = controls.querySelector('[data-plyr="settings"]'); + if (settingsBtn) { + settingsBtn.insertAdjacentElement('beforebegin', audioContainer); + } else { + controls.appendChild(audioContainer); + } + } + + if (hlsInstance && hlsInstance.audioTracks && hlsInstance.audioTracks.length > 0) { + // Prefer "original" audio track + const originalIdx = hlsInstance.audioTracks.findIndex(t => + (t.name || '').toLowerCase().includes('original') + ); + if (originalIdx !== -1) { + hlsInstance.audioTrack = originalIdx; + console.log('Selected original audio track:', hlsInstance.audioTracks[originalIdx].name); + } + + const currentTrack = hlsInstance.audioTracks[hlsInstance.audioTrack]; + if (currentTrack) { + const audioText = document.getElementById('plyr-audio-text'); + if (audioText) { + const trackName = currentTrack.name || currentTrack.lang || 'Audio'; + audioText.textContent = trackName.length > 8 ? trackName.substring(0, 6) + '...' : trackName; + } + } + } + + hlsInstance.on(Hls.Events.AUDIO_TRACKS_UPDATED, () => { + console.log('Audio tracks updated, count:', hlsInstance.audioTracks?.length); + if (hlsInstance.audioTracks?.length > 0) { + updateAudioDropdown(); + const currentTrack = hlsInstance.audioTracks[hlsInstance.audioTrack]; + if (currentTrack) { + const audioText = document.getElementById('plyr-audio-text'); + if (audioText) { + const trackName = currentTrack.name || currentTrack.lang || 'Audio'; + audioText.textContent = trackName.length > 8 ? trackName.substring(0, 6) + '...' : trackName; + } + } + } + }); + + console.log('Custom audio tracks control added'); + }); + } + + /** + * Initialize Plyr with HLS quality options + */ + function initPlyrWithQuality(hlsInstance) { + const video = document.getElementById('js-video-player'); + + if (!hlsInstance || !hlsInstance.levels || hlsInstance.levels.length === 0) { + console.error('HLS not ready'); + return; + } + + if (!video) { + console.error('Video element not found'); + return; + } + + console.log('HLS levels available:', hlsInstance.levels.length); + + const sortedLevels = [...hlsInstance.levels].sort((a, b) => b.height - a.height); + + const seenHeights = new Set(); + const uniqueLevels = []; + + sortedLevels.forEach((level) => { + if (!seenHeights.has(level.height)) { + seenHeights.add(level.height); + uniqueLevels.push(level); + } + }); + + const qualityLabels = ['auto']; + uniqueLevels.forEach((level) => { + const originalIndex = hlsInstance.levels.indexOf(level); + const label = level.height + 'p'; + if (!window.hlsQualityMap[label]) { + qualityLabels.push(label); + window.hlsQualityMap[label] = originalIndex; + } + }); + + console.log('Quality labels:', qualityLabels); + + const playerOptions = { + autoplay: autoplayActive, + disableContextMenu: false, + captions: { + active: captionsActive, + language: typeof data !== 'undefined' ? data.settings.subtitles_language : 'en', + }, + controls: [ + 'play-large', + 'play', + 'progress', + 'current-time', + 'duration', + 'mute', + 'volume', + 'captions', + 'settings', + 'pip', + 'airplay', + 'fullscreen', + ], + iconUrl: '/youtube.com/static/modules/plyr/plyr.svg', + blankVideo: '/youtube.com/static/modules/plyr/blank.webm', + debug: false, + storage: { enabled: false }, + previewThumbnails: { + enabled: typeof storyboard_url !== 'undefined' && storyboard_url !== null, + src: typeof storyboard_url !== 'undefined' && storyboard_url !== null ? [storyboard_url] : [], + }, + settings: ['captions', 'speed', 'loop'], + tooltips: { + controls: true, + }, + }; + + console.log('Creating Plyr...'); + + try { + plyrInstance = new Plyr(video, playerOptions); + console.log('Plyr instance created'); + + window.plyrInstance = plyrInstance; + + addCustomQualityControl(plyrInstance, qualityLabels); + addCustomAudioTracksControl(plyrInstance, hlsInstance); + + if (plyrInstance.eventListeners) { + plyrInstance.eventListeners.forEach(function(eventListener) { + if(eventListener.type === 'dblclick') { + eventListener.element.removeEventListener(eventListener.type, eventListener.callback, eventListener.options); + } + }); + } + + plyrInstance.started = false; + plyrInstance.once('playing', function(){this.started = true}); + + if (typeof data !== 'undefined' && data.time_start != 0) { + video.addEventListener('loadedmetadata', function() { + video.currentTime = data.time_start; + }); + } + + console.log('Plyr init complete'); + } catch (e) { + console.error('Failed to initialize Plyr:', e); + } + } + + /** + * Main initialization + */ + async function start() { + console.log('Starting Plyr with HLS...'); + + if (typeof hls_manifest_url === 'undefined' || !hls_manifest_url) { + console.error('No HLS manifest URL available'); + return; + } + + try { + const hlsInstance = await initHLS(hls_manifest_url); + initPlyrWithQuality(hlsInstance); + } catch (error) { + console.error('Failed to initialize:', error); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', start); + } else { + start(); + } +})(); |
