mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-29 19:21:33 -07:00
feat(youtube): add mediaCache mode and safer stream media extraction (#130)
This commit is contained in:
+132
-3
@@ -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/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user