diff --git a/AGENTS.md b/AGENTS.md index 667d372704a..d5ca5e3c0cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,7 @@ The API is the most important thing to get right. A bad shape costs a breaking change in every language, and the surface is huge. - Report the public API and wire impact of every change, in the PR description and whenever asked. +- A breaking change adds an entry under `## Unreleased` in each affected package's `MIGRATE.md` saying how to migrate. - Keep things private until a consumer needs them. Scrutinize every new exported item. - Never add `foo_with_x`, `foo_checked`, or a compatibility shim. Make the breaking change to `foo` instead. - Let the type system make misuse unrepresentable diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d168b341cee..e1fd6f18837 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ Have at least these sections: - **Problem**: a summary of the problem and why this PR is needed. - **Approach**: a summary of the approach taken to solve the problem. -- **Impact**: a bullet point for every public API/wire change made. +- **Impact**: a bullet point for every public API/wire change made. A breaking one also gets a `MIGRATE.md` entry in each affected package. - **Alternatives**: any alternative approaches considered. - **Follow-ups**: any issues encountered or quests created. @@ -83,6 +83,7 @@ For an advisory against Quinn, follow the advisory triage in [`rs/moq-quic/READM `main` is the trunk; `release` is what ships, and release-plz and every branch-triggered publish run only there. - A release is cut by hand: a PR merging `main` into `release`, with a merge commit. +- A breaking cut first renames, on `main`, each changed `MIGRATE.md`'s `## Unreleased` to its version transition (`## 0.3 -> 0.4`), leaving an empty `## Unreleased` above it. - Suggest a `release` backport for any serious bug fix (crash, security, data loss, broken interop), whether found on `main` or reported against a release. Land it on `main` first, then port it onto `release` as a separate PR that keeps the published API, so it ships as a patch. - After any release publishes, patch or breaking, run `just test wire-compat --remeasure` on `main` and land the rewritten `test/interop/compat/planned-breaks.json` with refreshed reasons. Otherwise the nightly wire-compat check fails on the stale pins. - Nothing merges `release` into `main` automatically. Before cutting a minor or breaking release, the maintainer merges `release` into `main` once, so trunk carries the published versions, CHANGELOGs, and any `release`-only commits. It lands as a merge commit with `--admin`, never squashed, so the cut starts from an advanced merge base. diff --git a/cpp/moq/MIGRATE.md b/cpp/moq/MIGRATE.md new file mode 100644 index 00000000000..63ce5456a5b --- /dev/null +++ b/cpp/moq/MIGRATE.md @@ -0,0 +1,62 @@ +# Migrating the C++ moq library + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Subscriptions carry group ranges (#5732) + +`Subscription::group_start` and `group_end` are replaced by `ranges`, a list of `Range` whose ends are an `Anchor` (an absolute group and frame, or a count of groups before the latest), and an optional `order`. +To start at group `n`, pass one range starting at the absolute anchor for group `n`, frame 0. + +### Receive estimates come from `Session::recv_bandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session::recv_bandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and `cancel()` it to stop. + +```cpp +auto bandwidth = session->recv_bandwidth(); +auto rate = bandwidth ? bandwidth->peek() : std::nullopt; +``` + +### `AnnouncedBroadcast::available()` waits for the next publisher instance (#5700) + +The first call returns the broadcast serving the path, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + +### A timed track refuses a group that rewinds (#5803) + +Writing a frame fails with `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + +### Bandwidth reserves a track's demand (#5845) + +`Bandwidth::reserve` takes the track's `demand()` instead of the producer, and no longer fails: pass `ok(track->demand())`. + +### Generated types drop the `Moq` prefix (#5727) + +`moq::MoqFoo` is `moq::Foo`, the name the aliases already gave it. +`moq::expected` is `tl::expected` at every C++ standard instead of `std::expected` under C++23, and `MOQ_ABI`, `MOQ_ABI_NAME`, and `MOQ_IMPLEMENTATION` are gone. + +### Media subscribe and fetch take a catalog rendition (#5834) + +`moq::MediaTrackConfig` and `moq::MediaGroupConfig` replace `name` and `container` with a `moq::Rendition` and follow its broadcast and track themselves. + +```cpp +auto media = ok(moq::MediaTrackConsumer::subscribe(broadcast, {moq::Rendition::kVideo{id, video}}).get()); +``` + +### Native codecs have audio and video types (#5694) + +`BroadcastProducer::encode_audio` / `encode_video` are `moq::AudioProducer::init` / `moq::VideoProducer::init`, which take a `moq::MediaCatalogProducer`. +The audio track name moves into `AudioEncoderOutput::track`, and a zero `frame_duration_us` takes the codec's own frame. +`BroadcastConsumer::decode_audio` / `decode_video` are `moq::AudioConsumer::subscribe` / `moq::VideoConsumer::subscribe`, taking the broadcast first. +Producers expose subscriber state only through `demand()`, and a video `gop` is a `moq::VideoGop` keyframe variant. +`moq::AudioEncoder` / `moq::VideoEncoder` encode without publishing. + +```cpp +moq::AudioEncoderOutput output{moq::AudioCodec::opus()}; +output.track = "audio"; +auto catalog = ok(moq::MediaCatalogProducer::init(broadcast)); +auto audio = ok(moq::AudioProducer::init(catalog, input, output, nullptr)); +``` diff --git a/cpp/obs/MIGRATE.md b/cpp/obs/MIGRATE.md new file mode 100644 index 00000000000..8407b8b1a1a --- /dev/null +++ b/cpp/obs/MIGRATE.md @@ -0,0 +1,10 @@ +# Migrating the obs-moq plugin + +How to update your OBS setup for each breaking change, newest first. + +## Unreleased + +### The video bitrate follows the bandwidth estimate (#5845) + +The configured video bitrate is now a ceiling: the encoder drops below it when the uplink narrows, with no opt-out. +An encoder shared with a recording follows too, so give the recording its own encoder to keep it at the full rate. diff --git a/dart/moq/MIGRATE.md b/dart/moq/MIGRATE.md new file mode 100644 index 00000000000..965ea3b1cd6 --- /dev/null +++ b/dart/moq/MIGRATE.md @@ -0,0 +1,134 @@ +# Migrating the Dart moq package + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Subscriptions carry group ranges (#5732) + +`Subscription.groupStart` and `groupEnd` are replaced by `ranges`, a list of `Range` whose ends are an `Anchor` (an absolute group and frame, or a count of groups before the latest), and an optional `order`. +To start at group `n`, pass one range starting at the absolute anchor for group `n`, frame 0. + +### Receive estimates come from `Session.recvBandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session.recvBandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and `cancel()` it to stop. +Replace `stats().estimatedRecvRateBps` with `recvBandwidth()?.peek()`. + +### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) + +The first call returns the broadcast serving the path, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + +### A timed track refuses a group that rewinds (#5803) + +Writing a frame fails with `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + +### Bandwidth reserves a track's demand (#5845) + +`Bandwidth.reserve` takes the track's `demand()` instead of the producer, and no longer throws. +Pass `demand: track.demand()` instead of `track: track`. + +### Media subscribe and fetch take a catalog rendition (#5834) + +`media.TrackConfig` and `media.GroupConfig` replace `name` and `container` with a `rendition` (`media.VideoRendition` or `media.AudioRendition`) and follow its broadcast and track themselves. + +```dart +final config = media.TrackConfig(rendition: media.VideoRendition(id: id, config: video)); +``` + +### Background panics are reported (#5562) + +A panic in a background task throws `TaskMoqException` with the panic message as `reason`, and other objects keep running. + +### Announcements are events with a restart + +`Moq.announcements()` is `announced(...).updates()`, a `Stream`: `AnnounceEventStart`, `AnnounceEventUpdate`, `AnnounceEventEnd`, or `AnnounceEventRestart`, each carrying an `Announce`. +`AnnounceUpdate` is gone. +A restart means another publisher instance now serves the prefix: request the path again. + +```dart +moq.announced().updates().listen((event) { + switch (event) { + case AnnounceEventStart(:final announce) || + AnnounceEventRestart(:final announce): + subscribe(announce.prefix); + default: + } +}); +``` + +### "Epoch" means only the publisher epoch (#5146) + +`Moq.epoch` is `connects`. + +### The consume origin is `consume` + +`ConnectOptions.subscribe` is `consume`. +A value the native side cannot use fails `Moq.connect` with `ConfigMoqException`, including a bad bind address. +A server request takes its origins in `accept(publish: ..., consume: ...)`, where null inherits the server's origin. + +### Data tracks wrap a track + +`BroadcastProducer.publishJsonSnapshot` / `publishJsonStream`, the binary variants, and the matching consumers are gone. +Create the track, then wrap it with `package:moq/json.dart` (or `package:moq/flate.dart` for opaque bytes). + +```dart +final state = moq_json.SnapshotProducer( + broadcast: broadcast, + track: broadcast.publishTrack(name: 'state', info: null), + config: moq_json.SnapshotConfig(), +); +``` + +### Media lives in `package:moq/media.dart` + +`BroadcastProducer.publishAudio`, `publishVideo`, `publishContainer`, their `OnTrack` and `Stream` variants, `setVideoProperties`, and `setCatalogSection`, and the consumer's catalog and media subscriptions, are gone, along with the `MediaProducer`, `MediaConsumer`, and `MediaGroupConsumer` aliases. +Use `media.TrackProducer.audio` / `video`, `ContainerProducer`, `CatalogProducer`, `CatalogConsumer.subscribe`, `TrackConsumer.subscribe`, and `GroupConsumer.fetch`. +Producers drop `name`, `used`, and `unused`; read them through `demand()`. + +```dart +final audio = media.TrackProducer.audio(broadcast: broadcast, target: media.Named(null), init: init); +``` + +### Container byte-stream importers take an init record + +`media.ContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. +Its optional `timebase`, also on `ContainerInit`, shares one source's offset across container imports. +Replace `format: ...` with `init: media.ContainerStreamInit(format: ...)`. + +### Timestamps are optional + +`Frame.timestampUs` and `Datagram.timestampUs` are nullable, null on a frame read from an untimed track, and so are their `timestamp` getters. +A raw write to a timed track without a timestamp throws `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. +`media.CatalogProducer.now()` samples the broadcast clock for local-event stamps. +Pass `timestampUs` when writing to a timed track, and handle a null `timestamp` when reading. + +### Subscriber staleness is `maxDelay` + +`Subscription.maxAgeUs` / `maxAge` is `maxDelayUs` / `maxDelay`. +`TrackInfo.maxAge` keeps its name: it is the publisher's retention. + +### Error payloads are named `reason` + +Exceptions that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. +Read `reason` instead of `v0`. + +### `Transport` gains `webTransport` + +A WebTransport session reports `MoqTransport.webTransport`; `quic` now means raw QUIC only, so an exhaustive `switch` needs one more case. +Match `MoqTransport.webTransport` wherever you matched `MoqTransport.quic`. + +### `stalled` is `enabled` (#4915) + +`media.Video.stalled` is `enabled`, defaulting to true with the meaning inverted, and `media.Audio` gains `enabled`. +Replace `!video.stalled` with `video.enabled`. + +### Routes have one cost (#4661) + +`MoqRoute.cold` is removed; drop the `cold` argument. + +### `BroadcastProducer.finish()` is removed (#4152) + +Call `close()` instead. diff --git a/doc/bin/cli.md b/doc/bin/cli.md index 6a54f98ae64..86e3b237205 100644 --- a/doc/bin/cli.md +++ b/doc/bin/cli.md @@ -62,7 +62,8 @@ each session can publish or subscribe to, including stats; see the relay's [statistics configuration](/bin/relay/config#stats). `moq import --help` lists the sources and `moq import rtmp --help` a specific -one; every flag is documented there. +one; every flag is documented there. Flags renamed between releases are listed in +[MIGRATE.md](https://github.com/moq-dev/moq/blob/main/rs/moq-cli/MIGRATE.md). ```bash # Publish a file (remux to MPEG-TS without re-encoding) diff --git a/doc/bin/relay/index.md b/doc/bin/relay/index.md index acd69bfae74..e0e8d46257e 100644 --- a/doc/bin/relay/index.md +++ b/doc/bin/relay/index.md @@ -48,7 +48,9 @@ Every option is also a `--flag` or `MOQ_*` environment variable, and `RUST_LOG` controls logging. The [configuration reference](/bin/relay/config) covers every section, and [`demo/relay/`](https://github.com/moq-dev/moq/tree/main/demo/relay) has -working configs for development, production, and a cluster. +working configs for development, production, and a cluster. Options renamed +between releases are listed in +[MIGRATE.md](https://github.com/moq-dev/moq/blob/main/rs/moq-relay/MIGRATE.md). ## Embed diff --git a/doc/lib/index.md b/doc/lib/index.md index 71a5a434fd6..d2a08e444a3 100644 --- a/doc/lib/index.md +++ b/doc/lib/index.md @@ -47,3 +47,8 @@ how it looks in that language: - **Connection stats** and **errors** that tell an auth rejection (don't retry) from a shutdown or a transport failure. C's audio decoder neither takes nor reports a playout delay. + +## Upgrading + +Each package keeps a `MIGRATE.md` saying how to migrate past every breaking +change; [Upgrade](/setup/upgrade#migration-guides) links them all. diff --git a/doc/lib/rs/moq-flate.md b/doc/lib/rs/moq-flate.md index f0b2853f6a0..0a3ab119654 100644 --- a/doc/lib/rs/moq-flate.md +++ b/doc/lib/rs/moq-flate.md @@ -32,8 +32,8 @@ an untimed track has no timescale, and a timed track requires a timestamp on every write. A publisher that wants to stamp now samples its own clock. Writes return the encoded frame size. -When upgrading, replace `Timed` with `Timed

` and pass capture times -explicitly instead of relying on a write-time default. +Upgrading from an older release: see moq-flate's +[MIGRATE.md](https://github.com/moq-dev/moq/blob/main/rs/moq-flate/MIGRATE.md). A stream rides one group, so the whole log shares moq-net's group budget: 32 MiB of payload and 8192 payloads. An `append` that might not fit returns diff --git a/doc/lib/rs/moq-json.md b/doc/lib/rs/moq-json.md index fb0dafc3334..2338572eae0 100644 --- a/doc/lib/rs/moq-json.md +++ b/doc/lib/rs/moq-json.md @@ -23,9 +23,8 @@ The timestamp must match the track: an untimed track has no timescale, and a timed track requires a timestamp on every write. Editing guards take the same optional capture time when opened, including when they publish on drop. -When upgrading, replace `Timed` with `Timed

` and pass timestamps -explicitly. A publisher that wants to stamp an event now samples its own -clock; the producer never supplies one. +Upgrading from an older release: see moq-json's +[MIGRATE.md](https://github.com/moq-dev/moq/blob/main/rs/moq-json/MIGRATE.md). A stream rides one group, so the whole log shares moq-net's group budget: 32 MiB of payload and 8192 records. An append that might not fit is refused diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index 6e91415c773..db894231a00 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -1,484 +1,83 @@ --- title: Upgrade -description: Breaking changes between the 2026-09-17 releases and the 2026-09-23 release train, with the replacement for each +description: The order to roll out a release across relays, auth servers, and clients, and each package's migration guide --- # Upgrade -This page walks from the 2026-09-17 releases (moq-relay 0.14.18, moq-cli -0.11.2, moq-net 0.2.22, @moq/net 0.3.5, moq-ffi 0.3.19) to the 2026-09-23 -release train (moq-relay 0.15.1, moq-cli 0.12.1, moq-net 0.3.0, @moq/net 0.4.0, -moq-ffi 0.4.1). Each crate's `CHANGELOG.md` has the full list; this page is the -subset that breaks a working setup. +Every hand-written published package keeps a `MIGRATE.md` entry for each +breaking change, newest first, saying how to migrate. The links below track +`main`, which also holds the released sections: apply those from the version you +run up to the one you install. `Unreleased` applies only when you build from +`main`. Most binding users install a wrapper, such as `moq-rs` on PyPI or +`dev.moq:moq` on Maven, whose guide is in the index below. The raw UniFFI +packages published as `moq-ffi` have no guide of their own: +[moq-ffi's](https://github.com/moq-dev/moq/blob/main/rs/moq-ffi/MIGRATE.md) +covers that layer, in UniFFI names. This page covers what spans packages: the +wire changes that decide the order to upgrade a deployment. A released flag, environment variable, or config key that was renamed is refused at startup with its replacement named, rather than ignored. Fix what the -error lists and rerun. +error lists and rerun. The one exception is moq-cli's removed `MOQ_HOP`, which is +ignored; its guide says what replaces it. -## Unreleased +## Migration guides -These land with the next breaking release, not the 2026-09-23 train. +- **Binaries:** [moq-relay](https://github.com/moq-dev/moq/blob/main/rs/moq-relay/MIGRATE.md), [moq-cli (`moq`)](https://github.com/moq-dev/moq/blob/main/rs/moq-cli/MIGRATE.md), [moq-gst](https://github.com/moq-dev/moq/blob/main/rs/moq-gst/MIGRATE.md), [obs-moq](https://github.com/moq-dev/moq/blob/main/cpp/obs/MIGRATE.md) +- **Bindings:** [moq-ffi (UniFFI)](https://github.com/moq-dev/moq/blob/main/rs/moq-ffi/MIGRATE.md), [moq-c (C)](https://github.com/moq-dev/moq/blob/main/rs/moq-c/MIGRATE.md), [Python](https://github.com/moq-dev/moq/blob/main/py/moq-rs/MIGRATE.md), [Go](https://github.com/moq-dev/moq/blob/main/go/wrapper/MIGRATE.md), [Swift](https://github.com/moq-dev/moq/blob/main/swift/MIGRATE.md), [Kotlin](https://github.com/moq-dev/moq/blob/main/kt/moq/MIGRATE.md), [Dart](https://github.com/moq-dev/moq/blob/main/dart/moq/MIGRATE.md), [C++](https://github.com/moq-dev/moq/blob/main/cpp/moq/MIGRATE.md) +- **Rust:** [hang](https://github.com/moq-dev/moq/blob/main/rs/hang/MIGRATE.md), [kio](https://github.com/moq-dev/moq/blob/main/rs/kio/MIGRATE.md), [moq-archive](https://github.com/moq-dev/moq/blob/main/rs/moq-archive/MIGRATE.md), [moq-audio](https://github.com/moq-dev/moq/blob/main/rs/moq-audio/MIGRATE.md), [moq-auth](https://github.com/moq-dev/moq/blob/main/rs/moq-auth/MIGRATE.md), [moq-boy](https://github.com/moq-dev/moq/blob/main/rs/moq-boy/MIGRATE.md), [moq-clock](https://github.com/moq-dev/moq/blob/main/rs/moq-clock/MIGRATE.md), [moq-clock-macros](https://github.com/moq-dev/moq/blob/main/rs/moq-clock/macros/MIGRATE.md), [moq-e2ee](https://github.com/moq-dev/moq/blob/main/rs/moq-e2ee/MIGRATE.md), [moq-flate](https://github.com/moq-dev/moq/blob/main/rs/moq-flate/MIGRATE.md), [moq-hls](https://github.com/moq-dev/moq/blob/main/rs/moq-hls/MIGRATE.md), [moq-ice](https://github.com/moq-dev/moq/blob/main/rs/moq-ice/MIGRATE.md), [moq-json](https://github.com/moq-dev/moq/blob/main/rs/moq-json/MIGRATE.md), [moq-loc](https://github.com/moq-dev/moq/blob/main/rs/moq-loc/MIGRATE.md), [moq-msf](https://github.com/moq-dev/moq/blob/main/rs/moq-msf/MIGRATE.md), [moq-mux](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/MIGRATE.md), [moq-net](https://github.com/moq-dev/moq/blob/main/rs/moq-net/MIGRATE.md), [moq-nvenc](https://github.com/moq-dev/moq/blob/main/rs/moq-nvenc/MIGRATE.md), [moq-pattern](https://github.com/moq-dev/moq/blob/main/rs/moq-pattern/MIGRATE.md), [moq-quic](https://github.com/moq-dev/moq/blob/main/rs/moq-quic/MIGRATE.md), [moq-room](https://github.com/moq-dev/moq/blob/main/rs/moq-room/MIGRATE.md), [moq-rtc](https://github.com/moq-dev/moq/blob/main/rs/moq-rtc/MIGRATE.md), [moq-rtmp](https://github.com/moq-dev/moq/blob/main/rs/moq-rtmp/MIGRATE.md), [moq-sock](https://github.com/moq-dev/moq/blob/main/rs/moq-sock/MIGRATE.md), [moq-srt](https://github.com/moq-dev/moq/blob/main/rs/moq-srt/MIGRATE.md), [moq-stats](https://github.com/moq-dev/moq/blob/main/rs/moq-stats/MIGRATE.md), [moq-tokio](https://github.com/moq-dev/moq/blob/main/rs/moq-tokio/MIGRATE.md), [moq-transcode](https://github.com/moq-dev/moq/blob/main/rs/moq-transcode/MIGRATE.md), [moq-uring](https://github.com/moq-dev/moq/blob/main/rs/moq-uring/MIGRATE.md), [moq-v4l](https://github.com/moq-dev/moq/blob/main/rs/moq-v4l/MIGRATE.md), [moq-video](https://github.com/moq-dev/moq/blob/main/rs/moq-video/MIGRATE.md) +- **JavaScript:** [`@moq/auth`](https://github.com/moq-dev/moq/blob/main/js/auth/MIGRATE.md), [`@moq/boy`](https://github.com/moq-dev/moq/blob/main/js/moq-boy/MIGRATE.md), [`@moq/clock`](https://github.com/moq-dev/moq/blob/main/js/clock/MIGRATE.md), [`@moq/flate`](https://github.com/moq-dev/moq/blob/main/js/flate/MIGRATE.md), [`@moq/hang`](https://github.com/moq-dev/moq/blob/main/js/hang/MIGRATE.md), [`@moq/json`](https://github.com/moq-dev/moq/blob/main/js/json/MIGRATE.md), [`@moq/loc`](https://github.com/moq-dev/moq/blob/main/js/loc/MIGRATE.md), [`@moq/msf`](https://github.com/moq-dev/moq/blob/main/js/msf/MIGRATE.md), [`@moq/net`](https://github.com/moq-dev/moq/blob/main/js/net/MIGRATE.md), [`@moq/p2p`](https://github.com/moq-dev/moq/blob/main/js/p2p/MIGRATE.md), [`@moq/pattern`](https://github.com/moq-dev/moq/blob/main/js/pattern/MIGRATE.md), [`@moq/publish`](https://github.com/moq-dev/moq/blob/main/js/publish/MIGRATE.md), [`@moq/room`](https://github.com/moq-dev/moq/blob/main/js/room/MIGRATE.md), [`@moq/signals`](https://github.com/moq-dev/moq/blob/main/js/signals/MIGRATE.md), [`@moq/video`](https://github.com/moq-dev/moq/blob/main/js/video/MIGRATE.md), [`@moq/watch`](https://github.com/moq-dev/moq/blob/main/js/watch/MIGRATE.md) -- **Native codecs have `audio` and `video` namespaces.** Construct an - `audio.Producer` or `video.Producer` from the broadcast's - `media.CatalogProducer` instead of `BroadcastProducer.encode_audio` or - `encode_video`; construct codec-only `Encoder` types from settings alone. - Subscribe with `audio.Consumer.subscribe` or `video.Consumer.subscribe` - instead of `BroadcastConsumer.decode_audio` or `decode_video`. Producers - expose subscriber state only through `demand()`. - Audio output now carries the optional track name and defaults frame duration - to zero, which selects the codec's own frame. Video GOP configuration is a - `Keyframe { interval }` variant rather than a bare integer. Python and Go - expose the duration fields as `timedelta` and `time.Duration`. -- **moq-mux's catalog consumer is opaque.** Construct `catalog::Consumer` with - `Consumer::new(broadcast, format).await` or convert an existing - `catalog::hang::Consumer` with `.into()`, then read it through `catalog::Stream`. - Its `Hang`, `Msf`, and `Closed` variants and `catalog::msf::Consumer::track` - are private. -- **Rust audio reads carry encoder ends.** `moq_audio::decode::Consumer::read()` - returns `decode::Read` events: `Frame { discontinuity, frame }` and - `End { discontinuity, end }`; `None` still means the track is finished. An - `End` can be followed by final PCM with the same `discontinuity` before a fresh - encoder bumps it. A bump with no `End` before it is a loss. - Binding audio frame APIs keep their existing shape. -- **JS catalog ranking names its media kind.** Replace `Catalog.ranked(renditions)` - with `Catalog.Video.ranked(renditions)`. `Catalog.Audio.ranked(renditions)` - orders audio by enabled state, bitrate, sample rate, channel count, and name. - Both helpers put disabled renditions last. -- **The `noq` cargo feature is `quic`.** Rename it wherever you enable it on - moq-tokio, moq-uring, moq-relay, moq-cli, or the other crates that forward - it. The QUIC stack is now MoQ's own `moq-quic` rather than the `moq-noq` - crates. In Rust, `moq_tokio::noq::Error` is `moq_tokio::quic::Error`, and - `moq_tokio::Error::Noq` is `moq_tokio::Error::Quic`. A qlog directory gets one - file per endpoint instead of one per connection; on io\_uring workers, one per - endpoint and one per dial. -- **moq-tokio config durations are optional.** Wrap durations in `Some(...)` for - connect `race`, `resolution_delay`, and `timeout`; listen `timeout`; QUIC - `idle_timeout` and `keep_alive`; backoff `initial`, `max`, and `timeout`; - GOAWAY `handover`; and WebSocket `delay`. `None` uses the default, while - `Some(Duration::ZERO)` keeps each knob's zero behavior. Values set in code - now take effect on parsed and merged configs unless the flag or environment - variable was actually given. TOML keys, C ABI, binding records, and wire - formats are unchanged. -- **Max age and max delay count wall-clock time too.** A superseded group is - stale once its successor arrived a full budget ago, as well as by media time, - so an untimed track is no longer exempt: with the default zero `max_delay`, - an untimed subscriber starts at the latest group and skips each superseded - one. Give it a budget to replay or tolerate reordering. -- **`cache::Pool::gc` takes a waiter.** In Rust, `gc(now)` is - `gc(now, waiter)`, like `Driver::poll`: call it again when the waiter wakes, - since a parked read can arm an earlier deadline than the one returned. -- **Native background panics.** UniFFI reports `MoqError::Task` with a panic - reason, and C reports `MOQ_ERROR_PANIC`. Other objects keep running. Browser - WebAssembly still aborts on panic with the current toolchain. -- **Accepted sessions return their driver task.** `moq_tokio::server::Request::ok()` - now returns `(session, driver)`. Await the supervised driver before classifying - the session close, so a driver panic reports `moq_tokio::Error::Panic` rather - than the cancellation its teardown leaves on a channel. -- **A QUIC load-balancer nonce needs its server id.** `listen.lb_nonce`, - `--listen-quic-lb-nonce`, and `MOQ_LISTEN_QUIC_LB_NONCE` without `lb_id` stop - startup. Setting `lb_id` (or `lb_nonce`) and `load_balancer` together also - stops; `lb_id` used to win and the other value was ignored. Set one of them. -- **A replaced broadcast restarts announce consumers, and subscriptions stay.** - Rust's `AnnounceEvent` gains `Restart`, `@moq/net`'s announce events gain the - `"restart"` kind, moq-ffi's `MoqAnnounceEvent` gains `Restart`, and libmoq's - `moq_announce_kind` gains `MOQ_ANNOUNCE_KIND_RESTART`: another publisher - instance now serves the prefix (a newer epoch, or another route without one, - including a reconnect), so request the path again. A newer epoch no longer - ends subscriptions with `Unroutable`; they stay on the old instance until - dropped or its route goes. A restart was an end then a start, and still is on - moq-lite 06 and older and moq-transport. moq-lite 07 adds `ANNOUNCE_RESTART` - (0x3), and lite-06's `ANNOUNCE_RESTART` is named `ANNOUNCE_UPDATE`, as it - always meant. -- **@moq/net announced requests follow restarts, and `broadcasts()` is gone.** - An unpinned `request(path, { announced: true })` now stays open across - publisher instances: its `active` moves to each new instance's broadcast and - clears between them, instead of the request ending with `Unroutable`. Read - `active` reactively and close the request when done; add `epoch` to keep the - old single-instance behavior. `Origin.Producer` and `Origin.Consumer` drop - `broadcasts(scope)`; iterate `announced(scope)` and keep the entries you need. - `@moq/watch` with `announced` off no longer moves to a later publisher. -- **@moq/net discovers only what something watches, and `discovery` is gone.** - A `consume` origin stays empty until a reader watches it with - `announced(scope)` or `request(path, { announced: true })`; the session then - asks the relay about those prefixes only. Code that peeked the table - (`request(path)` expecting a route) without watching should hold an - `announced(scope)` stream. The `discovery` option on `connect`, `accept`, and - `Connection` and `Established.discovery` are removed, and passing the option - throws a `TypeError`: drop it. A relay that refuses an announce request - downgrades that session to blind requests on its own. -- **JS time units and timers come from `@moq/clock`.** `Time.Nano`, `Time.Micro`, - `Time.Milli`, and `Time.Second` are gone from `@moq/net`: import `Nano`, - `Micro`, `Milli`, and `Second` from `@moq/clock`, which throws on negative or - non-finite values. `effect.timer`, `effect.timeout`, and `effect.interval` - take a `Nano` (`Nano.fromMilli(Milli(250))`), not milliseconds, and run on the - installed clock. `sleep` and `sleepUntil` take `{ signal }` instead of a bare - signal. `@moq/room`'s `HISTORY` is a `Nano`. -- **"Epoch" means only the publisher epoch.** The reconnect counter is - `connects()`: `moq_tokio::Connection::epoch()` and moq-ffi's - `MoqSession::epoch()` are `connects()`, which is Python's `connects()`, Go's - `Connects()`, Swift's and Kotlin's `connects()`, and Dart's `connects`. The - audio re-anchor is `reanchor()`: `moq_audio::encode::Producer::reset_epoch` and - moq-ffi's `MoqAudioProducer::reset_epoch` are `reanchor`, which is Python's - `reanchor()` (was `reset_epoch()`), Swift's, Kotlin's, and Dart `moq_ffi`'s - `reanchor()` (was `resetEpoch()`), and Go's `Reanchor()` (was `ResetEpoch()`). -- **`moq export ts --linger` waits only for the same publisher instance.** A - restarted publisher (a new epoch, or any return on a version without one) - exits 1 unless `--stitch` follows it as a program switch. `export srt` takes - the same two flags. In Rust, moq-mux's `ts::Export::resume` is - `ts::Export::follow`, `Source::returned` is gone, and `ts::Follower` follows a - path's announcements the way both exports do. - `ts::Export::new(source, catalog)` now takes a catalog stream, like the other - container exporters. Subscribe with `source.catalog::(format).await?` - to retain MPEG-TS metadata, or use `()` for media only; select renditions on the - stream before passing it in. `follow(broadcast, catalog)` also takes the replacement - stream. Both methods are synchronous; `with_ts` and `with_catalog_format` are removed. -- **moq-mux's `Source` is built from the resolved catalog broadcast.** - `Source::new(origin, path)` is `Source::new(origin, broadcast)`: resolve the - broadcast first (`origin.request_broadcast(path, None)`), and its path is - where catalog references resolve from. `Source::broadcast()` returns that - broadcast without awaiting. Every later request for a path, siblings - included, stays on the broadcast the source first held there and fails once - it ends, rather than reaching a replacement; `bind` included. Build a new - `Source` to follow a new instance. moq-hls's `Broadcaster::new` no longer - awaits or fails, and a rendition whose sibling broadcast ends now ends its - playlist instead of rebinding. -- **fMP4 export of Annex-B H.264 and H.265 inits from the catalog.** When the - catalog codec string and dimensions are enough, `moq export fmp4` writes an - `avc3` or `hev1` init segment before the first keyframe and leaves SPS, PPS, - and VPS in the samples. High AVC profiles (110, 122, 244, and the rest whose - chroma or bit depth the string does not carry), HEVC beyond Main and Main - Still Picture, and a catalog missing dimensions still wait for the SPS. A - returning Annex-B rendition is matched on that catalog record, so an encoder - that restarts with a new SPS can return. A keyframe whose parameter sets never - appeared in the track ends the export instead of waiting 30 seconds. -- **moq-binary is moq-flate, and @moq/binary is @moq/flate.** The opaque - `snapshot` and `stream` tracks moved beside the codec; the wire and the - catalog's `binary` section are unchanged. In Rust, `moq_binary::X` is - `moq_flate::X`, and `moq_binary::Error::Flate(e)` is the matching - `moq_flate::Error` variant (`Decompress`, `TooLarge`). `moq_flate::Error` - now carries `moq_net::Error`, so it is no longer `PartialEq`. moq-mux's - `Error::Binary` is `Error::Flate`. In TypeScript, import `Snapshot` and - `Stream` from `@moq/flate`. -- **moq-ffi data tracks wrap a track.** The broadcast's - `publish_binary_*`, `publish_json_*`, and `subscribe_json_*` are gone. - Create the track with `publish_track` or `subscribe_track`, then construct - `MoqFlateSnapshotProducer` / `MoqFlateStreamProducer` (taking - `MoqFlateConfig`) or `MoqJsonSnapshotProducer` / `MoqJsonStreamProducer` - from the broadcast and the track, or `MoqJsonSnapshotConsumer` / - `MoqJsonStreamConsumer` from the track. The wrappers keep JSON under its own - namespace: `moq.json` in Python, `moq.dev/moq/json` in Go, `dev.moq.json` in - Kotlin, `package:moq/json.dart` in Dart, and `Json` in Swift. The C - `moq_publish_binary_*` calls are unchanged. -- **moq-ffi clients and servers take a config record.** `MoqClient::new()` and - `MoqServer::new()` plus their setters are `MoqClient::new(MoqClientConfig)` and - `MoqServer::new(MoqServerConfig)`, with nested `tls`, `quic`, `websocket`, and - `backoff` records and a new `versions` list. A value the native side cannot use - fails construction with `MoqError::Config`, including a bad bind address that - used to fail later as `Bind`. The `subscribe` origin is `consume` everywhere: - Python's `Client(..., subscribe=)` is `consume=`, Go's `WithSubscribeOrigin` is - `WithConsumeOrigin`. A server request's `set_publish` / `set_consume` are - arguments to `accept(publish, consume)`, where null inherits the server's - origin (Go: `Accept(ctx, nil, nil)`). Go's `Requests(ctx)` iterators are - `All(ctx)`, and its `Status*` constants are `ConnectionStatus*`; Kotlin and - Dart read announcements as `announced(config).updates()`. -- **moq-ffi media lives in a `media` namespace.** The broadcast's - `publish_audio`, `publish_video`, `publish_container`, their `_on_track` and - `_stream` variants, `set_video_properties`, `set_catalog_section`, - `subscribe_catalog`, `subscribe_media`, and `fetch_media_group` are gone. - Construct `MoqMediaTrackProducer::audio` / `video` (from the broadcast, a - `MoqMediaTarget::Named` or `Requested` target, and the init record), - `MoqMediaContainerProducer`, `MoqMediaCatalogProducer`, - `MoqMediaCatalogConsumer::subscribe`, `MoqMediaTrackConsumer::subscribe`, - or `MoqMediaGroupConsumer::fetch` instead. Producers drop `name`, - `used`, and `unused`; read them through `demand()`. The wrappers expose these - as `moq.media` in Python, `moq.dev/moq/media` in Go, `dev.moq.media` in - Kotlin, `package:moq/media.dart` in Dart, and `Media` in Swift. Go fetches a - group with `media.FetchGroup`; its `NewGroupConsumer` decodes - a raw group instead. -- **Media subscribe and fetch take a catalog rendition.** `MoqMediaTrackConfig` - and `MoqMediaGroupConfig` replace `name` and `container` with a `rendition` - (`MoqRendition::Video` or `Audio`: the rendition's catalog `id` and its - config), and follow the rendition's `broadcast` and `track` themselves, so drop - the `resolve` call and the `track`-or-ID fallback before them. Python passes - the ID and config (`TrackConsumer.subscribe(broadcast, id, rendition)`, - `GroupConsumer.fetch(broadcast, id, rendition, sequence)`); Go builds one with - `media.VideoRendition` or `media.AudioRendition`. The audio and video decode - consumers' `name` argument is `id` too, matching hang and `@moq/hang`: Swift's - `subscribe(broadcast:name:rendition:)` is `subscribe(broadcast:id:rendition:)`. -- **moq-ffi error payloads are named.** `MoqError` cases that carried a - positional string (`Transport`, `Internal`, `Connect`, ...) name it - `reason` (`Reason` in Go, the `reason:` label in Swift). The Display text is - unchanged. -- **Python and Go durations are native.** `Frame`, `Datagram`, `Subscription`, - `TrackInfo`, and `ConnectionStats` carry `timedelta` in Python and - `time.Duration` in Go instead of microsecond integers: `max_delay_us` is - `max_delay`, `timestamp_us` is `timestamp`, and `rtt_us` is `rtt`. A frame - timestamp read from an untimed track is `None` / `nil`. A Python string - passed where a list of strings belongs (`tls_roots="ca.pem"`) raises - `TypeError` instead of splitting into characters. -- **Demand is read through `demand()`.** In Rust, `track::Producer`'s - `is_used`, `used`, `unused`, and `poll_unused` are `producer.demand().X`, - and so are `group::Producer`'s `used` and `unused`. - `track::Request::poll_unused`, `track::Dynamic::poll_unused`, and - `group::Request::poll_unused` are `demand().poll_unused`, which returns - `Poll>` instead of `Poll<()>`: an `Err` means the request closed, - so treat it as unused too. A `group::Request` no longer needs polling to be - withdrawn; the last `fetch_group` caller leaving does it, so a handler that - sees it unused just drops it. A fetch arriving after that queues a fresh - request instead of joining the abandoned one, so a handler that keeps serving - without watching `demand()` may see its `accept` return `Error::Duplicate`. - The moq-json snapshot and moq-flate `is_used()` is `demand().is_used()`. - In TypeScript, `Track.Producer`'s `used` and `unused()` are - `producer.demand().used` and `.unused()`, as are `Group.Producer`'s, and - `Allocator.reserve` takes `producer.demand()`, replacing the - `Bandwidth.Demand` interface. -- **`request_broadcast` takes an epoch.** In Rust, - `origin::Consumer::request_broadcast(path)` is - `request_broadcast(path, None)`; pass an `Epoch` instead to refuse any other - publisher instance. In TypeScript, `RequestOptions.epoch` does the same. A - dynamic route updated to another epoch now refuses the requests its handler - still holds with `Unroutable`, so a handler answering them late sees its - answer dropped. -- **`moq --hop` is removed; `--epoch` replaces it.** A redundant pair shares an - epoch, a UUIDv7 such as `uuidgen -7` prints, instead of a Hop ID: pass the - same `--epoch` (or `MOQ_EPOCH`) to both. Unlike a rename, `--hop` is now an - unknown flag and `MOQ_HOP` is silently ignored, so remove it from any - deployment: a pair still keyed on `MOQ_HOP` mints an epoch per process, and - whenever either member starts or restarts, its new epoch replaces the other's - broadcast and restarts its viewers. `--cluster-id` no longer falls - back to `--hop`, so a node that pinned its Hop ID with `--hop` passes - `--cluster-id`. Relays no longer put a random hop in front of a route that - names no publisher; it keeps its 0. -- **The `"auto"` delay is measured, not derived from RTT** (#4162). It is sized - from how late frames arrive (see [audio jitter](/concept/audio-jitter)) in - `@moq/watch` and `moq play`, which now defaults `--delay` to `auto` instead of - `100ms`. A numeric `@moq/watch` delay is taken literally instead of having - the rendition's own delay added on top. `Sync.out.jitter` now always - equals `Sync.out.delay`, and `"auto"` with no decoder registered resolves to - 0 rather than 100 ms. -- **`moq export ts`, `flv`, and `mkv` take `--delay`** (#4645). Each frame is - written that long after its decode time, on a clock that follows the - source's; the older staleness spellings are refused with the new flag - named. In Rust, moq-mux's `ts::Export::with_max_age` and the FLV and MKV - exports' `with_max_age` are `with_delay`. moq-rtmp's FLV egress releases - frames on its `max_delay` the same way. `ts::stats::Export` gains the - dropped-frame count (late frames, and the video frames then dropped waiting - for a keyframe), measured drift and out-of-tolerance count beside its - `streams` rows. It is no longer `Eq`. -- **moq-mux has no clock translators.** `clock::Anchor`, `clock::Lane`, and - `SourceMap` (#4667) are gone, along with the importers' `live()`. Publish the - source's own timestamps and let the catalog clock map them to wall time. - An importer whose first frame arrives once the clock is in use (taken with - `catalog.clock()`, published in a catalog, or pinned with - `Config::with_clock`) shifts its timestamps onto it, so its first frame lands - at now; a `with_clock` catalog no longer keeps an importer's timestamps - verbatim. Importers sharing a timestamp base reserve through one - `catalog.timebase()`. -- **moq-net owns its transport traits.** `moq_net::web_transport_trait` is - gone, and `transport::poll::{Session, SendStream, RecvStream}` no longer - extend `web_transport_trait::poll`. They carry their own `poll_*` methods, - `transport::Error`, and `transport::Stats`, and `Error::from_transport` takes - a `transport::Error`. moq-tokio's `Client` and `Server` are unchanged. A - custom transport handed straight to moq-net implements these traits; a - `moq-uring` session is wrapped with `moq_uring::transport::Session::new`. - Each session must report `binding()`: `transport::Binding::Uri` when the - transport handshake carries the request URI, or `InBand` when SETUP carries - it. The moq-tokio and moq-uring adapter constructors take this binding - explicitly; browser WebTransport adapters always report `Uri`. -- **`--cluster-mesh` and `--cluster-linger` are unknown flags.** moq-relay - 0.17 refuses them by name; later relays reject them, and TOML `mesh` and - `linger`, like any unknown setting. `MOQ_CLUSTER_MESH` and - `MOQ_CLUSTER_LINGER` are no longer read, so drop them from the environment. - In Rust, `cluster::Config` has no `mesh` or `linger` field. -- **Settings no listener reads stop startup.** A stream-only relay or `moq` - listener (TCP or Unix, no `--listen`) refuses the QUIC-only - `--listen-preferred-v4`/`-v6`, `--listen-quic-lb-id`, and pinned `tls.peers`, - and a `--listen-tls-cert`, `-key`, or `-generate` unless `--listen-tcp-tls` - serves it. `--listen-unix-allow-*` needs `--listen-unix-bind`, and - `web.https.cert`, `key`, and `root` need `web.https.listen`. `moq` without a - listener refuses `--listen-*` and `--auth-*` flags. Each used to be ignored; - drop it, or add the listener it configures. -- **Data publishes preserve optional timestamps.** `moq_net::Timed

` drops - its clock type parameter. JSON and flate snapshot/stream producers and JSON - window `push` / `pop` publish `at: None` untimed, so their track must also be - untimed. Pass a timestamp explicitly on timed tracks. Snapshot `modify(at)` - and `mutate(at, f)` carry the caller's optional time through the edit guard. - moq-mux data timestamps use the broadcast clock; convert a capture `Instant` - with `.at(catalog.clock().capture(instant)?)`. FFI data operations take an - optional timestamp in microseconds, and raw track info's null timescale now - means untimed on publish as well as receive. Python/Swift raw writes no - longer default to zero. Whole-frame media writes now require caller time; - container imports keep their existing timeline handling. `media.CatalogProducer.now()` samples and fixes the - broadcast clock for explicit local-event stamps. A supplied timestamp ahead - of now is published rather than refused. -- **Container byte-stream importers take an init record.** The bindings' - `ContainerStreamProducer` (moq-ffi's `MoqMediaContainerStreamProducer`) takes - a `ContainerStreamInit` holding the format instead of a bare format. Its - optional `timebase`, also on `ContainerInit`, shares one source's offset - across container imports; leave it unset for the previous independent offset. -- **moq-mux importers publish the catalog at their first frame.** The fMP4, - MKV, and MPEG-TS importers used to publish it at their init segment (`moov`, - `Tracks`, or the first PMT) on a provisional clock, then re-anchor it on the - first frame. They now hold it until that frame, as FLV already did, so the - first snapshot carries the final root `clock`. A reader waiting for the - catalog now waits for media of a selected track, not just the init segment: - a track `with_select` deselects doesn't release it, even if its media - arrives first. A `moov` or `Tracks` decoded after `finish()` is refused with - `fmp4::Error::MoovAfterFinish` or `mkv::Error::TracksAfterFinish`, since the - tracks it declares could never finish. -- **fMP4 export fixes its track set at the init segment.** moq-mux's - `fmp4::Error` drops `MissingVideoTrack`, `MissingAudioTrack`, and - `NoCatalogSnapshot`, and adds `TrackAdded`, `TrackChanged`, `TrackRewound`, - and `TrackUndescribed`. `fmp4::Export` and `moq export fmp4` now end with one - of these where they used to write a track missing from the moov, and a - broadcast that ends with media queued behind an undescribed track is an error - rather than an empty `Ok(None)`. Restart the export to pick up a new - rendition. -- **Subscriber staleness is max delay.** How far a group may fall behind the - live edge before a subscriber skips it is now `max_delay`, so it no longer - shares a name with a publisher's retention, which keeps `max_age`. In Rust, - `track::Subscription::max_age` and `with_max_age` are `max_delay` and - `with_max_delay`; moq-mux's `container::Consumer::set_max_age` and the fMP4, - H.264, and H.265 exports' `with_max_age` are `set_max_delay` and - `with_max_delay`; moq-audio's and moq-video's `decode::Options::max_age` is - `max_delay`, as is moq-audio's `decode::Consumer::max_age()`; and moq-rtmp's - `Play::with_max_age`, `Client::with_export_max_age`, - `listen::Config::export_max_age`, and `DEFAULT_MAX_AGE` are `with_max_delay`, - `with_export_max_delay`, `export_max_delay`, and `DEFAULT_MAX_DELAY`. In - TypeScript, `Track.Subscription`'s `maxAge` is `maxDelay`, as are - `Container.Consumer`'s `maxAge` prop and `@moq/watch`'s `Sync.out.maxAge`; - JavaScript refuses a `maxAge` key in subscription options or container consumer - props with a `TypeError` naming `maxDelay`, including when both keys are supplied - or `maxAge` is `undefined`. Untyped callers must rename it. - moq-ffi's `MoqSubscription`, `MoqAudioDecoderOutput`, and - `MoqVideoDecoderOutput` take `max_delay_us` (each binding in its own casing), - and so do C's `moq_subscription`, `moq_audio_decoder_output`, - `moq_video_decoder_output`, `moq_consume_video`, and `moq_consume_audio`. - `moq export fmp4`, `h264`, `h265`, and `rtmp` take `--max-delay`, and - refuse `--max-age`. `track::Info::max_age`, - `MoqTrackInfo.max_age_us`, and `moq import --max-age` are unchanged, as is the wire. -- **moq-relay auth takes the client-CA answer.** `auth::Config::validate` and - `init` take `client_ca: bool`, whether any listener verifies client - certificates, and `validate_client_ca` is gone. `moq --listen` with an - invalid auth config stops at startup instead of refusing every session. -- **A client CA needs a QUIC listener.** A stream-only relay or `moq` listener - (TCP or Unix, no `--listen`) refuses to start with `listen.tls.root`, which - nothing verified; moq-tokio returns `Error::MtlsUnsupported` for it. -- **moq-tokio's `Transport` names WebTransport.** `moq_tokio::server::Transport` - is `moq_tokio::Transport`, with no re-export. A WebTransport session reports - `Transport::WebTransport` (`"webtransport"` in logs) instead of `Quic`, which - now means raw QUIC only, and `Connection::transport()` reports the live - transport. The bindings' `MoqTransport` gains a `WebTransport` case, so an - exhaustive `switch` or `when` needs one more arm. -- **moq-net has no `VarInt`.** Varints are plain `u64`s: - `VarInt::decode_quic(buf)?.into_inner()` is `moq_net::varint::decode_quic(buf)?`, - and `VarInt::try_from(v)?.encode_quic(buf)` is - `moq_net::varint::encode_quic(v, buf)`, which fails past - `varint::MAX_QUIC` (2^62 - 1). -- **Opus mapping family lives only on `mapping`.** - `moq_mux::codec::opus::Config::mapping_family` is gone. Family 0 is - `mapping: None`; any other family is the mapping's own (`mapping.family()`). - Set `mapping` alone when building a surround head. The OpusHead bytes are - unchanged. -- **moq-mux TS stats live in `ts::stats`.** `ts::Stats` is - `ts::stats::Snapshot` and `ts::StreamStats` is `ts::stats::Stream`, whose - `track` is an owned `String`. `ts::Export::stats` returns - `ts::stats::Export`; feed it to `stats::Log` with `.into()`. `ts::MultipleProgramsError` is - `#[non_exhaustive]`: recover it by downcast and read `programs`. -- **@moq/publish drops `OpusConfig.usedtx`.** Chromium's DTX output shifts the - audio timeline, so Opus DTX is always off (the WebCodecs default). Remove the - field; a plain-JS caller still passing it is ignored. -- **FLV export takes a catalog stream.** `flv::Export::new` is synchronous and - takes `(source, catalog)`, like `fmp4::Export` and `mkv::Export`. - `with_catalog_format` and `with_select` are gone. Open the catalog yourself - (`source.catalog(format)`) and narrow it with `catalog::Stream::select` - before constructing the export. `moq export flv` still applies the same - rendition flags. +## Unreleased + +These land with the next breaking release. + +- **Data-track breaks are an object with no bytes** (#5815). A hang data + track (TS verbatim PES, sections, `m2ts`, text) marks a discontinuity with a + group of one empty object instead of a skipped group. Older consumers fail + to decode it, so upgrade exporters and players before publishers. +- **Auth requests name `webtransport`** (#5293). Relays report a WebTransport + session as `webtransport` instead of `quic`, and older auth servers reject the + new value, so upgrade auth servers before relays. +- **Capped grants need upgraded verifiers** (#5697). A token whose `publish` + or `subscribe` carries a `rate` cap encodes that role as an object, which + released verifiers reject, so upgrade every auth server and verifier before + issuing capped tokens. +- **Archive timelines declare a duration** (#5715). Each catalog + `archive.timelines` entry is an object instead of a track name. Neither side + reads the other's shape, and a catalog it can't read keeps its live media but + drops the archive, so seeking, recording, and HLS history need publishers and + their readers upgraded together. +- **Announce restarts.** moq-lite 07 adds `ANNOUNCE_RESTART` (0x3): another + publisher instance now serves the prefix. lite-06's `ANNOUNCE_RESTART` is + named `ANNOUNCE_UPDATE`, as it always meant. On moq-lite 06 and older and + moq-transport, a restart is still an end then a start. +- **Publisher epochs ride the route** (#4942). moq-lite 07 carries the epoch + after the path in `ANNOUNCE_START`, `TRACK`, `SUBSCRIBE`, and `FETCH`. + Routes without an epoch no longer resume across routes, so default lite-06 + sessions, including cluster GOAWAY redials, lose seamless cross-route + failover until lite-07 is the default. +- **Route cost is one value** (#4661). Upgraded nodes write lite-06's cold + cost at 2^62-1, so an older relay that still ranks on `(warm, cold)` breaks + equal-warm ties against routes that crossed an upgraded node. It is only a + tie-break and ends once the fleet is upgraded. - **A track is timed or untimed, and nothing fills in a timestamp on - receive.** An untimed track (no timescale) arrives untimed, except over - moq-lite 05 and later, which can't encode absence yet and carry a send time - instead; see [untimed tracks](/concept/moq-lite#subscriptions). A write whose - timedness doesn't match its track fails with `TimestampMismatch`. - - moq-net: `track::Info.timescale` is `Option`, `None` for an - untimed track (`with_timescale` takes `impl Into>`), and - `group::{Producer,Consumer}::timescale()` return `Option`. - `Frame.timestamp`, `frame::Info.timestamp`, and `Datagram.timestamp` are - `Option`. The `write_frame`, `append_datagram`, and - `insert_datagram` calls take `impl Into>`, so passing a - `Timestamp` still compiles. - An undeclared timescale is untimed: `track::Info::default()`, - `create_track(name, None)`, and `accept(None)` no longer default to - milliseconds, and `Timescale` has no `Default`. A track you write - timestamps to declares one, such as - `Info::default().with_timescale(Timescale::MILLI)`. - - moq-e2ee: `Frame` and `Datagram` timestamps are optional, and its writers - take `impl Into>`. - - moq-archive: recording an untimed track fails with `Error::Untimed`, so - `moq export archive` over moq-lite before 05 or moq-transport drafts - 14–16 now fails instead of recording arrival times. - - moq-ffi: `MoqFrame.timestamp_us` and `MoqDatagram.timestamp_us` are - optional, null on a frame read from an untimed track, and default to null. - A raw track you publish is timed, so a raw write without one now fails - with `TimestampMismatch` instead of going out at 0. - `MoqMediaProducer::write_frame` without one used to pass a PTS of 0; it - now passes none, so the importer derives the time itself (elapsed since - its first frame) or refuses a codec that needs one (avc1, hvc1). - `MoqTrackInfo.timescale` is null on a received untimed track. In Go, - `Frame.TimestampUs` and `Datagram.TimestampUs` are `*uint64`; in Kotlin - and Dart, `Frame.timestamp` and `Datagram.timestamp` are nullable. - - libmoq: `moq_frame` and `moq_datagram` gain `timestamp_present`, which - changes their size, so recompile against the new `moq.h`. - - Receivers: tracks from moq-lite before 05, and moq-transport tracks - without `TIMESCALE` (every track on drafts 14–16), arrive untimed instead - of stamped with their arrival time; an object-scope Timescale is ignored. - On a moq-transport track with `TIMESCALE`, an object without a Timestamp - is malformed. A new subscriber with no start on an untimed track starts at - the latest group. -- **CMAF decodes at the frame timestamp.** A fragment's earliest sample - presents at its moq-net frame timestamp; `tfdt` only orders the samples. In - TypeScript, `Container.Format.decode` and `Cmaf.decodeDataSegment` take that - timestamp: pass `frame.timestamp` alongside `frame.payload`, or `undefined` - for an untimed frame, whose samples present at `tfdt`. - `Cmaf.decodeTimestamp` is gone; the frame timestamp is the fragment's time. - moq-mux's CMAF `Wire::write` refuses a track whose timescale isn't the init's - `mdhd` timescale with `fmp4::Error::TimescaleMismatch`. `import::Track` and - `TrackStream` accept a CMAF rendition's track at that timescale; a track you - create yourself declares it with `track::Info::with_timescale`. Both decoders refuse a `trun` whose - `data_offset` doesn't start at the next sample in the `mdat`. -- **@moq/json and @moq/flate consumers return `Timed` values.** The snapshot - and stream consumers' `next()` and async iterator yield `{ value, at }`, - where `at` is the frame's timestamp and absent on an untimed track; read - `.value` for what they used to return. A snapshot consumer's `next()` now - yields every state in order. Call `latest()` for the old behavior, which - skips to the newest state. A caller that passes the result on as `any` or - `unknown` (logging, `JSON.stringify`, a generic setter) still compiles but - now gets the wrapper and every intermediate state, so search for each - `next()` and `for await` over a snapshot consumer rather than relying on - type errors. On an untimed track nothing marks a group stale, so a `next()` - reader that falls behind replays the whole backlog where it used to skip it. -- **`moq export ts --delay` defaults to 1 s and includes the send-ahead.** Each - frame still goes out up to the delay ahead of its decode time, now no further - than its decoder buffer holds, and that comes out of the same budget: the - output trails a TS source by the delay instead of twice it. With a multiplex - rate, video whose HRD declares a longer buffer delay than `--delay` stops the - export with the delay it needs named, so a broadcast feed run at - `--delay 500ms` needs `--delay 1s`. A source that sends every track at its - decode time (RTMP, a browser) trails by the delay plus the video's - send-ahead: 2 s at the new default, where `--delay 500ms` keeps the 1 s it - had. moq-srt egress runs at its SRT latency or 1 s, whichever is longer. -- **`moq_origin_announced_broadcast` delivers every publisher instance.** Its - callback gets a new broadcast handle each time another instance takes over - the path or the broadcast returns, and its terminal `0` only on cancel. Free - each handle with `moq_consume_close` once its successor arrives. A binding's - `AnnouncedBroadcast.available()` works the same way, and its first call is - unchanged. + receive.** Tracks from moq-lite before 05, and moq-transport tracks without + `TIMESCALE` (every track on drafts 14 to 16), arrive untimed instead of + stamped with their arrival time. moq-lite 05 and later can't encode absence + yet and carry a send time instead; see + [untimed tracks](/concept/moq-lite#subscriptions). A new subscriber with no + start on an untimed track starts at the latest group. +- **Max age and max delay count wall-clock time too.** A superseded group is + stale once its successor arrived a full budget ago, as well as by media + time, so an untimed track is no longer exempt. With the default zero + `max_delay`, an untimed subscriber starts at the latest group and skips each + superseded one; give it a budget to replay or tolerate reordering. -## Wire +## Released Older protocol versions still negotiate, so relays and clients can be upgraded -in any order, apart from [pattern-only token grants](#relay-and-cli) and two wire -changes: +in any order, apart from these: - The lite 06 ALPN is `moq-lite-06`, not `moq-lite-06-wip`. An explicit `moq-lite-06-wip` in a version list is refused (#3941). @@ -486,206 +85,16 @@ changes: a root `clock: { wall, timescale }` (#3612, #3675). A new `moq export hls` finds no timeline in an old publisher's catalog, so upgrade publishers before the HLS gateway. - -## Relay and CLI - -The `moq-relay` and `moq` flags split into `--listen-*` (accepting), -`--connect-*` (dialing), and a shared `--quic-*` section. Environment -variables follow the flag (`MOQ_SERVER_BIND` is `MOQ_LISTEN`). - -| Before | After | -| --- | --- | -| `--server-bind` | `--listen` | -| `--server-*`, `--tls-cert`, `--tls-key`, `--tls-generate` | `--listen-*`, `--listen-tls-cert`, `--listen-tls-key`, `--listen-tls-generate` | -| `--client-connect` | `--connect` | -| `--client-*` | `--connect-*` | -| `--client-failover-delay` | `--connect-race` | -| `--client-reconnect=false` | `--connect-once` (inverted) | -| `--tls-disable-verify`, `--client-tls-disable-verify` | `--connect-tls-insecure` | -| `--server-quic-*`, `--client-quic-*` | `--quic-*`, applied to both directions | -| TOML `[server]`, `[client]` | `[listen]`, `[connect]` | -| TOML `[server.quic]`, `[client.quic]` | `[quic]` | -| TOML `listen`, `connect`, `failover_delay`, `reconnect`, `disable_verify` | `bind`, `url`, `race`, `once` (inverted), `insecure` | -| `--cluster-linger` | removed; a broadcast closes when its last publisher is lost | -| `--cluster-connect host:port` | a full URL, `https://host/?jwt=TOKEN` | -| `--cluster-mesh`, TOML `mesh` | removed; list every peer with `--cluster-connect` or `--cluster-connect-api` | -| `moq --origin`, `--name`, `--latency-max` | `--hop` (removed for `--epoch` after [Unreleased](#unreleased)), `--broadcast`, `--max-age` | -| `moq publish`, `moq subscribe` | `moq import`, `moq export` | -| `moq token`, the `moq-token` binary | `moq auth` | - -Other changes to a deployment: - -- **Auth is one contract** (#3688). The relay asks an auth server per session - (`--auth-url`) or applies a static anonymous grant (`--auth-public`); exactly - one is required. `--auth-key`, `--auth-key-dir`, `--auth-api`, - `--auth-api-mode`, `--auth-public-api`, `--auth-domain`, `--auth-mtls-tier`, and `--auth-tls-*` - are gone: run `moq auth serve --key-dir ...` next to the relay and point - `--auth-url` at it. The flag-by-flag mapping is in - [Migrating from the relay flags](/bin/relay/auth#migrating-from-the-relay-flags). -- **Grants are patterns, not prefixes.** `anon` is now exactly the broadcast - `anon`; write `anon/**` for the subtree. This applies to `--auth-public`, - TOML `public`, and the `[auth.public]` table, which is now - `public_subscribe` / `public_publish`. A public or mTLS pattern with no - wildcard refuses to start, naming the subtree to write, rather than pick - one reading silently. The patterns are rooted at `/`, as in 0.14, so - `anon/**` admits a client dialed at `/anon` and refuses one dialed outside - `anon/`. Earlier 0.15 releases rooted them at the dialed path instead. -- **Token grants are patterns.** JWT `publish` and `subscribe` claims are - patterns, so a token granting `alice` covers only `alice`; sign `alice/**` - instead. Existing `put`/`get` tokens and key scopes keep working as subtrees, - and subtree-only grants are still signed in that form, so a `moq-token` - deployment can upgrade issuers and verifiers in either order. Verifiers on - the pattern-only `moq-auth` 0.1.0/0.1.1 or `@moq/auth` 0.1.x/0.2.0 refuse - that form, so upgrade them before their issuers. Grants only a pattern can - express need an upgraded verifier. -- **Removed auth settings refuse to start.** 0.14's `[auth]` `key`, `key_dir`, - `auth_api`, `domains`, `mtls_tier`, and `[auth.tls]`, and their flags and - `MOQ_AUTH_*` variables, stop the relay with the replacement named rather - than being ignored. -- **Token claims.** `iss`, `sub`, and `jti` are ignored and `nbf` is enforced. - Any other claim refuses the token with its name, `aud` and `cluster` - included, where 0.14 ignored all but `aud`: an issuer adding app claims such - as `user_id` must drop them. -- **Every credential is evaluated or refused.** A relay on `--auth-public` - refuses a session presenting a token, as 0.14 did, including peers sending - `cluster.token`, and refuses to start with a client CA. `moq auth serve` - refuses a session presenting both a JWT and a certificate, so a peer - presents one or the other. -- **`moq auth serve` never re-checks or expires by default**, as 0.14 never - did. `--revalidate` needs `--expires`, and `--limit-*` needs `--revalidate`. -- **mTLS admits nothing on its own.** A verified client certificate is reported - to the auth server, which grants it. `moq auth serve --mtls-publish '**' --mtls-subscribe '**' --mtls-peer` restores the old full access for every certificate - the relay's client CA verifies, as a cluster peer, so keep that CA to cluster peers. -- **`moq --listen` needs auth.** A CLI listener refuses to start without - `--auth-url` or `--auth-public` instead of accepting everyone. -- **Gossip discovery is removed.** A relay dials only the peers it lists or - finds on the LAN, never a URL learned from an announcement, and no longer - announces `.internal/origins`. The `/nodes` endpoint lists only peers this - relay dialed. Until every relay that ran `--cluster-mesh` is upgraded, keep - client grants off `.internal/`: an older relay still dials any URL announced - there with `cluster.token`, and an upgraded peer still forwards it. -- **Other 0.14 auth differences kept.** Peers identify by certificate or LAN - path. An auth server's `root` alias may have any depth. - A path in `--cluster-connect` is not refused, although it shifts the mesh - frame. `moq auth serve --key` takes a file, not an https or JWKS URL. - `moq auth sign --root` is the token root and JS `verify --root` the dialed - path. `/.cluster*` roots are reserved. `--auth-public a,b` splits on commas. -- **noq is the only QUIC stack** (#3811). The `quinn` and `quiche` cargo - features and the backend setting are gone. -- **Stats counters** are `*_started` / `*_ended` (`sessions_started`, - `announces_ended`, ...). This release still writes the old `announced` / - `*_closed` names beside them, so move consumers now. - -## GStreamer - -- `moqsink` properties `estimated-send-bitrate` / `estimated-recv-bitrate` are - `estimated-send-rate` / `estimated-recv-rate`, with no alias; a `gst-launch` - line naming the old ones fails at runtime. - -## Rust - -- **moq-native is moq-tokio** (#2896). moq-native 0.20.0 is a stub that fails - to compile with the rename. `ClientConfig::default().init()?` is - `moq_tokio::connect::Config::default().init(quic)?`, and - `with_publisher(&origin).with_subscriber(origin)` is `with_origin(origin)`. - Names sit under their modules (`connection::Goaway`, `connection::Monitor`, - `transport::Session`; #3745). -- **moq-token is moq-auth** (#3684). `Claims` holds `Patterns`, and - `Claims::authorize` returns pattern residuals. -- **Origins** (#3400, #3804). `Origin::random().produce()` is - `moq_tokio::origin::spawn()`. `create_broadcast(path, route)` is - `origin.publish(path, route)`, or `create_broadcast(path)` then - `broadcast.announce(route)`. `with_root` plus `scope(prefixes)` is one - `scope(root, &patterns)` returning `Result`. `origin::Info` is - `origin::Config`. -- **Announcements are prefix routes** (#3225, #3770). `announce::Update` is - `{ prefix, route, kind, captures }`: skip `!update.kind.is_active()` and - resolve the broadcast with `consumer.request_broadcast(&update.prefix, update.route.epoch)`. - Serve a subtree on demand with `origin.dynamic(prefix, route)`. -- **Tracks.** `with_latency_max` / `latency_max` is `with_max_age` / `max_age`. - `write_datagram(Datagram)` is `insert_datagram(sequence, timestamp, payload)` - (#3666); `append_datagram` is unchanged. `track::SubscriberControl` is - `track::Control`, `track::GroupRequest` is `group::Request`, and - `ConnectionStats` is `session::Stats` with `estimated_send_rate`. Receive - estimates come from an owned `session.recv_bandwidth()` consumer: hold it to - request PROBE and drop the last consumer to stop it. -- **Oversized groups abort** with `GroupTooLarge` instead of shedding their head - (#3585). -- **Catalog edits are fallible** (#3644, #3813). moq-mux and moq-json `lock()` - is `modify()?`, and a guard that fails to publish on drop aborts the - track. `timeline::Config::wall` is the broadcast `Clock`. -- **moq-json config** (#3718). `compression: bool` is a `Compression` enum; - track-owning options are `producer::Config` / `consumer::Config`. -- **moq-mux imports are typed.** `import::Init` splits into `AudioInit`, - `VideoInit`, and `ContainerInit`, with typed formats instead of strings, and - `Track::new` is `Track::audio` / `Track::video`. -- **moq-relay embedding** (#3638). `Relay` fields are private: clone the - handles you need, mount routes, then call `Relay::run`. - `Cluster::with_cache` moved to `cluster::Options`. - -## JavaScript - -The JavaScript packages have no changelog; this list follows the breaking -PRs, so a minor rename may be missing. - -- **@moq/token is @moq/auth.** `sign` / `verify` are `Key.sign` / `Key.verify`, - and claims are pattern unions (see [Token grants are patterns](#relay-and-cli)). -- **One `Connection`** (#3614, #3636). `Connection.Reload` is - `new Moq.Connection({ url })`, which pools one connection per relay. `closed` - settles only on `close()`; the error that stopped retrying is `error`. -- **Origins hold broadcasts** (#2705, #3225). `connection.publish(path, - broadcast)` is `origin.createBroadcast(path)` then `broadcast.announce()`, and - `connection.announced(prefix)` is `origin.announced(scope)` yielding - `{ prefix, kind, route }`. `ignoreSelf` is gone: reflected announces - are always dropped. -- **Names mirror Rust** (#3710). `latencyMax` is `maxAge` (a `Time.Milli`), - `writeDatagram` is `insertDatagram`, and `RemoteError` is `Error.Stream` / - `Error.Session`. -- **@moq/watch** (#3396, #3817). `latency`, `latency-min`, `latency-max`, and - `jitter` are `delay` and `buffer`, which need a unit (`delay="100ms"`, - `buffer="30s"`). `reload` is `announced`. `Watch.Broadcast({ connection })` - is `Watch.Player({ origin: connection.origin, ... })`. -- **@moq/publish** takes `origin: connection.origin` instead of a - `connection` signal. -- **@moq/json and @moq/binary** (now `@moq/flate`) take one options object (#3640): - `new Json.Snapshot.Consumer({ track })` instead of `(track, config)`. -- **@moq/hang** reads the catalog `archive` entry instead of `timeline`. - -## Bindings - -Python (`moq-rs` 0.5.0), Swift (0.5.0), Kotlin (`dev.moq:moq` 0.5.0), and Go -wrap moq-ffi 0.4. These are the moq-ffi names; each wrapper follows them in its -own casing: - -- **Go module path** is `moq.dev/moq` (was `github.com/moq-dev/moq-go/moq`). -- **Publish split by kind.** `publish_media*` is `publish_audio`, - `publish_video`, or `publish_container`, each taking its own init. The raw - encoder paths that used to be `publish_audio` / `publish_video` are - `encode_audio` / `encode_video`. -- **Durations are microseconds** (`max_age_us`, `MoqBackoff.initial_us`), and - send estimates are `estimated_send_rate_bps`. Read receive estimates through - the owned generic bandwidth consumer returned by `recv_bandwidth()`; its - `peek()` reads the estimate and `changed()` waits for a new one. Stats no - longer carry a receive estimate or request PROBE. -- **Setters are fallible** (#3642). Client and server configuration setters - return an error (`Busy` during connect or listen) instead of dropping the - value. `set_tls_disable_verify(bool)` is `set_tls_verify(bool)`. -- **Announcements.** `MoqAnnounced` is `MoqAnnounceConsumer` and - `MoqAnnouncement` is `MoqAnnounceUpdate`; `MoqBroadcastRequest::abort` is - `reject`, and `MoqOriginOptions` is `MoqOriginConfig`. -- **`MoqAudioCodec`** is an `opus()` object (#3671). -- **Errors.** `MoqError::Protocol` carries a `MoqProtocolError` (scope, wire - code, kind) instead of a flattened message. -- **Track and group `finish()`** keeps the handle open so a later `abort()` can - still run. - -C (libmoq 0.6): - -- The 41 `moq_client_*` setters are one zero-initializable `moq_client_config`, - whose durations are `_us`. -- `moq_publish_media` is `moq_publish_audio`, `moq_publish_video`, or - `moq_publish_container`; the raw encoders are `moq_encode_audio*` / - `moq_encode_video*`. Formats are enums instead of strings. -- `moq_announced` is `moq_announce_update`, `moq_origin_consume_announced` is - `moq_origin_announced_broadcast`, and `moq_broadcast_request_abort` is - `moq_broadcast_request_reject`. +- **Token grants are patterns.** Existing `put`/`get` tokens and key scopes + keep working as subtrees, and subtree-only grants are still signed in that + form, so a `moq-token` deployment can upgrade issuers and verifiers in either + order. Verifiers on the pattern-only `moq-auth` 0.1.0/0.1.1 or `@moq/auth` + 0.1.x/0.2.0 refuse that form, so upgrade them before their issuers. Grants + only a pattern can express need an upgraded verifier. +- **Gossip discovery is removed.** Until every relay that ran + `--cluster-mesh` is upgraded, keep client grants off `.internal/`: an older + relay still dials any URL announced there with `cluster.token`, and an + upgraded peer still forwards it. +- **Stats counters were renamed.** Relays still write the legacy names beside + the new ones, so move consumers to the new names now; [Stats](/concept/stats#traffic) + lists both. diff --git a/go/wrapper/MIGRATE.md b/go/wrapper/MIGRATE.md new file mode 100644 index 00000000000..8689478554b --- /dev/null +++ b/go/wrapper/MIGRATE.md @@ -0,0 +1,174 @@ +# Migrating the Go moq.dev/moq module + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Subscriptions carry group ranges (#5732) + +`Subscription.GroupStart` and `GroupEnd` are replaced by `Ranges`, a slice of `Range` whose ends are `AnchorAbsolute` or `AnchorRelative` (an absolute group and frame, or a count of groups before the latest), and an optional `Order`. +To start at group `n`, pass one range starting at the absolute anchor for group `n`, frame 0. + +### Receive estimates come from `Session.RecvBandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session.RecvBandwidth` returns to request PROBE, read it with `Peek()` or wait with `Changed(ctx)`, and `Close()` it to stop. +It returns `nil` when the version has no PROBE. + +### `AnnouncedBroadcast.Available` waits for the next publisher instance (#5700) + +The first call returns the broadcast serving the path, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + +### A timed track refuses a group that rewinds (#5803) + +Writing a frame fails with `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + +### Bandwidth reserves a track's demand (#5845) + +`Bandwidth.Reserve` takes the track's `Demand()` instead of the producer, and no longer returns an error. + +### Media subscribe and fetch take a catalog rendition (#5834) + +`media.TrackConfig` and `media.GroupConfig` replace `Name` and `Container` with a `Rendition`, built by `media.VideoRendition` or `media.AudioRendition`, and follow its broadcast and track themselves. + +```go +config := media.TrackConfig{Rendition: media.VideoRendition(id, video)} +``` + +### Native codecs have `audio` and `video` packages (#5694) + +`BroadcastProducer.EncodeAudio` / `EncodeVideo` are `audio.NewProducer` / `video.NewProducer` (`moq.dev/moq/audio`, `moq.dev/moq/video`), built from a `media.CatalogProducer`. +The audio track name moves into `EncoderOutput.Track`, and durations and timestamps are `time.Duration` (`FrameDuration`, `audio.Frame.Timestamp`, `video.Frame.Timestamp`); a zero `FrameDuration` takes the codec's own frame. +`BroadcastConsumer.DecodeAudio` / `DecodeVideo` are `audio.Subscribe` / `video.Subscribe`, taking the broadcast after the context. +Producers expose subscriber state only through `Demand()`, and a video GOP is `video.Keyframe(interval)`. +`audio.NewEncoder` / `video.NewEncoder` encode without publishing. + +```go +catalog, err := moqmedia.NewCatalogProducer(broadcast) +video, err := moqvideo.NewProducer(catalog, moqvideo.EncoderInput{...}, moqvideo.EncoderOutput{...}, nil) +err = video.Write(moqvideo.Frame{Timestamp: time.Duration(pts) * time.Microsecond, Data: rgba}) +frames, err := moqvideo.Subscribe(ctx, consumer, name, rendition, output) +``` + +### Background panics are reported (#5562) + +A panic in a background task returns an `ErrTask` error carrying the panic message, and other objects keep running. + +### Announcements are events with a restart + +`AnnounceConsumer.Next` and `All` yield an `AnnounceEvent`: `AnnounceEventStart`, `AnnounceEventUpdate`, `AnnounceEventEnd`, or `AnnounceEventRestart`, each carrying an `Announce`. +`AnnounceUpdate` and its `Active()` are gone. +A restart means another publisher instance now serves the prefix: request the path again. + +```go +for event, err := range announced.All(ctx) { + switch e := event.(type) { + case moq.AnnounceEventStart: + subscribe(e.Announce.Prefix) + case moq.AnnounceEventRestart: + subscribe(e.Announce.Prefix) + } +} +``` + +### "Epoch" means only the publisher epoch (#5146) + +`Session.Epoch()` is `Connects()`, and `AudioProducer.ResetEpoch()` is `Reanchor()`. + +### Origins are `consume`, and servers accept with them + +`WithSubscribeOrigin` is `WithConsumeOrigin`. +A server request's `SetPublish` / `SetConsume` are arguments to `Accept(ctx, publish, consume)`, where `nil` inherits the server's origin. +`Server.Requests(ctx)` is `All(ctx)`, and the `Status*` constants are `ConnectionStatus*`. +A value the native side cannot use fails `Dial` with a config error, including a bad bind address. + +### Media lives in `moq.dev/moq/media` + +`BroadcastProducer.PublishAudio`, `PublishVideo`, `PublishContainer`, their `OnTrack` and `Stream` variants, `SetVideoProperties`, and `SetCatalogSection`, and the consumer's catalog and media subscriptions, are gone. +Use `media.NewAudioTrackProducer` / `NewVideoTrackProducer`, `NewContainerProducer`, `NewCatalogProducer`, `NewCatalogConsumer`, `NewTrackConsumer`, and `media.FetchGroup`; `media.NewGroupConsumer` decodes a raw group. +Producers drop `Name`, `Used`, and `Unused`; read them through `Demand()`. +JSON and opaque data tracks live in `moq.dev/moq/json` and `moq.dev/moq/flate`, wrapping a track from `PublishTrack`. + +```go +audio, err := media.NewAudioTrackProducer(broadcast, media.Named{}, media.AudioInit{Format: media.AudioFormatOpus, Data: opusHead}) +``` + +### Container byte-stream importers take an init record + +`media.NewContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. +Its optional `Timebase`, also on `ContainerInit`, shares one source's offset across container imports. + +### Durations are `time.Duration` + +`Frame`, `Datagram`, `Subscription`, `TrackInfo`, and `ConnectionStats` carry `time.Duration` instead of microsecond integers: `TimestampUs` is `Timestamp`, and `RttUs` is `RTT`. +A frame timestamp is a `*time.Duration`, `nil` on a frame read from an untimed track. + +### Timestamps are optional + +A raw write to a timed track without a timestamp fails with `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. +`media.CatalogProducer.Now()` samples the broadcast clock for local-event stamps. +Set `Timestamp` on every write to a timed track. + +### Subscriber staleness is `MaxDelay` + +`Subscription.MaxAgeUs` is `MaxDelay`, a `time.Duration`, and `audio.DecoderOutput` / `video.DecoderOutput` take `MaxDelay *time.Duration`. +`TrackInfo.MaxAge` keeps its name: it is the publisher's retention. + +### Error payloads are named `Reason` + +Errors that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `Reason`: read `Reason` instead of `Field0`. + +### `Transport` gains `WebTransport` + +A WebTransport session reports `TransportWebTransport`; `TransportQUIC` now means raw QUIC only. +Check for `TransportWebTransport` too where you matched `TransportQUIC`. + +### `Stalled` is `Enabled` (#4915) + +`media.Video.Stalled` is `Enabled`, defaulting to true with the meaning inverted, and `media.Audio` gains `Enabled`. +Read `Enabled` where you read `!Stalled`. + +### Routes have one cost (#4661) + +`Route.Cold` is removed; set only `Cost`. + +### Decoded video frames are objects + +`VideoDecodedFrame` is a handle with `TimestampUs()`, `Width()`, `Height()`, `Pixels(format)`, `Surface()`, and `Close()`. +`VideoDecoderOutput.Format` is gone; set `Surface: true` to keep the decoder's native surface. +Call `frame.Pixels(moq.VideoPixelFormatRgba)` instead of reading `frame.Data`, and `Close()` the frame when done. + +### `BroadcastProducer.Finish()` is removed (#4152) + +Call `Close()` instead. + +## 0.5 -> 0.7 + +### The module path is `moq.dev/moq` + +Import `moq.dev/moq` instead of `github.com/moq-dev/moq-go/moq`. + +### Publish split by kind + +`PublishMedia*` is `PublishAudio`, `PublishVideo`, or `PublishContainer`, each taking its own init. +The raw encoder paths that used to be `PublishAudio` / `PublishVideo` are `EncodeAudio` / `EncodeVideo`. + +### Renames + +| Before | After | +| --- | --- | +| millisecond durations | microseconds (`MaxAgeUs`, `Backoff.InitialUs`) | +| `EstimatedSendBitrate` / `EstimatedRecvBitrate` | `EstimatedSendRate` / `EstimatedRecvRate` | +| `WithTLSDisableVerify(true)` | `WithTLSVerify(false)` | +| `Announced` / `Announcement` | `AnnounceConsumer` / `AnnounceUpdate` | +| `BroadcastRequest.Abort` | `Reject` | +| `OriginOptions` | `OriginConfig` | + +### Errors are structured + +Protocol errors carry a `ProtocolError` (scope, wire code, kind) instead of a flattened message. +Setters return an error (`Busy` during connect or listen) instead of dropping the value. +Track and group `Finish()` keep the handle open, so a later `Abort()` can still run. +Match with `moq.ProtocolError(err)` instead of `errors.Is(err, moq.ErrProtocol)`, and read its `Kind` and `Code`. diff --git a/js/auth/MIGRATE.md b/js/auth/MIGRATE.md new file mode 100644 index 00000000000..d1512bd3e80 --- /dev/null +++ b/js/auth/MIGRATE.md @@ -0,0 +1,30 @@ +# Migrating @moq/auth + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Roles carry `Access` (#5697) + +Parsed `Claims`, `Grant`, and `authorize`'s permissions hold `publish` and `subscribe` as `Access`: path patterns plus an optional per-connection `rate` cap. +Read the patterns from `.path`, as in `permissions.publish.path.matches(path)`. `Key.sign` still takes pattern strings, but a hand-built `Claims` value must come from `ClaimsSchema.parse`. +A gateway must enforce a cap or refuse the grant before admitting media. +Released verifiers reject a capped grant, so upgrade every verifier before an issuer sets `rate`. + +### A token must carry its key's `kid` (#3943) + +`Key.verify` refuses a token whose `kid` header does not match the key's, and a key set accepts a token without a `kid` only when it holds one key that has none. +Sign with the key that will verify it, so the `kid` matches. +The strict transport schema also accepts `rtmp`, `srt`, `webrtc`, and `webtransport`, so an exhaustive `switch` over it needs those arms. +A WebTransport session reports `webtransport` instead of `quic` (#5293), so a policy keyed on `quic` must accept both. + +## @moq/token 0.3 -> @moq/auth 0.2 + +### @moq/token is @moq/auth + +`sign` and `verify` are `Key.sign` and `Key.verify`, and claims are pattern unions: `alice` covers only `alice`, so sign `alice/**` for the subtree. + +```ts +import { Key } from "@moq/auth"; +const token = await Key.sign(key, { publish: "alice/**" }); +``` diff --git a/js/clock/MIGRATE.md b/js/clock/MIGRATE.md new file mode 100644 index 00000000000..4fb228b08f6 --- /dev/null +++ b/js/clock/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/clock + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/flate/MIGRATE.md b/js/flate/MIGRATE.md new file mode 100644 index 00000000000..3c9ea9653a1 --- /dev/null +++ b/js/flate/MIGRATE.md @@ -0,0 +1,22 @@ +# Migrating @moq/flate + +How to update your code for each breaking change, newest first. + +## Unreleased + +### @moq/binary is @moq/flate + +The opaque `Snapshot` and `Stream` tracks moved beside the codec; the wire and the catalog's `binary` section are unchanged. +Import `Snapshot` and `Stream` from `@moq/flate` instead of `@moq/binary`. + +### Consumers return `Timed` values (#5099) + +The snapshot and stream consumers' `next()` and async iterator yield `{ value, at }`, where `at` is absent on an untimed track. +A snapshot consumer's `next()` yields every state in order; call `latest()` to skip to the newest. +Where you awaited `next()` for the newest bytes, read `(await consumer.latest())?.value`. + +### Producers never invent a timestamp (#5070) + +`Snapshot.Producer.update` and `Stream.Producer.append` take a `Timed`. +Pass `{ value: bytes, at: Time.Timestamp.now() }` instead of the bytes. +Omit `at` only on an untimed track. diff --git a/js/hang/MIGRATE.md b/js/hang/MIGRATE.md new file mode 100644 index 00000000000..eba07b0f593 --- /dev/null +++ b/js/hang/MIGRATE.md @@ -0,0 +1,55 @@ +# Migrating @moq/hang + +How to update your code for each breaking change, newest first. + +## Unreleased + +### The catalog root reserves `version` (#5806) + +`version` is the oldest moq-hang draft that reads the catalog, and parsing throws `UnsupportedVersion` for one newer than this reader. +Rename an extension field that was called `version`. + +### Archive timelines declare a per-track duration (#5715) + +Each `archive.timelines` value is `{ track, duration }` instead of the timeline track's name, and `archive.durationMax` is removed. +`duration` is the nominal record duration in the archive timescale, absent for an untimed track. +Read the track name from the value's `track`, and use the per-track `duration` instead of `durationMax`. +A catalog on the other side of this change parses with no `archive`; see the [upgrade guide](https://github.com/moq-dev/moq/blob/main/doc/setup/upgrade.md). + +### Timeline records are optionally timed (#5720) + +`Timeline.Record.pts` and `.duration` are optional, absent on an untimed track. +Handle a record whose `pts` is `undefined`. + +### Catalog ranking names its media kind + +`Catalog.ranked(renditions)` is `Catalog.Video.ranked(renditions)`. +`Catalog.Audio.ranked(renditions)` orders audio by enabled state, bitrate, sample rate, channel count, and name. +Both put disabled renditions last. + +### One `enabled` flag replaces `stalled` (#4915) + +`Catalog.Stalled` is gone, `VideoConfig.stalled` is `enabled` with the meaning inverted, and `AudioConfig` gains `enabled`. +Replace a `config.stalled` check with `config.enabled === false`. + +### CMAF decodes at the frame timestamp (#4826) + +A fragment's earliest sample presents at its moq-net frame timestamp; `tfdt` only orders the samples. +`Container.Format.decode` and `Cmaf.decodeDataSegment` take that timestamp, or `undefined` for an untimed frame, whose samples present at `tfdt`. +Pass `frame.timestamp` after the payload. +`Cmaf.decodeTimestamp` is gone. + +### Container consumers take `maxDelay` + +`Container.Consumer`'s `maxAge` prop is `maxDelay`, and passing `maxAge` throws a `TypeError`. + +### `cut` is `discontinuity` (#4141) + +`Container.Legacy.Producer.cut(end?)` is `discontinuity(end?)`. + +## 0.4 -> 0.5 + +### The catalog's `timeline` entry is `archive` + +@moq/hang reads the root catalog's `archive` entry instead of `timeline`, and wall time from the root `clock`. +Read `catalog.archive` instead of `catalog.timeline`, and `catalog.clock?.wall` instead of `catalog.timeline?.wall`. diff --git a/js/json/MIGRATE.md b/js/json/MIGRATE.md new file mode 100644 index 00000000000..d569462e496 --- /dev/null +++ b/js/json/MIGRATE.md @@ -0,0 +1,25 @@ +# Migrating @moq/json + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Consumers return `Timed` values (#5099) + +The snapshot and stream consumers' `next()` and async iterator yield `{ value, at }`, where `at` is the frame's timestamp and absent on an untimed track. +A snapshot consumer's `next()` yields every state in order; call `latest()` to skip to the newest, as `next()` used to. +Where you awaited `next()` for the newest value, read `(await consumer.latest())?.value`. +A caller that passes the result on as `any` or `unknown` still compiles, so search for every `next()` and `for await` rather than relying on type errors. + +### Producers never invent a timestamp (#5070) + +`Snapshot.Producer.update`, `Stream.Producer.append`, and `Window.Producer.push` take a `Timed`, and `Window.Producer.pop` and `Snapshot.Producer.mutate` take an optional `at`. +Pass `{ value, at: Time.Timestamp.now() }` instead of the value. +Omit `at` only on an untimed track. + +## 0.3 -> 0.4 + +### One options object (#3640) + +Constructors take one options object. +Spread the config beside the track: `new Json.Snapshot.Consumer({ track, ...config })`. diff --git a/js/loc/MIGRATE.md b/js/loc/MIGRATE.md new file mode 100644 index 00000000000..9c775bae813 --- /dev/null +++ b/js/loc/MIGRATE.md @@ -0,0 +1,10 @@ +# Migrating @moq/loc + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Timestamps are `@moq/clock` units (#5706) + +Frame timestamps are typed as `Micro` from `@moq/clock` instead of `Time.Micro` from `@moq/net`. +Import the `Micro` type from `@moq/clock`. diff --git a/js/moq-boy/MIGRATE.md b/js/moq-boy/MIGRATE.md new file mode 100644 index 00000000000..9548201a3ad --- /dev/null +++ b/js/moq-boy/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/boy + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/msf/MIGRATE.md b/js/msf/MIGRATE.md new file mode 100644 index 00000000000..8d2cbe631b4 --- /dev/null +++ b/js/msf/MIGRATE.md @@ -0,0 +1,10 @@ +# Migrating @moq/msf + +How to update your code for each breaking change, newest first. + +## Unreleased + +### `Track.stalled` is removed (#4915) + +The non-standard `stalled` track field is gone and no longer read or written. +Drop it from any track you build. diff --git a/js/net/MIGRATE.md b/js/net/MIGRATE.md new file mode 100644 index 00000000000..bad4da80e35 --- /dev/null +++ b/js/net/MIGRATE.md @@ -0,0 +1,132 @@ +# Migrating @moq/net + +How to update your code for each breaking change, newest first. + +## Unreleased + +### A timed track refuses a group that rewinds (#5803) + +A local write whose first frame starts before a lower-sequence group's, or after a higher one's, throws `TimestampMismatch`. +A publisher whose timeline restarts publishes a new broadcast instead. + +### `Auth.Auth` is `Auth.Handle`, and `Auth.Token` is `Auth.Auth` (#5695) + +Rename `Auth.Auth` to `Auth.Handle` first, then `Auth.Token` to `Auth.Auth`. + +### Superseded groups age by wall clock too (#5548) + +A superseded group is stale once its successor arrived a full `maxDelay` ago, as well as by media time. +Untimed tracks are no longer exempt: with the default zero `maxDelay`, an untimed subscriber starts at the latest group and skips each superseded one. +Give it a budget to replay or tolerate reordering. + +```ts +const sub = track.subscribe({ maxDelay: Milli(1000) }); +``` + +### Time units and timers come from `@moq/clock` (#5706) + +`Time.Nano`, `Time.Micro`, `Time.Milli`, and `Time.Second` are gone. +Import them from `@moq/clock`, which throws on negative or non-finite values. + +### Announced requests follow restarts, and `broadcasts()` is gone + +An unpinned `request(path, { announced: true })` stays open across publisher instances: its `active` moves to each new instance's broadcast and clears between them, instead of ending with `Unroutable`. +Read `active` reactively and close the request when done, or pass `epoch` to keep the old single-instance behavior. +`Origin.Producer` and `Origin.Consumer` drop `broadcasts(scope)`; iterate `announced(scope)` instead. + +```ts +for await (const event of origin.announced(scope)) { + if (event.kind === "start" || event.kind === "restart") { ... } +} +``` + +### Discovery follows what something watches, and `discovery` is gone + +A `consume` origin stays empty until a reader watches it with `announced(scope)` or `request(path, { announced: true })`; the session then asks the relay about those prefixes only. +Code that called `request(path)` expecting a route without watching should hold an `announced(scope)` stream. +The `discovery` option on `connect`, `accept`, and `Connection`, and `Established.discovery`, are removed; passing the option throws a `TypeError`. +Drop the option. + +### Announce streams yield `start`, `update`, `end`, and `restart` events + +`Announce.Consumer` yields an `Announce.Event`: an `Announce.Announce` (`prefix`, `captures`, `route`) plus a `kind`. +`Announce.Update`, `Announce.Kind`, and `Announce.isActive` are gone. +Replace `Announce.isActive` with a check on `event.kind`: skip `"end"`, and treat `"start"` and `"restart"` as active. +A `"restart"` means another publisher instance now serves the prefix: request the path again. + +### Routes carry the publisher epoch (#4942) + +`Path.joinEpoch` and `Path.splitEpoch` are gone. +Set the epoch on the route with `announce({ epoch })` and read `Route.epoch`; pin a request to one instance with `RequestOptions.epoch`. +Nothing mints an epoch by default. + +### Publishing never invents a timestamp (#5070) + +`Group.Producer` and `Track.Producer` drop `writeString`, `writeJson`, and `writeBool`, and `@moq/net/zod` drops `write`. +Encode the payload yourself and write it as a frame with an explicit timestamp, or none on an untimed track. + +```ts +track.writeFrame({ payload: new TextEncoder().encode(JSON.stringify(value)), timestamp }); +``` + +### A track is timed or untimed (#4968) + +`Track.Info.timescale` is optional; omitted or `null` means untimed, and `infoDefaults()` no longer fills in milliseconds. +`Group.Frame.timestamp` and `Track.Datagram.timestamp` are optional, and `appendDatagram` / `insertDatagram` take `Timestamp | undefined`. +A write whose timedness does not match its track throws `Error.TimestampMismatch`, so declare a timescale, such as `{ timescale: Timescale.MILLI }`, on every track you stamp. + +### Subscriber staleness is `maxDelay` + +`Track.Subscription`'s `maxAge` is `maxDelay`; `maxAge` stays the publisher's retention on `Track.Info`. +A `maxAge` key in subscription options throws a `TypeError`, even when `undefined` or beside `maxDelay`. + +### Publisher retention is optional (#4242) + +`Track.Info.maxAge` is optional, absent meaning no publisher limit, and `DEFAULT_MAX_AGE_MS` is gone. +Pass an explicit value such as `Milli(30_000)` where you used the constant. + +### Demand is read through `demand()` + +`Track.Producer` and `Group.Producer` move `used` and `unused()` to `demand()`, so `producer.unused()` is `producer.demand().unused()`. +`Allocator.reserve` takes `producer.demand()`, replacing the `Bandwidth.Demand` interface. + +### A replacement track owns its start sequence (#5068) + +`BroadcastProducer.insertTrack` is removed; create the track with `broadcast.createTrack(name, info)` instead. +`Track.Producer.appendGroup` and `appendDatagram` throw `duplicate sequence` instead of continuing past an earlier producer of the same name; `createTrack` now answers a queued request instead of throwing `duplicate track`. +`Request.accept` closes the request's producer with the error whenever it throws. + +### Route cost is a `bigint` (#4661) + +`Route.cost` is one `bigint` instead of separate warm and cold numbers. +Pass one value, such as `cost: 10n`. + +### Datagrams outside the group range are skipped (#4982) + +A subscriber no longer receives datagrams outside its group range, and a FETCH never returns one. + +### Broadcast `close` takes no abort reason (#4152) + +`broadcast.Producer.close(abort)` and `broadcast.Consumer.close(abort)` lose the parameter, and `closed` is a `GetPromise`. +Call `close()` with no argument. + +## 0.3 -> 0.4 + +### One `Connection` (#3614, #3636) + +`Connection.Reload` is `new Connection({ url })`, which pools one connection per relay. +`closed` settles only on `close()`; the error that stopped retrying is `error`. + +### Origins hold broadcasts (#2705, #3225) + +`connection.publish(path, broadcast)` is `origin.createBroadcast(path)` then `broadcast.announce()`. +`connection.announced(prefix)` is `origin.announced(scope)`, yielding `{ prefix, kind, route }`. +`ignoreSelf` is gone: reflected announces are always dropped. + +### Names mirror Rust (#3710) + +| Before | After | +| --- | --- | +| `latencyMax` | `maxAge` (a `Time.Milli`) | +| `writeDatagram` | `insertDatagram` | +| `RemoteError` | `Error.Stream` / `Error.Session` | diff --git a/js/p2p/MIGRATE.md b/js/p2p/MIGRATE.md new file mode 100644 index 00000000000..1dd9e38670a --- /dev/null +++ b/js/p2p/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/p2p + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/pattern/MIGRATE.md b/js/pattern/MIGRATE.md new file mode 100644 index 00000000000..cd044d9ce24 --- /dev/null +++ b/js/pattern/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/pattern + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/publish/MIGRATE.md b/js/publish/MIGRATE.md new file mode 100644 index 00000000000..0e66059ea83 --- /dev/null +++ b/js/publish/MIGRATE.md @@ -0,0 +1,26 @@ +# Migrating @moq/publish + +How to update your code for each breaking change, newest first. + +## Unreleased + +### A disabled rendition stays in the catalog (#4915) + +`enabled: false` keeps the rendition in the catalog with `enabled: false` instead of removing it, and viewers skip it. +Code that read a missing rendition as disabled should check `catalog.video?.renditions[name]?.enabled === false`. + +### Tracks persist for the broadcast's life (#5068) + +`Rendition.close()` no longer closes the network track. +A `maxAge` change, or replacing a closed rendition track, recreates `broadcast.net`, and registering a name under a different kind on the same network broadcast throws. + +### `OpusConfig.usedtx` is removed + +Opus DTX is always off, since Chromium's DTX output shifts the audio timeline. +Remove the field; a plain-JS caller still passing it is ignored. + +## 0.4 -> 0.5 + +### Publish takes an origin + +Pass `origin: connection.origin` to `Publish.Broadcast` instead of a `connection` signal. diff --git a/js/room/MIGRATE.md b/js/room/MIGRATE.md new file mode 100644 index 00000000000..f5d865cd37c --- /dev/null +++ b/js/room/MIGRATE.md @@ -0,0 +1,15 @@ +# Migrating @moq/room + +How to update your code for each breaking change, newest first. + +## Unreleased + +### `HISTORY` is a `Nano` (#5706) + +`HISTORY` is a `Nano` from `@moq/clock` instead of milliseconds. +Use `Milli.fromNano(HISTORY)` where you need milliseconds. + +### Chat tracks are timed (#4968) + +`Chat.info()` includes a `timescale`, so a chat track you create yourself must declare one before writing timestamps. +Create it with `broadcast.createTrack(Chat.TRACK, Chat.info())`. diff --git a/js/signals/MIGRATE.md b/js/signals/MIGRATE.md new file mode 100644 index 00000000000..f25f2892d6d --- /dev/null +++ b/js/signals/MIGRATE.md @@ -0,0 +1,22 @@ +# Migrating @moq/signals + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Timers take a `Nano` (#5706) + +`effect.timer`, `effect.timeout`, and `effect.interval` take a `Nano` from `@moq/clock` instead of milliseconds, and run on the installed clock. +Convert a millisecond value with `Nano.fromMilli(Milli(250))`, importing both from `@moq/clock`. + +### Effect cleanups run last-in, first-out (#4889) + +An effect tears down its cleanups newest first, like `DisposableStack`. +A resource registered later, such as a nested `run` built on an earlier one, is now released before what it depends on. +Check any cleanup that relied on running after one registered later. + +### `Signal.race` returns a `GetPromise` (#4226) + +`Signal.race` releases its losing listeners and returns a `GetPromise` instead of a native `Promise`. +`await` and `.then()` still work; `.catch()` and `.finally()` do not, so wrap the `await` in `try`/`catch` instead. +Waiting begins when it is awaited or subscribed. diff --git a/js/video/MIGRATE.md b/js/video/MIGRATE.md new file mode 100644 index 00000000000..df967b406e7 --- /dev/null +++ b/js/video/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/video + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/watch/MIGRATE.md b/js/watch/MIGRATE.md new file mode 100644 index 00000000000..f95f080faf5 --- /dev/null +++ b/js/watch/MIGRATE.md @@ -0,0 +1,40 @@ +# Migrating @moq/watch + +How to update your code for each breaking change, newest first. + +## Unreleased + +### `"auto"` delay is measured (#4162) + +`"auto"` is sized from how late frames arrive instead of derived from RTT. +A numeric `delay` is taken literally instead of having the rendition's own delay added on top. +`Sync.out.jitter` always equals `Sync.out.delay`, and `"auto"` with no decoder registered resolves to 0 instead of 100 ms. + +### Staleness is `maxDelay` + +Rename `Sync.out.maxAge` to `Sync.out.maxDelay`. + +### Disabled renditions are never selected (#4915) + +A rendition the catalog marks `enabled: false` is skipped, even when named. +With no video rendition selectable, the picture clears to black. + +### `announced` off stays on one publisher + +With `announced` off, the player no longer moves to a later publisher at the same path. +Turn `announced` on to follow restarts. + +## 0.5 -> 0.6 + +### Delay attributes need a unit (#3396, #3817) + +`latency`, `latency-min`, `latency-max`, and `jitter` are `delay` and `buffer`, each with a unit. +`reload` is `announced`. + +```html + +``` + +### `Watch.Broadcast` is `Watch.Player` + +Rename `Watch.Broadcast` to `Watch.Player` and pass `origin: connection.origin` instead of `connection`. diff --git a/kt/moq/MIGRATE.md b/kt/moq/MIGRATE.md new file mode 100644 index 00000000000..2552722b9e0 --- /dev/null +++ b/kt/moq/MIGRATE.md @@ -0,0 +1,185 @@ +# Migrating the Kotlin dev.moq:moq package + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Subscriptions carry group ranges (#5732) + +`Subscription.groupStart` and `groupEnd` are replaced by `ranges`, a list of `Range` whose ends are `AnchorAbsolute` or `AnchorRelative` (an absolute group and frame, or a count of groups before the latest), and an optional `order`. +To start at group `n`, pass one range starting at the absolute anchor for group `n`, frame 0. + +### Receive estimates come from `Session.recvBandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session.recvBandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and `cancel()` it to stop. +Replace `stats().estimatedRecvRateBps` with `recvBandwidth()?.peek()`. + +### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) + +The first call returns the broadcast serving the path, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + +### A timed track refuses a group that rewinds (#5803) + +Writing a frame fails with `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + +### Bandwidth reserves a track's demand (#5845) + +`Bandwidth.reserve` takes the track's `demand()` instead of the producer, and no longer throws. + +### Media subscribe and fetch take a catalog rendition (#5834) + +`TrackConfig` and `GroupConfig` in `dev.moq.media` replace `name` and `container` with a `rendition` (`VideoRendition` or `AudioRendition`) and follow its broadcast and track themselves. + +```kotlin +val config = TrackConfig(rendition = VideoRendition(id, video)) +``` + +### Native codecs have `audio` and `video` packages (#5694) + +`BroadcastProducer.encodeAudio` / `encodeVideo` are the `dev.moq.audio.Producer` / `dev.moq.video.Producer` constructors, which take a `CatalogProducer`. +The audio track name moves into the encoder output's `track`, and a zero `frameDurationUs` takes the codec's own frame. +`BroadcastConsumer.decodeAudio` / `decodeVideo` are `dev.moq.audio.Consumer.subscribe(broadcast, ...)` / `dev.moq.video.Consumer.subscribe(broadcast, ...)`. +Producers expose subscriber state only through `demand()`, and a video `gop` is `MoqVideoGop.Keyframe(interval)`. +`dev.moq.audio.Encoder` / `dev.moq.video.Encoder` encode without publishing. + +```kotlin +val video = dev.moq.video.Producer(CatalogProducer(broadcast), input, output) +val frames = dev.moq.video.Consumer.subscribe(consumer, name, rendition, decoder) +``` + +### Background panics are reported (#5562) + +A panic in a background task throws `uniffi.moq.MoqException.Task` with the panic message as `reason`, and other objects keep running. + +### Announcements are events with a restart + +`announced(config).updates()` emits an `AnnounceEvent`: `AnnounceEventStart`, `AnnounceEventUpdate`, `AnnounceEventEnd`, or `AnnounceEventRestart`, each carrying an `Announce` as `announce`. +`Moq.announcements()` and `AnnounceUpdate` are gone. +A restart means another publisher instance now serves the prefix: request the path again. + +```kotlin +moq.announced().updates().collect { event -> + when (event) { + is AnnounceEventStart -> subscribe(event.announce.prefix) + is AnnounceEventRestart -> subscribe(event.announce.prefix) + else -> {} + } +} +``` + +### "Epoch" means only the publisher epoch (#5146) + +`Moq.epoch()` is `connects()`, and `AudioProducer.resetEpoch()` is `reanchor()`. + +### `connect` takes a `ClientConfig` + +The keyword arguments moved into a `ClientConfig` with nested `tls`, `quic`, `websocket`, and `backoff` records. +The `subscribe` origin is `consume`, and `reconnect = false` is `once = true`. +A value the native side cannot use throws `MoqException.Config`, including a bad bind address. +A server request takes its origins in `accept(publish, consume)`, where `null` inherits the server's origin. + +```kotlin +val moq = Moq.connect(url, ClientConfig(tls = ClientTls(insecure = true), consume = origin)) +``` + +### Data tracks wrap a track + +`BroadcastProducer.publishJsonSnapshot` / `publishJsonStream`, the binary variants, and the matching consumers are gone. +Create the track, then wrap it with `dev.moq.json` (or `dev.moq.flate` for opaque bytes). + +```kotlin +val state = dev.moq.json.SnapshotProducer(broadcast, broadcast.publishTrack("state", null), config) +``` + +### Media lives in `dev.moq.media` + +`BroadcastProducer.publishAudio`, `publishVideo`, `publishContainer`, their `OnTrack` and `Stream` variants, `setVideoProperties`, and `setCatalogSection`, and the consumer's catalog and media subscriptions, are gone. +Use `TrackProducer.audio` / `video`, `ContainerProducer`, `CatalogProducer` (with `setSection`), `CatalogConsumer.subscribe`, `TrackConsumer.subscribe`, and `GroupConsumer.fetch`. +Producers drop `name`, `used`, and `unused`; read them through `demand()`. + +```kotlin +val audio = dev.moq.media.TrackProducer.audio(broadcast, Named(null), AudioInit(format = AudioFormat.OPUS, data = opusHead)) +``` + +### Container byte-stream importers take an init record + +`ContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. +Its optional `timebase`, also on `ContainerInit`, shares one source's offset across container imports. + +### Timestamps are optional + +`Frame.timestamp` and `Datagram.timestamp` are `Duration?`, null on a frame read from an untimed track. +A raw write to a timed track without a timestamp throws `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. +`CatalogProducer.now()` samples the broadcast clock for local-event stamps. +Pass `timestampUs` when writing to a timed track, and handle a null `timestamp` when reading. + +### Subscriber staleness is `maxDelay` + +`Subscription.maxAge`, `AudioDecoderOutput.maxAge`, and `VideoDecoderOutput.maxAge` are `maxDelay`, and the `maxAgeUs` argument is `maxDelayUs`. +`TrackInfo.maxAge` keeps its name: it is the publisher's retention. + +### Error payloads are named `reason` + +Exceptions that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. +Read `e.reason` instead of `e.v1`. + +### `Transport` gains `WEB_TRANSPORT` + +A WebTransport session reports `Transport.WEB_TRANSPORT`; `QUIC` now means raw QUIC only, so an exhaustive `when` needs one more branch. +Check for `WEB_TRANSPORT` wherever you checked for `QUIC`. + +### `stalled` is `enabled` (#4915) + +`Video.stalled` is `enabled`, defaulting to true with the meaning inverted, and `Audio` gains `enabled`. +Replace `!video.stalled` with `video.enabled`. + +### Routes have one cost (#4661) + +`Route.cold` is removed; drop the `cold` argument. + +### Decoded video frames are objects + +`VideoDecodedFrame` is a closeable handle with `width()`, `height()`, `pixels(format)`, and `surface()`. +`VideoDecoderOutput.format` is gone; set `surface = true` to keep the decoder's native surface. +Replace `frame.data` with `frame.use { it.pixels(VideoPixelFormat.RGBA) }`. + +### `BroadcastProducer.finish()` is removed (#4152) + +Call `end()` to finish the broadcast and keep the handle, or `close()` to release it. + +## 0.4 -> 0.5 + +### Durations are `kotlin.time.Duration` + +Extension properties expose every microsecond field as a `Duration`: `Frame.timestamp`, `Subscription.maxAge`, `ConnectionStats.rtt`, `Backoff.initial`, and the rest. +Replace the millisecond fields, such as `backoff.initialMs`, with these properties. + +### `connect` grows options + +`Moq.connect` gains `reconnect`, `backoff`, `maxStreams`, and the WebSocket fallback settings as named arguments. +Pass the origin by name: `Moq.connect(url, publish = origin)`. + +### Publish split by kind + +`publishMedia*` is `publishAudio`, `publishVideo`, or `publishContainer`, each taking its own init. +The raw encoder paths that used to be `publishAudio` / `publishVideo` are `encodeAudio` / `encodeVideo`. +For example, `publishAudio(AudioInit(format = AudioFormat.OPUS, data = opusHead))`. + +### Renames + +| Before | After | +| --- | --- | +| `estimatedSendBitrate` / `estimatedRecvBitrate` | `estimatedSendRate` / `estimatedRecvRate` | +| `Announced` / `Announcement` | `AnnounceConsumer` / `AnnounceUpdate` | +| `BroadcastRequest.abort` | `reject` | +| `OriginOptions` | `OriginConfig` | + +### Errors are structured + +`MoqException.Protocol` carries a `ProtocolError` (scope, wire code, kind) instead of a flattened message. +Setters throw (`Busy` during connect or listen) instead of dropping the value. +Track and group `finish()` keep the handle open, so a later `abort()` can still run. +Read `e.details.kind` and `e.details.code` instead of `e.message`. diff --git a/py/moq-rs/MIGRATE.md b/py/moq-rs/MIGRATE.md new file mode 100644 index 00000000000..980942e7d1e --- /dev/null +++ b/py/moq-rs/MIGRATE.md @@ -0,0 +1,171 @@ +# Migrating the Python moq-rs package + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Subscriptions carry group ranges (#5732) + +`Subscription.group_start` and `group_end` are replaced by `ranges`, a list of `Range` whose ends are an `Anchor` (an absolute group and frame, or a count of groups before the latest), and an optional `order`. +To start at group `n`, pass one range starting at the absolute anchor for group `n`, frame 0. + +### Receive estimates come from `Session.recv_bandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session.recv_bandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and drop it to stop. + +```python +bandwidth = session.recv_bandwidth() +rate = bandwidth.peek() if bandwidth else None +``` + +### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) + +The first call returns the broadcast serving the path, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + +### A timed track refuses a group that rewinds (#5803) + +Writing a frame fails with `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + +### Bandwidth reserves a track's demand (#5845) + +`Bandwidth.reserve` takes the track's `demand()` instead of the producer. + +### Media subscribe and fetch take a catalog rendition (#5834) + +`media.TrackConsumer.subscribe` and `media.GroupConsumer.fetch` take the catalog ID and its `Video` or `Audio` config instead of a track name and container, and follow the rendition's broadcast and track; `fetch` takes them before `sequence`. +The decode consumers' `name` argument is `id`. + +### Native codecs have `audio` and `video` modules (#5694) + +`BroadcastProducer.encode_audio` / `encode_video` are `moq.audio.Producer` / `moq.video.Producer`, built from a `moq.media.CatalogProducer`. +The audio track name moves into `EncoderOutput(track=...)`, and durations and timestamps are `timedelta` (`frame_duration`, `Frame.timestamp`) instead of `_us` integers; a zero `frame_duration` takes the codec's own frame. +`BroadcastConsumer.decode_audio` / `decode_video` are `await moq.audio.Consumer.subscribe(broadcast, ...)` / `await moq.video.Consumer.subscribe(broadcast, ...)`. +Producers expose subscriber state only through `demand()`, and a video `gop` is `moq.video.Gop.KEYFRAME(interval=...)`. +`moq.audio.Encoder` / `moq.video.Encoder` encode without publishing. + +```python +audio = moq.audio.Producer( + moq.media.CatalogProducer(broadcast), + moq.audio.EncoderInput(format=moq.audio.SampleFormat.F32, sample_rate=48000, channels=1), + moq.audio.EncoderOutput(codec=moq.audio.Codec.opus(), track="audio", frame_duration=timedelta(milliseconds=20)), +) +audio.write(moq.audio.Frame(timestamp=timedelta(0), data=pcm)) +``` + +### Background panics are reported (#5562) + +A panic in a background task raises `moq.Error.Task` with the panic message as `reason`, and other objects keep running. + +### Announcements are events with a restart + +`AnnounceConsumer` yields an `AnnounceEvent`: `AnnounceEventStart`, `AnnounceEventUpdate`, `AnnounceEventEnd`, or `AnnounceEventRestart`, each carrying an `Announce` as `announce`. +`AnnounceUpdate` is gone. +A restart means another publisher instance now serves the prefix: request the path again. +Where you checked `update.active`, act on `AnnounceEventStart` and `AnnounceEventRestart` and read `event.announce.prefix`. + +### "Epoch" means only the publisher epoch (#5146) + +`Session.epoch()` is `connects()`, and `AudioProducer.reset_epoch()` is `reanchor()`. + +### The consume origin is `consume` + +`Client(..., subscribe=origin)` is `Client(..., consume=origin)`. +A server request takes its origins in `accept(publish=..., consume=...)`, where `None` inherits the server's origin. +A value the native side cannot use raises `moq.Error.Config` at construction, including a bad bind address. + +### JSON data tracks wrap a track + +`BroadcastProducer.publish_json_snapshot` / `publish_json_stream` and the matching consumers are gone. +Create the track, then wrap it with `moq.json` (or `moq.flate` for opaque bytes). + +```python +track = broadcast.publish_track("state") +state = moq.json.SnapshotProducer(broadcast, track) +``` + +### Media lives in `moq.media` + +`BroadcastProducer.publish_audio`, `publish_video`, `publish_container`, their `_on_track` and `_stream` variants, `set_video_properties`, and `set_catalog_section`, and the consumer's catalog and media subscriptions, are gone. +Use `moq.media.TrackProducer.audio` / `video`, `ContainerProducer`, `CatalogProducer`, `CatalogConsumer`, `TrackConsumer`, and `GroupConsumer`. +Producers drop `name`, `used`, and `unused`; read them through `demand()`. + +```python +audio = moq.media.TrackProducer.audio(broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head)) +``` + +### Container byte-stream importers take an init record + +Wrap the format passed to `moq.media.ContainerStreamProducer` in a `moq.media.ContainerStreamInit`. +Its optional `timebase`, also on `ContainerInit`, shares one source's offset across container imports. + +### Durations are `timedelta` + +`Frame`, `Datagram`, `Subscription`, `TrackInfo`, and `ConnectionStats` carry `timedelta` instead of microsecond integers: `timestamp_us` is `timestamp`, and `rtt_us` is `rtt`. +A string passed where a list of strings belongs (`tls_roots="ca.pem"`) raises `TypeError` instead of splitting into characters. + +### Timestamps are optional + +A frame read from an untimed track has `timestamp` set to `None`. +Raw writes no longer default to zero, so a write to a timed track without a timestamp raises `TimestampMismatch`; whole-frame media writes require one. +Pass `timestamp=` on every write to a timed track. +`moq.media.CatalogProducer.now()` samples the broadcast clock for local-event stamps. + +### Subscriber staleness is `max_delay` + +`Subscription.max_age_us` is `max_delay`, a `timedelta`, and the decoder outputs take `max_delay`, also a `timedelta`. +`TrackInfo` keeps its `max_age`: it is the publisher's retention. + +### Error payloads are named `reason` + +Errors that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`: read `err.reason` instead of `err[0]`. + +### `Transport` gains `WEB_TRANSPORT` + +A WebTransport session reports `moq.Transport.WEB_TRANSPORT`; `QUIC` now means raw QUIC only, so a server that admits only `QUIC` refuses every browser. +Check for both where you meant any QUIC session. + +### `stalled` is `enabled` (#4915) + +`moq.media.Video.stalled` is `enabled`, defaulting to `True` with the meaning inverted, and `moq.media.Audio` gains `enabled`. + +### Routes have one cost (#4661) + +`Route.cold` is removed: drop the argument and keep `cost`. + +### Decoded video frames are objects + +A `VideoDecodedFrame`'s fields are methods: `timestamp_us()`, `width()`, `height()`, and `pixels(format)` replacing `data` and `format`. +`VideoDecoderOutput.format` is gone; set `surface=True` to keep the decoder's native surface. +Read `frame.pixels(moq.VideoPixelFormat.RGBA)` instead of `frame.data`. + +### `BroadcastProducer.finish()` is removed (#4152) + +Call `broadcast.close()` instead. + +## 0.4 -> 0.5 + +### Publish split by kind + +`publish_media*` is `publish_audio`, `publish_video`, or `publish_container`, each taking its own init. +For example, `publish_media("opus", opus_head)` is `publish_audio(moq.AudioFormat.OPUS, opus_head)`. +The raw encoder paths that used to be `publish_audio` / `publish_video` are `encode_audio` / `encode_video`. + +### Renames + +| Before | After | +| --- | --- | +| millisecond durations | microseconds (`max_age_us`, `Backoff.initial_us`) | +| `estimated_send_bitrate` / `estimated_recv_bitrate` | `estimated_send_rate` / `estimated_recv_rate` | +| `tls_disable_verify=True` | `tls_verify=False` | +| `Announced` / `Announcement` | `AnnounceConsumer` / `AnnounceUpdate` | +| `BroadcastRequest.abort` | `reject` | +| `OriginOptions` | `OriginConfig` | + +### Errors are structured + +`Error.Protocol` carries a `ProtocolError` (scope, wire code, kind) instead of a flattened message: read `e.details.kind` instead of parsing `str(e)`. +Setters raise (`Busy` during connect or listen) instead of dropping the value. +Track and group `finish()` keep the handle open, so a later `abort()` can still run. diff --git a/quest/m1/README.md b/quest/m1/README.md index e09e6d7b703..1c6877da24b 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -51,7 +51,6 @@ lands before the cut. - [Release builds](/quest/m0/release-link.md) - package artifacts can be built - [qmux credit](/quest/m0/qmux-credit.md) - dropped/stopped streams return credit and close is delivered -- [Per-package MIGRATE.md](/quest/m1/migrate.md) - every published package documents how to migrate each breaking change, and breaking PRs add their entry - [moq-net follows announced instances](/quest/m1/net-announced-follow.md) - the Rust equivalent of JS `request(path, { announced: true })`, replacing the follow loops in moq-ffi and moq-c - [Sink restart follow](/quest/m1/sink-restart-follow.md) - moq-cli's server and push sinks follow an epochless `Restart` where their output can mark it, and fail loud otherwise - [OBS restart follow](/quest/m1/obs-restart-follow.md) - the OBS source keeps playing across a GOAWAY drain or a publisher replacement diff --git a/quest/m1/c/README.md b/quest/m1/c/README.md index 2e16a2efd97..debcba5abae 100644 --- a/quest/m1/c/README.md +++ b/quest/m1/c/README.md @@ -34,8 +34,8 @@ Decided: `moq-c` (`rs/moq-c`), and the generated package takes over that name and `moq::c` target, so C users migrate once. Its first release is 0.8.0, a minor bump over the hand-written 0.7.x. -- Docs change inline: the consumer quest rewrites `doc/lib/c`, and retirement - adds an upgrade note. No separate guide. +- Docs change inline: the consumer quest rewrites `doc/lib/c`, and the + package's `MIGRATE.md` carries the move. No separate guide. The line owns the pre-cut check: every `doc/lib/c` sample and the C interop client build and run against the built release archive, not only in-tree. @@ -48,7 +48,7 @@ and fetch quests were abandoned for this line, and hidden is done. Decided in the quest audit: [claim epochs](/quest/m1/claim-answer-epochs.md) is a narrow correctness exception. Its existing claim API migrates with Rust and JS so C cannot retain the unsafe accept path while this line waits. -That change owns its C tests and upgrade notes; other API reshaping stays here. +That change owns its C tests and `MIGRATE.md` entries; other API reshaping stays here. ## Required diff --git a/quest/m1/c/package.md b/quest/m1/c/package.md index db3016200a3..310b6ed4159 100644 --- a/quest/m1/c/package.md +++ b/quest/m1/c/package.md @@ -21,6 +21,9 @@ under the same names. - A release workflow and nightly/CI jobs follow the C++ package's, on the same generator pin; `v0.11.0-kixelated.4+v0.32.2` already carries the C backend (`--lang c`). +- Ship a `MIGRATE.md` with the package, since it is what C users install. + Carry over `rs/moq-c/MIGRATE.md`'s history and add an `## Unreleased` + entry mapping the hand-written ABI to the generated one. - Ship a hand-written `moq_shutdown` that calls `moq_ffi_shutdown`, then `moq_shutdown_dispatcher`. - moq-c and moq-cpp each embed the moq-ffi staticlib, so one program can't link diff --git a/quest/m1/ffi-shape/README.md b/quest/m1/ffi-shape/README.md index 72a2cdb4f88..b79fef5151c 100644 --- a/quest/m1/ffi-shape/README.md +++ b/quest/m1/ffi-shape/README.md @@ -82,7 +82,7 @@ Settled shape: Each child reshapes one group end to end: moq-ffi, all five wrappers, the C++ package and `cpp/obs`, the `doc/lib` samples per the cross-package table, -and its entries in the Unreleased section of `doc/setup/upgrade.md`. Each +and its entries under `## Unreleased` in each affected package's `MIGRATE.md`. Each runs `just test interop --all`. ## Required diff --git a/quest/m1/ffi-shape/stream-producer-name.md b/quest/m1/ffi-shape/stream-producer-name.md index 1c149ec02e4..c3cba9c5c29 100644 --- a/quest/m1/ffi-shape/stream-producer-name.md +++ b/quest/m1/ffi-shape/stream-producer-name.md @@ -17,5 +17,5 @@ had owned this review. Ask with 2-3 candidate names and a recommendation. A rename touches moq-ffi, every wrapper, `cpp/moq`'s alias list, the `doc/lib` samples, and the -Unreleased section of `doc/setup/upgrade.md`; keeping the name deletes this +`## Unreleased` of each affected `MIGRATE.md`; keeping the name deletes this quest. diff --git a/quest/m1/json-window-timestamps.md b/quest/m1/json-window-timestamps.md index b3de92da6a9..e878e3687b4 100644 --- a/quest/m1/json-window-timestamps.md +++ b/quest/m1/json-window-timestamps.md @@ -20,7 +20,7 @@ stream consumers `Timed`. The window consumer (`window::Consumer::next` in - Decided: Rust and JS together, same names and shape. Update moq-ffi and the bindings if they expose the window consumer (see the Cross-Package Sync table). - Update in-repo readers (`js/hang` timeline, `js/room` chat) and add a line to - the Unreleased section of `doc/setup/upgrade.md`. No new doc page. + `## Unreleased` of each affected package's `MIGRATE.md`. No new doc page. - Land before the next `@moq/json` release, so its break ships together with #5099's. Not blocked on the Rust snapshot and stream quest: both Rust breaks use the same `moq_net::Timed` and ship in the next moq-json release together. diff --git a/quest/m1/migrate.md b/quest/m1/migrate.md deleted file mode 100644 index 26fe90d3b6a..00000000000 --- a/quest/m1/migrate.md +++ /dev/null @@ -1,62 +0,0 @@ -# [M] Per-package MIGRATE.md - -## Goal - -Every hand-written published package carries a `MIGRATE.md` that tells a -downstream user how to update their code, config, or invocation for each -breaking change, and AGENTS.md and CONTRIBUTING.md tell every breaking PR to -add its entry there. Entries for the breaks already on `main` since the last -release are in place before the breaking cut. - -Non-goals: wire-format and rollout guidance for operators (stays in -`doc/setup/upgrade.md`), and CI enforcement on PRs into `main` (the -[API-diff check](/quest/m2/release/migrate-audit.md) on PRs into `release` -catches misses). - -## Plan - -Decided 2026-10-16 (maintainer interview): - -- Audience is downstream users upgrading across a breaking release. - `CHANGELOG.md` stays release-plz generated and untouched. -- Every hand-written published package gets a `MIGRATE.md` now, even if - empty: the published Rust crates, the `@moq/*` packages with a release - script, `rs/moq-ffi`, and the binding wrappers (`py/moq-rs`, `kt/moq`, - `dart/moq`, `go/wrapper`, `swift`, `cpp/moq`, `cpp/obs`). Generated FFI - layers (`py/moq-ffi`, `kt/moq-ffi`, `dart/moq_ffi`, `go/ffi`, `swift/ffi`) - get none: they have no hand-written API. -- Layout: `## Unreleased` on top, newest first; the release cut renames it to - the version transition (e.g. `## 0.3 -> 0.4`) and history stays. One - before/after snippet per break. -- Each binding wrapper's file shows the change in its own language, matching - the Cross-Package Sync table that already makes a `moq-ffi` change touch - every wrapper. -- Binaries (`moq-relay`, `moq-cli`, `moq-gst`, `obs`, ...) use the same file; - entries map an old flag, env var, or config key to its replacement with an - example invocation. -- `MIGRATE.md` is the single source. Move every API entry out of - `doc/setup/upgrade.md` into its package's file; the page keeps the - cross-cutting wire and relay rollout guidance plus an index linking every - `MIGRATE.md`. `doc/lib` pages link to their package's file instead of - repeating it. -- No CI check on `main`. AGENTS.md (Public API section) and CONTRIBUTING.md - (PRs, Impact) instruct a breaking PR to add its entry; the API-diff check - on PRs into `release` is the backstop. -- Backfill from the 87 `!:` commits since the `release` merge base and the - existing upgrade.md entries, not an API diff; the release-PR check covers - unmarked breaks. As of 2026-10-16 the heaviest are moq-net/@moq/net, moq-mux, - moq-ffi and every wrapper, moq-tokio, moq-relay/moq-cli, - moq-video/moq-audio, and @moq/hang/@moq/publish/@moq/watch. -- Ranked first in m1 so breaking work landing after it carries its entry. - -Repoint every active quest that sends breaking entries to upgrade.md, so -they land in package files instead: `quest/m1/ffi-shape/README.md`, -`quest/m1/ffi-shape/stream-producer-name.md`, -`quest/m1/json-window-timestamps.md`, `quest/m2/release/c-retire.md`, and the -release epic's note that the upgrade guide describes public API -(`quest/m2/release/README.md`). Grep `quest/` for `upgrade.md` again when -landing, since other quests may add references meanwhile. - -## Related - -- [API-diff check](/quest/m2/release/migrate-audit.md) - verifies these files against the API diff on the cut PR diff --git a/quest/m1/refresh/core.md b/quest/m1/refresh/core.md index f41a6da4c07..6fda8b7127f 100644 --- a/quest/m1/refresh/core.md +++ b/quest/m1/refresh/core.md @@ -64,8 +64,8 @@ Decided 2026-10-16 (see the [epic](/quest/m1/refresh/README.md)). on append, `refresh: None`, and a replaced producer. A benchmark sweeps tracks against subscribers per track, so the join check stays on the touched track. -- Update the Unreleased section of `doc/setup/upgrade.md` for the `Demand` - break, and `doc/concept/standard.md` for the IETF mapping. +- Add a `MIGRATE.md` entry in each affected package for the `Demand` + break, and update `doc/concept/standard.md` for the IETF mapping. Public API: breaking (`Demand` in Rust, JS, moq-ffi, and every binding; `track::Info::refresh` and `MoqTrackInfo::refresh` added). Wire: none in this quest. IETF fields are diff --git a/quest/m1/refresh/json.md b/quest/m1/refresh/json.md index b43b5ec0e62..836325a2bba 100644 --- a/quest/m1/refresh/json.md +++ b/quest/m1/refresh/json.md @@ -71,7 +71,7 @@ revised in the #5734 review. `doc/lib/*`. The ratio reaches them through `MoqTrackInfo::refresh` from [core](/quest/m1/refresh/core.md), and the moq-ffi driver above refreshes their producers with no wrapper change. Update the moq-json and `@moq/json` - READMEs and the Unreleased section of `doc/setup/upgrade.md`. + READMEs and add an entry to each affected package's `MIGRATE.md`. - moq-flate is unchanged. Its snapshot mode already opens a group per update, and its stream mode keeps one group by design. diff --git a/quest/m2/release/README.md b/quest/m2/release/README.md index 3979f7dac25..3a28fa6ef62 100644 --- a/quest/m2/release/README.md +++ b/quest/m2/release/README.md @@ -17,8 +17,9 @@ bumps and tags happen at this cut, not in the milestone-planning PR. Run the release's compatibility, package, draft, and all-language interop checks on the candidate. Publish finalized lite-07 with first-party clients preferring it and falling back to supported older versions. Verify artifacts -from their registries before downstream pin migrations. Existing upgrade and -identity guides describe public API, wire, archive-format, and legacy limits. +from their registries before downstream pin migrations. Package `MIGRATE.md` +files describe public API breaks; the upgrade and identity guides describe +wire, archive-format, and legacy limits. Downstream staging, mixed-version upgrade/rollback, canaries, and broad rollout follow publication and require their own operator authorization. Do not mark diff --git a/quest/m2/release/c-retire.md b/quest/m2/release/c-retire.md index 3a8333924f7..1c4f12b2e9b 100644 --- a/quest/m2/release/c-retire.md +++ b/quest/m2/release/c-retire.md @@ -4,7 +4,7 @@ The hand-written C crate is gone: its final `moq-c` 0.7.x release points users at the generated `moq-c` 0.8.0, and the source, workflows, and docs that only served -it are deleted. `doc/setup/upgrade.md` tells C users how to move. +it are deleted. The generated `moq-c` package's `MIGRATE.md` tells C users how to move. ## Plan diff --git a/quest/m2/release/migrate-audit.md b/quest/m2/release/migrate-audit.md index 5caee2ad030..b2eef6250d8 100644 --- a/quest/m2/release/migrate-audit.md +++ b/quest/m2/release/migrate-audit.md @@ -38,7 +38,12 @@ Decided 2026-10-16 (maintainer interview): cut's audit also reads the diff of those paths by hand. Per-language wrapper checks are out of scope until a missed break shows they are needed. - For this cut, fill every gap the report and the manual pass show in the - matching `MIGRATE.md`. + matching `MIGRATE.md`. Known gaps: breaking releases cut after the + 2026-09-23 train have no transition section yet (moq-json 0.4 -> 0.5, + moq-flate 0.1 -> 0.2, and possibly moq-audio and moq-video), because their + breaks predate the backfill's merge base. +- Compile-check the code in every `MIGRATE.md` section the cut ships against + the candidate. Nothing else builds it. - Required by the cut, after m1's breaking work has landed. ## Required diff --git a/rs/hang/MIGRATE.md b/rs/hang/MIGRATE.md new file mode 100644 index 00000000000..6e2dd754974 --- /dev/null +++ b/rs/hang/MIGRATE.md @@ -0,0 +1,37 @@ +# Migrating hang + +How to update your code for each breaking change, newest first. + +## Unreleased + +### The catalog root carries a reader `version` (#5806) + +`version` is reserved at the catalog root: decoding refuses a catalog newer than this crate reads with `Error::UnsupportedVersion`, and serializing fails if the extension `E` has its own `version` member. +Rename that member, such as to `app_version`. + +### Archive timelines declare a per-track duration (#5715) + +Each `Archive::timelines` value is a `Timeline` (the timeline `track` plus its nominal `duration` in the archive timescale, `None` when untimed) instead of the timeline track's name, and `Archive::duration_max` is removed. +A catalog on the other side of this change decodes with no archive; see the [upgrade guide](https://github.com/moq-dev/moq/blob/main/doc/setup/upgrade.md). +Insert `Timeline::new(track, duration)` and read the track name from `.track`. + +### Timeline records are optionally timed (#5720) + +`timeline::Record::pts` and `duration` are `Option`, absent on an untimed track. +Build an untimed record with `Record::untimed(sequence, start, end)`. +Handle `None` where you read `record.pts` or `record.duration`. + +### One `enabled` flag replaces `stalled` (#4915) + +`VideoConfig::stalled: Option` is `enabled: bool`, with the meaning inverted, and `AudioConfig` gains `enabled`. +The `catalog::stalled` module is gone, and `Video::ranked()` puts disabled renditions last. +`stalled` is no longer written and is ignored on read. +Set `enabled = false` where you set `stalled = Some(true)`. + +## 0.20 -> 0.21 + +### The catalog's `timeline` entry is `archive` + +The root catalog's `timeline` is `archive` (`catalog::Archive`), and wall time moved to the root `clock: { wall, timescale }`. +An old publisher's catalog has no `archive`, so upgrade publishers before readers that need it. +Read wall time from `catalog.clock` instead of `catalog.timeline`. diff --git a/rs/kio/MIGRATE.md b/rs/kio/MIGRATE.md new file mode 100644 index 00000000000..2810c099b36 --- /dev/null +++ b/rs/kio/MIGRATE.md @@ -0,0 +1,13 @@ +# Migrating kio + +How to update your code for each breaking change, newest first. + +## Unreleased + +### `Pollable` merged into `Task`, and unused surface removed (#4156) + +`Fan`, `Hold`, `WeakLock`, `Pollable`, and the public `Lock::downgrade` are gone. +`Task` gained an `Output` type and has a blanket impl for `FnMut(&Waiter) -> Poll`. +`poll_used` and `poll_unused` return `Poll>` instead of `Poll>`. +`Waiter::context()` replaces `Context::from_waker(waiter.waker())`. +Match `Poll::Ready(Err(kio::Closed))` where you matched `Poll::Ready(None)`. diff --git a/rs/moq-archive/MIGRATE.md b/rs/moq-archive/MIGRATE.md new file mode 100644 index 00000000000..93e45670400 --- /dev/null +++ b/rs/moq-archive/MIGRATE.md @@ -0,0 +1,14 @@ +# Migrating moq-archive + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Untimed tracks record untimed (#5720) + +`Info::new` and `Info::timescale` take and return `Option`, and `Error::TimescaleMismatch` carries `Option` fields. +`segment::Frame::timestamp` is `Option`. +`Object::decode(buf, timed)` and `Store::get_segments(track, segment, timed)` take whether the track is timed. +`Error::Untimed` now means a DVR cannot record an untimed track; mismatched timing is `Error::Timing`. +Untimed recordings use the version 2 format, which an older version 2 reader refuses. +Pass `Some(timescale)` to `Info::new`, and `info.timescale.is_some()` as `timed`. diff --git a/rs/moq-audio/MIGRATE.md b/rs/moq-audio/MIGRATE.md new file mode 100644 index 00000000000..6efb57e67c9 --- /dev/null +++ b/rs/moq-audio/MIGRATE.md @@ -0,0 +1,25 @@ +# Migrating moq-audio + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Decoded reads carry encoder ends (#5687) + +`decode::Consumer::read()` returns `decode::Read` events instead of bare frames: `Frame { discontinuity, frame }` and `End { discontinuity, end }`; `None` still means the track is finished. +An `End` can be followed by final PCM with the same `discontinuity` before a fresh encoder bumps it, and a bump with no `End` before it is a loss. +Binding audio frame APIs keep their existing shape. +Play `decode::Read::Frame { frame, .. }` as before, and trim after `decode::Read::End { end, .. }`. + +### Capture handles are `Control` and `Capture` + +`encode::Publication` is `encode::Control`, and `encode::PublicationOptions` is `encode::Capture`, without its `clock` field: the driver stamps on `catalog.clock()`. +Build it from `encode::Capture::default()`, setting `capture` and `encode`. + +### `reset_epoch` is `reanchor` (#5146) + +"Epoch" means only the publisher epoch, so the audio re-anchor is renamed. + +### Decoder staleness is `max_delay` + +`decode::Options::max_age` is `max_delay`, and `decode::Consumer::max_age()` is `max_delay()`. diff --git a/rs/moq-auth/MIGRATE.md b/rs/moq-auth/MIGRATE.md new file mode 100644 index 00000000000..5d7dc6c7940 --- /dev/null +++ b/rs/moq-auth/MIGRATE.md @@ -0,0 +1,44 @@ +# Migrating moq-auth + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Roles carry `Access` (#5697) + +`publish` and `subscribe` on `Claims`, `Permissions`, and `Grant` are `Access`, which holds the path patterns and an optional per-connection `Rate` cap. +Read the patterns from `.path`; constructors and `Claims::with_publish` / `with_subscribe` take anything `Into`, such as a `Pattern`, `Patterns`, or array, so collect a pattern iterator first. +A capped role serializes as `{"path":"alice/**","rate":8000000}`, and key scopes refuse caps. +Released verifiers reject that form, so upgrade every verifier before issuing capped tokens. +A gateway that embeds this crate must enforce a cap or refuse the grant before admitting media. + +### Gateway transports and key sets are non-exhaustive (#3943) + +`Transport` gains `Rtmp`, `Srt`, `WebRtc`, and `WebTransport`, and `serve::Keys` gains `Set`; both are `#[non_exhaustive]`, so add a wildcard arm. +A WebTransport session reports `WebTransport` instead of `Quic`, so a policy keyed on `Quic` must accept both. +A JWT verified with one key must carry that key's `kid`; a token without one is accepted only when the key has none. + +```rust +match transport { + Transport::Quic => {} + _ => {} +} +``` + +## 0.1 -> 0.2 + +### `serve` matches 0.14 relay auth (#4319) + +`serve::Policy::revalidate` and `expires` are `Option`, defaulting to `None`, so a grant is never re-checked or closed unless you set them. +`serve::Server::new` returns a `Result`: it refuses `revalidate` without `expires` (`Error::UnboundedRevalidate`) and session limits without `revalidate` (`Error::LimitsWithoutRevalidate`). +A session presenting both a JWT and a client certificate is refused with `Refusal::TokenAndCertificate`. +`Key::verify` enforces `nbf` (`Claims::not_before`, failing with `Error::TokenNotYetValid`) and ignores `iss`, `sub`, and `jti`; any other unknown claim still refuses the token. +To keep the old 60 s re-check bounded at 24 h, set `revalidate` and `expires` to those values, and handle the `Result` from `Server::new`. + +## moq-token 0.7 -> moq-auth 0.1 + +### moq-token is moq-auth (#3684) + +`Claims` holds `Patterns`, and `Claims::authorize` returns pattern residuals. +A `publish` or `subscribe` grant is a pattern: `alice` covers only `alice`, so sign `alice/**` for the subtree. +Replace `moq_token` with `moq_auth` in your imports. diff --git a/rs/moq-boy/MIGRATE.md b/rs/moq-boy/MIGRATE.md new file mode 100644 index 00000000000..0ebf5eefe81 --- /dev/null +++ b/rs/moq-boy/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-boy + +How to update your invocation and config for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-c/MIGRATE.md b/rs/moq-c/MIGRATE.md new file mode 100644 index 00000000000..3168e31494e --- /dev/null +++ b/rs/moq-c/MIGRATE.md @@ -0,0 +1,124 @@ +# Migrating moq-c + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Subscriptions carry group ranges (#5732) + +`moq_subscription`'s `group_start`, `group_end`, and their `_present` flags are replaced by `ranges` and `ranges_len`, an array of `moq_range` whose ends are `moq_anchor`s, and `order` (`moq_order`), which changes its ABI. + +### Receive estimates come from `moq_session_recv_bandwidth` (#5777) + +`moq_connection_stats` drops `estimated_recv_rate_bps` and `estimated_recv_rate_valid`, which changes its ABI. +Hold the consumer `moq_session_recv_bandwidth` returns to request PROBE, read it with `moq_bandwidth_consumer_peek` or wait with `moq_bandwidth_consumer_changed`, and release it with `moq_bandwidth_consumer_close`. + +```c +int32_t consumer = moq_session_recv_bandwidth(session); // 0 when the version has no PROBE +if (consumer > 0) { + uint64_t bps; + bool present; + if (moq_bandwidth_consumer_peek(consumer, &bps, &present) == 0 && present) use(bps); + moq_bandwidth_consumer_close(consumer); +} +``` + +### `moq_origin_announced_broadcast` delivers every publisher instance (#5700) + +Its callback gets a new broadcast handle each time another instance takes over the path or the broadcast returns, and its terminal `0` only on cancel. +Free each handle with `moq_consume_close` once its successor arrives. + +### A timed track refuses a group that rewinds (#5803) + +A frame write fails with a protocol error of kind `MOQ_PROTOCOL_KIND_TIMESTAMP_MISMATCH` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + +### libmoq is moq-c (#4288) + +The C bindings ship as the `moq-c` crate, starting at 0.7.0. +The header is still `moq.h` and the library is still `libmoq.a` / `moq.lib` (`-lmoq`); the C ABI did not change with the rename. +The CMake package, pkg-config name, release tags, and archives follow the new name. + +| Before | After | +| --- | --- | +| `find_package(moq)`, target `moq` | `find_package(moq-c)`, target `moq::c` | +| `pkg-config --libs moq` | `pkg-config --libs moq-c` | +| nix `.#libmoq` | nix `.#moq-c` | +| tags `libmoq-v*`, archives `libmoq--` | tags `moq-c-v*`, archives `moq-c--` | + +### Background panics are reported (#5562) + +A panic in a background task reports `MOQ_ERROR_PANIC`, and other handles keep running. + +### Announcements carry a kind (#4266, #4384, #5087) + +`moq_announce_update` is `moq_announce_event`, and its `active` flag is a `kind`: `MOQ_ANNOUNCE_KIND_START`, `_UPDATE`, `_END`, or `_RESTART`. +`_RESTART` means another publisher instance now serves the prefix: request the path again. +Subscribe on `MOQ_ANNOUNCE_KIND_START` or `_RESTART` where you checked `active`. + +### `moq_origin_announced` takes a config (#4111) + +The prefix and filter arguments moved into a `moq_announce_config`; pass `NULL` to receive everything. + +```c +moq_announce_config config = { .prefix = prefix, .prefix_len = prefix_len }; +moq_origin_announced(origin, &config, on_announce, user_data); +``` + +### Frames say whether they are timed + +`moq_frame` and `moq_datagram` gain `timestamp_present`, which changes their size, so recompile against the new `moq.h`. +A frame read from an untimed track has `timestamp_present` false. + +```c +if (frame.timestamp_present) use(frame.timestamp_us); +``` + +### Subscriber staleness is `max_delay_us` + +`moq_subscription`, `moq_audio_decoder_output`, `moq_video_decoder_output`, `moq_consume_video`, and `moq_consume_audio` take `max_delay_us` instead of `max_age_us`. + +### `stalled` is `enabled` (#4915) + +`moq_consume_video_stalled` is `moq_consume_video_enabled`, with the meaning inverted. + +### Routes have one cost (#4661) + +`moq_route.cold` and `has_cold` are removed; drop them. + +### `moq_publish_finish` is removed (#4152) + +Close the broadcast with `moq_publish_close`. + +### A decoded frame can fail on its own (#4094) + +`moq_decode_video_frame` can return a conversion error for a single frame; it used to end the stream at the terminal callback. + +## libmoq 0.5 -> 0.6 + +### One client config + +The 41 `moq_client_*` setters are one zero-initializable `moq_client_config`, whose durations are `_us`, passed to `moq_session_connect` instead of `moq_client_connect`. + +```c +moq_client_config config = { .tls_disable_verify = true }; +moq_session_connect(url, url_len, &config, origin, 0, on_status, user_data); +``` + +### Publish split by kind + +`moq_publish_media` is `moq_publish_audio`, `moq_publish_video`, or `moq_publish_container`, and the raw encoders are `moq_encode_audio*` / `moq_encode_video*`. +Formats are enums instead of strings. + +```c +moq_audio_init init = { .format = MOQ_AUDIO_FORMAT_OPUS, .init = opus_head, .init_len = opus_head_len }; +int track = moq_publish_audio(broadcast, &init); +``` + +### Announcement renames + +| Before | After | +| --- | --- | +| `moq_announced` | `moq_announce_update` | +| `moq_origin_consume_announced` | `moq_origin_announced_broadcast` | +| `moq_broadcast_request_abort` | `moq_broadcast_request_reject` | diff --git a/rs/moq-cli/MIGRATE.md b/rs/moq-cli/MIGRATE.md new file mode 100644 index 00000000000..06ae45737dd --- /dev/null +++ b/rs/moq-cli/MIGRATE.md @@ -0,0 +1,106 @@ +# Migrating moq-cli + +How to update your invocation and config for each breaking change, newest first. + +## Unreleased + +### `moq export ts --delay` defaults to 1 s and includes the send-ahead (#5553) + +The output trails a TS source by the delay instead of twice it. +With a multiplex rate, video whose HRD declares a longer buffer delay than `--delay` stops the export, naming the delay it needs. +Double an explicit `--delay` to keep the same trail, such as `500ms` to `1s`. + +### `--max-age` on fMP4, H.264, H.265, and RTMP exports is `--max-delay` + +`moq export fmp4`, `h264`, `h265`, and `rtmp` refuse `--max-age`. +`moq import --max-age` is unchanged: it is the publisher's retention. +Rename `--max-age` to `--max-delay` on those exports. + +### `moq export ts`, `flv`, and `mkv` take `--delay` (#4645) + +Each frame is written that long after its decode time; the older staleness spellings are refused with `--delay` named. +Rename `--max-age` to `--delay`. + +### `moq export ts --linger` follows only the same publisher instance + +A restarted publisher exits 1 unless `--stitch` follows it as a program switch. +`moq export srt` takes the same two flags. + +```bash +moq export ts --linger 10s --stitch ... +``` + +### `moq export archive` refuses untimed tracks + +Recording a track with no timestamps fails with `Untimed`, so an archive over moq-lite before 05 or moq-transport drafts 14 to 16 fails instead of recording arrival times. + +### `moq export fmp4` fixes its track set at the init segment + +A rendition added, changed, or left undescribed after the init segment ends the export with an error instead of writing a track missing from the moov. +Restart the export to pick up a new rendition. +Annex-B H.264 and H.265 sources get an `avc3` or `hev1` init from the catalog when it carries enough, and a keyframe whose parameter sets never appeared ends the export instead of waiting 30 seconds. + +### `moq play --delay` defaults to `auto` (#4162) + +`auto` is measured from how late frames arrive instead of derived from RTT. +Pass `--delay 100ms` to keep the old fixed delay. + +### `--hop` is removed; `--epoch` replaces it (#4969) + +A redundant pair shares an epoch, a UUIDv7 such as `uuidgen -7` prints, instead of a Hop ID. +`--hop` is an unknown flag and `MOQ_HOP` is silently ignored, so remove it: a pair still keyed on `MOQ_HOP` mints an epoch per process and each restart replaces the other's broadcast. +A node that pinned its cluster id with `--hop` passes `--cluster-id` instead. +Give both members the same `MOQ_EPOCH`, such as one `uuidgen -7` value. + +### Settings no listener reads stop startup (#5006) + +`moq` without a listener refuses `--listen-*` and `--auth-*` flags. +A stream-only listener (TCP or Unix, no `--listen`) refuses the QUIC-only flags, a client CA, and a `--listen-tls-*` certificate unless `--listen-tcp-tls` serves it. +`--listen-quic-lb-nonce` needs `--listen-quic-lb-id`. +Each used to be ignored: drop it, or add the listener it configures. + +### `moq --listen` with an invalid auth config stops at startup (#4912) + +It used to start and refuse every session. + +### The `noq` cargo feature is `quic` + +Rename it in `--features` when building from source. + +## 0.13 -> 0.14 + +### Gossip discovery is removed (#4601) + +`--cluster-mesh` and TOML `mesh` are refused: list every peer with `--cluster-connect`. + +## 0.12 -> 0.13 + +### `moq auth serve` matches 0.14 relay auth (#4319) + +`moq auth serve` never re-checks or expires a session by default; `--revalidate` needs `--expires`, and `--limit-*` needs `--revalidate`. +It refuses a session presenting both a JWT and a certificate. +`--key` takes a file, not an https or JWKS URL. +Pass `--revalidate 1m --expires 1d` to keep re-checking sessions. + +## 0.11 -> 0.12 + +### Commands and flags are renamed + +The listen and connect flags split by direction, as in [moq-relay](../moq-relay/MIGRATE.md#flags-split-by-direction). +A renamed flag or environment variable is refused with its replacement named. + +| Before | After | +| --- | --- | +| `moq publish`, `moq subscribe` | `moq import`, `moq export` | +| `moq token`, the `moq-token` binary | `moq auth` | +| `--origin` | `--hop`, now `--epoch` | +| `--name` | `--broadcast` | +| `--latency-max` | `--max-age` | +| `--server-bind` | `--listen` | +| `--client-connect` | `--connect` | +| `--tls-disable-verify` | `--connect-tls-insecure` | + +### `moq --listen` needs auth + +A listener refuses to start without `--auth-url` or `--auth-public` instead of accepting everyone. +Pass `--auth-public '**'` to keep accepting everyone. diff --git a/rs/moq-clock/MIGRATE.md b/rs/moq-clock/MIGRATE.md new file mode 100644 index 00000000000..c05e7ad07ad --- /dev/null +++ b/rs/moq-clock/MIGRATE.md @@ -0,0 +1,11 @@ +# Migrating moq-clock + +How to update your code for each breaking change, newest first. + +## Unreleased + +### moq-clock is a time library (#5465) + +moq-clock 0.10 was the "CLOCK over QUIC" example binary. +0.11 is a library: logical instants, wall time, deadlines, and a deterministic simulator. +The clock example lives on as `cargo run -p moq-tokio --example clock`, which takes `--connect` instead of `--url`. diff --git a/rs/moq-clock/macros/MIGRATE.md b/rs/moq-clock/macros/MIGRATE.md new file mode 100644 index 00000000000..f7e0a138a83 --- /dev/null +++ b/rs/moq-clock/macros/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-clock-macros + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-e2ee/MIGRATE.md b/rs/moq-e2ee/MIGRATE.md new file mode 100644 index 00000000000..1d05f563d98 --- /dev/null +++ b/rs/moq-e2ee/MIGRATE.md @@ -0,0 +1,17 @@ +# Migrating moq-e2ee + +How to update your code for each breaking change, newest first. + +## Unreleased + +### The epoch comes from the route (#4942) + +The protected path is the opaque broadcast path; the publisher epoch is read from the route instead of a path suffix. +On wires that do not carry an epoch, the application supplies it. +Drop `join_epoch` and `split_epoch`: announce with `Route::default().with_epoch(epoch)`, and read it back with `consumer.routed(&path).await.and_then(|route| route.epoch)`. + +### Timestamps are optional + +`Frame` and `Datagram` timestamps are `Option`, and the writers take `impl Into>`, so passing a `Timestamp` still compiles. +A frame read from an untimed track has no timestamp. +Handle `None` where you read `frame.timestamp`. diff --git a/rs/moq-ffi/MIGRATE.md b/rs/moq-ffi/MIGRATE.md new file mode 100644 index 00000000000..e75eb72a719 --- /dev/null +++ b/rs/moq-ffi/MIGRATE.md @@ -0,0 +1,195 @@ +# Migrating moq-ffi + +How to update your code for each breaking change, newest first. +These are the UniFFI names; each language wrapper has its own `MIGRATE.md` in its own casing. + +## Unreleased + +### Subscriptions carry group ranges (#5732) + +`MoqSubscription::group_start` and `group_end` are replaced by `ranges`, a list of `MoqRange { start, end }` whose ends are `MoqAnchor::Absolute { group, frame }` or `MoqAnchor::Relative { groups }`, and an optional `order`. +A start group `n` is `MoqRange { start: MoqAnchor::Absolute { group: n, frame: 0 }, end: None }`. + +### Receive estimates come from `MoqSession::recv_bandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `MoqSession::recv_bandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and drop it to stop. `MoqConnectionStats::estimated_recv_rate_bps` is gone. + +```rust +let bandwidth = session.recv_bandwidth(); +let rate = bandwidth.as_ref().and_then(|b| b.peek()); +``` + +### `available()` waits for the next publisher instance (#5700) + +`MoqAnnouncedBroadcast::available` returns the broadcast serving the path on its first call, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + +### A timed track refuses a group that rewinds (#5803) + +`write_frame` on a `MoqTrackProducer` or `MoqGroupProducer` fails with a `MoqError::Protocol` of kind `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + +### Bandwidth reserves a track's demand (#5845) + +`MoqBandwidth::reserve` takes a `MoqTrackDemand` instead of a `MoqTrackProducer`, so raw, media, and JSON tracks reserve the same way, and it no longer fails: pass `track.demand()?`. + +### Media subscribe and fetch take a catalog rendition (#5834) + +`MoqMediaTrackConfig` and `MoqMediaGroupConfig` replace `name` and `container` with a `rendition` (`MoqRendition::Video` or `Audio`: the catalog `id` and its config) and follow its `broadcast` and `track` themselves, so drop the `resolve` call and the `track`-or-ID fallback. +The audio and video decode consumers' `name` argument is `id`. + +```rust +let config = MoqMediaTrackConfig { rendition: MoqRendition::Video { id, config: video }, subscription: None }; +``` + +### Native codecs have audio and video namespaces (#5694) + +`MoqBroadcastProducer::encode_audio` / `encode_video` are the `MoqAudioProducer::new` / `MoqVideoProducer::new` constructors, which take the broadcast's `MoqMediaCatalogProducer`. +The audio track name moves into `MoqAudioEncoderOutput::track`, and a zero `frame_duration_us` (the new default) takes the codec's own frame. +`MoqBroadcastConsumer::decode_audio` / `decode_video` are `MoqAudioConsumer::subscribe` / `MoqVideoConsumer::subscribe`, taking the broadcast first. +The producers' `name()`, `used()`, and `unused()` are gone; read subscriber state through `demand()`. +`MoqVideoEncoderOutput::gop` is `Option`, so `Some(30)` is `Some(MoqVideoGop::Keyframe { interval: 30 })`. +Codec-only `MoqAudioEncoder` / `MoqVideoEncoder` encode without publishing. + +```rust +let catalog = MoqMediaCatalogProducer::new(broadcast)?; +let video = MoqVideoProducer::new(&catalog, input, output, None)?; +let frames = MoqVideoConsumer::subscribe(&consumer, name, rendition, decoder).await?; +``` + +### Background panics are reported (#5562) + +A panic in a binding-owned background task reports `MoqError::Task { reason }` with the panic message, and other objects keep running. +Handle the new case where you match on errors. + +### Announcements are flat events with a restart (#4266, #4384, #5087) + +`MoqAnnounceConsumer::next` returns `Option`: `Start`, `Update`, `End`, or `Restart`, each carrying a `MoqAnnounce` record (`prefix`, `captures`, `route`). +The `MoqAnnounceUpdate` object and its `active()` are gone. +`Restart` means another publisher instance now serves the prefix: request the path again. +Subscribe on `Start` or `Restart` where you checked `active()`. + +### "Epoch" means only the publisher epoch (#5146) + +`MoqSession::epoch()` is `connects()`, and `MoqAudioProducer::reset_epoch()` is `reanchor()`. + +### Clients and servers take a config record (#4519) + +`MoqClient::new()` and `MoqServer::new()` plus their setters are `MoqClient::new(MoqClientConfig)` and `MoqServer::new(MoqServerConfig)`, with nested `tls`, `quic`, `websocket`, and `backoff` records and a `versions` list. +A value the native side cannot use fails construction with `MoqError::Config`, including a bad bind address that used to fail later as `Bind`. +The `subscribe` origin is `consume`. +A server request's `set_publish` / `set_consume` are arguments to `accept(publish, consume)`, where null inherits the server's origin. + +```rust +let client = MoqClient::new(MoqClientConfig { + tls: MoqClientTls { insecure: true, ..Default::default() }, + consume: Some(origin), + ..Default::default() +})?; +``` + +### Data tracks wrap a track (#4519) + +The broadcast's `publish_binary_*`, `publish_json_*`, and `subscribe_json_*` are gone. +Create the track with `publish_track` or `subscribe_track`, then wrap it. + +```rust +let track = broadcast.publish_track("state".into(), None)?; +let producer = MoqJsonSnapshotProducer::new(&broadcast, &track, config)?; +``` + +Likewise `MoqJsonStreamProducer`, `MoqFlateSnapshotProducer` / `MoqFlateStreamProducer` (taking `MoqFlateConfig`), and `MoqJsonSnapshotConsumer` / `MoqJsonStreamConsumer` built from a `MoqTrackConsumer`. + +### Media lives in a `media` namespace (#4519) + +The broadcast's `publish_audio`, `publish_video`, `publish_container`, their `_on_track` and `_stream` variants, `set_video_properties`, `set_catalog_section`, `subscribe_catalog`, `subscribe_media`, and `fetch_media_group` are gone. +Construct `MoqMediaTrackProducer::audio` / `video` from the broadcast, a `MoqMediaTarget` (`Named` or `Requested`), and the init record; or `MoqMediaContainerProducer`, `MoqMediaCatalogProducer`, `MoqMediaCatalogConsumer::subscribe`, `MoqMediaTrackConsumer::subscribe`, or `MoqMediaGroupConsumer::fetch`. +Producers drop `name`, `used`, and `unused`; read them through `demand()`. + +```rust +let audio = MoqMediaTrackProducer::audio(&broadcast, MoqMediaTarget::Named { name: None }, init)?; +``` + +### Container byte-stream importers take an init record + +`MoqMediaContainerStreamProducer` takes a `MoqContainerStreamInit` holding the format instead of a bare format. +Its optional `timebase`, also on `MoqContainerInit`, shares one source's offset across container imports; leave it unset for independent offsets. + +### Error payloads are named + +`MoqError` cases that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`, so match `MoqError::Transport { reason }`. +The Display text is unchanged. + +### Timestamps are optional + +`MoqFrame.timestamp_us` and `MoqDatagram.timestamp_us` are optional, null on a frame read from an untimed track, and default to null. +`MoqTrackInfo.timescale` is null on a received untimed track, and a null timescale on publish makes the track untimed. +A raw track you publish is timed by default, so a raw write without a timestamp fails with `TimestampMismatch` instead of going out at 0. +Data operations take an optional timestamp in microseconds, and whole-frame media writes require one. +`MoqMediaCatalogProducer::now()` samples the broadcast clock for local-event stamps. +Wrap raw frame timestamps in `Some`. + +### Subscriber staleness is `max_delay_us` + +`MoqSubscription`, `MoqAudioDecoderOutput`, and `MoqVideoDecoderOutput` take `max_delay_us` instead of `max_age_us`. +`MoqTrackInfo.max_age_us` keeps its name: it is the publisher's retention. + +### `MoqTransport` gains `WebTransport` + +A WebTransport session reports `MoqTransport::WebTransport`; `Quic` now means raw QUIC only. +An exhaustive match needs one more arm, and a `Quic` check that meant any QUIC session also matches `WebTransport`. + +### `stalled` is `enabled` (#4915) + +`MoqVideo.stalled` is `enabled`, defaulting to true with the meaning inverted, and `MoqAudio` gains `enabled`. + +### Routes have one cost (#4661) + +`MoqRoute.cold` is removed; drop it. + +### Decoded video frames own their surface + +`MoqVideoConsumer::next` returns an `Arc` object with `timestamp_us()`, `width()`, `height()`, `pixels(format)`, and `surface()`. +`MoqVideoDecoderOutput.format` is gone; set `surface: true` to keep the decoder's native surface, readable as a `MoqVideoSurface`. +Read RGBA with `pixels(MoqVideoPixelFormat::Rgba)` instead of `data`. + +### Broadcast `finish` is removed (#4152) + +`MoqBroadcastProducer::finish()` is gone; close the broadcast instead. + +### The `android-logcat` feature is removed (#4608) + +Android builds always log to logcat; drop the feature. + +## 0.3 -> 0.4 + +### Publish split by kind + +`publish_media*` is `publish_audio`, `publish_video`, or `publish_container`, each taking its own init (`MoqAudioInit` with a `MoqAudioFormat` for audio). +The raw encoder paths that used to be `publish_audio` / `publish_video` are `encode_audio` / `encode_video`. + +### Renames + +| Before | After | +| --- | --- | +| durations in milliseconds | microseconds (`max_age_us`, `MoqBackoff.initial_us`) | +| `estimated_send_bitrate` / `estimated_recv_bitrate` | `estimated_send_rate` / `estimated_recv_rate` | +| `set_tls_disable_verify(bool)` | `set_tls_verify(bool)` | +| `MoqAnnounced` | `MoqAnnounceConsumer` | +| `MoqAnnouncement` | `MoqAnnounceUpdate` | +| `MoqBroadcastRequest::abort` | `reject` | +| `MoqOriginOptions` | `MoqOriginConfig` | +| `MoqAudioCodec` enum | an `opus()` object (#3671) | + +### Setters are fallible (#3642) + +Client and server setters return an error (`Busy` during connect or listen) instead of dropping the value, so handle it. + +### Protocol errors are structured + +`MoqError::Protocol` carries a `MoqProtocolError` (scope, wire code, kind) in its `details` field instead of a flattened message. + +### Track and group `finish()` keep the handle + +`finish()` keeps the handle open, so a later `abort()` can still run. diff --git a/rs/moq-flate/MIGRATE.md b/rs/moq-flate/MIGRATE.md new file mode 100644 index 00000000000..a9fdeef918e --- /dev/null +++ b/rs/moq-flate/MIGRATE.md @@ -0,0 +1,21 @@ +# Migrating moq-flate + +How to update your code for each breaking change, newest first. + +## Unreleased + +### moq-binary is moq-flate + +The opaque `snapshot` and `stream` tracks moved beside the codec, so `moq_binary::X` is `moq_flate::X`; the wire is unchanged. +`moq_binary::Error::Flate(e)` is the matching `moq_flate::Error` variant (`Decompress`, `TooLarge`). +`moq_flate::Error` carries `moq_net::Error`, so it is no longer `PartialEq`. + +### Demand is read through `demand()` + +Call `producer.demand().is_used()` instead of `producer.is_used()`. + +### Timestamps are explicit + +Snapshot and stream producers publish `at: None` untimed, so their track must be untimed; pass a timestamp on a timed track. +Snapshot `modify(at)` and `mutate(at, f)` carry the caller's optional time. +Wrap each update as `Timed::from(bytes).at(timestamp)`. diff --git a/rs/moq-gst/MIGRATE.md b/rs/moq-gst/MIGRATE.md new file mode 100644 index 00000000000..cfde2335929 --- /dev/null +++ b/rs/moq-gst/MIGRATE.md @@ -0,0 +1,11 @@ +# Migrating moq-gst + +How to update your pipelines for each breaking change, newest first. + +## Unreleased + +## 0.3 -> 0.4 + +### Rate estimates are `estimated-*-rate` + +The `moqsink` properties `estimated-send-bitrate` and `estimated-recv-bitrate` are `estimated-send-rate` and `estimated-recv-rate`, with no alias: an application or `gst-launch` line naming the old ones fails at runtime. diff --git a/rs/moq-hls/MIGRATE.md b/rs/moq-hls/MIGRATE.md new file mode 100644 index 00000000000..766df31baee --- /dev/null +++ b/rs/moq-hls/MIGRATE.md @@ -0,0 +1,33 @@ +# Migrating moq-hls + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Playlists take an optional range (#5705) + +`export::Rendition::playlist(query)` is `playlist(range, query)`. +Pass `None` for the live or history playlist. + +### Discontinuities are absolute sequences + +`export::segments::Segment::discontinuity` is a `u64` sequence, absolute within one `Broadcaster`, instead of a `bool` or a per-cursor count. +Store the value with each segment and use the first retained one as `EXT-X-DISCONTINUITY-SEQUENCE`. +A new `Broadcaster` starts a new sequence namespace: start a new recording, or map its sequences behind a discontinuity. + +### Playlists are bounded (#4978) + +Every timeline, durable ones included, lists a capped window of 256 segments. +Set `Config::history` to list a durable timeline past the window. +`master.m3u8` lists a video rendition only once a segment starts at a sync point, and answers 404 until one can start. + +```rust +let mut config = moq_hls::export::Config::default(); +config.history = true; +``` + +### `Broadcaster::new` is synchronous + +`Broadcaster::new` no longer awaits or fails. +A rendition whose sibling broadcast ends now ends its playlist instead of rebinding to a replacement. +Drop the `.await?`. diff --git a/rs/moq-ice/MIGRATE.md b/rs/moq-ice/MIGRATE.md new file mode 100644 index 00000000000..4be91a0b380 --- /dev/null +++ b/rs/moq-ice/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-ice + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-json/MIGRATE.md b/rs/moq-json/MIGRATE.md new file mode 100644 index 00000000000..ba870ca1ad8 --- /dev/null +++ b/rs/moq-json/MIGRATE.md @@ -0,0 +1,24 @@ +# Migrating moq-json + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Demand is read through `demand()` + +Call `producer.demand().is_used()` instead of `producer.is_used()`. + +### Timestamps are explicit + +`moq_net::Timed

` drops its clock type parameter. +Snapshot, stream, and window producers publish `at: None` untimed, so their track must be untimed; pass a timestamp on a timed track. +Snapshot `modify(at)` and `mutate(at, f)` carry the caller's optional time through the edit guard. +Wrap each update as `Timed::from(&value).at(timestamp)`, and pass `Some(timestamp)` to `modify`. + +## 0.3 -> 0.4 + +### Configs split by role (#3718) + +`compression: bool` is a `Compression` enum, and options that own a track live in each mode's `producer::Config` and `consumer::Config`. +`lock()` is `modify()?`, and a guard that fails to publish on drop aborts the track (#3644). +Set `Compression::Deflate` where you set `true`. diff --git a/rs/moq-loc/MIGRATE.md b/rs/moq-loc/MIGRATE.md new file mode 100644 index 00000000000..ad61a7f1b12 --- /dev/null +++ b/rs/moq-loc/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-loc + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-msf/MIGRATE.md b/rs/moq-msf/MIGRATE.md new file mode 100644 index 00000000000..9ac0d723c83 --- /dev/null +++ b/rs/moq-msf/MIGRATE.md @@ -0,0 +1,10 @@ +# Migrating moq-msf + +How to update your code for each breaking change, newest first. + +## Unreleased + +### `Track::stalled` is removed (#4915) + +The non-standard `stalled` track field is gone and no longer written. +Drop it from any `Track` you build. diff --git a/rs/moq-mux/MIGRATE.md b/rs/moq-mux/MIGRATE.md index 196c2c717e3..cf7adb6888e 100644 --- a/rs/moq-mux/MIGRATE.md +++ b/rs/moq-mux/MIGRATE.md @@ -4,20 +4,154 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `fmp4::Wire::new` refuses a track that isn't audio or video (#5815) + +`container::fmp4::Wire::new` returns `Result`, as `Wire::from_init` already did, and refuses any other handler instead of treating it as data. +Handle the error from `Wire::new`. + ### `ts::Programs::finish` returns each program's `Finish` (#5795) `Programs::finish` returns `anyhow::Result>` instead of `anyhow::Result<()>`. Each handle keeps its program's broadcast servable until its media is stale, and dropping it closes that program. -Await `Finish::wait` to linger, or drop the handles to close every program now. +Await `Finish::wait` on each handle to linger, or drop the handles to close every program now. + +### Timeline durations are hints (#5715) + +`timeline::Config::duration_max`, `with_duration_max`, and `DEFAULT_DURATION_MAX` are gone; set `duration_hint` with `with_duration_hint`, the expected record duration such as the GOP, and the split ceiling follows from it. +`DEFAULT_DURATION_MIN` is 4 s instead of 2 s, so set `with_duration_min` to keep the shorter records. + +### The catalog consumer is opaque (#5630) + +Construct `catalog::Consumer` with `Consumer::new(broadcast, format).await`, or convert a `catalog::hang::Consumer` with `.into()`, then read it through `catalog::Stream`. +Its `Hang`, `Msf`, and `Closed` variants and `catalog::msf::Consumer::track` are private. + +### Timelines record untimed tracks (#5720) + +`timeline::Segmenter::frame` takes `pts: impl Into>`, so existing callers compile. +A segmenter fed the wrong timedness for its track fails with `Error::TimelineTiming` or `Error::TimelineUntimed`. + +### An unset host clock publishes no wall mapping (#5429) + +`Clock::wall()` returns `Option` instead of panicking, so handle `None`. +`Clock::wall_clock` and `Timebase::place` refuse a clock without one. + +### TS export lingers within one publisher instance (#5147) + +`ts::Export::resume` is `ts::Export::follow`, `Source::returned` is gone, and `ts::Follower` follows a path's announcements the way `moq export ts` does. +`ts::Export::new(source, catalog)` takes a catalog stream like the other exporters: subscribe with `source.catalog::(format).await?` to keep MPEG-TS metadata, or `()` for media only, and select renditions on the stream before passing it in. +`follow(broadcast, catalog)` takes the replacement stream too, both methods are synchronous, and `with_ts` and `with_catalog_format` are removed. + +### `Source` is built from the resolved broadcast (#5252) + +`Source::new(origin, path)` is `Source::new(origin, broadcast)`: resolve the broadcast first, and catalog references resolve from its path. +`Source::broadcast()` returns it without awaiting. +Every later request for a path, siblings included, stays on the broadcast first held there and fails once it ends, so build a new `Source` to follow a new instance. ```rust -// Before -programs.finish()?; +let broadcast = origin.request_broadcast(path, None).await?; +let source = Source::new(origin, broadcast); +``` + +### The export delay is `with_delay` (#4645) + +`ts::Export::with_max_age` and the FLV and MKV exports' `with_max_age` are `with_delay`: each frame is written that long after its decode time, on a clock that follows the source's. +`ts::Export` defaults to 1 s, and the send-ahead comes out of the same budget, so the output trails a TS source by the delay instead of twice it. +`ts::stats::Export` gains a dropped-frame count, measured drift, and an out-of-tolerance count, and is no longer `Eq`. + +### Subscriber staleness is `max_delay` + +| Before | After | +| --- | --- | +| `container::Consumer::set_max_age` | `set_max_delay` | +| fMP4, H.264, and H.265 exports' `with_max_age` | `with_max_delay` | + +### Importers keep the source's timestamps + +`clock::Anchor`, `clock::Lane`, `SourceMap`, `Clock::source`, and the importers' `live()` are gone; delete the `live()` calls. +Importers publish the source's own timestamps and anchor the catalog clock on their first frame. +An importer whose first frame arrives once the clock is in use (taken with `catalog.clock()`, published, or pinned with `Config::with_clock`) shifts its timestamps onto it, so its first frame lands at now. +Importers sharing a timestamp base reserve through one `catalog.timebase()`. + +### Importers refuse a timestamp rewind + +A backward timestamp is an encoder restart and needs a new broadcast. +Every importer, MPEG-TS included, now ends with `TimestampRewind` on a loop wrap, restart, or join below the edge instead of shifting past it. +`container::Producer` refuses a group that does not start strictly after the previous group's start, or a frame below the start of the group before its own. + +### Importers publish the catalog at their first frame + +The fMP4, MKV, and MPEG-TS importers hold the catalog until the first frame of a selected track, as FLV already did, so the first snapshot carries the final root `clock`. +A track deselected by `with_select` does not release it. +A `moov` or `Tracks` decoded after `finish()` is refused with `fmp4::Error::MoovAfterFinish` or `mkv::Error::TracksAfterFinish`. + +### Video importers drop `tick`, `idle`, and `observe_lag` (#4915) + +The H.264, H.265, AV1, VP8, and VP9 `Import`s no longer take liveness hints; a stalled rendition is marked by the catalog's `enabled` flag instead. +Delete the calls. -// After: serve the finished programs until their media is stale -let finished = programs.finish()?; -futures::future::join_all(finished.into_iter().map(moq_net::broadcast::Finish::wait)).await; +### fMP4 export fixes its track set at the init segment -// After: close every program now -drop(programs.finish()?); +`fmp4::Error` drops `MissingVideoTrack`, `MissingAudioTrack`, and `NoCatalogSnapshot`, and adds `TrackAdded`, `TrackChanged`, `TrackRewound`, and `TrackUndescribed`. +`fmp4::Export` ends with one of these where it used to write a track missing from the moov, and a broadcast that ends with media queued behind an undescribed track is an error instead of `Ok(None)`. +Restart the export to pick up a new rendition. + +### fMP4 export of Annex-B H.264 and H.265 starts from the catalog + +When the catalog codec string and dimensions are enough, the export writes an `avc3` or `hev1` init before the first keyframe and leaves the parameter sets in the samples. +A keyframe whose parameter sets never appeared ends the export instead of waiting 30 seconds. + +### CMAF timescales must match the init + +CMAF `Wire::write` refuses a track whose timescale is not the init's `mdhd` timescale with `fmp4::Error::TimescaleMismatch`. +A track you create yourself declares it with `track::Info::with_timescale`, passed to `create_track`. + +```rust +let info = track::Info::default().with_timescale(Timescale::new(mdhd_timescale)?); +let track = broadcast.create_track(name, info)?; ``` + +### Data timestamps use the broadcast clock (#4821) + +JSON and flate producers publish `at: None` untimed, so their track must be untimed too; pass a timestamp on a timed track. +Convert a capture `Instant` through the catalog clock. + +```rust +producer.update(Timed::from(&value).at(catalog.clock().capture(instant)?))?; +``` + +### moq-binary is moq-flate + +`Error::Binary` is `Error::Flate`, carrying a `moq_flate::Error`. + +### AAC refuses channel counts it cannot name (#4973) + +`aac::Config::encode` returns a `Result` and fails with `UnsupportedChannelCount` for a count no `channelConfiguration` names, instead of writing stereo. +`From for hang::catalog::AudioConfig` is `TryFrom`, so convert with `try_into()?`. + +### The Opus mapping family lives only on `mapping` (#4851) + +`codec::opus::Config::mapping_family` is gone; drop the field. +Family 0 is `mapping: None`; any other family is `mapping.family()`. + +### TS stats live in `ts::stats` (#4909) + +`ts::Stats` is `ts::stats::Snapshot`, and `ts::StreamStats` is `ts::stats::Stream`, whose `track` is an owned `String`. +`ts::Export::stats` returns `ts::stats::Export`; feed it to `stats::Log` with `.into()`. +`ts::MultipleProgramsError` is `#[non_exhaustive]`: recover it by downcast and read `programs`. + +### FLV export takes a catalog stream (#4987) + +`flv::Export::new` is synchronous and takes `(source, catalog)`, like `fmp4::Export` and `mkv::Export`. +`with_catalog_format` and `with_select` are gone: open the catalog yourself with `source.catalog(format).await?` and narrow it with `catalog::Stream::select`. + +## 0.9 -> 0.10 + +### Catalog edits are fallible (#3644, #3813) + +`lock()` is `modify()?`, and a guard that fails to publish on drop aborts the track. +`timeline::Config::wall` is the broadcast `Clock`. + +### Imports are typed + +`import::Init` splits into `AudioInit`, `VideoInit`, and `ContainerInit`, with typed formats instead of strings. +`Track::new` is `Track::audio` or `Track::video`. diff --git a/rs/moq-net/MIGRATE.md b/rs/moq-net/MIGRATE.md index 1a66b269119..17a15cdb876 100644 --- a/rs/moq-net/MIGRATE.md +++ b/rs/moq-net/MIGRATE.md @@ -4,27 +4,242 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Subscriptions carry group ranges (#5732) + +`track::Subscription::start` and `end` are replaced by `ranges: Vec`, and `with_start` and `with_end` are gone. +Use `with_groups(start..end)` for one span of groups, or `with_ranges` with `Range::live()`, `Range::oldest()` (the default), or `Range::groups(..)`. + +### Receive estimates come from `Session::recv_bandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session::recv_bandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and drop it to stop. + +```rust +let bandwidth = session.recv_bandwidth(); +let rate = bandwidth.as_ref().and_then(|b| b.peek()); +``` + ### A timed track refuses a group that rewinds (#5803) A local write whose first frame starts before a lower-sequence group's, or after a higher one's, fails with `TimestampMismatch`; `@moq/net` throws it the same way. Received groups are accepted as is, since their timestamps may be send times. A publisher whose timeline restarts, such as an HLS `EXT-X-DISCONTINUITY` that resets decode times, publishes a new broadcast instead. -### A bare track handle is not demand (#5820) +### `broadcast::Producer::finish` is back, and lingers (#5795) -Holding a `track::Consumer` sends nothing and no longer keeps the producer used. -Only a query, subscription, or fetch made from it counts, until it is dropped. -Hold one of those for as long as you want the track. +`finish()` ends the broadcast cleanly and returns a `Finish` that keeps its finished tracks servable until their media is stale. +Dropping the `Finish` closes the broadcast, so `close()` still ends it now. + +```rust +// Close now +producer.close(); +// Serve late subscribers until the media is stale +producer.finish().wait().await; +``` + +### `auth::Token` is `auth::Auth` (#5695) + +Rename `auth::Token` to `auth::Auth`. +`auth::Handle::add` returns an `auth::Auth`, which is `Clone` and compares by identity, so a route can name it with `origin::Route::with_auth`. + +### Custom transports send datagrams without polling (#5770) + +`transport::poll::Session::send_datagram` replaces `poll_send_datagram`. +It takes the payload as `Bytes`, the send group to schedule it in (`None` for ungrouped), and an order, and never blocks. ```rust -// Before -let track = broadcast.track("video")?; // kept the producer used -// After -let track = broadcast.track("video")?; -let _sub = track.subscribe(None); // or `track.query()` to only learn its info +fn send_datagram( + &mut self, + payload: Bytes, + group: Option<&::Group>, + order: i64, +) -> Result<(), Self::Error> { ... } ``` +### A bare track handle is not demand (#5820) + +Holding a `track::Consumer` sends nothing and no longer keeps the producer used. +Only a query, subscription, or fetch made from it counts, until it is dropped. +Hold one of those, such as `track.subscribe(None)` or `track.query()` to only learn its info, for as long as you want the track. + ### Dynamic broadcasts see requests when they are made (#5820) `broadcast::Consumer::track` no longer enqueues a request. `broadcast::Producer::requested_track` yields it once the handle queries, subscribes, or fetches. + +### Custom transports declare send groups (#5711) + +A `transport::poll::SendStream` declares `type Group` (`Ungrouped` when the transport has none) and implements `set_group`, and `set_priority` takes an `i64`. +A `transport::poll::Session` implements `send_group`, returning `None` without groups. + +```rust +type Group = Ungrouped; +fn set_priority(&mut self, priority: i64) { ... } +fn set_group(&mut self, group: &Ungrouped) { match *group {} } +// and on the Session +fn send_group(&mut self, _priority: i64) -> Option { None } +``` + +### Superseded groups age by wall clock too (#5548) + +A superseded group is stale once its successor arrived a full `max_delay` ago, as well as by media time. +Untimed tracks are no longer exempt: with the default zero `max_delay`, an untimed subscriber starts at the latest group and skips each superseded one. +Give it a budget to replay or tolerate reordering. + +```rust +let sub = track::Subscription::default().with_max_delay(Duration::from_secs(1)); +``` + +### `cache::Pool::gc` takes a waiter + +Call `pool.gc(now, &waiter)`, and call it again when the waiter wakes, since a parked read can arm an earlier deadline than the one returned. + +### A replacement track owns its start sequence (#5068) + +`append_group` and `append_datagram` on a track that replaces an earlier producer of the same name return `Error::Duplicate` instead of continuing past the earlier producer's sequence. +Pick the start sequence yourself with `create_group(group::Info { sequence })`, or publish a new broadcast. + +### Announcements are `Start`, `Update`, `End`, and `Restart` events + +`announce::Consumer` yields `announce::Event`, carrying an `announce::Announce { prefix, captures, route }`. +`announce::Update` and `announce::Kind` are gone. +`Restart` means another publisher instance now serves the prefix: request the path again. +A newer epoch no longer ends open subscriptions with `Unroutable`; they stay on the old instance until dropped or its route goes. + +```rust +while let Some(event) = announced.next().await { + match event { + announce::Event::Start(a) | announce::Event::Restart(a) => { + let broadcast = origin.request_broadcast(&a.prefix, a.route.epoch).await?; + } + announce::Event::Update(_) => {} + announce::Event::End(_) => {} + } +} +``` + +### Routes carry the publisher epoch (#4942) + +An epoch is a property of the route, not the path. +`Path::join_epoch` and `Path::split_epoch` are gone; publish the plain path with `route.with_epoch(epoch)` and read `Route::epoch`. +Nothing mints an epoch by default, and a route without one does not resume across routes. + +### `request_broadcast` takes an epoch + +Pass `None` to accept any publisher instance, or an `Epoch` to refuse every other one. +A dynamic route updated to another epoch refuses the requests its handler still holds with `Unroutable`. + +### Demand is read through `demand()` + +`track::Producer`'s `is_used`, `used`, `unused`, and `poll_unused`, and `group::Producer`'s `used` and `unused`, moved to `demand()`: `track.unused().await` is `track.demand().unused().await?`. +`track::Request`, `track::Dynamic`, and `group::Request` poll it through `demand().poll_unused`, which returns `Poll>`: treat `Err` (the request closed) as unused too. +A `group::Request` is withdrawn once its last `fetch_group` caller leaves, so a handler that sees it unused just drops it; a later fetch queues a fresh request, and serving the abandoned one returns `Error::Duplicate`. + +### moq-net owns its transport traits + +`moq_net::web_transport_trait` is gone. +`transport::poll::{Session, SendStream, RecvStream}` carry their own `poll_*` methods, `transport::Error`, and `transport::Stats`, and `Error::from_transport` takes a `transport::Error`. +A custom transport implements these traits and reports `binding()`: `transport::Binding::Uri` when its handshake carries the request URI, or `InBand` when SETUP carries it. + +```rust +impl moq_net::transport::poll::Session for MySession { + fn binding(&self) -> moq_net::transport::Binding { + moq_net::transport::Binding::Uri + } + // poll_accept_bi, poll_open_uni, ... +} +``` + +### Data publishes keep optional timestamps + +`Timed

` drops its clock type parameter and its `at` is an `Option`. +Build it as `Timed { value, at: Some(at) }`. +Publish `at: None` only on an untimed track; pass a timestamp on a timed one. + +### A track is timed or untimed + +`track::Info.timescale` is `Option`, `None` for an untimed track, and an undeclared timescale no longer defaults to milliseconds. +`Frame.timestamp`, `frame::Info.timestamp`, and `Datagram.timestamp` are `Option`, and `group::{Producer, Consumer}::timescale()` return `Option`. +A write whose timedness does not match its track fails with `TimestampMismatch`. +Tracks from moq-lite before 05 and moq-transport without `TIMESCALE` arrive untimed instead of stamped with their arrival time. +To keep millisecond timestamps, pass `track::Info::default().with_timescale(Timescale::MILLI)` to `create_track` instead of `None`. + +### Subscriber staleness is `max_delay` + +How far a group may fall behind the live edge before a subscriber skips it is no longer named `max_age`, which stays the publisher's retention on `track::Info`. +Call `track::Subscription::with_max_delay` instead of `with_max_age`. + +### Publisher retention is optional (#4242) + +`track::Info::max_age` is `Option`, `None` meaning no publisher limit. +`track::DEFAULT_MAX_AGE` and `origin::Config::default_max_age` are gone: pass your own `Duration` to `with_max_age`. + +### Route cost is one value (#4661) + +`Cost` is opaque: construct it with `Cost::new` and read it with `value()`. +The `warm` and `cold` fields are gone. + +### Datagrams are unfetchable and bounded by the group range (#4982) + +A FETCH that reaches a datagram fails with `Error::NotFetchable` (received as `Error::Stream(StreamError::NotFetchable)`). +Datagrams outside a subscriber's group range are skipped. +Treat both like `NotFound` if you match on errors. + +### Sessions cap peer requests (#4820) + +A peer past the session's announce, subscription, or fetch bound loses the session with `TooManyRequests`. +Raise the defaults for a large relay mesh. + +```rust +let client = Client::new().with_limits(session::Limits { subscriptions: 100_000, ..Default::default() }); +``` + +### A path is one broadcast (#4741) + +Broadcast identity is the path, whatever the publisher or first hop. +A publisher that restarts its group numbering must publish under a new broadcast name. + +### Broadcast `finish` and `abort` are removed (#4152) + +Close a broadcast with `close()`; its end carries no cause. +`broadcast::Consumer::is_finished` is gone, and `closed()` on `Consumer` and `Dynamic` returns `()`. + +### No `VarInt` + +Varints are plain `u64`s. +Use `moq_net::varint::decode_quic(buf)` and `moq_net::varint::encode_quic(v, buf)`, which fails past `varint::MAX_QUIC`. + +## 0.2 -> 0.3 + +### Origins (#3400, #3804) + +`create_broadcast(path, route)` is `origin.publish(path, route)?`, or `create_broadcast(path)` then `broadcast.announce(route)`. +`with_root` plus `scope(prefixes)` is one `scope(root, &patterns)` returning `Result`. +`origin::Info` is `origin::Config`. + +### Announcements are prefix routes (#3225, #3770) + +`announce::Update` is `{ prefix, route, kind, captures }`. +Skip inactive updates and resolve the broadcast through the origin. +Serve a subtree on demand with `origin.dynamic(prefix, route)`. + +```rust +if !update.kind.is_active() { + continue; +} +let broadcast = consumer.request_broadcast(&update.prefix).await?; +``` + +### Track renames (#3666) + +| Before | After | +| --- | --- | +| `with_latency_max` / `latency_max` | `with_max_age` / `max_age` | +| `write_datagram(Datagram)` | `insert_datagram(sequence, timestamp, payload)` | +| `track::SubscriberControl` | `track::Control` | +| `track::GroupRequest` | `group::Request` | +| `ConnectionStats` | `session::Stats`, with `estimated_send_rate` / `estimated_recv_rate` | + +### Oversized groups abort (#3585) + +A group past its size limit aborts with `GroupTooLarge` instead of shedding its head. diff --git a/rs/moq-nvenc/MIGRATE.md b/rs/moq-nvenc/MIGRATE.md new file mode 100644 index 00000000000..d7b0893b066 --- /dev/null +++ b/rs/moq-nvenc/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-nvenc + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-pattern/MIGRATE.md b/rs/moq-pattern/MIGRATE.md new file mode 100644 index 00000000000..26d95c4c66b --- /dev/null +++ b/rs/moq-pattern/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-pattern + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-quic/MIGRATE.md b/rs/moq-quic/MIGRATE.md index 0bff7f133db..7072146ca40 100644 --- a/rs/moq-quic/MIGRATE.md +++ b/rs/moq-quic/MIGRATE.md @@ -1,16 +1,15 @@ # Migrating moq-quic +How to update your code for each breaking change, newest first. + ## Unreleased +### `Datagrams::send` takes a send group and order (#5770) + +`Datagrams::send(data, group, order)` replaces `send(data, drop)`; pass `None` for an ungrouped datagram. +A full buffer drops the oldest datagram of the lowest-priority group, so `SendDatagramError::Blocked` and `Event::DatagramsUnblocked` are gone. + ### `congestion::NewReno` removed `congestion::NewReno` and `congestion::NewRenoConfig` are gone. Use `Bbr3Config` (the default) or `CubicConfig` instead. - -```rust -// Before -config.congestion_controller_factory(Arc::new(congestion::NewRenoConfig::default())); - -// After -config.congestion_controller_factory(Arc::new(congestion::CubicConfig::default())); -``` diff --git a/rs/moq-relay/MIGRATE.md b/rs/moq-relay/MIGRATE.md new file mode 100644 index 00000000000..8145e268168 --- /dev/null +++ b/rs/moq-relay/MIGRATE.md @@ -0,0 +1,137 @@ +# Migrating moq-relay + +How to update your code, invocation, and config for each breaking change, newest first. +Rolling a cluster across a release also follows the [upgrade guide](https://moq.dev/setup/upgrade). + +## Unreleased + +### `max_streams = 0` is refused (#5871) + +A `[quic]` `max_streams` of 0 stops startup, as `[peer.quic]` already did, since zero streams wedge the connection. +Remove it to keep the default. + +### Auth roles carry `moq_auth::Access` (#5697) + +`auth::Token::publish` and `subscribe` are `moq_auth::Access`, so an embedder reads the patterns from `.path`. +The relay refuses every grant with a bitrate cap, including one added in-band or at revalidation, because no transport meters it yet; HTTP `/fetch` and `/announced` always refuse caps. +An embedder running its own gateway must enforce the cap or refuse the grant before forwarding media. + +### Workers default to unpinned (#5688) + +`runtime.pin` defaults to off, so two worker groups on one host no longer crowd onto the first cores. +Set it to keep the old placement. + +```toml +[runtime] +pin = true +``` + +### iroh is opt-in (#5223) + +The default build has no iroh listener and no `--iroh-*` flags or `[iroh]` table. +Build with `--features iroh` to keep it. + +### The `noq` cargo feature is `quic` + +Rename it wherever you enable it; the QUIC stack is MoQ's own `moq-quic`. +A qlog directory gets one file per endpoint instead of one per connection. + +### A QUIC load-balancer nonce needs its server id (#5114) + +`listen.lb_nonce` (`--listen-quic-lb-nonce`, `MOQ_LISTEN_QUIC_LB_NONCE`) without `lb_id` stops startup. +Setting `lb_id` or `lb_nonce` together with `load_balancer` also stops; `lb_id` used to win. +Set `load_balancer`, or `lb_id` alone (nonce 8), or `lb_id` with `lb_nonce`. + +### Settings no listener reads stop startup (#5006) + +A stream-only relay (TCP or Unix, no `--listen`) refuses the QUIC-only `--listen-preferred-v4` / `-v6`, `--listen-quic-lb-id`, and pinned `tls.peers`. +It also refuses `--listen-tls-cert`, `-key`, or `-generate` unless `--listen-tcp-tls` serves it, and `listen.tls.root`, since only QUIC verifies a client CA. +`--listen-unix-allow-*` needs `--listen-unix-bind`, and `web.https.cert`, `key`, and `root` need `web.https.listen`. +Each used to be ignored: drop it, or add the listener it configures. + +### `--cluster-mesh` and `--cluster-linger` are unknown flags (#4818) + +They, TOML `mesh` and `linger`, and `MOQ_CLUSTER_MESH` / `MOQ_CLUSTER_LINGER` are no longer read. +List every peer with `--cluster-connect` or `--cluster-connect-api`. +In Rust, `cluster::Config` has no `mesh` or `linger` field. + +### A route without a publisher keeps hop 0 (#4969) + +A route that names no publisher keeps its 0 instead of gaining a random hop. + +### Embedding: auth takes the client-CA answer (#4912) + +`auth::Config::validate` and `init` take `client_ca: bool`, whether any listener verifies client certificates, and `validate_client_ca` is gone. +Pass `client_ca` to `validate` and delete the `validate_client_ca` call. + +### Embedding: io\_uring workers bind from the server config (#5312) + +`uring::Workers::bind` takes a `moq_tokio::server::Config`, so generated and rotating certificates work in both worker modes. +Set `listen` and `quic` on a `moq_tokio::server::Config` and pass it to `bind`. + +## 0.16 -> 0.17 + +### Gossip discovery is removed (#4601) + +A relay dials only the peers it lists or finds on the LAN, never a URL learned from an announcement, and no longer announces `.internal/origins`. +`--cluster-mesh` and TOML `mesh` are refused by name: list every peer with `--cluster-connect` or `--cluster-connect-api`. +The `/nodes` endpoint lists only peers this relay dialed. + +## 0.15 -> 0.16 + +### 0.14 auth behavior is restored (#4319) + +- Removed 0.14 auth settings (`[auth]` `key`, `key_dir`, `auth_api`, `domains`, `mtls_tier`, `[auth.tls]`, their flags and `MOQ_AUTH_*` variables) stop the relay with the replacement named. +- Public and mTLS patterns are rooted at `/`, as in 0.14: `anon/**` admits a client dialed at `/anon`. +- A token's `iss`, `sub`, and `jti` are ignored and `nbf` is enforced; any other claim, `aud` and `cluster` included, refuses the token. +- A relay on `--auth-public` refuses a session presenting a token, including a peer's `cluster.token`, and refuses to start with a client CA. + +Replace `key_dir` with `moq auth serve --key-dir keys/` and point `[auth]` `url` at it, such as `http://127.0.0.1:4440`. + +## 0.14 -> 0.15 + +### Flags split by direction + +The flags split into `--listen-*` (accepting), `--connect-*` (dialing), and a shared `--quic-*` section. +Environment variables follow the flag (`MOQ_SERVER_BIND` is `MOQ_LISTEN`), and a renamed one is refused with its replacement named. + +| Before | After | +| --- | --- | +| `--server-bind` | `--listen` | +| `--server-*`, `--tls-cert`, `--tls-key`, `--tls-generate` | `--listen-*`, `--listen-tls-cert`, `--listen-tls-key`, `--listen-tls-generate` | +| `--client-connect` | `--connect` | +| `--client-*` | `--connect-*` | +| `--client-failover-delay` | `--connect-race` | +| `--client-reconnect=false` | `--connect-once` | +| `--tls-disable-verify`, `--client-tls-disable-verify` | `--connect-tls-insecure` | +| `--server-quic-*`, `--client-quic-*` | `--quic-*`, applied to both directions | +| TOML `[server]`, `[client]` | `[listen]`, `[connect]` | +| TOML `[server.quic]`, `[client.quic]` | `[quic]` | +| TOML `listen`, `connect`, `failover_delay`, `reconnect`, `disable_verify` | `bind`, `url`, `race`, `once` (inverted), `insecure` | +| `--cluster-linger` | removed; a broadcast closes when its last publisher is lost | +| `--cluster-connect host:port` | a full URL, `https://host/?jwt=TOKEN` | + +### Auth is one contract (#3688) + +The relay asks an auth server per session (`--auth-url`) or applies a static grant (`--auth-public`); exactly one is required. +`--auth-key`, `--auth-key-dir`, `--auth-api`, `--auth-api-mode`, `--auth-public-api`, `--auth-domain`, `--auth-mtls-tier`, and `--auth-tls-*` are gone: run `moq auth serve --key-dir ...` beside the relay and point `--auth-url` at it. +The flag-by-flag mapping is in [Migrating from the relay flags](https://moq.dev/bin/relay/auth#migrating-from-the-relay-flags). +A verified client certificate admits nothing on its own; the auth server grants it. + +```bash +moq auth serve --key-dir keys/ --public-publish 'anon/**' --public-subscribe 'anon/**' & +moq-relay --auth-url http://127.0.0.1:4440 +``` + +### Grants are patterns + +`anon` is exactly the broadcast `anon`; write `anon/**` for the subtree. +This applies to `--auth-public`, TOML `public`, and `[auth.public]`, which is now `public_subscribe` / `public_publish`. +A public or mTLS pattern with no wildcard refuses to start, naming the subtree to write. +JWT `publish` and `subscribe` claims are patterns too, so a token granting `alice` covers only `alice`. + +### Embedding + +`Relay` fields are private (#3638): clone the handles you need, such as `relay.cluster().origin.clone()`, mount routes, then call `Relay::run`. +`Cluster::with_cache` moved to `cluster::Options`. +The `quinn` and `quiche` features and the backend setting are gone (#3811). diff --git a/rs/moq-room/MIGRATE.md b/rs/moq-room/MIGRATE.md new file mode 100644 index 00000000000..fab249cc52d --- /dev/null +++ b/rs/moq-room/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-room + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-rtc/MIGRATE.md b/rs/moq-rtc/MIGRATE.md new file mode 100644 index 00000000000..144d254252f --- /dev/null +++ b/rs/moq-rtc/MIGRATE.md @@ -0,0 +1,10 @@ +# Migrating moq-rtc + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Sessions return their UDP wire source (#5632) + +`Client::publish` and `Client::subscribe` return `Result` instead of `Result<()>`. +Read per-peer byte counts with `source.stats()`, or discard it. diff --git a/rs/moq-rtmp/MIGRATE.md b/rs/moq-rtmp/MIGRATE.md new file mode 100644 index 00000000000..b292743a7f8 --- /dev/null +++ b/rs/moq-rtmp/MIGRATE.md @@ -0,0 +1,16 @@ +# Migrating moq-rtmp + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Export staleness is `max_delay` + +| Before | After | +| --- | --- | +| `Play::with_max_age` | `Play::with_max_delay` | +| `Client::with_export_max_age` | `Client::with_export_max_delay` | +| `listen::Config::export_max_age` | `listen::Config::export_max_delay` | +| `DEFAULT_MAX_AGE` | `DEFAULT_MAX_DELAY` | + +FLV egress now releases each frame on that delay after its decode time, instead of dropping frames older than it. diff --git a/rs/moq-sock/MIGRATE.md b/rs/moq-sock/MIGRATE.md new file mode 100644 index 00000000000..ed64e78be57 --- /dev/null +++ b/rs/moq-sock/MIGRATE.md @@ -0,0 +1,18 @@ +# Migrating moq-sock + +How to update your code for each breaking change, newest first. + +## Unreleased + +### A shard group binds in one call (#5688) + +`shard::Group`, `Member`, `Claim`, `cid`, and `CID_LEN` are gone. +`shard::bind(addr, count)` returns the complete `Bound` group; take sockets from it with `member()`. +Connection IDs are steered by `moq_quic::RandomConnectionIdGenerator::set_shard` instead of `shard::cid`. + +```rust +let mut bound = shard::bind(addr, count)?; +while let Some(socket) = bound.member()? { + // serve socket +} +``` diff --git a/rs/moq-srt/MIGRATE.md b/rs/moq-srt/MIGRATE.md new file mode 100644 index 00000000000..ffd651633bc --- /dev/null +++ b/rs/moq-srt/MIGRATE.md @@ -0,0 +1,10 @@ +# Migrating moq-srt + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Egress runs at least one second behind (#5553) + +SRT egress delays the TS export by the SRT latency or 1 s, whichever is longer, and the send-ahead now comes out of that budget. +A feed whose video HRD buffer declares a longer delay stops, naming the delay it needs. diff --git a/rs/moq-stats/MIGRATE.md b/rs/moq-stats/MIGRATE.md new file mode 100644 index 00000000000..a14972577b2 --- /dev/null +++ b/rs/moq-stats/MIGRATE.md @@ -0,0 +1,10 @@ +# Migrating moq-stats + +How to update your code for each breaking change, newest first. + +## Unreleased + +### A returning group restarts under a new epoch (#4904) + +A stats group that returns announces a new epoch and starts its group numbers at zero, instead of continuing the old sequence. +A reader that keyed history on the path alone should end that history when the announcement ends and start fresh on the next one. diff --git a/rs/moq-tokio/MIGRATE.md b/rs/moq-tokio/MIGRATE.md new file mode 100644 index 00000000000..d408979627a --- /dev/null +++ b/rs/moq-tokio/MIGRATE.md @@ -0,0 +1,112 @@ +# Migrating moq-tokio + +How to update your code for each breaking change, newest first. + +## Unreleased + +### QUIC limits are `moq_quic::Limits`, set through `Request::quic` (#5871) + +`server::Request::set_quic_limits` and `Error::QuicLimits` are gone. +`Request::quic()` returns the `quic::Connection` under a QUIC or WebTransport request, or `None` on other transports, and its `set_limits` takes `quic::Limits`, now `moq_quic::Limits`, where each `None` field keeps that limit. +Call `set_limits` on `request.quic()` instead, starting from `quic::Limits::default()` and setting only the fields you change. +`quic::Resolved`'s `receive_window`, `stream_receive_window`, and `send_window` are plain `u64`s with the defaults filled in, and `quic.max_streams = 0` is refused. + +### Auth handles live in `connection::auth` (#5695) + +The root `Auth` re-export is gone. +`Connection::auth` returns a `connection::auth::Handle`, and its `add` returns a `connection::auth::Auth` instead of a `Token`. + +### Datagrams take a send group and order (#5770) + +`quic::driver::Connection::send_datagram` and `quic::web::Session::send_datagram` take the send group (`None` for ungrouped) and an order, and never block. +`send_datagram_wait` and `SendDatagram` are gone; call `send_datagram(data, None, 0)` instead. + +### The `noq` feature is `quic` (#5664) + +The QUIC stack is MoQ's own `moq-quic` instead of the `moq-noq` crates. +Rename the `noq` cargo feature to `quic`, and the error types with it: `Error::Noq` is `Error::Quic`, carrying a `moq_tokio::quic::Error`. +A qlog directory gets one file per endpoint instead of one per connection. + +### Workers are unpinned by default (#5688) + +`worker::Config::new(count)` no longer pins worker *i* to core *i*. +Call `with_pin(true)` to keep pinning. + +```rust +let workers = moq_tokio::worker::Config::new(8).with_pin(true); +``` + +### Accepted sessions return their driver task (#5562) + +`server::Request::ok()` returns `(session, driver)`. +Await the driver before classifying the session close, so a driver panic reports `Error::Panic` instead of the cancellation its teardown leaves behind. + +### Config durations are optional + +Wrap durations in `Some(...)` for connect `race`, `resolution_delay`, and `timeout`; listen `timeout`; QUIC `idle_timeout` and `keep_alive`; backoff `initial`, `max`, and `timeout`; GOAWAY `handover`; and WebSocket `delay`. +`None` uses the default, and `Some(Duration::ZERO)` keeps each knob's zero behavior. +A value set in code now wins over the parser default unless the flag or environment variable was actually given. +TOML keys are unchanged. + +### Rotating TLS identities are a `tls::Serve` (#5312) + +`tls::Listen::server_config_reloading` and `tls::ReloadingServerConfig` are gone; load once with `tls::Listen::load` and read each handshake's config from the returned `tls::Serve` with `config(alpns)`. +`Error::WorkerTlsGenerate` is gone because generated certificates now work on workers too. +A TLS trust-root watcher that fails to register now fails client and server construction. + +### "Epoch" means only the publisher epoch (#5146) + +The reconnect counter is `connects()`; rename `epoch()` calls. + +### A QUIC load-balancer nonce needs its server id (#5114) + +`listen.lb_nonce` without `lb_id` fails to start, so set `lb_id` next to it. +Setting `lb_id` (or `lb_nonce`) together with `load_balancer` also fails; `lb_id` used to win silently. +Set `load_balancer`, or `lb_id` alone (nonce 8), or `lb_id` with `lb_nonce`. + +### Settings no listener reads stop startup (#5006) + +A stream-only listener (TCP or Unix, no QUIC bind) refuses the QUIC-only `preferred_v4` / `preferred_v6`, `lb_id`, pinned `tls.peers`, and a TLS cert, key, or `generate` unless `tcp.tls` serves it. +`unix.allow_*` needs `unix.bind`. +Each used to be ignored: drop it, or add the listener it configures. + +### A client CA needs a QUIC listener + +A stream-only listener with `listen.tls.root` returns `Error::MtlsUnsupported` instead of silently verifying nothing. + +### `Transport` names WebTransport + +`server::Transport` is `moq_tokio::Transport`, with no re-export. +A WebTransport session reports `Transport::WebTransport` (`"webtransport"` in logs); `Quic` now means raw QUIC only, so match both where you meant either. +`Connection::transport()` reports the live transport. + +### Custom sessions declare their request binding + +`transport::Session::new` takes the session's `moq_net::transport::Binding`: `Uri` when the transport handshake carries the request URI, `InBand` when SETUP does. +`Client` and `Server` are unchanged. + +### `Redirect::resolve` is removed (#4854) + +`Redirect` and `Goaway` stay, but the lenient `resolve(uri, current)` is gone. +Follow a GOAWAY through the connection, which validates the target itself: set `config.goaway.redirect`. + +### The `android-logcat` feature is removed (#4608) + +Android builds always log to logcat. +Drop the feature from your dependency. + +## moq-native 0.19 -> moq-tokio 0.19 + +### moq-native is moq-tokio (#2896) + +moq-native 0.20.0 is a stub that fails to compile, pointing at the rename. +Names sit under their modules: `connection::Goaway`, `connection::Monitor`, `transport::Session` (#3745). + +```rust +let client = moq_tokio::connect::Config::default().init(quic)?; +client.with_origin(origin); +``` + +### Origins + +`Origin::random().produce()` is `moq_tokio::origin::spawn()`. diff --git a/rs/moq-transcode/MIGRATE.md b/rs/moq-transcode/MIGRATE.md new file mode 100644 index 00000000000..284a4f84fa4 --- /dev/null +++ b/rs/moq-transcode/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-transcode + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-uring/MIGRATE.md b/rs/moq-uring/MIGRATE.md new file mode 100644 index 00000000000..05350df5ed0 --- /dev/null +++ b/rs/moq-uring/MIGRATE.md @@ -0,0 +1,34 @@ +# Migrating moq-uring + +How to update your code for each breaking change, newest first. + +## Unreleased + +### QUIC limits are `moq_quic::Limits` (#5871) + +`quic::Limits` is `moq_quic::Limits`, where each `None` field keeps that limit, and an invalid one fails `set_limits` with `Error::Limits` instead of `WindowRange` or `StreamsRange`. +Start from `quic::Limits::default()` and set only the fields you change. + +### The `noq` feature is `quic` (#5664) + +The QUIC stack is MoQ's own `moq-quic` instead of `moq-noq-proto`. +Rename the `noq` cargo feature to `quic`. +A qlog directory gets one file per endpoint and one per dial instead of one per connection. + +### `CID_LEN` is gone (#5688) + +`quic::endpoint::CID_LEN` is removed; endpoints still issue 8-byte connection IDs, sharded or not. + +### The server config takes a rustls config (#5312) + +`quic::server::Config` drops `identity`, `alpn`, and `client_auth` for one `tls: Arc`, and `Config::new` takes it. +Build it from an identity with `identity.server(&alpns, client_auth)?`, or share a resolver with other listeners. + +### Sessions are wrapped for moq-net + +moq-net no longer takes a `web_transport_trait` session. +Wrap a moq-uring session with `transport::Session::new`, naming how the request target travels. + +```rust +let session = moq_uring::transport::Session::new(connection, moq_net::transport::Binding::Uri); +``` diff --git a/rs/moq-v4l/MIGRATE.md b/rs/moq-v4l/MIGRATE.md new file mode 100644 index 00000000000..f2cf389afb4 --- /dev/null +++ b/rs/moq-v4l/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-v4l + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-video/MIGRATE.md b/rs/moq-video/MIGRATE.md index 3daffb9ddcb..8c1b2a21e36 100644 --- a/rs/moq-video/MIGRATE.md +++ b/rs/moq-video/MIGRATE.md @@ -7,30 +7,50 @@ How to update your code for each breaking change, newest first. ### `Color` is a struct of matrix, range, primaries and transfer The four variants become constants plus a range, and the new `color` module holds `Matrix`, `Range`, `Primaries` and `Transfer`. -`Bt601Limited` is `Color::BT601`, `Bt709Limited` is `Color::BT709`, and the `Full` variants set `range: Range::Full`. -Match on the field you need instead of the variant. +`Bt601Limited` is `Color::BT601`, `Bt709Limited` is `Color::BT709`, and the `Full` variants set `range: Range::Full`, as in `Color { range: Range::Full, ..Color::BT709 }`. +Match on the field you need instead of the variant, such as `color.matrix == Matrix::Bt709`. BT.601 now names its primaries (SMPTE 170M), and BT.2020 with PQ or HLG is representable. -```rust -// Before -let color = Color::Bt709Full; -let hd = matches!(color, Color::Bt709Limited | Color::Bt709Full); -// After -use moq_video::color::{Matrix, Range}; -let color = Color { range: Range::Full, ..Color::BT709 }; -let hd = color.matrix == Matrix::Bt709; -``` - ### `Encoder::encode` refuses a frame in another space Decoded and resized frames now report their source's space from `Surface::color`, and `Encoder::encode` fails with `Error::ColorMismatch` when a frame's space differs from the one the encoder signals, rather than mislabelling it. -Open the encoder with the first frame's space, and reopen it when the space changes. - -```rust -// Before -let encoder = Encoder::new(&encode::Config::new(width, height, rate))?; -// After -let mut config = encode::Config::new(width, height, rate); -config.color = frame.surface.color(); -let encoder = Encoder::new(&config)?; -``` +Set `encode::Config::color` to the first frame's `frame.surface.color()`, and reopen the encoder when the space changes. + +### `encode::Producer` takes a catalog reservation (#5715) + +`encode::Producer::new` and `with_track` take a `moq_mux::catalog::Reserved` instead of the `catalog::Producer`, so the caller declares the encoder's GOP as the timeline hint. +Pass `catalog.reserve().with_timeline_hint(gop)` instead of the catalog. + +### Capture takes one `encode::Capture`, and `Control::new` returns the driver + +`publish_capture(broadcast, catalog, capture, encode, clock)` is `publish_capture(broadcast, catalog, encode::Capture)`. +`Capture` has no clock: the driver stamps on `catalog.clock()`. +`encode::Control::new` returns the control handle and the driver; dropping the last `Control` cancels an in-flight open or encode. +Build it from `encode::Capture::default()`, setting `capture` and `encode`. + +### `Control::cut` returns a `Result` + +`cut()` fails with `Error::CutUnsupported` once the driver knows its backend cannot force a keyframe, and the publish carries on at its cadence. +It counts cadence keyframes too, so a request may be served without forcing one. +On `Error::CutUnsupported`, wait for the next cadence keyframe. + +### `Encoded` marks keyframes (#4295) + +`encode::Encoded::new` takes a third `keyframe: bool`, also readable as `Encoded::keyframe`. + +### Producers drop `tick`, `idle`, and `observe_lag` (#4915) + +`encode::Producer` no longer takes liveness hints, so delete the calls; disable a rendition through the catalog's `enabled` flag instead. + +### External Vulkan and DMA-BUF inputs (#4975) + +On Linux, `Surface::Vulkan` no longer requires the `nvidia` feature. +`vulkan::Slot::new` takes exported handles, an `Image`, and a producer guard, replacing the CUDA-owned producer API. +`vulkan::Importer`, `vulkan::ImportError`, `cuda::Converter`, and `cuda::Slot` leave the public API; `Surface::Cuda` stays for NVDEC output. +`DmaBuf::new` takes an owned FD, a `DmaBufLayout`, and a release guard, and an external DMA-BUF refuses CPU readback. +Bind eager open to an external device with `encode::Config::input`; an unsupported device is refused instead of falling back to software. +Replace `Importer::new` and `import` with `frame::vulkan::Slot::new(frame::vulkan::Handles { memory, timeline }, image, owner)`. + +### Decoder staleness is `max_delay` + +`decode::Options::max_age` is `max_delay`. diff --git a/swift/MIGRATE.md b/swift/MIGRATE.md new file mode 100644 index 00000000000..9227b0b2d5e --- /dev/null +++ b/swift/MIGRATE.md @@ -0,0 +1,177 @@ +# Migrating the Swift Moq package + +How to update your code for each breaking change, newest first. + +## Unreleased + +### Subscriptions carry group ranges (#5732) + +`Subscription.groupStart` and `groupEnd` are replaced by `ranges`, a list of `Subscription.Range` whose ends are a `Subscription.Anchor` (`.absolute(group:frame:)` or `.relative(groups:)`), and an optional `order`. +To start at group `n`, pass one range starting at the absolute anchor for group `n`, frame 0. + +### Receive estimates come from `Session.recvBandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session.recvBandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and drop it to stop. +It returns `nil` when the version has no PROBE. + +### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) + +The first call returns the broadcast serving the path, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + +### A timed track refuses a group that rewinds (#5803) + +Writing a frame fails with `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + +### Bandwidth reserves a track's demand (#5845) + +`Bandwidth.reserve(demand:maxBps:)` takes the track's `demand()` instead of the producer, and no longer throws. + +### Media subscribe and fetch take a catalog rendition (#5834) + +`Media.TrackConfig` and `Media.GroupConfig` replace `name` and `container` with a `Media.Rendition` and follow its broadcast and track themselves. +`Audio.Consumer.subscribe` and `Video.Consumer.subscribe` label the catalog key `id:` instead of `name:`. + +```swift +let config = Media.TrackConfig(rendition: .video(id: id, config: video)) +``` + +### Native codecs have `Audio` and `Video` namespaces (#5694) + +`BroadcastProducer.encodeAudio` / `encodeVideo` are `Audio.Producer` / `Video.Producer` initializers that take a `Media.CatalogProducer`. +The audio track name moves into the encoder output's `track`, and a zero frame duration takes the codec's own frame. +`BroadcastConsumer.decodeAudio` / `decodeVideo` are `Audio.Consumer.subscribe(broadcast:...)` / `Video.Consumer.subscribe(broadcast:...)`. +Producers expose subscriber state only through `demand()`, and a video GOP is `Video.Gop.keyframe(interval:)`. +`Audio.Encoder` / `Video.Encoder` encode without publishing. + +```swift +let video = try Video.Producer(catalog: Media.CatalogProducer(broadcast: broadcast), input: input, output: output) +let frames = try await Video.Consumer.subscribe(broadcast: consumer, id: name, rendition: rendition, output: decoder) +``` + +### Background panics are reported (#5562) + +A panic in a background task throws `MoqError.Task` with the panic message as `reason`, and other objects keep running. + +### Announcements are events with a restart + +`AnnounceConsumer` yields an `AnnounceEvent`: `.start`, `.update`, `.end`, or `.restart`, each carrying an `Announce`. +`AnnounceUpdate` is gone. +A restart means another publisher instance now serves the prefix: request the path again. + +```swift +for try await event in try session.consume.announced(prefix: "live/") { + switch event { + case .start(let announce), .restart(let announce): subscribe(announce.prefix) + default: continue + } +} +``` + +### "Epoch" means only the publisher epoch (#5146) + +`Session.epoch()` is `connects()`, and `AudioProducer.resetEpoch()` is `reanchor()`. + +### `Client` takes its config at init + +The `Client` setters are `Client(...)` arguments, with `tls`, `quic`, `websocket`, and `backoff` records. +`setReconnect(false)` is `once: true`. +A value the native side cannot use throws `MoqError.Config`, including a bad bind address. +`Server` takes its config at init too, and a request takes its origins in `accept(publish:consume:)`, where `nil` inherits the server's origin. +`Session.shutdown()` is `async throws` and drains before returning. + +```swift +let client = try Client(tls: ClientTls(insecure: true), consume: origin) +``` + +### Data tracks wrap a track + +`BroadcastProducer.publishJsonSnapshot` / `publishJsonStream` and the consumer's `subscribeJson*` are gone. +Create the track, then wrap it with `Json` (or `Flate` for opaque bytes). + +```swift +let state = try Json.SnapshotProducer(broadcast: broadcast, track: try broadcast.publishTrack(name: "state")) +``` + +### Media lives in `Media` + +`BroadcastProducer.publishAudio`, `publishVideo`, `publishContainer`, their `on:` and stream variants, `setVideoProperties`, and `setCatalogSection`, and the consumer's `subscribeCatalog`, `subscribeMedia`, and `fetchMediaGroup`, are gone. +Use `Media.TrackProducer.audio` / `video`, `Media.ContainerProducer`, `Media.CatalogProducer`, `Media.CatalogConsumer.subscribe`, `Media.TrackConsumer.subscribe`, and `Media.GroupConsumer.fetch`. +The catalog types (`Catalog`, `Video`, `Audio`, `AudioFormat`, ...) moved under `Media`. +Producers drop `name`, `used`, and `unused`; read them through `demand()`. + +```swift +let audio = try Media.TrackProducer.audio(broadcast: broadcast, initData: Media.AudioInit(format: .opus, data: opusHead)) +``` + +### Container byte-stream importers take an init record + +`Media.ContainerStreamProducer` takes a `Media.ContainerStreamInit` holding the format instead of a bare format. +Its optional `timebase`, also on `Media.ContainerInit`, shares one source's offset across container imports. + +### Timestamps are optional + +Raw `writeFrame(_:timestampUs:)` takes a `UInt64?` defaulting to `nil`, and a frame read from an untimed track has a `nil` `timestampUs`. +A write to a timed track without a timestamp throws `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. +`Media.CatalogProducer.now()` samples the broadcast clock for local-event stamps. +Pass `timestampUs:` on every write to a timed track, and handle a `nil` `timestampUs` on read. + +### Subscriber staleness is `maxDelayUs` + +`Subscription`, `AudioDecoderOutput`, and `VideoDecoderOutput` take `maxDelayUs` instead of `maxAgeUs`. +`TrackInfo.maxAgeUs` keeps its name: it is the publisher's retention. + +### Error payloads are named `reason` + +Cases that carried a positional string (`Transport`, `Internal`, `Connect`, ...) take a `reason:` label. + +### `Transport` gains `.webTransport` + +A WebTransport session reports `.webTransport`; `.quic` now means raw QUIC only, so an exhaustive `switch` needs one more case. +Check for `.webTransport` too where you matched `.quic`. + +### `stalled` is `enabled` (#4915) + +`Media.Video.stalled` is `enabled`, defaulting to true with the meaning inverted, and `Media.Audio` gains `enabled`. +Read `enabled` where you read `!stalled`. + +### Routes have one cost (#4661) + +`Route.cold` is removed; pass only `cost:`. + +### Decoded video frames are objects + +`VideoDecodedFrame` has `timestampUs()`, `width()`, `height()`, `pixels(format:)`, and `surface()` instead of stored fields. +`VideoDecoderOutput.format` is gone; set `surface: true` to keep the decoder's native surface. +Call `try frame.pixels(format: .rgba)` instead of reading `frame.data`. + +### `BroadcastProducer.finish()` is `close()` (#4152) + +Close the broadcast instead; tracks already subscribed carry on to their own end. + +## 0.4 -> 0.5 + +### Publish split by kind + +`publishMedia` is `publishAudio`, `publishVideo`, or `publishContainer`, each taking its own init. +The raw encoder paths that used to be `publishAudio` / `publishVideo` are `encodeAudio` / `encodeVideo`. + +### Renames + +| Before | After | +| --- | --- | +| millisecond durations | microseconds (`maxAgeUs`, `Backoff.initialUs`) | +| `estimatedSendBitrate` / `estimatedRecvBitrate` | `estimatedSendRate` / `estimatedRecvRate` | +| `setTlsDisableVerify(true)` | `setTlsVerify(false)` | +| `Announced` / `Announcement` | `AnnounceConsumer` / `AnnounceUpdate` | +| `BroadcastRequest.abort` | `reject` | +| `OriginOptions` | `OriginConfig` | + +### Errors are structured + +`MoqError.Protocol` carries `details` (scope, wire code, kind) instead of a flattened message. +Setters throw (`.Busy` during connect or listen) instead of dropping the value. +Track and group `finish()` keep the handle open, so a later `abort()` can still run. +Match `MoqError.Protocol(let details)` and read `details.kind` and `details.code`, and call setters with `try`.