@desklink/host 0.5.3 → 0.5.6

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 (76) hide show
  1. package/README.md +33 -19
  2. package/dist/bridge.js +3 -3
  3. package/dist/bridge.js.map +1 -1
  4. package/dist/engineProcess.d.ts +5 -0
  5. package/dist/engineProcess.d.ts.map +1 -1
  6. package/dist/engineProcess.js +6 -3
  7. package/dist/engineProcess.js.map +1 -1
  8. package/dist/protocol.d.ts +39 -0
  9. package/dist/protocol.d.ts.map +1 -1
  10. package/dist/protocol.js.map +1 -1
  11. package/docs/BOUNDARY.md +4 -2
  12. package/docs/PROTOCOL.md +70 -7
  13. package/engine/Cargo.lock +6 -8
  14. package/engine/Cargo.toml +13 -1
  15. package/engine/src/audio.rs +398 -0
  16. package/engine/src/clipboard.rs +0 -34
  17. package/engine/src/convert.rs +0 -170
  18. package/engine/src/cursor.rs +0 -23
  19. package/engine/src/input.rs +0 -65
  20. package/engine/src/input_mac.rs +0 -11
  21. package/engine/src/keeper.rs +0 -137
  22. package/engine/src/keymap.rs +0 -107
  23. package/engine/src/keymap_mac.rs +0 -25
  24. package/engine/src/mac.rs +1 -28
  25. package/engine/src/main.rs +22 -0
  26. package/engine/src/peer.rs +297 -0
  27. package/engine/src/portal.rs +0 -18
  28. package/engine/src/protocol.rs +22 -162
  29. package/engine/src/session.rs +176 -3
  30. package/engine/src/win.rs +1 -0
  31. package/engine/src/x11.rs +0 -13
  32. package/engine/vendor/rtc-ice/Cargo.toml +137 -0
  33. package/engine/vendor/rtc-ice/DESKLINK-PATCH.md +111 -0
  34. package/engine/vendor/rtc-ice/LICENSE-APACHE +201 -0
  35. package/engine/vendor/rtc-ice/LICENSE-MIT +21 -0
  36. package/engine/vendor/rtc-ice/README.md +30 -0
  37. package/engine/vendor/rtc-ice/examples/ping_pong.rs +376 -0
  38. package/engine/vendor/rtc-ice/src/agent/agent_config.rs +126 -0
  39. package/engine/vendor/rtc-ice/src/agent/agent_proto.rs +172 -0
  40. package/engine/vendor/rtc-ice/src/agent/agent_selector.rs +647 -0
  41. package/engine/vendor/rtc-ice/src/agent/agent_stats.rs +292 -0
  42. package/engine/vendor/rtc-ice/src/agent/agent_test.rs +3594 -0
  43. package/engine/vendor/rtc-ice/src/agent/mod.rs +1753 -0
  44. package/engine/vendor/rtc-ice/src/attributes/control/control_test.rs +168 -0
  45. package/engine/vendor/rtc-ice/src/attributes/control/mod.rs +142 -0
  46. package/engine/vendor/rtc-ice/src/attributes/mod.rs +6 -0
  47. package/engine/vendor/rtc-ice/src/attributes/priority/mod.rs +37 -0
  48. package/engine/vendor/rtc-ice/src/attributes/priority/priority_test.rs +39 -0
  49. package/engine/vendor/rtc-ice/src/attributes/use_candidate/mod.rs +33 -0
  50. package/engine/vendor/rtc-ice/src/attributes/use_candidate/use_candidate_test.rs +19 -0
  51. package/engine/vendor/rtc-ice/src/candidate/candidate_host.rs +53 -0
  52. package/engine/vendor/rtc-ice/src/candidate/candidate_pair.rs +215 -0
  53. package/engine/vendor/rtc-ice/src/candidate/candidate_pair_test.rs +201 -0
  54. package/engine/vendor/rtc-ice/src/candidate/candidate_peer_reflexive.rs +49 -0
  55. package/engine/vendor/rtc-ice/src/candidate/candidate_relay.rs +53 -0
  56. package/engine/vendor/rtc-ice/src/candidate/candidate_relay_test.rs +114 -0
  57. package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive.rs +53 -0
  58. package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive_test.rs +91 -0
  59. package/engine/vendor/rtc-ice/src/candidate/candidate_test.rs +540 -0
  60. package/engine/vendor/rtc-ice/src/candidate/mod.rs +679 -0
  61. package/engine/vendor/rtc-ice/src/dl_trace.rs +49 -0
  62. package/engine/vendor/rtc-ice/src/lib.rs +88 -0
  63. package/engine/vendor/rtc-ice/src/mdns/mdns_test.rs +150 -0
  64. package/engine/vendor/rtc-ice/src/mdns/mod.rs +69 -0
  65. package/engine/vendor/rtc-ice/src/network_type/mod.rs +162 -0
  66. package/engine/vendor/rtc-ice/src/network_type/network_type_test.rs +95 -0
  67. package/engine/vendor/rtc-ice/src/rand/mod.rs +91 -0
  68. package/engine/vendor/rtc-ice/src/rand/rand_test.rs +115 -0
  69. package/engine/vendor/rtc-ice/src/state/mod.rs +106 -0
  70. package/engine/vendor/rtc-ice/src/state/state_test.rs +46 -0
  71. package/engine/vendor/rtc-ice/src/stats/mod.rs +166 -0
  72. package/engine/vendor/rtc-ice/src/tcp_type/mod.rs +47 -0
  73. package/engine/vendor/rtc-ice/src/tcp_type/tcp_type_test.rs +18 -0
  74. package/engine/vendor/rtc-ice/src/url/mod.rs +266 -0
  75. package/engine/vendor/rtc-ice/src/url/url_test.rs +142 -0
  76. package/package.json +8 -3
@@ -47,6 +47,33 @@ use webrtc::peer_connection::{
47
47
  };
48
48
  use webrtc::runtime::default_runtime;
49
49
 
50
+ // desklink scout trace (dl-roam-postnom-attr): engine-side restart timeline.
51
+ // Enabled with `DESKLINK_ICE_TRACE=1`; stamps microseconds since the Unix epoch
52
+ // so these lines interleave with the vendored ICE agent's `[ice ...]` lines in
53
+ // one order. Droppable: grep `desklink scout trace`.
54
+ pub(crate) fn dl_trace_on() -> bool {
55
+ std::env::var("DESKLINK_ICE_TRACE")
56
+ .map(|v| v != "0" && !v.is_empty())
57
+ .unwrap_or(false)
58
+ }
59
+
60
+ pub(crate) fn dl_trace_us() -> u128 {
61
+ std::time::SystemTime::now()
62
+ .duration_since(std::time::UNIX_EPOCH)
63
+ .map(|d| d.as_micros())
64
+ .unwrap_or(0)
65
+ }
66
+
67
+ macro_rules! dl_eng {
68
+ ($($arg:tt)*) => {
69
+ if $crate::peer::dl_trace_on() {
70
+ eprintln!("[eng {}] {}", $crate::peer::dl_trace_us(), format_args!($($arg)*));
71
+ }
72
+ };
73
+ }
74
+
75
+ pub(crate) use dl_eng;
76
+
50
77
  /// VP9 clock rate, fixed by RFC 9628.
51
78
  const VP9_CLOCK_RATE: u32 = 90_000;
52
79
 
@@ -420,6 +447,12 @@ impl PeerConnectionEventHandler for Handler {
420
447
  }
421
448
 
422
449
  async fn on_connection_state_change(&self, state: RTCPeerConnectionState) {
450
+ // desklink scout trace (dl-roam-postnom-attr): the engine's rtc-level
451
+ // transport state event, on the same microsecond clock as the vendored
452
+ // agent's `[ice ...]` lines, so the rtc Connected gate is separable.
453
+ if dl_trace_on() {
454
+ dl_eng!("peer_state_change {state:?}");
455
+ }
423
456
  let connected = matches!(state, RTCPeerConnectionState::Connected);
424
457
  if connected {
425
458
  self.wants_keyframe.store(true, Ordering::SeqCst);
@@ -1153,6 +1186,7 @@ fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
1153
1186
  #[cfg(test)]
1154
1187
  pub(crate) mod tests {
1155
1188
  use super::*;
1189
+ use webrtc::media_stream::track_remote::{TrackRemote, TrackRemoteEvent};
1156
1190
 
1157
1191
  #[test]
1158
1192
  fn a_standing_queue_is_measured_and_one_frames_burst_is_not() {
@@ -1589,6 +1623,269 @@ pub(crate) mod tests {
1589
1623
  .expect("the restart answer is accepted");
1590
1624
  }
1591
1625
 
1626
+ /// How long the restarted generation may take to nominate and reconnect,
1627
+ /// measured from the answered offer: two `check_interval` ticks. The vendored
1628
+ /// agent ticks every 200 ms (`vendor/rtc-ice/src/agent/agent_config.rs`), and
1629
+ /// the routed roam leg measured the engine's own first-valid-pair ->
1630
+ /// USE-CANDIDATE at 197 ms, one tick; the extra tick is headroom for the
1631
+ /// loopback's round trip and for a loaded machine. See the assertion in
1632
+ /// `a_restart_keeps_media_flowing_on_the_selected_pair_until_the_new_one_nominates`.
1633
+ const RECONNECT_BUDGET: Duration = Duration::from_millis(400);
1634
+
1635
+ /// A receiver peer that records when each inbound RTP packet arrives and
1636
+ /// reports its own ICE candidates, so two peers can really connect.
1637
+ struct MediaReceiver {
1638
+ candidates: tokio::sync::mpsc::UnboundedSender<RTCIceCandidateInit>,
1639
+ arrivals: Arc<Mutex<Vec<Instant>>>,
1640
+ connected: Arc<AtomicBool>,
1641
+ }
1642
+
1643
+ #[async_trait::async_trait]
1644
+ impl PeerConnectionEventHandler for MediaReceiver {
1645
+ async fn on_ice_candidate(&self, event: RTCPeerConnectionIceEvent) {
1646
+ if let Ok(init) = event.candidate.to_json() {
1647
+ let _ = self.candidates.send(init);
1648
+ }
1649
+ }
1650
+
1651
+ async fn on_connection_state_change(&self, state: RTCPeerConnectionState) {
1652
+ self.connected.store(
1653
+ matches!(state, RTCPeerConnectionState::Connected),
1654
+ Ordering::SeqCst,
1655
+ );
1656
+ }
1657
+
1658
+ async fn on_track(&self, track: Arc<dyn TrackRemote>) {
1659
+ let arrivals = Arc::clone(&self.arrivals);
1660
+ tokio::spawn(async move {
1661
+ while let Some(event) = track.poll().await {
1662
+ if matches!(event, TrackRemoteEvent::OnRtpPacket(_)) {
1663
+ arrivals.lock().unwrap().push(Instant::now());
1664
+ }
1665
+ }
1666
+ });
1667
+ }
1668
+ }
1669
+
1670
+ async fn media_receiver(receiver: Arc<MediaReceiver>) -> Arc<dyn PeerConnection> {
1671
+ let mut media_engine = MediaEngine::default();
1672
+ media_engine
1673
+ .register_codec(vp9_codec(), RtpCodecKind::Video)
1674
+ .expect("the codec registers");
1675
+ let registry = register_default_interceptors(Registry::new(), &mut media_engine)
1676
+ .expect("interceptors register");
1677
+ let peer = PeerConnectionBuilder::<std::net::SocketAddr>::new()
1678
+ .with_media_engine(media_engine)
1679
+ .with_interceptor_registry(registry)
1680
+ .with_runtime(default_runtime().expect("a runtime"))
1681
+ .with_handler(receiver)
1682
+ .with_udp_addrs(vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))])
1683
+ .build()
1684
+ .await
1685
+ .expect("a receiving peer");
1686
+ Arc::new(peer)
1687
+ }
1688
+
1689
+ /// A restart offer/answer round must not interrupt media already flowing on
1690
+ /// the selected pair: the picture keeps advancing (no gap over 200 ms) while
1691
+ /// the old pair stays reachable, and the ICE generation still turns over.
1692
+ #[tokio::test]
1693
+ async fn a_restart_keeps_media_flowing_on_the_selected_pair_until_the_new_one_nominates() {
1694
+ let (events, mut events_rx) = tokio::sync::mpsc::unbounded_channel();
1695
+ let (engine, offer) = VideoPeer::offer(transport(), events, VideoCodec::Vp9)
1696
+ .await
1697
+ .expect("an offering peer");
1698
+ let engine = Arc::new(engine);
1699
+
1700
+ let arrivals: Arc<Mutex<Vec<Instant>>> = Arc::new(Mutex::new(Vec::new()));
1701
+ let connected = Arc::new(AtomicBool::new(false));
1702
+ let (client_candidates_tx, mut client_candidates_rx) =
1703
+ tokio::sync::mpsc::unbounded_channel();
1704
+ let receiver = Arc::new(MediaReceiver {
1705
+ candidates: client_candidates_tx,
1706
+ arrivals: Arc::clone(&arrivals),
1707
+ connected: Arc::clone(&connected),
1708
+ });
1709
+ let client = media_receiver(Arc::clone(&receiver)).await;
1710
+
1711
+ client
1712
+ .set_remote_description(RTCSessionDescription::offer(offer.clone()).expect("an offer"))
1713
+ .await
1714
+ .expect("the offer applies");
1715
+ let answer = client.create_answer(None).await.expect("an answer");
1716
+ client
1717
+ .set_local_description(answer.clone())
1718
+ .await
1719
+ .expect("a local answer");
1720
+ engine
1721
+ .accept_answer(answer.sdp.clone())
1722
+ .await
1723
+ .expect("the answer is accepted");
1724
+
1725
+ // The engine's candidates reach the client.
1726
+ let client_for_engine_candidates = Arc::clone(&client);
1727
+ tokio::spawn(async move {
1728
+ while let Some(event) = events_rx.recv().await {
1729
+ if let PeerEvent::Candidate {
1730
+ candidate,
1731
+ sdp_mid,
1732
+ sdp_m_line_index,
1733
+ } = event
1734
+ {
1735
+ let _ = client_for_engine_candidates
1736
+ .add_ice_candidate(RTCIceCandidateInit {
1737
+ candidate,
1738
+ sdp_mid,
1739
+ sdp_mline_index: sdp_m_line_index,
1740
+ username_fragment: None,
1741
+ url: None,
1742
+ })
1743
+ .await;
1744
+ }
1745
+ }
1746
+ });
1747
+ // The client's candidates reach the engine.
1748
+ let engine_for_client_candidates = Arc::clone(&engine);
1749
+ tokio::spawn(async move {
1750
+ while let Some(init) = client_candidates_rx.recv().await {
1751
+ let _ = engine_for_client_candidates
1752
+ .add_candidate(init.candidate, init.sdp_mid, init.sdp_mline_index)
1753
+ .await;
1754
+ }
1755
+ });
1756
+
1757
+ // Wait for the ICE path to come up on both ends.
1758
+ let deadline = Instant::now() + Duration::from_secs(5);
1759
+ while !(engine.is_connected() && connected.load(Ordering::SeqCst)) {
1760
+ assert!(Instant::now() < deadline, "the peers never connected");
1761
+ tokio::time::sleep(Duration::from_millis(10)).await;
1762
+ }
1763
+
1764
+ // Any bytes the VP9 packetizer turns into one access unit.
1765
+ let frame: Vec<u8> = (0..200u32).map(|i| (i % 251) as u8).collect();
1766
+ let sending = Arc::new(AtomicBool::new(true));
1767
+ let engine_for_sending = Arc::clone(&engine);
1768
+ let sending_for_task = Arc::clone(&sending);
1769
+ let frame_for_task = frame.clone();
1770
+ tokio::spawn(async move {
1771
+ while sending_for_task.load(Ordering::SeqCst) {
1772
+ let _ = engine_for_sending
1773
+ .send_frame(&frame_for_task, true, Instant::now())
1774
+ .await;
1775
+ tokio::time::sleep(Duration::from_millis(5)).await;
1776
+ }
1777
+ });
1778
+
1779
+ // Let the picture flow, then take a clean baseline.
1780
+ tokio::time::sleep(Duration::from_millis(300)).await;
1781
+ arrivals.lock().unwrap().clear();
1782
+ tokio::time::sleep(Duration::from_millis(200)).await;
1783
+ let flowing = arrivals.lock().unwrap().len();
1784
+ assert!(
1785
+ flowing >= 5,
1786
+ "media was not flowing before the restart: {flowing} packets"
1787
+ );
1788
+
1789
+ // The restart a client drives when its carrier comes back on a roam.
1790
+ let restart = engine.restart_ice().await.expect("a restart offer");
1791
+ let ufrag = |sdp: &str| {
1792
+ sdp.lines()
1793
+ .find_map(|line| line.strip_prefix("a=ice-ufrag:"))
1794
+ .expect("ICE credentials")
1795
+ .to_owned()
1796
+ };
1797
+ assert_ne!(
1798
+ ufrag(&restart),
1799
+ ufrag(&offer),
1800
+ "the restart must turn the ICE generation over"
1801
+ );
1802
+
1803
+ let restart_at = Instant::now();
1804
+ arrivals.lock().unwrap().clear();
1805
+
1806
+ // The restart offer crosses the consumer's signaling channel before the
1807
+ // client answers, and the engine applies the restart at
1808
+ // `set_local_description` — inside `restart_ice` above, before the offer
1809
+ // has even left. It must keep feeding media on the old pair for that
1810
+ // whole round trip, not only until its own teardown. A real channel
1811
+ // costs more than a loopback hop, so hold this window for the bar's
1812
+ // length: a teardown shows up here, a preserved pair does not.
1813
+ tokio::time::sleep(Duration::from_millis(300)).await;
1814
+
1815
+ client
1816
+ .set_remote_description(RTCSessionDescription::offer(restart).expect("a restart offer"))
1817
+ .await
1818
+ .expect("the restart offer applies");
1819
+ let reanswer = client.create_answer(None).await.expect("an answer");
1820
+ client
1821
+ .set_local_description(reanswer.clone())
1822
+ .await
1823
+ .expect("a local answer");
1824
+ engine
1825
+ .accept_answer(reanswer.sdp)
1826
+ .await
1827
+ .expect("the restart answer is accepted");
1828
+ let answered_at = Instant::now();
1829
+
1830
+ // The restarted generation has to be nominated off its own check cycle:
1831
+ // nomination is decided in the agent's `contact_candidates` pass, which the
1832
+ // driver reaches on a `check_interval` tick (200 ms) or when an inbound
1833
+ // Binding request arrives while a valid pair already exists. A pair that
1834
+ // becomes valid just after a tick would therefore wait a full tick for its
1835
+ // USE-CANDIDATE, and one tick is the whole engine-side cost measured on the
1836
+ // routed roam leg (first valid pair -> USE-CANDIDATE: 197 ms, i.e. one tick).
1837
+ //
1838
+ // What this asserts, and what it cannot: the engine's "first valid pair"
1839
+ // instant is not observable from here (the peer connection exposes no pair
1840
+ // state), so the bound is measured from the answer, the earliest instant the
1841
+ // engine could nominate, at two ticks. The loopback's client answers checks
1842
+ // and sends its own triggered checks in the same burst, so an inbound request
1843
+ // is nearly always available and the base engine meets this bound too; the
1844
+ // tick itself is graded by the routed roam leg, whose relay log carries both
1845
+ // instants (197 ms, one tick). This guards the property going forward: a
1846
+ // lost pass, a skipped tick or a stalled nomination blows the bound.
1847
+ let reconnected = async {
1848
+ while !engine.is_connected() {
1849
+ tokio::time::sleep(Duration::from_millis(5)).await;
1850
+ }
1851
+ answered_at.elapsed()
1852
+ };
1853
+ let reconnect = tokio::time::timeout(RECONNECT_BUDGET, reconnected)
1854
+ .await
1855
+ .expect("the restarted generation was never nominated");
1856
+ eprintln!(
1857
+ "restart: transport reconnected {reconnect:?} after the answer (budget {RECONNECT_BUDGET:?})"
1858
+ );
1859
+
1860
+ // Keep the picture advancing across and after the restart.
1861
+ tokio::time::sleep(Duration::from_millis(800)).await;
1862
+ sending.store(false, Ordering::SeqCst);
1863
+
1864
+ let stamps = arrivals.lock().unwrap().clone();
1865
+ assert!(
1866
+ !stamps.is_empty(),
1867
+ "no media arrived at all after the restart"
1868
+ );
1869
+ let mut previous = restart_at;
1870
+ let mut worst = Duration::ZERO;
1871
+ for stamp in &stamps {
1872
+ worst = worst.max(stamp.duration_since(previous));
1873
+ previous = *stamp;
1874
+ }
1875
+ eprintln!(
1876
+ "restart: {} packets after the offer, worst media gap {worst:?}",
1877
+ stamps.len()
1878
+ );
1879
+ assert!(
1880
+ worst < Duration::from_millis(200),
1881
+ "media stalled for {worst:?} through the restart: {:?}",
1882
+ stamps
1883
+ );
1884
+
1885
+ engine.close().await;
1886
+ let _ = client.close().await;
1887
+ }
1888
+
1592
1889
  #[tokio::test]
1593
1890
  async fn a_forwarded_client_is_offered_ice_over_tcp_on_the_loopback() {
1594
1891
  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
- }
@@ -144,6 +144,11 @@ pub struct OpenParams {
144
144
  /// the engine then spends nothing per frame on either.
145
145
  #[serde(default = "default_local_frames")]
146
146
  pub local_frames: bool,
147
+ /// Capture the desktop's own system audio for `session.audio`. Absent means
148
+ /// video only; there is never a silent downgrade, so a caller that asks and
149
+ /// cannot be answered is refused.
150
+ #[serde(default)]
151
+ pub audio: bool,
147
152
  }
148
153
 
149
154
  fn default_local_frames() -> bool {
@@ -244,6 +249,23 @@ pub struct SessionRef {
244
249
  pub generation: Option<u64>,
245
250
  }
246
251
 
252
+ /// Read the desktop's own audio, the way `FrameParams` reads a frame: bytes go
253
+ /// to the caller's path and JSON carries only metadata.
254
+ #[derive(Debug, Deserialize)]
255
+ pub struct AudioParams {
256
+ pub session_id: String,
257
+ #[serde(default)]
258
+ pub path: String,
259
+ /// The most bytes to write. Bounded to whole sample frames, so a truncated
260
+ /// chunk never ends mid-sample.
261
+ #[serde(default = "default_max_audio_bytes")]
262
+ pub max_bytes: usize,
263
+ }
264
+
265
+ fn default_max_audio_bytes() -> usize {
266
+ 1 << 20
267
+ }
268
+
247
269
  #[derive(Debug, Deserialize)]
248
270
  pub struct DescriptionParams {
249
271
  pub session_id: String,
@@ -395,168 +417,6 @@ pub enum ControlReply<'a> {
395
417
  },
396
418
  }
397
419
 
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
420
  /// Monotonic engine-process clock shared by local frame and cursor notices.
561
421
  pub fn timestamp_us(at: std::time::Instant) -> u64 {
562
422
  static EPOCH: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();