mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-27 16:49:51 -07:00
feat(youtube): add mediaCache mode and safer stream media extraction (#130)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
export interface MediaInputOptions {
|
||||
reconnect?: boolean;
|
||||
userAgent?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type MediaInput =
|
||||
| string
|
||||
| {
|
||||
path: string;
|
||||
source?: string;
|
||||
inputOptions?: MediaInputOptions;
|
||||
singleResolvedStream?: boolean;
|
||||
};
|
||||
|
||||
export type NormalizedMediaInput = {
|
||||
path: string;
|
||||
inputArgs: string[];
|
||||
singleResolvedStream: boolean;
|
||||
};
|
||||
|
||||
const BLOCKED_FFMPEG_HEADER_NAMES = new Set(['authorization', 'cookie', 'proxy-authorization']);
|
||||
|
||||
function trimToNonEmptyString(value: unknown): string | null {
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function normalizeHeaderName(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
if (!/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
if (BLOCKED_FFMPEG_HEADER_NAMES.has(trimmed.toLowerCase())) {
|
||||
return null;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function serializeFfmpegHeaders(headers: Record<string, string> | undefined): string | null {
|
||||
if (!headers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
for (const [rawName, rawValue] of Object.entries(headers)) {
|
||||
const name = normalizeHeaderName(rawName);
|
||||
const value = trimToNonEmptyString(rawValue);
|
||||
if (!name || !value) {
|
||||
continue;
|
||||
}
|
||||
lines.push(`${name}: ${value.replace(/[\r\n]+/g, ' ')}`);
|
||||
}
|
||||
|
||||
return lines.length > 0 ? `${lines.join('\r\n')}\r\n` : null;
|
||||
}
|
||||
|
||||
export function normalizeMediaInput(input: MediaInput): NormalizedMediaInput {
|
||||
if (typeof input === 'string') {
|
||||
return { path: input, inputArgs: [], singleResolvedStream: false };
|
||||
}
|
||||
|
||||
const inputArgs: string[] = [];
|
||||
if (input.inputOptions?.reconnect) {
|
||||
inputArgs.push(
|
||||
'-reconnect',
|
||||
'1',
|
||||
'-reconnect_streamed',
|
||||
'1',
|
||||
'-reconnect_on_network_error',
|
||||
'1',
|
||||
'-reconnect_on_http_error',
|
||||
'403,5xx',
|
||||
'-reconnect_delay_max',
|
||||
'5',
|
||||
);
|
||||
}
|
||||
|
||||
const userAgent = trimToNonEmptyString(input.inputOptions?.userAgent);
|
||||
if (userAgent) {
|
||||
inputArgs.push('-user_agent', userAgent);
|
||||
}
|
||||
|
||||
const headers = serializeFfmpegHeaders(input.inputOptions?.headers);
|
||||
if (headers) {
|
||||
inputArgs.push('-headers', headers);
|
||||
}
|
||||
|
||||
return {
|
||||
path: input.path,
|
||||
inputArgs,
|
||||
singleResolvedStream: input.singleResolvedStream === true,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user