@desklink/host 0.2.0 → 0.3.1

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 (82) hide show
  1. package/NOTICE +26 -0
  2. package/README.md +134 -56
  3. package/bin/desklink-host.mjs +13 -5
  4. package/dist/bridge.d.ts +1 -0
  5. package/dist/bridge.d.ts.map +1 -1
  6. package/dist/bridge.js +12 -4
  7. package/dist/bridge.js.map +1 -1
  8. package/dist/engineProcess.d.ts +6 -1
  9. package/dist/engineProcess.d.ts.map +1 -1
  10. package/dist/engineProcess.js +35 -4
  11. package/dist/engineProcess.js.map +1 -1
  12. package/dist/protocol.d.ts +58 -0
  13. package/dist/protocol.d.ts.map +1 -1
  14. package/dist/protocol.js.map +1 -1
  15. package/dist/resolveEngine.d.ts +7 -1
  16. package/dist/resolveEngine.d.ts.map +1 -1
  17. package/dist/resolveEngine.js +66 -14
  18. package/dist/resolveEngine.js.map +1 -1
  19. package/docs/BOUNDARY.md +15 -7
  20. package/docs/PROTOCOL.md +267 -27
  21. package/engine/Cargo.lock +47 -2
  22. package/engine/Cargo.toml +18 -13
  23. package/engine/build.rs +86 -4
  24. package/engine/native/agent_overlay_mac.m +51 -4
  25. package/engine/native/agent_overlay_wayland.c +145 -12
  26. package/engine/native/agent_overlay_x11.c +72 -0
  27. package/engine/native/nvenc_shim.c +453 -0
  28. package/engine/native/vaapi_shim.c +727 -0
  29. package/engine/native/vpx_shim.c +54 -4
  30. package/engine/native/vt_shim.c +448 -0
  31. package/engine/src/capture.rs +167 -58
  32. package/engine/src/capture_mac.rs +24 -14
  33. package/engine/src/capture_win.rs +23 -0
  34. package/engine/src/clipboard_win.rs +8 -0
  35. package/engine/src/convert.rs +286 -61
  36. package/engine/src/cursor.rs +118 -0
  37. package/engine/src/encoder.rs +122 -13
  38. package/engine/src/h264.rs +188 -0
  39. package/engine/src/h264_nvenc.rs +308 -0
  40. package/engine/src/h264_openh264.rs +735 -0
  41. package/engine/src/h264_vaapi.rs +263 -0
  42. package/engine/src/h264_vt.rs +368 -0
  43. package/engine/src/indicator.rs +104 -3
  44. package/engine/src/input_win.rs +91 -0
  45. package/engine/src/keymap_win.rs +39 -0
  46. package/engine/src/mac.rs +34 -9
  47. package/engine/src/main.rs +186 -26
  48. package/engine/src/peer.rs +466 -28
  49. package/engine/src/portal.rs +42 -4
  50. package/engine/src/portal_win.rs +37 -0
  51. package/engine/src/protocol.rs +64 -2
  52. package/engine/src/session.rs +1252 -105
  53. package/engine/src/win.rs +110 -0
  54. package/engine/src/x11.rs +225 -9
  55. package/engine/src/x11_mac.rs +12 -0
  56. package/engine/vendor/libva/COPYING +19 -0
  57. package/engine/vendor/libva/va/va.h +5450 -0
  58. package/engine/vendor/libva/va/va_dec_av1.h +695 -0
  59. package/engine/vendor/libva/va/va_dec_hevc.h +616 -0
  60. package/engine/vendor/libva/va/va_dec_jpeg.h +157 -0
  61. package/engine/vendor/libva/va/va_dec_vp8.h +249 -0
  62. package/engine/vendor/libva/va/va_dec_vp9.h +312 -0
  63. package/engine/vendor/libva/va/va_dec_vvc.h +688 -0
  64. package/engine/vendor/libva/va/va_drm.h +61 -0
  65. package/engine/vendor/libva/va/va_enc_av1.h +1010 -0
  66. package/engine/vendor/libva/va/va_enc_h264.h +659 -0
  67. package/engine/vendor/libva/va/va_enc_hevc.h +958 -0
  68. package/engine/vendor/libva/va/va_enc_jpeg.h +162 -0
  69. package/engine/vendor/libva/va/va_enc_mpeg2.h +308 -0
  70. package/engine/vendor/libva/va/va_enc_vp8.h +350 -0
  71. package/engine/vendor/libva/va/va_enc_vp9.h +639 -0
  72. package/engine/vendor/libva/va/va_fei.h +170 -0
  73. package/engine/vendor/libva/va/va_fei_h264.h +505 -0
  74. package/engine/vendor/libva/va/va_fei_hevc.h +254 -0
  75. package/engine/vendor/libva/va/va_prot.h +525 -0
  76. package/engine/vendor/libva/va/va_version.h +87 -0
  77. package/engine/vendor/libva/va/va_vpp.h +1590 -0
  78. package/engine/vendor/nv-codec-headers/LICENSE +24 -0
  79. package/engine/vendor/nv-codec-headers/README +7 -0
  80. package/engine/vendor/nv-codec-headers/include/ffnvcodec/nvEncodeAPI.h +3825 -0
  81. package/examples/reference-client.html +1 -1
  82. package/package.json +4 -3
@@ -33,6 +33,7 @@ pub struct SelectedSource {
33
33
 
34
34
  pub struct PortalSession {
35
35
  pub fd: OwnedFd,
36
+ pub cursor_mode: &'static str,
36
37
  pub source: SelectedSource,
37
38
  pub restore_token: Option<String>,
38
39
  }
@@ -46,7 +47,10 @@ pub async fn open_display(_: Option<u32>) -> Result<PortalSession> {
46
47
  anyhow::bail!("display selection is only supported on macOS")
47
48
  }
48
49
 
49
- pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
50
+ pub async fn open(
51
+ restore_token: Option<&str>,
52
+ cursor: crate::protocol::CursorMode,
53
+ ) -> Result<PortalSession> {
50
54
  let proxy = Screencast::new()
51
55
  .await
52
56
  .context("compositor has no ScreenCast portal")?;
@@ -59,6 +63,15 @@ pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
59
63
  anyhow::bail!("ScreenCast portal advertises no monitor source");
60
64
  }
61
65
 
66
+ let cursor_mode = if cursor == crate::protocol::CursorMode::Hidden {
67
+ let available = proxy
68
+ .available_cursor_modes()
69
+ .await
70
+ .context("portal did not advertise cursor modes")?;
71
+ choose_cursor_mode(available)?
72
+ } else {
73
+ CursorMode::Embedded
74
+ };
62
75
  let session = proxy
63
76
  .create_session()
64
77
  .await
@@ -67,9 +80,7 @@ pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
67
80
  proxy
68
81
  .select_sources(
69
82
  &session,
70
- // The cursor is part of the picture the user drives; hiding it would
71
- // make precise placement impossible.
72
- CursorMode::Embedded,
83
+ cursor_mode,
73
84
  BitFlags::from(SourceType::Monitor),
74
85
  false,
75
86
  restore_token,
@@ -104,6 +115,11 @@ pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
104
115
  .context("OpenPipeWireRemote failed")?;
105
116
 
106
117
  Ok(PortalSession {
118
+ cursor_mode: match cursor_mode {
119
+ CursorMode::Metadata => "metadata",
120
+ CursorMode::Hidden => "hidden",
121
+ _ => "embedded",
122
+ },
107
123
  fd,
108
124
  source: SelectedSource {
109
125
  node_id: stream.pipe_wire_node_id(),
@@ -117,3 +133,25 @@ pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
117
133
  restore_token: streams.restore_token().map(str::to_owned),
118
134
  })
119
135
  }
136
+
137
+ fn choose_cursor_mode(available: BitFlags<CursorMode>) -> Result<CursorMode> {
138
+ if available.contains(CursorMode::Metadata) {
139
+ return Ok(CursorMode::Metadata);
140
+ }
141
+ anyhow::bail!("cursor_positions_unavailable: portal does not provide cursor metadata")
142
+ }
143
+
144
+ #[cfg(test)]
145
+ mod tests {
146
+ use super::*;
147
+ #[test]
148
+ fn hidden_requires_metadata_and_refuses_hidden_only_portals() {
149
+ assert_eq!(
150
+ choose_cursor_mode(CursorMode::Metadata | CursorMode::Hidden).unwrap(),
151
+ CursorMode::Metadata
152
+ );
153
+ let error = choose_cursor_mode(CursorMode::Hidden.into()).unwrap_err();
154
+ assert!(error.to_string().contains("cursor_positions_unavailable:"));
155
+ assert!(choose_cursor_mode(CursorMode::Embedded.into()).is_err());
156
+ }
157
+ }
@@ -0,0 +1,37 @@
1
+ use anyhow::{bail, Result};
2
+ use serde_json::Value;
3
+
4
+ /// A selected display; Windows enumeration is deferred to the capture lane.
5
+ #[derive(Debug, Clone, serde::Serialize)]
6
+ pub struct SelectedSource {
7
+ pub node_id: u32,
8
+ pub width: i32,
9
+ pub height: i32,
10
+ #[serde(skip_serializing_if = "Option::is_none")]
11
+ pub position: Option<(i32, i32)>,
12
+ #[serde(skip_serializing_if = "Option::is_none")]
13
+ pub source_type: Option<String>,
14
+ pub origin_x: i32,
15
+ pub origin_y: i32,
16
+ }
17
+
18
+ pub struct PortalSession {
19
+ pub source: SelectedSource,
20
+ pub restore_token: Option<String>,
21
+ }
22
+
23
+ pub async fn open(_restore_token: Option<&str>) -> Result<PortalSession> {
24
+ open_display_inner(None)
25
+ }
26
+
27
+ pub async fn open_display(display_id: Option<u32>) -> Result<PortalSession> {
28
+ open_display_inner(display_id)
29
+ }
30
+
31
+ fn open_display_inner(_display_id: Option<u32>) -> Result<PortalSession> {
32
+ bail!("{}", crate::capture::unavailable_reason())
33
+ }
34
+
35
+ pub fn displays() -> Vec<Value> {
36
+ Vec::new()
37
+ }
@@ -111,8 +111,18 @@ pub enum EncodedCodec {
111
111
  H264,
112
112
  }
113
113
 
114
+ #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115
+ #[serde(rename_all = "snake_case")]
116
+ pub enum CursorMode {
117
+ #[default]
118
+ Embedded,
119
+ Hidden,
120
+ }
121
+
114
122
  #[derive(Debug, Deserialize)]
115
123
  pub struct OpenParams {
124
+ #[serde(default)]
125
+ pub cursor: CursorMode,
116
126
  /// Absent means "the portal, with the user's consent".
117
127
  #[serde(default)]
118
128
  pub source: Option<SourceRequest>,
@@ -139,6 +149,15 @@ pub struct OpenParams {
139
149
  /// Show local, capture-excluded feedback for an agent controlling this session.
140
150
  #[serde(default)]
141
151
  pub agent_indicator: bool,
152
+ /// Keep the latest pre-encode frame for `session.frame` and emit
153
+ /// `session.frame.changed`. A consumer that only streams turns it off, and
154
+ /// the engine then spends nothing per frame on either.
155
+ #[serde(default = "default_local_frames")]
156
+ pub local_frames: bool,
157
+ }
158
+
159
+ fn default_local_frames() -> bool {
160
+ true
142
161
  }
143
162
 
144
163
  /// The encode box when the consumer names none: a desktop's own pixels up to
@@ -153,8 +172,24 @@ fn default_max_height() -> usize {
153
172
  fn default_bitrate() -> u32 {
154
173
  0
155
174
  }
175
+ /// A still desktop costs nothing at 60: frames are only produced on change,
176
+ /// so consumers get the smooth path without naming a rate.
156
177
  fn default_fps() -> u32 {
157
- 30
178
+ 60
179
+ }
180
+
181
+ /// A host-only visual cue; never an input message.
182
+ #[derive(Debug, Deserialize)]
183
+ #[serde(deny_unknown_fields)]
184
+ pub struct PointParams {
185
+ pub session_id: String,
186
+ pub x: Option<i64>,
187
+ pub y: Option<i64>,
188
+ #[serde(default)]
189
+ pub label: String,
190
+ pub timeout_ms: Option<u64>,
191
+ #[serde(default)]
192
+ pub clear: bool,
158
193
  }
159
194
 
160
195
  #[derive(Debug, Deserialize)]
@@ -366,6 +401,7 @@ pub enum ControlReply<'a> {
366
401
  },
367
402
  Revoked {
368
403
  reason: &'a str,
404
+ code: &'a str,
369
405
  },
370
406
  }
371
407
 
@@ -373,6 +409,16 @@ pub enum ControlReply<'a> {
373
409
  mod tests {
374
410
  use super::*;
375
411
 
412
+ #[test]
413
+ fn cursor_option_defaults_and_refuses_unknown_modes() {
414
+ let default: OpenParams = serde_json::from_str("{}").unwrap();
415
+ assert_eq!(default.cursor, CursorMode::Embedded);
416
+ let hidden: OpenParams = serde_json::from_str(r#"{"cursor":"hidden"}"#).unwrap();
417
+ assert_eq!(hidden.cursor, CursorMode::Hidden);
418
+ assert!(serde_json::from_str::<OpenParams>(r#"{"cursor":"metadata"}"#).is_err());
419
+ assert!(serde_json::from_str::<OpenParams>(r#"{"cursor":false}"#).is_err());
420
+ }
421
+
376
422
  #[test]
377
423
  fn a_pointer_message_round_trips_with_its_sequence() {
378
424
  let parsed: ControlMessage =
@@ -501,7 +547,8 @@ mod tests {
501
547
  params.bitrate_kbps, 0,
502
548
  "the engine sizes the rate to the surface"
503
549
  );
504
- assert_eq!(params.max_fps, 30);
550
+ assert_eq!(params.max_fps, 60);
551
+ assert!(params.local_frames, "session.frame works unless turned off");
505
552
  assert!(
506
553
  !params.agent_indicator,
507
554
  "human control has no overlay by default"
@@ -512,3 +559,18 @@ mod tests {
512
559
  assert!(agent.agent_indicator);
513
560
  }
514
561
  }
562
+
563
+ /// Monotonic engine-process clock shared by local frame and cursor notices.
564
+ pub fn timestamp_us(at: std::time::Instant) -> u64 {
565
+ static EPOCH: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
566
+ at.saturating_duration_since(*EPOCH.get_or_init(|| at))
567
+ .as_micros() as u64
568
+ }
569
+
570
+ #[derive(Debug, Clone, Copy)]
571
+ pub struct CursorPosition {
572
+ pub x: i32,
573
+ pub y: i32,
574
+ pub visible: bool,
575
+ pub timestamp_us: u64,
576
+ }