Compare commits

...

4 Commits

Author SHA1 Message Date
w33b f7c37df755 Show subtitles toggle & menu 2026-07-29 22:12:51 +02:00
w33b 6a9d3b25bf Add styling to plyr and fix switcher not hiding 2026-07-28 21:54:50 +02:00
w33b 75b98de746 Add plyr again as fallback 2026-07-28 21:41:30 +02:00
w33b eaf48276f0 Update player setting design 2026-07-28 20:33:20 +02:00
8 changed files with 745 additions and 60 deletions
+10 -10
View File
@@ -13,7 +13,7 @@
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"dashjs": "^5.0.0", "dashjs": "^5.0.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"plyr": "^3.7.8", "plyr": "^3.8.4",
"tw-elements": "^1.1.0", "tw-elements": "^1.1.0",
"vidstack": "^1.12.13" "vidstack": "^1.12.13"
}, },
@@ -3132,6 +3132,15 @@
"postcss": "^8.0.9" "postcss": "^8.0.9"
} }
}, },
"node_modules/tw-elements/node_modules/yaml": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/unplugin": { "node_modules/unplugin": {
"version": "1.16.1", "version": "1.16.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
@@ -3349,15 +3358,6 @@
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"license": "MIT" "license": "MIT"
},
"node_modules/yaml": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
} }
} }
} }
+1 -1
View File
@@ -25,7 +25,7 @@
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"dashjs": "^5.0.0", "dashjs": "^5.0.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"plyr": "^3.7.8", "plyr": "^3.8.4",
"tw-elements": "^1.1.0", "tw-elements": "^1.1.0",
"vidstack": "^1.12.13" "vidstack": "^1.12.13"
} }
+156 -6
View File
@@ -190,7 +190,7 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 100%; bottom: 100%;
height: 48px; height: 32px;
pointer-events: none; pointer-events: none;
z-index: 0; z-index: 0;
} }
@@ -439,13 +439,12 @@
.hstream-player__menu-container { .hstream-player__menu-container {
position: absolute; position: absolute;
right: 12px; right: 12px;
bottom: 100%; bottom: 80px;
margin-bottom: 2px;
min-width: 220px; min-width: 220px;
max-width: 280px; max-width: 280px;
background: rgba(20, 20, 20, 0.95); background: rgba(20, 20, 20, 0.70);
backdrop-filter: blur(16px); backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
@@ -646,6 +645,88 @@
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
/* ---- Player Switcher Toggle ---- */
.player-switcher {
position: absolute;
top: 12px;
right: 12px;
z-index: 100;
display: flex;
align-items: center;
gap: 8px;
background: rgba(10, 10, 10, 0.65);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 9999px;
padding: 6px 4px 6px 14px;
transition: all 0.3s ease;
opacity: 0;
pointer-events: none;
}
@media (hover: hover) {
.aspect-\[16\/9\]:hover .player-switcher {
opacity: 1;
pointer-events: auto;
}
}
.player-switcher--visible {
opacity: 1;
pointer-events: auto;
}
.player-switcher__label {
color: rgba(255, 255, 255, 0.6);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.player-switcher__btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 14px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 9999px;
color: rgba(255, 255, 255, 0.85);
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
font-family: 'Figtree', sans-serif;
white-space: nowrap;
}
.player-switcher__btn:hover {
background: rgba(198, 30, 84, 0.2);
border-color: rgba(198, 30, 84, 0.4);
color: #fff;
}
.player-switcher__btn i {
font-size: 11px;
}
.player-switcher__btn--active {
background: rgba(198, 30, 84, 0.25);
border-color: rgba(198, 30, 84, 0.5);
color: #fff;
}
.player-switcher__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #c61e54;
box-shadow: 0 0 6px rgba(198, 30, 84, 0.6);
}
/* ---- Fullscreen (remove rounded borders) ---- */ /* ---- Fullscreen (remove rounded borders) ---- */
.hstream-player:fullscreen, .hstream-player:fullscreen,
.hstream-player:-webkit-full-screen { .hstream-player:-webkit-full-screen {
@@ -794,3 +875,72 @@
height: 36px; height: 36px;
} }
} }
/* ============================================================
Plyr Player Overrides pink accent, rounded corners, skip overlay
============================================================ */
.plyr--full-ui input[type='range'] {
color: var(--plyr-range-fill-background, var(--plyr-color-main, var(--plyr-color-main, #c61e54))) !important;
}
.plyr__control--overlaid {
background: var(--plyr-video-control-background-hover, var(--plyr-color-main, var(--plyr-color-main, #c61e54))) !important;
}
.plyr--video .plyr__control.plyr__tab-focus,
.plyr--video .plyr__control:hover,
.plyr--video .plyr__control[aria-expanded='true'] {
background: var(--plyr-video-control-background-hover, var(--plyr-color-main, var(--plyr-color-main, #c61e54))) !important;
}
.plyr__menu__container .plyr__control[role='menuitemradio'][aria-checked='true']::before {
background: var(--plyr-control-toggle-checked-background, var(--plyr-color-main, var(--plyr-color-main, #c61e54))) !important;
}
.plyr--full-ui {
border-radius: 15px;
}
/* Plyr Engagement Heatmap, overlays the progress bar track */
.plyr__progress__heatmap {
position: absolute;
left: -6px;
right: -6px;
bottom: 40%;
height: 48px;
pointer-events: none;
z-index: 0;
}
.plyr__progress__heatmap-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
/* Plyr Mobile Double-tap Skip Overlay */
#plyr__time_skip {
background: #c61e54;
border: 0;
border-radius: 50%;
color: #fff;
left: 50%;
min-width: 60px;
width: min-content;
max-width: 100px;
max-height: 90px;
opacity: 0;
display: table-cell;
text-align: center;
vertical-align: middle;
transform: translate(-50%, -50%);
padding-top: 15px;
padding-bottom: 15px;
position: absolute;
top: 50%;
transition: 1s;
z-index: 3;
pointer-events: none;
box-shadow: 0px 0px 45px #000000;
}
+436
View File
@@ -0,0 +1,436 @@
// 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 = '<span>Ambient Mode<span class="plyr__menu__value">Off</span></span>';
} else {
ambientMode = true;
localStorage.ambientMode = 'true';
setCanvasDimension(canvas, video);
paintStaticVideo(ctx, video);
document.getElementById('ambient-mode-toggle').innerHTML = '<span>Ambient Mode<span class="plyr__menu__value">On</span></span>';
}
}
function toggleAsiaServer() {
if (serverFallback) {
serverFallback = false;
localStorage.hstreamServerFallback = 'false';
document.getElementById('server-fallback-toggle').innerHTML = '<span>Fallback Server<span class="plyr__menu__value">Off</span></span>';
streamServers = apiResponse.stream_domains;
} else {
serverFallback = true;
localStorage.hstreamServerFallback = 'true';
document.getElementById('server-fallback-toggle').innerHTML = '<span>Fallback Server<span class="plyr__menu__value">On</span></span>';
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', '<button id="ambient-mode-toggle" type="button" class="plyr__control" role="menuitem" aria-haspopup="true"><span>Ambient Mode<span class="plyr__menu__value">On</span></span></button>');
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', '<button id="server-fallback-toggle" type="button" class="plyr__control" role="menuitem" aria-haspopup="true"><span>Fallback Server<span class="plyr__menu__value">' + value + '</span></span></button>');
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;
}
+51 -32
View File
@@ -181,6 +181,9 @@ function initPlayer() {
}, },
onLanguageChange: function (lang) { onLanguageChange: function (lang) {
initSubtitles(lang); initSubtitles(lang);
if (player) {
player.setSubtitleInstance(subtitleInstance);
}
}, },
onServerChange: function (index) { onServerChange: function (index) {
streamServerIndex = index; streamServerIndex = index;
@@ -199,6 +202,11 @@ function initPlayer() {
window.player = player; window.player = player;
if (player.captionsActive) {
initSubtitles(player.captionLanguage);
player.setSubtitleInstance(subtitleInstance);
}
if (!isMobile()) { if (!isMobile()) {
player.initThumbnails(vttThumbsUrl); player.initThumbnails(vttThumbsUrl);
} }
@@ -268,38 +276,49 @@ async function initVidstackPlayer() {
}); });
} }
window.axios.post('/player/api', { window.setPlayerPreference = function(pref) {
episode_id: document.getElementById('e_id').value localStorage.setItem('hstreamPlayerPreference', pref);
}).then(function (response) { window.location.reload();
if (response.status === 200) { };
apiResponse = response.data;
streamServers = apiResponse.stream_domains || [];
fallbackServers = apiResponse.asia_stream_domains || [];
const cdnCount = streamServers.length; const playerPreference = localStorage.getItem('hstreamPlayerPreference') || 'hstream';
if (cdnCount > 0) {
streamServerIndex = Math.floor(Math.random() * cdnCount); if (playerPreference === 'plyr' && !isIOS()) {
streamServer = streamServers[streamServerIndex]; import('./player-plyr.js').then(m => m.initPlyrPlayer(document.getElementById('e_id').value));
} else { } else {
const fallbackCount = fallbackServers.length; window.axios.post('/player/api', {
streamServerIndex = Math.floor(Math.random() * fallbackCount); episode_id: document.getElementById('e_id').value
streamServer = fallbackServers[streamServerIndex]; }).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) {
streamServerCount = streamServers.length + fallbackServers.length; var alert = document.getElementById('player-alert');
console.log('Selected Server: ' + streamServer + ' with Index: ' + streamServerIndex); if (alert) {
alert.innerText = 'The player encountered a problem: ' + error;
if (!isIOS()) { alert.classList.remove('hidden');
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');
}
});
+54 -11
View File
@@ -235,7 +235,7 @@ export class HStreamPlayer {
// Build settings menu // Build settings menu
this._buildSettingsMenu(); this._buildSettingsMenu();
this._controlsEl.appendChild(this._menuContainer); this.container.appendChild(this._menuContainer);
} }
_createButton(action, iconClass, label) { _createButton(action, iconClass, label) {
@@ -272,7 +272,7 @@ export class HStreamPlayer {
mainPanel.appendChild(addItem('Quality', this._qualityLabel(this._quality), 'quality')); mainPanel.appendChild(addItem('Quality', this._qualityLabel(this._quality), 'quality'));
mainPanel.appendChild(addItem('Playback Speed', this._speedLabel(this.video.playbackRate), 'speed')); mainPanel.appendChild(addItem('Playback Speed', this._speedLabel(this.video.playbackRate), 'speed'));
if (this._subtitleTracks.length > 1) { if (this._subtitleTracks.length >= 1) {
mainPanel.appendChild(addItem('Subtitles', this._captionLabel(), 'captions')); mainPanel.appendChild(addItem('Subtitles', this._captionLabel(), 'captions'));
} }
@@ -436,6 +436,7 @@ export class HStreamPlayer {
} }
item.type = 'button'; item.type = 'button';
item.setAttribute('role', 'menuitemradio'); item.setAttribute('role', 'menuitemradio');
item.setAttribute('data-srclang', track.srclang);
item.innerHTML = `<span>${track.label}</span><span class="hstream-player__menu-item-radio"></span>`; item.innerHTML = `<span>${track.label}</span><span class="hstream-player__menu-item-radio"></span>`;
item.addEventListener('click', (e) => { item.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -953,21 +954,21 @@ export class HStreamPlayer {
toggleCaptions() { toggleCaptions() {
this._captionsActive = !this._captionsActive; this._captionsActive = !this._captionsActive;
if (this.subtitleInstance && this.subtitleInstance.canvas) {
this.subtitleInstance.canvas.style.visibility = this._captionsActive ? 'visible' : 'hidden';
}
const libassParent = document.querySelector('.libassjs-canvas-parent');
if (libassParent) {
libassParent.style.visibility = this._captionsActive ? 'visible' : 'hidden';
}
if (this._captionsActive) { if (this._captionsActive) {
this._captionsBtn.classList.add('hstream-player__button--active'); this._captionsBtn.classList.add('hstream-player__button--active');
} else { } else {
this._captionsBtn.classList.remove('hstream-player__button--active'); this._captionsBtn.classList.remove('hstream-player__button--active');
} }
var libassParent = document.querySelector('.libassjs-canvas-parent');
if (libassParent) {
libassParent.style.visibility = this._captionsActive ? 'visible' : 'hidden';
} else if (this._captionsActive && this._onLanguageChange) {
this._onLanguageChange(this._captionLanguage);
}
localStorage.setItem('hstreamCaptions', this._captionsActive.toString()); localStorage.setItem('hstreamCaptions', this._captionsActive.toString());
this._updateCaptionsPanel();
if (this._onCaptionsToggle) { if (this._onCaptionsToggle) {
this._onCaptionsToggle(this._captionsActive); this._onCaptionsToggle(this._captionsActive);
} }
@@ -983,6 +984,7 @@ export class HStreamPlayer {
if (this._onLanguageChange) { if (this._onLanguageChange) {
this._onLanguageChange(lang); this._onLanguageChange(lang);
} }
this._updateCaptionsPanel();
} }
get captionLanguage() { get captionLanguage() {
@@ -1062,6 +1064,43 @@ export class HStreamPlayer {
} }
} }
_updateCaptionsPanel() {
var captionsPanel = this._menuContainer.querySelector('[data-panel="captions"]');
if (!captionsPanel) return;
captionsPanel.querySelectorAll('.hstream-player__menu-item').forEach(function (item) {
item.classList.remove('hstream-player__menu-item--checked');
item.setAttribute('aria-checked', 'false');
});
if (!this._captionsActive) {
var offItem = captionsPanel.querySelector('.hstream-player__menu-item:not([data-srclang])');
if (offItem) {
offItem.classList.add('hstream-player__menu-item--checked');
offItem.setAttribute('aria-checked', 'true');
}
} else {
var langItem = captionsPanel.querySelector('.hstream-player__menu-item[data-srclang="' + this._captionLanguage + '"]');
if (langItem) {
langItem.classList.add('hstream-player__menu-item--checked');
langItem.setAttribute('aria-checked', 'true');
}
}
var mainPanel = this._menuContainer.querySelector('[data-panel="main"]');
if (mainPanel) {
var items = mainPanel.querySelectorAll('.hstream-player__menu-item');
for (var i = 0; i < items.length; i++) {
var text = items[i].textContent.trim();
if (text.startsWith('Subtitles')) {
var valueEl = items[i].querySelector('.hstream-player__menu-value');
if (valueEl) valueEl.textContent = this._captionLabel();
break;
}
}
}
}
get quality() { get quality() {
return this._quality; return this._quality;
} }
@@ -1070,6 +1109,10 @@ export class HStreamPlayer {
return !!document.fullscreenElement; return !!document.fullscreenElement;
} }
setSubtitleInstance(instance) {
this.subtitleInstance = instance;
}
toggleAmbientMode() { toggleAmbientMode() {
this._ambientMode = !this._ambientMode; this._ambientMode = !this._ambientMode;
localStorage.setItem('ambientMode', this._ambientMode.toString()); localStorage.setItem('ambientMode', this._ambientMode.toString());
@@ -1202,7 +1245,7 @@ export class HStreamPlayer {
if (this._captionsActive) { if (this._captionsActive) {
this._captionsBtn.classList.add('hstream-player__button--active'); this._captionsBtn.classList.add('hstream-player__button--active');
} }
if (this._subtitleTracks.length > 1) { if (this._subtitleTracks.length >= 1) {
this._captionsBtn.style.display = ''; this._captionsBtn.style.display = '';
} }
@@ -8,6 +8,42 @@
@endif @endif
<canvas id="ambientVideo" class="decoy"></canvas> <canvas id="ambientVideo" class="decoy"></canvas>
<div class="relative w-full aspect-[16/9]"> <div class="relative w-full aspect-[16/9]">
<div class="player-switcher" id="player-switcher">
<span class="player-switcher__label">Player</span>
<button type="button" class="player-switcher__btn" id="plyr-toggle-btn"
onclick="window.setPlayerPreference('plyr')" title="Switch to Plyr player">
<span class="player-switcher__dot"></span> Plyr
</button>
<button type="button" class="player-switcher__btn player-switcher__btn--active" id="hstream-toggle-btn"
onclick="window.setPlayerPreference('hstream')" title="Switch to HStream player">
<span class="player-switcher__dot"></span> HStream
</button>
</div>
<video id="player" playsinline crossorigin class="absolute inset-0 w-full h-full"></video> <video id="player" playsinline crossorigin class="absolute inset-0 w-full h-full"></video>
</div> </div>
<script>
(function() {
var pref = localStorage.getItem('hstreamPlayerPreference') || 'hstream';
var hstreamBtn = document.getElementById('hstream-toggle-btn');
var plyrBtn = document.getElementById('plyr-toggle-btn');
if (pref === 'plyr') {
hstreamBtn.classList.remove('player-switcher__btn--active');
plyrBtn.classList.add('player-switcher__btn--active');
}
var switcher = document.getElementById('player-switcher');
var container = switcher.parentElement;
var hideTimer = null;
function showSwitcher() {
switcher.classList.add('player-switcher--visible');
clearTimeout(hideTimer);
hideTimer = setTimeout(function () {
switcher.classList.remove('player-switcher--visible');
}, 3000);
}
container.addEventListener('pointerdown', showSwitcher);
})();
</script>
</div> </div>
+1
View File
@@ -13,6 +13,7 @@ export default defineConfig({
'resources/js/modals-playlist.js', 'resources/js/modals-playlist.js',
'resources/js/theme.js', 'resources/js/theme.js',
'resources/js/player.js', 'resources/js/player.js',
'resources/js/player-plyr.js',
'resources/js/playlist.js', 'resources/js/playlist.js',
'resources/js/upload.js', 'resources/js/upload.js',
'resources/js/user-blacklist.js', 'resources/js/user-blacklist.js',