Skip to content

Commit f6637b4

Browse files
committed
feat(media): add conformance corpus and MSF-01/CMSF-01 playback support
Establish a language-neutral media conformance corpus and bring the media stack up to the current catalog drafts. Conformance: add a schema-validated corpus of LOC property blocks, LOC semantics, catalogs, and BMFF structures, with a runner that executes all 127 vectors against production. Expectations are authored independently of the implementation. An opt-in lane compares results against an external LibMoQ build over the media-probe protocol; neither repository vendors the other. Transport/LOC: use vi64 for the draft-18 property wire, selected from the negotiated draft rather than configuration. Support the draft-14, draft-16, and draft-18 wire profiles while retaining LOC-01 semantic resolution behind APIs designed for additive LOC-02/-04 and MSFTS support. Catalogs: parse MSF-01/CMSF-01 documents alongside MSF-00, including root initDataList, per-track initRef, and content-protection metadata. Parse and apply MSF-01 op-array deltas in document order while preserving reference integrity. Player: resolve catalog init references for CMAF bootstrap, apply ordered catalog updates, and preserve content-protection metadata without claiming protected-playback support. Browser/MSE: make playback startup a single owned, cancellable lifecycle; wait for non-zero startup positioning to settle; prevent late play completions and superseded SourceBuffer events from mutating current state; and serialize re-entrant state announcements. Add debug-only startup and buffer chronology diagnostics. Examples/CI: allow the Node publisher to emit modern catalog and delta shapes, and add the conformance and soak workflows. MediaSource implementation, attachment, and legacy-epoch controls are diagnostic tools. Production defaults remain ManagedMediaSource-first, automatic attachment, and shared cross-track epochs. This commit does not claim that the Safari A/V synchronization issue is resolved.
1 parent 1462872 commit f6637b4

227 files changed

Lines changed: 23374 additions & 2745 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Wire vectors are normative BYTES. Marking them binary stops Git from
2+
# diffing them as text (a vector ending in 0x20 is not "trailing whitespace")
3+
# and, more importantly, from ever applying EOL normalization — a CRLF rewrite
4+
# on checkout would silently change the bytes the vector asserts.
5+
*.bin binary

.github/workflows/ci.yml

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# PR gate — fast, hermetic checks only. Every step here exists and runs today;
2+
# expensive lanes (soaks, fuzz, browser, differential) live in nightly.yml and
3+
# are added only when their implementations land.
4+
name: ci
5+
6+
on:
7+
pull_request:
8+
push:
9+
branches: [main]
10+
11+
concurrency:
12+
group: ci-${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
permissions:
16+
contents: read
17+
18+
jobs:
19+
gate:
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 30
22+
steps:
23+
- uses: actions/checkout@v4
24+
with:
25+
# Full history so the whitespace check can diff the PR's committed range.
26+
fetch-depth: 0
27+
28+
# pnpm version comes from the root package.json "packageManager" field.
29+
- uses: pnpm/action-setup@v4
30+
31+
- uses: actions/setup-node@v4
32+
with:
33+
node-version: 24
34+
cache: pnpm
35+
36+
- name: Install (frozen lockfile)
37+
run: pnpm install --frozen-lockfile
38+
39+
- name: Build all packages
40+
run: pnpm -r build
41+
42+
- name: Test
43+
run: pnpm test
44+
45+
- name: Typecheck (per package)
46+
run: |
47+
set -euo pipefail
48+
for p in transport webtransport playback loc player browser msf playa; do
49+
echo "── tsc: packages/$p"
50+
pnpm --dir "packages/$p" exec tsc --noEmit
51+
done
52+
53+
- name: Typecheck + run the media conformance corpus
54+
run: |
55+
set -euo pipefail
56+
pnpm --filter @moqt/media-conformance-runner typecheck
57+
pnpm test:corpus
58+
59+
- name: Examples (player build + node-relay typecheck)
60+
run: |
61+
set -euo pipefail
62+
pnpm --filter @moqt/examples build
63+
pnpm --filter @moqt/example-node-relay typecheck
64+
65+
- name: Whitespace check (committed PR range)
66+
run: |
67+
set -euo pipefail
68+
if [ "${{ github.event_name }}" = "pull_request" ]; then
69+
base="${{ github.event.pull_request.base.sha }}"
70+
else
71+
# push: previous tip when available; first/force pushes fall back to HEAD~1.
72+
base="${{ github.event.before }}"
73+
if [ "$base" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$base" 2>/dev/null; then
74+
base="$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD)"
75+
fi
76+
fi
77+
echo "Checking whitespace over $base..HEAD"
78+
git diff --check "$base"..HEAD
79+
80+
- name: Post-build dirty-tree check
81+
# Build/test must not mutate tracked files: a diff here means generated
82+
# output drifted from what is committed (a plain diff --check on a clean
83+
# checkout would be vacuous — this is the check that has teeth).
84+
run: |
85+
set -euo pipefail
86+
status="$(git status --porcelain)"
87+
if [ -n "$status" ]; then
88+
echo "Working tree dirty after build/test:"
89+
echo "$status"
90+
git diff | head -100
91+
exit 1
92+
fi

.github/workflows/nightly.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Scheduled lanes — the expensive checks that must not block PRs or publishing.
2+
# Only lanes whose implementations exist are wired; the media differential,
3+
# browser matrix, and corpus fuzz lanes are added by their own slices.
4+
name: nightly
5+
6+
on:
7+
schedule:
8+
- cron: "17 9 * * *" # daily, 09:17 UTC (off the top-of-hour rush)
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
soak:
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 120
18+
steps:
19+
- uses: actions/checkout@v4
20+
- uses: pnpm/action-setup@v4
21+
- uses: actions/setup-node@v4
22+
with:
23+
node-version: 24
24+
cache: pnpm
25+
- name: Install (frozen lockfile)
26+
run: pnpm install --frozen-lockfile
27+
- name: Build all packages
28+
run: pnpm -r build
29+
- name: Scenario soak (draft-18)
30+
run: pnpm test:soak
31+
- name: Scenario soak (legacy drafts)
32+
run: pnpm test:soak:legacy
33+
34+
fuzz:
35+
runs-on: ubuntu-latest
36+
timeout-minutes: 120
37+
steps:
38+
- uses: actions/checkout@v4
39+
- uses: pnpm/action-setup@v4
40+
- uses: actions/setup-node@v4
41+
with:
42+
node-version: 24
43+
cache: pnpm
44+
- name: Install (frozen lockfile)
45+
run: pnpm install --frozen-lockfile
46+
- name: Build all packages
47+
run: pnpm -r build
48+
- name: Property/parser-crash fuzz (FC_RUNS=5000)
49+
run: pnpm test:soak:fuzz

conformance/media/MANIFEST.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"corpusSchema": "moq-media-corpus/1",
3+
"domains": {
4+
"bmff": 7,
5+
"catalog": 55,
6+
"loc": 32,
7+
"properties": 33
8+
},
9+
"totalVectors": 127
10+
}

conformance/media/README.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Media Conformance Corpus (`moq-media-corpus/1`)
2+
3+
A language-neutral corpus of media-format conformance vectors — LOC property
4+
blocks, LOC semantics, catalogs, BMFF structures — consumed by two
5+
implementations: this repository (TypeScript, via `runner/`) and an external
6+
LibMoQ (C) build, reached only through the `moq-media-probe/1` JSONL protocol —
7+
neither repository vendors the other. This is **private test infrastructure**:
8+
it lives outside `packages/` and is never published to npm.
9+
10+
Every vector is schema-validated, and a vector is executed only against a seam
11+
that actually exists. A vector kind whose runner API does not exist yet is
12+
capability-marked rather than silently skipped — counted and reported, never run
13+
against a non-existent API. Nothing is marked pending today.
14+
15+
## Layout
16+
17+
```
18+
conformance/media/
19+
README.md # this file — governance
20+
MANIFEST.json # derived index (schema + per-domain counts + total)
21+
schema/ # JSON Schema for the manifest + scenario formats
22+
vectors/
23+
properties/ # Layer-A property-block-decode (executable)
24+
loc/ # loc-properties (A+B) + loc-semantics (both executable)
25+
catalog/ # catalog-parse (executable via parseCatalogAuto)
26+
bmff/ # bmff-structure (executable via mp4-box utilities)
27+
runner/ # private TS workspace package: loader, validator, tests
28+
```
29+
30+
## Governance
31+
32+
### Provenance (where the bytes came from)
33+
34+
Every entry carries a mandatory `provenance` block with a `class`:
35+
36+
- **`spec-derived`** — bytes hand-constructed to match a cited draft section
37+
(e.g. a canonical vi64 encoding). The only class that is *conformance*
38+
evidence, and only after independent review against the citation.
39+
- **`third-party`** — output of an external tool (ffmpeg, GPAC, moq-rs). Interop
40+
evidence.
41+
- **`implementation-generated`** — output of Playa or LibMoQ. *Regression pins
42+
only*, never conformance evidence.
43+
44+
Provenance says where the bytes came from; `expectationBasis` (below) says what
45+
the expectation *claims*. A spec-derived vector can still be an `interpretation`.
46+
47+
### `expectationBasis` (what the expectation claims)
48+
49+
- **`normative`** — a MUST / registry entry with a precise citation.
50+
- **`interpretation`** — a documented Playa policy where the spec is ambiguous,
51+
silent, or its registry is incomplete (see policies below).
52+
- **`interop`** — matches observed third-party behavior.
53+
- **`regression`** — pins current behavior with no spec claim (e.g. the silent
54+
audio-level masking and duplicate last-wins the current parser exhibits).
55+
56+
### Oracle independence (the load-bearing rule)
57+
58+
For a `normative` or `interpretation` vector, the `expect` block is an
59+
**independently authored literal** — reasoned from the cited draft, never
60+
captured from the implementation under test. The authoring script then runs
61+
Playa *only* to populate `differential.playa`: absent (⇒ Playa matches the
62+
authored truth) or `diverges` + `currentBehavior` (⇒ it does not). This prevents
63+
an existing bug from being blessed as the normative answer. A `regression`
64+
vector is the sole exception — its `expect` legitimately IS the current
65+
implementation behavior, because that is what a regression pin means.
66+
67+
The eighteen draft-18 divergence drivers (`loc/props-d18-*-diverges`) are the
68+
concrete payoff — every draft-18 value ≥ 64 in both directions (9 decode + 9
69+
encode). They drove the vi64 wiring fix: the `-diverges` id suffix records that
70+
origin, and they now pass against production directly (no differential).
71+
72+
### Canonical ordering / duplicate policy
73+
74+
- **PropertyMap decode** preserves occurrence order and duplicates losslessly
75+
(dedup / last-wins is a Layer-B *semantic* policy, never a Layer-A behavior).
76+
- **Canonical encode** emits Key-Value-Pairs in **stable ascending-ID order**,
77+
preserving the relative order of duplicate IDs — matching the current
78+
encoders. (draft-18 §1.4.3.)
79+
- Trace event order is the **actual emission order** (never re-sorted); JSON key
80+
order within a record is cosmetic (structural comparison).
81+
82+
### Documented policies (all `interpretation`)
83+
84+
These are exercised by their vectors once the corresponding format support
85+
lands; recorded now so the corpus and its consumers share one reading. Each also becomes an upstream
86+
issue (to be filed).
87+
88+
- **LOC02-P1** — LOC-02 id `0x06` is interpreted as TIMESTAMP per the IANA table
89+
(§6.1), which registers only TIMESTAMP; the §2.3.3.1 Audio Level prose
90+
assignment (also `0x06`) is treated as an erratum. No value-magnitude
91+
heuristics; Audio Level is unavailable in LOC-02; a session-scoped diagnostic
92+
is emitted; the raw id/value is always preserved.
93+
- **LOC02-P1b** — LOC-02 ids `0x04` (Video Frame Marking) and `0x0D` (Video
94+
Config) are accepted per their unambiguous prose assignments even though the
95+
§6.1 registry omits them (the registry resolves conflicts; it does not
96+
invalidate unambiguous definitions). One session-scoped diagnostic lists the
97+
prose-only properties accepted.
98+
- **LOC02-P3** — running LOC-02 semantics over transport-18 is an *interop
99+
profile*, not strict paired-draft conformance (LOC-02 normatively references
100+
transport-17). Nine-byte Timestamps are accepted.
101+
- **LOC-P2** — a zero Timescale (unspecified in every LOC draft) is a
102+
construction-time rejection (`invalid-timescale-zero`).
103+
104+
### Stable ids + tombstones
105+
106+
Ids are stable slugs (`domain/name`). They are never renamed. A retired vector
107+
is **tombstoned rather than deleted**: its manifest entry is replaced by
108+
`{ "id": "domain/name", "retired": { "reason": "…" } }`, which the schema
109+
accepts as an inert entry (no execution, no file). This keeps the id reserved
110+
(never reused) and the history diffable. Reviving a retired behavior uses a new
111+
id.
112+
113+
### Canonical encoding
114+
115+
- Every u64/i64-typed field is **always a decimal string** — never a JSON number
116+
(a JSON number above 2^53-1 silently loses precision). The loader rejects a
117+
JSON number in a wide-integer field.
118+
- Floats are prohibited. Bytes are lowercase hex; long blobs are
119+
`{sha256, byteLength}`. Strings compare as exact code points.
120+
- Error comparisons use `error.category`, never message text.
121+
122+
### Read-only + regeneration
123+
124+
The corpus is read-only by default. The runtime `GEN_CORPUS=1` regeneration gate
125+
may rewrite ONLY `implementation-generated` entries produced by this repo;
126+
`assertRegenerable` refuses spec-derived and third-party bytes.
127+
128+
The corpus was authored by `runner/src/build-corpus.ts` (run manually with
129+
`AUTHOR_CORPUS=1`), which constructs spec-derived bytes deterministically and
130+
captures the exact current production behavior for executable vectors. It is
131+
byte-stable across runs.
132+
133+
### Third-party fixture import (LibMoQ)
134+
135+
The 14 LibMoQ MSF fixtures are imported byte-for-byte (5 executable MSF-00,
136+
9 forward-looking MSF-01/CMSF-01) with pinned provenance in
137+
`provenance/libmoq-msf-fixtures.json` (+ a human table in the sibling `.md`).
138+
139+
Normal authoring is **hermetic** — it re-derives from the checked-in snapshot
140+
(`vectors/catalog/libmoq_*.json`), so a fresh clone or CI reproduces the corpus
141+
with no external checkout. The pinned per-fixture SHA-256 gates every byte, so a
142+
tampered snapshot fails authoring.
143+
144+
```
145+
# Hermetic re-author (default; no LibMoQ needed):
146+
AUTHOR_CORPUS=1 tsx conformance/media/runner/src/build-corpus.ts
147+
148+
# Deliberately RE-IMPORT from a LibMoQ worktree (opt-in). Verifies the worktree
149+
# HEAD is the pinned commit before copying, then re-gates every byte by SHA-256:
150+
LIBMOQ_REFRESH=1 LIBMOQ_ROOT=/path/to/libmoq \
151+
AUTHOR_CORPUS=1 tsx conformance/media/runner/src/build-corpus.ts
152+
```
153+
154+
The pinned commit lives in `build-corpus.ts` (`LIBMOQ_COMMIT`) and the provenance
155+
file. Refresh runs `git rev-parse HEAD` via a direct exec (never a shell), so a
156+
`LIBMOQ_ROOT` containing spaces or metacharacters is handled literally.
157+
158+
## Running
159+
160+
```
161+
pnpm test:corpus # the runner test suite (loader, validator, per-domain execution)
162+
pnpm test # includes the corpus lane
163+
```
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# LibMoQ MSF fixture import — pinned provenance
2+
3+
**Source:** `openmoq/moq5 (LibMoQ)` @ `455318cade7445880a294e2ec6e6a5ccb67cb776` — path `media/msf/tests/fixtures`
4+
5+
The planned **13 executable + 1 MSF-01-shaped** split was **disproved** by the source audit (5 + 9); the
6+
MSF-01/CMSF-01 parser promoted the later-era catalogs and the op-array delta parser
7+
promoted the delta documents. All 14 fixtures now **resolve to 14 executable /
8+
0 forward-looking** (5 numeric MSF-00/CMSF-00 + 7 MSF-01/CMSF-01 catalogs + 2 MSF-01 op-array deltas).
9+
Byte-exact profile identities are preserved: 4 MSF-01 catalogs, 3 CMSF-01 catalogs, 2 MSF-01 delta documents.
10+
11+
This file is generated by `AUTHOR_CORPUS=1 build-corpus.ts`; the JSON sibling is the machine-checked authority.
12+
13+
| # | LibMoQ fixture | SHA-256 | Corpus ID | Corpus file | Era/profile | Capability | Basis |
14+
|--:|---|---|---|---|---|---|---|
15+
| 1 | `av_single.json` | `f07dd4722601…` | `catalog/libmoq-av-single` | `libmoq_av_single.json` | msf-00 | executable | normative |
16+
| 2 | `cmsf_clearkey.json` | `74831a3b1b74…` | `catalog/libmoq-cmsf-clearkey` | `libmoq_cmsf_clearkey.json` | cmsf-01 | executable | interpretation |
17+
| 3 | `cmsf_cmaf_simulcast.json` | `9d53c32066d0…` | `catalog/libmoq-cmsf-cmaf-simulcast` | `libmoq_cmsf_cmaf_simulcast.json` | cmsf-01 | executable | interpretation |
18+
| 4 | `cmsf_drm_cbcs.json` | `48b036709de7…` | `catalog/libmoq-cmsf-drm-cbcs` | `libmoq_cmsf_drm_cbcs.json` | cmsf-01 | executable | interpretation |
19+
| 5 | `delta_add_clone.json` | `9b1f6fa8140f…` | `catalog/libmoq-delta-add-clone` | `libmoq_delta_add_clone.json` | msf-01-delta | executable | interpretation |
20+
| 6 | `delta_remove.json` | `1cc3b8d80da3…` | `catalog/libmoq-delta-remove` | `libmoq_delta_remove.json` | msf-01-delta | executable | interpretation |
21+
| 7 | `empty_tracks.json` | `cc93a54da66a…` | `catalog/libmoq-empty-tracks` | `libmoq_empty_tracks.json` | msf-00 | executable | normative |
22+
| 8 | `mediatimeline.json` | `b5da1dc96f6f…` | `catalog/libmoq-mediatimeline` | `libmoq_mediatimeline.json` | msf-01 | executable | interpretation |
23+
| 9 | `minimal.json` | `a0bf6eed5a81…` | `catalog/libmoq-minimal` | `libmoq_minimal.json` | msf-00 | executable | normative |
24+
| 10 | `template.json` | `98d6cb8196bc…` | `catalog/libmoq-template` | `libmoq_template.json` | msf-01 | executable | interpretation |
25+
| 11 | `termination.json` | `dfdc4e8e5652…` | `catalog/libmoq-termination` | `libmoq_termination.json` | msf-01 | executable | interpretation |
26+
| 12 | `unknown_fields.json` | `4f3fc0bc4e0e…` | `catalog/libmoq-unknown-fields` | `libmoq_unknown_fields.json` | msf-00 | executable | regression |
27+
| 13 | `vod.json` | `e377cfa4e892…` | `catalog/libmoq-vod` | `libmoq_vod.json` | msf-01 | executable | interpretation |
28+
| 14 | `with_init_data.json` | `6c70626f51fe…` | `catalog/libmoq-with-init-data` | `libmoq_with_init_data.json` | msf-00 | executable | normative |

0 commit comments

Comments
 (0)