10 KiB
Proposal: contentBinding, a VideoFrame metadata member that ties a decoded frame to the content it was decoded from
Status: draft. Not filed. Whether, when and where to file it is the owner's decision. This file holds what a filer would need: the gap, a draft registry entry, a draft of the defining text, the issue text ready to paste, what was tested and what is still open. The normative WAI text, which a page that uses WAI's receipts follows today, is
extensions/browser-sink.md§7–§10.Texts read, each pinned to the version named: WebCodecs, W3C Working Draft 19 October 2026; the WebCodecs VideoFrame Metadata Registry, W3C Registry Draft 14 April 2026; the WebCodecs Codec Registry, W3C Registry Draft 24 September 2026; Media Capabilities, W3C Working Draft 18 October 2026.
1. The gap
A page can verify the bytes it fetched: a segment against a signed manifest, an object against
a receipt. Nothing ties that verification to the frame the page then shows. Between the two
sit a decoder, which outputs frames that name no input but a timestamp, and the page's own
handling, in which a frame is cloned, transferred to a worker, wrapped in a new VideoFrame,
drawn and re-encoded. A page that wants to say "the frame on screen is the one decoded from
the bytes I verified" has to keep that association itself, keyed by timestamp, and loses it at
every realm boundary unless it carries it beside the frame by hand.
VideoFrameMetadata is where WebCodecs carries per-frame facts through exactly those
operations: Clone VideoFrame assigns [[metadata]] to the clone, and the serialization and
transfer steps carry it across realms. Its registry lists capture-side facts (captureTime,
receiveTime, rtpTimestamp, segmentation and blur state). It has no member that says what
encoded content a frame was decoded from.
2. The registry's requirements, read against this proposal
The registry (Registry Draft, 14 April 2026, Registration) asks for an issue in the WebCodecs issue tracker, interest from at least one implementer, Media Working Group consensus, and an entry that:
| requirement | this proposal |
|---|---|
is a separate VideoFrameMetadata member |
contentBinding, one member whose value is a dictionary |
| is serializable | its members are strings; a dictionary of strings is serializable |
| is defined by a W3C specification that has reached consensus in its originating Working Group | not met by this document. WAI's extension is a profile of a producer, not a W3C specification. The issue asks the group where a definition belongs (a section of WebCodecs, a Media WG note, or another group's document) |
| has clear semantics, including how it interacts with encoders, decoders and renderers | §4 below |
3. Draft registry entry
| Member name | Public specification |
|---|---|
contentBinding |
(to be decided with the Media WG: the document that defines §4 below) |
4. Draft defining text
dictionary VideoFrameContentBinding {
required DOMString alg;
required DOMString digest;
DOMString receipt;
DOMString tier;
};
partial dictionary VideoFrameMetadata {
VideoFrameContentBinding contentBinding;
};
algnames the algorithmdigestwas computed with and which bytes it covers. Values are defined by the specification of whoever writes the binding; a reader that does not knowalgcannot checkdigestand treats the binding as absent. (WAI's value isjwp-object-1: BLAKE3 over a domain string and the encoded object as received.)digestis the lowercase hexadecimal digest of the encoded object the frame was decoded from: the bytes the page received and, where it checked them, verified.receipt, optional, is the lowercase hexadecimal identity of a signed record that commits todigestand that the page verified the object against. Absent when the page verified nothing beyond the digest.tier, optional, names how reproducible the frame's samples are as the writer's specification defines it (WAI's:decode-equivalence,entropy-consistency,none); a reader that does not know the token reads it as no claim.
Semantics through the pipeline.
| operation | the member |
|---|---|
| a decoder's output | not set by the decoder (Create a VideoFrame takes no metadata). A page sets it by constructing new VideoFrame(output, { metadata: { ...output.metadata(), contentBinding } }) |
clone() |
carried (Clone VideoFrame assigns [[metadata]]) |
| serialization and transfer | carried (the serialization and transfer steps carry [[metadata]]) |
new VideoFrame(frame, init) |
what init.metadata holds, as for every member: a page copies it when the new frame presents the same samples |
new VideoFrame from a canvas, an image or a buffer |
not set, unless the page wrote those samples itself from the bound content (a decoder implemented in WebAssembly, say) |
copyTo() |
the bytes written carry nothing; with no format option they are the frame's samples, which a page may hash |
VideoEncoder.encode() |
does not survive. An encoded chunk is new content; a frame decoded from it is bound, if at all, to that content |
rendering (drawImage, a texture) |
the rendered pixels carry nothing; a page that wants to say which frame a region shows keeps that itself |
What it is not. It is not a signature and proves nothing by itself: any script that can construct a frame can write any value. It says where to look — which content, under which receipt — so that a party holding that content and that receipt can check them, and so that the association survives the operations above without each page re-implementing a side table.
5. Draft issue text
Title: VideoFrame metadata registry:
contentBinding, the content a decoded frame came fromProblem. A page that verifies the encoded media it receives (against a signed manifest, a receipt, a content address) cannot carry that verification to the frames it decodes. Decoder outputs identify their input only by timestamp, and a page that keeps its own association loses it at
clone(), atnew VideoFrame(frame), and at everypostMessageto a worker unless it posts the association beside each frame by hand.VideoFrameMetadatais the dictionary WebCodecs already carries through clone, serialization and transfer; it has no member for what content a frame was decoded from.Proposal. A registry entry
contentBinding: a dictionary of strings{ alg, digest, receipt?, tier? }naming the digest algorithm and the digest of the encoded object the frame was decoded from, optionally the identity of a signed record committing to that digest which the page verified, and optionally a reproducibility claim for the samples. Semantics: set by the page (not by the decoder) on a decoder's output; carried byclone(), serialization and transfer as every member is; not carried into a re-encoded chunk or anything decoded from it; carries nothing intocopyTo()output or rendered pixels. A draft IDL and a table of the pipeline semantics are attached.Prior experience. We implemented the semantics as a page-side polyfill and tested it in a current user agent under automation: a side table keyed by (decoder, timestamp), with
clone()andclose()patched and the binding posted beside each transferred frame. It works, with three costs a registered member would remove: the page must refuse to bind when one decoder is given two chunks with one timestamp (nothing else says which chunk a frame came from); every realm needs its own table and everypostMessageits own convention; and code that does not know the table (a library, a worker the page did not write) drops the binding silently. In the user agent tested,metadata()returned an empty dictionary for a frame constructed withinit.metadataholdingcontentBinding, through both the buffer and the frame constructor, as a dictionary that does not declare a member drops it; it was also empty forrtpTimestamp, a registered member, given the same way.Questions for the group. (1) Which W3C document should define the member, given the registry's requirement that its definition reach consensus in a Working Group? (2) Strings or
ArrayBuffers fordigestandreceipt? (Strings are immutable and trivially serializable; buffers avoid hex.) (3) Shouldalgvalues be drawn from an existing registry of hash algorithm names, given that a value must also say which bytes are covered? (4) Is there interest in letting a decoder carry the member from anEncodedVideoChunkto its output frame, which would remove the timestamp matching altogether? That is beyond a registry entry and is asked only to learn whether the group sees it as in scope.
6. What was tested
joule-moq-web's end-to-end scenario binding drives a current user agent headless (its
version is in the run's log) against the page web/binding/ and a room with ?debug=binding:
- the side table over the user agent's own VP9 decoder: every output bound to the chunk it came
from by decoder and timestamp; two chunks under one timestamp refused (
binding_ambiguous); - a WebAssembly decoder (
wai.video.int_motion) that verifies each object against its signed receipts, decodes it and binds every frame; each frame's SHA-256 equals the published one; - the binding through
clone(),close(),new VideoFrame(frame), transfer to a worker and back,copyTo()with and without a format, and re-encode, as §4's table has it; init.metadatawithcontentBindingand withrtpTimestamp: the result quoted in §5;- in a two-party room, the frame on each tile bound to an object the page verified, under that object's group receipt, and shown as verified.
7. Open
- Whether the group wants the member at all, and where its definition lives (§2).
- No audio counterpart is proposed:
AudioDatahas no metadata dictionary. - A frame predicted from earlier frames depends on their content too. WAI's text has a page bind only when every object the decoder was given since its last key frame was verified; a registered member's definition would need to say whether it names one object or a chain.