@desklink/host 0.5.14 → 0.5.16
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 +6 -1
- package/bin/desklink-host.mjs +9 -3
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +220 -0
- package/dist/doctor.js.map +1 -0
- package/dist/engineProcess.d.ts +5 -0
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +15 -5
- package/dist/engineProcess.js.map +1 -1
- package/docs/PROTOCOL.md +4 -2
- package/engine/Cargo.lock +1 -1
- package/engine/Cargo.toml +1 -1
- package/engine/native/mac_stream.mm +4 -1
- package/engine/src/capture.rs +0 -79
- package/engine/src/diagnose.rs +99 -0
- package/engine/src/encoder.rs +0 -48
- package/engine/src/h264_nvenc.rs +0 -165
- package/engine/src/h264_openh264.rs +0 -245
- package/engine/src/h264_vaapi.rs +0 -97
- package/engine/src/indicator.rs +0 -101
- package/engine/src/input_portal.rs +337 -0
- package/engine/src/mac.rs +229 -6
- package/engine/src/main.rs +7 -88
- package/engine/src/observe.rs +0 -33
- package/engine/src/peer.rs +0 -433
- package/engine/src/portal.rs +0 -75
- package/engine/src/session.rs +112 -1085
- package/examples/reference-client.html +13 -0
- package/package.json +3 -3
package/engine/src/session.rs
CHANGED
|
@@ -208,9 +208,9 @@ unsafe extern "C" {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
/// The X11 agent cue is a real window, so root grabs contain it. Restore
|
|
211
|
-
///
|
|
212
|
-
///
|
|
213
|
-
///
|
|
211
|
+
/// the pixels the announced cues cover (with a small margin for edge rounding)
|
|
212
|
+
/// from the last clean frame; everything else, including what lies inside the
|
|
213
|
+
/// halo, stays live.
|
|
214
214
|
#[cfg(target_os = "linux")]
|
|
215
215
|
struct CueMask {
|
|
216
216
|
cues: crate::indicator::Cues,
|
|
@@ -230,18 +230,18 @@ impl CueMask {
|
|
|
230
230
|
.unwrap_or_default()
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
/// `
|
|
234
|
-
///
|
|
233
|
+
/// `announced` is the snapshot taken after the grab: the helper announces
|
|
234
|
+
/// every state before drawing it, so this covers whatever the grab saw.
|
|
235
235
|
fn apply(
|
|
236
236
|
&mut self,
|
|
237
237
|
raw: &mut [u8],
|
|
238
238
|
width: usize,
|
|
239
239
|
height: usize,
|
|
240
|
-
mut
|
|
240
|
+
mut announced: Vec<crate::indicator::Cue>,
|
|
241
241
|
) {
|
|
242
|
-
|
|
242
|
+
announced.extend(self.snapshot());
|
|
243
243
|
if let Some(clean) = self.clean.as_ref().filter(|clean| clean.len() == raw.len()) {
|
|
244
|
-
for cue in
|
|
244
|
+
for cue in &announced {
|
|
245
245
|
let side = cue.size as usize + 2;
|
|
246
246
|
let mut covered = vec![false; side * side];
|
|
247
247
|
for row in 0..cue.size as usize {
|
|
@@ -260,16 +260,26 @@ impl CueMask {
|
|
|
260
260
|
} != 0;
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
263
|
+
// Restore the cue's pixels and one pixel around them. The
|
|
264
|
+
// shaped window's edge rounds differently from the coverage
|
|
265
|
+
// test; the margin keeps its anti-aliased edge out of the
|
|
266
|
+
// picture without covering live pixels inside the halo.
|
|
267
|
+
let margin = 1isize;
|
|
268
|
+
for row in -margin..side as isize + margin {
|
|
269
|
+
for col in -margin..side as isize + margin {
|
|
270
|
+
let near = |r: isize, c: isize| {
|
|
271
|
+
(-margin..=margin).any(|dr| {
|
|
272
|
+
(-margin..=margin).any(|dc| {
|
|
273
|
+
let (rr, cc) = (r + dr, c + dc);
|
|
274
|
+
rr >= 0
|
|
275
|
+
&& cc >= 0
|
|
276
|
+
&& rr < side as isize
|
|
277
|
+
&& cc < side as isize
|
|
278
|
+
&& covered[rr as usize * side + cc as usize]
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
};
|
|
282
|
+
if !near(row, col) {
|
|
273
283
|
continue;
|
|
274
284
|
}
|
|
275
285
|
let (x, y) = (cue.x - 1 + col as i64, cue.y - 1 + row as i64);
|
|
@@ -282,7 +292,29 @@ impl CueMask {
|
|
|
282
292
|
}
|
|
283
293
|
}
|
|
284
294
|
}
|
|
285
|
-
|
|
295
|
+
// Keep the pre-cue baseline inside each cue's bounding box. Restoring
|
|
296
|
+
// only the announced shape can leave an anti-aliased edge pixel in the
|
|
297
|
+
// frame; if that pixel became the baseline, every later cue over it
|
|
298
|
+
// would restore the old cue instead of the desktop. Outside the boxes
|
|
299
|
+
// the grab is live and becomes the next baseline.
|
|
300
|
+
self.clean = Some(match self.clean.take() {
|
|
301
|
+
Some(old) if old.len() == raw.len() && !announced.is_empty() => {
|
|
302
|
+
let mut next = raw.to_vec();
|
|
303
|
+
for cue in &announced {
|
|
304
|
+
let x0 = (cue.x - 4).max(0) as usize;
|
|
305
|
+
let y0 = (cue.y - 4).max(0) as usize;
|
|
306
|
+
let x1 = ((cue.x + cue.size + 4).max(0) as usize).min(width);
|
|
307
|
+
let y1 = ((cue.y + cue.size + 4).max(0) as usize).min(height);
|
|
308
|
+
for y in y0..y1 {
|
|
309
|
+
let start = (y * width + x0) * 4;
|
|
310
|
+
let end = (y * width + x1) * 4;
|
|
311
|
+
next[start..end].copy_from_slice(&old[start..end]);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
next
|
|
315
|
+
}
|
|
316
|
+
_ => raw.to_vec(),
|
|
317
|
+
});
|
|
286
318
|
}
|
|
287
319
|
}
|
|
288
320
|
|
|
@@ -371,11 +403,17 @@ fn select_x11(
|
|
|
371
403
|
let grabbed = Instant::now();
|
|
372
404
|
#[cfg(target_os = "linux")]
|
|
373
405
|
let frame = {
|
|
374
|
-
let before = mask.as_ref().map(CueMask::snapshot);
|
|
375
406
|
let mut desktop = lock(&desktop);
|
|
376
407
|
desktop.capture_raw().and_then(|mut raw| {
|
|
377
|
-
if let
|
|
378
|
-
|
|
408
|
+
if let Some(mask) = mask.as_mut() {
|
|
409
|
+
// The helper announces each cue before it draws
|
|
410
|
+
// it, so reading the feed after the grab covers
|
|
411
|
+
// every state the grab could have seen. Reading
|
|
412
|
+
// it before the grab would miss a cue drawn
|
|
413
|
+
// during the grab and bake it into the clean
|
|
414
|
+
// baseline.
|
|
415
|
+
let after = mask.snapshot();
|
|
416
|
+
mask.apply(&mut raw, width as usize, height as usize, after);
|
|
379
417
|
}
|
|
380
418
|
Ok((desktop.convert(&raw, max_width, max_height)?, raw))
|
|
381
419
|
})
|
|
@@ -474,6 +512,9 @@ struct InputTarget {
|
|
|
474
512
|
|
|
475
513
|
enum Applier {
|
|
476
514
|
Uinput(InputDevices),
|
|
515
|
+
/// Keyboard only: the portal grants keys, not a pointer.
|
|
516
|
+
#[cfg(target_os = "linux")]
|
|
517
|
+
PortalKeys(crate::input_portal::PortalKeys),
|
|
477
518
|
X11(Arc<Mutex<X11Desktop>>),
|
|
478
519
|
#[cfg(test)]
|
|
479
520
|
Recording(Arc<Mutex<Vec<(i16, bool)>>>),
|
|
@@ -486,6 +527,8 @@ impl InputTarget {
|
|
|
486
527
|
devices.move_absolute(x, y);
|
|
487
528
|
Ok(())
|
|
488
529
|
}
|
|
530
|
+
#[cfg(target_os = "linux")]
|
|
531
|
+
Applier::PortalKeys(_) => anyhow::bail!(NO_PORTAL_POINTER),
|
|
489
532
|
Applier::X11(desktop) => lock(desktop).move_pointer(x, y),
|
|
490
533
|
#[cfg(test)]
|
|
491
534
|
Applier::Recording(_) => Ok(()),
|
|
@@ -499,6 +542,8 @@ impl InputTarget {
|
|
|
499
542
|
devices.button(button, down);
|
|
500
543
|
Ok(())
|
|
501
544
|
}
|
|
545
|
+
#[cfg(target_os = "linux")]
|
|
546
|
+
Applier::PortalKeys(_) => anyhow::bail!(NO_PORTAL_POINTER),
|
|
502
547
|
Applier::X11(desktop) => lock(desktop).button(x11_button(button), down),
|
|
503
548
|
#[cfg(test)]
|
|
504
549
|
Applier::Recording(log) => {
|
|
@@ -516,6 +561,8 @@ impl InputTarget {
|
|
|
516
561
|
devices.scroll(dx, dy);
|
|
517
562
|
Ok(())
|
|
518
563
|
}
|
|
564
|
+
#[cfg(target_os = "linux")]
|
|
565
|
+
Applier::PortalKeys(_) => anyhow::bail!(NO_PORTAL_POINTER),
|
|
519
566
|
Applier::X11(desktop) => {
|
|
520
567
|
let (rest_x, rest_y) = (self.wheel_rest.0 + dx, self.wheel_rest.1 + dy);
|
|
521
568
|
let (whole_x, whole_y) = (rest_x.trunc(), rest_y.trunc());
|
|
@@ -539,6 +586,8 @@ impl InputTarget {
|
|
|
539
586
|
fn key(&mut self, code: i16, down: bool) -> Result<()> {
|
|
540
587
|
match &mut self.applier {
|
|
541
588
|
Applier::Uinput(devices) => devices.key(code, down),
|
|
589
|
+
#[cfg(target_os = "linux")]
|
|
590
|
+
Applier::PortalKeys(keys) => keys.key(code, down),
|
|
542
591
|
Applier::X11(desktop) => lock(desktop).key(code, down),
|
|
543
592
|
#[cfg(test)]
|
|
544
593
|
Applier::Recording(log) => {
|
|
@@ -637,6 +686,8 @@ impl InputTarget {
|
|
|
637
686
|
for button in buttons {
|
|
638
687
|
match &mut self.applier {
|
|
639
688
|
Applier::Uinput(devices) => devices.button(button, false),
|
|
689
|
+
#[cfg(target_os = "linux")]
|
|
690
|
+
Applier::PortalKeys(_) => {}
|
|
640
691
|
Applier::X11(desktop) => lock(desktop).button(x11_button(button), false)?,
|
|
641
692
|
#[cfg(test)]
|
|
642
693
|
Applier::Recording(log) => {
|
|
@@ -649,6 +700,8 @@ impl InputTarget {
|
|
|
649
700
|
for code in keys {
|
|
650
701
|
match &mut self.applier {
|
|
651
702
|
Applier::Uinput(devices) => devices.key(code, false)?,
|
|
703
|
+
#[cfg(target_os = "linux")]
|
|
704
|
+
Applier::PortalKeys(keys) => keys.key(code, false)?,
|
|
652
705
|
Applier::X11(desktop) => lock(desktop).key(code, false)?,
|
|
653
706
|
#[cfg(test)]
|
|
654
707
|
Applier::Recording(log) => {
|
|
@@ -662,6 +715,9 @@ impl InputTarget {
|
|
|
662
715
|
}
|
|
663
716
|
}
|
|
664
717
|
|
|
718
|
+
#[cfg(target_os = "linux")]
|
|
719
|
+
const NO_PORTAL_POINTER: &str = "pointer input is unavailable: the portal grants the keyboard only";
|
|
720
|
+
|
|
665
721
|
fn x11_button(button: Button) -> u8 {
|
|
666
722
|
match button {
|
|
667
723
|
Button::Left => crate::x11::button::LEFT,
|
|
@@ -904,6 +960,13 @@ fn wayland_clipboard_available() -> bool {
|
|
|
904
960
|
/// What the engine can actually do on this machine right now.
|
|
905
961
|
#[cfg(target_os = "linux")]
|
|
906
962
|
pub fn capabilities() -> serde_json::Value {
|
|
963
|
+
capabilities_with(crate::input::probe())
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/// [`capabilities`] given the `uinput` probe's answer, so the choice between
|
|
967
|
+
/// `uinput` and the portal keyboard is testable on a host of either kind.
|
|
968
|
+
#[cfg(target_os = "linux")]
|
|
969
|
+
pub fn capabilities_with(uinput: Result<(), crate::input::InputUnavailable>) -> serde_json::Value {
|
|
907
970
|
let session_kind = if std::env::var_os("WAYLAND_DISPLAY").is_some() {
|
|
908
971
|
"wayland"
|
|
909
972
|
} else if std::env::var_os("DISPLAY").is_some() {
|
|
@@ -918,7 +981,10 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
918
981
|
// Asked, not taken: the probe connects to this session's PipeWire, reads the
|
|
919
982
|
// registry and leaves. No portal, no prompt, no capture.
|
|
920
983
|
let audio = crate::audio::probe();
|
|
921
|
-
|
|
984
|
+
// Fallback only: reads one portal property when uinput cannot be created.
|
|
985
|
+
let portal_keys =
|
|
986
|
+
uinput.is_err() && x11.is_null() && crate::input_portal::advertised_blocking();
|
|
987
|
+
let (input, grant_state, unavailable) = match uinput {
|
|
922
988
|
Ok(()) => (serde_json::json!(true), "granted", serde_json::Value::Null),
|
|
923
989
|
Err(unavailable) => (
|
|
924
990
|
serde_json::json!(false),
|
|
@@ -951,14 +1017,14 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
951
1017
|
},
|
|
952
1018
|
"encode": encode_capabilities(),
|
|
953
1019
|
"input": {
|
|
954
|
-
"mechanism": "inputtino/uinput",
|
|
955
|
-
"pointer": input,
|
|
956
|
-
"wheel": input,
|
|
957
|
-
"keyboard": input,
|
|
1020
|
+
"mechanism": if portal_keys { "portal-remotedesktop" } else { "inputtino/uinput" },
|
|
1021
|
+
"pointer": input == true && !portal_keys,
|
|
1022
|
+
"wheel": input == true && !portal_keys,
|
|
1023
|
+
"keyboard": input == true || portal_keys,
|
|
958
1024
|
"text": ["layout-reachable"],
|
|
959
1025
|
"layout": keymap::LayoutNames::from_environment().identity(),
|
|
960
|
-
"unavailable_reason": unavailable,
|
|
961
|
-
"grant": grant_state,
|
|
1026
|
+
"unavailable_reason": if portal_keys { serde_json::Value::Null } else { unavailable },
|
|
1027
|
+
"grant": if portal_keys { "portal" } else { grant_state },
|
|
962
1028
|
},
|
|
963
1029
|
"clipboard": { "read": clipboard, "write": clipboard && clipboard::writer_available(), "mime": ["text/plain;charset=utf-8"],
|
|
964
1030
|
"maxBytes": clipboard::MAX_CLIPBOARD_BYTES },
|
|
@@ -1431,15 +1497,28 @@ impl Session {
|
|
|
1431
1497
|
.input_available()
|
|
1432
1498
|
.then(|| Applier::X11(desktop.clone())),
|
|
1433
1499
|
None => {
|
|
1500
|
+
// uinput as before; only when it cannot be created, the
|
|
1501
|
+
// portal's keyboard (keys only, no host-wide device).
|
|
1434
1502
|
#[cfg(target_os = "linux")]
|
|
1435
|
-
let
|
|
1503
|
+
let applier = match InputDevices::create(source_w as i32, source_h as i32) {
|
|
1504
|
+
Ok(devices) => Some(Applier::Uinput(devices)),
|
|
1505
|
+
Err(error) => crate::input_portal::open()
|
|
1506
|
+
.await
|
|
1507
|
+
.map_err(|portal| {
|
|
1508
|
+
log::info!("no input: uinput: {error:#}; portal keyboard: {portal:#}")
|
|
1509
|
+
})
|
|
1510
|
+
.ok()
|
|
1511
|
+
.map(Applier::PortalKeys),
|
|
1512
|
+
};
|
|
1436
1513
|
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
1437
|
-
let
|
|
1514
|
+
let applier = InputDevices::create_for_display(
|
|
1438
1515
|
source_w as i32,
|
|
1439
1516
|
source_h as i32,
|
|
1440
1517
|
source.node_id,
|
|
1441
|
-
)
|
|
1442
|
-
|
|
1518
|
+
)
|
|
1519
|
+
.ok()
|
|
1520
|
+
.map(Applier::Uinput);
|
|
1521
|
+
applier
|
|
1443
1522
|
}
|
|
1444
1523
|
};
|
|
1445
1524
|
let input = applier.map(|applier| InputTarget {
|
|
@@ -3767,167 +3846,8 @@ fn opaque_id() -> String {
|
|
|
3767
3846
|
|
|
3768
3847
|
#[cfg(test)]
|
|
3769
3848
|
mod tests {
|
|
3770
|
-
#[cfg(target_os = "linux")]
|
|
3771
|
-
#[test]
|
|
3772
|
-
fn x11_cue_mask_restores_only_the_cue_pixels() {
|
|
3773
|
-
use crate::indicator::Cue;
|
|
3774
|
-
let (w, h) = (200usize, 120usize);
|
|
3775
|
-
let at = |x: usize, y: usize| (y * w + x) * 4;
|
|
3776
|
-
let (cx, cy) = (20 + 55, 10 + 55);
|
|
3777
|
-
// Shaped (128) and composited (1) helpers announce different thresholds.
|
|
3778
|
-
for threshold in [128, 1] {
|
|
3779
|
-
let cue = Cue {
|
|
3780
|
-
x: 20,
|
|
3781
|
-
y: 10,
|
|
3782
|
-
size: 110,
|
|
3783
|
-
unit: 1.0,
|
|
3784
|
-
opacity: 1.0,
|
|
3785
|
-
click: -1.0,
|
|
3786
|
-
typed: -1.0,
|
|
3787
|
-
threshold,
|
|
3788
|
-
reveal: 99.0,
|
|
3789
|
-
};
|
|
3790
|
-
let mut mask = CueMask::new(Default::default());
|
|
3791
|
-
let clean: Vec<u8> = (0..w * h * 4).map(|i| (i % 251) as u8).collect();
|
|
3792
|
-
mask.apply(&mut clean.clone(), w, h, vec![]);
|
|
3793
|
-
// Paint the cue's keyline and live content inside the halo.
|
|
3794
|
-
let mut frame = clean.clone();
|
|
3795
|
-
frame[at(cx - 24, cy)..at(cx - 24, cy) + 4].fill(255);
|
|
3796
|
-
frame[at(cx - 5, cy)..at(cx - 5, cy) + 4].fill(7);
|
|
3797
|
-
frame[at(190, 110)..at(190, 110) + 4].fill(9);
|
|
3798
|
-
mask.apply(&mut frame, w, h, vec![cue]);
|
|
3799
|
-
assert_eq!(
|
|
3800
|
-
&frame[at(cx - 24, cy)..at(cx - 24, cy) + 4],
|
|
3801
|
-
&clean[at(cx - 24, cy)..at(cx - 24, cy) + 4]
|
|
3802
|
-
);
|
|
3803
|
-
assert_eq!(
|
|
3804
|
-
&frame[at(cx - 5, cy)..at(cx - 5, cy) + 4],
|
|
3805
|
-
&[7; 4],
|
|
3806
|
-
"the halo interior stays live"
|
|
3807
|
-
);
|
|
3808
|
-
assert_eq!(
|
|
3809
|
-
&frame[at(190, 110)..at(190, 110) + 4],
|
|
3810
|
-
&[9; 4],
|
|
3811
|
-
"pixels away from the cue stay live"
|
|
3812
|
-
);
|
|
3813
|
-
let mut after = clean.clone();
|
|
3814
|
-
after[at(cx - 24, cy)..at(cx - 24, cy) + 4].fill(3);
|
|
3815
|
-
mask.apply(&mut after, w, h, vec![]);
|
|
3816
|
-
assert_eq!(
|
|
3817
|
-
&after[at(cx - 24, cy)..at(cx - 24, cy) + 4],
|
|
3818
|
-
&[3; 4],
|
|
3819
|
-
"a hidden cue masks nothing"
|
|
3820
|
-
);
|
|
3821
|
-
}
|
|
3822
|
-
}
|
|
3823
|
-
|
|
3824
3849
|
use super::*;
|
|
3825
3850
|
|
|
3826
|
-
#[tokio::test]
|
|
3827
|
-
async fn a_closed_encoded_session_ends_the_tasks_its_channels_park() {
|
|
3828
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
3829
|
-
let (peer_events, peer_events_rx) = tokio_mpsc::unbounded_channel::<PeerEvent>();
|
|
3830
|
-
let (feed, feed_rx) = tokio_mpsc::channel::<FedAccessUnit>(FEED_BACKLOG);
|
|
3831
|
-
let inner = Arc::new(Inner {
|
|
3832
|
-
id: String::from("encoded-session"),
|
|
3833
|
-
generation: 1,
|
|
3834
|
-
source: SelectedSource {
|
|
3835
|
-
node_id: 0,
|
|
3836
|
-
width: 640,
|
|
3837
|
-
height: 480,
|
|
3838
|
-
position: None,
|
|
3839
|
-
source_type: Some(String::from("encoded")),
|
|
3840
|
-
origin_x: 0,
|
|
3841
|
-
origin_y: 0,
|
|
3842
|
-
},
|
|
3843
|
-
geometry: serde_json::json!({
|
|
3844
|
-
"source": { "width": 640, "height": 480 },
|
|
3845
|
-
"encoded": { "width": 640, "height": 480 },
|
|
3846
|
-
"origin": { "x": 0, "y": 0 },
|
|
3847
|
-
}),
|
|
3848
|
-
metrics: Arc::new(Mutex::new(Metrics::default())),
|
|
3849
|
-
latest: Arc::new(Mutex::new(None)),
|
|
3850
|
-
capture_error: Arc::new(Mutex::new(None)),
|
|
3851
|
-
frame_changes: tokio::sync::watch::channel(0).1,
|
|
3852
|
-
peer: Mutex::new(None),
|
|
3853
|
-
encoder: Mutex::new(None),
|
|
3854
|
-
input: Mutex::new(None),
|
|
3855
|
-
indicator: Mutex::new(None),
|
|
3856
|
-
point_overlay: Mutex::new(None),
|
|
3857
|
-
#[cfg(target_os = "linux")]
|
|
3858
|
-
point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
|
|
3859
|
-
#[cfg(target_os = "linux")]
|
|
3860
|
-
window: Mutex::new(None),
|
|
3861
|
-
#[cfg(target_os = "linux")]
|
|
3862
|
-
cue_output: Mutex::new(None),
|
|
3863
|
-
#[cfg(target_os = "linux")]
|
|
3864
|
-
point_output: Mutex::new(None),
|
|
3865
|
-
capture: Mutex::new(None),
|
|
3866
|
-
layout: Mutex::new(None),
|
|
3867
|
-
#[cfg(target_os = "linux")]
|
|
3868
|
-
audio: Mutex::new(None),
|
|
3869
|
-
last_seq: Mutex::new(0),
|
|
3870
|
-
control_open: AtomicBool::new(false),
|
|
3871
|
-
closed: AtomicBool::new(false),
|
|
3872
|
-
pipeline: Arc::new(AtomicBool::new(false)),
|
|
3873
|
-
events,
|
|
3874
|
-
encoded: Mutex::new(Some(Encoded {
|
|
3875
|
-
codec: EncodedCodec::H264,
|
|
3876
|
-
transport: TransportOptions {
|
|
3877
|
-
ice_servers: Vec::new(),
|
|
3878
|
-
loopback_tcp: false,
|
|
3879
|
-
pace_bps: 20_000_000.0,
|
|
3880
|
-
},
|
|
3881
|
-
peer_events,
|
|
3882
|
-
feed,
|
|
3883
|
-
})),
|
|
3884
|
-
local_frames: false,
|
|
3885
|
-
cursor_info: serde_json::json!({"mode":"unavailable", "positions":false}),
|
|
3886
|
-
observation: serde_json::json!({"observing": false}),
|
|
3887
|
-
observer: Mutex::new(None),
|
|
3888
|
-
});
|
|
3889
|
-
spawn_peer_events(&inner, peer_events_rx);
|
|
3890
|
-
spawn_encoded(&inner, feed_rx);
|
|
3891
|
-
inner.close("test", "closed").await;
|
|
3892
|
-
for _ in 0..100 {
|
|
3893
|
-
if Arc::strong_count(&inner) == 1 {
|
|
3894
|
-
break;
|
|
3895
|
-
}
|
|
3896
|
-
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
3897
|
-
}
|
|
3898
|
-
assert_eq!(
|
|
3899
|
-
Arc::strong_count(&inner),
|
|
3900
|
-
1,
|
|
3901
|
-
"closing must end the tasks whose channels the close takes away",
|
|
3902
|
-
);
|
|
3903
|
-
}
|
|
3904
|
-
|
|
3905
|
-
#[test]
|
|
3906
|
-
fn an_encoded_surface_pixel_maps_to_the_sources_own_pixel() {
|
|
3907
|
-
let source = SelectedSource {
|
|
3908
|
-
node_id: 0,
|
|
3909
|
-
width: 2560,
|
|
3910
|
-
height: 1440,
|
|
3911
|
-
position: Some((1920, 0)),
|
|
3912
|
-
source_type: None,
|
|
3913
|
-
origin_x: 1920,
|
|
3914
|
-
origin_y: 0,
|
|
3915
|
-
};
|
|
3916
|
-
// A tap at the centre of a 1280x720 surface must reach the centre of the
|
|
3917
|
-
// 2560x1440 source, not two thirds of the way there; a tap in the right
|
|
3918
|
-
// half must stay source-local rather than land on a layout origin the
|
|
3919
|
-
// applier would clamp away.
|
|
3920
|
-
assert_eq!(
|
|
3921
|
-
to_source_pixels(640, 360, (1280, 720), &source),
|
|
3922
|
-
(1280, 720)
|
|
3923
|
-
);
|
|
3924
|
-
assert_eq!(
|
|
3925
|
-
to_source_pixels(1279, 719, (1280, 720), &source),
|
|
3926
|
-
(2558, 1438)
|
|
3927
|
-
);
|
|
3928
|
-
assert_eq!(to_source_pixels(0, 0, (1280, 720), &source), (0, 0));
|
|
3929
|
-
}
|
|
3930
|
-
|
|
3931
3851
|
async fn test_inner(
|
|
3932
3852
|
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
3933
3853
|
) -> (Arc<Inner>, Arc<Mutex<Vec<(i16, bool)>>>) {
|
|
@@ -3946,28 +3866,6 @@ mod tests {
|
|
|
3946
3866
|
test_inner_with(events, Arc::new(peer), 64, 64)
|
|
3947
3867
|
}
|
|
3948
3868
|
|
|
3949
|
-
#[cfg(target_os = "linux")]
|
|
3950
|
-
#[tokio::test]
|
|
3951
|
-
async fn a_wayland_portal_session_reports_typed_point_unsupported() {
|
|
3952
|
-
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
3953
|
-
let (inner, _) = test_inner(events).await;
|
|
3954
|
-
let session = Session { inner };
|
|
3955
|
-
|
|
3956
|
-
let error = session
|
|
3957
|
-
.point(&crate::protocol::PointParams {
|
|
3958
|
-
session_id: String::from("test-session"),
|
|
3959
|
-
x: Some(10),
|
|
3960
|
-
y: Some(20),
|
|
3961
|
-
label: String::new(),
|
|
3962
|
-
timeout_ms: None,
|
|
3963
|
-
clear: false,
|
|
3964
|
-
})
|
|
3965
|
-
.expect_err("compositors without layer-shell do not support point overlays");
|
|
3966
|
-
|
|
3967
|
-
assert_eq!(error.code, "point_unsupported");
|
|
3968
|
-
assert!(error.message.contains("reason: layer_shell_unavailable"));
|
|
3969
|
-
}
|
|
3970
|
-
|
|
3971
3869
|
// Driven only on the private headless compositor created by point-wayland-flow.py.
|
|
3972
3870
|
#[cfg(target_os = "linux")]
|
|
3973
3871
|
#[tokio::test]
|
|
@@ -4145,875 +4043,4 @@ mod tests {
|
|
|
4145
4043
|
});
|
|
4146
4044
|
(inner, recorded)
|
|
4147
4045
|
}
|
|
4148
|
-
|
|
4149
|
-
#[tokio::test]
|
|
4150
|
-
async fn paired_input_without_a_backend_is_an_action_failure() {
|
|
4151
|
-
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
4152
|
-
let (inner, recorded) = test_inner(events).await;
|
|
4153
|
-
*lock(&inner.input) = None;
|
|
4154
|
-
assert_eq!(
|
|
4155
|
-
inner.with_input(|_| Ok(())).unwrap_err().0,
|
|
4156
|
-
"input-unavailable"
|
|
4157
|
-
);
|
|
4158
|
-
inner.apply(ControlMessage::Pointer {
|
|
4159
|
-
phase: PointerPhase::Move,
|
|
4160
|
-
x: 10,
|
|
4161
|
-
y: 20,
|
|
4162
|
-
button: 1,
|
|
4163
|
-
seq: 1,
|
|
4164
|
-
});
|
|
4165
|
-
assert_eq!(
|
|
4166
|
-
*lock(&inner.last_seq),
|
|
4167
|
-
1,
|
|
4168
|
-
"paired action passes admission, never a permission refusal"
|
|
4169
|
-
);
|
|
4170
|
-
assert_eq!(lock(&inner.metrics).input_rejected, 1);
|
|
4171
|
-
assert_eq!(lock(&inner.metrics).input_applied, 0);
|
|
4172
|
-
assert!(lock(&recorded).is_empty());
|
|
4173
|
-
assert!(
|
|
4174
|
-
!inner.closed.load(Ordering::Relaxed),
|
|
4175
|
-
"missing input does not close view access"
|
|
4176
|
-
);
|
|
4177
|
-
}
|
|
4178
|
-
|
|
4179
|
-
#[tokio::test]
|
|
4180
|
-
async fn a_lease_that_expires_ends_the_session_and_releases_held_input() {
|
|
4181
|
-
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4182
|
-
let (inner, recorded) = test_inner(events).await;
|
|
4183
|
-
|
|
4184
|
-
#[cfg(target_os = "windows")]
|
|
4185
|
-
inner
|
|
4186
|
-
.with_input(|target| target.chord(1, Vec::new(), true))
|
|
4187
|
-
.unwrap();
|
|
4188
|
-
#[cfg(not(target_os = "windows"))]
|
|
4189
|
-
inner.apply(ControlMessage::Key {
|
|
4190
|
-
name: None,
|
|
4191
|
-
character: Some(String::from("a")),
|
|
4192
|
-
down: true,
|
|
4193
|
-
modifiers: Vec::new(),
|
|
4194
|
-
seq: 1,
|
|
4195
|
-
});
|
|
4196
|
-
assert!(
|
|
4197
|
-
recorded.lock().unwrap().iter().any(|(_, down)| *down),
|
|
4198
|
-
"the press reached the desktop",
|
|
4199
|
-
);
|
|
4200
|
-
|
|
4201
|
-
spawn_lease(&inner, Duration::from_millis(20));
|
|
4202
|
-
tokio::time::sleep(Duration::from_millis(150)).await;
|
|
4203
|
-
|
|
4204
|
-
assert!(
|
|
4205
|
-
inner.revoked_reason().is_some(),
|
|
4206
|
-
"an expired lease must end the session"
|
|
4207
|
-
);
|
|
4208
|
-
assert!(
|
|
4209
|
-
!inner.pipeline.load(Ordering::SeqCst),
|
|
4210
|
-
"an expired lease must stop capture",
|
|
4211
|
-
);
|
|
4212
|
-
assert!(
|
|
4213
|
-
recorded.lock().unwrap().iter().any(|(_, down)| !*down),
|
|
4214
|
-
"an expired lease must release what the session pressed",
|
|
4215
|
-
);
|
|
4216
|
-
assert!(
|
|
4217
|
-
matches!(
|
|
4218
|
-
received.try_recv(),
|
|
4219
|
-
Ok(Notice {
|
|
4220
|
-
event: SessionEvent::Revoked { code: "lease", .. },
|
|
4221
|
-
..
|
|
4222
|
-
})
|
|
4223
|
-
),
|
|
4224
|
-
"the consumer is still told the session's authority ended",
|
|
4225
|
-
);
|
|
4226
|
-
|
|
4227
|
-
let rejected = inner.metrics.lock().unwrap().input_rejected;
|
|
4228
|
-
inner.apply(ControlMessage::Key {
|
|
4229
|
-
name: None,
|
|
4230
|
-
character: Some(String::from("b")),
|
|
4231
|
-
down: true,
|
|
4232
|
-
modifiers: Vec::new(),
|
|
4233
|
-
seq: 2,
|
|
4234
|
-
});
|
|
4235
|
-
assert_eq!(
|
|
4236
|
-
inner.metrics.lock().unwrap().input_rejected,
|
|
4237
|
-
rejected + 1,
|
|
4238
|
-
"a control message after expiry must be refused",
|
|
4239
|
-
);
|
|
4240
|
-
}
|
|
4241
|
-
|
|
4242
|
-
#[cfg(not(target_os = "windows"))]
|
|
4243
|
-
#[tokio::test]
|
|
4244
|
-
async fn a_held_modifier_survives_character_named_and_text_chords() {
|
|
4245
|
-
use crate::input::keycode;
|
|
4246
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
4247
|
-
let (inner, recorded) = test_inner(events).await;
|
|
4248
|
-
for (seq, name) in ["Control", "Shift", "Alt", "Meta"].iter().enumerate() {
|
|
4249
|
-
inner.apply(ControlMessage::Key {
|
|
4250
|
-
name: Some((*name).into()),
|
|
4251
|
-
character: None,
|
|
4252
|
-
down: true,
|
|
4253
|
-
modifiers: Vec::new(),
|
|
4254
|
-
seq: seq as u64 + 1,
|
|
4255
|
-
});
|
|
4256
|
-
}
|
|
4257
|
-
inner.apply(ControlMessage::Key {
|
|
4258
|
-
name: None,
|
|
4259
|
-
character: Some(String::from("C")),
|
|
4260
|
-
down: true,
|
|
4261
|
-
modifiers: vec![String::from("Control"), String::from("Shift")],
|
|
4262
|
-
seq: 5,
|
|
4263
|
-
});
|
|
4264
|
-
inner.apply(ControlMessage::Key {
|
|
4265
|
-
name: None,
|
|
4266
|
-
character: Some(String::from("C")),
|
|
4267
|
-
down: false,
|
|
4268
|
-
modifiers: vec![String::from("Control"), String::from("Shift")],
|
|
4269
|
-
seq: 6,
|
|
4270
|
-
});
|
|
4271
|
-
inner.apply(ControlMessage::Key {
|
|
4272
|
-
name: Some(String::from("ArrowLeft")),
|
|
4273
|
-
character: None,
|
|
4274
|
-
down: true,
|
|
4275
|
-
modifiers: vec![String::from("Alt"), String::from("Meta")],
|
|
4276
|
-
seq: 7,
|
|
4277
|
-
});
|
|
4278
|
-
inner.apply(ControlMessage::Key {
|
|
4279
|
-
name: Some(String::from("ArrowLeft")),
|
|
4280
|
-
character: None,
|
|
4281
|
-
down: false,
|
|
4282
|
-
modifiers: vec![String::from("Alt"), String::from("Meta")],
|
|
4283
|
-
seq: 8,
|
|
4284
|
-
});
|
|
4285
|
-
inner.apply(ControlMessage::Text {
|
|
4286
|
-
text: String::from("C"),
|
|
4287
|
-
seq: 9,
|
|
4288
|
-
});
|
|
4289
|
-
let modifiers = [
|
|
4290
|
-
keycode::LEFT_CTRL,
|
|
4291
|
-
keycode::LEFT_SHIFT,
|
|
4292
|
-
keycode::LEFT_ALT,
|
|
4293
|
-
keycode::LEFT_META,
|
|
4294
|
-
];
|
|
4295
|
-
let strokes = recorded.lock().unwrap().clone();
|
|
4296
|
-
for code in modifiers {
|
|
4297
|
-
assert_eq!(
|
|
4298
|
-
strokes
|
|
4299
|
-
.iter()
|
|
4300
|
-
.filter(|stroke| **stroke == (code, true))
|
|
4301
|
-
.count(),
|
|
4302
|
-
1
|
|
4303
|
-
);
|
|
4304
|
-
assert!(
|
|
4305
|
-
!strokes.contains(&(code, false)),
|
|
4306
|
-
"the separate key still holds {code}"
|
|
4307
|
-
);
|
|
4308
|
-
}
|
|
4309
|
-
for (seq, name) in ["Control", "Shift", "Alt", "Meta"].iter().enumerate() {
|
|
4310
|
-
inner.apply(ControlMessage::Key {
|
|
4311
|
-
name: Some((*name).into()),
|
|
4312
|
-
character: None,
|
|
4313
|
-
down: false,
|
|
4314
|
-
modifiers: Vec::new(),
|
|
4315
|
-
seq: seq as u64 + 10,
|
|
4316
|
-
});
|
|
4317
|
-
}
|
|
4318
|
-
{
|
|
4319
|
-
let strokes = recorded.lock().unwrap();
|
|
4320
|
-
for code in modifiers {
|
|
4321
|
-
assert_eq!(
|
|
4322
|
-
strokes
|
|
4323
|
-
.iter()
|
|
4324
|
-
.filter(|stroke| **stroke == (code, false))
|
|
4325
|
-
.count(),
|
|
4326
|
-
1
|
|
4327
|
-
);
|
|
4328
|
-
}
|
|
4329
|
-
}
|
|
4330
|
-
inner.apply(ControlMessage::Key {
|
|
4331
|
-
name: Some(String::from("ArrowLeft")),
|
|
4332
|
-
character: None,
|
|
4333
|
-
down: true,
|
|
4334
|
-
modifiers: vec![String::from("Control")],
|
|
4335
|
-
seq: 14,
|
|
4336
|
-
});
|
|
4337
|
-
inner.apply(ControlMessage::Key {
|
|
4338
|
-
name: Some(String::from("ArrowLeft")),
|
|
4339
|
-
character: None,
|
|
4340
|
-
down: false,
|
|
4341
|
-
modifiers: vec![String::from("Control")],
|
|
4342
|
-
seq: 15,
|
|
4343
|
-
});
|
|
4344
|
-
{
|
|
4345
|
-
let strokes = recorded.lock().unwrap();
|
|
4346
|
-
assert_eq!(
|
|
4347
|
-
strokes
|
|
4348
|
-
.iter()
|
|
4349
|
-
.filter(|stroke| **stroke == (keycode::LEFT_CTRL, true))
|
|
4350
|
-
.count(),
|
|
4351
|
-
2
|
|
4352
|
-
);
|
|
4353
|
-
assert_eq!(
|
|
4354
|
-
strokes
|
|
4355
|
-
.iter()
|
|
4356
|
-
.filter(|stroke| **stroke == (keycode::LEFT_CTRL, false))
|
|
4357
|
-
.count(),
|
|
4358
|
-
2
|
|
4359
|
-
);
|
|
4360
|
-
}
|
|
4361
|
-
inner.apply(ControlMessage::Key {
|
|
4362
|
-
name: Some(String::from("Control")),
|
|
4363
|
-
character: None,
|
|
4364
|
-
down: true,
|
|
4365
|
-
modifiers: Vec::new(),
|
|
4366
|
-
seq: 16,
|
|
4367
|
-
});
|
|
4368
|
-
for seq in [17, 18] {
|
|
4369
|
-
inner.apply(ControlMessage::Key {
|
|
4370
|
-
name: None,
|
|
4371
|
-
character: Some(String::from("c")),
|
|
4372
|
-
down: true,
|
|
4373
|
-
modifiers: vec![String::from("Control")],
|
|
4374
|
-
seq,
|
|
4375
|
-
});
|
|
4376
|
-
}
|
|
4377
|
-
inner.apply(ControlMessage::Key {
|
|
4378
|
-
name: Some(String::from("Control")),
|
|
4379
|
-
character: None,
|
|
4380
|
-
down: false,
|
|
4381
|
-
modifiers: Vec::new(),
|
|
4382
|
-
seq: 19,
|
|
4383
|
-
});
|
|
4384
|
-
assert!(!recorded
|
|
4385
|
-
.lock()
|
|
4386
|
-
.unwrap()
|
|
4387
|
-
.iter()
|
|
4388
|
-
.rev()
|
|
4389
|
-
.take(2)
|
|
4390
|
-
.any(|event| *event == (keycode::LEFT_CTRL, false)));
|
|
4391
|
-
inner.apply(ControlMessage::Key {
|
|
4392
|
-
name: None,
|
|
4393
|
-
character: Some(String::from("c")),
|
|
4394
|
-
down: false,
|
|
4395
|
-
modifiers: Vec::new(),
|
|
4396
|
-
seq: 20,
|
|
4397
|
-
});
|
|
4398
|
-
inner.apply(ControlMessage::Key {
|
|
4399
|
-
name: Some(String::from("ArrowLeft")),
|
|
4400
|
-
character: None,
|
|
4401
|
-
down: true,
|
|
4402
|
-
modifiers: vec![String::from("Shift")],
|
|
4403
|
-
seq: 21,
|
|
4404
|
-
});
|
|
4405
|
-
inner.apply(ControlMessage::Key {
|
|
4406
|
-
name: Some(String::from("ArrowLeft")),
|
|
4407
|
-
character: None,
|
|
4408
|
-
down: false,
|
|
4409
|
-
modifiers: Vec::new(),
|
|
4410
|
-
seq: 22,
|
|
4411
|
-
});
|
|
4412
|
-
let strokes = recorded.lock().unwrap();
|
|
4413
|
-
assert_eq!(
|
|
4414
|
-
strokes
|
|
4415
|
-
.iter()
|
|
4416
|
-
.filter(|stroke| **stroke == (keycode::LEFT_CTRL, true))
|
|
4417
|
-
.count(),
|
|
4418
|
-
3
|
|
4419
|
-
);
|
|
4420
|
-
assert_eq!(
|
|
4421
|
-
strokes
|
|
4422
|
-
.iter()
|
|
4423
|
-
.filter(|stroke| **stroke == (keycode::LEFT_CTRL, false))
|
|
4424
|
-
.count(),
|
|
4425
|
-
3
|
|
4426
|
-
);
|
|
4427
|
-
assert_eq!(
|
|
4428
|
-
strokes
|
|
4429
|
-
.iter()
|
|
4430
|
-
.filter(|stroke| **stroke == (keycode::LEFT_SHIFT, false))
|
|
4431
|
-
.count(),
|
|
4432
|
-
2
|
|
4433
|
-
);
|
|
4434
|
-
}
|
|
4435
|
-
|
|
4436
|
-
#[tokio::test]
|
|
4437
|
-
async fn x11_capture_does_not_refuse_clipboard() {
|
|
4438
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
4439
|
-
let (mut inner, _recorded) = test_inner(events).await;
|
|
4440
|
-
let owned = Arc::get_mut(&mut inner).unwrap();
|
|
4441
|
-
owned.source.source_type = Some(String::from("x11-root"));
|
|
4442
|
-
#[cfg(target_os = "linux")]
|
|
4443
|
-
assert_eq!(inner.clipboard_refusal(), None);
|
|
4444
|
-
Arc::get_mut(&mut inner).unwrap().source.source_type = Some(String::from("encoded"));
|
|
4445
|
-
assert_eq!(
|
|
4446
|
-
inner.clipboard_refusal().unwrap().0,
|
|
4447
|
-
"clipboard-unsupported"
|
|
4448
|
-
);
|
|
4449
|
-
}
|
|
4450
|
-
|
|
4451
|
-
#[tokio::test]
|
|
4452
|
-
async fn a_stopped_capture_never_serves_the_last_frame() {
|
|
4453
|
-
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
4454
|
-
let (inner, _) = test_inner(events).await;
|
|
4455
|
-
let session = Session { inner };
|
|
4456
|
-
*lock(&session.inner.latest) =
|
|
4457
|
-
Some((1, Instant::now(), 2, 2, vec![0; 16], vec![1], vec![]));
|
|
4458
|
-
*lock(&session.inner.capture_error) = Some(String::from("SCStreamErrorDomain -3821"));
|
|
4459
|
-
assert_eq!(
|
|
4460
|
-
session.frame(None, "", None).unwrap_err().code,
|
|
4461
|
-
"stream-stopped"
|
|
4462
|
-
);
|
|
4463
|
-
assert_eq!(
|
|
4464
|
-
session.wait_frame(None, None, 10).await.unwrap_err().code,
|
|
4465
|
-
"stream-stopped"
|
|
4466
|
-
);
|
|
4467
|
-
}
|
|
4468
|
-
|
|
4469
|
-
#[tokio::test]
|
|
4470
|
-
async fn a_frame_the_encoder_refuses_ends_the_session_with_a_reason() {
|
|
4471
|
-
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4472
|
-
let (inner, _recorded) = test_inner(events).await;
|
|
4473
|
-
let (frame_tx, frame_rx) = latest_frame();
|
|
4474
|
-
spawn_pipeline(
|
|
4475
|
-
&inner,
|
|
4476
|
-
current_peer(&inner).expect("the test's peer"),
|
|
4477
|
-
frame_rx,
|
|
4478
|
-
Arc::new(AtomicBool::new(true)),
|
|
4479
|
-
30,
|
|
4480
|
-
1000,
|
|
4481
|
-
);
|
|
4482
|
-
|
|
4483
|
-
// The encoder is 64x64; a 32x32 frame is the dimension mismatch that
|
|
4484
|
-
// used to be counted and dropped behind a permanently black picture.
|
|
4485
|
-
frame_tx.put(
|
|
4486
|
-
I420 {
|
|
4487
|
-
width: 32,
|
|
4488
|
-
height: 32,
|
|
4489
|
-
data: vec![128u8; 32 * 32 + 2 * 16 * 16],
|
|
4490
|
-
},
|
|
4491
|
-
Instant::now(),
|
|
4492
|
-
);
|
|
4493
|
-
tokio::time::sleep(Duration::from_millis(150)).await;
|
|
4494
|
-
|
|
4495
|
-
let mut reason = None;
|
|
4496
|
-
while let Ok(event) = received.try_recv() {
|
|
4497
|
-
if let Notice {
|
|
4498
|
-
event: SessionEvent::Revoked { reason: why, code },
|
|
4499
|
-
..
|
|
4500
|
-
} = event
|
|
4501
|
-
{
|
|
4502
|
-
assert_eq!(code, "error", "a refused frame is a fault, not a lost path");
|
|
4503
|
-
reason = Some(why);
|
|
4504
|
-
}
|
|
4505
|
-
}
|
|
4506
|
-
let reason = reason.expect("a refused frame must surface as a revocation");
|
|
4507
|
-
assert_eq!(
|
|
4508
|
-
reason, "the encoder rejected a frame",
|
|
4509
|
-
"the reason is a stable token the client can map to copy",
|
|
4510
|
-
);
|
|
4511
|
-
assert!(
|
|
4512
|
-
inner.revoked_reason().is_some(),
|
|
4513
|
-
"the session is closed, not left black"
|
|
4514
|
-
);
|
|
4515
|
-
}
|
|
4516
|
-
|
|
4517
|
-
/// A consumer-fed stream: a keyframe carrying SPS, PPS and an IDR slice.
|
|
4518
|
-
fn fed_keyframe() -> Vec<u8> {
|
|
4519
|
-
let mut stream = vec![0x00, 0x00, 0x00, 0x01];
|
|
4520
|
-
stream.extend_from_slice(&[0x67, 0x42, 0xc0, 0x29, 0x8c, 0x8d]);
|
|
4521
|
-
stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x68, 0xce, 0x3c, 0x80]);
|
|
4522
|
-
stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x00]);
|
|
4523
|
-
stream
|
|
4524
|
-
}
|
|
4525
|
-
|
|
4526
|
-
fn encoded_open() -> OpenRequest {
|
|
4527
|
-
OpenRequest {
|
|
4528
|
-
cursor: crate::protocol::CursorMode::Embedded,
|
|
4529
|
-
source: Some(SourceRequest::Encoded {
|
|
4530
|
-
codec: EncodedCodec::H264,
|
|
4531
|
-
width: 486,
|
|
4532
|
-
height: 1080,
|
|
4533
|
-
}),
|
|
4534
|
-
max_width: 3840,
|
|
4535
|
-
max_height: 2160,
|
|
4536
|
-
bitrate_kbps: 0,
|
|
4537
|
-
max_fps: 30,
|
|
4538
|
-
ice_servers: Vec::new(),
|
|
4539
|
-
restore_token: None,
|
|
4540
|
-
ttl: Some(Duration::from_secs(30)),
|
|
4541
|
-
loopback_tcp: false,
|
|
4542
|
-
agent_indicator: false,
|
|
4543
|
-
local_frames: true,
|
|
4544
|
-
audio: false,
|
|
4545
|
-
observe_input: false,
|
|
4546
|
-
}
|
|
4547
|
-
}
|
|
4548
|
-
|
|
4549
|
-
async fn next_notice(received: &mut tokio_mpsc::UnboundedReceiver<Notice>) -> Notice {
|
|
4550
|
-
tokio::time::timeout(Duration::from_secs(5), received.recv())
|
|
4551
|
-
.await
|
|
4552
|
-
.expect("a notification arrives")
|
|
4553
|
-
.expect("the channel stays open")
|
|
4554
|
-
}
|
|
4555
|
-
|
|
4556
|
-
#[tokio::test]
|
|
4557
|
-
async fn hidden_cursor_is_refused_for_consumer_encoded_pixels() {
|
|
4558
|
-
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
4559
|
-
let mut request = encoded_open();
|
|
4560
|
-
request.cursor = crate::protocol::CursorMode::Hidden;
|
|
4561
|
-
let error = Session::open(request, events)
|
|
4562
|
-
.await
|
|
4563
|
-
.err()
|
|
4564
|
-
.expect("cannot alter encoded pixels");
|
|
4565
|
-
assert_eq!(error.code, "cursor-unavailable");
|
|
4566
|
-
}
|
|
4567
|
-
|
|
4568
|
-
#[tokio::test]
|
|
4569
|
-
async fn an_encoded_session_opens_without_capture_encoder_or_input() {
|
|
4570
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
4571
|
-
let session = Session::open(encoded_open(), events)
|
|
4572
|
-
.await
|
|
4573
|
-
.expect("an encoded source needs no display and no input device");
|
|
4574
|
-
let inner = &session.inner;
|
|
4575
|
-
assert_eq!(session.restore_source(), "encoded");
|
|
4576
|
-
assert_eq!(session.geometry()["encoded"]["width"], 486);
|
|
4577
|
-
assert_eq!(session.geometry()["encoded"]["height"], 1080);
|
|
4578
|
-
assert!(
|
|
4579
|
-
inner.encoder.lock().unwrap().is_none(),
|
|
4580
|
-
"the consumer is the encoder for this source"
|
|
4581
|
-
);
|
|
4582
|
-
assert!(
|
|
4583
|
-
inner.capture.lock().unwrap().is_none(),
|
|
4584
|
-
"there is nothing to capture"
|
|
4585
|
-
);
|
|
4586
|
-
assert!(
|
|
4587
|
-
inner.input.lock().unwrap().is_none(),
|
|
4588
|
-
"the client's input belongs to the consumer's device"
|
|
4589
|
-
);
|
|
4590
|
-
assert!(
|
|
4591
|
-
inner.peer.lock().unwrap().is_none(),
|
|
4592
|
-
"the offer waits for the stream's own SPS"
|
|
4593
|
-
);
|
|
4594
|
-
}
|
|
4595
|
-
|
|
4596
|
-
#[tokio::test]
|
|
4597
|
-
async fn the_offer_for_a_fed_stream_names_that_streams_own_profile() {
|
|
4598
|
-
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4599
|
-
let session = Session::open(encoded_open(), events)
|
|
4600
|
-
.await
|
|
4601
|
-
.expect("an encoded session");
|
|
4602
|
-
let first = next_notice(&mut received).await;
|
|
4603
|
-
assert!(
|
|
4604
|
-
matches!(
|
|
4605
|
-
first.event,
|
|
4606
|
-
SessionEvent::State {
|
|
4607
|
-
capture: "consented",
|
|
4608
|
-
..
|
|
4609
|
-
}
|
|
4610
|
-
),
|
|
4611
|
-
"the session is open before any offer exists: {:?}",
|
|
4612
|
-
first.event,
|
|
4613
|
-
);
|
|
4614
|
-
|
|
4615
|
-
session
|
|
4616
|
-
.feed(true, fed_keyframe())
|
|
4617
|
-
.expect("a keyframe is taken");
|
|
4618
|
-
let offered = loop {
|
|
4619
|
-
let notice = next_notice(&mut received).await;
|
|
4620
|
-
if let SessionEvent::Description { sdp, .. } = notice.event {
|
|
4621
|
-
break sdp;
|
|
4622
|
-
}
|
|
4623
|
-
};
|
|
4624
|
-
assert!(
|
|
4625
|
-
offered.contains("H264/90000"),
|
|
4626
|
-
"the fed track is offered as H.264: {offered}"
|
|
4627
|
-
);
|
|
4628
|
-
assert!(
|
|
4629
|
-
offered.contains("profile-level-id=42c029"),
|
|
4630
|
-
"the profile-level-id comes from the fed SPS, not a default: {offered}"
|
|
4631
|
-
);
|
|
4632
|
-
assert_eq!(session.metrics().fed_frames, 1);
|
|
4633
|
-
}
|
|
4634
|
-
|
|
4635
|
-
#[tokio::test]
|
|
4636
|
-
async fn a_fed_stream_forwards_its_controls_instead_of_applying_them() {
|
|
4637
|
-
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4638
|
-
let session = Session::open(encoded_open(), events)
|
|
4639
|
-
.await
|
|
4640
|
-
.expect("an encoded session");
|
|
4641
|
-
session.inner.control_open.store(true, Ordering::SeqCst);
|
|
4642
|
-
|
|
4643
|
-
session.inner.apply(ControlMessage::Pointer {
|
|
4644
|
-
phase: PointerPhase::Down,
|
|
4645
|
-
x: 10,
|
|
4646
|
-
y: 20,
|
|
4647
|
-
button: 1,
|
|
4648
|
-
seq: 1,
|
|
4649
|
-
});
|
|
4650
|
-
|
|
4651
|
-
let forwarded = loop {
|
|
4652
|
-
let notice = next_notice(&mut received).await;
|
|
4653
|
-
if let SessionEvent::Input { input } = notice.event {
|
|
4654
|
-
break input;
|
|
4655
|
-
}
|
|
4656
|
-
};
|
|
4657
|
-
assert_eq!(forwarded["kind"], "pointer");
|
|
4658
|
-
assert_eq!(forwarded["phase"], "down");
|
|
4659
|
-
assert_eq!(forwarded["x"], 10);
|
|
4660
|
-
assert_eq!(forwarded["seq"], 1);
|
|
4661
|
-
assert_eq!(session.metrics().input_forwarded, 1);
|
|
4662
|
-
assert_eq!(
|
|
4663
|
-
session.metrics().input_applied,
|
|
4664
|
-
0,
|
|
4665
|
-
"nothing was applied to this machine"
|
|
4666
|
-
);
|
|
4667
|
-
|
|
4668
|
-
// The stream's own coordinates are what travels, so an encoded surface
|
|
4669
|
-
// cannot be driven by a coordinate the far end never rendered.
|
|
4670
|
-
session.inner.apply(ControlMessage::Pointer {
|
|
4671
|
-
phase: PointerPhase::Move,
|
|
4672
|
-
x: 9_000,
|
|
4673
|
-
y: 20,
|
|
4674
|
-
button: 1,
|
|
4675
|
-
seq: 2,
|
|
4676
|
-
});
|
|
4677
|
-
assert_eq!(session.metrics().input_forwarded, 2);
|
|
4678
|
-
}
|
|
4679
|
-
|
|
4680
|
-
#[tokio::test]
|
|
4681
|
-
async fn a_capture_session_refuses_a_fed_stream() {
|
|
4682
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
4683
|
-
let (inner, _recorded) = test_inner(events).await;
|
|
4684
|
-
let session = Session { inner };
|
|
4685
|
-
assert_eq!(
|
|
4686
|
-
session.feed(true, fed_keyframe()).unwrap_err().code,
|
|
4687
|
-
"operation",
|
|
4688
|
-
"only an encoded source takes access units",
|
|
4689
|
-
);
|
|
4690
|
-
}
|
|
4691
|
-
|
|
4692
|
-
#[tokio::test]
|
|
4693
|
-
async fn an_encoded_session_has_no_frame_to_read() {
|
|
4694
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
4695
|
-
let session = Session::open(encoded_open(), events)
|
|
4696
|
-
.await
|
|
4697
|
-
.expect("an encoded session");
|
|
4698
|
-
assert_eq!(session.frame(None, "", None).unwrap_err().code, "operation");
|
|
4699
|
-
assert_eq!(
|
|
4700
|
-
session.wait_frame(None, None, 10).await.unwrap_err().code,
|
|
4701
|
-
"operation"
|
|
4702
|
-
);
|
|
4703
|
-
assert_eq!(
|
|
4704
|
-
session.read_clipboard().await.unwrap_err(),
|
|
4705
|
-
"clipboard is unavailable for this desktop source",
|
|
4706
|
-
);
|
|
4707
|
-
}
|
|
4708
|
-
|
|
4709
|
-
#[tokio::test]
|
|
4710
|
-
async fn three_queued_ice_restarts_reemit_one_offer_and_accept_its_answer() {
|
|
4711
|
-
let codec = crate::peer::tests::h264_receiver_codec();
|
|
4712
|
-
let (peer, _) = crate::peer::tests::answered(vec![codec.clone()]).await;
|
|
4713
|
-
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4714
|
-
let (inner, _) = test_inner_with(events, Arc::new(peer), 64, 64);
|
|
4715
|
-
let session = Session { inner };
|
|
4716
|
-
let mut offers = Vec::new();
|
|
4717
|
-
for _ in 0..3 {
|
|
4718
|
-
session.restart_ice().await.expect("restart accepted");
|
|
4719
|
-
match received.recv().await.expect("offer event").event {
|
|
4720
|
-
SessionEvent::Description { sdp, .. } => offers.push(sdp),
|
|
4721
|
-
_ => panic!("expected the pending offer"),
|
|
4722
|
-
}
|
|
4723
|
-
}
|
|
4724
|
-
let ufrag = |sdp: &str| {
|
|
4725
|
-
sdp.lines()
|
|
4726
|
-
.find_map(|line| line.strip_prefix("a=ice-ufrag:"))
|
|
4727
|
-
.expect("ICE credentials")
|
|
4728
|
-
.to_owned()
|
|
4729
|
-
};
|
|
4730
|
-
assert!(offers.iter().all(|sdp| ufrag(sdp) == ufrag(&offers[0])));
|
|
4731
|
-
let client = crate::peer::tests::answerer_with(&offers[0], vec![codec]).await;
|
|
4732
|
-
let answer = client.create_answer(None).await.expect("client answer");
|
|
4733
|
-
client
|
|
4734
|
-
.set_local_description(answer.clone())
|
|
4735
|
-
.await
|
|
4736
|
-
.expect("local answer");
|
|
4737
|
-
session
|
|
4738
|
-
.accept_answer(answer.sdp)
|
|
4739
|
-
.await
|
|
4740
|
-
.expect("pending answer accepted");
|
|
4741
|
-
client.close().await.expect("close client");
|
|
4742
|
-
session.current_peer().unwrap().close().await;
|
|
4743
|
-
}
|
|
4744
|
-
|
|
4745
|
-
#[tokio::test]
|
|
4746
|
-
async fn a_lost_transport_releases_input_then_ends_the_session_without_sctp_close() {
|
|
4747
|
-
use webrtc::peer_connection::RTCPeerConnectionState as State;
|
|
4748
|
-
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4749
|
-
let (inner, recorded) = test_inner(events).await;
|
|
4750
|
-
#[cfg(target_os = "windows")]
|
|
4751
|
-
inner
|
|
4752
|
-
.with_input(|target| target.chord(1, Vec::new(), true))
|
|
4753
|
-
.unwrap();
|
|
4754
|
-
#[cfg(not(target_os = "windows"))]
|
|
4755
|
-
inner.apply(ControlMessage::Key {
|
|
4756
|
-
name: Some(String::from("Control")),
|
|
4757
|
-
character: None,
|
|
4758
|
-
down: true,
|
|
4759
|
-
modifiers: Vec::new(),
|
|
4760
|
-
seq: 1,
|
|
4761
|
-
});
|
|
4762
|
-
assert!(recorded.lock().unwrap().iter().any(|(_, down)| *down));
|
|
4763
|
-
|
|
4764
|
-
let (peer_tx, peer_rx) = tokio_mpsc::unbounded_channel();
|
|
4765
|
-
spawn_peer_events(&inner, peer_rx);
|
|
4766
|
-
peer_tx.send(PeerEvent::State(State::Disconnected)).unwrap();
|
|
4767
|
-
tokio::time::timeout(Duration::from_secs(2), received.recv())
|
|
4768
|
-
.await
|
|
4769
|
-
.unwrap();
|
|
4770
|
-
assert!(
|
|
4771
|
-
recorded.lock().unwrap().iter().any(|(_, down)| !*down),
|
|
4772
|
-
"a disconnected transport must release held input without SCTP OnClose"
|
|
4773
|
-
);
|
|
4774
|
-
assert!(
|
|
4775
|
-
inner.revoked_reason().is_none(),
|
|
4776
|
-
"a transient loss may recover"
|
|
4777
|
-
);
|
|
4778
|
-
|
|
4779
|
-
peer_tx.send(PeerEvent::State(State::Failed)).unwrap();
|
|
4780
|
-
let mut transport_lost = false;
|
|
4781
|
-
tokio::time::timeout(Duration::from_secs(2), async {
|
|
4782
|
-
while let Some(notice) = received.recv().await {
|
|
4783
|
-
// A lost path is the one revocation a new session can heal.
|
|
4784
|
-
transport_lost |= matches!(
|
|
4785
|
-
notice.event,
|
|
4786
|
-
SessionEvent::Revoked {
|
|
4787
|
-
code: "transport",
|
|
4788
|
-
..
|
|
4789
|
-
}
|
|
4790
|
-
);
|
|
4791
|
-
if matches!(
|
|
4792
|
-
notice.event,
|
|
4793
|
-
SessionEvent::State {
|
|
4794
|
-
capture: "ended",
|
|
4795
|
-
..
|
|
4796
|
-
}
|
|
4797
|
-
) {
|
|
4798
|
-
return;
|
|
4799
|
-
}
|
|
4800
|
-
}
|
|
4801
|
-
panic!("failed transport did not end capture");
|
|
4802
|
-
})
|
|
4803
|
-
.await
|
|
4804
|
-
.unwrap();
|
|
4805
|
-
assert!(
|
|
4806
|
-
transport_lost,
|
|
4807
|
-
"a failed transport must revoke with code transport"
|
|
4808
|
-
);
|
|
4809
|
-
assert!(inner.revoked_reason().is_some());
|
|
4810
|
-
assert!(!inner.pipeline.load(Ordering::SeqCst));
|
|
4811
|
-
assert!(inner.input.lock().unwrap().is_none());
|
|
4812
|
-
}
|
|
4813
|
-
|
|
4814
|
-
#[tokio::test]
|
|
4815
|
-
async fn the_pipeline_caps_the_rate_codes_the_last_frame_and_refines_a_still_desktop_once() {
|
|
4816
|
-
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
4817
|
-
let (inner, _recorded) = test_inner(events).await;
|
|
4818
|
-
let (frame_tx, frame_rx) = latest_frame();
|
|
4819
|
-
spawn_pipeline(
|
|
4820
|
-
&inner,
|
|
4821
|
-
current_peer(&inner).expect("the test's peer"),
|
|
4822
|
-
frame_rx,
|
|
4823
|
-
Arc::new(AtomicBool::new(true)),
|
|
4824
|
-
20,
|
|
4825
|
-
1000,
|
|
4826
|
-
);
|
|
4827
|
-
|
|
4828
|
-
// 40 frames nominally over 200 ms; Windows timers can round each 5 ms
|
|
4829
|
-
// sleep upward, so its cap assertion uses the actual burst duration.
|
|
4830
|
-
#[cfg(target_os = "windows")]
|
|
4831
|
-
let burst_started = Instant::now();
|
|
4832
|
-
for _ in 0..40 {
|
|
4833
|
-
frame_tx.put(
|
|
4834
|
-
I420 {
|
|
4835
|
-
width: 64,
|
|
4836
|
-
height: 64,
|
|
4837
|
-
data: vec![128u8; 64 * 64 + 2 * 32 * 32],
|
|
4838
|
-
},
|
|
4839
|
-
Instant::now(),
|
|
4840
|
-
);
|
|
4841
|
-
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
4842
|
-
}
|
|
4843
|
-
let motion = inner.metrics.lock().unwrap().encoded_frames;
|
|
4844
|
-
#[cfg(target_os = "windows")]
|
|
4845
|
-
let cap = (burst_started.elapsed().as_secs_f64() * 20.0).ceil() as u64 + 3;
|
|
4846
|
-
#[cfg(not(target_os = "windows"))]
|
|
4847
|
-
let cap = 7;
|
|
4848
|
-
assert!(
|
|
4849
|
-
motion <= cap,
|
|
4850
|
-
"the requested rate is a cap, got {motion} of 40 (cap {cap})"
|
|
4851
|
-
);
|
|
4852
|
-
assert!(
|
|
4853
|
-
motion >= 2,
|
|
4854
|
-
"the pipeline must still emit frames, got {motion}"
|
|
4855
|
-
);
|
|
4856
|
-
|
|
4857
|
-
// Still for longer than a refinement takes: the last frame of the burst
|
|
4858
|
-
// is coded (a refinement only follows a coded frame), then refined once.
|
|
4859
|
-
tokio::time::sleep(Duration::from_millis(450)).await;
|
|
4860
|
-
let metrics = inner.metrics.lock().unwrap().clone();
|
|
4861
|
-
assert_eq!(
|
|
4862
|
-
metrics.refined_frames, 1,
|
|
4863
|
-
"a still desktop is refined exactly once"
|
|
4864
|
-
);
|
|
4865
|
-
assert_eq!(metrics.key_frames, 1, "only the first frame is a key frame");
|
|
4866
|
-
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
4867
|
-
assert_eq!(
|
|
4868
|
-
inner.metrics.lock().unwrap().encoded_frames,
|
|
4869
|
-
metrics.encoded_frames,
|
|
4870
|
-
"nothing is sent while still"
|
|
4871
|
-
);
|
|
4872
|
-
|
|
4873
|
-
// A still stream is kept alive, so a receiver never waits long enough
|
|
4874
|
-
// to ask for a key frame; the keepalive is neither a key frame nor
|
|
4875
|
-
// another refinement.
|
|
4876
|
-
tokio::time::sleep(Duration::from_millis(900)).await;
|
|
4877
|
-
let later = inner.metrics.lock().unwrap().clone();
|
|
4878
|
-
assert!(
|
|
4879
|
-
later.encoded_frames > metrics.encoded_frames,
|
|
4880
|
-
"a still stream still sends"
|
|
4881
|
-
);
|
|
4882
|
-
assert_eq!((later.key_frames, later.refined_frames), (1, 1));
|
|
4883
|
-
assert_eq!((later.codec, later.encoder), ("vp9", "libvpx"));
|
|
4884
|
-
}
|
|
4885
|
-
|
|
4886
|
-
/// The rate target after 3 s of delivery jitter on a connected pair: a queue
|
|
4887
|
-
/// that alternates between 5 and 30 ms with nothing lost, and 800 kbps
|
|
4888
|
-
/// delivered. The pipeline runs for real; only the path reports are fed.
|
|
4889
|
-
async fn target_under_bridge_jitter(tcp: bool) -> u32 {
|
|
4890
|
-
let peers = crate::peer::tests::connected(
|
|
4891
|
-
if tcp {
|
|
4892
|
-
Vec::new()
|
|
4893
|
-
} else {
|
|
4894
|
-
vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))]
|
|
4895
|
-
},
|
|
4896
|
-
if tcp {
|
|
4897
|
-
vec![std::net::SocketAddr::from(([127, 0, 0, 1], 0))]
|
|
4898
|
-
} else {
|
|
4899
|
-
Vec::new()
|
|
4900
|
-
},
|
|
4901
|
-
tcp,
|
|
4902
|
-
)
|
|
4903
|
-
.await;
|
|
4904
|
-
let engine = Arc::clone(&peers.engine);
|
|
4905
|
-
assert_eq!(
|
|
4906
|
-
engine.selected_pair_is_tcp().await,
|
|
4907
|
-
tcp,
|
|
4908
|
-
"the selected pair must run over {}",
|
|
4909
|
-
if tcp { "TCP" } else { "UDP" }
|
|
4910
|
-
);
|
|
4911
|
-
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
4912
|
-
let (inner, _recorded) = test_inner_with(events, Arc::clone(&engine), 64, 64);
|
|
4913
|
-
let (_frame_tx, frame_rx) = latest_frame();
|
|
4914
|
-
spawn_pipeline(
|
|
4915
|
-
&inner,
|
|
4916
|
-
Arc::clone(&engine),
|
|
4917
|
-
frame_rx,
|
|
4918
|
-
Arc::new(AtomicBool::new(true)),
|
|
4919
|
-
30,
|
|
4920
|
-
1000,
|
|
4921
|
-
);
|
|
4922
|
-
for step in 0..300 {
|
|
4923
|
-
let queue_delay = Duration::from_millis(if step % 2 == 0 { 5 } else { 30 });
|
|
4924
|
-
engine.report_path(PathReport {
|
|
4925
|
-
queue_delay,
|
|
4926
|
-
delivered_kbps: 800,
|
|
4927
|
-
});
|
|
4928
|
-
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
4929
|
-
}
|
|
4930
|
-
let target = inner.metrics.lock().unwrap().target_kbps;
|
|
4931
|
-
drop(peers);
|
|
4932
|
-
target
|
|
4933
|
-
}
|
|
4934
|
-
|
|
4935
|
-
#[tokio::test(flavor = "multi_thread")]
|
|
4936
|
-
async fn a_tcp_path_with_bridge_jitter_holds_its_rate_ceiling() {
|
|
4937
|
-
assert_eq!(
|
|
4938
|
-
target_under_bridge_jitter(true).await,
|
|
4939
|
-
1000,
|
|
4940
|
-
"a 5-30 ms jitter that is not a queue must not back a TCP path off"
|
|
4941
|
-
);
|
|
4942
|
-
}
|
|
4943
|
-
|
|
4944
|
-
#[tokio::test(flavor = "multi_thread")]
|
|
4945
|
-
async fn a_udp_path_with_the_same_jitter_still_backs_off() {
|
|
4946
|
-
assert!(
|
|
4947
|
-
target_under_bridge_jitter(false).await < 1000,
|
|
4948
|
-
"a UDP path reads the same 30 ms as a standing queue and must back off"
|
|
4949
|
-
);
|
|
4950
|
-
}
|
|
4951
|
-
|
|
4952
|
-
#[tokio::test]
|
|
4953
|
-
async fn a_receiver_that_asks_for_h264_is_sent_h264_from_the_first_frame() {
|
|
4954
|
-
let Some(info) = tokio::task::spawn_blocking(crate::h264::probe)
|
|
4955
|
-
.await
|
|
4956
|
-
.unwrap()
|
|
4957
|
-
else {
|
|
4958
|
-
eprintln!("skipped: no H.264 encoder starts on this machine");
|
|
4959
|
-
return;
|
|
4960
|
-
};
|
|
4961
|
-
let (peer, _answer) =
|
|
4962
|
-
crate::peer::tests::answered(vec![crate::peer::tests::h264_receiver_codec()]).await;
|
|
4963
|
-
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
4964
|
-
let (inner, _recorded) = test_inner_with(events, Arc::new(peer), 320, 240);
|
|
4965
|
-
// The answer is real, so ICE runs and, with no candidates, never
|
|
4966
|
-
// connects; each state it reports would take back `assume_connected`.
|
|
4967
|
-
let pinned = current_peer(&inner).expect("the test's peer");
|
|
4968
|
-
let pin = tokio::spawn(async move {
|
|
4969
|
-
loop {
|
|
4970
|
-
pinned.assume_connected();
|
|
4971
|
-
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
4972
|
-
}
|
|
4973
|
-
});
|
|
4974
|
-
let (frame_tx, frame_rx) = latest_frame();
|
|
4975
|
-
spawn_pipeline(
|
|
4976
|
-
&inner,
|
|
4977
|
-
current_peer(&inner).expect("the test's peer"),
|
|
4978
|
-
frame_rx,
|
|
4979
|
-
Arc::new(AtomicBool::new(true)),
|
|
4980
|
-
20,
|
|
4981
|
-
1000,
|
|
4982
|
-
);
|
|
4983
|
-
for shade in 0..10u8 {
|
|
4984
|
-
frame_tx.put(
|
|
4985
|
-
I420 {
|
|
4986
|
-
width: 320,
|
|
4987
|
-
height: 240,
|
|
4988
|
-
data: vec![shade * 20; 320 * 240 * 3 / 2],
|
|
4989
|
-
},
|
|
4990
|
-
Instant::now(),
|
|
4991
|
-
);
|
|
4992
|
-
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
4993
|
-
}
|
|
4994
|
-
// Opening a hardware session can take a good part of a second, more
|
|
4995
|
-
// with other tests holding the GPU: wait for the still to be refined.
|
|
4996
|
-
let deadline = Instant::now() + Duration::from_secs(10);
|
|
4997
|
-
while inner.metrics.lock().unwrap().refined_frames == 0 && Instant::now() < deadline {
|
|
4998
|
-
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
4999
|
-
}
|
|
5000
|
-
let metrics = inner.metrics.lock().unwrap().clone();
|
|
5001
|
-
assert_eq!((metrics.codec, metrics.encoder), ("h264", info.encoder));
|
|
5002
|
-
assert!(metrics.encoded_frames >= 2, "{metrics:?}");
|
|
5003
|
-
assert_eq!(
|
|
5004
|
-
metrics.key_frames, 1,
|
|
5005
|
-
"one IDR, then inter frames: {metrics:?}"
|
|
5006
|
-
);
|
|
5007
|
-
assert_eq!(metrics.refined_frames, 1, "a still desktop is refined once");
|
|
5008
|
-
assert_eq!(
|
|
5009
|
-
metrics.halved_frames, 0,
|
|
5010
|
-
"H.264 is always coded at full size"
|
|
5011
|
-
);
|
|
5012
|
-
pin.abort();
|
|
5013
|
-
inner.close("done", "closed").await;
|
|
5014
|
-
assert!(
|
|
5015
|
-
lock(&inner.encoder).is_none(),
|
|
5016
|
-
"closing releases the encoder"
|
|
5017
|
-
);
|
|
5018
|
-
}
|
|
5019
4046
|
}
|