aboutsummaryrefslogtreecommitdiffstats
path: root/youtube/static/js/plyr.hls.start.js
diff options
context:
space:
mode:
authorAstounds <kirito@disroot.org>2026-04-05 14:56:51 -0500
committerAstounds <kirito@disroot.org>2026-04-05 14:56:51 -0500
commitf0649be5dec84ce06a3164a2d9ee90f5385ac92f (patch)
tree6dcae30ff3e0d66c895033aab9e92a4c9e4ed513 /youtube/static/js/plyr.hls.start.js
parent62a028968e6d9b4e821b6014d6658b8317328fcf (diff)
downloadyt-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.js536
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();
+ }
+})();