Skip to content

feat(render): report encoding and assembling progress - #5137

Merged
miguel-heygen merged 5 commits into
mainfrom
feat/render-encoding-progress
Oct 7, 2026
Merged

miguel-heygen merged 5 commits into
mainfrom
feat/render-encoding-progress

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

What

hyperframes render now reports progress while ffmpeg encodes and assembles. Before, the bar sat at one number for the whole encode. On a 4K render that was 25 s of silence at 75%.

  • Encode: the stage reads Encoding frame N/M and moves up to 90%: from 75% on the disk path, and from where capture left the bar on the streaming and HDR paths. The frame count is ffmpeg's own frame= stat from stderr, for both the disk encoder (single and chunked) and the streaming encoder while it finishes after capture. GIF and PNG-sequence output report their start and their last frame.

  • Assemble: the stage moves from 90% to 99% as the audio mux and MP4 faststart write output seconds (time=). 100% still means the file is complete.

  • Every stage closes: encode and assemble always end with a done = total line, even when ffmpeg's last count differs or a pass reports nothing (WebM, MOV, HLS).

  • For programs: when stdout is not a terminal, every progress update also prints one machine line beside the unchanged human line:

    @hf-progress {"code":"encode","done":812,"total":1800,"pct":83}
    

    code is one of compile, extract_video, process_audio, start_browsers, capture, encode, assemble. done/total are frames for capture and encode, seconds for assemble, and ready workers for start_browsers. They are left out when a stage has no count. pct never goes back. Lines come at most 4 times a second, plus each stage change and stage end. This is the shape HyperFrames Desktop's export panel agreed to read. Terminal output is unchanged. The finished render is the exit code: there is no 100% machine line.

  • --docker in a terminal: output is unchanged. The container's stdout is a pipe even when the host's is a terminal, so the host passes HYPERFRAMES_STDOUT_IS_TTY=1 and the container prints no machine lines.

The producer carries the stage on RenderJob.stageProgress, so the render API's progress callback gets it too. The render API already catches a callback that throws, so a bad callback cannot stop a render here either.

Proof: a real 4K render

product-promo from the registry, --resolution 4k, 600 frames, on Linux.

Before (main): capture ended at 70%, then 75% Encoding video stayed unchanged for 25.5 s of encode, then the render completed.

After (this branch): the same render, with timestamps from the piped log:

22:39:27.597 @hf-progress {"code":"capture","done":600,"total":600,"pct":70}
22:39:33.691 @hf-progress {"code":"encode","done":7,"total":600,"pct":75}
22:39:35.711 @hf-progress {"code":"encode","done":115,"total":600,"pct":78}
22:39:37.690 @hf-progress {"code":"encode","done":276,"total":600,"pct":82}
22:39:39.711 @hf-progress {"code":"encode","done":414,"total":600,"pct":85}
22:39:41.702 @hf-progress {"code":"encode","done":543,"total":600,"pct":89}
22:39:41.979 @hf-progress {"code":"encode","done":600,"total":600,"pct":90}
22:39:42.166 @hf-progress {"code":"assemble","done":0,"total":20,"pct":90}
22:39:42.193 @hf-progress {"code":"assemble","done":20,"total":20,"pct":99}
22:39:42.241   █████████████████████████  100%  Render complete

A second render with audio, the producer's audio-mux-parity fixture at normal resolution, took the streaming path. It reported capture, then the encoder's last frames (246/300, 300/300), then assemble from 0 to 10 s through the audio mux.

Failure telemetry keeps its stage buckets. A failed render's failed_stage_code is built from the stage label at failure. Labels with live counts (Capturing frame 120/600 (6 workers), and now Encoding frame 600/600) would give each render length its own code, so counts and parentheses are dropped before naming the stage. A video encode failure keeps its existing code, encoding_video. GIF and PNG-sequence encodes now report frames under the same label, so their failures also land in encoding_video (main had encoding_gif and writing_png_sequence); the event's output format still tells them apart. Capture failures, already unbounded on main, become capturing_frame, streaming_frame and layered_composite_frame. The raw failed_stage field still carries the full label, as it did on main.

Checks

  • New tests: ffmpeg stats lines split across chunks, a copy pass with no frame count and exact centiseconds; the streaming encoder's count while close() waits; encode and assemble wiring and their closing lines (GIF, and a stage with no reporting pass); no progress for a zero total; the percent mapping; the machine line (piped vs terminal); the Docker terminal flag only for a terminal, and no machine line when it is set; the streaming stage's closing line; a PNG-sequence close. Each one fails when the code it covers is broken, and passes 3 runs in a row.
  • Engine, producer and CLI typecheck. Existing tests in the touched files pass.

Not covered

  • The segmented capture path's per-segment encodes are not reported as encode progress. Its capture progress is unchanged.
  • GIF and PNG-sequence output report no frames between start and end.
  • A render with no known duration reports assemble without done/total.
  • --batch without --json prints each row's lines in turn with no row id.
  • The HDR closing line and the chunked encoder's frame offset have no unit test. The frame offset was right on a real 4-chunk render; the HDR path was not rendered.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Edit accuracy: accurate 2059 (base branch 2059), smooth 1609 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • crop-none-px-r0-nested-z100: tracking 0.04, pressJump 0, drop 40.07, reload 40.07, render 40.03, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.08, reload 0.08, render 0.03, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.08, reload 0.08, render 0.03, renderKey -, undo true, teleport true

@miguel-heygen
miguel-heygen force-pushed the feat/render-encoding-progress branch from 7c5b46e to 22eecba Compare October 7, 2026 02:24
@miguel-heygen
miguel-heygen marked this pull request as ready for review October 7, 2026 03:28

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at e059c680.

What I checked

  • Stats parsing. ffmpegStatsReader keeps the partial line between chunks and splits on both \r and \n, which is how ffmpeg writes its stats line. The encode and mux passes set no -loglevel and no -nostats, so ffmpeg prints the frame= / time= stats on all three paths (disk, chunked, streaming close()). runFfmpeg and ManagedChildProcess already carried onStderr, so the stderr plumbing needed no change.
  • Percent mapping. Disk encode goes from 75 to 90. The streaming and HDR paths encode from wherever capture left the bar (80), up to 90. Assemble goes from 90 to 99, and only Render complete reaches 100. updateJobStatus still clamps to a monotonic max, so pct cannot go back. Assemble is a single pass on each branch (mux with audio, faststart without, or HLS), so done never resets inside the stage either.
  • Machine line. It is gated on !isTTY, the Docker flag and a set stageProgress. --quiet (which includes batch JSON) has no onProgress, so no machine line can get into a JSON document. Docker passes the flag only when the host stdout is a TTY.
  • Telemetry codes. I stripped the counts from the labels in the producer. Two codes change shape: "Starting browsers (k/n ready)" and "Capturing/Streaming frame N/M (…)". Neither of those values was stable before this PR either. Encoding frame maps to encoding_video, so the video-encode bucket is unchanged.
  • Reuse / simplicity. The repo has no existing ffmpeg progress parser and no existing machine-progress line. The studio server's SSE already reads currentStage, so it picks up the richer labels without a change. The new pieces are small and each has real callers.

Tests

  • Engine (runFfmpeg, streamingEncoder): 87 pass. CLI (render, progress, dockerRunArgs): 146 pass. Producer (shared, encodeStage, captureStreamingStage, assembleStage): 67 pass.
  • Mutations: 10 of 12 were killed. They covered the split-line carry, the immediate count and the live count in close(), the Docker TTY flag on both sides, removing counts from stage codes, the duplicate suppression, and the closing line on assemble, disk encode and streaming encode.
  • Two mutations survived:
    • Dropping the startNumber + chunk offset. This is already listed under "Not covered".
    • Removing the Math.min(done, total) clamp in reportEncodeProgress.

Nit (PR body only, the code is fine). The body says GIF and PNG-sequence failures now land in encoding_video. They do not. startEncodeProgress sets the label to Encoding GIF or Writing PNG sequence, and the frame-count report (encoded()) only runs after the encode succeeds. So a failed GIF or PNG encode still has that label at failure and still lands in encoding_gif or writing_png_sequence, as on main. That is arguably better than what the body describes. Worth correcting the sentence so nobody goes looking for a bucket move that never happened.

— Rames

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit cffc3aa Oct 7, 2026
187 checks passed
@miguel-heygen
miguel-heygen deleted the feat/render-encoding-progress branch October 7, 2026 04:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants