Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/interop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ jobs:
# and a passing one is deleted as usual.
MOQ_TEST_RUNS: ${{ runner.temp }}/moq-test-runs

# Registry installs and the release matrix run only on main, nightly or
# on demand: a break only needs catching before the next release.
- name: Released wire compatibility
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
run: nix develop --command just test wire-compat
shell: bash -leo pipefail {0}
env:
GH_TOKEN: ${{ github.token }}
MOQ_TEST_RUNS: ${{ runner.temp }}/moq-test-runs

- name: Interop negative control
run: nix develop --command just test interop-negative
shell: bash -leo pipefail {0}
Expand Down
1 change: 0 additions & 1 deletion quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ were done or not worth their cost.
- [FFI runtime](/quest/m1/ffi-runtime.md) - moq-ffi drives moq on a multi-thread runtime instead of one thread
- [Audio group duration](/quest/m1/audio-group-duration.md) - audio groups span at least 20 ms by default, so small frames don't mint a group each
- [Pipelined requests](/quest/m1/pipeline-requests/README.md) - SUBSCRIBE and the first FETCH go out with the track-info request at every hop, so first data arrives a round trip sooner per hop
- [Wire compatibility](/quest/m1/wire-compat.md) - a nightly run tests this checkout against the last published release for tokens, session wire, and catalog/container
- [Kotlin wrapper POMs](/quest/m1/kt-ffi-pom.md) - Maven builds of `dev.moq:moq` resolve a published moq-ffi instead of the missing `0.0.0-dev`
- [Installable moq-gst](/quest/m1/gst-brew-path.md) - `brew install moq-gst` installs the plugin from the tarball's `lib/gstreamer-1.0/`, and the Nix package carries `x264enc`, `avenc_aac`, and `avdec_h264`
- [Same-epoch importers](/quest/m1/hop-aligned-import.md) - importers sharing one `--epoch` and fed one stream publish identical groups and timestamps, so failover between a redundant pair survives
Expand Down
1 change: 0 additions & 1 deletion quest/m1/lite07-finalize.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,4 @@ is published; older versions are unchanged.
## Related

- [The moq.pro mesh runs lite-07](/quest/m3/lite07-mesh.md) - the deployment condition that follows this
- [Wire compatibility](/quest/m1/wire-compat.md) - the nightly covers lite-07 once a release carries it
- [moq.pro: lite-07 on cluster dials](https://github.com/moq-dev/moq.pro/blob/main/quest/m2/lite07.md) - the mesh rollout that adopts the finalized version, and the customer rollout after it; moq.pro holds it in m2 until the cut is called
61 changes: 0 additions & 61 deletions quest/m1/wire-compat.md

This file was deleted.

47 changes: 47 additions & 0 deletions test/interop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,53 @@ Subscriber names: `rust`, `python`, `go`, `cpp`, `js` (browser), `js-native-node
A client whose source build fails fails only its own matrix cells (see
`mark_broken` in `interop.sh`); it never aborts the rest of the run.

## Released wire compatibility

`just test wire-compat` compares this checkout with the newest stable,
non-yanked crates.io and npm releases. The existing Interop workflow runs it
nightly and on demand on `main`. Registry installation is omitted from PR runs;
resolver and removed-version regressions run through `just test harness`.

The run records exact releases, npm's resolved lockfile, both CLI help outputs,
and each matrix cell in the harness artifacts. It uses checksummed GitHub
binaries when available on Linux x86\_64, with an exact-version `cargo install`
fallback. Run inside `nix develop` with GitHub CLI registry access.

Four sources sign JWTs: current and published Rust CLI, and current and
published `@moq/auth`. Both Rust CLIs and both JavaScript packages verify every
token and assert its normalized root and permission scope. Current and released
`hang` and `@moq/hang` encode and decode one another's catalogs and legacy frame
headers, checking the exact timestamp and payload. These are format checks,
independent of a media decoder.

The session lanes take the moq-lite drafts from each CLI's `--connect-version`
choices. IETF drafts are left out on purpose: our clients and relays always
prefer moq-lite with each other, and `just test interop` covers IETF. The relay
offers only the cell's version, and JavaScript checks the negotiated version.
Current Rust media publishers feed released Rust and JS readers, then released
publishers feed current readers, through both relay sources. Rust exports must
decode to a video frame through ffmpeg. The existing JS subscriber
reconstructs the catalog and decodes the container.

Each `moq-net` library states whether a draft has FETCH (lite-05 onward
today). A draft without it in both is logged and its FETCH lanes skipped; one
the release supports and the checkout dropped fails. Both Rust versions FETCH the group the
publisher's own JS subscriber observed and compare every frame's exact
payload. JS publishers write one group after that subscriber's demand, and the
opposite-source Rust CLI FETCHes it by the same observed ID.

Every session cell runs, and the run counts the cells that ran and were
skipped, then lists each failing cell before it fails.
Checkout-only versions are logged and omitted. Removing a released version
fails before sessions start. A maintainer-approved break belongs in
`compat/planned-breaks.json` with its reason and the exact affected release
versions. Keyed by a protocol name it drops that version; with `cells` it
skips only the named lanes, optionally narrowed to versions, a relay source,
or a publisher source. Every skipped cell is logged with the break's name.
Entries become errors once an affected release changes or no longer offers a
listed version, forcing removal or a fresh review. Future WIP drafts receive
no automatic exception.

## Media QA

The matrix asks "did bytes arrive and did a pixel light up". That passes on a
Expand Down
42 changes: 30 additions & 12 deletions test/interop/clients/js-native/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*
* @module
*/
import { writeFileSync } from "node:fs";
import { parseArgs } from "node:util";
import * as Catalog from "@moq/hang/catalog";
import * as Container from "@moq/hang/container";
Expand All @@ -36,6 +37,8 @@ const { positionals, values } = parseArgs({
url: { type: "string" },
broadcast: { type: "string" },
timeout: { type: "string", default: "20" },
"track-file": { type: "string" },
track: { type: "string" },
},
});

Expand All @@ -48,9 +51,26 @@ if (role !== "subscribe" || !url || !broadcast || !Number.isFinite(timeoutMs) ||
process.exit(2);
}

// The .hang catalog lives on the "catalog.json" track. It's a @moq/json snapshot+delta
// value, reconstructed by Json.Snapshot.Consumer. A lazy publisher may announce video in a
// later update, so keep reading until one has it.
async function video(bc: Moq.Broadcast.Consumer): Promise<[string, Catalog.VideoConfig]> {
const track = bc.track("catalog.json").subscribe({ priority: Catalog.PRIORITY.catalog });
const catalog = new Json.Snapshot.Consumer<Catalog.Root>({ track, schema: Catalog.RootSchema });
for (;;) {
const root = await catalog.next();
if (!root) throw new Error("catalog ended without a video track");
const first = Object.entries(root.video?.renditions ?? {})[0];
if (first) return first;
}
}

async function run(): Promise<void> {
const origin = new Moq.Origin.Producer();
const connection = await Moq.Connection.connect({ url: new URL(url as string), consume: origin });
const connect = process.env.INTEROP_COMPAT_TRANSPORT
? (await import(process.env.INTEROP_COMPAT_TRANSPORT)).connect
: Moq.Connection.connect;
const connection = await connect({ url: new URL(url as string), consume: origin });
let requested: Moq.Origin.Requesting | undefined;
try {
const path = Moq.Path.from(broadcast as string);
Expand All @@ -61,19 +81,17 @@ async function run(): Promise<void> {
bc = requested.active.peek();
}

// The .hang catalog lives on the "catalog.json" track. It's a @moq/json
// snapshot+delta value, reconstructed by Json.Snapshot.Consumer. A lazy publisher may
// announce video in a later update, so keep reading until one has it.
const track = bc.track("catalog.json").subscribe({ priority: Catalog.PRIORITY.catalog });
const catalog = new Json.Snapshot.Consumer<Catalog.Root>({ track, schema: Catalog.RootSchema });
let video: [string, Catalog.VideoConfig] | undefined;
while (!video) {
const root = await catalog.next();
if (!root) throw new Error("catalog ended without a video track");
video = Object.entries(root.video?.renditions ?? {})[0];
if (values["track-file"]) {
// Name a group that live demand is filling, so a FETCH of it never races eviction.
// The track is `--track`, or the catalog's first video rendition.
const name = values.track ?? (await video(bc))[0];
const group = await bc.track(name).subscribe({ priority: 0 }).recvGroup();
if (!group) throw new Error(`${name} ended before its first group`);
writeFileSync(values["track-file"], `${name}\n${group.sequence}\n`);
return;
}

const [name, config] = video;
const [name, config] = await video(bc);
let format: Container.Format;
if (config.container.kind === "legacy") {
format = new Container.Legacy.Format(config);
Expand Down
Loading
Loading