@desklink/host 0.1.0 → 0.2.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 (55) hide show
  1. package/NOTICE +3 -0
  2. package/README.md +72 -16
  3. package/bin/desklink-host.mjs +14 -3
  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/index.d.ts +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/keeper.d.ts +52 -0
  15. package/dist/keeper.d.ts.map +1 -0
  16. package/dist/keeper.js +101 -0
  17. package/dist/keeper.js.map +1 -0
  18. package/dist/protocol.d.ts +42 -5
  19. package/dist/protocol.d.ts.map +1 -1
  20. package/dist/protocol.js +1 -1
  21. package/dist/protocol.js.map +1 -1
  22. package/dist/resolveEngine.d.ts +3 -2
  23. package/dist/resolveEngine.d.ts.map +1 -1
  24. package/dist/resolveEngine.js +20 -5
  25. package/dist/resolveEngine.js.map +1 -1
  26. package/docs/BOUNDARY.md +11 -6
  27. package/docs/PROTOCOL.md +297 -33
  28. package/engine/Cargo.lock +196 -1
  29. package/engine/Cargo.toml +26 -5
  30. package/engine/build.rs +98 -23
  31. package/engine/native/agent_overlay_mac.m +133 -0
  32. package/engine/native/agent_overlay_wayland.c +192 -0
  33. package/engine/native/agent_overlay_x11.c +103 -0
  34. package/engine/native/mac_stream.mm +188 -0
  35. package/engine/src/capture.rs +169 -19
  36. package/engine/src/capture_mac.rs +145 -0
  37. package/engine/src/clipboard_mac.rs +182 -0
  38. package/engine/src/convert.rs +57 -1
  39. package/engine/src/indicator.rs +162 -0
  40. package/engine/src/input.rs +12 -0
  41. package/engine/src/input_mac.rs +312 -0
  42. package/engine/src/keeper.rs +622 -0
  43. package/engine/src/keymap.rs +25 -0
  44. package/engine/src/keymap_mac.rs +209 -0
  45. package/engine/src/mac.rs +722 -0
  46. package/engine/src/main.rs +158 -2
  47. package/engine/src/peer.rs +429 -22
  48. package/engine/src/portal.rs +4 -0
  49. package/engine/src/portal_mac.rs +68 -0
  50. package/engine/src/protocol.rs +186 -3
  51. package/engine/src/session.rs +1307 -100
  52. package/engine/src/x11.rs +23 -3
  53. package/engine/src/x11_mac.rs +40 -0
  54. package/examples/reference-client.html +33 -4
  55. package/package.json +2 -2
@@ -6,25 +6,77 @@
6
6
  //! process, owns its stdin/stdout, and carries the SDP/ICE the engine produces
7
7
  //! over whatever authenticated channel it already has.
8
8
 
9
+ #[cfg(target_os = "linux")]
9
10
  mod capture;
11
+ #[cfg(target_os = "macos")]
12
+ #[path = "capture_mac.rs"]
13
+ mod capture;
14
+ #[cfg(target_os = "linux")]
15
+ mod clipboard;
16
+ #[cfg(target_os = "macos")]
17
+ #[path = "clipboard_mac.rs"]
10
18
  mod clipboard;
19
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
11
20
  mod convert;
21
+ #[cfg(target_os = "linux")]
22
+ mod encoder;
23
+ #[cfg(all(target_os = "macos", desklink_vpx))]
12
24
  mod encoder;
25
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
26
+ mod indicator;
27
+ #[cfg(target_os = "linux")]
13
28
  mod input;
29
+ #[cfg(target_os = "macos")]
30
+ #[path = "input_mac.rs"]
31
+ mod input;
32
+ #[cfg(target_os = "linux")]
33
+ mod keeper;
34
+ #[cfg(target_os = "linux")]
35
+ mod keymap;
36
+ #[cfg(target_os = "macos")]
37
+ #[path = "keymap_mac.rs"]
14
38
  mod keymap;
39
+ #[cfg(target_os = "macos")]
40
+ mod mac;
41
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
15
42
  mod peer;
43
+ #[cfg(target_os = "linux")]
44
+ mod portal;
45
+ #[cfg(target_os = "macos")]
46
+ #[path = "portal_mac.rs"]
16
47
  mod portal;
48
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
17
49
  mod protocol;
50
+ #[cfg(target_os = "linux")]
18
51
  mod session;
52
+ #[cfg(all(target_os = "macos", desklink_vpx))]
53
+ mod session;
54
+ #[cfg(target_os = "linux")]
55
+ mod x11;
56
+ #[cfg(target_os = "macos")]
57
+ #[path = "x11_mac.rs"]
19
58
  mod x11;
20
59
 
60
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
21
61
  use anyhow::Result;
62
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
22
63
  use protocol::{ErrorBody, Event, Request, Response};
64
+ #[cfg(target_os = "linux")]
23
65
  use std::sync::mpsc as std_mpsc;
66
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
24
67
  use std::time::Duration;
68
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
25
69
  use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
70
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
26
71
  use tokio::sync::mpsc as tokio_mpsc;
27
72
 
73
+ #[cfg(target_os = "linux")]
74
+ unsafe extern "C" {
75
+ fn desklink_agent_overlay_main(display: *const std::ffi::c_char) -> i32;
76
+ fn desklink_agent_overlay_wayland(source_w: i32, source_h: i32) -> i32;
77
+ }
78
+
79
+ #[cfg(target_os = "linux")]
28
80
  fn main() {
29
81
  let args: Vec<String> = std::env::args().skip(1).collect();
30
82
  let command = args.first().map(String::as_str).unwrap_or("help");
@@ -43,6 +95,20 @@ fn main() {
43
95
  println!("{}", env!("CARGO_PKG_VERSION"));
44
96
  0
45
97
  }
98
+ "agent-overlay" => {
99
+ let display = args.get(1).map(String::as_str).unwrap_or("");
100
+ let display = std::ffi::CString::new(display).expect("valid X display");
101
+ unsafe { desklink_agent_overlay_main(display.as_ptr()) }
102
+ }
103
+ "agent-overlay-wayland" => {
104
+ let width = args.get(1).and_then(|v| v.parse().ok()).unwrap_or(0);
105
+ let height = args.get(2).and_then(|v| v.parse().ok()).unwrap_or(0);
106
+ if width <= 0 || height <= 0 {
107
+ 2
108
+ } else {
109
+ unsafe { desklink_agent_overlay_wayland(width, height) }
110
+ }
111
+ }
46
112
  "capabilities" => {
47
113
  println!(
48
114
  "{}",
@@ -51,6 +117,7 @@ fn main() {
51
117
  0
52
118
  }
53
119
  "serve" => report(runtime.block_on(serve())),
120
+ "keep" => keeper::command(&args[1..]),
54
121
  "capture-probe" => report(runtime.block_on(probe(
55
122
  args.get(1).and_then(|value| value.parse().ok()),
56
123
  args.get(2).map(String::as_str),
@@ -72,6 +139,7 @@ fn main() {
72
139
  std::process::exit(code);
73
140
  }
74
141
 
142
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
75
143
  fn report(result: Result<()>) -> i32 {
76
144
  match result {
77
145
  Ok(()) => 0,
@@ -82,12 +150,16 @@ fn report(result: Result<()>) -> i32 {
82
150
  }
83
151
  }
84
152
 
153
+ #[cfg(target_os = "linux")]
85
154
  fn print_help() {
86
155
  println!(
87
156
  "desklink-host {}
88
157
 
89
158
  USAGE:
90
159
  desklink-host serve speak the local control protocol on stdin/stdout
160
+ desklink-host keep --display :N
161
+ keep one private X screen: kiosk geometry, focus,
162
+ and a JSON-lines window report on stdout
91
163
  desklink-host capabilities print what this machine can do right now
92
164
  desklink-host capture-probe [seconds] [display]
93
165
  capture frames and report the stream; with a
@@ -105,6 +177,7 @@ anything. See docs/PROTOCOL.md for the protocol.",
105
177
 
106
178
  /// Deliberately does not perform the change. Kernel input access is a privileged
107
179
  /// decision, so the engine explains it and stops; the user runs the command.
180
+ #[cfg(target_os = "linux")]
108
181
  fn print_input_setup() {
109
182
  match input::probe() {
110
183
  Ok(()) => {
@@ -154,6 +227,7 @@ fn print_input_setup() {
154
227
 
155
228
  /// Start the portal session, capture a few frames, and print what actually
156
229
  /// arrived. This is the diagnostic to run when the picture does not appear.
230
+ #[cfg(target_os = "linux")]
157
231
  async fn probe(seconds: Option<u64>, display: Option<&str>) -> Result<()> {
158
232
  let seconds = seconds.unwrap_or(3);
159
233
  match display {
@@ -164,6 +238,7 @@ async fn probe(seconds: Option<u64>, display: Option<&str>) -> Result<()> {
164
238
 
165
239
  /// Read a named X display's root window for a few seconds. The frame shape is
166
240
  /// reported as a coarse checksum so a flat or unreadable screen is visible.
241
+ #[cfg(target_os = "linux")]
167
242
  fn probe_x11(display: &str, seconds: u64) -> Result<()> {
168
243
  let mut desktop = crate::x11::X11Desktop::connect(Some(display))?;
169
244
  let (width, height) = desktop.screen_size();
@@ -195,6 +270,7 @@ fn probe_x11(display: &str, seconds: u64) -> Result<()> {
195
270
 
196
271
  /// A cheap fingerprint of a frame, enough to tell "the screen changed" from
197
272
  /// "the screen is one flat colour".
273
+ #[cfg(target_os = "linux")]
198
274
  fn frame_shape(frame: &crate::convert::I420) -> u64 {
199
275
  frame
200
276
  .y_plane()
@@ -207,6 +283,7 @@ fn frame_shape(frame: &crate::convert::I420) -> u64 {
207
283
  })
208
284
  }
209
285
 
286
+ #[cfg(target_os = "linux")]
210
287
  async fn probe_portal(seconds: u64) -> Result<()> {
211
288
  let portal = portal::open(None).await?;
212
289
  eprintln!("portal source: {:?}", portal.source);
@@ -217,9 +294,11 @@ async fn probe_portal(seconds: u64) -> Result<()> {
217
294
  0,
218
295
  0,
219
296
  30,
220
- Box::new(move |frame, seq| {
297
+ Box::new(move |frame, seq, _raw| {
221
298
  let _ = tx.send((frame.width, frame.height, seq));
222
299
  }),
300
+ None,
301
+ Box::new(|reason| eprintln!("{reason}")),
223
302
  )?;
224
303
 
225
304
  let deadline = std::time::Instant::now() + Duration::from_secs(seconds);
@@ -253,6 +332,7 @@ async fn probe_portal(seconds: u64) -> Result<()> {
253
332
  }
254
333
 
255
334
  /// The local control protocol loop.
335
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
256
336
  async fn serve() -> Result<()> {
257
337
  let (out_tx, mut out_rx) = tokio_mpsc::unbounded_channel::<String>();
258
338
  let writer = tokio::spawn(async move {
@@ -343,6 +423,7 @@ async fn serve() -> Result<()> {
343
423
  Ok(())
344
424
  }
345
425
 
426
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
346
427
  fn render_event(notice: session::Notice) -> Option<String> {
347
428
  let session::Notice { session_id, event } = notice;
348
429
  let (name, params) = match event {
@@ -373,10 +454,26 @@ fn render_event(notice: session::Notice) -> Option<String> {
373
454
  "session.state",
374
455
  serde_json::json!({ "sessionId": session_id, "capture": capture, "transport": transport, "firstFrame": first_frame }),
375
456
  ),
457
+ session::SessionEvent::Frame { seq, damage } => (
458
+ "session.frame.changed",
459
+ serde_json::json!({ "sessionId": session_id, "seq": seq, "damage": damage }),
460
+ ),
461
+ session::SessionEvent::Input { input } => (
462
+ "session.input",
463
+ serde_json::json!({ "sessionId": session_id, "input": input }),
464
+ ),
465
+ session::SessionEvent::KeyframeRequest { generation } => (
466
+ "session.keyframeRequest",
467
+ serde_json::json!({ "sessionId": session_id, "generation": generation }),
468
+ ),
376
469
  session::SessionEvent::RestoreToken(token) => (
377
470
  "session.restoreToken",
378
471
  serde_json::json!({ "sessionId": session_id, "token": token }),
379
472
  ),
473
+ session::SessionEvent::CaptureStopped { reason } => (
474
+ "session.capture.stopped",
475
+ serde_json::json!({ "sessionId": session_id, "reason": reason }),
476
+ ),
380
477
  session::SessionEvent::Revoked { reason } => (
381
478
  "session.revoked",
382
479
  serde_json::json!({ "sessionId": session_id, "reason": reason }),
@@ -389,6 +486,7 @@ fn render_event(notice: session::Notice) -> Option<String> {
389
486
  .ok()
390
487
  }
391
488
 
489
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
392
490
  async fn dispatch(
393
491
  request: &Request,
394
492
  hello_seen: &mut bool,
@@ -439,6 +537,7 @@ async fn dispatch(
439
537
  restore_token: params.restore_token,
440
538
  ttl: params.ttl_seconds.map(Duration::from_secs),
441
539
  loopback_tcp: params.loopback_tcp,
540
+ agent_indicator: params.agent_indicator,
442
541
  ice_servers: params
443
542
  .ice_servers
444
543
  .into_iter()
@@ -496,6 +595,21 @@ async fn dispatch(
496
595
  .map_err(|error| ErrorBody::new("transport", format!("{error:#}")))?;
497
596
  Ok(serde_json::json!({ "accepted": true }))
498
597
  }
598
+ "session.restart_ice" => {
599
+ // The client asks; the engine offers. The fresh offer travels as a
600
+ // new `session.description` event and the client answers it as
601
+ // usual, so no new event shape is needed and the session,
602
+ // generation and control channel survive.
603
+ let session = require_session(current)?;
604
+ let params: protocol::SessionRef = serde_json::from_value(request.params.clone())
605
+ .map_err(|error| ErrorBody::new("malformed", error.to_string()))?;
606
+ check_session(session, &params.session_id, params.generation)?;
607
+ session
608
+ .restart_ice()
609
+ .await
610
+ .map_err(|error| ErrorBody::new("transport", format!("{error:#}")))?;
611
+ Ok(serde_json::json!({ "accepted": true }))
612
+ }
499
613
  "session.clipboard.read" => {
500
614
  let session = require_session(current)?;
501
615
  let params: protocol::ClipboardParams = serde_json::from_value(request.params.clone())
@@ -518,6 +632,41 @@ async fn dispatch(
518
632
  .map_err(|reason| ErrorBody::new("clipboard", reason))?;
519
633
  Ok(serde_json::json!({ "written": true }))
520
634
  }
635
+ "session.frame" => {
636
+ let session = require_session(current)?;
637
+ let params: protocol::FrameParams = serde_json::from_value(request.params.clone())
638
+ .map_err(|error| ErrorBody::new("malformed", error.to_string()))?;
639
+ check_session(session, &params.session_id, None)?;
640
+ if params.after_seq.is_some()
641
+ || params.still_ms.is_some()
642
+ || params.timeout_ms.is_some()
643
+ {
644
+ session
645
+ .wait_frame(
646
+ params.after_seq,
647
+ params.still_ms,
648
+ params.timeout_ms.unwrap_or(5000),
649
+ )
650
+ .await
651
+ .map_err(|error| ErrorBody::new(error.code, error.message))?;
652
+ }
653
+ session
654
+ .frame(params.since, &params.path, params.region)
655
+ .map_err(|error| ErrorBody::new(error.code, error.message))
656
+ }
657
+ "session.feed" => {
658
+ let session = require_session(current)?;
659
+ let params: protocol::FeedParams = serde_json::from_value(request.params.clone())
660
+ .map_err(|error| ErrorBody::new("malformed", error.to_string()))?;
661
+ check_session(session, &params.session_id, None)?;
662
+ let data = params
663
+ .access_unit()
664
+ .map_err(|message| ErrorBody::new("malformed", message))?;
665
+ session
666
+ .feed(params.keyframe, data)
667
+ .map_err(|error| ErrorBody::new(error.code, error.message))?;
668
+ Ok(serde_json::json!({ "accepted": true }))
669
+ }
521
670
  "session.metrics" => {
522
671
  let session = require_session(current)?;
523
672
  Ok(serde_json::to_value(session.metrics()).unwrap_or(serde_json::Value::Null))
@@ -538,6 +687,7 @@ async fn dispatch(
538
687
  }
539
688
  }
540
689
 
690
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
541
691
  fn require_session(
542
692
  current: &mut Option<session::Session>,
543
693
  ) -> std::result::Result<&mut session::Session, ErrorBody> {
@@ -548,6 +698,7 @@ fn require_session(
548
698
 
549
699
  /// A late message for a session that has already ended must not revive it, and a
550
700
  /// message for a different session must not reach this one.
701
+ #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
551
702
  fn check_session(
552
703
  session: &session::Session,
553
704
  id: &str,
@@ -567,7 +718,12 @@ fn check_session(
567
718
  Ok(())
568
719
  }
569
720
 
570
- #[cfg(test)]
721
+ #[cfg(target_os = "macos")]
722
+ fn main() {
723
+ std::process::exit(mac::run());
724
+ }
725
+
726
+ #[cfg(all(test, target_os = "linux"))]
571
727
  mod tests {
572
728
  use super::*;
573
729