Repository navigation
Tag encoded video as BT.709 and convert to match - #173
Merged
Merged
Conversation
The MP4 files produced by every backend carried no color information at all: no `colr` box and no colour description in the SPS VUI. Players have to guess at the color space, and some warn about it. Tag the output as BT.709 limited range on all backends, and make the conversions actually produce BT.709 so the tags describe the pixels: - `VideoFrameBgra32::to_yuv420_planes`, used by the readback paths on Android and Windows, used BT.601 limited range coefficients. Replace them with the BT.709 equivalents. - The FFmpeg backend let FFmpeg pick the matrix, which is BT.601 at the resolutions this library records. Tag the frames so the conversion and the VUI both become BT.709. Per backend: - Apple: set the color properties on the VTCompressionSession, and add the matching format description extensions in the muxer. AVAssetWriter writes the `colr` box from the format description it is handed, and since encoded frames travel back through the caller as opaque buffers, the muxer never sees the encoder's own. - Android: set `color-standard`, `color-transfer` and `color-range` on the encoder format, and restate them on the muxer track format because codecs are not guaranteed to echo them back. - Windows: set the color attributes on both the input and the output media type. The muxer inherits them, as media type attributes are serialized generically. - FFmpeg: tag the frames with `setparams`. The `-color_primaries` and `-color_trc` output options are not reliably forwarded to the encoder. - WebCodecs: declare the source frame's color space, the only lever the API offers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The end-to-end harness now reads the color description from both the sample entry's colr box and the VUI of the SPS in avcC, and requires each present one to state BT.709 primaries, transfer and matrix in limited range. A colr box is required on every target except the web, whose muxer (muxide) writes none; there the SPS VUI is required instead. The CPU conversion in VideoFrameBgra32::to_yuv420_planes gains unit tests that pin the limited-range end points, neutral chroma for grays, the BT.709 values of the primaries, and agreement with the floating-point BT.709 definition across the RGB cube.
ruccho
marked this pull request as ready for review
October 2, 2026 02:44
WebCodecs has no encoder option for the output color space. Chrome converts an RGB VideoFrame into the matrix and range its color space declares and copies all four fields into the SPS VUI. Declaring the BGRA buffer as sRGB, as before, made the stream claim the sRGB transfer and full range, unlike every other backend; Chrome does the same when no color space is given. Declaring BT.709 primaries, transfer and matrix in limited range instead yields that exact VUI. Verified on Chrome 154 on macOS with both the OpenH264 software encoder and the hardware encoder, including that the decoded pixels are limited range (white at Y=235).
# Conflicts: # InstantReplay.Externals/unienc/crates/unienc_testkit/src/lib.rs
Handing the browser a BGRA frame leaves the YUV color space to it, and browsers disagree. Chrome 154 converts into the matrix and range the frame's color space names, while the Chrome on the CI runner, given the same declaration, converted to BT.601 and tagged the stream as such. The WebCodecs backend now converts with VideoFrameBgra32::to_i420, the same BT.709 limited-range conversion the Android and Windows readback paths use, and passes the browser I420 tagged accordingly. Dimensions are padded to even values for I420 and cropped by the visible rectangle. to_i420 writes the three planes into a single buffer, since a VideoFrame takes one buffer with per-plane offsets. Sharing the plane layout with to_yuv420_planes also fixes the chroma stride for odd widths, which was rounded down while the plane size was rounded up, so each chroma row after the first overwrote the end of the previous one.
The Windows e2e output carried no color description at all. The MPEG-4 file sink writes no colr box although the BT.709 attributes are set on the media type it is given, and the Microsoft software H.264 encoder writes no video signal type into the VUI; its documentation lists no color attribute or codec property among those it supports. After the sink has finalized the file, the muxer now appends a BT.709 limited-range nclx colr box to the video sample entry. The sink writes moov as the last top-level box, so moov is rewritten in place and no media data or chunk offset moves. A file laid out otherwise is left as the sink wrote it, with a warning. The color attributes on the encoder's media types are kept, since a hardware encoder may honor them.
hkmt-mmy
approved these changes
Oct 2, 2026
# Conflicts: # InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/mux/mod.rs
# Conflicts: # InstantReplay.Externals/unienc/crates/unienc_apple_vt/src/video/mod.rs # InstantReplay.Externals/unienc/crates/unienc_ffmpeg/src/video/mod.rs # InstantReplay.Externals/unienc/crates/unienc_testkit/src/verify.rs # InstantReplay.Externals/unienc/crates/unienc_webcodecs/src/video/mod.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #163.
Problem
The MP4 files produced by every backend carried no color information at all — no
colrbox and no colour description in the SPS VUI. Players have to guess at the color space, and some warn about it.Two backends were also converting with the wrong matrix, so this was not purely a tagging gap:
VideoFrameBgra32::to_yuv420_planes(the readback path on Android and Windows) used BT.601 limited-range coefficients.Y=81(BT.601) before,Y=63(BT.709) after.Change
Tag the output as BT.709 limited range (primaries 1, transfer 1, matrix 1, limited range) on all backends, and make the conversions actually produce BT.709 so the tags describe the pixels.
VTCompressionSession, plus matching format-description extensions in the muxer.AVAssetWriterwrites thecolrbox from the format description it is handed, and since encoded frames travel back through the caller as opaque buffers, the muxer never sees the encoder's own.color-standard/color-transfer/color-rangeon the encoder format, restated on the muxer track format because codecs are not guaranteed to echo them back.colrbox added by the muxer after finalizing. Neither the MPEG-4 file sink nor the Microsoft software H.264 encoder writes any color description, whatever the media types say: the sink writes nocolrbox, and the encoder writes no video signal type into the VUI and documents no attribute or codec property for it. The sink writesmoovas the last top-level box, somux/colr.rsrewritesmoovin place with annclxbox appended to the video sample entry; no media data or chunk offset moves. A file laid out any other way is left as written, with a warning. The media type attributes are kept for hardware encoders that may honor them.-vf setparams=.... The-color_primaries/-color_trcoutput options are not reliably forwarded to the encoder: on 8.0.1 only the matrix and range reach the VUI.setparamsalso selects the conversion matrix, fixing the BT.601 conversion above.VideoFrameBgra32::to_i420) and hand the browser an I420 frame tagged BT.709 limited range. Handing it a BGRA frame leaves the YUV conversion to the browser, and browsers disagree: Chrome 154 converts into whatever matrix and range the frame's color space declares, while the Chrome on the CI runner converted the same frame to BT.601 and tagged it as such. With no color space declared, Chrome tags the output as sRGB transfer and full range. muxide writes nocolrbox, so on the web the SPS VUI is the only declaration.to_i420shares its plane layout withto_yuv420_planes. This also fixes the chroma stride for odd widths, which was rounded down while the plane size was rounded up, so each chroma row after the first overwrote the end of the previous one.Behavior change
This changes the color of existing output. The CPU conversion coefficients change from BT.601 to BT.709, and the FFmpeg backend's conversion changes likewise. Footage recorded after this change will not be pixel-identical to footage recorded before it. The new output is correct: previously the pixels were BT.601 but untagged, so players defaulted to BT.709 for HD content and rendered them with a color shift.
On the web, frames are now converted on the CPU in WebAssembly before encoding instead of by the browser. This adds a per-frame cost on the main thread.
Tests
unienc_testkit) — the harness now reads the color description from both the video sample entry'scolrbox and the VUI of the SPS inavcC, and requires each one present to state BT.709 limited range. They are checked separately because they come from different places (the muxer and the encoder) and players disagree on which one wins. Acolrbox is required on every target except the web, where the SPS VUI is required instead. This runs on every CI target: Linux (FFmpeg), macOS, iOS simulator, Android emulator, Windows and the browser.unienc_common) —to_yuv420_planes/to_i420: limited-range end points, neutral chroma for every gray level, the BT.709 values of the primaries, agreement with the floating-point BT.709 definition across the RGB cube, padding, the odd-width chroma stride, and thatto_i420packs the same planes. Restoring the BT.601 coefficients or the old chroma stride fails them.unienc_testkit::h264,unienc_windows_mf::mux::colr) — the SPS VUI reader against SPS units written by libx264, and thecolrinsertion including box sizes and the untouched-file cases.Verification
All jobs of
CI uniencpass on d6a149b (run 36963200242). The color descriptions the e2e harness found:colrThe checks fail where the declaration is missing or wrong. With the Apple backend reverted to
main, the harness reportedcolr: none; SPS: nonelocally on macOS. Before the Windows and WebCodecs fixes, CI (run 36957393259) reportedcolr: none; SPS: noneon Windows andSPS: primaries 6, transfer 6, matrix 6on the runner's Chrome.Not covered by CI:
colrbox. CI runs the software encoder only.Transfer characteristics
The pixels reaching the encoder are sRGB-encoded in both Unity gamma and linear workflows. On the readback path,
FramePreprocessorrenders into anR8G8B8A8_UNormtarget in gamma and anR8G8B8A8_SRGBtarget in linear. On the blit paths,is_gamma_workflowselects the sRGB render pipeline and destination format on Apple and the source view format on Android to the same end. The same tags therefore apply to both workflows.Strictly, sRGB-encoded content is transfer characteristics 13 (IEC 61966-2-1) in ITU-T H.273, not 1. This change tags 1 (BT.709) anyway, following the common practice for SDR screen capture. Players generally render the two identically, while support for 13 is less consistent across players and hardware decoders.
Note on merge order
This touches
CompressionSession::newinunienc_apple_vt/src/video/mod.rsand theMediaFormatsetup inunienc_android_mc/src/video/mod.rs— the same places as #172 (configurable IDR interval). Depending on merge order one of the two will need conflict resolution.Before merging
The native binaries under
Packages/jp.co.cyberagent.instant-replay/UniEnc/Plugins/must be rebuilt via thebuild-unienc.ymlworkflow (manual dispatch).