[ci skip] Add native HLS playback for VIPLeague/DaddyLive streams (v1.3.1)
- Add HLS proxy (hlsproxy) for rewriting m3u8 playlists and proxying segments with correct Referer/Origin headers (uses ?domain= param) - Add playerconfig service for detecting stream types (VIPLeague, DaddyLive, HLS) and extracting auth params from ksohls pages - Add VIPLeague URL resolution: extract slug from URL path, match against DaddyLive 24/7 channel index with token-based scoring - Replace Clappr with direct HLS.js player for better compatibility - Add CryptoJS CDN for DaddyLive auth module support - Disable CrowdSec on f1-stream ingress to prevent false positives - Bump image to v1.3.1
This commit is contained in:
parent
a5f9c1595f
commit
0ff2aaec60
10 changed files with 1049 additions and 51 deletions
|
|
@ -1340,6 +1340,15 @@ dialog .dialog-cancel:hover {
|
|||
border: none;
|
||||
}
|
||||
|
||||
#clappr-player {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.browser-viewer-content .loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
|
|
|||
|
|
@ -194,6 +194,9 @@
|
|||
|
||||
<!-- Browser Session Viewer (inline, inside main via JS) -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/crypto-js@4/crypto-js.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js"></script>
|
||||
<script src="/static/js/player.js"></script>
|
||||
<script src="/static/js/utils.js"></script>
|
||||
<script src="/static/js/auth.js"></script>
|
||||
<script src="/static/js/streams.js"></script>
|
||||
|
|
|
|||
216
modules/kubernetes/f1-stream/files/static/js/player.js
Normal file
216
modules/kubernetes/f1-stream/files/static/js/player.js
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// player.js — Native HLS player management using HLS.js directly
|
||||
|
||||
var _hlsInstance = null;
|
||||
var _videoElement = null;
|
||||
|
||||
/**
|
||||
* Fetch player config for a stream from the backend.
|
||||
* Returns {type: "hls"|"daddylive"|"proxy", hls_url, auth_token, ...}
|
||||
*/
|
||||
async function getPlayerConfig(streamId) {
|
||||
try {
|
||||
const resp = await fetch('/api/streams/' + streamId + '/player-config');
|
||||
if (!resp.ok) return { type: 'proxy' };
|
||||
return await resp.json();
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch player config:', e);
|
||||
return { type: 'proxy' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a /hls/{b64} URL back to the original upstream URL.
|
||||
*/
|
||||
function decodeHLSURL(proxyURL) {
|
||||
if (!proxyURL || typeof proxyURL !== 'string') return proxyURL;
|
||||
var m = proxyURL.match(/\/hls\/([A-Za-z0-9_-]+)/);
|
||||
if (!m) return proxyURL;
|
||||
try {
|
||||
// base64url decode
|
||||
var b64 = m[1].replace(/-/g, '+').replace(/_/g, '/');
|
||||
// pad
|
||||
while (b64.length % 4 !== 0) b64 += '=';
|
||||
return atob(b64);
|
||||
} catch (e) {
|
||||
return proxyURL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HLS.js player for a plain HLS stream.
|
||||
*/
|
||||
function createHLSPlayer(containerSelector, hlsURL) {
|
||||
destroyNativePlayer();
|
||||
_buildPlayer(containerSelector, hlsURL, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HLS.js player for DaddyLive streams with auth module integration.
|
||||
*/
|
||||
function createDaddyLivePlayer(containerSelector, config) {
|
||||
destroyNativePlayer();
|
||||
|
||||
if (config.auth_mod_url) {
|
||||
_loadAuthModAndPlay(containerSelector, config);
|
||||
} else {
|
||||
_buildPlayer(containerSelector, config.hls_url, {});
|
||||
}
|
||||
}
|
||||
|
||||
function _loadAuthModAndPlay(containerSelector, config) {
|
||||
var script = document.createElement('script');
|
||||
script.src = config.auth_mod_url;
|
||||
script.onload = function () {
|
||||
_createDaddyLivePlayerWithAuth(containerSelector, config);
|
||||
};
|
||||
script.onerror = function () {
|
||||
console.warn('Failed to load auth module, falling back to direct HLS');
|
||||
_buildPlayer(containerSelector, config.hls_url, {});
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function _createDaddyLivePlayerWithAuth(containerSelector, config) {
|
||||
var hlsConfig = {};
|
||||
|
||||
// If EPlayerAuth is available, set up xhr wrapping
|
||||
if (typeof EPlayerAuth !== 'undefined' && typeof EPlayerAuth.init === 'function') {
|
||||
try {
|
||||
EPlayerAuth.init({
|
||||
authToken: config.auth_token,
|
||||
channelKey: config.channel_key,
|
||||
channelSalt: config.channel_salt,
|
||||
timestamp: config.timestamp,
|
||||
serverKey: config.server_key
|
||||
});
|
||||
|
||||
if (typeof EPlayerAuth.getXhrSetup === 'function') {
|
||||
var origSetup = EPlayerAuth.getXhrSetup();
|
||||
hlsConfig.xhrSetup = function (xhr, url) {
|
||||
// Decode the real upstream URL from our /hls/{b64} proxy path
|
||||
var realURL = decodeHLSURL(url);
|
||||
|
||||
// Create interceptor to capture headers the auth module sets
|
||||
var captured = {};
|
||||
var fakeXHR = {
|
||||
setRequestHeader: function (k, v) { captured[k] = v; }
|
||||
};
|
||||
|
||||
try {
|
||||
origSetup(fakeXHR, realURL);
|
||||
} catch (e) {
|
||||
console.warn('Auth xhrSetup error:', e);
|
||||
}
|
||||
|
||||
// Re-set captured headers with forwarding prefix
|
||||
for (var k in captured) {
|
||||
if (captured.hasOwnProperty(k)) {
|
||||
xhr.setRequestHeader('X-Hls-Forward-' + k, captured[k]);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('EPlayerAuth init failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
_buildPlayer(containerSelector, config.hls_url, hlsConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an HLS.js player with a <video> element.
|
||||
*/
|
||||
function _buildPlayer(containerSelector, hlsURL, extraConfig) {
|
||||
var container = document.querySelector(containerSelector);
|
||||
if (!container) return;
|
||||
|
||||
// Create video element
|
||||
var video = document.createElement('video');
|
||||
video.controls = true;
|
||||
video.autoplay = true;
|
||||
video.style.width = '100%';
|
||||
video.style.height = '100%';
|
||||
video.style.backgroundColor = '#000';
|
||||
container.appendChild(video);
|
||||
_videoElement = video;
|
||||
|
||||
if (Hls.isSupported()) {
|
||||
var config = {
|
||||
enableWorker: true,
|
||||
lowLatencyMode: false,
|
||||
maxBufferLength: 30,
|
||||
maxMaxBufferLength: 60
|
||||
};
|
||||
// Merge extra config (e.g. xhrSetup for auth)
|
||||
for (var k in extraConfig) {
|
||||
if (extraConfig.hasOwnProperty(k)) {
|
||||
config[k] = extraConfig[k];
|
||||
}
|
||||
}
|
||||
|
||||
var hls = new Hls(config);
|
||||
hls.loadSource(hlsURL);
|
||||
hls.attachMedia(video);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, function () {
|
||||
video.play().catch(function(e) {
|
||||
console.warn('Autoplay blocked:', e);
|
||||
});
|
||||
});
|
||||
hls.on(Hls.Events.ERROR, function (event, data) {
|
||||
console.error('HLS.js error:', data.type, data.details, data);
|
||||
if (data.fatal) {
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
console.warn('HLS network error, attempting recovery...');
|
||||
hls.startLoad();
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
console.warn('HLS media error, attempting recovery...');
|
||||
hls.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
console.error('HLS fatal error, cannot recover');
|
||||
hls.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
_hlsInstance = hls;
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
// Safari native HLS
|
||||
video.src = hlsURL;
|
||||
video.addEventListener('loadedmetadata', function () {
|
||||
video.play().catch(function(e) {
|
||||
console.warn('Autoplay blocked:', e);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
container.textContent = 'HLS playback is not supported in this browser.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the current native player instance.
|
||||
*/
|
||||
function destroyNativePlayer() {
|
||||
if (_hlsInstance) {
|
||||
try {
|
||||
_hlsInstance.destroy();
|
||||
} catch (e) {
|
||||
console.warn('Error destroying HLS instance:', e);
|
||||
}
|
||||
_hlsInstance = null;
|
||||
}
|
||||
if (_videoElement) {
|
||||
try {
|
||||
_videoElement.pause();
|
||||
_videoElement.removeAttribute('src');
|
||||
_videoElement.load();
|
||||
_videoElement.remove();
|
||||
} catch (e) {
|
||||
console.warn('Error removing video element:', e);
|
||||
}
|
||||
_videoElement = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -317,9 +317,9 @@ function closeRedditViewer() {
|
|||
contentEl.querySelectorAll(':scope > :not(#reddit-viewer-loader)').forEach(el => el.remove());
|
||||
}
|
||||
|
||||
// --- Browser Session Viewer (Iframe Proxy) ---
|
||||
// --- Browser Session Viewer (Iframe Proxy + Native Player) ---
|
||||
|
||||
function openBrowserSession(streamId, streamTitle, streamURL) {
|
||||
async function openBrowserSession(streamId, streamTitle, streamURL) {
|
||||
const viewer = document.getElementById('browser-viewer');
|
||||
const statusEl = viewer.querySelector('.browser-viewer-status');
|
||||
const contentEl = viewer.querySelector('.browser-viewer-content');
|
||||
|
|
@ -331,7 +331,41 @@ function openBrowserSession(streamId, streamTitle, streamURL) {
|
|||
statusEl.classList.remove('connected');
|
||||
loader.classList.remove('hidden');
|
||||
|
||||
// Parse the stream URL to extract origin and path
|
||||
if (urlText) urlText.textContent = streamURL;
|
||||
if (openOriginal) openOriginal.href = streamURL;
|
||||
|
||||
// Hide all tab content sections and show the viewer
|
||||
document.querySelectorAll('.tab-content').forEach(s => s.classList.remove('active'));
|
||||
viewer.classList.remove('hidden');
|
||||
viewer.classList.add('active');
|
||||
|
||||
// Remove any existing iframe or player
|
||||
contentEl.querySelectorAll('.browser-iframe').forEach(el => el.remove());
|
||||
contentEl.querySelectorAll('#clappr-player').forEach(el => el.remove());
|
||||
destroyNativePlayer();
|
||||
|
||||
// Fetch player config to determine stream type
|
||||
const config = await getPlayerConfig(streamId);
|
||||
|
||||
if (config.type === 'hls' || config.type === 'daddylive') {
|
||||
// Native player mode
|
||||
const playerDiv = document.createElement('div');
|
||||
playerDiv.id = 'clappr-player';
|
||||
contentEl.appendChild(playerDiv);
|
||||
|
||||
loader.classList.add('hidden');
|
||||
statusEl.textContent = 'Playing';
|
||||
statusEl.classList.add('connected');
|
||||
|
||||
if (config.type === 'daddylive') {
|
||||
createDaddyLivePlayer('#clappr-player', config);
|
||||
} else {
|
||||
createHLSPlayer('#clappr-player', config.hls_url);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: iframe proxy mode
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(streamURL);
|
||||
|
|
@ -344,25 +378,9 @@ function openBrowserSession(streamId, streamTitle, streamURL) {
|
|||
|
||||
const origin = parsed.origin;
|
||||
const pathAndSearch = parsed.pathname + parsed.search + parsed.hash;
|
||||
|
||||
// Base64-encode the origin (URL-safe, no padding)
|
||||
const b64Origin = btoa(origin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
|
||||
// Build proxy URL
|
||||
const proxyURL = '/proxy/' + b64Origin + pathAndSearch;
|
||||
|
||||
if (urlText) urlText.textContent = streamURL;
|
||||
if (openOriginal) openOriginal.href = streamURL;
|
||||
|
||||
// Hide all tab content sections and show the viewer
|
||||
document.querySelectorAll('.tab-content').forEach(s => s.classList.remove('active'));
|
||||
viewer.classList.remove('hidden');
|
||||
viewer.classList.add('active');
|
||||
|
||||
// Remove any existing iframe
|
||||
contentEl.querySelectorAll('.browser-iframe').forEach(el => el.remove());
|
||||
|
||||
// Create iframe with sandbox to prevent frame-busting and top-navigation
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = proxyURL;
|
||||
iframe.className = 'browser-iframe';
|
||||
|
|
@ -378,11 +396,13 @@ function openBrowserSession(streamId, streamTitle, streamURL) {
|
|||
}
|
||||
|
||||
function closeBrowserSession() {
|
||||
destroyNativePlayer();
|
||||
const viewer = document.getElementById('browser-viewer');
|
||||
viewer.classList.add('hidden');
|
||||
viewer.classList.remove('active');
|
||||
const contentEl = viewer.querySelector('.browser-viewer-content');
|
||||
contentEl.querySelectorAll('.browser-iframe').forEach(el => el.remove());
|
||||
contentEl.querySelectorAll('#clappr-player').forEach(el => el.remove());
|
||||
const statusEl = viewer.querySelector('.browser-viewer-status');
|
||||
statusEl.textContent = '';
|
||||
statusEl.classList.remove('connected');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue