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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 1 addition & 25 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6126,7 +6126,6 @@ abstract class MoqBroadcastProducerInterface {
void close();
MoqBroadcastConsumer consume();
MoqBroadcastDynamic dynamic_();
void finish();
MoqMediaProducer publishAudio({required MoqAudioInit init});
MoqMediaProducer publishAudioOnTrack({
required MoqTrackRequest request,
Expand Down Expand Up @@ -6296,15 +6295,6 @@ class MoqBroadcastProducer implements MoqBroadcastProducerInterface {
);
}

void finish() {
return rustCall((status) {
uniffi_moq_ffi_fn_method_moqbroadcastproducer_finish(
uniffiClonePointer(),
status,
);
}, moqExceptionErrorHandler);
}

MoqMediaProducer publishAudio({required MoqAudioInit init}) {
return rustCallWithLifter(
(status) => uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_audio(
Expand Down Expand Up @@ -10572,14 +10562,6 @@ external Pointer<Void> uniffi_moq_ffi_fn_method_moqbroadcastproducer_dynamic(
Pointer<RustCallStatus> uniffiStatus,
);

@Native<Void Function(Pointer<Void>, Pointer<RustCallStatus>)>(
assetId: _uniffiAssetId,
)
external void uniffi_moq_ffi_fn_method_moqbroadcastproducer_finish(
Pointer<Void> ptr,
Pointer<RustCallStatus> uniffiStatus,
);

@Native<
Pointer<Void> Function(Pointer<Void>, RustBuffer, Pointer<RustCallStatus>)
>(assetId: _uniffiAssetId)
Expand Down Expand Up @@ -12245,9 +12227,6 @@ external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_consume();
@Native<Uint16 Function()>(assetId: _uniffiAssetId)
external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_dynamic();

@Native<Uint16 Function()>(assetId: _uniffiAssetId)
external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_finish();

@Native<Uint16 Function()>(assetId: _uniffiAssetId)
external int
uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_audio();
Expand Down Expand Up @@ -12807,7 +12786,7 @@ void _checkApiChecksums() {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqoriginproducer_create_broadcast() !=
48971) {
45756) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqoriginproducer_dynamic() != 56233) {
Expand Down Expand Up @@ -12848,9 +12827,6 @@ void _checkApiChecksums() {
if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_dynamic() != 55635) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_finish() != 29562) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_audio() !=
31691) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/c/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ beside `pkgconfig/`.
- **Server.** `moq_server_listen` binds before it returns (a bad address or certificate fails there) and hands each incoming session to `on_request` as a request handle. Read `moq_session_request_path` and `_query` to route and authenticate, then `moq_session_request_accept` (a session handle, with origins like `moq_session_connect`) or `moq_session_request_reject` with an HTTP-style code (401 and 403 become the protocol's unauthorized close). An accepted session reports `1` once SETUP completes and never reconnects. `moq_server_addr` reports an ephemeral port and `moq_server_fingerprints` the hashes a client pins for a `tls_generate` certificate. `moq_server_close` stops listening; its terminal callback fires once the sockets are released.
- **Demand.** A watcher on a published track (`moq_publish_track_demand`, `moq_publish_media_demand`, `moq_encode_video_demand`, `moq_encode_audio_demand`) calls `on_demand` with `MOQ_DEMAND_USED` or `MOQ_DEMAND_UNUSED` right away and again on every change, so an encoder on a battery-powered device runs only while someone is watching. The first call is the current state, so a track that went unused before the watcher existed still reports it. `moq_publish_demand_cancel` stops it; the terminal callback still fires. A container has no single demand and is refused. Demand follows the last real subscriber: an origin that served the track drops its source copy on the unused edge, so a cache linger never delays it. Only a relay keeps what it already delivered warm for 30 seconds, and a returning subscriber is served that cache only once the publisher confirms it is still current.
- **Requests.** `moq_publish_dynamic` serves subscriptions to tracks the broadcast never declared: each arrives as a request handle, read its name with `moq_track_request_name`, then `moq_track_request_accept` (a raw track handle), `moq_track_request_video` / `_audio` (the media handle `moq_publish_video` / `_audio` return), or `moq_track_request_abort` with an application code the subscriber sees. Without a live handler an unknown name is refused. `moq_publish_track_dynamic` does the same for fetches of groups a track no longer has cached, delivered as `moq_group_request_*` (`sequence`, `priority`, `frame_start`); `moq_group_request_accept` starts the producer at `frame_start` so written frames keep their group indices. Register it with `moq_track_request_dynamic` before accepting a track that was itself requested by a fetch, so that pending group survives the transition. Both handlers stop with `moq_publish_dynamic_cancel`.
- **Everything the bindings can do** ([list](/lib/#what-every-binding-can-do)): media publish and consume with the catalog managed for you, raw pixels and PCM with the codec inside (`moq_encode_video`, `moq_encode_audio`, and the `moq_decode_*` mirrors), raw tracks with timestamps and datagrams, JSON and binary data tracks (snapshot or stream, each advertised in the catalog for as long as it lives), group fetch, catalog sections, shared video properties, and stalled hints. The three advertising operations are `moq_origin_create_broadcast` (unannounced producer, invisible to everyone), `moq_publish_announce` / `moq_publish_unannounce` (exact-path advertisement), `moq_publish_close` (ends the broadcast for good and releases its handle; `moq_publish_finish` is its deprecated alias), and `moq_origin_dynamic` (a claim over a path prefix and everything beneath it; `""` for everything). A route is a capability, not an inventory. `moq_origin_announced` takes a `moq_announce_config` (NULL or zeroed for everything): a literal prefix and an optional relative pattern filter; `moq_announce_update.prefix` stays relative to the origin, while `captures` reports what each wildcard matched when `has_captures` is true. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts); set `hidden` or name the dot segment in `prefix` to list them.
- **Everything the bindings can do** ([list](/lib/#what-every-binding-can-do)): media publish and consume with the catalog managed for you, raw pixels and PCM with the codec inside (`moq_encode_video`, `moq_encode_audio`, and the `moq_decode_*` mirrors), raw tracks with timestamps and datagrams, JSON and binary data tracks (snapshot or stream, each advertised in the catalog for as long as it lives), group fetch, catalog sections, shared video properties, and stalled hints. The three advertising operations are `moq_origin_create_broadcast` (unannounced producer, invisible to everyone), `moq_publish_announce` / `moq_publish_unannounce` (exact-path advertisement), `moq_publish_close` (ends the broadcast for good and releases its handle), and `moq_origin_dynamic` (a claim over a path prefix and everything beneath it; `""` for everything). A route is a capability, not an inventory. `moq_origin_announced` takes a `moq_announce_config` (NULL or zeroed for everything): a literal prefix and an optional relative pattern filter; `moq_announce_update.prefix` stays relative to the origin, while `captures` reports what each wildcard matched when `has_captures` is true. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts); set `hidden` or name the dot segment in `prefix` to list them.

```c
moq_client_config config;
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/dart/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The three advertising operations: `moq.createBroadcast(path)` (or
`origin.createBroadcast`) returns an unannounced producer, invisible to everyone;
`broadcast.announce(route:)` / `broadcast.unannounce()` own that exact-path
advertisement, and `broadcast.close()` ends the broadcast for good (a second
call is a no-op; `finish()` is its deprecated alias); `origin.dynamic_(prefix:, route:)` claims `prefix` and
call is a no-op); `origin.dynamic_(prefix:, route:)` claims `prefix` and
every path beneath it (`''` for everything; Dart spells the origin method
`dynamic_` because `dynamic` is reserved). Hold the returned handle while the
claim should stay advertised, and reject the requests you will not serve. A
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/go/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ The three advertising operations: `client.CreateBroadcast(path)` (or
`origin.CreateBroadcast`) returns an unannounced producer, invisible to everyone;
`broadcast.Announce(route)` / `broadcast.Unannounce()` own that exact-path
advertisement, and `broadcast.Close()` ends the broadcast for good (a second
call is a no-op; `Finish` is its deprecated alias); `origin.Dynamic(prefix, route)` claims `prefix` and every
call is a no-op); `origin.Dynamic(prefix, route)` claims `prefix` and every
path beneath it (`""` for everything). Hold the returned `OriginDynamic`
while the claim should stay advertised, and reject the requests you will not
serve. A route is a capability, not an inventory. `Announced(options)` combines
Expand Down
5 changes: 3 additions & 2 deletions doc/lib/kt/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ Call `media.discontinuity()` when the source seeks, pauses, or changes its time
The three advertising operations: `moq.createBroadcast(path)` (or
`origin.createBroadcast`) returns an unannounced producer, invisible to everyone;
`broadcast.announce(route)` / `broadcast.unannounce()` own that exact-path
advertisement, and `broadcast.close()` (or `use { }`) releases the producer,
ending the broadcast once no `dynamic()` handle remains; `origin.dynamic(prefix, route)` claims `prefix` and every
advertisement, and `broadcast.end()` ends the broadcast for good (a second call
is a no-op; Kotlin spells it `end` because `close()`, or `use { }`, releases the
handle, which ends the broadcast only once no `dynamic()` handle remains); `origin.dynamic(prefix, route)` claims `prefix` and every
path beneath it (`""` for everything). Hold the returned `OriginDynamic`
while the claim should stay advertised, and reject the requests you will not
serve. A route is a capability, not an inventory. `announcements(config)` takes
Expand Down
3 changes: 1 addition & 2 deletions doc/lib/py/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ The three advertising operations, as the other bindings spell them:
`client.create_broadcast(path)` (or `OriginProducer.create_broadcast`) returns
an unannounced producer, invisible to everyone; `broadcast.announce(route)` /
`broadcast.unannounce()` own that exact-path advertisement, and
`broadcast.close()` ends the broadcast for good (a second call is a no-op;
`finish()` is its deprecated alias);
`broadcast.close()` ends the broadcast for good (a second call is a no-op);
`origin.dynamic(prefix, route)` claims `prefix` and every path beneath it
(`""` for everything). Hold the returned handle while the claim should stay
advertised, and reject the requests you will not serve. A route is a
Expand Down
3 changes: 1 addition & 2 deletions doc/lib/swift/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ Call `audio.discontinuity()` when the source seeks, pauses, or changes its time
The three advertising operations: `session.publish.createBroadcast(path:)`
returns an unannounced producer, invisible to everyone; `broadcast.announce(route:)` /
`broadcast.unannounce()` own that exact-path advertisement, and
`broadcast.close()` ends the broadcast for good (a second call is a no-op;
`finish()` is its deprecated alias);
`broadcast.close()` ends the broadcast for good (a second call is a no-op);
`session.publish.dynamic(prefix:route:)` claims `prefix` and every path
beneath it (`""` for everything). Hold the returned `OriginDynamic` while the
claim should stay advertised, and reject the requests you will not serve. A
Expand Down
7 changes: 0 additions & 7 deletions go/wrapper/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,13 +268,6 @@ func (b *BroadcastProducer) Close() error {
return b.inner.Close()
}

// Finish ends the broadcast.
//
// Deprecated: use [BroadcastProducer.Close]; a broadcast end carries no cause.
func (b *BroadcastProducer) Finish() error {
return b.inner.Close()
}

// BroadcastDynamic is a stream of subscriber-requested tracks.
type BroadcastDynamic struct {
inner *ffi.MoqBroadcastDynamic
Expand Down
46 changes: 18 additions & 28 deletions js/net/src/broadcast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ let attachAnnouncer: (producer: Producer, announcer: Announcer) => void;
class BroadcastState {
requested = new Signal<track.Request[]>([]);
pending = new Set<track.Request>();
closed = new Once<Error | null>();
closed = new Once<null>();
tracks = new Map<string, track.Producer>();
sequences = new Map<string, TrackSequence>();
// Live consumer handles sharing this state (see {@link Consumer.clone}). The broadcast
Expand All @@ -45,10 +45,10 @@ function dequeueRequest(state: BroadcastState): track.Request | undefined {
//
// Once.set throws on a second settle, and the producer and each consumer handle close
// independently, so this has to be idempotent.
function closeState(state: BroadcastState, abort?: Error) {
function closeState(state: BroadcastState) {
if (state.closed.peek() !== undefined) return;
state.closed.set(abort ?? null);
for (const request of state.pending) request.reject(abort);
state.closed.set(null);
for (const request of state.pending) request.reject();
state.requested.mutate((requests) => {
requests.length = 0;
});
Expand All @@ -68,7 +68,7 @@ function subscribe(
register = false,
): track.Subscriber {
if (state.closed.peek() !== undefined) {
throw new Error(`broadcast is closed: ${state.closed.peek()}`);
throw new Error("broadcast is closed");
}

const existing = state.tracks.get(name);
Expand Down Expand Up @@ -103,7 +103,7 @@ async function resolveTrackInfo(state: BroadcastState, name: string): Promise<tr
}

if (state.closed.peek() !== undefined) {
return Promise.reject(new Error(`broadcast is closed: ${state.closed.peek()}`));
return Promise.reject(new Error("broadcast is closed"));
}

const producer = new track.Producer(name);
Expand Down Expand Up @@ -171,10 +171,10 @@ export class Producer {
}

/**
* Settles once the broadcast closes: `null` on a clean close, or the abort {@link Error}.
* Settles with `null` once the broadcast closes; a broadcast end carries no cause.
* Peek it synchronously (`undefined` while open), observe it reactively, or `await` it.
*/
get closed(): GetPromise<Error | null> {
get closed(): GetPromise<null> {
return this.#state.closed;
}

Expand All @@ -188,9 +188,7 @@ export class Producer {
const request = dequeueRequest(this.#state);
if (request) return request;

const closed = this.#state.closed.peek();
if (closed instanceof Error) throw closed;
if (closed !== undefined) return undefined;
if (this.#state.closed.peek() !== undefined) return undefined;

await Signal.race(this.#state.requested, this.#state.closed);
}
Expand All @@ -199,7 +197,7 @@ export class Producer {
/** Insert a track that is served directly, without an on-demand request round-trip. */
insertTrack(track: track.Producer): void {
if (this.#state.closed.peek() !== undefined) {
throw new Error(`broadcast is closed: ${this.#state.closed.peek()}`);
throw new Error("broadcast is closed");
}

const existing = this.#state.tracks.get(track.name);
Expand Down Expand Up @@ -252,7 +250,7 @@ export class Producer {
*/
announce(route: Route | { hops?: Route["hops"]; cost?: Route["cost"] | bigint } = Route.default): void {
if (this.#state.closed.peek() !== undefined) {
throw new Error(`broadcast is closed: ${this.#state.closed.peek()}`);
throw new Error("broadcast is closed");
}
if (!this.#announcer) throw new Error("broadcast is not attached to an origin");
this.#announcer.announce(Route.normalize(route));
Expand All @@ -267,13 +265,10 @@ export class Producer {
}

/** End the broadcast for good: retract it, serve no new tracks, and refuse a later {@link announce}. Idempotent. */
close(): void;
/** @deprecated A broadcast end carries no cause; call `close()` without one. */
close(abort?: Error): void;
close(abort?: Error) {
close(): void {
this.#announcer?.unannounce();
this.#announcer = undefined;
closeState(this.#state, abort);
closeState(this.#state);
}
}

Expand Down Expand Up @@ -312,13 +307,13 @@ export class Consumer {
}

/**
* Settles once the broadcast closes: `null` on a clean close, or the abort {@link Error}.
* Settles with `null` once the broadcast closes; a broadcast end carries no cause.
* Peek it synchronously (`undefined` while open), observe it reactively, or `await` it.
*
* Shared by every {@link clone}: it settles once the last handle closes. The subscribing
* wire layer peeks it to evict a closed entry from its per-path consume cache.
*/
get closed(): GetPromise<Error | null> {
get closed(): GetPromise<null> {
return this.#state.closed;
}

Expand Down Expand Up @@ -350,9 +345,7 @@ export class Consumer {
const request = dequeueRequest(this.#state);
if (request) return request;

const closed = this.#state.closed.peek();
if (closed instanceof Error) throw closed;
if (closed !== undefined) return undefined;
if (this.#state.closed.peek() !== undefined) return undefined;

await Signal.race(this.#state.requested, this.#state.closed);
}
Expand All @@ -362,13 +355,10 @@ export class Consumer {
* Release this handle. The broadcast is closed once this was the last live handle;
* while other {@link clone}s remain open it stays live.
*/
close(): void;
/** @deprecated A broadcast end carries no cause; call `close()` without one. */
close(abort?: Error): void;
close(abort?: Error) {
close(): void {
if (this.#closed) return;
this.#closed = true;
if (--this.#state.consumers > 0) return;
closeState(this.#state, abort);
closeState(this.#state);
}
}
4 changes: 2 additions & 2 deletions kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Moq.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class Moq internal constructor(
/**
* Create an unannounced broadcast at [path], invisible to everyone until announced.
*
* Advertise it with `announce` after populating tracks. `close()` (or `use`) ends it once no
* `dynamic()` handle remains.
* Advertise it with `announce` after populating tracks. `end()` ends it for good; `close()`
* (or `use`) releases the handle, which ends it once no `dynamic()` handle remains.
*/
fun createBroadcast(path: String): BroadcastProducer = session.publish().createBroadcast(path)

Expand Down
5 changes: 3 additions & 2 deletions kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Server.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ class Server internal constructor(
* Create a live broadcast at [path], served to incoming sessions.
*
* The origin announces the path so subscribers can discover it, becoming visible
* Advertise it with `announce` after populating tracks. `close()` (or `use`)
* ends it once no `dynamic()` handle remains.
* Advertise it with `announce` after populating tracks. `end()` ends it for
* good; `close()` (or `use`) releases the handle, which ends it once no
* `dynamic()` handle remains.
*/
fun createBroadcast(path: String): BroadcastProducer {
val origin = publishOrigin ?: throw IllegalStateException("no publish origin configured")
Expand Down
13 changes: 13 additions & 0 deletions kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,19 @@ class SmokeTest {
assertFailsWith<MoqException> { consumer.subscribeTrack("events", null) }
}

@Test
fun `ending a broadcast ends it while a dynamic handle remains`() = runTest {
BroadcastProducer().use { broadcast ->
broadcast.dynamic().use {
val consumer = broadcast.consume()
// Releasing the producer alone would leave the dynamic handle holding it open.
broadcast.end()
broadcast.end()
assertFailsWith<MoqException> { consumer.subscribeTrack("events", null) }
}
}
}

@Test
fun `broadcast updates shared video properties`() {
BroadcastProducer().use { broadcast ->
Expand Down
Loading
Loading