Skip to content

Clarify color space handling across decoding, encoding, and rendering - #998

Open
Djuffin wants to merge 1 commit into
w3c:mainfrom
Djuffin:color-space
Open

Djuffin wants to merge 1 commit into
w3c:mainfrom
Djuffin:color-space

Conversation

@Djuffin

@Djuffin Djuffin commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor
  • Define an internal color space on video media resources for rendering, conversion, and encoding.
  • In Output VideoFrames, expose VideoDecoderConfig.colorSpace as-is when at least one member is non-null; treat an empty colorSpace like omitting it.
  • Populate outputConfig.colorSpace in Output EncodedVideoChunks.
  • Remove heuristic guessing from Pick Color Space and fall back to sRGB or REC709 at render time when primaries, transfer, or matrix are unspecified.

Fixes #940


Preview | Diff

- Define an internal color space on video media resources for rendering,
  conversion, and encoding.
- In Output VideoFrames, expose VideoDecoderConfig.colorSpace as-is when
  at least one member is non-null; treat an empty colorSpace like
  omitting it.
- Populate outputConfig.colorSpace in Output EncodedVideoChunks.
- Remove heuristic guessing from Pick Color Space and fall back to sRGB
  or REC709 at render time when primaries, transfer, or matrix are
  unspecified.

Fixes w3c#940
@Djuffin
Djuffin requested a review from youennf November 10, 2026 10:19
@Djuffin

Djuffin commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Gist of the change

Referring to #954 as Option 1 (most-specified wins), #957 as Option 2 (most-specified + HW decoders can override), and this PR as Option 3 (config-as-is):

Browser behavior (Option 3 vs. Options 1 & 2):

  • Decoding — decouples frame.colorSpace from internal color space: Options 1 & 2 picked whichever of config.colorSpace or the bitstream had more non-null fields and always exposed the internal color space (including any OS/hardware decoder defaults) on frame.colorSpace. In Option 3, any VideoDecoderConfig.colorSpace with at least one non-null field is exposed as-is on frame.colorSpace (unset fields stay null), while colorSpace: {} is treated like undefined (exposing the internal color space).
  • Decoding — merges internally for rendering/encoding: The media resource's internal color space still combines config.colorSpace (preferring its non-null fields), bitstream metadata, and hardware/OS decoder defaults or transforms—without leaking OS guesses back onto frame.colorSpace.
  • Encoding — defines metadata.decoderConfig.colorSpace: Computed from the post-encode color space in four steps:
    1. Start |inputColorSpace| from the input frame's internal color space.
    2. Allow the encoder/pipeline to update matrix and fullRange for pixel conversions (e.g., converting an RGB <canvas> frame to limited-range bt709 YUV) while keeping the input's primaries and transfer (e.g., Display P3 or HDR PQ).
    3. Overlay only specified components emitted by the encoder onto |inputColorSpace| (unlike Options 1 & 2's all-or-nothing replacement, per-component merging means hardware encoders that write unspecified into the bitstream for some fields don't wipe out known pre-encode metadata).
    4. Expose the result in decoderConfig.colorSpace (with unspecified/unmapped H.273 components as null), emitting a new metadata.decoderConfig whenever outputConfig.colorSpace changes mid-stream.
  • Rendering & RGB copyTo(): Browsers must use internal color space only when primaries, transfer, and matrix are all specified (defaulting omitted fullRange to true for RGB / false for YUV), falling back to sRGB or REC709 otherwise.

What it means for web developers:

  • Predictable frame.colorSpace on decode: Unlike Options 1 & 2, a partial VideoDecoderConfig.colorSpace is never silently overridden by a fuller bitstream or mutated on frame.colorSpace by OS decoder defaults (e.g., VideoToolbox guesses); colorSpace: {} safely uses bitstream detection.
  • Accurate VideoEncoder → VideoDecoder configs: Encoding RGB <canvas> or HDR frames into YUV produces a ready-to-use metadata.decoderConfig.colorSpace with the converted YUV matrix/fullRange and original primaries/transfer, even when OS hardware encoders omit color tags in the output bitstream.
  • Custom buffer VideoFrames need 3 fields: new VideoFrame(buffer, init) requires primaries, transfer, and matrix to all be set for custom color conversion during rendering/copyTo(), otherwise falling back to REC709/sRGB.
  • Direct VideoFrame use vs. CPU buffer copy: Passing a VideoFrame directly to <canvas>, WebGPU/WebGL, or VideoEncoder preserves its full internal color space, whereas copying raw bytes into new VideoFrame(buf, { colorSpace: oldFrame.colorSpace }) only retains the non-null fields visible on oldFrame.colorSpace.

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.

ColorSpace override is not well defined

1 participant