# WAI Extension: LOC Carriage

> Status: Draft, tracking its drafts as [moq-streaming-format](moq-streaming-format.md) does.
> Maps a WAI envelope and its receipt onto one object of a track of packaging `loc`: the Low
> Overhead Media Container of `draft-ietf-moq-loc-04` (20 July 2026), the packaging
> `draft-ietf-moq-msf-01` §4.1 names, over `draft-ietf-moq-transport-22`. A WAI envelope whose
> capability's payload is one chunk of a codec the WebCodecs Codec Registry registers travels as
> one LOC object: its payload is the LOC Payload, unchanged; its manifest is an Immutable
> Property, `WAI_MANIFEST`; its receipt commitment is the `WAI_RECEIPT` property of
> moq-streaming-format §5. A subscriber that reads LOC and nothing else hands the payload to its
> decoder as a chunk; a subscriber that reads WAI rebuilds the envelope byte for byte and checks
> its receipt against it. No new transport. The reference implementation is the `loc` module of
> `joule-moq` (`joule-code/joule-code-rs/crates/joule-moq`); the corpus is
> [`live-conformance/loc/`](../live-conformance/loc/), and its second implementation is
> `live-conformance/loc_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> A section number of LOC, MSF or MOQT here is one of those revisions. Where a later revision
> renumbers or renames what this text cites, the citation is what changes (moq-streaming-format
> §6).

## 1. Scope

§1.1 Packaging `wai` (moq-streaming-format §3) carries whole envelopes, and a subscriber that
does not read WAI cannot use its objects. Packaging `loc` carries a codec's chunks, which any
LOC subscriber can decode. This extension lets one track be both: its objects are LOC objects,
and a WAI subscriber reads each as the envelope it came from.

§1.2 It maps the capabilities of §3 and no others. A capability whose payload is not one chunk
of a registered WebCodecs codec travels as packaging `wai`.

§1.3 It changes no envelope and no receipt: the envelope a subscriber rebuilds is the one the
publisher wrote, byte for byte, and the receipt commits to it exactly as jwp-receipts §2.1 says.

## 2. LOC, as this extension reads it

§2.1 A LOC object's MOQ Object Properties hold the LOC Public Properties, and its payload holds
the LOC Private Properties followed by the LOC Payload, the "internal data" of a WebCodecs
`EncodedAudioChunk` or `EncodedVideoChunk` (LOC §2, §2.2). LOC defines Private Properties only
with Secure Objects (LOC §3.1.3): an object this extension writes carries none, and a sealed
object ([confidentiality](confidentiality.md) §4) is opened first and then read as here.

§2.2 The LOC properties (LOC §2.3, §6.1), each at most once on an object:

| type | name | value | rule |
|---|---|---|---|
| `0x10` | Timestamp | varint | |
| `0x08` | Timescale | varint | not 0 |
| `0x09` | Video Frame Marking | bytes | 1 to 4 bytes (RFC 9626) |
| `0x0C` | Audio Level | varint | at most 255 (RFC 6464 §3) |
| `0x0D` | Video Config | bytes | not empty |
| `0x0F` | Audio Config | bytes | not empty |

§2.3 MSF §4.1 places one chunk in each object and a group of pictures in one group; MSF §5.2
names the track's `codec` by the WebCodecs codec string.

## 3. Mapped capabilities

§3.1 The capabilities this extension maps, each with the WebCodecs codec whose chunk its payload
is:

| capability | WebCodecs codec | the payload, one chunk |
|---|---|---|
| `wai.audio.opus.packet` | `opus`, format `opus` | one Opus packet (RFC 6716 §3) |
| `wai.video.avc` | `avc1` or `avc3`, Annex B (no `description`) | one access unit as an Annex B byte stream |
| `wai.video.hevc` | `hev1` or `hvc1`, Annex B (no `description`) | one access unit as an Annex B byte stream, of the base layer only |
| `wai.audio.aac` | `mp4a.40`, format `adts` | one ADTS frame |

Not mapped: `wai.video.vp8` and `wai.video.vp9` (their payload is an IVF stream, a chunk is one
frame), `wai.video.av1` (its payload carries a framing header), `wai.audio.flac` (a FLAC stream
with its `STREAMINFO`, where a chunk is one frame), `wai.video.vvc` (the WebCodecs registry does
not register VVC), `wai.video.mvhevc` (a WebCodecs HEVC access unit holds exactly one base-layer
picture), and every capability with no WebCodecs codec.

§3.2 **One chunk.** A writer and a reader hold the payload to the form of its capability, and
refuse another with `loc_payload_form`:

- `wai.audio.opus.packet`: at least one byte;
- `wai.video.avc`: an Annex B byte stream (after at least two zero bytes, the prefix
  `0x000001`) whose NAL units each have a `forbidden_zero_bit` of 0, of which exactly one is a
  VCL NAL unit (types 1 to 5) whose slice header begins with a `first_mb_in_slice` of 0;
- `wai.video.hevc`: an Annex B byte stream whose NAL units each have a `forbidden_zero_bit` of 0
  and a `nuh_layer_id` of 0, of which exactly one is a VCL NAL unit (types 0 to 31) whose
  `first_slice_segment_in_pic_flag` is 1;
- `wai.audio.aac`: at least 7 bytes, the 12-bit sync word, layer 0, and a `frame_length` equal to
  the payload's length.

## 4. Writing an object

§4.1 A publisher writes an envelope (SPEC §2) of a mapped capability as one object:

- its **payload** is the envelope's payload, with nothing before or after it;
- its **Immutable Properties** (MOQT §10.7) hold `WAI_MANIFEST`, the envelope's manifest bytes
  exactly as the envelope holds them; the LOC properties the publisher states (LOC §3.1.2 asks
  for them to be immutable); and, where the publisher states a receipt, `WAI_RECEIPT`, its value
  laid out as moq-streaming-format §5 lays it out, whose `content_hash` is the envelope's
  (jwp-receipts §2.1).

`WAI_MANIFEST` has type `0x3A61`, from the range MOQT §8.4 leaves to an application's own use
(odd, so its value is bytes). Like `WAI_RECEIPT`'s `0x3A41`, a subscriber reads it only on a
track that §4.2 declares. A manifest longer than 65 535 bytes, the largest value MOQT §8.3
allows, is not written (`loc_manifest_invalid`), and neither is an envelope that is not a `WAI1`
envelope.

§4.2 The track's catalog entry declares `packaging` `"loc"` and `codec` (MSF §5.2), and the
fields of moq-streaming-format §4: `wai_capabilities`, holding exactly the mapped capability,
and `wai_receipts` where its objects carry `WAI_RECEIPT`. A subscriber that reads WAI recognises
such a track by `wai_capabilities` on a `loc` track.

§4.3 The LOC Timestamp and Timescale are the publisher's, and nothing in the envelope fixes
them; the receipt commits to the envelope, so it does not cover them. A subscriber that needs a
signed time for an object reads it from a claim (jwp-receipts §2.4.3).

## 5. Reading an object

§5.1 A subscriber reading an object of such a track, whose catalog names the capability `C`,
checks, in this order:

1. at most one Immutable Properties, which parses and holds none of its own:
   `loc_property_invalid`;
2. each LOC property of §2.2 at most once, across both property lists, with its rule:
   `loc_property_invalid`;
3. `WAI_MANIFEST` and `WAI_RECEIPT` only inside the Immutable Properties, each at most once:
   `loc_property_invalid`; no `WAI_MANIFEST`: `loc_manifest_absent`;
4. the manifest is a JSON object whose `wai` is of major version 1, whose
   `model_requirement.capability` is a string, and whose `security.encryption` is absent,
   `null` or `"none"`: `loc_manifest_invalid`; that capability is mapped (§3.1):
   `loc_capability_unmapped`; and it is `C`: `loc_capability_mismatch`;
5. the payload is one chunk (§3.2): `loc_payload_form`;
6. the envelope is `WAI1`, the manifest's length as a `u32` little-endian value, the manifest,
   the payload's length the same way, and the payload (SPEC §2);
7. where `WAI_RECEIPT` is present: its value reads and holds to moq-streaming-format §5's rules
   on the property alone (`property_invalid`), and its `content_hash` is the envelope's
   (jwp-receipts §2.1): `loc_receipt_mismatch`.

§5.2 The envelope a reader rebuilds is then read as any envelope is (SPEC §2 to §4), and its
receipt verified as jwp-receipts §3 says, against the group receipt the property names. This
extension checks only what §5.1 lists.

## 6. Refusals

| code | meaning |
|---|---|
| `loc_property_invalid` | the properties do not parse, a LOC property breaks its rule or is given twice, or `WAI_MANIFEST` or `WAI_RECEIPT` is outside the Immutable Properties or given twice |
| `loc_manifest_absent` | no `WAI_MANIFEST` |
| `loc_manifest_invalid` | the manifest is not a plaintext `WAI1` manifest of major version 1 naming a capability, or (writing) the envelope is not a `WAI1` envelope or its manifest is longer than 65 535 bytes |
| `loc_capability_unmapped` | the manifest's capability is not one of §3.1 |
| `loc_capability_mismatch` | the manifest's capability is not the track's |
| `loc_payload_form` | the payload is not one chunk of its codec (§3.2) |
| `loc_receipt_mismatch` | `WAI_RECEIPT`'s `content_hash` is not the rebuilt envelope's |
| `property_invalid` | `WAI_RECEIPT` itself is refused (moq-streaming-format §5) |

## 7. What this extension does not do

§7.1 It does not change LOC's payload, define a LOC property beyond `WAI_MANIFEST`, or restate
MOQT's encoding of properties. It does not map a `WAI2` envelope: a multi-rendition envelope is
several payloads, and each rendition a publisher sends over LOC is its own track.

## 8. Conformance

§8.1 [`live-conformance/loc/vectors.json`](../live-conformance/loc/vectors.json) holds objects of
every mapped capability, each with the envelope it carries, its MOQ Object Properties as MOQT
-22 encodes them, its payload, its LOC properties and its receipt property; envelopes the writer
refuses; and objects the reader refuses, each with its code, every code of §6 given. Its HEVC
objects carry access units of `format-probe-conformance/ext/hevc/base_64x64.265`; its Opus
object a 20 ms packet of silence; its AVC and AAC objects fixed bytes in the form of §3.2, which
are not decodable.

§8.2 `joule-moq`'s `tests/loc_vectors.rs` regenerates the corpus byte for byte and requires
every outcome; `loc_verify.py`, written from this text and sharing no code with `joule-moq`,
decodes every object's properties, rebuilds every envelope, computes every content hash with
BLAKE3 itself, requires every outcome, and requires every mutated copy of the corpus to fail.
