@desklink/host 0.1.0 → 0.1.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 (45) hide show
  1. package/NOTICE +3 -0
  2. package/README.md +50 -16
  3. package/bin/desklink-host.mjs +13 -2
  4. package/dist/bridge.d.ts.map +1 -1
  5. package/dist/bridge.js +5 -1
  6. package/dist/bridge.js.map +1 -1
  7. package/dist/engineProcess.d.ts.map +1 -1
  8. package/dist/engineProcess.js +17 -5
  9. package/dist/engineProcess.js.map +1 -1
  10. package/dist/protocol.d.ts +42 -5
  11. package/dist/protocol.d.ts.map +1 -1
  12. package/dist/protocol.js +1 -1
  13. package/dist/protocol.js.map +1 -1
  14. package/dist/resolveEngine.d.ts +1 -0
  15. package/dist/resolveEngine.d.ts.map +1 -1
  16. package/dist/resolveEngine.js +16 -1
  17. package/dist/resolveEngine.js.map +1 -1
  18. package/docs/BOUNDARY.md +4 -3
  19. package/docs/PROTOCOL.md +95 -27
  20. package/engine/Cargo.lock +195 -1
  21. package/engine/Cargo.toml +25 -5
  22. package/engine/build.rs +98 -23
  23. package/engine/native/agent_overlay_mac.m +133 -0
  24. package/engine/native/agent_overlay_wayland.c +192 -0
  25. package/engine/native/agent_overlay_x11.c +103 -0
  26. package/engine/native/mac_stream.mm +188 -0
  27. package/engine/src/capture.rs +169 -19
  28. package/engine/src/capture_mac.rs +145 -0
  29. package/engine/src/clipboard_mac.rs +182 -0
  30. package/engine/src/convert.rs +57 -1
  31. package/engine/src/indicator.rs +162 -0
  32. package/engine/src/input.rs +12 -0
  33. package/engine/src/input_mac.rs +312 -0
  34. package/engine/src/keymap.rs +25 -0
  35. package/engine/src/keymap_mac.rs +209 -0
  36. package/engine/src/mac.rs +722 -0
  37. package/engine/src/main.rs +116 -2
  38. package/engine/src/portal.rs +4 -0
  39. package/engine/src/portal_mac.rs +68 -0
  40. package/engine/src/protocol.rs +48 -1
  41. package/engine/src/session.rs +556 -62
  42. package/engine/src/x11.rs +23 -3
  43. package/engine/src/x11_mac.rs +40 -0
  44. package/examples/reference-client.html +33 -4
  45. package/package.json +2 -2
package/engine/src/x11.rs CHANGED
@@ -83,6 +83,15 @@ impl X11Desktop {
83
83
  /// compositor this is every window drawn onto the framebuffer, which is what
84
84
  /// the viewer expects to see.
85
85
  pub fn capture(&mut self, max_width: usize, max_height: usize) -> Result<I420> {
86
+ self.capture_with_pixels(max_width, max_height)
87
+ .map(|(frame, _)| frame)
88
+ }
89
+
90
+ pub fn capture_with_pixels(
91
+ &mut self,
92
+ max_width: usize,
93
+ max_height: usize,
94
+ ) -> Result<(I420, Vec<u8>)> {
86
95
  let image = self
87
96
  .connection
88
97
  .get_image(
@@ -110,16 +119,27 @@ impl X11Desktop {
110
119
  // The server's depth is the only field that can tell us how the pixel is
111
120
  // packed; the engine assumes the byte order every TrueColor server uses.
112
121
  let _ = self.depth;
113
- to_i420(
122
+ let frame = to_i420(
123
+ &image.data,
124
+ self.width,
125
+ self.height,
126
+ stride,
127
+ crate::convert::PixelFormat::Bgrx,
128
+ width,
129
+ height,
130
+ )
131
+ .context("the captured X11 pixels are not a format this engine can read")?;
132
+ let raw = crate::convert::to_bgrx(
114
133
  &image.data,
115
134
  self.width,
116
135
  self.height,
117
136
  stride,
118
- crate::capture::PixelFormat::Bgrx,
137
+ crate::convert::PixelFormat::Bgrx,
119
138
  width,
120
139
  height,
121
140
  )
122
- .context("the captured X11 pixels are not a format this engine can read")
141
+ .context("the captured X11 pixels could not be copied")?;
142
+ Ok((frame, raw))
123
143
  }
124
144
 
125
145
  fn flush(&self) -> Result<()> {
@@ -0,0 +1,40 @@
1
+ use crate::convert::I420;
2
+ use anyhow::{bail, Result};
3
+
4
+ pub mod button {
5
+ pub const LEFT: u8 = 1;
6
+ pub const MIDDLE: u8 = 2;
7
+ pub const RIGHT: u8 = 3;
8
+ }
9
+
10
+ pub struct X11Desktop;
11
+
12
+ impl X11Desktop {
13
+ pub fn connect(_: Option<&str>) -> Result<Self> {
14
+ bail!("the X11 backend is only available on Linux")
15
+ }
16
+
17
+ pub fn screen_size(&self) -> (i32, i32) {
18
+ (0, 0)
19
+ }
20
+
21
+ pub fn capture_with_pixels(&mut self, _: usize, _: usize) -> Result<(I420, Vec<u8>)> {
22
+ bail!("the X11 backend is only available on Linux")
23
+ }
24
+
25
+ pub fn move_pointer(&mut self, _: i64, _: i64) -> Result<()> {
26
+ bail!("the X11 backend is only available on Linux")
27
+ }
28
+
29
+ pub fn button(&mut self, _: u8, _: bool) -> Result<()> {
30
+ bail!("the X11 backend is only available on Linux")
31
+ }
32
+
33
+ pub fn scroll(&mut self, _: i64, _: i64) -> Result<()> {
34
+ bail!("the X11 backend is only available on Linux")
35
+ }
36
+
37
+ pub fn key(&mut self, _: i16, _: bool) -> Result<()> {
38
+ bail!("the X11 backend is only available on Linux")
39
+ }
40
+ }
@@ -67,12 +67,15 @@ const overlay = document.getElementById('overlay');
67
67
  const status = document.getElementById('status');
68
68
  const reconnect = document.getElementById('reconnect');
69
69
  const keyboard = document.getElementById('remote-keyboard');
70
+ const keyboardButton = document.getElementById('keyboard');
70
71
  const copy = document.getElementById('copy');
71
72
  const paste = document.getElementById('paste');
72
73
 
73
74
  let socket = null;
74
75
  let peer = null;
75
76
  let channel = null;
77
+ let currentSessionId = null;
78
+ let pendingRemoteCandidates = [];
76
79
  let surface = { width: 0, height: 0 };
77
80
  let nextSeq = 0;
78
81
  let pending = new Map();
@@ -265,10 +268,16 @@ async function start() {
265
268
  try {
266
269
  const capabilities = await request('capabilities');
267
270
  const clipboardAvailable = capabilities.clipboard?.read === true && capabilities.clipboard?.write === true;
268
- const session = await request('session.open', {
269
- permissions: clipboardAvailable ? ['view', 'control', 'clipboard'] : ['view', 'control'],
270
- max_fps: 30,
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, max_fps: 30 });
276
+ currentSessionId = session.sessionId;
277
+ keyboard.disabled = !controlAvailable;
278
+ keyboardButton.disabled = !controlAvailable;
279
+ keyboardButton.title = controlAvailable ? '' : capabilities.input?.unavailable_reason?.remedy ?? 'This session is view-only';
280
+ video.style.pointerEvents = controlAvailable ? 'auto' : 'none';
272
281
  copy.hidden = !clipboardAvailable;
273
282
  paste.hidden = !clipboardAvailable;
274
283
  surface = session.geometry.encoded;
@@ -332,6 +341,9 @@ async function start() {
332
341
  // The engine is the offerer, so this side waits for the offer and answers.
333
342
  const offer = await waitForOffer();
334
343
  await pc.setRemoteDescription({ type: 'offer', sdp: offer });
344
+ for (const pending of pendingRemoteCandidates.splice(0)) {
345
+ if (pending.sessionId === session.sessionId) await pc.addIceCandidate(pending.candidate);
346
+ }
335
347
  const answer = await pc.createAnswer();
336
348
  await pc.setLocalDescription(answer);
337
349
  await request('session.description', {
@@ -371,6 +383,8 @@ function teardown(message) {
371
383
  peer?.close();
372
384
  peer = null;
373
385
  channel = null;
386
+ currentSessionId = null;
387
+ pendingRemoteCandidates = [];
374
388
  video.srcObject = null;
375
389
  copy.hidden = true;
376
390
  paste.hidden = true;
@@ -392,6 +406,21 @@ function connectSocket() {
392
406
  else waiting.resolve(message.result);
393
407
  return;
394
408
  }
409
+ if (message.event === 'session.candidate') {
410
+ const params = message.params;
411
+ if (currentSessionId !== null && params.sessionId !== currentSessionId) return;
412
+ const candidate = {
413
+ candidate: params.candidate,
414
+ sdpMid: params.sdpMid,
415
+ sdpMLineIndex: params.sdpMLineIndex,
416
+ };
417
+ if (peer?.remoteDescription) {
418
+ void peer.addIceCandidate(candidate).catch(() => undefined);
419
+ } else {
420
+ pendingRemoteCandidates.push({ sessionId: params.sessionId, candidate });
421
+ }
422
+ return;
423
+ }
395
424
  if (message.event === 'session.description') {
396
425
  const sdp = message.params.description.sdp;
397
426
  if (offerWaiter !== null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desklink/host",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",
@@ -33,6 +33,6 @@
33
33
  "ws": "^8.18.0"
34
34
  },
35
35
  "optionalDependencies": {
36
- "@desklink/host-linux-x64-gnu": "0.1.0"
36
+ "@desklink/host-linux-x64-gnu": "0.1.1"
37
37
  }
38
38
  }