| Internet-Draft | LOCMAF | July 2026 |
| Einarsson | Expires 6 January 2027 | [Page] |
This document specifies LOCMAF (Low Overhead CMAF for Media over
QUIC), a compact packaging for low-latency CMAF media carried
end-to-end as MoQ Transport (MOQT) Object payloads, with per-object
overhead comparable to the Low Overhead Container (LOC). LOCMAF
carries the CMAF chunk head metadata from a single moof (movie
fragment) as a small set of tagged fields, while leaving the sample
data (mdat) untouched. Boxes that may surround the moof in a
CMAF chunk — styp (segment type), prft (producer reference
time), and any number of emsg (event message) boxes — are carried
verbatim, each through a generic box element (a genBox). The first Object of
each MOQT group carries a full reference; subsequent Objects in the
same group carry only the differences. The receiver reconstructs
CMAF chunks that are decode-equivalent to the sender input,
including the encryption metadata required by CMAF DRM (Common
Encryption) pipelines, and a canonical byte-identical
reconstruction — independent of the encoder's representation
choices — is defined for conformance testing.¶
This note is to be removed before publishing as an RFC.¶
The latest revision of this draft can be found at https://Eyevinn.github.io/locmaf-id/draft-einarsson-moq-locmaf.html. Status information for this document may be found at https://datatracker.ietf.org/doc/draft-einarsson-moq-locmaf/.¶
Discussion of this document takes place on the Media Over QUIC Working Group mailing list (mailto:moq@ietf.org), which is archived at https://mailarchive.ietf.org/arch/browse/moq/. Subscribe at https://www.ietf.org/mailman/listinfo/moq/.¶
Source for this draft and an issue tracker can be found at https://github.com/Eyevinn/locmaf-id.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 6 January 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
CMAF [CMAF] chunk headers have a size starting at around 100 bytes,
which can be as large as, or larger than, the coded frame the chunk
describes — at low latency and low bitrate, such as audio, a frame is
often only a few hundred bytes. Carrying CMAF directly over MoQ
Transport [MOQT] therefore incurs a per-object overhead that the
Low Overhead Container (LOC) [LOC] avoids by carrying raw codec
frames with a minimal set of metadata. LOC, however, cannot transport
the per-sample CENC [CENC] metadata needed for browser EME / CDM
decryption of DRM-protected live streams, nor the prft (Producer
Reference Time), emsg (DASH Event Message), and uuid boxes that a
CMAF chunk may carry alongside the moof.¶
LOCMAF closes this gap. It is a packaging — a compact container for
CMAF media — that exploits the observation that consecutive CMAF
chunk heads within a single CMAF segment are nearly identical: the
first chunk of a MOQT group is sent in full, subsequent chunks are
sent as compact deltas against the previous chunk in the same group,
and mdat payloads are passed through unchanged. The receiver
reconstructs full CMAF chunks suitable for any unmodified CMAF
playback pipeline (such as browser MSE / EME), or feeds the
elementary samples directly to a frame-based decoder interface
(such as WebCodecs); see Section 16.¶
LOCMAF is carried end-to-end as the payload of MOQT Objects. MOQT relays forward the Object payload unchanged; only the encoder produces LOCMAF and only the receiver expands it. LOCMAF is therefore a media packaging, not a hop-by-hop transfer encoding, and not a transport — MOQT [MOQT] is the transport.¶
This document specifies the LOCMAF Object encoding, the generic box
and raw-boxes elements, the full and delta chunk encodings, the
catalog signaling (aligned with [CMSF]), the canonical CMAF
reconstruction, and the DRM box round-trip. Auxiliary top-level boxes
such as uuid ride alongside the moof (Section 8), and content
outside the field model can be carried as whole boxes verbatim
(Section 9). A reference implementation, conformance test vectors,
golden files, and worked examples are published at [LOCMAF-REPO].¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
Throughout this document, vi64 denotes a variable-length integer
as defined in Section 1.4.1 of [MOQT].¶
The following terms are used throughout this document:¶
One moof + one mdat pair, optionally preceded by at most one
styp, at most one prft, zero or more emsg boxes, and other
top-level boxes (e.g. uuid), as defined in [CMAF] §7.3.3.2. The
smallest CMAF addressable media object.¶
One or more CMAF chunks whose first chunk starts at a Stream Access
Point ([CMAF] §7.3.2.2). A fragment is logically a single
MovieFragmentBox worth of samples; in "chunked" CMAF the samples
are split across multiple smaller moof + mdat pairs.¶
One or more CMAF fragments in decode order ([CMAF] §7.3.2.4). The segment is the typical unit of HTTP delivery in DASH and HLS-fMP4; in LOCMAF the segment corresponds to one MOQT group.¶
The ftyp + moov pair that initializes a CMAF track. Also called
an initialization segment in DASH parlance and carried or
referenced as initialization data by CMSF [CMSF] catalogs.¶
A MOQT Object whose payload is a LOCMAF Object encoding: either a
sequence of generic box elements (see below) and exactly one
moof-header element, followed by the mdat payload, or a single
raw-boxes element, as defined in Section 7.¶
A generic box element that carries one ISO BMFF box that precedes
moof in a CMAF chunk (for example styp, prft, emsg, or
uuid). See Section 8.¶
A raw-boxes element that carries one or more complete ISO BMFF boxes verbatim, including their box headers, as the entire payload of a LOCMAF Object. See Section 9.¶
The single moof-header element of a LOCMAF Object. It carries the
tagged moof fields in either a full (absolute) or delta encoding
and marks the position of moof in the reconstructed chunk. See
Section 7.¶
A LOCMAF Object whose moof-header element is a full header. It carries an absolute encoding of the CMAF chunk head and serves as the in-group reference for subsequent delta Objects. See Section 11.¶
A LOCMAF Object whose moof-header element is a delta header. It encodes differences against the most recently received full LOCMAF chunk in the same MOQT group. See Section 12.¶
LOCMAF assumes the following mapping from CMAF to MOQT:¶
One MOQT group per CMAF segment. Group boundaries align with random access points.¶
One MOQT Object per CMAF chunk. Each MOQT Object is a LOCMAF Object
carrying the (full or delta) chunk head followed by the unmodified
mdat payload.¶
Audio MOQT groups typically have the same duration as the video MOQT groups with which they will be muxed, to enable joint tune-in.¶
Sparse tracks, such as subtitle, event, or metadata tracks, are more likely to have groups that are not aligned with video.¶
Delta chunks reference the preceding chunk in the same group (see Section 12), so LOCMAF depends on in-order delivery within a group. MOQT [MOQT] guarantees ordering only within a subgroup: a subgroup carries Objects of one group in ascending Object ID order on a single stream, while datagrams are unordered. A publisher MUST therefore send all Objects of a LOCMAF group in a single subgroup, and MUST NOT use the Datagram forwarding preference for LOCMAF tracks. The first moof-carrying Object of each group MUST be a full LOCMAF chunk so a subscriber tuning in at a group boundary has a complete reference (see Section 7.1).¶
When a receiver detects that Objects are missing within a group — a gap in Object IDs, or a reset of the subgroup's stream — it MUST NOT apply subsequent delta chunks; it resumes either at the next full LOCMAF chunk or rawBoxes Object (Section 9) in the same group or at the start of the next group.¶
LOCMAF targets the low-latency CMAF case: short CMAF fragments composed of small CMAF chunks (often one sample per chunk), optionally carrying CENC encryption metadata. To keep the packaging minimal, the following constraints apply.¶
A LOCMAF publisher MUST ensure that:¶
Single trak per moov. The CMAF Header contains exactly
one trak box. Multi-track ISO BMFF files MUST be demuxed
before LOCMAF encoding.¶
No key ID (KID) change within a CMAF chunk. Key-identifier
transitions MUST align with fragment (and therefore chunk)
boundaries. This removes the need for sgpd / sbgp boxes in
the packaging.¶
If a source violates either of these, the publisher MUST NOT use LOCMAF packaging for that track and MUST instead use plain CMAF packaging. LOCMAF and plain CMAF tracks MAY coexist in the same catalog under the same namespace (see Section 5).¶
The following are recommendations whose violation costs bytes but does not break LOCMAF:¶
A LOCMAF encoder MAY operate in strict cmf2 mode, in which it
always emits the four tfhd defaults (sample duration, size,
flags, sample-description index) in the full chunk header even when
they match trex. This costs a handful of bytes per group but
produces reconstructed CMAF chunks in which each chunk is a
self-decodable single-chunk fragment per [CMAF] §7.7.3. It need
not be signaled, since it does not affect compatibility between
encoders and decoders. Strict cmf2 mode is an opt-in encoder
tweak and is not the canonical baseline used for conformance;
the canonical reconstruction defined in Section 15 emits a
tfhd default only when it differs from trex.¶
LOCMAF media is signaled in a CMSF [CMSF] catalog. This document
extends the allowed packaging values defined in [MSF] with one
new entry, in the same manner that [CMSF] adds "cmaf":¶
| Name | Value | Reference |
|---|---|---|
| LOCMAF |
locmaf
|
This document |
Every track entry in a CMSF catalog that carries LOCMAF-encoded
media MUST declare a packaging value of "locmaf". As with
"cmaf", the "locmaf" packaging is defined for CMSF catalogs
only, not for plain MSF [MSF] catalogs. The "locmaf" packaging
value and the locmafVersion field below are expected to migrate
into [CMSF] once it registers them; until then they are defined
here.¶
The LOCMAF Object encoding is versioned independently of the CMSF
catalog version. This document adds one track-level catalog
field, following the field-definition conventions of [MSF]:¶
| Field | Name | Location | Required | JSON Type |
|---|---|---|---|---|
| LOCMAF version |
locmafVersion
|
T | Conditional | String |
locmafVersion identifies the LOCMAF packaging version of the
track. It MUST be present when packaging is "locmaf" and MUST NOT be present otherwise. The version specified by this document
is "0.3". A receiver MUST NOT subscribe to a LOCMAF track whose
locmafVersion it does not support; when the catalog offers the
same source under an alternative packaging, it MAY select that
instead.¶
The unknown-field rule of Section 7.3 covers additive evolution
within a version; locmafVersion signals behavioral changes that
reinterpret existing wire syntax, which a receiver cannot detect
from the wire bytes alone.¶
The CMAF Header for a locmaf track is carried by the same
mechanism [CMSF] uses for a cmaf track: an initDataList entry
referenced from the track entry by initRef (see Section 6).
LOCMAF places no additional restriction on the initDataList entry
type and has the same flexibility as cmaf packaging: the inline
base64 type is the common case, but any initDataList type defined
by [MSF] or [CMSF] MAY be used. LOCMAF defines no init carriage
of its own. A consequence is that a cmaf track and a locmaf
track wrapping the same source MAY refer to the same initData
entry; only the per-chunk Object encoding differs.¶
DRM is signaled exactly as for a cmaf track, by the CMSF
[CMSF] root-level contentProtections array referenced from the
track entry by contentProtectionRefIDs, following the DASH-IF
content-protection model [DASHIF-ECCP]. LOCMAF does not use the
MSF moq-secure-objects mechanism; the encryption metadata travels
inside the reconstructed CMAF boxes (see Section 13). The precise
catalog field names are those defined by [MSF] and [CMSF];
beyond the packaging value "locmaf" and the locmafVersion
field, LOCMAF introduces no catalog fields.¶
cmaf and locmaf tracks MAY be mixed in the same catalog under the
same namespace — for example video using cmaf packaging while audio
uses locmaf — and MAY coexist as alternative encodings of the same
source.¶
The CMAF Header for a LOCMAF track is byte-identical to the CMAF
Header a plain cmaf track of the same source would carry — ftyp
followed by moov (which contains mvex with trex, and, for
protected content, optionally pssh). It is carried or referenced
via the same CMSF [CMSF] mechanism as for cmaf packaging (an
initDataList entry referenced by initRef), using whichever
initDataList type the catalog specifies; the inline base64 type
is the common case. There is no LOCMAF-specific CMAF Header carrier.¶
The moov in the CMAF Header MUST contain exactly one trak box
(see Section 4).¶
A LOCMAF receiver:¶
Resolves the track's initRef to its initDataList entry and
obtains the CMAF Header bytes as the entry's type dictates
(base64-decoding an inline entry).¶
Feeds the bytes to its MSE / decoder pipeline exactly as it would for a plain CMAF track.¶
Extracts the parameters required to reconstruct CMAF chunks — the
single trak's track_ID, mdhd.timescale, trex defaults, and
any track-encryption information (tenc defaults: default KID,
default per-sample IV size, constant IV, scheme type, and pattern
parameters) — from the decoded CMAF Header. These values seed the
per-track reconstruction state (see Section 15).¶
Begins receiving LOCMAF-encoded media Objects on the subscribed track and reconstructs each CMAF chunk from the LOCMAF Object payload.¶
Compression of the CMAF Header (init data), which can be sizeable,
is out of scope for LOCMAF: it is a property of the initDataList
entry type and handled by the CMSF [CMSF] / MSF [MSF] init-data
mechanism, not defined here.¶
A LOCMAF Object is the payload of a single MOQT [MOQT] object. It
carries one CMAF chunk: the moof chunk head as a small set of
tagged fields, any boxes that precede the moof, and the
unmodified mdat sample data. MOQT relays forward the Object
payload unchanged; only the encoder produces LOCMAF and only the
receiver expands it back to CMAF.¶
A LOCMAF Object payload takes one of two shapes. The first — the
moof-carrying shape — is an ordered sequence of elements followed
by the raw mdat payload:¶
[ genBox ]* zero or more pre-moof boxes, in reconstruction order locmafHeader exactly one: a full or delta moof header mdat raw payload length = MOQT-object-len minus the elements above¶
The second shape is a single raw-boxes element spanning the entire payload:¶
rawBoxes complete ISO BMFF boxes, verbatim ({{rawboxes}})
¶
The raw-boxes shape is not the common per-chunk path. It serves
in-group resync (Section 3), verbatim carriage of chunks whose
moof falls outside the LOCMAF field model, and self-framed storage
outside MOQT (Section 17); see Section 9.¶
In the moof-carrying shape, the locmafHeader marks where the
moof sits in the reconstructed chunk: every genBox before it
renders before the moof, and the mdat immediately follows the
moof. The common case — no boxes outside the moof — is simply
locmafHeader followed by mdat; genBox is purely additive.¶
Each element begins with an element_type vi64. Four element
types are defined:¶
| element_type | Symbol | Meaning | Delimited by |
|---|---|---|---|
| 1 |
genBox
|
one generic pre-moof box (Section 8) |
its own box_size field |
| 2 |
locmafFullHeader
|
full moof header (absolute encoding) | its properties_length
|
| 3 |
locmafDeltaHeader
|
delta moof header (in-group deltas) | its properties_length
|
| 4 |
rawBoxes
|
complete boxes, verbatim (Section 9) | the Object end (sole element) |
The mdat payload carries no element_type tag — it is whatever
bytes remain after the locmafHeader.¶
A receiver parses elements in a loop, reading element_type vi64
values:¶
If the first element_type of the Object is 4, the Object is a
single rawBoxes element (Section 9) and the loop ends;
element_type 4 anywhere but first is malformed.¶
While the element_type is 1, it parses one genBox (delimited
by its box_size, see Section 8) and continues.¶
When the element_type is 2 or 3, it parses exactly one
locmafHeader, full or delta respectively (delimited by its
properties_length, see Section 11 and Section 12).
The bytes following that header's property block, to the end of
the MOQT object, are the mdat payload.¶
Exactly one header element MUST appear in a moof-carrying LOCMAF
Object, and it MUST be the last element before the mdat payload.
A genBox that follows the header is malformed.¶
The full-vs-delta distinction is signaled exclusively by the
header element_type (2 for full, 3 for delta), never by the
MOQT object position within a group:¶
The first moof-carrying object of every MOQT group MUST carry a
locmafFullHeader, so a subscriber tuning in at a group
boundary has a complete reference. The only objects that may
precede it in the group are rawBoxes Objects (Section 9).¶
The encoder MAY emit a locmafFullHeader at any object position
within a group, not only at object index 0. A mid-group full
chunk re-anchors the in-group reference for subsequent delta
chunks.¶
After parsing a locmafFullHeader, the receiver MUST discard
its in-group delta state and treat the new full chunk as the
reference for any following locmafDeltaHeader objects in the
group.¶
A rawBoxes Object likewise resets the in-group delta state; the
next moof-carrying object in the group MUST carry a
locmafFullHeader (see Section 9).¶
The receiver MUST dispatch on the header element_type alone. It MUST NOT infer "full" from object index 0 or "delta" from object index greater than 0.¶
An element_type not defined in the table above is not
self-delimiting, so a receiver cannot skip it. A receiver that
reads an unrecognized
leading element_type MUST treat the Object as malformed and reject
it; there is no generic skip for unknown top-level elements. This
hard failure is deliberate: an element one receiver silently
skipped and another understood would make the two reconstruct
different chunks, defeating canonical comparison (Section 15).
Extension happens through new genBox box_name FourCCs and new
header field IDs (see Section 7.3) — both of which are
self-delimiting — not through new element types.¶
For event-only tracks (see Section 14) the mdat payload MAY
be zero bytes; the receiver reconstructs an empty mdat box
(8-byte header only).¶
A locmafHeader element (full or delta) is, in order:¶
element_type vi64 = 2 (full) or 3 (delta) properties_length vi64 byte length of the property block property block properties_length bytes of (field_id, value) tuples¶
element_type and properties_length are vi64 values. The
property block is the flat sequence of
(field_id, value) tuples defined in Section 7.3 and is exactly
properties_length bytes long, so the header element is
self-delimited. When this is the Object's header element, the bytes
following the property block, to the end of the MOQT object, are the
mdat payload (see Section 7.1). A delta header with
properties_length == 0 carries no field changes (see
Section 12).¶
The property block inside a locmafHeader is a flat sequence of
(field_id, value) tuples. Field IDs are vi64 values.
This is the property scheme of [LOC] §2.3: the value encoding is
determined by the parity of the field ID. LOCMAF reuses that parity
scheme but governs its own field IDs in this document (see
Section 10); they are not [LOC] properties and are not
IANA-registered.¶
Even ID — scalar. The value is a single vi64 with no
length prefix. In a full header it is an absolute unsigned
vi64; in a delta header it is a zigzag vi64 (see Section 7.4)
of the signed delta against the in-group reference.¶
Odd ID — length-prefixed bytes. The tuple is field_id |
byte_length | bytes. The interpretation of the bytes is
per-field. vi64-list fields concatenate their elements, each an
absolute unsigned vi64 in full context and a per-element
zigzag vi64 (see Section 7.4) in delta context. Raw-byte fields
(sencInitializationVector, ID 9) carry opaque content verbatim
in both contexts.¶
Parity governs framing: it tells a receiver how far every tuple extends, whether or not it recognizes the field ID. The interpretation of the value bytes is per-field (Section 10). Three fields deviate from the absolute-in-full / delta-in-delta value encoding above:¶
trunSampleCompositionTimeOffsets (ID 5): elements are zigzag
vi64 values (see Section 7.4) in both full and delta context,
because composition time offsets are signed in trun version 1 —
the common video case, where B-frames make the composition/
decode-time relation non-monotonic.¶
sencInitializationVector (ID 9): opaque raw bytes, carried
verbatim in both contexts (overwrite, never a delta).¶
deltaDeletedLocmafIDs (ID 27): delta-only control list whose
elements are plain unsigned vi64 field IDs, never zigzag (see
Section 10.4).¶
Two fields are additionally restricted to one header kind:
tfdtBaseMediaDecodeTime (ID 10) appears only in full headers — a
delta chunk's BMDT is always derived (see Section 12.2) —
and deltaDeletedLocmafIDs (ID 27) appears only in delta headers.¶
A receiver that encounters a field ID not defined in this document
MUST skip its value using the parity rule — one vi64 for an even
ID, byte_length bytes for an odd ID — and MUST otherwise ignore
it. New field IDs can therefore be added backward-compatibly, as
long as a reconstruction that ignores them remains correct; an
extension that cannot be safely ignored requires a new packaging
value instead.¶
The ID space is structurally aligned with the parity rule: every scalar/default field has an even ID and every list or byte field has an odd ID. A field ID MUST NOT appear twice in one property block; a receiver MUST reject a property block that repeats one. Field IDs MAY appear in any order and receivers MUST tolerate any ordering; encoders SHOULD emit IDs in ascending order, and the canonical encoding (Section 15.9) requires it.¶
A zigzag vi64 is a signed integer encoded as an unsigned vi64
by interleaving non-negative and negative values so that
small-magnitude values of either sign occupy small unsigned values,
and thus the shortest vi64 forms.¶
For a signed 64-bit integer n, the mapping to its unsigned zigzag
representation z is:¶
encode: z = (n << 1) ^ (n >> 63) ; arithmetic right shift
; equivalently:
; n >= 0: z = 2 * n
; n < 0: z = -2 * n - 1
decode: n = (z >> 1) ^ -(z & 1) ; equivalently:
; z even: n = z / 2
; z odd: n = -(z + 1) / 2
¶
The first few mappings: 0↔0, -1↔1, 1↔2, -2↔3, 2↔4, -3↔5, 3↔6, ….¶
The encoded z is then serialized as an unsigned vi64; decoders
read the vi64 and apply the decode rule above.¶
This zigzag mapping is widely used in compact binary serialization formats; the description is included here for self-containment.¶
LOCMAF uses zigzag vi64 values wherever a signed value is written: for
deltas against the in-group reference, and for the signed list
trunSampleCompositionTimeOffsets (ID 5) in both contexts (see
Section 7.3).¶
A genBox is the single generic carrier for every box that sits
outside the moof in a CMAF chunk — styp, prft, emsg,
uuid, and any other ISO box type. One genBox element carries
exactly one ISO box.¶
Boxes inside moof.traf are not genBoxes. In particular the
CENC boxes senc, saiz, and saio live inside traf, so their
data is carried as moof-header fields (senc) or recomputed on
reconstruction (saiz, saio); see Section 10 and Section 13.
Only boxes outside the moof are genBoxes.¶
A genBox element is, in order:¶
element_type vi64 = 1 (genBox)
box_size vi64 length in bytes of box_name + payload
box_name 4 bytes the ISO box type FourCC
('styp','emsg','prft','uuid', ...)
payload box_size − 4 bytes the box contents WITHOUT the
8-byte ISO box header
¶
element_type and box_size are vi64 values. box_size covers
the box_name and the payload — the
entire remainder of the element — mirroring ISOBMFF, where a box's
size covers its type and contents (the ISO size additionally
counts its own 4 bytes). The element on the wire is therefore
1 | box_size | box_name(4) | payload(box_size − 4) and is fully
self-delimited by box_size; a box_size less than 4 is
malformed. Every element that other elements may follow — genBox
and the two headers — thus shares the same type | length | body
shape, so the length field alone delimits it; only rawBoxes,
which nothing ever follows, carries no length (Section 9).¶
The payload is the box contents that would follow the 8-byte ISO
box header (size + type). For a uuid box, the 16-byte
usertype is part of the box contents: the encoder MUST place the
usertype as the first 16 bytes of payload, followed by the
remaining box data.¶
genBoxes always carry full bytes; there is no cross-chunk
genBox delta and no genBox deletion marker. Presence in the
Object payload means the box is rendered in this chunk, in the
position implied by element order; absence means the box is not
rendered in this chunk. A delta locmafHeader MAY still be
combined with full genBoxes in the same Object — for example a
per-chunk prft genBox in front of a delta moof header.¶
To reconstruct the ISO box from a genBox, the receiver wraps
payload in a standard ISO box header:¶
Let L = 4 + box_size. L MUST fit in 32 bits: a box_size
above 0xFFFFFFFB is malformed and the receiver MUST reject
the Object.¶
Emit the box header uint32be(L) | box_name(4), then
payload. The total box is L bytes.¶
For a uuid box (box_name == 'uuid'), the 16-byte usertype
is the first 16 bytes of payload (see above) and is therefore
emitted immediately after the box header as part of payload.
The receiver performs no reordering; reconstruction is the
standard byte-for-byte wrap.¶
The ISO size escape values 0 (box extends to end of file) and 1
(64-bit largesize follows, [ISOBMFF]) are never produced: a
reconstructed genBox always carries its actual size in the 32-bit
size field. An encoder MUST NOT emit a genBox that would
require either escape.¶
Every genBox in the Object payload renders, in payload order,
before the moof; the mdat immediately follows the moof. The
reconstructed chunk is genBox* (in payload order), then moof,
then mdat.¶
The payload of every genBox is simply the box contents as
defined by [ISOBMFF] (or the specification owning the box type),
carried verbatim — LOCMAF re-encodes no fields.¶
The set of box types carried as genBoxes is open: any box outside
the moof is carried under its ISO box_name FourCC, which is
self-describing and needs no LOCMAF identifier allocation.¶
A rawBoxes element carries one or more complete ISO BMFF boxes
verbatim — box headers included — as the entire payload of a LOCMAF
Object. Where a genBox carries one pre-moof box alongside a
locmafHeader, a rawBoxes element replaces the header and mdat
entirely: it is the escape from the moof-header model for content
that LOCMAF does not otherwise carry. Two uses motivate it:¶
In-band CMAF Header. In self-framed carriage
(Section 17), a leading rawBoxes Object holds the ftyp +
moov bytes, so a stored LOCMAF segment is self-contained and
the initialization bytes round-trip exactly.¶
Verbatim chunk carriage. A chunk whose moof uses structures
outside the LOCMAF field model rides verbatim, at plain-CMAF
cost, without forcing the whole track onto plain CMAF packaging.¶
A rawBoxes element is, in order:¶
element_type vi64 = 4 (rawBoxes) boxes all remaining bytes complete ISO BMFF boxes, verbatim¶
element_type is a vi64. boxes is the concatenation of one or
more complete ISO BMFF boxes [ISOBMFF], each starting with its
own box header, spanning the entire remainder of the Object; the
box sizes MUST sum to exactly that remainder. A rawBoxes element
carries no length field of its own: it is always the sole element
of its Object (see below), so the Object length — supplied by MOQT,
or by the object_length prefix in self-framed carriage
(Section 17) — delimits it, exactly as it delimits the
untagged mdat payload of a moof-carrying Object. The ISO size
escape values 0 (box extends to end of file) and 1 (64-bit
largesize follows) are not allowed: every box in boxes MUST
carry its actual size in the 32-bit size field. An empty boxes
is malformed.¶
A rawBoxes element MUST be the only element of its LOCMAF Object:
element_type 4 MUST appear first in the Object payload, and a
receiver MUST reject an Object in which it appears after another
element. A rawBoxes Object carries no genBox, no locmafHeader,
and no mdat payload. Restricting rawBoxes to whole Objects keeps
the representation unambiguous — a pre-moof box accompanying a
moof header has exactly one carrier, genBox, so canonical
comparison (Section 15) never reconciles two encodings of the
same chunk — and is what lets the element drop its length field.¶
A rawBoxes Object resets the in-group delta chain. On receiving
one, the receiver MUST discard its in-group reference state; the
next moof-header element in the same group MUST be a full header,
and a receiver MUST reject a locmafDeltaHeader that follows a
rawBoxes Object without an intervening locmafFullHeader. This
rule keeps receivers writer-only: deriving delta state from boxes
would require parsing a moof out of the raw bytes, which
reconstruction never otherwise needs.¶
The reconstructed bytes of a rawBoxes Object are boxes, verbatim.
This is also its canonical form (Section 15): no normalization
is applied, and canonical comparison is plain byte equality.¶
This section defines the (field_id, value) tuples carried inside a
full or delta locmafHeader element (element types 2 and 3, see
Section 7.1). These IDs occupy a namespace distinct from
the top-level element-type IDs; they MUST NOT be confused with them.¶
Framing and value encoding follow the parity rule of Section 7.3, including its four per-field exceptions. LOCMAF governs these IDs in this document; they are not LOC properties and are not registered with IANA (see Section 19).¶
The fields are drawn from the boxes inside moof.traf. Each symbol
prefix names its containing box, and the table groups them by box in
reconstruction order — tfhd, then tfdt, then trun, then
senc. The field IDs are therefore not consecutive: the parity rule
(Section 7.3) fixes every scalar to an even ID and every list to an
odd ID, and the lowest IDs are held for the common-media fields so
they encode in a single byte. The field IDs are identical across
full and delta headers; only the value encoding differs (absolute in
a full header, delta in a delta header — see Section 11 and
Section 12).¶
| ID | Symbol | Kind |
|---|---|---|
| 2 |
tfhdSampleDescriptionIndex
|
scalar |
| 4 |
tfhdDefaultSampleDuration
|
scalar |
| 6 |
tfhdDefaultSampleSize
|
scalar |
| 8 |
tfhdDefaultSampleFlags
|
scalar |
| 10 |
tfdtBaseMediaDecodeTime
|
scalar |
| 1 |
trunSampleSizes
|
list |
| 3 |
trunSampleDurations
|
list |
| 5 |
trunSampleCompositionTimeOffsets
|
signed list ‡ |
| 7 |
trunSampleFlags
|
list |
| 12 |
trunFirstSampleFlags
|
scalar |
| 14 |
trunSampleCount
|
scalar |
| 9 |
sencInitializationVector
|
raw bytes |
| 11 |
sencSubsampleCount
|
list |
| 13 |
sencBytesOfClearData
|
list |
| 15 |
sencBytesOfProtectedData
|
list |
| 16 |
sencPerSampleIVSize
|
scalar |
| 27 |
deltaDeletedLocmafIDs
|
list |
‡ Signed: elements are zigzag vi64 values (see Section 7.4) in
both full and delta context (see Section 7.3).¶
trunSampleFlags (ID 7), tfhdDefaultSampleFlags (ID 8), and
trunFirstSampleFlags (ID 12) each carry the complete 32-bit ISO
sample_flags value ([ISOBMFF] §8.8.3.1): the full value in a
full header, and a difference in a delta header, both as vi64
(the difference signed, see Section 7.4).¶
The per-sample list fields (IDs 1, 3, 5, 7, 11) carry the
sample[i].* values from their source box. The per-subsample list
fields (IDs 13, 15) carry the
senc.sample[i].subsample[j].* values flattened in chunk order,
with a total length equal to the sum of sencSubsampleCount[i]
over all samples. sencInitializationVector (ID 9) is the
concatenation of per-sample IVs, each per_sample_IV_size bytes
long (see Section 13).¶
trunSampleCount (ID 14) — always present in a full header, and
carried in a delta header only when it changes — anchors every list
length: the per-sample lists have trunSampleCount elements, with
the single exception of trunSampleSizes (ID 1), which carries
trunSampleCount − 1 entries because the last sample size is
derived from the mdat-payload length (see
Section 11.2). The receiver therefore knows the
length of every list field before parsing its bytes.¶
sencInitializationVector (9), sencSubsampleCount (11),
sencBytesOfClearData (13), sencBytesOfProtectedData (15), and
sencPerSampleIVSize (16) are drawn from the senc box inside
moof.traf. They are therefore locmafHeader fields, not
genBox. Only boxes outside moof are carried as generic boxes (see
Section 8). The saio and saiz boxes are not carried at all;
the receiver recomputes them during canonical reconstruction (see
Section 13 and Section 15).¶
The CENC fields apply only to protected tracks. A receiver MUST
reject a chunk that carries any of them (IDs 9, 11, 13, 15, 16) on
a track whose CMAF Header does not signal protection
(tenc.default_isProtected = 1): the reconstruction of
Section 15.8 is defined only for protected tracks, so
implementations could not otherwise agree on what such a chunk
reconstructs to.¶
deltaDeletedLocmafIDs (ID 27) is a control field used only in a
delta header. It carries the list of field IDs that were present in
the previous chunk of the same group but are absent in the current
one. Its elements are plain unsigned vi64 values — not
zigzag, and
not deltas against any prior deletion list. The list length is
determined by the field's byte-length prefix: the receiver reads
unsigned vi64 values until the prefix is exhausted. This field
MUST NOT
appear in a full header. Its application is specified in
Section 12.¶
A full locmafHeader (element type 2) carries an absolute encoding
of one CMAF chunk's moof. Any boxes that preceded the moof in
the source chunk (styp, prft, emsg, …) are carried as genBox
elements ahead of the header (see Section 8); the mdat payload
follows the header's property block unchanged.¶
In a full header, values are absolute, encoded per Section 7.3. The first moof-carrying Object of every MOQT group MUST carry a full header so a subscriber tuning in at a group boundary has a complete in-group reference (see Section 7.1).¶
The encoder first derives each sample's effective duration,
flags, and composition-time offset from the source moof — the
per-sample trun value when present, else the tfhd default when
present, else the trex default (offsets default to 0) — and emits
fields from those effective values. The encoding is therefore
independent of how the source moof happened to distribute values
between per-sample entries and defaults: two decode-equivalent
source moofs yield the same emitted fields.¶
trunSampleCount (14)always.¶
tfdtBaseMediaDecodeTime (10)always.¶
tfhdSampleDescriptionIndex (2)emit iff the effective sample-description index ≠ trex.default_sample_description_index.¶
trunSampleDurations (3)emit iff the effective sample durations are not all equal.¶
tfhdDefaultSampleDuration (4)emit iff the effective sample durations are all equal AND that value ≠ trex.default_sample_duration.¶
trunSampleSizes (1)emit iff sample sizes are not all equal AND sample_count > 1; the list carries sample_count − 1 values (the first n − 1 in chunk order).¶
tfhdDefaultSampleSize (6)emit iff all samples in the chunk share one size AND that size ≠ trex.default_sample_size AND sample_count > 1 (see Section 11.2).¶
trunSampleFlags (7)emit iff the effective sample flags are neither all equal, nor equal on all samples but the first.¶
trunFirstSampleFlags (12)emit iff sample_count > 1 AND the first sample's effective flags differ from the (equal) effective flags of all other samples.¶
tfhdDefaultSampleFlags (8)emit iff the effective flags shared by the samples it covers (all samples, or all but the first when trunFirstSampleFlags is emitted) ≠ trex.default_sample_flags; never emitted together with trunSampleFlags.¶
trunSampleCompositionTimeOffsets (5)emit iff any effective composition-time offset ≠ 0.¶
sencPerSampleIVSize (16)emit iff senc is present AND per_sample_IV_size ≠ tenc.default_Per_Sample_IV_Size.¶
sencInitializationVector (9)emit iff senc is present AND per_sample_IV_size > 0.¶
sencSubsampleCount (11), sencBytesOfClearData (13), sencBytesOfProtectedData (15)emit iff senc is present AND the samples carry subsample maps.¶
These rules produce the minimal encoding and are part of the
canonical encoding (Section 15.9). An encoder MAY
additionally emit a field whose value matches the applicable
default — strict cmf2 mode (Section 4) does exactly that for the
four tfhd defaults — without affecting the decoded effective
values or the canonical reconstruction (Section 15).¶
Let n = trunSampleCount and let P be the chunk's mdat-payload
length (the MOQT object length minus the bytes consumed by all
elements). The receiver MUST derive sample sizes as follows:¶
If trunSampleSizes (ID 1) is present, it carries exactly
n − 1 values. sample_size[i] = listed[i] for i in
[0, n−1), and sample_size[n−1] = P − sum(listed). The
receiver MUST NOT consult tfhdDefaultSampleSize,
trex.default_sample_size, or any other source for sample sizes
in this chunk.¶
Else if tfhdDefaultSampleSize (ID 6) is present, all n
samples have that size.¶
Else if n == 1, the lone sample's size is P. The encoder
omits all size fields for a single-sample chunk (see below), so
the payload length is authoritative and
trex.default_sample_size is never consulted at n == 1.¶
Else if trex.default_sample_size is non-zero, all n samples
have that size.¶
Else if P == 0, all n samples have size 0 (e.g. an event
track carrying several zero-size samples per chunk).¶
Else the chunk is malformed and the receiver MUST reject it.¶
Correspondingly, when sample_count == 1 both trunSampleSizes
and tfhdDefaultSampleSize MUST be omitted — the single sample's
size is always P. When sample_count > 1 with uniform sizes the
encoder MUST emit tfhdDefaultSampleSize (subject to the
trex.default_sample_size equality rule) and MUST NOT emit
trunSampleSizes; when sizes vary the encoder MUST emit
trunSampleSizes with exactly n − 1 entries and MUST NOT emit
tfhdDefaultSampleSize. The receiver MUST reject a chunk in which
sum(listed) > P, a chunk whose default-derived sizes do not
satisfy n × size = P, and a chunk with n == 0 whose mdat
payload is not empty. Omitting the last sample size shaves one vi64
per chunk; using the default for uniform-size tracks (common for
fixed-bitrate audio, e.g. AC-3) collapses n − 1 vi64 values into
one.¶
A delta locmafHeader (element type 3) carries only the
differences between the current chunk's moof and the most
recently received full chunk in the same MOQT group. Delta encoding
is additive: a field absent from the delta header is unchanged from
the previous chunk.¶
Each emitted field's value is interpreted relative to its kind:¶
| Kind | Wire encoding | Reconstruction |
|---|---|---|
| scalar (even ID) | zigzag vi64 (see Section 7.4) of current − previous
|
current = previous + delta
|
| list (odd ID) | zigzag vi64 per element, concatenated; element delta = current[i] − previous[i]
|
element-wise sum with the previous list |
| raw bytes (odd ID) | full new bytes, verbatim | overwrite the previous bytes |
The "previous value" for each field is its value in the in-group reference state: the represented fields stored from the previous LOCMAF chunk in the same group (or, after a mid-group full header, from that re-anchor) — not the expanded effective values. A field absent from that state has previous value 0, so a delta header MAY introduce a field the previous chunk did not carry; the same rule applies per element to lists that grow (see below).¶
The length of each per-sample list in the current chunk is the
chunk's effective trunSampleCount: the value established by the
in-group reference, changed only when a delta for ID 14 is present
(absence means unchanged, like any delta field).
This covers trunSampleDurations (3),
trunSampleCompositionTimeOffsets (5), trunSampleFlags (7), and
sencSubsampleCount (11). trunSampleSizes (1) carries
trunSampleCount − 1 entries, or none when trunSampleCount is 0
(see Section 11.2). The
per-subsample lists (13, 15) have a total length equal to the sum
of the new sencSubsampleCount[i]. Consequently the receiver knows
len(current) for every list before parsing its bytes.¶
When len(current) ≠ len(previous) the delta rule extends as
follows:¶
For indices i in [0, min(len(current), len(previous))): the
wire carries zigzag(current[i] − previous[i]) and the receiver
reconstructs current[i] = previous[i] + delta[i].¶
For indices i in [len(previous), len(current)) (current
longer): the wire carries zigzag(current[i]), the absolute
value, equivalent to treating the missing previous entry as 0.
The receiver reconstructs current[i] = delta[i].¶
For indices i in [len(current), len(previous)) (current
shorter): no bytes are emitted for these positions; the receiver
truncates to len(current).¶
tfdt BMDT derivation
The receiver derives a delta chunk's BMDT as previous_bmdt
plus the sum of the previous chunk's effective sample durations
(the per-sample values when present, else sample_count times the
applicable default). tfdtBaseMediaDecodeTime (ID 10) is a
full-header field: it MUST NOT appear in a delta header, and a
receiver MUST reject a delta header that carries it. The
derivation is safe because CMAF ([CMAF]) requires the decode
timeline to be contiguous: each fragment's baseMediaDecodeTime
equals the previous fragment's plus the sum of its sample
durations.¶
When the source BMDT diverges from this derivation (a splice, a capture gap, a stream re-anchor), the encoder MUST emit a full LOCMAF chunk (Section 11): a timeline discontinuity re-anchors the entire in-group reference, which is also what a recovering or late-joining receiver needs at exactly that point.¶
Because delta encoding is additive, a field that was present in the
previous chunk but is genuinely gone from the current chunk cannot
be signaled by mere absence — absence means "unchanged." The
deletion marker deltaDeletedLocmafIDs (ID 27, see
Section 10.4) provides that signal: it lists the field IDs
present in the previous chunk that no longer apply. The receiver
MUST apply deletions before applying deltas, removing each
listed field from the previous-chunk state so the current chunk
falls back to the appropriate default (trex-derived or absent).¶
The motivating case is trunFirstSampleFlags (ID 12). A SAP-1
random-access chunk emits this field to flag its first sample as a
sync sample; the immediately following non-sync chunk must say
"this override no longer applies" so the receiver falls back to
trex.default_sample_flags for the first sample. The second chunk
emits ID 27 with a one-element list containing the value 12. The
typical cost is two bytes — one length-prefixed list of one field
ID — versus the tens to hundreds of bytes of re-anchoring with a
full header. In a one-sample-per-chunk stream the same pattern
rides tfhdDefaultSampleFlags (ID 8) instead: the sync chunk
emits ID 8 and the next chunk deletes it, falling back to
trex.default_sample_flags.¶
An empty delta property block (properties_length == 0) is valid
and means "no field changed since the previous chunk." This is the
steady-state case for sample-level fragmented streams. The on-wire
object reduces to the delta header element plus the mdat payload.¶
LOCMAF preserves the per-sample Common Encryption (CENC) [CENC]
metadata that EME-based decryption pipelines require. The track's
key identifier, scheme, pattern parameters, and other static
encryption defaults are carried in the CMAF Header's
tenc box (inside schi inside sinf) and signaled in the
catalog through CMSF contentProtections and
contentProtectionRefIDs (see Section 5). The per-sample
material that varies chunk to chunk is carried as locmafHeader
fields.¶
LOCMAF is scheme-agnostic. The packaging carries the per-sample
senc metadata — initialization vectors and subsample maps — for
any CENC [CENC] protection scheme; the scheme itself is
identified by tenc.default_isProtected = 1 and the four-character
scheme_type of the surrounding schm box, both carried in the
CMAF Header, and the reconstruction of Section 15.8 does not
depend on it. Schemes with per-sample initialization vectors
(cenc, cbc1, cens) carry them explicitly in
sencInitializationVector (ID 9) on every chunk; LOCMAF defines
no IV derivation or prediction rule.¶
One scheme has scheme-specific behavior. Under cbcs (AES-128-CBC
subsample pattern encryption), the constant initialization vector
(tenc.default_constant_IV) and the pattern
(default_crypt_byte_block / default_skip_byte_block) travel in
the CMAF Header ([CENC] §10.4). No per-sample IV appears in
senc: per_sample_IV_size is 0, sencInitializationVector
(ID 9) is empty and sencPerSampleIVSize (ID 16) is 0 or omitted;
the reconstructed senc still sets flags = 0x000002 and carries
the subsample map.¶
In practice, CMAF presentation profiles ([CMAF] Annex A) and the
deployed DRM ecosystem [DASHIF-ECCP] use cenc and cbcs.¶
| Box | Where in CMAF | LOCMAF treatment |
|---|---|---|
senc
|
inside traf
|
per-sample IVs and subsample maps carried via locmafHeader field IDs 9, 11, 13, 15, 16. |
saio
|
inside traf
|
not carried; recomputed by the receiver to point at the reconstructed senc (see Section 15). |
saiz
|
inside traf
|
not carried; recomputed from the per-sample IV size and subsample counts (see Section 15). |
tenc
|
inside schi in moov
|
carried verbatim in the CMAF Header. |
The following DRM boxes are not supported. Sources that require them MUST use plain CMAF packaging:¶
| Box | Reason for exclusion |
|---|---|
sgpd / sbgp
|
Mid-fragment key rotation via seig sample groups is out of scope; KID changes MUST align with fragment boundaries (see Section 4). |
pssh (per-fragment) |
License-acquisition information is signaled via the CMSF contentProtections mechanism (see Section 5), per [CMAF] §7.4.3. |
subs
|
Sub-sample information for image subtitle profiles (e.g. im1i) is out of scope. |
DASH-IF Ingest [DASH-IF-INGEST] defines a CMAF-based push
interface for live encoders. One of its track shapes is the sparse
event-only track: a CMAF track that carries no media samples (or
zero-size samples) and exists purely to deliver timed events via
emsg boxes attached to its chunks. In LOCMAF, each such emsg
box rides verbatim as a genBox element ahead of the chunk's
locmafHeader (see Section 8). Multiple events in one chunk
become multiple genBox elements.¶
LOCMAF supports event-only tracks with no further extension. A full
header for an event-only group sets trunSampleCount = 0, carries
tfdtBaseMediaDecodeTime, is preceded by the chunk's emsg
genBoxes, and is followed by an empty mdat payload (the receiver
reconstructs an mdat box with an 8-byte header only). Two
encoder strategies are valid for subsequent chunks in the same
group:¶
Full header per chunk. Every event chunk carries a full
header with trunSampleCount = 0 and its absolute
tfdtBaseMediaDecodeTime, since a zero sample count produces
no derivation increment (Section 12.2). This costs a few
bytes per chunk and is recommended for sparse event-only
tracks.¶
Synthetic per-chunk sample. The encoder sets
trunSampleCount = 1 with a default_sample_duration equal to
the intended per-chunk advancement and a zero-size sample. BMDT
derivation then works, and subsequent chunks use delta headers.
This matches how DASH-IF Ingest commonly shapes sparse metadata
tracks (urim, stpp).¶
For new MOQT deployments, an MSF eventtimeline companion track
[MSF] is the preferred mechanism for event metadata. LOCMAF
event-only tracks are intended for gateways that transit-relay CMAF
Ingest content unchanged across MOQT.¶
This section defines a deterministic CMAF rebuild. Given the same
effective values (Section 15.1), genBox list, mdat
payload, and CMAF Header, every conformant implementation that
follows this section produces byte-identical output. That
byte-identical output is the canonical form and is the reference
used for conformance and golden-vector comparison.¶
A rawBoxes Object (Section 9) needs no rebuild: its canonical
form is its boxes bytes, verbatim. The remainder of this section
applies to moof-carrying Objects.¶
Because the canonical form is a function of the effective values
alone, it is invariant to every encoder representation choice:
where the group was split into full and delta chunks, field
ordering, redundant fields such as strict-cmf2 tfhd defaults,
and whether a value traveled as a per-sample list or as a default.
Two LOCMAF encodings decode to the same canonical bytes iff they
carry the same media — which is exactly the comparison an interop
harness needs.¶
A decoder MAY emit any functionally-equivalent CMAF that suits its
own pipeline — a different legal box order, a different tr_flags
packing, or extra tfhd defaults are all acceptable for local
playback. The canonical form is not a constraint on what a decoder
feeds its renderer; it is the single byte-comparable reference an
interop harness regenerates and diffs. A receiver MUST NOT assume
that some other endpoint's reconstructed bytes match its own unless
both produced the canonical form.¶
Decoding a chunk — applying deltas and deletions to the in-group reference, then the derivations of Section 11.2 and Section 12.2 — yields the chunk's effective values:¶
n = trunSampleCount and the BMDT;¶
the effective sample-description index: field 2 when present,
else trex.default_sample_description_index;¶
for each sample i in [0, n):¶
duration[i]: field 3's element i when present, else field 4
when present, else trex.default_sample_duration;¶
size[i]: per Section 11.2;¶
flags[i]: field 7's element i when present; else field 12
when present and i == 0; else field 8 when present; else
trex.default_sample_flags;¶
cto[i]: field 5's element i when present, else 0;¶
when CENC is in use: IV[i] (field 9) and the subsample map
(fields 11, 13, 15);¶
the ordered genBox list (see Section 8) and the mdat payload
P, whose length is the MOQT object length minus the bytes
consumed by all preceding elements.¶
The effective values are the chunk's meaning: they are what a
frame interface consumes directly (Section 16), and the only
chunk-derived input to the canonical reconstruction below. The
remaining inputs come from the CMAF Header moov: the single
trak's track_ID, the trex defaults, mdhd.timescale, and
(when CENC is in use) the tenc defaults.¶
The reconstructed chunk is the concatenation, in this order:¶
Inside moof, the order is mfhd then traf. Inside traf the
order is tfhd, tfdt, trun, and — when CENC metadata is present
— saiz, saio, senc appended in that order. This saiz/saio/
senc order is fixed by this section (see Section 15.8).¶
version=0, flags=0. LOCMAF does not carry
mfhd.sequence_number and it is not reconstructable from the LOCMAF
object alone; mfhd.sequence_number is not load-bearing for CMAF
chunk decode in playback pipelines. The canonical rule is therefore
mfhd.sequence_number = 0. (An implementation that needs the real
sequence number can derive it from the MOQT object identity, but
canonical comparison uses 0.)¶
version=0. The canonical tf_flags:¶
so sample-data offsets are relative to the start of the containing
moof ([ISOBMFF]). Each optional default sets its flag and emits
its value iff the effective values call for it — wire presence is
irrelevant, so a strict-cmf2 encoding (Section 4) canonicalizes
identically to its minimal counterpart:¶
sample-description-index-present (0x000002) iff the effective
sample-description index ≠
trex.default_sample_description_index;¶
default-sample-duration-present (0x000008) iff the effective
durations are all equal AND that value ≠
trex.default_sample_duration;¶
default-sample-size-present (0x000010) iff the uniform-size
case of Section 15.6.1 places a size here;¶
default-sample-flags-present (0x000020) iff the effective
flags covered by the default (all samples, or all but the first
when first-sample-flags is used, see Section 15.6) are
equal AND ≠ trex.default_sample_flags.¶
tfhd.track_ID is set to the track_ID of the single trak in
the CMAF Header's moov. The field order in the box is the
standard ISO order: track_ID, then each present optional in
flag-bit order.¶
version=1 always (64-bit baseMediaDecodeTime), flags=0. The
value is the absolute BMDT — carried in a full header, or derived
per Section 12.2 for a delta chunk. A fixed
version avoids a magnitude-dependent conditional in the canonical
form, and live decode timelines routinely exceed 32 bits anyway
(2^32 ticks is under 14 hours at a 90 kHz timescale, and
wallclock-anchored timelines are past it from the start); the 4
extra bytes exist only in the reconstructed chunk, not on the wire.¶
trun.version = 1 iff any effective composition-time offset is
negative, otherwise 0.¶
The canonical tr_flags, set and emitted in this order:¶
data-offset-present (0x000001) — always set;¶
first-sample-flags-present (0x000004) — set iff n > 1 and
the first sample's effective flags differ from the (equal)
effective flags of all other samples;¶
sample-duration-present (0x000100) — set iff the effective
durations are not all equal;¶
sample-size-present (0x000200) — set iff per-sample sizes are
used (see Section 15.6.1);¶
sample-flags-present (0x000400) — set iff the effective flags
are neither all equal nor covered by the first-sample-flags case
above;¶
sample-composition-time-offsets-present (0x000800) — set iff
any effective composition-time offset ≠ 0.¶
The trun field layout is sample_count, then data_offset, then
(if first-sample-flags-present) first_sample_flags (the effective
flags of the first sample), then the per-sample records populated
from the effective vectors, each carrying its present fields in ISO
order:
sample_duration, sample_size, sample_flags,
sample_composition_time_offset. When trun.version == 1 the
composition-time offset is a signed 32-bit value; when
trun.version == 0 it is unsigned (reached only when every offset
is ≥ 0).¶
When n == 0 (an event-only chunk, Section 14) there are no
effective per-sample values: every conditional tr_flags bit above
is clear, no per-sample records are emitted, and no optional tfhd
default is present (Section 15.4).¶
Let n = trunSampleCount and P = len(mdat payload).¶
Uniform sizes (all n samples share a single size s; a
single-sample chunk is trivially uniform with s = P). Place s
in tfhd.default_sample_size and set its flag per
Section 15.4 iff s ≠ trex.default_sample_size; when s
equals trex.default_sample_size, omit it from tfhd and rely
on trex. Emit no per-sample sizes in trun and clear
sample-size-present. Note that while the wire omits a single
sample's size (Section 11.2), the canonical CMAF
chunk MUST still carry it whenever it differs from the trex
default: ISO BMFF has no rule deriving a sample size from the
mdat length, so a chunk without it would not be
decode-equivalent.¶
Varying sizes (n > 1, sizes differ). Emit per-sample sizes
in trun and set sample-size-present; do not set
tfhd.default_sample_size. On the wire LOCMAF carries n − 1
sizes (field 1); the canonical trun reconstructs all n with
sample_size[n−1] = P − sum(first n−1 sizes).¶
Malformed cases the receiver MUST reject:¶
trun.data_offset = moof_size + 8: it points at the first byte of
the mdat sample data, just past the 8-byte mdat header, with
default-base-is-moof making offsets relative to the moof start.
The mdat header is always 8 bytes: uint32be(8 + len(P)) |
'mdat'. The ISO size escapes 0 and 1 are not allowed here
either: an mdat payload that does not fit the 32-bit size
(len(P) > 0xFFFFFFF7) cannot be packaged as LOCMAF, and a
receiver MUST reject a chunk that would require it.¶
This section applies when the track is protected
(tenc.default_isProtected = 1) and the chunk's effective values
(Section 15.1) include per-sample auxiliary information: a
non-zero per_sample_IV_size, an effective subsample map, or
both. Wire presence in the current header is irrelevant — under
delta encoding every CENC field may be inherited from the in-group
reference. The receiver reconstructs senc, saiz, and saio
with the following pinned layouts; saiz and saio are never
carried on the wire (see Section 13) and are recomputed here.¶
When a protected chunk has no per-sample auxiliary information —
per_sample_IV_size is 0 and there is no subsample map, as under
cbcs full-sample encryption with a constant IV — none of the
three boxes is emitted.¶
senc. version=0. flags = 0x000002
(senc_use_subsamples) iff the effective subsample map is
present, otherwise flags = 0. Layout: sample_count = n, then
for each sample i:¶
the InitializationVector, per_sample_IV_size bytes, taken
from sencInitializationVector (field 9);¶
when flags = 0x000002: subsample_count[i] as uint16 (from
field 11; 0 for a sample without subsamples), followed by
subsample_count[i] pairs of (BytesOfClearData as uint16,
BytesOfProtectedData as uint32) drawn from fields 13 and 15,
flattened in chunk order. Every sample carries the
subsample_count field when the flag is set.¶
saiz. version=0, flags=0; with flags=0 the optional
aux_info_type and aux_info_type_parameter fields are omitted —
their defaults per [CENC] §7.1 are the track's protection scheme
and 0. For each sample, the auxiliary information size is:¶
aux_size[i] = per_sample_IV_size ; flags = 0
aux_size[i] = per_sample_IV_size
+ 2 + 6 * subsample_count[i] ; flags = 0x000002
¶
so when the subsample flag is set, a sample with
subsample_count = 0 still counts its 2-byte subsample_count
field, matching the senc layout above. An aux_size[i] above
255 does not fit the 8-bit sample_info_size of [ISOBMFF] and
the receiver MUST reject the chunk. If all aux_size[i] are
equal, set default_sample_info_size to that common value,
sample_count = n, and emit an empty per-sample array. Otherwise
set default_sample_info_size = 0, sample_count = n, and emit
the n-entry sample_info_size array.¶
saio. version=0, flags=0, entry_count=1, with a single
offset locating the first byte of the first sample's auxiliary
information in senc (the first per-sample IV when
per_sample_IV_size > 0). Per
[ISOBMFF], saio offsets in a movie fragment are relative to the
same base as trun.data_offset — with default-base-is-moof
(Section 15.4), the first byte of the moof. genBoxes
preceding the moof therefore do not enter into the offset:¶
saio.offset = offset_of_senc_within_moof + 16¶
where offset_of_senc_within_moof is the byte offset of the senc
box from the start of the moof, and the 16 bytes skip the senc
box header (8 bytes), the FullBox version and flags (4 bytes),
and the sample_count field (4 bytes). The 32-bit offset of
version=0 always suffices, since the offset points within the
moof, whose size is bounded by its own 32-bit box size.¶
The encode side has an equally deterministic reference form. A LOCMAF encoding is canonical when:¶
every full and delta header emits exactly the fields required by the emission rules of Section 11, with no redundant fields;¶
every delta header carries exactly the fields whose represented
values changed from the in-group reference state — a list field
counts as changed when its length changes, even when the
surviving prefix is element-wise identical, so canonical encoding
never relies on inherited-list truncation alone — plus
deltaDeletedLocmafIDs listing exactly the fields that leave
that state (Section 12.3);¶
a full header appears only as the first moof-carrying Object of each group, immediately after a rawBoxes Object (Section 9), or where Section 12.2 requires one;¶
field IDs appear in ascending order; and¶
every vi64 uses its shortest form.¶
Two canonical encoders given the same CMAF input therefore produce byte-identical LOCMAF Objects, enabling golden vectors on the encode side as well as the decode side. Canonical encoding is not required for interoperability: any conformant encoding decodes to the same effective values and thus the same canonical CMAF.¶
Canonical reconstruction requires a normalization pass: an
implementation MUST NOT rely on incidental serializer output (e.g.
box order or tr_flags packing produced by a general-purpose ISO
BMFF writer) to match the canonical bytes. Golden vectors compare
the reconstructed genBox and moof bytes exactly; the mdat
payload is passed through unchanged.¶
LOCMAF carries the same elementary encoded samples as LOC
[LOC]: the mdat payload is the concatenation of coded samples
in decode order, and the moof-header fields give each sample's
size, decode time, composition-time offset, and (when protected)
its CENC metadata. A receiver therefore has two equally valid
consumption paths, matching the two shapes playback interfaces
come in: those that accept ISO BMFF chunks and those that accept
individual media frames.¶
Chunk interfaces. Reconstruct a CMAF chunk per Section 15
and feed it to an interface that accepts ISO BMFF / CMAF input,
initialized with the CMAF Header (see Section 6). In the
browser this is Media Source Extensions (MSE): append the chunk
to a SourceBuffer whose initialization segment is the CMAF
Header. Native media pipelines built around a fragmented-MP4
demuxer consume the same reconstructed chunks.¶
Frame interfaces. Slice the mdat payload into per-sample
byte ranges using the effective sample values
(Section 15.1) and feed each coded frame to a decoder,
using the decode time, composition-time offset, and sample flags
to set the frame's timestamp, duration, and key/delta type. The
CMAF Header supplies the codec configuration, and for
CENC-protected content the effective per-sample IV and subsample
map supply the decryption parameters. In the browser this is
WebCodecs: each frame becomes an EncodedVideoChunk /
EncodedAudioChunk fed to a VideoDecoder / AudioDecoder.
Native decoder APIs that accept individual coded frames work the
same way.¶
LOCMAF is a media container, not tied to any particular playback interface; neither path is privileged by this document. Producing the canonical CMAF chunk is required only for golden-vector conformance, not for playback.¶
This section is informative. LOCMAF is specified for carriage as MOQT Object payloads, where the transport supplies the framing: MOQT gives every Object its length, the subgroup gives in-order delivery, and the group boundary marks where a full header re-anchors the delta chain. A LOCMAF Object carries no length of its own, so any use outside MOQT has to restore that framing. This section sketches the minimal wrapping; a full specification of such carriage is out of scope for this document.¶
A LOCMAF segment is the self-framed equivalent of one MOQT group: the concatenation of the group's LOCMAF Objects (Section 7) in decode order, each prefixed by its length —¶
object_length vi64 LOCMAF Object object_length bytes¶
repeated once per Object. The first moof-carrying Object of a segment carries a full header, exactly as at a MOQT group boundary, and delta chunks reference the preceding Object in the same segment (Section 12).¶
The same framing doubles as a storage format. A LOCMAF segment is
directly a file on disk, and segments concatenate into longer
files that remain parseable, since every Object is length-prefixed
and every full header re-anchors decoding. The CMAF Header is
either stored alongside, as for CMAF initialization and media
segments, or carried in-band as a leading length-prefixed rawBoxes
Object (Section 9) holding the ftyp + moov bytes verbatim.
The in-band form makes the file self-contained: a reader obtains
the initialization bytes exactly as published and decodes every
subsequent Object against them, just as over MOQT.¶
The framing also maps directly onto low-latency HTTP delivery:¶
In low-latency DASH, a LOCMAF segment is delivered progressively under chunked transfer encoding, each length-prefixed Object taking the role a CMAF chunk has today, with the CMAF Header referenced from the MPD as the initialization segment.¶
In low-latency HLS, each part carries one or more length-prefixed Objects. A part advertised as independent begins with an Object carrying a full header, so that a client joining there has a complete decode anchor — the part-level counterpart of the mid-group full header of Section 7.1. Because the framing is part of the payload rather than of the HTTP envelope, concatenating the parts of a segment yields the same bytes as the DASH segment and the same file as stored at rest.¶
The catalog signaling of Section 5 has no MPD or playlist
counterpart defined here: at minimum the packaging, the
locmafVersion, the initialization-data reference, and any
content-protection signaling need manifest-level equivalents, and
the segments need a dedicated media type (such as video/locmaf,
audio/locmaf, or application/locmaf) so that clients do not
mistake them for CMAF. Registration of such media types would
accompany a future specification of this carriage.¶
LOCMAF is a compact packaging for CMAF media and introduces no new authentication or confidentiality mechanism. It is carried as MOQT [MOQT] Object payloads and inherits QUIC's transport security. Per-sample encryption metadata defined by [CENC] is preserved through the LOCMAF round-trip; LOCMAF neither weakens nor strengthens the underlying DRM scheme.¶
A receiver expands attacker-influenceable input (the delta state, the per-sample lists, the genBox payloads) into ISO BMFF [ISOBMFF] boxes that are then fed to a media pipeline. A receiver MUST validate that every reconstructed box is well-formed before passing it onward, and in particular:¶
The receiver MUST bound the length of every reconstructed
per-sample and per-subsample list against trunSampleCount (and,
for the subsample lists, against the reconstructed
sencSubsampleCount totals) before allocating or copying.¶
The receiver MUST verify that the sum of reconstructed
sencBytesOfClearData and sencBytesOfProtectedData for each
sample with a non-zero subsample count equals that sample's size
(a sample whose subsample count is 0 carries no subsample map and
is unconstrained).¶
The receiver MUST verify that every reconstructed
BytesOfClearData value fits in 16 bits and every
BytesOfProtectedData value fits in 32 bits — the senc field
widths of Section 15.8 — and MUST reject the chunk
otherwise.¶
The receiver MUST verify that every sample's reconstructed
auxiliary-information size fits the 8-bit saiz
sample_info_size (Section 15.8) and MUST reject the
chunk otherwise.¶
The receiver MUST reject the malformed sample-size cases listed in Section 15.6.1.¶
The receiver MUST treat an unknown leading element_type as a malformed object (see Section 7.1): unknown top-level element types are not self-delimiting and MUST NOT be skipped.¶
Each genBox payload is wrapped into an ISO BMFF box of declared
length 4 + box_size; the receiver MUST ensure box_size is at
least 4, at most 0xFFFFFFFB, and does not exceed the bytes
actually present in the element, and MUST validate the wrapped box
against the structural rules for its box_name before use.¶
A rawBoxes element's boxes content (Section 9) is likewise
attacker-influenceable and is passed onward verbatim. Before use,
the receiver MUST verify that it parses as a sequence of complete
top-level boxes — each declared size at least 8, neither ISO
size escape used, and the sizes summing to exactly the Object
bytes that follow the element_type — and MUST validate each box
against the structural rules for its type.¶
Replay considerations within a MOQT group are inherited from [MOQT]; LOCMAF adds no new replay attack surface.¶
This document has no IANA actions.¶
The initial version of LOCMAF was developed as part of the Master Thesis work of Hugo Björs at Eyevinn Technology, supervised by Torbjörn Einarsson.¶
The author thanks the Media over QUIC working group, in particular the authors and contributors to [MOQT], [MSF], and [CMSF], for the prior art this work builds on.¶