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,238 @@
1
+ //! Transport events, which the host takes with `reactor_effect_peer_take_event`.
2
+
3
+ use super::TrackKind;
4
+ use crate::abi::Channel;
5
+ use crate::error::{BridgeError, FailureClass};
6
+ use crate::protocol;
7
+ use reactor_webrtc::{IceCandidate, PeerConnectionState};
8
+ use serde::Serialize;
9
+
10
+ /// A transport event for the host.
11
+ ///
12
+ /// On the wire an event is a packet: `[u32 little-endian header length][UTF-8
13
+ /// JSON header][payload]`. The header is this enum, tagged by `type`; only a
14
+ /// [`Event::Message`] has a payload.
15
+ #[derive(Debug, Serialize)]
16
+ #[serde(tag = "type", rename_all = "lowercase")]
17
+ pub(crate) enum Event<'a> {
18
+ /// The aggregate connection state changed.
19
+ State { state: &'static str },
20
+ /// A local ICE candidate, or with none, the end of gathering.
21
+ Ice {
22
+ #[serde(skip_serializing_if = "Option::is_none")]
23
+ candidate: Option<LocalCandidate<'a>>,
24
+ },
25
+ /// A bridge data channel opened or closed.
26
+ Channel { channel: Channel, open: bool },
27
+ /// A binary message arrived on a bridge data channel.
28
+ Message {
29
+ channel: Channel,
30
+ /// The message, which travels as the packet payload.
31
+ #[serde(skip)]
32
+ bytes: &'a [u8],
33
+ },
34
+ /// A remote track arrived for a declared receive mapping.
35
+ Track { name: &'a str, mid: &'a str },
36
+ /// That track's decoded media now reaches its queue.
37
+ Decoded {
38
+ kind: TrackKind,
39
+ name: &'a str,
40
+ mid: &'a str,
41
+ },
42
+ /// The connection failed; `status` is the failure class.
43
+ Error { status: i32, message: &'a str },
44
+ }
45
+
46
+ impl<'a> Event<'a> {
47
+ /// A failure of the connection itself, rather than of a host call.
48
+ pub(crate) fn error(class: FailureClass, message: &'a str) -> Self {
49
+ Self::Error {
50
+ status: class.status().code(),
51
+ message,
52
+ }
53
+ }
54
+
55
+ /// Frame this event as a packet.
56
+ pub(crate) fn to_packet(&self) -> Result<Vec<u8>, BridgeError> {
57
+ let header = protocol::encode(self)?;
58
+ let header_len = u32::try_from(header.len()).map_err(|error| {
59
+ BridgeError::overflow(format!("event header length does not fit a u32: {error}"))
60
+ })?;
61
+ let payload = match self {
62
+ Self::Message { bytes, .. } => bytes,
63
+ Self::State { .. }
64
+ | Self::Ice { .. }
65
+ | Self::Channel { .. }
66
+ | Self::Track { .. }
67
+ | Self::Decoded { .. }
68
+ | Self::Error { .. } => &[][..],
69
+ };
70
+ let mut packet = Vec::with_capacity(4 + header.len() + payload.len());
71
+ packet.extend_from_slice(&header_len.to_le_bytes());
72
+ packet.extend_from_slice(&header);
73
+ packet.extend_from_slice(payload);
74
+ Ok(packet)
75
+ }
76
+ }
77
+
78
+ /// A local ICE candidate, as the host forwards it to signaling.
79
+ #[derive(Debug, Serialize)]
80
+ pub(crate) struct LocalCandidate<'a> {
81
+ candidate: &'a str,
82
+ sdp_mid: Option<&'a str>,
83
+ sdp_mline_index: Option<u16>,
84
+ }
85
+
86
+ impl<'a> From<&'a IceCandidate> for LocalCandidate<'a> {
87
+ fn from(candidate: &'a IceCandidate) -> Self {
88
+ Self {
89
+ candidate: &candidate.candidate,
90
+ sdp_mid: candidate.sdp_mid.as_deref(),
91
+ sdp_mline_index: candidate.sdp_mline_index,
92
+ }
93
+ }
94
+ }
95
+
96
+ /// The name of a connection state in `state` events.
97
+ pub(crate) fn connection_state(state: PeerConnectionState) -> &'static str {
98
+ match state {
99
+ PeerConnectionState::New => "new",
100
+ PeerConnectionState::Connecting => "connecting",
101
+ PeerConnectionState::Connected => "connected",
102
+ PeerConnectionState::Disconnected => "disconnected",
103
+ PeerConnectionState::Failed => "failed",
104
+ PeerConnectionState::Closed => "closed",
105
+ }
106
+ }
107
+
108
+ #[cfg(test)]
109
+ mod tests {
110
+ use super::*;
111
+ use crate::test_support::parse_packet;
112
+ use serde_json::{Value, json};
113
+
114
+ fn header(event: &Event<'_>) -> Value {
115
+ let packet = event.to_packet().unwrap();
116
+ let (header, payload) = parse_packet(&packet);
117
+ assert!(payload.is_empty(), "only a message carries a payload");
118
+ header
119
+ }
120
+
121
+ #[test]
122
+ fn a_packet_is_its_length_prefixed_header_then_the_message_bytes() {
123
+ let payload = [0, 1, 2, 255];
124
+ let event = Event::Message {
125
+ channel: Channel::Data,
126
+ bytes: &payload,
127
+ };
128
+ let packet = event.to_packet().unwrap();
129
+ let header = br#"{"type":"message","channel":"data"}"#;
130
+ assert_eq!(
131
+ packet[..4],
132
+ u32::try_from(header.len()).unwrap().to_le_bytes()
133
+ );
134
+ assert_eq!(packet[4..4 + header.len()], *header);
135
+ assert_eq!(packet[4 + header.len()..], payload);
136
+ }
137
+
138
+ // `parseEvent` in packages/native/src/_internal/peer.ts reads these.
139
+ #[test]
140
+ fn each_event_has_the_header_the_host_parses() {
141
+ let candidate = IceCandidate {
142
+ candidate: "candidate:1 1 udp 1 127.0.0.1 9 typ host".into(),
143
+ sdp_mid: Some("0".into()),
144
+ sdp_mline_index: Some(0),
145
+ };
146
+ let cases = [
147
+ (
148
+ Event::State {
149
+ state: connection_state(PeerConnectionState::Connected),
150
+ },
151
+ json!({ "type": "state", "state": "connected" }),
152
+ ),
153
+ (Event::Ice { candidate: None }, json!({ "type": "ice" })),
154
+ (
155
+ Event::Ice {
156
+ candidate: Some(LocalCandidate::from(&candidate)),
157
+ },
158
+ json!({
159
+ "type": "ice",
160
+ "candidate": {
161
+ "candidate": "candidate:1 1 udp 1 127.0.0.1 9 typ host",
162
+ "sdp_mid": "0",
163
+ "sdp_mline_index": 0,
164
+ },
165
+ }),
166
+ ),
167
+ (
168
+ Event::Channel {
169
+ channel: Channel::Control,
170
+ open: true,
171
+ },
172
+ json!({ "type": "channel", "channel": "control", "open": true }),
173
+ ),
174
+ (
175
+ Event::Track {
176
+ name: "main",
177
+ mid: "2",
178
+ },
179
+ json!({ "type": "track", "name": "main", "mid": "2" }),
180
+ ),
181
+ (
182
+ Event::Decoded {
183
+ kind: TrackKind::Audio,
184
+ name: "main",
185
+ mid: "2",
186
+ },
187
+ json!({ "type": "decoded", "kind": "audio", "name": "main", "mid": "2" }),
188
+ ),
189
+ (
190
+ Event::error(FailureClass::Protocol, "undeclared track"),
191
+ json!({ "type": "error", "status": -4, "message": "undeclared track" }),
192
+ ),
193
+ ];
194
+ for (event, expected) in cases {
195
+ assert_eq!(header(&event), expected);
196
+ }
197
+ }
198
+
199
+ #[test]
200
+ fn a_candidate_without_a_mid_or_index_sends_nulls() {
201
+ let candidate = IceCandidate {
202
+ candidate: "candidate:2".into(),
203
+ sdp_mid: None,
204
+ sdp_mline_index: None,
205
+ };
206
+ let event = Event::Ice {
207
+ candidate: Some(LocalCandidate::from(&candidate)),
208
+ };
209
+ assert_eq!(
210
+ header(&event)["candidate"],
211
+ json!({ "candidate": "candidate:2", "sdp_mid": null, "sdp_mline_index": null })
212
+ );
213
+ }
214
+
215
+ #[test]
216
+ fn every_connection_state_has_a_name_the_host_accepts() {
217
+ let names = [
218
+ PeerConnectionState::New,
219
+ PeerConnectionState::Connecting,
220
+ PeerConnectionState::Connected,
221
+ PeerConnectionState::Disconnected,
222
+ PeerConnectionState::Failed,
223
+ PeerConnectionState::Closed,
224
+ ]
225
+ .map(connection_state);
226
+ assert_eq!(
227
+ names,
228
+ [
229
+ "new",
230
+ "connecting",
231
+ "connected",
232
+ "disconnected",
233
+ "failed",
234
+ "closed"
235
+ ]
236
+ );
237
+ }
238
+ }
@@ -0,0 +1,347 @@
1
+ //! Call requests, their bounds, and the `Prepare` response.
2
+
3
+ use super::decode;
4
+ use crate::error::BridgeError;
5
+ use reactor_webrtc::{IceServer, MediaKind, TransceiverDirection};
6
+ use serde::{Deserialize, Serialize};
7
+ use std::collections::HashSet;
8
+
9
+ /// The most ICE servers a prepare request may name.
10
+ const MAX_ICE_SERVERS: usize = 64;
11
+ /// The most URLs one ICE server may list.
12
+ const MAX_ICE_URLS: usize = 16;
13
+ /// The longest ICE URL, in bytes.
14
+ const MAX_ICE_URL_BYTES: usize = 2048;
15
+ /// The most tracks a prepare request may declare. The host checks the same
16
+ /// bound on the mapping it receives.
17
+ const MAX_TRACKS: usize = 64;
18
+ /// The longest track name, in bytes.
19
+ const MAX_TRACK_NAME_BYTES: usize = 256;
20
+
21
+ /// The media a track carries.
22
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23
+ #[serde(rename_all = "lowercase")]
24
+ pub(crate) enum TrackKind {
25
+ Video,
26
+ Audio,
27
+ }
28
+
29
+ impl From<TrackKind> for MediaKind {
30
+ fn from(kind: TrackKind) -> Self {
31
+ match kind {
32
+ TrackKind::Video => Self::Video,
33
+ TrackKind::Audio => Self::Audio,
34
+ }
35
+ }
36
+ }
37
+
38
+ /// Which way a track's media flows, seen from the bridge.
39
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40
+ #[serde(rename_all = "lowercase")]
41
+ pub(crate) enum Direction {
42
+ /// The remote peer sends and the bridge decodes.
43
+ RecvOnly,
44
+ /// The bridge sends.
45
+ SendOnly,
46
+ }
47
+
48
+ impl From<Direction> for TransceiverDirection {
49
+ fn from(direction: Direction) -> Self {
50
+ match direction {
51
+ Direction::RecvOnly => Self::RecvOnly,
52
+ Direction::SendOnly => Self::SendOnly,
53
+ }
54
+ }
55
+ }
56
+
57
+ /// `Prepare`: the ICE servers to use and the tracks to negotiate, in order.
58
+ #[derive(Debug, Deserialize)]
59
+ pub(crate) struct PrepareRequest {
60
+ pub(crate) servers: Vec<IceServerSpec>,
61
+ pub(crate) tracks: Vec<TrackSpec>,
62
+ }
63
+
64
+ /// A STUN or TURN server.
65
+ #[derive(Debug, Deserialize)]
66
+ pub(crate) struct IceServerSpec {
67
+ urls: Vec<String>,
68
+ #[serde(default)]
69
+ username: String,
70
+ #[serde(default)]
71
+ credential: String,
72
+ }
73
+
74
+ impl From<&IceServerSpec> for IceServer {
75
+ fn from(server: &IceServerSpec) -> Self {
76
+ Self {
77
+ urls: server.urls.clone(),
78
+ username: server.username.clone(),
79
+ password: server.credential.clone(),
80
+ }
81
+ }
82
+ }
83
+
84
+ /// A track to negotiate. Its index in the request identifies it in media
85
+ /// headers.
86
+ #[derive(Debug, Deserialize)]
87
+ pub(crate) struct TrackSpec {
88
+ pub(crate) name: String,
89
+ pub(crate) kind: TrackKind,
90
+ pub(crate) direction: Direction,
91
+ }
92
+
93
+ impl PrepareRequest {
94
+ /// Decode a prepare request and check it against the bridge's bounds.
95
+ pub(crate) fn parse(request: &[u8]) -> Result<Self, BridgeError> {
96
+ let request: Self = decode(request)?;
97
+ request.check_bounds()?;
98
+ Ok(request)
99
+ }
100
+
101
+ fn check_bounds(&self) -> Result<(), BridgeError> {
102
+ if self.servers.len() > MAX_ICE_SERVERS {
103
+ return Err(BridgeError::invalid(format!(
104
+ "at most {MAX_ICE_SERVERS} ICE servers are supported"
105
+ )));
106
+ }
107
+ if self.tracks.len() > MAX_TRACKS {
108
+ return Err(BridgeError::invalid(format!(
109
+ "at most {MAX_TRACKS} tracks are supported"
110
+ )));
111
+ }
112
+ for server in &self.servers {
113
+ if !(1..=MAX_ICE_URLS).contains(&server.urls.len()) {
114
+ return Err(BridgeError::invalid(format!(
115
+ "each ICE server must contain 1..={MAX_ICE_URLS} URLs"
116
+ )));
117
+ }
118
+ if server
119
+ .urls
120
+ .iter()
121
+ .any(|url| url.is_empty() || url.len() > MAX_ICE_URL_BYTES)
122
+ {
123
+ return Err(BridgeError::invalid("invalid ICE URL length"));
124
+ }
125
+ }
126
+ let mut names = HashSet::with_capacity(self.tracks.len());
127
+ for track in &self.tracks {
128
+ let name = track.name.as_str();
129
+ if name.is_empty() || name.len() > MAX_TRACK_NAME_BYTES || name.contains('\0') {
130
+ return Err(BridgeError::invalid("invalid track name"));
131
+ }
132
+ if !names.insert(name) {
133
+ return Err(BridgeError::invalid("duplicate track name"));
134
+ }
135
+ }
136
+ Ok(())
137
+ }
138
+ }
139
+
140
+ /// A declared track and the MID libwebrtc gave its transceiver.
141
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142
+ pub(crate) struct Mapping {
143
+ pub(crate) name: String,
144
+ pub(crate) kind: TrackKind,
145
+ pub(crate) direction: Direction,
146
+ pub(crate) mid: String,
147
+ }
148
+
149
+ /// The `Prepare` response: the local offer and each track's mapping, in
150
+ /// request order.
151
+ #[derive(Debug, Serialize)]
152
+ pub(crate) struct PrepareResponse<'a> {
153
+ pub(crate) sdp: &'a str,
154
+ pub(crate) mapping: &'a [Mapping],
155
+ }
156
+
157
+ /// `Direction`: pause or resume a declared track.
158
+ #[derive(Debug, Deserialize)]
159
+ pub(crate) struct DirectionRequest {
160
+ pub(crate) name: String,
161
+ pub(crate) active: bool,
162
+ }
163
+
164
+ /// `MaxBitrate`: cap an outgoing track's send bitrate.
165
+ #[derive(Debug)]
166
+ pub(crate) struct BitrateRequest {
167
+ pub(crate) name: String,
168
+ /// Always positive, as libwebrtc's `int` requires.
169
+ pub(crate) bits_per_second: i32,
170
+ }
171
+
172
+ impl BitrateRequest {
173
+ pub(crate) fn parse(request: &[u8]) -> Result<Self, BridgeError> {
174
+ #[derive(Deserialize)]
175
+ #[serde(rename_all = "camelCase")]
176
+ struct Json {
177
+ name: String,
178
+ bits_per_second: u32,
179
+ }
180
+
181
+ let Json {
182
+ name,
183
+ bits_per_second,
184
+ } = decode(request)?;
185
+ let bits_per_second = i32::try_from(bits_per_second)
186
+ .ok()
187
+ .filter(|bits| *bits > 0)
188
+ .ok_or_else(|| {
189
+ BridgeError::invalid("bitsPerSecond must be an integer in 1..=2147483647")
190
+ })?;
191
+ Ok(Self {
192
+ name,
193
+ bits_per_second,
194
+ })
195
+ }
196
+ }
197
+
198
+ #[cfg(test)]
199
+ mod tests {
200
+ use super::*;
201
+ use crate::error::FailureClass;
202
+ use serde_json::{Value, json};
203
+
204
+ fn server(urls: usize, url_bytes: usize) -> Value {
205
+ json!({ "urls": vec!["s".repeat(url_bytes); urls], "username": "u", "credential": "c" })
206
+ }
207
+
208
+ fn track(name: &str) -> Value {
209
+ json!({ "name": name, "kind": "video", "direction": "recvonly" })
210
+ }
211
+
212
+ fn prepare(servers: &[Value], tracks: &[Value]) -> Vec<u8> {
213
+ serde_json::to_vec(&json!({ "servers": servers, "tracks": tracks })).unwrap()
214
+ }
215
+
216
+ fn numbered_tracks(count: usize) -> Vec<Value> {
217
+ (0..count)
218
+ .map(|index| track(&format!("t{index}")))
219
+ .collect()
220
+ }
221
+
222
+ #[test]
223
+ fn prepare_accepts_a_request_at_every_bound() {
224
+ let long_name = "n".repeat(MAX_TRACK_NAME_BYTES);
225
+ let mut tracks = numbered_tracks(MAX_TRACKS - 1);
226
+ tracks.push(json!({ "name": long_name, "kind": "audio", "direction": "sendonly" }));
227
+ let request = prepare(
228
+ &vec![server(MAX_ICE_URLS, MAX_ICE_URL_BYTES); MAX_ICE_SERVERS],
229
+ &tracks,
230
+ );
231
+
232
+ let parsed = PrepareRequest::parse(&request).unwrap();
233
+ assert_eq!(parsed.servers.len(), MAX_ICE_SERVERS);
234
+ let last = parsed.tracks.last().unwrap();
235
+ assert_eq!(
236
+ (last.name.len(), last.kind, last.direction),
237
+ (MAX_TRACK_NAME_BYTES, TrackKind::Audio, Direction::SendOnly)
238
+ );
239
+ }
240
+
241
+ #[test]
242
+ fn prepare_rejects_a_request_past_any_bound_as_invalid_input() {
243
+ let cases: [(&str, Vec<u8>); 14] = [
244
+ (
245
+ "too many servers",
246
+ prepare(&vec![server(1, 1); MAX_ICE_SERVERS + 1], &[]),
247
+ ),
248
+ (
249
+ "too many tracks",
250
+ prepare(&[], &numbered_tracks(MAX_TRACKS + 1)),
251
+ ),
252
+ ("a server without URLs", prepare(&[server(0, 1)], &[])),
253
+ (
254
+ "too many URLs",
255
+ prepare(&[server(MAX_ICE_URLS + 1, 1)], &[]),
256
+ ),
257
+ ("an empty URL", prepare(&[server(1, 0)], &[])),
258
+ (
259
+ "an overlong URL",
260
+ prepare(&[server(1, MAX_ICE_URL_BYTES + 1)], &[]),
261
+ ),
262
+ ("an empty track name", prepare(&[], &[track("")])),
263
+ (
264
+ "an overlong track name",
265
+ prepare(&[], &[track(&"n".repeat(MAX_TRACK_NAME_BYTES + 1))]),
266
+ ),
267
+ ("a NUL in a track name", prepare(&[], &[track("a\0b")])),
268
+ (
269
+ "a duplicate track name",
270
+ prepare(&[], &[track("same"), track("same")]),
271
+ ),
272
+ (
273
+ "an unknown kind",
274
+ prepare(
275
+ &[],
276
+ &[json!({ "name": "t", "kind": "data", "direction": "recvonly" })],
277
+ ),
278
+ ),
279
+ (
280
+ "an unknown direction",
281
+ prepare(
282
+ &[],
283
+ &[json!({ "name": "t", "kind": "video", "direction": "sendrecv" })],
284
+ ),
285
+ ),
286
+ ("missing tracks", br#"{"servers":[]}"#.to_vec()),
287
+ ("malformed JSON", b"{".to_vec()),
288
+ ];
289
+ for (case, request) in cases {
290
+ let error = PrepareRequest::parse(&request).expect_err(case);
291
+ assert_eq!(error.class, FailureClass::InvalidInput, "{case}");
292
+ }
293
+ }
294
+
295
+ #[test]
296
+ fn ice_credentials_default_to_empty_strings() {
297
+ let request = prepare(&[json!({ "urls": ["stun:example.org"] })], &[]);
298
+ let parsed = PrepareRequest::parse(&request).unwrap();
299
+ let server = IceServer::from(&parsed.servers[0]);
300
+ assert_eq!(server.urls, ["stun:example.org"]);
301
+ assert_eq!(
302
+ (server.username.as_str(), server.password.as_str()),
303
+ ("", "")
304
+ );
305
+ }
306
+
307
+ #[test]
308
+ fn a_bitrate_must_be_a_positive_c_int() {
309
+ let parse = |bits: Value| {
310
+ let request = json!({ "name": "out", "bitsPerSecond": bits });
311
+ BitrateRequest::parse(&serde_json::to_vec(&request).unwrap())
312
+ };
313
+ assert_eq!(parse(json!(1)).unwrap().bits_per_second, 1);
314
+ assert_eq!(parse(json!(i32::MAX)).unwrap().bits_per_second, i32::MAX);
315
+ for bits in [
316
+ json!(0),
317
+ json!(-1),
318
+ json!(2_147_483_648_u64),
319
+ json!(1.5),
320
+ json!("1"),
321
+ ] {
322
+ let error = parse(bits.clone()).expect_err(&bits.to_string());
323
+ assert_eq!(error.class, FailureClass::InvalidInput, "{bits}");
324
+ }
325
+ }
326
+
327
+ #[test]
328
+ fn mappings_carry_kinds_and_directions_as_the_host_names_them() {
329
+ let mapping = Mapping {
330
+ name: "out".into(),
331
+ kind: TrackKind::Video,
332
+ direction: Direction::SendOnly,
333
+ mid: "1".into(),
334
+ };
335
+ let response = PrepareResponse {
336
+ sdp: "v=0",
337
+ mapping: &[mapping],
338
+ };
339
+ assert_eq!(
340
+ serde_json::to_value(&response).unwrap(),
341
+ json!({
342
+ "sdp": "v=0",
343
+ "mapping": [{ "name": "out", "kind": "video", "direction": "sendonly", "mid": "1" }],
344
+ })
345
+ );
346
+ }
347
+ }