This commit is contained in:
+4228
File diff suppressed because it is too large
Load Diff
+1070
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,707 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>播放器</title>
|
||||
<script src="/vendor/hls.min.js"></script>
|
||||
<script src="/vendor/flv.min.js"></script>
|
||||
<script src="/vendor/dash.all.min.js"></script>
|
||||
<script src="/vendor/artplayer.js"></script>
|
||||
<script src="/vendor/artplayer-plugin-hls-control.js"></script>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif;
|
||||
}
|
||||
|
||||
.artplayer-app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 0 auto;
|
||||
border: 4px solid #6b7280;
|
||||
border-top-color: #4f46e5;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
width: min(100% - 36px, 460px);
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-container.waiting-live {
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
border-radius: 8px;
|
||||
background: rgba(17, 24, 39, 0.72);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.36);
|
||||
}
|
||||
|
||||
.loading-container.waiting-live .loader {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#msg {
|
||||
margin: 16px 0 0;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.loading-container.waiting-live #msg {
|
||||
margin-top: 0;
|
||||
font-size: 1.08rem;
|
||||
}
|
||||
|
||||
.sub-msg {
|
||||
margin: 8px 0 0;
|
||||
color: #aeb8c8;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.password-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.password-card {
|
||||
width: min(100%, 380px);
|
||||
border-radius: 8px;
|
||||
padding: 28px;
|
||||
background: #1f2937;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.password-card h2 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.password-card input,
|
||||
.password-card button {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.password-card input {
|
||||
margin-bottom: 14px;
|
||||
color: #fff;
|
||||
background: #374151;
|
||||
outline: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.password-card button {
|
||||
color: #fff;
|
||||
background: #4f46e5;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 12px 0 0;
|
||||
color: #ef4444;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="loading-container" class="loading-container">
|
||||
<div class="loader"></div>
|
||||
<p id="msg"></p>
|
||||
<p id="sub-msg" class="sub-msg"></p>
|
||||
</div>
|
||||
|
||||
<div id="player-container" class="artplayer-app hidden"></div>
|
||||
|
||||
<div id="password-modal" class="password-modal hidden">
|
||||
<div class="password-card">
|
||||
<h2 id="pwd-title"></h2>
|
||||
<form id="password-form">
|
||||
<input type="password" id="pwd-in" required>
|
||||
<button type="submit" id="pwd-submit"></button>
|
||||
<p id="pwd-err" class="error hidden"></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Minimal i18n: read language preference set by the main site (index.html / admin.html).
|
||||
const PLAYER_I18N = {
|
||||
zh: {
|
||||
loading: '正在加载...',
|
||||
pwd_title: '需要密码',
|
||||
pwd_placeholder: '请输入密码',
|
||||
pwd_submit: '确 定',
|
||||
pwd_error: '密码错误',
|
||||
invalid_id: '无效的直播 ID',
|
||||
check_failed: '检测失败',
|
||||
no_stream: '没有监测到推流信息',
|
||||
no_sources: '暂无播放源',
|
||||
checking: '正在检测推流信息...',
|
||||
checking_sub: '检测到推流后会自动进入播放器。',
|
||||
waiting_sub: '正在自动检测,推流开始后会自动进入播放器。',
|
||||
resuming_sub: '正在自动检测,推流恢复后会自动进入播放器。',
|
||||
flv_unsupported: '当前浏览器不支持 FLV 播放',
|
||||
hls_unsupported: '当前浏览器不支持 HLS 播放',
|
||||
dash_unavailable: 'DASH 播放组件未加载',
|
||||
quality: '画质',
|
||||
// API error codes from server
|
||||
'err.stream_not_found': '直播不存在',
|
||||
'err.stream_disabled': '直播已关闭',
|
||||
'err.stream_not_found_or_disabled':'直播不存在或已关闭',
|
||||
'err.auth_incorrect_password': '密码错误',
|
||||
'err.missing_session_id': '缺少会话 ID',
|
||||
'err.viewer_session_not_found': '观看会话不存在',
|
||||
'err.auth_required': '未登录或会话已过期',
|
||||
'err.server_error': '服务器内部错误',
|
||||
},
|
||||
en: {
|
||||
loading: 'Loading...',
|
||||
pwd_title: 'Password required',
|
||||
pwd_placeholder: 'Enter password',
|
||||
pwd_submit: 'OK',
|
||||
pwd_error: 'Incorrect password',
|
||||
invalid_id: 'Invalid stream ID',
|
||||
check_failed: 'Check failed',
|
||||
no_stream: 'No stream detected',
|
||||
no_sources: 'No playback sources',
|
||||
checking: 'Checking for stream...',
|
||||
checking_sub: 'The player will start automatically once a stream is detected.',
|
||||
waiting_sub: 'Auto-checking. The player will start when the stream begins.',
|
||||
resuming_sub: 'Auto-checking. The player will resume when the stream recovers.',
|
||||
flv_unsupported: 'FLV playback is not supported in this browser',
|
||||
hls_unsupported: 'HLS playback is not supported in this browser',
|
||||
dash_unavailable: 'DASH player component not loaded',
|
||||
quality: 'Quality',
|
||||
// API error codes from server
|
||||
'err.stream_not_found': 'Stream not found',
|
||||
'err.stream_disabled': 'Stream is disabled',
|
||||
'err.stream_not_found_or_disabled':'Stream not found or disabled',
|
||||
'err.auth_incorrect_password': 'Incorrect password',
|
||||
'err.missing_session_id': 'Missing session ID',
|
||||
'err.viewer_session_not_found': 'Viewer session not found',
|
||||
'err.auth_required': 'Not authenticated or session expired',
|
||||
'err.server_error': 'Internal server error',
|
||||
},
|
||||
};
|
||||
const PLAYER_LANG = (localStorage.getItem('lang_pref') || 'zh') === 'en' ? 'en' : 'zh';
|
||||
const t = key => (PLAYER_I18N[PLAYER_LANG] || PLAYER_I18N.zh)[key] || key;
|
||||
|
||||
// Initialise static text in the HTML that cannot use data-i18n.
|
||||
document.getElementById('msg').textContent = t('loading');
|
||||
document.getElementById('pwd-title').textContent = t('pwd_title');
|
||||
document.getElementById('pwd-in').placeholder = t('pwd_placeholder');
|
||||
document.getElementById('pwd-submit').textContent = t('pwd_submit');
|
||||
document.getElementById('pwd-err').textContent = t('pwd_error');
|
||||
|
||||
// Set initial page title from cached site settings (same cache key as index.html).
|
||||
try {
|
||||
const cached = JSON.parse(localStorage.getItem('site_settings_cache') || 'null');
|
||||
if (cached && cached.site_title) document.title = cached.site_title;
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
function hexToArrayBuffer(hex) {
|
||||
hex = hex.trim();
|
||||
if (hex.length !== 32) throw new Error("AES key must be 16 bytes (32 hex chars)");
|
||||
const arr = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
arr[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||||
}
|
||||
return arr.buffer;
|
||||
}
|
||||
|
||||
function normalizeHexKey(hex) {
|
||||
return String(hex || '').trim().replace(/^0x/i, '').replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
function decodeProxyTarget(url) {
|
||||
try {
|
||||
const parsed = new URL(url, window.location.href);
|
||||
const parts = parsed.pathname.split('/');
|
||||
if (parts.length < 5 || parts[1] !== 'proxy' || parts[2] !== 'hls') return url;
|
||||
const encoded = parts[4].replace(/-/g, '+').replace(/_/g, '/');
|
||||
const padded = encoded + '='.repeat((4 - encoded.length % 4) % 4);
|
||||
return decodeURIComponent(escape(atob(padded)));
|
||||
} catch (e) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function parseHlsKeyOverride(input) {
|
||||
const text = String(input || '').trim();
|
||||
if (!text) return null;
|
||||
const direct = normalizeHexKey(text);
|
||||
if (/^[0-9a-fA-F]{32}$/.test(direct)) return { defaultKey: direct, rules: [] };
|
||||
const rules = [];
|
||||
text.split(/\n+/).forEach(line => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return;
|
||||
const parts = trimmed.split(/[:=]/);
|
||||
if (parts.length < 2) return;
|
||||
const pattern = parts.shift().trim();
|
||||
const key = normalizeHexKey(parts.join(':'));
|
||||
if (pattern && /^[0-9a-fA-F]{32}$/.test(key)) rules.push({ pattern, key });
|
||||
});
|
||||
return rules.length ? { defaultKey: '', rules } : null;
|
||||
}
|
||||
|
||||
function selectHlsKeyOverride(config, requestUrl) {
|
||||
if (!config) return '';
|
||||
const targetUrl = decodeProxyTarget(requestUrl || '');
|
||||
const rule = config.rules.find(item => targetUrl.includes(item.pattern) || String(requestUrl || '').includes(item.pattern));
|
||||
return rule ? rule.key : config.defaultKey;
|
||||
}
|
||||
|
||||
function hexToBase64Url(hex) {
|
||||
const clean = hex.trim().replace(/^0x/i, '');
|
||||
if (!/^[0-9a-fA-F]+$/.test(clean) || clean.length % 2 !== 0) return hex.trim();
|
||||
let binary = '';
|
||||
for (let i = 0; i < clean.length; i += 2) {
|
||||
binary += String.fromCharCode(parseInt(clean.slice(i, i + 2), 16));
|
||||
}
|
||||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
function normalizeClearKeyValue(value) {
|
||||
const text = String(value || '').trim();
|
||||
return /^[0-9a-fA-F]{32}$/.test(text.replace(/^0x/i, '')) ? hexToBase64Url(text) : text;
|
||||
}
|
||||
|
||||
function buildClearKeyProtectionData(input) {
|
||||
if (!input) return null;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(input);
|
||||
} catch (e) {
|
||||
parsed = input.split(/[\n,]+/).reduce((acc, item) => {
|
||||
const parts = item.split(/[:=]/);
|
||||
if (parts.length >= 2) acc[parts[0].trim()] = parts.slice(1).join(':').trim();
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
const source = parsed.clearkeys || (parsed.kid && parsed.key ? { [parsed.kid]: parsed.key } : parsed);
|
||||
const clearkeys = Object.entries(source).reduce((acc, [kid, key]) => {
|
||||
if (kid && key) acc[normalizeClearKeyValue(kid)] = normalizeClearKeyValue(key);
|
||||
return acc;
|
||||
}, {});
|
||||
return Object.keys(clearkeys).length ? { 'org.w3.clearkey': { clearkeys } } : null;
|
||||
}
|
||||
|
||||
function getLinkType(link) {
|
||||
if (link.type) return link.type;
|
||||
const path = (link.url || '').split('?')[0].toLowerCase();
|
||||
if (path.endsWith('.mpd')) return 'dash';
|
||||
if (path.endsWith('.m3u8')) return 'm3u8';
|
||||
if (path.endsWith('.flv')) return 'flv';
|
||||
return '';
|
||||
}
|
||||
|
||||
function getPlaybackUrl(link) {
|
||||
return link.playback_url || link.url;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const streamId = urlParams.get('id');
|
||||
const els = {
|
||||
loading: document.getElementById('loading-container'),
|
||||
player: document.getElementById('player-container'),
|
||||
pwdModal: document.getElementById('password-modal'),
|
||||
msg: document.getElementById('msg'),
|
||||
subMsg: document.getElementById('sub-msg')
|
||||
};
|
||||
const livePollMs = 5000;
|
||||
const playbackMonitorMs = 10000;
|
||||
let livePollTimer = null;
|
||||
let playbackMonitorTimer = null;
|
||||
let viewerHeartbeatTimer = null;
|
||||
let viewerSessionId = '';
|
||||
let viewerVisitorId = localStorage.getItem('streamhall_visitor_id') || '';
|
||||
let playerInstance = null;
|
||||
let playbackMisses = 0;
|
||||
if (!viewerVisitorId) {
|
||||
viewerVisitorId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
||||
localStorage.setItem('streamhall_visitor_id', viewerVisitorId);
|
||||
}
|
||||
|
||||
const applyPlayerTitle = (data = {}) => {
|
||||
if (!data.eventName) return;
|
||||
document.title = data.siteTitle ? `${data.eventName} - ${data.siteTitle}` : data.eventName;
|
||||
};
|
||||
|
||||
const applySiteIcon = (url = '') => {
|
||||
const iconUrl = String(url || '').trim();
|
||||
let link = document.querySelector('link[rel="icon"]');
|
||||
if (!iconUrl) {
|
||||
if (link) link.remove();
|
||||
return;
|
||||
}
|
||||
if (!link) {
|
||||
link = document.createElement('link');
|
||||
link.rel = 'icon';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
link.href = iconUrl;
|
||||
};
|
||||
|
||||
const setLoadingState = (message, detail = '', waiting = false) => {
|
||||
els.player.classList.add('hidden');
|
||||
els.loading.classList.remove('hidden');
|
||||
els.loading.classList.toggle('waiting-live', waiting);
|
||||
els.msg.textContent = message;
|
||||
els.subMsg.textContent = detail;
|
||||
};
|
||||
|
||||
const clearLivePoll = () => {
|
||||
if (livePollTimer) window.clearInterval(livePollTimer);
|
||||
livePollTimer = null;
|
||||
};
|
||||
|
||||
const clearPlaybackMonitor = () => {
|
||||
if (playbackMonitorTimer) window.clearInterval(playbackMonitorTimer);
|
||||
playbackMonitorTimer = null;
|
||||
playbackMisses = 0;
|
||||
};
|
||||
|
||||
const destroyPlayer = () => {
|
||||
clearPlaybackMonitor();
|
||||
if (!playerInstance) return;
|
||||
try {
|
||||
playerInstance.destroy(false);
|
||||
} catch (e) {
|
||||
console.warn('Player destroy failed:', e);
|
||||
}
|
||||
playerInstance = null;
|
||||
els.player.innerHTML = '';
|
||||
};
|
||||
|
||||
const viewerState = () => document.hidden ? 'hidden' : 'viewing';
|
||||
|
||||
const postViewerEvent = async (action, payload = {}) => {
|
||||
const res = await fetch(`/api?action=${action}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.status !== 'success') throw new Error(t('err.' + (json.code || '')) || json.code || action);
|
||||
return json.data || {};
|
||||
};
|
||||
|
||||
const startViewerStats = async () => {
|
||||
if (viewerSessionId) return;
|
||||
try {
|
||||
const data = await postViewerEvent('viewer_start', {
|
||||
id: streamId,
|
||||
visitorId: viewerVisitorId,
|
||||
referer: document.referrer || '',
|
||||
state: viewerState()
|
||||
});
|
||||
viewerSessionId = data.sessionId || '';
|
||||
if (!viewerSessionId) return;
|
||||
viewerHeartbeatTimer = window.setInterval(() => {
|
||||
postViewerEvent('viewer_heartbeat', {
|
||||
sessionId: viewerSessionId,
|
||||
state: viewerState()
|
||||
}).catch(() => {});
|
||||
}, 15000);
|
||||
} catch (e) {
|
||||
console.warn('Viewer stats start failed:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const endViewerStats = () => {
|
||||
if (viewerHeartbeatTimer) window.clearInterval(viewerHeartbeatTimer);
|
||||
viewerHeartbeatTimer = null;
|
||||
if (!viewerSessionId) return;
|
||||
const payload = JSON.stringify({ sessionId: viewerSessionId, state: 'ended' });
|
||||
try {
|
||||
navigator.sendBeacon('/api?action=viewer_end', new Blob([payload], { type: 'application/json' }));
|
||||
} catch (e) {
|
||||
fetch('/api?action=viewer_end', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: payload,
|
||||
keepalive: true
|
||||
}).catch(() => {});
|
||||
}
|
||||
viewerSessionId = '';
|
||||
};
|
||||
|
||||
window.addEventListener('pagehide', endViewerStats);
|
||||
|
||||
if (!streamId) {
|
||||
setLoadingState(t('invalid_id'), '', true);
|
||||
document.title = t('invalid_id');
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchData = async (pwd = null) => {
|
||||
const url = `/api?action=${pwd ? 'verify_password' : 'get_player_data'}${pwd ? '' : '&id=' + encodeURIComponent(streamId)}`;
|
||||
const opts = pwd ? {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ id: streamId, password: pwd }),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
} : {};
|
||||
const res = await fetch(url, opts);
|
||||
const json = await res.json();
|
||||
if (json.status !== 'success') throw new Error(t('err.' + (json.code || '')) || json.code || '');
|
||||
return json.data;
|
||||
};
|
||||
|
||||
const checkPlayerStream = async (password = '') => {
|
||||
const res = await fetch('/api?action=check_player_stream', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: streamId, password })
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.status !== 'success') throw new Error(t('err.' + (json.code || '')) || json.code || t('check_failed'));
|
||||
return json.data || { valid: false };
|
||||
};
|
||||
|
||||
const waitForLiveAndStart = (data, password = '') => {
|
||||
clearLivePoll();
|
||||
destroyPlayer();
|
||||
if (!data.links || data.links.length === 0) {
|
||||
setLoadingState(t('no_sources'), '', true);
|
||||
return;
|
||||
}
|
||||
|
||||
let started = false;
|
||||
const probeAndStart = async (showChecking = false) => {
|
||||
if (started) return;
|
||||
if (showChecking) {
|
||||
setLoadingState(t('checking'), t('checking_sub'));
|
||||
}
|
||||
try {
|
||||
const result = await checkPlayerStream(password);
|
||||
if (result.valid) {
|
||||
started = true;
|
||||
clearLivePoll();
|
||||
initPlayer(data, result.url || '', password);
|
||||
return;
|
||||
}
|
||||
setLoadingState(t('no_stream'), t('waiting_sub'), true);
|
||||
} catch (e) {
|
||||
setLoadingState(e.message || t('no_stream'), t('waiting_sub'), true);
|
||||
}
|
||||
};
|
||||
|
||||
probeAndStart(true);
|
||||
livePollTimer = window.setInterval(() => probeAndStart(false), livePollMs);
|
||||
};
|
||||
|
||||
try {
|
||||
const data = await fetchData();
|
||||
applyPlayerTitle(data);
|
||||
applySiteIcon(data.siteIconUrl || '');
|
||||
if (data.requires_password) {
|
||||
els.loading.classList.add('hidden');
|
||||
els.pwdModal.classList.remove('hidden');
|
||||
document.getElementById('pwd-in').focus();
|
||||
} else {
|
||||
startViewerStats();
|
||||
waitForLiveAndStart(data);
|
||||
}
|
||||
} catch (e) {
|
||||
setLoadingState(e.message, '', true);
|
||||
}
|
||||
|
||||
document.getElementById('password-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const pwd = document.getElementById('pwd-in').value;
|
||||
try {
|
||||
const data = await fetchData(pwd);
|
||||
applyPlayerTitle(data);
|
||||
applySiteIcon(data.siteIconUrl || '');
|
||||
els.pwdModal.classList.add('hidden');
|
||||
startViewerStats();
|
||||
waitForLiveAndStart(data, pwd);
|
||||
} catch (err) {
|
||||
const errEl = document.getElementById('pwd-err');
|
||||
errEl.textContent = err.message;
|
||||
errEl.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
function startPlaybackMonitor(data, password = '') {
|
||||
clearPlaybackMonitor();
|
||||
playbackMonitorTimer = window.setInterval(async () => {
|
||||
try {
|
||||
const result = await checkPlayerStream(password);
|
||||
playbackMisses = result.valid ? 0 : playbackMisses + 1;
|
||||
} catch (e) {
|
||||
playbackMisses += 1;
|
||||
}
|
||||
if (playbackMisses >= 2) {
|
||||
setLoadingState(t('no_stream'), t('resuming_sub'), true);
|
||||
waitForLiveAndStart(data, password);
|
||||
}
|
||||
}, playbackMonitorMs);
|
||||
}
|
||||
|
||||
function initPlayer(data, preferredUrl = '', password = '') {
|
||||
clearLivePoll();
|
||||
destroyPlayer();
|
||||
els.loading.classList.add('hidden');
|
||||
els.loading.classList.remove('waiting-live');
|
||||
els.player.classList.remove('hidden');
|
||||
const links = data.links || [];
|
||||
if (links.length === 0) {
|
||||
els.player.classList.add('hidden');
|
||||
els.loading.classList.remove('hidden');
|
||||
els.msg.textContent = t('no_sources');
|
||||
return;
|
||||
}
|
||||
|
||||
const activeLinks = [...links];
|
||||
const preferredIndex = activeLinks.findIndex(link => link.url === preferredUrl || getPlaybackUrl(link) === preferredUrl);
|
||||
if (preferredIndex > 0) {
|
||||
activeLinks.unshift(activeLinks.splice(preferredIndex, 1)[0]);
|
||||
}
|
||||
|
||||
const quality = activeLinks.map((l, i) => ({
|
||||
default: i === 0,
|
||||
html: l.name,
|
||||
url: getPlaybackUrl(l),
|
||||
type: getLinkType(l)
|
||||
}));
|
||||
|
||||
playerInstance = new Artplayer({
|
||||
container: '.artplayer-app',
|
||||
url: quality[0].url,
|
||||
type: quality[0].type,
|
||||
quality: quality,
|
||||
title: data.eventName,
|
||||
autoplay: true,
|
||||
volume: 0.5,
|
||||
autoSize: true,
|
||||
fullscreen: true,
|
||||
fullscreenWeb: true,
|
||||
setting: true,
|
||||
pip: true,
|
||||
customType: {
|
||||
flv: function (video, url, art) {
|
||||
if (flvjs.isSupported()) {
|
||||
const flv = flvjs.createPlayer({ type: 'flv', url: url });
|
||||
flv.attachMediaElement(video);
|
||||
flv.load();
|
||||
art.flv = flv;
|
||||
art.on('destroy', () => flv.destroy());
|
||||
} else {
|
||||
art.notice.show = t('flv_unsupported');
|
||||
}
|
||||
},
|
||||
m3u8: function (video, url, art) {
|
||||
if (Hls.isSupported()) {
|
||||
if (art.hls) art.hls.destroy();
|
||||
const currentLink = activeLinks.find(l => l.url === url || getPlaybackUrl(l) === url);
|
||||
const keyOverride = currentLink ? parseHlsKeyOverride(currentLink.key) : null;
|
||||
|
||||
class CustomLoader extends Hls.DefaultConfig.loader {
|
||||
loadInternal() {
|
||||
const { context } = this;
|
||||
const overrideKey = keyOverride && context && context.keyInfo ? selectHlsKeyOverride(keyOverride, context.url) : '';
|
||||
if (overrideKey) {
|
||||
try {
|
||||
const keyData = hexToArrayBuffer(overrideKey);
|
||||
this.loader = {
|
||||
readyState: 4,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
responseType: 'arraybuffer',
|
||||
response: keyData,
|
||||
responseText: keyData,
|
||||
responseURL: context.url
|
||||
};
|
||||
this.readystatechange();
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error('Key Override Error:', e);
|
||||
}
|
||||
}
|
||||
super.loadInternal();
|
||||
}
|
||||
}
|
||||
|
||||
const hls = new Hls({
|
||||
enableSoftwareAES: !!keyOverride,
|
||||
loader: keyOverride ? CustomLoader : Hls.DefaultConfig.loader,
|
||||
debug: false
|
||||
});
|
||||
hls.loadSource(url);
|
||||
hls.attachMedia(video);
|
||||
art.hls = hls;
|
||||
art.on('destroy', () => hls.destroy());
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = url;
|
||||
} else {
|
||||
art.notice.show = t('hls_unsupported');
|
||||
}
|
||||
},
|
||||
dash: function (video, url, art) {
|
||||
if (!window.dashjs) {
|
||||
art.notice.show = t('dash_unavailable');
|
||||
return;
|
||||
}
|
||||
if (art.dash) art.dash.destroy();
|
||||
const currentLink = activeLinks.find(l => l.url === url || getPlaybackUrl(l) === url);
|
||||
const dash = dashjs.MediaPlayer().create();
|
||||
const protectionData = currentLink ? buildClearKeyProtectionData(currentLink.clearkey) : null;
|
||||
if (protectionData) dash.setProtectionData(protectionData);
|
||||
dash.initialize(video, url, art.option.autoplay);
|
||||
art.dash = dash;
|
||||
art.on('destroy', () => dash.destroy());
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
artplayerPluginHlsControl({
|
||||
quality: { control: true, setting: true, getName: (l) => l.height + 'P', title: t('quality') }
|
||||
})
|
||||
]
|
||||
});
|
||||
startPlaybackMonitor(data, password);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
/*!
|
||||
* artplayer-plugin-hls-control.js v1.0.1
|
||||
* Github: https://github.com/zhw2590582/ArtPlayer
|
||||
* (c) 2017-2024 Harvey Zack
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t,n,o,l){var r="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},i="function"==typeof r[o]&&r[o],u=i.cache||{},a="undefined"!=typeof module&&"function"==typeof module.require&&module.require.bind(module);function c(t,n){if(!u[t]){if(!e[t]){var l="function"==typeof r[o]&&r[o];if(!n&&l)return l(t,!0);if(i)return i(t,!0);if(a&&"string"==typeof t)return a(t);var s=Error("Cannot find module '"+t+"'");throw s.code="MODULE_NOT_FOUND",s}d.resolve=function(n){var o=e[t][1][n];return null!=o?o:n},d.cache={};var f=u[t]=new c.Module(t);e[t][0].call(f.exports,d,f,f.exports,this)}return u[t].exports;function d(e){var t=d.resolve(e);return!1===t?{}:c(t)}}c.isParcelRequire=!0,c.Module=function(e){this.id=e,this.bundle=c,this.exports={}},c.modules=e,c.cache=u,c.parent=i,c.register=function(t,n){e[t]=[function(e,t){t.exports=n},{}]},Object.defineProperty(c,"root",{get:function(){return r[o]}}),r[o]=c;for(var s=0;s<t.length;s++)c(t[s]);if(n){var f=c(n);"object"==typeof exports&&"undefined"!=typeof module?module.exports=f:"function"==typeof define&&define.amd&&define(function(){return f})}}({haa6A:[function(e,t,n){var o=e("@parcel/transformer-js/src/esmodule-helpers.js");o.defineInteropFlag(n),o.export(n,"default",()=>c);var l=e("bundle-text:./quality.svg"),r=o.interopDefault(l),i=e("bundle-text:./audio.svg"),u=o.interopDefault(i);function a(e,t){let n=new Map;return e.filter(e=>{let o=e[t];return void 0===o||!n.has(o)&&n.set(o,1)})}function c(e={}){return t=>{let{$video:n}=t.template,{errorHandle:o}=t.constructor.utils;function l(){o(t.hls?.media===n,'Cannot find instance of HLS from "art.hls"'),function(n){if(!n.levels.length)return;let o=e.quality||{},l=o.auto||"Auto",i=o.title||"Quality",u=o.getName||(e=>e.name||e.height+"P"),c=n.levels[n.currentLevel],s=c?u(c):l,f=a(n.levels.map((e,t)=>({html:u(e,t),value:t,default:n.currentLevel===t})),"html").sort((e,t)=>t.value-e.value);f.push({html:l,value:-1,default:-1===n.currentLevel});let d=e=>(n.currentLevel=e.value,t.notice.show=`${i}: ${e.html}`,o.control&&t.controls.check(e),o.setting&&t.setting.check(e),e.html);o.control&&t.controls.update({name:"hls-quality",position:"right",html:s,style:{padding:"0 10px"},selector:f,onSelect:d}),o.setting&&t.setting.update({name:"hls-quality",tooltip:s,html:i,icon:r.default,width:200,selector:f,onSelect:d})}(t.hls),function(n){if(!n.audioTracks.length)return;let o=e.audio||{},l=o.auto||"Auto",r=o.title||"Audio",i=o.getName||(e=>e.name||e.lang||e.language),c=n.audioTracks[n.audioTrack],s=c?i(c):l,f=a(n.audioTracks.map((e,t)=>({html:i(e,t),value:e.id,default:n.audioTrack===e.id})),"html"),d=e=>(n.audioTrack=e.value,t.notice.show=`${r}: ${e.html}`,o.control&&t.controls.check(e),o.setting&&t.setting.check(e),e.html);o.control&&t.controls.update({name:"hls-audio",position:"right",html:s,style:{padding:"0 10px"},selector:f,onSelect:d}),o.setting&&t.setting.update({name:"hls-audio",tooltip:s,html:r,icon:u.default,width:200,selector:f,onSelect:d})}(t.hls)}return t.on("ready",l),t.on("restart",l),{name:"artplayerPluginHlsControl",update:l}}}"undefined"!=typeof window&&(window.artplayerPluginHlsControl=c)},{"bundle-text:./quality.svg":"5aI3W","bundle-text:./audio.svg":"kbgg8","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"5aI3W":[function(e,t,n){t.exports='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96c0-35.3 28.7-64 64-64h384c35.3 0 64 28.7 64 64v320c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zm323.8 106.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6-26.5-33.1c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4S78.8 416 88 416h336c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'},{}],kbgg8:[function(e,t,n){t.exports='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6 26.5 0 48 21.5 48 48v128c0 26.5-21.5 48-48 48-44.2 0-80-35.8-80-80V288C0 146.6 114.6 32 256 32s256 114.6 256 256v112c0 44.2-35.8 80-80 80-26.5 0-48-21.5-48-48V304c0-26.5 21.5-48 48-48 10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'},{}],"9pCYc":[function(e,t,n){n.interopDefault=function(e){return e&&e.__esModule?e:{default:e}},n.defineInteropFlag=function(e){Object.defineProperty(e,"__esModule",{value:!0})},n.exportAll=function(e,t){return Object.keys(e).forEach(function(n){"default"===n||"__esModule"===n||Object.prototype.hasOwnProperty.call(t,n)||Object.defineProperty(t,n,{enumerable:!0,get:function(){return e[n]}})}),t},n.export=function(e,t,n){Object.defineProperty(e,t,{enumerable:!0,get:n})}},{}]},["haa6A"],"haa6A","parcelRequire4dc0");
|
||||
Vendored
+7
File diff suppressed because one or more lines are too long
Vendored
+3
File diff suppressed because one or more lines are too long
Vendored
+10
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user