fix(producer): assert render artifact duration and frame count before commit (#3506)

* fix(producer): assert render artifact duration and frame count before commit

Refuse to publish an artifact that is significantly shorter or has fewer frames

than the capture pipeline just reported. Adds a duration/frame-count gate on top

of the existing readable-non-empty check inside ArtifactTransaction.validate(),

keyed off the values the orchestrator already carries. Closes #3395.

* fix(producer): wire ffprobe frame count into the artifact duration probe

The frame-count gate added in #3395 accepts an expectedFrames value from
the orchestrator, but defaultArtifactDurationProbe was still returning
only durationSeconds - so the wire was half-built and the assertion
short-circuited on undefined for every real render. Forward meta.frames
from ffprobe so the field-packet case the issue names (container duration
correct, stream shorter) is actually caught by the frame-count check,
not just the duration one.

extractMediaMetadata now populates a new frames field from the video
stream's nb_frames tag, returning undefined when the demuxer did not
report one (fragmented MP4, malformed streams, muxes that require
-count_packets). Callers that gate on the count must treat undefined as
no answer; the assertion already does.

The previous CI run (#32589981916) cancelled shard-6 at the 1h job
timeout after bun install failed to extract the aws-cdk-lib tarball
mid-Docker-build - a cache flake, not a code regression. Pushing a
follow-up commit retriggers CI against the now-populated cache layer;
the regression should clear without further code changes.

---------

Co-authored-by: Santhi Prakash <b.santhiprakash@gmail.com>
This commit is contained in:
miga-heygen
2026-08-28 00:19:27 +00:00
committed by GitHub
co-authored by Santhi Prakash
parent e5c7dc75e8
commit 05275c1e8c
4 changed files with 376 additions and 25 deletions
+15
View File
@@ -188,6 +188,11 @@ export interface VideoMetadata {
hasAlpha: boolean;
/** Color space info from the video stream. Null if ffprobe didn't report it. */
colorSpace: VideoColorSpace | null;
/** Decoded frame count from the video stream's `nb_frames`. Omitted when the
* container does not surface a reliable count (still images, malformed
* streams, or muxes that require `-count_packets` to populate). Callers
* that gate on a frame count must treat `undefined` as "no answer". */
frames?: number;
}
export interface AudioMetadata {
@@ -731,6 +736,7 @@ export async function extractMediaMetadata(filePath: string): Promise<VideoMetad
isVFR: false,
hasAlpha: false,
colorSpace: stillImageMeta.colorSpace,
frames: 1,
};
}
throw new Error("[FFmpeg] No video stream found");
@@ -775,6 +781,14 @@ export async function extractMediaMetadata(filePath: string): Promise<VideoMetad
const streamDuration = videoStream.duration ? parseFloat(videoStream.duration) : 0;
const parsedStreamStart = videoStream.start_time ? parseFloat(videoStream.start_time) : 0;
const streamStart = Number.isFinite(parsedStreamStart) ? parsedStreamStart : 0;
// `nb_frames` is populated by the container demuxer; a muxer that requires
// `-count_packets` to enumerate frames will leave it undefined, in which
// case the caller must treat the frame-count check as "no answer". This
// path is hit by, for example, fragmented MP4 where the moov box lacks a
// frame count — the captured duration is still correct.
const parsedNbFrames = videoStream.nb_frames ? parseInt(videoStream.nb_frames, 10) : NaN;
const frames =
Number.isFinite(parsedNbFrames) && parsedNbFrames > 0 ? parsedNbFrames : undefined;
return {
durationSeconds: containerDuration,
@@ -788,6 +802,7 @@ export async function extractMediaMetadata(filePath: string): Promise<VideoMetad
isVFR,
hasAlpha,
colorSpace,
frames,
};
})();