platform-decode §2 reads the query a page asks the platform from the payload of every capability browser-sink §4.1 gives a user-agent decoder, not only AV1 and HEVC. §2.1 adds VP9 (the codec string from the first key frame's uncompressed header, the level from VP9's level table over the frame size and the mean rate of the IVF timestamps, the matrix of its colour space), AVC (avc1 and the SPS's profile, constraint byte and level, the size after the cropping rectangle) and VP8. §2.2 adds the audio query, asked of AudioDecoder.isConfigSupported and of decodingInfo with an AudioConfiguration: Ogg Opus (its OpusHead the description), one Opus packet (the TOC byte), FLAC (STREAMINFO; the marker and metadata the description) and AAC in ADTS (mp4a.40.<object type>, or mp4a.66 to mp4a.68 for MPEG-2). §2.3 reads, by the same readers, the chunks a page gives the decoder and which of them are key. §3: answers to several configurations under one codec string are merged, so no object is routed to a decoder that refused its configuration. browser-sink §3 takes the audio answers into the report; §14.3 has the composing page ask about every alternative platform-decode maps and decode one routed ua through WebCodecs from §2.3's chunks. wai::platform_decode: Query::Video and Query::Audio, chunks, chunks_json, merge_answers, media_of. wai-web: platformMapped, platformChunks, platformMerge; compose.js asks and decodes through them for every mapped capability, video and audio. platform-decode-conformance/queries.json: the query and chunk summary of real payloads of every mapped capability (payloads/, written by payloads/make.sh with an independent prober's readings in prober.tsv, and the identify and encode corpora's streams), and planted faults with their outcomes. live-conformance/browser_sink_verify.py, given that directory, recomputes every case with its own readers and requires every mutated copy to fail. verify_platform_decode.mjs reads every payload through the wasm32 build in Chromium and WebKit and decodes it where the platform accepts its configuration; platform-decode §5 records the run. Co-Authored-By: Claude Opus 5.5 <[email protected]>
39 KiB
WAI Extension: Platform Decode Capabilities
Status: Draft. Maps two web platform interfaces, Media Capabilities
decodingInfo()and WebCodecsisConfigSupported()(VideoDecoder's with its hardware-acceleration preference, andAudioDecoder's), onto the capability report of browser-sink §3: what a sink asks the platform about an entry of every capability browser-sink §4.1 gives a user-agent decoder, AV1, HEVC, AVC, VP9 and VP8 video and Opus, FLAC and AAC audio, read from the entry's payload (§2), the chunks it gives the decoder, read the same way (§2.3), and how the answers enter the report (§3). The answers are hints. They order the alternatives a page can decode, inside browser-sink's routing, by SPEC §4's energy order (browser-sink §4.7); they never change a route, are never an energy figure and are never evidence of hardware. A grain-free AV1 stream decoded by the platform is a basewai.video.int_refinerefines, once a probe has shown the platform's decoder gives the decoding process's frames (§6). No manifest member and no wire format. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.
1. Scope
A sink running in a web platform can decode a standard video or audio codec through the platform's own decoders, which may be fixed-function hardware, or through a decoder of its own: WebAssembly, or a learned reconstruction such as WAI's integer codecs. The platform says, for a configuration, whether it can decode it, whether it can do so without dropping frames, and whether it judges the decode power-efficient (Media Capabilities §2.2). A learned reconstruction runs on the sink's CPU, GPU or neural engine, and what it draws there is known only where it has been measured. This extension says how a WAI sink asks the platform about an entry, and how it enters what the platform says in browser-sink's capability report, where the routing (browser-sink §4) decides what the page can decode and the energy order (SPEC §4, Energy order; browser-sink §4.7) orders what it can.
It reads these documents:
- Media Capabilities, W3C Working Draft, 18 October 2026:
MediaDecodingConfiguration,VideoConfiguration,MediaCapabilitiesInfo(§2.1, §2.2), the decoding-info algorithm (§2.3.2), and the privacy considerations (§3); - Media Capabilities, W3C Working Draft, 9 November 2026:
AudioConfigurationandMediaDecodingConfiguration(which members are required), and the valid audio configuration steps; - WebCodecs, W3C Working Draft, 19 October 2026:
VideoDecoderConfig(§7.6),isConfigSupported()and Check Configuration Support (§4, §7.1), andHardwareAcceleration(§7.9); - the WebCodecs AV1 codec registration (Group Note Draft, 13 October 2026) and HEVC codec registration (Group Note Draft, 8 June 2026), which define the codec strings by the AV1 ISOBMFF binding (AV1 Codec ISO Media File Format Binding v1.3.0, §5) and by ISO/IEC 14496-15 Annex E.3;
- the WebCodecs VP9 and VP8 codec registrations (Group Note Drafts, 16 October 2026): the
codec strings, a chunk as one frame, a key chunk, and no
description; the VP Codec ISO Media File Format Binding's Codecs Parameter String (the eight fields, which are mandatory, the defaults, the subsampling and level values); and VP9 Levels and Decoder Testing (each level's limits); - the WebCodecs AVC, Opus, FLAC and AAC codec registrations: the codec strings (
avc1.oravc3.and six characters;opus;flac; themp4a.strings), a chunk (an access unit; an Opus packet, or an audio data packet in theoggformat; a FLAC frame; an ADTS frame or a raw data block), and thedescription(none for Annex B and ADTS; the identification header for Ogg Opus; thefLaCmarker andSTREAMINFO, required, for FLAC).
ISO/IEC 14496-15 was not read. The HEVC codec string §2 writes follows the form the HEVC
registration describes (four dot-separated fields of Annex E.3 and the constraint bytes
after them), checked against an open-source parser of that form (Chromium's
ParseHEVCCodecId) and against a platform that decodes with it (§5). The headers §2.1 and
§2.2 read (the H.264 SPS, the VP9 uncompressed header, the VP8 frame tag, Ogg pages,
OpusHead and the TOC byte, FLAC's metadata and frame headers, the ADTS header) are read as
the clauses cited beside them define them; ISO/IEC 14496-3 and 13818-7 were not read for this
revision, and the ADTS fields follow the AAC registration's reference to them. Every payload
of the vectors is also read by an independent media prober, whose readings the vectors keep
(§5).
2. The query
For an entry whose capability §2.1 or §2.2 maps, the sink forms the query from the entry's payload. It MUST NOT form it from anything the envelope declares apart from the payload: the codec string a payload's encoder declared can differ from the stream (§5 records one that did). A payload a reader below refuses gives no query; the sink decodes the entry, or does not, as it would without this extension.
2.1 Video
| capability | codec string | size | frame rate | bit rate |
|---|---|---|---|---|
wai.video.av1, wai.video.av1.lossless |
from the first temporal unit's sequence header OBU: av01., the profile, the level (two digits) and the tier (M or H), the bit depth (two digits); then the monochrome flag, the chroma subsampling (three digits), the colour primaries, transfer characteristics and matrix coefficients (two digits each, 01 each where the sequence header carries no colour description), and the full-range flag; the part after the bit depth omitted where it is exactly .0.110.01.01.01.0 (AV1-ISOBMFF §5) |
w × h of SPEC §5's AV1 framing |
fps_num / fps_den of the framing |
the payload's bits × the frame rate ÷ n, rounded up |
wai.video.hevc |
from the first SPS NAL unit (H.265 §7.3.2.2, §7.3.3): hev1.; the profile space as nothing, A, B or C, then general_profile_idc in decimal; the 32 general_profile_compatibility_flags as one hexadecimal value with flag j at bit j; L or H for general_tier_flag, then general_level_idc in decimal; then the six bytes from general_progressive_source_flag on, each in hexadecimal, trailing zero bytes omitted. hev1 because an Annex B stream carries its parameter sets in band |
pic_width_in_luma_samples × pic_height_in_luma_samples, less the conformance window; the coded size before it |
none in the payload: the rate the sink will present at, where it has one | the payload's bits × the frame rate ÷ the pictures the stream holds, rounded up |
wai.video.avc |
from the first SPS NAL unit (H.264 §7.3.2.1.1): avc1., then profile_idc, the byte of constraint_set0_flag to reserved_zero_2bits, and level_idc, two upper-case hexadecimal digits each (RFC 6381 §3.3). avc1, not avc3: the AVC registration lets the string begin with either and fixes the bitstream format by the configuration's description, not by the prefix (an Annex B stream has none), and one engine measured answers every avc3. string unsupported while every engine measured accepts avc1. (§5) |
PicWidthInMbs · 16 × FrameHeightInMbs · 16, less the frame cropping rectangle (H.264 §7.4.2.1.1); the coded size before it |
none in the payload: the rate the sink will present at, where it has one | the payload's bits × the frame rate ÷ the pictures the stream holds, rounded up |
wai.video.vp9 |
from the first frame of the IVF container, which is a key frame (VP9 §6.2, §7.2): vp09., the profile, the level (below) and the bit depth, two decimal digits each; then the chroma subsampling (01 for 4:2:0, 02 for 4:2:2, 03 for 4:4:4), the colour primaries and transfer characteristics (01 each: VP9 states neither), the matrix coefficients of its color_space (below), and color_range, two digits each; the part after the bit depth omitted where it is exactly .01.01.01.01.00 (VP Codec ISO Media File Format Binding, Codecs Parameter String). A 4:4:0 stream has no subsampling code and gives no query |
the first frame's frame_width_minus_1 + 1 × frame_height_minus_1 + 1, which is also the coded size |
the mean over the frames' IVF timestamps: k − 1 frame periods from the first frame's timestamp to the last's, k the frames the container holds, in the time base its header states (bytes 16 to 23, as a denominator then a numerator), reduced to lowest terms; where that is not known (one frame, a zero time base, a last timestamp not after the first), the rate the sink will present at |
the payload's bits × the frame rate ÷ k, rounded up |
wai.video.vp8 |
vp8 |
the first frame's, which is a key frame: the 14-bit width and height after its start code (RFC 6386 §9.1) | as VP9's | as VP9's |
An Annex B stream's pictures are its slices that begin one: an H.264 slice (nal_unit_type 1
or 5) whose first_mb_in_slice is 0, an H.265 VCL NAL unit whose
first_slice_segment_in_pic_flag is 1. An IVF stream is read whole: the signature DKIF,
version 0, a header of at least 32 bytes and the capability's FourCC (VP90, VP80), then
frames of a 12-byte header and the bytes it counts, tiling the rest of the payload; the size
and frame count the IVF header states are a container's declarations and are not read. A
payload whose first frame is not a key frame, or whose key frame header breaks its
specification (a VP9 frame_marker other than 2, a sync code other than 0x49 0x83 0x42, the
reserved colour space, an RGB one in profile 0 or 2; a VP8 version past 3 or a missing start
code), gives no query.
VP9 levels. VP9 states no level. The query's is the first level of this table (VP9 Levels and Decoder Testing) whose maximum luma picture size, maximum luma width and height, and, where the query has a frame rate, maximum luma sample rate, the size × the rate, the stream does not exceed. The levels' other limits (bit rate, buffer size, compression ratio, tiles, alt-ref distance, reference frames) are not read, so the level is the least a decoder must support for that size and rate. A stream past level 6.2 gives no query.
| level | max luma sample rate | max luma picture size | max luma width or height |
|---|---|---|---|
10 |
829440 |
36864 |
512 |
11 |
2764800 |
73728 |
768 |
20 |
4608000 |
122880 |
960 |
21 |
9216000 |
245760 |
1344 |
30 |
20736000 |
552960 |
2048 |
31 |
36864000 |
983040 |
2752 |
40 |
83558400 |
2228224 |
4160 |
41 |
160432128 |
2228224 |
4160 |
50 |
311951360 |
8912896 |
8384 |
51 |
588251136 |
8912896 |
8384 |
52 |
1176502272 |
8912896 |
8384 |
60 |
1176502272 |
35651584 |
16832 |
61 |
2353004544 |
35651584 |
16832 |
62 |
4706009088 |
35651584 |
16832 |
VP9 colour. The matrix coefficients (ISO/IEC 23091-2) a VP9 color_space names:
color_space |
VP9 | matrix coefficients |
|---|---|---|
0 |
CS_UNKNOWN: none stated, so the default |
01 |
1 |
CS_BT_601 |
05 |
2 |
CS_BT_709 |
01 |
3 |
CS_SMPTE_170 |
06 |
4 |
CS_SMPTE_240 |
07 |
5 |
CS_BT_2020 |
09 |
7 |
CS_RGB, with full range and no subsampling |
00 |
CS_BT_601 takes the BT.601 row that is not SMPTE ST 170's (05), and CS_SMPTE_170 that one
(06); the two rows' equations are the same.
The sink asks:
navigator.mediaCapabilities.decodingInfo()with aMediaDecodingConfigurationoftype"media-source"and avideomember:contentTypevideo/mp4; codecs="<codec string>", the size, the bit rate and the frame rate. Where the query has no frame rate it does not ask:framerateandbitrateare required members, and a sink MUST NOT invent them.VideoDecoder.isConfigSupported()with aVideoDecoderConfigof the codec string, the coded size ascodedWidthandcodedHeight, and thehardwareAccelerationthe sink will configure, when it decodes the entry through WebCodecs.
2.2 Audio
| capability | codec string | sample rate | channels | description |
bit rate |
|---|---|---|---|---|---|
wai.audio.opus |
opus |
48000, the rate Opus decodes at (RFC 7845 §5.1: the input rate OpusHead states is informational) |
OpusHead's channel count |
the OpusHead packet |
the payload's bits × 48000 ÷ (the last granule position a page states − OpusHead's pre-skip), rounded up; none where that is not positive |
wai.audio.opus.packet |
opus |
48000 | 2 where the TOC byte's stereo flag is set, else 1 (RFC 6716 §3.1) | none | the payload's bits × 48000 ÷ the samples its TOC byte states, rounded up |
wai.audio.flac |
flac |
STREAMINFO's (RFC 9639 §8.2) |
STREAMINFO's |
the fLaC marker and every metadata block, as the payload holds them |
the payload's bits × the sample rate ÷ STREAMINFO's total samples, rounded up; none where it states 0 |
wai.audio.aac |
from the first ADTS frame header (ISO/IEC 14496-3 §1.A.3.2): for ID 0, mp4a.40. and the audio object type, profile + 1, in decimal; for ID 1, mp4a. and the object type indication of its profile, 66 (Main), 67 (LC) or 68 (SSR) (RFC 6381 §3.3) |
its sampling_frequency_index's |
its channel_configuration, 8 for 7 |
none | the payload's bits × the sample rate ÷ 1024 × the raw data blocks of every frame, rounded up |
An Ogg Opus payload is read whole as one logical Ogg bitstream (RFC 3533): pages that tile the
payload, each with the capture pattern, version 0 and its CRC, the first beginning the stream,
each of its serial number and the sequence number after the one before, and a packet
continuing onto a page exactly where that page says it does; then the identification header
(OpusHead, a major version of 0, one channel or more, at most two under mapping family 0,
a channel mapping table under any other), the comment header (OpusTags), and one audio
packet or more (RFC 7845 §3). An Opus packet's samples are its configuration's frame
duration times its frame count (RFC 6716 §3.1); an empty packet, a code 3 packet with no
frame count, and a packet of more than 120 ms are refused. A FLAC payload is the fLaC
marker, STREAMINFO first, then metadata blocks up to the one whose last-block flag is set,
then frames (§2.3); a STREAMINFO of a zero sample rate, a depth under 4 bits or a minimum
block size under 16 is refused. An ADTS payload is frames that tile it, each header with its
sync word, layer 0, a listed sampling frequency, a channel_configuration other than 0 (which
leaves the channels to a program configuration element the query does not read), the first
frame's configuration (ID, profile, sampling frequency, channels), and a length at least
its header's. An HE-AAC stream signalled implicitly, inside its raw data blocks, is asked
about as the object type and the core rate its headers state.
The sink asks:
navigator.mediaCapabilities.decodingInfo()with aMediaDecodingConfigurationoftype"media-source"and anaudiomember (Media Capabilities, W3C Working Draft, 9 November 2026,AudioConfiguration):contentTypeaudio/mp4; codecs="<codec string>",channelsas a string,samplerate, andbitratewhere the query has one. OnlycontentTypeis a required member, so a query with no bit rate is still asked.AudioDecoder.isConfigSupported()with anAudioDecoderConfigof the codec string, the sample rate, the channels asnumberOfChannels, and thedescription, when it decodes the entry through WebCodecs.AudioDecoderConfighas no hardware-acceleration preference.
2.3 What a decoder is given
The chunks a sink gives a WebCodecs decoder are read from the payload by the readers of §2.1
and §2.2, so a payload that gives no query gives no chunks, and each chunk's type is read
from its own bytes (platform_decode::chunks):
| capability | description |
a chunk | a key chunk |
|---|---|---|---|
wai.video.av1, wai.video.av1.lossless |
none | a temporal unit of SPEC §5's framing | its first frame header is of a sequence with reduced_still_picture_header 1, or has show_existing_frame 0 and frame_type KEY_FRAME |
wai.video.hevc, wai.video.avc |
none: the registrations' Annex B format | an access unit, from the start code of its first NAL unit to the next unit's | it holds an IRAP slice (H.265, nal_unit_type 16 to 23) or an IDR one (H.264, 5) |
wai.video.vp9, wai.video.vp8 |
none | an IVF frame's data | the first frame it holds is a key frame (VP9 frame_type KEY_FRAME with show_existing_frame 0; VP8 the frame tag's key-frame flag) |
wai.audio.opus |
the OpusHead packet: the registration's ogg format |
an audio packet | every chunk |
wai.audio.opus.packet |
none: the registration's opus format |
the payload | every chunk |
wai.audio.flac |
the fLaC marker and the metadata blocks |
a frame | every chunk |
wai.audio.aac |
none: the registration's adts format |
an ADTS frame, its header included | every chunk |
An access unit begins at the stream's first NAL unit, and then, after the last VCL NAL unit of the one before, at the first NAL unit of a type that begins one (H.264 §7.4.1.2.3: access unit delimiter, SPS, PPS, SEI, types 14 to 18; H.265 §7.4.2.4.4: access unit delimiter, VPS, SPS, PPS, prefix SEI, types 41 to 44 and 48 to 55) or at a slice that begins a picture; NAL units after the stream's last VCL unit stay in its unit. A FLAC frame begins with a frame header that reads (RFC 9639 §9.1: its sync code, no reserved block size, sample rate, channel or depth code, its reserved bit 0, its coded number, the uncommon block size and rate it calls for, and its CRC-8) and ends where its CRC-16 over the frame, footer included, is 0 and either the payload ends or the next frame's header reads. A video payload whose first chunk is not a key chunk is refused. An audio chunk's samples are those its own headers state: an Opus packet's TOC byte, a FLAC frame's block size, 1024 × an ADTS frame's raw data blocks.
2.4 The order of the questions
WebCodecs gives no smoothness or power answer, and Media Capabilities has no WebCodecs
decoding type. A sink that decodes through WebCodecs asks isConfigSupported() first, and
asks decodingInfo() of the media-source query of the same configuration only once
isConfigSupported() answers supported, and only for a codec string browser-sink §4.2's
registry gate passes: Media Capabilities is a further question about a decoder already
found (http-delivery §12.2). It reads smooth and powerEfficient from that answer as
hints and nothing more (§3).
3. The answers
A page enters what the platform said in browser-sink §3's capability report, one answer per
codec string, {supported, smooth, power_efficient, probe}, and in no other shape
(platform_decode::report_answer):
supportedisisConfigSupported()'s answer,VideoDecoder's for the preference the sink will configure orAudioDecoder's, alone. A Media Capabilitiessupportedis not read: amedia-sourceanswer is about the media-source pipeline, its container support included, not about the decoder WebCodecs found, so adecodingInfo()answer ofsupported: falseoverrides nothing.supported: falsemakes the user agent's leg unavailable for that object (browser-sink §4.3 step 4.5,ua_unsupported); the object then routes to a host or a wasm decoder where the page holds one, or nowhere. It is not a decode error and not a reason to try the decode anyway.smoothandpower_efficientaredecodingInfo()'s, entered only wheresupportedis true, andnullotherwise or where nothing was asked. They order the alternatives a page can decode, and only for an object routedua(browser-sink §4.7): an object the page routes to a host or a wasm decoder, because the user agent's tier is below what the object needs (AV1's user-agent tier isnone, browser-sink §4.1) or because no probe ran for a codec whose need isdecode-equivalence(browser-sink §4.4), is not ranked on an answer about a decoder that will not run it.powerEfficientis the user agent's judgement that the decode's power draw is optimal, its definition left to the user agent, which SHOULD NOT mark hardware decoding power-efficient by default (Media Capabilities §2.2). It is a hint, not a measurement and not evidence of hardware. A sink MUST NOT write it, or anything derived from it, into a receipt, a measurement claim or an energy figure, and MUST NOT present it as an acquisition class (energy-measurement §2).probeisnot_runfor the answers of this section: routing runs no probe (browser-sink §4.4). The probe of §6.2 decides only whether a grain-free AV1 decode is a refinement base, and changes no route.
One answer a codec string. The report keys answers by codec string, and two objects can
share one while their queries differ: two sizes of one AV1 configuration, an Opus stream of one
channel and another of two. A page that asks about several configurations under one codec
string enters one answer for it (platform_decode::merge_answers): supported only where
every configuration was answered supported; then smooth and power_efficient each true
where every answer's is true, false where any is false, and null otherwise. No object is
then routed to a decoder that refused its configuration, and none is ranked on a hint another
configuration earned.
The hardware-acceleration preference. prefer-hardware and prefer-software are
hints a user agent may ignore for any reason (WebCodecs §7.9); an audio decoder takes none. An isConfigSupported()
answer of supported: true with prefer-hardware says the platform accepts that
configuration with that preference; it is not evidence that the decode runs on hardware,
and a sink MUST NOT report it as such.
Privacy. The answers can be used to fingerprint the sink (Media Capabilities §3.2). A sink SHOULD NOT send them, or the order they give, to any party; the order is computed where the answers are. A platform that fakes its capabilities to resist fingerprinting (§3.2 recommends a fixed set rather than always yes or always no) gives answers the order uses as given: the result is a choice that platform made, and the decode still conforms.
4. The order the answers feed
This extension defines no order of its own, and no second meaning of "the sink advertises
cap". What a page advertises for an object is its route (browser-sink §4.3, SPEC §4).
Among alternatives the page can route, SPEC §4's energy order applies, as browser-sink §4.7
writes it: first for classical decode routed ua that the platform answers supported,
smooth and power-efficient, then smooth, then classical (any other routed classical
decode, and a learned one with a qualifying sink-metered figure in browser-sink §6's energy
entry), then learned-without-budget. A WAI1 envelope's capability and fallback are two
such alternatives. The order reads no figure's value (browser-sink §6).
5. Conformance
The reference implementation is wai::platform_decode (wai/wai-rs/src/platform_decode.rs):
query forms §2's query, chunks §2.3's chunks, and report_answer and merge_answers
§3's answer. It calls no platform: the answers are inputs. The vectors are in
wai/platform-decode-conformance/: queries.json holds, for every capability §2 maps,
payloads written by real encoders and muxers, each with its query and the summary of its
chunks (the packaging corpus's floor objects, whose codec string is the one the packager wrote
into its HLS master; an HEVC SPS the platform's encoder wrote through WebCodecs, configured
with prefer-hardware; the elementary streams of the identify and encode corpora; and
payloads/, VP8, VP9 4:4:4 and 10-bit BT.2020 full range, AVC High with B-frames and a
cropped height, stereo AAC, 24-bit FLAC, stereo Ogg Opus and one Opus packet, with what an
independent media prober read of each in payloads/prober.tsv), and planted faults, a payload
with one edit and the outcome it gives. The order's vectors are the choices of
live-conformance/browser/.
An implementation conforms when it gives every query's members and every chunk summary, and
refuses every case the vectors refuse. The reference's tests (platform_decode::tests) also
hold the AV1 codec string to the AV1-ISOBMFF binding's own examples, derive every floor of the
packaging corpus to the codec string and size the packager wrote into the pack's HLS master,
read constructed HEVC and H.264 SPSs (sub-layers, scaling lists, field coding, 4:2:2 and
4:4:4, emulation-prevention bytes), VP9 key frames of every profile and colour space, VP8 key
frames, Ogg pages, Opus TOC bytes and ADTS headers, hold §2.1's VP9 level table at its edges,
split Annex B streams into access units and AV1 payloads into key and other temporal units,
and hold report_answer and merge_answers to §3. live-conformance/browser_sink_verify.py,
given platform-decode-conformance/ as its second argument, is the second implementation:
written from this text, with its own readers of SPEC §5's AV1 framing, IVF, Annex B, Ogg, FLAC
and ADTS, and §2.1's tables read from the text, it recomputes every case and requires every
mutated copy of the vectors to fail.
wai-web's platform_decode bundle binds the same code for a page (platformQuery,
platformChunks, platformMapped, platformAnswer, platformMerge, and platformChoose,
browser-sink §4.7's choice), and wai-web/demo/platform_decode.html
(verify_platform_decode.mjs) runs it against a browser's platform through the composing
sink's own calls (compose.js uaAnswer and decodeUa): every AV1 floor object of the
packaging corpus and every payload of the vectors that is not a planted fault, queried from
the wasm32 build, which must read each codec string and chunk count the vectors state, and,
where the platform accepts a configuration, decoded through WebCodecs from §2.3's chunks;
HEVC and AVC streams the platform's encoders write, queried from the stream and decoded back;
and the choice between a learned alternative and an AV1 floor object, from the report the
page built from those answers. Every query the page asks is read from a payload.
Measured 2026-10-22 with that page on an arm64 desktop (macOS 27.0.1), in headless Chromium 148 and a Chromium-based browser at 155, with the same results in both, before the page's order moved into browser-sink's routing:
- every AV1 floor object of the packaging corpus (64×36,
av01.0.00M.08) decoded every temporal unit through WebCodecs. The platform answeredsupportedandsmooth, notpowerEfficient, and did not acceptprefer-hardwareat that size; - the platform's HEVC encoder, configured as
hvc1.1.6.L93.B0for 640×360 withprefer-hardware, wrote an SPS withgeneral_level_idc63, and the codec string it declared for its output repeated the configured one. The query readhev1.1.6.L63.B0from the stream; the platform accepted that configuration with both preferences, answeredsupported,smoothandpowerEfficient, and decoded every picture back with it.
Measured 2026-11-09 with that page and verify_platform_decode.mjs on an arm64 desktop
(macOS 27.2), in headless Chromium 148 and the WebKit build Playwright drives (26.4), over 65
payloads (the 27 of the vectors read as they are, and every AV1 floor object of the packaging
corpus):
- In both engines the wasm32 build read every codec string, chunk count and key-chunk count the vectors state.
- Chromium accepted every configuration and decoded every payload whole: AV1 (with
no-preferenceandprefer-software; it did not acceptprefer-hardwareat 64×36), HEVC, AVC (Constrained Baseline, High with B-frames and a cropped height, the platform's and a software encoder's streams), VP9 profiles 0, 1 (4:4:4) and 2 (10-bit), VP8, Ogg Opus, one Opus packet, 16- and 24-bit FLAC, and AAC-LC. It answeredsmoothand notpowerEfficientfor AV1, VP9 and AVC at 64×36 and below,powerEfficientfor AVC at 128×72 and 256×144, for HEVC and for every audio configuration, and neither for VP8. - WebKit decoded VP9 profiles 0 and 2, VP8, every AVC and HEVC payload, both Opus forms and
AAC-LC whole, and answered VP9 4:4:4 unsupported. It accepted AV1 at 64×36 with every
preference, and FLAC, and its decoders refused every one of those decodes (an
EncodingError), so the page met a refusal aftersupported(browser-sink §14.3: the next alternative is decoded); the composing page's AV1 floors, at 320×180, it decoded whole. It answeredsmoothandpowerEfficientfor AV1, VP9, AVC and HEVC, neither for VP8, Opus or AAC. - The platforms' encoders, configured with
prefer-hardware: Chromium's HEVC encoder wrotegeneral_level_idc63 where it was configured and declaredhvc1.1.6.L93.B0, and its AVC encoderlevel_idc30 where it was configured and declaredavc1.42001f; WebKit's AVC encoder wrote the constraint byteE0(avc1.42E01F) under the same declaration, and its HEVC encoder wrote a stream with no start codes though asked for Annex B, which the query refuses. Every stream the query read decoded back with the configuration it gave. - The choice: in Chromium the AV1 floor object ranked
smooth, in WebKitfirst; the learned alternative without a figure rankedlearned-without-budget, with the synthetic figureclassical, and the AV1 floor was chosen in both engines both times.
compose/run.mjs played the composing package in both engines with the dispatch asking the
user agent about every mapped alternative the page does not decode in WebAssembly (its AV1
floors; its FLAC floors routed to WebAssembly, browser-sink §14.3), every check holding. Not
measured: a platform that fakes its answers, and any engine but these two.
6. Grain-free AV1: a refinement base the platform decodes
AV1's decoding process is normative: a conforming decoder's output frames are identical to
the decoding process's, unless the payload signals film grain, whose synthesis is held only
to a perceptual criterion (AV1 §7.2; SPEC §5). A stream that signals no film grain decodes to
the same frames on every conforming decoder, the platform's included, and those frames are a
base a residual refinement can be written against once and applied on every sink. This
section defines that base. The layers are wai.video.int_refine (SPEC §5, Refinement
capabilities; staged-delivery §6.1); the base is the zeroth-condition
wai.video.av1 rendition, which a sink without WAI's integer decoders presents as it is.
6.1 The base
A wai.video.av1 payload is grain-free when every sequence header OBU it holds has
film_grain_params_present 0 (AV1 §5.5.1, the flag after color_config()), and then no
frame of it applies film grain synthesis. A sink reads the payload whole before it takes it
as a base (staged::av1::read_base), and refuses it, in this order, when:
- it does not read as SPEC §5's framing: exactly
ntemporal units, each of OBUs with their size fields and their forbidden bit 0, the first holding a sequence header (av1_malformed); - any sequence header has
film_grain_params_present1 (av1_film_grain); - any sequence header is not 8-bit 4:2:0 with chroma:
BitDepth8,mono_chrome0,subsampling_xandsubsampling_y1 (av1_format); - its frames pass the residual form's caps,
w·h·3 ≤ 2^26for one andn·w·h·3 ≤ 2^30for all (staged-delivery Appendix A.3 step 6) (av1_too_large).
A refused payload is not a base. The sink still presents it as SPEC §4 dispatches it; it holds
no stage set for it, so a layer naming it is held for a base and discarded at the hold bound
(base-absent, staged-delivery §4.3). A base's frames are what a decoder output for it, one
a temporal unit, each w × h as the framing states (av1_frames otherwise), read as §6.2's
samples.
6.2 The probe
A platform's answer of supported says its decoder accepts the configuration; it does not
say the decoder is conforming. A page shows that it is with a probe: a grain-free stream
of the same configuration (the codec string of §2 and the coded size) whose decoded frames'
SHA-256 digests are published. The digest of a frame is over its samples as WebCodecs
copyTo() writes them with no format, rect or layout option: I420, the luma plane,
then Cb, then Cr, rows tightly packed, the chroma planes ⌈w/2⌉ × ⌈h/2⌉; a frame output as
NV12 is compared after its interleaved chroma plane is split into its two planes, which
reorders samples and changes none (browser-sink §4.4). The probe is match when the decoder
output as many frames as the probe has and every frame's digest is the published one,
mismatch otherwise, and not_run when the page decoded none.
The base step of a grain-free AV1 base (staged-delivery §5) holds
decode-equivalence when its frames are the reference decode's, or a platform decoder's whose
probe for that configuration is match; otherwise it holds no tier. SPEC §5's Base
admission admits wai.video.int_refine over wai.video.av1 only where the base step holds
decode-equivalence, so a layer over a platform decode with no matching probe is refused
base-not-admitted. The probe never changes a route (browser-sink §4.3): a page decodes the
base where its routing sends it, and the probe decides only whether the frames are a base.
6.3 The units
The residual form refines RGB8 frames (staged-delivery §6.1). A grain-free AV1 base's unit
is its frame converted by the JFIF conversion (ITU-T T.871) in 16-bit fixed point, each chroma
sample covering the 2×2 luma samples it sits over. With Cb' = Cb − 128 and Cr' = Cr − 128:
R = clamp((Y·65536 + 91881·Cr' + 32768) >> 16)
G = clamp((Y·65536 − 22554·Cb' − 46802·Cr' + 32768) >> 16)
B = clamp((Y·65536 + 116130·Cb' + 32768) >> 16)
for the luma sample (i, j) and the chroma sample (i >> 1, j >> 1), where >> is the
arithmetic shift (floor division), every intermediate fits in 32 signed bits, and the clamp
is to [0, 255]. The conversion is part of the base's definition, not colour management: a
producer writes its layers against the units this conversion gives from the reference decode,
and a sink computes the same units from the same samples, natively or in a page. A sink that
presents a refined frame converts it for display as it would any RGB8 picture.
6.4 Conformance
The corpus is platform-decode-conformance/grain-free-av1/:
a grain-free stream (base.wai, 160×96, eight frames, av01.0.31M.08), its reference decode
(base.i420) and probe (probe.json), two wai.video.int_refine layers written against that
decode toward the source the stream was encoded from (layer1.wai, step 6; layer2.wai, step
1, which reaches the source), a stream with film grain (base-grain.wai) and a layer naming
it, and cases.json: the base alone; one layer and two layers over a decode whose probe
matched; two layers over the reference decode; the second layer offered first; one layer over
a decode whose probe did not run; and the stream with film grain. Each case states every
report, every unit's stage, producer, tier and stage hash, and the clip hash.
wai_av1_refine_vectors verify(featureav1_refine_vectors) decodesbase.waithrough libdav1d and requiresbase.i420byte for byte, refusesbase-grain.waibefore decoding it, and regenerates every other file but the two streams byte for byte.tests/av1_refine.rsreplays every case throughstaged::StageSink, finds the one bit that sets the grain flag ofbase.waiand requires the stream so changed to be refused, and requires a frame with one sample changed to change that frame's stage hash; with featureav1_baseit also decodes through libdav1d.verify.py, beside the corpus, is a second implementation: its own reader of the framing, the OBUs and the sequence header, its own codec string and conversion, and the sink ofstaged-conformance/staged_verify.py; it replays every case, requires the mutations above to fail, and with--dav1ddecodes the stream with the dav1d tool and requiresbase.i420.wai-web'sStageBase.fromAv1,av1Baseandav1Probebind the same Rust for a page;demo/verify_av1_refine_wasm.mjsreplays every case through the wasm32 build frombase.i420, anddemo/av1_refine.html(verify_av1_refine.mjs) decodesbase.waithrough the browser's WebCodecsVideoDecoderwith each hardware-acceleration preference the platform accepts, puts every decode to the probe, and replays every case from the frames of the decode configured withno-preference.
Measured 2026-11-09. libdav1d's decode of base.wai, through the library on an arm64 desktop
(macOS 27.2) and through its command-line tool, is base.i420 byte for byte; verify.py, the
wasm32 replay and tests/av1_refine.rs reproduce every case on that host, and verify.py and
the test on an x86_64 Linux host. In headless Chromium 156 on the same desktop, the platform
answered supported for no-preference and prefer-software and not for prefer-hardware
at 160×96; both decodes output eight I420 frames of 160×96, every one with its probe
digest, and every case reproduced from the no-preference frames, the stage hashes and clip
hashes the native sink gives.
In the WebKit build Playwright drives (27.2), on the same desktop, the platform answered
supported for all three preferences in one run; each decode output eight NV12 frames of
160×96, every one with its probe digest once its chroma plane was split, and every case
reproduced from the no-preference frames. In runs before it, on the same host and with the
same build, it answered isConfigSupported() false for every AV1 configuration asked (four
levels, three sizes, two preferences) while it answered true for VP9, AVC and HEVC, and in a
run after it false again: a page that is told so routes the object elsewhere
(ua_unsupported) and takes no base from it. Which decoder ran is not known; an answer to a preference is not evidence of
hardware (§3). The WPE build on an arm64 Linux host answered supported and failed every
decode: each AV1 decoder its GStreamer offered, two hardware and one software, refused the
caps the engine gave the stream (an OBU stream of 160×96 with alignment frame), and no
other AV1 decoder was installed.
7. Security and privacy considerations
The answers are fingerprinting surface (§3, Privacy). The order reads them where they are produced and sends nothing. The query reads only the payload's headers; a malformed payload gives no query, and the entry is decoded, or not, as it would be without this extension. An encoder cannot raise an entry's rank by what it declares: the codec string is read from the payload, the class from the registry, the route from browser-sink's routing, and a learned entry ranks with classical decode only on a sink-metered figure the sink holds, never on one an envelope carries.
A page cannot raise a platform decode to a refinement base by what it states: the probe's outcome is computed from the frames the decoder gave and the published digests, the grain flag is read from every sequence header of the payload, and the units are converted from the frames by §6.3. A stream that signals film grain anywhere is refused as a base before it is decoded. The probe's digests are of public decoded frames and reveal nothing of the page.