@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.
- package/NOTICE +3 -0
- package/README.md +72 -16
- package/bin/desklink-host.mjs +14 -3
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +5 -1
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +17 -5
- package/dist/engineProcess.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/keeper.d.ts +52 -0
- package/dist/keeper.d.ts.map +1 -0
- package/dist/keeper.js +101 -0
- package/dist/keeper.js.map +1 -0
- package/dist/protocol.d.ts +42 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolveEngine.d.ts +3 -2
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +20 -5
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +11 -6
- package/docs/PROTOCOL.md +297 -33
- package/engine/Cargo.lock +196 -1
- package/engine/Cargo.toml +26 -5
- package/engine/build.rs +98 -23
- package/engine/native/agent_overlay_mac.m +133 -0
- package/engine/native/agent_overlay_wayland.c +192 -0
- package/engine/native/agent_overlay_x11.c +103 -0
- package/engine/native/mac_stream.mm +188 -0
- package/engine/src/capture.rs +169 -19
- package/engine/src/capture_mac.rs +145 -0
- package/engine/src/clipboard_mac.rs +182 -0
- package/engine/src/convert.rs +57 -1
- package/engine/src/indicator.rs +162 -0
- package/engine/src/input.rs +12 -0
- package/engine/src/input_mac.rs +312 -0
- package/engine/src/keeper.rs +622 -0
- package/engine/src/keymap.rs +25 -0
- package/engine/src/keymap_mac.rs +209 -0
- package/engine/src/mac.rs +722 -0
- package/engine/src/main.rs +158 -2
- package/engine/src/peer.rs +429 -22
- package/engine/src/portal.rs +4 -0
- package/engine/src/portal_mac.rs +68 -0
- package/engine/src/protocol.rs +186 -3
- package/engine/src/session.rs +1307 -100
- package/engine/src/x11.rs +23 -3
- package/engine/src/x11_mac.rs +40 -0
- package/examples/reference-client.html +33 -4
- package/package.json +2 -2
package/engine/src/main.rs
CHANGED
|
@@ -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, ¶ms.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, ¶ms.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, ¶ms.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, ¶ms.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(
|
|
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
|
|