Sync sans I/O WebRTC implementation in Rust.
THIS IS NOT READY FOR PRODUCTION USE!
But I have managed to transfer some kind of video.
The http-post
example
and the tests roughly illustrates how to use this library. The example shows how to run single threaded
without any async I/O.
Currently discussing str0m things on the webrtc-rs discord server, in the #str0m channel. Join us!
For passive connections, i.e. where the media and initial OFFER is made by a remote peer, we need these steps to open the connection.
let mut rtc = Rtc::new(); // 1
rtc.add_local_candidate(candidate); // 2
let answer = rtc.accept_offer(offer).unwrap(); // 3
// 4
- Instantiate a new Rtc instance.
- Add some ICE candidate such as a locally bound UDP port.
- Accept an incoming offer and get the corresponding answer. The candidates in 2 will be
communicated in the answer. Similarly to the standard WebRTC API, how offer/answer are
transported between the
Rtc
instance and the client is a separate concern, but typically done via HTTP POST or a WebSocket. - Go to Run loop below.
Active connections means we are making the inital OFFER and waiting for a remote ANSWER to start the connection.
let mut rtc = Rtc::new(); // 1
rtc.add_local_candidate(candidate); // 2
let mut change = rtc.create_offer(); // 3
let mid = change.add_media(MediaKind::Audio, Direction::SendRecv); // 4
let offer = change.apply(); // 5
// 6
rtc.pending_changes().unwrap().accept_answer(answer)?; // 7
// 8
- Instantiate a new Rtc instance.
- Add some ICE candidate such as a locally bound UDP port.
- Create a
ChangeSet
. The change set is a builder pattern that lets us make multiple changes before sending the offer. - Do some change. A valid OFFER needs at least one "m-line" (media).
- Get the offer.
- Forward the offer to the remote peer and await the answer. How to transfer this is outside the scope for this library.
- Apply answer.
- Go to Run loop below.
Driving the state of the Rtc
forward is a run loop that looks like this.
loop {
// Poll output until we get a timeout. The timeout means we are either awaiting UDP socket input
// or the timeout to happen.
let timeout = match rtc.poll_output()? { // 1
Output::Timeout(v) => v, // 2
Output::Transmit(v) => {
// Transmit data via the bound UDP socket.
socket.send_to(&v.contents, v.destination)?;
continue;
}
Output::Event(v) => {
// Handle events from the Rtc instance.
// Abort if we disconnect.
if v == Event::IceConnectionStateChange(IceConnectionState::Disconnected) {
return Ok(());
}
continue;
}
};
let timeout = timeout - Instant::now();
// socket.set_read_timeout(Some(0)) is not ok
if timeout.is_zero() {
rtc.handle_input(Input::Timeout(Instant::now()))?;
continue;
}
socket.set_read_timeout(Some(timeout))?;
buf.resize(2000, 0);
let input = match socket.recv_from(&mut buf) { // 3
Ok((n, source)) => {
buf.truncate(n);
Input::Receive(
Instant::now(),
Receive {
source,
destination: socket.local_addr().unwrap(),
contents: buf.as_slice().try_into()?,
},
)
}
Err(e) => match e.kind() {
// Expected error for set_read_timeout(). One for windows, one for the rest.
ErrorKind::WouldBlock | ErrorKind::TimedOut => Input::Timeout(Instant::now()),
_ => return Err(e.into()),
},
};
rtc.handle_input(input)?; // 4
}
- Call
rtc.poll_output()
, the output can be of three kinds: a. Transmit. Some UDP data that needs transmitting. b. Event. Some state change, or incoming media data. c. Timeout. The time theRtc
instance needs time to be moved forward. - Keep doing 1 until we get a
c
timeout. - Await the time in 2, or receive UDP input data.
- Push UDP data or the timeout from 2 using
rtc.handle_input(<time or input data>)
. - Repeat from 1.
When creating the m-line, we can decide which codecs to support, which is then negotiated with the remote side. Each codec corresponds to a "payload type" (PT). To send media data we need to figure out which PT to use when sending.
let media = rtc.media(mid).unwrap(); // 1
let pt = media.codecs()[0]; // 2
let writer = media.get_writer(pt); // 3
writer.write(time, data)? // 4
- Get the
Media
for thismid
. - Get the payload type (pt) for the wanted codec.
- Get the media writer for the payload type.
- Write the data.
This project is heavily inspired by quinn, and specifically quinn-proto which is the sync underpinnings of quinn. Similarly, str0m could be packaged up in a simpler async API, this is however out of scope for now.
Sans I/O is a pattern where we turn both network input/output as well as time passing into external input to the API. This means str0m has no internal threads, just an enormous state machine that is driven forward by different kinds of input.
- Incoming network data
- Time going forward
- User operations such as pushing media data.
In response to this input, the API will react with various output.
- Outgoing network data
- Next required time to "wake up"
- Incoming events such as media data.
Rust shines when we can eschew locks and heavily rely &mut
for data write access. Since str0m
has no internal threads, we never have to deal with shared data. Furthermore the the internals of
the library is organized such that we don't need multiple references to the same entities.
This means all input to the lib can be modelled as handle_something(&mut self, something)
.
The library deliberately steps away from the "standard" WebRTC API as seen in JavaScript and/or webrtc-rs (or Pion in Go). There are few reasons for this.
First, in the standard API, events are callbacks, which are not a great fit for Rust, since
callbacks require some kind of reference (ownership?) over the entity the callback is being
dispatched upon. I.e. if in Rust we want to pc.addEventListener(x)
, x
needs to be wholly
owned by pc
, or have some shared reference (like Arc
). Shared references means shared data,
and to get mutable shared data, we will need some kind of lock. i.e. Arc<Mutex<EventListener>>
or similar.
As an alternative we could turn all events into mpsc
channels, but listening to multiple channels
is awkward without async.
Second, in the standard API, entities like RTCPeerConnection
and RTCRtpTransceiver
, are
easily clonable and/or long lived references. I.e. pc.getTranscievers()
returns objects that
can be retained and owned by the caller. This pattern is fine for garbage collected or reference
counted languages, but not great with Rust.
For the browser to do WebRTC, all traffic must be under TLS. The project ships with a self-signed
certificate that is used for the examples. The certificate is for hostname str0m.test
since
TLD .test should never resolve to a real DNS name.
- Edit
/etc/hosts
sostr0m.test
to loopback.
127.0.0.1 localhost str0m.test
-
Start the example server
cargo run --example http-post
-
In a browser, visit
https://str0m.test:3000/
. This will complain about the TLS certificate, you need to accept the "risk". How to do this depends on browser. In Chrome you can expand "Advanced" and chose "Proceed to str0m.test (unsafe)". For Safari, you can similarly chose to "Visit website" despite the warning. -
Click "Cam" and/or "Mic" followed by "Rtc". And hopefully you will see something like this in the log:
Dec 18 11:33:06.850 INFO str0m: MediaData(MediaData { mid: Mid(0), pt: Pt(104), time: MediaTime(3099135646, 90000), len: 1464 })
Dec 18 11:33:06.867 INFO str0m: MediaData(MediaData { mid: Mid(0), pt: Pt(104), time: MediaTime(3099138706, 90000), len: 1093 })
Dec 18 11:33:06.907 INFO str0m: MediaData(MediaData { mid: Mid(0), pt: Pt(104), time: MediaTime(3099141676, 90000), len: 1202 })
Licensed with MIT license