fix(anime): alias disguised HLS segments for ffmpeg

- Expose local `.ts` aliases for `.image` MPEG-TS segments
- Cover proxy rewriting and upstream alias removal with tests
This commit is contained in:
2026-08-15 16:01:10 -07:00
parent 333ee5eea4
commit cc43d3007a
4 changed files with 87 additions and 3 deletions
@@ -6,6 +6,7 @@ import {
findTsSyncOffset,
rewritePlaylistOrigins,
startStreamStripProxy,
TS_SEGMENT_ALIAS_SUFFIX,
TS_PACKET_LENGTH,
} from './stream-strip-proxy';
@@ -60,6 +61,25 @@ test('rewritePlaylistOrigins swaps absolute upstream URLs and keeps relative lin
assert.ok(!rewritten.includes('41569'));
});
test('rewritePlaylistOrigins gives proxied image segments an ffmpeg-safe TS suffix', () => {
const body = [
'#EXTM3U',
'#EXTINF:6.006,',
'/video/relative.image?token=one',
'#EXTINF:4.463,',
'http://127.0.0.1:41569/video/absolute.image',
'#EXTINF:3,',
'https://cdn.example/video/external.image',
].join('\n');
const rewritten = rewritePlaylistOrigins(body, 'http://127.0.0.1:41569', 'http://127.0.0.1:9999');
assert.ok(rewritten.includes(`/video/relative.image${TS_SEGMENT_ALIAS_SUFFIX}?token=one`));
assert.ok(
rewritten.includes(`http://127.0.0.1:9999/video/absolute.image${TS_SEGMENT_ALIAS_SUFFIX}`),
);
assert.ok(rewritten.includes('https://cdn.example/video/external.image'));
});
/* ---------- proxy end-to-end ---------- */
type Route = { status: number; contentType: string; body: Buffer };
@@ -158,6 +178,32 @@ test('proxy rewrites absolute upstream playlist entries to its own origin', asyn
);
});
test('proxy aliases image segment URLs and removes the alias before fetching upstream', async () => {
const ts = makeTsPackets(8);
const disguised = Buffer.concat([PNG_HEADER, ts]);
await withProxy(
{
'/video/list.m3u8': {
status: 200,
contentType: 'application/vnd.apple.mpegurl',
body: Buffer.from('#EXTM3U\n#EXTINF:6,\n/video/seg.image\n'),
},
'/video/seg.image': { status: 200, contentType: 'image/png', body: disguised },
},
async (proxyOrigin) => {
const playlist = await fetch(`${proxyOrigin}/video/list.m3u8`).then((response) =>
response.text(),
);
const segmentPath = playlist.split('\n').find((line) => line.startsWith('/video/'));
assert.equal(segmentPath, `/video/seg.image${TS_SEGMENT_ALIAS_SUFFIX}`);
const { status, body } = await fetchBytes(`${proxyOrigin}${segmentPath}`);
assert.equal(status, 200);
assert.deepEqual(body, ts);
},
);
});
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
+37 -1
View File
@@ -23,6 +23,12 @@ const TS_SYNC_BYTE = 0x47;
* row at 188-byte strides do not.
*/
const SYNC_RUN = 5;
/**
* FFmpeg 8.1 rejects HLS media whose URL suffix is not in its segment allowlist.
* The disguised MPEG-TS segments seen in the wild use `.image`, so the local
* playlist gives them this safe alias and removes it again before forwarding.
*/
export const TS_SEGMENT_ALIAS_SUFFIX = '.subminer.ts';
/** A disguise prefix is small; give up scanning after this much. */
export const DEFAULT_SCAN_LIMIT_BYTES = 1024 * 1024;
/** Bytes needed to either find a run within the limit or rule one out. */
@@ -61,7 +67,36 @@ export function rewritePlaylistOrigins(
upstreamOrigin: string,
proxyOrigin: string,
): string {
return body.split(upstreamOrigin).join(proxyOrigin);
const rebased = body.split(upstreamOrigin).join(proxyOrigin);
return rebased
.split(/(\r?\n)/)
.map((line) => {
const uri = line.trim();
if (!uri || uri.startsWith('#')) return line;
let resolved: URL;
try {
resolved = new URL(uri, proxyOrigin);
} catch {
return line;
}
if (resolved.origin !== proxyOrigin || !resolved.pathname.toLowerCase().endsWith('.image')) {
return line;
}
const queryIndex = uri.search(/[?#]/);
const aliasIndex = queryIndex === -1 ? uri.length : queryIndex;
const leadingWhitespace = line.slice(0, line.indexOf(uri));
const trailingWhitespace = line.slice(leadingWhitespace.length + uri.length);
return `${leadingWhitespace}${uri.slice(0, aliasIndex)}${TS_SEGMENT_ALIAS_SUFFIX}${uri.slice(aliasIndex)}${trailingWhitespace}`;
})
.join('');
}
function removeTsSegmentAlias(url: URL): void {
if (url.pathname.endsWith(TS_SEGMENT_ALIAS_SUFFIX)) {
url.pathname = url.pathname.slice(0, -TS_SEGMENT_ALIAS_SUFFIX.length);
}
}
export interface StreamStripProxyOptions {
@@ -118,6 +153,7 @@ export function startStreamStripProxy(
let upstreamUrl: URL;
try {
upstreamUrl = new URL(req.url ?? '/', options.upstreamOrigin());
removeTsSegmentAlias(upstreamUrl);
} catch {
res.writeHead(502).end();
return;