push
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
PLAYER — plays a stream in a <video> element and supports switching
|
||||
sources mid-watch without losing your place.
|
||||
|
||||
Pipeline per stream:
|
||||
- hls.js for .m3u8 when available
|
||||
- native HLS for Safari (canPlayType check)
|
||||
- direct <video src> for mp4/webm
|
||||
- everything else (DASH, mkv, magnet, …) → friendly error
|
||||
|
||||
Every media + subtitle URL is routed through the API proxy.
|
||||
On source switch we carry over currentTime, playbackRate, volume, muted
|
||||
and the playing state, clamped to the new duration.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function create(video, handlers = {}) {
|
||||
const onStatus = handlers.onStatus || (() => {});
|
||||
const onError = handlers.onError || (() => {});
|
||||
const onReady = handlers.onReady || (() => {});
|
||||
|
||||
let hls = null;
|
||||
let token = 0; // guards against overlapping loads
|
||||
let networkRetry = false;
|
||||
|
||||
function teardown() {
|
||||
token++;
|
||||
if (hls) {
|
||||
try {
|
||||
hls.destroy();
|
||||
} catch (e) {
|
||||
/* already destroyed */
|
||||
}
|
||||
hls = null;
|
||||
}
|
||||
for (const track of video.querySelectorAll('track')) track.remove();
|
||||
video.removeAttribute('src');
|
||||
try {
|
||||
video.load();
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function capture() {
|
||||
const v = video;
|
||||
return {
|
||||
time: isFinite(v.currentTime) && v.currentTime > 0 ? v.currentTime : 0,
|
||||
rate: v.playbackRate || 1,
|
||||
volume: v.volume,
|
||||
muted: v.muted,
|
||||
playing: !v.paused && !v.ended,
|
||||
};
|
||||
}
|
||||
|
||||
function restore(st) {
|
||||
const v = video;
|
||||
v.playbackRate = st.rate;
|
||||
v.volume = st.volume;
|
||||
v.muted = st.muted;
|
||||
const seek = () => {
|
||||
const d = v.duration;
|
||||
let t = st.time;
|
||||
if (isFinite(d) && d > 0) t = Math.min(t, Math.max(0, d - 5));
|
||||
if (t > 10) v.currentTime = t;
|
||||
if (st.playing) v.play().catch(() => {});
|
||||
};
|
||||
if (isFinite(v.duration) && v.duration > 0) seek();
|
||||
else v.addEventListener('loadedmetadata', seek, { once: true });
|
||||
}
|
||||
|
||||
function attachSubtitles(stream) {
|
||||
for (const sub of stream.subtitles || []) {
|
||||
let resolved;
|
||||
try {
|
||||
resolved = new URL(sub.url, stream.url).toString();
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
const track = document.createElement('track');
|
||||
track.kind = 'subtitles';
|
||||
track.srclang = sub.language || 'en';
|
||||
track.label = sub.name || sub.language || 'Subtitles';
|
||||
track.src = SR.api.proxyUrl(resolved, stream.headers);
|
||||
video.appendChild(track);
|
||||
}
|
||||
}
|
||||
|
||||
function load(stream, opts = {}) {
|
||||
const st = capture();
|
||||
if (typeof opts.resumeAt === 'number' && opts.resumeAt > 10) st.time = opts.resumeAt;
|
||||
|
||||
teardown();
|
||||
networkRetry = false;
|
||||
const myToken = token;
|
||||
const guarded = (fn) => () => {
|
||||
if (myToken === token) fn();
|
||||
};
|
||||
|
||||
const url = stream.url || '';
|
||||
const isHls = stream.type === 'm3u8' || /\.m3u8($|\?)/i.test(url);
|
||||
const isDash = stream.type === 'dash' || /\.mpd($|\?)/i.test(url);
|
||||
const isDirect =
|
||||
stream.type === 'mp4' ||
|
||||
stream.type === 'webm' ||
|
||||
/\.(mp4|m4v|webm)(\?|#|$)/i.test(url);
|
||||
|
||||
if (isDash) {
|
||||
onError('DASH streams are not supported yet — pick another source.');
|
||||
return;
|
||||
}
|
||||
if (!isHls && !isDirect) {
|
||||
onError(`The browser can't play ${stream.type} files directly — pick an MP4 or HLS source.`);
|
||||
return;
|
||||
}
|
||||
|
||||
attachSubtitles(stream);
|
||||
const proxied = SR.api.proxyUrl(url, stream.headers);
|
||||
|
||||
if (isHls && window.Hls && Hls.isSupported()) {
|
||||
onStatus('Loading stream…');
|
||||
const h = new Hls({ enableWorker: true });
|
||||
hls = h;
|
||||
h.loadSource(proxied);
|
||||
h.attachMedia(video);
|
||||
h.on(Hls.Events.MANIFEST_PARSED, guarded(() => {
|
||||
restore(st);
|
||||
onReady(st.time > 10 ? st.time : 0);
|
||||
}));
|
||||
h.on(Hls.Events.ERROR, (evt, data) => {
|
||||
if (myToken !== token || !data.fatal) return;
|
||||
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
|
||||
if (!networkRetry) {
|
||||
networkRetry = true;
|
||||
setTimeout(() => {
|
||||
networkRetry = false;
|
||||
if (myToken === token && hls === h) h.startLoad();
|
||||
}, 2000);
|
||||
}
|
||||
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
||||
h.recoverMediaError();
|
||||
} else {
|
||||
onError('Playback failed: ' + (data.details || 'unknown stream error'));
|
||||
teardown();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// native HLS (Safari) or direct file
|
||||
onStatus('Loading stream…');
|
||||
video.src = proxied;
|
||||
video.addEventListener(
|
||||
'loadedmetadata',
|
||||
guarded(() => {
|
||||
restore(st);
|
||||
onReady(st.time > 10 ? st.time : 0);
|
||||
}),
|
||||
{ once: true }
|
||||
);
|
||||
video.addEventListener(
|
||||
'error',
|
||||
guarded(() => {
|
||||
onError('Could not load this stream — try another source.');
|
||||
}),
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
teardown();
|
||||
}
|
||||
|
||||
return { load, destroy };
|
||||
}
|
||||
|
||||
window.SR = window.SR || {};
|
||||
SR.player = { create };
|
||||
})();
|
||||
Reference in New Issue
Block a user