@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/session.rs
CHANGED
|
@@ -202,12 +202,14 @@ struct Selected {
|
|
|
202
202
|
|
|
203
203
|
/// Open an X display, start reading its root window, and hand back the same
|
|
204
204
|
/// shape the portal path produces.
|
|
205
|
+
#[allow(clippy::too_many_arguments)]
|
|
205
206
|
fn select_x11(
|
|
206
207
|
display: Option<&str>,
|
|
207
208
|
max_width: usize,
|
|
208
209
|
max_height: usize,
|
|
209
210
|
max_fps: u32,
|
|
210
211
|
metrics: Arc<Mutex<Metrics>>,
|
|
212
|
+
#[cfg(target_os = "linux")] cursor_sink: Option<crate::cursor::CursorSink>,
|
|
211
213
|
sink: capture::FrameSink,
|
|
212
214
|
on_stop: Box<dyn Fn(String) + Send>,
|
|
213
215
|
) -> Result<Selected> {
|
|
@@ -238,15 +240,23 @@ fn select_x11(
|
|
|
238
240
|
let interval = frame_interval(max_fps);
|
|
239
241
|
let mut sequence = 0u64;
|
|
240
242
|
let mut last_sent: Option<Instant> = None;
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
+
// XDamage wakes the loop, but pixels decide whether the root
|
|
244
|
+
// image actually changed; without XDamage, polling does so.
|
|
243
245
|
let hashed = !lock(&desktop).damage_armed();
|
|
244
246
|
let mut last_hash = None;
|
|
245
247
|
// The first frame is always grabbed; afterwards Damage wakes
|
|
246
248
|
// the loop, so a still screen costs no grab, no conversion
|
|
247
249
|
// and no hash. The fps cap stays a minimum spacing.
|
|
248
250
|
let mut first = true;
|
|
251
|
+
#[cfg(target_os = "linux")]
|
|
252
|
+
let mut cursor = cursor_sink.map(crate::cursor::Reporter::new);
|
|
249
253
|
while !stop.load(Ordering::SeqCst) {
|
|
254
|
+
#[cfg(target_os = "linux")]
|
|
255
|
+
if let Some(cursor) = cursor.as_mut() {
|
|
256
|
+
if let Ok((x, y, visible)) = lock(&desktop).cursor_position() {
|
|
257
|
+
cursor.update(x, y, visible);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
250
260
|
if !first {
|
|
251
261
|
let changed = lock(&desktop).take_damage();
|
|
252
262
|
if !changed {
|
|
@@ -269,7 +279,7 @@ fn select_x11(
|
|
|
269
279
|
let grab_micros = grabbed.elapsed().as_micros() as u64;
|
|
270
280
|
match frame {
|
|
271
281
|
Ok((frame, raw)) => {
|
|
272
|
-
let changed =
|
|
282
|
+
let changed = {
|
|
273
283
|
use std::hash::{Hash, Hasher};
|
|
274
284
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
|
275
285
|
raw.hash(&mut hasher);
|
|
@@ -280,8 +290,6 @@ fn select_x11(
|
|
|
280
290
|
last_hash = Some(hash);
|
|
281
291
|
true
|
|
282
292
|
}
|
|
283
|
-
} else {
|
|
284
|
-
true
|
|
285
293
|
};
|
|
286
294
|
if changed {
|
|
287
295
|
// Handing an unchanged frame on would keep the
|
|
@@ -480,10 +488,10 @@ impl InputTarget {
|
|
|
480
488
|
Ok(())
|
|
481
489
|
}
|
|
482
490
|
|
|
483
|
-
#[cfg(target_os = "macos")]
|
|
491
|
+
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
484
492
|
fn unicode_text(&mut self, text: &str) -> Result<()> {
|
|
485
493
|
match &mut self.applier {
|
|
486
|
-
#[cfg(target_os = "macos")]
|
|
494
|
+
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
487
495
|
Applier::Uinput(devices) => devices.unicode_text(text),
|
|
488
496
|
#[cfg(target_os = "linux")]
|
|
489
497
|
Applier::Uinput(_) => anyhow::bail!("Unicode text input is unavailable on Linux"),
|
|
@@ -582,6 +590,7 @@ impl SessionError {
|
|
|
582
590
|
}
|
|
583
591
|
|
|
584
592
|
pub struct OpenRequest {
|
|
593
|
+
pub cursor: crate::protocol::CursorMode,
|
|
585
594
|
/** Which desktop to capture; absent means the portal. */
|
|
586
595
|
pub source: Option<SourceRequest>,
|
|
587
596
|
pub permissions: Vec<Permission>,
|
|
@@ -603,6 +612,9 @@ pub struct OpenRequest {
|
|
|
603
612
|
/// Events a session raises for its consumer.
|
|
604
613
|
#[derive(Debug)]
|
|
605
614
|
pub enum SessionEvent {
|
|
615
|
+
Cursor {
|
|
616
|
+
position: crate::protocol::CursorPosition,
|
|
617
|
+
},
|
|
606
618
|
Description {
|
|
607
619
|
generation: u64,
|
|
608
620
|
sdp: String,
|
|
@@ -619,6 +631,7 @@ pub enum SessionEvent {
|
|
|
619
631
|
first_frame: bool,
|
|
620
632
|
},
|
|
621
633
|
Frame {
|
|
634
|
+
timestamp_us: u64,
|
|
622
635
|
seq: u64,
|
|
623
636
|
damage: Vec<[usize; 4]>,
|
|
624
637
|
},
|
|
@@ -661,6 +674,13 @@ type FrameSnapshot = (u64, Instant, usize, usize, Vec<u8>, Vec<u64>, Vec<u64>);
|
|
|
661
674
|
|
|
662
675
|
/// Everything a session's background tasks need, shared rather than borrowed so
|
|
663
676
|
/// the consumer can own the `Session` handle while the pipeline runs.
|
|
677
|
+
#[cfg(target_os = "linux")]
|
|
678
|
+
enum PointDesktop {
|
|
679
|
+
X11(String),
|
|
680
|
+
Wayland,
|
|
681
|
+
Unavailable(&'static str),
|
|
682
|
+
}
|
|
683
|
+
|
|
664
684
|
struct Inner {
|
|
665
685
|
id: String,
|
|
666
686
|
generation: u64,
|
|
@@ -679,6 +699,9 @@ struct Inner {
|
|
|
679
699
|
encoder: Mutex<Option<VideoEncoder>>,
|
|
680
700
|
input: Mutex<Option<InputTarget>>,
|
|
681
701
|
indicator: Mutex<Option<crate::indicator::Indicator>>,
|
|
702
|
+
point_overlay: Mutex<Option<crate::indicator::Indicator>>,
|
|
703
|
+
#[cfg(target_os = "linux")]
|
|
704
|
+
point_desktop: PointDesktop,
|
|
682
705
|
capture: Mutex<Option<FrameSource>>,
|
|
683
706
|
/// Absent for an encoded source: there is no local keyboard to translate a
|
|
684
707
|
/// character through, because the client's keys are forwarded instead.
|
|
@@ -694,6 +717,7 @@ struct Inner {
|
|
|
694
717
|
encoded: Mutex<Option<Encoded>>,
|
|
695
718
|
/// `OpenRequest::local_frames`: whether `latest` is kept at all.
|
|
696
719
|
local_frames: bool,
|
|
720
|
+
cursor_info: serde_json::Value,
|
|
697
721
|
}
|
|
698
722
|
|
|
699
723
|
/// A source whose video arrives already encoded.
|
|
@@ -772,6 +796,8 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
772
796
|
"backends": ["portal-screencast+pipewire", "x11-root"],
|
|
773
797
|
"formats": ["bgrx", "bgra", "rgbx", "rgba"],
|
|
774
798
|
"cursor": "embedded",
|
|
799
|
+
"cursor_modes": ["embedded", "hidden"],
|
|
800
|
+
"cursor_positions": {"x11": true, "portal": "negotiated"},
|
|
775
801
|
"audio": false,
|
|
776
802
|
},
|
|
777
803
|
"encode": encode_capabilities(),
|
|
@@ -824,6 +850,7 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
824
850
|
"backends": ["screencapturekit"],
|
|
825
851
|
"formats": ["bgra"],
|
|
826
852
|
"cursor": "embedded",
|
|
853
|
+
"cursor_modes": ["embedded"],
|
|
827
854
|
"audio": false,
|
|
828
855
|
"displays": crate::mac::displays(),
|
|
829
856
|
"grant": if capture_granted { "granted" } else { "missing-screen-recording" },
|
|
@@ -849,6 +876,16 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
849
876
|
})
|
|
850
877
|
}
|
|
851
878
|
|
|
879
|
+
#[cfg(target_os = "windows")]
|
|
880
|
+
pub fn capabilities() -> serde_json::Value {
|
|
881
|
+
crate::win::capabilities()
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
#[cfg(target_os = "windows")]
|
|
885
|
+
fn wayland_clipboard_available() -> bool {
|
|
886
|
+
false
|
|
887
|
+
}
|
|
888
|
+
|
|
852
889
|
/// `capabilities.encode`: VP9 always, H.264 when an encoder for it starts
|
|
853
890
|
/// here now. `hardware` describes VP9, the default.
|
|
854
891
|
fn encode_capabilities() -> serde_json::Value {
|
|
@@ -878,6 +915,26 @@ impl Session {
|
|
|
878
915
|
"session.open requires the view permission",
|
|
879
916
|
));
|
|
880
917
|
}
|
|
918
|
+
#[cfg(target_os = "windows")]
|
|
919
|
+
if request.agent_indicator {
|
|
920
|
+
return Err(SessionError::new(
|
|
921
|
+
"indicator-unavailable",
|
|
922
|
+
"Windows agent indicator is not implemented in this build.",
|
|
923
|
+
));
|
|
924
|
+
}
|
|
925
|
+
if request.cursor == crate::protocol::CursorMode::Hidden {
|
|
926
|
+
#[cfg(not(target_os = "linux"))]
|
|
927
|
+
return Err(SessionError::new(
|
|
928
|
+
"cursor-unavailable",
|
|
929
|
+
"hidden cursor capture is not implemented on this platform",
|
|
930
|
+
));
|
|
931
|
+
if matches!(request.source, Some(SourceRequest::Encoded { .. })) {
|
|
932
|
+
return Err(SessionError::new(
|
|
933
|
+
"cursor-unavailable",
|
|
934
|
+
"the engine cannot remove cursor pixels from consumer-encoded video",
|
|
935
|
+
));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
881
938
|
let id = opaque_id();
|
|
882
939
|
let wants_control = request.permissions.contains(&Permission::Control);
|
|
883
940
|
let wants_x11 = matches!(request.source, Some(SourceRequest::X11 { .. }));
|
|
@@ -975,7 +1032,7 @@ impl Session {
|
|
|
975
1032
|
let damage = dirty_regions(&hashes, &previous, frame.width, frame.height);
|
|
976
1033
|
*held = Some((
|
|
977
1034
|
seq,
|
|
978
|
-
|
|
1035
|
+
captured_at,
|
|
979
1036
|
frame.width,
|
|
980
1037
|
frame.height,
|
|
981
1038
|
raw,
|
|
@@ -985,7 +1042,11 @@ impl Session {
|
|
|
985
1042
|
frame_tx_signal.send_replace(seq);
|
|
986
1043
|
let _ = frame_events.send(Notice {
|
|
987
1044
|
session_id: frame_session_id.clone(),
|
|
988
|
-
event: SessionEvent::Frame {
|
|
1045
|
+
event: SessionEvent::Frame {
|
|
1046
|
+
seq,
|
|
1047
|
+
damage,
|
|
1048
|
+
timestamp_us: crate::protocol::timestamp_us(captured_at),
|
|
1049
|
+
},
|
|
989
1050
|
});
|
|
990
1051
|
}
|
|
991
1052
|
}
|
|
@@ -1002,6 +1063,21 @@ impl Session {
|
|
|
1002
1063
|
}
|
|
1003
1064
|
});
|
|
1004
1065
|
|
|
1066
|
+
#[cfg(target_os = "linux")]
|
|
1067
|
+
let cursor_sink: Option<crate::cursor::CursorSink> =
|
|
1068
|
+
if request.cursor == crate::protocol::CursorMode::Hidden {
|
|
1069
|
+
let events = events.clone();
|
|
1070
|
+
let id = id.clone();
|
|
1071
|
+
Some(Box::new(move |position| {
|
|
1072
|
+
let _ = events.send(Notice {
|
|
1073
|
+
session_id: id.clone(),
|
|
1074
|
+
event: SessionEvent::Cursor { position },
|
|
1075
|
+
});
|
|
1076
|
+
}))
|
|
1077
|
+
} else {
|
|
1078
|
+
None
|
|
1079
|
+
};
|
|
1080
|
+
let mut cursor_info = serde_json::json!({"mode":"embedded", "positions":false});
|
|
1005
1081
|
let Selected {
|
|
1006
1082
|
source,
|
|
1007
1083
|
capture,
|
|
@@ -1013,6 +1089,8 @@ impl Session {
|
|
|
1013
1089
|
request.max_height,
|
|
1014
1090
|
max_fps,
|
|
1015
1091
|
metrics.clone(),
|
|
1092
|
+
#[cfg(target_os = "linux")]
|
|
1093
|
+
cursor_sink,
|
|
1016
1094
|
sink,
|
|
1017
1095
|
Box::new({
|
|
1018
1096
|
let status = capture_status.clone();
|
|
@@ -1025,9 +1103,30 @@ impl Session {
|
|
|
1025
1103
|
Some(SourceRequest::Display { display_id }) => {
|
|
1026
1104
|
portal::open_display(*display_id).await
|
|
1027
1105
|
}
|
|
1028
|
-
_ =>
|
|
1106
|
+
_ => {
|
|
1107
|
+
#[cfg(target_os = "linux")]
|
|
1108
|
+
{
|
|
1109
|
+
portal::open(request.restore_token.as_deref(), request.cursor).await
|
|
1110
|
+
}
|
|
1111
|
+
#[cfg(not(target_os = "linux"))]
|
|
1112
|
+
{
|
|
1113
|
+
portal::open(request.restore_token.as_deref()).await
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
.map_err(|error| {
|
|
1118
|
+
let message = format!("{error:#}");
|
|
1119
|
+
let code = if message.contains("cursor_positions_unavailable:") {
|
|
1120
|
+
"cursor_positions_unavailable"
|
|
1121
|
+
} else {
|
|
1122
|
+
"source"
|
|
1123
|
+
};
|
|
1124
|
+
SessionError::new(code, message)
|
|
1125
|
+
})?;
|
|
1126
|
+
#[cfg(target_os = "linux")]
|
|
1127
|
+
{
|
|
1128
|
+
cursor_info = serde_json::json!({"mode":portal.cursor_mode, "positions":false});
|
|
1029
1129
|
}
|
|
1030
|
-
.map_err(|error| SessionError::new("source", format!("{error:#}")))?;
|
|
1031
1130
|
if let Some(token) = &portal.restore_token {
|
|
1032
1131
|
let _ = events.send(Notice {
|
|
1033
1132
|
session_id: id.clone(),
|
|
@@ -1072,12 +1171,26 @@ impl Session {
|
|
|
1072
1171
|
}),
|
|
1073
1172
|
)
|
|
1074
1173
|
.map_err(|error| SessionError::new("source", format!("{error:#}")))?;
|
|
1174
|
+
#[cfg(target_os = "windows")]
|
|
1175
|
+
let capture = capture::start(
|
|
1176
|
+
portal,
|
|
1177
|
+
width,
|
|
1178
|
+
height,
|
|
1179
|
+
max_fps,
|
|
1180
|
+
sink,
|
|
1181
|
+
Box::new({
|
|
1182
|
+
let status = capture_status.clone();
|
|
1183
|
+
move |running, reason| status(running, reason)
|
|
1184
|
+
}),
|
|
1185
|
+
)
|
|
1186
|
+
.map_err(|error| SessionError::new("source", format!("{error:#}")))?;
|
|
1075
1187
|
#[cfg(target_os = "linux")]
|
|
1076
1188
|
let capture = capture::start(
|
|
1077
1189
|
portal,
|
|
1078
1190
|
width,
|
|
1079
1191
|
height,
|
|
1080
1192
|
max_fps,
|
|
1193
|
+
cursor_sink,
|
|
1081
1194
|
sink,
|
|
1082
1195
|
indicator
|
|
1083
1196
|
.as_ref()
|
|
@@ -1089,6 +1202,18 @@ impl Session {
|
|
|
1089
1202
|
)
|
|
1090
1203
|
.map_err(|error| SessionError::new("source", format!("{error:#}")))?;
|
|
1091
1204
|
#[cfg(target_os = "linux")]
|
|
1205
|
+
{
|
|
1206
|
+
if request.cursor == crate::protocol::CursorMode::Hidden
|
|
1207
|
+
&& !capture.cursor_positions()
|
|
1208
|
+
{
|
|
1209
|
+
return Err(SessionError::new(
|
|
1210
|
+
"cursor_positions_unavailable",
|
|
1211
|
+
"portal stream did not provide SPA cursor metadata",
|
|
1212
|
+
));
|
|
1213
|
+
}
|
|
1214
|
+
cursor_info["positions"] = serde_json::json!(capture.cursor_positions());
|
|
1215
|
+
}
|
|
1216
|
+
#[cfg(target_os = "linux")]
|
|
1092
1217
|
if let Some(indicator) = indicator.as_mut() {
|
|
1093
1218
|
indicator.event('A', -1, -1);
|
|
1094
1219
|
}
|
|
@@ -1099,6 +1224,9 @@ impl Session {
|
|
|
1099
1224
|
}
|
|
1100
1225
|
}
|
|
1101
1226
|
};
|
|
1227
|
+
if wants_x11 {
|
|
1228
|
+
cursor_info = serde_json::json!({"mode":"hidden", "positions": request.cursor == crate::protocol::CursorMode::Hidden});
|
|
1229
|
+
}
|
|
1102
1230
|
|
|
1103
1231
|
let source_w = source.width.max(1) as usize;
|
|
1104
1232
|
let source_h = source.height.max(1) as usize;
|
|
@@ -1150,7 +1278,7 @@ impl Session {
|
|
|
1150
1278
|
applier: Applier::Uinput({
|
|
1151
1279
|
#[cfg(target_os = "linux")]
|
|
1152
1280
|
let devices = InputDevices::create(source_w as i32, source_h as i32);
|
|
1153
|
-
#[cfg(target_os = "macos")]
|
|
1281
|
+
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
1154
1282
|
let devices = InputDevices::create_for_display(
|
|
1155
1283
|
source_w as i32,
|
|
1156
1284
|
source_h as i32,
|
|
@@ -1221,6 +1349,20 @@ impl Session {
|
|
|
1221
1349
|
encoder: Mutex::new(Some(VideoEncoder::Vp9(encoder))),
|
|
1222
1350
|
input: Mutex::new(input),
|
|
1223
1351
|
indicator: Mutex::new(indicator),
|
|
1352
|
+
point_overlay: Mutex::new(None),
|
|
1353
|
+
#[cfg(target_os = "linux")]
|
|
1354
|
+
point_desktop: if wants_x11 {
|
|
1355
|
+
match &request.source {
|
|
1356
|
+
Some(SourceRequest::X11 { display }) => display
|
|
1357
|
+
.clone()
|
|
1358
|
+
.or_else(|| std::env::var("DISPLAY").ok())
|
|
1359
|
+
.map(PointDesktop::X11)
|
|
1360
|
+
.unwrap_or(PointDesktop::Unavailable("x11-unavailable")),
|
|
1361
|
+
_ => PointDesktop::Unavailable("x11-unavailable"),
|
|
1362
|
+
}
|
|
1363
|
+
} else {
|
|
1364
|
+
PointDesktop::Wayland
|
|
1365
|
+
},
|
|
1224
1366
|
capture: Mutex::new(Some(capture)),
|
|
1225
1367
|
layout: Mutex::new(Some(layout)),
|
|
1226
1368
|
last_seq: Mutex::new(0),
|
|
@@ -1230,6 +1372,7 @@ impl Session {
|
|
|
1230
1372
|
events: events.clone(),
|
|
1231
1373
|
encoded: Mutex::new(None),
|
|
1232
1374
|
local_frames: request.local_frames,
|
|
1375
|
+
cursor_info,
|
|
1233
1376
|
});
|
|
1234
1377
|
|
|
1235
1378
|
inner.notify(SessionEvent::Description {
|
|
@@ -1308,6 +1451,9 @@ impl Session {
|
|
|
1308
1451
|
encoder: Mutex::new(None),
|
|
1309
1452
|
input: Mutex::new(None),
|
|
1310
1453
|
indicator: Mutex::new(None),
|
|
1454
|
+
point_overlay: Mutex::new(None),
|
|
1455
|
+
#[cfg(target_os = "linux")]
|
|
1456
|
+
point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
|
|
1311
1457
|
capture: Mutex::new(None),
|
|
1312
1458
|
layout: Mutex::new(None),
|
|
1313
1459
|
last_seq: Mutex::new(0),
|
|
@@ -1328,6 +1474,7 @@ impl Session {
|
|
|
1328
1474
|
feed,
|
|
1329
1475
|
})),
|
|
1330
1476
|
local_frames: false,
|
|
1477
|
+
cursor_info: serde_json::json!({"mode":"unavailable", "positions":false}),
|
|
1331
1478
|
});
|
|
1332
1479
|
inner.notify(SessionEvent::State {
|
|
1333
1480
|
capture: "consented",
|
|
@@ -1385,6 +1532,10 @@ impl Session {
|
|
|
1385
1532
|
self.inner.generation
|
|
1386
1533
|
}
|
|
1387
1534
|
|
|
1535
|
+
pub fn cursor_info(&self) -> &serde_json::Value {
|
|
1536
|
+
&self.inner.cursor_info
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1388
1539
|
pub fn geometry(&self) -> &serde_json::Value {
|
|
1389
1540
|
&self.inner.geometry
|
|
1390
1541
|
}
|
|
@@ -1393,6 +1544,128 @@ impl Session {
|
|
|
1393
1544
|
&self.inner.source
|
|
1394
1545
|
}
|
|
1395
1546
|
|
|
1547
|
+
pub fn point(
|
|
1548
|
+
&self,
|
|
1549
|
+
params: &crate::protocol::PointParams,
|
|
1550
|
+
) -> std::result::Result<(), SessionError> {
|
|
1551
|
+
// Lock through validation and drawing, so a concurrent close cannot leave
|
|
1552
|
+
// a newly spawned marker alive after teardown.
|
|
1553
|
+
let mut overlay = lock(&self.inner.point_overlay);
|
|
1554
|
+
if self.inner.closed.load(Ordering::SeqCst) {
|
|
1555
|
+
return Err(SessionError::new("session", "the session is closed"));
|
|
1556
|
+
}
|
|
1557
|
+
if !self.inner.permissions.contains(&Permission::View) {
|
|
1558
|
+
return Err(SessionError::new(
|
|
1559
|
+
"permission",
|
|
1560
|
+
"point requires view permission",
|
|
1561
|
+
));
|
|
1562
|
+
}
|
|
1563
|
+
if lock(&self.inner.encoded).is_some() {
|
|
1564
|
+
return Err(SessionError::new(
|
|
1565
|
+
"operation",
|
|
1566
|
+
"an encoded source has no host desktop",
|
|
1567
|
+
));
|
|
1568
|
+
}
|
|
1569
|
+
#[cfg(target_os = "linux")]
|
|
1570
|
+
if let PointDesktop::Unavailable(reason) = &self.inner.point_desktop {
|
|
1571
|
+
return Err(SessionError::new(
|
|
1572
|
+
"point_unsupported",
|
|
1573
|
+
format!("reason: {reason}; point overlay is unavailable"),
|
|
1574
|
+
));
|
|
1575
|
+
}
|
|
1576
|
+
if params.clear {
|
|
1577
|
+
if params.x.is_some()
|
|
1578
|
+
|| params.y.is_some()
|
|
1579
|
+
|| params.timeout_ms.is_some()
|
|
1580
|
+
|| !params.label.is_empty()
|
|
1581
|
+
{
|
|
1582
|
+
return Err(SessionError::new(
|
|
1583
|
+
"malformed",
|
|
1584
|
+
"clear accepts no coordinates, label or timeout",
|
|
1585
|
+
));
|
|
1586
|
+
}
|
|
1587
|
+
#[cfg(target_os = "linux")]
|
|
1588
|
+
if overlay.is_none() && matches!(self.inner.point_desktop, PointDesktop::Wayland) {
|
|
1589
|
+
let available = crate::indicator::Indicator::wayland_layer_shell_available()
|
|
1590
|
+
.map_err(|error| {
|
|
1591
|
+
SessionError::new("indicator-unavailable", format!("{error:#}"))
|
|
1592
|
+
})?;
|
|
1593
|
+
if !available {
|
|
1594
|
+
return Err(SessionError::new(
|
|
1595
|
+
"point_unsupported",
|
|
1596
|
+
"reason: layer_shell_unavailable; compositor has no layer-shell",
|
|
1597
|
+
));
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if let Some(helper) = overlay.take() {
|
|
1601
|
+
helper.stop();
|
|
1602
|
+
}
|
|
1603
|
+
return Ok(());
|
|
1604
|
+
}
|
|
1605
|
+
let (x, y) = params
|
|
1606
|
+
.x
|
|
1607
|
+
.zip(params.y)
|
|
1608
|
+
.ok_or_else(|| SessionError::new("malformed", "point requires x and y"))?;
|
|
1609
|
+
let source = &self.inner.source;
|
|
1610
|
+
if x < 0 || y < 0 || x >= i64::from(source.width) || y >= i64::from(source.height) {
|
|
1611
|
+
return Err(SessionError::new(
|
|
1612
|
+
"coordinates",
|
|
1613
|
+
"point is outside the selected desktop",
|
|
1614
|
+
));
|
|
1615
|
+
}
|
|
1616
|
+
let timeout = params.timeout_ms.unwrap_or(3000);
|
|
1617
|
+
if !(1..=120_000).contains(&timeout)
|
|
1618
|
+
|| params.label.len() > 96
|
|
1619
|
+
|| !params.label.bytes().all(|b| (32..=126).contains(&b))
|
|
1620
|
+
{
|
|
1621
|
+
return Err(SessionError::new(
|
|
1622
|
+
"malformed",
|
|
1623
|
+
"timeout must be 1..120000 ms; label must be at most 96 printable ASCII bytes",
|
|
1624
|
+
));
|
|
1625
|
+
}
|
|
1626
|
+
#[cfg(not(target_os = "windows"))]
|
|
1627
|
+
if overlay.is_none() {
|
|
1628
|
+
#[cfg(target_os = "linux")]
|
|
1629
|
+
let helper = match &self.inner.point_desktop {
|
|
1630
|
+
PointDesktop::X11(display) => crate::indicator::Indicator::start_point(display),
|
|
1631
|
+
PointDesktop::Wayland => crate::indicator::Indicator::start_point_wayland(
|
|
1632
|
+
source.width,
|
|
1633
|
+
source.height,
|
|
1634
|
+
source.position,
|
|
1635
|
+
),
|
|
1636
|
+
PointDesktop::Unavailable(_) => unreachable!("validated point target"),
|
|
1637
|
+
};
|
|
1638
|
+
#[cfg(target_os = "macos")]
|
|
1639
|
+
let helper = crate::indicator::Indicator::start_point(
|
|
1640
|
+
source.node_id,
|
|
1641
|
+
source.width as usize,
|
|
1642
|
+
source.height as usize,
|
|
1643
|
+
);
|
|
1644
|
+
*overlay = Some(helper.map_err(|error| {
|
|
1645
|
+
let message = format!("{error:#}");
|
|
1646
|
+
if message.contains("layer_shell_unavailable") {
|
|
1647
|
+
SessionError::new(
|
|
1648
|
+
"point_unsupported",
|
|
1649
|
+
"reason: layer_shell_unavailable; compositor has no layer-shell",
|
|
1650
|
+
)
|
|
1651
|
+
} else {
|
|
1652
|
+
SessionError::new("indicator-unavailable", message)
|
|
1653
|
+
}
|
|
1654
|
+
})?);
|
|
1655
|
+
}
|
|
1656
|
+
#[cfg(not(target_os = "windows"))]
|
|
1657
|
+
return overlay
|
|
1658
|
+
.as_mut()
|
|
1659
|
+
.unwrap()
|
|
1660
|
+
.point(x, y, timeout, ¶ms.label)
|
|
1661
|
+
.map_err(|error| SessionError::new("indicator-unavailable", format!("{error:#}")));
|
|
1662
|
+
#[cfg(target_os = "windows")]
|
|
1663
|
+
return Err(SessionError::new(
|
|
1664
|
+
"point_unsupported",
|
|
1665
|
+
"Windows point overlays are unavailable in this build.",
|
|
1666
|
+
));
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1396
1669
|
pub async fn wait_frame(
|
|
1397
1670
|
&self,
|
|
1398
1671
|
after_seq: Option<u64>,
|
|
@@ -1507,7 +1780,7 @@ impl Session {
|
|
|
1507
1780
|
}
|
|
1508
1781
|
Some(pixels)
|
|
1509
1782
|
};
|
|
1510
|
-
let result = serde_json::json!({ "seq": seq, "still_ms": at.elapsed().as_millis() as u64,
|
|
1783
|
+
let result = serde_json::json!({ "seq": seq, "timestamp_us": crate::protocol::timestamp_us(*at), "still_ms": at.elapsed().as_millis() as u64,
|
|
1511
1784
|
"width": w, "height": h, "format": "bgrx", "damage": damage, "written": !path.is_empty() });
|
|
1512
1785
|
drop(held);
|
|
1513
1786
|
if let Some(pixels) = pixels {
|
|
@@ -1630,6 +1903,9 @@ impl Inner {
|
|
|
1630
1903
|
if let Ok(mut indicator) = self.indicator.lock() {
|
|
1631
1904
|
indicator.take();
|
|
1632
1905
|
}
|
|
1906
|
+
if let Some(helper) = lock(&self.point_overlay).take() {
|
|
1907
|
+
helper.stop();
|
|
1908
|
+
}
|
|
1633
1909
|
// Release first, then tear down: a stuck modifier is the one failure the
|
|
1634
1910
|
// user cannot undo by reconnecting.
|
|
1635
1911
|
if let Ok(mut input) = self.input.lock() {
|
|
@@ -1976,7 +2252,7 @@ impl Inner {
|
|
|
1976
2252
|
if text.len() > 4096 {
|
|
1977
2253
|
return Err(("text-too-large", String::from("text exceeds 4096 bytes")));
|
|
1978
2254
|
}
|
|
1979
|
-
#[cfg(target_os = "macos")]
|
|
2255
|
+
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
1980
2256
|
{
|
|
1981
2257
|
let _ = seq;
|
|
1982
2258
|
return self.with_input(|target| target.unicode_text(text));
|
|
@@ -2973,6 +3249,9 @@ mod tests {
|
|
|
2973
3249
|
encoder: Mutex::new(None),
|
|
2974
3250
|
input: Mutex::new(None),
|
|
2975
3251
|
indicator: Mutex::new(None),
|
|
3252
|
+
point_overlay: Mutex::new(None),
|
|
3253
|
+
#[cfg(target_os = "linux")]
|
|
3254
|
+
point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
|
|
2976
3255
|
capture: Mutex::new(None),
|
|
2977
3256
|
layout: Mutex::new(None),
|
|
2978
3257
|
last_seq: Mutex::new(0),
|
|
@@ -2991,6 +3270,7 @@ mod tests {
|
|
|
2991
3270
|
feed,
|
|
2992
3271
|
})),
|
|
2993
3272
|
local_frames: false,
|
|
3273
|
+
cursor_info: serde_json::json!({"mode":"unavailable", "positions":false}),
|
|
2994
3274
|
});
|
|
2995
3275
|
spawn_peer_events(&inner, peer_events_rx);
|
|
2996
3276
|
spawn_encoded(&inner, feed_rx);
|
|
@@ -3112,6 +3392,140 @@ mod tests {
|
|
|
3112
3392
|
test_inner_with(events, peer, 64, 64)
|
|
3113
3393
|
}
|
|
3114
3394
|
|
|
3395
|
+
#[cfg(target_os = "linux")]
|
|
3396
|
+
#[tokio::test]
|
|
3397
|
+
async fn a_wayland_portal_session_reports_typed_point_unsupported() {
|
|
3398
|
+
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
3399
|
+
let (mut inner, _) = test_inner(events).await;
|
|
3400
|
+
Arc::get_mut(&mut inner)
|
|
3401
|
+
.expect("test session has one owner")
|
|
3402
|
+
.permissions = vec![Permission::View];
|
|
3403
|
+
let session = Session { inner };
|
|
3404
|
+
|
|
3405
|
+
let error = session
|
|
3406
|
+
.point(&crate::protocol::PointParams {
|
|
3407
|
+
session_id: String::from("test-session"),
|
|
3408
|
+
x: Some(10),
|
|
3409
|
+
y: Some(20),
|
|
3410
|
+
label: String::new(),
|
|
3411
|
+
timeout_ms: None,
|
|
3412
|
+
clear: false,
|
|
3413
|
+
})
|
|
3414
|
+
.expect_err("compositors without layer-shell do not support point overlays");
|
|
3415
|
+
|
|
3416
|
+
assert_eq!(error.code, "point_unsupported");
|
|
3417
|
+
assert!(error.message.contains("reason: layer_shell_unavailable"));
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
// Driven only on the private headless compositor created by point-wayland-flow.py.
|
|
3421
|
+
#[cfg(target_os = "linux")]
|
|
3422
|
+
#[tokio::test]
|
|
3423
|
+
#[ignore = "requires the task-owned compositor and screenshot driver"]
|
|
3424
|
+
async fn point_wayland_private_flow() {
|
|
3425
|
+
let evidence = std::path::PathBuf::from(
|
|
3426
|
+
std::env::var_os("DESKLINK_POINT_TEST_DIR").expect("private evidence"),
|
|
3427
|
+
);
|
|
3428
|
+
let socket =
|
|
3429
|
+
std::path::PathBuf::from(std::env::var_os("WAYLAND_DISPLAY").expect("private socket"));
|
|
3430
|
+
assert!(
|
|
3431
|
+
socket.is_absolute() && socket.starts_with(evidence.join("runtime")),
|
|
3432
|
+
"never use an ambient desktop"
|
|
3433
|
+
);
|
|
3434
|
+
fn checkpoint(dir: &std::path::Path, name: &str) {
|
|
3435
|
+
std::fs::write(dir.join(format!("{name}.ready")), b"").unwrap();
|
|
3436
|
+
let deadline = Instant::now() + Duration::from_secs(15);
|
|
3437
|
+
while !dir.join(format!("{name}.continue")).exists() {
|
|
3438
|
+
assert!(
|
|
3439
|
+
Instant::now() < deadline,
|
|
3440
|
+
"screenshot driver timed out at {name}"
|
|
3441
|
+
);
|
|
3442
|
+
std::thread::sleep(Duration::from_millis(20));
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
3446
|
+
let (mut inner, _) = test_inner(events).await;
|
|
3447
|
+
let state = Arc::get_mut(&mut inner).unwrap();
|
|
3448
|
+
state.permissions = vec![Permission::View];
|
|
3449
|
+
state.point_desktop = PointDesktop::Wayland;
|
|
3450
|
+
state.source.width = 1280;
|
|
3451
|
+
state.source.height = 720;
|
|
3452
|
+
state.source.position = Some((1280, 0));
|
|
3453
|
+
let mut session = Session { inner };
|
|
3454
|
+
let mut params = crate::protocol::PointParams {
|
|
3455
|
+
session_id: String::from("test-session"),
|
|
3456
|
+
x: Some(400),
|
|
3457
|
+
y: Some(300),
|
|
3458
|
+
label: String::from("Here"),
|
|
3459
|
+
timeout_ms: Some(3000),
|
|
3460
|
+
clear: false,
|
|
3461
|
+
};
|
|
3462
|
+
let unsupported_socket = std::env::var_os("DESKLINK_POINT_UNSUPPORTED_SOCKET").unwrap();
|
|
3463
|
+
assert!(std::path::Path::new(&unsupported_socket).starts_with(evidence.join("runtime")));
|
|
3464
|
+
// This test runs alone, in a separate process. Only child helper startup
|
|
3465
|
+
// and the read-only clear probe read this private socket environment.
|
|
3466
|
+
std::env::set_var("WAYLAND_DISPLAY", &unsupported_socket);
|
|
3467
|
+
let refusal = session.point(¶ms).unwrap_err();
|
|
3468
|
+
assert_eq!(refusal.code, "point_unsupported");
|
|
3469
|
+
assert!(refusal.message.contains("reason: layer_shell_unavailable"));
|
|
3470
|
+
let clear = crate::protocol::PointParams {
|
|
3471
|
+
session_id: params.session_id.clone(),
|
|
3472
|
+
clear: true,
|
|
3473
|
+
x: None,
|
|
3474
|
+
y: None,
|
|
3475
|
+
label: String::new(),
|
|
3476
|
+
timeout_ms: None,
|
|
3477
|
+
};
|
|
3478
|
+
let refusal = session.point(&clear).unwrap_err();
|
|
3479
|
+
assert_eq!(refusal.code, "point_unsupported");
|
|
3480
|
+
std::env::set_var("WAYLAND_DISPLAY", &socket);
|
|
3481
|
+
Arc::get_mut(&mut session.inner).unwrap().source.position = None;
|
|
3482
|
+
let ambiguous = session.point(¶ms).unwrap_err();
|
|
3483
|
+
assert_eq!(ambiguous.code, "indicator-unavailable");
|
|
3484
|
+
assert!(ambiguous.message.contains("selected_output_unavailable"));
|
|
3485
|
+
Arc::get_mut(&mut session.inner).unwrap().source.position = Some((1280, 0));
|
|
3486
|
+
checkpoint(&evidence, "before");
|
|
3487
|
+
session.point(¶ms).unwrap();
|
|
3488
|
+
checkpoint(&evidence, "after");
|
|
3489
|
+
params.x = Some(1280);
|
|
3490
|
+
assert_eq!(session.point(¶ms).unwrap_err().code, "coordinates");
|
|
3491
|
+
params.x = Some(400);
|
|
3492
|
+
params.label = String::from("bad\nlabel");
|
|
3493
|
+
assert_eq!(session.point(¶ms).unwrap_err().code, "malformed");
|
|
3494
|
+
checkpoint(&evidence, "invalid");
|
|
3495
|
+
tokio::time::sleep(Duration::from_millis(3100)).await;
|
|
3496
|
+
checkpoint(&evidence, "expired");
|
|
3497
|
+
params.x = Some(700);
|
|
3498
|
+
params.y = Some(500);
|
|
3499
|
+
params.label.clear();
|
|
3500
|
+
params.timeout_ms = Some(10000);
|
|
3501
|
+
session.point(¶ms).unwrap();
|
|
3502
|
+
checkpoint(&evidence, "replaced");
|
|
3503
|
+
params.clear = true;
|
|
3504
|
+
params.x = None;
|
|
3505
|
+
params.y = None;
|
|
3506
|
+
params.timeout_ms = None;
|
|
3507
|
+
session.point(¶ms).unwrap();
|
|
3508
|
+
session.point(¶ms).unwrap();
|
|
3509
|
+
assert!(lock(&session.inner.point_overlay).is_none());
|
|
3510
|
+
checkpoint(&evidence, "cleared");
|
|
3511
|
+
params.clear = false;
|
|
3512
|
+
params.x = Some(400);
|
|
3513
|
+
params.y = Some(300);
|
|
3514
|
+
session.point(¶ms).unwrap();
|
|
3515
|
+
let pid = lock(&session.inner.point_overlay)
|
|
3516
|
+
.as_ref()
|
|
3517
|
+
.unwrap()
|
|
3518
|
+
.test_pid();
|
|
3519
|
+
checkpoint(&evidence, "clickthrough");
|
|
3520
|
+
session.close("test complete").await;
|
|
3521
|
+
assert_eq!(
|
|
3522
|
+
unsafe { libc::kill(pid as i32, 0) },
|
|
3523
|
+
-1,
|
|
3524
|
+
"helper reaped on session close"
|
|
3525
|
+
);
|
|
3526
|
+
checkpoint(&evidence, "closed");
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3115
3529
|
fn test_inner_with(
|
|
3116
3530
|
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
3117
3531
|
peer: VideoPeer,
|
|
@@ -3157,6 +3571,9 @@ mod tests {
|
|
|
3157
3571
|
})),
|
|
3158
3572
|
capture: Mutex::new(None),
|
|
3159
3573
|
indicator: Mutex::new(None),
|
|
3574
|
+
point_overlay: Mutex::new(None),
|
|
3575
|
+
#[cfg(target_os = "linux")]
|
|
3576
|
+
point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
|
|
3160
3577
|
layout: Mutex::new(Some(Layout::from_environment().expect("a keymap"))),
|
|
3161
3578
|
last_seq: Mutex::new(0),
|
|
3162
3579
|
control_open: AtomicBool::new(true),
|
|
@@ -3165,6 +3582,7 @@ mod tests {
|
|
|
3165
3582
|
events,
|
|
3166
3583
|
encoded: Mutex::new(None),
|
|
3167
3584
|
local_frames: true,
|
|
3585
|
+
cursor_info: serde_json::json!({"mode":"embedded", "positions":false}),
|
|
3168
3586
|
});
|
|
3169
3587
|
(inner, recorded)
|
|
3170
3588
|
}
|
|
@@ -3174,6 +3592,11 @@ mod tests {
|
|
|
3174
3592
|
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
3175
3593
|
let (inner, recorded) = test_inner(events).await;
|
|
3176
3594
|
|
|
3595
|
+
#[cfg(target_os = "windows")]
|
|
3596
|
+
inner
|
|
3597
|
+
.with_input(|target| target.chord(1, Vec::new(), true))
|
|
3598
|
+
.unwrap();
|
|
3599
|
+
#[cfg(not(target_os = "windows"))]
|
|
3177
3600
|
inner.apply(ControlMessage::Key {
|
|
3178
3601
|
name: None,
|
|
3179
3602
|
character: Some(String::from("a")),
|
|
@@ -3227,6 +3650,7 @@ mod tests {
|
|
|
3227
3650
|
);
|
|
3228
3651
|
}
|
|
3229
3652
|
|
|
3653
|
+
#[cfg(not(target_os = "windows"))]
|
|
3230
3654
|
#[tokio::test]
|
|
3231
3655
|
async fn a_held_modifier_survives_character_named_and_text_chords() {
|
|
3232
3656
|
use crate::input::keycode;
|
|
@@ -3526,6 +3950,7 @@ mod tests {
|
|
|
3526
3950
|
|
|
3527
3951
|
fn encoded_open() -> OpenRequest {
|
|
3528
3952
|
OpenRequest {
|
|
3953
|
+
cursor: crate::protocol::CursorMode::Embedded,
|
|
3529
3954
|
source: Some(SourceRequest::Encoded {
|
|
3530
3955
|
codec: EncodedCodec::H264,
|
|
3531
3956
|
width: 486,
|
|
@@ -3552,6 +3977,18 @@ mod tests {
|
|
|
3552
3977
|
.expect("the channel stays open")
|
|
3553
3978
|
}
|
|
3554
3979
|
|
|
3980
|
+
#[tokio::test]
|
|
3981
|
+
async fn hidden_cursor_is_refused_for_consumer_encoded_pixels() {
|
|
3982
|
+
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
3983
|
+
let mut request = encoded_open();
|
|
3984
|
+
request.cursor = crate::protocol::CursorMode::Hidden;
|
|
3985
|
+
let error = Session::open(request, events)
|
|
3986
|
+
.await
|
|
3987
|
+
.err()
|
|
3988
|
+
.expect("cannot alter encoded pixels");
|
|
3989
|
+
assert_eq!(error.code, "cursor-unavailable");
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3555
3992
|
#[tokio::test]
|
|
3556
3993
|
async fn an_encoded_session_opens_without_capture_encoder_or_input() {
|
|
3557
3994
|
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
@@ -3698,6 +4135,7 @@ mod tests {
|
|
|
3698
4135
|
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
3699
4136
|
let result = Session::open(
|
|
3700
4137
|
OpenRequest {
|
|
4138
|
+
cursor: crate::protocol::CursorMode::Embedded,
|
|
3701
4139
|
source: None,
|
|
3702
4140
|
permissions: vec![Permission::Control],
|
|
3703
4141
|
max_width: 640,
|
|
@@ -3726,6 +4164,11 @@ mod tests {
|
|
|
3726
4164
|
use webrtc::peer_connection::RTCPeerConnectionState as State;
|
|
3727
4165
|
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
3728
4166
|
let (inner, recorded) = test_inner(events).await;
|
|
4167
|
+
#[cfg(target_os = "windows")]
|
|
4168
|
+
inner
|
|
4169
|
+
.with_input(|target| target.chord(1, Vec::new(), true))
|
|
4170
|
+
.unwrap();
|
|
4171
|
+
#[cfg(not(target_os = "windows"))]
|
|
3729
4172
|
inner.apply(ControlMessage::Key {
|
|
3730
4173
|
name: Some(String::from("Control")),
|
|
3731
4174
|
character: None,
|
|
@@ -3942,7 +4385,10 @@ mod tests {
|
|
|
3942
4385
|
1000,
|
|
3943
4386
|
);
|
|
3944
4387
|
|
|
3945
|
-
// 40 frames over 200 ms
|
|
4388
|
+
// 40 frames nominally over 200 ms; Windows timers can round each 5 ms
|
|
4389
|
+
// sleep upward, so its cap assertion uses the actual burst duration.
|
|
4390
|
+
#[cfg(target_os = "windows")]
|
|
4391
|
+
let burst_started = Instant::now();
|
|
3946
4392
|
for _ in 0..40 {
|
|
3947
4393
|
frame_tx.put(
|
|
3948
4394
|
I420 {
|
|
@@ -3955,9 +4401,13 @@ mod tests {
|
|
|
3955
4401
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
3956
4402
|
}
|
|
3957
4403
|
let motion = inner.metrics.lock().unwrap().encoded_frames;
|
|
4404
|
+
#[cfg(target_os = "windows")]
|
|
4405
|
+
let cap = (burst_started.elapsed().as_secs_f64() * 20.0).ceil() as u64 + 3;
|
|
4406
|
+
#[cfg(not(target_os = "windows"))]
|
|
4407
|
+
let cap = 7;
|
|
3958
4408
|
assert!(
|
|
3959
|
-
motion <=
|
|
3960
|
-
"the requested rate is a cap, got {motion} of 40"
|
|
4409
|
+
motion <= cap,
|
|
4410
|
+
"the requested rate is a cap, got {motion} of 40 (cap {cap})"
|
|
3961
4411
|
);
|
|
3962
4412
|
assert!(
|
|
3963
4413
|
motion >= 2,
|