Skip to content

[REA-6855] Write one step's output as a single MP4 file - #237

Open
Dere-Wah wants to merge 2 commits into
dere/rea-6855-step-results-manifest-blockfrom
dere/rea-6855-single-file-mp4-writer
Open

Dere-Wah wants to merge 2 commits into
dere/rea-6855-step-results-manifest-blockfrom
dere/rea-6855-single-file-mp4-writer

Conversation

@Dere-Wah

@Dere-Wah Dere-Wah commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Why

A saved step has to be one file a caller can download and play, holding every track the step produced. The recorder's encoder writes segmented fMP4 for HLS, with a timeline that spans the session; a step is written once, whole, so it needs none of that and a small writer of its own. recording/ does not change.

What Changed

write_mp4(path, bundle, fps, config) encodes a MediaBundle into one MP4 with PyAV. Video tracks come first, as H.264 or H.265 streams played at fps, then audio tracks, encoded with the configured codec and bitrate at the track's own rate (48 kHz when it declares none). Each keeps the order the output declares. The file is laid out with +faststart, so it plays while it downloads. Codecs, preset, quality, and audio bitrate come from StepResultsConfig, and the pixel format and profile are the recorder's.

A frame with an odd width or height is padded by one row or column, repeating the edge, because the encoder's pixel format needs even dimensions. Every stream is added before the first packet is written: the muxer writes the file header with that packet, and a stream added after it crashes libav, which is how a step with two video tracks first failed. Each stream is encoded into its own list of packets, and the lists are merged by decode time before they reach the muxer: the muxer buffers only a bounded span of one stream while it waits for the others, so encoding video to the end before any audio fails for a longer step (600 frames at 30 fps with 20 s of audio raised Invalid argument).

A payload the writer cannot encode (no tracks, fps <= 0, a frame of the wrong shape) is refused before the file opens, and a file that fails part-way is removed, so the path holds a whole file or nothing.

The tests decode every file they write back: stream count, frame count, size, rate, codec (h264, hevc, aac), audio length, the progressive layout, the errors, the cleanup after a failure mid-write, and a twenty-second video and audio round trip.

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

[codex-review] - [P1] src/reactor_runtime/step_results/mp4.py: Sequential stream encoding causes longer video/audio outputs to exceed FFmpeg’s interleave limit and fail.

Scope: full (498bd52..1c8fe61).

View workflow run.

Comment thread src/reactor_runtime/step_results/mp4.py Outdated

@tempusfrangit tempusfrangit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reproduced the failure described in the existing inline comment: 600 video frames at 30 fps with 20 seconds of 48 kHz audio make write_mp4 raise ArgumentError while encoding the video. Please interleave stream encoding or muxing and add a longer video/audio round-trip regression test before merging.

@Dere-Wah
Dere-Wah force-pushed the dere/rea-6855-step-results-manifest-block branch from 498bd52 to 3e06488 Compare October 4, 2026 16:39
@Dere-Wah
Dere-Wah force-pushed the dere/rea-6855-single-file-mp4-writer branch 2 times, most recently from f5bf9c5 to e6ae667 Compare October 4, 2026 17:46
@Dere-Wah

Dere-Wah commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

@tempusfrangit reproduced the 600-frame + 20 s audio failure and fixed it in e6ae667: every stream's packets are merged by decode time before muxing. test_a_long_video_and_audio_output_is_interleaved round-trips that exact input.

@Dere-Wah
Dere-Wah force-pushed the dere/rea-6855-single-file-mp4-writer branch from e6ae667 to a510431 Compare October 4, 2026 18:28
@Dere-Wah
Dere-Wah force-pushed the dere/rea-6855-step-results-manifest-block branch from fcfd87c to 9036aff Compare October 4, 2026 18:28
Dere-Wah and others added 2 commits October 4, 2026 16:44
A saved step result has to be one file a client can download and play,
holding every track the step produced. write_mp4() encodes a MediaBundle
that way: each video track becomes an H.264 or H.265 stream at the
model's frame rate, each audio track an audio stream at its own rate,
and the file is laid out to play while it downloads.

Codecs, preset, quality and audio bitrate come from the
runtime.step_results block. A frame with an odd size is padded by
repeating its edge, because the encoder's pixel format needs even
dimensions. Every stream is added before the first packet, because the
muxer writes the file header with it. A payload the writer cannot
encode is refused before the file opens, and a file that fails part-way
is removed, so the path holds a whole file or nothing.

Signed-off-by: Dere-Wah <derexcontact@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The writer encoded each stream to the end before feeding the next, so a
longer step with video and audio failed: the muxer buffers only a bounded
span of one stream while it waits for the others, and 600 frames at 30
fps reached that limit before any audio packet arrived, which surfaced
as an Invalid argument error from FFmpeg.

Each stream is now encoded into its own list of packets, and the lists
are merged by decode time before they reach the muxer. A step is short,
so its compressed packets fit in memory. A twenty-second video and audio
round trip covers it.

Signed-off-by: Dere-Wah <derexcontact@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@Dere-Wah
Dere-Wah force-pushed the dere/rea-6855-step-results-manifest-block branch from 9036aff to 966a3b6 Compare October 4, 2026 23:47
@Dere-Wah
Dere-Wah force-pushed the dere/rea-6855-single-file-mp4-writer branch from a510431 to 355b7ab Compare October 4, 2026 23:47
@Dere-Wah
Dere-Wah requested a review from tempusfrangit October 5, 2026 00:03

This branch has not been deployed

No deployments
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