feat(youtube): add mediaCache mode and safer stream media extraction (#130)

This commit is contained in:
2026-06-27 19:20:18 -07:00
committed by GitHub
parent d199376364
commit 5326ad32f5
52 changed files with 4065 additions and 165 deletions
+132 -3
View File
@@ -8,6 +8,10 @@ import { buildAnimatedImageVideoFilter, MediaGenerator } from './media-generator
async function withStubbedFfmpeg(
run: (generator: MediaGenerator, argsPath: string) => Promise<void>,
options: {
logDebug?: (message: string) => void;
now?: () => number;
} = {},
): Promise<void> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-media-generator-test-'));
const binDir = path.join(root, 'bin');
@@ -25,7 +29,7 @@ async function withStubbedFfmpeg(
" console.log(' V..... libaom-av1');",
' process.exit(0);',
'}',
"fs.writeFileSync(process.env.SUBMINER_TEST_FFMPEG_ARGS, `${args.join('\\n')}\\n`, 'utf8');",
"fs.writeFileSync(process.env.SUBMINER_TEST_FFMPEG_ARGS, JSON.stringify(args), 'utf8');",
'const outputPath = args.at(-1);',
"fs.writeFileSync(outputPath, 'avif', 'utf8');",
].join('\n'),
@@ -44,7 +48,7 @@ async function withStubbedFfmpeg(
const originalArgsPath = process.env.SUBMINER_TEST_FFMPEG_ARGS;
process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ''}`;
process.env.SUBMINER_TEST_FFMPEG_ARGS = argsPath;
const generator = new MediaGenerator(tempDir);
const generator = new MediaGenerator(tempDir, options);
try {
await run(generator, argsPath);
@@ -61,7 +65,7 @@ async function withStubbedFfmpeg(
}
function readFfmpegArgs(argsPath: string): string[] {
return fs.readFileSync(argsPath, 'utf8').trim().split('\n');
return JSON.parse(fs.readFileSync(argsPath, 'utf8')) as string[];
}
test('buildAnimatedImageVideoFilter holds lead-in until the next frame after the audio boundary', () => {
@@ -182,3 +186,128 @@ test('generateAudio recreates missing temp directory before invoking ffmpeg', as
assert.equal(fs.existsSync(path.dirname(outputPath!)), true);
});
});
test('generateAudio adds remote input options before the ffmpeg input', async () => {
await withStubbedFfmpeg(async (generator, argsPath) => {
await generator.generateAudio(
{
path: 'https://rr1---sn.example.googlevideo.com/videoplayback?mime=audio%2Fwebm',
inputOptions: {
reconnect: true,
userAgent: 'Mozilla/5.0',
headers: {
Referer: 'https://www.youtube.com/',
Origin: 'https://www.youtube.com',
},
},
},
10,
12,
);
const args = readFfmpegArgs(argsPath);
const inputIndex = args.indexOf('-i');
assert.ok(inputIndex > 0);
assert.ok(args.indexOf('-reconnect') > -1);
assert.ok(args.indexOf('-reconnect') < inputIndex);
assert.equal(args[args.indexOf('-reconnect') + 1], '1');
assert.equal(args[args.indexOf('-reconnect_streamed') + 1], '1');
assert.equal(args[args.indexOf('-reconnect_on_network_error') + 1], '1');
assert.equal(args[args.indexOf('-reconnect_on_http_error') + 1], '403,5xx');
assert.equal(args[args.indexOf('-reconnect_delay_max') + 1], '5');
assert.equal(args[args.indexOf('-user_agent') + 1], 'Mozilla/5.0');
assert.equal(
args[args.indexOf('-headers') + 1],
'Referer: https://www.youtube.com/\r\nOrigin: https://www.youtube.com\r\n',
);
});
});
test('generateAudio skips stale audio stream maps for single resolved streams', async () => {
await withStubbedFfmpeg(async (generator, argsPath) => {
await generator.generateAudio(
{
path: 'https://rr1---sn.example.googlevideo.com/videoplayback?mime=audio%2Fwebm',
singleResolvedStream: true,
},
10,
12,
0,
22,
);
const args = readFfmpegArgs(argsPath);
assert.equal(args.includes('-map'), false);
});
});
test('generateAudio keeps explicit audio stream maps for normal media paths', async () => {
await withStubbedFfmpeg(async (generator, argsPath) => {
await generator.generateAudio('/video.mp4', 10, 12, 0, 2);
const args = readFfmpegArgs(argsPath);
assert.equal(args[args.indexOf('-map') + 1], '0:2');
});
});
test('generateAudio debug-logs cached input and completion timing', async () => {
const logs: string[] = [];
const times = [1000, 1052];
await withStubbedFfmpeg(
async (generator) => {
await generator.generateAudio(
{
path: '/tmp/subminer-youtube-media-cache/abc123/media.mkv',
source: 'youtube-cache',
},
10,
12,
);
},
{
logDebug: (message) => logs.push(message),
now: () => times.shift() ?? 1052,
},
);
assert.match(logs.join('\n'), /\[media-generator\] audio start/);
assert.match(logs.join('\n'), /source=youtube-cache/);
assert.match(
logs.join('\n'),
/input=local:\/tmp\/subminer-youtube-media-cache\/abc123\/media\.mkv/,
);
assert.match(logs.join('\n'), /\[media-generator\] audio complete/);
assert.match(logs.join('\n'), /elapsedMs=52/);
assert.match(logs.join('\n'), /bytes=4/);
});
test('generateAudio debug logs sanitize remote inputs', async () => {
const logs: string[] = [];
const times = [1000, 1003];
await withStubbedFfmpeg(
async (generator) => {
await generator.generateAudio(
{
path: 'https://rr1---sn.example.googlevideo.com/videoplayback?signature=secret&expire=123',
inputOptions: {
reconnect: true,
headers: {
Referer: 'https://www.youtube.com/watch?v=abc123',
},
},
},
10,
12,
);
},
{
logDebug: (message) => logs.push(message),
now: () => times.shift() ?? 1003,
},
);
assert.match(logs.join('\n'), /input=remote:rr1---sn\.example\.googlevideo\.com/);
assert.doesNotMatch(logs.join('\n'), /signature=secret|expire=123|Referer|abc123/);
});