@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
@@ -0,0 +1,110 @@
1
+ //! Windows host command entry point; native desktop access remains unavailable.
2
+
3
+ use serde_json::{json, Value};
4
+
5
+ pub fn capabilities() -> Value {
6
+ let mut value = json!({
7
+ "protocol": 3,
8
+ "engine": format!("desklink-host/{}", env!("CARGO_PKG_VERSION")),
9
+ "platform": "windows",
10
+ "session": { "kind": "windows" },
11
+ "x11": { "available": false },
12
+ "capture": {
13
+ "mechanism": "unavailable",
14
+ "backends": [],
15
+ "formats": [],
16
+ "cursor": "unavailable",
17
+ "cursor_modes": [],
18
+ "audio": false,
19
+ "displays": crate::portal::displays(),
20
+ "grant": "unavailable",
21
+ "unavailable_reason": {
22
+ "reason": crate::capture::unavailable_reason(),
23
+ "remedy": "Use a future Windows capture-enabled build."
24
+ }
25
+ },
26
+ "encode": {
27
+ "codecs": [],
28
+ "hardware": false,
29
+ "unavailable_reason": {
30
+ "reason": "Windows encoding is not implemented in this build; libvpx is not linked.",
31
+ "remedy": "Use a future Windows VP9-enabled build."
32
+ }
33
+ },
34
+ "encoded": { "codecs": [] },
35
+ "input": {
36
+ "mechanism": "unavailable",
37
+ "pointer": false,
38
+ "wheel": false,
39
+ "keyboard": false,
40
+ "text": [],
41
+ "layout": crate::keymap::keyboard_layout(),
42
+ "grant": "unavailable",
43
+ "unavailable_reason": {
44
+ "reason": crate::input::unavailable_reason(),
45
+ "remedy": "Use a future Windows input-enabled build."
46
+ }
47
+ },
48
+ "clipboard": {
49
+ "read": false,
50
+ "write": false,
51
+ "mime": [],
52
+ "maxBytes": crate::clipboard::MAX_CLIPBOARD_BYTES,
53
+ "unavailable_reason": {
54
+ "reason": "Windows clipboard transfer is not implemented in this build.",
55
+ "remedy": "Use a future Windows clipboard-enabled build."
56
+ }
57
+ }
58
+ });
59
+ if cfg!(desklink_vpx) {
60
+ value["encode"] = json!({"codecs": ["vp9"], "hardware": false,
61
+ "h264": {"encoder": null, "hardware": false}});
62
+ value["encoded"] = json!({"codecs": ["h264"]});
63
+ }
64
+ value
65
+ }
66
+
67
+ pub fn run() -> i32 {
68
+ let command = std::env::args()
69
+ .nth(1)
70
+ .unwrap_or_else(|| String::from("help"));
71
+ match command.as_str() {
72
+ "version" | "--version" | "-V" => println!("{}", env!("CARGO_PKG_VERSION")),
73
+ "capabilities" => {
74
+ #[cfg(desklink_vpx)]
75
+ let value = crate::session::capabilities();
76
+ #[cfg(not(desklink_vpx))]
77
+ let value = capabilities();
78
+ println!("{}", serde_json::to_string_pretty(&value).unwrap());
79
+ }
80
+ "serve" => {
81
+ #[cfg(desklink_vpx)]
82
+ {
83
+ let runtime = match tokio::runtime::Builder::new_multi_thread()
84
+ .enable_all()
85
+ .build()
86
+ {
87
+ Ok(runtime) => runtime,
88
+ Err(error) => {
89
+ eprintln!("error: cannot start the runtime: {error}");
90
+ return 1;
91
+ }
92
+ };
93
+ return crate::report(runtime.block_on(crate::serve()));
94
+ }
95
+ #[cfg(not(desklink_vpx))]
96
+ {
97
+ eprintln!("serve requires a VP9-enabled build; set DESKLINK_VPX_STATIC_DIR when building on Windows");
98
+ return 1;
99
+ }
100
+ }
101
+ "help" | "--help" | "-h" => {
102
+ println!("desklink-host — Windows host preview\n\nCommands: version, capabilities, serve\nWindows session serving requires a static libvpx build.");
103
+ }
104
+ _ => {
105
+ eprintln!("unknown command: {command}; run desklink-host --help");
106
+ return 2;
107
+ }
108
+ }
109
+ 0
110
+ }
package/engine/src/x11.rs CHANGED
@@ -13,9 +13,13 @@
13
13
 
14
14
  use crate::convert::{fit, to_i420, I420};
15
15
  use anyhow::{Context, Result};
16
- use x11rb::connection::Connection;
16
+ use std::os::unix::io::{AsFd, AsRawFd};
17
+ use x11rb::connection::{Connection, RequestConnection};
18
+ use x11rb::protocol::damage::ConnectionExt as _;
19
+ use x11rb::protocol::shm::ConnectionExt as _;
17
20
  use x11rb::protocol::xproto::{ConnectionExt as _, ImageFormat, Screen};
18
21
  use x11rb::protocol::xtest::ConnectionExt as _;
22
+ use x11rb::protocol::Event;
19
23
  use x11rb::rust_connection::RustConnection;
20
24
 
21
25
  /// X key codes are evdev codes offset by 8 on every evdev-backed server,
@@ -41,6 +45,35 @@ pub struct X11Desktop {
41
45
  /// Bytes per pixel the server reports; 4 for the 24/32-bit TrueColor that
42
46
  /// every normal server uses, and the only depth this backend reads.
43
47
  depth: u8,
48
+ /// A reused server-allocated MIT-SHM segment. Absent on a server without
49
+ /// MIT-SHM (remote X), where grabs fall back to socket GetImage.
50
+ shm: Option<ShmSegment>,
51
+ /// An XDamage object watching the root. Absent where the server lacks the
52
+ /// extension, where the capture loop falls back to pixel hashing.
53
+ damage: Option<u32>,
54
+ }
55
+
56
+ /// A server-allocated MIT-SHM segment, mmapped into this process and reused
57
+ /// for every grab. The server frees its side when this connection closes, so
58
+ /// dropping the mapping and the fd here is the whole cleanup.
59
+ struct ShmSegment {
60
+ seg: u32,
61
+ // Held open for the mapping's lifetime and closed on drop; never read.
62
+ #[allow(dead_code)]
63
+ fd: std::os::unix::io::OwnedFd,
64
+ ptr: *mut u8,
65
+ len: usize,
66
+ }
67
+
68
+ // Only touched through `&mut X11Desktop`, itself always behind a Mutex.
69
+ unsafe impl Send for ShmSegment {}
70
+
71
+ impl Drop for ShmSegment {
72
+ fn drop(&mut self) {
73
+ unsafe {
74
+ libc::munmap(self.ptr as *mut libc::c_void, self.len);
75
+ }
76
+ }
44
77
  }
45
78
 
46
79
  impl X11Desktop {
@@ -69,10 +102,117 @@ impl X11Desktop {
69
102
  width: screen.width_in_pixels as usize,
70
103
  height: screen.height_in_pixels as usize,
71
104
  depth: screen.root_depth,
105
+ shm: Self::setup_shm(
106
+ &connection,
107
+ screen.width_in_pixels as usize,
108
+ screen.height_in_pixels as usize,
109
+ ),
110
+ damage: Self::setup_damage(&connection, screen.root),
72
111
  connection,
73
112
  })
74
113
  }
75
114
 
115
+ /// Allocate one MIT-SHM segment for the full root and map it here. Any
116
+ /// failure means "no SHM on this server", never an error: the caller
117
+ /// falls back to socket GetImage and reports it via [`Self::capture_path`].
118
+ fn setup_shm(connection: &RustConnection, width: usize, height: usize) -> Option<ShmSegment> {
119
+ let len = width.checked_mul(height)?.checked_mul(4)?;
120
+ let size: u32 = len.try_into().ok()?;
121
+ if size == 0 {
122
+ return None;
123
+ }
124
+ connection
125
+ .extension_information(x11rb::protocol::shm::X11_EXTENSION_NAME)
126
+ .ok()??;
127
+ let reply = connection.shm_query_version().ok()?.reply().ok()?;
128
+ let _ = (reply.major_version, reply.minor_version);
129
+ let seg = connection.generate_id().ok()?;
130
+ let reply = connection
131
+ .shm_create_segment(seg, size, false)
132
+ .ok()?
133
+ .reply()
134
+ .ok()?;
135
+ let ptr = unsafe {
136
+ libc::mmap(
137
+ std::ptr::null_mut(),
138
+ len,
139
+ libc::PROT_READ | libc::PROT_WRITE,
140
+ libc::MAP_SHARED,
141
+ reply.shm_fd.as_fd().as_raw_fd(),
142
+ 0,
143
+ )
144
+ };
145
+ if ptr == libc::MAP_FAILED {
146
+ return None;
147
+ }
148
+ Some(ShmSegment {
149
+ seg,
150
+ fd: reply.shm_fd,
151
+ ptr: ptr as *mut u8,
152
+ len,
153
+ })
154
+ }
155
+
156
+ /// Watch the root for changes at NON_EMPTY level: one event per change
157
+ /// batch, re-armed by [`Self::take_damage`]. Absent where unsupported.
158
+ fn setup_damage(connection: &RustConnection, root: u32) -> Option<u32> {
159
+ use x11rb::protocol::damage::ReportLevel;
160
+ let ext = connection
161
+ .extension_information(x11rb::protocol::damage::X11_EXTENSION_NAME)
162
+ .ok()?;
163
+ ext?;
164
+ connection.damage_query_version(1, 1).ok()?.reply().ok()?;
165
+ let damage = connection.generate_id().ok()?;
166
+ connection
167
+ .damage_create(damage, root, ReportLevel::NON_EMPTY)
168
+ .ok()?
169
+ .check()
170
+ .ok()?;
171
+ connection.flush().ok()?;
172
+ Some(damage)
173
+ }
174
+
175
+ /// Which grab path is in use: `"shm"`, or `"get_image"` on a server
176
+ /// without MIT-SHM. Labs cite this so a number is never ambiguous.
177
+ pub fn capture_path(&self) -> &'static str {
178
+ if self.shm.is_some() {
179
+ "shm"
180
+ } else {
181
+ "get_image"
182
+ }
183
+ }
184
+
185
+ /// Whether XDamage watches the root. When false the capture loop keeps
186
+ /// the old pixel-hash change detection.
187
+ pub fn damage_armed(&self) -> bool {
188
+ self.damage.is_some()
189
+ }
190
+
191
+ /// Drain queued events and report whether the root changed since the last
192
+ /// call. Without Damage this always returns true and the caller hashes.
193
+ pub fn take_damage(&self) -> bool {
194
+ let damage = match self.damage {
195
+ Some(damage) => damage,
196
+ None => return true,
197
+ };
198
+ let mut changed = false;
199
+ loop {
200
+ match self.connection.poll_for_event() {
201
+ Ok(Some(Event::DamageNotify(_))) => changed = true,
202
+ Ok(Some(_)) => {}
203
+ Ok(None) => break,
204
+ // Let the next grab surface connection trouble as an error.
205
+ Err(_) => return true,
206
+ }
207
+ }
208
+ if changed {
209
+ // Clear the pending region so the next change notifies again.
210
+ let _ = self.connection.damage_subtract(damage, 0u32, 0u32);
211
+ let _ = self.connection.flush();
212
+ }
213
+ changed
214
+ }
215
+
76
216
  pub fn screen_size(&self) -> (i32, i32) {
77
217
  (self.width as i32, self.height as i32)
78
218
  }
@@ -87,11 +227,25 @@ impl X11Desktop {
87
227
  .map(|(frame, _)| frame)
88
228
  }
89
229
 
230
+ /// The frame and the root image it came from: `width * 4`-byte BGRX rows at
231
+ /// the screen's own size, which a caller that keeps pre-encode pixels
232
+ /// samples into the frame's geometry with `convert::to_bgrx`.
90
233
  pub fn capture_with_pixels(
91
234
  &mut self,
92
235
  max_width: usize,
93
236
  max_height: usize,
94
237
  ) -> Result<(I420, Vec<u8>)> {
238
+ // The server's depth is the only field that can tell us how the pixel is
239
+ // packed; the engine assumes the byte order every TrueColor server uses.
240
+ let _ = self.depth;
241
+ let stride = self.width * 4;
242
+ // Prefer MIT-SHM. A segment that worked at connect can still break
243
+ // later (revoked by the server); that degrades to the socket path,
244
+ // which keeps capturing, instead of stalling on failing grabs.
245
+ if let Ok(output) = self.capture_shm(stride, max_width, max_height) {
246
+ return Ok(output);
247
+ }
248
+ self.shm = None;
95
249
  let image = self
96
250
  .connection
97
251
  .get_image(
@@ -106,7 +260,6 @@ impl X11Desktop {
106
260
  .context("X11 GetImage failed")?
107
261
  .reply()
108
262
  .context("X11 GetImage returned no reply")?;
109
- let stride = self.width * 4;
110
263
  if image.data.len() < stride * self.height {
111
264
  anyhow::bail!(
112
265
  "X11 returned {} bytes for a {}x{} screen",
@@ -116,9 +269,6 @@ impl X11Desktop {
116
269
  );
117
270
  }
118
271
  let (width, height) = fit(self.width, self.height, max_width, max_height);
119
- // The server's depth is the only field that can tell us how the pixel is
120
- // packed; the engine assumes the byte order every TrueColor server uses.
121
- let _ = self.depth;
122
272
  let frame = to_i420(
123
273
  &image.data,
124
274
  self.width,
@@ -129,8 +279,59 @@ impl X11Desktop {
129
279
  height,
130
280
  )
131
281
  .context("the captured X11 pixels are not a format this engine can read")?;
132
- let raw = crate::convert::to_bgrx(
133
- &image.data,
282
+ Ok((frame, image.data))
283
+ }
284
+
285
+ /// One MIT-SHM grab into the reused segment. The caller tears the
286
+ /// segment down on any error and falls back to the socket path.
287
+ fn capture_shm(
288
+ &self,
289
+ stride: usize,
290
+ max_width: usize,
291
+ max_height: usize,
292
+ ) -> Result<(I420, Vec<u8>)> {
293
+ let (seg, ptr, len) = match &self.shm {
294
+ Some(shm) => (shm.seg, shm.ptr, shm.len),
295
+ None => anyhow::bail!("no MIT-SHM segment"),
296
+ };
297
+ // MIT-SHM: the server writes the root straight into our mapping;
298
+ // the reply only signals completion. The segment is reused, so
299
+ // copy out through the conversion below before the next grab.
300
+ self.connection
301
+ .shm_get_image(
302
+ self.root,
303
+ 0,
304
+ 0,
305
+ self.width as u16,
306
+ self.height as u16,
307
+ u32::MAX,
308
+ u8::from(ImageFormat::Z_PIXMAP),
309
+ seg,
310
+ 0,
311
+ )
312
+ .context("X11 ShmGetImage failed")?
313
+ .reply()
314
+ .context("X11 ShmGetImage returned no reply")?;
315
+ let pixels = unsafe { std::slice::from_raw_parts(ptr as *const u8, len) };
316
+ // Snapshot the length: the caller drops the mapping on error, so no
317
+ // slice may be alive past this return.
318
+ let byte_len = pixels.len();
319
+ if byte_len < stride * self.height {
320
+ anyhow::bail!(
321
+ "X11 shared memory holds {} bytes for a {}x{} screen",
322
+ byte_len,
323
+ self.width,
324
+ self.height
325
+ );
326
+ }
327
+ // Copy out of the reused segment before the next grab overwrites
328
+ // it; the caller samples these full-screen rows into the frame's
329
+ // geometry. This copy is inherent to SHM; the socket arm below
330
+ // moves its reply instead.
331
+ let owned = pixels.to_vec();
332
+ let (width, height) = fit(self.width, self.height, max_width, max_height);
333
+ let frame = to_i420(
334
+ &owned,
134
335
  self.width,
135
336
  self.height,
136
337
  stride,
@@ -138,8 +339,8 @@ impl X11Desktop {
138
339
  width,
139
340
  height,
140
341
  )
141
- .context("the captured X11 pixels could not be copied")?;
142
- Ok((frame, raw))
342
+ .context("the captured X11 pixels are not a format this engine can read")?;
343
+ Ok((frame, owned))
143
344
  }
144
345
 
145
346
  fn flush(&self) -> Result<()> {
@@ -148,6 +349,21 @@ impl X11Desktop {
148
349
  .context("the X11 connection dropped")
149
350
  }
150
351
 
352
+ /// Query the root pointer independently of pixel damage. GetImage and
353
+ /// MIT-SHM do not contain the server cursor and we never composite it.
354
+ pub fn cursor_position(&self) -> Result<(i32, i32, bool)> {
355
+ let pointer = self.connection.query_pointer(self.root)?.reply()?;
356
+ Ok((
357
+ i32::from(pointer.root_x),
358
+ i32::from(pointer.root_y),
359
+ pointer.same_screen
360
+ && pointer.root_x >= 0
361
+ && pointer.root_y >= 0
362
+ && (pointer.root_x as usize) < self.width
363
+ && (pointer.root_y as usize) < self.height,
364
+ ))
365
+ }
366
+
151
367
  pub fn move_pointer(&self, x: i64, y: i64) -> Result<()> {
152
368
  let (x, y) = self.clamp(x, y);
153
369
  self.connection
@@ -22,6 +22,18 @@ impl X11Desktop {
22
22
  bail!("the X11 backend is only available on Linux")
23
23
  }
24
24
 
25
+ pub fn capture_path(&self) -> &'static str {
26
+ "get_image"
27
+ }
28
+
29
+ pub fn damage_armed(&self) -> bool {
30
+ false
31
+ }
32
+
33
+ pub fn take_damage(&self) -> bool {
34
+ true
35
+ }
36
+
25
37
  pub fn move_pointer(&mut self, _: i64, _: i64) -> Result<()> {
26
38
  bail!("the X11 backend is only available on Linux")
27
39
  }
@@ -0,0 +1,19 @@
1
+ Permission is hereby granted, free of charge, to any person obtaining a
2
+ copy of this software and associated documentation files (the
3
+ "Software"), to deal in the Software without restriction, including
4
+ without limitation the rights to use, copy, modify, merge, publish,
5
+ distribute, sub license, and/or sell copies of the Software, and to
6
+ permit persons to whom the Software is furnished to do so, subject to
7
+ the following conditions:
8
+
9
+ The above copyright notice and this permission notice (including the
10
+ next paragraph) shall be included in all copies or substantial portions
11
+ of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
14
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
16
+ IN NO EVENT SHALL PRECISION INSIGHT AND/OR ITS SUPPLIERS BE LIABLE FOR
17
+ ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
18
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.