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.
Files changed (63) hide show
  1. package/Dockerfile +4 -2
  2. package/README.md +90 -15
  3. package/dist/_internal/bridge.d.ts +70 -14
  4. package/dist/_internal/bridge.d.ts.map +1 -1
  5. package/dist/_internal/bridge.js +290 -145
  6. package/dist/_internal/bridge.js.map +1 -1
  7. package/dist/_internal/isolated/child.d.ts +2 -0
  8. package/dist/_internal/isolated/child.d.ts.map +1 -0
  9. package/dist/_internal/isolated/child.js +176 -0
  10. package/dist/_internal/isolated/child.js.map +1 -0
  11. package/dist/_internal/isolated/host.d.ts +147 -0
  12. package/dist/_internal/isolated/host.d.ts.map +1 -0
  13. package/dist/_internal/isolated/host.js +645 -0
  14. package/dist/_internal/isolated/host.js.map +1 -0
  15. package/dist/_internal/isolated/protocol.d.ts +399 -0
  16. package/dist/_internal/isolated/protocol.d.ts.map +1 -0
  17. package/dist/_internal/isolated/protocol.js +250 -0
  18. package/dist/_internal/isolated/protocol.js.map +1 -0
  19. package/dist/_internal/peer.d.ts +68 -13
  20. package/dist/_internal/peer.d.ts.map +1 -1
  21. package/dist/_internal/peer.js +233 -157
  22. package/dist/_internal/peer.js.map +1 -1
  23. package/dist/index.d.ts +19 -10
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +37 -33
  26. package/dist/index.js.map +1 -1
  27. package/dist/isolated.d.ts +25 -0
  28. package/dist/isolated.d.ts.map +1 -0
  29. package/dist/isolated.js +39 -0
  30. package/dist/isolated.js.map +1 -0
  31. package/lib/darwin-arm64/libreactor_effect_native.dylib +0 -0
  32. package/lib/darwin-arm64/native-identity.json +4 -3
  33. package/lib/linux-x64/libreactor_effect_native.so +0 -0
  34. package/lib/linux-x64/native-identity.json +4 -3
  35. package/package.json +7 -3
  36. package/rust/Cargo.toml +108 -2
  37. package/rust/build.rs +253 -114
  38. package/rust/clippy.toml +7 -0
  39. package/rust/include/reactor_effect_native.h +101 -42
  40. package/rust/src/abi.rs +258 -0
  41. package/rust/src/error.rs +149 -0
  42. package/rust/src/ffi/memory.rs +256 -0
  43. package/rust/src/ffi/tests.rs +719 -0
  44. package/rust/src/ffi.rs +474 -0
  45. package/rust/src/lib.rs +36 -2341
  46. package/rust/src/peer/callbacks.rs +145 -0
  47. package/rust/src/peer/media.rs +221 -0
  48. package/rust/src/peer/owner/tests.rs +188 -0
  49. package/rust/src/peer/owner.rs +353 -0
  50. package/rust/src/peer/shared.rs +323 -0
  51. package/rust/src/peer/tests.rs +513 -0
  52. package/rust/src/peer.rs +189 -0
  53. package/rust/src/protocol/event.rs +238 -0
  54. package/rust/src/protocol/request.rs +347 -0
  55. package/rust/src/protocol/stats.rs +356 -0
  56. package/rust/src/protocol.rs +71 -0
  57. package/rust/src/sync/gate.rs +159 -0
  58. package/rust/src/sync/notifier.rs +136 -0
  59. package/rust/src/sync/queue.rs +384 -0
  60. package/rust/src/sync.rs +20 -0
  61. package/rust/src/test_support.rs +99 -0
  62. package/rust-toolchain.toml +7 -0
  63. 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
+ }