fix: synth silence bed for audio-less produce sources
Audio-less inputs no longer emit [0:a] maps that ffmpeg rejects; the worker branches to an anullsrc bed configured at the output preset's rate/layout. Producer tests 42/42.
This commit is contained in:
parent
613aba236b
commit
5808e89dde
6 changed files with 130 additions and 8 deletions
|
|
@ -43,6 +43,17 @@ function makeClip(path: string): void {
|
|||
|
||||
function key(bytes: Buffer): string { return `sha256/${createHash("sha256").update(bytes).digest("hex")}.mp4`; }
|
||||
|
||||
/**
|
||||
* The audio-less class from the bug report: video only, no lavfi sine, no audio
|
||||
* track at all (muted screen recording / GIF conversion / synthetic test clip).
|
||||
*/
|
||||
function makeSilentClip(path: string): void {
|
||||
const r = spawnSync("ffmpeg", ["-hide_banner", "-loglevel", "error", "-y",
|
||||
"-f", "lavfi", "-i", "testsrc2=s=320x180:r=30:d=2",
|
||||
"-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", "-movflags", "+faststart", path]);
|
||||
if (r.status !== 0) throw new Error(`silent fixture encode failed: ${r.stderr?.toString() ?? ""}`);
|
||||
}
|
||||
|
||||
function doubles() {
|
||||
const events: Array<{ type: string; jobId?: string; detail?: string }> = [];
|
||||
const jobs: Array<{ jobId: string; state: string; reason?: string; output?: { storageKey: string; producedAssetId: string } }> = [];
|
||||
|
|
@ -132,6 +143,32 @@ test("compose job: encode, publish, derived asset, complete", { skip: !hasFfmpeg
|
|||
assert.ok(body !== undefined && body.subarray(4, 8).toString("latin1") === "ftyp");
|
||||
});
|
||||
|
||||
test("an audio-less source produces output WITH audio (synthetic bed), not a [0:a] crash", { skip: !hasFfmpeg() || !hasFont() ? "needs ffmpeg + a ttf font" : false }, async () => {
|
||||
const d = doubles();
|
||||
const clip = join(SCRATCH, "source-silent.mp4");
|
||||
makeSilentClip(clip);
|
||||
const bytes = await import("node:fs/promises").then((fs) => fs.readFile(clip));
|
||||
const inputKey = key(bytes);
|
||||
d.objects.set(inputKey, bytes);
|
||||
// hasAudio:false is what the pg store reports for a probed audio-less asset.
|
||||
d.setQueue([job(inputKey, "compose",
|
||||
{ overlays: [{ kind: "lower_third", text: "Ada Lovelace", position: { x: 8, y: 85 } }], lane: "local" },
|
||||
{ hasAudio: false, durationMs: 2000 })]);
|
||||
|
||||
await runner(d).pollOnce();
|
||||
|
||||
assert.equal(d.jobs[0]?.state, "complete", d.jobs[0]?.reason);
|
||||
assert.ok(d.jobs[0]?.output?.storageKey.startsWith("sha256/"));
|
||||
// The produced mp4 must actually carry an audio stream — the whole point of the bed.
|
||||
const body = d.objects.get(d.published()!.key);
|
||||
assert.ok(body !== undefined && body.subarray(4, 8).toString("latin1") === "ftyp");
|
||||
const probe = spawnSync("ffprobe", ["-v", "error", "-print_format", "json", "-show_streams", "-i", `pipe:0`],
|
||||
{ input: body, encoding: "buffer" });
|
||||
const streams = JSON.parse(probe.stdout.toString("utf8")).streams as Array<{ codec_type: string }>;
|
||||
assert.ok(streams.some((s) => s.codec_type === "audio"), "output must have an audio stream");
|
||||
assert.ok(streams.some((s) => s.codec_type === "video"), "output must keep its video stream");
|
||||
});
|
||||
|
||||
test("a mix job with a voiceover runs the ducking path end to end", { skip: !hasFfmpeg() || !hasFont() ? "needs ffmpeg + a ttf font" : false }, async () => {
|
||||
const d = doubles();
|
||||
const clip = join(SCRATCH, "source-mix.mp4");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue