Add HLS support to multi-audio
This commit is contained in:
536
youtube/static/js/plyr.hls.start.js
Normal file
536
youtube/static/js/plyr.hls.start.js
Normal file
@@ -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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user