reactor-effect-native 0.2.0 → 0.3.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +4 -2
- package/README.md +90 -15
- package/dist/_internal/bridge.d.ts +70 -14
- package/dist/_internal/bridge.d.ts.map +1 -1
- package/dist/_internal/bridge.js +290 -145
- package/dist/_internal/bridge.js.map +1 -1
- package/dist/_internal/isolated/child.d.ts +2 -0
- package/dist/_internal/isolated/child.d.ts.map +1 -0
- package/dist/_internal/isolated/child.js +176 -0
- package/dist/_internal/isolated/child.js.map +1 -0
- package/dist/_internal/isolated/host.d.ts +147 -0
- package/dist/_internal/isolated/host.d.ts.map +1 -0
- package/dist/_internal/isolated/host.js +645 -0
- package/dist/_internal/isolated/host.js.map +1 -0
- package/dist/_internal/isolated/protocol.d.ts +399 -0
- package/dist/_internal/isolated/protocol.d.ts.map +1 -0
- package/dist/_internal/isolated/protocol.js +250 -0
- package/dist/_internal/isolated/protocol.js.map +1 -0
- package/dist/_internal/peer.d.ts +68 -13
- package/dist/_internal/peer.d.ts.map +1 -1
- package/dist/_internal/peer.js +233 -157
- package/dist/_internal/peer.js.map +1 -1
- package/dist/index.d.ts +19 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -33
- package/dist/index.js.map +1 -1
- package/dist/isolated.d.ts +25 -0
- package/dist/isolated.d.ts.map +1 -0
- package/dist/isolated.js +39 -0
- package/dist/isolated.js.map +1 -0
- package/lib/darwin-arm64/libreactor_effect_native.dylib +0 -0
- package/lib/darwin-arm64/native-identity.json +4 -3
- package/lib/linux-x64/libreactor_effect_native.so +0 -0
- package/lib/linux-x64/native-identity.json +4 -3
- package/package.json +7 -3
- package/rust/Cargo.toml +108 -2
- package/rust/build.rs +253 -114
- package/rust/clippy.toml +7 -0
- package/rust/include/reactor_effect_native.h +101 -42
- package/rust/src/abi.rs +258 -0
- package/rust/src/error.rs +149 -0
- package/rust/src/ffi/memory.rs +256 -0
- package/rust/src/ffi/tests.rs +719 -0
- package/rust/src/ffi.rs +474 -0
- package/rust/src/lib.rs +36 -2341
- package/rust/src/peer/callbacks.rs +145 -0
- package/rust/src/peer/media.rs +221 -0
- package/rust/src/peer/owner/tests.rs +188 -0
- package/rust/src/peer/owner.rs +353 -0
- package/rust/src/peer/shared.rs +323 -0
- package/rust/src/peer/tests.rs +513 -0
- package/rust/src/peer.rs +189 -0
- package/rust/src/protocol/event.rs +238 -0
- package/rust/src/protocol/request.rs +347 -0
- package/rust/src/protocol/stats.rs +356 -0
- package/rust/src/protocol.rs +71 -0
- package/rust/src/sync/gate.rs +159 -0
- package/rust/src/sync/notifier.rs +136 -0
- package/rust/src/sync/queue.rs +384 -0
- package/rust/src/sync.rs +20 -0
- package/rust/src/test_support.rs +99 -0
- package/rust-toolchain.toml +7 -0
- package/scripts/stage.mjs +41 -1
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
//! The owner thread: the one thread that holds a peer's libwebrtc objects.
|
|
2
|
+
|
|
3
|
+
use super::{Shared, callbacks};
|
|
4
|
+
use crate::abi::{Channel, MAX_BUFFERED_SEND_BYTES, Operation};
|
|
5
|
+
use crate::error::{BridgeError, Classify, FailureClass};
|
|
6
|
+
use crate::protocol::{
|
|
7
|
+
self, BitrateRequest, Direction, DirectionRequest, Mapping, PrepareRequest, PrepareResponse,
|
|
8
|
+
stats_json,
|
|
9
|
+
};
|
|
10
|
+
use crate::sync::lock;
|
|
11
|
+
use reactor_webrtc::{
|
|
12
|
+
DataChannel, DataChannelState, PeerConnection, PeerConnectionFactory, RtcConfiguration,
|
|
13
|
+
SdpType, SessionDescription, Transceiver, TransceiverDirection,
|
|
14
|
+
};
|
|
15
|
+
use std::collections::HashMap;
|
|
16
|
+
use std::sync::mpsc::{Receiver, SyncSender};
|
|
17
|
+
use std::sync::{Arc, Mutex};
|
|
18
|
+
|
|
19
|
+
#[cfg(test)]
|
|
20
|
+
mod tests;
|
|
21
|
+
|
|
22
|
+
/// Where the owner thread sends one command's result.
|
|
23
|
+
pub(crate) type Reply<T> = SyncSender<Result<T, BridgeError>>;
|
|
24
|
+
|
|
25
|
+
/// Work for the owner thread, which runs commands one at a time.
|
|
26
|
+
pub(crate) enum Command {
|
|
27
|
+
Call {
|
|
28
|
+
operation: Operation,
|
|
29
|
+
request: Vec<u8>,
|
|
30
|
+
reply: Reply<Vec<u8>>,
|
|
31
|
+
},
|
|
32
|
+
Send {
|
|
33
|
+
channel: Channel,
|
|
34
|
+
bytes: Vec<u8>,
|
|
35
|
+
reply: Reply<()>,
|
|
36
|
+
},
|
|
37
|
+
Shutdown,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// libwebrtc's threads are process-global, so reactor-webrtc requires one
|
|
41
|
+
/// factory per process. Every peer shares this one; it is created on first
|
|
42
|
+
/// use and never destroyed. A `Mutex` rather than a `OnceLock` lets a failed
|
|
43
|
+
/// creation be retried.
|
|
44
|
+
static FACTORY: Mutex<Option<&'static PeerConnectionFactory>> = Mutex::new(None);
|
|
45
|
+
|
|
46
|
+
/// The process's libwebrtc factory.
|
|
47
|
+
pub(crate) fn factory() -> Result<&'static PeerConnectionFactory, BridgeError> {
|
|
48
|
+
let mut slot = lock(&FACTORY);
|
|
49
|
+
if let Some(factory) = *slot {
|
|
50
|
+
return Ok(factory);
|
|
51
|
+
}
|
|
52
|
+
let factory = PeerConnectionFactory::builder()
|
|
53
|
+
.with_synthetic_adm()
|
|
54
|
+
.build()
|
|
55
|
+
.classify(FailureClass::Native, "create_factory")?;
|
|
56
|
+
let factory: &'static PeerConnectionFactory = Box::leak(Box::new(factory));
|
|
57
|
+
*slot = Some(factory);
|
|
58
|
+
Ok(factory)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Send a command's result to the peer that queued it.
|
|
62
|
+
fn respond<T>(reply: &Reply<T>, result: Result<T, BridgeError>) {
|
|
63
|
+
#[expect(
|
|
64
|
+
clippy::let_underscore_must_use,
|
|
65
|
+
reason = "the peer waits for every reply, so a failed send means a panic unwound it \
|
|
66
|
+
and no one is waiting"
|
|
67
|
+
)]
|
|
68
|
+
let _ = reply.send(result);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// A negotiated connection and the libwebrtc objects created with it.
|
|
72
|
+
struct Connection {
|
|
73
|
+
peer: PeerConnection,
|
|
74
|
+
control: DataChannel,
|
|
75
|
+
data: DataChannel,
|
|
76
|
+
tracks: HashMap<String, Track>,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
impl Connection {
|
|
80
|
+
fn channel(&self, channel: Channel) -> &DataChannel {
|
|
81
|
+
match channel {
|
|
82
|
+
Channel::Control => &self.control,
|
|
83
|
+
Channel::Data => &self.data,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// A declared track's transceiver.
|
|
89
|
+
struct Track {
|
|
90
|
+
direction: Direction,
|
|
91
|
+
transceiver: Transceiver,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// The owner thread's state.
|
|
95
|
+
pub(crate) struct Owner {
|
|
96
|
+
shared: Arc<Shared>,
|
|
97
|
+
/// Set by `Prepare`.
|
|
98
|
+
connection: Option<Connection>,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl Owner {
|
|
102
|
+
pub(crate) fn new(shared: Arc<Shared>) -> Self {
|
|
103
|
+
Self {
|
|
104
|
+
shared,
|
|
105
|
+
connection: None,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Run commands until shutdown, then release the libwebrtc objects and
|
|
110
|
+
/// wait for every admitted callback to return.
|
|
111
|
+
pub(crate) fn run(mut self, commands: &Receiver<Command>) {
|
|
112
|
+
while let Ok(command) = commands.recv() {
|
|
113
|
+
match command {
|
|
114
|
+
Command::Call {
|
|
115
|
+
operation,
|
|
116
|
+
request,
|
|
117
|
+
reply,
|
|
118
|
+
} => {
|
|
119
|
+
let result = self.unless_closed(|owner| owner.call(operation, &request));
|
|
120
|
+
respond(&reply, result);
|
|
121
|
+
}
|
|
122
|
+
Command::Send {
|
|
123
|
+
channel,
|
|
124
|
+
bytes,
|
|
125
|
+
reply,
|
|
126
|
+
} => {
|
|
127
|
+
let result = self.unless_closed(|owner| owner.send(channel, &bytes));
|
|
128
|
+
respond(&reply, result);
|
|
129
|
+
}
|
|
130
|
+
Command::Shutdown => break,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
self.shutdown();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Run a command, unless the peer closed while it waited in the queue.
|
|
137
|
+
fn unless_closed<T>(
|
|
138
|
+
&mut self,
|
|
139
|
+
command: impl FnOnce(&mut Self) -> Result<T, BridgeError>,
|
|
140
|
+
) -> Result<T, BridgeError> {
|
|
141
|
+
if self.shared.gate.is_open() {
|
|
142
|
+
command(self)
|
|
143
|
+
} else {
|
|
144
|
+
Err(BridgeError::closed())
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fn call(&mut self, operation: Operation, request: &[u8]) -> Result<Vec<u8>, BridgeError> {
|
|
149
|
+
match operation {
|
|
150
|
+
Operation::Prepare => self.prepare(request),
|
|
151
|
+
Operation::Answer => self.answer(request),
|
|
152
|
+
Operation::Direction => self.set_direction(request),
|
|
153
|
+
Operation::MaxBitrate => self.set_max_bitrate(request),
|
|
154
|
+
Operation::Stats => self.stats(),
|
|
155
|
+
// The peer answers snapshots itself so they never wait behind a
|
|
156
|
+
// blocking call; answering one here as well keeps this total.
|
|
157
|
+
Operation::MediaSnapshot => protocol::encode(&self.shared.snapshot()),
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Create the connection, its data channels and a transceiver per
|
|
162
|
+
/// declared track, and return the local offer with each track's MID.
|
|
163
|
+
fn prepare(&mut self, request: &[u8]) -> Result<Vec<u8>, BridgeError> {
|
|
164
|
+
if self.connection.is_some() {
|
|
165
|
+
return Err(BridgeError::invalid("native peer is already prepared"));
|
|
166
|
+
}
|
|
167
|
+
let request = PrepareRequest::parse(request)?;
|
|
168
|
+
let config = RtcConfiguration {
|
|
169
|
+
ice_servers: request.servers.iter().map(Into::into).collect(),
|
|
170
|
+
..RtcConfiguration::default()
|
|
171
|
+
};
|
|
172
|
+
let peer = factory()?
|
|
173
|
+
.create_peer_connection(&config, callbacks::observer(&self.shared))
|
|
174
|
+
.classify(FailureClass::Native, "create_peer_connection")?;
|
|
175
|
+
let control = callbacks::open_channel(&peer, Channel::Control, &self.shared)?;
|
|
176
|
+
let data = callbacks::open_channel(&peer, Channel::Data, &self.shared)?;
|
|
177
|
+
let transceivers = request
|
|
178
|
+
.tracks
|
|
179
|
+
.iter()
|
|
180
|
+
.map(|track| {
|
|
181
|
+
peer.add_transceiver(track.kind.into(), track.direction.into())
|
|
182
|
+
.classify(FailureClass::Native, "add_transceiver")
|
|
183
|
+
})
|
|
184
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
185
|
+
|
|
186
|
+
let offer = peer
|
|
187
|
+
.create_offer()
|
|
188
|
+
.classify(FailureClass::SdpRejected, "create_offer")?;
|
|
189
|
+
peer.set_local_description(&offer)
|
|
190
|
+
.classify(FailureClass::SdpRejected, "set_local_description")?;
|
|
191
|
+
|
|
192
|
+
// Transceivers have their MIDs once the local description is set.
|
|
193
|
+
let mapping = request
|
|
194
|
+
.tracks
|
|
195
|
+
.iter()
|
|
196
|
+
.zip(&transceivers)
|
|
197
|
+
.map(|(track, transceiver)| {
|
|
198
|
+
let mid = transceiver.mid().ok_or_else(|| {
|
|
199
|
+
BridgeError::new(
|
|
200
|
+
FailureClass::Native,
|
|
201
|
+
format!("missing MID after local description: {}", track.name),
|
|
202
|
+
)
|
|
203
|
+
})?;
|
|
204
|
+
Ok(Mapping {
|
|
205
|
+
name: track.name.clone(),
|
|
206
|
+
kind: track.kind,
|
|
207
|
+
direction: track.direction,
|
|
208
|
+
mid,
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
.collect::<Result<Vec<_>, BridgeError>>()?;
|
|
212
|
+
let response = protocol::encode(&PrepareResponse {
|
|
213
|
+
sdp: &offer.sdp,
|
|
214
|
+
mapping: &mapping,
|
|
215
|
+
})?;
|
|
216
|
+
self.shared.set_bindings(&mapping);
|
|
217
|
+
|
|
218
|
+
let tracks = request
|
|
219
|
+
.tracks
|
|
220
|
+
.into_iter()
|
|
221
|
+
.zip(transceivers)
|
|
222
|
+
.map(|(track, transceiver)| {
|
|
223
|
+
let track_state = Track {
|
|
224
|
+
direction: track.direction,
|
|
225
|
+
transceiver,
|
|
226
|
+
};
|
|
227
|
+
(track.name, track_state)
|
|
228
|
+
})
|
|
229
|
+
.collect();
|
|
230
|
+
self.connection = Some(Connection {
|
|
231
|
+
peer,
|
|
232
|
+
control,
|
|
233
|
+
data,
|
|
234
|
+
tracks,
|
|
235
|
+
});
|
|
236
|
+
Ok(response)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// Apply the remote answer, which carries the remote peer's candidates.
|
|
240
|
+
fn answer(&self, request: &[u8]) -> Result<Vec<u8>, BridgeError> {
|
|
241
|
+
let sdp = std::str::from_utf8(request)
|
|
242
|
+
.map_err(|error| BridgeError::invalid(format!("answer SDP is not UTF-8: {error}")))?;
|
|
243
|
+
if sdp.is_empty() {
|
|
244
|
+
return Err(BridgeError::invalid("answer SDP is empty"));
|
|
245
|
+
}
|
|
246
|
+
let answer = SessionDescription {
|
|
247
|
+
kind: SdpType::Answer,
|
|
248
|
+
sdp: sdp.to_owned(),
|
|
249
|
+
};
|
|
250
|
+
self.connection()?
|
|
251
|
+
.peer
|
|
252
|
+
.set_remote_description(&answer)
|
|
253
|
+
.classify(FailureClass::SdpRejected, "set_remote_description")?;
|
|
254
|
+
Ok(protocol::empty_response())
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// Pause a declared track, or resume it in its declared direction.
|
|
258
|
+
fn set_direction(&self, request: &[u8]) -> Result<Vec<u8>, BridgeError> {
|
|
259
|
+
let request: DirectionRequest = protocol::decode(request)?;
|
|
260
|
+
let track = self.track(&request.name)?;
|
|
261
|
+
let direction = if request.active {
|
|
262
|
+
track.direction.into()
|
|
263
|
+
} else {
|
|
264
|
+
TransceiverDirection::Inactive
|
|
265
|
+
};
|
|
266
|
+
track
|
|
267
|
+
.transceiver
|
|
268
|
+
.set_direction(direction)
|
|
269
|
+
.classify(FailureClass::Native, "set_direction")?;
|
|
270
|
+
Ok(protocol::empty_response())
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// Cap an outgoing track's send bitrate.
|
|
274
|
+
fn set_max_bitrate(&self, request: &[u8]) -> Result<Vec<u8>, BridgeError> {
|
|
275
|
+
let request = BitrateRequest::parse(request)?;
|
|
276
|
+
let track = self.track(&request.name)?;
|
|
277
|
+
if track.direction != Direction::SendOnly {
|
|
278
|
+
return Err(BridgeError::invalid(format!(
|
|
279
|
+
"{} is not an outgoing track",
|
|
280
|
+
request.name
|
|
281
|
+
)));
|
|
282
|
+
}
|
|
283
|
+
track
|
|
284
|
+
.transceiver
|
|
285
|
+
.set_send_bitrate(None, Some(request.bits_per_second))
|
|
286
|
+
.classify(FailureClass::Native, "set_send_bitrate")?;
|
|
287
|
+
Ok(protocol::empty_response())
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
fn stats(&self) -> Result<Vec<u8>, BridgeError> {
|
|
291
|
+
let report = self
|
|
292
|
+
.connection()?
|
|
293
|
+
.peer
|
|
294
|
+
.get_stats()
|
|
295
|
+
.classify(FailureClass::Native, "get_stats")?;
|
|
296
|
+
protocol::encode(&stats_json(&report))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/// Send one binary message on an open bridge channel, unless it would
|
|
300
|
+
/// push the channel's unsent bytes past their bound.
|
|
301
|
+
fn send(&self, channel: Channel, bytes: &[u8]) -> Result<(), BridgeError> {
|
|
302
|
+
let open = self
|
|
303
|
+
.connection
|
|
304
|
+
.as_ref()
|
|
305
|
+
.map(|connection| connection.channel(channel))
|
|
306
|
+
.filter(|data_channel| data_channel.state() == DataChannelState::Open);
|
|
307
|
+
let Some(data_channel) = open else {
|
|
308
|
+
return Err(BridgeError::new(
|
|
309
|
+
FailureClass::ChannelClosed,
|
|
310
|
+
format!("{} channel is not open", channel.label()),
|
|
311
|
+
));
|
|
312
|
+
};
|
|
313
|
+
let buffered = data_channel.buffered_amount();
|
|
314
|
+
if buffered.saturating_add(bytes.len() as u64) > MAX_BUFFERED_SEND_BYTES {
|
|
315
|
+
return Err(BridgeError::overflow(
|
|
316
|
+
"native data channel buffered amount bound exceeded",
|
|
317
|
+
));
|
|
318
|
+
}
|
|
319
|
+
data_channel
|
|
320
|
+
.send(bytes, true)
|
|
321
|
+
.classify(FailureClass::Native, "send")
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fn connection(&self) -> Result<&Connection, BridgeError> {
|
|
325
|
+
self.connection.as_ref().ok_or_else(BridgeError::closed)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
fn track(&self, name: &str) -> Result<&Track, BridgeError> {
|
|
329
|
+
self.connection
|
|
330
|
+
.as_ref()
|
|
331
|
+
.and_then(|connection| connection.tracks.get(name))
|
|
332
|
+
.ok_or_else(|| BridgeError::invalid(format!("unknown track: {name}")))
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/// Release the libwebrtc objects children first, wait for every admitted
|
|
336
|
+
/// callback to return, then close the queues.
|
|
337
|
+
fn shutdown(self) {
|
|
338
|
+
if let Some(Connection {
|
|
339
|
+
peer,
|
|
340
|
+
control,
|
|
341
|
+
data,
|
|
342
|
+
tracks,
|
|
343
|
+
}) = self.connection
|
|
344
|
+
{
|
|
345
|
+
drop((control, data));
|
|
346
|
+
self.shared.release_remote_tracks();
|
|
347
|
+
drop(tracks);
|
|
348
|
+
drop(peer);
|
|
349
|
+
}
|
|
350
|
+
self.shared.gate.wait_idle();
|
|
351
|
+
self.shared.close_queues();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
//! The state a peer shares between its owner thread, libwebrtc callbacks and
|
|
2
|
+
//! the host's takes.
|
|
3
|
+
|
|
4
|
+
use super::media::{self, AudioItem, VideoItem};
|
|
5
|
+
use crate::abi::Ready;
|
|
6
|
+
use crate::error::FailureClass;
|
|
7
|
+
use crate::protocol::{DecimalU64, Direction, Event, Mapping, MediaSnapshot, TrackKind};
|
|
8
|
+
use crate::sync::{CallbackGate, Notifier, Push, Queue, lock};
|
|
9
|
+
use reactor_webrtc::RemoteTrack;
|
|
10
|
+
use std::collections::VecDeque;
|
|
11
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
12
|
+
use std::sync::{Arc, Mutex};
|
|
13
|
+
|
|
14
|
+
// Queue bounds. No single item can exceed its queue's byte bound, so the host
|
|
15
|
+
// sizes its take buffers from them: keep them in step with the MAX_*_BYTES and
|
|
16
|
+
// MAX_AUDIO_SAMPLES constants in packages/native/src/_internal/bridge.ts.
|
|
17
|
+
const EVENT_QUEUE_ITEMS: usize = 1024;
|
|
18
|
+
const EVENT_QUEUE_BYTES: usize = 16 * 1024 * 1024;
|
|
19
|
+
/// A third of a second at 24 fps.
|
|
20
|
+
const VIDEO_QUEUE_FRAMES: usize = 8;
|
|
21
|
+
const VIDEO_QUEUE_BYTES: usize = 64 * 1024 * 1024;
|
|
22
|
+
/// 2.56 s of 10 ms blocks.
|
|
23
|
+
const AUDIO_QUEUE_BLOCKS: usize = 256;
|
|
24
|
+
const AUDIO_QUEUE_BYTES: usize = 4 * 1024 * 1024;
|
|
25
|
+
// A media item's C header states its lengths as `u32`.
|
|
26
|
+
const _: () = assert!(VIDEO_QUEUE_BYTES <= u32::MAX as usize);
|
|
27
|
+
const _: () = assert!(AUDIO_QUEUE_BYTES <= u32::MAX as usize);
|
|
28
|
+
|
|
29
|
+
/// What a peer's threads share. libwebrtc callbacks copy into its queues and
|
|
30
|
+
/// signal readiness; the host takes from the queues.
|
|
31
|
+
pub(crate) struct Shared {
|
|
32
|
+
/// Admits libwebrtc callbacks, and host calls, until the peer closes.
|
|
33
|
+
pub(crate) gate: CallbackGate,
|
|
34
|
+
/// Transport events, each an encoded packet.
|
|
35
|
+
pub(crate) events: Queue<Vec<u8>>,
|
|
36
|
+
pub(crate) video: Queue<VideoItem>,
|
|
37
|
+
pub(crate) audio: Queue<AudioItem>,
|
|
38
|
+
pub(crate) notifier: Notifier,
|
|
39
|
+
/// Declared receive tracks not yet claimed by a remote track.
|
|
40
|
+
bindings: Mutex<Bindings>,
|
|
41
|
+
/// Remote tracks, kept alive so their sinks keep delivering.
|
|
42
|
+
remote_tracks: Mutex<Vec<RemoteTrack>>,
|
|
43
|
+
/// Set once an event is lost, which retires the connection.
|
|
44
|
+
retired: AtomicBool,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// A declared receive track waiting for its remote track.
|
|
48
|
+
#[derive(Debug)]
|
|
49
|
+
struct Binding {
|
|
50
|
+
name: String,
|
|
51
|
+
mid: String,
|
|
52
|
+
/// The track's index in the prepare request.
|
|
53
|
+
index: u32,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Unclaimed bindings per kind, in declaration order.
|
|
57
|
+
///
|
|
58
|
+
/// Pinned reactor-webrtc does not tell a remote track's MID, so a remote
|
|
59
|
+
/// track claims the first unclaimed binding of its kind. The host therefore
|
|
60
|
+
/// declares at most one receive track per kind.
|
|
61
|
+
#[derive(Debug, Default)]
|
|
62
|
+
struct Bindings {
|
|
63
|
+
video: VecDeque<Binding>,
|
|
64
|
+
audio: VecDeque<Binding>,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
impl Shared {
|
|
68
|
+
pub(crate) fn new() -> Self {
|
|
69
|
+
Self {
|
|
70
|
+
gate: CallbackGate::new(),
|
|
71
|
+
events: Queue::new(EVENT_QUEUE_ITEMS, EVENT_QUEUE_BYTES),
|
|
72
|
+
video: Queue::new(VIDEO_QUEUE_FRAMES, VIDEO_QUEUE_BYTES),
|
|
73
|
+
audio: Queue::new(AUDIO_QUEUE_BLOCKS, AUDIO_QUEUE_BYTES),
|
|
74
|
+
notifier: Notifier::default(),
|
|
75
|
+
bindings: Mutex::default(),
|
|
76
|
+
remote_tracks: Mutex::default(),
|
|
77
|
+
retired: AtomicBool::new(false),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Run a libwebrtc callback's body unless the peer has closed. Shutdown
|
|
82
|
+
/// waits for the body to return.
|
|
83
|
+
pub(crate) fn admit(&self, body: impl FnOnce()) {
|
|
84
|
+
if let Some(_running) = self.gate.enter() {
|
|
85
|
+
body();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Queue a transport event for the host. An event that cannot be queued
|
|
90
|
+
/// retires the connection rather than go missing.
|
|
91
|
+
pub(crate) fn emit(&self, event: &Event<'_>) {
|
|
92
|
+
let packet = match event.to_packet() {
|
|
93
|
+
Ok(packet) => packet,
|
|
94
|
+
Err(error) => {
|
|
95
|
+
self.retire(error.class, &error.message);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
match self.events.try_push(packet) {
|
|
100
|
+
Push::Accepted => self.notifier.signal(Ready::Events),
|
|
101
|
+
Push::Closed => {}
|
|
102
|
+
Push::Overflow => self.retire(
|
|
103
|
+
FailureClass::Overflow,
|
|
104
|
+
"native transport event queue overflowed; connection retired",
|
|
105
|
+
),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Report a failure of the connection itself as an `error` event.
|
|
110
|
+
pub(crate) fn emit_error(&self, class: FailureClass, message: &str) {
|
|
111
|
+
self.emit(&Event::error(class, message));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Retire the connection: fence admission and replace the event backlog
|
|
115
|
+
/// with the one diagnostic that says why.
|
|
116
|
+
fn retire(&self, class: FailureClass, message: &str) {
|
|
117
|
+
if self.retired.swap(true, Ordering::AcqRel) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
self.gate.close();
|
|
121
|
+
if let Ok(diagnostic) = Event::error(class, message).to_packet() {
|
|
122
|
+
self.events.replace(diagnostic);
|
|
123
|
+
self.notifier.signal(Ready::Events);
|
|
124
|
+
} else {
|
|
125
|
+
// Without its diagnostic, closed queues still tell the host that
|
|
126
|
+
// the connection is gone.
|
|
127
|
+
self.close_queues();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
pub(crate) fn push_video(&self, frame: VideoItem) {
|
|
132
|
+
if self.video.push_drop_oldest(frame) {
|
|
133
|
+
self.notifier.signal(Ready::Video);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pub(crate) fn push_audio(&self, block: AudioItem) {
|
|
138
|
+
if self.audio.push_drop_oldest(block) {
|
|
139
|
+
self.notifier.signal(Ready::Audio);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Record which declared tracks remote tracks may claim.
|
|
144
|
+
pub(crate) fn set_bindings(&self, mapping: &[Mapping]) {
|
|
145
|
+
let mut bindings = Bindings::default();
|
|
146
|
+
for (index, entry) in (0..).zip(mapping) {
|
|
147
|
+
if entry.direction != Direction::RecvOnly {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
let binding = Binding {
|
|
151
|
+
name: entry.name.clone(),
|
|
152
|
+
mid: entry.mid.clone(),
|
|
153
|
+
index,
|
|
154
|
+
};
|
|
155
|
+
match entry.kind {
|
|
156
|
+
TrackKind::Video => bindings.video.push_back(binding),
|
|
157
|
+
TrackKind::Audio => bindings.audio.push_back(binding),
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
*lock(&self.bindings) = bindings;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn claim_binding(&self, kind: TrackKind) -> Option<Binding> {
|
|
164
|
+
let mut bindings = lock(&self.bindings);
|
|
165
|
+
match kind {
|
|
166
|
+
TrackKind::Video => bindings.video.pop_front(),
|
|
167
|
+
TrackKind::Audio => bindings.audio.pop_front(),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Bind a remote track to a declared receive track and route its decoded
|
|
172
|
+
/// media to the queues. A track nobody declared breaks the negotiated
|
|
173
|
+
/// contract.
|
|
174
|
+
pub(crate) fn accept_remote(self: &Arc<Self>, track: RemoteTrack) {
|
|
175
|
+
let kind = media::kind_of(&track);
|
|
176
|
+
let Some(binding) = self.claim_binding(kind) else {
|
|
177
|
+
self.emit_error(
|
|
178
|
+
FailureClass::Protocol,
|
|
179
|
+
"received native track without a declared receive mapping",
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
};
|
|
183
|
+
media::route(&track, binding.index, self);
|
|
184
|
+
let (name, mid) = (binding.name.as_str(), binding.mid.as_str());
|
|
185
|
+
self.emit(&Event::Track { name, mid });
|
|
186
|
+
self.emit(&Event::Decoded { kind, name, mid });
|
|
187
|
+
lock(&self.remote_tracks).push(track);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Drop the remote tracks, which stops their sinks.
|
|
191
|
+
pub(crate) fn release_remote_tracks(&self) {
|
|
192
|
+
lock(&self.remote_tracks).clear();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
pub(crate) fn snapshot(&self) -> MediaSnapshot {
|
|
196
|
+
let (events, video, audio) = (
|
|
197
|
+
self.events.counts(),
|
|
198
|
+
self.video.counts(),
|
|
199
|
+
self.audio.counts(),
|
|
200
|
+
);
|
|
201
|
+
MediaSnapshot {
|
|
202
|
+
closed: !self.gate.is_open(),
|
|
203
|
+
queued_control: events.queued,
|
|
204
|
+
queued_video: video.queued,
|
|
205
|
+
queued_audio: audio.queued,
|
|
206
|
+
queued_bytes: events.bytes + video.bytes + audio.bytes,
|
|
207
|
+
dropped_video: DecimalU64(video.dropped),
|
|
208
|
+
dropped_audio: DecimalU64(audio.dropped),
|
|
209
|
+
delivered_video: DecimalU64(video.taken),
|
|
210
|
+
delivered_audio: DecimalU64(audio.taken),
|
|
211
|
+
pending_requests: 0,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// Fence admission and discard what is queued: `reactor_effect_peer_close`.
|
|
216
|
+
pub(crate) fn close(&self) {
|
|
217
|
+
self.gate.close();
|
|
218
|
+
self.close_queues();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// Refuse later items, discard queued ones and stop the notifier thread.
|
|
222
|
+
pub(crate) fn close_queues(&self) {
|
|
223
|
+
self.events.close();
|
|
224
|
+
self.video.close();
|
|
225
|
+
self.audio.close();
|
|
226
|
+
self.notifier.close();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[cfg(test)]
|
|
231
|
+
mod tests {
|
|
232
|
+
use super::*;
|
|
233
|
+
use crate::abi::Status;
|
|
234
|
+
use crate::sync::Taken;
|
|
235
|
+
use crate::test_support::parse_packet;
|
|
236
|
+
use serde_json::json;
|
|
237
|
+
|
|
238
|
+
fn mapping(name: &str, kind: TrackKind, direction: Direction) -> Mapping {
|
|
239
|
+
Mapping {
|
|
240
|
+
name: name.into(),
|
|
241
|
+
kind,
|
|
242
|
+
direction,
|
|
243
|
+
mid: format!("mid-{name}"),
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[test]
|
|
248
|
+
fn event_overflow_retires_the_connection_with_one_diagnostic() {
|
|
249
|
+
let shared = Shared::new();
|
|
250
|
+
for index in 0..EVENT_QUEUE_ITEMS {
|
|
251
|
+
let name = index.to_string();
|
|
252
|
+
shared.emit(&Event::Track {
|
|
253
|
+
name: &name,
|
|
254
|
+
mid: "0",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
assert!(shared.gate.is_open());
|
|
258
|
+
|
|
259
|
+
shared.emit(&Event::Ice { candidate: None });
|
|
260
|
+
assert!(
|
|
261
|
+
!shared.gate.is_open(),
|
|
262
|
+
"overflow must retire the connection"
|
|
263
|
+
);
|
|
264
|
+
assert_eq!(shared.events.counts().queued, 1, "the backlog collapses");
|
|
265
|
+
let Taken::Item(packet) = shared.events.take(|_| true) else {
|
|
266
|
+
panic!("the overflow diagnostic must be queued");
|
|
267
|
+
};
|
|
268
|
+
let (header, payload) = parse_packet(&packet);
|
|
269
|
+
assert!(payload.is_empty());
|
|
270
|
+
assert_eq!(header["type"], "error");
|
|
271
|
+
assert_eq!(header["status"], Status::Overflow.code());
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#[test]
|
|
275
|
+
fn remote_tracks_claim_declared_receive_tracks_by_kind_in_order() {
|
|
276
|
+
let shared = Shared::new();
|
|
277
|
+
shared.set_bindings(&[
|
|
278
|
+
mapping("out", TrackKind::Video, Direction::SendOnly),
|
|
279
|
+
mapping("video", TrackKind::Video, Direction::RecvOnly),
|
|
280
|
+
mapping("audio", TrackKind::Audio, Direction::RecvOnly),
|
|
281
|
+
mapping("second", TrackKind::Video, Direction::RecvOnly),
|
|
282
|
+
]);
|
|
283
|
+
let claimed = |kind| {
|
|
284
|
+
shared
|
|
285
|
+
.claim_binding(kind)
|
|
286
|
+
.map(|binding| (binding.name, binding.index))
|
|
287
|
+
};
|
|
288
|
+
assert_eq!(claimed(TrackKind::Audio), Some(("audio".into(), 2)));
|
|
289
|
+
assert_eq!(claimed(TrackKind::Video), Some(("video".into(), 1)));
|
|
290
|
+
assert_eq!(claimed(TrackKind::Video), Some(("second".into(), 3)));
|
|
291
|
+
assert_eq!(
|
|
292
|
+
claimed(TrackKind::Video),
|
|
293
|
+
None,
|
|
294
|
+
"sending tracks are never claimed"
|
|
295
|
+
);
|
|
296
|
+
assert_eq!(claimed(TrackKind::Audio), None);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#[test]
|
|
300
|
+
fn close_fences_events_and_media_and_the_snapshot_says_so() {
|
|
301
|
+
let shared = Shared::new();
|
|
302
|
+
shared.emit(&Event::Ice { candidate: None });
|
|
303
|
+
shared.close();
|
|
304
|
+
shared.emit(&Event::Ice { candidate: None });
|
|
305
|
+
assert_eq!(shared.events.take(|_| true), Taken::Closed);
|
|
306
|
+
let snapshot = serde_json::to_value(shared.snapshot()).unwrap();
|
|
307
|
+
assert_eq!(
|
|
308
|
+
snapshot,
|
|
309
|
+
json!({
|
|
310
|
+
"closed": true,
|
|
311
|
+
"queuedControl": 0,
|
|
312
|
+
"queuedVideo": 0,
|
|
313
|
+
"queuedAudio": 0,
|
|
314
|
+
"queuedBytes": 0,
|
|
315
|
+
"droppedVideo": "0",
|
|
316
|
+
"droppedAudio": "0",
|
|
317
|
+
"deliveredVideo": "0",
|
|
318
|
+
"deliveredAudio": "0",
|
|
319
|
+
"pendingRequests": 0,
|
|
320
|
+
})
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|