Part of the protocol reference: the records carried on every hop, the GATT byte stream and the compact-header and video-codec drafts.
The BMS1 hop header is 17 bytes:
| Field | Bytes |
|---|---|
Magic BMS1 |
4 |
| Type | 1 |
| Sequence | 8 |
| Ciphertext length including GCM tag | 4 |
The whole header is AES-GCM associated data. The nonce is four zero bytes followed by the 64-bit sequence. Each direction starts at zero and requires the exact next sequence. Active types are endpoint handshake (4), endpoint content (5), immediate-child heartbeat (6), serialized mesh envelope (7), group relay control (8), immediate-route withdrawal (9) and withdrawal acknowledgement (10). Removed plaintext-video types 1/2 and congestion-report type 3 are rejected. Store-and-forward type 11 is rejected as unknown by every reader except one that explicitly opts in on a link it owns (see "Store-and-forward messages").
The BME1 endpoint header is 18 bytes:
| Field | Bytes |
|---|---|
Magic BME1 |
4 |
| Content type | 1 |
| Sequence | 8 |
| Packet flags | 1 |
| Ciphertext length including GCM tag | 4 |
The header is also AES-GCM associated data, using the sequence nonce under the endpoint directional key. Endpoint opening accepts increasing sequence gaps after media drops but rejects replay and reordering; this differs from exact-next BMS1 hop ordering. Active types are stream header (1), media packet (2), lifecycle (4), keyframe request (5), private-call signal (6), wrapped broadcast-group key (7) and terminal acknowledgement (8). Congestion-report type 3 is rejected. Non-media content must have zero packet flags.
Media contains compressed video or Opus audio, distinguished by the audio flag. Visible flags permit relay routing and dependent-video drops until a recovery keyframe. The decrypted packet repeats those flags; a mismatch fails closed. Lifecycle payload is exactly one byte: LIVE=1, PAUSED=2, ENDED=3. Keyframe requests and terminal acknowledgements have empty payloads.
BTV3 contains video format, display rotation, audio codec, sample rate and channel count. Each inner packet has length, flags, one shared monotonic presentation timestamp and one compressed video/audio payload. It is accepted only inside authenticated BME1; Android rejects BTV1 and BTV2. The retained Windows normal receiver's older video-only BTV2 path still needs BTV3 audio/discovery integration.
Mesh routing and inner media flags use separate namespaces. BMV1 reserves bit 4 for lifecycle and bit 5 for terminal End; BTV3 uses those positions for DEBUG integrity and video discontinuity. Source serialization removes those two bits only from the outer media envelope. Full inner flags stay authenticated in BME1 and repeated in the decrypted packet. An opaque relay checks outer media flags against the mapped metadata. Only a lifecycle End gets the outer terminal bit; a recovery keyframe must not stop discovery or child delivery. Bit 7 (128) marks a video packet as discardable (a temporal enhancement frame whose droppability the source verified from the AV1/VP9 bitstream). Only the Experimental temporal-layering option (off by default) sets it; receivers accept it either way, and with the option off no queue skips anything.
The BTV3 header names a stream's or call's maximum picture size; the encoder may start below it and a ladder (the Auto stream ladder, the call resolution ladder) restarts the encoder at another size mid-stream. The coded size of every keyframe is carried by its bitstream: the VP9 keyframe header (frame_width_minus_1, frame_height_minus_1) or the AV1 sequence header (max_frame_width_minus_1, max_frame_height_minus_1) in the keyframe's temporal unit or, for an encoder that sends it only as configuration, in its codec-config bytes. No field, flag bit or record type was added: a record that older receivers cannot parse (a display-rotation record with a longer payload, a configuration record that is not a configuration) would break them, so the size is read from the bitstream and an unreadable keyframe leaves the receiver's decoder as it is. The sender marks the first keyframe of an encoder restarted at another size with the existing FLAG_DISCONTINUITY (bit 5, the inner flag; the mesh envelope never carries it). A receiver drops every queued frame before a discontinuity keyframe. Receivers that predate size-aware decoding treat it as a recovery point (flush, then keyframe); they see no other change. A restart that keeps the size (camera flip, camera off/on) is not marked. A size-aware receiver re-creates its decoder when a keyframe's coded size, or the codec configuration that arrived with it, differs from the decoder's: new configuration is held back and becomes the new decoder's csd (MediaCodec accepts a changed configuration only together with a keyframe, and AV1 is not among the formats Android documents for mid-stream changes); configuration equal to the decoder's is queued as before. Recovery re-queues the newest configuration, never the stream-start one. A source or relay keeps for late joiners only the codec-config packets of the current encoder: a video configuration packet that follows video media starts a new batch and replaces the cached video packets (audio configuration is kept). A relay sees only the outer mesh flags, so it cannot use the discontinuity bit; a source also drops its cached video configuration at a discontinuity keyframe that was not preceded by new configuration. Receivers record the change as BENCHMARK decoder_rebuild: reason=size|csd|size+csd | to=WxH ... and the decoder's adaptive-playback support in BENCHMARK decoder_format. The viewer's display transform follows the new size through the existing video-format callback.
On a GATT link the byte stream (the BMH handshakes, then BMS1 hop records) travels as ATT values: client writes without response in one direction and server notifications in the other. Every value starts with a type byte:
| Type | Layout | Reader |
|---|---|---|
1 data |
01 | sequence (u32, big-endian) | payload (1 or more bytes) |
Sequences start at 0 per direction and connection and grow by one per value. A duplicate of a recently accepted sequence (the last 4,096) is ignored; a later sequence is held until the missing ones arrive (bounded); an earlier sequence that is not a recent duplicate fails the link. |
2 |
anything | Ignored by every current reader (Android client and server, Windows, iOS), whatever its length. No current sender emits it. |
| other | Windows and iOS fail the link; Android ignores the value. |
Chunk headers are not authenticated: the sequence only orders the bytes, and integrity comes from BMS1, whose exact-next record sequence fails closed on any missing, extra or altered byte. Since the lost-chunk fail-fast (default on, test_gatt_gap_fast_fail), an Android reader ends the stream when a sequence gap lasts 1 s or 128 later values wait behind it (DECISIONS "Design GR", option A).
Draft: lost-chunk recovery GR1 (Design GR option C; revised after review 1; Android code behind a default-off switch, not yet run on phones)
Switch: test_gatt_chunk_recovery (on by default since 2026-10-21, design decision Q-GR1-DEFAULT A; DEBUG false is the control arm; Android GattChunkRecovery.kt), and the same setting on Windows when built. Reasons, measurements and review 1: DECISIONS "Design GR". The constants below are part of the contract: every platform uses the same values.
Sequence arithmetic. Chunk sequences are compared modulo 2^32: a is after b when (a - b) mod 2^32 is between 1 and 2^31 - 1. Every distance below is computed this way.
Messages. Every message is a type-2 value, so a reader without GR1 ignores it:
02 | 47 52 ("GR") | op (1) | body
op 1 HELLO body: version (1) = 1 | history values (u16) | history bytes (u32) | history ms (u16) 13 bytes in all
op 2 NACK body: count (1), 1..8 | count x ( first sequence (u32) | run (1), 1..255 ) 4 + 1 + 5 x count bytes
op 3 GONE body: first sequence (u32) | run (1), 1..255 9 bytes
op 4 HIGH body: last data sequence sent (u32) 8 bytes
All integers are big-endian. A NACK carries as many ranges as fit the link's value limit (three at the 20-byte limit of the default 23-byte MTU). A GR1 value with an unknown op, a version other than 1, a wrong length, a count of 0 or above 8, a run of 0, or a HELLO announcing 0 values or 0 bytes is ignored. Any other type-2 value is ignored as before. GR1 values carry no sequence of their own, never enter the byte stream, and do not count as hearing the peer (liveness and arrival measurements count data values only).
Capability.
- Each side sends HELLO when its end of the link can send values (the client after the notification descriptor write and the MTU exchange; the server after the client's descriptor write), and once more 1,000 ms later.
- HELLO announces the history it keeps. A reader clamps the announced numbers to this build's own limits (1,024 values, 262,144 bytes, 2,000 ms), so a larger claim cannot widen what it waits for.
- HELLO only tells the reader that it may wait for resends. A reader waits (instead of the option A fail-fast) only after it has received the peer's HELLO and its own switch is on.
- A sender keeps its history and answers NACKs whenever its own switch is on, from sequence 0, whether or not the peer's HELLO arrived: a NACK itself shows that the peer has
GR1. - If a HELLO is lost, only that direction's reader keeps option A.
- HELLO is unauthenticated, like the chunk header. Stripping it only downgrades to option A; forging it can only make this side send NACKs that an older peer ignores.
Sender (switch on).
- History. It keeps every data value it sends, byte for byte, until the value is older than 2,000 ms, or the history holds more than 1,024 values or more than 262,144 bytes (oldest dropped first). HELLO announces these three numbers.
- The 1,024-value bound keeps every resend inside the reader's 4,096-sequence duplicate window.
- The history holds its own copy, never changed after the send. A resend is that copy; no path re-chunks or re-encrypts it.
- NACK. For each requested sequence:
- Already sent and still in history: resend the stored value unchanged (same type byte, same sequence, same payload), before any new data value.
- Already sent but no longer held: answer GONE (consecutive sequences in one range).
- Not yet sent (above the last sequence sent): skip it. After 8 such ranges on one link, ignore its NACKs for the rest of the connection and log it.
- Limits.
- Resent bytes per rolling second are at most 25% of the data bytes this link carried in the last second (its measured budget), and at least 1,024 bytes per second, so a quiet link can still resend.
- Each sequence is resent at most 3 times per link.
- A request beyond a limit is skipped, not queued, and logged (
event=capped); the reader then fails at its deadline.
- HIGH. When 200 ms pass after a data value with no further data value, the sender sends HIGH with the last data sequence sent; at most 2 HIGH per second. This lets a reader notice that the last value before a pause was lost.
- Pacing. Resends and
GR1values count against the link's pacing and byte window like any value. They never change the requested bitrate, frame rate or resolution. - Sending. Control values and resends go out before the next new data value. They are sent by the link's own writer (the data writer between values, or a recovery writer while it is idle), never from a Bluetooth receive callback: a blocking send there would deadlock. On Android a server notification waits for
onNotificationSentand a client write foronCharacteristicWrite, both on the callback path. A value already handed to the stack is not preempted: an outstanding acknowledged write finishes first.
Reader (switch on and the peer's HELLO received).
- Missing sequences. The reader keeps them as ranges.
- When a data value is held behind a gap, every sequence from the next expected one up to the highest held one that has not arrived is missing.
- A HIGH whose sequence is at or after the next expected one, and not yet received, makes the sequences from the next expected one through the HIGH sequence missing.
- Targets are bounded: only sequences less than
min(peer's history values, 1,024)after the next expected one are requested. A HIGH beyond that is ignored and logged.
- NACK. It sends a NACK at once for missing sequences not requested in the last 250 ms.
- Each sequence is requested at most 3 times: when first missing, then at 250 ms and 500 ms if still missing.
- At most 20 NACK values per second per link.
- NACK and HELLO go before any data value already queued in this side's own sending direction.
- Delivery. A resent value is an ordinary data value: the reader delivers it at its place and releases what was held behind it. A copy that arrives after the original was delivered is a duplicate and is ignored.
- End. The reader ends the stream when any of these happens:
- GONE for a missing sequence: "GATT chunk N was lost and the peer no longer holds it".
- A sequence still missing 1,000 ms after it was first missing: "GATT chunk N was lost (3 requests, T ms)".
- More values or bytes held behind the gap than the peer's announced history: the sender can no longer have the chunk.
- The option A 128-value limit does not apply while recovery is in use; the 1,000 ms deadline does.
Records and encryption. A resend is the original value, never a new encryption.
- Every
BMS1record inside it keeps its sequence number, its nonce (four zero bytes and the 64-bit record sequence) and its ciphertext. - The reader delivers each data sequence exactly once and in order, so
BMS1receives the same byte stream the sender produced and its exact-next rule is unchanged. EndpointBME1records and relayed ciphertext inside it are untouched. - A sender must not rebuild a lost value by encrypting again: a record re-sealed under a new sequence would break the reader's record order, and one re-sealed under its old sequence would reuse a nonce.
Scope. GR1 is hop-local: it is between the two ends of one GATT connection, and a relay never forwards a NACK or a resend to another hop. It covers notifications and writes without response. A client using acknowledged writes (the legacy Qualcomm relay-call flush policy, or a Windows write with response) loses no value silently: a request lost in the receiving host stalls the client's ATT transaction until its timeout ends the link. LE CoC, RFCOMM and the Windows normal receiver's other transports are unchanged.
Threats.
- An on-link attacker who can inject values could forge a NACK. It can make the sender resend ciphertext it already sent over the air, within 25% of the link's measured budget, and the reader ignores the copies.
- It could forge a GONE or a HIGH to end the stream, or forge a resend whose bytes fail
BMS1and end the link. All three equal its existing power to inject a malformed data value. - No plaintext, key or nonce is exposed or reused.
- Passive observers already see chunk headers. HELLO also shows that the build has recovery, which adds little to the GATT service UUID that already marks the app.
Logs (Android). Every line is BENCHMARK gatt_recovery: event=<event> | ... | link=<client|server>:
| Event | Fields |
|---|---|
hello_sent, hello |
n; the peer's clamped historyValues, historyBytes, historyMs |
nack, nack_received |
seq, ranges, round; queued, gone, exhausted |
resend |
seq, bytes, queuedAheadMs (from the NACK's arrival to the resend's send) |
capped |
values, budgetBytesPerSecond |
recovered |
seq, count, waitedMs, rounds |
gone_sent, high_sent |
seq |
failed, ignored, nacks_ignored, writer_stopped |
reason |
The DEBUG hook test_gatt_drop_received_every=N drops the first copy of every received data value whose sequence is N-1 mod N and logs BENCHMARK gatt_debug_drop: seq=<s> | every=<N>, so option A or recovery can be forced on any pair.
Status: wire draft only. Nothing here is sent by any build. Reasons, savings and review questions: DECISIONS "Design CH". Every compact form below is a different encoding of the same logical record: AEAD keys, nonces, associated data, sequence rules and checks are those of BMS1, BME1, BMV1 and BTV3, and a node can always rewrite a compact header into the full one without keys.
| Scope | Signal | Meaning when both peers carry it |
|---|---|---|
| Hop (one link) | Link-hello decode-mask bit 6 (0x40) |
CH1 hop features: BMS2 hop headers, BMV2 stream indexes, hop type 15 BUNDLE, and on GATT the chunk type 3 |
| End to end | End-to-end hello transport-mask bit 6 (0x40) |
CH1 endpoint features: BME2 headers; on calls also content type 13 (compact media packet), BTA3 audio batches and Opus packets of 10 to 60 ms |
- The link-hello transport mask has no free bit (3
BDO1, 4SETUP, 5 reserved by the Classic draft, 6 call relay, 7 build tag). Decode-mask bits 0-1 are codecs and bit 7 is used only in end-to-end call hellos. Every current reader tests codec bits with a mask, so an unknown decode bit in a link hello is ignored. Senders must never copy bit 6 into an announcement's capability summary, where0x40means audio-only. - A bit is signed inside the hello, so it cannot be added or stripped without breaking the handshake. Clearing it is always allowed and gives the current wire exactly.
- Every compact form is self-describing by its first byte (below). A sender may switch to it at any record after it has read the peer's hello carrying the bit, and a reader that advertised the bit accepts both forms on every record. No switch-over point needs to be agreed. A reader that did not advertise the bit treats a compact form exactly as today: an invalid magic or unknown type closes the link.
byte 0 : 0x80 | type (type = the BMS1 hop record type, 1..127)
bytes 1..n : ciphertext length incl. 16-byte tag, unsigned LEB128, minimal encoding, 1..4 bytes
then : ciphertext and tag, exactly as BMS1
BMS1starts with0x42(B), and aBMS2first byte is0x80or more, so the two forms never collide.- The sequence is implicit. It is the hop counter both sides already keep, because
BMS1requires exactly the next sequence. - Associated data is the canonical 17-byte
BMS1header (BMS1, type, u64 sequence, u32 length), rebuilt by the reader. The nonce is unchanged: 4 zero bytes followed by the u64 sequence. - A non-minimal or 5-byte length, a length above the reader's limit, or type 0 fails closed. The header is 2 to 4 bytes instead of 17.
byte 0 : 0x80 | content type
byte 1 : packet flags (as BME1)
bytes 2..3 : low 16 bits of the absolute sequence (u16, big-endian)
bytes : ciphertext length incl. tag, unsigned LEB128, minimal, 1..4 bytes
then : ciphertext and tag, exactly as BME1
- Associated data is the canonical 18-byte
BME1header with the absolute sequence. The nonce is unchanged. - The opener rebuilds the absolute sequence as the smallest value greater than its last accepted sequence whose low 16 bits match. This keeps the current rule (strictly increasing; gaps allowed after media drops), whoever dropped records: the sender, a relay or a forwarder.
- A replayed or reordered record rebuilds to a later sequence than it was sealed with, so its tag fails.
- A run of more than 65,535 consecutive missing records cannot be rebuilt and fails closed, as a lost link would. A forwarder that drops that many records towards one child sends that child a full
BME1header.
- The header is 5 to 8 bytes instead of 18.
- Rewriting. A node that copies endpoint records it cannot open (an opaque call relay, a stream forwarder towards its child) may pass
BME2on unchanged only to a next hop whose endpoint session negotiatedCH1; otherwise it rewrites the header toBME1from the absolute sequence it tracks for that stream. The ciphertext never changes.- A call relay, even of an older build, copies records unchanged, because both call endpoints negotiated end to end and drops do not break the rebuild.
plaintext : PTS delta (signed, zigzag LEB128, minimal) | packet payload (to the end)
- The packet flags are the record's authenticated
BME2flags; there is no repeated flag byte and no length field. PTS = previous PTS of the same class + delta. The classes are audio, video and other (display rotation), and each starts at 0 after the stream header.- It replaces
BTV3media packets on call endpoints that both carry the end-to-end bit (direct and relayed calls). - Fragments (type 9) keep carrying an encoded
BTV3packet, whose 13 bytes are paid once per fragmented frame. - Streams keep
BTV3: a forwarder copies the source's sealed packet, so every receiver of one sealing must understand it.
magic 'BTA3' (4) | count (1, 2..20) | unit duration code (1: 1 = 10 ms, 2 = 20 ms, 3 = 40 ms, 4 = 60 ms)
| count x unit length (unsigned LEB128, minimal, 1..2 bytes, each 1..1275) | units back to back
- Unit
ihasPTS = packet PTS + i x duration. - A sender whose units are not evenly spaced (DTX, a capture gap) uses
BTA1/BTA2as today. - Readers keep accepting
BTA1andBTA2. - Opus packets of 10 to 60 ms (one frame per packet, TOC config by duration) are allowed only between call endpoints that both carry the end-to-end bit. Everything else stays 20 ms.
byte 0 : 0x80 | stream index (0..15) (BMV1 starts with 0x42)
bytes 1..2 : low 16 bits of the mesh sequence (u16, big-endian), rebuilt as for BME2
byte : TTL
byte : mesh flags
then : the endpoint record (BME1 or BME2), to the end of the hop record
- Index assignment. On each link direction, the first envelope of a (stream ID, source node ID) pair is always a full
BMV1. That pair takes the next free index in order of first appearance (0, 1, ...). Hop ordering is exact, so both sides assign the same index.- A pair beyond index 15 stays
BMV1. - An index is never reused on that link.
- A pair beyond index 15 stays
- The reader applies every current
BMV1check (source equals the endpoint peer, TTL, increasing sequence, duplicate window) to the rebuilt envelope. - A forwarder rebuilds the full envelope before deciding what to send to each child, and encodes per child.
- The header is 5 bytes instead of 50.
plaintext : item count (1, 2..64) | count x [ inner hop type (1) | length (unsigned LEB128, minimal) | inner plaintext ]
- Inner types allowed: 5 (endpoint content), 6 (heartbeat or report) and 7 (mesh envelope). Any other type, an empty item, or trailing bytes fails closed.
- The items are processed in order, exactly as consecutive hop records would be. The bundle uses one hop sequence number and one tag.
- A sender bundles only records that are already queued when it flushes; it never waits to fill a bundle.
03 | sequence (u16, big-endian) | payload (1 or more bytes)
- Its sequence is the low 16 bits of the type 1 sequence counter.
- A reader rebuilds the 32-bit sequence as the unique value within 32,768 of the next expected one. Duplicate, hold and gap rules are those of type 1; the 4,096-value duplicate window and the
GR1limits fit inside 65,536. - A sender uses type 3 only after it has read a peer link hello carrying the hop bit, so the handshake values themselves stay type 1.
- Windows and iOS readers fail on an unknown chunk type, so the bit is the only gate.
- Handshakes (
BMH2,BMH3),BDO1,BRS1,BFG1,BGK1, labels andBMC2payloads, call video fragments, store-and-forward records, all AEAD keys, nonces and tags (16 bytes). - The exact-next hop sequence, the increasing endpoint sequence, mesh replay and duplicate rules, admission and overload guards.
- Separately reviewed proposals, not part of
CH1: one AEAD layer on direct calls, and amortised signatures on audio-only live records (DECISIONS "Design CH", sections P3 and P6).
Draft: HEVC and H.264 video codecs (Design HC, revision 3 after reviews 1 and 2, ready for implementation)
Status: wire draft only. Nothing sends it.
- Reasons, platform coverage, switches and open questions: DECISIONS "Design HC".
- Numbers: "Draft number registry". The H.264 numbers are the ones pencilled in DECISIONS "iPhone interoperability".
- With the masters
hevc_codecandh264_codecoff (the default before 0.4.2; on since), no HEVC or H.264 bit is set anywhere and every byte is the two-codec format.
| Where | H.264 (AVC) | HEVC (H.265) |
|---|---|---|
BTV3 video codec ID |
4 | 5 |
| Encode and decode masks, link and end-to-end hellos | bit 2 (0x04) |
bit 3 (0x08) |
Announcement capabilitySummary (call offers and video stream offers) |
bit 2 (0x04) |
bit 7 (0x80) |
| Compact (iPhone) UUID codec bits | 56 (pencilled first) | none (proposed in open question Q-HC2: take bit 56, so H.264 has none) |
End-to-end decode-mask bit 4 (0x10, DECODE_AV1_SOFTWARE) means "my AV1 decoding is software". It is carried only in end-to-end call hellos, like bit 7: link hellos never carry it, it is not a codec, and codec tests ignore it.
One mapping table. The hello bits and the summary bits differ: summary bit 3 is the dual-listening hint and bit 4 is relay consent. Implementations map codec ID, hello bit and summary bit with one explicit table (VP9 0x01, AV1 0x02, H.264 0x04, HEVC 0x80), and never copy a hello mask into a summary. A video offer never sets bit 6; the audio-only marker stays exactly 0x40. The pencilled end-to-end bit 6 ("prefer H.264") is not used and stays pencilled.
Old readers. Every shipped reader tests codec bits with a mask: Android uses bits 0-1 (codecCapabilityMask), iOS & 0x3, Windows & 7 against a decode mask that has no bit 2. For them, summary bits 2 and 7 and hello bits 2 to 4 mean "no common codec" or nothing. Results:
- A 22-byte stream offer in only these codecs is listed dimmed as not decodable and cannot be joined.
- A PC announcement kind-2 offer (Design QV) with
0x80is hidden by strict readers without HEVC (Android before 0.4.2, or with the HEVC master off); with the master on it is listed, and dimmed like any offer this phone cannot decode. - A
BTV3header with codec 4 or 5 fails closed as an unsupported codec. - No reader rejects an unknown mask bit.
-
Calls. Each sender chooses for its own direction from the peer's end-to-end decode mask, which relays copy unchanged.
- A family
CallVp9Compatibilitypolicy on the sender gives VP9 when the selected codec is 3, 4 or 5. - Peer bit 7 (prefer VP9), counted only when the peer also sets VP9 bit 0: a sender that can encode VP9 sends VP9, unless it has a hardware HEVC encoder and the peer decodes HEVC; then hardware HEVC comes first and VP9 second. Bit 7 still puts VP9 above AV1, H.264 and software encoders.
- Otherwise the first eligible encoder whose codec the peer decodes, in the order hardware AV1, hardware HEVC, hardware VP9, hardware H.264, then today's software order. If peer bit 4 is set, AV1 moves below the sender's hardware HEVC and hardware VP9 encoders that the peer decodes, and only below those.
These steps build one ordered, filtered list per direction, and fallback walks only that list. A codec whose prefer switch is off is not in the Automatic list at all. A manual choice the peer decodes is sent as chosen; steps 2 and 3 do not override it. A receiver checks the header's codec against its own advertised decode mask, as today.
- A family
-
Streams. The source's order follows open question Q-HC1 (answered A on 2026-10-28: hardware AV1, hardware VP9, hardware HEVC, hardware H.264, then software, except that the Android app's bundled libvpx VP9 encoder sits between hardware HEVC and hardware H.264, tied to the coverage table in DECISIONS). The source checks no viewer. Admission refuses a viewer or forwarder whose decode mask lacks the codec's bit, as for any codec. A forwarder derives its offer summary from the authenticated header through the table.
-
Eligible encoders.
- Hardware only: MediaCodec
isHardwareAccelerated, VideoToolbox withRequireHardwareAcceleratedVideoEncoder, Media Foundation hardware MFTs. - The size range and the measured or achievable frame rate cover the session.
- CBR is explicit where the encoder declares it. An encoder whose only mode is CQ is not eligible. An encoder that declares no modes keeps its resolved mode, with the requested and effective modes logged.
- B-frames and temporal layers are disabled.
- Hardware only: MediaCodec
-
Decode bits mean the whole wire envelope. The H.264 bit means Constrained Baseline, Main and High (8-bit 4:2:0); the HEVC bit means Main. Each bit is advertised only when the full decode path is probed at the advertised maximum size and FPS; hardware or platform software decoders both count. A sender never infers decoder support from its own encoder list.
-
Compact discovery. While Q-HC2 is open, HEVC is not representable. An iPhone does not stream HEVC, and compact call offers carry their VP9/AV1 bits and negotiate HEVC and H.264 in the hello. If Q-HC2 is answered A:
- bit 56 means HEVC, and current decoders map it explicitly to and from summary
0x80(never by shifting the summary); - shipped decoders map it to summary bit 2 and find no common codec;
- no compact sender announces an H.264 stream.
- bit 56 means HEVC, and current decoders map it explicitly to and from summary
-
PC call offer (PC announcement kind 0). Its packed three-bit codec field (byte 8, bits 0-2, below the route cost in bits 3-6) carries VP9, AV1 and H.264 hints only. HEVC is never encoded there and is negotiated only in the hello. A PC always has a representable fallback (VP9).
-
Unchanged. An open-broadcast source (
BOB1CARD) uses VP9 or AV1 only.BTL1and the call fragment format are unchanged. The PC announcement's kind-2 summary rule (Design QV) accepts0x80while the HEVC master is on (Android 0.4.2).
- Byte stream. Annex B, one access unit (AU) per
BTV3video packet, exactly one coded picture in exactly one slice. The discardable flag (bit 7) is never set. Wider subsets (several slices, CRA) need a later revision with vectors. - No codec-config packet. A
FLAG_CODEC_CONFIGvideo packet is never sent for codecs 4 and 5. - Parameter sets (HEVC VPS, SPS and PPS; H.264 SPS and PPS) go in-band, in that order, before the first slice of every keyframe AU, and in no other AU.
- The sender merges the encoder's consecutive codec-config buffers into one cached set, replacing the cache when the encoder emits new ones.
- It prepends the cached sets to every keyframe AU whose own sets are missing, and strips sets from non-keyframe AUs.
- It does not ask the encoder to prepend them (
KEY_PREPEND_HEADER_TO_SYNC_FRAMESis not used).
- Keyframes. A keyframe is an IDR: HEVC
IDR_W_RADL(19) orIDR_N_LP(20), or H.264 type 5. CRA is not sent. TheBTV3keyframe flag is set from the NAL type, never from the encoder's buffer flag. Closed GOP is configured where offered. - Keyframe requests. A keyframe request must be answered by an IDR, judged from the emitted NAL types. If none follows within 1 s the request is repeated. After 3 misses the sender ends video with
no_idrand the component is ineligible for this process. - Order. No B-frames or reordered pictures, and
BTV3presentation timestamps strictly increase.- HEVC:
sps_max_num_reorder_pics0. - H.264: only I and P slices, and either
pic_order_cnt_type2 or a VUIbitstream_restrictionwithmax_num_reorder_frames0.
- HEVC:
- Stripped. SEI, AUD and filler data are removed.
- Size changes. As for VP9/AV1: a
FLAG_DISCONTINUITYIDR whose in-band SPS carries the new size. The coded size is the SPS size minus its cropping:- HEVC:
pic_width_in_luma_samplesandpic_height_in_luma_samplesminus twice the conformance-window offsets; - H.264:
(pic_width_in_mbs_minus1 + 1) x 16and(pic_height_in_map_units_minus1 + 1) x 16minus twice the frame-crop offsets.
- HEVC:
A bounded structural parser checks every packet before a decoder sees it. It reads only the fields named here. Everything else in the codec syntax is left to the platform decoder: the parser is a gate, not a sanitizer, and an AU that passes still reaches that decoder.
Outcomes.
- A failed AU is dropped as corrupt video under the existing rules (drop, request a keyframe).
- A
FLAG_CODEC_CONFIGvideo packet, orBTV3flag bit 7 (discardable), on codec 4 or 5 closes the session as an unsupported stream. - Three rejected keyframes in a row within 10 s also close it.
Scanning and escapes. One pass over the payload finds start codes and NAL units and checks the escapes. Inside a NAL unit:
00 00 03followed by00,01,02or03is an escape, and only that03is removed;00 00 03as the last bytes of the NAL unit is allowed;00 00 00,00 00 01and00 00 02end the NAL unit or are forbidden there;00 00 03followed by a byte above03rejects the AU.
Parse budget. Parameter-set parsing reads at most the first 512 unescaped bytes, and slice-header parsing the first 64. If a field this section needs lies beyond the budget, or a read passes the end, the AU is rejected. A ue(v) with more than 31 leading zeros, or an Exp-Golomb value that overflows 32 bits, also rejects it. Loops (scaling lists, HRD CPB entries, POC cycle offsets) are bounded by their specification maxima.
Log only. Values above the usual range are accepted up to the specification's maximum and logged as au_unusual.
- Framing. The payload is 1 byte up to the
BTV3video payload limit and starts with00 00 01or00 00 00 01. Zero bytes before a start code are trailing zeros. An AU has at most 8 NAL units, each of at least the NAL header size. - NAL header.
- HEVC:
forbidden_zero_bit0,nuh_layer_id0,nuh_temporal_id_plus11. - H.264:
forbidden_zero_bit0.
- HEVC:
- Allowed types. Any other type rejects the AU, including HEVC CRA (21), which is deferred.
- HEVC: 0 and 1 (trailing), 19 and 20 (IDR), 32 VPS, 33 SPS, 34 PPS, 35 AUD, 38 filler, 39 and 40 SEI.
- H.264: 1 (non-IDR slice), 5 (IDR slice), 6 SEI, 7 SPS, 8 PPS, 9 AUD, 12 filler.
- One picture, one slice. Exactly one slice NAL unit per AU. Parameter sets, AUD and prefix SEI come before it; filler and HEVC suffix SEI come after it.
- HEVC slice header.
first_slice_segment_in_pic_flagmust be 1. Thenno_output_of_prior_pics_flag(IDR only) andslice_pic_parameter_set_id. Thennum_extra_slice_header_bitsreserved bits, the count taken from the referenced PPS. Thenslice_type, which must be 1 (P) or 2 (I), and 2 in an IDR. - H.264 slice header.
first_mb_in_slice0,slice_type0, 2, 5 or 7 (P or I; I only in an IDR), thenpic_parameter_set_id.
- HEVC slice header.
- Keyframes and parameter-set activation.
- The keyframe flag is set if and only if the slice is an IDR.
- An IDR AU carries exactly one VPS (HEVC), one SPS and one PPS, in that order, each at most 1,024 bytes.
- The chain must resolve inside that AU: slice PPS ID to that PPS, PPS SPS ID to that SPS, and the HEVC SPS VPS ID to that VPS.
- Only after the whole AU passes does this set become the active configuration, atomically. If it differs from the previous one, the decoder is flushed and rebuilt before the AU is queued.
- A non-IDR AU carries no parameter set, and its slice must name the active PPS.
- A rejected IDR leaves the previous active configuration. Non-IDR AUs are then dropped until the next valid IDR.
- Session envelope (rules 8 and 9). Both coded dimensions, not just the visible ones, are bounded per axis. Either orientation is accepted: the larger side is compared with the larger header side. Crops use checked arithmetic. Crop offsets are in the codec's crop units (2 luma samples for 4:2:0 frames), and the visible width and height must stay at least 16.
- H.264: each coded side is at most the
BTV3header's maximum width or height rounded up to a multiple of 16 (macroblocks). - HEVC: each coded side is a multiple of 8 (the smallest MinCbSizeY) and exceeds its cropped (conformance-window) side by less than 64 (the largest coding tree block), and the cropped size fits the header's maximum width and height. An encoder that pads to its coding-tree size and crops back (NVIDIA: 426x240 coded 448x256) is accepted; the coded size stays below the header plus 64 per axis, and the DPB limit is unchanged.
- H.264: each coded side is at most the
- HEVC VPS.
vps_max_layers_minus10 andvps_max_sub_layers_minus10. - HEVC SPS.
sps_max_sub_layers_minus10, soprofile_tier_levelis the fixed 12-byte general part.general_profile_space0, andgeneral_profile_idc1 orgeneral_profile_compatibility_flag[1]set (Main).chroma_format_idc1, and both bit depths 8.pic_width_in_luma_samplesandpic_height_in_luma_sampleswithin the envelope; the conformance window as in rule 6.sps_max_num_reorder_pics0.- Log only:
general_level_idcabove 123, up to 186;sps_max_dec_pic_buffering_minus1above 5, up to 15.
- H.264 SPS.
profile_idc66, 77 or 100 (no constraint-flag requirement; rule 10 excludes the Baseline features we do not use).- For profile 100:
chroma_format_idc1, both bit depths 8, and scaling lists parsed and accepted. pic_order_cnt_type2, or 0 or 1 with VUIbitstream_restriction_flag1 andmax_num_reorder_frames0.frame_mbs_only_flag1.- Macroblock width and height within the envelope; frame cropping as in rule 6.
- Log only:
level_idcabove 41, up to 62;max_num_ref_framesabove 4, up to 16.
- PPS.
- HEVC:
pps_pic_parameter_set_idat most 63, andnum_extra_slice_header_bitsread for rule 4. - H.264:
pic_parameter_set_idat most 255,num_slice_groups_minus10 (no FMO),redundant_pic_cnt_present_flag0.
- HEVC:
- Removed. SEI, AUD and filler are removed before decoding; their contents are never parsed.
Decoder input.
- Decoders run in low-latency mode where offered: Android
KEY_LOW_LATENCYwhenFEATURE_LowLatencyis supported, Media FoundationCODECAPI_AVLowLatencyMode. - Android queues the Annex B AU. csd comes from the active configuration: H.264 csd-0 SPS and csd-1 PPS; HEVC csd-0 VPS+SPS+PPS. The decoder is rebuilt when the sets or the size change.
- Apple converts to 4-byte length-prefixed NAL units, builds the format description from the keyframe's sets (
CMVideoFormatDescriptionCreateFromHEVCParameterSetsor...FromH264ParameterSets) and rebuilds the session when they change. - Windows feeds Annex B to the
MFVideoFormat_HEVCorMFVideoFormat_H264decoder MFT, which loads in the app's process.
Logs.
BENCHMARK encoder_choice: ... codec=HEVC|H264 | reason=...(DECISIONS "Design HC").BENCHMARK au_reject: codec= | rule=<1..11> | type= | bytes=.BENCHMARK au_unusual: codec= | field= | value=.BENCHMARK decoder_hold: codec= | p50_ms= | p95_ms=.
Vectors (written with the code): a keyframe AU and a non-keyframe AU captured from each platform's hardware encoder for each codec, plus one negative AU per rule above.