@desklink/host 0.1.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/LICENSE +202 -0
- package/NOTICE +44 -0
- package/README.md +266 -0
- package/bin/desklink-host.mjs +113 -0
- package/dist/bridge.d.ts +57 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +208 -0
- package/dist/bridge.js.map +1 -0
- package/dist/engineProcess.d.ts +82 -0
- package/dist/engineProcess.d.ts.map +1 -0
- package/dist/engineProcess.js +243 -0
- package/dist/engineProcess.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +169 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +18 -0
- package/dist/protocol.js.map +1 -0
- package/dist/resolveEngine.d.ts +34 -0
- package/dist/resolveEngine.d.ts.map +1 -0
- package/dist/resolveEngine.js +103 -0
- package/dist/resolveEngine.js.map +1 -0
- package/docs/BOUNDARY.md +145 -0
- package/docs/PROTOCOL.md +363 -0
- package/engine/Cargo.lock +3077 -0
- package/engine/Cargo.toml +37 -0
- package/engine/build.rs +107 -0
- package/engine/native/inputtino_shim.cpp +11 -0
- package/engine/native/vpx_shim.c +184 -0
- package/engine/native/wrapper.h +7 -0
- package/engine/src/capture.rs +510 -0
- package/engine/src/clipboard.rs +132 -0
- package/engine/src/convert.rs +299 -0
- package/engine/src/encoder.rs +204 -0
- package/engine/src/input.rs +355 -0
- package/engine/src/keymap.rs +338 -0
- package/engine/src/main.rs +614 -0
- package/engine/src/peer.rs +775 -0
- package/engine/src/portal.rs +115 -0
- package/engine/src/protocol.rs +331 -0
- package/engine/src/session.rs +2159 -0
- package/engine/src/x11.rs +244 -0
- package/engine/vendor/inputtino/.clang-format +166 -0
- package/engine/vendor/inputtino/.clang-tidy +30 -0
- package/engine/vendor/inputtino/.dockerignore +11 -0
- package/engine/vendor/inputtino/.github/workflows/mirror-repo.yml +14 -0
- package/engine/vendor/inputtino/.github/workflows/python-lint-format-test-typecheck.yml +124 -0
- package/engine/vendor/inputtino/.github/workflows/python-test-install.yaml +49 -0
- package/engine/vendor/inputtino/.github/workflows/rust.yml +40 -0
- package/engine/vendor/inputtino/CMakeLists.txt +187 -0
- package/engine/vendor/inputtino/LICENSE +21 -0
- package/engine/vendor/inputtino/README.md +110 -0
- package/engine/vendor/inputtino/bindings/python/.devcontainer/Dockerfile +23 -0
- package/engine/vendor/inputtino/bindings/python/.devcontainer/devcontainer.json +28 -0
- package/engine/vendor/inputtino/bindings/python/.python-version +1 -0
- package/engine/vendor/inputtino/bindings/python/CMakeLists.txt +23 -0
- package/engine/vendor/inputtino/bindings/python/CPPLINT.cfg +2 -0
- package/engine/vendor/inputtino/bindings/python/Makefile +49 -0
- package/engine/vendor/inputtino/bindings/python/README.md +215 -0
- package/engine/vendor/inputtino/bindings/python/pyproject.toml +178 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/__init__.py +43 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/_core.pyi +533 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/base.py +63 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/joypad.py +248 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/keyboard.py +256 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/mouse.py +106 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/pentablet.py +73 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/py.typed +0 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/touchscreen.py +61 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/trackpad.py +74 -0
- package/engine/vendor/inputtino/bindings/python/src/main.cpp +217 -0
- package/engine/vendor/inputtino/bindings/python/tests/__init__.py +0 -0
- package/engine/vendor/inputtino/bindings/python/tests/helpers.py +3 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_base.py +60 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_inputtino.py +5 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_joypad.py +332 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_keyboard.py +164 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_mouse.py +185 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_pentablet.py +198 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_touchscreen.py +144 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_trackpad.py +146 -0
- package/engine/vendor/inputtino/bindings/python/uv.lock +390 -0
- package/engine/vendor/inputtino/bindings/rust/Cargo.toml +6 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/.dockerignore +1 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/Cargo.toml +18 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/examples/keyboard.rs +33 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/examples/mouse.rs +17 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/common.rs +116 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/error.rs +17 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad.rs +74 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_nintendo.rs +135 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_ps5.rs +280 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_xbox.rs +134 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/keyboard.rs +76 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/lib.rs +32 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/mouse.rs +178 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/common.rs +38 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/joypads.rs +150 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/keyboard.rs +73 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/mouse.rs +139 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/.dockerignore +1 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/Cargo.toml +12 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/build.rs +48 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/src/lib.rs +5 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/wrapper.h +1 -0
- package/engine/vendor/inputtino/cmake/utils.cmake +9 -0
- package/engine/vendor/inputtino/docker/Dockerfile +70 -0
- package/engine/vendor/inputtino/include/inputtino/input.h +324 -0
- package/engine/vendor/inputtino/include/inputtino/input.hpp +466 -0
- package/engine/vendor/inputtino/include/inputtino/result.hpp +37 -0
- package/engine/vendor/inputtino/share/pkgconfig/libinputtino.pc.in +17 -0
- package/engine/vendor/inputtino/src/c-bindings/helpers.hpp +19 -0
- package/engine/vendor/inputtino/src/c-bindings/joypad_ps5.cpp +115 -0
- package/engine/vendor/inputtino/src/c-bindings/joypad_switch.cpp +63 -0
- package/engine/vendor/inputtino/src/c-bindings/joypad_xone.cpp +62 -0
- package/engine/vendor/inputtino/src/c-bindings/keyboard.cpp +42 -0
- package/engine/vendor/inputtino/src/c-bindings/mouse.cpp +67 -0
- package/engine/vendor/inputtino/src/c-bindings/pen_tablet.cpp +50 -0
- package/engine/vendor/inputtino/src/c-bindings/touchscreen.cpp +43 -0
- package/engine/vendor/inputtino/src/c-bindings/trackpad.cpp +49 -0
- package/engine/vendor/inputtino/src/uhid/README.adoc +63 -0
- package/engine/vendor/inputtino/src/uhid/include/crc32.hpp +38 -0
- package/engine/vendor/inputtino/src/uhid/include/uhid/protected_types.hpp +36 -0
- package/engine/vendor/inputtino/src/uhid/include/uhid/ps5.hpp +550 -0
- package/engine/vendor/inputtino/src/uhid/include/uhid/uhid.hpp +168 -0
- package/engine/vendor/inputtino/src/uhid/joypad_ps5.cpp +580 -0
- package/engine/vendor/inputtino/src/uinput/include/inputtino/keyboard.hpp +84 -0
- package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_ps5_types.hpp +7 -0
- package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_types.hpp +110 -0
- package/engine/vendor/inputtino/src/uinput/joypad_nintendo.cpp +190 -0
- package/engine/vendor/inputtino/src/uinput/joypad_ps.cpp +203 -0
- package/engine/vendor/inputtino/src/uinput/joypad_utils.hpp +246 -0
- package/engine/vendor/inputtino/src/uinput/joypad_xbox.cpp +190 -0
- package/engine/vendor/inputtino/src/uinput/keyboard.cpp +120 -0
- package/engine/vendor/inputtino/src/uinput/mouse.cpp +199 -0
- package/engine/vendor/inputtino/src/uinput/pentablet.cpp +162 -0
- package/engine/vendor/inputtino/src/uinput/touchscreen.cpp +140 -0
- package/engine/vendor/inputtino/src/uinput/trackpad.cpp +192 -0
- package/examples/reference-client.html +428 -0
- package/package.json +38 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
//! The WebRTC handoff: one VP9 video track out, one control data channel in.
|
|
2
|
+
//!
|
|
3
|
+
//! The engine is the offerer. The consumer's own authenticated channel carries
|
|
4
|
+
//! the SDP and ICE candidates to the client and brings the answer back; this
|
|
5
|
+
//! module never assumes anything about that channel, which is what keeps the
|
|
6
|
+
//! engine usable by an application that is not this one.
|
|
7
|
+
//!
|
|
8
|
+
//! Input and clipboard ride the session's data channel rather than the local
|
|
9
|
+
//! protocol. The channel is inside the DTLS/SRTP session the engine created for
|
|
10
|
+
//! one authorized grant, so it inherits that session's identity and dies with
|
|
11
|
+
//! it — a revoked session cannot be driven by a client that kept a socket open.
|
|
12
|
+
|
|
13
|
+
use anyhow::{Context, Result};
|
|
14
|
+
use rtc::interceptor::{
|
|
15
|
+
Attribute, Interceptor, PacerBuilder, Packet, Registry, Slot, StreamInfo, TaggedPacket,
|
|
16
|
+
};
|
|
17
|
+
use rtc::media_stream::MediaStreamTrack;
|
|
18
|
+
use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors;
|
|
19
|
+
use rtc::peer_connection::configuration::media_engine::{MediaEngine, MIME_TYPE_VP9};
|
|
20
|
+
use rtc::peer_connection::configuration::RTCConfigurationBuilder;
|
|
21
|
+
use rtc::peer_connection::event::RTCPeerConnectionIceEvent;
|
|
22
|
+
use rtc::peer_connection::sdp::RTCSessionDescription;
|
|
23
|
+
use rtc::peer_connection::transport::{RTCIceCandidateInit, RTCIceServer};
|
|
24
|
+
use rtc::rtcp::payload_feedbacks::full_intra_request::FullIntraRequest;
|
|
25
|
+
use rtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication;
|
|
26
|
+
use rtc::rtcp::receiver_report::ReceiverReport;
|
|
27
|
+
use rtc::rtp_transceiver::rtp_sender::{
|
|
28
|
+
RTCRtpCodec, RTCRtpCodecParameters, RTCRtpCodingParameters, RTCRtpEncodingParameters,
|
|
29
|
+
RtpCodecKind,
|
|
30
|
+
};
|
|
31
|
+
use rtc::rtp_transceiver::PayloadType;
|
|
32
|
+
use rtc::sansio::Protocol;
|
|
33
|
+
use std::collections::VecDeque;
|
|
34
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
35
|
+
use std::sync::{Arc, Mutex};
|
|
36
|
+
use std::time::{Duration, Instant};
|
|
37
|
+
use webrtc::data_channel::{DataChannel, DataChannelEvent};
|
|
38
|
+
use webrtc::media_stream::track_local::static_rtp::TrackLocalStaticRTP;
|
|
39
|
+
use webrtc::media_stream::track_local::{TrackLocal, TrackLocalEvent};
|
|
40
|
+
use webrtc::peer_connection::{
|
|
41
|
+
PeerConnection, PeerConnectionBuilder, PeerConnectionEventHandler, RTCPeerConnectionState,
|
|
42
|
+
};
|
|
43
|
+
use webrtc::runtime::default_runtime;
|
|
44
|
+
|
|
45
|
+
/// VP9 clock rate, fixed by RFC 9628.
|
|
46
|
+
const VP9_CLOCK_RATE: u32 = 90_000;
|
|
47
|
+
|
|
48
|
+
/// Payload type the engine offers VP9 on. Any value in the dynamic range works;
|
|
49
|
+
/// this one is the common convention and keeps packet captures readable.
|
|
50
|
+
pub const VP9_PAYLOAD_TYPE: PayloadType = 98;
|
|
51
|
+
|
|
52
|
+
fn vp9_codec() -> RTCRtpCodecParameters {
|
|
53
|
+
RTCRtpCodecParameters {
|
|
54
|
+
rtp_codec: RTCRtpCodec {
|
|
55
|
+
mime_type: MIME_TYPE_VP9.to_owned(),
|
|
56
|
+
clock_rate: VP9_CLOCK_RATE,
|
|
57
|
+
channels: 0,
|
|
58
|
+
sdp_fmtp_line: String::new(),
|
|
59
|
+
rtcp_feedback: Vec::new(),
|
|
60
|
+
},
|
|
61
|
+
payload_type: VP9_PAYLOAD_TYPE,
|
|
62
|
+
..Default::default()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// What the consumer needs to know about a live peer.
|
|
67
|
+
#[derive(Debug, Clone)]
|
|
68
|
+
pub enum PeerEvent {
|
|
69
|
+
/// A local ICE candidate to forward to the client.
|
|
70
|
+
Candidate {
|
|
71
|
+
candidate: String,
|
|
72
|
+
sdp_mid: Option<String>,
|
|
73
|
+
sdp_m_line_index: Option<u16>,
|
|
74
|
+
},
|
|
75
|
+
/// The transport reached a new state.
|
|
76
|
+
State(RTCPeerConnectionState),
|
|
77
|
+
/// The control channel opened; input may now be accepted.
|
|
78
|
+
ControlOpen,
|
|
79
|
+
/// The control channel closed.
|
|
80
|
+
ControlClosed,
|
|
81
|
+
/// A control message arrived from the client.
|
|
82
|
+
ControlMessage(String),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Transport settings the consumer chose for this session.
|
|
86
|
+
pub struct TransportOptions {
|
|
87
|
+
pub ice_servers: Vec<(String, Option<String>, Option<String>)>,
|
|
88
|
+
/// Listen for ICE over TCP on the loopback too. A phone that reaches this
|
|
89
|
+
/// computer only through SSH has no UDP path; it forwards this port over
|
|
90
|
+
/// the SSH connection it already has, and the picture rides that.
|
|
91
|
+
pub loopback_tcp: bool,
|
|
92
|
+
/// The rate video packets are released at, in bits per second.
|
|
93
|
+
pub pace_bps: f64,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// What the pacer lets go back to back: a few dozen packets. A 4K key frame is
|
|
97
|
+
/// hundreds of packets, and released at once it overflows the receiver's
|
|
98
|
+
/// socket buffer, which loses the frame and draws another key frame request —
|
|
99
|
+
/// a loop that never shows a sharp picture.
|
|
100
|
+
const PACE_BURST_BITS: f64 = 256_000.0;
|
|
101
|
+
|
|
102
|
+
struct Handler {
|
|
103
|
+
events: tokio::sync::mpsc::UnboundedSender<PeerEvent>,
|
|
104
|
+
wants_keyframe: Arc<AtomicBool>,
|
|
105
|
+
connected: Arc<AtomicBool>,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Watch the control channel the engine created.
|
|
109
|
+
///
|
|
110
|
+
/// The engine is the offerer, so it is the side that creates this channel, and
|
|
111
|
+
/// `on_data_channel` only fires for a channel the *remote* opened. Serving our
|
|
112
|
+
/// own channel is what makes the session two-way: a client that creates its own
|
|
113
|
+
/// channel with the same label gets an unrelated stream, its requests arrive
|
|
114
|
+
/// here and every reply lands on a channel nobody is reading.
|
|
115
|
+
async fn serve_control(
|
|
116
|
+
channel: Arc<dyn DataChannel>,
|
|
117
|
+
events: tokio::sync::mpsc::UnboundedSender<PeerEvent>,
|
|
118
|
+
) {
|
|
119
|
+
while let Some(event) = channel.poll().await {
|
|
120
|
+
match event {
|
|
121
|
+
DataChannelEvent::OnOpen => {
|
|
122
|
+
let _ = events.send(PeerEvent::ControlOpen);
|
|
123
|
+
}
|
|
124
|
+
DataChannelEvent::OnMessage(message) => {
|
|
125
|
+
if let Ok(text) = String::from_utf8(message.data.to_vec()) {
|
|
126
|
+
let _ = events.send(PeerEvent::ControlMessage(text));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
DataChannelEvent::OnClose => {
|
|
130
|
+
let _ = events.send(PeerEvent::ControlClosed);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
_ => {}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
let _ = events.send(PeerEvent::ControlClosed);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[async_trait::async_trait]
|
|
140
|
+
impl PeerConnectionEventHandler for Handler {
|
|
141
|
+
async fn on_ice_candidate(&self, event: RTCPeerConnectionIceEvent) {
|
|
142
|
+
if let Ok(init) = event.candidate.to_json() {
|
|
143
|
+
let _ = self.events.send(PeerEvent::Candidate {
|
|
144
|
+
candidate: init.candidate,
|
|
145
|
+
sdp_mid: init.sdp_mid,
|
|
146
|
+
sdp_m_line_index: init.sdp_mline_index,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async fn on_connection_state_change(&self, state: RTCPeerConnectionState) {
|
|
152
|
+
let connected = matches!(state, RTCPeerConnectionState::Connected);
|
|
153
|
+
if connected {
|
|
154
|
+
self.wants_keyframe.store(true, Ordering::SeqCst);
|
|
155
|
+
}
|
|
156
|
+
self.connected.store(connected, Ordering::SeqCst);
|
|
157
|
+
let _ = self.events.send(PeerEvent::State(state));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async fn on_data_channel(&self, _channel: Arc<dyn DataChannel>) {
|
|
161
|
+
// The engine's own channel is served by `serve_control`. A channel the
|
|
162
|
+
// remote opens is a second, unrelated stream and is deliberately not
|
|
163
|
+
// adopted: two control channels would mean two ways to drive one
|
|
164
|
+
// desktop, which is the ambiguity this protocol exists to avoid.
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Passes the application the inbound RTCP only it can act on: a receiver
|
|
169
|
+
/// asking for a key frame (PLI, FIR), and receiver reports, whose loss figure
|
|
170
|
+
/// sets how much the encoder may send. Everything else stays with the
|
|
171
|
+
/// interceptors that consume it (NACK, reports), and so does a copy of these:
|
|
172
|
+
/// this sits last, after all of them.
|
|
173
|
+
#[derive(Default)]
|
|
174
|
+
struct FeedbackForwarder {
|
|
175
|
+
read: VecDeque<TaggedPacket>,
|
|
176
|
+
write: VecDeque<TaggedPacket>,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn is_feedback(packet: &Box<dyn rtc::rtcp::Packet>) -> bool {
|
|
180
|
+
let packet = packet.as_any();
|
|
181
|
+
packet.is::<PictureLossIndication>()
|
|
182
|
+
|| packet.is::<FullIntraRequest>()
|
|
183
|
+
|| packet.is::<ReceiverReport>()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
impl Protocol<TaggedPacket, TaggedPacket, ()> for FeedbackForwarder {
|
|
187
|
+
type Rout = TaggedPacket;
|
|
188
|
+
type Wout = TaggedPacket;
|
|
189
|
+
type Eout = ();
|
|
190
|
+
type Error = rtc::shared::error::Error;
|
|
191
|
+
type Time = Instant;
|
|
192
|
+
|
|
193
|
+
fn handle_read(&mut self, mut msg: TaggedPacket) -> std::result::Result<(), Self::Error> {
|
|
194
|
+
if let Packet::Rtcp(packets) = &msg.message.packet {
|
|
195
|
+
let wanted: Vec<Box<dyn rtc::rtcp::Packet>> = packets
|
|
196
|
+
.iter()
|
|
197
|
+
.filter(|packet| is_feedback(packet))
|
|
198
|
+
.cloned()
|
|
199
|
+
.collect();
|
|
200
|
+
if wanted.is_empty() {
|
|
201
|
+
return Ok(());
|
|
202
|
+
}
|
|
203
|
+
msg.message.packet = Packet::Rtcp(wanted);
|
|
204
|
+
// Inbound RTCP ends at the chain's last stage unless marked for us.
|
|
205
|
+
msg.message.add(Attribute::DeliverToApplication);
|
|
206
|
+
}
|
|
207
|
+
self.read.push_back(msg);
|
|
208
|
+
Ok(())
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fn poll_read(&mut self) -> Option<Self::Rout> {
|
|
212
|
+
self.read.pop_front()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fn handle_write(&mut self, msg: TaggedPacket) -> std::result::Result<(), Self::Error> {
|
|
216
|
+
self.write.push_back(msg);
|
|
217
|
+
Ok(())
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn poll_write(&mut self) -> Option<Self::Wout> {
|
|
221
|
+
self.write.pop_front()
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
impl Interceptor for FeedbackForwarder {
|
|
226
|
+
fn bind_local_stream(&mut self, _info: &StreamInfo) {}
|
|
227
|
+
fn unbind_local_stream(&mut self, _info: &StreamInfo) {}
|
|
228
|
+
fn bind_remote_stream(&mut self, _info: &StreamInfo) {}
|
|
229
|
+
fn unbind_remote_stream(&mut self, _info: &StreamInfo) {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// Read the video sender's feedback until the peer closes it (by aborting this
|
|
233
|
+
/// task: the track's feedback channel outlives a closed peer): key frame
|
|
234
|
+
/// requests set `wants_keyframe`, receiver reports queue their loss fraction.
|
|
235
|
+
async fn serve_feedback(
|
|
236
|
+
track: Arc<TrackLocalStaticRTP>,
|
|
237
|
+
ssrc: u32,
|
|
238
|
+
wants_keyframe: Arc<AtomicBool>,
|
|
239
|
+
loss: Arc<Mutex<Vec<u8>>>,
|
|
240
|
+
) {
|
|
241
|
+
loop {
|
|
242
|
+
// The track is bound once the answer is applied; until then, and after
|
|
243
|
+
// it is unbound, there is nothing to read.
|
|
244
|
+
let Some(TrackLocalEvent::OnRtcpPacket(packets)) = track.poll().await else {
|
|
245
|
+
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
246
|
+
continue;
|
|
247
|
+
};
|
|
248
|
+
for packet in &packets {
|
|
249
|
+
let packet = packet.as_any();
|
|
250
|
+
if packet.is::<PictureLossIndication>() || packet.is::<FullIntraRequest>() {
|
|
251
|
+
wants_keyframe.store(true, Ordering::SeqCst);
|
|
252
|
+
} else if let Some(report) = packet.downcast_ref::<ReceiverReport>() {
|
|
253
|
+
let mut queue = lock(&loss);
|
|
254
|
+
queue.extend(
|
|
255
|
+
report
|
|
256
|
+
.reports
|
|
257
|
+
.iter()
|
|
258
|
+
.filter(|r| r.ssrc == ssrc)
|
|
259
|
+
.map(|r| r.fraction_lost),
|
|
260
|
+
);
|
|
261
|
+
let excess = queue.len().saturating_sub(64);
|
|
262
|
+
queue.drain(..excess);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// The largest RTP packet the engine sends, headers included: under every
|
|
269
|
+
/// path MTU a desktop stream meets, DTLS/SRTP and TURN overhead included.
|
|
270
|
+
const RTP_MTU: usize = 1200;
|
|
271
|
+
const RTP_HEADER: usize = 12;
|
|
272
|
+
|
|
273
|
+
/// VP9 over RTP (RFC 9628) in flexible mode, one layer.
|
|
274
|
+
///
|
|
275
|
+
/// The payload descriptor is what a receiver builds its reference graph from,
|
|
276
|
+
/// so it has to tell the truth about each frame: a key frame is not
|
|
277
|
+
/// inter-predicted, and every other frame is, with the previous picture as its
|
|
278
|
+
/// reference. A descriptor that marks every frame as independent (as a generic
|
|
279
|
+
/// payloader does) makes the receiver drop the frames that are not really
|
|
280
|
+
/// independent and ask for key frames instead — the picture only moves on key
|
|
281
|
+
/// frames, which is exactly the failure a desktop cannot hide.
|
|
282
|
+
struct Vp9Packetizer {
|
|
283
|
+
sequence: u16,
|
|
284
|
+
picture_id: u16,
|
|
285
|
+
timestamp_base: u32,
|
|
286
|
+
started: Instant,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
impl Vp9Packetizer {
|
|
290
|
+
fn new() -> Self {
|
|
291
|
+
Self {
|
|
292
|
+
sequence: rand::random(),
|
|
293
|
+
picture_id: rand::random::<u16>() & 0x7fff,
|
|
294
|
+
timestamp_base: rand::random(),
|
|
295
|
+
started: Instant::now(),
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
fn packetize(
|
|
300
|
+
&mut self,
|
|
301
|
+
frame: &[u8],
|
|
302
|
+
keyframe: bool,
|
|
303
|
+
captured: Instant,
|
|
304
|
+
ssrc: u32,
|
|
305
|
+
payload_type: PayloadType,
|
|
306
|
+
) -> Vec<rtc::rtp::Packet> {
|
|
307
|
+
// The RTP clock is the capture clock, so the receiver paces playout by
|
|
308
|
+
// when frames were taken, not by when they happened to be sent.
|
|
309
|
+
// Wraps, as the RTP clock does, rather than saturating after 13 hours.
|
|
310
|
+
let ticks = (captured
|
|
311
|
+
.saturating_duration_since(self.started)
|
|
312
|
+
.as_secs_f64()
|
|
313
|
+
* VP9_CLOCK_RATE as f64) as u64 as u32;
|
|
314
|
+
let timestamp = self.timestamp_base.wrapping_add(ticks);
|
|
315
|
+
let descriptor = if keyframe { 3 } else { 4 };
|
|
316
|
+
let chunks: Vec<&[u8]> = frame.chunks(RTP_MTU - RTP_HEADER - descriptor).collect();
|
|
317
|
+
let last = chunks.len().saturating_sub(1);
|
|
318
|
+
let packets = chunks
|
|
319
|
+
.into_iter()
|
|
320
|
+
.enumerate()
|
|
321
|
+
.map(|(index, chunk)| {
|
|
322
|
+
let mut payload = bytes::BytesMut::with_capacity(descriptor + chunk.len());
|
|
323
|
+
// I (picture id) and F (flexible mode) always; P for an inter
|
|
324
|
+
// frame; B and E mark the frame's first and last packet.
|
|
325
|
+
let mut first = 0x80 | 0x10;
|
|
326
|
+
if !keyframe {
|
|
327
|
+
first |= 0x40;
|
|
328
|
+
}
|
|
329
|
+
if index == 0 {
|
|
330
|
+
first |= 0x08;
|
|
331
|
+
}
|
|
332
|
+
if index == last {
|
|
333
|
+
first |= 0x04;
|
|
334
|
+
}
|
|
335
|
+
payload.extend_from_slice(&[
|
|
336
|
+
first,
|
|
337
|
+
0x80 | (self.picture_id >> 8) as u8,
|
|
338
|
+
self.picture_id as u8,
|
|
339
|
+
]);
|
|
340
|
+
if !keyframe {
|
|
341
|
+
// One reference: the previous picture (P_DIFF 1, N 0).
|
|
342
|
+
payload.extend_from_slice(&[1 << 1]);
|
|
343
|
+
}
|
|
344
|
+
payload.extend_from_slice(chunk);
|
|
345
|
+
let sequence_number = self.sequence;
|
|
346
|
+
self.sequence = self.sequence.wrapping_add(1);
|
|
347
|
+
rtc::rtp::Packet {
|
|
348
|
+
header: rtc::rtp::Header {
|
|
349
|
+
version: 2,
|
|
350
|
+
marker: index == last,
|
|
351
|
+
payload_type,
|
|
352
|
+
sequence_number,
|
|
353
|
+
timestamp,
|
|
354
|
+
ssrc,
|
|
355
|
+
..Default::default()
|
|
356
|
+
},
|
|
357
|
+
payload: payload.freeze(),
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
.collect();
|
|
361
|
+
self.picture_id = (self.picture_id + 1) & 0x7fff;
|
|
362
|
+
packets
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
pub struct VideoPeer {
|
|
367
|
+
peer: Arc<dyn PeerConnection>,
|
|
368
|
+
/// Set when the far end needs a fresh reference frame.
|
|
369
|
+
///
|
|
370
|
+
/// The encoder's first frame is a key frame, but the far end is not
|
|
371
|
+
/// receiving yet: ICE is still negotiating, and the decoder therefore starts
|
|
372
|
+
/// mid-stream without the reference the following inter frames need. Asking
|
|
373
|
+
/// for a key frame *when the transport connects* is what makes the picture
|
|
374
|
+
/// appear at all; without it the browser reports frames received and zero
|
|
375
|
+
/// frames decoded, which is exactly the failure this was.
|
|
376
|
+
wants_keyframe: Arc<AtomicBool>,
|
|
377
|
+
/// True while the transport is connected: frames sent before it are lost.
|
|
378
|
+
connected: Arc<AtomicBool>,
|
|
379
|
+
track: Arc<TrackLocalStaticRTP>,
|
|
380
|
+
packetizer: Mutex<Vp9Packetizer>,
|
|
381
|
+
ssrc: u32,
|
|
382
|
+
control: Arc<dyn DataChannel>,
|
|
383
|
+
payload_type: PayloadType,
|
|
384
|
+
/// Loss fractions (RFC 3550, /256) the far end reported, oldest first.
|
|
385
|
+
loss: Arc<Mutex<Vec<u8>>>,
|
|
386
|
+
feedback: tokio::task::JoinHandle<()>,
|
|
387
|
+
/// The engine is the offerer, so a candidate can arrive before the answer
|
|
388
|
+
/// that supplies its remote description; those are held here until it does.
|
|
389
|
+
candidates: Mutex<PendingCandidates>,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
#[derive(Default)]
|
|
393
|
+
struct PendingCandidates {
|
|
394
|
+
ready: bool,
|
|
395
|
+
held: Vec<RTCIceCandidateInit>,
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
impl VideoPeer {
|
|
399
|
+
/// Build the peer, add the video track and the control channel, and produce
|
|
400
|
+
/// the offer for the consumer to carry to the client.
|
|
401
|
+
pub async fn offer(
|
|
402
|
+
options: TransportOptions,
|
|
403
|
+
events: tokio::sync::mpsc::UnboundedSender<PeerEvent>,
|
|
404
|
+
) -> Result<(Self, String)> {
|
|
405
|
+
let runtime = default_runtime().context("no WebRTC runtime is enabled")?;
|
|
406
|
+
|
|
407
|
+
let mut media_engine = MediaEngine::default();
|
|
408
|
+
let video_codec = vp9_codec();
|
|
409
|
+
media_engine
|
|
410
|
+
.register_codec(video_codec.clone(), RtpCodecKind::Video)
|
|
411
|
+
.context("failed to offer VP9")?;
|
|
412
|
+
let registry = register_default_interceptors(Registry::new(), &mut media_engine)?
|
|
413
|
+
.with(
|
|
414
|
+
Slot::Pacer,
|
|
415
|
+
PacerBuilder::new()
|
|
416
|
+
.with_target_bitrate(options.pace_bps)
|
|
417
|
+
.with_burst_bits(PACE_BURST_BITS)
|
|
418
|
+
.build(),
|
|
419
|
+
)
|
|
420
|
+
// Last, so every interceptor has seen the whole of the inbound RTCP.
|
|
421
|
+
.with(Slot::from(14_000), FeedbackForwarder::default());
|
|
422
|
+
|
|
423
|
+
let mut builder = RTCConfigurationBuilder::new();
|
|
424
|
+
if !options.ice_servers.is_empty() {
|
|
425
|
+
builder = builder.with_ice_servers(
|
|
426
|
+
options
|
|
427
|
+
.ice_servers
|
|
428
|
+
.iter()
|
|
429
|
+
.map(|(url, username, credential)| RTCIceServer {
|
|
430
|
+
urls: vec![url.clone()],
|
|
431
|
+
username: username.clone().unwrap_or_default(),
|
|
432
|
+
credential: credential.clone().unwrap_or_default(),
|
|
433
|
+
})
|
|
434
|
+
.collect(),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
let wants_keyframe = Arc::new(AtomicBool::new(true));
|
|
438
|
+
let connected = Arc::new(AtomicBool::new(false));
|
|
439
|
+
let control_events = events.clone();
|
|
440
|
+
let peer = PeerConnectionBuilder::<std::net::SocketAddr>::new()
|
|
441
|
+
.with_configuration(builder.build())
|
|
442
|
+
.with_media_engine(media_engine)
|
|
443
|
+
.with_interceptor_registry(registry)
|
|
444
|
+
.with_handler(Arc::new(Handler {
|
|
445
|
+
events,
|
|
446
|
+
wants_keyframe: wants_keyframe.clone(),
|
|
447
|
+
connected: connected.clone(),
|
|
448
|
+
}))
|
|
449
|
+
.with_runtime(runtime)
|
|
450
|
+
// An ephemeral port on every interface: ICE needs a socket to gather
|
|
451
|
+
// candidates from, and the peer needs no fixed port because the
|
|
452
|
+
// consumer's authenticated channel carries the candidates.
|
|
453
|
+
.with_udp_addrs(vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))])
|
|
454
|
+
.with_tcp_addrs(if options.loopback_tcp {
|
|
455
|
+
vec![std::net::SocketAddr::from(([127, 0, 0, 1], 0))]
|
|
456
|
+
} else {
|
|
457
|
+
Vec::new()
|
|
458
|
+
})
|
|
459
|
+
.build()
|
|
460
|
+
.await
|
|
461
|
+
.context("failed to create the peer connection")?;
|
|
462
|
+
let peer: Arc<dyn PeerConnection> = Arc::new(peer);
|
|
463
|
+
|
|
464
|
+
let ssrc = rand::random::<u32>();
|
|
465
|
+
let track: Arc<TrackLocalStaticRTP> =
|
|
466
|
+
Arc::new(TrackLocalStaticRTP::new(MediaStreamTrack::new(
|
|
467
|
+
String::from("desklink"),
|
|
468
|
+
String::from("desktop"),
|
|
469
|
+
String::from("desktop"),
|
|
470
|
+
RtpCodecKind::Video,
|
|
471
|
+
vec![RTCRtpEncodingParameters {
|
|
472
|
+
rtp_coding_parameters: RTCRtpCodingParameters {
|
|
473
|
+
ssrc: Some(ssrc),
|
|
474
|
+
..Default::default()
|
|
475
|
+
},
|
|
476
|
+
codec: video_codec.rtp_codec.clone(),
|
|
477
|
+
..Default::default()
|
|
478
|
+
}],
|
|
479
|
+
)));
|
|
480
|
+
let sender = peer
|
|
481
|
+
.add_track(Arc::clone(&track) as Arc<dyn TrackLocal>)
|
|
482
|
+
.await
|
|
483
|
+
.context("failed to add the video track")?;
|
|
484
|
+
let payload_type = sender
|
|
485
|
+
.get_parameters()
|
|
486
|
+
.await?
|
|
487
|
+
.rtp_parameters
|
|
488
|
+
.codecs
|
|
489
|
+
.first()
|
|
490
|
+
.map(|codec| codec.payload_type)
|
|
491
|
+
.context("the video sender has no negotiated codec")?;
|
|
492
|
+
|
|
493
|
+
let control = peer
|
|
494
|
+
.create_data_channel("control", None)
|
|
495
|
+
.await
|
|
496
|
+
.context("failed to create the control channel")?;
|
|
497
|
+
tokio::spawn(serve_control(Arc::clone(&control), control_events));
|
|
498
|
+
|
|
499
|
+
let loss = Arc::new(Mutex::new(Vec::new()));
|
|
500
|
+
let feedback = tokio::spawn(serve_feedback(
|
|
501
|
+
Arc::clone(&track),
|
|
502
|
+
ssrc,
|
|
503
|
+
wants_keyframe.clone(),
|
|
504
|
+
loss.clone(),
|
|
505
|
+
));
|
|
506
|
+
|
|
507
|
+
let offer = peer.create_offer(None).await?;
|
|
508
|
+
peer.set_local_description(offer.clone()).await?;
|
|
509
|
+
|
|
510
|
+
Ok((
|
|
511
|
+
Self {
|
|
512
|
+
peer,
|
|
513
|
+
wants_keyframe,
|
|
514
|
+
connected,
|
|
515
|
+
track,
|
|
516
|
+
packetizer: Mutex::new(Vp9Packetizer::new()),
|
|
517
|
+
ssrc,
|
|
518
|
+
control,
|
|
519
|
+
payload_type,
|
|
520
|
+
loss,
|
|
521
|
+
feedback,
|
|
522
|
+
candidates: Mutex::new(PendingCandidates::default()),
|
|
523
|
+
},
|
|
524
|
+
offer.sdp,
|
|
525
|
+
))
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
pub async fn accept_answer(&self, sdp: String) -> Result<usize> {
|
|
529
|
+
let answer = RTCSessionDescription::answer(sdp).context("the answer is not valid SDP")?;
|
|
530
|
+
self.peer
|
|
531
|
+
.set_remote_description(answer)
|
|
532
|
+
.await
|
|
533
|
+
.context("the peer refused the answer")?;
|
|
534
|
+
let held = {
|
|
535
|
+
let mut state = lock(&self.candidates);
|
|
536
|
+
state.ready = true;
|
|
537
|
+
std::mem::take(&mut state.held)
|
|
538
|
+
};
|
|
539
|
+
let mut applied = 0;
|
|
540
|
+
for candidate in held {
|
|
541
|
+
// Each was acknowledged when it arrived; a bad one must not turn the
|
|
542
|
+
// answer into a failure.
|
|
543
|
+
if self.peer.add_ice_candidate(candidate).await.is_ok() {
|
|
544
|
+
applied += 1;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
Ok(applied)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
pub async fn add_candidate(
|
|
551
|
+
&self,
|
|
552
|
+
candidate: String,
|
|
553
|
+
sdp_mid: Option<String>,
|
|
554
|
+
sdp_m_line_index: Option<u16>,
|
|
555
|
+
) -> Result<()> {
|
|
556
|
+
let candidate = RTCIceCandidateInit {
|
|
557
|
+
candidate,
|
|
558
|
+
sdp_mid,
|
|
559
|
+
sdp_mline_index: sdp_m_line_index,
|
|
560
|
+
username_fragment: None,
|
|
561
|
+
url: None,
|
|
562
|
+
};
|
|
563
|
+
{
|
|
564
|
+
let mut state = lock(&self.candidates);
|
|
565
|
+
if !state.ready {
|
|
566
|
+
state.held.push(candidate);
|
|
567
|
+
return Ok(());
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
self.peer
|
|
571
|
+
.add_ice_candidate(candidate)
|
|
572
|
+
.await
|
|
573
|
+
.context("the peer refused an ICE candidate")
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/// Whether the transport is connected; a frame sent before it is lost.
|
|
577
|
+
pub fn is_connected(&self) -> bool {
|
|
578
|
+
self.connected.load(Ordering::SeqCst)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/// Stand in for a connected transport, for a pipeline under test.
|
|
582
|
+
#[cfg(test)]
|
|
583
|
+
pub fn assume_connected(&self) {
|
|
584
|
+
self.connected.store(true, Ordering::SeqCst);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/// Packetize one encoded frame and hand it to the track, paced by the
|
|
588
|
+
/// interceptor chain. `captured` is when its picture was taken.
|
|
589
|
+
pub async fn send_frame(&self, data: &[u8], keyframe: bool, captured: Instant) -> Result<()> {
|
|
590
|
+
let packets = lock(&self.packetizer).packetize(
|
|
591
|
+
data,
|
|
592
|
+
keyframe,
|
|
593
|
+
captured,
|
|
594
|
+
self.ssrc,
|
|
595
|
+
self.payload_type,
|
|
596
|
+
);
|
|
597
|
+
for packet in packets {
|
|
598
|
+
self.track
|
|
599
|
+
.write_rtp_with_extensions(packet, &[])
|
|
600
|
+
.await
|
|
601
|
+
.context("failed to hand a packet to the track")?;
|
|
602
|
+
}
|
|
603
|
+
Ok(())
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/// Take the loss fractions reported since the last call, oldest first.
|
|
607
|
+
pub fn take_loss_reports(&self) -> Vec<u8> {
|
|
608
|
+
std::mem::take(&mut *lock(&self.loss))
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/// Take the pending "the far end needs a reference frame" request, if any.
|
|
612
|
+
pub fn take_keyframe_request(&self) -> bool {
|
|
613
|
+
self.wants_keyframe.swap(false, Ordering::SeqCst)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/// Send a control message back to the client (clipboard replies, revocation).
|
|
617
|
+
pub async fn send_control(&self, message: &str) -> Result<()> {
|
|
618
|
+
self.control
|
|
619
|
+
.send_text(message)
|
|
620
|
+
.await
|
|
621
|
+
.context("the control channel is not writable")
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
pub async fn close(&self) {
|
|
625
|
+
self.feedback.abort();
|
|
626
|
+
let _ = self.control.close().await;
|
|
627
|
+
let _ = self.peer.close().await;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
impl Drop for VideoPeer {
|
|
632
|
+
fn drop(&mut self) {
|
|
633
|
+
self.feedback.abort();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
|
638
|
+
mutex
|
|
639
|
+
.lock()
|
|
640
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
#[cfg(test)]
|
|
644
|
+
mod tests {
|
|
645
|
+
use super::*;
|
|
646
|
+
|
|
647
|
+
struct NoopHandler;
|
|
648
|
+
|
|
649
|
+
impl PeerConnectionEventHandler for NoopHandler {}
|
|
650
|
+
|
|
651
|
+
async fn answerer(offer: &str) -> Arc<dyn PeerConnection> {
|
|
652
|
+
let mut media_engine = MediaEngine::default();
|
|
653
|
+
media_engine
|
|
654
|
+
.register_codec(vp9_codec(), RtpCodecKind::Video)
|
|
655
|
+
.expect("VP9 registers");
|
|
656
|
+
let registry = register_default_interceptors(Registry::new(), &mut media_engine)
|
|
657
|
+
.expect("interceptors register");
|
|
658
|
+
let peer = PeerConnectionBuilder::<std::net::SocketAddr>::new()
|
|
659
|
+
.with_media_engine(media_engine)
|
|
660
|
+
.with_interceptor_registry(registry)
|
|
661
|
+
.with_runtime(default_runtime().expect("a runtime"))
|
|
662
|
+
.with_handler(Arc::new(NoopHandler))
|
|
663
|
+
.with_udp_addrs(vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))])
|
|
664
|
+
.build()
|
|
665
|
+
.await
|
|
666
|
+
.expect("an answering peer");
|
|
667
|
+
peer.set_remote_description(
|
|
668
|
+
RTCSessionDescription::offer(offer.to_owned()).expect("a valid offer"),
|
|
669
|
+
)
|
|
670
|
+
.await
|
|
671
|
+
.expect("the offer is accepted");
|
|
672
|
+
Arc::new(peer)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
#[tokio::test]
|
|
676
|
+
async fn a_candidate_that_arrives_before_the_answer_is_not_refused() {
|
|
677
|
+
let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
678
|
+
let (peer, _offer) = VideoPeer::offer(
|
|
679
|
+
TransportOptions {
|
|
680
|
+
ice_servers: Vec::new(),
|
|
681
|
+
loopback_tcp: false,
|
|
682
|
+
pace_bps: 20_000_000.0,
|
|
683
|
+
},
|
|
684
|
+
events,
|
|
685
|
+
)
|
|
686
|
+
.await
|
|
687
|
+
.expect("a peer connection");
|
|
688
|
+
|
|
689
|
+
// The engine is the offerer, so the remote description only arrives with
|
|
690
|
+
// the answer; this candidate must be held, not refused.
|
|
691
|
+
peer.add_candidate(
|
|
692
|
+
String::from("candidate:1 1 udp 2113937151 192.0.2.1 40000 typ host"),
|
|
693
|
+
Some(String::from("0")),
|
|
694
|
+
Some(0),
|
|
695
|
+
)
|
|
696
|
+
.await
|
|
697
|
+
.expect("a candidate that arrives before the answer is buffered");
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
#[tokio::test]
|
|
701
|
+
async fn held_candidates_reach_the_peer_when_the_answer_arrives() {
|
|
702
|
+
let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
703
|
+
let (peer, offer) = VideoPeer::offer(
|
|
704
|
+
TransportOptions {
|
|
705
|
+
ice_servers: Vec::new(),
|
|
706
|
+
loopback_tcp: false,
|
|
707
|
+
pace_bps: 20_000_000.0,
|
|
708
|
+
},
|
|
709
|
+
events,
|
|
710
|
+
)
|
|
711
|
+
.await
|
|
712
|
+
.expect("a peer connection");
|
|
713
|
+
|
|
714
|
+
let other = answerer(&offer).await;
|
|
715
|
+
let answer = other.create_answer(None).await.expect("an answer");
|
|
716
|
+
other
|
|
717
|
+
.set_local_description(answer.clone())
|
|
718
|
+
.await
|
|
719
|
+
.expect("a local description");
|
|
720
|
+
|
|
721
|
+
peer.add_candidate(
|
|
722
|
+
String::from("candidate:1 1 udp 2113937151 192.0.2.1 40000 typ host"),
|
|
723
|
+
Some(String::from("0")),
|
|
724
|
+
Some(0),
|
|
725
|
+
)
|
|
726
|
+
.await
|
|
727
|
+
.expect("a candidate before the answer is buffered");
|
|
728
|
+
|
|
729
|
+
let applied = peer
|
|
730
|
+
.accept_answer(answer.sdp)
|
|
731
|
+
.await
|
|
732
|
+
.expect("the answer is accepted");
|
|
733
|
+
assert_eq!(applied, 1, "the held candidate must reach the peer");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
#[tokio::test]
|
|
737
|
+
async fn a_forwarded_client_is_offered_ice_over_tcp_on_the_loopback() {
|
|
738
|
+
let offered = |loopback_tcp: bool| async move {
|
|
739
|
+
let (events, mut events_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
740
|
+
let (_peer, offer) = VideoPeer::offer(
|
|
741
|
+
TransportOptions {
|
|
742
|
+
ice_servers: Vec::new(),
|
|
743
|
+
loopback_tcp,
|
|
744
|
+
pace_bps: 20_000_000.0,
|
|
745
|
+
},
|
|
746
|
+
events,
|
|
747
|
+
)
|
|
748
|
+
.await
|
|
749
|
+
.expect("a peer connection");
|
|
750
|
+
let mut candidates = vec![offer];
|
|
751
|
+
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
|
752
|
+
while let Ok(Some(event)) = tokio::time::timeout_at(deadline, events_rx.recv()).await {
|
|
753
|
+
if let PeerEvent::Candidate { candidate, .. } = event {
|
|
754
|
+
candidates.push(candidate);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
candidates.join("\n")
|
|
758
|
+
};
|
|
759
|
+
let passive = |text: &str| {
|
|
760
|
+
text.lines().any(|line| {
|
|
761
|
+
line.contains(" tcp ")
|
|
762
|
+
&& line.contains(" 127.0.0.1 ")
|
|
763
|
+
&& line.contains("tcptype passive")
|
|
764
|
+
})
|
|
765
|
+
};
|
|
766
|
+
assert!(
|
|
767
|
+
passive(&offered(true).await),
|
|
768
|
+
"a client behind a forward needs a passive TCP candidate on the loopback"
|
|
769
|
+
);
|
|
770
|
+
assert!(
|
|
771
|
+
!passive(&offered(false).await),
|
|
772
|
+
"no one else is offered a port on the loopback"
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
}
|