MOV to MP4 Transition: Status and Completion Plan

Engineering status of the recording-pipeline container switch (build 75 onward): what is fixed, what remains, the publish-boundary definition, and the C2PA ordering constraints that govern the remaining work.

Summary

The recording pipeline switched from MOV to MP4 in build 75 (commit 8724785): iOS records via AVAssetWriter .mpeg4Movie, telemetry SRT tracks convert to mov_text (tx3g, the MP4-native timed-text codec), and C2PA signing operates on BMFF/MP4 (c2pa-rs 0.48). The switch is ~90% complete. Two regressions it caused are fixed on pending branches; the remaining work is itemised below with its governing constraint: signing is the terminal byte mutation.

The publish boundary

“At publish” is a byte-stream moment, not a UX moment: the last point on device where the file’s bytes change. After it, the artifact must be immutable through S3 and every future verifier.

capture (container A)                      working files, unsigned
  -> trim (FFmpeg -c copy)                 working file, unsigned
    -> ONE final FFmpeg pass:              the publish boundary starts here
         remux/mux SRT (mov_text)
         + -movflags +faststart
      -> SHA-256 hashes
        -> C2PA sign (manifest embedded)   TERMINAL byte mutation
          -> upload exactly those bytes    content-type video/mp4

Why the ordering is forced: the manifest’s hard binding for video is the BMFF hash assertion. Any post-sign remux (including moving the moov box for faststart) rewrites box offsets, invalidates the binding, and FFmpeg drops the manifest’s uuid box entirely. Therefore faststart and every container operation must precede signing; c2pa-rs corrects stco/co64 offsets when inserting the manifest, so sign-after-faststart is safe. The backend must be byte-transparent: no server-side transcode or in-place rewrite of published videos, ever. Derived assets get their own manifest with the original as a C2PA ingredient.

Fixed and verified (build 80 code)

Item Where
SRT to mov_text conversion, both tracks survive MP4 HybridVideoPostprocessor.swift (FFmpeg -c:s mov_text, maps for readable + JSON tracks)
Track identification on MP4: handler_name survives the mux (title does not) HybridVideoPostprocessor.swift (#196 fix)
Extension-agnostic output paths (the .mov.mp4 class of bug) deletingPathExtension() + explicit .mp4 append (commit 5734663)
C2PA signing on BMFF/MP4, zero-silent-failure status file c2pa-rs 0.48 path + signstatus.json
Trim is lossless and time-coherent: -ss/-to -c copy -avoid_negative_ts make_zero, sidecar re-windowed via cropSidecar(), SRT regenerated from the cropped sidecar TrimScreen.tsx, useSensorSidecar.ts
MIME types video/mp4 end-to-end in the app layer PublishScreen.tsx, HasuraAPIAdapter.ts

Pending merge (blocked on org Actions billing)

Item Reference
Sidecar forwarding + upload restored (TDD, regression test pins the APPEND naming convention) PR #195, ruling in ADR-001
HUD playback fix rebased to keep the fix, drop the architecture change #199

Remaining work items

# Item Issue
1 +faststart in the final pre-sign mux (signed outputs are moov-at-end today; streaming penalty) #201
2 Sidecar SHA-256 assertion in the C2PA manifest (mind the CBOR serialization bug; spike first) #200
3 HUD burn-in: last MOV remnant, leaves device unsigned; switch to MP4 + sign with the published video as ingredient #202
4 Android postprocessor parity (full stub today: no mux, no hash, no signing; trim publishes naked video) Peregrine ③ milestone, gate 1 of #203
5 Trim keyframe skew: -ss -c copy snaps to keyframes while cropSidecar() cuts exact milliseconds; validate duration before signing follow-up under #198
6 Playback selector robustness: explicit handler_name-aware selection with index/language fallback (builds 75-79 clips have anonymous tracks and are permanently unaddressable by title) follow-up under #198
7 Audio codec / multi-track validation before mux; rotation transform validation post-mux follow-up under #198
8 Release/dist tagging in the Sentry/GlitchTip init (events currently carry no build attribution) follow-up under #198

Known content generations in production

Generation Container Track identification Sidecar on S3 Playback HUD
pre-build-75 MOV title preserved yes (when publish path worked) works
builds 75-79 MP4 anonymous (no title, no handler_name) no (#194 bug) permanently broken by title-selection
build 80 MP4 handler_name no (deliberately removed, overruled) works
build 81+ (planned) MP4 handler_name + title yes (ADR-001) works