These past few months I’ve shared a lot of info on my experiments with QUIC as a new transport for real-time media. I detailed my efforts on writing a basic QUIC stack, followed by deeper dives in RTP Over QUIC (RoQ) and, most importantly, Media Over QUIC (MoQT), where I tried to sketch and prototype a potential interoperability between QUIC and WebRTC as well. I eventually ended up making a presentation on all this and more at RTC.On.
In all of those blog posts (and in the presentation) I mentioned a library I was working on to prototype these new protocols, but so far that library hadn’t been made available yet. That changes now, so I’m happy to introduce imquic, our experimental QUIC stack with native RoQ/MoQ support!
As a side note, I’ll also present this at the upcoming FOSDEM 2025 in Brussels, so if this is of interest to you, please do pass by and say hello.
A new QUIC library? Why?
That’s a fair question, and one I briefly discussed in my first blog post too. QUIC is a popular and, at the same time, complex protocol, that requires good knowledge of different levels of networking, since it mixes things that are usually handled at different layers (e.g., cryptography, flow and congestion control, etc.). There are many very good open source implementations out there, like ngtcp2, picoquic, lsquic, aioquic, quiche (two of them!), quic-go and many more. Considering there are libraries in pretty much every programming language, all developers interested in QUIC as a technology probably have an option they can safely rely upon, often in ways that in part hide most of the compexity of the protocol itself.
That said, as I explained in my introductory blog post, I knew nothing about QUIC when I started a few months ago, and while picking an existing library could have been an option for me too, I learn best when I get my hands “dirty”. Learning was indeed my first objective, at the time, which meant I started hacking together a basic QUIC stack while I was studying the protocol: this allowed me to more easily familiarize with the basics of QUIC and the different concepts that make it special.
Long story short, I ended up with a basic QUIC stack that supported both raw QUIC and WebTransport, and so with something I could use as a basis for more “fun” stuff, like indeed RoQ and MoQT. More or less organically, I ended up structuring the code as a library with a high level API, and so something I could see myself using in the future to build something cool.
Whether this will actually be the case, or we’ll just use this as a playground to then move to a more established QUIC library in the future, I don’t know. I do know that this is the library I’m using every day to continue my QUIC journey and my exploration of these new real-time media technologies, and that I wanted to share this as an open source library to allow other people to do the same. Hopefully, with the help of other interested companies and individuals, we’ll be able to turn imquic into something more than the “toy” it arguably is today.
Where does the name come from?
I’m glad you asked! (well, you didn’t, but for the sake of theatrics let’s pretend you did…)
Of course, it’s common practice for every QUIC-related implementation to somehow include the word “QUIC” in the project name, and possibly make some pun around it (whether it’s about being “quick” or something else). For instance, good ol’ Saúl said that the perfect name would have been NesQUIC, like the milky chocolate beverage. While it’s indeed a fun name, the last thing I wanted was a mail from Nestle’s lawyers, so I thought of something else instead.
Making a step back, as those that have known me from some time will be well aware of, one of my favourite bands of all time is Iron Maiden, and one of my favourite songs of them is called Be Quick or Be Dead. I like that song so much that I used its video in a ton of demos I did, like this old Dangerous Demo from the very first Kamailio World I ever attended, in late 2016…
And when I talked about QUIC being one of the most interesting protocols to look at, at the first JanusCon 5 years ago as well as the one we hosted in Naples a few months ago, the cover of the single was the picture I used to highlight the fact we’d better be quick about it too!
As such, I knew I wanted all that history to be part of the library somehow. I briefly considered BQOBD as the name of library, until I realized there would be no way in hell people would, 1. remember it, and 2. be able to pronounce it without losing a tooth. I needed something catchier and simpler.
And this is basically how I eventually landed on “imquic”: yes, you can definitely read it as “I am quick” (that’s the QUIC pun for you there!), but the “im” part also represents the initials of Iron Maiden, making it the “Iron Maiden (be) QUIC (or be dead) library”
Come on, isn’t this cool? Isn’t it?!
A first look at imquic
You’re probably regretting not skipping the previous section, so let’s get back to being serious again, and let’s have a first quick glance at the library, as in how it was structured in this first incarnation.
The whole codebase, which is licensed under the terms of the MIT License, was written in C, just as Janus. This was a conscious choice considering our expertise with the language, especially after 10 years of work on Janus itself. Its only dependencies are GLib 2.0 (mostly for its helper resources and its compatibility layers, which we used a lot in Janus too) and quic-tls (for the mandatory cryptography features in QUIC). In case you’re wondering why we chose quic-tls rather than OpenSSL, the reason is that QUIC “embeds” TLS and so uses it differently from other transport protocols: unfortunately OpenSSL chose not to provide a QUIC API like BoringSSL originally did, but decided to implement a whole QUIC stack of their own inside OpenSSL. Daniel Stenberg provides more details on this with some additional context in an excellent blog post, if you’re curious. Akamai and Microsoft, among others, disagreed with that decision, and so they forked OpenSSL as quic-tls, to add the BoringSSL QUIC API to it. While I started using BoringSSL myself for the library, I soon switched to quic-tls, for no other reason that it was easier for me to use and link to. In theory, you should still be able to use BoringSSL with imquic, if you want, but in practice I haven’t tested that in a while. It may be interesting to explore NGINX’s approach to an OpenSSL compatibility layer as well, in the future (as that would allow us to still rely on OpenSSL, which remains very widespread), but that can definitely wait.
That said, the code is available as an open source repo on GitHub, where you can also find instructions on how to build it and, optionally, install it. At the time of writing, Linux is the only “official” target (since that’s what I use myself), but since imquic is a library, we of course also plan to make it available for other systems as well: macOS and Windows for sure, but maybe mobile and other platforms as well, in the future. We’ll probably need some help for that, so in case you feel you’re up to the task, please don’t hesitate to contribute to the project!
From an architecture point of view, the library is relatively simple, since its core is based on an integrated event loop (at the moment powered by a single thread; this will definitely change in the future) that feeds different features. When you create a QUIC client or server, a network endpoint is spawned with a few specific settings, and is mapped to a TLS stack for cryptography and polled by the loop. Dedicated resources have then been written to deal with the concepts of connections, streams, buffers and, of course, a QUIC stack to parse, craft and serialize messages.
This functionality is exposed to end users via a high level API. This means that, as an application leveraging the library, you typically do the following:
- you initialize the library (only once, in case your application will then spawn multiple clients and/or servers before shutting down);
- you create a QUIC server or client, providing a few key settings;
- you configure callbacks to be notified about specific events;
- you start the endpoint (e.g., start listening for servers, attempt a connection for clients);
- once connected, you can interact with the connection somehow.
The initialization, as explained above, only needs to happen once, and the only argument it requires is an optional path to an SSLKEYLOG file. When provided, the library will log the connection secrets there, which is very helpful any time you need to debug the contents of QUIC connection exchanges with, e.g., Wireshark, either in real-time or after the fact.
Initializing the library also spawns the above mentioned event loop: at the moment, this means spawning a single thread that a GLib context/loop will then be running upon. This loop will then be used to monitor network sockets (for both clients and servers handled by the library), but also to handle the life cycle of new and established connections (e.g., to schedule delivery of data, or handle timers for PTO and more). While this “works”, it’s not really that scalable or performant at the moment, so plans for the future definitely include refactoring this event loop a bit: decoupling the library loop from loops we may want to dedicate to networking may be a good idea, for instance, in order to get closer to the flexibility we have in that regard in Janus (where we can have one thread/loop per PeerConnection, or a pool of threads/loops that are mapped, e.g., to the number of cores on the machine). The events we use internally for managing connections could use some refactoring as well, but for now they do their job.
When we’re done with the application, and have closed all our connections and shutdown our endpoints, the library should be uninitialized, which guarantees that resources the library may have been allocated will be released (e.g., the above mentioned loop).
Coming to how to use the library for establishing QUIC connections, instead, all APIs are meant to be more high level, which means that at the moment you may not find a way to really customize everything you may want to be able to customize. A few of the libraries that are available, for instance, provide a much lower level access to the protocol, giving a great deal of flexibility and control of the stack, but at the same time making the management of the state harder. I always try to keep a good trade-off between level of control and ease of use, which is why in this first iteration of the library I steered towards such a higher level approach for the library API. Whether this is a good approach we’ll see, especially once more people start playing with it: I’m sure requirements will emerge that will prompt for at least opening the API to more options. At the very least, for instance, I’ll expect there will be people interested in having more control on the value of QUIC transport parameters during the connection establishment phase, or on dynamic flow control and credits.
In the next sections we’ll see how the API currently works, by presenting some differences in terms of using the library as a generic QUIC one, versus one more targeted to natively supported protocol.
imquic API: generic or “native”?
We anticipated in the intro that, although I conceived this library as a playground for real-time media usages of QUIC, it was actually born as a more generic QUIC stack, which means it can (still) be used as such accordingly. More precisely, when using the library you can choose whether you want to use it as a raw QUIC library (where you negotiate the ALPN you want) and/or as a WebTransport one (where an HTTP/3 stack is used for the sole purpose of establishing a WebTransport connection). When you create a QUIC client of server, you can choose to use either one or both on the same port, which makes it more flexible in some contexts where you don’t know in advance what kind of transport your peer will support.
This generic usage of the library is what I used (and still use) a lot whenever I want to focus more on the QUIC aspects of the library, rather than any of the natively supported application protocols. A typical test, for instance, involves simulating DNS Over QUIC (DoQ) connections with the related demos in aioquic (which is an excellent library to test against). Considering pretty much any kind of application can be built on top of QUIC, this opens up the door to many interesting usages of the library.
As anticipated, in general you use the library by creating a server or client, configuring some callback for relevant events, and then start the endpoint: once connected, you can start interacting on the connection, e.g., by proactively sending STREAM
or DATAGRAM
data, or waiting for callbacks to fire in order to react to them. The generic imquic API allows you to define callbacks for the following:
- being notified about a new connection (new client if we’re a server, successful connection if we’re a client);
- being notified about incoming data (using separate callbacks for
STREAM
andDATAGRAM
); - being notified about a connection going away.
When it comes to proactive requests, we can create streams, and send data using STREAM
and DATAGRAM
. In its simplicity, that’s really all we need as basics for a generic QUIC client or servers: it allows us to establish a connection, handle its life cycle, and exchange data with our peer. We won’t delve into the details of the specific C APIs as that would go beyond the scope of this introductory post, but you can refer to the documentation and demos for a more detailed overview.
This generic approach allows us to build pretty much anything on top of that (well, not really everything… see a later section on what that means for HTTP/3), but that also means that, in case the application protocol built on top of QUIC is a complex one, that the burden of implementing state, parsing and crafting of those messages falls on the application itself. We’ve mentioned a couple of interesting protocols in the domain of real-time multimedia, for instance, like RoQ and MoQT, that can fall in that criteria.
Even just using what we’ve seen so far, we could already work with RoQ and MoQT. In fact, we could simply use imquic as a QUIC stack, and then build our own RoQ and MoQT stack leveraving the incoming/outgoing data as made possible by the imquic API. That definitely works, but again, that means putting the burden of implementing those protocols on application developers, and then have them write the logic to use those protocols too (e.g., is this for a MoQT publisher, subscriber or relay?).
As such, I decided to implement the stack of the latest versions of both protocols within the library itself. The idea is basically the same: leverage the QUIC functionality of the library to send/receive messages, and then parse/craft them accordingly using RoQ or MoQT. The key difference is that this is now happening inside the library, which means having access to some more internals, and being able to expose dedicated APIs to end users as well, again for the sake of making it as simple as possible for developers to use those protocols.
For both protocols, I decided to mostly stick to the crafting/parsing and validation of those messages, but leaving out any aspect related to the logic associated with them: to make a simple example, if you create a MoQT relay, you’ll have access to the related callbacks and you’ll be able to send/receive the messages as a relay, but any decision on what to do with these messages (e.g., an incoming ANNOUNCE
or SUBSCRIBE
) is up to the application. This was done on purpose to ensure applications have complete control on the use cases, or at least as much control as possible.
In order to put a clear separation between using imquic as a generic QUIC library and leveraging its native support for some protocols, there are different methods for creating RoQ/MoQT clients and servers, all while following the same approach, though. To make a simple example, you create a generic client or server using imquic_create_client
and imquic_create_server
: this assumes the callbacks we’ve glanced over previously will need to be configured on the new endpoint. Should we want to use the native integration of RoQ, though, we will NOT use those constructors with the RoQ ALPN, as that would leave the management of the RoQ protocol up to us: we’ll use imquic_create_roq_client
and imquic_create_roq_server
instead, which will instruct the library to not only establish a QUIC connection, but also parse and craft RoQ messages on our behalf. At the same time, this means we won’t be able to use the generic callbacks we seen before, but we’ll need new ones. Specifically, in the case of RoQ it means callbacks for:
- being notified about a new connection (pretty much the same as before);
- being notified about incoming RTP/RTCP packets (independently of how they were serialized or multiplexed on the wire);
- being notified about a connection going away (pretty much the same as before).
Having a dedicated callback to just be notified about RTP/RTCP packets makes the management of RoQ much easier from an application perspective, especially when considering we’d otherwise need to figure out when to serialize the flow ID, when to serialize the packet length, how to serialize the actual RTP/RTCP packet, how to handle the multiplexing on top of STREAM
or DATAGRAM
frames, and so on and so forth. With a native support of RoQ within imquic, it’s the library that deals with that for us, and then just lets us know about the actual packet (plus the associated flow ID), so the only thing we need from an application perspective. The same reasoning is applied to proactive data as well: rather than use raw APIs to send STREAM
or DATAGRAM
frames, using the imquic RoQ integration we have a dedicated API to send an RTP/RTCP packet, which just requires the associated flow ID, and what multiplexing method should be used for serializing the data (e.g., DATAGRAM
, a packet per STREAM
or multiple packets per STREAM
).
This native integration approach has an even deeper integration when we take MoQT into account. In fact, while for RoQ all we need to worry about is mostly how RTP/RTCP packets are sent and received on a QUIC connection, with MoQT there’s a much more complex management to take care of, in terms of roles, messages that can be sent and received, subscriptions, objects and so on. This means that, again, we can always still use imquic_create_client
and imquic_create_server
if we just want imquic to give us a QUIC connection, and then use the incoming and outgoing data to parse and manage MoQT ourselves: or, we can use imquic_create_moq_client
and imquic_create_moq_server
to have imquic deal with all of that for us, and then use the higher level APIs that the library exposes to actually use MoQT more programmatically.
When it comes to callbacks, we again cannot rely on the generic ones, and considering the amount of requests that MoQT supports and the different roles, there’s a much higher number of callbacks that can be specified. To keep this short, it’s basically the following:
- being notified about a new connection: when this happens, MoQT is not reasy to be used yet, as here we’re supposed to say which version of MoQT we’d like to use (imquic supports different versions of the draft) and the role we’d like to have (publisher, subscriber, relay);
- being notified when the connection is ready (we and our peer negotiated the version to use and our roles in the conversation);
- incoming MoQT requests, depending on the role (e.g., we may want to be notified about incoming
SUBSCRIBE
,ANNOUNCE
, or other requests, responses to our requests, and/or incoming objects); - being notified about a connection going away.
The amount of MoQT requests and responses that are part of the specification obviously also translates in about as many proactive methods the API exposes to send those messages programmatically: this means we’ll have ways to, e.g., send a SUBSCRIBE
request, or accept/reject an ANNOUNCE
we received. At the same time, exactly as in RoQ we have simple ways to send and receive objects, which is how media is serialized over MoQT: no matter how they’re delivered (STREAM
vs. DATAGRAM
, using grouping or not, etc.), the application always handles objects in their entirety, both on the way in and on the way out. This is particularly helpful, for instance, when dealing with stuff like video frames, which can span multiple datagrams when looking at them from the perspective of serialization on a QUIC connection.
Long story short, the idea is that with imquic you’re supposed to be able to deal with application data any way you want: you can either handle it entirely yourself (generic API), or, in case the library has native support for the protocol, you can leave it up to the library and just deal with the application level of the protocol itself. Time will tell if this will be a good or sustainable approach: it definitely makes things much easier to developers interested in the protocol without necessarily having a good knowledge of QUIC, but it will probably require some tuning for a better management of underlying concepts like flow control and congestion control, which are only sketched in the library at the moment.
What can we do with the demos?
The repo comes, out of the box, with many demos you can play with. These demos, albeit fairly basic, try to take advantage of both the generic and native APIs, in order to demonstrate how they can be used for different use cases.
In fact, these are all demos that I wrote to prototype my support for different functionality in the library, especially with respect to the support for those native protocols we went through. I try to regularly use those demos for some interop tests as well, which means they can be very helpful to see if/how things work from an application perspective, or to analyze the QUIC traffic in order to study or debug the exchanges. You can see a few command line one-liners for the demos in the examples README on the repo, but in the next paragraphs we’ll try and describe what the demos do more from a high level perspective.
For a generic usage of the library, for instance, I wrote a couple of basic echo applications: imquic-echo-server
is a QUIC server (that can do both raw QUIC and WebTransport) that simply echoes back whatever data it receives using the same frame that was used initially, while imquic-echo-client
is a QUIC client (again, raw QUIC or WebTransport) that simply sends something (a “ciao” string on a STREAM
) and then prints what it gets back. As you see, nothing fancy, but they already provide good foundations to do some testing, or build something more interesting. I often test the echo server with the webtransport.day online demo, for instance: it’s a web page that allows you to specify a WebTransport server to connect to (e.g., a local instance of imquic-echo-server
) and then send data using different approaches, all while showing what’s being received. Testing this with my echo server was invaluable in ensuring I was doing everything correctly. I often tested both my echo demos with aioquic demos as well, as described in my first blog post on QUIC.
In a previous blog post I also showed how I performed a basic translation between WebRTC data channels and WebTransport by integrating my library in a custom Janus plugin. This integration is currently not available in the imquic repo, as I plan to provide those examples either as an entirely separate repo (that will use imquic as a dependency) or as part of the Janus repo itself.
The RoQ demos also proved quite helpful, especially when working on interop testing with Mathis’ implementation at the IETF 120 hackathon in Vancouver. The demos are quite basic, since imquic-roq-server
does nothing more than wait for connections and RTP packets, and when RTP packets arrive, it prints the flow ID and RTP headers; on the other end, imquic-roq-client
was conceived to simply forward via RoQ any RTP packet it receives on a couple of ports, e.g., as sent by GStreamer or FFmpeg. For instance, assuming we’re telling the client to expect audio packets on port 15002 and video packets on port 15004, if we send RTP packets on those ports, the client will then pack them in RoQ to send them to the server it connected to. The client will also map audio and video to specific flow IDs.
Again, in the blog post I wrote on RoQ a few weeks ago I also showcased an integration of RoQ in Janus, specifically for the purpose of gatewaying RTP over QUIC to/from RTP over WebRTC. As I mentioned before, while that specific demo is not available at the moment, some form of it will appear later in time, as soon as I clean up the code and present it in a more usable form. Notice that, considering that imquic-roq-client expects plain RTP packets on some specific port, a simple excercise you could make already is feeding it with RTP packets coming from an RTP forwarder (e.g., a VideoRoom publisher), in order to then have those same RTP packets relayed via QUIC to a RoQ server.
To conclude, the repo also comes with a few MoQT demos. Namely, there’s a basic publisher in imquic-moq-pub
, a basic subscriber in imquic-moq-sub
, and a basic relay in imquic-moq-relay
; there’s also a stub for a moq-chat endpoint in imquic-moq-chat
, but that’s still incomplete and doesn’t really do much. When I say those demos are basic, I kinda mean it: they do what they have to do, but don’t expect anything fancy out of the box! Specifically, the publisher can only push objects in the same format as moq-rs moq-clock
demo, which is a cool text based simulation of how differential video encoding can be mapped to groups (or subgroups). The subscriber can be told to handle incoming objects in different ways (e.g., text, mp4 video frames, or LOC), but it will do nothing more than display some of its content for debugging purposes (or, in case of mp4 frames coming from moq-rs moq-pub
, save them to an mp4 file to replay it later). The relay can act as an intermediary between publishers and subscribers for some simpler use cases: it will store objects to allow fetching them later on, but it is nevertheless still a bit limited when it comes to more specific and fancier use cases.
That said, they should allow you to see some simple MoQT use cases happening in practice, both by having the demos only interact with each other, or by involving third party MoQT implementations (e.g., moq-rs, moxygen, moq-encoder-player and more).
As with the previous demos, the MoQT blog post covered some more exciting interactions with Janus, where we performed some POC WebRTC-to-MoQT (and back) translations, but again that’s not available in the existing code base yet. More on that in the future, so stay tuned!
Any documentation available?
Of course! Demos are cool, but what’s even cooler is trying to write something of your own using the library instead, or, why not, improve the library itself with fixes and/or new features.
More precisely, there are two different documentation sections: one for those interested in using the library as it is (the “public” API), and another for those interested in better understanding the internals of the library, e.g., for the sake of modifying or enhancing it (the “internal” API). At the time of writing the documentation for the v0.0.1-alpha
version is available at this address.
For both the public and the internal APIs, there are different sections depending on what you actually need. Specifically, there’s a major distinction, obviously, between setting up a QUIC client and a QUIC server, but more importantly, as we’ve seen there are also key differences related to what you’d be using that library for: this means that, depending on whether you want to use imquic as a generic QUIC/WebTransport library or use of the natively supported protocols, the APIs to use accordingly will be different, which is something the documentation tries to cover.
We tried to also provide as much information as possible on how the library works internally. That’s not really relevant or of interest to those that will use the library as it is (the public API documentation will suffice for that), but will definitely be of interest to those interested in learning more on how the library internals. Ideally this should provide a way for QUIC developers to familiarize with the architecture of the library, and give them enough information on where to put their hands should they want to change the behaviour in some specific areas, or add new functionality that the library may be currently missing. It also provides more context on how the native support for RoQ and MoQT works inside the library, which may be important to extend the existing support for those protocols too.
Should you find the documentation too obscure, convoluted, or simply plain wrong (the library has undergone many changes that may not have always been reflected in the docs), pleased to let us know so that it can be improved!
What’s next?
Well, as you probably figured out if you got this far, there’s still a lot of work to do. The library does its job, and works nicely as a simple playground for testing new RoQ and MoQT features. At the same time, though, it’s admittedly not a very robust or performant application in its current state: the underlying QUIC stack definitely needs some improvement, but there are a few other architectural details, both in the core and the native protocol integrations, that would benefit from some more work.
On the top of my head, a few areas that will need work are the following.
- While the library in theory supports both IPv4 and IPv6, I haven’t really tested IPv6 support yet, as I don’t use it at home where I work. Should you find out it’s broken, it would be nice to know!
- We’re currently missing proper error management in a few areas: for instance, we most definitely still don’t close connections with a protocol violation error when we should, and the same applies to a few other error conditions we should react to as per the specification.
- The management of ACKs in the core is a bit convoluted, especially those we send ourselves (as in tracking what we received and telling about it), and I plan to improve it.
- We still don’t support congestion control: Appendix B. of RFC9002 comes with pseudocode for using NewReno in a QUIC stack, and my efforts on bandwidth estimation in WebRTC from last year may help too. That said, figuring out how to hook those core congestion control mechanics to the higher level APIs we expose to applications will be a challenge of its own.
- We don’t expose proper controls and APIs for flow control either, which we’ll probably need to do (at the moment, we automatically ask for more credits when we’re halfway there).
- Other cool QUIC features to have would be 0-RTT and connection migration: for the former the existing code is mostly working, while the latter will probably need more work (including better tracking of connection IDs advertised via
NEW_CONNECTION_ID
). - I’d definitely love to add QLOG support to the library, as it was immensely helpful when testing early versions of my library with aioquic, which does generate QLOG files. Considering it’s an ongoing specification that will probably be subject to changes, the challenge will be figuring out what we should support for maximum compatibility with existing implementations (e.g., QLOG visualizers).
- At the moment, once you create a server you can’t change the certificate/key that will be used for incoming connections: with WebTransport mandating certificates that last at maximum 14 days, a way to dynamically update those settings without recreating the server would be quite important.
- As we anticipated, the event loop approach the library is based upon, at the moment, is very basic and quite limiting, so that will need some refactoring as well.
- The demos could use some love too: the MoQT relay, for instance, at the moment uses a global lock that completely ignores performance for the sake of simplicity. Considering I’m planning to write a dedicated relay application based on imquic, this may not be a priority, but it’s still worth taking note of.
Apart from work on the library (and demos), there comes a discussion about integration, of course. Specifically, in this and previous blog posts I often referenced using Janus in conjunction with imquic for the purpose of showcasing some form of interaction between QUIC and WebRTC endpoints. It was the case, for instance, of the datachannel-to-WebTransport translation, but also the RoQ-to-WebRTC gateway and the MoQ-to-WebRTC demo. This was all done by writing a new, custom, Janus plugin leveraging imquic as a library, that would then take care of the related translation efforts. As you can see looking at the repo, this Janus plugin is not available there, for the simple reason that, as it is now, it’s a hacky mess: this is fine for my needs, as I use it as a playground, but less so if I want other people to play with it.
As such, I plan to release different levels of integrations in Janus for different use cases, all leveraging imquic of course. Basically, the steps I’m planning are the following:
- Allow Janus to properly detect quic-tls during the configuration and compilation processes, and at runtime. This will be fundamental, since imquic relies on some APIs that the “regular” OpenSSL doesn’t provide, while quic-tls does instead. In my current integration, I did it using some assumptions that work fine in my setup, but would not on generic installations, and so that would need to be addressed.
- Rather than bake everything QUIC related into a plugin, my plan would then be adding different features at different levels:
- A WebTransport transport plugin for the Janus and Admin APIs could be a simple starting point: just as we can control a Janus instance using HTTP, WebSocket and other protocols, having WebTransport in there as an option as well would be an interesting addition, and a simple one to add.
- Considering the heavy usage we make of RTP forwarders, adding an option to support RoQ as well for that functionality could be very interesting: in fact, although mostly used by plugins, RTP forwarders are implemented in the core, and so leveraging RoQ “trunks” could provide a reliable channel for exchanging RTP packets, possibly by bundling multiple forwarders on the same QUIC connection in case the target is the same (which could be particularly interesting for cascading support in the VideoRoom plugin).
- Another interesting use case is the Streaming plugin, whose main job is receiving plain RTP packets and broadcasting them via WebRTC to one or more recipients. This is another place where having RoQ support may be very interesting, as it could allow us to map a RoQ server sitting in Janus to a Streaming plugin mountpoint.
- Of course, there’s a MoQT-to-WebRTC translation to take into account, but that will probably be the subject of a much more complex integration, due to the heterogeneity of scenarios MoQT itself can cover. A dedicated plugin, as I did, may be an option, but would make an integration with, e.g., the VideoRoom harder: at the same time, baking MoQT support in the VideoRoom would probably increase the complexity of that plugin too much. A potential generic MoQT publisher/subscriber support in the core (as we did for RTP forwarders) may be an option, but again that will be something that will need a proper investigation (especially considering the MoQT specification is very much in flux).
- There’s also the option of writing external tools for all that: e.g., applications that perform RoQ-to-plain-RTP and plain-RTP-to-RoQ could be used as companion tools with existing an unmodified Janus instances. MoQT related tools could be implemented as well.
In a nutshell, one way or another there will be things to keep us busy! And hopefully these will be interesting enough topics that some of you will consider contributing to the development of the library to get us closer to the finish line sooner rather than later.
What about HTTP/3?
When we say you can use imquic as a generic QUIC library, we unfortunately don’t mean the same applies to the whole HTTP/3 ecosystem. HTTP/3 is a quite complex specification, that envisages a deeply tied integration with the QUIC semantics: as such, you can’t really negotiate, e.g., h3
as an ALPN and then build your HTTP/3 stack from the POV of an application using the library, as that would require a tighter interation in the QUIC stack itself. As a matter of fact, in order to add native support for WebTransport, we added a basic HTTP/3 and QPACK stack within the imquic core, which is only confined and limited to the setup of WebTransport, though. This means that it’s hardcoded to only deal with CONNECT
messages for the establishment of WebTransport sessions: any attempt to do more than that will simply not work, at the moment.
That said, I’m definitely not ruling out a more comprehensive support for other HTTP/3 requests, in the future: to be honest, though, it’s arguably not really a priority for me, at least not for the moment, and especially with so many other things in the pipeline. But if you want to help on that, don’t let that stop you! Having dedicated APIs for HTTP as we do for RoQ and MoQT might indeed be a cool thing to have.
That’s all, folks!
I hope you’re as excited about this announcement as I was when I first made the new imquic repo available. While it may not be perfect in this initial stage, I still think it’s a good starting point towards a very interesting future: after all, Janus itself was very different when we first released it 10 years ago, and look where that journey led us!
I’m really looking forward to your thoughts on the library, so please start playing it and share your feedback, especially if you’re interested in real-time media!