From 3bda0b9c8706b437fd999cb9b031d7dd121d5fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sun, 12 Jul 2026 09:27:43 +0000 Subject: [PATCH] fix(render): reject incomplete captured frames --- .../src/services/renderOrchestrator.test.ts | 19 +++++++++++++++++-- .../src/services/renderOrchestrator.ts | 7 ++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/producer/src/services/renderOrchestrator.test.ts b/packages/producer/src/services/renderOrchestrator.test.ts index 2472932db9..88db3e2ea1 100644 --- a/packages/producer/src/services/renderOrchestrator.test.ts +++ b/packages/producer/src/services/renderOrchestrator.test.ts @@ -230,7 +230,7 @@ describe("executeDiskCaptureWithAdaptiveRetry — transient Target-closed single const writeAllFrames = (framesDir: string, totalFrames: number): void => { for (let i = 0; i < totalFrames; i++) { - writeFileSync(join(framesDir, formatCaptureFrameName(i, "jpg")), "x"); + writeFileSync(join(framesDir, formatCaptureFrameName(i, "jpg")), "captured-frame"); } }; @@ -1432,7 +1432,10 @@ describe("adaptive missing-frame retry helpers", () => { it("finds contiguous missing frame ranges from captured disk frames", () => { const framesDir = makeFramesDir(); for (const frameIndex of [0, 1, 4]) { - writeFileSync(join(framesDir, `frame_${String(frameIndex).padStart(6, "0")}.jpg`), "x"); + writeFileSync( + join(framesDir, `frame_${String(frameIndex).padStart(6, "0")}.jpg`), + "captured-frame", + ); } expect(findMissingFrameRanges(6, framesDir, "jpg")).toEqual([ @@ -1441,6 +1444,18 @@ describe("adaptive missing-frame retry helpers", () => { ]); }); + it("retries a worker placeholder instead of accepting a truncated sequence", () => { + const framesDir = makeFramesDir(); + for (let frameIndex = 0; frameIndex < 4; frameIndex++) { + writeFileSync( + join(framesDir, `frame_${String(frameIndex).padStart(6, "0")}.jpg`), + frameIndex === 2 ? "x" : "captured-frame", + ); + } + + expect(findMissingFrameRanges(4, framesDir, "jpg")).toEqual([{ startFrame: 2, endFrame: 3 }]); + }); + it("builds retry batches that cap active workers per attempt", () => { const batches = buildMissingFrameRetryBatches( [ diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts index 74dcb3d17b..599e1ed907 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -603,7 +603,12 @@ export function findMissingFrameRanges( for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) { const framePath = join(framesDir, formatCaptureFrameName(frameIndex, frameExt)); - const missing = !existsSync(framePath); + // A capture worker can leave a zero/one-byte placeholder behind when it + // exits between creating the destination and writing the image. FFmpeg's + // image2 demuxer treats that as end-of-sequence but still exits 0, which + // used to let a truncated video be reported as successful. Real JPEG and + // PNG captures are necessarily larger than their 8-byte file signatures. + const missing = !existsSync(framePath) || statSync(framePath).size <= 8; if (missing && rangeStart === null) { rangeStart = frameIndex; } else if (!missing && rangeStart !== null) {