diff --git a/changes/anime-browser.md b/changes/anime-browser.md index 98017763..15904034 100644 --- a/changes/anime-browser.md +++ b/changes/anime-browser.md @@ -15,6 +15,7 @@ area: anime - The primary subtitle slot stays reserved for Japanese: a source that only has, say, English subtitles gets them added with a normalized language tag (`English` → `en`) but not selected, so the regular `secondarySub` auto-load can route them to the secondary slot instead. - HLS streams pass through a local strip proxy that removes fake image headers some hosts glue onto their video segments, so streams ffmpeg would otherwise probe as "a PNG" and abandon now play in mpv. - The strip proxy retries a failed segment fetch once after a short pause and logs upstream error statuses; a host that errors on the very first fetches right after an episode resolves no longer kills the whole playback. +- The strip proxy no longer forwards `Range` headers to the bridge: ffmpeg opens every HLS segment with `Range: bytes=0-`, the bridge answers some of those with 206, and a partial response bypassed the disguise strip, so whether an episode played depended on the bridge's cache state. - "Playing" is only reported once mpv actually configures a video output; when a stream fails to decode, the browser shows mpv's error instead of claiming playback started while no window ever appeared. - The Linux x64 bridge bundle is verified and pinned, so the anime browser starts on Linux instead of refusing with "No pinned checksum for linux-x64-bundle.zip". - The bridge bundle is fetched from the pinned release tag rather than whatever release is newest, so an upstream publish no longer breaks every install with a checksum mismatch. diff --git a/src/anime-bridge/stream-strip-proxy.test.ts b/src/anime-bridge/stream-strip-proxy.test.ts index 570684a5..a0473346 100644 --- a/src/anime-bridge/stream-strip-proxy.test.ts +++ b/src/anime-bridge/stream-strip-proxy.test.ts @@ -156,6 +156,42 @@ test('proxy rewrites absolute upstream playlist entries to its own origin', asyn } }); +test('proxy strips even when the client asks for a byte range', async () => { + // ffmpeg opens every HLS segment with `Range: bytes=0-`; the bridge answers + // some of those with 206, which must not bypass the strip. + const ts = makeTsPackets(8); + const disguised = Buffer.concat([PNG_HEADER, ts]); + const upstream = http.createServer((req, res) => { + if (req.headers.range !== undefined) { + res.writeHead(206, { + 'content-type': 'image/png', + 'content-range': `bytes 0-${disguised.length - 1}/${disguised.length}`, + }); + res.end(disguised); + return; + } + res.writeHead(200, { 'content-type': 'image/png' }); + res.end(disguised); + }); + await new Promise((resolve) => upstream.listen(0, '127.0.0.1', resolve)); + const upstreamOrigin = `http://127.0.0.1:${(upstream.address() as AddressInfo).port}`; + const proxy = await startStreamStripProxy({ + upstreamOrigin: () => upstreamOrigin, + retryDelayMs: 5, + }); + try { + const response = await fetch(`${proxy.origin}/video/seg.ts`, { + headers: { Range: 'bytes=0-' }, + }); + const body = Buffer.from(await response.arrayBuffer()); + assert.equal(response.status, 200); + assert.deepEqual(body, ts); + } finally { + await proxy.close(); + await new Promise((resolve) => upstream.close(() => resolve())); + } +}); + test('proxy forwards error statuses without touching the body', async () => { await withProxy( { '/video/gone.ts': { status: 404, contentType: 'text/plain', body: Buffer.from('nope') } }, diff --git a/src/anime-bridge/stream-strip-proxy.ts b/src/anime-bridge/stream-strip-proxy.ts index 883acd05..214da2e9 100644 --- a/src/anime-bridge/stream-strip-proxy.ts +++ b/src/anime-bridge/stream-strip-proxy.ts @@ -119,6 +119,11 @@ export function startStreamStripProxy( const requestHeaders = forwardableHeaders(req.headers); delete requestHeaders.host; + // Never forward Range: ffmpeg opens every segment with `bytes=0-`, the + // bridge answers some of those 206, and a partial response cannot be + // stripped (only full 200 bodies are). Byte ranges into a resource whose + // bytes this proxy rewrites would be incoherent anyway. + delete requestHeaders.range; res.on('error', () => {}); requestUpstream(req, res, upstreamUrl, requestHeaders, 0);