Loading...
Searching...
No Matches
Native support for Media Over QUIC (MoQ)

As explained in the imquic public API, imquic_create_server and imquic_create_client are the methods you can use to create a new, generic, QUIC server or client, with the related callbacks to be notified about what happens on new or existing connections. That API assumes you'll be entirely responsible of the application level protocol details, though.

Out of the box, imquic provides native support for a few specific protocols, meaning it can deal with the lower level details of the application level protocol, while exposing a simpler and higher level API to use the protocol features programmatically. Medias Over QUIC is one of those protocols.

When you want to use the native MoQ features of imquic, you must not use the generic functions and callbacks, but will need to use the MoQ variants defined in this page instead. Specifically, to create a MoQ server you won't use imquic_create_server, but will use imquic_create_moq_server instead; likewise, a imquic_create_moq_client variant exists for creating MoQ clients too.

It's important to point out, though, that in MoQ there's a clear distinction between the QUIC role (client or server) and the MoQ role (publisher, subscriber, relay). The above mentioned methods specify the QUIC role, while the MoQ role is configured reacting to one of the specific MoQ callbacks in the library.

Speaking of callbacks, considering the library needs to take care of the MoQ protocol internally, attempting to use the generic callback setters on a MoQ endpoint will do nothing, and show an error on the logs: you'll need to use the MoQ specific callbacks, in order to let the library do its job, and expose higher level functionality via API instead. This means that, for instance, to be notified about a new MoQ connection (whether you're a client or a server), you'll use imquic_set_new_moq_connection_cb, while to be notified about connections being closed you'll need to use imquic_set_moq_connection_gone_cb instead. The same considerations made on reference-counting connections in generic callbacks applies here too, since the same structs are used for endpoints and connections: it's just the internals that are different. Starting the endpoint after configuring the callbacks, instead, works exactly the same way as in the generic API, meaning you'll be able to use imquic_start_endpoint.

What's important to point out is that being notified about a new MoQ connection won't make it usable right away: it's just an indication that a QUIC or WebTransport connection was successfully established, but no MoQ message has been exchanged yet. Most importantly, that callback precedes the MoQ handshake/setup, which means that's the perfect place to specify the MoQ role and version of your endpoint, which you can do with a call to imquic_moq_set_role and imquic_moq_set_version respectively. As soon as you return from the callback function, in case the application is a client the internal MoQ stack in imquic will perform the MoQ setup accordingly; for servers it will wait for a connection from a client to do so.

To be notified when the MoQ session has been established, and MoQ messages can finally be exchanged, you can use the imquic_set_moq_ready_cb callback setter. As soon as that callback fires, you'll be able to start exchanging messages, e.g., to subscribe to a namespace or publish one.

Depending on the MoQ role, you can configure different callbacks and use different methods to send MoQ requests of your own. For instance, a relay may want to be aware of incomming ANNOUNCE requests, but a publisher won't care; at the same time, a publisher will need to be aware of incoming SUBSCRIBE requests, but a subscriber won't need/care about those. The following sections will cover the different callbacks and methods you can use as a publisher and a subscriber, taking into account that a relay is basically a mix of the two, which means it will probably implement a mix of both, if not all of them, depending on what it's configured to do.

Namespace tuples and track names

Without delving too much into the details of the specification (please refer to the draft for that), key identifiers in MoQ are the namespace a publisher can advertise, and the track names that publishers may publish within the context of those namespaces.

A namespace is formatted as a tuple, where this tuple will have a hierarchical meaning: a tuple of Italy / Meetecho / Lorenzo, for instance, will be made of three different namespace blocks (Italy, Meetecho, Lorenzo), which give context to the whole tuple (Lorenzo is part of Meetecho, Meetecho is part of Italy). This is particularly important because it can give context to applications as well, and to requests like SUBSCRIBE_ANNOUNCES : if we imagine a chat application, for instance, we may have a tuple like moq-chat / 1234 / Lorenzo to indicate that Lorenzo is in room 1234 of application moq-chat, which means that anyone sending a SUBSCRIBE_ANNOUNCES for the partial tuple moq-chat / 1234 will be notified when Lorenzo announces and unannounces their presence in that context.

In the imquic MoQ integration, this is made possible using the imquic_moq_namespace structure, where each instance identifies a specific namespace that can be part of a linked list implementing the actual tuple: building a tuple is a matter of adding multiple imquic_moq_namespace instances, and using the next pointer to point to the next namespace in the tuple. The demos provide examples of how this can be used. Of course, a namespace can be built as a tuple of 1, with no further part set in the next pointer.

Track names are simpler to address, instead, and can be addressed using the imquic_moq_name structure.

Both namespaces and tuples are formatted in the respective structures as array of bytes of a specific length: while most of the times they'll be strings for the sake of simplicity (which is what we do in our demos too, for instance), the specification doesn't make that assumption, and so the library doesn't either.

MoQ Publishers

In MoQ, a publisher is an endpoint that announces a specific namespace, and reacts to incoming subscriptions to specific tracks of that namespace by sending objects, possibly using different multiplexing/forwarding modes. This means that, in principle, it will need to be able to send ANNOUNCE and UNANNOUNCE requests, be notified about the result of those requests and incoming subscriptions, send responses to subscriptions and send objects.

In order to be advertised as a publisher, the role in MoQ must be set accordingly. This means passing IMQUIC_MOQ_PUBLISHER to the imquic_moq_set_role function, e.g., when the connection is available:

        static void imquic_demo_new_connection(imquic_connection *conn, void *user_data) {
                imquic_connection_ref(conn);
                IMQUIC_LOG(IMQUIC_LOG_INFO, "[%s] New MoQ connection\n", imquic_get_connection_name(conn));
                imquic_moq_set_role(conn, IMQUIC_MOQ_PUBLISHER);
        }

You can also specificy a version using the imquic_moq_set_role function in the same place. By default, the MoQ stack will set the version to IMQUIC_MOQ_VERSION_ANY , which means that for clients it will offer all supported versions higher than v06, while for servers it will accept the first offered among the supported ones (still if higher than v06); a "legacy" version called IMQUIC_MOQ_VERSION_ANY_LEGACY is available, to negotiate any supported version that is lower than v06. At the time of writing, this stack supports MoQ versions from v03 ( IMQUIC_MOQ_VERSION_03 ) up to v07 ( IMQUIC_MOQ_VERSION_07 ), but not all versions will be supported forever. It should also be pointed out that not all features of all versions are currently supported, so there may be some missing functionality depending on which version you decide to negotiate. The IMQUIC_MOQ_VERSION_MIN and IMQUIC_MOQ_VERSION_MAX defines can be used to programmatically check the minimum and maximum supported versions.

Speaking of callbacks (since those must be configured before starting the imquic endpoint), there are two different callbacks that need to be configured to know if an ANNOUNCE request was successful:

Both callbacks will reference a imquic_moq_namespace object with info on the namespace they're referring to (which will typically be the same one sent in a previous ANNOUNCE request).

To deal with incoming subscriptions, instead, you can configure a callback for intercepting incoming SUBSCRIBE requests via imquic_set_incoming_subscribe_cb, and another for intercepting an UNSUBSCRIBE via imquic_set_incoming_unsubscribe_cb. In both cases, the publisher is supposed to answer with either a success or an error. FETCH subscriptions can be tracked using imquic_set_incoming_fetch_cb, while a FETCH_CANCEL can be intercepted via imquic_set_incoming_fetch_cancel_cb.

That said, once callbacks have been configured, the endpoint started, and the publisher role set, a publisher can start sending requests. To announce a new namespace they'll be responsible for, they can use imquic_moq_announce; the imquic_moq_unannounce request, instead, notifies the peer (e.g., a relay), that the publisher is not serving that namespace anymore. Both will reference the namespace in a imquic_moq_namespace property.

When a subscriber decides to subscribe to a track in the publisher's namespace, and the publisher is notified via the previously set callback, they can decide to either accept it (and so start serving objects) or reject it (e.g., because the track doesn't exist, or because the provided authentication info is incorrect). Accepting a subscription can be done via imquic_moq_accept_subscribe, while it can be rejected with imquic_moq_reject_subscribe. While an incoming subscribe will include more info to address a specific resource (most importantly the namespace in imquic_moq_namespace and the track name in imquic_moq_name), a subscribe_id integer will act as a "shortcut" to address that specific subscription, both in upcoming events (e.g., when notified about a will to unsubscribe) and when sending responses or delivering objects.

Sending objects can be done with a call to imquic_moq_send_object. At the time of writing, MoQ comes with different multiplexing modes for sending an object (e.g., DATAGRAM , a STREAM per object, a STREAM per group or a STREAM per track). The imquic_moq_send_object request hides the specifics of how the object is serialized over the wire, as it just expects the imquic_moq_delivery mode as one of the properties set on the object to send. Besides the delivery mode, the object itself will need to be filled with the relevant MoQ object data (e.g., subscribe_id, object_id, group_id, etc.), as that info will be serialized accordingly, depending on the multiplexing mode. Depending on the negotiated version, only a subset of the properties may actually be serialized on the wire, while others may be dropped.

MoQ Subscribers

In MoQ, a subscriber is an endpoint that subscribes to one or more tracks within a namespace, for the purpose of receiving objects from a publisher, often via a relay. This means that, assuming it knows using out-of-band mechanisms which namespaces and tracks are available, it should be able to send SUBSCRIBE and UNSUBSCRIBE requests (and/or FETCH and FETCH_CANCEL for retrieving past objects), be prepared to receive responses to those requests, and have a way of receiving MoQ objects to consume. It can also ask a relay to be notified when specific namespaces (or namespaces prefixed by a specific tuple) become available, via SUBSCRIBE_ANNOUNCES and UNSUBSCRIBE_ANNOUNCES requests.

In order to be advertised as a subscriber, the role in MoQ should be set accordingly. This means passing IMQUIC_MOQ_SUBSCRIBER to the imquic_moq_set_role function, e.g., when the connection is available:

        static void imquic_demo_new_connection(imquic_connection *conn, void *user_data) {
                imquic_connection_ref(conn);
                IMQUIC_LOG(IMQUIC_LOG_INFO, "[%s] New MoQ connection\n", imquic_get_connection_name(conn));
                imquic_moq_set_role(conn, IMQUIC_MOQ_SUBSCRIBER);
        }

Speaking of callbacks (since those must be configured before starting the imquic endpoint), there are three different callbacks that can be configured to know the outcome of, or the progression of, a SUBSCRIBE request:

  • imquic_set_subscribe_accepted_cb configures the callback to be notified about the request being successful;
  • imquic_set_subscribe_error_cb configures the callback to be notified about the request being rejected (e.g., because the track doesn't exist, or the provided athentication info was incorrect);
  • imquic_set_subscribe_done_cb configures the callback to be notified about a subscription being completed (currently unused, as also the subjects of discussions within the MoQ standardization efforts).

All those callbacks refer to the subscribe_id identifier that was previously mapped to that subscription (more on that later).

Similar callbacks are available for when a SUBSCRIBE_ANNOUNCES has been sent, namely imquic_set_subscribe_announces_accepted_cb, imquic_set_subscribe_announces_error_cb. It's worth pointing out that if a subscriber expressed interest in getting info on ANNOUNCE requests related to specific tuple namespaces, it should also configure the imquic_set_incoming_announce_cb and imquic_set_incoming_unannounce_cb we introduced in the publisher section before.

The outcome of FETCH requests can be intercepted via the following callbacks instead:

  • imquic_set_fetch_accepted_cb configures the callback to be notified about the request being successful;
  • imquic_set_fetch_error_cb configures the callback to be notified about the request being rejected (e.g., because the track doesn't exist, or the provided athentication info was incorrect).

When successfully subscribed to something, a subscriber may receive MoQ objects as part of that subscription. Independently of how that object was multiplexed (something we discussed when introducing MoQ Publishers), a specific callback can be configured to be notified about incoming objects, using imquic_set_incoming_object_cb. Any time a new object is available, that callback function will be invoked, where all the relevant info (identifiers and payload) will be made available in a imquic_moq_object instance. The delivery property will specify how the object was delivered, in case that's important.

Coming to active requests, to issue a SUBSCRIBE request the subscriber must provided the information to uniquely address the resource they're interested in (the namespace via imquic_moq_namespace, the track name via imquic_moq_name and, if needed, the authentication info via imquic_moq_auth_info), but at the same time they should also provide unique subscribe_id and track_alias numeric identifiers to act as shortcuts to address that subscription in subsequent responses, events and incoming objects. Subscribing can be done with a call to the imquic_moq_subscribe function, while to unsubscribe the corresponding imquic_moq_unsubscribe function can be used instead.

Issuing FETCH related requests is similar, as imquic_moq_fetch allows you to try and fetch some objects, while imquic_moq_cancel_fetch is what you use to stop the delivery and cancel the request. Just as with SUBSCRIBE requests, a subscribe_id identifier is used to address a specific FETCH context.

MoQ Relays

In MoQ, a relay is an endpoint that can act as both a publisher and a subscriber at the same time. It can act as an intermediary for subscription requests on behalf of a publisher, for instance: to make a simple example, a publisher may announce a specific namespace to a relay, which will then make it available to interested subscribers (or to other relays). If multiple subscribers are interested in the same content, the relay may only send a single SUBSCRIBE request back to the publisher, and then selectively forward the same objects it will receive to all interested subscribers, potentially caching them as well. Keeping track of announced namespaces, a relay usually also advertizes their presence to subscribers that sent a SUBSCRIBE_ANNOUNCES request matching the tuple.

In a nutshell, this means that, from a functional perspective, a relay will need to be able to act both as a publisher and a subscriber at the same time, which means it will probably implement the callbacks of both, and use the requests of both as well (e.g., to relay objects it received on one connection to other connections). As such, you can refer to the documentation provided in the previous two sections for more details.

What's important to point out, though, is that in order to be advertised as relay, the role in MoQ should be set accordingly. This means passing IMQUIC_MOQ_PUBSUB to the imquic_moq_set_role function, e.g., when the connection is available:

        static void imquic_demo_new_connection(imquic_connection *conn, void *user_data) {
                imquic_connection_ref(conn);
                IMQUIC_LOG(IMQUIC_LOG_INFO, "[%s] New MoQ connection\n", imquic_get_connection_name(conn));
                imquic_moq_set_role(conn, IMQUIC_MOQ_PUBSUB);
        }