broke up monolithic structure
This commit is contained in:
221
frontend/js/player.js
Normal file
221
frontend/js/player.js
Normal file
@@ -0,0 +1,221 @@
|
||||
window.App = window.App || {};
|
||||
App.player = App.player || {};
|
||||
|
||||
(function() {
|
||||
const state = App.state;
|
||||
|
||||
// Playback heuristics for full-screen behavior on mobile/TV browsers.
|
||||
function isMobilePlayback() {
|
||||
if (navigator.userAgentData && typeof navigator.userAgentData.mobile === 'boolean') {
|
||||
return navigator.userAgentData.mobile;
|
||||
}
|
||||
const ua = navigator.userAgent || '';
|
||||
if (/iPhone|iPad|iPod|Android/i.test(ua)) return true;
|
||||
return window.matchMedia('(pointer: coarse)').matches && window.matchMedia('(max-width: 900px)').matches;
|
||||
}
|
||||
|
||||
function isTvPlayback() {
|
||||
const ua = navigator.userAgent || '';
|
||||
return /SMART-TV|SmartTV|Smart TV|Internet\.TV|HbbTV|NetCast|Web0S|webOS|Tizen|AppleTV|Apple TV|GoogleTV|Android TV|AFTB|AFTS|AFTM|AFTT|AFTQ|AFTK|AFTN|AFTMM|AFTKR|Roku|DTV|BRAVIA|VIZIO|SHIELD|PhilipsTV|Hisense|VIDAA|TOSHIBA/i.test(ua);
|
||||
}
|
||||
|
||||
function getMobileVideoHost() {
|
||||
let host = document.getElementById('mobile-video-host');
|
||||
if (!host) {
|
||||
host = document.createElement('div');
|
||||
host.id = 'mobile-video-host';
|
||||
document.body.appendChild(host);
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
App.player.open = async function(url) {
|
||||
const modal = document.getElementById('video-modal');
|
||||
const video = document.getElementById('player');
|
||||
if (!modal || !video) return;
|
||||
const useMobileFullscreen = isMobilePlayback() || isTvPlayback();
|
||||
let playbackStarted = false;
|
||||
|
||||
if (!state.playerHome) {
|
||||
state.playerHome = video.parentElement;
|
||||
}
|
||||
|
||||
// Normalize stream URL + optional referer forwarding.
|
||||
let refererParam = '';
|
||||
try {
|
||||
const origin = new URL(url).origin;
|
||||
refererParam = `&referer=${encodeURIComponent(origin + '/')}`;
|
||||
} catch (err) {
|
||||
refererParam = '';
|
||||
}
|
||||
const streamUrl = `/api/stream?url=${encodeURIComponent(url)}${refererParam}`;
|
||||
let isHls = /\.m3u8($|\?)/i.test(url);
|
||||
|
||||
// Cleanup existing player instance to prevent aborted bindings.
|
||||
if (state.hlsPlayer) {
|
||||
state.hlsPlayer.stopLoad();
|
||||
state.hlsPlayer.detachMedia();
|
||||
state.hlsPlayer.destroy();
|
||||
state.hlsPlayer = null;
|
||||
}
|
||||
|
||||
// Reset the video element before re-binding a new source.
|
||||
video.pause();
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
|
||||
if (!isHls) {
|
||||
try {
|
||||
const headResp = await fetch(streamUrl, { method: 'HEAD' });
|
||||
const contentType = headResp.headers.get('Content-Type') || '';
|
||||
if (contentType.includes('application/vnd.apple.mpegurl')) {
|
||||
isHls = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to detect stream type', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (useMobileFullscreen) {
|
||||
const host = getMobileVideoHost();
|
||||
if (video.parentElement !== host) {
|
||||
host.appendChild(video);
|
||||
}
|
||||
state.playerMode = 'mobile';
|
||||
video.removeAttribute('playsinline');
|
||||
video.removeAttribute('webkit-playsinline');
|
||||
video.playsInline = false;
|
||||
} else {
|
||||
if (state.playerHome && video.parentElement !== state.playerHome) {
|
||||
state.playerHome.appendChild(video);
|
||||
}
|
||||
state.playerMode = 'modal';
|
||||
video.setAttribute('playsinline', '');
|
||||
video.setAttribute('webkit-playsinline', '');
|
||||
video.playsInline = true;
|
||||
}
|
||||
|
||||
const requestFullscreen = () => {
|
||||
if (state.playerMode !== 'mobile') return;
|
||||
if (typeof video.webkitEnterFullscreen === 'function') {
|
||||
try {
|
||||
video.webkitEnterFullscreen();
|
||||
} catch (err) {
|
||||
// Ignore if fullscreen is not allowed.
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (video.requestFullscreen) {
|
||||
video.requestFullscreen().catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const startPlayback = () => {
|
||||
if (playbackStarted) return;
|
||||
playbackStarted = true;
|
||||
const playPromise = video.play();
|
||||
if (playPromise && typeof playPromise.catch === 'function') {
|
||||
playPromise.catch(() => {});
|
||||
}
|
||||
if (state.playerMode === 'mobile') {
|
||||
if (video.readyState >= 1) {
|
||||
requestFullscreen();
|
||||
} else {
|
||||
video.addEventListener('loadedmetadata', requestFullscreen, { once: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isHls) {
|
||||
if (window.Hls && window.Hls.isSupported()) {
|
||||
state.hlsPlayer = new window.Hls();
|
||||
state.hlsPlayer.loadSource(streamUrl);
|
||||
state.hlsPlayer.attachMedia(video);
|
||||
state.hlsPlayer.on(window.Hls.Events.MANIFEST_PARSED, function() {
|
||||
startPlayback();
|
||||
});
|
||||
startPlayback();
|
||||
state.hlsPlayer.on(window.Hls.Events.ERROR, function(event, data) {
|
||||
if (data && data.fatal) {
|
||||
if (App.ui && App.ui.showError) {
|
||||
App.ui.showError('Unable to play this stream.');
|
||||
}
|
||||
App.player.close();
|
||||
}
|
||||
});
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = streamUrl;
|
||||
startPlayback();
|
||||
} else {
|
||||
console.error("HLS not supported in this browser.");
|
||||
if (App.ui && App.ui.showError) {
|
||||
App.ui.showError('HLS is not supported in this browser.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
video.src = streamUrl;
|
||||
startPlayback();
|
||||
}
|
||||
|
||||
video.onerror = () => {
|
||||
if (App.ui && App.ui.showError) {
|
||||
App.ui.showError('Video failed to load.');
|
||||
}
|
||||
App.player.close();
|
||||
};
|
||||
|
||||
if (state.playerMode === 'modal') {
|
||||
modal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
if (!state.onFullscreenChange) {
|
||||
state.onFullscreenChange = () => {
|
||||
if (state.playerMode === 'mobile' && !document.fullscreenElement) {
|
||||
App.player.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
document.addEventListener('fullscreenchange', state.onFullscreenChange);
|
||||
if (!state.onWebkitEndFullscreen) {
|
||||
state.onWebkitEndFullscreen = () => {
|
||||
if (state.playerMode === 'mobile') {
|
||||
App.player.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
video.addEventListener('webkitendfullscreen', state.onWebkitEndFullscreen);
|
||||
}
|
||||
};
|
||||
|
||||
App.player.close = function() {
|
||||
const modal = document.getElementById('video-modal');
|
||||
const video = document.getElementById('player');
|
||||
if (!modal || !video) return;
|
||||
|
||||
if (state.hlsPlayer) {
|
||||
state.hlsPlayer.destroy();
|
||||
state.hlsPlayer = null;
|
||||
}
|
||||
if (document.fullscreenElement && document.exitFullscreen) {
|
||||
document.exitFullscreen().catch(() => {});
|
||||
}
|
||||
if (state.onFullscreenChange) {
|
||||
document.removeEventListener('fullscreenchange', state.onFullscreenChange);
|
||||
}
|
||||
if (state.onWebkitEndFullscreen) {
|
||||
video.removeEventListener('webkitendfullscreen', state.onWebkitEndFullscreen);
|
||||
}
|
||||
video.onerror = null;
|
||||
video.pause();
|
||||
video.src = '';
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
if (state.playerHome && video.parentElement !== state.playerHome) {
|
||||
state.playerHome.appendChild(video);
|
||||
}
|
||||
state.playerMode = 'modal';
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user