@desklink/host 0.5.9 → 0.5.10
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/README.md +5 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +2 -1
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +1 -0
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +58 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/docs/PROTOCOL.md +56 -0
- package/engine/Cargo.lock +2 -1
- package/engine/Cargo.toml +3 -1
- package/engine/src/input_mac.rs +433 -0
- package/engine/src/keymap_mac.rs +257 -0
- package/engine/src/mac.rs +8 -0
- package/engine/src/main.rs +12 -1
- package/engine/src/observe.rs +94 -0
- package/engine/src/portal.rs +83 -1
- package/engine/src/protocol.rs +30 -1
- package/engine/src/session.rs +59 -0
- package/package.json +3 -3
package/engine/src/mac.rs
CHANGED
|
@@ -39,6 +39,8 @@ unsafe extern "C" {
|
|
|
39
39
|
pub(crate) fn CGPreflightScreenCaptureAccess() -> bool;
|
|
40
40
|
pub(crate) fn CGPreflightPostEventAccess() -> bool;
|
|
41
41
|
pub(crate) fn CGRequestPostEventAccess() -> bool;
|
|
42
|
+
pub(crate) fn CGPreflightListenEventAccess() -> bool;
|
|
43
|
+
pub(crate) fn CGRequestListenEventAccess() -> bool;
|
|
42
44
|
fn CGGetActiveDisplayList(max: u32, displays: *mut DisplayId, count: *mut u32) -> i32;
|
|
43
45
|
fn CGMainDisplayID() -> DisplayId;
|
|
44
46
|
fn CGDisplayPixelsWide(display: DisplayId) -> usize;
|
|
@@ -713,6 +715,12 @@ pub fn run() -> i32 {
|
|
|
713
715
|
let _ = crate::input::request_access();
|
|
714
716
|
println!("If the Accessibility alert did not open, allow {} in System Settings › Privacy & Security › Accessibility.", tcc_responsible_app_name());
|
|
715
717
|
}
|
|
718
|
+
if crate::input::observe_probe().is_ok() {
|
|
719
|
+
println!("Input Monitoring permission is already granted to {}.", tcc_responsible_app_name());
|
|
720
|
+
} else {
|
|
721
|
+
let _ = crate::input::request_observe_access();
|
|
722
|
+
println!("If the Input Monitoring alert did not open, allow {} in System Settings › Privacy & Security › Input Monitoring.", tcc_responsible_app_name());
|
|
723
|
+
}
|
|
716
724
|
}
|
|
717
725
|
_ => println!("desklink-host {}\nUSAGE: desklink-host [serve|capabilities|capture-probe|encode-probe|setup-input|version]", env!("CARGO_PKG_VERSION")),
|
|
718
726
|
}
|
package/engine/src/main.rs
CHANGED
|
@@ -26,7 +26,7 @@ mod clipboard;
|
|
|
26
26
|
mod clipboard;
|
|
27
27
|
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
|
|
28
28
|
mod convert;
|
|
29
|
-
#[cfg(target_os = "linux")]
|
|
29
|
+
#[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
|
|
30
30
|
mod cursor;
|
|
31
31
|
#[cfg(target_os = "linux")]
|
|
32
32
|
mod encoder;
|
|
@@ -72,6 +72,8 @@ mod keymap;
|
|
|
72
72
|
mod keymap;
|
|
73
73
|
#[cfg(target_os = "macos")]
|
|
74
74
|
mod mac;
|
|
75
|
+
#[cfg(any(target_os = "linux", all(any(target_os = "macos", target_os = "windows"), desklink_vpx)))]
|
|
76
|
+
mod observe;
|
|
75
77
|
#[cfg(any(
|
|
76
78
|
target_os = "linux",
|
|
77
79
|
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
@@ -666,6 +668,13 @@ fn render_event(notice: session::Notice) -> Option<String> {
|
|
|
666
668
|
"session.input",
|
|
667
669
|
serde_json::json!({ "sessionId": session_id, "input": input }),
|
|
668
670
|
),
|
|
671
|
+
session::SessionEvent::Observed {
|
|
672
|
+
timestamp_us,
|
|
673
|
+
input,
|
|
674
|
+
} => (
|
|
675
|
+
"session.input.observed",
|
|
676
|
+
serde_json::json!({ "sessionId": session_id, "timestamp_us": timestamp_us, "input": input }),
|
|
677
|
+
),
|
|
669
678
|
session::SessionEvent::KeyframeRequest { generation } => (
|
|
670
679
|
"session.keyframeRequest",
|
|
671
680
|
serde_json::json!({ "sessionId": session_id, "generation": generation }),
|
|
@@ -752,6 +761,7 @@ async fn dispatch(
|
|
|
752
761
|
agent_indicator: params.agent_indicator,
|
|
753
762
|
local_frames: params.local_frames,
|
|
754
763
|
audio: params.audio,
|
|
764
|
+
observe_input: params.observe_input,
|
|
755
765
|
ice_servers: usable.0,
|
|
756
766
|
};
|
|
757
767
|
let session = session::Session::open(open, events.clone())
|
|
@@ -770,6 +780,7 @@ async fn dispatch(
|
|
|
770
780
|
"cursor": session.cursor_info(),
|
|
771
781
|
"agentIndicator": session.agent_indicator(),
|
|
772
782
|
"audio": session.audio_info(),
|
|
783
|
+
"inputObservation": session.observation_info(),
|
|
773
784
|
});
|
|
774
785
|
// URLs the WebRTC stack cannot dial were left out of the peer's
|
|
775
786
|
// configuration; each skipped one is named once instead of
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//! Passive observation of the desktop's own pointer buttons, cursor and keys.
|
|
2
|
+
//!
|
|
3
|
+
//! The engine only listens; events are the ones `docs/PROTOCOL.md` describes. A
|
|
4
|
+
//! platform that cannot observe says why with a typed reason and delivers nothing.
|
|
5
|
+
|
|
6
|
+
use crate::protocol::{CursorPosition, ObservedInput};
|
|
7
|
+
use std::sync::Arc;
|
|
8
|
+
|
|
9
|
+
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
|
10
|
+
pub enum Observation {
|
|
11
|
+
Input {
|
|
12
|
+
timestamp_us: u64,
|
|
13
|
+
input: ObservedInput,
|
|
14
|
+
},
|
|
15
|
+
Cursor(CursorPosition),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub type Sink = Arc<dyn Fn(Observation) + Send + Sync + 'static>;
|
|
19
|
+
|
|
20
|
+
/// Keeps an observer running; dropping it stops it.
|
|
21
|
+
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
|
22
|
+
pub struct Tap {
|
|
23
|
+
pub mechanism: &'static str,
|
|
24
|
+
pub guard: Box<dyn Send>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Why the desktop's input cannot be observed, and what the owner can do.
|
|
28
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
29
|
+
pub struct Unavailable {
|
|
30
|
+
pub code: &'static str,
|
|
31
|
+
pub reason: String,
|
|
32
|
+
pub remedy: String,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[cfg(target_os = "macos")]
|
|
36
|
+
pub fn start(display_id: u32, pixels: (usize, usize), sink: Sink) -> Result<Tap, Unavailable> {
|
|
37
|
+
crate::input::observe(display_id, pixels, sink)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[cfg(not(target_os = "macos"))]
|
|
41
|
+
pub fn start(_: u32, _: (usize, usize), _: Sink) -> Result<Tap, Unavailable> {
|
|
42
|
+
Err(Unavailable {
|
|
43
|
+
code: "unsupported-platform",
|
|
44
|
+
reason: String::from("observing the desktop's own input is only implemented on macOS"),
|
|
45
|
+
remedy: String::from("none: this platform delivers no input events"),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// The `inputObservation` member of the `session.open` reply.
|
|
50
|
+
pub fn status(result: Option<&Result<Tap, Unavailable>>) -> serde_json::Value {
|
|
51
|
+
match result {
|
|
52
|
+
None => serde_json::json!({"observing": false}),
|
|
53
|
+
Some(Ok(tap)) => serde_json::json!({"observing": true, "mechanism": tap.mechanism}),
|
|
54
|
+
Some(Err(error)) => serde_json::json!({
|
|
55
|
+
"observing": false,
|
|
56
|
+
"code": error.code,
|
|
57
|
+
"reason": error.reason,
|
|
58
|
+
"remedy": error.remedy,
|
|
59
|
+
}),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[cfg(test)]
|
|
64
|
+
mod tests {
|
|
65
|
+
use super::*;
|
|
66
|
+
|
|
67
|
+
#[test]
|
|
68
|
+
#[cfg(not(target_os = "macos"))]
|
|
69
|
+
fn a_platform_that_cannot_observe_says_so_and_delivers_nothing() {
|
|
70
|
+
let delivered = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
71
|
+
let counted = delivered.clone();
|
|
72
|
+
let sink: Sink = Arc::new(move |_| {
|
|
73
|
+
counted.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
74
|
+
});
|
|
75
|
+
let refused = start(0, (1920, 1080), sink);
|
|
76
|
+
let status = status(Some(&refused));
|
|
77
|
+
assert_eq!(status["observing"], false);
|
|
78
|
+
assert_eq!(status["code"], "unsupported-platform");
|
|
79
|
+
assert_eq!(delivered.load(std::sync::atomic::Ordering::SeqCst), 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn a_refusal_carries_its_code_reason_and_remedy() {
|
|
84
|
+
let refused: Result<Tap, Unavailable> = Err(Unavailable {
|
|
85
|
+
code: "missing-input-monitoring",
|
|
86
|
+
reason: String::from("reason"),
|
|
87
|
+
remedy: String::from("remedy"),
|
|
88
|
+
});
|
|
89
|
+
assert_eq!(
|
|
90
|
+
status(Some(&refused)),
|
|
91
|
+
serde_json::json!({"observing": false, "code": "missing-input-monitoring", "reason": "reason", "remedy": "remedy"})
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
package/engine/src/portal.rs
CHANGED
|
@@ -47,13 +47,20 @@ pub async fn open_display(_: Option<u32>) -> Result<PortalSession> {
|
|
|
47
47
|
anyhow::bail!("display selection is only supported on macOS")
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/// The portal service answered without a ScreenCast interface. That happens
|
|
51
|
+
/// when xdg-desktop-portal started before the compositor's portal and cached an
|
|
52
|
+
/// interface list without it; only a restart makes it look again.
|
|
53
|
+
const NO_SCREENCAST: &str = "compositor has no ScreenCast portal: xdg-desktop-portal does not export \
|
|
54
|
+
org.freedesktop.portal.ScreenCast. If it started before the compositor's portal, run \
|
|
55
|
+
`systemctl --user restart xdg-desktop-portal` once the compositor portal is running";
|
|
56
|
+
|
|
50
57
|
pub async fn open(
|
|
51
58
|
restore_token: Option<&str>,
|
|
52
59
|
cursor: crate::protocol::CursorMode,
|
|
53
60
|
) -> Result<PortalSession> {
|
|
54
61
|
let proxy = Screencast::new()
|
|
55
62
|
.await
|
|
56
|
-
.context(
|
|
63
|
+
.context(NO_SCREENCAST)?;
|
|
57
64
|
|
|
58
65
|
if proxy
|
|
59
66
|
.available_source_types()
|
|
@@ -142,3 +149,78 @@ fn choose_cursor_mode(available: BitFlags<CursorMode>) -> Result<CursorMode> {
|
|
|
142
149
|
.find(|mode| available.contains(*mode))
|
|
143
150
|
.context("cursor-unavailable: portal cannot leave the cursor out of capture")
|
|
144
151
|
}
|
|
152
|
+
|
|
153
|
+
#[cfg(all(test, target_os = "linux"))]
|
|
154
|
+
mod tests {
|
|
155
|
+
use super::*;
|
|
156
|
+
use std::process::{Child, Command, Stdio};
|
|
157
|
+
use zbus::{connection::Builder, message::Type, MessageStream};
|
|
158
|
+
|
|
159
|
+
struct Bus(Child);
|
|
160
|
+
|
|
161
|
+
impl Drop for Bus {
|
|
162
|
+
fn drop(&mut self) {
|
|
163
|
+
let _ = self.0.kill();
|
|
164
|
+
let _ = self.0.wait();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// A private session bus whose `org.freedesktop.portal.Desktop` owner
|
|
169
|
+
/// answers every property read the way a stale xdg-desktop-portal answers
|
|
170
|
+
/// for ScreenCast: no such interface.
|
|
171
|
+
#[tokio::test]
|
|
172
|
+
async fn open_names_the_fix_when_the_portal_exports_no_screencast() {
|
|
173
|
+
use zbus::export::futures_core::Stream;
|
|
174
|
+
|
|
175
|
+
let mut daemon = Command::new("dbus-daemon")
|
|
176
|
+
.args(["--session", "--nofork", "--print-address=1"])
|
|
177
|
+
.stdout(Stdio::piped())
|
|
178
|
+
.spawn()
|
|
179
|
+
.expect("dbus-daemon");
|
|
180
|
+
let mut address = String::new();
|
|
181
|
+
std::io::BufRead::read_line(
|
|
182
|
+
&mut std::io::BufReader::new(daemon.stdout.take().unwrap()),
|
|
183
|
+
&mut address,
|
|
184
|
+
)
|
|
185
|
+
.unwrap();
|
|
186
|
+
let _bus = Bus(daemon);
|
|
187
|
+
let address = address.trim().to_owned();
|
|
188
|
+
|
|
189
|
+
let fake = Builder::address(address.as_str())
|
|
190
|
+
.unwrap()
|
|
191
|
+
.name("org.freedesktop.portal.Desktop")
|
|
192
|
+
.unwrap()
|
|
193
|
+
.build()
|
|
194
|
+
.await
|
|
195
|
+
.unwrap();
|
|
196
|
+
let service = fake.clone();
|
|
197
|
+
let answering = tokio::spawn(async move {
|
|
198
|
+
let mut messages = MessageStream::from(&service);
|
|
199
|
+
while let Some(Ok(message)) =
|
|
200
|
+
std::future::poll_fn(|cx| std::pin::Pin::new(&mut messages).poll_next(cx)).await
|
|
201
|
+
{
|
|
202
|
+
if message.message_type() != Type::MethodCall {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
let _ = service
|
|
206
|
+
.reply_dbus_error(
|
|
207
|
+
&message.header(),
|
|
208
|
+
zbus::fdo::Error::InvalidArgs(
|
|
209
|
+
"No such interface \u{201c}org.freedesktop.portal.ScreenCast\u{201d}".into(),
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
.await;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
// Screencast::new reads the session bus from the environment.
|
|
216
|
+
std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &address);
|
|
217
|
+
|
|
218
|
+
let error = match open(None, crate::protocol::CursorMode::Embedded).await {
|
|
219
|
+
Ok(_) => panic!("a portal without ScreenCast must not open a session"),
|
|
220
|
+
Err(error) => format!("{error:#}"),
|
|
221
|
+
};
|
|
222
|
+
answering.abort();
|
|
223
|
+
assert!(error.starts_with("compositor has no ScreenCast portal"), "{error}");
|
|
224
|
+
assert!(error.contains("systemctl --user restart xdg-desktop-portal"), "{error}");
|
|
225
|
+
}
|
|
226
|
+
}
|
package/engine/src/protocol.rs
CHANGED
|
@@ -149,6 +149,11 @@ pub struct OpenParams {
|
|
|
149
149
|
/// cannot be answered is refused.
|
|
150
150
|
#[serde(default)]
|
|
151
151
|
pub audio: bool,
|
|
152
|
+
/// Observe this computer's own pointer buttons, cursor and keys and raise
|
|
153
|
+
/// them as `session.input.observed` and `session.cursor`. Off unless asked:
|
|
154
|
+
/// it reports every key typed on the desktop, in every application.
|
|
155
|
+
#[serde(default)]
|
|
156
|
+
pub observe_input: bool,
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
fn default_local_frames() -> bool {
|
|
@@ -379,7 +384,7 @@ impl ControlMessage {
|
|
|
379
384
|
}
|
|
380
385
|
}
|
|
381
386
|
|
|
382
|
-
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
387
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
383
388
|
#[serde(rename_all = "snake_case")]
|
|
384
389
|
pub enum PointerPhase {
|
|
385
390
|
Move,
|
|
@@ -388,6 +393,30 @@ pub enum PointerPhase {
|
|
|
388
393
|
Cancel,
|
|
389
394
|
}
|
|
390
395
|
|
|
396
|
+
/// A pointer button or key the person pressed on the observed desktop. The
|
|
397
|
+
/// vocabulary is the control channel's own (`pointer` and `key`), so a consumer
|
|
398
|
+
/// reads what it sent and what it saw the same way. `x`/`y` are in the source's
|
|
399
|
+
/// own pixels (`geometry.source`); `modifiers` is the modifier state after the
|
|
400
|
+
/// event.
|
|
401
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
402
|
+
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
403
|
+
pub enum ObservedInput {
|
|
404
|
+
Pointer {
|
|
405
|
+
phase: PointerPhase,
|
|
406
|
+
x: i64,
|
|
407
|
+
y: i64,
|
|
408
|
+
button: i64,
|
|
409
|
+
},
|
|
410
|
+
Key {
|
|
411
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
412
|
+
name: Option<String>,
|
|
413
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
414
|
+
character: Option<String>,
|
|
415
|
+
down: bool,
|
|
416
|
+
modifiers: Vec<String>,
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
|
|
391
420
|
/// Messages the engine sends back on the control channel.
|
|
392
421
|
#[derive(Debug, Serialize)]
|
|
393
422
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
package/engine/src/session.rs
CHANGED
|
@@ -714,6 +714,8 @@ pub struct OpenRequest {
|
|
|
714
714
|
pub local_frames: bool,
|
|
715
715
|
/// Capture the desktop's own system audio for `session.audio`.
|
|
716
716
|
pub audio: bool,
|
|
717
|
+
/// Listen to the desktop's own pointer buttons, cursor and keys.
|
|
718
|
+
pub observe_input: bool,
|
|
717
719
|
}
|
|
718
720
|
|
|
719
721
|
/// Events a session raises for its consumer.
|
|
@@ -755,6 +757,11 @@ pub enum SessionEvent {
|
|
|
755
757
|
Input {
|
|
756
758
|
input: serde_json::Value,
|
|
757
759
|
},
|
|
760
|
+
/// A pointer button or key the person pressed, observed rather than applied.
|
|
761
|
+
Observed {
|
|
762
|
+
timestamp_us: u64,
|
|
763
|
+
input: crate::protocol::ObservedInput,
|
|
764
|
+
},
|
|
758
765
|
/// The far end needs a fresh reference frame and only the consumer can make
|
|
759
766
|
/// one: the encoder for this track is on its side. Raised by an RTCP PLI or
|
|
760
767
|
/// FIR, and once when the transport connects, because everything sent before
|
|
@@ -854,6 +861,9 @@ struct Inner {
|
|
|
854
861
|
#[cfg(target_os = "linux")]
|
|
855
862
|
audio: Mutex<Option<crate::audio::AudioCapture>>,
|
|
856
863
|
cursor_info: serde_json::Value,
|
|
864
|
+
/// `session.open`'s `inputObservation` member, fixed when the session opened.
|
|
865
|
+
observation: serde_json::Value,
|
|
866
|
+
observer: Mutex<Option<crate::observe::Tap>>,
|
|
857
867
|
}
|
|
858
868
|
|
|
859
869
|
/// A source whose video arrives already encoded.
|
|
@@ -978,6 +988,7 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
978
988
|
}),
|
|
979
989
|
),
|
|
980
990
|
};
|
|
991
|
+
let observe = crate::input::observe_probe();
|
|
981
992
|
serde_json::json!({
|
|
982
993
|
"protocol": crate::protocol::PROTOCOL_VERSION,
|
|
983
994
|
"engine": format!("desklink-host/{}", env!("CARGO_PKG_VERSION")),
|
|
@@ -1009,6 +1020,11 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
1009
1020
|
"layout": keymap::LayoutNames::from_environment().identity(),
|
|
1010
1021
|
"unavailable_reason": unavailable,
|
|
1011
1022
|
"grant": input_grant,
|
|
1023
|
+
"observe": {
|
|
1024
|
+
"mechanism": "cgeventtap",
|
|
1025
|
+
"grant": if observe.is_ok() { "granted" } else { "missing-input-monitoring" },
|
|
1026
|
+
"unavailable_reason": observe.as_ref().err().map(|e| serde_json::json!({"reason": e.reason, "remedy": e.remedy})),
|
|
1027
|
+
},
|
|
1012
1028
|
},
|
|
1013
1029
|
"clipboard": { "read": true, "write": true, "mime": ["text/plain;charset=utf-8"],
|
|
1014
1030
|
"maxBytes": clipboard::MAX_CLIPBOARD_BYTES },
|
|
@@ -1480,6 +1496,29 @@ impl Session {
|
|
|
1480
1496
|
"origin": { "x": source.origin_x, "y": source.origin_y },
|
|
1481
1497
|
});
|
|
1482
1498
|
|
|
1499
|
+
// Observation is an addition to the stream, never a condition of it: a
|
|
1500
|
+
// desktop that cannot be observed still streams, and the reply says why.
|
|
1501
|
+
let observed = request.observe_input.then(|| {
|
|
1502
|
+
let sink: crate::observe::Sink = {
|
|
1503
|
+
let events = events.clone();
|
|
1504
|
+
let id = id.clone();
|
|
1505
|
+
Arc::new(move |observation| {
|
|
1506
|
+
let event = match observation {
|
|
1507
|
+
crate::observe::Observation::Input { timestamp_us, input } => {
|
|
1508
|
+
SessionEvent::Observed { timestamp_us, input }
|
|
1509
|
+
}
|
|
1510
|
+
crate::observe::Observation::Cursor(position) => SessionEvent::Cursor { position },
|
|
1511
|
+
};
|
|
1512
|
+
let _ = events.send(Notice { session_id: id.clone(), event });
|
|
1513
|
+
})
|
|
1514
|
+
};
|
|
1515
|
+
crate::observe::start(source.node_id, (source_w, source_h), sink)
|
|
1516
|
+
});
|
|
1517
|
+
if let Some(Err(error)) = &observed {
|
|
1518
|
+
log::warn!("input observation unavailable: {} ({})", error.reason, error.code);
|
|
1519
|
+
}
|
|
1520
|
+
let observation = crate::observe::status(observed.as_ref());
|
|
1521
|
+
|
|
1483
1522
|
let pipeline = Arc::new(AtomicBool::new(true));
|
|
1484
1523
|
let inner = Arc::new(Inner {
|
|
1485
1524
|
id,
|
|
@@ -1526,6 +1565,8 @@ impl Session {
|
|
|
1526
1565
|
encoded: Mutex::new(None),
|
|
1527
1566
|
local_frames: request.local_frames,
|
|
1528
1567
|
cursor_info,
|
|
1568
|
+
observation,
|
|
1569
|
+
observer: Mutex::new(observed.and_then(Result::ok)),
|
|
1529
1570
|
});
|
|
1530
1571
|
|
|
1531
1572
|
inner.notify(SessionEvent::Description {
|
|
@@ -1641,6 +1682,13 @@ impl Session {
|
|
|
1641
1682
|
})),
|
|
1642
1683
|
local_frames: false,
|
|
1643
1684
|
cursor_info: serde_json::json!({"mode":"unavailable", "positions":false}),
|
|
1685
|
+
observation: serde_json::json!({
|
|
1686
|
+
"observing": false,
|
|
1687
|
+
"code": "unsupported-source",
|
|
1688
|
+
"reason": "consumer-encoded video has no desktop of its own to observe",
|
|
1689
|
+
"remedy": "observe the device where its pixels come from",
|
|
1690
|
+
}),
|
|
1691
|
+
observer: Mutex::new(None),
|
|
1644
1692
|
});
|
|
1645
1693
|
inner.notify(SessionEvent::State {
|
|
1646
1694
|
capture: "consented",
|
|
@@ -1706,6 +1754,10 @@ impl Session {
|
|
|
1706
1754
|
&self.inner.cursor_info
|
|
1707
1755
|
}
|
|
1708
1756
|
|
|
1757
|
+
pub fn observation_info(&self) -> &serde_json::Value {
|
|
1758
|
+
&self.inner.observation
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1709
1761
|
pub fn geometry(&self) -> &serde_json::Value {
|
|
1710
1762
|
&self.inner.geometry
|
|
1711
1763
|
}
|
|
@@ -2176,6 +2228,8 @@ impl Inner {
|
|
|
2176
2228
|
// handlers, and `shutdown` exits right after this. The encode thread
|
|
2177
2229
|
// finds it gone and stops.
|
|
2178
2230
|
drop(lock(&self.encoder).take());
|
|
2231
|
+
// Stop listening to the person's keys the moment the session ends.
|
|
2232
|
+
drop(lock(&self.observer).take());
|
|
2179
2233
|
if let Ok(mut indicator) = self.indicator.lock() {
|
|
2180
2234
|
indicator.take();
|
|
2181
2235
|
}
|
|
@@ -3829,6 +3883,8 @@ mod tests {
|
|
|
3829
3883
|
})),
|
|
3830
3884
|
local_frames: false,
|
|
3831
3885
|
cursor_info: serde_json::json!({"mode":"unavailable", "positions":false}),
|
|
3886
|
+
observation: serde_json::json!({"observing": false}),
|
|
3887
|
+
observer: Mutex::new(None),
|
|
3832
3888
|
});
|
|
3833
3889
|
spawn_peer_events(&inner, peer_events_rx);
|
|
3834
3890
|
spawn_encoded(&inner, feed_rx);
|
|
@@ -4084,6 +4140,8 @@ mod tests {
|
|
|
4084
4140
|
encoded: Mutex::new(None),
|
|
4085
4141
|
local_frames: true,
|
|
4086
4142
|
cursor_info: serde_json::json!({"mode":"embedded", "positions":false}),
|
|
4143
|
+
observation: serde_json::json!({"observing": false}),
|
|
4144
|
+
observer: Mutex::new(None),
|
|
4087
4145
|
});
|
|
4088
4146
|
(inner, recorded)
|
|
4089
4147
|
}
|
|
@@ -4484,6 +4542,7 @@ mod tests {
|
|
|
4484
4542
|
agent_indicator: false,
|
|
4485
4543
|
local_frames: true,
|
|
4486
4544
|
audio: false,
|
|
4545
|
+
observe_input: false,
|
|
4487
4546
|
}
|
|
4488
4547
|
}
|
|
4489
4548
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desklink/host",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"ws": "8.21.3"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
|
-
"@desklink/host-linux-x64-gnu": "0.5.
|
|
42
|
-
"@desklink/host-darwin-arm64": "0.5.
|
|
41
|
+
"@desklink/host-linux-x64-gnu": "0.5.10",
|
|
42
|
+
"@desklink/host-darwin-arm64": "0.5.10"
|
|
43
43
|
}
|
|
44
44
|
}
|