Files
SubMiner/src/anime-bridge/stream-strip-proxy.test.ts
T
sudacode 336e9fb8a5 fix(anime): cap outcome polling and upstream stalls by wall clock
- watchPlaybackOutcome now tracks a real deadline so slow property reads eat the timeout budget instead of extending it, and a zero probe interval still terminates
- stream strip proxy aborts upstream GETs that go silent for 15s instead of hanging mpv on that segment forever
- anime browser runtime drops a stream proxy that finishes starting after its bridge already died, instead of leaking a listener pointed at nothing
2026-08-01 03:13:48 -07:00

264 lines
8.9 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import type { AddressInfo } from 'node:net';
import {
findTsSyncOffset,
rewritePlaylistOrigins,
startStreamStripProxy,
TS_PACKET_LENGTH,
} from './stream-strip-proxy';
/** A valid MPEG-TS payload: 0x47 sync byte every 188 bytes. */
function makeTsPackets(count: number): Buffer {
const data = Buffer.alloc(count * TS_PACKET_LENGTH, 0x11);
for (let i = 0; i < count; i++) data[i * TS_PACKET_LENGTH] = 0x47;
return data;
}
/** The disguise seen in the wild: a real PNG header glued before the TS data. */
const PNG_HEADER = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
]);
test('findTsSyncOffset returns 0 for clean TS data', () => {
assert.equal(findTsSyncOffset(makeTsPackets(6)), 0);
});
test('findTsSyncOffset finds TS data behind a PNG prefix', () => {
const disguised = Buffer.concat([PNG_HEADER, makeTsPackets(6)]);
assert.equal(findTsSyncOffset(disguised), PNG_HEADER.length);
});
test('findTsSyncOffset ignores a lone sync byte in the junk prefix', () => {
const junk = Buffer.alloc(64, 0x00);
junk[10] = 0x47; // decoy: no packet run follows it
const disguised = Buffer.concat([junk, makeTsPackets(6)]);
assert.equal(findTsSyncOffset(disguised), junk.length);
});
test('findTsSyncOffset returns null when no TS run exists', () => {
assert.equal(findTsSyncOffset(Buffer.alloc(4096, 0x42)), null);
});
test('findTsSyncOffset returns null when the run starts past the scan limit', () => {
const disguised = Buffer.concat([Buffer.alloc(600, 0x00), makeTsPackets(6)]);
assert.equal(findTsSyncOffset(disguised, 500), null);
});
test('rewritePlaylistOrigins swaps absolute upstream URLs and keeps relative lines', () => {
const body = [
'#EXTM3U',
'#EXTINF:6.006,',
'/video/aaa.ts',
'#EXTINF:4.463,',
'http://127.0.0.1:41569/video/bbb.ts',
].join('\n');
const rewritten = rewritePlaylistOrigins(body, 'http://127.0.0.1:41569', 'http://127.0.0.1:9999');
assert.ok(rewritten.includes('http://127.0.0.1:9999/video/bbb.ts'));
assert.ok(rewritten.includes('\n/video/aaa.ts\n'));
assert.ok(!rewritten.includes('41569'));
});
/* ---------- proxy end-to-end ---------- */
type Route = { status: number; contentType: string; body: Buffer };
/** Either a static routing table or a handler, for upstreams that need one. */
async function withProxy(
routes: Record<string, Route> | http.RequestListener,
run: (proxyOrigin: string, upstreamOrigin: string) => Promise<void>,
): Promise<void> {
const upstream = http.createServer((req, res) => {
if (typeof routes === 'function') {
routes(req, res);
return;
}
const route = routes[req.url ?? ''];
if (!route) {
res.writeHead(404).end('missing');
return;
}
res.writeHead(route.status, { 'content-type': route.contentType });
res.end(route.body);
});
await new Promise<void>((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 {
await run(proxy.origin, upstreamOrigin);
} finally {
await proxy.close();
await new Promise<void>((resolve) => upstream.close(() => resolve()));
}
}
async function fetchBytes(url: string): Promise<{ status: number; body: Buffer }> {
const response = await fetch(url);
return { status: response.status, body: Buffer.from(await response.arrayBuffer()) };
}
test('proxy strips the PNG disguise off a segment', async () => {
const ts = makeTsPackets(8);
const disguised = Buffer.concat([PNG_HEADER, ts]);
await withProxy(
{ '/video/seg.ts': { status: 200, contentType: 'video/mp2t', body: disguised } },
async (origin) => {
const { status, body } = await fetchBytes(`${origin}/video/seg.ts`);
assert.equal(status, 200);
assert.deepEqual(body, ts);
},
);
});
test('proxy passes a clean segment through unchanged', async () => {
const ts = makeTsPackets(8);
await withProxy(
{ '/video/seg.ts': { status: 200, contentType: 'video/mp2t', body: ts } },
async (origin) => {
const { body } = await fetchBytes(`${origin}/video/seg.ts`);
assert.deepEqual(body, ts);
},
);
});
test('proxy leaves non-TS bodies alone', async () => {
const vtt = Buffer.from('WEBVTT\n\n00:00.000 --> 00:01.000\nhello\n');
await withProxy(
{ '/video/sub.vtt': { status: 200, contentType: 'text/vtt', body: vtt } },
async (origin) => {
const { body } = await fetchBytes(`${origin}/video/sub.vtt`);
assert.deepEqual(body, vtt);
},
);
});
test('proxy rewrites absolute upstream playlist entries to its own origin', async () => {
await withProxy(
(req, res) => {
if (req.url !== '/video/list.m3u8') {
res.writeHead(404).end();
return;
}
// The proxy rewrites the Host header to the upstream it dialled, so this
// is that origin — the one the playlist must not leak to mpv.
res.writeHead(200, { 'content-type': 'application/vnd.apple.mpegurl' });
res.end(`#EXTM3U\n#EXTINF:6,\nhttp://${req.headers.host}/video/abs.ts\n`);
},
async (proxyOrigin, upstreamOrigin) => {
const { body } = await fetchBytes(`${proxyOrigin}/video/list.m3u8`);
const text = body.toString('utf8');
assert.ok(text.includes(`${proxyOrigin}/video/abs.ts`));
assert.ok(!text.includes(upstreamOrigin));
},
);
});
test('proxy strips even when the client asks for a byte range', async () => {
// ffmpeg opens every HLS segment with `Range: bytes=0-`. The proxy drops the
// header, so the upstream answers 200 with the whole body and the strip
// applies; a 206 would have been forwarded untouched.
const ts = makeTsPackets(8);
const disguised = Buffer.concat([PNG_HEADER, ts]);
await withProxy(
(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);
},
async (proxyOrigin) => {
const response = await fetch(`${proxyOrigin}/video/seg.ts`, {
headers: { Range: 'bytes=0-' },
});
const body = Buffer.from(await response.arrayBuffer());
assert.equal(response.status, 200);
assert.deepEqual(body, ts);
},
);
});
test('proxy forwards error statuses without touching the body', async () => {
await withProxy(
{ '/video/gone.ts': { status: 404, contentType: 'text/plain', body: Buffer.from('nope') } },
async (origin) => {
const { status, body } = await fetchBytes(`${origin}/video/gone.ts`);
assert.equal(status, 404);
assert.equal(body.toString('utf8'), 'nope');
},
);
});
/**
* Upstream that fails the first `failures` hits per path, then serves the
* route. Mirrors the bridge right after an episode resolve: mpv's immediate
* segment fetch errors, the same fetch a moment later works.
*/
async function withFlakyUpstream(
failures: number,
failStatus: number,
route: Route,
run: (proxyOrigin: string, hits: () => number) => Promise<void>,
): Promise<void> {
let hits = 0;
const upstream = http.createServer((_req, res) => {
hits += 1;
if (hits <= failures) {
res.writeHead(failStatus, { 'content-type': 'text/plain' }).end('not ready');
return;
}
res.writeHead(route.status, { 'content-type': route.contentType });
res.end(route.body);
});
await new Promise<void>((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 {
await run(proxy.origin, () => hits);
} finally {
await proxy.close();
await new Promise<void>((resolve) => upstream.close(() => resolve()));
}
}
test('proxy retries a failed segment fetch once and serves the retry', async () => {
const ts = makeTsPackets(8);
await withFlakyUpstream(
1,
404,
{ status: 200, contentType: 'video/mp2t', body: ts },
async (origin, hits) => {
const { status, body } = await fetchBytes(`${origin}/video/seg.ts`);
assert.equal(status, 200);
assert.deepEqual(body, ts);
assert.equal(hits(), 2);
},
);
});
test('proxy gives up after one retry and forwards the error', async () => {
await withFlakyUpstream(
Infinity,
503,
{ status: 200, contentType: 'video/mp2t', body: makeTsPackets(8) },
async (origin, hits) => {
const { status } = await fetchBytes(`${origin}/video/seg.ts`);
assert.equal(status, 503);
assert.equal(hits(), 2);
},
);
});