Add plyr again as fallback

This commit is contained in:
2026-07-28 21:41:30 +02:00
parent eaf48276f0
commit 75b98de746
7 changed files with 589 additions and 43 deletions
+436
View File
@@ -0,0 +1,436 @@
// Plyr Fallback Player
import Plyr from 'plyr';
import 'plyr/dist/plyr.css';
import * as dashjs from 'dashjs';
import SubtitlesOctopus from '@jellyfin/libass-wasm';
import { initMobileWidescreen } from './player-mobile';
import { mobileDoubleClick } from './player-mobile';
import { playNextPlaylistVideo } from './playlist';
import { addVideoTracks } from './player-data';
import { addSubtitleTracks } from './player-data';
import { serverSelectMenuItem, serverSelectSubmenu, serverSelectMenuClickToggle } from './player-server-select';
import { isIOS } from './detect-ios';
import { startEngagementTracking, stopEngagementTracking } from './player/player-engagement';
import { renderHeatmap } from './player/player-heatmap';
var player = null;
var av1Supported = (!!document.createElement('video').canPlayType('video/webm; codecs="av01.0.05M.08, opus"'));
var dashSupported = dashjs.supportsMediaSource();
var apiResponse = {};
var volume = 0.5;
var muted = false;
var captions = true;
var lastTime = 0.0;
var streamServer = '';
var streamServers = [];
var streamServerIndex = 0;
var streamServerCount = 0;
var ambientMode = true;
var serverFallback = false;
var saveInterval;
var watchTracked = false;
var subtitleInstance = null;
function trackWatchTime() {
if (watchTracked) return;
var video = document.getElementsByTagName('video')[0];
if (video && video.currentTime >= 10) {
watchTracked = true;
var episodeId = document.getElementById('e_id').value;
window.axios.post('/watched/track', {
episode_id: episodeId
}).then(function () {
console.log('Watch tracked for episode ' + episodeId);
}).catch(function (error) {
console.error('Failed to track watch: ' + error);
});
}
}
var controls = [
'play-large',
'play',
'progress',
'current-time',
'duration',
'mute',
'volume',
'captions',
'settings',
'fullscreen',
];
if (localStorage.hstreamVolume) {
volume = parseFloat(localStorage.getItem('hstreamVolume')).toFixed(2);
console.log('Loaded Audio Volume from Local Storage: ' + volume);
}
if (localStorage.hstreamCaptions) {
captions = (localStorage.getItem('hstreamCaptions') == 'true');
console.log('Loaded Captions Status from Local Storage: ' + captions);
}
if (localStorage.hstreamMuted) {
muted = (localStorage.getItem('hstreamMuted') == 'true');
console.log('Loaded Muted Status from Local Storage: ' + muted);
}
if (localStorage.hstreamServerFallback) {
serverFallback = (localStorage.getItem('hstreamServerFallback') == 'true');
console.log('Loaded Server Fallback Status from Local Storage: ' + serverFallback);
}
if (!av1Supported) {
document.getElementById('av1-unsupported').classList.remove('hidden');
}
function initDash(data, player) {
const video = document.querySelector('video');
data.forEach(function (el) {
if (el.mode === 'mpd' && el.size === player.config.quality.selected) {
const dash = dashjs.MediaPlayer().create();
dash.initialize(video, el.src, true);
window.player = player;
window.dash = dash;
}
});
}
function setCanvasDimension(canvas, video) {
canvas.height = video.offsetHeight;
canvas.width = video.offsetWidth;
}
function paintStaticVideo(ctx, video) {
if (localStorage.theme == 'light') {
return;
}
if (!ambientMode) {
return;
}
ctx.drawImage(video, 0, 0, video.offsetWidth, video.offsetHeight);
}
function toggleAmbientMode() {
let canvas = document.getElementById('ambientVideo'), ctx = canvas.getContext('2d'), video = document.getElementsByTagName('video')[0];
if (ambientMode) {
ambientMode = false;
localStorage.ambientMode = 'false';
setCanvasDimension(canvas, video);
document.getElementById('ambient-mode-toggle').innerHTML = '<span>Ambient Mode<span class="plyr__menu__value">Off</span></span>';
} else {
ambientMode = true;
localStorage.ambientMode = 'true';
setCanvasDimension(canvas, video);
paintStaticVideo(ctx, video);
document.getElementById('ambient-mode-toggle').innerHTML = '<span>Ambient Mode<span class="plyr__menu__value">On</span></span>';
}
}
function toggleAsiaServer() {
if (serverFallback) {
serverFallback = false;
localStorage.hstreamServerFallback = 'false';
document.getElementById('server-fallback-toggle').innerHTML = '<span>Fallback Server<span class="plyr__menu__value">Off</span></span>';
streamServers = apiResponse.stream_domains;
} else {
serverFallback = true;
localStorage.hstreamServerFallback = 'true';
document.getElementById('server-fallback-toggle').innerHTML = '<span>Fallback Server<span class="plyr__menu__value">On</span></span>';
streamServers = apiResponse.asia_stream_domains;
}
streamServerCount = streamServers.length;
streamServerIndex = Math.floor(Math.random() * streamServerCount);
streamServer = streamServers[streamServerIndex];
console.log('Selected Server: ' + streamServer);
if (player) {
clearInterval(saveInterval);
stopEngagementTracking();
player.destroy();
}
initPlayer();
}
function initSubtitles(lang) {
if (isIOS()) {
return;
}
if (subtitleInstance != null && subtitleInstance instanceof SubtitlesOctopus) {
subtitleInstance.dispose();
}
let newSubUrl = streamServer + '/' + apiResponse.stream_url + '/';
if (lang != 'en') {
newSubUrl += 'autotrans/' + lang + '.ass';
} else {
newSubUrl += 'eng.ass';
}
let subFont = '/fonts/Figtree-ExtraBold.woff2';
if (lang == 'hi') {
subFont = '/fonts/Hind-SemiBold.ttf';
}
var options = {
video: document.getElementsByTagName('video')[0],
subUrl: newSubUrl,
workerUrl: '/build/js/subtitles-octopus-worker.js',
legacyWorkerUrl: '/build/js/subtitles-octopus-worker-legacy.js',
fonts: [subFont],
renderMode: 'wasm-blend',
};
subtitleInstance = new SubtitlesOctopus(options);
}
function initPlayer() {
player = new Plyr('#player', {
controls,
quality: {
default: 720,
options: [2161, 2160, 1081, 1080, 720]
},
i18n: {
qualityLabel: {
2161: '2160p48',
2160: '2160p',
1081: '1080p48',
1080: '1080p',
720: '720p'
},
qualityBadge: {
2161: 'UHD@48',
1081: 'FHD@48',
1080: 'FHD',
},
},
fullscreen: { enabled: true, fallback: true, iosNative: true }
});
var data = addVideoTracks(streamServer, apiResponse, av1Supported, dashSupported);
player.source = {
type: 'video',
title: apiResponse.title,
poster: apiResponse.poster,
previewThumbnails: {
enabled: true,
src: streamServer + '/' + apiResponse.stream_url + '/thumbs.vtt',
},
sources: data,
tracks: addSubtitleTracks(streamServer, apiResponse)
};
player.volume = volume;
player.muted = muted;
player.captions.language = 'en';
player.captions.active = captions;
if (dashSupported && !apiResponse.legacy) {
player.on('qualitychange', () => {
initDash(data, player);
});
initDash(data, player);
}
let canvas = document.getElementById('ambientVideo'), ctx = canvas.getContext('2d'), video = document.getElementsByTagName('video')[0];
setCanvasDimension(canvas, video);
paintStaticVideo(ctx, video);
var allItems = document.getElementsByClassName('plyr__control--forward');
var lastItem = allItems[allItems.length - 1];
lastItem.insertAdjacentHTML('afterend', '<button id="ambient-mode-toggle" type="button" class="plyr__control" role="menuitem" aria-haspopup="true"><span>Ambient Mode<span class="plyr__menu__value">On</span></span></button>');
document.getElementById('ambient-mode-toggle').addEventListener('click', toggleAmbientMode);
if (localStorage.ambientMode == 'false') {
toggleAmbientMode();
}
lastItem = allItems[allItems.length - 1];
let value = 'Off';
if (serverFallback) { value = 'On'; }
lastItem.insertAdjacentHTML('afterend', '<button id="server-fallback-toggle" type="button" class="plyr__control" role="menuitem" aria-haspopup="true"><span>Fallback Server<span class="plyr__menu__value">' + value + '</span></span></button>');
document.getElementById('server-fallback-toggle').addEventListener('click', toggleAsiaServer);
var clickedPlay = false;
player.on('play', () => {
if (!clickedPlay) {
player.stop();
console.log('Stopped video, because user didn\'t click play.');
}
const episodeId = document.getElementById('e_id').value;
startEngagementTracking(episodeId);
setCanvasDimension(canvas, video);
console.log('Play => Function Loop()');
var $this = video;
(function loop() {
if (!player.paused && !player.ended && localStorage.theme == 'dark' && ambientMode) {
ctx.drawImage($this, 0, 0, $this.offsetWidth, $this.offsetHeight);
setTimeout(loop, 24000 / 1001);
}
})();
});
player.on('seeked', () => {
paintStaticVideo(ctx, video);
if (player.currentTime > 0) {
lastTime = player.currentTime;
}
console.log('Seeked => paintStaticVideo() at ' + player.currentTime);
});
window.addEventListener('resize', () => {
setCanvasDimension(canvas, video);
if (player.paused) {
paintStaticVideo(ctx, video);
}
});
player.on('captionsenabled', () => {
document.getElementsByClassName('libassjs-canvas-parent')[0].style.visibility = 'visible';
localStorage.setItem('hstreamCaptions', 'true');
console.log('Set Captions Status to Local Storage: true');
});
player.on('captionsdisabled', () => {
document.getElementsByClassName('libassjs-canvas-parent')[0].style.visibility = 'hidden';
localStorage.setItem('hstreamCaptions', 'false');
console.log('Set Captions Status to Local Storage: false');
});
player.on('volumechange', () => {
console.log('Saving Audio Volume to Local Storage: ' + player.volume);
localStorage.setItem('hstreamVolume', player.volume.toString());
console.log('Saving Audio Muted to Local Storage: ' + player.muted.toString());
localStorage.setItem('hstreamMuted', player.muted.toString());
});
player.on('ended', () => {
playNextPlaylistVideo();
});
player.on('timeupdate', () => {
trackWatchTime();
});
player.on('languagechange', (event) => {
let lang = event.detail.plyr.captions.language;
console.log('Subtitle Event ' + lang);
initSubtitles(lang);
});
function playerPlayTemp() {
clickedPlay = true;
}
document.querySelectorAll('[data-plyr="play"]').forEach(play =>
play.addEventListener('click', playerPlayTemp)
);
document.getElementsByClassName('plyr--video')[0].addEventListener('click', playerPlayTemp);
initMobileWidescreen();
setTimeout(function () {
const params = new URLSearchParams(window.location.search);
const time = parseInt(params.get('t'));
if (!isNaN(time)) {
player.currentTime = time;
console.log('Skipping to ' + time);
}
if (lastTime > 0) {
player.currentTime = lastTime;
console.log('Skipping to ' + lastTime);
}
}, 500);
player.on('ready', () => {
mobileDoubleClick(player);
const video = document.querySelector('video');
const episodeId = document.getElementById('e_id').value;
if (video && video.duration) {
renderHeatmap(episodeId, video.duration);
} else if (video) {
video.addEventListener('loadedmetadata', function onMeta() {
video.removeEventListener('loadedmetadata', onMeta);
renderHeatmap(episodeId, video.duration);
});
}
});
var settingElements = document.getElementsByClassName('plyr__control--forward');
if (settingElements.length == 3) {
settingElements[2].insertAdjacentHTML('afterend', serverSelectMenuItem(streamServerIndex));
var settingNodes = document.getElementsByClassName('plyr__menu__container')[0].childNodes[0].childNodes;
if (settingNodes.length == 4) {
document.getElementsByClassName('plyr__menu__container')[0].childNodes[0].childNodes[3].insertAdjacentHTML('afterend', serverSelectSubmenu(streamServerIndex, streamServerCount));
}
document.getElementById('server-select').addEventListener('click', serverSelectMenuClickToggle);
document.getElementById('server-select-list-back-btn').addEventListener('click', serverSelectMenuClickToggle);
let serverSelects = document.getElementsByClassName('change_server');
for (let i = 0; i < serverSelects.length; i++) {
serverSelects[i].addEventListener('click', function () {
streamServerIndex = Number(this.value);
streamServer = streamServers[streamServerIndex];
console.log('Selected Server: ' + streamServer);
if (player) {
clearInterval(saveInterval);
stopEngagementTracking();
player.destroy();
}
initPlayer();
});
}
}
saveInterval = setInterval(function () {
lastTime = player.currentTime;
console.log('Last Player Position: ' + lastTime);
}, 10000);
}
export function initPlyrPlayer(episodeId) {
window.axios.post('/player/api', {
episode_id: episodeId
}).then(function (response) {
if (response.status == 200) {
apiResponse = response.data;
streamServers = apiResponse.stream_domains;
if (serverFallback) {
streamServers = apiResponse.asia_stream_domains;
}
streamServerCount = streamServers.length;
streamServerIndex = Math.floor(Math.random() * streamServerCount);
streamServer = streamServers[streamServerIndex];
console.log('Selected Server: ' + streamServer + ' with Index: ' + streamServerIndex);
initPlayer();
}
}).catch(function (error) {
var alert = document.getElementById('player-alert');
if (alert) {
alert.innerText = 'The player encountered a problem: ' + error;
alert.classList.remove('hidden');
}
});
return player;
}