Race the CDN and the proxy, for thumbnails and for playback
A thumbnail used to try the provider and only ask /api/image once that had failed, so every hotlink-blocked host cost a wasted request per card before anything appeared. Both routes now go out together for the first thumbnail of a host, and the rest of the batch waits on that one answer rather than each rediscovering it. Speed decides which image is shown; capability decides what the host is remembered as, since the proxy tends to win first contact merely for being same-origin -- pinning a host to it over that would push a whole page of thumbnails through our own server. Playback asks the same question, but per video and at play time: one provider can spread its media over several CDNs, so there is nothing useful to pre-compute, and the old per-card probe answered for whichever card happened to scroll past. The direct route is now tested alongside the proxied playback and takes over if it answers before a frame is decoded. Whatever loses is cancelled -- the token guards stopped stale callbacks but left their requests running, so the losing route kept pulling bytes and the server kept an upstream connection open for them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
This commit is contained in:
@@ -25,9 +25,40 @@ App.player = App.player || {};
|
||||
originEl: null,
|
||||
hudHovered: false, // mouse resting on the controls (desktop)
|
||||
activeUrl: '', // media URL actually playing, for the format menu's tick
|
||||
attemptToken: 0 // bumps on every open()/format switch to void stale async callbacks
|
||||
attemptToken: 0, // bumps on every open()/format switch to void stale async callbacks
|
||||
fetchAbort: null // aborts the current attempt's own requests
|
||||
};
|
||||
|
||||
// Stops everything the current attempt has in flight. The token guards keep
|
||||
// stale *callbacks* from acting, but they don't stop the requests those
|
||||
// callbacks were waiting on: hls.js goes on pulling segments through the
|
||||
// proxy, the media element keeps its connection open, and the content-type
|
||||
// sniff keeps a whole upstream fetch alive on the server. When an attempt is
|
||||
// superseded -- most of all when the direct route wins the race and the
|
||||
// proxy has nothing left to do -- that work is pure waste at both ends.
|
||||
function cancelInFlight() {
|
||||
if (cp.fetchAbort) {
|
||||
cp.fetchAbort.abort();
|
||||
cp.fetchAbort = null;
|
||||
}
|
||||
if (state.hlsPlayer) {
|
||||
state.hlsPlayer.stopLoad();
|
||||
state.hlsPlayer.detachMedia();
|
||||
state.hlsPlayer.destroy();
|
||||
state.hlsPlayer = null;
|
||||
}
|
||||
const video = cp.video;
|
||||
if (video) {
|
||||
video.onerror = null;
|
||||
video.pause();
|
||||
// Dropping the source is what closes the connection the media
|
||||
// element is holding; load() makes the element let go of it now
|
||||
// rather than whenever it next feels like it.
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
}
|
||||
}
|
||||
|
||||
const addCleanup = (fn) => cp.cleanups.push(fn);
|
||||
const runCleanups = () => {
|
||||
cp.cleanups.forEach((fn) => { try { fn(); } catch (err) { /* ignore */ } });
|
||||
@@ -652,6 +683,10 @@ App.player = App.player || {};
|
||||
function playSources(videoData, opts) {
|
||||
const video = cp.video;
|
||||
const token = ++cp.attemptToken;
|
||||
// Every route into here supersedes whatever was playing or loading: the
|
||||
// direct route winning its race, a quality switch, a retry, a re-open.
|
||||
// Void the old attempt's callbacks, then stop its requests.
|
||||
cancelInFlight();
|
||||
const resumeAt = (opts && opts.resumeAt) || 0;
|
||||
// Captured once per call rather than read from the shared `cp`
|
||||
// object later: if open() is ever re-entered for a different video
|
||||
@@ -679,6 +714,39 @@ App.player = App.player || {};
|
||||
plan.push({ resolved, direct: false });
|
||||
});
|
||||
|
||||
// Whether a CDN will serve the browser directly is asked here, at play
|
||||
// time, about this video's own media URL -- not in advance about the
|
||||
// listing's. One provider can spread its media across several CDNs, so
|
||||
// there is no single answer to pre-compute, and any answer taken from
|
||||
// another video may not hold for this one.
|
||||
//
|
||||
// The question runs *alongside* the proxied playback rather than ahead
|
||||
// of it, so it never delays anything: the proxy is already carrying the
|
||||
// video while the direct route is being tested. If the answer comes
|
||||
// back before any frame has been decoded, the attempt restarts on the
|
||||
// direct URL -- nothing is on screen yet, so there is nothing to
|
||||
// interrupt. If playback has already begun, the answer is kept, and the
|
||||
// next video from that CDN starts direct without asking again.
|
||||
const raceDirect = function(resolved) {
|
||||
if (!App.videos || typeof App.videos.probeDirect !== 'function') return;
|
||||
if (!resolved.url || resolved.isLive) return;
|
||||
// An origin that demands a Referer can never be fetched directly by
|
||||
// a browser, so there is nothing to find out.
|
||||
if (resolved.refererRequired) return;
|
||||
if (directProven(resolved.url)) return;
|
||||
App.videos.probeDirect(resolved.url).then((ok) => {
|
||||
if (!ok || token !== cp.attemptToken) return;
|
||||
// readyState >= HAVE_CURRENT_DATA means a frame is up; leave a
|
||||
// playing video alone rather than trading a visible stall for a
|
||||
// saved hop.
|
||||
if (!cp.video || cp.video.readyState >= 2) return;
|
||||
// Direct won. Restarting cancels the proxy's fetch on the way
|
||||
// in (see cancelInFlight), so the losing route stops pulling
|
||||
// bytes instead of running to completion behind the winner.
|
||||
playSources(videoData, Object.assign({}, opts, { resumeAt: resumeAt }));
|
||||
});
|
||||
};
|
||||
|
||||
const attempt = async (index) => {
|
||||
if (token !== cp.attemptToken) return;
|
||||
const entry = plan[index];
|
||||
@@ -723,16 +791,13 @@ App.player = App.player || {};
|
||||
let isHls = kind.isHls;
|
||||
let isDirectMedia = kind.isDirectMedia;
|
||||
|
||||
video.onerror = null;
|
||||
if (state.hlsPlayer) {
|
||||
state.hlsPlayer.stopLoad();
|
||||
state.hlsPlayer.detachMedia();
|
||||
state.hlsPlayer.destroy();
|
||||
state.hlsPlayer = null;
|
||||
}
|
||||
video.pause();
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
cancelInFlight();
|
||||
const attemptAbort = new AbortController();
|
||||
cp.fetchAbort = attemptAbort;
|
||||
|
||||
// Going out through the proxy: find out in parallel whether this
|
||||
// CDN would have taken the browser directly.
|
||||
if (!entry.direct) raceDirect(resolved);
|
||||
|
||||
// Last resort only: a HEAD through the proxy is a whole upstream
|
||||
// connection (handshake included) before the first byte of video is
|
||||
@@ -740,14 +805,23 @@ App.player = App.player || {};
|
||||
// extractor's protocol says what this source is.
|
||||
if (!isHls && !isDirectMedia && !entry.direct) {
|
||||
try {
|
||||
const headResp = await fetch(streamUrl, { method: 'HEAD' });
|
||||
if (token !== cp.attemptToken) return;
|
||||
const headResp = await fetch(streamUrl, {
|
||||
method: 'HEAD',
|
||||
signal: attemptAbort.signal
|
||||
});
|
||||
const contentType = headResp.headers.get('Content-Type') || '';
|
||||
if (contentType.includes('application/vnd.apple.mpegurl')) isHls = true;
|
||||
else if (contentType.startsWith('video/') || contentType.startsWith('audio/')) isDirectMedia = true;
|
||||
} catch (err) {
|
||||
// Best-effort sniff only.
|
||||
// Best-effort sniff only -- including the abort that
|
||||
// cancelInFlight fires, which lands here rather than at the
|
||||
// guard below.
|
||||
}
|
||||
// Outside the catch on purpose: an aborted sniff means this
|
||||
// attempt has been superseded, and swallowing that with the
|
||||
// failure of a best-effort sniff would let a dead attempt walk
|
||||
// on and attach a stream to the player that replaced it.
|
||||
if (token !== cp.attemptToken) return;
|
||||
}
|
||||
|
||||
const startPlayback = () => {
|
||||
@@ -845,14 +919,7 @@ App.player = App.player || {};
|
||||
const reopening = !!(cp.container && cp.container.classList.contains('open'));
|
||||
if (reopening) {
|
||||
cp.attemptToken++;
|
||||
if (state.hlsPlayer) {
|
||||
state.hlsPlayer.destroy();
|
||||
state.hlsPlayer = null;
|
||||
}
|
||||
if (cp.video) {
|
||||
cp.video.onerror = null;
|
||||
cp.video.pause();
|
||||
}
|
||||
cancelInFlight();
|
||||
clearIdleTimer();
|
||||
if (cp.originEl) cp.originEl.classList.remove('is-loading');
|
||||
}
|
||||
@@ -944,17 +1011,10 @@ App.player = App.player || {};
|
||||
App.player.close = function(opts) {
|
||||
if (!cp.container || !cp.container.classList.contains('open')) return;
|
||||
cp.attemptToken++; // void any in-flight attempt()/HEAD-probe callbacks
|
||||
|
||||
if (state.hlsPlayer) {
|
||||
state.hlsPlayer.destroy();
|
||||
state.hlsPlayer = null;
|
||||
}
|
||||
if (cp.video) {
|
||||
cp.video.onerror = null;
|
||||
cp.video.pause();
|
||||
cp.video.removeAttribute('src');
|
||||
cp.video.load();
|
||||
}
|
||||
// Closing the player must also stop what it was fetching -- otherwise a
|
||||
// proxied stream keeps being pulled, and the server keeps an upstream
|
||||
// connection open, for a video nobody is watching any more.
|
||||
cancelInFlight();
|
||||
clearIdleTimer();
|
||||
runCleanups();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user