@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.
Files changed (142) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +44 -0
  3. package/README.md +266 -0
  4. package/bin/desklink-host.mjs +113 -0
  5. package/dist/bridge.d.ts +57 -0
  6. package/dist/bridge.d.ts.map +1 -0
  7. package/dist/bridge.js +208 -0
  8. package/dist/bridge.js.map +1 -0
  9. package/dist/engineProcess.d.ts +82 -0
  10. package/dist/engineProcess.d.ts.map +1 -0
  11. package/dist/engineProcess.js +243 -0
  12. package/dist/engineProcess.js.map +1 -0
  13. package/dist/index.d.ts +12 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +12 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/protocol.d.ts +169 -0
  18. package/dist/protocol.d.ts.map +1 -0
  19. package/dist/protocol.js +18 -0
  20. package/dist/protocol.js.map +1 -0
  21. package/dist/resolveEngine.d.ts +34 -0
  22. package/dist/resolveEngine.d.ts.map +1 -0
  23. package/dist/resolveEngine.js +103 -0
  24. package/dist/resolveEngine.js.map +1 -0
  25. package/docs/BOUNDARY.md +145 -0
  26. package/docs/PROTOCOL.md +363 -0
  27. package/engine/Cargo.lock +3077 -0
  28. package/engine/Cargo.toml +37 -0
  29. package/engine/build.rs +107 -0
  30. package/engine/native/inputtino_shim.cpp +11 -0
  31. package/engine/native/vpx_shim.c +184 -0
  32. package/engine/native/wrapper.h +7 -0
  33. package/engine/src/capture.rs +510 -0
  34. package/engine/src/clipboard.rs +132 -0
  35. package/engine/src/convert.rs +299 -0
  36. package/engine/src/encoder.rs +204 -0
  37. package/engine/src/input.rs +355 -0
  38. package/engine/src/keymap.rs +338 -0
  39. package/engine/src/main.rs +614 -0
  40. package/engine/src/peer.rs +775 -0
  41. package/engine/src/portal.rs +115 -0
  42. package/engine/src/protocol.rs +331 -0
  43. package/engine/src/session.rs +2159 -0
  44. package/engine/src/x11.rs +244 -0
  45. package/engine/vendor/inputtino/.clang-format +166 -0
  46. package/engine/vendor/inputtino/.clang-tidy +30 -0
  47. package/engine/vendor/inputtino/.dockerignore +11 -0
  48. package/engine/vendor/inputtino/.github/workflows/mirror-repo.yml +14 -0
  49. package/engine/vendor/inputtino/.github/workflows/python-lint-format-test-typecheck.yml +124 -0
  50. package/engine/vendor/inputtino/.github/workflows/python-test-install.yaml +49 -0
  51. package/engine/vendor/inputtino/.github/workflows/rust.yml +40 -0
  52. package/engine/vendor/inputtino/CMakeLists.txt +187 -0
  53. package/engine/vendor/inputtino/LICENSE +21 -0
  54. package/engine/vendor/inputtino/README.md +110 -0
  55. package/engine/vendor/inputtino/bindings/python/.devcontainer/Dockerfile +23 -0
  56. package/engine/vendor/inputtino/bindings/python/.devcontainer/devcontainer.json +28 -0
  57. package/engine/vendor/inputtino/bindings/python/.python-version +1 -0
  58. package/engine/vendor/inputtino/bindings/python/CMakeLists.txt +23 -0
  59. package/engine/vendor/inputtino/bindings/python/CPPLINT.cfg +2 -0
  60. package/engine/vendor/inputtino/bindings/python/Makefile +49 -0
  61. package/engine/vendor/inputtino/bindings/python/README.md +215 -0
  62. package/engine/vendor/inputtino/bindings/python/pyproject.toml +178 -0
  63. package/engine/vendor/inputtino/bindings/python/src/inputtino/__init__.py +43 -0
  64. package/engine/vendor/inputtino/bindings/python/src/inputtino/_core.pyi +533 -0
  65. package/engine/vendor/inputtino/bindings/python/src/inputtino/base.py +63 -0
  66. package/engine/vendor/inputtino/bindings/python/src/inputtino/joypad.py +248 -0
  67. package/engine/vendor/inputtino/bindings/python/src/inputtino/keyboard.py +256 -0
  68. package/engine/vendor/inputtino/bindings/python/src/inputtino/mouse.py +106 -0
  69. package/engine/vendor/inputtino/bindings/python/src/inputtino/pentablet.py +73 -0
  70. package/engine/vendor/inputtino/bindings/python/src/inputtino/py.typed +0 -0
  71. package/engine/vendor/inputtino/bindings/python/src/inputtino/touchscreen.py +61 -0
  72. package/engine/vendor/inputtino/bindings/python/src/inputtino/trackpad.py +74 -0
  73. package/engine/vendor/inputtino/bindings/python/src/main.cpp +217 -0
  74. package/engine/vendor/inputtino/bindings/python/tests/__init__.py +0 -0
  75. package/engine/vendor/inputtino/bindings/python/tests/helpers.py +3 -0
  76. package/engine/vendor/inputtino/bindings/python/tests/test_base.py +60 -0
  77. package/engine/vendor/inputtino/bindings/python/tests/test_inputtino.py +5 -0
  78. package/engine/vendor/inputtino/bindings/python/tests/test_joypad.py +332 -0
  79. package/engine/vendor/inputtino/bindings/python/tests/test_keyboard.py +164 -0
  80. package/engine/vendor/inputtino/bindings/python/tests/test_mouse.py +185 -0
  81. package/engine/vendor/inputtino/bindings/python/tests/test_pentablet.py +198 -0
  82. package/engine/vendor/inputtino/bindings/python/tests/test_touchscreen.py +144 -0
  83. package/engine/vendor/inputtino/bindings/python/tests/test_trackpad.py +146 -0
  84. package/engine/vendor/inputtino/bindings/python/uv.lock +390 -0
  85. package/engine/vendor/inputtino/bindings/rust/Cargo.toml +6 -0
  86. package/engine/vendor/inputtino/bindings/rust/inputtino/.dockerignore +1 -0
  87. package/engine/vendor/inputtino/bindings/rust/inputtino/Cargo.toml +18 -0
  88. package/engine/vendor/inputtino/bindings/rust/inputtino/examples/keyboard.rs +33 -0
  89. package/engine/vendor/inputtino/bindings/rust/inputtino/examples/mouse.rs +17 -0
  90. package/engine/vendor/inputtino/bindings/rust/inputtino/src/common.rs +116 -0
  91. package/engine/vendor/inputtino/bindings/rust/inputtino/src/error.rs +17 -0
  92. package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad.rs +74 -0
  93. package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_nintendo.rs +135 -0
  94. package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_ps5.rs +280 -0
  95. package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_xbox.rs +134 -0
  96. package/engine/vendor/inputtino/bindings/rust/inputtino/src/keyboard.rs +76 -0
  97. package/engine/vendor/inputtino/bindings/rust/inputtino/src/lib.rs +32 -0
  98. package/engine/vendor/inputtino/bindings/rust/inputtino/src/mouse.rs +178 -0
  99. package/engine/vendor/inputtino/bindings/rust/inputtino/tests/common.rs +38 -0
  100. package/engine/vendor/inputtino/bindings/rust/inputtino/tests/joypads.rs +150 -0
  101. package/engine/vendor/inputtino/bindings/rust/inputtino/tests/keyboard.rs +73 -0
  102. package/engine/vendor/inputtino/bindings/rust/inputtino/tests/mouse.rs +139 -0
  103. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/.dockerignore +1 -0
  104. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/Cargo.toml +12 -0
  105. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/build.rs +48 -0
  106. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/src/lib.rs +5 -0
  107. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/wrapper.h +1 -0
  108. package/engine/vendor/inputtino/cmake/utils.cmake +9 -0
  109. package/engine/vendor/inputtino/docker/Dockerfile +70 -0
  110. package/engine/vendor/inputtino/include/inputtino/input.h +324 -0
  111. package/engine/vendor/inputtino/include/inputtino/input.hpp +466 -0
  112. package/engine/vendor/inputtino/include/inputtino/result.hpp +37 -0
  113. package/engine/vendor/inputtino/share/pkgconfig/libinputtino.pc.in +17 -0
  114. package/engine/vendor/inputtino/src/c-bindings/helpers.hpp +19 -0
  115. package/engine/vendor/inputtino/src/c-bindings/joypad_ps5.cpp +115 -0
  116. package/engine/vendor/inputtino/src/c-bindings/joypad_switch.cpp +63 -0
  117. package/engine/vendor/inputtino/src/c-bindings/joypad_xone.cpp +62 -0
  118. package/engine/vendor/inputtino/src/c-bindings/keyboard.cpp +42 -0
  119. package/engine/vendor/inputtino/src/c-bindings/mouse.cpp +67 -0
  120. package/engine/vendor/inputtino/src/c-bindings/pen_tablet.cpp +50 -0
  121. package/engine/vendor/inputtino/src/c-bindings/touchscreen.cpp +43 -0
  122. package/engine/vendor/inputtino/src/c-bindings/trackpad.cpp +49 -0
  123. package/engine/vendor/inputtino/src/uhid/README.adoc +63 -0
  124. package/engine/vendor/inputtino/src/uhid/include/crc32.hpp +38 -0
  125. package/engine/vendor/inputtino/src/uhid/include/uhid/protected_types.hpp +36 -0
  126. package/engine/vendor/inputtino/src/uhid/include/uhid/ps5.hpp +550 -0
  127. package/engine/vendor/inputtino/src/uhid/include/uhid/uhid.hpp +168 -0
  128. package/engine/vendor/inputtino/src/uhid/joypad_ps5.cpp +580 -0
  129. package/engine/vendor/inputtino/src/uinput/include/inputtino/keyboard.hpp +84 -0
  130. package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_ps5_types.hpp +7 -0
  131. package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_types.hpp +110 -0
  132. package/engine/vendor/inputtino/src/uinput/joypad_nintendo.cpp +190 -0
  133. package/engine/vendor/inputtino/src/uinput/joypad_ps.cpp +203 -0
  134. package/engine/vendor/inputtino/src/uinput/joypad_utils.hpp +246 -0
  135. package/engine/vendor/inputtino/src/uinput/joypad_xbox.cpp +190 -0
  136. package/engine/vendor/inputtino/src/uinput/keyboard.cpp +120 -0
  137. package/engine/vendor/inputtino/src/uinput/mouse.cpp +199 -0
  138. package/engine/vendor/inputtino/src/uinput/pentablet.cpp +162 -0
  139. package/engine/vendor/inputtino/src/uinput/touchscreen.cpp +140 -0
  140. package/engine/vendor/inputtino/src/uinput/trackpad.cpp +192 -0
  141. package/examples/reference-client.html +428 -0
  142. 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
+ }