// Plyr Player import Plyr from 'plyr/dist/plyr.polyfilled.min.js'; import 'plyr/dist/plyr.css'; // Vidstack Player import 'vidstack/player/styles/default/theme.css'; import 'vidstack/player/styles/default/layouts/video.css'; import { VidstackPlayer, VidstackPlayerLayout } from 'vidstack/global/player'; // Dash Support import dashjs from 'dashjs'; // Subtitle Support import SubtitlesOctopus from '@jellyfin/libass-wasm'; // Custom JS 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'; // Variables 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 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 subtitleInstance = null; var controls = [ 'play-large', // The large play button in the center 'play', // Play/pause playback 'progress', // The progress bar and scrubber for playback and buffering 'current-time', // The current time of playback 'duration', // The full duration of the media 'mute', // Toggle mute 'volume', // Volume control 'captions', // Toggle captions 'settings', // Settings menu 'fullscreen', // Toggle fullscreen ]; // Load Volume from LocalStorage if (localStorage.hstreamVolume) { volume = parseFloat(localStorage.getItem('hstreamVolume')).toFixed(2); console.log('Loaded Audio Volume from Local Storage: ' + volume); } // Load Captions from LocalStorage if (localStorage.hstreamCaptions) { captions = (localStorage.getItem('hstreamCaptions') == 'true'); console.log('Loaded Captions Status from Local Storage: ' + captions); } // Asia Server Fallback if (localStorage.hstreamServerFallback) { serverFallback = (localStorage.getItem('hstreamServerFallback') == 'true'); console.log('Loaded Captions Status from Local Storage: ' + captions); } // Alert User when AV1 is not supported 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); // Expose player and dash so they can be used from the console 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); player.destroy(); } initPlayer(); } function initSubtitles(lang) { if (isIOS()) { return; } // Dispose old instance 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'; // Hindi font if (lang == 'hi') { subFont = '/fonts/Hind-SemiBold.ttf'; } // Subtitles var options = { video: document.getElementsByTagName('video')[0], // HTML5 video element subUrl: newSubUrl, // Link to subtitles workerUrl: '/build/js/subtitles-octopus-worker.js', // Link to WebAssembly-based file "libassjs-worker.js" legacyWorkerUrl: '/build/js/subtitles-octopus-worker-legacy.js', // Link to non-WebAssembly worker 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 } }); // Player Track Data 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.captions.languages = ['en']; player.captions.language = 'en'; player.captions.active = captions; if (dashSupported && !apiResponse.legacy) { player.on('qualitychange', () => { initDash(data, player); }); initDash(data, player); } // Ambient Mode 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(); } // Server select (Asia) 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.") } 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); // drawing at 30fps } })(); }); 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()) }); player.on('ended', () => { playNextPlaylistVideo(); }); 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(); // Start time 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); }); // Server Select // I hate this... 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)); } // Event Listeners 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); player.destroy(); } initPlayer(); }); } } // Periodically save last timestamp saveInterval = setInterval(function () { lastTime = player.currentTime; console.log("Last Player Position: " + lastTime); }, 10000); } async function initVidstackPlayer() { const videoSource = streamServer + '/' + apiResponse.stream_url + '/x264.720p.mp4'; const videoThumbs = streamServer + '/' + apiResponse.stream_url + '/thumbs.vtt'; const 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, } ] }); // Ambient Mode let canvas = document.getElementById("ambientVideo"), ctx = canvas.getContext("2d"), video = document.getElementsByTagName('video')[0]; setCanvasDimension(canvas, video); paintStaticVideo(ctx, video); player.addEventListener('play', () => { 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); // drawing at 30fps } })(); }); } // Get Data from API 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; 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); if (!isIOS()) { initPlayer(); } else { console.log("Detected Apple Shit. Using different player.") initVidstackPlayer(); } } }).catch(function (error) { var alert = document.getElementById("player-alert"); alert.innerText = 'The player encountered a problem: ' + error; alert.classList.remove("hidden"); });