Files
hstream/resources/js/player.js
T
2026-07-26 22:26:37 +02:00

306 lines
9.9 KiB
JavaScript

// HStream Custom Video Player
import 'vidstack/player/styles/default/theme.css';
import 'vidstack/player/styles/default/layouts/video.css';
import { VidstackPlayer, VidstackPlayerLayout } from 'vidstack/global/player';
import * as dashjs from 'dashjs';
import SubtitlesOctopus from '@jellyfin/libass-wasm';
import { HStreamPlayer } from './player/player-core';
import { initMobileWidescreen, initMobileDoubleTap, isMobile } from './player/player-mobile';
import { playNextPlaylistVideo } from './playlist';
import { addVideoTracks, addSubtitleTracks } from './player/player-data';
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 fallbackServers = [];
var streamServerIndex = 0;
var streamServerCount = 0;
var ambientMode = true;
var saveInterval;
var watchTracked = false;
var subtitleInstance = null;
function trackWatchTime() {
if (watchTracked) return;
var videoEl = document.getElementsByTagName('video')[0];
if (videoEl && videoEl.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);
});
}
}
if (localStorage.hstreamVolume) {
volume = parseFloat(localStorage.getItem('hstreamVolume'));
if (!isNaN(volume)) volume = Math.max(0, Math.min(1, volume));
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 (!av1Supported) {
document.getElementById('av1-unsupported').classList.remove('hidden');
}
function initSubtitles(lang) {
if (isIOS()) {
return;
}
if (subtitleInstance !== null && subtitleInstance instanceof SubtitlesOctopus) {
subtitleInstance.dispose();
}
var newSubUrl = streamServer + '/' + apiResponse.stream_url + '/';
if (lang !== 'en') {
newSubUrl += 'autotrans/' + lang + '.ass';
} else {
newSubUrl += 'eng.ass';
}
var 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 initPlayerQualityChange(data) {
if (dashSupported && !apiResponse.legacy) {
player.on('qualitychange', function () {
initDash(data);
});
initDash(data);
}
}
function initDash(data) {
var videoEl = document.querySelector('video');
var quality = player.quality;
data.forEach(function (el) {
if (el.mode === 'mpd' && el.size === quality) {
var dash = dashjs.MediaPlayer().create();
dash.initialize(videoEl, el.src, true);
window.dash = dash;
player.dash = dash;
}
});
}
function initPlayer() {
var videoEl = document.querySelector('#player');
var container = videoEl.parentElement;
var data = addVideoTracks(streamServer, apiResponse, av1Supported, dashSupported);
var subtitleTracks = addSubtitleTracks(streamServer, apiResponse);
var vttThumbsUrl = streamServer + '/' + apiResponse.stream_url + '/thumbs.vtt';
player = new HStreamPlayer({
container: container,
video: videoEl,
apiResponse: apiResponse,
streamServer: streamServer,
streamServers: streamServers,
fallbackServers: fallbackServers,
streamServerIndex: streamServerIndex,
av1Supported: av1Supported,
dashSupported: dashSupported,
poster: apiResponse.poster,
title: apiResponse.title,
data: data,
subtitleTracks: subtitleTracks,
volume: volume,
muted: muted,
captionsActive: captions,
captionLanguage: 'en',
ambientMode: ambientMode,
isMobile: isMobile(),
quality: parseInt(localStorage.getItem('hstreamQuality')) || 1080,
lastTime: lastTime,
subtitleInstance: subtitleInstance,
onEnded: function () {
playNextPlaylistVideo();
},
onTimeUpdate: function () {
trackWatchTime();
},
onQualityChange: function (size) {
if (dashSupported && !apiResponse.legacy) {
initDash(data);
}
},
onVolumeChange: function () {
localStorage.setItem('hstreamVolume', player.volume.toString());
localStorage.setItem('hstreamMuted', player.muted.toString());
},
onCaptionsToggle: function (active) {
localStorage.setItem('hstreamCaptions', active.toString());
if (subtitleInstance && subtitleInstance.canvas) {
subtitleInstance.canvas.style.visibility = active ? 'visible' : 'hidden';
}
var libassParent = document.querySelector('.libassjs-canvas-parent');
if (libassParent) {
libassParent.style.visibility = active ? 'visible' : 'hidden';
}
},
onLanguageChange: function (lang) {
initSubtitles(lang);
},
onServerChange: function (index) {
streamServerIndex = index;
var allServers = streamServers.concat(fallbackServers);
streamServer = allServers[streamServerIndex];
console.log('Selected Server: ' + streamServer);
if (player) {
clearInterval(saveInterval);
stopEngagementTracking();
player.destroy();
}
initPlayer();
},
});
window.player = player;
if (!isMobile()) {
player.initThumbnails(vttThumbsUrl);
}
if (dashSupported && !apiResponse.legacy) {
initDash(data);
}
initMobileWidescreen(container, videoEl);
initMobileDoubleTap(container, videoEl, player);
var episodeId = document.getElementById('e_id').value;
player.initHeatmap(episodeId);
videoEl.addEventListener('play', function onFirstPlay() {
videoEl.removeEventListener('play', onFirstPlay);
startEngagementTracking(episodeId);
});
setTimeout(function () {
var params = new URLSearchParams(window.location.search);
var 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);
saveInterval = setInterval(function () {
lastTime = player.currentTime;
}, 10000);
}
async function initVidstackPlayer() {
var videoSource = streamServer + '/' + apiResponse.stream_url + '/x264.720p.mp4';
var videoThumbs = streamServer + '/' + apiResponse.stream_url + '/thumbs.vtt';
var videoCaption = streamServer + '/' + apiResponse.stream_url + '/eng.vtt';
player = await VidstackPlayer.create({
target: '#player',
title: apiResponse.title,
src: videoSource,
poster: apiResponse.poster,
layout: new VidstackPlayerLayout({
thumbnails: videoThumbs,
}),
tracks: [
{
src: videoCaption,
label: 'English',
language: 'en-US',
kind: 'subtitles',
type: 'vtt',
default: true,
}
]
});
window.player = player;
player.addEventListener('time-update', function () {
trackWatchTime();
});
}
window.axios.post('/player/api', {
episode_id: document.getElementById('e_id').value
}).then(function (response) {
if (response.status === 200) {
apiResponse = response.data;
streamServers = apiResponse.stream_domains || [];
fallbackServers = apiResponse.asia_stream_domains || [];
const cdnCount = streamServers.length;
if (cdnCount > 0) {
streamServerIndex = Math.floor(Math.random() * cdnCount);
streamServer = streamServers[streamServerIndex];
} else {
const fallbackCount = fallbackServers.length;
streamServerIndex = Math.floor(Math.random() * fallbackCount);
streamServer = fallbackServers[streamServerIndex];
}
streamServerCount = streamServers.length + fallbackServers.length;
console.log('Selected Server: ' + streamServer + ' with Index: ' + streamServerIndex);
if (!isIOS()) {
initPlayer();
} else {
console.log('Detected Apple device. Using Vidstack fallback player.');
initVidstackPlayer();
}
}
}).catch(function (error) {
var alert = document.getElementById('player-alert');
if (alert) {
alert.innerText = 'The player encountered a problem: ' + error;
alert.classList.remove('hidden');
}
});