@desklink/host 0.5.2 → 0.5.4
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/README.md +45 -18
- package/bin/desklink-host.mjs +5 -1
- package/dist/bridge.d.ts +7 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +55 -7
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts +9 -2
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +26 -15
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -1
- package/dist/protocol.js.map +1 -1
- package/docs/BOUNDARY.md +4 -1
- package/docs/PROTOCOL.md +20 -5
- package/engine/Cargo.lock +6 -8
- package/engine/Cargo.toml +13 -1
- package/engine/src/clipboard.rs +0 -34
- package/engine/src/convert.rs +0 -170
- package/engine/src/cursor.rs +0 -23
- package/engine/src/input.rs +0 -65
- package/engine/src/input_mac.rs +0 -11
- package/engine/src/keeper.rs +0 -137
- package/engine/src/keymap.rs +0 -107
- package/engine/src/keymap_mac.rs +0 -25
- package/engine/src/mac.rs +0 -28
- package/engine/src/main.rs +3 -2
- package/engine/src/peer.rs +231 -1
- package/engine/src/portal.rs +0 -18
- package/engine/src/protocol.rs +0 -162
- package/engine/src/session.rs +37 -1
- package/engine/src/x11.rs +0 -13
- package/engine/vendor/rtc-ice/Cargo.toml +137 -0
- package/engine/vendor/rtc-ice/DESKLINK-PATCH.md +59 -0
- package/engine/vendor/rtc-ice/LICENSE-APACHE +201 -0
- package/engine/vendor/rtc-ice/LICENSE-MIT +21 -0
- package/engine/vendor/rtc-ice/README.md +30 -0
- package/engine/vendor/rtc-ice/examples/ping_pong.rs +376 -0
- package/engine/vendor/rtc-ice/src/agent/agent_config.rs +126 -0
- package/engine/vendor/rtc-ice/src/agent/agent_proto.rs +172 -0
- package/engine/vendor/rtc-ice/src/agent/agent_selector.rs +566 -0
- package/engine/vendor/rtc-ice/src/agent/agent_stats.rs +292 -0
- package/engine/vendor/rtc-ice/src/agent/agent_test.rs +3388 -0
- package/engine/vendor/rtc-ice/src/agent/mod.rs +1662 -0
- package/engine/vendor/rtc-ice/src/attributes/control/control_test.rs +168 -0
- package/engine/vendor/rtc-ice/src/attributes/control/mod.rs +142 -0
- package/engine/vendor/rtc-ice/src/attributes/mod.rs +6 -0
- package/engine/vendor/rtc-ice/src/attributes/priority/mod.rs +37 -0
- package/engine/vendor/rtc-ice/src/attributes/priority/priority_test.rs +39 -0
- package/engine/vendor/rtc-ice/src/attributes/use_candidate/mod.rs +33 -0
- package/engine/vendor/rtc-ice/src/attributes/use_candidate/use_candidate_test.rs +19 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_host.rs +53 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_pair.rs +215 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_pair_test.rs +201 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_peer_reflexive.rs +49 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_relay.rs +53 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_relay_test.rs +114 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive.rs +53 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive_test.rs +91 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_test.rs +540 -0
- package/engine/vendor/rtc-ice/src/candidate/mod.rs +679 -0
- package/engine/vendor/rtc-ice/src/lib.rs +85 -0
- package/engine/vendor/rtc-ice/src/mdns/mdns_test.rs +150 -0
- package/engine/vendor/rtc-ice/src/mdns/mod.rs +69 -0
- package/engine/vendor/rtc-ice/src/network_type/mod.rs +162 -0
- package/engine/vendor/rtc-ice/src/network_type/network_type_test.rs +95 -0
- package/engine/vendor/rtc-ice/src/rand/mod.rs +91 -0
- package/engine/vendor/rtc-ice/src/rand/rand_test.rs +115 -0
- package/engine/vendor/rtc-ice/src/state/mod.rs +106 -0
- package/engine/vendor/rtc-ice/src/state/state_test.rs +46 -0
- package/engine/vendor/rtc-ice/src/stats/mod.rs +166 -0
- package/engine/vendor/rtc-ice/src/tcp_type/mod.rs +47 -0
- package/engine/vendor/rtc-ice/src/tcp_type/tcp_type_test.rs +18 -0
- package/engine/vendor/rtc-ice/src/url/mod.rs +266 -0
- package/engine/vendor/rtc-ice/src/url/url_test.rs +142 -0
- package/package.json +8 -3
package/engine/src/keymap.rs
CHANGED
|
@@ -254,110 +254,3 @@ impl Layout {
|
|
|
254
254
|
(plan, unreachable)
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
|
-
|
|
258
|
-
#[cfg(test)]
|
|
259
|
-
mod tests {
|
|
260
|
-
use super::*;
|
|
261
|
-
|
|
262
|
-
#[test]
|
|
263
|
-
fn a_layout_that_comes_from_the_environment_is_the_one_compiled() {
|
|
264
|
-
let names = LayoutNames::from_lookup(|key| match key {
|
|
265
|
-
"XKB_DEFAULT_LAYOUT" => Some(String::from("de")),
|
|
266
|
-
_ => None,
|
|
267
|
-
});
|
|
268
|
-
assert_eq!(names.identity(), "de");
|
|
269
|
-
|
|
270
|
-
let layout = Layout::from_names(&names).expect("the German keymap should compile");
|
|
271
|
-
let (plan, unreachable) = layout.plan_text("z");
|
|
272
|
-
assert!(
|
|
273
|
-
unreachable.is_empty(),
|
|
274
|
-
"z should be reachable: {unreachable:?}"
|
|
275
|
-
);
|
|
276
|
-
// QWERTZ puts z where a US layout puts y, so the compiled keymap is the
|
|
277
|
-
// environment's, not the library default.
|
|
278
|
-
assert_eq!(plan[0][0].code, 21, "z must sit on the y key position");
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
#[test]
|
|
282
|
-
fn a_session_with_no_layout_variable_reports_the_library_default() {
|
|
283
|
-
assert_eq!(LayoutNames::from_lookup(|_| None).identity(), "us");
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
#[test]
|
|
287
|
-
fn a_character_is_planned_as_a_real_key_event_on_the_compiled_layout() {
|
|
288
|
-
let layout = Layout::from_names(&LayoutNames::from_lookup(|_| None))
|
|
289
|
-
.expect("a keymap should compile");
|
|
290
|
-
let (plan, unreachable) = layout.plan_text("aA.\n");
|
|
291
|
-
assert!(
|
|
292
|
-
unreachable.is_empty(),
|
|
293
|
-
"ASCII should be reachable: {unreachable:?}"
|
|
294
|
-
);
|
|
295
|
-
assert_eq!(plan.len(), 4);
|
|
296
|
-
assert!(!plan[0][0].shift, "lower-case a needs no shift");
|
|
297
|
-
assert!(
|
|
298
|
-
plan[1][0].shift,
|
|
299
|
-
"upper-case A needs shift on any normal layout"
|
|
300
|
-
);
|
|
301
|
-
assert_eq!(plan[0][0].code, plan[1][0].code, "both letters share a key");
|
|
302
|
-
// The plan is evdev, but the real injector expects Moonlight/VK codes.
|
|
303
|
-
// Passing Linux N (49) straight through instead presses VK_1.
|
|
304
|
-
let native: Vec<_> = plan
|
|
305
|
-
.iter()
|
|
306
|
-
.flatten()
|
|
307
|
-
.map(|stroke| crate::input::native_keycode(stroke.code).unwrap())
|
|
308
|
-
.collect();
|
|
309
|
-
assert_eq!(native, [0x41, 0x41, 0xBE, 0x0D]);
|
|
310
|
-
assert_eq!(
|
|
311
|
-
crate::input::native_keycode(plan[1][0].modifiers()[0]).unwrap(),
|
|
312
|
-
0x10
|
|
313
|
-
);
|
|
314
|
-
assert_eq!(
|
|
315
|
-
crate::input::native_keycode(modifier_key("Control").unwrap()).unwrap(),
|
|
316
|
-
0x11
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
#[test]
|
|
321
|
-
fn named_keys_and_modifiers_resolve_to_linux_key_codes() {
|
|
322
|
-
assert_eq!(named_key("Enter").unwrap().code, keycode::ENTER);
|
|
323
|
-
assert_eq!(named_key("ArrowLeft").unwrap().code, keycode::LEFT);
|
|
324
|
-
for (name, code) in [
|
|
325
|
-
("Delete", keycode::DELETE), ("Home", keycode::HOME), ("End", keycode::END),
|
|
326
|
-
("PageUp", keycode::PAGE_UP), ("PageDown", keycode::PAGE_DOWN),
|
|
327
|
-
("F1", keycode::F1), ("F2", keycode::F2), ("F3", keycode::F3),
|
|
328
|
-
("F4", keycode::F4), ("F5", keycode::F5), ("F6", keycode::F6),
|
|
329
|
-
("F7", keycode::F7), ("F8", keycode::F8), ("F9", keycode::F9),
|
|
330
|
-
("F10", keycode::F10), ("F11", keycode::F11), ("F12", keycode::F12),
|
|
331
|
-
] {
|
|
332
|
-
assert_eq!(named_key(name).unwrap().code, code);
|
|
333
|
-
assert!(crate::input::native_keycode(code).is_ok(), "{name} must reach inputtino");
|
|
334
|
-
}
|
|
335
|
-
assert_eq!(modifier_key("Control"), Some(keycode::LEFT_CTRL));
|
|
336
|
-
assert_eq!(modifier_key("Alt"), Some(keycode::LEFT_ALT));
|
|
337
|
-
assert_eq!(modifier_key("Super"), Some(keycode::LEFT_META));
|
|
338
|
-
assert!(named_key("Frobnicate").is_none());
|
|
339
|
-
assert!(modifier_key("Hyper").is_none());
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
#[test]
|
|
343
|
-
fn text_this_layout_cannot_produce_is_reported_not_invented() {
|
|
344
|
-
let layout = Layout::from_environment().unwrap();
|
|
345
|
-
let (_, unreachable) = layout.plan_text("\u{1F600}");
|
|
346
|
-
assert_eq!(unreachable, "\u{1F600}");
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
#[test]
|
|
350
|
-
fn a_symbol_only_reachable_beyond_four_levels_is_refused() {
|
|
351
|
-
let names = LayoutNames::from_lookup(|key| match key {
|
|
352
|
-
"XKB_DEFAULT_LAYOUT" => Some(String::from("de")),
|
|
353
|
-
"XKB_DEFAULT_VARIANT" => Some(String::from("neo")),
|
|
354
|
-
_ => None,
|
|
355
|
-
});
|
|
356
|
-
let layout = Layout::from_names(&names).expect("the Neo keymap should compile");
|
|
357
|
-
assert!(
|
|
358
|
-
layout.keystroke_for_char('\u{a3}').is_none(),
|
|
359
|
-
"the pound sign sits on a level this engine cannot press"
|
|
360
|
-
);
|
|
361
|
-
assert!(layout.keystroke_for_char('6').is_some());
|
|
362
|
-
}
|
|
363
|
-
}
|
package/engine/src/keymap_mac.rs
CHANGED
|
@@ -182,28 +182,3 @@ fn us_ansi_key(character: char) -> Option<(i16, bool)> {
|
|
|
182
182
|
_ => return None,
|
|
183
183
|
})
|
|
184
184
|
}
|
|
185
|
-
|
|
186
|
-
#[cfg(test)]
|
|
187
|
-
mod tests {
|
|
188
|
-
use super::*;
|
|
189
|
-
|
|
190
|
-
#[test]
|
|
191
|
-
fn maps_common_ascii_to_ansi_virtual_keys() {
|
|
192
|
-
let layout = Layout::from_environment().unwrap();
|
|
193
|
-
let lower = layout.keystroke_for_char('a').unwrap();
|
|
194
|
-
let upper = layout.keystroke_for_char('A').unwrap();
|
|
195
|
-
assert_eq!(lower.code, 0);
|
|
196
|
-
assert_eq!(upper.code, lower.code);
|
|
197
|
-
assert!(upper.shift);
|
|
198
|
-
assert_eq!(layout.keystroke_for_char('!').unwrap().code, 18);
|
|
199
|
-
assert!(layout.keystroke_for_char('\u{1f600}').is_none());
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
#[test]
|
|
203
|
-
fn resolves_mac_named_keys_and_modifiers() {
|
|
204
|
-
assert_eq!(named_key("Return").unwrap().code, 36);
|
|
205
|
-
assert_eq!(named_key("ArrowLeft").unwrap().code, 123);
|
|
206
|
-
assert_eq!(modifier_key("Command"), Some(55));
|
|
207
|
-
assert!(named_key("Frobnicate").is_none());
|
|
208
|
-
}
|
|
209
|
-
}
|
package/engine/src/mac.rs
CHANGED
|
@@ -717,31 +717,3 @@ pub fn run() -> i32 {
|
|
|
717
717
|
}
|
|
718
718
|
0
|
|
719
719
|
}
|
|
720
|
-
|
|
721
|
-
#[cfg(test)]
|
|
722
|
-
mod tests {
|
|
723
|
-
use super::*;
|
|
724
|
-
|
|
725
|
-
#[cfg(desklink_vpx)]
|
|
726
|
-
#[test]
|
|
727
|
-
fn session_capabilities_are_non_prompting_and_expose_mac_features() {
|
|
728
|
-
let value = crate::session::capabilities();
|
|
729
|
-
assert_eq!(value["protocol"], crate::protocol::PROTOCOL_VERSION);
|
|
730
|
-
assert_eq!(value["platform"], "macos");
|
|
731
|
-
assert_eq!(value["capture"]["mechanism"], "screencapturekit");
|
|
732
|
-
assert_eq!(value["encode"]["codecs"][0], "vp9", "VP9 is always offered");
|
|
733
|
-
// Every Mac this engine runs on has a hardware H.264 encoder.
|
|
734
|
-
assert_eq!(value["encode"]["codecs"][1], "h264");
|
|
735
|
-
assert_eq!(value["encode"]["h264"]["encoder"], "videotoolbox");
|
|
736
|
-
assert!(value["capture"]["displays"].is_array());
|
|
737
|
-
assert!(value["clipboard"]["read"].as_bool().unwrap());
|
|
738
|
-
assert!(matches!(
|
|
739
|
-
value["capture"]["grant"].as_str(),
|
|
740
|
-
Some("granted" | "missing-screen-recording")
|
|
741
|
-
));
|
|
742
|
-
assert!(matches!(
|
|
743
|
-
value["input"]["grant"].as_str(),
|
|
744
|
-
Some("granted" | "missing-accessibility")
|
|
745
|
-
));
|
|
746
|
-
}
|
|
747
|
-
}
|
package/engine/src/main.rs
CHANGED
|
@@ -728,8 +728,9 @@ async fn dispatch(
|
|
|
728
728
|
Ok(serde_json::json!({ "accepted": true }))
|
|
729
729
|
}
|
|
730
730
|
"session.restart_ice" => {
|
|
731
|
-
// The client asks; the engine offers. The
|
|
732
|
-
//
|
|
731
|
+
// The client asks; the engine offers. The offer — fresh, or the
|
|
732
|
+
// still-pending one when restarts queue — travels as a new
|
|
733
|
+
// `session.description` event and the client answers it as
|
|
733
734
|
// usual, so no new event shape is needed and the session,
|
|
734
735
|
// generation and control channel survive.
|
|
735
736
|
let session = require_session(current)?;
|
package/engine/src/peer.rs
CHANGED
|
@@ -1008,11 +1008,17 @@ impl VideoPeer {
|
|
|
1008
1008
|
Ok(applied)
|
|
1009
1009
|
}
|
|
1010
1010
|
|
|
1011
|
-
/// Restart ICE on this peer
|
|
1011
|
+
/// Restart ICE on this peer, reusing any unanswered local offer; otherwise
|
|
1012
|
+
/// create a fresh offer with a new ICE generation,
|
|
1012
1013
|
/// for the client to answer as usual. The engine stays the offerer, so
|
|
1013
1014
|
/// this is the same local-offer path as the initial negotiation.
|
|
1014
1015
|
pub async fn restart_ice(&self) -> Result<String> {
|
|
1015
1016
|
use rtc::peer_connection::configuration::RTCOfferOptions;
|
|
1017
|
+
// The stdio dispatcher serializes requests; overlapping carriers must
|
|
1018
|
+
// answer this offer before another restart can turn over credentials.
|
|
1019
|
+
if let Some(offer) = self.peer.pending_local_description().await {
|
|
1020
|
+
return Ok(offer.sdp);
|
|
1021
|
+
}
|
|
1016
1022
|
let offer = self
|
|
1017
1023
|
.peer
|
|
1018
1024
|
.create_offer(Some(RTCOfferOptions {
|
|
@@ -1147,6 +1153,7 @@ fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
|
|
1147
1153
|
#[cfg(test)]
|
|
1148
1154
|
pub(crate) mod tests {
|
|
1149
1155
|
use super::*;
|
|
1156
|
+
use webrtc::media_stream::track_remote::{TrackRemote, TrackRemoteEvent};
|
|
1150
1157
|
|
|
1151
1158
|
#[test]
|
|
1152
1159
|
fn a_standing_queue_is_measured_and_one_frames_burst_is_not() {
|
|
@@ -1583,6 +1590,229 @@ pub(crate) mod tests {
|
|
|
1583
1590
|
.expect("the restart answer is accepted");
|
|
1584
1591
|
}
|
|
1585
1592
|
|
|
1593
|
+
/// A receiver peer that records when each inbound RTP packet arrives and
|
|
1594
|
+
/// reports its own ICE candidates, so two peers can really connect.
|
|
1595
|
+
struct MediaReceiver {
|
|
1596
|
+
candidates: tokio::sync::mpsc::UnboundedSender<RTCIceCandidateInit>,
|
|
1597
|
+
arrivals: Arc<Mutex<Vec<Instant>>>,
|
|
1598
|
+
connected: Arc<AtomicBool>,
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
#[async_trait::async_trait]
|
|
1602
|
+
impl PeerConnectionEventHandler for MediaReceiver {
|
|
1603
|
+
async fn on_ice_candidate(&self, event: RTCPeerConnectionIceEvent) {
|
|
1604
|
+
if let Ok(init) = event.candidate.to_json() {
|
|
1605
|
+
let _ = self.candidates.send(init);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
async fn on_connection_state_change(&self, state: RTCPeerConnectionState) {
|
|
1610
|
+
self.connected.store(
|
|
1611
|
+
matches!(state, RTCPeerConnectionState::Connected),
|
|
1612
|
+
Ordering::SeqCst,
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
async fn on_track(&self, track: Arc<dyn TrackRemote>) {
|
|
1617
|
+
let arrivals = Arc::clone(&self.arrivals);
|
|
1618
|
+
tokio::spawn(async move {
|
|
1619
|
+
while let Some(event) = track.poll().await {
|
|
1620
|
+
if matches!(event, TrackRemoteEvent::OnRtpPacket(_)) {
|
|
1621
|
+
arrivals.lock().unwrap().push(Instant::now());
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
async fn media_receiver(receiver: Arc<MediaReceiver>) -> Arc<dyn PeerConnection> {
|
|
1629
|
+
let mut media_engine = MediaEngine::default();
|
|
1630
|
+
media_engine
|
|
1631
|
+
.register_codec(vp9_codec(), RtpCodecKind::Video)
|
|
1632
|
+
.expect("the codec registers");
|
|
1633
|
+
let registry = register_default_interceptors(Registry::new(), &mut media_engine)
|
|
1634
|
+
.expect("interceptors register");
|
|
1635
|
+
let peer = PeerConnectionBuilder::<std::net::SocketAddr>::new()
|
|
1636
|
+
.with_media_engine(media_engine)
|
|
1637
|
+
.with_interceptor_registry(registry)
|
|
1638
|
+
.with_runtime(default_runtime().expect("a runtime"))
|
|
1639
|
+
.with_handler(receiver)
|
|
1640
|
+
.with_udp_addrs(vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))])
|
|
1641
|
+
.build()
|
|
1642
|
+
.await
|
|
1643
|
+
.expect("a receiving peer");
|
|
1644
|
+
Arc::new(peer)
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/// A restart offer/answer round must not interrupt media already flowing on
|
|
1648
|
+
/// the selected pair: the picture keeps advancing (no gap over 200 ms) while
|
|
1649
|
+
/// the old pair stays reachable, and the ICE generation still turns over.
|
|
1650
|
+
#[tokio::test]
|
|
1651
|
+
async fn a_restart_keeps_media_flowing_on_the_selected_pair_until_the_new_one_nominates() {
|
|
1652
|
+
let (events, mut events_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
1653
|
+
let (engine, offer) = VideoPeer::offer(transport(), events, VideoCodec::Vp9)
|
|
1654
|
+
.await
|
|
1655
|
+
.expect("an offering peer");
|
|
1656
|
+
let engine = Arc::new(engine);
|
|
1657
|
+
|
|
1658
|
+
let arrivals: Arc<Mutex<Vec<Instant>>> = Arc::new(Mutex::new(Vec::new()));
|
|
1659
|
+
let connected = Arc::new(AtomicBool::new(false));
|
|
1660
|
+
let (client_candidates_tx, mut client_candidates_rx) =
|
|
1661
|
+
tokio::sync::mpsc::unbounded_channel();
|
|
1662
|
+
let receiver = Arc::new(MediaReceiver {
|
|
1663
|
+
candidates: client_candidates_tx,
|
|
1664
|
+
arrivals: Arc::clone(&arrivals),
|
|
1665
|
+
connected: Arc::clone(&connected),
|
|
1666
|
+
});
|
|
1667
|
+
let client = media_receiver(Arc::clone(&receiver)).await;
|
|
1668
|
+
|
|
1669
|
+
client
|
|
1670
|
+
.set_remote_description(RTCSessionDescription::offer(offer.clone()).expect("an offer"))
|
|
1671
|
+
.await
|
|
1672
|
+
.expect("the offer applies");
|
|
1673
|
+
let answer = client.create_answer(None).await.expect("an answer");
|
|
1674
|
+
client
|
|
1675
|
+
.set_local_description(answer.clone())
|
|
1676
|
+
.await
|
|
1677
|
+
.expect("a local answer");
|
|
1678
|
+
engine
|
|
1679
|
+
.accept_answer(answer.sdp.clone())
|
|
1680
|
+
.await
|
|
1681
|
+
.expect("the answer is accepted");
|
|
1682
|
+
|
|
1683
|
+
// The engine's candidates reach the client.
|
|
1684
|
+
let client_for_engine_candidates = Arc::clone(&client);
|
|
1685
|
+
tokio::spawn(async move {
|
|
1686
|
+
while let Some(event) = events_rx.recv().await {
|
|
1687
|
+
if let PeerEvent::Candidate {
|
|
1688
|
+
candidate,
|
|
1689
|
+
sdp_mid,
|
|
1690
|
+
sdp_m_line_index,
|
|
1691
|
+
} = event
|
|
1692
|
+
{
|
|
1693
|
+
let _ = client_for_engine_candidates
|
|
1694
|
+
.add_ice_candidate(RTCIceCandidateInit {
|
|
1695
|
+
candidate,
|
|
1696
|
+
sdp_mid,
|
|
1697
|
+
sdp_mline_index: sdp_m_line_index,
|
|
1698
|
+
username_fragment: None,
|
|
1699
|
+
url: None,
|
|
1700
|
+
})
|
|
1701
|
+
.await;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
// The client's candidates reach the engine.
|
|
1706
|
+
let engine_for_client_candidates = Arc::clone(&engine);
|
|
1707
|
+
tokio::spawn(async move {
|
|
1708
|
+
while let Some(init) = client_candidates_rx.recv().await {
|
|
1709
|
+
let _ = engine_for_client_candidates
|
|
1710
|
+
.add_candidate(init.candidate, init.sdp_mid, init.sdp_mline_index)
|
|
1711
|
+
.await;
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
// Wait for the ICE path to come up on both ends.
|
|
1716
|
+
let deadline = Instant::now() + Duration::from_secs(5);
|
|
1717
|
+
while !(engine.is_connected() && connected.load(Ordering::SeqCst)) {
|
|
1718
|
+
assert!(Instant::now() < deadline, "the peers never connected");
|
|
1719
|
+
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// Any bytes the VP9 packetizer turns into one access unit.
|
|
1723
|
+
let frame: Vec<u8> = (0..200u32).map(|i| (i % 251) as u8).collect();
|
|
1724
|
+
let sending = Arc::new(AtomicBool::new(true));
|
|
1725
|
+
let engine_for_sending = Arc::clone(&engine);
|
|
1726
|
+
let sending_for_task = Arc::clone(&sending);
|
|
1727
|
+
let frame_for_task = frame.clone();
|
|
1728
|
+
tokio::spawn(async move {
|
|
1729
|
+
while sending_for_task.load(Ordering::SeqCst) {
|
|
1730
|
+
let _ = engine_for_sending
|
|
1731
|
+
.send_frame(&frame_for_task, true, Instant::now())
|
|
1732
|
+
.await;
|
|
1733
|
+
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
// Let the picture flow, then take a clean baseline.
|
|
1738
|
+
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
1739
|
+
arrivals.lock().unwrap().clear();
|
|
1740
|
+
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
1741
|
+
let flowing = arrivals.lock().unwrap().len();
|
|
1742
|
+
assert!(
|
|
1743
|
+
flowing >= 5,
|
|
1744
|
+
"media was not flowing before the restart: {flowing} packets"
|
|
1745
|
+
);
|
|
1746
|
+
|
|
1747
|
+
// The restart a client drives when its carrier comes back on a roam.
|
|
1748
|
+
let restart = engine.restart_ice().await.expect("a restart offer");
|
|
1749
|
+
let ufrag = |sdp: &str| {
|
|
1750
|
+
sdp.lines()
|
|
1751
|
+
.find_map(|line| line.strip_prefix("a=ice-ufrag:"))
|
|
1752
|
+
.expect("ICE credentials")
|
|
1753
|
+
.to_owned()
|
|
1754
|
+
};
|
|
1755
|
+
assert_ne!(
|
|
1756
|
+
ufrag(&restart),
|
|
1757
|
+
ufrag(&offer),
|
|
1758
|
+
"the restart must turn the ICE generation over"
|
|
1759
|
+
);
|
|
1760
|
+
|
|
1761
|
+
let restart_at = Instant::now();
|
|
1762
|
+
arrivals.lock().unwrap().clear();
|
|
1763
|
+
|
|
1764
|
+
// The restart offer crosses the consumer's signaling channel before the
|
|
1765
|
+
// client answers, and the engine applies the restart at
|
|
1766
|
+
// `set_local_description` — inside `restart_ice` above, before the offer
|
|
1767
|
+
// has even left. It must keep feeding media on the old pair for that
|
|
1768
|
+
// whole round trip, not only until its own teardown. A real channel
|
|
1769
|
+
// costs more than a loopback hop, so hold this window for the bar's
|
|
1770
|
+
// length: a teardown shows up here, a preserved pair does not.
|
|
1771
|
+
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
1772
|
+
|
|
1773
|
+
client
|
|
1774
|
+
.set_remote_description(RTCSessionDescription::offer(restart).expect("a restart offer"))
|
|
1775
|
+
.await
|
|
1776
|
+
.expect("the restart offer applies");
|
|
1777
|
+
let reanswer = client.create_answer(None).await.expect("an answer");
|
|
1778
|
+
client
|
|
1779
|
+
.set_local_description(reanswer.clone())
|
|
1780
|
+
.await
|
|
1781
|
+
.expect("a local answer");
|
|
1782
|
+
engine
|
|
1783
|
+
.accept_answer(reanswer.sdp)
|
|
1784
|
+
.await
|
|
1785
|
+
.expect("the restart answer is accepted");
|
|
1786
|
+
|
|
1787
|
+
// Keep the picture advancing across and after the restart.
|
|
1788
|
+
tokio::time::sleep(Duration::from_millis(800)).await;
|
|
1789
|
+
sending.store(false, Ordering::SeqCst);
|
|
1790
|
+
|
|
1791
|
+
let stamps = arrivals.lock().unwrap().clone();
|
|
1792
|
+
assert!(
|
|
1793
|
+
!stamps.is_empty(),
|
|
1794
|
+
"no media arrived at all after the restart"
|
|
1795
|
+
);
|
|
1796
|
+
let mut previous = restart_at;
|
|
1797
|
+
let mut worst = Duration::ZERO;
|
|
1798
|
+
for stamp in &stamps {
|
|
1799
|
+
worst = worst.max(stamp.duration_since(previous));
|
|
1800
|
+
previous = *stamp;
|
|
1801
|
+
}
|
|
1802
|
+
eprintln!(
|
|
1803
|
+
"restart: {} packets after the offer, worst media gap {worst:?}",
|
|
1804
|
+
stamps.len()
|
|
1805
|
+
);
|
|
1806
|
+
assert!(
|
|
1807
|
+
worst < Duration::from_millis(200),
|
|
1808
|
+
"media stalled for {worst:?} through the restart: {:?}",
|
|
1809
|
+
stamps
|
|
1810
|
+
);
|
|
1811
|
+
|
|
1812
|
+
engine.close().await;
|
|
1813
|
+
let _ = client.close().await;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1586
1816
|
#[tokio::test]
|
|
1587
1817
|
async fn a_forwarded_client_is_offered_ice_over_tcp_on_the_loopback() {
|
|
1588
1818
|
let offered = |loopback_tcp: bool| async move {
|
package/engine/src/portal.rs
CHANGED
|
@@ -142,21 +142,3 @@ fn choose_cursor_mode(available: BitFlags<CursorMode>) -> Result<CursorMode> {
|
|
|
142
142
|
.find(|mode| available.contains(*mode))
|
|
143
143
|
.context("cursor-unavailable: portal cannot leave the cursor out of capture")
|
|
144
144
|
}
|
|
145
|
-
|
|
146
|
-
#[cfg(test)]
|
|
147
|
-
mod tests {
|
|
148
|
-
use super::*;
|
|
149
|
-
#[test]
|
|
150
|
-
fn hidden_takes_any_cursor_free_mode_and_refuses_embedded_only_portals() {
|
|
151
|
-
assert_eq!(
|
|
152
|
-
choose_cursor_mode(CursorMode::Metadata | CursorMode::Hidden).unwrap(),
|
|
153
|
-
CursorMode::Hidden
|
|
154
|
-
);
|
|
155
|
-
assert_eq!(
|
|
156
|
-
choose_cursor_mode(CursorMode::Metadata.into()).unwrap(),
|
|
157
|
-
CursorMode::Metadata
|
|
158
|
-
);
|
|
159
|
-
let error = choose_cursor_mode(CursorMode::Embedded.into()).unwrap_err();
|
|
160
|
-
assert!(error.to_string().contains("cursor-unavailable:"));
|
|
161
|
-
}
|
|
162
|
-
}
|
package/engine/src/protocol.rs
CHANGED
|
@@ -395,168 +395,6 @@ pub enum ControlReply<'a> {
|
|
|
395
395
|
},
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
-
#[cfg(test)]
|
|
399
|
-
mod tests {
|
|
400
|
-
use super::*;
|
|
401
|
-
|
|
402
|
-
#[test]
|
|
403
|
-
fn cursor_option_defaults_and_refuses_unknown_modes() {
|
|
404
|
-
let default: OpenParams = serde_json::from_str("{}").unwrap();
|
|
405
|
-
assert_eq!(default.cursor, CursorMode::Embedded);
|
|
406
|
-
let hidden: OpenParams = serde_json::from_str(r#"{"cursor":"hidden"}"#).unwrap();
|
|
407
|
-
assert_eq!(hidden.cursor, CursorMode::Hidden);
|
|
408
|
-
assert!(serde_json::from_str::<OpenParams>(r#"{"cursor":"metadata"}"#).is_err());
|
|
409
|
-
assert!(serde_json::from_str::<OpenParams>(r#"{"cursor":false}"#).is_err());
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
#[test]
|
|
413
|
-
fn a_pointer_message_round_trips_with_its_sequence() {
|
|
414
|
-
let parsed: ControlMessage =
|
|
415
|
-
serde_json::from_str(r#"{"kind":"pointer","phase":"down","x":10,"y":20,"seq":3}"#)
|
|
416
|
-
.unwrap();
|
|
417
|
-
match parsed {
|
|
418
|
-
ControlMessage::Pointer {
|
|
419
|
-
phase,
|
|
420
|
-
x,
|
|
421
|
-
y,
|
|
422
|
-
seq,
|
|
423
|
-
button,
|
|
424
|
-
} => {
|
|
425
|
-
assert!(matches!(phase, PointerPhase::Down));
|
|
426
|
-
assert_eq!((x, y, seq, button), (10, 20, 3, 1));
|
|
427
|
-
}
|
|
428
|
-
other => panic!("wrong variant: {other:?}"),
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
#[test]
|
|
433
|
-
fn an_unknown_action_is_refused_rather_than_ignored() {
|
|
434
|
-
assert!(serde_json::from_str::<ControlMessage>(r#"{"kind":"teleport"}"#).is_err());
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
#[test]
|
|
438
|
-
fn display_source_round_trips_its_explicit_id() {
|
|
439
|
-
let request: SourceRequest = serde_json::from_value(serde_json::json!({
|
|
440
|
-
"kind": "display",
|
|
441
|
-
"display_id": 42
|
|
442
|
-
}))
|
|
443
|
-
.unwrap();
|
|
444
|
-
assert!(matches!(
|
|
445
|
-
request,
|
|
446
|
-
SourceRequest::Display {
|
|
447
|
-
display_id: Some(42)
|
|
448
|
-
}
|
|
449
|
-
));
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
#[test]
|
|
453
|
-
fn an_encoded_source_names_its_codec_and_its_own_size() {
|
|
454
|
-
let request: SourceRequest = serde_json::from_value(serde_json::json!({
|
|
455
|
-
"kind": "encoded",
|
|
456
|
-
"codec": "h264",
|
|
457
|
-
"width": 486,
|
|
458
|
-
"height": 1080
|
|
459
|
-
}))
|
|
460
|
-
.unwrap();
|
|
461
|
-
assert!(matches!(
|
|
462
|
-
request,
|
|
463
|
-
SourceRequest::Encoded {
|
|
464
|
-
codec: EncodedCodec::H264,
|
|
465
|
-
width: 486,
|
|
466
|
-
height: 1080
|
|
467
|
-
}
|
|
468
|
-
));
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
#[test]
|
|
472
|
-
fn an_unknown_encoded_codec_is_refused_rather_than_guessed() {
|
|
473
|
-
assert!(serde_json::from_value::<SourceRequest>(serde_json::json!({
|
|
474
|
-
"kind": "encoded",
|
|
475
|
-
"codec": "av1",
|
|
476
|
-
"width": 486,
|
|
477
|
-
"height": 1080
|
|
478
|
-
}))
|
|
479
|
-
.is_err());
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
#[test]
|
|
483
|
-
fn a_feed_request_is_one_access_unit_with_its_own_keyframe_flag() {
|
|
484
|
-
let params: FeedParams =
|
|
485
|
-
serde_json::from_str(r#"{"session_id":"s","keyframe":true,"data_b64":"AAAA"}"#)
|
|
486
|
-
.unwrap();
|
|
487
|
-
assert_eq!(params.session_id, "s");
|
|
488
|
-
assert!(params.keyframe, "a feed says whether its unit is an IDR");
|
|
489
|
-
assert_eq!(params.data_b64, "AAAA");
|
|
490
|
-
assert_eq!(params.access_unit().unwrap(), vec![0x00, 0x00, 0x00]);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
#[test]
|
|
494
|
-
fn a_feed_request_that_is_not_base64_is_refused() {
|
|
495
|
-
let params = FeedParams {
|
|
496
|
-
session_id: String::from("s"),
|
|
497
|
-
keyframe: false,
|
|
498
|
-
data_b64: String::from("not base64!!"),
|
|
499
|
-
};
|
|
500
|
-
assert!(params.access_unit().is_err());
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/// The reason this protocol carries bytes in base64 rather than adding a
|
|
504
|
-
/// second inherited fd: at 2 Mbit/s it costs a fraction of a per cent of a
|
|
505
|
-
/// core, against a whole second channel to keep ordered and bounded. The
|
|
506
|
-
/// bound below is deliberately loose — it is a sanity check on the order of
|
|
507
|
-
/// magnitude, not a benchmark, so it does not flake on a loaded machine.
|
|
508
|
-
#[test]
|
|
509
|
-
fn decoding_a_two_megabit_stream_is_nowhere_near_a_core() {
|
|
510
|
-
// One second of a 2 Mbit/s stream, in access units.
|
|
511
|
-
let unit: Vec<u8> = (0..8_000u32).map(|index| index as u8).collect();
|
|
512
|
-
let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &unit);
|
|
513
|
-
let params = FeedParams {
|
|
514
|
-
session_id: String::from("s"),
|
|
515
|
-
keyframe: false,
|
|
516
|
-
data_b64: encoded,
|
|
517
|
-
};
|
|
518
|
-
let started = std::time::Instant::now();
|
|
519
|
-
for _ in 0..(250_000 / unit.len()) {
|
|
520
|
-
assert_eq!(params.access_unit().unwrap().len(), unit.len());
|
|
521
|
-
}
|
|
522
|
-
let elapsed = started.elapsed();
|
|
523
|
-
assert!(
|
|
524
|
-
elapsed < std::time::Duration::from_millis(500),
|
|
525
|
-
"a second of 2 Mbit/s base64 took {elapsed:?}, which is far past 5 % of a core",
|
|
526
|
-
);
|
|
527
|
-
println!("a second of 2 Mbit/s stream decoded in {elapsed:?}");
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
#[test]
|
|
531
|
-
fn open_defaults_keep_the_desktops_own_pixels_up_to_4k() {
|
|
532
|
-
for payload in [
|
|
533
|
-
r#"{}"#,
|
|
534
|
-
r#"{"permissions":["view"]}"#,
|
|
535
|
-
r#"{"permissions":["view","unknown-legacy-scope"]}"#,
|
|
536
|
-
] {
|
|
537
|
-
let _: OpenParams = serde_json::from_str(payload).unwrap();
|
|
538
|
-
}
|
|
539
|
-
let params: OpenParams =
|
|
540
|
-
serde_json::from_str(r#"{"permissions":["view","control"]}"#).unwrap();
|
|
541
|
-
assert_eq!(params.max_width, 3840);
|
|
542
|
-
assert_eq!(params.max_height, 2160);
|
|
543
|
-
assert_eq!(
|
|
544
|
-
params.bitrate_kbps, 0,
|
|
545
|
-
"the engine sizes the rate to the surface"
|
|
546
|
-
);
|
|
547
|
-
assert_eq!(params.max_fps, 60);
|
|
548
|
-
assert!(params.local_frames, "session.frame works unless turned off");
|
|
549
|
-
assert!(
|
|
550
|
-
!params.agent_indicator,
|
|
551
|
-
"human control has no overlay by default"
|
|
552
|
-
);
|
|
553
|
-
let agent: OpenParams =
|
|
554
|
-
serde_json::from_str(r#"{"permissions":["view","control"],"agent_indicator":true}"#)
|
|
555
|
-
.unwrap();
|
|
556
|
-
assert!(agent.agent_indicator);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
398
|
/// Monotonic engine-process clock shared by local frame and cursor notices.
|
|
561
399
|
pub fn timestamp_us(at: std::time::Instant) -> u64 {
|
|
562
400
|
static EPOCH: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
|
package/engine/src/session.rs
CHANGED
|
@@ -1875,7 +1875,7 @@ impl Session {
|
|
|
1875
1875
|
.ok_or_else(|| anyhow::anyhow!("no offer has been made for this session yet"))
|
|
1876
1876
|
}
|
|
1877
1877
|
|
|
1878
|
-
/// Restart ICE: a fresh offer on the same peer, announced as a new
|
|
1878
|
+
/// Restart ICE: a pending or fresh offer on the same peer, announced as a new
|
|
1879
1879
|
/// `session.description` event for the client to answer as usual.
|
|
1880
1880
|
pub async fn restart_ice(&self) -> Result<()> {
|
|
1881
1881
|
let offer = self.current_peer()?.restart_ice().await?;
|
|
@@ -4243,6 +4243,42 @@ mod tests {
|
|
|
4243
4243
|
);
|
|
4244
4244
|
}
|
|
4245
4245
|
|
|
4246
|
+
#[tokio::test]
|
|
4247
|
+
async fn three_queued_ice_restarts_reemit_one_offer_and_accept_its_answer() {
|
|
4248
|
+
let codec = crate::peer::tests::h264_receiver_codec();
|
|
4249
|
+
let (peer, _) = crate::peer::tests::answered(vec![codec.clone()]).await;
|
|
4250
|
+
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4251
|
+
let (inner, _) = test_inner_with(events, peer, 64, 64);
|
|
4252
|
+
let session = Session { inner };
|
|
4253
|
+
let mut offers = Vec::new();
|
|
4254
|
+
for _ in 0..3 {
|
|
4255
|
+
session.restart_ice().await.expect("restart accepted");
|
|
4256
|
+
match received.recv().await.expect("offer event").event {
|
|
4257
|
+
SessionEvent::Description { sdp, .. } => offers.push(sdp),
|
|
4258
|
+
_ => panic!("expected the pending offer"),
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
let ufrag = |sdp: &str| {
|
|
4262
|
+
sdp.lines()
|
|
4263
|
+
.find_map(|line| line.strip_prefix("a=ice-ufrag:"))
|
|
4264
|
+
.expect("ICE credentials")
|
|
4265
|
+
.to_owned()
|
|
4266
|
+
};
|
|
4267
|
+
assert!(offers.iter().all(|sdp| ufrag(sdp) == ufrag(&offers[0])));
|
|
4268
|
+
let client = crate::peer::tests::answerer_with(&offers[0], vec![codec]).await;
|
|
4269
|
+
let answer = client.create_answer(None).await.expect("client answer");
|
|
4270
|
+
client
|
|
4271
|
+
.set_local_description(answer.clone())
|
|
4272
|
+
.await
|
|
4273
|
+
.expect("local answer");
|
|
4274
|
+
session
|
|
4275
|
+
.accept_answer(answer.sdp)
|
|
4276
|
+
.await
|
|
4277
|
+
.expect("pending answer accepted");
|
|
4278
|
+
client.close().await.expect("close client");
|
|
4279
|
+
session.current_peer().unwrap().close().await;
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4246
4282
|
#[tokio::test]
|
|
4247
4283
|
async fn a_lost_transport_releases_input_then_ends_the_session_without_sctp_close() {
|
|
4248
4284
|
use webrtc::peer_connection::RTCPeerConnectionState as State;
|