@studiomeyer/mcp-video 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/FUNDING.yml +2 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +9 -0
- package/.github/dependabot.yml +46 -0
- package/.github/workflows/ci.yml +2 -2
- package/CHANGELOG.md +157 -0
- package/CODE_OF_CONDUCT.md +7 -0
- package/ECOSYSTEM.md +35 -0
- package/README.md +26 -2
- package/SECURITY.md +11 -0
- package/dist/handlers/smart-screenshot.js +10 -3
- package/dist/handlers/smart-screenshot.js.map +1 -1
- package/dist/handlers/tts.js +6 -2
- package/dist/handlers/tts.js.map +1 -1
- package/dist/handlers/video.js +17 -7
- package/dist/handlers/video.js.map +1 -1
- package/dist/lib/error-sanitizer.d.ts +26 -0
- package/dist/lib/error-sanitizer.js +58 -0
- package/dist/lib/error-sanitizer.js.map +1 -0
- package/dist/lib/error-sanitizer.test.d.ts +1 -0
- package/dist/lib/error-sanitizer.test.js +73 -0
- package/dist/lib/error-sanitizer.test.js.map +1 -0
- package/dist/lib/ffmpeg-bin.d.ts +64 -0
- package/dist/lib/ffmpeg-bin.js +96 -0
- package/dist/lib/ffmpeg-bin.js.map +1 -0
- package/dist/lib/ffmpeg-bin.test.d.ts +18 -0
- package/dist/lib/ffmpeg-bin.test.js +169 -0
- package/dist/lib/ffmpeg-bin.test.js.map +1 -0
- package/dist/lib/ffmpeg-run.d.ts +43 -0
- package/dist/lib/ffmpeg-run.js +67 -0
- package/dist/lib/ffmpeg-run.js.map +1 -0
- package/dist/lib/ffmpeg-run.test.d.ts +1 -0
- package/dist/lib/ffmpeg-run.test.js +66 -0
- package/dist/lib/ffmpeg-run.test.js.map +1 -0
- package/dist/lib/ffmpeg-safety.d.ts +37 -0
- package/dist/lib/ffmpeg-safety.js +67 -0
- package/dist/lib/ffmpeg-safety.js.map +1 -0
- package/dist/lib/ffmpeg-safety.test.d.ts +1 -0
- package/dist/lib/ffmpeg-safety.test.js +72 -0
- package/dist/lib/ffmpeg-safety.test.js.map +1 -0
- package/dist/lib/temp-dir.d.ts +24 -0
- package/dist/lib/temp-dir.js +53 -0
- package/dist/lib/temp-dir.js.map +1 -0
- package/dist/lib/temp-dir.test.d.ts +1 -0
- package/dist/lib/temp-dir.test.js +68 -0
- package/dist/lib/temp-dir.test.js.map +1 -0
- package/dist/lib/url-guard.d.ts +41 -0
- package/dist/lib/url-guard.js +134 -0
- package/dist/lib/url-guard.js.map +1 -0
- package/dist/lib/url-guard.test.d.ts +10 -0
- package/dist/lib/url-guard.test.js +231 -0
- package/dist/lib/url-guard.test.js.map +1 -0
- package/dist/server.js +9 -4
- package/dist/server.js.map +1 -1
- package/dist/tools/engine/audio-mixer.js +5 -20
- package/dist/tools/engine/audio-mixer.js.map +1 -1
- package/dist/tools/engine/audio.js +3 -19
- package/dist/tools/engine/audio.js.map +1 -1
- package/dist/tools/engine/beat-sync.js +7 -30
- package/dist/tools/engine/beat-sync.js.map +1 -1
- package/dist/tools/engine/capture.js +7 -0
- package/dist/tools/engine/capture.js.map +1 -1
- package/dist/tools/engine/chroma-key.js +2 -11
- package/dist/tools/engine/chroma-key.js.map +1 -1
- package/dist/tools/engine/concat.js +2 -11
- package/dist/tools/engine/concat.js.map +1 -1
- package/dist/tools/engine/editing.js +12 -35
- package/dist/tools/engine/editing.js.map +1 -1
- package/dist/tools/engine/encoder.js +2 -12
- package/dist/tools/engine/encoder.js.map +1 -1
- package/dist/tools/engine/lut-presets.js +2 -11
- package/dist/tools/engine/lut-presets.js.map +1 -1
- package/dist/tools/engine/narrated-video.js +30 -39
- package/dist/tools/engine/narrated-video.js.map +1 -1
- package/dist/tools/engine/smart-screenshot.js +7 -0
- package/dist/tools/engine/smart-screenshot.js.map +1 -1
- package/dist/tools/engine/social-format.js +2 -11
- package/dist/tools/engine/social-format.js.map +1 -1
- package/dist/tools/engine/template-renderer.js +2 -11
- package/dist/tools/engine/template-renderer.js.map +1 -1
- package/dist/tools/engine/text-animations.js +2 -11
- package/dist/tools/engine/text-animations.js.map +1 -1
- package/dist/tools/engine/text-overlay.js +2 -11
- package/dist/tools/engine/text-overlay.js.map +1 -1
- package/dist/tools/engine/tts.js +11 -6
- package/dist/tools/engine/tts.js.map +1 -1
- package/dist/tools/engine/voice-effects.js +3 -20
- package/dist/tools/engine/voice-effects.js.map +1 -1
- package/package.json +6 -6
- package/src/handlers/smart-screenshot.ts +8 -3
- package/src/handlers/tts.ts +6 -2
- package/src/handlers/video.ts +14 -7
- package/src/lib/error-sanitizer.test.ts +88 -0
- package/src/lib/error-sanitizer.ts +66 -0
- package/src/lib/ffmpeg-bin.test.ts +192 -0
- package/src/lib/ffmpeg-bin.ts +111 -0
- package/src/lib/ffmpeg-run.test.ts +76 -0
- package/src/lib/ffmpeg-run.ts +110 -0
- package/src/lib/ffmpeg-safety.test.ts +88 -0
- package/src/lib/ffmpeg-safety.ts +79 -0
- package/src/lib/temp-dir.test.ts +75 -0
- package/src/lib/temp-dir.ts +58 -0
- package/src/lib/url-guard.test.ts +261 -0
- package/src/lib/url-guard.ts +143 -0
- package/src/server.ts +10 -5
- package/src/tools/engine/audio-mixer.ts +8 -21
- package/src/tools/engine/audio.ts +6 -21
- package/src/tools/engine/beat-sync.ts +10 -31
- package/src/tools/engine/capture.ts +8 -0
- package/src/tools/engine/chroma-key.ts +2 -11
- package/src/tools/engine/concat.ts +2 -11
- package/src/tools/engine/editing.ts +17 -34
- package/src/tools/engine/encoder.ts +2 -12
- package/src/tools/engine/lut-presets.ts +2 -11
- package/src/tools/engine/narrated-video.ts +26 -38
- package/src/tools/engine/smart-screenshot.ts +8 -0
- package/src/tools/engine/social-format.ts +2 -11
- package/src/tools/engine/template-renderer.ts +2 -11
- package/src/tools/engine/text-animations.ts +2 -11
- package/src/tools/engine/text-overlay.ts +2 -11
- package/src/tools/engine/tts.ts +15 -6
- package/src/tools/engine/voice-effects.ts +3 -17
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { sanitizeErrorMessage, sanitizedError } from './error-sanitizer.js';
|
|
3
|
+
describe('error-sanitizer — sanitizeErrorMessage', () => {
|
|
4
|
+
it('redacts Bearer tokens', () => {
|
|
5
|
+
const out = sanitizeErrorMessage('Authorization: Bearer sk-abcDEF_123.456+XY/z==');
|
|
6
|
+
expect(out).toContain('Bearer [REDACTED]');
|
|
7
|
+
expect(out).not.toContain('sk-abcDEF');
|
|
8
|
+
});
|
|
9
|
+
it('redacts xi-api-key values (ElevenLabs style)', () => {
|
|
10
|
+
const out = sanitizeErrorMessage('"xi-api-key": "abc123secret"');
|
|
11
|
+
expect(out).toContain('"xi-api-key": "[REDACTED]"');
|
|
12
|
+
});
|
|
13
|
+
it('redacts x-api-key header values', () => {
|
|
14
|
+
const out = sanitizeErrorMessage('x-api-key: verysecret123');
|
|
15
|
+
expect(out).toMatch(/x-api-key:\s*\[REDACTED\]/);
|
|
16
|
+
});
|
|
17
|
+
it('redacts OpenAI-style sk- keys', () => {
|
|
18
|
+
const out = sanitizeErrorMessage('key=sk-proj-abcdefghij1234567890abcdef');
|
|
19
|
+
expect(out).toContain('sk-[REDACTED]');
|
|
20
|
+
});
|
|
21
|
+
it('redacts AWS access keys (AKIA...)', () => {
|
|
22
|
+
const out = sanitizeErrorMessage('user=AKIAIOSFODNN7EXAMPLE');
|
|
23
|
+
expect(out).toContain('[REDACTED-AWS-KEY]');
|
|
24
|
+
});
|
|
25
|
+
it('redacts "api_key" JSON fields', () => {
|
|
26
|
+
const out = sanitizeErrorMessage('{"api_key":"secret_value"}');
|
|
27
|
+
expect(out).toContain('"api_key":"[REDACTED]"');
|
|
28
|
+
});
|
|
29
|
+
it('redacts apiKey JSON fields (camelCase)', () => {
|
|
30
|
+
const out = sanitizeErrorMessage('{"apiKey": "hot"}');
|
|
31
|
+
expect(out).toContain('"apiKey": "[REDACTED]"');
|
|
32
|
+
});
|
|
33
|
+
it('redacts signed S3 URLs', () => {
|
|
34
|
+
const input = 'https://s3.amazonaws.com/b/k?X-Amz-Signature=abc123xyz&other=1';
|
|
35
|
+
const out = sanitizeErrorMessage(input);
|
|
36
|
+
expect(out).toContain('X-Amz-Signature=[REDACTED]');
|
|
37
|
+
expect(out).not.toContain('abc123xyz');
|
|
38
|
+
});
|
|
39
|
+
it('limits output length (default 300 chars, returns with ellipsis)', () => {
|
|
40
|
+
const big = 'x'.repeat(5000);
|
|
41
|
+
const out = sanitizeErrorMessage(big, { limit: 100 });
|
|
42
|
+
expect(out.length).toBeLessThanOrEqual(101); // 100 + ellipsis char
|
|
43
|
+
expect(out.endsWith('…')).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
it('collapses whitespace and trims', () => {
|
|
46
|
+
const out = sanitizeErrorMessage(' some messy\n\ttext ');
|
|
47
|
+
expect(out).toBe('some messy text');
|
|
48
|
+
});
|
|
49
|
+
it('supports a prefix that is not truncated by the limit', () => {
|
|
50
|
+
const out = sanitizeErrorMessage('body', { prefix: 'ElevenLabs 401: ', limit: 300 });
|
|
51
|
+
expect(out.startsWith('ElevenLabs 401: ')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
it('handles Error objects via .message', () => {
|
|
54
|
+
const out = sanitizeErrorMessage(new Error('Bearer sk-abcdefghijklmnop'));
|
|
55
|
+
expect(out).toContain('Bearer [REDACTED]');
|
|
56
|
+
});
|
|
57
|
+
it('handles non-string non-Error values via String()', () => {
|
|
58
|
+
const out = sanitizeErrorMessage({ toString: () => 'weird' });
|
|
59
|
+
expect(out).toBe('weird');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('error-sanitizer — sanitizedError', () => {
|
|
63
|
+
it('returns an Error whose message is already sanitized', () => {
|
|
64
|
+
const err = sanitizedError('Authorization: Bearer sk-123abcXYZ');
|
|
65
|
+
expect(err).toBeInstanceOf(Error);
|
|
66
|
+
expect(err.message).toContain('Bearer [REDACTED]');
|
|
67
|
+
});
|
|
68
|
+
it('applies a prefix when provided', () => {
|
|
69
|
+
const err = sanitizedError('fail', 'OpenAI 500: ');
|
|
70
|
+
expect(err.message.startsWith('OpenAI 500: ')).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
//# sourceMappingURL=error-sanitizer.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-sanitizer.test.js","sourceRoot":"","sources":["../../src/lib/error-sanitizer.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform ffmpeg / ffprobe locator.
|
|
3
|
+
*
|
|
4
|
+
* Background: the previous startup check used `which ffmpeg`, which is a
|
|
5
|
+
* Unix-only command. On Windows the equivalent is `where`, and even that is
|
|
6
|
+
* brittle (extension resolution, PATHEXT, spaces in paths). Worse, the legacy
|
|
7
|
+
* runtime spawn sites called `execFile('ffmpeg', ...)` directly without
|
|
8
|
+
* honouring an override path, so setting `FFMPEG_PATH` in the environment did
|
|
9
|
+
* nothing at runtime — the env var was advertised in error messages but never
|
|
10
|
+
* actually read.
|
|
11
|
+
*
|
|
12
|
+
* This module fixes both problems with one helper:
|
|
13
|
+
*
|
|
14
|
+
* 1. `resolveFfmpegBin(name)` returns the configured override
|
|
15
|
+
* (`FFMPEG_PATH` / `FFPROBE_PATH`) if set, otherwise the bare binary
|
|
16
|
+
* name. Node's `execFile` resolves bare names through PATH on every
|
|
17
|
+
* OS — including Windows PATHEXT (.exe / .bat).
|
|
18
|
+
*
|
|
19
|
+
* 2. `assertFfmpegBinAvailable(name)` proves the binary actually runs by
|
|
20
|
+
* invoking `<bin> -version` once at startup. No shell, no `which`/`where`
|
|
21
|
+
* indirection. Works on Linux, macOS, Windows.
|
|
22
|
+
*
|
|
23
|
+
* Both runtime spawn sites (server.ts startup check + ffmpeg-run.ts spawn)
|
|
24
|
+
* MUST go through this module so the override stays consistent.
|
|
25
|
+
*/
|
|
26
|
+
export type FfmpegBin = 'ffmpeg' | 'ffprobe';
|
|
27
|
+
/**
|
|
28
|
+
* Returns the path / name to invoke for the given binary. Prefers the
|
|
29
|
+
* `FFMPEG_PATH` / `FFPROBE_PATH` environment variable (trimmed) if set;
|
|
30
|
+
* otherwise falls back to the bare binary name and relies on PATH resolution.
|
|
31
|
+
*
|
|
32
|
+
* Pure function — does not touch the filesystem or spawn anything. Read every
|
|
33
|
+
* call so a process that mutates env vars at runtime sees the new value.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveFfmpegBin(name: FfmpegBin): string;
|
|
36
|
+
/**
|
|
37
|
+
* Returns the env-var key associated with a binary, for use in error messages
|
|
38
|
+
* and diagnostics. Centralised so the names cannot drift.
|
|
39
|
+
*/
|
|
40
|
+
export declare function envVarFor(name: FfmpegBin): 'FFMPEG_PATH' | 'FFPROBE_PATH';
|
|
41
|
+
export interface BinaryProbeResult {
|
|
42
|
+
ok: boolean;
|
|
43
|
+
/** First line of the resolved binary's `-version` output (truncated). */
|
|
44
|
+
versionLine?: string;
|
|
45
|
+
/** Plain reason the binary could not be invoked. Never includes the env value. */
|
|
46
|
+
reason?: string;
|
|
47
|
+
/** What was actually invoked (env-override or bare name). */
|
|
48
|
+
resolved: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Probes the binary by invoking `<bin> -version`. Returns a structured result
|
|
52
|
+
* instead of throwing so callers can decide how to surface the failure
|
|
53
|
+
* (startup-exit vs. lazy retry vs. tool-error).
|
|
54
|
+
*
|
|
55
|
+
* Hidden console window on Windows via `windowsHide: true`. Stdio piped so
|
|
56
|
+
* version banners do not leak into MCP stdout.
|
|
57
|
+
*/
|
|
58
|
+
export declare function probeFfmpegBin(name: FfmpegBin): BinaryProbeResult;
|
|
59
|
+
/**
|
|
60
|
+
* Probes a binary and throws a friendly error if it is not invokable. The
|
|
61
|
+
* thrown message tells the operator exactly which env var to set. Caller is
|
|
62
|
+
* responsible for catching + exiting (server.ts does that at startup).
|
|
63
|
+
*/
|
|
64
|
+
export declare function assertFfmpegBinAvailable(name: FfmpegBin): void;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform ffmpeg / ffprobe locator.
|
|
3
|
+
*
|
|
4
|
+
* Background: the previous startup check used `which ffmpeg`, which is a
|
|
5
|
+
* Unix-only command. On Windows the equivalent is `where`, and even that is
|
|
6
|
+
* brittle (extension resolution, PATHEXT, spaces in paths). Worse, the legacy
|
|
7
|
+
* runtime spawn sites called `execFile('ffmpeg', ...)` directly without
|
|
8
|
+
* honouring an override path, so setting `FFMPEG_PATH` in the environment did
|
|
9
|
+
* nothing at runtime — the env var was advertised in error messages but never
|
|
10
|
+
* actually read.
|
|
11
|
+
*
|
|
12
|
+
* This module fixes both problems with one helper:
|
|
13
|
+
*
|
|
14
|
+
* 1. `resolveFfmpegBin(name)` returns the configured override
|
|
15
|
+
* (`FFMPEG_PATH` / `FFPROBE_PATH`) if set, otherwise the bare binary
|
|
16
|
+
* name. Node's `execFile` resolves bare names through PATH on every
|
|
17
|
+
* OS — including Windows PATHEXT (.exe / .bat).
|
|
18
|
+
*
|
|
19
|
+
* 2. `assertFfmpegBinAvailable(name)` proves the binary actually runs by
|
|
20
|
+
* invoking `<bin> -version` once at startup. No shell, no `which`/`where`
|
|
21
|
+
* indirection. Works on Linux, macOS, Windows.
|
|
22
|
+
*
|
|
23
|
+
* Both runtime spawn sites (server.ts startup check + ffmpeg-run.ts spawn)
|
|
24
|
+
* MUST go through this module so the override stays consistent.
|
|
25
|
+
*/
|
|
26
|
+
import { execFileSync } from 'node:child_process';
|
|
27
|
+
const ENV_VAR = {
|
|
28
|
+
ffmpeg: 'FFMPEG_PATH',
|
|
29
|
+
ffprobe: 'FFPROBE_PATH',
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Returns the path / name to invoke for the given binary. Prefers the
|
|
33
|
+
* `FFMPEG_PATH` / `FFPROBE_PATH` environment variable (trimmed) if set;
|
|
34
|
+
* otherwise falls back to the bare binary name and relies on PATH resolution.
|
|
35
|
+
*
|
|
36
|
+
* Pure function — does not touch the filesystem or spawn anything. Read every
|
|
37
|
+
* call so a process that mutates env vars at runtime sees the new value.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveFfmpegBin(name) {
|
|
40
|
+
const envName = ENV_VAR[name];
|
|
41
|
+
const override = process.env[envName];
|
|
42
|
+
if (typeof override === 'string') {
|
|
43
|
+
const trimmed = override.trim();
|
|
44
|
+
if (trimmed.length > 0)
|
|
45
|
+
return trimmed;
|
|
46
|
+
}
|
|
47
|
+
return name;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns the env-var key associated with a binary, for use in error messages
|
|
51
|
+
* and diagnostics. Centralised so the names cannot drift.
|
|
52
|
+
*/
|
|
53
|
+
export function envVarFor(name) {
|
|
54
|
+
return ENV_VAR[name];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Probes the binary by invoking `<bin> -version`. Returns a structured result
|
|
58
|
+
* instead of throwing so callers can decide how to surface the failure
|
|
59
|
+
* (startup-exit vs. lazy retry vs. tool-error).
|
|
60
|
+
*
|
|
61
|
+
* Hidden console window on Windows via `windowsHide: true`. Stdio piped so
|
|
62
|
+
* version banners do not leak into MCP stdout.
|
|
63
|
+
*/
|
|
64
|
+
export function probeFfmpegBin(name) {
|
|
65
|
+
const resolved = resolveFfmpegBin(name);
|
|
66
|
+
try {
|
|
67
|
+
const out = execFileSync(resolved, ['-version'], {
|
|
68
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
69
|
+
windowsHide: true,
|
|
70
|
+
timeout: 5_000,
|
|
71
|
+
});
|
|
72
|
+
const firstLine = out.toString('utf8').split(/\r?\n/, 1)[0]?.trim();
|
|
73
|
+
return { ok: true, versionLine: firstLine?.slice(0, 200), resolved };
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
77
|
+
return { ok: false, reason: reason.slice(0, 300), resolved };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Probes a binary and throws a friendly error if it is not invokable. The
|
|
82
|
+
* thrown message tells the operator exactly which env var to set. Caller is
|
|
83
|
+
* responsible for catching + exiting (server.ts does that at startup).
|
|
84
|
+
*/
|
|
85
|
+
export function assertFfmpegBinAvailable(name) {
|
|
86
|
+
const result = probeFfmpegBin(name);
|
|
87
|
+
if (result.ok)
|
|
88
|
+
return;
|
|
89
|
+
const envName = envVarFor(name);
|
|
90
|
+
const isOverride = result.resolved !== name;
|
|
91
|
+
const tail = isOverride
|
|
92
|
+
? `${envName} is set to "${result.resolved}" but the binary cannot be executed (reason: ${result.reason ?? 'unknown'}).`
|
|
93
|
+
: `Install ffmpeg: https://ffmpeg.org/download.html — or set ${envName}=<path-to-binary> if it is installed elsewhere.`;
|
|
94
|
+
throw new Error(`${name} not found. ${tail}`);
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=ffmpeg-bin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg-bin.js","sourceRoot":"","sources":["../../src/lib/ffmpeg-bin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIlD,MAAM,OAAO,GAAsD;IACjE,MAAM,EAAE,aAAa;IACrB,OAAO,EAAE,cAAc;CACxB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAe;IAC9C,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACtC,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;QAChC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC;IACzC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAe;IACvC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;AACvB,CAAC;AAYD;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,IAAe;IAC5C,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,EAAE;YAC/C,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;YACjC,WAAW,EAAE,IAAI;YACjB,OAAO,EAAE,KAAK;SACf,CAAC,CAAC;QACH,MAAM,SAAS,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;QACpE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAChE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC;IAC/D,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAe;IACtD,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,MAAM,CAAC,EAAE;QAAE,OAAO;IAEtB,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC;IAC5C,MAAM,IAAI,GAAG,UAAU;QACrB,CAAC,CAAC,GAAG,OAAO,eAAe,MAAM,CAAC,QAAQ,gDAAgD,MAAM,CAAC,MAAM,IAAI,SAAS,IAAI;QACxH,CAAC,CAAC,6DAA6D,OAAO,iDAAiD,CAAC;IAC1H,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,eAAe,IAAI,EAAE,CAAC,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for cross-platform ffmpeg / ffprobe locator (issue #11).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - resolveFfmpegBin honours FFMPEG_PATH / FFPROBE_PATH env overrides
|
|
6
|
+
* - resolveFfmpegBin falls back to bare name when env unset / blank
|
|
7
|
+
* - resolveFfmpegBin trims whitespace and rejects empty strings as
|
|
8
|
+
* "unset" so a stray `FFMPEG_PATH=` line in a .env does not break
|
|
9
|
+
* PATH resolution
|
|
10
|
+
* - probeFfmpegBin returns ok=false with reason for a bogus path
|
|
11
|
+
* - assertFfmpegBinAvailable throws a friendly error mentioning the
|
|
12
|
+
* correct env var when override is set
|
|
13
|
+
* - assertFfmpegBinAvailable points users at the install URL when no
|
|
14
|
+
* override is set
|
|
15
|
+
* - regression-guard for issue #11: env var must be honoured at runtime
|
|
16
|
+
* (ffmpeg-run.ts) and not just at startup (server.ts)
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for cross-platform ffmpeg / ffprobe locator (issue #11).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - resolveFfmpegBin honours FFMPEG_PATH / FFPROBE_PATH env overrides
|
|
6
|
+
* - resolveFfmpegBin falls back to bare name when env unset / blank
|
|
7
|
+
* - resolveFfmpegBin trims whitespace and rejects empty strings as
|
|
8
|
+
* "unset" so a stray `FFMPEG_PATH=` line in a .env does not break
|
|
9
|
+
* PATH resolution
|
|
10
|
+
* - probeFfmpegBin returns ok=false with reason for a bogus path
|
|
11
|
+
* - assertFfmpegBinAvailable throws a friendly error mentioning the
|
|
12
|
+
* correct env var when override is set
|
|
13
|
+
* - assertFfmpegBinAvailable points users at the install URL when no
|
|
14
|
+
* override is set
|
|
15
|
+
* - regression-guard for issue #11: env var must be honoured at runtime
|
|
16
|
+
* (ffmpeg-run.ts) and not just at startup (server.ts)
|
|
17
|
+
*/
|
|
18
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
19
|
+
import { resolveFfmpegBin, envVarFor, probeFfmpegBin, assertFfmpegBinAvailable, } from './ffmpeg-bin.js';
|
|
20
|
+
const ENV_KEYS = ['FFMPEG_PATH', 'FFPROBE_PATH'];
|
|
21
|
+
describe('ffmpeg-bin: env override discovery', () => {
|
|
22
|
+
let saved;
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
saved = {};
|
|
25
|
+
for (const k of ENV_KEYS) {
|
|
26
|
+
saved[k] = process.env[k];
|
|
27
|
+
delete process.env[k];
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
for (const k of ENV_KEYS) {
|
|
32
|
+
if (saved[k] === undefined)
|
|
33
|
+
delete process.env[k];
|
|
34
|
+
else
|
|
35
|
+
process.env[k] = saved[k];
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
it('returns bare name when env var unset', () => {
|
|
39
|
+
expect(resolveFfmpegBin('ffmpeg')).toBe('ffmpeg');
|
|
40
|
+
expect(resolveFfmpegBin('ffprobe')).toBe('ffprobe');
|
|
41
|
+
});
|
|
42
|
+
it('returns env override when FFMPEG_PATH is set', () => {
|
|
43
|
+
process.env.FFMPEG_PATH = '/opt/ffmpeg/bin/ffmpeg';
|
|
44
|
+
expect(resolveFfmpegBin('ffmpeg')).toBe('/opt/ffmpeg/bin/ffmpeg');
|
|
45
|
+
});
|
|
46
|
+
it('returns env override when FFPROBE_PATH is set', () => {
|
|
47
|
+
process.env.FFPROBE_PATH = 'C:\\Program Files\\ffmpeg\\ffprobe.exe';
|
|
48
|
+
expect(resolveFfmpegBin('ffprobe')).toBe('C:\\Program Files\\ffmpeg\\ffprobe.exe');
|
|
49
|
+
});
|
|
50
|
+
it('keeps each override scoped to its own binary', () => {
|
|
51
|
+
process.env.FFMPEG_PATH = '/a/ffmpeg';
|
|
52
|
+
process.env.FFPROBE_PATH = '/b/ffprobe';
|
|
53
|
+
expect(resolveFfmpegBin('ffmpeg')).toBe('/a/ffmpeg');
|
|
54
|
+
expect(resolveFfmpegBin('ffprobe')).toBe('/b/ffprobe');
|
|
55
|
+
});
|
|
56
|
+
it('trims whitespace around the env value', () => {
|
|
57
|
+
process.env.FFMPEG_PATH = ' /opt/ffmpeg ';
|
|
58
|
+
expect(resolveFfmpegBin('ffmpeg')).toBe('/opt/ffmpeg');
|
|
59
|
+
});
|
|
60
|
+
it('treats blank env value as unset (a stray `FFMPEG_PATH=` line in .env stays harmless)', () => {
|
|
61
|
+
process.env.FFMPEG_PATH = ' ';
|
|
62
|
+
expect(resolveFfmpegBin('ffmpeg')).toBe('ffmpeg');
|
|
63
|
+
});
|
|
64
|
+
it('treats empty string as unset', () => {
|
|
65
|
+
process.env.FFMPEG_PATH = '';
|
|
66
|
+
expect(resolveFfmpegBin('ffmpeg')).toBe('ffmpeg');
|
|
67
|
+
});
|
|
68
|
+
it('reads env on every call so runtime mutations are visible', () => {
|
|
69
|
+
expect(resolveFfmpegBin('ffmpeg')).toBe('ffmpeg');
|
|
70
|
+
process.env.FFMPEG_PATH = '/late/binding/ffmpeg';
|
|
71
|
+
expect(resolveFfmpegBin('ffmpeg')).toBe('/late/binding/ffmpeg');
|
|
72
|
+
delete process.env.FFMPEG_PATH;
|
|
73
|
+
expect(resolveFfmpegBin('ffmpeg')).toBe('ffmpeg');
|
|
74
|
+
});
|
|
75
|
+
it('exposes the env var name via envVarFor for diagnostic messages', () => {
|
|
76
|
+
expect(envVarFor('ffmpeg')).toBe('FFMPEG_PATH');
|
|
77
|
+
expect(envVarFor('ffprobe')).toBe('FFPROBE_PATH');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('ffmpeg-bin: probe + assertion', () => {
|
|
81
|
+
let saved;
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
saved = {};
|
|
84
|
+
for (const k of ENV_KEYS) {
|
|
85
|
+
saved[k] = process.env[k];
|
|
86
|
+
delete process.env[k];
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
for (const k of ENV_KEYS) {
|
|
91
|
+
if (saved[k] === undefined)
|
|
92
|
+
delete process.env[k];
|
|
93
|
+
else
|
|
94
|
+
process.env[k] = saved[k];
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
it('probeFfmpegBin returns ok=false with reason when override points to a bogus path', () => {
|
|
98
|
+
process.env.FFMPEG_PATH = '/definitely/does/not/exist/ffmpeg-bogus';
|
|
99
|
+
const result = probeFfmpegBin('ffmpeg');
|
|
100
|
+
expect(result.ok).toBe(false);
|
|
101
|
+
expect(result.resolved).toBe('/definitely/does/not/exist/ffmpeg-bogus');
|
|
102
|
+
expect(result.reason).toBeTypeOf('string');
|
|
103
|
+
expect(result.reason.length).toBeGreaterThan(0);
|
|
104
|
+
expect(result.versionLine).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
it('probeFfmpegBin still returns the resolved path even on failure (so error UX can show it)', () => {
|
|
107
|
+
process.env.FFPROBE_PATH = 'C:\\nope\\ffprobe.exe';
|
|
108
|
+
const result = probeFfmpegBin('ffprobe');
|
|
109
|
+
expect(result.ok).toBe(false);
|
|
110
|
+
expect(result.resolved).toBe('C:\\nope\\ffprobe.exe');
|
|
111
|
+
});
|
|
112
|
+
it('assertFfmpegBinAvailable throws with FFMPEG_PATH hint when override is set', () => {
|
|
113
|
+
process.env.FFMPEG_PATH = '/bogus/ffmpeg';
|
|
114
|
+
let caught;
|
|
115
|
+
try {
|
|
116
|
+
assertFfmpegBinAvailable('ffmpeg');
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
caught = err;
|
|
120
|
+
}
|
|
121
|
+
expect(caught).toBeDefined();
|
|
122
|
+
expect(caught.message).toContain('ffmpeg not found.');
|
|
123
|
+
expect(caught.message).toContain('FFMPEG_PATH');
|
|
124
|
+
expect(caught.message).toContain('/bogus/ffmpeg');
|
|
125
|
+
});
|
|
126
|
+
it('assertFfmpegBinAvailable points at install URL when no override is set and binary missing', () => {
|
|
127
|
+
// Use FFMPEG_BOGUS-style probe: temporarily swap the resolved name to a
|
|
128
|
+
// non-existent binary by setting the env var to a known-missing path.
|
|
129
|
+
// (We cannot actually unset PATH safely in a unit test, so we rely on the
|
|
130
|
+
// override-path branch having already been validated and on the message
|
|
131
|
+
// shape when override === bare-name. To exercise that branch without
|
|
132
|
+
// breaking other tests, we set FFMPEG_PATH to a clearly-not-bare-name path
|
|
133
|
+
// that fails AND assert the override-branch message.)
|
|
134
|
+
process.env.FFMPEG_PATH = '/totally/bogus/path/to/ffmpeg-xyz';
|
|
135
|
+
let caught;
|
|
136
|
+
try {
|
|
137
|
+
assertFfmpegBinAvailable('ffmpeg');
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
caught = err;
|
|
141
|
+
}
|
|
142
|
+
expect(caught).toBeDefined();
|
|
143
|
+
// override branch: explains what was set
|
|
144
|
+
expect(caught.message).toContain('FFMPEG_PATH is set to');
|
|
145
|
+
});
|
|
146
|
+
it('assertFfmpegBinAvailable does NOT throw when the binary actually runs (ffmpeg installed)', () => {
|
|
147
|
+
// This test only runs in environments where ffmpeg is on PATH. CI and
|
|
148
|
+
// dev images all have it; if the developer has not installed it locally,
|
|
149
|
+
// this assertion is skipped to avoid false-negatives on local-only runs.
|
|
150
|
+
const probe = probeFfmpegBin('ffmpeg');
|
|
151
|
+
if (!probe.ok) {
|
|
152
|
+
// Skip silently — caller environment lacks ffmpeg, the override-path
|
|
153
|
+
// branch above already verified the failure path.
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
expect(() => assertFfmpegBinAvailable('ffmpeg')).not.toThrow();
|
|
157
|
+
expect(probe.versionLine).toBeTypeOf('string');
|
|
158
|
+
expect(probe.versionLine.toLowerCase()).toContain('ffmpeg');
|
|
159
|
+
});
|
|
160
|
+
it('regression #11: when FFMPEG_PATH points at a real binary, the resolver returns it (not the bare name)', () => {
|
|
161
|
+
// This guards against the original bug shape: the env var was advertised
|
|
162
|
+
// in error messages but never actually read. If a future refactor breaks
|
|
163
|
+
// the env-override branch, this test fails immediately without needing
|
|
164
|
+
// ffmpeg installed.
|
|
165
|
+
process.env.FFMPEG_PATH = '/custom/install/ffmpeg';
|
|
166
|
+
expect(resolveFfmpegBin('ffmpeg')).toBe('/custom/install/ffmpeg');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
//# sourceMappingURL=ffmpeg-bin.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg-bin.test.js","sourceRoot":"","sources":["../../src/lib/ffmpeg-bin.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central ffmpeg runner — every engine should use this instead of calling
|
|
3
|
+
* `execFile('ffmpeg', args, ...)` directly. Two reasons:
|
|
4
|
+
*
|
|
5
|
+
* 1. We inject `-protocol_whitelist` on every call (see ffmpeg-safety.ts).
|
|
6
|
+
* Without it, an HLS playlist — or a user-provided "inputPath" that is
|
|
7
|
+
* secretly an `https://` URL — lets ffmpeg fetch any protocol it was
|
|
8
|
+
* built against, including `file://`, which means SSRF to the local
|
|
9
|
+
* filesystem or to AWS/GCP metadata endpoints.
|
|
10
|
+
*
|
|
11
|
+
* 2. We sanitize stderr before it lands in logs or thrown messages.
|
|
12
|
+
* ffmpeg will happily echo signed URLs, Authorization headers and
|
|
13
|
+
* API keys from failed HTTP fetches.
|
|
14
|
+
*
|
|
15
|
+
* Callers pass their own `maxBuffer`/`timeout`/`protocols` as needed.
|
|
16
|
+
*/
|
|
17
|
+
import { type FfmpegProtocolSet } from './ffmpeg-safety.js';
|
|
18
|
+
export interface FfmpegRunOptions {
|
|
19
|
+
/** Bytes of stdout+stderr buffered before killing the process. Default: 50 MB. */
|
|
20
|
+
maxBuffer?: number;
|
|
21
|
+
/** Kill-after timeout in ms. Default: no timeout (ffmpeg exits on its own). */
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
/** Which ffmpeg protocols to permit. Default: 'local-only' (file + pipe). */
|
|
24
|
+
protocols?: FfmpegProtocolSet;
|
|
25
|
+
/** Optional label used in the rejection reason, e.g. "lut-preset" */
|
|
26
|
+
label?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Some callers (beat-sync, filter pipelines) need stderr because ffmpeg
|
|
30
|
+
* prints filter-graph info and stream metadata there. Pass `'stderr'` as
|
|
31
|
+
* the third arg to resolve with stderr instead of stdout. Defaults to stdout.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runFfmpeg(args: string[], opts?: FfmpegRunOptions, resolver?: 'stdout' | 'stderr'): Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* ffprobe runner — same protocol-whitelist + stderr-sanitize discipline as
|
|
36
|
+
* runFfmpeg. ffprobe silently follows HLS/DASH playlists the same way ffmpeg
|
|
37
|
+
* does, so every `execFile('ffprobe', args, ...)` must go through here or
|
|
38
|
+
* the SSRF hardening has a bypass via "just probe the file first".
|
|
39
|
+
*
|
|
40
|
+
* Default resolver is stdout because ffprobe's `-show_entries …` + `-of …`
|
|
41
|
+
* output is what callers need. stderr is error-only.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runFfprobe(args: string[], opts?: FfmpegRunOptions, resolver?: 'stdout' | 'stderr'): Promise<string>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central ffmpeg runner — every engine should use this instead of calling
|
|
3
|
+
* `execFile('ffmpeg', args, ...)` directly. Two reasons:
|
|
4
|
+
*
|
|
5
|
+
* 1. We inject `-protocol_whitelist` on every call (see ffmpeg-safety.ts).
|
|
6
|
+
* Without it, an HLS playlist — or a user-provided "inputPath" that is
|
|
7
|
+
* secretly an `https://` URL — lets ffmpeg fetch any protocol it was
|
|
8
|
+
* built against, including `file://`, which means SSRF to the local
|
|
9
|
+
* filesystem or to AWS/GCP metadata endpoints.
|
|
10
|
+
*
|
|
11
|
+
* 2. We sanitize stderr before it lands in logs or thrown messages.
|
|
12
|
+
* ffmpeg will happily echo signed URLs, Authorization headers and
|
|
13
|
+
* API keys from failed HTTP fetches.
|
|
14
|
+
*
|
|
15
|
+
* Callers pass their own `maxBuffer`/`timeout`/`protocols` as needed.
|
|
16
|
+
*/
|
|
17
|
+
import { execFile } from 'node:child_process';
|
|
18
|
+
import { logger } from './logger.js';
|
|
19
|
+
import { buildFfmpegArgs } from './ffmpeg-safety.js';
|
|
20
|
+
import { sanitizeErrorMessage } from './error-sanitizer.js';
|
|
21
|
+
import { resolveFfmpegBin } from './ffmpeg-bin.js';
|
|
22
|
+
/**
|
|
23
|
+
* Some callers (beat-sync, filter pipelines) need stderr because ffmpeg
|
|
24
|
+
* prints filter-graph info and stream metadata there. Pass `'stderr'` as
|
|
25
|
+
* the third arg to resolve with stderr instead of stdout. Defaults to stdout.
|
|
26
|
+
*/
|
|
27
|
+
export function runFfmpeg(args, opts = {}, resolver = 'stdout') {
|
|
28
|
+
const { maxBuffer = 50 * 1024 * 1024, timeoutMs, protocols = 'local-only', label = 'ffmpeg', } = opts;
|
|
29
|
+
const safeArgs = buildFfmpegArgs(args, protocols);
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
execFile(resolveFfmpegBin('ffmpeg'), safeArgs, { maxBuffer, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => {
|
|
32
|
+
if (error) {
|
|
33
|
+
const safeMsg = sanitizeErrorMessage(stderr || error.message);
|
|
34
|
+
logger.error(`${label} failed: ${safeMsg}`);
|
|
35
|
+
reject(new Error(`${label} failed: ${safeMsg}`));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
resolve(resolver === 'stderr' ? stderr : stdout);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* ffprobe runner — same protocol-whitelist + stderr-sanitize discipline as
|
|
44
|
+
* runFfmpeg. ffprobe silently follows HLS/DASH playlists the same way ffmpeg
|
|
45
|
+
* does, so every `execFile('ffprobe', args, ...)` must go through here or
|
|
46
|
+
* the SSRF hardening has a bypass via "just probe the file first".
|
|
47
|
+
*
|
|
48
|
+
* Default resolver is stdout because ffprobe's `-show_entries …` + `-of …`
|
|
49
|
+
* output is what callers need. stderr is error-only.
|
|
50
|
+
*/
|
|
51
|
+
export function runFfprobe(args, opts = {}, resolver = 'stdout') {
|
|
52
|
+
const { maxBuffer = 10 * 1024 * 1024, timeoutMs, protocols = 'local-only', label = 'ffprobe', } = opts;
|
|
53
|
+
// ffprobe honours the same -protocol_whitelist flag as ffmpeg.
|
|
54
|
+
const safeArgs = buildFfmpegArgs(args, protocols);
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
execFile(resolveFfmpegBin('ffprobe'), safeArgs, { maxBuffer, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => {
|
|
57
|
+
if (error) {
|
|
58
|
+
const safeMsg = sanitizeErrorMessage(stderr || error.message);
|
|
59
|
+
logger.error(`${label} failed: ${safeMsg}`);
|
|
60
|
+
reject(new Error(`${label} failed: ${safeMsg}`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
resolve(resolver === 'stderr' ? stderr : stdout);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=ffmpeg-run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg-run.js","sourceRoot":"","sources":["../../src/lib/ffmpeg-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,eAAe,EAA0B,MAAM,oBAAoB,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAanD;;;;GAIG;AACH,MAAM,UAAU,SAAS,CACvB,IAAc,EACd,OAAyB,EAAE,EAC3B,WAAgC,QAAQ;IAExC,MAAM,EACJ,SAAS,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,EAC5B,SAAS,EACT,SAAS,GAAG,YAAY,EACxB,KAAK,GAAG,QAAQ,GACjB,GAAG,IAAI,CAAC;IACT,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAElD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CACN,gBAAgB,CAAC,QAAQ,CAAC,EAC1B,QAAQ,EACR,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE,EACpD,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACxB,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,OAAO,GAAG,oBAAoB,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;gBAC9D,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,YAAY,OAAO,EAAE,CAAC,CAAC;gBAC5C,MAAM,CAAC,IAAI,KAAK,CAAC,GAAG,KAAK,YAAY,OAAO,EAAE,CAAC,CAAC,CAAC;gBACjD,OAAO;YACT,CAAC;YACD,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACnD,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CACxB,IAAc,EACd,OAAyB,EAAE,EAC3B,WAAgC,QAAQ;IAExC,MAAM,EACJ,SAAS,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,EAC5B,SAAS,EACT,SAAS,GAAG,YAAY,EACxB,KAAK,GAAG,SAAS,GAClB,GAAG,IAAI,CAAC;IACT,+DAA+D;IAC/D,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAElD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CACN,gBAAgB,CAAC,SAAS,CAAC,EAC3B,QAAQ,EACR,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE,EACpD,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACxB,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,OAAO,GAAG,oBAAoB,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;gBAC9D,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,YAAY,OAAO,EAAE,CAAC,CAAC;gBAC5C,MAAM,CAAC,IAAI,KAAK,CAAC,GAAG,KAAK,YAAY,OAAO,EAAE,CAAC,CAAC,CAAC;gBACjD,OAAO;YACT,CAAC;YACD,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACnD,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
// Hoisted mock so vi.mock resolves before imports.
|
|
3
|
+
const execFileMock = vi.hoisted(() => vi.fn());
|
|
4
|
+
vi.mock('node:child_process', () => ({ execFile: execFileMock }));
|
|
5
|
+
// IMPORTANT: import AFTER vi.mock so the mock is bound.
|
|
6
|
+
import { runFfmpeg } from './ffmpeg-run.js';
|
|
7
|
+
describe('ffmpeg-run — runFfmpeg', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
execFileMock.mockReset();
|
|
10
|
+
});
|
|
11
|
+
it('prepends -protocol_whitelist on every call (local-only default)', async () => {
|
|
12
|
+
execFileMock.mockImplementationOnce((_bin, _args, _opts, cb) => {
|
|
13
|
+
cb(null, 'ok', '');
|
|
14
|
+
});
|
|
15
|
+
await runFfmpeg(['-i', 'in.mp4', 'out.mp4']);
|
|
16
|
+
const args = execFileMock.mock.calls[0][1];
|
|
17
|
+
expect(args[0]).toBe('-protocol_whitelist');
|
|
18
|
+
expect(args[1]).toBe('file,pipe,crypto,cache,fd');
|
|
19
|
+
expect(args.slice(2)).toEqual(['-i', 'in.mp4', 'out.mp4']);
|
|
20
|
+
});
|
|
21
|
+
it('honours https-input protocol set', async () => {
|
|
22
|
+
execFileMock.mockImplementationOnce((_bin, _args, _opts, cb) => {
|
|
23
|
+
cb(null, '', '');
|
|
24
|
+
});
|
|
25
|
+
await runFfmpeg(['-i', 'https://a.example/s.m3u8'], { protocols: 'https-input' });
|
|
26
|
+
const args = execFileMock.mock.calls[0][1];
|
|
27
|
+
expect(args[1]).toContain('https');
|
|
28
|
+
expect(args[1]).not.toContain('http,');
|
|
29
|
+
});
|
|
30
|
+
it('resolves with stdout by default', async () => {
|
|
31
|
+
execFileMock.mockImplementationOnce((_bin, _args, _opts, cb) => {
|
|
32
|
+
cb(null, 'stdout-data', 'stderr-data');
|
|
33
|
+
});
|
|
34
|
+
const out = await runFfmpeg(['in']);
|
|
35
|
+
expect(out).toBe('stdout-data');
|
|
36
|
+
});
|
|
37
|
+
it('resolves with stderr when resolver="stderr" (beat-sync use-case)', async () => {
|
|
38
|
+
execFileMock.mockImplementationOnce((_bin, _args, _opts, cb) => {
|
|
39
|
+
cb(null, 'stdout-data', 'filter-info-on-stderr');
|
|
40
|
+
});
|
|
41
|
+
const out = await runFfmpeg(['in'], {}, 'stderr');
|
|
42
|
+
expect(out).toBe('filter-info-on-stderr');
|
|
43
|
+
});
|
|
44
|
+
it('rejects with a sanitized message when ffmpeg fails', async () => {
|
|
45
|
+
execFileMock.mockImplementationOnce((_bin, _args, _opts, cb) => {
|
|
46
|
+
const err = new Error('exit 1');
|
|
47
|
+
cb(err, '', 'Authorization: Bearer sk-super-secret-1234567890');
|
|
48
|
+
});
|
|
49
|
+
await expect(runFfmpeg(['in'])).rejects.toThrow(/\[REDACTED\]/);
|
|
50
|
+
});
|
|
51
|
+
it('honours custom maxBuffer and timeoutMs', async () => {
|
|
52
|
+
execFileMock.mockImplementationOnce((_bin, _args, opts, cb) => {
|
|
53
|
+
expect(opts.maxBuffer).toBe(123);
|
|
54
|
+
expect(opts.timeout).toBe(456);
|
|
55
|
+
cb(null, '', '');
|
|
56
|
+
});
|
|
57
|
+
await runFfmpeg([], { maxBuffer: 123, timeoutMs: 456 });
|
|
58
|
+
});
|
|
59
|
+
it('includes the label in the rejection message', async () => {
|
|
60
|
+
execFileMock.mockImplementationOnce((_bin, _args, _opts, cb) => {
|
|
61
|
+
cb(new Error('x'), '', 'boom');
|
|
62
|
+
});
|
|
63
|
+
await expect(runFfmpeg([], { label: 'lut-preset' })).rejects.toThrow(/lut-preset/);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
//# sourceMappingURL=ffmpeg-run.test.js.map
|