@desklink/host 0.3.0 → 0.4.0

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 (47) hide show
  1. package/NOTICE +3 -0
  2. package/README.md +92 -23
  3. package/bin/desklink-host.mjs +14 -5
  4. package/dist/bridge.d.ts +3 -4
  5. package/dist/bridge.d.ts.map +1 -1
  6. package/dist/bridge.js +18 -7
  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 +34 -5
  11. package/dist/engineProcess.js.map +1 -1
  12. package/dist/protocol.d.ts +35 -2
  13. package/dist/protocol.d.ts.map +1 -1
  14. package/dist/protocol.js.map +1 -1
  15. package/dist/resolveEngine.d.ts +6 -0
  16. package/dist/resolveEngine.d.ts.map +1 -1
  17. package/dist/resolveEngine.js +45 -6
  18. package/dist/resolveEngine.js.map +1 -1
  19. package/docs/BOUNDARY.md +8 -8
  20. package/docs/PROTOCOL.md +185 -45
  21. package/engine/Cargo.lock +2 -2
  22. package/engine/Cargo.toml +7 -12
  23. package/engine/build.rs +62 -5
  24. package/engine/native/agent_indicator.h +57 -0
  25. package/engine/native/agent_overlay_mac.m +51 -4
  26. package/engine/native/agent_overlay_wayland.c +189 -35
  27. package/engine/native/agent_overlay_x11.c +186 -55
  28. package/engine/src/capture.rs +96 -5
  29. package/engine/src/capture_win.rs +23 -0
  30. package/engine/src/clipboard.rs +174 -0
  31. package/engine/src/clipboard_win.rs +8 -0
  32. package/engine/src/cursor.rs +118 -0
  33. package/engine/src/indicator.rs +271 -5
  34. package/engine/src/input_win.rs +91 -0
  35. package/engine/src/keymap_win.rs +39 -0
  36. package/engine/src/mac.rs +28 -6
  37. package/engine/src/main.rs +167 -25
  38. package/engine/src/peer.rs +10 -4
  39. package/engine/src/portal.rs +42 -4
  40. package/engine/src/portal_win.rs +37 -0
  41. package/engine/src/protocol.rs +56 -10
  42. package/engine/src/session.rs +749 -203
  43. package/engine/src/win.rs +110 -0
  44. package/engine/src/x11.rs +44 -29
  45. package/engine/src/x11_mac.rs +4 -0
  46. package/examples/reference-client.html +3 -5
  47. package/package.json +4 -4
@@ -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
@@ -112,6 +112,14 @@ impl X11Desktop {
112
112
  })
113
113
  }
114
114
 
115
+ pub fn input_available(&self) -> bool {
116
+ self.connection
117
+ .xtest_get_version(2, 2)
118
+ .ok()
119
+ .and_then(|cookie| cookie.reply().ok())
120
+ .is_some()
121
+ }
122
+
115
123
  /// Allocate one MIT-SHM segment for the full root and map it here. Any
116
124
  /// failure means "no SHM on this server", never an error: the caller
117
125
  /// falls back to socket GetImage and reports it via [`Self::capture_path`].
@@ -235,6 +243,13 @@ impl X11Desktop {
235
243
  max_width: usize,
236
244
  max_height: usize,
237
245
  ) -> Result<(I420, Vec<u8>)> {
246
+ let raw = self.capture_raw()?;
247
+ let frame = self.convert(&raw, max_width, max_height)?;
248
+ Ok((frame, raw))
249
+ }
250
+
251
+ /// The root image as `width * 4`-byte BGRX rows at the screen's own size.
252
+ pub fn capture_raw(&mut self) -> Result<Vec<u8>> {
238
253
  // The server's depth is the only field that can tell us how the pixel is
239
254
  // packed; the engine assumes the byte order every TrueColor server uses.
240
255
  let _ = self.depth;
@@ -242,7 +257,7 @@ impl X11Desktop {
242
257
  // Prefer MIT-SHM. A segment that worked at connect can still break
243
258
  // later (revoked by the server); that degrades to the socket path,
244
259
  // which keeps capturing, instead of stalling on failing grabs.
245
- if let Ok(output) = self.capture_shm(stride, max_width, max_height) {
260
+ if let Ok(output) = self.capture_shm(stride) {
246
261
  return Ok(output);
247
262
  }
248
263
  self.shm = None;
@@ -268,35 +283,34 @@ impl X11Desktop {
268
283
  self.height
269
284
  );
270
285
  }
286
+ Ok(image.data)
287
+ }
288
+
289
+ /// Downscale a [`Self::capture_raw`] image into the caller's box as I420.
290
+ pub fn convert(&self, raw: &[u8], max_width: usize, max_height: usize) -> Result<I420> {
271
291
  let (width, height) = fit(self.width, self.height, max_width, max_height);
272
- let frame = to_i420(
273
- &image.data,
292
+ to_i420(
293
+ raw,
274
294
  self.width,
275
295
  self.height,
276
- stride,
296
+ self.width * 4,
277
297
  crate::convert::PixelFormat::Bgrx,
278
298
  width,
279
299
  height,
280
300
  )
281
- .context("the captured X11 pixels are not a format this engine can read")?;
282
- Ok((frame, image.data))
301
+ .context("the captured X11 pixels are not a format this engine can read")
283
302
  }
284
303
 
285
304
  /// One MIT-SHM grab into the reused segment. The caller tears the
286
305
  /// 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>)> {
306
+ fn capture_shm(&self, stride: usize) -> Result<Vec<u8>> {
293
307
  let (seg, ptr, len) = match &self.shm {
294
308
  Some(shm) => (shm.seg, shm.ptr, shm.len),
295
309
  None => anyhow::bail!("no MIT-SHM segment"),
296
310
  };
297
311
  // MIT-SHM: the server writes the root straight into our mapping;
298
312
  // the reply only signals completion. The segment is reused, so
299
- // copy out through the conversion below before the next grab.
313
+ // copy out before the next grab.
300
314
  self.connection
301
315
  .shm_get_image(
302
316
  self.root,
@@ -325,22 +339,8 @@ impl X11Desktop {
325
339
  );
326
340
  }
327
341
  // 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,
335
- self.width,
336
- self.height,
337
- stride,
338
- crate::convert::PixelFormat::Bgrx,
339
- width,
340
- height,
341
- )
342
- .context("the captured X11 pixels are not a format this engine can read")?;
343
- Ok((frame, owned))
342
+ // it. This copy is inherent to SHM; the socket arm moves its reply.
343
+ Ok(pixels.to_vec())
344
344
  }
345
345
 
346
346
  fn flush(&self) -> Result<()> {
@@ -349,6 +349,21 @@ impl X11Desktop {
349
349
  .context("the X11 connection dropped")
350
350
  }
351
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
+
352
367
  pub fn move_pointer(&self, x: i64, y: i64) -> Result<()> {
353
368
  let (x, y) = self.clamp(x, y);
354
369
  self.connection
@@ -18,6 +18,10 @@ impl X11Desktop {
18
18
  (0, 0)
19
19
  }
20
20
 
21
+ pub fn input_available(&self) -> bool {
22
+ false
23
+ }
24
+
21
25
  pub fn capture_with_pixels(&mut self, _: usize, _: usize) -> Result<(I420, Vec<u8>)> {
22
26
  bail!("the X11 backend is only available on Linux")
23
27
  }
@@ -269,14 +269,12 @@ async function start() {
269
269
  const capabilities = await request('capabilities');
270
270
  const clipboardAvailable = capabilities.clipboard?.read === true && capabilities.clipboard?.write === true;
271
271
  const controlAvailable = capabilities.input?.pointer === true && capabilities.input?.keyboard === true;
272
- const permissions = ['view'];
273
- if (controlAvailable) permissions.push('control');
274
- if (clipboardAvailable) permissions.push('clipboard');
275
- const session = await request('session.open', { permissions });
272
+ // The bridge token is the pairing: a paired session has view, input and clipboard.
273
+ const session = await request('session.open', {});
276
274
  currentSessionId = session.sessionId;
277
275
  keyboard.disabled = !controlAvailable;
278
276
  keyboardButton.disabled = !controlAvailable;
279
- keyboardButton.title = controlAvailable ? '' : capabilities.input?.unavailable_reason?.remedy ?? 'This session is view-only';
277
+ keyboardButton.title = controlAvailable ? '' : capabilities.input?.unavailable_reason?.remedy ?? 'Input is unavailable on this machine';
280
278
  video.style.pointerEvents = controlAvailable ? 'auto' : 'none';
281
279
  copy.hidden = !clipboardAvailable;
282
280
  paste.hidden = !clipboardAvailable;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desklink/host",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Per-user, on-demand desktop capture, encode and input engine with a versioned local control protocol",
6
6
  "type": "module",
@@ -30,10 +30,10 @@
30
30
  "node": ">=22"
31
31
  },
32
32
  "dependencies": {
33
- "ws": "^8.18.0"
33
+ "ws": "8.21.3"
34
34
  },
35
35
  "optionalDependencies": {
36
- "@desklink/host-linux-x64-gnu": "0.3.0",
37
- "@desklink/host-darwin-arm64": "0.3.0"
36
+ "@desklink/host-linux-x64-gnu": "0.4.0",
37
+ "@desklink/host-darwin-arm64": "0.4.0"
38
38
  }
39
39
  }