// 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 = 'Ambient ModeOff'; } else { ambientMode = true; localStorage.ambientMode = 'true'; setCanvasDimension(canvas, video); paintStaticVideo(ctx, video); document.getElementById('ambient-mode-toggle').innerHTML = 'Ambient ModeOn'; } } function toggleAsiaServer() { if (serverFallback) { serverFallback = false; localStorage.hstreamServerFallback = 'false'; document.getElementById('server-fallback-toggle').innerHTML = 'Fallback ServerOff'; streamServers = apiResponse.stream_domains; } else { serverFallback = true; localStorage.hstreamServerFallback = 'true'; document.getElementById('server-fallback-toggle').innerHTML = 'Fallback ServerOn'; 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', ''); 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', ''); 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; }