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,145 @@
|
|
|
1
|
+
//! libwebrtc callbacks. Each runs on a libwebrtc thread, is admitted through
|
|
2
|
+
//! the peer's callback gate, and only copies into the shared queues: none
|
|
3
|
+
//! calls or waits on the host.
|
|
4
|
+
|
|
5
|
+
use super::Shared;
|
|
6
|
+
use crate::abi::{Channel, MAX_MESSAGE_BYTES};
|
|
7
|
+
use crate::error::{BridgeError, Classify, FailureClass};
|
|
8
|
+
use crate::protocol::{Event, LocalCandidate, connection_state};
|
|
9
|
+
use reactor_webrtc::{
|
|
10
|
+
DataChannel, DataChannelState, IceGatheringState, PeerConnection, PeerConnectionObserver,
|
|
11
|
+
};
|
|
12
|
+
use std::sync::Arc;
|
|
13
|
+
|
|
14
|
+
/// The connection's observer: state changes, local ICE candidates and remote
|
|
15
|
+
/// tracks become events.
|
|
16
|
+
pub(crate) fn observer(shared: &Arc<Shared>) -> PeerConnectionObserver {
|
|
17
|
+
PeerConnectionObserver::new()
|
|
18
|
+
.on_connection_state_change({
|
|
19
|
+
let shared = Arc::clone(shared);
|
|
20
|
+
move |state| {
|
|
21
|
+
shared.admit(|| {
|
|
22
|
+
shared.emit(&Event::State {
|
|
23
|
+
state: connection_state(state),
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
.on_ice_gathering_change({
|
|
29
|
+
let shared = Arc::clone(shared);
|
|
30
|
+
move |state| {
|
|
31
|
+
shared.admit(|| {
|
|
32
|
+
if state == IceGatheringState::Complete {
|
|
33
|
+
shared.emit(&Event::Ice { candidate: None });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.on_ice_candidate({
|
|
39
|
+
let shared = Arc::clone(shared);
|
|
40
|
+
move |candidate| {
|
|
41
|
+
shared.admit(|| {
|
|
42
|
+
shared.emit(&Event::Ice {
|
|
43
|
+
candidate: Some(LocalCandidate::from(&candidate)),
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
.on_track({
|
|
49
|
+
let shared = Arc::clone(shared);
|
|
50
|
+
move |track| shared.admit(|| shared.accept_remote(track))
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Create one of the bridge's data channels and turn its messages and state
|
|
55
|
+
/// changes into events.
|
|
56
|
+
pub(crate) fn open_channel(
|
|
57
|
+
peer: &PeerConnection,
|
|
58
|
+
channel: Channel,
|
|
59
|
+
shared: &Arc<Shared>,
|
|
60
|
+
) -> Result<DataChannel, BridgeError> {
|
|
61
|
+
let mut data_channel = peer
|
|
62
|
+
.create_data_channel(channel.label())
|
|
63
|
+
.classify(FailureClass::Native, "create_data_channel")?;
|
|
64
|
+
data_channel.on_message({
|
|
65
|
+
let shared = Arc::clone(shared);
|
|
66
|
+
move |bytes, binary| shared.admit(|| receive(&shared, channel, bytes, binary))
|
|
67
|
+
});
|
|
68
|
+
data_channel.on_state_change({
|
|
69
|
+
let shared = Arc::clone(shared);
|
|
70
|
+
move |state| {
|
|
71
|
+
shared.admit(|| {
|
|
72
|
+
let open = match state {
|
|
73
|
+
DataChannelState::Open => true,
|
|
74
|
+
DataChannelState::Closed => false,
|
|
75
|
+
DataChannelState::Connecting | DataChannelState::Closing => return,
|
|
76
|
+
};
|
|
77
|
+
shared.emit(&Event::Channel { channel, open });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
Ok(data_channel)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Pass a message to the host. The bridge's channels carry binary messages
|
|
85
|
+
/// within the local bound; anything else fails the connection.
|
|
86
|
+
fn receive(shared: &Shared, channel: Channel, bytes: &[u8], binary: bool) {
|
|
87
|
+
let label = channel.label();
|
|
88
|
+
if !binary {
|
|
89
|
+
shared.emit_error(
|
|
90
|
+
FailureClass::Protocol,
|
|
91
|
+
&format!("{label} data channel delivered a nonbinary message"),
|
|
92
|
+
);
|
|
93
|
+
} else if bytes.len() > MAX_MESSAGE_BYTES {
|
|
94
|
+
shared.emit_error(
|
|
95
|
+
FailureClass::Overflow,
|
|
96
|
+
&format!("{label} data channel message exceeds local bound"),
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
shared.emit(&Event::Message { channel, bytes });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[cfg(test)]
|
|
104
|
+
mod tests {
|
|
105
|
+
use super::*;
|
|
106
|
+
use crate::abi::Status;
|
|
107
|
+
use crate::sync::Taken;
|
|
108
|
+
use crate::test_support::parse_packet;
|
|
109
|
+
use serde_json::{Value, json};
|
|
110
|
+
|
|
111
|
+
fn next_event(shared: &Shared) -> (Value, Vec<u8>) {
|
|
112
|
+
let Taken::Item(packet) = shared.events.take(|_| true) else {
|
|
113
|
+
panic!("an event must be queued");
|
|
114
|
+
};
|
|
115
|
+
let (header, payload) = parse_packet(&packet);
|
|
116
|
+
(header, payload.to_vec())
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[test]
|
|
120
|
+
fn a_binary_message_within_the_bound_reaches_the_host_unchanged() {
|
|
121
|
+
let shared = Shared::new();
|
|
122
|
+
let message = vec![7; MAX_MESSAGE_BYTES];
|
|
123
|
+
receive(&shared, Channel::Control, &message, true);
|
|
124
|
+
let (header, payload) = next_event(&shared);
|
|
125
|
+
assert_eq!(header, json!({ "type": "message", "channel": "control" }));
|
|
126
|
+
assert_eq!(payload, message);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[test]
|
|
130
|
+
fn a_text_or_oversized_message_fails_the_connection() {
|
|
131
|
+
let shared = Shared::new();
|
|
132
|
+
receive(&shared, Channel::Data, b"text", false);
|
|
133
|
+
receive(
|
|
134
|
+
&shared,
|
|
135
|
+
Channel::Data,
|
|
136
|
+
&vec![0; MAX_MESSAGE_BYTES + 1],
|
|
137
|
+
true,
|
|
138
|
+
);
|
|
139
|
+
let (text, _) = next_event(&shared);
|
|
140
|
+
assert_eq!(text["type"], "error");
|
|
141
|
+
assert_eq!(text["status"], Status::Protocol.code());
|
|
142
|
+
let (oversized, _) = next_event(&shared);
|
|
143
|
+
assert_eq!(oversized["status"], Status::Overflow.code());
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
//! Decoded media: the items the media queues hold, and the sinks that copy
|
|
2
|
+
//! each frame libwebrtc decodes into them.
|
|
3
|
+
|
|
4
|
+
use super::Shared;
|
|
5
|
+
use crate::ffi::{ReactorEffectAudioHeader, ReactorEffectVideoHeader};
|
|
6
|
+
use crate::protocol::TrackKind;
|
|
7
|
+
use crate::sync::QueueItem;
|
|
8
|
+
use reactor_webrtc::{AudioFrame, RemoteTrack, VideoFrame};
|
|
9
|
+
use std::sync::Arc;
|
|
10
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
11
|
+
|
|
12
|
+
/// One decoded BGRA frame and its sender metadata.
|
|
13
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
14
|
+
pub(crate) struct VideoItem {
|
|
15
|
+
/// The track's index in the prepare request.
|
|
16
|
+
pub(crate) track: u32,
|
|
17
|
+
pub(crate) width: u32,
|
|
18
|
+
pub(crate) height: u32,
|
|
19
|
+
/// 0 when the sender supplied none.
|
|
20
|
+
pub(crate) frame_id: u64,
|
|
21
|
+
/// The sender's capture time; 0 when absent.
|
|
22
|
+
pub(crate) timestamp_us: u64,
|
|
23
|
+
/// The frame's admission sequence on its track.
|
|
24
|
+
pub(crate) sequence: u64,
|
|
25
|
+
pub(crate) bgra: Vec<u8>,
|
|
26
|
+
pub(crate) metadata: Vec<u8>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl VideoItem {
|
|
30
|
+
/// Copy a frame that libwebrtc lends only for the duration of its
|
|
31
|
+
/// callback. This is the one native copy of each frame.
|
|
32
|
+
fn copy(track: u32, sequence: u64, frame: VideoFrame<'_>) -> Self {
|
|
33
|
+
let (frame_id, timestamp_us, metadata) = frame
|
|
34
|
+
.metadata
|
|
35
|
+
.map(|metadata| {
|
|
36
|
+
(
|
|
37
|
+
metadata.frame_id,
|
|
38
|
+
metadata.capture_time_us,
|
|
39
|
+
metadata.user_data,
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
.unwrap_or_default();
|
|
43
|
+
Self {
|
|
44
|
+
track,
|
|
45
|
+
width: frame.width,
|
|
46
|
+
height: frame.height,
|
|
47
|
+
frame_id,
|
|
48
|
+
timestamp_us,
|
|
49
|
+
sequence,
|
|
50
|
+
bgra: frame.bgra.to_vec(),
|
|
51
|
+
metadata,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub(crate) fn header(&self) -> ReactorEffectVideoHeader {
|
|
56
|
+
ReactorEffectVideoHeader {
|
|
57
|
+
width: self.width,
|
|
58
|
+
height: self.height,
|
|
59
|
+
data_len: queued_len(self.bgra.len()),
|
|
60
|
+
metadata_len: queued_len(self.metadata.len()),
|
|
61
|
+
frame_id: self.frame_id,
|
|
62
|
+
timestamp_us: self.timestamp_us,
|
|
63
|
+
track: self.track,
|
|
64
|
+
reserved: 0,
|
|
65
|
+
sequence: self.sequence,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
impl QueueItem for VideoItem {
|
|
71
|
+
fn byte_len(&self) -> usize {
|
|
72
|
+
self.bgra.len() + self.metadata.len()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// One block of decoded, interleaved PCM.
|
|
77
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
78
|
+
pub(crate) struct AudioItem {
|
|
79
|
+
/// The track's index in the prepare request.
|
|
80
|
+
pub(crate) track: u32,
|
|
81
|
+
pub(crate) sample_rate: u32,
|
|
82
|
+
pub(crate) channels: u32,
|
|
83
|
+
/// The block's admission sequence on its track.
|
|
84
|
+
pub(crate) sequence: u64,
|
|
85
|
+
pub(crate) pcm: Vec<i16>,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
impl AudioItem {
|
|
89
|
+
/// Copy a block that libwebrtc lends only for the duration of its callback.
|
|
90
|
+
fn copy(track: u32, sequence: u64, frame: &AudioFrame<'_>) -> Self {
|
|
91
|
+
Self {
|
|
92
|
+
track,
|
|
93
|
+
sample_rate: frame.sample_rate,
|
|
94
|
+
channels: frame.channels,
|
|
95
|
+
sequence,
|
|
96
|
+
pcm: frame.pcm.to_vec(),
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
pub(crate) fn header(&self) -> ReactorEffectAudioHeader {
|
|
101
|
+
ReactorEffectAudioHeader {
|
|
102
|
+
sample_rate: self.sample_rate,
|
|
103
|
+
channels: self.channels,
|
|
104
|
+
samples: queued_len(self.pcm.len()),
|
|
105
|
+
track: self.track,
|
|
106
|
+
sequence: self.sequence,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
impl QueueItem for AudioItem {
|
|
112
|
+
fn byte_len(&self) -> usize {
|
|
113
|
+
size_of_val(self.pcm.as_slice())
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// A length for a C header. Only queued items get headers, and a queue refuses
|
|
118
|
+
/// any item over its byte bound, which `Shared` keeps within `u32`: the
|
|
119
|
+
/// saturation never happens.
|
|
120
|
+
fn queued_len(len: usize) -> u32 {
|
|
121
|
+
u32::try_from(len).unwrap_or(u32::MAX)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pub(crate) fn kind_of(track: &RemoteTrack) -> TrackKind {
|
|
125
|
+
match track {
|
|
126
|
+
RemoteTrack::Video(_) => TrackKind::Video,
|
|
127
|
+
RemoteTrack::Audio(_) => TrackKind::Audio,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Copy a remote track's decoded frames into the media queues, labelled with
|
|
132
|
+
/// the declared track's index and numbered in admission order. The number is
|
|
133
|
+
/// taken before the queue can drop the frame, so a reader sees each drop as a
|
|
134
|
+
/// gap. libwebrtc delivers one track's frames on one thread, in order.
|
|
135
|
+
pub(crate) fn route(track: &RemoteTrack, index: u32, shared: &Arc<Shared>) {
|
|
136
|
+
let shared = Arc::clone(shared);
|
|
137
|
+
let admitted = AtomicU64::new(0);
|
|
138
|
+
match track {
|
|
139
|
+
RemoteTrack::Video(video) => video.on_frame(move |frame| {
|
|
140
|
+
let sequence = admitted.fetch_add(1, Ordering::Relaxed);
|
|
141
|
+
shared.admit(|| shared.push_video(VideoItem::copy(index, sequence, frame)));
|
|
142
|
+
}),
|
|
143
|
+
RemoteTrack::Audio(audio) => audio.on_frame(move |frame| {
|
|
144
|
+
let sequence = admitted.fetch_add(1, Ordering::Relaxed);
|
|
145
|
+
shared.admit(|| shared.push_audio(AudioItem::copy(index, sequence, &frame)));
|
|
146
|
+
}),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[cfg(test)]
|
|
151
|
+
mod tests {
|
|
152
|
+
use super::*;
|
|
153
|
+
|
|
154
|
+
#[test]
|
|
155
|
+
fn media_items_count_their_payload_bytes() {
|
|
156
|
+
let video = VideoItem {
|
|
157
|
+
track: 0,
|
|
158
|
+
width: 2,
|
|
159
|
+
height: 1,
|
|
160
|
+
frame_id: 0,
|
|
161
|
+
timestamp_us: 0,
|
|
162
|
+
sequence: 0,
|
|
163
|
+
bgra: vec![0; 8],
|
|
164
|
+
metadata: vec![0; 3],
|
|
165
|
+
};
|
|
166
|
+
let audio = AudioItem {
|
|
167
|
+
track: 1,
|
|
168
|
+
sample_rate: 48_000,
|
|
169
|
+
channels: 2,
|
|
170
|
+
sequence: 0,
|
|
171
|
+
pcm: vec![0; 480],
|
|
172
|
+
};
|
|
173
|
+
assert_eq!(video.byte_len(), 11);
|
|
174
|
+
assert_eq!(audio.byte_len(), 960, "samples are two bytes each");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn headers_describe_the_queued_payload() {
|
|
179
|
+
let video = VideoItem {
|
|
180
|
+
track: 3,
|
|
181
|
+
width: 2,
|
|
182
|
+
height: 1,
|
|
183
|
+
frame_id: u64::MAX,
|
|
184
|
+
timestamp_us: 9_007_199_254_740_993,
|
|
185
|
+
sequence: 41,
|
|
186
|
+
bgra: vec![0x21; 8],
|
|
187
|
+
metadata: b"meta".to_vec(),
|
|
188
|
+
};
|
|
189
|
+
assert_eq!(
|
|
190
|
+
video.header(),
|
|
191
|
+
ReactorEffectVideoHeader {
|
|
192
|
+
width: 2,
|
|
193
|
+
height: 1,
|
|
194
|
+
data_len: 8,
|
|
195
|
+
metadata_len: 4,
|
|
196
|
+
frame_id: u64::MAX,
|
|
197
|
+
timestamp_us: 9_007_199_254_740_993,
|
|
198
|
+
track: 3,
|
|
199
|
+
reserved: 0,
|
|
200
|
+
sequence: 41,
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
let audio = AudioItem {
|
|
204
|
+
track: 1,
|
|
205
|
+
sample_rate: 48_000,
|
|
206
|
+
channels: 2,
|
|
207
|
+
sequence: 7,
|
|
208
|
+
pcm: vec![0; 960],
|
|
209
|
+
};
|
|
210
|
+
assert_eq!(
|
|
211
|
+
audio.header(),
|
|
212
|
+
ReactorEffectAudioHeader {
|
|
213
|
+
sample_rate: 48_000,
|
|
214
|
+
channels: 2,
|
|
215
|
+
samples: 960,
|
|
216
|
+
track: 1,
|
|
217
|
+
sequence: 7,
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
use crate::protocol::TrackKind;
|
|
3
|
+
use crate::sync::Taken;
|
|
4
|
+
use serde_json::{Value, json};
|
|
5
|
+
|
|
6
|
+
fn owner() -> Owner {
|
|
7
|
+
Owner::new(Arc::new(Shared::new()))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
fn prepare_request(tracks: &Value) -> Vec<u8> {
|
|
11
|
+
serde_json::to_vec(&json!({ "servers": [], "tracks": tracks })).unwrap()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// An owner prepared with one receive and one send track.
|
|
15
|
+
fn prepared() -> Owner {
|
|
16
|
+
let mut owner = owner();
|
|
17
|
+
owner
|
|
18
|
+
.prepare(&prepare_request(&json!([
|
|
19
|
+
{ "name": "in", "kind": "video", "direction": "recvonly" },
|
|
20
|
+
{ "name": "out", "kind": "video", "direction": "sendonly" },
|
|
21
|
+
])))
|
|
22
|
+
.expect("prepare");
|
|
23
|
+
owner
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[test]
|
|
27
|
+
fn prepare_offers_every_track_and_maps_each_to_its_mid_in_request_order() {
|
|
28
|
+
let mut owner = owner();
|
|
29
|
+
let response = owner
|
|
30
|
+
.prepare(&prepare_request(&json!([
|
|
31
|
+
{ "name": "video", "kind": "video", "direction": "recvonly" },
|
|
32
|
+
{ "name": "audio", "kind": "audio", "direction": "recvonly" },
|
|
33
|
+
{ "name": "camera", "kind": "video", "direction": "sendonly" },
|
|
34
|
+
])))
|
|
35
|
+
.expect("prepare");
|
|
36
|
+
let response: Value = serde_json::from_slice(&response).unwrap();
|
|
37
|
+
let sdp = response["sdp"].as_str().unwrap();
|
|
38
|
+
assert!(sdp.contains("m=video") && sdp.contains("m=audio"), "{sdp}");
|
|
39
|
+
let mapping: Vec<Mapping> = serde_json::from_value(response["mapping"].clone()).unwrap();
|
|
40
|
+
let declared: Vec<_> = mapping
|
|
41
|
+
.iter()
|
|
42
|
+
.map(|entry| (entry.name.as_str(), entry.kind, entry.direction))
|
|
43
|
+
.collect();
|
|
44
|
+
assert_eq!(
|
|
45
|
+
declared,
|
|
46
|
+
[
|
|
47
|
+
("video", TrackKind::Video, Direction::RecvOnly),
|
|
48
|
+
("audio", TrackKind::Audio, Direction::RecvOnly),
|
|
49
|
+
("camera", TrackKind::Video, Direction::SendOnly),
|
|
50
|
+
]
|
|
51
|
+
);
|
|
52
|
+
for entry in &mapping {
|
|
53
|
+
assert!(sdp.contains(&format!("a=mid:{}", entry.mid)), "{entry:?}");
|
|
54
|
+
}
|
|
55
|
+
owner.shutdown();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[test]
|
|
59
|
+
fn a_peer_prepares_once() {
|
|
60
|
+
let mut owner = prepared();
|
|
61
|
+
let error = owner.prepare(&prepare_request(&json!([]))).unwrap_err();
|
|
62
|
+
assert_eq!(
|
|
63
|
+
error,
|
|
64
|
+
BridgeError::invalid("native peer is already prepared")
|
|
65
|
+
);
|
|
66
|
+
owner.shutdown();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[test]
|
|
70
|
+
fn a_rejected_answer_is_classified_as_sdp_rejected() {
|
|
71
|
+
let owner = prepared();
|
|
72
|
+
let error = owner
|
|
73
|
+
.answer(b"v=0\r\nthis is not an answer\r\n")
|
|
74
|
+
.expect_err("libwebrtc must reject a malformed answer");
|
|
75
|
+
assert_eq!(error.class, FailureClass::SdpRejected);
|
|
76
|
+
assert!(
|
|
77
|
+
error.message.starts_with("set_remote_description: "),
|
|
78
|
+
"{error}"
|
|
79
|
+
);
|
|
80
|
+
owner.shutdown();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#[test]
|
|
84
|
+
fn an_empty_or_non_utf8_answer_is_invalid_input() {
|
|
85
|
+
let owner = prepared();
|
|
86
|
+
assert_eq!(
|
|
87
|
+
owner.answer(b"").unwrap_err(),
|
|
88
|
+
BridgeError::invalid("answer SDP is empty")
|
|
89
|
+
);
|
|
90
|
+
let error = owner.answer(&[0xff, 0xfe]).unwrap_err();
|
|
91
|
+
assert_eq!(error.class, FailureClass::InvalidInput);
|
|
92
|
+
assert!(
|
|
93
|
+
error.message.starts_with("answer SDP is not UTF-8: "),
|
|
94
|
+
"{error}"
|
|
95
|
+
);
|
|
96
|
+
owner.shutdown();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn before_prepare_each_call_fails_with_its_class() {
|
|
101
|
+
let owner = owner();
|
|
102
|
+
assert_eq!(owner.answer(b"v=0").unwrap_err(), BridgeError::closed());
|
|
103
|
+
assert_eq!(owner.stats().unwrap_err(), BridgeError::closed());
|
|
104
|
+
let error = owner
|
|
105
|
+
.set_direction(br#"{"name":"out","active":false}"#)
|
|
106
|
+
.unwrap_err();
|
|
107
|
+
assert_eq!(error, BridgeError::invalid("unknown track: out"));
|
|
108
|
+
let error = owner.send(Channel::Data, b"early").unwrap_err();
|
|
109
|
+
assert_eq!(
|
|
110
|
+
error,
|
|
111
|
+
BridgeError::new(FailureClass::ChannelClosed, "data channel is not open")
|
|
112
|
+
);
|
|
113
|
+
owner.shutdown();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[test]
|
|
117
|
+
fn a_declared_track_pauses_resumes_and_caps_its_bitrate() {
|
|
118
|
+
let owner = prepared();
|
|
119
|
+
for request in [
|
|
120
|
+
br#"{"name":"out","active":false}"#.as_slice(),
|
|
121
|
+
br#"{"name":"out","active":true}"#,
|
|
122
|
+
br#"{"name":"in","active":false}"#,
|
|
123
|
+
br#"{"name":"in","active":true}"#,
|
|
124
|
+
] {
|
|
125
|
+
let response = owner.set_direction(request).expect("direction");
|
|
126
|
+
assert_eq!(response, b"{}");
|
|
127
|
+
}
|
|
128
|
+
let response = owner
|
|
129
|
+
.set_max_bitrate(br#"{"name":"out","bitsPerSecond":2000000}"#)
|
|
130
|
+
.expect("bitrate");
|
|
131
|
+
assert_eq!(response, b"{}");
|
|
132
|
+
owner.shutdown();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[test]
|
|
136
|
+
fn only_a_declared_sending_track_takes_a_bitrate() {
|
|
137
|
+
let owner = prepared();
|
|
138
|
+
assert_eq!(
|
|
139
|
+
owner
|
|
140
|
+
.set_max_bitrate(br#"{"name":"in","bitsPerSecond":1000}"#)
|
|
141
|
+
.unwrap_err(),
|
|
142
|
+
BridgeError::invalid("in is not an outgoing track")
|
|
143
|
+
);
|
|
144
|
+
assert_eq!(
|
|
145
|
+
owner
|
|
146
|
+
.set_max_bitrate(br#"{"name":"missing","bitsPerSecond":1000}"#)
|
|
147
|
+
.unwrap_err(),
|
|
148
|
+
BridgeError::invalid("unknown track: missing")
|
|
149
|
+
);
|
|
150
|
+
assert_eq!(
|
|
151
|
+
owner
|
|
152
|
+
.set_direction(br#"{"name":"missing","active":true}"#)
|
|
153
|
+
.unwrap_err(),
|
|
154
|
+
BridgeError::invalid("unknown track: missing")
|
|
155
|
+
);
|
|
156
|
+
owner.shutdown();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#[test]
|
|
160
|
+
fn a_prepared_peer_reports_stats_before_it_connects() {
|
|
161
|
+
let owner = prepared();
|
|
162
|
+
let stats: Value = serde_json::from_slice(&owner.stats().expect("stats")).unwrap();
|
|
163
|
+
assert!(stats.is_array(), "{stats}");
|
|
164
|
+
owner.shutdown();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
fn commands_queued_behind_close_are_refused() {
|
|
169
|
+
let mut owner = prepared();
|
|
170
|
+
owner.shared.close();
|
|
171
|
+
let refused = owner
|
|
172
|
+
.unless_closed(|_| -> Result<(), BridgeError> { panic!("a closed peer runs no command") });
|
|
173
|
+
assert_eq!(refused, Err(BridgeError::closed()));
|
|
174
|
+
owner.shutdown();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn shutdown_closes_every_queue() {
|
|
179
|
+
let shared = Arc::new(Shared::new());
|
|
180
|
+
let mut owner = Owner::new(Arc::clone(&shared));
|
|
181
|
+
owner
|
|
182
|
+
.prepare(&prepare_request(&json!([])))
|
|
183
|
+
.expect("prepare");
|
|
184
|
+
owner.shutdown();
|
|
185
|
+
assert_eq!(shared.events.take(|_| true), Taken::Closed);
|
|
186
|
+
assert_eq!(shared.video.take(|_| true), Taken::Closed);
|
|
187
|
+
assert_eq!(shared.audio.take(|_| true), Taken::Closed);
|
|
188
|
+
}
|