diff --git a/cpp/moq/MIGRATE.md b/cpp/moq/MIGRATE.md index f7e71829805..ed65c7f0dd6 100644 --- a/cpp/moq/MIGRATE.md +++ b/cpp/moq/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `announce` takes an `AnnounceRoute` + +`BroadcastProducer::announce` takes `moq::AnnounceRoute{cost, epoch}` instead of a `moq::Route`: the origin builds the hop chain, so `hops` is no longer an input. +`moq::Route` is only what announcements report. To re-announce an observed route, copy its `cost` and `epoch`. + ### A broadcast reference waits for its sibling's announcement (#6254) Resolving a rendition's `broadcast` reference, through `BroadcastConsumer::resolve` and the rendition-based subscribes and fetches, now waits for a sibling nothing announces yet instead of failing as unroutable, so a late sibling plays once it is announced. @@ -30,7 +35,7 @@ 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. +`BroadcastConsumer::resolve(reference)` becomes `resolve(rendition)`, taking the catalog `Rendition` and reading its `broadcast` and `epoch` from it. 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) @@ -105,11 +110,11 @@ To start at group `n`, pass one range starting at the absolute anchor for group ### 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. +Hold the consumer `Session::recv_bandwidth` returns to request PROBE, read it with `rate()` or wait with `rate_changed()`, and `cancel()` it to stop. ```cpp auto bandwidth = session->recv_bandwidth(); -auto rate = bandwidth ? bandwidth->peek() : std::nullopt; +auto rate = bandwidth ? bandwidth->rate() : std::nullopt; ``` ### `AnnouncedBroadcast::available()` waits for the next publisher instance (#5700) diff --git a/cpp/moq/include/moq/moq.hpp b/cpp/moq/include/moq/moq.hpp index 495830cace7..b29dedbc953 100644 --- a/cpp/moq/include/moq/moq.hpp +++ b/cpp/moq/include/moq/moq.hpp @@ -84,7 +84,7 @@ inline bool qlog_supported() { return moq_qlog_supported(); } -// Mints a fresh publisher epoch, ordered newest last. Announce one per run in Route::epoch, +// Mints a fresh publisher epoch, ordered newest last. Announce one per run in AnnounceRoute::epoch, // so viewers see a restart as a new broadcast instead of a stalled one. inline std::string mint_epoch() { return moq_mint_epoch(); diff --git a/cpp/obs/src/moq-output.cpp b/cpp/obs/src/moq-output.cpp index b6e12385428..b8a780f4677 100644 --- a/cpp/obs/src/moq-output.cpp +++ b/cpp/obs/src/moq-output.cpp @@ -192,7 +192,7 @@ bool MoQOutput::Start() LOG_ERROR("Failed to create broadcast: %s", next_broadcast.error().to_string().c_str()); return false; } - moq::Route route; + moq::AnnounceRoute route; route.epoch = moq::mint_epoch(); if (auto announced = (*next_broadcast)->announce(route); !announced) { LOG_ERROR("Failed to announce broadcast: %s", announced.error().to_string().c_str()); @@ -502,7 +502,7 @@ bool MoQOutput::TryGetConnectionStats(ConnectionStats *out) snapshot.estimated_send_rate_valid = raw.estimated_send_rate_bps.has_value(); snapshot.estimated_send_rate_bps = raw.estimated_send_rate_bps ? static_cast(*raw.estimated_send_rate_bps) : 0; - const auto recv_rate = receive ? receive->peek() : std::nullopt; + const auto recv_rate = receive ? receive->rate() : std::nullopt; snapshot.estimated_recv_rate_valid = recv_rate.has_value(); snapshot.estimated_recv_rate_bps = recv_rate ? static_cast(*recv_rate) : 0; snapshot.bytes_sent_valid = raw.bytes_sent.has_value(); diff --git a/cpp/obs/test/moq-audio-encoder-test.cpp b/cpp/obs/test/moq-audio-encoder-test.cpp index 361a37f1b90..a6efb3c0c92 100644 --- a/cpp/obs/test/moq-audio-encoder-test.cpp +++ b/cpp/obs/test/moq-audio-encoder-test.cpp @@ -203,7 +203,7 @@ void CheckSubscriberDecodes(const char *preset, size_t channels) auto origin = moq::OriginProducer::init(moq::OriginConfig{}); auto broadcast = TestOk(origin->create_broadcast("room"), "create_broadcast"); - moq::Route route; + moq::AnnounceRoute route; route.epoch = moq::mint_epoch(); TestOk(broadcast->announce(route), "announce"); diff --git a/cpp/obs/test/moq-source-test.cpp b/cpp/obs/test/moq-source-test.cpp index a0c0ded8416..18f9db69fab 100644 --- a/cpp/obs/test/moq-source-test.cpp +++ b/cpp/obs/test/moq-source-test.cpp @@ -533,7 +533,7 @@ class Publisher { TestOk(moq::MediaTrackProducer::audio(broadcast, moq::MediaTarget::kNamed{}, init), "audio producer"); } - TestOk(broadcast->announce(moq::Route{}), "announce"); + TestOk(broadcast->announce(moq::AnnounceRoute{}), "announce"); } ~Publisher() { (void)broadcast->close(); } diff --git a/dart/moq/MIGRATE.md b/dart/moq/MIGRATE.md index e0608552502..219980e3eb7 100644 --- a/dart/moq/MIGRATE.md +++ b/dart/moq/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `announce` takes an `AnnounceRoute` + +`BroadcastProducer.announce(route:)` takes `AnnounceRoute(cost:, epoch:)` instead of a `MoqRoute`: the origin builds the hop chain, so `hops` is no longer an input. +`MoqRoute` is only what announcements report. To re-announce an observed route, copy its `cost` and `epoch`. + ### Cancelling a stream cancels its pending read `await subscription.cancel()` on announcements, authorization grants, and server requests now waits for the pending native operation to release, even when no event arrives. @@ -43,7 +48,7 @@ Pass the new `ContainerFormat.cmaf` to keep publishing CMAF. ### Catalog references carry their epoch `Video` and `Audio` renditions gain `epoch`, the publisher instance their `broadcast` reference names. -`BroadcastConsumer.resolve(reference:)` requires `epoch:` too; pass the rendition's `epoch` alongside its `broadcast`, or `null` to resolve whichever instance answers. +`BroadcastConsumer.resolve(reference:)` becomes `resolve(rendition:)`, taking the catalog `Rendition` and reading its `broadcast` and `epoch` from it. Resolving, and the rendition-based consumers, throw for a rendition whose broadcast is proven to serve another epoch. ### Timestamps stop at 2^61 - 1 microseconds (#6168) @@ -89,7 +94,7 @@ A subscription now reads only the groups its own `ranges` cover. `TrackDemand.isUsed()`, `used()`, and `unused()` are gone. `wanted()` returns null while nobody subscribes, otherwise the group subscribers want next, and `wantedChanged()` waits for it to change (the first call returns at once). Loop on `wantedChanged()` until it returns non-null where you awaited `used()`, and null where you awaited `unused()`. -`TrackInfo` gains `refresh`. `GroupDemand` is unchanged. +`TrackInfo` gains `refresh`. ### `CongestionControl` gained `none` (#5926) @@ -109,8 +114,8 @@ To start at group `n`, pass one range starting at the absolute anchor for group ### 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()`. +Hold the consumer `Session.recvBandwidth` returns to request PROBE, read it with `rate()` or wait with `rateChanged()`, and `cancel()` it to stop. +Replace `stats().estimatedRecvRateBps` with `recvBandwidth()?.rate()`. ### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) diff --git a/dart/moq/README.md b/dart/moq/README.md index c3bfa3b0e1d..67e66d10332 100644 --- a/dart/moq/README.md +++ b/dart/moq/README.md @@ -33,7 +33,7 @@ final server = await Server.listen( ), ); final broadcast = server.createBroadcast('live'); -broadcast.announce(route: MoqRoute()); // unannounced broadcasts are invisible +broadcast.announce(route: AnnounceRoute()); // unannounced broadcasts are invisible await for (final request in server.requests()) { final session = await request.accept(); print(session.connects()); diff --git a/dart/moq/lib/src/aliases.dart b/dart/moq/lib/src/aliases.dart index d026dbbf33a..be0aecd9f61 100644 --- a/dart/moq/lib/src/aliases.dart +++ b/dart/moq/lib/src/aliases.dart @@ -55,6 +55,9 @@ typedef AnnounceConfig = MoqAnnounceConfig; /// A pending wait for a route to cover a specific path. typedef AnnouncedBroadcast = MoqAnnouncedBroadcast; +/// How a broadcast advertises its exact path: its static production cost and publisher epoch. +typedef AnnounceRoute = MoqAnnounceRoute; + /// A route over a prefix: its origin-relative path, wildcard captures, and route metadata. typedef Announce = MoqAnnounce; @@ -106,9 +109,6 @@ typedef TrackConsumer = MoqTrackConsumer; /// A request to produce one uncached group for a fetch consumer. typedef GroupRequest = MoqGroupRequest; -/// A watch-only handle to whether any caller still wants a requested group; holding it keeps nothing open. -typedef GroupDemand = MoqGroupDemand; - /// The write side of a single group: append frames to it. typedef GroupProducer = MoqGroupProducer; diff --git a/dart/moq/lib/src/epoch.dart b/dart/moq/lib/src/epoch.dart index d628417c0b5..7479a523e99 100644 --- a/dart/moq/lib/src/epoch.dart +++ b/dart/moq/lib/src/epoch.dart @@ -2,7 +2,7 @@ import 'package:moq_ffi/moq_ffi.dart'; /// Mint a fresh publisher epoch from the wall clock and secure randomness, /// ordered newest last. Mint one per publisher run and announce it in -/// `MoqRoute.epoch`, so viewers see a restart as a new broadcast instead of a +/// `AnnounceRoute.epoch`, so viewers see a restart as a new broadcast instead of a /// stalled one. String mintEpoch() => moqMintEpoch(); diff --git a/dart/moq/test/moq_test.dart b/dart/moq/test/moq_test.dart index 01e4836c8ec..0c7f83e7216 100644 --- a/dart/moq/test/moq_test.dart +++ b/dart/moq/test/moq_test.dart @@ -231,7 +231,7 @@ void main() { name: 'events', info: TrackInfo(timescale: 1000000), ); - broadcast.announce(route: MoqRoute()); + broadcast.announce(route: AnnounceRoute()); final announced = (await announcement.timeout(timeout) as AnnounceEventStart).announce; expect(announced.prefix, 'live'); @@ -309,7 +309,7 @@ void main() { ); final broadcast = server.createBroadcast('live'); final track = broadcast.publishTrack(name: 'events', info: null); - broadcast.announce(route: MoqRoute()); + broadcast.announce(route: AnnounceRoute()); final announced = (await announcement.timeout(timeout) as AnnounceEventStart).announce; expect(announced.prefix, 'live'); @@ -445,7 +445,7 @@ void main() { throwsA(anything), ); - broadcast.announce(route: MoqRoute()); + broadcast.announce(route: AnnounceRoute()); final announced = consumer.announced(config: MoqAnnounceConfig()); final first = await nextRoute(announced); expect(first, isA()); @@ -460,7 +460,7 @@ void main() { throwsA(anything), ); - broadcast.announce(route: MoqRoute()); + broadcast.announce(route: AnnounceRoute()); expect(await nextRoute(announced), isA()); await consumer.requestBroadcast(path: 'live').timeout(timeout); announced.cancel(); @@ -477,12 +477,12 @@ void main() { epochTime('018bcfe5-6800-7000-8000-000000000000'), DateTime.utc(2023, 11, 14, 22, 13, 20), ); - broadcast.announce(route: MoqRoute(epoch: first)); + broadcast.announce(route: AnnounceRoute(epoch: first)); final start = await nextRoute(announced) as AnnounceEventStart; expect(start.announce.route.epoch, first); final second = mintEpoch(); - broadcast.announce(route: MoqRoute(epoch: second)); + broadcast.announce(route: AnnounceRoute(epoch: second)); final restart = await nextRoute(announced) as AnnounceEventRestart; expect(restart.announce.route.epoch, second); @@ -497,7 +497,7 @@ void main() { config: MoqAnnounceConfig(prefix: 'room', filter: '*/chat'), ); final broadcast = origin.createBroadcast(path: 'room/alice/chat'); - broadcast.announce(route: MoqRoute()); + broadcast.announce(route: AnnounceRoute()); final update = await nextRoute(announced) as AnnounceEventStart; expect(update.announce.prefix, 'room/alice/chat'); diff --git a/dart/moq_ffi/MIGRATE.md b/dart/moq_ffi/MIGRATE.md index 2b8497d8a60..44b21311827 100644 --- a/dart/moq_ffi/MIGRATE.md +++ b/dart/moq_ffi/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `announce` takes a `MoqAnnounceRoute` + +`MoqBroadcastProducer.announce` takes `MoqAnnounceRoute` (`cost`, `epoch`) instead of a `MoqRoute`: the origin builds the hop chain, so `hops` is no longer an input. +`MoqRoute` is only what announcements report, and the `InvalidRoute` error is gone. + ### Native futures support operation cancellation Generated async calls return `UniffiRustFuture`, which implements `Future` and adds `Future cancel()`. @@ -38,7 +43,7 @@ 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. +`MoqBroadcastConsumer.resolve(reference:)` becomes `resolve(rendition:)`, taking the catalog `MoqRendition` and reading its `broadcast` and `epoch` from it. 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) diff --git a/dart/moq_ffi/lib/src/moq.dart b/dart/moq_ffi/lib/src/moq.dart index 355c90e6b1c..7e60959cf9d 100644 --- a/dart/moq_ffi/lib/src/moq.dart +++ b/dart/moq_ffi/lib/src/moq.dart @@ -8,7 +8,9 @@ import "dart:ffi"; import "dart:io" show Platform, File, Directory; import "dart:isolate"; import "dart:typed_data"; + import "package:ffi/ffi.dart"; + import "uniffi_runtime.dart"; export "uniffi_runtime.dart"; @@ -2598,6 +2600,65 @@ class FfiConverterMoqAnnounceConfig { } } +class MoqAnnounceRoute { + final int cost; + final String? epoch; + MoqAnnounceRoute({this.cost = 0, this.epoch = null}); +} + +class FfiConverterMoqAnnounceRoute { + static MoqAnnounceRoute lift(RustBuffer buf) { + return FfiConverterMoqAnnounceRoute.read(buf.asUint8List()).value; + } + + static LiftRetVal read(Uint8List buf) { + int new_offset = buf.offsetInBytes; + final cost_lifted = FfiConverterUInt64.read( + Uint8List.view(buf.buffer, new_offset), + ); + final cost = cost_lifted.value; + new_offset += cost_lifted.bytesRead; + final epoch_lifted = FfiConverterOptionalString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final epoch = epoch_lifted.value; + new_offset += epoch_lifted.bytesRead; + return LiftRetVal( + MoqAnnounceRoute(cost: cost, epoch: epoch), + new_offset - buf.offsetInBytes, + ); + } + + static RustBuffer lower(MoqAnnounceRoute value) { + final total_length = + FfiConverterUInt64.allocationSize(value.cost) + + FfiConverterOptionalString.allocationSize(value.epoch) + + 0; + final buf = Uint8List(total_length); + write(value, buf); + return toRustBuffer(buf); + } + + static int write(MoqAnnounceRoute value, Uint8List buf) { + int new_offset = buf.offsetInBytes; + new_offset += FfiConverterUInt64.write( + value.cost, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterOptionalString.write( + value.epoch, + Uint8List.view(buf.buffer, new_offset), + ); + return new_offset - buf.offsetInBytes; + } + + static int allocationSize(MoqAnnounceRoute value) { + return FfiConverterUInt64.allocationSize(value.cost) + + FfiConverterOptionalString.allocationSize(value.epoch) + + 0; + } +} + class MoqOriginConfig { final int? cacheCapacityBytes; MoqOriginConfig({this.cacheCapacityBytes = null}); @@ -2650,10 +2711,10 @@ class MoqRoute { final bool anonymous; final String? epoch; MoqRoute({ - this.hops = const [], - this.cost = 0, - this.anonymous = false, - this.epoch = null, + required this.hops, + required this.cost, + required this.anonymous, + this.epoch, }); } @@ -4756,42 +4817,36 @@ class FfiConverterMoqException { lifted.bytesRead - subview.offsetInBytes + 4, ); case 28: - final lifted = InvalidRouteMoqException.read(subview); - return LiftRetVal( - lifted.value, - lifted.bytesRead - subview.offsetInBytes + 4, - ); - case 29: final lifted = InvalidPatternMoqException.read(subview); return LiftRetVal( lifted.value, lifted.bytesRead - subview.offsetInBytes + 4, ); - case 30: + case 29: final lifted = InvalidEpochMoqException.read(subview); return LiftRetVal( lifted.value, lifted.bytesRead - subview.offsetInBytes + 4, ); - case 31: + case 30: final lifted = UnresolvableBroadcastMoqException.read(subview); return LiftRetVal( lifted.value, lifted.bytesRead - subview.offsetInBytes + 4, ); - case 32: + case 31: final lifted = LogMoqException.read(subview); return LiftRetVal( lifted.value, lifted.bytesRead - subview.offsetInBytes + 4, ); - case 33: + case 32: final lifted = ConfigMoqException.read(subview); return LiftRetVal( lifted.value, lifted.bytesRead - subview.offsetInBytes + 4, ); - case 34: + case 33: final lifted = UnsupportedCatalogMoqException.read(subview); return LiftRetVal( lifted.value, @@ -5734,44 +5789,6 @@ class AlreadyCommittedMoqException extends MoqException { } } -class InvalidRouteMoqException extends MoqException { - final String reason; - InvalidRouteMoqException(String this.reason); - InvalidRouteMoqException._(String this.reason); - static LiftRetVal read(Uint8List buf) { - int new_offset = buf.offsetInBytes; - final reason_lifted = FfiConverterString.read( - Uint8List.view(buf.buffer, new_offset), - ); - final reason = reason_lifted.value; - new_offset += reason_lifted.bytesRead; - return LiftRetVal(InvalidRouteMoqException._(reason), new_offset); - } - - @override - RustBuffer lower() { - final buf = Uint8List(allocationSize()); - write(buf); - return toRustBuffer(buf); - } - - @override - int allocationSize() { - return FfiConverterString.allocationSize(reason) + 4; - } - - @override - int write(Uint8List buf) { - buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 28); - int new_offset = buf.offsetInBytes + 4; - new_offset += FfiConverterString.write( - reason, - Uint8List.view(buf.buffer, new_offset), - ); - return new_offset; - } -} - class InvalidPatternMoqException extends MoqException { final String reason; InvalidPatternMoqException(String this.reason); @@ -5800,7 +5817,7 @@ class InvalidPatternMoqException extends MoqException { @override int write(Uint8List buf) { - buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 29); + buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 28); int new_offset = buf.offsetInBytes + 4; new_offset += FfiConverterString.write( reason, @@ -5838,7 +5855,7 @@ class InvalidEpochMoqException extends MoqException { @override int write(Uint8List buf) { - buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 30); + buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 29); int new_offset = buf.offsetInBytes + 4; new_offset += FfiConverterString.write( reason, @@ -5876,7 +5893,7 @@ class UnresolvableBroadcastMoqException extends MoqException { @override int write(Uint8List buf) { - buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 31); + buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 30); int new_offset = buf.offsetInBytes + 4; new_offset += FfiConverterString.write( reason, @@ -5914,7 +5931,7 @@ class LogMoqException extends MoqException { @override int write(Uint8List buf) { - buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 32); + buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 31); int new_offset = buf.offsetInBytes + 4; new_offset += FfiConverterString.write( reason, @@ -5952,7 +5969,7 @@ class ConfigMoqException extends MoqException { @override int write(Uint8List buf) { - buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 33); + buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 32); int new_offset = buf.offsetInBytes + 4; new_offset += FfiConverterString.write( reason, @@ -5990,7 +6007,7 @@ class UnsupportedCatalogMoqException extends MoqException { @override int write(Uint8List buf) { - buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 34); + buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 33); int new_offset = buf.offsetInBytes + 4; new_offset += FfiConverterUInt64.write( version, @@ -8100,7 +8117,6 @@ class FfiConverterMoqAuthToken { abstract class MoqBandwidthInterface { MoqReservation reserve({required MoqTrackDemand demand, required int maxBps}); - void setTarget({required int? targetBps}); } final _MoqBandwidthFinalizer = Finalizer>((ptr) { @@ -8149,16 +8165,6 @@ class MoqBandwidth implements MoqBandwidthInterface { null, ); } - - void setTarget({required int? targetBps}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqbandwidth_set_target( - uniffiClonePointer(), - FfiConverterOptionalUInt64.lower(targetBps), - status, - ); - }, null); - } } class FfiConverterMoqBandwidth { @@ -8189,8 +8195,8 @@ class FfiConverterMoqBandwidth { abstract class MoqBandwidthConsumerInterface { void cancel(); - UniffiRustFuture changed(); - int? peek(); + int? rate(); + UniffiRustFuture rateChanged(); } final _MoqBandwidthConsumerFinalizer = Finalizer>((ptr) { @@ -8237,28 +8243,28 @@ class MoqBandwidthConsumer implements MoqBandwidthConsumerInterface { }, null); } - UniffiRustFuture changed() { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqbandwidthconsumer_changed( + int? rate() { + return rustCallWithLifter( + (status) => uniffi_moq_ffi_fn_method_moqbandwidthconsumer_rate( uniffiClonePointer(), + status, ), - ffi_moq_ffi_rust_future_poll_rust_buffer, - ffi_moq_ffi_rust_future_complete_rust_buffer, - ffi_moq_ffi_rust_future_cancel_rust_buffer, - ffi_moq_ffi_rust_future_free_rust_buffer, FfiConverterOptionalUInt64.lift, - moqExceptionErrorHandler, + null, ); } - int? peek() { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqbandwidthconsumer_peek( + UniffiRustFuture rateChanged() { + return uniffiRustCallAsync( + () => uniffi_moq_ffi_fn_method_moqbandwidthconsumer_rate_changed( uniffiClonePointer(), - status, ), + ffi_moq_ffi_rust_future_poll_rust_buffer, + ffi_moq_ffi_rust_future_complete_rust_buffer, + ffi_moq_ffi_rust_future_cancel_rust_buffer, + ffi_moq_ffi_rust_future_free_rust_buffer, FfiConverterOptionalUInt64.lift, - null, + moqExceptionErrorHandler, ); } } @@ -8380,8 +8386,7 @@ abstract class MoqBroadcastConsumerInterface { required MoqFetchGroupOptions? options, }); UniffiRustFuture resolve({ - required String? reference, - required String? epoch, + required MoqRendition rendition, }); UniffiRustFuture subscribeTrack({ required String name, @@ -8447,14 +8452,12 @@ class MoqBroadcastConsumer implements MoqBroadcastConsumerInterface { } UniffiRustFuture resolve({ - required String? reference, - required String? epoch, + required MoqRendition rendition, }) { return uniffiRustCallAsync( () => uniffi_moq_ffi_fn_method_moqbroadcastconsumer_resolve( uniffiClonePointer(), - FfiConverterOptionalString.lower(reference), - FfiConverterOptionalString.lower(epoch), + FfiConverterMoqRendition.lower(rendition), ), ffi_moq_ffi_rust_future_poll_u64, ffi_moq_ffi_rust_future_complete_u64, @@ -8765,118 +8768,6 @@ class FfiConverterMoqTrackConsumer { } } -abstract class MoqGroupDemandInterface { - bool isUsed(); - int sequence(); - UniffiRustFuture unused(); - UniffiRustFuture used(); -} - -final _MoqGroupDemandFinalizer = Finalizer>((ptr) { - rustCall((status) => uniffi_moq_ffi_fn_free_moqgroupdemand(ptr, status)); -}); - -class MoqGroupDemand implements MoqGroupDemandInterface { - late final Pointer _ptr; - bool _isDisposed = false; - MoqGroupDemand._(this._ptr) { - _MoqGroupDemandFinalizer.attach(this, _ptr, detach: this); - } - factory MoqGroupDemand.lift(Pointer ptr) { - return MoqGroupDemand._(ptr); - } - Pointer uniffiClonePointer() { - if (_isDisposed) { - throw StateError("Object has already been disposed"); - } - return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqgroupdemand(_ptr, status), - ); - } - - void dispose() { - if (_isDisposed) { - return; - } - _isDisposed = true; - _MoqGroupDemandFinalizer.detach(this); - rustCall((status) => uniffi_moq_ffi_fn_free_moqgroupdemand(_ptr, status)); - } - - bool isUsed() { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqgroupdemand_is_used( - uniffiClonePointer(), - status, - ), - FfiConverterBool.lift, - null, - ); - } - - int sequence() { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqgroupdemand_sequence( - uniffiClonePointer(), - status, - ), - FfiConverterUInt64.lift, - null, - ); - } - - UniffiRustFuture unused() { - return uniffiRustCallAsync( - () => - uniffi_moq_ffi_fn_method_moqgroupdemand_unused(uniffiClonePointer()), - ffi_moq_ffi_rust_future_poll_void, - ffi_moq_ffi_rust_future_complete_void, - ffi_moq_ffi_rust_future_cancel_void, - ffi_moq_ffi_rust_future_free_void, - (_) {}, - moqExceptionErrorHandler, - ); - } - - UniffiRustFuture used() { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqgroupdemand_used(uniffiClonePointer()), - ffi_moq_ffi_rust_future_poll_void, - ffi_moq_ffi_rust_future_complete_void, - ffi_moq_ffi_rust_future_cancel_void, - ffi_moq_ffi_rust_future_free_void, - (_) {}, - moqExceptionErrorHandler, - ); - } -} - -class FfiConverterMoqGroupDemand { - static MoqGroupDemand lift(Pointer ptr) { - return MoqGroupDemand.lift(ptr); - } - - static Pointer lower(MoqGroupDemand value) { - return value.uniffiClonePointer(); - } - - static int allocationSize(MoqGroupDemand value) { - return 8; - } - - static LiftRetVal read(Uint8List buf) { - final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); - final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqGroupDemand.lift(pointer), 8); - } - - static int write(MoqGroupDemand value, Uint8List buf) { - final handle = lower(value); - buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); - return 8; - } -} - abstract class MoqTrackDemandInterface { String name(); int? wanted(); @@ -11447,7 +11338,7 @@ class FfiConverterMoqBroadcastFinish { } abstract class MoqBroadcastProducerInterface { - void announce({required MoqRoute route}); + void announce({required MoqAnnounceRoute route}); void close(); MoqBroadcastConsumer consume(); MoqBroadcastDynamic dynamic_(); @@ -11502,11 +11393,11 @@ class MoqBroadcastProducer implements MoqBroadcastProducerInterface { ); } - void announce({required MoqRoute route}) { + void announce({required MoqAnnounceRoute route}) { return rustCall((status) { uniffi_moq_ffi_fn_method_moqbroadcastproducer_announce( uniffiClonePointer(), - FfiConverterMoqRoute.lower(route), + FfiConverterMoqAnnounceRoute.lower(route), status, ); }, moqExceptionErrorHandler); @@ -11726,7 +11617,6 @@ class FfiConverterMoqGroupProducer { abstract class MoqGroupRequestInterface { void abort({required int errorCode}); MoqGroupProducer accept(); - MoqGroupDemand demand(); int priority(); int sequence(); } @@ -11783,17 +11673,6 @@ class MoqGroupRequest implements MoqGroupRequestInterface { ); } - MoqGroupDemand demand() { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqgrouprequest_demand( - uniffiClonePointer(), - status, - ), - FfiConverterMoqGroupDemand.lift, - moqExceptionErrorHandler, - ); - } - int priority() { return rustCallWithLifter( (status) => uniffi_moq_ffi_fn_method_moqgrouprequest_priority( @@ -15992,15 +15871,6 @@ external Pointer uniffi_moq_ffi_fn_method_moqbandwidth_reserve( Pointer uniffiStatus, ); -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqbandwidth_set_target( - Pointer ptr, - RustBuffer target_bps, - Pointer uniffiStatus, -); - @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) @@ -16025,19 +15895,18 @@ external void uniffi_moq_ffi_fn_method_moqbandwidthconsumer_cancel( Pointer uniffiStatus, ); -@Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqbandwidthconsumer_changed( - Pointer ptr, -); - @Native, Pointer)>( assetId: _uniffiAssetId, ) -external RustBuffer uniffi_moq_ffi_fn_method_moqbandwidthconsumer_peek( +external RustBuffer uniffi_moq_ffi_fn_method_moqbandwidthconsumer_rate( Pointer ptr, Pointer uniffiStatus, ); +@Native Function(Pointer)>(assetId: _uniffiAssetId) +external Pointer +uniffi_moq_ffi_fn_method_moqbandwidthconsumer_rate_changed(Pointer ptr); + @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) @@ -16098,13 +15967,12 @@ uniffi_moq_ffi_fn_method_moqbroadcastconsumer_fetch_group( RustBuffer options, ); -@Native Function(Pointer, RustBuffer, RustBuffer)>( +@Native Function(Pointer, RustBuffer)>( assetId: _uniffiAssetId, ) external Pointer uniffi_moq_ffi_fn_method_moqbroadcastconsumer_resolve( Pointer ptr, - RustBuffer reference, - RustBuffer epoch, + RustBuffer rendition, ); @Native Function(Pointer, RustBuffer, RustBuffer)>( @@ -16215,48 +16083,6 @@ external void uniffi_moq_ffi_fn_method_moqtrackconsumer_update( Pointer uniffiStatus, ); -@Native Function(Pointer, Pointer)>( - assetId: _uniffiAssetId, -) -external Pointer uniffi_moq_ffi_fn_clone_moqgroupdemand( - Pointer handle, - Pointer uniffiStatus, -); - -@Native, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_free_moqgroupdemand( - Pointer handle, - Pointer uniffiStatus, -); - -@Native, Pointer)>( - assetId: _uniffiAssetId, -) -external int uniffi_moq_ffi_fn_method_moqgroupdemand_is_used( - Pointer ptr, - Pointer uniffiStatus, -); - -@Native, Pointer)>( - assetId: _uniffiAssetId, -) -external int uniffi_moq_ffi_fn_method_moqgroupdemand_sequence( - Pointer ptr, - Pointer uniffiStatus, -); - -@Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqgroupdemand_unused( - Pointer ptr, -); - -@Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqgroupdemand_used( - Pointer ptr, -); - @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) @@ -17530,14 +17356,6 @@ external Pointer uniffi_moq_ffi_fn_method_moqgrouprequest_accept( Pointer uniffiStatus, ); -@Native Function(Pointer, Pointer)>( - assetId: _uniffiAssetId, -) -external Pointer uniffi_moq_ffi_fn_method_moqgrouprequest_demand( - Pointer ptr, - Pointer uniffiStatus, -); - @Native, Pointer)>( assetId: _uniffiAssetId, ) @@ -18708,17 +18526,14 @@ external int uniffi_moq_ffi_checksum_method_moqauthtoken_grant(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqbandwidth_reserve(); -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqbandwidth_set_target(); - @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqbandwidthconsumer_cancel(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqbandwidthconsumer_changed(); +external int uniffi_moq_ffi_checksum_method_moqbandwidthconsumer_rate(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqbandwidthconsumer_peek(); +external int uniffi_moq_ffi_checksum_method_moqbandwidthconsumer_rate_changed(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqreservation_grant(); @@ -18766,18 +18581,6 @@ external int uniffi_moq_ffi_checksum_method_moqtrackconsumer_recv_group(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqtrackconsumer_update(); -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgroupdemand_is_used(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgroupdemand_sequence(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgroupdemand_unused(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgroupdemand_used(); - @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqtrackdemand_name(); @@ -19027,9 +18830,6 @@ external int uniffi_moq_ffi_checksum_method_moqgrouprequest_abort(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqgrouprequest_accept(); -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgrouprequest_demand(); - @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqgrouprequest_priority(); @@ -19399,16 +19199,14 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqbandwidth_reserve() != 27807) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbandwidth_set_target() != 36601) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } if (uniffi_moq_ffi_checksum_method_moqbandwidthconsumer_cancel() != 58304) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbandwidthconsumer_changed() != 64643) { + if (uniffi_moq_ffi_checksum_method_moqbandwidthconsumer_rate() != 420) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbandwidthconsumer_peek() != 9323) { + if (uniffi_moq_ffi_checksum_method_moqbandwidthconsumer_rate_changed() != + 38021) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqreservation_grant() != 59401) { @@ -19421,7 +19219,7 @@ void _checkApiChecksums() { 18633) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_resolve() != 42208) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_resolve() != 56822) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_subscribe_track() != @@ -19459,18 +19257,6 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqtrackconsumer_update() != 39083) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqgroupdemand_is_used() != 55523) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqgroupdemand_sequence() != 34558) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqgroupdemand_unused() != 1305) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqgroupdemand_used() != 16710) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } if (uniffi_moq_ffi_checksum_method_moqtrackdemand_name() != 14603) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } @@ -19673,7 +19459,7 @@ void _checkApiChecksums() { 45756) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqoriginproducer_dynamic() != 3545) { + if (uniffi_moq_ffi_checksum_method_moqoriginproducer_dynamic() != 17765) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqbroadcastdynamic_cancel() != 25875) { @@ -19692,7 +19478,7 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqbroadcastfinish_wait() != 36521) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_announce() != 13700) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_announce() != 3965) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_close() != 19191) { @@ -19736,9 +19522,6 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqgrouprequest_accept() != 48242) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqgrouprequest_demand() != 31288) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } if (uniffi_moq_ffi_checksum_method_moqgrouprequest_priority() != 15373) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } diff --git a/dart/moq_ffi/lib/src/uniffi_runtime.dart b/dart/moq_ffi/lib/src/uniffi_runtime.dart index c61178325f7..9018eb4e97e 100644 --- a/dart/moq_ffi/lib/src/uniffi_runtime.dart +++ b/dart/moq_ffi/lib/src/uniffi_runtime.dart @@ -8,6 +8,7 @@ import "dart:ffi"; import "dart:io" show Platform, File, Directory; import "dart:isolate"; import "dart:typed_data"; + import "package:ffi/ffi.dart"; class UniffiInternalError implements Exception { @@ -250,6 +251,7 @@ mixin FfiConverterPrimitive on FfiConverter { @override T lower(T value) => value; } + Uint8List createUint8ListFromInt(int value) { int length = value.bitLength ~/ 8 + 1; if (length != 4 && length != 8) { diff --git a/dart/moq_ffi/test/future_test.dart b/dart/moq_ffi/test/future_test.dart index 9cb694661e4..47c19534ead 100644 --- a/dart/moq_ffi/test/future_test.dart +++ b/dart/moq_ffi/test/future_test.dart @@ -139,7 +139,7 @@ void main() { final next = cursor.next(); final broadcast = origin.createBroadcast(path: 'still-listening'); try { - broadcast.announce(route: MoqRoute()); + broadcast.announce(route: MoqAnnounceRoute()); expect(await next, isA()); } finally { broadcast.close(); diff --git a/dart/moq_ffi/test/leak_test.dart b/dart/moq_ffi/test/leak_test.dart index d95fea33b23..1c8d132646a 100644 --- a/dart/moq_ffi/test/leak_test.dart +++ b/dart/moq_ffi/test/leak_test.dart @@ -35,11 +35,18 @@ void main() { test('a non-null optional argument is released', () async { // A local broadcast has no origin to resolve against, so each call lowers - // the optional String and then throws, also covering the error buffer. + // the rendition's optional String and then throws, also covering the error buffer. final consumer = MoqBroadcastProducer().consume(); - final reference = 'x' * size; + final rendition = VideoMoqRendition( + id: 'video', + config: MoqVideo( + broadcast: 'x' * size, + codec: 'avc1.64001f', + container: LegacyMoqContainer(), + ), + ); Future call() => expectLater( - consumer.resolve(reference: reference, epoch: null), + consumer.resolve(rendition: rendition), throwsA(isA()), ); diff --git a/demo/carla/sim/moq_carla_sim/main.py b/demo/carla/sim/moq_carla_sim/main.py index ac9642ea115..9ca7b604ac1 100644 --- a/demo/carla/sim/moq_carla_sim/main.py +++ b/demo/carla/sim/moq_carla_sim/main.py @@ -242,7 +242,7 @@ async def run(args: argparse.Namespace) -> None: try: # One epoch for the run: a restarted worker announces every # truck under a new one, which viewers see as a restart. - route = moq.Route(epoch=moq.mint_epoch()) + route = moq.AnnounceRoute(epoch=moq.mint_epoch()) for truck in world.trucks: broadcast = client.create_broadcast(broadcast_path(truck.name, args.camera)) video = await bridge.open(bridge_stream(truck.name, args.camera), renditions(camera), broadcast) diff --git a/demo/carla/sim/moq_carla_sim/runtime.py b/demo/carla/sim/moq_carla_sim/runtime.py index eb380f17075..2a1ceca410c 100644 --- a/demo/carla/sim/moq_carla_sim/runtime.py +++ b/demo/carla/sim/moq_carla_sim/runtime.py @@ -98,7 +98,7 @@ def __init__( location: str | None, map_name: str | None, clock: Clock, - route: moq.Route, + route: moq.AnnounceRoute, ) -> None: self._truck = truck self._video = video diff --git a/demo/carla/sim/tests/conftest.py b/demo/carla/sim/tests/conftest.py index 34ac3561510..2ba51aa858c 100644 --- a/demo/carla/sim/tests/conftest.py +++ b/demo/carla/sim/tests/conftest.py @@ -66,7 +66,7 @@ def __aiter__(self): moq.AnnounceEventUpdate = AnnounceEventUpdate moq.AnnounceEventEnd = AnnounceEventEnd moq.AnnounceEventRestart = AnnounceEventRestart -moq.Route = lambda **kwargs: SimpleNamespace(**kwargs) +moq.AnnounceRoute = lambda **kwargs: SimpleNamespace(**kwargs) moq.mint_epoch = lambda: str(uuid.uuid4()) moq_json = ModuleType("moq.json") diff --git a/doc/lib/cpp/index.md b/doc/lib/cpp/index.md index 725a713349c..0b3a8d836a2 100644 --- a/doc/lib/cpp/index.md +++ b/doc/lib/cpp/index.md @@ -123,7 +123,7 @@ moq::VideoEncoderOutput output{moq::VideoCodec::kH264, "camera", std::nullopt, s auto catalog = ok(moq::MediaCatalogProducer::init(broadcast)); auto video = ok(moq::VideoProducer::init(catalog, {moq::VideoPixelFormat::kRgba, 1280, 720, 30}, output, nullptr)); ok(video->write({0, rgba})); -moq::Route route; +moq::AnnounceRoute route; route.epoch = moq::mint_epoch(); // a fresh epoch per run ok(broadcast->announce(route)); ok(broadcast->close()); // keep the producer alive while publishing, then close explicitly @@ -189,7 +189,7 @@ Audio and video convenience decoders reject a catalog configuration with `enable ## Bandwidth estimates -Hold the owned `BandwidthConsumer` returned by `session->recv_bandwidth()` to receive bandwidth estimates. `peek()` reads the current estimate in bits per second and `changed()` waits for a new one. Releasing the last consumer stops PROBE; the session and stats snapshots do not request it. Client consumers survive reconnects and report no estimate while disconnected. +Hold the owned `BandwidthConsumer` returned by `session->recv_bandwidth()` to receive bandwidth estimates. `rate()` reads the current estimate in bits per second and `rate_changed()` waits for a new one. Releasing the last consumer stops PROBE; the session and stats snapshots do not request it. Client consumers survive reconnects and report no estimate while disconnected. ## Compressed tracks diff --git a/doc/lib/dart/index.md b/doc/lib/dart/index.md index 069613ffed8..c7294c17c6e 100644 --- a/doc/lib/dart/index.md +++ b/doc/lib/dart/index.md @@ -92,7 +92,7 @@ final encoder = video.Producer( ), ); encoder.write(frame: video.Frame(timestampUs: pts, data: rgba)); -camera.announce(route: MoqRoute(epoch: mintEpoch())); +camera.announce(route: AnnounceRoute(epoch: mintEpoch())); encoder.finish(); ``` @@ -120,7 +120,7 @@ final track = mine.publishTrack(name: 'video', info: null); final group = track.appendGroup(); group.writeFrame(frame: Frame(payload: bytes, timestampUs: null)); group.finish(); -mine.announce(route: MoqRoute(epoch: mintEpoch())); // a fresh epoch per run +mine.announce(route: AnnounceRoute(epoch: mintEpoch())); // a fresh epoch per run track.finish(); mine.close(); @@ -138,8 +138,8 @@ shows which module holds each. - **Live encoder timing.** After writing a frame you encoded yourself, call `flush(timestampUs: ...)` with the same timestamp so the catalog advertises your jitter. Skip it for file and network imports. On a seek or pause, call `discontinuity()`, then keep timestamps moving forward and resume video on a keyframe. - **Decoded frames hold decoder buffers.** `dispose()` each frame from a `video.Consumer` promptly rather than waiting for the garbage collector, or the decoder stalls. `resize` is best effort, so read each frame's `width()` and `height()`. - **Closing.** `await moq.close()` or `session.shutdown()` drains for up to one second and reports any error. `session.cancel(code: 0)` closes at once. Finish or abort live tracks first. Awaiting cancellation of an announcement, grant, or request subscription releases its pending native read. Announcements also release their cursor; authorization and server listeners leave their shared owner usable. Application delivery requires explicitly selecting `moq-lite-07-wip` on both peers; older Lite versions (including default lite-06) and IETF wait only for transport ACKs. -- **Ending cleanly.** When the input ends, finish every track, then call `broadcast.finish()`. The returned `BroadcastFinish` keeps the broadcast announced and serving the finished tracks for `linger`, their longest max age, so a subscriber still catching up can drain them. `await finish.wait()` completes once the linger passes, and `finish.close()` closes the broadcast at once, completing the wait. `broadcast.close()` stays the abrupt end. -- **Stats.** `session.stats()` reports `rttUs`, `estimatedSendRateBps`, and the byte and packet counters (`bytesSent`, `bytesReceived`, `bytesLost`, `packetsSent`, `packetsReceived`, `packetsLost`). A field is `null` when the transport does not report it, which is not the same as zero. `version` names the negotiated protocol, and while a client session reconnects `session.failure()` says why. Before the first connect, `client.failure()` says why the dial is retrying. Hold the owned `BandwidthConsumer` returned by `session.recvBandwidth()` to receive bandwidth estimates. `peek()` reads the current estimate in bits per second and `changed()` waits for a new one. Release the consumer or call `cancel()` when finished; releasing the last consumer stops PROBE; the session and stats snapshots do not request it. Client consumers survive reconnects and report no estimate while disconnected. +- **Ending cleanly.** When the input ends, finish every track, then call `broadcast.finish()`. The returned `BroadcastFinish` keeps the broadcast announced and serving the finished tracks for `linger`, their longest max age, so a subscriber still catching up can drain them. `await finish.wait()` completes once the linger passes, and `finish.close()` closes the broadcast at once, completing the wait. `broadcast.close()` stays the abrupt end. An unreferenced `BroadcastFinish` closes the broadcast only when the garbage collector reclaims it, possibly long after, so wait on or close it rather than dropping it. +- **Stats.** `session.stats()` reports `rttUs`, `estimatedSendRateBps`, and the byte and packet counters (`bytesSent`, `bytesReceived`, `bytesLost`, `packetsSent`, `packetsReceived`, `packetsLost`). A field is `null` when the transport does not report it, which is not the same as zero. `version` names the negotiated protocol, and while a client session reconnects `session.failure()` says why. Before the first connect, `client.failure()` says why the dial is retrying. Hold the owned `BandwidthConsumer` returned by `session.recvBandwidth()` to receive bandwidth estimates. `rate()` reads the current estimate in bits per second and `rateChanged()` waits for a new one. Release the consumer or call `cancel()` when finished; releasing the last consumer stops PROBE; the session and stats snapshots do not request it. Client consumers survive reconnects and report no estimate while disconnected. - **Authorization.** On `moq-lite-07-wip` (opt in with `ConnectOptions(versions: ...)`) and on moq-transport draft-17+ with the [MoQ Auth extension](/draft/moq-auth), listen to `moq.auth().grants()` for the `Grant` the relay sent back, `null` until it answers and while a client session reconnects. `await auth().add(token: ...)` presents a fresh token without reconnecting, returns an `AuthToken` whose grant joins the union until `cancel()`, and is presented again on every reconnect. A refusal throws with the relay's code. - **Errors.** Read diagnostic payloads from `reason` and structured protocol failures from `details`. - **Disabled renditions.** Audio and video convenience decoders reject a catalog configuration with `enabled: false`. Select an enabled rendition from the latest catalog before opening a decoder. diff --git a/doc/lib/go/index.md b/doc/lib/go/index.md index f930bb8c89e..2b9073716a9 100644 --- a/doc/lib/go/index.md +++ b/doc/lib/go/index.md @@ -109,7 +109,7 @@ video, _ := moqvideo.NewProducer(catalogProducer, ) _ = video.Write(moqvideo.Frame{Timestamp: time.Duration(pts) * time.Microsecond, Data: rgba}) epoch := moq.MintEpoch() // a fresh epoch per run -_ = broadcast.Announce(moq.Route{Epoch: &epoch}) +_ = broadcast.Announce(moq.AnnounceRoute{Epoch: &epoch}) _ = audio.Finish() _ = video.Finish() broadcast.Close() // keep the producer reachable while publishing, then close explicitly @@ -128,8 +128,8 @@ shows which module holds each. - **Live encoder timing.** After writing a frame you encoded yourself, call `Flush(timestamp)` with the same timestamp so the catalog advertises your jitter. Skip it for file and network imports. On a seek or pause, call `Discontinuity()`, then keep timestamps moving forward and resume video on a keyframe. - **Decoded frames hold decoder buffers.** `Close` each `VideoDecodedFrame` promptly, or the decoder stalls. `Resize` is best effort, so read each frame's `Width()` and `Height()`. - **Closing.** `client.Close()` drains for up to one second and returns an error on failure. `session.Shutdown(ctx)` does the same, and `session.Cancel(code)` closes at once. Finish or abort live tracks first. Application delivery requires explicitly selecting `moq-lite-07-wip` on both peers; older Lite versions (including default lite-06) and IETF wait only for transport ACKs. -- **Ending cleanly.** When the input ends, finish every track, then call `broadcast.Finish()`. The returned `*BroadcastFinish` keeps the broadcast announced and serving the finished tracks for `Linger()`, their longest max age, so a subscriber still catching up can drain them. `finish.Wait(ctx)` returns once the linger passes; ending `ctx`, or `finish.Close()`, closes the broadcast at once. `broadcast.Close()` stays the abrupt end. -- **Stats.** `Session().Stats()` reports `RTT`, `EstimatedSendRateBps`, and the byte and packet counters (`BytesSent`, `BytesReceived`, `BytesLost`, `PacketsSent`, `PacketsReceived`, `PacketsLost`). A field is nil when the transport does not report it, which is not the same as zero. `Version` names the negotiated protocol, and while a client session reconnects `Session().Failure()` says why. Hold the owned `BandwidthConsumer` returned by `Session().RecvBandwidth()` to receive bandwidth estimates. `Peek()` reads the current estimate in bits per second and `Changed(ctx)` waits for a new one. Calling `Close()` on the last consumer stops PROBE; the session and stats snapshots do not request it. Client consumers survive reconnects and report no estimate while disconnected. +- **Ending cleanly.** When the input ends, finish every track, then call `broadcast.Finish()`. The returned `*BroadcastFinish` keeps the broadcast announced and serving the finished tracks for `Linger()`, their longest max age, so a subscriber still catching up can drain them. `finish.Wait(ctx)` returns once the linger passes; ending `ctx`, or `finish.Close()`, closes the broadcast at once. `broadcast.Close()` stays the abrupt end. An unreferenced `*BroadcastFinish` closes the broadcast only when the garbage collector reclaims it, possibly long after, so wait on or close it rather than dropping it. +- **Stats.** `Session().Stats()` reports `RTT`, `EstimatedSendRateBps`, and the byte and packet counters (`BytesSent`, `BytesReceived`, `BytesLost`, `PacketsSent`, `PacketsReceived`, `PacketsLost`). A field is nil when the transport does not report it, which is not the same as zero. `Version` names the negotiated protocol, and while a client session reconnects `Session().Failure()` says why. Hold the owned `BandwidthConsumer` returned by `Session().RecvBandwidth()` to receive bandwidth estimates. `Rate()` reads the current estimate in bits per second and `RateChanged(ctx)` waits for a new one. Calling `Close()` on the last consumer stops PROBE; the session and stats snapshots do not request it. Client consumers survive reconnects and report no estimate while disconnected. - **Authorization.** On `moq-lite-07-wip` (opt in with `moq.WithVersions`) and on moq-transport draft-17+ with the [MoQ Auth extension](/draft/moq-auth), `Session().Auth().Grant()` is the `Grant` the relay sent back, nil until it answers and while a client session reconnects; `GrantChanged(ctx)` waits for the next one. `Add(ctx, token)` presents a fresh token without reconnecting, returns an `AuthToken` whose grant joins the union until `Close()`, and is presented again on every reconnect. A refusal returns the relay's code. - **Errors.** Read diagnostic payloads from `Reason` and structured protocol failures from `Details`. - **Disabled renditions.** Audio and video convenience decoders reject a catalog configuration with `enabled: false`. Select an enabled rendition from the latest catalog before opening a decoder. diff --git a/doc/lib/kt/index.md b/doc/lib/kt/index.md index 802721741f9..464b3e23799 100644 --- a/doc/lib/kt/index.md +++ b/doc/lib/kt/index.md @@ -80,7 +80,7 @@ Moq.connect("https://relay.example.com").use { moq -> VideoEncoderOutput(codec = VideoCodec.H264, track = "camera", bitrate = null, gop = null, kind = autoEncoder), ) video.write(VideoFrame(timestampUs = pts, data = rgba)) - broadcast.announce(Route(epoch = mintEpoch())) // a fresh epoch per run + broadcast.announce(AnnounceRoute(epoch = mintEpoch())) // a fresh epoch per run audio.finish() video.finish() broadcast.end() @@ -99,8 +99,8 @@ shows which module holds each. - **Live encoder timing.** After writing a frame you encoded yourself, call `flush(timestampUs)` with the same timestamp so the catalog advertises your jitter. Skip it for file and network imports. On a seek or pause, call `discontinuity()`, then keep timestamps moving forward and resume video on a keyframe. - **Decoded frames hold decoder buffers.** `close()` each frame from a `dev.moq.video.Consumer` promptly (or `use { }` it), or the decoder stalls. `resize` is best effort, so read each frame's `width()` and `height()`. - **Closing.** Suspending `moq.shutdown()` drains for up to one second and throws on failure. `Moq.close()`, and so `use { }`, cancels at once. Finish or abort live tracks first. Application delivery requires explicitly selecting `moq-lite-07-wip` on both peers; older Lite versions (including default lite-06) and IETF wait only for transport ACKs. -- **Ending cleanly.** When the input ends, finish every track, then call `broadcast.finish()`. The returned `BroadcastFinish` keeps the broadcast announced and serving the finished tracks for `linger`, their longest max age, so a subscriber still catching up can drain them. Suspending `finish.wait()` returns once the linger passes; cancelling its coroutine, or `finish.end()`, closes the broadcast at once. `broadcast.end()` stays the abrupt end. -- **Stats.** `session.stats()` reports `rttUs`, `estimatedSendRateBps`, and the byte and packet counters (`bytesSent`, `bytesReceived`, `bytesLost`, `packetsSent`, `packetsReceived`, `packetsLost`). A field is `null` when the transport does not report it, which is not the same as zero. `version` names the negotiated protocol, and while a client session reconnects `session.failure()` says why. Before the first connect, `client.failure()` says why the dial is retrying. Hold the owned `BandwidthConsumer` returned by `session.recvBandwidth()` to receive bandwidth estimates. `peek()` reads the current estimate in bits per second and `changed()` waits for a new one. Release the consumer or call `cancel()` when finished; releasing the last consumer stops PROBE; the session and stats snapshots do not request it. Client consumers survive reconnects and report no estimate while disconnected. +- **Ending cleanly.** When the input ends, finish every track, then call `broadcast.finish()`. The returned `BroadcastFinish` keeps the broadcast announced and serving the finished tracks for `linger`, their longest max age, so a subscriber still catching up can drain them. Suspending `finish.wait()` returns once the linger passes; cancelling its coroutine, or `finish.end()`, closes the broadcast at once. `broadcast.end()` stays the abrupt end. An unreferenced `BroadcastFinish` closes the broadcast only when the garbage collector reclaims it, possibly long after, so wait on or close it rather than dropping it. +- **Stats.** `session.stats()` reports `rttUs`, `estimatedSendRateBps`, and the byte and packet counters (`bytesSent`, `bytesReceived`, `bytesLost`, `packetsSent`, `packetsReceived`, `packetsLost`). A field is `null` when the transport does not report it, which is not the same as zero. `version` names the negotiated protocol, and while a client session reconnects `session.failure()` says why. Before the first connect, `client.failure()` says why the dial is retrying. Hold the owned `BandwidthConsumer` returned by `session.recvBandwidth()` to receive bandwidth estimates. `rate()` reads the current estimate in bits per second and `rateChanged()` waits for a new one. Release the consumer or call `cancel()` when finished; releasing the last consumer stops PROBE; the session and stats snapshots do not request it. Client consumers survive reconnects and report no estimate while disconnected. - **Authorization.** On `moq-lite-07-wip` (opt in with `ClientConfig(versions = ...)`) and on moq-transport draft-17+ with the [MoQ Auth extension](/draft/moq-auth), collect `moq.auth().grants()` for the `Grant` the relay sent back, `null` until it answers and while a client session reconnects. Suspending `auth().add(token)` presents a fresh token without reconnecting, returns an `AuthToken` whose grant joins the union until `cancel()`, and is presented again on every reconnect. A refusal throws with the relay's code. - **Errors.** Read diagnostic payloads from `reason` and structured protocol failures from `details`. - **Disabled renditions.** Audio and video convenience decoders reject a catalog configuration with `enabled: false`. Select an enabled rendition from the latest catalog before opening a decoder. diff --git a/doc/lib/py/index.md b/doc/lib/py/index.md index abeb6c4a8ce..e0e780e3d36 100644 --- a/doc/lib/py/index.md +++ b/doc/lib/py/index.md @@ -93,7 +93,7 @@ async def main(): status = moq.json.SnapshotProducer(broadcast, broadcast.publish_track("status"), compression=True) status.update({"state": "live", "viewers": 42}) - broadcast.announce(moq.Route(epoch=moq.mint_epoch())) # a fresh epoch per run + broadcast.announce(moq.AnnounceRoute(epoch=moq.mint_epoch())) # a fresh epoch per run audio.finish() video.finish() events.finish() @@ -115,7 +115,7 @@ shows which module holds each. - **Decoded frames hold decoder buffers.** Drop each frame from a `moq.video.Consumer` promptly, or the decoder stalls. `resize` is best effort, so read each frame's `width()` and `height()`. - **Closing.** `await session.shutdown()` drains for up to one second and raises on failure. `cancel(code)` closes at once. Finish or abort live tracks first. Application delivery requires explicitly selecting `moq-lite-07-wip` on both peers; older Lite versions (including default lite-06) and IETF wait only for transport ACKs. - **Ending cleanly.** When the input ends, finish every track, then call `broadcast.finish()`. The returned `BroadcastFinish` keeps the broadcast announced and serving the finished tracks for `linger`, their longest max age, so a subscriber still catching up can drain them. `await finish.wait()` returns once the linger passes; cancelling it, or `finish.close()`, closes the broadcast at once. `broadcast.close()` stays the abrupt end. -- **Stats.** `session.stats()` reports `rtt`, `estimated_send_rate_bps`, and the byte and packet counters (`bytes_sent`, `bytes_received`, `bytes_lost`, `packets_sent`, `packets_received`, `packets_lost`). A field is `None` when the transport does not report it, which is not the same as zero. `version` names the negotiated protocol, and while a client session reconnects `session.failure()` says why. Before the first connect, `client.failure()` says why the dial is retrying. Hold the owned `Bandwidth.Consumer` returned by `session.recv_bandwidth()` to receive bandwidth estimates. `peek()` reads the current estimate in bits per second and `changed()` waits for a new one. Release the consumer or call `cancel()` when finished; releasing the last consumer stops PROBE; the session and stats snapshots do not request it. Client consumers survive reconnects and report no estimate while disconnected. +- **Stats.** `session.stats()` reports `rtt`, `estimated_send_rate_bps`, and the byte and packet counters (`bytes_sent`, `bytes_received`, `bytes_lost`, `packets_sent`, `packets_received`, `packets_lost`). A field is `None` when the transport does not report it, which is not the same as zero. `version` names the negotiated protocol, and while a client session reconnects `session.failure()` says why. Before the first connect, `client.failure()` says why the dial is retrying. Hold the owned `Bandwidth.Consumer` returned by `session.recv_bandwidth()` to receive bandwidth estimates. `rate()` reads the current estimate in bits per second and `rate_changed()` waits for a new one. Release the consumer or call `cancel()` when finished; releasing the last consumer stops PROBE; the session and stats snapshots do not request it. Client consumers survive reconnects and report no estimate while disconnected. - **Authorization.** On `moq-lite-07-wip` (opt in with `versions=`) and on moq-transport draft-17+ with the [MoQ Auth extension](/draft/moq-auth), `session.auth().grant()` is the `Grant` the relay sent back, `None` until it answers and while a client session reconnects; `await auth.grant_changed()` waits for the next one. `await auth.add(token)` presents a fresh token without reconnecting, returns an `AuthToken` whose grant joins the union until `cancel()`, and is presented again on every reconnect. A refusal raises with the relay's code. - **Errors.** Read diagnostic payloads from `reason` and structured protocol failures from `details`. - **Disabled renditions.** Audio and video convenience decoders reject a catalog configuration with `enabled: false`. Select an enabled rendition from the latest catalog before opening a decoder. diff --git a/doc/lib/swift/index.md b/doc/lib/swift/index.md index 9af56d5a1da..008aeb9c346 100644 --- a/doc/lib/swift/index.md +++ b/doc/lib/swift/index.md @@ -80,7 +80,7 @@ let video = try Video.Producer( output: VideoEncoderOutput(codec: .h264, track: "camera", kind: .auto) ) try video.write(VideoFrame(timestampUs: pts, data: rgba)) -try broadcast.announce(route: Route(epoch: mintEpoch())) // a fresh epoch per run +try broadcast.announce(route: AnnounceRoute(epoch: mintEpoch())) // a fresh epoch per run try audio.finish() try video.finish() try broadcast.close() @@ -99,7 +99,7 @@ shows which module holds each. - **Decoded frames hold decoder buffers.** Release each frame from a `Video.Consumer` promptly, or the decoder stalls. `resize` is best effort, so read each frame's `width()` and `height()`. - **Closing.** `try await session.shutdown()` drains for up to one second and throws on failure. `session.cancel(code:)` closes at once. Finish or abort live tracks first. Application delivery requires explicitly selecting `moq-lite-07-wip` on both peers; older Lite versions (including default lite-06) and IETF wait only for transport ACKs. - **Ending cleanly.** When the input ends, finish every track, then call `try broadcast.finish()`. The returned `BroadcastFinish` keeps the broadcast announced and serving the finished tracks for `lingerUs`, their longest max age, so a subscriber still catching up can drain them. `try await finish.wait()` returns once the linger passes; cancelling its task, or `finish.close()`, closes the broadcast at once. `broadcast.close()` stays the abrupt end. -- **Stats.** `session.stats()` reports `rttUs`, `estimatedSendRateBps`, and the byte and packet counters (`bytesSent`, `bytesReceived`, `bytesLost`, `packetsSent`, `packetsReceived`, `packetsLost`). A field is `nil` when the transport does not report it, which is not the same as zero. `version` names the negotiated protocol, and while a client session reconnects `session.failure()` says why. Before the first connect, `client.failure()` says why the dial is retrying. Hold the owned `Bandwidth.Consumer` returned by `session.recvBandwidth()` to receive bandwidth estimates. `peek()` reads the current estimate in bits per second and `changed()` waits for a new one. Release the consumer or call `cancel()` when finished; releasing the last consumer stops PROBE; the session and stats snapshots do not request it. Client consumers survive reconnects and report no estimate while disconnected. +- **Stats.** `session.stats()` reports `rttUs`, `estimatedSendRateBps`, and the byte and packet counters (`bytesSent`, `bytesReceived`, `bytesLost`, `packetsSent`, `packetsReceived`, `packetsLost`). A field is `nil` when the transport does not report it, which is not the same as zero. `version` names the negotiated protocol, and while a client session reconnects `session.failure()` says why. Before the first connect, `client.failure()` says why the dial is retrying. Hold the owned `Bandwidth.Consumer` returned by `session.recvBandwidth()` to receive bandwidth estimates. `rate()` reads the current estimate in bits per second and `rateChanged()` waits for a new one. Release the consumer or call `cancel()` when finished; releasing the last consumer stops PROBE; the session and stats snapshots do not request it. Client consumers survive reconnects and report no estimate while disconnected. - **Authorization.** On `moq-lite-07-wip` (opt in with `Client(versions:)`) and on moq-transport draft-17+ with the [MoQ Auth extension](/draft/moq-auth), `session.auth().grant()` is the `Grant` the relay sent back, `nil` until it answers and while a client session reconnects; `try await auth.grantChanged()` waits for the next one. `try await auth.add(token:)` presents a fresh token without reconnecting, returns an `AuthToken` whose grant joins the union until `cancel()`, and is presented again on every reconnect. A refusal throws with the relay's code. - **Errors.** Diagnostic cases use the `reason:` argument label; match their associated value to inspect it. Protocol errors carry `details`. - **Disabled renditions.** Audio and video convenience decoders reject a catalog configuration with `enabled: false`. Select an enabled rendition from the latest catalog before opening a decoder. diff --git a/go/ffi/MIGRATE.md b/go/ffi/MIGRATE.md index 8b8121ffdd1..325b963e8c5 100644 --- a/go/ffi/MIGRATE.md +++ b/go/ffi/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `announce` takes a `MoqAnnounceRoute` + +`MoqBroadcastProducer.announce` takes `MoqAnnounceRoute` (`cost`, `epoch`) instead of a `MoqRoute`: the origin builds the hop chain, so `hops` is no longer an input. +`MoqRoute` is only what announcements report, and the `InvalidRoute` error is gone. + ### A broadcast reference waits for its sibling's announcement (#6254) Resolving a rendition's `broadcast` reference, through `MoqBroadcastConsumer.Resolve` and the rendition-based subscribes and fetches, now waits for a sibling nothing announces yet instead of failing as unroutable, so a late sibling plays once it is announced. @@ -28,7 +33,7 @@ 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. +`MoqBroadcastConsumer.Resolve` takes the catalog `MoqRendition` instead of its `reference`, and reads its `broadcast` and `epoch` from it. 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. diff --git a/go/wrapper/MIGRATE.md b/go/wrapper/MIGRATE.md index b76850ea132..9db258a3ce0 100644 --- a/go/wrapper/MIGRATE.md +++ b/go/wrapper/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `Announce` takes an `AnnounceRoute` + +`BroadcastProducer.Announce` takes `moq.AnnounceRoute{Cost, Epoch}` instead of a `moq.Route`: the origin builds the hop chain, so `Hops` is no longer an input, and `ErrInvalidRoute` is gone. +`Route` is only what announcements report. To re-announce an observed route, copy its `Cost` and `Epoch`. + ### A broadcast reference waits for its sibling's announcement (#6254) Resolving a rendition's `broadcast` reference, through `BroadcastConsumer.Resolve` and the rendition-based subscribes and fetches, now waits for a sibling nothing announces yet instead of failing as unroutable, so a late sibling plays once it is announced. @@ -36,7 +41,7 @@ Pass the new `media.ContainerFormatCmaf` to keep publishing CMAF. ### Catalog references carry their epoch `ffi.MoqVideo` and `ffi.MoqAudio` gain `Epoch`, the publisher instance their `Broadcast` reference names. -`BroadcastConsumer.Resolve(ctx, reference)` becomes `Resolve(ctx, reference, epoch)`; pass the rendition's `Epoch` alongside its `Broadcast`, or `nil` to resolve whichever instance answers. +`BroadcastConsumer.Resolve(ctx, reference)` becomes `Resolve(ctx, rendition)`, taking the catalog rendition (`media.VideoRendition` or `media.AudioRendition`) and reading its `Broadcast` and `Epoch` from it. Resolving, and the rendition-based consumers, error for a rendition whose broadcast is proven to serve another epoch. ### Timestamps stop at 2^61 - 1 microseconds (#6168) @@ -87,7 +92,7 @@ A subscription now reads only the groups its own `Ranges` cover. `TrackDemand.IsUsed`, `Used`, and `Unused` are gone. `Wanted()` returns nil while nobody subscribes, otherwise the group subscribers want next, and `WantedChanged(ctx)` waits for it to change (the first call returns at once). Loop on `WantedChanged` until it returns non-nil where you called `Used`, and nil where you called `Unused`. -`TrackInfo` gains `Refresh`. `GroupDemand` is unchanged. +`TrackInfo` gains `Refresh`. ### `CongestionControl` gained `CongestionControlNone` (#5926) @@ -107,7 +112,7 @@ To start at group `n`, pass one range starting at the absolute anchor for group ### 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. +Hold the consumer `Session.RecvBandwidth` returns to request PROBE, read it with `Rate()` or wait with `RateChanged(ctx)`, and `Close()` it to stop. It returns `nil` when the version has no PROBE. ### `AnnouncedBroadcast.Available` waits for the next publisher instance (#5700) diff --git a/go/wrapper/bandwidth_test.go b/go/wrapper/bandwidth_test.go index a81b537de4c..c1c1c85d634 100644 --- a/go/wrapper/bandwidth_test.go +++ b/go/wrapper/bandwidth_test.go @@ -22,10 +22,8 @@ func TestReservationUpdateReleasesSubscribedTrack(t *testing.T) { t.Fatal(err) } defer client.Close() - bandwidth := client.Session().Bandwidth() // Grant assertions use a live QUIC estimate, but need no traffic or probing. - off := uint64(0) - bandwidth.SetTarget(&off) + bandwidth := client.Session().Bandwidth() broadcast, err := moq.NewBroadcastProducer() if err != nil { t.Fatal(err) diff --git a/go/wrapper/doc.go b/go/wrapper/doc.go index e8294ca06e9..8d1f9ca6bcc 100644 --- a/go/wrapper/doc.go +++ b/go/wrapper/doc.go @@ -25,7 +25,7 @@ // // What a cancel tears down depends on the call. A one-shot call (a subscribe, a // fetch, RequestBroadcast, Resolve, a TrackDemand's WantedChanged, a -// GroupDemand's Used/Unused, Server.Accept) aborts on its own and leaves the +// Server.Accept) aborts on its own and leaves the // object it was made on usable, so the same // broadcast, producer, or server takes the next call. A stream read (any Next, // RecvGroup, ReadFrame, or the iterators over them) instead cancels the stream diff --git a/go/wrapper/errors.go b/go/wrapper/errors.go index 2dfb8be0d63..f873990bcb4 100644 --- a/go/wrapper/errors.go +++ b/go/wrapper/errors.go @@ -77,8 +77,6 @@ var ( ErrUnsupported = ffi.ErrMoqErrorUnsupported // ErrAlreadyCommitted is returned when a track already reading one way (arrival order, sequence order, or a typed reader) is asked to read another; subscribe again for a second cursor. ErrAlreadyCommitted = ffi.ErrMoqErrorAlreadyCommitted - // ErrInvalidRoute is returned when a route has an invalid hop ID or too many hops. - ErrInvalidRoute = ffi.ErrMoqErrorInvalidRoute // ErrInvalidEpoch is returned when an epoch is not a lowercase hyphenated UUIDv7. ErrInvalidEpoch = ffi.ErrMoqErrorInvalidEpoch // ErrInvalidPattern is returned when a path pattern is empty, has a doubled slash, or uses a reserved segment form. diff --git a/go/wrapper/errors_test.go b/go/wrapper/errors_test.go index 81684840d66..84262a5c93b 100644 --- a/go/wrapper/errors_test.go +++ b/go/wrapper/errors_test.go @@ -83,7 +83,6 @@ func TestErrorSentinels(t *testing.T) { {"Expired", ffi.NewMoqErrorExpired(), moq.ErrExpired}, {"NotFound", ffi.NewMoqErrorNotFound(), moq.ErrNotFound}, {"Unsupported", ffi.NewMoqErrorUnsupported(), moq.ErrUnsupported}, - {"InvalidRoute", ffi.NewMoqErrorInvalidRoute(""), moq.ErrInvalidRoute}, {"InvalidPattern", ffi.NewMoqErrorInvalidPattern(""), moq.ErrInvalidPattern}, {"InvalidEpoch", ffi.NewMoqErrorInvalidEpoch(""), moq.ErrInvalidEpoch}, {"UnresolvableBroadcast", ffi.NewMoqErrorUnresolvableBroadcast(""), moq.ErrUnresolvableBroadcast}, diff --git a/go/wrapper/example_test.go b/go/wrapper/example_test.go index c23b00e8db4..d780987db19 100644 --- a/go/wrapper/example_test.go +++ b/go/wrapper/example_test.go @@ -59,7 +59,7 @@ func ExampleClient_CreateBroadcast() { if err != nil { log.Fatal(err) } - _ = broadcast.Announce(moq.Route{}) + _ = broadcast.Announce(moq.AnnounceRoute{}) pts := 20 * time.Millisecond if err := media.WriteFrame(moq.Frame{Payload: []byte("opus frame"), Timestamp: &pts}); err != nil { diff --git a/go/wrapper/moq_test.go b/go/wrapper/moq_test.go index 2c458e0bc6d..7b9aca7e53e 100644 --- a/go/wrapper/moq_test.go +++ b/go/wrapper/moq_test.go @@ -291,7 +291,7 @@ func TestAudioConsumerDelay(t *testing.T) { if err != nil { t.Fatal(err) } - if err := broadcast.Announce(moq.Route{}); err != nil { + if err := broadcast.Announce(moq.AnnounceRoute{}); err != nil { t.Fatal(err) } @@ -439,7 +439,7 @@ func testDecodeVideoFrame(t *testing.T, surface bool) { if err != nil { t.Fatal(err) } - if err := broadcast.Announce(moq.Route{}); err != nil { + if err := broadcast.Announce(moq.AnnounceRoute{}); err != nil { t.Fatal(err) } @@ -593,13 +593,6 @@ func TestFetchGroupAndServeDynamicMiss(t *testing.T) { if request.Sequence() != 7 || request.Priority() != 11 { t.Fatalf("unexpected request: sequence=%d priority=%d", request.Sequence(), request.Priority()) } - demand, err := request.Demand() - if err != nil { - t.Fatal(err) - } - if demand.Sequence() != 7 || !demand.IsUsed() { - t.Fatalf("unexpected demand: sequence=%d used=%v", demand.Sequence(), demand.IsUsed()) - } produced, err := request.Accept() if err != nil { t.Fatal(err) @@ -645,7 +638,7 @@ func TestLocalPublishConsumeAudio(t *testing.T) { if err != nil { t.Fatal(err) } - if err := broadcast.Announce(moq.Route{}); err != nil { + if err := broadcast.Announce(moq.AnnounceRoute{}); err != nil { t.Fatal(err) } @@ -1432,7 +1425,7 @@ func TestBroadcastIsReachableOnlyWhileAnnounced(t *testing.T) { t.Fatal("an unannounced broadcast must be unroutable") } - if err := broadcast.Announce(moq.Route{Cost: 3}); err != nil { + if err := broadcast.Announce(moq.AnnounceRoute{Cost: 3}); err != nil { t.Fatal(err) } announced, err := consumer.Announced(moq.AnnounceOptions{}) @@ -1456,7 +1449,7 @@ func TestBroadcastIsReachableOnlyWhileAnnounced(t *testing.T) { t.Fatal("an unannounced broadcast must be unroutable") } - if err := broadcast.Announce(moq.Route{}); err != nil { + if err := broadcast.Announce(moq.AnnounceRoute{}); err != nil { t.Fatal(err) } nextAnnounced(t, ctx, announced) @@ -1484,14 +1477,14 @@ func TestAnnouncedPatternCaptures(t *testing.T) { if err != nil { t.Fatal(err) } - if err := audio.Announce(moq.Route{}); err != nil { + if err := audio.Announce(moq.AnnounceRoute{}); err != nil { t.Fatal(err) } chat, err := origin.CreateBroadcast("room/alice/chat") if err != nil { t.Fatal(err) } - if err := chat.Announce(moq.Route{}); err != nil { + if err := chat.Announce(moq.AnnounceRoute{}); err != nil { t.Fatal(err) } @@ -1522,7 +1515,7 @@ func TestAnnouncedExactFilterCapturesEmpty(t *testing.T) { if err != nil { t.Fatal(err) } - if err := chat.Announce(moq.Route{}); err != nil { + if err := chat.Announce(moq.AnnounceRoute{}); err != nil { t.Fatal(err) } @@ -1555,7 +1548,7 @@ func TestAnnouncedEpochRestarts(t *testing.T) { if at, err := moq.EpochTime("018bcfe5-6800-7000-8000-000000000000"); err != nil || !at.Equal(time.UnixMilli(1_700_000_000_000)) { t.Fatalf("EpochTime = %v, %v; want 2023-11-14T22:13:20Z", at, err) } - if err := broadcast.Announce(moq.Route{Epoch: &first}); err != nil { + if err := broadcast.Announce(moq.AnnounceRoute{Epoch: &first}); err != nil { t.Fatal(err) } if start := nextAnnounced(t, ctx, announced); start.Route.Epoch == nil || *start.Route.Epoch != first { @@ -1563,7 +1556,7 @@ func TestAnnouncedEpochRestarts(t *testing.T) { } second := moq.MintEpoch() - if err := broadcast.Announce(moq.Route{Epoch: &second}); err != nil { + if err := broadcast.Announce(moq.AnnounceRoute{Epoch: &second}); err != nil { t.Fatal(err) } restart, ok := nextRoute(t, ctx, announced).(moq.AnnounceEventRestart) diff --git a/go/wrapper/publish.go b/go/wrapper/publish.go index 3df32231205..7add3e867f6 100644 --- a/go/wrapper/publish.go +++ b/go/wrapper/publish.go @@ -41,7 +41,7 @@ func (b *BroadcastProducer) Dynamic() (*BroadcastDynamic, error) { // // Announcing again re-prices the route in place. Until announced, the // broadcast is invisible and unroutable for local consumers and peers alike. -func (b *BroadcastProducer) Announce(route Route) error { +func (b *BroadcastProducer) Announce(route AnnounceRoute) error { return b.inner.Announce(route) } @@ -97,7 +97,8 @@ func (b *BroadcastProducer) Finish() (*BroadcastFinish, error) { // BroadcastFinish is a cleanly ended broadcast, still announced and serving its // finished tracks. Closing it, or cancelling a [BroadcastFinish.Wait], closes -// the broadcast at once. +// the broadcast at once. An unreferenced one closes the broadcast only when the +// garbage collector reclaims it, possibly long after, so wait on or close it. type BroadcastFinish struct { inner *ffi.MoqBroadcastFinish } @@ -389,15 +390,6 @@ func (r *GroupRequest) Priority() int8 { return r.inner.Priority() } -// Demand returns a watch-only handle to whether any caller still wants the group. -func (r *GroupRequest) Demand() (*GroupDemand, error) { - inner, err := r.inner.Demand() - if err != nil { - return nil, err - } - return &GroupDemand{inner: inner}, nil -} - // Accept accepts the request and returns a producer for the group. func (r *GroupRequest) Accept() (*GroupProducer, error) { inner, err := r.inner.Accept() @@ -411,33 +403,3 @@ func (r *GroupRequest) Accept() (*GroupProducer, error) { func (r *GroupRequest) Abort(errorCode uint16) error { return r.inner.Abort(errorCode) } - -// GroupDemand watches the callers waiting on a requested group. -// -// It is weak: holding it does not keep the request alive. The last caller to -// leave withdraws the request, so once unused, demand never returns: drop the -// request. Waits fail with ErrClosed after the request is accepted or dropped, -// and keep the rejection reason after it is rejected. -type GroupDemand struct { - inner *ffi.MoqGroupDemand -} - -// Sequence is the sequence of the group this watches. -func (d *GroupDemand) Sequence() uint64 { - return d.inner.Sequence() -} - -// IsUsed reports whether the group has at least one waiting caller right now. -func (d *GroupDemand) IsUsed() bool { - return d.inner.IsUsed() -} - -// Used blocks until the group has at least one waiting caller. -func (d *GroupDemand) Used(ctx context.Context) error { - return d.inner.Used(ctx) -} - -// Unused blocks until the group has no waiting callers. -func (d *GroupDemand) Unused(ctx context.Context) error { - return d.inner.Unused(ctx) -} diff --git a/go/wrapper/reconnect_test.go b/go/wrapper/reconnect_test.go index 6b550d43c89..03678a46c80 100644 --- a/go/wrapper/reconnect_test.go +++ b/go/wrapper/reconnect_test.go @@ -191,7 +191,7 @@ func TestReconnectAcrossRelayRestart(t *testing.T) { t.Fatal(err) } defer track.Finish() - if err := broadcast.Announce(moq.Route{}); err != nil { + if err := broadcast.Announce(moq.AnnounceRoute{}); err != nil { t.Fatal(err) } diff --git a/go/wrapper/session.go b/go/wrapper/session.go index cd810498c7f..80053038451 100644 --- a/go/wrapper/session.go +++ b/go/wrapper/session.go @@ -151,14 +151,6 @@ func (b *Bandwidth) Reserve(demand *TrackDemand, maxBps uint64) *Reservation { return &Reservation{inner: b.inner.Reserve(demand.inner, maxBps)} } -// SetTarget probes up to targetBps instead of the sum of the reservations. The -// session probes for what its reservations add up to by default, so the -// estimate can rise to fund them. 0 turns probing off, and nil follows the -// reservations again. -func (b *Bandwidth) SetTarget(targetBps *uint64) { - b.inner.SetTarget(targetBps) -} - // Reservation is one track's standing claim on a Bandwidth. // // Grant is a snapshot: nil means no estimate or no demand, so hold the current @@ -222,12 +214,12 @@ func (s *Session) RecvBandwidth() *BandwidthConsumer { // BandwidthConsumer owns a bandwidth estimate, in bits per second. type BandwidthConsumer struct{ inner *ffi.MoqBandwidthConsumer } -// Peek reads the current estimate, or nil while unavailable. -func (b *BandwidthConsumer) Peek() *uint64 { return b.inner.Peek() } +// Rate is the current estimate, or nil while unavailable. +func (b *BandwidthConsumer) Rate() *uint64 { return b.inner.Rate() } -// Changed waits for the estimate to change, or its producer to close. -func (b *BandwidthConsumer) Changed(ctx context.Context) (*uint64, error) { - return bridge.Call(ctx, b.inner.Cancel, b.inner.Changed) +// RateChanged waits for the estimate to change and returns the new Rate, or fails once its producer closes. +func (b *BandwidthConsumer) RateChanged(ctx context.Context) (*uint64, error) { + return bridge.Call(ctx, b.inner.Cancel, b.inner.RateChanged) } // Close releases this consumer's demand, including a pending wait. diff --git a/go/wrapper/subscribe.go b/go/wrapper/subscribe.go index d077dd03280..c9088458dfe 100644 --- a/go/wrapper/subscribe.go +++ b/go/wrapper/subscribe.go @@ -46,19 +46,20 @@ func (b *BroadcastConsumer) FetchGroup( return &GroupConsumer{inner: inner}, nil } -// Resolve returns the broadcast serving a catalog rendition, honoring its broadcast -// reference: ffi.MoqVideo.Broadcast / ffi.MoqAudio.Broadcast. A nil or empty reference names this -// broadcast, anything else names a sibling relative to it (e.g. "./source"). epoch is the same -// rendition's Epoch: a broadcast proven to serve another instance errors instead of resolving, -// and a route without an epoch cannot prove one, so there it is best effort. Call it on a -// rendition that carries a reference before SubscribeTrack or FetchGroup, which take a track name -// rather than a rendition: the rendition's Track, or its ID when nil. The rendition-based -// media.NewTrackConsumer, media.FetchGroup, audio.Subscribe, and video.Subscribe resolve both themselves. +// Resolve returns the broadcast serving a catalog rendition, built with +// media.VideoRendition or media.AudioRendition. The rendition's Broadcast reference, nil or +// empty, names this broadcast; anything else names a sibling relative to it (e.g. "./source"). +// Its Epoch is the publisher instance the reference names: a broadcast proven to serve another +// instance errors instead of resolving, and a route without an epoch cannot prove one, so there +// it is best effort. Call it on a rendition that carries a reference before SubscribeTrack or +// FetchGroup, which take a track name rather than a rendition: the rendition's Track, or its ID +// when nil. The rendition-based media.NewTrackConsumer, media.FetchGroup, audio.Subscribe, and +// video.Subscribe resolve it themselves. // // Errors if this broadcast came from a local producer rather than an origin, since a // standalone broadcast has no sibling to name. -func (b *BroadcastConsumer) Resolve(ctx context.Context, reference *string, epoch *string) (*BroadcastConsumer, error) { - inner, err := b.inner.Resolve(ctx, reference, epoch) +func (b *BroadcastConsumer) Resolve(ctx context.Context, rendition ffi.MoqRendition) (*BroadcastConsumer, error) { + inner, err := b.inner.Resolve(ctx, rendition) if err != nil { return nil, err } diff --git a/go/wrapper/types.go b/go/wrapper/types.go index c55269463c2..1e1f32e68af 100644 --- a/go/wrapper/types.go +++ b/go/wrapper/types.go @@ -35,7 +35,9 @@ type ( ProtocolKind = ffi.MoqProtocolKind // OriginConfig configures a new origin, such as its maximum cache size in bytes. OriginConfig = ffi.MoqOriginConfig - // Route is the publisher Epoch a broadcast is announced under (nil when unknown), the hop chain it takes to reach an origin, and its static production and link cost (lower wins). + // AnnounceRoute is how a broadcast advertises its exact path: its static production Cost (lower wins) and the publisher Epoch it serves (nil when unknown). + AnnounceRoute = ffi.MoqAnnounceRoute + // Route is an observed route: the publisher Epoch it serves (nil when unknown), the hop chain it took to reach an origin, and its static production and link cost (lower wins). Route = ffi.MoqRoute // Announce is a route over a prefix: the origin-relative Prefix, what each filter // wildcard matched (nil Captures for a route that only overlaps the scope), and the @@ -194,7 +196,7 @@ func LogLevel(level string) error { } // MintEpoch mints a fresh publisher epoch from the wall clock and secure randomness, -// ordered newest last. Mint one per publisher run and announce it in Route.Epoch, so +// ordered newest last. Mint one per publisher run and announce it in AnnounceRoute.Epoch, so // viewers see a restart as a new broadcast instead of a stalled one. func MintEpoch() string { return ffi.MoqMintEpoch() diff --git a/kt/moq-ffi/MIGRATE.md b/kt/moq-ffi/MIGRATE.md index 26889124bb9..5b1d29aed88 100644 --- a/kt/moq-ffi/MIGRATE.md +++ b/kt/moq-ffi/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `announce` takes a `MoqAnnounceRoute` + +`MoqBroadcastProducer.announce` takes `MoqAnnounceRoute` (`cost`, `epoch`) instead of a `MoqRoute`: the origin builds the hop chain, so `hops` is no longer an input. +`MoqRoute` is only what announcements report, and the `InvalidRoute` error is gone. + ### A broadcast reference waits for its sibling's announcement (#6254) Resolving a rendition's `broadcast` reference, through `MoqBroadcastConsumer.resolve` and the rendition-based subscribes and fetches, now waits for a sibling nothing announces yet instead of failing as unroutable, so a late sibling plays once it is announced. diff --git a/kt/moq/MIGRATE.md b/kt/moq/MIGRATE.md index 995d262f220..717cf625e20 100644 --- a/kt/moq/MIGRATE.md +++ b/kt/moq/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `announce` takes an `AnnounceRoute` + +`BroadcastProducer.announce` takes `AnnounceRoute(cost, epoch)` instead of a `Route`: the origin builds the hop chain, so `hops` is no longer an input. +`Route` is only what announcements report. To re-announce an observed route, copy its `cost` and `epoch`. + ### A broadcast reference waits for its sibling's announcement (#6254) Resolving a rendition's `broadcast` reference in the rendition-based subscribes and fetches now waits for a sibling nothing announces yet instead of failing as unroutable, so a late sibling plays once it is announced. @@ -36,7 +41,7 @@ Pass the new `ContainerFormat.CMAF` to keep publishing CMAF. ### Catalog references carry their epoch `Video` and `Audio` gain `epoch`, the publisher instance their `broadcast` reference names; it defaults to `null`, so only positional construction changes. -`BroadcastConsumer.resolve(reference)` becomes `resolve(reference, epoch)`; pass the rendition's `epoch` alongside its `broadcast`, or `null` to resolve whichever instance answers. +`BroadcastConsumer.resolve(reference)` becomes `resolve(rendition)`, taking the catalog `Rendition` and reading its `broadcast` and `epoch` from it. Resolving, and the rendition-based consumers, throw `MoqException.Mux` for a rendition whose broadcast is proven to serve another epoch. ### Timestamps stop at 2^61 - 1 microseconds (#6168) @@ -82,7 +87,7 @@ A subscription now reads only the groups its own `ranges` cover. `TrackDemand.isUsed()`, `used()`, and `unused()` are gone. `wanted()` returns null while nobody subscribes, otherwise the group subscribers want next, and `wantedChanged()` waits for it to change (the first call returns at once). Loop on `wantedChanged()` until it returns non-null where you called `used()`, and null where you called `unused()`. -`TrackInfo` gains `refresh`. `GroupDemand` is unchanged. +`TrackInfo` gains `refresh`. ### `CongestionControl` gained `NONE` (#5926) @@ -102,8 +107,8 @@ To start at group `n`, pass one range starting at the absolute anchor for group ### 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()`. +Hold the consumer `Session.recvBandwidth` returns to request PROBE, read it with `rate()` or wait with `rateChanged()`, and `cancel()` it to stop. +Replace `stats().estimatedRecvRateBps` with `recvBandwidth()?.rate()`. ### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt index 016772c6ba9..84da35126c6 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt @@ -89,8 +89,6 @@ typealias TrackDemand = uniffi.moq.MoqTrackDemand typealias TrackConsumer = uniffi.moq.MoqTrackConsumer /** A request to produce one uncached group for a fetch consumer. */ typealias GroupRequest = uniffi.moq.MoqGroupRequest -/** A watch-only handle to whether any caller still wants a requested group; holding it keeps nothing open. */ -typealias GroupDemand = uniffi.moq.MoqGroupDemand /** The write side of a single group: append frames to it. */ typealias GroupProducer = uniffi.moq.MoqGroupProducer /** The read side of a single group: yields timestamped raw frames. */ @@ -101,7 +99,9 @@ typealias GroupConsumer = uniffi.moq.MoqGroupConsumer typealias Datagram = uniffi.moq.MoqDatagram /** A payload plus the timestamp it should be presented at. */ typealias Frame = uniffi.moq.MoqFrame -/** A path-prefix route: the publisher epoch it serves (null when unknown), relay hop ids (oldest first), and static production and link cost (lower wins). */ +/** How a broadcast advertises its exact path: its static production cost (lower wins) and the publisher epoch it serves (null when unknown). */ +typealias AnnounceRoute = uniffi.moq.MoqAnnounceRoute +/** An observed path-prefix route: the publisher epoch it serves (null when unknown), relay hop ids (oldest first), and static production and link cost (lower wins). */ typealias Route = uniffi.moq.MoqRoute /** Tunes how a track subscription is delivered: priority, latency budget, ranges, and group ordering. */ typealias Subscription = uniffi.moq.MoqSubscription diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Client.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Client.kt index 750d62f62c7..33a92c6abe0 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Client.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Client.kt @@ -1,13 +1,5 @@ package dev.moq -/** - * The settings a default [ClientConfig] dials with, for a settings UI to show. - * - * Knobs whose default depends on the backend (`tls.systemRoots`, `quic.gso`, - * `quic.mtuDiscovery`, `quic.congestionControl`) stay null. - */ -fun clientDefaults(): ClientConfig = uniffi.moq.moqClientDefaults() - /** The protocol versions offered by default, most preferred first, spelled the way [ClientConfig.versions] takes them. */ fun versions(): List = uniffi.moq.moqVersions() diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Epoch.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Epoch.kt index d68c9678f3f..6417bdbe744 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Epoch.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Epoch.kt @@ -4,7 +4,7 @@ import kotlin.time.Instant /** * Mint a fresh publisher epoch from the wall clock and secure randomness, ordered - * newest last. Mint one per publisher run and announce it in [Route.epoch], so + * newest last. Mint one per publisher run and announce it in [AnnounceRoute.epoch], so * viewers see a restart as a new broadcast instead of a stalled one. */ fun mintEpoch(): String = uniffi.moq.moqMintEpoch() diff --git a/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt b/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt index cb250c62126..457076a341b 100644 --- a/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt +++ b/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt @@ -145,7 +145,7 @@ class SmokeTest { /** A settings UI reads its defaults and menus from the library rather than repeating them. */ @Test fun `client defaults and versions come from the library`() { - val defaults = clientDefaults() + val defaults = uniffi.moq.moqClientDefaults() assertNotNull(defaults.quic.maxStreams) assertNotNull(defaults.quic.idleTimeout) assertNotNull(defaults.timeout) @@ -501,7 +501,7 @@ class SmokeTest { assertEquals(64, fingerprints[0].length) server.createBroadcast("live").use { broadcast -> - broadcast.announce(Route()) + broadcast.announce(AnnounceRoute()) broadcast.unannounce() } } @@ -527,7 +527,7 @@ class SmokeTest { val consumer = origin.consume() assertFailsWith { consumer.requestBroadcast("live") } - broadcast.announce(Route()) + broadcast.announce(AnnounceRoute()) val announced = consumer.announced(AnnounceConfig()) val first = assertIs(announced.nextRoute()) assertEquals("live", first.announce.prefix) @@ -537,7 +537,7 @@ class SmokeTest { assertEquals("live", retracted.announce.prefix) assertFailsWith { consumer.requestBroadcast("live") } - broadcast.announce(Route()) + broadcast.announce(AnnounceRoute()) assertIs(announced.nextRoute()) consumer.requestBroadcast("live") } @@ -551,12 +551,12 @@ class SmokeTest { origin.createBroadcast("epoch").use { broadcast -> val first = mintEpoch() assertEquals(Instant.fromEpochMilliseconds(1_700_000_000_000), epochTime("018bcfe5-6800-7000-8000-000000000000")) - broadcast.announce(Route(epoch = first)) + broadcast.announce(AnnounceRoute(epoch = first)) val start = assertIs(announced.nextRoute()) assertEquals(first, start.announce.route.epoch) val second = mintEpoch() - broadcast.announce(Route(epoch = second)) + broadcast.announce(AnnounceRoute(epoch = second)) val restart = assertIs(announced.nextRoute()) assertEquals(second, restart.announce.route.epoch) @@ -570,7 +570,7 @@ class SmokeTest { OriginProducer(OriginConfig()).use { origin -> val announced = origin.consume().announced(AnnounceConfig(prefix = "room", filter = "*/chat")) origin.createBroadcast("room/alice/chat").use { broadcast -> - broadcast.announce(Route()) + broadcast.announce(AnnounceRoute()) val update = assertIs(announced.nextRoute()) assertEquals("room/alice/chat", update.announce.prefix) assertEquals(listOf("alice"), update.announce.captures) @@ -697,7 +697,7 @@ class SmokeTest { AudioEncoderInput(format = AudioSampleFormat.F32, sampleRate = 48_000u, channels = 1u), AudioEncoderOutput(codec = AudioCodec.opus(), track = "mic"), ) - broadcast.announce(Route()) + broadcast.announce(AnnounceRoute()) val consumer = origin.consume().requestBroadcast("audio-delay") val catalog = CatalogConsumer.subscribe(consumer).next()!! @@ -738,7 +738,7 @@ class SmokeTest { VideoEncoderOutput(codec = VideoCodec.H264, track = "camera", kind = softwareEncoder), null, ) - broadcast.announce(Route()) + broadcast.announce(AnnounceRoute()) // Seed the track so a subscriber joining below lands on encoded media. val rgba = ByteArray(320 * 240 * 4) { 0x80.toByte() } diff --git a/py/moq-ffi/MIGRATE.md b/py/moq-ffi/MIGRATE.md index 44d3a96c07a..e8e6f819959 100644 --- a/py/moq-ffi/MIGRATE.md +++ b/py/moq-ffi/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `announce` takes a `MoqAnnounceRoute` + +`MoqBroadcastProducer.announce` takes `MoqAnnounceRoute` (`cost`, `epoch`) instead of a `MoqRoute`: the origin builds the hop chain, so `hops` is no longer an input. +`MoqRoute` is only what announcements report, and the `InvalidRoute` error is gone. + ### A broadcast reference waits for its sibling's announcement (#6254) Resolving a rendition's `broadcast` reference, through `MoqBroadcastConsumer.resolve()` and the rendition-based subscribes and fetches, now waits for a sibling nothing announces yet instead of failing as unroutable, so a late sibling plays once it is announced. @@ -21,7 +26,7 @@ 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. +`MoqBroadcastConsumer.resolve` takes the catalog `MoqRendition` instead of its `reference`, and reads its `broadcast` and `epoch` from it. 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. diff --git a/py/moq-ffi/tests/exit_inside_callback.py b/py/moq-ffi/tests/exit_inside_callback.py index a519a519641..6fbe52f04d6 100644 --- a/py/moq-ffi/tests/exit_inside_callback.py +++ b/py/moq-ffi/tests/exit_inside_callback.py @@ -55,7 +55,7 @@ def _preempted_continuation(future_ptr, poll_code): async def main() -> None: origin = moq_ffi.MoqOriginProducer(moq_ffi.MoqOriginConfig()) broadcast = origin.create_broadcast("exit") - broadcast.announce(moq_ffi.MoqRoute()) + broadcast.announce(moq_ffi.MoqAnnounceRoute()) track = broadcast.publish_track("data", moq_ffi.MoqTrackInfo(timescale=1_000_000)) consumer = await origin.consume().request_broadcast("exit") diff --git a/py/moq-rs/MIGRATE.md b/py/moq-rs/MIGRATE.md index b8ec8a192fa..3c22a3b0cc9 100644 --- a/py/moq-rs/MIGRATE.md +++ b/py/moq-rs/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `announce` takes an `AnnounceRoute` + +`BroadcastProducer.announce` takes `moq.AnnounceRoute(cost=..., epoch=...)` instead of a `Route`: the origin builds the hop chain, so `hops` is no longer an input. +`Route` is only what announcements report. To re-announce an observed route, copy its `cost` and `epoch`. + ### A broadcast reference waits for its sibling's announcement (#6254) Resolving a rendition's `broadcast` reference, through `BroadcastConsumer.resolve()` and the rendition-based subscribes and fetches, now waits for a sibling nothing announces yet instead of failing as unroutable, so a late sibling plays once it is announced. @@ -36,7 +41,7 @@ Pass the new `ContainerFormat.CMAF` to keep publishing CMAF. ### Catalog references carry their epoch `Video` and `Audio` renditions gain `epoch`, the publisher instance their `broadcast` reference names. -`BroadcastConsumer.resolve(reference)` becomes `resolve(reference, epoch)`; pass the rendition's `epoch` alongside its `broadcast`, or `None` to resolve whichever instance answers. +`BroadcastConsumer.resolve(reference)` becomes `resolve(id, rendition)`, taking the catalog rendition the reference belongs to and reading its `broadcast` and `epoch` from it. Resolving, and the rendition-based consumers, raise for a rendition whose broadcast is proven to serve another epoch. ### Timestamps stop at 2^61 - 1 microseconds (#6168) @@ -81,7 +86,7 @@ A subscription now reads only the groups its own `ranges` cover. `TrackDemand.is_used()`, `used()`, and `unused()` are gone. `wanted()` returns `None` while nobody subscribes, otherwise the group subscribers want next, and `await wanted_changed()` waits for it to change (the first call returns at once). Loop on `wanted_changed()` until it returns non-`None` where you awaited `used()`, and `None` where you awaited `unused()`. -`TrackInfo` gains `refresh`. `GroupDemand` is unchanged. +`TrackInfo` gains `refresh`. ### `CongestionControl` gained `NONE` (#5926) @@ -101,11 +106,11 @@ To start at group `n`, pass one range starting at the absolute anchor for group ### 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. +Hold the consumer `Session.recv_bandwidth` returns to request PROBE, read it with `rate()` or wait with `rate_changed()`, and drop it to stop. ```python bandwidth = session.recv_bandwidth() -rate = bandwidth.peek() if bandwidth else None +rate = bandwidth.rate() if bandwidth else None ``` ### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) diff --git a/py/moq-rs/README.md b/py/moq-rs/README.md index 1b37695547c..6cd705eb2f2 100644 --- a/py/moq-rs/README.md +++ b/py/moq-rs/README.md @@ -71,7 +71,7 @@ async def main(): audio.cut() # A fresh epoch per run tells viewers a restart is a new broadcast. - broadcast.announce(moq.Route(epoch=moq.mint_epoch())) + broadcast.announce(moq.AnnounceRoute(epoch=moq.mint_epoch())) # Clean up audio.finish() diff --git a/py/moq-rs/docs/index.md b/py/moq-rs/docs/index.md index 64e1347d2dd..ad8364aa5ca 100644 --- a/py/moq-rs/docs/index.md +++ b/py/moq-rs/docs/index.md @@ -63,7 +63,6 @@ asyncio.run(main()) TrackRequest GroupProducer GroupRequest - GroupDemand ``` ## Subscribing @@ -221,6 +220,7 @@ Rust side ([`moq-ffi`](https://crates.io/crates/moq-ffi)). Subscription TrackInfo FetchGroupOptions + AnnounceRoute Route ConnectionStats ``` diff --git a/py/moq-rs/moq/__init__.py b/py/moq-rs/moq/__init__.py index 25530525006..883f1554055 100644 --- a/py/moq-rs/moq/__init__.py +++ b/py/moq-rs/moq/__init__.py @@ -34,7 +34,6 @@ BroadcastDynamic, BroadcastFinish, BroadcastProducer, - GroupDemand, GroupProducer, GroupRequest, TrackDemand, @@ -51,6 +50,7 @@ ) from .types import ( Anchor, + AnnounceRoute, AudioCodec, AudioEncoderInput, AudioSampleFormat, @@ -87,6 +87,7 @@ "AnnounceEventRestart", "AnnounceEventStart", "AnnounceEventUpdate", + "AnnounceRoute", "AudioCodec", "AudioEncoderInput", "AudioSampleFormat", @@ -113,7 +114,6 @@ "FetchGroupOptions", "Grant", "GroupConsumer", - "GroupDemand", "GroupRequest", "GroupProducer", "OriginConsumer", diff --git a/py/moq-rs/moq/epoch.py b/py/moq-rs/moq/epoch.py index cab5657a446..745b7e6d69e 100644 --- a/py/moq-rs/moq/epoch.py +++ b/py/moq-rs/moq/epoch.py @@ -10,7 +10,7 @@ def mint_epoch() -> str: """Mint a fresh epoch from the wall clock and secure randomness, ordered newest last. - Mint one per publisher run and announce it with ``Route(epoch=...)``, so a + Mint one per publisher run and announce it with ``AnnounceRoute(epoch=...)``, so a restart reads as a new broadcast to viewers instead of a stalled one. """ return moq_mint_epoch() diff --git a/py/moq-rs/moq/publish.py b/py/moq-rs/moq/publish.py index 57ba12e828c..79db88f262f 100644 --- a/py/moq-rs/moq/publish.py +++ b/py/moq-rs/moq/publish.py @@ -11,7 +11,6 @@ MoqBroadcastDynamic, MoqBroadcastFinish, MoqBroadcastProducer, - MoqGroupDemand, MoqGroupProducer, MoqGroupRequest, MoqTrackDemand, @@ -23,8 +22,8 @@ from ._records import _from_us, _subscription, _track_info from ._stream import _Stream from .types import ( + AnnounceRoute, Frame, - Route, Subscription, TrackInfo, ) @@ -193,37 +192,6 @@ def abort(self, error_code: int) -> None: self._inner.abort(error_code) -class GroupDemand: - """A watch-only handle to the callers waiting on a requested group. - - Returned by ``GroupRequest.demand()``. Weak: holding it does not keep the - request alive. The last caller to leave withdraws the request, so once - unused, demand never returns: drop the request. Waits raise - ``moq.Error.Closed`` after the request is accepted or dropped, and keep - the rejection reason after it is rejected. - """ - - def __init__(self, inner: MoqGroupDemand) -> None: - self._inner = inner - - @property - def sequence(self) -> int: - """The sequence of the group this watches.""" - return self._inner.sequence() - - def is_used(self) -> bool: - """Whether the group has at least one waiting caller right now.""" - return self._inner.is_used() - - async def used(self) -> None: - """Wait until the group has at least one waiting caller.""" - await self._inner.used() - - async def unused(self) -> None: - """Wait until the group has no waiting callers.""" - await self._inner.unused() - - class GroupRequest: """A request to produce one uncached group for a fetch consumer.""" @@ -240,10 +208,6 @@ def priority(self) -> int: """The consumer's delivery priority for this fetch.""" return self._inner.priority() - def demand(self) -> GroupDemand: - """A watch-only handle to whether any caller still wants this group.""" - return GroupDemand(self._inner.demand()) - def accept(self) -> GroupProducer: """Accept the request and return a producer for the group.""" return GroupProducer(self._inner.accept()) @@ -318,13 +282,13 @@ def dynamic(self) -> BroadcastDynamic: """Accept subscriptions to tracks that are not published yet.""" return BroadcastDynamic(self._inner.dynamic()) - def announce(self, route: Route | None = None) -> None: + def announce(self, route: AnnounceRoute | None = None) -> None: """Advertise this broadcast's exact path as a route. Announcing again re-prices the route in place. Until announced, the broadcast is invisible and unroutable for local consumers and peers alike. """ - self._inner.announce(route if route is not None else Route()) + self._inner.announce(route if route is not None else AnnounceRoute()) def unannounce(self) -> None: """Retract this broadcast's advertisement, if any, from local consumers and peers alike.""" diff --git a/py/moq-rs/moq/session.py b/py/moq-rs/moq/session.py index e1782c99579..d3e3f86d602 100644 --- a/py/moq-rs/moq/session.py +++ b/py/moq-rs/moq/session.py @@ -198,13 +198,13 @@ class Consumer: def __init__(self, inner: MoqBandwidthConsumer) -> None: self._inner = inner - def peek(self) -> int | None: - """Read the current estimate, or None while unavailable.""" - return self._inner.peek() + def rate(self) -> int | None: + """The current estimate, or None while unavailable.""" + return self._inner.rate() - async def changed(self) -> int | None: - """Wait for the estimate to change, raising when its producer closes.""" - return await self._inner.changed() + async def rate_changed(self) -> int | None: + """Wait for the estimate to change and return the new rate, raising when its producer closes.""" + return await self._inner.rate_changed() def cancel(self) -> None: """Release this consumer's demand, including a pending wait.""" @@ -222,15 +222,6 @@ def reserve(self, demand: TrackDemand, max_bps: int) -> Reservation: """ return Reservation(self._inner.reserve(demand._inner, max_bps)) - def set_target(self, target_bps: int | None) -> None: - """Probe up to ``target_bps`` instead of the sum of the reservations. - - The session probes for what its reservations add up to by default, so - the estimate can rise to fund them. ``0`` turns probing off, and - ``None`` follows the reservations again. - """ - self._inner.set_target(target_bps) - class Reservation: """One track's standing claim on a :class:`Bandwidth`. diff --git a/py/moq-rs/moq/subscribe.py b/py/moq-rs/moq/subscribe.py index aaa6747d4fc..b13089cad24 100644 --- a/py/moq-rs/moq/subscribe.py +++ b/py/moq-rs/moq/subscribe.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import AsyncIterator +from typing import TYPE_CHECKING from moq_ffi import ( MoqBroadcastConsumer, @@ -21,6 +22,9 @@ TrackInfo, ) +if TYPE_CHECKING: + from .media import Audio, Video + def _frame(frame: MoqFrame | None) -> Frame | None: return None if frame is None else Frame._from_ffi(frame) @@ -167,22 +171,22 @@ async def fetch_group( """ return GroupConsumer(await self._inner.fetch_group(name, sequence, options)) - async def resolve(self, reference: str | None, epoch: str | None) -> "BroadcastConsumer": - """Resolve a catalog rendition's ``broadcast`` reference to the broadcast - serving its track. - - ``reference`` is ``catalog.video[name].broadcast`` / - ``catalog.audio[name].broadcast``: ``None`` or empty names this broadcast, - anything else names a sibling relative to it (e.g. ``./source``). ``epoch`` is - the same rendition's ``epoch``: a broadcast proven to serve another instance - raises instead of resolving, and a route without an epoch cannot prove one, so - there it is best effort. Call it on a rendition that carries a reference before - :meth:`subscribe_track` or :meth:`fetch_group`, which take a track name rather - than a rendition: the rendition's ``track``, or its ID when unset. The - rendition-based consumers in the media, audio, and video namespaces resolve - both themselves. + async def resolve(self, id: str, rendition: Video | Audio) -> "BroadcastConsumer": + """Resolve the catalog rendition ``id`` to the broadcast serving its track. + + The rendition's ``broadcast`` reference, ``None`` or empty, names this + broadcast; anything else names a sibling relative to it (e.g. ``./source``). + Its ``epoch`` is the publisher instance the reference names: a broadcast + proven to serve another instance raises instead of resolving, and a route + without an epoch cannot prove one, so there it is best effort. Call it on a + rendition that carries a reference before :meth:`subscribe_track` or + :meth:`fetch_group`, which take a track name rather than a rendition: the + rendition's ``track``, or ``id`` when unset. The rendition-based consumers in + the media, audio, and video namespaces resolve it themselves. Raises if this broadcast came from a local producer rather than an origin, since a standalone broadcast has no sibling to name. """ - return BroadcastConsumer(await self._inner.resolve(reference, epoch)) + from .media import _rendition + + return BroadcastConsumer(await self._inner.resolve(_rendition(id, rendition))) diff --git a/py/moq-rs/moq/types.py b/py/moq-rs/moq/types.py index 262b69d98ee..9c7333ea9a6 100644 --- a/py/moq-rs/moq/types.py +++ b/py/moq-rs/moq/types.py @@ -6,6 +6,9 @@ from moq_ffi import ( MoqAnchor as Anchor, ) +from moq_ffi import ( + MoqAnnounceRoute as AnnounceRoute, +) from moq_ffi import ( MoqAudioCodec as AudioCodec, ) @@ -67,6 +70,7 @@ from ._records import Backoff, ConnectionStats, Datagram, Frame, Grant, Subscription, TrackInfo __all__ = [ + "AnnounceRoute", "Anchor", "AudioCodec", "AudioEncoderInput", diff --git a/py/moq-rs/tests/test_local.py b/py/moq-rs/tests/test_local.py index c3318208144..022e163df76 100644 --- a/py/moq-rs/tests/test_local.py +++ b/py/moq-rs/tests/test_local.py @@ -390,13 +390,13 @@ async def test_announced_epoch_restarts(): first = moq.mint_epoch() fixed = datetime(2023, 11, 14, 22, 13, 20, tzinfo=timezone.utc) assert moq.epoch_time("018bcfe5-6800-7000-8000-000000000000") == fixed - broadcast.announce(moq.Route(epoch=first)) + broadcast.announce(moq.AnnounceRoute(epoch=first)) start = await next_route(announced) assert isinstance(start, moq.AnnounceEventStart) assert start.announce.route.epoch == first second = moq.mint_epoch() - broadcast.announce(moq.Route(epoch=second)) + broadcast.announce(moq.AnnounceRoute(epoch=second)) restart = await next_route(announced) assert isinstance(restart, moq.AnnounceEventRestart) assert restart.announce.route.epoch == second @@ -406,7 +406,7 @@ async def test_announced_epoch_restarts(): with pytest.raises(OverflowError): moq.epoch_time("ffffffff-ffff-7000-8000-000000000000") with pytest.raises(moq.Error.InvalidEpoch): # type: ignore[attr-defined] - broadcast.announce(moq.Route(epoch="not-an-epoch")) + broadcast.announce(moq.AnnounceRoute(epoch="not-an-epoch")) broadcast.close() @@ -518,9 +518,6 @@ async def test_fetch_group_and_serve_dynamic_miss(): request = await asyncio.wait_for(dynamic.requested_group(), timeout=5.0) assert request.sequence == 7 assert request.priority == 11 - demand = request.demand() - assert demand.sequence == 7 - assert demand.is_used() produced = request.accept() produced.write_frame(b"archive", timedelta(microseconds=140_000)) diff --git a/py/moq-rs/tests/test_server.py b/py/moq-rs/tests/test_server.py index c88e146bdf8..1606ad3c44a 100644 --- a/py/moq-rs/tests/test_server.py +++ b/py/moq-rs/tests/test_server.py @@ -443,12 +443,12 @@ async def test_route_update_observes_restart(): """A route metadata update arrives as an AnnounceEventUpdate. The publisher re-prices its announced route; the subscriber observes the new - hop chain in place (no retraction), and unannouncing retracts it. + cost in place (no retraction), and unannouncing retracts it. """ origin = moq.OriginProducer() async with moq.Server("127.0.0.1:0", tls_generate=["localhost"], publish=origin) as server: announce = origin.create_broadcast("routed") - announce.announce(moq.Route(hops=[42])) + announce.announce(moq.AnnounceRoute(cost=1)) serve_task = asyncio.create_task(server.serve()) try: @@ -461,15 +461,14 @@ async def test_route_update_observes_restart(): first = await next_route(announced) assert isinstance(first, moq.AnnounceEventStart) assert first.announce.prefix == "routed" - assert 42 in first.announce.route.hops - assert 77 not in first.announce.route.hops + first_cost = first.announce.route.cost - # The publisher advertises a longer chain: an in-place update. - announce.announce(moq.Route(hops=[42, 77])) + # The publisher raises its cost: an in-place update. + announce.announce(moq.AnnounceRoute(cost=5)) updated = await next_route(announced) assert isinstance(updated, moq.AnnounceEventUpdate) assert updated.announce.prefix == "routed" - assert 77 in updated.announce.route.hops + assert updated.announce.route.cost == first_cost + 4 # Unannouncing retracts the route. announce.unannounce() diff --git a/quest/m1/api-audit/README.md b/quest/m1/api-audit/README.md index 76cb4f8980b..37769e20bbf 100644 --- a/quest/m1/api-audit/README.md +++ b/quest/m1/api-audit/README.md @@ -80,7 +80,6 @@ Rejected in planning: - [Session limits only where needed](/quest/m1/api-audit/session-limits.md) - `session::Limits` keeps only bounds that nothing else enforces, and its documented example compiles - [Ranges refuse what the wire drops](/quest/m1/api-audit/ranges-wire.md) - relative and time anchors fail loud where a wire cannot carry them, `floor()` honors the subscription, and bindings take an optional max delay - [Derived origins borrow](/quest/m1/api-audit/derived-origin-close.md) - closing a JS `scope()` or `withPreference()` handle releases only that handle -- [Binding surface trim](/quest/m1/api-audit/binding-trim.md) - route input is separate from route observation, watch verbs are uniform, and unused binding objects are gone - [Wrapper config names](/quest/m1/api-audit/wrapper-config.md) - Python, Go, and Dart client options use the FFI's names and nesting - [One decode read](/quest/m1/api-audit/decode-read.md) - audio and video decoders yield the same `Frame`, `Gap`, `End` read as moq-mux containers - [Watch and publish trim](/quest/m1/api-audit/watch-trim.md) - the clock is re-exported, unused watch outputs are gone, and derived publish state is read-only diff --git a/quest/m1/api-audit/binding-trim.md b/quest/m1/api-audit/binding-trim.md deleted file mode 100644 index e7991fd211e..00000000000 --- a/quest/m1/api-audit/binding-trim.md +++ /dev/null @@ -1,32 +0,0 @@ -# [S] Binding surface trim - -## Goal - -moq-ffi takes route input separately from route observation, uses one watch -verb pattern, and ships no objects without a consumer. - -## Plan - -Decided 2026-10-22 (API audit): - -- `MoqRoute` (`rs/moq-ffi/src/origin.rs`) is both an announce input and an - observation. Its writable `anonymous` is derived from `hops` on read and - ignored on input, and `hops` is writable on announce, so a local publisher - can fabricate a hop chain. Announce takes `MoqAnnounceRoute { cost, epoch }`; - `MoqRoute` stays observation-only with `anonymous` derived. Fix the doc that - pairs a route with `MoqOriginProducer::dynamic`, which takes none since - #5678. -- One watch pattern, `x()` and `x_changed()`: `MoqBandwidthConsumer::peek`/ - `changed` become `rate`/`rate_changed`; `MoqGroupDemand`'s - `is_used`/`used`/`unused` go with it. -- Remove `MoqGroupRequest::demand` and `MoqGroupDemand`; nothing beyond - wrapper pass-throughs uses them. -- `MoqBroadcastConsumer::resolve(reference, epoch)` takes two adjacent - `Option`s; take `MoqRendition`, as the rendition consumers do. -- Document that a dropped `MoqBroadcastFinish` closes the broadcast, which a - garbage-collected language (Kotlin, Go, Dart) does late. -- moq-c: `moq_bandwidth_set_target` has no consumer beyond the README; keep - it only if [native rung disable](/quest/m1/simulcast-rung-disable.md) uses - it. Align `moq_client_defaults` wrapping across languages. - -Public API: moq-ffi, moq-c, wrappers breaking. Wire: none. diff --git a/rs/moq-c/README.md b/rs/moq-c/README.md index 57779201691..43d5ee36ff4 100644 --- a/rs/moq-c/README.md +++ b/rs/moq-c/README.md @@ -35,7 +35,6 @@ moq_client_config moq_client_defaults(void); int32_t moq_session_close(uint32_t session); int32_t moq_session_bandwidth(uint32_t session); int32_t moq_bandwidth_reserve(uint32_t bandwidth, uint32_t track, uint64_t max_bps); -int32_t moq_bandwidth_set_target(uint32_t bandwidth, uint64_t target_bps, bool present); int32_t moq_bandwidth_close(uint32_t bandwidth); int32_t moq_reservation_grant(uint32_t reservation, uint64_t *bps, bool *present); int32_t moq_reservation_update(uint32_t reservation, uint64_t max_bps); diff --git a/rs/moq-c/src/bandwidth.rs b/rs/moq-c/src/bandwidth.rs index 37bc90b121a..2ee23a081a0 100644 --- a/rs/moq-c/src/bandwidth.rs +++ b/rs/moq-c/src/bandwidth.rs @@ -70,21 +70,6 @@ pub extern "C" fn moq_bandwidth_close(bandwidth: u32) -> i32 { }) } -/// Probe up to `target_bps` instead of the sum of the reservations. -/// -/// The session probes for what its reservations add up to by default, so the -/// estimate can rise to fund them. A `target_bps` of 0 turns probing off, and -/// `present` false follows the reservations again. -#[unsafe(no_mangle)] -pub extern "C" fn moq_bandwidth_set_target(bandwidth: u32, target_bps: u64, present: bool) -> i32 { - ffi::enter(move || { - let bandwidth = ffi::parse_id(bandwidth)?; - let allocator = State::lock().bandwidth.allocator(bandwidth)?; - allocator.set_target(present.then_some(moq_net::bandwidth::Rate::from_bps(target_bps))); - Ok(()) - }) -} - /// Reserve up to `max_bps` for `track`, returning a reservation handle. /// /// `max_bps` is a ceiling, not a measurement: reserve the most the track can diff --git a/rs/moq-ffi/MIGRATE.md b/rs/moq-ffi/MIGRATE.md index 63a09626841..a28d1fbdda2 100644 --- a/rs/moq-ffi/MIGRATE.md +++ b/rs/moq-ffi/MIGRATE.md @@ -5,6 +5,12 @@ These are the UniFFI names; each language wrapper has its own `MIGRATE.md` in it ## Unreleased +### `announce` takes a `MoqAnnounceRoute` + +`MoqBroadcastProducer::announce` takes `MoqAnnounceRoute { cost, epoch }` instead of a `MoqRoute`: the origin builds the hop chain, so a publisher no longer passes `hops` or `anonymous`. +`MoqRoute` is only what `MoqAnnounceConsumer` reports, so its fields lose their defaults, and `MoqError::InvalidRoute` is gone. +To re-announce an observed route, copy its `cost` and `epoch`. + ### A broadcast reference waits for its sibling's announcement (#6254) Resolving a rendition's `broadcast` reference, through `MoqBroadcastConsumer::resolve` and the rendition-based subscribes and fetches, now waits for a sibling nothing announces yet instead of failing as unroutable, so a late sibling plays once it is announced. @@ -37,7 +43,7 @@ Pass the new `MoqContainerFormat::Cmaf` to keep publishing CMAF. ### Catalog references carry their epoch `MoqVideo` and `MoqAudio` gain `epoch`, the publisher instance their `broadcast` reference names. It defaults to null in every generated binding; a Rust struct literal adds `epoch: None`. -`MoqBroadcastConsumer::resolve` takes the rendition's `epoch` after `reference`; pass it alongside, or `None` to resolve whichever instance answers. +`MoqBroadcastConsumer::resolve` takes the catalog `MoqRendition` instead of its `reference`, and reads the reference and its `epoch` from it. Resolving, and the rendition-based consumers, fail a rendition whose broadcast is proven to serve another epoch with `Mux`. ### Timestamps stop at 2^61 - 1 microseconds (#6168) @@ -89,7 +95,6 @@ A subscription now reads only the groups its own `ranges` cover, rather than sta `wanted()` returns null 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 non-null where you awaited `used()`, and null where you awaited `unused()`. `MoqTrackInfo` gains `refresh`, the ratio past which a joining subscriber asks for a new group. -`MoqGroupDemand` is unchanged. ### `MoqCongestionControl` gained `None` (#5926) @@ -114,11 +119,11 @@ A start group `n` is `MoqRange { start: MoqAnchor::Absolute { group: n, frame: 0 ### 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. +Hold the consumer `MoqSession::recv_bandwidth` returns to request PROBE, read it with `rate()` or wait with `rate_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()); +let rate = bandwidth.as_ref().and_then(|b| b.rate()); ``` ### `available()` waits for the next publisher instance (#5700) diff --git a/rs/moq-ffi/src/bandwidth.rs b/rs/moq-ffi/src/bandwidth.rs index f01b576bed1..74f8d079768 100644 --- a/rs/moq-ffi/src/bandwidth.rs +++ b/rs/moq-ffi/src/bandwidth.rs @@ -6,8 +6,7 @@ use std::sync::Arc; use crate::demand::MoqTrackDemand; use crate::error::MoqError; -/// Divides one connection's send estimate among the tracks sharing it, and sets -/// what the session probes for. +/// Divides one connection's send estimate among the tracks sharing it. /// /// Minted by [`MoqSession::bandwidth`](crate::session::MoqSession::bandwidth). /// Clones share one reservation registry, so two handles from the same session @@ -48,16 +47,6 @@ impl MoqBandwidth { let _guard = crate::ffi::enter(); self.reserve_demand(demand.inner(), max_bps) } - - /// Probe up to `target_bps` instead of the sum of the reservations, or `None` to follow them again. - /// - /// The session probes for what its reservations add up to by default, so the - /// estimate can rise to fund them. `Some(0)` turns probing off. - pub fn set_target(&self, target_bps: Option) { - let _guard = crate::ffi::enter(); - self.allocator - .set_target(target_bps.map(moq_net::bandwidth::Rate::from_bps)); - } } /// One track's standing claim on a [`MoqBandwidth`]. @@ -271,8 +260,8 @@ impl MoqBandwidthConsumer { #[uniffi::export] impl MoqBandwidthConsumer { - /// Read the current estimate in bits per second, or None while unavailable. - pub fn peek(&self) -> Option { + /// The current estimate in bits per second, or None while unavailable. + pub fn rate(&self) -> Option { self.inner .lock() .unwrap() @@ -281,8 +270,9 @@ impl MoqBandwidthConsumer { .map(moq_net::bandwidth::Rate::as_bps) } - /// Wait for the estimate to change, returning an error when its producer closes. - pub async fn changed(&self) -> Result, MoqError> { + /// Wait for the estimate to change and return the new [`rate`](Self::rate), erroring when + /// its producer closes. + pub async fn rate_changed(&self) -> Result, MoqError> { self.changed .run(|_guard| async move { moq_net::kio::wait(|waiter| { @@ -315,12 +305,12 @@ mod consumer_tests { }; #[tokio::test] - async fn owned_consumer_peeks_while_waiting_and_cancel_releases_demand() { + async fn owned_consumer_reads_while_waiting_and_cancel_releases_demand() { tokio::time::pause(); let producer = moq_net::bandwidth::Producer::new(); let consumer = MoqBandwidthConsumer::new(producer.consume()); assert!(producer.is_used()); - let mut changed = std::pin::pin!(consumer.changed()); + let mut changed = std::pin::pin!(consumer.rate_changed()); assert!( changed .as_mut() @@ -328,9 +318,9 @@ mod consumer_tests { .is_pending() ); producer.set(Some(moq_net::bandwidth::Rate::from_bps(123))).unwrap(); - assert_eq!(consumer.peek(), Some(123)); + assert_eq!(consumer.rate(), Some(123)); assert_eq!(changed.await.unwrap(), Some(123)); - let mut changed = std::pin::pin!(consumer.changed()); + let mut changed = std::pin::pin!(consumer.rate_changed()); assert!( changed .as_mut() @@ -339,7 +329,7 @@ mod consumer_tests { ); consumer.cancel(); assert!(!producer.is_used()); - assert_eq!(consumer.peek(), None); + assert_eq!(consumer.rate(), None); assert!(matches!(changed.await, Err(MoqError::Cancelled))); } @@ -358,6 +348,6 @@ mod consumer_tests { let producer = moq_net::bandwidth::Producer::new(); let consumer = MoqBandwidthConsumer::new(producer.consume()); producer.abort(moq_net::Error::Cancel).unwrap(); - assert!(matches!(consumer.changed().await, Err(MoqError::Cancelled))); + assert!(matches!(consumer.rate_changed().await, Err(MoqError::Cancelled))); } } diff --git a/rs/moq-ffi/src/consumer.rs b/rs/moq-ffi/src/consumer.rs index 9300d72effb..4b34bdae9d6 100644 --- a/rs/moq-ffi/src/consumer.rs +++ b/rs/moq-ffi/src/consumer.rs @@ -256,26 +256,23 @@ impl MoqBroadcastConsumer { #[uniffi::export] impl MoqBroadcastConsumer { - /// Resolve a catalog rendition's `broadcast` reference to the broadcast serving its track. + /// Resolve a catalog rendition to the broadcast serving its track. /// - /// `reference` is [`MoqVideo::broadcast`] / [`MoqAudio::broadcast`]: absent or empty names - /// this broadcast, anything else names a sibling relative to it (e.g. `./source`). `epoch` is - /// the same rendition's [`MoqVideo::epoch`] / [`MoqAudio::epoch`], the publisher instance the - /// reference names: a broadcast proven to serve another one fails with `Mux` instead of - /// resolving, while a route without an epoch cannot prove one, so there it is best effort. - /// Call it on a rendition that carries a reference before [`Self::subscribe_track`] or - /// [`Self::fetch_group`], which take a track name rather than a rendition. The rendition-based - /// consumers (`MoqMediaTrackConsumer::subscribe`, `MoqMediaGroupConsumer::fetch`, - /// `MoqVideoConsumer::subscribe`, `MoqAudioConsumer::subscribe`) resolve both themselves. + /// The rendition's `broadcast` reference, absent or empty, names this broadcast; anything + /// else names a sibling relative to it (e.g. `./source`). Its `epoch` is the publisher + /// instance the reference names: a broadcast proven to serve another one fails with `Mux` + /// instead of resolving, while a route without an epoch cannot prove one, so there it is best + /// effort. Call it on a rendition that carries a reference before [`Self::subscribe_track`] or + /// [`Self::fetch_group`], which take the rendition's `track` (or its ID when unset) rather + /// than a rendition. The rendition-based consumers (`MoqMediaTrackConsumer::subscribe`, + /// `MoqMediaGroupConsumer::fetch`, `MoqVideoConsumer::subscribe`, + /// `MoqAudioConsumer::subscribe`) resolve it themselves. /// /// Errors if this broadcast came from a local producer rather than an origin, since a /// standalone broadcast has no sibling to name. A sibling nothing announces yet is waited for, /// so a late one resolves once it is announced; cancel the call to give up on it. - pub async fn resolve( - &self, - reference: Option, - epoch: Option, - ) -> Result, MoqError> { + pub async fn resolve(&self, rendition: MoqRendition) -> Result, MoqError> { + let (reference, epoch, _, _) = rendition.into_parts(); let broadcast = self.resolve_inner(reference.as_deref(), epoch.as_deref()).await?; Ok(Arc::new(match self.origin.clone() { Some(origin) => Self::routed(broadcast, origin, self.driver.clone()), diff --git a/rs/moq-ffi/src/demand.rs b/rs/moq-ffi/src/demand.rs index f06fa74d663..c5808271a23 100644 --- a/rs/moq-ffi/src/demand.rs +++ b/rs/moq-ffi/src/demand.rs @@ -1,4 +1,4 @@ -//! Watching what subscribers want of a published track, or whether a requested group is still wanted. +//! Watching what subscribers want of a published track. use std::sync::Arc; @@ -63,52 +63,10 @@ impl MoqTrackDemand { } } -/// A watch-only handle to the callers waiting on a requested group. +/// Report a track released without an abort as [`MoqError::Closed`]. /// -/// Weak: holding it does not keep the request alive. The last caller to leave withdraws the -/// request, and a later fetch of the group queues a fresh one, so once unused, demand never -/// returns: drop the request. Waits fail with `Closed` after the request is accepted or dropped, -/// and keep the rejection reason after it is rejected. -#[derive(uniffi::Object)] -pub struct MoqGroupDemand { - inner: moq_net::group::Demand, -} - -impl MoqGroupDemand { - pub(crate) fn new(inner: moq_net::group::Demand) -> Arc { - Arc::new(Self { inner }) - } -} - -#[uniffi::export] -impl MoqGroupDemand { - /// The sequence of the group this watches. - pub fn sequence(&self) -> u64 { - self.inner.sequence() - } - - /// Whether the group has at least one waiting caller right now, without waiting. - pub fn is_used(&self) -> bool { - self.inner.is_used() - } - - /// Wait until the group has at least one waiting caller. - pub async fn used(&self) -> Result<(), MoqError> { - let demand = self.inner.clone(); - crate::ffi::detached(async move { gone(demand.used().await) }).await - } - - /// Wait until the group has no waiting callers. - pub async fn unused(&self) -> Result<(), MoqError> { - let demand = self.inner.clone(); - crate::ffi::detached(async move { gone(demand.unused().await) }).await - } -} - -/// Report a track or group released without an abort as [`MoqError::Closed`]. -/// -/// That is how a finished track or request ends, which a demand watcher expects, not the internal -/// failure `Dropped` means to a consumer. An aborted track or rejected request keeps its reason. +/// That is how a finished track ends, which a demand watcher expects, not the internal failure +/// `Dropped` means to a consumer. An aborted track keeps its reason. fn gone(result: Result) -> Result { match result { Err(moq_net::Error::Dropped) => Err(MoqError::Closed), diff --git a/rs/moq-ffi/src/error.rs b/rs/moq-ffi/src/error.rs index 5fc89ceff14..cc29d2bd5f0 100644 --- a/rs/moq-ffi/src/error.rs +++ b/rs/moq-ffi/src/error.rs @@ -339,13 +339,6 @@ pub enum MoqError { #[error("already committed to another way of reading")] AlreadyCommitted, - /// A route carried an invalid hop id or too many hops. - #[error("invalid route: {reason}")] - InvalidRoute { - /// A diagnostic message describing the failure. - reason: String, - }, - /// A path pattern was empty, had a doubled slash, or used a reserved segment form. #[error("invalid pattern: {reason}")] InvalidPattern { diff --git a/rs/moq-ffi/src/media.rs b/rs/moq-ffi/src/media.rs index f9d5ce1ac6b..0f1035f175b 100644 --- a/rs/moq-ffi/src/media.rs +++ b/rs/moq-ffi/src/media.rs @@ -1187,7 +1187,7 @@ pub enum MoqRendition { impl MoqRendition { /// Split into the broadcast reference and its epoch, the track name, and the container. - fn into_parts(self) -> (Option, Option, String, MoqContainer) { + pub(crate) fn into_parts(self) -> (Option, Option, String, MoqContainer) { let (id, broadcast, epoch, track, container) = match self { Self::Video { id, config } => (id, config.broadcast, config.epoch, config.track, config.container), Self::Audio { id, config } => (id, config.broadcast, config.epoch, config.track, config.container), diff --git a/rs/moq-ffi/src/origin.rs b/rs/moq-ffi/src/origin.rs index 6bbc6af4af4..3f0e621660c 100644 --- a/rs/moq-ffi/src/origin.rs +++ b/rs/moq-ffi/src/origin.rs @@ -29,29 +29,17 @@ pub struct MoqAnnounceConfig { pub hidden: bool, } -/// A path-prefix route: hops, a static cost, and the publisher epoch for an advertisement. +/// How a broadcast advertises its exact path: a static cost and the publisher epoch. /// -/// Pair one with `MoqBroadcastProducer::announce` for an exact path, or with -/// `MoqOriginProducer::dynamic` for a prefix. Observe them with -/// `MoqOriginConsumer::announced`. A route claims capability, not inventory: by -/// convention a publisher announces each broadcast's exact path, so subscribers -/// can enumerate broadcasts, while a service advertises a prefix and answers -/// whatever is requested beneath it. +/// Pass one to `MoqBroadcastProducer::announce`. The hop chain is the origin's to build, +/// so a local publisher cannot claim relay hops it never took. #[derive(Clone, Debug, Default, PartialEq, Eq, uniffi::Record)] -pub struct MoqRoute { - /// Hop ids of the relay hops the route traversed, oldest first. 0 is the - /// anonymous mark and is legal on a received chain. - #[uniffi(default = [])] - pub hops: Vec, +pub struct MoqAnnounceRoute { /// Preference among routes covering the same prefix: lower wins. A publisher /// sets its production cost here: zero for content it is already producing, /// larger for content it would have to start producing on demand. #[uniffi(default = 0)] pub cost: u64, - /// Whether the chain holds a 0 anywhere. An anonymous route ranks below every - /// fully identified one, whatever the costs say. - #[uniffi(default = false)] - pub anonymous: bool, /// The publisher instance the route serves, as UUIDv7 text, or `None` when unknown. /// /// A publisher that can restart sets a fresh one per run from `moq_mint_epoch`, so @@ -62,6 +50,38 @@ pub struct MoqRoute { pub epoch: Option, } +impl TryFrom for moq_net::origin::Route { + type Error = MoqError; + + fn try_from(route: MoqAnnounceRoute) -> Result { + let mut out = moq_net::origin::Route::default().with_cost(moq_net::origin::Cost::new(route.cost)); + if let Some(epoch) = route.epoch { + out = out.with_epoch(epoch.parse()?); + } + Ok(out) + } +} + +/// An observed path-prefix route: hops, a static cost, and the publisher epoch. +/// +/// Carried by `MoqAnnounceConsumer` events. A route claims capability, not +/// inventory: by convention a publisher announces each broadcast's exact path, +/// so subscribers can enumerate broadcasts, while a service advertises a prefix +/// with `MoqOriginProducer::dynamic` and answers whatever is requested beneath it. +#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)] +pub struct MoqRoute { + /// Hop ids of the relay hops the route traversed, oldest first. 0 is the + /// anonymous mark. + pub hops: Vec, + /// Preference among routes covering the same prefix: lower wins. + pub cost: u64, + /// Whether the chain holds a 0 anywhere. An anonymous route ranks below every + /// fully identified one, whatever the costs say. + pub anonymous: bool, + /// The publisher instance the route serves, as UUIDv7 text, or `None` when unknown. + pub epoch: Option, +} + impl From for MoqRoute { fn from(route: moq_net::origin::Route) -> Self { Self { @@ -73,30 +93,6 @@ impl From for MoqRoute { } } -impl TryFrom for moq_net::origin::Route { - type Error = MoqError; - - fn try_from(route: MoqRoute) -> Result { - let mut hops = moq_net::Hops::new(); - for id in route.hops { - let origin = if id == 0 { - moq_net::Hop::UNKNOWN - } else { - moq_net::Hop::new(id).map_err(|e| MoqError::InvalidRoute { reason: e.to_string() })? - }; - hops.push(origin) - .map_err(|e| MoqError::InvalidRoute { reason: e.to_string() })?; - } - let mut out = moq_net::origin::Route::default() - .with_cost(moq_net::origin::Cost::new(route.cost)) - .with_hops(hops); - if let Some(epoch) = route.epoch { - out = out.with_epoch(epoch.parse()?); - } - Ok(out) - } -} - /// Mint a fresh publisher epoch from the wall clock and secure randomness, ordered newest last. #[uniffi::export] pub fn moq_mint_epoch() -> String { @@ -400,8 +396,7 @@ impl MoqOriginProducer { /// claims every path). A service that only serves some of them advertises /// the covering prefix and rejects the rest as they are requested. Hold /// the returned handle while the route should stay advertised and missing - /// broadcasts should be served. Create, attach this for tracks served on - /// demand, populate, then announce. + /// broadcasts should be served. pub fn dynamic(&self, prefix: String) -> Result, MoqError> { let _guard = crate::ffi::enter(); self.inner.driver.check()?; diff --git a/rs/moq-ffi/src/producer.rs b/rs/moq-ffi/src/producer.rs index cc78d7529fd..646bae7fc5b 100644 --- a/rs/moq-ffi/src/producer.rs +++ b/rs/moq-ffi/src/producer.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use moq_mux::catalog::hang::Extra; use crate::consumer::{MoqBroadcastConsumer, MoqGroupConsumer, MoqSubscription, MoqTrackConsumer}; -use crate::demand::{MoqGroupDemand, MoqTrackDemand}; +use crate::demand::MoqTrackDemand; use crate::error::MoqError; use crate::ffi::{Background, Task}; use crate::media::MoqFrame; @@ -282,7 +282,7 @@ impl MoqBroadcastProducer { /// Until announced, the broadcast is invisible and unroutable for local /// consumers and peers alike. Announcing again re-prices the route in place. /// Errors with `Closed` on a standalone broadcast (no origin to announce on). - pub fn announce(&self, route: crate::origin::MoqRoute) -> Result<(), MoqError> { + pub fn announce(&self, route: crate::origin::MoqAnnounceRoute) -> Result<(), MoqError> { let _guard = crate::ffi::enter(); let route: moq_net::origin::Route = route.try_into()?; self.with_state(|state| { @@ -376,7 +376,9 @@ impl MoqBroadcastProducer { /// A cleanly ended broadcast, still announced and serving its finished tracks. /// /// Returned by [`MoqBroadcastProducer::finish`]. Closing or dropping it closes the broadcast, -/// which is how an interrupt ends the linger early. +/// which is how an interrupt ends the linger early. A garbage-collected language (Kotlin, Go, +/// Dart) drops it only when the collector reclaims it, possibly long after the last reference +/// goes, so wait on or close it rather than leave it for the collector. #[derive(uniffi::Object)] pub struct MoqBroadcastFinish { linger: std::time::Duration, @@ -515,12 +517,6 @@ impl MoqGroupRequest { self.priority } - /// A handle that watches whether any caller still wants this group. - pub fn demand(&self) -> Result, MoqError> { - let guard = self.inner.lock().unwrap(); - Ok(MoqGroupDemand::new(guard.as_ref().ok_or(MoqError::Closed)?.demand())) - } - /// Accept the request and return a producer for filling the fetched group. pub fn accept(&self) -> Result, MoqError> { let _guard = crate::ffi::enter(); diff --git a/rs/moq-ffi/src/test.rs b/rs/moq-ffi/src/test.rs index 0be53ed7879..df354f647a0 100644 --- a/rs/moq-ffi/src/test.rs +++ b/rs/moq-ffi/src/test.rs @@ -234,7 +234,7 @@ async fn await_announced(consumer: &MoqOriginConsumer, path: &str) -> Arc Arc { let broadcast = origin.create_broadcast(path.into()).unwrap(); - broadcast.announce(MoqRoute::default()).unwrap(); + broadcast.announce(MoqAnnounceRoute::default()).unwrap(); broadcast } @@ -276,6 +276,17 @@ fn rendition(id: &str, container: MoqContainer) -> MoqRendition { } } +/// A rendition whose catalog `broadcast` field names `reference`, served by `epoch`. +fn referencing(reference: Option<&str>, epoch: Option<&str>) -> MoqRendition { + let mut rendition = rendition("video", MoqContainer::Legacy); + let MoqRendition::Video { config, .. } = &mut rendition else { + unreachable!() + }; + config.broadcast = reference.map(Into::into); + config.epoch = epoch.map(Into::into); + rendition +} + /// An Opus rendition whose catalog `broadcast` field names `reference`. #[cfg(feature = "audio")] fn sibling_audio(reference: &str) -> crate::media::MoqAudio { @@ -350,23 +361,15 @@ fn origin_config_set_cache_capacity() { } #[test] -fn route_cost_conversions_are_lossless() { - // A static cost survives conversion in both directions. +fn route_conversions_keep_cost_and_derive_anonymity() { + // An observed route reports its static cost. let route = moq_net::origin::Route::default().with_cost(moq_net::origin::Cost::new(9)); - let ffi = MoqRoute::from(route.clone()); - assert_eq!(ffi.cost, 9); - let back = moq_net::origin::Route::try_from(ffi).unwrap(); - assert_eq!(back.cost, route.cost); + assert_eq!(MoqRoute::from(route).cost, 9); // A publisher seeds its production cost with one number. - let seeded = moq_net::origin::Route::try_from(MoqRoute { - hops: vec![], - cost: 5, - anonymous: false, - epoch: None, - }) - .unwrap(); + let seeded = moq_net::origin::Route::try_from(MoqAnnounceRoute { cost: 5, epoch: None }).unwrap(); assert_eq!(seeded.cost, moq_net::origin::Cost::new(5)); + assert!(seeded.hops.is_empty()); let anonymous = MoqRoute::from( moq_net::origin::Route::default().with_hops(moq_net::Hops::try_from(vec![moq_net::Hop::UNKNOWN]).unwrap()), @@ -380,14 +383,7 @@ async fn announced_route_keeps_static_cost_on_reannounce() { let origin = MoqOriginProducer::new(MoqOriginConfig::default()); let consumer = origin.consume(); let broadcast = origin.create_broadcast("priced-route".into()).unwrap(); - broadcast - .announce(MoqRoute { - hops: vec![], - cost: 9, - anonymous: false, - epoch: None, - }) - .unwrap(); + broadcast.announce(MoqAnnounceRoute { cost: 9, epoch: None }).unwrap(); // Reading an advertisement preserves its static production price. let announced = consumer.announced(MoqAnnounceConfig::default()).unwrap(); @@ -401,12 +397,15 @@ async fn announced_route_keeps_static_cost_on_reannounce() { }; assert_eq!(route.cost, 9); - // Re-announcing the observed route preserves its static price. An identical - // advertisement is not redelivered, so check its conversion instead. - broadcast.announce(route.clone()).unwrap(); - let back = moq_net::origin::Route::try_from(route.clone()).unwrap(); - assert_eq!(back.cost, moq_net::origin::Cost::new(9)); - assert_eq!(MoqRoute::from(back), route); + // Re-announcing what was observed keeps the static price. + broadcast + .announce(MoqAnnounceRoute { + cost: route.cost, + epoch: route.epoch.clone(), + }) + .unwrap(); + let current = broadcast.state.lock().unwrap().as_ref().unwrap().broadcast.route(); + assert_eq!(current.map(MoqRoute::from), Some(route)); broadcast.close().unwrap(); } @@ -420,13 +419,13 @@ fn epoch_helpers_mint_parse_and_refuse() { assert!(moq_mint_epoch() > epoch, "a later mint orders newer"); // The text the route carries round-trips, and anything but a UUIDv7 is refused. - let route = MoqRoute { + let route = MoqAnnounceRoute { epoch: Some(epoch.clone()), ..Default::default() }; - let back = moq_net::origin::Route::try_from(route.clone()).unwrap(); + let back = moq_net::origin::Route::try_from(route).unwrap(); assert_eq!(back.epoch, Some(parsed)); - assert_eq!(MoqRoute::from(back), route); + assert_eq!(MoqRoute::from(back).epoch, Some(epoch.clone())); let uuid_v4 = "6ba7b810-9dad-41d1-80b4-00c04fd430c8".to_string(); for bad in [uuid_v4, epoch.to_uppercase(), String::new()] { @@ -434,7 +433,7 @@ fn epoch_helpers_mint_parse_and_refuse() { moq_epoch_time_ms(bad.clone()), Err(MoqError::InvalidEpoch { .. }) )); - let route = MoqRoute { + let route = MoqAnnounceRoute { epoch: Some(bad), ..Default::default() }; @@ -454,7 +453,7 @@ async fn announced_epoch_restarts_on_a_new_epoch() { let first = moq_mint_epoch(); broadcast - .announce(MoqRoute { + .announce(MoqAnnounceRoute { epoch: Some(first.clone()), ..Default::default() }) @@ -465,7 +464,7 @@ async fn announced_epoch_restarts_on_a_new_epoch() { // A new run of the same publisher is another instance: consumers restart onto it. let second = moq_mint_epoch(); broadcast - .announce(MoqRoute { + .announce(MoqAnnounceRoute { epoch: Some(second.clone()), ..Default::default() }) @@ -2206,55 +2205,6 @@ async fn dynamic_track_serves_fetch_miss_and_priority() { assert_eq!(frame.timestamp_us, Some(100_000)); } -#[tokio::test] -async fn group_request_demand_ends_when_the_fetcher_leaves() { - let broadcast = MoqBroadcastProducer::new().unwrap(); - let track = broadcast.publish_track("events".into(), None).unwrap(); - let dynamic = track.dynamic().unwrap(); - let consumer = broadcast.consume().unwrap(); - - let fetch = tokio::spawn(async move { consumer.fetch_group("events".into(), 5, None).await }); - let request = tokio::time::timeout(TIMEOUT, dynamic.requested_group()) - .await - .expect("timed out waiting for group request") - .unwrap(); - let demand = request.demand().unwrap(); - assert_eq!(demand.sequence(), 5); - assert!(demand.is_used()); - - fetch.abort(); - tokio::time::timeout(TIMEOUT, demand.unused()) - .await - .expect("timed out waiting for the abandoned fetch to become unused") - .unwrap(); - assert!(!demand.is_used()); - - request.abort(404).unwrap(); - assert!(matches!(request.demand(), Err(MoqError::Closed))); -} - -/// An accepted request's demand closes cleanly while its fetch gets the group. -#[tokio::test] -async fn group_request_demand_closes_after_accept() { - tokio::time::pause(); - let broadcast = MoqBroadcastProducer::new().unwrap(); - let track = broadcast.publish_track("events".into(), None).unwrap(); - let dynamic = track.dynamic().unwrap(); - let consumer = broadcast.consume().unwrap(); - - let fetch = tokio::spawn(async move { consumer.fetch_group("events".into(), 5, None).await }); - let request = dynamic.requested_group().await.unwrap(); - let demand = request.demand().unwrap(); - let group = request.accept().unwrap(); - group.finish().unwrap(); - - for result in [demand.used().await, demand.unused().await] { - assert!(matches!(result, Err(MoqError::Closed)), "got {result:?}"); - } - assert!(!demand.is_used()); - fetch.await.expect("fetch task panicked").unwrap(); -} - #[tokio::test] async fn dynamic_track_rejects_fetch_miss() { let broadcast = MoqBroadcastProducer::new().unwrap(); @@ -2645,7 +2595,7 @@ async fn create_broadcast_is_invisible_until_announced() { assert!(!pending.is_finished(), "create_broadcast must not advertise the path"); assert!(!waited.is_finished(), "an unannounced broadcast must not resolve"); - broadcast.announce(MoqRoute::default()).unwrap(); + broadcast.announce(MoqAnnounceRoute::default()).unwrap(); tokio::time::timeout(TIMEOUT, waited) .await .expect("timed out waiting for the announced broadcast") @@ -2869,7 +2819,7 @@ async fn announced_broadcasts_resolve_siblings_under_the_prefix() { await_announced(&consumer, "a/source").await; - let sibling = tokio::time::timeout(TIMEOUT, broadcast.resolve(Some("./source".into()), None)) + let sibling = tokio::time::timeout(TIMEOUT, broadcast.resolve(referencing(Some("./source"), None))) .await .expect("timed out resolving the reference") .unwrap(); @@ -2946,13 +2896,13 @@ async fn resolve_rejects_a_reference_without_an_origin() { // An absent or empty reference still names this broadcast, so it needs no origin. A reference // made only of slashes normalizes to the empty one, so it must be treated the same rather than // looking like a cross-broadcast reference. - consumer.resolve(None, None).await.unwrap(); - consumer.resolve(Some(String::new()), None).await.unwrap(); - consumer.resolve(Some("/".into()), None).await.unwrap(); + consumer.resolve(referencing(None, None)).await.unwrap(); + consumer.resolve(referencing(Some(""), None)).await.unwrap(); + consumer.resolve(referencing(Some("/"), None)).await.unwrap(); // Reported in normalized form, matching `EscapingBroadcast` and moq-c: it names what the // resolver actually tried to reach, not the caller's spelling of it. - match consumer.resolve(Some("./source".into()), None).await { + match consumer.resolve(referencing(Some("./source"), None)).await { Err(MoqError::UnresolvableBroadcast { reason: reference }) => assert_eq!(reference, "source"), Err(err) => panic!("wrong error for an unresolvable reference: {err:?}"), Ok(_) => panic!("a standalone broadcast cannot resolve a sibling"), @@ -2972,7 +2922,7 @@ async fn resolve_returns_a_broadcast_that_resolves_further_references() { let broadcast = await_announced(&consumer, "a/pub").await; await_announced(&consumer, "a/source").await; - let sibling = tokio::time::timeout(TIMEOUT, broadcast.resolve(Some("./source".into()), None)) + let sibling = tokio::time::timeout(TIMEOUT, broadcast.resolve(referencing(Some("./source"), None))) .await .expect("timed out resolving the reference") .unwrap(); @@ -2985,7 +2935,7 @@ async fn resolve_returns_a_broadcast_that_resolves_further_references() { .unwrap(); // And it can follow a reference of its own, back to where we started. - let back = tokio::time::timeout(TIMEOUT, sibling.resolve(Some("./pub".into()), None)) + let back = tokio::time::timeout(TIMEOUT, sibling.resolve(referencing(Some("./pub"), None))) .await .expect("timed out resolving back") .unwrap(); @@ -3005,19 +2955,16 @@ async fn resolve_honors_the_reference_epoch() { reference: &str, epoch: Option<&str>, ) -> Result, MoqError> { - tokio::time::timeout( - TIMEOUT, - broadcast.resolve(Some(reference.into()), epoch.map(Into::into)), - ) - .await - .expect("timed out resolving the reference") + tokio::time::timeout(TIMEOUT, broadcast.resolve(referencing(Some(reference), epoch))) + .await + .expect("timed out resolving the reference") } let origin = MoqOriginProducer::new(MoqOriginConfig::default()); let consumer = origin.consume(); let served = moq_mint_epoch(); let other = moq_mint_epoch(); - let route = MoqRoute { + let route = MoqAnnounceRoute { epoch: Some(served.clone()), ..Default::default() }; @@ -3054,14 +3001,8 @@ async fn resolve_honors_the_reference_epoch() { )); // The rendition-based consumers read the epoch off the rendition. - let mut mismatched = rendition("video", MoqContainer::Legacy); - let MoqRendition::Video { config, .. } = &mut mismatched else { - unreachable!() - }; - config.broadcast = Some("./source".into()); - config.epoch = Some(other); let config = MoqMediaTrackConfig { - rendition: mismatched, + rendition: referencing(Some("./source"), Some(&other)), subscription: None, }; let subscribed = tokio::time::timeout(TIMEOUT, MoqMediaTrackConsumer::subscribe(&broadcast, config)) @@ -3079,7 +3020,7 @@ async fn announce_and_unannounce_toggles_discovery() { let origin = MoqOriginProducer::new(MoqOriginConfig::default()); let consumer = origin.consume(); let broadcast = origin.create_broadcast("live".into()).unwrap(); - broadcast.announce(MoqRoute::default()).unwrap(); + broadcast.announce(MoqAnnounceRoute::default()).unwrap(); // The consumer observes the flag through the announce stream: an active // announcement, then its retraction. @@ -3105,7 +3046,7 @@ async fn announce_and_unannounce_toggles_discovery() { assert!(unroutable.is_err(), "an unannounced broadcast is unroutable"); // Announcing again brings it back. - broadcast.announce(MoqRoute::default()).unwrap(); + broadcast.announce(MoqAnnounceRoute::default()).unwrap(); wait_live(&announced, true).await; tokio::time::timeout(TIMEOUT, consumer.request_broadcast("live".into())) .await @@ -4027,7 +3968,10 @@ async fn dynamic_broadcast_request() { let served = prepared.broadcast(); let track = served.publish_track("status".into(), Some(timed_info())).unwrap(); // Only publish announces the answer, under its epoch and after admission. - assert!(matches!(served.announce(MoqRoute::default()), Err(MoqError::Closed))); + assert!(matches!( + served.announce(MoqAnnounceRoute::default()), + Err(MoqError::Closed) + )); prepared.publish().unwrap(); assert!(matches!(request.path(), Err(MoqError::Closed))); @@ -5240,7 +5184,7 @@ async fn announced_broadcast_hands_out_a_held_takeover_once() { async fn announced_broadcast_hands_out_the_instance_again_after_an_end() { let origin = MoqOriginProducer::new(MoqOriginConfig::default()); let followed = origin.consume().announced_broadcast("live".into()).unwrap(); - let route = MoqRoute { + let route = MoqAnnounceRoute { epoch: Some(moq_mint_epoch()), ..Default::default() }; diff --git a/swift/MIGRATE.md b/swift/MIGRATE.md index 976a5a725ad..4e6bf56fcd1 100644 --- a/swift/MIGRATE.md +++ b/swift/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `announce` takes an `AnnounceRoute` + +`BroadcastProducer.announce(route:)` takes `AnnounceRoute(cost:epoch:)` instead of a `Route`: the origin builds the hop chain, so `hops` is no longer an input. +`Route` is only what announcements report. To re-announce an observed route, copy its `cost` and `epoch`. + ### A broadcast reference waits for its sibling's announcement (#6254) Resolving a rendition's `broadcast` reference, through `BroadcastConsumer.resolve(_:epoch:)` and the rendition-based subscribes and fetches, now waits for a sibling nothing announces yet instead of failing as unroutable, so a late sibling plays once it is announced. @@ -36,7 +41,7 @@ Pass the new `ContainerFormat.cmaf` to keep publishing CMAF. ### Catalog references carry their epoch `Media.Video` and `Media.Audio` gain `epoch`, the publisher instance their `broadcast` reference names. -`BroadcastConsumer.resolve(_:)` becomes `resolve(_:epoch:)`; pass the rendition's `epoch` alongside its `broadcast`, or `nil` to resolve whichever instance answers. +`BroadcastConsumer.resolve(_:)` takes the catalog `Media.Rendition` instead of its reference, and reads its `broadcast` and `epoch` from it. Resolving, and the rendition-based consumers, throw for a rendition whose broadcast is proven to serve another epoch. ### Timestamps stop at 2^61 - 1 microseconds (#6168) @@ -82,7 +87,7 @@ A subscription now reads only the groups its own `ranges` cover. `TrackDemand.isUsed`, `used()`, and `unused()` are gone. `wanted` is nil while nobody subscribes, otherwise the group subscribers want next, and `wantedChanged()` waits for it to change (the first call returns at once). Loop on `wantedChanged()` until it returns non-nil where you awaited `used()`, and nil where you awaited `unused()`. -`TrackInfo` gains `refresh`. `GroupDemand` is unchanged. +`TrackInfo` gains `refresh`. ### `CongestionControl` gained `none` (#5926) @@ -103,7 +108,7 @@ To start at group `n`, pass one range starting at the absolute anchor for group ### 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. +Hold the consumer `Session.recvBandwidth` returns to request PROBE, read it with `rate()` or wait with `rateChanged()`, and drop it to stop. It returns `nil` when the version has no PROBE. ### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) diff --git a/swift/Sources/Moq/Aliases.swift b/swift/Sources/Moq/Aliases.swift index 03a5fc8c37f..d01eccb6feb 100644 --- a/swift/Sources/Moq/Aliases.swift +++ b/swift/Sources/Moq/Aliases.swift @@ -48,9 +48,12 @@ public typealias VideoCodec = MoqFFI.MoqVideoCodec public typealias VideoEncoderKind = MoqFFI.MoqVideoEncoderKind /// A best-effort raw-track datagram as received: sequence, timestamp, and payload. public typealias Datagram = MoqFFI.MoqDatagram -/// A path-prefix route: the publisher `epoch` it serves (`nil` when unknown), -/// relay hop ids (oldest first), and its static production and link cost, -/// lower wins. +/// How a broadcast advertises its exact path: its static production `cost`, +/// lower wins, and the publisher `epoch` it serves (`nil` when unknown). +public typealias AnnounceRoute = MoqFFI.MoqAnnounceRoute +/// An observed path-prefix route: the publisher `epoch` it serves (`nil` when +/// unknown), relay hop ids (oldest first), and its static production and link +/// cost, lower wins. public typealias Route = MoqFFI.MoqRoute /// A route over a prefix: the origin-relative `prefix`, what each filter /// wildcard matched (`captures`, `nil` for a partial overlap), and the `route` diff --git a/swift/Sources/Moq/Bandwidth.swift b/swift/Sources/Moq/Bandwidth.swift index b10f4f66d80..1bb855b52c5 100644 --- a/swift/Sources/Moq/Bandwidth.swift +++ b/swift/Sources/Moq/Bandwidth.swift @@ -11,11 +11,11 @@ public final class Bandwidth: Sendable { init(_ ffi: MoqBandwidthConsumer) { self.ffi = ffi } - /// Read the current estimate, or nil while unavailable. - public func peek() -> UInt64? { ffi.peek() } + /// The current estimate, or nil while unavailable. + public func rate() -> UInt64? { ffi.rate() } - /// Wait for the estimate to change, throwing when its producer closes. - public func changed() async throws -> UInt64? { try await ffi.changed() } + /// Wait for the estimate to change and return the new rate, throwing when its producer closes. + public func rateChanged() async throws -> UInt64? { try await ffi.rateChanged() } /// Release this consumer's demand, including a pending wait. public func cancel() { ffi.cancel() } @@ -34,14 +34,6 @@ public final class Bandwidth: Sendable { public func reserve(demand: TrackDemand, maxBps: UInt64) -> Reservation { Reservation(ffi.reserve(demand: demand.ffi, maxBps: maxBps)) } - - /// Probe up to `targetBps` instead of the sum of the reservations. - /// - /// The session probes for what its reservations add up to by default, so the estimate can rise to fund them. `0` turns - /// probing off, and `nil` follows the reservations again. - public func setTarget(_ targetBps: UInt64?) { - ffi.setTarget(targetBps: targetBps) - } } /// One track's standing claim on a `Bandwidth`. diff --git a/swift/Sources/Moq/Broadcast.swift b/swift/Sources/Moq/Broadcast.swift index 8c8cc5958fd..0ea57cb0ee7 100644 --- a/swift/Sources/Moq/Broadcast.swift +++ b/swift/Sources/Moq/Broadcast.swift @@ -26,21 +26,21 @@ public final class BroadcastConsumer: Sendable { GroupConsumer(try await ffi.fetchGroup(name: name, sequence: sequence, options: options)) } - /// Resolve a catalog rendition's `broadcast` reference to the broadcast serving its track. + /// Resolve a catalog rendition to the broadcast serving its track. /// - /// `reference` is `Media.Video.broadcast` / `Media.Audio.broadcast`: `nil` or empty names this - /// broadcast, anything else names a sibling relative to it (e.g. `./source`). `epoch` is the - /// same rendition's `epoch`: a broadcast proven to serve another instance throws instead of - /// resolving, and a route without an epoch cannot prove one, so there it is best effort. Call - /// it on a rendition that carries a reference before `subscribeTrack` or `fetchGroup`, which - /// take a track name rather than a rendition: the rendition's `track`, or its ID when `nil`. - /// The rendition-based `Media.TrackConsumer.subscribe`, `Media.GroupConsumer.fetch`, - /// `Audio.Consumer.subscribe`, and `Video.Consumer.subscribe` resolve both themselves. + /// The rendition's `broadcast` reference, `nil` or empty, names this broadcast; anything else + /// names a sibling relative to it (e.g. `./source`). Its `epoch` is the publisher instance the + /// reference names: a broadcast proven to serve another instance throws instead of resolving, + /// and a route without an epoch cannot prove one, so there it is best effort. Call it on a + /// rendition that carries a reference before `subscribeTrack` or `fetchGroup`, which take a + /// track name rather than a rendition: the rendition's `track`, or its ID when `nil`. The + /// rendition-based `Media.TrackConsumer.subscribe`, `Media.GroupConsumer.fetch`, + /// `Audio.Consumer.subscribe`, and `Video.Consumer.subscribe` resolve it themselves. /// /// Throws if this broadcast came from a local producer rather than an origin, since a /// standalone broadcast has no sibling to name. - public func resolve(_ reference: String?, epoch: String?) async throws -> BroadcastConsumer { - BroadcastConsumer(try await ffi.resolve(reference: reference, epoch: epoch)) + public func resolve(_ rendition: Media.Rendition) async throws -> BroadcastConsumer { + BroadcastConsumer(try await ffi.resolve(rendition: rendition)) } } @@ -78,7 +78,7 @@ public final class BroadcastProducer: Sendable { /// /// Announcing again re-prices the route in place. Until announced, the /// broadcast is invisible and unroutable for local consumers and peers alike. - public func announce(route: Route = Route()) throws { + public func announce(route: AnnounceRoute = AnnounceRoute()) throws { try ffi.announce(route: route) } diff --git a/swift/Sources/Moq/Dynamic.swift b/swift/Sources/Moq/Dynamic.swift index 211d7157dc3..7767071f1cc 100644 --- a/swift/Sources/Moq/Dynamic.swift +++ b/swift/Sources/Moq/Dynamic.swift @@ -37,40 +37,6 @@ public final class TrackRequest: Sendable { } } -/// A watch-only handle to the callers waiting on a requested group. -/// -/// Returned by `GroupRequest.demand()`. Weak: holding it does not keep the request alive. The last -/// caller to leave withdraws the request, so once unused, demand never returns: drop the request. -/// Waits throw `MoqError.Closed` after the request is accepted or dropped, and keep the rejection -/// reason after it is rejected. -public final class GroupDemand: Sendable { - let ffi: MoqGroupDemand - - init(_ ffi: MoqGroupDemand) { - self.ffi = ffi - } - - /// The sequence of the group this watches. - public var sequence: UInt64 { - ffi.sequence() - } - - /// Whether the group has at least one waiting caller right now. - public var isUsed: Bool { - ffi.isUsed() - } - - /// Suspend until the group has at least one waiting caller. - public func used() async throws { - try await ffi.used() - } - - /// Suspend until the group has no waiting callers. - public func unused() async throws { - try await ffi.unused() - } -} - /// A request to produce one uncached group for a fetch consumer. public final class GroupRequest: Sendable { let ffi: MoqGroupRequest @@ -89,11 +55,6 @@ public final class GroupRequest: Sendable { ffi.priority() } - /// A watch-only handle to whether any caller still wants this group. - public func demand() throws -> GroupDemand { - GroupDemand(try ffi.demand()) - } - /// Accept the request and return a producer for the group. public func accept() throws -> GroupProducer { GroupProducer(try ffi.accept()) diff --git a/swift/Sources/Moq/Epoch.swift b/swift/Sources/Moq/Epoch.swift index 6b249201f9f..4845249640f 100644 --- a/swift/Sources/Moq/Epoch.swift +++ b/swift/Sources/Moq/Epoch.swift @@ -3,7 +3,7 @@ import MoqFFI /// Mint a fresh publisher epoch from the wall clock and secure randomness, /// ordered newest last. Mint one per publisher run and announce it in -/// `Route.epoch`, so viewers see a restart as a new broadcast instead of a +/// `AnnounceRoute.epoch`, so viewers see a restart as a new broadcast instead of a /// stalled one. public func mintEpoch() -> String { moqMintEpoch() diff --git a/swift/Tests/MoqTests/SmokeTests.swift b/swift/Tests/MoqTests/SmokeTests.swift index 2e9ea4c4b34..35ba840b0c6 100644 --- a/swift/Tests/MoqTests/SmokeTests.swift +++ b/swift/Tests/MoqTests/SmokeTests.swift @@ -217,14 +217,14 @@ final class SmokeTests: XCTestCase { let first = mintEpoch() XCTAssertEqual(try epochTime("018bcfe5-6800-7000-8000-000000000000"), Date(timeIntervalSince1970: 1_700_000_000)) - try broadcast.announce(route: Route(epoch: first)) + try broadcast.announce(route: AnnounceRoute(epoch: first)) guard case .start(let start) = try await nextRoute(announced) else { return XCTFail("expected an announcement") } XCTAssertEqual(start.route.epoch, first) let second = mintEpoch() - try broadcast.announce(route: Route(epoch: second)) + try broadcast.announce(route: AnnounceRoute(epoch: second)) guard case .restart(let restart) = try await nextRoute(announced) else { return XCTFail("expected a restart") } diff --git a/swift/ffi/MIGRATE.md b/swift/ffi/MIGRATE.md index f27cf7ce292..b71386a0365 100644 --- a/swift/ffi/MIGRATE.md +++ b/swift/ffi/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `announce` takes a `MoqAnnounceRoute` + +`MoqBroadcastProducer.announce` takes `MoqAnnounceRoute` (`cost`, `epoch`) instead of a `MoqRoute`: the origin builds the hop chain, so `hops` is no longer an input. +`MoqRoute` is only what announcements report, and the `InvalidRoute` error is gone. + ### A broadcast reference waits for its sibling's announcement (#6254) Resolving a rendition's `broadcast` reference, through `MoqBroadcastConsumer.resolve(reference:epoch:)` and the rendition-based subscribes and fetches, now waits for a sibling nothing announces yet instead of failing as unroutable, so a late sibling plays once it is announced. @@ -21,7 +26,7 @@ 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. +`MoqBroadcastConsumer.resolve(reference:)` becomes `resolve(rendition:)`, taking the catalog `MoqRendition` and reading its `broadcast` and `epoch` from it. 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. diff --git a/test/interop/clients/go/main.go b/test/interop/clients/go/main.go index 1b988744e3f..2654b82ebbb 100644 --- a/test/interop/clients/go/main.go +++ b/test/interop/clients/go/main.go @@ -164,7 +164,7 @@ func publish(ctx context.Context, url, broadcast string, opts dialOptions, timeo if err != nil { return err } - if err := producer.Announce(moq.Route{}); err != nil { + if err := producer.Announce(moq.AnnounceRoute{}); err != nil { return err } fmt.Printf("publishing %q (Annex-B H.264 from stdin + a %v Hz tone) to %s\n", broadcast, tones, url) @@ -244,7 +244,7 @@ func publish(ctx context.Context, url, broadcast string, opts dialOptions, timeo if err := live.Finish(); err != nil { return err } - if err := health.Announce(moq.Route{}); err != nil { + if err := health.Announce(moq.AnnounceRoute{}); err != nil { return err } ack, err := os.Open(finishAck) diff --git a/test/smoke/clients/go/smoke.go b/test/smoke/clients/go/smoke.go index ac6f05ac457..2f9d694f210 100644 --- a/test/smoke/clients/go/smoke.go +++ b/test/smoke/clients/go/smoke.go @@ -87,7 +87,7 @@ func publish(url, broadcast string) error { // CreateBroadcast only registers the path locally; Announce advertises it to // the relay, once its tracks exist. - if err := producer.Announce(moq.Route{}); err != nil { + if err := producer.Announce(moq.AnnounceRoute{}); err != nil { return err } diff --git a/test/wasm/uniffi/cases.ts b/test/wasm/uniffi/cases.ts index be937ef60db..7bf417c415e 100644 --- a/test/wasm/uniffi/cases.ts +++ b/test/wasm/uniffi/cases.ts @@ -7,13 +7,13 @@ * @module */ import { + MoqAnnounceRoute, MoqBroadcastProducer, MoqClient, MoqClientConfig, MoqClientTls, type MoqGroupConsumer, type MoqGroupConsumerLike, - MoqRoute, type MoqSessionLike, type MoqTrackConsumer, MoqTrackInfo, @@ -262,7 +262,7 @@ const remote: Record Promise> = { const path = `uniffi/u64/${crypto.randomUUID()}`; const broadcast = publisher.publish().createBroadcast(path); const track = broadcast.publishTrack("data", TIMED); - broadcast.announce(MoqRoute.create({})); + broadcast.announce(MoqAnnounceRoute.create({})); const consumer = await within(subscriber.consume().announcedBroadcast(path).available(), 5000, "announce"); const sub = await consumer.subscribeTrack("data", undefined); const seen: string[] = []; @@ -294,7 +294,7 @@ const remote: Record Promise> = { const path = `uniffi/refuse/${crypto.randomUUID()}`; const broadcast = publisher.publish().createBroadcast(path); broadcast.publishTrack("data", TIMED); - broadcast.announce(MoqRoute.create({})); + broadcast.announce(MoqAnnounceRoute.create({})); const consumer = await within(subscriber.consume().announcedBroadcast(path).available(), 5000, "announce"); const outcome = await settle( consumer.subscribeTrack("missing", undefined).then((track) => track.recvGroup()), @@ -318,7 +318,7 @@ const remote: Record Promise> = { const path = `uniffi/close/${crypto.randomUUID()}`; const broadcast = publisher.publish().createBroadcast(path); const track = broadcast.publishTrack("data", TIMED); - broadcast.announce(MoqRoute.create({})); + broadcast.announce(MoqAnnounceRoute.create({})); const consumer = await within(subscriber.consume().announcedBroadcast(path).available(), 5000, "announce"); const sub = await consumer.subscribeTrack("data", undefined); const group = track.appendGroup();