mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-16 13:55:51 -07:00
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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user