@desklink/host 0.3.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.
- package/NOTICE +3 -0
- package/README.md +63 -15
- package/bin/desklink-host.mjs +12 -4
- package/dist/engineProcess.d.ts +6 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +34 -4
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +32 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolveEngine.d.ts +6 -0
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +45 -6
- package/dist/resolveEngine.js.map +1 -1
- package/docs/PROTOCOL.md +119 -6
- package/engine/Cargo.lock +2 -2
- package/engine/Cargo.toml +7 -12
- package/engine/build.rs +61 -5
- package/engine/native/agent_overlay_mac.m +51 -4
- package/engine/native/agent_overlay_wayland.c +145 -12
- package/engine/native/agent_overlay_x11.c +72 -0
- package/engine/src/capture.rs +92 -3
- package/engine/src/capture_win.rs +23 -0
- package/engine/src/clipboard_win.rs +8 -0
- package/engine/src/cursor.rs +118 -0
- package/engine/src/indicator.rs +104 -3
- package/engine/src/input_win.rs +91 -0
- package/engine/src/keymap_win.rs +39 -0
- package/engine/src/mac.rs +28 -6
- package/engine/src/main.rs +165 -23
- package/engine/src/portal.rs +42 -4
- package/engine/src/portal_win.rs +37 -0
- package/engine/src/protocol.rs +49 -0
- package/engine/src/session.rs +467 -17
- package/engine/src/win.rs +110 -0
- package/engine/src/x11.rs +15 -0
- package/package.json +4 -4
package/engine/src/indicator.rs
CHANGED
|
@@ -13,7 +13,30 @@ pub struct Indicator {
|
|
|
13
13
|
position: std::sync::Arc<std::sync::Mutex<Option<(i64, i64)>>>,
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
#[cfg(target_os = "linux")]
|
|
17
|
+
fn point_wayland_executable() -> Result<std::path::PathBuf> {
|
|
18
|
+
#[cfg(test)]
|
|
19
|
+
if let Some(path) = std::env::var_os("DESKLINK_POINT_TEST_ENGINE") {
|
|
20
|
+
return Ok(path.into());
|
|
21
|
+
}
|
|
22
|
+
Ok(std::env::current_exe()?)
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
impl Indicator {
|
|
26
|
+
#[cfg(target_os = "linux")]
|
|
27
|
+
pub fn wayland_layer_shell_available() -> Result<bool> {
|
|
28
|
+
let mut command = Command::new(point_wayland_executable()?);
|
|
29
|
+
command.arg("point-layer-shell-probe");
|
|
30
|
+
match Self::spawn(&mut command, 1.0, 1.0) {
|
|
31
|
+
Ok(helper) => {
|
|
32
|
+
helper.stop();
|
|
33
|
+
Ok(true)
|
|
34
|
+
}
|
|
35
|
+
Err(error) if error.to_string().contains("layer_shell_unavailable") => Ok(false),
|
|
36
|
+
Err(error) => Err(error),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
17
40
|
#[cfg(target_os = "macos")]
|
|
18
41
|
pub fn start(display_id: u32, width: usize, height: usize) -> Result<Self> {
|
|
19
42
|
let executable = std::env::current_exe()?;
|
|
@@ -39,6 +62,77 @@ impl Indicator {
|
|
|
39
62
|
)
|
|
40
63
|
}
|
|
41
64
|
|
|
65
|
+
#[cfg(target_os = "linux")]
|
|
66
|
+
pub fn start_point(display: &str) -> Result<Self> {
|
|
67
|
+
Self::spawn(
|
|
68
|
+
Command::new(std::env::current_exe()?)
|
|
69
|
+
.arg("point-overlay")
|
|
70
|
+
.arg(display),
|
|
71
|
+
1.0,
|
|
72
|
+
1.0,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[cfg(target_os = "linux")]
|
|
77
|
+
pub fn start_point_wayland(
|
|
78
|
+
width: i32,
|
|
79
|
+
height: i32,
|
|
80
|
+
position: Option<(i32, i32)>,
|
|
81
|
+
) -> Result<Self> {
|
|
82
|
+
let (x, y) = position.unwrap_or_default();
|
|
83
|
+
Self::spawn(
|
|
84
|
+
Command::new(point_wayland_executable()?)
|
|
85
|
+
.arg("point-overlay-wayland")
|
|
86
|
+
.args([
|
|
87
|
+
width.to_string(),
|
|
88
|
+
height.to_string(),
|
|
89
|
+
i32::from(position.is_some()).to_string(),
|
|
90
|
+
x.to_string(),
|
|
91
|
+
y.to_string(),
|
|
92
|
+
]),
|
|
93
|
+
1.0,
|
|
94
|
+
1.0,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[cfg(target_os = "macos")]
|
|
99
|
+
pub fn start_point(display_id: u32, width: usize, height: usize) -> Result<Self> {
|
|
100
|
+
let (_, _, points_w, points_h) = crate::mac::display_geometry(display_id)
|
|
101
|
+
.context("the selected display is unavailable")?;
|
|
102
|
+
Self::spawn(
|
|
103
|
+
Command::new(std::env::current_exe()?)
|
|
104
|
+
.arg("point-overlay")
|
|
105
|
+
.arg(display_id.to_string()),
|
|
106
|
+
points_w / width as f64,
|
|
107
|
+
points_h / height as f64,
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[cfg(all(test, target_os = "linux"))]
|
|
112
|
+
pub fn test_pid(&self) -> u32 {
|
|
113
|
+
self.child.as_ref().unwrap().id()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
pub fn point(&mut self, x: i64, y: i64, timeout_ms: u64, label: &str) -> Result<()> {
|
|
117
|
+
let input = self.input.as_mut().context("point overlay exited")?;
|
|
118
|
+
writeln!(
|
|
119
|
+
input,
|
|
120
|
+
"P {:.2} {:.2} {timeout_ms} {label}",
|
|
121
|
+
x as f64 * self.scale_x,
|
|
122
|
+
y as f64 * self.scale_y
|
|
123
|
+
)?;
|
|
124
|
+
Ok(())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Point markers have no fade: close/clear must remove them and reap the helper.
|
|
128
|
+
pub fn stop(mut self) {
|
|
129
|
+
self.input.take();
|
|
130
|
+
if let Some(mut child) = self.child.take() {
|
|
131
|
+
let _ = child.kill();
|
|
132
|
+
let _ = child.wait();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
42
136
|
#[cfg(target_os = "linux")]
|
|
43
137
|
pub fn start_wayland(width: usize, height: usize) -> Result<Self> {
|
|
44
138
|
let executable = std::env::current_exe()?;
|
|
@@ -69,13 +163,20 @@ impl Indicator {
|
|
|
69
163
|
let reader = std::thread::spawn(move || {
|
|
70
164
|
let mut ready = String::new();
|
|
71
165
|
let result = BufReader::new(stdout).read_line(&mut ready);
|
|
72
|
-
let _ = ready_tx.send(result.is_ok()
|
|
166
|
+
let _ = ready_tx.send(if result.is_ok() {
|
|
167
|
+
ready.trim().to_owned()
|
|
168
|
+
} else {
|
|
169
|
+
String::new()
|
|
170
|
+
});
|
|
73
171
|
});
|
|
74
|
-
|
|
172
|
+
let ready = ready_rx
|
|
173
|
+
.recv_timeout(std::time::Duration::from_secs(5))
|
|
174
|
+
.unwrap_or_default();
|
|
175
|
+
if ready != "READY" {
|
|
75
176
|
let _ = child.kill();
|
|
76
177
|
let _ = child.wait();
|
|
77
178
|
let _ = reader.join();
|
|
78
|
-
anyhow::bail!("agent indicator did not open its overlay within 5s");
|
|
179
|
+
anyhow::bail!("agent indicator did not open its overlay within 5s: {ready}");
|
|
79
180
|
}
|
|
80
181
|
let _ = reader.join();
|
|
81
182
|
Ok(Self {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
use anyhow::{bail, Result};
|
|
2
|
+
|
|
3
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
4
|
+
pub enum Button {
|
|
5
|
+
Left,
|
|
6
|
+
Middle,
|
|
7
|
+
Right,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
impl Button {
|
|
11
|
+
pub fn from_number(number: i64) -> Self {
|
|
12
|
+
match number {
|
|
13
|
+
2 => Self::Middle,
|
|
14
|
+
3 => Self::Right,
|
|
15
|
+
_ => Self::Left,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
|
21
|
+
pub struct HeldState {
|
|
22
|
+
keys: Vec<i16>,
|
|
23
|
+
buttons: Vec<Button>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl HeldState {
|
|
27
|
+
pub fn key(&mut self, code: i16, down: bool) {
|
|
28
|
+
if down {
|
|
29
|
+
if !self.keys.contains(&code) {
|
|
30
|
+
self.keys.push(code);
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
self.keys.retain(|held| *held != code);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub fn button(&mut self, button: Button, down: bool) {
|
|
38
|
+
if down {
|
|
39
|
+
if !self.buttons.contains(&button) {
|
|
40
|
+
self.buttons.push(button);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
self.buttons.retain(|held| *held != button);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub fn release_plan(&mut self) -> (Vec<Button>, Vec<i16>) {
|
|
48
|
+
(
|
|
49
|
+
std::mem::take(&mut self.buttons),
|
|
50
|
+
std::mem::take(&mut self.keys),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[derive(Debug, Clone, serde::Serialize)]
|
|
56
|
+
pub struct InputUnavailable {
|
|
57
|
+
pub reason: String,
|
|
58
|
+
pub remedy: String,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub fn unavailable_reason() -> &'static str {
|
|
62
|
+
"Windows input is not implemented in this build."
|
|
63
|
+
}
|
|
64
|
+
pub fn probe() -> Result<(), InputUnavailable> {
|
|
65
|
+
Err(InputUnavailable {
|
|
66
|
+
reason: unavailable_reason().into(),
|
|
67
|
+
remedy: "Use a future Windows input-enabled build.".into(),
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
// This private, unconstructible adapter cannot inject native input.
|
|
71
|
+
pub struct InputDevices {
|
|
72
|
+
_private: (),
|
|
73
|
+
}
|
|
74
|
+
impl InputDevices {
|
|
75
|
+
pub fn create_for_display(_width: i32, _height: i32, _display: u32) -> Result<Self> {
|
|
76
|
+
bail!("{}", unavailable_reason())
|
|
77
|
+
}
|
|
78
|
+
pub fn move_absolute(&mut self, _x: i64, _y: i64) {}
|
|
79
|
+
pub fn button(&mut self, _button: Button, _down: bool) {}
|
|
80
|
+
pub fn scroll(&mut self, _dx: f64, _dy: f64) {}
|
|
81
|
+
pub fn key(&mut self, _code: i16, _down: bool) -> Result<()> {
|
|
82
|
+
bail!("{}", unavailable_reason())
|
|
83
|
+
}
|
|
84
|
+
pub fn unicode_text(&mut self, _text: &str) -> Result<()> {
|
|
85
|
+
bail!("{}", unavailable_reason())
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub fn native_keycode(_code: i16) -> Result<i16> {
|
|
90
|
+
bail!("{}", unavailable_reason())
|
|
91
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
|
|
3
|
+
// No key mapping is advertised until the native input lane is implemented.
|
|
4
|
+
pub struct Keystroke {
|
|
5
|
+
pub code: i16,
|
|
6
|
+
}
|
|
7
|
+
impl Keystroke {
|
|
8
|
+
pub fn modifiers(&self) -> Vec<i16> {
|
|
9
|
+
Vec::new()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub fn named_key(_name: &str) -> Option<Keystroke> {
|
|
14
|
+
None
|
|
15
|
+
}
|
|
16
|
+
pub fn modifier_key(_name: &str) -> Option<i16> {
|
|
17
|
+
None
|
|
18
|
+
}
|
|
19
|
+
pub fn keyboard_layout() -> &'static str {
|
|
20
|
+
"unknown"
|
|
21
|
+
}
|
|
22
|
+
pub struct LayoutNames;
|
|
23
|
+
impl LayoutNames {
|
|
24
|
+
pub fn from_environment() -> Self {
|
|
25
|
+
Self
|
|
26
|
+
}
|
|
27
|
+
pub fn identity(&self) -> String {
|
|
28
|
+
keyboard_layout().into()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
pub struct Layout;
|
|
32
|
+
impl Layout {
|
|
33
|
+
pub fn from_environment() -> Result<Self> {
|
|
34
|
+
Ok(Self)
|
|
35
|
+
}
|
|
36
|
+
pub fn keystroke_for_char(&self, _character: char) -> Option<Keystroke> {
|
|
37
|
+
None
|
|
38
|
+
}
|
|
39
|
+
}
|
package/engine/src/mac.rs
CHANGED
|
@@ -26,6 +26,7 @@ struct CGRect {
|
|
|
26
26
|
type DisplayId = u32;
|
|
27
27
|
|
|
28
28
|
unsafe extern "C" {
|
|
29
|
+
fn desklink_point_overlay_main(display_id: i32) -> i32;
|
|
29
30
|
fn desklink_agent_overlay_main(display_id: i32) -> i32;
|
|
30
31
|
fn desklink_image_bgra(image: *const c_void, length: *mut usize, stride: *mut usize)
|
|
31
32
|
-> *mut u8;
|
|
@@ -182,6 +183,7 @@ fn capabilities() -> Value {
|
|
|
182
183
|
"backends": ["screencapturekit"],
|
|
183
184
|
"formats": ["bgra"],
|
|
184
185
|
"cursor": "embedded",
|
|
186
|
+
"cursor_modes": ["embedded"],
|
|
185
187
|
"audio": false,
|
|
186
188
|
"displays": displays(),
|
|
187
189
|
"grant": if capture_granted { "granted" } else { "missing-screen-recording" },
|
|
@@ -463,14 +465,25 @@ fn indicator_demo(display_id: u32, path: &str) -> Result<(), String> {
|
|
|
463
465
|
fn record_agent_overlay(path: &str) -> Result<(), String> {
|
|
464
466
|
use std::io::Write;
|
|
465
467
|
let display = unsafe { CGMainDisplayID() };
|
|
466
|
-
let (w, h) = crate::convert::fit(
|
|
467
|
-
unsafe {
|
|
468
|
+
let (w, h) = crate::convert::fit(
|
|
469
|
+
unsafe { CGDisplayPixelsWide(display) },
|
|
470
|
+
unsafe { CGDisplayPixelsHigh(display) },
|
|
471
|
+
960,
|
|
472
|
+
640,
|
|
473
|
+
);
|
|
468
474
|
let mut file = std::fs::File::create(path).map_err(|error| error.to_string())?;
|
|
469
475
|
for _ in 0..30 {
|
|
470
476
|
let (pixels, sw, sh, stride) = capture_display(display)?;
|
|
471
|
-
let raw = crate::convert::to_bgrx(
|
|
472
|
-
|
|
473
|
-
|
|
477
|
+
let raw = crate::convert::to_bgrx(
|
|
478
|
+
&pixels,
|
|
479
|
+
sw,
|
|
480
|
+
sh,
|
|
481
|
+
stride,
|
|
482
|
+
crate::convert::PixelFormat::Bgra,
|
|
483
|
+
w,
|
|
484
|
+
h,
|
|
485
|
+
)
|
|
486
|
+
.ok_or_else(|| String::from("could not scale recording frame"))?;
|
|
474
487
|
file.write_all(&raw).map_err(|error| error.to_string())?;
|
|
475
488
|
std::thread::sleep(Duration::from_millis(100));
|
|
476
489
|
}
|
|
@@ -581,7 +594,12 @@ pub fn run() -> i32 {
|
|
|
581
594
|
let command = args.next().unwrap_or_else(|| "help".into());
|
|
582
595
|
if matches!(
|
|
583
596
|
command.as_str(),
|
|
584
|
-
"serve"
|
|
597
|
+
"serve"
|
|
598
|
+
| "capabilities"
|
|
599
|
+
| "capture-probe"
|
|
600
|
+
| "indicator-demo"
|
|
601
|
+
| "axi-record"
|
|
602
|
+
| "setup-input"
|
|
585
603
|
) {
|
|
586
604
|
#[cfg(not(desklink_macos_cli))]
|
|
587
605
|
if let Err(error) = ensure_disclaimed() {
|
|
@@ -599,6 +617,10 @@ pub fn run() -> i32 {
|
|
|
599
617
|
.status();
|
|
600
618
|
return match result { Ok(status) => status.code().unwrap_or(1), Err(error) => { eprintln!("axi bridge: {error}"); 1 } };
|
|
601
619
|
}
|
|
620
|
+
"point-overlay" => {
|
|
621
|
+
let display = args.next().and_then(|value| value.parse().ok()).unwrap_or(0);
|
|
622
|
+
return unsafe { desklink_point_overlay_main(display) };
|
|
623
|
+
}
|
|
602
624
|
"agent-overlay" => {
|
|
603
625
|
let display = args.next().and_then(|value| value.parse().ok()).unwrap_or(0);
|
|
604
626
|
return unsafe { desklink_agent_overlay_main(display) };
|
package/engine/src/main.rs
CHANGED
|
@@ -11,18 +11,29 @@ mod capture;
|
|
|
11
11
|
#[cfg(target_os = "macos")]
|
|
12
12
|
#[path = "capture_mac.rs"]
|
|
13
13
|
mod capture;
|
|
14
|
+
#[cfg(target_os = "windows")]
|
|
15
|
+
#[path = "capture_win.rs"]
|
|
16
|
+
mod capture;
|
|
14
17
|
#[cfg(target_os = "linux")]
|
|
15
18
|
mod clipboard;
|
|
16
19
|
#[cfg(target_os = "macos")]
|
|
17
20
|
#[path = "clipboard_mac.rs"]
|
|
18
21
|
mod clipboard;
|
|
19
|
-
#[cfg(
|
|
22
|
+
#[cfg(target_os = "windows")]
|
|
23
|
+
#[path = "clipboard_win.rs"]
|
|
24
|
+
mod clipboard;
|
|
25
|
+
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
|
|
20
26
|
mod convert;
|
|
21
27
|
#[cfg(target_os = "linux")]
|
|
28
|
+
mod cursor;
|
|
29
|
+
#[cfg(target_os = "linux")]
|
|
22
30
|
mod encoder;
|
|
23
|
-
#[cfg(all(target_os = "macos", desklink_vpx))]
|
|
31
|
+
#[cfg(all(any(target_os = "macos", target_os = "windows"), desklink_vpx))]
|
|
24
32
|
mod encoder;
|
|
25
|
-
#[cfg(any(
|
|
33
|
+
#[cfg(any(
|
|
34
|
+
target_os = "linux",
|
|
35
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
36
|
+
))]
|
|
26
37
|
mod h264;
|
|
27
38
|
#[cfg(target_os = "linux")]
|
|
28
39
|
mod h264_nvenc;
|
|
@@ -32,13 +43,19 @@ mod h264_openh264;
|
|
|
32
43
|
mod h264_vaapi;
|
|
33
44
|
#[cfg(all(target_os = "macos", desklink_vpx))]
|
|
34
45
|
mod h264_vt;
|
|
35
|
-
#[cfg(any(
|
|
46
|
+
#[cfg(any(
|
|
47
|
+
target_os = "linux",
|
|
48
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
49
|
+
))]
|
|
36
50
|
mod indicator;
|
|
37
51
|
#[cfg(target_os = "linux")]
|
|
38
52
|
mod input;
|
|
39
53
|
#[cfg(target_os = "macos")]
|
|
40
54
|
#[path = "input_mac.rs"]
|
|
41
55
|
mod input;
|
|
56
|
+
#[cfg(target_os = "windows")]
|
|
57
|
+
#[path = "input_win.rs"]
|
|
58
|
+
mod input;
|
|
42
59
|
#[cfg(target_os = "linux")]
|
|
43
60
|
mod keeper;
|
|
44
61
|
#[cfg(target_os = "linux")]
|
|
@@ -46,44 +63,76 @@ mod keymap;
|
|
|
46
63
|
#[cfg(target_os = "macos")]
|
|
47
64
|
#[path = "keymap_mac.rs"]
|
|
48
65
|
mod keymap;
|
|
66
|
+
#[cfg(target_os = "windows")]
|
|
67
|
+
#[path = "keymap_win.rs"]
|
|
68
|
+
mod keymap;
|
|
49
69
|
#[cfg(target_os = "macos")]
|
|
50
70
|
mod mac;
|
|
51
|
-
#[cfg(any(
|
|
71
|
+
#[cfg(any(
|
|
72
|
+
target_os = "linux",
|
|
73
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
74
|
+
))]
|
|
52
75
|
mod peer;
|
|
53
76
|
#[cfg(target_os = "linux")]
|
|
54
77
|
mod portal;
|
|
55
78
|
#[cfg(target_os = "macos")]
|
|
56
79
|
#[path = "portal_mac.rs"]
|
|
57
80
|
mod portal;
|
|
58
|
-
#[cfg(
|
|
81
|
+
#[cfg(target_os = "windows")]
|
|
82
|
+
#[path = "portal_win.rs"]
|
|
83
|
+
mod portal;
|
|
84
|
+
#[cfg(any(
|
|
85
|
+
target_os = "linux",
|
|
86
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
87
|
+
))]
|
|
59
88
|
mod protocol;
|
|
60
89
|
#[cfg(target_os = "linux")]
|
|
61
90
|
mod session;
|
|
62
|
-
#[cfg(all(target_os = "macos", desklink_vpx))]
|
|
91
|
+
#[cfg(all(any(target_os = "macos", target_os = "windows"), desklink_vpx))]
|
|
63
92
|
mod session;
|
|
93
|
+
#[cfg(target_os = "windows")]
|
|
94
|
+
mod win;
|
|
64
95
|
#[cfg(target_os = "linux")]
|
|
65
96
|
mod x11;
|
|
66
|
-
#[cfg(target_os = "macos")]
|
|
97
|
+
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
67
98
|
#[path = "x11_mac.rs"]
|
|
68
99
|
mod x11;
|
|
69
100
|
|
|
70
|
-
#[cfg(any(
|
|
101
|
+
#[cfg(any(
|
|
102
|
+
target_os = "linux",
|
|
103
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
104
|
+
))]
|
|
71
105
|
use anyhow::Result;
|
|
72
|
-
#[cfg(any(
|
|
106
|
+
#[cfg(any(
|
|
107
|
+
target_os = "linux",
|
|
108
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
109
|
+
))]
|
|
73
110
|
use protocol::{ErrorBody, Event, Request, Response};
|
|
74
111
|
#[cfg(target_os = "linux")]
|
|
75
112
|
use std::sync::mpsc as std_mpsc;
|
|
76
|
-
#[cfg(any(
|
|
113
|
+
#[cfg(any(
|
|
114
|
+
target_os = "linux",
|
|
115
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
116
|
+
))]
|
|
77
117
|
use std::time::Duration;
|
|
78
|
-
#[cfg(any(
|
|
118
|
+
#[cfg(any(
|
|
119
|
+
target_os = "linux",
|
|
120
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
121
|
+
))]
|
|
79
122
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
|
80
|
-
#[cfg(any(
|
|
123
|
+
#[cfg(any(
|
|
124
|
+
target_os = "linux",
|
|
125
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
126
|
+
))]
|
|
81
127
|
use tokio::sync::mpsc as tokio_mpsc;
|
|
82
128
|
|
|
83
129
|
#[cfg(target_os = "linux")]
|
|
84
130
|
unsafe extern "C" {
|
|
131
|
+
fn desklink_point_overlay_main(display: *const std::ffi::c_char) -> i32;
|
|
85
132
|
fn desklink_agent_overlay_main(display: *const std::ffi::c_char) -> i32;
|
|
86
133
|
fn desklink_agent_overlay_wayland(source_w: i32, source_h: i32) -> i32;
|
|
134
|
+
fn desklink_wayland_layer_shell_available() -> i32;
|
|
135
|
+
fn desklink_point_overlay_wayland(w: i32, h: i32, known: i32, x: i32, y: i32) -> i32;
|
|
87
136
|
}
|
|
88
137
|
|
|
89
138
|
#[cfg(target_os = "linux")]
|
|
@@ -105,11 +154,39 @@ fn main() {
|
|
|
105
154
|
println!("{}", env!("CARGO_PKG_VERSION"));
|
|
106
155
|
0
|
|
107
156
|
}
|
|
157
|
+
"point-overlay" => {
|
|
158
|
+
let display = args.get(1).map(String::as_str).unwrap_or("");
|
|
159
|
+
let display = std::ffi::CString::new(display).expect("valid X display");
|
|
160
|
+
unsafe { desklink_point_overlay_main(display.as_ptr()) }
|
|
161
|
+
}
|
|
108
162
|
"agent-overlay" => {
|
|
109
163
|
let display = args.get(1).map(String::as_str).unwrap_or("");
|
|
110
164
|
let display = std::ffi::CString::new(display).expect("valid X display");
|
|
111
165
|
unsafe { desklink_agent_overlay_main(display.as_ptr()) }
|
|
112
166
|
}
|
|
167
|
+
"point-layer-shell-probe" => match unsafe { desklink_wayland_layer_shell_available() } {
|
|
168
|
+
1 => {
|
|
169
|
+
println!("READY");
|
|
170
|
+
0
|
|
171
|
+
}
|
|
172
|
+
0 => {
|
|
173
|
+
println!("layer_shell_unavailable");
|
|
174
|
+
2
|
|
175
|
+
}
|
|
176
|
+
_ => 2,
|
|
177
|
+
},
|
|
178
|
+
"point-overlay-wayland" => {
|
|
179
|
+
let values: Vec<i32> = args.iter().skip(1).filter_map(|v| v.parse().ok()).collect();
|
|
180
|
+
if values.len() != 5 || values[0] <= 0 || values[1] <= 0 {
|
|
181
|
+
2
|
|
182
|
+
} else {
|
|
183
|
+
unsafe {
|
|
184
|
+
desklink_point_overlay_wayland(
|
|
185
|
+
values[0], values[1], values[2], values[3], values[4],
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
113
190
|
"agent-overlay-wayland" => {
|
|
114
191
|
let width = args.get(1).and_then(|v| v.parse().ok()).unwrap_or(0);
|
|
115
192
|
let height = args.get(2).and_then(|v| v.parse().ok()).unwrap_or(0);
|
|
@@ -149,7 +226,10 @@ fn main() {
|
|
|
149
226
|
std::process::exit(code);
|
|
150
227
|
}
|
|
151
228
|
|
|
152
|
-
#[cfg(any(
|
|
229
|
+
#[cfg(any(
|
|
230
|
+
target_os = "linux",
|
|
231
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
232
|
+
))]
|
|
153
233
|
fn report(result: Result<()>) -> i32 {
|
|
154
234
|
match result {
|
|
155
235
|
Ok(()) => 0,
|
|
@@ -295,7 +375,7 @@ fn frame_shape(frame: &crate::convert::I420) -> u64 {
|
|
|
295
375
|
|
|
296
376
|
#[cfg(target_os = "linux")]
|
|
297
377
|
async fn probe_portal(seconds: u64) -> Result<()> {
|
|
298
|
-
let portal = portal::open(None).await?;
|
|
378
|
+
let portal = portal::open(None, protocol::CursorMode::Embedded).await?;
|
|
299
379
|
eprintln!("portal source: {:?}", portal.source);
|
|
300
380
|
// The source's own size: a zero box converts nothing and reports no frames.
|
|
301
381
|
let (width, height) = crate::convert::fit(
|
|
@@ -311,6 +391,7 @@ async fn probe_portal(seconds: u64) -> Result<()> {
|
|
|
311
391
|
width,
|
|
312
392
|
height,
|
|
313
393
|
30,
|
|
394
|
+
None,
|
|
314
395
|
Box::new(move |frame, seq, _raw| {
|
|
315
396
|
let _ = tx.send((frame.width, frame.height, seq));
|
|
316
397
|
}),
|
|
@@ -349,7 +430,10 @@ async fn probe_portal(seconds: u64) -> Result<()> {
|
|
|
349
430
|
}
|
|
350
431
|
|
|
351
432
|
/// The local control protocol loop.
|
|
352
|
-
#[cfg(any(
|
|
433
|
+
#[cfg(any(
|
|
434
|
+
target_os = "linux",
|
|
435
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
436
|
+
))]
|
|
353
437
|
async fn serve() -> Result<()> {
|
|
354
438
|
let (out_tx, mut out_rx) = tokio_mpsc::unbounded_channel::<String>();
|
|
355
439
|
let writer = tokio::spawn(async move {
|
|
@@ -434,13 +518,16 @@ async fn serve() -> Result<()> {
|
|
|
434
518
|
if let Some(session) = current.take() {
|
|
435
519
|
session.close("the consumer disconnected").await;
|
|
436
520
|
}
|
|
437
|
-
|
|
521
|
+
drop(events_tx);
|
|
438
522
|
drop(out_tx);
|
|
439
523
|
let _ = writer.await;
|
|
440
524
|
Ok(())
|
|
441
525
|
}
|
|
442
526
|
|
|
443
|
-
#[cfg(any(
|
|
527
|
+
#[cfg(any(
|
|
528
|
+
target_os = "linux",
|
|
529
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
530
|
+
))]
|
|
444
531
|
fn render_event(notice: session::Notice) -> Option<String> {
|
|
445
532
|
let session::Notice { session_id, event } = notice;
|
|
446
533
|
let (name, params) = match event {
|
|
@@ -471,9 +558,17 @@ fn render_event(notice: session::Notice) -> Option<String> {
|
|
|
471
558
|
"session.state",
|
|
472
559
|
serde_json::json!({ "sessionId": session_id, "capture": capture, "transport": transport, "firstFrame": first_frame }),
|
|
473
560
|
),
|
|
474
|
-
session::SessionEvent::
|
|
561
|
+
session::SessionEvent::Cursor { position } => (
|
|
562
|
+
"session.cursor",
|
|
563
|
+
serde_json::json!({ "sessionId": session_id, "x": position.x, "y": position.y, "visible": position.visible, "timestamp_us": position.timestamp_us }),
|
|
564
|
+
),
|
|
565
|
+
session::SessionEvent::Frame {
|
|
566
|
+
seq,
|
|
567
|
+
damage,
|
|
568
|
+
timestamp_us,
|
|
569
|
+
} => (
|
|
475
570
|
"session.frame.changed",
|
|
476
|
-
serde_json::json!({ "sessionId": session_id, "seq": seq, "damage": damage }),
|
|
571
|
+
serde_json::json!({ "sessionId": session_id, "seq": seq, "damage": damage, "timestamp_us": timestamp_us }),
|
|
477
572
|
),
|
|
478
573
|
session::SessionEvent::Input { input } => (
|
|
479
574
|
"session.input",
|
|
@@ -503,7 +598,10 @@ fn render_event(notice: session::Notice) -> Option<String> {
|
|
|
503
598
|
.ok()
|
|
504
599
|
}
|
|
505
600
|
|
|
506
|
-
#[cfg(any(
|
|
601
|
+
#[cfg(any(
|
|
602
|
+
target_os = "linux",
|
|
603
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
604
|
+
))]
|
|
507
605
|
async fn dispatch(
|
|
508
606
|
request: &Request,
|
|
509
607
|
hello_seen: &mut bool,
|
|
@@ -546,6 +644,7 @@ async fn dispatch(
|
|
|
546
644
|
.map_err(|error| ErrorBody::new("malformed", error.to_string()))?;
|
|
547
645
|
let open = session::OpenRequest {
|
|
548
646
|
source: params.source,
|
|
647
|
+
cursor: params.cursor,
|
|
549
648
|
permissions: params.permissions,
|
|
550
649
|
max_width: params.max_width,
|
|
551
650
|
max_height: params.max_height,
|
|
@@ -580,10 +679,21 @@ async fn dispatch(
|
|
|
580
679
|
"origin": { "x": session.source().origin_x, "y": session.source().origin_y },
|
|
581
680
|
},
|
|
582
681
|
"geometry": session.geometry(),
|
|
682
|
+
"cursor": session.cursor_info(),
|
|
583
683
|
});
|
|
584
684
|
*current = Some(session);
|
|
585
685
|
Ok(result)
|
|
586
686
|
}
|
|
687
|
+
"session.point" => {
|
|
688
|
+
let session = require_session(current)?;
|
|
689
|
+
let params: protocol::PointParams = serde_json::from_value(request.params.clone())
|
|
690
|
+
.map_err(|error| ErrorBody::new("malformed", error.to_string()))?;
|
|
691
|
+
check_session(session, ¶ms.session_id, None)?;
|
|
692
|
+
session
|
|
693
|
+
.point(¶ms)
|
|
694
|
+
.map_err(|error| ErrorBody::new(error.code, error.message))?;
|
|
695
|
+
Ok(serde_json::json!({"shown": !params.clear}))
|
|
696
|
+
}
|
|
587
697
|
"session.description" => {
|
|
588
698
|
let session = require_session(current)?;
|
|
589
699
|
let params: protocol::DescriptionParams =
|
|
@@ -705,7 +815,10 @@ async fn dispatch(
|
|
|
705
815
|
}
|
|
706
816
|
}
|
|
707
817
|
|
|
708
|
-
#[cfg(any(
|
|
818
|
+
#[cfg(any(
|
|
819
|
+
target_os = "linux",
|
|
820
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
821
|
+
))]
|
|
709
822
|
fn require_session(
|
|
710
823
|
current: &mut Option<session::Session>,
|
|
711
824
|
) -> std::result::Result<&mut session::Session, ErrorBody> {
|
|
@@ -716,7 +829,10 @@ fn require_session(
|
|
|
716
829
|
|
|
717
830
|
/// A late message for a session that has already ended must not revive it, and a
|
|
718
831
|
/// message for a different session must not reach this one.
|
|
719
|
-
#[cfg(any(
|
|
832
|
+
#[cfg(any(
|
|
833
|
+
target_os = "linux",
|
|
834
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
835
|
+
))]
|
|
720
836
|
fn check_session(
|
|
721
837
|
session: &session::Session,
|
|
722
838
|
id: &str,
|
|
@@ -741,6 +857,11 @@ fn main() {
|
|
|
741
857
|
std::process::exit(mac::run());
|
|
742
858
|
}
|
|
743
859
|
|
|
860
|
+
#[cfg(target_os = "windows")]
|
|
861
|
+
fn main() {
|
|
862
|
+
std::process::exit(win::run());
|
|
863
|
+
}
|
|
864
|
+
|
|
744
865
|
#[cfg(all(test, target_os = "linux"))]
|
|
745
866
|
mod tests {
|
|
746
867
|
use super::*;
|
|
@@ -762,6 +883,27 @@ mod tests {
|
|
|
762
883
|
assert!(!hello_seen, "a refused handshake must not open the gate");
|
|
763
884
|
}
|
|
764
885
|
|
|
886
|
+
#[test]
|
|
887
|
+
fn cursor_notice_preserves_source_coordinates_visibility_and_clock() {
|
|
888
|
+
let line = render_event(session::Notice {
|
|
889
|
+
session_id: String::from("cursor-session"),
|
|
890
|
+
event: session::SessionEvent::Cursor {
|
|
891
|
+
position: protocol::CursorPosition {
|
|
892
|
+
x: 32,
|
|
893
|
+
y: 64,
|
|
894
|
+
visible: false,
|
|
895
|
+
timestamp_us: 123456,
|
|
896
|
+
},
|
|
897
|
+
},
|
|
898
|
+
})
|
|
899
|
+
.unwrap();
|
|
900
|
+
let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
|
|
901
|
+
assert_eq!(
|
|
902
|
+
parsed,
|
|
903
|
+
serde_json::json!({"event":"session.cursor","params":{"sessionId":"cursor-session","x":32,"y":64,"visible":false,"timestamp_us":123456}})
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
|
|
765
907
|
#[tokio::test]
|
|
766
908
|
async fn the_state_notification_carries_exactly_what_the_document_promises() {
|
|
767
909
|
let line = render_event(session::Notice {
|