537 lines
17 KiB
JavaScript
537 lines
17 KiB
JavaScript
(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();
|
|
}
|
|
})();
|