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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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() !=
Expand Down
2 changes: 0 additions & 2 deletions dart/moq_ffi/lib/src/uniffi_runtime.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -234,7 +233,6 @@ mixin FfiConverterPrimitive<T> on FfiConverter<T, T> {
@override
T lower(T value) => value;
}

Uint8List createUint8ListFromInt(int value) {
int length = value.bitLength ~/ 8 + 1;
if (length != 4 && length != 8) {
Expand Down
11 changes: 6 additions & 5 deletions doc/concept/hang.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -100,16 +101,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

Expand Down
11 changes: 1 addition & 10 deletions drafts/draft-lcurley-moq-hang.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,6 @@ type JsonSchema = {
"compression": Compression | undefined,
"schema": string | undefined,
"broadcast": string | undefined,
"timeline": TimelineSchema | undefined,
}
~~~

Expand All @@ -402,7 +401,6 @@ type BinarySchema = {
"compression": Compression | undefined,
"mime": string | undefined,
"broadcast": string | undefined,
"timeline": TimelineSchema | undefined,
}
~~~

Expand Down Expand Up @@ -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}}).
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 17 additions & 3 deletions js/hang/src/catalog/archive.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
Expand Down
4 changes: 0 additions & 4 deletions js/hang/src/catalog/binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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),
});

/**
Expand Down
2 changes: 1 addition & 1 deletion js/hang/src/catalog/clock.ts
Original file line number Diff line number Diff line change
@@ -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));
Expand Down
1 change: 0 additions & 1 deletion js/hang/src/catalog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
4 changes: 0 additions & 4 deletions js/hang/src/catalog/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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),
});

/**
Expand Down
43 changes: 0 additions & 43 deletions js/hang/src/catalog/timeline.ts

This file was deleted.

1 change: 0 additions & 1 deletion js/hang/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
2 changes: 1 addition & 1 deletion js/hang/src/container/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
1 change: 1 addition & 0 deletions js/hang/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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++) {
Expand Down Expand Up @@ -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));
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions js/hang/src/container/timeline.ts → js/hang/src/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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. */
Expand Down
1 change: 0 additions & 1 deletion quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<E>` 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
Expand Down
50 changes: 0 additions & 50 deletions quest/m1/api-hang-catalog.md

This file was deleted.

Loading
Loading