A standalone WebRTC and ORTC data channel implementation.
The following list represents all features that are planned for RAWRTCDC. Features with a check mark are already implemented.
- C-API based on the W3C CG ORTC API
- SCTP-based data channels [draft-ietf-rtcweb-data-channel-13]
- DCEP implementation [draft-ietf-rtcweb-data-protocol-09]
- Support for arbitrarily sized messages
- Support for SCTP ndata RFC 8260 (partially implemented, see #14)
- Streaming mode (partially implemented, see #16)
- Hardware CRC32-C checksum offloading (requires SSE4.2)
-
Who should use this?
If you have built a WebRTC stack and...
- you want data channel support but you don't have it so far, or
- you don't want to maintain your own data channel implementation, or
- your data channel implementation is lacking some features,
then you should consider integrating this library. 🎉
-
But I also need ICE/DTLS!
Check out RAWRTC.
-
How does it work?
Basically, you pass in SCTP packets and you get out SCTP packets. Put that on top of your DTLS transport and you're ready to go.
See the Getting Started section on how to set it up.
-
Can I use it in an event loop?
Yes.
-
Can I use it in a threaded environment?
Yes. Just make sure you're always calling it from the same thread the event loop is running on, or either lock/unlock the event loop thread or use the message queues provided by re to exchange data with the event loop thread. However, it is important that you only run one re event loop in one thread.
-
Does it create threads?
No.
-
Is this a custom SCTP implementation?
No, it uses usrsctp underneath but handles all the nitty-gritty details for you.
The following tools are required:
cd <path-to-rawrtcdc>
mkdir build
meson build
cd build
ninja
Now that you've built the library, let's get started integrating this library into your stack.
Before doing anything, initialise the library:
#include <rawrtcc.h>
#include <rawrtcdc.h>
[...]
enum rawrtc_code error = rawrtcdc_init(init_re, timer_handler);
if (error) {
your_log_function("Initialisation failed: %s", rawrtc_code_to_str(error));
}
In the following code examples, the error handling will be omitted. But you of course still need to handle it in your code.
Unless you're initialising re yourselves, the init_re
parameter to
rawrtcdc_init
should be true
. The second is a pointer to a timer handler.
The timer handler function works in the following way (see comments inline):
enum rawrtc_code timer_handler(bool const on, uint_fast16_t const interval) {
if (on) {
// Start a timer that calls `rawrtcdc_timer_tick` every `interval`
// milliseconds.
} else {
// Stop the timer.
}
// Indicate success. In case something fails for you, you can also
// backpropagate an appropriate error code here.
return RAWRTC_CODE_SUCCESS;
}
Before you can create data channels, you will need to create an SCTP transport:
// Create SCTP transport context
struct rawrtc_sctp_transport_context context = {
.role_getter = dtls_role_getter,
.state_getter = dtls_transport_state_getter,
.outbound_handler = sctp_transport_outbound_handler,
.detach_handler = sctp_transport_detach_handler,
.destroyed_handler = sctp_transport_destroy,
.trace_packets = false,
.arg = your_reference,
};
// Create SCTP transport
struct rawrtc_sctp_transport transport;
error = rawrtc_sctp_transport_create_from_external(
&transport, &context, local_sctp_port,
data_channel_handler, state_change_handler, arg);
if (error) {
your_log_function("Creating SCTP transport failed: %s",
rawrtc_code_to_str(error));
goto out;
}
// Attach your DTLS transport here
After the transport has been created successfully, transport
will point to
some dynamically allocated memory which is reference counted (and the reference
counter value will be 1
after the function returned). If you want to increase
the reference, call mem_ref(transport)
. If you need to decrease the
reference, call mem_deref(transport)
. Once the counter value reaches 0
, it
will run a destructor function and free the memory. However, you should
normally stop the transport with rawrtc_sctp_transport_stop
in a more
graceful manner before doing so. We're pointing this out here since basically
everything in this library that allocates dynamic memory works that way.
Furthermore, from this moment on your DTLS transport should feed SCTP packets
into the SCTP transport by calling
rawrtc_sctp_transport_feed_inbound(transport, buffer, ecn_bits)
. Check the
header file for details on the parameters.
You're probably already wondering what the SCTP transport context is all about.
Basically, it contains pointers to some handler functions you will need to
define. The only exception is the arg
field which let's you pass an arbitrary
pointer to the various handler functions. So, let's go through them:
enum rawrtc_code dtls_role_getter(
enum rawrtc_external_dtls_role* const rolep, void* const arg) {
// Set the local role of your DTLS transport
*rolep = your_dtls_transport.local_role;
return RAWRTC_CODE_SUCCESS;
}
enum rawrtc_code dtls_transport_state_getter(
enum rawrtc_external_dtls_transport_state* const statep, void* const arg) {
// Set the state of your DTLS transport
*statep = your_dtls_transport.state;
return RAWRTC_CODE_SUCCESS;
}
These DTLS handler functions were fairly straightforward. Now to the SCTP handler that hands back outbound SCTP packets. These packets will need to be fed into the DTLS transport as application data and sent to the other peer:
enum rawrtc_code sctp_transport_outbound_handler(
struct mbuf* const buffer, uint8_t const tos, uint8_t const set_df,
void* const arg) {
// Feed the data to the DTLS transport
your_dtls_transport_send(buffer, tos, set_df);
return RAWRTC_CODE_SUCCESS;
}
The struct buffer
and its functions are documented here. As a rule
of thumb, you should call mbuf_buf(buffer)
to get a pointer to the current
position and mbuf_get_left(buffer)
to get the amount of bytes left in the
buffer.
Be aware buffer
in this case is not dynamically allocated and shall not be
referenced. This has been done for optimisation purposes.
Check the header file for further details on the various
parameters passed to this handler function.
The following handler is merely a way to let you know that you should not feed any data to the SCTP transport anymore:
void sctp_transport_detach_handler(void* const arg) {
// Detach from DTLS transport
your_dtls_transport.stop_feeding_data = true;
}
The last handler function we need to talk about is a way to tell you that the
SCTP transport's reference count has been decreased to 0
and its about to be
free'd. Be aware that you may not call any SCTP transport or data channel
functions once this handler is being called.
void sctp_transport_destroy(void* const arg) {
// Your cleanup code here
}
The trace_packets
attribute allows you to enable writing SCTP packets to a
trace file. The name of that file is randomly generated and will be placed in
the current working directory.
That's all for the SCTP transport.
The data channel API is very similar to the one used in WebRTC and ORTC, so we will not go into detail for them. Here's a quick example on how to create a data channel with the following properties:
- label:
meow
- protocol:
v1.cat-noises
- reliable
- unordered
- pre-negotiated
- stream id is fixed to
42
// Create data channel parameters
struct rawrtc_data_channel_parameters parameters;
error = rawrtc_data_channel_parameters_create(
¶meters, "meow", RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_UNORDERED, 0,
"v1.cat-noises", true, 42);
// Create the data channel, using the transport and the parameters
struct rawrtc_data_channel channel;
error = rawrtc_data_channel_create(
&channel, transport, parameters,
open_handler, buffered_amount_low_handler, error_handler,
close_handler, message_handler, pointer_passed_to_handlers);
mem_deref(parameters);
Instead of adding handler functions on creation, you can also pass NULL
and
register handler functions later.
For further explanation on the various parameters, check the header files.
Once the SCTP transport goes into the connected state, the created channels will open. If you see this happening, this is a good indication that you've set everything up correctly. 👏
Once your code exits, you should call rawrtcdc_close(close_re)
. If the
close_re
parameter is true
, re will be closed as well.
Do you feel like this now? If yes, please join our gitter chat, so we can help you and work out what's missing in this little tutorial.
When creating a pull request, it is recommended to run format-all.sh
to
apply a consistent code style.