probe videos in background
This commit is contained in:
@@ -290,6 +290,9 @@ App.videos = App.videos || {};
|
||||
items.forEach(v => {
|
||||
if (state.renderedVideoIds.has(v.id)) return;
|
||||
state.loadedVideos.push(v);
|
||||
// Probe in the background whether this video's best source plays
|
||||
// directly, so playback can bypass the proxy when proven.
|
||||
App.videos.probeVideoSources(v);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'video-card';
|
||||
@@ -685,6 +688,81 @@ App.videos = App.videos || {};
|
||||
return sources.length ? sources[0] : { url: '', referer: '', userAgent: '', isLive: false };
|
||||
};
|
||||
|
||||
// Background "direct playability" probe. The backend proxy exists to work
|
||||
// around CORS, hotlink (403) protection, and TLS fingerprinting. When the
|
||||
// browser can fetch a media URL cross-origin and actually read the response
|
||||
// (CORS allowed, not blocked/403), playing it directly works and the proxy
|
||||
// is pure overhead. We probe each loaded video's best source in the
|
||||
// background and, only when proven, let the player skip the proxy.
|
||||
const DIRECT_PROBE_TIMEOUT_MS = 8000;
|
||||
// Only URLs that the player can hand straight to <video>/hls.js are worth
|
||||
// probing; anything else (e.g. a live channel page URL) is resolved
|
||||
// server-side and must keep going through the proxy.
|
||||
const DIRECT_PLAYABLE_RE = /\.(mp4|m4v|m4s|webm|ts|mov|m3u8)($|\?)/i;
|
||||
|
||||
// url -> true (proven directly playable) | false (proven not). Absent means
|
||||
// unknown/unprobed, in which case the proxy is used.
|
||||
App.videos._directStatus = new Map();
|
||||
const directPending = new Map();
|
||||
|
||||
App.videos.isDirectProven = function(url) {
|
||||
return App.videos._directStatus.get(url) === true;
|
||||
};
|
||||
|
||||
App.videos.probeDirect = function(url) {
|
||||
if (!url) return Promise.resolve(false);
|
||||
if (App.videos._directStatus.has(url)) {
|
||||
return Promise.resolve(App.videos._directStatus.get(url));
|
||||
}
|
||||
if (directPending.has(url)) {
|
||||
return directPending.get(url);
|
||||
}
|
||||
const promise = (async () => {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), DIRECT_PROBE_TIMEOUT_MS);
|
||||
let ok = false;
|
||||
try {
|
||||
// A simple GET (no custom headers) avoids a CORS preflight. If
|
||||
// the response is readable and successful, CORS + reachability
|
||||
// are both proven; we abort immediately so the body isn't
|
||||
// downloaded (it can be a whole video file).
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
signal: controller.signal
|
||||
});
|
||||
ok = res.ok || res.status === 206;
|
||||
controller.abort();
|
||||
} catch (err) {
|
||||
ok = false;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
App.videos._directStatus.set(url, ok);
|
||||
directPending.delete(url);
|
||||
return ok;
|
||||
})();
|
||||
directPending.set(url, promise);
|
||||
return promise;
|
||||
};
|
||||
|
||||
// Kicks off a background probe of a video's best (first-played) source so a
|
||||
// later playback can skip the proxy if the direct URL is proven reachable.
|
||||
App.videos.probeVideoSources = function(video) {
|
||||
if (!video || typeof video !== 'object') return;
|
||||
let sources;
|
||||
try {
|
||||
sources = App.videos.resolveStreamSources(video);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
const best = sources && sources[0];
|
||||
if (!best || !best.url || best.isLive) return;
|
||||
if (!DIRECT_PLAYABLE_RE.test(best.url)) return;
|
||||
App.videos.probeDirect(best.url);
|
||||
};
|
||||
|
||||
// Builds a proxied stream URL. Extra params other than `url` are forwarded
|
||||
// by the backend as request headers, so use real header names here.
|
||||
App.videos.buildStreamUrlFromSource = function(resolved) {
|
||||
|
||||
Reference in New Issue
Block a user