diff --git a/cpp/obs/src/moq-settings.cpp b/cpp/obs/src/moq-settings.cpp index 07ef5ed989..17cc2b8f57 100644 --- a/cpp/obs/src/moq-settings.cpp +++ b/cpp/obs/src/moq-settings.cpp @@ -196,16 +196,16 @@ const DefaultValues &LibraryDefaults() return d; } - d.connect_timeout_ms = (long long)config.connect_timeout_ms; - d.failover_delay_ms = (long long)config.failover_delay_ms; + d.connect_timeout_ms = (long long)(config.connect_timeout_us / 1000); + d.failover_delay_ms = (long long)(config.failover_delay_us / 1000); d.backoff_initial_ms = (long long)(config.backoff_initial_us / 1000); d.backoff_max_ms = (long long)(config.backoff_max_us / 1000); d.backoff_timeout_ms = (long long)(config.backoff_timeout_us / 1000); d.quic_max_streams = (long long)config.quic_max_streams; - d.quic_idle_timeout_ms = (long long)config.quic_idle_timeout_ms; + d.quic_idle_timeout_ms = (long long)(config.quic_idle_timeout_us / 1000); // Absent means "no keep-alive", which the UI shows as zero. - d.quic_keep_alive_ms = config.has_quic_keep_alive ? (long long)config.quic_keep_alive_ms : 0; - d.websocket_delay_ms = config.has_websocket_delay ? (long long)config.websocket_delay_ms : 0; + d.quic_keep_alive_ms = config.has_quic_keep_alive ? (long long)(config.quic_keep_alive_us / 1000) : 0; + d.websocket_delay_ms = config.has_websocket_delay ? (long long)(config.websocket_delay_us / 1000) : 0; d.websocket_enabled = config.websocket_enabled; d.loaded = true; return d; @@ -403,9 +403,9 @@ bool BuildConfig(obs_data_t *settings, Config *out) borrow(OptionalString(settings, BACKEND), &out->backend, &config.backend, &config.backend_len); borrow(OptionalString(settings, BIND), &out->bind, &config.bind, &config.bind_len); - config.connect_timeout_ms = (uint64_t)Amount(settings, CONNECT_TIMEOUT); + config.connect_timeout_us = (uint64_t)Amount(settings, CONNECT_TIMEOUT) * 1000; config.has_connect_timeout = true; - config.failover_delay_ms = (uint64_t)Amount(settings, FAILOVER_DELAY); + config.failover_delay_us = (uint64_t)Amount(settings, FAILOVER_DELAY) * 1000; config.has_failover_delay = true; config.tls_disable_verify = obs_data_get_bool(settings, TLS_DISABLE_VERIFY); @@ -436,9 +436,9 @@ bool BuildConfig(obs_data_t *settings, Config *out) config.quic_max_streams = (uint64_t)Amount(settings, QUIC_MAX_STREAMS); config.has_quic_max_streams = true; - config.quic_idle_timeout_ms = (uint64_t)Amount(settings, QUIC_IDLE_TIMEOUT); + config.quic_idle_timeout_us = (uint64_t)Amount(settings, QUIC_IDLE_TIMEOUT) * 1000; config.has_quic_idle_timeout = true; - config.quic_keep_alive_ms = (uint64_t)Amount(settings, QUIC_KEEP_ALIVE); + config.quic_keep_alive_us = (uint64_t)Amount(settings, QUIC_KEEP_ALIVE) * 1000; config.has_quic_keep_alive = true; // The tri-states stay unset when the user left them on Automatic, which is what @@ -465,7 +465,7 @@ bool BuildConfig(obs_data_t *settings, Config *out) config.websocket_enabled = obs_data_get_bool(settings, WEBSOCKET_ENABLED); config.has_websocket_enabled = true; - config.websocket_delay_ms = (uint64_t)Amount(settings, WEBSOCKET_DELAY); + config.websocket_delay_us = (uint64_t)Amount(settings, WEBSOCKET_DELAY) * 1000; config.has_websocket_delay = true; return true; diff --git a/cpp/obs/src/moq-source.cpp b/cpp/obs/src/moq-source.cpp index 45d0c56f04..43b38fa1db 100644 --- a/cpp/obs/src/moq-source.cpp +++ b/cpp/obs/src/moq-source.cpp @@ -225,7 +225,7 @@ struct subscription_ref { subscription_ref &operator=(const subscription_ref &) = delete; }; -// user_data for a single moq_origin_consume_announced. The generation must travel +// user_data for a single moq_origin_announced_broadcast. The generation must travel // with the request rather than live on ctx: a reconnect can issue a new request while // an older one still has a delivery in flight, and a single slot on ctx would let // that stale delivery read the new generation and pass the staleness check. @@ -686,14 +686,14 @@ static void moq_source_subscribe_video(struct moq_source *ctx, int32_t catalog, ctx->video_track = track; pthread_mutex_unlock(&ctx->mutex); if (old_track >= 0) - moq_consume_video_close(old_track); + moq_consume_video_cancel(old_track); LOG_INFO("Subscribed to video track successfully"); } else { // Stale or shutting down: close the track we just created; its terminal // callback releases the reference added above. pthread_mutex_unlock(&ctx->mutex); if (!video_state->terminal.load()) - moq_consume_video_close(track); + moq_consume_video_cancel(track); } } @@ -794,12 +794,12 @@ static void moq_source_subscribe_audio(struct moq_source *ctx, int32_t catalog, ctx->audio_track = track; pthread_mutex_unlock(&ctx->mutex); if (old_track >= 0) - moq_consume_audio_close(old_track); + moq_consume_audio_cancel(old_track); LOG_INFO("Subscribed to audio track successfully (%u Hz, %u ch)", sample_rate, channels); } else { pthread_mutex_unlock(&ctx->mutex); if (!audio_state->terminal.load()) - moq_consume_audio_close(track); + moq_consume_audio_cancel(track); } } @@ -1013,7 +1013,7 @@ static void moq_source_start_consume(struct moq_source *ctx, uint32_t expected_g // so it need not outlive this call, and delivers the broadcast handle // asynchronously to on_broadcast. int32_t request = - moq_origin_consume_announced(origin, broadcast_copy, strlen(broadcast_copy), on_broadcast, req); + moq_origin_announced_broadcast(origin, broadcast_copy, strlen(broadcast_copy), on_broadcast, req); if (request < 0) { LOG_ERROR("Failed to request broadcast '%s': %d", broadcast_copy, request); bfree(broadcast_copy); @@ -1041,13 +1041,13 @@ static void moq_source_start_consume(struct moq_source *ctx, uint32_t expected_g } else { // Stale or shutting down: close it; its terminal releases the reference. pthread_mutex_unlock(&ctx->mutex); - moq_origin_consume_announced_close(request); + moq_origin_announced_broadcast_cancel(request); } } // Receives the announced broadcast: a positive handle once announced, then exactly // once more with a terminal code (0 = finished, including after -// moq_origin_consume_announced_close; < 0 = error). The terminal is the last touch +// moq_origin_announced_broadcast_cancel; < 0 = error). The terminal is the last touch // of user_data, so it both frees the request context and releases the request's // lifetime reference via subscription_ref. static void on_broadcast(void *user_data, int32_t broadcast) @@ -1137,7 +1137,7 @@ static void on_broadcast(void *user_data, int32_t broadcast) // Stale or shutting down: close it; its terminal releases the reference. pthread_mutex_unlock(&ctx->mutex); if (!state->terminal.load()) - moq_consume_catalog_close(catalog_handle); + moq_consume_catalog_cancel(catalog_handle); } } @@ -1152,7 +1152,7 @@ static void moq_source_clear_video_locked(struct moq_source *ctx) { ctx->video_attempt++; if (ctx->video_track >= 0) { - moq_consume_video_close(ctx->video_track); + moq_consume_video_cancel(ctx->video_track); ctx->video_track = -1; } moq_source_destroy_decoder_locked(ctx); @@ -1170,7 +1170,7 @@ static void moq_source_disconnect_locked(struct moq_source *ctx) moq_source_clear_audio_locked(ctx); if (ctx->catalog_handle >= 0) { - moq_consume_catalog_close(ctx->catalog_handle); + moq_consume_catalog_cancel(ctx->catalog_handle); ctx->catalog_handle = -1; } @@ -1178,7 +1178,7 @@ static void moq_source_disconnect_locked(struct moq_source *ctx) // fire (with 0) instead of leaving it pending until the source dies. This is the // path that ends a wait for a broadcast that is never announced. if (ctx->request >= 0) { - moq_origin_consume_announced_close(ctx->request); + moq_origin_announced_broadcast_cancel(ctx->request); ctx->request = -1; } @@ -1636,7 +1636,7 @@ static void moq_source_clear_audio_locked(struct moq_source *ctx) { ctx->audio_attempt++; if (ctx->audio_track >= 0) { - moq_consume_audio_close(ctx->audio_track); + moq_consume_audio_cancel(ctx->audio_track); ctx->audio_track = -1; } moq_source_destroy_audio_decoder_locked(ctx); diff --git a/cpp/obs/test/moq-source-test.cpp b/cpp/obs/test/moq-source-test.cpp index b55a03bc70..ec9e7e4799 100644 --- a/cpp/obs/test/moq-source-test.cpp +++ b/cpp/obs/test/moq-source-test.cpp @@ -557,7 +557,7 @@ std::atomic g_origin_closes{0}; std::atomic g_session_connects{0}; std::atomic g_announced_calls{0}; // moq_origin_request resolves only broadcasts that are already announced. The -// source must wait with moq_origin_consume_announced, so this stays zero. +// source must wait with moq_origin_announced_broadcast, so this stays zero. std::atomic g_request_calls{0}; std::atomic g_catalog_calls{0}; std::atomic g_video_calls{0}; @@ -735,8 +735,8 @@ int32_t moq_session_close(uint32_t session) return closeSub(static_cast(session)); } -int32_t moq_origin_consume_announced(uint32_t, const char *, uintptr_t, void (*on_broadcast)(void *, int32_t), - void *user_data) +int32_t moq_origin_announced_broadcast(uint32_t, const char *, uintptr_t, void (*on_broadcast)(void *, int32_t), + void *user_data) { if (g_announced_result < 0) return g_announced_result; @@ -746,7 +746,7 @@ int32_t moq_origin_consume_announced(uint32_t, const char *, uintptr_t, void (*o return handle; } -int32_t moq_origin_consume_announced_close(uint32_t task) +int32_t moq_origin_announced_broadcast_cancel(uint32_t task) { return closeSub(static_cast(task)); } @@ -769,7 +769,7 @@ int32_t moq_consume_catalog(uint32_t, void (*on_catalog)(void *, int32_t), void return handle; } -int32_t moq_consume_catalog_close(uint32_t catalog) +int32_t moq_consume_catalog_cancel(uint32_t catalog) { return closeSub(static_cast(catalog)); } @@ -832,7 +832,7 @@ int32_t moq_consume_video(uint32_t catalog, uint32_t, uint64_t, void (*on_frame) return handle; } -int32_t moq_consume_video_close(uint32_t track) +int32_t moq_consume_video_cancel(uint32_t track) { return closeSub(static_cast(track)); } @@ -882,7 +882,7 @@ int32_t moq_consume_audio(uint32_t catalog, uint32_t, uint64_t, void (*on_frame) return handle; } -int32_t moq_consume_audio_close(uint32_t track) +int32_t moq_consume_audio_cancel(uint32_t track) { g_audio_closes++; return closeSub(static_cast(track)); diff --git a/dart/moq/README.md b/dart/moq/README.md index 872141ef23..db424df1f9 100644 --- a/dart/moq/README.md +++ b/dart/moq/README.md @@ -7,7 +7,8 @@ import 'package:moq/moq.dart'; final connection = await Moq.connect('https://relay.example.com'); await for (final announcement in connection.announcements()) { - print(announcement.path()); + // The covered prefix is relative to the requested announcements prefix. + print(announcement.prefix()); } ``` diff --git a/dart/moq/lib/moq.dart b/dart/moq/lib/moq.dart index 597d30b299..3a13145c28 100644 --- a/dart/moq/lib/moq.dart +++ b/dart/moq/lib/moq.dart @@ -30,7 +30,7 @@ final class Moq { }) async { final client = MoqClient(); try { - if (!tlsVerify) client.setTlsDisableVerify(disable: true); + if (!tlsVerify) client.setTlsVerify(verify: false); if (tlsRoots != null) client.setTlsRoots(paths: tlsRoots); if (tlsSystemRoots != null) { client.setTlsSystemRoots(systemRoots: tlsSystemRoots); @@ -60,7 +60,7 @@ final class Moq { MoqBroadcastProducer createBroadcast(String path) => session.publish().createBroadcast(path: path); - /// Stream announcements whose paths begin with [prefix]. + /// Stream routes under requested [prefix]; updates return relative covered prefixes. Stream announcements({String prefix = ''}) async* { final announced = session.consume().announced(prefix: prefix); try { @@ -75,7 +75,7 @@ final class Moq { } } - /// Return the raw announcement cursor for [prefix]. + /// Return the raw cursor for requested [prefix]; updates return relative covered prefixes. MoqAnnounceConsumer announced({String prefix = ''}) => session.consume().announced(prefix: prefix); diff --git a/dart/moq/test/moq_test.dart b/dart/moq/test/moq_test.dart index d4eb39eef6..2d9780465f 100644 --- a/dart/moq/test/moq_test.dart +++ b/dart/moq/test/moq_test.dart @@ -34,10 +34,10 @@ void main() { final track = broadcast.publishTrack(name: 'events', info: null); broadcast.announce(route: MoqRoute()); final announced = await announcement.timeout(timeout); - expect(announced.path(), 'live'); + expect(announced.prefix(), 'live'); final requested = await client - .requestBroadcast(announced.path()) + .requestBroadcast(announced.prefix()) .timeout(timeout); final consumer = await requested .subscribeTrack(name: 'events', subscription: null) @@ -71,12 +71,12 @@ void main() { final announced = origin.consume().announced(prefix: ''); final first = await announced.next().timeout(timeout); - expect(first?.path(), 'live'); + expect(first?.prefix(), 'live'); expect(first?.active(), isTrue); broadcast.unannounce(); final retracted = await announced.next().timeout(timeout); - expect(retracted?.path(), 'live'); + expect(retracted?.prefix(), 'live'); expect(retracted?.active(), isFalse); await origin.consume().requestBroadcast(path: 'live').timeout(timeout); announced.cancel(); diff --git a/dart/moq_ffi/lib/src/moq.dart b/dart/moq_ffi/lib/src/moq.dart index 1bb7f9bf6d..ba771e3b8a 100644 --- a/dart/moq_ffi/lib/src/moq.dart +++ b/dart/moq_ffi/lib/src/moq.dart @@ -3660,6 +3660,48 @@ class FfiConverterMoqVideoFormat { } } +enum MoqTransport { quic, iroh, webSocket, tcp, unix } + +class FfiConverterMoqTransport { + static LiftRetVal read(Uint8List buf) { + final index = buf.buffer.asByteData(buf.offsetInBytes).getInt32(0); + switch (index) { + case 1: + return LiftRetVal(MoqTransport.quic, 4); + case 2: + return LiftRetVal(MoqTransport.iroh, 4); + case 3: + return LiftRetVal(MoqTransport.webSocket, 4); + case 4: + return LiftRetVal(MoqTransport.tcp, 4); + case 5: + return LiftRetVal(MoqTransport.unix, 4); + default: + throw UniffiInternalError( + UniffiInternalError.unexpectedEnumCase, + "Unable to determine enum variant", + ); + } + } + + static MoqTransport lift(RustBuffer buffer) { + return FfiConverterMoqTransport.read(buffer.asUint8List()).value; + } + + static RustBuffer lower(MoqTransport input) { + return toRustBuffer(createUint8ListFromInt(input.index + 1)); + } + + static int allocationSize(MoqTransport _value) { + return 4; + } + + static int write(MoqTransport value, Uint8List buf) { + buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, value.index + 1); + return 4; + } +} + enum MoqConnectionStatus { connected, disconnected, migrating } class FfiConverterMoqConnectionStatus { @@ -4967,7 +5009,7 @@ class FfiConverterMoqAnnounceConsumer { abstract class MoqAnnounceUpdateInterface { bool active(); - String path(); + String prefix(); MoqRoute route(); } @@ -5007,9 +5049,9 @@ class MoqAnnounceUpdate implements MoqAnnounceUpdateInterface { ); } - String path() { + String prefix() { return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqannounceupdate_path( + (status) => uniffi_moq_ffi_fn_method_moqannounceupdate_prefix( uniffiClonePointer(), status, ), @@ -6912,7 +6954,7 @@ abstract class MoqRequestInterface { Future reject({required int code}); void setConsume({required MoqOriginProducer? origin}); void setPublish({required MoqOriginProducer? origin}); - String transport(); + MoqTransport transport(); String? url(); } @@ -7012,13 +7054,13 @@ class MoqRequest implements MoqRequestInterface { }, moqExceptionErrorHandler); } - String transport() { + MoqTransport transport() { return rustCallWithLifter( (status) => uniffi_moq_ffi_fn_method_moqrequest_transport( uniffiClonePointer(), status, ), - FfiConverterString.lift, + FfiConverterMoqTransport.lift, null, ); } @@ -7238,11 +7280,11 @@ abstract class MoqClientInterface { void setQuicMaxStreams({required int maxStreams}); void setReconnect({required bool enabled}); void setTlsCert({required String? path}); - void setTlsDisableVerify({required bool disable}); void setTlsFingerprints({required List fingerprints}); void setTlsKey({required String? path}); void setTlsRoots({required List paths}); void setTlsSystemRoots({required bool systemRoots}); + void setTlsVerify({required bool verify}); } final _MoqClientFinalizer = Finalizer>((ptr) { @@ -7365,16 +7407,6 @@ class MoqClient implements MoqClientInterface { }, moqExceptionErrorHandler); } - void setTlsDisableVerify({required bool disable}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_tls_disable_verify( - uniffiClonePointer(), - FfiConverterBool.lower(disable), - status, - ); - }, moqExceptionErrorHandler); - } - void setTlsFingerprints({required List fingerprints}) { return rustCall((status) { uniffi_moq_ffi_fn_method_moqclient_set_tls_fingerprints( @@ -7414,6 +7446,16 @@ class MoqClient implements MoqClientInterface { ); }, moqExceptionErrorHandler); } + + void setTlsVerify({required bool verify}) { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqclient_set_tls_verify( + uniffiClonePointer(), + FfiConverterBool.lower(verify), + status, + ); + }, moqExceptionErrorHandler); + } } class FfiConverterMoqClient { @@ -9466,7 +9508,7 @@ external int uniffi_moq_ffi_fn_method_moqannounceupdate_active( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external RustBuffer uniffi_moq_ffi_fn_method_moqannounceupdate_path( +external RustBuffer uniffi_moq_ffi_fn_method_moqannounceupdate_prefix( Pointer ptr, Pointer uniffiStatus, ); @@ -10714,15 +10756,6 @@ external void uniffi_moq_ffi_fn_method_moqclient_set_tls_cert( Pointer uniffiStatus, ); -@Native, Int8, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_tls_disable_verify( - Pointer ptr, - int disable, - Pointer uniffiStatus, -); - @Native, RustBuffer, Pointer)>( assetId: _uniffiAssetId, ) @@ -10759,6 +10792,15 @@ external void uniffi_moq_ffi_fn_method_moqclient_set_tls_system_roots( Pointer uniffiStatus, ); +@Native, Int8, Pointer)>( + assetId: _uniffiAssetId, +) +external void uniffi_moq_ffi_fn_method_moqclient_set_tls_verify( + Pointer ptr, + int verify, + Pointer uniffiStatus, +); + @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) @@ -11301,7 +11343,7 @@ external int uniffi_moq_ffi_checksum_method_moqannounceconsumer_next(); external int uniffi_moq_ffi_checksum_method_moqannounceupdate_active(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqannounceupdate_path(); +external int uniffi_moq_ffi_checksum_method_moqannounceupdate_prefix(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqannounceupdate_route(); @@ -11636,9 +11678,6 @@ external int uniffi_moq_ffi_checksum_method_moqclient_set_reconnect(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqclient_set_tls_cert(); -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_tls_disable_verify(); - @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqclient_set_tls_fingerprints(); @@ -11651,6 +11690,9 @@ external int uniffi_moq_ffi_checksum_method_moqclient_set_tls_roots(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqclient_set_tls_system_roots(); +@Native(assetId: _uniffiAssetId) +external int uniffi_moq_ffi_checksum_method_moqclient_set_tls_verify(); + @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqsession_bandwidth(); @@ -11836,7 +11878,7 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqannounceupdate_active() != 49521) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqannounceupdate_path() != 7124) { + if (uniffi_moq_ffi_checksum_method_moqannounceupdate_prefix() != 10019) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqannounceupdate_route() != 8074) { @@ -11858,7 +11900,7 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqbroadcastrequest_reject() != 9727) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqoriginconsumer_announced() != 45144) { + if (uniffi_moq_ffi_checksum_method_moqoriginconsumer_announced() != 36171) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqoriginconsumer_announced_broadcast() != @@ -12115,7 +12157,7 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqrequest_set_publish() != 10746) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqrequest_transport() != 5942) { + if (uniffi_moq_ffi_checksum_method_moqrequest_transport() != 57171) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqrequest_url() != 34138) { @@ -12179,10 +12221,6 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqclient_set_tls_cert() != 12773) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_tls_disable_verify() != - 2912) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } if (uniffi_moq_ffi_checksum_method_moqclient_set_tls_fingerprints() != 50038) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); @@ -12197,6 +12235,9 @@ void _checkApiChecksums() { 10239) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } + if (uniffi_moq_ffi_checksum_method_moqclient_set_tls_verify() != 64525) { + throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); + } if (uniffi_moq_ffi_checksum_method_moqsession_bandwidth() != 8006) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } diff --git a/doc/lib/c/index.md b/doc/lib/c/index.md index 17e392719f..59b173b2dd 100644 --- a/doc/lib/c/index.md +++ b/doc/lib/c/index.md @@ -30,17 +30,17 @@ and `target/include/moq.h`. ## Shape of the API -- **Handles and callbacks.** Every object is an integer handle; every async result arrives on a callback with a `void *user_data`. A status `> 0` is a live result, `0` a clean close, `< 0` an error, and the last two are terminal: libmoq never touches `user_data` again, so free it there. `*_close` only requests shutdown; the terminal callback still fires. -- **Errors.** Negative return codes, with `moq_error()` giving the reason for the last failure on the calling thread. Auth rejections (401, 403) have their own codes so you don't retry them. A protocol failure also fills `moq_error_protocol()` with the session or stream scope, the verbatim wire code, and a known kind; do not parse `moq_error()` for that. +- **Handles and callbacks.** Every object is an integer handle; every async result arrives on a required `moq_status_callback` with a `void *user_data`. A status `> 0` is a live result, `0` a clean close, `< 0` an error, and the last two are terminal: libmoq never touches `user_data` again, so free it there. `*_cancel` only requests shutdown; the terminal callback still fires. +- **Errors.** Negative return codes named by `MOQ_ERROR_*` in `moq.h`, with `moq_error()` giving the reason for the last failure on the calling thread. Auth rejections (401, 403) have their own codes so you don't retry them. A protocol failure also fills `moq_error_protocol()` with the session or stream scope, the verbatim wire code, and a known kind; do not parse `moq_error()` for that. - **Threading.** Any function from any thread. Raw publish calls block until the codec takes the frame, which paces a publisher. - **Connection health.** `moq_session_stats()` reports available metrics with per-field validity flags. `moq_session_snapshot()` samples those metrics and the negotiated draft name together from the same connection. Its protocol string is backed by static storage. Both return an offline error between reconnects and leave the destination untouched. `moq_session_bandwidth()` mints an allocator over the send estimate; `moq_bandwidth_reserve` claims a share for an app-owned track, and `moq_encode_video` / `moq_encode_audio` take the same handle so the built-in video encoder follows the grant. - **Raw playback.** Raw audio and video consumers start at the newest cached group when opened, so rebuilding a live decoder skips the retained backlog. - **Raw decode output.** `moq_video_decoder_output` selects the decoded CPU pixel format (`MOQ_VIDEO_PIXEL_FORMAT_I420` or `_RGBA`) and target size (`width`/`height`, both zero for native; otherwise even and non-zero). Unknown formats and invalid sizes fail `moq_decode_video` before subscribing; accepted requests deliver exactly that layout or fail on the terminal callback. - **Encoded video metadata.** `moq_video_init.hint` is a zero-initialized `moq_video_hint` with `has_*` flags for coded dimensions, bitrate (bits per second), frame rate, and latency preference. Hints seed a video codec track's catalog; detected dimensions take precedence. -- **Client config.** A zeroed `moq_client_config` means the defaults for every knob, which is what lets a new one be appended without disturbing callers. Fields cover protocol (`versions`), TLS (`tls_fingerprints`, `tls_roots`, `tls_cert`/`_key`, `tls_host_name`), transport (`backend`, `bind`, `connect_timeout_ms`, the Happy Eyeballs delays, `websocket_enabled`), and tuning (reconnect backoff, `quic_*`). A knob whose default isn't zero carries a `has_*` flag, so setting `backoff_timeout_us = 0` needs `has_backoff_timeout = true` to mean "retry forever" rather than "use the default". `moq_client_defaults()` reports what a NULL config dials with. -- **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_close` stops it; the terminal callback still fires. A container has no single demand and is refused. Demand is counted at the producer: a session that served the track keeps a warm copy for 30 seconds after its last subscriber leaves, so an unused edge behind a relay arrives after that linger. -- **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 close with `moq_publish_dynamic_close`. -- **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 snapshot and stream tracks, group fetch, catalog sections, shared video properties, and stalled hints. The three advertising operations are `moq_origin_create_broadcast` (unadvertised producer), `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_announce_update.path` is the covered prefix. +- **Client config.** A zeroed `moq_client_config` means the defaults for every knob, which is what lets a new one be appended without disturbing callers. Fields cover protocol (`versions`), TLS (`tls_fingerprints`, `tls_roots`, `tls_cert`/`_key`, `tls_host_name`), transport (`backend`, `bind`, `connect_timeout_us`, the Happy Eyeballs delays, `websocket_enabled`), and tuning (reconnect backoff, `quic_*`). Every duration is in microseconds. A knob whose default isn't zero carries a `has_*` flag, so setting `backoff_timeout_us = 0` needs `has_backoff_timeout = true` to mean "retry forever" rather than "use the default". `moq_client_defaults()` reports what a NULL config dials with. +- **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 is counted at the producer: a session that served the track keeps a warm copy for 30 seconds after its last subscriber leaves, so an unused edge behind a relay arrives after that linger. +- **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 snapshot and stream tracks, group fetch, catalog sections, shared video properties, and stalled hints. The three advertising operations are `moq_origin_create_broadcast` (unadvertised producer), `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_announce_update.prefix` is the concrete covered prefix relative to the origin root. ```c moq_client_config config; diff --git a/doc/lib/dart/index.md b/doc/lib/dart/index.md index c24baeea9c..56eda09c0e 100644 --- a/doc/lib/dart/index.md +++ b/doc/lib/dart/index.md @@ -28,7 +28,7 @@ final moq = await Moq.connect('https://relay.example.com'); // Subscribe. The stream is live, so listen to it rather than awaiting its end. moq.announcements(prefix: 'live/').listen((announcement) { - print(announcement.path()); + print(announcement.prefix()); }); final broadcast = await moq.requestBroadcast('live/camera'); ``` @@ -50,8 +50,9 @@ advertisement; `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 -route is a capability, not an inventory; `announcement.path()` is the covered -prefix. +route is a capability, not an inventory. `announcements(prefix:)` is the +requested discovery scope; `announcement.prefix()` is the concrete covered +prefix relative to it. Sessions reconnect with backoff when the transport drops and re-announce local broadcasts. `moq.epoch` counts the connections, 1 on the first, pairing with @@ -61,7 +62,7 @@ inbound stream cap for a subscriber to many tracks. Cancelling a stream releases the native cursor. The package re-exports `moq_ffi`, so the full generated API is available without a second import. Generated configuration setters throw if a connect, listen, or accept is in -flight, or after `cancel()`. +flight, or after `cancel()`. Incoming requests report a `MoqTransport` enum. `ProtocolMoqException` carries a `MoqProtocolException` as `details` (scope, verbatim code, kind) when the peer sent a session or stream code. diff --git a/doc/lib/go/index.md b/doc/lib/go/index.md index 90a7cf17c2..ed4648dd80 100644 --- a/doc/lib/go/index.md +++ b/doc/lib/go/index.md @@ -36,8 +36,8 @@ for ann, err := range announced.All(ctx) { if moq.IsShutdown(err) { break } log.Fatal(err) } - // An announcement is a route; resolve the broadcast at its path. - broadcast, err := client.RequestBroadcast(ctx, "live/" + ann.Path()) + // The requested prefix scopes discovery; each update's prefix is relative to it. + broadcast, err := client.RequestBroadcast(ctx, "live/" + ann.Prefix()) if err != nil { log.Fatal(err) } @@ -73,8 +73,9 @@ The three advertising operations: `client.CreateBroadcast(path)` (or advertisement; `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; `ann.Path()` is the covered -prefix. +serve. A route is a capability, not an inventory. `Announced(prefix)` is the +requested discovery scope; `ann.Prefix()` is the concrete covered prefix +relative to it. Every call that can block takes a `context.Context` first. Cancelling it returns `ctx.Err()` promptly and tears the in-flight native work down, so a @@ -92,7 +93,8 @@ each reconnect by number; `moq.WithBackoff` tunes the pacing, with `moq.RetryForever` as the timeout; and `moq.WithQUICMaxStreams` raises the peer's inbound stream cap for a subscriber to many tracks. -`moq.Listen` accepts sessions with per-request `Accept`/`Reject`. +`moq.Listen` accepts sessions with per-request `Accept`/`Reject`; `Request.Transport()` +returns the closed `moq.Transport` enum. `Request.SetPublish`/`SetConsume` return an error if the request is already answered, cancelled, or currently accepting; `ErrBusy` is the race with an in-flight Accept. JSON tracks diff --git a/doc/lib/kt/index.md b/doc/lib/kt/index.md index 7db5912ed5..45d18a0580 100644 --- a/doc/lib/kt/index.md +++ b/doc/lib/kt/index.md @@ -26,8 +26,8 @@ import dev.moq.* // Subscribe. The Flow is live, so run it in its own coroutine. Moq.connect("https://relay.example.com", tlsRoots = listOf("ca.pem")).use { moq -> moq.announcements("live/").collect { announcement -> - // An announcement is a route; its path is relative to the prefix. - val broadcast = moq.requestBroadcast("live/" + announcement.path()) + // The requested prefix scopes discovery; each update's prefix is relative to it. + val broadcast = moq.requestBroadcast("live/" + announcement.prefix()) println(broadcast.catalog()) } } @@ -56,8 +56,9 @@ The three advertising operations: `moq.createBroadcast(path)` (or advertisement; `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; `announcement.path()` is -the covered prefix. +serve. A route is a capability, not an inventory. `announcements(prefix)` is +the requested discovery scope; `announcement.prefix()` is the concrete covered +prefix relative to it. Sessions reconnect with backoff when the transport drops and re-announce local broadcasts. `moq.epoch()` counts the connections, 1 on the first, pairing with @@ -68,7 +69,8 @@ inbound stream cap. `Server.listen(bind, tlsGenerate = ...)` accepts sessions with per-request `accept()`/`reject()`. Generated configuration setters, including `MoqRequest.setPublish`/`setConsume`, throw if a connect, listen, or accept is -in flight, or after cancel. JSON tracks take `@Serializable` types +in flight, or after cancel. `MoqRequest.transport()` returns a `Transport` enum. +JSON tracks take `@Serializable` types (`publishJsonSnapshot`, `publishJsonStream`, `valuesAs()`), and the rest of the [shared feature list](/lib/#what-every-binding-can-do) maps one to one: `fetchGroup`/`fetchMediaGroup`, `dynamic()` for tracks and `dynamic(prefix)` for broadcasts, `appendDatagram`/`datagrams()`, diff --git a/doc/lib/py/index.md b/doc/lib/py/index.md index 4fc9bb3ae1..63e7dca729 100644 --- a/doc/lib/py/index.md +++ b/doc/lib/py/index.md @@ -22,9 +22,9 @@ import asyncio, moq async def main(): async with moq.Client("https://cdn.moq.dev/anon") as client: - # Subscribe to media. An announcement is a route; resolve the broadcast at its path. + # The requested prefix scopes discovery; each update's prefix is relative to it. async for announcement in client.announced("live/"): - broadcast = await client.request_broadcast(announcement.path) + broadcast = await client.request_broadcast("live/" + announcement.prefix) catalog = await broadcast.catalog() name, track = next(iter(catalog.audio.items())) async for frame in await broadcast.subscribe_media(name, track): @@ -71,7 +71,8 @@ an unadvertised producer; `broadcast.announce(route)` / `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 -capability, not an inventory; announcement `.path` is the covered prefix. +capability, not an inventory. `announced(prefix)` is the requested discovery +scope; each announcement `.prefix` is the concrete covered prefix relative to it. Sessions reconnect with backoff when the transport drops and re-announce local broadcasts. `session.epoch()` counts the connections, 1 on the first, pairing @@ -88,6 +89,8 @@ subscribed. `request.set_publish`/`set_consume` raise if the request is already answered, cancelled, or currently accepting. `session.bandwidth()` divides the connection's send estimate; pass it to `encode_video` / `encode_audio` or `reserve` a share for an app-owned track. `moq.is_auth(err)` and `moq.is_shutdown(err)` classify errors. `moq.protocol_error(err)` is the structured protocol failure (scope, verbatim code, kind) when the peer sent one. Catch `moq.Error.Busy` when a setter races an in-flight connect, listen, or accept. +Each server request reports a `moq.Transport` enum, including QUIC, Iroh, +WebSocket, TCP, and Unix sockets. - API reference: [moq-rs.readthedocs.io](https://moq-rs.readthedocs.io) - Source and examples: [`py/moq-rs`](https://github.com/moq-dev/moq/tree/main/py/moq-rs) diff --git a/doc/lib/swift/index.md b/doc/lib/swift/index.md index 7c4af319ce..555ef6f67a 100644 --- a/doc/lib/swift/index.md +++ b/doc/lib/swift/index.md @@ -29,8 +29,8 @@ let client = Client() let session = try await client.connect(to: "https://relay.example.com") for try await announcement in try session.consume.announced(prefix: "live/") { - // An announcement is a route; its path is relative to the prefix. - let broadcast = try await session.consume.requestBroadcast(path: "live/" + announcement.path) + // The requested prefix scopes discovery; each update's prefix is relative to it. + let broadcast = try await session.consume.requestBroadcast(path: "live/" + announcement.prefix) for try await catalog in try broadcast.subscribeCatalog() { print(catalog) } @@ -60,8 +60,8 @@ returns an unadvertised producer; `broadcast.announce(route:)` / `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 -route is a capability, not an inventory; `announcement.path` is the covered -prefix. +route is a capability, not an inventory. `announced(prefix:)` is the requested +discovery scope; `announcement.prefix` is the concrete covered prefix relative to it. For a self-signed relay on your own test network, `try client.setTlsVerify(false)` accepts any certificate; prefer `setTlsRoots` or a fingerprint anywhere else. @@ -73,7 +73,7 @@ with `session.status()` to log each reconnect; `client.setBackoff` tunes the pacing; and `client.setQuicMaxStreams` raises the peer's inbound stream cap. `Server` binds, generates or loads TLS, and hands you each request to -`accept()` or `reject(code:)`. JSON tracks take `Codable` types +`accept()` or `reject(code:)`; `request.transport` is a `Transport` enum. JSON tracks take `Codable` types (`publishJsonSnapshot(name:of:)`, `subscribeJsonStream(name:as:)`), and the rest of the [shared feature list](/lib/#what-every-binding-can-do) maps one to one: `fetchGroup`/`fetchMediaGroup`, `dynamic()` for tracks and `dynamic(prefix:)` for broadcasts, `appendDatagram`/ diff --git a/go/wrapper/README.md b/go/wrapper/README.md index 36c2d6d9fb..b2b94ad5a9 100644 --- a/go/wrapper/README.md +++ b/go/wrapper/README.md @@ -47,7 +47,8 @@ for ann, err := range announced.All(ctx) { } log.Fatal(err) } - fmt.Println("got broadcast", ann.Path()) + // The covered prefix is relative to the requested "demos/" prefix. + fmt.Println("got broadcast", ann.Prefix()) } ``` diff --git a/go/wrapper/client.go b/go/wrapper/client.go index cb8c9c61d7..542356a1c8 100644 --- a/go/wrapper/client.go +++ b/go/wrapper/client.go @@ -202,7 +202,7 @@ func Dial(ctx context.Context, url string, opts ...ClientOption) (*Client, error inner := ffi.NewMoqClient() var err error if !cfg.tlsVerify { - err = inner.SetTlsDisableVerify(true) + err = inner.SetTlsVerify(false) } if err == nil && cfg.tlsRootsSet { err = inner.SetTlsRoots(cfg.tlsRoots) diff --git a/go/wrapper/example_test.go b/go/wrapper/example_test.go index 2af209b3f4..90ad626e73 100644 --- a/go/wrapper/example_test.go +++ b/go/wrapper/example_test.go @@ -35,7 +35,7 @@ func ExampleClient_Announced() { if !ann.Active() { continue } - fmt.Println("broadcast:", ann.Path()) + fmt.Println("broadcast:", ann.Prefix()) } } diff --git a/go/wrapper/moq_test.go b/go/wrapper/moq_test.go index 82554ef227..d307e5ba06 100644 --- a/go/wrapper/moq_test.go +++ b/go/wrapper/moq_test.go @@ -320,8 +320,8 @@ func TestLocalPublishConsumeAudio(t *testing.T) { if ann == nil { t.Fatal("expected an announcement") } - if ann.Path() != "live" { - t.Fatalf("path = %q, want %q", ann.Path(), "live") + if ann.Prefix() != "live" { + t.Fatalf("prefix = %q, want %q", ann.Prefix(), "live") } if !ann.Active() { t.Fatal("expected an active announcement") @@ -330,7 +330,7 @@ func TestLocalPublishConsumeAudio(t *testing.T) { t.Fatalf("route hops = %v, want empty for local origin", route.Hops) } - bc, err := consumer.RequestBroadcast(ctx, ann.Path()) + bc, err := consumer.RequestBroadcast(ctx, ann.Prefix()) if err != nil { t.Fatal(err) } @@ -1087,7 +1087,7 @@ func TestAnnounceThenUnannounceIsVisible(t *testing.T) { defer announced.Cancel() ann, err := announced.Next(ctx) - if err != nil || ann == nil || ann.Path() != "live" || !ann.Active() { + if err != nil || ann == nil || ann.Prefix() != "live" || !ann.Active() { t.Fatalf("announce: ann=%+v err=%v", ann, err) } @@ -1095,7 +1095,7 @@ func TestAnnounceThenUnannounceIsVisible(t *testing.T) { t.Fatal(err) } ann, err = announced.Next(ctx) - if err != nil || ann == nil || ann.Path() != "live" || ann.Active() { + if err != nil || ann == nil || ann.Prefix() != "live" || ann.Active() { t.Fatalf("unannounce: ann=%+v err=%v", ann, err) } if _, err := consumer.RequestBroadcast(ctx, "live"); err != nil { @@ -1140,4 +1140,3 @@ func TestDynamicServesARequestUnderAPrefix(t *testing.T) { t.Fatal(err) } } - diff --git a/go/wrapper/origin.go b/go/wrapper/origin.go index f6428dfa69..7cfd064307 100644 --- a/go/wrapper/origin.go +++ b/go/wrapper/origin.go @@ -119,7 +119,7 @@ type OriginConsumer struct { inner *ffi.MoqOriginConsumer } -// Announced streams route announcements whose prefix starts with prefix. +// Announced streams routes under the requested prefix. Each update returns a covered prefix relative to it. func (o *OriginConsumer) Announced(prefix string) (*AnnounceConsumer, error) { inner, err := o.inner.Announced(prefix) if err != nil { @@ -152,20 +152,20 @@ func (o *OriginConsumer) RequestBroadcast(ctx context.Context, path string) (*Br } // AnnounceUpdate is a route announcement or retraction. A route claims that -// Path and every path beneath it can be served; it carries no broadcast. Resolve a specific +// Prefix and every path beneath it can be served; it carries no broadcast. Resolve a specific // path with [OriginConsumer.RequestBroadcast]. By convention a publisher // announces each broadcast's exact path. type AnnounceUpdate struct { inner *ffi.MoqAnnounceUpdate } -// Path is the prefix the route covers, relative to the announced prefix. -func (a *AnnounceUpdate) Path() string { - return a.inner.Path() +// Prefix is the covered prefix, relative to the requested announcements prefix. +func (a *AnnounceUpdate) Prefix() string { + return a.inner.Prefix() } // Active reports whether the route is active (true) or was retracted (false). -// A repeated active announcement for the same path is a metadata update. +// A repeated active announcement for the same prefix is a metadata update. func (a *AnnounceUpdate) Active() bool { return a.inner.Active() } diff --git a/go/wrapper/reconnect_test.go b/go/wrapper/reconnect_test.go index df1e9a6fe8..8f47c77660 100644 --- a/go/wrapper/reconnect_test.go +++ b/go/wrapper/reconnect_test.go @@ -125,7 +125,7 @@ func awaitAnnouncement(t *testing.T, ctx context.Context, announced *moq.Announc if ann == nil { t.Fatalf("announcement stream ended before %q", path) } - if ann.Active() && ann.Path() == path { + if ann.Active() && ann.Prefix() == path { return } } diff --git a/go/wrapper/server.go b/go/wrapper/server.go index f7b83d7cc3..4d93f503ac 100644 --- a/go/wrapper/server.go +++ b/go/wrapper/server.go @@ -8,18 +8,20 @@ import ( ffi "moq.dev/moq-ffi/moq" ) -// Transport is the wire transport an incoming session arrived over. -type Transport string +// Transport is the network transport carrying an incoming session. +type Transport = ffi.MoqTransport -// Known transports reported by Request.Transport. Future native versions may -// report values not listed here, so treat Transport as an open set. const ( // TransportQUIC is a session that arrived over native QUIC. - TransportQUIC Transport = "quic" + TransportQUIC = ffi.MoqTransportQuic // TransportIroh is a session that arrived over an Iroh peer-to-peer connection. - TransportIroh Transport = "iroh" + TransportIroh = ffi.MoqTransportIroh // TransportWebSocket is a session that arrived over the WebSocket fallback transport. - TransportWebSocket Transport = "websocket" + TransportWebSocket = ffi.MoqTransportWebSocket + // TransportTCP is a session that arrived over a plaintext TCP connection. + TransportTCP = ffi.MoqTransportTcp + // TransportUnix is a session that arrived over a Unix domain socket. + TransportUnix = ffi.MoqTransportUnix ) // Request is an incoming session that can be accepted (Accept) or rejected (Reject). @@ -45,7 +47,7 @@ func (r *Request) Query() *string { // Transport is the wire transport the request arrived over, e.g. TransportQUIC. func (r *Request) Transport() Transport { - return Transport(r.inner.Transport()) + return r.inner.Transport() } // SetPublish overrides the publish origin for this session. Pass nil to fall diff --git a/js/publish/src/broadcast.test.ts b/js/publish/src/broadcast.test.ts index cdc5937621..c2f55c27aa 100644 --- a/js/publish/src/broadcast.test.ts +++ b/js/publish/src/broadcast.test.ts @@ -148,3 +148,37 @@ test("serves the catalog through a shared static track", async () => { broadcast.close(); }); + +test("keeps the current catalog snapshot for a reconnecting viewer", async () => { + const real = performance.now.bind(performance); + let now = real(); + performance.now = () => now; + + try { + const broadcast = new Broadcast({ + enabled: true, + origin: new Origin.Producer(), + name: Path.from("test.hang"), + }); + broadcast.video("video").config.set(videoConfig); + await settle(); + + const net = broadcast.net.peek(); + if (!net) throw new Error("expected a network producer once connected"); + + const first = net.track(Broadcast.CATALOG_TRACK).subscribe(); + expect((await new Json.Snapshot.Consumer({ track: first }).next())?.video).toBeDefined(); + first.close(); + + // The default track retention is five seconds. A reconnect after it must still receive + // the catalog's sole snapshot instead of waiting forever for an edit that may never come. + now += 60_000; + const second = net.track(Broadcast.CATALOG_TRACK).subscribe(); + expect((await new Json.Snapshot.Consumer({ track: second }).next())?.video).toBeDefined(); + second.close(); + + broadcast.close(); + } finally { + performance.now = real; + } +}); diff --git a/js/publish/src/broadcast.ts b/js/publish/src/broadcast.ts index 5fecc3a2fa..db69f99fa9 100644 --- a/js/publish/src/broadcast.ts +++ b/js/publish/src/broadcast.ts @@ -232,7 +232,13 @@ export class Broadcast { [Broadcast.CATALOG_TRACK, false], [Broadcast.CATALOG_TRACK_COMPRESSED, true], ] as const) { - const track = broadcast.createTrack(name, { priority: Catalog.PRIORITY.catalog }); + // A catalog may publish once and stay unchanged for the broadcast's whole life. Keep + // that sole closed snapshot replayable so a viewer arriving after the ordinary media + // retention window can still bootstrap. + const track = broadcast.createTrack(name, { + maxAge: Moq.Time.Milli(Number.MAX_SAFE_INTEGER), + priority: Catalog.PRIORITY.catalog, + }); effect.cleanup(() => track.close()); this.catalog.serve(track, effect, { compression }); } diff --git a/js/publish/src/video/encoder.test.ts b/js/publish/src/video/encoder.test.ts index e8f2245e3f..0d6c6b9cc7 100644 --- a/js/publish/src/video/encoder.test.ts +++ b/js/publish/src/video/encoder.test.ts @@ -1,4 +1,5 @@ import { expect, spyOn, test } from "bun:test"; +import * as Container from "@moq/hang/container"; import * as Moq from "@moq/net"; import { Signal } from "@moq/signals"; import { Encoder } from "./encoder"; @@ -89,6 +90,51 @@ test("encoding tracks encoder config in its child effect", async () => { } }); +test("a demand gap leaves the broadcast-owned track open for resume", async () => { + using _videoEncoder = installFakeVideoEncoder(); + const cut = spyOn(Container.Legacy.Producer.prototype, "cut"); + + const track = new Moq.Track.Producer("video").accept({ priority: 60 }); + const live = new Signal(track); + const rendition = { + config: new Signal(undefined), + track: live, + close: () => track.close(), + }; + const capture = { + in: { source: new Signal(undefined) }, + out: { + display: new Signal({ width: 640, height: 480 }), + frames: new Signal(undefined), + }, + }; + const encoder = new Encoder("video", { + enabled: true, + broadcast: { video: () => rendition } as never, + capture: capture as never, + }); + + try { + await settle(); + live.set(undefined); + await settle(); + expect(track.closed.peek()).toBeUndefined(); + + live.set(track); + await settle(); + expect(track.closed.peek()).toBeUndefined(); + + cut.mockClear(); + track.close(); + encoder.close(); + expect(cut).not.toHaveBeenCalled(); + } finally { + encoder.close(); + track.close(); + cut.mockRestore(); + } +}); + // A bandwidth sample used to rerun the whole resolve effect, which blanked the resolved config (and // with it the catalog entry) and re-probed the hardware for a codec. A subscriber returning during // that window got a VideoEncoder that was never configured, so every captured frame was dropped. diff --git a/js/publish/src/video/encoder.ts b/js/publish/src/video/encoder.ts index eb8466f8cd..fe7695f4c1 100644 --- a/js/publish/src/video/encoder.ts +++ b/js/publish/src/video/encoder.ts @@ -235,7 +235,12 @@ export class Encoder { this.#lastCaptureWall = performance.now(); const producer = new Container.Legacy.Producer(track, new Container.Legacy.Format("video")); - effect.cleanup(() => producer.close()); + // The broadcast owns this static track across demand gaps. End only the current + // group when demand disappears so a later subscriber can resume on the same track. + // A fatal encoder error still aborts the track through producer.close(err) below. + effect.cleanup(() => { + if (track.closed.peek() === undefined) producer.cut(); + }); let lastKeyframe: Time.Micro | undefined; let lastEncoded: Time.Micro | undefined; diff --git a/kt/README.md b/kt/README.md index 0692e26300..b770cc5a90 100644 --- a/kt/README.md +++ b/kt/README.md @@ -30,7 +30,8 @@ import kotlinx.coroutines.flow.collect // connect() wires up an internal origin and returns a live connection. Moq.connect("https://relay.example.com").use { moq -> moq.announcements("demos/").collect { announcement -> - println("got broadcast ${announcement.path()}") + // The returned covered prefix is relative to the requested "demos/" prefix. + println("got broadcast ${announcement.prefix()}") val catalog = announcement.broadcast().catalog() println("catalog: $catalog") diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt index ab179d05aa..19194b51dd 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt @@ -15,6 +15,8 @@ typealias Client = uniffi.moq.MoqClient typealias Session = uniffi.moq.MoqSession /** An incoming session awaiting a decision: accept it to handshake, or reject it. */ typealias Request = uniffi.moq.MoqRequest +/** The network transport carrying an incoming session. */ +typealias Transport = uniffi.moq.MoqTransport // Origin (broadcast discovery / announcement). /** The publish side of an origin: create broadcasts so subscribers can discover them. */ diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Moq.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Moq.kt index d9be5feea2..0dd89586d2 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Moq.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Moq.kt @@ -33,13 +33,13 @@ class Moq internal constructor( fun createBroadcast(path: String): BroadcastProducer = session.publish().createBroadcast(path) /** - * Discover routes whose prefix starts with [prefix] as a [Flow]. The - * subscription is acquired on collection and cancelled when collection - * ends. Use [announced] for the raw handle. + * Discover routes under the requested [prefix] as a [Flow]. Each update + * returns a covered prefix relative to it. The subscription is acquired on + * collection and cancelled when collection ends. Use [announced] for the raw handle. */ fun announcements(prefix: String = ""): Flow = session.consume().announcements(prefix) - /** Raw announcement handle under [prefix]. */ + /** Raw handle under requested [prefix]; updates return covered prefixes relative to it. */ fun announced(prefix: String = ""): MoqAnnounceConsumer = session.consume().announced(prefix) /** @@ -125,7 +125,7 @@ class Moq internal constructor( ): Moq { val client = MoqClient() try { - if (!tlsVerify) client.setTlsDisableVerify(true) + if (!tlsVerify) client.setTlsVerify(false) if (tlsRoots != null) client.setTlsRoots(tlsRoots) if (tlsSystemRoots != null) client.setTlsSystemRoots(tlsSystemRoots) if (tlsFingerprints != null) client.setTlsFingerprints(tlsFingerprints) diff --git a/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt b/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt index 42f829e679..f0cfd144f6 100644 --- a/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt +++ b/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt @@ -233,11 +233,11 @@ class SmokeTest { broadcast.announce(Route()) val announced = origin.consume().announced("") val first = announced.next()!! - assertEquals("live", first.path()) + assertEquals("live", first.prefix()) assertTrue(first.active()) broadcast.unannounce() val retracted = announced.next()!! - assertEquals("live", retracted.path()) + assertEquals("live", retracted.prefix()) assertTrue(!retracted.active()) } } diff --git a/py/moq-rs/README.md b/py/moq-rs/README.md index d4638dc196..6cc511a2b7 100644 --- a/py/moq-rs/README.md +++ b/py/moq-rs/README.md @@ -194,7 +194,7 @@ All consumers (`CatalogConsumer`, `MediaConsumer`, `TrackConsumer`, `AudioConsum - `await .requested_broadcast() → BroadcastRequest`. Call `.accept(broadcast)` to serve it, or `.reject(code)` to fail the requester. - Async iterator yielding `BroadcastRequest` - **`OriginConsumer`**. Discover broadcasts. - - `.announced(prefix) → AnnounceConsumer` (async iterator) + - `.announced(prefix) → AnnounceConsumer` (async iterator); `prefix` scopes the request and each update's `.prefix` is relative to it - `.announced_broadcast(path) → AnnouncedBroadcast` (awaitable, waits for a future announcement) - `.request_broadcast(path) → BroadcastConsumer` (awaitable; announced now or a dynamic fallback, else raises) diff --git a/py/moq-rs/examples/announced.py b/py/moq-rs/examples/announced.py index 4e25eebd7d..3080bd57de 100644 --- a/py/moq-rs/examples/announced.py +++ b/py/moq-rs/examples/announced.py @@ -15,7 +15,7 @@ async def run(url: str, prefix: str, tls_verify: bool) -> None: print(f"watching route announcements under {prefix!r} at {url}") async for announcement in client.announced(prefix): sign = "+" if announcement.active else "-" - print(f" {sign} {announcement.path}") + print(f" {sign} {announcement.prefix}") def main() -> None: diff --git a/py/moq-rs/moq/client.py b/py/moq-rs/moq/client.py index de2dd3f685..00deac72e6 100644 --- a/py/moq-rs/moq/client.py +++ b/py/moq-rs/moq/client.py @@ -79,7 +79,7 @@ async def __aenter__(self): self._inner = MoqClient() if not self._tls_verify: - self._inner.set_tls_disable_verify(True) + self._inner.set_tls_verify(False) if self._tls_roots: self._inner.set_tls_roots(self._tls_roots) if self._tls_system_roots is not None: diff --git a/py/moq-rs/moq/origin.py b/py/moq-rs/moq/origin.py index f7e2d8bdcf..b16d57afd6 100644 --- a/py/moq-rs/moq/origin.py +++ b/py/moq-rs/moq/origin.py @@ -23,7 +23,7 @@ class AnnounceUpdate: """A route announcement (or retraction) from :meth:`OriginConsumer.announced`. - A route claims that :attr:`path` and every path beneath it can be served; it + A route claims that :attr:`prefix` and every path beneath it can be served; it carries no broadcast. Resolve a specific path with :meth:`OriginConsumer.request_broadcast`. By convention a publisher announces each broadcast's exact path, so subscribers can enumerate broadcasts from routes. @@ -33,15 +33,15 @@ def __init__(self, inner: MoqAnnounceUpdate) -> None: self._inner = inner @property - def path(self) -> str: - """The prefix the route covers, relative to the ``announced`` prefix.""" - return self._inner.path() + def prefix(self) -> str: + """The covered prefix, relative to the requested announcements prefix.""" + return self._inner.prefix() @property def active(self) -> bool: """Whether the route is active (``True``) or was retracted (``False``). - A repeated active announcement for the same pattern is a metadata update. + A repeated active announcement for the same prefix is a metadata update. """ return self._inner.active() @@ -166,7 +166,7 @@ def __init__(self, inner: MoqOriginConsumer) -> None: self._inner = inner def announced(self, prefix: str = "") -> AnnounceConsumer: - """Async-iterate route announcements under ``prefix`` (empty matches all).""" + """Iterate routes under the requested ``prefix``; updates return relative prefixes.""" return AnnounceConsumer(self._inner.announced(prefix)) def announced_broadcast(self, path: str) -> AnnouncedBroadcast: diff --git a/py/moq-rs/moq/server.py b/py/moq-rs/moq/server.py index 3d9687d6b8..60e7ab3bcf 100644 --- a/py/moq-rs/moq/server.py +++ b/py/moq-rs/moq/server.py @@ -4,16 +4,15 @@ import asyncio from collections.abc import Sequence -from typing import Literal -from moq_ffi import MoqRequest, MoqServer +from moq_ffi import MoqRequest, MoqServer, MoqTransport from .origin import OriginProducer from .publish import BroadcastProducer from .session import Session -# The wire transport carrying a session: raw QUIC, iroh's peer-to-peer QUIC, or WebSocket. -Transport = Literal["quic", "iroh", "websocket"] +# The network transport carrying an incoming session. +Transport = MoqTransport class Request: @@ -46,8 +45,8 @@ def query(self) -> str | None: @property def transport(self) -> Transport: - """The wire transport carrying this session (`"quic"`, `"iroh"`, or `"websocket"`).""" - return self._inner.transport() # type: ignore[return-value] + """The network transport carrying this session.""" + return self._inner.transport() def set_publish(self, origin: OriginProducer | None) -> None: """Override the publish origin for this session. Falls back to the diff --git a/py/moq-rs/tests/test_local.py b/py/moq-rs/tests/test_local.py index 6dbc39daaa..b80aaaeaa8 100644 --- a/py/moq-rs/tests/test_local.py +++ b/py/moq-rs/tests/test_local.py @@ -176,9 +176,9 @@ async def test_local_publish_consume_audio(): consumer = origin.consume() async for announcement in consumer.announced(): - assert announcement.path == "live" + assert announcement.prefix == "live" - broadcast_consumer = await consumer.request_broadcast(announcement.path) + broadcast_consumer = await consumer.request_broadcast(announcement.prefix) catalog = await broadcast_consumer.catalog() assert len(catalog.audio) == 1 @@ -211,7 +211,7 @@ async def test_video_publish_consume(): consumer = origin.consume() async for announcement in consumer.announced(): - broadcast_consumer = await consumer.request_broadcast(announcement.path) + broadcast_consumer = await consumer.request_broadcast(announcement.prefix) catalog = await broadcast_consumer.catalog() assert len(catalog.video) == 1 @@ -245,7 +245,7 @@ async def test_multiple_frames_ordering(): consumer = origin.consume() async for announcement in consumer.announced(): - broadcast_consumer = await consumer.request_broadcast(announcement.path) + broadcast_consumer = await consumer.request_broadcast(announcement.prefix) catalog = await broadcast_consumer.catalog() track_name = list(catalog.audio.keys())[0] audio = catalog.audio[track_name] @@ -272,7 +272,7 @@ async def test_catalog_update_on_new_track(): consumer = origin.consume() async for announcement in consumer.announced(): - broadcast_consumer = await consumer.request_broadcast(announcement.path) + broadcast_consumer = await consumer.request_broadcast(announcement.prefix) cat_consumer = await broadcast_consumer.subscribe_catalog() # First catalog: 1 audio track. @@ -304,8 +304,8 @@ async def test_announced_broadcast(): consumer = origin.consume() async for announcement in consumer.announced(): - assert announcement.path == "test/broadcast" - broadcast_consumer = await consumer.request_broadcast(announcement.path) + assert announcement.prefix == "test/broadcast" + broadcast_consumer = await consumer.request_broadcast(announcement.prefix) _catalog = await broadcast_consumer.subscribe_catalog() break @@ -633,7 +633,7 @@ async def test_subscribe_media_default_latency_and_context_manager(): consumer = origin.consume() async for announcement in consumer.announced(): - broadcast_consumer = await consumer.request_broadcast(announcement.path) + broadcast_consumer = await consumer.request_broadcast(announcement.prefix) catalog = await broadcast_consumer.catalog() track_name, audio = next(iter(catalog.audio.items())) @@ -657,9 +657,9 @@ async def test_raw_publish_consume(): consumer = origin.consume() async for announcement in consumer.announced(): - assert announcement.path == "robot/arm" + assert announcement.prefix == "robot/arm" - broadcast_consumer = await consumer.request_broadcast(announcement.path) + broadcast_consumer = await consumer.request_broadcast(announcement.prefix) raw_consumer = await broadcast_consumer.subscribe_track("events") payload = b'{"cmd": "button_changed", "arm": "left", "button": "THUMB", "state": "PRESSED"}' @@ -682,7 +682,7 @@ async def test_raw_multiple_frames(): consumer = origin.consume() async for announcement in consumer.announced(): - broadcast_consumer = await consumer.request_broadcast(announcement.path) + broadcast_consumer = await consumer.request_broadcast(announcement.prefix) raw_consumer = await broadcast_consumer.subscribe_track("commands", moq.Subscription(max_age_us=1_000_000)) messages = [ @@ -764,7 +764,7 @@ async def test_raw_group_sequence(): consumer = origin.consume() async for announcement in consumer.announced(): - broadcast_consumer = await consumer.request_broadcast(announcement.path) + broadcast_consumer = await consumer.request_broadcast(announcement.prefix) raw_consumer = await broadcast_consumer.subscribe_track("seq", moq.Subscription(max_age_us=1_000_000)) sent_sequences = [] @@ -831,7 +831,7 @@ async def test_raw_multi_frame_group(): consumer = origin.consume() async for announcement in consumer.announced(): - broadcast_consumer = await consumer.request_broadcast(announcement.path) + broadcast_consumer = await consumer.request_broadcast(announcement.prefix) raw_consumer = await broadcast_consumer.subscribe_track("chunks") group_producer = raw.append_group() @@ -1046,12 +1046,12 @@ async def test_announce_then_unannounce_is_visible(): consumer = origin.consume() announced = consumer.announced() first = await asyncio.wait_for(anext(announced), timeout=5.0) - assert first.path == "live" + assert first.prefix == "live" assert first.active broadcast.unannounce() retracted = await asyncio.wait_for(anext(announced), timeout=5.0) - assert retracted.path == "live" + assert retracted.prefix == "live" assert not retracted.active await asyncio.wait_for(consumer.request_broadcast("live"), timeout=5.0) diff --git a/py/moq-rs/tests/test_server.py b/py/moq-rs/tests/test_server.py index eed3df40d9..4b491048cc 100644 --- a/py/moq-rs/tests/test_server.py +++ b/py/moq-rs/tests/test_server.py @@ -46,9 +46,9 @@ async def accept_loop() -> None: bind="127.0.0.1:0", ) as client: async for announcement in client.announced(): - assert announcement.path == "hello" + assert announcement.prefix == "hello" - broadcast_consumer = await client.request_broadcast(announcement.path) + broadcast_consumer = await client.request_broadcast(announcement.prefix) catalog = await broadcast_consumer.catalog() track_name, audio = next(iter(catalog.audio.items())) assert audio.codec == "opus" @@ -124,7 +124,7 @@ async def accept_loop() -> None: broadcast = server.create_broadcast("after-reconnect") broadcast.announce() async for announcement in client.announced(): - assert announcement.path == "after-reconnect" + assert announcement.prefix == "after-reconnect" break broadcast.finish() finally: @@ -146,7 +146,7 @@ async def reject_loop() -> None: reject_task = asyncio.create_task(reject_loop()) try: client = moq_ffi.MoqClient() - client.set_tls_disable_verify(True) + client.set_tls_verify(False) client.set_bind("127.0.0.1:0") # One-shot, so this dial's outcome is what surfaces here rather than # whatever the reconnect loop eventually reports. @@ -173,10 +173,10 @@ async def reject_loop() -> None: async def test_client_setters_fail_after_cancel(): """A cancelled client refuses further configuration rather than ignoring it.""" client = moq_ffi.MoqClient() - client.set_tls_disable_verify(True) + client.set_tls_verify(False) client.cancel() with pytest.raises(moq_ffi.MoqError.Cancelled): # type: ignore[misc] - client.set_tls_disable_verify(False) + client.set_tls_verify(True) with pytest.raises(moq_ffi.MoqError.Cancelled): # type: ignore[misc] client.set_bind("127.0.0.1:0") @@ -237,7 +237,7 @@ async def test_serve_helper_accepts_clients(): bind="127.0.0.1:0", ) as client: async for announcement in client.announced(): - assert announcement.path == "via-serve" + assert announcement.prefix == "via-serve" break finally: serve_task.cancel() @@ -262,7 +262,7 @@ async def test_broadcast_route_over_wire(): bind="127.0.0.1:0", ) as client: async for announcement in client.announced(): - assert announcement.path == "with-route" + assert announcement.prefix == "with-route" assert announcement.active route = announcement.route assert all(isinstance(h, int) for h in route.hops) @@ -297,7 +297,7 @@ async def test_route_update_observes_restart(): ) as client: announced = client.announced() first = await asyncio.wait_for(announced.__anext__(), timeout=5.0) - assert first.path == "routed" + assert first.prefix == "routed" assert first.active assert 42 in first.route.hops assert 77 not in first.route.hops @@ -305,14 +305,14 @@ async def test_route_update_observes_restart(): # The publisher advertises a longer chain: an in-place update. announce.update(moq.Route(hops=[42, 77])) updated = await asyncio.wait_for(announced.__anext__(), timeout=5.0) - assert updated.path == "routed" + assert updated.prefix == "routed" assert updated.active assert 77 in updated.route.hops # Cancelling retracts the route. announce.cancel() ended = await asyncio.wait_for(announced.__anext__(), timeout=5.0) - assert ended.path == "routed" + assert ended.prefix == "routed" assert not ended.active finally: serve_task.cancel() diff --git a/quest/m1/README.md b/quest/m1/README.md index 7017717037..9e0c8fa7b5 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -33,7 +33,6 @@ the transport line in m2 assumes a single stack. - [PathPrefixes](/quest/m1/api-path-prefixes.md) - the unused moq_net::PathPrefixes type is deleted before the release - [Rendition ownership](/quest/m1/api-mux-rendition.md) - one handle publishes a media track and reports its estimate, instead of five - [Gateway types](/quest/m1/api-gateways.md) - no `anyhow` in a gateway `Error`, `PathOwned` prefixes, `Duration` segments, `moq_rtc::Server::new(config)`, an SRT reject with a reason -- [libmoq units](/quest/m1/api-libmoq-units.md) - `moq_client_config` is all microseconds, the header declares every enum and error code, NULL callbacks are refused - [Cluster -01](/quest/m1/cluster-01/README.md) - rs/moq-net and js/net speak the revised cluster extension (HOP_ID, REQUEST_UPDATE repricing) and -01 is published - [API review gate](/quest/m1/api-review-gate.md) - each `api-*` quest above is landed or deferred by the maintainer before the merge PR opens - [Merge dev](/quest/m1/merge-dev.md) - dev lands on main with a closing keyword for every issue it fixed diff --git a/quest/m1/api-libmoq-units.md b/quest/m1/api-libmoq-units.md deleted file mode 100644 index ae00010a78..0000000000 --- a/quest/m1/api-libmoq-units.md +++ /dev/null @@ -1,54 +0,0 @@ -# [S] libmoq's config speaks one unit and its header declares every enum - -## Goal - -A C caller of `moq.h` reads every duration in microseconds, finds every -enum the structs use, and gets a terminal callback for every task it -registers. The 0.5 header is already rewritten on dev (41 client setters -became `moq_client_config`), so this is the release to finish it. - -## Plan - -- `moq_client_config` mixes `connect_timeout_ms`, `failover_delay_ms`, - `resolution_delay_ms`, `websocket_delay_ms`, `quic_idle_timeout_ms`, and - `quic_keep_alive_ms` with `backoff_*_us` in one struct; every field is - `_us`, matching the microseconds rule #3744 set for the bindings. OBS - (`cpp/obs/src/moq-settings.cpp`) converts at its dials. -- `rs/libmoq/build.rs` lists `moq_audio_format` among the enums cbindgen - must emit, but the PCM layout enum was renamed `moq_audio_sample_format` - and `moq_audio_format` is now the codec enum; the sample format reaches - no signature (its fields are `u32`), so the generated header omits it and - a C caller hardcodes 0 to 7. Add it to the list and delete the stale - "named `_max`" comment on `moq_audio_decoder_output.max_age_us`. -- Every registrar refuses a NULL callback with `InvalidPointer`, as - `moq_origin_dynamic` already does; the other twelve accept `None`, never - fire the terminal, and leak the `user_data`. -- `moq_origin_dynamic_close` returns a negative code on a second call as - its doc says; the implementation is the one `_close` without `ok_or`. -- `#define MOQ_ERROR_*` for the 38 codes in `error.rs` (the header has none) - and a comment reserving the retired -1, -11, -12, -39. -- One `typedef` for the status callback in place of fourteen inline - signatures. -- Every task's `_close` becomes `_cancel`, the verb the uniffi core and - every wrapper use, and `moq_origin_consume_announced` becomes - `moq_origin_announced_broadcast` to match `announced_broadcast`; fourteen - functions plus `cpp/obs`. One break now instead of a second later. -- In moq-ffi, on the same release: `set_tls_disable_verify(bool)` becomes - `set_tls_verify(bool)`, the polarity every wrapper already inverts by - hand, and `MoqRequest::transport` is an enum instead of a `String` that - Go turns into an open-set type and Python into a `Literal`. Both ripple - through the five wrappers and `doc/lib/*` per the Cross-Package Sync - checklist. -- `CHANGELOG.md` `[Unreleased]` names the setter deletion, the bandwidth - functions, `create_broadcast`/`announce`/`dynamic`, the decoder output - format, `moq_route`, the `estimated_*_rate` stats fields, and - `frame_duration_us`; the `[0.5.14]` section holds unreleased work under - a released heading. `doc/lib/c/index.md` still advertises - `moq_publish_video_raw` and the `_consume_*_raw` mirrors. - -Public API: breaking on libmoq's C ABI, so on dev. Wire: none. Consumers: -`cpp/obs`, `doc/bin/obs.md`, `doc/lib/c`. - -## Related - -- [Binding parity](/quest/m2/binding-parity.md) - the uniffi wrappers' half diff --git a/quest/m1/api-review-gate.md b/quest/m1/api-review-gate.md index 6d440b4796..8c7e5b754d 100644 --- a/quest/m1/api-review-gate.md +++ b/quest/m1/api-review-gate.md @@ -19,8 +19,7 @@ quest is deleted too. No code. The list: [Announce event](/quest/m1/api-net-announce.md), [Origin scoping](/quest/m1/api-net-origin.md), [Rendition ownership](/quest/m1/api-mux-rendition.md), -[Gateway types](/quest/m1/api-gateways.md), -[libmoq units](/quest/m1/api-libmoq-units.md). +[Gateway types](/quest/m1/api-gateways.md). ## Related diff --git a/quest/m2/binding-parity.md b/quest/m2/binding-parity.md index bb55d6cfa7..f5acd89681 100644 --- a/quest/m2/binding-parity.md +++ b/quest/m2/binding-parity.md @@ -27,8 +27,8 @@ Gaps found method by method against `rs/moq-ffi/src`: - Verbs: `announced` is `announcements` in Kotlin and Dart; `next` is `All`/`Requests`/`Updates`/`Frames`/`Values` in Go; `set_consume` is `subscribe=` in four. The wrappers move to the core's spelling; the two - core changes (`set_tls_verify`, the transport enum) land on dev in - [libmoq units](/quest/m1/api-libmoq-units.md). + core changes (`set_tls_verify`, the transport enum) land on main in the + libmoq release cleanup. - `rtt_us` and the other microsecond fields are raw integers in every wrapper; a `Duration`/`timedelta` at the boundary where the language has one. diff --git a/quest/m2/ffi-websocket-fallback.md b/quest/m2/ffi-websocket-fallback.md index 9ec9364b43..d5d81636a7 100644 --- a/quest/m2/ffi-websocket-fallback.md +++ b/quest/m2/ffi-websocket-fallback.md @@ -7,35 +7,35 @@ disable the WebSocket fallback or change the head start QUIC gets before it, the way the CLI (`--connect-websocket-enabled`, `--connect-websocket-delay`) and libmoq's `moq_client_config` already can. Today a wrapper user on a WebTransport-only relay has no way to stop the fallback from racing at all. -Additive, on main after the dev merge lands `rs/moq-tokio` there. +Additive, on main. ## Plan `MoqClient` (`rs/moq-ffi/src/session.rs:310`) holds a `moq_tokio::connect::Config` (line 381) and exposes it through setters that -each lock the task state and write one field: `set_tls_disable_verify` at -line 389 writes `config.tls.insecure`, `set_reconnect` at 469 writes -`config.once`, `set_backoff` at 476 unpacks a `MoqBackoff` record of `_ms` -fields into `config.backoff`. The fallback knobs live at +each lock the task state and write one field: `set_tls_verify` at +line 389 writes the inverse to `config.tls.insecure`, `set_reconnect` at +469 writes `config.once`, `set_backoff` at 476 unpacks a `MoqBackoff` record +of `_us` fields into `config.backoff`. The fallback knobs live at `config.websocket`: `websocket::Config` (`rs/moq-tokio/src/websocket.rs:114`) with `enabled: Option` (131, `None` means on) and `delay: CliDuration` (141, default 200 ms). libmoq already maps both: `moq_client_config` (`rs/libmoq/src/api.rs:843`) carries `websocket_enabled` and -`websocket_delay_ms` with `has_` flags (874 to 879), applied in +`websocket_delay_us` with `has_` flags (874 to 879), applied in `rs/libmoq/src/client.rs:64` to `:69`, and defaults exported at `api.rs:977`. Shape, two options: - Two setters beside the tls ones, `set_websocket_enabled(bool)` and - `set_websocket_delay(delay_ms: u64)`, mirroring the libmoq fields and the - `_ms` convention `MoqBackoff` uses. Recommended: smallest surface, and + `set_websocket_delay(delay_us: u64)`, mirroring the libmoq fields and the + `_us` convention `MoqBackoff` uses. Recommended: smallest surface, and every wrapper already has a one-line pattern for a boolean setter. -- One `set_websocket(MoqWebsocket { enabled, delay_ms })` record like +- One `set_websocket(MoqWebsocket { enabled, delay_us })` record like `set_backoff`. Only worth it if a third knob appears. Cross-package sync from the root CLAUDE.md table, with the lines that mirror -`set_tls_disable_verify` today: +`set_tls_verify` today: - `rs/libmoq`: already has both fields; nothing to add. - `py/moq-rs/moq/client.py:80` (constructor kwargs, applied in `__aenter__`). diff --git a/rs/libmoq/CHANGELOG.md b/rs/libmoq/CHANGELOG.md index a4e1b36b4a..8b1e1e2e01 100644 --- a/rs/libmoq/CHANGELOG.md +++ b/rs/libmoq/CHANGELOG.md @@ -9,16 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `moq_origin_create_broadcast`, `moq_publish_announce` / `_unannounce`, and + `moq_origin_dynamic` split creation from exact-path and prefix advertisements. +- `moq_session_bandwidth`, `moq_bandwidth_reserve`, `moq_reservation_grant` / `_update` / + `_close` expose the connection's send estimate to app-owned encoders. +- `moq_session_snapshot` samples connection statistics and the negotiated protocol together; + `moq_connection_stats` includes `estimated_send_rate` and `estimated_receive_rate`. +- `moq_video_decoder_output` selects the decoded pixel format and dimensions. +- `moq_route` carries the advertised warm and cold path costs. +- `moq_audio_encoder_output.frame_duration_us` configures the Opus packet duration. +- Publish and consume human-readable audio and video rendition labels. +- The generated header defines every `MOQ_ERROR_*` return code, every enum used by a struct, + and one `moq_status_callback` type for asynchronous registrars. - Demand watchers: `moq_publish_track_demand`, `moq_publish_media_demand`, `moq_encode_video_demand`, and `moq_encode_audio_demand` report `MOQ_DEMAND_USED` / `MOQ_DEMAND_UNUSED` (a `moq_demand`) immediately and on every change, closed by - `moq_publish_demand_close`. + `moq_publish_demand_cancel`. - Track requests: `moq_publish_dynamic` serves subscriptions to undeclared tracks as `moq_track_request_*` handles (`name`, `accept`, `video`, `audio`, `abort`, `free`). Group requests: `moq_publish_track_dynamic` and `moq_track_request_dynamic` serve fetches of uncached groups as `moq_group_request_*` handles (`sequence`, `priority`, `frame_start`, `accept`, `abort`, `free`). `accept` positions the producer at `frame_start`. Both - handlers close with `moq_publish_dynamic_close`. + handlers stop with `moq_publish_dynamic_cancel`. - `moq_error_protocol` fills a `moq_protocol_error` (scope, verbatim wire code, kind) for the last protocol failure on this thread. Do not parse `moq_error()` for that. Local `Unauthorized` still returns status -34; a session-scoped unauthorized protocol close is @@ -28,12 +40,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bare-integer durations are microseconds: `max_age_us` on decoder outputs, track info, subscriptions, and `moq_consume_video` / `moq_consume_audio`; - reconnect backoff is `backoff_initial_us` / `backoff_max_us` / - `backoff_timeout_us`. -- `moq_announced` is `moq_announce_update` with `pattern` / `pattern_len` instead of `path` / `path_len`. `moq_broadcast_request_abort` is `moq_broadcast_request_reject`; `_free` is unchanged. + every duration in `moq_client_config` is `_us`. +- The 41 `moq_client_*` setters are replaced by one zero-initializable `moq_client_config`. +- Asynchronous task shutdown is consistently `_cancel`; `moq_origin_consume_announced` is + `moq_origin_announced_broadcast`. Registrars reject a NULL callback before retaining + `user_data`. - `moq_publish_media` splits into `moq_publish_audio`, `moq_publish_video`, and `moq_publish_container`, taking `moq_audio_init`, `moq_video_init`, and `moq_container_init`. Each carries only the fields its kind can honor, so a label on a container no longer compiles. +- `moq_announced` is `moq_announce_update` with `prefix` / `prefix_len` instead + of `path` / `path_len`. The prefix is relative to the requested announcements + scope. `moq_broadcast_request_abort` is `moq_broadcast_request_reject`; `_free` + is unchanged. - Formats are enums (`moq_audio_format`, `moq_video_format`, `moq_container_format`) rather than strings. As with `moq_audio_sample_format`, the struct field is a `u32` and an out-of-range code is rejected, since matching an invalid discriminant as a Rust enum would be undefined behavior. @@ -82,20 +100,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - take every feature that needs a library or libclang at build time off the defaults ([#3464](https://github.com/moq-dev/moq/pull/3464)) -### Added - -- Publish and consume human-readable audio and video rendition labels. -- `moq_session_snapshot` for statistics and negotiated protocol from the same live connection - -### Changed - -- `moq_publish_media` now takes an extensible `moq_media_config` instead of positional arguments. -- `moq_video_config` and `moq_audio_config` gained trailing `label` / `label_len` fields. Both structs - are caller-allocated and the consume calls write into them, so this is a recompile, not a - drop-in replacement for an existing header. -- `moq_publish_media` rejects a label on a container format (fmp4, mkv, ts, flv) instead of - silently dropping it. - ## [0.5.13](https://github.com/moq-dev/moq/compare/libmoq-v0.5.12...libmoq-v0.5.13) - 2026-09-02 ### Added diff --git a/rs/libmoq/README.md b/rs/libmoq/README.md index 1cc0cf34b6..f7f989f4b2 100644 --- a/rs/libmoq/README.md +++ b/rs/libmoq/README.md @@ -25,7 +25,7 @@ The library exposes the following C functions, see [api.rs](src/api.rs) for full int32_t moq_log_level(const char *level, uintptr_t level_len); // Session -int32_t moq_session_connect(const char *url, uintptr_t url_len, const moq_client_config *config, uint32_t origin_publish, uint32_t origin_consume, void (*on_status)(void *user_data, int32_t code), void *user_data); +int32_t moq_session_connect(const char *url, uintptr_t url_len, const moq_client_config *config, uint32_t origin_publish, uint32_t origin_consume, moq_status_callback on_status, void *user_data); moq_client_config moq_client_defaults(void); int32_t moq_session_close(uint32_t session); int32_t moq_session_bandwidth(uint32_t session); @@ -39,14 +39,15 @@ int32_t moq_reservation_close(uint32_t reservation); int32_t moq_origin_create(void); int32_t moq_origin_close(uint32_t origin); int32_t moq_origin_create_broadcast(uint32_t origin, const char *path, uintptr_t path_len); -int32_t moq_origin_request(uint32_t origin, const char *path, uintptr_t path_len, void (*on_broadcast)(void *user_data, int32_t broadcast), void *user_data); -int32_t moq_origin_request_close(uint32_t task); -int32_t moq_origin_consume_announced(uint32_t origin, const char *path, uintptr_t path_len, void (*on_broadcast)(void *user_data, int32_t broadcast), void *user_data); -int32_t moq_origin_consume_announced_close(uint32_t task); -int32_t moq_origin_announced(uint32_t origin, void (*on_announce)(void *user_data, int32_t announced), void *user_data); +int32_t moq_origin_request(uint32_t origin, const char *path, uintptr_t path_len, moq_status_callback on_broadcast, void *user_data); +int32_t moq_origin_request_cancel(uint32_t task); +int32_t moq_origin_announced_broadcast(uint32_t origin, const char *path, uintptr_t path_len, moq_status_callback on_broadcast, void *user_data); +int32_t moq_origin_announced_broadcast_cancel(uint32_t task); +int32_t moq_origin_announced(uint32_t origin, moq_status_callback on_announce, void *user_data); int32_t moq_origin_announced_info(uint32_t announced, moq_announce_update *dst); int32_t moq_origin_announced_free(uint32_t announced); -int32_t moq_origin_announced_close(uint32_t announced); +int32_t moq_origin_announced_cancel(uint32_t announced); +// The request is rooted at the origin; each moq_announce_update.prefix is relative to that root. // Publishing int32_t moq_publish_announce(uint32_t broadcast, const moq_route *route); @@ -59,7 +60,7 @@ int32_t moq_publish_container_write(uint32_t container, const uint8_t *payload, int32_t moq_publish_container_finish(uint32_t container); int32_t moq_publish_media_finish(uint32_t media); int32_t moq_publish_media_frame(uint32_t media, const uint8_t *payload, uintptr_t payload_size, uint64_t timestamp_us); -int32_t moq_publish_track(uint32_t broadcast, const char *name, uintptr_t name_len); +int32_t moq_publish_track(uint32_t broadcast, const char *name, uintptr_t name_len, const moq_track_info *info); int32_t moq_publish_track_group(uint32_t track); int32_t moq_publish_track_frame(uint32_t track, const uint8_t *payload, uintptr_t payload_size, uint64_t timestamp_us); int32_t moq_publish_group_frame(uint32_t group, const uint8_t *payload, uintptr_t payload_size, uint64_t timestamp_us); @@ -67,17 +68,17 @@ int32_t moq_publish_group_finish(uint32_t group); int32_t moq_publish_track_finish(uint32_t track); // Publishing: Demand -int32_t moq_publish_track_demand(uint32_t track, void (*on_demand)(void *user_data, int32_t status), void *user_data); -int32_t moq_publish_media_demand(uint32_t media, void (*on_demand)(void *user_data, int32_t status), void *user_data); -int32_t moq_encode_video_demand(uint32_t producer, void (*on_demand)(void *user_data, int32_t status), void *user_data); -int32_t moq_encode_audio_demand(uint32_t producer, void (*on_demand)(void *user_data, int32_t status), void *user_data); -int32_t moq_publish_demand_close(uint32_t watcher); +int32_t moq_publish_track_demand(uint32_t track, moq_status_callback on_demand, void *user_data); +int32_t moq_publish_media_demand(uint32_t media, moq_status_callback on_demand, void *user_data); +int32_t moq_encode_video_demand(uint32_t producer, moq_status_callback on_demand, void *user_data); +int32_t moq_encode_audio_demand(uint32_t producer, moq_status_callback on_demand, void *user_data); +int32_t moq_publish_demand_cancel(uint32_t watcher); // Publishing: Requests -int32_t moq_publish_dynamic(uint32_t broadcast, void (*on_request)(void *user_data, int32_t request), void *user_data); -int32_t moq_publish_track_dynamic(uint32_t track, void (*on_group)(void *user_data, int32_t request), void *user_data); -int32_t moq_track_request_dynamic(uint32_t request, void (*on_group)(void *user_data, int32_t request), void *user_data); -int32_t moq_publish_dynamic_close(uint32_t dynamic); +int32_t moq_publish_dynamic(uint32_t broadcast, moq_status_callback on_request, void *user_data); +int32_t moq_publish_track_dynamic(uint32_t track, moq_status_callback on_group, void *user_data); +int32_t moq_track_request_dynamic(uint32_t request, moq_status_callback on_group, void *user_data); +int32_t moq_publish_dynamic_cancel(uint32_t dynamic); int32_t moq_track_request_name(uint32_t request, moq_string *dst); int32_t moq_track_request_accept(uint32_t request, const moq_track_info *info); int32_t moq_track_request_video(uint32_t request, const moq_video_init *config); @@ -95,28 +96,28 @@ int32_t moq_group_request_free(uint32_t request); int32_t moq_consume_close(uint32_t consume); // Consuming: Catalog -int32_t moq_consume_catalog(uint32_t broadcast, void (*on_catalog)(void *user_data, int32_t catalog), void *user_data); -int32_t moq_consume_catalog_close(uint32_t catalog); +int32_t moq_consume_catalog(uint32_t broadcast, moq_status_callback on_catalog, void *user_data); +int32_t moq_consume_catalog_cancel(uint32_t catalog); int32_t moq_consume_catalog_free(uint32_t catalog); int32_t moq_consume_video_config(uint32_t catalog, uint32_t index, moq_video_config *dst); int32_t moq_consume_video_stalled(uint32_t catalog, uint32_t index, bool *dst); int32_t moq_consume_audio_config(uint32_t catalog, uint32_t index, moq_audio_config *dst); // Consuming: Video -int32_t moq_consume_video(uint32_t catalog, uint32_t index, uint64_t max_age_us, void (*on_frame)(void *user_data, int32_t frame), void *user_data); -int32_t moq_consume_video_close(uint32_t track); +int32_t moq_consume_video(uint32_t catalog, uint32_t index, uint64_t max_age_us, moq_status_callback on_frame, void *user_data); +int32_t moq_consume_video_cancel(uint32_t track); // Consuming: Audio -int32_t moq_consume_audio(uint32_t catalog, uint32_t index, uint64_t max_age_us, void (*on_frame)(void *user_data, int32_t frame), void *user_data); -int32_t moq_consume_audio_close(uint32_t track); +int32_t moq_consume_audio(uint32_t catalog, uint32_t index, uint64_t max_age_us, moq_status_callback on_frame, void *user_data); +int32_t moq_consume_audio_cancel(uint32_t track); // Consuming: Frames int32_t moq_consume_frame(uint32_t frame, moq_frame *dst); int32_t moq_consume_frame_free(uint32_t frame); -int32_t moq_consume_track(uint32_t broadcast, const char *name, uintptr_t name_len, void (*on_frame)(void *user_data, int32_t frame), void *user_data); +int32_t moq_consume_track(uint32_t broadcast, const char *name, uintptr_t name_len, const moq_subscription *subscription, moq_status_callback on_frame, void *user_data); int32_t moq_consume_track_frame(uint32_t frame, moq_frame *dst); int32_t moq_consume_track_frame_free(uint32_t frame); -int32_t moq_consume_track_close(uint32_t track); +int32_t moq_consume_track_cancel(uint32_t track); ``` Raw track frames use the same `moq_frame` record as media frames. Use diff --git a/rs/libmoq/build.rs b/rs/libmoq/build.rs index edd2cf63f6..639c3521b4 100644 --- a/rs/libmoq/build.rs +++ b/rs/libmoq/build.rs @@ -8,6 +8,7 @@ const LIB_NAME: &str = "moq"; const ENUMS: &[&str] = &[ "moq_container_kind", "moq_audio_format", + "moq_audio_sample_format", "moq_video_format", "moq_container_format", "moq_video_pixel_format", @@ -35,12 +36,13 @@ fn main() { fs::create_dir_all(&include_dir).expect("Failed to create include directory"); let header = include_dir.join(format!("{}.h", LIB_NAME)); let config = cbindgen::Config { + header: Some("/* Error codes -1, -11, -12, and -39 are retired and reserved. */".into()), // cbindgen.toml is never loaded (see its header comment), so the generated // header has no include guard unless we ask for one here. Without it a // project reaching moq.h down two include paths gets redefinition errors. pragma_once: true, export: cbindgen::ExportConfig { - // The codec enums cross the ABI as plain `uint32_t`, so that an unknown + // These enums cross the ABI as plain `uint32_t`, so that an unknown // discriminant from C is an error rather than UB. That leaves no signature // referencing them, and cbindgen emits only what a signature reaches, so // name them here: without this a C caller has to hardcode the integers. diff --git a/rs/libmoq/c-tests/decode-output.c b/rs/libmoq/c-tests/decode-output.c index 9f0632862e..585f107bf2 100644 --- a/rs/libmoq/c-tests/decode-output.c +++ b/rs/libmoq/c-tests/decode-output.c @@ -53,6 +53,11 @@ static _Noreturn void fail(const char *fmt, ...) { _exit(1); } +static void ignore_status(void *user_data, int32_t status) { + (void)user_data; + (void)status; +} + static void check_refusals(void) { // Unknown pixel format. moq_video_decoder_output bad_format = {0, 999, 0, 0}; @@ -74,7 +79,7 @@ static void check_refusals(void) { // A valid request gets past validation and fails on the bogus catalog. moq_video_decoder_output valid = {0, MOQ_VIDEO_PIXEL_FORMAT_RGBA, 160, 120}; - if (moq_decode_video(INT32_MAX, 0, &valid, NULL, NULL) != MOQ_ERR_CATALOG_NOT_FOUND) + if (moq_decode_video(INT32_MAX, 0, &valid, ignore_status, NULL) != MOQ_ERR_CATALOG_NOT_FOUND) fail("error: valid request did not reach catalog lookup: %s\n", moq_error()); fprintf(stderr, "decoder output refusals ok\n"); @@ -202,8 +207,8 @@ static void decode_once(ctx_t *c, int32_t catalog, const moq_video_decoder_outpu wait_for(&c->mu, &c->cv, &c->got_frame, 15.0); pthread_mutex_unlock(&c->mu); - if (moq_decode_video_close((uint32_t)consumer) < 0) - fail("error: moq_decode_video_close failed (%s)\n", moq_error()); + if (moq_decode_video_cancel((uint32_t)consumer) < 0) + fail("error: moq_decode_video_cancel failed (%s)\n", moq_error()); pthread_mutex_lock(&c->mu); wait_for(&c->mu, &c->cv, &c->done_frame, 10.0); pthread_mutex_unlock(&c->mu); @@ -266,15 +271,15 @@ int main(void) { if (moq_consume_catalog_free((uint32_t)catalog) < 0) fail("error: moq_consume_catalog_free failed (%s)\n", moq_error()); c.catalog_snapshot = 0; - if (moq_consume_catalog_close((uint32_t)catalog_sub) < 0) - fail("error: moq_consume_catalog_close failed (%s)\n", moq_error()); + if (moq_consume_catalog_cancel((uint32_t)catalog_sub) < 0) + fail("error: moq_consume_catalog_cancel failed (%s)\n", moq_error()); // The request already terminated once it delivered the broadcast, so its // task is gone; close only a still-pending wait. pthread_mutex_lock(&c.mu); int request_pending = !c.done_broadcast; pthread_mutex_unlock(&c.mu); - if (request_pending && moq_origin_request_close((uint32_t)request) < 0) - fail("error: moq_origin_request_close failed (%s)\n", moq_error()); + if (request_pending && moq_origin_request_cancel((uint32_t)request) < 0) + fail("error: moq_origin_request_cancel failed (%s)\n", moq_error()); if (moq_consume_close((uint32_t)consume) < 0) fail("error: moq_consume_close failed (%s)\n", moq_error()); if (moq_encode_video_finish((uint32_t)producer) < 0) diff --git a/rs/libmoq/src/api.rs b/rs/libmoq/src/api.rs index 96d9ac329e..fb07918723 100644 --- a/rs/libmoq/src/api.rs +++ b/rs/libmoq/src/api.rs @@ -682,9 +682,9 @@ unsafe fn parse_route(route: *const moq_route) -> Result bool { moq_tokio::qlog_supported() } -/// A duration as the milliseconds the setters take, saturating rather than wrapping. -fn millis(duration: std::time::Duration) -> u64 { - duration.as_millis().min(u64::MAX as u128) as u64 -} - -/// A duration as the microseconds reconnect backoff fields take, saturating rather than wrapping. +/// A duration as microseconds, saturating rather than wrapping. fn micros(duration: std::time::Duration) -> u64 { duration.as_micros().min(u64::MAX as u128) as u64 } @@ -980,15 +975,15 @@ pub struct moq_client_config { pub bind_len: usize, /// How long a dial may take before it gives up. - pub connect_timeout_ms: u64, + pub connect_timeout_us: u64, pub has_connect_timeout: bool, /// Happy Eyeballs: how long before the next address is also dialed. - pub failover_delay_ms: u64, + pub failover_delay_us: u64, pub has_failover_delay: bool, /// Happy Eyeballs: how long the first family waits for the AAAA answer. - pub resolution_delay_ms: u64, + pub resolution_delay_us: u64, pub has_resolution_delay: bool, /// Whether the WebSocket fallback may be raced, for a UDP-blocked network. @@ -997,7 +992,7 @@ pub struct moq_client_config { pub has_websocket_enabled: bool, /// How long QUIC gets before the WebSocket fallback is also dialed. - pub websocket_delay_ms: u64, + pub websocket_delay_us: u64, pub has_websocket_delay: bool, /// Accept any certificate. Development only: prefer `tls_fingerprints`, @@ -1043,9 +1038,9 @@ pub struct moq_client_config { /// QUIC transport tuning, all ignored by the WebSocket fallback. pub quic_max_streams: u64, pub has_quic_max_streams: bool, - pub quic_idle_timeout_ms: u64, + pub quic_idle_timeout_us: u64, pub has_quic_idle_timeout: bool, - pub quic_keep_alive_ms: u64, + pub quic_keep_alive_us: u64, pub has_quic_keep_alive: bool, /// Generic segmentation offload and path MTU discovery. Both default to the /// backend's choice, so both need their flag. @@ -1090,17 +1085,17 @@ pub extern "C" fn moq_client_defaults() -> moq_client_config { let config = crate::client::Config::default(); let connect = config.connect.resolve(); - dst.connect_timeout_ms = millis(connect.timeout); + dst.connect_timeout_us = micros(connect.timeout); dst.has_connect_timeout = true; - dst.failover_delay_ms = millis(connect.race); + dst.failover_delay_us = micros(connect.race); dst.has_failover_delay = true; - dst.resolution_delay_ms = millis(connect.resolution_delay); + dst.resolution_delay_us = micros(connect.resolution_delay); dst.has_resolution_delay = true; let websocket = config.connect.websocket.resolve(); dst.websocket_enabled = websocket.enabled; dst.has_websocket_enabled = true; - dst.websocket_delay_ms = millis(websocket.delay); + dst.websocket_delay_us = micros(websocket.delay); dst.has_websocket_delay = true; dst.backoff_initial_us = micros(config.connect.backoff.initial); @@ -1115,10 +1110,10 @@ pub extern "C" fn moq_client_defaults() -> moq_client_config { let quic = config.quic.resolve(); dst.quic_max_streams = quic.max_streams; dst.has_quic_max_streams = true; - dst.quic_idle_timeout_ms = millis(quic.idle_timeout); + dst.quic_idle_timeout_us = micros(quic.idle_timeout); dst.has_quic_idle_timeout = true; if let Some(keep_alive) = quic.keep_alive { - dst.quic_keep_alive_ms = millis(keep_alive); + dst.quic_keep_alive_us = micros(keep_alive); dst.has_quic_keep_alive = true; } @@ -1140,7 +1135,7 @@ unsafe fn connect_session( config: *const moq_client_config, origin_publish: u32, origin_consume: u32, - on_status: Option, + on_status: ffi::moq_status_callback, user_data: *mut c_void, ) -> Result { let url = ffi::parse_url(url, url_len)?; @@ -1158,7 +1153,7 @@ unsafe fn connect_session( (publish, consume) }; - let callback = unsafe { ffi::OnStatus::new(user_data, on_status) }; + let callback = unsafe { ffi::OnStatus::new(user_data, on_status)? }; let request = Connect { config, url, @@ -1220,7 +1215,7 @@ pub unsafe extern "C" fn moq_session_connect( config: *const moq_client_config, origin_publish: u32, origin_consume: u32, - on_status: Option, + on_status: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || unsafe { @@ -1346,7 +1341,7 @@ pub unsafe extern "C" fn moq_origin_create_broadcast(origin: u32, path: *const c /// required: a NULL callback is refused before the route is advertised. It is /// invoked with a positive request handle for each /// pending broadcast, then exactly once more with a terminal code: `0` (stopped -/// cleanly, including after [moq_origin_dynamic_close]) or a negative error. +/// cleanly, including after [moq_origin_dynamic_cancel]) or a negative error. /// After the terminal (`<= 0`) callback, `user_data` is never touched again. /// /// Returns a non-zero handle on success, or a negative code on failure. @@ -1362,15 +1357,14 @@ pub unsafe extern "C" fn moq_origin_dynamic( prefix: *const c_char, prefix_len: usize, route: *const moq_route, - on_request: Option, + on_request: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let origin = ffi::parse_id(origin)?; - let on_request = on_request.ok_or(Error::InvalidPointer)?; let prefix = unsafe { ffi::parse_str(prefix, prefix_len)? }; let route = unsafe { parse_route(route)? }; - let on_request = unsafe { ffi::OnStatus::new(user_data, Some(on_request)) }; + let on_request = unsafe { ffi::OnStatus::new(user_data, on_request)? }; State::lock().origin.dynamic(origin, prefix, route, on_request) }) } @@ -1397,7 +1391,7 @@ pub unsafe extern "C" fn moq_origin_dynamic_update(dynamic: u32, route: *const m /// terminal `0` (or a negative error), and that final callback is where /// `user_data` should be released. #[unsafe(no_mangle)] -pub extern "C" fn moq_origin_dynamic_close(dynamic: u32) -> i32 { +pub extern "C" fn moq_origin_dynamic_cancel(dynamic: u32) -> i32 { ffi::enter(move || { let dynamic = ffi::parse_id(dynamic)?; State::lock().origin.dynamic_close(dynamic) @@ -1472,11 +1466,11 @@ pub extern "C" fn moq_broadcast_request_free(request: u32) -> i32 { /// then exactly once more with a terminal code: `0` (stopped cleanly) or a /// negative error. After the terminal (`<= 0`) callback, `on_announce` is never /// called again and `user_data` is never touched again, so release `user_data` -/// there. The terminal callback fires even after [moq_origin_announced_close]. +/// there. The terminal callback fires even after [moq_origin_announced_cancel]. /// /// - [moq_origin_announced_info] is used to query information about the broadcast. /// - [moq_origin_announced_free] releases each delivered announced ID once read. -/// - [moq_origin_announced_close] is used to stop receiving announcements. +/// - [moq_origin_announced_cancel] is used to stop receiving announcements. /// /// Returns a non-zero handle on success, or a negative code on failure. /// @@ -1485,19 +1479,19 @@ pub extern "C" fn moq_broadcast_request_free(request: u32) -> i32 { #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_origin_announced( origin: u32, - on_announce: Option, + on_announce: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let origin = ffi::parse_id(origin)?; - let on_announce = unsafe { ffi::OnStatus::new(user_data, on_announce) }; + let on_announce = unsafe { ffi::OnStatus::new(user_data, on_announce)? }; State::lock().origin.announced(origin, on_announce) }) } /// Query information about a broadcast discovered by [moq_origin_announced]. /// -/// The destination is filled with the broadcast information. The `pattern` pointer borrows +/// The destination is filled with the route information. The `prefix` pointer borrows /// the announcement's storage: copy it out before calling [moq_origin_announced_free], which /// invalidates it. /// @@ -1519,7 +1513,7 @@ pub unsafe extern "C" fn moq_origin_announced_info(announced: u32, dst: *mut moq /// Each announce / unannounce event hands the callback a distinct announcement handle (read /// with [moq_origin_announced_info]); release it here once done to avoid leaking one per event /// over the life of the listener. This is per-announcement and distinct from -/// [moq_origin_announced_close], which stops the listener itself. After freeing, any `pattern` +/// [moq_origin_announced_cancel], which stops the listener itself. After freeing, any `prefix` /// pointer obtained from [moq_origin_announced_info] for this handle is dangling. /// /// Returns zero on success, or a negative code if the handle is unknown. @@ -1538,7 +1532,7 @@ pub extern "C" fn moq_origin_announced_free(announced: u32) -> i32 { /// still fires once more with a terminal `0` (or a negative error), and that /// final callback is where `user_data` should be released. #[unsafe(no_mangle)] -pub extern "C" fn moq_origin_announced_close(announced: u32) -> i32 { +pub extern "C" fn moq_origin_announced_cancel(announced: u32) -> i32 { ffi::enter(move || { let announced = ffi::parse_id(announced)?; State::lock().origin.announced_close(announced) @@ -1555,7 +1549,7 @@ pub extern "C" fn moq_origin_announced_close(announced: u32) -> i32 { /// /// `on_broadcast` is invoked with a positive broadcast handle once announced, then exactly once /// more with a terminal code: `0` (the wait finished, including after -/// [moq_origin_consume_announced_close]) or a negative error. After the terminal (`<= 0`) callback, +/// [moq_origin_announced_broadcast_cancel]) or a negative error. After the terminal (`<= 0`) callback, /// `on_broadcast` is never called again and `user_data` is never touched again, so release /// `user_data` there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track] /// and must be freed separately with [moq_consume_close]. @@ -1566,30 +1560,30 @@ pub extern "C" fn moq_origin_announced_close(announced: u32) -> i32 { /// - The caller must ensure that path is a valid pointer to path_len bytes of data. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback. #[unsafe(no_mangle)] -pub unsafe extern "C" fn moq_origin_consume_announced( +pub unsafe extern "C" fn moq_origin_announced_broadcast( origin: u32, path: *const c_char, path_len: usize, - on_broadcast: Option, + on_broadcast: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let origin = ffi::parse_id(origin)?; let path = unsafe { ffi::parse_str(path, path_len)? }.to_string(); - let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast) }; + let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast)? }; State::lock().origin.consume_announced(origin, path, on_broadcast) }) } -/// Abort a wait started by [moq_origin_consume_announced]. +/// Abort a wait started by [moq_origin_announced_broadcast]. /// /// Returns immediately: zero on success, or a negative code if already closed. Does NOT free -/// `user_data`. The [moq_origin_consume_announced] `on_broadcast` callback still fires once more +/// `user_data`. The [moq_origin_announced_broadcast] `on_broadcast` callback still fires once more /// with a terminal `0` (or a negative error), and that final callback is where `user_data` should /// be released. Any broadcast handle already delivered is unaffected and must still be freed with /// [moq_consume_close]. #[unsafe(no_mangle)] -pub extern "C" fn moq_origin_consume_announced_close(task: u32) -> i32 { +pub extern "C" fn moq_origin_announced_broadcast_cancel(task: u32) -> i32 { ffi::enter(move || { let task = ffi::parse_id(task)?; State::lock().origin.consume_announced_close(task) @@ -1599,12 +1593,12 @@ pub extern "C" fn moq_origin_consume_announced_close(task: u32) -> i32 { /// Request a broadcast from an origin by path, resolving as soon as it can be served. /// /// Resolves against what is reachable by exact path *now*, where -/// [moq_origin_consume_announced] waits indefinitely for a future announcement: it returns an +/// [moq_origin_announced_broadcast] waits indefinitely for a future announcement: it returns an /// existing broadcast at once, whether announced or not, and fails when none is reachable. It does /// NOT wait for a later announcement. Serve on-demand paths with [moq_origin_dynamic]. /// /// `on_broadcast` is invoked with a positive broadcast handle once served, then exactly once more -/// with a terminal code: `0` (finished, including after [moq_origin_request_close]) or a negative +/// with a terminal code: `0` (finished, including after [moq_origin_request_cancel]) or a negative /// error. After the terminal (`<= 0`) callback, `user_data` is never touched again, so release it /// there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track] and must /// be freed separately with [moq_consume_close]. @@ -1619,13 +1613,13 @@ pub unsafe extern "C" fn moq_origin_request( origin: u32, path: *const c_char, path_len: usize, - on_broadcast: Option, + on_broadcast: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let origin = ffi::parse_id(origin)?; let path = unsafe { ffi::parse_str(path, path_len)? }.to_string(); - let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast) }; + let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast)? }; State::lock().origin.request(origin, path, on_broadcast) }) } @@ -1637,7 +1631,7 @@ pub unsafe extern "C" fn moq_origin_request( /// code, which is where `user_data` should be released. Any broadcast handle already delivered is /// unaffected and must still be freed with [moq_consume_close]. #[unsafe(no_mangle)] -pub extern "C" fn moq_origin_request_close(task: u32) -> i32 { +pub extern "C" fn moq_origin_request_cancel(task: u32) -> i32 { ffi::enter(move || { let task = ffi::parse_id(task)?; State::lock().origin.consume_announced_close(task) @@ -1838,7 +1832,7 @@ pub extern "C" fn moq_publish_media_finish(export: u32) -> i32 { /// /// `on_demand` fires right away with the current [moq_demand] state, again on every /// change, then exactly once more with a terminal code: `0` (the track ended or the -/// watcher was closed with [moq_publish_demand_close]) or a negative error. After the +/// watcher was stopped with [moq_publish_demand_cancel]) or a negative error. After the /// terminal (`<= 0`) callback, `user_data` is never touched again. Reporting the current /// state first means a track that went unused before the watcher existed still reports it. /// @@ -1852,13 +1846,12 @@ pub extern "C" fn moq_publish_media_finish(export: u32) -> i32 { #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_media_demand( media: u32, - on_demand: Option, + on_demand: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let media = ffi::parse_id(media)?; - let on_demand = on_demand.ok_or(Error::InvalidPointer)?; - let on_demand = unsafe { ffi::OnStatus::new(user_data, Some(on_demand)) }; + let on_demand = unsafe { ffi::OnStatus::new(user_data, on_demand)? }; let mut state = State::lock(); let demand = state.publish.media_demand(media)?; state.publish.demand(demand, on_demand) @@ -1872,7 +1865,7 @@ pub unsafe extern "C" fn moq_publish_media_demand( /// watcher's `on_demand` callback still fires once more with a terminal `0`, and /// that final callback is where `user_data` should be released. #[unsafe(no_mangle)] -pub extern "C" fn moq_publish_demand_close(watcher: u32) -> i32 { +pub extern "C" fn moq_publish_demand_cancel(watcher: u32) -> i32 { ffi::enter(move || { let watcher = ffi::parse_id(watcher)?; State::lock().publish.demand_close(watcher) @@ -2339,13 +2332,12 @@ pub extern "C" fn moq_publish_track_abort(track: u32, error_code: u16) -> i32 { #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_track_demand( track: u32, - on_demand: Option, + on_demand: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; - let on_demand = on_demand.ok_or(Error::InvalidPointer)?; - let on_demand = unsafe { ffi::OnStatus::new(user_data, Some(on_demand)) }; + let on_demand = unsafe { ffi::OnStatus::new(user_data, on_demand)? }; let mut state = State::lock(); let demand = state.publish.track_demand(track)?; state.publish.demand(demand, on_demand) @@ -2357,7 +2349,7 @@ pub unsafe extern "C" fn moq_publish_track_demand( /// Without a live handler a subscription to an unknown track name is refused. While one /// is live, `on_request` is invoked with a positive request handle for each pending /// track, then exactly once more with a terminal code: `0` (the broadcast finished, or -/// [moq_publish_dynamic_close] was called) or a negative error. After the terminal +/// [moq_publish_dynamic_cancel] was called) or a negative error. After the terminal /// (`<= 0`) callback, `user_data` is never touched again. Answer each request with /// [moq_track_request_accept], [moq_track_request_video], [moq_track_request_audio], /// or [moq_track_request_abort]; the subscriber waits until you do. @@ -2370,13 +2362,12 @@ pub unsafe extern "C" fn moq_publish_track_demand( #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_dynamic( broadcast: u32, - on_request: Option, + on_request: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; - let on_request = on_request.ok_or(Error::InvalidPointer)?; - let on_request = unsafe { ffi::OnStatus::new(user_data, Some(on_request)) }; + let on_request = unsafe { ffi::OnStatus::new(user_data, on_request)? }; State::lock().publish.dynamic(broadcast, on_request) }) } @@ -2386,7 +2377,7 @@ pub unsafe extern "C" fn moq_publish_dynamic( /// Without a live handler a fetch that misses the cache fails as not found. While one is /// live, `on_group` is invoked with a positive group-request handle for each miss, then /// exactly once more with a terminal code: `0` (the track ended, or -/// [moq_publish_dynamic_close] was called) or a negative error. After the terminal +/// [moq_publish_dynamic_cancel] was called) or a negative error. After the terminal /// (`<= 0`) callback, `user_data` is never touched again. Cached groups never reach the /// handler. Answer each request with [moq_group_request_accept] or [moq_group_request_abort]. /// @@ -2398,13 +2389,12 @@ pub unsafe extern "C" fn moq_publish_dynamic( #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_track_dynamic( track: u32, - on_group: Option, + on_group: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; - let on_group = on_group.ok_or(Error::InvalidPointer)?; - let on_group = unsafe { ffi::OnStatus::new(user_data, Some(on_group)) }; + let on_group = unsafe { ffi::OnStatus::new(user_data, on_group)? }; State::lock().publish.track_dynamic(track, on_group) }) } @@ -2416,7 +2406,7 @@ pub unsafe extern "C" fn moq_publish_track_dynamic( /// handler's callback still fires once more with a terminal `0`, and that final /// callback is where `user_data` should be released. #[unsafe(no_mangle)] -pub extern "C" fn moq_publish_dynamic_close(dynamic: u32) -> i32 { +pub extern "C" fn moq_publish_dynamic_cancel(dynamic: u32) -> i32 { ffi::enter(move || { let dynamic = ffi::parse_id(dynamic)?; State::lock().publish.dynamic_close(dynamic) @@ -2455,13 +2445,12 @@ pub unsafe extern "C" fn moq_track_request_name(request: u32, dst: *mut moq_stri #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_track_request_dynamic( request: u32, - on_group: Option, + on_group: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; - let on_group = on_group.ok_or(Error::InvalidPointer)?; - let on_group = unsafe { ffi::OnStatus::new(user_data, Some(on_group)) }; + let on_group = unsafe { ffi::OnStatus::new(user_data, on_group)? }; State::lock().publish.track_request_dynamic(request, on_group) }) } @@ -2790,7 +2779,7 @@ pub extern "C" fn moq_publish_json_stream_finish(stream: u32) -> i32 { /// a terminal code: `0` (closed cleanly) or a negative error. After the terminal /// (`<= 0`) callback, `on_catalog` is never called again and `user_data` is never /// touched again, so release `user_data` there. The terminal callback fires even -/// after [moq_consume_catalog_close]. +/// after [moq_consume_catalog_cancel]. /// /// Returns a non-zero handle on success, or a negative code on failure. /// @@ -2799,12 +2788,12 @@ pub extern "C" fn moq_publish_json_stream_finish(stream: u32) -> i32 { #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_catalog( broadcast: u32, - on_catalog: Option, + on_catalog: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; - let on_catalog = unsafe { ffi::OnStatus::new(user_data, on_catalog) }; + let on_catalog = unsafe { ffi::OnStatus::new(user_data, on_catalog)? }; State::lock().consume.catalog(broadcast, on_catalog) }) } @@ -2817,7 +2806,7 @@ pub unsafe extern "C" fn moq_consume_catalog( /// should be released. Catalog snapshots previously delivered via the callback /// remain valid until freed with [moq_consume_catalog_free]. #[unsafe(no_mangle)] -pub extern "C" fn moq_consume_catalog_close(catalog: u32) -> i32 { +pub extern "C" fn moq_consume_catalog_cancel(catalog: u32) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; State::lock().consume.catalog_close(catalog) @@ -2993,7 +2982,7 @@ pub unsafe extern "C" fn moq_consume_catalog_section( /// more with a terminal code: `0` (closed cleanly) or a negative error. After /// the terminal (`<= 0`) callback, `on_frame` is never called again and /// `user_data` is never touched again, so release `user_data` there. The -/// terminal callback fires even after [moq_consume_video_close]. +/// terminal callback fires even after [moq_consume_video_cancel]. /// /// Returns a non-zero handle to the track on success, or a negative code on failure. /// @@ -3004,14 +2993,14 @@ pub unsafe extern "C" fn moq_consume_video( catalog: u32, index: u32, max_age_us: u64, - on_frame: Option, + on_frame: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; let index = index as usize; let max_age = std::time::Duration::from_micros(max_age_us); - let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) }; + let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame)? }; State::lock().consume.video(catalog, index, max_age, on_frame) }) } @@ -3023,7 +3012,7 @@ pub unsafe extern "C" fn moq_consume_video( /// still fires once more with a terminal `0` (or a negative error), which is /// where `user_data` should be released. #[unsafe(no_mangle)] -pub extern "C" fn moq_consume_video_close(track: u32) -> i32 { +pub extern "C" fn moq_consume_video_cancel(track: u32) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; State::lock().consume.track_close(track) @@ -3036,7 +3025,7 @@ pub extern "C" fn moq_consume_video_close(track: u32) -> i32 { /// more with a terminal code: `0` (closed cleanly) or a negative error. After /// the terminal (`<= 0`) callback, `on_frame` is never called again and /// `user_data` is never touched again, so release `user_data` there. The -/// terminal callback fires even after [moq_consume_audio_close]. +/// terminal callback fires even after [moq_consume_audio_cancel]. /// The `max_age_us` parameter controls how long to wait before skipping frames. /// /// Returns a non-zero handle to the track on success, or a negative code on failure. @@ -3048,14 +3037,14 @@ pub unsafe extern "C" fn moq_consume_audio( catalog: u32, index: u32, max_age_us: u64, - on_frame: Option, + on_frame: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; let index = index as usize; let max_age = std::time::Duration::from_micros(max_age_us); - let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) }; + let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame)? }; State::lock().consume.audio(catalog, index, max_age, on_frame) }) } @@ -3067,7 +3056,7 @@ pub unsafe extern "C" fn moq_consume_audio( /// still fires once more with a terminal `0` (or a negative error), which is /// where `user_data` should be released. #[unsafe(no_mangle)] -pub extern "C" fn moq_consume_audio_close(track: u32) -> i32 { +pub extern "C" fn moq_consume_audio_cancel(track: u32) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; State::lock().consume.track_close(track) @@ -3125,7 +3114,7 @@ pub extern "C" fn moq_consume_close(consume: u32) -> i32 { /// (closed cleanly) or a negative error. After the terminal (`<= 0`) callback, /// `on_frame` is never called again and `user_data` is never touched again, so /// release `user_data` there. The terminal callback fires even after -/// [moq_consume_track_close]. Read each frame with [moq_consume_track_frame] and +/// [moq_consume_track_cancel]. Read each frame with [moq_consume_track_frame] and /// release it with [moq_consume_track_frame_free]. Pass NULL for `subscription` /// to use moq-net defaults. /// @@ -3141,14 +3130,14 @@ pub unsafe extern "C" fn moq_consume_track( name: *const c_char, name_len: usize, subscription: *const moq_subscription, - on_frame: Option, + on_frame: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; let subscription = unsafe { subscription.as_ref() }.map(moq_net::track::Subscription::from); - let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) }; + let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame)? }; State::lock().consume.raw_track(broadcast, name, subscription, on_frame) }) } @@ -3209,7 +3198,7 @@ pub extern "C" fn moq_consume_track_frame_free(frame: u32) -> i32 { /// `user_data` should be released. Frames already delivered via the callback /// remain valid until released with [moq_consume_track_frame_free]. #[unsafe(no_mangle)] -pub extern "C" fn moq_consume_track_close(track: u32) -> i32 { +pub extern "C" fn moq_consume_track_cancel(track: u32) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; State::lock().consume.raw_track_close(track) @@ -3223,7 +3212,7 @@ pub extern "C" fn moq_consume_track_close(track: u32) -> i32 { /// once more with a terminal code: `0` (closed cleanly) or a negative error. After the /// terminal (`<= 0`) callback, `on_datagram` is never called again and `user_data` is never /// touched again, so release `user_data` there. The terminal callback fires even after -/// [moq_consume_datagrams_close]. Read each datagram with [moq_consume_datagram] and release +/// [moq_consume_datagrams_cancel]. Read each datagram with [moq_consume_datagram] and release /// it with [moq_consume_datagram_free]. Datagrams arrive only over datagram-capable /// transports and lite-05 or newer moq-lite; there is no stream fallback. /// @@ -3237,13 +3226,13 @@ pub unsafe extern "C" fn moq_consume_datagrams( broadcast: u32, name: *const c_char, name_len: usize, - on_datagram: Option, + on_datagram: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; - let on_datagram = unsafe { ffi::OnStatus::new(user_data, on_datagram) }; + let on_datagram = unsafe { ffi::OnStatus::new(user_data, on_datagram)? }; State::lock().consume.datagram_track(broadcast, name, on_datagram) }) } @@ -3284,7 +3273,7 @@ pub extern "C" fn moq_consume_datagram_free(datagram: u32) -> i32 { /// terminal `0` (or a negative error), which is where `user_data` should be released. Datagrams /// already delivered via the callback remain valid until released with [moq_consume_datagram_free]. #[unsafe(no_mangle)] -pub extern "C" fn moq_consume_datagrams_close(task: u32) -> i32 { +pub extern "C" fn moq_consume_datagrams_cancel(task: u32) -> i32 { ffi::enter(move || { let task = ffi::parse_id(task)?; State::lock().consume.datagram_track_close(task) @@ -3310,7 +3299,7 @@ pub unsafe extern "C" fn moq_consume_json_snapshot( name: *const c_char, name_len: usize, config: *const moq_json_snapshot_config, - on_value: Option, + on_value: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { @@ -3323,7 +3312,7 @@ pub unsafe extern "C" fn moq_consume_json_snapshot( } else { moq_json::Compression::None }; - let on_value = unsafe { ffi::OnStatus::new(user_data, on_value) }; + let on_value = unsafe { ffi::OnStatus::new(user_data, on_value)? }; State::lock().consume.json_snapshot(broadcast, name, consumer, on_value) }) } @@ -3345,7 +3334,7 @@ pub unsafe extern "C" fn moq_consume_json_stream( name: *const c_char, name_len: usize, config: *const moq_json_stream_config, - on_value: Option, + on_value: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { @@ -3356,7 +3345,7 @@ pub unsafe extern "C" fn moq_consume_json_stream( if config.compression { consumer.compression = moq_json::Compression::Deflate; } - let on_value = unsafe { ffi::OnStatus::new(user_data, on_value) }; + let on_value = unsafe { ffi::OnStatus::new(user_data, on_value)? }; State::lock().consume.json_stream(broadcast, name, consumer, on_value) }) } @@ -3397,7 +3386,7 @@ pub extern "C" fn moq_consume_json_value_free(value: u32) -> i32 { /// error), which is where `user_data` should be released. Values already delivered remain valid /// until released with [moq_consume_json_value_free]. #[unsafe(no_mangle)] -pub extern "C" fn moq_consume_json_close(task: u32) -> i32 { +pub extern "C" fn moq_consume_json_cancel(task: u32) -> i32 { ffi::enter(move || { let task = ffi::parse_id(task)?; State::lock().consume.json_close(task) diff --git a/rs/libmoq/src/audio.rs b/rs/libmoq/src/audio.rs index 13e4378fa3..9be45bc89d 100644 --- a/rs/libmoq/src/audio.rs +++ b/rs/libmoq/src/audio.rs @@ -408,13 +408,12 @@ pub extern "C" fn moq_encode_audio_reservation(producer: u32) -> i32 { #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_encode_audio_demand( producer: u32, - on_demand: Option, + on_demand: crate::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let producer = ffi::parse_id(producer)?; - let on_demand = on_demand.ok_or(Error::InvalidPointer)?; - let on_demand = unsafe { OnStatus::new(user_data, Some(on_demand)) }; + let on_demand = unsafe { OnStatus::new(user_data, on_demand)? }; let mut state = State::lock(); let demand = state.audio.demand(producer)?; state.publish.demand(demand, on_demand) @@ -484,7 +483,7 @@ pub extern "C" fn moq_encode_audio_finish(producer: u32) -> i32 { /// more with a terminal code: `0` (closed cleanly) or a negative error. After /// the terminal (`<= 0`) callback, `on_frame` is never called again and /// `user_data` is never touched again, so release `user_data` there. The -/// terminal callback fires even after [`moq_decode_audio_close`]. +/// terminal callback fires even after [`moq_decode_audio_cancel`]. /// /// Starts at the newest cached group so reopening live playback skips the backlog. /// @@ -499,7 +498,7 @@ pub unsafe extern "C" fn moq_decode_audio( catalog: u32, index: u32, output: *const moq_audio_decoder_output, - on_frame: Option, + on_frame: crate::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { @@ -513,7 +512,7 @@ pub unsafe extern "C" fn moq_decode_audio( config.channels = zeroable(raw.channels); config.max_age = Duration::from_micros(raw.max_age_us); - let on_frame = unsafe { OnStatus::new(user_data, on_frame) }; + let on_frame = unsafe { OnStatus::new(user_data, on_frame)? }; let mut state = State::lock(); let (broadcast, audio_cfg, name) = state.consume.audio_rendition(catalog, index as usize)?; @@ -531,7 +530,7 @@ pub unsafe extern "C" fn moq_decode_audio( /// released. Frame IDs already delivered to the callback are likewise not freed; /// release each with [`moq_decode_audio_frame_free`]. #[unsafe(no_mangle)] -pub extern "C" fn moq_decode_audio_close(consumer: u32) -> i32 { +pub extern "C" fn moq_decode_audio_cancel(consumer: u32) -> i32 { ffi::enter(move || { let consumer = ffi::parse_id(consumer)?; State::lock().audio.consume_close(consumer) diff --git a/rs/libmoq/src/client.rs b/rs/libmoq/src/client.rs index 5945822a0c..e6a1aade92 100644 --- a/rs/libmoq/src/client.rs +++ b/rs/libmoq/src/client.rs @@ -53,19 +53,19 @@ pub unsafe fn parse_client(config: Option<&moq_client_config>) -> Result) -> Result i32 { match self { - Error::Moq(_) => -2, - Error::Url(_) => -3, - Error::Utf8(_) => -4, - Error::Connect(_) => -5, - Error::InvalidPointer => -6, - Error::InvalidId => -7, - Error::NotFound => -8, - Error::UnknownFormat(_) => -9, - Error::InitFailed(_) => -10, - Error::TimestampOverflow(_) => -13, - Error::Level(_) => -14, - Error::InvalidCode => -15, - Error::Panic => -16, - Error::Offline => -17, - Error::Hang(_) => -18, - Error::NoIndex => -19, - Error::NulError(_) => -20, - Error::SessionNotFound => -21, - Error::OriginNotFound => -22, - Error::AnnouncementNotFound => -23, - Error::BroadcastNotFound => -24, - Error::CatalogNotFound => -25, - Error::MediaNotFound => -26, - Error::TrackNotFound => -27, - Error::FrameNotFound => -28, - Error::Mux(_) => -29, - Error::Audio(_) => -30, - Error::BufferNotConsumed => -31, - Error::GroupNotFound => -32, - Error::Native(_) => -33, - Error::Unauthorized => -34, - Error::Forbidden => -35, - Error::Video(_) => -36, - Error::Json(_) => -37, - Error::JsonTrack(_) => -38, - Error::InvalidConfig(_) => -40, - Error::UnresolvableBroadcast(_) => -41, + Error::Moq(_) => MOQ_ERROR_MOQ, + Error::Url(_) => MOQ_ERROR_URL, + Error::Utf8(_) => MOQ_ERROR_UTF8, + Error::Connect(_) => MOQ_ERROR_CONNECT, + Error::InvalidPointer => MOQ_ERROR_INVALID_POINTER, + Error::InvalidId => MOQ_ERROR_INVALID_ID, + Error::NotFound => MOQ_ERROR_NOT_FOUND, + Error::UnknownFormat(_) => MOQ_ERROR_UNKNOWN_FORMAT, + Error::InitFailed(_) => MOQ_ERROR_INIT_FAILED, + Error::TimestampOverflow(_) => MOQ_ERROR_TIMESTAMP_OVERFLOW, + Error::Level(_) => MOQ_ERROR_LEVEL, + Error::InvalidCode => MOQ_ERROR_INVALID_CODE, + Error::Panic => MOQ_ERROR_PANIC, + Error::Offline => MOQ_ERROR_OFFLINE, + Error::Hang(_) => MOQ_ERROR_HANG, + Error::NoIndex => MOQ_ERROR_NO_INDEX, + Error::NulError(_) => MOQ_ERROR_NUL, + Error::SessionNotFound => MOQ_ERROR_SESSION_NOT_FOUND, + Error::OriginNotFound => MOQ_ERROR_ORIGIN_NOT_FOUND, + Error::AnnouncementNotFound => MOQ_ERROR_ANNOUNCEMENT_NOT_FOUND, + Error::BroadcastNotFound => MOQ_ERROR_BROADCAST_NOT_FOUND, + Error::CatalogNotFound => MOQ_ERROR_CATALOG_NOT_FOUND, + Error::MediaNotFound => MOQ_ERROR_MEDIA_NOT_FOUND, + Error::TrackNotFound => MOQ_ERROR_TRACK_NOT_FOUND, + Error::FrameNotFound => MOQ_ERROR_FRAME_NOT_FOUND, + Error::Mux(_) => MOQ_ERROR_MUX, + Error::Audio(_) => MOQ_ERROR_AUDIO, + Error::BufferNotConsumed => MOQ_ERROR_BUFFER_NOT_CONSUMED, + Error::GroupNotFound => MOQ_ERROR_GROUP_NOT_FOUND, + Error::Native(_) => MOQ_ERROR_NATIVE, + Error::Unauthorized => MOQ_ERROR_UNAUTHORIZED, + Error::Forbidden => MOQ_ERROR_FORBIDDEN, + Error::Video(_) => MOQ_ERROR_VIDEO, + Error::Json(_) => MOQ_ERROR_JSON, + Error::JsonTrack(_) => MOQ_ERROR_JSON_TRACK, + Error::InvalidConfig(_) => MOQ_ERROR_INVALID_CONFIG, + Error::UnresolvableBroadcast(_) => MOQ_ERROR_UNRESOLVABLE_BROADCAST, } } } diff --git a/rs/libmoq/src/ffi.rs b/rs/libmoq/src/ffi.rs index 5b072f8f9b..c081700c98 100644 --- a/rs/libmoq/src/ffi.rs +++ b/rs/libmoq/src/ffi.rs @@ -8,6 +8,10 @@ use url::Url; use crate::{Error, Id, moq_protocol_error}; +/// A callback receiving a positive handle/value, zero on clean completion, or a negative error. +#[allow(non_camel_case_types)] +pub type moq_status_callback = Option; + pub static RUNTIME: LazyLock = LazyLock::new(|| { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() @@ -54,7 +58,7 @@ pub fn enter C>(f: F) -> i32 { #[derive(Clone, Copy)] pub struct OnStatus { user_data: *mut c_void, - on_status: Option, + on_status: extern "C" fn(user_data: *mut c_void, code: i32), } impl OnStatus { @@ -63,11 +67,11 @@ impl OnStatus { /// # Safety /// - The caller must ensure user_data remains valid for the callback's lifetime. /// - The callback function pointer must be valid if provided. - pub unsafe fn new( - user_data: *mut c_void, - on_status: Option, - ) -> Self { - Self { user_data, on_status } + pub unsafe fn new(user_data: *mut c_void, on_status: moq_status_callback) -> Result { + Ok(Self { + user_data, + on_status: on_status.ok_or(Error::InvalidPointer)?, + }) } /// Invoke the callback with a result code. @@ -77,9 +81,7 @@ impl OnStatus { pub fn call(&self, ret: C) { record_error(&ret); let code = ret.code(); - if let Some(on_status) = &self.on_status { - on_status(self.user_data, code); - } + (self.on_status)(self.user_data, code); } } diff --git a/rs/libmoq/src/lib.rs b/rs/libmoq/src/lib.rs index 61b3356c51..5e9a3398b6 100644 --- a/rs/libmoq/src/lib.rs +++ b/rs/libmoq/src/lib.rs @@ -34,6 +34,7 @@ pub use api::*; pub use audio::*; pub use bandwidth::*; pub use error::*; +pub use ffi::moq_status_callback; pub use id::*; pub use video::*; diff --git a/rs/libmoq/src/origin.rs b/rs/libmoq/src/origin.rs index 80badce2f6..bb558262b4 100644 --- a/rs/libmoq/src/origin.rs +++ b/rs/libmoq/src/origin.rs @@ -111,8 +111,8 @@ impl Origin { pub fn announced_info(&self, announced: Id, dst: &mut moq_announce_update) -> Result<(), Error> { let announced = self.announced.get(announced).ok_or(Error::AnnouncementNotFound)?; *dst = moq_announce_update { - path: announced.0.as_str().as_ptr() as *const c_char, - path_len: announced.0.len(), + prefix: announced.0.as_str().as_ptr() as *const c_char, + prefix_len: announced.0.len(), active: announced.1, }; Ok(()) @@ -356,7 +356,7 @@ impl Origin { .get_mut(dynamic) .and_then(|entry| entry.as_mut()) .ok_or(Error::NotFound)?; - let inner = entry.inner.take(); + let inner = entry.inner.take().ok_or(Error::NotFound)?; entry.close.take(); drop(inner); Ok(()) diff --git a/rs/libmoq/src/test.rs b/rs/libmoq/src/test.rs index 246e11d4b2..f72c262984 100644 --- a/rs/libmoq/src/test.rs +++ b/rs/libmoq/src/test.rs @@ -114,6 +114,9 @@ extern "C" fn channel_callback(user_data: *mut c_void, code: i32) { let _ = tx.send(code); } +/// FFI callback for tests that only inspect the registrar's immediate return code. +extern "C" fn ignore_callback(_user_data: *mut c_void, _code: i32) {} + /// Build a valid OpusHead init buffer (RFC 7845 §5.1). fn opus_head() -> Vec { let mut head = Vec::with_capacity(19); @@ -223,12 +226,18 @@ fn last_error_set_before_callback() { } let mut captured: Option = None; - let cb = unsafe { OnStatus::new(&mut captured as *mut _ as *mut c_void, Some(capture)) }; + let cb = unsafe { OnStatus::new(&mut captured as *mut _ as *mut c_void, Some(capture)) }.unwrap(); cb.call(Err::<(), Error>(Error::OriginNotFound)); assert_eq!(captured.as_deref(), Some("origin not found")); } +#[test] +fn status_callback_refuses_null() { + let callback = unsafe { crate::ffi::OnStatus::new(std::ptr::null_mut(), None) }; + assert!(matches!(callback, Err(Error::InvalidPointer))); +} + #[test] fn last_error_protocol_is_none_for_a_local_failure() { assert!(moq_origin_close(9999) < 0); @@ -259,7 +268,7 @@ fn last_error_protocol_captures_a_stream_app_code() { } let mut captured: Option = None; - let cb = unsafe { OnStatus::new(&mut captured as *mut _ as *mut c_void, Some(capture)) }; + let cb = unsafe { OnStatus::new(&mut captured as *mut _ as *mut c_void, Some(capture)) }.unwrap(); cb.call(Err::<(), Error>(Error::Moq(moq_net::StreamError::App(404).into()))); let protocol = captured.expect("expected a protocol error"); @@ -324,7 +333,7 @@ fn last_error_protocol_captures_session_known_app_and_unknown() { ), ] { let mut captured: Option = None; - let cb = unsafe { OnStatus::new(&mut captured as *mut _ as *mut c_void, Some(capture)) }; + let cb = unsafe { OnStatus::new(&mut captured as *mut _ as *mut c_void, Some(capture)) }.unwrap(); cb.call(Err::<(), Error>(Error::Moq(err))); let protocol = captured.expect("expected a protocol error"); assert_eq!(protocol.scope, scope); @@ -477,7 +486,7 @@ fn publish_media_labels_config_without_naming_track() { assert_eq!(moq_consume_catalog_free(catalog_id1), 0); assert_eq!(moq_consume_catalog_free(catalog_id2), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_terminal(), 0, "catalog close delivers terminal 0"); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_publish_media_finish(media1), 0); @@ -859,7 +868,7 @@ fn publish_catalog_roundtrip() { assert_eq!(moq_consume_catalog_free(catalog_id), 0); assert_eq!(moq_consume_catalog_free(active_catalog_id), 0); assert_eq!(moq_consume_catalog_free(catalog_id2), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_terminal(), 0, "catalog close delivers terminal 0"); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_publish_finish(broadcast), 0); @@ -931,13 +940,13 @@ fn a_half_specified_coded_size_round_trips() { assert_eq!(read.coded_height, 0, "forwarding must not invent the absent height"); assert_eq!(moq_consume_catalog_free(forwarded_catalog), 0); - assert_eq!(moq_consume_catalog_close(forwarded_task), 0); + assert_eq!(moq_consume_catalog_cancel(forwarded_task), 0); assert_eq!(forwarded_cb.recv_catalog_terminal(), 0); assert_eq!(moq_consume_close(forwarded), 0); assert_eq!(moq_publish_finish(forward), 0); assert_eq!(moq_consume_catalog_free(catalog), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_catalog_terminal(), 0); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_publish_finish(broadcast), 0); @@ -1029,10 +1038,10 @@ fn raw_loc_video_uses_the_declared_catalog_container() { ); assert_eq!(moq_consume_frame_free(frame_id), 0); - assert_eq!(moq_consume_video_close(consumer), 0); + assert_eq!(moq_consume_video_cancel(consumer), 0); assert_eq!(frame_cb.recv_terminal(), 0); assert_eq!(moq_consume_catalog_free(catalog), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_terminal(), 0); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_publish_track_finish(track), 0); @@ -1097,7 +1106,7 @@ fn cmaf_catalog_container_carries_its_init_segment() { ); assert_eq!(moq_consume_catalog_free(catalog), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_terminal(), 0); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_publish_finish(broadcast), 0); @@ -1312,7 +1321,7 @@ fn catalog_section_roundtrip() { assert_eq!(moq_consume_catalog_free(catalog_id), 0); assert_eq!(moq_consume_catalog_free(catalog_id2), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_terminal(), 0, "catalog close delivers terminal 0"); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_publish_finish(broadcast), 0); @@ -1454,11 +1463,11 @@ fn raw_track_publish_consume() { assert_eq!(moq_consume_track_frame_free(frame_id), 0); } - assert_eq!(moq_consume_track_close(consumer), 0); + assert_eq!(moq_consume_track_cancel(consumer), 0); // The task delivers one final terminal callback after close; drain it // before the Callback (user_data) drops. assert_eq!(frame_cb.recv_terminal(), 0, "clean close delivers terminal 0"); - assert!(moq_consume_track_close(consumer) < 0, "double-close should fail"); + assert!(moq_consume_track_cancel(consumer) < 0, "double-close should fail"); assert_eq!(moq_publish_track_finish(track), 0); assert!(moq_publish_track_finish(track) < 0, "double-close should fail"); assert_eq!(moq_consume_close(consume), 0); @@ -1517,11 +1526,11 @@ fn raw_track_datagram_publish_consume() { assert_eq!(datagram.sequence, sequence); assert_eq!(moq_consume_datagram_free(dg_id), 0); - assert_eq!(moq_consume_datagrams_close(consumer), 0); + assert_eq!(moq_consume_datagrams_cancel(consumer), 0); // The task delivers one final terminal callback after close; drain it // before the Callback (user_data) drops. assert_eq!(dg_cb.recv_terminal(), 0, "clean close delivers terminal 0"); - assert!(moq_consume_datagrams_close(consumer) < 0, "double-close should fail"); + assert!(moq_consume_datagrams_cancel(consumer) < 0, "double-close should fail"); assert_eq!(moq_publish_track_finish(track), 0); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_publish_finish(broadcast), 0); @@ -1640,7 +1649,7 @@ fn raw_track_subscription_options_and_update() { assert_eq!(frame.timestamp_us, 40_000); assert_eq!(moq_consume_track_frame_free(frame_id), 0); - assert_eq!(moq_consume_track_close(consumer), 0); + assert_eq!(moq_consume_track_cancel(consumer), 0); assert_eq!(frame_cb.recv_terminal(), 0); assert_eq!(moq_publish_track_finish(track), 0); assert_eq!(moq_consume_close(consume), 0); @@ -1701,9 +1710,9 @@ fn json_snapshot_publish_consume() { assert_eq!(moq_consume_json_value_free(value_id), 0); } - assert_eq!(moq_consume_json_close(consumer), 0); + assert_eq!(moq_consume_json_cancel(consumer), 0); assert_eq!(value_cb.recv_terminal(), 0, "clean close delivers terminal 0"); - assert!(moq_consume_json_close(consumer) < 0, "double-close should fail"); + assert!(moq_consume_json_cancel(consumer) < 0, "double-close should fail"); assert_eq!(moq_publish_json_snapshot_finish(producer), 0); assert!( moq_publish_json_snapshot_finish(producer) < 0, @@ -1764,9 +1773,9 @@ fn json_stream_publish_consume() { assert_eq!(moq_consume_json_value_free(value_id), 0); } - assert_eq!(moq_consume_json_close(consumer), 0); + assert_eq!(moq_consume_json_cancel(consumer), 0); assert_eq!(value_cb.recv_terminal(), 0, "clean close delivers terminal 0"); - assert!(moq_consume_json_close(consumer) < 0, "double-close should fail"); + assert!(moq_consume_json_cancel(consumer) < 0, "double-close should fail"); assert_eq!(moq_publish_json_stream_finish(producer), 0); assert!(moq_publish_json_stream_finish(producer) < 0, "double-close should fail"); assert_eq!(moq_consume_close(consume), 0); @@ -1801,14 +1810,14 @@ fn announced_free_lifecycle() { // Its info reports our path, active. let mut info = moq_announce_update { - path: std::ptr::null(), - path_len: 0, + prefix: std::ptr::null(), + prefix_len: 0, active: false, }; assert_eq!(unsafe { moq_origin_announced_info(announced, &mut info) }, 0); assert!(info.active, "broadcast should be active"); - let got = unsafe { std::slice::from_raw_parts(info.path.cast::(), info.path_len) }; - assert_eq!(got, path, "announced path should match"); + let got = unsafe { std::slice::from_raw_parts(info.prefix.cast::(), info.prefix_len) }; + assert_eq!(got, path, "announced prefix should match"); // Freeing the record succeeds once; the handle is then unknown. assert_eq!(moq_origin_announced_free(announced), 0); @@ -1819,7 +1828,7 @@ fn announced_free_lifecycle() { ); // Stop the listener and drain its terminal callback before the Callback drops. - assert_eq!(moq_origin_announced_close(ann_task), 0); + assert_eq!(moq_origin_announced_cancel(ann_task), 0); ann_cb.recv_terminal(); assert_eq!(moq_origin_close(origin), 0); @@ -1867,16 +1876,16 @@ fn double_close_all_resource_types() { assert_eq!(moq_consume_frame_free(frame_id), 0); assert!(moq_consume_frame_free(frame_id) < 0); - assert_eq!(moq_consume_audio_close(track), 0); + assert_eq!(moq_consume_audio_cancel(track), 0); assert_eq!(frame_cb.recv_terminal(), 0, "audio close delivers terminal 0"); - assert!(moq_consume_audio_close(track) < 0); + assert!(moq_consume_audio_cancel(track) < 0); assert_eq!(moq_consume_catalog_free(catalog_id), 0); assert!(moq_consume_catalog_free(catalog_id) < 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_terminal(), 0, "catalog close delivers terminal 0"); - assert!(moq_consume_catalog_close(catalog_task) < 0); + assert!(moq_consume_catalog_cancel(catalog_task) < 0); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_publish_media_finish(media), 0); @@ -1954,18 +1963,18 @@ fn local_announce() { let announced_id = id(cb.recv()); let mut info = moq_announce_update { - path: std::ptr::null(), - path_len: 0, + prefix: std::ptr::null(), + prefix_len: 0, active: false, }; assert_eq!(unsafe { moq_origin_announced_info(announced_id, &mut info) }, 0); assert!(info.active, "broadcast should be active"); - let announced_path = - unsafe { std::str::from_utf8(std::slice::from_raw_parts(info.path.cast::(), info.path_len)).unwrap() }; - assert_eq!(announced_path, "test/broadcast"); + let announced_prefix = + unsafe { std::str::from_utf8(std::slice::from_raw_parts(info.prefix.cast::(), info.prefix_len)).unwrap() }; + assert_eq!(announced_prefix, "test/broadcast"); - assert_eq!(moq_origin_announced_close(announced_task), 0); + assert_eq!(moq_origin_announced_cancel(announced_task), 0); assert_eq!(cb.recv_terminal(), 0, "announced close delivers terminal 0"); assert_eq!(moq_publish_finish(broadcast), 0); assert_eq!(moq_origin_close(origin), 0); @@ -1982,8 +1991,8 @@ fn announced_deactivation() { let announced_id = id(cb.recv()); let mut info = moq_announce_update { - path: std::ptr::null(), - path_len: 0, + prefix: std::ptr::null(), + prefix_len: 0, active: false, }; assert_eq!(unsafe { moq_origin_announced_info(announced_id, &mut info) }, 0); @@ -1997,7 +2006,7 @@ fn announced_deactivation() { assert_eq!(unsafe { moq_origin_announced_info(deactivated_id, &mut info) }, 0); assert!(!info.active, "broadcast should be inactive after unannounce"); - assert_eq!(moq_origin_announced_close(announced_task), 0); + assert_eq!(moq_origin_announced_cancel(announced_task), 0); assert_eq!(cb.recv_terminal(), 0, "announced close delivers terminal 0"); assert_eq!(moq_publish_finish(broadcast), 0); assert_eq!(moq_origin_close(origin), 0); @@ -2017,14 +2026,14 @@ fn create_broadcast_does_not_announce() { assert_eq!(unsafe { moq_publish_announce(broadcast, std::ptr::null()) }, 0); let announced_id = id(cb.recv()); let mut info = moq_announce_update { - path: std::ptr::null(), - path_len: 0, + prefix: std::ptr::null(), + prefix_len: 0, active: false, }; assert_eq!(unsafe { moq_origin_announced_info(announced_id, &mut info) }, 0); assert!(info.active); - assert_eq!(moq_origin_announced_close(announced_task), 0); + assert_eq!(moq_origin_announced_cancel(announced_task), 0); assert_eq!(cb.recv_terminal(), 0); assert_eq!(moq_publish_finish(broadcast), 0); assert_eq!(moq_origin_close(origin), 0); @@ -2090,7 +2099,8 @@ fn dynamic_serves_a_request_under_a_prefix() { assert!(req_cb.recv() > 0); req_cb.recv_terminal(); - assert_eq!(moq_origin_dynamic_close(dynamic), 0); + assert_eq!(moq_origin_dynamic_cancel(dynamic), 0); + assert!(moq_origin_dynamic_cancel(dynamic) < 0, "double-cancel should fail"); assert_eq!(cb.recv_terminal(), 0); assert_eq!(moq_publish_finish(served), 0); assert_eq!(moq_origin_close(origin), 0); @@ -2201,7 +2211,7 @@ fn track_demand_follows_subscribers() { let consumer = consume_track(consume, name, &frame_cb); assert_eq!(demand_cb.recv(), moq_demand::MOQ_DEMAND_USED as i32); - assert_eq!(moq_consume_track_close(consumer), 0); + assert_eq!(moq_consume_track_cancel(consumer), 0); assert_eq!(frame_cb.recv_terminal(), 0); assert_eq!(demand_cb.recv(), moq_demand::MOQ_DEMAND_UNUSED as i32); @@ -2209,15 +2219,15 @@ fn track_demand_follows_subscribers() { let late_cb = Callback::new(); let late = id(unsafe { moq_publish_track_demand(track, Some(channel_callback), late_cb.ptr) }); assert_eq!(late_cb.recv(), moq_demand::MOQ_DEMAND_UNUSED as i32); - assert_eq!(moq_publish_demand_close(late), 0); + assert_eq!(moq_publish_demand_cancel(late), 0); assert_eq!(late_cb.recv_terminal(), 0); - assert!(moq_publish_demand_close(late) < 0, "double-close should fail"); + assert!(moq_publish_demand_cancel(late) < 0, "double-close should fail"); // Finishing the track ends the remaining watcher cleanly. assert_eq!(moq_publish_track_finish(track), 0); assert_eq!(demand_cb.recv_terminal(), 0); assert!( - moq_publish_demand_close(watcher) < 0, + moq_publish_demand_cancel(watcher) < 0, "the watcher is gone after its terminal" ); @@ -2242,7 +2252,7 @@ fn track_demand_reports_current_state_before_close() { .unwrap(); let cb = Callback::new(); - let on_demand = unsafe { crate::ffi::OnStatus::new(cb.ptr, Some(channel_callback)) }; + let on_demand = unsafe { crate::ffi::OnStatus::new(cb.ptr, Some(channel_callback)) }.unwrap(); let (close, closed) = tokio::sync::oneshot::channel(); drop(close); crate::ffi::RUNTIME @@ -2397,7 +2407,7 @@ fn dynamic_serves_track_requests() { assert_eq!(protocol.kind, moq_protocol_kind::MOQ_PROTOCOL_KIND_APP as u32); assert_eq!(protocol.code, 64 + 404); assert!( - moq_consume_track_close(denied) < 0, + moq_consume_track_cancel(denied) < 0, "the subscriber is gone after its terminal" ); @@ -2408,11 +2418,11 @@ fn dynamic_serves_track_requests() { assert_eq!(moq_track_request_free(request), 0); assert!(freed_cb.recv_terminal() < 0, "a freed request fails its subscriber"); - assert_eq!(moq_publish_dynamic_close(dynamic), 0); + assert_eq!(moq_publish_dynamic_cancel(dynamic), 0); assert_eq!(request_cb.recv_terminal(), 0); - assert!(moq_publish_dynamic_close(dynamic) < 0, "double-close should fail"); + assert!(moq_publish_dynamic_cancel(dynamic) < 0, "double-close should fail"); - assert_eq!(moq_consume_track_close(consumer), 0); + assert_eq!(moq_consume_track_cancel(consumer), 0); assert_eq!(frame_cb.recv_terminal(), 0); assert_eq!(demand_cb.recv(), moq_demand::MOQ_DEMAND_UNUSED as i32); assert_eq!(moq_publish_track_finish(track), 0); @@ -2457,7 +2467,7 @@ fn dynamic_track_request_publishes_media() { let frame_id = id(frame_cb.recv()); assert_eq!(moq_consume_track_frame_free(frame_id), 0); - assert_eq!(moq_consume_track_close(consumer), 0); + assert_eq!(moq_consume_track_cancel(consumer), 0); assert_eq!(frame_cb.recv_terminal(), 0); assert_eq!(demand_cb.recv(), moq_demand::MOQ_DEMAND_UNUSED as i32); assert_eq!(moq_publish_media_finish(media), 0); @@ -2544,7 +2554,7 @@ fn track_dynamic_serves_a_fetch_miss() { .expect_err("a rejected fetch fails"); assert!(matches!(err, moq_net::Error::App(9)), "got {err:?}"); - assert_eq!(moq_publish_dynamic_close(dynamic), 0); + assert_eq!(moq_publish_dynamic_cancel(dynamic), 0); assert_eq!(group_cb.recv_terminal(), 0); assert_eq!(moq_publish_track_finish(track), 0); assert_eq!(moq_publish_finish(broadcast), 0); @@ -2579,7 +2589,7 @@ fn track_dynamic_serves_a_fetch_from_frame_start() { assert_eq!(index, 3, "the consumer resumes at the requested frame"); assert_eq!(got, payload); - assert_eq!(moq_publish_dynamic_close(dynamic), 0); + assert_eq!(moq_publish_dynamic_cancel(dynamic), 0); assert_eq!(group_cb.recv_terminal(), 0); assert_eq!(moq_publish_track_finish(track), 0); assert_eq!(moq_publish_finish(broadcast), 0); @@ -2613,7 +2623,7 @@ fn track_request_dynamic_survives_accept() { assert_eq!(moq_publish_group_finish(group), 0); assert_eq!(fetch.recv_timeout(TIMEOUT).unwrap().unwrap(), (0, payload.to_vec())); - assert_eq!(moq_publish_dynamic_close(track_dynamic), 0); + assert_eq!(moq_publish_dynamic_cancel(track_dynamic), 0); assert_eq!(group_cb.recv_terminal(), 0); assert_eq!(moq_publish_track_finish(track), 0); assert_eq!(moq_publish_finish(broadcast), 0); @@ -2707,10 +2717,10 @@ fn local_publish_consume() { assert_eq!(received, payload, "frame payload should match"); assert_eq!(moq_consume_frame_free(frame_id), 0); - assert_eq!(moq_consume_audio_close(track), 0); + assert_eq!(moq_consume_audio_cancel(track), 0); assert_eq!(frame_cb.recv_terminal(), 0, "audio close delivers terminal 0"); assert_eq!(moq_consume_catalog_free(catalog_id), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_terminal(), 0, "catalog close delivers terminal 0"); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_publish_media_finish(media), 0); @@ -2726,7 +2736,7 @@ fn consume_announced_local() { let cb = Callback::new(); let path = b"live"; let _task = id(unsafe { - moq_origin_consume_announced( + moq_origin_announced_broadcast( origin, path.as_ptr() as *const c_char, path.len(), @@ -2767,7 +2777,7 @@ fn consume_announced_local() { assert_eq!(audio_cfg.channel_count, 2); assert_eq!(moq_consume_catalog_free(catalog_id), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_terminal(), 0, "catalog close delivers terminal 0"); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_publish_media_finish(media), 0); @@ -2866,10 +2876,10 @@ fn consume_audio_follows_a_sibling_broadcast_reference() { assert_eq!(frame.timestamp_us, timestamp_us); assert_eq!(moq_consume_frame_free(frame_id), 0); - assert_eq!(moq_consume_audio_close(track), 0); + assert_eq!(moq_consume_audio_cancel(track), 0); assert_eq!(frame_cb.recv_terminal(), 0, "audio close delivers terminal 0"); assert_eq!(moq_consume_catalog_free(catalog_id), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_terminal(), 0, "catalog close delivers terminal 0"); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_publish_media_finish(media), 0); @@ -2886,7 +2896,7 @@ fn consume_announced_close_cancels() { let cb = Callback::new(); let path = b"never"; let task = id(unsafe { - moq_origin_consume_announced( + moq_origin_announced_broadcast( origin, path.as_ptr() as *const c_char, path.len(), @@ -2895,9 +2905,12 @@ fn consume_announced_close_cancels() { ) }); - assert_eq!(moq_origin_consume_announced_close(task), 0); + assert_eq!(moq_origin_announced_broadcast_cancel(task), 0); assert_eq!(cb.recv_terminal(), 0, "close delivers terminal 0"); - assert!(moq_origin_consume_announced_close(task) < 0, "double-close should fail"); + assert!( + moq_origin_announced_broadcast_cancel(task) < 0, + "double-close should fail" + ); assert_eq!(moq_origin_close(origin), 0); } @@ -2991,10 +3004,10 @@ fn video_publish_consume() { assert!(frame.payload_size > 0, "frame should have payload data"); assert_eq!(moq_consume_frame_free(frame_id), 0); - assert_eq!(moq_consume_video_close(track), 0); + assert_eq!(moq_consume_video_cancel(track), 0); assert_eq!(frame_cb.recv_terminal(), 0, "video close delivers terminal 0"); assert_eq!(moq_consume_catalog_free(catalog_id), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_terminal(), 0, "catalog close delivers terminal 0"); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_publish_media_finish(media), 0); @@ -3195,7 +3208,7 @@ fn video_raw_publish_consume() { assert_eq!(frame.data_size, 320 * 240 * 3 / 2, "tightly-packed I420"); assert_eq!(moq_decode_video_frame_free(frame_id), 0); - assert_eq!(moq_decode_video_close(consumer), 0); + assert_eq!(moq_decode_video_cancel(consumer), 0); loop { let code = frame_cb.recv(); if code > 0 { @@ -3207,7 +3220,7 @@ fn video_raw_publish_consume() { } assert_eq!(moq_consume_catalog_free(catalog_id), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_catalog_terminal(), 0); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_encode_video_finish(producer), 0); @@ -3231,7 +3244,7 @@ fn video_raw_decode_output_rejected() { height: 0, }; assert_eq!( - unsafe { moq_decode_video(1, 0, &bad_format, None, std::ptr::null_mut()) }, + unsafe { moq_decode_video(1, 0, &bad_format, Some(ignore_callback), std::ptr::null_mut()) }, Error::InvalidCode.code() ); @@ -3243,14 +3256,14 @@ fn video_raw_decode_output_rejected() { height, }; assert_eq!( - unsafe { moq_decode_video(1, 0, &bad_size, None, std::ptr::null_mut()) }, + unsafe { moq_decode_video(1, 0, &bad_size, Some(ignore_callback), std::ptr::null_mut()) }, Error::InvalidConfig(String::new()).code(), "size {width}x{height} must be refused" ); } assert_eq!( - unsafe { moq_decode_video(1, 0, std::ptr::null(), None, std::ptr::null_mut()) }, + unsafe { moq_decode_video(1, 0, std::ptr::null(), Some(ignore_callback), std::ptr::null_mut()) }, Error::InvalidPointer.code() ); @@ -3261,7 +3274,7 @@ fn video_raw_decode_output_rejected() { height: 120, }; assert_eq!( - unsafe { moq_decode_video(u32::MAX / 2, 0, &valid, None, std::ptr::null_mut()) }, + unsafe { moq_decode_video(u32::MAX / 2, 0, &valid, Some(ignore_callback), std::ptr::null_mut(),) }, Error::CatalogNotFound.code(), "a valid request fails on the catalog lookup, not on validation" ); @@ -3333,7 +3346,7 @@ fn decode_first_frame(output: &moq_video_decoder_output) -> (u32, u32, usize) { let result = (frame.width, frame.height, frame.data_size); assert_eq!(moq_decode_video_frame_free(frame_id), 0); - assert_eq!(moq_decode_video_close(consumer), 0); + assert_eq!(moq_decode_video_cancel(consumer), 0); loop { let code = frame_cb.recv(); if code > 0 { @@ -3345,7 +3358,7 @@ fn decode_first_frame(output: &moq_video_decoder_output) -> (u32, u32, usize) { } assert_eq!(moq_consume_catalog_free(catalog_id), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_catalog_terminal(), 0); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_encode_video_finish(producer), 0); @@ -3730,7 +3743,7 @@ fn video_raw_decode() { assert!(!frame.data.is_null()); assert_eq!(moq_decode_video_frame_free(frame_id), 0); - assert_eq!(moq_decode_video_close(consumer), 0); + assert_eq!(moq_decode_video_cancel(consumer), 0); // Drain any other decoded frames already queued, then expect the terminal 0. loop { @@ -3743,7 +3756,7 @@ fn video_raw_decode() { } } assert_eq!(moq_consume_catalog_free(catalog_id), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); // The publisher may emit more than one catalog snapshot (e.g. as the track's // stats settle), so drain any extra snapshots before the terminal. loop { @@ -3806,10 +3819,10 @@ fn multiple_frames_ordering() { assert_eq!(moq_consume_frame_free(frame_id), 0); } - assert_eq!(moq_consume_audio_close(track), 0); + assert_eq!(moq_consume_audio_cancel(track), 0); assert_eq!(frame_cb.recv_terminal(), 0, "audio close delivers terminal 0"); assert_eq!(moq_consume_catalog_free(catalog_id), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!( catalog_cb.recv_catalog_terminal(), 0, @@ -3861,7 +3874,7 @@ fn catalog_update_on_new_track() { assert_eq!(moq_consume_catalog_free(catalog_id1), 0); assert_eq!(moq_consume_catalog_free(catalog_id2), 0); - assert_eq!(moq_consume_catalog_close(catalog_task), 0); + assert_eq!(moq_consume_catalog_cancel(catalog_task), 0); assert_eq!(catalog_cb.recv_terminal(), 0, "catalog close delivers terminal 0"); assert_eq!(moq_consume_close(consume), 0); assert_eq!(moq_publish_media_finish(media1), 0); @@ -3958,7 +3971,7 @@ fn dial(config: Option<&moq_client_config>) -> i32 { config.map_or(std::ptr::null(), |c| c as *const _), 0, 0, - None, + Some(ignore_callback), std::ptr::null_mut(), ) } @@ -4022,11 +4035,11 @@ fn defaults_report_what_a_zeroed_config_dials() { let connect = expected.connect.resolve(); assert!(config.has_connect_timeout); - assert_eq!(config.connect_timeout_ms, connect.timeout.as_millis() as u64); + assert_eq!(config.connect_timeout_us, connect.timeout.as_micros() as u64); assert!(config.has_failover_delay); - assert_eq!(config.failover_delay_ms, connect.race.as_millis() as u64); + assert_eq!(config.failover_delay_us, connect.race.as_micros() as u64); assert!(config.has_resolution_delay); - assert_eq!(config.resolution_delay_ms, connect.resolution_delay.as_millis() as u64); + assert_eq!(config.resolution_delay_us, connect.resolution_delay.as_micros() as u64); assert!(config.has_backoff_initial); assert_eq!( @@ -4047,15 +4060,15 @@ fn defaults_report_what_a_zeroed_config_dials() { assert!(config.has_websocket_enabled); assert_eq!(config.websocket_enabled, websocket.enabled); assert!(config.has_websocket_delay); - assert_eq!(config.websocket_delay_ms, websocket.delay.as_millis() as u64); + assert_eq!(config.websocket_delay_us, websocket.delay.as_micros() as u64); assert!(config.has_quic_max_streams); assert_eq!(config.quic_max_streams, quic.max_streams); assert!(config.has_quic_idle_timeout); - assert_eq!(config.quic_idle_timeout_ms, quic.idle_timeout.as_millis() as u64); + assert_eq!(config.quic_idle_timeout_us, quic.idle_timeout.as_micros() as u64); assert_eq!( - config.has_quic_keep_alive.then_some(config.quic_keep_alive_ms), - quic.keep_alive.map(|d| d.as_millis() as u64) + config.has_quic_keep_alive.then_some(config.quic_keep_alive_us), + quic.keep_alive.map(|d| d.as_micros() as u64) ); // The backend-dependent knobs have no single value to report, so they come @@ -4082,7 +4095,7 @@ fn zero_with_a_flag_set_is_a_real_value() { let mut config = client_config(); config.backoff_timeout_us = 0; config.has_backoff_timeout = true; - config.quic_keep_alive_ms = 0; + config.quic_keep_alive_us = 0; config.has_quic_keep_alive = true; let explicit = parsed(&config); @@ -4205,7 +4218,7 @@ fn config_quic_and_backoff_knobs_apply() { config.quic_max_streams = 4096; config.has_quic_max_streams = true; - config.quic_idle_timeout_ms = 15_000; + config.quic_idle_timeout_us = 15_000_000; config.has_quic_idle_timeout = true; config.quic_gso = false; config.has_quic_gso = true; @@ -4230,20 +4243,6 @@ fn config_quic_and_backoff_knobs_apply() { assert_eq!(parsed.quic.qlog.as_deref(), Some(std::path::Path::new(dir))); } -/// An idle timeout outside QUIC's millisecond varint is an ordinary configuration -/// error, and later calls remain usable. -#[test] -fn dial_rejects_an_unrepresentable_idle_timeout() { - let mut config = client_config(); - config.quic_idle_timeout_ms = u64::MAX; - config.has_quic_idle_timeout = true; - - assert_eq!(dial(Some(&config)), Error::InvalidConfig(String::new()).code()); - - // A rejected dial leaves the library usable. - assert!(moq_client_defaults().has_connect_timeout); -} - /// The backend variants are feature-gated, so a hardcoded menu offers options this /// build rejects. Every name reported must be one a dial takes, same contract as /// `moq_versions`. @@ -4355,7 +4354,7 @@ fn dial_applies_the_config() { let mut config = client_config(); config.versions = versions.as_ptr(); config.versions_len = versions.len(); - config.connect_timeout_ms = 100; + config.connect_timeout_us = 100_000; config.has_connect_timeout = true; let cb = Callback::new(); @@ -4459,8 +4458,8 @@ fn bandwidth_reservations_split_the_estimate() { assert_eq!(moq_reservation_close(second), 0); assert_eq!(moq_bandwidth_close(bandwidth), 0); - assert_eq!(moq_consume_track_close(first_sub), 0); - assert_eq!(moq_consume_track_close(second_sub), 0); + assert_eq!(moq_consume_track_cancel(first_sub), 0); + assert_eq!(moq_consume_track_cancel(second_sub), 0); let _ = first_cb.recv_terminal(); let _ = second_cb.recv_terminal(); assert_eq!(moq_consume_close(consume), 0); @@ -4502,8 +4501,8 @@ fn bandwidth_handles_share_the_registry() { assert_eq!(moq_reservation_close(other), 0); assert_eq!(moq_bandwidth_close(first), 0); assert_eq!(moq_bandwidth_close(second), 0); - assert_eq!(moq_consume_track_close(first_sub), 0); - assert_eq!(moq_consume_track_close(second_sub), 0); + assert_eq!(moq_consume_track_cancel(first_sub), 0); + assert_eq!(moq_consume_track_cancel(second_sub), 0); let _ = first_cb.recv_terminal(); let _ = second_cb.recv_terminal(); assert_eq!(moq_consume_close(consume), 0); @@ -4552,7 +4551,7 @@ fn encode_video_bitrate_caps_the_reservation() { assert_eq!(grant(reservation), Some(1_000_000)); assert_eq!(moq_reservation_close(reservation), 0); - assert_eq!(moq_consume_track_close(sub), 0); + assert_eq!(moq_consume_track_cancel(sub), 0); let _ = cb.recv_terminal(); assert_eq!(moq_encode_video_finish(producer), 0); assert_eq!(moq_bandwidth_close(bandwidth), 0); diff --git a/rs/libmoq/src/video.rs b/rs/libmoq/src/video.rs index d7c87ebb43..a01feb5466 100644 --- a/rs/libmoq/src/video.rs +++ b/rs/libmoq/src/video.rs @@ -741,13 +741,12 @@ pub extern "C" fn moq_encode_video_reservation(producer: u32) -> i32 { #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_encode_video_demand( producer: u32, - on_demand: Option, + on_demand: crate::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let producer = ffi::parse_id(producer)?; - let on_demand = on_demand.ok_or(Error::InvalidPointer)?; - let on_demand = unsafe { OnStatus::new(user_data, Some(on_demand)) }; + let on_demand = unsafe { OnStatus::new(user_data, on_demand)? }; let mut state = State::lock(); let demand = state.video.demand(producer)?; state.publish.demand(demand, on_demand) @@ -861,7 +860,7 @@ pub extern "C" fn moq_encode_video_finish(producer: u32) -> i32 { /// once more with a terminal code: `0` (closed cleanly) or a negative error. /// After the terminal (`<= 0`) callback, `on_frame` is never called again and /// `user_data` is never touched again, so release `user_data` there. The terminal -/// callback fires even after [`moq_decode_video_close`]. +/// callback fires even after [`moq_decode_video_cancel`]. /// /// Starts at the newest cached group so reopening live playback skips the backlog. /// @@ -873,7 +872,7 @@ pub unsafe extern "C" fn moq_decode_video( catalog: u32, index: u32, output: *const moq_video_decoder_output, - on_frame: Option, + on_frame: crate::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { @@ -893,7 +892,7 @@ pub unsafe extern "C" fn moq_decode_video( // delivery loop still enforces it, since other backends ignore it. config.resize = size; let output = DecoderOutput { format, size }; - let on_frame = unsafe { OnStatus::new(user_data, on_frame) }; + let on_frame = unsafe { OnStatus::new(user_data, on_frame)? }; let mut state = State::lock(); let (broadcast, video_cfg, name) = state.consume.video_rendition(catalog, index as usize)?; @@ -911,7 +910,7 @@ pub unsafe extern "C" fn moq_decode_video( /// released. Frame ids already delivered are likewise not freed; release each /// with [`moq_decode_video_frame_free`]. #[unsafe(no_mangle)] -pub extern "C" fn moq_decode_video_close(consumer: u32) -> i32 { +pub extern "C" fn moq_decode_video_cancel(consumer: u32) -> i32 { ffi::enter(move || { let consumer = ffi::parse_id(consumer)?; State::lock().video.consume_close(consumer) diff --git a/rs/moq-ffi/CHANGELOG.md b/rs/moq-ffi/CHANGELOG.md index b3484a629a..6793aad31c 100644 --- a/rs/moq-ffi/CHANGELOG.md +++ b/rs/moq-ffi/CHANGELOG.md @@ -25,11 +25,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- `MoqClient::set_tls_disable_verify(bool)` is `set_tls_verify(bool)`, so the + argument's polarity matches every ergonomic wrapper. +- `MoqRequest::transport()` returns the closed `MoqTransport` enum instead of a string. - Bare-integer durations are microseconds: `max_age_us` on decoder outputs, track info, and subscriptions; `MoqBackoff` is `initial_us` / `max_us` / `timeout_us`. `MoqSession::publish()` / `consume()` match `set_publish` / `set_consume`. -- `MoqAnnounced` is `MoqAnnounceConsumer`, `MoqAnnouncement` is `MoqAnnounceUpdate` with `pattern()` instead of `path()`, `MoqBroadcastRequest::abort` is `reject`, and `MoqOriginOptions` is `MoqOriginConfig`. +- `MoqAnnounced` is `MoqAnnounceConsumer`, `MoqAnnouncement` is + `MoqAnnounceUpdate` with `prefix()` instead of `path()`; the returned covered + prefix is relative to the prefix passed to `announced`. `MoqBroadcastRequest::abort` + is `reject`, and `MoqOriginOptions` is `MoqOriginConfig`. - [**breaking**] `MoqTrackProducer::finish` and `MoqGroupProducer::finish` keep the handle open so a later `abort` can still run. Broadcast, audio, video, and JSON producers still close on finish. diff --git a/rs/moq-ffi/examples/server_smoke.py b/rs/moq-ffi/examples/server_smoke.py index db0adea734..f3782fead6 100644 --- a/rs/moq-ffi/examples/server_smoke.py +++ b/rs/moq-ffi/examples/server_smoke.py @@ -41,7 +41,7 @@ async def accept_one() -> moq.MoqSession: accept_task = asyncio.create_task(accept_one()) client = moq.MoqClient() - client.set_tls_disable_verify(True) + client.set_tls_verify(False) client.set_bind("127.0.0.1:0") client_session = await client.connect(f"https://{addr}") diff --git a/rs/moq-ffi/src/origin.rs b/rs/moq-ffi/src/origin.rs index cdfe09c0ca..7bb7372fb6 100644 --- a/rs/moq-ffi/src/origin.rs +++ b/rs/moq-ffi/src/origin.rs @@ -135,7 +135,7 @@ impl Announced { async fn next(&mut self) -> Result>, MoqError> { match self.inner.next().await { Some(update) => Ok(Some(Arc::new(MoqAnnounceUpdate { - path: update.path.to_string(), + prefix: update.path.to_string(), route: update.route.into(), active: update.kind.is_active(), }))), @@ -164,10 +164,11 @@ impl AnnouncedBroadcast { /// /// Carries no broadcast: resolve a specific path with /// `MoqOriginConsumer::request_broadcast` (after this update proves it is -/// covered). The application decides which paths name broadcasts. +/// covered). Its prefix is relative to the prefix requested from +/// `MoqOriginConsumer::announced`. The application decides which paths name broadcasts. #[derive(uniffi::Object)] pub struct MoqAnnounceUpdate { - path: String, + prefix: String, route: MoqRoute, active: bool, } @@ -298,7 +299,7 @@ impl MoqOriginProducer { #[uniffi::export] impl MoqOriginConsumer { - /// Subscribe to all route announcements under a prefix. + /// Subscribe to routes under a requested prefix; updates return covered prefixes relative to it. pub fn announced(&self, prefix: String) -> Result, MoqError> { let _guard = crate::ffi::enter(); let origin = self.inner.with_root(prefix).ok_or(MoqError::Unauthorized)?; @@ -447,9 +448,9 @@ impl MoqAnnounceConsumer { #[uniffi::export] impl MoqAnnounceUpdate { - /// The covered prefix, relative to the `announced` call's prefix. - pub fn path(&self) -> String { - self.path.clone() + /// The covered prefix, relative to the requested announcements prefix. + pub fn prefix(&self) -> String { + self.prefix.clone() } /// The route serving the prefix: its hops and costs. diff --git a/rs/moq-ffi/src/server.rs b/rs/moq-ffi/src/server.rs index 35afc0feda..d17634b6ea 100644 --- a/rs/moq-ffi/src/server.rs +++ b/rs/moq-ffi/src/server.rs @@ -42,7 +42,7 @@ impl ServerState { let publish = self.publish.clone(); let consume = self.consume.clone(); match server.accept().await { - Some(request) => Ok(Some(MoqRequest::new(request, publish, consume))), + Some(request) => Ok(Some(MoqRequest::new(request, publish, consume)?)), None => Ok(None), } } @@ -191,6 +191,54 @@ struct RequestState { consume: Option>, } +/// The network transport carrying an incoming session. +#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] +pub enum MoqTransport { + /// QUIC, either directly or through WebTransport over HTTP/3. + Quic, + /// An Iroh QUIC connection. + Iroh, + /// A WebSocket connection using qmux framing. + WebSocket, + /// A plaintext TCP connection using qmux framing. + Tcp, + /// A Unix domain socket using qmux framing. + Unix, +} + +impl TryFrom for MoqTransport { + type Error = MoqError; + + fn try_from(value: moq_tokio::server::Transport) -> Result { + Ok(match value { + moq_tokio::server::Transport::Quic => Self::Quic, + moq_tokio::server::Transport::Iroh => Self::Iroh, + moq_tokio::server::Transport::WebSocket => Self::WebSocket, + moq_tokio::server::Transport::Tcp => Self::Tcp, + moq_tokio::server::Transport::Unix => Self::Unix, + _ => return Err(MoqError::Unsupported), + }) + } +} + +#[cfg(test)] +mod transport_tests { + use super::MoqTransport; + use moq_tokio::server::Transport; + + #[test] + fn converts_supported_transports() { + assert_eq!(MoqTransport::try_from(Transport::Quic).unwrap(), MoqTransport::Quic); + assert_eq!(MoqTransport::try_from(Transport::Iroh).unwrap(), MoqTransport::Iroh); + assert_eq!( + MoqTransport::try_from(Transport::WebSocket).unwrap(), + MoqTransport::WebSocket + ); + assert_eq!(MoqTransport::try_from(Transport::Tcp).unwrap(), MoqTransport::Tcp); + assert_eq!(MoqTransport::try_from(Transport::Unix).unwrap(), MoqTransport::Unix); + } +} + /// An incoming MoQ session that can be accepted or rejected. /// /// Origin overrides are captured at [`accept`](Self::accept). Setters fail with @@ -199,7 +247,7 @@ struct RequestState { #[derive(uniffi::Object)] pub struct MoqRequest { task: Task, - transport: String, + transport: MoqTransport, url: Option, path: String, query: Option, @@ -210,12 +258,12 @@ impl MoqRequest { request: moq_tokio::server::Request, publish: Option>, consume: Option>, - ) -> Arc { - let transport = request.transport().to_string(); + ) -> Result, MoqError> { + let transport = request.transport().try_into()?; let url = request.url().map(|u| u.to_string()); let path = request.path().to_string(); let query = request.query().map(str::to_string); - Arc::new(Self { + Ok(Arc::new(Self { task: Task::new(RequestState { request: Some(request), publish, @@ -225,7 +273,7 @@ impl MoqRequest { url, path, query, - }) + })) } fn configure_origin(&self, f: impl FnOnce(&mut RequestState)) -> Result<(), MoqError> { @@ -276,9 +324,9 @@ impl MoqRequest { self.query.clone() } - /// The transport type, e.g. `"quic"`, `"iroh"`, or `"websocket"`. - pub fn transport(&self) -> String { - self.transport.clone() + /// The network transport carrying this session. + pub fn transport(&self) -> MoqTransport { + self.transport } /// Override the publish origin for this session. Falls back to the server's diff --git a/rs/moq-ffi/src/session.rs b/rs/moq-ffi/src/session.rs index 750b3871c2..8375257e5a 100644 --- a/rs/moq-ffi/src/session.rs +++ b/rs/moq-ffi/src/session.rs @@ -232,9 +232,9 @@ mod tests { #[test] fn setters_fail_after_cancel() { let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.cancel(); - assert!(matches!(client.set_tls_disable_verify(false), Err(MoqError::Cancelled))); + assert!(matches!(client.set_tls_verify(true), Err(MoqError::Cancelled))); assert!(matches!( client.set_bind("127.0.0.1:0".into()), Err(MoqError::Cancelled) @@ -436,10 +436,10 @@ impl MoqClient { }) } - /// Disable TLS certificate verification (for development only). - pub fn set_tls_disable_verify(&self, disable: bool) -> Result<(), MoqError> { + /// Enable or disable TLS certificate verification. + pub fn set_tls_verify(&self, verify: bool) -> Result<(), MoqError> { self.configure(|state| { - state.config.tls.insecure = Some(disable); + state.config.tls.insecure = Some(!verify); }) } diff --git a/rs/moq-ffi/src/test.rs b/rs/moq-ffi/src/test.rs index 84c96debfa..003800f4e3 100644 --- a/rs/moq-ffi/src/test.rs +++ b/rs/moq-ffi/src/test.rs @@ -314,7 +314,7 @@ async fn announced_route_keeps_cold_cost_on_reannounce() { .expect("timed out waiting for an announce update") .unwrap() .expect("origin ended while waiting for an announce update"); - if announcement.path() == "cold-route" && announcement.active() { + if announcement.prefix() == "cold-route" && announcement.active() { break announcement.route(); } }; @@ -1404,7 +1404,12 @@ async fn announced_broadcasts_resolve_siblings_under_the_prefix() { .expect("timed out waiting for the announcement") .unwrap() .expect("the origin should keep announcing"); - if announcement.path() == "pub" { + let prefix = announcement.prefix(); + assert!( + matches!(prefix.as_str(), "pub" | "source"), + "covered prefix should be relative to the requested a/ prefix: {prefix}" + ); + if prefix == "pub" { break await_announced(&consumer, "a/pub").await; } }; @@ -1501,7 +1506,7 @@ async fn announce_and_unannounce_toggles_discovery() { .expect("timed out waiting for an announce update") .unwrap() .expect("origin ended while waiting for an announce update"); - if announcement.path() == "live" && announcement.active() == announce { + if announcement.prefix() == "live" && announcement.active() == announce { return; } } @@ -1563,9 +1568,9 @@ async fn local_publish_consume_audio() { .unwrap() .expect("expected an announcement"); - assert_eq!(announcement.path(), "live"); + assert_eq!(announcement.prefix(), "live"); - let broadcast_consumer = await_announced(&consumer, &announcement.path()).await; + let broadcast_consumer = await_announced(&consumer, &announcement.prefix()).await; let catalog_consumer = broadcast_consumer.subscribe_catalog().await.unwrap(); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) @@ -1622,7 +1627,7 @@ async fn video_publish_consume() { .unwrap() .expect("expected announcement"); - let broadcast_consumer = await_announced(&consumer, &announcement.path()).await; + let broadcast_consumer = await_announced(&consumer, &announcement.prefix()).await; let catalog_consumer = broadcast_consumer.subscribe_catalog().await.unwrap(); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) @@ -1733,7 +1738,7 @@ async fn video_raw_publish_consume() { .unwrap() .expect("expected announcement"); - let broadcast_consumer = await_announced(&consumer, &announcement.path()).await; + let broadcast_consumer = await_announced(&consumer, &announcement.prefix()).await; let catalog_consumer = broadcast_consumer.subscribe_catalog().await.unwrap(); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await @@ -1848,7 +1853,7 @@ async fn video_raw_publish_from_many_threads() { .expect("timed out") .unwrap() .expect("expected announcement"); - let catalog_consumer = await_announced(&consumer, &announcement.path()) + let catalog_consumer = await_announced(&consumer, &announcement.prefix()) .await .subscribe_catalog() .await @@ -1946,7 +1951,7 @@ async fn multiple_frames_ordering() { .unwrap() .unwrap(); - let broadcast_consumer = await_announced(&consumer, &announcement.path()).await; + let broadcast_consumer = await_announced(&consumer, &announcement.prefix()).await; let catalog_consumer = broadcast_consumer.subscribe_catalog().await.unwrap(); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await @@ -2003,7 +2008,7 @@ async fn catalog_update_on_new_track() { .unwrap() .unwrap(); - let broadcast_consumer = await_announced(&consumer, &announcement.path()).await; + let broadcast_consumer = await_announced(&consumer, &announcement.prefix()).await; let catalog_consumer = broadcast_consumer.subscribe_catalog().await.unwrap(); let catalog1 = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) @@ -2056,8 +2061,8 @@ async fn announced_broadcast() { .unwrap() .expect("expected announcement"); - assert_eq!(announcement.path(), "test/broadcast"); - let _catalog = await_announced(&consumer, &announcement.path()) + assert_eq!(announcement.prefix(), "test/broadcast"); + let _catalog = await_announced(&consumer, &announcement.prefix()) .await .subscribe_catalog() .await @@ -2889,11 +2894,11 @@ fn without_runtime() { let announced = consumer.announced("".into()).unwrap(); let announcement = pollster::block_on(announced.next()).unwrap().unwrap(); - assert_eq!(announcement.path(), "test"); + assert_eq!(announcement.prefix(), "test"); let _bc = pollster::block_on(consumer.request_broadcast("test".into())).unwrap(); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_consume(Some(origin)).unwrap(); announced.cancel(); @@ -2939,7 +2944,7 @@ async fn server_client_roundtrip() { // Client side: connect, subscribe via a consume origin. let client_origin = MoqOriginProducer::new(MoqOriginConfig::default()); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_bind("127.0.0.1:0".into()).unwrap(); client.set_consume(Some(client_origin.clone())).unwrap(); let cs = tokio::time::timeout(TIMEOUT, client.connect(url)) @@ -2965,7 +2970,7 @@ async fn server_client_roundtrip() { .expect("timed out waiting for announcement over the wire") .unwrap() .expect("expected an announcement"); - assert_eq!(announcement.path(), "hello"); + assert_eq!(announcement.prefix(), "hello"); // Subscribe to the audio track and verify a frame round-trips. let bc = await_announced(&consumer, "hello").await; @@ -3035,7 +3040,7 @@ async fn server_client_roundtrip_auto_origin() { // No set_publish / set_consume, so this uses the auto-origin path. let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_bind("127.0.0.1:0".into()).unwrap(); let cs = tokio::time::timeout(TIMEOUT, client.connect(url)) .await @@ -3061,7 +3066,7 @@ async fn server_client_roundtrip_auto_origin() { .expect("timed out waiting for announcement over the wire") .unwrap() .expect("expected an announcement"); - assert_eq!(announcement.path(), "hello"); + assert_eq!(announcement.prefix(), "hello"); // With neither side wired, both share one origin, so a broadcast announced on this // session's publisher is discoverable through its own consumer. @@ -3177,7 +3182,7 @@ async fn request_double_respond_returns_already_responded() { }); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_bind("127.0.0.1:0".into()).unwrap(); let _session = tokio::time::timeout(TIMEOUT, client.connect(url)) .await @@ -3220,7 +3225,7 @@ async fn request_per_session_publish_override() { let client_origin = MoqOriginProducer::new(MoqOriginConfig::default()); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_bind("127.0.0.1:0".into()).unwrap(); client.set_consume(Some(client_origin.clone())).unwrap(); let cs = tokio::time::timeout(TIMEOUT, client.connect(url)) @@ -3243,7 +3248,7 @@ async fn request_per_session_publish_override() { .expect("timed out waiting for override announcement") .unwrap() .expect("expected an announcement"); - assert_eq!(announcement.path(), "override-only"); + assert_eq!(announcement.prefix(), "override-only"); broadcast.finish().unwrap(); cs.cancel(0); @@ -3298,7 +3303,7 @@ async fn client_reconnects_and_resumes_announcements() { let client_origin = MoqOriginProducer::new(MoqOriginConfig::default()); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_bind("127.0.0.1:0".into()).unwrap(); client.set_consume(Some(client_origin.clone())).unwrap(); // Fast retries so the test doesn't wait out the default 1s backoff. @@ -3371,7 +3376,7 @@ async fn client_reconnects_and_resumes_announcements() { .expect("timed out waiting for the post-reconnect announcement") .unwrap() .expect("expected an announcement"); - assert_eq!(announcement.path(), "after-reconnect"); + assert_eq!(announcement.prefix(), "after-reconnect"); broadcast.finish().unwrap(); cs.cancel(0); @@ -3404,7 +3409,7 @@ async fn one_shot_client_close_surfaces_through_closed() { }); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_bind("127.0.0.1:0".into()).unwrap(); client.set_reconnect(false).unwrap(); @@ -3454,7 +3459,7 @@ async fn rejected_session_surfaces_through_closed() { }); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_bind("127.0.0.1:0".into()).unwrap(); client.set_reconnect(false).unwrap(); @@ -3480,7 +3485,7 @@ async fn rejected_session_surfaces_through_closed() { #[tokio::test] async fn cancel_before_connect_fails_fast() { let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.cancel(); let result = tokio::time::timeout( Duration::from_secs(5), @@ -3525,7 +3530,7 @@ async fn cancelled_status_does_not_swallow_the_next_transition() { }); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_bind("127.0.0.1:0".into()).unwrap(); client .set_backoff(MoqBackoff { @@ -3738,7 +3743,7 @@ async fn one_shot_peers() -> (Arc, Arc, Arc) }); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_bind("127.0.0.1:0".into()).unwrap(); client.set_reconnect(false).unwrap(); let client_session = tokio::time::timeout(TIMEOUT, client.connect(url)) @@ -3794,7 +3799,7 @@ async fn client_setters_busy_during_connect_and_cancelled_after() { let addr = server.listen().await.expect("listen failed"); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_bind("127.0.0.1:0".into()).unwrap(); client.set_reconnect(false).unwrap(); @@ -3803,18 +3808,14 @@ async fn client_setters_busy_during_connect_and_cancelled_after() { let connect = tokio::spawn(async move { connecting.connect(format!("https://{addr}")).await }); assert!(matches!( - wait_for_config_error( - || client.set_tls_disable_verify(true), - |err| matches!(err, MoqError::Busy) - ) - .await, + wait_for_config_error(|| client.set_tls_verify(false), |err| matches!(err, MoqError::Busy)).await, MoqError::Busy )); assert!(matches!(client.set_publish(None), Err(MoqError::Busy))); assert!(matches!(client.set_bind("127.0.0.1:0".into()), Err(MoqError::Busy))); client.cancel(); - assert!(matches!(client.set_tls_disable_verify(false), Err(MoqError::Cancelled))); + assert!(matches!(client.set_tls_verify(true), Err(MoqError::Cancelled))); assert!(matches!(client.set_publish(None), Err(MoqError::Cancelled))); let connect_err = tokio::time::timeout(TIMEOUT, connect) @@ -3843,7 +3844,7 @@ async fn client_setters_apply_after_connect_returns() { }); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_bind("127.0.0.1:0".into()).unwrap(); let session = tokio::time::timeout(TIMEOUT, client.connect(format!("https://{addr}"))) .await @@ -3921,7 +3922,7 @@ async fn request_origin_setters_apply_or_error() { let addr = server.listen().await.expect("listen failed"); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_bind("127.0.0.1:0".into()).unwrap(); client.set_reconnect(false).unwrap(); @@ -3988,7 +3989,7 @@ async fn request_origin_setters_cancelled_after_cancel() { let addr = server.listen().await.expect("listen failed"); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_bind("127.0.0.1:0".into()).unwrap(); client.set_reconnect(false).unwrap(); @@ -4044,7 +4045,7 @@ async fn shutdown_cancels_and_drops_cleanly() { let client_origin = MoqOriginProducer::new(MoqOriginConfig::default()); let client = MoqClient::new(); - client.set_tls_disable_verify(true).unwrap(); + client.set_tls_verify(false).unwrap(); client.set_consume(Some(client_origin.clone())).unwrap(); let session = tokio::time::timeout(TIMEOUT, client.connect(format!("https://{addr}"))) .await diff --git a/swift/README.md b/swift/README.md index 186c6aea74..0e5673d996 100644 --- a/swift/README.md +++ b/swift/README.md @@ -35,7 +35,8 @@ let session = try await client.connect(to: "https://relay.example.com") // origin between both sides. let announced = try session.consume.announced(prefix: "demos/") for try await announcement in announced { - print("got broadcast \(announcement.path)") + // The returned covered prefix is relative to the requested "demos/" prefix. + print("got broadcast \(announcement.prefix)") let catalog = try announcement.broadcast.subscribeCatalog() for try await update in catalog { diff --git a/swift/Sources/Moq/Aliases.swift b/swift/Sources/Moq/Aliases.swift index d20ac34e03..72ae8cb5ba 100644 --- a/swift/Sources/Moq/Aliases.swift +++ b/swift/Sources/Moq/Aliases.swift @@ -95,6 +95,9 @@ public typealias Backoff = MoqFFI.MoqBackoff /// A connection lifecycle transition reported by `Session.status()`. public typealias ConnectionStatus = MoqFFI.MoqConnectionStatus +/// The network transport carrying an incoming session. +public typealias Transport = MoqFFI.MoqTransport + /// The error thrown by every throwing call in this package. Already conforms to /// `Swift.Error` and `LocalizedError`; see `Errors.swift` for conveniences. public typealias MoqError = MoqFFI.MoqError diff --git a/swift/Sources/Moq/Client.swift b/swift/Sources/Moq/Client.swift index cd2613f500..1ea2a70366 100644 --- a/swift/Sources/Moq/Client.swift +++ b/swift/Sources/Moq/Client.swift @@ -16,7 +16,7 @@ public final class Client: Sendable { /// Toggle TLS certificate verification. Defaults to on; pass `false` only /// against a relay with a self-signed certificate during development. public func setTlsVerify(_ verify: Bool) throws { - try ffi.setTlsDisableVerify(disable: !verify) + try ffi.setTlsVerify(verify: verify) } /// Trust these PEM root certificate file path(s) instead of the system roots. diff --git a/swift/Sources/Moq/Origin.swift b/swift/Sources/Moq/Origin.swift index 755b6dd851..9cad1a453b 100644 --- a/swift/Sources/Moq/Origin.swift +++ b/swift/Sources/Moq/Origin.swift @@ -112,7 +112,7 @@ public final class OriginConsumer: Sendable { self.ffi = ffi } - /// Stream every route announced under a prefix. + /// Stream routes under the requested prefix; each update returns a relative covered prefix. public func announced(prefix: String) throws -> AnnounceConsumer { AnnounceConsumer(try ffi.announced(prefix: prefix)) } @@ -165,7 +165,7 @@ public final class AnnounceConsumer: AsyncSequence, Sendable { /// A single route announcement or retraction. /// -/// A route claims that `path` and every path beneath it can be served; it +/// A route claims that `prefix` and every path beneath it can be served; it /// carries no broadcast. Resolve a specific path with `OriginConsumer.requestBroadcast`. /// By convention a publisher announces each broadcast's exact path. public final class AnnounceUpdate: Sendable { @@ -175,13 +175,13 @@ public final class AnnounceUpdate: Sendable { self.ffi = ffi } - /// The prefix the route covers, relative to the `announced` prefix. - public var path: String { - ffi.path() + /// The covered prefix, relative to the requested announcements prefix. + public var prefix: String { + ffi.prefix() } /// Whether the route is active (`true`) or was retracted (`false`). A - /// repeated active announcement for the same path is a metadata update. + /// repeated active announcement for the same prefix is a metadata update. public var active: Bool { ffi.active() } diff --git a/swift/Sources/Moq/Server.swift b/swift/Sources/Moq/Server.swift index 7fe1b148c9..0d136b1df7 100644 --- a/swift/Sources/Moq/Server.swift +++ b/swift/Sources/Moq/Server.swift @@ -94,8 +94,8 @@ public final class Request: Sendable { ffi.query() } - /// The transport type, e.g. `"quic"`, `"iroh"`, or `"websocket"`. - public var transport: String { + /// The network transport carrying this session. + public var transport: Transport { ffi.transport() } diff --git a/swift/Tests/MoqTests/SmokeTests.swift b/swift/Tests/MoqTests/SmokeTests.swift index 2de32f2541..8670f9e9cc 100644 --- a/swift/Tests/MoqTests/SmokeTests.swift +++ b/swift/Tests/MoqTests/SmokeTests.swift @@ -63,12 +63,12 @@ final class SmokeTests: XCTestCase { let announced = try origin.consume().announced(prefix: "") let first = try await announced.next() - XCTAssertEqual(first?.path, "live") + XCTAssertEqual(first?.prefix, "live") XCTAssertEqual(first?.active, true) try broadcast.unannounce() let retracted = try await announced.next() - XCTAssertEqual(retracted?.path, "live") + XCTAssertEqual(retracted?.prefix, "live") XCTAssertEqual(retracted?.active, false) _ = try await origin.consume().requestBroadcast(path: "live") } diff --git a/test/smoke/clients/c/subscribe.c b/test/smoke/clients/c/subscribe.c index 9f29dc5579..ebb540ce96 100644 --- a/test/smoke/clients/c/subscribe.c +++ b/test/smoke/clients/c/subscribe.c @@ -27,7 +27,7 @@ typedef struct { int32_t origin; int32_t session; int32_t broadcast_wait; - int32_t broadcast; // handle delivered by moq_origin_consume_announced (0 until it arrives) + int32_t broadcast; // handle delivered by moq_origin_announced_broadcast (0 until it arrives) int32_t catalog; int32_t video_track; // handle from moq_consume_video (0 until on_catalog starts it) @@ -42,7 +42,7 @@ typedef struct { // on main's stack and libmoq keeps the pointer until each registration's // terminal (<= 0) callback fires, so main must not return until every one of // them has. Closing the session ends its status registration alone; -// moq_origin_consume_announced, moq_consume_catalog and moq_consume_video each +// moq_origin_announced_broadcast, moq_consume_catalog and moq_consume_video each // keep the pointer until their own terminal. See drain() at the bottom. static void done(ctx_t *c, int *flag) { pthread_mutex_lock(&c->mu); @@ -151,9 +151,9 @@ static void drain(ctx_t *c) { if (track <= 0) c->done_frame = 1; pthread_mutex_unlock(&c->mu); - if (track > 0) moq_consume_video_close((uint32_t)track); - moq_consume_catalog_close((uint32_t)c->catalog); - moq_origin_consume_announced_close((uint32_t)c->broadcast_wait); + if (track > 0) moq_consume_video_cancel((uint32_t)track); + moq_consume_catalog_cancel((uint32_t)c->catalog); + moq_origin_announced_broadcast_cancel((uint32_t)c->broadcast_wait); moq_session_close((uint32_t)c->session); struct timespec deadline; @@ -212,12 +212,12 @@ int main(int argc, char **argv) { deadline.tv_sec += (time_t)timeout_s; // The broadcast arrives over the network after connect, so wait for it to be - // announced. moq_origin_consume_announced resolves via on_broadcast once it's + // announced. moq_origin_announced_broadcast resolves via on_broadcast once it's // available; we block on the condvar until then (or the deadline). c.broadcast_wait = - moq_origin_consume_announced((uint32_t)c.origin, broadcast, strlen(broadcast), on_broadcast, &c); + moq_origin_announced_broadcast((uint32_t)c.origin, broadcast, strlen(broadcast), on_broadcast, &c); if (c.broadcast_wait <= 0) { - fail("error: moq_origin_consume_announced failed: %d\n", c.broadcast_wait); + fail("error: moq_origin_announced_broadcast failed: %d\n", c.broadcast_wait); } pthread_mutex_lock(&c.mu); diff --git a/test/smoke/clients/js-native/subscribe.ts b/test/smoke/clients/js-native/subscribe.ts index 9df20cb1da..72b145ddeb 100644 --- a/test/smoke/clients/js-native/subscribe.ts +++ b/test/smoke/clients/js-native/subscribe.ts @@ -41,36 +41,22 @@ if (role !== "subscribe" || !url || !broadcast || !Number.isFinite(timeoutMs) || } async function run(): Promise { - const connection = await Moq.Connection.connect(new URL(url as string)); + const origin = new Moq.Origin.Producer(); + const connection = await Moq.Connection.connect({ url: new URL(url as string), consume: origin }); + let requested: Moq.Origin.Requesting | undefined; try { const path = Moq.Path.from(broadcast as string); - - // Wait for the broadcast to be announced before subscribing. Subscribing to a - // track on a broadcast the publisher hasn't announced yet races the relay, - // which resets the catalog stream (RESET_STREAM). The Rust API folds this - // wait into consume(); the JS API leaves it to the caller. The outer timeout - // below bounds how long we wait. - // - // The scope is the subtree at `path`, so each entry's pattern is relative to the - // connection (`path` itself for the broadcast there). Any active entry means a - // matching broadcast is up, so wait for one. - const announced = connection.announced(Moq.Path.Pattern.subtree(path)); - try { - for (;;) { - const entry = await announced.next(); - if (!entry) throw new Error("connection closed before broadcast was announced"); - if (entry.active) break; - } - } finally { - announced.close(); + requested = origin.request(path, { announced: true }); + let bc = requested.active.peek(); + while (!bc) { + await requested.active.changed(); + bc = requested.active.peek(); } - const bc = connection.consume(path); - // The .hang catalog lives on the "catalog.json" track. It's a @moq/json // snapshot+delta value, reconstructed by Json.Snapshot.Consumer. A lazy publisher may // announce video in a later update, so keep reading until one has it. - const track = bc.subscribe("catalog.json", { priority: Catalog.PRIORITY.catalog }); + const track = bc.track("catalog.json").subscribe({ priority: Catalog.PRIORITY.catalog }); const catalog = new Json.Snapshot.Consumer({ track, schema: Catalog.RootSchema }); let videoTrack: string | undefined; while (!videoTrack) { @@ -80,7 +66,7 @@ async function run(): Promise { if (renditions) videoTrack = Object.keys(renditions)[0]; } - const video = bc.subscribe(videoTrack, { priority: 0 }); + const video = bc.track(videoTrack).subscribe({ priority: 0 }); let total = 0; for (;;) { const group = await video.recvGroup(); @@ -101,7 +87,9 @@ async function run(): Promise { } throw new Error("no frame data received"); } finally { + requested?.close(); connection.close(); // returns void, not a promise + origin.close(); } } diff --git a/test/smoke/clients/js/media.ts b/test/smoke/clients/js/media.ts index ab6ad78908..78b79b6670 100644 --- a/test/smoke/clients/js/media.ts +++ b/test/smoke/clients/js/media.ts @@ -39,7 +39,7 @@ import { waitForState, waitForWatch, } from "./harness"; -import { FAULTS, leakedPlayerStarted, SAMPLE_MS, SAMPLE_RATE } from "./src/contract"; +import { FAULTS, KEYFRAME_INTERVAL_MS, leakedPlayerStarted, SAMPLE_MS, SAMPLE_RATE } from "./src/contract"; import * as Pattern from "./src/pattern"; /** Cases beyond the mandatory capability probe, publisher readiness, and cold start. */ @@ -103,6 +103,9 @@ const MIN_RATE = 0.5; */ const MAX_SKEW_STEPS = 1; +/** The first decodable frame may start at the current GOP's keyframe, but never in older history. */ +const MAX_LATE_JOIN_LAG_FRAMES = Math.ceil((Pattern.FPS * KEYFRAME_INTERVAL_MS) / 1000); + const percentile = (values: number[], p: number) => { if (values.length === 0) return Number.NaN; const sorted = [...values].sort((a, b) => a - b); @@ -541,10 +544,15 @@ try { description: "the latecomer to present the fixture", predicate: (state) => state.frameId !== undefined && state.audioContext === "running", }); + // The fixture sample names the frame painted immediately before the page opens. The first + // decodable frame can be the keyframe at the start of the current GOP, so require it to be + // within that GOP rather than requiring an impossible zero-frame capture/encode delay. + const lag = live.frameId - (joined.frameId ?? 0); check( - (joined.frameId ?? 0) >= live.frameId, + lag <= MAX_LATE_JOIN_LAG_FRAMES, "late join starts live", - () => `joined at frame ${joined.frameId}, behind the ${live.frameId} already published when it opened`, + () => + `joined at frame ${joined.frameId}, ${lag} frames behind the ${live.frameId} already published when it opened (one GOP is ${MAX_LATE_JOIN_LAG_FRAMES})`, ); console.error(` joined at frame ${joined.frameId}, live edge was ${live.frameId}`); assertMedia(await collect(player, playerErrors, WINDOW_MS), "late join"); diff --git a/test/smoke/clients/js/src/contract.ts b/test/smoke/clients/js/src/contract.ts index 6392657fb9..8c443cf0cb 100644 --- a/test/smoke/clients/js/src/contract.ts +++ b/test/smoke/clients/js/src/contract.ts @@ -46,6 +46,9 @@ export type FixtureState = { /** Rate the tone is generated and captured at. Stated rather than probed, so the catalog is fixed. */ export const SAMPLE_RATE = 48000; +/** Maximum distance between video keyframes in the deterministic fixture. */ +export const KEYFRAME_INTERVAL_MS = 500; + // ── the subscriber's measurements ─────────────────────────────────────────── /** How often the page takes a sample. Fast enough to see a 200ms tone step, cheap enough to sustain. */ diff --git a/test/smoke/clients/js/src/fixture.ts b/test/smoke/clients/js/src/fixture.ts index 0e806afa30..7990b2760a 100644 --- a/test/smoke/clients/js/src/fixture.ts +++ b/test/smoke/clients/js/src/fixture.ts @@ -17,14 +17,14 @@ import { Time } from "@moq/net"; import * as Publish from "@moq/publish"; import { Effect, Signal } from "@moq/signals"; import type { Fault, FixtureState } from "./contract"; -import { OFFSET_STEPS, SAMPLE_RATE } from "./contract"; +import { KEYFRAME_INTERVAL_MS, OFFSET_STEPS, SAMPLE_RATE } from "./contract"; import * as Pattern from "./pattern"; /** Cap the encoder rather than letting it track a bandwidth estimate, so runs are comparable. */ const MAX_BITRATE = 1_000_000; /** Short GOP so a late subscriber tunes in quickly and a rejoin is not dominated by keyframe wait. */ -const KEYFRAME_INTERVAL = Time.Milli.fromSecond(0.5 as Time.Second); +const KEYFRAME_INTERVAL = Time.Milli(KEYFRAME_INTERVAL_MS); /** How far ahead the tone table is scheduled on the audio clock. */ const SCHEDULE_AHEAD = 2; // seconds