@desklink/host 0.5.3 → 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.
Files changed (64) hide show
  1. package/README.md +25 -17
  2. package/dist/engineProcess.d.ts +5 -0
  3. package/dist/engineProcess.d.ts.map +1 -1
  4. package/dist/engineProcess.js +5 -3
  5. package/dist/engineProcess.js.map +1 -1
  6. package/engine/Cargo.lock +6 -8
  7. package/engine/Cargo.toml +13 -1
  8. package/engine/src/clipboard.rs +0 -34
  9. package/engine/src/convert.rs +0 -170
  10. package/engine/src/cursor.rs +0 -23
  11. package/engine/src/input.rs +0 -65
  12. package/engine/src/input_mac.rs +0 -11
  13. package/engine/src/keeper.rs +0 -137
  14. package/engine/src/keymap.rs +0 -107
  15. package/engine/src/keymap_mac.rs +0 -25
  16. package/engine/src/mac.rs +0 -28
  17. package/engine/src/peer.rs +224 -0
  18. package/engine/src/portal.rs +0 -18
  19. package/engine/src/protocol.rs +0 -162
  20. package/engine/src/x11.rs +0 -13
  21. package/engine/vendor/rtc-ice/Cargo.toml +137 -0
  22. package/engine/vendor/rtc-ice/DESKLINK-PATCH.md +59 -0
  23. package/engine/vendor/rtc-ice/LICENSE-APACHE +201 -0
  24. package/engine/vendor/rtc-ice/LICENSE-MIT +21 -0
  25. package/engine/vendor/rtc-ice/README.md +30 -0
  26. package/engine/vendor/rtc-ice/examples/ping_pong.rs +376 -0
  27. package/engine/vendor/rtc-ice/src/agent/agent_config.rs +126 -0
  28. package/engine/vendor/rtc-ice/src/agent/agent_proto.rs +172 -0
  29. package/engine/vendor/rtc-ice/src/agent/agent_selector.rs +566 -0
  30. package/engine/vendor/rtc-ice/src/agent/agent_stats.rs +292 -0
  31. package/engine/vendor/rtc-ice/src/agent/agent_test.rs +3388 -0
  32. package/engine/vendor/rtc-ice/src/agent/mod.rs +1662 -0
  33. package/engine/vendor/rtc-ice/src/attributes/control/control_test.rs +168 -0
  34. package/engine/vendor/rtc-ice/src/attributes/control/mod.rs +142 -0
  35. package/engine/vendor/rtc-ice/src/attributes/mod.rs +6 -0
  36. package/engine/vendor/rtc-ice/src/attributes/priority/mod.rs +37 -0
  37. package/engine/vendor/rtc-ice/src/attributes/priority/priority_test.rs +39 -0
  38. package/engine/vendor/rtc-ice/src/attributes/use_candidate/mod.rs +33 -0
  39. package/engine/vendor/rtc-ice/src/attributes/use_candidate/use_candidate_test.rs +19 -0
  40. package/engine/vendor/rtc-ice/src/candidate/candidate_host.rs +53 -0
  41. package/engine/vendor/rtc-ice/src/candidate/candidate_pair.rs +215 -0
  42. package/engine/vendor/rtc-ice/src/candidate/candidate_pair_test.rs +201 -0
  43. package/engine/vendor/rtc-ice/src/candidate/candidate_peer_reflexive.rs +49 -0
  44. package/engine/vendor/rtc-ice/src/candidate/candidate_relay.rs +53 -0
  45. package/engine/vendor/rtc-ice/src/candidate/candidate_relay_test.rs +114 -0
  46. package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive.rs +53 -0
  47. package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive_test.rs +91 -0
  48. package/engine/vendor/rtc-ice/src/candidate/candidate_test.rs +540 -0
  49. package/engine/vendor/rtc-ice/src/candidate/mod.rs +679 -0
  50. package/engine/vendor/rtc-ice/src/lib.rs +85 -0
  51. package/engine/vendor/rtc-ice/src/mdns/mdns_test.rs +150 -0
  52. package/engine/vendor/rtc-ice/src/mdns/mod.rs +69 -0
  53. package/engine/vendor/rtc-ice/src/network_type/mod.rs +162 -0
  54. package/engine/vendor/rtc-ice/src/network_type/network_type_test.rs +95 -0
  55. package/engine/vendor/rtc-ice/src/rand/mod.rs +91 -0
  56. package/engine/vendor/rtc-ice/src/rand/rand_test.rs +115 -0
  57. package/engine/vendor/rtc-ice/src/state/mod.rs +106 -0
  58. package/engine/vendor/rtc-ice/src/state/state_test.rs +46 -0
  59. package/engine/vendor/rtc-ice/src/stats/mod.rs +166 -0
  60. package/engine/vendor/rtc-ice/src/tcp_type/mod.rs +47 -0
  61. package/engine/vendor/rtc-ice/src/tcp_type/tcp_type_test.rs +18 -0
  62. package/engine/vendor/rtc-ice/src/url/mod.rs +266 -0
  63. package/engine/vendor/rtc-ice/src/url/url_test.rs +142 -0
  64. package/package.json +8 -3
@@ -483,140 +483,3 @@ fn fatal(out: &mut impl Write, error: &str) -> Result<()> {
483
483
  write_line(out, &serde_json::json!({ "error": error }).to_string())?;
484
484
  Err(anyhow!("{error}"))
485
485
  }
486
-
487
- #[cfg(test)]
488
- mod tests {
489
- use super::*;
490
-
491
- #[test]
492
- fn content_fills_the_screen_and_helpers_are_parked() {
493
- assert_eq!(kiosk_geometry(1240, 760, 1280, 800), (0, 0, 1280, 800));
494
- // Exactly at the limit is content; one pixel under is not.
495
- assert_eq!(kiosk_geometry(120, 120, 1280, 800), (0, 0, 1280, 800));
496
- assert_eq!(
497
- kiosk_geometry(119, 800, 1280, 800),
498
- (i32::from(PARKED_XY), i32::from(PARKED_XY), 119, 800)
499
- );
500
- assert_eq!(
501
- kiosk_geometry(1000, 24, 1280, 800),
502
- (i32::from(PARKED_XY), i32::from(PARKED_XY), 1000, 24)
503
- );
504
- }
505
-
506
- #[test]
507
- fn wm_class_reads_the_class_not_the_instance() {
508
- assert_eq!(
509
- decode_wm_class(b"google-chrome\0Google-chrome\0"),
510
- Some(String::from("Google-chrome"))
511
- );
512
- assert_eq!(decode_wm_class(b"instance-only\0"), None);
513
- assert_eq!(decode_wm_class(b""), None);
514
- }
515
-
516
- #[test]
517
- fn titles_come_from_either_name_property_and_blank_is_none() {
518
- assert_eq!(
519
- decode_title("Pricing — Acme Store"),
520
- Some(String::from("Pricing — Acme Store"))
521
- );
522
- assert_eq!(
523
- decode_title("PPF probe - Google Chrome"),
524
- Some(String::from("PPF probe - Google Chrome"))
525
- );
526
- assert_eq!(decode_title(" "), None);
527
- assert_eq!(decode_title(""), None);
528
- assert_eq!(
529
- decode_title(&latin1(b"Gr\xf6\xdfe")),
530
- Some(String::from("Gr\u{00f6}\u{00df}e"))
531
- );
532
- }
533
-
534
- #[test]
535
- fn the_report_line_is_exactly_the_promised_shape() {
536
- let line = window_line(&[KeeperWindow {
537
- id: 0x0040_0001,
538
- title: Some(String::from("PPF probe - Google Chrome")),
539
- class: Some(String::from("Google-chrome")),
540
- pid: Some(1003571),
541
- width: 1280,
542
- height: 800,
543
- }]);
544
- let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
545
- assert_eq!(
546
- parsed,
547
- serde_json::json!({
548
- "windows": [{
549
- "id": 0x40_0001_u64,
550
- "title": "PPF probe - Google Chrome",
551
- "class": "Google-chrome",
552
- "pid": 1003571,
553
- "width": 1280,
554
- "height": 800,
555
- }]
556
- })
557
- );
558
- assert_eq!(window_line(&[]), r#"{"windows":[]}"#);
559
- }
560
-
561
- #[test]
562
- fn missing_fields_are_null_not_absent() {
563
- let line = window_line(&[KeeperWindow {
564
- id: 7,
565
- title: None,
566
- class: None,
567
- pid: None,
568
- width: 100,
569
- height: 100,
570
- }]);
571
- let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
572
- let window = &parsed["windows"][0];
573
- assert_eq!(window["title"], serde_json::Value::Null);
574
- assert_eq!(window["class"], serde_json::Value::Null);
575
- assert_eq!(window["pid"], serde_json::Value::Null);
576
- assert!(window.get("width").is_some());
577
- }
578
-
579
- #[test]
580
- fn an_unopenable_display_is_refused_in_one_error_line() {
581
- let mut out = Vec::new();
582
- assert!(keep(Some("not a display"), &mut out).is_err());
583
- let line: serde_json::Value =
584
- serde_json::from_str(&String::from_utf8(out).unwrap()).unwrap();
585
- assert!(line["error"]
586
- .as_str()
587
- .is_some_and(|error| !error.is_empty()));
588
- }
589
-
590
- #[test]
591
- fn a_refused_keeper_says_why_in_its_error_line() {
592
- let mut out = Vec::new();
593
- let error = fatal(&mut out, "another window manager").unwrap_err();
594
- assert_eq!(error.to_string(), "another window manager");
595
- assert_eq!(
596
- String::from_utf8(out).unwrap(),
597
- "{\"error\":\"another window manager\"}\n"
598
- );
599
- }
600
-
601
- #[test]
602
- fn the_display_flag_has_one_spelling() {
603
- let flag = |args: &[&str]| {
604
- display_argument(
605
- &args
606
- .iter()
607
- .map(|arg| String::from(*arg))
608
- .collect::<Vec<_>>(),
609
- )
610
- };
611
- assert_eq!(flag(&["--display", ":42"]), Some(String::from(":42")));
612
- assert_eq!(
613
- flag(&["--display", ":42", "--x"]),
614
- Some(String::from(":42"))
615
- );
616
- assert_eq!(flag(&[]), None);
617
- assert_eq!(flag(&["--display"]), None);
618
- // The undocumented `--display=:42` spelling is not a display: `keep`
619
- // refuses it rather than guessing what was meant.
620
- assert_eq!(flag(&["--display=:42"]), None);
621
- }
622
- }
@@ -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
- }
@@ -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
- }
@@ -1153,6 +1153,7 @@ fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
1153
1153
  #[cfg(test)]
1154
1154
  pub(crate) mod tests {
1155
1155
  use super::*;
1156
+ use webrtc::media_stream::track_remote::{TrackRemote, TrackRemoteEvent};
1156
1157
 
1157
1158
  #[test]
1158
1159
  fn a_standing_queue_is_measured_and_one_frames_burst_is_not() {
@@ -1589,6 +1590,229 @@ pub(crate) mod tests {
1589
1590
  .expect("the restart answer is accepted");
1590
1591
  }
1591
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
+
1592
1816
  #[tokio::test]
1593
1817
  async fn a_forwarded_client_is_offered_ice_over_tcp_on_the_loopback() {
1594
1818
  let offered = |loopback_tcp: bool| async move {
@@ -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
- }