From 785b991d01b527585037b08a45ac75e6b0667372 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 23 Jun 2026 07:54:35 +0000 Subject: [PATCH] probe videos in background --- frontend/js/player.js | 35 +++++++++++++++---- frontend/js/videos.js | 78 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/frontend/js/player.js b/frontend/js/player.js index 603c382..3e5d359 100644 --- a/frontend/js/player.js +++ b/frontend/js/player.js @@ -75,6 +75,19 @@ App.player = App.player || {}; return; } + // Expand the candidate sources into an ordered playback plan. When a + // source has been proven (in the background) to play directly, try the + // raw upstream URL first and keep the proxy as the immediate fallback; + // unproven sources go straight through the proxy. + const directProven = (url) => !!(App.videos && App.videos.isDirectProven && App.videos.isDirectProven(url)); + const playbackPlan = []; + sources.forEach((resolved) => { + if (directProven(resolved.url)) { + playbackPlan.push({ resolved, direct: true }); + } + playbackPlan.push({ resolved, direct: false }); + }); + if (useMobileFullscreen) { const host = getMobileVideoHost(); if (video.parentElement !== host) { @@ -120,8 +133,9 @@ App.player = App.player || {}; // Attempts to play a single source. On a fatal failure it advances to // the next candidate, or reports an error once the list is exhausted. const attempt = async (index) => { - const resolved = sources[index]; - const hasNext = index + 1 < sources.length; + const entry = playbackPlan[index]; + const resolved = entry.resolved; + const hasNext = index + 1 < playbackPlan.length; let playbackStarted = false; let settled = false; @@ -137,10 +151,17 @@ App.player = App.player || {}; } }; - const refererParam = resolved.referer ? `&referer=${encodeURIComponent(resolved.referer)}` : ''; - const userAgentParam = resolved.userAgent ? `&User-Agent=${encodeURIComponent(resolved.userAgent)}` : ''; - const liveParam = resolved.isLive ? '&live=1' : ''; - const streamUrl = `/api/stream?url=${encodeURIComponent(resolved.url)}${refererParam}${userAgentParam}${liveParam}`; + // Proven-direct entries hit the upstream URL straight from the + // browser; everything else is wrapped in the backend stream proxy. + let streamUrl; + if (entry.direct) { + streamUrl = resolved.url; + } else { + const refererParam = resolved.referer ? `&referer=${encodeURIComponent(resolved.referer)}` : ''; + const userAgentParam = resolved.userAgent ? `&User-Agent=${encodeURIComponent(resolved.userAgent)}` : ''; + const liveParam = resolved.isLive ? '&live=1' : ''; + streamUrl = `/api/stream?url=${encodeURIComponent(resolved.url)}${refererParam}${userAgentParam}${liveParam}`; + } let isHls = /\.m3u8($|\?)/i.test(resolved.url); let isDirectMedia = /\.(mp4|m4v|m4s|webm|ts|mov)($|\?)/i.test(resolved.url); // Live cam streams resolve (server-side) to HLS; treat them as HLS up @@ -165,7 +186,7 @@ App.player = App.player || {}; video.removeAttribute('src'); video.load(); - if (!isHls) { + if (!isHls && !entry.direct) { try { const headResp = await fetch(streamUrl, { method: 'HEAD' }); const contentType = headResp.headers.get('Content-Type') || ''; diff --git a/frontend/js/videos.js b/frontend/js/videos.js index 8d08769..a756138 100644 --- a/frontend/js/videos.js +++ b/frontend/js/videos.js @@ -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