From 84d538cb628941ec93dde56087ac548d6fc21f48 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 20 Sep 2026 07:20:43 -0700 Subject: [PATCH 1/6] chore(hang): claim catalog API quest Co-Authored-By: GPT-5 From ba71d708303a96413e115f1683385e9868fe583e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 20 Sep 2026 08:17:43 -0700 Subject: [PATCH 2/6] feat(hang)!: unify catalog APIs Co-Authored-By: GPT-5 --- doc/concept/hang.md | 10 +- drafts/draft-lcurley-moq-hang.md | 11 +- js/hang/src/catalog/archive.ts | 20 +- js/hang/src/catalog/binary.ts | 4 - js/hang/src/catalog/clock.ts | 2 +- js/hang/src/catalog/index.ts | 1 - js/hang/src/catalog/json.ts | 4 - js/hang/src/catalog/timeline.ts | 43 --- js/hang/src/container/index.ts | 1 - js/hang/src/container/legacy.ts | 2 +- js/hang/src/index.ts | 1 + js/hang/src/{container => }/timeline.test.ts | 11 +- js/hang/src/{container => }/timeline.ts | 10 +- quest/m1/README.md | 1 - quest/m1/api-hang-catalog.md | 50 --- quest/m1/api-mux-rendition.md | 4 - quest/m1/api-review-gate.md | 1 - quest/m2/hang-catalog-consumer.md | 4 +- rs/hang/examples/video.rs | 2 +- rs/hang/src/catalog/archive.rs | 92 ++--- rs/hang/src/catalog/binary.rs | 7 +- rs/hang/src/catalog/clock.rs | 196 +++++----- rs/hang/src/catalog/hex.rs | 4 +- rs/hang/src/catalog/json.rs | 7 +- rs/hang/src/catalog/millis.rs | 2 +- rs/hang/src/catalog/mod.rs | 14 +- rs/hang/src/catalog/root.rs | 161 +++++---- rs/hang/src/catalog/text/mod.rs | 17 - rs/hang/src/catalog/timeline.rs | 117 ------ rs/hang/src/error.rs | 2 +- rs/libmoq/src/consume.rs | 3 +- rs/libmoq/src/publish.rs | 5 +- rs/libmoq/src/video.rs | 6 +- rs/moq-audio/src/decode/consumer.rs | 2 +- rs/moq-audio/src/encode/capture.rs | 4 +- rs/moq-audio/src/encode/producer.rs | 22 +- rs/moq-audio/tests/roundtrip.rs | 6 +- rs/moq-boy/src/main.rs | 2 +- rs/moq-cli/src/complete.rs | 3 +- rs/moq-cli/src/hls.rs | 2 +- rs/moq-cli/src/publish.rs | 18 +- rs/moq-ffi/src/media.rs | 3 +- rs/moq-ffi/src/producer.rs | 5 +- rs/moq-gst/src/sink/pad.rs | 2 +- rs/moq-gst/src/sink/session.rs | 2 +- rs/moq-gst/src/source/imp.rs | 12 +- rs/moq-hls/src/export/mod.rs | 48 ++- rs/moq-hls/src/export/rendition.rs | 11 +- rs/moq-hls/src/export/renditions.rs | 2 +- rs/moq-hls/src/import.rs | 10 +- rs/moq-hls/src/server/routes.rs | 5 +- rs/moq-mux/src/binary.rs | 5 +- rs/moq-mux/src/catalog/entry.rs | 7 +- rs/moq-mux/src/catalog/hang/ext.rs | 178 +-------- rs/moq-mux/src/catalog/hang/mod.rs | 3 +- rs/moq-mux/src/catalog/msf/consumer.rs | 6 +- rs/moq-mux/src/catalog/producer.rs | 99 ++--- rs/moq-mux/src/catalog/tracks.rs | 49 +-- rs/moq-mux/src/clock.rs | 100 ++---- rs/moq-mux/src/codec/h264/import.rs | 2 +- rs/moq-mux/src/codec/h265/import.rs | 2 +- rs/moq-mux/src/codec/opus/import.rs | 2 +- rs/moq-mux/src/codec/video.rs | 2 +- rs/moq-mux/src/codec/vp8/import.rs | 2 +- rs/moq-mux/src/codec/vp9/import.rs | 2 +- rs/moq-mux/src/container/flv/export_test.rs | 36 +- rs/moq-mux/src/container/flv/import_test.rs | 34 +- rs/moq-mux/src/container/fmp4/export_test.rs | 4 +- rs/moq-mux/src/container/fmp4/import_test.rs | 34 +- rs/moq-mux/src/container/mkv/export_test.rs | 20 +- rs/moq-mux/src/container/mkv/import_test.rs | 8 +- rs/moq-mux/src/container/test_util.rs | 2 +- rs/moq-mux/src/container/ts/catalog.rs | 19 +- rs/moq-mux/src/container/ts/export.rs | 2 +- rs/moq-mux/src/container/ts/export_test.rs | 358 +++++++++++-------- rs/moq-mux/src/container/ts/import.rs | 138 ++++--- rs/moq-mux/src/container/ts/import_test.rs | 20 +- rs/moq-mux/src/container/ts/si.rs | 6 +- rs/moq-mux/src/import/container.rs | 2 +- rs/moq-mux/src/import/track.rs | 2 +- rs/moq-mux/src/json.rs | 25 +- rs/moq-mux/src/timeline.rs | 16 +- rs/moq-rtc/src/codec/bitstream_test.rs | 12 +- rs/moq-rtc/src/codec/vp8.rs | 4 +- rs/moq-rtc/src/codec/vp9.rs | 2 +- rs/moq-rtc/src/ingest.rs | 2 +- rs/moq-rtc/src/lib.rs | 3 +- rs/moq-rtmp/src/dial.rs | 4 +- rs/moq-rtmp/src/server.rs | 8 +- rs/moq-srt/src/server.rs | 4 +- rs/moq-srt/src/ts.rs | 9 +- rs/moq-transcode/src/catalog.rs | 2 +- rs/moq-transcode/src/lib.rs | 8 +- rs/moq-video/src/decode/consumer.rs | 5 +- rs/moq-video/src/encode/producer.rs | 12 +- 95 files changed, 960 insertions(+), 1274 deletions(-) delete mode 100644 js/hang/src/catalog/timeline.ts rename js/hang/src/{container => }/timeline.test.ts (97%) rename js/hang/src/{container => }/timeline.ts (99%) delete mode 100644 quest/m1/api-hang-catalog.md delete mode 100644 rs/hang/src/catalog/timeline.rs diff --git a/doc/concept/hang.md b/doc/concept/hang.md index ab7ff742ad..120f900320 100644 --- a/doc/concept/hang.md +++ b/doc/concept/hang.md @@ -100,16 +100,16 @@ document would silently discard everything but the last payload: The rest is descriptive: `compression` (`deflate`, the same group-scoped `deflate-raw` the catalog uses), `schema` on a JSON track, `mime` on a binary -one, plus the `broadcast` and `timeline` fields a media rendition takes. A +one, plus the optional `broadcast` reference. A consumer that doesn't recognize a `mode` or `compression` ignores that track and round-trips it verbatim. In Rust the catalog owns the lifetime: `catalog.json_stream(track, config)` (or `json_snapshot` / `binary_snapshot` / `binary_stream`) writes the entry and -retracts it when the producer drops, and `catalog.json_track(name)` returns an -entry that subscribes itself. In the browser, read the entry from -`catalog.json.tracks`, subscribe by name, and hand the track to `@moq/json` or -`@moq/binary`. +retracts it when the producer drops. Read the config from `catalog.json.tracks` +or `catalog.binary.tracks`, then pair its name and config with +`moq_mux::catalog::Entry::new` to subscribe. In the browser, read the same map, +subscribe by name, and hand the track to `@moq/json` or `@moq/binary`. ## Container diff --git a/drafts/draft-lcurley-moq-hang.md b/drafts/draft-lcurley-moq-hang.md index 5b083a159b..6a96aca4a3 100644 --- a/drafts/draft-lcurley-moq-hang.md +++ b/drafts/draft-lcurley-moq-hang.md @@ -388,7 +388,6 @@ type JsonSchema = { "compression": Compression | undefined, "schema": string | undefined, "broadcast": string | undefined, - "timeline": TimelineSchema | undefined, } ~~~ @@ -402,7 +401,6 @@ type BinarySchema = { "compression": Compression | undefined, "mime": string | undefined, "broadcast": string | undefined, - "timeline": TimelineSchema | undefined, } ~~~ @@ -455,9 +453,8 @@ A consumer MUST ignore a track whose `compression` it does not recognize, since The `deflate` value is the group-scoped DEFLATE of {{compression}}. A `snapshot` group covers a single value (plus any deltas), so its window spans that group alone; a `stream` group's frames compress against the earlier ones in the log. -### broadcast and timeline {#data-shared} +### broadcast {#data-shared} The `broadcast` field carries the same meaning here as it does for a media rendition ({{field-broadcast}}). -The `timeline` field advertises a companion timeline track indexing this track's groups, with the same `track` / `timescale` / `durationMax` fields as the catalog's root `archive` entry ({{archive-catalog}}). ## Binary Fields {#binary} A decoder config field carrying raw bytes, notably `description` (an `AllowSharedBufferSource` in WebCodecs), is carried in the catalog as a hex string ({{!RFC4648, Section 8}}). @@ -619,12 +616,6 @@ A broadcast that does not need aligned segments simply omits it. The catalog's root `archive` field is the one name for the segment index, and for any durable recording of those ranges: ~~~ -type TimelineSchema = { - "track": string, - "timescale": number | undefined, - "durationMax": number | undefined, -} - type ArchiveSchema = { "track": string, "timescale": number | undefined, diff --git a/js/hang/src/catalog/archive.ts b/js/hang/src/catalog/archive.ts index f4d111c123..c73e810ba3 100644 --- a/js/hang/src/catalog/archive.ts +++ b/js/hang/src/catalog/archive.ts @@ -1,7 +1,12 @@ import * as z from "@zod/mini"; -import { u53Schema } from "./integers"; +import { nonzeroU53Schema, u53, u53Schema } from "./integers"; import { RelativeBroadcastSchema } from "./path"; -import { TimelineSchema } from "./timeline"; + +/** The moq epoch (2020-01-01T00:00:00Z) in Unix-epoch milliseconds. */ +export const MOQ_EPOCH_UNIX_MILLIS = 1_577_836_800_000; + +/** A nonzero Rust `u32`, used by the archive timescale. */ +const timescaleSchema = nonzeroU53Schema.check(z.lte(4_294_967_295)); /** * The recording object format advertised in {@link Archive.version} when a store is present. @@ -17,7 +22,16 @@ export const ARCHIVE_VERSION = 1; * advertised range is FETCHable; with a store they are durable. There is no sibling * `timeline` entry. Wall-clock mapping lives at the catalog root (`clock`), not here. */ -export const ArchiveSchema = z.extend(TimelineSchema, { +export const ArchiveSchema = z.object({ + // The name of the MoQ track carrying the broadcast's segment records. + track: z.string(), + + // Units per second for the records' timestamps. Defaults to milliseconds. + timescale: z._default(timescaleSchema, u53(1000)), + + // The declared upper bound on a segment's duration in timescale units. + durationMax: z.optional(u53Schema), + // The MoQ broadcast the archive is served back from, relative to this catalog, if any. // Absent when the timeline lives on this broadcast. A wildcard replay path names no // generation: compare this path and `store` to tell recordings apart. diff --git a/js/hang/src/catalog/binary.ts b/js/hang/src/catalog/binary.ts index 24ff7754f2..2c209e1413 100644 --- a/js/hang/src/catalog/binary.ts +++ b/js/hang/src/catalog/binary.ts @@ -2,7 +2,6 @@ import * as z from "zod/mini"; import { CompressionSchema } from "./compression"; import { ModeSchema } from "./mode"; import { RelativeBroadcastSchema } from "./path"; -import { TimelineSchema } from "./timeline"; /** * Schema for a single binary track: application data published as opaque payloads. @@ -29,9 +28,6 @@ export const BinaryConfigSchema = z.looseObject({ // An optional media type for each payload (e.g. "image/jpeg"). Purely descriptive: // a consumer that doesn't recognize it can still read the track. mime: z.optional(z.string()), - - // The companion timeline track indexing this track's groups, if the publisher offers one. - timeline: z.optional(TimelineSchema), }); /** diff --git a/js/hang/src/catalog/clock.ts b/js/hang/src/catalog/clock.ts index ba2125b9f8..12b8adcce1 100644 --- a/js/hang/src/catalog/clock.ts +++ b/js/hang/src/catalog/clock.ts @@ -1,6 +1,6 @@ import * as z from "@zod/mini"; +import { MOQ_EPOCH_UNIX_MILLIS } from "./archive"; import { u53Schema } from "./integers"; -import { MOQ_EPOCH_UNIX_MILLIS } from "./timeline"; /** Units per second for a catalog clock. Matches Rust `u32`; zero is refused. */ const clockTimescaleSchema = z.number().check(z.int(), z.positive(), z.lte(4_294_967_295)); diff --git a/js/hang/src/catalog/index.ts b/js/hang/src/catalog/index.ts index e5da47739c..ceb52fd14f 100644 --- a/js/hang/src/catalog/index.ts +++ b/js/hang/src/catalog/index.ts @@ -22,6 +22,5 @@ export * from "./priority"; export * from "./root"; export * as Stalled from "./stalled"; export * from "./text"; -export * from "./timeline"; export * from "./track"; export * from "./video"; diff --git a/js/hang/src/catalog/json.ts b/js/hang/src/catalog/json.ts index c97ab21bfd..6ee22e78a0 100644 --- a/js/hang/src/catalog/json.ts +++ b/js/hang/src/catalog/json.ts @@ -2,7 +2,6 @@ import * as z from "zod/mini"; import { CompressionSchema } from "./compression"; import { ModeSchema } from "./mode"; import { RelativeBroadcastSchema } from "./path"; -import { TimelineSchema } from "./timeline"; /** * Schema for a single JSON track: application data published as a live JSON document or log. @@ -28,9 +27,6 @@ export const JsonConfigSchema = z.looseObject({ // An optional identifier for the shape of each value, typically a JSON Schema URL. // Purely descriptive: a consumer that doesn't recognize it can still read the track. schema: z.optional(z.string()), - - // The companion timeline track indexing this track's groups, if the publisher offers one. - timeline: z.optional(TimelineSchema), }); /** diff --git a/js/hang/src/catalog/timeline.ts b/js/hang/src/catalog/timeline.ts deleted file mode 100644 index 47b3d7dc79..0000000000 --- a/js/hang/src/catalog/timeline.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as z from "@zod/mini"; -import { nonzeroU53Schema, u53, u53Schema } from "./integers"; - -/** - * The moq epoch (2020-01-01T00:00:00Z) in Unix-epoch milliseconds. - * - * Broadcast {@link Clock} wall values are measured from here rather than the Unix epoch so the - * numbers stay small (safely within a 53-bit integer even at fine timescales); a consumer recovers - * Unix time by adding this back. - */ -export const MOQ_EPOCH_UNIX_MILLIS = 1_577_836_800_000; - -/** - * Describes the broadcast's timeline track: its segment index, one record per aligned segment - * mapping a span of content time to the group ranges that carry it on each media track, so a - * consumer can seek (or build an HLS/DASH playlist) without downloading the media itself. - * - * Lives inside the catalog's root {@link Archive} entry: there is one timeline per broadcast, - * because its whole point is that segments are aligned across the broadcast's tracks. A - * publisher that doesn't segment simply omits the archive entry. Wall-clock mapping is the - * catalog root {@link Clock}'s job, not this section's: every track and this index refer to - * that one mapping after timescale conversion. - */ -export const TimelineSchema = z.object({ - // The name of the MoQ track carrying the broadcast's segment records. - track: z.string(), - - // Units per second for the records' `pts`. Defaults to 1000 (milliseconds). Zero is - // refused: no timestamp can be expressed in it. - timescale: z._default(nonzeroU53Schema, u53(1000)), - - // The declared upper bound on a segment's duration, in `timescale` units, when the - // publisher can promise one. A publisher that controls its encoder knows its keyframe - // cadence up front, so a consumer can size buffers or write an HLS EXT-X-TARGETDURATION - // from the catalog alone, before observing a single segment; no record ever exceeds it. - // Absent when the media decides instead (real-time, where a GOP can be minutes long, or an - // import of a source the publisher doesn't control), and a consumer needing a bound then - // derives one from the records it has seen. - durationMax: z.optional(u53Schema), -}); - -/** A media track's companion timeline description. */ -export type Timeline = z.infer; diff --git a/js/hang/src/container/index.ts b/js/hang/src/container/index.ts index 93a3c748c6..5ef19f7f7a 100644 --- a/js/hang/src/container/index.ts +++ b/js/hang/src/container/index.ts @@ -10,6 +10,5 @@ export * as Cmaf from "./cmaf"; export { Consumer, type ConsumerProps } from "./consumer"; export type { Format } from "./format"; export * as Legacy from "./legacy"; -export * as Timeline from "./timeline"; export { type TrackInfoOptions, trackInfo } from "./track"; export * from "./types"; diff --git a/js/hang/src/container/legacy.ts b/js/hang/src/container/legacy.ts index b81de8b4b9..cf8647fddf 100644 --- a/js/hang/src/container/legacy.ts +++ b/js/hang/src/container/legacy.ts @@ -4,8 +4,8 @@ import { Time } from "@moq/net"; export type { BufferedRange, BufferedRanges, Frame } from "./types"; import type { AudioConfig, VideoConfig } from "../catalog"; +import type { Recorder as TimelineRecorder } from "../timeline"; import type { Format as ContainerFormat } from "./format"; -import type { Recorder as TimelineRecorder } from "./timeline"; import type { Frame } from "./types"; /** The legacy hang container: a microsecond timestamp varint followed by the raw codec payload. */ diff --git a/js/hang/src/index.ts b/js/hang/src/index.ts index b042ffee5d..83a4850789 100644 --- a/js/hang/src/index.ts +++ b/js/hang/src/index.ts @@ -9,3 +9,4 @@ export * as Net from "@moq/net"; export * as Signals from "@moq/signals"; export * as Catalog from "./catalog"; export * as Container from "./container"; +export * as Timeline from "./timeline"; diff --git a/js/hang/src/container/timeline.test.ts b/js/hang/src/timeline.test.ts similarity index 97% rename from js/hang/src/container/timeline.test.ts rename to js/hang/src/timeline.test.ts index cdc08fd5c3..995f7857e6 100644 --- a/js/hang/src/container/timeline.test.ts +++ b/js/hang/src/timeline.test.ts @@ -1,8 +1,7 @@ import { expect, test } from "bun:test"; import * as Json from "@moq/json"; -import type { Time } from "@moq/net"; -import { Track } from "@moq/net"; -import { u53 } from "../catalog"; +import { Time, Track } from "@moq/net"; +import { u53 } from "./catalog"; import { Producer, type Record } from "./timeline.ts"; const us = (ms: number): Time.Micro => (ms * 1000) as Time.Micro; @@ -105,7 +104,7 @@ test("a GOP longer than the minimum is one segment", async () => { // Groups shorter than the minimum pack into one segment rather than each becoming one. test("short groups pack up to the minimum", async () => { - const { timeline, records } = capture({ durationMin: 1500 }); + const { timeline, records } = capture({ durationMin: Time.Milli(1500) }); const audio = timeline.pacingTrack("audio0"); for (let seq = 0; seq < 8; seq++) { @@ -191,7 +190,7 @@ test("a cut below the minimum is ignored", async () => { // segment that breaks it fails the timeline rather than publishing a record that contradicts // the catalog. test("exceeding the declared maximum fails the timeline", async () => { - const { timeline, records } = capture({ durationMin: 1000, durationMax: 3000 }); + const { timeline, records } = capture({ durationMin: Time.Milli(1000), durationMax: Time.Milli(3000) }); const video = timeline.pacingTrack("video0"); video.record(0, us(0)); @@ -211,7 +210,7 @@ test("exceeding the declared maximum fails the timeline", async () => { test("an undeclared maximum is omitted from the catalog", () => { expect(capture().timeline.section().durationMax).toBeUndefined(); - expect(capture({ durationMax: 2500 }).timeline.section().durationMax).toBe(u53(2500)); + expect(capture({ durationMax: Time.Milli(2500) }).timeline.section().durationMax).toBe(u53(2500)); }); // The last group of a broadcast has no successor to bound it, so without a reported end the diff --git a/js/hang/src/container/timeline.ts b/js/hang/src/timeline.ts similarity index 99% rename from js/hang/src/container/timeline.ts rename to js/hang/src/timeline.ts index 9ca9749ac7..98ff82deb2 100644 --- a/js/hang/src/container/timeline.ts +++ b/js/hang/src/timeline.ts @@ -17,8 +17,8 @@ import * as Json from "@moq/json"; import type * as Moq from "@moq/net"; import type { Time } from "@moq/net"; -import type * as Catalog from "../catalog"; -import { u53 } from "../catalog"; +import type * as Catalog from "./catalog"; +import { u53 } from "./catalog"; /** * A contiguous run of groups a track contributes to a segment, `start` through `end` @@ -55,7 +55,7 @@ export const DEFAULT_TIMESCALE = 1000; * The conventional {@link ProducerProps.durationMin} (1 second), in milliseconds, for callers * with no opinion of their own. */ -export const DEFAULT_DURATION_MIN_MS = 1000; +export const DEFAULT_DURATION_MIN_MS = 1000 as Time.Milli; /** Recent segment records repeated when the Window track rolls to a new group. */ const CHECKPOINT_RECORDS = 256; @@ -80,7 +80,7 @@ export interface ProducerProps { * always satisfiable (wait longer), while a ceiling is not. Defaults to * {@link DEFAULT_DURATION_MIN_MS}. */ - durationMin?: number; + durationMin?: Time.Milli; /** * The longest a segment may be, in milliseconds of media time, advertised in the catalog @@ -93,7 +93,7 @@ export interface ProducerProps { * it unset when the media decides, which is the common case for real-time and for anything * importing a source the publisher doesn't control. */ - durationMax?: number; + durationMax?: Time.Milli; } /** One enrolled track's report state. */ diff --git a/quest/m1/README.md b/quest/m1/README.md index 55a715c516..42797c9fa9 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -32,7 +32,6 @@ the transport line in m2 assumes a single stack. - [Bindings announce match](/quest/m1/api-origin-scopes.md) - every binding takes a pattern scope and reports the announce match with its captures - [PathPrefixes](/quest/m1/api-path-prefixes.md) - the unused moq_net::PathPrefixes type is deleted before the release - [Route cost](/quest/m1/api-route-cost.md) - `Route::with_hop` and `Cost: From<(u64, u64)>` go; ffi and libmoq build `Hops` and `Cost::from_warm_cold` -- [Catalog types](/quest/m1/api-hang-catalog.md) - `hang::Catalog` is the one section list, `Clock` holds a `Timestamp`, `Timeline` folds into `Archive` - [Rendition ownership](/quest/m1/api-mux-rendition.md) - one handle publishes a media track and reports its estimate, instead of five - [Gateway types](/quest/m1/api-gateways.md) - no `anyhow` in a gateway `Error`, `PathOwned` prefixes, `Duration` segments, `moq_rtc::Server::new(config)`, an SRT reject with a reason - [libmoq units](/quest/m1/api-libmoq-units.md) - `moq_client_config` is all microseconds, the header declares every enum and error code, NULL callbacks are refused diff --git a/quest/m1/api-hang-catalog.md b/quest/m1/api-hang-catalog.md deleted file mode 100644 index 95c3e2d746..0000000000 --- a/quest/m1/api-hang-catalog.md +++ /dev/null @@ -1,50 +0,0 @@ -# [M] The catalog types carry moq-net's types, once - -## Goal - -`hang::Catalog` is the one struct that lists the catalog's sections, its -clock is a `Timestamp`, its archive is one record, and every section is -read through the same `Section` trait. No consumer unpacks a timestamp into -`(u64, u32)` or derefs a plain struct to reach a field. - -## Plan - -- `hang::Catalog` gains `#[serde(flatten)] pub ext: E` (the way - `timeline::Record` already works) and `moq_mux::catalog::hang::Catalog` - is deleted. That copy re-lists every base field with the same serde - attributes and grew from two fields to seven on dev; every new section is - a three-place edit today, the third being `js/hang/src/catalog/root.ts`. -- `catalog::Clock { wall: Timestamp }` with serde emitting `{ wall, - timescale }`; `Clock::new(wall)` refuses `> 2^53 - 1` and a zero scale; - `wall_clock(&self, pts: Timestamp)`. Delete `with_timescale`, - `default_timescale`, and `deserialize_wall` from the surface. Both real - callers (`rs/moq-mux/src/clock.rs`, `rs/moq-hls/src/export/rendition.rs`) - hold a `Timestamp` and unpack it. -- Fold `catalog::Timeline` into `Archive` and drop the `Deref`/`DerefMut`/ - `From`; after the dead per-track field goes it appears nowhere - else. `moq_mux::timeline::Consumer::subscribe` takes `&Archive`. JS - `ArchiveSchema` stops extending `TimelineSchema`. -- `impl Section for Text { const MAP = "renditions"; }` and delete - `deserialize_text`. -- `moq_mux::Clock`: `new()` and `at(epoch, wall)` replace three - constructors; `now() -> Timestamp` replaces `micros() -> u64`; `wall()` - returns the catalog `Clock`. -- `catalog::Producer::new(broadcast, Config)` is the one constructor; - `with_catalog` duplicates `Config::with_catalog` and `with_timeline` - (no caller) re-takes the broadcast and silently replaces an enrolled - timeline. -- JS parity in the same pass: the archive `timescale` bound matches Rust's - u32; the timeline `durationMin`/`durationMax` props are `Time.Milli`; - `Catalog.TRACK` and `Catalog::DEFAULT_NAME` agree on a name; the JS - timeline moves from `Container.Timeline` (it is not a container) to a - `Hang.Timeline` namespace, matching the Rust split between `hang::timeline` - records and `moq_mux::timeline` machinery. - -Public API: breaking on hang, @moq/hang, moq-mux, and moq-hls, so on dev. -Wire: none (serde shapes are unchanged). Consumers: moq-mux, moq-hls, -moq-cli, moq.pro's overlay and recorder. - -## Related - -- [Rendition ownership](/quest/m1/api-mux-rendition.md) - the producer-side reshape on the same crate -- [Catalog consumer](/quest/m2/hang-catalog-consumer.md) - the reader both languages lack diff --git a/quest/m1/api-mux-rendition.md b/quest/m1/api-mux-rendition.md index d7de0ea0a3..40731e8409 100644 --- a/quest/m1/api-mux-rendition.md +++ b/quest/m1/api-mux-rendition.md @@ -30,10 +30,6 @@ Public API: breaking on moq-mux and the crates that publish through it (moq-audio, moq-video, moq-ffi, libmoq, moq-cli, moq-gst, the gateways), so on dev. Wire: none. Consumers here plus moq.pro's recorder and overlay. -## Required - -- [Catalog types](/quest/m1/api-hang-catalog.md) - the producer constructors and clock this builds on - ## Related - [Rendition ownership #2869](/quest/m1/merge-dev.md) - the earlier decision that made `Rendition` the sole catalog writer, which this keeps diff --git a/quest/m1/api-review-gate.md b/quest/m1/api-review-gate.md index 1374eab9e3..83b5859af8 100644 --- a/quest/m1/api-review-gate.md +++ b/quest/m1/api-review-gate.md @@ -19,7 +19,6 @@ quest is deleted too. No code. The list: [Announce event](/quest/m1/api-net-announce.md), [Origin scoping](/quest/m1/api-net-origin.md), [Route cost](/quest/m1/api-route-cost.md), -[Catalog types](/quest/m1/api-hang-catalog.md), [Rendition ownership](/quest/m1/api-mux-rendition.md), [Gateway types](/quest/m1/api-gateways.md), [libmoq units](/quest/m1/api-libmoq-units.md). diff --git a/quest/m2/hang-catalog-consumer.md b/quest/m2/hang-catalog-consumer.md index 6339bdd8c6..7a1cf8d913 100644 --- a/quest/m2/hang-catalog-consumer.md +++ b/quest/m2/hang-catalog-consumer.md @@ -13,8 +13,7 @@ name and runs its own read loop. - `hang::Catalog::subscribe(&broadcast::Consumer) -> catalog::Consumer` in Rust; `Catalog.watch(broadcast): AsyncIterable` in `@moq/hang`. -- JS `Hang.Timeline` (moved there by [catalog types](/quest/m1/api-hang-catalog.md)) - gains a `Consumer` yielding `{ push } | { pop } | { skip }` to mirror the +- JS `Hang.Timeline` gains a `Consumer` yielding `{ push } | { pop } | { skip }` to mirror the Rust `Event`; nothing in JS reads `archive`, `clock`, or `wallClockTime` today. - `hang::container::MAX_AGE` is public; moq.pro's fleet config cites it by @@ -28,7 +27,6 @@ Public API: additive on hang, @moq/hang, @moq/json. Wire: none. ## Required -- [Catalog types](/quest/m1/api-hang-catalog.md) - the types the reader yields - [Merge dev](/quest/m1/merge-dev.md) - starts on main ## Related diff --git a/rs/hang/examples/video.rs b/rs/hang/examples/video.rs index ed05a8a0fb..e6a0c83cc8 100644 --- a/rs/hang/examples/video.rs +++ b/rs/hang/examples/video.rs @@ -61,7 +61,7 @@ fn create_track(broadcast: &mut moq_net::broadcast::Producer) -> anyhow::Result< // Create the catalog describing our video track. // Multiple renditions allow the viewer to choose based on their capabilities. - let mut catalog = hang::catalog::Catalog::default(); + let mut catalog = hang::catalog::Catalog::<()>::default(); catalog.video.insert(video_track, video_config)?; // Publish the catalog as a "catalog.json" track in the broadcast. diff --git a/rs/hang/src/catalog/archive.rs b/rs/hang/src/catalog/archive.rs index 44501781a9..e3c0135c26 100644 --- a/rs/hang/src/catalog/archive.rs +++ b/rs/hang/src/catalog/archive.rs @@ -1,71 +1,59 @@ -use std::ops::{Deref, DerefMut}; - use serde::{Deserialize, Serialize}; -use super::Timeline; +/// The moq epoch (2020-01-01T00:00:00Z) in Unix-epoch milliseconds. +pub const MOQ_EPOCH_UNIX_MILLIS: u64 = 1_577_836_800_000; /// Discovers the broadcast's segment index and any durable archive. /// -/// This is the catalog's one name for the segment index: a live publisher sets -/// the flattened [`timeline`](Self::timeline) fields alone, and a recording also -/// names the replay broadcast and object store those ranges live under. Every -/// advertised range is FETCHable; with a store they are durable. There is no -/// sibling `timeline` entry. +/// A live publisher sets the timeline fields (`track`, `timescale`, `duration_max`) alone, and a +/// recording also names the replay broadcast and object store those ranges live under. Every +/// advertised range is FETCHable; with a store they are durable. #[serde_with::skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct Archive { - /// The timeline track: its name, timescale, and optional duration bound. Flattened on - /// the wire so a live publisher's `archive` is those fields alone. Wall-clock mapping lives - /// at the catalog root ([`Clock`](super::Clock)), not here: there is one broadcast clock, and - /// this index refers to it after timescale conversion. - #[serde(flatten)] - pub timeline: Timeline, - - /// The MoQ broadcast the archive is served back from, relative to this catalog, if - /// any. - /// - /// Absent when the timeline lives on this broadcast. A wildcard replay path names - /// no generation: compare this path and [`store`](Self::store) to tell recordings - /// apart. Authorization is external. + /// The MoQ track carrying the broadcast's segment records. + pub track: String, + + /// Units per second for the timeline's timestamps. Defaults to milliseconds. + #[serde( + default = "Archive::default_timescale", + deserialize_with = "super::deserialize_timescale_or_default" + )] + pub timescale: u32, + + /// The declared upper bound on a segment's duration, in [`timescale`](Self::timescale) units. + pub duration_max: Option, + + /// The MoQ broadcast the archive is served back from, relative to this catalog, if any. #[serde(default)] pub replay: Option, - /// The object-store URL the recording objects live under, if the publisher exposes - /// one. Authorization is external, so a managed store and a customer-owned one share - /// this field. + /// The object-store URL the recording objects live under, if exposed by the publisher. #[serde(default)] pub store: Option, - /// The recording object format version, [`VERSION`](Self::VERSION) when this crate - /// writes objects. Absent when there is no store. + /// The recording object format version, [`VERSION`](Self::VERSION) when a store is present. #[serde(default)] pub version: Option, } impl Archive { - /// The recording object format advertised in [`version`](Self::version) when a store - /// is present. + /// The recording object format advertised in [`version`](Self::version). pub const VERSION: u32 = 1; - /// An archive naming `track` as its timeline, with no replay path, store, or format - /// version. Set [`replay`](Self::replay) / [`store`](Self::store) / - /// [`version`](Self::version) afterward. - pub fn new(track: impl Into) -> Self { - Self { - timeline: Timeline::new(track), - replay: None, - store: None, - version: None, - } + /// The default timeline timescale, milliseconds. + pub const fn default_timescale() -> u32 { + 1000 } -} -impl From for Archive { - fn from(timeline: Timeline) -> Self { + /// An archive naming `track` as its timeline, with no duration bound or durable storage. + pub fn new(track: impl Into) -> Self { Self { - timeline, + track: track.into(), + timescale: Self::default_timescale(), + duration_max: None, replay: None, store: None, version: None, @@ -73,20 +61,6 @@ impl From for Archive { } } -impl Deref for Archive { - type Target = Timeline; - - fn deref(&self) -> &Timeline { - &self.timeline - } -} - -impl DerefMut for Archive { - fn deref_mut(&mut self) -> &mut Timeline { - &mut self.timeline - } -} - #[cfg(test)] mod test { use super::*; @@ -116,6 +90,12 @@ mod test { assert_eq!(serde_json::from_str::(&json).unwrap(), archive); } + #[test] + fn zero_timescale_is_refused() { + serde_json::from_str::(r#"{"track":"timeline.z","timescale":0}"#) + .expect_err("a zero timescale must not decode"); + } + #[test] fn invalid_store_url_is_refused() { serde_json::from_str::(r#"{"track":"timeline.z","store":"not a url"}"#) diff --git a/rs/hang/src/catalog/binary.rs b/rs/hang/src/catalog/binary.rs index 0ab44d2bea..13ad085f77 100644 --- a/rs/hang/src/catalog/binary.rs +++ b/rs/hang/src/catalog/binary.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, btree_map}; use serde::{Deserialize, Serialize}; -use crate::catalog::{Compression, Mode, Timeline}; +use crate::catalog::{Compression, Mode}; /// The binary tracks a broadcast publishes, keyed by track name. /// @@ -86,10 +86,6 @@ pub struct BinaryConfig { #[serde(default)] pub mime: Option, - /// The companion timeline track indexing this track's groups, if the publisher offers one. - #[serde(default)] - pub timeline: Option, - /// Fields this build doesn't recognize, kept so the entry round-trips. /// /// A future [`Mode`] or [`Compression`] almost certainly comes with fields describing it, and @@ -108,7 +104,6 @@ impl BinaryConfig { mode, compression: None, mime: None, - timeline: None, extra: Default::default(), } } diff --git a/rs/hang/src/catalog/clock.rs b/rs/hang/src/catalog/clock.rs index bfcf99b259..83584e401d 100644 --- a/rs/hang/src/catalog/clock.rs +++ b/rs/hang/src/catalog/clock.rs @@ -4,13 +4,9 @@ use super::MOQ_EPOCH_UNIX_MILLIS; use crate::Result; /// The largest integer JSON preserves exactly (2^53 - 1). -/// -/// Catalog wall values must fit here so browser consumers read the same number the publisher -/// wrote. Anything larger is refused rather than truncated. pub const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991; -/// Deserialize a wall value, refusing anything outside the JSON-safe integer range. -pub(crate) fn deserialize_wall<'de, D>(deserializer: D) -> std::result::Result +fn deserialize_wall<'de, D>(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { @@ -21,103 +17,56 @@ where Ok(value) } -/// The broadcast's one continuous clock, advertised at the catalog root. -/// -/// `wall` is the wall-clock time of PTS zero, in [`timescale`](Self::timescale) units since the -/// moq epoch ([`MOQ_EPOCH_UNIX_MILLIS`], 2020-01-01). A consumer derives the wall-clock time of -/// any media timestamp as `wall + pts` after converting that timestamp into this timescale, and -/// Unix time by adding the moq epoch back (an absolute clock for HLS -/// `EXT-X-PROGRAM-DATE-TIME` / DASH `availabilityStartTime`). -/// -/// There is one mapping per broadcast: every media track and the archive index refer to it after -/// timescale conversion, so there are no competing wall epochs. The publisher fixes it once and -/// never overwrites it: a discontinuity marker is a delivery event, not a new epoch, and a -/// system-clock adjustment never retimes it. It is independent of -/// [`Archive`](super::Archive): a live-only publisher exposes its clock without creating a -/// segment index. -/// -/// Measured from 2020 rather than 1970 so the value stays small and safely within a 53-bit -/// integer even at fine timescales. -#[serde_with::skip_serializing_none] -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +const fn default_timescale() -> u32 { + 1_000_000 +} + +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[non_exhaustive] -pub struct Clock { - /// The wall-clock time of PTS zero, in [`timescale`](Self::timescale) units since the moq - /// epoch. Must fit in a JSON-safe integer. +struct Wire { #[serde(deserialize_with = "deserialize_wall")] - pub wall: u64, - - /// Units per second for [`wall`](Self::wall). Defaults to 1,000,000 (microseconds), the - /// broadcast clock's own timescale. An omitted field takes that default; an explicit - /// null is refused. + wall: u64, #[serde( - default = "Clock::default_timescale", - deserialize_with = "deserialize_timescale_or_default" + default = "default_timescale", + deserialize_with = "super::deserialize_timescale_or_default" )] - pub timescale: u32, + timescale: u32, } -pub(crate) fn deserialize_timescale_or_default<'de, D>(deserializer: D) -> std::result::Result -where - D: Deserializer<'de>, -{ - // A missing field uses the serde `default`; an explicit null is not a number and is refused. - let value = u32::deserialize(deserializer)?; - if value == 0 { - return Err(serde::de::Error::custom("invalid timescale: 0")); - } - Ok(value) +/// The broadcast's one continuous clock, advertised at the catalog root. +/// +/// [`wall`](Self::wall) is the wall-clock time of PTS zero since the moq epoch +/// ([`MOQ_EPOCH_UNIX_MILLIS`], 2020-01-01). Its embedded timescale is serialized beside it as +/// `{ wall, timescale }`, preserving the catalog wire shape while keeping the pair inseparable in +/// Rust. Every media track and the archive index refer to this mapping after timescale conversion. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub struct Clock { + /// The wall-clock time of PTS zero since the moq epoch. + pub wall: moq_net::Timestamp, } impl Clock { - /// The default timescale (1,000,000, i.e. microseconds) for a clock section that omits the - /// field. Matches the broadcast clock's own timescale. - pub fn default_timescale() -> u32 { - 1_000_000 - } - - /// A clock section with this `wall` at the default microsecond timescale. - /// - /// Errors on a wall outside the JSON-safe integer range, since a browser would read a - /// different number than the publisher wrote. - pub fn new(wall: u64) -> Result { - Self::with_timescale(wall, Self::default_timescale()) - } - - /// A clock section with this `wall` and `timescale`. + /// Construct a catalog clock from its typed wall timestamp. /// - /// Errors on a zero timescale or a wall outside the JSON-safe integer range, rather than - /// publishing a mapping no consumer can convert or read back exactly. - pub fn with_timescale(wall: u64, timescale: u32) -> Result { - if timescale == 0 { - return Err(crate::Error::InvalidTimescale(0)); + /// Refuses a value outside the JSON-safe integer range or a timescale that cannot be represented + /// by the catalog's `u32` wire field. + pub fn new(wall: moq_net::Timestamp) -> Result { + if wall.value() > MAX_SAFE_INTEGER { + return Err(crate::Error::InvalidWall(wall.value())); } - if wall > MAX_SAFE_INTEGER { - return Err(crate::Error::InvalidWall(wall)); + if wall.scale().as_u64() > u32::MAX as u64 { + return Err(crate::Error::InvalidTimescale(wall.scale().as_u64())); } - Ok(Self { wall, timescale }) + Ok(Self { wall }) } - /// The wall-clock time of `pts`, given in `pts_timescale` units per second. - /// - /// Converts `pts` into this clock's timescale explicitly, then applies the fixed mapping - /// `wall + pts`. Refuses a zero `pts_timescale`, an out-of-range result, or a timestamp the - /// timescales cannot represent, rather than truncating. - pub fn wall_clock(&self, pts: u64, pts_timescale: u32) -> Result { - if self.timescale == 0 { - return Err(crate::Error::InvalidTimescale(0)); - } - if pts_timescale == 0 { - return Err(crate::Error::InvalidTimescale(0)); - } - let scale = moq_net::Timescale::new(self.timescale as u64)?; - let pts = moq_net::Timestamp::new(pts, moq_net::Timescale::new(pts_timescale as u64)?)?; - let units = pts.as_scale(scale); - - let total = self.wall as u128 + units; + /// The wall-clock time of `pts` under this fixed mapping. + pub fn wall_clock(&self, pts: moq_net::Timestamp) -> Result { + let scale = self.wall.scale(); + let total = self.wall.value() as u128 + pts.as_scale(scale); if total > MAX_SAFE_INTEGER as u128 { - return Err(crate::Error::InvalidWall(total as u64)); + return Err(crate::Error::InvalidWall(u64::try_from(total).unwrap_or(u64::MAX))); } let unix_millis = MOQ_EPOCH_UNIX_MILLIS as u128 + total * 1000 / scale.as_u64() as u128; @@ -127,6 +76,31 @@ impl Clock { } } +impl Serialize for Clock { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + Wire { + wall: self.wall.value(), + timescale: self.wall.scale().as_u64() as u32, + } + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Clock { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let wire = Wire::deserialize(deserializer)?; + let wall = + moq_net::Timestamp::from_scale(wire.wall, wire.timescale as u64).map_err(serde::de::Error::custom)?; + Self::new(wall).map_err(serde::de::Error::custom) + } +} + #[cfg(test)] mod test { use super::*; @@ -134,7 +108,7 @@ mod test { #[test] fn defaults_timescale_to_micros() { let decoded: Clock = serde_json::from_str(r#"{"wall":1000}"#).unwrap(); - assert_eq!(decoded.timescale, 1_000_000); + assert_eq!(decoded.wall, moq_net::Timestamp::from_micros(1000).unwrap()); assert_eq!( serde_json::to_string(&decoded).unwrap(), r#"{"wall":1000,"timescale":1000000}"# @@ -143,12 +117,9 @@ mod test { #[test] fn roundtrip() { - let clock = Clock { - wall: 175_184_640_000_000, - timescale: 1000, - }; + let clock = Clock::new(moq_net::Timestamp::from_millis(175_184_640_000).unwrap()).unwrap(); let json = serde_json::to_string(&clock).unwrap(); - assert_eq!(json, r#"{"wall":175184640000000,"timescale":1000}"#); + assert_eq!(json, r#"{"wall":175184640000,"timescale":1000}"#); assert_eq!(serde_json::from_str::(&json).unwrap(), clock); } @@ -166,26 +137,37 @@ mod test { #[test] fn wall_beyond_json_safe_integers_is_refused() { serde_json::from_str::(r#"{"wall":9007199254740992}"#).expect_err("a wall past 2^53-1 must not decode"); - assert!(Clock::new(MAX_SAFE_INTEGER + 1).is_err()); + let wall = moq_net::Timestamp::new(MAX_SAFE_INTEGER + 1, moq_net::Timescale::MICRO).unwrap(); + assert!(Clock::new(wall).is_err()); } #[test] fn wall_clock_converts_across_timescales() { - // PTS zero is the wall epoch itself. - let clock = Clock::new(1_000_000).unwrap(); + let clock = Clock::new(moq_net::Timestamp::from_micros(1_000_000).unwrap()).unwrap(); let epoch = std::time::UNIX_EPOCH + std::time::Duration::from_millis(MOQ_EPOCH_UNIX_MILLIS + 1_000); - assert_eq!(clock.wall_clock(0, 1000).unwrap(), epoch); + assert_eq!( + clock.wall_clock(moq_net::Timestamp::from_millis(0).unwrap()).unwrap(), + epoch + ); - // One media second later, whatever timescale names it. let second = std::time::UNIX_EPOCH + std::time::Duration::from_millis(MOQ_EPOCH_UNIX_MILLIS + 2_000); - assert_eq!(clock.wall_clock(1000, 1000).unwrap(), second); - assert_eq!(clock.wall_clock(48_000, 48_000).unwrap(), second); - assert_eq!(clock.wall_clock(90_000, 90_000).unwrap(), second); - } - - #[test] - fn wall_clock_refuses_a_zero_pts_timescale() { - let clock = Clock::new(0).unwrap(); - assert!(clock.wall_clock(0, 0).is_err()); + assert_eq!( + clock + .wall_clock(moq_net::Timestamp::from_millis(1000).unwrap()) + .unwrap(), + second + ); + assert_eq!( + clock + .wall_clock(moq_net::Timestamp::from_scale(48_000, 48_000).unwrap()) + .unwrap(), + second + ); + assert_eq!( + clock + .wall_clock(moq_net::Timestamp::from_scale(90_000, 90_000).unwrap()) + .unwrap(), + second + ); } } diff --git a/rs/hang/src/catalog/hex.rs b/rs/hang/src/catalog/hex.rs index 20b8faa54b..6372a01bbd 100644 --- a/rs/hang/src/catalog/hex.rs +++ b/rs/hang/src/catalog/hex.rs @@ -38,7 +38,7 @@ mod test { fn base64_description() { let json = r#"{"video":{"renditions":{"video/0":{"codec":"avc1.42e02a","container":{"kind":"legacy"},"description":"AUIAKv/hABtnQgAq"}}}}"#; - let err = Catalog::from_str(json).unwrap_err().to_string(); + let err = Catalog::<()>::from_str(json).unwrap_err().to_string(); assert!(err.contains("expected hex"), "{err}"); // Also from an already-parsed Value, which is what a consumer deserializes: moq-json @@ -52,7 +52,7 @@ mod test { #[test] fn hex_description() { let json = r#"{"video":{"renditions":{"video/0":{"codec":"avc1.42e02a","container":{"kind":"legacy"},"description":"0142002a"}}}}"#; - let catalog = Catalog::from_str(json).unwrap(); + let catalog = Catalog::<()>::from_str(json).unwrap(); let rendition = catalog.video.renditions.get("video/0").unwrap(); assert_eq!(rendition.description.as_deref(), Some(&[0x01, 0x42, 0x00, 0x2a][..])); diff --git a/rs/hang/src/catalog/json.rs b/rs/hang/src/catalog/json.rs index 5029f99dce..c0ef21794d 100644 --- a/rs/hang/src/catalog/json.rs +++ b/rs/hang/src/catalog/json.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, btree_map}; use serde::{Deserialize, Serialize}; -use crate::catalog::{Compression, Mode, Timeline}; +use crate::catalog::{Compression, Mode}; /// The JSON tracks a broadcast publishes, keyed by track name. /// @@ -84,10 +84,6 @@ pub struct JsonConfig { #[serde(default)] pub schema: Option, - /// The companion timeline track indexing this track's groups, if the publisher offers one. - #[serde(default)] - pub timeline: Option, - /// Fields this build doesn't recognize, kept so the entry round-trips. /// /// A future [`Mode`] or [`Compression`] almost certainly comes with fields describing it, and @@ -106,7 +102,6 @@ impl JsonConfig { mode, compression: None, schema: None, - timeline: None, extra: Default::default(), } } diff --git a/rs/hang/src/catalog/millis.rs b/rs/hang/src/catalog/millis.rs index d57e24ec17..8b61d1da19 100644 --- a/rs/hang/src/catalog/millis.rs +++ b/rs/hang/src/catalog/millis.rs @@ -87,7 +87,7 @@ mod test { let json = r#"{"audio":{"renditions":{"audio":{"codec":"opus","sampleRate":48000,"numberOfChannels":2,"jitter":0}}}}"#; - let catalog = Catalog::from_str(json).unwrap(); + let catalog = Catalog::<()>::from_str(json).unwrap(); assert_eq!(catalog.audio.renditions["audio"].jitter, None); // So it round-trips as an omission rather than failing to re-encode. diff --git a/rs/hang/src/catalog/mod.rs b/rs/hang/src/catalog/mod.rs index 5650ceb8b8..991d96e215 100644 --- a/rs/hang/src/catalog/mod.rs +++ b/rs/hang/src/catalog/mod.rs @@ -19,7 +19,6 @@ mod priority; mod root; pub mod stalled; mod text; -mod timeline; mod video; pub use archive::*; @@ -33,9 +32,20 @@ pub use mode::*; pub use priority::*; pub use root::*; pub use text::*; -pub use timeline::*; pub use video::*; +pub(crate) fn deserialize_timescale_or_default<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + use serde::Deserialize; + let value = u32::deserialize(deserializer)?; + if value == 0 { + return Err(serde::de::Error::custom("invalid timescale: 0")); + } + Ok(value) +} + /// A catalog section: a map of track name to config, published under one well-known root key. /// /// The associated [`MAP`](Self::MAP) is what tells a real section apart from an application's own diff --git a/rs/hang/src/catalog/root.rs b/rs/hang/src/catalog/root.rs index 09330e51db..741b273476 100644 --- a/rs/hang/src/catalog/root.rs +++ b/rs/hang/src/catalog/root.rs @@ -1,6 +1,7 @@ //! This module contains the structs and functions for the MoQ catalog format use crate::Result; use crate::catalog::{Audio, Binary, Json, PRIORITY, Text, Video}; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; /// A catalog track, created by a broadcaster to describe the tracks available in a broadcast. @@ -9,10 +10,9 @@ use serde::{Deserialize, Serialize}; /// `archive` (the segment index and any durable recording), the optional `clock` (the /// broadcast's one wall-clock mapping), and the data sections /// (`json`, `binary`) for application tracks that aren't media. -/// Applications extend it with their own root sections (e.g. `scte35`) by flattening -/// this struct into their own with `#[serde(flatten)]`. The catalog does not deny unknown fields, -/// so a base consumer ignores the extra sections and an extended catalog stays wire-compatible. -/// See the `extension_roundtrip` test. +/// Applications extend it with their own root sections (e.g. `scte35`) through `E`. The catalog +/// does not deny unknown fields, so a base consumer ignores the extra sections and an extended +/// catalog stays wire-compatible. See the `extension_roundtrip` test. /// /// Marked `#[non_exhaustive]` so a future base section can be added without bumping the major /// version. External callers start from [`Catalog::default`] and fill in the sections they @@ -22,8 +22,9 @@ use serde::{Deserialize, Serialize}; #[serde_with::skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] #[serde(default, rename_all = "camelCase")] +#[serde(bound(serialize = "E: Serialize", deserialize = "E: DeserializeOwned + Default"))] #[non_exhaustive] -pub struct Catalog { +pub struct Catalog { /// Video track information with multiple renditions. /// /// Contains a map of video track renditions that the viewer can choose from @@ -63,7 +64,7 @@ pub struct Catalog { #[serde( default, skip_serializing_if = "Text::is_empty", - deserialize_with = "crate::catalog::deserialize_text" + deserialize_with = "crate::catalog::deserialize_section" )] pub text: Text, @@ -92,9 +93,13 @@ pub struct Catalog { deserialize_with = "crate::catalog::deserialize_section" )] pub binary: Binary, + + /// Application-defined root sections, flattened alongside the base catalog sections. + #[serde(flatten)] + pub ext: E, } -impl Catalog { +impl Catalog<()> { /// The default name for the catalog track. pub const DEFAULT_NAME: &str = "catalog.json"; @@ -104,6 +109,38 @@ impl Catalog { /// both tracks; a consumer reads whichever it prefers. pub const COMPRESSED_NAME: &str = "catalog.json.z"; + /// Track properties for creating the catalog track via + /// [`create_track`](moq_net::broadcast::Producer::create_track) at + /// [`DEFAULT_NAME`](Self::DEFAULT_NAME). + /// + /// Keeps the bare `moq_net` retention rather than the media one: the catalog is + /// snapshot mode, so the useful value is the live edge, which is always kept. + pub fn default_track_info() -> moq_net::track::Info { + moq_net::track::Info::default().with_priority(PRIORITY.catalog) + } + + /// The subscription preferences used for the catalog track (high priority so + /// it preempts media tracks). + pub fn default_subscription() -> moq_net::track::Subscription { + moq_net::track::Subscription::default().with_priority(PRIORITY.catalog) + } +} + +impl Catalog { + /// Copy the media sections into a catalog with no application extension. + pub fn media(&self) -> Catalog<()> { + Catalog { + video: self.video.clone(), + audio: self.audio.clone(), + archive: self.archive.clone(), + clock: self.clock, + text: self.text.clone(), + ..Default::default() + } + } +} + +impl Catalog { /// Parse a catalog from a string. #[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Result { @@ -119,7 +156,9 @@ impl Catalog { pub fn from_reader(reader: impl std::io::Read) -> Result { Ok(serde_json::from_reader(reader)?) } +} +impl Catalog { /// Serialize the catalog to a JSON string. pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) @@ -139,22 +178,6 @@ impl Catalog { pub fn to_writer(&self, writer: impl std::io::Write) -> Result<()> { Ok(serde_json::to_writer(writer, self)?) } - - /// Track properties for creating the catalog track via - /// [`create_track`](moq_net::broadcast::Producer::create_track) at - /// [`DEFAULT_NAME`](Self::DEFAULT_NAME). - /// - /// Keeps the bare `moq_net` retention rather than the media one: the catalog is - /// snapshot mode, so the useful value is the live edge, which is always kept. - pub fn default_track_info() -> moq_net::track::Info { - moq_net::track::Info::default().with_priority(PRIORITY.catalog) - } - - /// The subscription preferences used for the catalog track (high priority so - /// it preempts media tracks). - pub fn default_subscription() -> moq_net::track::Subscription { - moq_net::track::Subscription::default().with_priority(PRIORITY.catalog) - } } #[cfg(test)] @@ -220,11 +243,11 @@ mod test { let mut audio_renditions = BTreeMap::new(); audio_renditions.insert("audio".to_string(), audio_config); - let mut decoded = Catalog::default(); + let mut decoded = Catalog::<()>::default(); decoded.video.renditions = video_renditions; decoded.audio.renditions = audio_renditions; - let output = Catalog::from_str(&encoded).expect("failed to decode"); + let output = Catalog::<()>::from_str(&encoded).expect("failed to decode"); assert_eq!(decoded, output, "wrong decoded output"); let output = decoded.to_json().expect("failed to encode"); @@ -304,11 +327,11 @@ mod test { }, ); - let mut catalog = Catalog::default(); + let mut catalog = Catalog::<()>::default(); catalog.video.renditions = video_renditions; catalog.audio.renditions = audio_renditions; - let decoded = Catalog::from_str(&encoded).expect("failed to decode"); + let decoded = Catalog::<()>::from_str(&encoded).expect("failed to decode"); assert_eq!(catalog, decoded, "decode mismatch"); let output = catalog.to_json().expect("failed to encode"); @@ -333,7 +356,7 @@ mod test { } }"#; - let parsed = Catalog::from_str(encoded).expect("failed to decode"); + let parsed = Catalog::<()>::from_str(encoded).expect("failed to decode"); let rendition = parsed.video.renditions.get("video").expect("missing rendition"); assert_eq!( rendition.broadcast.as_ref().map(|p| p.as_str()), @@ -344,7 +367,7 @@ mod test { // Full encode -> decode -> equality, so the test catches any encoder regression // (e.g. wrong key, double-emission, or `null` instead of skip). let output = parsed.to_json().expect("failed to encode"); - let reparsed = Catalog::from_str(&output).expect("failed to re-decode"); + let reparsed = Catalog::<()>::from_str(&output).expect("failed to re-decode"); assert_eq!(parsed, reparsed, "re-encoded catalog did not round-trip"); } @@ -363,7 +386,7 @@ mod test { let mut renditions = BTreeMap::new(); renditions.insert("video".to_string(), video_config); - let catalog = Catalog { + let catalog = Catalog::<()> { video: Video { renditions, ..Default::default() @@ -394,7 +417,7 @@ mod test { } }"#; - let parsed = Catalog::from_str(encoded).expect("failed to decode"); + let parsed = Catalog::<()>::from_str(encoded).expect("failed to decode"); let rendition = parsed.video.renditions.get("video").expect("missing rendition"); assert_eq!( rendition.broadcast.as_ref().map(|p| p.is_empty()), @@ -417,7 +440,7 @@ mod test { } }"#; - let parsed = Catalog::from_str(encoded).expect("failed to decode"); + let parsed = Catalog::<()>::from_str(encoded).expect("failed to decode"); let rendition = parsed.video.renditions.get("video").expect("missing rendition"); assert_eq!( rendition.broadcast.as_ref().map(|p| p.as_str()), @@ -446,7 +469,7 @@ mod test { } }"#; - let parsed = Catalog::from_str(encoded).expect("failed to decode"); + let parsed = Catalog::<()>::from_str(encoded).expect("failed to decode"); let known = parsed.video.renditions.get("legacy").expect("missing rendition"); assert_eq!(known.container, Container::Legacy); @@ -460,7 +483,7 @@ mod test { // The unknown rendition survives a republish intact. let output = parsed.to_json().expect("failed to encode"); - let reparsed = Catalog::from_str(&output).expect("failed to re-decode"); + let reparsed = Catalog::<()>::from_str(&output).expect("failed to re-decode"); assert_eq!(parsed, reparsed, "re-encoded catalog did not round-trip"); assert!(output.contains(r#""magic":7"#), "unknown fields dropped: {output}"); } @@ -469,7 +492,7 @@ mod test { fn empty_text_section_omitted() { // A catalog without captions must stay byte-identical to before the text section existed: // the empty section is skipped, unlike the always-present video/audio sections. - let catalog = Catalog::default(); + let catalog = Catalog::<()>::default(); let output = catalog.to_json().expect("failed to encode"); assert!(!output.contains("text"), "empty text section leaked: {output}"); } @@ -485,7 +508,7 @@ mod test { let mut text = Text::default(); text.insert("captions.en", config).expect("insert"); - let catalog = Catalog { + let catalog = Catalog::<()> { text, ..Default::default() }; @@ -493,7 +516,7 @@ mod test { let json = catalog.to_json().expect("failed to encode"); assert!(json.contains("\"text\""), "text section missing: {json}"); - let decoded = Catalog::from_str(&json).expect("failed to decode"); + let decoded = Catalog::<()>::from_str(&json).expect("failed to decode"); assert_eq!(catalog, decoded, "text section did not round-trip"); } @@ -508,7 +531,8 @@ mod test { r#"{"renditions":42}"#, ] { let json = format!(r#"{{"video":{{"renditions":{{}}}},"audio":{{"renditions":{{}}}},"text":{legacy}}}"#); - let catalog = Catalog::from_str(&json).unwrap_or_else(|e| panic!("legacy text {legacy} broke it: {e}")); + let catalog = + Catalog::<()>::from_str(&json).unwrap_or_else(|e| panic!("legacy text {legacy} broke it: {e}")); assert!(catalog.text.is_empty(), "legacy text {legacy} decoded as captions"); } } @@ -520,7 +544,7 @@ mod test { fn a_malformed_text_section_still_fails() { let json = r#"{"text":{"renditions":{"captions":{"container":{"kind":"legacy"}}}}}"#; assert!( - Catalog::from_str(json).is_err(), + Catalog::<()>::from_str(json).is_err(), "a format-less text rendition decoded instead of failing" ); } @@ -531,7 +555,7 @@ mod test { // playing even when a caption rendition is classified with a vocabulary we don't know yet. let json = r#"{"video":{"renditions":{}},"audio":{"renditions":{}},"text":{"renditions":{"subs":{"format":"vtt","role":"commentary"}}}}"#; - let catalog = Catalog::from_str(json).expect("unknown role rejected the catalog"); + let catalog = Catalog::<()>::from_str(json).expect("unknown role rejected the catalog"); assert_eq!(catalog.text.renditions.len(), 1); let encoded = catalog.to_json().expect("failed to encode unknown role"); assert!( @@ -548,7 +572,7 @@ mod test { for section in ["json", "binary"] { let wire = format!(r#"{{"video":{{"renditions":{{}}}},"{section}":{{"messages":"chat"}}}}"#); let catalog = - Catalog::from_str(&wire).unwrap_or_else(|err| panic!("{section} took the catalog down: {err}")); + Catalog::<()>::from_str(&wire).unwrap_or_else(|err| panic!("{section} took the catalog down: {err}")); assert!(catalog.json.is_empty(), "{section}"); assert!(catalog.binary.is_empty(), "{section}"); } @@ -562,7 +586,7 @@ mod test { for section in ["json", "binary"] { let wire = format!(r#"{{"{section}":{{"tracks":{{"chat":{{"compression":"deflate"}}}}}}}}"#); assert!( - Catalog::from_str(&wire).is_err(), + Catalog::<()>::from_str(&wire).is_err(), "a mode-less {section} track decoded instead of failing" ); } @@ -572,27 +596,31 @@ mod test { fn data_sections_stay_off_the_wire_when_empty() { // A media-only catalog must serialize exactly as it did before the data sections existed, // or every existing publisher's bytes change. - let output = Catalog::default().to_json().expect("failed to encode"); + let output = Catalog::<()>::default().to_json().expect("failed to encode"); assert_eq!(output, r#"{"video":{"renditions":{}},"audio":{"renditions":{}}}"#); } #[test] fn clock_roundtrips_at_the_root() { - let clock = crate::catalog::Clock::new(1_751_846_400_000_000).unwrap(); - let catalog = Catalog { + let clock = + crate::catalog::Clock::new(moq_net::Timestamp::from_micros(1_751_846_400_000_000).unwrap()).unwrap(); + let catalog = Catalog::<()> { clock: Some(clock), ..Default::default() }; let json = catalog.to_json().expect("failed to encode"); assert!(json.contains(r#""clock":{"wall":1751846400000000"#), "{json}"); - assert_eq!(Catalog::from_str(&json).expect("failed to decode").clock, Some(clock)); + assert_eq!( + Catalog::<()>::from_str(&json).expect("failed to decode").clock, + Some(clock) + ); } #[test] fn clock_stays_off_the_wire_when_absent() { // A catalog without a clock serializes exactly as before the section existed. - let output = Catalog::default().to_json().expect("failed to encode"); + let output = Catalog::<()>::default().to_json().expect("failed to encode"); assert_eq!(output, r#"{"video":{"renditions":{}},"audio":{"renditions":{}}}"#); } @@ -601,10 +629,10 @@ mod test { // The canonical new shape: the root clock plus an archive without a wall field. // A reader maps archive timestamps through the root clock after timescale conversion. let json = include_str!("../../fixtures/catalog-clock.json"); - let catalog = Catalog::from_str(json).expect("the packaged fixture must decode"); + let catalog = Catalog::<()>::from_str(json).expect("the packaged fixture must decode"); let clock = catalog.clock.expect("the fixture carries a root clock"); - assert_eq!(clock.timescale, 1_000_000); + assert_eq!(clock.wall.scale().as_u64(), 1_000_000); let archive = catalog.archive.expect("the fixture carries an archive"); assert_eq!(archive.track, "timeline.z"); @@ -613,7 +641,12 @@ mod test { // Archive PTS 2000 (ms) lands 2s after the wall epoch, whatever the track timescale. let expected = std::time::UNIX_EPOCH + std::time::Duration::from_millis(crate::catalog::MOQ_EPOCH_UNIX_MILLIS + 1_751_846_402_000); - assert_eq!(clock.wall_clock(2000, archive.timescale).unwrap(), expected); + assert_eq!( + clock + .wall_clock(moq_net::Timestamp::from_scale(2000, archive.timescale as u64).unwrap()) + .unwrap(), + expected + ); } #[test] @@ -623,7 +656,7 @@ mod test { archive.replay = Some(moq_net::PathRelativeOwned::new("recordings/clip")); archive.version = Some(crate::catalog::Archive::VERSION); - let catalog = Catalog { + let catalog = Catalog::<()> { archive: Some(archive.clone()), ..Default::default() }; @@ -635,7 +668,7 @@ mod test { "the old root key must not appear: {json}" ); assert_eq!( - Catalog::from_str(&json).expect("failed to decode").archive, + Catalog::<()>::from_str(&json).expect("failed to decode").archive, Some(archive) ); } @@ -644,7 +677,7 @@ mod test { fn a_legacy_root_timeline_is_not_an_archive() { // No alias or fallback: a catalog that still names `timeline` at the root has no archive. let catalog = - Catalog::from_str(r#"{"timeline":{"track":"timeline.z"}}"#).expect("legacy timeline broke decode"); + Catalog::<()>::from_str(r#"{"timeline":{"track":"timeline.z"}}"#).expect("legacy timeline broke decode"); assert_eq!(catalog.archive, None); } @@ -688,12 +721,12 @@ mod test { let mut thumbnail = BinaryConfig::new(Mode::Snapshot); thumbnail.mime = Some("image/jpeg".to_string()); - let mut catalog = Catalog::default(); + let mut catalog = Catalog::<()>::default(); catalog.json.insert("chat", chat).unwrap(); catalog.json.insert("status", status).unwrap(); catalog.binary.insert("thumbnail", thumbnail).unwrap(); - let decoded = Catalog::from_str(&encoded).expect("failed to decode"); + let decoded = Catalog::<()>::from_str(&encoded).expect("failed to decode"); assert_eq!(decoded, catalog, "decode mismatch"); let output = catalog.to_json().expect("failed to encode"); @@ -713,7 +746,7 @@ mod test { } }"#; - let parsed = Catalog::from_str(encoded).expect("failed to decode"); + let parsed = Catalog::<()>::from_str(encoded).expect("failed to decode"); let known = parsed.json.tracks.get("known").expect("missing track"); assert_eq!(known.mode, Mode::Stream); @@ -728,7 +761,7 @@ mod test { output.contains(r#""mode":"windowed""#), "unknown mode dropped: {output}" ); - let reparsed = Catalog::from_str(&output).expect("failed to re-decode"); + let reparsed = Catalog::<()>::from_str(&output).expect("failed to re-decode"); assert_eq!(parsed, reparsed, "re-encoded catalog did not round-trip"); } @@ -739,27 +772,27 @@ mod test { fn unknown_mode_fields_round_trip() { let encoded = r#"{"json":{"tracks":{"future":{"mode":"windowed","window":10}}}}"#; - let parsed = Catalog::from_str(encoded).expect("failed to decode"); + let parsed = Catalog::<()>::from_str(encoded).expect("failed to decode"); let future = parsed.json.tracks.get("future").expect("missing track"); assert_eq!(future.mode, Mode::Unknown("windowed".to_string())); assert_eq!(future.extra.get("window"), Some(&serde_json::json!(10))); let output = parsed.to_json().expect("failed to encode"); assert!(output.contains(r#""window":10"#), "unknown fields dropped: {output}"); - assert_eq!(Catalog::from_str(&output).expect("re-decode"), parsed); + assert_eq!(Catalog::<()>::from_str(&output).expect("re-decode"), parsed); } /// There is no safe default: reading a stream as a snapshot silently drops every record but the /// last, so an entry without a mode is malformed rather than assumed. #[test] fn a_track_without_a_mode_is_rejected() { - Catalog::from_str(r#"{"json":{"tracks":{"chat":{"compression":"deflate"}}}}"#) + Catalog::<()>::from_str(r#"{"json":{"tracks":{"chat":{"compression":"deflate"}}}}"#) .expect_err("a mode-less track must not decode"); } #[test] fn duplicate_data_track_names_are_rejected() { - let mut catalog = Catalog::default(); + let mut catalog = Catalog::<()>::default(); catalog.json.insert("chat", JsonConfig::new(Mode::Stream)).unwrap(); assert!(matches!( catalog.json.insert("chat", JsonConfig::new(Mode::Snapshot)), @@ -787,15 +820,15 @@ mod test { } let app = AppCatalog { - base: Catalog::default(), + base: Catalog::<()>::default(), scte35: Some(Scte35 { splice_id: 42 }), }; let json = serde_json::to_string(&app).expect("failed to encode"); // A base consumer ignores the unknown section. - let base = Catalog::from_str(&json).expect("failed to decode base"); - assert_eq!(base, Catalog::default()); + let base = Catalog::<()>::from_str(&json).expect("failed to decode base"); + assert_eq!(base, Catalog::<()>::default()); // The extended consumer round-trips its own section. let decoded: AppCatalog = serde_json::from_str(&json).expect("failed to decode app"); diff --git a/rs/hang/src/catalog/text/mod.rs b/rs/hang/src/catalog/text/mod.rs index 6353f45503..2e34d3968b 100644 --- a/rs/hang/src/catalog/text/mod.rs +++ b/rs/hang/src/catalog/text/mod.rs @@ -29,23 +29,6 @@ pub struct Text { pub renditions: BTreeMap, } -/// A `serde` `deserialize_with` helper that decodes a catalog `text` section, falling back to empty -/// when the value isn't one. -/// -/// `text` only became a reserved media section with captions; before that an application could -/// carry its own `text` key through the catalog extension mechanism. Failing the decode would take -/// the whole catalog down with it, so a section we can't read costs its captions and nothing else. -/// The JS parser does the same (`section` in `catalog/root.ts`). -/// -/// Use it on the `text` field of any catalog root that embeds this section: -/// `#[serde(default, deserialize_with = "hang::catalog::deserialize_text")]`. -pub fn deserialize_text<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - crate::catalog::deserialize_section(deserializer) -} - impl crate::catalog::Section for Text { const MAP: &'static str = "renditions"; } diff --git a/rs/hang/src/catalog/timeline.rs b/rs/hang/src/catalog/timeline.rs deleted file mode 100644 index bcb6e95146..0000000000 --- a/rs/hang/src/catalog/timeline.rs +++ /dev/null @@ -1,117 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// The moq epoch (2020-01-01T00:00:00Z) in Unix-epoch milliseconds. -/// -/// Timeline [`wall`](crate::catalog::Clock::wall) values are measured from here rather than the Unix epoch so -/// the numbers stay small (and safely within a 53-bit integer even at fine timescales); a -/// consumer recovers Unix time by adding this back. -pub const MOQ_EPOCH_UNIX_MILLIS: u64 = 1_577_836_800_000; - -/// Describes the broadcast's timeline track, its segment index. -/// -/// The timeline track carries one record per aligned segment: a span of content time shared -/// by every media track, mapped to the group ranges that carry it on each track (see the -/// [`timeline`](crate::timeline) module for the record format). A consumer can seek, or build -/// an HLS/DASH playlist, without downloading the media itself. -/// -/// The section lives inside the catalog's root [`Archive`](crate::catalog::Archive) (flattened -/// on the wire): there is one timeline per broadcast, because its whole point is that -/// segments are aligned across the broadcast's tracks. A publisher that doesn't segment -/// simply omits the archive entry. Wall-clock mapping is the catalog root -/// [`Clock`](crate::catalog::Clock)'s job, not this section's: every track and this index -/// refer to that one mapping after timescale conversion. -#[serde_with::skip_serializing_none] -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[non_exhaustive] -pub struct Timeline { - /// The name of the MoQ track carrying the broadcast's segment records - /// ([`timeline::DEFAULT_NAME`](crate::timeline::DEFAULT_NAME) by convention). - pub track: String, - - /// Units per second for the timeline's `pts`. Defaults to 1000 - /// (milliseconds). Zero is refused: no timestamp can be expressed in it. - #[serde( - default = "Timeline::default_timescale", - deserialize_with = "super::deserialize_timescale_or_default" - )] - pub timescale: u32, - - /// The declared upper bound on a segment's duration, in [`timescale`](Self::timescale) - /// units, when the publisher can promise one. - /// - /// A publisher that controls its encoder knows its keyframe cadence up front, so a consumer - /// can size buffers or write an HLS `EXT-X-TARGETDURATION` from the catalog alone, before - /// observing a single segment. No record ever exceeds it: the publisher fails the timeline - /// rather than contradict this. - /// - /// Absent when the media decides instead, which is the common case for real-time (where a - /// GOP can be minutes long) and for a publisher importing a source it doesn't control. A - /// consumer needing a bound then derives one from the records it has seen. - pub duration_max: Option, -} - -impl Timeline { - /// The default timescale (1000, i.e. milliseconds) for a timeline whose catalog section - /// omits the field. - pub fn default_timescale() -> u32 { - 1000 - } - - /// A timeline section naming `track`, at the default millisecond timescale, with no - /// declared duration bound. Set [`timescale`](Self::timescale) / - /// [`duration_max`](Self::duration_max) afterward. - pub fn new(track: impl Into) -> Self { - Self { - track: track.into(), - timescale: Self::default_timescale(), - duration_max: None, - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn defaults_timescale_to_ms() { - let json = r#"{"track":"timeline.z","durationMax":2000}"#; - let decoded: Timeline = serde_json::from_str(json).unwrap(); - assert_eq!(decoded.track, "timeline.z"); - assert_eq!(decoded.timescale, 1000); - assert_eq!(decoded.duration_max, Some(2000)); - } - - #[test] - fn duration_max_is_optional() { - let json = r#"{"track":"timeline.z"}"#; - let decoded: Timeline = serde_json::from_str(json).unwrap(); - assert_eq!(decoded.duration_max, None); - assert_eq!( - serde_json::to_string(&decoded).unwrap(), - r#"{"track":"timeline.z","timescale":1000}"# - ); - } - - #[test] - fn zero_timescale_is_refused() { - serde_json::from_str::(r#"{"track":"timeline.z","timescale":0}"#) - .expect_err("a zero timescale must not decode"); - } - - #[test] - fn explicit_null_timescale_is_refused() { - serde_json::from_str::(r#"{"track":"timeline.z","timescale":null}"#) - .expect_err("an explicit null timescale must not decode as the default"); - } - - #[test] - fn roundtrip() { - let mut timeline = Timeline::new("timeline.z"); - timeline.duration_max = Some(2000); - let json = serde_json::to_string(&timeline).unwrap(); - assert_eq!(json, r#"{"track":"timeline.z","timescale":1000,"durationMax":2000}"#); - assert_eq!(serde_json::from_str::(&json).unwrap(), timeline); - } -} diff --git a/rs/hang/src/error.rs b/rs/hang/src/error.rs index dfdfd033ea..e77ce1fdee 100644 --- a/rs/hang/src/error.rs +++ b/rs/hang/src/error.rs @@ -55,7 +55,7 @@ pub enum Error { /// A catalog timescale is zero, so no timestamp can be expressed in it. #[error("invalid timescale: {0}")] - InvalidTimescale(u32), + InvalidTimescale(u64), /// A catalog wall-clock value is outside the JSON-safe integer range. #[error("invalid wall clock: {0}")] diff --git a/rs/libmoq/src/consume.rs b/rs/libmoq/src/consume.rs index 6dd890588a..5499db2ccd 100644 --- a/rs/libmoq/src/consume.rs +++ b/rs/libmoq/src/consume.rs @@ -213,7 +213,8 @@ impl Consume { // Serialize the untyped application sections to owned strings so the // C section APIs can borrow stable pointers from the snapshot. let sections = update - .sections() + .ext + .iter() .map(|(name, value)| (name.clone(), value.to_string())) .collect(); diff --git a/rs/libmoq/src/publish.rs b/rs/libmoq/src/publish.rs index 4b4d0c2aa9..1c7f312b16 100644 --- a/rs/libmoq/src/publish.rs +++ b/rs/libmoq/src/publish.rs @@ -119,8 +119,9 @@ impl Publish { /// Store an origin-created broadcast producer, attaching the catalog track /// every libmoq broadcast carries. pub fn create(&mut self, mut broadcast: moq_net::broadcast::Producer) -> Result { - let catalog = - moq_mux::catalog::Producer::with_catalog(&mut broadcast, moq_mux::catalog::hang::Catalog::default())?; + let config = moq_mux::catalog::Config::default() + .with_catalog(moq_mux::catalog::hang::Catalog::::default()); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; let id = self.broadcasts.insert(Broadcast { producer: broadcast, diff --git a/rs/libmoq/src/video.rs b/rs/libmoq/src/video.rs index 14a38535a1..d7c87ebb43 100644 --- a/rs/libmoq/src/video.rs +++ b/rs/libmoq/src/video.rs @@ -954,9 +954,9 @@ mod tests { moq_net::track::Subscriber, ) { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = - moq_mux::catalog::Producer::with_catalog(&mut broadcast, moq_mux::catalog::hang::Catalog::default()) - .unwrap(); + let config = moq_mux::catalog::Config::default() + .with_catalog(moq_mux::catalog::hang::Catalog::::default()); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config).unwrap(); let consumer = broadcast.consume(); // Probed rather than hand-built, so the test track carries what a real one would. let rendition = moq_video::encode::Config::new(320, 240, 30).probe().await.unwrap(); diff --git a/rs/moq-audio/src/decode/consumer.rs b/rs/moq-audio/src/decode/consumer.rs index 9a4770323e..9947c304fa 100644 --- a/rs/moq-audio/src/decode/consumer.rs +++ b/rs/moq-audio/src/decode/consumer.rs @@ -483,7 +483,7 @@ mod tests { #[tokio::test] async fn remixes_mono_stream_to_stereo_output() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let subscriber = broadcast.consume(); let input = Input { format: Format::F32, diff --git a/rs/moq-audio/src/encode/capture.rs b/rs/moq-audio/src/encode/capture.rs index 9df7d82bd3..abdd817619 100644 --- a/rs/moq-audio/src/encode/capture.rs +++ b/rs/moq-audio/src/encode/capture.rs @@ -1441,7 +1441,7 @@ mod tests { ) { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut options = PublicationOptions::default(); options.capture.source = capture::Source::Microphone(Some("first".into())); options.encode.track = Some("audio".into()); @@ -1464,7 +1464,7 @@ mod tests { ) { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut options = PublicationOptions::default(); options.capture.source = capture::Source::Microphone(Some("first".into())); options.encode.track = Some("audio".into()); diff --git a/rs/moq-audio/src/encode/producer.rs b/rs/moq-audio/src/encode/producer.rs index bbd7d8c8d7..ab962f2935 100644 --- a/rs/moq-audio/src/encode/producer.rs +++ b/rs/moq-audio/src/encode/producer.rs @@ -548,7 +548,7 @@ mod tests { let decoder_config = Encoder::new(&options.config(input.clone())).unwrap().catalog(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let consumer = broadcast.consume(); let mut producer = Producer::new(&mut broadcast, catalog, input, &options).unwrap(); let mut audio = AudioConsumer::new( @@ -593,7 +593,7 @@ mod tests { }; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let consumer = broadcast.consume(); let options = Options { track: Some("audio".to_string()), @@ -640,7 +640,7 @@ mod tests { }; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let consumer = broadcast.consume(); let options = Options { track: Some("audio".to_string()), @@ -675,7 +675,7 @@ mod tests { #[tokio::test] async fn reset_epoch_drops_the_encoder_lookahead() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let consumer = broadcast.consume(); let options = Options { track: Some("audio".to_string()), @@ -724,7 +724,7 @@ mod tests { let decoder_config = Encoder::new(&options.config(input.clone())).unwrap().catalog(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let subscriber = broadcast.consume(); let mut producer = Producer::new(&mut broadcast, catalog, input, &options).unwrap(); let mut audio = AudioConsumer::new( @@ -772,7 +772,7 @@ mod tests { let decoder_config = Encoder::new(&options.config(input.clone())).unwrap().catalog(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let subscriber = broadcast.consume(); let mut producer = Producer::new(&mut broadcast, catalog, input, &options).unwrap(); let mut consumer = AudioConsumer::new(&subscriber, &decoder_config, "audio", DecodeConfig::new()) @@ -820,7 +820,7 @@ mod tests { /// frame's `write`. async fn published_pts(frames: &[Frame], reset_before: Option) -> Vec { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let consumer = broadcast.consume(); // Input rate == Opus codec rate, so there's no resampler and sample @@ -869,7 +869,7 @@ mod tests { #[tokio::test] async fn resampling_does_not_shift_the_first_pts() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let consumer = broadcast.consume(); // 44.1 kHz in, and Opus only runs at 48 kHz, so this one resamples. @@ -922,7 +922,7 @@ mod tests { #[tokio::test] async fn abort_after_finish() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let consumer = broadcast.consume(); let options = Options { track: Some("audio".to_string()), @@ -959,7 +959,7 @@ mod tests { #[tokio::test] async fn write_after_finish_is_closed() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let options = Options { track: Some("audio".to_string()), ..Options::default() @@ -987,7 +987,7 @@ mod tests { #[tokio::test] async fn default_options_derive_the_track_name() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let first = Producer::new(&mut broadcast, catalog.clone(), Input::default(), &Options::default()).unwrap(); assert_eq!(first.track_name(), "0.opus"); diff --git a/rs/moq-audio/tests/roundtrip.rs b/rs/moq-audio/tests/roundtrip.rs index 62efbfbe67..622df1edb2 100644 --- a/rs/moq-audio/tests/roundtrip.rs +++ b/rs/moq-audio/tests/roundtrip.rs @@ -32,7 +32,7 @@ fn f32_bytes(samples: &[f32]) -> Bytes { #[tokio::test] async fn opus_round_trip_48k_stereo() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut catalog_consumer = catalog.consume().unwrap(); let broadcast_consumer = broadcast.consume(); @@ -104,7 +104,7 @@ async fn opus_round_trip_48k_stereo() { #[tokio::test] async fn opus_round_trip_44100_s16_resampled() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut catalog_consumer = catalog.consume().unwrap(); let broadcast_consumer = broadcast.consume(); @@ -167,7 +167,7 @@ async fn opus_round_trip_44100_s16_resampled() { #[tokio::test] async fn pcm_round_trip_is_lossless() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut catalog_consumer = catalog.consume().unwrap(); let broadcast_consumer = broadcast.consume(); diff --git a/rs/moq-boy/src/main.rs b/rs/moq-boy/src/main.rs index ca6a938195..9ee6a32033 100644 --- a/rs/moq-boy/src/main.rs +++ b/rs/moq-boy/src/main.rs @@ -257,7 +257,7 @@ async fn run(config: &Config) -> Result<()> { .connect(url); // Set up catalog and encoders. - let catalog = moq_mux::catalog::Producer::new(&mut broadcast)?; + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default())?; let video_encoder = video::VideoEncoder::spawn(broadcast.clone(), catalog.clone()).await; let audio_encoder = audio::AudioEncoder::new(broadcast.clone(), catalog.clone(), 44100)?; diff --git a/rs/moq-cli/src/complete.rs b/rs/moq-cli/src/complete.rs index a405aae513..e926f13606 100644 --- a/rs/moq-cli/src/complete.rs +++ b/rs/moq-cli/src/complete.rs @@ -900,7 +900,8 @@ mod tests { for (path, video, audio) in [("wanted", "hd", "stereo"), ("other", "sd", "mono")] { let mut broadcast = origin.create_broadcast(path).expect("broadcast"); broadcast.announce(Default::default()).expect("broadcast"); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).expect("catalog"); + let mut catalog = + moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).expect("catalog"); let mut edit = catalog.modify().unwrap(); edit.video.renditions.insert( video.to_string(), diff --git a/rs/moq-cli/src/hls.rs b/rs/moq-cli/src/hls.rs index 754d9252f3..5d23f79a56 100644 --- a/rs/moq-cli/src/hls.rs +++ b/rs/moq-cli/src/hls.rs @@ -55,7 +55,7 @@ pub async fn import(target: ImportTarget, playlist: String) -> anyhow::Result<() let config = moq_mux::catalog::Config::default() .with_max_age(max_age) .with_bandwidth(bandwidth); - let catalog = moq_mux::catalog::Producer::with_config(&mut producer, config)?; + let catalog = moq_mux::catalog::Producer::new(&mut producer, config)?; producer .announce(Default::default()) .context("failed to announce broadcast")?; diff --git a/rs/moq-cli/src/publish.rs b/rs/moq-cli/src/publish.rs index 50e5ccb66e..8af61728be 100644 --- a/rs/moq-cli/src/publish.rs +++ b/rs/moq-cli/src/publish.rs @@ -265,7 +265,7 @@ impl Publish { // gets exactly one; TS builds its `Ext` catalog here instead of the shared `()` below. if let PublishFormat::Ts = format { let config = config.with_catalog(moq_mux::catalog::hang::Catalog::::default()); - let catalog = moq_mux::catalog::Producer::with_config(&mut broadcast, config)?; + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; let ts = ts::Import::new(broadcast.clone(), catalog.reserve()); return Ok(Self { source: Source::Stream(PublishDecoder::Ts(Box::new(ts))), @@ -273,7 +273,7 @@ impl Publish { }); } - let catalog = moq_mux::catalog::Producer::with_config(&mut broadcast, config)?; + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; let source = match format { PublishFormat::Avc3 => { let track = broadcast.unique_track(".avc3", catalog.track_info(hang::catalog::PRIORITY.video))?; @@ -315,7 +315,7 @@ impl Publish { max_age: Option, ) -> anyhow::Result { let config = moq_mux::catalog::Config::default().with_max_age(max_age); - let catalog = moq_mux::catalog::Producer::with_config(&mut broadcast, config)?; + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; let video = (!args.no_video).then(|| (args.video_config(), args.video_encode(bandwidth.clone()))); let audio = (!args.no_audio).then(|| (args.audio_config(), args.audio_encode(bandwidth))); @@ -562,8 +562,8 @@ mod tests { let mut broadcast = origin.create_broadcast("cli").unwrap(); broadcast.announce(Default::default()).unwrap(); settle().await; - let mut catalog = - moq_mux::catalog::Producer::with_catalog(&mut broadcast, Catalog::::default()).unwrap(); + let config = moq_mux::catalog::Config::default().with_catalog(Catalog::::default()); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, config).unwrap(); // Section-framed verbatim stream (SCTE-35, stream_type 0x86). let section = broadcast @@ -574,6 +574,7 @@ mod tests { catalog .modify() .unwrap() + .ext .mpegts .tracks .insert(section.name().to_string(), section_track); @@ -604,6 +605,7 @@ mod tests { catalog .modify() .unwrap() + .ext .mpegts .tracks .insert(pes.name().to_string(), pes_track); @@ -690,14 +692,15 @@ mod tests { // Re-import the round-tripped TS and inspect the recovered `mpegts` section. let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = - moq_mux::catalog::Producer::with_catalog(&mut broadcast, Catalog::::default()).unwrap(); + let config = moq_mux::catalog::Config::default().with_catalog(Catalog::::default()); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config).unwrap(); let mut import = Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&output[..])).unwrap(); import.finish().unwrap(); let snapshot = catalog.snapshot(); let (section_name, section) = snapshot + .ext .mpegts .tracks .iter() @@ -712,6 +715,7 @@ mod tests { let section_name = section_name.clone(); let (pes_name, pes) = snapshot + .ext .mpegts .tracks .iter() diff --git a/rs/moq-ffi/src/media.rs b/rs/moq-ffi/src/media.rs index 2f3f6774d7..4e7407571e 100644 --- a/rs/moq-ffi/src/media.rs +++ b/rs/moq-ffi/src/media.rs @@ -406,7 +406,8 @@ pub(crate) fn convert_catalog(catalog: &moq_mux::catalog::hang::Catalog Result { - let catalog = - moq_mux::catalog::Producer::with_catalog(&mut broadcast, moq_mux::catalog::hang::Catalog::default())?; + let config = + moq_mux::catalog::Config::default().with_catalog(moq_mux::catalog::hang::Catalog::::default()); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; Ok(Self { state: std::sync::Mutex::new(Some(BroadcastProducer { broadcast, catalog })), }) diff --git a/rs/moq-gst/src/sink/pad.rs b/rs/moq-gst/src/sink/pad.rs index 57a2779051..321569af68 100644 --- a/rs/moq-gst/src/sink/pad.rs +++ b/rs/moq-gst/src/sink/pad.rs @@ -775,7 +775,7 @@ mod tests { /// Local producers, no network: a broadcast plus its catalog, exactly what the element holds. fn producers() -> (moq_net::broadcast::Producer, moq_mux::catalog::Producer) { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); (broadcast, catalog) } diff --git a/rs/moq-gst/src/sink/session.rs b/rs/moq-gst/src/sink/session.rs index c5acd8b7b9..534a92dfb0 100644 --- a/rs/moq-gst/src/sink/session.rs +++ b/rs/moq-gst/src/sink/session.rs @@ -282,7 +282,7 @@ impl Session { let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); let mut broadcast = origin.create_broadcast(&settings.broadcast)?; broadcast.announce(moq_net::origin::Route::default())?; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast)?; + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default())?; let status = Arc::new(Status::default()); let completion = CompletionState::new(); diff --git a/rs/moq-gst/src/source/imp.rs b/rs/moq-gst/src/source/imp.rs index 04927f9ad2..9ed67cb0d9 100644 --- a/rs/moq-gst/src/source/imp.rs +++ b/rs/moq-gst/src/source/imp.rs @@ -1044,7 +1044,7 @@ mod session_tests { // A live handler is what makes an unserved name park rather than resolve `NotFound`, // which is how it behaves over the wire: the publisher just never answers. let mut dynamic = broadcast.dynamic(); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); // First update announces audio only, and no producer ever answers for it. { @@ -1087,7 +1087,7 @@ mod session_tests { let element = element(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); // Both renditions in one snapshot, so the result can't hinge on which update the // session read: with no handler alive, `audio` resolves `NotFound` rather than parking, @@ -1121,7 +1121,7 @@ mod session_tests { let mut broadcast = moq_net::broadcast::Info::new().produce(); let mut dynamic = broadcast.dynamic(); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); { let mut guard = catalog.modify().unwrap(); @@ -1162,7 +1162,7 @@ mod session_tests { let element = element(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let _video = broadcast.create_track("video", None).unwrap(); { @@ -1192,7 +1192,7 @@ mod session_tests { let element = element(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); // One rendition that streams, and one reserved by name that nobody ever accepts. let video = broadcast @@ -1248,7 +1248,7 @@ mod session_tests { let mut broadcast = moq_net::broadcast::Info::new().produce(); let _dynamic = broadcast.dynamic(); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); // A video rendition nobody serves, alongside an audio one that arrives. The audio pad // is the signal that this update was reconciled, so the second update below is a diff --git a/rs/moq-hls/src/export/mod.rs b/rs/moq-hls/src/export/mod.rs index a72dba6744..e9be958134 100644 --- a/rs/moq-hls/src/export/mod.rs +++ b/rs/moq-hls/src/export/mod.rs @@ -298,8 +298,7 @@ async fn watch_catalog( // records out to every rendition. if !timeline_started && let Some(archive) = catalog.archive.clone() { timeline_started = true; - let watcher = - tokio::spawn(watch_timeline(broadcast.clone(), archive.timeline, renditions.fanout())); + let watcher = tokio::spawn(watch_timeline(broadcast.clone(), archive, renditions.fanout())); *timeline_watcher.lock().unwrap() = Some(watcher); } } @@ -319,7 +318,7 @@ async fn watch_catalog( /// to every rendition's window. async fn watch_timeline( broadcast: moq_net::broadcast::Consumer, - section: hang::catalog::Timeline, + section: hang::catalog::Archive, renditions: renditions::Fanout, ) { match watch(&broadcast, §ion, &renditions).await { @@ -339,7 +338,7 @@ async fn watch_timeline( async fn watch( broadcast: &moq_net::broadcast::Consumer, - section: &hang::catalog::Timeline, + section: &hang::catalog::Archive, renditions: &renditions::Fanout, ) -> crate::Result<()> { let mut timeline = moq_mux::timeline::Consumer::<()>::subscribe(broadcast, section).await?; @@ -640,7 +639,7 @@ mod tests { let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut registration = reserved.video("video0").unwrap(); @@ -717,7 +716,7 @@ mod tests { let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut registration = reserved.video("video0").unwrap(); @@ -758,7 +757,7 @@ mod tests { let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut video_registration = reserved.video("video0").unwrap(); @@ -852,8 +851,8 @@ mod tests { // PTS zero at exactly the moq epoch, so every timestamp maps to a fixed string. let wall = UNIX_EPOCH + Duration::from_millis(hang::catalog::MOQ_EPOCH_UNIX_MILLIS); let config = moq_mux::catalog::Config::default() - .with_clock(moq_mux::Clock::with_wall(wall).expect("a representable wall")); - let mut catalog = moq_mux::catalog::Producer::with_config(&mut broadcast, config).unwrap(); + .with_clock(moq_mux::Clock::at(std::time::Instant::now(), wall).expect("a representable wall")); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, config).unwrap(); let reserved = catalog.reserve(); let mut registration = reserved.video("video0").unwrap(); @@ -906,7 +905,7 @@ mod tests { let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); // Stage the clock's removal before the first snapshot publishes, so no consumer ever @@ -954,7 +953,7 @@ mod tests { let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut registration = reserved.video("video0").unwrap(); @@ -1017,7 +1016,7 @@ mod tests { let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut registration = reserved.video("video0").unwrap(); @@ -1058,7 +1057,7 @@ mod tests { let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut video_registration = reserved.video("video0").unwrap(); @@ -1148,7 +1147,7 @@ mod tests { let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut registration = reserved.video("video0").unwrap(); @@ -1219,11 +1218,7 @@ mod tests { let renditions = renditions::Producer::new(Config::default().window); renditions.sync(upstream, &catalog); - let watcher = tokio::spawn(watch_timeline( - upstream.broadcast.clone(), - archive.timeline, - renditions.fanout(), - )); + let watcher = tokio::spawn(watch_timeline(upstream.broadcast.clone(), archive, renditions.fanout())); let rendition = renditions.get(Kind::Video, "video0").expect("rendition synced"); (rendition, watcher) } @@ -1247,7 +1242,8 @@ mod tests { ) -> (Box, hang::catalog::VideoConfig) { let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = + moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut registration = reserved.video("video0").unwrap(); @@ -1370,7 +1366,7 @@ mod tests { // The catalog broadcast carries the catalog and the timeline; the media lives next door. let mut live = origin.create_broadcast("live").expect("publish allowed"); live.announce(Default::default()).expect("publish allowed"); - let mut catalog = moq_mux::catalog::Producer::new(&mut live).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap(); let recorder = catalog.enroll("video0").unwrap(); let old_media = publish_media(&origin, OLD, Some(recorder)); @@ -1510,7 +1506,7 @@ mod tests { let origin = produce_origin(); let mut live = origin.create_broadcast("live").expect("publish allowed"); live.announce(Default::default()).expect("publish allowed"); - let mut catalog = moq_mux::catalog::Producer::new(&mut live).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap(); let recorder = catalog.enroll("video0").unwrap(); let mut old_media = moq_net::broadcast::Info::new().produce(); @@ -1604,7 +1600,7 @@ mod tests { let origin = produce_origin(); let mut live = origin.create_broadcast("live").expect("publish allowed"); live.announce(Default::default()).expect("publish allowed"); - let mut catalog = moq_mux::catalog::Producer::new(&mut live).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap(); let recorder = catalog.enroll("video0").unwrap(); let mut media = moq_net::broadcast::Info::new().produce(); @@ -1646,7 +1642,7 @@ mod tests { let origin = produce_origin(); let mut live = origin.create_broadcast("live").expect("publish allowed"); live.announce(Default::default()).expect("publish allowed"); - let mut catalog = moq_mux::catalog::Producer::new(&mut live).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap(); let recorder = catalog.enroll("video0").unwrap(); let mut old_media = moq_net::broadcast::Info::new().produce(); @@ -1764,7 +1760,7 @@ mod tests { let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut registration = reserved.video("video0").unwrap(); @@ -1823,7 +1819,7 @@ mod tests { let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut registration = reserved.video("video0").unwrap(); diff --git a/rs/moq-hls/src/export/rendition.rs b/rs/moq-hls/src/export/rendition.rs index 874ecee618..bfb28d4b12 100644 --- a/rs/moq-hls/src/export/rendition.rs +++ b/rs/moq-hls/src/export/rendition.rs @@ -6,7 +6,7 @@ use std::task::Poll; use std::time::{Duration, SystemTime}; use bytes::Bytes; -use hang::catalog::{AudioConfig, Clock, Timeline, VideoConfig}; +use hang::catalog::{Archive, AudioConfig, Clock, VideoConfig}; use moq_mux::container::fmp4::Muxer; use moq_mux::timeline::Entry; @@ -200,7 +200,7 @@ pub struct Rendition { config: Config, /// The catalog's root archive timeline: the timescale timings decode with. - section: Timeline, + section: Archive, /// The catalog's root broadcast clock: the wall anchor timings map through, after /// timescale conversion. Absent when the publisher exposes none. clock: Option, @@ -264,7 +264,7 @@ impl Rendition { name: String, config: &VideoConfig, upstream: &Upstream, - section: Timeline, + section: Archive, clock: Option, ) -> moq_mux::Result { Ok(Self { @@ -289,7 +289,7 @@ impl Rendition { name: String, config: &AudioConfig, upstream: &Upstream, - section: Timeline, + section: Archive, clock: Option, ) -> moq_mux::Result { Ok(Self { @@ -508,8 +508,7 @@ impl Rendition { /// the root mapping; an unrepresentable result maps to no time rather than a truncated one. pub(crate) fn wall_clock(&self, pts: moq_net::Timestamp) -> Option { let clock = self.clock.as_ref()?; - let scale = u32::try_from(pts.scale().as_u64()).ok()?; - clock.wall_clock(pts.value(), scale).ok() + clock.wall_clock(pts).ok() } fn muxer(&self) -> Result { diff --git a/rs/moq-hls/src/export/renditions.rs b/rs/moq-hls/src/export/renditions.rs index cbb66a68ac..91d9acba7e 100644 --- a/rs/moq-hls/src/export/renditions.rs +++ b/rs/moq-hls/src/export/renditions.rs @@ -324,7 +324,7 @@ impl Producer { } return; }; - let section = archive.timeline; + let section = archive; let clock = catalog.clock; let Ok(mut current) = self.state.write() else { diff --git a/rs/moq-hls/src/import.rs b/rs/moq-hls/src/import.rs index 40c4b0391a..c97379518a 100644 --- a/rs/moq-hls/src/import.rs +++ b/rs/moq-hls/src/import.rs @@ -1053,7 +1053,7 @@ mod tests { std::fs::write(&playlist_path, playlist).unwrap(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = CatalogProducer::new(&mut broadcast).unwrap(); + let catalog = CatalogProducer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let cfg = Config::new(playlist_path.to_string_lossy().into_owned()); let import = Import::new(broadcast, catalog.clone(), cfg).unwrap(); (import, catalog) @@ -1126,7 +1126,7 @@ mod tests { fn sink() -> Sink { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = CatalogProducer::new(&mut broadcast).unwrap(); + let catalog = CatalogProducer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); Sink { broadcast, catalog } } @@ -1193,7 +1193,7 @@ mod tests { #[test] fn hls_import_starts_without_tracks() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = CatalogProducer::new(&mut broadcast).unwrap(); + let catalog = CatalogProducer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let url = "https://example.com/master.m3u8".to_string(); let cfg = Config::new(url); let hls = Import::new(broadcast, catalog, cfg).unwrap(); @@ -1255,7 +1255,7 @@ mod tests { .unwrap(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = CatalogProducer::new(&mut broadcast).unwrap(); + let catalog = CatalogProducer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let cfg = Config::new(path.to_string_lossy().into_owned()); let mut import = Import::new(broadcast, catalog, cfg).unwrap(); @@ -1542,7 +1542,7 @@ mod tests { std::fs::write(&path, master_body).unwrap(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = CatalogProducer::new(&mut broadcast).unwrap(); + let catalog = CatalogProducer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); // `Config` takes a filesystem path for non-http inputs. let cfg = Config::new(path.to_str().unwrap().to_string()); let mut hls = Import::new(broadcast, catalog, cfg).unwrap(); diff --git a/rs/moq-hls/src/server/routes.rs b/rs/moq-hls/src/server/routes.rs index c00279ed6f..b1389d1dc7 100644 --- a/rs/moq-hls/src/server/routes.rs +++ b/rs/moq-hls/src/server/routes.rs @@ -453,7 +453,7 @@ mod tests { moq_net::track::Producer, moq_mux::container::Producer, ) { - let mut catalog = moq_mux::catalog::Producer::new(broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut registration = reserved.video("video0").unwrap(); registration.set(config).unwrap(); @@ -538,7 +538,8 @@ mod tests { let media_broadcast = pair.pub_origin.create_broadcast("room/source").expect("media"); media_broadcast.announce(Default::default()).expect("announce media"); - let mut catalog = moq_mux::catalog::Producer::new(&mut catalog_broadcast).unwrap(); + let mut catalog = + moq_mux::catalog::Producer::new(&mut catalog_broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut registration = reserved.video("video0").unwrap(); let mut config = video_config(); diff --git a/rs/moq-mux/src/binary.rs b/rs/moq-mux/src/binary.rs index d0a18e9331..4f5ab5f4dc 100644 --- a/rs/moq-mux/src/binary.rs +++ b/rs/moq-mux/src/binary.rs @@ -37,7 +37,8 @@ //! # source: &moq_mux::Source, //! # catalog: &moq_mux::catalog::hang::Catalog, //! # ) -> moq_mux::Result<()> { -//! let entry = catalog.binary_track("thumbnail").expect("no thumbnail track"); +//! let config = catalog.binary.tracks.get("thumbnail").expect("no thumbnail track"); +//! let entry = moq_mux::catalog::Entry::new("thumbnail", config); //! let mut thumbnail = entry.subscribe(source).await?; //! while let Some(jpeg) = thumbnail.next().await? { //! // ... @@ -319,7 +320,7 @@ mod test { fn catalog() -> (moq_net::broadcast::Producer, crate::catalog::Producer) { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); (broadcast, catalog) } diff --git a/rs/moq-mux/src/catalog/entry.rs b/rs/moq-mux/src/catalog/entry.rs index 2b8a646e13..5789c35400 100644 --- a/rs/moq-mux/src/catalog/entry.rs +++ b/rs/moq-mux/src/catalog/entry.rs @@ -9,8 +9,8 @@ use std::ops::Deref; /// a caller can't accidentally subscribe to one track with another's config, and it only has to /// name the track once. /// -/// Get one from [`Catalog`](super::hang::Catalog), by name or by iterating a section. It derefs to -/// the config, so the config's fields are reachable directly (`entry.mode`, `entry.compression`). +/// Create one from a catalog section's map entry with [`Entry::new`]. It derefs to the config, so +/// the config's fields are reachable directly (`entry.mode`, `entry.compression`). /// /// The entry is what a consumer is built from: see /// [`json::Consumer`](crate::json::Consumer) and [`binary::Consumer`](crate::binary::Consumer), @@ -22,7 +22,8 @@ pub struct Entry<'a, C> { } impl<'a, C> Entry<'a, C> { - pub(crate) fn new(name: &'a str, config: &'a C) -> Self { + /// Pair a catalog map key with the config stored under it. + pub fn new(name: &'a str, config: &'a C) -> Self { Self { name, config } } diff --git a/rs/moq-mux/src/catalog/hang/ext.rs b/rs/moq-mux/src/catalog/hang/ext.rs index 79d927b87c..151ae48967 100644 --- a/rs/moq-mux/src/catalog/hang/ext.rs +++ b/rs/moq-mux/src/catalog/hang/ext.rs @@ -1,13 +1,10 @@ -use std::ops::{Deref, DerefMut}; - -use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; /// An application's catalog extension: a plain serde struct of extra root sections that are /// serialized as a flat union with the base media sections. /// /// Implement it (no methods) on a struct of your own sections, then publish/consume a -/// [`Catalog`]: +/// [`hang::Catalog`]: /// /// ``` /// use serde::{Deserialize, Serialize}; @@ -26,7 +23,7 @@ use serde::{Deserialize, Serialize}; /// impl moq_mux::catalog::hang::CatalogExt for Scte35Ext {} /// ``` /// -/// The unit type `()` is the no-extension case, so [`Catalog<()>`] is just the base media catalog. +/// The unit type `()` is the no-extension case, so [`hang::Catalog<()>`] is just the base media catalog. /// /// The same extension rides the MSF catalog track, so this requires [`moq_msf::CatalogExt`]; /// that trait is blanket-implemented, so one `impl CatalogExt` is still all a caller writes. @@ -39,7 +36,7 @@ impl CatalogExt for () {} /// /// This is the extension a caller reaches for when the section names aren't known at /// compile time, e.g. across the FFI/C boundary where a typed [`CatalogExt`] struct can't -/// cross. Publish/consume a [`Catalog`] and use [`set`](Self::set)/[`get`](Self::get). +/// cross. Publish/consume a [`hang::Catalog`] and use [`set`](Self::set)/[`get`](Self::get). /// The default extension stays `()` (unknown sections dropped); opt into `Extra` explicitly. /// /// `video`, `audio`, `text`, `archive`, `clock`, `json`, `binary`, and the retired `timeline` @@ -98,163 +95,11 @@ impl Extra { } } -/// The base sections plus an application extension `E` (defaulting to `()` for none), serialized -/// as a flat union: the `video`/`audio`/`text` media sections, the shared `archive` and `clock`, -/// the `json`/`binary` data sections, and the extension's sections share one JSON object on the wire. -/// -/// The data sections (`json`/`binary`) carry application tracks that aren't media. Every base -/// section is a direct field (`catalog.video`), and the catalog derefs to the extension so its -/// sections are reachable directly too (`catalog.scte35`, or `catalog.ext.scte35` explicitly). A -/// consumer reading a different extension (or none) ignores sections it doesn't know. -/// -/// Marked `#[non_exhaustive]` so a future base section can be added without breaking callers, which -/// is what [`hang::catalog::Catalog`] already does. Build one with -/// [`default`](Default::default) and set the fields you need. -#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)] -#[serde(bound(serialize = "E: Serialize", deserialize = "E: DeserializeOwned"))] -#[non_exhaustive] -pub struct Catalog { - #[serde(default)] - pub video: hang::catalog::Video, - - #[serde(default)] - pub audio: hang::catalog::Audio, - - /// The broadcast's segment index and any durable archive, if the publisher offers one. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub archive: Option, - - /// The broadcast's one continuous clock, if the publisher exposes one. - /// - /// Independent of [`archive`](Self::archive): a live-only publisher exposes its mapping - /// without creating a segment index. See [`hang::catalog::Clock`]. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub clock: Option, - - /// Caption/subtitle renditions. Omitted from the wire when empty, so a broadcast without - /// captions stays byte-identical to before this section existed. - /// - /// Decoded leniently: an application could carry its own `text` section through [`Extra`] before - /// this one was reserved, and that must not take the whole catalog down. - #[serde( - default, - skip_serializing_if = "hang::catalog::Text::is_empty", - deserialize_with = "hang::catalog::deserialize_text" - )] - pub text: hang::catalog::Text, - - /// JSON tracks: application data published as live JSON documents or logs. Omitted from the - /// wire when empty, so a media-only catalog is unchanged. - /// - /// Decoded leniently for the same reason as [`text`](Self::text): `json` is a generic enough - /// key that an application could have been carrying its own through [`Extra`] before this one - /// was reserved. - #[serde( - default, - skip_serializing_if = "hang::catalog::Json::is_empty", - deserialize_with = "hang::catalog::deserialize_section" - )] - pub json: hang::catalog::Json, - - /// Binary tracks: application data published as opaque payloads. Omitted from the wire when - /// empty, so a media-only catalog is unchanged. Decoded leniently for the same reason as - /// [`json`](Self::json). - #[serde( - default, - skip_serializing_if = "hang::catalog::Binary::is_empty", - deserialize_with = "hang::catalog::deserialize_section" - )] - pub binary: hang::catalog::Binary, - - #[serde(flatten)] - pub ext: E, -} - -impl Catalog { - /// The JSON track named `name`, or `None` if the catalog doesn't list one. - /// - /// The returned [`Entry`](crate::catalog::Entry) carries the name along with the config, so - /// reading the track is one call that can't mismatch the two: - /// `catalog.json_track("chat")?.subscribe::(&source).await?`. - pub fn json_track(&self, name: &str) -> Option> { - let (name, config) = self.json.tracks.get_key_value(name)?; - Some(crate::catalog::Entry::new(name, config)) - } - - /// Every JSON track the catalog lists, in name order. - /// - /// This is the discovery path: the catalog is the only thing that announces a data track, so a - /// consumer finds them by walking this. - pub fn json_tracks(&self) -> impl Iterator> { - self.json - .tracks - .iter() - .map(|(name, config)| crate::catalog::Entry::new(name, config)) - } - - /// The binary track named `name`, or `None` if the catalog doesn't list one. - /// - /// See [`json_track`](Self::json_track). - pub fn binary_track(&self, name: &str) -> Option> { - let (name, config) = self.binary.tracks.get_key_value(name)?; - Some(crate::catalog::Entry::new(name, config)) - } - - /// Every binary track the catalog lists, in name order. - /// - /// See [`json_tracks`](Self::json_tracks). - pub fn binary_tracks(&self) -> impl Iterator> { - self.binary - .tracks - .iter() - .map(|(name, config)| crate::catalog::Entry::new(name, config)) - } - - /// The base catalog carrying just the media sections, used to derive the MSF track. - /// - /// MSF describes media only, so the data sections are deliberately left out. - pub(crate) fn media(&self) -> hang::Catalog { - let mut catalog = hang::Catalog::default(); - catalog.video = self.video.clone(); - catalog.audio = self.audio.clone(); - catalog.archive = self.archive.clone(); - catalog.text = self.text.clone(); - catalog - } -} - -impl Catalog { - /// Look up an application catalog section by name, returning its raw JSON value. - pub fn section(&self, name: &str) -> Option<&serde_json::Value> { - self.ext.get(name) - } - - /// Iterate over the application catalog sections as `(name, value)` pairs. - pub fn sections(&self) -> impl Iterator { - self.ext.iter() - } -} - -// Deref to the extension so its sections are reachable directly (the base media sections are -// already real fields, so they shadow this and stay accessible as `catalog.video`/`catalog.audio`). -impl Deref for Catalog { - type Target = E; - - fn deref(&self) -> &E { - &self.ext - } -} - -impl DerefMut for Catalog { - fn deref_mut(&mut self) -> &mut E { - &mut self.ext - } -} - #[cfg(test)] mod test { use std::task::Poll; + use hang::Catalog; use serde::{Deserialize, Serialize}; use super::*; @@ -288,8 +133,8 @@ mod test { #[test] fn extension_roundtrip() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut producer = - crate::catalog::Producer::with_catalog(&mut broadcast, Catalog::::default()).unwrap(); + let config = crate::catalog::Config::default().with_catalog(Catalog::::default()); + let mut producer = crate::catalog::Producer::new(&mut broadcast, config).unwrap(); let mut consumer = producer.consume().unwrap(); // The media pipeline sets a base section (flat field); the app adds its own extension. @@ -298,7 +143,7 @@ mod test { "audio0".to_string(), hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 2), ); - producer.modify().unwrap().scte35 = Some(Scte35 { splice_id: 42 }); // flat, via deref to the extension + producer.modify().unwrap().ext.scte35 = Some(Scte35 { splice_id: 42 }); let waiter = kio::Waiter::noop(); let mut latest = None; @@ -308,13 +153,14 @@ mod test { let catalog = latest.expect("catalog published"); assert!(catalog.audio.renditions.contains_key("audio0")); - assert_eq!(catalog.scte35, Some(Scte35 { splice_id: 42 })); + assert_eq!(catalog.ext.scte35, Some(Scte35 { splice_id: 42 })); } #[test] fn untyped_extra_roundtrip() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut producer = crate::catalog::Producer::::with_catalog(&mut broadcast, Catalog::default()).unwrap(); + let config = crate::catalog::Config::default().with_catalog(Catalog::::default()); + let mut producer = crate::catalog::Producer::::new(&mut broadcast, config).unwrap(); let mut consumer = producer.consume().unwrap(); // A media section (flat field) coexists with an arbitrary untyped application section. @@ -366,9 +212,9 @@ mod test { let catalog = latest.expect("catalog published"); assert!(catalog.audio.renditions.contains_key("audio0")); assert_eq!( - catalog.section("transcript"), + catalog.ext.get("transcript"), Some(&serde_json::json!({ "track": "transcript.json" })) ); - assert_eq!(catalog.sections().count(), 1); + assert_eq!(catalog.ext.iter().count(), 1); } } diff --git a/rs/moq-mux/src/catalog/hang/mod.rs b/rs/moq-mux/src/catalog/hang/mod.rs index 6621b929da..ac0c170d72 100644 --- a/rs/moq-mux/src/catalog/hang/mod.rs +++ b/rs/moq-mux/src/catalog/hang/mod.rs @@ -13,4 +13,5 @@ mod ext; pub use consumer::Consumer; pub use container::Container; pub(crate) use container::supported; -pub use ext::{Catalog, CatalogExt, Extra}; +pub use ext::{CatalogExt, Extra}; +pub use hang::Catalog; diff --git a/rs/moq-mux/src/catalog/msf/consumer.rs b/rs/moq-mux/src/catalog/msf/consumer.rs index 384794ad3c..9dcce3e19b 100644 --- a/rs/moq-mux/src/catalog/msf/consumer.rs +++ b/rs/moq-mux/src/catalog/msf/consumer.rs @@ -110,10 +110,8 @@ impl From for Consumer { /// `render_group`, `alt_group`, `max_grp_sap_starting_type`, `max_obj_sap_starting_type`) are /// dropped. pub(crate) fn from_msf(msf: &moq_msf::Catalog) -> Result> { - let mut catalog = Catalog { - ext: msf.ext.clone(), - ..Default::default() - }; + let mut catalog = Catalog::default(); + catalog.ext = msf.ext.clone(); for track in &msf.tracks { let Some(role) = track.role.as_ref() else { diff --git a/rs/moq-mux/src/catalog/producer.rs b/rs/moq-mux/src/catalog/producer.rs index a2b7a9a794..d387d3f8bc 100644 --- a/rs/moq-mux/src/catalog/producer.rs +++ b/rs/moq-mux/src/catalog/producer.rs @@ -146,7 +146,7 @@ impl Outputs { /// /// Generic over the application extension `E` (defaulting to `()` for none). The catalog is a /// [`Catalog`](super::hang::Catalog): `video`/`audio` are direct fields (`catalog.video`) and the -/// extension is reachable directly via deref (`catalog.scte35`) or as `catalog.ext`. Define an +/// extension is reachable through `catalog.ext` (for example, `catalog.ext.scte35`). Define an /// extension with [`CatalogExt`](super::hang::CatalogExt). The MSF track carries the same catalog: /// the media sections become MSF tracks and the extension's sections ride the MSF root. /// @@ -211,6 +211,7 @@ pub struct Config { max_age: Option, bandwidth: moq_net::bandwidth::Allocator, clock: crate::Clock, + timeline: crate::timeline::Config, } impl Default for Config<()> { @@ -220,6 +221,7 @@ impl Default for Config<()> { max_age: None, bandwidth: moq_net::bandwidth::Allocator::unlimited(), clock: crate::Clock::new(), + timeline: crate::timeline::Config::default(), } } } @@ -234,6 +236,7 @@ impl Config { max_age: self.max_age, bandwidth: self.bandwidth, clock: self.clock, + timeline: self.timeline, } } @@ -274,32 +277,17 @@ impl Config { self.bandwidth = bandwidth; self } -} -impl Producer<()> { - /// Create a new media-only catalog producer with the default (empty) catalog. - /// - /// For an extended catalog or a retention override, use - /// [`with_config`](Self::with_config). - pub fn new(broadcast: &mut moq_net::broadcast::Producer) -> Result { - Self::with_config(broadcast, Config::default()) + /// Pace the broadcast's timeline with `timeline`. + pub fn with_timeline(mut self, timeline: crate::timeline::Config) -> Self { + self.timeline = timeline; + self } } impl Producer { - /// Create a new catalog producer with the given initial catalog. - pub fn with_catalog( - broadcast: &mut moq_net::broadcast::Producer, - catalog: Catalog, - ) -> Result { - Self::with_config(broadcast, Config::default().with_catalog(catalog)) - } - /// Create a new catalog producer from a full [`Config`]. - pub fn with_config( - broadcast: &mut moq_net::broadcast::Producer, - config: Config, - ) -> Result { + pub fn new(broadcast: &mut moq_net::broadcast::Producer, config: Config) -> Result { let hang_track = broadcast.create_track(hang::Catalog::DEFAULT_NAME, hang::Catalog::default_track_info())?; let hangz_track = broadcast.create_track(hang::Catalog::COMPRESSED_NAME, hang::Catalog::default_track_info())?; @@ -319,7 +307,7 @@ impl Producer { json_config.compression = moq_json::Compression::Deflate; let hangz = moq_json::snapshot::Producer::new(hangz_track, json_config); - let timeline = crate::timeline::Producer::new(broadcast, crate::timeline::Config::default()); + let timeline = crate::timeline::Producer::new(broadcast, config.timeline); #[allow(clippy::arc_with_non_send_sync)] let catalog_timeline = Arc::new(Mutex::new(CatalogTimeline { recorder: timeline.track(hang::Catalog::DEFAULT_NAME), @@ -330,7 +318,7 @@ impl Producer { // independently of any archive timeline: a live-only publisher exposes its mapping // without creating a segment index. let mut catalog = config.catalog; - catalog.clock = Some(config.clock.section()); + catalog.clock = Some(config.clock.wall()); // The contents are `Send + Sync` natively; on wasm moq-net's handles are // `Rc`-backed, so clippy sees a pointlessly atomic `Arc`. Keeping one type for @@ -379,7 +367,7 @@ impl Producer { pub fn timestamp(&self, hint: Option) -> crate::Result { match hint { Some(pts) => Ok(pts), - None => Ok(moq_net::Timestamp::from_micros(self.clock.micros())?), + None => Ok(self.clock.now()), } } @@ -422,21 +410,6 @@ impl Producer { self.clock } - /// Pace the broadcast's timeline with `config` instead of the default. - /// - /// Call it before any track enrolls (the timeline's own track doesn't exist until then, so - /// this replaces it wholesale); afterwards the pacing is fixed for the broadcast, since the - /// catalog has advertised what it promises. - #[allow(clippy::arc_with_non_send_sync)] - pub fn with_timeline(mut self, broadcast: &moq_net::broadcast::Producer, config: crate::timeline::Config) -> Self { - self.timeline = crate::timeline::Producer::new(broadcast, config); - self.outputs.catalog_timeline = Arc::new(Mutex::new(CatalogTimeline { - recorder: self.timeline.track(hang::Catalog::DEFAULT_NAME), - last_sequence: None, - })); - self - } - /// Begin reserving the initial track set, returning a clonable [`Reserved`](super::Reserved). /// /// Hand it (or clones) to importers; each reserves its rendition via @@ -661,8 +634,8 @@ impl Producer { /// RAII guard for modifying a catalog with automatic publishing on drop. /// -/// Obtained via [`Producer::modify`]. Derefs to the [`Catalog`](super::hang::Catalog), so `video`/`audio` -/// and (through the catalog's own deref) the extension sections are editable directly. +/// Obtained via [`Producer::modify`]. Derefs to the [`Catalog`](super::hang::Catalog), so base +/// sections are editable directly and application sections are editable through `ext`. /// /// On drop, the hang, compressed-hang, and MSF catalog tracks are updated if the catalog was /// mutated. That publish cannot return an error, so a failure (an extension that won't serialize, @@ -911,7 +884,7 @@ mod test { use hang::catalog::PRIORITY; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = Producer::new(&mut broadcast).unwrap(); + let catalog = Producer::new(&mut broadcast, Config::default()).unwrap(); // Pacing enrollment is what mints the timeline track; a passive one publishes none. catalog.timeline().pacing_track("video").unwrap(); @@ -934,7 +907,7 @@ mod test { // Unset, a catalog mints hang's media defaults, sized so a segmented egress can serve a // full playlist window rather than moq-net's live-edge default. - let catalog = Producer::new(&mut broadcast).unwrap(); + let catalog = Producer::new(&mut broadcast, Config::default()).unwrap(); assert_eq!( catalog.track_info(hang::catalog::PRIORITY.video).max_age, hang::container::track_info(hang::catalog::PRIORITY.video).max_age @@ -945,7 +918,7 @@ mod test { // timescale hang pins (or survive a retimescale for a source-scale container). let mut broadcast = moq_net::broadcast::Info::new().produce(); let config = Config::default().with_max_age(std::time::Duration::from_secs(3)); - let catalog = Producer::with_config(&mut broadcast, config).unwrap(); + let catalog = Producer::new(&mut broadcast, config).unwrap(); let info = catalog.track_info(hang::catalog::PRIORITY.video); assert_eq!(info.max_age, std::time::Duration::from_secs(3)); @@ -970,7 +943,7 @@ mod test { #[test] fn publishes_plain_and_compressed_tracks() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut catalog = Producer::new(&mut broadcast).unwrap(); + let mut catalog = Producer::new(&mut broadcast, Config::default()).unwrap(); let mut plain = Consumer::new(catalog.outputs.hang.consume()); let mut compressed = Consumer::compressed(catalog.outputs.hangz.consume()); @@ -1001,7 +974,7 @@ mod test { #[test] fn modify_refuses_a_finished_catalog() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut catalog = Producer::new(&mut broadcast).unwrap(); + let mut catalog = Producer::new(&mut broadcast, Config::default()).unwrap(); catalog .modify() @@ -1024,7 +997,8 @@ mod test { #[test] fn a_dropped_failed_edit_aborts_every_catalog_track() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut catalog = Producer::::with_catalog(&mut broadcast, Catalog::default()).unwrap(); + let config = Config::default().with_catalog(Catalog::::default()); + let mut catalog = Producer::::new(&mut broadcast, config).unwrap(); let mut hang = catalog.outputs.hang.consume().ordered(); let mut msf = catalog.outputs.msf_track.subscribe(None).ordered(); @@ -1056,7 +1030,8 @@ mod test { #[test] fn a_failed_commit_leaves_the_catalog_open() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut catalog = Producer::::with_catalog(&mut broadcast, Catalog::default()).unwrap(); + let config = Config::default().with_catalog(Catalog::::default()); + let mut catalog = Producer::::new(&mut broadcast, config).unwrap(); let track = catalog.outputs.hang.consume(); let mut guard = catalog.modify().unwrap(); @@ -1077,7 +1052,7 @@ mod test { #[test] fn commit_publishes_once() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut catalog = Producer::new(&mut broadcast).unwrap(); + let mut catalog = Producer::new(&mut broadcast, Config::default()).unwrap(); let track = catalog.outputs.hang.consume(); let mut guard = catalog.modify().unwrap(); @@ -1095,7 +1070,7 @@ mod test { #[test] fn timeline_reports_a_track_collision() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut catalog = Producer::new(&mut broadcast).unwrap(); + let mut catalog = Producer::new(&mut broadcast, Config::default()).unwrap(); // Something else already took the name the timeline track wants. let _taken = broadcast.create_track(hang::timeline::DEFAULT_NAME, None).unwrap(); @@ -1105,7 +1080,7 @@ mod test { #[test] fn enrolling_advertises_the_catalog_section() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut catalog = Producer::new(&mut broadcast).unwrap(); + let mut catalog = Producer::new(&mut broadcast, Config::default()).unwrap(); // A broadcast that never segments never advertises an archive. assert_eq!(catalog.snapshot().archive, None); @@ -1120,12 +1095,12 @@ mod test { #[test] fn clock_is_advertised_without_an_archive() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = Producer::new(&mut broadcast).unwrap(); + let catalog = Producer::new(&mut broadcast, Config::default()).unwrap(); // A live-only broadcast exposes its clock without creating a segment index. let snapshot = catalog.snapshot(); assert_eq!(snapshot.archive, None); - assert_eq!(snapshot.clock, Some(catalog.clock().section())); + assert_eq!(snapshot.clock, Some(catalog.clock().wall())); } #[test] @@ -1134,11 +1109,15 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let wall = SystemTime::UNIX_EPOCH + Duration::from_millis(hang::catalog::MOQ_EPOCH_UNIX_MILLIS + 60_000); - let config = Config::default().with_clock(crate::Clock::with_wall(wall).expect("a representable wall")); - let catalog = Producer::with_config(&mut broadcast, config).unwrap(); + let config = Config::default() + .with_clock(crate::Clock::at(std::time::Instant::now(), wall).expect("a representable wall")); + let catalog = Producer::new(&mut broadcast, config).unwrap(); // A recording import advertises the content's start, not the construction instant. - assert_eq!(catalog.snapshot().clock.map(|clock| clock.wall), Some(60_000_000)); + assert_eq!( + catalog.snapshot().clock.map(|clock| clock.wall), + Some(moq_net::Timestamp::from_micros(60_000_000).unwrap()) + ); } fn h264_config() -> VideoConfig { @@ -1158,7 +1137,7 @@ mod test { #[test] fn reservation_gates_until_all_renditions_resolve() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = Producer::new(&mut broadcast).unwrap(); + let catalog = Producer::new(&mut broadcast, Config::default()).unwrap(); let mut consumer: Consumer = Consumer::new(catalog.outputs.hang.consume()); let waiter = kio::Waiter::noop(); @@ -1191,7 +1170,7 @@ mod test { #[test] fn live_rendition_owns_its_name_without_gating_the_catalog() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = Producer::new(&mut broadcast).unwrap(); + let catalog = Producer::new(&mut broadcast, Config::default()).unwrap(); let mut consumer: Consumer = Consumer::new(catalog.outputs.hang.consume()); let waiter = kio::Waiter::noop(); @@ -1216,7 +1195,7 @@ mod test { #[test] fn reservation_gate_opens_when_unresolved_reservation_is_dropped() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = Producer::new(&mut broadcast).unwrap(); + let catalog = Producer::new(&mut broadcast, Config::default()).unwrap(); let mut consumer: Consumer = Consumer::new(catalog.outputs.hang.consume()); let waiter = kio::Waiter::noop(); @@ -1244,7 +1223,7 @@ mod test { #[test] fn staged_change_waits_for_a_held_reservation() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = Producer::new(&mut broadcast).unwrap(); + let catalog = Producer::new(&mut broadcast, Config::default()).unwrap(); let mut consumer: Consumer = Consumer::new(catalog.outputs.hang.consume()); let waiter = kio::Waiter::noop(); diff --git a/rs/moq-mux/src/catalog/tracks.rs b/rs/moq-mux/src/catalog/tracks.rs index b7bf43d49a..210589d89b 100644 --- a/rs/moq-mux/src/catalog/tracks.rs +++ b/rs/moq-mux/src/catalog/tracks.rs @@ -32,13 +32,13 @@ use super::hang::{Catalog, CatalogExt}; /// /// impl RenditionConfig for Telemetry { /// fn insert(self, catalog: &mut Catalog, name: &str) { -/// catalog.telemetry.insert(name.to_string(), self); +/// catalog.ext.telemetry.insert(name.to_string(), self); /// } /// fn get_mut<'a>(catalog: &'a mut Catalog, name: &str) -> Option<&'a mut Self> { -/// catalog.telemetry.get_mut(name) +/// catalog.ext.telemetry.get_mut(name) /// } /// fn remove(catalog: &mut Catalog, name: &str) { -/// catalog.telemetry.remove(name); +/// catalog.ext.telemetry.remove(name); /// } /// /// // Opt into bitrate detection; jitter is left undetected. @@ -539,7 +539,7 @@ mod tests { fn video_track() -> (moq_net::broadcast::Producer, super::super::Producer, VideoTrack) { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = super::super::Producer::new(&mut broadcast).unwrap(); + let catalog = super::super::Producer::new(&mut broadcast, super::super::Config::default()).unwrap(); let reserved = catalog.reserve(); let rendition = reserved.video("v").unwrap(); // Drop the standalone reservation so only the rendition's own gate remains, which `set` @@ -573,7 +573,7 @@ mod tests { #[test] fn importer_returns_rejected_jitter() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = super::super::Producer::new(&mut broadcast).unwrap(); + let catalog = super::super::Producer::new(&mut broadcast, super::super::Config::default()).unwrap(); let reserved = catalog.reserve(); let track = broadcast .create_track("audio", hang::container::track_info(hang::catalog::PRIORITY.audio)) @@ -826,7 +826,7 @@ mod tests { #[test] fn an_existing_entry_owns_its_name() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut catalog = super::super::Producer::new(&mut broadcast).unwrap(); + let mut catalog = super::super::Producer::new(&mut broadcast, super::super::Config::default()).unwrap(); catalog.modify().unwrap().video.insert("v", config(None, None)).unwrap(); assert!(catalog.reserve().video("v").is_err(), "the catalog already carries it"); @@ -837,7 +837,7 @@ mod tests { #[test] fn renditions_share_the_broadcast_timeline() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut catalog = super::super::Producer::new(&mut broadcast).unwrap(); + let mut catalog = super::super::Producer::new(&mut broadcast, super::super::Config::default()).unwrap(); let _recorder = catalog.enroll("video0").unwrap(); let timeline = catalog.timeline(); @@ -877,13 +877,13 @@ mod tests { impl RenditionConfig for Telemetry { fn insert(self, catalog: &mut Catalog, name: &str) { - catalog.telemetry.insert(name.to_string(), self); + catalog.ext.telemetry.insert(name.to_string(), self); } fn get_mut<'a>(catalog: &'a mut Catalog, name: &str) -> Option<&'a mut Self> { - catalog.telemetry.get_mut(name) + catalog.ext.telemetry.get_mut(name) } fn remove(catalog: &mut Catalog, name: &str) { - catalog.telemetry.remove(name); + catalog.ext.telemetry.remove(name); } // Opts into bitrate detection only; jitter is left undetected. @@ -904,7 +904,8 @@ mod tests { fn produce() -> (moq_net::broadcast::Producer, crate::catalog::Producer) { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::with_catalog(&mut broadcast, Catalog::default()).unwrap(); + let config = crate::catalog::Config::default().with_catalog(Catalog::::default()); + let catalog = crate::catalog::Producer::new(&mut broadcast, config).unwrap(); (broadcast, catalog) } @@ -919,15 +920,15 @@ mod tests { impl RenditionConfig for Exploding { fn insert(self, catalog: &mut Catalog, name: &str) { if self.wrote { - catalog.exploding.insert(name.to_string(), self); + catalog.ext.exploding.insert(name.to_string(), self); } panic!("insert exploded"); } fn get_mut<'a>(catalog: &'a mut Catalog, name: &str) -> Option<&'a mut Self> { - catalog.exploding.get_mut(name) + catalog.ext.exploding.get_mut(name) } fn remove(catalog: &mut Catalog, name: &str) { - catalog.exploding.remove(name); + catalog.ext.exploding.remove(name); } } @@ -951,7 +952,7 @@ mod tests { ); // The rendition unwound, so it released its name and the catalog is still usable. - assert!(catalog.snapshot().telemetry.is_empty()); + assert!(catalog.snapshot().ext.telemetry.is_empty()); reserved .init::("gps") .expect("the unwound rendition released its name"); @@ -964,13 +965,13 @@ mod tests { impl RenditionConfig for Stubborn { fn insert(self, catalog: &mut Catalog, name: &str) { - catalog.stubborn.insert(name.to_string(), self); + catalog.ext.stubborn.insert(name.to_string(), self); } fn get_mut<'a>(catalog: &'a mut Catalog, name: &str) -> Option<&'a mut Self> { - catalog.stubborn.get_mut(name) + catalog.ext.stubborn.get_mut(name) } fn remove(catalog: &mut Catalog, name: &str) { - catalog.stubborn.remove(name); + catalog.ext.stubborn.remove(name); panic!("remove exploded"); } } @@ -1007,7 +1008,7 @@ mod tests { .expect_err("the config's insert panics after writing"); assert!( - catalog.snapshot().exploding.is_empty(), + catalog.snapshot().ext.exploding.is_empty(), "the entry the panicking insert wrote is retired with its owner" ); reserved @@ -1027,12 +1028,12 @@ mod tests { feed(&mut rendition); let snapshot = catalog.snapshot(); - let config = snapshot.telemetry.get("gps").unwrap(); + let config = snapshot.ext.telemetry.get("gps").unwrap(); assert!(config.bitrate.is_some(), "absent bitrate should be auto-detected"); drop(rendition); assert!( - !catalog.snapshot().telemetry.contains_key("gps"), + !catalog.snapshot().ext.telemetry.contains_key("gps"), "the rendition should be removed on drop" ); } @@ -1070,7 +1071,7 @@ mod tests { } let snapshot = catalog.snapshot(); - let config = snapshot.telemetry.get("gps").unwrap(); + let config = snapshot.ext.telemetry.get("gps").unwrap(); assert_eq!(config.bitrate, Some(1_000_000)); } @@ -1086,7 +1087,7 @@ mod tests { feed(&mut rendition); let snapshot = catalog.snapshot(); - assert_eq!(snapshot.telemetry.get("gps").unwrap().bitrate, Some(4_200)); + assert_eq!(snapshot.ext.telemetry.get("gps").unwrap().bitrate, Some(4_200)); } /// Custom and media renditions share one reservation gate, so the first snapshot carries both. @@ -1115,7 +1116,7 @@ mod tests { } let published = latest.expect("catalog published"); assert!(published.video.renditions.contains_key("v")); - assert!(published.telemetry.contains_key("gps")); + assert!(published.ext.telemetry.contains_key("gps")); } } } diff --git a/rs/moq-mux/src/clock.rs b/rs/moq-mux/src/clock.rs index 334616c6d9..2929b0591e 100644 --- a/rs/moq-mux/src/clock.rs +++ b/rs/moq-mux/src/clock.rs @@ -20,8 +20,8 @@ use std::time::{Duration, Instant, SystemTime}; use hang::catalog::{MAX_SAFE_INTEGER, MOQ_EPOCH_UNIX_MILLIS}; -/// The wall-clock time of PTS zero, in [`Clock::TIMESCALE`] units since the moq epoch. -fn wall_units(wall: SystemTime) -> crate::Result { +/// The catalog clock for PTS zero at `wall`. +fn wall_clock(wall: SystemTime) -> crate::Result { let unix_micros = wall .duration_since(SystemTime::UNIX_EPOCH) .map(|d| d.as_micros()) @@ -35,7 +35,7 @@ fn wall_units(wall: SystemTime) -> crate::Result { if wall > MAX_SAFE_INTEGER { return Err(hang::Error::InvalidWall(wall).into()); } - Ok(wall) + Ok(hang::catalog::Clock::new(moq_net::Timestamp::from_micros(wall)?)?) } /// A monotonic clock for stamping media frames so that tracks produced @@ -49,7 +49,7 @@ fn wall_units(wall: SystemTime) -> crate::Result { #[derive(Clone, Copy, Debug)] pub struct Clock { epoch: Instant, - wall: u64, + wall: hang::catalog::Clock, } impl Clock { @@ -61,37 +61,26 @@ impl Clock { /// Start a clock anchored at the current instant, with PTS zero at the current wall time. pub fn new() -> Self { - Self::new_at(Instant::now(), SystemTime::now()) + Self::at(Instant::now(), SystemTime::now()) .expect("the current wall time is representable as a broadcast clock") } - /// Start a clock anchored at the current instant, with PTS zero at `wall`. - /// - /// For an import whose content carries its own start (a recording): the media keeps its - /// relative spacing and that start names the wall epoch. Refuses a wall before the moq - /// epoch or outside the JSON-safe integer range rather than publishing a fabricated mapping. - pub fn with_wall(wall: SystemTime) -> crate::Result { - Ok(Self { - epoch: Instant::now(), - wall: wall_units(wall)?, - }) - } - /// Start a clock at an explicit monotonic epoch and wall time. /// /// The deterministic constructor: synthetic sources and fixtures pin both ends instead of - /// sampling. Refuses an unrepresentable wall like [`with_wall`](Self::with_wall). - pub fn new_at(epoch: Instant, wall: SystemTime) -> crate::Result { + /// sampling. Refuses an unrepresentable wall. + pub fn at(epoch: Instant, wall: SystemTime) -> crate::Result { Ok(Self { epoch, - wall: wall_units(wall)?, + wall: wall_clock(wall)?, }) } - /// Microseconds elapsed since the clock's epoch. - pub fn micros(&self) -> u64 { + /// The current timestamp since the clock's epoch. + pub fn now(&self) -> moq_net::Timestamp { // u128 -> u64 truncation is unreachable: u64 microseconds is ~584,000 years. - self.epoch.elapsed().as_micros() as u64 + moq_net::Timestamp::from_micros(self.epoch.elapsed().as_micros() as u64) + .expect("an instant elapsed duration fits in a timestamp") } /// Units per second for [`wall`](Self::wall): [`TIMESCALE`](Self::TIMESCALE). @@ -99,27 +88,17 @@ impl Clock { Self::TIMESCALE } - /// The wall-clock time of PTS zero, in [`TIMESCALE`](Self::TIMESCALE) units since the moq epoch. - pub fn wall(&self) -> u64 { + /// The catalog root section advertising this clock. + pub fn wall(&self) -> hang::catalog::Clock { self.wall } - /// The catalog root section advertising this clock: `clock: { wall, timescale }`. - pub fn section(&self) -> hang::catalog::Clock { - hang::catalog::Clock::with_timescale(self.wall, Self::TIMESCALE.as_u64() as u32) - .expect("a constructed clock is always representable") - } - /// The wall-clock time of `pts` under this broadcast's fixed mapping. /// /// Pure in the stored epoch: a system-clock adjustment after construction changes nothing. /// Refuses an unrepresentable result rather than truncating it. pub fn wall_clock(&self, pts: moq_net::Timestamp) -> crate::Result { - let scale = u32::try_from(pts.scale().as_u64()) - .map_err(|_| crate::Error::UnmappableTimestamp(format!("timescale {} exceeds u32", pts.scale())))?; - self.section() - .wall_clock(pts.value(), scale) - .map_err(crate::Error::from) + self.wall.wall_clock(pts).map_err(crate::Error::from) } /// Translate a source with its own zero onto this broadcast's mapping. @@ -157,7 +136,7 @@ pub struct SourceMap { offset: Option, last_source: Option, last_broadcast: Option, - /// `clock.micros()` when the last frame was translated: the idle gap's start. + /// `clock.now()` when the last frame was translated: the idle gap's start. last_arrival: Option, } @@ -188,7 +167,7 @@ impl SourceMap { /// Translate `pts` onto the broadcast clock, sampling the arrival time. pub fn translate(&mut self, pts: moq_net::Timestamp) -> crate::Result { - self.translate_at(pts, self.clock.micros()) + self.translate_at(pts, self.clock.now().value()) } /// Translate `pts` onto the broadcast clock, arriving at monotonic `now` micros. @@ -239,7 +218,7 @@ impl SourceMap { /// the next frame continues after everything published so far plus the downtime since the /// previous frame, instead of rewinding the broadcast. pub fn reset(&mut self, pts: moq_net::Timestamp) -> crate::Result { - self.reset_at(pts, self.clock.micros()) + self.reset_at(pts, self.clock.now().value()) } /// [`reset`](Self::reset) with an explicit arrival instant, for synthetic sources. @@ -287,9 +266,9 @@ mod tests { #[test] fn copies_share_one_epoch() { - let clock = Clock::new_at(epoch(), moq_epoch() + Duration::from_secs(1)).unwrap(); + let clock = Clock::at(epoch(), moq_epoch() + Duration::from_secs(1)).unwrap(); let shared = clock; - // Compare the anchors, not live readings: two `micros()` calls race the clock. + // Compare the anchors, not live readings: two `now()` calls race the clock. assert_eq!(clock.epoch, shared.epoch); assert_eq!(clock.wall(), shared.wall()); } @@ -297,14 +276,13 @@ mod tests { #[test] fn section_advertises_wall_in_clock_timescale() { // PTS zero at exactly the moq epoch advertises 0. - let clock = Clock::new_at(epoch(), moq_epoch()).unwrap(); - assert_eq!(clock.section().wall, 0); - assert_eq!(clock.wall(), 0); - assert_eq!(clock.section().timescale, Clock::TIMESCALE.as_u64() as u32); + let clock = Clock::at(epoch(), moq_epoch()).unwrap(); + assert_eq!(clock.wall().wall.value(), 0); + assert_eq!(clock.wall().wall.scale(), Clock::TIMESCALE); // A second later is a second's worth of clock units. - let clock = Clock::new_at(epoch(), moq_epoch() + Duration::from_secs(1)).unwrap(); - assert_eq!(clock.wall(), 1_000_000); + let clock = Clock::at(epoch(), moq_epoch() + Duration::from_secs(1)).unwrap(); + assert_eq!(clock.wall().wall.value(), 1_000_000); } #[test] @@ -312,19 +290,17 @@ mod tests { // One micro past the JSON-safe integer range: browsers would read a different number. let past_safe = MOQ_EPOCH_UNIX_MILLIS * 1000 + MAX_SAFE_INTEGER + 1; let far = SystemTime::UNIX_EPOCH + Duration::from_micros(past_safe); - assert!(Clock::with_wall(far).is_err()); - assert!(Clock::new_at(epoch(), far).is_err()); + assert!(Clock::at(epoch(), far).is_err()); // Before 2020 cannot be named on the wire; saturating to the epoch would lie. - assert!(Clock::with_wall(SystemTime::UNIX_EPOCH).is_err()); - assert!(Clock::new_at(epoch(), SystemTime::UNIX_EPOCH).is_err()); - assert!(Clock::with_wall(moq_epoch() - Duration::from_micros(1)).is_err()); - assert_eq!(Clock::new_at(epoch(), moq_epoch()).unwrap().wall(), 0); + assert!(Clock::at(epoch(), SystemTime::UNIX_EPOCH).is_err()); + assert!(Clock::at(epoch(), moq_epoch() - Duration::from_micros(1)).is_err()); + assert_eq!(Clock::at(epoch(), moq_epoch()).unwrap().wall().wall.value(), 0); } #[test] fn delayed_first_frame_anchors_live() { - let clock = Clock::new_at(epoch(), moq_epoch()).unwrap(); + let clock = Clock::at(epoch(), moq_epoch()).unwrap(); let mut source = clock.source(); // The source's first frame already carries 5s of PTS; it is live now, not now + 5s. @@ -344,7 +320,7 @@ mod tests { #[test] fn multiple_timescales_share_one_mapping() { - let clock = Clock::new_at(epoch(), moq_epoch()).unwrap(); + let clock = Clock::at(epoch(), moq_epoch()).unwrap(); // Two sources, 90kHz video and 48kHz audio, anchored at the same arrival instant. let mut video = clock.source(); @@ -386,7 +362,7 @@ mod tests { #[test] fn reset_translation_preserves_the_idle_gap() { - let clock = Clock::new_at(epoch(), moq_epoch()).unwrap(); + let clock = Clock::at(epoch(), moq_epoch()).unwrap(); let mut source = clock.source(); assert_eq!(source.translate_at(us(0), 1_000_000).unwrap().as_micros(), 1_000_000); @@ -409,7 +385,7 @@ mod tests { #[test] fn explicit_reset_marks_a_detected_restart() { - let clock = Clock::new_at(epoch(), moq_epoch()).unwrap(); + let clock = Clock::at(epoch(), moq_epoch()).unwrap(); let mut source = clock.source(); assert_eq!(source.translate_at(us(0), 1_000_000).unwrap().as_micros(), 1_000_000); @@ -421,7 +397,7 @@ mod tests { #[test] fn bframe_reordering_within_a_group_survives() { - let clock = Clock::new_at(epoch(), moq_epoch()).unwrap(); + let clock = Clock::at(epoch(), moq_epoch()).unwrap(); let mut source = clock.source(); assert_eq!(source.translate_at(us(0), 1_000_000).unwrap().as_micros(), 1_000_000); @@ -439,7 +415,7 @@ mod tests { #[test] fn mapping_past_u64_micros_is_refused() { - let clock = Clock::new_at(epoch(), moq_epoch()).unwrap(); + let clock = Clock::at(epoch(), moq_epoch()).unwrap(); let mut source = clock.source(); assert_eq!(source.translate_at(us(0), 0).unwrap().as_micros(), 0); @@ -453,7 +429,7 @@ mod tests { #[test] fn mapping_before_the_broadcast_began_is_refused() { - let clock = Clock::new_at(epoch(), moq_epoch()).unwrap(); + let clock = Clock::at(epoch(), moq_epoch()).unwrap(); let mut source = clock.source(); // The first frame carries 100ms of PTS but arrives 50ms in: the offset is negative. @@ -467,7 +443,7 @@ mod tests { #[test] fn wall_mapping_survives_a_system_clock_adjustment() { - let clock = Clock::new_at(epoch(), moq_epoch()).unwrap(); + let clock = Clock::at(epoch(), moq_epoch()).unwrap(); let before = clock.wall_clock(us(2_000_000)).unwrap(); // The mapping is a stored epoch, not a sampled clock: reading it again (after whatever @@ -482,7 +458,7 @@ mod tests { #[test] fn wall_clock_validates_bounds() { - let clock = Clock::new_at(epoch(), moq_epoch()).unwrap(); + let clock = Clock::at(epoch(), moq_epoch()).unwrap(); // The largest representable broadcast timestamp still maps. let max = moq_net::Timestamp::from_micros((1u64 << 62) - 1).unwrap(); assert!(clock.wall_clock(max).is_err(), "past the JSON-safe range is refused"); diff --git a/rs/moq-mux/src/codec/h264/import.rs b/rs/moq-mux/src/codec/h264/import.rs index 983317e071..2ea5c14bf1 100644 --- a/rs/moq-mux/src/codec/h264/import.rs +++ b/rs/moq-mux/src/codec/h264/import.rs @@ -321,7 +321,7 @@ mod tests { fn setup(name: &str) -> (moq_net::track::Producer, crate::catalog::Producer) { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track(name, hang::container::track_info(hang::catalog::PRIORITY.video)) .unwrap(); diff --git a/rs/moq-mux/src/codec/h265/import.rs b/rs/moq-mux/src/codec/h265/import.rs index 49d93b771d..d195fbcb8b 100644 --- a/rs/moq-mux/src/codec/h265/import.rs +++ b/rs/moq-mux/src/codec/h265/import.rs @@ -369,7 +369,7 @@ mod tests { fn setup(name: &str) -> (moq_net::track::Producer, crate::catalog::Producer) { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track(name, hang::container::track_info(hang::catalog::PRIORITY.video)) .unwrap(); diff --git a/rs/moq-mux/src/codec/opus/import.rs b/rs/moq-mux/src/codec/opus/import.rs index 3cf30c788c..d15ffc6792 100644 --- a/rs/moq-mux/src/codec/opus/import.rs +++ b/rs/moq-mux/src/codec/opus/import.rs @@ -149,7 +149,7 @@ mod tests { #[tokio::test(start_paused = true)] async fn a_loc_reservation_reaches_the_wire_and_the_catalog() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track("audio", hang::container::track_info(hang::catalog::PRIORITY.audio)) .unwrap(); diff --git a/rs/moq-mux/src/codec/video.rs b/rs/moq-mux/src/codec/video.rs index d7ee629c54..5f4dbb4866 100644 --- a/rs/moq-mux/src/codec/video.rs +++ b/rs/moq-mux/src/codec/video.rs @@ -144,7 +144,7 @@ mod tests { #[test] fn quiet_startup_tracks_demand() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut rendition = reserved.video("video").unwrap(); let mut state = Catalog::new(VideoHint::default()); diff --git a/rs/moq-mux/src/codec/vp8/import.rs b/rs/moq-mux/src/codec/vp8/import.rs index 4ec558cc3f..0158747571 100644 --- a/rs/moq-mux/src/codec/vp8/import.rs +++ b/rs/moq-mux/src/codec/vp8/import.rs @@ -165,7 +165,7 @@ mod tests { fn setup() -> (moq_net::track::Producer, crate::catalog::Producer) { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track("0.vp8", hang::container::track_info(hang::catalog::PRIORITY.video)) .unwrap(); diff --git a/rs/moq-mux/src/codec/vp9/import.rs b/rs/moq-mux/src/codec/vp9/import.rs index 95ebc160cf..d3235167ac 100644 --- a/rs/moq-mux/src/codec/vp9/import.rs +++ b/rs/moq-mux/src/codec/vp9/import.rs @@ -168,7 +168,7 @@ mod tests { fn setup() -> (moq_net::track::Producer, crate::catalog::Producer) { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track("0.vp9", hang::container::track_info(hang::catalog::PRIORITY.video)) .unwrap(); diff --git a/rs/moq-mux/src/container/flv/export_test.rs b/rs/moq-mux/src/container/flv/export_test.rs index b6da353aad..b1dfc069e3 100644 --- a/rs/moq-mux/src/container/flv/export_test.rs +++ b/rs/moq-mux/src/container/flv/export_test.rs @@ -135,7 +135,7 @@ async fn drain_export(mut exporter: Export, mut importer: Import) -> Vec { async fn export_roundtrips_through_import() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let mut catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(synth_flv().as_slice())).unwrap(); @@ -149,7 +149,7 @@ async fn export_roundtrips_through_import() { // Re-import the exported bytes and confirm the catalog rebuilds identically. let mut bcast2 = moq_net::broadcast::Info::new().produce(); - let cat2 = crate::catalog::Producer::new(&mut bcast2).unwrap(); + let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); imp2.finish().unwrap(); @@ -172,7 +172,7 @@ async fn export_roundtrips_through_import() { async fn export_emits_sequence_headers_and_frames() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let mut catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(synth_flv().as_slice())).unwrap(); @@ -252,7 +252,7 @@ fn synth_enhanced_flv() -> Vec { async fn export_roundtrips_enhanced() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let mut catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer @@ -281,7 +281,7 @@ async fn export_roundtrips_enhanced() { // Re-import the exported bytes and confirm the codecs rebuild. let mut bcast2 = moq_net::broadcast::Info::new().produce(); - let cat2 = crate::catalog::Producer::new(&mut bcast2).unwrap(); + let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); imp2.finish().unwrap(); @@ -317,7 +317,7 @@ async fn export_roundtrips_mp3() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let mut catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(flv.as_slice())).unwrap(); @@ -336,7 +336,7 @@ async fn export_roundtrips_mp3() { // Re-import and confirm the codec rebuilds. let mut bcast2 = moq_net::broadcast::Info::new().produce(); - let cat2 = crate::catalog::Producer::new(&mut bcast2).unwrap(); + let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); imp2.finish().unwrap(); @@ -368,7 +368,7 @@ fn synth_av1_flv() -> Vec { async fn export_roundtrips_av1() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let mut catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer @@ -396,7 +396,7 @@ async fn export_roundtrips_av1() { ); let mut bcast2 = moq_net::broadcast::Info::new().produce(); - let cat2 = crate::catalog::Producer::new(&mut bcast2).unwrap(); + let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); imp2.finish().unwrap(); @@ -421,7 +421,7 @@ fn synth_enhanced_audio_flv(fourcc: &[u8; 4], frame: &[u8]) -> Vec { async fn export_roundtrips_ac3() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let mut catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer @@ -441,7 +441,7 @@ async fn export_roundtrips_ac3() { ); let mut bcast2 = moq_net::broadcast::Info::new().produce(); - let cat2 = crate::catalog::Producer::new(&mut bcast2).unwrap(); + let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); imp2.finish().unwrap(); @@ -455,7 +455,7 @@ async fn export_roundtrips_ac3() { async fn export_roundtrips_eac3() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let mut catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer @@ -475,7 +475,7 @@ async fn export_roundtrips_eac3() { ); let mut bcast2 = moq_net::broadcast::Info::new().produce(); - let cat2 = crate::catalog::Producer::new(&mut bcast2).unwrap(); + let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); imp2.finish().unwrap(); @@ -513,7 +513,7 @@ fn build_multitrack_broadcast() -> (moq_net::broadcast::Consumer, Vec>, let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let mut catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); // A finished track stays subscribable only while its producer is alive, so keep // each one until the exporter has drained. let mut tracks = Vec::new(); @@ -635,7 +635,7 @@ async fn export_multitrack_roundtrips_all_renditions() { // Re-import and confirm both video renditions plus the audio rebuild. let mut bcast2 = moq_net::broadcast::Info::new().produce(); - let cat2 = crate::catalog::Producer::new(&mut bcast2).unwrap(); + let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); imp2.finish().unwrap(); @@ -679,7 +679,7 @@ async fn export_without_multitrack_keeps_first_rendition() { ); let mut bcast2 = moq_net::broadcast::Info::new().produce(); - let cat2 = crate::catalog::Producer::new(&mut bcast2).unwrap(); + let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); imp2.finish().unwrap(); @@ -725,7 +725,7 @@ fn parse_tags(flv: &[u8]) -> Vec { async fn export_preserves_timestamps() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let mut catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(synth_flv().as_slice())).unwrap(); @@ -751,7 +751,7 @@ async fn export_authors_dts_and_composition_time_for_reordered_avc() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let mut catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let video_track = producer.create_track(producer.unique_name(".avc1"), None).unwrap(); let audio_track = producer.create_track(producer.unique_name(".aac"), None).unwrap(); diff --git a/rs/moq-mux/src/container/flv/import_test.rs b/rs/moq-mux/src/container/flv/import_test.rs index d37160d5d2..d2b983f4e8 100644 --- a/rs/moq-mux/src/container/flv/import_test.rs +++ b/rs/moq-mux/src/container/flv/import_test.rs @@ -101,7 +101,7 @@ async fn rendition_is_not_published_when_the_media_producer_fails() { // assertion below cannot pass merely because the fixture stopped reaching track import. { let mut producer = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(data.as_slice())).unwrap(); assert_eq!( @@ -112,7 +112,7 @@ async fn rendition_is_not_published_when_the_media_producer_fails() { } let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); // Squat the broadcast's timeline track, so enrolling the first rendition (and with it building // its media producer) fails. The handle must stay alive: the broadcast tracks names weakly, so @@ -132,7 +132,7 @@ async fn rendition_is_not_published_when_the_media_producer_fails() { #[tokio::test(start_paused = true)] async fn import_populates_catalog() { let mut producer = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); let buf = bytes::BytesMut::from(synth_flv().as_slice()); @@ -158,7 +158,7 @@ async fn import_populates_catalog() { async fn import_emits_frames() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); let buf = bytes::BytesMut::from(synth_flv().as_slice()); @@ -190,7 +190,7 @@ async fn public_container_preserves_loc_for_flv() { let data = synth_flv(); let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut import = super::Import::new(broadcast, reserved).with_container(hang::catalog::Container::Loc); import.decode(&data).unwrap(); @@ -220,7 +220,7 @@ async fn import_handles_split_input() { let (head, tail) = flv.split_at(flv.len() / 2); let mut producer = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(head)).unwrap(); @@ -254,7 +254,7 @@ async fn import_enhanced_vp9() { write_tag(&mut out, super::TAG_VIDEO, 0, &body); let mut producer = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); importer.finish().unwrap(); @@ -293,7 +293,7 @@ async fn import_enhanced_opus() { write_tag(&mut out, super::TAG_AUDIO, 20, &frame); let mut producer = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); importer.finish().unwrap(); @@ -328,7 +328,7 @@ async fn import_legacy_mp3() { write_tag(&mut out, super::TAG_AUDIO, 0, &tag); let mut producer = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); importer.finish().unwrap(); @@ -416,7 +416,7 @@ async fn import_multitrack_video_many_tracks() { write_tag(&mut out, super::TAG_VIDEO, 0, &frames); let mut producer = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); importer.finish().unwrap(); @@ -456,7 +456,7 @@ async fn import_multitrack_video_many_codecs() { write_tag(&mut out, super::TAG_VIDEO, 0, &seq); let mut producer = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); importer.finish().unwrap(); @@ -481,7 +481,7 @@ async fn import_enhanced_av1() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); importer.finish().unwrap(); @@ -515,7 +515,7 @@ async fn import_enhanced_ac3() { write_tag(&mut out, super::TAG_AUDIO, 0, &frame); let mut producer = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); importer.finish().unwrap(); @@ -537,7 +537,7 @@ async fn import_enhanced_eac3() { write_tag(&mut out, super::TAG_AUDIO, 0, &frame); let mut producer = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); importer.finish().unwrap(); @@ -586,7 +586,7 @@ async fn import_reports_negative_pts_and_can_resume() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); let err = importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap_err(); assert!(matches!( @@ -634,7 +634,7 @@ async fn import_enhanced_hvc1_applies_composition_time() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); importer.finish().unwrap(); @@ -658,7 +658,7 @@ async fn import_enhanced_hvc1_applies_composition_time() { #[tokio::test(start_paused = true)] async fn import_rejects_non_flv() { let mut producer = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); let buf = bytes::BytesMut::from(&b"NOTFLV\x00\x00\x00"[..]); diff --git a/rs/moq-mux/src/container/fmp4/export_test.rs b/rs/moq-mux/src/container/fmp4/export_test.rs index 699f63bde4..c5d20ebac6 100644 --- a/rs/moq-mux/src/container/fmp4/export_test.rs +++ b/rs/moq-mux/src/container/fmp4/export_test.rs @@ -408,7 +408,7 @@ async fn cmaf_source_to_cmaf_export_passthrough() { let mut producer = broadcast.produce(); let consumer = producer.consume(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = crate::container::fmp4::Import::new(producer, catalog.reserve()); let buf = BytesMut::from(data.as_slice()); let _ = importer.decode(&buf); @@ -466,7 +466,7 @@ async fn single_track_export_init_matches_fragment_track_id() { let mut producer = moq_net::broadcast::Info::new().produce(); let consumer = producer.consume(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = crate::container::fmp4::Import::new(producer, catalog.reserve()); let buf = BytesMut::from(data.as_slice()); let _ = importer.decode(&buf); diff --git a/rs/moq-mux/src/container/fmp4/import_test.rs b/rs/moq-mux/src/container/fmp4/import_test.rs index 0fcdc6b281..9f0108e436 100644 --- a/rs/moq-mux/src/container/fmp4/import_test.rs +++ b/rs/moq-mux/src/container/fmp4/import_test.rs @@ -28,7 +28,7 @@ fn drain_group_sequences(consumer: &mut moq_net::track::Subscriber) -> Vec fn run_fmp4(data: &[u8]) -> crate::catalog::hang::Catalog { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); @@ -41,7 +41,7 @@ fn run_fmp4(data: &[u8]) -> crate::catalog::hang::Catalog { fn run_fmp4_select(data: &[u8], select: crate::select::Broadcast) -> crate::catalog::hang::Catalog { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()).with_select(select); @@ -98,7 +98,7 @@ fn every_rendition_is_cmaf() { mp4_atom::Moov::decode(&mut cursor).unwrap(); let init = &data[..cursor.position() as usize]; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); import.decode(init).unwrap(); import.finish().unwrap(); @@ -130,7 +130,7 @@ fn aac_without_decoder_specific_info_is_rejected() { moov.encode(&mut init).unwrap(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); let err = fmp4.decode(&init).unwrap_err(); @@ -169,7 +169,7 @@ fn aac_decoder_specific_info_is_preserved() { fn dropping_import_retires_catalog_renditions() { let data = include_bytes!("test_data/bbb.mp4"); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); { let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); @@ -313,7 +313,7 @@ async fn test_seek_sets_initial_sequence() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let broadcast_consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); let data = include_bytes!("test_data/bbb.mp4"); @@ -379,7 +379,7 @@ async fn test_msf_catalog_roundtrip() { // Take the consumer before adding tracks; track() is called after the // MSF catalog track has been created by `catalog::Producer::new`. let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); let data = include_bytes!("test_data/bbb.mp4"); @@ -427,7 +427,7 @@ async fn test_msf_catalog_roundtrip() { async fn import_populates_the_broadcast_timeline() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); let data = include_bytes!("test_data/bbb.mp4"); @@ -664,7 +664,7 @@ async fn segmented_source_groups_per_segment() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); fmp4.decode(&init).unwrap(); @@ -715,7 +715,7 @@ async fn segmented_source_indexes_one_group_range_per_track() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); fmp4.decode(&init).unwrap(); @@ -797,7 +797,7 @@ async fn segment_ranges_with_skew( let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); fmp4.decode(&init).unwrap(); @@ -904,7 +904,7 @@ async fn a_single_leading_styp_still_segments_on_keyframes() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); fmp4.decode(&init).unwrap(); @@ -991,7 +991,7 @@ fn non_advancing_fragment_decode_time_is_rejected() { moov.encode(&mut init).unwrap(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); fmp4.decode(&init).unwrap(); @@ -1023,7 +1023,7 @@ fn seek_resets_fragment_decode_time() { ftyp.encode(&mut init).unwrap(); moov.encode(&mut init).unwrap(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); fmp4.decode(&init).unwrap(); fmp4.decode(&audio_fragment(4096, 1024, 327)).unwrap(); @@ -1039,7 +1039,7 @@ fn rejected_fragment_preserves_decode_time() { ftyp.encode(&mut init).unwrap(); moov.encode(&mut init).unwrap(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); fmp4.decode(&init).unwrap(); fmp4.decode(&audio_fragment(4096, 1024, 327)).unwrap(); @@ -1094,7 +1094,7 @@ fn fragment_jitter_never_shrinks() { moov.encode(&mut init).unwrap(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); fmp4.decode(&init).unwrap(); @@ -1151,7 +1151,7 @@ fn fragment_jitter_uses_sample_endpoints() { ftyp.encode(&mut init).unwrap(); moov.encode(&mut init).unwrap(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); import.decode(&init).unwrap(); let moof = mp4_atom::Moof { diff --git a/rs/moq-mux/src/container/mkv/export_test.rs b/rs/moq-mux/src/container/mkv/export_test.rs index f4d11be155..95a7fe72c3 100644 --- a/rs/moq-mux/src/container/mkv/export_test.rs +++ b/rs/moq-mux/src/container/mkv/export_test.rs @@ -32,7 +32,7 @@ async fn export_header_roundtrip_vp9_opus() { let mut producer = broadcast.produce(); let consumer = producer.consume(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = crate::container::mkv::Import::new(producer, catalog.reserve()); let buf = bytes::BytesMut::from(import_bytes.as_slice()); importer.decode(&buf).unwrap(); @@ -135,7 +135,7 @@ async fn export_header_roundtrip_vp9_opus() { // Verify the round-trip by re-importing the header (a header alone is enough // to populate the catalog). let mut broadcast2 = moq_net::broadcast::Info::new().produce(); - let catalog2 = crate::catalog::Producer::new(&mut broadcast2).unwrap(); + let catalog2 = crate::catalog::Producer::new(&mut broadcast2, crate::catalog::Config::default()).unwrap(); let mut importer2 = crate::container::mkv::Import::new(broadcast2, catalog2.reserve()); let hbuf = bytes::BytesMut::from(header.as_ref()); importer2.decode(&hbuf).unwrap(); @@ -212,7 +212,7 @@ async fn export_header_roundtrip_mp3() { let mut producer = broadcast.produce(); let consumer = producer.consume(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = crate::container::mkv::Import::new(producer, catalog.reserve()); importer .decode(&bytes::BytesMut::from(import_bytes.as_slice())) @@ -232,7 +232,7 @@ async fn export_header_roundtrip_mp3() { // Re-import the exported header and confirm the codec rebuilds. let mut broadcast2 = moq_net::broadcast::Info::new().produce(); - let catalog2 = crate::catalog::Producer::new(&mut broadcast2).unwrap(); + let catalog2 = crate::catalog::Producer::new(&mut broadcast2, crate::catalog::Config::default()).unwrap(); let mut importer2 = crate::container::mkv::Import::new(broadcast2, catalog2.reserve()); importer2.decode(&bytes::BytesMut::from(header.as_ref())).unwrap(); @@ -296,7 +296,7 @@ async fn export_waits_for_catalog_before_header() { // The catalog track exists (so the subscriber can attach) but no renditions // have been published yet: `tracks` stays empty on the first polls. - let _catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let _catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let catalog_stream = crate::catalog::Consumer::<()>::new(&consumer, crate::catalog::CatalogFormat::Hang) .await @@ -345,7 +345,7 @@ async fn export_derives_video_geometry_before_header() { .expect("expected header bytes"); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut importer = crate::container::mkv::Import::new(broadcast, catalog.reserve()); importer.decode(&bytes::BytesMut::from(header.as_ref())).unwrap(); let video = catalog.snapshot().video.renditions.values().next().unwrap().clone(); @@ -364,7 +364,7 @@ async fn export_emits_blocks_for_each_frame() { let mut producer = broadcast.produce(); let consumer = producer.consume(); - let catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = crate::container::mkv::Import::new(producer, catalog.reserve()); let buf = bytes::BytesMut::from(import_bytes.as_slice()); importer.decode(&buf).unwrap(); @@ -414,7 +414,7 @@ async fn export_emits_blocks_for_each_frame() { // Round-trip verification: feed the exported bytes back through the importer // and check the catalog repopulates with the same codecs. let mut bcast2 = moq_net::broadcast::Info::new().produce(); - let cat2 = crate::catalog::Producer::new(&mut bcast2).unwrap(); + let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = crate::container::mkv::Import::new(bcast2, cat2.reserve()); let rt = bytes::BytesMut::from(exported.as_slice()); imp2.decode(&rt).unwrap(); @@ -578,7 +578,7 @@ async fn export_avc3_source_synthesizes_avcc_and_length_prefixes() { // mistakes in the avcC layout that the slot-by-slot check above might // pass even when the record as a whole is malformed. let mut bcast2 = moq_net::broadcast::Info::new().produce(); - let cat2 = crate::catalog::Producer::new(&mut bcast2).unwrap(); + let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = crate::container::mkv::Import::new(bcast2, cat2.reserve()); let rt = bytes::BytesMut::from(exported.as_slice()); imp2.decode(&rt).unwrap(); @@ -604,7 +604,7 @@ async fn export_fragment_duration_batches_blocks() { let mut producer = broadcast.produce(); let consumer = producer.consume(); - let mut catalog = crate::catalog::Producer::new(&mut producer).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = crate::container::mkv::Import::new(producer, catalog.reserve()); let buf = bytes::BytesMut::from(import_bytes.as_slice()); importer.decode(&buf).unwrap(); diff --git a/rs/moq-mux/src/container/mkv/import_test.rs b/rs/moq-mux/src/container/mkv/import_test.rs index 1b573e4a08..d102e937a2 100644 --- a/rs/moq-mux/src/container/mkv/import_test.rs +++ b/rs/moq-mux/src/container/mkv/import_test.rs @@ -180,7 +180,7 @@ fn track_entry_video_vp9(number: u64, width: u64, height: u64) -> MatroskaSpec { fn run(data: &[u8]) -> crate::catalog::hang::Catalog { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut mkv = crate::container::mkv::Import::new(broadcast, catalog.reserve()); let buf = bytes::BytesMut::from(data); mkv.decode(&buf).expect("decode"); @@ -248,7 +248,7 @@ async fn public_container_preserves_loc_for_mkv() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut import = super::Import::new(broadcast, reserved).with_container(hang::catalog::Container::Loc); import.decode(&data).unwrap(); @@ -348,7 +348,7 @@ fn test_chunked_decode_dedup() { .build(); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut mkv = crate::container::mkv::Import::new(broadcast, catalog.reserve()); // Feed in 16-byte chunks to stress the chunked-restart code path. @@ -432,7 +432,7 @@ fn rendition_is_not_published_when_the_media_producer_fails() { assert_eq!(run(&data).video.renditions.len(), 1, "fixture must publish a rendition"); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); // Squat the broadcast's timeline track, so building the media producer (which creates it // on first use) fails. The handle must stay alive: the broadcast tracks names weakly, so diff --git a/rs/moq-mux/src/container/test_util.rs b/rs/moq-mux/src/container/test_util.rs index 0b16868deb..c95462d376 100644 --- a/rs/moq-mux/src/container/test_util.rs +++ b/rs/moq-mux/src/container/test_util.rs @@ -18,7 +18,7 @@ impl Live { pub(crate) fn new(name: &str, insert: impl FnOnce(&mut crate::catalog::Producer, String)) -> Self { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track( broadcast.unique_name(name), diff --git a/rs/moq-mux/src/container/ts/catalog.rs b/rs/moq-mux/src/container/ts/catalog.rs index 64f14c50cb..2b368ca245 100644 --- a/rs/moq-mux/src/container/ts/catalog.rs +++ b/rs/moq-mux/src/container/ts/catalog.rs @@ -429,18 +429,20 @@ mod test { use crate::catalog::Stream as _; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut producer = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut producer = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let mut guard = producer.modify().unwrap(); - guard.mpegts.program = Some(Program { + guard.ext.mpegts.program = Some(Program { transport_stream_id: 0x1234, program_number: 1, pmt_pid: 0x0064, ..Default::default() }); - guard.mpegts.tracks.insert( + guard.ext.mpegts.tracks.insert( ".ts".to_string(), Track { pid: 0x0102, @@ -451,7 +453,7 @@ mod test { verbatim: Some(Verbatim::new(0x86, Framing::Section)), }, ); - let expected = guard.mpegts.clone(); + let expected = guard.ext.mpegts.clone(); drop(guard); let mut consumer = @@ -460,6 +462,9 @@ mod test { .unwrap(); let catalog = consumer.next().await.unwrap().expect("catalog published"); - assert_eq!(catalog.mpegts, expected, "the mpegts section survives the MSF track"); + assert_eq!( + catalog.ext.mpegts, expected, + "the mpegts section survives the MSF track" + ); } } diff --git a/rs/moq-mux/src/container/ts/export.rs b/rs/moq-mux/src/container/ts/export.rs index 39770990ea..e9d89aaae0 100644 --- a/rs/moq-mux/src/container/ts/export.rs +++ b/rs/moq-mux/src/container/ts/export.rs @@ -708,7 +708,7 @@ impl Export { // The MPEG-TS section lives in the extension. The trait only exposes // `mpegts_mut`, and this snapshot is owned, so clone it out (`()` yields the // empty default: no verbatim streams, no preserved PIDs/descriptors). - let mpegts = catalog.mpegts_mut().cloned().unwrap_or_default(); + let mpegts = catalog.ext.mpegts_mut().cloned().unwrap_or_default(); self.program_descriptors = mpegts.program_descriptors.clone(); self.program = mpegts.program.clone(); diff --git a/rs/moq-mux/src/container/ts/export_test.rs b/rs/moq-mux/src/container/ts/export_test.rs index 4679b92e14..96b8afe8f0 100644 --- a/rs/moq-mux/src/container/ts/export_test.rs +++ b/rs/moq-mux/src/container/ts/export_test.rs @@ -106,7 +106,7 @@ fn assert_packet_aligned(ts: &[u8]) { async fn export_aac_roundtrip() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track( @@ -221,7 +221,7 @@ fn collect_pes_pts(ts: &[u8]) -> (Vec, Vec) { async fn export_lead_audio() -> BytesMut { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); // In-band avc3 video (SPS/PPS inline on keyframes; no out-of-band description). let vtrack = broadcast @@ -372,7 +372,7 @@ fn reassemble_video(ts: &[u8], expected_stream_type: StreamType) -> Vec { async fn export_avc3_in_band_reassembles() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track( @@ -423,7 +423,7 @@ async fn export_avc3_in_band_reassembles() { async fn export_avc3_preserves_multiple_pps() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track( @@ -474,7 +474,7 @@ async fn export_avc3_preserves_multiple_pps() { async fn export_avc1_out_of_band_reassembles() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let avcc = crate::codec::h264::build_avcc(&[Bytes::from_static(SPS)], &[Bytes::from_static(PPS)]).unwrap(); @@ -549,7 +549,7 @@ async fn export_import_h265_keeps_suffix_sei_on_its_picture() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track( @@ -591,7 +591,7 @@ async fn export_import_h265_keeps_suffix_sei_on_its_picture() { // Import: the same TS back through the demuxer must rebuild the same access units. let mut imported = moq_net::broadcast::Info::new().produce(); let imported_consumer = imported.consume(); - let import_catalog = crate::catalog::Producer::new(&mut imported).unwrap(); + let import_catalog = crate::catalog::Producer::new(&mut imported, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(imported, import_catalog.reserve()); import.decode(&ts).unwrap(); import.finish().unwrap(); @@ -618,7 +618,7 @@ async fn export_bframe_video_authors_dts() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); import.finish().unwrap(); @@ -685,7 +685,7 @@ async fn export_pcr_is_a_uniform_ramp() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); import.finish().unwrap(); @@ -744,7 +744,7 @@ async fn export_pcr_is_a_uniform_ramp() { async fn export_pcr_wraps_below_the_reserve_at_start() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track( @@ -835,7 +835,7 @@ async fn export_pcr_wraps_below_the_reserve_at_start() { async fn export_pcr_respects_every_renditions_reserve() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let avcc = crate::codec::h264::build_avcc(&[Bytes::from_static(SPS)], &[Bytes::from_static(PPS)]).unwrap(); let mut make = |name: &str, jitter: Option| { @@ -908,7 +908,7 @@ async fn export_pcr_respects_every_renditions_reserve() { async fn export_pcr_backfills_a_coarse_cadence() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let avcc = crate::codec::h264::build_avcc(&[Bytes::from_static(SPS)], &[Bytes::from_static(PPS)]).unwrap(); let track = broadcast @@ -980,9 +980,11 @@ async fn export_scte35_roundtrip() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); // Create and write the SCTE-35 cue track BEFORE moving `broadcast` into // `Import` (which consumes it); the producer stays alive so the exporter can @@ -997,7 +999,13 @@ async fn export_scte35_roundtrip() { descriptors: Vec::new(), verbatim: Some(tscat::Verbatim::new(0x86, tscat::Framing::Section)), }; - catalog.modify().unwrap().mpegts.tracks.insert(scte_name.clone(), track); + catalog + .modify() + .unwrap() + .ext + .mpegts + .tracks + .insert(scte_name.clone(), track); } let mut scte_producer = Producer::new(scte, HangContainer::Legacy(crate::container::Kind::Data)); // bbb's first video keyframe is at 1.4 s; stamp the cue just after it so it survives @@ -1053,15 +1061,23 @@ async fn export_scte35_roundtrip() { // Re-import the exported TS and read the .scte35 frame back. let mut broadcast2 = moq_net::broadcast::Info::new().produce(); let consumer2 = broadcast2.consume(); - let catalog2 = - crate::catalog::Producer::with_catalog(&mut broadcast2, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let catalog2 = crate::catalog::Producer::new( + &mut broadcast2, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); import2.finish().unwrap(); let snapshot = catalog2.snapshot(); - let verbatim = snapshot.mpegts.tracks.values().filter(|t| t.verbatim.is_some()).count(); + let verbatim = snapshot + .ext + .mpegts + .tracks + .values() + .filter(|t| t.verbatim.is_some()) + .count(); assert_eq!(verbatim, 1, "round-trip lost the SCTE-35 track"); let name = scte_track(&snapshot).expect("a scte35 track"); @@ -1099,9 +1115,11 @@ async fn export_pes_verbatim_roundtrip() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); // Build the verbatim PES track BEFORE moving `broadcast` into `Import`; the // producer stays alive so the exporter can subscribe to the retained track. @@ -1114,7 +1132,13 @@ async fn export_pes_verbatim_roundtrip() { verbatim.stream_id = Some(STREAM_ID); let mut track = tscat::Track::new(DATA_PID); track.verbatim = Some(verbatim); - catalog.modify().unwrap().mpegts.tracks.insert(data_name.clone(), track); + catalog + .modify() + .unwrap() + .ext + .mpegts + .tracks + .insert(data_name.clone(), track); } let mut data_producer = Producer::new(data_track, HangContainer::Legacy(crate::container::Kind::Data)); // bbb's first video keyframe is at 1.4 s; stamp the PES just after it so it survives @@ -1147,15 +1171,18 @@ async fn export_pes_verbatim_roundtrip() { // Re-import the exported TS and recover the verbatim PES stream. let mut broadcast2 = moq_net::broadcast::Info::new().produce(); let consumer2 = broadcast2.consume(); - let catalog2 = - crate::catalog::Producer::with_catalog(&mut broadcast2, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let catalog2 = crate::catalog::Producer::new( + &mut broadcast2, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); import2.finish().unwrap(); let snapshot = catalog2.snapshot(); let (name, track) = snapshot + .ext .mpegts .tracks .iter() @@ -1192,9 +1219,11 @@ async fn export_pes_verbatim_roundtrip() { async fn scte35_without_video_export_is_rejected() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); // A SCTE-35 cue track and nothing else. let scte = broadcast @@ -1207,7 +1236,7 @@ async fn scte35_without_video_export_is_rejected() { descriptors: Vec::new(), verbatim: Some(tscat::Verbatim::new(0x86, tscat::Framing::Section)), }; - catalog.modify().unwrap().mpegts.tracks.insert(scte_name, track); + catalog.modify().unwrap().ext.mpegts.tracks.insert(scte_name, track); } let mut producer = Producer::new(scte, HangContainer::Legacy(crate::container::Kind::Data)); producer @@ -1266,7 +1295,7 @@ async fn mp2_kyrion_roundtrip_byte_exact() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); import.finish().unwrap(); @@ -1302,7 +1331,7 @@ async fn mp2_kyrion_roundtrip_byte_exact() { let mut broadcast2 = moq_net::broadcast::Info::new().produce(); let consumer2 = broadcast2.consume(); - let catalog2 = crate::catalog::Producer::new(&mut broadcast2).unwrap(); + let catalog2 = crate::catalog::Producer::new(&mut broadcast2, crate::catalog::Config::default()).unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); import2.finish().unwrap(); @@ -1335,7 +1364,7 @@ async fn ac3_roundtrip_byte_exact() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); import.finish().unwrap(); @@ -1379,7 +1408,7 @@ async fn ac3_roundtrip_byte_exact() { let mut broadcast2 = moq_net::broadcast::Info::new().produce(); let consumer2 = broadcast2.consume(); - let catalog2 = crate::catalog::Producer::new(&mut broadcast2).unwrap(); + let catalog2 = crate::catalog::Producer::new(&mut broadcast2, crate::catalog::Config::default()).unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); import2.finish().unwrap(); @@ -1405,7 +1434,7 @@ async fn eac3_roundtrip_byte_exact() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); import.finish().unwrap(); @@ -1452,7 +1481,7 @@ async fn eac3_roundtrip_byte_exact() { let mut broadcast2 = moq_net::broadcast::Info::new().produce(); let consumer2 = broadcast2.consume(); - let catalog2 = crate::catalog::Producer::new(&mut broadcast2).unwrap(); + let catalog2 = crate::catalog::Producer::new(&mut broadcast2, crate::catalog::Config::default()).unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); import2.finish().unwrap(); @@ -1492,7 +1521,7 @@ async fn kyrion_ac3_mp2_roundtrip_byte_exact() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); import.finish().unwrap(); @@ -1540,7 +1569,7 @@ async fn kyrion_ac3_mp2_roundtrip_byte_exact() { let mut broadcast2 = moq_net::broadcast::Info::new().produce(); let consumer2 = broadcast2.consume(); - let catalog2 = crate::catalog::Producer::new(&mut broadcast2).unwrap(); + let catalog2 = crate::catalog::Producer::new(&mut broadcast2, crate::catalog::Config::default()).unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); import2.finish().unwrap(); @@ -1552,7 +1581,8 @@ async fn kyrion_ac3_mp2_roundtrip_byte_exact() { /// Find the SCTE-35 verbatim stream (stream_type 0x86) in a catalog snapshot. A /// clip may carry other undecoded streams verbatim, so select by type, not order. fn scte_track(snap: &crate::catalog::hang::Catalog) -> Option { - snap.mpegts + snap.ext + .mpegts .tracks .iter() .find(|(_, t)| t.verbatim.as_ref().is_some_and(|v| v.stream_type == 0x86)) @@ -1634,9 +1664,9 @@ async fn scte35_fixtures_survive_roundtrip() { // Ingest the fixture. let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::with_catalog( + let catalog = crate::catalog::Producer::new( &mut broadcast, - crate::catalog::hang::Catalog::::default(), + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), ) .unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); @@ -1690,9 +1720,9 @@ async fn scte35_fixtures_survive_roundtrip() { let mut broadcast2 = moq_net::broadcast::Info::new().produce(); let consumer2 = broadcast2.consume(); - let catalog2 = crate::catalog::Producer::with_catalog( + let catalog2 = crate::catalog::Producer::new( &mut broadcast2, - crate::catalog::hang::Catalog::::default(), + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), ) .unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); @@ -1869,19 +1899,21 @@ async fn service_layer_survives_roundtrip() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&input[..])).unwrap(); import.finish().unwrap(); let snapshot = catalog.snapshot(); - let program = snapshot.mpegts.program.clone().expect("a program record"); + let program = snapshot.ext.mpegts.program.clone().expect("a program record"); assert_eq!(program.transport_stream_id, 1, "TSID captured from the PAT"); assert_eq!(program.program_number, 1, "program number captured from the PAT"); assert_eq!(program.pmt_pid, 0x1000, "original PMT PID captured from the PAT"); - let si = snapshot.mpegts.si.clone(); + let si = snapshot.ext.mpegts.si.clone(); let entry = |pid: u16, table_id: u8| { si.get(&pid) .and_then(|tables| tables.get(&table_id)) @@ -1949,15 +1981,18 @@ async fn service_layer_survives_roundtrip() { // Re-import: the SDT and NIT must come back byte-for-byte. let mut broadcast2 = moq_net::broadcast::Info::new().produce(); let consumer2 = broadcast2.consume(); - let catalog2 = - crate::catalog::Producer::with_catalog(&mut broadcast2, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let catalog2 = crate::catalog::Producer::new( + &mut broadcast2, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); import2.finish().unwrap(); let snapshot2 = catalog2.snapshot(); let program2 = snapshot2 + .ext .mpegts .program .clone() @@ -1971,14 +2006,14 @@ async fn service_layer_survives_roundtrip() { "program number survived" ); assert_eq!(program2.pmt_pid, program.pmt_pid, "PMT PID survived"); - assert_eq!(snapshot2.mpegts.si, si, "every SI entry survived the round-trip"); + assert_eq!(snapshot2.ext.mpegts.si, si, "every SI entry survived the round-trip"); assert_eq!( - read_si_sections(&consumer2, &snapshot2.mpegts.si[&0x0011][&0x42].track).await, + read_si_sections(&consumer2, &snapshot2.ext.mpegts.si[&0x0011][&0x42].track).await, vec![sdt], "the SDT survived byte-for-byte" ); assert_eq!( - read_si_sections(&consumer2, &snapshot2.mpegts.si[&0x0010][&0x40].track).await, + read_si_sections(&consumer2, &snapshot2.ext.mpegts.si[&0x0010][&0x40].track).await, vec![Bytes::from(nit)], "the NIT survived byte-for-byte" ); @@ -1993,9 +2028,11 @@ async fn service_layer_survives_roundtrip() { async fn si_pids_are_re_emitted_on_their_own_interval() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let avcc = crate::codec::h264::build_avcc(&[Bytes::from_static(SPS)], &[Bytes::from_static(PPS)]).unwrap(); let track = broadcast @@ -2035,7 +2072,7 @@ async fn si_pids_are_re_emitted_on_their_own_interval() { guard.video.renditions.insert(name.clone(), cfg); // SDT every 2s, NIT every 10s: the DVB maxima import fills in. - guard.mpegts.si.entry(0x0011).or_default().insert( + guard.ext.mpegts.si.entry(0x0011).or_default().insert( 0x42, tscat::SiEntry { track: "0x0011-0x42.si".to_string(), @@ -2043,7 +2080,7 @@ async fn si_pids_are_re_emitted_on_their_own_interval() { ..Default::default() }, ); - guard.mpegts.si.entry(0x0010).or_default().insert( + guard.ext.mpegts.si.entry(0x0010).or_default().insert( 0x40, tscat::SiEntry { track: "0x0010-0x40.si".to_string(), @@ -2116,9 +2153,11 @@ fn count_discontinuity(frames: &[Frame]) -> usize { async fn export_rejects_si_pid_on_the_pmt() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let track = broadcast .create_track( @@ -2132,13 +2171,13 @@ async fn export_rejects_si_pid_on_the_pmt() { cfg.container = Container::Legacy; let mut guard = catalog.modify().unwrap(); guard.audio.renditions.insert(name, cfg); - guard.mpegts.program = Some(tscat::Program { + guard.ext.mpegts.program = Some(tscat::Program { transport_stream_id: 1, program_number: 1, pmt_pid: 0x1000, ..Default::default() }); - guard.mpegts.si.entry(0x1000).or_default().insert( + guard.ext.mpegts.si.entry(0x1000).or_default().insert( 0x42, tscat::SiEntry { track: "si".to_string(), @@ -2168,9 +2207,11 @@ async fn export_rejects_si_pid_on_the_pmt() { async fn export_rejects_si_pid_on_an_elementary_stream() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let track = broadcast .create_track( @@ -2184,8 +2225,8 @@ async fn export_rejects_si_pid_on_an_elementary_stream() { cfg.container = Container::Legacy; let mut guard = catalog.modify().unwrap(); guard.audio.renditions.insert(name.clone(), cfg); - guard.mpegts.tracks.insert(name, tscat::Track::new(0x101)); - guard.mpegts.si.entry(0x101).or_default().insert( + guard.ext.mpegts.tracks.insert(name, tscat::Track::new(0x101)); + guard.ext.mpegts.si.entry(0x101).or_default().insert( 0x42, tscat::SiEntry { track: "si".to_string(), @@ -2229,14 +2270,22 @@ fn publish_sdt( Bytes::from(make_long_section(0x42, 1, 0, 0, 0, &[0xaa; 8])), ) .unwrap(); - catalog.modify().unwrap().mpegts.si.entry(0x0011).or_default().insert( - 0x42, - tscat::SiEntry { - track: track.name().to_string(), - interval: Some(Duration::from_secs(2)), - ..Default::default() - }, - ); + catalog + .modify() + .unwrap() + .ext + .mpegts + .si + .entry(0x0011) + .or_default() + .insert( + 0x42, + tscat::SiEntry { + track: track.name().to_string(), + interval: Some(Duration::from_secs(2)), + ..Default::default() + }, + ); track } @@ -2247,9 +2296,11 @@ fn publish_sdt( async fn discontinuity_re_emits_tables_and_resumes_the_clock() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let track = broadcast .create_track( @@ -2352,9 +2403,11 @@ async fn discontinuity_re_emits_tables_and_resumes_the_clock() { async fn reordered_video_keeps_the_table_cadence() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let avcc = crate::codec::h264::build_avcc(&[Bytes::from_static(SPS)], &[Bytes::from_static(PPS)]).unwrap(); let track = broadcast @@ -2421,9 +2474,11 @@ async fn reordered_video_keeps_the_table_cadence() { async fn discontinuity_flags_the_break_once_across_tracks() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let avcc = crate::codec::h264::build_avcc(&[Bytes::from_static(SPS)], &[Bytes::from_static(PPS)]).unwrap(); let video_track = broadcast @@ -2600,14 +2655,16 @@ async fn multi_packet_si_section_is_captured() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&input[..])).unwrap(); import.finish().unwrap(); - let si = catalog.snapshot().mpegts.si.clone(); + let si = catalog.snapshot().ext.mpegts.si.clone(); let entry = si .get(&0x0011) .and_then(|tables| tables.get(&0x42)) @@ -2630,9 +2687,11 @@ struct SiRig { fn si_rig() -> SiRig { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let import = crate::container::ts::Import::new(broadcast, catalog.reserve()); SiRig { import, @@ -2663,7 +2722,7 @@ async fn torn_transition_is_never_published() { .unwrap(); rig.import.finish().unwrap(); - let si = rig.catalog.snapshot().mpegts.si.clone(); + let si = rig.catalog.snapshot().ext.mpegts.si.clone(); let track = &si[&0x0011][&0x42].track; let groups = read_si_groups(&rig.consumer, track).await; assert!(!groups.is_empty(), "at least one snapshot group"); @@ -2705,7 +2764,7 @@ async fn eit_now_next_and_schedule_are_captured() { rig.import.decode(&BytesMut::from(&input[..])).unwrap(); rig.import.finish().unwrap(); - let si = rig.catalog.snapshot().mpegts.si.clone(); + let si = rig.catalog.snapshot().ext.mpegts.si.clone(); let eit = si.get(&0x0012).expect("EIT entries"); let pf = eit.get(&0x4E).expect("a now/next entry"); @@ -2743,7 +2802,7 @@ async fn sdt_other_networks_do_not_collide() { rig.import.decode(&BytesMut::from(&input[..])).unwrap(); rig.import.finish().unwrap(); - let si = rig.catalog.snapshot().mpegts.si.clone(); + let si = rig.catalog.snapshot().ext.mpegts.si.clone(); let entry = &si[&0x0011][&0x46]; assert_eq!( read_si_sections(&rig.consumer, &entry.track).await, @@ -2770,7 +2829,7 @@ async fn next_version_sections_are_dropped() { rig.import.decode(&BytesMut::from(&input[..])).unwrap(); rig.import.finish().unwrap(); - let si = rig.catalog.snapshot().mpegts.si.clone(); + let si = rig.catalog.snapshot().ext.mpegts.si.clone(); assert!(si.contains_key(&0x0010), "the current NIT was captured (control)"); assert!(!si.contains_key(&0x0011), "a next-version section creates no entry"); } @@ -2796,7 +2855,7 @@ async fn tdt_round_trips_as_latest_value() { .unwrap(); rig.import.finish().unwrap(); - let si = rig.catalog.snapshot().mpegts.si.clone(); + let si = rig.catalog.snapshot().ext.mpegts.si.clone(); assert!(si.contains_key(&0x0011), "the SDT was captured (control)"); let entry = si .get(&0x0014) @@ -2836,13 +2895,13 @@ async fn abort_removes_si_catalog_entries() { let mut rig = si_rig(); rig.import.decode(&BytesMut::from(&input[..])).unwrap(); assert!( - !rig.catalog.snapshot().mpegts.si.is_empty(), + !rig.catalog.snapshot().ext.mpegts.si.is_empty(), "the SDT entry was advertised" ); rig.import.abort(moq_net::Error::Cancel); assert!( - rig.catalog.snapshot().mpegts.si.is_empty(), + rig.catalog.snapshot().ext.mpegts.si.is_empty(), "abort removed the advertised entries" ); } @@ -2862,7 +2921,7 @@ async fn short_form_repetition_cuts_no_group() { .unwrap(); rig.import.finish().unwrap(); - let si = rig.catalog.snapshot().mpegts.si.clone(); + let si = rig.catalog.snapshot().ext.mpegts.si.clone(); let groups = read_si_groups(&rig.consumer, &si[&0x0014][&0x70].track).await; assert_eq!(groups.len(), 1, "a repetition cut no further group"); } @@ -2874,9 +2933,11 @@ async fn short_form_repetition_cuts_no_group() { async fn stale_si_entry_does_not_block_output() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); // A track that exists (the subscription resolves) but never produces a group. let ghost = broadcast.create_track("ghost.si", None).unwrap(); @@ -2900,7 +2961,7 @@ async fn stale_si_entry_does_not_block_output() { cfg.container = Container::Legacy; cfg.description = Some(avcc); guard.video.renditions.insert(name.clone(), cfg); - guard.mpegts.si.entry(0x0011).or_default().insert( + guard.ext.mpegts.si.entry(0x0011).or_default().insert( 0x42, tscat::SiEntry { track: "ghost.si".to_string(), @@ -2984,7 +3045,7 @@ fn strip_opus_control(mut data: &[u8]) -> Vec> { async fn export_opus_roundtrip() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track( @@ -3064,7 +3125,7 @@ async fn export_opus_roundtrip() { async fn opus_export_import_roundtrip() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track( @@ -3099,7 +3160,7 @@ async fn opus_export_import_roundtrip() { // Re-import the TS we just produced. let mut imported = moq_net::broadcast::Info::new().produce(); let imported_consumer = imported.consume(); - let import_catalog = crate::catalog::Producer::new(&mut imported).unwrap(); + let import_catalog = crate::catalog::Producer::new(&mut imported, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(imported, import_catalog.reserve()); import.decode(&ts).unwrap(); import.finish().unwrap(); @@ -3147,7 +3208,7 @@ const JOIN: u64 = 75; async fn export_twice(with_video: bool) -> (Vec, Vec) { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut video = with_video.then(|| { let track = broadcast @@ -3348,7 +3409,7 @@ async fn lost_dense_section_recovers_on_the_next_cycle() { rig.import.decode(&BytesMut::from(&input[..])).unwrap(); rig.import.finish().unwrap(); - let si = rig.catalog.snapshot().mpegts.si.clone(); + let si = rig.catalog.snapshot().ext.mpegts.si.clone(); assert_eq!( read_si_sections(&rig.consumer, &si[&0x0011][&0x42].track).await, vec![Bytes::from(s0), Bytes::from(s1), Bytes::from(s2)], @@ -3366,9 +3427,11 @@ async fn repointed_si_entry_resubscribes() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let mut track_a = broadcast.create_track("a.si", None).unwrap(); track_a @@ -3398,7 +3461,7 @@ async fn repointed_si_entry_resubscribes() { cfg.container = Container::Legacy; cfg.description = Some(avcc); guard.video.renditions.insert(name.clone(), cfg); - guard.mpegts.si.entry(0x0011).or_default().insert( + guard.ext.mpegts.si.entry(0x0011).or_default().insert( 0x42, tscat::SiEntry { track: "a.si".to_string(), @@ -3437,14 +3500,22 @@ async fn repointed_si_entry_resubscribes() { } // Repoint the entry at the replacement track. - catalog.modify().unwrap().mpegts.si.get_mut(&0x0011).unwrap().insert( - 0x42, - tscat::SiEntry { - track: "b.si".to_string(), - interval: Some(Duration::from_secs(2)), - ..Default::default() - }, - ); + catalog + .modify() + .unwrap() + .ext + .mpegts + .si + .get_mut(&0x0011) + .unwrap() + .insert( + 0x42, + tscat::SiEntry { + track: "b.si".to_string(), + interval: Some(Duration::from_secs(2)), + ..Default::default() + }, + ); // The repointed entry resubscribes through the origin, whose driver mints and // splices the new track between polls: give it the scheduler before the frames @@ -3489,9 +3560,11 @@ async fn si_revision_after_final_media_frame_is_flushed() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let mut si_track = broadcast.create_track("0x0011-0x42.si", None).unwrap(); si_track @@ -3517,7 +3590,7 @@ async fn si_revision_after_final_media_frame_is_flushed() { cfg.container = Container::Legacy; cfg.description = Some(avcc); guard.video.renditions.insert(name.clone(), cfg); - guard.mpegts.si.entry(0x0011).or_default().insert( + guard.ext.mpegts.si.entry(0x0011).or_default().insert( 0x42, tscat::SiEntry { track: "0x0011-0x42.si".to_string(), @@ -3595,9 +3668,11 @@ struct SiCadenceRig { async fn si_cadence_rig(pid: u16, table_id: u8, interval: Duration) -> SiCadenceRig { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let mut catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let si_name = format!("{pid:#06x}-{table_id:#04x}.si"); let si_track = broadcast.create_track(si_name.as_str(), None).unwrap(); @@ -3621,7 +3696,7 @@ async fn si_cadence_rig(pid: u16, table_id: u8, interval: Duration) -> SiCadence cfg.container = Container::Legacy; cfg.description = Some(avcc); guard.video.renditions.insert(name, cfg); - guard.mpegts.si.entry(pid).or_default().insert( + guard.ext.mpegts.si.entry(pid).or_default().insert( table_id, tscat::SiEntry { track: si_name, @@ -3800,6 +3875,7 @@ async fn si_anchor_survives_a_zero_interval_reorder() { rig.catalog .modify() .unwrap() + .ext .mpegts .si .get_mut(&0x0011) @@ -3864,9 +3940,11 @@ async fn overlapping_capture_teardown_keeps_the_survivors_mapping() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let _consumer = broadcast.consume(); - let catalog = - crate::catalog::Producer::with_catalog(&mut broadcast, crate::catalog::hang::Catalog::::default()) - .unwrap(); + let catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), + ) + .unwrap(); let mut old = crate::container::ts::Import::new(broadcast.clone(), catalog.reserve()); old.decode(&BytesMut::from(&input[..])).unwrap(); @@ -3874,12 +3952,12 @@ async fn overlapping_capture_teardown_keeps_the_survivors_mapping() { // name is taken, so it advertises under a fallback name, overwriting the key. let mut new = crate::container::ts::Import::new(broadcast, catalog.reserve()); new.decode(&BytesMut::from(&input[..])).unwrap(); - let survivor = catalog.snapshot().mpegts.si[&0x0011][&0x42].track.clone(); + let survivor = catalog.snapshot().ext.mpegts.si[&0x0011][&0x42].track.clone(); assert_ne!(survivor, "0x0011-0x42.si", "the replacement fell back to a unique name"); drop(old); assert_eq!( - catalog.snapshot().mpegts.si[&0x0011][&0x42].track, + catalog.snapshot().ext.mpegts.si[&0x0011][&0x42].track, survivor, "the old capture's teardown left the survivor's mapping in place" ); @@ -3905,7 +3983,7 @@ async fn contiguous_same_version_commit_replaces_stale_sections() { rig.import.decode(&BytesMut::from(&input[..])).unwrap(); rig.import.finish().unwrap(); - let si = rig.catalog.snapshot().mpegts.si.clone(); + let si = rig.catalog.snapshot().ext.mpegts.si.clone(); assert_eq!( read_si_sections(&rig.consumer, &si[&0x0011][&0x42].track).await, vec![Bytes::from(b0), Bytes::from(b1)], @@ -3926,7 +4004,7 @@ async fn debounce_opens_without_a_media_clock() { input.extend_from_slice(&si_packet_cc(0x0011, &sdt(0, 0xaa), 1)); rig.import.decode(&BytesMut::from(&input[..])).unwrap(); - let name = rig.catalog.snapshot().mpegts.si[&0x0011][&0x42].track.clone(); + let name = rig.catalog.snapshot().ext.mpegts.si[&0x0011][&0x42].track.clone(); let track = rig.consumer.track(&name).unwrap().subscribe(None).await.unwrap(); assert_eq!(track.latest(), Some(0), "the first snapshot cut immediately"); @@ -3971,7 +4049,7 @@ fn collect_pcrs(frames: &[Frame]) -> Vec<(usize, u64, u128)> { async fn export_cbr_video() -> Vec { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track( @@ -4094,7 +4172,7 @@ async fn pcr_stays_among_the_bytes_across_reordered_tracks() { let data = include_bytes!("test_data/scte35/kyrion_dirtystart.ts"); let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); import.finish().unwrap(); diff --git a/rs/moq-mux/src/container/ts/import.rs b/rs/moq-mux/src/container/ts/import.rs index 077f7206ab..2256f9ba8e 100644 --- a/rs/moq-mux/src/container/ts/import.rs +++ b/rs/moq-mux/src/container/ts/import.rs @@ -142,7 +142,7 @@ impl Import { // Sample the real catalog once at construction, not E::default(): an extension // may carry the section by value, and a snapshot clones under the mutex (no publish). let mut snapshot = catalog.snapshot(); - let supports_mpegts = snapshot.mpegts_mut().is_some(); + let supports_mpegts = snapshot.ext.mpegts_mut().is_some(); let si = super::si::Capture::new(broadcast.clone(), catalog.clone()); Self { broadcast, @@ -366,7 +366,7 @@ impl Import { // export re-emits them verbatim, including the original CUEI. if self.supports_mpegts && !self.program_recorded && !pmt.program_info.is_empty() { let program = to_descriptors(&pmt.program_info); - if let Some(mpegts) = self.catalog.modify()?.mpegts_mut() { + if let Some(mpegts) = self.catalog.modify()?.ext.mpegts_mut() { mpegts.program_descriptors = program; } self.program_recorded = true; @@ -741,7 +741,7 @@ impl Import { self.es_descriptors.get(&pid.as_u16()).cloned().unwrap_or_default(), ) }; - if let Some(mpegts) = self.catalog.modify()?.mpegts_mut() { + if let Some(mpegts) = self.catalog.modify()?.ext.mpegts_mut() { let entry = mpegts .tracks .entry(name) @@ -763,7 +763,7 @@ impl Import { let Some(entry) = pat.table.iter().find(|entry| entry.program_num != 0) else { return Ok(()); }; - if let Some(mpegts) = self.catalog.modify()?.mpegts_mut() { + if let Some(mpegts) = self.catalog.modify()?.ext.mpegts_mut() { let program = mpegts.program.get_or_insert_with(Default::default); program.transport_stream_id = pat.transport_stream_id; program.program_number = entry.program_num; @@ -974,7 +974,7 @@ fn register_verbatim( )?; let mut guard = catalog.modify()?; - let Some(mpegts) = guard.mpegts_mut() else { + let Some(mpegts) = guard.ext.mpegts_mut() else { // supports_mpegts was true when sampled at construction; None here means the // catalog dropped the section since. anyhow::bail!("catalog extension no longer carries an mpegts section"); @@ -998,7 +998,7 @@ fn unregister_verbatim(catalog: &mut crate::catalog::Produc let Ok(mut catalog) = catalog.modify() else { return; }; - if let Some(mpegts) = catalog.mpegts_mut() { + if let Some(mpegts) = catalog.ext.mpegts_mut() { mpegts.tracks.remove(name); } } @@ -1155,7 +1155,7 @@ impl VerbatimStream { // re-emits the stream under its real id (e.g. 0xBD for teletext/DVB AC-3). if !self.stream_id_recorded { let name = self.track.name().to_string(); - if let Some(mpegts) = self.entry.catalog.modify()?.mpegts_mut() + if let Some(mpegts) = self.entry.catalog.modify()?.ext.mpegts_mut() && let Some(verbatim) = mpegts.tracks.get_mut(&name).and_then(|t| t.verbatim.as_mut()) { verbatim.stream_id = Some(pending.stream_id); @@ -2752,7 +2752,7 @@ mod test { #[test] fn remapping_a_pid_keeps_its_retired_resync_stats() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pid = mpeg2ts::ts::Pid::new(0x61).unwrap(); let mut stream = import.legacy_stream(pid, &super::mp2::DESCRIPTOR); @@ -3011,7 +3011,11 @@ mod test { use crate::container::ts::catalog::Ext; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::with_catalog(&mut broadcast, Catalog::::default()).unwrap(); + let catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(Catalog::::default()), + ) + .unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let mut bytes = bytes::BytesMut::new(); @@ -3021,7 +3025,7 @@ mod test { import.finish().unwrap(); assert_eq!( - catalog.snapshot().mpegts.tracks.len(), + catalog.snapshot().ext.mpegts.tracks.len(), 1, "expected one scte35 rendition" ); @@ -3033,7 +3037,7 @@ mod test { #[tokio::test(start_paused = true)] async fn base_catalog_routes_cue_pid_to_ignored() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut updates = catalog.consume().unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); @@ -3078,7 +3082,11 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::with_catalog(&mut broadcast, Catalog::::default()).unwrap(); + let catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(Catalog::::default()), + ) + .unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); // First PMT lacks CUEI: the 0x86 PID is ambiguous and routes to Ignored. @@ -3104,14 +3112,14 @@ mod test { "upgrade drops the stale Ignored route" ); assert_eq!( - catalog.snapshot().mpegts.tracks.len(), + catalog.snapshot().ext.mpegts.tracks.len(), 1, "upgrade advertises the cue track" ); // The importer clears its verbatim entries from the catalog when it drops, so read the // track name while it is still registered. - let name = catalog.snapshot().mpegts.tracks.keys().next().unwrap().clone(); + let name = catalog.snapshot().ext.mpegts.tracks.keys().next().unwrap().clone(); import.finish().unwrap(); let track = consumer .track(&name) @@ -3167,7 +3175,7 @@ mod test { const PRIVATE_PID: u16 = 0x0051; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let mut bytes = bytes::BytesMut::new(); @@ -3239,7 +3247,7 @@ mod test { const VIDEO: u16 = 0x61; const OTHER: u16 = 0x62; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); import .decode(&synth_pmt( @@ -3289,7 +3297,7 @@ mod test { fn aac_pes_jitter_survives_bitrate_updates() { const PID: u16 = 0x60; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); import.decode(&synth_pmt(&[(StreamType::AdtsAac, PID)], false)).unwrap(); let mut frame = super::adts::write_header(2, 44_100, 2, 8).unwrap().to_vec(); @@ -3328,7 +3336,7 @@ mod test { fn aac_pes_jitter_counts_completed_split_frames() { const PID: u16 = 0x60; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); import.decode(&synth_pmt(&[(StreamType::AdtsAac, PID)], false)).unwrap(); let mut frame = super::adts::write_header(2, 44_100, 2, 8).unwrap().to_vec(); @@ -3406,7 +3414,7 @@ mod test { const PER_PES: u64 = 7; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let mut bytes = bytes::BytesMut::new(); @@ -3456,7 +3464,7 @@ mod test { const MP2_PID: u16 = 0x0061; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let mut bytes = bytes::BytesMut::new(); @@ -3550,7 +3558,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); import .decode(&bytes::BytesMut::from(&looped[..])) @@ -3569,7 +3577,7 @@ mod test { let pristine = { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); import.decode(&bytes::BytesMut::from(&data[..])).unwrap(); import.finish().unwrap(); @@ -3643,7 +3651,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::H264, VIDEO_PID)], false); @@ -3695,7 +3703,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false); @@ -3736,7 +3744,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false); @@ -3769,7 +3777,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false); @@ -3829,7 +3837,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false); @@ -3875,7 +3883,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false); @@ -3924,7 +3932,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -3962,7 +3970,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -4021,7 +4029,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -4076,7 +4084,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -4123,7 +4131,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -4170,7 +4178,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -4223,7 +4231,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -4271,7 +4279,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -4314,7 +4322,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -4362,7 +4370,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -4406,7 +4414,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt( @@ -4459,7 +4467,7 @@ mod test { const MP2_PID: u16 = 0x0061; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -4495,7 +4503,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -4545,7 +4553,7 @@ mod test { const AAC_PID: u16 = 0x0060; let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false); @@ -4580,7 +4588,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -4626,7 +4634,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false); @@ -4674,7 +4682,11 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::with_catalog(&mut broadcast, Catalog::::default()).unwrap(); + let catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(Catalog::::default()), + ) + .unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let mut bytes = bytes::BytesMut::new(); @@ -4691,7 +4703,7 @@ mod test { let clock = import.last_pts.expect("video set the media clock"); import.finish().unwrap(); - let name = catalog.snapshot().mpegts.tracks.keys().next().unwrap().clone(); + let name = catalog.snapshot().ext.mpegts.tracks.keys().next().unwrap().clone(); let track = consumer .track(&name) .unwrap() @@ -4730,7 +4742,11 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); // catalog::Ext (not the base catalog) makes a wrong ensure_scte() observable: it // would create a rendition, which the base catalog silently drops. - let catalog = crate::catalog::Producer::with_catalog(&mut broadcast, Catalog::::default()).unwrap(); + let catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(Catalog::::default()), + ) + .unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let mut bytes = bytes::BytesMut::new(); @@ -4751,7 +4767,7 @@ mod test { "video kept importing past the dropped section PID" ); assert!( - catalog.snapshot().mpegts.tracks.is_empty(), + catalog.snapshot().ext.mpegts.tracks.is_empty(), "a 0x86 PID without CUEI must not be cataloged" ); } @@ -4839,7 +4855,11 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::with_catalog(&mut broadcast, Catalog::::default()).unwrap(); + let catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(Catalog::::default()), + ) + .unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let mut bytes = bytes::BytesMut::new(); @@ -4857,8 +4877,12 @@ mod test { import.finish().unwrap(); let snap = catalog.snapshot(); - assert_eq!(snap.mpegts.tracks.len(), 1, "the private PES PID is carried verbatim"); - let (name, track) = snap.mpegts.tracks.iter().next().unwrap(); + assert_eq!( + snap.ext.mpegts.tracks.len(), + 1, + "the private PES PID is carried verbatim" + ); + let (name, track) = snap.ext.mpegts.tracks.iter().next().unwrap(); let verbatim = track.verbatim.as_ref().expect("a verbatim carriage record"); assert_eq!(verbatim.stream_type, 0x06, "recorded the PMT stream_type"); assert_eq!(verbatim.framing, Framing::Pes, "private PES is PES-framed"); @@ -4955,7 +4979,7 @@ mod test { fn two_stream_import() -> (moq_net::broadcast::Consumer, crate::catalog::Producer, super::Import) { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); let pmt = synth_pmt( &[(StreamType::Mpeg1Audio, PCR_PID), (StreamType::Mpeg1Audio, PEER_PID)], @@ -4993,7 +5017,11 @@ mod test { use crate::container::ts::catalog::Ext; let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::with_catalog(&mut broadcast, Catalog::::default()).unwrap(); + let catalog = crate::catalog::Producer::new( + &mut broadcast, + crate::catalog::Config::default().with_catalog(Catalog::::default()), + ) + .unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); import .decode( @@ -5013,7 +5041,7 @@ mod test { import.decode(clock_break_packet(PCR_PID).as_slice()).unwrap(); import.decode(packet(true, 2, 0, &CUE).as_slice()).unwrap(); import.finish().unwrap(); - let name = catalog.snapshot().mpegts.tracks.keys().next().unwrap().clone(); + let name = catalog.snapshot().ext.mpegts.tracks.keys().next().unwrap().clone(); let (frames, breaks) = read_breaks(&consumer, &name).await; assert_eq!(frames.len(), 3); assert_eq!( @@ -5271,7 +5299,7 @@ mod test { async fn export_discontinuities(data: &[u8]) -> usize { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); import.decode(&bytes::BytesMut::from(data)).unwrap(); import.finish().unwrap(); diff --git a/rs/moq-mux/src/container/ts/import_test.rs b/rs/moq-mux/src/container/ts/import_test.rs index fca6fa4c8d..69a92a6f37 100644 --- a/rs/moq-mux/src/container/ts/import_test.rs +++ b/rs/moq-mux/src/container/ts/import_test.rs @@ -16,7 +16,7 @@ const RECORDING_MAX_AGE: std::time::Duration = std::time::Duration::from_secs(30 /// Decode a whole TS buffer into a fresh broadcast and return the catalog. fn import_ts(data: &[u8]) -> crate::catalog::hang::Catalog { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); let buf = BytesMut::from(data); @@ -58,7 +58,7 @@ async fn public_container_preserves_loc_for_ts() { let data = include_bytes!("test_data/bbb.ts"); let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut import = super::Import::new(broadcast, reserved).with_container(hang::catalog::Container::Loc); import.decode(data).unwrap(); @@ -165,7 +165,7 @@ async fn import_opus_frames() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); import.finish().unwrap(); @@ -292,7 +292,7 @@ fn resyncs_across_chunk_boundaries() { misaligned.extend_from_slice(data); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); for chunk in misaligned.chunks(100) { import.decode(&BytesMut::from(chunk)).unwrap(); @@ -319,7 +319,7 @@ async fn import_export_import_roundtrip() { // Import the fixture into a broadcast. let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); let buf = BytesMut::from(&data[..]); import.decode(&buf).unwrap(); @@ -369,7 +369,7 @@ async fn survives_midstream_join() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import .decode(&BytesMut::from(&buf[..])) @@ -412,9 +412,10 @@ async fn kyrion_dirtystart_extracts_real_cues() { let data = include_bytes!("test_data/scte35/kyrion_dirtystart.ts"); let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = crate::catalog::Producer::with_catalog( + let catalog = crate::catalog::Producer::new( &mut broadcast, - crate::catalog::hang::Catalog::::default(), + crate::catalog::Config::default() + .with_catalog(crate::catalog::hang::Catalog::::default()), ) .unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); @@ -428,6 +429,7 @@ async fn kyrion_dirtystart_extracts_real_cues() { // Select the SCTE-35 stream by its verbatim stream_type; media tracks also appear // in mpegts.tracks now (with their PID + descriptors). let name = snap + .ext .mpegts .tracks .iter() @@ -472,7 +474,7 @@ fn import_handles_unaligned_chunks() { let data = include_bytes!("test_data/bbb.ts"); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); for chunk in data.chunks(100) { diff --git a/rs/moq-mux/src/container/ts/si.rs b/rs/moq-mux/src/container/ts/si.rs index 6470a3e564..eb934cc49b 100644 --- a/rs/moq-mux/src/container/ts/si.rs +++ b/rs/moq-mux/src/container/ts/si.rs @@ -370,7 +370,7 @@ impl Capture { /// the host clock ([`DEBOUNCE`]), and a first snapshot is never delayed. `force` /// overrides the debounce, for end of stream. pub fn flush(&mut self, pts: Timestamp, force: bool) -> anyhow::Result<()> { - let now = u128::from(self.clock.micros()); + let now = self.clock.now().as_micros(); for entry in self.entries.values_mut() { if !entry.dirty { continue; @@ -386,7 +386,7 @@ impl Capture { if self.entries.values().any(|e| !e.advertised && e.last_cut.is_some()) { let mut guard = self.catalog.modify()?; - let Some(mpegts) = guard.mpegts_mut() else { + let Some(mpegts) = guard.ext.mpegts_mut() else { anyhow::bail!("catalog extension no longer carries an mpegts section"); }; for ((pid, table_id), entry) in self.entries.iter_mut() { @@ -436,7 +436,7 @@ impl Capture { let Ok(mut guard) = self.catalog.modify() else { return; }; - let Some(mpegts) = guard.mpegts_mut() else { + let Some(mpegts) = guard.ext.mpegts_mut() else { return; }; for ((pid, table_id), entry) in self.entries.iter_mut() { diff --git a/rs/moq-mux/src/import/container.rs b/rs/moq-mux/src/import/container.rs index 8fe0a37dc8..a8194797c2 100644 --- a/rs/moq-mux/src/import/container.rs +++ b/rs/moq-mux/src/import/container.rs @@ -216,7 +216,7 @@ mod tests { let (head, tail) = data.split_at(100); let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut stream: ContainerStream = ContainerStream::new(broadcast, catalog.reserve(), ContainerFormat::Fmp4).unwrap(); diff --git a/rs/moq-mux/src/import/track.rs b/rs/moq-mux/src/import/track.rs index e9f07c7092..54365db005 100644 --- a/rs/moq-mux/src/import/track.rs +++ b/rs/moq-mux/src/import/track.rs @@ -780,7 +780,7 @@ mod tests { fn new_broadcast() -> (moq_net::broadcast::Producer, crate::catalog::Producer) { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); (broadcast, catalog) } diff --git a/rs/moq-mux/src/json.rs b/rs/moq-mux/src/json.rs index 9e5211e360..a49c8999f2 100644 --- a/rs/moq-mux/src/json.rs +++ b/rs/moq-mux/src/json.rs @@ -41,7 +41,8 @@ //! # source: &moq_mux::Source, //! # catalog: &moq_mux::catalog::hang::Catalog, //! # ) -> moq_mux::Result<()> { -//! let entry = catalog.json_track("chat").expect("no chat track"); +//! let config = catalog.json.tracks.get("chat").expect("no chat track"); +//! let entry = moq_mux::catalog::Entry::new("chat", config); //! let mut chat = entry.subscribe::(source).await?; //! while let Some(message) = chat.next().await? { //! // ... @@ -325,7 +326,7 @@ mod test { fn catalog() -> (moq_net::broadcast::Producer, crate::catalog::Producer) { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); (broadcast, catalog) } @@ -458,7 +459,8 @@ mod test { let mut seed = crate::catalog::hang::Catalog::<()>::default(); seed.json.tracks.insert("chat".to_string(), existing.clone()); - let catalog = crate::catalog::Producer::with_catalog(&mut broadcast, seed).unwrap(); + let config = crate::catalog::Config::default().with_catalog(seed); + let catalog = crate::catalog::Producer::new(&mut broadcast, config).unwrap(); // Nothing local holds the track name, so `create_track` alone would have let this through. assert!(matches!( @@ -481,14 +483,19 @@ mod test { .unwrap(); let snapshot = catalog.snapshot(); - let found: Vec<(&str, &Mode)> = snapshot.json_tracks().map(|t| (t.name(), &t.config().mode)).collect(); + let found: Vec<(&str, &Mode)> = snapshot + .json + .tracks + .iter() + .map(|(name, config)| (name.as_str(), &config.mode)) + .collect(); assert_eq!(found, vec![("chat", &Mode::Stream), ("status", &Mode::Snapshot)]); // A name the catalog doesn't list has no entry, rather than a config to misread. - assert!(snapshot.json_track("nope").is_none()); + assert!(!snapshot.json.tracks.contains_key("nope")); - // The entry derefs to its config, so the fields read directly. - let chat = snapshot.json_track("chat").expect("missing entry"); + // Pairing the map key with its config gives consumers one subscription handle. + let chat = crate::catalog::Entry::new("chat", snapshot.json.tracks.get("chat").expect("missing entry")); assert_eq!(chat.name(), "chat"); assert_eq!(chat.mode, Mode::Stream); } @@ -498,7 +505,7 @@ mod test { #[tokio::test] async fn an_entry_subscribes_and_reads() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let source = crate::source::announced(&broadcast.consume()); let mut chat = catalog @@ -507,7 +514,7 @@ mod test { chat.append(&json!({ "text": "hello" })).unwrap(); let snapshot = catalog.snapshot(); - let entry = snapshot.json_track("chat").expect("missing entry"); + let entry = crate::catalog::Entry::new("chat", snapshot.json.tracks.get("chat").expect("missing entry")); let mut consumer = entry.subscribe::(&source).await.unwrap(); chat.finish().unwrap(); diff --git a/rs/moq-mux/src/timeline.rs b/rs/moq-mux/src/timeline.rs index fe2a770022..3b3392b364 100644 --- a/rs/moq-mux/src/timeline.rs +++ b/rs/moq-mux/src/timeline.rs @@ -55,7 +55,7 @@ use std::sync::{Arc, Mutex}; use std::task::Poll; use std::time::Duration; -use hang::catalog::{Archive, Timeline}; +use hang::catalog::Archive; use hang::timeline::{DEFAULT_NAME, Range, Record, RecordExt}; use moq_net::{Timescale, Timestamp}; @@ -439,8 +439,8 @@ impl State { /// /// Carries the track name, timescale, and duration bound. Wall-clock mapping is the /// catalog root clock's job ([`Clock`](crate::Clock)), not this section's. - fn section(&self) -> Timeline { - let mut section = Timeline::new(DEFAULT_NAME); + fn section(&self) -> Archive { + let mut section = Archive::new(DEFAULT_NAME); section.timescale = self.timescale.as_u64() as u32; section.duration_max = self.config.duration_max.map(|max| self.units(max)); section @@ -556,7 +556,7 @@ impl Segmenter { } /// The catalog section describing records built by this segmenter. - pub fn section(&self) -> Timeline { + pub fn section(&self) -> Archive { self.state.lock().unwrap().section() } @@ -623,7 +623,7 @@ impl Deferred { /// The catalog's root `archive` entry describing records built by this segmenter. pub fn section(&self) -> Archive { - self.segmenter.section().into() + self.segmenter.section() } /// Flush the final open segment and return a handle that can only drain pending records. @@ -885,7 +885,7 @@ impl Producer { /// The catalog's root `archive` entry advertising this timeline. pub fn section(&self) -> Archive { - self.segmenter.section().into() + self.segmenter.section() } /// Create a handle that closes segments without publishing them automatically. @@ -1176,7 +1176,7 @@ impl Consumer { /// The section supplies both the track name and the timescale, so a reader can't pair the /// wrong scale with the track. Errors if the section declares a timescale that isn't /// representable. - pub async fn subscribe(broadcast: &moq_net::broadcast::Consumer, section: &Timeline) -> crate::Result { + pub async fn subscribe(broadcast: &moq_net::broadcast::Consumer, section: &Archive) -> crate::Result { let track = broadcast.track(§ion.track)?.subscribe(None).await?; let config = moq_json::window::ConsumerConfig::default().with_compression(true); @@ -2073,7 +2073,7 @@ mod test { #[tokio::test] async fn the_built_in_catalog_is_recorded_without_pacing() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let timeline = catalog.timeline(); let mut video = catalog.enroll("video0").unwrap(); diff --git a/rs/moq-rtc/src/codec/bitstream_test.rs b/rs/moq-rtc/src/codec/bitstream_test.rs index 92c3bd61c9..7605875028 100644 --- a/rs/moq-rtc/src/codec/bitstream_test.rs +++ b/rs/moq-rtc/src/codec/bitstream_test.rs @@ -39,7 +39,7 @@ async fn h264_annexb_frame_publishes_catalog_entry() { let broadcast = moq_net::broadcast::Info::new(); let mut producer = broadcast.produce(); - let catalog = moq_mux::catalog::Producer::new(&mut producer).expect("catalog"); + let catalog = moq_mux::catalog::Producer::new(&mut producer, moq_mux::catalog::Config::default()).expect("catalog"); let mut bridge = codec::h264::Bridge::new(producer, catalog.clone()).expect("bridge"); @@ -69,7 +69,7 @@ async fn h264_annexb_frame_publishes_catalog_entry() { async fn opus_frame_publishes_catalog_entry() { let broadcast = moq_net::broadcast::Info::new(); let mut producer = broadcast.produce(); - let catalog = moq_mux::catalog::Producer::new(&mut producer).expect("catalog"); + let catalog = moq_mux::catalog::Producer::new(&mut producer, moq_mux::catalog::Config::default()).expect("catalog"); let mut bridge = codec::opus::Bridge::new(producer, catalog.clone(), 48_000, 2).expect("bridge"); @@ -92,7 +92,7 @@ async fn opus_frame_publishes_catalog_entry() { async fn idle_video_bridges_do_not_gate_audio_catalog() { let broadcast = moq_net::broadcast::Info::new(); let mut producer = broadcast.produce(); - let catalog = moq_mux::catalog::Producer::new(&mut producer).expect("catalog"); + let catalog = moq_mux::catalog::Producer::new(&mut producer, moq_mux::catalog::Config::default()).expect("catalog"); let mut updates = catalog.consume().expect("catalog consumer"); let _vp8 = codec::vp8::Bridge::new(producer.clone(), catalog.clone()).expect("vp8 bridge"); @@ -120,7 +120,7 @@ async fn idle_video_bridges_do_not_gate_audio_catalog() { async fn vp9_keyframe_publishes_dimensions_and_starts_group() { let broadcast = moq_net::broadcast::Info::new(); let mut producer = broadcast.produce(); - let catalog = moq_mux::catalog::Producer::new(&mut producer).expect("catalog"); + let catalog = moq_mux::catalog::Producer::new(&mut producer, moq_mux::catalog::Config::default()).expect("catalog"); let mut bridge = codec::vp9::Bridge::new(producer, catalog.clone()).expect("bridge"); @@ -172,7 +172,7 @@ async fn egress_opus_passthrough() { // Build an opus broadcast via the ingest bridge. let broadcast = moq_net::broadcast::Info::new(); let mut producer = broadcast.produce(); - let catalog = moq_mux::catalog::Producer::new(&mut producer).expect("catalog"); + let catalog = moq_mux::catalog::Producer::new(&mut producer, moq_mux::catalog::Config::default()).expect("catalog"); let mut bridge = codec::opus::Bridge::new(producer.clone(), catalog.clone(), 48_000, 2).expect("bridge"); let payload = Bytes::from_static(&[0xfc, 0xff, 0xfe]); @@ -214,7 +214,7 @@ async fn egress_h264_avc3_passthrough() { let broadcast = moq_net::broadcast::Info::new(); let mut producer = broadcast.produce(); - let catalog = moq_mux::catalog::Producer::new(&mut producer).expect("catalog"); + let catalog = moq_mux::catalog::Producer::new(&mut producer, moq_mux::catalog::Config::default()).expect("catalog"); let mut bridge = codec::h264::Bridge::new(producer.clone(), catalog.clone()).expect("bridge"); Bridge::push( diff --git a/rs/moq-rtc/src/codec/vp8.rs b/rs/moq-rtc/src/codec/vp8.rs index f0bdc0ae90..7013d8d331 100644 --- a/rs/moq-rtc/src/codec/vp8.rs +++ b/rs/moq-rtc/src/codec/vp8.rs @@ -45,7 +45,7 @@ mod tests { #[test] fn keyframe_publishes_catalog_dimensions() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut bridge = super::Bridge::new(broadcast, catalog.clone()).unwrap(); assert!(catalog.snapshot().video.renditions.is_empty()); @@ -67,7 +67,7 @@ mod tests { #[tokio::test] async fn importer_creation_failure_preserves_abort_error() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let _collision = broadcast.create_track(hang::timeline::DEFAULT_NAME, None).unwrap(); let consumer = broadcast.consume(); let mut bridge = super::Bridge::new(broadcast, catalog).unwrap(); diff --git a/rs/moq-rtc/src/codec/vp9.rs b/rs/moq-rtc/src/codec/vp9.rs index 5d815f241f..f2d60b3754 100644 --- a/rs/moq-rtc/src/codec/vp9.rs +++ b/rs/moq-rtc/src/codec/vp9.rs @@ -45,7 +45,7 @@ mod tests { #[test] fn keyframe_publishes_catalog_dimensions() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut bridge = super::Bridge::new(broadcast, catalog.clone()).unwrap(); assert!(catalog.snapshot().video.renditions.is_empty()); diff --git a/rs/moq-rtc/src/ingest.rs b/rs/moq-rtc/src/ingest.rs index e7a52ecc84..269d660b61 100644 --- a/rs/moq-rtc/src/ingest.rs +++ b/rs/moq-rtc/src/ingest.rs @@ -16,7 +16,7 @@ pub struct IngestSink { impl IngestSink { pub fn new(mut broadcast: moq_net::broadcast::Producer, config: moq_mux::catalog::Config) -> Result { - let catalog = moq_mux::catalog::Producer::with_config(&mut broadcast, config)?; + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; Ok(Self { broadcast, catalog, diff --git a/rs/moq-rtc/src/lib.rs b/rs/moq-rtc/src/lib.rs index 764d9d23bd..89eae87b68 100644 --- a/rs/moq-rtc/src/lib.rs +++ b/rs/moq-rtc/src/lib.rs @@ -94,7 +94,8 @@ mod tests { source .announce(moq_net::origin::Route::default()) .expect("announce source broadcast"); - let catalog = moq_mux::catalog::Producer::new(&mut source).expect("create source catalog"); + let catalog = moq_mux::catalog::Producer::new(&mut source, moq_mux::catalog::Config::default()) + .expect("create source catalog"); let mut opus = crate::codec::opus::Bridge::new(source, catalog, 48_000, 2).expect("create Opus bridge"); Bridge::push( &mut opus, diff --git a/rs/moq-rtmp/src/dial.rs b/rs/moq-rtmp/src/dial.rs index 740daf70f6..f1b78f4e27 100644 --- a/rs/moq-rtmp/src/dial.rs +++ b/rs/moq-rtmp/src/dial.rs @@ -501,7 +501,7 @@ impl Publisher { broadcast .announce(moq_net::origin::Route::default()) .map_err(|err| anyhow::anyhow!("broadcast '{path}' could not be announced: {err}"))?; - let catalog = moq_mux::catalog::Producer::with_config(&mut broadcast, config)?; + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; let handle = broadcast.clone(); let mut importer = FlvImport::new(broadcast, catalog.reserve()); @@ -567,7 +567,7 @@ mod tests { let server_origin = moq_tokio::origin::spawn(moq_net::Hop::random()); let mut broadcast = server_origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(Default::default()).unwrap(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut importer = FlvImport::new(broadcast, catalog.reserve()); importer.decode(&flv::file_header()).unwrap(); importer.decode(&flv::tag(flv::TAG_VIDEO, 0, &vseq)).unwrap(); diff --git a/rs/moq-rtmp/src/server.rs b/rs/moq-rtmp/src/server.rs index aa13cef3ae..758069507e 100644 --- a/rs/moq-rtmp/src/server.rs +++ b/rs/moq-rtmp/src/server.rs @@ -1298,7 +1298,7 @@ impl Publisher { fn new(origin: &origin::Producer, path: &str, config: moq_mux::catalog::Config) -> anyhow::Result { let mut broadcast = origin.create_broadcast(path)?; broadcast.announce(moq_net::origin::Route::default())?; - let catalog = moq_mux::catalog::Producer::with_config(&mut broadcast, config)?; + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; let handle = broadcast.clone(); let mut importer = FlvImport::new(broadcast, catalog.reserve()); @@ -1672,7 +1672,7 @@ mod tests { let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); let mut broadcast = origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(Default::default()).unwrap(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut importer = FlvImport::new(broadcast, catalog.reserve()); importer.decode(&flv::file_header()).unwrap(); importer.decode(&flv::tag(flv::TAG_VIDEO, 0, &vseq)).unwrap(); @@ -1720,7 +1720,7 @@ mod tests { let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); let mut broadcast = origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(Default::default()).unwrap(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut importer = FlvImport::new(broadcast, catalog.reserve()); importer.decode(&flv::file_header()).unwrap(); @@ -1842,7 +1842,7 @@ mod tests { let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); let mut broadcast = origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(Default::default()).unwrap(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut importer = FlvImport::new(broadcast, catalog.reserve()); importer.decode(&flv::file_header()).unwrap(); importer.decode(&flv::tag(flv::TAG_VIDEO, 0, &seq)).unwrap(); diff --git a/rs/moq-srt/src/server.rs b/rs/moq-srt/src/server.rs index 38c33b0c32..3077b316eb 100644 --- a/rs/moq-srt/src/server.rs +++ b/rs/moq-srt/src/server.rs @@ -822,9 +822,9 @@ mod tests { tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); let mut broadcast = origin.create_broadcast("rewind").unwrap(); broadcast.announce(moq_net::origin::Route::default()).unwrap(); - let mut catalog = moq_mux::catalog::Producer::with_catalog( + let mut catalog = moq_mux::catalog::Producer::new( &mut broadcast, - moq_mux::catalog::hang::Catalog::::default(), + moq_mux::catalog::Config::default().with_catalog(moq_mux::catalog::hang::Catalog::::default()), ) .unwrap(); let track = broadcast diff --git a/rs/moq-srt/src/ts.rs b/rs/moq-srt/src/ts.rs index 9a8e6b894a..9c62053bed 100644 --- a/rs/moq-srt/src/ts.rs +++ b/rs/moq-srt/src/ts.rs @@ -43,7 +43,7 @@ impl Publisher { let mut broadcast = origin.create_broadcast(path)?; broadcast.announce(moq_net::origin::Route::default())?; let config = config.with_catalog(moq_mux::catalog::hang::Catalog::::default()); - let catalog = moq_mux::catalog::Producer::with_config(&mut broadcast, config)?; + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; let handle = broadcast.clone(); let importer = ts::Import::new(broadcast, catalog.reserve()); tracing::info!(%path, "publishing ingest broadcast"); @@ -253,7 +253,7 @@ mod tests { .expect("no catalog snapshot carried the cue track") .unwrap() .expect("the catalog ended without the cue track"); - if let Some((name, track)) = snapshot.mpegts.tracks.iter().find(|(_, track)| { + if let Some((name, track)) = snapshot.ext.mpegts.tracks.iter().find(|(_, track)| { track .verbatim .as_ref() @@ -296,9 +296,9 @@ mod tests { let mut roundtrip = moq_net::broadcast::Info::new().produce(); let roundtrip_consumer = roundtrip.consume(); - let roundtrip_catalog = moq_mux::catalog::Producer::with_catalog( + let roundtrip_catalog = moq_mux::catalog::Producer::new( &mut roundtrip, - moq_mux::catalog::hang::Catalog::::default(), + moq_mux::catalog::Config::default().with_catalog(moq_mux::catalog::hang::Catalog::::default()), ) .unwrap(); let mut roundtrip_import = ts::Import::new(roundtrip, roundtrip_catalog.reserve()); @@ -307,6 +307,7 @@ mod tests { let snapshot = roundtrip_catalog.snapshot(); let (name, _) = snapshot + .ext .mpegts .tracks .iter() diff --git a/rs/moq-transcode/src/catalog.rs b/rs/moq-transcode/src/catalog.rs index 81b4fea034..29e89769d5 100644 --- a/rs/moq-transcode/src/catalog.rs +++ b/rs/moq-transcode/src/catalog.rs @@ -551,7 +551,7 @@ mod tests { archive.replay = Some(PathRelativeOwned::from("./recordings/clip".to_string())); archive.version = Some(hang::catalog::Archive::VERSION); child.archive = Some(archive.clone()); - let clock = hang::catalog::Clock::new(1_751_846_400_000_000).unwrap(); + let clock = hang::catalog::Clock::new(moq_net::Timestamp::from_micros(1_751_846_400_000_000).unwrap()).unwrap(); child.clock = Some(clock); let mut out = moq_mux::catalog::hang::Catalog::<()>::default(); diff --git a/rs/moq-transcode/src/lib.rs b/rs/moq-transcode/src/lib.rs index dde9948dd1..9cd7172ee9 100644 --- a/rs/moq-transcode/src/lib.rs +++ b/rs/moq-transcode/src/lib.rs @@ -81,7 +81,7 @@ impl Transcoder { ) -> Result { // The catalog starts empty and fills in during `run`, exactly like a // media importer that hasn't seen parameter sets yet. - let derived = moq_mux::catalog::Producer::new(&mut output)?; + let derived = moq_mux::catalog::Producer::new(&mut output, moq_mux::catalog::Config::default())?; let dynamic = output.dynamic(); Ok(Self { @@ -248,7 +248,7 @@ mod tests { /// resolve a ladder, since no rung encodes until someone asks. fn source_catalog(width: u32, height: u32) -> Source { let mut broadcast = moq_net::broadcast::Info::default().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let track = broadcast .create_track("video", hang::container::track_info(hang::catalog::PRIORITY.video)) .unwrap(); @@ -336,7 +336,7 @@ mod tests { /// `groups` groups of `frames` gray frames each, encoded with openh264. fn source_broadcast(groups: u64, frames: u64) -> Source { let mut broadcast = moq_net::broadcast::Info::default().produce(); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 { inline: true, @@ -393,7 +393,7 @@ mod tests { /// producing task's handle (the track producer lives inside it). fn source_broadcast_live(groups: u64, frames: u64) -> (Source, tokio::task::JoinHandle<()>) { let mut broadcast = moq_net::broadcast::Info::default().produce(); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 { inline: true, diff --git a/rs/moq-video/src/decode/consumer.rs b/rs/moq-video/src/decode/consumer.rs index 9ef9ffa745..31b36c5e54 100644 --- a/rs/moq-video/src/decode/consumer.rs +++ b/rs/moq-video/src/decode/consumer.rs @@ -162,7 +162,8 @@ mod tests { async fn reads_cmaf_container_declared_by_catalog() { let mut source_broadcast = moq_net::broadcast::Info::new().produce(); let source_subscriber = source_broadcast.consume(); - let source_catalog = moq_mux::catalog::Producer::new(&mut source_broadcast).unwrap(); + let source_catalog = + moq_mux::catalog::Producer::new(&mut source_broadcast, moq_mux::catalog::Config::default()).unwrap(); let config = EncodeConfig { kind: EncodeKind::Software, ..EncodeConfig::new(320, 240, 30) @@ -201,7 +202,7 @@ mod tests { let mut broadcast = moq_net::broadcast::Info::new().produce(); let subscriber = broadcast.consume(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut import = moq_mux::container::fmp4::Import::new(broadcast, catalog.reserve()); import.decode(&init).unwrap(); import.decode(&fragment).unwrap(); diff --git a/rs/moq-video/src/encode/producer.rs b/rs/moq-video/src/encode/producer.rs index 95c6a67fd0..5e8aaa3a00 100644 --- a/rs/moq-video/src/encode/producer.rs +++ b/rs/moq-video/src/encode/producer.rs @@ -609,7 +609,7 @@ mod tests { /// Linux CI would try the NVENC backend and panic in cudarc on a GPU-less runner. async fn roundtrip_rendition(codec: Codec, kind: encoder::Kind) -> (String, hang::catalog::VideoConfig) { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut config = Config::new(320, 240, 30); config.codec = codec; @@ -666,7 +666,7 @@ mod tests { #[tokio::test] async fn idle_capture_publishes_a_discontinuity_before_resume() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); // The synthetic clock jumps ten seconds. Keep every fixture group readable until the // assertion instead of letting the default five-second publisher window evict the marker. let replay = std::time::Duration::from_secs(11); @@ -701,7 +701,7 @@ mod tests { #[tokio::test] async fn source_resize_updates_the_published_rendition() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut initial = Config::new(320, 240, 30); initial.kind = encoder::Kind::Software; let mut producer = Producer::new(broadcast, catalog.clone(), initial.probe().await.unwrap()).unwrap(); @@ -731,7 +731,7 @@ mod tests { #[tokio::test] async fn a_selected_container_survives_the_rendition_hint() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut config = Config::new(320, 240, 30); // Software (openh264) so the test is deterministic and never touches a hardware backend. @@ -756,7 +756,7 @@ mod tests { async fn the_rendition_reaches_the_wire_before_the_first_frame() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut config = Config::new(1920, 1080, 30); config.bitrate = Some(moq_net::bandwidth::Rate::from_mbps(6)); @@ -795,7 +795,7 @@ mod tests { #[tokio::test] async fn abort_after_finish() { let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut config = Config::new(320, 240, 30); config.kind = encoder::Kind::Software; let track = broadcast From f1a6d59c5eb33d50ec4c82eb3401aa6b23bd1f0f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 20 Sep 2026 14:00:41 -0700 Subject: [PATCH 3/6] fix(mux): reject retired timeline extension Co-Authored-By: OpenAI Codex --- rs/hang/CHANGELOG.md | 2 +- rs/moq-hls/CHANGELOG.md | 2 +- rs/moq-mux/src/catalog/hang/ext.rs | 36 ++++++++++++++++++++++++------ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/rs/hang/CHANGELOG.md b/rs/hang/CHANGELOG.md index 9ad2c478c0..f37a4c52c6 100644 --- a/rs/hang/CHANGELOG.md +++ b/rs/hang/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - [**breaking**] Replace the catalog root `timeline` entry with `archive`. The - flattened timeline fields (`track`, `timescale`, `durationMax`, `wall`) stay; + flattened timeline fields (`track`, `timescale`, `durationMax`) stay; optional `replay`, `store`, and `version` advertise a durable recording. - [**breaking**] Replace the archive timeline `wall` field with a root `clock` section (`{wall, timescale}`). Wall time is one fixed broadcast mapping: PTS zero in the diff --git a/rs/moq-hls/CHANGELOG.md b/rs/moq-hls/CHANGELOG.md index 1b2a85a948..18ed7d5eff 100644 --- a/rs/moq-hls/CHANGELOG.md +++ b/rs/moq-hls/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- [**breaking**] Read the broadcast timeline from `catalog.archive.timeline` +- [**breaking**] Read the broadcast timeline from `catalog.archive` instead of the removed root `timeline` entry. ### Fixed diff --git a/rs/moq-mux/src/catalog/hang/ext.rs b/rs/moq-mux/src/catalog/hang/ext.rs index 151ae48967..86ce7f36c1 100644 --- a/rs/moq-mux/src/catalog/hang/ext.rs +++ b/rs/moq-mux/src/catalog/hang/ext.rs @@ -40,13 +40,31 @@ impl CatalogExt for () {} /// The default extension stays `()` (unknown sections dropped); opt into `Extra` explicitly. /// /// `video`, `audio`, `text`, `archive`, `clock`, `json`, `binary`, and the retired `timeline` -/// key are reserved, so [`set`](Self::set) rejects them to keep the wire JSON free of duplicate keys. -#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)] +/// key are reserved, so decoding and [`set`](Self::set) reject them to keep the wire JSON free of +/// duplicate or retired keys. +#[derive(Serialize, Clone, Default, Debug, PartialEq)] #[serde(transparent)] pub struct Extra(serde_json::Map); impl CatalogExt for Extra {} +impl<'de> Deserialize<'de> for Extra { + fn deserialize>(deserializer: D) -> Result { + let sections = serde_json::Map::::deserialize(deserializer)?; + if let Some(name) = sections.keys().find(|name| reserved(name)) { + return Err(serde::de::Error::custom(format!("reserved catalog section: {name}"))); + } + Ok(Self(sections)) + } +} + +fn reserved(name: &str) -> bool { + matches!( + name, + "video" | "audio" | "text" | "archive" | "clock" | "json" | "binary" | "timeline" + ) || moq_msf::reserved_root(name) +} + impl Extra { /// Look up a section by name. pub fn get(&self, name: &str) -> Option<&serde_json::Value> { @@ -78,11 +96,7 @@ impl Extra { /// which serde emits without complaint. pub fn set(&mut self, name: impl Into, value: serde_json::Value) -> crate::Result<()> { let name = name.into(); - if matches!( - name.as_str(), - "video" | "audio" | "text" | "archive" | "clock" | "json" | "binary" | "timeline" - ) || moq_msf::reserved_root(&name) - { + if reserved(&name) { return Err(crate::Error::ReservedSection(name)); } self.0.insert(name, value); @@ -130,6 +144,14 @@ mod test { assert!(catalog.ext.get("scte35").is_some(), "unrelated sections still decode"); } + #[test] + fn retired_timeline_section_is_rejected() { + let json = r#"{"timeline":{"track":"timeline.json"}}"#; + + let error = serde_json::from_str::>(json).expect_err("retired timeline section was accepted"); + assert!(error.to_string().contains("reserved catalog section: timeline")); + } + #[test] fn extension_roundtrip() { let mut broadcast = moq_net::broadcast::Info::new().produce(); From 9004bfc25e56d829c54f784cc408efa8268d1b07 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 20 Sep 2026 14:20:54 -0700 Subject: [PATCH 4/6] docs(catalog): list reserved extension roots --- rs/moq-ffi/src/producer.rs | 5 +++-- rs/moq-mux/src/catalog/producer.rs | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/rs/moq-ffi/src/producer.rs b/rs/moq-ffi/src/producer.rs index 5d4754f9f6..51da37156d 100644 --- a/rs/moq-ffi/src/producer.rs +++ b/rs/moq-ffi/src/producer.rs @@ -287,8 +287,9 @@ impl MoqBroadcastProducer { /// /// `json` is any JSON document (object, array, string, ...) serialized as a UTF-8 string. /// Errors with [`MoqError::Json`] if `json` doesn't parse, or with the reserved-section - /// error if `name` is `video`/`audio` (owned by the media pipeline). The section is - /// republished on the catalog track immediately. + /// error if `name` is a HANG root (`video`, `audio`, `text`, `archive`, `clock`, `json`, + /// `binary`, or retired `timeline`) or an MSF root (`version`, `generatedAt`, `isComplete`, + /// `tracks`, or `initDataList`). The section is republished on the catalog track immediately. pub fn set_catalog_section(&self, name: String, json: String) -> Result<(), MoqError> { let _guard = crate::ffi::enter(); let value: serde_json::Value = serde_json::from_str(&json)?; diff --git a/rs/moq-mux/src/catalog/producer.rs b/rs/moq-mux/src/catalog/producer.rs index d387d3f8bc..86aaca65cc 100644 --- a/rs/moq-mux/src/catalog/producer.rs +++ b/rs/moq-mux/src/catalog/producer.rs @@ -718,7 +718,9 @@ impl DerefMut for Guard<'_, E> { impl Guard<'_, Extra> { /// Set (or replace) a top-level application catalog section, republished on drop. /// - /// Errors if `name` collides with a reserved media section (`video`/`audio`). + /// Errors if `name` is a HANG root (`video`, `audio`, `text`, `archive`, `clock`, `json`, + /// `binary`, or retired `timeline`) or an MSF root (`version`, `generatedAt`, `isComplete`, + /// `tracks`, or `initDataList`). pub fn set_section(&mut self, name: impl Into, value: serde_json::Value) -> crate::Result<()> { self.state.catalog.ext.set(name, value)?; self.updated = true; From 0b1a332fc16ae39c604e98d9ff8efa30f06671bb Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 20 Sep 2026 14:34:12 -0700 Subject: [PATCH 5/6] fix(ffi): refresh dart API checksum --- dart/moq_ffi/lib/src/moq.dart | 4 +--- dart/moq_ffi/lib/src/uniffi_runtime.dart | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/dart/moq_ffi/lib/src/moq.dart b/dart/moq_ffi/lib/src/moq.dart index 9747c7794c..1bb7f9bf6d 100644 --- a/dart/moq_ffi/lib/src/moq.dart +++ b/dart/moq_ffi/lib/src/moq.dart @@ -8,9 +8,7 @@ import "dart:ffi"; import "dart:io" show Platform, File, Directory; import "dart:isolate"; import "dart:typed_data"; - import "package:ffi/ffi.dart"; - import "uniffi_runtime.dart"; export "uniffi_runtime.dart"; @@ -11955,7 +11953,7 @@ void _checkApiChecksums() { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_set_catalog_section() != - 25735) { + 28423) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_set_video_properties() != diff --git a/dart/moq_ffi/lib/src/uniffi_runtime.dart b/dart/moq_ffi/lib/src/uniffi_runtime.dart index 042df9c187..ef3dc04b16 100644 --- a/dart/moq_ffi/lib/src/uniffi_runtime.dart +++ b/dart/moq_ffi/lib/src/uniffi_runtime.dart @@ -8,7 +8,6 @@ import "dart:ffi"; import "dart:io" show Platform, File, Directory; import "dart:isolate"; import "dart:typed_data"; - import "package:ffi/ffi.dart"; class UniffiInternalError implements Exception { @@ -234,7 +233,6 @@ mixin FfiConverterPrimitive on FfiConverter { @override T lower(T value) => value; } - Uint8List createUint8ListFromInt(int value) { int length = value.bitLength ~/ 8 + 1; if (length != 4 && length != 8) { From cfbf672924f9561e3bc0423fbeb35792e229dc79 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 20 Sep 2026 14:46:43 -0700 Subject: [PATCH 6/6] docs(hang): complete catalog contracts --- doc/concept/hang.md | 1 + rs/libmoq/src/publish.rs | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/concept/hang.md b/doc/concept/hang.md index 120f900320..96b0a3409c 100644 --- a/doc/concept/hang.md +++ b/doc/concept/hang.md @@ -54,6 +54,7 @@ A few things the catalog can express beyond decoder config: - **Jitter.** A rendition can say how long the publisher holds a frame before flushing it, in whole milliseconds rounded up: one frame for a track flushed immediately, the B-frame depth for a reordered one, the fragment for a segmented one. It describes the publisher, never the network, only grows over the life of a stream, and a player sizes its buffer to at least this much. A `0` is read as absent. - **Stalled renditions.** A publisher can flag a rendition as temporarily bad so players prefer another one without the track disappearing. First-party video publishers set this flag after more than three frame intervals of source silence or encoding lag while subscribed, and clear it after three on-time completed frames or when idle. Browser and native capture poll while waiting; FLV and MPEG-TS importers observe video silence as container data arrives. The shared detector is `hang::catalog::stalled::Detector` in Rust and `Catalog.Stalled.Detector` in JavaScript. It is a playback diagnostic, not an authorization or routing signal. - **Archive.** A broadcast may advertise an `archive` entry naming its timeline track (a small index of each complete aligned segment) and, if recorded, the replay MoQ path, object-store URL, and format version. The timeline is what lets the [HLS gateway](/bin/hls) build playlists without subscribing to media. +- **Clock.** The optional root `clock` maps PTS zero to wall time so every media track and the archive index share one fixed epoch after timescale conversion. It is independent of `archive`, so a live-only publisher can expose wall-clock timing without creating a segment index. - **Extensions.** The root is a loose object. Applications add their own sections (`scte35`, for example) next to the ones hang defines, optionally naming a track that carries the data. Every library exposes a way to write your section without clobbering the built-in ones, and readers ignore what they don't know. ## Text diff --git a/rs/libmoq/src/publish.rs b/rs/libmoq/src/publish.rs index 1c7f312b16..9a3cc18f9f 100644 --- a/rs/libmoq/src/publish.rs +++ b/rs/libmoq/src/publish.rs @@ -350,8 +350,10 @@ impl Publish { /// Insert or replace a top-level application catalog section by name. /// - /// `value` is any JSON document. Errors if `name` is reserved (`video`/`audio`). - /// The catalog is republished automatically. + /// `value` is any JSON document. Errors if `name` is a HANG root (`video`, `audio`, `text`, + /// `archive`, `clock`, `json`, `binary`, or retired `timeline`) or an MSF root (`version`, + /// `generatedAt`, `isComplete`, `tracks`, or `initDataList`). The catalog is republished + /// automatically. pub fn catalog_section_set(&mut self, broadcast: Id, name: &str, value: serde_json::Value) -> Result<(), Error> { let catalog = self.catalog(broadcast)?; let mut guard = catalog.modify()?;