@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.
- package/NOTICE +3 -0
- package/README.md +92 -23
- package/bin/desklink-host.mjs +14 -5
- package/dist/bridge.d.ts +3 -4
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +18 -7
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts +6 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +34 -5
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +35 -2
- 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/BOUNDARY.md +8 -8
- package/docs/PROTOCOL.md +185 -45
- package/engine/Cargo.lock +2 -2
- package/engine/Cargo.toml +7 -12
- package/engine/build.rs +62 -5
- package/engine/native/agent_indicator.h +57 -0
- package/engine/native/agent_overlay_mac.m +51 -4
- package/engine/native/agent_overlay_wayland.c +189 -35
- package/engine/native/agent_overlay_x11.c +186 -55
- package/engine/src/capture.rs +96 -5
- package/engine/src/capture_win.rs +23 -0
- package/engine/src/clipboard.rs +174 -0
- package/engine/src/clipboard_win.rs +8 -0
- package/engine/src/cursor.rs +118 -0
- package/engine/src/indicator.rs +271 -5
- 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 +167 -25
- package/engine/src/peer.rs +10 -4
- package/engine/src/portal.rs +42 -4
- package/engine/src/portal_win.rs +37 -0
- package/engine/src/protocol.rs +56 -10
- package/engine/src/session.rs +749 -203
- package/engine/src/win.rs +110 -0
- package/engine/src/x11.rs +44 -29
- package/engine/src/x11_mac.rs +4 -0
- package/examples/reference-client.html +3 -5
- package/package.json +4 -4
|
@@ -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,
|
|
@@ -230,7 +310,7 @@ fn print_input_setup() {
|
|
|
230
310
|
println!(
|
|
231
311
|
"To undo it: remove the rule, remove the user from the group, reload the rules."
|
|
232
312
|
);
|
|
233
|
-
println!("Without it the engine still captures the desktop
|
|
313
|
+
println!("Without it the engine still captures the desktop; input actions are refused with input-unavailable.");
|
|
234
314
|
}
|
|
235
315
|
}
|
|
236
316
|
}
|
|
@@ -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,7 +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,
|
|
549
|
-
|
|
647
|
+
cursor: params.cursor,
|
|
550
648
|
max_width: params.max_width,
|
|
551
649
|
max_height: params.max_height,
|
|
552
650
|
bitrate_kbps: params.bitrate_kbps,
|
|
@@ -580,10 +678,22 @@ async fn dispatch(
|
|
|
580
678
|
"origin": { "x": session.source().origin_x, "y": session.source().origin_y },
|
|
581
679
|
},
|
|
582
680
|
"geometry": session.geometry(),
|
|
681
|
+
"cursor": session.cursor_info(),
|
|
682
|
+
"agentIndicator": session.agent_indicator(),
|
|
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 {
|
package/engine/src/peer.rs
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
//! module never assumes anything about that channel, which is what keeps the
|
|
6
6
|
//! engine usable by an application that is not this one.
|
|
7
7
|
//!
|
|
8
|
-
//!
|
|
9
|
-
//!
|
|
10
|
-
//!
|
|
11
|
-
//!
|
|
8
|
+
//! Remote input and clipboard ride the session's data channel; co-located
|
|
9
|
+
//! consumers can also use the local clipboard protocol. The channel is inside
|
|
10
|
+
//! the DTLS/SRTP session opened by a paired consumer and dies with it — a revoked
|
|
11
|
+
//! session cannot be driven by a client that kept a socket open.
|
|
12
12
|
|
|
13
13
|
use anyhow::{Context, Result};
|
|
14
14
|
use rtc::interceptor::{
|
|
@@ -1120,6 +1120,12 @@ impl VideoPeer {
|
|
|
1120
1120
|
}
|
|
1121
1121
|
|
|
1122
1122
|
pub async fn close(&self) {
|
|
1123
|
+
let _ = tokio::time::timeout(Duration::from_millis(500), async {
|
|
1124
|
+
while self.control.outstanding_bytes().await.unwrap_or(0) > 0 {
|
|
1125
|
+
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
1126
|
+
}
|
|
1127
|
+
})
|
|
1128
|
+
.await;
|
|
1123
1129
|
self.feedback.abort();
|
|
1124
1130
|
let _ = self.control.close().await;
|
|
1125
1131
|
let _ = self.peer.close().await;
|
package/engine/src/portal.rs
CHANGED
|
@@ -33,6 +33,7 @@ pub struct SelectedSource {
|
|
|
33
33
|
|
|
34
34
|
pub struct PortalSession {
|
|
35
35
|
pub fd: OwnedFd,
|
|
36
|
+
pub cursor_mode: &'static str,
|
|
36
37
|
pub source: SelectedSource,
|
|
37
38
|
pub restore_token: Option<String>,
|
|
38
39
|
}
|
|
@@ -46,7 +47,10 @@ pub async fn open_display(_: Option<u32>) -> Result<PortalSession> {
|
|
|
46
47
|
anyhow::bail!("display selection is only supported on macOS")
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
pub async fn open(
|
|
50
|
+
pub async fn open(
|
|
51
|
+
restore_token: Option<&str>,
|
|
52
|
+
cursor: crate::protocol::CursorMode,
|
|
53
|
+
) -> Result<PortalSession> {
|
|
50
54
|
let proxy = Screencast::new()
|
|
51
55
|
.await
|
|
52
56
|
.context("compositor has no ScreenCast portal")?;
|
|
@@ -59,6 +63,15 @@ pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
|
|
|
59
63
|
anyhow::bail!("ScreenCast portal advertises no monitor source");
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
let cursor_mode = if cursor == crate::protocol::CursorMode::Hidden {
|
|
67
|
+
let available = proxy
|
|
68
|
+
.available_cursor_modes()
|
|
69
|
+
.await
|
|
70
|
+
.context("portal did not advertise cursor modes")?;
|
|
71
|
+
choose_cursor_mode(available)?
|
|
72
|
+
} else {
|
|
73
|
+
CursorMode::Embedded
|
|
74
|
+
};
|
|
62
75
|
let session = proxy
|
|
63
76
|
.create_session()
|
|
64
77
|
.await
|
|
@@ -67,9 +80,7 @@ pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
|
|
|
67
80
|
proxy
|
|
68
81
|
.select_sources(
|
|
69
82
|
&session,
|
|
70
|
-
|
|
71
|
-
// make precise placement impossible.
|
|
72
|
-
CursorMode::Embedded,
|
|
83
|
+
cursor_mode,
|
|
73
84
|
BitFlags::from(SourceType::Monitor),
|
|
74
85
|
false,
|
|
75
86
|
restore_token,
|
|
@@ -104,6 +115,11 @@ pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
|
|
|
104
115
|
.context("OpenPipeWireRemote failed")?;
|
|
105
116
|
|
|
106
117
|
Ok(PortalSession {
|
|
118
|
+
cursor_mode: match cursor_mode {
|
|
119
|
+
CursorMode::Metadata => "metadata",
|
|
120
|
+
CursorMode::Hidden => "hidden",
|
|
121
|
+
_ => "embedded",
|
|
122
|
+
},
|
|
107
123
|
fd,
|
|
108
124
|
source: SelectedSource {
|
|
109
125
|
node_id: stream.pipe_wire_node_id(),
|
|
@@ -117,3 +133,25 @@ pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
|
|
|
117
133
|
restore_token: streams.restore_token().map(str::to_owned),
|
|
118
134
|
})
|
|
119
135
|
}
|
|
136
|
+
|
|
137
|
+
fn choose_cursor_mode(available: BitFlags<CursorMode>) -> Result<CursorMode> {
|
|
138
|
+
if available.contains(CursorMode::Metadata) {
|
|
139
|
+
return Ok(CursorMode::Metadata);
|
|
140
|
+
}
|
|
141
|
+
anyhow::bail!("cursor_positions_unavailable: portal does not provide cursor metadata")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#[cfg(test)]
|
|
145
|
+
mod tests {
|
|
146
|
+
use super::*;
|
|
147
|
+
#[test]
|
|
148
|
+
fn hidden_requires_metadata_and_refuses_hidden_only_portals() {
|
|
149
|
+
assert_eq!(
|
|
150
|
+
choose_cursor_mode(CursorMode::Metadata | CursorMode::Hidden).unwrap(),
|
|
151
|
+
CursorMode::Metadata
|
|
152
|
+
);
|
|
153
|
+
let error = choose_cursor_mode(CursorMode::Hidden.into()).unwrap_err();
|
|
154
|
+
assert!(error.to_string().contains("cursor_positions_unavailable:"));
|
|
155
|
+
assert!(choose_cursor_mode(CursorMode::Embedded.into()).is_err());
|
|
156
|
+
}
|
|
157
|
+
}
|