fix(worker): drain ffmpeg's pipes so thumbnail generation stops deadlocking
Deploy / deploy (push) Successful in 9m16s

ffmpeg was started with stdout and stderr redirected and neither pipe ever
read. Windows gives an anonymous pipe a ~4 KB buffer, which ffmpeg's banner
and stream summary exceed on their own, so it blocked forever on the write,
never exited, and the two-minute cap fired:

    TaskCanceledException at Process.WaitForExitAsync
    Preview complete for job ... in 221s (mov + mp4)

The exception was swallowed as "non-fatal", so every preview silently
produced no thumbnail while paying 120s for the privilege. Nothing
downstream was wrong — there was simply never a thumbnail to promote.

Both pipes are now drained concurrently before the wait. A timeout kills the
process tree rather than leaking an ffmpeg per job, and attempts report a
three-way outcome so a hung or unstartable ffmpeg is not retried — without
that, the slate fallback would have doubled the wasted time to four minutes.

Failures are no longer silent: an unset or missing ffmpegPath, the seek that
was chosen, and ffmpeg's own exit code, command and stderr are all logged.

finalizePreview likewise records every promotion outcome on the export
timeline, not just success. Silence there was indistinguishable from a broken
worker, a project whose flag never saved, and the feature working as
configured — which is what made this take so long to find.

Adds scripts/diagnose-thumbnails.mjs, which walks the chain (ffmpeg →
Export.thumbnailKey → project flag → Shot.generatedThumbnailUrl → what the UI
resolves) and names the first broken link.

Worker 1.5.1 -> 1.5.2; republish required for the fix to take effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
twotalesanimation
2026-08-27 22:48:29 +02:00
parent 9cd7b0df6e
commit 1fad6a739d
4 changed files with 225 additions and 18 deletions
+19 -5
View File
@@ -413,13 +413,27 @@ export async function finalizePreview(jobId: string, machineId: string, body: Fi
// reference frame that came with the brief and the pipeline never touches
// it. Shows that supply stills therefore look exactly as they did; shows
// that supply none get their empty grids filled from the renders instead.
let thumbnailPromoted = false;
if (artifacts.thumbnailKey) {
//
// Every outcome is recorded on the export timeline, including the ones that
// did nothing: silence here is indistinguishable from a broken worker, a
// project whose flag never saved, and a feature working as configured.
let thumbnailNote: string;
if (!artifacts.thumbnailKey) {
thumbnailNote =
" — no thumbnail in this render, so the shot's was left alone " +
"(the worker reported no thumbnail: check ffmpegPath in its config.json)";
} else {
const shot = await tx.shot.findUnique({
where: { id: exportRow.shotId },
select: { project: { select: { pipelineConfig: true } } },
});
if (shot && autoShotThumbnailEnabled(shot.project)) {
if (!shot) {
thumbnailNote = " — shot not found, thumbnail not applied";
} else if (!autoShotThumbnailEnabled(shot.project)) {
thumbnailNote =
" — shot thumbnail left alone (this project has \"Use render frames as " +
"shot thumbnails\" turned off)";
} else {
await tx.shot.update({
where: { id: exportRow.shotId },
data: {
@@ -427,7 +441,7 @@ export async function finalizePreview(jobId: string, machineId: string, body: Fi
generatedThumbnailFrom: exportRow.versionString,
},
});
thumbnailPromoted = true;
thumbnailNote = " — shot thumbnail updated from this render";
}
}
@@ -469,7 +483,7 @@ export async function finalizePreview(jobId: string, machineId: string, body: Fi
: ` — review version created (${inheritedApproval ?? "PENDING_REVIEW"})`
: " — no preview media reported") +
(taskRenamed ? ` — task renamed to ${taskTitle}` : "") +
(thumbnailPromoted ? " — shot thumbnail updated from this render" : ""),
thumbnailNote,
},
{
...(versionId ? { versionId } : {}),