Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/uniffi-bindgen/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ runs:
case "$GENERATOR" in
cpp)
repo=https://github.com/kixelated/uniffi-bindgen-cpp
rev=a5a1ac411772b3f589859edf50aef7bc7538100b # v0.11.0-kixelated.7+v0.32.2
rev=6ea9f9bb764e2f9e753ff2bb623e899598df7960 # v0.11.0-kixelated.8+v0.32.2
;;
go)
repo=https://github.com/kixelated/uniffi-bindgen-go
Expand Down
130 changes: 127 additions & 3 deletions c/moq/test/probe.c
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,125 @@ static moq_connection_state *next_state(moq_session *session, moq_connection_sta
return next;
}

static void on_announce(void *user_data, moq_error *error, moq_announce_event *value)
{
on_value(user_data, error, value);
}

/* Over moq-lite-06, whose wire carries no instance, the session mints the announced one.
* Passed back as is, it pins request and follow; its text, parsed, names another instance. */
static void minted_instance(void)
{
moq_instance *empty = NULL;
moq_error *error = moq_instance_parse("", &empty);
CHECK(error && error->tag == MOQ_ERROR_INVALID_INSTANCE && !empty);
moq_error_free(error);

moq_origin_config origin_config = moq_origin_config_default();
moq_origin_producer *origin = moq_origin_producer_new(&origin_config);
moq_route_options route_options = moq_route_options_default();
moq_route *route = NULL;
ok(moq_origin_producer_route(origin, "live", &route_options, &route), "minted route");

const char *generate[] = {"localhost"};
const char *versions[] = {"moq-lite-06"};
moq_server_config server_config = moq_server_config_default();
server_config.bind = "127.0.0.1:0";
server_config.versions.data = versions;
server_config.versions.len = 1;
server_config.tls.generate.data = generate;
server_config.tls.generate.len = 1;
server_config.publish = origin;
moq_server *server = NULL;
ok(moq_server_new(&server_config, &server), "lite-06 server new");
result listening = {0};
moq_task *task = moq_server_listen(server, on_string, &listening);
char *addr = await(&listening, "lite-06 listen");
moq_task_free(task);

moq_client_config client_config = moq_client_config_default();
client_config.tls.insecure = true;
client_config.versions = server_config.versions;
client_config.implementation = MOQ_IMPLEMENTATION;
moq_client *client = NULL;
ok(moq_client_new(&client_config, &client), "lite-06 client new");
char url[128];
snprintf(url, sizeof(url), "https://%s", addr);
moq_string_free(addr);
result accepting = {0};
result connecting = {0};
moq_task *accept_task = moq_server_accept(server, on_request, &accepting);
moq_task *connect_task = moq_client_connect(client, url, on_session, &connecting);
moq_request *request = await(&accepting, "lite-06 accept");
CHECK(request);
result serving = {0};
moq_task *serve_task = moq_request_accept(request, NULL, NULL, on_session, &serving);
moq_session *served = await(&serving, "lite-06 request accept");
moq_session *session = await(&connecting, "lite-06 connect");
moq_task_free(accept_task);
moq_task_free(connect_task);
moq_task_free(serve_task);

moq_origin_consumer *consumer = moq_session_consume(session);
moq_announce_config announce_config = moq_announce_config_default();
moq_announce_consumer *announced = NULL;
ok(moq_origin_consumer_announced(consumer, &announce_config, &announced), "announced");
result next = {0};
task = moq_announce_consumer_next(announced, on_announce, &next);
moq_announce_event *event = await(&next, "announce event");
moq_task_free(task);
CHECK(event && event->tag == MOQ_ANNOUNCE_EVENT_PRESENT);
moq_instance *minted = event->present.announce.route.instance;
CHECK(moq_instance_is_minted(minted));

result requested = {0};
task = moq_origin_consumer_request(consumer, "live", minted, on_broadcast, &requested);
moq_broadcast_consumer_free(await(&requested, "request the minted instance"));
moq_task_free(task);
moq_following *following = NULL;
ok(moq_origin_consumer_follow(consumer, "live", minted, &following), "follow the minted instance");
result available = {0};
task = moq_following_available(following, on_broadcast, &available);
moq_broadcast_consumer *followed = await(&available, "available the minted instance");
CHECK(followed);
moq_broadcast_consumer_free(followed);
moq_task_free(task);

/* A handle's == compares pointers, so moq_instance_eq compares instances. */
char *text = moq_instance_text(minted);
moq_instance *named = NULL;
ok(moq_instance_parse(text, &named), "parse the minted text");
moq_string_free(text);
CHECK(!moq_instance_is_minted(named));
CHECK(!moq_instance_eq(named, minted) && moq_instance_ne(named, minted));
CHECK(moq_instance_eq(minted, minted));
result refused = {0};
task = moq_origin_consumer_request(consumer, "live", named, on_broadcast, &refused);
CHECK(pump(&refused.done, 5000));
CHECK(refused.error && refused.error->tag == MOQ_ERROR_PROTOCOL && !refused.value);
moq_error_free(refused.error);
moq_task_free(task);
moq_instance_free(named);
moq_announce_event_free(event);

moq_following_cancel(following);
moq_announce_consumer_cancel(announced);
moq_session_cancel(session, 0);
moq_session_cancel(served, 0);
moq_server_cancel(server);
moq_route_cancel(route);
moq_following_free(following);
moq_announce_consumer_free(announced);
moq_origin_consumer_free(consumer);
moq_session_free(session);
moq_session_free(served);
moq_request_free(request);
moq_client_free(client);
moq_server_free(server);
moq_route_free(route);
moq_origin_producer_free(origin);
}

int main(void)
{
#ifdef _WIN32
Expand All @@ -274,6 +393,8 @@ int main(void)
moq_subscription bare = moq_subscription_default();
CHECK(!bare.delay.max.has_value);

minted_instance();

/* A synchronous error is a returned value with Rust's Display message. */
moq_server_config unbound_config = moq_server_config_default();
moq_server *unbound = NULL;
Expand All @@ -294,9 +415,12 @@ int main(void)
moq_route *route = NULL;
ok(moq_origin_producer_route(origin, "probe", &route_options, &route), "route");
moq_broadcast_producer *broadcast = moq_route_broadcast(route);
char *instance = moq_route_instance_id(route);
CHECK(instance && instance[0] != '\0');
moq_string_free(instance);
moq_instance *instance = moq_route_instance_id(route);
CHECK(instance && !moq_instance_is_minted(instance));
char *text = moq_instance_text(instance);
CHECK(text && text[0] != '\0');
moq_string_free(text);
moq_instance_free(instance);
moq_track_info info = moq_track_info_default();
info.timescale.has_value = true;
info.timescale.value = 1000000;
Expand Down
3 changes: 2 additions & 1 deletion cpp/moq/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The generator is a fork, [kixelated/uniffi-bindgen-cpp](https://github.com/kixel
```bash
cargo install uniffi-bindgen-cpp --locked \
--git https://github.com/kixelated/uniffi-bindgen-cpp \
--tag v0.11.0-kixelated.7+v0.32.2
--tag v0.11.0-kixelated.8+v0.32.2
```

`flake.nix` pins the same tag and lists every other place that names it.
Expand All @@ -31,6 +31,7 @@ Every generated type lives in `namespace moq`. Objects are `std::shared_ptr`; re
- A fallible call returns `moq::expected<T>`, the bundled `tl::expected` at every standard, so bindings built at C++17 link with code built at C++23. With exceptions off, `value()` on an error aborts with a message.
- An async call returns `moq::Future<T>`. Block on it with `get()` or `wait_for()`, attach a continuation with `std::move(future).then(executor, callback)`, which returns a `moq::Continuation`, or `co_await` it on C++20.
- `moq::Error` is a value holding a `std::variant` of its cases, and `to_string()` gives Rust's `Display` message. Nothing throws. A Rust panic inside a call aborts with a message on stderr, and so does reading an invalid future (see [Cancellation](#cancellation)). A panic in owned background work reaches the next call on that object as a task error; other objects keep running.
- An object Rust compares, hashes, or prints, such as `moq::Instance`, gets `eq()`, `ne()`, `hash()`, and `to_string()`. `==` on its `std::shared_ptr` compares handles, not values.
- Futures are polled on one process-wide executor thread unless the application installs its own with `moq::set_executor` before the first async call. The executor passed to `then()` dispatches only the continuation. `moq::shutdown()` stops the moq-ffi runtime workers (`moq_ffi_shutdown`), then the executor, before unloading the code a continuation could call into.

## Cancellation
Expand Down
64 changes: 62 additions & 2 deletions cpp/moq/test/probe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,65 @@ void audio_roundtrip()
ok(broadcast->close(), "close audio broadcast");
}

// Over moq-lite-06, whose wire carries no instance, the session mints the announced one.
// Passed back as is, it pins request and follow; its text, parsed, names another instance.
void minted_instance()
{
CHECK(std::holds_alternative<moq::Error::kInvalidInstance>(moq::Instance::parse("").error().get_variant()));

auto origin = moq::OriginProducer::init({});
auto route = ok(origin->route("live", {}), "minted route");

moq::ServerConfig server_config;
server_config.bind = "127.0.0.1:0";
server_config.versions = {"moq-lite-06"};
server_config.tls.generate = {"localhost"};
server_config.publish = origin;
auto server = ok(moq::Server::init(server_config), "lite-06 server init");
auto addr = ok(server->listen().get(), "lite-06 listen");

moq::ClientConfig client_config;
client_config.tls.insecure = true;
client_config.versions = server_config.versions;
client_config.implementation = moq::IMPLEMENTATION;
auto client = ok(moq::Client::init(client_config), "lite-06 client init");
auto accepting = server->accept();
auto connecting = client->connect("https://" + addr);
auto request = ok(accepting.get(), "lite-06 accept");
CHECK(request != nullptr);
auto served = ok(request->accept(nullptr, nullptr).get(), "lite-06 request accept");
auto session = ok(connecting.get(), "lite-06 connect");

auto consumer = session->consume();
auto announced = ok(consumer->announced({}), "announced");
auto event = ok(announced->next().get(), "announce event");
CHECK(event);
const auto *present = std::get_if<moq::AnnounceEvent::kPresent>(&event->get_variant());
CHECK(present != nullptr);
auto minted = present->announce.route.instance;
CHECK(minted->is_minted());

ok(consumer->request("live", minted).get(), "request the minted instance");
auto following = ok(consumer->follow("live", minted), "follow the minted instance");
CHECK(ok(following->available().get(), "available the minted instance"));

// A shared_ptr's == compares handles, so the generated eq() compares instances.
auto named = ok(moq::Instance::parse(minted->text()), "parse the minted text");
CHECK(!named->is_minted());
CHECK(named->to_string() == minted->to_string());
CHECK(!named->eq(minted));
auto refused = consumer->request("live", named).get();
CHECK(!refused);
CHECK(std::holds_alternative<moq::Error::kProtocol>(refused.error().get_variant()));

following->cancel();
announced->cancel();
session->cancel(0);
served->cancel(0);
server->cancel();
route->cancel();
}

} // namespace

int main()
Expand All @@ -264,6 +323,7 @@ int main()
datagram_tracks();
subscription_defaults();
audio_roundtrip();
minted_instance();
auto timebase_owner = ok(moq::BroadcastProducer::init(), "timebase owner");
auto timebase_other = ok(moq::BroadcastProducer::init(), "timebase other");
auto timebase = ok(moq::MediaTimebase::init(timebase_owner), "timebase");
Expand Down Expand Up @@ -299,7 +359,7 @@ int main()
moq::RouteOptions hidden;
hidden.hidden = true;
auto route = ok(origin->route("probe", hidden), "route");
CHECK(!route->instance_id().empty());
CHECK(route->instance_id() && !route->instance_id()->is_minted());
auto broadcast = route->broadcast();
moq::TrackInfo info;
info.timescale = 1000000;
Expand Down Expand Up @@ -339,7 +399,7 @@ int main()
auto session = ok(connecting.get(), "connect");

// Subscriber: resolve the announced broadcast and subscribe to its track.
auto announced = ok(session->consume()->follow("probe", std::nullopt), "follow");
auto announced = ok(session->consume()->follow("probe", nullptr), "follow");
// The first available() is never offline.
auto remote = ok(announced->available().get(), "available");
CHECK(remote);
Expand Down
2 changes: 1 addition & 1 deletion cpp/obs/src/moq-output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ bool MoQOutput::Start()
{
std::lock_guard<std::mutex> lock(mutex);
url = server;
instance = (*next_route)->instance_id();
instance = (*next_route)->instance_id()->text();
}
{
std::lock_guard<std::mutex> lock(media_mutex);
Expand Down
2 changes: 1 addition & 1 deletion cpp/obs/src/moq-source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ static void moq_source_on_connect(struct moq_source *ctx, const std::shared_ptr<
// Wait for the broadcast to be announced. Announcements arrive over the session
// after it connects, so resolving against only what is announced *now* would race
// them and blank the source for a broadcast that is live.
auto announced = conn->session->consume()->follow(conn->broadcast, std::nullopt);
auto announced = conn->session->consume()->follow(conn->broadcast, nullptr);
if (!announced) {
LOG_ERROR("Failed to request broadcast '%s': %s", conn->broadcast.c_str(),
announced.error().to_string().c_str());
Expand Down
2 changes: 1 addition & 1 deletion cpp/obs/test/moq-audio-encoder-test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ void CheckSubscriberDecodes(const char *preset, size_t channels)
init.data = encoder.ExtraData();
auto track = TestOk(moq::MediaTrackProducer::audio(broadcast, moq::MediaTarget::kNamed{}, init), "audio track");

auto announced = TestOk(origin->consume()->follow("room", std::nullopt), "follow");
auto announced = TestOk(origin->consume()->follow("room", nullptr), "follow");
auto consumer = TestOk(announced->available().get(), "available");
auto catalogs = TestOk(moq::MediaCatalogConsumer::subscribe(consumer).get(), "catalog consumer");
std::optional<moq::Catalog> catalog;
Expand Down
4 changes: 2 additions & 2 deletions cpp/obs/test/moq-output-test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ encoder_packet audioPacket(std::vector<uint8_t> &payload, int64_t pts)
// The output's broadcast as the relay serves it, or null once the wait times out.
std::shared_ptr<moq::BroadcastConsumer> relayBroadcast(TestRelay &relay)
{
auto announced = TestOk(relay.origin->consume()->follow("room", std::nullopt), "follow");
auto announced = TestOk(relay.origin->consume()->follow("room", nullptr), "follow");
auto available = announced->available();
if (available.wait_for(std::chrono::seconds(10)) != std::future_status::ready)
return nullptr;
Expand Down Expand Up @@ -411,7 +411,7 @@ struct MoQOutputStatsTest {
const auto *present = std::get_if<moq::AnnounceEvent::kPresent>(&event->get_variant());
if (!present)
return std::nullopt;
return present->announce.route.instance;
return present->announce.route.instance->text();
}

static std::shared_ptr<moq::Session> Session(MoQOutput &output)
Expand Down
2 changes: 1 addition & 1 deletion cpp/obs/test/moq-source-test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1585,7 +1585,7 @@ int main()
// A different stream than the one the rendition pins, hidden until announced.
moq::RouteOptions options;
options.hidden = true;
options.instance = "obs-test-other";
options.instance = TestOk(moq::Instance::parse("obs-test-other"), "instance");
hidden = TestOk(relay.origin->route("obs/hidden", options), "unannounced sibling");
config.broadcast = "./hidden";
config.instance = "obs-test-pinned";
Expand Down
8 changes: 7 additions & 1 deletion dart/moq/lib/src/aliases.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ typedef OriginConsumer = MoqOriginConsumer;
/// A requested broadcast not yet answered: serve a held broadcast, route a new one, or reject it.
typedef BroadcastRequest = MoqBroadcastRequest;

/// How a route advertises itself: hidden until announced, its static cost, and a stable instance.
/// How a route advertises itself: hidden until announced, its static cost, and a stable [Instance].
typedef RouteOptions = MoqRouteOptions;

/// A stream of announce events under a prefix.
Expand All @@ -70,6 +70,12 @@ typedef Following = MoqFollowing;
/// The read-only facts on a received route: a static cost and the stream's instance.
typedef RouteInfo = MoqRouteInfo;

/// One continuous stream at a path: `Instance.parse` names one, and only a session mints one.
///
/// Two are equal only when their text and minted flag both match, so pass an
/// instance a route reported back as is, rather than re-parsing its text.
typedef Instance = MoqInstance;

/// A route over a prefix: its origin-relative path, required wildcard captures (empty without wildcards), and route metadata.
typedef Announce = MoqAnnounce;

Expand Down
24 changes: 13 additions & 11 deletions dart/moq/lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -234,14 +234,14 @@ final class Moq {

/// Follow the broadcast at exactly [path]: each `available()` resolves the
/// next instance serving it, such as a restarted publisher, or null once the
/// path goes offline. Pass an [instance] to follow only that stream; another
/// one taking over also resolves null.
Following follow(String path, {String? instance}) =>
/// path goes offline. Pass an [instance] a route reported to follow only that
/// stream; another one taking over also resolves null.
Following follow(String path, {Instance? instance}) =>
session.consume().follow(path: path, instance: instance);

/// Resolve one publisher instance of the broadcast at [path]. Pass an
/// [instance] to refuse any other stream.
Future<BroadcastConsumer> request(String path, {String? instance}) =>
/// [instance] a route reported to refuse any other stream.
Future<BroadcastConsumer> request(String path, {Instance? instance}) =>
session.consume().request(path: path, instance: instance);

/// The session's bandwidth allocator.
Expand Down Expand Up @@ -304,18 +304,20 @@ extension GrantConsumerUpdates on GrantConsumer {

/// A route's instance under the name the other bindings use.
extension RouteInstance on MoqRoute {
/// The stream this route serves now. A named instance never changes. A job's
/// route takes its claim's current one when it first announces or when this
/// first reads it, so a hidden job route returns it before announcing, and
/// [MoqRoute.announce] fails with `Unroutable` once the claim has moved past it.
String instance() => instanceId();
/// The stream this route serves now, or once cancelled, the last one it
/// reported or announced under (null if it did neither). A named instance
/// never changes. A job's route takes its claim's current one when it first
/// announces or when this first reads it, so a hidden job route returns it
/// before announcing, and [MoqRoute.announce] fails with `Unroutable` once
/// the claim has moved past it.
Instance? instance() => instanceId();
}

/// A claim's instance under the name the other bindings use.
extension ClaimInstance on MoqClaim {
/// The stream this claim serves now. A named instance never changes. Unnamed,
/// the claim moves to a fresh one each time a job holding its current one ends.
String instance() => instanceId();
Instance instance() => instanceId();
}

/// A [Stream] view over an announcement cursor.
Expand Down
Loading
Loading