// 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'); } }); }