Skip to content

perf(aws-lambda): build the final mp4 inside S3 with its index up front - #5805

Merged
miguel-heygen merged 1 commit into
cloudrender/mp4-indexfrom
cloudrender/s3-faststart-stitch
Oct 18, 2026
Merged

miguel-heygen merged 1 commit into
cloudrender/mp4-indexfrom
cloudrender/s3-faststart-stitch

Conversation

@miguel-heygen

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

Copy link
Copy Markdown
Collaborator

Stacked on #5812 (the MP4 index builder), which is stacked on #5803 (chunks carry their audio). This is the AWS half: the final mp4 is assembled inside S3 as a regular faststart file, so browsers start it from a URL.

What changed

On the AWS Lambda path, format: "mp4" renders (without cfr) now plan with chunksCarryAudio. Assemble builds a regular faststart MP4 inside S3: ftyp, one moov that indexes every sample, then one 64-bit mdat.

  • stitchMp4InS3 reads each chunk's ftyp and moov with two small ranged GETs and builds the header with perf(cloud): assemble chunk output with a single faststart index #5812's buildStitchedHeader.
  • One multipart upload writes the file:
    • Part 1 is the header plus just enough leading bytes of chunk 0 (downloaded) to reach S3's 5 MiB minimum.
    • Every other part is UploadPartCopy of one chunk's mdat byte range, so those bytes never pass through the function.
    • Part rule: every part but the last must be at least 5 MiB, and a copy reads one object. A range of at least 5 MiB is copied as it is. Anything smaller is downloaded into an uploaded part, which borrows the next range's head when the rest of that range can still be copied. Short renders take the same path, so there is no local fallback.
    • Any failure aborts the upload. The SAM template grants s3:AbortMultipartUpload, which S3CrudPolicy lacks; the snapshot test pins it.
  • Audio layout: each chunk's audio packets sit inside that chunk's payload, interleaved with its video as ffmpeg muxes any MP4. A player streaming from the start reads audio and video from the same few MB.
  • The lambda-local harness's disk-usage sampler no longer crashes when a scratch file disappears between listing and stat (hit while testing this).
  • chunksAreStitchedMp4, buildStitchedHeader, readMp4Layout and muxChunkWithAudio are exported from @hyperframes/producer/distributed for this package.

What I measured

Locally:

  • s3Stitch.test.ts:
    • The part planner on exact sizes, and abort on a failed part.
    • 3 real x264 + AAC chunks through an S3 stand-in. It serves ranged reads, joins parts and rejects a non-last part under the minimum, which is set to 16 KiB so small chunks take every path that 5 MiB takes. Results: ffprobe -v error silent, boxes ftyp, moov, mdat, 91 frames and all audio packets. Frames, PCM and stream durations equal today's assemble(), and the local assemble() output is byte-identical to the S3 output.
  • handler.test.ts: a plan whose chunks carry audio assembles through S3 and never calls the local join.
  • 3 runs in a row green. Each mutation turns its test red: borrowing that leaves a short copy, no abort, the handler skipping the S3 path. The S3 integration test also goes red for offsets ignoring earlier chunks and for the priming not being skipped.
  • tsc (producer, engine, aws-lambda), oxlint, oxfmt, fallow audit (new-only gate) and the comment checks pass.

On AWS: a 30-minute 1080p30 benchmark, on the same fixture as main's baseline with inputs pinned by sha256. Deployed with --concurrency 300 and rendered with --max-parallel-chunks 225 (225 chunks of 8 s). Measured on the same code before it was split into this stack (#5803, #5812, this PR). The split changed only file layout, a flag name and the in-chunk sample count check, which now uses ffprobe. I have not re-run AWS on the split branches.

Today (ffmpeg join) Fragmented MP4 + S3 stitch This PR
Assemble, Step Functions state 81.5 s 1.8 s 2.4 s
Assemble, inside the function download 2 GB, ffmpeg, upload 2 GB 1.6 s 2.2 s: 0.06 s plan read, 2.16 s for 226 parts (3.3 MB downloaded, 2 MB header uploaded)
Chrome from a presigned URL, duration known 1.8 s / 3.4 s never within 60 s 1.6 s / 3.1 s
WebKit from a presigned URL, duration known 5.5 s / 2.7 s never within 60 s 3.3 s / 5.5 s
Seeks to 5, 900, 1799 s from the URL all land 5 s never lands all land
Video / audio duration 1800.000 / 1800.000 s 1800.000 / 1800.021 s 1800.000 / 1800.000 s
  • The streaming rows come from one session: two rounds, all three layouts back to back, order rotated. "Today" is this run's output remuxed with -movflags +faststart, and "fragmented" is the same streams as fragmented MP4.
  • Time to "ready to play through" (readyState 4) is left out on purpose. It also needs the link to outrun the video's 9.2 Mb/s, and on this 4-7 Mb/s link it swung from 2 s to over 60 s for the same file. On every round it moved together for this PR's file and today's layout.
  • End to end: 289.5 s in Step Functions (plan 72.1 s, render map 215.0 s, assemble 2.4 s). Only the assemble is this change.
  • Measured cost (Lambda REPORT lines plus Step Functions): $1.62. 227 invocations, all OK.
  • Output:
    • 2.07 GB; ffprobe -v error silent. Boxes: ftyp, a 1.99 MB moov at the front, one mdat.
    • 54000 frames, and 84376 AAC packets (main's count) decoding to exactly 86,400,000 samples.
    • Loudness as on main, with no level jump at any chunk seam.
  • Pictures at 5, 900 and 1799 s are byte-identical to an earlier run of the same fixture, and I looked at them, including the frames each player showed while streaming.
  • Players:
    • A 10 s render from disk passes ffprobe, AVFoundation, Chrome and WebKit, with the same frame in each.
    • Streamed from its URL, the 30-minute file plays in AVFoundation with exact frames at 5, 900 and 1799 s.
  • The stack, its buckets and its log group are torn down, and nothing is left.

What I did not exercise

  • Real Safari: safaridriver needs "Allow remote automation" on the test Mac, so the browser runs are Playwright WebKit, not Safari.
  • An AWS run of the split branches themselves (see above).

The Lambda plan asks chunks to carry their audio. Assemble writes the faststart header as
part 1 of a multipart upload and copies each chunk's mdat payload with UploadPartCopy, so
the video bytes never pass through the function; any failure aborts the upload.
@miguel-heygen
miguel-heygen force-pushed the cloudrender/s3-faststart-stitch branch from 26c28d7 to 0a60dee Compare October 17, 2026 17:50
@miguel-heygen
miguel-heygen changed the base branch from cloudrender/chunk-audio to cloudrender/mp4-index October 17, 2026 17:51
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Split for review: the MP4 index builder (producer) moved to #5812, and this PR is now only the AWS half (S3 multipart stitch, handler wiring, IAM grant, tests), retargeted onto #5812. Stack: #5803, then #5812, then this.

@miguel-heygen miguel-heygen reopened this Oct 18, 2026
@miguel-heygen
miguel-heygen merged commit 0a60dee into cloudrender/mp4-index Oct 18, 2026
46 of 49 checks passed
@miguel-heygen
miguel-heygen deleted the cloudrender/s3-faststart-stitch branch October 18, 2026 01:00
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.

1 participant