Add HLS support to multi-audio
This commit is contained in:
323
youtube/static/js/watch.hls.js
Normal file
323
youtube/static/js/watch.hls.js
Normal file
@@ -0,0 +1,323 @@
|
||||
const video = document.getElementById('js-video-player');
|
||||
|
||||
window.hls = null;
|
||||
let hls = null;
|
||||
|
||||
// ===========
|
||||
// HLS NATIVE
|
||||
// ===========
|
||||
function initHLSNative(manifestUrl) {
|
||||
if (!manifestUrl) {
|
||||
console.error('No HLS manifest URL provided');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Initializing native HLS player with manifest:', manifestUrl);
|
||||
|
||||
if (hls) {
|
||||
window.hls = null;
|
||||
hls.destroy();
|
||||
hls = null;
|
||||
}
|
||||
|
||||
if (Hls.isSupported()) {
|
||||
hls = new Hls({
|
||||
enableWorker: true,
|
||||
lowLatencyMode: false,
|
||||
maxBufferLength: 30,
|
||||
maxMaxBufferLength: 60,
|
||||
startLevel: -1,
|
||||
});
|
||||
|
||||
window.hls = hls;
|
||||
|
||||
hls.loadSource(manifestUrl);
|
||||
hls.attachMedia(video);
|
||||
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, function(event, data) {
|
||||
console.log('Native manifest parsed');
|
||||
console.log('Levels:', data.levels.length);
|
||||
|
||||
const qualitySelect = document.getElementById('quality-select');
|
||||
|
||||
if (qualitySelect && data.levels?.length) {
|
||||
qualitySelect.innerHTML = '<option value="-1">Auto</option>';
|
||||
|
||||
const sorted = [...data.levels].sort((a, b) => b.height - a.height);
|
||||
const seen = new Set();
|
||||
|
||||
sorted.forEach(level => {
|
||||
if (!seen.has(level.height)) {
|
||||
seen.add(level.height);
|
||||
|
||||
const i = data.levels.indexOf(level);
|
||||
const opt = document.createElement('option');
|
||||
|
||||
opt.value = i;
|
||||
opt.textContent = level.height + 'p';
|
||||
|
||||
qualitySelect.appendChild(opt);
|
||||
}
|
||||
});
|
||||
|
||||
// Set initial quality from settings
|
||||
if (typeof window.data !== 'undefined' && window.data.settings) {
|
||||
const defaultRes = window.data.settings.default_resolution;
|
||||
if (defaultRes !== 'auto' && defaultRes) {
|
||||
const target = parseInt(defaultRes);
|
||||
let bestLevel = -1;
|
||||
let bestHeight = 0;
|
||||
for (let i = 0; i < hls.levels.length; i++) {
|
||||
const h = hls.levels[i].height;
|
||||
if (h <= target && h > bestHeight) {
|
||||
bestHeight = h;
|
||||
bestLevel = i;
|
||||
}
|
||||
}
|
||||
if (bestLevel !== -1) {
|
||||
hls.currentLevel = bestLevel;
|
||||
qualitySelect.value = bestLevel;
|
||||
console.log('Starting at resolution:', bestHeight + 'p');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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:
|
||||
hls.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = manifestUrl;
|
||||
} else {
|
||||
console.error('HLS not supported');
|
||||
}
|
||||
}
|
||||
|
||||
// ======
|
||||
// INIT
|
||||
// ======
|
||||
function initPlayer() {
|
||||
console.log('Init native player');
|
||||
|
||||
if (typeof hls_manifest_url === 'undefined' || !hls_manifest_url) {
|
||||
console.error('No manifest URL');
|
||||
return;
|
||||
}
|
||||
|
||||
initHLSNative(hls_manifest_url);
|
||||
|
||||
const qualitySelect = document.getElementById('quality-select');
|
||||
if (qualitySelect) {
|
||||
qualitySelect.addEventListener('change', function () {
|
||||
const level = parseInt(this.value);
|
||||
|
||||
if (hls) {
|
||||
hls.currentLevel = level;
|
||||
console.log('Quality:', level === -1 ? 'Auto' : hls.levels[level]?.height + 'p');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// DOM READY
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initPlayer);
|
||||
} else {
|
||||
initPlayer();
|
||||
}
|
||||
|
||||
// =============
|
||||
// AUDIO TRACKS
|
||||
// =============
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const audioTrackSelect = document.getElementById('audio-track-select');
|
||||
|
||||
if (audioTrackSelect) {
|
||||
audioTrackSelect.addEventListener('change', function() {
|
||||
const trackId = this.value;
|
||||
|
||||
if (hls && hls.audioTracks) {
|
||||
const index = hls.audioTracks.findIndex(t =>
|
||||
t.lang === trackId || t.name === trackId
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
hls.audioTrack = index;
|
||||
console.log('Audio track changed to:', index);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (hls) {
|
||||
hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, (_, data) => {
|
||||
console.log('Audio tracks:', data.audioTracks);
|
||||
|
||||
// Populate audio track select if needed
|
||||
if (audioTrackSelect && data.audioTracks.length > 0) {
|
||||
audioTrackSelect.innerHTML = '<option value="">Select audio track</option>';
|
||||
data.audioTracks.forEach(track => {
|
||||
const option = document.createElement('option');
|
||||
option.value = track.lang || track.name;
|
||||
option.textContent = track.name || track.lang || `Track ${track.id}`;
|
||||
audioTrackSelect.appendChild(option);
|
||||
});
|
||||
audioTrackSelect.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ============
|
||||
// START TIME
|
||||
// ============
|
||||
if (typeof data !== 'undefined' && data.time_start != 0 && video) {
|
||||
video.addEventListener('loadedmetadata', function() {
|
||||
video.currentTime = data.time_start;
|
||||
});
|
||||
}
|
||||
|
||||
// ==============
|
||||
// SPEED CONTROL
|
||||
// ==============
|
||||
let speedInput = document.getElementById('speed-control');
|
||||
|
||||
if (speedInput) {
|
||||
speedInput.addEventListener('keyup', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
let speed = parseFloat(speedInput.value);
|
||||
if(!isNaN(speed)){
|
||||
video.playbackRate = speed;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =========
|
||||
// Autoplay
|
||||
// =========
|
||||
(function() {
|
||||
if (typeof data === 'undefined' || (data.settings.related_videos_mode === 0 && data.playlist === null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let playability_error = !!data.playability_error;
|
||||
let isPlaylist = false;
|
||||
if (data.playlist !== null && data.playlist['current_index'] !== null)
|
||||
isPlaylist = true;
|
||||
|
||||
// read cookies on whether to autoplay
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie
|
||||
let cookieValue;
|
||||
let playlist_id;
|
||||
if (isPlaylist) {
|
||||
// from https://stackoverflow.com/a/6969486
|
||||
function escapeRegExp(string) {
|
||||
// $& means the whole matched string
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
playlist_id = data.playlist['id'];
|
||||
playlist_id = escapeRegExp(playlist_id);
|
||||
|
||||
cookieValue = document.cookie.replace(new RegExp(
|
||||
'(?:(?:^|.*;\\s*)autoplay_'
|
||||
+ playlist_id + '\\s*\\=\\s*([^;]*).*$)|^.*$'
|
||||
), '$1');
|
||||
} else {
|
||||
cookieValue = document.cookie.replace(new RegExp(
|
||||
'(?:(?:^|.*;\\s*)autoplay\\s*\\=\\s*([^;]*).*$)|^.*$'
|
||||
),'$1');
|
||||
}
|
||||
|
||||
let autoplayEnabled = 0;
|
||||
if(cookieValue.length === 0){
|
||||
autoplayEnabled = 0;
|
||||
} else {
|
||||
autoplayEnabled = Number(cookieValue);
|
||||
}
|
||||
|
||||
// check the checkbox if autoplay is on
|
||||
let checkbox = document.querySelector('.autoplay-toggle');
|
||||
if(autoplayEnabled){
|
||||
checkbox.checked = true;
|
||||
}
|
||||
|
||||
// listen for checkbox to turn autoplay on and off
|
||||
let cookie = 'autoplay'
|
||||
if (isPlaylist)
|
||||
cookie += '_' + playlist_id;
|
||||
|
||||
checkbox.addEventListener( 'change', function() {
|
||||
if(this.checked) {
|
||||
autoplayEnabled = 1;
|
||||
document.cookie = cookie + '=1; SameSite=Strict';
|
||||
} else {
|
||||
autoplayEnabled = 0;
|
||||
document.cookie = cookie + '=0; SameSite=Strict';
|
||||
}
|
||||
});
|
||||
|
||||
if(!playability_error){
|
||||
// play the video if autoplay is on
|
||||
if(autoplayEnabled){
|
||||
video.play().catch(function(e) {
|
||||
// Autoplay blocked by browser - ignore silently
|
||||
console.log('Autoplay blocked:', e.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// determine next video url
|
||||
let nextVideoUrl;
|
||||
if (isPlaylist) {
|
||||
let currentIndex = data.playlist['current_index'];
|
||||
if (data.playlist['current_index']+1 == data.playlist['items'].length)
|
||||
nextVideoUrl = null;
|
||||
else
|
||||
nextVideoUrl = data.playlist['items'][data.playlist['current_index']+1]['url'];
|
||||
|
||||
// scroll playlist to proper position
|
||||
// item height + gap == 100
|
||||
let pl = document.querySelector('.playlist-videos');
|
||||
pl.scrollTop = 100*currentIndex;
|
||||
} else {
|
||||
if (data.related.length === 0)
|
||||
nextVideoUrl = null;
|
||||
else
|
||||
nextVideoUrl = data.related[0]['url'];
|
||||
}
|
||||
let nextVideoDelay = 1000;
|
||||
|
||||
// go to next video when video ends
|
||||
// https://stackoverflow.com/a/2880950
|
||||
if (nextVideoUrl) {
|
||||
if(playability_error){
|
||||
videoEnded();
|
||||
} else {
|
||||
video.addEventListener('ended', videoEnded, false);
|
||||
}
|
||||
function nextVideo(){
|
||||
if(autoplayEnabled){
|
||||
window.location.href = nextVideoUrl;
|
||||
}
|
||||
}
|
||||
function videoEnded(e) {
|
||||
window.setTimeout(nextVideo, nextVideoDelay);
|
||||
}
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user