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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cpp/obs/src/moq-output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -301,10 +301,10 @@ void MoQOutput::Reset()
}
audio_tracks.clear();

// Finish the broadcast so the origin unpublishes it immediately; Start()
// Close the broadcast so the origin retracts it immediately; Start()
// creates a fresh one on restart.
if (broadcast > 0) {
moq_publish_finish(broadcast);
moq_publish_close(broadcast);
broadcast = 0;
}
}
Expand Down
2 changes: 1 addition & 1 deletion cpp/obs/test/moq-output-test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ int32_t moq_publish_announce(uint32_t, const moq_route *)
return 0;
}

int32_t moq_publish_finish(uint32_t)
int32_t moq_publish_close(uint32_t)
{
return 0;
}
Expand Down
3 changes: 2 additions & 1 deletion dart/moq/test/moq_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ void main() {
client.close();
serverSession.cancel(code: 0);
track.finish();
broadcast.finish();
broadcast.close();
broadcast.close(); // a second close is a no-op
server.close();
});

Expand Down
30 changes: 26 additions & 4 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import "dart:ffi";
import "dart:io" show Platform, File, Directory;
import "dart:isolate";
import "dart:typed_data";

import "package:ffi/ffi.dart";

import "uniffi_runtime.dart";
export "uniffi_runtime.dart";

Expand Down Expand Up @@ -6125,6 +6123,7 @@ abstract class MoqBroadcastProducerInterface {
required MoqJsonStreamConfig config,
});
void announce({required MoqRoute route});
void close();
MoqBroadcastConsumer consume();
MoqBroadcastDynamic dynamic_();
void finish();
Expand Down Expand Up @@ -6266,6 +6265,15 @@ class MoqBroadcastProducer implements MoqBroadcastProducerInterface {
}, moqExceptionErrorHandler);
}

void close() {
return rustCall((status) {
uniffi_moq_ffi_fn_method_moqbroadcastproducer_close(
uniffiClonePointer(),
status,
);
}, moqExceptionErrorHandler);
}

MoqBroadcastConsumer consume() {
return rustCallWithLifter(
(status) => uniffi_moq_ffi_fn_method_moqbroadcastproducer_consume(
Expand Down Expand Up @@ -10530,6 +10538,14 @@ external void uniffi_moq_ffi_fn_method_moqbroadcastproducer_announce(
Pointer<RustCallStatus> uniffiStatus,
);

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

@Native<Pointer<Void> Function(Pointer<Void>, Pointer<RustCallStatus>)>(
assetId: _uniffiAssetId,
)
Expand Down Expand Up @@ -12202,6 +12218,9 @@ uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_json_stream();
@Native<Uint16 Function()>(assetId: _uniffiAssetId)
external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_announce();

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

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

Expand Down Expand Up @@ -12727,7 +12746,7 @@ void _checkApiChecksums() {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqannouncedbroadcast_available() !=
42497) {
37458) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqannouncedbroadcast_cancel() != 63175) {
Expand Down Expand Up @@ -12799,13 +12818,16 @@ void _checkApiChecksums() {
if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_announce() != 13700) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_close() != 19191) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_consume() != 27634) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_dynamic() != 55635) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_finish() != 7183) {
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() !=
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 @@ -42,7 +42,7 @@ and `target/include/moq.h`.
- **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), 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 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); 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; `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 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); name the dot segment in `prefix` to list them.

```c
moq_client_config config;
Expand Down
3 changes: 2 additions & 1 deletion doc/lib/dart/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ await for (final request in server.requests()) {
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; `origin.dynamic_(prefix:, route:)` claims `prefix` and
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 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
5 changes: 3 additions & 2 deletions doc/lib/go/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,16 @@ video, _ := broadcast.EncodeVideo(
)
_ = video.Write(moq.VideoFrame{TimestampUs: pts, Data: rgba})
_ = broadcast.Announce(moq.Route{})
broadcast.Finish() // keep the producer reachable while publishing, then finish explicitly
broadcast.Close() // keep the producer reachable while publishing, then close explicitly
```

For locally encoded media, call `MediaProducer.Flush(timestampUs)` after `WriteFrame` with the same broadcast-clock PTS. It measures catalog jitter at the transport handoff. File, pipe, and network imports should omit `Flush`; built-in encoders observe their own output.

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; `origin.Dynamic(prefix, route)` claims `prefix` and every
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
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
3 changes: 2 additions & 1 deletion doc/lib/kt/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ Moq.connect("https://relay.example.com").use { moq ->
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; `origin.dynamic(prefix, route)` claims `prefix` and every
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
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
4 changes: 3 additions & 1 deletion doc/lib/py/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ For already-encoded live output, call `audio.flush(timestamp_us)` after each `au
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;
`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 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
6 changes: 5 additions & 1 deletion doc/lib/rs/moq-net.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,12 @@ Three operations, on an origin:
- `broadcast.announce(route)` / `broadcast.unannounce()` own that
advertisement. Announcing again re-prices the standing route, which competes
on cost with remote routes at the same path (a tie goes to the local
broadcast). The route retracts on `unannounce()`, `finish()`, or the last
broadcast). The route retracts on `unannounce()`, `close()`, or the last
producer dropping; tracks already in flight carry on to their own end.
- `broadcast.close()` ends the broadcast for good: it retracts, leaves local
discovery, and answers every later track lookup with `Unroutable`. Tracks
already subscribed carry on to their own end. It can never be announced
again. Dropping the last producer does the same.
- `origin.dynamic(prefix, route)` claims `prefix` and every path beneath it
(`""` claims everything). Hold the returned `origin::Dynamic` while the
claim should stay advertised; drop it to retract. A request beneath it with
Expand Down
4 changes: 3 additions & 1 deletion doc/lib/swift/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ For already-encoded live output, call `audio.flush(timestampUs:)` after `writeFr

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;
`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);
`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
6 changes: 3 additions & 3 deletions go/wrapper/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ func ExampleClient_CreateBroadcast() {
if err != nil {
log.Fatal(err)
}
// Finishing unpublishes the broadcast immediately.
defer broadcast.Finish()
// Closing ends the broadcast for good.
defer broadcast.Close()

media, err := broadcast.PublishAudio(moq.AudioFormatOpus, opusHead())
if err != nil {
Expand Down Expand Up @@ -91,7 +91,7 @@ func ExampleBroadcastProducer_PublishVideo_videoHint() {
if err != nil {
log.Fatal(err)
}
defer broadcast.Finish()
defer broadcast.Close()

media, err := broadcast.PublishVideo(moq.VideoFormatAvc3, nil, moq.WithVideoHint(moq.VideoHint{}))
if err != nil {
Expand Down
46 changes: 31 additions & 15 deletions go/wrapper/moq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func TestDynamicBroadcastRequest(t *testing.T) {
if err := track.Finish(); err != nil {
t.Fatal(err)
}
if err := served.Finish(); err != nil {
if err := served.Close(); err != nil {
t.Fatal(err)
}
}
Expand All @@ -135,11 +135,27 @@ func TestPublishAudioLifecycle(t *testing.T) {
if err := media.Finish(); err != nil {
t.Fatal(err)
}
if err := broadcast.Finish(); err != nil {
if err := broadcast.Close(); err != nil {
t.Fatal(err)
}
}

func TestBroadcastCloseTwiceIsNoop(t *testing.T) {
broadcast, err := moq.NewBroadcastProducer()
if err != nil {
t.Fatal(err)
}
if err := broadcast.Close(); err != nil {
t.Fatal(err)
}
if err := broadcast.Close(); err != nil {
t.Fatalf("second Close: %v", err)
}
if _, err := broadcast.PublishTrack("events", nil); err == nil {
t.Fatal("PublishTrack after Close succeeded")
}
}

func TestEncodeAudioWithOpusObject(t *testing.T) {
// The producer retains the codec, so releasing the codec and the
// config in either order must still encode.
Expand Down Expand Up @@ -179,7 +195,7 @@ func TestEncodeAudioWithOpusObject(t *testing.T) {
if err := producer.Finish(); err != nil {
t.Fatal(err)
}
if err := broadcast.Finish(); err != nil {
if err := broadcast.Close(); err != nil {
t.Fatal(err)
}
}
Expand Down Expand Up @@ -221,7 +237,7 @@ func TestEncodeAudioFrameDurations(t *testing.T) {
t.Fatalf("err = %v, want ErrAudio: 2 ms is not an opus frame duration", err)
}

if err := broadcast.Finish(); err != nil {
if err := broadcast.Close(); err != nil {
t.Fatal(err)
}
}
Expand All @@ -235,7 +251,7 @@ func TestVideoPropertiesUseDefaultedFields(t *testing.T) {
if err := broadcast.SetVideoProperties(moq.VideoProperties{Rotation: &rotation}); err != nil {
t.Fatal(err)
}
if err := broadcast.Finish(); err != nil {
if err := broadcast.Close(); err != nil {
t.Fatal(err)
}
}
Expand Down Expand Up @@ -350,7 +366,7 @@ func TestDecodeVideoFormat(t *testing.T) {
if err := video.Finish(); err != nil {
t.Fatal(err)
}
if err := broadcast.Finish(); err != nil {
if err := broadcast.Close(); err != nil {
t.Fatal(err)
}
}
Expand Down Expand Up @@ -806,7 +822,7 @@ func TestDynamicTrackRequest(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer broadcast.Finish()
defer broadcast.Close()

dynamic, err := broadcast.Dynamic()
if err != nil {
Expand Down Expand Up @@ -882,7 +898,7 @@ func TestDynamicTrackRequestCanPublishAudio(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer broadcast.Finish()
defer broadcast.Close()

dynamic, err := broadcast.Dynamic()
if err != nil {
Expand Down Expand Up @@ -974,7 +990,7 @@ func TestRecvGroupCancelRace(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer broadcast.Finish()
defer broadcast.Close()

var wg sync.WaitGroup
for i := 0; i < 16; i++ {
Expand Down Expand Up @@ -1007,7 +1023,7 @@ func TestConsumerCancelConcurrent(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer broadcast.Finish()
defer broadcast.Close()

track, err := broadcast.PublishTrack("x", nil)
if err != nil {
Expand Down Expand Up @@ -1075,7 +1091,7 @@ func TestRequestBroadcastCancelKeepsTheOrigin(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer served.Finish()
defer served.Close()

resolved := make(chan error, 1)
go func() {
Expand Down Expand Up @@ -1108,7 +1124,7 @@ func TestSubscribeTrackCancelKeepsTheBroadcast(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer broadcast.Finish()
defer broadcast.Close()

dynamic, err := broadcast.Dynamic()
if err != nil {
Expand Down Expand Up @@ -1177,7 +1193,7 @@ func TestUsedCancelKeepsTheTrack(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer broadcast.Finish()
defer broadcast.Close()

track, err := broadcast.PublishTrack("status", nil)
if err != nil {
Expand Down Expand Up @@ -1224,7 +1240,7 @@ func TestCancelDoesNotLeakGoroutines(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer broadcast.Finish()
defer broadcast.Close()

dynamic, err := broadcast.Dynamic()
if err != nil {
Expand Down Expand Up @@ -1338,7 +1354,7 @@ func TestBroadcastIsReachableOnlyWhileAnnounced(t *testing.T) {
if _, err := consumer.RequestBroadcast(ctx, "live"); err != nil {
t.Fatal(err)
}
if err := broadcast.Finish(); err != nil {
if err := broadcast.Close(); err != nil {
t.Fatal(err)
}
}
Expand Down
5 changes: 2 additions & 3 deletions go/wrapper/origin.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ func (o *OriginProducer) Dynamic(prefix string, route Route) (*OriginDynamic, er
//
// The broadcast is invisible and unroutable, for this origin's consumers and
// peers alike, until [BroadcastProducer.Announce]. Announce it after
// populating tracks. Finish unpublishes immediately, while dropping the
// producer without finishing also unpublishes but reads to subscribers as a
// failure rather than a deliberate end.
// populating tracks. [BroadcastProducer.Close] ends it for good; dropping the
// last handle does the same.
func (o *OriginProducer) CreateBroadcast(path string) (*BroadcastProducer, error) {
inner, err := o.inner.CreateBroadcast(path)
if err != nil {
Expand Down
Loading
Loading