317 lines
10 KiB
JavaScript
317 lines
10 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.setPlayerPreference = function(pref) {
|
|
localStorage.setItem('hstreamPlayerPreference', pref);
|
|
window.location.reload();
|
|
};
|
|
|
|
const playerPreference = localStorage.getItem('hstreamPlayerPreference') || 'hstream';
|
|
|
|
if (playerPreference === 'plyr' && !isIOS()) {
|
|
import('./player-plyr.js').then(m => m.initPlyrPlayer(document.getElementById('e_id').value));
|
|
} else {
|
|
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');
|
|
}
|
|
});
|
|
}
|