diff --git a/cpp/moq/include/moq/moq.hpp b/cpp/moq/include/moq/moq.hpp index f9fe75c22ee..3ce636c350d 100644 --- a/cpp/moq/include/moq/moq.hpp +++ b/cpp/moq/include/moq/moq.hpp @@ -56,10 +56,7 @@ using BroadcastConsumer = MoqBroadcastConsumer; using BroadcastDynamic = MoqBroadcastDynamic; using BroadcastProducer = MoqBroadcastProducer; using BroadcastRequest = MoqBroadcastRequest; -using CatalogConsumer = MoqCatalogConsumer; using Client = MoqClient; -using ContainerProducer = MoqContainerProducer; -using ContainerStreamProducer = MoqContainerStreamProducer; using FlateSnapshotProducer = MoqFlateSnapshotProducer; using FlateStreamProducer = MoqFlateStreamProducer; using GroupConsumer = MoqGroupConsumer; @@ -69,10 +66,14 @@ using JsonSnapshotConsumer = MoqJsonSnapshotConsumer; using JsonSnapshotProducer = MoqJsonSnapshotProducer; using JsonStreamConsumer = MoqJsonStreamConsumer; using JsonStreamProducer = MoqJsonStreamProducer; -using MediaConsumer = MoqMediaConsumer; -using MediaGroupConsumer = MoqMediaGroupConsumer; -using MediaProducer = MoqMediaProducer; -using MediaStreamProducer = MoqMediaStreamProducer; +using MediaCatalogConsumer = MoqMediaCatalogConsumer; +using MediaCatalogProducer = MoqMediaCatalogProducer; +using MediaContainerConsumer = MoqMediaContainerConsumer; +using MediaContainerGroupConsumer = MoqMediaContainerGroupConsumer; +using MediaContainerProducer = MoqMediaContainerProducer; +using MediaContainerStreamProducer = MoqMediaContainerStreamProducer; +using MediaTrackProducer = MoqMediaTrackProducer; +using MediaTrackStreamProducer = MoqMediaTrackStreamProducer; using OriginConsumer = MoqOriginConsumer; using OriginDynamic = MoqOriginDynamic; using OriginProducer = MoqOriginProducer; @@ -97,6 +98,8 @@ using AudioFrame = MoqAudioFrame; using AudioInit = MoqAudioInit; using Backoff = MoqBackoff; using Catalog = MoqCatalog; +using ClientConfig = MoqClientConfig; +using ClientTls = MoqClientTls; using ConnectionStats = MoqConnectionStats; using ContainerInit = MoqContainerInit; using Datagram = MoqDatagram; @@ -106,10 +109,15 @@ using FlateConfig = MoqFlateConfig; using Frame = MoqFrame; using JsonSnapshotConfig = MoqJsonSnapshotConfig; using JsonStreamConfig = MoqJsonStreamConfig; +using MediaContainerConfig = MoqMediaContainerConfig; +using MediaContainerGroupConfig = MoqMediaContainerGroupConfig; using MediaFrame = MoqMediaFrame; using OriginConfig = MoqOriginConfig; using ProtocolError = MoqProtocolError; +using QuicConfig = MoqQuicConfig; using Route = MoqRoute; +using ServerConfig = MoqServerConfig; +using ServerTls = MoqServerTls; using Subscription = MoqSubscription; using TrackInfo = MoqTrackInfo; using Video = MoqVideo; @@ -121,6 +129,7 @@ using VideoFrame = MoqVideoFrame; using VideoHint = MoqVideoHint; using VideoInit = MoqVideoInit; using VideoProperties = MoqVideoProperties; +using WebSocketConfig = MoqWebSocketConfig; using AnnounceEvent = MoqAnnounceEvent; using AudioFormat = MoqAudioFormat; using AudioSampleFormat = MoqAudioSampleFormat; @@ -129,6 +138,7 @@ using Container = MoqContainer; using ContainerFormat = MoqContainerFormat; using Error = MoqError; using ErrorScope = MoqErrorScope; +using MediaTarget = MoqMediaTarget; using ProtocolKind = MoqProtocolKind; using Transport = MoqTransport; using VideoCodec = MoqVideoCodec; diff --git a/cpp/moq/test/doc-samples.hpp b/cpp/moq/test/doc-samples.hpp index 4e691e91ef4..0dea9f1f5c2 100644 --- a/cpp/moq/test/doc-samples.hpp +++ b/cpp/moq/test/doc-samples.hpp @@ -28,7 +28,7 @@ inline void ok(moq::expected result) { } inline std::shared_ptr session; -inline std::shared_ptr media; +inline std::shared_ptr media; inline std::vector opus_init; inline std::vector packet; inline std::vector rgba; diff --git a/cpp/moq/test/probe.cpp b/cpp/moq/test/probe.cpp index 012274c03e4..6611c2197c4 100644 --- a/cpp/moq/test/probe.cpp +++ b/cpp/moq/test/probe.cpp @@ -100,7 +100,7 @@ Coroutine read_one(std::shared_ptr consumer, std::optionalcert_fingerprints(); CHECK(!fingerprints); CHECK(std::holds_alternative(fingerprints.error().get_variant())); @@ -113,21 +113,23 @@ int main() { auto track = ok(broadcast->publish_track("data", std::nullopt), "publish_track"); ok(broadcast->announce({}), "announce"); - auto server = moq::Server::init(); - ok(server->set_bind("127.0.0.1:0"), "set_bind"); - ok(server->set_tls_generate({"localhost"}), "set_tls_generate"); - ok(server->set_publish(origin), "set_publish"); + moq::ServerConfig server_config; + server_config.bind = "127.0.0.1:0"; + server_config.tls.generate = {"localhost"}; + server_config.publish = origin; + auto server = ok(moq::Server::init(server_config), "server init"); auto addr = ok(server->listen().get(), "listen"); // Both halves of the handshake are futures, so they run concurrently while this // thread blocks on one at a time. - auto client = moq::Client::init(); - ok(client->set_tls_verify(false), "set_tls_verify"); + moq::ClientConfig client_config; + client_config.tls.insecure = true; + auto client = ok(moq::Client::init(client_config), "client init"); auto accepting = server->accept(); auto connecting = client->connect("https://" + addr); auto request = ok(accepting.get(), "accept"); CHECK(request != nullptr); - auto served = ok(request->accept().get(), "request accept"); + auto served = ok(request->accept(nullptr, nullptr).get(), "request accept"); auto session = ok(connecting.get(), "connect"); // Subscriber: resolve the announced broadcast and subscribe to its track. @@ -205,7 +207,7 @@ int main() { auto ended = consumer->read_frame().get(); CHECK(ended && !ended->has_value()); - auto closed = moq::Client::init(); + auto closed = ok(moq::Client::init({}), "client init"); closed->cancel(); auto refused = closed->connect("https://" + addr).get(); CHECK(!refused); diff --git a/cpp/obs/src/moq-output.cpp b/cpp/obs/src/moq-output.cpp index 855b825038c..62f18c09be3 100644 --- a/cpp/obs/src/moq-output.cpp +++ b/cpp/obs/src/moq-output.cpp @@ -161,24 +161,6 @@ bool MoQOutput::Start() return false; } - auto next = std::make_shared(); - next->client = moq::Client::init(); - next->url = server; - - // Advanced settings live on the service alongside the URL and path. With the group - // switched off the client keeps the library defaults. - OBSDataAutoRelease service_settings = obs_service_get_settings(service); - std::string invalid; - if (!MoQSettings::Configure(service_settings, *next->client, &invalid)) { - // Refusing to start beats connecting with a setting the user asked for - // quietly dropped. - LOG_ERROR("Invalid advanced MoQ settings: %s", invalid.c_str()); - std::lock_guard signal_lock(signal_mutex); - obs_output_set_last_error(output, ("Invalid advanced MoQ settings: " + invalid).c_str()); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); - return false; - } - // Create the broadcast on an origin of our own, then announce it so subscribers // can discover it. Stop() finishes it, so each Start creates a fresh one. LOG_INFO("Publishing broadcast: %s", path.c_str()); @@ -192,11 +174,29 @@ bool MoQOutput::Start() LOG_ERROR("Failed to announce broadcast: %s", announced.error().to_string().c_str()); return false; } - if (auto wired = next->client->set_publish(next_origin); !wired) { - LOG_ERROR("Failed to publish through the client: %s", wired.error().to_string().c_str()); + + // Advanced settings live on the service alongside the URL and path. With the group + // switched off the client keeps the library defaults. + moq::ClientConfig config; + config.publish = next_origin; + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + MoQSettings::Configure(service_settings, config); + auto client = moq::Client::init(config); + if (!client) { + // Refusing to start beats connecting with a setting the user asked for + // quietly dropped. + std::string invalid = client.error().to_string(); + LOG_ERROR("Invalid advanced MoQ settings: %s", invalid.c_str()); + std::lock_guard signal_lock(signal_mutex); + obs_output_set_last_error(output, ("Invalid advanced MoQ settings: " + invalid).c_str()); + obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); return false; } + auto next = std::make_shared(); + next->client = *client; + next->url = server; + LOG_INFO("Connecting to MoQ server: %s", MoQRedactUrl(server).c_str()); // Held from the connect through obs_output_begin_data_capture. Every @@ -654,7 +654,7 @@ void MoQOutput::VideoInit(obs_encoder_t *encoder) hint.optimize_for_latency = true; config.hint = hint; - auto track = broadcast->publish_video(config); + auto track = moq::MediaTrackProducer::video(broadcast, moq::MediaTarget::kNamed{}, config); if (!track) { LOG_ERROR("Failed to initialize video track: %s", track.error().to_string().c_str()); video_tracks[encoder] = nullptr; @@ -715,7 +715,7 @@ void MoQOutput::AudioInit(obs_encoder_t *encoder) if (extra_data && extra_size > 0) config.data.assign(extra_data, extra_data + extra_size); - auto track = broadcast->publish_audio(config); + auto track = moq::MediaTrackProducer::audio(broadcast, moq::MediaTarget::kNamed{}, config); if (!track) { LOG_ERROR("Failed to initialize audio track: %s", track.error().to_string().c_str()); audio_tracks[encoder] = nullptr; diff --git a/cpp/obs/src/moq-output.h b/cpp/obs/src/moq-output.h index 0509cc7902b..573ab8ff5a9 100644 --- a/cpp/obs/src/moq-output.h +++ b/cpp/obs/src/moq-output.h @@ -141,8 +141,8 @@ class MoQOutput { std::shared_ptr origin; std::shared_ptr broadcast; // An encoder maps to null when its track failed to initialize, so it isn't retried. - std::map> video_tracks; - std::map> audio_tracks; + std::map> video_tracks; + std::map> audio_tracks; // Retired sessions still draining their finished tracks, which the destructor // waits out. Guarded by signal_mutex. diff --git a/cpp/obs/src/moq-settings.cpp b/cpp/obs/src/moq-settings.cpp index 495b90a4524..4713bc61c1a 100644 --- a/cpp/obs/src/moq-settings.cpp +++ b/cpp/obs/src/moq-settings.cpp @@ -28,8 +28,11 @@ constexpr const char *QUIC_MAX_STREAMS = "quic_max_streams"; constexpr const char *WEBSOCKET_ENABLED = "websocket_enabled"; constexpr const char *WEBSOCKET_DELAY = "websocket_delay_ms"; -// moq-ffi documents these defaults on the MoqClient setters but does not report them, -// so they are repeated here. The backoff defaults come from moq::Backoff itself. +// moq-ffi documents these defaults on MoqClientConfig but leaves each field null rather +// than reporting it, so they are repeated here. +constexpr long long BACKOFF_INITIAL_DEFAULT_MS = 1000; +constexpr long long BACKOFF_MAX_DEFAULT_MS = 5000; +constexpr long long BACKOFF_TIMEOUT_DEFAULT_MS = 10000; constexpr long long QUIC_MAX_STREAMS_DEFAULT = 1024; constexpr bool WEBSOCKET_ENABLED_DEFAULT = true; constexpr long long WEBSOCKET_DELAY_DEFAULT_MS = 200; @@ -78,17 +81,11 @@ Field File(const char *key, const char *label, const char *filter, const char *t return Field{key, label, tooltip, Kind::File, 0, 0, 0, false, 0, "", {}, false, filter}; } -long long Millis(uint64_t us) -{ - return static_cast(us / 1000); -} - } // namespace const std::vector &Fields() { static const std::vector fields = [] { - const moq::Backoff backoff{}; std::vector f; f.push_back(Text(BIND, "Bind address", @@ -109,11 +106,11 @@ const std::vector &Fields() "Trust this CA instead of the system roots.")); // Reconnect. - f.push_back(Number(BACKOFF_INITIAL, "Reconnect delay (ms)", Millis(backoff.initial_us), 1, 60000, 100, + f.push_back(Number(BACKOFF_INITIAL, "Reconnect delay (ms)", BACKOFF_INITIAL_DEFAULT_MS, 1, 60000, 100, "Delay before the first reconnect attempt; it grows from here.")); - f.push_back(Number(BACKOFF_MAX, "Reconnect delay cap (ms)", Millis(backoff.max_us), 1, 600000, 1000, + f.push_back(Number(BACKOFF_MAX, "Reconnect delay cap (ms)", BACKOFF_MAX_DEFAULT_MS, 1, 600000, 1000, "Ceiling on the growing reconnect delay.")); - f.push_back(Number(BACKOFF_TIMEOUT, "Give up after (ms)", Millis(backoff.timeout_us), 0, 3600000, 1000, + f.push_back(Number(BACKOFF_TIMEOUT, "Give up after (ms)", BACKOFF_TIMEOUT_DEFAULT_MS, 0, 3600000, 1000, "Total time to keep retrying before the stream fails. Also how long the " "broadcast lingers for viewers across the gap. 0 retries forever.")); @@ -199,52 +196,29 @@ void AddProperties(obs_properties_t *props) obs_properties_add_group(props, ENABLED, "Advanced", OBS_GROUP_CHECKABLE, group); } -bool Configure(obs_data_t *settings, moq::Client &client, std::string *error) +void Configure(obs_data_t *settings, moq::ClientConfig &config) { if (!settings || !obs_data_get_bool(settings, ENABLED)) - return true; // advanced off: the client keeps the library defaults - - // Stops at the first setter moq-ffi rejects, naming the setting it came from. - auto apply = [&](const char *key, moq::expected result) { - if (!result && error) - *error = std::string(key) + ": " + result.error().to_string(); - return result.has_value(); - }; - - if (const char *bind = OptionalString(settings, BIND)) { - if (!apply(BIND, client.set_bind(bind))) - return false; - } + return; // advanced off: the client keeps the library defaults - if (obs_data_get_bool(settings, TLS_DISABLE_VERIFY)) { - if (!apply(TLS_DISABLE_VERIFY, client.set_tls_verify(false))) - return false; - } - if (const char *fingerprint = OptionalString(settings, TLS_FINGERPRINT)) { - if (!apply(TLS_FINGERPRINT, client.set_tls_fingerprints({fingerprint}))) - return false; - } - if (const char *root = OptionalString(settings, TLS_ROOT)) { - if (!apply(TLS_ROOT, client.set_tls_roots({root}))) - return false; - } + if (const char *bind = OptionalString(settings, BIND)) + config.bind = bind; - moq::Backoff backoff{}; - backoff.initial_us = Amount(settings, BACKOFF_INITIAL) * 1000; - backoff.max_us = Amount(settings, BACKOFF_MAX) * 1000; - backoff.timeout_us = Amount(settings, BACKOFF_TIMEOUT) * 1000; - if (!apply(BACKOFF_INITIAL, client.set_backoff(backoff))) - return false; + if (obs_data_get_bool(settings, TLS_DISABLE_VERIFY)) + config.tls.insecure = true; + if (const char *fingerprint = OptionalString(settings, TLS_FINGERPRINT)) + config.tls.fingerprints = {fingerprint}; + if (const char *root = OptionalString(settings, TLS_ROOT)) + config.tls.roots = {root}; - if (!apply(QUIC_MAX_STREAMS, client.set_quic_max_streams(Amount(settings, QUIC_MAX_STREAMS)))) - return false; + config.backoff.initial_us = Amount(settings, BACKOFF_INITIAL) * 1000; + config.backoff.max_us = Amount(settings, BACKOFF_MAX) * 1000; + config.backoff.timeout_us = Amount(settings, BACKOFF_TIMEOUT) * 1000; - if (!apply(WEBSOCKET_ENABLED, client.set_websocket_enabled(obs_data_get_bool(settings, WEBSOCKET_ENABLED)))) - return false; - if (!apply(WEBSOCKET_DELAY, client.set_websocket_delay(Amount(settings, WEBSOCKET_DELAY) * 1000))) - return false; + config.quic.max_streams = Amount(settings, QUIC_MAX_STREAMS); - return true; + config.websocket.enabled = obs_data_get_bool(settings, WEBSOCKET_ENABLED); + config.websocket.delay_us = Amount(settings, WEBSOCKET_DELAY) * 1000; } } // namespace MoQSettings diff --git a/cpp/obs/src/moq-settings.h b/cpp/obs/src/moq-settings.h index 9bcff5bd705..34e640b1dd3 100644 --- a/cpp/obs/src/moq-settings.h +++ b/cpp/obs/src/moq-settings.h @@ -12,7 +12,7 @@ // One set of keys on an obs_data_t backs every surface that edits them: the service // properties page, the dock's advanced dialog, and the output that reads them at connect // time. The fields are described once in Fields() so both UIs are generated from the -// same list; adding a knob means adding a Field entry and one setter call in Configure. +// same list; adding a knob means adding a Field entry and one config field in Configure. namespace MoQSettings { // Whether any of this applies. With it off the output dials with the library defaults and @@ -77,11 +77,12 @@ void Defaults(obs_data_t *settings); // Add the checkable "Advanced" group to a properties list. void AddProperties(obs_properties_t *props); -// Apply these settings to a client before it connects. +// Fill a client's config from these settings before the client is built. // -// Returns false with the offending setting and moq-ffi's reason in `error` when a value -// is rejected, such as a bind address that doesn't parse; the caller should refuse to -// start rather than dial with a setting the user asked for silently dropped. -bool Configure(obs_data_t *settings, moq::Client &client, std::string *error); +// moq-ffi validates the record when it builds the client: a value it rejects, such as a +// bind address that doesn't parse, fails `moq::Client::init` with a `Config` error naming +// it, and the caller should refuse to start rather than dial with a setting the user asked +// for silently dropped. +void Configure(obs_data_t *settings, moq::ClientConfig &config); } // namespace MoQSettings diff --git a/cpp/obs/src/moq-source.cpp b/cpp/obs/src/moq-source.cpp index 8ee472918e4..366c93fe611 100644 --- a/cpp/obs/src/moq-source.cpp +++ b/cpp/obs/src/moq-source.cpp @@ -161,7 +161,7 @@ struct Track { // Decodes one frame and hands it to OBS, under ctx->mutex. Decode decode; - std::shared_ptr consumer; + std::shared_ptr consumer; std::optional call; }; @@ -176,7 +176,7 @@ struct Connection { std::shared_ptr session; std::shared_ptr announced; std::shared_ptr consumer; - std::shared_ptr catalog; + std::shared_ptr catalog; // The connect, then the session's status transitions. std::optional session_call; @@ -272,7 +272,7 @@ static void moq_source_on_connect(struct moq_source *ctx, const std::shared_ptr< static void moq_source_on_broadcast(struct moq_source *ctx, const std::shared_ptr &conn, moq::expected> result); static void moq_source_on_catalogs(struct moq_source *ctx, const std::shared_ptr &conn, - moq::expected> result); + moq::expected> result); static void moq_source_watch_status(struct moq_source *ctx, const std::shared_ptr &conn); static void moq_source_on_status(struct moq_source *ctx, const std::shared_ptr &conn, moq::expected result); @@ -423,7 +423,13 @@ static void moq_source_reconnect(struct moq_source *ctx) conn->broadcast = ctx->broadcast; // The source dials with the defaults. Neither origin side is wired, so the // session's consume side carries the remote's announcements. - conn->client = moq::Client::init(); + auto client = moq::Client::init(moq::ClientConfig{}); + if (!client) { + // Already disconnected and blanked above. + LOG_ERROR("Failed to create MoQ client: %s", client.error().to_string().c_str()); + return; + } + conn->client = *client; ctx->connection = conn; LOG_INFO("Connecting to MoQ server: %s", MoQRedactUrl(conn->url).c_str()); @@ -475,13 +481,15 @@ static void moq_source_on_broadcast(struct moq_source *ctx, const std::shared_pt } conn->consumer = *result; - conn->broadcast_call = conn->consumer->subscribe_catalog().then( - ctx->worker.Executor(), moq_source_current>>( - ctx, conn, moq_source_on_catalogs)); + conn->broadcast_call = + moq::MediaCatalogConsumer::subscribe(conn->consumer) + .then(ctx->worker.Executor(), + moq_source_current>>( + ctx, conn, moq_source_on_catalogs)); } static void moq_source_on_catalogs(struct moq_source *ctx, const std::shared_ptr &conn, - moq::expected> result) + moq::expected> result) { if (!result) { LOG_ERROR("Failed to subscribe to catalog: %s", result.error().to_string().c_str()); @@ -571,7 +579,7 @@ moq_source_first_rendition(const std::unordered_map &ren static void moq_source_on_subscribed(struct moq_source *ctx, const std::shared_ptr &conn, const std::shared_ptr &track, - moq::expected> result); + moq::expected> result); static void moq_source_read_track(struct moq_source *ctx, const std::shared_ptr &conn, const std::shared_ptr &track); static void moq_source_on_frame(struct moq_source *ctx, const std::shared_ptr &conn, @@ -610,16 +618,16 @@ static void moq_source_on_resolved(struct moq_source *ctx, const std::shared_ptr LOG_ERROR("Failed to resolve %s track broadcast: %s", track->kind, result.error().to_string().c_str()); return; } - track->call = (*result) - ->subscribe_media(track->name, track->container, std::nullopt) - .then(ctx->worker.Executor(), - moq_source_track_current>>( - ctx, conn, track, moq_source_on_subscribed)); + track->call = + moq::MediaContainerConsumer::subscribe(*result, {track->name, track->container}) + .then(ctx->worker.Executor(), + moq_source_track_current>>( + ctx, conn, track, moq_source_on_subscribed)); } static void moq_source_on_subscribed(struct moq_source *ctx, const std::shared_ptr &conn, const std::shared_ptr &track, - moq::expected> result) + moq::expected> result) { if (!result) { LOG_ERROR("Failed to subscribe to %s track: %s", track->kind, result.error().to_string().c_str()); diff --git a/cpp/obs/test/moq-output-test.cpp b/cpp/obs/test/moq-output-test.cpp index c4e9e3831d8..2929681b093 100644 --- a/cpp/obs/test/moq-output-test.cpp +++ b/cpp/obs/test/moq-output-test.cpp @@ -186,18 +186,15 @@ long long obs_data_get_int(obs_data_t *, const char *) } // extern "C" // Stands in for the advanced settings: reconnect fast, so a dropped session is -// back within a test's patience. +// back within a test's patience. An unparseable bind address is what moq-ffi refuses +// when it builds the client. namespace MoQSettings { -bool Configure(obs_data_t *, moq::Client &client, std::string *error) +void Configure(obs_data_t *, moq::ClientConfig &config) { - if (!g_settings_ok) { - *error = "bind: invalid address"; - return false; - } - moq::Backoff backoff{}; - backoff.initial_us = 10'000; - backoff.max_us = 50'000; - return client.set_backoff(backoff).has_value(); + if (!g_settings_ok) + config.bind = "not an address"; + config.backoff.initial_us = 10'000; + config.backoff.max_us = 50'000; } } // namespace MoQSettings @@ -292,7 +289,7 @@ std::optional relayCatalog(const std::shared_ptrsubscribe_catalog().get(), "subscribe_catalog"); + auto catalogs = TestOk(moq::MediaCatalogConsumer::subscribe(broadcast).get(), "catalog consumer"); for (;;) { auto next = catalogs->next(); if (next.wait_for(std::chrono::seconds(10)) != std::future_status::ready) @@ -380,8 +377,9 @@ int main() CHECK(catalog.has_value()); if (catalog) { const auto &[name, rendition] = *catalog->video.begin(); - auto media = TestOk(broadcast->subscribe_media(name, rendition.container, std::nullopt).get(), - "subscribe_media"); + auto media = TestOk( + moq::MediaContainerConsumer::subscribe(broadcast, {name, rendition.container}).get(), + "container consumer"); // Read a frame first, so the relay is subscribed before the last one is written. auto next = media->next(); CHECK(next.wait_for(std::chrono::seconds(10)) == std::future_status::ready); diff --git a/cpp/obs/test/moq-source-test.cpp b/cpp/obs/test/moq-source-test.cpp index ee1b3409957..9c9060f6d52 100644 --- a/cpp/obs/test/moq-source-test.cpp +++ b/cpp/obs/test/moq-source-test.cpp @@ -493,13 +493,17 @@ class Publisher { moq::VideoInit init{}; init.format = moq::VideoFormat::kAvc3; init.data = TestH264Init(); - this->video = TestOk(broadcast->publish_video(init), "publish_video"); + this->video = + TestOk(moq::MediaTrackProducer::video(broadcast, moq::MediaTarget::kNamed{}, init), + "video producer"); } if (audio) { moq::AudioInit init{}; init.format = moq::AudioFormat::kOpus; init.data = TestOpusHead(); - this->audio = TestOk(broadcast->publish_audio(init), "publish_audio"); + this->audio = + TestOk(moq::MediaTrackProducer::audio(broadcast, moq::MediaTarget::kNamed{}, init), + "audio producer"); } TestOk(broadcast->announce(moq::Route{}), "announce"); } @@ -531,8 +535,8 @@ class Publisher { } std::shared_ptr broadcast; - std::shared_ptr video; - std::shared_ptr audio; + std::shared_ptr video; + std::shared_ptr audio; private: uint64_t timestamp_us = 1'000'000; diff --git a/cpp/obs/test/moq-test-relay.h b/cpp/obs/test/moq-test-relay.h index 1f311b1d4ec..08287ee6dfb 100644 --- a/cpp/obs/test/moq-test-relay.h +++ b/cpp/obs/test/moq-test-relay.h @@ -71,11 +71,12 @@ class TestRelay { for (int attempt = 0; attempt < 20 && http_fd < 0; attempt++) { if (server) server->cancel(); - server = moq::Server::init(); - TestOk(server->set_bind("127.0.0.1:0"), "set_bind"); - TestOk(server->set_tls_generate({"localhost"}), "set_tls_generate"); - TestOk(server->set_publish(origin), "set_publish"); - TestOk(server->set_consume(origin), "set_consume"); + moq::ServerConfig config; + config.bind = "127.0.0.1:0"; + config.tls.generate = {"localhost"}; + config.publish = origin; + config.consume = origin; + server = TestOk(moq::Server::init(config), "server init"); addr = TestOk(server->listen().get(), "listen"); http_fd = BindFingerprint(); } @@ -142,7 +143,7 @@ class TestRelay { auto request = server->accept().get(); if (!request || !*request) return; - auto session = (*request)->accept().get(); + auto session = (*request)->accept(nullptr, nullptr).get(); if (!session) continue; std::lock_guard lock(mutex); diff --git a/cpp/obs/test/obs-moq-test.cpp b/cpp/obs/test/obs-moq-test.cpp index 7bcb2416569..a7461e53565 100644 --- a/cpp/obs/test/obs-moq-test.cpp +++ b/cpp/obs/test/obs-moq-test.cpp @@ -45,9 +45,10 @@ int main() } // A call parked on the runtime, which only a shutdown can end without a peer. - auto server = moq::Server::init(); - TestOk(server->set_bind("127.0.0.1:0"), "set_bind"); - TestOk(server->set_tls_generate({"localhost"}), "set_tls_generate"); + moq::ServerConfig config; + config.bind = "127.0.0.1:0"; + config.tls.generate = {"localhost"}; + auto server = TestOk(moq::Server::init(config), "server init"); TestOk(server->listen().get(), "listen"); auto pending = server->accept(); if (pending.wait_for(std::chrono::milliseconds(50)) != std::future_status::timeout) { diff --git a/dart/moq/README.md b/dart/moq/README.md index eab774edb82..08d2f7db99e 100644 --- a/dart/moq/README.md +++ b/dart/moq/README.md @@ -6,9 +6,10 @@ Idiomatic Dart and Flutter bindings for Media over QUIC. import 'package:moq/moq.dart'; final connection = await Moq.connect('https://relay.example.com'); -await for (final event in connection.announcements( +final announced = connection.announced( options: const AnnounceOptions(prefix: 'live/', filter: '*/camera'), -)) { +); +await for (final event in announced.updates()) { if (event is AnnounceEventStart) { // Prefix stays origin-relative; captures reports wildcard matches. print(event.announce.prefix); diff --git a/dart/moq/lib/json.dart b/dart/moq/lib/json.dart new file mode 100644 index 00000000000..539fb5f6340 --- /dev/null +++ b/dart/moq/lib/json.dart @@ -0,0 +1,37 @@ +/// JSON tracks, mirroring the `moq-json` crate. +/// +/// Each type wraps a track: a producer takes over a `TrackProducer` and +/// advertises it in the broadcast's catalog, and a consumer takes over a +/// `TrackConsumer` that has not read a group yet. Values cross as JSON +/// strings. Import it with a prefix, since `dart:convert` has a `json` too: +/// +/// ```dart +/// import 'package:moq/json.dart' as moq_json; +/// +/// final status = moq_json.SnapshotProducer( +/// broadcast: broadcast, +/// track: broadcast.publishTrack(name: 'status', info: null), +/// config: moq_json.SnapshotConfig(), +/// ); +/// ``` +library; + +import 'package:moq_ffi/moq_ffi.dart'; + +/// Publishes lossy latest-value JSON snapshots on a track it takes over. +typedef SnapshotProducer = MoqJsonSnapshotProducer; + +/// Consumes reconstructed latest-value JSON snapshots from a track it takes over. +typedef SnapshotConsumer = MoqJsonSnapshotConsumer; + +/// Configures a lossy latest-value JSON track. +typedef SnapshotConfig = MoqJsonSnapshotConfig; + +/// Publishes a lossless stream of JSON records on a track it takes over. +typedef StreamProducer = MoqJsonStreamProducer; + +/// Consumes a lossless stream of JSON records from a track it takes over. +typedef StreamConsumer = MoqJsonStreamConsumer; + +/// Configures a lossless JSON stream track. +typedef StreamConfig = MoqJsonStreamConfig; diff --git a/dart/moq/lib/media.dart b/dart/moq/lib/media.dart new file mode 100644 index 00000000000..33ecdb3953e --- /dev/null +++ b/dart/moq/lib/media.dart @@ -0,0 +1,91 @@ +/// Catalogs, media imports, and container consumers. +library; + +import 'package:moq_ffi/moq_ffi.dart'; + +/// The write side of a media track; discontinuity() marks a break between pre-framed payloads. +typedef TrackProducer = MoqMediaTrackProducer; + +/// The write side of a media track fed a raw byte stream, with frame boundaries inferred. +typedef TrackStreamProducer = MoqMediaTrackStreamProducer; + +/// The write side of a container, which publishes each track it describes. +typedef ContainerProducer = MoqMediaContainerProducer; + +/// The write side of a container fed a raw byte stream. +typedef ContainerStreamProducer = MoqMediaContainerStreamProducer; + +/// The read side of a media track: yields frames with codec metadata in decode order. +typedef ContainerConsumer = MoqMediaContainerConsumer; + +/// A finite fetched media group: yields container-decoded frames until the group ends. +typedef ContainerGroupConsumer = MoqMediaContainerGroupConsumer; + +/// The read side of a broadcast's catalog: yields updates as the set of tracks changes. +typedef CatalogConsumer = MoqMediaCatalogConsumer; + +/// A broadcast's catalog: its tracks and their properties, plus any application sections. +typedef Catalog = MoqCatalog; + +/// A media frame whose keyframe flag marks group starts or video keyframes; audio flags only group starts. +typedef MediaFrame = MoqMediaFrame; + +/// The catalog description of a video track, including whether it is enabled (a disabled one has no frames coming). +typedef Video = MoqVideo; + +/// Caller-provided catalog fields for a video track. +typedef VideoHint = MoqVideoHint; + +/// A video codec, optional init bytes, a label, and catalog hints. +typedef VideoInit = MoqVideoInit; + +/// Catalog properties shared by every video rendition; absent fields clear those properties. +typedef VideoProperties = MoqVideoProperties; + +/// A single video codec an importer can parse. +typedef VideoFormat = MoqVideoFormat; + +/// The catalog description of an audio track: codec, sample rate, channels, whether it is enabled, and container. +typedef Audio = MoqAudio; + +/// An audio codec, its required init bytes, and an optional label. +typedef AudioInit = MoqAudioInit; + +/// A single audio codec an importer can parse. +typedef AudioFormat = MoqAudioFormat; + +/// A container that publishes its own tracks. +typedef ContainerFormat = MoqContainerFormat; + +/// A container format and its leading bytes. +typedef ContainerInit = MoqContainerInit; + +/// A width and height pair, in pixels. +typedef Dimensions = MoqDimensions; + +/// A weak catalog handle that closes with its broadcast. +typedef CatalogProducer = MoqMediaCatalogProducer; + +/// A named or requested track target for an importer. +typedef Target = MoqMediaTarget; + +/// A new track with a chosen or format-derived name. +typedef Named = NamedMoqMediaTarget; + +/// A subscriber-requested track. +typedef Requested = RequestedMoqMediaTarget; + +/// A track name, container, and live delivery options. +typedef ContainerConfig = MoqMediaContainerConfig; + +/// A fetched group name, sequence, container, and delivery options. +typedef ContainerGroupConfig = MoqMediaContainerGroupConfig; + +/// The packaging advertised for a media track. +typedef Container = MoqContainer; + +/// Duration view over a media frame's presentation time. +extension MediaFrameDuration on MoqMediaFrame { + /// Presentation timestamp. + Duration get timestamp => Duration(microseconds: timestampUs); +} diff --git a/dart/moq/lib/src/aliases.dart b/dart/moq/lib/src/aliases.dart index d5884af22e3..18ad02441bd 100644 --- a/dart/moq/lib/src/aliases.dart +++ b/dart/moq/lib/src/aliases.dart @@ -98,90 +98,12 @@ typedef GroupProducer = MoqGroupProducer; /// The read side of a single group: yields timestamped raw frames. typedef GroupConsumer = MoqGroupConsumer; -/// The write side of a media track; discontinuity() marks a break between pre-framed payloads. -typedef MediaProducer = MoqMediaProducer; - -/// The write side of a media track fed a raw byte stream, with frame boundaries inferred. -typedef MediaStreamProducer = MoqMediaStreamProducer; - -/// The write side of a container, which publishes each track it describes. -typedef ContainerProducer = MoqContainerProducer; - -/// The write side of a container fed a raw byte stream. -typedef ContainerStreamProducer = MoqContainerStreamProducer; - -/// The read side of a media track: yields frames with codec metadata in decode order. -typedef MediaConsumer = MoqMediaConsumer; - -/// A finite fetched media group: yields container-decoded frames until the group ends. -typedef MediaGroupConsumer = MoqMediaGroupConsumer; - -/// The read side of a broadcast's catalog: yields updates as the set of tracks changes. -typedef CatalogConsumer = MoqCatalogConsumer; - -/// Publishes lossy latest-value JSON snapshots. -typedef JsonSnapshotProducer = MoqJsonSnapshotProducer; - -/// Consumes reconstructed latest-value JSON snapshots. -typedef JsonSnapshotConsumer = MoqJsonSnapshotConsumer; - -/// Configures a lossy latest-value JSON track. -typedef JsonSnapshotConfig = MoqJsonSnapshotConfig; - -/// Publishes a lossless stream of JSON records. -typedef JsonStreamProducer = MoqJsonStreamProducer; - -/// Consumes a lossless stream of JSON records. -typedef JsonStreamConsumer = MoqJsonStreamConsumer; - -/// Configures a lossless JSON stream track. -typedef JsonStreamConfig = MoqJsonStreamConfig; - -/// A broadcast's catalog: its tracks and their properties, plus any application sections. -typedef Catalog = MoqCatalog; - /// A datagram-delivered frame, tagged with a per-track sequence number. typedef Datagram = MoqDatagram; /// A payload plus the timestamp it should be presented at. typedef Frame = MoqFrame; -/// A media [Frame] whose keyframe flag marks group starts or video keyframes; audio flags only group starts. -typedef MediaFrame = MoqMediaFrame; - -/// The catalog description of a video track, including whether it is enabled (a disabled one has no frames coming). -typedef Video = MoqVideo; - -/// Caller-provided catalog fields for a video track. -typedef VideoHint = MoqVideoHint; - -/// A video codec, optional init bytes, a label, and catalog hints. -typedef VideoInit = MoqVideoInit; - -/// Catalog properties shared by every video rendition; absent fields clear those properties. -typedef VideoProperties = MoqVideoProperties; - -/// A single video codec an importer can parse. -typedef VideoFormat = MoqVideoFormat; - -/// The catalog description of an audio track: codec, sample rate, channels, whether it is enabled, and container. -typedef Audio = MoqAudio; - -/// An audio codec, its required init bytes, and an optional label. -typedef AudioInit = MoqAudioInit; - -/// A single audio codec an importer can parse. -typedef AudioFormat = MoqAudioFormat; - -/// A container that publishes its own tracks. -typedef ContainerFormat = MoqContainerFormat; - -/// A container format and its leading bytes. -typedef ContainerInit = MoqContainerInit; - -/// A width and height pair, in pixels. -typedef Dimensions = MoqDimensions; - /// Tunes how a track subscription is delivered: priority, group ordering, and range. typedef Subscription = MoqSubscription; diff --git a/dart/moq/lib/src/client.dart b/dart/moq/lib/src/client.dart index fb65eefae5a..bfc239de7a2 100644 --- a/dart/moq/lib/src/client.dart +++ b/dart/moq/lib/src/client.dart @@ -1,3 +1,5 @@ +import 'package:moq_ffi/moq_ffi.dart'; + import 'aliases.dart'; /// Scope for discovering announcements. @@ -18,6 +20,9 @@ final class AnnounceOptions { } /// Everything [Moq.connect] can be told beyond the URL. +/// +/// Every field is optional; a null keeps the native default. A value the native +/// side cannot use fails [Moq.connect] with `MoqException.Config`. final class ConnectOptions { /// Set false to skip certificate verification (local dev only). final bool tlsVerify; @@ -40,6 +45,10 @@ final class ConnectOptions { /// Local socket address to bind, e.g. `0.0.0.0:0`. final String? bind; + /// Protocol versions to offer, most preferred first, e.g. `moq-lite-03`. + /// Null offers every supported version. + final List? versions; + /// Cap on the concurrent QUIC streams the peer may open toward this /// connection. MoQ opens one stream per group, so a subscriber to many /// tracks may want this raised. @@ -55,7 +64,7 @@ final class ConnectOptions { /// Set false for a one-shot dial. By default the session redials with /// backoff whenever the transport drops. - final bool? reconnect; + final bool reconnect; /// Retry pacing for the automatic reconnect. final Backoff? backoff; @@ -64,7 +73,7 @@ final class ConnectOptions { final OriginProducer? publish; /// Origin to discover broadcasts through; auto-created when null. - final OriginProducer? subscribe; + final OriginProducer? consume; const ConnectOptions({ this.tlsVerify = true, @@ -74,14 +83,47 @@ final class ConnectOptions { this.tlsCert, this.tlsKey, this.bind, + this.versions, this.maxStreams, this.websocketEnabled, this.websocketDelay, - this.reconnect, + this.reconnect = true, this.backoff, this.publish, - this.subscribe, + this.consume, }); + + MoqClientConfig get _ffi { + final delay = websocketDelay; + if (delay != null && delay.isNegative) { + throw ArgumentError.value( + delay, + 'websocketDelay', + 'must not be negative', + ); + } + return MoqClientConfig( + bind: bind, + versions: versions ?? const [], + tls: MoqClientTls( + insecure: !tlsVerify, + roots: tlsRoots ?? const [], + systemRoots: tlsSystemRoots, + fingerprints: tlsFingerprints ?? const [], + cert: tlsCert, + key: tlsKey, + ), + quic: MoqQuicConfig(maxStreams: maxStreams), + websocket: MoqWebSocketConfig( + enabled: websocketEnabled, + delayUs: delay?.inMicroseconds, + ), + once: !reconnect, + backoff: backoff ?? MoqBackoff(), + publish: publish, + consume: consume, + ); + } } /// A connected MoQ session with publishing and subscription conveniences. @@ -95,58 +137,16 @@ final class Moq { /// Connect to a relay at [url]. /// - /// With neither [ConnectOptions.publish] nor [ConnectOptions.subscribe] + /// With neither [ConnectOptions.publish] nor [ConnectOptions.consume] /// given, both sides of the session share one origin, so a broadcast - /// announced here is discoverable through [announcements]. Wiring either + /// announced here is discoverable through [announced]. Wiring either /// side opts out and isolates the two directions. static Future connect( String url, { ConnectOptions options = const ConnectOptions(), }) async { - final websocketDelay = options.websocketDelay; - if (websocketDelay != null && websocketDelay.isNegative) { - throw ArgumentError.value( - websocketDelay, - 'websocketDelay', - 'must not be negative', - ); - } - - final client = Client(); + final client = Client(config: options._ffi); try { - if (!options.tlsVerify) client.setTlsVerify(verify: false); - if (options.tlsRoots != null) { - client.setTlsRoots(paths: options.tlsRoots!); - } - if (options.tlsSystemRoots != null) { - client.setTlsSystemRoots(systemRoots: options.tlsSystemRoots!); - } - if (options.tlsFingerprints != null) { - client.setTlsFingerprints(fingerprints: options.tlsFingerprints!); - } - if (options.tlsCert != null) client.setTlsCert(path: options.tlsCert); - if (options.tlsKey != null) client.setTlsKey(path: options.tlsKey); - if (options.bind != null) client.setBind(addr: options.bind!); - if (options.maxStreams != null) { - client.setQuicMaxStreams(maxStreams: options.maxStreams!); - } - if (options.websocketEnabled != null) { - client.setWebsocketEnabled(enabled: options.websocketEnabled!); - } - if (websocketDelay != null) { - client.setWebsocketDelay(delayUs: websocketDelay.inMicroseconds); - } - if (options.reconnect != null) { - client.setReconnect(enabled: options.reconnect!); - } - if (options.backoff != null) { - client.setBackoff(backoff: options.backoff!); - } - if (options.publish != null) client.setPublish(origin: options.publish); - if (options.subscribe != null) { - client.setConsume(origin: options.subscribe); - } - final session = await client.connect(url: url); return Moq._(session, client); } catch (_) { @@ -162,24 +162,10 @@ final class Moq { BroadcastProducer createBroadcast(String path) => session.publish().createBroadcast(path: path); - /// Stream announce events matching [options]; prefixes stay relative to the origin. - Stream announcements({ - AnnounceOptions options = const AnnounceOptions(), - }) async* { - final announced = session.consume().announced(config: options._ffi); - try { - while (true) { - final event = await announced.next(); - if (event == null) return; - yield event; - } - } finally { - announced.cancel(); - announced.dispose(); - } - } - - /// Return the raw cursor for [options]. + /// Discover routes matching [options]; prefixes stay relative to the origin. + /// + /// Listen to `announced(...).updates()` for a [Stream] of [AnnounceEvent] + /// that releases the cursor when the subscription ends. AnnounceConsumer announced({ AnnounceOptions options = const AnnounceOptions(), }) => session.consume().announced(config: options._ffi); @@ -212,3 +198,23 @@ final class Moq { } } } + +/// A [Stream] view over an announcement cursor. +extension AnnounceConsumerUpdates on AnnounceConsumer { + /// Stream announce events until the cursor ends. + /// + /// Listen once: the cursor is cancelled and released when the subscription + /// ends. + Stream updates() async* { + try { + while (true) { + final event = await next(); + if (event == null) return; + yield event; + } + } finally { + cancel(); + dispose(); + } + } +} diff --git a/dart/moq/lib/src/durations.dart b/dart/moq/lib/src/durations.dart index 5d890644dea..ca766268bbc 100644 --- a/dart/moq/lib/src/durations.dart +++ b/dart/moq/lib/src/durations.dart @@ -13,14 +13,17 @@ extension ConnectionStatsDuration on MoqConnectionStats { /// Duration views over the reconnect pacing. extension BackoffDuration on MoqBackoff { - /// Delay before the first reconnect attempt. - Duration get initial => Duration(microseconds: initialUs); + /// Delay before the first reconnect attempt, or null for the default. + Duration? get initial => + initialUs == null ? null : Duration(microseconds: initialUs!); - /// Maximum delay between reconnect attempts. - Duration get max => Duration(microseconds: maxUs); + /// Maximum delay between reconnect attempts, or null for the default. + Duration? get max => maxUs == null ? null : Duration(microseconds: maxUs!); - /// Time spent retrying before giving up. [Duration.zero] retries forever. - Duration get timeout => Duration(microseconds: timeoutUs); + /// Time spent retrying before giving up, or null for the default. + /// [Duration.zero] retries forever. + Duration? get timeout => + timeoutUs == null ? null : Duration(microseconds: timeoutUs!); } /// Duration views over the subscription knobs. @@ -43,12 +46,6 @@ extension FrameDuration on MoqFrame { timestampUs == null ? null : Duration(microseconds: timestampUs!); } -/// Duration view over a media frame's presentation time. -extension MediaFrameDuration on MoqMediaFrame { - /// Presentation timestamp. - Duration get timestamp => Duration(microseconds: timestampUs); -} - /// Duration view over a datagram's presentation time. extension DatagramDuration on MoqDatagram { /// Presentation timestamp, or null for an untimed datagram. diff --git a/dart/moq/lib/src/server.dart b/dart/moq/lib/src/server.dart index eacc0cfe625..cdbbd3f6806 100644 --- a/dart/moq/lib/src/server.dart +++ b/dart/moq/lib/src/server.dart @@ -3,6 +3,9 @@ import 'package:moq_ffi/moq_ffi.dart'; import 'aliases.dart'; /// Everything [Server.listen] can be told. +/// +/// A value the native side cannot use fails [Server.listen] with +/// `MoqException.Config`. final class ListenOptions { /// Local socket address to listen on, e.g. `127.0.0.1:4443` or `[::]:443`. final String bind; @@ -16,19 +19,28 @@ final class ListenOptions { /// Hostnames to generate a self-signed certificate for. final List? tlsGenerate; + /// Protocol versions to accept, e.g. `moq-lite-03`. Null accepts every + /// supported version. + final List? versions; + + /// Cap on the concurrent QUIC streams each peer may open toward this server. + final int? maxStreams; + /// Origin whose broadcasts are served to incoming sessions; auto-created when null. final OriginProducer? publish; /// Origin that receives broadcasts published by incoming sessions; auto-created when null. - final OriginProducer? subscribe; + final OriginProducer? consume; const ListenOptions({ this.bind = '[::]:443', this.tlsCert, this.tlsKey, this.tlsGenerate, + this.versions, + this.maxStreams, this.publish, - this.subscribe, + this.consume, }); } @@ -52,30 +64,33 @@ final class Server { /// Bind a server at [ListenOptions.bind] and start accepting. /// - /// With neither [ListenOptions.publish] nor [ListenOptions.subscribe] given, + /// With neither [ListenOptions.publish] nor [ListenOptions.consume] given, /// both sides share one origin, so a broadcast created here is also visible /// to sessions publishing into this server. Wiring either side opts out and /// isolates the two directions. static Future listen({ ListenOptions options = const ListenOptions(), }) async { - final shared = options.publish == null && options.subscribe == null + final shared = options.publish == null && options.consume == null ? OriginProducer(config: OriginConfig()) : null; final publishOrigin = options.publish ?? shared; - final subscribeOrigin = options.subscribe ?? shared; - final server = MoqServer(); + final server = MoqServer( + config: MoqServerConfig( + bind: options.bind, + versions: options.versions ?? const [], + tls: MoqServerTls( + cert: options.tlsCert ?? const [], + key: options.tlsKey ?? const [], + generate: options.tlsGenerate ?? const [], + ), + quic: MoqQuicConfig(maxStreams: options.maxStreams), + publish: publishOrigin, + consume: options.consume ?? shared, + ), + ); try { - server.setBind(addr: options.bind); - if (options.tlsCert != null) server.setTlsCert(paths: options.tlsCert!); - if (options.tlsKey != null) server.setTlsKey(paths: options.tlsKey!); - if (options.tlsGenerate != null) { - server.setTlsGenerate(hostnames: options.tlsGenerate!); - } - if (publishOrigin != null) server.setPublish(origin: publishOrigin); - if (subscribeOrigin != null) server.setConsume(origin: subscribeOrigin); - final localAddr = await server.listen(); return Server._(server, localAddr, publishOrigin); } catch (_) { @@ -88,7 +103,7 @@ final class Server { /// Create a broadcast at [path], served to incoming sessions. /// /// Advertise it with `announce` after populating tracks. Throws when [listen] - /// was given a [ListenOptions.subscribe] origin but no + /// was given a [ListenOptions.consume] origin but no /// [ListenOptions.publish] one, since there is then nothing to serve from. BroadcastProducer createBroadcast(String path) { final origin = _publishOrigin; diff --git a/dart/moq/test/moq_test.dart b/dart/moq/test/moq_test.dart index d5f9fc3cb32..ed555c7ef52 100644 --- a/dart/moq/test/moq_test.dart +++ b/dart/moq/test/moq_test.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:moq/media.dart' as moq_media; +import 'package:moq/json.dart' as moq_json; import 'package:moq/moq.dart'; import 'package:test/test.dart'; @@ -14,13 +16,28 @@ Future nextRoute(AnnounceConsumer announced) async { } void main() { + test('catalog handle closes with its broadcast', () { + final broadcast = BroadcastProducer(); + final catalog = moq_media.CatalogProducer(broadcast: broadcast); + catalog.setSection(name: 'app', json: '{"value":42}'); + broadcast.close(); + expect( + () => catalog.removeSection(name: 'app'), + throwsA(isA()), + ); + }); + test('connects, announces, subscribes, and delivers a frame', () async { final relay = MoqOriginProducer(config: MoqOriginConfig()); - final server = MoqServer(); - server.setBind(addr: '127.0.0.1:0'); - server.setTlsGenerate(hostnames: ['localhost']); - server.setPublish(origin: relay); - server.setConsume(origin: relay); + final server = MoqServer( + config: MoqServerConfig( + bind: '127.0.0.1:0', + tls: MoqServerTls(generate: ['localhost']), + quic: MoqQuicConfig(), + publish: relay, + consume: relay, + ), + ); final address = await server.listen().timeout(timeout); final accepted = () async { @@ -36,7 +53,7 @@ void main() { final serverSession = await accepted; expect(client.bandwidth(), isA()); - final announcement = client.announcements().firstWhere( + final announcement = client.announced().updates().firstWhere( (event) => event is AnnounceEventStart, ); final broadcast = relay.createBroadcast(path: 'live'); @@ -55,7 +72,7 @@ void main() { // Routed subscriptions pull their source lazily when the consumer is first read. final nextGroup = consumer.nextGroup(); - await track.used().timeout(timeout); + await track.demand().used().timeout(timeout); final producer = track.appendGroup(); producer.writeFrame( @@ -102,7 +119,7 @@ void main() { ).timeout(timeout); final serverSession = await accepted; - final announcement = client.announcements().firstWhere( + final announcement = client.announced().updates().firstWhere( (event) => event is AnnounceEventStart, ); final broadcast = server.createBroadcast('live'); @@ -132,6 +149,16 @@ void main() { ); }); + test('an invalid config fails before dialing', () { + expect( + Moq.connect( + 'https://localhost', + options: const ConnectOptions(versions: ['moq-lite-99']), + ), + throwsA(isA()), + ); + }); + test('closing a server releases its port', () async { final first = await Server.listen( options: const ListenOptions( @@ -225,4 +252,29 @@ void main() { dynamic.dispose(); served.dispose(); }); + + test('JSON tracks take over a raw track', () async { + final broadcast = MoqBroadcastProducer(); + final producer = moq_json.SnapshotProducer( + broadcast: broadcast, + track: broadcast.publishTrack(name: 'status', info: null), + config: moq_json.SnapshotConfig(), + ); + final track = await broadcast + .consume() + .subscribeTrack(name: 'status', subscription: null) + .timeout(timeout); + final consumer = moq_json.SnapshotConsumer( + track: track, + config: moq_json.SnapshotConfig(), + ); + + producer.update(value: jsonEncode({'state': 'live'})); + final value = await consumer.next().timeout(timeout); + expect(jsonDecode(value!), {'state': 'live'}); + + consumer.cancel(); + producer.finish(); + broadcast.close(); + }); } diff --git a/dart/moq_ffi/lib/src/moq.dart b/dart/moq_ffi/lib/src/moq.dart index 32fbbf9c441..1d0ba2f4931 100644 --- a/dart/moq_ffi/lib/src/moq.dart +++ b/dart/moq_ffi/lib/src/moq.dart @@ -8,7 +8,9 @@ import "dart:ffi"; import "dart:io" show Platform, File, Directory; import "dart:isolate"; import "dart:typed_data"; + import "package:ffi/ffi.dart"; + import "uniffi_runtime.dart"; export "uniffi_runtime.dart"; @@ -571,13 +573,7 @@ class MoqAudioInit { final MoqAudioFormat format; final Uint8List data; final String? label; - final String? track; - MoqAudioInit({ - required this.format, - required this.data, - this.label = null, - this.track = null, - }); + MoqAudioInit({required this.format, required this.data, this.label = null}); } class FfiConverterMoqAudioInit { @@ -602,13 +598,8 @@ class FfiConverterMoqAudioInit { ); final label = label_lifted.value; new_offset += label_lifted.bytesRead; - final track_lifted = FfiConverterOptionalString.read( - Uint8List.view(buf.buffer, new_offset), - ); - final track = track_lifted.value; - new_offset += track_lifted.bytesRead; return LiftRetVal( - MoqAudioInit(format: format, data: data, label: label, track: track), + MoqAudioInit(format: format, data: data, label: label), new_offset - buf.offsetInBytes, ); } @@ -618,7 +609,6 @@ class FfiConverterMoqAudioInit { FfiConverterMoqAudioFormat.allocationSize(value.format) + FfiConverterUint8List.allocationSize(value.data) + FfiConverterOptionalString.allocationSize(value.label) + - FfiConverterOptionalString.allocationSize(value.track) + 0; final buf = Uint8List(total_length); write(value, buf); @@ -639,10 +629,6 @@ class FfiConverterMoqAudioInit { value.label, Uint8List.view(buf.buffer, new_offset), ); - new_offset += FfiConverterOptionalString.write( - value.track, - Uint8List.view(buf.buffer, new_offset), - ); return new_offset - buf.offsetInBytes; } @@ -650,7 +636,6 @@ class FfiConverterMoqAudioInit { return FfiConverterMoqAudioFormat.allocationSize(value.format) + FfiConverterUint8List.allocationSize(value.data) + FfiConverterOptionalString.allocationSize(value.label) + - FfiConverterOptionalString.allocationSize(value.track) + 0; } } @@ -1032,6 +1017,180 @@ class FfiConverterMoqFrame { } } +class MoqMediaContainerConfig { + final String name; + final MoqContainer container; + final MoqSubscription? subscription; + MoqMediaContainerConfig({ + required this.name, + required this.container, + this.subscription = null, + }); +} + +class FfiConverterMoqMediaContainerConfig { + static MoqMediaContainerConfig lift(RustBuffer buf) { + return FfiConverterMoqMediaContainerConfig.read(buf.asUint8List()).value; + } + + static LiftRetVal read(Uint8List buf) { + int new_offset = buf.offsetInBytes; + final name_lifted = FfiConverterString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final name = name_lifted.value; + new_offset += name_lifted.bytesRead; + final container_lifted = FfiConverterMoqContainer.read( + Uint8List.view(buf.buffer, new_offset), + ); + final container = container_lifted.value; + new_offset += container_lifted.bytesRead; + final subscription_lifted = FfiConverterOptionalMoqSubscription.read( + Uint8List.view(buf.buffer, new_offset), + ); + final subscription = subscription_lifted.value; + new_offset += subscription_lifted.bytesRead; + return LiftRetVal( + MoqMediaContainerConfig( + name: name, + container: container, + subscription: subscription, + ), + new_offset - buf.offsetInBytes, + ); + } + + static RustBuffer lower(MoqMediaContainerConfig value) { + final total_length = + FfiConverterString.allocationSize(value.name) + + FfiConverterMoqContainer.allocationSize(value.container) + + FfiConverterOptionalMoqSubscription.allocationSize(value.subscription) + + 0; + final buf = Uint8List(total_length); + write(value, buf); + return toRustBuffer(buf); + } + + static int write(MoqMediaContainerConfig value, Uint8List buf) { + int new_offset = buf.offsetInBytes; + new_offset += FfiConverterString.write( + value.name, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterMoqContainer.write( + value.container, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterOptionalMoqSubscription.write( + value.subscription, + Uint8List.view(buf.buffer, new_offset), + ); + return new_offset - buf.offsetInBytes; + } + + static int allocationSize(MoqMediaContainerConfig value) { + return FfiConverterString.allocationSize(value.name) + + FfiConverterMoqContainer.allocationSize(value.container) + + FfiConverterOptionalMoqSubscription.allocationSize(value.subscription) + + 0; + } +} + +class MoqMediaContainerGroupConfig { + final String name; + final int sequence; + final MoqContainer container; + final MoqFetchGroupOptions? options; + MoqMediaContainerGroupConfig({ + required this.name, + required this.sequence, + required this.container, + this.options = null, + }); +} + +class FfiConverterMoqMediaContainerGroupConfig { + static MoqMediaContainerGroupConfig lift(RustBuffer buf) { + return FfiConverterMoqMediaContainerGroupConfig.read( + buf.asUint8List(), + ).value; + } + + static LiftRetVal read(Uint8List buf) { + int new_offset = buf.offsetInBytes; + final name_lifted = FfiConverterString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final name = name_lifted.value; + new_offset += name_lifted.bytesRead; + final sequence_lifted = FfiConverterUInt64.read( + Uint8List.view(buf.buffer, new_offset), + ); + final sequence = sequence_lifted.value; + new_offset += sequence_lifted.bytesRead; + final container_lifted = FfiConverterMoqContainer.read( + Uint8List.view(buf.buffer, new_offset), + ); + final container = container_lifted.value; + new_offset += container_lifted.bytesRead; + final options_lifted = FfiConverterOptionalMoqFetchGroupOptions.read( + Uint8List.view(buf.buffer, new_offset), + ); + final options = options_lifted.value; + new_offset += options_lifted.bytesRead; + return LiftRetVal( + MoqMediaContainerGroupConfig( + name: name, + sequence: sequence, + container: container, + options: options, + ), + new_offset - buf.offsetInBytes, + ); + } + + static RustBuffer lower(MoqMediaContainerGroupConfig value) { + final total_length = + FfiConverterString.allocationSize(value.name) + + FfiConverterUInt64.allocationSize(value.sequence) + + FfiConverterMoqContainer.allocationSize(value.container) + + FfiConverterOptionalMoqFetchGroupOptions.allocationSize(value.options) + + 0; + final buf = Uint8List(total_length); + write(value, buf); + return toRustBuffer(buf); + } + + static int write(MoqMediaContainerGroupConfig value, Uint8List buf) { + int new_offset = buf.offsetInBytes; + new_offset += FfiConverterString.write( + value.name, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterUInt64.write( + value.sequence, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterMoqContainer.write( + value.container, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterOptionalMoqFetchGroupOptions.write( + value.options, + Uint8List.view(buf.buffer, new_offset), + ); + return new_offset - buf.offsetInBytes; + } + + static int allocationSize(MoqMediaContainerGroupConfig value) { + return FfiConverterString.allocationSize(value.name) + + FfiConverterUInt64.allocationSize(value.sequence) + + FfiConverterMoqContainer.allocationSize(value.container) + + FfiConverterOptionalMoqFetchGroupOptions.allocationSize(value.options) + + 0; + } +} + class MoqMediaFrame { final Uint8List payload; final int timestampUs; @@ -1400,13 +1559,11 @@ class MoqVideoInit { final Uint8List data; final String? label; final MoqVideoHint? hint; - final String? track; MoqVideoInit({ required this.format, required this.data, this.label = null, this.hint = null, - this.track = null, }); } @@ -1437,19 +1594,8 @@ class FfiConverterMoqVideoInit { ); final hint = hint_lifted.value; new_offset += hint_lifted.bytesRead; - final track_lifted = FfiConverterOptionalString.read( - Uint8List.view(buf.buffer, new_offset), - ); - final track = track_lifted.value; - new_offset += track_lifted.bytesRead; return LiftRetVal( - MoqVideoInit( - format: format, - data: data, - label: label, - hint: hint, - track: track, - ), + MoqVideoInit(format: format, data: data, label: label, hint: hint), new_offset - buf.offsetInBytes, ); } @@ -1460,7 +1606,6 @@ class FfiConverterMoqVideoInit { FfiConverterUint8List.allocationSize(value.data) + FfiConverterOptionalString.allocationSize(value.label) + FfiConverterOptionalMoqVideoHint.allocationSize(value.hint) + - FfiConverterOptionalString.allocationSize(value.track) + 0; final buf = Uint8List(total_length); write(value, buf); @@ -1485,10 +1630,6 @@ class FfiConverterMoqVideoInit { value.hint, Uint8List.view(buf.buffer, new_offset), ); - new_offset += FfiConverterOptionalString.write( - value.track, - Uint8List.view(buf.buffer, new_offset), - ); return new_offset - buf.offsetInBytes; } @@ -1497,7 +1638,6 @@ class FfiConverterMoqVideoInit { FfiConverterUint8List.allocationSize(value.data) + FfiConverterOptionalString.allocationSize(value.label) + FfiConverterOptionalMoqVideoHint.allocationSize(value.hint) + - FfiConverterOptionalString.allocationSize(value.track) + 0; } } @@ -1919,16 +2059,212 @@ class FfiConverterMoqTrackInfo { } } +class MoqServerConfig { + final String? bind; + final List versions; + final MoqServerTls tls; + final MoqQuicConfig quic; + final MoqOriginProducer? publish; + final MoqOriginProducer? consume; + MoqServerConfig({ + this.bind = null, + this.versions = const [], + required this.tls, + required this.quic, + this.publish = null, + this.consume = null, + }); +} + +class FfiConverterMoqServerConfig { + static MoqServerConfig lift(RustBuffer buf) { + return FfiConverterMoqServerConfig.read(buf.asUint8List()).value; + } + + static LiftRetVal read(Uint8List buf) { + int new_offset = buf.offsetInBytes; + final bind_lifted = FfiConverterOptionalString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final bind = bind_lifted.value; + new_offset += bind_lifted.bytesRead; + final versions_lifted = FfiConverterSequenceString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final versions = versions_lifted.value; + new_offset += versions_lifted.bytesRead; + final tls_lifted = FfiConverterMoqServerTls.read( + Uint8List.view(buf.buffer, new_offset), + ); + final tls = tls_lifted.value; + new_offset += tls_lifted.bytesRead; + final quic_lifted = FfiConverterMoqQuicConfig.read( + Uint8List.view(buf.buffer, new_offset), + ); + final quic = quic_lifted.value; + new_offset += quic_lifted.bytesRead; + final publish_lifted = FfiConverterOptionalMoqOriginProducer.read( + Uint8List.view(buf.buffer, new_offset), + ); + final publish = publish_lifted.value; + new_offset += publish_lifted.bytesRead; + final consume_lifted = FfiConverterOptionalMoqOriginProducer.read( + Uint8List.view(buf.buffer, new_offset), + ); + final consume = consume_lifted.value; + new_offset += consume_lifted.bytesRead; + return LiftRetVal( + MoqServerConfig( + bind: bind, + versions: versions, + tls: tls, + quic: quic, + publish: publish, + consume: consume, + ), + new_offset - buf.offsetInBytes, + ); + } + + static RustBuffer lower(MoqServerConfig value) { + final total_length = + FfiConverterOptionalString.allocationSize(value.bind) + + FfiConverterSequenceString.allocationSize(value.versions) + + FfiConverterMoqServerTls.allocationSize(value.tls) + + FfiConverterMoqQuicConfig.allocationSize(value.quic) + + FfiConverterOptionalMoqOriginProducer.allocationSize(value.publish) + + FfiConverterOptionalMoqOriginProducer.allocationSize(value.consume) + + 0; + final buf = Uint8List(total_length); + write(value, buf); + return toRustBuffer(buf); + } + + static int write(MoqServerConfig value, Uint8List buf) { + int new_offset = buf.offsetInBytes; + new_offset += FfiConverterOptionalString.write( + value.bind, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterSequenceString.write( + value.versions, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterMoqServerTls.write( + value.tls, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterMoqQuicConfig.write( + value.quic, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterOptionalMoqOriginProducer.write( + value.publish, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterOptionalMoqOriginProducer.write( + value.consume, + Uint8List.view(buf.buffer, new_offset), + ); + return new_offset - buf.offsetInBytes; + } + + static int allocationSize(MoqServerConfig value) { + return FfiConverterOptionalString.allocationSize(value.bind) + + FfiConverterSequenceString.allocationSize(value.versions) + + FfiConverterMoqServerTls.allocationSize(value.tls) + + FfiConverterMoqQuicConfig.allocationSize(value.quic) + + FfiConverterOptionalMoqOriginProducer.allocationSize(value.publish) + + FfiConverterOptionalMoqOriginProducer.allocationSize(value.consume) + + 0; + } +} + +class MoqServerTls { + final List cert; + final List key; + final List generate; + MoqServerTls({ + this.cert = const [], + this.key = const [], + this.generate = const [], + }); +} + +class FfiConverterMoqServerTls { + static MoqServerTls lift(RustBuffer buf) { + return FfiConverterMoqServerTls.read(buf.asUint8List()).value; + } + + static LiftRetVal read(Uint8List buf) { + int new_offset = buf.offsetInBytes; + final cert_lifted = FfiConverterSequenceString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final cert = cert_lifted.value; + new_offset += cert_lifted.bytesRead; + final key_lifted = FfiConverterSequenceString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final key = key_lifted.value; + new_offset += key_lifted.bytesRead; + final generate_lifted = FfiConverterSequenceString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final generate = generate_lifted.value; + new_offset += generate_lifted.bytesRead; + return LiftRetVal( + MoqServerTls(cert: cert, key: key, generate: generate), + new_offset - buf.offsetInBytes, + ); + } + + static RustBuffer lower(MoqServerTls value) { + final total_length = + FfiConverterSequenceString.allocationSize(value.cert) + + FfiConverterSequenceString.allocationSize(value.key) + + FfiConverterSequenceString.allocationSize(value.generate) + + 0; + final buf = Uint8List(total_length); + write(value, buf); + return toRustBuffer(buf); + } + + static int write(MoqServerTls value, Uint8List buf) { + int new_offset = buf.offsetInBytes; + new_offset += FfiConverterSequenceString.write( + value.cert, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterSequenceString.write( + value.key, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterSequenceString.write( + value.generate, + Uint8List.view(buf.buffer, new_offset), + ); + return new_offset - buf.offsetInBytes; + } + + static int allocationSize(MoqServerTls value) { + return FfiConverterSequenceString.allocationSize(value.cert) + + FfiConverterSequenceString.allocationSize(value.key) + + FfiConverterSequenceString.allocationSize(value.generate) + + 0; + } +} + class MoqBackoff { - final int initialUs; - final int multiplier; - final int maxUs; - final int timeoutUs; + final int? initialUs; + final int? multiplier; + final int? maxUs; + final int? timeoutUs; MoqBackoff({ - this.initialUs = 1000000, - this.multiplier = 2, - this.maxUs = 5000000, - this.timeoutUs = 10000000, + this.initialUs = null, + this.multiplier = null, + this.maxUs = null, + this.timeoutUs = null, }); } @@ -1939,22 +2275,22 @@ class FfiConverterMoqBackoff { static LiftRetVal read(Uint8List buf) { int new_offset = buf.offsetInBytes; - final initialUs_lifted = FfiConverterUInt64.read( + final initialUs_lifted = FfiConverterOptionalUInt64.read( Uint8List.view(buf.buffer, new_offset), ); final initialUs = initialUs_lifted.value; new_offset += initialUs_lifted.bytesRead; - final multiplier_lifted = FfiConverterUInt32.read( + final multiplier_lifted = FfiConverterOptionalUInt32.read( Uint8List.view(buf.buffer, new_offset), ); final multiplier = multiplier_lifted.value; new_offset += multiplier_lifted.bytesRead; - final maxUs_lifted = FfiConverterUInt64.read( + final maxUs_lifted = FfiConverterOptionalUInt64.read( Uint8List.view(buf.buffer, new_offset), ); final maxUs = maxUs_lifted.value; new_offset += maxUs_lifted.bytesRead; - final timeoutUs_lifted = FfiConverterUInt64.read( + final timeoutUs_lifted = FfiConverterOptionalUInt64.read( Uint8List.view(buf.buffer, new_offset), ); final timeoutUs = timeoutUs_lifted.value; @@ -1972,10 +2308,10 @@ class FfiConverterMoqBackoff { static RustBuffer lower(MoqBackoff value) { final total_length = - FfiConverterUInt64.allocationSize(value.initialUs) + - FfiConverterUInt32.allocationSize(value.multiplier) + - FfiConverterUInt64.allocationSize(value.maxUs) + - FfiConverterUInt64.allocationSize(value.timeoutUs) + + FfiConverterOptionalUInt64.allocationSize(value.initialUs) + + FfiConverterOptionalUInt32.allocationSize(value.multiplier) + + FfiConverterOptionalUInt64.allocationSize(value.maxUs) + + FfiConverterOptionalUInt64.allocationSize(value.timeoutUs) + 0; final buf = Uint8List(total_length); write(value, buf); @@ -1984,19 +2320,19 @@ class FfiConverterMoqBackoff { static int write(MoqBackoff value, Uint8List buf) { int new_offset = buf.offsetInBytes; - new_offset += FfiConverterUInt64.write( + new_offset += FfiConverterOptionalUInt64.write( value.initialUs, Uint8List.view(buf.buffer, new_offset), ); - new_offset += FfiConverterUInt32.write( + new_offset += FfiConverterOptionalUInt32.write( value.multiplier, Uint8List.view(buf.buffer, new_offset), ); - new_offset += FfiConverterUInt64.write( + new_offset += FfiConverterOptionalUInt64.write( value.maxUs, Uint8List.view(buf.buffer, new_offset), ); - new_offset += FfiConverterUInt64.write( + new_offset += FfiConverterOptionalUInt64.write( value.timeoutUs, Uint8List.view(buf.buffer, new_offset), ); @@ -2004,48 +2340,332 @@ class FfiConverterMoqBackoff { } static int allocationSize(MoqBackoff value) { - return FfiConverterUInt64.allocationSize(value.initialUs) + - FfiConverterUInt32.allocationSize(value.multiplier) + - FfiConverterUInt64.allocationSize(value.maxUs) + - FfiConverterUInt64.allocationSize(value.timeoutUs) + + return FfiConverterOptionalUInt64.allocationSize(value.initialUs) + + FfiConverterOptionalUInt32.allocationSize(value.multiplier) + + FfiConverterOptionalUInt64.allocationSize(value.maxUs) + + FfiConverterOptionalUInt64.allocationSize(value.timeoutUs) + 0; } } -class MoqConnectionStats { - final int? rttUs; - final int? estimatedSendRateBps; - final int? estimatedRecvRateBps; - final int? bytesSent; - final int? bytesReceived; - final int? bytesLost; - final int? packetsSent; - final int? packetsReceived; - final int? packetsLost; - MoqConnectionStats({ - this.rttUs, - this.estimatedSendRateBps, - this.estimatedRecvRateBps, - this.bytesSent, - this.bytesReceived, - this.bytesLost, - this.packetsSent, - this.packetsReceived, - this.packetsLost, +class MoqClientConfig { + final String? bind; + final List versions; + final MoqClientTls tls; + final MoqQuicConfig quic; + final MoqWebSocketConfig websocket; + final bool once; + final MoqBackoff backoff; + final MoqOriginProducer? publish; + final MoqOriginProducer? consume; + MoqClientConfig({ + this.bind = null, + this.versions = const [], + required this.tls, + required this.quic, + required this.websocket, + this.once = false, + required this.backoff, + this.publish = null, + this.consume = null, }); } -class FfiConverterMoqConnectionStats { - static MoqConnectionStats lift(RustBuffer buf) { - return FfiConverterMoqConnectionStats.read(buf.asUint8List()).value; +class FfiConverterMoqClientConfig { + static MoqClientConfig lift(RustBuffer buf) { + return FfiConverterMoqClientConfig.read(buf.asUint8List()).value; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { int new_offset = buf.offsetInBytes; - final rttUs_lifted = FfiConverterOptionalUInt64.read( + final bind_lifted = FfiConverterOptionalString.read( Uint8List.view(buf.buffer, new_offset), ); - final rttUs = rttUs_lifted.value; + final bind = bind_lifted.value; + new_offset += bind_lifted.bytesRead; + final versions_lifted = FfiConverterSequenceString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final versions = versions_lifted.value; + new_offset += versions_lifted.bytesRead; + final tls_lifted = FfiConverterMoqClientTls.read( + Uint8List.view(buf.buffer, new_offset), + ); + final tls = tls_lifted.value; + new_offset += tls_lifted.bytesRead; + final quic_lifted = FfiConverterMoqQuicConfig.read( + Uint8List.view(buf.buffer, new_offset), + ); + final quic = quic_lifted.value; + new_offset += quic_lifted.bytesRead; + final websocket_lifted = FfiConverterMoqWebSocketConfig.read( + Uint8List.view(buf.buffer, new_offset), + ); + final websocket = websocket_lifted.value; + new_offset += websocket_lifted.bytesRead; + final once_lifted = FfiConverterBool.read( + Uint8List.view(buf.buffer, new_offset), + ); + final once = once_lifted.value; + new_offset += once_lifted.bytesRead; + final backoff_lifted = FfiConverterMoqBackoff.read( + Uint8List.view(buf.buffer, new_offset), + ); + final backoff = backoff_lifted.value; + new_offset += backoff_lifted.bytesRead; + final publish_lifted = FfiConverterOptionalMoqOriginProducer.read( + Uint8List.view(buf.buffer, new_offset), + ); + final publish = publish_lifted.value; + new_offset += publish_lifted.bytesRead; + final consume_lifted = FfiConverterOptionalMoqOriginProducer.read( + Uint8List.view(buf.buffer, new_offset), + ); + final consume = consume_lifted.value; + new_offset += consume_lifted.bytesRead; + return LiftRetVal( + MoqClientConfig( + bind: bind, + versions: versions, + tls: tls, + quic: quic, + websocket: websocket, + once: once, + backoff: backoff, + publish: publish, + consume: consume, + ), + new_offset - buf.offsetInBytes, + ); + } + + static RustBuffer lower(MoqClientConfig value) { + final total_length = + FfiConverterOptionalString.allocationSize(value.bind) + + FfiConverterSequenceString.allocationSize(value.versions) + + FfiConverterMoqClientTls.allocationSize(value.tls) + + FfiConverterMoqQuicConfig.allocationSize(value.quic) + + FfiConverterMoqWebSocketConfig.allocationSize(value.websocket) + + FfiConverterBool.allocationSize(value.once) + + FfiConverterMoqBackoff.allocationSize(value.backoff) + + FfiConverterOptionalMoqOriginProducer.allocationSize(value.publish) + + FfiConverterOptionalMoqOriginProducer.allocationSize(value.consume) + + 0; + final buf = Uint8List(total_length); + write(value, buf); + return toRustBuffer(buf); + } + + static int write(MoqClientConfig value, Uint8List buf) { + int new_offset = buf.offsetInBytes; + new_offset += FfiConverterOptionalString.write( + value.bind, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterSequenceString.write( + value.versions, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterMoqClientTls.write( + value.tls, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterMoqQuicConfig.write( + value.quic, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterMoqWebSocketConfig.write( + value.websocket, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterBool.write( + value.once, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterMoqBackoff.write( + value.backoff, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterOptionalMoqOriginProducer.write( + value.publish, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterOptionalMoqOriginProducer.write( + value.consume, + Uint8List.view(buf.buffer, new_offset), + ); + return new_offset - buf.offsetInBytes; + } + + static int allocationSize(MoqClientConfig value) { + return FfiConverterOptionalString.allocationSize(value.bind) + + FfiConverterSequenceString.allocationSize(value.versions) + + FfiConverterMoqClientTls.allocationSize(value.tls) + + FfiConverterMoqQuicConfig.allocationSize(value.quic) + + FfiConverterMoqWebSocketConfig.allocationSize(value.websocket) + + FfiConverterBool.allocationSize(value.once) + + FfiConverterMoqBackoff.allocationSize(value.backoff) + + FfiConverterOptionalMoqOriginProducer.allocationSize(value.publish) + + FfiConverterOptionalMoqOriginProducer.allocationSize(value.consume) + + 0; + } +} + +class MoqClientTls { + final bool insecure; + final List roots; + final bool? systemRoots; + final List fingerprints; + final String? cert; + final String? key; + MoqClientTls({ + this.insecure = false, + this.roots = const [], + this.systemRoots = null, + this.fingerprints = const [], + this.cert = null, + this.key = null, + }); +} + +class FfiConverterMoqClientTls { + static MoqClientTls lift(RustBuffer buf) { + return FfiConverterMoqClientTls.read(buf.asUint8List()).value; + } + + static LiftRetVal read(Uint8List buf) { + int new_offset = buf.offsetInBytes; + final insecure_lifted = FfiConverterBool.read( + Uint8List.view(buf.buffer, new_offset), + ); + final insecure = insecure_lifted.value; + new_offset += insecure_lifted.bytesRead; + final roots_lifted = FfiConverterSequenceString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final roots = roots_lifted.value; + new_offset += roots_lifted.bytesRead; + final systemRoots_lifted = FfiConverterOptionalBool.read( + Uint8List.view(buf.buffer, new_offset), + ); + final systemRoots = systemRoots_lifted.value; + new_offset += systemRoots_lifted.bytesRead; + final fingerprints_lifted = FfiConverterSequenceString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final fingerprints = fingerprints_lifted.value; + new_offset += fingerprints_lifted.bytesRead; + final cert_lifted = FfiConverterOptionalString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final cert = cert_lifted.value; + new_offset += cert_lifted.bytesRead; + final key_lifted = FfiConverterOptionalString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final key = key_lifted.value; + new_offset += key_lifted.bytesRead; + return LiftRetVal( + MoqClientTls( + insecure: insecure, + roots: roots, + systemRoots: systemRoots, + fingerprints: fingerprints, + cert: cert, + key: key, + ), + new_offset - buf.offsetInBytes, + ); + } + + static RustBuffer lower(MoqClientTls value) { + final total_length = + FfiConverterBool.allocationSize(value.insecure) + + FfiConverterSequenceString.allocationSize(value.roots) + + FfiConverterOptionalBool.allocationSize(value.systemRoots) + + FfiConverterSequenceString.allocationSize(value.fingerprints) + + FfiConverterOptionalString.allocationSize(value.cert) + + FfiConverterOptionalString.allocationSize(value.key) + + 0; + final buf = Uint8List(total_length); + write(value, buf); + return toRustBuffer(buf); + } + + static int write(MoqClientTls value, Uint8List buf) { + int new_offset = buf.offsetInBytes; + new_offset += FfiConverterBool.write( + value.insecure, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterSequenceString.write( + value.roots, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterOptionalBool.write( + value.systemRoots, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterSequenceString.write( + value.fingerprints, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterOptionalString.write( + value.cert, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterOptionalString.write( + value.key, + Uint8List.view(buf.buffer, new_offset), + ); + return new_offset - buf.offsetInBytes; + } + + static int allocationSize(MoqClientTls value) { + return FfiConverterBool.allocationSize(value.insecure) + + FfiConverterSequenceString.allocationSize(value.roots) + + FfiConverterOptionalBool.allocationSize(value.systemRoots) + + FfiConverterSequenceString.allocationSize(value.fingerprints) + + FfiConverterOptionalString.allocationSize(value.cert) + + FfiConverterOptionalString.allocationSize(value.key) + + 0; + } +} + +class MoqConnectionStats { + final int? rttUs; + final int? estimatedSendRateBps; + final int? estimatedRecvRateBps; + final int? bytesSent; + final int? bytesReceived; + final int? bytesLost; + final int? packetsSent; + final int? packetsReceived; + final int? packetsLost; + MoqConnectionStats({ + this.rttUs, + this.estimatedSendRateBps, + this.estimatedRecvRateBps, + this.bytesSent, + this.bytesReceived, + this.bytesLost, + this.packetsSent, + this.packetsReceived, + this.packetsLost, + }); +} + +class FfiConverterMoqConnectionStats { + static MoqConnectionStats lift(RustBuffer buf) { + return FfiConverterMoqConnectionStats.read(buf.asUint8List()).value; + } + + static LiftRetVal read(Uint8List buf) { + int new_offset = buf.offsetInBytes; + final rttUs_lifted = FfiConverterOptionalUInt64.read( + Uint8List.view(buf.buffer, new_offset), + ); + final rttUs = rttUs_lifted.value; new_offset += rttUs_lifted.bytesRead; final estimatedSendRateBps_lifted = FfiConverterOptionalUInt64.read( Uint8List.view(buf.buffer, new_offset), @@ -2175,6 +2795,110 @@ class FfiConverterMoqConnectionStats { } } +class MoqQuicConfig { + final int? maxStreams; + MoqQuicConfig({this.maxStreams = null}); +} + +class FfiConverterMoqQuicConfig { + static MoqQuicConfig lift(RustBuffer buf) { + return FfiConverterMoqQuicConfig.read(buf.asUint8List()).value; + } + + static LiftRetVal read(Uint8List buf) { + int new_offset = buf.offsetInBytes; + final maxStreams_lifted = FfiConverterOptionalUInt64.read( + Uint8List.view(buf.buffer, new_offset), + ); + final maxStreams = maxStreams_lifted.value; + new_offset += maxStreams_lifted.bytesRead; + return LiftRetVal( + MoqQuicConfig(maxStreams: maxStreams), + new_offset - buf.offsetInBytes, + ); + } + + static RustBuffer lower(MoqQuicConfig value) { + final total_length = + FfiConverterOptionalUInt64.allocationSize(value.maxStreams) + 0; + final buf = Uint8List(total_length); + write(value, buf); + return toRustBuffer(buf); + } + + static int write(MoqQuicConfig value, Uint8List buf) { + int new_offset = buf.offsetInBytes; + new_offset += FfiConverterOptionalUInt64.write( + value.maxStreams, + Uint8List.view(buf.buffer, new_offset), + ); + return new_offset - buf.offsetInBytes; + } + + static int allocationSize(MoqQuicConfig value) { + return FfiConverterOptionalUInt64.allocationSize(value.maxStreams) + 0; + } +} + +class MoqWebSocketConfig { + final bool? enabled; + final int? delayUs; + MoqWebSocketConfig({this.enabled = null, this.delayUs = null}); +} + +class FfiConverterMoqWebSocketConfig { + static MoqWebSocketConfig lift(RustBuffer buf) { + return FfiConverterMoqWebSocketConfig.read(buf.asUint8List()).value; + } + + static LiftRetVal read(Uint8List buf) { + int new_offset = buf.offsetInBytes; + final enabled_lifted = FfiConverterOptionalBool.read( + Uint8List.view(buf.buffer, new_offset), + ); + final enabled = enabled_lifted.value; + new_offset += enabled_lifted.bytesRead; + final delayUs_lifted = FfiConverterOptionalUInt64.read( + Uint8List.view(buf.buffer, new_offset), + ); + final delayUs = delayUs_lifted.value; + new_offset += delayUs_lifted.bytesRead; + return LiftRetVal( + MoqWebSocketConfig(enabled: enabled, delayUs: delayUs), + new_offset - buf.offsetInBytes, + ); + } + + static RustBuffer lower(MoqWebSocketConfig value) { + final total_length = + FfiConverterOptionalBool.allocationSize(value.enabled) + + FfiConverterOptionalUInt64.allocationSize(value.delayUs) + + 0; + final buf = Uint8List(total_length); + write(value, buf); + return toRustBuffer(buf); + } + + static int write(MoqWebSocketConfig value, Uint8List buf) { + int new_offset = buf.offsetInBytes; + new_offset += FfiConverterOptionalBool.write( + value.enabled, + Uint8List.view(buf.buffer, new_offset), + ); + new_offset += FfiConverterOptionalUInt64.write( + value.delayUs, + Uint8List.view(buf.buffer, new_offset), + ); + return new_offset - buf.offsetInBytes; + } + + static int allocationSize(MoqWebSocketConfig value) { + return FfiConverterOptionalBool.allocationSize(value.enabled) + + FfiConverterOptionalUInt64.allocationSize(value.delayUs) + + 0; + } +} + abstract class MoqException implements Exception { RustBuffer lower(); int allocationSize(); @@ -2368,6 +3092,12 @@ class FfiConverterMoqException { lifted.value, lifted.bytesRead - subview.offsetInBytes + 4, ); + case 29: + final lifted = ConfigMoqException.read(subview); + return LiftRetVal( + lifted.value, + lifted.bytesRead - subview.offsetInBytes + 4, + ); default: throw UniffiInternalError( UniffiInternalError.unexpectedEnumCase, @@ -3353,6 +4083,44 @@ class LogMoqException extends MoqException { } } +class ConfigMoqException extends MoqException { + final String v0; + ConfigMoqException(String this.v0); + ConfigMoqException._(String this.v0); + static LiftRetVal read(Uint8List buf) { + int new_offset = buf.offsetInBytes; + final v0_lifted = FfiConverterString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final v0 = v0_lifted.value; + new_offset += v0_lifted.bytesRead; + return LiftRetVal(ConfigMoqException._(v0), new_offset); + } + + @override + RustBuffer lower() { + final buf = Uint8List(allocationSize()); + write(buf); + return toRustBuffer(buf); + } + + @override + int allocationSize() { + return FfiConverterString.allocationSize(v0) + 4; + } + + @override + int write(Uint8List buf) { + buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 29); + int new_offset = buf.offsetInBytes + 4; + new_offset += FfiConverterString.write( + v0, + Uint8List.view(buf.buffer, new_offset), + ); + return new_offset; + } +} + class MoqExceptionErrorHandler extends UniffiRustCallStatusErrorHandler { @override Exception lift(RustBuffer errorBuf) { @@ -3726,11 +4494,135 @@ class FfiConverterMoqContainerFormat { } } -enum MoqVideoFormat { avc1, avc3, hvc1, hev1, av01, vp8, vp9 } +abstract class MoqMediaTarget { + RustBuffer lower(); + int allocationSize(); + int write(Uint8List buf); +} -class FfiConverterMoqVideoFormat { - static LiftRetVal read(Uint8List buf) { - final index = buf.buffer.asByteData(buf.offsetInBytes).getInt32(0); +class FfiConverterMoqMediaTarget { + static MoqMediaTarget lift(RustBuffer buffer) { + return FfiConverterMoqMediaTarget.read(buffer.asUint8List()).value; + } + + static LiftRetVal read(Uint8List buf) { + final index = buf.buffer.asByteData(buf.offsetInBytes).getInt32(0); + final subview = Uint8List.view(buf.buffer, buf.offsetInBytes + 4); + switch (index) { + case 1: + final lifted = NamedMoqMediaTarget.read(subview); + return LiftRetVal( + lifted.value, + lifted.bytesRead - subview.offsetInBytes + 4, + ); + case 2: + final lifted = RequestedMoqMediaTarget.read(subview); + return LiftRetVal( + lifted.value, + lifted.bytesRead - subview.offsetInBytes + 4, + ); + default: + throw UniffiInternalError( + UniffiInternalError.unexpectedEnumCase, + "Unable to determine enum variant", + ); + } + } + + static RustBuffer lower(MoqMediaTarget value) { + return value.lower(); + } + + static int allocationSize(MoqMediaTarget value) { + return value.allocationSize(); + } + + static int write(MoqMediaTarget value, Uint8List buf) { + return value.write(buf) - buf.offsetInBytes; + } +} + +class NamedMoqMediaTarget extends MoqMediaTarget { + final String? name; + NamedMoqMediaTarget(String? this.name); + NamedMoqMediaTarget._(String? this.name); + static LiftRetVal read(Uint8List buf) { + int new_offset = buf.offsetInBytes; + final name_lifted = FfiConverterOptionalString.read( + Uint8List.view(buf.buffer, new_offset), + ); + final name = name_lifted.value; + new_offset += name_lifted.bytesRead; + return LiftRetVal(NamedMoqMediaTarget._(name), new_offset); + } + + @override + RustBuffer lower() { + final buf = Uint8List(allocationSize()); + write(buf); + return toRustBuffer(buf); + } + + @override + int allocationSize() { + return FfiConverterOptionalString.allocationSize(name) + 4; + } + + @override + int write(Uint8List buf) { + buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 1); + int new_offset = buf.offsetInBytes + 4; + new_offset += FfiConverterOptionalString.write( + name, + Uint8List.view(buf.buffer, new_offset), + ); + return new_offset; + } +} + +class RequestedMoqMediaTarget extends MoqMediaTarget { + final MoqTrackRequest request; + RequestedMoqMediaTarget(MoqTrackRequest this.request); + RequestedMoqMediaTarget._(MoqTrackRequest this.request); + static LiftRetVal read(Uint8List buf) { + int new_offset = buf.offsetInBytes; + final request_lifted = FfiConverterMoqTrackRequest.read( + Uint8List.view(buf.buffer, new_offset), + ); + final request = request_lifted.value; + new_offset += request_lifted.bytesRead; + return LiftRetVal(RequestedMoqMediaTarget._(request), new_offset); + } + + @override + RustBuffer lower() { + final buf = Uint8List(allocationSize()); + write(buf); + return toRustBuffer(buf); + } + + @override + int allocationSize() { + return FfiConverterMoqTrackRequest.allocationSize(request) + 4; + } + + @override + int write(Uint8List buf) { + buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 2); + int new_offset = buf.offsetInBytes + 4; + new_offset += FfiConverterMoqTrackRequest.write( + request, + Uint8List.view(buf.buffer, new_offset), + ); + return new_offset; + } +} + +enum MoqVideoFormat { avc1, avc3, hvc1, hev1, av01, vp8, vp9 } + +class FfiConverterMoqVideoFormat { + static LiftRetVal read(Uint8List buf) { + final index = buf.buffer.asByteData(buf.offsetInBytes).getInt32(0); switch (index) { case 1: return LiftRetVal(MoqVideoFormat.avc1, 4); @@ -4177,31 +5069,11 @@ abstract class MoqBroadcastConsumerInterface { required int sequence, required MoqFetchGroupOptions? options, }); - Future fetchMediaGroup({ - required String name, - required int sequence, - required MoqContainer container, - required MoqFetchGroupOptions? options, - }); Future resolve({required String? reference}); - Future subscribeCatalog(); - Future subscribeMedia({ - required String name, - required MoqContainer container, - required MoqSubscription? subscription, - }); Future subscribeTrack({ required String name, required MoqSubscription? subscription, }); - Future subscribeJsonSnapshot({ - required String name, - required MoqJsonSnapshotConfig config, - }); - Future subscribeJsonStream({ - required String name, - required MoqJsonStreamConfig config, - }); } final _MoqBroadcastConsumerFinalizer = Finalizer>((ptr) { @@ -4252,30 +5124,6 @@ class MoqBroadcastConsumer implements MoqBroadcastConsumerInterface { ); } - Future fetchMediaGroup({ - required String name, - required int sequence, - required MoqContainer container, - required MoqFetchGroupOptions? options, - }) { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqbroadcastconsumer_fetch_media_group( - uniffiClonePointer(), - FfiConverterString.lower(name), - FfiConverterUInt64.lower(sequence), - FfiConverterMoqContainer.lower(container), - FfiConverterOptionalMoqFetchGroupOptions.lower(options), - ), - ffi_moq_ffi_rust_future_poll_u64, - ffi_moq_ffi_rust_future_complete_u64, - ffi_moq_ffi_rust_future_free_u64, - (ptr) => FfiConverterMoqMediaGroupConsumer.lift( - Pointer.fromAddress(ptr), - ), - moqExceptionErrorHandler, - ); - } - Future resolve({required String? reference}) { return uniffiRustCallAsync( () => uniffi_moq_ffi_fn_method_moqbroadcastconsumer_resolve( @@ -4291,41 +5139,6 @@ class MoqBroadcastConsumer implements MoqBroadcastConsumerInterface { ); } - Future subscribeCatalog() { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqbroadcastconsumer_subscribe_catalog( - uniffiClonePointer(), - ), - ffi_moq_ffi_rust_future_poll_u64, - ffi_moq_ffi_rust_future_complete_u64, - ffi_moq_ffi_rust_future_free_u64, - (ptr) => - FfiConverterMoqCatalogConsumer.lift(Pointer.fromAddress(ptr)), - moqExceptionErrorHandler, - ); - } - - Future subscribeMedia({ - required String name, - required MoqContainer container, - required MoqSubscription? subscription, - }) { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqbroadcastconsumer_subscribe_media( - uniffiClonePointer(), - FfiConverterString.lower(name), - FfiConverterMoqContainer.lower(container), - FfiConverterOptionalMoqSubscription.lower(subscription), - ), - ffi_moq_ffi_rust_future_poll_u64, - ffi_moq_ffi_rust_future_complete_u64, - ffi_moq_ffi_rust_future_free_u64, - (ptr) => - FfiConverterMoqMediaConsumer.lift(Pointer.fromAddress(ptr)), - moqExceptionErrorHandler, - ); - } - Future subscribeTrack({ required String name, required MoqSubscription? subscription, @@ -4344,47 +5157,6 @@ class MoqBroadcastConsumer implements MoqBroadcastConsumerInterface { moqExceptionErrorHandler, ); } - - Future subscribeJsonSnapshot({ - required String name, - required MoqJsonSnapshotConfig config, - }) { - return uniffiRustCallAsync( - () => - uniffi_moq_ffi_fn_method_moqbroadcastconsumer_subscribe_json_snapshot( - uniffiClonePointer(), - FfiConverterString.lower(name), - FfiConverterMoqJsonSnapshotConfig.lower(config), - ), - ffi_moq_ffi_rust_future_poll_u64, - ffi_moq_ffi_rust_future_complete_u64, - ffi_moq_ffi_rust_future_free_u64, - (ptr) => FfiConverterMoqJsonSnapshotConsumer.lift( - Pointer.fromAddress(ptr), - ), - moqExceptionErrorHandler, - ); - } - - Future subscribeJsonStream({ - required String name, - required MoqJsonStreamConfig config, - }) { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqbroadcastconsumer_subscribe_json_stream( - uniffiClonePointer(), - FfiConverterString.lower(name), - FfiConverterMoqJsonStreamConfig.lower(config), - ), - ffi_moq_ffi_rust_future_poll_u64, - ffi_moq_ffi_rust_future_complete_u64, - ffi_moq_ffi_rust_future_free_u64, - (ptr) => FfiConverterMoqJsonStreamConsumer.lift( - Pointer.fromAddress(ptr), - ), - moqExceptionErrorHandler, - ); - } } class FfiConverterMoqBroadcastConsumer { @@ -4413,85 +5185,6 @@ class FfiConverterMoqBroadcastConsumer { } } -abstract class MoqCatalogConsumerInterface { - void cancel(); - Future next(); -} - -final _MoqCatalogConsumerFinalizer = Finalizer>((ptr) { - rustCall((status) => uniffi_moq_ffi_fn_free_moqcatalogconsumer(ptr, status)); -}); - -class MoqCatalogConsumer implements MoqCatalogConsumerInterface { - late final Pointer _ptr; - MoqCatalogConsumer._(this._ptr) { - _MoqCatalogConsumerFinalizer.attach(this, _ptr, detach: this); - } - factory MoqCatalogConsumer.lift(Pointer ptr) { - return MoqCatalogConsumer._(ptr); - } - Pointer uniffiClonePointer() { - return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqcatalogconsumer(_ptr, status), - ); - } - - void dispose() { - _MoqCatalogConsumerFinalizer.detach(this); - rustCall( - (status) => uniffi_moq_ffi_fn_free_moqcatalogconsumer(_ptr, status), - ); - } - - void cancel() { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqcatalogconsumer_cancel( - uniffiClonePointer(), - status, - ); - }, null); - } - - Future next() { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqcatalogconsumer_next( - uniffiClonePointer(), - ), - ffi_moq_ffi_rust_future_poll_rust_buffer, - ffi_moq_ffi_rust_future_complete_rust_buffer, - ffi_moq_ffi_rust_future_free_rust_buffer, - FfiConverterOptionalMoqCatalog.lift, - moqExceptionErrorHandler, - ); - } -} - -class FfiConverterMoqCatalogConsumer { - static MoqCatalogConsumer lift(Pointer ptr) { - return MoqCatalogConsumer.lift(ptr); - } - - static Pointer lower(MoqCatalogConsumer value) { - return value.uniffiClonePointer(); - } - - static int allocationSize(MoqCatalogConsumer value) { - return 8; - } - - static LiftRetVal read(Uint8List buf) { - final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); - final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqCatalogConsumer.lift(pointer), 8); - } - - static int write(MoqCatalogConsumer value, Uint8List buf) { - final handle = lower(value); - buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); - return 8; - } -} - abstract class MoqGroupConsumerInterface { void cancel(); Future readFrame(); @@ -4581,211 +5274,42 @@ class FfiConverterMoqGroupConsumer { } } -abstract class MoqMediaConsumerInterface { +abstract class MoqTrackConsumerInterface { void cancel(); - Future next(); + MoqTrackInfo info(); + Future nextGroup(); + Future readFrame(); + Future recvDatagram(); + Future recvGroup(); + void update({required MoqSubscription subscription}); } -final _MoqMediaConsumerFinalizer = Finalizer>((ptr) { - rustCall((status) => uniffi_moq_ffi_fn_free_moqmediaconsumer(ptr, status)); +final _MoqTrackConsumerFinalizer = Finalizer>((ptr) { + rustCall((status) => uniffi_moq_ffi_fn_free_moqtrackconsumer(ptr, status)); }); -class MoqMediaConsumer implements MoqMediaConsumerInterface { +class MoqTrackConsumer implements MoqTrackConsumerInterface { late final Pointer _ptr; - MoqMediaConsumer._(this._ptr) { - _MoqMediaConsumerFinalizer.attach(this, _ptr, detach: this); + MoqTrackConsumer._(this._ptr) { + _MoqTrackConsumerFinalizer.attach(this, _ptr, detach: this); } - factory MoqMediaConsumer.lift(Pointer ptr) { - return MoqMediaConsumer._(ptr); + factory MoqTrackConsumer.lift(Pointer ptr) { + return MoqTrackConsumer._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqmediaconsumer(_ptr, status), + (status) => uniffi_moq_ffi_fn_clone_moqtrackconsumer(_ptr, status), ); } void dispose() { - _MoqMediaConsumerFinalizer.detach(this); - rustCall((status) => uniffi_moq_ffi_fn_free_moqmediaconsumer(_ptr, status)); + _MoqTrackConsumerFinalizer.detach(this); + rustCall((status) => uniffi_moq_ffi_fn_free_moqtrackconsumer(_ptr, status)); } void cancel() { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqmediaconsumer_cancel( - uniffiClonePointer(), - status, - ); - }, null); - } - - Future next() { - return uniffiRustCallAsync( - () => - uniffi_moq_ffi_fn_method_moqmediaconsumer_next(uniffiClonePointer()), - ffi_moq_ffi_rust_future_poll_rust_buffer, - ffi_moq_ffi_rust_future_complete_rust_buffer, - ffi_moq_ffi_rust_future_free_rust_buffer, - FfiConverterOptionalMoqMediaFrame.lift, - moqExceptionErrorHandler, - ); - } -} - -class FfiConverterMoqMediaConsumer { - static MoqMediaConsumer lift(Pointer ptr) { - return MoqMediaConsumer.lift(ptr); - } - - static Pointer lower(MoqMediaConsumer value) { - return value.uniffiClonePointer(); - } - - static int allocationSize(MoqMediaConsumer value) { - return 8; - } - - static LiftRetVal read(Uint8List buf) { - final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); - final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqMediaConsumer.lift(pointer), 8); - } - - static int write(MoqMediaConsumer value, Uint8List buf) { - final handle = lower(value); - buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); - return 8; - } -} - -abstract class MoqMediaGroupConsumerInterface { - void cancel(); - Future next(); - int sequence(); -} - -final _MoqMediaGroupConsumerFinalizer = Finalizer>((ptr) { - rustCall( - (status) => uniffi_moq_ffi_fn_free_moqmediagroupconsumer(ptr, status), - ); -}); - -class MoqMediaGroupConsumer implements MoqMediaGroupConsumerInterface { - late final Pointer _ptr; - MoqMediaGroupConsumer._(this._ptr) { - _MoqMediaGroupConsumerFinalizer.attach(this, _ptr, detach: this); - } - factory MoqMediaGroupConsumer.lift(Pointer ptr) { - return MoqMediaGroupConsumer._(ptr); - } - Pointer uniffiClonePointer() { - return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqmediagroupconsumer(_ptr, status), - ); - } - - void dispose() { - _MoqMediaGroupConsumerFinalizer.detach(this); - rustCall( - (status) => uniffi_moq_ffi_fn_free_moqmediagroupconsumer(_ptr, status), - ); - } - - void cancel() { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqmediagroupconsumer_cancel( - uniffiClonePointer(), - status, - ); - }, null); - } - - Future next() { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqmediagroupconsumer_next( - uniffiClonePointer(), - ), - ffi_moq_ffi_rust_future_poll_rust_buffer, - ffi_moq_ffi_rust_future_complete_rust_buffer, - ffi_moq_ffi_rust_future_free_rust_buffer, - FfiConverterOptionalMoqMediaFrame.lift, - moqExceptionErrorHandler, - ); - } - - int sequence() { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqmediagroupconsumer_sequence( - uniffiClonePointer(), - status, - ), - FfiConverterUInt64.lift, - null, - ); - } -} - -class FfiConverterMoqMediaGroupConsumer { - static MoqMediaGroupConsumer lift(Pointer ptr) { - return MoqMediaGroupConsumer.lift(ptr); - } - - static Pointer lower(MoqMediaGroupConsumer value) { - return value.uniffiClonePointer(); - } - - static int allocationSize(MoqMediaGroupConsumer value) { - return 8; - } - - static LiftRetVal read(Uint8List buf) { - final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); - final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqMediaGroupConsumer.lift(pointer), 8); - } - - static int write(MoqMediaGroupConsumer value, Uint8List buf) { - final handle = lower(value); - buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); - return 8; - } -} - -abstract class MoqTrackConsumerInterface { - void cancel(); - MoqTrackInfo info(); - Future nextGroup(); - Future readFrame(); - Future recvDatagram(); - Future recvGroup(); - void update({required MoqSubscription subscription}); -} - -final _MoqTrackConsumerFinalizer = Finalizer>((ptr) { - rustCall((status) => uniffi_moq_ffi_fn_free_moqtrackconsumer(ptr, status)); -}); - -class MoqTrackConsumer implements MoqTrackConsumerInterface { - late final Pointer _ptr; - MoqTrackConsumer._(this._ptr) { - _MoqTrackConsumerFinalizer.attach(this, _ptr, detach: this); - } - factory MoqTrackConsumer.lift(Pointer ptr) { - return MoqTrackConsumer._(ptr); - } - Pointer uniffiClonePointer() { - return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqtrackconsumer(_ptr, status), - ); - } - - void dispose() { - _MoqTrackConsumerFinalizer.detach(this); - rustCall((status) => uniffi_moq_ffi_fn_free_moqtrackconsumer(_ptr, status)); - } - - void cancel() { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqtrackconsumer_cancel( + uniffi_moq_ffi_fn_method_moqtrackconsumer_cancel( uniffiClonePointer(), status, ); @@ -5010,6 +5534,21 @@ class MoqFlateSnapshotProducer implements MoqFlateSnapshotProducerInterface { MoqFlateSnapshotProducer._(this._ptr) { _MoqFlateSnapshotProducerFinalizer.attach(this, _ptr, detach: this); } + MoqFlateSnapshotProducer({ + required MoqBroadcastProducer broadcast, + required MoqTrackProducer track, + required MoqFlateConfig config, + }) : _ptr = rustCall( + (status) => uniffi_moq_ffi_fn_constructor_moqflatesnapshotproducer_new( + FfiConverterMoqBroadcastProducer.lower(broadcast), + FfiConverterMoqTrackProducer.lower(track), + FfiConverterMoqFlateConfig.lower(config), + status, + ), + moqExceptionErrorHandler, + ) { + _MoqFlateSnapshotProducerFinalizer.attach(this, _ptr, detach: this); + } factory MoqFlateSnapshotProducer.lift(Pointer ptr) { return MoqFlateSnapshotProducer._(ptr); } @@ -5089,6 +5628,21 @@ class MoqFlateStreamProducer implements MoqFlateStreamProducerInterface { MoqFlateStreamProducer._(this._ptr) { _MoqFlateStreamProducerFinalizer.attach(this, _ptr, detach: this); } + MoqFlateStreamProducer({ + required MoqBroadcastProducer broadcast, + required MoqTrackProducer track, + required MoqFlateConfig config, + }) : _ptr = rustCall( + (status) => uniffi_moq_ffi_fn_constructor_moqflatestreamproducer_new( + FfiConverterMoqBroadcastProducer.lower(broadcast), + FfiConverterMoqTrackProducer.lower(track), + FfiConverterMoqFlateConfig.lower(config), + status, + ), + moqExceptionErrorHandler, + ) { + _MoqFlateStreamProducerFinalizer.attach(this, _ptr, detach: this); + } factory MoqFlateStreamProducer.lift(Pointer ptr) { return MoqFlateStreamProducer._(ptr); } @@ -5167,6 +5721,19 @@ class MoqJsonSnapshotConsumer implements MoqJsonSnapshotConsumerInterface { MoqJsonSnapshotConsumer._(this._ptr) { _MoqJsonSnapshotConsumerFinalizer.attach(this, _ptr, detach: this); } + MoqJsonSnapshotConsumer({ + required MoqTrackConsumer track, + required MoqJsonSnapshotConfig config, + }) : _ptr = rustCall( + (status) => uniffi_moq_ffi_fn_constructor_moqjsonsnapshotconsumer_new( + FfiConverterMoqTrackConsumer.lower(track), + FfiConverterMoqJsonSnapshotConfig.lower(config), + status, + ), + moqExceptionErrorHandler, + ) { + _MoqJsonSnapshotConsumerFinalizer.attach(this, _ptr, detach: this); + } factory MoqJsonSnapshotConsumer.lift(Pointer ptr) { return MoqJsonSnapshotConsumer._(ptr); } @@ -5249,6 +5816,21 @@ class MoqJsonSnapshotProducer implements MoqJsonSnapshotProducerInterface { MoqJsonSnapshotProducer._(this._ptr) { _MoqJsonSnapshotProducerFinalizer.attach(this, _ptr, detach: this); } + MoqJsonSnapshotProducer({ + required MoqBroadcastProducer broadcast, + required MoqTrackProducer track, + required MoqJsonSnapshotConfig config, + }) : _ptr = rustCall( + (status) => uniffi_moq_ffi_fn_constructor_moqjsonsnapshotproducer_new( + FfiConverterMoqBroadcastProducer.lower(broadcast), + FfiConverterMoqTrackProducer.lower(track), + FfiConverterMoqJsonSnapshotConfig.lower(config), + status, + ), + moqExceptionErrorHandler, + ) { + _MoqJsonSnapshotProducerFinalizer.attach(this, _ptr, detach: this); + } factory MoqJsonSnapshotProducer.lift(Pointer ptr) { return MoqJsonSnapshotProducer._(ptr); } @@ -5338,6 +5920,19 @@ class MoqJsonStreamConsumer implements MoqJsonStreamConsumerInterface { MoqJsonStreamConsumer._(this._ptr) { _MoqJsonStreamConsumerFinalizer.attach(this, _ptr, detach: this); } + MoqJsonStreamConsumer({ + required MoqTrackConsumer track, + required MoqJsonStreamConfig config, + }) : _ptr = rustCall( + (status) => uniffi_moq_ffi_fn_constructor_moqjsonstreamconsumer_new( + FfiConverterMoqTrackConsumer.lower(track), + FfiConverterMoqJsonStreamConfig.lower(config), + status, + ), + moqExceptionErrorHandler, + ) { + _MoqJsonStreamConsumerFinalizer.attach(this, _ptr, detach: this); + } factory MoqJsonStreamConsumer.lift(Pointer ptr) { return MoqJsonStreamConsumer._(ptr); } @@ -5420,6 +6015,21 @@ class MoqJsonStreamProducer implements MoqJsonStreamProducerInterface { MoqJsonStreamProducer._(this._ptr) { _MoqJsonStreamProducerFinalizer.attach(this, _ptr, detach: this); } + MoqJsonStreamProducer({ + required MoqBroadcastProducer broadcast, + required MoqTrackProducer track, + required MoqJsonStreamConfig config, + }) : _ptr = rustCall( + (status) => uniffi_moq_ffi_fn_constructor_moqjsonstreamproducer_new( + FfiConverterMoqBroadcastProducer.lower(broadcast), + FfiConverterMoqTrackProducer.lower(track), + FfiConverterMoqJsonStreamConfig.lower(config), + status, + ), + moqExceptionErrorHandler, + ) { + _MoqJsonStreamProducerFinalizer.attach(this, _ptr, detach: this); + } factory MoqJsonStreamProducer.lift(Pointer ptr) { return MoqJsonStreamProducer._(ptr); } @@ -5493,1520 +6103,1742 @@ class FfiConverterMoqJsonStreamProducer { } } -abstract class MoqAnnounceConsumerInterface { +abstract class MoqMediaCatalogConsumerInterface { void cancel(); - Future next(); + Future next(); } -final _MoqAnnounceConsumerFinalizer = Finalizer>((ptr) { - rustCall((status) => uniffi_moq_ffi_fn_free_moqannounceconsumer(ptr, status)); +final _MoqMediaCatalogConsumerFinalizer = Finalizer>((ptr) { + rustCall( + (status) => uniffi_moq_ffi_fn_free_moqmediacatalogconsumer(ptr, status), + ); }); -class MoqAnnounceConsumer implements MoqAnnounceConsumerInterface { +class MoqMediaCatalogConsumer implements MoqMediaCatalogConsumerInterface { late final Pointer _ptr; - MoqAnnounceConsumer._(this._ptr) { - _MoqAnnounceConsumerFinalizer.attach(this, _ptr, detach: this); + MoqMediaCatalogConsumer._(this._ptr) { + _MoqMediaCatalogConsumerFinalizer.attach(this, _ptr, detach: this); } - factory MoqAnnounceConsumer.lift(Pointer ptr) { - return MoqAnnounceConsumer._(ptr); + static Future subscribe({ + required MoqBroadcastConsumer broadcast, + }) { + return uniffiRustCallAsync( + () => uniffi_moq_ffi_fn_constructor_moqmediacatalogconsumer_subscribe( + FfiConverterMoqBroadcastConsumer.lower(broadcast), + ), + ffi_moq_ffi_rust_future_poll_u64, + ffi_moq_ffi_rust_future_complete_u64, + ffi_moq_ffi_rust_future_free_u64, + (int handle) => + MoqMediaCatalogConsumer._(Pointer.fromAddress(handle)), + moqExceptionErrorHandler, + ); + } + + factory MoqMediaCatalogConsumer.lift(Pointer ptr) { + return MoqMediaCatalogConsumer._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqannounceconsumer(_ptr, status), + (status) => uniffi_moq_ffi_fn_clone_moqmediacatalogconsumer(_ptr, status), ); } void dispose() { - _MoqAnnounceConsumerFinalizer.detach(this); + _MoqMediaCatalogConsumerFinalizer.detach(this); rustCall( - (status) => uniffi_moq_ffi_fn_free_moqannounceconsumer(_ptr, status), + (status) => uniffi_moq_ffi_fn_free_moqmediacatalogconsumer(_ptr, status), ); } void cancel() { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqannounceconsumer_cancel( + uniffi_moq_ffi_fn_method_moqmediacatalogconsumer_cancel( uniffiClonePointer(), status, ); }, null); } - Future next() { + Future next() { return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqannounceconsumer_next( + () => uniffi_moq_ffi_fn_method_moqmediacatalogconsumer_next( uniffiClonePointer(), ), ffi_moq_ffi_rust_future_poll_rust_buffer, ffi_moq_ffi_rust_future_complete_rust_buffer, ffi_moq_ffi_rust_future_free_rust_buffer, - FfiConverterOptionalMoqAnnounceEvent.lift, + FfiConverterOptionalMoqCatalog.lift, moqExceptionErrorHandler, ); } } -class FfiConverterMoqAnnounceConsumer { - static MoqAnnounceConsumer lift(Pointer ptr) { - return MoqAnnounceConsumer.lift(ptr); +class FfiConverterMoqMediaCatalogConsumer { + static MoqMediaCatalogConsumer lift(Pointer ptr) { + return MoqMediaCatalogConsumer.lift(ptr); } - static Pointer lower(MoqAnnounceConsumer value) { + static Pointer lower(MoqMediaCatalogConsumer value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqAnnounceConsumer value) { + static int allocationSize(MoqMediaCatalogConsumer value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqAnnounceConsumer.lift(pointer), 8); + return LiftRetVal(MoqMediaCatalogConsumer.lift(pointer), 8); } - static int write(MoqAnnounceConsumer value, Uint8List buf) { + static int write(MoqMediaCatalogConsumer value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; } } -abstract class MoqAnnouncedBroadcastInterface { - Future available(); - void cancel(); +abstract class MoqMediaCatalogProducerInterface { + void removeSection({required String name}); + void setSection({required String name, required String json}); + void setVideoProperties({required MoqVideoProperties properties}); } -final _MoqAnnouncedBroadcastFinalizer = Finalizer>((ptr) { +final _MoqMediaCatalogProducerFinalizer = Finalizer>((ptr) { rustCall( - (status) => uniffi_moq_ffi_fn_free_moqannouncedbroadcast(ptr, status), + (status) => uniffi_moq_ffi_fn_free_moqmediacatalogproducer(ptr, status), ); }); -class MoqAnnouncedBroadcast implements MoqAnnouncedBroadcastInterface { +class MoqMediaCatalogProducer implements MoqMediaCatalogProducerInterface { late final Pointer _ptr; - MoqAnnouncedBroadcast._(this._ptr) { - _MoqAnnouncedBroadcastFinalizer.attach(this, _ptr, detach: this); + MoqMediaCatalogProducer._(this._ptr) { + _MoqMediaCatalogProducerFinalizer.attach(this, _ptr, detach: this); } - factory MoqAnnouncedBroadcast.lift(Pointer ptr) { - return MoqAnnouncedBroadcast._(ptr); + MoqMediaCatalogProducer({required MoqBroadcastProducer broadcast}) + : _ptr = rustCall( + (status) => uniffi_moq_ffi_fn_constructor_moqmediacatalogproducer_new( + FfiConverterMoqBroadcastProducer.lower(broadcast), + status, + ), + moqExceptionErrorHandler, + ) { + _MoqMediaCatalogProducerFinalizer.attach(this, _ptr, detach: this); + } + factory MoqMediaCatalogProducer.lift(Pointer ptr) { + return MoqMediaCatalogProducer._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqannouncedbroadcast(_ptr, status), + (status) => uniffi_moq_ffi_fn_clone_moqmediacatalogproducer(_ptr, status), ); } void dispose() { - _MoqAnnouncedBroadcastFinalizer.detach(this); + _MoqMediaCatalogProducerFinalizer.detach(this); rustCall( - (status) => uniffi_moq_ffi_fn_free_moqannouncedbroadcast(_ptr, status), + (status) => uniffi_moq_ffi_fn_free_moqmediacatalogproducer(_ptr, status), ); } - Future available() { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqannouncedbroadcast_available( + void removeSection({required String name}) { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqmediacatalogproducer_remove_section( uniffiClonePointer(), - ), - ffi_moq_ffi_rust_future_poll_u64, - ffi_moq_ffi_rust_future_complete_u64, - ffi_moq_ffi_rust_future_free_u64, - (ptr) => - FfiConverterMoqBroadcastConsumer.lift(Pointer.fromAddress(ptr)), - moqExceptionErrorHandler, - ); + FfiConverterString.lower(name), + status, + ); + }, moqExceptionErrorHandler); } - void cancel() { + void setSection({required String name, required String json}) { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqannouncedbroadcast_cancel( + uniffi_moq_ffi_fn_method_moqmediacatalogproducer_set_section( uniffiClonePointer(), + FfiConverterString.lower(name), + FfiConverterString.lower(json), status, ); - }, null); + }, moqExceptionErrorHandler); + } + + void setVideoProperties({required MoqVideoProperties properties}) { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqmediacatalogproducer_set_video_properties( + uniffiClonePointer(), + FfiConverterMoqVideoProperties.lower(properties), + status, + ); + }, moqExceptionErrorHandler); } } -class FfiConverterMoqAnnouncedBroadcast { - static MoqAnnouncedBroadcast lift(Pointer ptr) { - return MoqAnnouncedBroadcast.lift(ptr); +class FfiConverterMoqMediaCatalogProducer { + static MoqMediaCatalogProducer lift(Pointer ptr) { + return MoqMediaCatalogProducer.lift(ptr); } - static Pointer lower(MoqAnnouncedBroadcast value) { + static Pointer lower(MoqMediaCatalogProducer value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqAnnouncedBroadcast value) { + static int allocationSize(MoqMediaCatalogProducer value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqAnnouncedBroadcast.lift(pointer), 8); + return LiftRetVal(MoqMediaCatalogProducer.lift(pointer), 8); } - static int write(MoqAnnouncedBroadcast value, Uint8List buf) { + static int write(MoqMediaCatalogProducer value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; } } -abstract class MoqBroadcastRequestInterface { - void accept({required MoqBroadcastProducer broadcast}); - String path(); - void reject({required int errorCode}); +abstract class MoqMediaContainerConsumerInterface { + void cancel(); + Future next(); } -final _MoqBroadcastRequestFinalizer = Finalizer>((ptr) { - rustCall((status) => uniffi_moq_ffi_fn_free_moqbroadcastrequest(ptr, status)); +final _MoqMediaContainerConsumerFinalizer = Finalizer>((ptr) { + rustCall( + (status) => uniffi_moq_ffi_fn_free_moqmediacontainerconsumer(ptr, status), + ); }); -class MoqBroadcastRequest implements MoqBroadcastRequestInterface { +class MoqMediaContainerConsumer implements MoqMediaContainerConsumerInterface { late final Pointer _ptr; - MoqBroadcastRequest._(this._ptr) { - _MoqBroadcastRequestFinalizer.attach(this, _ptr, detach: this); + MoqMediaContainerConsumer._(this._ptr) { + _MoqMediaContainerConsumerFinalizer.attach(this, _ptr, detach: this); } - factory MoqBroadcastRequest.lift(Pointer ptr) { - return MoqBroadcastRequest._(ptr); + static Future subscribe({ + required MoqBroadcastConsumer broadcast, + required MoqMediaContainerConfig config, + }) { + return uniffiRustCallAsync( + () => uniffi_moq_ffi_fn_constructor_moqmediacontainerconsumer_subscribe( + FfiConverterMoqBroadcastConsumer.lower(broadcast), + FfiConverterMoqMediaContainerConfig.lower(config), + ), + ffi_moq_ffi_rust_future_poll_u64, + ffi_moq_ffi_rust_future_complete_u64, + ffi_moq_ffi_rust_future_free_u64, + (int handle) => + MoqMediaContainerConsumer._(Pointer.fromAddress(handle)), + moqExceptionErrorHandler, + ); + } + + factory MoqMediaContainerConsumer.lift(Pointer ptr) { + return MoqMediaContainerConsumer._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqbroadcastrequest(_ptr, status), + (status) => + uniffi_moq_ffi_fn_clone_moqmediacontainerconsumer(_ptr, status), ); } void dispose() { - _MoqBroadcastRequestFinalizer.detach(this); + _MoqMediaContainerConsumerFinalizer.detach(this); rustCall( - (status) => uniffi_moq_ffi_fn_free_moqbroadcastrequest(_ptr, status), + (status) => + uniffi_moq_ffi_fn_free_moqmediacontainerconsumer(_ptr, status), ); } - void accept({required MoqBroadcastProducer broadcast}) { + void cancel() { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqbroadcastrequest_accept( + uniffi_moq_ffi_fn_method_moqmediacontainerconsumer_cancel( uniffiClonePointer(), - FfiConverterMoqBroadcastProducer.lower(broadcast), status, ); - }, moqExceptionErrorHandler); + }, null); } - String path() { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqbroadcastrequest_path( + Future next() { + return uniffiRustCallAsync( + () => uniffi_moq_ffi_fn_method_moqmediacontainerconsumer_next( uniffiClonePointer(), - status, ), - FfiConverterString.lift, + ffi_moq_ffi_rust_future_poll_rust_buffer, + ffi_moq_ffi_rust_future_complete_rust_buffer, + ffi_moq_ffi_rust_future_free_rust_buffer, + FfiConverterOptionalMoqMediaFrame.lift, moqExceptionErrorHandler, ); } - - void reject({required int errorCode}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqbroadcastrequest_reject( - uniffiClonePointer(), - FfiConverterUInt16.lower(errorCode), - status, - ); - }, moqExceptionErrorHandler); - } } -class FfiConverterMoqBroadcastRequest { - static MoqBroadcastRequest lift(Pointer ptr) { - return MoqBroadcastRequest.lift(ptr); +class FfiConverterMoqMediaContainerConsumer { + static MoqMediaContainerConsumer lift(Pointer ptr) { + return MoqMediaContainerConsumer.lift(ptr); } - static Pointer lower(MoqBroadcastRequest value) { + static Pointer lower(MoqMediaContainerConsumer value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqBroadcastRequest value) { + static int allocationSize(MoqMediaContainerConsumer value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqBroadcastRequest.lift(pointer), 8); + return LiftRetVal(MoqMediaContainerConsumer.lift(pointer), 8); } - static int write(MoqBroadcastRequest value, Uint8List buf) { + static int write(MoqMediaContainerConsumer value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; } } -abstract class MoqOriginConsumerInterface { - MoqAnnounceConsumer announced({required MoqAnnounceConfig config}); - MoqAnnouncedBroadcast announcedBroadcast({required String path}); - Future requestBroadcast({required String path}); +abstract class MoqMediaContainerGroupConsumerInterface { + void cancel(); + Future next(); + int sequence(); } -final _MoqOriginConsumerFinalizer = Finalizer>((ptr) { - rustCall((status) => uniffi_moq_ffi_fn_free_moqoriginconsumer(ptr, status)); +final _MoqMediaContainerGroupConsumerFinalizer = Finalizer>(( + ptr, +) { + rustCall( + (status) => + uniffi_moq_ffi_fn_free_moqmediacontainergroupconsumer(ptr, status), + ); }); -class MoqOriginConsumer implements MoqOriginConsumerInterface { +class MoqMediaContainerGroupConsumer + implements MoqMediaContainerGroupConsumerInterface { late final Pointer _ptr; - MoqOriginConsumer._(this._ptr) { - _MoqOriginConsumerFinalizer.attach(this, _ptr, detach: this); + MoqMediaContainerGroupConsumer._(this._ptr) { + _MoqMediaContainerGroupConsumerFinalizer.attach(this, _ptr, detach: this); } - factory MoqOriginConsumer.lift(Pointer ptr) { - return MoqOriginConsumer._(ptr); + static Future fetch({ + required MoqBroadcastConsumer broadcast, + required MoqMediaContainerGroupConfig config, + }) { + return uniffiRustCallAsync( + () => uniffi_moq_ffi_fn_constructor_moqmediacontainergroupconsumer_fetch( + FfiConverterMoqBroadcastConsumer.lower(broadcast), + FfiConverterMoqMediaContainerGroupConfig.lower(config), + ), + ffi_moq_ffi_rust_future_poll_u64, + ffi_moq_ffi_rust_future_complete_u64, + ffi_moq_ffi_rust_future_free_u64, + (int handle) => + MoqMediaContainerGroupConsumer._(Pointer.fromAddress(handle)), + moqExceptionErrorHandler, + ); + } + + factory MoqMediaContainerGroupConsumer.lift(Pointer ptr) { + return MoqMediaContainerGroupConsumer._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqoriginconsumer(_ptr, status), + (status) => + uniffi_moq_ffi_fn_clone_moqmediacontainergroupconsumer(_ptr, status), ); } void dispose() { - _MoqOriginConsumerFinalizer.detach(this); + _MoqMediaContainerGroupConsumerFinalizer.detach(this); rustCall( - (status) => uniffi_moq_ffi_fn_free_moqoriginconsumer(_ptr, status), + (status) => + uniffi_moq_ffi_fn_free_moqmediacontainergroupconsumer(_ptr, status), ); } - MoqAnnounceConsumer announced({required MoqAnnounceConfig config}) { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqoriginconsumer_announced( + void cancel() { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqmediacontainergroupconsumer_cancel( uniffiClonePointer(), - FfiConverterMoqAnnounceConfig.lower(config), status, + ); + }, null); + } + + Future next() { + return uniffiRustCallAsync( + () => uniffi_moq_ffi_fn_method_moqmediacontainergroupconsumer_next( + uniffiClonePointer(), ), - FfiConverterMoqAnnounceConsumer.lift, + ffi_moq_ffi_rust_future_poll_rust_buffer, + ffi_moq_ffi_rust_future_complete_rust_buffer, + ffi_moq_ffi_rust_future_free_rust_buffer, + FfiConverterOptionalMoqMediaFrame.lift, moqExceptionErrorHandler, ); } - MoqAnnouncedBroadcast announcedBroadcast({required String path}) { + int sequence() { return rustCallWithLifter( (status) => - uniffi_moq_ffi_fn_method_moqoriginconsumer_announced_broadcast( + uniffi_moq_ffi_fn_method_moqmediacontainergroupconsumer_sequence( uniffiClonePointer(), - FfiConverterString.lower(path), status, ), - FfiConverterMoqAnnouncedBroadcast.lift, - moqExceptionErrorHandler, - ); - } - - Future requestBroadcast({required String path}) { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqoriginconsumer_request_broadcast( - uniffiClonePointer(), - FfiConverterString.lower(path), - ), - ffi_moq_ffi_rust_future_poll_u64, - ffi_moq_ffi_rust_future_complete_u64, - ffi_moq_ffi_rust_future_free_u64, - (ptr) => - FfiConverterMoqBroadcastConsumer.lift(Pointer.fromAddress(ptr)), - moqExceptionErrorHandler, + FfiConverterUInt64.lift, + null, ); } } -class FfiConverterMoqOriginConsumer { - static MoqOriginConsumer lift(Pointer ptr) { - return MoqOriginConsumer.lift(ptr); +class FfiConverterMoqMediaContainerGroupConsumer { + static MoqMediaContainerGroupConsumer lift(Pointer ptr) { + return MoqMediaContainerGroupConsumer.lift(ptr); } - static Pointer lower(MoqOriginConsumer value) { + static Pointer lower(MoqMediaContainerGroupConsumer value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqOriginConsumer value) { + static int allocationSize(MoqMediaContainerGroupConsumer value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqOriginConsumer.lift(pointer), 8); + return LiftRetVal(MoqMediaContainerGroupConsumer.lift(pointer), 8); } - static int write(MoqOriginConsumer value, Uint8List buf) { + static int write(MoqMediaContainerGroupConsumer value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; } } -abstract class MoqOriginDynamicInterface { - void cancel(); - Future requestedBroadcast(); - void update({required MoqRoute route}); +abstract class MoqMediaContainerProducerInterface { + void cut(); + void finish(); + void seek({required int sequence}); + void write({required Uint8List payload}); } -final _MoqOriginDynamicFinalizer = Finalizer>((ptr) { - rustCall((status) => uniffi_moq_ffi_fn_free_moqorigindynamic(ptr, status)); +final _MoqMediaContainerProducerFinalizer = Finalizer>((ptr) { + rustCall( + (status) => uniffi_moq_ffi_fn_free_moqmediacontainerproducer(ptr, status), + ); }); -class MoqOriginDynamic implements MoqOriginDynamicInterface { +class MoqMediaContainerProducer implements MoqMediaContainerProducerInterface { late final Pointer _ptr; - MoqOriginDynamic._(this._ptr) { - _MoqOriginDynamicFinalizer.attach(this, _ptr, detach: this); - } - factory MoqOriginDynamic.lift(Pointer ptr) { - return MoqOriginDynamic._(ptr); + MoqMediaContainerProducer._(this._ptr) { + _MoqMediaContainerProducerFinalizer.attach(this, _ptr, detach: this); + } + MoqMediaContainerProducer({ + required MoqBroadcastProducer broadcast, + required MoqContainerInit init, + }) : _ptr = rustCall( + (status) => + uniffi_moq_ffi_fn_constructor_moqmediacontainerproducer_new( + FfiConverterMoqBroadcastProducer.lower(broadcast), + FfiConverterMoqContainerInit.lower(init), + status, + ), + moqExceptionErrorHandler, + ) { + _MoqMediaContainerProducerFinalizer.attach(this, _ptr, detach: this); + } + factory MoqMediaContainerProducer.lift(Pointer ptr) { + return MoqMediaContainerProducer._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqorigindynamic(_ptr, status), + (status) => + uniffi_moq_ffi_fn_clone_moqmediacontainerproducer(_ptr, status), ); } void dispose() { - _MoqOriginDynamicFinalizer.detach(this); - rustCall((status) => uniffi_moq_ffi_fn_free_moqorigindynamic(_ptr, status)); + _MoqMediaContainerProducerFinalizer.detach(this); + rustCall( + (status) => + uniffi_moq_ffi_fn_free_moqmediacontainerproducer(_ptr, status), + ); } - void cancel() { + void cut() { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqorigindynamic_cancel( + uniffi_moq_ffi_fn_method_moqmediacontainerproducer_cut( uniffiClonePointer(), status, ); - }, null); + }, moqExceptionErrorHandler); } - Future requestedBroadcast() { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqorigindynamic_requested_broadcast( + void finish() { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqmediacontainerproducer_finish( uniffiClonePointer(), - ), - ffi_moq_ffi_rust_future_poll_u64, - ffi_moq_ffi_rust_future_complete_u64, - ffi_moq_ffi_rust_future_free_u64, - (ptr) => - FfiConverterMoqBroadcastRequest.lift(Pointer.fromAddress(ptr)), - moqExceptionErrorHandler, - ); + status, + ); + }, moqExceptionErrorHandler); } - void update({required MoqRoute route}) { + void seek({required int sequence}) { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqorigindynamic_update( + uniffi_moq_ffi_fn_method_moqmediacontainerproducer_seek( uniffiClonePointer(), - FfiConverterMoqRoute.lower(route), + FfiConverterUInt64.lower(sequence), + status, + ); + }, moqExceptionErrorHandler); + } + + void write({required Uint8List payload}) { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqmediacontainerproducer_write( + uniffiClonePointer(), + FfiConverterUint8List.lower(payload), status, ); }, moqExceptionErrorHandler); } } -class FfiConverterMoqOriginDynamic { - static MoqOriginDynamic lift(Pointer ptr) { - return MoqOriginDynamic.lift(ptr); +class FfiConverterMoqMediaContainerProducer { + static MoqMediaContainerProducer lift(Pointer ptr) { + return MoqMediaContainerProducer.lift(ptr); } - static Pointer lower(MoqOriginDynamic value) { + static Pointer lower(MoqMediaContainerProducer value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqOriginDynamic value) { + static int allocationSize(MoqMediaContainerProducer value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqOriginDynamic.lift(pointer), 8); + return LiftRetVal(MoqMediaContainerProducer.lift(pointer), 8); } - static int write(MoqOriginDynamic value, Uint8List buf) { + static int write(MoqMediaContainerProducer value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; } } -abstract class MoqOriginProducerInterface { - MoqOriginConsumer consume(); - MoqBroadcastProducer createBroadcast({required String path}); - MoqOriginDynamic dynamic_({required String prefix, required MoqRoute route}); +abstract class MoqMediaContainerStreamProducerInterface { + void finish(); + void write({required Uint8List payload}); } -final _MoqOriginProducerFinalizer = Finalizer>((ptr) { - rustCall((status) => uniffi_moq_ffi_fn_free_moqoriginproducer(ptr, status)); +final _MoqMediaContainerStreamProducerFinalizer = Finalizer>(( + ptr, +) { + rustCall( + (status) => + uniffi_moq_ffi_fn_free_moqmediacontainerstreamproducer(ptr, status), + ); }); -class MoqOriginProducer implements MoqOriginProducerInterface { +class MoqMediaContainerStreamProducer + implements MoqMediaContainerStreamProducerInterface { late final Pointer _ptr; - MoqOriginProducer._(this._ptr) { - _MoqOriginProducerFinalizer.attach(this, _ptr, detach: this); - } - MoqOriginProducer({required MoqOriginConfig config}) - : _ptr = rustCall( - (status) => uniffi_moq_ffi_fn_constructor_moqoriginproducer_new( - FfiConverterMoqOriginConfig.lower(config), - status, - ), - null, - ) { - _MoqOriginProducerFinalizer.attach(this, _ptr, detach: this); + MoqMediaContainerStreamProducer._(this._ptr) { + _MoqMediaContainerStreamProducerFinalizer.attach(this, _ptr, detach: this); } - factory MoqOriginProducer.lift(Pointer ptr) { - return MoqOriginProducer._(ptr); + MoqMediaContainerStreamProducer({ + required MoqBroadcastProducer broadcast, + required MoqContainerFormat format, + }) : _ptr = rustCall( + (status) => + uniffi_moq_ffi_fn_constructor_moqmediacontainerstreamproducer_new( + FfiConverterMoqBroadcastProducer.lower(broadcast), + FfiConverterMoqContainerFormat.lower(format), + status, + ), + moqExceptionErrorHandler, + ) { + _MoqMediaContainerStreamProducerFinalizer.attach(this, _ptr, detach: this); + } + factory MoqMediaContainerStreamProducer.lift(Pointer ptr) { + return MoqMediaContainerStreamProducer._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqoriginproducer(_ptr, status), + (status) => + uniffi_moq_ffi_fn_clone_moqmediacontainerstreamproducer(_ptr, status), ); } void dispose() { - _MoqOriginProducerFinalizer.detach(this); + _MoqMediaContainerStreamProducerFinalizer.detach(this); rustCall( - (status) => uniffi_moq_ffi_fn_free_moqoriginproducer(_ptr, status), + (status) => + uniffi_moq_ffi_fn_free_moqmediacontainerstreamproducer(_ptr, status), ); } - MoqOriginConsumer consume() { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqoriginproducer_consume( + void finish() { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqmediacontainerstreamproducer_finish( uniffiClonePointer(), status, - ), - FfiConverterMoqOriginConsumer.lift, - null, - ); - } - - MoqBroadcastProducer createBroadcast({required String path}) { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqoriginproducer_create_broadcast( - uniffiClonePointer(), - FfiConverterString.lower(path), - status, - ), - FfiConverterMoqBroadcastProducer.lift, - moqExceptionErrorHandler, - ); + ); + }, moqExceptionErrorHandler); } - MoqOriginDynamic dynamic_({required String prefix, required MoqRoute route}) { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqoriginproducer_dynamic( + void write({required Uint8List payload}) { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqmediacontainerstreamproducer_write( uniffiClonePointer(), - FfiConverterString.lower(prefix), - FfiConverterMoqRoute.lower(route), + FfiConverterUint8List.lower(payload), status, - ), - FfiConverterMoqOriginDynamic.lift, - moqExceptionErrorHandler, - ); + ); + }, moqExceptionErrorHandler); } } -class FfiConverterMoqOriginProducer { - static MoqOriginProducer lift(Pointer ptr) { - return MoqOriginProducer.lift(ptr); +class FfiConverterMoqMediaContainerStreamProducer { + static MoqMediaContainerStreamProducer lift(Pointer ptr) { + return MoqMediaContainerStreamProducer.lift(ptr); } - static Pointer lower(MoqOriginProducer value) { + static Pointer lower(MoqMediaContainerStreamProducer value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqOriginProducer value) { + static int allocationSize(MoqMediaContainerStreamProducer value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqOriginProducer.lift(pointer), 8); + return LiftRetVal(MoqMediaContainerStreamProducer.lift(pointer), 8); } - static int write(MoqOriginProducer value, Uint8List buf) { + static int write(MoqMediaContainerStreamProducer value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; } } -abstract class MoqBroadcastDynamicInterface { - void cancel(); - Future requestedTrack(); +abstract class MoqMediaTrackProducerInterface { + void cut(); + MoqTrackDemand demand(); + void discontinuity(); + void finish(); + void flush({required int timestampUs}); + void seek({required int sequence}); + void writeFrame({required MoqFrame frame}); } -final _MoqBroadcastDynamicFinalizer = Finalizer>((ptr) { - rustCall((status) => uniffi_moq_ffi_fn_free_moqbroadcastdynamic(ptr, status)); +final _MoqMediaTrackProducerFinalizer = Finalizer>((ptr) { + rustCall( + (status) => uniffi_moq_ffi_fn_free_moqmediatrackproducer(ptr, status), + ); }); -class MoqBroadcastDynamic implements MoqBroadcastDynamicInterface { +class MoqMediaTrackProducer implements MoqMediaTrackProducerInterface { late final Pointer _ptr; - MoqBroadcastDynamic._(this._ptr) { - _MoqBroadcastDynamicFinalizer.attach(this, _ptr, detach: this); + MoqMediaTrackProducer._(this._ptr) { + _MoqMediaTrackProducerFinalizer.attach(this, _ptr, detach: this); } - factory MoqBroadcastDynamic.lift(Pointer ptr) { - return MoqBroadcastDynamic._(ptr); + MoqMediaTrackProducer.audio({ + required MoqBroadcastProducer broadcast, + required MoqMediaTarget target, + required MoqAudioInit init, + }) : _ptr = rustCall( + (status) => uniffi_moq_ffi_fn_constructor_moqmediatrackproducer_audio( + FfiConverterMoqBroadcastProducer.lower(broadcast), + FfiConverterMoqMediaTarget.lower(target), + FfiConverterMoqAudioInit.lower(init), + status, + ), + moqExceptionErrorHandler, + ) { + _MoqMediaTrackProducerFinalizer.attach(this, _ptr, detach: this); + } + MoqMediaTrackProducer.video({ + required MoqBroadcastProducer broadcast, + required MoqMediaTarget target, + required MoqVideoInit init, + }) : _ptr = rustCall( + (status) => uniffi_moq_ffi_fn_constructor_moqmediatrackproducer_video( + FfiConverterMoqBroadcastProducer.lower(broadcast), + FfiConverterMoqMediaTarget.lower(target), + FfiConverterMoqVideoInit.lower(init), + status, + ), + moqExceptionErrorHandler, + ) { + _MoqMediaTrackProducerFinalizer.attach(this, _ptr, detach: this); + } + factory MoqMediaTrackProducer.lift(Pointer ptr) { + return MoqMediaTrackProducer._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqbroadcastdynamic(_ptr, status), + (status) => uniffi_moq_ffi_fn_clone_moqmediatrackproducer(_ptr, status), ); } void dispose() { - _MoqBroadcastDynamicFinalizer.detach(this); + _MoqMediaTrackProducerFinalizer.detach(this); rustCall( - (status) => uniffi_moq_ffi_fn_free_moqbroadcastdynamic(_ptr, status), + (status) => uniffi_moq_ffi_fn_free_moqmediatrackproducer(_ptr, status), ); } - void cancel() { + void cut() { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqbroadcastdynamic_cancel( + uniffi_moq_ffi_fn_method_moqmediatrackproducer_cut( uniffiClonePointer(), status, ); - }, null); + }, moqExceptionErrorHandler); } - Future requestedTrack() { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqbroadcastdynamic_requested_track( + MoqTrackDemand demand() { + return rustCallWithLifter( + (status) => uniffi_moq_ffi_fn_method_moqmediatrackproducer_demand( uniffiClonePointer(), + status, ), - ffi_moq_ffi_rust_future_poll_u64, - ffi_moq_ffi_rust_future_complete_u64, - ffi_moq_ffi_rust_future_free_u64, - (ptr) => FfiConverterMoqTrackRequest.lift(Pointer.fromAddress(ptr)), + FfiConverterMoqTrackDemand.lift, moqExceptionErrorHandler, ); } + + void discontinuity() { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqmediatrackproducer_discontinuity( + uniffiClonePointer(), + status, + ); + }, moqExceptionErrorHandler); + } + + void finish() { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqmediatrackproducer_finish( + uniffiClonePointer(), + status, + ); + }, moqExceptionErrorHandler); + } + + void flush({required int timestampUs}) { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqmediatrackproducer_flush( + uniffiClonePointer(), + FfiConverterUInt64.lower(timestampUs), + status, + ); + }, moqExceptionErrorHandler); + } + + void seek({required int sequence}) { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqmediatrackproducer_seek( + uniffiClonePointer(), + FfiConverterUInt64.lower(sequence), + status, + ); + }, moqExceptionErrorHandler); + } + + void writeFrame({required MoqFrame frame}) { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqmediatrackproducer_write_frame( + uniffiClonePointer(), + FfiConverterMoqFrame.lower(frame), + status, + ); + }, moqExceptionErrorHandler); + } } -class FfiConverterMoqBroadcastDynamic { - static MoqBroadcastDynamic lift(Pointer ptr) { - return MoqBroadcastDynamic.lift(ptr); +class FfiConverterMoqMediaTrackProducer { + static MoqMediaTrackProducer lift(Pointer ptr) { + return MoqMediaTrackProducer.lift(ptr); } - static Pointer lower(MoqBroadcastDynamic value) { + static Pointer lower(MoqMediaTrackProducer value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqBroadcastDynamic value) { + static int allocationSize(MoqMediaTrackProducer value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqBroadcastDynamic.lift(pointer), 8); + return LiftRetVal(MoqMediaTrackProducer.lift(pointer), 8); } - static int write(MoqBroadcastDynamic value, Uint8List buf) { + static int write(MoqMediaTrackProducer value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; } } -abstract class MoqBroadcastProducerInterface { - MoqFlateSnapshotProducer publishFlateSnapshot({ - required String name, - required MoqFlateConfig config, - }); - MoqFlateStreamProducer publishFlateStream({ - required String name, - required MoqFlateConfig config, - }); - MoqJsonSnapshotProducer publishJsonSnapshot({ - required String name, - required MoqJsonSnapshotConfig config, - }); - MoqJsonStreamProducer publishJsonStream({ - required String name, - required MoqJsonStreamConfig config, - }); - void announce({required MoqRoute route}); - void close(); - MoqBroadcastConsumer consume(); - MoqBroadcastDynamic dynamic_(); - MoqMediaProducer publishAudio({required MoqAudioInit init}); - MoqMediaProducer publishAudioOnTrack({ - required MoqTrackRequest request, - required MoqAudioInit init, - }); - MoqContainerProducer publishContainer({required MoqContainerInit init}); - MoqContainerStreamProducer publishContainerStream({ - required MoqContainerFormat format, - }); - MoqTrackProducer publishTrack({ - required String name, - required MoqTrackInfo? info, - }); - MoqMediaProducer publishVideo({required MoqVideoInit init}); - MoqMediaProducer publishVideoOnTrack({ - required MoqTrackRequest request, - required MoqVideoInit init, - }); - MoqMediaStreamProducer publishVideoStream({required MoqVideoInit init}); - void removeCatalogSection({required String name}); - void setCatalogSection({required String name, required String json}); - void setVideoProperties({required MoqVideoProperties properties}); - void unannounce(); +abstract class MoqMediaTrackStreamProducerInterface { + MoqTrackDemand demand(); + void finish(); + void write({required Uint8List payload}); } -final _MoqBroadcastProducerFinalizer = Finalizer>((ptr) { +final _MoqMediaTrackStreamProducerFinalizer = Finalizer>((ptr) { rustCall( - (status) => uniffi_moq_ffi_fn_free_moqbroadcastproducer(ptr, status), + (status) => uniffi_moq_ffi_fn_free_moqmediatrackstreamproducer(ptr, status), ); }); -class MoqBroadcastProducer implements MoqBroadcastProducerInterface { +class MoqMediaTrackStreamProducer + implements MoqMediaTrackStreamProducerInterface { late final Pointer _ptr; - MoqBroadcastProducer._(this._ptr) { - _MoqBroadcastProducerFinalizer.attach(this, _ptr, detach: this); - } - MoqBroadcastProducer() - : _ptr = rustCall( - (status) => - uniffi_moq_ffi_fn_constructor_moqbroadcastproducer_new(status), - moqExceptionErrorHandler, - ) { - _MoqBroadcastProducerFinalizer.attach(this, _ptr, detach: this); + MoqMediaTrackStreamProducer._(this._ptr) { + _MoqMediaTrackStreamProducerFinalizer.attach(this, _ptr, detach: this); } - factory MoqBroadcastProducer.lift(Pointer ptr) { - return MoqBroadcastProducer._(ptr); + MoqMediaTrackStreamProducer.video({ + required MoqBroadcastProducer broadcast, + required MoqMediaTarget target, + required MoqVideoInit init, + }) : _ptr = rustCall( + (status) => + uniffi_moq_ffi_fn_constructor_moqmediatrackstreamproducer_video( + FfiConverterMoqBroadcastProducer.lower(broadcast), + FfiConverterMoqMediaTarget.lower(target), + FfiConverterMoqVideoInit.lower(init), + status, + ), + moqExceptionErrorHandler, + ) { + _MoqMediaTrackStreamProducerFinalizer.attach(this, _ptr, detach: this); + } + factory MoqMediaTrackStreamProducer.lift(Pointer ptr) { + return MoqMediaTrackStreamProducer._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqbroadcastproducer(_ptr, status), + (status) => + uniffi_moq_ffi_fn_clone_moqmediatrackstreamproducer(_ptr, status), ); } void dispose() { - _MoqBroadcastProducerFinalizer.detach(this); + _MoqMediaTrackStreamProducerFinalizer.detach(this); rustCall( - (status) => uniffi_moq_ffi_fn_free_moqbroadcastproducer(_ptr, status), - ); - } - - MoqFlateSnapshotProducer publishFlateSnapshot({ - required String name, - required MoqFlateConfig config, - }) { - return rustCallWithLifter( - (status) => - uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_flate_snapshot( - uniffiClonePointer(), - FfiConverterString.lower(name), - FfiConverterMoqFlateConfig.lower(config), - status, - ), - FfiConverterMoqFlateSnapshotProducer.lift, - moqExceptionErrorHandler, - ); - } - - MoqFlateStreamProducer publishFlateStream({ - required String name, - required MoqFlateConfig config, - }) { - return rustCallWithLifter( - (status) => - uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_flate_stream( - uniffiClonePointer(), - FfiConverterString.lower(name), - FfiConverterMoqFlateConfig.lower(config), - status, - ), - FfiConverterMoqFlateStreamProducer.lift, - moqExceptionErrorHandler, - ); - } - - MoqJsonSnapshotProducer publishJsonSnapshot({ - required String name, - required MoqJsonSnapshotConfig config, - }) { - return rustCallWithLifter( (status) => - uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_json_snapshot( - uniffiClonePointer(), - FfiConverterString.lower(name), - FfiConverterMoqJsonSnapshotConfig.lower(config), - status, - ), - FfiConverterMoqJsonSnapshotProducer.lift, - moqExceptionErrorHandler, + uniffi_moq_ffi_fn_free_moqmediatrackstreamproducer(_ptr, status), ); } - MoqJsonStreamProducer publishJsonStream({ - required String name, - required MoqJsonStreamConfig config, - }) { + MoqTrackDemand demand() { return rustCallWithLifter( - (status) => - uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_json_stream( - uniffiClonePointer(), - FfiConverterString.lower(name), - FfiConverterMoqJsonStreamConfig.lower(config), - status, - ), - FfiConverterMoqJsonStreamProducer.lift, + (status) => uniffi_moq_ffi_fn_method_moqmediatrackstreamproducer_demand( + uniffiClonePointer(), + status, + ), + FfiConverterMoqTrackDemand.lift, moqExceptionErrorHandler, ); } - void announce({required MoqRoute route}) { + void finish() { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqbroadcastproducer_announce( + uniffi_moq_ffi_fn_method_moqmediatrackstreamproducer_finish( uniffiClonePointer(), - FfiConverterMoqRoute.lower(route), status, ); }, moqExceptionErrorHandler); } - void close() { + void write({required Uint8List payload}) { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqbroadcastproducer_close( + uniffi_moq_ffi_fn_method_moqmediatrackstreamproducer_write( uniffiClonePointer(), + FfiConverterUint8List.lower(payload), status, ); }, moqExceptionErrorHandler); } +} - MoqBroadcastConsumer consume() { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqbroadcastproducer_consume( - uniffiClonePointer(), - status, - ), - FfiConverterMoqBroadcastConsumer.lift, - moqExceptionErrorHandler, - ); +class FfiConverterMoqMediaTrackStreamProducer { + static MoqMediaTrackStreamProducer lift(Pointer ptr) { + return MoqMediaTrackStreamProducer.lift(ptr); } - MoqBroadcastDynamic dynamic_() { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqbroadcastproducer_dynamic( - uniffiClonePointer(), - status, - ), - FfiConverterMoqBroadcastDynamic.lift, - moqExceptionErrorHandler, - ); + static Pointer lower(MoqMediaTrackStreamProducer value) { + return value.uniffiClonePointer(); } - MoqMediaProducer publishAudio({required MoqAudioInit init}) { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_audio( + static int allocationSize(MoqMediaTrackStreamProducer value) { + return 8; + } + + static LiftRetVal read(Uint8List buf) { + final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); + final pointer = Pointer.fromAddress(handle); + return LiftRetVal(MoqMediaTrackStreamProducer.lift(pointer), 8); + } + + static int write(MoqMediaTrackStreamProducer value, Uint8List buf) { + final handle = lower(value); + buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); + return 8; + } +} + +abstract class MoqAnnounceConsumerInterface { + void cancel(); + Future next(); +} + +final _MoqAnnounceConsumerFinalizer = Finalizer>((ptr) { + rustCall((status) => uniffi_moq_ffi_fn_free_moqannounceconsumer(ptr, status)); +}); + +class MoqAnnounceConsumer implements MoqAnnounceConsumerInterface { + late final Pointer _ptr; + MoqAnnounceConsumer._(this._ptr) { + _MoqAnnounceConsumerFinalizer.attach(this, _ptr, detach: this); + } + factory MoqAnnounceConsumer.lift(Pointer ptr) { + return MoqAnnounceConsumer._(ptr); + } + Pointer uniffiClonePointer() { + return rustCall( + (status) => uniffi_moq_ffi_fn_clone_moqannounceconsumer(_ptr, status), + ); + } + + void dispose() { + _MoqAnnounceConsumerFinalizer.detach(this); + rustCall( + (status) => uniffi_moq_ffi_fn_free_moqannounceconsumer(_ptr, status), + ); + } + + void cancel() { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqannounceconsumer_cancel( uniffiClonePointer(), - FfiConverterMoqAudioInit.lower(init), status, + ); + }, null); + } + + Future next() { + return uniffiRustCallAsync( + () => uniffi_moq_ffi_fn_method_moqannounceconsumer_next( + uniffiClonePointer(), ), - FfiConverterMoqMediaProducer.lift, + ffi_moq_ffi_rust_future_poll_rust_buffer, + ffi_moq_ffi_rust_future_complete_rust_buffer, + ffi_moq_ffi_rust_future_free_rust_buffer, + FfiConverterOptionalMoqAnnounceEvent.lift, moqExceptionErrorHandler, ); } +} - MoqMediaProducer publishAudioOnTrack({ - required MoqTrackRequest request, - required MoqAudioInit init, - }) { - return rustCallWithLifter( - (status) => - uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_audio_on_track( - uniffiClonePointer(), - FfiConverterMoqTrackRequest.lower(request), - FfiConverterMoqAudioInit.lower(init), - status, - ), - FfiConverterMoqMediaProducer.lift, - moqExceptionErrorHandler, - ); +class FfiConverterMoqAnnounceConsumer { + static MoqAnnounceConsumer lift(Pointer ptr) { + return MoqAnnounceConsumer.lift(ptr); } - MoqContainerProducer publishContainer({required MoqContainerInit init}) { - return rustCallWithLifter( - (status) => - uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_container( - uniffiClonePointer(), - FfiConverterMoqContainerInit.lower(init), - status, - ), - FfiConverterMoqContainerProducer.lift, - moqExceptionErrorHandler, + static Pointer lower(MoqAnnounceConsumer value) { + return value.uniffiClonePointer(); + } + + static int allocationSize(MoqAnnounceConsumer value) { + return 8; + } + + static LiftRetVal read(Uint8List buf) { + final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); + final pointer = Pointer.fromAddress(handle); + return LiftRetVal(MoqAnnounceConsumer.lift(pointer), 8); + } + + static int write(MoqAnnounceConsumer value, Uint8List buf) { + final handle = lower(value); + buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); + return 8; + } +} + +abstract class MoqAnnouncedBroadcastInterface { + Future available(); + void cancel(); +} + +final _MoqAnnouncedBroadcastFinalizer = Finalizer>((ptr) { + rustCall( + (status) => uniffi_moq_ffi_fn_free_moqannouncedbroadcast(ptr, status), + ); +}); + +class MoqAnnouncedBroadcast implements MoqAnnouncedBroadcastInterface { + late final Pointer _ptr; + MoqAnnouncedBroadcast._(this._ptr) { + _MoqAnnouncedBroadcastFinalizer.attach(this, _ptr, detach: this); + } + factory MoqAnnouncedBroadcast.lift(Pointer ptr) { + return MoqAnnouncedBroadcast._(ptr); + } + Pointer uniffiClonePointer() { + return rustCall( + (status) => uniffi_moq_ffi_fn_clone_moqannouncedbroadcast(_ptr, status), ); } - MoqContainerStreamProducer publishContainerStream({ - required MoqContainerFormat format, - }) { - return rustCallWithLifter( - (status) => - uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_container_stream( - uniffiClonePointer(), - FfiConverterMoqContainerFormat.lower(format), - status, - ), - FfiConverterMoqContainerStreamProducer.lift, - moqExceptionErrorHandler, + void dispose() { + _MoqAnnouncedBroadcastFinalizer.detach(this); + rustCall( + (status) => uniffi_moq_ffi_fn_free_moqannouncedbroadcast(_ptr, status), ); } - MoqTrackProducer publishTrack({ - required String name, - required MoqTrackInfo? info, - }) { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_track( + Future available() { + return uniffiRustCallAsync( + () => uniffi_moq_ffi_fn_method_moqannouncedbroadcast_available( uniffiClonePointer(), - FfiConverterString.lower(name), - FfiConverterOptionalMoqTrackInfo.lower(info), - status, ), - FfiConverterMoqTrackProducer.lift, + ffi_moq_ffi_rust_future_poll_u64, + ffi_moq_ffi_rust_future_complete_u64, + ffi_moq_ffi_rust_future_free_u64, + (ptr) => + FfiConverterMoqBroadcastConsumer.lift(Pointer.fromAddress(ptr)), moqExceptionErrorHandler, ); } - MoqMediaProducer publishVideo({required MoqVideoInit init}) { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_video( + void cancel() { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqannouncedbroadcast_cancel( uniffiClonePointer(), - FfiConverterMoqVideoInit.lower(init), status, - ), - FfiConverterMoqMediaProducer.lift, - moqExceptionErrorHandler, - ); + ); + }, null); + } +} + +class FfiConverterMoqAnnouncedBroadcast { + static MoqAnnouncedBroadcast lift(Pointer ptr) { + return MoqAnnouncedBroadcast.lift(ptr); } - MoqMediaProducer publishVideoOnTrack({ - required MoqTrackRequest request, - required MoqVideoInit init, - }) { - return rustCallWithLifter( - (status) => - uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_video_on_track( - uniffiClonePointer(), - FfiConverterMoqTrackRequest.lower(request), - FfiConverterMoqVideoInit.lower(init), - status, - ), - FfiConverterMoqMediaProducer.lift, - moqExceptionErrorHandler, + static Pointer lower(MoqAnnouncedBroadcast value) { + return value.uniffiClonePointer(); + } + + static int allocationSize(MoqAnnouncedBroadcast value) { + return 8; + } + + static LiftRetVal read(Uint8List buf) { + final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); + final pointer = Pointer.fromAddress(handle); + return LiftRetVal(MoqAnnouncedBroadcast.lift(pointer), 8); + } + + static int write(MoqAnnouncedBroadcast value, Uint8List buf) { + final handle = lower(value); + buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); + return 8; + } +} + +abstract class MoqBroadcastRequestInterface { + void accept({required MoqBroadcastProducer broadcast}); + String path(); + void reject({required int errorCode}); +} + +final _MoqBroadcastRequestFinalizer = Finalizer>((ptr) { + rustCall((status) => uniffi_moq_ffi_fn_free_moqbroadcastrequest(ptr, status)); +}); + +class MoqBroadcastRequest implements MoqBroadcastRequestInterface { + late final Pointer _ptr; + MoqBroadcastRequest._(this._ptr) { + _MoqBroadcastRequestFinalizer.attach(this, _ptr, detach: this); + } + factory MoqBroadcastRequest.lift(Pointer ptr) { + return MoqBroadcastRequest._(ptr); + } + Pointer uniffiClonePointer() { + return rustCall( + (status) => uniffi_moq_ffi_fn_clone_moqbroadcastrequest(_ptr, status), ); } - MoqMediaStreamProducer publishVideoStream({required MoqVideoInit init}) { - return rustCallWithLifter( - (status) => - uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_video_stream( - uniffiClonePointer(), - FfiConverterMoqVideoInit.lower(init), - status, - ), - FfiConverterMoqMediaStreamProducer.lift, - moqExceptionErrorHandler, + void dispose() { + _MoqBroadcastRequestFinalizer.detach(this); + rustCall( + (status) => uniffi_moq_ffi_fn_free_moqbroadcastrequest(_ptr, status), ); } - void removeCatalogSection({required String name}) { + void accept({required MoqBroadcastProducer broadcast}) { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqbroadcastproducer_remove_catalog_section( + uniffi_moq_ffi_fn_method_moqbroadcastrequest_accept( uniffiClonePointer(), - FfiConverterString.lower(name), + FfiConverterMoqBroadcastProducer.lower(broadcast), status, ); }, moqExceptionErrorHandler); } - void setCatalogSection({required String name, required String json}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqbroadcastproducer_set_catalog_section( + String path() { + return rustCallWithLifter( + (status) => uniffi_moq_ffi_fn_method_moqbroadcastrequest_path( uniffiClonePointer(), - FfiConverterString.lower(name), - FfiConverterString.lower(json), status, - ); - }, moqExceptionErrorHandler); + ), + FfiConverterString.lift, + moqExceptionErrorHandler, + ); } - void setVideoProperties({required MoqVideoProperties properties}) { + void reject({required int errorCode}) { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqbroadcastproducer_set_video_properties( + uniffi_moq_ffi_fn_method_moqbroadcastrequest_reject( uniffiClonePointer(), - FfiConverterMoqVideoProperties.lower(properties), + FfiConverterUInt16.lower(errorCode), status, ); }, moqExceptionErrorHandler); } +} + +class FfiConverterMoqBroadcastRequest { + static MoqBroadcastRequest lift(Pointer ptr) { + return MoqBroadcastRequest.lift(ptr); + } + + static Pointer lower(MoqBroadcastRequest value) { + return value.uniffiClonePointer(); + } + + static int allocationSize(MoqBroadcastRequest value) { + return 8; + } + + static LiftRetVal read(Uint8List buf) { + final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); + final pointer = Pointer.fromAddress(handle); + return LiftRetVal(MoqBroadcastRequest.lift(pointer), 8); + } + + static int write(MoqBroadcastRequest value, Uint8List buf) { + final handle = lower(value); + buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); + return 8; + } +} + +abstract class MoqOriginConsumerInterface { + MoqAnnounceConsumer announced({required MoqAnnounceConfig config}); + MoqAnnouncedBroadcast announcedBroadcast({required String path}); + Future requestBroadcast({required String path}); +} + +final _MoqOriginConsumerFinalizer = Finalizer>((ptr) { + rustCall((status) => uniffi_moq_ffi_fn_free_moqoriginconsumer(ptr, status)); +}); + +class MoqOriginConsumer implements MoqOriginConsumerInterface { + late final Pointer _ptr; + MoqOriginConsumer._(this._ptr) { + _MoqOriginConsumerFinalizer.attach(this, _ptr, detach: this); + } + factory MoqOriginConsumer.lift(Pointer ptr) { + return MoqOriginConsumer._(ptr); + } + Pointer uniffiClonePointer() { + return rustCall( + (status) => uniffi_moq_ffi_fn_clone_moqoriginconsumer(_ptr, status), + ); + } + + void dispose() { + _MoqOriginConsumerFinalizer.detach(this); + rustCall( + (status) => uniffi_moq_ffi_fn_free_moqoriginconsumer(_ptr, status), + ); + } + + MoqAnnounceConsumer announced({required MoqAnnounceConfig config}) { + return rustCallWithLifter( + (status) => uniffi_moq_ffi_fn_method_moqoriginconsumer_announced( + uniffiClonePointer(), + FfiConverterMoqAnnounceConfig.lower(config), + status, + ), + FfiConverterMoqAnnounceConsumer.lift, + moqExceptionErrorHandler, + ); + } + + MoqAnnouncedBroadcast announcedBroadcast({required String path}) { + return rustCallWithLifter( + (status) => + uniffi_moq_ffi_fn_method_moqoriginconsumer_announced_broadcast( + uniffiClonePointer(), + FfiConverterString.lower(path), + status, + ), + FfiConverterMoqAnnouncedBroadcast.lift, + moqExceptionErrorHandler, + ); + } - void unannounce() { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqbroadcastproducer_unannounce( + Future requestBroadcast({required String path}) { + return uniffiRustCallAsync( + () => uniffi_moq_ffi_fn_method_moqoriginconsumer_request_broadcast( uniffiClonePointer(), - status, - ); - }, moqExceptionErrorHandler); + FfiConverterString.lower(path), + ), + ffi_moq_ffi_rust_future_poll_u64, + ffi_moq_ffi_rust_future_complete_u64, + ffi_moq_ffi_rust_future_free_u64, + (ptr) => + FfiConverterMoqBroadcastConsumer.lift(Pointer.fromAddress(ptr)), + moqExceptionErrorHandler, + ); } } -class FfiConverterMoqBroadcastProducer { - static MoqBroadcastProducer lift(Pointer ptr) { - return MoqBroadcastProducer.lift(ptr); +class FfiConverterMoqOriginConsumer { + static MoqOriginConsumer lift(Pointer ptr) { + return MoqOriginConsumer.lift(ptr); } - static Pointer lower(MoqBroadcastProducer value) { + static Pointer lower(MoqOriginConsumer value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqBroadcastProducer value) { + static int allocationSize(MoqOriginConsumer value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqBroadcastProducer.lift(pointer), 8); + return LiftRetVal(MoqOriginConsumer.lift(pointer), 8); } - static int write(MoqBroadcastProducer value, Uint8List buf) { + static int write(MoqOriginConsumer value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; } } -abstract class MoqContainerProducerInterface { - void cut(); - void finish(); - void seek({required int sequence}); - void write({required Uint8List payload}); +abstract class MoqOriginDynamicInterface { + void cancel(); + Future requestedBroadcast(); + void update({required MoqRoute route}); } -final _MoqContainerProducerFinalizer = Finalizer>((ptr) { - rustCall( - (status) => uniffi_moq_ffi_fn_free_moqcontainerproducer(ptr, status), - ); +final _MoqOriginDynamicFinalizer = Finalizer>((ptr) { + rustCall((status) => uniffi_moq_ffi_fn_free_moqorigindynamic(ptr, status)); }); -class MoqContainerProducer implements MoqContainerProducerInterface { +class MoqOriginDynamic implements MoqOriginDynamicInterface { late final Pointer _ptr; - MoqContainerProducer._(this._ptr) { - _MoqContainerProducerFinalizer.attach(this, _ptr, detach: this); + MoqOriginDynamic._(this._ptr) { + _MoqOriginDynamicFinalizer.attach(this, _ptr, detach: this); } - factory MoqContainerProducer.lift(Pointer ptr) { - return MoqContainerProducer._(ptr); + factory MoqOriginDynamic.lift(Pointer ptr) { + return MoqOriginDynamic._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqcontainerproducer(_ptr, status), + (status) => uniffi_moq_ffi_fn_clone_moqorigindynamic(_ptr, status), ); } void dispose() { - _MoqContainerProducerFinalizer.detach(this); - rustCall( - (status) => uniffi_moq_ffi_fn_free_moqcontainerproducer(_ptr, status), - ); - } - - void cut() { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqcontainerproducer_cut( - uniffiClonePointer(), - status, - ); - }, moqExceptionErrorHandler); + _MoqOriginDynamicFinalizer.detach(this); + rustCall((status) => uniffi_moq_ffi_fn_free_moqorigindynamic(_ptr, status)); } - void finish() { + void cancel() { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqcontainerproducer_finish( + uniffi_moq_ffi_fn_method_moqorigindynamic_cancel( uniffiClonePointer(), status, ); - }, moqExceptionErrorHandler); + }, null); } - void seek({required int sequence}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqcontainerproducer_seek( + Future requestedBroadcast() { + return uniffiRustCallAsync( + () => uniffi_moq_ffi_fn_method_moqorigindynamic_requested_broadcast( uniffiClonePointer(), - FfiConverterUInt64.lower(sequence), - status, - ); - }, moqExceptionErrorHandler); + ), + ffi_moq_ffi_rust_future_poll_u64, + ffi_moq_ffi_rust_future_complete_u64, + ffi_moq_ffi_rust_future_free_u64, + (ptr) => + FfiConverterMoqBroadcastRequest.lift(Pointer.fromAddress(ptr)), + moqExceptionErrorHandler, + ); } - void write({required Uint8List payload}) { + void update({required MoqRoute route}) { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqcontainerproducer_write( + uniffi_moq_ffi_fn_method_moqorigindynamic_update( uniffiClonePointer(), - FfiConverterUint8List.lower(payload), + FfiConverterMoqRoute.lower(route), status, ); }, moqExceptionErrorHandler); } } -class FfiConverterMoqContainerProducer { - static MoqContainerProducer lift(Pointer ptr) { - return MoqContainerProducer.lift(ptr); +class FfiConverterMoqOriginDynamic { + static MoqOriginDynamic lift(Pointer ptr) { + return MoqOriginDynamic.lift(ptr); } - static Pointer lower(MoqContainerProducer value) { + static Pointer lower(MoqOriginDynamic value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqContainerProducer value) { + static int allocationSize(MoqOriginDynamic value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqContainerProducer.lift(pointer), 8); + return LiftRetVal(MoqOriginDynamic.lift(pointer), 8); } - static int write(MoqContainerProducer value, Uint8List buf) { + static int write(MoqOriginDynamic value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; } } -abstract class MoqContainerStreamProducerInterface { - void finish(); - void write({required Uint8List payload}); +abstract class MoqOriginProducerInterface { + MoqOriginConsumer consume(); + MoqBroadcastProducer createBroadcast({required String path}); + MoqOriginDynamic dynamic_({required String prefix, required MoqRoute route}); } -final _MoqContainerStreamProducerFinalizer = Finalizer>((ptr) { - rustCall( - (status) => uniffi_moq_ffi_fn_free_moqcontainerstreamproducer(ptr, status), - ); +final _MoqOriginProducerFinalizer = Finalizer>((ptr) { + rustCall((status) => uniffi_moq_ffi_fn_free_moqoriginproducer(ptr, status)); }); -class MoqContainerStreamProducer - implements MoqContainerStreamProducerInterface { +class MoqOriginProducer implements MoqOriginProducerInterface { late final Pointer _ptr; - MoqContainerStreamProducer._(this._ptr) { - _MoqContainerStreamProducerFinalizer.attach(this, _ptr, detach: this); + MoqOriginProducer._(this._ptr) { + _MoqOriginProducerFinalizer.attach(this, _ptr, detach: this); + } + MoqOriginProducer({required MoqOriginConfig config}) + : _ptr = rustCall( + (status) => uniffi_moq_ffi_fn_constructor_moqoriginproducer_new( + FfiConverterMoqOriginConfig.lower(config), + status, + ), + null, + ) { + _MoqOriginProducerFinalizer.attach(this, _ptr, detach: this); } - factory MoqContainerStreamProducer.lift(Pointer ptr) { - return MoqContainerStreamProducer._(ptr); + factory MoqOriginProducer.lift(Pointer ptr) { + return MoqOriginProducer._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => - uniffi_moq_ffi_fn_clone_moqcontainerstreamproducer(_ptr, status), + (status) => uniffi_moq_ffi_fn_clone_moqoriginproducer(_ptr, status), ); } void dispose() { - _MoqContainerStreamProducerFinalizer.detach(this); + _MoqOriginProducerFinalizer.detach(this); rustCall( - (status) => - uniffi_moq_ffi_fn_free_moqcontainerstreamproducer(_ptr, status), + (status) => uniffi_moq_ffi_fn_free_moqoriginproducer(_ptr, status), ); } - void finish() { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqcontainerstreamproducer_finish( + MoqOriginConsumer consume() { + return rustCallWithLifter( + (status) => uniffi_moq_ffi_fn_method_moqoriginproducer_consume( uniffiClonePointer(), status, - ); - }, moqExceptionErrorHandler); + ), + FfiConverterMoqOriginConsumer.lift, + null, + ); } - void write({required Uint8List payload}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqcontainerstreamproducer_write( + MoqBroadcastProducer createBroadcast({required String path}) { + return rustCallWithLifter( + (status) => uniffi_moq_ffi_fn_method_moqoriginproducer_create_broadcast( uniffiClonePointer(), - FfiConverterUint8List.lower(payload), + FfiConverterString.lower(path), status, - ); - }, moqExceptionErrorHandler); + ), + FfiConverterMoqBroadcastProducer.lift, + moqExceptionErrorHandler, + ); + } + + MoqOriginDynamic dynamic_({required String prefix, required MoqRoute route}) { + return rustCallWithLifter( + (status) => uniffi_moq_ffi_fn_method_moqoriginproducer_dynamic( + uniffiClonePointer(), + FfiConverterString.lower(prefix), + FfiConverterMoqRoute.lower(route), + status, + ), + FfiConverterMoqOriginDynamic.lift, + moqExceptionErrorHandler, + ); } } -class FfiConverterMoqContainerStreamProducer { - static MoqContainerStreamProducer lift(Pointer ptr) { - return MoqContainerStreamProducer.lift(ptr); +class FfiConverterMoqOriginProducer { + static MoqOriginProducer lift(Pointer ptr) { + return MoqOriginProducer.lift(ptr); } - static Pointer lower(MoqContainerStreamProducer value) { + static Pointer lower(MoqOriginProducer value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqContainerStreamProducer value) { + static int allocationSize(MoqOriginProducer value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqContainerStreamProducer.lift(pointer), 8); + return LiftRetVal(MoqOriginProducer.lift(pointer), 8); } - static int write(MoqContainerStreamProducer value, Uint8List buf) { + static int write(MoqOriginProducer value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; } } -abstract class MoqGroupProducerInterface { - void abort({required int errorCode}); - MoqGroupConsumer consume(); - void finish(); - int sequence(); - void writeFrame({required MoqFrame frame}); +abstract class MoqBroadcastDynamicInterface { + void cancel(); + Future requestedTrack(); } -final _MoqGroupProducerFinalizer = Finalizer>((ptr) { - rustCall((status) => uniffi_moq_ffi_fn_free_moqgroupproducer(ptr, status)); +final _MoqBroadcastDynamicFinalizer = Finalizer>((ptr) { + rustCall((status) => uniffi_moq_ffi_fn_free_moqbroadcastdynamic(ptr, status)); }); -class MoqGroupProducer implements MoqGroupProducerInterface { +class MoqBroadcastDynamic implements MoqBroadcastDynamicInterface { late final Pointer _ptr; - MoqGroupProducer._(this._ptr) { - _MoqGroupProducerFinalizer.attach(this, _ptr, detach: this); + MoqBroadcastDynamic._(this._ptr) { + _MoqBroadcastDynamicFinalizer.attach(this, _ptr, detach: this); } - factory MoqGroupProducer.lift(Pointer ptr) { - return MoqGroupProducer._(ptr); + factory MoqBroadcastDynamic.lift(Pointer ptr) { + return MoqBroadcastDynamic._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqgroupproducer(_ptr, status), + (status) => uniffi_moq_ffi_fn_clone_moqbroadcastdynamic(_ptr, status), ); } void dispose() { - _MoqGroupProducerFinalizer.detach(this); - rustCall((status) => uniffi_moq_ffi_fn_free_moqgroupproducer(_ptr, status)); - } - - void abort({required int errorCode}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqgroupproducer_abort( - uniffiClonePointer(), - FfiConverterUInt16.lower(errorCode), - status, - ); - }, moqExceptionErrorHandler); - } - - MoqGroupConsumer consume() { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqgroupproducer_consume( - uniffiClonePointer(), - status, - ), - FfiConverterMoqGroupConsumer.lift, - moqExceptionErrorHandler, - ); - } - - void finish() { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqgroupproducer_finish( - uniffiClonePointer(), - status, - ); - }, moqExceptionErrorHandler); - } - - int sequence() { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqgroupproducer_sequence( - uniffiClonePointer(), - status, - ), - FfiConverterUInt64.lift, - null, + _MoqBroadcastDynamicFinalizer.detach(this); + rustCall( + (status) => uniffi_moq_ffi_fn_free_moqbroadcastdynamic(_ptr, status), ); } - void writeFrame({required MoqFrame frame}) { + void cancel() { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqgroupproducer_write_frame( + uniffi_moq_ffi_fn_method_moqbroadcastdynamic_cancel( uniffiClonePointer(), - FfiConverterMoqFrame.lower(frame), status, ); - }, moqExceptionErrorHandler); + }, null); + } + + Future requestedTrack() { + return uniffiRustCallAsync( + () => uniffi_moq_ffi_fn_method_moqbroadcastdynamic_requested_track( + uniffiClonePointer(), + ), + ffi_moq_ffi_rust_future_poll_u64, + ffi_moq_ffi_rust_future_complete_u64, + ffi_moq_ffi_rust_future_free_u64, + (ptr) => FfiConverterMoqTrackRequest.lift(Pointer.fromAddress(ptr)), + moqExceptionErrorHandler, + ); } } -class FfiConverterMoqGroupProducer { - static MoqGroupProducer lift(Pointer ptr) { - return MoqGroupProducer.lift(ptr); +class FfiConverterMoqBroadcastDynamic { + static MoqBroadcastDynamic lift(Pointer ptr) { + return MoqBroadcastDynamic.lift(ptr); } - static Pointer lower(MoqGroupProducer value) { + static Pointer lower(MoqBroadcastDynamic value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqGroupProducer value) { + static int allocationSize(MoqBroadcastDynamic value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqGroupProducer.lift(pointer), 8); + return LiftRetVal(MoqBroadcastDynamic.lift(pointer), 8); } - static int write(MoqGroupProducer value, Uint8List buf) { + static int write(MoqBroadcastDynamic value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; } } -abstract class MoqGroupRequestInterface { - void abort({required int errorCode}); - MoqGroupProducer accept(); - int priority(); - int sequence(); +abstract class MoqBroadcastProducerInterface { + void announce({required MoqRoute route}); + void close(); + MoqBroadcastConsumer consume(); + MoqBroadcastDynamic dynamic_(); + MoqTrackProducer publishTrack({ + required String name, + required MoqTrackInfo? info, + }); + void unannounce(); } -final _MoqGroupRequestFinalizer = Finalizer>((ptr) { - rustCall((status) => uniffi_moq_ffi_fn_free_moqgrouprequest(ptr, status)); +final _MoqBroadcastProducerFinalizer = Finalizer>((ptr) { + rustCall( + (status) => uniffi_moq_ffi_fn_free_moqbroadcastproducer(ptr, status), + ); }); -class MoqGroupRequest implements MoqGroupRequestInterface { +class MoqBroadcastProducer implements MoqBroadcastProducerInterface { late final Pointer _ptr; - MoqGroupRequest._(this._ptr) { - _MoqGroupRequestFinalizer.attach(this, _ptr, detach: this); + MoqBroadcastProducer._(this._ptr) { + _MoqBroadcastProducerFinalizer.attach(this, _ptr, detach: this); } - factory MoqGroupRequest.lift(Pointer ptr) { - return MoqGroupRequest._(ptr); + MoqBroadcastProducer() + : _ptr = rustCall( + (status) => + uniffi_moq_ffi_fn_constructor_moqbroadcastproducer_new(status), + moqExceptionErrorHandler, + ) { + _MoqBroadcastProducerFinalizer.attach(this, _ptr, detach: this); + } + factory MoqBroadcastProducer.lift(Pointer ptr) { + return MoqBroadcastProducer._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqgrouprequest(_ptr, status), + (status) => uniffi_moq_ffi_fn_clone_moqbroadcastproducer(_ptr, status), ); } void dispose() { - _MoqGroupRequestFinalizer.detach(this); - rustCall((status) => uniffi_moq_ffi_fn_free_moqgrouprequest(_ptr, status)); + _MoqBroadcastProducerFinalizer.detach(this); + rustCall( + (status) => uniffi_moq_ffi_fn_free_moqbroadcastproducer(_ptr, status), + ); } - void abort({required int errorCode}) { + void announce({required MoqRoute route}) { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqgrouprequest_abort( + uniffi_moq_ffi_fn_method_moqbroadcastproducer_announce( uniffiClonePointer(), - FfiConverterUInt16.lower(errorCode), + FfiConverterMoqRoute.lower(route), status, ); }, moqExceptionErrorHandler); } - MoqGroupProducer accept() { + void close() { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqbroadcastproducer_close( + uniffiClonePointer(), + status, + ); + }, moqExceptionErrorHandler); + } + + MoqBroadcastConsumer consume() { return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqgrouprequest_accept( + (status) => uniffi_moq_ffi_fn_method_moqbroadcastproducer_consume( uniffiClonePointer(), status, ), - FfiConverterMoqGroupProducer.lift, + FfiConverterMoqBroadcastConsumer.lift, moqExceptionErrorHandler, ); } - int priority() { + MoqBroadcastDynamic dynamic_() { return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqgrouprequest_priority( + (status) => uniffi_moq_ffi_fn_method_moqbroadcastproducer_dynamic( uniffiClonePointer(), status, ), - FfiConverterUInt8.lift, - null, + FfiConverterMoqBroadcastDynamic.lift, + moqExceptionErrorHandler, ); } - int sequence() { + MoqTrackProducer publishTrack({ + required String name, + required MoqTrackInfo? info, + }) { return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqgrouprequest_sequence( + (status) => uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_track( uniffiClonePointer(), + FfiConverterString.lower(name), + FfiConverterOptionalMoqTrackInfo.lower(info), status, ), - FfiConverterUInt64.lift, - null, + FfiConverterMoqTrackProducer.lift, + moqExceptionErrorHandler, ); } + + void unannounce() { + return rustCall((status) { + uniffi_moq_ffi_fn_method_moqbroadcastproducer_unannounce( + uniffiClonePointer(), + status, + ); + }, moqExceptionErrorHandler); + } } -class FfiConverterMoqGroupRequest { - static MoqGroupRequest lift(Pointer ptr) { - return MoqGroupRequest.lift(ptr); +class FfiConverterMoqBroadcastProducer { + static MoqBroadcastProducer lift(Pointer ptr) { + return MoqBroadcastProducer.lift(ptr); } - static Pointer lower(MoqGroupRequest value) { + static Pointer lower(MoqBroadcastProducer value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqGroupRequest value) { + static int allocationSize(MoqBroadcastProducer value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqGroupRequest.lift(pointer), 8); + return LiftRetVal(MoqBroadcastProducer.lift(pointer), 8); } - static int write(MoqGroupRequest value, Uint8List buf) { + static int write(MoqBroadcastProducer value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; } } -abstract class MoqMediaProducerInterface { - void cut(); - MoqTrackDemand demand(); - void discontinuity(); +abstract class MoqGroupProducerInterface { + void abort({required int errorCode}); + MoqGroupConsumer consume(); void finish(); - void flush({required int timestampUs}); - String name(); - void seek({required int sequence}); - Future unused(); - Future used(); + int sequence(); void writeFrame({required MoqFrame frame}); } -final _MoqMediaProducerFinalizer = Finalizer>((ptr) { - rustCall((status) => uniffi_moq_ffi_fn_free_moqmediaproducer(ptr, status)); +final _MoqGroupProducerFinalizer = Finalizer>((ptr) { + rustCall((status) => uniffi_moq_ffi_fn_free_moqgroupproducer(ptr, status)); }); -class MoqMediaProducer implements MoqMediaProducerInterface { +class MoqGroupProducer implements MoqGroupProducerInterface { late final Pointer _ptr; - MoqMediaProducer._(this._ptr) { - _MoqMediaProducerFinalizer.attach(this, _ptr, detach: this); + MoqGroupProducer._(this._ptr) { + _MoqGroupProducerFinalizer.attach(this, _ptr, detach: this); } - factory MoqMediaProducer.lift(Pointer ptr) { - return MoqMediaProducer._(ptr); + factory MoqGroupProducer.lift(Pointer ptr) { + return MoqGroupProducer._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqmediaproducer(_ptr, status), + (status) => uniffi_moq_ffi_fn_clone_moqgroupproducer(_ptr, status), ); } void dispose() { - _MoqMediaProducerFinalizer.detach(this); - rustCall((status) => uniffi_moq_ffi_fn_free_moqmediaproducer(_ptr, status)); + _MoqGroupProducerFinalizer.detach(this); + rustCall((status) => uniffi_moq_ffi_fn_free_moqgroupproducer(_ptr, status)); } - void cut() { + void abort({required int errorCode}) { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqmediaproducer_cut( + uniffi_moq_ffi_fn_method_moqgroupproducer_abort( uniffiClonePointer(), + FfiConverterUInt16.lower(errorCode), status, ); }, moqExceptionErrorHandler); } - MoqTrackDemand demand() { + MoqGroupConsumer consume() { return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqmediaproducer_demand( + (status) => uniffi_moq_ffi_fn_method_moqgroupproducer_consume( uniffiClonePointer(), status, ), - FfiConverterMoqTrackDemand.lift, + FfiConverterMoqGroupConsumer.lift, moqExceptionErrorHandler, ); } - void discontinuity() { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqmediaproducer_discontinuity( - uniffiClonePointer(), - status, - ); - }, moqExceptionErrorHandler); - } - void finish() { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqmediaproducer_finish( - uniffiClonePointer(), - status, - ); - }, moqExceptionErrorHandler); - } - - void flush({required int timestampUs}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqmediaproducer_flush( + uniffi_moq_ffi_fn_method_moqgroupproducer_finish( uniffiClonePointer(), - FfiConverterUInt64.lower(timestampUs), status, ); }, moqExceptionErrorHandler); } - String name() { + int sequence() { return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqmediaproducer_name( - uniffiClonePointer(), - status, - ), - FfiConverterString.lift, - moqExceptionErrorHandler, - ); - } - - void seek({required int sequence}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqmediaproducer_seek( + (status) => uniffi_moq_ffi_fn_method_moqgroupproducer_sequence( uniffiClonePointer(), - FfiConverterUInt64.lower(sequence), status, - ); - }, moqExceptionErrorHandler); - } - - Future unused() { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqmediaproducer_unused( - uniffiClonePointer(), ), - ffi_moq_ffi_rust_future_poll_void, - ffi_moq_ffi_rust_future_complete_void, - ffi_moq_ffi_rust_future_free_void, - (_) {}, - moqExceptionErrorHandler, - ); - } - - Future used() { - return uniffiRustCallAsync( - () => - uniffi_moq_ffi_fn_method_moqmediaproducer_used(uniffiClonePointer()), - ffi_moq_ffi_rust_future_poll_void, - ffi_moq_ffi_rust_future_complete_void, - ffi_moq_ffi_rust_future_free_void, - (_) {}, - moqExceptionErrorHandler, + FfiConverterUInt64.lift, + null, ); } void writeFrame({required MoqFrame frame}) { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqmediaproducer_write_frame( + uniffi_moq_ffi_fn_method_moqgroupproducer_write_frame( uniffiClonePointer(), FfiConverterMoqFrame.lower(frame), status, @@ -7015,104 +7847,126 @@ class MoqMediaProducer implements MoqMediaProducerInterface { } } -class FfiConverterMoqMediaProducer { - static MoqMediaProducer lift(Pointer ptr) { - return MoqMediaProducer.lift(ptr); +class FfiConverterMoqGroupProducer { + static MoqGroupProducer lift(Pointer ptr) { + return MoqGroupProducer.lift(ptr); } - static Pointer lower(MoqMediaProducer value) { + static Pointer lower(MoqGroupProducer value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqMediaProducer value) { + static int allocationSize(MoqGroupProducer value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqMediaProducer.lift(pointer), 8); + return LiftRetVal(MoqGroupProducer.lift(pointer), 8); } - static int write(MoqMediaProducer value, Uint8List buf) { + static int write(MoqGroupProducer value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; } } -abstract class MoqMediaStreamProducerInterface { - void finish(); - void write({required Uint8List payload}); +abstract class MoqGroupRequestInterface { + void abort({required int errorCode}); + MoqGroupProducer accept(); + int priority(); + int sequence(); } -final _MoqMediaStreamProducerFinalizer = Finalizer>((ptr) { - rustCall( - (status) => uniffi_moq_ffi_fn_free_moqmediastreamproducer(ptr, status), - ); +final _MoqGroupRequestFinalizer = Finalizer>((ptr) { + rustCall((status) => uniffi_moq_ffi_fn_free_moqgrouprequest(ptr, status)); }); -class MoqMediaStreamProducer implements MoqMediaStreamProducerInterface { +class MoqGroupRequest implements MoqGroupRequestInterface { late final Pointer _ptr; - MoqMediaStreamProducer._(this._ptr) { - _MoqMediaStreamProducerFinalizer.attach(this, _ptr, detach: this); + MoqGroupRequest._(this._ptr) { + _MoqGroupRequestFinalizer.attach(this, _ptr, detach: this); } - factory MoqMediaStreamProducer.lift(Pointer ptr) { - return MoqMediaStreamProducer._(ptr); + factory MoqGroupRequest.lift(Pointer ptr) { + return MoqGroupRequest._(ptr); } Pointer uniffiClonePointer() { return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqmediastreamproducer(_ptr, status), + (status) => uniffi_moq_ffi_fn_clone_moqgrouprequest(_ptr, status), ); } void dispose() { - _MoqMediaStreamProducerFinalizer.detach(this); - rustCall( - (status) => uniffi_moq_ffi_fn_free_moqmediastreamproducer(_ptr, status), - ); + _MoqGroupRequestFinalizer.detach(this); + rustCall((status) => uniffi_moq_ffi_fn_free_moqgrouprequest(_ptr, status)); } - void finish() { + void abort({required int errorCode}) { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqmediastreamproducer_finish( + uniffi_moq_ffi_fn_method_moqgrouprequest_abort( uniffiClonePointer(), + FfiConverterUInt16.lower(errorCode), status, ); }, moqExceptionErrorHandler); } - void write({required Uint8List payload}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqmediastreamproducer_write( + MoqGroupProducer accept() { + return rustCallWithLifter( + (status) => uniffi_moq_ffi_fn_method_moqgrouprequest_accept( uniffiClonePointer(), - FfiConverterUint8List.lower(payload), status, - ); - }, moqExceptionErrorHandler); + ), + FfiConverterMoqGroupProducer.lift, + moqExceptionErrorHandler, + ); + } + + int priority() { + return rustCallWithLifter( + (status) => uniffi_moq_ffi_fn_method_moqgrouprequest_priority( + uniffiClonePointer(), + status, + ), + FfiConverterUInt8.lift, + null, + ); + } + + int sequence() { + return rustCallWithLifter( + (status) => uniffi_moq_ffi_fn_method_moqgrouprequest_sequence( + uniffiClonePointer(), + status, + ), + FfiConverterUInt64.lift, + null, + ); } } -class FfiConverterMoqMediaStreamProducer { - static MoqMediaStreamProducer lift(Pointer ptr) { - return MoqMediaStreamProducer.lift(ptr); +class FfiConverterMoqGroupRequest { + static MoqGroupRequest lift(Pointer ptr) { + return MoqGroupRequest.lift(ptr); } - static Pointer lower(MoqMediaStreamProducer value) { + static Pointer lower(MoqGroupRequest value) { return value.uniffiClonePointer(); } - static int allocationSize(MoqMediaStreamProducer value) { + static int allocationSize(MoqGroupRequest value) { return 8; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0); final pointer = Pointer.fromAddress(handle); - return LiftRetVal(MoqMediaStreamProducer.lift(pointer), 8); + return LiftRetVal(MoqGroupRequest.lift(pointer), 8); } - static int write(MoqMediaStreamProducer value, Uint8List buf) { + static int write(MoqGroupRequest value, Uint8List buf) { final handle = lower(value); buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address); return 8; @@ -7206,9 +8060,6 @@ abstract class MoqTrackProducerInterface { MoqTrackDynamic dynamic_(); void finish(); void finishAt({required int finalSequence}); - String name(); - Future unused(); - Future used(); void writeFrame({required MoqFrame frame}); } @@ -7333,42 +8184,6 @@ class MoqTrackProducer implements MoqTrackProducerInterface { }, moqExceptionErrorHandler); } - String name() { - return rustCallWithLifter( - (status) => uniffi_moq_ffi_fn_method_moqtrackproducer_name( - uniffiClonePointer(), - status, - ), - FfiConverterString.lift, - moqExceptionErrorHandler, - ); - } - - Future unused() { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqtrackproducer_unused( - uniffiClonePointer(), - ), - ffi_moq_ffi_rust_future_poll_void, - ffi_moq_ffi_rust_future_complete_void, - ffi_moq_ffi_rust_future_free_void, - (_) {}, - moqExceptionErrorHandler, - ); - } - - Future used() { - return uniffiRustCallAsync( - () => - uniffi_moq_ffi_fn_method_moqtrackproducer_used(uniffiClonePointer()), - ffi_moq_ffi_rust_future_poll_void, - ffi_moq_ffi_rust_future_complete_void, - ffi_moq_ffi_rust_future_free_void, - (_) {}, - moqExceptionErrorHandler, - ); - } - void writeFrame({required MoqFrame frame}) { return rustCall((status) { uniffi_moq_ffi_fn_method_moqtrackproducer_write_frame( @@ -7508,13 +8323,14 @@ class FfiConverterMoqTrackRequest { } abstract class MoqRequestInterface { - Future accept(); + Future accept({ + MoqOriginProducer? publish = null, + MoqOriginProducer? consume = null, + }); void cancel(); String path(); String? query(); Future reject({required int code}); - void setConsume({required MoqOriginProducer? origin}); - void setPublish({required MoqOriginProducer? origin}); MoqTransport transport(); String? url(); } @@ -7542,9 +8358,16 @@ class MoqRequest implements MoqRequestInterface { rustCall((status) => uniffi_moq_ffi_fn_free_moqrequest(_ptr, status)); } - Future accept() { + Future accept({ + MoqOriginProducer? publish = null, + MoqOriginProducer? consume = null, + }) { return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqrequest_accept(uniffiClonePointer()), + () => uniffi_moq_ffi_fn_method_moqrequest_accept( + uniffiClonePointer(), + FfiConverterOptionalMoqOriginProducer.lower(publish), + FfiConverterOptionalMoqOriginProducer.lower(consume), + ), ffi_moq_ffi_rust_future_poll_u64, ffi_moq_ffi_rust_future_complete_u64, ffi_moq_ffi_rust_future_free_u64, @@ -7595,26 +8418,6 @@ class MoqRequest implements MoqRequestInterface { ); } - void setConsume({required MoqOriginProducer? origin}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqrequest_set_consume( - uniffiClonePointer(), - FfiConverterOptionalMoqOriginProducer.lower(origin), - status, - ); - }, moqExceptionErrorHandler); - } - - void setPublish({required MoqOriginProducer? origin}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqrequest_set_publish( - uniffiClonePointer(), - FfiConverterOptionalMoqOriginProducer.lower(origin), - status, - ); - }, moqExceptionErrorHandler); - } - MoqTransport transport() { return rustCallWithLifter( (status) => uniffi_moq_ffi_fn_method_moqrequest_transport( @@ -7667,12 +8470,6 @@ abstract class MoqServerInterface { void cancel(); List certFingerprints(); Future listen(); - void setBind({required String addr}); - void setConsume({required MoqOriginProducer? origin}); - void setPublish({required MoqOriginProducer? origin}); - void setTlsCert({required List paths}); - void setTlsGenerate({required List hostnames}); - void setTlsKey({required List paths}); } final _MoqServerFinalizer = Finalizer>((ptr) { @@ -7684,10 +8481,13 @@ class MoqServer implements MoqServerInterface { MoqServer._(this._ptr) { _MoqServerFinalizer.attach(this, _ptr, detach: this); } - MoqServer() + MoqServer({required MoqServerConfig config}) : _ptr = rustCall( - (status) => uniffi_moq_ffi_fn_constructor_moqserver_new(status), - null, + (status) => uniffi_moq_ffi_fn_constructor_moqserver_new( + FfiConverterMoqServerConfig.lower(config), + status, + ), + moqExceptionErrorHandler, ) { _MoqServerFinalizer.attach(this, _ptr, detach: this); } @@ -7743,66 +8543,6 @@ class MoqServer implements MoqServerInterface { moqExceptionErrorHandler, ); } - - void setBind({required String addr}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqserver_set_bind( - uniffiClonePointer(), - FfiConverterString.lower(addr), - status, - ); - }, moqExceptionErrorHandler); - } - - void setConsume({required MoqOriginProducer? origin}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqserver_set_consume( - uniffiClonePointer(), - FfiConverterOptionalMoqOriginProducer.lower(origin), - status, - ); - }, moqExceptionErrorHandler); - } - - void setPublish({required MoqOriginProducer? origin}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqserver_set_publish( - uniffiClonePointer(), - FfiConverterOptionalMoqOriginProducer.lower(origin), - status, - ); - }, moqExceptionErrorHandler); - } - - void setTlsCert({required List paths}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqserver_set_tls_cert( - uniffiClonePointer(), - FfiConverterSequenceString.lower(paths), - status, - ); - }, moqExceptionErrorHandler); - } - - void setTlsGenerate({required List hostnames}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqserver_set_tls_generate( - uniffiClonePointer(), - FfiConverterSequenceString.lower(hostnames), - status, - ); - }, moqExceptionErrorHandler); - } - - void setTlsKey({required List paths}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqserver_set_tls_key( - uniffiClonePointer(), - FfiConverterSequenceString.lower(paths), - status, - ); - }, moqExceptionErrorHandler); - } } class FfiConverterMoqServer { @@ -7834,20 +8574,6 @@ class FfiConverterMoqServer { abstract class MoqClientInterface { void cancel(); Future connect({required String url}); - void setBackoff({required MoqBackoff backoff}); - void setBind({required String addr}); - void setConsume({required MoqOriginProducer? origin}); - void setPublish({required MoqOriginProducer? origin}); - void setQuicMaxStreams({required int maxStreams}); - void setReconnect({required bool enabled}); - void setTlsCert({required String? path}); - 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}); - void setWebsocketDelay({required int delayUs}); - void setWebsocketEnabled({required bool enabled}); } final _MoqClientFinalizer = Finalizer>((ptr) { @@ -7857,187 +8583,50 @@ final _MoqClientFinalizer = Finalizer>((ptr) { class MoqClient implements MoqClientInterface { late final Pointer _ptr; MoqClient._(this._ptr) { - _MoqClientFinalizer.attach(this, _ptr, detach: this); - } - MoqClient() - : _ptr = rustCall( - (status) => uniffi_moq_ffi_fn_constructor_moqclient_new(status), - null, - ) { - _MoqClientFinalizer.attach(this, _ptr, detach: this); - } - factory MoqClient.lift(Pointer ptr) { - return MoqClient._(ptr); - } - Pointer uniffiClonePointer() { - return rustCall( - (status) => uniffi_moq_ffi_fn_clone_moqclient(_ptr, status), - ); - } - - void dispose() { - _MoqClientFinalizer.detach(this); - rustCall((status) => uniffi_moq_ffi_fn_free_moqclient(_ptr, status)); - } - - void cancel() { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_cancel(uniffiClonePointer(), status); - }, null); - } - - Future connect({required String url}) { - return uniffiRustCallAsync( - () => uniffi_moq_ffi_fn_method_moqclient_connect( - uniffiClonePointer(), - FfiConverterString.lower(url), - ), - ffi_moq_ffi_rust_future_poll_u64, - ffi_moq_ffi_rust_future_complete_u64, - ffi_moq_ffi_rust_future_free_u64, - (ptr) => FfiConverterMoqSession.lift(Pointer.fromAddress(ptr)), - moqExceptionErrorHandler, - ); - } - - void setBackoff({required MoqBackoff backoff}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_backoff( - uniffiClonePointer(), - FfiConverterMoqBackoff.lower(backoff), - status, - ); - }, moqExceptionErrorHandler); - } - - void setBind({required String addr}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_bind( - uniffiClonePointer(), - FfiConverterString.lower(addr), - status, - ); - }, moqExceptionErrorHandler); - } - - void setConsume({required MoqOriginProducer? origin}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_consume( - uniffiClonePointer(), - FfiConverterOptionalMoqOriginProducer.lower(origin), - status, - ); - }, moqExceptionErrorHandler); - } - - void setPublish({required MoqOriginProducer? origin}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_publish( - uniffiClonePointer(), - FfiConverterOptionalMoqOriginProducer.lower(origin), - status, - ); - }, moqExceptionErrorHandler); - } - - void setQuicMaxStreams({required int maxStreams}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_quic_max_streams( - uniffiClonePointer(), - FfiConverterUInt64.lower(maxStreams), - status, - ); - }, moqExceptionErrorHandler); - } - - void setReconnect({required bool enabled}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_reconnect( - uniffiClonePointer(), - FfiConverterBool.lower(enabled), - status, - ); - }, moqExceptionErrorHandler); - } - - void setTlsCert({required String? path}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_tls_cert( - uniffiClonePointer(), - FfiConverterOptionalString.lower(path), - status, - ); - }, moqExceptionErrorHandler); - } - - void setTlsFingerprints({required List fingerprints}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_tls_fingerprints( - uniffiClonePointer(), - FfiConverterSequenceString.lower(fingerprints), - status, - ); - }, moqExceptionErrorHandler); - } - - void setTlsKey({required String? path}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_tls_key( - uniffiClonePointer(), - FfiConverterOptionalString.lower(path), - status, - ); - }, moqExceptionErrorHandler); + _MoqClientFinalizer.attach(this, _ptr, detach: this); } - - void setTlsRoots({required List paths}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_tls_roots( - uniffiClonePointer(), - FfiConverterSequenceString.lower(paths), - status, - ); - }, moqExceptionErrorHandler); + MoqClient({required MoqClientConfig config}) + : _ptr = rustCall( + (status) => uniffi_moq_ffi_fn_constructor_moqclient_new( + FfiConverterMoqClientConfig.lower(config), + status, + ), + moqExceptionErrorHandler, + ) { + _MoqClientFinalizer.attach(this, _ptr, detach: this); } - - void setTlsSystemRoots({required bool systemRoots}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_tls_system_roots( - uniffiClonePointer(), - FfiConverterBool.lower(systemRoots), - status, - ); - }, moqExceptionErrorHandler); + factory MoqClient.lift(Pointer ptr) { + return MoqClient._(ptr); + } + Pointer uniffiClonePointer() { + return rustCall( + (status) => uniffi_moq_ffi_fn_clone_moqclient(_ptr, status), + ); } - void setTlsVerify({required bool verify}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_tls_verify( - uniffiClonePointer(), - FfiConverterBool.lower(verify), - status, - ); - }, moqExceptionErrorHandler); + void dispose() { + _MoqClientFinalizer.detach(this); + rustCall((status) => uniffi_moq_ffi_fn_free_moqclient(_ptr, status)); } - void setWebsocketDelay({required int delayUs}) { + void cancel() { return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_websocket_delay( - uniffiClonePointer(), - FfiConverterUInt64.lower(delayUs), - status, - ); - }, moqExceptionErrorHandler); + uniffi_moq_ffi_fn_method_moqclient_cancel(uniffiClonePointer(), status); + }, null); } - void setWebsocketEnabled({required bool enabled}) { - return rustCall((status) { - uniffi_moq_ffi_fn_method_moqclient_set_websocket_enabled( + Future connect({required String url}) { + return uniffiRustCallAsync( + () => uniffi_moq_ffi_fn_method_moqclient_connect( uniffiClonePointer(), - FfiConverterBool.lower(enabled), - status, - ); - }, moqExceptionErrorHandler); + FfiConverterString.lower(url), + ), + ffi_moq_ffi_rust_future_poll_u64, + ffi_moq_ffi_rust_future_complete_u64, + ffi_moq_ffi_rust_future_free_u64, + (ptr) => FfiConverterMoqSession.lift(Pointer.fromAddress(ptr)), + moqExceptionErrorHandler, + ); } } @@ -9222,6 +9811,51 @@ class FfiConverterOptionalString { } } +class FfiConverterOptionalUInt32 { + static int? lift(RustBuffer buf) { + return FfiConverterOptionalUInt32.read(buf.asUint8List()).value; + } + + static LiftRetVal read(Uint8List buf) { + if (ByteData.view(buf.buffer, buf.offsetInBytes).getInt8(0) == 0) { + return LiftRetVal(null, 1); + } + final result = FfiConverterUInt32.read( + Uint8List.view(buf.buffer, buf.offsetInBytes + 1), + ); + return LiftRetVal(result.value, result.bytesRead + 1); + } + + static int allocationSize([int? value]) { + if (value == null) { + return 1; + } + return FfiConverterUInt32.allocationSize(value) + 1; + } + + static RustBuffer lower(int? value) { + if (value == null) { + return toRustBuffer(Uint8List.fromList([0])); + } + final buf = Uint8List(FfiConverterOptionalUInt32.allocationSize(value)); + FfiConverterOptionalUInt32.write(value, buf); + return toRustBuffer(buf); + } + + static int write(int? value, Uint8List buf) { + if (value == null) { + buf[0] = 0; + return 1; + } + buf[0] = 1; + return FfiConverterUInt32.write( + value, + Uint8List.view(buf.buffer, buf.offsetInBytes + 1), + ) + + 1; + } +} + class FfiConverterOptionalUInt64 { static int? lift(RustBuffer buf) { return FfiConverterOptionalUInt64.read(buf.asUint8List()).value; @@ -9507,198 +10141,34 @@ class FfiConverterUint8List { return toRustBuffer(buf); } - static int allocationSize([Uint8List? value]) { - if (value == null) { - return 4; - } - return 4 + value.length; - } - - static int write(Uint8List value, Uint8List buf) { - buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, value.length); - buf.setRange(4, 4 + value.length, value); - return 4 + value.length; - } -} - -const _uniffiAssetId = "package:moq_ffi/uniffi:moq_ffi"; -void moqLogLevel({required String level}) { - return rustCall((status) { - uniffi_moq_ffi_fn_func_moq_log_level( - FfiConverterString.lower(level), - status, - ); - }, moqExceptionErrorHandler); -} - -@Native Function(Pointer, Pointer)>( - assetId: _uniffiAssetId, -) -external Pointer uniffi_moq_ffi_fn_clone_moqbandwidth( - Pointer handle, - Pointer uniffiStatus, -); - -@Native, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_free_moqbandwidth( - Pointer handle, - Pointer uniffiStatus, -); - -@Native< - Pointer Function( - Pointer, - Pointer, - Uint64, - Pointer, - ) ->(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqbandwidth_reserve( - Pointer ptr, - Pointer track, - int max_bps, - Pointer uniffiStatus, -); - -@Native Function(Pointer, Pointer)>( - assetId: _uniffiAssetId, -) -external Pointer uniffi_moq_ffi_fn_clone_moqreservation( - Pointer handle, - Pointer uniffiStatus, -); - -@Native, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_free_moqreservation( - Pointer handle, - Pointer uniffiStatus, -); - -@Native, Pointer)>( - assetId: _uniffiAssetId, -) -external RustBuffer uniffi_moq_ffi_fn_method_moqreservation_grant( - Pointer ptr, - Pointer uniffiStatus, -); - -@Native, Uint64, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqreservation_update( - Pointer ptr, - int max_bps, - Pointer uniffiStatus, -); - -@Native Function(Pointer, Pointer)>( - assetId: _uniffiAssetId, -) -external Pointer uniffi_moq_ffi_fn_clone_moqbroadcastconsumer( - Pointer handle, - Pointer uniffiStatus, -); - -@Native, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_free_moqbroadcastconsumer( - Pointer handle, - Pointer uniffiStatus, -); - -@Native Function(Pointer, RustBuffer, Uint64, RustBuffer)>( - assetId: _uniffiAssetId, -) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastconsumer_fetch_group( - Pointer ptr, - RustBuffer name, - int sequence, - RustBuffer options, -); - -@Native< - Pointer Function( - Pointer, - RustBuffer, - Uint64, - RustBuffer, - RustBuffer, - ) ->(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastconsumer_fetch_media_group( - Pointer ptr, - RustBuffer name, - int sequence, - RustBuffer container, - RustBuffer options, -); - -@Native Function(Pointer, RustBuffer)>( - assetId: _uniffiAssetId, -) -external Pointer uniffi_moq_ffi_fn_method_moqbroadcastconsumer_resolve( - Pointer ptr, - RustBuffer reference, -); - -@Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastconsumer_subscribe_catalog( - Pointer ptr, -); - -@Native< - Pointer Function(Pointer, RustBuffer, RustBuffer, RustBuffer) ->(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastconsumer_subscribe_media( - Pointer ptr, - RustBuffer name, - RustBuffer container, - RustBuffer subscription, -); - -@Native Function(Pointer, RustBuffer, RustBuffer)>( - assetId: _uniffiAssetId, -) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastconsumer_subscribe_track( - Pointer ptr, - RustBuffer name, - RustBuffer subscription, -); - -@Native Function(Pointer, RustBuffer, RustBuffer)>( - assetId: _uniffiAssetId, -) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastconsumer_subscribe_json_snapshot( - Pointer ptr, - RustBuffer name, - RustBuffer config, -); + static int allocationSize([Uint8List? value]) { + if (value == null) { + return 4; + } + return 4 + value.length; + } -@Native Function(Pointer, RustBuffer, RustBuffer)>( - assetId: _uniffiAssetId, -) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastconsumer_subscribe_json_stream( - Pointer ptr, - RustBuffer name, - RustBuffer config, -); + static int write(Uint8List value, Uint8List buf) { + buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, value.length); + buf.setRange(4, 4 + value.length, value); + return 4 + value.length; + } +} + +const _uniffiAssetId = "package:moq_ffi/uniffi:moq_ffi"; +void moqLogLevel({required String level}) { + return rustCall((status) { + uniffi_moq_ffi_fn_func_moq_log_level( + FfiConverterString.lower(level), + status, + ); + }, moqExceptionErrorHandler); +} @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqcatalogconsumer( +external Pointer uniffi_moq_ffi_fn_clone_moqbandwidth( Pointer handle, Pointer uniffiStatus, ); @@ -9706,28 +10176,30 @@ external Pointer uniffi_moq_ffi_fn_clone_moqcatalogconsumer( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqcatalogconsumer( +external void uniffi_moq_ffi_fn_free_moqbandwidth( Pointer handle, Pointer uniffiStatus, ); -@Native, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqcatalogconsumer_cancel( +@Native< + Pointer Function( + Pointer, + Pointer, + Uint64, + Pointer, + ) +>(assetId: _uniffiAssetId) +external Pointer uniffi_moq_ffi_fn_method_moqbandwidth_reserve( Pointer ptr, + Pointer track, + int max_bps, Pointer uniffiStatus, ); -@Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqcatalogconsumer_next( - Pointer ptr, -); - @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqgroupconsumer( +external Pointer uniffi_moq_ffi_fn_clone_moqreservation( Pointer handle, Pointer uniffiStatus, ); @@ -9735,36 +10207,32 @@ external Pointer uniffi_moq_ffi_fn_clone_moqgroupconsumer( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqgroupconsumer( +external void uniffi_moq_ffi_fn_free_moqreservation( Pointer handle, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqgroupconsumer_cancel( +external RustBuffer uniffi_moq_ffi_fn_method_moqreservation_grant( Pointer ptr, Pointer uniffiStatus, ); -@Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqgroupconsumer_read_frame( - Pointer ptr, -); - -@Native, Pointer)>( +@Native, Uint64, Pointer)>( assetId: _uniffiAssetId, ) -external int uniffi_moq_ffi_fn_method_moqgroupconsumer_sequence( +external void uniffi_moq_ffi_fn_method_moqreservation_update( Pointer ptr, + int max_bps, Pointer uniffiStatus, ); @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqmediaconsumer( +external Pointer uniffi_moq_ffi_fn_clone_moqbroadcastconsumer( Pointer handle, Pointer uniffiStatus, ); @@ -9772,28 +10240,44 @@ external Pointer uniffi_moq_ffi_fn_clone_moqmediaconsumer( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqmediaconsumer( +external void uniffi_moq_ffi_fn_free_moqbroadcastconsumer( Pointer handle, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native Function(Pointer, RustBuffer, Uint64, RustBuffer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqmediaconsumer_cancel( +external Pointer +uniffi_moq_ffi_fn_method_moqbroadcastconsumer_fetch_group( Pointer ptr, - Pointer uniffiStatus, + RustBuffer name, + int sequence, + RustBuffer options, ); -@Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqmediaconsumer_next( +@Native Function(Pointer, RustBuffer)>( + assetId: _uniffiAssetId, +) +external Pointer uniffi_moq_ffi_fn_method_moqbroadcastconsumer_resolve( + Pointer ptr, + RustBuffer reference, +); + +@Native Function(Pointer, RustBuffer, RustBuffer)>( + assetId: _uniffiAssetId, +) +external Pointer +uniffi_moq_ffi_fn_method_moqbroadcastconsumer_subscribe_track( Pointer ptr, + RustBuffer name, + RustBuffer subscription, ); @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqmediagroupconsumer( +external Pointer uniffi_moq_ffi_fn_clone_moqgroupconsumer( Pointer handle, Pointer uniffiStatus, ); @@ -9801,7 +10285,7 @@ external Pointer uniffi_moq_ffi_fn_clone_moqmediagroupconsumer( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqmediagroupconsumer( +external void uniffi_moq_ffi_fn_free_moqgroupconsumer( Pointer handle, Pointer uniffiStatus, ); @@ -9809,20 +10293,20 @@ external void uniffi_moq_ffi_fn_free_moqmediagroupconsumer( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqmediagroupconsumer_cancel( +external void uniffi_moq_ffi_fn_method_moqgroupconsumer_cancel( Pointer ptr, Pointer uniffiStatus, ); @Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqmediagroupconsumer_next( +external Pointer uniffi_moq_ffi_fn_method_moqgroupconsumer_read_frame( Pointer ptr, ); @Native, Pointer)>( assetId: _uniffiAssetId, ) -external int uniffi_moq_ffi_fn_method_moqmediagroupconsumer_sequence( +external int uniffi_moq_ffi_fn_method_moqgroupconsumer_sequence( Pointer ptr, Pointer uniffiStatus, ); @@ -9946,6 +10430,22 @@ external void uniffi_moq_ffi_fn_free_moqflatesnapshotproducer( Pointer uniffiStatus, ); +@Native< + Pointer Function( + Pointer, + Pointer, + RustBuffer, + Pointer, + ) +>(assetId: _uniffiAssetId) +external Pointer +uniffi_moq_ffi_fn_constructor_moqflatesnapshotproducer_new( + Pointer broadcast, + Pointer track, + RustBuffer config, + Pointer uniffiStatus, +); + @Native, Pointer)>( assetId: _uniffiAssetId, ) @@ -9979,6 +10479,21 @@ external void uniffi_moq_ffi_fn_free_moqflatestreamproducer( Pointer uniffiStatus, ); +@Native< + Pointer Function( + Pointer, + Pointer, + RustBuffer, + Pointer, + ) +>(assetId: _uniffiAssetId) +external Pointer uniffi_moq_ffi_fn_constructor_moqflatestreamproducer_new( + Pointer broadcast, + Pointer track, + RustBuffer config, + Pointer uniffiStatus, +); + @Native, RustBuffer, Pointer)>( assetId: _uniffiAssetId, ) @@ -10012,6 +10527,16 @@ external void uniffi_moq_ffi_fn_free_moqjsonsnapshotconsumer( Pointer uniffiStatus, ); +@Native< + Pointer Function(Pointer, RustBuffer, Pointer) +>(assetId: _uniffiAssetId) +external Pointer +uniffi_moq_ffi_fn_constructor_moqjsonsnapshotconsumer_new( + Pointer track, + RustBuffer config, + Pointer uniffiStatus, +); + @Native, Pointer)>( assetId: _uniffiAssetId, ) @@ -10041,6 +10566,22 @@ external void uniffi_moq_ffi_fn_free_moqjsonsnapshotproducer( Pointer uniffiStatus, ); +@Native< + Pointer Function( + Pointer, + Pointer, + RustBuffer, + Pointer, + ) +>(assetId: _uniffiAssetId) +external Pointer +uniffi_moq_ffi_fn_constructor_moqjsonsnapshotproducer_new( + Pointer broadcast, + Pointer track, + RustBuffer config, + Pointer uniffiStatus, +); + @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) @@ -10082,6 +10623,15 @@ external void uniffi_moq_ffi_fn_free_moqjsonstreamconsumer( Pointer uniffiStatus, ); +@Native< + Pointer Function(Pointer, RustBuffer, Pointer) +>(assetId: _uniffiAssetId) +external Pointer uniffi_moq_ffi_fn_constructor_moqjsonstreamconsumer_new( + Pointer track, + RustBuffer config, + Pointer uniffiStatus, +); + @Native, Pointer)>( assetId: _uniffiAssetId, ) @@ -10111,6 +10661,21 @@ external void uniffi_moq_ffi_fn_free_moqjsonstreamproducer( Pointer uniffiStatus, ); +@Native< + Pointer Function( + Pointer, + Pointer, + RustBuffer, + Pointer, + ) +>(assetId: _uniffiAssetId) +external Pointer uniffi_moq_ffi_fn_constructor_moqjsonstreamproducer_new( + Pointer broadcast, + Pointer track, + RustBuffer config, + Pointer uniffiStatus, +); + @Native, RustBuffer, Pointer)>( assetId: _uniffiAssetId, ) @@ -10139,7 +10704,7 @@ external void uniffi_moq_ffi_fn_method_moqjsonstreamproducer_finish( @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqannounceconsumer( +external Pointer uniffi_moq_ffi_fn_clone_moqmediacatalogconsumer( Pointer handle, Pointer uniffiStatus, ); @@ -10147,57 +10712,34 @@ external Pointer uniffi_moq_ffi_fn_clone_moqannounceconsumer( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqannounceconsumer( +external void uniffi_moq_ffi_fn_free_moqmediacatalogconsumer( Pointer handle, Pointer uniffiStatus, ); -@Native, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqannounceconsumer_cancel( - Pointer ptr, - Pointer uniffiStatus, -); - @Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqannounceconsumer_next( - Pointer ptr, -); - -@Native Function(Pointer, Pointer)>( - assetId: _uniffiAssetId, -) -external Pointer uniffi_moq_ffi_fn_clone_moqannouncedbroadcast( - Pointer handle, - Pointer uniffiStatus, +external Pointer +uniffi_moq_ffi_fn_constructor_moqmediacatalogconsumer_subscribe( + Pointer broadcast, ); @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqannouncedbroadcast( - Pointer handle, +external void uniffi_moq_ffi_fn_method_moqmediacatalogconsumer_cancel( + Pointer ptr, Pointer uniffiStatus, ); @Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqannouncedbroadcast_available( +external Pointer uniffi_moq_ffi_fn_method_moqmediacatalogconsumer_next( Pointer ptr, ); -@Native, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqannouncedbroadcast_cancel( - Pointer ptr, - Pointer uniffiStatus, -); - @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqbroadcastrequest( +external Pointer uniffi_moq_ffi_fn_clone_moqmediacatalogproducer( Pointer handle, Pointer uniffiStatus, ); @@ -10205,69 +10747,62 @@ external Pointer uniffi_moq_ffi_fn_clone_moqbroadcastrequest( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqbroadcastrequest( +external void uniffi_moq_ffi_fn_free_moqmediacatalogproducer( Pointer handle, Pointer uniffiStatus, ); -@Native, Pointer, Pointer)>( +@Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqbroadcastrequest_accept( - Pointer ptr, +external Pointer +uniffi_moq_ffi_fn_constructor_moqmediacatalogproducer_new( Pointer broadcast, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native, RustBuffer, Pointer)>( assetId: _uniffiAssetId, ) -external RustBuffer uniffi_moq_ffi_fn_method_moqbroadcastrequest_path( +external void uniffi_moq_ffi_fn_method_moqmediacatalogproducer_remove_section( Pointer ptr, + RustBuffer name, Pointer uniffiStatus, ); -@Native, Uint16, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqbroadcastrequest_reject( +@Native< + Void Function(Pointer, RustBuffer, RustBuffer, Pointer) +>(assetId: _uniffiAssetId) +external void uniffi_moq_ffi_fn_method_moqmediacatalogproducer_set_section( Pointer ptr, - int error_code, + RustBuffer name, + RustBuffer json, Pointer uniffiStatus, ); -@Native Function(Pointer, Pointer)>( +@Native, RustBuffer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqoriginconsumer( - Pointer handle, +external void +uniffi_moq_ffi_fn_method_moqmediacatalogproducer_set_video_properties( + Pointer ptr, + RustBuffer properties, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqoriginconsumer( +external Pointer uniffi_moq_ffi_fn_clone_moqmediacontainerconsumer( Pointer handle, Pointer uniffiStatus, ); -@Native< - Pointer Function(Pointer, RustBuffer, Pointer) ->(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqoriginconsumer_announced( - Pointer ptr, - RustBuffer config, - Pointer uniffiStatus, -); - -@Native< - Pointer Function(Pointer, RustBuffer, Pointer) ->(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqoriginconsumer_announced_broadcast( - Pointer ptr, - RustBuffer path, +@Native, Pointer)>( + assetId: _uniffiAssetId, +) +external void uniffi_moq_ffi_fn_free_moqmediacontainerconsumer( + Pointer handle, Pointer uniffiStatus, ); @@ -10275,15 +10810,28 @@ uniffi_moq_ffi_fn_method_moqoriginconsumer_announced_broadcast( assetId: _uniffiAssetId, ) external Pointer -uniffi_moq_ffi_fn_method_moqoriginconsumer_request_broadcast( +uniffi_moq_ffi_fn_constructor_moqmediacontainerconsumer_subscribe( + Pointer broadcast, + RustBuffer config, +); + +@Native, Pointer)>( + assetId: _uniffiAssetId, +) +external void uniffi_moq_ffi_fn_method_moqmediacontainerconsumer_cancel( + Pointer ptr, + Pointer uniffiStatus, +); + +@Native Function(Pointer)>(assetId: _uniffiAssetId) +external Pointer uniffi_moq_ffi_fn_method_moqmediacontainerconsumer_next( Pointer ptr, - RustBuffer path, ); @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqorigindynamic( +external Pointer uniffi_moq_ffi_fn_clone_moqmediacontainergroupconsumer( Pointer handle, Pointer uniffiStatus, ); @@ -10291,38 +10839,44 @@ external Pointer uniffi_moq_ffi_fn_clone_moqorigindynamic( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqorigindynamic( +external void uniffi_moq_ffi_fn_free_moqmediacontainergroupconsumer( Pointer handle, Pointer uniffiStatus, ); +@Native Function(Pointer, RustBuffer)>( + assetId: _uniffiAssetId, +) +external Pointer +uniffi_moq_ffi_fn_constructor_moqmediacontainergroupconsumer_fetch( + Pointer broadcast, + RustBuffer config, +); + @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqorigindynamic_cancel( +external void uniffi_moq_ffi_fn_method_moqmediacontainergroupconsumer_cancel( Pointer ptr, Pointer uniffiStatus, ); @Native Function(Pointer)>(assetId: _uniffiAssetId) external Pointer -uniffi_moq_ffi_fn_method_moqorigindynamic_requested_broadcast( - Pointer ptr, -); +uniffi_moq_ffi_fn_method_moqmediacontainergroupconsumer_next(Pointer ptr); -@Native, RustBuffer, Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqorigindynamic_update( +external int uniffi_moq_ffi_fn_method_moqmediacontainergroupconsumer_sequence( Pointer ptr, - RustBuffer route, Pointer uniffiStatus, ); @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqoriginproducer( +external Pointer uniffi_moq_ffi_fn_clone_moqmediacontainerproducer( Pointer handle, Pointer uniffiStatus, ); @@ -10330,56 +10884,59 @@ external Pointer uniffi_moq_ffi_fn_clone_moqoriginproducer( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqoriginproducer( +external void uniffi_moq_ffi_fn_free_moqmediacontainerproducer( Pointer handle, Pointer uniffiStatus, ); -@Native Function(RustBuffer, Pointer)>( +@Native< + Pointer Function(Pointer, RustBuffer, Pointer) +>(assetId: _uniffiAssetId) +external Pointer +uniffi_moq_ffi_fn_constructor_moqmediacontainerproducer_new( + Pointer broadcast, + RustBuffer init, + Pointer uniffiStatus, +); + +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_constructor_moqoriginproducer_new( - RustBuffer config, +external void uniffi_moq_ffi_fn_method_moqmediacontainerproducer_cut( + Pointer ptr, Pointer uniffiStatus, ); -@Native Function(Pointer, Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_method_moqoriginproducer_consume( +external void uniffi_moq_ffi_fn_method_moqmediacontainerproducer_finish( Pointer ptr, Pointer uniffiStatus, ); -@Native< - Pointer Function(Pointer, RustBuffer, Pointer) ->(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqoriginproducer_create_broadcast( +@Native, Uint64, Pointer)>( + assetId: _uniffiAssetId, +) +external void uniffi_moq_ffi_fn_method_moqmediacontainerproducer_seek( Pointer ptr, - RustBuffer path, + int sequence, Pointer uniffiStatus, ); -@Native< - Pointer Function( - Pointer, - RustBuffer, - RustBuffer, - Pointer, - ) ->(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqoriginproducer_dynamic( +@Native, RustBuffer, Pointer)>( + assetId: _uniffiAssetId, +) +external void uniffi_moq_ffi_fn_method_moqmediacontainerproducer_write( Pointer ptr, - RustBuffer prefix, - RustBuffer route, + RustBuffer payload, Pointer uniffiStatus, ); @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqbroadcastdynamic( +external Pointer uniffi_moq_ffi_fn_clone_moqmediacontainerstreamproducer( Pointer handle, Pointer uniffiStatus, ); @@ -10387,59 +10944,51 @@ external Pointer uniffi_moq_ffi_fn_clone_moqbroadcastdynamic( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqbroadcastdynamic( +external void uniffi_moq_ffi_fn_free_moqmediacontainerstreamproducer( Pointer handle, Pointer uniffiStatus, ); +@Native< + Pointer Function(Pointer, RustBuffer, Pointer) +>(assetId: _uniffiAssetId) +external Pointer +uniffi_moq_ffi_fn_constructor_moqmediacontainerstreamproducer_new( + Pointer broadcast, + RustBuffer format, + Pointer uniffiStatus, +); + @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqbroadcastdynamic_cancel( +external void uniffi_moq_ffi_fn_method_moqmediacontainerstreamproducer_finish( Pointer ptr, Pointer uniffiStatus, ); -@Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastdynamic_requested_track(Pointer ptr); - -@Native Function(Pointer, Pointer)>( +@Native, RustBuffer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqbroadcastproducer( - Pointer handle, +external void uniffi_moq_ffi_fn_method_moqmediacontainerstreamproducer_write( + Pointer ptr, + RustBuffer payload, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqbroadcastproducer( +external Pointer uniffi_moq_ffi_fn_clone_moqmediatrackproducer( Pointer handle, Pointer uniffiStatus, ); -@Native Function(Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_constructor_moqbroadcastproducer_new( - Pointer uniffiStatus, -); - -@Native< - Pointer Function( - Pointer, - RustBuffer, - RustBuffer, - Pointer, - ) ->(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_flate_snapshot( - Pointer ptr, - RustBuffer name, - RustBuffer config, +external void uniffi_moq_ffi_fn_free_moqmediatrackproducer( + Pointer handle, Pointer uniffiStatus, ); @@ -10452,10 +11001,10 @@ uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_flate_snapshot( ) >(assetId: _uniffiAssetId) external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_flate_stream( - Pointer ptr, - RustBuffer name, - RustBuffer config, +uniffi_moq_ffi_fn_constructor_moqmediatrackproducer_audio( + Pointer broadcast, + RustBuffer target, + RustBuffer init, Pointer uniffiStatus, ); @@ -10468,105 +11017,85 @@ uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_flate_stream( ) >(assetId: _uniffiAssetId) external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_json_snapshot( - Pointer ptr, - RustBuffer name, - RustBuffer config, +uniffi_moq_ffi_fn_constructor_moqmediatrackproducer_video( + Pointer broadcast, + RustBuffer target, + RustBuffer init, Pointer uniffiStatus, ); -@Native< - Pointer Function( - Pointer, - RustBuffer, - RustBuffer, - Pointer, - ) ->(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_json_stream( +@Native, Pointer)>( + assetId: _uniffiAssetId, +) +external void uniffi_moq_ffi_fn_method_moqmediatrackproducer_cut( Pointer ptr, - RustBuffer name, - RustBuffer config, Pointer uniffiStatus, ); -@Native, RustBuffer, Pointer)>( +@Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqbroadcastproducer_announce( +external Pointer uniffi_moq_ffi_fn_method_moqmediatrackproducer_demand( Pointer ptr, - RustBuffer route, Pointer uniffiStatus, ); @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqbroadcastproducer_close( +external void uniffi_moq_ffi_fn_method_moqmediatrackproducer_discontinuity( Pointer ptr, Pointer uniffiStatus, ); -@Native Function(Pointer, Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_method_moqbroadcastproducer_consume( +external void uniffi_moq_ffi_fn_method_moqmediatrackproducer_finish( Pointer ptr, Pointer uniffiStatus, ); -@Native Function(Pointer, Pointer)>( +@Native, Uint64, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_method_moqbroadcastproducer_dynamic( +external void uniffi_moq_ffi_fn_method_moqmediatrackproducer_flush( Pointer ptr, + int timestamp_us, Pointer uniffiStatus, ); -@Native< - Pointer Function(Pointer, RustBuffer, Pointer) ->(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_audio( +@Native, Uint64, Pointer)>( + assetId: _uniffiAssetId, +) +external void uniffi_moq_ffi_fn_method_moqmediatrackproducer_seek( Pointer ptr, - RustBuffer init, + int sequence, Pointer uniffiStatus, ); -@Native< - Pointer Function( - Pointer, - Pointer, - RustBuffer, - Pointer, - ) ->(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_audio_on_track( +@Native, RustBuffer, Pointer)>( + assetId: _uniffiAssetId, +) +external void uniffi_moq_ffi_fn_method_moqmediatrackproducer_write_frame( Pointer ptr, - Pointer request, - RustBuffer init, + RustBuffer frame, Pointer uniffiStatus, ); -@Native< - Pointer Function(Pointer, RustBuffer, Pointer) ->(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_container( - Pointer ptr, - RustBuffer init, +@Native Function(Pointer, Pointer)>( + assetId: _uniffiAssetId, +) +external Pointer uniffi_moq_ffi_fn_clone_moqmediatrackstreamproducer( + Pointer handle, Pointer uniffiStatus, ); -@Native< - Pointer Function(Pointer, RustBuffer, Pointer) ->(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_container_stream( - Pointer ptr, - RustBuffer format, +@Native, Pointer)>( + assetId: _uniffiAssetId, +) +external void uniffi_moq_ffi_fn_free_moqmediatrackstreamproducer( + Pointer handle, Pointer uniffiStatus, ); @@ -10579,83 +11108,93 @@ uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_container_stream( ) >(assetId: _uniffiAssetId) external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_track( - Pointer ptr, - RustBuffer name, - RustBuffer info, +uniffi_moq_ffi_fn_constructor_moqmediatrackstreamproducer_video( + Pointer broadcast, + RustBuffer target, + RustBuffer init, Pointer uniffiStatus, ); -@Native< - Pointer Function(Pointer, RustBuffer, Pointer) ->(assetId: _uniffiAssetId) +@Native Function(Pointer, Pointer)>( + assetId: _uniffiAssetId, +) external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_video( +uniffi_moq_ffi_fn_method_moqmediatrackstreamproducer_demand( Pointer ptr, - RustBuffer init, Pointer uniffiStatus, ); -@Native< - Pointer Function( - Pointer, - Pointer, - RustBuffer, - Pointer, - ) ->(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_video_on_track( +@Native, Pointer)>( + assetId: _uniffiAssetId, +) +external void uniffi_moq_ffi_fn_method_moqmediatrackstreamproducer_finish( Pointer ptr, - Pointer request, - RustBuffer init, Pointer uniffiStatus, ); -@Native< - Pointer Function(Pointer, RustBuffer, Pointer) ->(assetId: _uniffiAssetId) -external Pointer -uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_video_stream( +@Native, RustBuffer, Pointer)>( + assetId: _uniffiAssetId, +) +external void uniffi_moq_ffi_fn_method_moqmediatrackstreamproducer_write( Pointer ptr, - RustBuffer init, + RustBuffer payload, + Pointer uniffiStatus, +); + +@Native Function(Pointer, Pointer)>( + assetId: _uniffiAssetId, +) +external Pointer uniffi_moq_ffi_fn_clone_moqannounceconsumer( + Pointer handle, + Pointer uniffiStatus, +); + +@Native, Pointer)>( + assetId: _uniffiAssetId, +) +external void uniffi_moq_ffi_fn_free_moqannounceconsumer( + Pointer handle, Pointer uniffiStatus, ); -@Native, RustBuffer, Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external void -uniffi_moq_ffi_fn_method_moqbroadcastproducer_remove_catalog_section( +external void uniffi_moq_ffi_fn_method_moqannounceconsumer_cancel( Pointer ptr, - RustBuffer name, Pointer uniffiStatus, ); -@Native< - Void Function(Pointer, RustBuffer, RustBuffer, Pointer) ->(assetId: _uniffiAssetId) -external void uniffi_moq_ffi_fn_method_moqbroadcastproducer_set_catalog_section( +@Native Function(Pointer)>(assetId: _uniffiAssetId) +external Pointer uniffi_moq_ffi_fn_method_moqannounceconsumer_next( Pointer ptr, - RustBuffer name, - RustBuffer json, +); + +@Native Function(Pointer, Pointer)>( + assetId: _uniffiAssetId, +) +external Pointer uniffi_moq_ffi_fn_clone_moqannouncedbroadcast( + Pointer handle, Pointer uniffiStatus, ); -@Native, RustBuffer, Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external void -uniffi_moq_ffi_fn_method_moqbroadcastproducer_set_video_properties( - Pointer ptr, - RustBuffer properties, +external void uniffi_moq_ffi_fn_free_moqannouncedbroadcast( + Pointer handle, Pointer uniffiStatus, ); +@Native Function(Pointer)>(assetId: _uniffiAssetId) +external Pointer uniffi_moq_ffi_fn_method_moqannouncedbroadcast_available( + Pointer ptr, +); + @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqbroadcastproducer_unannounce( +external void uniffi_moq_ffi_fn_method_moqannouncedbroadcast_cancel( Pointer ptr, Pointer uniffiStatus, ); @@ -10663,7 +11202,7 @@ external void uniffi_moq_ffi_fn_method_moqbroadcastproducer_unannounce( @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqcontainerproducer( +external Pointer uniffi_moq_ffi_fn_clone_moqbroadcastrequest( Pointer handle, Pointer uniffiStatus, ); @@ -10671,49 +11210,85 @@ external Pointer uniffi_moq_ffi_fn_clone_moqcontainerproducer( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqcontainerproducer( +external void uniffi_moq_ffi_fn_free_moqbroadcastrequest( Pointer handle, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native, Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqcontainerproducer_cut( +external void uniffi_moq_ffi_fn_method_moqbroadcastrequest_accept( Pointer ptr, + Pointer broadcast, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqcontainerproducer_finish( +external RustBuffer uniffi_moq_ffi_fn_method_moqbroadcastrequest_path( Pointer ptr, Pointer uniffiStatus, ); -@Native, Uint64, Pointer)>( +@Native, Uint16, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqcontainerproducer_seek( +external void uniffi_moq_ffi_fn_method_moqbroadcastrequest_reject( Pointer ptr, - int sequence, + int error_code, Pointer uniffiStatus, ); -@Native, RustBuffer, Pointer)>( +@Native Function(Pointer, Pointer)>( + assetId: _uniffiAssetId, +) +external Pointer uniffi_moq_ffi_fn_clone_moqoriginconsumer( + Pointer handle, + Pointer uniffiStatus, +); + +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqcontainerproducer_write( +external void uniffi_moq_ffi_fn_free_moqoriginconsumer( + Pointer handle, + Pointer uniffiStatus, +); + +@Native< + Pointer Function(Pointer, RustBuffer, Pointer) +>(assetId: _uniffiAssetId) +external Pointer uniffi_moq_ffi_fn_method_moqoriginconsumer_announced( Pointer ptr, - RustBuffer payload, + RustBuffer config, + Pointer uniffiStatus, +); + +@Native< + Pointer Function(Pointer, RustBuffer, Pointer) +>(assetId: _uniffiAssetId) +external Pointer +uniffi_moq_ffi_fn_method_moqoriginconsumer_announced_broadcast( + Pointer ptr, + RustBuffer path, Pointer uniffiStatus, ); +@Native Function(Pointer, RustBuffer)>( + assetId: _uniffiAssetId, +) +external Pointer +uniffi_moq_ffi_fn_method_moqoriginconsumer_request_broadcast( + Pointer ptr, + RustBuffer path, +); + @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqcontainerstreamproducer( +external Pointer uniffi_moq_ffi_fn_clone_moqorigindynamic( Pointer handle, Pointer uniffiStatus, ); @@ -10721,7 +11296,7 @@ external Pointer uniffi_moq_ffi_fn_clone_moqcontainerstreamproducer( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqcontainerstreamproducer( +external void uniffi_moq_ffi_fn_free_moqorigindynamic( Pointer handle, Pointer uniffiStatus, ); @@ -10729,24 +11304,30 @@ external void uniffi_moq_ffi_fn_free_moqcontainerstreamproducer( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqcontainerstreamproducer_finish( +external void uniffi_moq_ffi_fn_method_moqorigindynamic_cancel( Pointer ptr, Pointer uniffiStatus, ); +@Native Function(Pointer)>(assetId: _uniffiAssetId) +external Pointer +uniffi_moq_ffi_fn_method_moqorigindynamic_requested_broadcast( + Pointer ptr, +); + @Native, RustBuffer, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqcontainerstreamproducer_write( +external void uniffi_moq_ffi_fn_method_moqorigindynamic_update( Pointer ptr, - RustBuffer payload, + RustBuffer route, Pointer uniffiStatus, ); @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqgroupproducer( +external Pointer uniffi_moq_ffi_fn_clone_moqoriginproducer( Pointer handle, Pointer uniffiStatus, ); @@ -10754,57 +11335,84 @@ external Pointer uniffi_moq_ffi_fn_clone_moqgroupproducer( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqgroupproducer( +external void uniffi_moq_ffi_fn_free_moqoriginproducer( Pointer handle, Pointer uniffiStatus, ); -@Native, Uint16, Pointer)>( +@Native Function(RustBuffer, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqgroupproducer_abort( - Pointer ptr, - int error_code, +external Pointer uniffi_moq_ffi_fn_constructor_moqoriginproducer_new( + RustBuffer config, Pointer uniffiStatus, ); @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_method_moqgroupproducer_consume( +external Pointer uniffi_moq_ffi_fn_method_moqoriginproducer_consume( Pointer ptr, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native< + Pointer Function(Pointer, RustBuffer, Pointer) +>(assetId: _uniffiAssetId) +external Pointer +uniffi_moq_ffi_fn_method_moqoriginproducer_create_broadcast( + Pointer ptr, + RustBuffer path, + Pointer uniffiStatus, +); + +@Native< + Pointer Function( + Pointer, + RustBuffer, + RustBuffer, + Pointer, + ) +>(assetId: _uniffiAssetId) +external Pointer uniffi_moq_ffi_fn_method_moqoriginproducer_dynamic( + Pointer ptr, + RustBuffer prefix, + RustBuffer route, + Pointer uniffiStatus, +); + +@Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqgroupproducer_finish( - Pointer ptr, +external Pointer uniffi_moq_ffi_fn_clone_moqbroadcastdynamic( + Pointer handle, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external int uniffi_moq_ffi_fn_method_moqgroupproducer_sequence( - Pointer ptr, +external void uniffi_moq_ffi_fn_free_moqbroadcastdynamic( + Pointer handle, Pointer uniffiStatus, ); -@Native, RustBuffer, Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqgroupproducer_write_frame( +external void uniffi_moq_ffi_fn_method_moqbroadcastdynamic_cancel( Pointer ptr, - RustBuffer frame, Pointer uniffiStatus, ); +@Native Function(Pointer)>(assetId: _uniffiAssetId) +external Pointer +uniffi_moq_ffi_fn_method_moqbroadcastdynamic_requested_track(Pointer ptr); + @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqgrouprequest( +external Pointer uniffi_moq_ffi_fn_clone_moqbroadcastproducer( Pointer handle, Pointer uniffiStatus, ); @@ -10812,40 +11420,39 @@ external Pointer uniffi_moq_ffi_fn_clone_moqgrouprequest( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqgrouprequest( +external void uniffi_moq_ffi_fn_free_moqbroadcastproducer( Pointer handle, Pointer uniffiStatus, ); -@Native, Uint16, Pointer)>( +@Native Function(Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqgrouprequest_abort( - Pointer ptr, - int error_code, +external Pointer uniffi_moq_ffi_fn_constructor_moqbroadcastproducer_new( Pointer uniffiStatus, ); -@Native Function(Pointer, Pointer)>( +@Native, RustBuffer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_method_moqgrouprequest_accept( +external void uniffi_moq_ffi_fn_method_moqbroadcastproducer_announce( Pointer ptr, + RustBuffer route, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external int uniffi_moq_ffi_fn_method_moqgrouprequest_priority( +external void uniffi_moq_ffi_fn_method_moqbroadcastproducer_close( Pointer ptr, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external int uniffi_moq_ffi_fn_method_moqgrouprequest_sequence( +external Pointer uniffi_moq_ffi_fn_method_moqbroadcastproducer_consume( Pointer ptr, Pointer uniffiStatus, ); @@ -10853,23 +11460,31 @@ external int uniffi_moq_ffi_fn_method_moqgrouprequest_sequence( @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqmediaproducer( - Pointer handle, +external Pointer uniffi_moq_ffi_fn_method_moqbroadcastproducer_dynamic( + Pointer ptr, Pointer uniffiStatus, ); -@Native, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_free_moqmediaproducer( - Pointer handle, +@Native< + Pointer Function( + Pointer, + RustBuffer, + RustBuffer, + Pointer, + ) +>(assetId: _uniffiAssetId) +external Pointer +uniffi_moq_ffi_fn_method_moqbroadcastproducer_publish_track( + Pointer ptr, + RustBuffer name, + RustBuffer info, Pointer uniffiStatus, ); @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqmediaproducer_cut( +external void uniffi_moq_ffi_fn_method_moqbroadcastproducer_unannounce( Pointer ptr, Pointer uniffiStatus, ); @@ -10877,67 +11492,56 @@ external void uniffi_moq_ffi_fn_method_moqmediaproducer_cut( @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_method_moqmediaproducer_demand( - Pointer ptr, +external Pointer uniffi_moq_ffi_fn_clone_moqgroupproducer( + Pointer handle, Pointer uniffiStatus, ); @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqmediaproducer_discontinuity( - Pointer ptr, +external void uniffi_moq_ffi_fn_free_moqgroupproducer( + Pointer handle, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native, Uint16, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqmediaproducer_finish( +external void uniffi_moq_ffi_fn_method_moqgroupproducer_abort( Pointer ptr, + int error_code, Pointer uniffiStatus, ); -@Native, Uint64, Pointer)>( +@Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqmediaproducer_flush( +external Pointer uniffi_moq_ffi_fn_method_moqgroupproducer_consume( Pointer ptr, - int timestamp_us, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external RustBuffer uniffi_moq_ffi_fn_method_moqmediaproducer_name( +external void uniffi_moq_ffi_fn_method_moqgroupproducer_finish( Pointer ptr, Pointer uniffiStatus, ); -@Native, Uint64, Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqmediaproducer_seek( +external int uniffi_moq_ffi_fn_method_moqgroupproducer_sequence( Pointer ptr, - int sequence, Pointer uniffiStatus, ); -@Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqmediaproducer_unused( - Pointer ptr, -); - -@Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqmediaproducer_used( - Pointer ptr, -); - @Native, RustBuffer, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqmediaproducer_write_frame( +external void uniffi_moq_ffi_fn_method_moqgroupproducer_write_frame( Pointer ptr, RustBuffer frame, Pointer uniffiStatus, @@ -10946,7 +11550,7 @@ external void uniffi_moq_ffi_fn_method_moqmediaproducer_write_frame( @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) -external Pointer uniffi_moq_ffi_fn_clone_moqmediastreamproducer( +external Pointer uniffi_moq_ffi_fn_clone_moqgrouprequest( Pointer handle, Pointer uniffiStatus, ); @@ -10954,25 +11558,41 @@ external Pointer uniffi_moq_ffi_fn_clone_moqmediastreamproducer( @Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_free_moqmediastreamproducer( +external void uniffi_moq_ffi_fn_free_moqgrouprequest( Pointer handle, Pointer uniffiStatus, ); -@Native, Pointer)>( +@Native, Uint16, Pointer)>( + assetId: _uniffiAssetId, +) +external void uniffi_moq_ffi_fn_method_moqgrouprequest_abort( + Pointer ptr, + int error_code, + Pointer uniffiStatus, +); + +@Native Function(Pointer, Pointer)>( + assetId: _uniffiAssetId, +) +external Pointer uniffi_moq_ffi_fn_method_moqgrouprequest_accept( + Pointer ptr, + Pointer uniffiStatus, +); + +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqmediastreamproducer_finish( +external int uniffi_moq_ffi_fn_method_moqgrouprequest_priority( Pointer ptr, Pointer uniffiStatus, ); -@Native, RustBuffer, Pointer)>( +@Native, Pointer)>( assetId: _uniffiAssetId, ) -external void uniffi_moq_ffi_fn_method_moqmediastreamproducer_write( +external int uniffi_moq_ffi_fn_method_moqgrouprequest_sequence( Pointer ptr, - RustBuffer payload, Pointer uniffiStatus, ); @@ -11098,24 +11718,6 @@ external void uniffi_moq_ffi_fn_method_moqtrackproducer_finish_at( Pointer uniffiStatus, ); -@Native, Pointer)>( - assetId: _uniffiAssetId, -) -external RustBuffer uniffi_moq_ffi_fn_method_moqtrackproducer_name( - Pointer ptr, - Pointer uniffiStatus, -); - -@Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqtrackproducer_unused( - Pointer ptr, -); - -@Native Function(Pointer)>(assetId: _uniffiAssetId) -external Pointer uniffi_moq_ffi_fn_method_moqtrackproducer_used( - Pointer ptr, -); - @Native, RustBuffer, Pointer)>( assetId: _uniffiAssetId, ) @@ -11191,9 +11793,13 @@ external void uniffi_moq_ffi_fn_free_moqrequest( Pointer uniffiStatus, ); -@Native Function(Pointer)>(assetId: _uniffiAssetId) +@Native Function(Pointer, RustBuffer, RustBuffer)>( + assetId: _uniffiAssetId, +) external Pointer uniffi_moq_ffi_fn_method_moqrequest_accept( Pointer ptr, + RustBuffer publish, + RustBuffer consume, ); @Native, Pointer)>( @@ -11226,24 +11832,6 @@ external Pointer uniffi_moq_ffi_fn_method_moqrequest_reject( int code, ); -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqrequest_set_consume( - Pointer ptr, - RustBuffer origin, - Pointer uniffiStatus, -); - -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqrequest_set_publish( - Pointer ptr, - RustBuffer origin, - Pointer uniffiStatus, -); - @Native, Pointer)>( assetId: _uniffiAssetId, ) @@ -11276,10 +11864,11 @@ external void uniffi_moq_ffi_fn_free_moqserver( Pointer uniffiStatus, ); -@Native Function(Pointer)>( +@Native Function(RustBuffer, Pointer)>( assetId: _uniffiAssetId, ) external Pointer uniffi_moq_ffi_fn_constructor_moqserver_new( + RustBuffer config, Pointer uniffiStatus, ); @@ -11309,60 +11898,6 @@ external Pointer uniffi_moq_ffi_fn_method_moqserver_listen( Pointer ptr, ); -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqserver_set_bind( - Pointer ptr, - RustBuffer addr, - Pointer uniffiStatus, -); - -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqserver_set_consume( - Pointer ptr, - RustBuffer origin, - Pointer uniffiStatus, -); - -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqserver_set_publish( - Pointer ptr, - RustBuffer origin, - Pointer uniffiStatus, -); - -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqserver_set_tls_cert( - Pointer ptr, - RustBuffer paths, - Pointer uniffiStatus, -); - -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqserver_set_tls_generate( - Pointer ptr, - RustBuffer hostnames, - Pointer uniffiStatus, -); - -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqserver_set_tls_key( - Pointer ptr, - RustBuffer paths, - Pointer uniffiStatus, -); - @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) @@ -11379,10 +11914,11 @@ external void uniffi_moq_ffi_fn_free_moqclient( Pointer uniffiStatus, ); -@Native Function(Pointer)>( +@Native Function(RustBuffer, Pointer)>( assetId: _uniffiAssetId, ) external Pointer uniffi_moq_ffi_fn_constructor_moqclient_new( + RustBuffer config, Pointer uniffiStatus, ); @@ -11402,132 +11938,6 @@ external Pointer uniffi_moq_ffi_fn_method_moqclient_connect( RustBuffer url, ); -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_backoff( - Pointer ptr, - RustBuffer backoff, - Pointer uniffiStatus, -); - -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_bind( - Pointer ptr, - RustBuffer addr, - Pointer uniffiStatus, -); - -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_consume( - Pointer ptr, - RustBuffer origin, - Pointer uniffiStatus, -); - -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_publish( - Pointer ptr, - RustBuffer origin, - Pointer uniffiStatus, -); - -@Native, Uint64, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_quic_max_streams( - Pointer ptr, - int max_streams, - Pointer uniffiStatus, -); - -@Native, Int8, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_reconnect( - Pointer ptr, - int enabled, - Pointer uniffiStatus, -); - -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_tls_cert( - Pointer ptr, - RustBuffer path, - Pointer uniffiStatus, -); - -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_tls_fingerprints( - Pointer ptr, - RustBuffer fingerprints, - Pointer uniffiStatus, -); - -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_tls_key( - Pointer ptr, - RustBuffer path, - Pointer uniffiStatus, -); - -@Native, RustBuffer, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_tls_roots( - Pointer ptr, - RustBuffer paths, - Pointer uniffiStatus, -); - -@Native, Int8, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_tls_system_roots( - Pointer ptr, - int 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, Uint64, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_websocket_delay( - Pointer ptr, - int delay_us, - Pointer uniffiStatus, -); - -@Native, Int8, Pointer)>( - assetId: _uniffiAssetId, -) -external void uniffi_moq_ffi_fn_method_moqclient_set_websocket_enabled( - Pointer ptr, - int enabled, - Pointer uniffiStatus, -); - @Native Function(Pointer, Pointer)>( assetId: _uniffiAssetId, ) @@ -11963,39 +12373,13 @@ external int uniffi_moq_ffi_checksum_method_moqreservation_update(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_fetch_group(); -@Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_fetch_media_group(); - @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_resolve(); -@Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_subscribe_catalog(); - -@Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_subscribe_media(); - @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_subscribe_track(); -@Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_subscribe_json_snapshot(); - -@Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_subscribe_json_stream(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqcatalogconsumer_cancel(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqcatalogconsumer_next(); - @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqgroupconsumer_cancel(); @@ -12005,21 +12389,6 @@ external int uniffi_moq_ffi_checksum_method_moqgroupconsumer_read_frame(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqgroupconsumer_sequence(); -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediaconsumer_cancel(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediaconsumer_next(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediagroupconsumer_cancel(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediagroupconsumer_next(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediagroupconsumer_sequence(); - @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqtrackconsumer_cancel(); @@ -12096,219 +12465,198 @@ external int uniffi_moq_ffi_checksum_method_moqjsonstreamproducer_demand(); external int uniffi_moq_ffi_checksum_method_moqjsonstreamproducer_finish(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqannounceconsumer_cancel(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqannounceconsumer_next(); +external int uniffi_moq_ffi_checksum_method_moqmediacatalogconsumer_cancel(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqannouncedbroadcast_available(); +external int uniffi_moq_ffi_checksum_method_moqmediacatalogconsumer_next(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqannouncedbroadcast_cancel(); +external int +uniffi_moq_ffi_checksum_method_moqmediacatalogproducer_remove_section(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqbroadcastrequest_accept(); +external int +uniffi_moq_ffi_checksum_method_moqmediacatalogproducer_set_section(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqbroadcastrequest_path(); +external int +uniffi_moq_ffi_checksum_method_moqmediacatalogproducer_set_video_properties(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqbroadcastrequest_reject(); +external int uniffi_moq_ffi_checksum_method_moqmediacontainerconsumer_cancel(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqoriginconsumer_announced(); +external int uniffi_moq_ffi_checksum_method_moqmediacontainerconsumer_next(); @Native(assetId: _uniffiAssetId) external int -uniffi_moq_ffi_checksum_method_moqoriginconsumer_announced_broadcast(); +uniffi_moq_ffi_checksum_method_moqmediacontainergroupconsumer_cancel(); @Native(assetId: _uniffiAssetId) external int -uniffi_moq_ffi_checksum_method_moqoriginconsumer_request_broadcast(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqorigindynamic_cancel(); +uniffi_moq_ffi_checksum_method_moqmediacontainergroupconsumer_next(); @Native(assetId: _uniffiAssetId) external int -uniffi_moq_ffi_checksum_method_moqorigindynamic_requested_broadcast(); +uniffi_moq_ffi_checksum_method_moqmediacontainergroupconsumer_sequence(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqorigindynamic_update(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqoriginproducer_consume(); +external int uniffi_moq_ffi_checksum_method_moqmediacontainerproducer_cut(); @Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqoriginproducer_create_broadcast(); +external int uniffi_moq_ffi_checksum_method_moqmediacontainerproducer_finish(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqoriginproducer_dynamic(); +external int uniffi_moq_ffi_checksum_method_moqmediacontainerproducer_seek(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqbroadcastdynamic_cancel(); +external int uniffi_moq_ffi_checksum_method_moqmediacontainerproducer_write(); @Native(assetId: _uniffiAssetId) external int -uniffi_moq_ffi_checksum_method_moqbroadcastdynamic_requested_track(); +uniffi_moq_ffi_checksum_method_moqmediacontainerstreamproducer_finish(); @Native(assetId: _uniffiAssetId) external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_flate_snapshot(); +uniffi_moq_ffi_checksum_method_moqmediacontainerstreamproducer_write(); @Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_flate_stream(); +external int uniffi_moq_ffi_checksum_method_moqmediatrackproducer_cut(); @Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_json_snapshot(); +external int uniffi_moq_ffi_checksum_method_moqmediatrackproducer_demand(); @Native(assetId: _uniffiAssetId) external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_json_stream(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_announce(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_close(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_consume(); +uniffi_moq_ffi_checksum_method_moqmediatrackproducer_discontinuity(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_dynamic(); +external int uniffi_moq_ffi_checksum_method_moqmediatrackproducer_finish(); @Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_audio(); +external int uniffi_moq_ffi_checksum_method_moqmediatrackproducer_flush(); @Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_audio_on_track(); +external int uniffi_moq_ffi_checksum_method_moqmediatrackproducer_seek(); @Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_container(); +external int uniffi_moq_ffi_checksum_method_moqmediatrackproducer_write_frame(); @Native(assetId: _uniffiAssetId) external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_container_stream(); +uniffi_moq_ffi_checksum_method_moqmediatrackstreamproducer_demand(); @Native(assetId: _uniffiAssetId) external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_track(); +uniffi_moq_ffi_checksum_method_moqmediatrackstreamproducer_finish(); @Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_video(); +external int uniffi_moq_ffi_checksum_method_moqmediatrackstreamproducer_write(); @Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_video_on_track(); +external int uniffi_moq_ffi_checksum_method_moqannounceconsumer_cancel(); @Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_video_stream(); +external int uniffi_moq_ffi_checksum_method_moqannounceconsumer_next(); @Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_remove_catalog_section(); +external int uniffi_moq_ffi_checksum_method_moqannouncedbroadcast_available(); @Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_set_catalog_section(); +external int uniffi_moq_ffi_checksum_method_moqannouncedbroadcast_cancel(); @Native(assetId: _uniffiAssetId) -external int -uniffi_moq_ffi_checksum_method_moqbroadcastproducer_set_video_properties(); +external int uniffi_moq_ffi_checksum_method_moqbroadcastrequest_accept(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_unannounce(); +external int uniffi_moq_ffi_checksum_method_moqbroadcastrequest_path(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqcontainerproducer_cut(); +external int uniffi_moq_ffi_checksum_method_moqbroadcastrequest_reject(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqcontainerproducer_finish(); +external int uniffi_moq_ffi_checksum_method_moqoriginconsumer_announced(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqcontainerproducer_seek(); +external int +uniffi_moq_ffi_checksum_method_moqoriginconsumer_announced_broadcast(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqcontainerproducer_write(); +external int +uniffi_moq_ffi_checksum_method_moqoriginconsumer_request_broadcast(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqcontainerstreamproducer_finish(); +external int uniffi_moq_ffi_checksum_method_moqorigindynamic_cancel(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqcontainerstreamproducer_write(); +external int +uniffi_moq_ffi_checksum_method_moqorigindynamic_requested_broadcast(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgroupproducer_abort(); +external int uniffi_moq_ffi_checksum_method_moqorigindynamic_update(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgroupproducer_consume(); +external int uniffi_moq_ffi_checksum_method_moqoriginproducer_consume(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgroupproducer_finish(); +external int +uniffi_moq_ffi_checksum_method_moqoriginproducer_create_broadcast(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgroupproducer_sequence(); +external int uniffi_moq_ffi_checksum_method_moqoriginproducer_dynamic(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgroupproducer_write_frame(); +external int uniffi_moq_ffi_checksum_method_moqbroadcastdynamic_cancel(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgrouprequest_abort(); +external int +uniffi_moq_ffi_checksum_method_moqbroadcastdynamic_requested_track(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgrouprequest_accept(); +external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_announce(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgrouprequest_priority(); +external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_close(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqgrouprequest_sequence(); +external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_consume(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediaproducer_cut(); +external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_dynamic(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediaproducer_demand(); +external int +uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_track(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediaproducer_discontinuity(); +external int uniffi_moq_ffi_checksum_method_moqbroadcastproducer_unannounce(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediaproducer_finish(); +external int uniffi_moq_ffi_checksum_method_moqgroupproducer_abort(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediaproducer_flush(); +external int uniffi_moq_ffi_checksum_method_moqgroupproducer_consume(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediaproducer_name(); +external int uniffi_moq_ffi_checksum_method_moqgroupproducer_finish(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediaproducer_seek(); +external int uniffi_moq_ffi_checksum_method_moqgroupproducer_sequence(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediaproducer_unused(); +external int uniffi_moq_ffi_checksum_method_moqgroupproducer_write_frame(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediaproducer_used(); +external int uniffi_moq_ffi_checksum_method_moqgrouprequest_abort(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediaproducer_write_frame(); +external int uniffi_moq_ffi_checksum_method_moqgrouprequest_accept(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediastreamproducer_finish(); +external int uniffi_moq_ffi_checksum_method_moqgrouprequest_priority(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqmediastreamproducer_write(); +external int uniffi_moq_ffi_checksum_method_moqgrouprequest_sequence(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqtrackdynamic_cancel(); @@ -12343,15 +12691,6 @@ external int uniffi_moq_ffi_checksum_method_moqtrackproducer_finish(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqtrackproducer_finish_at(); -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqtrackproducer_name(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqtrackproducer_unused(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqtrackproducer_used(); - @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqtrackproducer_write_frame(); @@ -12382,12 +12721,6 @@ external int uniffi_moq_ffi_checksum_method_moqrequest_query(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqrequest_reject(); -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqrequest_set_consume(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqrequest_set_publish(); - @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqrequest_transport(); @@ -12406,24 +12739,6 @@ external int uniffi_moq_ffi_checksum_method_moqserver_cert_fingerprints(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqserver_listen(); -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqserver_set_bind(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqserver_set_consume(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqserver_set_publish(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqserver_set_tls_cert(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqserver_set_tls_generate(); - -@Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqserver_set_tls_key(); - @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_method_moqclient_cancel(); @@ -12431,73 +12746,82 @@ external int uniffi_moq_ffi_checksum_method_moqclient_cancel(); external int uniffi_moq_ffi_checksum_method_moqclient_connect(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_backoff(); +external int uniffi_moq_ffi_checksum_method_moqsession_bandwidth(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_bind(); +external int uniffi_moq_ffi_checksum_method_moqsession_cancel(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_consume(); +external int uniffi_moq_ffi_checksum_method_moqsession_closed(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_publish(); +external int uniffi_moq_ffi_checksum_method_moqsession_consume(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_quic_max_streams(); +external int uniffi_moq_ffi_checksum_method_moqsession_epoch(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_reconnect(); +external int uniffi_moq_ffi_checksum_method_moqsession_publish(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_tls_cert(); +external int uniffi_moq_ffi_checksum_method_moqsession_shutdown(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_tls_fingerprints(); +external int uniffi_moq_ffi_checksum_method_moqsession_stats(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_tls_key(); +external int uniffi_moq_ffi_checksum_method_moqsession_status(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_tls_roots(); +external int uniffi_moq_ffi_checksum_constructor_moqflatesnapshotproducer_new(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_tls_system_roots(); +external int uniffi_moq_ffi_checksum_constructor_moqflatestreamproducer_new(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_tls_verify(); +external int uniffi_moq_ffi_checksum_constructor_moqjsonsnapshotconsumer_new(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_websocket_delay(); +external int uniffi_moq_ffi_checksum_constructor_moqjsonsnapshotproducer_new(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqclient_set_websocket_enabled(); +external int uniffi_moq_ffi_checksum_constructor_moqjsonstreamconsumer_new(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqsession_bandwidth(); +external int uniffi_moq_ffi_checksum_constructor_moqjsonstreamproducer_new(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqsession_cancel(); +external int +uniffi_moq_ffi_checksum_constructor_moqmediacatalogconsumer_subscribe(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqsession_closed(); +external int uniffi_moq_ffi_checksum_constructor_moqmediacatalogproducer_new(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqsession_consume(); +external int +uniffi_moq_ffi_checksum_constructor_moqmediacontainerconsumer_subscribe(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqsession_epoch(); +external int +uniffi_moq_ffi_checksum_constructor_moqmediacontainergroupconsumer_fetch(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqsession_publish(); +external int +uniffi_moq_ffi_checksum_constructor_moqmediacontainerproducer_new(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqsession_shutdown(); +external int +uniffi_moq_ffi_checksum_constructor_moqmediacontainerstreamproducer_new(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqsession_stats(); +external int uniffi_moq_ffi_checksum_constructor_moqmediatrackproducer_audio(); @Native(assetId: _uniffiAssetId) -external int uniffi_moq_ffi_checksum_method_moqsession_status(); +external int uniffi_moq_ffi_checksum_constructor_moqmediatrackproducer_video(); + +@Native(assetId: _uniffiAssetId) +external int +uniffi_moq_ffi_checksum_constructor_moqmediatrackstreamproducer_video(); @Native(assetId: _uniffiAssetId) external int uniffi_moq_ffi_checksum_constructor_moqoriginproducer_new(); @@ -12541,39 +12865,13 @@ void _checkApiChecksums() { 18633) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_fetch_media_group() != - 40237) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_resolve() != 55875) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_subscribe_catalog() != - 34722) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_subscribe_media() != - 8119) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_resolve() != 65350) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_subscribe_track() != 2348) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_subscribe_json_snapshot() != - 46473) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqbroadcastconsumer_subscribe_json_stream() != - 3028) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqcatalogconsumer_cancel() != 65421) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqcatalogconsumer_next() != 33133) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } if (uniffi_moq_ffi_checksum_method_moqgroupconsumer_cancel() != 52548) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } @@ -12583,22 +12881,6 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqgroupconsumer_sequence() != 46527) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqmediaconsumer_cancel() != 14280) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqmediaconsumer_next() != 42389) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqmediagroupconsumer_cancel() != 47486) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqmediagroupconsumer_next() != 22636) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqmediagroupconsumer_sequence() != - 22332) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } if (uniffi_moq_ffi_checksum_method_moqtrackconsumer_cancel() != 65022) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } @@ -12681,224 +12963,207 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqjsonstreamproducer_finish() != 51459) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqannounceconsumer_cancel() != 10799) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqannounceconsumer_next() != 38311) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqannouncedbroadcast_available() != - 17719) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqannouncedbroadcast_cancel() != 63175) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqbroadcastrequest_accept() != 36946) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqbroadcastrequest_path() != 6534) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqbroadcastrequest_reject() != 9727) { + if (uniffi_moq_ffi_checksum_method_moqmediacatalogconsumer_cancel() != + 44590) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqoriginconsumer_announced() != 16595) { + if (uniffi_moq_ffi_checksum_method_moqmediacatalogconsumer_next() != 43296) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqoriginconsumer_announced_broadcast() != - 8509) { + if (uniffi_moq_ffi_checksum_method_moqmediacatalogproducer_remove_section() != + 36089) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqoriginconsumer_request_broadcast() != - 64026) { + if (uniffi_moq_ffi_checksum_method_moqmediacatalogproducer_set_section() != + 32625) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqorigindynamic_cancel() != 47453) { + if (uniffi_moq_ffi_checksum_method_moqmediacatalogproducer_set_video_properties() != + 6577) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqorigindynamic_requested_broadcast() != - 54021) { + if (uniffi_moq_ffi_checksum_method_moqmediacontainerconsumer_cancel() != + 27429) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqorigindynamic_update() != 7304) { + if (uniffi_moq_ffi_checksum_method_moqmediacontainerconsumer_next() != + 17029) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqoriginproducer_consume() != 52357) { + if (uniffi_moq_ffi_checksum_method_moqmediacontainergroupconsumer_cancel() != + 8253) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqoriginproducer_create_broadcast() != - 45756) { + if (uniffi_moq_ffi_checksum_method_moqmediacontainergroupconsumer_next() != + 11213) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqoriginproducer_dynamic() != 56233) { + if (uniffi_moq_ffi_checksum_method_moqmediacontainergroupconsumer_sequence() != + 15687) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastdynamic_cancel() != 25875) { + if (uniffi_moq_ffi_checksum_method_moqmediacontainerproducer_cut() != 35477) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastdynamic_requested_track() != - 24118) { + if (uniffi_moq_ffi_checksum_method_moqmediacontainerproducer_finish() != + 5827) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_flate_snapshot() != - 8575) { + if (uniffi_moq_ffi_checksum_method_moqmediacontainerproducer_seek() != 6771) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_flate_stream() != - 56158) { + if (uniffi_moq_ffi_checksum_method_moqmediacontainerproducer_write() != + 44490) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_json_snapshot() != - 64276) { + if (uniffi_moq_ffi_checksum_method_moqmediacontainerstreamproducer_finish() != + 29671) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_json_stream() != - 54975) { + if (uniffi_moq_ffi_checksum_method_moqmediacontainerstreamproducer_write() != + 13648) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_announce() != 13700) { + if (uniffi_moq_ffi_checksum_method_moqmediatrackproducer_cut() != 54122) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_close() != 19191) { + if (uniffi_moq_ffi_checksum_method_moqmediatrackproducer_demand() != 16488) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_consume() != 27634) { + if (uniffi_moq_ffi_checksum_method_moqmediatrackproducer_discontinuity() != + 32931) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_dynamic() != 55635) { + if (uniffi_moq_ffi_checksum_method_moqmediatrackproducer_finish() != 15731) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_audio() != - 31691) { + if (uniffi_moq_ffi_checksum_method_moqmediatrackproducer_flush() != 51126) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_audio_on_track() != - 33897) { + if (uniffi_moq_ffi_checksum_method_moqmediatrackproducer_seek() != 37244) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_container() != - 24539) { + if (uniffi_moq_ffi_checksum_method_moqmediatrackproducer_write_frame() != + 53915) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_container_stream() != - 11217) { + if (uniffi_moq_ffi_checksum_method_moqmediatrackstreamproducer_demand() != + 23377) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_track() != - 44452) { + if (uniffi_moq_ffi_checksum_method_moqmediatrackstreamproducer_finish() != + 57446) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_video() != - 16383) { + if (uniffi_moq_ffi_checksum_method_moqmediatrackstreamproducer_write() != + 44842) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_video_on_track() != - 60666) { + if (uniffi_moq_ffi_checksum_method_moqannounceconsumer_cancel() != 10799) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_video_stream() != - 28640) { + if (uniffi_moq_ffi_checksum_method_moqannounceconsumer_next() != 38311) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_remove_catalog_section() != - 8608) { + if (uniffi_moq_ffi_checksum_method_moqannouncedbroadcast_available() != + 17719) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_set_catalog_section() != - 28423) { + if (uniffi_moq_ffi_checksum_method_moqannouncedbroadcast_cancel() != 63175) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_set_video_properties() != - 9178) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastrequest_accept() != 36946) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_unannounce() != - 49647) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastrequest_path() != 6534) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqcontainerproducer_cut() != 17534) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastrequest_reject() != 9727) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqcontainerproducer_finish() != 13064) { + if (uniffi_moq_ffi_checksum_method_moqoriginconsumer_announced() != 16595) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqcontainerproducer_seek() != 61349) { + if (uniffi_moq_ffi_checksum_method_moqoriginconsumer_announced_broadcast() != + 8509) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqcontainerproducer_write() != 13274) { + if (uniffi_moq_ffi_checksum_method_moqoriginconsumer_request_broadcast() != + 64026) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqcontainerstreamproducer_finish() != - 29733) { + if (uniffi_moq_ffi_checksum_method_moqorigindynamic_cancel() != 47453) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqcontainerstreamproducer_write() != - 18446) { + if (uniffi_moq_ffi_checksum_method_moqorigindynamic_requested_broadcast() != + 54021) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqgroupproducer_abort() != 59787) { + if (uniffi_moq_ffi_checksum_method_moqorigindynamic_update() != 7304) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqgroupproducer_consume() != 53274) { + if (uniffi_moq_ffi_checksum_method_moqoriginproducer_consume() != 52357) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqgroupproducer_finish() != 61241) { + if (uniffi_moq_ffi_checksum_method_moqoriginproducer_create_broadcast() != + 45756) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqgroupproducer_sequence() != 21067) { + if (uniffi_moq_ffi_checksum_method_moqoriginproducer_dynamic() != 56233) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqgroupproducer_write_frame() != 51857) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastdynamic_cancel() != 25875) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqgrouprequest_abort() != 26970) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastdynamic_requested_track() != + 5884) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqgrouprequest_accept() != 48242) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_announce() != 13700) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqgrouprequest_priority() != 1745) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_close() != 19191) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqgrouprequest_sequence() != 29523) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_consume() != 27634) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqmediaproducer_cut() != 58543) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_dynamic() != 55635) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqmediaproducer_demand() != 44491) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_track() != + 44452) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqmediaproducer_discontinuity() != - 35894) { + if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_unannounce() != + 49647) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqmediaproducer_finish() != 38480) { + if (uniffi_moq_ffi_checksum_method_moqgroupproducer_abort() != 59787) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqmediaproducer_flush() != 10235) { + if (uniffi_moq_ffi_checksum_method_moqgroupproducer_consume() != 53274) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqmediaproducer_name() != 7199) { + if (uniffi_moq_ffi_checksum_method_moqgroupproducer_finish() != 61241) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqmediaproducer_seek() != 43157) { + if (uniffi_moq_ffi_checksum_method_moqgroupproducer_sequence() != 21067) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqmediaproducer_unused() != 38139) { + if (uniffi_moq_ffi_checksum_method_moqgroupproducer_write_frame() != 51857) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqmediaproducer_used() != 55925) { + if (uniffi_moq_ffi_checksum_method_moqgrouprequest_abort() != 26970) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqmediaproducer_write_frame() != 7321) { + if (uniffi_moq_ffi_checksum_method_moqgrouprequest_accept() != 48242) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqmediastreamproducer_finish() != 2732) { + if (uniffi_moq_ffi_checksum_method_moqgrouprequest_priority() != 1745) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqmediastreamproducer_write() != 31109) { + if (uniffi_moq_ffi_checksum_method_moqgrouprequest_sequence() != 29523) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqtrackdynamic_cancel() != 57913) { @@ -12924,7 +13189,7 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqtrackproducer_create_group() != 38978) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqtrackproducer_demand() != 32311) { + if (uniffi_moq_ffi_checksum_method_moqtrackproducer_demand() != 7311) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqtrackproducer_dynamic() != 58584) { @@ -12936,22 +13201,13 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqtrackproducer_finish_at() != 24581) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqtrackproducer_name() != 14598) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqtrackproducer_unused() != 29609) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqtrackproducer_used() != 19906) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } if (uniffi_moq_ffi_checksum_method_moqtrackproducer_write_frame() != 1418) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqtrackrequest_abort() != 62713) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqtrackrequest_accept() != 47766) { + if (uniffi_moq_ffi_checksum_method_moqtrackrequest_accept() != 31841) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqtrackrequest_dynamic() != 24801) { @@ -12960,7 +13216,7 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqtrackrequest_name() != 56715) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqrequest_accept() != 46183) { + if (uniffi_moq_ffi_checksum_method_moqrequest_accept() != 55136) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqrequest_cancel() != 25859) { @@ -12975,12 +13231,6 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqrequest_reject() != 2829) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqrequest_set_consume() != 45399) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - 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() != 57171) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } @@ -12993,107 +13243,102 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqserver_cancel() != 56970) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqserver_cert_fingerprints() != 32082) { + if (uniffi_moq_ffi_checksum_method_moqserver_cert_fingerprints() != 57398) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqserver_listen() != 9040) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqserver_set_bind() != 55505) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqserver_set_consume() != 13635) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqserver_set_publish() != 48695) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqserver_set_tls_cert() != 33276) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqserver_set_tls_generate() != 148) { - throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); - } - if (uniffi_moq_ffi_checksum_method_moqserver_set_tls_key() != 56395) { + if (uniffi_moq_ffi_checksum_method_moqclient_cancel() != 29949) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_cancel() != 29949) { + if (uniffi_moq_ffi_checksum_method_moqclient_connect() != 61750) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_connect() != 42368) { + if (uniffi_moq_ffi_checksum_method_moqsession_bandwidth() != 8006) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_backoff() != 63523) { + if (uniffi_moq_ffi_checksum_method_moqsession_cancel() != 39476) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_bind() != 56346) { + if (uniffi_moq_ffi_checksum_method_moqsession_closed() != 7901) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_consume() != 4978) { + if (uniffi_moq_ffi_checksum_method_moqsession_consume() != 57909) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_publish() != 64932) { + if (uniffi_moq_ffi_checksum_method_moqsession_epoch() != 32695) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_quic_max_streams() != - 17062) { + if (uniffi_moq_ffi_checksum_method_moqsession_publish() != 15240) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_reconnect() != 53736) { + if (uniffi_moq_ffi_checksum_method_moqsession_shutdown() != 64390) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_tls_cert() != 12773) { + if (uniffi_moq_ffi_checksum_method_moqsession_stats() != 44305) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_tls_fingerprints() != - 50038) { + if (uniffi_moq_ffi_checksum_method_moqsession_status() != 49725) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_tls_key() != 19390) { + if (uniffi_moq_ffi_checksum_constructor_moqflatesnapshotproducer_new() != + 40377) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_tls_roots() != 5399) { + if (uniffi_moq_ffi_checksum_constructor_moqflatestreamproducer_new() != + 13731) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_tls_system_roots() != - 10239) { + if (uniffi_moq_ffi_checksum_constructor_moqjsonsnapshotconsumer_new() != + 62847) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_tls_verify() != 64525) { + if (uniffi_moq_ffi_checksum_constructor_moqjsonsnapshotproducer_new() != + 4988) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_websocket_delay() != 53033) { + if (uniffi_moq_ffi_checksum_constructor_moqjsonstreamconsumer_new() != + 44851) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqclient_set_websocket_enabled() != - 65261) { + if (uniffi_moq_ffi_checksum_constructor_moqjsonstreamproducer_new() != 9785) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqsession_bandwidth() != 8006) { + if (uniffi_moq_ffi_checksum_constructor_moqmediacatalogconsumer_subscribe() != + 2004) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqsession_cancel() != 39476) { + if (uniffi_moq_ffi_checksum_constructor_moqmediacatalogproducer_new() != + 4684) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqsession_closed() != 7901) { + if (uniffi_moq_ffi_checksum_constructor_moqmediacontainerconsumer_subscribe() != + 56807) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqsession_consume() != 45358) { + if (uniffi_moq_ffi_checksum_constructor_moqmediacontainergroupconsumer_fetch() != + 48900) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqsession_epoch() != 32695) { + if (uniffi_moq_ffi_checksum_constructor_moqmediacontainerproducer_new() != + 7951) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqsession_publish() != 37960) { + if (uniffi_moq_ffi_checksum_constructor_moqmediacontainerstreamproducer_new() != + 32133) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqsession_shutdown() != 64390) { + if (uniffi_moq_ffi_checksum_constructor_moqmediatrackproducer_audio() != + 8819) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqsession_stats() != 44305) { + if (uniffi_moq_ffi_checksum_constructor_moqmediatrackproducer_video() != + 16061) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqsession_status() != 49725) { + if (uniffi_moq_ffi_checksum_constructor_moqmediatrackstreamproducer_video() != + 39137) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_constructor_moqoriginproducer_new() != 48126) { @@ -13102,10 +13347,10 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_constructor_moqbroadcastproducer_new() != 37572) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_constructor_moqserver_new() != 42979) { + if (uniffi_moq_ffi_checksum_constructor_moqserver_new() != 49910) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_constructor_moqclient_new() != 44907) { + if (uniffi_moq_ffi_checksum_constructor_moqclient_new() != 48355) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } } diff --git a/dart/moq_ffi/test/leak_test.dart b/dart/moq_ffi/test/leak_test.dart index c0df265b31d..d54207a473a 100644 --- a/dart/moq_ffi/test/leak_test.dart +++ b/dart/moq_ffi/test/leak_test.dart @@ -17,18 +17,20 @@ void main() { name: 'x' * size, info: null, ); + final demand = track.demand(); // Warm up so one-time allocations do not count as growth. for (var i = 0; i < 100; i++) { - track.name(); + demand.name(); } final before = ProcessInfo.currentRss; for (var i = 0; i < iterations; i++) { - track.name(); + demand.name(); } final growth = ProcessInfo.currentRss - before; expect(growth, lessThan(leaked ~/ 4)); + track.dispose(); }); test('a non-null optional argument is released', () async { diff --git a/doc/lib/cpp/index.md b/doc/lib/cpp/index.md index b6fd4ae09fa..dca6016ac7e 100644 --- a/doc/lib/cpp/index.md +++ b/doc/lib/cpp/index.md @@ -91,14 +91,14 @@ void ok(moq::expected result) { ```cpp // Subscribe. -auto client = moq::Client::init(); +auto client = ok(moq::Client::init({})); // a moq::ClientConfig; {} takes every default auto session = ok(client->connect("https://relay.example.com").get()); auto announced = ok(session->consume()->announced_broadcast("my-stream.hang")); auto broadcast = ok(announced->available().get()); -auto catalogs = ok(broadcast->subscribe_catalog().get()); +auto catalogs = ok(moq::MediaCatalogConsumer::subscribe(broadcast).get()); if (auto catalog = ok(catalogs->next().get())) { // std::nullopt once the catalog ends for (const auto &[name, video] : catalog->video) { - auto media = ok(broadcast->subscribe_media(name, video.container, std::nullopt).get()); + auto media = ok(moq::MediaContainerConsumer::subscribe(broadcast, {name, video.container}).get()); auto frame = ok(media->next().get()); // std::nullopt once the track ends } } @@ -109,7 +109,7 @@ if (auto catalog = ok(catalogs->next().get())) { // std::nullopt once the cata // session is connected as above; opus_init, packet, and rgba come from your // encoder or capture source. auto broadcast = ok(session->publish()->create_broadcast("my-stream.hang")); -auto audio = ok(broadcast->publish_audio({moq::AudioFormat::kOpus, opus_init})); +auto audio = ok(moq::MediaTrackProducer::audio(broadcast, moq::MediaTarget::kNamed{}, {moq::AudioFormat::kOpus, opus_init})); ok(audio->write_frame({packet, 20'000})); moq::VideoEncoderOutput output{moq::VideoCodec::kH264, "camera", std::nullopt, std::nullopt, moq::VideoEncoderKind::kAuto{}}; diff --git a/doc/lib/dart/index.md b/doc/lib/dart/index.md index 7fbd4f90083..6cc48d322c5 100644 --- a/doc/lib/dart/index.md +++ b/doc/lib/dart/index.md @@ -13,26 +13,37 @@ futures and streams. A Native Assets hook supplies the Rust core for Android (API 24+), iOS (16+), Linux, macOS, and Windows. Flutter web is not supported, since it can't load a native library. +Import `package:moq/media.dart` with a prefix for catalogs, encoded-media imports, and +container consumers. `TrackProducer.audio` / `.video` take a broadcast, an init record, +and a `Named` or `Requested` target. `CatalogProducer(broadcast: ...)` owns catalog properties +and sections; it holds the broadcast weakly and fails after the broadcast closes. +`CatalogConsumer.subscribe`, `ContainerConsumer.subscribe`, and `ContainerGroupConsumer.fetch` +construct the read side from a broadcast. + ```bash dart pub add moq # or: flutter pub add moq ``` ```dart +import 'package:moq/media.dart' as media; import 'package:moq/moq.dart'; 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( - options: const AnnounceOptions(prefix: 'live/', filter: '*/camera'), -).listen((event) { +moq + .announced( + options: const AnnounceOptions(prefix: 'live/', filter: '*/camera'), + ) + .updates() + .listen((event) { if (event is AnnounceEventStart) { print(event.announce.prefix); print(event.announce.captures); } }); final broadcast = await moq.requestBroadcast('live/camera'); -final catalog = await broadcast.subscribeCatalog(); +final catalog = await media.CatalogConsumer.subscribe(broadcast: broadcast); print(await catalog.next()); ``` @@ -41,7 +52,7 @@ print(await catalog.next()); final mine = moq.createBroadcast('live/camera'); final track = mine.publishTrack(name: 'video', info: null); final group = track.appendGroup(); -group.writeFrame(frame: Frame(payload: bytes)); +group.writeFrame(frame: Frame(payload: bytes, timestampUs: 0)); group.finish(); mine.announce(route: MoqRoute()); track.finish(); @@ -55,7 +66,7 @@ await moq.close(); The rest of the [shared feature list](/lib/#what-every-binding-can-do) maps one to one; the API reference has the names. -- **No codecs.** Unlike the other bindings, the published Dart binaries carry no encoder or decoder. Already-encoded frames flow through `MediaProducer` and `MediaConsumer`; encoding is up to `package:camera`, platform channels, or another codec package. +- **No codecs.** Unlike the other bindings, the published Dart binaries carry no encoder or decoder. Already-encoded frames flow through `media.TrackProducer` and `media.ContainerConsumer`; encoding is up to `package:camera`, platform channels, or another codec package. - **Names.** Types drop the `Moq` prefix (`Session`, `BroadcastProducer`) as aliases, so the generated names still work. `Container`, `Route`, and the exceptions keep it, since Flutter owns those names. `dynamic` is reserved, so the origin method is `dynamic_`. - **Audio needs cuts.** Video groups at its keyframes, but audio forms a group only where you call `cut()`: after every frame, or at a segment cadence to align with video. - **Live encoder timing.** After writing a frame you encoded yourself, call `flush(timestampUs: ...)` with the same timestamp so the catalog advertises your jitter. Skip it for file and network imports. On a seek or pause, call `discontinuity()`, then keep timestamps moving forward and resume video on a keyframe. diff --git a/doc/lib/go/index.md b/doc/lib/go/index.md index af34aabbb58..4426728b382 100644 --- a/doc/lib/go/index.md +++ b/doc/lib/go/index.md @@ -13,6 +13,11 @@ core arrives as a prebuilt static library through the `moq.dev/moq-ffi` module, `go get` is all it takes (`CGO_ENABLED=1`, the default on Unix). Targets: linux/amd64, linux/arm64, darwin/arm64 (macOS 12.3+), windows/amd64. +`moq.dev/moq/media` owns catalogs, encoded imports, and container consumers. +Construct `NewAudioTrackProducer` / `NewVideoTrackProducer` from a broadcast, an init record, +and a `Named` or `Requested` target. `NewCatalogProducer` holds its broadcast weakly; +catalog writes fail with `ErrClosed` after the broadcast closes. Media timestamps use `time.Duration`. + ```bash go get moq.dev/moq@latest ``` @@ -21,6 +26,7 @@ go get moq.dev/moq@latest import "fmt" import "log" import "moq.dev/moq" +import moqmedia "moq.dev/moq/media" // Subscribe. The iterator is live, so run it in its own goroutine. client, err := moq.Dial(ctx, "https://relay.example.com", moq.WithTLSRoots("ca.pem")) @@ -49,7 +55,7 @@ for event, err := range announced.All(ctx) { if err != nil { log.Fatal(err) } - catalog, err := broadcast.Catalog(ctx) + catalog, err := moqmedia.CatalogSnapshot(ctx, broadcast) if err != nil { log.Fatal(err) } @@ -58,12 +64,14 @@ for event, err := range announced.All(ctx) { ``` ```go +import "time" + // Publish encoded frames, or raw pixels with the codec inside the binding. // opusInit, packet, pts, and rgba come from your encoder or capture source. broadcast, _ := client.CreateBroadcast("my-stream.hang") -audio, _ := broadcast.PublishAudio(moq.AudioFormatOpus, opusInit) -pts := uint64(20_000) -_ = audio.WriteFrame(moq.Frame{Payload: packet, TimestampUs: &pts}) +audio, _ := moqmedia.NewAudioTrackProducer(broadcast, moqmedia.Named{}, moqmedia.AudioInit{Format: moqmedia.AudioFormatOpus, Data: opusInit}) +timestamp := 20 * time.Millisecond +_ = audio.WriteFrame(moq.Frame{Payload: packet, Timestamp: ×tamp}) _ = audio.Cut() // audio has no keyframes, so this is what gives it groups track := "camera" @@ -87,10 +95,10 @@ one to one; the API reference has the names. - **Context cancellation.** Every call that can block takes a `context.Context` first, and cancelling it tears down the native work, not just your wait. A one-shot call (`RequestBroadcast`, `FetchGroup`, `Server.Accept`) aborts alone; a stream read (`Next`, `ReadFrame`, and the iterators over them) cancels the stream it reads. - **Keep owners reachable.** An origin from `moq.NewOriginProducer` has no `Close`. It ends when the garbage collector reaches its last owner (each producer, published broadcast, and `OriginDynamic`), and its consumers then fail with `moq.ErrClosed`. - **Audio needs cuts.** Video groups at its keyframes, but audio forms a group only where you call `Cut()`: after every frame, or at a segment cadence to align with video. -- **Live encoder timing.** After writing a frame you encoded yourself, call `Flush(timestampUs)` with the same timestamp so the catalog advertises your jitter. Skip it for file and network imports. On a seek or pause, call `Discontinuity()`, then keep timestamps moving forward and resume video on a keyframe. +- **Live encoder timing.** After writing a frame you encoded yourself, call `Flush(timestamp)` with the same timestamp so the catalog advertises your jitter. Skip it for file and network imports. On a seek or pause, call `Discontinuity()`, then keep timestamps moving forward and resume video on a keyframe. - **Decoded frames hold decoder buffers.** `Close` each `VideoDecodedFrame` promptly, or the decoder stalls. `Resize` is best effort, so read each frame's `Width()` and `Height()`. - **Closing.** `client.Close()` gives finished tracks up to one second to deliver and returns an error if they did not. `session.Shutdown(ctx)` does the same, and `session.Cancel(code)` closes at once. Finish or abort live tracks first. -- **Stats.** `Session().Stats()` reports `RttUs`, `EstimatedSendRateBps`, `EstimatedRecvRateBps`, and the byte and packet counters (`BytesSent`, `BytesReceived`, `BytesLost`, `PacketsSent`, `PacketsReceived`, `PacketsLost`). A field is nil when the transport does not report it, which is not the same as zero. +- **Stats.** `Session().Stats()` reports `RTT`, `EstimatedSendRateBps`, `EstimatedRecvRateBps`, and the byte and packet counters (`BytesSent`, `BytesReceived`, `BytesLost`, `PacketsSent`, `PacketsReceived`, `PacketsLost`). A field is nil when the transport does not report it, which is not the same as zero. ## Reference diff --git a/doc/lib/kt/index.md b/doc/lib/kt/index.md index 644753d8e0b..76ff144d80f 100644 --- a/doc/lib/kt/index.md +++ b/doc/lib/kt/index.md @@ -13,6 +13,12 @@ cancellation that reaches the native consumer. It pulls in `dev.moq:moq-ffi`, which carries the native binaries for Android (arm64-v8a, armeabi-v7a, x86\_64) and desktop JVM (Linux x86\_64/aarch64, macOS arm64, Windows x64). +`dev.moq.media` owns catalogs, encoded imports, and container consumers. `TrackProducer.audio` +/ `.video` take a broadcast, a `Named` or `Requested` target, and an init record. +`CatalogProducer(broadcast)` holds its broadcast weakly and fails with `Closed` after it closes. +Construct readers with `CatalogConsumer.subscribe`, `ContainerConsumer.subscribe`, or +`ContainerGroupConsumer.fetch`. + ```kotlin ignore dependencies { implementation("dev.moq:moq:") // latest: see the badge above @@ -22,15 +28,16 @@ dependencies { ```kotlin import dev.moq.* +import dev.moq.media.* // 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(AnnounceConfig(prefix = "live/", filter = "*/camera")).collect { event -> +Moq.connect("https://relay.example.com", ClientConfig(tls = ClientTls(roots = listOf("ca.pem")))).use { moq -> + moq.announced(AnnounceConfig(prefix = "live/", filter = "*/camera")).updates().collect { event -> if (event !is AnnounceEventStart) return@collect // Update or End // Prefixes stay origin-relative; captures reports what each wildcard matched. println(event.announce.captures) val broadcast = moq.requestBroadcast(event.announce.prefix) - println(broadcast.catalog()) + println(catalog(broadcast)) } } ``` @@ -40,7 +47,7 @@ Moq.connect("https://relay.example.com", tlsRoots = listOf("ca.pem")).use { moq // opusInit, packet, pts, and rgba come from your encoder or capture source. Moq.connect("https://relay.example.com").use { moq -> val broadcast = moq.createBroadcast("my-stream.hang") - val audio = broadcast.publishAudio(AudioInit(format = AudioFormat.OPUS, data = opusInit)) + val audio = dev.moq.media.TrackProducer.audio(broadcast, Named(null), AudioInit(format = AudioFormat.OPUS, data = opusInit)) audio.writeFrame(Frame(payload = packet, timestampUs = 20_000u)) audio.cut() // audio has no keyframes, so this is what gives it groups diff --git a/doc/lib/py/index.md b/doc/lib/py/index.md index cd5e42ccfbc..402ffc3ed01 100644 --- a/doc/lib/py/index.md +++ b/doc/lib/py/index.md @@ -13,6 +13,12 @@ async iterators for announcements, groups, and frames, and no `Moq` prefixes. Python 3.10+, with wheels for Linux x86\_64/aarch64, macOS arm64, and Windows x64. +`moq.media` owns catalog snapshots, encoded-media importers, and container consumers. +Use `Named(name)` or `Requested(request)` as the target for `TrackProducer.audio` / `.video`. +`CatalogProducer(broadcast)` holds the broadcast weakly and owns catalog properties and sections; +its writes fail with `Error.Closed` after the broadcast closes or is released. +`MediaFrame.timestamp`, media writes, and `flush` use `datetime.timedelta`. + ```bash pip install moq-rs # or: uv add moq-rs ``` @@ -29,10 +35,10 @@ async def main(): announcement = event.announce print(announcement.captures) # what * matched, or None for a partial overlap broadcast = await client.request_broadcast(announcement.prefix) - catalog = await broadcast.catalog() + catalog = await moq.media.catalog(broadcast) name, track = next(iter(catalog.audio.items())) - async for frame in await broadcast.subscribe_media(name, track): - print(frame.timestamp_us, len(frame.payload)) + async for frame in await moq.media.ContainerConsumer.subscribe(broadcast, name, track.container): + print(frame.timestamp, len(frame.payload)) asyncio.run(main()) ``` @@ -46,8 +52,8 @@ async def main(): broadcast = client.create_broadcast("my-stream.hang") # Already-encoded frames: the catalog is filled from the bitstream - audio = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_init_bytes) - audio.write_frame(payload, timestamp_us=0) + audio = moq.media.TrackProducer.audio(broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_init_bytes)) + audio.write_frame(payload) audio.cut() # audio has no keyframes, so this is what gives it groups # Or raw pixels, encoded inside the binding (VideoToolbox, Media Foundation, NVENC, openh264) @@ -63,8 +69,8 @@ async def main(): # Raw bytes and JSON events = broadcast.publish_track("events") - events.write_frame(b'{"cmd": "ready"}', 0) - status = broadcast.publish_json_snapshot("status", compression=True) + events.write_frame(b'{"cmd": "ready"}') + status = moq.json.SnapshotProducer(broadcast, broadcast.publish_track("status"), compression=True) status.update({"state": "live", "viewers": 42}) broadcast.announce() @@ -84,10 +90,10 @@ one to one; the API reference has the names. - **Context managers.** `async with` on a client or session awaits a graceful shutdown when the body exits cleanly, and cancels at once when it raises, so your exception survives. - **Audio needs cuts.** Video groups at its keyframes, but audio forms a group only where you call `cut()`: after every frame, or at a segment cadence to align with video. -- **Live encoder timing.** After writing a frame you encoded yourself, call `flush(timestamp_us)` with the same timestamp so the catalog advertises your jitter. Skip it for file and network imports. On a seek or pause, call `discontinuity()`, then keep timestamps moving forward and resume video on a keyframe. +- **Live encoder timing.** After writing a frame you encoded yourself, call `flush(timestamp)` with the same timestamp so the catalog advertises your jitter. Skip it for file and network imports. On a seek or pause, call `discontinuity()`, then keep timestamps moving forward and resume video on a keyframe. - **Decoded frames hold decoder buffers.** Drop each frame from `decode_video` promptly, or the decoder stalls. `resize` is best effort, so read each frame's `width()` and `height()`. - **Closing.** `await session.shutdown()` gives finished tracks up to one second to deliver and raises if they did not. `cancel(code)` closes at once. Finish or abort live tracks first. -- **Stats.** `session.stats()` reports `rtt_us`, `estimated_send_rate_bps`, `estimated_recv_rate_bps`, and the byte and packet counters (`bytes_sent`, `bytes_received`, `bytes_lost`, `packets_sent`, `packets_received`, `packets_lost`). A field is `None` when the transport does not report it, which is not the same as zero. +- **Stats.** `session.stats()` reports `rtt`, `estimated_send_rate_bps`, `estimated_recv_rate_bps`, and the byte and packet counters (`bytes_sent`, `bytes_received`, `bytes_lost`, `packets_sent`, `packets_received`, `packets_lost`). A field is `None` when the transport does not report it, which is not the same as zero. ## Reference diff --git a/doc/lib/swift/index.md b/doc/lib/swift/index.md index d1758715d07..d8c52caf79f 100644 --- a/doc/lib/swift/index.md +++ b/doc/lib/swift/index.md @@ -12,6 +12,12 @@ consumer, `Sendable` handles, and `Task` cancellation that reaches the native side. It depends on `MoqFFI`, which ships a prebuilt XCFramework with arm64 slices for iOS 16+, the iOS Simulator, and macOS 12.3+. +`Media` owns catalogs, encoded-media importers, and container consumers. +`Media.TrackProducer.audio` / `.video` take a broadcast, an init record, and a +`.named(name:)` or `.requested(request)` target. `Media.CatalogProducer(broadcast:)` +updates catalog properties and sections without keeping the broadcast open. +Its writes fail after closing or releasing the broadcast. + ```swift ignore dependencies: [ .package(url: "https://github.com/moq-dev/moq-swift", from: ""), // latest: see the badge above @@ -25,7 +31,7 @@ targets: [ import Moq // Subscribe. The sequence is live, so run it in its own Task. -let client = Client() +let client = try Client() let session = try await client.connect(to: "https://relay.example.com") for try await event in try session.consume.announced(prefix: "live/", filter: "*/camera") { @@ -33,7 +39,7 @@ for try await event in try session.consume.announced(prefix: "live/", filter: "* // Prefixes stay origin-relative; captures reports what each wildcard matched. print(announcement.captures ?? []) let broadcast = try await session.consume.requestBroadcast(path: announcement.prefix) - for try await catalog in try await broadcast.subscribeCatalog() { + for try await catalog in try await Media.CatalogConsumer.subscribe(broadcast: broadcast) { print(catalog) } } @@ -43,7 +49,7 @@ for try await event in try session.consume.announced(prefix: "live/", filter: "* // Publish encoded frames, or raw pixels with the codec inside the binding (VideoToolbox). // opusInit, packet, pts, and rgba come from your encoder or capture source. let broadcast = try session.publish.createBroadcast(path: "my-stream.hang") -let audio = try broadcast.publishAudio(format: .opus, initData: opusInit) +let audio = try Media.TrackProducer.audio(broadcast: broadcast, initData: Media.AudioInit(format: .opus, data: opusInit)) try audio.writeFrame(packet, timestampUs: 20_000) try audio.cut() // audio has no keyframes, so this is what gives it groups diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index 6c3c91247a4..4a4b34f0bcd 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -36,10 +36,47 @@ These land with the next breaking release, not the 2026-09-23 train. now carries `moq_net::Error`, so it is no longer `PartialEq`. moq-mux's `Error::Binary` is `Error::Flate`. In TypeScript, import `Snapshot` and `Stream` from `@moq/flate`. -- **moq-ffi flate tracks.** `publish_binary_snapshot` / `publish_binary_stream` - are `publish_flate_snapshot` / `publish_flate_stream`, taking - `MoqFlateConfig` and returning `MoqFlateSnapshotProducer` / - `MoqFlateStreamProducer`. The C `moq_publish_binary_*` calls are unchanged. +- **moq-ffi data tracks wrap a track.** The broadcast's + `publish_binary_*`, `publish_json_*`, and `subscribe_json_*` are gone. + Create the track with `publish_track` or `subscribe_track`, then construct + `MoqFlateSnapshotProducer` / `MoqFlateStreamProducer` (taking + `MoqFlateConfig`) or `MoqJsonSnapshotProducer` / `MoqJsonStreamProducer` + from the broadcast and the track, or `MoqJsonSnapshotConsumer` / + `MoqJsonStreamConsumer` from the track. The wrappers keep JSON under its own + namespace: `moq.json` in Python, `moq.dev/moq/json` in Go, `dev.moq.json` in + Kotlin, `package:moq/json.dart` in Dart, and `Json` in Swift. The C + `moq_publish_binary_*` calls are unchanged. +- **moq-ffi clients and servers take a config record.** `MoqClient::new()` and + `MoqServer::new()` plus their setters are `MoqClient::new(MoqClientConfig)` and + `MoqServer::new(MoqServerConfig)`, with nested `tls`, `quic`, `websocket`, and + `backoff` records and a new `versions` list. A value the native side cannot use + fails construction with `MoqError::Config`, including a bad bind address that + used to fail later as `Bind`. The `subscribe` origin is `consume` everywhere: + Python's `Client(..., subscribe=)` is `consume=`, Go's `WithSubscribeOrigin` is + `WithConsumeOrigin`. A server request's `set_publish` / `set_consume` are + arguments to `accept(publish, consume)`, where null inherits the server's + origin (Go: `Accept(ctx, nil, nil)`). Go's `Requests(ctx)` iterators are + `All(ctx)`, and its `Status*` constants are `ConnectionStatus*`; Kotlin and + Dart read announcements as `announced(config).updates()`. +- **moq-ffi media lives in a `media` namespace.** The broadcast's + `publish_audio`, `publish_video`, `publish_container`, their `_on_track` and + `_stream` variants, `set_video_properties`, `set_catalog_section`, + `subscribe_catalog`, `subscribe_media`, and `fetch_media_group` are gone. + Construct `MoqMediaTrackProducer::audio` / `video` (from the broadcast, a + `MoqMediaTarget::Named` or `Requested` target, and the init record), + `MoqMediaContainerProducer`, `MoqMediaCatalogProducer`, + `MoqMediaCatalogConsumer::subscribe`, `MoqMediaContainerConsumer::subscribe`, + or `MoqMediaContainerGroupConsumer::fetch` instead. Producers drop `name`, + `used`, and `unused`; read them through `demand()`. The wrappers expose these + as `moq.media` in Python, `moq.dev/moq/media` in Go, `dev.moq.media` in + Kotlin, `package:moq/media.dart` in Dart, and `Media` in Swift. +- **Python and Go durations are native.** `Frame`, `Datagram`, `Subscription`, + `TrackInfo`, and `ConnectionStats` carry `timedelta` in Python and + `time.Duration` in Go instead of microsecond integers: `max_delay_us` is + `max_delay`, `timestamp_us` is `timestamp`, and `rtt_us` is `rtt`. A frame + timestamp read from an untimed track is `None` / `nil`. A Python string + passed where a list of strings belongs (`tls_roots="ca.pem"`) raises + `TypeError` instead of splitting into characters. - **Demand is read through `demand()`.** In Rust, `track::Producer`'s `is_used`, `used`, `unused`, and `poll_unused` are `producer.demand().X`, and so are `group::Producer`'s `used` and `unused`. diff --git a/go/wrapper/README.md b/go/wrapper/README.md index 41cf42dfd6a..7845ae8bd9d 100644 --- a/go/wrapper/README.md +++ b/go/wrapper/README.md @@ -71,27 +71,40 @@ Incoming server requests expose the query-free `Path()` before `Accept()`. It is consistent across transports and returns an empty string for the root or missing path. `Query()` returns the encoded query and may contain credentials. +Media lives in `moq.dev/moq/media`. `NewAudioTrackProducer` / `NewVideoTrackProducer` +take a broadcast, a `Named` or `Requested` target, and an `AudioInit` / `VideoInit`. +A `Named` target chooses a track name or lets the importer derive one; a `Requested` +target takes over a subscriber's pending request. A duplicate name fails. +`VideoInit.Hint` seeds fields the bitstream cannot supply. `NewContainerProducer` / +`NewContainerStreamProducer` demux whole chunks or a byte stream into their own tracks. +`NewCatalogProducer` holds its broadcast weakly and owns catalog properties and sections. +`NewCatalogConsumer`, `NewContainerConsumer`, and `NewContainerGroupConsumer` read +catalog snapshots, live media, and one fetched media group. Frames and `Flush` use +`time.Duration`; `Demand()` owns track names and subscriber waits. + `BroadcastProducer.Dynamic()` accepts subscriber-requested tracks. Call -`TrackRequest.Accept()` for raw tracks, or `BroadcastProducer.PublishAudioOnTrack()` / -`PublishVideoOnTrack()` -for media tracks whose timescale should be selected by the importer. - -`PublishAudio`, `PublishVideo`, `PublishContainer`, their `OnTrack` variants, and -`PublishVideoStream` / `PublishContainerStream` accept -`WithVideoHint(moq.VideoHint{...})` for video catalog fields that are known -before the stream reveals them. - -`WithAudioTrack(name)` / `WithVideoTrack(name)` name the track instead of -deriving a unique name from the format. A duplicate name fails, and the -`OnTrack` variants refuse it because the request already names the track. - -JSON tracks are available in two modes. `PublishJSONSnapshot` / `SubscribeJSONSnapshot` -carry lossy latest state, while `PublishJSONStream` / `SubscribeJSONStream` carry every -record in order. Producers accept any `encoding/json` value; consumers return +`TrackRequest.Accept()` for raw tracks, or pass the request as `media.Requested` +so the media importer selects the timescale. + +JSON tracks live in the `moq.dev/moq/json` subpackage, mirroring the `moq-json` crate. +Import it under an alias next to `encoding/json`. Each type wraps a track: +`NewSnapshotProducer` / `NewStreamProducer` take over a `TrackProducer` and advertise it +in the broadcast's catalog, and `NewSnapshotConsumer` / `NewStreamConsumer` take over a +`TrackConsumer` that has not read a group yet. Snapshots carry lossy latest state; streams +carry every record in order. Producers accept any `encoding/json` value; consumers return `json.RawMessage` so callers choose their own decoded type. -Publishing takes `JSONSnapshotOptions` or `JSONStreamOptions`; both subscribe calls take -`JSONSubscribeOptions`, where only `Compression` has to match the producer. +```go +import moqjson "moq.dev/moq/json" + +broadcast, _ := client.CreateBroadcast("room") +track, _ := broadcast.PublishTrack("status", nil) +status, _ := moqjson.NewSnapshotProducer(broadcast, track, moqjson.SnapshotOptions{Compression: true}) +_ = status.Update(map[string]any{"viewers": 42}) +``` + +Producers take `SnapshotOptions` or `StreamOptions`; both consumers take +`ConsumerOptions`, where only `Compression` has to match the producer. ## Errors diff --git a/go/wrapper/backoff_internal_test.go b/go/wrapper/backoff_internal_test.go index 67185c52813..23f121c5e6e 100644 --- a/go/wrapper/backoff_internal_test.go +++ b/go/wrapper/backoff_internal_test.go @@ -1,61 +1,75 @@ package moq import ( + "reflect" "testing" "time" ffi "moq.dev/moq-ffi/moq" ) +func ptr[T any](v T) *T { return &v } + // The conversion is where Go's zero value meets the native encoding, and the two // disagree: zero means "unset" to a Go caller but "no delay" and "retry forever" -// to the reconnect loop. Passing it through unresolved turns the most natural +// to the reconnect loop. Passing it through as zero turns the most natural // literal a caller writes, Backoff{}, into an unthrottled dial loop. -func TestBackoffFfiResolvesUnsetFields(t *testing.T) { - defaults := ffi.MoqBackoff{InitialUs: 1_000_000, Multiplier: 2, MaxUs: 5_000_000, TimeoutUs: 10_000_000} - +func TestBackoffFfiLeavesUnsetFieldsToTheNativeDefaults(t *testing.T) { cases := []struct { name string in Backoff want ffi.MoqBackoff }{ { - name: "zero value is the documented default, never an unpaced loop", + name: "zero value is the native default, never an unpaced loop", in: Backoff{}, - want: defaults, + want: ffi.MoqBackoff{}, }, { name: "a partial override keeps the defaults for everything else", in: Backoff{Max: time.Second}, - want: ffi.MoqBackoff{InitialUs: 1_000_000, Multiplier: 2, MaxUs: 1_000_000, TimeoutUs: 10_000_000}, + want: ffi.MoqBackoff{MaxUs: ptr[uint64](1_000_000)}, }, { name: "RetryForever is the only way to reach the native zero timeout", in: Backoff{Timeout: RetryForever}, - want: ffi.MoqBackoff{InitialUs: 1_000_000, Multiplier: 2, MaxUs: 5_000_000, TimeoutUs: 0}, - }, - { - name: "negatives fall back instead of wrapping to ~1.8e19 us", - in: Backoff{Initial: -time.Second, Max: -time.Hour}, - want: defaults, + want: ffi.MoqBackoff{TimeoutUs: ptr[uint64](0)}, }, { name: "a sub-microsecond delay floors at 1us instead of truncating to zero", in: Backoff{Initial: time.Nanosecond}, - want: ffi.MoqBackoff{InitialUs: 1, Multiplier: 2, MaxUs: 5_000_000, TimeoutUs: 10_000_000}, + want: ffi.MoqBackoff{InitialUs: ptr[uint64](1)}, }, { name: "explicit values pass through", in: Backoff{Initial: 500 * time.Millisecond, Multiplier: 3, Max: 10 * time.Second, Timeout: time.Minute}, - want: ffi.MoqBackoff{InitialUs: 500_000, Multiplier: 3, MaxUs: 10_000_000, TimeoutUs: 60_000_000}, + want: ffi.MoqBackoff{ + InitialUs: ptr[uint64](500_000), + Multiplier: ptr[uint32](3), + MaxUs: ptr[uint64](10_000_000), + TimeoutUs: ptr[uint64](60_000_000), + }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - if got := tc.in.ffi(); got != tc.want { + got, err := tc.in.ffi() + if err != nil { + t.Fatalf("Backoff%+v.ffi() failed: %v", tc.in, err) + } + if !reflect.DeepEqual(got, tc.want) { t.Errorf("Backoff%+v.ffi() = %+v, want %+v", tc.in, got, tc.want) } }) } } + +// A negative duration would wrap to ~1.8e19 us when cast, so it is refused. +func TestBackoffFfiRejectsNegatives(t *testing.T) { + for _, in := range []Backoff{{Initial: -time.Second}, {Max: -time.Hour}, {Timeout: -2 * time.Second}} { + if _, err := in.ffi(); err == nil { + t.Errorf("Backoff%+v.ffi() succeeded, want an error", in) + } + } +} diff --git a/go/wrapper/bridge.go b/go/wrapper/bridge.go new file mode 100644 index 00000000000..525733cef8e --- /dev/null +++ b/go/wrapper/bridge.go @@ -0,0 +1,23 @@ +package moq + +import ( + "time" + + ffi "moq.dev/moq-ffi/moq" + "moq.dev/moq/internal/bridge" +) + +// Hand the layer subpackages the handles behind these types +// without exporting them. +func init() { + bridge.BroadcastConsumer = func(v any) *ffi.MoqBroadcastConsumer { return v.(*BroadcastConsumer).inner } + bridge.TrackRequest = func(v any) *ffi.MoqTrackRequest { return v.(*TrackRequest).inner } + bridge.Frame = func(v any) (ffi.MoqFrame, error) { return v.(Frame).ffi() } + bridge.Subscription = func(v any) (*ffi.MoqSubscription, error) { return subscriptionFFI(v.(*Subscription)) } + bridge.Duration = func(d time.Duration, name string) (uint64, error) { return micros(name, d) } + + bridge.BroadcastProducer = func(v any) *ffi.MoqBroadcastProducer { return v.(*BroadcastProducer).inner } + bridge.TrackProducer = func(v any) *ffi.MoqTrackProducer { return v.(*TrackProducer).inner } + bridge.TrackConsumer = func(v any) *ffi.MoqTrackConsumer { return v.(*TrackConsumer).inner } + bridge.TrackDemand = func(inner *ffi.MoqTrackDemand) any { return &TrackDemand{inner: inner} } +} diff --git a/go/wrapper/client.go b/go/wrapper/client.go index bfb6db3f161..caf04525848 100644 --- a/go/wrapper/client.go +++ b/go/wrapper/client.go @@ -2,132 +2,152 @@ package moq import ( "context" - "fmt" "sync" "time" ffi "moq.dev/moq-ffi/moq" + "moq.dev/moq/internal/bridge" ) // ClientOption configures a client created with Dial. type ClientOption func(*clientConfig) +// clientConfig collects the options; Dial validates it into the native config, +// whose unset fields keep moq-ffi's defaults. type clientConfig struct { - tlsVerify bool - tlsRoots []string - tlsRootsSet bool - tlsSystemRoots bool - tlsSystemRootsSet bool - tlsFingerprints []string - tlsFingerprintsSet bool - tlsCert *string - tlsKey *string - bind *string - quicMaxStreams *uint64 - websocketEnabled *bool - websocketDelay *time.Duration - reconnect *bool - backoff *Backoff - publish *OriginProducer - subscribe *OriginProducer + tlsInsecure bool + tlsRoots []string + tlsSystemRoots *bool + tlsFingerprints []string + tlsCert *string + tlsKey *string + bind *string + versions []string + quicMaxStreams *uint64 + websocketEnabled *bool + websocketDelay *time.Duration + once bool + backoff Backoff + publish *OriginProducer + consume *OriginProducer +} + +func (c *clientConfig) ffi() (ffi.MoqClientConfig, error) { + delay, err := optMicros("websocket delay", c.websocketDelay) + if err != nil { + return ffi.MoqClientConfig{}, err + } + backoff, err := c.backoff.ffi() + if err != nil { + return ffi.MoqClientConfig{}, err + } + cfg := ffi.MoqClientConfig{ + Bind: c.bind, + Versions: c.versions, + Tls: ffi.MoqClientTls{ + Insecure: c.tlsInsecure, + Roots: c.tlsRoots, + SystemRoots: c.tlsSystemRoots, + Fingerprints: c.tlsFingerprints, + Cert: c.tlsCert, + Key: c.tlsKey, + }, + Quic: ffi.MoqQuicConfig{MaxStreams: c.quicMaxStreams}, + Websocket: ffi.MoqWebSocketConfig{Enabled: c.websocketEnabled, DelayUs: delay}, + Once: c.once, + Backoff: backoff, + } + if c.publish != nil { + cfg.Publish = &c.publish.inner + } + if c.consume != nil { + cfg.Consume = &c.consume.inner + } + return cfg, nil } // Backoff is the retry pacing for automatic reconnects: the delay starts at // Initial, multiplies by Multiplier after each failed attempt, and caps at Max. // After Timeout of consecutive failures the connection gives up for good. // -// Every field is optional: the zero value means the default beside it, so a -// partial Backoff overrides only what it sets. Pass RetryForever as Timeout to -// keep retrying indefinitely. +// Every field is optional: the zero value means the native default (1s, x2, 5s, +// and a 10s window), so a partial Backoff overrides only what it sets. Pass +// RetryForever as Timeout to keep retrying indefinitely. type Backoff struct { - Initial time.Duration // delay before the first retry (default 1s) - Multiplier uint32 // applied to the delay after each failure (default 2) - Max time.Duration // ceiling on the delay (default 5s) - Timeout time.Duration // give up after this long (default 10s) + Initial time.Duration // delay before the first retry + Multiplier uint32 // applied to the delay after each failure + Max time.Duration // ceiling on the delay + Timeout time.Duration // give up after this long } // RetryForever, passed as Backoff.Timeout, keeps a reconnecting session retrying // indefinitely instead of giving up. const RetryForever time.Duration = -1 -const ( - defaultBackoffInitial = time.Second - defaultBackoffMultiplier = 2 - defaultBackoffMax = 5 * time.Second - defaultBackoffTimeout = 10 * time.Second -) - -// ffi resolves the unset fields, which is load-bearing rather than cosmetic: -// the native side reads a zero timeout as "retry forever" and a zero delay as -// no pacing at all, so passing Go's zero value straight through would turn +// ffi leaves the zero fields unset, which is load-bearing rather than cosmetic: +// the native side reads a zero timeout as "retry forever" and a zero delay as no +// pacing at all, so passing Go's zero value straight through would turn // Backoff{} into an unthrottled dial loop. -func (b Backoff) ffi() ffi.MoqBackoff { - multiplier := b.Multiplier - if multiplier == 0 { - multiplier = defaultBackoffMultiplier +func (b Backoff) ffi() (ffi.MoqBackoff, error) { + var out ffi.MoqBackoff + var err error + if out.InitialUs, err = backoffMicros("backoff initial", b.Initial); err != nil { + return out, err + } + if out.MaxUs, err = backoffMicros("backoff max", b.Max); err != nil { + return out, err + } + if b.Multiplier != 0 { + out.Multiplier = &b.Multiplier } - // Zero is the native encoding of "forever" and also Go's zero value, so the - // two are spelled apart here: Backoff{} keeps the documented default and - // forever is explicit at the call site. - timeoutUs := backoffUs(b.Timeout, defaultBackoffTimeout) + // two are spelled apart here: Backoff{} keeps the default and forever is + // explicit at the call site. if b.Timeout == RetryForever { - timeoutUs = 0 - } - - return ffi.MoqBackoff{ - InitialUs: backoffUs(b.Initial, defaultBackoffInitial), - Multiplier: multiplier, - MaxUs: backoffUs(b.Max, defaultBackoffMax), - TimeoutUs: timeoutUs, + forever := uint64(0) + out.TimeoutUs = &forever + } else if out.TimeoutUs, err = backoffMicros("backoff timeout", b.Timeout); err != nil { + return out, err } + return out, nil } -// backoffUs converts d to microseconds, substituting def when it is unset or -// negative (a negative would wrap when cast to uint64) and flooring at 1us so a -// sub-microsecond duration doesn't truncate to an unpaced zero. -func backoffUs(d, def time.Duration) uint64 { - if d <= 0 { - d = def +// backoffMicros leaves a zero duration unset and floors a positive one at 1us, +// so a sub-microsecond duration doesn't truncate to an unpaced zero. +func backoffMicros(name string, d time.Duration) (*uint64, error) { + if d == 0 { + return nil, nil } - us := d.Microseconds() - if us < 1 { - us = 1 + us, err := micros(name, d) + if err != nil { + return nil, err } - return uint64(us) + us = max(us, 1) + return &us, nil } // WithTLSVerify toggles TLS certificate verification. Verification is on by // default; pass false only against a relay with a self-signed certificate // during development. func WithTLSVerify(verify bool) ClientOption { - return func(c *clientConfig) { c.tlsVerify = verify } + return func(c *clientConfig) { c.tlsInsecure = !verify } } // WithTLSRoots trusts PEM root certificate files instead of the system roots. func WithTLSRoots(paths ...string) ClientOption { roots := append([]string(nil), paths...) - return func(c *clientConfig) { - c.tlsRoots = roots - c.tlsRootsSet = true - } + return func(c *clientConfig) { c.tlsRoots = roots } } // WithTLSSystemRoots controls whether platform roots are trusted with custom roots. func WithTLSSystemRoots(systemRoots bool) ClientOption { - return func(c *clientConfig) { - c.tlsSystemRoots = systemRoots - c.tlsSystemRootsSet = true - } + return func(c *clientConfig) { c.tlsSystemRoots = &systemRoots } } // WithTLSFingerprints pins the peer to one of these SHA-256 certificate fingerprints. func WithTLSFingerprints(fingerprints ...string) ClientOption { pins := append([]string(nil), fingerprints...) - return func(c *clientConfig) { - c.tlsFingerprints = pins - c.tlsFingerprintsSet = true - } + return func(c *clientConfig) { c.tlsFingerprints = pins } } // WithClientTLSCert sets the path to a PEM certificate chain for mTLS. @@ -145,6 +165,13 @@ func WithBind(addr string) ClientOption { return func(c *clientConfig) { c.bind = &addr } } +// WithVersions restricts the protocol versions offered, most preferred first, +// spelled like "moq-lite-03". By default every supported version is offered. +func WithVersions(versions ...string) ClientOption { + offered := append([]string(nil), versions...) + return func(c *clientConfig) { c.versions = offered } +} + // WithQUICMaxStreams caps the concurrent QUIC streams the peer may open toward // this connection (default 1024). MoQ opens a stream per group, and for a // subscriber those arrive from the relay, so a client subscribing to many tracks @@ -173,25 +200,25 @@ func WithWebSocketDelay(delay time.Duration) ClientOption { // consumed through it ride out the gap. Pass false for a one-shot dial whose // transport close ends the session. func WithReconnect(enabled bool) ClientOption { - return func(c *clientConfig) { c.reconnect = &enabled } + return func(c *clientConfig) { c.once = !enabled } } // WithBackoff sets retry pacing for the automatic reconnect. func WithBackoff(backoff Backoff) ClientOption { - return func(c *clientConfig) { c.backoff = &backoff } + return func(c *clientConfig) { c.backoff = backoff } } // WithPublishOrigin sets the origin whose broadcasts are published to the -// remote. Pair with WithSubscribeOrigin for full control; omit both to get a +// remote. Pair with WithConsumeOrigin for full control; omit both to get a // shared internal origin. func WithPublishOrigin(o *OriginProducer) ClientOption { return func(c *clientConfig) { c.publish = o } } -// WithSubscribeOrigin sets the origin that receives broadcasts consumed from +// WithConsumeOrigin sets the origin that receives broadcasts consumed from // the remote. -func WithSubscribeOrigin(o *OriginProducer) ClientOption { - return func(c *clientConfig) { c.subscribe = o } +func WithConsumeOrigin(o *OriginProducer) ClientOption { + return func(c *clientConfig) { c.consume = o } } // Client is a connected MoQ client with automatic origin wiring. When no origin @@ -209,69 +236,22 @@ type Client struct { // Dial connects to a MoQ server and returns the established client. Cancel ctx // to abort an in-flight connect. func Dial(ctx context.Context, url string, opts ...ClientOption) (*Client, error) { - // Verification is on unless WithTLSVerify(false) says otherwise; the zero - // value would mean the opposite. - cfg := clientConfig{tlsVerify: true} + var cfg clientConfig for _, opt := range opts { opt(&cfg) } - - c := &Client{} - inner := ffi.NewMoqClient() - var err error - if !cfg.tlsVerify { - err = inner.SetTlsVerify(false) - } - if err == nil && cfg.tlsRootsSet { - err = inner.SetTlsRoots(cfg.tlsRoots) - } - if err == nil && cfg.tlsSystemRootsSet { - err = inner.SetTlsSystemRoots(cfg.tlsSystemRoots) - } - if err == nil && cfg.tlsFingerprintsSet { - err = inner.SetTlsFingerprints(cfg.tlsFingerprints) - } - if err == nil && cfg.tlsCert != nil { - err = inner.SetTlsCert(cfg.tlsCert) - } - if err == nil && cfg.tlsKey != nil { - err = inner.SetTlsKey(cfg.tlsKey) - } - if err == nil && cfg.bind != nil { - err = inner.SetBind(*cfg.bind) - } - if err == nil && cfg.quicMaxStreams != nil { - err = inner.SetQuicMaxStreams(*cfg.quicMaxStreams) - } - if err == nil && cfg.websocketEnabled != nil { - err = inner.SetWebsocketEnabled(*cfg.websocketEnabled) - } - if err == nil && cfg.websocketDelay != nil { - if *cfg.websocketDelay < 0 { - err = fmt.Errorf("negative websocket delay: %v", *cfg.websocketDelay) - } else { - err = inner.SetWebsocketDelay(uint64(cfg.websocketDelay.Microseconds())) - } - } - if err == nil && cfg.reconnect != nil { - err = inner.SetReconnect(*cfg.reconnect) - } - if err == nil && cfg.backoff != nil { - err = inner.SetBackoff(cfg.backoff.ffi()) - } - if err == nil && cfg.publish != nil { - err = inner.SetPublish(&cfg.publish.inner) - } - if err == nil && cfg.subscribe != nil { - err = inner.SetConsume(&cfg.subscribe.inner) + config, err := cfg.ffi() + if err != nil { + return nil, err } + + inner, err := ffi.NewMoqClient(config) if err != nil { - inner.Cancel() return nil, err } - c.inner = inner + c := &Client{inner: inner} - session, err := runHandle(ctx, inner.Cancel, func(ctx context.Context) (*ffi.MoqSession, error) { + session, err := bridge.CallHandle(ctx, inner.Cancel, func(ctx context.Context) (*ffi.MoqSession, error) { return inner.Connect(ctx, url) }) if err != nil { diff --git a/go/wrapper/doc.go b/go/wrapper/doc.go index 18f04b1c18f..867586ccbbc 100644 --- a/go/wrapper/doc.go +++ b/go/wrapper/doc.go @@ -4,7 +4,8 @@ // It wraps the raw UniFFI bindings in moq.dev/moq-ffi with // idiomatic Go: context.Context cancellation, Go error returns, and Go 1.23 // range-over-func iterators (iter.Seq2) for live streams. The raw record, -// enum, and small object types are re-exported here without the Moq prefix (see types.go), so +// enum, and small object types are re-exported here without the Moq prefix (see types.go), +// records with durations carry time.Duration (see records.go), so // most programs never need to import the ffi package directly. // // A typical full-duplex client wires a single origin as both publish source @@ -23,7 +24,7 @@ // owner. // // What a cancel tears down depends on the call. A one-shot call (a subscribe, a -// fetch, RequestBroadcast, Resolve, a producer's Used/Unused, Server.Accept) +// fetch, RequestBroadcast, Resolve, a TrackDemand's Used/Unused, Server.Accept) // aborts on its own and leaves the object it was made on usable, so the same // broadcast, producer, or server takes the next call. A stream read (any Next, // RecvGroup, ReadFrame, or the iterators over them) instead cancels the stream diff --git a/go/wrapper/errors.go b/go/wrapper/errors.go index ab9fd032fd9..c7ea7001534 100644 --- a/go/wrapper/errors.go +++ b/go/wrapper/errors.go @@ -1,7 +1,6 @@ package moq import ( - "context" "errors" ffi "moq.dev/moq-ffi/moq" @@ -54,7 +53,7 @@ var ( ErrCancelled = ffi.ErrMoqErrorCancelled // ErrClosed is returned when the session or stream has closed; IsShutdown treats it as a graceful stop. ErrClosed = ffi.ErrMoqErrorClosed - // ErrBusy is returned when a configuration setter races an in-flight connect, listen, or accept. + // ErrBusy is returned when CertFingerprints races an in-flight server operation. ErrBusy = ffi.ErrMoqErrorBusy // ErrConnect is returned when establishing a client session fails. ErrConnect = ffi.ErrMoqErrorConnect @@ -74,7 +73,7 @@ var ( ErrNotFound = ffi.ErrMoqErrorNotFound // ErrUnsupported is returned when the requested operation is not supported by this build or peer, however it is asked for. ErrUnsupported = ffi.ErrMoqErrorUnsupported - // ErrAlreadyCommitted is returned when a track already reading in one group order is asked for the other; subscribe again for a second cursor. + // ErrAlreadyCommitted is returned when a track already reading one way (arrival order, sequence order, or a typed reader) is asked to read another; subscribe again for a second cursor. ErrAlreadyCommitted = ffi.ErrMoqErrorAlreadyCommitted // ErrInvalidRoute is returned when a route has an invalid hop ID or too many hops. ErrInvalidRoute = ffi.ErrMoqErrorInvalidRoute @@ -84,6 +83,8 @@ var ( ErrUnresolvableBroadcast = ffi.ErrMoqErrorUnresolvableBroadcast // ErrLog is returned when installing or configuring the native log subscriber fails. ErrLog = ffi.ErrMoqErrorLog + // ErrConfig is returned when Dial or Listen is given a value the native side cannot use. + ErrConfig = ffi.ErrMoqErrorConfig ) // IsShutdown reports whether err is the expected result of a graceful shutdown @@ -114,113 +115,3 @@ func ProtocolError(err error) (ffi.MoqProtocolError, bool) { } return proto.Details, true } - -// handle is a native object the wrapper owns. Every uniffi-generated object has -// Destroy, which drops the Rust-side Arc, and comparable so an optional one can -// be checked for nil before it is destroyed. -type handle interface { - comparable - Destroy() -} - -// run races a blocking FFI call against ctx. -// -// `cancel` is the object's own cancel() for a call that owns a stream, session, -// or listener, so a cancelled Next still ends the stream and a cancelled Connect -// still tears the client down. The generated binding is given a context that -// does not cancel with the caller (see objectCallContext); otherwise it can -// return ctx.Err() into the result channel first and skip that abort. The -// blocked goroutine then unwinds on its own; the result channel is buffered so -// its send never blocks and it can't leak. -// -// release, for a call that returns a native handle, disposes of a result nobody -// received. Cancelling is not a retraction: select picks at random when the -// deadline and the result are both ready, so a call can succeed and still lose, -// and the handle it produced is live either way. -func run[T any](ctx context.Context, cancel func(), call func() (T, error), release func(T)) (T, error) { - // A context that is already done starts no native work at all. Racing it - // instead would let a call that resolves immediately (a subscribe to a track - // already there) win the select and hand back a live handle the caller asked - // not to have, and every such call has side effects on the way. - if err := ctx.Err(); err != nil { - var zero T - return zero, err - } - - type result struct { - val T - err error - } - ch := make(chan result, 1) - go func() { - val, err := call() - ch <- result{val, err} - }() - - select { - case <-ctx.Done(): - if cancel != nil { - cancel() - } - if release != nil { - // Exactly one receiver takes the result, so it is this one once the - // caller has given up. Waiting here would put the native unwind on - // the caller's deadline, so it happens off to the side. - go func() { - if r := <-ch; r.err == nil { - release(r.val) - } - }() - } - var zero T - return zero, ctx.Err() - case r := <-ch: - return r.val, r.err - } -} - -// runCancellable runs a blocking FFI call that yields no handle of its own, so a -// result the caller never sees costs nothing to drop. -// -// cancel is the object's own cancel() for a call that owns its stream, session, -// or listener. One-shot calls pass ctx straight to the generated binding. -func runCancellable[T any](ctx context.Context, cancel func(), call func(context.Context) (T, error)) (T, error) { - return run(ctx, cancel, func() (T, error) { return call(objectCallContext(ctx, cancel)) }, nil) -} - -// runHandle is runCancellable for a call that returns a native handle, which is -// destroyed rather than abandoned when the caller has already given up. Left to -// the Go finalizer it would stay live in the meantime: a subscription still -// running on the wire, or an incoming request accepted and never answered. -func runHandle[T handle](ctx context.Context, cancel func(), call func(context.Context) (T, error)) (T, error) { - return run(ctx, cancel, func() (T, error) { return call(objectCallContext(ctx, cancel)) }, releaseHandle[T]) -} - -// objectCallContext is the context the generated binding sees. Object-owned -// calls already abort via cancel; sharing the caller's ctx lets the binding -// return ctx.Err() first and skip that abort, leaving the stream, session, or -// request active. -func objectCallContext(ctx context.Context, cancel func()) context.Context { - if cancel == nil { - return ctx - } - return context.WithoutCancel(ctx) -} - -// runErr is runCancellable for calls that return only an error. -func runErr(ctx context.Context, cancel func(), call func(context.Context) error) error { - _, err := runCancellable(ctx, cancel, func(ctx context.Context) (struct{}, error) { - return struct{}{}, call(ctx) - }) - return err -} - -// releaseHandle drops a handle the caller never received. A successful call can -// still yield none (an Accept once the server has stopped), which is the zero -// value rather than something to destroy. -func releaseHandle[T handle](val T) { - var zero T - if val != zero { - val.Destroy() - } -} diff --git a/go/wrapper/errors_test.go b/go/wrapper/errors_test.go index 360e8e98f60..21463c317c3 100644 --- a/go/wrapper/errors_test.go +++ b/go/wrapper/errors_test.go @@ -87,6 +87,7 @@ func TestErrorSentinels(t *testing.T) { {"UnresolvableBroadcast", ffi.NewMoqErrorUnresolvableBroadcast(""), moq.ErrUnresolvableBroadcast}, {"AlreadyCommitted", ffi.NewMoqErrorAlreadyCommitted(), moq.ErrAlreadyCommitted}, {"Log", ffi.NewMoqErrorLog(""), moq.ErrLog}, + {"Config", ffi.NewMoqErrorConfig(""), moq.ErrConfig}, } for _, test := range tests { diff --git a/go/wrapper/example_test.go b/go/wrapper/example_test.go index 2f15e1704df..c23b00e8db4 100644 --- a/go/wrapper/example_test.go +++ b/go/wrapper/example_test.go @@ -4,8 +4,10 @@ import ( "context" "fmt" "log" + "time" "moq.dev/moq" + moqmedia "moq.dev/moq/media" ) // Subscribe to a broadcast and print its catalog. These examples have no Output @@ -53,13 +55,14 @@ func ExampleClient_CreateBroadcast() { log.Fatal(err) } - media, err := broadcast.PublishAudio(moq.AudioFormatOpus, opusHead()) + media, err := moqmedia.NewAudioTrackProducer(broadcast, moqmedia.Named{}, moqmedia.AudioInit{Format: moqmedia.AudioFormatOpus, Data: opusHead()}) if err != nil { log.Fatal(err) } _ = broadcast.Announce(moq.Route{}) - if err := media.WriteFrame(moq.Frame{Payload: []byte("opus frame")}); err != nil { + pts := 20 * time.Millisecond + if err := media.WriteFrame(moq.Frame{Payload: []byte("opus frame"), Timestamp: &pts}); err != nil { log.Fatal(err) } @@ -84,18 +87,18 @@ func ExampleClient_Session_stats() { defer client.Close() stats := client.Session().Stats() - fmt.Println("rtt:", stats.RttUs) + fmt.Println("rtt:", stats.RTT) } // Publish a video track with catalog hints known before the first keyframe. -func ExampleBroadcastProducer_PublishVideo_videoHint() { +func Example_mediaVideoHint() { broadcast, err := moq.NewBroadcastProducer() if err != nil { log.Fatal(err) } defer broadcast.Close() - media, err := broadcast.PublishVideo(moq.VideoFormatAvc3, nil, moq.WithVideoHint(moq.VideoHint{})) + media, err := moqmedia.NewVideoTrackProducer(broadcast, moqmedia.Named{}, moqmedia.VideoInit{Format: moqmedia.VideoFormatAvc3, Data: nil, Hint: &moqmedia.VideoHint{}}) if err != nil { log.Fatal(err) } @@ -118,7 +121,7 @@ func ExampleListen() { } // Drive the accept loop directly to decide which sessions to admit. -func ExampleServer_Requests() { +func ExampleServer_All() { ctx := context.Background() server, err := moq.Listen(ctx, "127.0.0.1:4443", moq.WithTLSGenerate("localhost")) @@ -127,7 +130,7 @@ func ExampleServer_Requests() { } defer server.Close() - for req, err := range server.Requests(ctx) { + for req, err := range server.All(ctx) { if err != nil { if moq.IsShutdown(err) { break @@ -141,7 +144,7 @@ func ExampleServer_Requests() { continue } - session, err := req.Accept(ctx) + session, err := req.Accept(ctx, nil, nil) if err != nil { continue } diff --git a/go/wrapper/internal/bridge/call.go b/go/wrapper/internal/bridge/call.go new file mode 100644 index 00000000000..7e1f9a848ed --- /dev/null +++ b/go/wrapper/internal/bridge/call.go @@ -0,0 +1,150 @@ +// Package bridge is what moq.dev/moq shares with its subpackages: the context +// bridge for blocking FFI calls, and the generated handles behind the root +// package's wrapper types. +package bridge + +import ( + "context" + "iter" +) + +// Handle is a native object the wrapper owns. Every uniffi-generated object has +// Destroy, which drops the Rust-side Arc, and comparable so an optional one can +// be checked for nil before it is destroyed. +type Handle interface { + comparable + Destroy() +} + +// run races a blocking FFI call against ctx. +// +// `cancel` is the object's own cancel() for a call that owns a stream, session, +// or listener, so a cancelled Next still ends the stream and a cancelled Connect +// still tears the client down. The generated binding is given a context that +// does not cancel with the caller (see objectCallContext); otherwise it can +// return ctx.Err() into the result channel first and skip that abort. The +// blocked goroutine then unwinds on its own; the result channel is buffered so +// its send never blocks and it can't leak. +// +// release, for a call that returns a native handle, disposes of a result nobody +// received. Cancelling is not a retraction: select picks at random when the +// deadline and the result are both ready, so a call can succeed and still lose, +// and the handle it produced is live either way. +func run[T any](ctx context.Context, cancel func(), call func() (T, error), release func(T)) (T, error) { + // A context that is already done starts no native work at all. Racing it + // instead would let a call that resolves immediately (a subscribe to a track + // already there) win the select and hand back a live handle the caller asked + // not to have, and every such call has side effects on the way. + if err := ctx.Err(); err != nil { + var zero T + return zero, err + } + + type result struct { + val T + err error + } + ch := make(chan result, 1) + go func() { + val, err := call() + ch <- result{val, err} + }() + + select { + case <-ctx.Done(): + if cancel != nil { + cancel() + } + if release != nil { + // Exactly one receiver takes the result, so it is this one once the + // caller has given up. Waiting here would put the native unwind on + // the caller's deadline, so it happens off to the side. + go func() { + if r := <-ch; r.err == nil { + release(r.val) + } + }() + } + var zero T + return zero, ctx.Err() + case r := <-ch: + return r.val, r.err + } +} + +// Call runs a blocking FFI call that yields no handle of its own, so a +// result the caller never sees costs nothing to drop. +// +// cancel is the object's own cancel() for a call that owns its stream, session, +// or listener. One-shot calls pass ctx straight to the generated binding. +func Call[T any](ctx context.Context, cancel func(), call func(context.Context) (T, error)) (T, error) { + return run(ctx, cancel, func() (T, error) { return call(objectCallContext(ctx, cancel)) }, nil) +} + +// CallHandle is Call for a call that returns a native handle, which is +// destroyed rather than abandoned when the caller has already given up. Left to +// the Go finalizer it would stay live in the meantime: a subscription still +// running on the wire, or an incoming request accepted and never answered. +func CallHandle[T Handle](ctx context.Context, cancel func(), call func(context.Context) (T, error)) (T, error) { + return run(ctx, cancel, func() (T, error) { return call(objectCallContext(ctx, cancel)) }, releaseHandle[T]) +} + +// objectCallContext is the context the generated binding sees. Object-owned +// calls already abort via cancel; sharing the caller's ctx lets the binding +// return ctx.Err() first and skip that abort, leaving the stream, session, or +// request active. +func objectCallContext(ctx context.Context, cancel func()) context.Context { + if cancel == nil { + return ctx + } + return context.WithoutCancel(ctx) +} + +// CallErr is Call for calls that return only an error. +func CallErr(ctx context.Context, cancel func(), call func(context.Context) error) error { + _, err := Call(ctx, cancel, func(ctx context.Context) (struct{}, error) { + return struct{}{}, call(ctx) + }) + return err +} + +// releaseHandle drops a handle the caller never received. A successful call can +// still yield none (an Accept once the server has stopped), which is the zero +// value rather than something to destroy. +func releaseHandle[T Handle](val T) { + var zero T + if val != zero { + val.Destroy() + } +} + +// Seq turns an end-on-nil Next method (returning a pointer or interface) +// into a Go 1.23 range-over-func sequence. It yields (value, nil) for each item, +// yields (nil, err) once if a call fails, and stops cleanly when Next returns nil +// (the stream ended) or when the consumer breaks out of the range loop. +// +// for frame, err := range consumer.All(ctx) { +// if err != nil { +// if moq.IsShutdown(err) { break } +// return err +// } +// // use frame +// } +func Seq[T comparable](ctx context.Context, next func(context.Context) (T, error)) iter.Seq2[T, error] { + return func(yield func(T, error) bool) { + var zero T + for { + val, err := next(ctx) + if err != nil { + yield(zero, err) + return + } + if val == zero { + return + } + if !yield(val, nil) { + return + } + } + } +} diff --git a/go/wrapper/errors_internal_test.go b/go/wrapper/internal/bridge/call_test.go similarity index 89% rename from go/wrapper/errors_internal_test.go rename to go/wrapper/internal/bridge/call_test.go index 23aaf48316c..0d086a20c0c 100644 --- a/go/wrapper/errors_internal_test.go +++ b/go/wrapper/internal/bridge/call_test.go @@ -1,4 +1,4 @@ -package moq +package bridge import ( "context" @@ -36,7 +36,7 @@ func TestRunHandleReleasesAResultNobodyReceived(t *testing.T) { cancelCtx() }() - val, err := runHandle(ctx, nil, func(context.Context) (*stubHandle, error) { + val, err := CallHandle(ctx, nil, func(context.Context) (*stubHandle, error) { close(started) <-finish return stub, nil @@ -70,7 +70,7 @@ func TestRunHandleIgnoresAResultThatIsNoHandle(t *testing.T) { cancelCtx() }() - _, err := runHandle(ctx, nil, func(context.Context) (*stubHandle, error) { + _, err := CallHandle(ctx, nil, func(context.Context) (*stubHandle, error) { close(started) <-finish close(returned) @@ -94,7 +94,7 @@ func TestRunHandleStartsNothingForADoneContext(t *testing.T) { cancelCtx() called := false - val, err := runHandle(ctx, nil, func(context.Context) (*stubHandle, error) { + val, err := CallHandle(ctx, nil, func(context.Context) (*stubHandle, error) { called = true return &stubHandle{destroyed: make(chan struct{})}, nil }) @@ -118,7 +118,7 @@ func TestRunHidesCallerCancelFromAnObjectOwnedCall(t *testing.T) { defer cancelCtx() got := make(chan context.Context, 1) - _, err := runCancellable(ctx, func() {}, func(callCtx context.Context) (int, error) { + _, err := Call(ctx, func() {}, func(callCtx context.Context) (int, error) { got <- callCtx return 1, nil }) @@ -139,7 +139,7 @@ func TestRunPassesCallerCancelWhenThereIsNoObjectCancel(t *testing.T) { defer cancelCtx() got := make(chan context.Context, 1) - _, err := runCancellable(ctx, nil, func(callCtx context.Context) (int, error) { + _, err := Call(ctx, nil, func(callCtx context.Context) (int, error) { got <- callCtx return 1, nil }) @@ -167,7 +167,7 @@ func TestRunCancelsTheObjectWhenCtxEndsDuringTheCall(t *testing.T) { cancelCtx() }() - _, err := runCancellable(ctx, func() { close(objectCancelled) }, func(callCtx context.Context) (int, error) { + _, err := Call(ctx, func() { close(objectCancelled) }, func(callCtx context.Context) (int, error) { close(started) <-objectCancelled select { @@ -198,7 +198,7 @@ func TestRunCancelsTheObjectWhenCtxEndsDuringTheCall(t *testing.T) { func TestRunHandleReturnsAResultThatWon(t *testing.T) { stub := &stubHandle{destroyed: make(chan struct{})} - val, err := runHandle(context.Background(), nil, func(context.Context) (*stubHandle, error) { + val, err := CallHandle(context.Background(), nil, func(context.Context) (*stubHandle, error) { return stub, nil }) if err != nil { diff --git a/go/wrapper/internal/bridge/handles.go b/go/wrapper/internal/bridge/handles.go new file mode 100644 index 00000000000..b37eaeeeec7 --- /dev/null +++ b/go/wrapper/internal/bridge/handles.go @@ -0,0 +1,37 @@ +package bridge + +import ( + "time" + + ffi "moq.dev/moq-ffi/moq" +) + +// The generated handles behind the root package's wrapper types, which it keeps +// unexported. Package moq sets these from init, and every subpackage imports +// moq, so they are set before a subpackage can call them. They take and return +// `any` because this package cannot import moq's types without a cycle. +var ( + // BroadcastProducer returns the handle behind a *moq.BroadcastProducer. + BroadcastProducer func(any) *ffi.MoqBroadcastProducer + // TrackProducer returns the handle behind a *moq.TrackProducer. + TrackProducer func(any) *ffi.MoqTrackProducer + // TrackConsumer returns the handle behind a *moq.TrackConsumer. + TrackConsumer func(any) *ffi.MoqTrackConsumer + // TrackDemand wraps a handle as a *moq.TrackDemand. + TrackDemand func(*ffi.MoqTrackDemand) any +) + +// BroadcastConsumer returns the handle behind a *moq.BroadcastConsumer. +var BroadcastConsumer func(any) *ffi.MoqBroadcastConsumer + +// TrackRequest returns the handle behind a *moq.TrackRequest. +var TrackRequest func(any) *ffi.MoqTrackRequest + +// Frame converts a moq.Frame to the generated record. +var Frame func(any) (ffi.MoqFrame, error) + +// Subscription converts a *moq.Subscription to the generated record. +var Subscription func(any) (*ffi.MoqSubscription, error) + +// Duration converts a duration into nonnegative microseconds. +var Duration func(time.Duration, string) (uint64, error) diff --git a/go/wrapper/iter.go b/go/wrapper/iter.go deleted file mode 100644 index aaec05d0771..00000000000 --- a/go/wrapper/iter.go +++ /dev/null @@ -1,37 +0,0 @@ -package moq - -import ( - "context" - "iter" -) - -// streamSeq turns an end-on-nil Next method (returning a pointer or interface) -// into a Go 1.23 range-over-func sequence. It yields (value, nil) for each item, -// yields (nil, err) once if a call fails, and stops cleanly when Next returns nil -// (the stream ended) or when the consumer breaks out of the range loop. -// -// for frame, err := range consumer.Frames(ctx) { -// if err != nil { -// if moq.IsShutdown(err) { break } -// return err -// } -// // use frame -// } -func streamSeq[T comparable](ctx context.Context, next func(context.Context) (T, error)) iter.Seq2[T, error] { - return func(yield func(T, error) bool) { - var zero T - for { - val, err := next(ctx) - if err != nil { - yield(zero, err) - return - } - if val == zero { - return - } - if !yield(val, nil) { - return - } - } - } -} diff --git a/go/wrapper/json.go b/go/wrapper/json.go deleted file mode 100644 index a1ba6f34086..00000000000 --- a/go/wrapper/json.go +++ /dev/null @@ -1,205 +0,0 @@ -package moq - -import ( - "context" - "encoding/json" - "iter" - - ffi "moq.dev/moq-ffi/moq" -) - -// DefaultDeltaRatio is used when [JSONSnapshotOptions.DeltaRatio] is nil. Go has no field -// defaults, so unlike the other bindings this restates the `#[uniffi(default = 8)]` on -// moq-ffi's MoqJsonSnapshotConfig (itself pinned to moq-json's default by a Rust test). -const DefaultDeltaRatio uint32 = 8 - -// JSONSnapshotOptions configures publishing a lossy latest-value JSON track. -type JSONSnapshotOptions struct { - // DeltaRatio controls how aggressively deltas replace full snapshots. Nil uses - // [DefaultDeltaRatio]; a pointer to zero disables deltas entirely. - DeltaRatio *uint32 - // Compression enables group-scoped DEFLATE and must match on the consumer. - Compression bool -} - -// deltaRatio resolves the configured ratio, falling back to the shared default. -func (o JSONSnapshotOptions) deltaRatio() uint32 { - if o.DeltaRatio == nil { - return DefaultDeltaRatio - } - return *o.DeltaRatio -} - -// JSONStreamOptions configures publishing a lossless append-log JSON track. -type JSONStreamOptions struct { - // Compression enables group-scoped DEFLATE and must match on the consumer. - Compression bool -} - -// JSONSubscribeOptions configures subscribing to either kind of JSON track. -// -// Delta encoding is a producer-side choice the consumer reconstructs automatically, -// so only the compression setting has to match. -type JSONSubscribeOptions struct { - // Compression enables group-scoped DEFLATE and must match the producer. - Compression bool -} - -// JSONSnapshotProducer publishes a JSON value as lossy latest state. -type JSONSnapshotProducer struct { - inner *ffi.MoqJsonSnapshotProducer -} - -// Update publishes value as a JSON snapshot or delta. It is a no-op when unchanged. -func (p *JSONSnapshotProducer) Update(value any) error { - encoded, err := json.Marshal(value) - if err != nil { - return err - } - return p.inner.Update(string(encoded)) -} - -// Demand returns a watch-only handle to whether the track has subscribers. -func (p *JSONSnapshotProducer) Demand() (*TrackDemand, error) { - inner, err := p.inner.Demand() - if err != nil { - return nil, err - } - return &TrackDemand{inner: inner}, nil -} - -// Finish closes the snapshot track. -func (p *JSONSnapshotProducer) Finish() error { - return p.inner.Finish() -} - -// JSONStreamProducer publishes an ordered lossless log of JSON records. -type JSONStreamProducer struct { - inner *ffi.MoqJsonStreamProducer -} - -// Append adds one JSON record to the log. -func (p *JSONStreamProducer) Append(value any) error { - encoded, err := json.Marshal(value) - if err != nil { - return err - } - return p.inner.Append(string(encoded)) -} - -// Demand returns a watch-only handle to whether the track has subscribers. -func (p *JSONStreamProducer) Demand() (*TrackDemand, error) { - inner, err := p.inner.Demand() - if err != nil { - return nil, err - } - return &TrackDemand{inner: inner}, nil -} - -// Finish closes the stream track. -func (p *JSONStreamProducer) Finish() error { - return p.inner.Finish() -} - -// JSONSnapshotConsumer receives the latest reconstructed JSON value. -type JSONSnapshotConsumer struct { - inner *ffi.MoqJsonSnapshotConsumer -} - -// Next returns the next JSON value, or nil when the track ends. -func (c *JSONSnapshotConsumer) Next(ctx context.Context) (*json.RawMessage, error) { - value, err := runCancellable(ctx, c.inner.Cancel, c.inner.Next) - if err != nil || value == nil { - return nil, err - } - decoded := json.RawMessage(*value) - return &decoded, nil -} - -// Values ranges over reconstructed JSON values until the track ends. -func (c *JSONSnapshotConsumer) Values(ctx context.Context) iter.Seq2[*json.RawMessage, error] { - return streamSeq(ctx, c.Next) -} - -// Cancel stops current and future reads. -func (c *JSONSnapshotConsumer) Cancel() { - c.inner.Cancel() -} - -// JSONStreamConsumer receives every JSON record in order. -type JSONStreamConsumer struct { - inner *ffi.MoqJsonStreamConsumer -} - -// Next returns the next JSON record, or nil when the track ends. -func (c *JSONStreamConsumer) Next(ctx context.Context) (*json.RawMessage, error) { - value, err := runCancellable(ctx, c.inner.Cancel, c.inner.Next) - if err != nil || value == nil { - return nil, err - } - decoded := json.RawMessage(*value) - return &decoded, nil -} - -// Values ranges over every JSON record until the track ends. -func (c *JSONStreamConsumer) Values(ctx context.Context) iter.Seq2[*json.RawMessage, error] { - return streamSeq(ctx, c.Next) -} - -// Cancel stops current and future reads. -func (c *JSONStreamConsumer) Cancel() { - c.inner.Cancel() -} - -// PublishJSONSnapshot opens a lossy latest-value JSON track. -func (b *BroadcastProducer) PublishJSONSnapshot( - name string, - options JSONSnapshotOptions, -) (*JSONSnapshotProducer, error) { - inner, err := b.inner.PublishJsonSnapshot(name, ffi.MoqJsonSnapshotConfig{ - DeltaRatio: options.deltaRatio(), - Compression: options.Compression, - }) - if err != nil { - return nil, err - } - return &JSONSnapshotProducer{inner: inner}, nil -} - -// PublishJSONStream opens a lossless append-log JSON track. -func (b *BroadcastProducer) PublishJSONStream(name string, options JSONStreamOptions) (*JSONStreamProducer, error) { - inner, err := b.inner.PublishJsonStream(name, ffi.MoqJsonStreamConfig{Compression: options.Compression}) - if err != nil { - return nil, err - } - return &JSONStreamProducer{inner: inner}, nil -} - -// SubscribeJSONSnapshot subscribes to a lossy latest-value JSON track. -func (b *BroadcastConsumer) SubscribeJSONSnapshot( - ctx context.Context, - name string, - options JSONSubscribeOptions, -) (*JSONSnapshotConsumer, error) { - inner, err := b.inner.SubscribeJsonSnapshot(ctx, name, ffi.MoqJsonSnapshotConfig{ - DeltaRatio: 0, - Compression: options.Compression, - }) - if err != nil { - return nil, err - } - return &JSONSnapshotConsumer{inner: inner}, nil -} - -// SubscribeJSONStream subscribes to a lossless append-log JSON track. -func (b *BroadcastConsumer) SubscribeJSONStream( - ctx context.Context, - name string, - options JSONSubscribeOptions, -) (*JSONStreamConsumer, error) { - inner, err := b.inner.SubscribeJsonStream(ctx, name, ffi.MoqJsonStreamConfig{Compression: options.Compression}) - if err != nil { - return nil, err - } - return &JSONStreamConsumer{inner: inner}, nil -} diff --git a/go/wrapper/json/json.go b/go/wrapper/json/json.go new file mode 100644 index 00000000000..ff217590cab --- /dev/null +++ b/go/wrapper/json/json.go @@ -0,0 +1,220 @@ +// Package json publishes and consumes JSON tracks, mirroring the moq-json crate. +// +// Each type wraps a track from package moq: a producer takes over a +// [moq.TrackProducer] and advertises it in its broadcast's catalog, and a +// consumer takes over a [moq.TrackConsumer] that has not read a group yet. The +// track can come from a request as well as by name. Import it under an alias +// next to encoding/json: +// +// import moqjson "moq.dev/moq/json" +package json + +import ( + "context" + stdjson "encoding/json" + "iter" + + "moq.dev/moq" + ffi "moq.dev/moq-ffi/moq" + "moq.dev/moq/internal/bridge" +) + +// DefaultDeltaRatio is used when [SnapshotOptions.DeltaRatio] is nil. Go has no field +// defaults, so unlike the other bindings this restates the `#[uniffi(default = 8)]` on +// moq-ffi's MoqJsonSnapshotConfig (itself pinned to moq-json's default by a Rust test). +const DefaultDeltaRatio uint32 = 8 + +// SnapshotOptions configures a lossy latest-value JSON producer. +type SnapshotOptions struct { + // DeltaRatio controls how aggressively deltas replace full snapshots. Nil uses + // [DefaultDeltaRatio]; a pointer to zero disables deltas entirely. + DeltaRatio *uint32 + // Compression enables group-scoped DEFLATE and must match on the consumer. + Compression bool +} + +// deltaRatio resolves the configured ratio, falling back to the shared default. +func (o SnapshotOptions) deltaRatio() uint32 { + if o.DeltaRatio == nil { + return DefaultDeltaRatio + } + return *o.DeltaRatio +} + +// StreamOptions configures a lossless append-log JSON producer. +type StreamOptions struct { + // Compression enables group-scoped DEFLATE and must match on the consumer. + Compression bool +} + +// ConsumerOptions configures reading either kind of JSON track. +// +// Delta encoding is a producer-side choice the consumer reconstructs automatically, +// so only the compression setting has to match. +type ConsumerOptions struct { + // Compression enables group-scoped DEFLATE and must match the producer. + Compression bool +} + +// SnapshotProducer publishes a JSON value as lossy latest state. +type SnapshotProducer struct { + inner *ffi.MoqJsonSnapshotProducer +} + +// NewSnapshotProducer publishes track as a lossy latest-value JSON track, advertised in +// broadcast's catalog until it finishes. It takes over track, whose handle is closed +// afterward, and fails if the catalog already carries the track's name. +func NewSnapshotProducer(broadcast *moq.BroadcastProducer, track *moq.TrackProducer, options SnapshotOptions) (*SnapshotProducer, error) { + inner, err := ffi.NewMoqJsonSnapshotProducer( + bridge.BroadcastProducer(broadcast), + bridge.TrackProducer(track), + ffi.MoqJsonSnapshotConfig{DeltaRatio: options.deltaRatio(), Compression: options.Compression}, + ) + if err != nil { + return nil, err + } + return &SnapshotProducer{inner: inner}, nil +} + +// Update publishes value as a JSON snapshot or delta. It is a no-op when unchanged. +func (p *SnapshotProducer) Update(value any) error { + encoded, err := stdjson.Marshal(value) + if err != nil { + return err + } + return p.inner.Update(string(encoded)) +} + +// Demand returns a watch-only handle to whether the track has subscribers. +func (p *SnapshotProducer) Demand() (*moq.TrackDemand, error) { + inner, err := p.inner.Demand() + if err != nil { + return nil, err + } + return bridge.TrackDemand(inner).(*moq.TrackDemand), nil +} + +// Finish closes the snapshot track. +func (p *SnapshotProducer) Finish() error { + return p.inner.Finish() +} + +// StreamProducer publishes an ordered lossless log of JSON records. +type StreamProducer struct { + inner *ffi.MoqJsonStreamProducer +} + +// NewStreamProducer publishes track as a lossless append-log JSON track, advertised in +// broadcast's catalog until it finishes. It takes over track, whose handle is closed +// afterward, and fails if the catalog already carries the track's name. +func NewStreamProducer(broadcast *moq.BroadcastProducer, track *moq.TrackProducer, options StreamOptions) (*StreamProducer, error) { + inner, err := ffi.NewMoqJsonStreamProducer( + bridge.BroadcastProducer(broadcast), + bridge.TrackProducer(track), + ffi.MoqJsonStreamConfig{Compression: options.Compression}, + ) + if err != nil { + return nil, err + } + return &StreamProducer{inner: inner}, nil +} + +// Append adds one JSON record to the log. +func (p *StreamProducer) Append(value any) error { + encoded, err := stdjson.Marshal(value) + if err != nil { + return err + } + return p.inner.Append(string(encoded)) +} + +// Demand returns a watch-only handle to whether the track has subscribers. +func (p *StreamProducer) Demand() (*moq.TrackDemand, error) { + inner, err := p.inner.Demand() + if err != nil { + return nil, err + } + return bridge.TrackDemand(inner).(*moq.TrackDemand), nil +} + +// Finish closes the stream track. +func (p *StreamProducer) Finish() error { + return p.inner.Finish() +} + +// SnapshotConsumer receives the latest reconstructed JSON value. +type SnapshotConsumer struct { + inner *ffi.MoqJsonSnapshotConsumer +} + +// NewSnapshotConsumer reads track as a lossy latest-value JSON track. It takes over +// track, whose handle is closed afterward, and fails if track has already read a group. +func NewSnapshotConsumer(track *moq.TrackConsumer, options ConsumerOptions) (*SnapshotConsumer, error) { + // DeltaRatio is producer-only; the consumer ignores it. + inner, err := ffi.NewMoqJsonSnapshotConsumer( + bridge.TrackConsumer(track), + ffi.MoqJsonSnapshotConfig{Compression: options.Compression}, + ) + if err != nil { + return nil, err + } + return &SnapshotConsumer{inner: inner}, nil +} + +// Next returns the next JSON value, or nil when the track ends. +func (c *SnapshotConsumer) Next(ctx context.Context) (*stdjson.RawMessage, error) { + value, err := bridge.Call(ctx, c.inner.Cancel, c.inner.Next) + if err != nil || value == nil { + return nil, err + } + decoded := stdjson.RawMessage(*value) + return &decoded, nil +} + +// All ranges over reconstructed JSON values until the track ends. +func (c *SnapshotConsumer) All(ctx context.Context) iter.Seq2[*stdjson.RawMessage, error] { + return bridge.Seq(ctx, c.Next) +} + +// Cancel stops current and future reads. +func (c *SnapshotConsumer) Cancel() { + c.inner.Cancel() +} + +// StreamConsumer receives every JSON record in order. +type StreamConsumer struct { + inner *ffi.MoqJsonStreamConsumer +} + +// NewStreamConsumer reads track as a lossless append-log JSON track. It takes over +// track, whose handle is closed afterward, and fails if track has already read a group. +func NewStreamConsumer(track *moq.TrackConsumer, options ConsumerOptions) (*StreamConsumer, error) { + inner, err := ffi.NewMoqJsonStreamConsumer( + bridge.TrackConsumer(track), + ffi.MoqJsonStreamConfig{Compression: options.Compression}, + ) + if err != nil { + return nil, err + } + return &StreamConsumer{inner: inner}, nil +} + +// Next returns the next JSON record, or nil when the track ends. +func (c *StreamConsumer) Next(ctx context.Context) (*stdjson.RawMessage, error) { + value, err := bridge.Call(ctx, c.inner.Cancel, c.inner.Next) + if err != nil || value == nil { + return nil, err + } + decoded := stdjson.RawMessage(*value) + return &decoded, nil +} + +// All ranges over every JSON record until the track ends. +func (c *StreamConsumer) All(ctx context.Context) iter.Seq2[*stdjson.RawMessage, error] { + return bridge.Seq(ctx, c.Next) +} + +// Cancel stops current and future reads. +func (c *StreamConsumer) Cancel() { + c.inner.Cancel() +} diff --git a/go/wrapper/json/json_test.go b/go/wrapper/json/json_test.go new file mode 100644 index 00000000000..f5105f420f3 --- /dev/null +++ b/go/wrapper/json/json_test.go @@ -0,0 +1,181 @@ +package json_test + +import ( + "context" + "encoding/json" + "errors" + "testing" + "time" + + "moq.dev/moq" + moqjson "moq.dev/moq/json" +) + +// testTimeout bounds the blocking stream calls so a regression fails the test +// job instead of hanging it. +const testTimeout = 10 * time.Second + +// newBroadcast returns a standalone broadcast and a consumer reading it. +func newBroadcast(t *testing.T) (*moq.BroadcastProducer, *moq.BroadcastConsumer) { + t.Helper() + broadcast, err := moq.NewBroadcastProducer() + if err != nil { + t.Fatal(err) + } + consumer, err := broadcast.Consume() + if err != nil { + t.Fatal(err) + } + return broadcast, consumer +} + +// publishTrack creates a raw track for a JSON producer to take over. +func publishTrack(t *testing.T, broadcast *moq.BroadcastProducer, name string) *moq.TrackProducer { + t.Helper() + track, err := broadcast.PublishTrack(name, nil) + if err != nil { + t.Fatal(err) + } + return track +} + +// subscribeTrack subscribes to a raw track for a JSON consumer to take over. +func subscribeTrack(ctx context.Context, t *testing.T, consumer *moq.BroadcastConsumer, name string) *moq.TrackConsumer { + t.Helper() + track, err := consumer.SubscribeTrack(ctx, name, nil) + if err != nil { + t.Fatal(err) + } + return track +} + +func TestTracks(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + broadcast, consumer := newBroadcast(t) + + snapshot, err := moqjson.NewSnapshotProducer(broadcast, publishTrack(t, broadcast, "status"), moqjson.SnapshotOptions{Compression: true}) + if err != nil { + t.Fatal(err) + } + snapshotConsumer, err := moqjson.NewSnapshotConsumer(subscribeTrack(ctx, t, consumer, "status"), moqjson.ConsumerOptions{Compression: true}) + if err != nil { + t.Fatal(err) + } + defer snapshotConsumer.Cancel() + if err := snapshot.Update(map[string]any{"viewers": 42}); err != nil { + t.Fatal(err) + } + value, err := snapshotConsumer.Next(ctx) + if err != nil { + t.Fatal(err) + } + var decoded map[string]any + if err := json.Unmarshal(*value, &decoded); err != nil { + t.Fatal(err) + } + if decoded["viewers"] != float64(42) { + t.Fatalf("snapshot = %s", *value) + } + + stream, err := moqjson.NewStreamProducer(broadcast, publishTrack(t, broadcast, "events"), moqjson.StreamOptions{Compression: true}) + if err != nil { + t.Fatal(err) + } + streamConsumer, err := moqjson.NewStreamConsumer(subscribeTrack(ctx, t, consumer, "events"), moqjson.ConsumerOptions{Compression: true}) + if err != nil { + t.Fatal(err) + } + defer streamConsumer.Cancel() + if err := stream.Append(map[string]any{"n": 1}); err != nil { + t.Fatal(err) + } + record, err := streamConsumer.Next(ctx) + if err != nil { + t.Fatal(err) + } + if string(*record) != `{"n":1}` { + t.Fatalf("record = %s", *record) + } + + if err := snapshot.Finish(); err != nil { + t.Fatal(err) + } + if err := stream.Finish(); err != nil { + t.Fatal(err) + } +} + +// A producer takes over its track, so the raw handle is closed afterward. +func TestProducerTakesTheTrack(t *testing.T) { + broadcast, _ := newBroadcast(t) + track := publishTrack(t, broadcast, "status") + snapshot, err := moqjson.NewSnapshotProducer(broadcast, track, moqjson.SnapshotOptions{}) + if err != nil { + t.Fatal(err) + } + defer snapshot.Finish() + if _, err := track.Demand(); !errors.Is(err, moq.ErrClosed) { + t.Fatalf("Demand after take = %v, want ErrClosed", err) + } +} + +func TestDemand(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + broadcast, consumer := newBroadcast(t) + + snapshot, err := moqjson.NewSnapshotProducer(broadcast, publishTrack(t, broadcast, "status"), moqjson.SnapshotOptions{}) + if err != nil { + t.Fatal(err) + } + stream, err := moqjson.NewStreamProducer(broadcast, publishTrack(t, broadcast, "events"), moqjson.StreamOptions{}) + if err != nil { + t.Fatal(err) + } + snapshotDemand, err := snapshot.Demand() + if err != nil { + t.Fatal(err) + } + streamDemand, err := stream.Demand() + if err != nil { + t.Fatal(err) + } + if name := snapshotDemand.Name(); name != "status" { + t.Fatalf("Name = %q, want status", name) + } + if snapshotDemand.IsUsed() { + t.Fatal("IsUsed before any subscriber") + } + + snapshotConsumer, err := moqjson.NewSnapshotConsumer(subscribeTrack(ctx, t, consumer, "status"), moqjson.ConsumerOptions{}) + if err != nil { + t.Fatal(err) + } + streamConsumer, err := moqjson.NewStreamConsumer(subscribeTrack(ctx, t, consumer, "events"), moqjson.ConsumerOptions{}) + if err != nil { + t.Fatal(err) + } + if err := snapshotDemand.Used(ctx); err != nil { + t.Fatal(err) + } + if err := streamDemand.Used(ctx); err != nil { + t.Fatal(err) + } + + snapshotConsumer.Cancel() + streamConsumer.Cancel() + if err := snapshotDemand.Unused(ctx); err != nil { + t.Fatal(err) + } + if err := streamDemand.Unused(ctx); err != nil { + t.Fatal(err) + } + + if err := snapshot.Finish(); err != nil { + t.Fatal(err) + } + if err := snapshotDemand.Used(ctx); !errors.Is(err, moq.ErrClosed) { + t.Fatalf("Used after Finish = %v, want ErrClosed", err) + } +} diff --git a/go/wrapper/media/media.go b/go/wrapper/media/media.go new file mode 100644 index 00000000000..f0cb6fdc416 --- /dev/null +++ b/go/wrapper/media/media.go @@ -0,0 +1,503 @@ +// Package media owns catalogs, encoded media imports, and container consumers. +package media + +import ( + "context" + "encoding/json" + "errors" + "iter" + "time" + + "moq.dev/moq" + ffi "moq.dev/moq-ffi/moq" + "moq.dev/moq/internal/bridge" +) + +type ( + // Audio describes one audio rendition in a broadcast catalog: codec, sample rate, channel count, and container. + Audio = ffi.MoqAudio + // Catalog is a broadcast's manifest: its video and audio renditions plus display metadata. + Catalog = ffi.MoqCatalog + // Dimensions is a width and height in pixels. + Dimensions = ffi.MoqDimensions + // Video describes one catalog rendition, including whether the publisher recommends temporarily avoiding it. + Video = ffi.MoqVideo + // VideoHint supplies catalog fields a video stream can't reveal itself, such as bitrate, filling only the gaps. + VideoHint = ffi.MoqVideoHint + // AudioFormat is a single audio codec an importer can parse. + AudioFormat = ffi.MoqAudioFormat + // VideoFormat is a single video codec an importer can parse. + VideoFormat = ffi.MoqVideoFormat + // ContainerFormat is a container that publishes its own tracks. + ContainerFormat = ffi.MoqContainerFormat + // VideoProperties holds catalog properties shared by every video rendition; nil fields clear those properties. + VideoProperties = ffi.MoqVideoProperties + // Container selects how subscribed media frames are demuxed. Build one with + // LegacyContainer, CmafContainer, or LocContainer. + Container = ffi.MoqContainer + // ContainerLegacy is the legacy hang container variant of Container; build one with LegacyContainer. + ContainerLegacy = ffi.MoqContainerLegacy + // ContainerCmaf is the CMAF (fMP4) container variant of Container, carrying its init segment; build one with CmafContainer. + ContainerCmaf = ffi.MoqContainerCmaf + // ContainerLoc is the low-overhead container variant of Container; build one with LocContainer. + ContainerLoc = ffi.MoqContainerLoc + + // AudioInit describes an audio codec and its initialization bytes. + AudioInit = ffi.MoqAudioInit + // VideoInit describes a video codec, initialization bytes, and hints. + VideoInit = ffi.MoqVideoInit + // ContainerInit describes a container and its leading bytes. + ContainerInit = ffi.MoqContainerInit +) + +// MediaFrame is a container-decoded payload, presentation timestamp, and keyframe flag. +type MediaFrame struct { + Payload []byte + Timestamp time.Duration + Keyframe bool +} + +// Target is the named or requested track an importer publishes. +type Target interface{ target() } + +// Named publishes a new track with a chosen name, or a format-derived unique name. +type Named struct{ Name *string } + +func (Named) target() {} + +// Requested publishes a subscriber's pending track request. +type Requested struct{ Request *moq.TrackRequest } + +func (Requested) target() {} + +func targetFFI(target Target) (ffi.MoqMediaTarget, error) { + switch target := target.(type) { + case Named: + return ffi.MoqMediaTargetNamed{Name: target.Name}, nil + case Requested: + if target.Request != nil { + return ffi.MoqMediaTargetRequested{Request: bridge.TrackRequest(target.Request)}, nil + } + case *Named: + if target != nil { + return targetFFI(*target) + } + case *Requested: + if target != nil { + return targetFFI(*target) + } + } + return nil, errors.New("media: a named target or a non-nil requested track is required") +} + +// CatalogProducer updates a catalog without keeping its broadcast open. +type CatalogProducer struct{ inner *ffi.MoqMediaCatalogProducer } + +// NewCatalogProducer creates a weak catalog handle for broadcast. +func NewCatalogProducer(broadcast *moq.BroadcastProducer) (*CatalogProducer, error) { + inner, err := ffi.NewMoqMediaCatalogProducer(bridge.BroadcastProducer(broadcast)) + if err != nil { + return nil, err + } + return &CatalogProducer{inner: inner}, nil +} + +// SetVideoProperties replaces the video properties shared by all renditions. +func (c *CatalogProducer) SetVideoProperties(properties VideoProperties) error { + return c.inner.SetVideoProperties(properties) +} + +// SetSection publishes an application catalog section from a JSON-serializable value. +func (c *CatalogProducer) SetSection(name string, value any) error { + data, err := json.Marshal(value) + if err != nil { + return err + } + return c.inner.SetSection(name, string(data)) +} + +// RemoveSection removes an application catalog section if present. +func (c *CatalogProducer) RemoveSection(name string) error { return c.inner.RemoveSection(name) } + +// NewAudioTrackProducer imports encoded audio frames on target. +func NewAudioTrackProducer(broadcast *moq.BroadcastProducer, target Target, init AudioInit) (*TrackProducer, error) { + resolved, err := targetFFI(target) + if err != nil { + return nil, err + } + inner, err := ffi.MoqMediaTrackProducerAudio(bridge.BroadcastProducer(broadcast), resolved, init) + if err != nil { + return nil, err + } + return &TrackProducer{inner: inner}, nil +} + +// NewVideoTrackProducer imports encoded video frames on target. +func NewVideoTrackProducer(broadcast *moq.BroadcastProducer, target Target, init VideoInit) (*TrackProducer, error) { + resolved, err := targetFFI(target) + if err != nil { + return nil, err + } + inner, err := ffi.MoqMediaTrackProducerVideo(bridge.BroadcastProducer(broadcast), resolved, init) + if err != nil { + return nil, err + } + return &TrackProducer{inner: inner}, nil +} + +// NewVideoTrackStreamProducer imports a video byte stream on target. +func NewVideoTrackStreamProducer(broadcast *moq.BroadcastProducer, target Target, init VideoInit) (*TrackStreamProducer, error) { + resolved, err := targetFFI(target) + if err != nil { + return nil, err + } + inner, err := ffi.MoqMediaTrackStreamProducerVideo(bridge.BroadcastProducer(broadcast), resolved, init) + if err != nil { + return nil, err + } + return &TrackStreamProducer{inner: inner}, nil +} + +// NewContainerProducer imports complete container chunks. +func NewContainerProducer(broadcast *moq.BroadcastProducer, init ContainerInit) (*ContainerProducer, error) { + inner, err := ffi.NewMoqMediaContainerProducer(bridge.BroadcastProducer(broadcast), init) + if err != nil { + return nil, err + } + return &ContainerProducer{inner: inner}, nil +} + +// NewContainerStreamProducer imports a container byte stream. +func NewContainerStreamProducer(broadcast *moq.BroadcastProducer, format ContainerFormat) (*ContainerStreamProducer, error) { + inner, err := ffi.NewMoqMediaContainerStreamProducer(bridge.BroadcastProducer(broadcast), format) + if err != nil { + return nil, err + } + return &ContainerStreamProducer{inner: inner}, nil +} + +// NewCatalogConsumer subscribes to broadcast's catalog snapshots. +func NewCatalogConsumer(ctx context.Context, broadcast *moq.BroadcastConsumer) (*CatalogConsumer, error) { + inner, err := ffi.MoqMediaCatalogConsumerSubscribe(ctx, bridge.BroadcastConsumer(broadcast)) + if err != nil { + return nil, err + } + return &CatalogConsumer{inner: inner}, nil +} + +// ContainerConfig names a media track, its container, and live delivery options. +type ContainerConfig struct { + Name string + Container Container + Subscription *moq.Subscription +} + +// NewContainerConsumer subscribes to and decodes a media track. +func NewContainerConsumer(ctx context.Context, broadcast *moq.BroadcastConsumer, config ContainerConfig) (*ContainerConsumer, error) { + sub, err := bridge.Subscription(config.Subscription) + if err != nil { + return nil, err + } + inner, err := ffi.MoqMediaContainerConsumerSubscribe(ctx, bridge.BroadcastConsumer(broadcast), ffi.MoqMediaContainerConfig{Name: config.Name, Container: config.Container, Subscription: sub}) + if err != nil { + return nil, err + } + return &ContainerConsumer{inner: inner}, nil +} + +// ContainerGroupConfig names a group, its container, and fetch delivery options. +type ContainerGroupConfig struct { + Name string + Sequence uint64 + Container Container + Options *moq.FetchGroupOptions +} + +// NewContainerGroupConsumer fetches and decodes exactly one media group. +func NewContainerGroupConsumer(ctx context.Context, broadcast *moq.BroadcastConsumer, config ContainerGroupConfig) (*ContainerGroupConsumer, error) { + inner, err := ffi.MoqMediaContainerGroupConsumerFetch(ctx, bridge.BroadcastConsumer(broadcast), ffi.MoqMediaContainerGroupConfig{Name: config.Name, Sequence: config.Sequence, Container: config.Container, Options: config.Options}) + if err != nil { + return nil, err + } + return &ContainerGroupConsumer{inner: inner}, nil +} + +// LegacyContainer selects the legacy hang container for a media subscription. +func LegacyContainer() Container { + return ContainerLegacy{} +} + +// CmafContainer selects the CMAF (fMP4) container for a media subscription, +// initialized from the given init segment. +func CmafContainer(init []byte) Container { + return ContainerCmaf{Init: init} +} + +// LocContainer selects the low-overhead container for a media subscription. +func LocContainer() Container { + return ContainerLoc{} +} + +// AudioFormat values: a single audio codec an importer can parse. +const ( + // AudioFormatAac is Advanced Audio Coding, configured by an AudioSpecificConfig. + AudioFormatAac = ffi.MoqAudioFormatAac + // AudioFormatOpus is Opus, configured by an OpusHead. + AudioFormatOpus = ffi.MoqAudioFormatOpus + // AudioFormatFlac is FLAC, configured by its STREAMINFO block. + AudioFormatFlac = ffi.MoqAudioFormatFlac + // AudioFormatMp3 is MPEG-1/2 Audio Layer III. + AudioFormatMp3 = ffi.MoqAudioFormatMp3 +) + +// VideoFormat values: a single video codec an importer can parse. The two H.26x pairs +// differ by framing, not codec: Avc1/Hvc1 are length-prefixed with an out-of-band config +// record, Avc3/Hev1 are Annex-B with the parameter sets inline. +const ( + // VideoFormatAvc1 is H.264 with length-prefixed NALUs and an out-of-band avcC. + VideoFormatAvc1 = ffi.MoqVideoFormatAvc1 + // VideoFormatAvc3 is H.264 in Annex-B with inline SPS/PPS. + VideoFormatAvc3 = ffi.MoqVideoFormatAvc3 + // VideoFormatHvc1 is H.265 with length-prefixed NALUs and an out-of-band hvcC. + VideoFormatHvc1 = ffi.MoqVideoFormatHvc1 + // VideoFormatHev1 is H.265 in Annex-B with inline parameter sets. + VideoFormatHev1 = ffi.MoqVideoFormatHev1 + // VideoFormatAv01 is AV1. + VideoFormatAv01 = ffi.MoqVideoFormatAv01 + // VideoFormatVp8 is VP8. + VideoFormatVp8 = ffi.MoqVideoFormatVp8 + // VideoFormatVp9 is VP9. + VideoFormatVp9 = ffi.MoqVideoFormatVp9 +) + +// ContainerFormat values: a container that publishes its own tracks. +const ( + // ContainerFormatFmp4 is fragmented MP4 / CMAF. + ContainerFormatFmp4 = ffi.MoqContainerFormatFmp4 + // ContainerFormatMkv is Matroska / WebM. + ContainerFormatMkv = ffi.MoqContainerFormatMkv + // ContainerFormatTs is an MPEG-2 transport stream. + ContainerFormatTs = ffi.MoqContainerFormatTs + // ContainerFormatFlv is Flash Video, as used by RTMP. + ContainerFormatFlv = ffi.MoqContainerFormatFlv +) + +// TrackProducer writes timestamped frames into a media track. +type TrackProducer struct { + inner *ffi.MoqMediaTrackProducer +} + +// Demand returns a watch-only handle to whether the track has subscribers. +func (m *TrackProducer) Demand() (*moq.TrackDemand, error) { + inner, err := m.inner.Demand() + if err != nil { + return nil, err + } + return bridge.TrackDemand(inner).(*moq.TrackDemand), nil +} + +// WriteFrame appends frame to the media track. The importer derives keyframe status from +// the bitstream, so a Frame carries only the payload and its timestamp. +func (m *TrackProducer) WriteFrame(frame moq.Frame) error { + f, err := bridge.Frame(frame) + if err != nil { + return err + } + return m.inner.WriteFrame(f) +} + +// Flush records a local encoder's frame handoff on the broadcast media clock. +// Call after WriteFrame only for local encoder output, not file or network imports. +func (m *TrackProducer) Flush(timestamp time.Duration) error { + us, err := bridge.Duration(timestamp, "timestamp") + if err != nil { + return err + } + return m.inner.Flush(us) +} + +// Discontinuity marks a timeline break and restarts handoff measurement, preserving advertised jitter. +func (m *TrackProducer) Discontinuity() error { + return m.inner.Discontinuity() +} + +// Cut draws a group boundary here. +// +// Audio has no boundary of its own (every packet is independently decodable), so this is +// the only thing that gives it groups: call it after every frame for one group (one QUIC +// stream) the relay forwards without waiting, or at a segment cadence to align with video. +// Video groups at its own keyframes and needs this only to override that. +func (m *TrackProducer) Cut() error { + return m.inner.Cut() +} + +// Seek draws a group boundary and numbers the next group sequence. +// +// Cut with an explicit sequence, for a publisher whose group numbers have to be +// deterministic: two encoders aligning per GOP so a consumer can fail over between them. +func (m *TrackProducer) Seek(sequence uint64) error { + return m.inner.Seek(sequence) +} + +// Finish closes the media track. +func (m *TrackProducer) Finish() error { + return m.inner.Finish() +} + +// ContainerProducer publishes a container, which demuxes and publishes its own +// tracks. Unlike TrackProducer there is no per-frame timestamp: a container +// carries its tracks' timing itself. +type ContainerProducer struct { + inner *ffi.MoqMediaContainerProducer +} + +// Write pushes a whole chunk of container bytes. +func (c *ContainerProducer) Write(payload []byte) error { + return c.inner.Write(payload) +} + +// Cut declares that the next chunk starts a new segment, rolling a group on +// every track. An fMP4 source carrying styp atoms declares its own, so this is +// only needed when it doesn't; MKV, TS, and FLV ignore it. +func (c *ContainerProducer) Cut() error { + return c.inner.Cut() +} + +// Seek starts a new segment and numbers its groups sequence. +func (c *ContainerProducer) Seek(sequence uint64) error { + return c.inner.Seek(sequence) +} + +// Finish finishes every track this container publishes. +func (c *ContainerProducer) Finish() error { + return c.inner.Finish() +} + +// ContainerStreamProducer publishes a container fed by a raw byte stream, which +// recovers its own framing. +type ContainerStreamProducer struct { + inner *ffi.MoqMediaContainerStreamProducer +} + +// Write pushes raw container bytes; chunk boundaries don't matter. +func (c *ContainerStreamProducer) Write(payload []byte) error { + return c.inner.Write(payload) +} + +// Finish finishes every track this container publishes. +func (c *ContainerStreamProducer) Finish() error { + return c.inner.Finish() +} + +// TrackStreamProducer feeds a raw encoder byte stream; whole frames are emitted +// as they complete. +type TrackStreamProducer struct { + inner *ffi.MoqMediaTrackStreamProducer +} + +// Demand watches whether this track has subscribers. +func (m *TrackStreamProducer) Demand() (*moq.TrackDemand, error) { + inner, err := m.inner.Demand() + if err != nil { + return nil, err + } + return bridge.TrackDemand(inner).(*moq.TrackDemand), nil +} + +// Write pushes raw stream bytes. +func (m *TrackStreamProducer) Write(payload []byte) error { + return m.inner.Write(payload) +} + +// Finish closes the stream. +func (m *TrackStreamProducer) Finish() error { + return m.inner.Finish() +} + +// ContainerConsumer is a stream of decoded media frames. +type ContainerConsumer struct { + inner *ffi.MoqMediaContainerConsumer +} + +// Next returns the next frame, or (nil, nil) when the track ends. +func (m *ContainerConsumer) Next(ctx context.Context) (*MediaFrame, error) { + frame, err := bridge.Call(ctx, m.inner.Cancel, m.inner.Next) + if frame == nil || err != nil { + return nil, err + } + return &MediaFrame{Payload: frame.Payload, Timestamp: time.Duration(frame.TimestampUs) * time.Microsecond, Keyframe: frame.Keyframe}, nil +} + +// All ranges over frames until the track ends or the loop breaks. +func (m *ContainerConsumer) All(ctx context.Context) iter.Seq2[*MediaFrame, error] { + return bridge.Seq(ctx, m.Next) +} + +// Cancel stops the stream. +func (m *ContainerConsumer) Cancel() { + m.inner.Cancel() +} + +// ContainerGroupConsumer is a finite stream of decoded media frames from one fetched +// group. It ends after the group's last frame. +type ContainerGroupConsumer struct { + inner *ffi.MoqMediaContainerGroupConsumer +} + +// Sequence is this group's sequence number within the track. +func (m *ContainerGroupConsumer) Sequence() uint64 { + return m.inner.Sequence() +} + +// Next returns the next decoded frame, or (nil, nil) when the group ends. +func (m *ContainerGroupConsumer) Next(ctx context.Context) (*MediaFrame, error) { + frame, err := bridge.Call(ctx, m.inner.Cancel, m.inner.Next) + if frame == nil || err != nil { + return nil, err + } + return &MediaFrame{Payload: frame.Payload, Timestamp: time.Duration(frame.TimestampUs) * time.Microsecond, Keyframe: frame.Keyframe}, nil +} + +// All ranges over decoded frames until the group ends or the loop breaks. +func (m *ContainerGroupConsumer) All(ctx context.Context) iter.Seq2[*MediaFrame, error] { + return bridge.Seq(ctx, m.Next) +} + +// Cancel stops the stream. +func (m *ContainerGroupConsumer) Cancel() { + m.inner.Cancel() +} + +// CatalogConsumer is a stream of catalog updates. +type CatalogConsumer struct { + inner *ffi.MoqMediaCatalogConsumer +} + +// Next returns the next catalog, or (nil, nil) when the track ends. +func (c *CatalogConsumer) Next(ctx context.Context) (*Catalog, error) { + return bridge.Call(ctx, c.inner.Cancel, c.inner.Next) +} + +// All ranges over catalog updates until the track ends or the loop breaks. +func (c *CatalogConsumer) All(ctx context.Context) iter.Seq2[*Catalog, error] { + return bridge.Seq(ctx, c.Next) +} + +// Cancel stops the stream. +func (c *CatalogConsumer) Cancel() { + c.inner.Cancel() +} + +// CatalogSnapshot reads the first catalog snapshot and releases its subscription. +func CatalogSnapshot(ctx context.Context, broadcast *moq.BroadcastConsumer) (*Catalog, error) { + consumer, err := NewCatalogConsumer(ctx, broadcast) + if err != nil { + return nil, err + } + defer consumer.Cancel() + snapshot, err := consumer.Next(ctx) + if err == nil && snapshot == nil { + err = moq.ErrClosed + } + return snapshot, err +} diff --git a/go/wrapper/media/media_test.go b/go/wrapper/media/media_test.go new file mode 100644 index 00000000000..41944f24ad5 --- /dev/null +++ b/go/wrapper/media/media_test.go @@ -0,0 +1,37 @@ +package media_test + +import ( + "errors" + "testing" + + "moq.dev/moq" + "moq.dev/moq/media" +) + +func TestCatalogClosesWithBroadcast(t *testing.T) { + broadcast, err := moq.NewBroadcastProducer() + if err != nil { + t.Fatal(err) + } + catalog, err := media.NewCatalogProducer(broadcast) + if err != nil { + t.Fatal(err) + } + if err := catalog.SetSection("app", map[string]int{"value": 42}); err != nil { + t.Fatal(err) + } + if err := broadcast.Close(); err != nil { + t.Fatal(err) + } + if err := catalog.RemoveSection("app"); !errors.Is(err, moq.ErrClosed) { + t.Fatalf("remove after close: %v", err) + } +} + +func TestRequestedTargetRequiresRequest(t *testing.T) { + for _, target := range []media.Target{nil, media.Requested{}, (*media.Named)(nil), (*media.Requested)(nil)} { + if _, err := media.NewAudioTrackProducer(nil, target, media.AudioInit{}); err == nil { + t.Fatal("invalid target succeeded") + } + } +} diff --git a/go/wrapper/moq_test.go b/go/wrapper/moq_test.go index a069684f4ce..135caaf441e 100644 --- a/go/wrapper/moq_test.go +++ b/go/wrapper/moq_test.go @@ -3,7 +3,6 @@ package moq_test import ( "context" "encoding/binary" - "encoding/json" "errors" "fmt" "runtime" @@ -12,6 +11,7 @@ import ( "time" "moq.dev/moq" + moqmedia "moq.dev/moq/media" ) // testTimeout bounds the blocking stream calls so a regression fails the test @@ -110,14 +110,14 @@ func TestDynamicBroadcastRequest(t *testing.T) { defer trackConsumer.Cancel() payload := []byte("served dynamically") - if err := track.WriteFrame(moq.Frame{Payload: payload, TimestampUs: us(0)}); err != nil { + if err := track.WriteFrame(moq.Frame{Payload: payload, Timestamp: ts(0)}); err != nil { t.Fatal(err) } frame, err := trackConsumer.ReadFrame(ctx) if err != nil { t.Fatal(err) } - if frame == nil || string(frame.Payload) != string(payload) || frame.TimestampUs == nil || *frame.TimestampUs != 0 { + if frame == nil || string(frame.Payload) != string(payload) || frame.Timestamp == nil || *frame.Timestamp != 0 { t.Fatalf("frame = %+v, want payload=%q ts=0", frame, payload) } @@ -134,11 +134,11 @@ func TestPublishAudioLifecycle(t *testing.T) { if err != nil { t.Fatal(err) } - media, err := broadcast.PublishAudio(moq.AudioFormatOpus, opusHead()) + media, err := moqmedia.NewAudioTrackProducer(broadcast, moqmedia.Named{}, moqmedia.AudioInit{Format: moqmedia.AudioFormatOpus, Data: opusHead()}) if err != nil { t.Fatal(err) } - if err := media.WriteFrame(moq.Frame{Payload: []byte("opus frame"), TimestampUs: us(1000)}); err != nil { + if err := media.WriteFrame(moq.Frame{Payload: []byte("opus frame"), Timestamp: ts(1000 * time.Microsecond)}); err != nil { t.Fatal(err) } if err := media.Finish(); err != nil { @@ -257,7 +257,11 @@ func TestVideoPropertiesUseDefaultedFields(t *testing.T) { t.Fatal(err) } rotation := 315.0 - if err := broadcast.SetVideoProperties(moq.VideoProperties{Rotation: &rotation}); err != nil { + catalogProducer, err := moqmedia.NewCatalogProducer(broadcast) + if err != nil { + t.Fatal(err) + } + if err := catalogProducer.SetVideoProperties(moqmedia.VideoProperties{Rotation: &rotation}); err != nil { t.Fatal(err) } if err := broadcast.Close(); err != nil { @@ -315,7 +319,7 @@ func testDecodeVideoFrame(t *testing.T, surface bool) { if err != nil { t.Fatal(err) } - catalog, err := bc.Catalog(ctx) + catalog, err := moqmedia.CatalogSnapshot(ctx, bc) if err != nil { t.Fatal(err) } @@ -410,7 +414,7 @@ func TestFetchGroupAndServeDynamicMiss(t *testing.T) { if err != nil { t.Fatal(err) } - if err := cached.WriteFrame(moq.Frame{Payload: []byte("cached"), TimestampUs: us(0)}); err != nil { + if err := cached.WriteFrame(moq.Frame{Payload: []byte("cached"), Timestamp: ts(0)}); err != nil { t.Fatal(err) } if err := cached.Finish(); err != nil { @@ -451,7 +455,7 @@ func TestFetchGroupAndServeDynamicMiss(t *testing.T) { if err != nil { t.Fatal(err) } - if err := produced.WriteFrame(moq.Frame{Payload: []byte("archive"), TimestampUs: us(request.Sequence() * 20_000)}); err != nil { + if err := produced.WriteFrame(moq.Frame{Payload: []byte("archive"), Timestamp: ts(time.Duration(request.Sequence()*20_000) * time.Microsecond)}); err != nil { t.Fatal(err) } if err := produced.Finish(); err != nil { @@ -474,7 +478,7 @@ func TestUnknownFormat(t *testing.T) { t.Fatal(err) } // A bad format is no longer expressible: it is an enum. Bad init bytes still are. - if _, err := broadcast.PublishAudio(moq.AudioFormatOpus, nil); err == nil { + if _, err := moqmedia.NewAudioTrackProducer(broadcast, moqmedia.Named{}, moqmedia.AudioInit{Format: moqmedia.AudioFormatOpus, Data: nil}); err == nil { t.Fatal("expected error for unknown format") } } @@ -488,7 +492,7 @@ func TestLocalPublishConsumeAudio(t *testing.T) { if err != nil { t.Fatal(err) } - media, err := broadcast.PublishAudio(moq.AudioFormatOpus, opusHead()) + media, err := moqmedia.NewAudioTrackProducer(broadcast, moqmedia.Named{}, moqmedia.AudioInit{Format: moqmedia.AudioFormatOpus, Data: opusHead()}) if err != nil { t.Fatal(err) } @@ -516,7 +520,7 @@ func TestLocalPublishConsumeAudio(t *testing.T) { t.Fatal(err) } - catalog, err := bc.Catalog(ctx) + catalog, err := moqmedia.CatalogSnapshot(ctx, bc) if err != nil { t.Fatal(err) } @@ -525,7 +529,7 @@ func TestLocalPublishConsumeAudio(t *testing.T) { } var trackName string - var audio moq.Audio + var audio moqmedia.Audio for name, a := range catalog.Audio { trackName, audio = name, a } @@ -533,14 +537,14 @@ func TestLocalPublishConsumeAudio(t *testing.T) { t.Fatalf("audio = %+v, want opus/48000/2", audio) } - mediaConsumer, err := bc.SubscribeMedia(ctx, trackName, audio.Container, nil) + mediaConsumer, err := moqmedia.NewContainerConsumer(ctx, bc, moqmedia.ContainerConfig{Name: trackName, Container: audio.Container, Subscription: nil}) if err != nil { t.Fatal(err) } defer mediaConsumer.Cancel() payload := []byte("opus audio payload data") - if err := media.WriteFrame(moq.Frame{Payload: payload, TimestampUs: us(1_000_000)}); err != nil { + if err := media.WriteFrame(moq.Frame{Payload: payload, Timestamp: ts(1_000_000 * time.Microsecond)}); err != nil { t.Fatal(err) } @@ -551,7 +555,7 @@ func TestLocalPublishConsumeAudio(t *testing.T) { if frame == nil { t.Fatal("expected a frame") } - if string(frame.Payload) != string(payload) || frame.TimestampUs != 1_000_000 { + if string(frame.Payload) != string(payload) || uint64(frame.Timestamp.Microseconds()) != 1_000_000 { t.Fatalf("frame = %+v, want payload=%q ts=1000000", frame, payload) } } @@ -574,7 +578,7 @@ func TestTrackPublishConsume(t *testing.T) { } defer consumer.Cancel() - if err := track.WriteFrame(moq.Frame{Payload: []byte("hello"), TimestampUs: us(12_345)}); err != nil { + if err := track.WriteFrame(moq.Frame{Payload: []byte("hello"), Timestamp: ts(12_345 * time.Microsecond)}); err != nil { t.Fatal(err) } @@ -585,7 +589,7 @@ func TestTrackPublishConsume(t *testing.T) { if frame == nil { t.Fatal("expected a frame") } - if string(frame.Payload) != "hello" || frame.TimestampUs == nil || *frame.TimestampUs != 12_345 { + if string(frame.Payload) != "hello" || frame.Timestamp == nil || *frame.Timestamp != 12_345*time.Microsecond { t.Fatalf("frame = %+v, want payload=hello ts=12345", frame) } @@ -598,7 +602,7 @@ func TestTrackPublishConsume(t *testing.T) { t.Fatal(err) } defer groupConsumer.Cancel() - if err := group.WriteFrame(moq.Frame{Payload: []byte("group"), TimestampUs: us(23_456)}); err != nil { + if err := group.WriteFrame(moq.Frame{Payload: []byte("group"), Timestamp: ts(23_456 * time.Microsecond)}); err != nil { t.Fatal(err) } if err := group.Finish(); err != nil { @@ -611,7 +615,7 @@ func TestTrackPublishConsume(t *testing.T) { if frame == nil { t.Fatal("expected a group frame") } - if string(frame.Payload) != "group" || frame.TimestampUs == nil || *frame.TimestampUs != 23_456 { + if string(frame.Payload) != "group" || frame.Timestamp == nil || *frame.Timestamp != 23_456*time.Microsecond { t.Fatalf("frame = %+v, want payload=group ts=23456", frame) } } @@ -641,7 +645,7 @@ func TestReadFrameSkipsEmptyThenPopulatedGroups(t *testing.T) { if err := empty.Finish(); err != nil { t.Fatal(err) } - if err := track.WriteFrame(moq.Frame{Payload: []byte("populated"), TimestampUs: us(2_000)}); err != nil { + if err := track.WriteFrame(moq.Frame{Payload: []byte("populated"), Timestamp: ts(2_000 * time.Microsecond)}); err != nil { t.Fatal(err) } @@ -649,7 +653,7 @@ func TestReadFrameSkipsEmptyThenPopulatedGroups(t *testing.T) { if err != nil { t.Fatal(err) } - if frame == nil || string(frame.Payload) != "populated" || frame.TimestampUs == nil || *frame.TimestampUs != 2_000 { + if frame == nil || string(frame.Payload) != "populated" || frame.Timestamp == nil || *frame.Timestamp != 2_000*time.Microsecond { t.Fatalf("frame = %+v, want payload=populated ts=2000", frame) } } @@ -691,138 +695,6 @@ func TestTrackSparseGroupsAndKnownEnd(t *testing.T) { } } -func TestJSONTracks(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - broadcast, err := moq.NewBroadcastProducer() - if err != nil { - t.Fatal(err) - } - consumer, err := broadcast.Consume() - if err != nil { - t.Fatal(err) - } - - snapshot, err := broadcast.PublishJSONSnapshot("status", moq.JSONSnapshotOptions{Compression: true}) - if err != nil { - t.Fatal(err) - } - snapshotConsumer, err := consumer.SubscribeJSONSnapshot(ctx, "status", moq.JSONSubscribeOptions{Compression: true}) - if err != nil { - t.Fatal(err) - } - defer snapshotConsumer.Cancel() - if err := snapshot.Update(map[string]any{"viewers": 42}); err != nil { - t.Fatal(err) - } - value, err := snapshotConsumer.Next(ctx) - if err != nil { - t.Fatal(err) - } - var decoded map[string]any - if err := json.Unmarshal(*value, &decoded); err != nil { - t.Fatal(err) - } - if decoded["viewers"] != float64(42) { - t.Fatalf("snapshot = %s", *value) - } - - stream, err := broadcast.PublishJSONStream("events", moq.JSONStreamOptions{Compression: true}) - if err != nil { - t.Fatal(err) - } - streamConsumer, err := consumer.SubscribeJSONStream(ctx, "events", moq.JSONSubscribeOptions{Compression: true}) - if err != nil { - t.Fatal(err) - } - defer streamConsumer.Cancel() - if err := stream.Append(map[string]any{"n": 1}); err != nil { - t.Fatal(err) - } - record, err := streamConsumer.Next(ctx) - if err != nil { - t.Fatal(err) - } - if string(*record) != `{"n":1}` { - t.Fatalf("record = %s", *record) - } - - if err := snapshot.Finish(); err != nil { - t.Fatal(err) - } - if err := stream.Finish(); err != nil { - t.Fatal(err) - } -} - -func TestJSONDemand(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - broadcast, err := moq.NewBroadcastProducer() - if err != nil { - t.Fatal(err) - } - consumer, err := broadcast.Consume() - if err != nil { - t.Fatal(err) - } - snapshot, err := broadcast.PublishJSONSnapshot("status", moq.JSONSnapshotOptions{}) - if err != nil { - t.Fatal(err) - } - stream, err := broadcast.PublishJSONStream("events", moq.JSONStreamOptions{}) - if err != nil { - t.Fatal(err) - } - snapshotDemand, err := snapshot.Demand() - if err != nil { - t.Fatal(err) - } - streamDemand, err := stream.Demand() - if err != nil { - t.Fatal(err) - } - if name := snapshotDemand.Name(); name != "status" { - t.Fatalf("Name = %q, want status", name) - } - if snapshotDemand.IsUsed() { - t.Fatal("IsUsed before any subscriber") - } - - snapshotConsumer, err := consumer.SubscribeJSONSnapshot(ctx, "status", moq.JSONSubscribeOptions{}) - if err != nil { - t.Fatal(err) - } - streamConsumer, err := consumer.SubscribeJSONStream(ctx, "events", moq.JSONSubscribeOptions{}) - if err != nil { - t.Fatal(err) - } - if err := snapshotDemand.Used(ctx); err != nil { - t.Fatal(err) - } - if err := streamDemand.Used(ctx); err != nil { - t.Fatal(err) - } - - snapshotConsumer.Cancel() - streamConsumer.Cancel() - if err := snapshotDemand.Unused(ctx); err != nil { - t.Fatal(err) - } - if err := streamDemand.Unused(ctx); err != nil { - t.Fatal(err) - } - - if err := snapshot.Finish(); err != nil { - t.Fatal(err) - } - if err := snapshotDemand.Used(ctx); !errors.Is(err, moq.ErrClosed) { - t.Fatalf("Used after Finish = %v, want ErrClosed", err) - } -} - func TestDynamicTrackRequest(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() @@ -871,7 +743,7 @@ func TestDynamicTrackRequest(t *testing.T) { t.Fatal(err) } payload := []byte("hello dynamic track") - if err := track.WriteFrame(moq.Frame{Payload: payload, TimestampUs: us(0)}); err != nil { + if err := track.WriteFrame(moq.Frame{Payload: payload, Timestamp: ts(0)}); err != nil { t.Fatal(err) } @@ -891,7 +763,7 @@ func TestDynamicTrackRequest(t *testing.T) { if err != nil { t.Fatal(err) } - if frame == nil || string(frame.Payload) != string(payload) || frame.TimestampUs == nil || *frame.TimestampUs != 0 { + if frame == nil || string(frame.Payload) != string(payload) || frame.Timestamp == nil || *frame.Timestamp != 0 { t.Fatalf("frame = %+v, want payload=%q ts=0", frame, payload) } if err := track.Finish(); err != nil { @@ -921,12 +793,12 @@ func TestDynamicTrackRequestCanPublishAudio(t *testing.T) { } type subscribeResult struct { - media *moq.MediaConsumer + media *moqmedia.ContainerConsumer err error } subscribe := make(chan subscribeResult, 1) go func() { - media, err := consumer.SubscribeMedia(ctx, "requested-audio", moq.LegacyContainer(), nil) + media, err := moqmedia.NewContainerConsumer(ctx, consumer, moqmedia.ContainerConfig{Name: "requested-audio", Container: moqmedia.LegacyContainer(), Subscription: nil}) subscribe <- subscribeResult{media: media, err: err} }() @@ -942,14 +814,15 @@ func TestDynamicTrackRequestCanPublishAudio(t *testing.T) { t.Fatalf("request name = %q, want requested-audio", name) } - media, err := broadcast.PublishAudioOnTrack(request, moq.AudioFormatOpus, opusHead()) + media, err := moqmedia.NewAudioTrackProducer(broadcast, moqmedia.Requested{Request: request}, moqmedia.AudioInit{Format: moqmedia.AudioFormatOpus, Data: opusHead()}) if err != nil { t.Fatal(err) } - mediaName, err := media.Name() + mediaDemand, err := media.Demand() if err != nil { t.Fatal(err) } + mediaName := mediaDemand.Name() if mediaName != "requested-audio" { t.Fatalf("media name = %q, want requested-audio", mediaName) } @@ -957,7 +830,7 @@ func TestDynamicTrackRequestCanPublishAudio(t *testing.T) { t.Fatalf("request name after accept error = %v, want ErrClosed", err) } - var mediaConsumer *moq.MediaConsumer + var mediaConsumer *moqmedia.ContainerConsumer select { case res := <-subscribe: if res.err != nil { @@ -970,7 +843,7 @@ func TestDynamicTrackRequestCanPublishAudio(t *testing.T) { defer mediaConsumer.Cancel() payload := []byte("dynamic opus frame") - if err := media.WriteFrame(moq.Frame{Payload: payload, TimestampUs: us(20_000)}); err != nil { + if err := media.WriteFrame(moq.Frame{Payload: payload, Timestamp: ts(20_000 * time.Microsecond)}); err != nil { t.Fatal(err) } @@ -981,7 +854,7 @@ func TestDynamicTrackRequestCanPublishAudio(t *testing.T) { if frame == nil { t.Fatal("expected a frame") } - if string(frame.Payload) != string(payload) || frame.TimestampUs != 20_000 { + if string(frame.Payload) != string(payload) || uint64(frame.Timestamp.Microseconds()) != 20_000 { t.Fatalf("frame = %+v, want payload=%q ts=20000", frame, payload) } if err := media.Finish(); err != nil { @@ -989,7 +862,7 @@ func TestDynamicTrackRequestCanPublishAudio(t *testing.T) { } } -// TestRecvGroupCancelRace exercises the core runCancellable path under -race: +// TestRecvGroupCancelRace exercises the core bridge.Call path under -race: // the native RecvGroup runs on an internal goroutine while ctx expiry triggers a // concurrent Cancel on the same consumer. No group is ever written, so each read // blocks until its short ctx fires. The race detector flags any unsynchronized @@ -1195,7 +1068,7 @@ func TestSubscribeTrackCancelKeepsTheBroadcast(t *testing.T) { } } -// TestUsedCancelKeepsTheTrack cancels a producer-side Used wait, which has no +// TestUsedCancelKeepsTheTrack cancels a demand Used wait, which has no // object-wide cancel to fall back on, and confirms the track still publishes. func TestUsedCancelKeepsTheTrack(t *testing.T) { broadcast, err := moq.NewBroadcastProducer() @@ -1208,10 +1081,14 @@ func TestUsedCancelKeepsTheTrack(t *testing.T) { if err != nil { t.Fatal(err) } + demand, err := track.Demand() + if err != nil { + t.Fatal(err) + } ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() - if err := track.Used(ctx); !errors.Is(err, context.DeadlineExceeded) { + if err := demand.Used(ctx); !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("Used error = %v, want context.DeadlineExceeded", err) } @@ -1223,12 +1100,12 @@ func TestUsedCancelKeepsTheTrack(t *testing.T) { t.Fatal(err) } defer consumer.Cancel() - if err := track.Used(readCtx); err != nil { + if err := demand.Used(readCtx); err != nil { t.Fatal(err) } payload := []byte("still publishing") - if err := track.WriteFrame(moq.Frame{Payload: payload, TimestampUs: us(0)}); err != nil { + if err := track.WriteFrame(moq.Frame{Payload: payload, Timestamp: ts(0)}); err != nil { t.Fatal(err) } frame, err := consumer.ReadFrame(readCtx) @@ -1497,3 +1374,5 @@ func TestDynamicServesARequestUnderAPrefix(t *testing.T) { // us is a raw frame's optional timestamp in microseconds. func us(v uint64) *uint64 { return &v } + +func ts(d time.Duration) *time.Duration { return &d } diff --git a/go/wrapper/origin.go b/go/wrapper/origin.go index 1c088abe38c..73bd7815be9 100644 --- a/go/wrapper/origin.go +++ b/go/wrapper/origin.go @@ -6,6 +6,7 @@ import ( "iter" ffi "moq.dev/moq-ffi/moq" + "moq.dev/moq/internal/bridge" ) // OriginProducer publishes broadcasts under paths and hands out consumers that @@ -72,17 +73,17 @@ type OriginDynamic struct { // RequestedBroadcast blocks until a consumer requests a path with no existing // exact-path broadcast. func (d *OriginDynamic) RequestedBroadcast(ctx context.Context) (*BroadcastRequest, error) { - inner, err := runHandle(ctx, d.inner.Cancel, d.inner.RequestedBroadcast) + inner, err := bridge.CallHandle(ctx, d.inner.Cancel, d.inner.RequestedBroadcast) if err != nil { return nil, err } return &BroadcastRequest{inner: inner}, nil } -// Requests ranges over requested broadcasts until the stream errors or the loop +// All ranges over requested broadcasts until the stream errors or the loop // breaks. -func (d *OriginDynamic) Requests(ctx context.Context) iter.Seq2[*BroadcastRequest, error] { - return streamSeq(ctx, d.RequestedBroadcast) +func (d *OriginDynamic) All(ctx context.Context) iter.Seq2[*BroadcastRequest, error] { + return bridge.Seq(ctx, d.RequestedBroadcast) } // Update re-prices the route in place: replaces its hops and costs. @@ -177,7 +178,7 @@ type AnnounceConsumer struct { // Next returns the next announce event, or (nil, nil) when the stream ends. func (a *AnnounceConsumer) Next(ctx context.Context) (AnnounceEvent, error) { - res, err := runCancellable(ctx, a.inner.Cancel, a.inner.Next) + res, err := bridge.Call(ctx, a.inner.Cancel, a.inner.Next) if err != nil || res == nil { return nil, err } @@ -186,7 +187,7 @@ func (a *AnnounceConsumer) Next(ctx context.Context) (AnnounceEvent, error) { // All ranges over announce events until the stream ends or the loop breaks. func (a *AnnounceConsumer) All(ctx context.Context) iter.Seq2[AnnounceEvent, error] { - return streamSeq(ctx, a.Next) + return bridge.Seq(ctx, a.Next) } // Cancel stops the announcement stream. @@ -201,7 +202,7 @@ type AnnouncedBroadcast struct { // Available blocks until the broadcast is available and returns its consumer. func (a *AnnouncedBroadcast) Available(ctx context.Context) (*BroadcastConsumer, error) { - inner, err := runHandle(ctx, a.inner.Cancel, a.inner.Available) + inner, err := bridge.CallHandle(ctx, a.inner.Cancel, a.inner.Available) if err != nil { return nil, err } diff --git a/go/wrapper/publish.go b/go/wrapper/publish.go index f5db177a78c..541fb9dff4e 100644 --- a/go/wrapper/publish.go +++ b/go/wrapper/publish.go @@ -2,73 +2,12 @@ package moq import ( "context" - "errors" "iter" ffi "moq.dev/moq-ffi/moq" + "moq.dev/moq/internal/bridge" ) -// AudioOption configures an audio publish. -type AudioOption func(*ffi.MoqAudioInit) - -// VideoOption configures a video publish. -type VideoOption func(*ffi.MoqVideoInit) - -var errNilTrackRequest = errors.New("moq: nil track request") - -// WithAudioLabel sets the human-readable rendition name stored in the catalog. -func WithAudioLabel(label string) AudioOption { - return func(init *ffi.MoqAudioInit) { - init.Label = &label - } -} - -// WithVideoLabel sets the human-readable rendition name stored in the catalog. -func WithVideoLabel(label string) VideoOption { - return func(init *ffi.MoqVideoInit) { - init.Label = &label - } -} - -// WithAudioTrack names the track instead of deriving a unique name from the -// format. A requested track already has a name, so the OnTrack variant refuses it. -func WithAudioTrack(track string) AudioOption { - return func(init *ffi.MoqAudioInit) { - init.Track = &track - } -} - -// WithVideoTrack names the track instead of deriving a unique name from the -// format. A requested track already has a name, so the OnTrack variant refuses it. -func WithVideoTrack(track string) VideoOption { - return func(init *ffi.MoqVideoInit) { - init.Track = &track - } -} - -// WithVideoHint seeds catalog fields that a video stream cannot reveal itself. -func WithVideoHint(hint VideoHint) VideoOption { - return func(init *ffi.MoqVideoInit) { - init.Hint = &hint - } -} - -func audioInit(format AudioFormat, init []byte, opts []AudioOption) ffi.MoqAudioInit { - cfg := ffi.MoqAudioInit{Format: format, Data: init} - for _, opt := range opts { - opt(&cfg) - } - return cfg -} - -func videoInit(format VideoFormat, init []byte, opts []VideoOption) ffi.MoqVideoInit { - cfg := ffi.MoqVideoInit{Format: format, Data: init} - for _, opt := range opts { - opt(&cfg) - } - return cfg -} - // BroadcastProducer publishes a collection of tracks. Create one at a path with // [OriginProducer.CreateBroadcast] (or [Client.CreateBroadcast]), then publish // tracks onto it. @@ -110,87 +49,6 @@ func (b *BroadcastProducer) Unannounce() error { return b.inner.Unannounce() } -// SetVideoProperties replaces the catalog properties shared by every video rendition. -func (b *BroadcastProducer) SetVideoProperties(properties VideoProperties) error { - return b.inner.SetVideoProperties(properties) -} - -// PublishAudio publishes one audio codec as a new track, fed frame by frame with -// explicit timestamps. init carries the codec init bytes, which audio requires. -func (b *BroadcastProducer) PublishAudio(format AudioFormat, init []byte, opts ...AudioOption) (*MediaProducer, error) { - inner, err := b.inner.PublishAudio(audioInit(format, init, opts)) - if err != nil { - return nil, err - } - return &MediaProducer{inner: inner}, nil -} - -// PublishVideo publishes one video codec as a new track. init may be nil for a -// format that resolves in band. -func (b *BroadcastProducer) PublishVideo(format VideoFormat, init []byte, opts ...VideoOption) (*MediaProducer, error) { - inner, err := b.inner.PublishVideo(videoInit(format, init, opts)) - if err != nil { - return nil, err - } - return &MediaProducer{inner: inner}, nil -} - -// PublishContainer publishes a container, which demuxes and publishes its own -// tracks. There is no label or hint: a container describes each track itself. -func (b *BroadcastProducer) PublishContainer(format ContainerFormat, init []byte) (*ContainerProducer, error) { - inner, err := b.inner.PublishContainer(ffi.MoqContainerInit{Format: format, Data: init}) - if err != nil { - return nil, err - } - return &ContainerProducer{inner: inner}, nil -} - -// PublishAudioOnTrack publishes one audio codec onto a subscriber-requested track. -func (b *BroadcastProducer) PublishAudioOnTrack(request *TrackRequest, format AudioFormat, init []byte, opts ...AudioOption) (*MediaProducer, error) { - if request == nil { - return nil, errNilTrackRequest - } - inner, err := b.inner.PublishAudioOnTrack(request.inner, audioInit(format, init, opts)) - if err != nil { - return nil, err - } - return &MediaProducer{inner: inner}, nil -} - -// PublishVideoOnTrack publishes one video codec onto a subscriber-requested track. -func (b *BroadcastProducer) PublishVideoOnTrack(request *TrackRequest, format VideoFormat, init []byte, opts ...VideoOption) (*MediaProducer, error) { - if request == nil { - return nil, errNilTrackRequest - } - inner, err := b.inner.PublishVideoOnTrack(request.inner, videoInit(format, init, opts)) - if err != nil { - return nil, err - } - return &MediaProducer{inner: inner}, nil -} - -// PublishVideoStream publishes a video track fed by a raw byte stream with -// unknown frame boundaries (e.g. Annex-B H.264). Only the self-delimiting -// formats work: Avc3, Hev1, Av01. Audio has no counterpart, having no frame -// boundaries to infer. -func (b *BroadcastProducer) PublishVideoStream(format VideoFormat, opts ...VideoOption) (*MediaStreamProducer, error) { - inner, err := b.inner.PublishVideoStream(videoInit(format, nil, opts)) - if err != nil { - return nil, err - } - return &MediaStreamProducer{inner: inner}, nil -} - -// PublishContainerStream publishes a container fed by a raw byte stream, which -// recovers its own framing. -func (b *BroadcastProducer) PublishContainerStream(format ContainerFormat) (*ContainerStreamProducer, error) { - inner, err := b.inner.PublishContainerStream(format) - if err != nil { - return nil, err - } - return &ContainerStreamProducer{inner: inner}, nil -} - // EncodeAudio publishes a raw-audio track with an in-process encoder. // // Select the codec with OpusAudioCodec or AacAudioCodec. @@ -232,7 +90,11 @@ func (b *BroadcastProducer) EncodeVideo(input VideoEncoderInput, output VideoEnc // codec validation. info sets track properties (priority, cache, timescale); // pass nil for defaults. func (b *BroadcastProducer) PublishTrack(name string, info *TrackInfo) (*TrackProducer, error) { - inner, err := b.inner.PublishTrack(name, info) + ffiInfo, err := trackInfoFFI(info) + if err != nil { + return nil, err + } + inner, err := b.inner.PublishTrack(name, ffiInfo) if err != nil { return nil, err } @@ -248,20 +110,6 @@ func (b *BroadcastProducer) Consume() (*BroadcastConsumer, error) { return &BroadcastConsumer{inner: inner}, nil } -// SetCatalogSection sets (or replaces) an untyped application catalog section by -// name. json is any JSON document as a string; it rides alongside video/audio and -// reaches subscribers via Catalog.Sections. name must not be a reserved media -// section ("video"/"audio"). The catalog is republished automatically. -func (b *BroadcastProducer) SetCatalogSection(name string, json string) error { - return b.inner.SetCatalogSection(name, json) -} - -// RemoveCatalogSection removes an untyped application catalog section by name. It -// is a no-op if the section was absent. -func (b *BroadcastProducer) RemoveCatalogSection(name string) error { - return b.inner.RemoveCatalogSection(name) -} - // Close ends the broadcast for good: it retracts and serves no new tracks. // Tracks already subscribed carry on to their own end. Closing again is a no-op. func (b *BroadcastProducer) Close() error { @@ -275,16 +123,16 @@ type BroadcastDynamic struct { // RequestedTrack waits for the next subscriber-requested track. func (d *BroadcastDynamic) RequestedTrack(ctx context.Context) (*TrackRequest, error) { - inner, err := runHandle(ctx, d.inner.Cancel, d.inner.RequestedTrack) + inner, err := bridge.CallHandle(ctx, d.inner.Cancel, d.inner.RequestedTrack) if err != nil { return nil, err } return &TrackRequest{inner: inner}, nil } -// Requests ranges over subscriber-requested tracks until the dynamic source ends. -func (d *BroadcastDynamic) Requests(ctx context.Context) iter.Seq2[*TrackRequest, error] { - return streamSeq(ctx, d.RequestedTrack) +// All ranges over subscriber-requested tracks until the dynamic source ends. +func (d *BroadcastDynamic) All(ctx context.Context) iter.Seq2[*TrackRequest, error] { + return bridge.Seq(ctx, d.RequestedTrack) } // Cancel stops the dynamic request stream. @@ -311,9 +159,13 @@ func (r *TrackRequest) Dynamic() (*TrackDynamic, error) { return &TrackDynamic{inner: inner}, nil } -// Accept accepts the request as a raw track. For media, use PublishAudioOnTrack or PublishVideoOnTrack. +// Accept accepts the request as a raw track. For media, pass the request as media.Requested to an importer constructor. func (r *TrackRequest) Accept(info *TrackInfo) (*TrackProducer, error) { - inner, err := r.inner.Accept(info) + ffiInfo, err := trackInfoFFI(info) + if err != nil { + return nil, err + } + inner, err := r.inner.Accept(ffiInfo) if err != nil { return nil, err } @@ -325,138 +177,6 @@ func (r *TrackRequest) Abort(errorCode uint16) error { return r.inner.Abort(errorCode) } -// MediaProducer writes timestamped frames into a media track. -type MediaProducer struct { - inner *ffi.MoqMediaProducer -} - -// Name is the generated media track name. -func (m *MediaProducer) Name() (string, error) { - return m.inner.Name() -} - -// Demand returns a watch-only handle to whether the track has subscribers. -func (m *MediaProducer) Demand() (*TrackDemand, error) { - inner, err := m.inner.Demand() - if err != nil { - return nil, err - } - return &TrackDemand{inner: inner}, nil -} - -// Used blocks until the track has at least one active subscriber. Prefer Demand. -func (m *MediaProducer) Used(ctx context.Context) error { - return m.inner.Used(ctx) -} - -// Unused blocks until the track has no active subscribers. Prefer Demand. -func (m *MediaProducer) Unused(ctx context.Context) error { - return m.inner.Unused(ctx) -} - -// WriteFrame appends frame to the media track. The importer derives keyframe status from -// the bitstream, so a Frame carries only the payload and its timestamp. -func (m *MediaProducer) WriteFrame(frame Frame) error { - return m.inner.WriteFrame(frame) -} - -// Flush records a local encoder's frame handoff on the broadcast media clock. -// Call after WriteFrame only for local encoder output, not file or network imports. -func (m *MediaProducer) Flush(timestampUs uint64) error { - return m.inner.Flush(timestampUs) -} - -// Discontinuity marks a timeline break and restarts handoff measurement, preserving advertised jitter. -func (m *MediaProducer) Discontinuity() error { - return m.inner.Discontinuity() -} - -// Cut draws a group boundary here. -// -// Audio has no boundary of its own (every packet is independently decodable), so this is -// the only thing that gives it groups: call it after every frame for one group (one QUIC -// stream) the relay forwards without waiting, or at a segment cadence to align with video. -// Video groups at its own keyframes and needs this only to override that. -// -// On a container this declares a new segment, rolling a group on every track it publishes. -func (m *MediaProducer) Cut() error { - return m.inner.Cut() -} - -// Seek draws a group boundary and numbers the next group sequence. -// -// Cut with an explicit sequence, for a publisher whose group numbers have to be -// deterministic: two encoders aligning per GOP so a consumer can fail over between them. -func (m *MediaProducer) Seek(sequence uint64) error { - return m.inner.Seek(sequence) -} - -// Finish closes the media track. -func (m *MediaProducer) Finish() error { - return m.inner.Finish() -} - -// ContainerProducer publishes a container, which demuxes and publishes its own -// tracks. Unlike MediaProducer there is no per-frame timestamp: a container -// carries its tracks' timing itself. -type ContainerProducer struct { - inner *ffi.MoqContainerProducer -} - -// Write pushes a whole chunk of container bytes. -func (c *ContainerProducer) Write(payload []byte) error { - return c.inner.Write(payload) -} - -// Cut declares that the next chunk starts a new segment, rolling a group on -// every track. An fMP4 source carrying styp atoms declares its own, so this is -// only needed when it doesn't; MKV, TS, and FLV ignore it. -func (c *ContainerProducer) Cut() error { - return c.inner.Cut() -} - -// Seek starts a new segment and numbers its groups sequence. -func (c *ContainerProducer) Seek(sequence uint64) error { - return c.inner.Seek(sequence) -} - -// Finish finishes every track this container publishes. -func (c *ContainerProducer) Finish() error { - return c.inner.Finish() -} - -// ContainerStreamProducer publishes a container fed by a raw byte stream, which -// recovers its own framing. -type ContainerStreamProducer struct { - inner *ffi.MoqContainerStreamProducer -} - -// Write pushes raw container bytes; chunk boundaries don't matter. -func (c *ContainerStreamProducer) Write(payload []byte) error { - return c.inner.Write(payload) -} - -// Finish finishes every track this container publishes. -func (c *ContainerStreamProducer) Finish() error { - return c.inner.Finish() -} - -// MediaStreamProducer feeds a raw encoder byte stream; whole frames are emitted -// as they complete. -type MediaStreamProducer struct { - inner *ffi.MoqMediaStreamProducer -} - -// Write pushes raw stream bytes. -func (m *MediaStreamProducer) Write(payload []byte) error { - return m.inner.Write(payload) -} - -// Finish closes the stream. -func (m *MediaStreamProducer) Finish() error { - return m.inner.Finish() -} - // TrackDemand watches whether a published track has subscribers. // // It is weak: holding it neither keeps the track open nor locks the producer, so @@ -491,12 +211,7 @@ type TrackProducer struct { inner *ffi.MoqTrackProducer } -// Name is the track name. -func (t *TrackProducer) Name() (string, error) { - return t.inner.Name() -} - -// Demand returns a watch-only handle to whether the track has subscribers. +// Demand returns a watch-only handle to the track's name and whether it has subscribers. func (t *TrackProducer) Demand() (*TrackDemand, error) { inner, err := t.inner.Demand() if err != nil { @@ -505,16 +220,6 @@ func (t *TrackProducer) Demand() (*TrackDemand, error) { return &TrackDemand{inner: inner}, nil } -// Used blocks until the track has at least one active subscriber. Prefer Demand. -func (t *TrackProducer) Used(ctx context.Context) error { - return t.inner.Used(ctx) -} - -// Unused blocks until the track has no active subscribers. Prefer Demand. -func (t *TrackProducer) Unused(ctx context.Context) error { - return t.inner.Unused(ctx) -} - // Dynamic serves fetches for groups that are not currently cached. func (t *TrackProducer) Dynamic() (*TrackDynamic, error) { inner, err := t.inner.Dynamic() @@ -544,13 +249,21 @@ func (t *TrackProducer) CreateGroup(sequence uint64) (*GroupProducer, error) { // WriteFrame writes frame as a single-frame group. func (t *TrackProducer) WriteFrame(frame Frame) error { - return t.inner.WriteFrame(frame) + f, err := frame.ffi() + if err != nil { + return err + } + return t.inner.WriteFrame(f) } // AppendDatagram sends frame as a best-effort datagram and returns the sequence number // assigned to it. Payloads are capped at 1200 bytes. There is no stream fallback. func (t *TrackProducer) AppendDatagram(frame Frame) (uint64, error) { - return t.inner.AppendDatagram(frame) + f, err := frame.ffi() + if err != nil { + return 0, err + } + return t.inner.AppendDatagram(f) } // Abort closes the track with an application error code. @@ -561,7 +274,11 @@ func (t *TrackProducer) Abort(errorCode uint16) error { // Consume reads directly from this producer's track. subscription tunes delivery // (delivery priority, group range); pass nil for defaults. func (t *TrackProducer) Consume(subscription *Subscription) (*TrackConsumer, error) { - inner, err := t.inner.Consume(subscription) + sub, err := subscriptionFFI(subscription) + if err != nil { + return nil, err + } + inner, err := t.inner.Consume(sub) if err != nil { return nil, err } @@ -600,7 +317,11 @@ func (g *GroupProducer) Consume() (*GroupConsumer, error) { // WriteFrame appends frame to the group. func (g *GroupProducer) WriteFrame(frame Frame) error { - return g.inner.WriteFrame(frame) + f, err := frame.ffi() + if err != nil { + return err + } + return g.inner.WriteFrame(f) } // Finish marks the group complete. The handle remains so Abort can still run. @@ -620,16 +341,16 @@ type TrackDynamic struct { // RequestedGroup waits for the next uncached group request. func (d *TrackDynamic) RequestedGroup(ctx context.Context) (*GroupRequest, error) { - inner, err := runHandle(ctx, d.inner.Cancel, d.inner.RequestedGroup) + inner, err := bridge.CallHandle(ctx, d.inner.Cancel, d.inner.RequestedGroup) if err != nil { return nil, err } return &GroupRequest{inner: inner}, nil } -// Requests ranges over uncached group requests until the dynamic source ends. -func (d *TrackDynamic) Requests(ctx context.Context) iter.Seq2[*GroupRequest, error] { - return streamSeq(ctx, d.RequestedGroup) +// All ranges over uncached group requests until the dynamic source ends. +func (d *TrackDynamic) All(ctx context.Context) iter.Seq2[*GroupRequest, error] { + return bridge.Seq(ctx, d.RequestedGroup) } // Cancel stops current and future requested-group waits. diff --git a/go/wrapper/reconnect_test.go b/go/wrapper/reconnect_test.go index 24b00e19a33..9442547e712 100644 --- a/go/wrapper/reconnect_test.go +++ b/go/wrapper/reconnect_test.go @@ -37,11 +37,11 @@ func startRelay(t *testing.T, ctx context.Context, addr string) *relay { r := &relay{server: server, addr: server.LocalAddr()} go func() { - for req, err := range server.Requests(ctx) { + for req, err := range server.All(ctx) { if err != nil { return } - session, err := req.Accept(ctx) + session, err := req.Accept(ctx, nil, nil) if err != nil { continue } @@ -186,7 +186,7 @@ func TestReconnectAcrossRelayRestart(t *testing.T) { return default: } - _ = track.WriteFrame(moq.Frame{Payload: fmt.Appendf(nil, "frame-%d", i), TimestampUs: us(0)}) + _ = track.WriteFrame(moq.Frame{Payload: fmt.Appendf(nil, "frame-%d", i), Timestamp: ts(0)}) time.Sleep(10 * time.Millisecond) } }() diff --git a/go/wrapper/records.go b/go/wrapper/records.go new file mode 100644 index 00000000000..562eed33557 --- /dev/null +++ b/go/wrapper/records.go @@ -0,0 +1,155 @@ +package moq + +import ( + "fmt" + "time" + + ffi "moq.dev/moq-ffi/moq" +) + +// moq-ffi spells every duration as integer microseconds, because not every +// target language has a duration type. These records mirror the generated ones +// with time.Duration fields and convert at the boundary. + +// micros converts d to microseconds, refusing a negative duration rather than +// letting it wrap when cast to uint64. +func micros(name string, d time.Duration) (uint64, error) { + if d < 0 { + return 0, fmt.Errorf("negative %s: %v", name, d) + } + return uint64(d.Microseconds()), nil +} + +func optMicros(name string, d *time.Duration) (*uint64, error) { + if d == nil { + return nil, nil + } + us, err := micros(name, *d) + return &us, err +} + +func fromMicros(us uint64) time.Duration { + return time.Duration(us) * time.Microsecond +} + +func optFromMicros(us *uint64) *time.Duration { + if us == nil { + return nil + } + d := fromMicros(*us) + return &d +} + +// Frame is a raw track frame: a payload and its presentation timestamp. +// Timestamp is nil on a frame read from an untimed track; a raw track published +// here is timed, so writing one needs it. +type Frame struct { + Payload []byte + Timestamp *time.Duration +} + +func (f Frame) ffi() (ffi.MoqFrame, error) { + us, err := optMicros("timestamp", f.Timestamp) + return ffi.MoqFrame{Payload: f.Payload, TimestampUs: us}, err +} + +func frameFromFFI(f *ffi.MoqFrame) *Frame { + if f == nil { + return nil + } + return &Frame{Payload: f.Payload, Timestamp: optFromMicros(f.TimestampUs)} +} + +// Datagram is a best-effort track datagram as received: sequence number, timestamp +// (nil from an untimed track), and payload. +type Datagram struct { + Sequence uint64 + Timestamp *time.Duration + Payload []byte +} + +func datagramFromFFI(d *ffi.MoqDatagram) *Datagram { + if d == nil { + return nil + } + return &Datagram{Sequence: d.Sequence, Timestamp: optFromMicros(d.TimestampUs), Payload: d.Payload} +} + +// Subscription holds subscriber-side delivery preferences: priority, the max delay +// a non-latest group may fall behind before it is skipped (zero skips at once), and +// an optional group range (GroupStart a floor, GroupEnd exclusive). +type Subscription struct { + Priority uint8 + MaxDelay time.Duration + GroupStart *uint64 + GroupEnd *uint64 +} + +func (s Subscription) ffi() (ffi.MoqSubscription, error) { + maxDelay, err := micros("max delay", s.MaxDelay) + return ffi.MoqSubscription{ + Priority: s.Priority, + MaxDelayUs: maxDelay, + GroupStart: s.GroupStart, + GroupEnd: s.GroupEnd, + }, err +} + +func subscriptionFFI(s *Subscription) (*ffi.MoqSubscription, error) { + if s == nil { + return nil, nil + } + out, err := s.ffi() + return &out, err +} + +// TrackInfo holds publisher-side track properties: priority, how long a +// non-latest group is cached (nil for no limit), and the timescale in ticks per +// second (nil uses microseconds when publishing, and means the source declared no +// timeline on a received track). A zero Priority is the least urgent, not the +// default; set 127 for the midpoint a nil TrackInfo uses. +type TrackInfo struct { + Priority uint8 + MaxAge *time.Duration + Timescale *uint64 +} + +func trackInfoFFI(info *TrackInfo) (*ffi.MoqTrackInfo, error) { + if info == nil { + return nil, nil + } + maxAge, err := optMicros("max age", info.MaxAge) + return &ffi.MoqTrackInfo{Priority: info.Priority, MaxAgeUs: maxAge, Timescale: info.Timescale}, err +} + +func trackInfoFromFFI(info ffi.MoqTrackInfo) TrackInfo { + return TrackInfo{Priority: info.Priority, MaxAge: optFromMicros(info.MaxAgeUs), Timescale: info.Timescale} +} + +// ConnectionStats holds transport metrics for a session (RTT, bandwidth, byte +// and packet counters); each field is nil when unreported. +type ConnectionStats struct { + RTT *time.Duration + EstimatedSendRateBps *uint64 + EstimatedRecvRateBps *uint64 + BytesSent *uint64 + BytesReceived *uint64 + BytesLost *uint64 + PacketsSent *uint64 + PacketsReceived *uint64 + PacketsLost *uint64 +} + +func connectionStatsFromFFI(s ffi.MoqConnectionStats) ConnectionStats { + return ConnectionStats{ + RTT: optFromMicros(s.RttUs), + EstimatedSendRateBps: s.EstimatedSendRateBps, + EstimatedRecvRateBps: s.EstimatedRecvRateBps, + BytesSent: s.BytesSent, + BytesReceived: s.BytesReceived, + BytesLost: s.BytesLost, + PacketsSent: s.PacketsSent, + PacketsReceived: s.PacketsReceived, + PacketsLost: s.PacketsLost, + } +} diff --git a/go/wrapper/server.go b/go/wrapper/server.go index 108e51037d6..a3fc9e4854e 100644 --- a/go/wrapper/server.go +++ b/go/wrapper/server.go @@ -6,6 +6,7 @@ import ( "sync" ffi "moq.dev/moq-ffi/moq" + "moq.dev/moq/internal/bridge" ) // Transport is the network transport carrying an incoming session. @@ -52,28 +53,20 @@ func (r *Request) Transport() Transport { return r.inner.Transport() } -// SetPublish overrides the publish origin for this session. Pass nil to fall -// back to the server's configured publish origin. Captured at Accept. -func (r *Request) SetPublish(o *OriginProducer) error { - if o == nil { - return r.inner.SetPublish(nil) +// Accept completes the handshake, inheriting server origins wherever an argument is nil. +// Hold the session to keep the connection alive. Pass fresh origins for isolation, +// or the same fresh origin on both sides to share it. +func (r *Request) Accept(ctx context.Context, publish, consume *OriginProducer) (*Session, error) { + var ffiPublish, ffiConsume **ffi.MoqOriginProducer + if publish != nil { + ffiPublish = &publish.inner } - return r.inner.SetPublish(&o.inner) -} - -// SetConsume overrides the consume origin for this session. Pass nil to fall -// back to the server's configured consume origin. Captured at Accept. -func (r *Request) SetConsume(o *OriginProducer) error { - if o == nil { - return r.inner.SetConsume(nil) + if consume != nil { + ffiConsume = &consume.inner } - return r.inner.SetConsume(&o.inner) -} - -// Accept completes the handshake and returns the established session. Hold the -// session to keep the connection alive. -func (r *Request) Accept(ctx context.Context) (*Session, error) { - inner, err := runHandle(ctx, r.inner.Cancel, r.inner.Accept) + inner, err := bridge.CallHandle(ctx, r.inner.Cancel, func(ctx context.Context) (*ffi.MoqSession, error) { + return r.inner.Accept(ctx, ffiPublish, ffiConsume) + }) if err != nil { return nil, err } @@ -82,7 +75,7 @@ func (r *Request) Accept(ctx context.Context) (*Session, error) { // Reject refuses the session with an application error code; 401 and 403 map to unauthorized. func (r *Request) Reject(ctx context.Context, code uint16) error { - return runErr(ctx, r.inner.Cancel, func(ctx context.Context) error { + return bridge.CallErr(ctx, r.inner.Cancel, func(ctx context.Context) error { return r.inner.Reject(ctx, code) }) } @@ -96,11 +89,13 @@ func (r *Request) Cancel() { type ServerOption func(*serverConfig) type serverConfig struct { - tlsCert []string - tlsKey []string - tlsGenerate []string - publish *OriginProducer - subscribe *OriginProducer + tlsCert []string + tlsKey []string + tlsGenerate []string + versions []string + quicMaxStreams *uint64 + publish *OriginProducer + consume *OriginProducer } // WithTLSCert sets paths to TLS certificate chains. @@ -118,24 +113,35 @@ func WithTLSGenerate(hostnames ...string) ServerOption { return func(c *serverConfig) { c.tlsGenerate = hostnames } } +// WithServerVersions restricts the protocol versions accepted, spelled like +// "moq-lite-03". By default every supported version is accepted. +func WithServerVersions(versions ...string) ServerOption { + accepted := append([]string(nil), versions...) + return func(c *serverConfig) { c.versions = accepted } +} + +// WithServerQUICMaxStreams caps the concurrent QUIC streams each peer may open +// toward this server (default 1024). +func WithServerQUICMaxStreams(maxStreams uint64) ServerOption { + return func(c *serverConfig) { c.quicMaxStreams = &maxStreams } +} + // WithServerPublishOrigin sets the origin whose broadcasts are served to // incoming sessions. Omit both origin options to get a shared internal origin. func WithServerPublishOrigin(o *OriginProducer) ServerOption { return func(c *serverConfig) { c.publish = o } } -// WithServerSubscribeOrigin sets the origin that receives broadcasts published +// WithServerConsumeOrigin sets the origin that receives broadcasts published // by incoming sessions. -func WithServerSubscribeOrigin(o *OriginProducer) ServerOption { - return func(c *serverConfig) { c.subscribe = o } +func WithServerConsumeOrigin(o *OriginProducer) ServerOption { + return func(c *serverConfig) { c.consume = o } } // Server accepts incoming sessions with automatic origin wiring. type Server struct { inner *ffi.MoqServer - origin *OriginProducer publishOrigin *OriginProducer - consumeOrigin *OriginProducer localAddr string closeOnce sync.Once } @@ -146,41 +152,36 @@ func Listen(ctx context.Context, bind string, opts ...ServerOption) (*Server, er for _, opt := range opts { opt(&cfg) } - - s := &Server{} - if cfg.publish == nil && cfg.subscribe == nil { - s.origin = NewOriginProducer() - s.publishOrigin = s.origin - s.consumeOrigin = s.origin - } else { - s.publishOrigin = cfg.publish - s.consumeOrigin = cfg.subscribe + if cfg.publish == nil && cfg.consume == nil { + shared := NewOriginProducer() + cfg.publish = shared + cfg.consume = shared } - inner := ffi.NewMoqServer() - err := inner.SetBind(bind) - if err == nil && len(cfg.tlsCert) > 0 { - err = inner.SetTlsCert(cfg.tlsCert) - } - if err == nil && len(cfg.tlsKey) > 0 { - err = inner.SetTlsKey(cfg.tlsKey) + config := ffi.MoqServerConfig{ + Bind: &bind, + Versions: cfg.versions, + Tls: ffi.MoqServerTls{ + Cert: cfg.tlsCert, + Key: cfg.tlsKey, + Generate: cfg.tlsGenerate, + }, + Quic: ffi.MoqQuicConfig{MaxStreams: cfg.quicMaxStreams}, } - if err == nil && len(cfg.tlsGenerate) > 0 { - err = inner.SetTlsGenerate(cfg.tlsGenerate) + if cfg.publish != nil { + config.Publish = &cfg.publish.inner } - if err == nil && s.publishOrigin != nil { - err = inner.SetPublish(&s.publishOrigin.inner) - } - if err == nil && s.consumeOrigin != nil { - err = inner.SetConsume(&s.consumeOrigin.inner) + if cfg.consume != nil { + config.Consume = &cfg.consume.inner } + + inner, err := ffi.NewMoqServer(config) if err != nil { - inner.Cancel() return nil, err } - s.inner = inner + s := &Server{inner: inner, publishOrigin: cfg.publish} - addr, err := runCancellable(ctx, inner.Cancel, inner.Listen) + addr, err := bridge.Call(ctx, inner.Cancel, inner.Listen) if err != nil { inner.Cancel() return nil, err @@ -223,9 +224,9 @@ func (s *Server) Accept(ctx context.Context) (*Request, error) { return &Request{inner: *res}, nil } -// Requests ranges over incoming requests until the server stops or the loop +// All ranges over incoming requests until the server stops or the loop // breaks. Each request must be answered with Accept or Reject. -func (s *Server) Requests(ctx context.Context) iter.Seq2[*Request, error] { +func (s *Server) All(ctx context.Context) iter.Seq2[*Request, error] { return func(yield func(*Request, error) bool) { for { req, err := s.Accept(ctx) @@ -250,7 +251,7 @@ func (s *Server) Requests(ctx context.Context) iter.Seq2[*Request, error] { // // To inspect or reject requests, range over Requests instead: // -// for req, err := range server.Requests(ctx) { +// for req, err := range server.All(ctx) { // if err != nil { // return err // } @@ -258,7 +259,7 @@ func (s *Server) Requests(ctx context.Context) iter.Seq2[*Request, error] { // _ = req.Reject(ctx, 403) // continue // } -// session, err := req.Accept(ctx) +// session, err := req.Accept(ctx, nil, nil) // // hold the session to keep the connection alive // } func (s *Server) Serve(ctx context.Context) error { @@ -282,7 +283,7 @@ func (s *Server) Serve(ctx context.Context) error { wg.Add(1) go func(req *Request) { defer wg.Done() - session, err := req.Accept(ctx) + session, err := req.Accept(ctx, nil, nil) if err != nil { return } diff --git a/go/wrapper/session.go b/go/wrapper/session.go index d97c104dc08..ff8b3c458a4 100644 --- a/go/wrapper/session.go +++ b/go/wrapper/session.go @@ -4,6 +4,7 @@ import ( "context" ffi "moq.dev/moq-ffi/moq" + "moq.dev/moq/internal/bridge" ) // Session is an established MoQ connection. Hold it (or the Client/Server that @@ -18,13 +19,13 @@ type Session struct { // up waiting and shuts the session down, so a caller that no longer cares about // the connection can tear it down by cancelling. func (s *Session) Closed(ctx context.Context) error { - return runErr(ctx, func() { s.inner.Cancel(0) }, s.inner.Closed) + return bridge.CallErr(ctx, s.abort, s.inner.Closed) } // Status blocks until the connection status differs from the one this session -// last reported. A client session reports StatusConnected first (the connect it -// was built from), then follows the reconnect loop: StatusDisconnected while -// redialing, StatusMigrating during a GOAWAY handover. It returns an error once +// last reported. A client session reports ConnectionStatusConnected first (the connect it +// was built from), then follows the reconnect loop: ConnectionStatusDisconnected while +// redialing, ConnectionStatusMigrating during a GOAWAY handover. It returns an error once // the connection stops for good. A server-accepted session's only transition is // terminal, so Status waits for the close and returns its reason. Cancelling ctx // gives up waiting and shuts the session down. @@ -33,13 +34,13 @@ func (s *Session) Closed(ctx context.Context) error { // before the next call is coalesced away, so the outages it hides are the ones // that already healed. Don't count outages with it. func (s *Session) Status(ctx context.Context) (ConnectionStatus, error) { - return runCancellable(ctx, func() { s.inner.Cancel(0) }, s.inner.Status) + return bridge.Call(ctx, s.abort, s.inner.Status) } // Epoch is the connection epoch: 1 for the connect that built this session, one // more on each reconnect. A server-accepted session stays at 1. // -// Pair it with Status to log each reconnect by number: a StatusConnected whose +// Pair it with Status to log each reconnect by number: a ConnectionStatusConnected whose // Epoch grew is a reconnect. Like Status, it reports the current state, so a // drop that reconnects between reads is coalesced away. func (s *Session) Epoch() uint64 { @@ -48,7 +49,7 @@ func (s *Session) Epoch() uint64 { // Stats snapshots the current connection statistics. func (s *Session) Stats() ConnectionStats { - return s.inner.Stats() + return connectionStatsFromFFI(s.inner.Stats()) } // Bandwidth is the session's bandwidth allocator. Every call returns a handle @@ -106,7 +107,12 @@ func (s *Session) Consume() *OriginConsumer { // Shutdown drains finished tracks within one second, returning any delivery error. // Cancelling ctx aborts the session immediately. func (s *Session) Shutdown(ctx context.Context) error { - return runErr(ctx, func() { s.inner.Cancel(0) }, s.inner.Shutdown) + return bridge.CallErr(ctx, s.abort, s.inner.Shutdown) +} + +// abort tears the session down without draining, for a cancelled ctx. +func (s *Session) abort() { + s.inner.Cancel(0) } // Cancel closes the session abruptly with an application error code. diff --git a/go/wrapper/subscribe.go b/go/wrapper/subscribe.go index 6bfa4221408..f7cad4c7007 100644 --- a/go/wrapper/subscribe.go +++ b/go/wrapper/subscribe.go @@ -5,6 +5,7 @@ import ( "iter" ffi "moq.dev/moq-ffi/moq" + "moq.dev/moq/internal/bridge" ) // BroadcastConsumer reads tracks from a broadcast. @@ -12,15 +13,6 @@ type BroadcastConsumer struct { inner *ffi.MoqBroadcastConsumer } -// SubscribeCatalog subscribes to the broadcast's catalog track. -func (b *BroadcastConsumer) SubscribeCatalog(ctx context.Context) (*CatalogConsumer, error) { - inner, err := b.inner.SubscribeCatalog(ctx) - if err != nil { - return nil, err - } - return &CatalogConsumer{inner: inner}, nil -} - // SubscribeTrack subscribes to a track, receiving arbitrary byte payloads. // subscription tunes delivery priority, group range, and staleness; pass nil for defaults. func (b *BroadcastConsumer) SubscribeTrack( @@ -28,7 +20,11 @@ func (b *BroadcastConsumer) SubscribeTrack( name string, subscription *Subscription, ) (*TrackConsumer, error) { - inner, err := b.inner.SubscribeTrack(ctx, name, subscription) + sub, err := subscriptionFFI(subscription) + if err != nil { + return nil, err + } + inner, err := b.inner.SubscribeTrack(ctx, name, sub) if err != nil { return nil, err } @@ -50,45 +46,11 @@ func (b *BroadcastConsumer) FetchGroup( return &GroupConsumer{inner: inner}, nil } -// FetchMediaGroup fetches one group by sequence and decodes it with the given -// container. Unlike SubscribeMedia this holds no live subscription and applies no -// latency-based group skipping, so every frame in the group is delivered. -func (b *BroadcastConsumer) FetchMediaGroup( - ctx context.Context, - name string, - sequence uint64, - container Container, - options *FetchGroupOptions, -) (*MediaGroupConsumer, error) { - inner, err := b.inner.FetchMediaGroup(ctx, name, sequence, container, options) - if err != nil { - return nil, err - } - return &MediaGroupConsumer{inner: inner}, nil -} - -// SubscribeMedia subscribes to a media track, decoded with the given container. -// subscription tunes delivery priority, group range, and -// the max delay; pass nil for defaults. Raise Subscription.MaxDelayUs to -// buffer instead of skipping a stalled group. -func (b *BroadcastConsumer) SubscribeMedia( - ctx context.Context, - name string, - container Container, - subscription *Subscription, -) (*MediaConsumer, error) { - inner, err := b.inner.SubscribeMedia(ctx, name, container, subscription) - if err != nil { - return nil, err - } - return &MediaConsumer{inner: inner}, nil -} - // Resolve returns the broadcast serving a catalog rendition, honoring its broadcast -// reference: Video.Broadcast / Audio.Broadcast. A nil or empty reference names this +// reference: ffi.MoqVideo.Broadcast / ffi.MoqAudio.Broadcast. A nil or empty reference names this // broadcast, anything else names a sibling relative to it (e.g. "./source"). Call it on a -// rendition that carries one before SubscribeMedia, SubscribeTrack, FetchGroup, or -// FetchMediaGroup, which take a track name rather than a rendition; DecodeAudio and +// rendition that carries one before media.NewContainerConsumer, SubscribeTrack, FetchGroup, or +// media.NewContainerGroupConsumer, which take a track name rather than a rendition; DecodeAudio and // DecodeVideo resolve it themselves. // // Errors if this broadcast came from a local producer rather than an origin, since a @@ -106,7 +68,7 @@ func (b *BroadcastConsumer) Resolve(ctx context.Context, reference *string) (*Br func (b *BroadcastConsumer) DecodeAudio( ctx context.Context, name string, - catalogAudio Audio, + catalogAudio ffi.MoqAudio, output AudioDecoderOutput, ) (*AudioConsumer, error) { inner, err := b.inner.DecodeAudio(ctx, name, catalogAudio, output) @@ -123,7 +85,7 @@ func (b *BroadcastConsumer) DecodeAudio( func (b *BroadcastConsumer) DecodeVideo( ctx context.Context, name string, - catalogVideo Video, + catalogVideo ffi.MoqVideo, output VideoDecoderOutput, ) (*VideoConsumer, error) { inner, err := b.inner.DecodeVideo(ctx, name, catalogVideo, output) @@ -133,70 +95,6 @@ func (b *BroadcastConsumer) DecodeVideo( return &VideoConsumer{inner: inner}, nil } -// Catalog subscribes and returns the first catalog. It reports ErrClosed if the -// catalog track ends before any catalog arrives. -func (b *BroadcastConsumer) Catalog(ctx context.Context) (*Catalog, error) { - consumer, err := b.SubscribeCatalog(ctx) - if err != nil { - return nil, err - } - defer consumer.Cancel() - catalog, err := consumer.Next(ctx) - if err != nil { - return nil, err - } - if catalog == nil { - return nil, ErrClosed - } - return catalog, nil -} - -// MediaConsumer is a stream of decoded media frames. -type MediaConsumer struct { - inner *ffi.MoqMediaConsumer -} - -// Next returns the next frame, or (nil, nil) when the track ends. -func (m *MediaConsumer) Next(ctx context.Context) (*MediaFrame, error) { - return runCancellable(ctx, m.inner.Cancel, m.inner.Next) -} - -// Frames ranges over frames until the track ends or the loop breaks. -func (m *MediaConsumer) Frames(ctx context.Context) iter.Seq2[*MediaFrame, error] { - return streamSeq(ctx, m.Next) -} - -// Cancel stops the stream. -func (m *MediaConsumer) Cancel() { - m.inner.Cancel() -} - -// MediaGroupConsumer is a finite stream of decoded media frames from one fetched -// group. It ends after the group's last frame. -type MediaGroupConsumer struct { - inner *ffi.MoqMediaGroupConsumer -} - -// Sequence is this group's sequence number within the track. -func (m *MediaGroupConsumer) Sequence() uint64 { - return m.inner.Sequence() -} - -// Next returns the next decoded frame, or (nil, nil) when the group ends. -func (m *MediaGroupConsumer) Next(ctx context.Context) (*MediaFrame, error) { - return runCancellable(ctx, m.inner.Cancel, m.inner.Next) -} - -// Frames ranges over decoded frames until the group ends or the loop breaks. -func (m *MediaGroupConsumer) Frames(ctx context.Context) iter.Seq2[*MediaFrame, error] { - return streamSeq(ctx, m.Next) -} - -// Cancel stops the stream. -func (m *MediaGroupConsumer) Cancel() { - m.inner.Cancel() -} - // GroupConsumer is a stream of timestamped raw frames within a single group. type GroupConsumer struct { inner *ffi.MoqGroupConsumer @@ -209,12 +107,13 @@ func (g *GroupConsumer) Sequence() uint64 { // ReadFrame returns the next timestamped frame, or (nil, nil) when the group ends. func (g *GroupConsumer) ReadFrame(ctx context.Context) (*Frame, error) { - return runCancellable(ctx, g.inner.Cancel, g.inner.ReadFrame) + frame, err := bridge.Call(ctx, g.inner.Cancel, g.inner.ReadFrame) + return frameFromFFI(frame), err } -// Frames ranges over timestamped frames until the group ends or the loop breaks. -func (g *GroupConsumer) Frames(ctx context.Context) iter.Seq2[*Frame, error] { - return streamSeq(ctx, g.ReadFrame) +// All ranges over timestamped frames until the group ends or the loop breaks. +func (g *GroupConsumer) All(ctx context.Context) iter.Seq2[*Frame, error] { + return bridge.Seq(ctx, g.ReadFrame) } // Cancel stops the stream. @@ -232,7 +131,7 @@ type TrackConsumer struct { // or (nil, nil) when the track ends. Prefer this for live, latency-sensitive // consumption. func (t *TrackConsumer) RecvGroup(ctx context.Context) (*GroupConsumer, error) { - res, err := runHandle(ctx, t.inner.Cancel, func(ctx context.Context) (*ffi.MoqGroupConsumer, error) { + res, err := bridge.CallHandle(ctx, t.inner.Cancel, func(ctx context.Context) (*ffi.MoqGroupConsumer, error) { res, err := t.inner.RecvGroup(ctx) if err != nil || res == nil { return nil, err @@ -250,7 +149,7 @@ func (t *TrackConsumer) RecvGroup(ctx context.Context) (*GroupConsumer, error) { // more than latency. Shares the sequence cursor with ReadFrame: a group one // method has already taken is not returned by the other. func (t *TrackConsumer) NextGroup(ctx context.Context) (*GroupConsumer, error) { - res, err := runHandle(ctx, t.inner.Cancel, func(ctx context.Context) (*ffi.MoqGroupConsumer, error) { + res, err := bridge.CallHandle(ctx, t.inner.Cancel, func(ctx context.Context) (*ffi.MoqGroupConsumer, error) { res, err := t.inner.NextGroup(ctx) if err != nil || res == nil { return nil, err @@ -269,39 +168,51 @@ func (t *TrackConsumer) NextGroup(ctx context.Context) (*GroupConsumer, error) { // Cancelling the context cancels this consumer, not just this call: see // package docs. func (t *TrackConsumer) ReadFrame(ctx context.Context) (*Frame, error) { - return runCancellable(ctx, t.inner.Cancel, t.inner.ReadFrame) + frame, err := bridge.Call(ctx, t.inner.Cancel, t.inner.ReadFrame) + return frameFromFFI(frame), err } // RecvDatagram returns the next best-effort datagram in arrival order, or // (nil, nil) when the track ends. func (t *TrackConsumer) RecvDatagram(ctx context.Context) (*Datagram, error) { - return runCancellable(ctx, t.inner.Cancel, t.inner.RecvDatagram) + datagram, err := bridge.Call(ctx, t.inner.Cancel, t.inner.RecvDatagram) + return datagramFromFFI(datagram), err } // Info returns the publisher-side track properties learned during subscription. func (t *TrackConsumer) Info() (TrackInfo, error) { - return t.inner.Info() + info, err := t.inner.Info() + if err != nil { + return TrackInfo{}, err + } + return trackInfoFromFFI(info), nil } -// Update changes this subscriber's delivery preferences. -func (t *TrackConsumer) Update(subscription Subscription) { - t.inner.Update(subscription) +// Update changes this subscriber's delivery preferences. It fails only on a +// negative MaxDelay. +func (t *TrackConsumer) Update(subscription Subscription) error { + sub, err := subscription.ffi() + if err != nil { + return err + } + t.inner.Update(sub) + return nil } // Groups ranges over groups in sequence order. func (t *TrackConsumer) Groups(ctx context.Context) iter.Seq2[*GroupConsumer, error] { - return streamSeq(ctx, t.NextGroup) + return bridge.Seq(ctx, t.NextGroup) } // GroupsAsArrived ranges over groups in arrival order, including // out-of-sequence deliveries. func (t *TrackConsumer) GroupsAsArrived(ctx context.Context) iter.Seq2[*GroupConsumer, error] { - return streamSeq(ctx, t.RecvGroup) + return bridge.Seq(ctx, t.RecvGroup) } // Datagrams ranges over best-effort datagrams in arrival order. func (t *TrackConsumer) Datagrams(ctx context.Context) iter.Seq2[*Datagram, error] { - return streamSeq(ctx, t.RecvDatagram) + return bridge.Seq(ctx, t.RecvDatagram) } // Cancel stops the stream. @@ -316,12 +227,12 @@ type AudioConsumer struct { // Next returns the next audio frame, or (nil, nil) when the track ends. func (a *AudioConsumer) Next(ctx context.Context) (*AudioFrame, error) { - return runCancellable(ctx, a.inner.Cancel, a.inner.Next) + return bridge.Call(ctx, a.inner.Cancel, a.inner.Next) } -// Frames ranges over audio frames until the track ends or the loop breaks. -func (a *AudioConsumer) Frames(ctx context.Context) iter.Seq2[*AudioFrame, error] { - return streamSeq(ctx, a.Next) +// All ranges over audio frames until the track ends or the loop breaks. +func (a *AudioConsumer) All(ctx context.Context) iter.Seq2[*AudioFrame, error] { + return bridge.Seq(ctx, a.Next) } // Cancel stops the stream. @@ -337,7 +248,7 @@ type VideoConsumer struct { // Next returns the next decoded frame, or (nil, nil) when the track ends. // Close each frame when done with it. func (v *VideoConsumer) Next(ctx context.Context) (*VideoDecodedFrame, error) { - res, err := runHandle(ctx, v.inner.Cancel, func(ctx context.Context) (*ffi.MoqVideoDecodedFrame, error) { + res, err := bridge.CallHandle(ctx, v.inner.Cancel, func(ctx context.Context) (*ffi.MoqVideoDecodedFrame, error) { res, err := v.inner.Next(ctx) if err != nil || res == nil { return nil, err @@ -350,9 +261,9 @@ func (v *VideoConsumer) Next(ctx context.Context) (*VideoDecodedFrame, error) { return &VideoDecodedFrame{inner: res}, nil } -// Frames ranges over decoded frames until the track ends or the loop breaks. -func (v *VideoConsumer) Frames(ctx context.Context) iter.Seq2[*VideoDecodedFrame, error] { - return streamSeq(ctx, v.Next) +// All ranges over decoded frames until the track ends or the loop breaks. +func (v *VideoConsumer) All(ctx context.Context) iter.Seq2[*VideoDecodedFrame, error] { + return bridge.Seq(ctx, v.Next) } // Cancel stops the stream. @@ -401,23 +312,3 @@ func (f *VideoDecodedFrame) Surface() VideoSurface { func (f *VideoDecodedFrame) Close() { f.inner.Destroy() } - -// CatalogConsumer is a stream of catalog updates. -type CatalogConsumer struct { - inner *ffi.MoqCatalogConsumer -} - -// Next returns the next catalog, or (nil, nil) when the track ends. -func (c *CatalogConsumer) Next(ctx context.Context) (*Catalog, error) { - return runCancellable(ctx, c.inner.Cancel, c.inner.Next) -} - -// Updates ranges over catalog updates until the track ends or the loop breaks. -func (c *CatalogConsumer) Updates(ctx context.Context) iter.Seq2[*Catalog, error] { - return streamSeq(ctx, c.Next) -} - -// Cancel stops the stream. -func (c *CatalogConsumer) Cancel() { - c.inner.Cancel() -} diff --git a/go/wrapper/types.go b/go/wrapper/types.go index 0f78269a3f5..709dc1d0ab2 100644 --- a/go/wrapper/types.go +++ b/go/wrapper/types.go @@ -6,8 +6,6 @@ import ffi "moq.dev/moq-ffi/moq" // These are plain data, so type aliases are exact: a moq.AudioFrame is an // ffi.MoqAudioFrame, constructible and comparable across the boundary. type ( - // Audio describes one audio rendition in a broadcast catalog: codec, sample rate, channel count, whether it is enabled, and container. - Audio = ffi.MoqAudio // AudioCodec selects the audio encoder codec. Build one with OpusAudioCodec or AacAudioCodec; // adding a codec later adds a constructor, not a breaking enum change. AudioCodec = ffi.MoqAudioCodec @@ -21,20 +19,8 @@ type ( AudioSampleFormat = ffi.MoqAudioSampleFormat // AudioFrame is one audio frame: PCM payload plus a presentation timestamp in microseconds. AudioFrame = ffi.MoqAudioFrame - // Catalog is a broadcast's manifest: its video and audio renditions plus display metadata. - Catalog = ffi.MoqCatalog - // ConnectionStats holds transport metrics for a session (RTT, bandwidth, byte and packet counters); each field is nil when unreported. - ConnectionStats = ffi.MoqConnectionStats // ConnectionStatus is a connection lifecycle transition reported by Session.Status. ConnectionStatus = ffi.MoqConnectionStatus - // Datagram is a best-effort track datagram as received: sequence number, timestamp, and payload. - Datagram = ffi.MoqDatagram - // Dimensions is a width and height in pixels. - Dimensions = ffi.MoqDimensions - // Frame is a raw track frame: a payload and its presentation timestamp in microseconds. - Frame = ffi.MoqFrame - // MediaFrame is a Frame plus a flag for group starts or video keyframes; audio flags only group starts. - MediaFrame = ffi.MoqMediaFrame // ErrorScope is whether a protocol code is from the session or stream registry. ErrorScope = ffi.MoqErrorScope // FetchGroupOptions configures a single FetchGroup call, currently just the delivery priority. @@ -59,29 +45,12 @@ type ( AnnounceEventUpdate = ffi.MoqAnnounceEventUpdate // AnnounceEventEnd reports that no route covers a prefix any more, carrying its last route. AnnounceEventEnd = ffi.MoqAnnounceEventEnd - // Subscription holds subscriber-side delivery preferences: priority, ordering, max delay, and group range. - Subscription = ffi.MoqSubscription - // TrackInfo holds publisher-side track properties: priority, ordering, max age, and timescale. - // A zero Priority is the least urgent, not the default; set 127 for the midpoint a nil TrackInfo uses. - TrackInfo = ffi.MoqTrackInfo - // Video describes one catalog rendition, including whether it is enabled (a disabled one has no frames coming). - Video = ffi.MoqVideo - // VideoHint supplies catalog fields a video stream can't reveal itself, such as bitrate, filling only the gaps. - VideoHint = ffi.MoqVideoHint // VideoDecoderOutput configures what DecodeVideo delivers: an optional resize, a max delay, and whether frames keep the decoder's surface (macOS only; refused elsewhere). VideoDecoderOutput = ffi.MoqVideoDecoderOutput // VideoSurface is a decoded frame's platform surface, from VideoDecodedFrame.Surface: VideoSurfacePixelBuffer on macOS and iOS. VideoSurface = ffi.MoqVideoSurface // VideoSurfacePixelBuffer is an Apple CVPixelBufferRef (IOSurface-backed NV12), as the address in Pointer. VideoSurfacePixelBuffer = ffi.MoqVideoSurfacePixelBuffer - // AudioFormat is a single audio codec an importer can parse. - AudioFormat = ffi.MoqAudioFormat - // VideoFormat is a single video codec an importer can parse. - VideoFormat = ffi.MoqVideoFormat - // ContainerFormat is a container that publishes its own tracks. - ContainerFormat = ffi.MoqContainerFormat - // VideoProperties holds catalog properties shared by every video rendition; nil fields clear those properties. - VideoProperties = ffi.MoqVideoProperties // VideoCodec identifies a published video track's codec: H.264 or H.265. VideoCodec = ffi.MoqVideoCodec // VideoPixelFormat is a CPU pixel layout (I420 or RGBA): written to a VideoProducer, or read from a VideoDecodedFrame. @@ -103,97 +72,6 @@ type ( VideoEncoderKindSoftware = ffi.MoqVideoEncoderKindSoftware // VideoEncoderKindNamed is the specific-backend variant of VideoEncoderKind; build one with NamedEncoder. VideoEncoderKindNamed = ffi.MoqVideoEncoderKindNamed - - // Container selects how subscribed media frames are demuxed. Build one with - // LegacyContainer, CmafContainer, or LocContainer. - Container = ffi.MoqContainer - // ContainerLegacy is the legacy hang container variant of Container; build one with LegacyContainer. - ContainerLegacy = ffi.MoqContainerLegacy - // ContainerCmaf is the CMAF (fMP4) container variant of Container, carrying its init segment; build one with CmafContainer. - ContainerCmaf = ffi.MoqContainerCmaf - // ContainerLoc is the low-overhead container variant of Container; build one with LocContainer. - ContainerLoc = ffi.MoqContainerLoc -) - -// LegacyContainer selects the legacy hang container for a media subscription. -func LegacyContainer() Container { - return ContainerLegacy{} -} - -// CmafContainer selects the CMAF (fMP4) container for a media subscription, -// initialized from the given init segment. -func CmafContainer(init []byte) Container { - return ContainerCmaf{Init: init} -} - -// LocContainer selects the low-overhead container for a media subscription. -func LocContainer() Container { - return ContainerLoc{} -} - -// AudioFormat values: a single audio codec an importer can parse. -const ( - // AudioFormatAac is Advanced Audio Coding, configured by an AudioSpecificConfig. - AudioFormatAac = ffi.MoqAudioFormatAac - // AudioFormatOpus is Opus, configured by an OpusHead. - AudioFormatOpus = ffi.MoqAudioFormatOpus - // AudioFormatFlac is FLAC, configured by its STREAMINFO block. - AudioFormatFlac = ffi.MoqAudioFormatFlac - // AudioFormatMp3 is MPEG-1/2 Audio Layer III. - AudioFormatMp3 = ffi.MoqAudioFormatMp3 -) - -// VideoFormat values: a single video codec an importer can parse. The two H.26x pairs -// differ by framing, not codec: Avc1/Hvc1 are length-prefixed with an out-of-band config -// record, Avc3/Hev1 are Annex-B with the parameter sets inline. -const ( - // VideoFormatAvc1 is H.264 with length-prefixed NALUs and an out-of-band avcC. - VideoFormatAvc1 = ffi.MoqVideoFormatAvc1 - // VideoFormatAvc3 is H.264 in Annex-B with inline SPS/PPS. - VideoFormatAvc3 = ffi.MoqVideoFormatAvc3 - // VideoFormatHvc1 is H.265 with length-prefixed NALUs and an out-of-band hvcC. - VideoFormatHvc1 = ffi.MoqVideoFormatHvc1 - // VideoFormatHev1 is H.265 in Annex-B with inline parameter sets. - VideoFormatHev1 = ffi.MoqVideoFormatHev1 - // VideoFormatAv01 is AV1. - VideoFormatAv01 = ffi.MoqVideoFormatAv01 - // VideoFormatVp8 is VP8. - VideoFormatVp8 = ffi.MoqVideoFormatVp8 - // VideoFormatVp9 is VP9. - VideoFormatVp9 = ffi.MoqVideoFormatVp9 -) - -// ContainerFormat values: a container that publishes its own tracks. -const ( - // ContainerFormatFmp4 is fragmented MP4 / CMAF. - ContainerFormatFmp4 = ffi.MoqContainerFormatFmp4 - // ContainerFormatMkv is Matroska / WebM. - ContainerFormatMkv = ffi.MoqContainerFormatMkv - // ContainerFormatTs is an MPEG-2 transport stream. - ContainerFormatTs = ffi.MoqContainerFormatTs - // ContainerFormatFlv is Flash Video, as used by RTMP. - ContainerFormatFlv = ffi.MoqContainerFormatFlv -) - -// AudioSampleFormat values: the raw PCM sample layout fed to or returned from the -// in-process Opus codec. -const ( - // AudioSampleFormatU8 is unsigned 8-bit interleaved PCM. - AudioSampleFormatU8 = ffi.MoqAudioSampleFormatU8 - // AudioSampleFormatS16 is signed 16-bit interleaved PCM. - AudioSampleFormatS16 = ffi.MoqAudioSampleFormatS16 - // AudioSampleFormatS32 is signed 32-bit interleaved PCM. - AudioSampleFormatS32 = ffi.MoqAudioSampleFormatS32 - // AudioSampleFormatF32 is 32-bit float interleaved PCM. - AudioSampleFormatF32 = ffi.MoqAudioSampleFormatF32 - // AudioSampleFormatU8Planar is unsigned 8-bit planar PCM, one buffer per channel. - AudioSampleFormatU8Planar = ffi.MoqAudioSampleFormatU8Planar - // AudioSampleFormatS16Planar is signed 16-bit planar PCM, one buffer per channel. - AudioSampleFormatS16Planar = ffi.MoqAudioSampleFormatS16Planar - // AudioSampleFormatS32Planar is signed 32-bit planar PCM, one buffer per channel. - AudioSampleFormatS32Planar = ffi.MoqAudioSampleFormatS32Planar - // AudioSampleFormatF32Planar is 32-bit float planar PCM, one buffer per channel. - AudioSampleFormatF32Planar = ffi.MoqAudioSampleFormatF32Planar ) // OpusAudioCodec selects Opus (RFC 6716) for EncodeAudio. @@ -250,12 +128,12 @@ func NamedEncoder(name string) VideoEncoderKind { // ConnectionStatus values: the lifecycle of a client session's connection. const ( - // StatusConnected means a session connected (the first connect, or a reconnect after a drop). - StatusConnected = ffi.MoqConnectionStatusConnected - // StatusDisconnected means the session dropped; a reconnect attempt follows. - StatusDisconnected = ffi.MoqConnectionStatusDisconnected - // StatusMigrating means the peer sent a GOAWAY; the replacement is being dialed while the old session keeps serving. - StatusMigrating = ffi.MoqConnectionStatusMigrating + // ConnectionStatusConnected means a session connected (the first connect, or a reconnect after a drop). + ConnectionStatusConnected = ffi.MoqConnectionStatusConnected + // ConnectionStatusDisconnected means the session dropped; a reconnect attempt follows. + ConnectionStatusDisconnected = ffi.MoqConnectionStatusDisconnected + // ConnectionStatusMigrating means the peer sent a GOAWAY; the replacement is being dialed while the old session keeps serving. + ConnectionStatusMigrating = ffi.MoqConnectionStatusMigrating ) // LogLevel configures the native tracing log level (e.g. "info", "debug"). @@ -314,3 +192,24 @@ const ( // ProtocolKindUnknown identifies unknown. ProtocolKindUnknown = ffi.MoqProtocolKindUnknown ) + +// AudioSampleFormat values: the raw PCM sample layout fed to or returned from the +// in-process Opus codec. +const ( + // AudioSampleFormatU8 is unsigned 8-bit interleaved PCM. + AudioSampleFormatU8 = ffi.MoqAudioSampleFormatU8 + // AudioSampleFormatS16 is signed 16-bit interleaved PCM. + AudioSampleFormatS16 = ffi.MoqAudioSampleFormatS16 + // AudioSampleFormatS32 is signed 32-bit interleaved PCM. + AudioSampleFormatS32 = ffi.MoqAudioSampleFormatS32 + // AudioSampleFormatF32 is 32-bit float interleaved PCM. + AudioSampleFormatF32 = ffi.MoqAudioSampleFormatF32 + // AudioSampleFormatU8Planar is unsigned 8-bit planar PCM, one buffer per channel. + AudioSampleFormatU8Planar = ffi.MoqAudioSampleFormatU8Planar + // AudioSampleFormatS16Planar is signed 16-bit planar PCM, one buffer per channel. + AudioSampleFormatS16Planar = ffi.MoqAudioSampleFormatS16Planar + // AudioSampleFormatS32Planar is signed 32-bit planar PCM, one buffer per channel. + AudioSampleFormatS32Planar = ffi.MoqAudioSampleFormatS32Planar + // AudioSampleFormatF32Planar is 32-bit float planar PCM, one buffer per channel. + AudioSampleFormatF32Planar = ffi.MoqAudioSampleFormatF32Planar +) diff --git a/kt/README.md b/kt/README.md index 0a0085498ea..1d8d539ebc9 100644 --- a/kt/README.md +++ b/kt/README.md @@ -25,32 +25,34 @@ The wrapper's POM declares `dev.moq:moq-ffi:[0.4.3,0.5)`, so Gradle resolves the ```kotlin import dev.moq.* +import dev.moq.media.* 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(AnnounceConfig(prefix = "demos/", filter = "*/camera")).collect { event -> + moq.announced(AnnounceConfig(prefix = "demos/", filter = "*/camera")).updates().collect { event -> if (event !is AnnounceEventStart) return@collect // Prefix stays origin-relative; captures reports what * matched. println("got broadcast ${event.announce.prefix}") println("captures ${event.announce.captures}") - val catalog = moq.requestBroadcast(event.announce.prefix).catalog() + val catalog = dev.moq.media.catalog(moq.requestBroadcast(event.announce.prefix)) println("catalog: $catalog") } } ``` -`Moq.connect` builds the `MoqClient`, applies TLS / bind options, wires the publish + subscribe origins, and hands back a `Moq` you can `use {}`. Cancelling the surrounding coroutine scope propagates through the Flow extensions to the native consumer's `cancel()` via their `onCompletion` hook. +`Moq.connect(url, ClientConfig(...))` builds the `MoqClient` from the config record, wires the publish + consume origins, and hands back a `Moq` you can `use {}`. Cancelling the surrounding coroutine scope propagates through the Flow extensions to the native consumer's `cancel()` via their `onCompletion` hook. ### What the wrapper adds The `dev.moq` package is intentionally thin: Kotlin has extension functions, so we keep the FFI objects and decorate them rather than re-wrapping every type. - **`Moq.connect(...)`**: a connection facade (`Moq.kt`), so you never hand-wire a `MoqClient`. -- **Typealiases** (`Aliases.kt`): re-export the `Moq*`-prefixed FFI types under clean `dev.moq` names (`OriginProducer`, `BroadcastConsumer`, `Catalog`, `Frame`, ...), so you import `dev.moq.*` only. A couple of sealed types (`Container`, `MoqException`) are not aliased because Kotlin can't resolve their subtypes through a typealias; use `uniffi.moq.*` for those. -- **Flow extensions** (`Flows.kt`): `updates()`, `groups()`, `frames()`, `announcements()`, `catalog()` turn the pull-based consumers into coroutine `Flow`s with cancellation wired through. `frames()` covers the media, audio, and video consumers alike. -- **Fetched media**: `fetchMediaGroup(...).frames()` streams the decoded frames of one retained group, then completes. +- **Typealiases** (`Aliases.kt`): re-export the `Moq*`-prefixed FFI types under clean `dev.moq` names (`OriginProducer`, `BroadcastConsumer`, `Frame`, ...), so you import `dev.moq.*` only. `MoqException` is not aliased because Kotlin can't resolve its subtypes through a typealias; use `uniffi.moq.*` for it. +- **Flow extensions** (`Flows.kt`): `updates()`, `groups()`, and `frames()` turn the pull-based consumers into coroutine `Flow`s with cancellation wired through. +- **Media** (`dev.moq.media`): catalog, importers, and container consumers, with `catalog(broadcast)` for a one-shot catalog read and `frames()` for container-decoded media. +- **Fetched media**: `ContainerGroupConsumer.fetch(...).frames()` streams the decoded frames of one retained group, then completes. - **Duration extensions** (`Durations.kt`): the FFI carries microseconds as integers, so `stats.rtt`, `backoff.initial`, `frame.timestamp`, and their siblings read back as a `kotlin.time.Duration`. - **`logLevel(...)`**: configures native Rust tracing without importing the raw bindings package. - **Raw datagrams**: `TrackProducer.appendDatagram(Frame(payload, timestampUs))` sends one best-effort frame and returns its sequence; `TrackConsumer.recvDatagram()` and `datagrams()` receive them. Payloads are capped at 1200 bytes, require a datagram-capable transport plus lite-05 or newer moq-lite or moq-transport, and have no stream fallback. diff --git a/kt/moq-ffi/src/jvmAndAndroidTest/kotlin/dev/moq/ffi/BindingsSmokeTest.kt b/kt/moq-ffi/src/jvmAndAndroidTest/kotlin/dev/moq/ffi/BindingsSmokeTest.kt index ebc481b3dd1..7932ddd3f96 100644 --- a/kt/moq-ffi/src/jvmAndAndroidTest/kotlin/dev/moq/ffi/BindingsSmokeTest.kt +++ b/kt/moq-ffi/src/jvmAndAndroidTest/kotlin/dev/moq/ffi/BindingsSmokeTest.kt @@ -2,6 +2,7 @@ package dev.moq.ffi import kotlinx.coroutines.test.runTest import uniffi.moq.MoqClient +import uniffi.moq.MoqClientConfig import uniffi.moq.MoqException import uniffi.moq.MoqOriginConfig import uniffi.moq.MoqOriginProducer @@ -17,7 +18,7 @@ import kotlin.test.assertFailsWith class BindingsSmokeTest { @Test fun `client constructs and connect fails fast on a bad url`() = runTest { - MoqClient().use { client -> + MoqClient(MoqClientConfig()).use { client -> client.cancel() assertFailsWith { client.connect("https://localhost:0/test") diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt index 85662a98082..9e2849810ad 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt @@ -9,8 +9,20 @@ package dev.moq // Session + connection handles. `Server` is not aliased: `dev.moq.Server` is the // listen facade (see Server.kt), which exposes the raw handle as `server`. -/** A MoQ client: configure the TLS/bind knobs, then connect to a relay. */ +/** A MoQ client built from a [ClientConfig]: connect it to a relay. */ typealias Client = uniffi.moq.MoqClient +/** Client configuration: bind address, versions, TLS, QUIC, WebSocket, reconnect pacing, and origins. */ +typealias ClientConfig = uniffi.moq.MoqClientConfig +/** Certificate trust and the mTLS identity for a [ClientConfig]. */ +typealias ClientTls = uniffi.moq.MoqClientTls +/** Server configuration: bind address, versions, TLS identity, QUIC, and origins. */ +typealias ServerConfig = uniffi.moq.MoqServerConfig +/** The served TLS identity for a [ServerConfig]: PEM files or generated hostnames. */ +typealias ServerTls = uniffi.moq.MoqServerTls +/** QUIC transport tuning, such as the peer's inbound stream cap. */ +typealias QuicConfig = uniffi.moq.MoqQuicConfig +/** The WebSocket fallback raced against QUIC: whether it runs and QUIC's head start. */ +typealias WebSocketConfig = uniffi.moq.MoqWebSocketConfig /** A live pub/sub session with a relay, exposing publish and consume origins. */ typealias Session = uniffi.moq.MoqSession /** An incoming session awaiting a decision: accept it to handshake, or reject it. */ @@ -75,18 +87,6 @@ typealias GroupProducer = uniffi.moq.MoqGroupProducer typealias GroupConsumer = uniffi.moq.MoqGroupConsumer // Media (codec-aware) producers and consumers. -/** The write side of a media track; discontinuity() marks a break between pre-framed payloads. */ -typealias MediaProducer = uniffi.moq.MoqMediaProducer -/** The write side of a media track fed a raw byte stream, with frame boundaries inferred. */ -typealias MediaStreamProducer = uniffi.moq.MoqMediaStreamProducer -/** The write side of a container, which publishes each track it describes. */ -typealias ContainerProducer = uniffi.moq.MoqContainerProducer -/** The write side of a container fed a raw byte stream. */ -typealias ContainerStreamProducer = uniffi.moq.MoqContainerStreamProducer -/** The read side of a media track: yields frames with codec metadata in decode order. */ -typealias MediaConsumer = uniffi.moq.MoqMediaConsumer -/** A finite fetched media group: yields container-decoded frames until the group ends. */ -typealias MediaGroupConsumer = uniffi.moq.MoqMediaGroupConsumer /** A demand-observable raw-audio track producer with explicit timeline re-anchoring after idle gaps. */ typealias AudioProducer = uniffi.moq.MoqAudioProducer /** The read side of a raw-audio track: yields decoded PCM frames. */ @@ -95,48 +95,12 @@ typealias AudioConsumer = uniffi.moq.MoqAudioConsumer typealias VideoConsumer = uniffi.moq.MoqVideoConsumer /** The write side of a raw-video track; pixels written here are encoded inside the FFI boundary. */ typealias VideoProducer = uniffi.moq.MoqVideoProducer -/** The read side of a broadcast's catalog: yields updates as the set of tracks changes. */ -typealias CatalogConsumer = uniffi.moq.MoqCatalogConsumer -/** Publishes lossy latest-value JSON snapshots. */ -typealias JsonSnapshotProducer = uniffi.moq.MoqJsonSnapshotProducer -/** Consumes reconstructed latest-value JSON snapshots. */ -typealias JsonSnapshotConsumer = uniffi.moq.MoqJsonSnapshotConsumer -/** Publishes a lossless stream of JSON records. */ -typealias JsonStreamProducer = uniffi.moq.MoqJsonStreamProducer -/** Consumes a lossless stream of JSON records. */ -typealias JsonStreamConsumer = uniffi.moq.MoqJsonStreamConsumer // Data types. -/** A broadcast's catalog: its tracks and their properties, plus any application sections. */ -typealias Catalog = uniffi.moq.MoqCatalog /** A datagram-delivered frame, tagged with a per-track sequence number. */ typealias Datagram = uniffi.moq.MoqDatagram /** A payload plus the timestamp it should be presented at. */ typealias Frame = uniffi.moq.MoqFrame -/** A media [Frame] whose keyframe flag marks group starts or video keyframes; audio flags only group starts. */ -typealias MediaFrame = uniffi.moq.MoqMediaFrame -/** The catalog description of a video track, including whether it is enabled (a disabled one has no frames coming). */ -typealias Video = uniffi.moq.MoqVideo -/** Caller-provided catalog fields for a video track. */ -typealias VideoHint = uniffi.moq.MoqVideoHint -/** A single audio codec an importer can parse. */ -typealias AudioFormat = uniffi.moq.MoqAudioFormat -/** A single video codec an importer can parse. */ -typealias VideoFormat = uniffi.moq.MoqVideoFormat -/** A container that publishes its own tracks. */ -typealias ContainerFormat = uniffi.moq.MoqContainerFormat -/** Catalog properties shared by every video rendition; absent fields clear those properties. */ -typealias VideoProperties = uniffi.moq.MoqVideoProperties -/** An audio codec, its required init bytes, and an optional label. */ -typealias AudioInit = uniffi.moq.MoqAudioInit -/** A video codec, optional init bytes, a label, and catalog hints. */ -typealias VideoInit = uniffi.moq.MoqVideoInit -/** A container format and its leading bytes. */ -typealias ContainerInit = uniffi.moq.MoqContainerInit -/** The catalog description of an audio track: codec, sample rate, channels, whether it is enabled, and container. */ -typealias Audio = uniffi.moq.MoqAudio -/** A width and height pair, in pixels. */ -typealias Dimensions = uniffi.moq.MoqDimensions /** A path-prefix route: the prefix it covers, relay hop ids (oldest first), and static production and link cost (lower wins). */ typealias Route = uniffi.moq.MoqRoute /** Tunes how a track subscription is delivered: priority, group ordering, and range. */ @@ -189,14 +153,10 @@ typealias ErrorScope = uniffi.moq.MoqErrorScope typealias ProtocolKind = uniffi.moq.MoqProtocolKind /** A protocol failure: scope, verbatim wire code, kind, and a diagnostic message. */ typealias ProtocolError = uniffi.moq.MoqProtocolError -/** Configures a lossy latest-value JSON track. */ -typealias JsonSnapshotConfig = uniffi.moq.MoqJsonSnapshotConfig -/** Configures a lossless JSON stream track. */ -typealias JsonStreamConfig = uniffi.moq.MoqJsonStreamConfig -// NOTE: a few types are intentionally NOT aliased. `MoqContainer` (sealed) and -// `MoqException` (sealed) need subtype access (`MoqContainer.Loc`, -// `MoqException.Closed`), which Kotlin 2.0.21 can't resolve through a typealias. -// Reference those as `uniffi.moq.MoqContainer` / `uniffi.moq.MoqException`. Enums +// NOTE: Kotlin 2.0.21 can't resolve a sealed type's subtypes through a typealias. +// `MoqException` is intentionally NOT aliased, so reference `uniffi.moq.MoqException.Closed` +// directly. `dev.moq.media.Container` aliases `MoqContainer` for signatures, but its +// variants still need the full name (`uniffi.moq.MoqContainer.Loc`). Enums // (AudioFormat) are fine: entry access through the alias works. Objects // (AudioCodec) expose constructors through the alias (`AudioCodec.opus()`). diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Durations.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Durations.kt index 60fa0bfb763..9618ba94aef 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Durations.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Durations.kt @@ -13,17 +13,21 @@ import kotlin.time.Duration.Companion.microseconds val ConnectionStats.rtt: Duration? get() = rttUs?.toLong()?.microseconds -/** Delay before the first reconnect attempt. */ -val Backoff.initial: Duration - get() = initialUs.toLong().microseconds +/** Delay before the first reconnect attempt, or null for the default. */ +val Backoff.initial: Duration? + get() = initialUs?.toLong()?.microseconds -/** Maximum delay between reconnect attempts. */ -val Backoff.max: Duration - get() = maxUs.toLong().microseconds +/** Maximum delay between reconnect attempts, or null for the default. */ +val Backoff.max: Duration? + get() = maxUs?.toLong()?.microseconds -/** Time spent retrying before giving up. [Duration.ZERO] retries forever. */ -val Backoff.timeout: Duration - get() = timeoutUs.toLong().microseconds +/** Time spent retrying before giving up, or null for the default. [Duration.ZERO] retries forever. */ +val Backoff.timeout: Duration? + get() = timeoutUs?.toLong()?.microseconds + +/** Head start QUIC gets before the WebSocket fallback joins, or null for the default. */ +val WebSocketConfig.delay: Duration? + get() = delayUs?.toLong()?.microseconds /** Upper bound on buffering before a stalled group is skipped. */ val Subscription.maxDelay: Duration @@ -49,10 +53,6 @@ val AudioEncoderOutput.frameDuration: Duration val Frame.timestamp: Duration? get() = timestampUs?.toLong()?.microseconds -/** Presentation timestamp. */ -val MediaFrame.timestamp: Duration - get() = timestampUs.toLong().microseconds - /** Presentation timestamp, or null for an untimed datagram. */ val Datagram.timestamp: Duration? get() = timestampUs?.toLong()?.microseconds diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Flows.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Flows.kt index 339ceb4dde4..c0db08bd7df 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Flows.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Flows.kt @@ -8,56 +8,6 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onCompletion import uniffi.moq.MoqException -/** - * Stream of catalog updates. Terminates when the underlying track ends. - * - * The Flow's [onCompletion] forwards Kotlin coroutine cancellation to the - * native consumer's `cancel()` so structured concurrency propagates through - * to the QUIC stream. - */ -fun CatalogConsumer.updates(): Flow = flow { - while (true) { - currentCoroutineContext().ensureActive() - emit(next() ?: break) - } -}.onCompletion { cause -> - if (cause is CancellationException) cancel() -} - -/** - * Subscribe to the catalog track and return the first catalog, cancelling the - * subscription before returning. Convenience for callers that only need the - * current catalog rather than a stream of updates (use [updates] for that). - */ -suspend fun BroadcastConsumer.catalog(): Catalog { - val consumer = subscribeCatalog() - try { - return consumer.next() ?: throw MoqException.Closed() - } finally { - consumer.cancel() - } -} - -/** Stream of decoded media frames in decode order. */ -fun MediaConsumer.frames(): Flow = flow { - while (true) { - currentCoroutineContext().ensureActive() - emit(next() ?: break) - } -}.onCompletion { cause -> - if (cause is CancellationException) cancel() -} - -/** Stream of decoded frames from one finite, fetched media group. */ -fun MediaGroupConsumer.frames(): Flow = flow { - while (true) { - currentCoroutineContext().ensureActive() - emit(next() ?: break) - } -}.onCompletion { cause -> - if (cause is CancellationException) cancel() -} - /** * Stream of decoded audio frames in the layout declared by the * [AudioDecoderOutput] the consumer was created with. @@ -84,29 +34,6 @@ fun VideoConsumer.frames(): Flow = flow { if (cause is CancellationException) cancel() } -/** - * Stream of JSON values (as strings) from a snapshot track, yielding the latest reconstructed - * value. A consumer that has fallen behind collapses the backlog to the latest. - */ -fun JsonSnapshotConsumer.values(): Flow = flow { - while (true) { - currentCoroutineContext().ensureActive() - emit(next() ?: break) - } -}.onCompletion { cause -> - if (cause is CancellationException) cancel() -} - -/** Stream of JSON records (as strings) from a stream track, in order. */ -fun JsonStreamConsumer.values(): Flow = flow { - while (true) { - currentCoroutineContext().ensureActive() - emit(next() ?: break) - } -}.onCompletion { cause -> - if (cause is CancellationException) cancel() -} - /** Stream of groups in sequence order, skipping forward if the reader falls behind. */ fun TrackConsumer.groups(): Flow = flow { while (true) { @@ -193,23 +120,13 @@ fun GroupConsumer.frames(): Flow = flow { } /** - * Stream of announce events matching [config]. + * Stream of announce events, as `announced(config).updates()`. * - * Acquires the subscription on first collection and cancels it when collection - * ends, so callers never touch the underlying handle. Use the raw - * `announced(config)` if you need to hold and cancel the handle yourself. + * Collect it once: the handle is cancelled when collection ends, however it ends. */ -fun OriginConsumer.announcements(config: AnnounceConfig = AnnounceConfig()): Flow { - val consumer = this - return flow { - val announced = consumer.announced(config) - try { - while (true) { - currentCoroutineContext().ensureActive() - emit(announced.next() ?: break) - } - } finally { - announced.cancel() - } +fun AnnounceConsumer.updates(): Flow = flow { + while (true) { + currentCoroutineContext().ensureActive() + emit(next() ?: break) } -} +}.onCompletion { cancel() } diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Json.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Json.kt index aa7a857e314..77c6a941098 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Json.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Json.kt @@ -1,65 +1,11 @@ package dev.moq -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import kotlinx.serialization.json.Json -import kotlinx.serialization.serializer -import uniffi.moq.MoqBroadcastProducer -import uniffi.moq.MoqJsonSnapshotConsumer -import uniffi.moq.MoqJsonSnapshotProducer -import uniffi.moq.MoqJsonStreamConsumer -import uniffi.moq.MoqJsonStreamProducer /** - * The [Json] format used by the typed helpers below. + * The [Json] format used by the typed helpers here and in `dev.moq.json`. * * Lenient about unknown keys so a producer adding a field does not break older * subscribers, matching how the catalog treats untyped sections. */ val MoqJson: Json = Json { ignoreUnknownKeys = true } - -/** - * Publish [value] as a JSON snapshot, superseding the last. A no-op when unchanged. - * - * The `@Serializable` type is encoded with [MoqJson]. Pass an already-encoded - * `String` to serialize with another library: the member overload takes it - * unchanged. - */ -inline fun MoqJsonSnapshotProducer.update(value: T) { - update(MoqJson.encodeToString(serializer(), value)) -} - -/** Append [value] to a JSON stream track as one record, encoded with [MoqJson]. */ -inline fun MoqJsonStreamProducer.append(value: T) { - append(MoqJson.encodeToString(serializer(), value)) -} - -/** - * Stream of decoded snapshot values, yielding the latest reconstructed value. - * - * A consumer that has fallen behind collapses the backlog to the latest. Use - * [MoqJsonSnapshotConsumer.values] for the undecoded JSON strings. - */ -inline fun MoqJsonSnapshotConsumer.valuesAs(): Flow = - values().map { MoqJson.decodeFromString(serializer(), it) } - -/** - * Stream of decoded stream records, in order. - * - * Use [MoqJsonStreamConsumer.values] for the undecoded JSON strings. - */ -inline fun MoqJsonStreamConsumer.valuesAs(): Flow = - values().map { MoqJson.decodeFromString(serializer(), it) } - -/** - * Set or replace an untyped application section in the catalog. - * - * [value] is encoded with [MoqJson] and lands as a top-level catalog key - * alongside `video`/`audio`, reaching subscribers via `Catalog.sections`. [name] - * must not be a reserved media section ("video"/"audio"). The catalog is - * republished automatically. Pass an already-encoded `String` to serialize with - * another library. - */ -inline fun MoqBroadcastProducer.setCatalogSection(name: String, value: T) { - setCatalogSection(name, MoqJson.encodeToString(serializer(), value)) -} diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Moq.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Moq.kt index a05ef2539ab..2e8dd95f60a 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Moq.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Moq.kt @@ -1,14 +1,11 @@ package dev.moq -import kotlinx.coroutines.flow.Flow -import kotlin.time.Duration - /** * A connected MoQ session with publish/subscribe conveniences. * * Build one with [Moq.connect]. The underlying [session] always exposes a - * publisher and a subscriber (wired from the origins you pass to [connect], or - * auto-created), so you can [createBroadcast] and iterate [announcements] + * publisher and a subscriber (wired from the origins in its [ClientConfig], or + * auto-created), so you can [createBroadcast] and collect [announced] updates * without touching the raw [Client] handle. * * Call [shutdown] to drain finished tracks before disconnecting. [AutoCloseable] @@ -28,14 +25,10 @@ class Moq internal constructor( fun createBroadcast(path: String): BroadcastProducer = session.publish().createBroadcast(path) /** - * Discover routes matching [config] as a [Flow] of [AnnounceEvent]. Prefixes stay - * relative to the origin. The subscription is acquired on - * collection and cancelled when collection ends. Use [announced] for the raw handle. + * Discover routes matching [config]; prefixes stay relative to the origin. + * Collect `announced(config).updates()` for a [Flow] of [AnnounceEvent] that + * cancels the handle when collection ends. */ - fun announcements(config: AnnounceConfig = AnnounceConfig()): Flow = - session.consume().announcements(config) - - /** Raw announcement handle for [config]; prefixes stay relative to the origin. */ fun announced(config: AnnounceConfig = AnnounceConfig()): AnnounceConsumer = session.consume().announced(config) @@ -93,72 +86,19 @@ class Moq internal constructor( /** * Connect to a relay at [url] and return the live [Moq] connection. * - * @param tlsVerify set false to skip certificate verification (local dev only). - * @param tlsRoots PEM root certificate paths to trust instead of platform roots. - * @param tlsSystemRoots whether to also trust platform roots when custom roots are set. - * @param tlsFingerprints peer certificate SHA-256 fingerprints to pin. - * @param tlsCert path to a PEM certificate chain to present for mTLS. - * @param tlsKey path to a PEM private key to present for mTLS. - * @param bind local socket address to bind, e.g. "0.0.0.0:0". - * @param maxStreams cap on the concurrent QUIC streams the peer may open toward - * this connection; MoQ opens one stream per group, and for a subscriber those - * arrive from the relay, so subscribing to many tracks may want this raised. - * @param reconnect set false for a one-shot dial. By default the session redials - * with backoff whenever the transport drops; watch [Session.status] for the - * transitions. - * @param backoff retry pacing for the automatic reconnect. - * @param websocketEnabled set false to stop the WebSocket fallback racing QUIC, - * e.g. against a relay that only serves QUIC. On by default. - * @param websocketDelay head start QUIC gets before the WebSocket fallback joins - * the race; 200ms by default, and zero races both at once. - * @param publish origin to announce broadcasts through; auto-created when null. - * @param subscribe origin to discover broadcasts through; auto-created when null. + * [config] carries the TLS trust, bind address, protocol versions, QUIC and + * WebSocket tuning, reconnect pacing, and origins; every field has a default. + * A value the native side cannot use throws `MoqException.Config`. * - * With neither [publish] nor [subscribe] given, both sides share one origin, so a - * broadcast announced on this connection is discoverable via its own [announcements] - * (loopback). Wiring either side opts out and isolates the two directions. + * With neither [ClientConfig.publish] nor [ClientConfig.consume] set, both sides + * share one origin, so a broadcast announced on this connection is discoverable + * via its own [announced] (loopback). Wiring either side opts out and isolates the + * two directions. */ - suspend fun connect( - url: String, - tlsVerify: Boolean = true, - tlsRoots: List? = null, - tlsSystemRoots: Boolean? = null, - tlsFingerprints: List? = null, - tlsCert: String? = null, - tlsKey: String? = null, - bind: String? = null, - reconnect: Boolean? = null, - backoff: Backoff? = null, - publish: OriginProducer? = null, - subscribe: OriginProducer? = null, - maxStreams: ULong? = null, - websocketEnabled: Boolean? = null, - websocketDelay: Duration? = null, - ): Moq { - require(websocketDelay == null || !websocketDelay.isNegative()) { - "websocketDelay must not be negative: $websocketDelay" - } - val client = Client() + suspend fun connect(url: String, config: ClientConfig = ClientConfig()): Moq { + val client = Client(config) try { - if (!tlsVerify) client.setTlsVerify(false) - if (tlsRoots != null) client.setTlsRoots(tlsRoots) - if (tlsSystemRoots != null) client.setTlsSystemRoots(tlsSystemRoots) - if (tlsFingerprints != null) client.setTlsFingerprints(tlsFingerprints) - if (tlsCert != null) client.setTlsCert(tlsCert) - if (tlsKey != null) client.setTlsKey(tlsKey) - if (bind != null) client.setBind(bind) - if (maxStreams != null) client.setQuicMaxStreams(maxStreams) - if (websocketEnabled != null) client.setWebsocketEnabled(websocketEnabled) - if (websocketDelay != null) { - client.setWebsocketDelay(websocketDelay.inWholeMicroseconds.toULong()) - } - if (reconnect != null) client.setReconnect(reconnect) - if (backoff != null) client.setBackoff(backoff) - if (publish != null) client.setPublish(publish) - if (subscribe != null) client.setConsume(subscribe) - - val session = client.connect(url) - return Moq(session, client) + return Moq(client.connect(url), client) } catch (e: Throwable) { // connect() failed: don't leak the client handle. client.cancel() diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Server.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Server.kt index e30c3aebbb4..a79f6465c5f 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Server.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Server.kt @@ -98,41 +98,28 @@ class Server internal constructor( companion object { /** - * Bind a server at [bind] and start accepting. + * Bind a server described by [config] and start accepting. * - * @param bind local socket address to listen on, e.g. "127.0.0.1:4443" or "[::]:443". - * @param tlsCert PEM certificate chain paths to serve. - * @param tlsKey PEM private key paths to serve. - * @param tlsGenerate hostnames to generate a self-signed certificate for. - * @param publish origin whose broadcasts are served to incoming sessions; auto-created when null. - * @param subscribe origin that receives broadcasts published by incoming sessions; auto-created when null. + * [config] carries the bind address (`[::]:443` when null), the TLS identity, + * protocol versions, QUIC tuning, and origins. A value the native side cannot + * use throws `MoqException.Config`. + * + * With neither [ServerConfig.publish] nor [ServerConfig.consume] set, one shared + * origin is wired to both, so a broadcast announced on this server is also + * visible to sessions publishing into it. Mirrors [Moq.connect]. */ - suspend fun listen( - bind: String = "[::]:443", - tlsCert: List? = null, - tlsKey: List? = null, - tlsGenerate: List? = null, - publish: OriginProducer? = null, - subscribe: OriginProducer? = null, - ): Server { - // With neither side specified, wire ONE shared origin to both so a - // broadcast announced on this server is also visible to sessions - // publishing into it. Mirrors Moq.connect. - val shared = if (publish == null && subscribe == null) OriginProducer(OriginConfig()) else null - val publishOrigin = publish ?: shared - val subscribeOrigin = subscribe ?: shared + suspend fun listen(config: ServerConfig): Server { + val wired = if (config.publish == null && config.consume == null) { + val shared = OriginProducer(OriginConfig()) + config.copy(publish = shared, consume = shared) + } else { + config + } - val server = MoqServer() + val server = MoqServer(wired) try { - server.setBind(bind) - if (tlsCert != null) server.setTlsCert(tlsCert) - if (tlsKey != null) server.setTlsKey(tlsKey) - if (tlsGenerate != null) server.setTlsGenerate(tlsGenerate) - if (publishOrigin != null) server.setPublish(publishOrigin) - if (subscribeOrigin != null) server.setConsume(subscribeOrigin) - val localAddr = server.listen() - return Server(server, localAddr, publishOrigin) + return Server(server, localAddr, wired.publish) } catch (e: Throwable) { // listen() failed: don't leak the server handle. server.cancel() diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/json/Tracks.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/json/Tracks.kt new file mode 100644 index 00000000000..9550f7600a3 --- /dev/null +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/json/Tracks.kt @@ -0,0 +1,88 @@ +package dev.moq.json + +// JSON tracks, mirroring the moq-json crate. Each type wraps a track: a +// producer takes over a `dev.moq.TrackProducer` and advertises it in the +// broadcast's catalog, and a consumer takes over a `dev.moq.TrackConsumer` +// that has not read a group yet: +// +// val status = SnapshotProducer(broadcast, broadcast.publishTrack("status", null), SnapshotConfig()) +// val reader = SnapshotConsumer(consumer.subscribeTrack("status", null), SnapshotConfig()) + +import dev.moq.MoqJson +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion +import kotlinx.serialization.serializer + +/** Publishes lossy latest-value JSON snapshots on a track it takes over. */ +typealias SnapshotProducer = uniffi.moq.MoqJsonSnapshotProducer +/** Consumes reconstructed latest-value JSON snapshots from a track it takes over. */ +typealias SnapshotConsumer = uniffi.moq.MoqJsonSnapshotConsumer +/** Publishes a lossless stream of JSON records on a track it takes over. */ +typealias StreamProducer = uniffi.moq.MoqJsonStreamProducer +/** Consumes a lossless stream of JSON records from a track it takes over. */ +typealias StreamConsumer = uniffi.moq.MoqJsonStreamConsumer +/** Configures a lossy latest-value JSON track. */ +typealias SnapshotConfig = uniffi.moq.MoqJsonSnapshotConfig +/** Configures a lossless JSON stream track. */ +typealias StreamConfig = uniffi.moq.MoqJsonStreamConfig + +/** + * Publish [value] as a JSON snapshot, superseding the last. A no-op when unchanged. + * + * The `@Serializable` type is encoded with [MoqJson]. Pass an already-encoded + * `String` to serialize with another library: the member overload takes it + * unchanged. + */ +inline fun SnapshotProducer.update(value: T) { + update(MoqJson.encodeToString(serializer(), value)) +} + +/** Append [value] to a JSON stream track as one record, encoded with [MoqJson]. */ +inline fun StreamProducer.append(value: T) { + append(MoqJson.encodeToString(serializer(), value)) +} + +/** + * Stream of JSON values (as strings) from a snapshot track, yielding the latest reconstructed + * value. A consumer that has fallen behind collapses the backlog to the latest. + */ +fun SnapshotConsumer.values(): Flow = flow { + while (true) { + currentCoroutineContext().ensureActive() + emit(next() ?: break) + } +}.onCompletion { cause -> + if (cause is CancellationException) cancel() +} + +/** Stream of JSON records (as strings) from a stream track, in order. */ +fun StreamConsumer.values(): Flow = flow { + while (true) { + currentCoroutineContext().ensureActive() + emit(next() ?: break) + } +}.onCompletion { cause -> + if (cause is CancellationException) cancel() +} + +/** + * Stream of decoded snapshot values, yielding the latest reconstructed value. + * + * A consumer that has fallen behind collapses the backlog to the latest. Use + * [SnapshotConsumer.values] for the undecoded JSON strings. + */ +inline fun SnapshotConsumer.valuesAs(): Flow = + values().map { MoqJson.decodeFromString(serializer(), it) } + +/** + * Stream of decoded stream records, in order. + * + * Use [StreamConsumer.values] for the undecoded JSON strings. + */ +inline fun StreamConsumer.valuesAs(): Flow = + values().map { MoqJson.decodeFromString(serializer(), it) } diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/media/Media.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/media/Media.kt new file mode 100644 index 00000000000..9f393d19721 --- /dev/null +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/media/Media.kt @@ -0,0 +1,134 @@ +package dev.moq.media + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onCompletion +import kotlin.time.Duration +import kotlin.time.Duration.Companion.microseconds +import dev.moq.Frame +import dev.moq.MoqJson +import kotlinx.serialization.serializer +import uniffi.moq.MoqException + +/** The write side of a media track; discontinuity() marks a break between pre-framed payloads. */ +typealias TrackProducer = uniffi.moq.MoqMediaTrackProducer +/** The write side of a media track fed a raw byte stream, with frame boundaries inferred. */ +typealias TrackStreamProducer = uniffi.moq.MoqMediaTrackStreamProducer +/** The write side of a container, which publishes each track it describes. */ +typealias ContainerProducer = uniffi.moq.MoqMediaContainerProducer +/** The write side of a container fed a raw byte stream. */ +typealias ContainerStreamProducer = uniffi.moq.MoqMediaContainerStreamProducer +/** The read side of a media track: yields frames with codec metadata in decode order. */ +typealias ContainerConsumer = uniffi.moq.MoqMediaContainerConsumer +/** A finite fetched media group: yields container-decoded frames until the group ends. */ +typealias ContainerGroupConsumer = uniffi.moq.MoqMediaContainerGroupConsumer +/** The read side of a broadcast's catalog: yields updates as the set of tracks changes. */ +typealias CatalogConsumer = uniffi.moq.MoqMediaCatalogConsumer +/** A broadcast's catalog: its tracks and their properties, plus any application sections. */ +typealias Catalog = uniffi.moq.MoqCatalog +/** A media [Frame] whose keyframe flag marks group starts or video keyframes; audio flags only group starts. */ +typealias MediaFrame = uniffi.moq.MoqMediaFrame +/** The catalog description of a video track, including whether it is enabled (a disabled one has no frames coming). */ +typealias Video = uniffi.moq.MoqVideo +/** Caller-provided catalog fields for a video track. */ +typealias VideoHint = uniffi.moq.MoqVideoHint +/** A single audio codec an importer can parse. */ +typealias AudioFormat = uniffi.moq.MoqAudioFormat +/** A single video codec an importer can parse. */ +typealias VideoFormat = uniffi.moq.MoqVideoFormat +/** A container that publishes its own tracks. */ +typealias ContainerFormat = uniffi.moq.MoqContainerFormat +/** Catalog properties shared by every video rendition; absent fields clear those properties. */ +typealias VideoProperties = uniffi.moq.MoqVideoProperties +/** An audio codec, its required init bytes, and an optional label. */ +typealias AudioInit = uniffi.moq.MoqAudioInit +/** A video codec, optional init bytes, a label, and catalog hints. */ +typealias VideoInit = uniffi.moq.MoqVideoInit +/** A container format and its leading bytes. */ +typealias ContainerInit = uniffi.moq.MoqContainerInit +/** The catalog description of an audio track: codec, sample rate, channels, whether it is enabled, and container. */ +typealias Audio = uniffi.moq.MoqAudio +/** A width and height pair, in pixels. */ +typealias Dimensions = uniffi.moq.MoqDimensions + +/** A weak catalog handle that closes with its broadcast. */ +typealias CatalogProducer = uniffi.moq.MoqMediaCatalogProducer +/** A named or requested track target for an importer. */ +typealias Target = uniffi.moq.MoqMediaTarget +/** A new track with a chosen or format-derived name. */ +typealias Named = uniffi.moq.MoqMediaTarget.Named +/** A subscriber-requested track. */ +typealias Requested = uniffi.moq.MoqMediaTarget.Requested +/** A track name, container, and live delivery options. */ +typealias ContainerConfig = uniffi.moq.MoqMediaContainerConfig +/** A fetched group name, sequence, container, and delivery options. */ +typealias ContainerGroupConfig = uniffi.moq.MoqMediaContainerGroupConfig +/** The packaging advertised for a media track. */ +typealias Container = uniffi.moq.MoqContainer + +/** + * Stream of catalog updates. Terminates when the underlying track ends. + * + * The Flow's [onCompletion] forwards Kotlin coroutine cancellation to the + * native consumer's `cancel()` so structured concurrency propagates through + * to the QUIC stream. + */ +fun CatalogConsumer.updates(): Flow = flow { + while (true) { + currentCoroutineContext().ensureActive() + emit(next() ?: break) + } +}.onCompletion { cause -> + if (cause is CancellationException) cancel() +} + +/** + * Subscribe to the catalog track and return the first catalog, cancelling the + * subscription before returning. Convenience for callers that only need the + * current catalog rather than a stream of updates (use [updates] for that). + */ +suspend fun catalog(broadcast: dev.moq.BroadcastConsumer): Catalog { + val consumer = CatalogConsumer.subscribe(broadcast) + try { + return consumer.next() ?: throw MoqException.Closed() + } finally { + consumer.cancel() + } +} + +/** Stream of decoded media frames in decode order. */ +fun ContainerConsumer.frames(): Flow = flow { + while (true) { + currentCoroutineContext().ensureActive() + emit(next() ?: break) + } +}.onCompletion { cause -> + if (cause is CancellationException) cancel() +} + +/** Stream of decoded frames from one finite, fetched media group. */ +fun ContainerGroupConsumer.frames(): Flow = flow { + while (true) { + currentCoroutineContext().ensureActive() + emit(next() ?: break) + } +}.onCompletion { cause -> + if (cause is CancellationException) cancel() +} + +/** Presentation timestamp of a container-decoded frame. */ +val MediaFrame.timestamp: Duration + get() = timestampUs.toLong().microseconds + +/** + * Set an application catalog section from a serializable value, encoded with [MoqJson]. + * + * A `String` argument resolves to the member `setSection(name, json)` instead, so it must + * already be encoded JSON. + */ +inline fun CatalogProducer.setSection(name: String, value: T) { + setSection(name, MoqJson.encodeToString(serializer(), value)) +} diff --git a/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt b/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt index 2f67bd9fa60..b6e4de26530 100644 --- a/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt +++ b/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt @@ -1,11 +1,19 @@ package dev.moq +import dev.moq.media.* + import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import kotlinx.coroutines.yield +import dev.moq.json.SnapshotConfig +import dev.moq.json.SnapshotConsumer +import dev.moq.json.SnapshotProducer +import dev.moq.json.StreamConfig +import dev.moq.json.update +import dev.moq.json.valuesAs import kotlinx.serialization.Serializable import uniffi.moq.MoqException import kotlin.test.Test @@ -40,9 +48,6 @@ private fun opusHead(): ByteArray = 0, ) -/** Wall-clock bound on polling for a configuration race, so a regression fails instead of hanging. */ -private const val CONFIG_RACE_TIMEOUT_NS = 10_000_000_000L - class SmokeTest { @Test fun `stream abort preserves protocol details`() = runTest { @@ -68,7 +73,7 @@ class SmokeTest { @Test fun `connect fails fast and surfaces a MoqException`() = runTest { val ex = assertFailsWith { - Moq.connect("https://localhost:0/test", tlsVerify = false, reconnect = false) + Moq.connect("https://localhost:0/test", ClientConfig(tls = ClientTls(insecure = true), once = true)) } assertTrue( ex.isShutdown || ex is MoqException.Connect || ex is MoqException.Url, @@ -78,22 +83,23 @@ class SmokeTest { /** * The WebSocket fallback knobs reach the native client: a QUIC-only dial with - * no head start still fails fast, and a negative delay is refused up front - * rather than wrapping into an enormous one. + * no head start still fails fast, and an invalid value is refused up front + * as a config error rather than at the dial. */ @Test fun `connect accepts the websocket fallback knobs`() = runTest { assertFailsWith { Moq.connect( "https://localhost:0/test", - tlsVerify = false, - reconnect = false, - websocketEnabled = false, - websocketDelay = 0.milliseconds, + ClientConfig( + tls = ClientTls(insecure = true), + once = true, + websocket = WebSocketConfig(enabled = false, delayUs = 0uL), + ), ) } - assertFailsWith { - Moq.connect("https://localhost:0/test", websocketDelay = (-1).milliseconds) + assertFailsWith { + Moq.connect("https://localhost:0/test", ClientConfig(versions = listOf("moq-lite-99"))) } } @@ -118,8 +124,8 @@ class SmokeTest { framerate = 60.0, optimizeForLatency = true, ) - val snapshot: JsonSnapshotConfig = JsonSnapshotConfig(deltaRatio = 8u, compression = false) - val stream: JsonStreamConfig = JsonStreamConfig(compression = false) + val snapshot: SnapshotConfig = SnapshotConfig(deltaRatio = 8u, compression = false) + val stream: StreamConfig = StreamConfig(compression = false) val properties: VideoProperties = VideoProperties(rotation = 315.0) val backoff: Backoff = Backoff( initialUs = 500_000uL, @@ -127,6 +133,15 @@ class SmokeTest { maxUs = 10_000_000uL, timeoutUs = 0uL, ) + val client: ClientConfig = ClientConfig( + tls = ClientTls(insecure = true), + quic = QuicConfig(maxStreams = 4096uL), + websocket = WebSocketConfig(enabled = false), + backoff = backoff, + ) + val server: ServerConfig = ServerConfig(tls = ServerTls(generate = listOf("localhost"))) + assertEquals(4096uL, client.quic.maxStreams) + assertEquals(listOf("localhost"), server.tls.generate) val status: ConnectionStatus = ConnectionStatus.CONNECTED assertEquals(4_000_000uL, hint.bitrate) assertEquals(8u, snapshot.deltaRatio) @@ -164,7 +179,7 @@ class SmokeTest { @Test fun `broadcast updates shared video properties`() { BroadcastProducer().use { broadcast -> - broadcast.setVideoProperties(VideoProperties(rotation = 315.0)) + CatalogProducer(broadcast).setVideoProperties(VideoProperties(rotation = 315.0)) } } @@ -201,26 +216,29 @@ class SmokeTest { } } + @Test + fun `catalog handle closes with its broadcast`() { + val broadcast = BroadcastProducer() + val catalog = CatalogProducer(broadcast) + catalog.setSection("app", "{\"value\":42}") + broadcast.close() + assertFailsWith { catalog.removeSection("app") } + catalog.close() + } + /** A fetched media group streams its decoded frames and then completes. */ @Test fun `media group helper streams fetched frames`() = runTest { BroadcastProducer().use { broadcast -> - val media = broadcast.publishAudio( - AudioInit(format = AudioFormat.OPUS, data = opusHead()), - ) + val media = dev.moq.media.TrackProducer.audio(broadcast, Named(null), AudioInit(format = AudioFormat.OPUS, data = opusHead())) val consumer = broadcast.consume() - val (name, audio) = consumer.catalog().audio.entries.single() + val (name, audio) = catalog(consumer).audio.entries.single() media.writeFrame(Frame(payload = "opus frame".encodeToByteArray(), timestampUs = 5_000_000uL)) // Fetch while the track is still published: finishing the media producer // unpublishes it, and the fetch would then miss with NotFound. - val fetched: MediaGroupConsumer = consumer.fetchMediaGroup( - name, - 0uL, - audio.container, - FetchGroupOptions(priority = 3u), - ) + val fetched: ContainerGroupConsumer = ContainerGroupConsumer.fetch(consumer, ContainerGroupConfig(name = name, sequence = 0uL, container = audio.container, options = FetchGroupOptions(priority = 3u))) // Close the group so the fetched stream terminates instead of waiting for more. media.finish() @@ -278,11 +296,11 @@ class SmokeTest { @Test fun `typed json snapshot round-trips a serializable value`() = runTest { BroadcastProducer().use { broadcast -> - val config = JsonSnapshotConfig(deltaRatio = 0u, compression = false) - val producer = broadcast.publishJsonSnapshot("status", config) + val config = SnapshotConfig(deltaRatio = 0u, compression = false) + val producer = SnapshotProducer(broadcast, broadcast.publishTrack("status", null), config) producer.update(Status(state = "live")) - val consumer = broadcast.consume().subscribeJsonSnapshot("status", config) + val consumer = SnapshotConsumer(broadcast.consume().subscribeTrack("status", null), config) assertEquals(Status(state = "live"), consumer.valuesAs().first()) } } @@ -290,12 +308,13 @@ class SmokeTest { @Test fun `json producer demand follows subscribers`() = runTest { BroadcastProducer().use { broadcast -> - val config = JsonSnapshotConfig(deltaRatio = 0u, compression = false) - val demand: TrackDemand = broadcast.publishJsonSnapshot("status", config).demand() + val config = SnapshotConfig(deltaRatio = 0u, compression = false) + val producer = SnapshotProducer(broadcast, broadcast.publishTrack("status", null), config) + val demand: TrackDemand = producer.demand() assertEquals("status", demand.name()) assertEquals(false, demand.isUsed()) - val consumer = broadcast.consume().subscribeJsonSnapshot("status", config) + val consumer = SnapshotConsumer(broadcast.consume().subscribeTrack("status", null), config) demand.used() consumer.cancel() demand.unused() @@ -310,18 +329,18 @@ class SmokeTest { @Test fun `raw json string passes through unencoded`() = runTest { BroadcastProducer().use { broadcast -> - val config = JsonSnapshotConfig(deltaRatio = 0u, compression = false) - val producer = broadcast.publishJsonSnapshot("status", config) + val config = SnapshotConfig(deltaRatio = 0u, compression = false) + val producer = SnapshotProducer(broadcast, broadcast.publishTrack("status", null), config) producer.update("""{"state":"raw"}""") - val consumer = broadcast.consume().subscribeJsonSnapshot("status", config) + val consumer = SnapshotConsumer(broadcast.consume().subscribeTrack("status", null), config) assertEquals(Status(state = "raw"), consumer.valuesAs().first()) } } @Test fun `server listens, publishes, and streams requests`() = runTest { - Server.listen("127.0.0.1:0", tlsGenerate = listOf("localhost")).use { server -> + Server.listen(ServerConfig(bind = "127.0.0.1:0", tls = ServerTls(generate = listOf("localhost")))).use { server -> assertTrue(server.localAddr.startsWith("127.0.0.1:"), "bound: ${server.localAddr}") val fingerprints = server.certFingerprints() @@ -337,12 +356,12 @@ class SmokeTest { @Test fun `closing a server releases its port`() = runTest { - val first = Server.listen("127.0.0.1:0", tlsGenerate = listOf("localhost")) + val first = Server.listen(ServerConfig(bind = "127.0.0.1:0", tls = ServerTls(generate = listOf("localhost")))) val addr = first.localAddr first.close() // No retry: close() released the listening socket before returning. - Server.listen(addr, tlsGenerate = listOf("localhost")).use { rebound -> + Server.listen(ServerConfig(bind = addr, tls = ServerTls(generate = listOf("localhost")))).use { rebound -> assertEquals(addr, rebound.localAddr) } } @@ -444,38 +463,20 @@ class SmokeTest { } /** - * Configuration must apply or fail: a setter racing an in-flight connect - * throws [MoqException.Busy], and one after [Client.cancel] throws - * [MoqException.Cancelled]. Mirrors `test_client_setters_fail_after_cancel` - * in `py/moq-rs/tests/test_server.py`. + * Cancelling the client aborts a connect parked on a server that never + * accepts, and the connect reports it as [MoqException.Cancelled]. */ @Test - fun `client setters are busy during connect and cancelled after`() = runTest { - Server.listen("127.0.0.1:0", tlsGenerate = listOf("localhost")).use { server -> - val client = Client() - client.setTlsVerify(false) - client.setBind("127.0.0.1:0") + fun `client cancel aborts a pending connect`() = runTest { + Server.listen(ServerConfig(bind = "127.0.0.1:0", tls = ServerTls(generate = listOf("localhost")))).use { server -> // A reconnecting client would redial instead of failing the connect. - client.setReconnect(false) + val client = Client(ClientConfig(bind = "127.0.0.1:0", tls = ClientTls(insecure = true), once = true)) - // Nothing accepts the request, so connect parks holding the client lock. // runCatching, because a failed `async` would cancel the test scope // before `await` ever reported it. val connect = async { runCatching { client.connect("https://${server.localAddr}") } } - - // The lock is taken on the ffi runtime thread, so poll until it is. - val deadline = System.nanoTime() + CONFIG_RACE_TIMEOUT_NS - var busy: Throwable? = null - while (busy == null && System.nanoTime() < deadline) { - busy = runCatching { client.setTlsVerify(false) }.exceptionOrNull() - yield() - } - assertTrue(busy is MoqException.Busy, "expected Busy while connecting, got: $busy") - assertFailsWith { client.setBind("127.0.0.1:0") } - + yield() client.cancel() - assertFailsWith { client.setTlsVerify(true) } - assertFailsWith { client.setBind("127.0.0.1:0") } val connected = connect.await().exceptionOrNull() assertTrue(connected is MoqException.Cancelled, "expected a cancelled connect, got: $connected") @@ -502,7 +503,7 @@ class SmokeTest { } val consumer = origin.consume().requestBroadcast("video-decode-frame") - val catalog = consumer.subscribeCatalog().next()!! + val catalog = CatalogConsumer.subscribe(consumer).next()!! val rendition = catalog.video["camera"]!! val decoder = consumer.decodeVideo("camera", rendition, VideoDecoderOutput()) diff --git a/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/docs/Prelude.kt b/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/docs/Prelude.kt index 9f15448a620..d395387e257 100644 --- a/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/docs/Prelude.kt +++ b/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/docs/Prelude.kt @@ -3,6 +3,8 @@ // names a missing symbol or the wrong arity fails the check. Never executed. package dev.moq.docs +import dev.moq.media.* + val opusInit: ByteArray get() = TODO() val packet: ByteArray get() = TODO() val pts: ULong get() = TODO() diff --git a/py/AGENTS.md b/py/AGENTS.md index b3177f075f5..fabb85cc18a 100644 --- a/py/AGENTS.md +++ b/py/AGENTS.md @@ -7,7 +7,7 @@ The `/py` uv workspace. Extends the root `AGENTS.md` and `rs/moq-ffi/AGENTS.md`. # moq-rs -`moq/__init__.py` is the single public surface and defines `__all__`. Modules map to roles; keep names aligned with `rs/moq-net`. +`moq/__init__.py` is the public surface for the moq-net root and defines `__all__`; each layer above it is a public submodule (`moq.json`) with its own `__all__`. Modules map to roles; keep names aligned with the Rust crate they mirror. # Conventions diff --git a/py/moq-ffi/tests/test_smoke.py b/py/moq-ffi/tests/test_smoke.py index df2547f253c..1eb210a8f10 100644 --- a/py/moq-ffi/tests/test_smoke.py +++ b/py/moq-ffi/tests/test_smoke.py @@ -11,5 +11,5 @@ def test_exports_core_classes(): def test_client_constructs(): - client = moq_ffi.MoqClient() + client = moq_ffi.MoqClient(moq_ffi.MoqClientConfig()) assert client is not None diff --git a/py/moq-rs/README.md b/py/moq-rs/README.md index 9514890c3ab..44fed722ff6 100644 --- a/py/moq-rs/README.md +++ b/py/moq-rs/README.md @@ -33,13 +33,13 @@ async def main(): continue # AnnounceEventUpdate or AnnounceEventEnd # A route covers a prefix and carries no broadcast, so resolve the path. broadcast = await client.request_broadcast(event.announce.prefix) - catalog = await broadcast.catalog() + catalog = await moq.media.catalog(broadcast) for name, track in catalog.audio.items(): - frames = await broadcast.subscribe_media(name, track) + frames = await moq.media.ContainerConsumer.subscribe(broadcast, name, track.container) async with frames: async for frame in frames: - print(f"Got frame: {len(frame.payload)} bytes, ts={frame.timestamp_us}") + print(f"Got frame: {len(frame.payload)} bytes, ts={frame.timestamp}") asyncio.run(main()) @@ -49,6 +49,7 @@ asyncio.run(main()) ```python import asyncio +from datetime import timedelta import moq @@ -57,13 +58,15 @@ async def main(): broadcast = client.create_broadcast("my-stream") # Publish an Opus audio track (init bytes from your encoder) - audio = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_init_bytes) + audio = moq.media.TrackProducer.audio( + broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_init_bytes) + ) # Write frames # Audio has no keyframes, so `cut` is what gives it group boundaries. - audio.write_frame(payload, timestamp_us=0) + audio.write_frame(payload) audio.cut() - audio.write_frame(payload, timestamp_us=20000) + audio.write_frame(payload, timestamp=timedelta(milliseconds=20)) audio.cut() broadcast.announce() @@ -114,7 +117,7 @@ origin = moq.OriginProducer() client = moq.Client( "https://cdn.moq.dev/anon", publish=origin, - subscribe=origin, + consume=origin, ) ``` @@ -122,23 +125,23 @@ client = moq.Client( ### Connection -- **`connect(url, *, tls_verify=True, tls_roots=None, tls_system_roots=None, tls_fingerprints=None, tls_cert=None, tls_key=None, bind=None, max_streams=None, reconnect=True, backoff=None, publish=None, subscribe=None)`**. Shorthand for `Client(...)`; use as `async with moq.connect(url) as client:`. -- **`Client(url, *, tls_verify=True, tls_roots=None, tls_system_roots=None, tls_fingerprints=None, tls_cert=None, tls_key=None, bind=None, max_streams=None, reconnect=True, backoff=None, publish=None, subscribe=None)`**. Async context manager for connecting to a relay. +- **`connect(url, *, tls_verify=True, tls_roots=(), tls_system_roots=None, tls_fingerprints=(), tls_cert=None, tls_key=None, bind=None, versions=(), max_streams=None, websocket_enabled=None, websocket_delay=None, reconnect=True, backoff=None, publish=None, consume=None)`**. Shorthand for `Client(...)`; use as `async with moq.connect(url) as client:`. +- **`Client(url, *, ...)`**, taking the same arguments. Async context manager for connecting to a relay. - `tls_roots`. PEM root certificate file path(s) to trust instead of the system roots. - `tls_system_roots`. Whether to trust platform roots in addition to custom roots. - `tls_fingerprints`. Hex SHA-256 fingerprint(s) to pin the peer's certificate to, the native equivalent of `serverCertificateHashes`. Accepts the values a server reports via `cert_fingerprints()`, so you can trust a self-signed certificate without `tls_verify=False`. - `tls_cert`, `tls_key`. Paired PEM certificate chain and private key paths for mTLS. - `max_streams`. Raise the peer's inbound stream cap. + - `versions`. Protocol versions to offer, most preferred first (e.g. `"moq-lite-03"`); empty offers all. - `reconnect`, `backoff`. Redial with a `Backoff` when the transport drops; `reconnect=False` dials once. - `.session`. The established `Session` (or `None` before connecting / after exit). -- **`Server(bind="[::]:443", *, tls_cert=(), tls_key=(), tls_generate=(), publish=None, subscribe=None)`**. Async context manager + async iterator of incoming `Request`s. +- **`Server(bind="[::]:443", *, tls_cert=(), tls_key=(), tls_generate=(), versions=(), max_streams=None, publish=None, consume=None)`**. Async context manager + async iterator of incoming `Request`s. - `.local_addr`. The bound address (useful when binding to port `0`). - `.cert_fingerprints()`. SHA-256 fingerprints of the configured TLS certificates, for `serverCertificateHashes` browser cert pinning. - `.create_broadcast(path) → BroadcastProducer`. Create an unannounced broadcast, invisible to everyone; `announce()` makes it discoverable and reachable; `close()` ends it. - **`Request`**. An incoming session, yielded by `async for request in server`. - `.url`, `.path`, `.query`, `.transport`. The query-free path is uniform across transports; the root or missing path is `""`. The encoded query may contain credentials. - - `.set_publish(origin)`, `.set_consume(origin)`. Per-request overrides, captured at `accept()`. Raise if the request is already answered, cancelled, or currently accepting. - - `await .accept() → Session`. Complete the handshake (hold the result to keep the connection alive). + - `await .accept(*, publish=None, consume=None) → Session`. Origins are captured when accept starts; None inherits the server default and a supplied origin replaces it. Pass fresh origins for isolation, or the same origin on both sides to share it. A second response raises `AlreadyResponded`; calls after cancel raise `Cancelled`. Complete the handshake (hold the result to keep the connection alive). - `await .reject(code)`. Reject with an application error code; 401 and 403 map to unauthorized. - `.cancel()`. Cancel an in-flight `accept()`/`reject()` call. - **`Session`**. An established connection. Holding it keeps the connection alive; it is also an `async with` context manager that drains on a clean exit and cancels on an error. @@ -153,35 +156,49 @@ client = moq.Client( - **`BroadcastProducer()`**. Create a broadcast to publish tracks into. - `.dynamic() → BroadcastDynamic` - - `.publish_audio(format, init, *, label=None, track=None) → MediaProducer`. `init` is required: an OpusHead or AudioSpecificConfig resolves the whole rendition. `track` names the track; otherwise a unique name is derived from the format. - - `.publish_video(format, init=b"", *, label=None, hint=None, track=None) → MediaProducer`. `init` may be empty for a format that resolves in band; a `VideoHint` pins catalog fields the stream can't reveal (bitrate) or publishes the catalog before the first keyframe. `track` names the track as in `publish_audio`. - `.encode_video(input, output, *, bandwidth=None) → VideoProducer`. Encode raw `VideoFrame`s inside the binding; `.write(frame)` each one. - `.encode_audio(name, input, output, *, bandwidth=None) → AudioProducer`. Encode raw PCM `AudioFrame`s; the codec is `output.codec`, e.g. `AudioCodec.opus()` or `AudioCodec.aac()`, with `output.frame_duration_us` setting the Opus frame length (0 takes the codec's own frame, which AAC needs). - `.close()` ends the broadcast for good; a second call is a no-op. - **`BroadcastDynamic`**. Async source of tracks requested by subscribers. - `await .requested_track() → TrackRequest`. Call `.accept()` on it for a `TrackProducer`, or `.abort(code)` to reject. - Async iterator yielding `TrackRequest` -- **`MediaProducer`**. Write frames to a track. - - `.write_frame(payload, timestamp_us=0)` - - `.cut()` / `.seek(sequence)` draw a group boundary (audio has none of its own) - - `.finish()` - **`TrackProducer` / `GroupProducer`**. Write raw payloads with no codec parsing. - - `.write_frame(payload, timestamp_us=0)` writes a payload with a presentation timestamp in microseconds. + - `.write_frame(payload, timestamp=timedelta(0))` writes a payload with its presentation timestamp. - `.create_group(sequence)` creates a sparse or replayed group at an explicit sequence. - `.finish()` ends at the live edge; the handle remains so `.abort(error_code)` can still run. - `.finish_at(final_sequence)` declares the first group that will never be produced while leaving lower groups writable. - `.abort(error_code)` terminates the track or group with an application error. - - `.append_datagram(payload, timestamp_us=0) -> sequence` (`TrackProducer`) sends a best-effort datagram. Payloads are capped at 1200 bytes and there is no stream fallback. + - `.append_datagram(payload, timestamp=timedelta(0)) -> sequence` (`TrackProducer`) sends a best-effort datagram. Payloads are capped at 1200 bytes and there is no stream fallback. + +### Media + +`moq.media` owns catalogs, encoded-media importers, and container consumers. + +- `media.TrackProducer.audio(broadcast, AudioInit(...), *, target=Named())` and + `.video(broadcast, VideoInit(...), *, target=Named())` import complete encoded frames. + A `Named(name)` target chooses a name; `Named()` derives a unique name from the format. + `Requested(request)` takes over a pending subscriber request, whose name is already fixed. +- `media.TrackStreamProducer.video(...)` infers video frame boundaries from a byte stream. +- `media.ContainerProducer(broadcast, ContainerInit(...))` demuxes complete container chunks; + `media.ContainerStreamProducer(broadcast, format)` recovers framing from a byte stream. +- `media.CatalogProducer(broadcast)` owns `set_video_properties`, `set_section`, and + `remove_section`. It holds the broadcast weakly; writes fail with `Error.Closed` after closing + or releasing the broadcast. +- `await media.CatalogConsumer.subscribe(broadcast)` streams catalog snapshots; + `await media.catalog(broadcast)` reads one snapshot and releases its subscription. +- `await media.ContainerConsumer.subscribe(broadcast, name, container, *, subscription=None)` + decodes live media. Pass the catalog rendition's `container`. +- `await media.ContainerGroupConsumer.fetch(broadcast, name, sequence, container, *, options=None)` + fetches and decodes exactly one group. + +`media.TrackProducer.write_frame(payload, timestamp=timedelta(0))` and `.flush(timestamp)` use +`timedelta`; returned `media.MediaFrame.timestamp` does too. `demand()` owns the track name and +subscriber waits. `.cut()` / `.seek(sequence)` draw group boundaries; `.finish()` ends the import. ### Subscribing - **`BroadcastConsumer`**. Subscribe to tracks within a broadcast. - - `await .subscribe_catalog() → CatalogConsumer` - `await .subscribe_track(name, subscription=None) → TrackConsumer` - - `await .subscribe_media(name, track, subscription=None) → MediaConsumer`. `track` is the catalog record (e.g. `catalog.video[name]`); its container tells the decoder how to parse the bitstream. - - `await .catalog() → Catalog` (convenience) -- **`CatalogConsumer`**. Async iterator of `Catalog`. -- **`MediaConsumer`**. Async iterator of `MediaFrame`. - **`TrackConsumer`**. Async iterator of raw groups, in sequence order. - `await .next_group() → GroupConsumer | None`. Sequence order; what the default iteration yields. - `await .recv_group() → GroupConsumer | None`. Arrival order, which may be out of sequence. Prefer it when latency matters more than order. @@ -193,7 +210,7 @@ client = moq.Client( - **`GroupConsumer`**. Async iterator of timestamped `Frame`s. - `.read_frame() -> Frame | None` returns a timestamped raw frame. -Every handle whose cleanup is `cancel()` is an async context manager, so exiting `async with` releases it: the consumers (`CatalogConsumer`, `MediaConsumer`, `MediaGroupConsumer`, `TrackConsumer`, `AudioConsumer`, `GroupConsumer`, `JsonSnapshotConsumer`, `JsonStreamConsumer`, `AnnounceConsumer`, `AnnouncedBroadcast`) and the dynamic sources (`OriginDynamic`, `BroadcastDynamic`, `TrackDynamic`). +Every handle whose cleanup is `cancel()` is an async context manager, so exiting `async with` releases it: the consumers (`media.CatalogConsumer`, `media.ContainerConsumer`, `media.ContainerGroupConsumer`, `TrackConsumer`, `AudioConsumer`, `GroupConsumer`, `json.SnapshotConsumer`, `json.StreamConsumer`, `AnnounceConsumer`, `AnnouncedBroadcast`) and the dynamic sources (`OriginDynamic`, `BroadcastDynamic`, `TrackDynamic`). ### Origin (advanced) @@ -211,16 +228,16 @@ Every handle whose cleanup is `cancel()` is an async context manager, so exiting ### Types -- **`Catalog`**. `.audio: dict[str, Audio]`, `.video: dict[str, Video]`, `.display`, `.rotation`, `.flip`. -- **`Frame`**. `.payload: bytes`, `.timestamp_us: int`. The unit of every write and every raw read. -- **`MediaFrame`**. `.payload: bytes`, `.timestamp_us: int`, `.keyframe: bool`. Returned by media subscriptions. `keyframe` marks a group start or video keyframe; for audio it is true only at a group start. -- **`Datagram`**. `.sequence: int`, `.timestamp_us: int`, `.payload: bytes`. Delivered only on datagram-capable transports with lite-05 or newer moq-lite, or moq-transport. -- **`Audio`**. `.codec`, `.sample_rate`, `.channel_count`, `.bitrate`, `.enabled`, `.description`. -- **`Video`**. `.codec`, `.coded: Dimensions`, `.display_aspect`, `.bitrate`, `.enabled`, `.framerate`, `.description`. A false `.enabled` means no frames are coming, so don't select the rendition. +- **`media.Catalog`**. `.audio: dict[str, Audio]`, `.video: dict[str, Video]`, `.display`, `.rotation`, `.flip`. +- **`Frame`**. `.payload: bytes`, `.timestamp: timedelta | None`. The unit of every write and every raw read; `None` only on a frame read from an untimed track. +- **`media.MediaFrame`**. `.payload: bytes`, `.timestamp: timedelta`, `.keyframe: bool`. Returned by media subscriptions. `keyframe` marks a group start or video keyframe; for audio it is true only at a group start. +- **`Datagram`**. `.sequence: int`, `.timestamp: timedelta | None`, `.payload: bytes`. Delivered only on datagram-capable transports with lite-05 or newer moq-lite, or moq-transport. +- **`media.Audio`**. `.codec`, `.sample_rate`, `.channel_count`, `.bitrate`, `.enabled`, `.description`. +- **`media.Video`**. `.codec`, `.coded: Dimensions`, `.display_aspect`, `.bitrate`, `.enabled`, `.framerate`, `.description`. A false `.enabled` means no frames are coming, so don't select the rendition. - **`Subscription`**. Subscriber delivery preferences: priority, staleness, and optional group range. - **`TrackInfo`**. Publisher track properties: priority, cache window, and timescale. -- **`Dimensions`**. `.width: int`, `.height: int`. -- **`Container`**. The catalog container enum, carried on each `Video`/`Audio` record. +- **`media.Dimensions`**. `.width: int`, `.height: int`. +- **`media.Container`**. The catalog container enum, carried on each `Video`/`Audio` record. ### Logging and errors diff --git a/py/moq-rs/docs/index.md b/py/moq-rs/docs/index.md index 14fafb62915..89170a1178c 100644 --- a/py/moq-rs/docs/index.md +++ b/py/moq-rs/docs/index.md @@ -19,7 +19,7 @@ async def main(): async for event in client.announced(): if isinstance(event, moq.AnnounceEventStart): broadcast = await client.request_broadcast(event.announce.prefix) - print(await broadcast.catalog()) + print(await moq.media.catalog(broadcast)) asyncio.run(main()) @@ -60,12 +60,8 @@ asyncio.run(main()) TrackRequest GroupProducer GroupRequest - MediaProducer - MediaStreamProducer AudioProducer VideoProducer - JsonSnapshotProducer - JsonStreamProducer ``` ## Subscribing @@ -80,11 +76,51 @@ asyncio.run(main()) BroadcastConsumer TrackConsumer GroupConsumer - MediaConsumer AudioConsumer +``` + +## Media + +`moq.media` owns catalogs, encoded-media importers, and container consumers. +Every importer takes a broadcast; single-track imports choose a `Named` or `Requested` target. + +```{eval-rst} +.. currentmodule:: moq.media + +.. autosummary:: + :toctree: api + :nosignatures: + + CatalogProducer CatalogConsumer - JsonSnapshotConsumer - JsonStreamConsumer + TrackProducer + TrackStreamProducer + ContainerProducer + ContainerStreamProducer + ContainerConsumer + ContainerGroupConsumer + Named + Requested + MediaFrame + catalog +``` + +## JSON tracks + +`moq.json` mirrors the `moq-json` crate: each type wraps a track from the +broadcast, and producers advertise it in the catalog. + +```{eval-rst} +.. currentmodule:: moq.json + +.. autosummary:: + :toctree: api + :nosignatures: + + SnapshotProducer + StreamProducer + SnapshotConsumer + StreamConsumer ``` ## Origin and announcements @@ -111,7 +147,7 @@ asyncio.run(main()) ## Data types These records, enums, and objects are re-exported from the native `moq_ffi` bindings; the -wrapper surfaces them under `moq` unchanged. Their fields are defined on the +wrapper groups them under `moq` and `moq.media`, with owned duration records at the boundary. Their fields are defined on the Rust side ([`moq-ffi`](https://crates.io/crates/moq-ffi)). ```{eval-rst} @@ -121,25 +157,25 @@ Rust side ([`moq-ffi`](https://crates.io/crates/moq-ffi)). :toctree: api :nosignatures: - Catalog - Container + media.Catalog + media.Container Frame - MediaFrame + media.MediaFrame Datagram - Video - VideoHint - VideoProperties + media.Video + media.VideoHint + media.VideoProperties VideoFrame VideoCodec VideoPixelFormat VideoEncoderInput VideoEncoderOutput VideoEncoderKind - Dimensions - Audio + media.Dimensions + media.Audio AudioFrame AudioCodec - AudioFormat + media.AudioFormat AudioDecoderOutput AudioEncoderInput AudioEncoderOutput diff --git a/py/moq-rs/examples/clock.py b/py/moq-rs/examples/clock.py index c3f73ac74b7..6d53edb906b 100644 --- a/py/moq-rs/examples/clock.py +++ b/py/moq-rs/examples/clock.py @@ -10,7 +10,7 @@ import argparse import asyncio -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone import moq @@ -24,14 +24,14 @@ async def publish(url: str, broadcast_name: str, track_name: str, tls_verify: bo while True: now = datetime.now(timezone.utc).replace(microsecond=0) - timestamp_us = int(now.timestamp()) * 1_000_000 + timestamp = timedelta(seconds=int(now.timestamp())) group = track.append_group() - group.write_frame(now.strftime("%Y-%m-%d %H:%M:").encode(), timestamp_us) + group.write_frame(now.strftime("%Y-%m-%d %H:%M:").encode(), timestamp) current_minute = now.minute while now.minute == current_minute: - timestamp_us = int(now.timestamp()) * 1_000_000 - group.write_frame(now.strftime("%S").encode(), timestamp_us) + timestamp = timedelta(seconds=int(now.timestamp())) + group.write_frame(now.strftime("%S").encode(), timestamp) await asyncio.sleep(1 - datetime.now(timezone.utc).microsecond / 1_000_000) now = datetime.now(timezone.utc).replace(microsecond=0) diff --git a/py/moq-rs/examples/serve_clock.py b/py/moq-rs/examples/serve_clock.py index 5375aa7057d..3fe7c8f5213 100644 --- a/py/moq-rs/examples/serve_clock.py +++ b/py/moq-rs/examples/serve_clock.py @@ -13,7 +13,7 @@ import argparse import asyncio -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone import moq @@ -31,14 +31,14 @@ async def run(bind: str, broadcast_name: str, track_name: str, host: str) -> Non try: while True: now = datetime.now(timezone.utc).replace(microsecond=0) - timestamp_us = int(now.timestamp()) * 1_000_000 + timestamp = timedelta(seconds=int(now.timestamp())) group = track.append_group() - group.write_frame(now.strftime("%Y-%m-%d %H:%M:").encode(), timestamp_us) + group.write_frame(now.strftime("%Y-%m-%d %H:%M:").encode(), timestamp) current_minute = now.minute while now.minute == current_minute: - timestamp_us = int(now.timestamp()) * 1_000_000 - group.write_frame(now.strftime("%S").encode(), timestamp_us) + timestamp = timedelta(seconds=int(now.timestamp())) + group.write_frame(now.strftime("%S").encode(), timestamp) await asyncio.sleep(1 - datetime.now(timezone.utc).microsecond / 1_000_000) now = datetime.now(timezone.utc).replace(microsecond=0) diff --git a/py/moq-rs/moq/__init__.py b/py/moq-rs/moq/__init__.py index 35eb0dfd09f..4b2519599b5 100644 --- a/py/moq-rs/moq/__init__.py +++ b/py/moq-rs/moq/__init__.py @@ -5,6 +5,8 @@ from moq_ffi import MoqError as Error +# A submodule, not in __all__: a star import would shadow the stdlib `json`. +from . import json as json from .client import Client, connect from .errors import is_auth, is_shutdown, protocol_error from .log import log_level @@ -25,14 +27,8 @@ AudioProducer, BroadcastDynamic, BroadcastProducer, - ContainerProducer, - ContainerStreamProducer, GroupProducer, GroupRequest, - JsonSnapshotProducer, - JsonStreamProducer, - MediaProducer, - MediaStreamProducer, TrackDemand, TrackDynamic, TrackProducer, @@ -44,53 +40,37 @@ from .subscribe import ( AudioConsumer, BroadcastConsumer, - CatalogConsumer, GroupConsumer, - JsonSnapshotConsumer, - JsonStreamConsumer, - MediaConsumer, - MediaGroupConsumer, TrackConsumer, VideoConsumer, ) from .types import ( - Audio, AudioCodec, AudioDecoderOutput, AudioEncoderInput, AudioEncoderOutput, - AudioFormat, AudioFrame, AudioSampleFormat, Backoff, - Catalog, ConnectionStats, ConnectionStatus, - Container, - ContainerFormat, Datagram, - Dimensions, ErrorScope, FetchGroupOptions, Frame, - MediaFrame, ProtocolError, ProtocolKind, Route, Subscription, TrackInfo, - Video, VideoCodec, VideoDecodedFrame, VideoDecoderOutput, VideoEncoderInput, VideoEncoderKind, VideoEncoderOutput, - VideoFormat, VideoFrame, - VideoHint, VideoPixelFormat, - VideoProperties, ) __all__ = [ @@ -101,18 +81,12 @@ "AnnounceEventEnd", "AnnounceEventStart", "AnnounceEventUpdate", - "Audio", "AudioCodec", "AudioConsumer", "AudioDecoderOutput", "AudioEncoderInput", "AudioEncoderOutput", - "AudioFormat", "AudioSampleFormat", - "VideoFormat", - "ContainerFormat", - "ContainerProducer", - "ContainerStreamProducer", "AudioFrame", "AudioProducer", "Backoff", @@ -121,32 +95,19 @@ "BroadcastDynamic", "BroadcastProducer", "BroadcastRequest", - "Catalog", - "CatalogConsumer", "Client", "ConnectionStats", "ConnectionStatus", - "Container", "Datagram", - "Dimensions", "Error", "ErrorScope", "Frame", "ProtocolError", "ProtocolKind", - "MediaFrame", "FetchGroupOptions", "GroupConsumer", "GroupRequest", "GroupProducer", - "JsonSnapshotConsumer", - "JsonSnapshotProducer", - "JsonStreamConsumer", - "JsonStreamProducer", - "MediaConsumer", - "MediaGroupConsumer", - "MediaProducer", - "MediaStreamProducer", "OriginConsumer", "OriginDynamic", "OriginProducer", @@ -163,7 +124,6 @@ "TrackProducer", "TrackRequest", "Transport", - "Video", "VideoCodec", "VideoEncoderInput", "VideoEncoderKind", @@ -172,13 +132,13 @@ "VideoConsumer", "VideoDecodedFrame", "VideoDecoderOutput", - "VideoHint", "VideoPixelFormat", "VideoProducer", - "VideoProperties", "connect", "is_auth", "is_shutdown", "log_level", "protocol_error", ] + +from . import media as media diff --git a/py/moq-rs/moq/_records.py b/py/moq-rs/moq/_records.py new file mode 100644 index 00000000000..adc2414b94b --- /dev/null +++ b/py/moq-rs/moq/_records.py @@ -0,0 +1,201 @@ +"""Root records that carry durations, owned here so they read as ``timedelta``. + +moq-ffi spells every duration as integer microseconds, because not every target +language has a duration type. Python does, so these mirror the generated records +with ``timedelta`` fields and convert at the boundary. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +from datetime import timedelta + +from moq_ffi import ( + MoqBackoff, + MoqConnectionStats, + MoqDatagram, + MoqFrame, + MoqSubscription, + MoqTrackInfo, +) + +_MICROSECOND = timedelta(microseconds=1) + + +def _to_us(value: timedelta, name: str) -> int: + if value < timedelta(0): + raise ValueError(f"{name} must not be negative: {value}") + return value // _MICROSECOND + + +def _opt_us(value: timedelta | None, name: str) -> int | None: + return None if value is None else _to_us(value, name) + + +def _from_us(us: int) -> timedelta: + return timedelta(microseconds=us) + + +def _strs(value: Sequence[str], name: str) -> list[str]: + # A str is itself a Sequence[str], so list() would split it into characters. + if isinstance(value, str): + raise TypeError(f"{name} takes a sequence of strings, not a str: {value!r}") + return list(value) + + +def _opt_from_us(us: int | None) -> timedelta | None: + return None if us is None else _from_us(us) + + +@dataclass(frozen=True) +class Frame: + """A raw track frame: a payload and its presentation timestamp. + + ``timestamp`` is ``None`` on a frame read from an untimed track. A raw track + published here is timed, so writing one needs it. + """ + + payload: bytes + timestamp: timedelta | None = timedelta(0) + + def _ffi(self) -> MoqFrame: + return MoqFrame(payload=self.payload, timestamp_us=_opt_us(self.timestamp, "timestamp")) + + @staticmethod + def _from_ffi(frame: MoqFrame) -> Frame: + return Frame(payload=frame.payload, timestamp=_opt_from_us(frame.timestamp_us)) + + +@dataclass(frozen=True) +class Datagram: + """A best-effort track datagram as received: sequence number, timestamp, and payload. + + ``timestamp`` is ``None`` on a datagram read from an untimed track. + """ + + sequence: int + timestamp: timedelta | None + payload: bytes + + @staticmethod + def _from_ffi(datagram: MoqDatagram) -> Datagram: + return Datagram( + sequence=datagram.sequence, + timestamp=_opt_from_us(datagram.timestamp_us), + payload=datagram.payload, + ) + + +@dataclass(frozen=True) +class Subscription: + """Subscriber-side delivery preferences, mirroring moq-net's ``track::Subscription``. + + ``max_delay`` is how far a non-latest group may fall behind before it is skipped; + zero skips at once. ``group_start`` is a floor and ``group_end`` an exclusive end, ``None`` for + no bound. + """ + + priority: int = 0 + max_delay: timedelta = timedelta(0) + group_start: int | None = None + group_end: int | None = None + + def _ffi(self) -> MoqSubscription: + return MoqSubscription( + priority=self.priority, + max_delay_us=_to_us(self.max_delay, "max_delay"), + group_start=self.group_start, + group_end=self.group_end, + ) + + +def _subscription(subscription: Subscription | None) -> MoqSubscription | None: + return None if subscription is None else subscription._ffi() + + +@dataclass(frozen=True) +class TrackInfo: + """Publisher-side track properties, mirroring moq-net's ``track::Info``. + + ``priority`` defaults to 127, the middle of the range. ``max_age`` is how long the + publisher caches a non-latest group, ``None`` for no limit. ``timescale`` is ticks + per second: ``None`` uses microseconds when publishing, and means the source + declared no timeline on a received track. + """ + + priority: int = 127 + max_age: timedelta | None = None + timescale: int | None = None + + def _ffi(self) -> MoqTrackInfo: + return MoqTrackInfo( + priority=self.priority, + max_age_us=_opt_us(self.max_age, "max_age"), + timescale=self.timescale, + ) + + @staticmethod + def _from_ffi(info: MoqTrackInfo) -> TrackInfo: + return TrackInfo( + priority=info.priority, + max_age=None if info.max_age_us is None else _from_us(info.max_age_us), + timescale=info.timescale, + ) + + +def _track_info(info: TrackInfo | None) -> MoqTrackInfo | None: + return None if info is None else info._ffi() + + +@dataclass(frozen=True) +class Backoff: + """Retry pacing for the automatic reconnect. + + The delay starts at ``initial``, multiplies by ``multiplier`` after each failed + attempt, and caps at ``max``. After ``timeout`` of consecutive failures the + connection gives up; ``timedelta(0)`` retries forever. ``None`` keeps each + default: 1s, x2, 5s, and a 10s window. + """ + + initial: timedelta | None = None + multiplier: int | None = None + max: timedelta | None = None + timeout: timedelta | None = None + + def _ffi(self) -> MoqBackoff: + return MoqBackoff( + initial_us=_opt_us(self.initial, "initial"), + multiplier=self.multiplier, + max_us=_opt_us(self.max, "max"), + timeout_us=_opt_us(self.timeout, "timeout"), + ) + + +@dataclass(frozen=True) +class ConnectionStats: + """Transport metrics for a session; each field is ``None`` when unreported.""" + + rtt: timedelta | None + estimated_send_rate_bps: int | None + estimated_recv_rate_bps: int | None + bytes_sent: int | None + bytes_received: int | None + bytes_lost: int | None + packets_sent: int | None + packets_received: int | None + packets_lost: int | None + + @staticmethod + def _from_ffi(stats: MoqConnectionStats) -> ConnectionStats: + return ConnectionStats( + rtt=None if stats.rtt_us is None else _from_us(stats.rtt_us), + estimated_send_rate_bps=stats.estimated_send_rate_bps, + estimated_recv_rate_bps=stats.estimated_recv_rate_bps, + bytes_sent=stats.bytes_sent, + bytes_received=stats.bytes_received, + bytes_lost=stats.bytes_lost, + packets_sent=stats.packets_sent, + packets_received=stats.packets_received, + packets_lost=stats.packets_lost, + ) diff --git a/py/moq-rs/moq/client.py b/py/moq-rs/moq/client.py index 8a937ac4377..3636aa26460 100644 --- a/py/moq-rs/moq/client.py +++ b/py/moq-rs/moq/client.py @@ -2,15 +2,22 @@ from __future__ import annotations +from collections.abc import Sequence from datetime import timedelta -from moq_ffi import MoqClient +from moq_ffi import ( + MoqClient, + MoqClientConfig, + MoqClientTls, + MoqQuicConfig, + MoqWebSocketConfig, +) +from ._records import Backoff, _opt_us, _strs from .origin import AnnounceConsumer, AnnouncedBroadcast, OriginConsumer, OriginProducer from .publish import BroadcastProducer from .session import Session from .subscribe import BroadcastConsumer -from .types import Backoff class Client: @@ -26,7 +33,7 @@ class Client: In advanced mode, provide your own origin for full control: origin = OriginProducer() - client = Client("https://relay.example.com", publish=origin, subscribe=origin) + client = Client("https://relay.example.com", publish=origin, consume=origin) The WebSocket fallback races QUIC for ``http(s)`` URLs after a 200 ms head start. Pass ``websocket_enabled=False`` against a QUIC-only relay, or a ``websocket_delay`` @@ -42,6 +49,9 @@ class Client: broadcasts consumed through it ride out the gap. Pass ``reconnect=False`` for a one-shot dial, or a :class:`Backoff` to tune the retry pacing; watch :meth:`Session.status` for the connect/disconnect transitions. + + ``versions`` restricts the protocol versions offered, most preferred first, spelled + like ``"moq-lite-03"``; empty offers every supported version. """ def __init__( @@ -49,40 +59,45 @@ def __init__( url: str, *, tls_verify: bool = True, - tls_roots: list[str] | None = None, + tls_roots: Sequence[str] = (), tls_system_roots: bool | None = None, - tls_fingerprints: list[str] | None = None, + tls_fingerprints: Sequence[str] = (), tls_cert: str | None = None, tls_key: str | None = None, bind: str | None = None, + versions: Sequence[str] = (), max_streams: int | None = None, websocket_enabled: bool | None = None, websocket_delay: timedelta | None = None, reconnect: bool = True, backoff: Backoff | None = None, publish: OriginProducer | None = None, - subscribe: OriginProducer | None = None, + consume: OriginProducer | None = None, ) -> None: self._url = url - self._tls_verify = tls_verify - self._tls_roots = tls_roots - self._tls_system_roots = tls_system_roots - self._tls_fingerprints = tls_fingerprints - self._tls_cert = tls_cert - self._tls_key = tls_key - self._bind = bind - self._max_streams = max_streams - self._websocket_enabled = websocket_enabled - if websocket_delay is not None and websocket_delay < timedelta(0): - raise ValueError(f"websocket_delay must not be negative: {websocket_delay}") - self._websocket_delay = websocket_delay - self._reconnect = reconnect - self._backoff = backoff - # With neither side given, moq-ffi wires one shared origin to both, so a broadcast # announced here is discoverable via announced() (loopback). - self._publish_origin = publish - self._consume_origin = subscribe + self._config = MoqClientConfig( + bind=bind, + versions=_strs(versions, "versions"), + tls=MoqClientTls( + insecure=not tls_verify, + roots=_strs(tls_roots, "tls_roots"), + system_roots=tls_system_roots, + fingerprints=_strs(tls_fingerprints, "tls_fingerprints"), + cert=tls_cert, + key=tls_key, + ), + quic=MoqQuicConfig(max_streams=max_streams), + websocket=MoqWebSocketConfig( + enabled=websocket_enabled, + delay_us=_opt_us(websocket_delay, "websocket_delay"), + ), + once=not reconnect, + backoff=(backoff or Backoff())._ffi(), + publish=None if publish is None else publish._inner, + consume=None if consume is None else consume._inner, + ) self._publisher: OriginProducer | None = None self._consumer: OriginConsumer | None = None @@ -90,39 +105,13 @@ def __init__( self._session: Session | None = None async def __aenter__(self): - self._inner = MoqClient() - - if not self._tls_verify: - 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: - self._inner.set_tls_system_roots(self._tls_system_roots) - if self._tls_fingerprints: - self._inner.set_tls_fingerprints(self._tls_fingerprints) - if self._tls_cert is not None: - self._inner.set_tls_cert(self._tls_cert) - if self._tls_key is not None: - self._inner.set_tls_key(self._tls_key) - if self._bind is not None: - self._inner.set_bind(self._bind) - if self._max_streams is not None: - self._inner.set_quic_max_streams(self._max_streams) - if self._websocket_enabled is not None: - self._inner.set_websocket_enabled(self._websocket_enabled) - if self._websocket_delay is not None: - self._inner.set_websocket_delay(self._websocket_delay // timedelta(microseconds=1)) - if not self._reconnect: - self._inner.set_reconnect(False) - if self._backoff is not None: - self._inner.set_backoff(self._backoff) - - if self._publish_origin is not None: - self._inner.set_publish(self._publish_origin._inner) - if self._consume_origin is not None: - self._inner.set_consume(self._consume_origin._inner) - - self._session = Session(await self._inner.connect(self._url)) + self._inner = MoqClient(self._config) + try: + self._session = Session(await self._inner.connect(self._url)) + except BaseException: + self._inner.cancel() + self._inner = None + raise # The session always exposes both sides, wired from the origins above or # auto-created, so publishing and discovery always have somewhere to go. @@ -195,19 +184,20 @@ def connect( url: str, *, tls_verify: bool = True, - tls_roots: list[str] | None = None, + tls_roots: Sequence[str] = (), tls_system_roots: bool | None = None, - tls_fingerprints: list[str] | None = None, + tls_fingerprints: Sequence[str] = (), tls_cert: str | None = None, tls_key: str | None = None, bind: str | None = None, + versions: Sequence[str] = (), max_streams: int | None = None, websocket_enabled: bool | None = None, websocket_delay: timedelta | None = None, reconnect: bool = True, backoff: Backoff | None = None, publish: OriginProducer | None = None, - subscribe: OriginProducer | None = None, + consume: OriginProducer | None = None, ) -> Client: """Shorthand for constructing a :class:`Client`. @@ -225,11 +215,12 @@ def connect( tls_cert=tls_cert, tls_key=tls_key, bind=bind, + versions=versions, max_streams=max_streams, websocket_enabled=websocket_enabled, websocket_delay=websocket_delay, reconnect=reconnect, backoff=backoff, publish=publish, - subscribe=subscribe, + consume=consume, ) diff --git a/py/moq-rs/moq/json.py b/py/moq-rs/moq/json.py new file mode 100644 index 00000000000..274914e8ea7 --- /dev/null +++ b/py/moq-rs/moq/json.py @@ -0,0 +1,154 @@ +"""JSON tracks, mirroring the ``moq-json`` crate. + +Each type wraps a track. A producer takes over a :class:`~moq.TrackProducer` and advertises it +in its broadcast's catalog (``json.tracks.``) for as long as it lives; a consumer takes over +a :class:`~moq.TrackConsumer` that has not read a group yet. Either track can come from a request +as well as by name. Values are any JSON-serializable Python object. +""" + +from __future__ import annotations + +import json +from typing import Any + +from moq_ffi import ( + MoqJsonSnapshotConfig, + MoqJsonSnapshotConsumer, + MoqJsonSnapshotProducer, + MoqJsonStreamConfig, + MoqJsonStreamConsumer, + MoqJsonStreamProducer, +) + +from .publish import BroadcastProducer, TrackDemand, TrackProducer +from .subscribe import TrackConsumer + +__all__ = ["SnapshotConsumer", "SnapshotProducer", "StreamConsumer", "StreamProducer"] + + +class SnapshotProducer: + """Publish a JSON value that consumers see as a single latest state (lossy). + + Each :meth:`update` supersedes the last; a late joiner only sees the newest value, encoded as + snapshots and merge-patch deltas automatically. ``delta_ratio`` controls how aggressively + deltas replace full snapshots (0 disables them); ``None`` uses the binding's default. Set + ``compression`` to DEFLATE-compress each group; the consumer must pass the same flag. A name + the catalog already carries is refused. + """ + + def __init__( + self, + broadcast: BroadcastProducer, + track: TrackProducer, + *, + delta_ratio: int | None = None, + compression: bool = False, + ) -> None: + # Let the record supply delta_ratio's default rather than restating it here. + config = ( + MoqJsonSnapshotConfig(compression=compression) + if delta_ratio is None + else MoqJsonSnapshotConfig(delta_ratio=delta_ratio, compression=compression) + ) + self._inner = MoqJsonSnapshotProducer(broadcast._inner, track._inner, config) + + def demand(self) -> TrackDemand: + """A watch-only handle to whether this track has subscribers.""" + return TrackDemand(self._inner.demand()) + + def update(self, value: Any) -> None: + """Publish a new value. A no-op if unchanged from the previous update.""" + self._inner.update(json.dumps(value)) + + def finish(self) -> None: + """Finish the track, closing any open group.""" + self._inner.finish() + + +class StreamProducer: + """Publish an ordered log of JSON records (lossless). + + Every :meth:`append` is preserved and delivered in order. Set ``compression`` to + DEFLATE-compress the group; the consumer must pass the same flag. A name the catalog already + carries is refused. + """ + + def __init__(self, broadcast: BroadcastProducer, track: TrackProducer, *, compression: bool = False) -> None: + config = MoqJsonStreamConfig(compression=compression) + self._inner = MoqJsonStreamProducer(broadcast._inner, track._inner, config) + + def demand(self) -> TrackDemand: + """A watch-only handle to whether this track has subscribers.""" + return TrackDemand(self._inner.demand()) + + def append(self, value: Any) -> None: + """Append one record to the log.""" + self._inner.append(json.dumps(value)) + + def finish(self) -> None: + """Finish the track, closing the group.""" + self._inner.finish() + + +class SnapshotConsumer: + """Async iterator over a JSON snapshot track, yielding the latest value (lossy). + + Each item is a parsed Python object. A consumer that has fallen behind collapses the backlog + and yields only the latest value. Pass the same ``compression`` the producer used. Usable as + an async context manager that cancels on exit. + """ + + def __init__(self, track: TrackConsumer, *, compression: bool = False) -> None: + # delta_ratio is producer-only, so leave it at its default here. + config = MoqJsonSnapshotConfig(compression=compression) + self._inner = MoqJsonSnapshotConsumer(track._inner, config) + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc) -> None: + self.cancel() + + def __aiter__(self): + return self + + async def __anext__(self) -> Any: + value = await self._inner.next() + if value is None: + raise StopAsyncIteration + return json.loads(value) + + def cancel(self) -> None: + """Cancel all current and future next() calls.""" + self._inner.cancel() + + +class StreamConsumer: + """Async iterator over a JSON stream track, yielding every record in order (lossless). + + Each item is a parsed Python object. Pass the same ``compression`` the producer used. Usable + as an async context manager that cancels on exit. + """ + + def __init__(self, track: TrackConsumer, *, compression: bool = False) -> None: + config = MoqJsonStreamConfig(compression=compression) + self._inner = MoqJsonStreamConsumer(track._inner, config) + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc) -> None: + self.cancel() + + def __aiter__(self): + return self + + async def __anext__(self) -> Any: + value = await self._inner.next() + if value is None: + raise StopAsyncIteration + return json.loads(value) + + def cancel(self) -> None: + """Cancel all current and future next() calls.""" + self._inner.cancel() diff --git a/py/moq-rs/moq/media.py b/py/moq-rs/moq/media.py new file mode 100644 index 00000000000..c5420aacc58 --- /dev/null +++ b/py/moq-rs/moq/media.py @@ -0,0 +1,418 @@ +"""Catalogs, media importers, and container consumers for a broadcast.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from datetime import timedelta +from typing import Any, cast + +from moq_ffi import MoqAudio as Audio +from moq_ffi import MoqAudioFormat as AudioFormat +from moq_ffi import ( + MoqAudioInit as AudioInit, +) +from moq_ffi import ( + MoqCatalog as Catalog, +) +from moq_ffi import ( + MoqContainer as Container, +) +from moq_ffi import ( + MoqContainerFormat as ContainerFormat, +) +from moq_ffi import ( + MoqContainerInit as ContainerInit, +) +from moq_ffi import MoqDimensions as Dimensions +from moq_ffi import ( + MoqError, + MoqMediaCatalogConsumer, + MoqMediaCatalogProducer, + MoqMediaContainerConfig, + MoqMediaContainerConsumer, + MoqMediaContainerGroupConfig, + MoqMediaContainerGroupConsumer, + MoqMediaContainerProducer, + MoqMediaContainerStreamProducer, + MoqMediaFrame, + MoqMediaTarget, + MoqMediaTrackProducer, + MoqMediaTrackStreamProducer, +) +from moq_ffi import MoqVideo as Video +from moq_ffi import MoqVideoFormat as VideoFormat +from moq_ffi import MoqVideoHint as VideoHint +from moq_ffi import ( + MoqVideoInit as VideoInit, +) +from moq_ffi import ( + MoqVideoProperties as VideoProperties, +) + +from ._records import Frame, Subscription, _from_us, _subscription, _to_us +from .publish import BroadcastProducer, TrackDemand, TrackRequest +from .subscribe import BroadcastConsumer +from .types import FetchGroupOptions + + +@dataclass(frozen=True) +class Named: + """Publish a new track with a chosen name, or a format-derived unique name.""" + + name: str | None = None + + def _ffi(self) -> MoqMediaTarget: + return cast(MoqMediaTarget, MoqMediaTarget.NAMED(name=self.name)) + + +@dataclass(frozen=True) +class Requested: + """Publish the track named by a subscriber's pending request.""" + + request: TrackRequest + + def _ffi(self) -> MoqMediaTarget: + return cast(MoqMediaTarget, MoqMediaTarget.REQUESTED(request=self.request._inner)) + + +Target = Named | Requested +"""The named or requested track an importer publishes.""" + + +@dataclass(frozen=True) +class MediaFrame: + """A container-decoded payload, presentation timestamp, and keyframe flag.""" + + payload: bytes + timestamp: timedelta + keyframe: bool + + @staticmethod + def _from_ffi(frame: MoqMediaFrame) -> MediaFrame: + return MediaFrame(frame.payload, _from_us(frame.timestamp_us), frame.keyframe) + + +class CatalogProducer: + """Update a broadcast's catalog without keeping the broadcast open.""" + + def __init__(self, broadcast: BroadcastProducer) -> None: + self._inner = MoqMediaCatalogProducer(broadcast._inner) + + def set_video_properties(self, properties: VideoProperties) -> None: + """Replace the video properties shared by every rendition.""" + self._inner.set_video_properties(properties) + + def set_section(self, name: str, value: Any) -> None: + """Set an application catalog section from a JSON-serializable value.""" + self._inner.set_section(name, json.dumps(value)) + + def remove_section(self, name: str) -> None: + """Remove an application catalog section if present.""" + self._inner.remove_section(name) + + +class TrackProducer: + """Publish encoded media frames on a single track, one payload at a time. + + Construct with :meth:`audio` or :meth:`video`. Push each encoded frame with + :meth:`write_frame`, then :meth:`finish` when the stream ends. + """ + + @classmethod + def audio(cls, broadcast: BroadcastProducer, init: AudioInit, *, target: Target = Named()) -> TrackProducer: + """Import complete encoded audio frames on a named or requested track.""" + return cls(MoqMediaTrackProducer.audio(broadcast._inner, target._ffi(), init)) + + @classmethod + def video(cls, broadcast: BroadcastProducer, init: VideoInit, *, target: Target = Named()) -> TrackProducer: + """Import complete encoded video frames on a named or requested track.""" + return cls(MoqMediaTrackProducer.video(broadcast._inner, target._ffi(), init)) + + def __init__(self, inner: MoqMediaTrackProducer) -> None: + self._inner = inner + + def demand(self) -> TrackDemand: + """A watch-only handle to whether this media track has subscribers.""" + return TrackDemand(self._inner.demand()) + + def write_frame(self, payload: bytes, timestamp: timedelta = timedelta(0)) -> None: + """Write one encoded frame with a presentation timestamp.""" + self._inner.write_frame(Frame(payload, timestamp)._ffi()) + + def flush(self, timestamp: timedelta) -> None: + """Record a local encoder's frame handoff on the broadcast media clock. + + Call this after ``write_frame`` only for encoded live output. File, pipe, + and network imports should leave their jitter estimate clock free. + """ + self._inner.flush(_to_us(timestamp, "timestamp")) + + def discontinuity(self) -> None: + """Mark a timeline break and restart handoff measurement, preserving advertised jitter.""" + self._inner.discontinuity() + + def cut(self) -> None: + """Draw a group boundary here. + + Audio has no boundary of its own (every packet is independently + decodable), so this is the only thing that gives it groups: call it + after every frame for one group (one QUIC stream) the relay forwards + without waiting, or at a segment cadence to align with video. Video + groups at its own keyframes and needs this only to override that. + """ + self._inner.cut() + + def seek(self, sequence: int) -> None: + """Draw a group boundary and number the next group ``sequence``. + + :meth:`cut` with an explicit sequence, for a publisher whose group + numbers have to be deterministic: two encoders aligning per GOP so a + consumer can fail over between them. + """ + self._inner.seek(sequence) + + def finish(self) -> None: + """Finish publishing and flush a clean end to subscribers.""" + self._inner.finish() + + +class ContainerProducer: + """Publish a container, which demuxes and publishes its own tracks. + + Unlike :class:`TrackProducer` there is no per-frame timestamp: a container + carries its tracks' timing itself. + """ + + def __init__(self, broadcast: BroadcastProducer, init: ContainerInit) -> None: + self._inner = MoqMediaContainerProducer(broadcast._inner, init) + + def write(self, payload: bytes) -> None: + """Write a whole chunk of container bytes.""" + self._inner.write(payload) + + def cut(self) -> None: + """Declare that the next chunk starts a new segment, rolling a group on every track. + + An fMP4 source carrying ``styp`` atoms declares its own segments, so + this is only needed when it doesn't. Formats with no segment concept + (MKV, TS, FLV) ignore it. + """ + self._inner.cut() + + def seek(self, sequence: int) -> None: + """Start a new segment and number its groups ``sequence``.""" + self._inner.seek(sequence) + + def finish(self) -> None: + """Finish every track this container publishes.""" + self._inner.finish() + + +class ContainerStreamProducer: + """Publish a container fed by a raw byte stream, which recovers its own framing.""" + + def __init__(self, broadcast: BroadcastProducer, format: ContainerFormat) -> None: + self._inner = MoqMediaContainerStreamProducer(broadcast._inner, format) + + def write(self, payload: bytes) -> None: + """Push raw container bytes; chunk boundaries don't matter.""" + self._inner.write(payload) + + def finish(self) -> None: + """Finish every track this container publishes.""" + self._inner.finish() + + +class TrackStreamProducer: + """Wraps MoqMediaTrackStreamProducer: feed a raw byte stream (e.g. Annex-B + H.264) and let the importer infer frame boundaries. + + Construct with :meth:`video`. Unlike + :class:`TrackProducer`, no per-frame timestamps are needed; just push + encoder bytes as they arrive. + """ + + @classmethod + def video(cls, broadcast: BroadcastProducer, init: VideoInit, *, target: Target = Named()) -> TrackStreamProducer: + """Import a video byte stream, inferring frame boundaries.""" + return cls(MoqMediaTrackStreamProducer.video(broadcast._inner, target._ffi(), init)) + + def __init__(self, inner: MoqMediaTrackStreamProducer) -> None: + self._inner = inner + + def demand(self) -> TrackDemand: + """A watch-only handle to whether this media track has subscribers.""" + return TrackDemand(self._inner.demand()) + + def write(self, payload: bytes) -> None: + """Push raw stream bytes; whole frames are emitted as they complete.""" + self._inner.write(payload) + + def finish(self) -> None: + """Finish publishing and flush a clean end to subscribers.""" + self._inner.finish() + + +class ContainerConsumer: + """Async-iterable stream of decoded :class:`MediaFrame` in decode order. + + Construct with :meth:`subscribe`. Iterate with ``async for``; + usable as an async context manager that cancels on exit. + """ + + @classmethod + async def subscribe( + cls, broadcast: BroadcastConsumer, name: str, container: Container, *, subscription: Subscription | None = None + ) -> ContainerConsumer: + """Subscribe to a media track and decode its container.""" + config = MoqMediaContainerConfig(name=name, container=container, subscription=_subscription(subscription)) + return cls(await MoqMediaContainerConsumer.subscribe(broadcast._inner, config)) + + def __init__(self, inner: MoqMediaContainerConsumer) -> None: + self._inner = inner + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc) -> None: + self.cancel() + + def __aiter__(self): + return self + + async def __anext__(self) -> MediaFrame: + frame = await self._inner.next() + if frame is None: + raise StopAsyncIteration + return MediaFrame._from_ffi(frame) + + def cancel(self) -> None: + """Cancel the subscription and stop delivering frames.""" + self._inner.cancel() + + +class ContainerGroupConsumer: + """Async iterator of decoded :class:`MediaFrame` within a single fetched group. + + Construct with :meth:`fetch`. Finite: iteration ends + after the group's last frame. Usable as an async context manager that cancels + on exit. + """ + + @classmethod + async def fetch( + cls, + broadcast: BroadcastConsumer, + name: str, + sequence: int, + container: Container, + *, + options: FetchGroupOptions | None = None, + ) -> ContainerGroupConsumer: + """Fetch and decode exactly one media group.""" + config = MoqMediaContainerGroupConfig(name=name, sequence=sequence, container=container, options=options) + return cls(await MoqMediaContainerGroupConsumer.fetch(broadcast._inner, config)) + + def __init__(self, inner: MoqMediaContainerGroupConsumer) -> None: + self._inner = inner + + @property + def sequence(self) -> int: + """The sequence number of this group within the track.""" + return self._inner.sequence() + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc) -> None: + self.cancel() + + def __aiter__(self): + return self + + async def __anext__(self) -> MediaFrame: + frame = await self._inner.next() + if frame is None: + raise StopAsyncIteration + return MediaFrame._from_ffi(frame) + + def cancel(self) -> None: + """Cancel reading this group and stop delivering frames.""" + self._inner.cancel() + + +class CatalogConsumer: + """Async-iterable stream of :class:`Catalog` snapshots as the broadcast updates. + + Construct with :meth:`subscribe`. Each item is the latest + catalog describing the broadcast's tracks; usable as an async context manager. + """ + + @classmethod + async def subscribe(cls, broadcast: BroadcastConsumer) -> CatalogConsumer: + """Subscribe to the broadcast's catalog snapshots.""" + return cls(await MoqMediaCatalogConsumer.subscribe(broadcast._inner)) + + def __init__(self, inner: MoqMediaCatalogConsumer) -> None: + self._inner = inner + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc) -> None: + self.cancel() + + def __aiter__(self): + return self + + async def __anext__(self) -> Catalog: + catalog = await self._inner.next() + if catalog is None: + raise StopAsyncIteration + return catalog + + def cancel(self) -> None: + """Cancel the catalog subscription and stop delivering updates.""" + self._inner.cancel() + + +async def catalog(broadcast: BroadcastConsumer) -> Catalog: + """Read the first catalog snapshot, then release the subscription.""" + consumer = await CatalogConsumer.subscribe(broadcast) + try: + return await anext(consumer) + except StopAsyncIteration: + raise MoqError.Closed() from None + finally: + consumer.cancel() + + +__all__ = [ + "Named", + "Requested", + "Target", + "MediaFrame", + "CatalogProducer", + "CatalogConsumer", + "TrackProducer", + "TrackStreamProducer", + "ContainerProducer", + "ContainerStreamProducer", + "ContainerConsumer", + "ContainerGroupConsumer", + "Audio", + "Video", + "Catalog", + "Container", + "Dimensions", + "AudioFormat", + "VideoFormat", + "ContainerFormat", + "VideoHint", + "VideoProperties", + "AudioInit", + "VideoInit", + "ContainerInit", + "catalog", +] diff --git a/py/moq-rs/moq/publish.py b/py/moq-rs/moq/publish.py index 71da932da6c..ddf245f6986 100644 --- a/py/moq-rs/moq/publish.py +++ b/py/moq-rs/moq/publish.py @@ -2,49 +2,34 @@ from __future__ import annotations -import json -from typing import TYPE_CHECKING, Any +from datetime import timedelta +from typing import TYPE_CHECKING from moq_ffi import ( - MoqAudioInit, MoqAudioProducer, MoqBroadcastDynamic, MoqBroadcastProducer, - MoqContainerInit, - MoqContainerProducer, - MoqContainerStreamProducer, MoqGroupProducer, MoqGroupRequest, - MoqJsonSnapshotConfig, - MoqJsonSnapshotProducer, - MoqJsonStreamConfig, - MoqJsonStreamProducer, - MoqMediaProducer, - MoqMediaStreamProducer, MoqTrackDemand, MoqTrackDynamic, MoqTrackProducer, MoqTrackRequest, - MoqVideoInit, MoqVideoProducer, ) +from ._records import _subscription, _track_info from .types import ( AudioEncoderInput, AudioEncoderOutput, - AudioFormat, AudioFrame, - ContainerFormat, Frame, Route, Subscription, TrackInfo, VideoEncoderInput, VideoEncoderOutput, - VideoFormat, VideoFrame, - VideoHint, - VideoProperties, ) if TYPE_CHECKING: @@ -52,157 +37,6 @@ from .subscribe import BroadcastConsumer, GroupConsumer, TrackConsumer -def _audio_init(format: AudioFormat, init: bytes, label: str | None, track: str | None = None) -> MoqAudioInit: - return MoqAudioInit(format=format, data=init, label=label, track=track) - - -def _video_init( - format: VideoFormat, init: bytes, label: str | None, hint: VideoHint | None, track: str | None = None -) -> MoqVideoInit: - return MoqVideoInit(format=format, data=init, label=label, hint=hint, track=track) - - -class MediaProducer: - """Publish encoded media frames on a single track, one payload at a time. - - Built via :meth:`BroadcastProducer.publish_audio` or - :meth:`BroadcastProducer.publish_video`. Push each encoded frame with - :meth:`write_frame`, then :meth:`finish` when the stream ends. - """ - - def __init__(self, inner: MoqMediaProducer) -> None: - self._inner = inner - - @property - def name(self) -> str: - """The generated media track name.""" - return self._inner.name() - - def demand(self) -> TrackDemand: - """A watch-only handle to whether this media track has subscribers.""" - return TrackDemand(self._inner.demand()) - - async def used(self) -> None: - """Wait until this media track has at least one active subscriber. Prefer :meth:`demand`.""" - await self._inner.used() - - async def unused(self) -> None: - """Wait until this media track has no active subscribers. Prefer :meth:`demand`.""" - await self._inner.unused() - - def write_frame(self, payload: bytes, timestamp_us: int = 0) -> None: - """Write one encoded frame with a presentation timestamp in microseconds.""" - self._inner.write_frame(Frame(payload=payload, timestamp_us=timestamp_us)) - - def flush(self, timestamp_us: int) -> None: - """Record a local encoder's frame handoff on the broadcast media clock. - - Call this after ``write_frame`` only for encoded live output. File, pipe, - and network imports should leave their jitter estimate clock free. - """ - self._inner.flush(timestamp_us) - - def discontinuity(self) -> None: - """Mark a timeline break and restart handoff measurement, preserving advertised jitter.""" - self._inner.discontinuity() - - def cut(self) -> None: - """Draw a group boundary here. - - Audio has no boundary of its own (every packet is independently - decodable), so this is the only thing that gives it groups: call it - after every frame for one group (one QUIC stream) the relay forwards - without waiting, or at a segment cadence to align with video. Video - groups at its own keyframes and needs this only to override that. - """ - self._inner.cut() - - def seek(self, sequence: int) -> None: - """Draw a group boundary and number the next group ``sequence``. - - :meth:`cut` with an explicit sequence, for a publisher whose group - numbers have to be deterministic: two encoders aligning per GOP so a - consumer can fail over between them. - """ - self._inner.seek(sequence) - - def finish(self) -> None: - """Finish publishing and flush a clean end to subscribers.""" - self._inner.finish() - - -class ContainerProducer: - """Publish a container, which demuxes and publishes its own tracks. - - Built via :meth:`BroadcastProducer.publish_container`. Unlike - :class:`MediaProducer` there is no per-frame timestamp: a container carries - its tracks' timing itself. - """ - - def __init__(self, inner: MoqContainerProducer) -> None: - self._inner = inner - - def write(self, payload: bytes) -> None: - """Write a whole chunk of container bytes.""" - self._inner.write(payload) - - def cut(self) -> None: - """Declare that the next chunk starts a new segment, rolling a group on every track. - - An fMP4 source carrying ``styp`` atoms declares its own segments, so - this is only needed when it doesn't. Formats with no segment concept - (MKV, TS, FLV) ignore it. - """ - self._inner.cut() - - def seek(self, sequence: int) -> None: - """Start a new segment and number its groups ``sequence``.""" - self._inner.seek(sequence) - - def finish(self) -> None: - """Finish every track this container publishes.""" - self._inner.finish() - - -class ContainerStreamProducer: - """Publish a container fed by a raw byte stream, which recovers its own framing. - - Built via :meth:`BroadcastProducer.publish_container_stream`. - """ - - def __init__(self, inner: MoqContainerStreamProducer) -> None: - self._inner = inner - - def write(self, payload: bytes) -> None: - """Push raw container bytes; chunk boundaries don't matter.""" - self._inner.write(payload) - - def finish(self) -> None: - """Finish every track this container publishes.""" - self._inner.finish() - - -class MediaStreamProducer: - """Wraps MoqMediaStreamProducer: feed a raw byte stream (e.g. Annex-B - H.264) and let the importer infer frame boundaries. - - Built via :meth:`BroadcastProducer.publish_video_stream`. Unlike - :class:`MediaProducer`, no per-frame timestamps are needed; just push - encoder bytes as they arrive. - """ - - def __init__(self, inner: MoqMediaStreamProducer) -> None: - self._inner = inner - - def write(self, payload: bytes) -> None: - """Push raw stream bytes; whole frames are emitted as they complete.""" - self._inner.write(payload) - - def finish(self) -> None: - """Finish publishing and flush a clean end to subscribers.""" - self._inner.finish() - - class GroupProducer: """Writes frames into a single group on a track.""" @@ -220,9 +54,9 @@ def consume(self) -> GroupConsumer: return GroupConsumer(self._inner.consume()) - def write_frame(self, payload: bytes, timestamp_us: int = 0) -> None: - """Write a frame with a presentation timestamp in microseconds.""" - self._inner.write_frame(Frame(payload=payload, timestamp_us=timestamp_us)) + def write_frame(self, payload: bytes, timestamp: timedelta = timedelta(0)) -> None: + """Write a frame with its presentation timestamp.""" + self._inner.write_frame(Frame(payload, timestamp)._ffi()) def finish(self) -> None: """Close this group cleanly, marking it complete for subscribers. @@ -275,23 +109,10 @@ class TrackProducer: def __init__(self, inner: MoqTrackProducer) -> None: self._inner = inner - @property - def name(self) -> str: - """The track name.""" - return self._inner.name() - def demand(self) -> TrackDemand: - """A watch-only handle to whether this track has subscribers.""" + """A watch-only handle to this track's name and whether it has subscribers.""" return TrackDemand(self._inner.demand()) - async def used(self) -> None: - """Wait until this track has at least one active subscriber. Prefer :meth:`demand`.""" - await self._inner.used() - - async def unused(self) -> None: - """Wait until this track has no active subscribers. Prefer :meth:`demand`.""" - await self._inner.unused() - def dynamic(self) -> TrackDynamic: """Serve fetches for groups that are not currently cached.""" return TrackDynamic(self._inner.dynamic()) @@ -304,17 +125,17 @@ def create_group(self, sequence: int) -> GroupProducer: """Create a group with an explicit sequence number.""" return GroupProducer(self._inner.create_group(sequence)) - def write_frame(self, payload: bytes, timestamp_us: int = 0) -> None: - """Write a single-frame group with a timestamp in microseconds.""" - self._inner.write_frame(Frame(payload=payload, timestamp_us=timestamp_us)) + def write_frame(self, payload: bytes, timestamp: timedelta = timedelta(0)) -> None: + """Write a single-frame group with its presentation timestamp.""" + self._inner.write_frame(Frame(payload, timestamp)._ffi()) - def append_datagram(self, payload: bytes, timestamp_us: int = 0) -> int: + def append_datagram(self, payload: bytes, timestamp: timedelta = timedelta(0)) -> int: """Send a best-effort datagram and return its sequence number. Payloads are capped at 1200 bytes. Datagram delivery requires a datagram-capable transport and wire version; there is no stream fallback. """ - return self._inner.append_datagram(Frame(payload=payload, timestamp_us=timestamp_us)) + return self._inner.append_datagram(Frame(payload, timestamp)._ffi()) def consume(self, subscription: Subscription | None = None) -> TrackConsumer: """Create a consumer that reads directly from this producer's track. @@ -323,7 +144,7 @@ def consume(self, subscription: Subscription | None = None) -> TrackConsumer: """ from .subscribe import TrackConsumer - return TrackConsumer(self._inner.consume(subscription)) + return TrackConsumer(self._inner.consume(_subscription(subscription))) def abort(self, error_code: int) -> None: """Abort this track with an application error code.""" @@ -344,8 +165,8 @@ def finish_at(self, final_sequence: int) -> None: class TrackRequest: """A subscriber-requested track that hasn't been accepted yet. - Accept it for raw writes, hand it to :meth:`BroadcastProducer.publish_audio_on_track` - or :meth:`BroadcastProducer.publish_video_on_track` to publish media (the importer + Accept it for raw writes, hand it to :class:`~moq.media.Requested` for :meth:`~moq.media.TrackProducer.audio` + or :meth:`~moq.media.TrackProducer.video` to publish media (the importer accepts it), or abort it to reject the subscriber. """ @@ -362,7 +183,7 @@ def accept(self, info: TrackInfo | None = None) -> TrackProducer: ``info`` fixes the track's timescale, priority, and cache; omit for defaults. """ - return TrackProducer(self._inner.accept(info)) + return TrackProducer(self._inner.accept(_track_info(info))) def dynamic(self) -> TrackDynamic: """Create a fetch handler before accepting this requested track.""" @@ -428,53 +249,6 @@ def cancel(self) -> None: self._inner.cancel() -class JsonSnapshotProducer: - """Publish a JSON value that consumers see as a single latest state (lossy). - - Built via :meth:`BroadcastProducer.publish_json_snapshot`. Each :meth:`update` supersedes the - last; a late joiner only sees the newest value. Values are any JSON-serializable Python - object, encoded as snapshots and merge-patch deltas automatically. - """ - - def __init__(self, inner: MoqJsonSnapshotProducer) -> None: - self._inner = inner - - def demand(self) -> TrackDemand: - """A watch-only handle to whether this track has subscribers.""" - return TrackDemand(self._inner.demand()) - - def update(self, value: Any) -> None: - """Publish a new value. A no-op if unchanged from the previous update.""" - self._inner.update(json.dumps(value)) - - def finish(self) -> None: - """Finish the track, closing any open group.""" - self._inner.finish() - - -class JsonStreamProducer: - """Publish an ordered log of JSON records (lossless). - - Built via :meth:`BroadcastProducer.publish_json_stream`. Every :meth:`append` is - preserved and delivered in order. Records are any JSON-serializable Python object. - """ - - def __init__(self, inner: MoqJsonStreamProducer) -> None: - self._inner = inner - - def demand(self) -> TrackDemand: - """A watch-only handle to whether this track has subscribers.""" - return TrackDemand(self._inner.demand()) - - def append(self, value: Any) -> None: - """Append one record to the log.""" - self._inner.append(json.dumps(value)) - - def finish(self) -> None: - """Finish the track, closing the group.""" - self._inner.finish() - - class AudioProducer: """Publish raw PCM and let libopus encode it on the way out. @@ -529,7 +303,7 @@ def finish(self) -> None: class VideoProducer: """Publish raw pictures and let a native encoder compress them on the way out. - Built via :meth:`BroadcastProducer.publish_video`. Pixel format, + Built via :meth:`BroadcastProducer.encode_video`. Pixel format, resolution, and framerate are fixed at construction; each :meth:`write` call passes only pixels and a presentation timestamp. """ @@ -661,88 +435,6 @@ def unannounce(self) -> None: """Retract this broadcast's advertisement, if any, from local consumers and peers alike.""" self._inner.unannounce() - def set_video_properties(self, properties: VideoProperties) -> None: - """Replace the catalog properties shared by every video rendition.""" - self._inner.set_video_properties(properties) - - def publish_audio( - self, - format: AudioFormat, - init: bytes, - *, - label: str | None = None, - track: str | None = None, - ) -> MediaProducer: - """Publish one audio codec as a new track. `init` is required: audio resolves its whole - rendition from those bytes (an OpusHead, an AudioSpecificConfig, a STREAMINFO). `label` is - the human-readable rendition name stored in the catalog. `track` names the track; otherwise - a unique name is derived from the format.""" - return MediaProducer(self._inner.publish_audio(_audio_init(format, init, label, track))) - - def publish_video( - self, - format: VideoFormat, - init: bytes = b"", - *, - label: str | None = None, - hint: VideoHint | None = None, - track: str | None = None, - ) -> MediaProducer: - """Publish one video codec as a new track. `init` may be empty for a format that resolves in - band. `hint` seeds catalog fields the stream can't reveal (bitrate) or publishes the catalog - before the first keyframe. See :class:`VideoHint`. `track` names the track; otherwise a - unique name is derived from the format.""" - return MediaProducer(self._inner.publish_video(_video_init(format, init, label, hint, track))) - - def publish_container( - self, - format: ContainerFormat, - init: bytes = b"", - ) -> ContainerProducer: - """Publish a container, which demuxes and publishes its own tracks. There is no label or - hint: a container describes each track it publishes from its own metadata.""" - return ContainerProducer(self._inner.publish_container(MoqContainerInit(format=format, data=init))) - - def publish_audio_on_track( - self, - request: TrackRequest, - format: AudioFormat, - init: bytes, - *, - label: str | None = None, - ) -> MediaProducer: - """Publish one audio codec onto a requested track. See :meth:`publish_audio`.""" - return MediaProducer(self._inner.publish_audio_on_track(request._inner, _audio_init(format, init, label))) - - def publish_video_on_track( - self, - request: TrackRequest, - format: VideoFormat, - init: bytes = b"", - *, - label: str | None = None, - hint: VideoHint | None = None, - ) -> MediaProducer: - """Publish one video codec onto a requested track. See :meth:`publish_video`.""" - return MediaProducer(self._inner.publish_video_on_track(request._inner, _video_init(format, init, label, hint))) - - def publish_video_stream( - self, - format: VideoFormat, - *, - label: str | None = None, - hint: VideoHint | None = None, - track: str | None = None, - ) -> MediaStreamProducer: - """Publish a video track fed by a raw byte stream (unknown frame boundaries). Only the - self-delimiting formats work: `AVC3`, `HEV1`, `AV01`. There is no audio counterpart, since - audio has no frame boundaries to infer. `track` names the track as in :meth:`publish_video`.""" - return MediaStreamProducer(self._inner.publish_video_stream(_video_init(format, b"", label, hint, track))) - - def publish_container_stream(self, format: ContainerFormat) -> ContainerStreamProducer: - """Publish a container fed by a raw byte stream, which recovers its own framing.""" - return ContainerStreamProducer(self._inner.publish_container_stream(format)) - def encode_audio( self, name: str, @@ -785,57 +477,7 @@ def encode_video( def publish_track(self, name: str, info: TrackInfo | None = None) -> TrackProducer: """Create a track. Send any bytes, no codec validation. ``info`` sets track properties (priority, cache, timescale); omit for defaults.""" - return TrackProducer(self._inner.publish_track(name, info)) - - def publish_json_snapshot( - self, name: str, *, delta_ratio: int | None = None, compression: bool = False - ) -> JsonSnapshotProducer: - """Publish a JSON snapshot track (lossy latest-value). - - Each update supersedes the last; a late joiner only sees the newest value. - ``delta_ratio`` controls how aggressively deltas are emitted instead of full - snapshots (0 disables deltas); ``None`` uses the binding's default. Set - ``compression`` to DEFLATE-compress each group; the consumer must pass the same - flag. The track is advertised in the broadcast's catalog (``json.tracks.``) - until it finishes; a name the catalog already carries is refused. - """ - # Let the record supply delta_ratio's default rather than restating it here. - config = ( - MoqJsonSnapshotConfig(compression=compression) - if delta_ratio is None - else MoqJsonSnapshotConfig(delta_ratio=delta_ratio, compression=compression) - ) - return JsonSnapshotProducer(self._inner.publish_json_snapshot(name, config)) - - def publish_json_stream(self, name: str, *, compression: bool = False) -> JsonStreamProducer: - """Publish a JSON stream track (lossless append-log). - - Every appended record is preserved and delivered in order. Set ``compression`` to - DEFLATE-compress the group; the consumer must pass the same flag. The track is - advertised in the broadcast's catalog (``json.tracks.``) until it finishes; a - name the catalog already carries is refused. - """ - config = MoqJsonStreamConfig(compression=compression) - return JsonStreamProducer(self._inner.publish_json_stream(name, config)) - - def set_catalog_section(self, name: str, value: Any) -> None: - """Set or replace an untyped application section in the catalog. - - `value` is any JSON-serializable Python object; it lands as a top-level catalog - key alongside `video`/`audio` and reaches subscribers via `Catalog.sections`. - `name` must not be a reserved media section ("video"/"audio"). The catalog is - republished automatically. Use this to advertise a side-channel track (e.g. a - transcript or captions track) that the catalog doesn't model natively. - """ - self._inner.set_catalog_section(name, json.dumps(value)) - - def remove_catalog_section(self, name: str) -> None: - """Remove an untyped application section from the catalog by name. - - A no-op if no section with that name exists. The catalog is republished - automatically. - """ - self._inner.remove_catalog_section(name) + return TrackProducer(self._inner.publish_track(name, _track_info(info))) def consume(self) -> BroadcastConsumer: """Create a consumer that reads from this broadcast's tracks.""" diff --git a/py/moq-rs/moq/server.py b/py/moq-rs/moq/server.py index 6b3db8b04bf..e54594041ac 100644 --- a/py/moq-rs/moq/server.py +++ b/py/moq-rs/moq/server.py @@ -5,8 +5,9 @@ import asyncio from collections.abc import Sequence -from moq_ffi import MoqRequest, MoqServer, MoqTransport +from moq_ffi import MoqQuicConfig, MoqRequest, MoqServer, MoqServerConfig, MoqServerTls, MoqTransport +from ._records import _strs from .origin import OriginProducer from .publish import BroadcastProducer from .session import Session @@ -48,30 +49,22 @@ def transport(self) -> Transport: """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 - server's configured publish origin if unset. Captured at ``accept()``. - - Raises if the request is currently accepting, already answered, or cancelled. - """ - self._inner.set_publish(origin._inner if origin is not None else None) - - def set_consume(self, origin: OriginProducer | None) -> None: - """Override the consume origin for this session. Falls back to the - server's configured consume origin if unset. Captured at ``accept()``. - - Raises if the request is currently accepting, already answered, or cancelled. - """ - self._inner.set_consume(origin._inner if origin is not None else None) - - async def accept(self) -> Session: + async def accept(self, *, publish: OriginProducer | None = None, consume: OriginProducer | None = None) -> Session: """Complete the MoQ handshake and return the established session. + None inherits the server origin; a supplied origin replaces it. + Pass a fresh origin for isolation, or the same origin for both sides. + The caller must hold the returned session to keep the connection alive; dropping it closes the session. Raises `Error.AlreadyResponded` if `accept()` or `reject()` has already been called. """ - return Session(await self._inner.accept()) + return Session( + await self._inner.accept( + publish=publish._inner if publish is not None else None, + consume=consume._inner if consume is not None else None, + ) + ) async def reject(self, code: int) -> None: """Reject the session with the given application error code. @@ -119,7 +112,7 @@ class Server: "127.0.0.1:4443", tls_generate=["localhost"], publish=origin, - subscribe=origin, + consume=origin, ) """ @@ -130,42 +123,40 @@ def __init__( tls_cert: Sequence[str] = (), tls_key: Sequence[str] = (), tls_generate: Sequence[str] = (), + versions: Sequence[str] = (), + max_streams: int | None = None, publish: OriginProducer | None = None, - subscribe: OriginProducer | None = None, + consume: OriginProducer | None = None, ) -> None: - self._bind = bind - self._tls_cert = list(tls_cert) - self._tls_key = list(tls_key) - self._tls_generate = list(tls_generate) - # If neither origin is provided, create a shared internal one. - if publish is None and subscribe is None: - self._origin: OriginProducer | None = OriginProducer() - self._publish_origin: OriginProducer | None = self._origin - self._consume_origin: OriginProducer | None = self._origin - else: - self._origin = None - self._publish_origin = publish - self._consume_origin = subscribe + if publish is None and consume is None: + publish = consume = OriginProducer() + self._publish_origin = publish + + self._config = MoqServerConfig( + bind=bind, + versions=_strs(versions, "versions"), + tls=MoqServerTls( + cert=_strs(tls_cert, "tls_cert"), + key=_strs(tls_key, "tls_key"), + generate=_strs(tls_generate, "tls_generate"), + ), + quic=MoqQuicConfig(max_streams=max_streams), + publish=None if publish is None else publish._inner, + consume=None if consume is None else consume._inner, + ) self._inner: MoqServer | None = None self._local_addr: str | None = None async def __aenter__(self): - self._inner = MoqServer() - self._inner.set_bind(self._bind) - if self._tls_cert: - self._inner.set_tls_cert(self._tls_cert) - if self._tls_key: - self._inner.set_tls_key(self._tls_key) - if self._tls_generate: - self._inner.set_tls_generate(self._tls_generate) - if self._publish_origin is not None: - self._inner.set_publish(self._publish_origin._inner) - if self._consume_origin is not None: - self._inner.set_consume(self._consume_origin._inner) - - self._local_addr = await self._inner.listen() + self._inner = MoqServer(self._config) + try: + self._local_addr = await self._inner.listen() + except BaseException: + self._inner.cancel() + self._inner = None + raise return self async def __aexit__(self, *exc) -> None: diff --git a/py/moq-rs/moq/session.py b/py/moq-rs/moq/session.py index 973cbb63e92..b0f55851a4e 100644 --- a/py/moq-rs/moq/session.py +++ b/py/moq-rs/moq/session.py @@ -91,7 +91,7 @@ def stats(self) -> ConnectionStats: byte/packet counters). Cheap to call; intended for periodic polling. Individual fields are ``None`` when the transport backend doesn't report them.""" - return self._inner.stats() + return ConnectionStats._from_ffi(self._inner.stats()) def bandwidth(self) -> Bandwidth: """The session's bandwidth allocator. diff --git a/py/moq-rs/moq/subscribe.py b/py/moq-rs/moq/subscribe.py index 9470cd526af..18df104473f 100644 --- a/py/moq-rs/moq/subscribe.py +++ b/py/moq-rs/moq/subscribe.py @@ -2,107 +2,35 @@ from __future__ import annotations -import json from collections.abc import AsyncIterator -from typing import Any +from moq_ffi import MoqAudio as Audio from moq_ffi import ( MoqAudioConsumer, MoqBroadcastConsumer, - MoqCatalogConsumer, + MoqFrame, MoqGroupConsumer, - MoqJsonSnapshotConfig, - MoqJsonSnapshotConsumer, - MoqJsonStreamConfig, - MoqJsonStreamConsumer, - MoqMediaConsumer, - MoqMediaGroupConsumer, MoqTrackConsumer, MoqVideoConsumer, ) +from moq_ffi import MoqVideo as Video +from ._records import _subscription from .types import ( - Audio, AudioDecoderOutput, AudioFrame, - Catalog, - Container, Datagram, FetchGroupOptions, Frame, - MediaFrame, Subscription, TrackInfo, - Video, VideoDecodedFrame, VideoDecoderOutput, ) -class MediaConsumer: - """Async-iterable stream of decoded :class:`MediaFrame` in decode order. - - Built via :meth:`BroadcastConsumer.subscribe_media`. Iterate with ``async for``; - usable as an async context manager that cancels on exit. - """ - - def __init__(self, inner: MoqMediaConsumer) -> None: - self._inner = inner - - async def __aenter__(self): - return self - - async def __aexit__(self, *exc) -> None: - self.cancel() - - def __aiter__(self): - return self - - async def __anext__(self) -> MediaFrame: - frame = await self._inner.next() - if frame is None: - raise StopAsyncIteration - return frame - - def cancel(self) -> None: - """Cancel the subscription and stop delivering frames.""" - self._inner.cancel() - - -class MediaGroupConsumer: - """Async iterator of decoded :class:`MediaFrame` within a single fetched group. - - Built via :meth:`BroadcastConsumer.fetch_media_group`. Finite: iteration ends - after the group's last frame. Usable as an async context manager that cancels - on exit. - """ - - def __init__(self, inner: MoqMediaGroupConsumer) -> None: - self._inner = inner - - @property - def sequence(self) -> int: - """The sequence number of this group within the track.""" - return self._inner.sequence() - - async def __aenter__(self): - return self - - async def __aexit__(self, *exc) -> None: - self.cancel() - - def __aiter__(self): - return self - - async def __anext__(self) -> MediaFrame: - frame = await self._inner.next() - if frame is None: - raise StopAsyncIteration - return frame - - def cancel(self) -> None: - """Cancel reading this group and stop delivering frames.""" - self._inner.cancel() +def _frame(frame: MoqFrame | None) -> Frame | None: + return None if frame is None else Frame._from_ffi(frame) class GroupConsumer: @@ -129,11 +57,11 @@ async def __anext__(self) -> Frame: frame = await self._inner.read_frame() if frame is None: raise StopAsyncIteration - return frame + return Frame._from_ffi(frame) async def read_frame(self) -> Frame | None: """Read the next timestamped frame. Returns `None` when the group ends.""" - return await self._inner.read_frame() + return _frame(await self._inner.read_frame()) def cancel(self) -> None: """Cancel reading this group and stop delivering frames.""" @@ -213,7 +141,7 @@ async def read_frame(self) -> Frame | None: `None` only when the track ends. Cancelling one call keeps the current group so a later :meth:`read_frame` or :meth:`next_group` still sees it. """ - return await self._inner.read_frame() + return _frame(await self._inner.read_frame()) async def recv_datagram(self) -> Datagram | None: """Receive the next best-effort datagram in arrival order. @@ -221,15 +149,16 @@ async def recv_datagram(self) -> Datagram | None: Returns ``None`` when the track ends. Datagrams are unavailable over stream-only transports and older wire versions. """ - return await self._inner.recv_datagram() + datagram = await self._inner.recv_datagram() + return None if datagram is None else Datagram._from_ffi(datagram) def info(self) -> TrackInfo: """Return the publisher-side track properties.""" - return self._inner.info() + return TrackInfo._from_ffi(self._inner.info()) def update(self, subscription: Subscription) -> None: """Change this subscriber's delivery preferences.""" - self._inner.update(subscription) + self._inner.update(subscription._ffi()) def cancel(self) -> None: """Cancel the subscription and stop delivering groups.""" @@ -300,97 +229,6 @@ def cancel(self) -> None: self._inner.cancel() -class JsonSnapshotConsumer: - """Async iterator over a JSON snapshot track, yielding the latest value (lossy). - - Built via :meth:`BroadcastConsumer.subscribe_json_snapshot`. Each item is a parsed Python object. - A consumer that has fallen behind collapses the backlog and yields only the latest value. - Usable as an async context manager that cancels on exit. - """ - - def __init__(self, inner: MoqJsonSnapshotConsumer) -> None: - self._inner = inner - - async def __aenter__(self): - return self - - async def __aexit__(self, *exc) -> None: - self.cancel() - - def __aiter__(self): - return self - - async def __anext__(self) -> Any: - value = await self._inner.next() - if value is None: - raise StopAsyncIteration - return json.loads(value) - - def cancel(self) -> None: - """Cancel all current and future next() calls.""" - self._inner.cancel() - - -class JsonStreamConsumer: - """Async iterator over a JSON stream track, yielding every record in order (lossless). - - Built via :meth:`BroadcastConsumer.subscribe_json_stream`. Each item is a parsed Python object. - Usable as an async context manager that cancels on exit. - """ - - def __init__(self, inner: MoqJsonStreamConsumer) -> None: - self._inner = inner - - async def __aenter__(self): - return self - - async def __aexit__(self, *exc) -> None: - self.cancel() - - def __aiter__(self): - return self - - async def __anext__(self) -> Any: - value = await self._inner.next() - if value is None: - raise StopAsyncIteration - return json.loads(value) - - def cancel(self) -> None: - """Cancel all current and future next() calls.""" - self._inner.cancel() - - -class CatalogConsumer: - """Async-iterable stream of :class:`Catalog` snapshots as the broadcast updates. - - Built via :meth:`BroadcastConsumer.subscribe_catalog`. Each item is the latest - catalog describing the broadcast's tracks; usable as an async context manager. - """ - - def __init__(self, inner: MoqCatalogConsumer) -> None: - self._inner = inner - - async def __aenter__(self): - return self - - async def __aexit__(self, *exc) -> None: - self.cancel() - - def __aiter__(self): - return self - - async def __anext__(self) -> Catalog: - catalog = await self._inner.next() - if catalog is None: - raise StopAsyncIteration - return catalog - - def cancel(self) -> None: - """Cancel the catalog subscription and stop delivering updates.""" - self._inner.cancel() - - class BroadcastConsumer: """The consume side of one broadcast: subscribe to its tracks, catalog, and media. @@ -401,33 +239,12 @@ class BroadcastConsumer: def __init__(self, inner: MoqBroadcastConsumer) -> None: self._inner = inner - async def subscribe_catalog(self) -> CatalogConsumer: - """Subscribe to the broadcast's catalog, async-iterating snapshots as it changes.""" - return CatalogConsumer(await self._inner.subscribe_catalog()) - async def subscribe_track(self, name: str, subscription: Subscription | None = None) -> TrackConsumer: """Subscribe to a track and receive arbitrary byte payloads. ``subscription`` tunes delivery priority, group range, and staleness; omit for defaults. """ - return TrackConsumer(await self._inner.subscribe_track(name, subscription)) - - async def subscribe_json_snapshot(self, name: str, *, compression: bool = False) -> JsonSnapshotConsumer: - """Subscribe to a JSON snapshot track (lossy latest-value). - - Yields parsed Python objects. Pass the same ``compression`` the producer used. - """ - # delta_ratio is producer-only, so leave it at its default here. - config = MoqJsonSnapshotConfig(compression=compression) - return JsonSnapshotConsumer(await self._inner.subscribe_json_snapshot(name, config)) - - async def subscribe_json_stream(self, name: str, *, compression: bool = False) -> JsonStreamConsumer: - """Subscribe to a JSON stream track (lossless append-log). - - Yields parsed Python objects in order. Pass the same ``compression`` the producer used. - """ - config = MoqJsonStreamConfig(compression=compression) - return JsonStreamConsumer(await self._inner.subscribe_json_stream(name, config)) + return TrackConsumer(await self._inner.subscribe_track(name, _subscription(subscription))) async def fetch_group( self, @@ -442,43 +259,6 @@ async def fetch_group( """ return GroupConsumer(await self._inner.fetch_group(name, sequence, options)) - async def fetch_media_group( - self, - name: str, - sequence: int, - track: Video | Audio | Container, - options: FetchGroupOptions | None = None, - ) -> MediaGroupConsumer: - """Fetch one group by sequence and decode it into media frames. - - Unlike :meth:`subscribe_media` this holds no live subscription and applies - no latency-based group skipping, so every frame in the group is delivered. - ``track`` is either the catalog entry for this track (e.g. - ``catalog.video[name]``) or a :class:`Container` directly. - """ - container = track if isinstance(track, Container) else track.container - return MediaGroupConsumer(await self._inner.fetch_media_group(name, sequence, container, options)) - - async def subscribe_media( - self, - name: str, - track: Video | Audio | Container, - subscription: Subscription | None = None, - ) -> MediaConsumer: - """Subscribe to a media track, delivering frames in decode order. - - ``track`` is either the catalog entry for this track (e.g. - ``catalog.video[name]``), whose ``container`` describes how to parse the - bitstream, or a :class:`Container` directly. Pass a bare container for the - dynamic flow, where you subscribe before the catalog exists. - ``subscription`` tunes delivery priority, group - range, and the max delay; omit for defaults. Raise - :attr:`Subscription.max_delay_us` to buffer instead of skipping a - stalled group. - """ - container = track if isinstance(track, Container) else track.container - return MediaConsumer(await self._inner.subscribe_media(name, container, subscription)) - async def resolve(self, reference: str | None) -> "BroadcastConsumer": """Resolve a catalog rendition's ``broadcast`` reference to the broadcast serving its track. @@ -486,8 +266,8 @@ async def resolve(self, reference: str | None) -> "BroadcastConsumer": ``reference`` is ``catalog.video[name].broadcast`` / ``catalog.audio[name].broadcast``: ``None`` or empty names this broadcast, anything else names a sibling relative to it (e.g. ``./source``). Call it on - a rendition that carries one before :meth:`subscribe_media`, - :meth:`subscribe_track`, :meth:`fetch_group`, or :meth:`fetch_media_group`, + a rendition that carries one before :meth:`~moq.media.ContainerConsumer.subscribe`, + :meth:`subscribe_track`, :meth:`fetch_group`, or :meth:`~moq.media.ContainerGroupConsumer.fetch`, which take a track name rather than a rendition; :meth:`decode_audio` and :meth:`decode_video` resolve it themselves. @@ -506,7 +286,7 @@ async def decode_audio( declared by ``output``. ``catalog_audio`` comes from the catalog (e.g. - ``await broadcast.catalog()`` followed by + ``await moq.media.catalog(broadcast)`` followed by ``catalog.audio[name]``). Only Opus and AAC-LC tracks are supported; AAC is decode only, since nothing here encodes it. Use ``output.max_delay_us`` to @@ -527,7 +307,7 @@ async def decode_video( """Subscribe to a video track and decode it inside the bindings. ``catalog_video`` comes from the catalog (e.g. - ``await broadcast.catalog()`` followed by ``catalog.video[name]``). + ``await moq.media.catalog(broadcast)`` followed by ``catalog.video[name]``). An unrecognized codec raises here; a recognized one no native backend handles raises when the decoder opens, both before the first frame. @@ -538,8 +318,3 @@ async def decode_video( effort, so read each frame's own dimensions. """ return VideoConsumer(await self._inner.decode_video(name, catalog_video, output or VideoDecoderOutput())) - - async def catalog(self) -> Catalog: - """Convenience: subscribe and return the first catalog.""" - consumer = await self.subscribe_catalog() - return await anext(consumer) diff --git a/py/moq-rs/moq/types.py b/py/moq-rs/moq/types.py index 49123f14fdd..7ad85d765f3 100644 --- a/py/moq-rs/moq/types.py +++ b/py/moq-rs/moq/types.py @@ -1,8 +1,8 @@ -"""Re-export moq-ffi record types without the Moq prefix.""" +"""Re-export moq-ffi record types without the Moq prefix. + +Records with duration fields are owned in `_records` instead, so they read as `timedelta`. +""" -from moq_ffi import ( - MoqAudio as Audio, -) from moq_ffi import ( MoqAudioCodec as AudioCodec, ) @@ -15,51 +15,21 @@ from moq_ffi import ( MoqAudioEncoderOutput as AudioEncoderOutput, ) -from moq_ffi import ( - MoqAudioFormat as AudioFormat, -) from moq_ffi import ( MoqAudioFrame as AudioFrame, ) from moq_ffi import ( MoqAudioSampleFormat as AudioSampleFormat, ) -from moq_ffi import ( - MoqBackoff as Backoff, -) -from moq_ffi import ( - MoqCatalog as Catalog, -) -from moq_ffi import ( - MoqConnectionStats as ConnectionStats, -) from moq_ffi import ( MoqConnectionStatus as ConnectionStatus, ) -from moq_ffi import ( - MoqContainer as Container, -) -from moq_ffi import ( - MoqContainerFormat as ContainerFormat, -) -from moq_ffi import ( - MoqDatagram as Datagram, -) -from moq_ffi import ( - MoqDimensions as Dimensions, -) from moq_ffi import ( MoqErrorScope as ErrorScope, ) from moq_ffi import ( MoqFetchGroupOptions as FetchGroupOptions, ) -from moq_ffi import ( - MoqFrame as Frame, -) -from moq_ffi import ( - MoqMediaFrame as MediaFrame, -) from moq_ffi import ( MoqProtocolError as ProtocolError, ) @@ -69,15 +39,6 @@ from moq_ffi import ( MoqRoute as Route, ) -from moq_ffi import ( - MoqSubscription as Subscription, -) -from moq_ffi import ( - MoqTrackInfo as TrackInfo, -) -from moq_ffi import ( - MoqVideo as Video, -) from moq_ffi import ( MoqVideoCodec as VideoCodec, ) @@ -96,53 +57,34 @@ from moq_ffi import ( MoqVideoEncoderOutput as VideoEncoderOutput, ) -from moq_ffi import ( - MoqVideoFormat as VideoFormat, -) from moq_ffi import ( MoqVideoFrame as VideoFrame, ) -from moq_ffi import ( - MoqVideoHint as VideoHint, -) from moq_ffi import ( MoqVideoPixelFormat as VideoPixelFormat, ) -from moq_ffi import ( - MoqVideoProperties, -) -VideoProperties = MoqVideoProperties -"""Video catalog properties shared by every rendition; ``None`` fields clear them.""" +from ._records import Backoff, ConnectionStats, Datagram, Frame, Subscription, TrackInfo __all__ = [ - "Audio", "AudioCodec", "AudioDecoderOutput", "AudioEncoderInput", "AudioEncoderOutput", - "AudioFormat", "AudioSampleFormat", - "VideoFormat", - "ContainerFormat", "AudioFrame", "Backoff", - "Catalog", "ConnectionStats", "ConnectionStatus", - "Container", "Datagram", - "Dimensions", "ErrorScope", "Frame", "FetchGroupOptions", "ProtocolError", "ProtocolKind", - "MediaFrame", "Route", "Subscription", "TrackInfo", - "Video", "VideoCodec", "VideoEncoderInput", "VideoEncoderKind", @@ -150,7 +92,5 @@ "VideoFrame", "VideoDecodedFrame", "VideoDecoderOutput", - "VideoHint", "VideoPixelFormat", - "VideoProperties", ] diff --git a/py/moq-rs/tests/test_local.py b/py/moq-rs/tests/test_local.py index ac7ed9a3bb4..f69645b8611 100644 --- a/py/moq-rs/tests/test_local.py +++ b/py/moq-rs/tests/test_local.py @@ -2,9 +2,11 @@ import asyncio import struct +from datetime import timedelta from typing import cast import moq +import moq_ffi import pytest @@ -143,8 +145,10 @@ def test_error_str_is_rust_display(): def test_publish_media_lifecycle(): broadcast = moq.BroadcastProducer() - media = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head()) - media.write_frame(b"opus frame", 1000) + media = moq.media.TrackProducer.audio( + broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head()) + ) + media.write_frame(b"opus frame", timedelta(microseconds=1000)) media.finish() broadcast.close() @@ -156,17 +160,19 @@ def test_publish_media_cut_and_seek(): late subscribers and the timeline alike. """ broadcast = moq.BroadcastProducer() - media = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head()) + media = moq.media.TrackProducer.audio( + broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head()) + ) for i in range(3): - media.write_frame(b"opus frame", i * 20_000) + media.write_frame(b"opus frame", timedelta(microseconds=i * 20_000)) media.cut() # The same boundary, with the next group explicitly numbered. - media.write_frame(b"opus frame", 60_000) + media.write_frame(b"opus frame", timedelta(microseconds=60_000)) media.seek(42) media.discontinuity() - media.write_frame(b"resumed opus frame", 100_000) + media.write_frame(b"resumed opus frame", timedelta(microseconds=100_000)) media.finish() broadcast.close() @@ -174,10 +180,10 @@ def test_publish_media_cut_and_seek(): def test_video_properties_use_defaulted_fields(): broadcast = moq.BroadcastProducer() - properties = moq.VideoProperties(rotation=315.0) + properties = moq.media.VideoProperties(rotation=315.0) assert properties.display is None assert properties.flip is None - broadcast.set_video_properties(properties) + moq.media.CatalogProducer(broadcast).set_video_properties(properties) broadcast.close() @@ -186,13 +192,15 @@ def test_audio_rejects_bad_init_bytes(): # init bytes that aren't an OpusHead, which must fail at publish rather than on a frame. broadcast = moq.BroadcastProducer() with pytest.raises(Exception): - broadcast.publish_audio(moq.AudioFormat.OPUS, b"") + moq.media.TrackProducer.audio(broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=b"")) async def test_local_publish_consume_audio(): origin = moq.OriginProducer() broadcast = create_announced(origin, "live") - media = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head()) + media = moq.media.TrackProducer.audio( + broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head()) + ) consumer = origin.consume() @@ -200,7 +208,7 @@ async def test_local_publish_consume_audio(): assert announcement.prefix == "live" broadcast_consumer = await consumer.request_broadcast(announcement.prefix) - catalog = await broadcast_consumer.catalog() + catalog = await moq.media.catalog(broadcast_consumer) assert len(catalog.audio) == 1 assert len(catalog.video) == 0 @@ -211,14 +219,14 @@ async def test_local_publish_consume_audio(): assert audio.sample_rate == 48000 assert audio.channel_count == 2 - media_consumer = await broadcast_consumer.subscribe_media(track_name, audio) + media_consumer = await moq.media.ContainerConsumer.subscribe(broadcast_consumer, track_name, audio.container) payload = b"opus audio payload data" - media.write_frame(payload, 1_000_000) + media.write_frame(payload, timedelta(microseconds=1_000_000)) async for frame in media_consumer: assert frame.payload == payload - assert frame.timestamp_us == 1_000_000 + assert frame.timestamp // timedelta(microseconds=1) == 1_000_000 break break @@ -227,13 +235,15 @@ async def test_local_publish_consume_audio(): async def test_video_publish_consume(): origin = moq.OriginProducer() broadcast = create_announced(origin, "video-test") - media = broadcast.publish_video(moq.VideoFormat.AVC3, h264_init()) + media = moq.media.TrackProducer.video( + broadcast, moq.media.VideoInit(format=moq.media.VideoFormat.AVC3, data=h264_init()) + ) consumer = origin.consume() async for announcement in routes(consumer.announced()): broadcast_consumer = await consumer.request_broadcast(announcement.prefix) - catalog = await broadcast_consumer.catalog() + catalog = await moq.media.catalog(broadcast_consumer) assert len(catalog.video) == 1 assert len(catalog.audio) == 0 @@ -245,13 +255,13 @@ async def test_video_publish_consume(): assert video.coded.width == 1280 assert video.coded.height == 720 - media_consumer = await broadcast_consumer.subscribe_media(track_name, video) + media_consumer = await moq.media.ContainerConsumer.subscribe(broadcast_consumer, track_name, video.container) keyframe = bytes([0x00, 0x00, 0x00, 0x01, 0x65, 0xAA, 0xBB, 0xCC]) - media.write_frame(keyframe, 0) + media.write_frame(keyframe, timedelta(microseconds=0)) async for frame in media_consumer: - assert frame.timestamp_us == 0 + assert frame.timestamp // timedelta(microseconds=1) == 0 assert len(frame.payload) > 0 break @@ -261,14 +271,18 @@ async def test_video_publish_consume(): async def test_video_publish_named_track(): origin = moq.OriginProducer() broadcast = create_announced(origin, "video-named-test") - media = broadcast.publish_video(moq.VideoFormat.AVC3, h264_init(), track="hd") - assert media.name == "hd" + media = moq.media.TrackProducer.video( + broadcast, + moq.media.VideoInit(format=moq.media.VideoFormat.AVC3, data=h264_init()), + target=moq.media.Named("hd"), + ) + assert media.demand().name == "hd" consumer = origin.consume() async for announcement in routes(consumer.announced()): broadcast_consumer = await consumer.request_broadcast(announcement.prefix) - catalog = await broadcast_consumer.catalog() + catalog = await moq.media.catalog(broadcast_consumer) assert list(catalog.video.keys()) == ["hd"] break @@ -276,24 +290,26 @@ async def test_video_publish_named_track(): async def test_multiple_frames_ordering(): origin = moq.OriginProducer() broadcast = create_announced(origin, "ordering-test") - media = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head()) + media = moq.media.TrackProducer.audio( + broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head()) + ) consumer = origin.consume() async for announcement in routes(consumer.announced()): broadcast_consumer = await consumer.request_broadcast(announcement.prefix) - catalog = await broadcast_consumer.catalog() + catalog = await moq.media.catalog(broadcast_consumer) track_name = list(catalog.audio.keys())[0] audio = catalog.audio[track_name] - media_consumer = await broadcast_consumer.subscribe_media(track_name, audio) + media_consumer = await moq.media.ContainerConsumer.subscribe(broadcast_consumer, track_name, audio.container) timestamps = [0, 20_000, 40_000, 60_000, 80_000] for i, ts in enumerate(timestamps): - media.write_frame(f"frame-{i}".encode(), ts) + media.write_frame(f"frame-{i}".encode(), timedelta(microseconds=ts)) for i, expected_ts in enumerate(timestamps): async for frame in media_consumer: - assert frame.timestamp_us == expected_ts + assert frame.timestamp // timedelta(microseconds=1) == expected_ts assert frame.payload == f"frame-{i}".encode() break @@ -303,20 +319,24 @@ async def test_multiple_frames_ordering(): async def test_catalog_update_on_new_track(): origin = moq.OriginProducer() broadcast = create_announced(origin, "catalog-update") - _media1 = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head()) + _media1 = moq.media.TrackProducer.audio( + broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head()) + ) consumer = origin.consume() async for announcement in routes(consumer.announced()): broadcast_consumer = await consumer.request_broadcast(announcement.prefix) - cat_consumer = await broadcast_consumer.subscribe_catalog() + cat_consumer = await moq.media.CatalogConsumer.subscribe(broadcast_consumer) # First catalog: 1 audio track. catalog1 = await anext(cat_consumer) assert len(catalog1.audio) == 1 # Add a second audio track, which triggers a catalog update. - _media2 = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head()) + _media2 = moq.media.TrackProducer.audio( + broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head()) + ) catalog2 = await anext(cat_consumer) assert len(catalog2.audio) == 2 @@ -326,12 +346,16 @@ async def test_catalog_update_on_new_track(): def test_close_twice_is_a_noop(): broadcast = moq.BroadcastProducer() - _media = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head()) + _media = moq.media.TrackProducer.audio( + broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head()) + ) broadcast.close() broadcast.close() with pytest.raises(Exception): - broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head()) + moq.media.TrackProducer.audio( + broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head()) + ) async def test_announced_broadcast(): @@ -343,14 +367,14 @@ async def test_announced_broadcast(): async for announcement in routes(consumer.announced()): assert announcement.prefix == "test/broadcast" broadcast_consumer = await consumer.request_broadcast(announcement.prefix) - _catalog = await broadcast_consumer.subscribe_catalog() + _catalog = await moq.media.CatalogConsumer.subscribe(broadcast_consumer) break def test_publish_lifecycle(): broadcast = moq.BroadcastProducer() track = broadcast.publish_track("status") - track.write_frame(b'{"cmd": "ready"}', 0) + track.write_frame(b'{"cmd": "ready"}') track.finish() broadcast.close() @@ -358,11 +382,11 @@ def test_publish_lifecycle(): async def test_publish_track_info_and_subscription(): """Raw track published with explicit TrackInfo, consumed with a Subscription.""" broadcast = moq.BroadcastProducer() - info = moq.TrackInfo(priority=5, max_age_us=2_000_000) + info = moq.TrackInfo(priority=5, max_age=timedelta(seconds=2)) track = broadcast.publish_track("status", info) consumer = track.consume(moq.Subscription(priority=3)) - track.write_frame(b"ready", 0) + track.write_frame(b"ready") frame = await asyncio.wait_for(consumer.read_frame(), timeout=5.0) assert frame is not None @@ -370,6 +394,22 @@ async def test_publish_track_info_and_subscription(): track.finish() +def test_track_info_keeps_native_default_priority(): + """A partially configured TrackInfo keeps moq-ffi's default priority of 127.""" + native = moq_ffi.MoqTrackInfo().priority + assert moq.TrackInfo()._ffi().priority == native + assert moq.TrackInfo(max_age=timedelta(seconds=2))._ffi().priority == native + assert moq.TrackInfo(timescale=90_000)._ffi().priority == native + + +def test_string_sequences_refuse_a_bare_string(): + """A bare str would split into characters, so the config refuses it up front.""" + with pytest.raises(TypeError, match="tls_roots"): + moq.Client("https://localhost:4443", tls_roots="ca.pem") + with pytest.raises(TypeError, match="versions"): + moq.Server(versions="moq-lite-03") + + async def test_fetch_group_and_serve_dynamic_miss(): """Fetch a cached group, then serve an uncached sequence through TrackDynamic.""" broadcast = moq.BroadcastProducer() @@ -377,7 +417,7 @@ async def test_fetch_group_and_serve_dynamic_miss(): consumer = broadcast.consume() cached = track.append_group() - cached.write_frame(b"cached", 0) + cached.write_frame(b"cached") cached.finish() fetched = await consumer.fetch_group("events", 0, moq.FetchGroupOptions(priority=3)) @@ -391,7 +431,7 @@ async def test_fetch_group_and_serve_dynamic_miss(): assert request.priority == 11 produced = request.accept() - produced.write_frame(b"archive", 140_000) + produced.write_frame(b"archive", timedelta(microseconds=140_000)) produced.finish() fetched = await asyncio.wait_for(pending, timeout=5.0) @@ -400,8 +440,8 @@ async def test_fetch_group_and_serve_dynamic_miss(): async def test_json_snapshot_roundtrip(): broadcast = moq.BroadcastProducer() - producer = broadcast.publish_json_snapshot("status", compression=True) - consumer = await broadcast.consume().subscribe_json_snapshot("status", compression=True) + producer = moq.json.SnapshotProducer(broadcast, broadcast.publish_track("status"), compression=True) + consumer = moq.json.SnapshotConsumer(await broadcast.consume().subscribe_track("status"), compression=True) producer.update({"state": "live", "viewers": 1}) value = await asyncio.wait_for(anext(consumer), timeout=5.0) @@ -416,8 +456,8 @@ async def test_json_snapshot_roundtrip(): async def test_json_stream_roundtrip(): broadcast = moq.BroadcastProducer() - producer = broadcast.publish_json_stream("events") - consumer = await broadcast.consume().subscribe_json_stream("events") + producer = moq.json.StreamProducer(broadcast, broadcast.publish_track("events")) + consumer = moq.json.StreamConsumer(await broadcast.consume().subscribe_track("events")) for n in range(3): producer.append({"n": n}) @@ -429,16 +469,16 @@ async def test_json_stream_roundtrip(): async def test_json_producers_report_demand(): broadcast = moq.BroadcastProducer() - snapshot = broadcast.publish_json_snapshot("status", compression=True) - stream = broadcast.publish_json_stream("events") + snapshot = moq.json.SnapshotProducer(broadcast, broadcast.publish_track("status"), compression=True) + stream = moq.json.StreamProducer(broadcast, broadcast.publish_track("events")) snapshot_demand = snapshot.demand() stream_demand = stream.demand() assert snapshot_demand.name == "status" assert not snapshot_demand.is_used() consumer = broadcast.consume() - snapshot_consumer = await consumer.subscribe_json_snapshot("status", compression=True) - stream_consumer = await consumer.subscribe_json_stream("events") + snapshot_consumer = moq.json.SnapshotConsumer(await consumer.subscribe_track("status"), compression=True) + stream_consumer = moq.json.StreamConsumer(await consumer.subscribe_track("events")) await asyncio.wait_for(snapshot_demand.used(), timeout=5.0) await asyncio.wait_for(stream_demand.used(), timeout=5.0) assert snapshot_demand.is_used() @@ -467,7 +507,7 @@ async def test_dynamic_track_request(): # Accept the request as a raw track (which unblocks the subscribe), then write. track = request.accept() payload = b"hello dynamic track" - track.write_frame(payload, 0) + track.write_frame(payload) track_consumer = await asyncio.wait_for(subscribe, timeout=5.0) frame = await asyncio.wait_for(track_consumer.read_frame(), timeout=5.0) @@ -481,19 +521,25 @@ async def test_dynamic_track_request_can_publish_media(): broadcast = moq.BroadcastProducer() dynamic = broadcast.dynamic() consumer = broadcast.consume() - catalog_consumer = await consumer.subscribe_catalog() + catalog_consumer = await moq.media.CatalogConsumer.subscribe(consumer) - # publish_audio_on_track accepts the request (at the media timescale), which is what - # unblocks subscribe_media, so run the subscribe concurrently until then. + # TrackProducer.audio with Requested accepts the request (at the media timescale), which is what + # unblocks ContainerConsumer.subscribe, so run the subscribe concurrently until then. subscribe = asyncio.create_task( - consumer.subscribe_media("requested-audio", cast(moq.Container, moq.Container.LEGACY())) + moq.media.ContainerConsumer.subscribe( + consumer, "requested-audio", cast(moq.media.Container, moq.media.Container.LEGACY()) + ) ) track = await asyncio.wait_for(dynamic.requested_track(), timeout=5.0) assert track.name == "requested-audio" - media = broadcast.publish_audio_on_track(track, moq.AudioFormat.OPUS, opus_head()) - assert media.name == "requested-audio" + media = moq.media.TrackProducer.audio( + broadcast, + moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head()), + target=moq.media.Requested(track), + ) + assert media.demand().name == "requested-audio" with pytest.raises(Exception): _ = track.name @@ -506,11 +552,11 @@ async def test_dynamic_track_request_can_publish_media(): assert audio.channel_count == 2 payload = b"dynamic opus frame" - media.write_frame(payload, 20_000) + media.write_frame(payload, timedelta(microseconds=20_000)) async for frame in media_consumer: assert frame.payload == payload - assert frame.timestamp_us == 20_000 + assert frame.timestamp // timedelta(microseconds=1) == 20_000 break media.finish() @@ -535,12 +581,12 @@ async def test_dynamic_broadcast_request(): broadcast = await asyncio.wait_for(request_broadcast, timeout=5.0) track_consumer = await broadcast.subscribe_track("status") payload = b"served dynamically" - track.write_frame(payload, 20_000) + track.write_frame(payload, timedelta(microseconds=20_000)) frame = await asyncio.wait_for(track_consumer.read_frame(), timeout=5.0) assert frame is not None assert frame.payload == payload - assert frame.timestamp_us == 20_000 + assert frame.timestamp == timedelta(microseconds=20_000) track.finish() served.close() @@ -583,7 +629,7 @@ def test_raw_group_write_multiple_frames(): group = track.append_group() for i in range(10): - group.write_frame(f"frame-{i}".encode(), i) + group.write_frame(f"frame-{i}".encode(), timedelta(microseconds=i)) group.finish() @@ -593,7 +639,7 @@ def test_raw_group_empty_payload(): track = broadcast.publish_track("empty") group = track.append_group() - group.write_frame(b"", 0) + group.write_frame(b"") group.finish() @@ -604,7 +650,7 @@ def test_raw_group_write_after_finish_fails(): group.finish() with pytest.raises(Exception): - group.write_frame(b"too late", 0) + group.write_frame(b"too late") def test_raw_group_abort_after_finish(): @@ -628,7 +674,7 @@ def test_raw_track_write_after_finish_fails(): track.finish() with pytest.raises(Exception): - track.write_frame(b"late", 0) + track.write_frame(b"late") def test_raw_sparse_groups_and_known_end(): @@ -660,9 +706,9 @@ def test_raw_parallel_groups(): assert g0.sequence == 0 assert g1.sequence == 1 - g0.write_frame(b"a0", 0) - g1.write_frame(b"b0", 0) - g0.write_frame(b"a1", 1) + g0.write_frame(b"a0") + g1.write_frame(b"b0") + g0.write_frame(b"a1", timedelta(microseconds=1)) g0.finish() g1.finish() @@ -682,29 +728,33 @@ def test_public_api_exports(): tls_roots=["root.pem"], tls_fingerprints=["abc123"], ) - assert client._tls_roots == ["root.pem"] - assert client._tls_fingerprints == ["abc123"] + assert client._config.tls.roots == ["root.pem"] + assert client._config.tls.fingerprints == ["abc123"] -async def test_subscribe_media_default_latency_and_context_manager(): - """subscribe_media takes the catalog record directly and defaults the +async def test_container_consumer_default_latency_and_context_manager(): + """ContainerConsumer.subscribe defaults the latency; the returned consumer is also an async context manager.""" origin = moq.OriginProducer() broadcast = create_announced(origin, "live") - media = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head()) + media = moq.media.TrackProducer.audio( + broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head()) + ) consumer = origin.consume() async for announcement in routes(consumer.announced()): broadcast_consumer = await consumer.request_broadcast(announcement.prefix) - catalog = await broadcast_consumer.catalog() + catalog = await moq.media.catalog(broadcast_consumer) track_name, audio = next(iter(catalog.audio.items())) # No container argument, no explicit latency. payload = b"opus audio payload data" - media.write_frame(payload, 1_000_000) + media.write_frame(payload, timedelta(microseconds=1_000_000)) - async with await broadcast_consumer.subscribe_media(track_name, audio) as media_consumer: + async with await moq.media.ContainerConsumer.subscribe( + broadcast_consumer, track_name, audio.container + ) as media_consumer: async for frame in media_consumer: assert frame.payload == payload break @@ -726,7 +776,7 @@ async def test_raw_publish_consume(): raw_consumer = await broadcast_consumer.subscribe_track("events") payload = b'{"cmd": "button_changed", "arm": "left", "button": "THUMB", "state": "PRESSED"}' - raw.write_frame(payload, 0) + raw.write_frame(payload) async for group in raw_consumer: async for frame in group: @@ -746,7 +796,8 @@ async def test_raw_multiple_frames(): async for announcement in routes(consumer.announced()): broadcast_consumer = await consumer.request_broadcast(announcement.prefix) - raw_consumer = await broadcast_consumer.subscribe_track("commands", moq.Subscription(max_delay_us=1_000_000)) + subscription = moq.Subscription(max_delay=timedelta(seconds=1)) + raw_consumer = await broadcast_consumer.subscribe_track("commands", subscription) messages = [ b'{"cmd": "led", "arm": "left", "led": "THUMB", "state": 1}', @@ -754,7 +805,7 @@ async def test_raw_multiple_frames(): b'{"cmd": "tone_stop", "arm": "right"}', ] for msg in messages: - raw.write_frame(msg, 0) + raw.write_frame(msg) received = [] async for group in raw_consumer: @@ -771,10 +822,10 @@ async def test_raw_producer_consume_direct(): """Consume a raw track directly from the producer, no origin/broadcast plumbing.""" broadcast = moq.BroadcastProducer() track = broadcast.publish_track("direct") - consumer = track.consume(moq.Subscription(max_delay_us=1_000_000)) + consumer = track.consume(moq.Subscription(max_delay=timedelta(seconds=1))) - track.write_frame(b"hello", 0) - track.write_frame(b"world", 0) + track.write_frame(b"hello") + track.write_frame(b"world") received = [] async for group in consumer: @@ -794,8 +845,8 @@ async def test_raw_group_producer_consume_direct(): group_consumer = group.consume() assert group_consumer.sequence == group.sequence - group.write_frame(b"a", 0) - group.write_frame(b"b", 0) + group.write_frame(b"a") + group.write_frame(b"b") group.finish() received = [frame.payload async for frame in group_consumer] @@ -809,7 +860,7 @@ async def test_broadcast_producer_consume_direct(): consumer = broadcast.consume() raw_consumer = await consumer.subscribe_track("events") - raw.write_frame(b"event-0", 0) + raw.write_frame(b"event-0") async for group in raw_consumer: async for frame in group: @@ -828,13 +879,13 @@ async def test_raw_group_sequence(): async for announcement in routes(consumer.announced()): broadcast_consumer = await consumer.request_broadcast(announcement.prefix) - raw_consumer = await broadcast_consumer.subscribe_track("seq", moq.Subscription(max_delay_us=1_000_000)) + raw_consumer = await broadcast_consumer.subscribe_track("seq", moq.Subscription(max_delay=timedelta(seconds=1))) sent_sequences = [] for i in range(3): group = raw.append_group() sent_sequences.append(group.sequence) - group.write_frame(f"msg-{i}".encode(), i) + group.write_frame(f"msg-{i}".encode(), timedelta(microseconds=i)) group.finish() received_sequences = [] @@ -859,13 +910,13 @@ async def test_default_iteration_is_sequence_order(): broadcast = create_announced(origin, "track/ordering") raw = broadcast.publish_track("ordering") - subscription = moq.Subscription(max_delay_us=1_000_000) + subscription = moq.Subscription(max_delay=timedelta(seconds=1)) seq_consumer = raw.consume(subscription) arr_consumer = raw.consume(subscription) for sequence in (5, 3): group = raw.create_group(sequence) - group.write_frame(f"group-{sequence}".encode(), 0) + group.write_frame(f"group-{sequence}".encode()) group.finish() # Arrival order sees them as produced, newest sequence first. @@ -900,7 +951,7 @@ async def test_raw_multi_frame_group(): group_producer = raw.append_group() chunks = [b"chunk-0", b"chunk-1", b"chunk-2"] for chunk in chunks: - group_producer.write_frame(chunk, 0) + group_producer.write_frame(chunk) group_producer.finish() async for group in raw_consumer: @@ -915,11 +966,11 @@ async def test_read_frame_one_per_group(): """read_frame() returns the first frame of each successive group.""" broadcast = moq.BroadcastProducer() track = broadcast.publish_track("status") - consumer = track.consume(moq.Subscription(max_delay_us=1_000_000)) + consumer = track.consume(moq.Subscription(max_delay=timedelta(seconds=1))) - track.write_frame(b"ready", 0) - track.write_frame(b"running", 0) - track.write_frame(b"done", 0) + track.write_frame(b"ready") + track.write_frame(b"running") + track.write_frame(b"done") frame = await consumer.read_frame() assert frame is not None @@ -938,35 +989,35 @@ async def test_raw_read_frame_preserves_timestamp(): track = broadcast.publish_track("status") consumer = track.consume() - track.write_frame(b"ready", 12_345) + track.write_frame(b"ready", timedelta(microseconds=12_345)) frame = await consumer.read_frame() assert frame is not None assert frame.payload == b"ready" - assert frame.timestamp_us == 12_345 + assert frame.timestamp == timedelta(microseconds=12_345) group = track.append_group() group_consumer = group.consume() - group.write_frame(b"group", 23_456) + group.write_frame(b"group", timedelta(microseconds=23_456)) group.finish() frame = await group_consumer.read_frame() assert frame is not None assert frame.payload == b"group" - assert frame.timestamp_us == 23_456 + assert frame.timestamp == timedelta(microseconds=23_456) async def test_read_frame_skips_remaining_frames_in_group(): """read_frame() only returns the first frame of a multi-frame group.""" broadcast = moq.BroadcastProducer() track = broadcast.publish_track("mixed") - consumer = track.consume(moq.Subscription(max_delay_us=1_000_000)) + consumer = track.consume(moq.Subscription(max_delay=timedelta(seconds=1))) group = track.append_group() - group.write_frame(b"first", 0) - group.write_frame(b"second-ignored", 0) + group.write_frame(b"first") + group.write_frame(b"second-ignored") group.finish() - track.write_frame(b"next-group-first", 0) + track.write_frame(b"next-group-first") frame = await consumer.read_frame() assert frame is not None @@ -982,7 +1033,7 @@ async def test_read_frame_returns_none_when_track_finished(): track = broadcast.publish_track("done") consumer = track.consume() - track.write_frame(b"only", 0) + track.write_frame(b"only") track.finish() frame = await consumer.read_frame() @@ -1003,11 +1054,11 @@ async def test_read_frame_skips_empty_group_on_open_track(): await asyncio.sleep(0.05) assert not read.done(), "empty group must not end an open track" - track.write_frame(b"after-empty", 1_000) + track.write_frame(b"after-empty", timedelta(microseconds=1_000)) frame = await asyncio.wait_for(read, timeout=5.0) assert frame is not None assert frame.payload == b"after-empty" - assert frame.timestamp_us == 1_000 + assert frame.timestamp == timedelta(microseconds=1_000) async def test_read_frame_skips_empty_then_populated_groups(): @@ -1018,12 +1069,12 @@ async def test_read_frame_skips_empty_then_populated_groups(): track.append_group().finish() track.append_group().finish() - track.write_frame(b"populated", 2_000) + track.write_frame(b"populated", timedelta(microseconds=2_000)) frame = await asyncio.wait_for(consumer.read_frame(), timeout=5.0) assert frame is not None assert frame.payload == b"populated" - assert frame.timestamp_us == 2_000 + assert frame.timestamp == timedelta(microseconds=2_000) async def test_read_frame_keeps_group_across_cancelled_call(): @@ -1040,20 +1091,20 @@ async def test_read_frame_keeps_group_across_cancelled_call(): with pytest.raises(asyncio.CancelledError): await read - group.write_frame(b"kept", 3_000) + group.write_frame(b"kept", timedelta(microseconds=3_000)) group.finish() track.finish() frame = await asyncio.wait_for(consumer.read_frame(), timeout=5.0) assert frame is not None assert frame.payload == b"kept" - assert frame.timestamp_us == 3_000 + assert frame.timestamp == timedelta(microseconds=3_000) assert await asyncio.wait_for(consumer.read_frame(), timeout=5.0) is None def test_optional_binding_records_use_none_defaults(): """Optional UniFFI record fields are optional in generated constructors.""" - hint = moq.VideoHint() + hint = moq.media.VideoHint() assert hint.coded is None assert hint.display_aspect is None assert hint.bitrate is None @@ -1124,7 +1175,7 @@ async def test_decode_video_frame(): consumer = origin.consume() broadcast_consumer = await asyncio.wait_for(consumer.request_broadcast("video-decode-frame"), timeout=5.0) - catalog = await asyncio.wait_for(broadcast_consumer.catalog(), timeout=5.0) + catalog = await asyncio.wait_for(moq.media.catalog(broadcast_consumer), timeout=5.0) track_name = next(iter(catalog.video)) rendition = catalog.video[track_name] @@ -1240,13 +1291,13 @@ async def assert_cancelled(awaitable) -> None: pass await assert_cancelled(track_dynamic.requested_group()) - snapshot = broadcast.publish_json_snapshot("state") - async with await broadcast.consume().subscribe_json_snapshot("state") as snapshot_consumer: + snapshot = moq.json.SnapshotProducer(broadcast, broadcast.publish_track("state")) + async with moq.json.SnapshotConsumer(await broadcast.consume().subscribe_track("state")) as snapshot_consumer: pass await assert_cancelled(anext(snapshot_consumer)) - stream = broadcast.publish_json_stream("log") - async with await broadcast.consume().subscribe_json_stream("log") as stream_consumer: + stream = moq.json.StreamProducer(broadcast, broadcast.publish_track("log")) + async with moq.json.StreamConsumer(await broadcast.consume().subscribe_track("log")) as stream_consumer: pass await assert_cancelled(anext(stream_consumer)) @@ -1254,3 +1305,12 @@ async def assert_cancelled(awaitable) -> None: stream.finish() track.finish() broadcast.close() + + +def test_media_catalog_handle_closes_with_broadcast(): + broadcast = moq.BroadcastProducer() + catalog = moq.media.CatalogProducer(broadcast) + catalog.set_section("app", {"value": 42}) + broadcast.close() + with pytest.raises(moq.Error.Closed): # type: ignore[attr-defined] + catalog.remove_section("app") diff --git a/py/moq-rs/tests/test_server.py b/py/moq-rs/tests/test_server.py index c981e9785f7..37d6df0feca 100644 --- a/py/moq-rs/tests/test_server.py +++ b/py/moq-rs/tests/test_server.py @@ -37,7 +37,9 @@ async def test_server_client_roundtrip(): async with moq.Server("127.0.0.1:0", tls_generate=["localhost"]) as server: # Publish a broadcast on the server side. broadcast = server.create_broadcast("hello") - media = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head()) + media = moq.media.TrackProducer.audio( + broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head()) + ) broadcast.announce() # Auto-accept incoming sessions in the background so the handshake @@ -62,18 +64,20 @@ async def accept_loop() -> None: assert announcement.prefix == "hello" broadcast_consumer = await client.request_broadcast(announcement.prefix) - catalog = await broadcast_consumer.catalog() + catalog = await moq.media.catalog(broadcast_consumer) track_name, audio = next(iter(catalog.audio.items())) assert audio.codec == "opus" - media_consumer = await broadcast_consumer.subscribe_media(track_name, audio) + media_consumer = await moq.media.ContainerConsumer.subscribe( + broadcast_consumer, track_name, audio.container + ) payload = b"hello over the wire" - media.write_frame(payload, 1_000_000) + media.write_frame(payload, timedelta(microseconds=1_000_000)) async for frame in media_consumer: assert frame.payload == payload - assert frame.timestamp_us == 1_000_000 + assert frame.timestamp // timedelta(microseconds=1) == 1_000_000 break break @@ -114,7 +118,12 @@ async def accept_loop() -> None: tls_verify=False, bind="127.0.0.1:0", # Fast retries so the test doesn't wait out the default 1s backoff. - backoff=moq.Backoff(initial_us=50_000, multiplier=2, max_us=200_000, timeout_us=0), + backoff=moq.Backoff( + initial=timedelta(milliseconds=50), + multiplier=2, + max=timedelta(milliseconds=200), + timeout=timedelta(0), + ), ) as client: session = client.session assert session is not None @@ -169,12 +178,15 @@ async def reject_loop() -> None: reject_task = asyncio.create_task(reject_loop()) try: - client = moq_ffi.MoqClient() - 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. - client.set_reconnect(False) + client = moq_ffi.MoqClient( + moq_ffi.MoqClientConfig( + bind="127.0.0.1:0", + tls=moq_ffi.MoqClientTls(insecure=True), + # One-shot, so this dial's outcome is what surfaces here rather than + # whatever the reconnect loop eventually reports. + once=True, + ) + ) # The rejection races the optimistic connect: it surfaces either as a # connect error or as the session's terminal close. MoqError is an # Exception subclass at runtime; UniFFI's generated code rebinds the @@ -224,15 +236,14 @@ async def accept_loop() -> None: moq.Client("https://localhost", websocket_delay=timedelta(milliseconds=-1)) -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_verify(False) - client.cancel() - with pytest.raises(moq_ffi.MoqError.Cancelled): # type: ignore[misc] - client.set_tls_verify(True) - with pytest.raises(moq_ffi.MoqError.Cancelled): # type: ignore[misc] - client.set_bind("127.0.0.1:0") +async def test_invalid_config_fails_on_enter(): + """A value the native side cannot use fails the connect or listen, not a later call.""" + with pytest.raises(moq.Error.Config): # type: ignore[attr-defined] + async with moq.Client("https://localhost", versions=["moq-lite-99"]): + pass + with pytest.raises(moq.Error.Config): # type: ignore[attr-defined] + async with moq.Server("not-an-address", tls_generate=["localhost"]): + pass async def test_cert_fingerprints_after_listen(): @@ -418,7 +429,7 @@ async def drain(reader: moq.TrackConsumer) -> None: consumer = await asyncio.wait_for(consume.request_broadcast("live"), timeout=5.0) reader = await asyncio.wait_for(consumer.subscribe_track("data"), timeout=5.0) reading.append(asyncio.create_task(drain(reader))) - await asyncio.wait_for(track.used(), timeout=5.0) + await asyncio.wait_for(track.demand().used(), timeout=5.0) raise ZeroDivisionError("boom") # The error exits the context without draining, but the session is over. assert session is not None diff --git a/quest/m0/broadcast-epoch/bindings.md b/quest/m0/broadcast-epoch/bindings.md index 9dde2edea77..957193588e6 100644 --- a/quest/m0/broadcast-epoch/bindings.md +++ b/quest/m0/broadcast-epoch/bindings.md @@ -25,10 +25,11 @@ audio timeline re-anchor `reset_epoch` are renamed so "epoch" has one meaning. Decided in the 2026-09-30 audit: libmoq is frozen (renamed `rs/moq-c`), so C and C++ consumers get epochs from moq-ffi. -Decided in the 2026-10-05 audit: this lands before the -[FFI shape](/quest/m1/ffi-shape/README.md) line (#4519), which reshapes the -same wrappers and keeps `epoch()` today. It rebases onto this quest and -adopts the epoch surface and the rename, so the wrappers break once each. +Reversed 2026-10-09 by the maintainer: the +[FFI shape](/quest/m1/ffi-shape/README.md) line (#4519) landed first, so this +builds on its reshaped wrappers (`MoqClientConfig`, the `media` namespace) +instead. Its `epoch()` rename should still reach the same binding release as +the line's [Codecs](/quest/m1/ffi-shape/codec.md), so the wrappers break once. ## Required diff --git a/quest/m1/cpp-generated-shape.md b/quest/m1/cpp-generated-shape.md index fc6a7d3e1e0..454ac74df48 100644 --- a/quest/m1/cpp-generated-shape.md +++ b/quest/m1/cpp-generated-shape.md @@ -20,10 +20,9 @@ place that names the pin. `cpp/moq/uniffi.toml` strips `Moq`. Delete the alias list in `cpp/moq/include/moq/moq.hpp` and the `just cpp check` audit of it, and drop the "each generated `moq::MoqFoo` is also `moq::Foo`" lines from - `doc/lib/cpp/index.md` and `cpp/moq/README.md`. The ffi-shape line ports - the hand-kept aliases today - ([The bindings mirror Rust's layers](/quest/m1/ffi-shape/README.md)), so - update that note when this lands. + `doc/lib/cpp/index.md` and `cpp/moq/README.md`. Then update the C++ note + in [The bindings mirror Rust's layers](/quest/m1/ffi-shape/README.md), + which keeps C++ flat with `Media*` names until this lands. - **expected**: the fork gains an option that always uses the bundled `tl::expected`. Delete the `MOQ_ABI` guard in `moq.hpp` and `src/moq.cpp`, the "C++ standard" sections of `doc/lib/cpp/index.md` and diff --git a/quest/m1/ffi-shape/README.md b/quest/m1/ffi-shape/README.md index ea33aa23c9d..e226be5520b 100644 --- a/quest/m1/ffi-shape/README.md +++ b/quest/m1/ffi-shape/README.md @@ -10,20 +10,27 @@ and verb maps to a Rust one. A docs page shows the layers in each language. ## Plan -Lands after the release, as one binding break in Python, Go, Swift, Kotlin, -and Dart. Dart is published now (`moq` 0.1.0 and `moq_ffi` 0.4.x on pub.dev), -so its renames get the same upgrade note as the others. +The line landed on `main` in #4519 (2026-10-09) with its json/flate, net, +media, and request-accept children, and the remaining children PR straight to +`main`. Decided by the maintainer, 2026-10-09: -Decided in the 2026-10-05 audit: - -- The m0 [Bindings](/quest/m0/broadcast-epoch/bindings.md) quest lands - first. This line rebases onto it and adopts its epoch surface and the - `session.epoch()` rename, rather than renaming again. -- The C++ package landed first (#4079, reversing the 2026-10-05 order so - the cpp questline could retire), so this line also ports `cpp/moq` - (including the hand-kept `moq::` aliases in `cpp/moq/include/moq/moq.hpp`, - which `just cpp check` audits), `cpp/obs`, and the C++ interop client - onto the reshaped moq-ffi. +- Land now rather than keep collecting on the line branch, because questline + branches are retiring ([Flat questlines](/quest/m1/quest-flat-lines.md)) and + each `main` merge meant hand-porting moq-ffi changes onto moved code. +- The bindings still break once per release, not once per merge: binding + releases (moq-ffi and the Python, Go, Swift, Kotlin, Dart, and C++ + packages) wait until [Codecs](/quest/m1/ffi-shape/codec.md) merges. Codecs + ranks first below for that reason, and the other breaking children + ([Bindings](/quest/m0/broadcast-epoch/bindings.md)' `epoch()` rename and + [named error fields](/quest/m1/ffi-shape/error-fields.md)) should land + before that release too. Dart is published (`moq` 0.1.0, `moq_ffi` 0.4.x), so + its renames get the same upgrade notes as the others. +- Bindings no longer lands first (reversing the 2026-10-05 audit): it adopts + the reshaped wrappers on `main` instead of this line rebasing onto it. +- The C++ package (#4079) landed first, so #4519 ported `cpp/moq` (including + the hand-kept `moq::` aliases `just cpp check` audits), `cpp/obs`, and the + C++ interop client. C++ stays a flat `moq::` namespace with `Media*` type + names until [unprefixed names](/quest/m1/cpp-generated-shape.md) lands. Settled shape: @@ -31,22 +38,23 @@ Settled shape: origin, broadcast, track, group); `media` merges hang and moq-mux, since a binding never sees that split (catalog, import producers, container consumers); `json`, `flate`, `audio`, and `video` own their producers and - consumers. `flate` holds the opaque snapshot and stream tracks moq-ffi - publishes as `publish_flate_*` today, named after the `moq-flate` crate. + consumers. `flate` holds the opaque snapshot and stream tracks, named after + the `moq-flate` crate. - A layer's type is constructed from the handles its Rust constructor takes, not reached through an accessor on the broadcast: JSON wraps a track (`moq_json::snapshot::Producer::new(track, config)`), so it also works on a track accepted from a request; the codecs take the broadcast and its - catalog. Sketch, not a contract: `json.SnapshotProducer(track, config)`, - `video.Encoder(broadcast, catalog, config)`. + catalog. JSON and flate producers take the broadcast too, whose catalog + advertises the track, as `moq_mux::catalog::Producer::json_snapshot` does. Sketch, not a contract: `json.SnapshotProducer(track, config)`, + `video.Producer(broadcast, catalog, config)` for Rust's + `encode::Producer`, and the codec-only `video.Encoder(config)` for + `encode::Encoder` (decided 2026-10-06, see + [Codecs](/quest/m1/ffi-shape/codec.md)). - UniFFI 0.32 allows one namespace per crate, so moq-ffi groups by type and the wrappers supply real namespaces in each language's idiom: Python submodules, Go subpackages (`moq.dev/moq/json`, aliased on import next to `encoding/json`), Kotlin packages, Dart libraries, Swift caseless-enum namespaces. -- `MoqError` variants name their fields (`Transport { message }`), so no - binding exposes a positional `v1` (as C++ does today). - Decided while iterating on #4079. - `demand()` is the one way to watch subscribers; producers drop their `name`/`is_used`/`used`/`unused` duplicates. - moq-ffi and its generated consumers, including `cpp/obs` (above). The @@ -54,22 +62,18 @@ Settled shape: [generated C](/quest/m1/c/README.md) and C++ bindings inherit this shape from moq-ffi, so reshaping the hand-written C ABI would break C users twice. +- `rs/moq-ffi/AGENTS.md` records the per-language namespace pattern `json` + set. -Each child reshapes one group end to end: moq-ffi, all five wrappers, and the -`doc/lib` samples, per the cross-package table. This README owns the -work no child does: - -- A layers guide under `doc/lib` mapping net, media, json, flate, audio, and video to - each language's module, linked from every binding page. -- The bindings section of the following release's upgrade page: old call to - new call per language. -- `MoqGroupRequest` gains `demand()` in moq-ffi and every wrapper, matching - Rust's `group::Request::demand`, so a group server can see when nobody - still wants the group (decided in #4868; the bullet was lost when #4946 - removed `net.md`). -- `just test interop --all` green on the finished line. +Each child reshapes one group end to end: moq-ffi, all five wrappers, the +C++ package and `cpp/obs`, the `doc/lib` samples per the cross-package table, +and its entries in the Unreleased section of `doc/setup/upgrade.md`. Each +runs `just test interop --all`. ## Required -- [Bindings](/quest/m0/broadcast-epoch/bindings.md) - lands first; this line adopts its epoch surface and the `session.epoch()` rename -- [Codecs](/quest/m1/ffi-shape/codec.md) - audio and video encoders and decoders move under their own namespaces with one constructor shape +- [Codecs](/quest/m1/ffi-shape/codec.md) - audio and video producers, codec-only encoders, and decoders move under their own namespaces, named as in Rust; binding releases wait for it +- [Named error fields](/quest/m1/ffi-shape/error-fields.md) - `MoqError` variants name their fields, so no binding exposes a positional `v1` +- [Bindings](/quest/m0/broadcast-epoch/bindings.md) - the wrappers expose epochs and rename `session.epoch()`, on the reshaped wrappers +- [Group request demand](/quest/m1/ffi-shape/group-request-demand.md) - `MoqGroupRequest::demand()` in moq-ffi and every wrapper +- [Layers guide](/quest/m1/ffi-shape/layers-guide.md) - a `doc/lib` page maps each Rust layer to every binding's module, once Codecs adds `audio` and `video` diff --git a/quest/m1/ffi-shape/codec.md b/quest/m1/ffi-shape/codec.md index b55db2b1b1e..14eeca4c66b 100644 --- a/quest/m1/ffi-shape/codec.md +++ b/quest/m1/ffi-shape/codec.md @@ -57,4 +57,10 @@ literal. Update the wrappers and docs that restate 20000 (the Python test asserting the default, `doc/lib/{py,swift,kt,go,dart}`). libmoq already reads 0 as the default. +Binding releases wait for this quest, so the bindings break once after the +line's other children landed on `main` (decided 2026-10-09 on #4519). The +generated C++ follows moq-ffi: update `cpp/moq`'s `moq::` aliases (which +`just cpp check` audits), anything in `cpp/obs` that calls the moved verbs, +and `doc/lib/cpp`. + Public API: breaking in every binding. Wire: none. diff --git a/quest/m1/ffi-shape/error-fields.md b/quest/m1/ffi-shape/error-fields.md new file mode 100644 index 00000000000..a54b5c20629 --- /dev/null +++ b/quest/m1/ffi-shape/error-fields.md @@ -0,0 +1,16 @@ +# [M] moq-ffi error variants name their fields + +## Goal + +Every `MoqError` variant that carries data names its fields +(`Transport { message }`), so no binding exposes a positional `v1`, as the +generated C++ does today. + +## Plan + +Decided while iterating on #4079 and recorded in the FFI shape README. It +moved into its own quest when the line landed (2026-10-09). Breaking in every +binding that matches on the payload, so it should merge before the next +binding release with [Codecs](/quest/m1/ffi-shape/codec.md). Update the +wrappers that read the payload, `cpp/obs`, and the `doc/lib` samples. +Wire: none. diff --git a/quest/m1/ffi-shape/group-request-demand.md b/quest/m1/ffi-shape/group-request-demand.md new file mode 100644 index 00000000000..f8697014869 --- /dev/null +++ b/quest/m1/ffi-shape/group-request-demand.md @@ -0,0 +1,14 @@ +# [S] A group request reports its demand + +## Goal + +`MoqGroupRequest` gains `demand()` in moq-ffi and every wrapper (Python, Go, +Swift, Kotlin, Dart, C++), matching Rust's `group::Request::demand`, so a +group server can see when nobody still wants the group and stop producing it. + +## Plan + +Decided in #4868; the bullet was lost when #4946 removed `net.md`, and it +moved out of the FFI shape README into its own quest when the line landed +(2026-10-09). Mirror `MoqTrackRequest`'s existing `demand()`, which returns a +`MoqTrackDemand`. Additive in every binding. Wire: none. diff --git a/quest/m1/ffi-shape/layers-guide.md b/quest/m1/ffi-shape/layers-guide.md new file mode 100644 index 00000000000..914ac114103 --- /dev/null +++ b/quest/m1/ffi-shape/layers-guide.md @@ -0,0 +1,21 @@ +# [S] A layers guide for the bindings + +## Goal + +A page under `doc/lib` maps each Rust layer (net, media, json, flate, audio, +and video) to its module in every binding, and every binding page links it, so +a reader who knows one language or the Rust crates finds the same type in +another. + +## Plan + +Waits for [Codecs](/quest/m1/ffi-shape/codec.md), since `audio` and `video` have +no namespace to map until then (decided 2026-10-09 on #4519). Keep it a table +per layer, not a method reference: the API docs own the names (see +`doc/AGENTS.md`). C++ stays flat until +[One expected type and unprefixed names](/quest/m1/cpp-generated-shape.md) +lands, so its column names the `moq::Media*` types instead of a namespace. + +## Required + +- [Codecs](/quest/m1/ffi-shape/codec.md) - the `audio` and `video` namespaces the guide maps diff --git a/quest/m1/publish-timestamp.md b/quest/m1/publish-timestamp.md index 8db37dd7fcf..fa4277e9a5d 100644 --- a/quest/m1/publish-timestamp.md +++ b/quest/m1/publish-timestamp.md @@ -53,8 +53,10 @@ Swift wrappers default them to 0. `= 0` defaults go with it. - Go (`go/wrapper/moq`) and Python (`py/moq-rs`) wrap only the JSON producers; [#4137](https://github.com/moq-dev/moq/pull/4137) added - `publish_binary_snapshot` / `publish_binary_stream` (now - `publish_flate_*`) to moq-ffi without them. Add hand-written flate wrappers there. The maintainer asked for this. + `publish_binary_snapshot` / `publish_binary_stream` (now the + `MoqFlateSnapshotProducer` / `MoqFlateStreamProducer` constructors) to + moq-ffi without them. Add hand-written flate wrappers there, in a `flate` + namespace beside each wrapper's `json` one (see `rs/moq-ffi/AGENTS.md`). The maintainer asked for this. Each wrapper gets a test that a timestamp round-trips and an untimed payload arrives untimed. - Docs: "stamped when written" in `doc/lib/rs/{moq-json,moq-flate}.md` and diff --git a/quest/m1/quest-flat-lines.md b/quest/m1/quest-flat-lines.md index 9479d3d570d..ad5aaf46c41 100644 --- a/quest/m1/quest-flat-lines.md +++ b/quest/m1/quest-flat-lines.md @@ -66,8 +66,8 @@ Remaining: (0x3A) and moves to 0x3B before it lands. - #4079 cpp: `main` merged in again and the `uniffi-bindgen-cpp` pin bumped to `v0.11.0-kixelated.4`, whose generated files end with a newline. - - #4519 ffi-shape waits on - [Bindings](/quest/m0/broadcast-epoch/bindings.md). + - #4519 ffi-shape: lands now (maintainer, 2026-10-09) with the C++ port + #4079 left it; Bindings, Codecs, and the rest PR straight to `main`. - #4133 qos: closed without landing (2026-10-09). Every change it had over `main` is the egress `lag` histogram, which [Viewer lag histogram](/quest/m1/qos/lag-histogram.md) ports from the diff --git a/rs/moq-ffi/AGENTS.md b/rs/moq-ffi/AGENTS.md index 9aeddf55604..ef23dcf80f0 100644 --- a/rs/moq-ffi/AGENTS.md +++ b/rs/moq-ffi/AGENTS.md @@ -13,6 +13,12 @@ Mirror every change in the same PR: - Docs under `doc/lib/{py,go,dart,swift,kt,c}`. - Then `just test interop --all` for the interop matrix. +# Layers + +UniFFI allows one namespace per crate, so moq-ffi names by type (`MoqJsonSnapshotProducer`) and gives each layer above moq-net a constructor over the handle it wraps, not a verb on `MoqBroadcastProducer`. A constructor that takes over a handle closes it. + +The wrappers give each layer its own namespace, following `json`: a Python submodule (`moq.json`), a Go subpackage (`moq.dev/moq/json`, reaching root handles through `internal/bridge`), a Kotlin package (`dev.moq.json`), a Dart library (`package:moq/json.dart`), and a Swift caseless enum (`Json`). + Keep the wrappers thin and their names aligned with the Rust API. Swift and Python extend additively through labeled/keyword args with defaults; Go, Kotlin, and Dart take an options struct like Rust. # Gotchas diff --git a/rs/moq-ffi/examples/server_smoke.py b/rs/moq-ffi/examples/server_smoke.py index aa627891e0c..6d9703f3c54 100644 --- a/rs/moq-ffi/examples/server_smoke.py +++ b/rs/moq-ffi/examples/server_smoke.py @@ -26,9 +26,9 @@ async def main() -> int: - server = moq.MoqServer() - server.set_bind("127.0.0.1:0") - server.set_tls_generate(["localhost"]) + server = moq.MoqServer( + moq.MoqServerConfig(bind="127.0.0.1:0", tls=moq.MoqServerTls(generate=["localhost"])) + ) addr = await server.listen() print(f"server bound on {addr}") @@ -40,9 +40,7 @@ async def accept_one() -> moq.MoqSession: accept_task = asyncio.create_task(accept_one()) - client = moq.MoqClient() - client.set_tls_verify(False) - client.set_bind("127.0.0.1:0") + client = moq.MoqClient(moq.MoqClientConfig(bind="127.0.0.1:0", tls=moq.MoqClientTls(insecure=True))) client_session = await client.connect(f"https://{addr}") server_session = await accept_task diff --git a/rs/moq-ffi/src/audio.rs b/rs/moq-ffi/src/audio.rs index 73d32bc35d8..7ae6156f7c3 100644 --- a/rs/moq-ffi/src/audio.rs +++ b/rs/moq-ffi/src/audio.rs @@ -1,7 +1,7 @@ //! Raw-audio import/export via [`moq_audio`]. //! -//! Sibling to [`producer::MoqMediaProducer`](crate::producer::MoqMediaProducer) -//! and [`consumer::MoqMediaConsumer`](crate::consumer::MoqMediaConsumer): +//! Sibling to [`media::MoqMediaTrackProducer`](crate::media::MoqMediaTrackProducer) +//! and [`media::MoqMediaContainerConsumer`](crate::media::MoqMediaContainerConsumer): //! those deal in already-encoded frames, these deal in PCM and run //! Opus encode/decode inside the FFI boundary. @@ -178,7 +178,7 @@ impl TryFrom for moq_audio::Frame { /// Producer for a raw-audio track. /// -/// Built via [`MoqBroadcastProducer::publish_audio`]. Each +/// Built via [`MoqBroadcastProducer::encode_audio`]. Each /// [`write`](Self::write) accepts an [`MoqAudioFrame`] whose `data` /// is PCM in the format declared by the [`MoqAudioEncoderInput`] /// passed at publish time. @@ -391,7 +391,7 @@ fn audio_config(catalog_audio: crate::media::MoqAudio) -> Result Result { +pub(crate) fn timestamp_us(timestamp: moq_net::Timestamp) -> Result { timestamp.as_micros().try_into().map_err(|_| MoqError::TimeOverflow) } @@ -21,24 +19,6 @@ fn raw_frame(frame: moq_net::frame::Frame) -> Result { }) } -fn media_frame(mut frame: moq_mux::container::Frame) -> Result { - let timestamp_us = timestamp_us(frame.timestamp)?; - let payload = frame.payload.copy_to_bytes(frame.payload.remaining()).to_vec(); - - Ok(MoqMediaFrame { - payload, - timestamp_us, - keyframe: frame.keyframe, - }) -} - -fn media_container(container: MoqContainer) -> Result { - let container: hang::catalog::Container = container.into(); - // This byte-oriented API has no media-kind configuration. - moq_mux::catalog::hang::Container::new(&container, moq_mux::container::Kind::Data) - .map_err(|e| MoqError::Codec(format!("invalid container: {e}"))) -} - /// Subscriber-side delivery preferences, mirroring [`moq_net::track::Subscription`]. /// /// Construct with the fields you care about; the rest default to moq-net's defaults @@ -52,7 +32,7 @@ pub struct MoqSubscription { /// `0` skips immediately; a larger value tolerates that much reordering. /// /// Enforced both by the publisher's cache (sent on the wire) and by any local - /// buffering, such as `subscribe_media`'s jitter buffer. + /// buffering, such as [`MoqMediaContainerConsumer`]'s jitter buffer. #[uniffi(default = 0)] pub max_delay_us: u64, /// The lowest group to deliver (a floor), or null for none. A floor is not a @@ -93,7 +73,7 @@ impl From for moq_net::track::Subscription { #[derive(Clone, uniffi::Object)] pub struct MoqBroadcastConsumer { - inner: moq_net::broadcast::Consumer, + pub(crate) inner: moq_net::broadcast::Consumer, /// The origin this broadcast was resolved through, if any. A catalog rendition may name a /// sibling broadcast, and only an origin can fetch one; a standalone broadcast (a local /// producer's `consume`) has none, so such a rendition is unresolvable rather than wrong. @@ -119,8 +99,7 @@ impl MoqBroadcastConsumer { } } - /// Access the underlying `moq_net::broadcast::Consumer` for sibling - /// modules (e.g. `audio`) that need to subscribe a typed track. + #[cfg(test)] pub(crate) fn inner(&self) -> &moq_net::broadcast::Consumer { &self.inner } @@ -150,42 +129,6 @@ impl MoqBroadcastConsumer { } } -#[derive(uniffi::Object)] -pub struct MoqCatalogConsumer { - task: Task, -} - -struct Catalog { - // Consume with the untyped `Extra` extension so application sections survive into - // `MoqCatalog.sections` instead of being dropped. - inner: moq_mux::catalog::hang::Consumer, -} - -impl Catalog { - async fn next(&mut self) -> Result, MoqError> { - match self.inner.next().await { - Ok(Some(catalog)) => Ok(Some(convert_catalog(&catalog))), - Ok(None) => Ok(None), - Err(e) => Err(e.into()), - } - } -} - -#[derive(uniffi::Object)] -pub struct MoqMediaConsumer { - task: Task, -} - -struct Media { - inner: moq_mux::container::Consumer, -} - -impl Media { - async fn next(&mut self) -> Result, MoqError> { - self.inner.read().await?.map(media_frame).transpose() - } -} - // ---- Broadcast ---- #[uniffi::export] @@ -194,8 +137,8 @@ impl MoqBroadcastConsumer { /// /// `reference` is [`MoqVideo::broadcast`] / [`MoqAudio::broadcast`]: absent or empty names /// this broadcast, anything else names a sibling relative to it (e.g. `./source`). Call it on a - /// rendition that carries one before [`Self::subscribe_media`], [`Self::subscribe_track`], - /// [`Self::fetch_group`], or [`Self::fetch_media_group`], which take a track name rather than a + /// rendition that carries one before [`crate::media::MoqMediaContainerConsumer::subscribe`], [`Self::subscribe_track`], + /// [`Self::fetch_group`], or [`crate::media::MoqMediaContainerGroupConsumer::fetch`], which take a track name rather than a /// rendition; `decode_video` and `decode_audio` resolve it themselves. /// /// Errors if this broadcast came from a local producer rather than an origin, since a @@ -210,19 +153,6 @@ impl MoqBroadcastConsumer { })) } - /// Subscribe to the catalog for this broadcast. - pub async fn subscribe_catalog(&self) -> Result, MoqError> { - let track = self - .inner - .track(hang::catalog::Catalog::DEFAULT_NAME)? - .subscribe(hang::catalog::Catalog::default_subscription()) - .await?; - let consumer = moq_mux::catalog::hang::Consumer::from(track); - Ok(Arc::new(MoqCatalogConsumer { - task: Task::new(Catalog { inner: consumer }), - })) - } - /// Subscribe to a track by name, the same pattern as moq-boy's command/status tracks. /// /// Frames are returned as plain byte payloads with no codec or container parsing. @@ -253,54 +183,9 @@ impl MoqBroadcastConsumer { let group = track.fetch_group(sequence, options).await.map_err(map_fetch_error)?; Ok(Arc::new(MoqGroupConsumer::new(group))) } - - /// Fetch one group and decode its track container into media frames. - /// - /// Unlike [`Self::subscribe_media`], this does not create a live subscription or apply - /// age-based group skipping. The returned consumer reads exactly the requested group - /// until [`MoqMediaGroupConsumer::next`] returns `None`. - pub async fn fetch_media_group( - &self, - name: String, - sequence: u64, - container: MoqContainer, - options: Option, - ) -> Result, MoqError> { - // Parse the container before fetching so invalid CMAF init data does not leave a - // dynamic group request waiting for a consumer that can never read it. - let media = media_container(container)?; - let options = options.map(moq_net::group::Fetch::from); - let track = self.inner.track(&name).map_err(map_fetch_error)?; - let group = track.fetch_group(sequence, options).await.map_err(map_fetch_error)?; - Ok(Arc::new(MoqMediaGroupConsumer::new(group, media))) - } - - /// Subscribe to a track by name, delivering frames in decode order. - /// - /// `container` is the track container from the catalog. - /// `subscription` tunes delivery priority, group range, and staleness; omit for defaults. - /// - /// [`MoqSubscription::max_delay_us`] bounds the local jitter buffer as well as - /// the publisher's cache, so both ends skip a stalled group on the same budget. - pub async fn subscribe_media( - &self, - name: String, - container: MoqContainer, - subscription: Option, - ) -> Result, MoqError> { - // Parse the container before subscribing so we don't leave a dangling - // subscription if init parsing fails. - let media = media_container(container)?; - let subscription = subscription.map(moq_net::track::Subscription::from).unwrap_or_default(); - let track = self.inner.track(&name)?.subscribe(subscription).await?; - let consumer = moq_mux::container::Consumer::new(track, media); - Ok(Arc::new(MoqMediaConsumer { - task: Task::new(Media { inner: consumer }), - })) - } } -fn map_fetch_error(err: moq_net::Error) -> MoqError { +pub(crate) fn map_fetch_error(err: moq_net::Error) -> MoqError { match err { moq_net::Error::NotFound | moq_net::Error::NotFetchable => MoqError::NotFound, moq_net::Error::Unsupported | moq_net::Error::Version => MoqError::Unsupported, @@ -544,6 +429,29 @@ impl MoqTrackConsumer { } } + /// Hand the subscription to a typed reader (a JSON consumer, say), closing this handle. + /// + /// Errors with [`MoqError::AlreadyCommitted`] once a group read has started, since the reader + /// would miss whatever that read consumed. + pub(crate) fn take(&self) -> Result { + let mut guard = self.shared.inner.lock().expect("track reader"); + match guard.as_ref().map(|inner| &inner.track) { + Some(Cursor::Uncommitted(_)) => {} + Some(_) => return Err(MoqError::AlreadyCommitted), + None => return Err(MoqError::Closed), + } + let Some(TrackInner { + track: Cursor::Uncommitted(track), + }) = guard.take() + else { + unreachable!("just matched Uncommitted"); + }; + drop(guard); + // Wake any datagram read still parked on the subscriber this handle no longer owns. + self.cancel_lanes(); + Ok(track) + } + fn cancel_lanes(&self) { self.shared.close(); self.groups.cancel(); @@ -681,54 +589,6 @@ pub struct MoqGroupConsumer { task: Task, } -struct MediaGroupInner { - inner: moq_mux::container::GroupConsumer, -} - -impl MediaGroupInner { - async fn next(&mut self) -> Result, MoqError> { - self.inner.read().await?.map(media_frame).transpose() - } -} - -/// A finite, container-decoded media group returned by -/// [`MoqBroadcastConsumer::fetch_media_group`]. -#[derive(uniffi::Object)] -pub struct MoqMediaGroupConsumer { - sequence: u64, - task: Task, -} - -impl MoqMediaGroupConsumer { - fn new(group: moq_net::group::Consumer, container: moq_mux::catalog::hang::Container) -> Self { - let inner = moq_mux::container::GroupConsumer::new(group, container); - Self { - sequence: inner.sequence(), - task: Task::new(MediaGroupInner { inner }), - } - } -} - -#[uniffi::export] -impl MoqMediaGroupConsumer { - /// The sequence number of this group within the track. - pub fn sequence(&self) -> u64 { - self.sequence - } - - /// Read the next decoded media frame, or `None` when the group ends. - pub async fn next(&self) -> Result, MoqError> { - self.task.run(|mut state| async move { state.next().await }).await - } - - /// Cancel all current and future `next()` calls. - /// - /// Terminal: the subscription is released here, not when the handle is. - pub fn cancel(&self) { - self.task.cancel(); - } -} - impl MoqGroupConsumer { pub(crate) fn new(group: moq_net::group::Consumer) -> Self { Self { @@ -759,37 +619,3 @@ impl MoqGroupConsumer { self.task.cancel(); } } - -// ---- Catalog Consumer ---- - -#[uniffi::export] -impl MoqCatalogConsumer { - /// Get the next catalog update. Returns `None` when the track ends or is closed. - pub async fn next(&self) -> Result, MoqError> { - self.task.run(|mut state| async move { state.next().await }).await - } - - /// Cancel all current and future `next()` calls. - /// - /// Terminal: the subscription is released here, not when the handle is. - pub fn cancel(&self) { - self.task.cancel(); - } -} - -// ---- Media Consumer ---- - -#[uniffi::export] -impl MoqMediaConsumer { - /// Get the next frame. Returns `None` when the track ends or is closed. - pub async fn next(&self) -> Result, MoqError> { - self.task.run(|mut state| async move { state.next().await }).await - } - - /// Cancel all current and future `next()` calls. - /// - /// Terminal: the subscription is released here, not when the handle is. - pub fn cancel(&self) { - self.task.cancel(); - } -} diff --git a/rs/moq-ffi/src/error.rs b/rs/moq-ffi/src/error.rs index 1f5ef3cdec9..58f857282db 100644 --- a/rs/moq-ffi/src/error.rs +++ b/rs/moq-ffi/src/error.rs @@ -250,14 +250,14 @@ pub enum MoqError { #[error("unsupported")] Unsupported, - /// This track already committed to the other delivery order. + /// This track already committed to another way of reading it. /// - /// A track is read in arrival order or in sequence order, never both, and the first - /// group read picks which. Reaching for the other one afterwards is this error rather - /// than [`Self::Unsupported`]: both orders work fine here, the track just isn't - /// reading in the one you asked for. Read the track through a second consumer if you - /// genuinely need both. - #[error("already committed to the other delivery order")] + /// A track is read in arrival order, in sequence order, or by a typed reader such as a + /// JSON consumer, never two of them, and the first group read picks the order. Reaching + /// for another one afterwards is this error rather than [`Self::Unsupported`]: each works + /// fine here, the track just isn't reading the way you asked for. Read the track through + /// a second consumer if you genuinely need both. + #[error("already committed to another way of reading")] AlreadyCommitted, /// A route carried an invalid hop id or too many hops. @@ -275,6 +275,10 @@ pub enum MoqError { #[error("log: {0}")] Log(String), + + /// A configuration record held a value the constructor cannot use. + #[error("config: {0}")] + Config(String), } impl From for MoqError { diff --git a/rs/moq-ffi/src/ffi.rs b/rs/moq-ffi/src/ffi.rs index 16019d1bb0d..184b09b1582 100644 --- a/rs/moq-ffi/src/ffi.rs +++ b/rs/moq-ffi/src/ffi.rs @@ -175,6 +175,8 @@ impl Task { } /// Try to lock the state synchronously. Returns `None` if a task is running or it was cancelled. + // Only the native server and its requests read state synchronously. + #[cfg(not(target_arch = "wasm32"))] pub fn lock(&self) -> Option> { let guard = self.state.clone().try_lock_owned().ok()?; @@ -191,6 +193,7 @@ impl Task { /// Whether [Self::cancel] has run, which [Self::lock] folds into the same `None` as a busy /// state. The state may still be a moment away from being dropped. + #[cfg(not(target_arch = "wasm32"))] pub fn is_cancelled(&self) -> bool { *self.cancel.borrow() } @@ -201,6 +204,7 @@ impl Task { /// Configuration must apply or fail, so this splits them: [`MoqError::Busy`] /// while a [`Self::run`] owns the state, [`MoqError::Cancelled`] after /// [`Self::cancel`]. + #[cfg(not(target_arch = "wasm32"))] pub fn configure(&self) -> Result, MoqError> { match self.lock() { Some(guard) => Ok(guard), diff --git a/rs/moq-ffi/src/flate.rs b/rs/moq-ffi/src/flate.rs index 2c86f745590..d2df56cbac6 100644 --- a/rs/moq-ffi/src/flate.rs +++ b/rs/moq-ffi/src/flate.rs @@ -1,7 +1,7 @@ //! Opaque data tracks over the FFI boundary, advertised in the catalog. //! //! The `moq-flate` counterpart of [`crate::json`]: opaque payloads (for example a camera's latest -//! JPEG thumbnail) on a named track, in either mode: `snapshot` (each payload supersedes the last) +//! JPEG thumbnail) on a track, in either mode: `snapshot` (each payload supersedes the last) //! or `stream` (every payload preserved in order). The broadcast's catalog carries //! `binary.tracks.` (mode, plus `mime` and `compression` when set) for as long as the track //! lives, so a consumer discovers it without knowing the application. @@ -12,7 +12,7 @@ use moq_mux::binary::Config; use moq_mux::catalog::hang::Extra; use crate::error::MoqError; -use crate::producer::MoqBroadcastProducer; +use crate::producer::{MoqBroadcastProducer, MoqTrackProducer}; /// Options for an opaque data track, in either mode (the mode is fixed by the constructor). #[derive(Clone, uniffi::Record)] @@ -36,45 +36,6 @@ impl From for Config { } } -#[uniffi::export] -impl MoqBroadcastProducer { - /// Publish an opaque snapshot track (lossy latest-value) by name, advertised in the catalog. - /// - /// Errors if the catalog already carries an entry under `name`. - pub fn publish_flate_snapshot( - &self, - name: String, - config: MoqFlateConfig, - ) -> Result, MoqError> { - let _guard = crate::ffi::enter(); - self.with_state(|state| { - let track = state.broadcast.create_track(name, None)?; - let producer = state.catalog.binary_snapshot(track, Config::from(config))?; - Ok(Arc::new(MoqFlateSnapshotProducer { - inner: std::sync::Mutex::new(Some(producer)), - })) - }) - } - - /// Publish an opaque stream track (lossless append-log) by name, advertised in the catalog. - /// - /// Errors if the catalog already carries an entry under `name`. - pub fn publish_flate_stream( - &self, - name: String, - config: MoqFlateConfig, - ) -> Result, MoqError> { - let _guard = crate::ffi::enter(); - self.with_state(|state| { - let track = state.broadcast.create_track(name, None)?; - let producer = state.catalog.binary_stream(track, Config::from(config))?; - Ok(Arc::new(MoqFlateStreamProducer { - inner: std::sync::Mutex::new(Some(producer)), - })) - }) - } -} - /// Publishes opaque payloads that consumers see as a single latest value. #[derive(uniffi::Object)] pub struct MoqFlateSnapshotProducer { @@ -83,6 +44,28 @@ pub struct MoqFlateSnapshotProducer { #[uniffi::export] impl MoqFlateSnapshotProducer { + /// Publish `track` as an opaque snapshot track (lossy latest-value), advertised in `broadcast`'s catalog. + /// + /// `track` must come from `broadcast`; otherwise the catalog advertises a track the broadcast + /// does not carry. + /// + /// Takes over `track`, whose handle is closed afterward. Errors if the catalog already + /// carries an entry under the track's name. + #[uniffi::constructor] + pub fn new( + broadcast: &MoqBroadcastProducer, + track: &MoqTrackProducer, + config: MoqFlateConfig, + ) -> Result, MoqError> { + let _guard = crate::ffi::enter(); + let config = Config::from(config); + let producer = + track.adopt(|track| broadcast.with_state(|state| Ok(state.catalog.binary_snapshot(track, config)?)))?; + Ok(Arc::new(Self { + inner: std::sync::Mutex::new(Some(producer)), + })) + } + /// Publish a new payload, superseding the last. pub fn update(&self, payload: Vec) -> Result<(), MoqError> { let _guard = crate::ffi::enter(); @@ -108,6 +91,28 @@ pub struct MoqFlateStreamProducer { #[uniffi::export] impl MoqFlateStreamProducer { + /// Publish `track` as an opaque stream track (lossless append-log), advertised in `broadcast`'s catalog. + /// + /// `track` must come from `broadcast`; otherwise the catalog advertises a track the broadcast + /// does not carry. + /// + /// Takes over `track`, whose handle is closed afterward. Errors if the catalog already + /// carries an entry under the track's name. + #[uniffi::constructor] + pub fn new( + broadcast: &MoqBroadcastProducer, + track: &MoqTrackProducer, + config: MoqFlateConfig, + ) -> Result, MoqError> { + let _guard = crate::ffi::enter(); + let config = Config::from(config); + let producer = + track.adopt(|track| broadcast.with_state(|state| Ok(state.catalog.binary_stream(track, config)?)))?; + Ok(Arc::new(Self { + inner: std::sync::Mutex::new(Some(producer)), + })) + } + /// Append one payload to the log. pub fn append(&self, payload: Vec) -> Result<(), MoqError> { let _guard = crate::ffi::enter(); diff --git a/rs/moq-ffi/src/json.rs b/rs/moq-ffi/src/json.rs index 6b10c40e109..5a8ec3f30e8 100644 --- a/rs/moq-ffi/src/json.rs +++ b/rs/moq-ffi/src/json.rs @@ -1,19 +1,20 @@ //! Generic JSON tracks over the FFI boundary. //! -//! Wraps [`moq_json`] so native callers can publish and consume JSON on an arbitrary named -//! track, in either mode: `snapshot` (lossy latest-value, RFC 7396 merge-patch deltas) or -//! `stream` (lossless append-log). Values cross the boundary as JSON strings; the caller parses -//! and serializes on its own side. +//! Wraps [`moq_json`] so native callers can publish and consume JSON on a track, in either mode: +//! `snapshot` (lossy latest-value, RFC 7396 merge-patch deltas) or `stream` (lossless +//! append-log). Each type is constructed from the track it wraps, so a track accepted from a +//! request works as well as one created by name. Values cross the boundary as JSON strings; the +//! caller parses and serializes on its own side. use std::sync::Arc; use serde_json::Value; -use crate::consumer::MoqBroadcastConsumer; +use crate::consumer::MoqTrackConsumer; use crate::demand::MoqTrackDemand; use crate::error::MoqError; use crate::ffi::Task; -use crate::producer::MoqBroadcastProducer; +use crate::producer::{MoqBroadcastProducer, MoqTrackProducer}; use moq_mux::catalog::hang::Extra; /// Options for a JSON snapshot track (lossy latest-value mode). @@ -98,85 +99,6 @@ mod tests { } } -// ---- Entry points ---- - -#[uniffi::export] -impl MoqBroadcastProducer { - /// Publish a JSON snapshot track (lossy latest-value) by name, advertised in the catalog. - /// - /// The broadcast's catalog carries `json.tracks.` (`mode: snapshot`, and - /// `compression: deflate` when set) for as long as the track lives; finishing or dropping the - /// producer retires it. Errors if the catalog already carries an entry under `name`. - pub fn publish_json_snapshot( - &self, - name: String, - config: MoqJsonSnapshotConfig, - ) -> Result, MoqError> { - let _guard = crate::ffi::enter(); - self.with_state(|state| { - let track = state.broadcast.create_track(name, None)?; - let config = moq_mux::json::Config::default() - .with_compression(config.compression) - .with_delta_ratio(config.delta_ratio); - let producer = state.catalog.json_snapshot::(track, config)?; - Ok(Arc::new(MoqJsonSnapshotProducer { - inner: std::sync::Mutex::new(Some(producer)), - })) - }) - } - - /// Publish a JSON stream track (lossless append-log) by name, advertised in the catalog. - /// - /// The broadcast's catalog carries `json.tracks.` (`mode: stream`) for as long as the - /// track lives. Errors if the catalog already carries an entry under `name`. - pub fn publish_json_stream( - &self, - name: String, - config: MoqJsonStreamConfig, - ) -> Result, MoqError> { - let _guard = crate::ffi::enter(); - self.with_state(|state| { - let track = state.broadcast.create_track(name, None)?; - let config = moq_mux::json::Config::default().with_compression(config.compression); - let producer = state.catalog.json_stream::(track, config)?; - Ok(Arc::new(MoqJsonStreamProducer { - inner: std::sync::Mutex::new(Some(producer)), - })) - }) - } -} - -#[uniffi::export] -impl MoqBroadcastConsumer { - /// Subscribe to a JSON snapshot track (lossy latest-value) by name. - /// - /// Pass the same [`MoqJsonSnapshotConfig::compression`] the producer used. - pub async fn subscribe_json_snapshot( - &self, - name: String, - config: MoqJsonSnapshotConfig, - ) -> Result, MoqError> { - let track = self.inner().track(&name)?.subscribe(None).await?; - let consumer = moq_json::snapshot::Consumer::::new(track, config.into()); - Ok(Arc::new(MoqJsonSnapshotConsumer { - task: Task::new(SnapshotConsumer { inner: consumer }), - })) - } - - /// Subscribe to a JSON stream track (lossless append-log) by name. - pub async fn subscribe_json_stream( - &self, - name: String, - config: MoqJsonStreamConfig, - ) -> Result, MoqError> { - let track = self.inner().track(&name)?.subscribe(None).await?; - let consumer = moq_json::stream::Consumer::::new(track, config.into()); - Ok(Arc::new(MoqJsonStreamConsumer { - task: Task::new(StreamConsumer { inner: consumer }), - })) - } -} - // ---- Snapshot ---- /// Publishes a JSON value that consumers see as a single latest state. @@ -187,6 +109,32 @@ pub struct MoqJsonSnapshotProducer { #[uniffi::export] impl MoqJsonSnapshotProducer { + /// Publish `track` as a JSON snapshot track (lossy latest-value), advertised in `broadcast`'s catalog. + /// + /// `track` must come from `broadcast`; otherwise the catalog advertises a track the broadcast + /// does not carry. + /// + /// The catalog carries `json.tracks.` (`mode: snapshot`, and `compression: deflate` when + /// set) for as long as the producer lives; finishing or dropping it retires the entry. Takes + /// over `track`, whose handle is closed afterward. Errors if the catalog already carries an + /// entry under the track's name. + #[uniffi::constructor] + pub fn new( + broadcast: &MoqBroadcastProducer, + track: &MoqTrackProducer, + config: MoqJsonSnapshotConfig, + ) -> Result, MoqError> { + let _guard = crate::ffi::enter(); + let config = moq_mux::json::Config::default() + .with_compression(config.compression) + .with_delta_ratio(config.delta_ratio); + let producer = track + .adopt(|track| broadcast.with_state(|state| Ok(state.catalog.json_snapshot::(track, config)?)))?; + Ok(Arc::new(Self { + inner: std::sync::Mutex::new(Some(producer)), + })) + } + /// Publish a new value, encoded as a snapshot or delta automatically. `value` is a JSON /// document. A no-op if unchanged from the previous update. pub fn update(&self, value: String) -> Result<(), MoqError> { @@ -234,6 +182,19 @@ pub struct MoqJsonSnapshotConsumer { #[uniffi::export] impl MoqJsonSnapshotConsumer { + /// Read `track` as a JSON snapshot track (lossy latest-value). + /// + /// Pass the same [`MoqJsonSnapshotConfig::compression`] the producer used. Takes over + /// `track`, whose handle is closed afterward; errors if it has already read a group. + #[uniffi::constructor] + pub fn new(track: &MoqTrackConsumer, config: MoqJsonSnapshotConfig) -> Result, MoqError> { + let _guard = crate::ffi::enter(); + let consumer = moq_json::snapshot::Consumer::::new(track.take()?, config.into()); + Ok(Arc::new(Self { + task: Task::new(SnapshotConsumer { inner: consumer }), + })) + } + /// Get the next value as a JSON string. Returns `None` once the track ends. /// /// A consumer that has fallen behind collapses the backlog and yields only the latest value. @@ -259,6 +220,29 @@ pub struct MoqJsonStreamProducer { #[uniffi::export] impl MoqJsonStreamProducer { + /// Publish `track` as a JSON stream track (lossless append-log), advertised in `broadcast`'s catalog. + /// + /// `track` must come from `broadcast`; otherwise the catalog advertises a track the broadcast + /// does not carry. + /// + /// The catalog carries `json.tracks.` (`mode: stream`) for as long as the producer + /// lives. Takes over `track`, whose handle is closed afterward. Errors if the catalog already + /// carries an entry under the track's name. + #[uniffi::constructor] + pub fn new( + broadcast: &MoqBroadcastProducer, + track: &MoqTrackProducer, + config: MoqJsonStreamConfig, + ) -> Result, MoqError> { + let _guard = crate::ffi::enter(); + let config = moq_mux::json::Config::default().with_compression(config.compression); + let producer = track + .adopt(|track| broadcast.with_state(|state| Ok(state.catalog.json_stream::(track, config)?)))?; + Ok(Arc::new(Self { + inner: std::sync::Mutex::new(Some(producer)), + })) + } + /// Append one record to the log. `value` is a JSON document. pub fn append(&self, value: String) -> Result<(), MoqError> { let _guard = crate::ffi::enter(); @@ -305,6 +289,19 @@ pub struct MoqJsonStreamConsumer { #[uniffi::export] impl MoqJsonStreamConsumer { + /// Read `track` as a JSON stream track (lossless append-log). + /// + /// Pass the same [`MoqJsonStreamConfig::compression`] the producer used. Takes over `track`, + /// whose handle is closed afterward; errors if it has already read a group. + #[uniffi::constructor] + pub fn new(track: &MoqTrackConsumer, config: MoqJsonStreamConfig) -> Result, MoqError> { + let _guard = crate::ffi::enter(); + let consumer = moq_json::stream::Consumer::::new(track.take()?, config.into()); + Ok(Arc::new(Self { + task: Task::new(StreamConsumer { inner: consumer }), + })) + } + /// Get the next record as a JSON string. Returns `None` once the track ends. pub async fn next(&self) -> Result, MoqError> { self.task.run(|mut state| async move { state.next().await }).await diff --git a/rs/moq-ffi/src/media.rs b/rs/moq-ffi/src/media.rs index 43c501dccc6..5a51313c1c6 100644 --- a/rs/moq-ffi/src/media.rs +++ b/rs/moq-ffi/src/media.rs @@ -1,5 +1,16 @@ use std::collections::HashMap; +use std::sync::{Arc, Weak}; +use bytes::Buf; +use moq_mux::catalog::hang::Extra; + +use crate::consumer::{MoqBroadcastConsumer, MoqFetchGroupOptions, MoqSubscription, map_fetch_error, timestamp_us}; +use crate::demand::MoqTrackDemand; +use crate::error::MoqError; +use crate::ffi::Task; +use crate::producer::{MoqBroadcastProducer, MoqTrackRequest}; + +/// A width and height in pixels. #[derive(Clone, uniffi::Record)] pub struct MoqDimensions { pub width: u32, @@ -73,7 +84,7 @@ pub struct MoqCatalog { /// Untyped application catalog sections, keyed by section name, each value a JSON string. /// These are the top-level catalog keys beyond `video`/`audio`, carried through verbatim /// (parse the JSON yourself). Set them on the publish side with - /// [`set_catalog_section`](crate::producer::MoqBroadcastProducer::set_catalog_section). + /// [`set_section`](MoqMediaCatalogProducer::set_section). pub sections: HashMap, } @@ -247,10 +258,6 @@ pub struct MoqAudioInit { /// Human-readable rendition name for a track picker. #[uniffi(default = None)] pub label: Option, - /// Track name. `None` derives a unique name from the format. Refused on a requested track, - /// which already carries its name. - #[uniffi(default = None)] - pub track: Option, } /// What a video publish needs: a format, optional init bytes, a label, and hints. @@ -269,10 +276,6 @@ pub struct MoqVideoInit { /// Catalog fields the stream cannot reveal itself. #[uniffi(default = None)] pub hint: Option, - /// Track name. `None` derives a unique name from the format. Refused on a requested track, - /// which already carries its name. - #[uniffi(default = None)] - pub track: Option, } /// What a container publish needs: a format and its leading bytes. @@ -499,3 +502,794 @@ mod test { assert_eq!(converted.audio["audio"].label.as_deref(), Some("English")); } } + +/// Where an importer publishes its single track. +#[derive(Clone, uniffi::Enum)] +pub enum MoqMediaTarget { + /// A new track with an explicit name, or a unique name derived from its format. + Named { name: Option }, + /// A requested track whose name and ownership are already fixed. + Requested { request: Arc }, +} + +impl MoqMediaTarget { + fn resolve( + self, + broadcast: &moq_net::broadcast::Producer, + format: &impl std::fmt::Display, + ) -> Result { + match self { + Self::Named { name } => reserve_track(broadcast, name, format), + Self::Requested { request } => request.take(), + } + } +} + +/// Updates a broadcast's catalog without keeping the broadcast open. +#[derive(uniffi::Object)] +pub struct MoqMediaCatalogProducer { + broadcast: Weak, +} + +#[uniffi::export] +impl MoqMediaCatalogProducer { + /// Construct a weak catalog handle for this broadcast. + #[uniffi::constructor] + pub fn new(broadcast: Arc) -> Result, MoqError> { + let _guard = crate::ffi::enter(); + broadcast.with_state(|_| Ok(()))?; + Ok(Arc::new(Self { + broadcast: Arc::downgrade(&broadcast), + })) + } + + /// Replace shared video properties, clearing fields that are absent. + /// + /// Rotation is clockwise and normalized to the nearest quarter turn. + pub fn set_video_properties(&self, properties: MoqVideoProperties) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut value = hang::catalog::VideoProperties::default(); + value.display = properties.display.map(|display| hang::catalog::Display { + width: display.width, + height: display.height, + }); + value.rotation = properties.rotation; + value.flip = properties.flip; + + self.broadcast.upgrade().ok_or(MoqError::Closed)?.with_state(|state| { + let mut catalog = state.catalog.modify()?; + catalog.video.set_properties(value)?; + catalog.commit()?; + Ok(()) + }) + } + + /// Set an application catalog section from JSON, refusing reserved section names. + /// + /// `json` is any JSON document. Errors with [`MoqError::Json`] if it doesn't parse, or with + /// the reserved-section error if `name` is a HANG root (`video`, `audio`, `text`, `archive`, `clock`, `json`, `binary`, or + /// retired `timeline`) or an MSF root (`version`, `generatedAt`, `isComplete`, `tracks`, or + /// `initDataList`). + pub fn set_section(&self, name: String, json: String) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let value: serde_json::Value = serde_json::from_str(&json)?; + self.broadcast.upgrade().ok_or(MoqError::Closed)?.with_state(|state| { + let mut guard = state.catalog.modify()?; + guard.set_section(name, value)?; + guard.commit()?; + Ok(()) + }) + } + + /// Remove an application catalog section if present. + pub fn remove_section(&self, name: String) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + self.broadcast.upgrade().ok_or(MoqError::Closed)?.with_state(|state| { + let mut guard = state.catalog.modify()?; + guard.remove_section(&name); + guard.commit()?; + Ok(()) + }) + } +} + +/// A whole-frame importer for one codec track. +/// +/// Separate from [`ContainerProducer`] because a container publishes several tracks and takes +/// chunks rather than timestamped frames. Sharing one type meant a `write_frame` timestamp that one +/// arm silently dropped. +struct MediaProducer { + // Boxed because the codec splitters/imports make this much larger than the container one. + import: Box, + /// Subscriber demand (name/used/unused) for the one track this publishes. + demand: moq_net::track::Demand, +} + +/// A whole-chunk importer for a container, which may publish several tracks. +struct ContainerProducer { + import: moq_mux::import::Container, +} + +/// A byte-stream importer for one codec track, where frame boundaries are inferred. +struct MediaStreamProducer { + import: Box, +} + +/// A byte-stream importer for a container, which recovers its own framing. +struct ContainerStreamProducer { + import: moq_mux::import::ContainerStream, +} + +/// Imports complete container chunks into a broadcast. +#[derive(uniffi::Object)] +pub struct MoqMediaContainerProducer { + inner: std::sync::Mutex>, +} + +/// Imports a container byte stream into a broadcast. +#[derive(uniffi::Object)] +pub struct MoqMediaContainerStreamProducer { + inner: std::sync::Mutex>, +} + +/// Imports complete encoded frames onto one track. +#[derive(uniffi::Object)] +pub struct MoqMediaTrackProducer { + inner: std::sync::Mutex>, +} + +/// Imports a video byte stream onto one track. +#[derive(uniffi::Object)] +pub struct MoqMediaTrackStreamProducer { + inner: std::sync::Mutex>, +} + +#[uniffi::export] +impl MoqMediaTrackProducer { + /// Import complete encoded audio frames on a named or requested track. + /// + /// [`MoqAudioInit::data`] is required: audio resolves its rendition entirely from those bytes. + #[uniffi::constructor] + pub fn audio( + broadcast: &MoqBroadcastProducer, + target: MoqMediaTarget, + init: MoqAudioInit, + ) -> Result, MoqError> { + let _guard = crate::ffi::enter(); + let guard = broadcast.state.lock().unwrap(); + let state = guard.as_ref().ok_or(MoqError::Closed)?; + + let init: moq_mux::import::AudioInit = init.into(); + let request = target.resolve(&state.broadcast, &init.format)?; + + let import = moq_mux::import::Track::audio(request, state.catalog.reserve(), init) + .map_err(|err| MoqError::Codec(format!("init failed: {err}")))?; + Ok(MoqMediaTrackProducer::from_import(import)) + } +} + +#[uniffi::export] +impl MoqMediaTrackProducer { + /// Import complete encoded video frames on a named or requested track. + /// + /// [`MoqVideoInit::data`] may be empty for a format that resolves in band; a hint carrying the + /// codec publishes the catalog before the first keyframe. + #[uniffi::constructor] + pub fn video( + broadcast: &MoqBroadcastProducer, + target: MoqMediaTarget, + init: MoqVideoInit, + ) -> Result, MoqError> { + let _guard = crate::ffi::enter(); + let guard = broadcast.state.lock().unwrap(); + let state = guard.as_ref().ok_or(MoqError::Closed)?; + + let init: moq_mux::import::VideoInit = init.into(); + let request = target.resolve(&state.broadcast, &init.format)?; + + let import = moq_mux::import::Track::video(request, state.catalog.reserve(), init) + .map_err(|err| MoqError::Codec(format!("init failed: {err}")))?; + Ok(MoqMediaTrackProducer::from_import(import)) + } +} + +#[uniffi::export] +impl MoqMediaContainerProducer { + /// Construct an importer from this broadcast. + #[uniffi::constructor] + pub fn new( + broadcast: &MoqBroadcastProducer, + init: MoqContainerInit, + ) -> Result, MoqError> { + let _guard = crate::ffi::enter(); + let guard = broadcast.state.lock().unwrap(); + let state = guard.as_ref().ok_or(MoqError::Closed)?; + + let init: moq_mux::import::ContainerInit = init.into(); + let import = moq_mux::import::Container::new(state.broadcast.clone(), state.catalog.reserve(), &init) + .map_err(|err| MoqError::Codec(format!("init failed: {err}")))?; + + Ok(Arc::new(MoqMediaContainerProducer { + inner: std::sync::Mutex::new(Some(ContainerProducer { import })), + })) + } +} + +#[uniffi::export] +impl MoqMediaTrackStreamProducer { + /// Import a video byte stream, inferring frame boundaries. + /// + /// Only the self-delimiting formats work here (`Avc3`, `Hev1`, `Av01`); the rest need length + /// prefixes or an out-of-band config record. There is no audio counterpart for the same reason. + #[uniffi::constructor] + pub fn video( + broadcast: &MoqBroadcastProducer, + target: MoqMediaTarget, + init: MoqVideoInit, + ) -> Result, MoqError> { + let _guard = crate::ffi::enter(); + let guard = broadcast.state.lock().unwrap(); + let state = guard.as_ref().ok_or(MoqError::Closed)?; + + let init: moq_mux::import::VideoInit = init.into(); + let request = target.resolve(&state.broadcast, &init.format)?; + + let import = moq_mux::import::TrackStream::video(request, state.catalog.reserve(), init) + .map_err(|err| MoqError::Codec(format!("init failed: {err}")))?; + + Ok(Arc::new(MoqMediaTrackStreamProducer { + inner: std::sync::Mutex::new(Some(MediaStreamProducer { + import: Box::new(import), + })), + })) + } +} + +#[uniffi::export] +impl MoqMediaContainerStreamProducer { + /// Construct an importer from this broadcast. + #[uniffi::constructor] + pub fn new( + broadcast: &MoqBroadcastProducer, + format: MoqContainerFormat, + ) -> Result, MoqError> { + let _guard = crate::ffi::enter(); + let guard = broadcast.state.lock().unwrap(); + let state = guard.as_ref().ok_or(MoqError::Closed)?; + + let import = + moq_mux::import::ContainerStream::new(state.broadcast.clone(), state.catalog.reserve(), format.into()) + .map_err(|err| MoqError::Codec(format!("init failed: {err}")))?; + + Ok(Arc::new(MoqMediaContainerStreamProducer { + inner: std::sync::Mutex::new(Some(ContainerStreamProducer { import })), + })) + } +} + +impl MoqMediaTrackProducer { + /// Wrap a single-codec importer, capturing the demand handle its track exposes. + fn from_import(import: moq_mux::import::Track) -> Arc { + let demand = import.demand(); + Arc::new(Self { + inner: std::sync::Mutex::new(Some(MediaProducer { + import: Box::new(import), + demand, + })), + }) + } +} + +#[uniffi::export] +impl MoqMediaTrackProducer { + /// A watch-only handle to whether this track has subscribers. + pub fn demand(&self) -> Result, MoqError> { + let guard = self.inner.lock().unwrap(); + Ok(MoqTrackDemand::new( + guard.as_ref().ok_or(MoqError::Closed)?.demand.clone(), + )) + } + + /// Write `frame` to this track. + /// + /// The importer derives keyframe status from the bitstream, so a [`MoqFrame`] carries only the + /// payload and its timestamp. + pub fn write_frame(&self, frame: MoqFrame) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut guard = self.inner.lock().unwrap(); + let media = guard.as_mut().ok_or(MoqError::Closed)?; + + let timestamp = frame + .timestamp_us + .map(hang::container::Timestamp::from_micros) + .transpose()?; + media + .import + .decode(&frame.payload, timestamp) + .map_err(|err| MoqError::Codec(format!("decode failed: {err}")))?; + + Ok(()) + } + + /// Record a locally encoded frame's handoff for catalog jitter measurement. + /// + /// `timestamp_us` is on the broadcast media clock. Call this after `write_frame` only for + /// encoder output; imported files, pipes, and network media stay clock-free. + pub fn flush(&self, timestamp_us: u64) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?; + let mut guard = self.inner.lock().unwrap(); + let media = guard.as_mut().ok_or(MoqError::Closed)?; + media.import.flush(timestamp, std::time::Instant::now())?; + Ok(()) + } + + /// Mark a timeline break and restart handoff measurement without lowering advertised jitter. + /// + /// Publishes a discontinuity marker; resumed frames must continue the broadcast media clock, + /// and video must resume on a keyframe. + pub fn discontinuity(&self) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut guard = self.inner.lock().unwrap(); + let media = guard.as_mut().ok_or(MoqError::Closed)?; + media.import.discontinuity()?; + Ok(()) + } + + /// Draw a group boundary here. + /// + /// Audio has no boundary of its own (every packet is independently decodable), so this is the + /// only thing that gives it groups: call it after every frame for one group (one QUIC stream) + /// the relay forwards without waiting, or at a segment cadence to align with video for + /// HLS/DASH. Video groups at its own keyframes and needs this only to override that. + pub fn cut(&self) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut guard = self.inner.lock().unwrap(); + let media = guard.as_mut().ok_or(MoqError::Closed)?; + media + .import + .cut(None) + .map_err(|err| MoqError::Codec(format!("cut failed: {err}")))?; + Ok(()) + } + + /// Draw a group boundary and number the next group `sequence`. + /// + /// [`cut`](Self::cut) with an explicit sequence, for a publisher whose group numbers have to + /// be deterministic: two encoders aligning per GOP so a consumer can fail over between them. + pub fn seek(&self, sequence: u64) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut guard = self.inner.lock().unwrap(); + let media = guard.as_mut().ok_or(MoqError::Closed)?; + media + .import + .seek(sequence) + .map_err(|err| MoqError::Codec(format!("seek failed: {err}")))?; + Ok(()) + } + + /// Finish this track and finalize encoding. + pub fn finish(&self) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut guard = self.inner.lock().unwrap(); + let mut media = guard.take().ok_or(MoqError::Closed)?; + media + .import + .finish() + .map_err(|err| MoqError::Codec(format!("finish failed: {err}")))?; + Ok(()) + } +} + +#[uniffi::export] +impl MoqMediaContainerProducer { + /// Write a whole chunk of the container. + /// + /// No timestamp: a container carries its tracks' timing itself, and the importer reads it out + /// rather than taking the caller's word for it. + pub fn write(&self, payload: Vec) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut guard = self.inner.lock().unwrap(); + let container = guard.as_mut().ok_or(MoqError::Closed)?; + container + .import + .decode(&payload) + .map_err(|err| MoqError::Codec(format!("decode failed: {err}")))?; + Ok(()) + } + + /// Declare that the next chunk starts a new segment, rolling a group on every track this + /// publishes. + /// + /// For a caller that knows its source's segmentation out of band. An fMP4 source carrying + /// `styp` atoms declares its own, so this is only needed when it doesn't, and formats with no + /// segment concept (MKV, TS, FLV) ignore it. + pub fn cut(&self) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut guard = self.inner.lock().unwrap(); + let container = guard.as_mut().ok_or(MoqError::Closed)?; + container.import.cut(); + Ok(()) + } + + /// Start a new segment and number its groups `sequence`. + pub fn seek(&self, sequence: u64) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut guard = self.inner.lock().unwrap(); + let container = guard.as_mut().ok_or(MoqError::Closed)?; + container + .import + .seek(sequence) + .map_err(|err| MoqError::Codec(format!("seek failed: {err}")))?; + Ok(()) + } + + /// Finish every track this container publishes. + pub fn finish(&self) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut guard = self.inner.lock().unwrap(); + let mut container = guard.take().ok_or(MoqError::Closed)?; + container + .import + .finish() + .map_err(|err| MoqError::Codec(format!("finish failed: {err}")))?; + Ok(()) + } +} + +#[uniffi::export] +impl MoqMediaTrackStreamProducer { + /// A watch-only handle to whether this track has subscribers. + pub fn demand(&self) -> Result, MoqError> { + let guard = self.inner.lock().unwrap(); + Ok(MoqTrackDemand::new( + guard.as_ref().ok_or(MoqError::Closed)?.import.demand(), + )) + } + + /// Push raw stream bytes (e.g. Annex-B H.264 from an encoder). The importer frames whole access + /// units and keeps any partial trailing frame for the next call, so callers can write arbitrary + /// chunks. + pub fn write(&self, payload: Vec) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut guard = self.inner.lock().unwrap(); + let media = guard.as_mut().ok_or(MoqError::Closed)?; + media + .import + .decode(&payload) + .map_err(|err| MoqError::Codec(format!("decode failed: {err}")))?; + Ok(()) + } + + /// Finalize the track. + /// + /// The importer emits each access unit when the *next* one's start code arrives, so a trailing + /// access unit with no following delimiter (e.g. the last frame at EOF) is not emitted. This + /// matches moq-cli's stdin path. + pub fn finish(&self) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut guard = self.inner.lock().unwrap(); + let mut media = guard.take().ok_or(MoqError::Closed)?; + media + .import + .finish() + .map_err(|err| MoqError::Codec(format!("finish failed: {err}")))?; + Ok(()) + } +} + +#[uniffi::export] +impl MoqMediaContainerStreamProducer { + /// Push raw container bytes. The importer recovers its own framing, so callers can write + /// arbitrary chunks. + pub fn write(&self, payload: Vec) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut guard = self.inner.lock().unwrap(); + let container = guard.as_mut().ok_or(MoqError::Closed)?; + container + .import + .decode(&payload) + .map_err(|err| MoqError::Codec(format!("decode failed: {err}")))?; + Ok(()) + } + + /// Finish every track this container publishes. + pub fn finish(&self) -> Result<(), MoqError> { + let _guard = crate::ffi::enter(); + let mut guard = self.inner.lock().unwrap(); + let mut container = guard.take().ok_or(MoqError::Closed)?; + container + .import + .finish() + .map_err(|err| MoqError::Codec(format!("finish failed: {err}")))?; + Ok(()) + } +} + +/// Reserve the named track, or a unique one named after the format. +fn reserve_track( + broadcast: &moq_net::broadcast::Producer, + track: Option, + format: &impl std::fmt::Display, +) -> Result { + let name = track.unwrap_or_else(|| broadcast.unique_name(&format!(".{format}"))); + broadcast + .reserve_track(name) + .map_err(|err| MoqError::Codec(format!("init failed: {err}"))) +} + +/// A media track's name, container, and live delivery options. +#[derive(Clone, uniffi::Record)] +pub struct MoqMediaContainerConfig { + /// The track name. + pub name: String, + /// How to decode the track's frames. + pub container: MoqContainer, + /// Live delivery options, or defaults when absent. + #[uniffi(default = None)] + pub subscription: Option, +} + +/// A fetched media group's name, sequence, container, and delivery options. +#[derive(Clone, uniffi::Record)] +pub struct MoqMediaContainerGroupConfig { + /// The track name. + pub name: String, + /// The group sequence. + pub sequence: u64, + /// How to decode the group's frames. + pub container: MoqContainer, + /// Fetch delivery options, or defaults when absent. + #[uniffi(default = None)] + pub options: Option, +} + +fn media_frame(mut frame: moq_mux::container::Frame) -> Result { + let timestamp_us = timestamp_us(frame.timestamp)?; + let payload = frame.payload.copy_to_bytes(frame.payload.remaining()).to_vec(); + + Ok(MoqMediaFrame { + payload, + timestamp_us, + keyframe: frame.keyframe, + }) +} + +fn media_container(container: MoqContainer) -> Result { + let container: hang::catalog::Container = container.into(); + // This byte-oriented API has no media-kind configuration. + moq_mux::catalog::hang::Container::new(&container, moq_mux::container::Kind::Data) + .map_err(|e| MoqError::Codec(format!("invalid container: {e}"))) +} + +/// Reads the latest catalog snapshots from a broadcast. +#[derive(uniffi::Object)] +pub struct MoqMediaCatalogConsumer { + task: Task, +} + +struct Catalog { + // Consume with the untyped `Extra` extension so application sections survive into + // `MoqCatalog.sections` instead of being dropped. + inner: moq_mux::catalog::hang::Consumer, +} + +impl Catalog { + async fn next(&mut self) -> Result, MoqError> { + match self.inner.next().await { + Ok(Some(catalog)) => Ok(Some(convert_catalog(&catalog))), + Ok(None) => Ok(None), + Err(e) => Err(e.into()), + } + } +} + +/// Reads container-decoded frames from a live media track. +#[derive(uniffi::Object)] +pub struct MoqMediaContainerConsumer { + task: Task, +} + +struct Media { + inner: moq_mux::container::Consumer, +} + +impl Media { + async fn next(&mut self) -> Result, MoqError> { + self.inner.read().await?.map(media_frame).transpose() + } +} + +struct MediaGroupInner { + inner: moq_mux::container::GroupConsumer, +} + +impl MediaGroupInner { + async fn next(&mut self) -> Result, MoqError> { + self.inner.read().await?.map(media_frame).transpose() + } +} + +/// A finite, container-decoded media group returned by +/// [`MoqMediaContainerGroupConsumer::fetch`]. +#[derive(uniffi::Object)] +pub struct MoqMediaContainerGroupConsumer { + sequence: u64, + task: Task, +} + +impl MoqMediaContainerGroupConsumer { + fn from_inner(group: moq_net::group::Consumer, container: moq_mux::catalog::hang::Container) -> Self { + let inner = moq_mux::container::GroupConsumer::new(group, container); + Self { + sequence: inner.sequence(), + task: Task::new(MediaGroupInner { inner }), + } + } +} + +#[uniffi::export] +impl MoqMediaContainerGroupConsumer { + /// The sequence number of this group within the track. + pub fn sequence(&self) -> u64 { + self.sequence + } + + /// Read the next decoded media frame, or `None` when the group ends. + pub async fn next(&self) -> Result, MoqError> { + self.task.run(|mut state| async move { state.next().await }).await + } + + /// Cancel all current and future `next()` calls. + /// + /// Terminal: the subscription is released here, not when the handle is. + pub fn cancel(&self) { + self.task.cancel(); + } +} + +// ---- Catalog Consumer ---- + +#[uniffi::export] +impl MoqMediaCatalogConsumer { + /// Get the next catalog update. Returns `None` when the track ends or is closed. + pub async fn next(&self) -> Result, MoqError> { + self.task.run(|mut state| async move { state.next().await }).await + } + + /// Cancel all current and future `next()` calls. + /// + /// Terminal: the subscription is released here, not when the handle is. + pub fn cancel(&self) { + self.task.cancel(); + } +} + +// ---- Media Consumer ---- + +#[uniffi::export] +impl MoqMediaContainerConsumer { + /// Get the next frame. Returns `None` when the track ends or is closed. + pub async fn next(&self) -> Result, MoqError> { + self.task.run(|mut state| async move { state.next().await }).await + } + + /// Cancel all current and future `next()` calls. + /// + /// Terminal: the subscription is released here, not when the handle is. + pub fn cancel(&self) { + self.task.cancel(); + } +} + +#[uniffi::export] +impl MoqMediaCatalogConsumer { + /// Subscribe to this broadcast's catalog snapshots. + #[uniffi::constructor] + pub async fn subscribe(broadcast: &MoqBroadcastConsumer) -> Result, MoqError> { + let track = broadcast + .inner + .track(hang::catalog::Catalog::DEFAULT_NAME)? + .subscribe(hang::catalog::Catalog::default_subscription()) + .await?; + let consumer = moq_mux::catalog::hang::Consumer::from(track); + Ok(Arc::new(MoqMediaCatalogConsumer { + task: Task::new(Catalog { inner: consumer }), + })) + } +} + +#[uniffi::export] +impl MoqMediaContainerConsumer { + /// Subscribe to a media track and decode its container. + #[uniffi::constructor] + pub async fn subscribe( + broadcast: &MoqBroadcastConsumer, + config: MoqMediaContainerConfig, + ) -> Result, MoqError> { + let MoqMediaContainerConfig { + name, + container, + subscription, + } = config; + // Parse the container before subscribing so we don't leave a dangling + // subscription if init parsing fails. + let media = media_container(container)?; + let subscription = subscription.map(moq_net::track::Subscription::from).unwrap_or_default(); + let track = broadcast.inner.track(&name)?.subscribe(subscription).await?; + let consumer = moq_mux::container::Consumer::new(track, media); + Ok(Arc::new(MoqMediaContainerConsumer { + task: Task::new(Media { inner: consumer }), + })) + } +} + +#[uniffi::export] +impl MoqMediaContainerGroupConsumer { + /// Fetch and decode exactly one media group. + #[uniffi::constructor] + pub async fn fetch( + broadcast: &MoqBroadcastConsumer, + config: MoqMediaContainerGroupConfig, + ) -> Result, MoqError> { + let MoqMediaContainerGroupConfig { + name, + sequence, + container, + options, + } = config; + // Parse the container before fetching so invalid CMAF init data does not leave a + // dynamic group request waiting for a consumer that can never read it. + let media = media_container(container)?; + let options = options.map(moq_net::group::Fetch::from); + let track = broadcast.inner.track(&name).map_err(map_fetch_error)?; + let group = track.fetch_group(sequence, options).await.map_err(map_fetch_error)?; + Ok(Arc::new(MoqMediaContainerGroupConsumer::from_inner(group, media))) + } +} + +#[cfg(test)] +mod catalog_tests { + use super::*; + + #[test] + fn catalog_handle_keeps_no_broadcast_open() { + let broadcast = MoqBroadcastProducer::new().unwrap(); + let catalog = MoqMediaCatalogProducer::new(broadcast.clone()).unwrap(); + drop(broadcast); + assert!(matches!( + catalog.set_section("app".into(), "{}".into()), + Err(MoqError::Closed) + )); + assert!(matches!(catalog.remove_section("app".into()), Err(MoqError::Closed))); + } + + #[test] + fn catalog_refuses_writes_after_broadcast_close() { + let broadcast = MoqBroadcastProducer::new().unwrap(); + let catalog = MoqMediaCatalogProducer::new(broadcast.clone()).unwrap(); + broadcast.close().unwrap(); + assert!(matches!( + catalog.set_video_properties(MoqVideoProperties::default()), + Err(MoqError::Closed) + )); + assert!(matches!(MoqMediaCatalogProducer::new(broadcast), Err(MoqError::Closed))); + } + + #[tokio::test] + async fn catalog_handle_updates_sections() { + let broadcast = MoqBroadcastProducer::new().unwrap(); + let catalog = MoqMediaCatalogProducer::new(broadcast.clone()).unwrap(); + let consumer = MoqMediaCatalogConsumer::subscribe(&broadcast.consume().unwrap()) + .await + .unwrap(); + catalog.set_section("app".into(), "{\"value\":42}".into()).unwrap(); + let snapshot = consumer.next().await.unwrap().unwrap(); + assert_eq!(snapshot.sections["app"], "{\"value\":42}"); + catalog.remove_section("app".into()).unwrap(); + assert!(!consumer.next().await.unwrap().unwrap().sections.contains_key("app")); + assert!(catalog.set_section("video".into(), "{}".into()).is_err()); + assert!(catalog.set_section("app".into(), "{".into()).is_err()); + consumer.cancel(); + } +} diff --git a/rs/moq-ffi/src/producer.rs b/rs/moq-ffi/src/producer.rs index 01e393b8b09..7c47b7ed500 100644 --- a/rs/moq-ffi/src/producer.rs +++ b/rs/moq-ffi/src/producer.rs @@ -6,7 +6,7 @@ use crate::consumer::{MoqBroadcastConsumer, MoqGroupConsumer, MoqSubscription, M use crate::demand::MoqTrackDemand; use crate::error::MoqError; use crate::ffi::Task; -use crate::media::{MoqAudioInit, MoqContainerFormat, MoqContainerInit, MoqFrame, MoqVideoInit, MoqVideoProperties}; +use crate::media::MoqFrame; /// Publisher-side track properties, mirroring [`moq_net::track::Info`]. /// @@ -80,36 +80,9 @@ pub(crate) struct BroadcastProducer { pub(crate) catalog: moq_mux::catalog::Producer, } -/// A whole-frame importer for one codec track. -/// -/// Separate from [`ContainerProducer`] because a container publishes several tracks and takes -/// chunks rather than timestamped frames. Sharing one type meant a `write_frame` timestamp that one -/// arm silently dropped. -struct MediaProducer { - // Boxed because the codec splitters/imports make this much larger than the container one. - import: Box, - /// Subscriber demand (name/used/unused) for the one track this publishes. - demand: moq_net::track::Demand, -} - -/// A whole-chunk importer for a container, which may publish several tracks. -struct ContainerProducer { - import: moq_mux::import::Container, -} - -/// A byte-stream importer for one codec track, where frame boundaries are inferred. -struct MediaStreamProducer { - import: Box, -} - -/// A byte-stream importer for a container, which recovers its own framing. -struct ContainerStreamProducer { - import: moq_mux::import::ContainerStream, -} - #[derive(uniffi::Object)] pub struct MoqBroadcastProducer { - state: std::sync::Mutex>, + pub(crate) state: std::sync::Mutex>, } #[derive(uniffi::Object)] @@ -187,26 +160,6 @@ impl MoqBroadcastProducer { } } -#[derive(uniffi::Object)] -pub struct MoqContainerProducer { - inner: std::sync::Mutex>, -} - -#[derive(uniffi::Object)] -pub struct MoqContainerStreamProducer { - inner: std::sync::Mutex>, -} - -#[derive(uniffi::Object)] -pub struct MoqMediaProducer { - inner: std::sync::Mutex>, -} - -#[derive(uniffi::Object)] -pub struct MoqMediaStreamProducer { - inner: std::sync::Mutex>, -} - #[uniffi::export] impl MoqBroadcastProducer { /// Create a consumer that reads from this broadcast's tracks. @@ -268,195 +221,6 @@ impl MoqBroadcastProducer { }) } - /// Replace the catalog properties shared by every video rendition. - /// - /// Rotation is clockwise and normalized to the nearest quarter turn. An absent field is removed from the next catalog update. - pub fn set_video_properties(&self, properties: MoqVideoProperties) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut value = hang::catalog::VideoProperties::default(); - value.display = properties.display.map(|display| hang::catalog::Display { - width: display.width, - height: display.height, - }); - value.rotation = properties.rotation; - value.flip = properties.flip; - - self.with_state(|state| { - let mut catalog = state.catalog.modify()?; - catalog.video.set_properties(value)?; - catalog.commit()?; - Ok(()) - }) - } - - /// Set (or replace) a top-level application catalog section by name. - /// - /// `json` is any JSON document (object, array, string, ...) serialized as a UTF-8 string. - /// Errors with [`MoqError::Json`] if `json` doesn't parse, or with the reserved-section - /// error if `name` is a HANG root (`video`, `audio`, `text`, `archive`, `clock`, `json`, - /// `binary`, or retired `timeline`) or an MSF root (`version`, `generatedAt`, `isComplete`, - /// `tracks`, or `initDataList`). The section is republished on the catalog track immediately. - pub fn set_catalog_section(&self, name: String, json: String) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let value: serde_json::Value = serde_json::from_str(&json)?; - self.with_state(|state| { - let mut guard = state.catalog.modify()?; - guard.set_section(name, value)?; - guard.commit()?; - Ok(()) - }) - } - - /// Remove a top-level application catalog section by name. - /// - /// Republishes the catalog if the section existed; a no-op otherwise. - pub fn remove_catalog_section(&self, name: String) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - self.with_state(|state| { - let mut guard = state.catalog.modify()?; - guard.remove_section(&name); - guard.commit()?; - Ok(()) - }) - } - - /// Publish one audio codec as a new track. - /// - /// The track is [`MoqAudioInit::track`], or else named after the format (`0.opus`), so the - /// catalog is how a subscriber finds it. [`MoqAudioInit::data`] is required: audio resolves its - /// rendition entirely from those bytes. - pub fn publish_audio(&self, mut init: MoqAudioInit) -> Result, MoqError> { - let _guard = crate::ffi::enter(); - let guard = self.state.lock().unwrap(); - let state = guard.as_ref().ok_or(MoqError::Closed)?; - - let track = init.track.take(); - let init: moq_mux::import::AudioInit = init.into(); - let request = reserve_track(&state.broadcast, track, &init.format)?; - - let import = moq_mux::import::Track::audio(request, state.catalog.reserve(), init) - .map_err(|err| MoqError::Codec(format!("init failed: {err}")))?; - Ok(MoqMediaProducer::new(import)) - } - - /// Publish one video codec as a new track. - /// - /// Named as in [`publish_audio`](Self::publish_audio). [`MoqVideoInit::data`] may be empty for a - /// format that resolves in band; a hint carrying the codec publishes the catalog before the - /// first keyframe. - pub fn publish_video(&self, mut init: MoqVideoInit) -> Result, MoqError> { - let _guard = crate::ffi::enter(); - let guard = self.state.lock().unwrap(); - let state = guard.as_ref().ok_or(MoqError::Closed)?; - - let track = init.track.take(); - let init: moq_mux::import::VideoInit = init.into(); - let request = reserve_track(&state.broadcast, track, &init.format)?; - - let import = moq_mux::import::Track::video(request, state.catalog.reserve(), init) - .map_err(|err| MoqError::Codec(format!("init failed: {err}")))?; - Ok(MoqMediaProducer::new(import)) - } - - /// Publish a container, which demuxes and publishes its own tracks. - /// - /// Unlike the codec entry points there is no label or hint: a container describes each track it - /// publishes from its own metadata, so a rendition field would have no single track to land on. - pub fn publish_container(&self, init: MoqContainerInit) -> Result, MoqError> { - let _guard = crate::ffi::enter(); - let guard = self.state.lock().unwrap(); - let state = guard.as_ref().ok_or(MoqError::Closed)?; - - let init: moq_mux::import::ContainerInit = init.into(); - let import = moq_mux::import::Container::new(state.broadcast.clone(), state.catalog.reserve(), &init) - .map_err(|err| MoqError::Codec(format!("init failed: {err}")))?; - - Ok(Arc::new(MoqContainerProducer { - inner: std::sync::Mutex::new(Some(ContainerProducer { import })), - })) - } - - /// Publish one audio codec onto a track requested through - /// [`MoqBroadcastDynamic::requested_track`], which the importer accepts. - pub fn publish_audio_on_track( - &self, - request: &MoqTrackRequest, - init: MoqAudioInit, - ) -> Result, MoqError> { - let _guard = crate::ffi::enter(); - let guard = self.state.lock().unwrap(); - let state = guard.as_ref().ok_or(MoqError::Closed)?; - - if init.track.is_some() { - return Err(MoqError::Codec("a requested track already has a name".into())); - } - let request = request.take()?; - let import = moq_mux::import::Track::audio(request, state.catalog.reserve(), init.into()) - .map_err(|err| MoqError::Codec(format!("init failed: {err}")))?; - Ok(MoqMediaProducer::new(import)) - } - - /// Publish one video codec onto a requested track. See - /// [`publish_audio_on_track`](Self::publish_audio_on_track). - pub fn publish_video_on_track( - &self, - request: &MoqTrackRequest, - init: MoqVideoInit, - ) -> Result, MoqError> { - let _guard = crate::ffi::enter(); - let guard = self.state.lock().unwrap(); - let state = guard.as_ref().ok_or(MoqError::Closed)?; - - if init.track.is_some() { - return Err(MoqError::Codec("a requested track already has a name".into())); - } - let request = request.take()?; - let import = moq_mux::import::Track::video(request, state.catalog.reserve(), init.into()) - .map_err(|err| MoqError::Codec(format!("init failed: {err}")))?; - Ok(MoqMediaProducer::new(import)) - } - - /// Publish one video codec fed by a raw byte stream, inferring frame boundaries. - /// - /// Only the self-delimiting formats work here (`Avc3`, `Hev1`, `Av01`); the rest need length - /// prefixes or an out-of-band config record. There is no audio counterpart for the same reason. - pub fn publish_video_stream(&self, mut init: MoqVideoInit) -> Result, MoqError> { - let _guard = crate::ffi::enter(); - let guard = self.state.lock().unwrap(); - let state = guard.as_ref().ok_or(MoqError::Closed)?; - - let track = init.track.take(); - let init: moq_mux::import::VideoInit = init.into(); - let request = reserve_track(&state.broadcast, track, &init.format)?; - - let import = moq_mux::import::TrackStream::video(request, state.catalog.reserve(), init) - .map_err(|err| MoqError::Codec(format!("init failed: {err}")))?; - - Ok(Arc::new(MoqMediaStreamProducer { - inner: std::sync::Mutex::new(Some(MediaStreamProducer { - import: Box::new(import), - })), - })) - } - - /// Publish a container fed by a raw byte stream, which recovers its own framing. - pub fn publish_container_stream( - &self, - format: MoqContainerFormat, - ) -> Result, MoqError> { - let _guard = crate::ffi::enter(); - let guard = self.state.lock().unwrap(); - let state = guard.as_ref().ok_or(MoqError::Closed)?; - - let import = - moq_mux::import::ContainerStream::new(state.broadcast.clone(), state.catalog.reserve(), format.into()) - .map_err(|err| MoqError::Codec(format!("init failed: {err}")))?; - - Ok(Arc::new(MoqContainerStreamProducer { - inner: std::sync::Mutex::new(Some(ContainerStreamProducer { import })), - })) - } - /// Create a track for arbitrary byte payloads, no codec or container. /// /// Same pattern as moq-boy's `status` and `command` tracks: raw UTF-8/JSON @@ -501,7 +265,7 @@ impl MoqBroadcastDynamic { /// /// Returns a [`MoqTrackRequest`]: accept it for raw writes with /// [`MoqTrackRequest::accept`], publish media onto it with - /// [`MoqBroadcastProducer::publish_audio_on_track`], or reject it with + /// [`crate::media::MoqMediaTrackProducer::audio`], or reject it with /// [`MoqTrackRequest::abort`]. The requesting subscriber stays pending until then. /// /// Returns an error once the broadcast is closed or aborted. @@ -600,7 +364,7 @@ impl MoqGroupRequest { /// A track requested by a subscriber that hasn't been accepted yet. /// /// Mirrors [`moq_net::track::Request`]: [`accept`](Self::accept) it to start producing raw -/// frames, hand it to [`MoqBroadcastProducer::publish_audio_on_track`] to publish media, +/// frames, hand it to [`crate::media::MoqMediaTrackProducer::audio`] to publish media, /// or [`abort`](Self::abort) it to reject the waiting subscriber. #[derive(uniffi::Object)] pub struct MoqTrackRequest { @@ -615,7 +379,7 @@ impl MoqTrackRequest { } /// Take the inner request so an importer can accept it (setting the timescale). Used by - /// [`MoqBroadcastProducer::publish_audio_on_track`]. + /// [`crate::media::MoqMediaTrackProducer::audio`]. pub(crate) fn take(&self) -> Result { self.inner.lock().unwrap().take().ok_or(MoqError::Closed) } @@ -644,7 +408,7 @@ impl MoqTrackRequest { /// Accept the request as a raw track, fixing its [`MoqTrackInfo`] (timescale, etc.). /// - /// For media use [`MoqBroadcastProducer::publish_audio_on_track`] instead, which lets + /// For media use [`crate::media::MoqMediaTrackProducer::audio`] instead, which lets /// the importer pick the timescale. pub fn accept(&self, info: Option) -> Result, MoqError> { let _guard = crate::ffi::enter(); @@ -677,18 +441,23 @@ impl MoqTrackProducer { let track = guard.as_ref().ok_or(MoqError::Closed)?; Ok(track.demand()) } + + /// Hand the track to a typed writer that `f` builds (a JSON producer, say), closing this + /// handle once `f` succeeds so the writer is the track's only producer. On failure the + /// handle stays open. + pub(crate) fn adopt( + &self, + f: impl FnOnce(moq_net::track::Producer) -> Result, + ) -> Result { + let mut guard = self.inner.lock().unwrap(); + let out = f(guard.as_ref().ok_or(MoqError::Closed)?.clone())?; + guard.take(); + Ok(out) + } } #[uniffi::export] impl MoqTrackProducer { - /// Return the name of this track. - pub fn name(&self) -> Result { - let _guard = crate::ffi::enter(); - let guard = self.inner.lock().unwrap(); - let track = guard.as_ref().ok_or(MoqError::Closed)?; - Ok(track.name().to_string()) - } - /// Create a handler for uncached group fetches on this track. /// /// Hold the returned object for as long as cache misses should wait to be @@ -700,27 +469,11 @@ impl MoqTrackProducer { Ok(Arc::new(MoqTrackDynamic::new(track.dynamic()))) } - /// A watch-only handle to whether this track has subscribers. + /// A watch-only handle to this track's name and whether it has subscribers. pub fn demand(&self) -> Result, MoqError> { Ok(MoqTrackDemand::new(self.track_demand()?)) } - /// Wait until this track has at least one active consumer. - /// - /// Prefer [`demand`](Self::demand), a handle that can wait without borrowing this producer. - pub async fn used(&self) -> Result<(), MoqError> { - let track = self.inner.lock().unwrap().as_ref().ok_or(MoqError::Closed)?.clone(); - crate::ffi::detached(async move { track.demand().used().await }).await - } - - /// Wait until this track has no active consumers. - /// - /// Prefer [`demand`](Self::demand), a handle that can wait without borrowing this producer. - pub async fn unused(&self) -> Result<(), MoqError> { - let track = self.inner.lock().unwrap().as_ref().ok_or(MoqError::Closed)?.clone(); - crate::ffi::detached(async move { track.demand().unused().await }).await - } - /// Create a consumer that reads from this producer's track. /// /// Useful for local pub/sub without going through an origin/broadcast. `subscription` @@ -891,289 +644,6 @@ impl MoqGroupProducer { } } -// ---- Media Producer ---- - -impl MoqMediaProducer { - /// Wrap a single-codec importer, capturing the demand handle its track exposes. - fn new(import: moq_mux::import::Track) -> Arc { - let demand = import.demand(); - Arc::new(Self { - inner: std::sync::Mutex::new(Some(MediaProducer { - import: Box::new(import), - demand, - })), - }) - } -} - -#[uniffi::export] -impl MoqMediaProducer { - /// The name of the track this publishes. - pub fn name(&self) -> Result { - let _guard = crate::ffi::enter(); - let guard = self.inner.lock().unwrap(); - let media = guard.as_ref().ok_or(MoqError::Closed)?; - Ok(media.demand.name().to_string()) - } - - /// A watch-only handle to whether this track has subscribers. - pub fn demand(&self) -> Result, MoqError> { - let guard = self.inner.lock().unwrap(); - Ok(MoqTrackDemand::new( - guard.as_ref().ok_or(MoqError::Closed)?.demand.clone(), - )) - } - - /// Wait until this track has at least one active consumer. - /// - /// Prefer [`demand`](Self::demand), a handle that can wait without borrowing this producer. - pub async fn used(&self) -> Result<(), MoqError> { - let demand = self - .inner - .lock() - .unwrap() - .as_ref() - .ok_or(MoqError::Closed)? - .demand - .clone(); - crate::ffi::detached(async move { demand.used().await }).await - } - - /// Wait until this track has no active consumers. - /// - /// Prefer [`demand`](Self::demand), a handle that can wait without borrowing this producer. - pub async fn unused(&self) -> Result<(), MoqError> { - let demand = self - .inner - .lock() - .unwrap() - .as_ref() - .ok_or(MoqError::Closed)? - .demand - .clone(); - crate::ffi::detached(async move { demand.unused().await }).await - } - - /// Write `frame` to this track. - /// - /// The importer derives keyframe status from the bitstream, so a [`MoqFrame`] carries only the - /// payload and its timestamp. - pub fn write_frame(&self, frame: MoqFrame) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut guard = self.inner.lock().unwrap(); - let media = guard.as_mut().ok_or(MoqError::Closed)?; - - let timestamp = frame - .timestamp_us - .map(hang::container::Timestamp::from_micros) - .transpose()?; - media - .import - .decode(&frame.payload, timestamp) - .map_err(|err| MoqError::Codec(format!("decode failed: {err}")))?; - - Ok(()) - } - - /// Record a locally encoded frame's handoff for catalog jitter measurement. - /// - /// `timestamp_us` is on the broadcast media clock. Call this after `write_frame` only for - /// encoder output; imported files, pipes, and network media stay clock-free. - pub fn flush(&self, timestamp_us: u64) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?; - let mut guard = self.inner.lock().unwrap(); - let media = guard.as_mut().ok_or(MoqError::Closed)?; - media.import.flush(timestamp, std::time::Instant::now())?; - Ok(()) - } - - /// Mark a timeline break and restart handoff measurement without lowering advertised jitter. - /// - /// Publishes a discontinuity marker; resumed frames must continue the broadcast media clock, - /// and video must resume on a keyframe. - pub fn discontinuity(&self) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut guard = self.inner.lock().unwrap(); - let media = guard.as_mut().ok_or(MoqError::Closed)?; - media.import.discontinuity()?; - Ok(()) - } - - /// Draw a group boundary here. - /// - /// Audio has no boundary of its own (every packet is independently decodable), so this is the - /// only thing that gives it groups: call it after every frame for one group (one QUIC stream) - /// the relay forwards without waiting, or at a segment cadence to align with video for - /// HLS/DASH. Video groups at its own keyframes and needs this only to override that. - pub fn cut(&self) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut guard = self.inner.lock().unwrap(); - let media = guard.as_mut().ok_or(MoqError::Closed)?; - media - .import - .cut(None) - .map_err(|err| MoqError::Codec(format!("cut failed: {err}")))?; - Ok(()) - } - - /// Draw a group boundary and number the next group `sequence`. - /// - /// [`cut`](Self::cut) with an explicit sequence, for a publisher whose group numbers have to - /// be deterministic: two encoders aligning per GOP so a consumer can fail over between them. - pub fn seek(&self, sequence: u64) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut guard = self.inner.lock().unwrap(); - let media = guard.as_mut().ok_or(MoqError::Closed)?; - media - .import - .seek(sequence) - .map_err(|err| MoqError::Codec(format!("seek failed: {err}")))?; - Ok(()) - } - - /// Finish this track and finalize encoding. - pub fn finish(&self) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut guard = self.inner.lock().unwrap(); - let mut media = guard.take().ok_or(MoqError::Closed)?; - media - .import - .finish() - .map_err(|err| MoqError::Codec(format!("finish failed: {err}")))?; - Ok(()) - } -} - -#[uniffi::export] -impl MoqContainerProducer { - /// Write a whole chunk of the container. - /// - /// No timestamp: a container carries its tracks' timing itself, and the importer reads it out - /// rather than taking the caller's word for it. - pub fn write(&self, payload: Vec) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut guard = self.inner.lock().unwrap(); - let container = guard.as_mut().ok_or(MoqError::Closed)?; - container - .import - .decode(&payload) - .map_err(|err| MoqError::Codec(format!("decode failed: {err}")))?; - Ok(()) - } - - /// Declare that the next chunk starts a new segment, rolling a group on every track this - /// publishes. - /// - /// For a caller that knows its source's segmentation out of band. An fMP4 source carrying - /// `styp` atoms declares its own, so this is only needed when it doesn't, and formats with no - /// segment concept (MKV, TS, FLV) ignore it. - pub fn cut(&self) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut guard = self.inner.lock().unwrap(); - let container = guard.as_mut().ok_or(MoqError::Closed)?; - container.import.cut(); - Ok(()) - } - - /// Start a new segment and number its groups `sequence`. - pub fn seek(&self, sequence: u64) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut guard = self.inner.lock().unwrap(); - let container = guard.as_mut().ok_or(MoqError::Closed)?; - container - .import - .seek(sequence) - .map_err(|err| MoqError::Codec(format!("seek failed: {err}")))?; - Ok(()) - } - - /// Finish every track this container publishes. - pub fn finish(&self) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut guard = self.inner.lock().unwrap(); - let mut container = guard.take().ok_or(MoqError::Closed)?; - container - .import - .finish() - .map_err(|err| MoqError::Codec(format!("finish failed: {err}")))?; - Ok(()) - } -} - -#[uniffi::export] -impl MoqMediaStreamProducer { - /// Push raw stream bytes (e.g. Annex-B H.264 from an encoder). The importer frames whole access - /// units and keeps any partial trailing frame for the next call, so callers can write arbitrary - /// chunks. - pub fn write(&self, payload: Vec) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut guard = self.inner.lock().unwrap(); - let media = guard.as_mut().ok_or(MoqError::Closed)?; - media - .import - .decode(&payload) - .map_err(|err| MoqError::Codec(format!("decode failed: {err}")))?; - Ok(()) - } - - /// Finalize the track. - /// - /// The importer emits each access unit when the *next* one's start code arrives, so a trailing - /// access unit with no following delimiter (e.g. the last frame at EOF) is not emitted. This - /// matches moq-cli's stdin path. - pub fn finish(&self) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut guard = self.inner.lock().unwrap(); - let mut media = guard.take().ok_or(MoqError::Closed)?; - media - .import - .finish() - .map_err(|err| MoqError::Codec(format!("finish failed: {err}")))?; - Ok(()) - } -} - -#[uniffi::export] -impl MoqContainerStreamProducer { - /// Push raw container bytes. The importer recovers its own framing, so callers can write - /// arbitrary chunks. - pub fn write(&self, payload: Vec) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut guard = self.inner.lock().unwrap(); - let container = guard.as_mut().ok_or(MoqError::Closed)?; - container - .import - .decode(&payload) - .map_err(|err| MoqError::Codec(format!("decode failed: {err}")))?; - Ok(()) - } - - /// Finish every track this container publishes. - pub fn finish(&self) -> Result<(), MoqError> { - let _guard = crate::ffi::enter(); - let mut guard = self.inner.lock().unwrap(); - let mut container = guard.take().ok_or(MoqError::Closed)?; - container - .import - .finish() - .map_err(|err| MoqError::Codec(format!("finish failed: {err}")))?; - Ok(()) - } -} - -/// Reserve the named track, or a unique one named after the format. -fn reserve_track( - broadcast: &moq_net::broadcast::Producer, - track: Option, - format: &impl std::fmt::Display, -) -> Result { - let name = track.unwrap_or_else(|| broadcast.unique_name(&format!(".{format}"))); - broadcast - .reserve_track(name) - .map_err(|err| MoqError::Codec(format!("init failed: {err}"))) -} - #[cfg(test)] mod metadata_tests { use super::*; diff --git a/rs/moq-ffi/src/server.rs b/rs/moq-ffi/src/server.rs index c42937fb7a2..bd32f9251da 100644 --- a/rs/moq-ffi/src/server.rs +++ b/rs/moq-ffi/src/server.rs @@ -4,16 +4,89 @@ use std::sync::Arc; use crate::error::MoqError; use crate::ffi::Task; use crate::origin::MoqOriginProducer; -use crate::session::MoqSession; +use crate::session::{MoqQuicConfig, MoqSession}; + +/// Configuration for [`MoqServer::new`], mirroring moq-tokio's server config. +/// +/// Every field has a default, so set only what you need. The TLS identity needs one of +/// `tls.generate` or a `tls.cert`/`tls.key` pair. +#[derive(Clone, Default, uniffi::Record)] +pub struct MoqServerConfig { + /// Address to bind, e.g. `127.0.0.1:4443`, `[::]:443`, or `localhost:0`. Null binds `[::]:443`. + /// + /// DNS hostnames are resolved when [`MoqServer::listen`] binds. + #[uniffi(default = None)] + pub bind: Option, + /// Protocol versions to accept, spelled like `moq-lite-03`. Empty accepts every supported version. + #[uniffi(default = [])] + pub versions: Vec, + /// The served TLS identity. + #[uniffi(default)] + pub tls: MoqServerTls, + /// QUIC transport tuning. + #[uniffi(default)] + pub quic: MoqQuicConfig, + /// The origin whose broadcasts are served to incoming sessions. + /// + /// With neither `publish` nor `consume` set, each session's two sides share one fresh + /// origin. A [`MoqRequest`] can override either side in [`accept`](MoqRequest::accept). + #[uniffi(default = None)] + pub publish: Option>, + /// The origin that receives broadcasts published by incoming sessions. See `publish`. + #[uniffi(default = None)] + pub consume: Option>, +} + +/// The served TLS identity for a [`MoqServerConfig`]. +#[derive(Clone, Debug, Default, uniffi::Record)] +pub struct MoqServerTls { + /// PEM certificate chain files, one per identity. + #[uniffi(default = [])] + pub cert: Vec, + /// PEM private key files, paired with `cert` in order. + #[uniffi(default = [])] + pub key: Vec, + /// Hostnames to generate a self-signed certificate for. + /// + /// Clients must either pin the certificate fingerprint or disable verification. + #[uniffi(default = [])] + pub generate: Vec, +} struct ServerState { config: moq_tokio::listen::Config, + quic: moq_tokio::quic::Config, publish: Option>, consume: Option>, server: Option, } impl ServerState { + fn new(config: MoqServerConfig) -> Result { + let mut listen = moq_tokio::listen::Config::default(); + if let Some(bind) = config.bind { + let parsed = bind + .parse() + .map_err(|_| MoqError::Config(format!("invalid bind address: {bind}")))?; + listen.bind = Some(parsed); + } + listen.version = crate::session::parse_versions(&config.versions)?; + listen.tls.cert = config.tls.cert.into_iter().map(PathBuf::from).collect(); + listen.tls.key = config.tls.key.into_iter().map(PathBuf::from).collect(); + listen.tls.generate = config.tls.generate; + + let mut quic = moq_tokio::quic::Config::default(); + quic.max_streams = config.quic.max_streams; + + Ok(Self { + config: listen, + quic, + publish: config.publish, + consume: config.consume, + server: None, + }) + } + async fn listen(&mut self) -> Result { if self.server.is_some() { return Err(MoqError::Bind("already listening".into())); @@ -21,7 +94,7 @@ impl ServerState { let server = self .config .clone() - .init(Default::default()) + .init(self.quic.clone()) .map_err(|err| MoqError::Bind(format!("{err}")))? .listen() .await @@ -49,103 +122,22 @@ impl ServerState { } /// A MoQ server that accepts incoming QUIC/WebTransport sessions. -/// -/// Bind and TLS are captured at [`listen`](Self::listen); those setters fail -/// afterwards. Origins are captured at each [`accept`](Self::accept). Every setter -/// fails with [`MoqError::Busy`] while listen/accept is in flight and -/// [`MoqError::Cancelled`] after [`cancel`](Self::cancel). #[derive(uniffi::Object)] pub struct MoqServer { task: Task, } -impl MoqServer { - fn configure(&self, f: impl FnOnce(&mut ServerState) -> R) -> Result { - Ok(f(&mut *self.task.configure()?)) - } - - fn configure_listen(&self, f: impl FnOnce(&mut ServerState) -> R) -> Result { - let mut state = self.task.configure()?; - if state.server.is_some() { - return Err(MoqError::Bind("already listening".into())); - } - Ok(f(&mut state)) - } -} - #[uniffi::export] impl MoqServer { - /// Create a new MoQ server with default configuration. + /// Create a server from `config`, failing on any value it cannot use. + /// + /// Nothing is bound until [`listen`](Self::listen). #[uniffi::constructor] - pub fn new() -> Arc { + pub fn new(config: MoqServerConfig) -> Result, MoqError> { let _guard = crate::ffi::runtime().enter(); - Arc::new(Self { - task: Task::new(ServerState { - config: moq_tokio::listen::Config::default(), - publish: None, - consume: None, - server: None, - }), - }) - } - - /// Set the address to bind, e.g. `127.0.0.1:4443`, `[::]:443`, or `localhost:0`. - /// - /// Validated syntactically up-front. DNS hostnames are accepted and resolved - /// at `listen()` time. Captured at [`listen`](Self::listen); fails afterwards. - pub fn set_bind(&self, addr: String) -> Result<(), MoqError> { - let bind = addr - .parse() - .map_err(|_| MoqError::Bind(format!("invalid bind address: {addr}")))?; - self.configure_listen(|state| { - state.config.bind = Some(bind); - }) - } - - /// Load TLS certificate chains from PEM files on disk. - /// - /// Captured at [`listen`](Self::listen); fails afterwards. - pub fn set_tls_cert(&self, paths: Vec) -> Result<(), MoqError> { - self.configure_listen(|state| { - state.config.tls.cert = paths.into_iter().map(PathBuf::from).collect(); - }) - } - - /// Load TLS private keys from PEM files on disk. - /// - /// Captured at [`listen`](Self::listen); fails afterwards. - pub fn set_tls_key(&self, paths: Vec) -> Result<(), MoqError> { - self.configure_listen(|state| { - state.config.tls.key = paths.into_iter().map(PathBuf::from).collect(); - }) - } - - /// Generate self-signed TLS certificates for the given hostnames. - /// - /// Clients must either pin the certificate fingerprint or disable verification. - /// Captured at [`listen`](Self::listen); fails afterwards. - pub fn set_tls_generate(&self, hostnames: Vec) -> Result<(), MoqError> { - self.configure_listen(|state| { - state.config.tls.generate = hostnames; - }) - } - - /// Set the origin to publish broadcasts to incoming sessions. - /// - /// Captured at each [`accept`](Self::accept). - pub fn set_publish(&self, origin: Option>) -> Result<(), MoqError> { - self.configure(|state| { - state.publish = origin; - }) - } - - /// Set the origin to consume broadcasts from incoming sessions. - /// - /// Captured at each [`accept`](Self::accept). - pub fn set_consume(&self, origin: Option>) -> Result<(), MoqError> { - self.configure(|state| { - state.consume = origin; - }) + Ok(Arc::new(Self { + task: Task::new(ServerState::new(config)?), + })) } /// Bind the listening socket. Returns the bound local address as a string, @@ -166,7 +158,7 @@ impl MoqServer { /// /// Useful for pinning a generated self-signed certificate in a browser via /// WebTransport's `serverCertificateHashes`. Returns an error if called - /// before `listen()`. + /// before `listen()`, and [`MoqError::Busy`] while `listen()` or `accept()` is in flight. pub fn cert_fingerprints(&self) -> Result, MoqError> { let state = self.task.configure()?; let server = state @@ -256,9 +248,9 @@ mod transport_tests { /// An incoming MoQ session that can be accepted or rejected. /// -/// Origin overrides are captured at [`accept`](Self::accept). Setters fail with -/// [`MoqError::Busy`] while accept/reject is in flight, [`MoqError::AlreadyResponded`] -/// after a response, and [`MoqError::Cancelled`] after [`cancel`](Self::cancel). +/// Origin arguments are captured when [`accept`](Self::accept) starts. A second +/// response fails with [`MoqError::AlreadyResponded`], and calls after +/// [`cancel`](Self::cancel) fail with [`MoqError::Cancelled`]. #[derive(uniffi::Object)] pub struct MoqRequest { task: Task, @@ -290,15 +282,6 @@ impl MoqRequest { query, })) } - - fn configure_origin(&self, f: impl FnOnce(&mut RequestState)) -> Result<(), MoqError> { - let mut state = self.task.configure()?; - if state.request.is_none() { - return Err(MoqError::AlreadyResponded); - } - f(&mut state); - Ok(()) - } } #[cfg(test)] @@ -306,7 +289,7 @@ impl MoqRequest { /// Hold the request lock until `held` finishes. /// /// `accept`/`reject` use the same `Task::run` path; a live handshake can - /// finish before a waiter samples `Busy`. + /// finish before a queued accept samples the locked state. pub(crate) async fn hold_lock(&self, held: F) -> Result<(), MoqError> where F: FnOnce() -> Fut + Send + 'static, @@ -344,32 +327,26 @@ impl MoqRequest { self.transport } - /// Override the publish origin for this session. Falls back to the server's - /// configured publish origin if unset. Captured at [`accept`](Self::accept). - pub fn set_publish(&self, origin: Option>) -> Result<(), MoqError> { - self.configure_origin(|state| { - state.publish = origin; - }) - } - - /// Override the consume origin for this session. Falls back to the server's - /// configured consume origin if unset. Captured at [`accept`](Self::accept). - pub fn set_consume(&self, origin: Option>) -> Result<(), MoqError> { - self.configure_origin(|state| { - state.consume = origin; - }) - } - /// Complete the MoQ handshake and return the established session. /// - /// Returns `AlreadyResponded` if `accept()` or `reject()` has already been called. - pub async fn accept(&self) -> Result, MoqError> { + /// A null origin inherits the server's configured origin; a supplied origin replaces it. + /// Pass a fresh origin for isolation, or the same fresh origin on both sides to share it. + /// Returns `AlreadyResponded` after a response and `Cancelled` after cancellation. + #[uniffi::method(default(publish = None, consume = None))] + pub async fn accept( + &self, + publish: Option>, + consume: Option>, + ) -> Result, MoqError> { self.task - .run(|mut state| async move { + .run(move |mut state| async move { let request = state.request.take().ok_or(MoqError::AlreadyResponded)?; // Materialize both origin sides so the session can publish/subscribe and the // FFI can hand back a publisher/consumer. - let (publish, subscribe) = crate::origin::resolve_pair(state.publish.as_ref(), state.consume.as_ref()); + let (publish, subscribe) = crate::origin::resolve_pair( + publish.as_ref().or(state.publish.as_ref()), + consume.as_ref().or(state.consume.as_ref()), + ); let session = request .with_publisher(&publish) .with_subscriber(subscribe.clone()) diff --git a/rs/moq-ffi/src/session.rs b/rs/moq-ffi/src/session.rs index e6a8f19e948..faeaca31d26 100644 --- a/rs/moq-ffi/src/session.rs +++ b/rs/moq-ffi/src/session.rs @@ -7,26 +7,205 @@ use crate::error::MoqError; use crate::ffi::Task; use crate::origin::{MoqOriginConsumer, MoqOriginProducer}; -/// Native QUIC/WebTransport client configuration. +/// Configuration for [`MoqClient::new`], mirroring moq-tokio's client config. +/// +/// Every field has a default, so set only what you need. The browser owns the socket and the +/// trust store, so a wasm build honors only `versions`, `tls.fingerprints`, and the origins, +/// always dials once, and fails `new` with `Unsupported` when anything else is set. +#[derive(Clone, Default, uniffi::Record)] +pub struct MoqClientConfig { + /// Local UDP address to bind, e.g. `0.0.0.0:0`. Null binds an ephemeral dual-stack port. + #[uniffi(default = None)] + pub bind: Option, + /// Protocol versions to offer, most preferred first, spelled like `moq-lite-03`. + /// Empty offers every supported version. + #[uniffi(default = [])] + pub versions: Vec, + /// Certificate trust and the mTLS identity. + #[uniffi(default)] + pub tls: MoqClientTls, + /// QUIC transport tuning. + #[uniffi(default)] + pub quic: MoqQuicConfig, + /// The WebSocket fallback, raced against QUIC for networks that block UDP. + #[uniffi(default)] + pub websocket: MoqWebSocketConfig, + /// Dial once instead of redialing with backoff whenever the transport drops. + /// + /// With this set, the transport's close ends the session (surfaced via + /// [`MoqSession::closed`]). + #[uniffi(default = false)] + pub once: bool, + /// Retry pacing for the automatic reconnect. + #[uniffi(default)] + pub backoff: MoqBackoff, + /// The origin whose broadcasts are published to the remote. + /// + /// With neither `publish` nor `consume` set, each session's two sides share one fresh + /// origin, so a broadcast announced on it is also discoverable through it. Setting either + /// opts out of that and gives the other side its own fresh origin. + #[uniffi(default = None)] + pub publish: Option>, + /// The origin that receives broadcasts consumed from the remote. See `publish`. + #[uniffi(default = None)] + pub consume: Option>, +} + +/// Certificate trust and the mTLS identity for a [`MoqClientConfig`]. +#[derive(Clone, Debug, Default, PartialEq, Eq, uniffi::Record)] +pub struct MoqClientTls { + /// Skip certificate verification. Local development only. + #[uniffi(default = false)] + pub insecure: bool, + /// PEM root certificate files to trust instead of the platform roots. + #[uniffi(default = [])] + pub roots: Vec, + /// Whether to also trust the platform roots. Null trusts them only when `roots` is empty. + #[uniffi(default = None)] + pub system_roots: Option, + /// SHA-256 certificate fingerprints, hex-encoded, to pin the peer to. + /// + /// The native equivalent of WebTransport's `serverCertificateHashes`, accepting what + /// `MoqServer.cert_fingerprints` reports, so a self-signed certificate is trusted without + /// disabling verification. + #[uniffi(default = [])] + pub fingerprints: Vec, + /// PEM certificate chain to present when the relay requires mTLS. Pair with `key`. + #[uniffi(default = None)] + pub cert: Option, + /// PEM private key to present when the relay requires mTLS. Pair with `cert`. + #[uniffi(default = None)] + pub key: Option, +} + +/// QUIC transport tuning, mirroring moq-tokio's QUIC config. +#[derive(Clone, Debug, Default, PartialEq, Eq, uniffi::Record)] +pub struct MoqQuicConfig { + /// Cap on the concurrent QUIC streams the peer may open toward this endpoint. Null uses 1024. + /// + /// MoQ opens a stream per group, and for a subscriber those arrive from the peer, so an + /// endpoint subscribing to many tracks wants this raised. Ignored by the WebSocket fallback. + #[uniffi(default = None)] + pub max_streams: Option, +} + +/// The WebSocket fallback for a [`MoqClientConfig`], raced against QUIC for `http(s)` URLs. +#[derive(Clone, Debug, Default, PartialEq, Eq, uniffi::Record)] +pub struct MoqWebSocketConfig { + /// Whether the fallback runs. Null enables it; disable it for a relay that only serves + /// QUIC, so a failed QUIC dial reports its own error. + #[uniffi(default = None)] + pub enabled: Option, + /// Head start QUIC gets before the fallback joins the race, in microseconds. Null uses + /// 200ms, and 0 races both at once. + #[uniffi(default = None)] + pub delay_us: Option, +} + +/// Retry pacing for the automatic reconnect. +/// +/// The delay starts at `initial_us`, multiplies by `multiplier` after each failed attempt, +/// and caps at `max_us`. After `timeout_us` of consecutive failures the connection gives up +/// for good; the window resets whenever a session stays up past `initial_us`. Each null +/// field uses moq-tokio's default: 1s, x2, 5s, and a 10s window. +#[derive(Clone, Debug, Default, PartialEq, Eq, uniffi::Record)] +pub struct MoqBackoff { + /// Delay before the first reconnect attempt, in microseconds. + #[uniffi(default = None)] + pub initial_us: Option, + /// Multiplier applied to the delay after each failure. + #[uniffi(default = None)] + pub multiplier: Option, + /// Maximum delay between reconnect attempts, in microseconds. + #[uniffi(default = None)] + pub max_us: Option, + /// Time spent retrying before giving up, in microseconds. 0 retries forever. + #[uniffi(default = None)] + pub timeout_us: Option, +} + +/// Parse protocol version names, as `MoqClientConfig::versions` and +/// `MoqServerConfig::versions` spell them. +pub(crate) fn parse_versions(versions: &[String]) -> Result, MoqError> { + versions + .iter() + .map(|version| version.parse().map_err(MoqError::Config)) + .collect() +} + +/// Native QUIC/WebTransport client state: the configured endpoint and the wired origins. #[cfg(not(target_arch = "wasm32"))] struct Client { - config: moq_tokio::connect::Config, - /// QUIC transport tuning the dial applies, e.g. `quic_max_streams`. - quic: moq_tokio::quic::Config, + client: moq_tokio::Client, publish: Option>, consume: Option>, } #[cfg(not(target_arch = "wasm32"))] impl Client { - async fn connect(&self, url: Url) -> Result, MoqError> { - let client = self.config.clone().init(self.quic.clone()).map_err(map_connect_error)?; + fn new(config: MoqClientConfig) -> Result { + let _guard = crate::ffi::enter(); + let mut connect = moq_tokio::connect::Config::default(); + if let Some(bind) = config.bind { + let bind = bind + .parse() + .map_err(|err| MoqError::Config(format!("invalid bind address {bind}: {err}")))?; + connect.bind = Some(bind); + } + connect.version = parse_versions(&config.versions)?; + + let tls = config.tls; + connect.tls.insecure = Some(tls.insecure); + connect.tls.root = tls.roots.into_iter().map(Into::into).collect(); + connect.tls.system_roots = tls.system_roots; + connect.tls.fingerprint = tls.fingerprints; + connect.tls.cert = tls.cert.map(Into::into); + connect.tls.key = tls.key.map(Into::into); + + connect.websocket.enabled = config.websocket.enabled; + if let Some(delay) = config.websocket.delay_us { + connect.websocket.delay = std::time::Duration::from_micros(delay); + } + + connect.once = Some(config.once); + + let backoff = config.backoff; + if let Some(initial) = backoff.initial_us { + connect.backoff.initial = std::time::Duration::from_micros(initial); + } + if let Some(multiplier) = backoff.multiplier { + connect.backoff.multiplier = multiplier; + } + if let Some(max) = backoff.max_us { + connect.backoff.max = std::time::Duration::from_micros(max); + } + if let Some(timeout) = backoff.timeout_us { + connect.backoff.timeout = std::time::Duration::from_micros(timeout); + } + + let mut quic = moq_tokio::quic::Config::default(); + quic.max_streams = config.quic.max_streams; + + // Building the endpoint here, not at connect, is what surfaces an unreadable + // certificate or a half-configured mTLS identity from `new`. + let client = connect.init(quic).map_err(|err| MoqError::Config(format!("{err}")))?; + + Ok(Self { + client, + publish: config.publish, + consume: config.consume, + }) + } + + async fn connect(&self, url: Url) -> Result, MoqError> { // Materialize both origin sides so the session can publish/subscribe and the FFI can // always hand back a publish/consume origin. let (publish, subscribe) = crate::origin::resolve_pair(self.publish.as_ref(), self.consume.as_ref()); - let connection = client + let connection = self + .client + .clone() .with_publisher(&publish) .with_subscriber(subscribe.clone()) .connect(url); @@ -106,7 +285,7 @@ mod tests { #[test] fn rejects_non_ascii_instead_of_panicking() { for input in ["aéa", "é", "ééééééééééééééééééééééééééééééé"] { - assert!(matches!(decode_hex(input), Err(MoqError::Connect(_))), "{input}"); + assert!(matches!(decode_hex(input), Err(MoqError::Config(_))), "{input}"); } } @@ -128,7 +307,7 @@ mod tests { format!("{}+0", &VALID[..62]), format!(" 0{}", &VALID[2..]), ] { - assert!(matches!(decode_hex(&bad), Err(MoqError::Connect(_))), "{bad}"); + assert!(matches!(decode_hex(&bad), Err(MoqError::Config(_))), "{bad}"); } } @@ -186,109 +365,49 @@ mod tests { )); } + /// `new` is where a bad value surfaces, rather than at the first connect. #[test] - fn sets_tls_system_roots() { - let client = MoqClient::new(); - - client.set_tls_system_roots(true).unwrap(); - { - let state = client.task.lock().expect("client state should be available"); - assert_eq!(state.config.tls.system_roots, Some(true)); - } - - client.set_tls_system_roots(false).unwrap(); - let state = client.task.lock().expect("client state should be available"); - assert_eq!(state.config.tls.system_roots, Some(false)); + fn new_rejects_invalid_config() { + let bind = MoqClientConfig { + bind: Some("not-an-address".into()), + ..Default::default() + }; + assert!(matches!(MoqClient::new(bind), Err(MoqError::Config(_)))); + + let version = MoqClientConfig { + versions: vec!["moq-lite-99".into()], + ..Default::default() + }; + assert!(matches!(MoqClient::new(version), Err(MoqError::Config(_)))); + + let missing = MoqClientConfig { + tls: MoqClientTls { + roots: vec!["/nonexistent/root.pem".into()], + ..Default::default() + }, + ..Default::default() + }; + assert!(matches!(MoqClient::new(missing), Err(MoqError::Config(_)))); } #[test] - fn sets_tls_client_cert_and_key() { - let client = MoqClient::new(); - - client.set_tls_cert(Some("cert.pem".into())).unwrap(); - client.set_tls_key(Some("key.pem".into())).unwrap(); - { - let state = client.task.lock().expect("client state should be available"); - assert_eq!(state.config.tls.cert.as_deref(), Some(std::path::Path::new("cert.pem"))); - assert_eq!(state.config.tls.key.as_deref(), Some(std::path::Path::new("key.pem"))); - } - - client.set_tls_cert(None).unwrap(); - client.set_tls_key(None).unwrap(); - let state = client.task.lock().expect("client state should be available"); - assert_eq!(state.config.tls.cert, None); - assert_eq!(state.config.tls.key, None); - } - - #[test] - fn sets_the_quic_stream_cap() { - let client = MoqClient::new(); - assert_eq!(client.task.lock().unwrap().quic.max_streams, None); - - client.set_quic_max_streams(4096).unwrap(); - assert_eq!(client.task.lock().unwrap().quic.max_streams, Some(4096)); - } - - #[test] - fn sets_the_websocket_fallback() { - let client = MoqClient::new(); - let resolved = client.task.lock().unwrap().config.websocket.resolve(); - assert!(resolved.enabled); - assert_eq!(resolved.delay, std::time::Duration::from_millis(200)); - - client.set_websocket_enabled(false).unwrap(); - client.set_websocket_delay(0).unwrap(); - let resolved = client.task.lock().unwrap().config.websocket.resolve(); - assert!(!resolved.enabled); - assert_eq!(resolved.delay, std::time::Duration::ZERO); - - client.set_websocket_enabled(true).unwrap(); - client.set_websocket_delay(1_500).unwrap(); - let resolved = client.task.lock().unwrap().config.websocket.resolve(); - assert!(resolved.enabled); - assert_eq!(resolved.delay, std::time::Duration::from_micros(1_500)); - } - - #[test] - fn setters_fail_after_cancel() { - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); - client.cancel(); - assert!(matches!(client.set_tls_verify(true), Err(MoqError::Cancelled))); - assert!(matches!( - client.set_bind("127.0.0.1:0".into()), - Err(MoqError::Cancelled) - )); - assert!(matches!(client.set_publish(None), Err(MoqError::Cancelled))); - assert!(matches!(client.set_consume(None), Err(MoqError::Cancelled))); + fn new_accepts_the_defaults_and_known_versions() { + MoqClient::new(MoqClientConfig::default()).unwrap(); + MoqClient::new(MoqClientConfig { + bind: Some("127.0.0.1:0".into()), + versions: vec!["moq-lite-03".into()], + once: true, + backoff: MoqBackoff { + timeout_us: Some(0), + ..Default::default() + }, + ..Default::default() + }) + .unwrap(); } } -/// Retry pacing for the automatic reconnect (see [`MoqClient::set_backoff`]). -/// -/// The delay starts at `initial_us`, multiplies by `multiplier` after each failed -/// attempt, and caps at `max_us`. After `timeout_us` of consecutive failures the -/// connection gives up for good (0 retries forever); the window resets whenever a -/// session stays up past `initial_us`. The defaults mirror the native -/// [`moq_tokio::Backoff`]: 1s, x2, 5s, and a 10s window. -#[cfg(not(target_arch = "wasm32"))] -#[derive(Clone, Debug, uniffi::Record)] -pub struct MoqBackoff { - /// Delay before the first reconnect attempt, in microseconds. - #[uniffi(default = 1000000)] - pub initial_us: u64, - /// Multiplier applied to the delay after each failure. - #[uniffi(default = 2)] - pub multiplier: u32, - /// Maximum delay between reconnect attempts, in microseconds. - #[uniffi(default = 5000000)] - pub max_us: u64, - /// Time spent retrying before giving up, in microseconds. 0 retries forever. - #[uniffi(default = 10000000)] - pub timeout_us: u64, -} - -/// Browser WebTransport client configuration. +/// Browser WebTransport client state. /// /// The browser owns the socket and the trust store, so none of the native TLS knobs /// (roots, mTLS, bind address) have an equivalent. Certificate hashes are the one @@ -296,18 +415,62 @@ pub struct MoqBackoff { #[cfg(target_arch = "wasm32")] struct Client { fingerprints: Vec>, + /// Offered as ALPNs to WebTransport and then enforced by moq-net, so the two agree. + versions: moq_net::Versions, publish: Option>, consume: Option>, } #[cfg(target_arch = "wasm32")] impl Client { + fn new(config: MoqClientConfig) -> Result { + let MoqClientConfig { + bind, + versions, + tls, + quic, + websocket, + // A browser session never redials, so it dials once either way. + once: _, + backoff, + publish, + consume, + } = config; + + let native_tls = MoqClientTls { + fingerprints: Vec::new(), + ..tls.clone() + }; + if bind.is_some() + || native_tls != MoqClientTls::default() + || quic != MoqQuicConfig::default() + || websocket != MoqWebSocketConfig::default() + || backoff != MoqBackoff::default() + { + return Err(MoqError::Unsupported); + } + + Ok(Self { + fingerprints: tls + .fingerprints + .iter() + .map(|hex| decode_hex(hex)) + .collect::>()?, + versions: match versions.is_empty() { + true => moq_net::Versions::default(), + false => parse_versions(&versions)?.into(), + }, + publish, + consume, + }) + } + async fn connect(&self, url: Url) -> Result, MoqError> { let (publish, subscribe) = crate::origin::resolve_pair(self.publish.as_ref(), self.consume.as_ref()); let transport = match self.fingerprints.is_empty() { - true => crate::transport::connect(url).await, - false => crate::transport::connect_with_hashes(url, self.fingerprints.clone()).await, + true => crate::transport::connect(url, &self.versions).await, + false => crate::transport::connect_with_hashes(url, &self.versions, self.fingerprints.clone()).await, } .map_err(|err| MoqError::Connect(format!("{err}")))?; @@ -315,6 +478,7 @@ impl Client { // holds no session clone, so dropping the last handle still closes the // transport and ends that task. let (session, driver) = moq_net::Client::new() + .with_versions(self.versions.clone()) .with_publisher(&publish) .with_subscriber(subscribe.clone()) .connect(web_async::time::Instant::now(), transport) @@ -343,14 +507,14 @@ fn decode_hex(hex: &str) -> Result, MoqError> { // non-ASCII would let a 2-byte chunk split a character, and `from_str_radix` accepts // a leading sign, so an unchecked "+a" would decode to 10 rather than erroring. if !hex.bytes().all(|b| b.is_ascii_hexdigit()) { - return Err(MoqError::Connect(format!("fingerprint is not hex: {hex}"))); + return Err(MoqError::Config(format!("fingerprint is not hex: {hex}"))); } // WebTransport's `serverCertificateHashes` only accepts a 32-byte sha-256, so a // different length can never match a certificate. Rejecting here beats failing // opaquely inside the browser. if hex.len() != LEN { - return Err(MoqError::Connect(format!( + return Err(MoqError::Config(format!( "expected a {LEN}-character sha-256 fingerprint, got {}", hex.len() ))); @@ -360,272 +524,45 @@ fn decode_hex(hex: &str) -> Result, MoqError> { .chunks(2) .map(|pair| { let pair = std::str::from_utf8(pair).expect("checked ascii above"); - u8::from_str_radix(pair, 16).map_err(|err| MoqError::Connect(format!("{err}"))) + u8::from_str_radix(pair, 16).map_err(|err| MoqError::Config(format!("{err}"))) }) .collect() } -/// Builds a [`MoqSession`]: configure it, then [`connect`](Self::connect). -/// -/// The configuration differs by target, because the transport does. Native builds expose -/// the QUIC socket and TLS trust store; the browser owns both, so a wasm build exposes -/// only the certificate hashes WebTransport accepts. +/// Dials a [`MoqSession`] with the configuration it was built from. /// -/// Setters write the configuration [`connect`](Self::connect) will snapshot. They fail -/// with [`MoqError::Busy`] while a connect is in flight and [`MoqError::Cancelled`] -/// after [`cancel`](Self::cancel). A finished connect does not freeze the handle: later -/// setters apply to the next dial until cancel. +/// The configuration differs by what the target honors, because the transport does; see +/// [`MoqClientConfig`]. [`connect`](Self::connect) may be called again for another session +/// until [`cancel`](Self::cancel). #[derive(uniffi::Object)] pub struct MoqClient { task: Task, } -impl MoqClient { - fn configure(&self, f: impl FnOnce(&mut Client) -> R) -> Result { - Ok(f(&mut *self.task.configure()?)) - } -} - -#[cfg(target_arch = "wasm32")] -#[uniffi::export] -impl MoqClient { - /// Create a new MoQ client with default configuration. - #[uniffi::constructor] - pub fn new() -> Arc { - Arc::new(Self { - task: Task::new(Client { - fingerprints: Vec::new(), - publish: None, - consume: None, - }), - }) - } - - /// Pin the peer to a certificate with one of these SHA-256 fingerprints, encoded as hex. - /// - /// Passed through to WebTransport's `serverCertificateHashes`. An empty list restores - /// the browser's normal certificate verification. - pub fn set_tls_fingerprints(&self, fingerprints: Vec) -> Result<(), MoqError> { - let parsed = fingerprints - .iter() - .map(|hex| decode_hex(hex)) - .collect::, _>>()?; - self.configure(|state| { - state.fingerprints = parsed; - }) - } - - /// Set the origin to publish local broadcasts to the remote. - pub fn set_publish(&self, origin: Option>) -> Result<(), MoqError> { - self.configure(|state| { - state.publish = origin; - }) - } - - /// Set the origin to consume remote broadcasts from the remote. - pub fn set_consume(&self, origin: Option>) -> Result<(), MoqError> { - self.configure(|state| { - state.consume = origin; - }) - } - - /// Connect to a MoQ server and wait for the session to be established. - /// - /// Can be cancelled by calling `cancel()`. - pub async fn connect(&self, url: String) -> Result, MoqError> { - let url = Url::parse(&url)?; - self.task.run(|state| async move { state.connect(url).await }).await - } - - /// Cancel all current and future `connect()` calls. - pub fn cancel(&self) { - self.task.cancel(); - } -} - -#[cfg(not(target_arch = "wasm32"))] #[uniffi::export] impl MoqClient { - /// Create a new MoQ client with default configuration. + /// Create a client from `config`, failing on any value it cannot use. #[uniffi::constructor] - pub fn new() -> Arc { - let _guard = crate::ffi::enter(); - Arc::new(Self { - task: Task::new(Client { - config: moq_tokio::connect::Config::default(), - quic: moq_tokio::quic::Config::default(), - publish: None, - consume: None, - }), - }) - } - - /// Enable or disable TLS certificate verification. - pub fn set_tls_verify(&self, verify: bool) -> Result<(), MoqError> { - self.configure(|state| { - state.config.tls.insecure = Some(!verify); - }) - } - - /// Trust these PEM root certificate file(s) instead of the system roots. - /// - /// Pass the paths to PEM-encoded CA certificates. An empty list restores the - /// default behavior of using the platform's native root store. - pub fn set_tls_roots(&self, paths: Vec) -> Result<(), MoqError> { - self.configure(|state| { - state.config.tls.root = paths.into_iter().map(Into::into).collect(); - }) - } - - /// Configure whether to also trust the platform's native root certificates. - /// - /// By default, system roots are trusted only when no custom roots are configured. - /// Set this to `true` to trust system roots in addition to roots from - /// `set_tls_roots`, or `false` to trust only custom roots. - pub fn set_tls_system_roots(&self, system_roots: bool) -> Result<(), MoqError> { - self.configure(|state| { - state.config.tls.system_roots = Some(system_roots); - }) - } - - /// Pin the peer to a certificate with one of these SHA-256 fingerprints, encoded as hex. - /// - /// This is the native equivalent of the browser's WebTransport `serverCertificateHashes` - /// and accepts the same values a server reports (see `MoqServer.cert_fingerprints`). Use it - /// to trust a self-signed certificate without disabling verification. An empty list clears - /// any pinned fingerprints. - pub fn set_tls_fingerprints(&self, fingerprints: Vec) -> Result<(), MoqError> { - self.configure(|state| { - state.config.tls.fingerprint = fingerprints; - }) - } - - /// Present this PEM certificate chain when the relay requires mTLS. - /// - /// Only certificates are read from the file; any private keys are ignored. Must be - /// paired with `set_tls_key`, otherwise `connect` fails with an incomplete-auth error. - /// Pass `None` to clear a previously set path. - pub fn set_tls_cert(&self, path: Option) -> Result<(), MoqError> { - self.configure(|state| { - state.config.tls.cert = path.map(Into::into); - }) - } - - /// Present this PEM private key when the relay requires mTLS. - /// - /// Only the private key is read from the file; any certificates are ignored. Must be - /// paired with `set_tls_cert`, otherwise `connect` fails with an incomplete-auth error. - /// Pass `None` to clear a previously set path. - pub fn set_tls_key(&self, path: Option) -> Result<(), MoqError> { - self.configure(|state| { - state.config.tls.key = path.map(Into::into); - }) - } - - /// Set the local UDP socket bind address. Defaults to `[::]:0`. - /// - /// Returns an error if the address cannot be parsed, if a connect is in flight, - /// or after [`cancel`](Self::cancel). - pub fn set_bind(&self, addr: String) -> Result<(), MoqError> { - let parsed: std::net::SocketAddr = addr - .parse() - .map_err(|err| MoqError::Bind(format!("invalid bind address: {err}")))?; - self.configure(|state| { - state.config.bind = Some(parsed); - }) - } - - /// Cap the concurrent QUIC streams the peer may open toward this connection. - /// Defaults to 1024. - /// - /// MoQ opens a stream per group, and for a subscriber those arrive from the relay, - /// so a client subscribing to many tracks wants this raised. A publisher's own - /// streams are bounded by the peer's advertised limit, not this one. Ignored by - /// the WebSocket fallback. - pub fn set_quic_max_streams(&self, max_streams: u64) -> Result<(), MoqError> { - self.configure(|state| { - state.quic.max_streams = Some(max_streams); - }) - } - - /// Enable or disable the WebSocket fallback. Enabled by default. - /// - /// The fallback races a WebSocket dial against QUIC for `http(s)` URLs, for networks - /// that block UDP. Disable it for a relay that only serves QUIC, so a failed QUIC dial - /// reports its own error instead of the fallback's. - pub fn set_websocket_enabled(&self, enabled: bool) -> Result<(), MoqError> { - self.configure(|state| { - state.config.websocket.enabled = Some(enabled); - }) - } - - /// Set the head start, in microseconds, QUIC gets before the WebSocket fallback joins - /// the race. Defaults to 200ms. - /// - /// Zero races both at once. A server where WebSocket already won skips the head start. - pub fn set_websocket_delay(&self, delay_us: u64) -> Result<(), MoqError> { - self.configure(|state| { - state.config.websocket.delay = std::time::Duration::from_micros(delay_us); - }) - } - - /// Enable or disable automatic reconnecting. Enabled by default. - /// - /// When enabled, the session returned by [`connect`](Self::connect) redials with - /// backoff whenever the transport drops, and broadcasts consumed through it survive - /// the gap. Disable for a one-shot dial: the transport's close then ends the session - /// (surfaced via [`MoqSession::closed`]). - pub fn set_reconnect(&self, enabled: bool) -> Result<(), MoqError> { - self.configure(|state| { - state.config.once = Some(!enabled); - }) - } - - /// Configure retry pacing for the automatic reconnect (see [`MoqBackoff`]). - pub fn set_backoff(&self, backoff: MoqBackoff) -> Result<(), MoqError> { - self.configure(|state| { - let mut out = moq_tokio::Backoff::default(); - out.initial = std::time::Duration::from_micros(backoff.initial_us); - out.multiplier = backoff.multiplier; - out.max = std::time::Duration::from_micros(backoff.max_us); - out.timeout = std::time::Duration::from_micros(backoff.timeout_us); - state.config.backoff = out; - }) - } - - /// Set the origin to publish local broadcasts to the remote. - pub fn set_publish(&self, origin: Option>) -> Result<(), MoqError> { - self.configure(|state| { - state.publish = origin; - }) - } - - /// Set the origin to consume remote broadcasts from the remote. - pub fn set_consume(&self, origin: Option>) -> Result<(), MoqError> { - self.configure(|state| { - state.consume = origin; - }) + pub fn new(config: MoqClientConfig) -> Result, MoqError> { + Ok(Arc::new(Self { + task: Task::new(Client::new(config)?), + })) } /// Connect to a MoQ server and wait for the session to be established. /// - /// The returned session automatically reconnects with backoff when the transport - /// drops (unless disabled via [`set_reconnect`](Self::set_reconnect)), and broadcasts - /// consumed through it ride out the gap. Watch [`MoqSession::status`] for the - /// connect/disconnect transitions, [`MoqSession::epoch`] for the reconnect count, - /// and [`MoqSession::closed`] for the connection giving up for good. + /// A native session automatically reconnects with backoff when the transport drops + /// (unless [`MoqClientConfig::once`] is set), and broadcasts consumed through it ride out + /// the gap. Watch [`MoqSession::status`] for the connect/disconnect transitions, + /// [`MoqSession::epoch`] for the reconnect count, and [`MoqSession::closed`] for the + /// connection giving up for good. /// /// Both origin sides are always accessible via [`MoqSession::publish`] and - /// [`MoqSession::consume`], without the caller constructing a [`MoqOriginProducer`] - /// themselves. With neither [`set_publish`](Self::set_publish) nor - /// [`set_consume`](Self::set_consume) wired, the two sides share one origin, so a broadcast - /// announced on this session is also discoverable through it. Wiring either side opts out of - /// that and gives the other side its own fresh origin. + /// [`MoqSession::consume`], without the caller constructing a [`MoqOriginProducer`]. /// /// Can be cancelled by calling `cancel()`, including while the initial dial is retrying. pub async fn connect(&self, url: String) -> Result, MoqError> { let url = Url::parse(&url)?; - self.task.run(|state| async move { state.connect(url).await }).await } @@ -892,7 +829,7 @@ impl MoqSession { /// The publish-side origin: where local broadcasts get advertised /// to the remote. Either the producer the caller wired via - /// `set_publish` / `set_consume` before connect/accept, or one + /// client config or request accept arguments, or one /// auto-created if neither was set. pub fn publish(&self) -> Arc { self.publisher.clone() @@ -900,7 +837,7 @@ impl MoqSession { /// The subscribe-side origin: a read handle for receiving /// announcements pushed by the remote. Either derived from the - /// origin the caller wired via `set_consume`, or auto-created if + /// consume origin the caller supplied, or auto-created if /// neither was set. pub fn consume(&self) -> Arc { self.consumer.clone() diff --git a/rs/moq-ffi/src/test.rs b/rs/moq-ffi/src/test.rs index 2cc1d089497..3191e8ebae1 100644 --- a/rs/moq-ffi/src/test.rs +++ b/rs/moq-ffi/src/test.rs @@ -1,15 +1,18 @@ use super::origin::*; use super::producer::*; -use super::server::MoqServer; -use super::session::{MoqClient, MoqSession}; +use super::server::{MoqServer, MoqServerConfig, MoqServerTls}; +use super::session::{MoqClient, MoqClientConfig, MoqClientTls, MoqSession}; use crate::consumer::MoqBroadcastConsumer; use crate::consumer::MoqFetchGroupOptions; use crate::consumer::MoqSubscription; use crate::consumer::MoqTrackConsumer; use crate::error::{MoqError, MoqProtocolError, MoqProtocolKind}; -use crate::flate::MoqFlateConfig; -use crate::json::{MoqJsonSnapshotConfig, MoqJsonStreamConfig}; -use crate::media::{MoqAudioFormat, MoqAudioInit, MoqFrame, MoqVideoFormat, MoqVideoInit}; +use crate::flate::{MoqFlateConfig, MoqFlateSnapshotProducer, MoqFlateStreamProducer}; +use crate::json::{ + MoqJsonSnapshotConfig, MoqJsonSnapshotConsumer, MoqJsonSnapshotProducer, MoqJsonStreamConfig, + MoqJsonStreamConsumer, MoqJsonStreamProducer, +}; +use crate::media::*; use crate::session::{MoqBackoff, MoqConnectionStatus}; use std::future::Future; @@ -60,6 +63,32 @@ where handle } +/// Trust any certificate, for dialing a server with a generated one. +fn insecure_tls() -> MoqClientTls { + MoqClientTls { + insecure: true, + ..Default::default() + } +} + +/// A self-signed identity for `localhost`. +fn localhost_tls() -> MoqServerTls { + MoqServerTls { + generate: vec!["localhost".into()], + ..Default::default() + } +} + +/// Retry pacing fast enough for a test, retrying forever. +fn fast_backoff() -> MoqBackoff { + MoqBackoff { + initial_us: Some(50_000), + multiplier: Some(2), + max_us: Some(200_000), + timeout_us: Some(0), + } +} + async fn wait_for_config_error( mut op: impl FnMut() -> Result<(), MoqError>, want: impl Fn(&MoqError) -> bool, @@ -156,7 +185,6 @@ fn audio_init(format: MoqAudioFormat, data: Vec) -> MoqAudioInit { format, data, label: None, - track: None, } } @@ -166,7 +194,6 @@ fn video_init(format: MoqVideoFormat, data: Vec) -> MoqVideoInit { data, label: None, hint: None, - track: None, } } @@ -347,7 +374,12 @@ async fn announced_route_keeps_static_cost_on_reannounce() { fn publish_media_lifecycle() { let broadcast = MoqBroadcastProducer::new().unwrap(); let init = opus_head(); - let media = broadcast.publish_audio(audio_init(MoqAudioFormat::Opus, init)).unwrap(); + let media = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Named { name: None }, + audio_init(MoqAudioFormat::Opus, init), + ) + .unwrap(); media .write_frame(MoqFrame { payload: b"opus frame".to_vec(), @@ -362,16 +394,17 @@ fn publish_media_lifecycle() { async fn raw_track_activity() { let broadcast = MoqBroadcastProducer::new().unwrap(); let track = broadcast.publish_track("status".into(), None).unwrap(); - assert_eq!(track.name().unwrap(), "status"); + let demand = track.demand().unwrap(); + assert_eq!(demand.name(), "status"); let consumer = track.consume(None).unwrap(); - tokio::time::timeout(TIMEOUT, track.used()) + tokio::time::timeout(TIMEOUT, demand.used()) .await .expect("timed out waiting for raw track to become used") .unwrap(); drop(consumer); - tokio::time::timeout(TIMEOUT, track.unused()) + tokio::time::timeout(TIMEOUT, demand.unused()) .await .expect("timed out waiting for raw track to become unused") .unwrap(); @@ -391,7 +424,7 @@ async fn raw_audio_activity() { let broadcast = MoqBroadcastProducer::new().unwrap(); let consumer = broadcast.consume().unwrap(); - let catalog_consumer = consumer.subscribe_catalog().await.unwrap(); + let catalog_consumer = MoqMediaCatalogConsumer::subscribe(&consumer).await.unwrap(); let audio = broadcast .encode_audio( "microphone".into(), @@ -418,10 +451,16 @@ async fn raw_audio_activity() { .unwrap() .expect("expected a raw audio catalog"); let container = catalog.audio.get("microphone").unwrap().container.clone(); - let subscription = consumer - .subscribe_media("microphone".into(), container.clone(), None) - .await - .unwrap(); + let subscription = MoqMediaContainerConsumer::subscribe( + &consumer, + MoqMediaContainerConfig { + name: "microphone".into(), + container: container.clone(), + subscription: None, + }, + ) + .await + .unwrap(); tokio::time::timeout(TIMEOUT, audio.used()) .await .expect("timed out waiting for raw audio to become used") @@ -445,10 +484,16 @@ async fn raw_audio_activity() { .expect("timed out waiting for raw audio to become unused") .unwrap(); - let subscription = consumer - .subscribe_media("microphone".into(), container, None) - .await - .unwrap(); + let subscription = MoqMediaContainerConsumer::subscribe( + &consumer, + MoqMediaContainerConfig { + name: "microphone".into(), + container, + subscription: None, + }, + ) + .await + .unwrap(); tokio::time::timeout(TIMEOUT, audio.used()) .await .expect("timed out waiting for raw audio to become used again") @@ -660,6 +705,16 @@ async fn raw_track_update_does_not_wait_for_pending_read() { assert_eq!(frame.timestamp_us, Some(20_000)); } +/// Subscribe to `name` on `broadcast` as a raw track, for a typed consumer to take over. +async fn subscribe(broadcast: &MoqBroadcastProducer, name: &str) -> Arc { + broadcast + .consume() + .unwrap() + .subscribe_track(name.into(), None) + .await + .unwrap() +} + #[tokio::test] async fn json_snapshot_roundtrip() { let broadcast = MoqBroadcastProducer::new().unwrap(); @@ -667,13 +722,13 @@ async fn json_snapshot_roundtrip() { delta_ratio: 8, compression: true, }; - let producer = broadcast.publish_json_snapshot("meta".into(), config.clone()).unwrap(); - let consumer = broadcast - .consume() - .unwrap() - .subscribe_json_snapshot("meta".into(), config) - .await - .unwrap(); + let track = broadcast.publish_track("meta".into(), None).unwrap(); + let producer = MoqJsonSnapshotProducer::new(&broadcast, &track, config.clone()).unwrap(); + assert!( + matches!(track.demand(), Err(MoqError::Closed)), + "the producer takes over the track" + ); + let consumer = MoqJsonSnapshotConsumer::new(&*subscribe(&broadcast, "meta").await, config).unwrap(); producer.update(r#"{"a":1}"#.into()).unwrap(); let value = tokio::time::timeout(TIMEOUT, consumer.next()) @@ -711,27 +766,19 @@ async fn json_demand() { compression: true, }; let stream_config = MoqJsonStreamConfig { compression: true }; - let snapshot = broadcast - .publish_json_snapshot("status".into(), snapshot_config.clone()) - .unwrap(); - let stream = broadcast - .publish_json_stream("events".into(), stream_config.clone()) - .unwrap(); + let track = broadcast.publish_track("status".into(), None).unwrap(); + let snapshot = MoqJsonSnapshotProducer::new(&broadcast, &track, snapshot_config.clone()).unwrap(); + let track = broadcast.publish_track("events".into(), None).unwrap(); + let stream = MoqJsonStreamProducer::new(&broadcast, &track, stream_config.clone()).unwrap(); let snapshot_demand = snapshot.demand().unwrap(); let stream_demand = stream.demand().unwrap(); assert_eq!(snapshot_demand.name(), "status"); assert_eq!(stream_demand.name(), "events"); assert!(!snapshot_demand.is_used()); - let consumer = broadcast.consume().unwrap(); - let snapshot_consumer = consumer - .subscribe_json_snapshot("status".into(), snapshot_config) - .await - .unwrap(); - let stream_consumer = consumer - .subscribe_json_stream("events".into(), stream_config) - .await - .unwrap(); + let snapshot_consumer = + MoqJsonSnapshotConsumer::new(&*subscribe(&broadcast, "status").await, snapshot_config).unwrap(); + let stream_consumer = MoqJsonStreamConsumer::new(&*subscribe(&broadcast, "events").await, stream_config).unwrap(); tokio::time::timeout(TIMEOUT, snapshot_demand.used()) .await @@ -770,12 +817,15 @@ async fn json_demand() { async fn demand_handle_outlives_finish() { let broadcast = MoqBroadcastProducer::new().unwrap(); let track = broadcast.publish_track("status".into(), None).unwrap(); - let media = broadcast - .publish_audio(audio_init(MoqAudioFormat::Opus, opus_head())) - .unwrap(); + let media = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Named { name: None }, + audio_init(MoqAudioFormat::Opus, opus_head()), + ) + .unwrap(); let track_demand = track.demand().unwrap(); let media_demand = media.demand().unwrap(); - assert_eq!(media_demand.name(), media.name().unwrap()); + assert_eq!(media_demand.name(), media.demand().unwrap().name()); let consumer = track.consume(None).unwrap(); tokio::time::timeout(TIMEOUT, track_demand.used()) @@ -801,13 +851,9 @@ async fn demand_handle_outlives_finish() { async fn json_stream_roundtrip() { let broadcast = MoqBroadcastProducer::new().unwrap(); let config = MoqJsonStreamConfig { compression: true }; - let producer = broadcast.publish_json_stream("events".into(), config.clone()).unwrap(); - let consumer = broadcast - .consume() - .unwrap() - .subscribe_json_stream("events".into(), config) - .await - .unwrap(); + let track = broadcast.publish_track("events".into(), None).unwrap(); + let producer = MoqJsonStreamProducer::new(&broadcast, &track, config.clone()).unwrap(); + let consumer = MoqJsonStreamConsumer::new(&*subscribe(&broadcast, "events").await, config).unwrap(); for n in 0..3 { producer.append(format!(r#"{{"n":{n}}}"#)).unwrap(); @@ -824,6 +870,72 @@ async fn json_stream_roundtrip() { producer.finish().unwrap(); } +/// A JSON producer serves a track a subscriber requested, which the broadcast never created by name. +#[tokio::test] +async fn json_on_requested_track() { + let broadcast = MoqBroadcastProducer::new().unwrap(); + let dynamic = broadcast.dynamic().unwrap(); + let config = MoqJsonStreamConfig { compression: false }; + + // The subscribe stays pending until the request is accepted, so run it concurrently. + let subscribe = { + let consumer = broadcast.consume().unwrap(); + tokio::spawn(async move { consumer.subscribe_track("events".into(), None).await }) + }; + let request = tokio::time::timeout(TIMEOUT, dynamic.requested_track()) + .await + .expect("timed out waiting for requested track") + .unwrap(); + let producer = MoqJsonStreamProducer::new(&broadcast, &request.accept(None).unwrap(), config.clone()).unwrap(); + let track = tokio::time::timeout(TIMEOUT, subscribe) + .await + .expect("timed out waiting for the subscribe") + .unwrap() + .unwrap(); + let consumer = MoqJsonStreamConsumer::new(&track, config).unwrap(); + + producer.append(r#"{"n":1}"#.into()).unwrap(); + let value = tokio::time::timeout(TIMEOUT, consumer.next()) + .await + .expect("timed out waiting for json stream record") + .unwrap() + .expect("expected a record"); + assert_eq!(value, r#"{"n":1}"#); +} + +/// A JSON consumer refuses a track that has already read a group, and closes one it takes over. +#[tokio::test] +async fn json_consumer_takes_an_unread_track() { + let broadcast = MoqBroadcastProducer::new().unwrap(); + let track = broadcast.publish_track("events".into(), None).unwrap(); + let config = MoqJsonStreamConfig { compression: false }; + + let read = broadcast + .consume() + .unwrap() + .subscribe_track("events".into(), None) + .await + .unwrap(); + track.append_group().unwrap(); + tokio::time::timeout(TIMEOUT, read.recv_group()) + .await + .expect("timed out waiting for a group") + .unwrap() + .expect("expected a group"); + assert!(matches!( + MoqJsonStreamConsumer::new(&read, config.clone()), + Err(MoqError::AlreadyCommitted) + )); + + let unread = subscribe(&broadcast, "events").await; + MoqJsonStreamConsumer::new(&unread, config.clone()).unwrap(); + assert!(matches!(unread.recv_group().await, Err(MoqError::Cancelled))); + assert!(matches!( + MoqJsonStreamConsumer::new(&unread, config), + Err(MoqError::Closed) + )); +} + #[tokio::test] async fn dynamic_track_request() { let broadcast = MoqBroadcastProducer::new().unwrap(); @@ -1095,15 +1207,17 @@ async fn fetches_cached_media_group_and_decodes_container() { .unwrap(); media.finish().unwrap(); - let fetched = consumer - .fetch_media_group( - "media".into(), - 0, - crate::media::MoqContainer::Legacy, - Some(MoqFetchGroupOptions { priority: 7 }), - ) - .await - .unwrap(); + let fetched = MoqMediaContainerGroupConsumer::fetch( + &consumer, + MoqMediaContainerGroupConfig { + name: "media".into(), + sequence: 0, + container: crate::media::MoqContainer::Legacy, + options: Some(MoqFetchGroupOptions { priority: 7 }), + }, + ) + .await + .unwrap(); assert_eq!(fetched.sequence(), 0); let frame = fetched.next().await.unwrap().expect("expected keyframe"); @@ -1123,14 +1237,16 @@ async fn fetch_media_group_rejects_invalid_container_before_fetching() { let _track = broadcast.create_track("media", None).unwrap(); let consumer = MoqBroadcastConsumer::new(broadcast.consume()); - let result = consumer - .fetch_media_group( - "media".into(), - 0, - crate::media::MoqContainer::Cmaf { init: Vec::new() }, - None, - ) - .await; + let result = MoqMediaContainerGroupConsumer::fetch( + &consumer, + MoqMediaContainerGroupConfig { + name: "media".into(), + sequence: 0, + container: crate::media::MoqContainer::Cmaf { init: Vec::new() }, + options: None, + }, + ) + .await; assert!(matches!(result, Err(MoqError::Codec(_)))); } @@ -1170,11 +1286,14 @@ async fn fetch_media_group_decodes_multiple_cmaf_samples() { let fetched = tokio::time::timeout( TIMEOUT, - consumer.fetch_media_group( - "video".into(), - 0, - crate::media::MoqContainer::Cmaf { init: init.to_vec() }, - None, + MoqMediaContainerGroupConsumer::fetch( + &consumer, + MoqMediaContainerGroupConfig { + name: "video".into(), + sequence: 0, + container: crate::media::MoqContainer::Cmaf { init: init.to_vec() }, + options: None, + }, ), ) .await @@ -1337,19 +1456,42 @@ async fn requested_track_dynamic_survives_accept() { async fn video_publish_named_track() { let broadcast = MoqBroadcastProducer::new().unwrap(); let consumer = broadcast.consume().unwrap(); - let catalog_consumer = consumer.subscribe_catalog().await.unwrap(); + let catalog_consumer = MoqMediaCatalogConsumer::subscribe(&consumer).await.unwrap(); - let named = |track: &str| MoqVideoInit { - track: Some(track.into()), - ..video_init(MoqVideoFormat::Avc3, h264_init()) - }; - let hd = broadcast.publish_video(named("hd")).unwrap(); - assert_eq!(hd.name().unwrap(), "hd"); - let sd = broadcast.publish_video_stream(named("sd")).unwrap(); + let init = || video_init(MoqVideoFormat::Avc3, h264_init()); + let hd = MoqMediaTrackProducer::video( + &broadcast, + MoqMediaTarget::Named { + name: Some("hd".into()), + }, + init(), + ) + .unwrap(); + assert_eq!(hd.demand().unwrap().name(), "hd"); + let sd = MoqMediaTrackStreamProducer::video( + &broadcast, + MoqMediaTarget::Named { + name: Some("sd".into()), + }, + init(), + ) + .unwrap(); + assert_eq!(sd.demand().unwrap().name(), "sd"); + sd.finish().unwrap(); + assert!(matches!(sd.demand(), Err(MoqError::Closed))); drop(sd); // A name is the caller's contract, so a duplicate fails rather than being made unique. - assert!(matches!(broadcast.publish_video(named("hd")), Err(MoqError::Codec(_)))); + assert!(matches!( + MoqMediaTrackProducer::video( + &broadcast, + MoqMediaTarget::Named { + name: Some("hd".into()) + }, + init() + ), + Err(MoqError::Codec(_)) + )); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await @@ -1360,14 +1502,20 @@ async fn video_publish_named_track() { } #[tokio::test] -async fn requested_track_refuses_a_name() { +async fn requested_track_keeps_its_name() { let broadcast = MoqBroadcastProducer::new().unwrap(); let dynamic = broadcast.dynamic().unwrap(); let consumer = broadcast.consume().unwrap(); let subscribe = tokio::spawn(async move { - consumer - .subscribe_media("requested".into(), crate::media::MoqContainer::Legacy, None) - .await + MoqMediaContainerConsumer::subscribe( + &consumer, + MoqMediaContainerConfig { + name: "requested".into(), + container: crate::media::MoqContainer::Legacy, + subscription: None, + }, + ) + .await }); let request = tokio::time::timeout(TIMEOUT, dynamic.requested_track()) @@ -1375,20 +1523,15 @@ async fn requested_track_refuses_a_name() { .expect("timed out waiting for requested track") .unwrap(); - let named = MoqVideoInit { - track: Some("other".into()), - ..video_init(MoqVideoFormat::Avc3, h264_init()) - }; - assert!(matches!( - broadcast.publish_video_on_track(&request, named), - Err(MoqError::Codec(_)) - )); - - // The refusal leaves the request unaccepted, so it still publishes under its own name. - let media = broadcast - .publish_video_on_track(&request, video_init(MoqVideoFormat::Avc3, h264_init())) - .unwrap(); - assert_eq!(media.name().unwrap(), "requested"); + let media = MoqMediaTrackProducer::video( + &broadcast, + MoqMediaTarget::Requested { + request: request.clone(), + }, + video_init(MoqVideoFormat::Avc3, h264_init()), + ) + .unwrap(); + assert_eq!(media.demand().unwrap().name(), "requested"); subscribe.abort(); } @@ -1397,16 +1540,22 @@ async fn dynamic_track_request_can_publish_media() { let broadcast = MoqBroadcastProducer::new().unwrap(); let dynamic = broadcast.dynamic().unwrap(); let consumer = broadcast.consume().unwrap(); - let catalog_consumer = consumer.subscribe_catalog().await.unwrap(); + let catalog_consumer = MoqMediaCatalogConsumer::subscribe(&consumer).await.unwrap(); - // publish_media_on_track accepts the request (at the media timescale), which is what - // unblocks subscribe_media, so the subscribe runs on a concurrent task until then. + // Importing onto the request accepts it (at the media timescale), which is what + // unblocks the media subscribe, so it runs on a concurrent task until then. let subscribe = { let consumer = consumer.clone(); tokio::spawn(async move { - consumer - .subscribe_media("requested-audio".into(), crate::media::MoqContainer::Legacy, None) - .await + MoqMediaContainerConsumer::subscribe( + &consumer, + MoqMediaContainerConfig { + name: "requested-audio".into(), + container: crate::media::MoqContainer::Legacy, + subscription: None, + }, + ) + .await }) }; @@ -1416,10 +1565,13 @@ async fn dynamic_track_request_can_publish_media() { .unwrap(); assert_eq!(track.name().unwrap(), "requested-audio"); - let media = broadcast - .publish_audio_on_track(&track, audio_init(MoqAudioFormat::Opus, opus_head())) - .unwrap(); - assert_eq!(media.name().unwrap(), "requested-audio"); + let media = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Requested { request: track.clone() }, + audio_init(MoqAudioFormat::Opus, opus_head()), + ) + .unwrap(); + assert_eq!(media.demand().unwrap().name(), "requested-audio"); assert!(matches!(track.name(), Err(MoqError::Closed))); let media_consumer = tokio::time::timeout(TIMEOUT, subscribe) @@ -1468,12 +1620,17 @@ async fn dynamic_track_request_can_publish_media() { async fn media_track_activity_and_name() { let broadcast = MoqBroadcastProducer::new().unwrap(); let init = opus_head(); - let media = broadcast.publish_audio(audio_init(MoqAudioFormat::Opus, init)).unwrap(); - let track_name = media.name().unwrap(); + let media = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Named { name: None }, + audio_init(MoqAudioFormat::Opus, init), + ) + .unwrap(); + let track_name = media.demand().unwrap().name(); assert_eq!(track_name, "0.opus"); let broadcast_consumer = broadcast.consume().unwrap(); - let catalog_consumer = broadcast_consumer.subscribe_catalog().await.unwrap(); + let catalog_consumer = MoqMediaCatalogConsumer::subscribe(&broadcast_consumer).await.unwrap(); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await .expect("timed out waiting for catalog") @@ -1482,13 +1639,13 @@ async fn media_track_activity_and_name() { assert!(catalog.audio.contains_key(&track_name)); let track_consumer = broadcast_consumer.subscribe_track(track_name, None).await.unwrap(); - tokio::time::timeout(TIMEOUT, media.used()) + tokio::time::timeout(TIMEOUT, media.demand().unwrap().used()) .await .expect("timed out waiting for media track to become used") .unwrap(); drop(track_consumer); - tokio::time::timeout(TIMEOUT, media.unused()) + tokio::time::timeout(TIMEOUT, media.demand().unwrap().unused()) .await .expect("timed out waiting for media track to become unused") .unwrap(); @@ -1503,12 +1660,15 @@ async fn publish_media_aac_populates_description() { channel_count: 2, }; let init = config.encode().unwrap(); - let _media = broadcast - .publish_audio(audio_init(MoqAudioFormat::Aac, init.to_vec())) - .unwrap(); + let _media = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Named { name: None }, + audio_init(MoqAudioFormat::Aac, init.to_vec()), + ) + .unwrap(); let consumer = broadcast.consume().unwrap(); - let catalog_consumer = consumer.subscribe_catalog().await.unwrap(); + let catalog_consumer = MoqMediaCatalogConsumer::subscribe(&consumer).await.unwrap(); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await .expect("timed out waiting for catalog") @@ -1529,10 +1689,13 @@ async fn publish_media_aac_populates_description() { #[test] fn audio_rejects_bad_init_bytes() { let broadcast = MoqBroadcastProducer::new().unwrap(); - let err = broadcast - .publish_audio(audio_init(MoqAudioFormat::Opus, vec![])) - .err() - .expect("an OpusHead-less opus track should fail"); + let err = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Named { name: None }, + audio_init(MoqAudioFormat::Opus, vec![]), + ) + .err() + .expect("an OpusHead-less opus track should fail"); assert!( matches!(err, crate::error::MoqError::Codec(_)), "expected Codec error, got {err}" @@ -1886,7 +2049,12 @@ async fn local_publish_consume_audio() { let origin = MoqOriginProducer::new(MoqOriginConfig::default()); let broadcast = create_announced(&origin, "live"); let init = opus_head(); - let media = broadcast.publish_audio(audio_init(MoqAudioFormat::Opus, init)).unwrap(); + let media = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Named { name: None }, + audio_init(MoqAudioFormat::Opus, init), + ) + .unwrap(); let consumer = origin.consume(); let announced = consumer.announced(MoqAnnounceConfig::default()).unwrap(); @@ -1896,7 +2064,7 @@ async fn local_publish_consume_audio() { assert_eq!(announcement.prefix, "live"); let broadcast_consumer = await_announced(&consumer, &announcement.prefix).await; - let catalog_consumer = broadcast_consumer.subscribe_catalog().await.unwrap(); + let catalog_consumer = MoqMediaCatalogConsumer::subscribe(&broadcast_consumer).await.unwrap(); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await @@ -1911,10 +2079,16 @@ async fn local_publish_consume_audio() { assert_eq!(audio.channel_count, 2); assert!(catalog.video.is_empty()); - let media_consumer = broadcast_consumer - .subscribe_media(track_name.clone(), audio.container.clone(), None) - .await - .unwrap(); + let media_consumer = MoqMediaContainerConsumer::subscribe( + &broadcast_consumer, + MoqMediaContainerConfig { + name: track_name.clone(), + container: audio.container.clone(), + subscription: None, + }, + ) + .await + .unwrap(); let payload = b"opus audio payload data".to_vec(); media @@ -1941,7 +2115,12 @@ async fn video_publish_consume() { let origin = MoqOriginProducer::new(MoqOriginConfig::default()); let broadcast = create_announced(&origin, "video-test"); let init = h264_init(); - let media = broadcast.publish_video(video_init(MoqVideoFormat::Avc3, init)).unwrap(); + let media = MoqMediaTrackProducer::video( + &broadcast, + MoqMediaTarget::Named { name: None }, + video_init(MoqVideoFormat::Avc3, init), + ) + .unwrap(); let consumer = origin.consume(); let announced = consumer.announced(MoqAnnounceConfig::default()).unwrap(); @@ -1949,7 +2128,7 @@ async fn video_publish_consume() { let announcement = next_announced(&announced).await; let broadcast_consumer = await_announced(&consumer, &announcement.prefix).await; - let catalog_consumer = broadcast_consumer.subscribe_catalog().await.unwrap(); + let catalog_consumer = MoqMediaCatalogConsumer::subscribe(&broadcast_consumer).await.unwrap(); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await @@ -1969,10 +2148,16 @@ async fn video_publish_consume() { assert_eq!(coded.height, 720); assert!(catalog.audio.is_empty()); - let media_consumer = broadcast_consumer - .subscribe_media(track_name.clone(), video.container.clone(), None) - .await - .unwrap(); + let media_consumer = MoqMediaContainerConsumer::subscribe( + &broadcast_consumer, + MoqMediaContainerConfig { + name: track_name.clone(), + container: video.container.clone(), + subscription: None, + }, + ) + .await + .unwrap(); let keyframe = vec![0x00, 0x00, 0x00, 0x01, 0x65, 0xAA, 0xBB, 0xCC]; media @@ -2056,7 +2241,7 @@ async fn video_raw_publish_consume() { let announcement = next_announced(&announced).await; let broadcast_consumer = await_announced(&consumer, &announcement.prefix).await; - let catalog_consumer = broadcast_consumer.subscribe_catalog().await.unwrap(); + let catalog_consumer = MoqMediaCatalogConsumer::subscribe(&broadcast_consumer).await.unwrap(); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await .expect("timed out") @@ -2076,10 +2261,16 @@ async fn video_raw_publish_consume() { assert_eq!(coded.height, 240); assert!(catalog.audio.is_empty()); - let media_consumer = broadcast_consumer - .subscribe_media(track_name.clone(), rendition.container.clone(), None) - .await - .unwrap(); + let media_consumer = MoqMediaContainerConsumer::subscribe( + &broadcast_consumer, + MoqMediaContainerConfig { + name: track_name.clone(), + container: rendition.container.clone(), + subscription: None, + }, + ) + .await + .unwrap(); // Keep feeding the encoder so the subscriber has frames to read after it // joins, whatever the group boundary it landed on. @@ -2150,7 +2341,7 @@ async fn video_decode_frame_ownership() { let consumer = origin.consume(); let broadcast_consumer = await_announced(&consumer, "video-decode-frame").await; - let catalog_consumer = broadcast_consumer.subscribe_catalog().await.unwrap(); + let catalog_consumer = MoqMediaCatalogConsumer::subscribe(&broadcast_consumer).await.unwrap(); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await .expect("timed out") @@ -2303,11 +2494,10 @@ async fn video_raw_publish_from_many_threads() { let consumer = origin.consume(); let announced = consumer.announced(MoqAnnounceConfig::default()).unwrap(); let announcement = next_announced(&announced).await; - let catalog_consumer = await_announced(&consumer, &announcement.prefix) - .await - .subscribe_catalog() - .await - .unwrap(); + let catalog_consumer = + MoqMediaCatalogConsumer::subscribe(await_announced(&consumer, &announcement.prefix).await.as_ref()) + .await + .unwrap(); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await .expect("timed out") @@ -2340,8 +2530,8 @@ async fn video_raw_publish_rejects_bad_frames() { framerate: 30, }; let output = || MoqVideoEncoderOutput { - codec: MoqVideoCodec::H264, track: None, + codec: MoqVideoCodec::H264, bitrate: None, gop: None, kind: MoqVideoEncoderKind::Software, @@ -2391,14 +2581,19 @@ async fn multiple_frames_ordering() { let origin = MoqOriginProducer::new(MoqOriginConfig::default()); let broadcast = create_announced(&origin, "ordering-test"); let init = opus_head(); - let media = broadcast.publish_audio(audio_init(MoqAudioFormat::Opus, init)).unwrap(); + let media = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Named { name: None }, + audio_init(MoqAudioFormat::Opus, init), + ) + .unwrap(); let consumer = origin.consume(); let announced = consumer.announced(MoqAnnounceConfig::default()).unwrap(); let announcement = next_announced(&announced).await; let broadcast_consumer = await_announced(&consumer, &announcement.prefix).await; - let catalog_consumer = broadcast_consumer.subscribe_catalog().await.unwrap(); + let catalog_consumer = MoqMediaCatalogConsumer::subscribe(&broadcast_consumer).await.unwrap(); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await .unwrap() @@ -2406,10 +2601,16 @@ async fn multiple_frames_ordering() { .unwrap(); let (track_name, audio) = catalog.audio.iter().next().unwrap(); - let media_consumer = broadcast_consumer - .subscribe_media(track_name.clone(), audio.container.clone(), None) - .await - .unwrap(); + let media_consumer = MoqMediaContainerConsumer::subscribe( + &broadcast_consumer, + MoqMediaContainerConfig { + name: track_name.clone(), + container: audio.container.clone(), + subscription: None, + }, + ) + .await + .unwrap(); let timestamps: [u64; 5] = [0, 20_000, 40_000, 60_000, 80_000]; for (i, &ts) in timestamps.iter().enumerate() { @@ -2444,14 +2645,14 @@ async fn catalog_update_on_new_track() { let init = opus_head(); let mut first = audio_init(MoqAudioFormat::Opus, init.clone()); first.label = Some("English".to_string()); - let _media1 = broadcast.publish_audio(first).unwrap(); + let _media1 = MoqMediaTrackProducer::audio(&broadcast, MoqMediaTarget::Named { name: None }, first).unwrap(); let consumer = origin.consume(); let announced = consumer.announced(MoqAnnounceConfig::default()).unwrap(); let announcement = next_announced(&announced).await; let broadcast_consumer = await_announced(&consumer, &announcement.prefix).await; - let catalog_consumer = broadcast_consumer.subscribe_catalog().await.unwrap(); + let catalog_consumer = MoqMediaCatalogConsumer::subscribe(&broadcast_consumer).await.unwrap(); let catalog1 = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await @@ -2461,7 +2662,12 @@ async fn catalog_update_on_new_track() { assert_eq!(catalog1.audio.len(), 1); assert_eq!(catalog1.audio["0.opus"].label.as_deref(), Some("English")); - let _media2 = broadcast.publish_audio(audio_init(MoqAudioFormat::Opus, init)).unwrap(); + let _media2 = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Named { name: None }, + audio_init(MoqAudioFormat::Opus, init), + ) + .unwrap(); let catalog2 = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await @@ -2479,13 +2685,20 @@ async fn catalog_update_on_new_track() { fn close_twice_is_a_noop() { let broadcast = MoqBroadcastProducer::new().unwrap(); let init = opus_head(); - let _media = broadcast - .publish_audio(audio_init(MoqAudioFormat::Opus, init.clone())) - .unwrap(); + let _media = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Named { name: None }, + audio_init(MoqAudioFormat::Opus, init.clone()), + ) + .unwrap(); broadcast.close().unwrap(); broadcast.close().unwrap(); - let Err(err) = broadcast.publish_audio(audio_init(MoqAudioFormat::Opus, init)) else { + let Err(err) = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Named { name: None }, + audio_init(MoqAudioFormat::Opus, init), + ) else { panic!("publishing after close succeeded"); }; assert!( @@ -2505,9 +2718,7 @@ async fn announced_broadcast() { let announcement = next_announced(&announced).await; assert_eq!(announcement.prefix, "test/broadcast"); - let _catalog = await_announced(&consumer, &announcement.prefix) - .await - .subscribe_catalog() + let _catalog = MoqMediaCatalogConsumer::subscribe(await_announced(&consumer, &announcement.prefix).await.as_ref()) .await .unwrap(); // Finish so consumers observe a deliberate end (the canonical end for a @@ -3244,8 +3455,9 @@ async fn raw_read_frame_terminal_cancel_releases_demand() { let broadcast = MoqBroadcastProducer::new().unwrap(); let track = broadcast.publish_track("status".into(), None).unwrap(); let consumer = track.consume(None).unwrap(); + let demand = track.demand().unwrap(); - tokio::time::timeout(TIMEOUT, track.used()) + tokio::time::timeout(TIMEOUT, demand.used()) .await .expect("timed out waiting for the subscriber") .unwrap(); @@ -3269,7 +3481,7 @@ async fn raw_read_frame_terminal_cancel_releases_demand() { Ok(None) => panic!("cancelled read returned EOF"), } - tokio::time::timeout(TIMEOUT, track.unused()) + tokio::time::timeout(TIMEOUT, demand.unused()) .await .expect("timed out waiting for demand to drop") .unwrap(); @@ -3335,7 +3547,12 @@ fn without_runtime() { let broadcast = create_announced(&origin, "test"); let init = opus_head(); - let media = broadcast.publish_audio(audio_init(MoqAudioFormat::Opus, init)).unwrap(); + let media = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Named { name: None }, + audio_init(MoqAudioFormat::Opus, init), + ) + .unwrap(); media .write_frame(MoqFrame { payload: b"hello".to_vec(), @@ -3351,9 +3568,12 @@ fn without_runtime() { assert_eq!(announcement.prefix, "test"); let _bc = pollster::block_on(consumer.request_broadcast("test".into())).unwrap(); - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); - client.set_consume(Some(origin)).unwrap(); + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + consume: Some(origin), + ..Default::default() + }) + .unwrap(); announced.cancel(); client.cancel(); @@ -3372,10 +3592,13 @@ fn without_runtime() { async fn server_client_roundtrip() { // Server side: bind, set a publish origin, accept incoming sessions. let server_origin = MoqOriginProducer::new(MoqOriginConfig::default()); - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); - server.set_publish(Some(server_origin.clone())).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + publish: Some(server_origin.clone()), + ..Default::default() + }) + .unwrap(); let addr = tokio::time::timeout(TIMEOUT, server.listen()) .await @@ -3392,15 +3615,18 @@ async fn server_client_roundtrip() { .expect("accept returned None"); assert_eq!(request.path(), "/test"); assert_eq!(request.query().as_deref(), Some("foo=bar")); - request.accept().await.expect("handshake failed") + request.accept(None, None).await.expect("handshake failed") }); // Client side: connect, subscribe via a consume origin. let client_origin = MoqOriginProducer::new(MoqOriginConfig::default()); - let client = MoqClient::new(); - 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 client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + bind: Some("127.0.0.1:0".into()), + consume: Some(client_origin.clone()), + ..Default::default() + }) + .unwrap(); let cs = tokio::time::timeout(TIMEOUT, client.connect(url)) .await .expect("connect timed out") @@ -3414,7 +3640,12 @@ async fn server_client_roundtrip() { // Publish a broadcast on the server side. let broadcast = create_announced(&server_origin, "hello"); let init = opus_head(); - let media = broadcast.publish_audio(audio_init(MoqAudioFormat::Opus, init)).unwrap(); + let media = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Named { name: None }, + audio_init(MoqAudioFormat::Opus, init), + ) + .unwrap(); // Receive the announcement on the client side via the consume origin. let consumer = client_origin.consume(); @@ -3424,17 +3655,23 @@ async fn server_client_roundtrip() { // Subscribe to the audio track and verify a frame round-trips. let bc = await_announced(&consumer, "hello").await; - let catalog_consumer = bc.subscribe_catalog().await.unwrap(); + let catalog_consumer = MoqMediaCatalogConsumer::subscribe(&bc).await.unwrap(); let catalog = tokio::time::timeout(TIMEOUT, catalog_consumer.next()) .await .expect("timed out waiting for catalog") .unwrap() .expect("expected a catalog"); let (track_name, audio) = catalog.audio.iter().next().unwrap(); - let media_consumer = bc - .subscribe_media(track_name.clone(), audio.container.clone(), None) - .await - .unwrap(); + let media_consumer = MoqMediaContainerConsumer::subscribe( + &bc, + MoqMediaContainerConfig { + name: track_name.clone(), + container: audio.container.clone(), + subscription: None, + }, + ) + .await + .unwrap(); let payload = b"hello over the wire".to_vec(); media @@ -3463,14 +3700,17 @@ async fn server_client_roundtrip() { #[tokio::test] async fn server_client_roundtrip_auto_origin() { - // Same shape as `server_client_roundtrip` but the client never calls - // `set_publish` / `set_consume`: the auto-created origin sides on + // Same shape as `server_client_roundtrip` but the client config omits + // origins: the auto-created origin sides on // `MoqClientSession` are what drive publishing and subscribing. let server_origin = MoqOriginProducer::new(MoqOriginConfig::default()); - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); - server.set_publish(Some(server_origin.clone())).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + publish: Some(server_origin.clone()), + ..Default::default() + }) + .unwrap(); let addr = tokio::time::timeout(TIMEOUT, server.listen()) .await @@ -3485,13 +3725,16 @@ async fn server_client_roundtrip_auto_origin() { .await .expect("accept errored") .expect("accept returned None"); - request.accept().await.expect("handshake failed") + request.accept(None, None).await.expect("handshake failed") }); - // No set_publish / set_consume, so this uses the auto-origin path. - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); - client.set_bind("127.0.0.1:0".into()).unwrap(); + // No configured origins, so this uses the auto-origin path. + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + bind: Some("127.0.0.1:0".into()), + ..Default::default() + }) + .unwrap(); let cs = tokio::time::timeout(TIMEOUT, client.connect(url)) .await .expect("connect timed out") @@ -3508,7 +3751,12 @@ async fn server_client_roundtrip_auto_origin() { // Server publishes; client receives via the auto consumer. let broadcast = create_announced(&server_origin, "hello"); let init = opus_head(); - let media = broadcast.publish_audio(audio_init(MoqAudioFormat::Opus, init)).unwrap(); + let media = MoqMediaTrackProducer::audio( + &broadcast, + MoqMediaTarget::Named { name: None }, + audio_init(MoqAudioFormat::Opus, init), + ) + .unwrap(); let announced = consumer.announced(MoqAnnounceConfig::default()).unwrap(); let announcement = next_announced(&announced).await; @@ -3533,26 +3781,34 @@ async fn server_client_roundtrip_auto_origin() { } #[tokio::test] -async fn server_set_bind_validates() { - let server = MoqServer::new(); - assert!(server.set_bind("127.0.0.1:0".into()).is_ok()); - assert!(server.set_bind("[::]:443".into()).is_ok()); - assert!(server.set_bind("localhost:4443".into()).is_ok()); - assert!(matches!( - server.set_bind("localhost:443:8443".into()), - Err(crate::error::MoqError::Bind(_)) - )); - assert!(matches!( - server.set_bind("not-an-address".into()), - Err(crate::error::MoqError::Bind(_)) - )); +async fn server_new_validates_the_config() { + let server = |bind: &str| { + MoqServer::new(MoqServerConfig { + bind: Some(bind.into()), + ..Default::default() + }) + }; + assert!(server("127.0.0.1:0").is_ok()); + assert!(server("[::]:443").is_ok()); + assert!(server("localhost:4443").is_ok()); + assert!(matches!(server("localhost:443:8443"), Err(MoqError::Config(_)))); + assert!(matches!(server("not-an-address"), Err(MoqError::Config(_)))); + + let version = MoqServer::new(MoqServerConfig { + versions: vec!["moq-lite-99".into()], + ..Default::default() + }); + assert!(matches!(version, Err(MoqError::Config(_)))); } #[tokio::test] async fn server_cert_fingerprints_available_after_listen() { - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); // Not available before listen(). assert!(matches!( @@ -3574,9 +3830,12 @@ async fn server_cert_fingerprints_available_after_listen() { #[tokio::test] async fn server_cert_fingerprints_rejected_after_cancel() { - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); tokio::time::timeout(TIMEOUT, server.listen()) .await @@ -3598,15 +3857,22 @@ async fn server_cert_fingerprints_rejected_after_cancel() { /// cancel has to unwind an in-flight run rather than an idle state. #[tokio::test] async fn server_cancel_releases_the_bound_port() { - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); let addr = server.listen().await.expect("listen failed"); // Park an accept on the server lock, the state a live server is closed in. let accepting = server.clone(); let accept = tokio::spawn(async move { accepting.accept().await }); - wait_for_config_error(|| server.set_publish(None), |err| matches!(err, MoqError::Busy)).await; + wait_for_config_error( + || server.cert_fingerprints().map(drop), + |err| matches!(err, MoqError::Busy), + ) + .await; server.cancel(); @@ -3616,9 +3882,12 @@ async fn server_cancel_releases_the_bound_port() { std::net::UdpSocket::bind(&addr).expect("cancel should release the socket before it returns"); // No retry: the socket is already closed, so this must succeed on the first try. - let rebound = MoqServer::new(); - rebound.set_bind(addr.clone()).unwrap(); - rebound.set_tls_generate(vec!["localhost".into()]).unwrap(); + let rebound = MoqServer::new(MoqServerConfig { + bind: Some(addr.clone()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); rebound.listen().await.expect("the port should rebind immediately"); let accept = tokio::time::timeout(TIMEOUT, accept) @@ -3634,9 +3903,12 @@ async fn server_cancel_releases_the_bound_port() { async fn request_double_respond_returns_already_responded() { use crate::error::MoqError; - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); let addr = server.listen().await.expect("listen failed"); let url = format!("https://{addr}"); @@ -3649,8 +3921,8 @@ async fn request_double_respond_returns_already_responded() { .expect("accept returned None"); // Accept once, then try a second response. It must error. - let session = request.accept().await.expect("first ok succeeds"); - let second_ok = request.accept().await; + let session = request.accept(None, None).await.expect("first ok succeeds"); + let second_ok = request.accept(None, None).await; assert!( matches!(second_ok, Err(MoqError::AlreadyResponded)), "second ok() must fail" @@ -3663,9 +3935,12 @@ async fn request_double_respond_returns_already_responded() { session }); - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); - client.set_bind("127.0.0.1:0".into()).unwrap(); + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + bind: Some("127.0.0.1:0".into()), + ..Default::default() + }) + .unwrap(); let _session = tokio::time::timeout(TIMEOUT, client.connect(url)) .await .expect("connect timed out") @@ -3683,9 +3958,12 @@ async fn request_double_respond_returns_already_responded() { #[tokio::test] async fn request_per_session_publish_override() { // The server's publish origin is empty; a per-request override is used instead. - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); let addr = server.listen().await.expect("listen failed"); let url = format!("https://{addr}"); @@ -3701,15 +3979,20 @@ async fn request_per_session_publish_override() { .expect("accept errored") .expect("accept returned None"); // Override publish on a per-request basis. - request.set_publish(Some(override_for_task)).unwrap(); - request.accept().await.expect("ok succeeds") + request + .accept(Some(override_for_task), None) + .await + .expect("ok succeeds") }); let client_origin = MoqOriginProducer::new(MoqOriginConfig::default()); - let client = MoqClient::new(); - 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 client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + bind: Some("127.0.0.1:0".into()), + consume: Some(client_origin.clone()), + ..Default::default() + }) + .unwrap(); let cs = tokio::time::timeout(TIMEOUT, client.connect(url)) .await .expect("connect timed out") @@ -3741,10 +4024,13 @@ async fn request_per_session_publish_override() { #[tokio::test] async fn client_reconnects_and_resumes_announcements() { let server_origin = MoqOriginProducer::new(MoqOriginConfig::default()); - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); - server.set_publish(Some(server_origin.clone())).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + publish: Some(server_origin.clone()), + ..Default::default() + }) + .unwrap(); let addr = tokio::time::timeout(TIMEOUT, server.listen()) .await @@ -3765,7 +4051,7 @@ async fn client_reconnects_and_resumes_announcements() { .await .expect("first accept errored") .expect("first accept returned None"); - let first = first.accept().await.expect("first handshake failed"); + let first = first.accept(None, None).await.expect("first handshake failed"); if first_tx.send(first).is_err() { panic!("test body gone"); } @@ -3776,23 +4062,19 @@ async fn client_reconnects_and_resumes_announcements() { .await .expect("second accept errored") .expect("second accept returned None"); - second.accept().await.expect("second handshake failed") + second.accept(None, None).await.expect("second handshake failed") }); let client_origin = MoqOriginProducer::new(MoqOriginConfig::default()); - let client = MoqClient::new(); - 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. - client - .set_backoff(MoqBackoff { - initial_us: 50_000, - multiplier: 2, - max_us: 200_000, - timeout_us: 0, - }) - .unwrap(); + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + bind: Some("127.0.0.1:0".into()), + consume: Some(client_origin.clone()), + // Fast retries so the test doesn't wait out the default 1s backoff. + backoff: fast_backoff(), + ..Default::default() + }) + .unwrap(); let cs = tokio::time::timeout(TIMEOUT, client.connect(url)) .await @@ -3862,9 +4144,12 @@ async fn client_reconnects_and_resumes_announcements() { /// the session, surfacing through `closed()` instead of a redial. #[tokio::test] async fn one_shot_client_close_surfaces_through_closed() { - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); let addr = tokio::time::timeout(TIMEOUT, server.listen()) .await @@ -3879,13 +4164,16 @@ async fn one_shot_client_close_surfaces_through_closed() { .await .expect("accept errored") .expect("accept returned None"); - request.accept().await.expect("handshake failed") + request.accept(None, None).await.expect("handshake failed") }); - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); - client.set_bind("127.0.0.1:0".into()).unwrap(); - client.set_reconnect(false).unwrap(); + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + bind: Some("127.0.0.1:0".into()), + once: true, + ..Default::default() + }) + .unwrap(); let cs = tokio::time::timeout(TIMEOUT, client.connect(url)) .await @@ -3912,9 +4200,12 @@ async fn one_shot_client_close_surfaces_through_closed() { /// test_server_request_close, which drives the same path through the bindings. #[tokio::test] async fn rejected_session_surfaces_through_closed() { - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); let addr = tokio::time::timeout(TIMEOUT, server.listen()) .await @@ -3932,10 +4223,13 @@ async fn rejected_session_surfaces_through_closed() { } }); - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); - client.set_bind("127.0.0.1:0".into()).unwrap(); - client.set_reconnect(false).unwrap(); + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + bind: Some("127.0.0.1:0".into()), + once: true, + ..Default::default() + }) + .unwrap(); // Either the dial fails outright, or the optimistic connect resolves and the // rejection lands as the session's terminal close. Both must surface within @@ -3958,8 +4252,11 @@ async fn rejected_session_surfaces_through_closed() { /// loop or not; the kt BindingsSmokeTest relies on this to fail fast. #[tokio::test] async fn cancel_before_connect_fails_fast() { - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + ..Default::default() + }) + .unwrap(); client.cancel(); let result = tokio::time::timeout( Duration::from_secs(5), @@ -3981,9 +4278,12 @@ async fn cancel_before_connect_fails_fast() { /// `recv_datagram`) sits on that path; `status` is just the easiest to drive. #[tokio::test] async fn cancelled_status_does_not_swallow_the_next_transition() { - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); let addr = tokio::time::timeout(TIMEOUT, server.listen()) .await @@ -4000,20 +4300,16 @@ async fn cancelled_status_does_not_swallow_the_next_transition() { .await .expect("accept errored") .expect("accept returned None"); - request.accept().await.expect("handshake failed") + request.accept(None, None).await.expect("handshake failed") }); - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); - client.set_bind("127.0.0.1:0".into()).unwrap(); - client - .set_backoff(MoqBackoff { - initial_us: 50_000, - multiplier: 2, - max_us: 200_000, - timeout_us: 0, - }) - .unwrap(); + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + bind: Some("127.0.0.1:0".into()), + backoff: fast_backoff(), + ..Default::default() + }) + .unwrap(); let cs = tokio::time::timeout(TIMEOUT, client.connect(url)) .await @@ -4197,9 +4493,12 @@ async fn set_bitrate_caps_a_later_bandwidth_grant() { } async fn one_shot_peers() -> (Arc, Arc, Arc) { - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); let addr = tokio::time::timeout(TIMEOUT, server.listen()) .await .expect("listen timed out") @@ -4213,13 +4512,16 @@ async fn one_shot_peers() -> (Arc, Arc, Arc) .await .expect("accept errored") .expect("accept returned None"); - request.accept().await.expect("handshake failed") + request.accept(None, None).await.expect("handshake failed") }); - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); - client.set_bind("127.0.0.1:0".into()).unwrap(); - client.set_reconnect(false).unwrap(); + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + bind: Some("127.0.0.1:0".into()), + once: true, + ..Default::default() + }) + .unwrap(); let client_session = tokio::time::timeout(TIMEOUT, client.connect(url)) .await .expect("connect timed out") @@ -4264,33 +4566,29 @@ async fn session_protocol_codes_cross_the_ffi() { server.cancel(); } } - #[tokio::test] -async fn client_setters_busy_during_connect_and_cancelled_after() { - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); +async fn client_cancel_aborts_a_pending_connect() { + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); let addr = server.listen().await.expect("listen failed"); - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); - client.set_bind("127.0.0.1:0".into()).unwrap(); - client.set_reconnect(false).unwrap(); + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + bind: Some("127.0.0.1:0".into()), + once: true, + ..Default::default() + }) + .unwrap(); - // The server never accepts, so connect holds the client lock in established(). + // The server never accepts, so connect parks in established(). let connecting = client.clone(); - let connect = tokio::spawn(async move { connecting.connect(format!("https://{addr}")).await }); - - assert!(matches!( - 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))); + let connect = spawn_parked(async move { connecting.connect(format!("https://{addr}")).await }).await; client.cancel(); - 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) .await @@ -4301,84 +4599,28 @@ async fn client_setters_busy_during_connect_and_cancelled_after() { } #[tokio::test] -async fn client_setters_apply_after_connect_returns() { - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); - let addr = server.listen().await.expect("listen failed"); - - let accept_server = server.clone(); - let accept = tokio::spawn(async move { - let request = accept_server - .accept() - .await - .expect("accept errored") - .expect("accept returned None"); - request.accept().await.expect("handshake failed") - }); - - let client = MoqClient::new(); - 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 - .expect("connect timed out") - .expect("connect failed"); - - // A finished connect releases the lock; later setters apply to the next dial. - client.set_quic_max_streams(2048).unwrap(); - client.set_reconnect(false).unwrap(); - - let server_session = tokio::time::timeout(TIMEOUT, accept) - .await - .expect("accept timed out") - .expect("accept task panicked"); - session.shutdown().await.unwrap(); - server_session.cancel(0); - server.cancel(); -} - -#[tokio::test] -async fn server_setters_busy_during_accept_cancelled_after_and_frozen_after_listen() { - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); +async fn server_cert_fingerprints_busy_during_accept_and_cancelled_after() { + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); server.listen().await.expect("listen failed"); - - assert!(matches!(server.set_bind("127.0.0.1:1".into()), Err(MoqError::Bind(_)))); - assert!(matches!( - server.set_tls_generate(vec!["other".into()]), - Err(MoqError::Bind(_)) - )); - assert!(matches!( - server.set_tls_cert(vec!["cert.pem".into()]), - Err(MoqError::Bind(_)) - )); - assert!(matches!( - server.set_tls_key(vec!["key.pem".into()]), - Err(MoqError::Bind(_)) - )); - - // Origins are captured at accept, so they still apply between accepts. - server.set_publish(None).unwrap(); - server.set_consume(None).unwrap(); + server + .cert_fingerprints() + .expect("fingerprints available between accepts"); let accepting = server.clone(); let accept = tokio::spawn(async move { accepting.accept().await }); - assert!(matches!( - wait_for_config_error(|| server.set_publish(None), |err| matches!(err, MoqError::Busy)).await, - MoqError::Busy - )); - assert!(matches!(server.set_consume(None), Err(MoqError::Busy))); - assert!(matches!(server.cert_fingerprints(), Err(MoqError::Busy))); + wait_for_config_error( + || server.cert_fingerprints().map(drop), + |err| matches!(err, MoqError::Busy), + ) + .await; server.cancel(); - assert!(matches!(server.set_publish(None), Err(MoqError::Cancelled))); - assert!(matches!( - server.set_bind("127.0.0.1:0".into()), - Err(MoqError::Cancelled) - )); assert!(matches!(server.cert_fingerprints(), Err(MoqError::Cancelled))); let accept_err = tokio::time::timeout(TIMEOUT, accept) @@ -4388,84 +4630,112 @@ async fn server_setters_busy_during_accept_cancelled_after_and_frozen_after_list assert!(matches!(accept_err, Err(MoqError::Cancelled))); } +// Queue accept behind another operation so the origin arguments must survive the +// caller releasing its handles before the handshake starts on the FFI runtime. #[tokio::test] -async fn request_origin_setters_apply_or_error() { - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); - let addr = server.listen().await.expect("listen failed"); - - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); - client.set_bind("127.0.0.1:0".into()).unwrap(); - client.set_reconnect(false).unwrap(); - - let connecting = client.clone(); - let connect = tokio::spawn(async move { connecting.connect(format!("https://{addr}")).await }); - - let request = tokio::time::timeout(TIMEOUT, server.accept()) - .await - .expect("accept timed out") - .expect("accept errored") - .expect("accept returned None"); - - request.set_publish(None).unwrap(); - request.set_consume(None).unwrap(); - - let (held_tx, held_rx) = tokio::sync::oneshot::channel(); - let (release_tx, release_rx) = tokio::sync::oneshot::channel(); - let holding = request.clone(); - let hold = holding.hold_lock(|| async move { - let _ = held_tx.send(()); - let _ = release_rx.await; - }); - tokio::pin!(hold); - tokio::select! { - biased; - result = &mut hold => panic!("lock holder returned before release: {result:?}"), - held = held_rx => held.expect("lock holder dropped"), +async fn request_accept_inherits_overrides_and_captures_origins() { + for (override_publish, override_consume) in [(false, false), (true, false), (false, true), (true, true)] { + let default_publish = MoqOriginProducer::new(MoqOriginConfig::default()); + let default_consume = MoqOriginProducer::new(MoqOriginConfig::default()); + let fresh = MoqOriginProducer::new(MoqOriginConfig::default()); + let publish = override_publish.then(|| fresh.clone()); + let consume = override_consume.then(|| fresh.clone()); + let expected_publish = publish.as_ref().unwrap_or(&default_publish).inner().hop(); + let expected_consume = consume.as_ref().unwrap_or(&default_consume).consume(); + let captured = Arc::downgrade(&fresh); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + publish: Some(default_publish), + consume: Some(default_consume), + ..Default::default() + }) + .unwrap(); + let addr = server.listen().await.unwrap(); + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + bind: Some("127.0.0.1:0".into()), + once: true, + ..Default::default() + }) + .unwrap(); + let connecting = client.clone(); + let connect = tokio::spawn(async move { connecting.connect(format!("https://{addr}")).await }); + let request = tokio::time::timeout(TIMEOUT, server.accept()) + .await + .unwrap() + .unwrap() + .unwrap(); + let (held_tx, held_rx) = tokio::sync::oneshot::channel(); + let (release_tx, release_rx) = tokio::sync::oneshot::channel(); + let holding = request.clone(); + let hold = tokio::spawn(async move { + holding + .hold_lock(|| async move { + let _ = held_tx.send(()); + let _ = release_rx.await; + }) + .await + }); + tokio::time::timeout(TIMEOUT, held_rx).await.unwrap().unwrap(); + let accepting = request.clone(); + let accept = spawn_parked(async move { accepting.accept(publish, consume).await }).await; + drop(fresh); + if override_publish || override_consume { + assert!( + captured.upgrade().is_some(), + "accept must own the origin arguments while queued" + ); + } + release_tx.send(()).unwrap(); + tokio::time::timeout(TIMEOUT, hold).await.unwrap().unwrap().unwrap(); + let session = tokio::time::timeout(TIMEOUT, accept).await.unwrap().unwrap().unwrap(); + let client_session = tokio::time::timeout(TIMEOUT, connect).await.unwrap().unwrap().unwrap(); + assert_eq!(session.publish().inner().hop(), expected_publish); + + // Incoming broadcasts reach the selected consume origin, independently + // of which publish side was selected. + let announced = expected_consume.announced(MoqAnnounceConfig::default()).unwrap(); + let broadcast = create_announced(&client_session.publish(), "incoming"); + assert_eq!(next_announced(&announced).await.prefix, "incoming"); + if override_publish && override_consume { + // Both explicit arguments referred to one fresh origin, so the + // publish handle must see broadcasts arriving on the consume side. + let shared = session + .publish() + .consume() + .announced(MoqAnnounceConfig::default()) + .unwrap(); + assert_eq!(next_announced(&shared).await.prefix, "incoming"); + } + assert!(matches!( + request.accept(None, None).await, + Err(MoqError::AlreadyResponded) + )); + broadcast.close().unwrap(); + client_session.cancel(0); + session.cancel(0); + server.cancel(); } - - assert!(matches!( - wait_for_config_error(|| request.set_publish(None), |err| matches!(err, MoqError::Busy)).await, - MoqError::Busy - )); - assert!(matches!(request.set_consume(None), Err(MoqError::Busy))); - - release_tx.send(()).expect("lock holder should still be waiting"); - tokio::time::timeout(TIMEOUT, hold) - .await - .expect("lock holder timed out") - .expect("lock holder failed"); - - let session = tokio::time::timeout(TIMEOUT, request.accept()) - .await - .expect("handshake timed out") - .expect("handshake failed"); - - assert!(matches!(request.set_publish(None), Err(MoqError::AlreadyResponded))); - assert!(matches!(request.set_consume(None), Err(MoqError::AlreadyResponded))); - - let _cs = tokio::time::timeout(TIMEOUT, connect) - .await - .expect("connect timed out") - .expect("connect task panicked") - .expect("connect failed"); - session.cancel(0); - server.cancel(); } #[tokio::test] -async fn request_origin_setters_cancelled_after_cancel() { - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); +async fn request_accept_cancelled_after_cancel() { + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); let addr = server.listen().await.expect("listen failed"); - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); - client.set_bind("127.0.0.1:0".into()).unwrap(); - client.set_reconnect(false).unwrap(); + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + bind: Some("127.0.0.1:0".into()), + once: true, + ..Default::default() + }) + .unwrap(); let connecting = client.clone(); let connect = tokio::spawn(async move { connecting.connect(format!("https://{addr}")).await }); @@ -4476,10 +4746,36 @@ async fn request_origin_setters_cancelled_after_cancel() { .expect("accept errored") .expect("accept returned None"); - request.set_publish(None).unwrap(); + let (held_tx, held_rx) = tokio::sync::oneshot::channel(); + let (_release_tx, release_rx) = tokio::sync::oneshot::channel::<()>(); + let holding = request.clone(); + let hold = tokio::spawn(async move { + holding + .hold_lock(|| async move { + let _ = held_tx.send(()); + let _ = release_rx.await; + }) + .await + }); + tokio::time::timeout(TIMEOUT, held_rx).await.unwrap().unwrap(); + let origin = MoqOriginProducer::new(MoqOriginConfig::default()); + let captured = Arc::downgrade(&origin); + let accepting = request.clone(); + let accept = spawn_parked(async move { accepting.accept(Some(origin), None).await }).await; + assert!(captured.upgrade().is_some()); + request.cancel(); - assert!(matches!(request.set_publish(None), Err(MoqError::Cancelled))); - assert!(matches!(request.set_consume(None), Err(MoqError::Cancelled))); + assert!(matches!(request.accept(None, None).await, Err(MoqError::Cancelled))); + assert!(matches!(request.reject(403).await, Err(MoqError::Cancelled))); + + let result = tokio::time::timeout(TIMEOUT, accept).await.unwrap().unwrap(); + assert!(matches!(result, Err(MoqError::Cancelled))); + let result = tokio::time::timeout(TIMEOUT, hold).await.unwrap().unwrap(); + assert!(matches!(result, Err(MoqError::Cancelled))); + assert!( + captured.upgrade().is_none(), + "cancel must release the queued origin arguments" + ); client.cancel(); let _ = tokio::time::timeout(TIMEOUT, connect).await; @@ -4502,10 +4798,13 @@ async fn shutdown_cancels_and_drops_cleanly() { } let server_origin = MoqOriginProducer::new(MoqOriginConfig::default()); - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); - server.set_publish(Some(server_origin.clone())).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + publish: Some(server_origin.clone()), + ..Default::default() + }) + .unwrap(); let addr = tokio::time::timeout(TIMEOUT, server.listen()) .await .expect("listen timed out") @@ -4514,13 +4813,16 @@ async fn shutdown_cancels_and_drops_cleanly() { let accept_server = server.clone(); let accept = tokio::spawn(async move { let request = accept_server.accept().await.unwrap().expect("accept returned None"); - request.accept().await.expect("handshake failed") + request.accept(None, None).await.expect("handshake failed") }); let client_origin = MoqOriginProducer::new(MoqOriginConfig::default()); - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); - client.set_consume(Some(client_origin.clone())).unwrap(); + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + consume: Some(client_origin.clone()), + ..Default::default() + }) + .unwrap(); let session = tokio::time::timeout(TIMEOUT, client.connect(format!("https://{addr}"))) .await .expect("connect timed out") @@ -4584,18 +4886,21 @@ fn published_catalog( #[tokio::test] async fn json_tracks_are_advertised_in_the_catalog() { let broadcast = MoqBroadcastProducer::new().unwrap(); - let snapshot = broadcast - .publish_json_snapshot( - "status".into(), - MoqJsonSnapshotConfig { - delta_ratio: 4, - compression: true, - }, - ) - .unwrap(); - let stream = broadcast - .publish_json_stream("events".into(), MoqJsonStreamConfig { compression: false }) - .unwrap(); + let snapshot = MoqJsonSnapshotProducer::new( + &broadcast, + &broadcast.publish_track("status".into(), None).unwrap(), + MoqJsonSnapshotConfig { + delta_ratio: 4, + compression: true, + }, + ) + .unwrap(); + let stream = MoqJsonStreamProducer::new( + &broadcast, + &broadcast.publish_track("events".into(), None).unwrap(), + MoqJsonStreamConfig { compression: false }, + ) + .unwrap(); let catalog = published_catalog(&broadcast); let entry = catalog.json.tracks.get("status").expect("snapshot track advertised"); @@ -4620,24 +4925,24 @@ async fn json_tracks_are_advertised_in_the_catalog() { #[tokio::test] async fn flate_tracks_are_advertised_in_the_catalog() { let broadcast = MoqBroadcastProducer::new().unwrap(); - let thumb = broadcast - .publish_flate_snapshot( - "thumbnail".into(), - MoqFlateConfig { - compression: false, - mime: Some("image/jpeg".into()), - }, - ) - .unwrap(); - let log = broadcast - .publish_flate_stream( - "log".into(), - MoqFlateConfig { - compression: false, - mime: None, - }, - ) - .unwrap(); + let thumb = MoqFlateSnapshotProducer::new( + &broadcast, + &broadcast.publish_track("thumbnail".into(), None).unwrap(), + MoqFlateConfig { + compression: false, + mime: Some("image/jpeg".into()), + }, + ) + .unwrap(); + let log = MoqFlateStreamProducer::new( + &broadcast, + &broadcast.publish_track("log".into(), None).unwrap(), + MoqFlateConfig { + compression: false, + mime: None, + }, + ) + .unwrap(); thumb.update(vec![0xff, 0xd8, 0xff]).unwrap(); log.append(vec![1, 2, 3]).unwrap(); @@ -4660,30 +4965,28 @@ async fn flate_tracks_are_advertised_in_the_catalog() { } /// A second data track under a name the catalog already carries is refused, leaving the first. +/// +/// The broadcast refuses a duplicate track itself, so the collision comes from a track created on +/// another broadcast. The refused track's handle stays open for the caller. #[tokio::test] async fn data_track_names_cannot_collide() { let broadcast = MoqBroadcastProducer::new().unwrap(); - let first = broadcast - .publish_json_snapshot( - "state".into(), - MoqJsonSnapshotConfig { - delta_ratio: 0, - compression: false, - }, - ) - .unwrap(); + let first = MoqJsonSnapshotProducer::new( + &broadcast, + &broadcast.publish_track("state".into(), None).unwrap(), + MoqJsonSnapshotConfig { + delta_ratio: 0, + compression: false, + }, + ) + .unwrap(); + let other = MoqBroadcastProducer::new().unwrap(); + let track = other.publish_track("state".into(), None).unwrap(); assert!( - broadcast - .publish_flate_stream( - "state".into(), - MoqFlateConfig { - compression: false, - mime: None, - }, - ) - .is_err(), + MoqJsonStreamProducer::new(&broadcast, &track, MoqJsonStreamConfig { compression: false }).is_err(), "a duplicate data track name should fail" ); + assert_eq!(track.demand().unwrap().name(), "state"); assert_eq!( published_catalog(&broadcast) .json @@ -4697,16 +5000,31 @@ async fn data_track_names_cannot_collide() { /// Real sockets exercise the FFI runtime and both accepted/client session handles. async fn shutdown_pair() -> (Arc, Arc, Arc) { - let server = MoqServer::new(); - server.set_bind("127.0.0.1:0".into()).unwrap(); - server.set_tls_generate(vec!["localhost".into()]).unwrap(); + let server = MoqServer::new(MoqServerConfig { + bind: Some("127.0.0.1:0".into()), + tls: localhost_tls(), + ..Default::default() + }) + .unwrap(); let addr = server.listen().await.unwrap(); let accepting = server.clone(); - let accepted = tokio::spawn(async move { accepting.accept().await.unwrap().unwrap().accept().await.unwrap() }); - let client = MoqClient::new(); - client.set_tls_verify(false).unwrap(); - client.set_bind("127.0.0.1:0".into()).unwrap(); - client.set_reconnect(false).unwrap(); + let accepted = tokio::spawn(async move { + accepting + .accept() + .await + .unwrap() + .unwrap() + .accept(None, None) + .await + .unwrap() + }); + let client = MoqClient::new(MoqClientConfig { + tls: insecure_tls(), + bind: Some("127.0.0.1:0".into()), + once: true, + ..Default::default() + }) + .unwrap(); let connected = client.connect(format!("https://{addr}")).await.unwrap(); (server, connected, accepted.await.unwrap()) } @@ -4732,7 +5050,7 @@ async fn shutdown_delivers_the_finished_track() { let end = reader.read_frame().await?; Ok::<_, MoqError>((frame, end)) }); - tokio::time::timeout(TIMEOUT, track.used()) + tokio::time::timeout(TIMEOUT, track.demand().unwrap().used()) .await .expect("used") .unwrap(); @@ -4777,7 +5095,10 @@ async fn shutdown_times_out_on_an_unfinished_track() { let remote = await_announced(&subscriber.consume(), "live").await; let reader = remote.subscribe_track("data".into(), None).await.unwrap(); let receiving = tokio::spawn(async move { reader.read_frame().await }); - tokio::time::timeout(TIMEOUT, track.used()).await.unwrap().unwrap(); + tokio::time::timeout(TIMEOUT, track.demand().unwrap().used()) + .await + .unwrap() + .unwrap(); let start = std::time::Instant::now(); let err = publisher.shutdown().await.expect_err("unfinished track must time out"); // The documented one second of draining, so an immediate bail still fails here. diff --git a/rs/moq-ffi/src/transport.rs b/rs/moq-ffi/src/transport.rs index a75b6a4efc7..d78c1cd3187 100644 --- a/rs/moq-ffi/src/transport.rs +++ b/rs/moq-ffi/src/transport.rs @@ -7,22 +7,30 @@ mod adapter; use url::Url; use web_transport_wasm::{ClientBuilder, Error}; -/// Advertise every version `moq-net` supports, so the peer picks one via ALPN. -/// Without this the browser negotiates no subprotocol and the session has no -/// version to start from. -fn builder() -> ClientBuilder { - ClientBuilder::new().with_protocols(moq_net::ALPNS.iter().copied()) +/// Advertise the ALPN of every allowed version, so the peer picks one the session +/// will accept. Without this the browser negotiates no subprotocol and the session +/// has no version to start from. +fn builder(versions: &moq_net::Versions) -> ClientBuilder { + ClientBuilder::new().with_protocols(versions.alpns()) } /// Open a browser WebTransport connection to `url`, trusting the system roots. -pub async fn connect(url: Url) -> Result { - builder().with_system_roots().connect(url).await.map(Session::new) +pub async fn connect(url: Url, versions: &moq_net::Versions) -> Result { + builder(versions) + .with_system_roots() + .connect(url) + .await + .map(Session::new) } /// Connect, trusting only the given sha-256 certificate hashes (serverless dev, /// matching the browser's `serverCertificateHashes` option). -pub async fn connect_with_hashes(url: Url, hashes: Vec>) -> Result { - builder() +pub async fn connect_with_hashes( + url: Url, + versions: &moq_net::Versions, + hashes: Vec>, +) -> Result { + builder(versions) .with_server_certificate_hashes(hashes) .connect(url) .await diff --git a/rs/moq-ffi/src/video.rs b/rs/moq-ffi/src/video.rs index 163ebfd8b15..5538eca18f5 100644 --- a/rs/moq-ffi/src/video.rs +++ b/rs/moq-ffi/src/video.rs @@ -1,7 +1,7 @@ //! Raw-video publishing via [`moq_video`]. //! //! Sibling to [`audio`](crate::audio)'s producer, and the video counterpart to -//! [`producer::MoqMediaProducer`](crate::producer::MoqMediaProducer): that one +//! [`media::MoqMediaTrackProducer`](crate::media::MoqMediaTrackProducer): that one //! takes already-encoded frames, this one takes raw pictures and runs the H.264 //! / H.265 encode inside the FFI boundary (VideoToolbox on macOS and iOS, Media //! Foundation on Windows, openh264 as the software fallback; no ffmpeg). @@ -232,7 +232,7 @@ impl VideoProducer { /// Producer for a raw-video track. /// -/// Built via [`MoqBroadcastProducer::publish_video`]. Each +/// Built via [`MoqBroadcastProducer::encode_video`]. Each /// [`write`](Self::write) accepts a [`MoqVideoFrame`] whose `data` is in the /// pixel format declared by the [`MoqVideoEncoderInput`] passed at publish time. #[derive(uniffi::Object)] @@ -754,7 +754,7 @@ impl MoqBroadcastConsumer { /// Subscribe to a video track and decode it inside the bindings. /// /// `catalog_video` comes from the catalog (see - /// [`MoqCatalogConsumer::next`](crate::consumer::MoqCatalogConsumer::next)); the codec is read + /// [`MoqMediaCatalogConsumer::next`](crate::media::MoqMediaCatalogConsumer::next)); the codec is read /// from it. Errors if no native backend handles that codec, rather than failing on the first /// frame. Also fails with [`MoqError::Unsupported`] when /// [`surface`](MoqVideoDecoderOutput::surface) is set on a platform with no surface to expose. diff --git a/sh/go/package-wrapper.sh b/sh/go/package-wrapper.sh index 52159e7932e..910c8b5478b 100755 --- a/sh/go/package-wrapper.sh +++ b/sh/go/package-wrapper.sh @@ -103,11 +103,13 @@ mkdir -p "$PKG_STAGE" # --- 1. Copy in-tree source --- # The package sits at the module root, so the import path is the module path: -# `import "moq.dev/moq"` binds `moq` with no alias. +# `import "moq.dev/moq"` binds `moq` with no alias. Subpackages (`moq.dev/moq/json`, +# `internal/`) keep their directories. `vendor/` is local `go mod vendor` output. cp "$SOURCE_DIR/go.mod" "$PKG_STAGE/" cp "$SOURCE_DIR/VERSION" "$PKG_STAGE/" -for f in "$SOURCE_DIR"/*.go; do - cp "$f" "$PKG_STAGE/" +(cd "$SOURCE_DIR" && find . -path ./vendor -prune -o -name '*.go' -print) | while IFS= read -r f; do + mkdir -p "$PKG_STAGE/$(dirname "$f")" + cp "$SOURCE_DIR/$f" "$PKG_STAGE/$f" done # Dual-license files lifted from the workspace root. diff --git a/sh/rs/stats-docs.py b/sh/rs/stats-docs.py index f4c95fd855c..e6b6b34c5c5 100644 --- a/sh/rs/stats-docs.py +++ b/sh/rs/stats-docs.py @@ -38,6 +38,13 @@ def pascal(field: str) -> str: "doc/lib/go/index.md": pascal, } +# Python and Go own their stats record so microseconds read as a duration type, +# which renames those fields. +RENAMED = { + "doc/lib/py/index.md": {"rtt_us": "rtt"}, + "doc/lib/go/index.md": {"rtt_us": "RTT"}, +} + def fields() -> list[str]: """The field names declared by `MoqConnectionStats`, in source order.""" @@ -57,7 +64,7 @@ def main() -> int: for page, spell in PAGES.items(): text = (ROOT / page).read_text() for field in names: - name = spell(field) + name = RENAMED.get(page, {}).get(field) or spell(field) if not re.search(rf"\b{re.escape(name)}\b", text): missing.append(f"{page}: {name}") if "_valid" not in (ROOT / C_PAGE).read_text(): diff --git a/swift/README.md b/swift/README.md index 14faa0918a4..46bae58e111 100644 --- a/swift/README.md +++ b/swift/README.md @@ -26,11 +26,11 @@ SPM resolves `MoqFFI` (and its prebuilt `MoqFFI.xcframework`, attached to the ma ```swift import Moq -let client = Client() +let client = try Client() let session = try await client.connect(to: "https://relay.example.com") // session.publish and session.consume are always populated: by whatever -// origin you wired via setPublish / setConsume before connect, or by a fresh +// origin you passed as Client(publish:consume:), or by a fresh // auto-created one. The duplex no-config path (the typical client) shares one // origin between both sides. let announced = try session.consume.announced(prefix: "demos/", filter: "*/camera") @@ -42,7 +42,7 @@ for try await event in announced { print("captures \(announcement.captures ?? [])") let broadcast = try await session.consume.requestBroadcast(path: announcement.prefix) - let catalog = try await broadcast.subscribeCatalog() + let catalog = try await Media.CatalogConsumer.subscribe(broadcast: broadcast) for try await update in catalog { print("catalog: \(update)") } @@ -65,7 +65,7 @@ A note on enum casing: `MoqError` keeps Rust's PascalCase variants, each carryin ## API shape -The wrapper fully wraps every stateful handle (`Client`, `Session`, `BroadcastProducer`, `TrackConsumer`, …) and re-exports the plain data records/enums under de-prefixed names via typealias (`Frame`, `Catalog`, `Audio`, `Container`, …). Because the records are typealiased, new fields on the `moq-ffi` side flow through automatically; only new FFI *methods* need a matching wrapper method. +The wrapper fully wraps every stateful handle (`Client`, `Session`, `BroadcastProducer`, `TrackConsumer`, …) and re-exports the plain data records/enums under de-prefixed names via typealias (`Frame`, `Media.Catalog`, `Media.Audio`, `Media.Container`, …). Because the records are typealiased, new fields on the `moq-ffi` side flow through automatically; only new FFI *methods* need a matching wrapper method. Every consumer conforms to `AsyncSequence`, so `for try await x in consumer` works directly. `TrackConsumer` iterates groups in sequence order; use its `groupsAsArrived` property for arrival order. @@ -77,11 +77,14 @@ and `TrackConsumer.datagrams` streams them in arrival order. Payloads are capped Datagrams require a datagram-capable transport and lite-05 or newer moq-lite, or moq-transport; pre-lite-05, WebSocket, and TCP paths do not deliver them, and there is no stream fallback. -JSON tracks carry your own `Codable` types with the framing handled for you. You opt into one of two -distinct modes: `publishJsonSnapshot`/`subscribeJsonSnapshot` (lossy latest-value, a subscriber only -sees the newest value) or `publishJsonStream`/`subscribeJsonStream` (lossless append-log, every record -preserved in order). Values encode/decode at the boundary with `JSONEncoder`/`JSONDecoder`; the consumer -is an `AsyncSequence` of the decoded type. Pass matching `compression` on both sides. +JSON tracks live under the `Json` namespace, mirroring the `moq-json` crate, and carry your own +`Codable` types with the framing handled for you. Each type wraps a track: a producer takes over a +`TrackProducer` and advertises it in the broadcast's catalog, and a consumer takes over a +`TrackConsumer`. You opt into one of two distinct modes: `Json.SnapshotProducer`/`Json.SnapshotConsumer` +(lossy latest-value, a subscriber only sees the newest value) or `Json.StreamProducer`/`Json.StreamConsumer` +(lossless append-log, every record preserved in order). Values encode/decode at the boundary with +`JSONEncoder`/`JSONDecoder`; the consumer is an `AsyncSequence` of the decoded type. Pass matching +`compression` on both sides. ## Local development diff --git a/swift/Sources/Moq/Aliases.swift b/swift/Sources/Moq/Aliases.swift index ebf9ecf1119..2d96d853fb8 100644 --- a/swift/Sources/Moq/Aliases.swift +++ b/swift/Sources/Moq/Aliases.swift @@ -10,33 +10,9 @@ import MoqFFI /// A payload plus the presentation timestamp it should play at. The unit of /// every raw write and raw read. public typealias Frame = MoqFFI.MoqFrame -/// A media frame whose keyframe flag marks a group start or video keyframe; audio flags only group starts. -public typealias MediaFrame = MoqFFI.MoqMediaFrame -/// The JSON manifest describing a broadcast's tracks: video and audio -/// renditions, display geometry, and untyped application sections. -public typealias Catalog = MoqFFI.MoqCatalog -/// A video rendition in the catalog: codec, dimensions, bitrate, whether it is -/// enabled, framerate, and container. -public typealias Video = MoqFFI.MoqVideo -/// Caller-provided catalog fields for a video track. -public typealias VideoHint = MoqFFI.MoqVideoHint -/// A single audio codec an importer can parse. -public typealias AudioFormat = MoqFFI.MoqAudioFormat -/// A single video codec an importer can parse. -public typealias VideoFormat = MoqFFI.MoqVideoFormat -/// A container that publishes its own tracks. -public typealias ContainerFormat = MoqFFI.MoqContainerFormat -/// Catalog properties shared by every video rendition. A `nil` field clears -/// that property from the next catalog snapshot. -public typealias VideoProperties = MoqFFI.MoqVideoProperties -/// An audio rendition in the catalog: codec, sample rate, channel count, -/// bitrate, whether it is enabled, and container. -public typealias Audio = MoqFFI.MoqAudio /// One raw-audio frame: PCM samples in the configured layout plus a /// presentation timestamp. public typealias AudioFrame = MoqFFI.MoqAudioFrame -/// A width and height in pixels. -public typealias Dimensions = MoqFFI.MoqDimensions /// The PCM layout (format, sample rate, channels) written to an `AudioProducer`. public typealias AudioEncoderInput = MoqFFI.MoqAudioEncoderInput /// The encoder-side config for a published audio track: codec, rate, channels, @@ -70,9 +46,6 @@ public typealias VideoCodec = MoqFFI.MoqVideoCodec /// Which encoder implementation to use: automatic, hardware, software, or one /// named backend. public typealias VideoEncoderKind = MoqFFI.MoqVideoEncoderKind -/// How a track's frames are packaged (Legacy, CMAF, or LOC), as advertised in -/// the catalog. -public typealias Container = MoqFFI.MoqContainer /// A best-effort raw-track datagram as received: sequence, timestamp, and payload. public typealias Datagram = MoqFFI.MoqDatagram /// A path-prefix route: the prefix it covers, relay hop ids (oldest first), @@ -98,9 +71,22 @@ public typealias TrackInfo = MoqFFI.MoqTrackInfo /// counters). Fields are `nil` when the transport backend doesn't report them. public typealias ConnectionStats = MoqFFI.MoqConnectionStats -/// Retry pacing for the automatic reconnect; see `Client.setBackoff`. +/// Retry pacing for the automatic reconnect; see `Client(backoff:)`. A `nil` +/// field keeps its default. public typealias Backoff = MoqFFI.MoqBackoff +/// Certificate trust and the mTLS identity for a `Client`. +public typealias ClientTls = MoqFFI.MoqClientTls + +/// The served TLS identity for a `Server`: PEM files or generated hostnames. +public typealias ServerTls = MoqFFI.MoqServerTls + +/// QUIC transport tuning, such as the peer's inbound stream cap. +public typealias QuicConfig = MoqFFI.MoqQuicConfig + +/// The WebSocket fallback raced against QUIC: whether it runs and QUIC's head start. +public typealias WebSocketConfig = MoqFFI.MoqWebSocketConfig + /// A connection lifecycle transition reported by `Session.status()`. public typealias ConnectionStatus = MoqFFI.MoqConnectionStatus diff --git a/swift/Sources/Moq/Broadcast.swift b/swift/Sources/Moq/Broadcast.swift index 8c65cbcc21d..c53fad24ada 100644 --- a/swift/Sources/Moq/Broadcast.swift +++ b/swift/Sources/Moq/Broadcast.swift @@ -9,11 +9,6 @@ public final class BroadcastConsumer: Sendable { self.ffi = ffi } - /// Subscribe to the broadcast's catalog (the description of its tracks). - public func subscribeCatalog() async throws -> CatalogConsumer { - CatalogConsumer(try await ffi.subscribeCatalog()) - } - /// Subscribe to a track by name, delivering raw frame payloads with no codec /// or container parsing. `subscription` tunes delivery priority, group range, and /// staleness; omit for defaults. @@ -31,43 +26,12 @@ public final class BroadcastConsumer: Sendable { GroupConsumer(try await ffi.fetchGroup(name: name, sequence: sequence, options: options)) } - /// Fetch one complete group and decode its track container into media frames. - public func fetchMediaGroup( - name: String, - sequence: UInt64, - container: Container, - options: FetchGroupOptions? = nil - ) async throws -> MediaGroupConsumer { - MediaGroupConsumer( - try await ffi.fetchMediaGroup( - name: name, - sequence: sequence, - container: container, - options: options - ) - ) - } - - /// Subscribe to a media track, delivering frames in decode order. `container` - /// comes from the catalog. `subscription` tunes delivery priority, group ordering - /// priority, group range, and the max delay; omit for defaults. Raise - /// `Subscription.maxDelayUs` to buffer instead of skipping a stalled group. - public func subscribeMedia( - name: String, - container: Container, - subscription: Subscription? = nil - ) async throws -> MediaConsumer { - MediaConsumer( - try await ffi.subscribeMedia( - name: name, container: container, subscription: subscription)) - } - /// Resolve a catalog rendition's `broadcast` reference to the broadcast serving its track. /// - /// `reference` is `Video.broadcast` / `Audio.broadcast`: `nil` or empty names this + /// `reference` is `Media.Video.broadcast` / `Media.Audio.broadcast`: `nil` or empty names this /// broadcast, anything else names a sibling relative to it (e.g. `./source`). Call it on a - /// rendition that carries one before `subscribeMedia`, `subscribeTrack`, `fetchGroup`, or - /// `fetchMediaGroup`, which take a track name rather than a rendition; `decodeAudio` and + /// rendition that carries one before `Media.ContainerConsumer.subscribe`, `subscribeTrack`, `fetchGroup`, or + /// `Media.ContainerGroupConsumer.fetch`, which take a track name rather than a rendition; `decodeAudio` and /// `decodeVideo` resolve it themselves. /// /// Throws if this broadcast came from a local producer rather than an origin, since a @@ -78,7 +42,7 @@ public final class BroadcastConsumer: Sendable { /// Subscribe to a raw-audio track, decoding to PCM in the layout `output` /// declares. `catalogAudio` is the matching rendition from the catalog. - public func decodeAudio(name: String, catalogAudio: Audio, output: AudioDecoderOutput) async throws -> AudioConsumer { + public func decodeAudio(name: String, catalogAudio: Media.Audio, output: AudioDecoderOutput) async throws -> AudioConsumer { AudioConsumer(try await ffi.decodeAudio(name: name, catalogAudio: catalogAudio, output: output)) } @@ -90,35 +54,11 @@ public final class BroadcastConsumer: Sendable { /// own dimensions. public func decodeVideo( name: String, - catalogVideo: Video, + catalogVideo: Media.Video, output: VideoDecoderOutput = VideoDecoderOutput() ) async throws -> VideoConsumer { VideoConsumer(try await ffi.decodeVideo(name: name, catalogVideo: catalogVideo, output: output)) } - - /// Subscribe to a JSON snapshot track (lossy latest-value), decoding each value as `Value`. - /// - /// Yields only the newest value, collapsing the backlog for a reader that has fallen behind. - /// `compression` must match the flag the producer used. - public func subscribeJsonSnapshot( - name: String, - as _: Value.Type, - compression: Bool = false - ) async throws -> JsonSnapshotConsumer { - // deltaRatio is producer-only, so leave it at its default here. - JsonSnapshotConsumer(try await ffi.subscribeJsonSnapshot(name: name, config: MoqJsonSnapshotConfig(compression: compression))) - } - - /// Subscribe to a JSON stream track (lossless append-log), decoding each record as `Value`. - /// - /// Yields every record in order. `compression` must match the flag the producer used. - public func subscribeJsonStream( - name: String, - as _: Value.Type, - compression: Bool = false - ) async throws -> JsonStreamConsumer { - JsonStreamConsumer(try await ffi.subscribeJsonStream(name: name, config: MoqJsonStreamConfig(compression: compression))) - } } /// Write side of a broadcast: open tracks and publish frames. @@ -163,108 +103,6 @@ public final class BroadcastProducer: Sendable { try ffi.unannounce() } - /// Replace the catalog properties shared by every video rendition. - public func setVideoProperties(_ properties: VideoProperties) throws { - try ffi.setVideoProperties(properties: properties) - } - - /// Open a media track. `format` controls how `initData` and frame payloads - /// are interpreted (e.g. `"opus"`, `"avc3"`). `video` seeds catalog fields - /// that the stream cannot reveal before its first keyframe. - /// Publish one audio codec as a new track. `initData` is required: audio resolves its whole - /// rendition from those bytes. `track` names the track; otherwise a unique name is derived from - /// the format. - public func publishAudio( - format: AudioFormat, - initData: Data, - label: String? = nil, - track: String? = nil - ) throws -> MediaProducer { - MediaProducer( - try ffi.publishAudio(init: MoqAudioInit(format: format, data: initData, label: label, track: track)) - ) - } - - /// Publish one video codec as a new track. `initData` may be empty for a format that resolves - /// in band; `hint` seeds catalog fields the stream can't reveal. `track` names the track; - /// otherwise a unique name is derived from the format. - public func publishVideo( - format: VideoFormat, - initData: Data = Data(), - label: String? = nil, - hint: VideoHint? = nil, - track: String? = nil - ) throws -> MediaProducer { - MediaProducer( - try ffi.publishVideo( - init: MoqVideoInit(format: format, data: initData, label: label, hint: hint, track: track) - ) - ) - } - - /// Publish a container, which demuxes and publishes its own tracks. There is no label or hint: - /// a container describes each track it publishes from its own metadata. - public func publishContainer( - format: ContainerFormat, - initData: Data = Data() - ) throws -> ContainerProducer { - ContainerProducer(try ffi.publishContainer(init: MoqContainerInit(format: format, data: initData))) - } - - /// Publish one audio codec onto a track requested through `BroadcastDynamic`. - public func publishAudio( - on request: TrackRequest, - format: AudioFormat, - initData: Data, - label: String? = nil - ) throws -> MediaProducer { - MediaProducer( - try ffi.publishAudioOnTrack( - request: request.ffi, - init: MoqAudioInit(format: format, data: initData, label: label) - ) - ) - } - - /// Publish one video codec onto a track requested through `BroadcastDynamic`. - public func publishVideo( - on request: TrackRequest, - format: VideoFormat, - initData: Data = Data(), - label: String? = nil, - hint: VideoHint? = nil - ) throws -> MediaProducer { - MediaProducer( - try ffi.publishVideoOnTrack( - request: request.ffi, - init: MoqVideoInit(format: format, data: initData, label: label, hint: hint) - ) - ) - } - - /// Open a video track fed by a raw byte stream with inferred frame boundaries (e.g. piped - /// Annex-B H.264). Only the self-delimiting formats work: `.avc3`, `.hev1`, `.av01`. Audio has - /// no counterpart, having no frame boundaries to infer. - public func publishVideoStream( - format: VideoFormat, - label: String? = nil, - hint: VideoHint? = nil, - track: String? = nil - ) throws -> MediaStreamProducer { - MediaStreamProducer( - try ffi.publishVideoStream( - init: MoqVideoInit(format: format, data: Data(), label: label, hint: hint, track: track) - ) - ) - } - - /// Open a container fed by a raw byte stream, which recovers its own framing. - public func publishContainerStream(format: ContainerFormat) throws -> ContainerStreamProducer { - ContainerStreamProducer( - try ffi.publishContainerStream(format: format) - ) - } - /// Open a track for arbitrary byte payloads, with no codec or container. /// `info` sets track properties (priority, cache, timescale); omit for defaults. public func publishTrack(name: String, info: TrackInfo? = nil) throws -> TrackProducer { @@ -303,47 +141,6 @@ public final class BroadcastProducer: Sendable { VideoProducer(try ffi.encodeVideo(input: input, output: output, bandwidth: bandwidth?.ffi)) } - /// Open a JSON snapshot track (lossy latest-value), encoding each value from `Value`. - /// - /// Each `update` supersedes the last; a late joiner only sees the newest value. `deltaRatio` - /// controls how aggressively merge-patch deltas replace full snapshots (`0` disables deltas). - /// Set `compression` to DEFLATE each group; the consumer must pass the same flag. Advertise - /// the track with `setCatalogSection` if consumers should discover it. - public func publishJsonSnapshot( - name: String, - of _: Value.Type, - deltaRatio: UInt32 = MoqJsonSnapshotConfig().deltaRatio, - compression: Bool = false - ) throws -> JsonSnapshotProducer { - JsonSnapshotProducer(try ffi.publishJsonSnapshot(name: name, config: MoqJsonSnapshotConfig(deltaRatio: deltaRatio, compression: compression))) - } - - /// Open a JSON stream track (lossless append-log), encoding each record from `Value`. - /// - /// Every appended record is preserved and delivered in order. Set `compression` to DEFLATE - /// the group; the consumer must pass the same flag. - public func publishJsonStream( - name: String, - of _: Value.Type, - compression: Bool = false - ) throws -> JsonStreamProducer { - JsonStreamProducer(try ffi.publishJsonStream(name: name, config: MoqJsonStreamConfig(compression: compression))) - } - - /// Set (or replace) an untyped application catalog section by name. - /// - /// `json` is any JSON document as a string; it rides alongside `video`/`audio` and reaches - /// subscribers via `Catalog.sections`. `name` must not be a reserved media section - /// (`video`/`audio`). The catalog is republished automatically. - public func setCatalogSection(name: String, json: String) throws { - try ffi.setCatalogSection(name: name, json: json) - } - - /// Remove an untyped application catalog section by name. A no-op if it was absent. - public func removeCatalogSection(name: String) throws { - try ffi.removeCatalogSection(name: name) - } - /// End the broadcast for good: retract it and serve no new tracks. /// /// Tracks already subscribed carry on to their own end. Closing again is a no-op. diff --git a/swift/Sources/Moq/Client.swift b/swift/Sources/Moq/Client.swift index cad513e7f4b..3cf061b0d51 100644 --- a/swift/Sources/Moq/Client.swift +++ b/swift/Sources/Moq/Client.swift @@ -1,108 +1,50 @@ import MoqFFI -/// A MoQ client. Configure the optional knobs, then `connect(to:)`. -/// -/// Setters write the configuration `connect` snapshots. They throw `.Busy` while -/// a connect is in flight and `.Cancelled` after `cancel()`. A finished connect -/// does not freeze the handle: later setters apply to the next dial until cancel. +/// A MoQ client, built from its configuration and then dialed with `connect(to:)`. public final class Client: Sendable { let ffi: MoqClient - /// Create a client with default settings, ready to configure and `connect(to:)`. - public init() { - ffi = MoqClient() - } - - /// 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.setTlsVerify(verify: verify) - } - - /// Trust these PEM root certificate file path(s) instead of the system roots. - public func setTlsRoots(_ paths: [String]) throws { - try ffi.setTlsRoots(paths: paths) - } - - /// Configure whether platform roots are trusted in addition to custom roots. - public func setTlsSystemRoots(_ enabled: Bool) throws { - try ffi.setTlsSystemRoots(systemRoots: enabled) - } - - /// Pin the peer certificate to these hex SHA-256 fingerprints, the native - /// equivalent of `serverCertificateHashes`. Accepts the values a server - /// reports via `Server.certFingerprints`, so a self-signed certificate can be - /// trusted without disabling verification. - public func setTlsFingerprints(_ fingerprints: [String]) throws { - try ffi.setTlsFingerprints(fingerprints: fingerprints) - } - - /// Set the path to a PEM certificate chain to present when the relay requires mTLS. - public func setTlsCert(_ path: String?) throws { - try ffi.setTlsCert(path: path) - } - - /// Set the path to a PEM private key to present when the relay requires mTLS. - public func setTlsKey(_ path: String?) throws { - try ffi.setTlsKey(path: path) - } - - /// Set the local UDP socket bind address (defaults to `[::]:0`). Throws if - /// the address cannot be parsed, if a connect is in flight, or after cancel. - public func bind(_ addr: String) throws { - try ffi.setBind(addr: addr) - } - - /// Cap the concurrent QUIC streams the peer may open toward this connection - /// (defaults to 1024). MoQ opens a stream per group, and for a subscriber - /// those arrive from the relay, so subscribing to many tracks may want this - /// raised. Ignored by the WebSocket fallback. - public func setQuicMaxStreams(_ maxStreams: UInt64) throws { - try ffi.setQuicMaxStreams(maxStreams: maxStreams) - } - - /// Enable or disable the WebSocket fallback (on by default), which races QUIC - /// for `http(s)` URLs. Disable it against a relay that only serves QUIC. - public func setWebsocketEnabled(_ enabled: Bool) throws { - try ffi.setWebsocketEnabled(enabled: enabled) - } - - /// Set the head start, in microseconds, QUIC gets before the WebSocket - /// fallback joins the race (defaults to 200ms). Zero races both at once. - public func setWebsocketDelay(_ delayUs: UInt64) throws { - try ffi.setWebsocketDelay(delayUs: delayUs) - } - - /// Wire the origin whose local broadcasts get advertised to the remote. If - /// left unset, `connect` auto-creates one, reachable via `Session.publish`. - public func setPublish(_ origin: OriginProducer?) throws { - try ffi.setPublish(origin: origin?.ffi) - } - - /// Wire the origin used to receive the remote's announcements. If left - /// unset, `connect` auto-creates one, reachable via `Session.consume`. - public func setConsume(_ origin: OriginProducer?) throws { - try ffi.setConsume(origin: origin?.ffi) - } - - /// Enable or disable automatic reconnecting (on by default). When enabled, the - /// session redials with backoff whenever the transport drops, and broadcasts - /// consumed through it ride out the gap. Disable for a one-shot dial whose - /// transport close ends the session. - public func setReconnect(_ enabled: Bool) throws { - try ffi.setReconnect(enabled: enabled) - } - - /// Configure retry pacing for the automatic reconnect. - public func setBackoff(_ backoff: Backoff) throws { - try ffi.setBackoff(backoff: backoff) + /// Create a client. Every argument has a default, so pass only what you need. + /// + /// - Parameters: + /// - bind: The local UDP address to bind; `nil` binds an ephemeral dual-stack port. + /// - versions: Protocol versions to offer, most preferred first (`"moq-lite-03"`); + /// empty offers every supported version. + /// - tls: Certificate trust (`insecure`, `roots`, `fingerprints`, ...) and the mTLS identity. + /// - quic: QUIC tuning, such as the peer's inbound stream cap. + /// - websocket: The WebSocket fallback raced against QUIC for `http(s)` URLs. + /// - once: Dial once instead of redialing with backoff whenever the transport drops. + /// - backoff: Retry pacing for the automatic reconnect. + /// - publish: The origin whose broadcasts are published to the remote. + /// - consume: The origin that receives the remote's broadcasts. With neither origin + /// given, both sides of each session share one, so a broadcast announced via + /// `Session.publish` is also discoverable through `Session.consume`. + /// - Throws: `MoqError.Config` for a value the native side cannot use. + public init( + bind: String? = nil, + versions: [String] = [], + tls: ClientTls = ClientTls(), + quic: QuicConfig = QuicConfig(), + websocket: WebSocketConfig = WebSocketConfig(), + once: Bool = false, + backoff: Backoff = Backoff(), + publish: OriginProducer? = nil, + consume: OriginProducer? = nil + ) throws { + ffi = try MoqClient(config: MoqClientConfig( + bind: bind, + versions: versions, + tls: tls, + quic: quic, + websocket: websocket, + once: once, + backoff: backoff, + publish: publish?.ffi, + consume: consume?.ffi + )) } /// Connect and wait for the session to be established. Cancellable via `cancel()`. - /// - /// With neither `setPublish` nor `setConsume` wired, both sides of the session share one - /// origin, so a broadcast announced via `Session.publish` is also discoverable through - /// `Session.consume`. Wiring either side opts out and isolates the two directions. public func connect(to url: String) async throws -> Session { Session(try await ffi.connect(url: url)) } @@ -122,13 +64,13 @@ public final class Session: Sendable { } /// The publish-side origin: where local broadcasts are advertised to the - /// remote. Either the one wired via `Client.setPublish`, or auto-created. + /// remote. Either the one wired via `Client(publish:)`, or auto-created. public var publish: OriginProducer { OriginProducer(ffi.publish()) } /// The subscribe-side origin: a read handle for the remote's announcements. - /// Either derived from `Client.setConsume`, or auto-created. + /// Either derived from `Client(consume:)`, or auto-created. public var consume: OriginConsumer { OriginConsumer(ffi.consume()) } diff --git a/swift/Sources/Moq/Json.swift b/swift/Sources/Moq/Json.swift index 21b215e606d..222fa7c677e 100644 --- a/swift/Sources/Moq/Json.swift +++ b/swift/Sources/Moq/Json.swift @@ -1,125 +1,161 @@ import Foundation import MoqFFI -/// Write side of a JSON snapshot track (lossy latest-value). +/// JSON tracks, mirroring the `moq-json` crate. /// -/// Built via `BroadcastProducer.publishJsonSnapshot`. Each `update` supersedes the last, so a -/// late joiner only sees the newest value. Values cross the FFI boundary as JSON, encoded from -/// `Value` with `JSONEncoder`, so `Value` must encode to a JSON object or array: `JSONEncoder` -/// rejects a top-level scalar (a bare number/string/bool) on Foundation before the swift-foundation -/// rewrite (iOS < 18 / macOS < 15). -public final class JsonSnapshotProducer: Sendable { - let ffi: MoqJsonSnapshotProducer - - init(_ ffi: MoqJsonSnapshotProducer) { - self.ffi = ffi - } - - /// Publish a new value, encoded as a snapshot or merge-patch delta automatically. A no-op - /// if unchanged from the previous update. - public func update(_ value: Value) throws { - try ffi.update(value: encodeJson(value)) - } - - /// A watch-only handle to whether the track has subscribers. - public func demand() throws -> TrackDemand { - TrackDemand(try ffi.demand()) - } - - /// Finish the track, closing any open group. - public func finish() throws { - try ffi.finish() - } -} - -/// Read side of a JSON snapshot track (lossy latest-value). Iterating yields the latest value, -/// collapsing the backlog for a reader that has fallen behind: `for try await value in json { ... }`. -public final class JsonSnapshotConsumer: AsyncSequence, Sendable { - /// The decoded snapshot value emitted by this sequence. - public typealias Element = Value - - let ffi: MoqJsonSnapshotConsumer - - init(_ ffi: MoqJsonSnapshotConsumer) { - self.ffi = ffi - } - - /// The next value, decoded as `Value`, or `nil` once the track ends or is closed. - public func next() async throws -> Value? { - guard let json = try await ffi.next() else { return nil } - return try decodeJson(json) as Value - } - - /// Cancel all current and future reads. - public func cancel() { - ffi.cancel() - } - - /// Create an iterator that cancels native reads when iteration ends. - public func makeAsyncIterator() -> AsyncThrowingStream.Iterator { - moqStream(cancel: { [ffi] in ffi.cancel() }) { [self] in - try await next() - }.makeAsyncIterator() - } -} - -/// Write side of a JSON stream track (lossless append-log). -/// -/// Built via `BroadcastProducer.publishJsonStream`. Every `append` is preserved and delivered -/// in order. Values cross the FFI boundary as JSON, encoded from `Value` with `JSONEncoder`, so -/// `Value` must encode to a JSON object or array: `JSONEncoder` rejects a top-level scalar (a bare -/// number/string/bool) on Foundation before the swift-foundation rewrite (iOS < 18 / macOS < 15). -public final class JsonStreamProducer: Sendable { - let ffi: MoqJsonStreamProducer - - init(_ ffi: MoqJsonStreamProducer) { - self.ffi = ffi - } - - /// Append one record to the log. - public func append(_ value: Value) throws { - try ffi.append(value: encodeJson(value)) - } - - /// A watch-only handle to whether the track has subscribers. - public func demand() throws -> TrackDemand { - TrackDemand(try ffi.demand()) - } - - /// Finish the track, closing the group. - public func finish() throws { - try ffi.finish() - } -} - -/// Read side of a JSON stream track (lossless append-log). Iterating yields every record in -/// order: `for try await record in json { ... }`. -public final class JsonStreamConsumer: AsyncSequence, Sendable { - /// The decoded stream record emitted by this sequence. - public typealias Element = Value - - let ffi: MoqJsonStreamConsumer - - init(_ ffi: MoqJsonStreamConsumer) { - self.ffi = ffi - } - - /// The next record, decoded as `Value`, or `nil` once the track ends or is closed. - public func next() async throws -> Value? { - guard let json = try await ffi.next() else { return nil } - return try decodeJson(json) as Value - } - - /// Cancel all current and future reads. - public func cancel() { - ffi.cancel() - } - - /// Create an iterator that cancels native reads when iteration ends. - public func makeAsyncIterator() -> AsyncThrowingStream.Iterator { - moqStream(cancel: { [ffi] in ffi.cancel() }) { [self] in - try await next() - }.makeAsyncIterator() +/// Each type wraps a track: a producer takes over a `TrackProducer` and advertises it in the +/// broadcast's catalog, and a consumer takes over a `TrackConsumer` that has not read a group +/// yet. Values cross the FFI boundary as JSON, encoded with `JSONEncoder` and decoded with +/// `JSONDecoder`. `JSONEncoder` rejects a top-level scalar (a bare number/string/bool) on +/// Foundation before the swift-foundation rewrite (iOS < 18 / macOS < 15), so encode an object +/// or array there. +public enum Json {} + +extension Json { + /// Write side of a JSON snapshot track (lossy latest-value). + /// + /// Each `update` supersedes the last, so a late joiner only sees the newest value. + public final class SnapshotProducer: Sendable { + let ffi: MoqJsonSnapshotProducer + + /// Publish `track` as a JSON snapshot track, advertised in `broadcast`'s catalog until it + /// finishes. Takes over `track`, whose handle is closed afterward; throws if the catalog + /// already carries the track's name. + /// + /// `deltaRatio` controls how aggressively merge-patch deltas replace full snapshots (`0` + /// disables deltas). Set `compression` to DEFLATE each group; the consumer must pass the + /// same flag. + public init( + broadcast: BroadcastProducer, + track: TrackProducer, + deltaRatio: UInt32 = MoqJsonSnapshotConfig().deltaRatio, + compression: Bool = false + ) throws { + ffi = try MoqJsonSnapshotProducer( + broadcast: broadcast.ffi, + track: track.ffi, + config: MoqJsonSnapshotConfig(deltaRatio: deltaRatio, compression: compression) + ) + } + + /// Publish a new value, encoded as a snapshot or merge-patch delta automatically. A no-op + /// if unchanged from the previous update. + public func update(_ value: Value) throws { + try ffi.update(value: encodeJson(value)) + } + + /// A watch-only handle to whether the track has subscribers. + public func demand() throws -> TrackDemand { + TrackDemand(try ffi.demand()) + } + + /// Finish the track, closing any open group. + public func finish() throws { + try ffi.finish() + } + } + + /// Read side of a JSON snapshot track (lossy latest-value). Iterating yields the latest value, + /// collapsing the backlog for a reader that has fallen behind: `for try await value in json { ... }`. + public final class SnapshotConsumer: AsyncSequence, Sendable { + /// The decoded snapshot value emitted by this sequence. + public typealias Element = Value + + let ffi: MoqJsonSnapshotConsumer + + /// Read `track` as a JSON snapshot track. Takes over `track`, whose handle is closed + /// afterward; throws if it has already read a group. `compression` must match the flag + /// the producer used. + public init(track: TrackConsumer, compression: Bool = false) throws { + // deltaRatio is producer-only, so leave it at its default here. + ffi = try MoqJsonSnapshotConsumer(track: track.ffi, config: MoqJsonSnapshotConfig(compression: compression)) + } + + /// The next value, decoded as `Value`, or `nil` once the track ends or is closed. + public func next() async throws -> Value? { + guard let json = try await ffi.next() else { return nil } + return try decodeJson(json) as Value + } + + /// Cancel all current and future reads. + public func cancel() { + ffi.cancel() + } + + /// Create an iterator that cancels native reads when iteration ends. + public func makeAsyncIterator() -> AsyncThrowingStream.Iterator { + moqStream(cancel: { [ffi] in ffi.cancel() }) { [self] in + try await next() + }.makeAsyncIterator() + } + } + + /// Write side of a JSON stream track (lossless append-log). + /// + /// Every `append` is preserved and delivered in order. + public final class StreamProducer: Sendable { + let ffi: MoqJsonStreamProducer + + /// Publish `track` as a JSON stream track, advertised in `broadcast`'s catalog until it + /// finishes. Takes over `track`, whose handle is closed afterward; throws if the catalog + /// already carries the track's name. Set `compression` to DEFLATE the group; the consumer + /// must pass the same flag. + public init(broadcast: BroadcastProducer, track: TrackProducer, compression: Bool = false) throws { + ffi = try MoqJsonStreamProducer( + broadcast: broadcast.ffi, + track: track.ffi, + config: MoqJsonStreamConfig(compression: compression) + ) + } + + /// Append one record to the log. + public func append(_ value: Value) throws { + try ffi.append(value: encodeJson(value)) + } + + /// A watch-only handle to whether the track has subscribers. + public func demand() throws -> TrackDemand { + TrackDemand(try ffi.demand()) + } + + /// Finish the track, closing the group. + public func finish() throws { + try ffi.finish() + } + } + + /// Read side of a JSON stream track (lossless append-log). Iterating yields every record in + /// order: `for try await record in json { ... }`. + public final class StreamConsumer: AsyncSequence, Sendable { + /// The decoded stream record emitted by this sequence. + public typealias Element = Value + + let ffi: MoqJsonStreamConsumer + + /// Read `track` as a JSON stream track. Takes over `track`, whose handle is closed + /// afterward; throws if it has already read a group. `compression` must match the flag + /// the producer used. + public init(track: TrackConsumer, compression: Bool = false) throws { + ffi = try MoqJsonStreamConsumer(track: track.ffi, config: MoqJsonStreamConfig(compression: compression)) + } + + /// The next record, decoded as `Value`, or `nil` once the track ends or is closed. + public func next() async throws -> Value? { + guard let json = try await ffi.next() else { return nil } + return try decodeJson(json) as Value + } + + /// Cancel all current and future reads. + public func cancel() { + ffi.cancel() + } + + /// Create an iterator that cancels native reads when iteration ends. + public func makeAsyncIterator() -> AsyncThrowingStream.Iterator { + moqStream(cancel: { [ffi] in ffi.cancel() }) { [self] in + try await next() + }.makeAsyncIterator() + } } } diff --git a/swift/Sources/Moq/Media.swift b/swift/Sources/Moq/Media.swift index d2494b20cd6..c496d2bb88c 100644 --- a/swift/Sources/Moq/Media.swift +++ b/swift/Sources/Moq/Media.swift @@ -1,241 +1,331 @@ import Foundation import MoqFFI -/// Read side of a broadcast's catalog. Iterating yields catalog updates as the -/// set of tracks changes. -public final class CatalogConsumer: AsyncSequence, Sendable { - /// The catalog update emitted by this sequence. - public typealias Element = Catalog - - let ffi: MoqCatalogConsumer - - init(_ ffi: MoqCatalogConsumer) { - self.ffi = ffi - } - - /// The next catalog update, or `nil` once the track ends or is closed. - public func next() async throws -> Catalog? { - try await ffi.next() - } - - /// Cancel all current and future reads. - public func cancel() { - ffi.cancel() - } - - /// Create an iterator that cancels native reads when iteration ends. - public func makeAsyncIterator() -> AsyncThrowingStream.Iterator { - moqStream(cancel: { [ffi] in ffi.cancel() }) { [ffi] in +/// Catalogs, encoded media imports, and container consumers. +public enum Media { + /// A media frame whose keyframe flag marks a group start or video keyframe; audio flags only group starts. + public typealias MediaFrame = MoqFFI.MoqMediaFrame + /// The JSON manifest describing a broadcast's tracks: video and audio + /// renditions, display geometry, and untyped application sections. + public typealias Catalog = MoqFFI.MoqCatalog + /// A video rendition in the catalog: codec, dimensions, bitrate, whether it is + /// enabled, framerate, and container. + public typealias Video = MoqFFI.MoqVideo + /// Caller-provided catalog fields for a video track. + public typealias VideoHint = MoqFFI.MoqVideoHint + /// A single audio codec an importer can parse. + public typealias AudioFormat = MoqFFI.MoqAudioFormat + /// A single video codec an importer can parse. + public typealias VideoFormat = MoqFFI.MoqVideoFormat + /// A container that publishes its own tracks. + public typealias ContainerFormat = MoqFFI.MoqContainerFormat + /// Catalog properties shared by every video rendition. A `nil` field clears + /// that property from the next catalog snapshot. + public typealias VideoProperties = MoqFFI.MoqVideoProperties + /// An audio rendition in the catalog: codec, sample rate, channel count, + /// bitrate, whether it is enabled, and container. + public typealias Audio = MoqFFI.MoqAudio + /// A width and height in pixels. + public typealias Dimensions = MoqFFI.MoqDimensions + /// How a track's frames are packaged (Legacy, CMAF, or LOC), as advertised in + /// the catalog. + public typealias Container = MoqFFI.MoqContainer + + /// The named or requested track an importer publishes. + public enum Target { + /// A new track with an explicit name, or a format-derived unique name. + case named(name: String?) + /// A subscriber-requested track. + case requested(TrackRequest) + var ffi: MoqMediaTarget { + switch self { + case .named(let name): return .named(name: name) + case .requested(let request): return .requested(request: request.ffi) + } + } + } + /// An audio codec, its initialization bytes, and label. + public typealias AudioInit = MoqAudioInit + /// A video codec, its initialization bytes, label, and hints. + public typealias VideoInit = MoqVideoInit + /// A container format and its leading bytes. + public typealias ContainerInit = MoqContainerInit + /// A track name, its container, and live delivery options. + public typealias ContainerConfig = MoqMediaContainerConfig + /// A fetched group's name, sequence, container, and delivery options. + public typealias ContainerGroupConfig = MoqMediaContainerGroupConfig + + /// Updates a catalog without keeping its broadcast open. + public final class CatalogProducer: Sendable { + let ffi: MoqMediaCatalogProducer + /// Construct a weak catalog handle for this broadcast. + public init(broadcast: BroadcastProducer) throws { ffi = try MoqMediaCatalogProducer(broadcast: broadcast.ffi) } + /// Replace the video properties shared by every rendition. + public func setVideoProperties(_ properties: VideoProperties) throws { try ffi.setVideoProperties(properties: properties) } + /// Set an application catalog section from its serialized JSON value. + public func setSection(name: String, json: String) throws { try ffi.setSection(name: name, json: json) } + /// Remove an application catalog section if present. + public func removeSection(name: String) throws { try ffi.removeSection(name: name) } + } + + /// Read side of a broadcast's catalog. Iterating yields catalog updates as the + /// set of tracks changes. + public final class CatalogConsumer: AsyncSequence, Sendable { + /// The catalog update emitted by this sequence. + public typealias Element = Catalog + + let ffi: MoqMediaCatalogConsumer + + /// Subscribe to this broadcast's catalog snapshots. + public static func subscribe(broadcast: BroadcastConsumer) async throws -> CatalogConsumer { + CatalogConsumer(try await MoqMediaCatalogConsumer.subscribe(broadcast: broadcast.ffi)) + } + init(_ ffi: MoqMediaCatalogConsumer) { + self.ffi = ffi + } + + /// The next catalog update, or `nil` once the track ends or is closed. + public func next() async throws -> Catalog? { try await ffi.next() - }.makeAsyncIterator() - } -} - -/// Read side of a media track. Iterating yields decoded frames in decode order. -public final class MediaConsumer: AsyncSequence, Sendable { - /// The decoded media frame emitted by this sequence. - public typealias Element = MediaFrame - - let ffi: MoqMediaConsumer - - init(_ ffi: MoqMediaConsumer) { - self.ffi = ffi - } - - /// The next frame, or `nil` once the track ends or is closed. - public func next() async throws -> MediaFrame? { - try await ffi.next() - } - - /// Cancel all current and future reads. - public func cancel() { - ffi.cancel() - } - - /// Create an iterator that cancels native reads when iteration ends. - public func makeAsyncIterator() -> AsyncThrowingStream.Iterator { - moqStream(cancel: { [ffi] in ffi.cancel() }) { [ffi] in + } + + /// Cancel all current and future reads. + public func cancel() { + ffi.cancel() + } + + /// Create an iterator that cancels native reads when iteration ends. + public func makeAsyncIterator() -> AsyncThrowingStream.Iterator { + moqStream(cancel: { [ffi] in ffi.cancel() }) { [ffi] in + try await ffi.next() + }.makeAsyncIterator() + } + } + + /// Read side of a media track. Iterating yields decoded frames in decode order. + public final class ContainerConsumer: AsyncSequence, Sendable { + /// The decoded media frame emitted by this sequence. + public typealias Element = MediaFrame + + let ffi: MoqMediaContainerConsumer + + /// Subscribe to a track and decode its container. + public static func subscribe(broadcast: BroadcastConsumer, config: ContainerConfig) async throws -> ContainerConsumer { + ContainerConsumer(try await MoqMediaContainerConsumer.subscribe(broadcast: broadcast.ffi, config: config)) + } + init(_ ffi: MoqMediaContainerConsumer) { + self.ffi = ffi + } + + /// The next frame, or `nil` once the track ends or is closed. + public func next() async throws -> MediaFrame? { try await ffi.next() - }.makeAsyncIterator() - } -} - -/// A finite, container-decoded media group returned by ``BroadcastConsumer/fetchMediaGroup(name:sequence:container:options:)``. -public final class MediaGroupConsumer: AsyncSequence, Sendable { - /// The decoded media frame emitted by this sequence. - public typealias Element = MediaFrame - - let ffi: MoqMediaGroupConsumer - - init(_ ffi: MoqMediaGroupConsumer) { - self.ffi = ffi - } - - /// The sequence number of this group within the track. - public var sequence: UInt64 { - ffi.sequence() - } - - /// The next frame, or `nil` once the group ends. - public func next() async throws -> MediaFrame? { - try await ffi.next() - } - - /// Cancel all current and future reads. - public func cancel() { - ffi.cancel() - } - - /// Create an iterator that cancels native reads when iteration ends. - public func makeAsyncIterator() -> AsyncThrowingStream.Iterator { - moqStream(cancel: { [ffi] in ffi.cancel() }) { [ffi] in + } + + /// Cancel all current and future reads. + public func cancel() { + ffi.cancel() + } + + /// Create an iterator that cancels native reads when iteration ends. + public func makeAsyncIterator() -> AsyncThrowingStream.Iterator { + moqStream(cancel: { [ffi] in ffi.cancel() }) { [ffi] in + try await ffi.next() + }.makeAsyncIterator() + } + } + + /// A finite, container-decoded media group returned by ``ContainerGroupConsumer/fetch(broadcast:config:)``. + public final class ContainerGroupConsumer: AsyncSequence, Sendable { + /// The decoded media frame emitted by this sequence. + public typealias Element = MediaFrame + + let ffi: MoqMediaContainerGroupConsumer + + /// Fetch and decode exactly one media group. + public static func fetch(broadcast: BroadcastConsumer, config: ContainerGroupConfig) async throws -> ContainerGroupConsumer { + ContainerGroupConsumer(try await MoqMediaContainerGroupConsumer.fetch(broadcast: broadcast.ffi, config: config)) + } + init(_ ffi: MoqMediaContainerGroupConsumer) { + self.ffi = ffi + } + + /// The sequence number of this group within the track. + public var sequence: UInt64 { + ffi.sequence() + } + + /// The next frame, or `nil` once the group ends. + public func next() async throws -> MediaFrame? { try await ffi.next() - }.makeAsyncIterator() - } -} - -/// Write side of a media track fed pre-framed payloads. -public final class MediaProducer: Sendable { - let ffi: MoqMediaProducer - - init(_ ffi: MoqMediaProducer) { - self.ffi = ffi - } - - /// The track's name. - public var name: String { - get throws { try ffi.name() } - } - - /// A watch-only handle to whether the track has subscribers. - public func demand() throws -> TrackDemand { - TrackDemand(try ffi.demand()) - } - - /// Suspend until the track has at least one active consumer. Prefer `demand()`. - public func used() async throws { - try await ffi.used() - } - - /// Suspend until the track has no active consumers. Prefer `demand()`. - public func unused() async throws { - try await ffi.unused() - } - - /// Write a frame with the given presentation timestamp (microseconds). - /// - /// The importer derives keyframe status from the bitstream, so only the payload and its - /// timestamp cross the boundary. - public func writeFrame(_ payload: Data, timestampUs: UInt64 = 0) throws { - try ffi.writeFrame(frame: Frame(payload: payload, timestampUs: timestampUs)) - } - - /// Record a local encoder's frame handoff on the broadcast media clock. - /// Call after `writeFrame` only for local encoder output. - public func flush(timestampUs: UInt64) throws { - try ffi.flush(timestampUs: timestampUs) - } - - /// Mark a timeline break and restart handoff measurement, preserving advertised jitter. - public func discontinuity() throws { - try ffi.discontinuity() - } - - /// Draw a group boundary here. - /// - /// Audio has no boundary of its own (every packet is independently decodable), so this is the - /// only thing that gives it groups: call it after every frame for one group (one QUIC stream) - /// the relay forwards without waiting, or at a segment cadence to align with video. Video - /// groups at its own keyframes and needs this only to override that. - public func cut() throws { - try ffi.cut() - } - - /// Draw a group boundary and number the next group `sequence`. - /// - /// ``cut()`` with an explicit sequence, for a publisher whose group numbers have to be - /// deterministic: two encoders aligning per GOP so a consumer can fail over between them. - public func seek(_ sequence: UInt64) throws { - try ffi.seek(sequence: sequence) - } - - /// Finish the track and finalize encoding. - public func finish() throws { - try ffi.finish() - } -} - -/// Write side of a container, which demuxes and publishes its own tracks. -public final class ContainerProducer: Sendable { - let ffi: MoqContainerProducer - - init(_ ffi: MoqContainerProducer) { - self.ffi = ffi - } - - /// Write a whole chunk of container bytes. - /// - /// No timestamp: a container carries its tracks' timing itself, and the importer reads it out - /// rather than taking the caller's word for it. - public func write(_ payload: Data) throws { - try ffi.write(payload: payload) - } - - /// Declare that the next chunk starts a new segment, rolling a group on every track. - /// - /// An fMP4 source carrying `styp` atoms declares its own, so this is only needed when it - /// doesn't; formats with no segment concept (MKV, TS, FLV) ignore it. - public func cut() throws { - try ffi.cut() - } - - /// Start a new segment and number its groups `sequence`. - public func seek(_ sequence: UInt64) throws { - try ffi.seek(sequence: sequence) - } - - /// Finish every track this container publishes. - public func finish() throws { - try ffi.finish() - } -} - -/// Write side of a container fed a raw byte stream, which recovers its own framing. -public final class ContainerStreamProducer: Sendable { - let ffi: MoqContainerStreamProducer - - init(_ ffi: MoqContainerStreamProducer) { - self.ffi = ffi - } - - /// Push raw container bytes; chunk boundaries don't matter. - public func write(_ payload: Data) throws { - try ffi.write(payload: payload) - } - - /// Finish every track this container publishes. - public func finish() throws { - try ffi.finish() - } -} - -/// Write side of a media track fed a raw byte stream with inferred frame boundaries. -public final class MediaStreamProducer: Sendable { - let ffi: MoqMediaStreamProducer - - init(_ ffi: MoqMediaStreamProducer) { - self.ffi = ffi - } - - /// Push raw stream bytes (e.g. Annex-B H.264). The importer frames whole - /// access units and buffers any partial trailing frame for the next call. - public func write(_ payload: Data) throws { - try ffi.write(payload: payload) - } - - /// Finalize the track. A trailing access unit with no following delimiter is - /// not emitted (matches the moq-cli stdin path). - public func finish() throws { - try ffi.finish() + } + + /// Cancel all current and future reads. + public func cancel() { + ffi.cancel() + } + + /// Create an iterator that cancels native reads when iteration ends. + public func makeAsyncIterator() -> AsyncThrowingStream.Iterator { + moqStream(cancel: { [ffi] in ffi.cancel() }) { [ffi] in + try await ffi.next() + }.makeAsyncIterator() + } + } + + /// Write side of a media track fed pre-framed payloads. + public final class TrackProducer: Sendable { + let ffi: MoqMediaTrackProducer + + /// Import complete audio frames on a named or requested track. + public static func audio(broadcast: BroadcastProducer, initData: AudioInit, target: Target = .named(name: nil)) throws -> TrackProducer { + TrackProducer(try MoqMediaTrackProducer.audio(broadcast: broadcast.ffi, target: target.ffi, init: initData)) + } + /// Import complete video frames on a named or requested track. + public static func video(broadcast: BroadcastProducer, initData: VideoInit, target: Target = .named(name: nil)) throws -> TrackProducer { + TrackProducer(try MoqMediaTrackProducer.video(broadcast: broadcast.ffi, target: target.ffi, init: initData)) + } + init(_ ffi: MoqMediaTrackProducer) { + self.ffi = ffi + } + + /// A watch-only handle to whether the track has subscribers. + public func demand() throws -> TrackDemand { + TrackDemand(try ffi.demand()) + } + + /// Write a frame with the given presentation timestamp (microseconds). + /// + /// The importer derives keyframe status from the bitstream, so only the payload and its + /// timestamp cross the boundary. + public func writeFrame(_ payload: Data, timestampUs: UInt64 = 0) throws { + try ffi.writeFrame(frame: Frame(payload: payload, timestampUs: timestampUs)) + } + + /// Record a local encoder's frame handoff on the broadcast media clock. + /// Call after `writeFrame` only for local encoder output. + public func flush(timestampUs: UInt64) throws { + try ffi.flush(timestampUs: timestampUs) + } + + /// Mark a timeline break and restart handoff measurement, preserving advertised jitter. + public func discontinuity() throws { + try ffi.discontinuity() + } + + /// Draw a group boundary here. + /// + /// Audio has no boundary of its own (every packet is independently decodable), so this is the + /// only thing that gives it groups: call it after every frame for one group (one QUIC stream) + /// the relay forwards without waiting, or at a segment cadence to align with video. Video + /// groups at its own keyframes and needs this only to override that. + public func cut() throws { + try ffi.cut() + } + + /// Draw a group boundary and number the next group `sequence`. + /// + /// ``cut()`` with an explicit sequence, for a publisher whose group numbers have to be + /// deterministic: two encoders aligning per GOP so a consumer can fail over between them. + public func seek(_ sequence: UInt64) throws { + try ffi.seek(sequence: sequence) + } + + /// Finish the track and finalize encoding. + public func finish() throws { + try ffi.finish() + } + } + + /// Write side of a container, which demuxes and publishes its own tracks. + public final class ContainerProducer: Sendable { + let ffi: MoqMediaContainerProducer + + /// Import complete container chunks into this broadcast. + public convenience init(broadcast: BroadcastProducer, initData: ContainerInit) throws { + self.init(try MoqMediaContainerProducer(broadcast: broadcast.ffi, init: initData)) + } + init(_ ffi: MoqMediaContainerProducer) { + self.ffi = ffi + } + + /// Write a whole chunk of container bytes. + /// + /// No timestamp: a container carries its tracks' timing itself, and the importer reads it out + /// rather than taking the caller's word for it. + public func write(_ payload: Data) throws { + try ffi.write(payload: payload) + } + + /// Declare that the next chunk starts a new segment, rolling a group on every track. + /// + /// An fMP4 source carrying `styp` atoms declares its own, so this is only needed when it + /// doesn't; formats with no segment concept (MKV, TS, FLV) ignore it. + public func cut() throws { + try ffi.cut() + } + + /// Start a new segment and number its groups `sequence`. + public func seek(_ sequence: UInt64) throws { + try ffi.seek(sequence: sequence) + } + + /// Finish every track this container publishes. + public func finish() throws { + try ffi.finish() + } + } + + /// Write side of a container fed a raw byte stream, which recovers its own framing. + public final class ContainerStreamProducer: Sendable { + let ffi: MoqMediaContainerStreamProducer + + /// Import a container byte stream into this broadcast. + public convenience init(broadcast: BroadcastProducer, format: ContainerFormat) throws { + self.init(try MoqMediaContainerStreamProducer(broadcast: broadcast.ffi, format: format)) + } + init(_ ffi: MoqMediaContainerStreamProducer) { + self.ffi = ffi + } + + /// Push raw container bytes; chunk boundaries don't matter. + public func write(_ payload: Data) throws { + try ffi.write(payload: payload) + } + + /// Finish every track this container publishes. + public func finish() throws { + try ffi.finish() + } + } + + /// Write side of a media track fed a raw byte stream with inferred frame boundaries. + public final class TrackStreamProducer: Sendable { + let ffi: MoqMediaTrackStreamProducer + + /// Import a video byte stream, inferring frame boundaries. + public static func video(broadcast: BroadcastProducer, initData: VideoInit, target: Target = .named(name: nil)) throws -> TrackStreamProducer { + TrackStreamProducer(try MoqMediaTrackStreamProducer.video(broadcast: broadcast.ffi, target: target.ffi, init: initData)) + } + init(_ ffi: MoqMediaTrackStreamProducer) { + self.ffi = ffi + } + + /// A watch-only handle to whether this media track has subscribers. + public func demand() throws -> TrackDemand { + TrackDemand(try ffi.demand()) + } + + /// Push raw stream bytes (e.g. Annex-B H.264). The importer frames whole + /// access units and buffers any partial trailing frame for the next call. + public func write(_ payload: Data) throws { + try ffi.write(payload: payload) + } + + /// Finalize the track. A trailing access unit with no following delimiter is + /// not emitted (matches the moq-cli stdin path). + public func finish() throws { + try ffi.finish() + } } } diff --git a/swift/Sources/Moq/Server.swift b/swift/Sources/Moq/Server.swift index 2e4707703d4..98bb78d0558 100644 --- a/swift/Sources/Moq/Server.swift +++ b/swift/Sources/Moq/Server.swift @@ -1,50 +1,38 @@ import MoqFFI /// A MoQ server that accepts incoming QUIC/WebTransport sessions. -/// -/// Bind and TLS are captured at `listen()`; those setters throw afterwards. -/// Origins are captured at each `accept()`. Every setter throws `.Busy` while -/// listen/accept is in flight and `.Cancelled` after `cancel()`. public final class Server: Sendable { let ffi: MoqServer - /// Create a server with default settings, ready to configure and `listen()`. - public init() { - ffi = MoqServer() - } - - /// Set the address to bind, e.g. `127.0.0.1:4443`, `[::]:443`, or `localhost:0`. - /// Validated syntactically here; DNS hostnames resolve at `listen()` time. - /// Captured at `listen()`; throws afterwards. - public func bind(_ addr: String) throws { - try ffi.setBind(addr: addr) - } - - /// Load TLS certificate chains from PEM files on disk. Captured at `listen()`. - public func setTlsCert(_ paths: [String]) throws { - try ffi.setTlsCert(paths: paths) - } - - /// Load TLS private keys from PEM files on disk. Captured at `listen()`. - public func setTlsKey(_ paths: [String]) throws { - try ffi.setTlsKey(paths: paths) - } - - /// Generate self-signed TLS certificates for the given hostnames. Clients - /// must pin the fingerprint (see `certFingerprints`) or disable verification. - /// Captured at `listen()`. - public func generateTls(hostnames: [String]) throws { - try ffi.setTlsGenerate(hostnames: hostnames) - } - - /// Set the origin to publish broadcasts to incoming sessions. Captured at each `accept()`. - public func setPublish(_ origin: OriginProducer?) throws { - try ffi.setPublish(origin: origin?.ffi) - } - - /// Set the origin to consume broadcasts from incoming sessions. Captured at each `accept()`. - public func setConsume(_ origin: OriginProducer?) throws { - try ffi.setConsume(origin: origin?.ffi) + /// Create a server. Nothing binds until `listen()`. + /// + /// - Parameters: + /// - bind: The address to bind, e.g. `127.0.0.1:4443` or `localhost:0`; `nil` binds + /// `[::]:443`. DNS hostnames resolve at `listen()`. + /// - versions: Protocol versions to accept (`"moq-lite-03"`); empty accepts every + /// supported version. + /// - tls: The served identity: PEM `cert`/`key` files, or hostnames to `generate` + /// a self-signed certificate for (clients then pin `certFingerprints` or skip verification). + /// - quic: QUIC tuning, such as each peer's inbound stream cap. + /// - publish: The origin whose broadcasts are served to incoming sessions. + /// - consume: The origin that receives broadcasts published by incoming sessions. + /// - Throws: `MoqError.Config` for a value the native side cannot use. + public init( + bind: String? = nil, + versions: [String] = [], + tls: ServerTls = ServerTls(), + quic: QuicConfig = QuicConfig(), + publish: OriginProducer? = nil, + consume: OriginProducer? = nil + ) throws { + ffi = try MoqServer(config: MoqServerConfig( + bind: bind, + versions: versions, + tls: tls, + quic: quic, + publish: publish?.ffi, + consume: consume?.ffi + )) } /// Bind the listening socket. Returns the bound local address, useful when @@ -102,21 +90,9 @@ public final class Request: Sendable { ffi.transport() } - /// Override the publish origin for this session, falling back to the server's. - /// Captured at `accept()`. Throws if the request is busy, already answered, or cancelled. - public func setPublish(_ origin: OriginProducer?) throws { - try ffi.setPublish(origin: origin?.ffi) - } - - /// Override the consume origin for this session, falling back to the server's. - /// Captured at `accept()`. Throws if the request is busy, already answered, or cancelled. - public func setConsume(_ origin: OriginProducer?) throws { - try ffi.setConsume(origin: origin?.ffi) - } - - /// Complete the handshake and return the established session. - public func accept() async throws -> Session { - Session(try await ffi.accept()) + /// Complete the handshake, inheriting server origins wherever an argument is nil. + public func accept(publish: OriginProducer? = nil, consume: OriginProducer? = nil) async throws -> Session { + Session(try await ffi.accept(publish: publish?.ffi, consume: consume?.ffi)) } /// Reject the session with an application error code; 401 and 403 map to unauthorized. diff --git a/swift/Sources/Moq/Track.swift b/swift/Sources/Moq/Track.swift index 20cd57f6ca4..3462c2001bc 100644 --- a/swift/Sources/Moq/Track.swift +++ b/swift/Sources/Moq/Track.swift @@ -153,32 +153,17 @@ public final class TrackProducer: Sendable { self.ffi = ffi } - /// The track's name. - public var name: String { - get throws { try ffi.name() } - } - /// A read handle for this track (local pub/sub, no origin needed). /// `subscription` tunes delivery priority, group range, and staleness; omit for defaults. public func consume(subscription: Subscription? = nil) throws -> TrackConsumer { TrackConsumer(try ffi.consume(subscription: subscription)) } - /// A watch-only handle to whether the track has subscribers. + /// A watch-only handle to the track's name and whether it has subscribers. public func demand() throws -> TrackDemand { TrackDemand(try ffi.demand()) } - /// Suspend until the track has at least one active consumer. Prefer `demand()`. - public func used() async throws { - try await ffi.used() - } - - /// Suspend until the track has no active consumers. Prefer `demand()`. - public func unused() async throws { - try await ffi.unused() - } - /// Serve fetches for groups that are not currently cached. public func dynamic() throws -> TrackDynamic { TrackDynamic(try ffi.dynamic()) diff --git a/swift/Tests/MoqTests/SmokeTests.swift b/swift/Tests/MoqTests/SmokeTests.swift index e7961e6dca0..65b68b645a0 100644 --- a/swift/Tests/MoqTests/SmokeTests.swift +++ b/swift/Tests/MoqTests/SmokeTests.swift @@ -37,12 +37,7 @@ final class SmokeTests: XCTestCase { /// generated API. No network needed: we just instantiate a few types and /// exercise the cancel path. func testClientConstructsAndCancels() async throws { - let client = Client() - try client.setTlsRoots([]) - try client.setTlsSystemRoots(true) - try client.setTlsFingerprints([]) - try client.setTlsCert(nil) - try client.setTlsKey(nil) + let client = try Client(tls: ClientTls(roots: [], systemRoots: true, fingerprints: [])) client.cancel() do { _ = try await client.connect(to: "https://localhost:0/test") @@ -61,19 +56,29 @@ final class SmokeTests: XCTestCase { } } + /// A value the native side cannot use throws from the constructor, not the dial. + func testInvalidConfigThrowsConfig() { + XCTAssertThrowsError(try Client(versions: ["moq-lite-99"])) { error in + guard case MoqError.Config = error else { + return XCTFail("expected Config, got \(error)") + } + } + XCTAssertThrowsError(try Server(bind: "not-an-address")) { error in + guard case MoqError.Config = error else { + return XCTFail("expected Config, got \(error)") + } + } + } + /// `cancel()` releases the listening socket before it returns, so the same /// address binds again with no retry. func testServerCloseReleasesPort() async throws { - let first = Server() - try first.bind("127.0.0.1:0") - try first.generateTls(hostnames: ["localhost"]) + let first = try Server(bind: "127.0.0.1:0", tls: ServerTls(generate: ["localhost"])) let addr = try await first.listen() first.cancel() // No retry: cancel() closed the socket, so this binds on the first try. - let second = Server() - try second.bind(addr) - try second.generateTls(hostnames: ["localhost"]) + let second = try Server(bind: addr, tls: ServerTls(generate: ["localhost"])) let rebound = try await second.listen() XCTAssertEqual(rebound, addr) second.cancel() @@ -150,7 +155,7 @@ final class SmokeTests: XCTestCase { func testBroadcastProducerOpensTracks() throws { let broadcast = try BroadcastProducer() let track = try broadcast.publishTrack(name: "events") - XCTAssertEqual(try track.name, "events") + XCTAssertEqual(try track.demand().name, "events") try track.finish() try broadcast.close() } @@ -164,21 +169,29 @@ final class SmokeTests: XCTestCase { func testVideoHintsReachMediaPublishApi() throws { let broadcast = try BroadcastProducer() - let hint = VideoHint( - coded: Dimensions(width: 1920, height: 1080), + let hint = Media.VideoHint( + coded: Media.Dimensions(width: 1920, height: 1080), bitrate: 4_000_000, framerate: 60, optimizeForLatency: true ) - let media = try broadcast.publishVideo(format: .avc3, hint: hint) + let media = try Media.TrackProducer.video(broadcast: broadcast, initData: Media.VideoInit(format: .avc3, data: Data(), hint: hint)) try media.finish() try broadcast.close() } func testVideoPropertiesUseDefaultedFields() throws { let broadcast = try BroadcastProducer() - try broadcast.setVideoProperties(VideoProperties(rotation: 315)) + try Media.CatalogProducer(broadcast: broadcast).setVideoProperties(Media.VideoProperties(rotation: 315)) + try broadcast.close() + } + + func testCatalogHandleClosesWithBroadcast() throws { + let broadcast = try BroadcastProducer() + let catalog = try Media.CatalogProducer(broadcast: broadcast) + try catalog.setSection(name: "app", json: "{\"value\":42}") try broadcast.close() + XCTAssertThrowsError(try catalog.removeSection(name: "app")) } func testBroadcastConsumerFetchesCachedGroup() async throws { @@ -208,9 +221,10 @@ final class SmokeTests: XCTestCase { } let broadcast = try BroadcastProducer() - let producer = try broadcast.publishJsonSnapshot(name: "status", of: Status.self, compression: true) - let consumer = try await broadcast.consume().subscribeJsonSnapshot( - name: "status", as: Status.self, compression: true) + let producer = try Json.SnapshotProducer( + broadcast: broadcast, track: try broadcast.publishTrack(name: "status"), compression: true) + let consumer = try Json.SnapshotConsumer( + track: try await broadcast.consume().subscribeTrack(name: "status"), compression: true) try producer.update(Status(state: "live", viewers: 42)) let first = try await consumer.next() @@ -232,9 +246,10 @@ final class SmokeTests: XCTestCase { } let broadcast = try BroadcastProducer() - let producer = try broadcast.publishJsonStream(name: "events", of: Event.self) - let consumer = try await broadcast.consume().subscribeJsonStream( - name: "events", as: Event.self) + let producer = try Json.StreamProducer( + broadcast: broadcast, track: try broadcast.publishTrack(name: "events")) + let consumer = try Json.StreamConsumer( + track: try await broadcast.consume().subscribeTrack(name: "events")) for n in 0..<3 { try producer.append(Event(n: n)) @@ -249,16 +264,20 @@ final class SmokeTests: XCTestCase { func testJsonProducersReportDemand() async throws { let broadcast = try BroadcastProducer() - let snapshot = try broadcast.publishJsonSnapshot(name: "status", of: [String: Int].self) - let stream = try broadcast.publishJsonStream(name: "events", of: [String: Int].self) + let snapshot = try Json.SnapshotProducer<[String: Int]>( + broadcast: broadcast, track: try broadcast.publishTrack(name: "status")) + let stream = try Json.StreamProducer<[String: Int]>( + broadcast: broadcast, track: try broadcast.publishTrack(name: "events")) let snapshotDemand = try snapshot.demand() let streamDemand = try stream.demand() XCTAssertEqual(snapshotDemand.name, "status") XCTAssertFalse(snapshotDemand.isUsed) let consumer = try broadcast.consume() - let snapshotConsumer = try await consumer.subscribeJsonSnapshot(name: "status", as: [String: Int].self) - let streamConsumer = try await consumer.subscribeJsonStream(name: "events", as: [String: Int].self) + let snapshotConsumer = try Json.SnapshotConsumer<[String: Int]>( + track: try await consumer.subscribeTrack(name: "status")) + let streamConsumer = try Json.StreamConsumer<[String: Int]>( + track: try await consumer.subscribeTrack(name: "events")) try await snapshotDemand.used() try await streamDemand.used() XCTAssertTrue(snapshotDemand.isUsed) @@ -378,7 +397,7 @@ final class SmokeTests: XCTestCase { } let consumer = try await origin.consume().requestBroadcast(path: "video-decode-frame") - let catalogs = try await consumer.subscribeCatalog() + let catalogs = try await Media.CatalogConsumer.subscribe(broadcast: consumer) // XCTUnwrap takes an autoclosure, which can't hold an await. let nextCatalog = try await catalogs.next() let catalog = try XCTUnwrap(nextCatalog) diff --git a/test/interop/clients/cpp/main.cpp b/test/interop/clients/cpp/main.cpp index d0c9208f573..4ff00cf8310 100644 --- a/test/interop/clients/cpp/main.cpp +++ b/test/interop/clients/cpp/main.cpp @@ -88,8 +88,9 @@ T until(moq::Future future, Clock::time_point deadline, const char *what) { // The session owns its connection; the client that made it can go. std::shared_ptr connect(const std::string &url, Clock::time_point deadline) { - auto client = moq::Client::init(); - ok(client->set_tls_verify(false), "set_tls_verify"); + moq::ClientConfig config; + config.tls.insecure = true; + auto client = ok(moq::Client::init(config), "client init"); return until(client->connect(url), deadline, "connect"); } @@ -120,7 +121,10 @@ int publish(const std::string &url, const std::string &path) { // Hold the producer for the lifetime of the publish loop; close() unpublishes. auto broadcast = ok(session->publish()->create_broadcast(path), "create_broadcast"); - auto media = ok(broadcast->publish_video_stream({moq::VideoFormat::kAvc3, {}}), "publish_video_stream"); + auto media = ok( + moq::MediaTrackStreamProducer::video(broadcast, moq::MediaTarget::kNamed{}, {moq::VideoFormat::kAvc3, {}}), + "video stream producer" + ); moq::AudioEncoderOutput output{moq::AudioCodec::opus()}; output.frame_duration_us = AUDIO_FRAME_DURATION_US; auto audio = ok( @@ -173,7 +177,7 @@ int subscribe(const std::string &url, const std::string &path, double timeout) { // The catalog is a live track. A lazy publisher (e.g. the browser, which only encodes on // demand) may announce video in a later update rather than the first snapshot, so wait // for a catalog that actually has a video track. - auto catalogs = until(consumer->subscribe_catalog(), deadline, "subscribe_catalog"); + auto catalogs = until(moq::MediaCatalogConsumer::subscribe(consumer), deadline, "catalog consumer"); std::optional catalog; while (!catalog || catalog->video.empty()) { catalog = until(catalogs->next(), deadline, "catalog"); @@ -185,7 +189,10 @@ int subscribe(const std::string &url, const std::string &path, double timeout) { moq::Subscription subscription; subscription.max_delay_us = MAX_DELAY_US; - auto media = until(consumer->subscribe_media(name, video.container, subscription), deadline, "subscribe_media"); + auto media = until( + moq::MediaContainerConsumer::subscribe(consumer, {name, video.container, subscription}), deadline, + "container consumer" + ); size_t total = 0; while (total == 0) { diff --git a/test/interop/clients/go/main.go b/test/interop/clients/go/main.go index 4095102d92a..17df78e2fc3 100644 --- a/test/interop/clients/go/main.go +++ b/test/interop/clients/go/main.go @@ -24,12 +24,13 @@ import ( "time" "moq.dev/moq" + moqmedia "moq.dev/moq/media" ) const readChunk = 64 * 1024 -// SubscribeMedia max delay: how much reordering the jitter buffer tolerates. -const maxDelayUs = 1_000_000 +// ContainerConsumer max delay: how much reordering the jitter buffer tolerates. +const maxDelay = time.Second // Synthetic audio: a 48 kHz mono tone, encoded as Opus. const ( @@ -86,7 +87,7 @@ func publish(ctx context.Context, url, broadcast string) error { } defer producer.Close() - media, err := producer.PublishVideoStream(moq.VideoFormatAvc3) + media, err := moqmedia.NewVideoTrackStreamProducer(producer, moqmedia.Named{}, moqmedia.VideoInit{Format: moqmedia.VideoFormatAvc3, Data: nil}) if err != nil { return err } @@ -148,8 +149,8 @@ func publish(ctx context.Context, url, broadcast string) error { // The catalog is a live track. A lazy publisher (e.g. the browser, which only // encodes on demand) may announce video in a later update rather than the first // snapshot, so wait for a catalog that actually has a video track. -func catalogWithVideo(ctx context.Context, consumer *moq.BroadcastConsumer) (*moq.Catalog, error) { - catalogs, err := consumer.SubscribeCatalog(ctx) +func catalogWithVideo(ctx context.Context, consumer *moq.BroadcastConsumer) (*moqmedia.Catalog, error) { + catalogs, err := moqmedia.NewCatalogConsumer(ctx, consumer) if err != nil { return nil, err } @@ -196,12 +197,12 @@ func subscribe(ctx context.Context, url, broadcast string, timeout time.Duration } var name string - var video moq.Video + var video moqmedia.Video for name, video = range catalog.Video { break } - media, err := consumer.SubscribeMedia(ctx, name, video.Container, &moq.Subscription{MaxDelayUs: maxDelayUs}) + media, err := moqmedia.NewContainerConsumer(ctx, consumer, moqmedia.ContainerConfig{Name: name, Container: video.Container, Subscription: &moq.Subscription{MaxDelay: maxDelay}}) if err != nil { return err } diff --git a/test/interop/clients/python/interop.py b/test/interop/clients/python/interop.py index edf2a05e018..3ef42d909dc 100644 --- a/test/interop/clients/python/interop.py +++ b/test/interop/clients/python/interop.py @@ -17,11 +17,12 @@ import math import struct import sys +from datetime import timedelta import moq READ_CHUNK = 64 * 1024 -MAX_DELAY_US = 1_000_000 # subscribe_media congestion-control / lookahead window +MAX_DELAY = timedelta(seconds=1) # container consumer congestion-control / lookahead window # Synthetic audio: a 48 kHz mono tone, encoded as Opus. AUDIO_TRACK = "tone" @@ -54,7 +55,9 @@ async def publish(url: str, broadcast: str) -> None: async with moq.Client(url, tls_verify=False) as client: # Hold the producer for the lifetime of the publish loop; finish() unpublishes. producer = client.create_broadcast(broadcast) - media = producer.publish_video_stream(moq.VideoFormat.AVC3) + media = moq.media.TrackStreamProducer.video( + producer, moq.media.VideoInit(format=moq.media.VideoFormat.AVC3, data=b"") + ) audio = producer.encode_audio( AUDIO_TRACK, moq.AudioEncoderInput(format=moq.AudioSampleFormat.F32, sample_rate=AUDIO_RATE, channels=1), @@ -85,11 +88,11 @@ async def publish(url: str, broadcast: str) -> None: media.finish() -async def _catalog_with_video(consumer: moq.BroadcastConsumer) -> moq.Catalog: +async def _catalog_with_video(consumer: moq.BroadcastConsumer) -> moq.media.Catalog: # The catalog is a live track. A lazy publisher (e.g. the browser, which only # encodes on demand) may announce video in a *later* update, not the first # snapshot, so wait for a catalog that actually has a video track. - catalog_consumer = await consumer.subscribe_catalog() + catalog_consumer = await moq.media.CatalogConsumer.subscribe(consumer) async for catalog in catalog_consumer: if catalog.video: return catalog @@ -104,8 +107,8 @@ async def subscribe(url: str, broadcast: str, timeout: float) -> None: track_name = next(iter(catalog.video)) video = catalog.video[track_name] - media = await consumer.subscribe_media( - track_name, video.container, moq.Subscription(max_delay_us=MAX_DELAY_US) + media = await moq.media.ContainerConsumer.subscribe( + consumer, track_name, video.container, subscription=moq.Subscription(max_delay=MAX_DELAY) ) total = 0