From 60efb3184e742ba7e3928fe0f9c8198ef02f1d4e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 22 Oct 2026 19:06:46 -0700 Subject: [PATCH 1/2] chore(quest): claim quest/m1/api-audit/migrate.md Quest-Claim: c8b89b96e56dfc32 From 570fb39b57288f0bfc9b0297dbc5fe1394cc700e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 22 Oct 2026 19:29:31 -0700 Subject: [PATCH 2/2] fix(migrate)!: align guides with released APIs Co-Authored-By: GPT-6 --- cpp/moq/MIGRATE.md | 150 +------------------------------- dart/moq/MIGRATE.md | 20 ++++- dart/moq_ffi/MIGRATE.md | 39 ++------- go/ffi/MIGRATE.md | 39 ++------- go/wrapper/MIGRATE.md | 20 ++++- js/clock/MIGRATE.md | 17 +--- js/hang/MIGRATE.md | 18 +++- js/publish/MIGRATE.md | 4 +- kt/moq-ffi/MIGRATE.md | 32 ++----- kt/moq/MIGRATE.md | 20 ++++- py/moq-ffi/MIGRATE.md | 32 ++----- py/moq-rs/MIGRATE.md | 20 ++++- quest/m1/api-audit/README.md | 1 - quest/m1/api-audit/migrate.md | 57 ------------ quest/m1/ffi-shutdown-sync.md | 1 - rs/hang/MIGRATE.md | 12 ++- rs/moq-archive/MIGRATE.md | 41 ++++----- rs/moq-cli/MIGRATE.md | 9 ++ rs/moq-ffi/MIGRATE.md | 29 +++--- rs/moq-hls/MIGRATE.md | 12 ++- rs/moq-net/MIGRATE.md | 19 +++- rs/moq-transcode/MIGRATE.md | 6 ++ rs/moq-transcode/src/catalog.rs | 6 +- rs/moq-transcode/src/ladder.rs | 53 +++++------ rs/moq-video/MIGRATE.md | 6 ++ swift/MIGRATE.md | 16 +++- swift/ffi/MIGRATE.md | 32 ++----- 27 files changed, 238 insertions(+), 473 deletions(-) delete mode 100644 quest/m1/api-audit/migrate.md diff --git a/cpp/moq/MIGRATE.md b/cpp/moq/MIGRATE.md index e128eb16e37..6ca4ebcf703 100644 --- a/cpp/moq/MIGRATE.md +++ b/cpp/moq/MIGRATE.md @@ -1,150 +1,8 @@ -# Migrating the C++ moq library - -How to update your code for each breaking change, newest first. +# Migrating the C++ MoQ package ## Unreleased -### Priorities are signed (#6142) - -`TrackInfo::priority`, `Subscription::priority`, and `FetchGroupOptions::priority` are `int8_t`, and `GroupRequest::priority()` returns one. -Higher is first and 0 is the unset midpoint; `TrackInfo::priority` defaulted to 127 and now defaults to 0. -To keep a value's moq-lite byte, subtract 128 from it, widening first (`static_cast(int(old) - 128)`): 255 becomes 127, 128 becomes 0, and 0 becomes -128. - -### A client asks only about what it watches (#6165) - -On moq-lite, a client session asks its relay for announcements only under paths something watches, through `announced` or `announced_broadcast`, instead of under its whole scope at connect. -`OriginConsumer::request_broadcast` watches nothing, so on a client it fails with an unroutable error for a path nothing watches, even when the relay announces it. -Resolve the path with `announced_broadcast`, or hold `announced` over its prefix while requesting. -moq-transport and moq-lite-03/04 sessions are unchanged. - -### `moq::ContainerFormat::kFmp4` unwraps to Legacy (#6250) - -A container import with `moq::ContainerFormat::kFmp4` now publishes one Legacy frame per sample instead of one CMAF frame per fragment. -Pass the new `moq::ContainerFormat::kCmaf` to keep publishing CMAF. - -### Catalog references carry their epoch - -`Video` and `Audio` gain `epoch`, the publisher instance their `broadcast` reference names, right after `broadcast`; positional initializers shift by one. -`BroadcastConsumer::resolve(reference)` becomes `resolve(reference, epoch)`; pass the rendition's `epoch` alongside its `broadcast`, or `std::nullopt` to resolve whichever instance answers. -Resolving, and the rendition-based consumers, fail with `moq::Error::kMux` for a rendition whose broadcast is proven to serve another epoch. - -### Timestamps stop at 2^61 - 1 microseconds (#6168) - -Writing a frame, or any other call that takes a timestamp, fails with `TimeOverflow` past `2^61 - 1` microseconds instead of `2^62 - 1`, the most moq-lite-05/06 can carry. -A received timestamp past it fails the group as a malformed track. -Clamp timestamps you build from unbounded input to `2^61 - 1`. - -### Finished tracks stay until the broadcast ends (#6157) - -A finished track now stays listed, with its catalog rendition and demand, until its broadcast closes or the new `BroadcastProducer::finish` lets go. -Publish a replacement under a new name, and expect a finished track's demand watch to end with the broadcast. - -### A claim answers by publishing (#5678) - -`OriginProducer::dynamic(prefix)` takes no route, and `OriginDynamic::update` is gone. -Replace `request->accept(broadcast)` with `request->prepare(epoch)`, which returns a `BroadcastPrepared`: fill its `broadcast()`, then `publish()` announces it at the exact path under that epoch and returns its producer. - -### Anchors can name a time (#6074) - -`Anchor` gains a `Time` case holding `timestamp_us`, the group containing that presentation time, so exhaustive visits need a case for it. -`TrackConsumer::update` now returns a result, failing with `TimestampMismatch` on a time anchor an untimed track can't serve. - -### Rebuild the matching raw bindings - -The documentation update changes UniFFI's audio-producer method metadata checksums. -Regenerate the C++ binding headers and sources from the rebuilt native library, -then rebuild the application; older generated guards reject the rebuilt library. -Application calls are unchanged. - -### JSON consumers return `JsonValue` (#5677) - -`JsonSnapshotConsumer::next()` and `JsonStreamConsumer::next()` return `JsonValue` (`value`, `timestamp_us`) instead of a string; `timestamp_us` is empty when untimed. -A snapshot consumer's `next()` now yields every state in order; call `latest()` for the newest. -Timestamps are rounded down to a whole microsecond. - -### Ranges filter reads (#5929) - -A subscription now reads only the groups its own `ranges` cover. - -### Track demand reports the wanted group (#5930) - -`TrackDemand::is_used`, `used`, and `unused` are gone. -`wanted()` returns `std::nullopt` while nobody subscribes, otherwise the group subscribers want next, and `wanted_changed()` waits for it to change (the first call returns at once). -Loop on `wanted_changed()` until it returns a value where you awaited `used()`, and `std::nullopt` where you awaited `unused()`. -`TrackInfo` gains `refresh`, the ratio past which a joining subscriber asks for a new group. - -### `CongestionControl` gained `kNone` (#5926) - -`moq::CongestionControl::kNone` runs no congestion control and no pacing, for tests and controlled links. -A `switch` without a `default` needs one more case. - -### Container byte streams use `MediaContainerProducer` (#5919) - -`moq::MediaContainerStreamProducer` and `moq::ContainerStreamInit` are gone: a container recovers its own framing, so `moq::MediaContainerProducer` takes chunks split anywhere. -Pass empty `data` and write the stream through it. - -```cpp -auto stream = ok(moq::MediaContainerProducer::init(broadcast, {moq::ContainerFormat::kFmp4, {}})); -``` - -### Key logging is opt-in (#5907) - -`SSLKEYLOGFILE` alone no longer writes TLS session secrets. -Set `QuicConfig::keylog` to `true` as well to capture them; without `SSLKEYLOGFILE` the client or server refuses to build. - -### 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. +### First release -```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)); -``` +There is no published C++ package to migrate from. Start with the current headers and examples, and build the wrapper, generated bindings, and native library together. +The underlying generated API follows the [moq-ffi migration guide](../../rs/moq-ffi/MIGRATE.md). diff --git a/dart/moq/MIGRATE.md b/dart/moq/MIGRATE.md index d466a24a91f..2e08de15b93 100644 --- a/dart/moq/MIGRATE.md +++ b/dart/moq/MIGRATE.md @@ -4,6 +4,20 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Await shutdown and client close + +`Session.shutdown()` is asynchronous and can fail rather than returning immediately. Await it to initiate and finish the drain. `Moq.close()` now returns `Future`; await that future to finish client cleanup. + +### Media track names belong to the target + +Audio and video init records no longer carry a track name. Pass that name on the media producer's named target instead; init records describe only the codec and media. + +### Broadcast finish returns a linger handle + +`BroadcastProducer.finish()` returns a `BroadcastFinish` handle instead of no value. +Keep it and await `wait()` to serve finished tracks until their media is stale. +Use `BroadcastProducer.close()` for an immediate end. + ### JSON snapshot `deltaRatio` is `deltas` `SnapshotConfig.deltaRatio` is replaced by `bool deltas` (default `true`). @@ -189,6 +203,8 @@ final stream = media.ContainerProducer(broadcast: broadcast, init: media.Contain ### Timestamps are optional +An omitted timescale now publishes a raw track untimed instead of selecting microseconds. Explicitly set a microsecond timescale to preserve the released default. + `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. @@ -217,7 +233,3 @@ 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/dart/moq_ffi/MIGRATE.md b/dart/moq_ffi/MIGRATE.md index 8df537cb61c..0f35ab478cc 100644 --- a/dart/moq_ffi/MIGRATE.md +++ b/dart/moq_ffi/MIGRATE.md @@ -1,39 +1,10 @@ # Migrating the Dart moq\_ffi package -How to update your code for each breaking change, newest first. - ## Unreleased -### Priorities are signed (#6142) - -`MoqTrackInfo.priority`, `MoqSubscription.priority`, `MoqFetchGroupOptions.priority`, and `MoqGroupRequest.priority()` take `-128..=127`, higher first, and 0 is the unset midpoint. -`MoqTrackInfo.priority` defaulted to 127 and now defaults to 0. -To keep a value's moq-lite byte, subtract 128 from it: 255 becomes 127, 128 becomes 0, and 0 becomes -128. -Regenerate the bindings for the changed checksums. - -### A client asks only about what it watches (#6165) - -On moq-lite, a client session asks its relay for announcements only under paths something watches, through `announced()` or `announcedBroadcast()`, instead of under its whole scope at connect. -`MoqOriginConsumer.requestBroadcast()` watches nothing, so on a client it fails with an unroutable error for a path nothing watches, even when the relay announces it. -Resolve the path with `announcedBroadcast()`, or hold `announced()` over its prefix while requesting. -moq-transport and moq-lite-03/04 sessions are unchanged. -Regenerate the bindings for the changed `request_broadcast` checksum. - -### Catalog references carry their epoch - -`MoqVideo` and `MoqAudio` gain `epoch`, the publisher instance their `broadcast` reference names. -`MoqBroadcastConsumer.resolve(reference:)` requires `epoch:` too; pass the rendition's `epoch` alongside, or `null` to resolve whichever instance answers. -Resolving, and the rendition-based consumers, fail a rendition whose broadcast is proven to serve another epoch with `MuxMoqException`. - -### Finished tracks stay until the broadcast ends (#6157) - -A finished track, raw or media, now stays listed, with its catalog rendition and demand, until its broadcast closes or the new `MoqBroadcastProducer.finish()` lets go, instead of unlisting when its producer finishes or is released. -Publish a replacement under a new name, and expect a finished track's demand watch to end with the broadcast. -Regenerate the bindings for the new `MoqBroadcastFinish` and the changed checksums. - -### Regenerate the matching bindings +### Follow the core API migration guide -Documentation on `MoqAudioProducer::write` and `finish` changes their UniFFI metadata checksums. -Keep the generated Dart code and native library together. -For a custom native build, regenerate the matching bindings with `just dart generate`. -The method signatures and behavior are unchanged. +These bindings are generated from moq-ffi. Apply every change in the +[moq-ffi migration guide](../../rs/moq-ffi/MIGRATE.md), using this language's generated casing. +Regenerate the bindings and rebuild the matching native library together; method metadata checksums are not a separate API migration. +The [hand-written wrapper](../moq/MIGRATE.md) has its own migration guide. diff --git a/go/ffi/MIGRATE.md b/go/ffi/MIGRATE.md index 5319a4adb04..b1665d13984 100644 --- a/go/ffi/MIGRATE.md +++ b/go/ffi/MIGRATE.md @@ -1,39 +1,10 @@ # Migrating the Go moq-ffi module -How to update your code for each breaking change, newest first. - ## Unreleased -### Priorities are signed (#6142) - -`MoqTrackInfo.Priority`, `MoqSubscription.Priority`, `MoqFetchGroupOptions.Priority`, and `MoqGroupRequest.Priority()` are `int8`, higher first, and 0 is the unset midpoint. -`MoqTrackInfo.Priority` defaulted to 127 and now defaults to 0. -To keep a value's moq-lite byte, subtract 128 from it: 255 becomes 127, 128 becomes 0, and 0 becomes -128. -Regenerate the bindings for the changed checksums. - -### A client asks only about what it watches (#6165) - -On moq-lite, a client session asks its relay for announcements only under paths something watches, through `Announced()` or `AnnouncedBroadcast()`, instead of under its whole scope at connect. -`MoqOriginConsumer.RequestBroadcast()` watches nothing, so on a client it fails with an unroutable error for a path nothing watches, even when the relay announces it. -Resolve the path with `AnnouncedBroadcast()`, or hold `Announced()` over its prefix while requesting. -moq-transport and moq-lite-03/04 sessions are unchanged. -Regenerate the bindings for the changed `request_broadcast` checksum. - -### Catalog references carry their epoch - -`MoqVideo` and `MoqAudio` gain `Epoch *string`, the publisher instance their `Broadcast` reference names. -`MoqBroadcastConsumer.Resolve` takes the rendition's `epoch` after `reference`; pass it alongside, or `nil` to resolve whichever instance answers. -Resolving, and the rendition-based consumers, fail a rendition whose broadcast is proven to serve another epoch with `Mux`. -Regenerate the bindings for the changed records and checksums. - -### Finished tracks stay until the broadcast ends (#6157) - -A finished track, raw or media, now stays listed, with its catalog rendition and demand, until its broadcast closes or the new `MoqBroadcastProducer.Finish()` lets go, instead of unlisting when its producer finishes or is released. -Publish a replacement under a new name, and expect a finished track's demand watch to end with the broadcast. -Regenerate the bindings for the new `MoqBroadcastFinish` and the changed checksums. - -### Regenerate the matching bindings +### Follow the core API migration guide -Documentation on `MoqAudioProducer::write` and `finish` changes their UniFFI metadata checksums. -Regenerate the Go sources and bundle the matching static libraries, then rebuild callers. -The method signatures and behavior are unchanged. +These bindings are generated from moq-ffi. Apply every change in the +[moq-ffi migration guide](../../rs/moq-ffi/MIGRATE.md), using this language's generated casing. +Regenerate the bindings and rebuild the matching native library together; method metadata checksums are not a separate API migration. +The [hand-written wrapper](../wrapper/MIGRATE.md) has its own migration guide. diff --git a/go/wrapper/MIGRATE.md b/go/wrapper/MIGRATE.md index 765675dbae7..c5499c9dc71 100644 --- a/go/wrapper/MIGRATE.md +++ b/go/wrapper/MIGRATE.md @@ -4,6 +4,20 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Shutdown takes a context and returns an error + +`Session.Shutdown()` is `Shutdown(ctx) error`. Pass the drain context and handle the returned error. `Client.Close()` now waits for the session drain rather than only requesting it; allow for that wait in cleanup. + +### Media track names belong to the target + +Audio and video init records no longer carry a track name. Pass that name on the media producer's named target instead; init records describe only the codec and media. + +### Broadcast finish returns a linger handle + +`BroadcastProducer.Finish()` returns a `BroadcastFinish` handle instead of no value. +Keep it and await `Wait(ctx)` to serve finished tracks until their media is stale. +Use `BroadcastProducer.Close()` for an immediate end. + ### JSON snapshot `DeltaRatio` is `Deltas` `json.SnapshotOptions.DeltaRatio *uint32` and `json.DefaultDeltaRatio` are replaced by `Deltas *bool` (nil enables deltas). @@ -202,6 +216,8 @@ A frame timestamp is a `*time.Duration`, `nil` on a frame read from an untimed t ### Timestamps are optional +An omitted timescale now publishes a raw track untimed instead of selecting microseconds. Explicitly set a microsecond timescale to preserve the released default. + 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. @@ -235,10 +251,6 @@ Read `Enabled` where you read `!Stalled`. `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` diff --git a/js/clock/MIGRATE.md b/js/clock/MIGRATE.md index 0c1407fd44c..207f0b2da1e 100644 --- a/js/clock/MIGRATE.md +++ b/js/clock/MIGRATE.md @@ -1,19 +1,8 @@ # Migrating @moq/clock -How to update your code for each breaking change, newest first. - ## Unreleased -### Manual time steps through timer deadlines - -Rename `await clock.advanceAsync(duration)` to `await clock.advance(duration)`. -The async method now visits every due deadline and drains tasks before continuing, -so timers that re-arm during the advance can fire again before its target. - -Rename synchronous `clock.advance(duration)` calls to `clock.jump(duration)`. -Use `jump` to model a stalled event loop, firing due timers late at the target; -use awaited `advance` for normal time progression. There are no compatibility aliases. +### First release -Drive each `Manual` serially. Until `advance` or `fire` settles, another -`advance` or `fire` rejects and `jump` throws without moving time. Timer callbacks -may arm timers, but must leave driving that clock to their caller. +The released repository contained a private clock example, not this published library. +Use the current clock API directly; intermediate `Manual` method renames do not describe a published upgrade path. diff --git a/js/hang/MIGRATE.md b/js/hang/MIGRATE.md index bce652a82da..1382f9c7f38 100644 --- a/js/hang/MIGRATE.md +++ b/js/hang/MIGRATE.md @@ -4,6 +4,18 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Timeline records describe one track + +`Timeline.Range` is replaced by `Position { group, frame? }`; a record has inclusive `start` and exclusive `end` positions for its own track instead of a `tracks` map of group ranges. +`Record.segment` is `sequence`, and `pts` / `duration` are absent for untimed records. +`Timeline.DEFAULT_NAME` is replaced by `SUFFIX`; name each media track's timeline separately and advertise it in `archive.timelines`. + +### Container reads are a tagged union + +`Container.Consumer.next()` returns a `Container.Read` union instead of an object with optional `frame` and continuity counters. +Switch on `kind`: `frame` carries `frame` and `group`, `gap` carries `timestamp` and `duration`, and `end` carries `group` and `end`. +Reset decoder state on `gap`, and process `end` as the declared media end rather than testing `continuous` or `discontinuity`. + ### CMAF decoding is a separate module Replace `Container.Cmaf` with `import * as Cmaf from "@moq/hang/cmaf"`. @@ -23,10 +35,8 @@ 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). +`archive.track` becomes `archive.timelines`, keyed by each media track name. Each value is `{ track, duration }`, naming that media track's timeline and nominal duration, absent when untimed. +Replace the shared `durationMax` with each track's `duration`. The archive version is 3 rather than 1; update publishers and consumers together. ### Timeline records are optionally timed (#5720) diff --git a/js/publish/MIGRATE.md b/js/publish/MIGRATE.md index def74f4eec0..03842e2d2e8 100644 --- a/js/publish/MIGRATE.md +++ b/js/publish/MIGRATE.md @@ -9,10 +9,8 @@ How to update your code for each breaking change, newest first. Code that builds a complete `BroadcastInput` adds `linger`, such as `new Signal(false)` to keep turning `announce` off unannouncing at once. `new Broadcast({ ... })` defaults it to `false`, so constructor calls need no change. -### `Encoder.cut()` is not rate limited, and video tracks advertise refresh (#6048) +### Video tracks advertise refresh (#6048) -`Encoder.cut()` no longer spaces keyframes 500 ms apart: every call forces one on the next frame, so frequent calls produce all-keyframe video. -Debounce your own calls if you relied on the spacing. Every `Broadcast.video()` track now advertises a refresh ratio of 2, so a producer registered there must open a group with a keyframe whenever `track.demand().wanted` passes the largest group. The bundled `Encoder` already does; a custom producer has to as well. diff --git a/kt/moq-ffi/MIGRATE.md b/kt/moq-ffi/MIGRATE.md index a87e06daea2..adf9c765ad6 100644 --- a/kt/moq-ffi/MIGRATE.md +++ b/kt/moq-ffi/MIGRATE.md @@ -1,32 +1,10 @@ # Migrating the Kotlin moq-ffi package -How to update your code for each breaking change, newest first. - ## Unreleased -### A client asks only about what it watches (#6165) - -On moq-lite, a client session asks its relay for announcements only under paths something watches, through `announced()` or `announcedBroadcast()`, instead of under its whole scope at connect. -`MoqOriginConsumer.requestBroadcast()` watches nothing, so on a client it fails with an unroutable error for a path nothing watches, even when the relay announces it. -Resolve the path with `announcedBroadcast()`, or hold `announced()` over its prefix while requesting. -moq-transport and moq-lite-03/04 sessions are unchanged. -Regenerate the bindings for the changed `request_broadcast` checksum. - -### Catalog references carry their epoch - -`MoqVideo` and `MoqAudio` gain `epoch`, the publisher instance their `broadcast` reference names; it defaults to `null`, so only positional construction changes. -`MoqBroadcastConsumer.resolve` takes the rendition's `epoch` after `reference`; pass it alongside, or `null` to resolve whichever instance answers. -Resolving, and the rendition-based consumers, throw `MoqException.Mux` for a rendition whose broadcast is proven to serve another epoch. -Regenerate the bindings for the changed records and checksums. - -### Finished tracks stay until the broadcast ends (#6157) - -A finished track, raw or media, now stays listed, with its catalog rendition and demand, until its broadcast closes or the new `MoqBroadcastProducer.finish()` lets go, instead of unlisting when its producer finishes or is released. -Publish a replacement under a new name, and expect a finished track's demand watch to end with the broadcast. -Regenerate the bindings for the new `MoqBroadcastFinish` and the changed checksums. - -### Regenerate the matching bindings +### Follow the core API migration guide -Documentation on `MoqAudioProducer::write` and `finish` changes their UniFFI metadata checksums. -Regenerate the Kotlin sources and package matching native libraries, then rebuild callers. -The method signatures and behavior are unchanged. +These bindings are generated from moq-ffi. Apply every change in the +[moq-ffi migration guide](../../rs/moq-ffi/MIGRATE.md), using this language's generated casing. +Regenerate the bindings and rebuild the matching native library together; method metadata checksums are not a separate API migration. +The [hand-written wrapper](../moq/MIGRATE.md) has its own migration guide. diff --git a/kt/moq/MIGRATE.md b/kt/moq/MIGRATE.md index 6e1db97c770..3d66e2f6e14 100644 --- a/kt/moq/MIGRATE.md +++ b/kt/moq/MIGRATE.md @@ -4,6 +4,20 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Shutdown suspends until drained + +`Session.shutdown()` is now a suspending operation that can throw, rather than a synchronous request. Call it from a coroutine and handle failure. + +### Media track names belong to the target + +Audio and video init records no longer carry a track name. Pass that name on the media producer's named target instead; init records describe only the codec and media. + +### Broadcast finish returns a linger handle + +`BroadcastProducer.finish()` returns a `BroadcastFinish` handle instead of no value. +Keep it and await `wait()` to serve finished tracks until their media is stale. +Use `BroadcastProducer.end()` for an immediate end; `close()` releases the binding object. + ### JSON snapshot `deltaRatio` is `deltas` `SnapshotConfig.deltaRatio: UInt` is replaced by `deltas: Boolean` (default `true`). @@ -201,6 +215,8 @@ val stream = ContainerProducer(broadcast, ContainerInit(ContainerFormat.FMP4, by ### Timestamps are optional +An omitted timescale now publishes a raw track untimed instead of selecting microseconds. Explicitly set a microsecond timescale to preserve the released default. + `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. @@ -236,10 +252,6 @@ Replace `!video.stalled` with `video.enabled`. `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` diff --git a/py/moq-ffi/MIGRATE.md b/py/moq-ffi/MIGRATE.md index 3390d71fffd..677af200662 100644 --- a/py/moq-ffi/MIGRATE.md +++ b/py/moq-ffi/MIGRATE.md @@ -1,32 +1,10 @@ # Migrating the Python moq-ffi package -How to update your code for each breaking change, newest first. - ## Unreleased -### A client asks only about what it watches (#6165) - -On moq-lite, a client session asks its relay for announcements only under paths something watches, through `announced()` or `announced_broadcast()`, instead of under its whole scope at connect. -`MoqOriginConsumer.request_broadcast()` watches nothing, so on a client it fails with an unroutable error for a path nothing watches, even when the relay announces it. -Resolve the path with `announced_broadcast()`, or hold `announced()` over its prefix while requesting. -moq-transport and moq-lite-03/04 sessions are unchanged. -Regenerate the bindings for the changed `request_broadcast` checksum. - -### Catalog references carry their epoch - -`MoqVideo` and `MoqAudio` gain `epoch`, the publisher instance their `broadcast` reference names; it defaults to `None`, so only positional construction changes. -`MoqBroadcastConsumer.resolve` takes the rendition's `epoch` after `reference`; pass it alongside, or `None` to resolve whichever instance answers. -Resolving, and the rendition-based consumers, raise `MoqError.Mux` for a rendition whose broadcast is proven to serve another epoch. -Rebuild or reinstall the complete wheel so its generated Python code and native library match. - -### Finished tracks stay until the broadcast ends (#6157) - -A finished track, raw or media, now stays listed, with its catalog rendition and demand, until its broadcast closes or the new `MoqBroadcastProducer.finish()` lets go, instead of unlisting when its producer finishes or is released. -Publish a replacement under a new name, and expect a finished track's demand watch to end with the broadcast. -Regenerate the bindings for the new `MoqBroadcastFinish` and the changed checksums. - -### Regenerate the matching bindings +### Follow the core API migration guide -Documentation on `MoqAudioProducer::write` and `finish` changes their UniFFI metadata checksums. -Rebuild or reinstall the complete wheel so its generated Python code and native library match. -The method signatures and behavior are unchanged. +These bindings are generated from moq-ffi. Apply every change in the +[moq-ffi migration guide](../../rs/moq-ffi/MIGRATE.md), using this language's generated casing. +Regenerate the bindings and rebuild the matching native library together; method metadata checksums are not a separate API migration. +The [hand-written wrapper](../moq-rs/MIGRATE.md) has its own migration guide. diff --git a/py/moq-rs/MIGRATE.md b/py/moq-rs/MIGRATE.md index b9a85594a45..18e6a75a9f0 100644 --- a/py/moq-rs/MIGRATE.md +++ b/py/moq-rs/MIGRATE.md @@ -4,6 +4,20 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Await session shutdown + +`Session.shutdown()` changes from a synchronous call to an async operation. Use `await session.shutdown()` and handle its error; an un-awaited call does not initiate shutdown. + +### Media track names belong to the target + +Audio and video init records no longer carry a track name. Pass that name on the media producer's named target instead; init records describe only the codec and media. + +### Broadcast finish returns a linger handle + +`BroadcastProducer.finish()` returns a `BroadcastFinish` handle instead of no value. +Keep it and await `wait()` to serve finished tracks until their media is stale. +Use `BroadcastProducer.close()` for an immediate end. + ### JSON snapshot `delta_ratio` is `deltas` `SnapshotProducer(delta_ratio=...)` is replaced by `deltas: bool = True`. @@ -197,6 +211,8 @@ A string passed where a list of strings belongs (`tls_roots="ca.pem"`) raises `T ### Timestamps are optional +An omitted timescale now publishes a raw track untimed instead of selecting microseconds. Explicitly set a microsecond timescale to preserve the released default. + 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. @@ -230,10 +246,6 @@ A `VideoDecodedFrame`'s fields are methods: `timestamp_us()`, `width()`, `height `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 diff --git a/quest/m1/api-audit/README.md b/quest/m1/api-audit/README.md index a223e5ad187..8dddb3f7f4a 100644 --- a/quest/m1/api-audit/README.md +++ b/quest/m1/api-audit/README.md @@ -68,7 +68,6 @@ Rejected in planning: ## Required -- [MIGRATE matches release](/quest/m1/api-audit/migrate.md) - every break already on main has a correct entry measured against release, and entries for unreleased packages are gone - [Two broadcast resolutions](/quest/m1/api-audit/resolve.md) - Rust, JS, and the bindings resolve a broadcast with `request(path, epoch)` or `follow(path, epoch)`, and every racy two-step caller is gone - [JS prepared answers](/quest/m1/api-audit/js-prepared.md) - a leaked or abandoned `Prepared` refuses its request instead of blocking the path - [The origin announces](/quest/m1/api-audit/announce-owner.md) - a broadcast producer cannot announce; the origin hands out the announcement, so `Prepared` and standalone broadcasts lose methods that always fail diff --git a/quest/m1/api-audit/migrate.md b/quest/m1/api-audit/migrate.md deleted file mode 100644 index e35e053117b..00000000000 --- a/quest/m1/api-audit/migrate.md +++ /dev/null @@ -1,57 +0,0 @@ -# [S] MIGRATE matches release - -## Goal - -Every break already on `main` has a `MIGRATE.md` entry under `## Unreleased` -that is correct against `release`, not against an intermediate `main`. -Entries that describe APIs that never shipped are rewritten or deleted, and -packages that were never published carry no migration entries. - -## Plan - -Decided 2026-10-22 (API audit). Known gaps, verify each against -`git show origin/release:`: - -- Bindings: `MoqSession::shutdown` went from sync `()` to async `Result` - (7bbac32721); only `swift/MIGRATE.md` says so. Add it to `rs/moq-ffi`, - `py/moq-rs` (an un-awaited call is now a silent no-op), `go/wrapper` - (`Shutdown(ctx) error`, and `Client.Close()` now blocks while draining), - `kt/moq`, `dart/moq` (`Moq.close()` returns `Future`). -- Bindings: merge the contradictory `finish` entries ("removed (#4152)" and - "the new `finish` lets go (#6157)") into one: `finish()` now returns a - linger handle; call `close()` for the old behavior. -- Bindings: `rs/moq-ffi/MIGRATE.md` says a raw track is timed by default, - but `MoqTrackInfo.timescale` defaults to `None` (untimed) while release - defaulted to microseconds; the net change is timed to untimed by default. -- Bindings: `MoqAudioInit.track` and `MoqVideoInit.track` moved to - `MoqMediaTarget::Named`, with no entry. -- `cpp/moq/MIGRATE.md`: about 130 lines for a package never released; empty - it. The generated-binding MIGRATEs (`go/ffi`, `py/moq-ffi`, `kt/moq-ffi`, - `swift/ffi`, `dart/moq_ffi`) list 4 of about 35 breaks plus checksum noise; - point them at `rs/moq-ffi/MIGRATE.md` instead. -- `rs/moq-net`: `finish_at(Position)`, `final_sequence` renamed `end()`, - `finished()` returning `Position`, and the transport error remapping - (`IdleTimeout`, `StreamError::from(Transport)`) have no entries. -- `rs/hang`: the #5715 entries describe a `timelines` shape that never - shipped; #4034 (`Range` to `Position`, `segment` to `sequence`, - `DEFAULT_NAME` to `SUFFIX`) and JS #5288 (`Container.Consumer.next()` - returns the `Read` union) have none. -- `rs/moq-archive`, `rs/moq-hls`: drop entries for unreleased intermediates - (`Writer`, `Info::epoch`, `Config::timebase`); add `Key::Groups`, - `Error::Bounds` to `Span`, `Rendition::segment(tag, n)`, and the segment URL - change to `seg/{reference}.{n}.m4s`. -- `rs/moq-video`: `frame::macos` to `apple`, `vulkan::Channels` to `Format`, - `Image` accessors becoming fields, `Handles::new` removed. -- `rs/moq-transcode`: `Rung::height` became `Rung::size`, meaning the short - side. Rename the field `short_side` in the same PR so the entry names the - final shape; moq.pro's `rs/transcode/src/ladder.rs` reads `.height`. -- `rs/moq-cli`: `--linger` worked on fmp4, mkv, flv, h264, and h265 on - release and now refuses a nonzero value; iroh left the default features - (#5581). -- `js/publish`: the "`Encoder.cut()` is not rate limited" entry describes an - API added after release (#4184); `cut()` is additive. -- `js/clock`: was private on release, so its rename entries become a - first-release note. - -Docs only, apart from the `short_side` rename. Other children write their own -entries. diff --git a/quest/m1/ffi-shutdown-sync.md b/quest/m1/ffi-shutdown-sync.md index 5671aa784a6..cb88523cf69 100644 --- a/quest/m1/ffi-shutdown-sync.md +++ b/quest/m1/ffi-shutdown-sync.md @@ -38,4 +38,3 @@ Public API: breaking in moq-ffi and every binding. Wire: none. ## Related - [FFI shutdown backport](/quest/m0/ffi-shutdown-backport.md) - the release line's async `drain()` -- [MIGRATE matches release](/quest/m1/api-audit/migrate.md) - plans entries saying `shutdown` became an async `Result`; they must describe the final sync shape instead diff --git a/rs/hang/MIGRATE.md b/rs/hang/MIGRATE.md index 33cd6fd87b4..b20ac85f61e 100644 --- a/rs/hang/MIGRATE.md +++ b/rs/hang/MIGRATE.md @@ -4,6 +4,12 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Timeline records describe one track + +`timeline::Range` is replaced by `Position { group, frame }`. A `Record` has `start` and exclusive `end` positions for its own track instead of a `tracks` map of inclusive group ranges. +`Record::segment` is `sequence`, and `Record::new` takes the start and end positions. `pts` and `duration` are optional for untimed records. +`timeline::DEFAULT_NAME` is replaced by `SUFFIX`; construct a per-track name with `timeline::default_name(track)`. + ### Priorities are signed (#6142) `catalog::Priorities` fields and `container::track_info`'s priority are `i8`, higher first, with 0 unset. @@ -35,9 +41,9 @@ 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`. +`Archive::track` becomes `Archive::timelines`, keyed by each media track name. Each value is `Timeline { track, duration }`, naming that media track's timeline and nominal duration, `None` when untimed. +Replace `Archive::new(track)` with `Archive::new()` and insert each `Timeline::new(track, duration)`; `duration_max` is removed. +The archive version is 3 rather than 1. Older catalog shapes are not read as this archive, so update publishers and consumers together. ### Timeline records are optionally timed (#5720) diff --git a/rs/moq-archive/MIGRATE.md b/rs/moq-archive/MIGRATE.md index c08212303ca..6983857afa4 100644 --- a/rs/moq-archive/MIGRATE.md +++ b/rs/moq-archive/MIGRATE.md @@ -4,27 +4,20 @@ How to update your code for each breaking change, newest first. ## Unreleased -### Recording format version 3 stores a signed priority (#6142) - -`Info::priority`, `Info::new`, and `Error::Priority` use `i8`, higher first, with 0 unset. -`VERSION` is 3, whose `.info` stores that `i8` as written; version 2 stored the moq-lite byte, `priority + 128`. -Readers still read version 2 recordings, converting the byte, so a stored 127 reads as -1; a decoded version 2 `Info` keeps `version: 2` and refuses to `encode`. -A `Producer` refuses to resume a version 2 recording with `Error::Version(2)`, so start a new prefix. - -### `Producer` replaces `Writer` (#6160) - -`Writer`, its `Control`, and `writer::{Config, Retention}` are removed; record with `Producer::new(store, broadcast, producer::Config::default()).run().await` instead. -A producer records every track of one broadcast, found with TRACKS rather than a catalog, and needs no timeline: the layout is per-track objects keyed by group, frame, and timestamp, documented in the crate docs. -Run one producer per broadcast under a key prefix you pick, such as `/@`; it stores no epoch, so `Info::epoch` and `Info::with_epoch` are removed. -`writer::Retention` becomes `producer::Dvr`, set with `Config::with_dvr`, and works on untimed tracks too. -`Error::{EpochMismatch, Enrolled, Untimed, Timeline, Closed}` are removed; a failed source listing is `Error::Source`. -The new layout is not readable by `Reader`, which still replays timeline recordings. - -### 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. -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`. +### Recording format version 3 replaces version 1 + +Release wrote version 1; the current codecs refuse it. Start a new recording prefix and retain an older reader for existing version 1 recordings. +`Info::priority` and `Info::new` take signed `i8` priority, with 0 unset, rather than the moq-lite byte. Subtract 128 from an old byte to preserve its wire priority. +`Info::timescale` is optional, and `Info::new` takes `Some(timescale)` for timed tracks or `None` for untimed tracks. + +### Recording is driven by a broadcast producer + +`Producer::new(store, broadcast, producer::Config::default()).run().await` records one broadcast, discovering its tracks with TRACKS instead of requiring a catalog or timeline. +Run one producer per broadcast under a new prefix such as `/@`; configure retention with `producer::Dvr` and `Config::with_dvr`. +The current `Reader` still replays timeline recordings, not this producer's new per-track layout; it does not provide an upgrade reader for new recordings. + +### Objects are keyed by timeline segment + +`Key::Groups`, `Key::groups`, and the `Store` group-range operations are removed. Use `Key::Segments` / `Key::segments` and `Store::get_segments` for timeline record sequences. +`Error::Bounds { smallest, largest }` is `Error::Span`; update exhaustive matches. +`segment::Frame::timestamp` is optional. Pass the track's timing to `Object::decode(buf, timed)` and `Store::get_segments(track, segment, timed)`; a timing mismatch is `Error::Timing`. diff --git a/rs/moq-cli/MIGRATE.md b/rs/moq-cli/MIGRATE.md index 215b3a099ff..936ba38cf91 100644 --- a/rs/moq-cli/MIGRATE.md +++ b/rs/moq-cli/MIGRATE.md @@ -4,6 +4,15 @@ How to update your invocation and config for each breaking change, newest first. ## Unreleased +### Iroh is opt-in + +The default source-build features no longer include `iroh`. Add `--features iroh` when building a binary that must use Iroh endpoints. + +### Non-restartable exports refuse linger + +`export fmp4`, `mkv`, `flv`, `h264`, and `h265` refuse a nonzero `--linger` that release accepted. +Drop the option or pass `--linger 0s`; use `export ts` or `export srt` when a restart must be marked in the output. + ### `import fmp4` and `import hls` unwrap to Legacy (#6250) Both now publish one `legacy` frame per sample instead of one `cmaf` frame per fragment. diff --git a/rs/moq-ffi/MIGRATE.md b/rs/moq-ffi/MIGRATE.md index 8ed40b7a0a6..2b55c9f2b1b 100644 --- a/rs/moq-ffi/MIGRATE.md +++ b/rs/moq-ffi/MIGRATE.md @@ -5,6 +5,21 @@ These are the UniFFI names; each language wrapper has its own `MIGRATE.md` in it ## Unreleased +### Session shutdown is asynchronous + +`MoqSession::shutdown()` changes from a synchronous request returning `()` to an async operation returning `Result<(), MoqError>`. Await it to initiate and complete the graceful drain, and handle failure. Calling it without polling the returned future does not request shutdown. + +### Media track naming moves out of init + +`MoqAudioInit::track` and `MoqVideoInit::track` are removed. +Pass that name through `MoqMediaTarget::Named { name }` when constructing `MoqMediaTrackProducer::audio` or `video`; init records describe the codec and media only. + +### Broadcast `finish` returns a linger handle + +`MoqBroadcastProducer::finish()` returns `MoqBroadcastFinish` instead of `()`. +Keep that handle and await `wait()` to serve finished tracks until their media is stale, or close the handle to end that service. +Call `MoqBroadcastProducer::close()` for an immediate end. + ### JSON snapshot `delta_ratio` is `deltas` `MoqJsonSnapshotConfig::delta_ratio` is replaced by `deltas: bool` (default `true`). @@ -61,13 +76,6 @@ It defaults to `false` in every generated binding; a Rust struct literal adds `c `MoqAnchor` gains `Time { timestamp_us }`, the group containing that presentation time, so exhaustive matches need an arm for it. `MoqTrackConsumer::update` now returns `Result<(), MoqError>`, failing with `TimestampMismatch` on a time anchor an untimed track can't serve. -### Regenerate bindings after the API documentation update - -The new documentation on `MoqAudioProducer::write` and `finish` changes their -UniFFI metadata checksums, even though their signatures and behavior are unchanged. -Regenerate each language's bindings from the rebuilt library and update them together. -Older generated bindings with active checksum guards reject this library during initialization. - ### JSON consumers return `MoqJsonValue` (#5677) `MoqJsonSnapshotConsumer::next` and `MoqJsonStreamConsumer::next` return `MoqJsonValue { value, timestamp_us }` instead of a string; `timestamp_us` is null when untimed. @@ -226,7 +234,8 @@ The Display text is unchanged. `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. +A raw track you publish is now untimed by default: a null `timescale` no longer selects microseconds. +Set `timescale: Some(1_000_000)` to keep the released default, and provide a timestamp on each timed write. 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`. @@ -255,10 +264,6 @@ An exhaustive match needs one more arm, and a `Quic` check that meant any QUIC s `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. diff --git a/rs/moq-hls/MIGRATE.md b/rs/moq-hls/MIGRATE.md index 93bf229f475..320d3ccf3f6 100644 --- a/rs/moq-hls/MIGRATE.md +++ b/rs/moq-hls/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Segment requests carry a timeline reference + +`export::Rendition::segment(n)` is `segment(tag, n)`. Use the tag in the playlist segment URI instead of choosing one locally. +Segment URLs change from `seg/{n}.m4s` to `seg/{tag}.{n}.m4s`, so route the tag and sequence separately to `segment`. + ### HLS import unwraps fMP4 to Legacy (#6250) `import::Import` now publishes one `Legacy` frame per sample instead of one `Cmaf` frame per fragment. @@ -35,13 +40,6 @@ match rendition.media_playlist(query) { } ``` -### Imports always anchor their own timebase (#5967) - -`import::Config::timebase` and `Error::ForeignTimebase` are removed. -Drop the field from `Config`, and any match arm on `ForeignTimebase`. -Each import mints its timebase from the catalog it writes, and its first fragment anchors it. -A placed timebase only made redundant importers share timestamps; each importer now publishes its own epoch instead. - ### A rendition waits for its own timeline (#6009) `export::Broadcaster::rendition` and the `renditions()` cursor's `Event::Added` see a rendition only once the catalog advertises its own timeline, not as soon as its init is known. diff --git a/rs/moq-net/MIGRATE.md b/rs/moq-net/MIGRATE.md index e16d361e179..d3a488c646b 100644 --- a/rs/moq-net/MIGRATE.md +++ b/rs/moq-net/MIGRATE.md @@ -4,6 +4,17 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Transport failures keep their meaning + +Handle `Error::IdleTimeout` separately from `Error::Transport`. A local transport close is session cancellation, not clean end-of-content. +Converting a transport failure to `StreamError` now gives `Unroutable` rather than `Internal`, so retry or re-resolve that route where you classified stream errors. + +### Track ends use positions + +`track::Producer::finish_at` takes a `track::Position` instead of an exclusive group sequence. +Pass `Position::group(n)` where you passed `n`, or a position within the last group to end after a frame. +`Producer::final_sequence()` is `end()` and returns `Option`; `Subscriber::finished()` and the ordered reader return `Position` instead of `u64`. + ### IETF whole-group FETCH answers name the requested group ([#6199](https://github.com/moq-dev/moq/issues/6199)) On moq-transport drafts 14 through 19, a whole-group `FETCH_OK` now names `{g, 0}` instead of `{g+1, 0}`. @@ -235,7 +246,7 @@ A track received over moq-lite-05/06 arrives untimed, since its timestamps may b One received over moq-transport keeps its timestamps unchecked, and a relay serves it to moq-lite untimed. A publisher whose timeline restarts, such as an HLS `EXT-X-DISCONTINUITY` that resets decode times, publishes a new broadcast instead. -### `broadcast::Producer::finish` is back, and lingers (#5795) +### Broadcast `finish` returns a linger handle (#5795) `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. @@ -416,10 +427,10 @@ let client = Client::new().with_limits(session::Limits { subscriptions: 100_000, 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) +### Broadcast `abort` is 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 `()`. +`broadcast::Producer::abort` is removed; use `close()` for an immediate end without a cause. +`broadcast::Consumer::is_finished` is gone, and `closed()` on `Consumer` and `Dynamic` returns `()`; the broadcast end carries no cause. ### No `VarInt` diff --git a/rs/moq-transcode/MIGRATE.md b/rs/moq-transcode/MIGRATE.md index 60ed04c6019..41fab2d4966 100644 --- a/rs/moq-transcode/MIGRATE.md +++ b/rs/moq-transcode/MIGRATE.md @@ -4,6 +4,12 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Ladder resolution names its short side + +`Rung::height` is `short_side`, and `Rung::new` takes the shorter coded dimension, not always the height. +The scalar fields in `ladder::Error::Empty` and `Unordered` are `short_side` and `below_short_side`. +For landscape output the value is unchanged; for portrait output it is the width, and the other dimension follows the source aspect ratio. + ### OpenH264 is opt-in (#5771) The default features drop `openh264`, so a GPU-less host has no H.264 encoder or decoder. diff --git a/rs/moq-transcode/src/catalog.rs b/rs/moq-transcode/src/catalog.rs index f21c0f5d6e9..67c84cdef35 100644 --- a/rs/moq-transcode/src/catalog.rs +++ b/rs/moq-transcode/src/catalog.rs @@ -288,7 +288,7 @@ pub(crate) fn resolve_rungs(ladder: &Ladder, source_name: &str, source: &VideoCo let mut resolved: Vec = Vec::new(); for rung in ladder.rungs() { - let side = u64::from(rung.size); + let side = u64::from(rung.short_side); let source_side = source_width.min(source_height); if side > source_side { continue; @@ -307,8 +307,8 @@ pub(crate) fn resolve_rungs(ladder: &Ladder, source_name: &str, source: &VideoCo continue; } resolved.push(Resolved { - name: format!("video/{}p", rung.size), - short_side: rung.size, + name: format!("video/{}p", rung.short_side), + short_side: rung.short_side, size: moq_video::Size::new(width as u32, height as u32), bitrate: rung.bitrate, framerate, diff --git a/rs/moq-transcode/src/ladder.rs b/rs/moq-transcode/src/ladder.rs index 78efba1d38b..82dc9c39e84 100644 --- a/rs/moq-transcode/src/ladder.rs +++ b/rs/moq-transcode/src/ladder.rs @@ -9,7 +9,7 @@ pub struct Rung { /// Output short side in pixels. Rounded down to even when it enters a [`Ladder`] /// (I420 chroma is 2x2). - pub size: u32, + pub short_side: u32, /// The configured maximum in bits per second: the CBR target advertised in the /// derivative catalog. @@ -17,9 +17,9 @@ pub struct Rung { } impl Rung { - /// A rung at `size` pixels and `bitrate` bits per second. - pub fn new(size: u32, bitrate: moq_net::bandwidth::Rate) -> Self { - Self { size, bitrate } + /// A rung at `short_side` pixels and `bitrate` bits per second. + pub fn new(short_side: u32, bitrate: moq_net::bandwidth::Rate) -> Self { + Self { short_side, bitrate } } } @@ -29,10 +29,10 @@ impl Rung { pub enum Error { /// A rung that encodes nothing: a size that rounds to zero pixels, or no /// bitrate to spend. - #[error("rung {size}p at {bitrate} bps encodes nothing")] + #[error("rung {short_side}p at {bitrate} bps encodes nothing")] Empty { /// The configured size, before rounding to even. - size: u32, + short_side: u32, /// The configured maximum, in bits per second. bitrate: u64, }, @@ -52,14 +52,14 @@ pub enum Error { /// Resolution runs backwards against bitrate: a rung costs more than the one /// below it without being taller, so bitrate and picture disagree on which /// rendition is lower. - #[error("rung {size}p at {bitrate} bps does not rise above {below_size}p at {below_bitrate} bps")] + #[error("rung {short_side}p at {bitrate} bps does not rise above {below_short_side}p at {below_bitrate} bps")] Unordered { /// The more expensive rung's size, in pixels. - size: u32, + short_side: u32, /// The more expensive rung's maximum, in bits per second. bitrate: u64, /// The cheaper rung's size, in pixels. - below_size: u32, + below_short_side: u32, /// The cheaper rung's maximum, in bits per second. below_bitrate: u64, }, @@ -88,32 +88,32 @@ impl Ladder { pub fn new(rungs: impl IntoIterator) -> Result { let mut rungs: Vec = rungs.into_iter().collect(); for rung in &mut rungs { - if rung.size < 2 || rung.bitrate.as_bps() == 0 { + if rung.short_side < 2 || rung.bitrate.as_bps() == 0 { return Err(Error::Empty { - size: rung.size, + short_side: rung.short_side, bitrate: rung.bitrate.as_bps(), }); } // Normalize before checking uniqueness: odd sizes can share a track name. - rung.size &= !1; + rung.short_side &= !1; } - rungs.sort_by_key(|rung| (rung.bitrate, rung.size)); + rungs.sort_by_key(|rung| (rung.bitrate, rung.short_side)); for pair in rungs.windows(2) { let (below, rung) = (pair[0], pair[1]); if below.bitrate == rung.bitrate { return Err(Error::DuplicateBitrate { bitrate: rung.bitrate.as_bps(), - first: below.size, - second: rung.size, + first: below.short_side, + second: rung.short_side, }); } - if rung.size <= below.size { + if rung.short_side <= below.short_side { return Err(Error::Unordered { - size: rung.size, + short_side: rung.short_side, bitrate: rung.bitrate.as_bps(), - below_size: below.size, + below_short_side: below.short_side, below_bitrate: below.bitrate.as_bps(), }); } @@ -148,7 +148,7 @@ mod tests { use super::*; fn sizes(ladder: &Ladder) -> Vec { - ladder.rungs().iter().map(|rung| rung.size).collect() + ladder.rungs().iter().map(|rung| rung.short_side).collect() } /// The operator writes the ladder top-down (or in whatever order), and it @@ -199,9 +199,9 @@ mod tests { assert_eq!( err, Error::Unordered { - size: 720, + short_side: 720, bitrate: 2_500_000, - below_size: 720, + below_short_side: 720, below_bitrate: 1_200_000, } ); @@ -219,9 +219,9 @@ mod tests { assert_eq!( err, Error::Unordered { - size: 360, + short_side: 360, bitrate: 3_000_000, - below_size: 1080, + below_short_side: 1080, below_bitrate: 1_000_000, } ); @@ -232,13 +232,16 @@ mod tests { assert_eq!( Ladder::new([Rung::new(1, moq_net::bandwidth::Rate::from_bps(350_000))]).unwrap_err(), Error::Empty { - size: 1, + short_side: 1, bitrate: 350_000 } ); assert_eq!( Ladder::new([Rung::new(240, moq_net::bandwidth::Rate::from_bps(0))]).unwrap_err(), - Error::Empty { size: 240, bitrate: 0 } + Error::Empty { + short_side: 240, + bitrate: 0 + } ); } diff --git a/rs/moq-video/MIGRATE.md b/rs/moq-video/MIGRATE.md index e8292cbf31f..84e05164e3c 100644 --- a/rs/moq-video/MIGRATE.md +++ b/rs/moq-video/MIGRATE.md @@ -4,6 +4,12 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Native frame imports use platform and format roles + +`frame::macos` is `frame::apple`. +`frame::vulkan::Channels` is `Format`. `Image` exposes `device`, `memory`, `size`, `allocation_size`, and `format` as fields; replace `device_uuid()` with `device.device_uuid`, `size()` with `size`, `allocation_size()` with `allocation_size`, and `channels()` with `format`. +Construct `Handles { memory, timeline }` directly instead of calling `Handles::new`. + ### `Control::cut` is not rate limited, and capture advertises refresh (#6048) `encode::Control::cut` no longer spaces keyframes 500 ms apart: every request forces one on the next frame, so frequent calls produce all-keyframe video. diff --git a/swift/MIGRATE.md b/swift/MIGRATE.md index ea2f0d9cce8..aa8ae7f1309 100644 --- a/swift/MIGRATE.md +++ b/swift/MIGRATE.md @@ -4,6 +4,16 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Media track names belong to the target + +Audio and video init records no longer carry a track name. Pass that name on the media producer's named target instead; init records describe only the codec and media. + +### Broadcast finish returns a linger handle + +`BroadcastProducer.finish()` returns a `BroadcastFinish` handle instead of no value. +Keep it and await `wait()` to serve finished tracks until their media is stale. +Use `BroadcastProducer.close()` for an immediate end. + ### JSON snapshot `deltaRatio` is `deltas` `Json.SnapshotProducer(deltaRatio:)` and `MoqJsonSnapshotConfig.deltaRatio` are replaced by `deltas: Bool` (default `true`). @@ -204,6 +214,8 @@ let stream = try Media.ContainerProducer(broadcast: broadcast, initData: Media.C ### Timestamps are optional +An omitted timescale now publishes a raw track untimed instead of selecting microseconds. Explicitly set a microsecond timescale to preserve the released default. + 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. @@ -238,10 +250,6 @@ Read `enabled` where you read `!stalled`. `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 diff --git a/swift/ffi/MIGRATE.md b/swift/ffi/MIGRATE.md index e50bf21fcd7..51bdd9b006f 100644 --- a/swift/ffi/MIGRATE.md +++ b/swift/ffi/MIGRATE.md @@ -1,32 +1,10 @@ # Migrating the Swift MoqFFI package -How to update your code for each breaking change, newest first. - ## Unreleased -### A client asks only about what it watches (#6165) - -On moq-lite, a client session asks its relay for announcements only under paths something watches, through `announced()` or `announcedBroadcast()`, instead of under its whole scope at connect. -`MoqOriginConsumer.requestBroadcast()` watches nothing, so on a client it fails with an unroutable error for a path nothing watches, even when the relay announces it. -Resolve the path with `announcedBroadcast()`, or hold `announced()` over its prefix while requesting. -moq-transport and moq-lite-03/04 sessions are unchanged. -Regenerate the bindings for the changed `request_broadcast` checksum. - -### Catalog references carry their epoch - -`MoqVideo` and `MoqAudio` gain `epoch`, the publisher instance their `broadcast` reference names; the memberwise initializer defaults it to `nil`. -`MoqBroadcastConsumer.resolve(reference:)` requires `epoch:` too; pass the rendition's `epoch` alongside, or `nil` to resolve whichever instance answers. -Resolving, and the rendition-based consumers, throw `MoqError.Mux` for a rendition whose broadcast is proven to serve another epoch. -Regenerate the Swift sources and rebuild the matching XCFramework, then rebuild callers. - -### Finished tracks stay until the broadcast ends (#6157) - -A finished track, raw or media, now stays listed, with its catalog rendition and demand, until its broadcast closes or the new `MoqBroadcastProducer.finish()` lets go, instead of unlisting when its producer finishes or is released. -Publish a replacement under a new name, and expect a finished track's demand watch to end with the broadcast. -Regenerate the bindings for the new `MoqBroadcastFinish` and the changed checksums. - -### Regenerate the matching bindings +### Follow the core API migration guide -Documentation on `MoqAudioProducer::write` and `finish` changes their UniFFI metadata checksums. -Regenerate the Swift sources and rebuild the matching XCFramework, then rebuild callers. -The method signatures and behavior are unchanged. +These bindings are generated from moq-ffi. Apply every change in the +[moq-ffi migration guide](../../rs/moq-ffi/MIGRATE.md), using this language's generated casing. +Regenerate the bindings and rebuild the matching native library together; method metadata checksums are not a separate API migration. +The [hand-written wrapper](../MIGRATE.md) has its own migration guide.