@desklink/host 0.5.15 → 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/docs/PROTOCOL.md +3 -1
- package/engine/Cargo.lock +1 -1
- package/engine/Cargo.toml +1 -1
- package/engine/src/capture.rs +0 -79
- 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/main.rs +2 -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 +52 -1063
- package/package.json +3 -3
package/engine/src/session.rs
CHANGED
|
@@ -512,6 +512,9 @@ struct InputTarget {
|
|
|
512
512
|
|
|
513
513
|
enum Applier {
|
|
514
514
|
Uinput(InputDevices),
|
|
515
|
+
/// Keyboard only: the portal grants keys, not a pointer.
|
|
516
|
+
#[cfg(target_os = "linux")]
|
|
517
|
+
PortalKeys(crate::input_portal::PortalKeys),
|
|
515
518
|
X11(Arc<Mutex<X11Desktop>>),
|
|
516
519
|
#[cfg(test)]
|
|
517
520
|
Recording(Arc<Mutex<Vec<(i16, bool)>>>),
|
|
@@ -524,6 +527,8 @@ impl InputTarget {
|
|
|
524
527
|
devices.move_absolute(x, y);
|
|
525
528
|
Ok(())
|
|
526
529
|
}
|
|
530
|
+
#[cfg(target_os = "linux")]
|
|
531
|
+
Applier::PortalKeys(_) => anyhow::bail!(NO_PORTAL_POINTER),
|
|
527
532
|
Applier::X11(desktop) => lock(desktop).move_pointer(x, y),
|
|
528
533
|
#[cfg(test)]
|
|
529
534
|
Applier::Recording(_) => Ok(()),
|
|
@@ -537,6 +542,8 @@ impl InputTarget {
|
|
|
537
542
|
devices.button(button, down);
|
|
538
543
|
Ok(())
|
|
539
544
|
}
|
|
545
|
+
#[cfg(target_os = "linux")]
|
|
546
|
+
Applier::PortalKeys(_) => anyhow::bail!(NO_PORTAL_POINTER),
|
|
540
547
|
Applier::X11(desktop) => lock(desktop).button(x11_button(button), down),
|
|
541
548
|
#[cfg(test)]
|
|
542
549
|
Applier::Recording(log) => {
|
|
@@ -554,6 +561,8 @@ impl InputTarget {
|
|
|
554
561
|
devices.scroll(dx, dy);
|
|
555
562
|
Ok(())
|
|
556
563
|
}
|
|
564
|
+
#[cfg(target_os = "linux")]
|
|
565
|
+
Applier::PortalKeys(_) => anyhow::bail!(NO_PORTAL_POINTER),
|
|
557
566
|
Applier::X11(desktop) => {
|
|
558
567
|
let (rest_x, rest_y) = (self.wheel_rest.0 + dx, self.wheel_rest.1 + dy);
|
|
559
568
|
let (whole_x, whole_y) = (rest_x.trunc(), rest_y.trunc());
|
|
@@ -577,6 +586,8 @@ impl InputTarget {
|
|
|
577
586
|
fn key(&mut self, code: i16, down: bool) -> Result<()> {
|
|
578
587
|
match &mut self.applier {
|
|
579
588
|
Applier::Uinput(devices) => devices.key(code, down),
|
|
589
|
+
#[cfg(target_os = "linux")]
|
|
590
|
+
Applier::PortalKeys(keys) => keys.key(code, down),
|
|
580
591
|
Applier::X11(desktop) => lock(desktop).key(code, down),
|
|
581
592
|
#[cfg(test)]
|
|
582
593
|
Applier::Recording(log) => {
|
|
@@ -675,6 +686,8 @@ impl InputTarget {
|
|
|
675
686
|
for button in buttons {
|
|
676
687
|
match &mut self.applier {
|
|
677
688
|
Applier::Uinput(devices) => devices.button(button, false),
|
|
689
|
+
#[cfg(target_os = "linux")]
|
|
690
|
+
Applier::PortalKeys(_) => {}
|
|
678
691
|
Applier::X11(desktop) => lock(desktop).button(x11_button(button), false)?,
|
|
679
692
|
#[cfg(test)]
|
|
680
693
|
Applier::Recording(log) => {
|
|
@@ -687,6 +700,8 @@ impl InputTarget {
|
|
|
687
700
|
for code in keys {
|
|
688
701
|
match &mut self.applier {
|
|
689
702
|
Applier::Uinput(devices) => devices.key(code, false)?,
|
|
703
|
+
#[cfg(target_os = "linux")]
|
|
704
|
+
Applier::PortalKeys(keys) => keys.key(code, false)?,
|
|
690
705
|
Applier::X11(desktop) => lock(desktop).key(code, false)?,
|
|
691
706
|
#[cfg(test)]
|
|
692
707
|
Applier::Recording(log) => {
|
|
@@ -700,6 +715,9 @@ impl InputTarget {
|
|
|
700
715
|
}
|
|
701
716
|
}
|
|
702
717
|
|
|
718
|
+
#[cfg(target_os = "linux")]
|
|
719
|
+
const NO_PORTAL_POINTER: &str = "pointer input is unavailable: the portal grants the keyboard only";
|
|
720
|
+
|
|
703
721
|
fn x11_button(button: Button) -> u8 {
|
|
704
722
|
match button {
|
|
705
723
|
Button::Left => crate::x11::button::LEFT,
|
|
@@ -942,6 +960,13 @@ fn wayland_clipboard_available() -> bool {
|
|
|
942
960
|
/// What the engine can actually do on this machine right now.
|
|
943
961
|
#[cfg(target_os = "linux")]
|
|
944
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 {
|
|
945
970
|
let session_kind = if std::env::var_os("WAYLAND_DISPLAY").is_some() {
|
|
946
971
|
"wayland"
|
|
947
972
|
} else if std::env::var_os("DISPLAY").is_some() {
|
|
@@ -956,7 +981,10 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
956
981
|
// Asked, not taken: the probe connects to this session's PipeWire, reads the
|
|
957
982
|
// registry and leaves. No portal, no prompt, no capture.
|
|
958
983
|
let audio = crate::audio::probe();
|
|
959
|
-
|
|
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 {
|
|
960
988
|
Ok(()) => (serde_json::json!(true), "granted", serde_json::Value::Null),
|
|
961
989
|
Err(unavailable) => (
|
|
962
990
|
serde_json::json!(false),
|
|
@@ -989,14 +1017,14 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
989
1017
|
},
|
|
990
1018
|
"encode": encode_capabilities(),
|
|
991
1019
|
"input": {
|
|
992
|
-
"mechanism": "inputtino/uinput",
|
|
993
|
-
"pointer": input,
|
|
994
|
-
"wheel": input,
|
|
995
|
-
"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,
|
|
996
1024
|
"text": ["layout-reachable"],
|
|
997
1025
|
"layout": keymap::LayoutNames::from_environment().identity(),
|
|
998
|
-
"unavailable_reason": unavailable,
|
|
999
|
-
"grant": grant_state,
|
|
1026
|
+
"unavailable_reason": if portal_keys { serde_json::Value::Null } else { unavailable },
|
|
1027
|
+
"grant": if portal_keys { "portal" } else { grant_state },
|
|
1000
1028
|
},
|
|
1001
1029
|
"clipboard": { "read": clipboard, "write": clipboard && clipboard::writer_available(), "mime": ["text/plain;charset=utf-8"],
|
|
1002
1030
|
"maxBytes": clipboard::MAX_CLIPBOARD_BYTES },
|
|
@@ -1469,15 +1497,28 @@ impl Session {
|
|
|
1469
1497
|
.input_available()
|
|
1470
1498
|
.then(|| Applier::X11(desktop.clone())),
|
|
1471
1499
|
None => {
|
|
1500
|
+
// uinput as before; only when it cannot be created, the
|
|
1501
|
+
// portal's keyboard (keys only, no host-wide device).
|
|
1472
1502
|
#[cfg(target_os = "linux")]
|
|
1473
|
-
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
|
+
};
|
|
1474
1513
|
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
1475
|
-
let
|
|
1514
|
+
let applier = InputDevices::create_for_display(
|
|
1476
1515
|
source_w as i32,
|
|
1477
1516
|
source_h as i32,
|
|
1478
1517
|
source.node_id,
|
|
1479
|
-
)
|
|
1480
|
-
|
|
1518
|
+
)
|
|
1519
|
+
.ok()
|
|
1520
|
+
.map(Applier::Uinput);
|
|
1521
|
+
applier
|
|
1481
1522
|
}
|
|
1482
1523
|
};
|
|
1483
1524
|
let input = applier.map(|applier| InputTarget {
|
|
@@ -3805,167 +3846,8 @@ fn opaque_id() -> String {
|
|
|
3805
3846
|
|
|
3806
3847
|
#[cfg(test)]
|
|
3807
3848
|
mod tests {
|
|
3808
|
-
#[cfg(target_os = "linux")]
|
|
3809
|
-
#[test]
|
|
3810
|
-
fn x11_cue_mask_restores_only_the_cue_pixels() {
|
|
3811
|
-
use crate::indicator::Cue;
|
|
3812
|
-
let (w, h) = (200usize, 120usize);
|
|
3813
|
-
let at = |x: usize, y: usize| (y * w + x) * 4;
|
|
3814
|
-
let (cx, cy) = (20 + 55, 10 + 55);
|
|
3815
|
-
// Shaped (128) and composited (1) helpers announce different thresholds.
|
|
3816
|
-
for threshold in [128, 1] {
|
|
3817
|
-
let cue = Cue {
|
|
3818
|
-
x: 20,
|
|
3819
|
-
y: 10,
|
|
3820
|
-
size: 110,
|
|
3821
|
-
unit: 1.0,
|
|
3822
|
-
opacity: 1.0,
|
|
3823
|
-
click: -1.0,
|
|
3824
|
-
typed: -1.0,
|
|
3825
|
-
threshold,
|
|
3826
|
-
reveal: 99.0,
|
|
3827
|
-
};
|
|
3828
|
-
let mut mask = CueMask::new(Default::default());
|
|
3829
|
-
let clean: Vec<u8> = (0..w * h * 4).map(|i| (i % 251) as u8).collect();
|
|
3830
|
-
mask.apply(&mut clean.clone(), w, h, vec![]);
|
|
3831
|
-
// Paint the cue's keyline and live content inside the halo.
|
|
3832
|
-
let mut frame = clean.clone();
|
|
3833
|
-
frame[at(cx - 24, cy)..at(cx - 24, cy) + 4].fill(255);
|
|
3834
|
-
frame[at(cx - 5, cy)..at(cx - 5, cy) + 4].fill(7);
|
|
3835
|
-
frame[at(190, 110)..at(190, 110) + 4].fill(9);
|
|
3836
|
-
mask.apply(&mut frame, w, h, vec![cue]);
|
|
3837
|
-
assert_eq!(
|
|
3838
|
-
&frame[at(cx - 24, cy)..at(cx - 24, cy) + 4],
|
|
3839
|
-
&clean[at(cx - 24, cy)..at(cx - 24, cy) + 4]
|
|
3840
|
-
);
|
|
3841
|
-
assert_eq!(
|
|
3842
|
-
&frame[at(cx - 5, cy)..at(cx - 5, cy) + 4],
|
|
3843
|
-
&[7; 4],
|
|
3844
|
-
"the halo interior stays live"
|
|
3845
|
-
);
|
|
3846
|
-
assert_eq!(
|
|
3847
|
-
&frame[at(190, 110)..at(190, 110) + 4],
|
|
3848
|
-
&[9; 4],
|
|
3849
|
-
"pixels away from the cue stay live"
|
|
3850
|
-
);
|
|
3851
|
-
let mut after = clean.clone();
|
|
3852
|
-
after[at(cx - 24, cy)..at(cx - 24, cy) + 4].fill(3);
|
|
3853
|
-
mask.apply(&mut after, w, h, vec![]);
|
|
3854
|
-
assert_eq!(
|
|
3855
|
-
&after[at(cx - 24, cy)..at(cx - 24, cy) + 4],
|
|
3856
|
-
&[3; 4],
|
|
3857
|
-
"a hidden cue masks nothing"
|
|
3858
|
-
);
|
|
3859
|
-
}
|
|
3860
|
-
}
|
|
3861
|
-
|
|
3862
3849
|
use super::*;
|
|
3863
3850
|
|
|
3864
|
-
#[tokio::test]
|
|
3865
|
-
async fn a_closed_encoded_session_ends_the_tasks_its_channels_park() {
|
|
3866
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
3867
|
-
let (peer_events, peer_events_rx) = tokio_mpsc::unbounded_channel::<PeerEvent>();
|
|
3868
|
-
let (feed, feed_rx) = tokio_mpsc::channel::<FedAccessUnit>(FEED_BACKLOG);
|
|
3869
|
-
let inner = Arc::new(Inner {
|
|
3870
|
-
id: String::from("encoded-session"),
|
|
3871
|
-
generation: 1,
|
|
3872
|
-
source: SelectedSource {
|
|
3873
|
-
node_id: 0,
|
|
3874
|
-
width: 640,
|
|
3875
|
-
height: 480,
|
|
3876
|
-
position: None,
|
|
3877
|
-
source_type: Some(String::from("encoded")),
|
|
3878
|
-
origin_x: 0,
|
|
3879
|
-
origin_y: 0,
|
|
3880
|
-
},
|
|
3881
|
-
geometry: serde_json::json!({
|
|
3882
|
-
"source": { "width": 640, "height": 480 },
|
|
3883
|
-
"encoded": { "width": 640, "height": 480 },
|
|
3884
|
-
"origin": { "x": 0, "y": 0 },
|
|
3885
|
-
}),
|
|
3886
|
-
metrics: Arc::new(Mutex::new(Metrics::default())),
|
|
3887
|
-
latest: Arc::new(Mutex::new(None)),
|
|
3888
|
-
capture_error: Arc::new(Mutex::new(None)),
|
|
3889
|
-
frame_changes: tokio::sync::watch::channel(0).1,
|
|
3890
|
-
peer: Mutex::new(None),
|
|
3891
|
-
encoder: Mutex::new(None),
|
|
3892
|
-
input: Mutex::new(None),
|
|
3893
|
-
indicator: Mutex::new(None),
|
|
3894
|
-
point_overlay: Mutex::new(None),
|
|
3895
|
-
#[cfg(target_os = "linux")]
|
|
3896
|
-
point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
|
|
3897
|
-
#[cfg(target_os = "linux")]
|
|
3898
|
-
window: Mutex::new(None),
|
|
3899
|
-
#[cfg(target_os = "linux")]
|
|
3900
|
-
cue_output: Mutex::new(None),
|
|
3901
|
-
#[cfg(target_os = "linux")]
|
|
3902
|
-
point_output: Mutex::new(None),
|
|
3903
|
-
capture: Mutex::new(None),
|
|
3904
|
-
layout: Mutex::new(None),
|
|
3905
|
-
#[cfg(target_os = "linux")]
|
|
3906
|
-
audio: Mutex::new(None),
|
|
3907
|
-
last_seq: Mutex::new(0),
|
|
3908
|
-
control_open: AtomicBool::new(false),
|
|
3909
|
-
closed: AtomicBool::new(false),
|
|
3910
|
-
pipeline: Arc::new(AtomicBool::new(false)),
|
|
3911
|
-
events,
|
|
3912
|
-
encoded: Mutex::new(Some(Encoded {
|
|
3913
|
-
codec: EncodedCodec::H264,
|
|
3914
|
-
transport: TransportOptions {
|
|
3915
|
-
ice_servers: Vec::new(),
|
|
3916
|
-
loopback_tcp: false,
|
|
3917
|
-
pace_bps: 20_000_000.0,
|
|
3918
|
-
},
|
|
3919
|
-
peer_events,
|
|
3920
|
-
feed,
|
|
3921
|
-
})),
|
|
3922
|
-
local_frames: false,
|
|
3923
|
-
cursor_info: serde_json::json!({"mode":"unavailable", "positions":false}),
|
|
3924
|
-
observation: serde_json::json!({"observing": false}),
|
|
3925
|
-
observer: Mutex::new(None),
|
|
3926
|
-
});
|
|
3927
|
-
spawn_peer_events(&inner, peer_events_rx);
|
|
3928
|
-
spawn_encoded(&inner, feed_rx);
|
|
3929
|
-
inner.close("test", "closed").await;
|
|
3930
|
-
for _ in 0..100 {
|
|
3931
|
-
if Arc::strong_count(&inner) == 1 {
|
|
3932
|
-
break;
|
|
3933
|
-
}
|
|
3934
|
-
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
3935
|
-
}
|
|
3936
|
-
assert_eq!(
|
|
3937
|
-
Arc::strong_count(&inner),
|
|
3938
|
-
1,
|
|
3939
|
-
"closing must end the tasks whose channels the close takes away",
|
|
3940
|
-
);
|
|
3941
|
-
}
|
|
3942
|
-
|
|
3943
|
-
#[test]
|
|
3944
|
-
fn an_encoded_surface_pixel_maps_to_the_sources_own_pixel() {
|
|
3945
|
-
let source = SelectedSource {
|
|
3946
|
-
node_id: 0,
|
|
3947
|
-
width: 2560,
|
|
3948
|
-
height: 1440,
|
|
3949
|
-
position: Some((1920, 0)),
|
|
3950
|
-
source_type: None,
|
|
3951
|
-
origin_x: 1920,
|
|
3952
|
-
origin_y: 0,
|
|
3953
|
-
};
|
|
3954
|
-
// A tap at the centre of a 1280x720 surface must reach the centre of the
|
|
3955
|
-
// 2560x1440 source, not two thirds of the way there; a tap in the right
|
|
3956
|
-
// half must stay source-local rather than land on a layout origin the
|
|
3957
|
-
// applier would clamp away.
|
|
3958
|
-
assert_eq!(
|
|
3959
|
-
to_source_pixels(640, 360, (1280, 720), &source),
|
|
3960
|
-
(1280, 720)
|
|
3961
|
-
);
|
|
3962
|
-
assert_eq!(
|
|
3963
|
-
to_source_pixels(1279, 719, (1280, 720), &source),
|
|
3964
|
-
(2558, 1438)
|
|
3965
|
-
);
|
|
3966
|
-
assert_eq!(to_source_pixels(0, 0, (1280, 720), &source), (0, 0));
|
|
3967
|
-
}
|
|
3968
|
-
|
|
3969
3851
|
async fn test_inner(
|
|
3970
3852
|
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
3971
3853
|
) -> (Arc<Inner>, Arc<Mutex<Vec<(i16, bool)>>>) {
|
|
@@ -3984,28 +3866,6 @@ mod tests {
|
|
|
3984
3866
|
test_inner_with(events, Arc::new(peer), 64, 64)
|
|
3985
3867
|
}
|
|
3986
3868
|
|
|
3987
|
-
#[cfg(target_os = "linux")]
|
|
3988
|
-
#[tokio::test]
|
|
3989
|
-
async fn a_wayland_portal_session_reports_typed_point_unsupported() {
|
|
3990
|
-
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
3991
|
-
let (inner, _) = test_inner(events).await;
|
|
3992
|
-
let session = Session { inner };
|
|
3993
|
-
|
|
3994
|
-
let error = session
|
|
3995
|
-
.point(&crate::protocol::PointParams {
|
|
3996
|
-
session_id: String::from("test-session"),
|
|
3997
|
-
x: Some(10),
|
|
3998
|
-
y: Some(20),
|
|
3999
|
-
label: String::new(),
|
|
4000
|
-
timeout_ms: None,
|
|
4001
|
-
clear: false,
|
|
4002
|
-
})
|
|
4003
|
-
.expect_err("compositors without layer-shell do not support point overlays");
|
|
4004
|
-
|
|
4005
|
-
assert_eq!(error.code, "point_unsupported");
|
|
4006
|
-
assert!(error.message.contains("reason: layer_shell_unavailable"));
|
|
4007
|
-
}
|
|
4008
|
-
|
|
4009
3869
|
// Driven only on the private headless compositor created by point-wayland-flow.py.
|
|
4010
3870
|
#[cfg(target_os = "linux")]
|
|
4011
3871
|
#[tokio::test]
|
|
@@ -4183,875 +4043,4 @@ mod tests {
|
|
|
4183
4043
|
});
|
|
4184
4044
|
(inner, recorded)
|
|
4185
4045
|
}
|
|
4186
|
-
|
|
4187
|
-
#[tokio::test]
|
|
4188
|
-
async fn paired_input_without_a_backend_is_an_action_failure() {
|
|
4189
|
-
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
4190
|
-
let (inner, recorded) = test_inner(events).await;
|
|
4191
|
-
*lock(&inner.input) = None;
|
|
4192
|
-
assert_eq!(
|
|
4193
|
-
inner.with_input(|_| Ok(())).unwrap_err().0,
|
|
4194
|
-
"input-unavailable"
|
|
4195
|
-
);
|
|
4196
|
-
inner.apply(ControlMessage::Pointer {
|
|
4197
|
-
phase: PointerPhase::Move,
|
|
4198
|
-
x: 10,
|
|
4199
|
-
y: 20,
|
|
4200
|
-
button: 1,
|
|
4201
|
-
seq: 1,
|
|
4202
|
-
});
|
|
4203
|
-
assert_eq!(
|
|
4204
|
-
*lock(&inner.last_seq),
|
|
4205
|
-
1,
|
|
4206
|
-
"paired action passes admission, never a permission refusal"
|
|
4207
|
-
);
|
|
4208
|
-
assert_eq!(lock(&inner.metrics).input_rejected, 1);
|
|
4209
|
-
assert_eq!(lock(&inner.metrics).input_applied, 0);
|
|
4210
|
-
assert!(lock(&recorded).is_empty());
|
|
4211
|
-
assert!(
|
|
4212
|
-
!inner.closed.load(Ordering::Relaxed),
|
|
4213
|
-
"missing input does not close view access"
|
|
4214
|
-
);
|
|
4215
|
-
}
|
|
4216
|
-
|
|
4217
|
-
#[tokio::test]
|
|
4218
|
-
async fn a_lease_that_expires_ends_the_session_and_releases_held_input() {
|
|
4219
|
-
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4220
|
-
let (inner, recorded) = test_inner(events).await;
|
|
4221
|
-
|
|
4222
|
-
#[cfg(target_os = "windows")]
|
|
4223
|
-
inner
|
|
4224
|
-
.with_input(|target| target.chord(1, Vec::new(), true))
|
|
4225
|
-
.unwrap();
|
|
4226
|
-
#[cfg(not(target_os = "windows"))]
|
|
4227
|
-
inner.apply(ControlMessage::Key {
|
|
4228
|
-
name: None,
|
|
4229
|
-
character: Some(String::from("a")),
|
|
4230
|
-
down: true,
|
|
4231
|
-
modifiers: Vec::new(),
|
|
4232
|
-
seq: 1,
|
|
4233
|
-
});
|
|
4234
|
-
assert!(
|
|
4235
|
-
recorded.lock().unwrap().iter().any(|(_, down)| *down),
|
|
4236
|
-
"the press reached the desktop",
|
|
4237
|
-
);
|
|
4238
|
-
|
|
4239
|
-
spawn_lease(&inner, Duration::from_millis(20));
|
|
4240
|
-
tokio::time::sleep(Duration::from_millis(150)).await;
|
|
4241
|
-
|
|
4242
|
-
assert!(
|
|
4243
|
-
inner.revoked_reason().is_some(),
|
|
4244
|
-
"an expired lease must end the session"
|
|
4245
|
-
);
|
|
4246
|
-
assert!(
|
|
4247
|
-
!inner.pipeline.load(Ordering::SeqCst),
|
|
4248
|
-
"an expired lease must stop capture",
|
|
4249
|
-
);
|
|
4250
|
-
assert!(
|
|
4251
|
-
recorded.lock().unwrap().iter().any(|(_, down)| !*down),
|
|
4252
|
-
"an expired lease must release what the session pressed",
|
|
4253
|
-
);
|
|
4254
|
-
assert!(
|
|
4255
|
-
matches!(
|
|
4256
|
-
received.try_recv(),
|
|
4257
|
-
Ok(Notice {
|
|
4258
|
-
event: SessionEvent::Revoked { code: "lease", .. },
|
|
4259
|
-
..
|
|
4260
|
-
})
|
|
4261
|
-
),
|
|
4262
|
-
"the consumer is still told the session's authority ended",
|
|
4263
|
-
);
|
|
4264
|
-
|
|
4265
|
-
let rejected = inner.metrics.lock().unwrap().input_rejected;
|
|
4266
|
-
inner.apply(ControlMessage::Key {
|
|
4267
|
-
name: None,
|
|
4268
|
-
character: Some(String::from("b")),
|
|
4269
|
-
down: true,
|
|
4270
|
-
modifiers: Vec::new(),
|
|
4271
|
-
seq: 2,
|
|
4272
|
-
});
|
|
4273
|
-
assert_eq!(
|
|
4274
|
-
inner.metrics.lock().unwrap().input_rejected,
|
|
4275
|
-
rejected + 1,
|
|
4276
|
-
"a control message after expiry must be refused",
|
|
4277
|
-
);
|
|
4278
|
-
}
|
|
4279
|
-
|
|
4280
|
-
#[cfg(not(target_os = "windows"))]
|
|
4281
|
-
#[tokio::test]
|
|
4282
|
-
async fn a_held_modifier_survives_character_named_and_text_chords() {
|
|
4283
|
-
use crate::input::keycode;
|
|
4284
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
4285
|
-
let (inner, recorded) = test_inner(events).await;
|
|
4286
|
-
for (seq, name) in ["Control", "Shift", "Alt", "Meta"].iter().enumerate() {
|
|
4287
|
-
inner.apply(ControlMessage::Key {
|
|
4288
|
-
name: Some((*name).into()),
|
|
4289
|
-
character: None,
|
|
4290
|
-
down: true,
|
|
4291
|
-
modifiers: Vec::new(),
|
|
4292
|
-
seq: seq as u64 + 1,
|
|
4293
|
-
});
|
|
4294
|
-
}
|
|
4295
|
-
inner.apply(ControlMessage::Key {
|
|
4296
|
-
name: None,
|
|
4297
|
-
character: Some(String::from("C")),
|
|
4298
|
-
down: true,
|
|
4299
|
-
modifiers: vec![String::from("Control"), String::from("Shift")],
|
|
4300
|
-
seq: 5,
|
|
4301
|
-
});
|
|
4302
|
-
inner.apply(ControlMessage::Key {
|
|
4303
|
-
name: None,
|
|
4304
|
-
character: Some(String::from("C")),
|
|
4305
|
-
down: false,
|
|
4306
|
-
modifiers: vec![String::from("Control"), String::from("Shift")],
|
|
4307
|
-
seq: 6,
|
|
4308
|
-
});
|
|
4309
|
-
inner.apply(ControlMessage::Key {
|
|
4310
|
-
name: Some(String::from("ArrowLeft")),
|
|
4311
|
-
character: None,
|
|
4312
|
-
down: true,
|
|
4313
|
-
modifiers: vec![String::from("Alt"), String::from("Meta")],
|
|
4314
|
-
seq: 7,
|
|
4315
|
-
});
|
|
4316
|
-
inner.apply(ControlMessage::Key {
|
|
4317
|
-
name: Some(String::from("ArrowLeft")),
|
|
4318
|
-
character: None,
|
|
4319
|
-
down: false,
|
|
4320
|
-
modifiers: vec![String::from("Alt"), String::from("Meta")],
|
|
4321
|
-
seq: 8,
|
|
4322
|
-
});
|
|
4323
|
-
inner.apply(ControlMessage::Text {
|
|
4324
|
-
text: String::from("C"),
|
|
4325
|
-
seq: 9,
|
|
4326
|
-
});
|
|
4327
|
-
let modifiers = [
|
|
4328
|
-
keycode::LEFT_CTRL,
|
|
4329
|
-
keycode::LEFT_SHIFT,
|
|
4330
|
-
keycode::LEFT_ALT,
|
|
4331
|
-
keycode::LEFT_META,
|
|
4332
|
-
];
|
|
4333
|
-
let strokes = recorded.lock().unwrap().clone();
|
|
4334
|
-
for code in modifiers {
|
|
4335
|
-
assert_eq!(
|
|
4336
|
-
strokes
|
|
4337
|
-
.iter()
|
|
4338
|
-
.filter(|stroke| **stroke == (code, true))
|
|
4339
|
-
.count(),
|
|
4340
|
-
1
|
|
4341
|
-
);
|
|
4342
|
-
assert!(
|
|
4343
|
-
!strokes.contains(&(code, false)),
|
|
4344
|
-
"the separate key still holds {code}"
|
|
4345
|
-
);
|
|
4346
|
-
}
|
|
4347
|
-
for (seq, name) in ["Control", "Shift", "Alt", "Meta"].iter().enumerate() {
|
|
4348
|
-
inner.apply(ControlMessage::Key {
|
|
4349
|
-
name: Some((*name).into()),
|
|
4350
|
-
character: None,
|
|
4351
|
-
down: false,
|
|
4352
|
-
modifiers: Vec::new(),
|
|
4353
|
-
seq: seq as u64 + 10,
|
|
4354
|
-
});
|
|
4355
|
-
}
|
|
4356
|
-
{
|
|
4357
|
-
let strokes = recorded.lock().unwrap();
|
|
4358
|
-
for code in modifiers {
|
|
4359
|
-
assert_eq!(
|
|
4360
|
-
strokes
|
|
4361
|
-
.iter()
|
|
4362
|
-
.filter(|stroke| **stroke == (code, false))
|
|
4363
|
-
.count(),
|
|
4364
|
-
1
|
|
4365
|
-
);
|
|
4366
|
-
}
|
|
4367
|
-
}
|
|
4368
|
-
inner.apply(ControlMessage::Key {
|
|
4369
|
-
name: Some(String::from("ArrowLeft")),
|
|
4370
|
-
character: None,
|
|
4371
|
-
down: true,
|
|
4372
|
-
modifiers: vec![String::from("Control")],
|
|
4373
|
-
seq: 14,
|
|
4374
|
-
});
|
|
4375
|
-
inner.apply(ControlMessage::Key {
|
|
4376
|
-
name: Some(String::from("ArrowLeft")),
|
|
4377
|
-
character: None,
|
|
4378
|
-
down: false,
|
|
4379
|
-
modifiers: vec![String::from("Control")],
|
|
4380
|
-
seq: 15,
|
|
4381
|
-
});
|
|
4382
|
-
{
|
|
4383
|
-
let strokes = recorded.lock().unwrap();
|
|
4384
|
-
assert_eq!(
|
|
4385
|
-
strokes
|
|
4386
|
-
.iter()
|
|
4387
|
-
.filter(|stroke| **stroke == (keycode::LEFT_CTRL, true))
|
|
4388
|
-
.count(),
|
|
4389
|
-
2
|
|
4390
|
-
);
|
|
4391
|
-
assert_eq!(
|
|
4392
|
-
strokes
|
|
4393
|
-
.iter()
|
|
4394
|
-
.filter(|stroke| **stroke == (keycode::LEFT_CTRL, false))
|
|
4395
|
-
.count(),
|
|
4396
|
-
2
|
|
4397
|
-
);
|
|
4398
|
-
}
|
|
4399
|
-
inner.apply(ControlMessage::Key {
|
|
4400
|
-
name: Some(String::from("Control")),
|
|
4401
|
-
character: None,
|
|
4402
|
-
down: true,
|
|
4403
|
-
modifiers: Vec::new(),
|
|
4404
|
-
seq: 16,
|
|
4405
|
-
});
|
|
4406
|
-
for seq in [17, 18] {
|
|
4407
|
-
inner.apply(ControlMessage::Key {
|
|
4408
|
-
name: None,
|
|
4409
|
-
character: Some(String::from("c")),
|
|
4410
|
-
down: true,
|
|
4411
|
-
modifiers: vec![String::from("Control")],
|
|
4412
|
-
seq,
|
|
4413
|
-
});
|
|
4414
|
-
}
|
|
4415
|
-
inner.apply(ControlMessage::Key {
|
|
4416
|
-
name: Some(String::from("Control")),
|
|
4417
|
-
character: None,
|
|
4418
|
-
down: false,
|
|
4419
|
-
modifiers: Vec::new(),
|
|
4420
|
-
seq: 19,
|
|
4421
|
-
});
|
|
4422
|
-
assert!(!recorded
|
|
4423
|
-
.lock()
|
|
4424
|
-
.unwrap()
|
|
4425
|
-
.iter()
|
|
4426
|
-
.rev()
|
|
4427
|
-
.take(2)
|
|
4428
|
-
.any(|event| *event == (keycode::LEFT_CTRL, false)));
|
|
4429
|
-
inner.apply(ControlMessage::Key {
|
|
4430
|
-
name: None,
|
|
4431
|
-
character: Some(String::from("c")),
|
|
4432
|
-
down: false,
|
|
4433
|
-
modifiers: Vec::new(),
|
|
4434
|
-
seq: 20,
|
|
4435
|
-
});
|
|
4436
|
-
inner.apply(ControlMessage::Key {
|
|
4437
|
-
name: Some(String::from("ArrowLeft")),
|
|
4438
|
-
character: None,
|
|
4439
|
-
down: true,
|
|
4440
|
-
modifiers: vec![String::from("Shift")],
|
|
4441
|
-
seq: 21,
|
|
4442
|
-
});
|
|
4443
|
-
inner.apply(ControlMessage::Key {
|
|
4444
|
-
name: Some(String::from("ArrowLeft")),
|
|
4445
|
-
character: None,
|
|
4446
|
-
down: false,
|
|
4447
|
-
modifiers: Vec::new(),
|
|
4448
|
-
seq: 22,
|
|
4449
|
-
});
|
|
4450
|
-
let strokes = recorded.lock().unwrap();
|
|
4451
|
-
assert_eq!(
|
|
4452
|
-
strokes
|
|
4453
|
-
.iter()
|
|
4454
|
-
.filter(|stroke| **stroke == (keycode::LEFT_CTRL, true))
|
|
4455
|
-
.count(),
|
|
4456
|
-
3
|
|
4457
|
-
);
|
|
4458
|
-
assert_eq!(
|
|
4459
|
-
strokes
|
|
4460
|
-
.iter()
|
|
4461
|
-
.filter(|stroke| **stroke == (keycode::LEFT_CTRL, false))
|
|
4462
|
-
.count(),
|
|
4463
|
-
3
|
|
4464
|
-
);
|
|
4465
|
-
assert_eq!(
|
|
4466
|
-
strokes
|
|
4467
|
-
.iter()
|
|
4468
|
-
.filter(|stroke| **stroke == (keycode::LEFT_SHIFT, false))
|
|
4469
|
-
.count(),
|
|
4470
|
-
2
|
|
4471
|
-
);
|
|
4472
|
-
}
|
|
4473
|
-
|
|
4474
|
-
#[tokio::test]
|
|
4475
|
-
async fn x11_capture_does_not_refuse_clipboard() {
|
|
4476
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
4477
|
-
let (mut inner, _recorded) = test_inner(events).await;
|
|
4478
|
-
let owned = Arc::get_mut(&mut inner).unwrap();
|
|
4479
|
-
owned.source.source_type = Some(String::from("x11-root"));
|
|
4480
|
-
#[cfg(target_os = "linux")]
|
|
4481
|
-
assert_eq!(inner.clipboard_refusal(), None);
|
|
4482
|
-
Arc::get_mut(&mut inner).unwrap().source.source_type = Some(String::from("encoded"));
|
|
4483
|
-
assert_eq!(
|
|
4484
|
-
inner.clipboard_refusal().unwrap().0,
|
|
4485
|
-
"clipboard-unsupported"
|
|
4486
|
-
);
|
|
4487
|
-
}
|
|
4488
|
-
|
|
4489
|
-
#[tokio::test]
|
|
4490
|
-
async fn a_stopped_capture_never_serves_the_last_frame() {
|
|
4491
|
-
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
4492
|
-
let (inner, _) = test_inner(events).await;
|
|
4493
|
-
let session = Session { inner };
|
|
4494
|
-
*lock(&session.inner.latest) =
|
|
4495
|
-
Some((1, Instant::now(), 2, 2, vec![0; 16], vec![1], vec![]));
|
|
4496
|
-
*lock(&session.inner.capture_error) = Some(String::from("SCStreamErrorDomain -3821"));
|
|
4497
|
-
assert_eq!(
|
|
4498
|
-
session.frame(None, "", None).unwrap_err().code,
|
|
4499
|
-
"stream-stopped"
|
|
4500
|
-
);
|
|
4501
|
-
assert_eq!(
|
|
4502
|
-
session.wait_frame(None, None, 10).await.unwrap_err().code,
|
|
4503
|
-
"stream-stopped"
|
|
4504
|
-
);
|
|
4505
|
-
}
|
|
4506
|
-
|
|
4507
|
-
#[tokio::test]
|
|
4508
|
-
async fn a_frame_the_encoder_refuses_ends_the_session_with_a_reason() {
|
|
4509
|
-
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4510
|
-
let (inner, _recorded) = test_inner(events).await;
|
|
4511
|
-
let (frame_tx, frame_rx) = latest_frame();
|
|
4512
|
-
spawn_pipeline(
|
|
4513
|
-
&inner,
|
|
4514
|
-
current_peer(&inner).expect("the test's peer"),
|
|
4515
|
-
frame_rx,
|
|
4516
|
-
Arc::new(AtomicBool::new(true)),
|
|
4517
|
-
30,
|
|
4518
|
-
1000,
|
|
4519
|
-
);
|
|
4520
|
-
|
|
4521
|
-
// The encoder is 64x64; a 32x32 frame is the dimension mismatch that
|
|
4522
|
-
// used to be counted and dropped behind a permanently black picture.
|
|
4523
|
-
frame_tx.put(
|
|
4524
|
-
I420 {
|
|
4525
|
-
width: 32,
|
|
4526
|
-
height: 32,
|
|
4527
|
-
data: vec![128u8; 32 * 32 + 2 * 16 * 16],
|
|
4528
|
-
},
|
|
4529
|
-
Instant::now(),
|
|
4530
|
-
);
|
|
4531
|
-
tokio::time::sleep(Duration::from_millis(150)).await;
|
|
4532
|
-
|
|
4533
|
-
let mut reason = None;
|
|
4534
|
-
while let Ok(event) = received.try_recv() {
|
|
4535
|
-
if let Notice {
|
|
4536
|
-
event: SessionEvent::Revoked { reason: why, code },
|
|
4537
|
-
..
|
|
4538
|
-
} = event
|
|
4539
|
-
{
|
|
4540
|
-
assert_eq!(code, "error", "a refused frame is a fault, not a lost path");
|
|
4541
|
-
reason = Some(why);
|
|
4542
|
-
}
|
|
4543
|
-
}
|
|
4544
|
-
let reason = reason.expect("a refused frame must surface as a revocation");
|
|
4545
|
-
assert_eq!(
|
|
4546
|
-
reason, "the encoder rejected a frame",
|
|
4547
|
-
"the reason is a stable token the client can map to copy",
|
|
4548
|
-
);
|
|
4549
|
-
assert!(
|
|
4550
|
-
inner.revoked_reason().is_some(),
|
|
4551
|
-
"the session is closed, not left black"
|
|
4552
|
-
);
|
|
4553
|
-
}
|
|
4554
|
-
|
|
4555
|
-
/// A consumer-fed stream: a keyframe carrying SPS, PPS and an IDR slice.
|
|
4556
|
-
fn fed_keyframe() -> Vec<u8> {
|
|
4557
|
-
let mut stream = vec![0x00, 0x00, 0x00, 0x01];
|
|
4558
|
-
stream.extend_from_slice(&[0x67, 0x42, 0xc0, 0x29, 0x8c, 0x8d]);
|
|
4559
|
-
stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x68, 0xce, 0x3c, 0x80]);
|
|
4560
|
-
stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x00]);
|
|
4561
|
-
stream
|
|
4562
|
-
}
|
|
4563
|
-
|
|
4564
|
-
fn encoded_open() -> OpenRequest {
|
|
4565
|
-
OpenRequest {
|
|
4566
|
-
cursor: crate::protocol::CursorMode::Embedded,
|
|
4567
|
-
source: Some(SourceRequest::Encoded {
|
|
4568
|
-
codec: EncodedCodec::H264,
|
|
4569
|
-
width: 486,
|
|
4570
|
-
height: 1080,
|
|
4571
|
-
}),
|
|
4572
|
-
max_width: 3840,
|
|
4573
|
-
max_height: 2160,
|
|
4574
|
-
bitrate_kbps: 0,
|
|
4575
|
-
max_fps: 30,
|
|
4576
|
-
ice_servers: Vec::new(),
|
|
4577
|
-
restore_token: None,
|
|
4578
|
-
ttl: Some(Duration::from_secs(30)),
|
|
4579
|
-
loopback_tcp: false,
|
|
4580
|
-
agent_indicator: false,
|
|
4581
|
-
local_frames: true,
|
|
4582
|
-
audio: false,
|
|
4583
|
-
observe_input: false,
|
|
4584
|
-
}
|
|
4585
|
-
}
|
|
4586
|
-
|
|
4587
|
-
async fn next_notice(received: &mut tokio_mpsc::UnboundedReceiver<Notice>) -> Notice {
|
|
4588
|
-
tokio::time::timeout(Duration::from_secs(5), received.recv())
|
|
4589
|
-
.await
|
|
4590
|
-
.expect("a notification arrives")
|
|
4591
|
-
.expect("the channel stays open")
|
|
4592
|
-
}
|
|
4593
|
-
|
|
4594
|
-
#[tokio::test]
|
|
4595
|
-
async fn hidden_cursor_is_refused_for_consumer_encoded_pixels() {
|
|
4596
|
-
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
4597
|
-
let mut request = encoded_open();
|
|
4598
|
-
request.cursor = crate::protocol::CursorMode::Hidden;
|
|
4599
|
-
let error = Session::open(request, events)
|
|
4600
|
-
.await
|
|
4601
|
-
.err()
|
|
4602
|
-
.expect("cannot alter encoded pixels");
|
|
4603
|
-
assert_eq!(error.code, "cursor-unavailable");
|
|
4604
|
-
}
|
|
4605
|
-
|
|
4606
|
-
#[tokio::test]
|
|
4607
|
-
async fn an_encoded_session_opens_without_capture_encoder_or_input() {
|
|
4608
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
4609
|
-
let session = Session::open(encoded_open(), events)
|
|
4610
|
-
.await
|
|
4611
|
-
.expect("an encoded source needs no display and no input device");
|
|
4612
|
-
let inner = &session.inner;
|
|
4613
|
-
assert_eq!(session.restore_source(), "encoded");
|
|
4614
|
-
assert_eq!(session.geometry()["encoded"]["width"], 486);
|
|
4615
|
-
assert_eq!(session.geometry()["encoded"]["height"], 1080);
|
|
4616
|
-
assert!(
|
|
4617
|
-
inner.encoder.lock().unwrap().is_none(),
|
|
4618
|
-
"the consumer is the encoder for this source"
|
|
4619
|
-
);
|
|
4620
|
-
assert!(
|
|
4621
|
-
inner.capture.lock().unwrap().is_none(),
|
|
4622
|
-
"there is nothing to capture"
|
|
4623
|
-
);
|
|
4624
|
-
assert!(
|
|
4625
|
-
inner.input.lock().unwrap().is_none(),
|
|
4626
|
-
"the client's input belongs to the consumer's device"
|
|
4627
|
-
);
|
|
4628
|
-
assert!(
|
|
4629
|
-
inner.peer.lock().unwrap().is_none(),
|
|
4630
|
-
"the offer waits for the stream's own SPS"
|
|
4631
|
-
);
|
|
4632
|
-
}
|
|
4633
|
-
|
|
4634
|
-
#[tokio::test]
|
|
4635
|
-
async fn the_offer_for_a_fed_stream_names_that_streams_own_profile() {
|
|
4636
|
-
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4637
|
-
let session = Session::open(encoded_open(), events)
|
|
4638
|
-
.await
|
|
4639
|
-
.expect("an encoded session");
|
|
4640
|
-
let first = next_notice(&mut received).await;
|
|
4641
|
-
assert!(
|
|
4642
|
-
matches!(
|
|
4643
|
-
first.event,
|
|
4644
|
-
SessionEvent::State {
|
|
4645
|
-
capture: "consented",
|
|
4646
|
-
..
|
|
4647
|
-
}
|
|
4648
|
-
),
|
|
4649
|
-
"the session is open before any offer exists: {:?}",
|
|
4650
|
-
first.event,
|
|
4651
|
-
);
|
|
4652
|
-
|
|
4653
|
-
session
|
|
4654
|
-
.feed(true, fed_keyframe())
|
|
4655
|
-
.expect("a keyframe is taken");
|
|
4656
|
-
let offered = loop {
|
|
4657
|
-
let notice = next_notice(&mut received).await;
|
|
4658
|
-
if let SessionEvent::Description { sdp, .. } = notice.event {
|
|
4659
|
-
break sdp;
|
|
4660
|
-
}
|
|
4661
|
-
};
|
|
4662
|
-
assert!(
|
|
4663
|
-
offered.contains("H264/90000"),
|
|
4664
|
-
"the fed track is offered as H.264: {offered}"
|
|
4665
|
-
);
|
|
4666
|
-
assert!(
|
|
4667
|
-
offered.contains("profile-level-id=42c029"),
|
|
4668
|
-
"the profile-level-id comes from the fed SPS, not a default: {offered}"
|
|
4669
|
-
);
|
|
4670
|
-
assert_eq!(session.metrics().fed_frames, 1);
|
|
4671
|
-
}
|
|
4672
|
-
|
|
4673
|
-
#[tokio::test]
|
|
4674
|
-
async fn a_fed_stream_forwards_its_controls_instead_of_applying_them() {
|
|
4675
|
-
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4676
|
-
let session = Session::open(encoded_open(), events)
|
|
4677
|
-
.await
|
|
4678
|
-
.expect("an encoded session");
|
|
4679
|
-
session.inner.control_open.store(true, Ordering::SeqCst);
|
|
4680
|
-
|
|
4681
|
-
session.inner.apply(ControlMessage::Pointer {
|
|
4682
|
-
phase: PointerPhase::Down,
|
|
4683
|
-
x: 10,
|
|
4684
|
-
y: 20,
|
|
4685
|
-
button: 1,
|
|
4686
|
-
seq: 1,
|
|
4687
|
-
});
|
|
4688
|
-
|
|
4689
|
-
let forwarded = loop {
|
|
4690
|
-
let notice = next_notice(&mut received).await;
|
|
4691
|
-
if let SessionEvent::Input { input } = notice.event {
|
|
4692
|
-
break input;
|
|
4693
|
-
}
|
|
4694
|
-
};
|
|
4695
|
-
assert_eq!(forwarded["kind"], "pointer");
|
|
4696
|
-
assert_eq!(forwarded["phase"], "down");
|
|
4697
|
-
assert_eq!(forwarded["x"], 10);
|
|
4698
|
-
assert_eq!(forwarded["seq"], 1);
|
|
4699
|
-
assert_eq!(session.metrics().input_forwarded, 1);
|
|
4700
|
-
assert_eq!(
|
|
4701
|
-
session.metrics().input_applied,
|
|
4702
|
-
0,
|
|
4703
|
-
"nothing was applied to this machine"
|
|
4704
|
-
);
|
|
4705
|
-
|
|
4706
|
-
// The stream's own coordinates are what travels, so an encoded surface
|
|
4707
|
-
// cannot be driven by a coordinate the far end never rendered.
|
|
4708
|
-
session.inner.apply(ControlMessage::Pointer {
|
|
4709
|
-
phase: PointerPhase::Move,
|
|
4710
|
-
x: 9_000,
|
|
4711
|
-
y: 20,
|
|
4712
|
-
button: 1,
|
|
4713
|
-
seq: 2,
|
|
4714
|
-
});
|
|
4715
|
-
assert_eq!(session.metrics().input_forwarded, 2);
|
|
4716
|
-
}
|
|
4717
|
-
|
|
4718
|
-
#[tokio::test]
|
|
4719
|
-
async fn a_capture_session_refuses_a_fed_stream() {
|
|
4720
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
4721
|
-
let (inner, _recorded) = test_inner(events).await;
|
|
4722
|
-
let session = Session { inner };
|
|
4723
|
-
assert_eq!(
|
|
4724
|
-
session.feed(true, fed_keyframe()).unwrap_err().code,
|
|
4725
|
-
"operation",
|
|
4726
|
-
"only an encoded source takes access units",
|
|
4727
|
-
);
|
|
4728
|
-
}
|
|
4729
|
-
|
|
4730
|
-
#[tokio::test]
|
|
4731
|
-
async fn an_encoded_session_has_no_frame_to_read() {
|
|
4732
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
4733
|
-
let session = Session::open(encoded_open(), events)
|
|
4734
|
-
.await
|
|
4735
|
-
.expect("an encoded session");
|
|
4736
|
-
assert_eq!(session.frame(None, "", None).unwrap_err().code, "operation");
|
|
4737
|
-
assert_eq!(
|
|
4738
|
-
session.wait_frame(None, None, 10).await.unwrap_err().code,
|
|
4739
|
-
"operation"
|
|
4740
|
-
);
|
|
4741
|
-
assert_eq!(
|
|
4742
|
-
session.read_clipboard().await.unwrap_err(),
|
|
4743
|
-
"clipboard is unavailable for this desktop source",
|
|
4744
|
-
);
|
|
4745
|
-
}
|
|
4746
|
-
|
|
4747
|
-
#[tokio::test]
|
|
4748
|
-
async fn three_queued_ice_restarts_reemit_one_offer_and_accept_its_answer() {
|
|
4749
|
-
let codec = crate::peer::tests::h264_receiver_codec();
|
|
4750
|
-
let (peer, _) = crate::peer::tests::answered(vec![codec.clone()]).await;
|
|
4751
|
-
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4752
|
-
let (inner, _) = test_inner_with(events, Arc::new(peer), 64, 64);
|
|
4753
|
-
let session = Session { inner };
|
|
4754
|
-
let mut offers = Vec::new();
|
|
4755
|
-
for _ in 0..3 {
|
|
4756
|
-
session.restart_ice().await.expect("restart accepted");
|
|
4757
|
-
match received.recv().await.expect("offer event").event {
|
|
4758
|
-
SessionEvent::Description { sdp, .. } => offers.push(sdp),
|
|
4759
|
-
_ => panic!("expected the pending offer"),
|
|
4760
|
-
}
|
|
4761
|
-
}
|
|
4762
|
-
let ufrag = |sdp: &str| {
|
|
4763
|
-
sdp.lines()
|
|
4764
|
-
.find_map(|line| line.strip_prefix("a=ice-ufrag:"))
|
|
4765
|
-
.expect("ICE credentials")
|
|
4766
|
-
.to_owned()
|
|
4767
|
-
};
|
|
4768
|
-
assert!(offers.iter().all(|sdp| ufrag(sdp) == ufrag(&offers[0])));
|
|
4769
|
-
let client = crate::peer::tests::answerer_with(&offers[0], vec![codec]).await;
|
|
4770
|
-
let answer = client.create_answer(None).await.expect("client answer");
|
|
4771
|
-
client
|
|
4772
|
-
.set_local_description(answer.clone())
|
|
4773
|
-
.await
|
|
4774
|
-
.expect("local answer");
|
|
4775
|
-
session
|
|
4776
|
-
.accept_answer(answer.sdp)
|
|
4777
|
-
.await
|
|
4778
|
-
.expect("pending answer accepted");
|
|
4779
|
-
client.close().await.expect("close client");
|
|
4780
|
-
session.current_peer().unwrap().close().await;
|
|
4781
|
-
}
|
|
4782
|
-
|
|
4783
|
-
#[tokio::test]
|
|
4784
|
-
async fn a_lost_transport_releases_input_then_ends_the_session_without_sctp_close() {
|
|
4785
|
-
use webrtc::peer_connection::RTCPeerConnectionState as State;
|
|
4786
|
-
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4787
|
-
let (inner, recorded) = test_inner(events).await;
|
|
4788
|
-
#[cfg(target_os = "windows")]
|
|
4789
|
-
inner
|
|
4790
|
-
.with_input(|target| target.chord(1, Vec::new(), true))
|
|
4791
|
-
.unwrap();
|
|
4792
|
-
#[cfg(not(target_os = "windows"))]
|
|
4793
|
-
inner.apply(ControlMessage::Key {
|
|
4794
|
-
name: Some(String::from("Control")),
|
|
4795
|
-
character: None,
|
|
4796
|
-
down: true,
|
|
4797
|
-
modifiers: Vec::new(),
|
|
4798
|
-
seq: 1,
|
|
4799
|
-
});
|
|
4800
|
-
assert!(recorded.lock().unwrap().iter().any(|(_, down)| *down));
|
|
4801
|
-
|
|
4802
|
-
let (peer_tx, peer_rx) = tokio_mpsc::unbounded_channel();
|
|
4803
|
-
spawn_peer_events(&inner, peer_rx);
|
|
4804
|
-
peer_tx.send(PeerEvent::State(State::Disconnected)).unwrap();
|
|
4805
|
-
tokio::time::timeout(Duration::from_secs(2), received.recv())
|
|
4806
|
-
.await
|
|
4807
|
-
.unwrap();
|
|
4808
|
-
assert!(
|
|
4809
|
-
recorded.lock().unwrap().iter().any(|(_, down)| !*down),
|
|
4810
|
-
"a disconnected transport must release held input without SCTP OnClose"
|
|
4811
|
-
);
|
|
4812
|
-
assert!(
|
|
4813
|
-
inner.revoked_reason().is_none(),
|
|
4814
|
-
"a transient loss may recover"
|
|
4815
|
-
);
|
|
4816
|
-
|
|
4817
|
-
peer_tx.send(PeerEvent::State(State::Failed)).unwrap();
|
|
4818
|
-
let mut transport_lost = false;
|
|
4819
|
-
tokio::time::timeout(Duration::from_secs(2), async {
|
|
4820
|
-
while let Some(notice) = received.recv().await {
|
|
4821
|
-
// A lost path is the one revocation a new session can heal.
|
|
4822
|
-
transport_lost |= matches!(
|
|
4823
|
-
notice.event,
|
|
4824
|
-
SessionEvent::Revoked {
|
|
4825
|
-
code: "transport",
|
|
4826
|
-
..
|
|
4827
|
-
}
|
|
4828
|
-
);
|
|
4829
|
-
if matches!(
|
|
4830
|
-
notice.event,
|
|
4831
|
-
SessionEvent::State {
|
|
4832
|
-
capture: "ended",
|
|
4833
|
-
..
|
|
4834
|
-
}
|
|
4835
|
-
) {
|
|
4836
|
-
return;
|
|
4837
|
-
}
|
|
4838
|
-
}
|
|
4839
|
-
panic!("failed transport did not end capture");
|
|
4840
|
-
})
|
|
4841
|
-
.await
|
|
4842
|
-
.unwrap();
|
|
4843
|
-
assert!(
|
|
4844
|
-
transport_lost,
|
|
4845
|
-
"a failed transport must revoke with code transport"
|
|
4846
|
-
);
|
|
4847
|
-
assert!(inner.revoked_reason().is_some());
|
|
4848
|
-
assert!(!inner.pipeline.load(Ordering::SeqCst));
|
|
4849
|
-
assert!(inner.input.lock().unwrap().is_none());
|
|
4850
|
-
}
|
|
4851
|
-
|
|
4852
|
-
#[tokio::test]
|
|
4853
|
-
async fn the_pipeline_caps_the_rate_codes_the_last_frame_and_refines_a_still_desktop_once() {
|
|
4854
|
-
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
4855
|
-
let (inner, _recorded) = test_inner(events).await;
|
|
4856
|
-
let (frame_tx, frame_rx) = latest_frame();
|
|
4857
|
-
spawn_pipeline(
|
|
4858
|
-
&inner,
|
|
4859
|
-
current_peer(&inner).expect("the test's peer"),
|
|
4860
|
-
frame_rx,
|
|
4861
|
-
Arc::new(AtomicBool::new(true)),
|
|
4862
|
-
20,
|
|
4863
|
-
1000,
|
|
4864
|
-
);
|
|
4865
|
-
|
|
4866
|
-
// 40 frames nominally over 200 ms; Windows timers can round each 5 ms
|
|
4867
|
-
// sleep upward, so its cap assertion uses the actual burst duration.
|
|
4868
|
-
#[cfg(target_os = "windows")]
|
|
4869
|
-
let burst_started = Instant::now();
|
|
4870
|
-
for _ in 0..40 {
|
|
4871
|
-
frame_tx.put(
|
|
4872
|
-
I420 {
|
|
4873
|
-
width: 64,
|
|
4874
|
-
height: 64,
|
|
4875
|
-
data: vec![128u8; 64 * 64 + 2 * 32 * 32],
|
|
4876
|
-
},
|
|
4877
|
-
Instant::now(),
|
|
4878
|
-
);
|
|
4879
|
-
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
4880
|
-
}
|
|
4881
|
-
let motion = inner.metrics.lock().unwrap().encoded_frames;
|
|
4882
|
-
#[cfg(target_os = "windows")]
|
|
4883
|
-
let cap = (burst_started.elapsed().as_secs_f64() * 20.0).ceil() as u64 + 3;
|
|
4884
|
-
#[cfg(not(target_os = "windows"))]
|
|
4885
|
-
let cap = 7;
|
|
4886
|
-
assert!(
|
|
4887
|
-
motion <= cap,
|
|
4888
|
-
"the requested rate is a cap, got {motion} of 40 (cap {cap})"
|
|
4889
|
-
);
|
|
4890
|
-
assert!(
|
|
4891
|
-
motion >= 2,
|
|
4892
|
-
"the pipeline must still emit frames, got {motion}"
|
|
4893
|
-
);
|
|
4894
|
-
|
|
4895
|
-
// Still for longer than a refinement takes: the last frame of the burst
|
|
4896
|
-
// is coded (a refinement only follows a coded frame), then refined once.
|
|
4897
|
-
tokio::time::sleep(Duration::from_millis(450)).await;
|
|
4898
|
-
let metrics = inner.metrics.lock().unwrap().clone();
|
|
4899
|
-
assert_eq!(
|
|
4900
|
-
metrics.refined_frames, 1,
|
|
4901
|
-
"a still desktop is refined exactly once"
|
|
4902
|
-
);
|
|
4903
|
-
assert_eq!(metrics.key_frames, 1, "only the first frame is a key frame");
|
|
4904
|
-
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
4905
|
-
assert_eq!(
|
|
4906
|
-
inner.metrics.lock().unwrap().encoded_frames,
|
|
4907
|
-
metrics.encoded_frames,
|
|
4908
|
-
"nothing is sent while still"
|
|
4909
|
-
);
|
|
4910
|
-
|
|
4911
|
-
// A still stream is kept alive, so a receiver never waits long enough
|
|
4912
|
-
// to ask for a key frame; the keepalive is neither a key frame nor
|
|
4913
|
-
// another refinement.
|
|
4914
|
-
tokio::time::sleep(Duration::from_millis(900)).await;
|
|
4915
|
-
let later = inner.metrics.lock().unwrap().clone();
|
|
4916
|
-
assert!(
|
|
4917
|
-
later.encoded_frames > metrics.encoded_frames,
|
|
4918
|
-
"a still stream still sends"
|
|
4919
|
-
);
|
|
4920
|
-
assert_eq!((later.key_frames, later.refined_frames), (1, 1));
|
|
4921
|
-
assert_eq!((later.codec, later.encoder), ("vp9", "libvpx"));
|
|
4922
|
-
}
|
|
4923
|
-
|
|
4924
|
-
/// The rate target after 3 s of delivery jitter on a connected pair: a queue
|
|
4925
|
-
/// that alternates between 5 and 30 ms with nothing lost, and 800 kbps
|
|
4926
|
-
/// delivered. The pipeline runs for real; only the path reports are fed.
|
|
4927
|
-
async fn target_under_bridge_jitter(tcp: bool) -> u32 {
|
|
4928
|
-
let peers = crate::peer::tests::connected(
|
|
4929
|
-
if tcp {
|
|
4930
|
-
Vec::new()
|
|
4931
|
-
} else {
|
|
4932
|
-
vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))]
|
|
4933
|
-
},
|
|
4934
|
-
if tcp {
|
|
4935
|
-
vec![std::net::SocketAddr::from(([127, 0, 0, 1], 0))]
|
|
4936
|
-
} else {
|
|
4937
|
-
Vec::new()
|
|
4938
|
-
},
|
|
4939
|
-
tcp,
|
|
4940
|
-
)
|
|
4941
|
-
.await;
|
|
4942
|
-
let engine = Arc::clone(&peers.engine);
|
|
4943
|
-
assert_eq!(
|
|
4944
|
-
engine.selected_pair_is_tcp().await,
|
|
4945
|
-
tcp,
|
|
4946
|
-
"the selected pair must run over {}",
|
|
4947
|
-
if tcp { "TCP" } else { "UDP" }
|
|
4948
|
-
);
|
|
4949
|
-
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
4950
|
-
let (inner, _recorded) = test_inner_with(events, Arc::clone(&engine), 64, 64);
|
|
4951
|
-
let (_frame_tx, frame_rx) = latest_frame();
|
|
4952
|
-
spawn_pipeline(
|
|
4953
|
-
&inner,
|
|
4954
|
-
Arc::clone(&engine),
|
|
4955
|
-
frame_rx,
|
|
4956
|
-
Arc::new(AtomicBool::new(true)),
|
|
4957
|
-
30,
|
|
4958
|
-
1000,
|
|
4959
|
-
);
|
|
4960
|
-
for step in 0..300 {
|
|
4961
|
-
let queue_delay = Duration::from_millis(if step % 2 == 0 { 5 } else { 30 });
|
|
4962
|
-
engine.report_path(PathReport {
|
|
4963
|
-
queue_delay,
|
|
4964
|
-
delivered_kbps: 800,
|
|
4965
|
-
});
|
|
4966
|
-
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
4967
|
-
}
|
|
4968
|
-
let target = inner.metrics.lock().unwrap().target_kbps;
|
|
4969
|
-
drop(peers);
|
|
4970
|
-
target
|
|
4971
|
-
}
|
|
4972
|
-
|
|
4973
|
-
#[tokio::test(flavor = "multi_thread")]
|
|
4974
|
-
async fn a_tcp_path_with_bridge_jitter_holds_its_rate_ceiling() {
|
|
4975
|
-
assert_eq!(
|
|
4976
|
-
target_under_bridge_jitter(true).await,
|
|
4977
|
-
1000,
|
|
4978
|
-
"a 5-30 ms jitter that is not a queue must not back a TCP path off"
|
|
4979
|
-
);
|
|
4980
|
-
}
|
|
4981
|
-
|
|
4982
|
-
#[tokio::test(flavor = "multi_thread")]
|
|
4983
|
-
async fn a_udp_path_with_the_same_jitter_still_backs_off() {
|
|
4984
|
-
assert!(
|
|
4985
|
-
target_under_bridge_jitter(false).await < 1000,
|
|
4986
|
-
"a UDP path reads the same 30 ms as a standing queue and must back off"
|
|
4987
|
-
);
|
|
4988
|
-
}
|
|
4989
|
-
|
|
4990
|
-
#[tokio::test]
|
|
4991
|
-
async fn a_receiver_that_asks_for_h264_is_sent_h264_from_the_first_frame() {
|
|
4992
|
-
let Some(info) = tokio::task::spawn_blocking(crate::h264::probe)
|
|
4993
|
-
.await
|
|
4994
|
-
.unwrap()
|
|
4995
|
-
else {
|
|
4996
|
-
eprintln!("skipped: no H.264 encoder starts on this machine");
|
|
4997
|
-
return;
|
|
4998
|
-
};
|
|
4999
|
-
let (peer, _answer) =
|
|
5000
|
-
crate::peer::tests::answered(vec![crate::peer::tests::h264_receiver_codec()]).await;
|
|
5001
|
-
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
5002
|
-
let (inner, _recorded) = test_inner_with(events, Arc::new(peer), 320, 240);
|
|
5003
|
-
// The answer is real, so ICE runs and, with no candidates, never
|
|
5004
|
-
// connects; each state it reports would take back `assume_connected`.
|
|
5005
|
-
let pinned = current_peer(&inner).expect("the test's peer");
|
|
5006
|
-
let pin = tokio::spawn(async move {
|
|
5007
|
-
loop {
|
|
5008
|
-
pinned.assume_connected();
|
|
5009
|
-
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
5010
|
-
}
|
|
5011
|
-
});
|
|
5012
|
-
let (frame_tx, frame_rx) = latest_frame();
|
|
5013
|
-
spawn_pipeline(
|
|
5014
|
-
&inner,
|
|
5015
|
-
current_peer(&inner).expect("the test's peer"),
|
|
5016
|
-
frame_rx,
|
|
5017
|
-
Arc::new(AtomicBool::new(true)),
|
|
5018
|
-
20,
|
|
5019
|
-
1000,
|
|
5020
|
-
);
|
|
5021
|
-
for shade in 0..10u8 {
|
|
5022
|
-
frame_tx.put(
|
|
5023
|
-
I420 {
|
|
5024
|
-
width: 320,
|
|
5025
|
-
height: 240,
|
|
5026
|
-
data: vec![shade * 20; 320 * 240 * 3 / 2],
|
|
5027
|
-
},
|
|
5028
|
-
Instant::now(),
|
|
5029
|
-
);
|
|
5030
|
-
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
5031
|
-
}
|
|
5032
|
-
// Opening a hardware session can take a good part of a second, more
|
|
5033
|
-
// with other tests holding the GPU: wait for the still to be refined.
|
|
5034
|
-
let deadline = Instant::now() + Duration::from_secs(10);
|
|
5035
|
-
while inner.metrics.lock().unwrap().refined_frames == 0 && Instant::now() < deadline {
|
|
5036
|
-
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
5037
|
-
}
|
|
5038
|
-
let metrics = inner.metrics.lock().unwrap().clone();
|
|
5039
|
-
assert_eq!((metrics.codec, metrics.encoder), ("h264", info.encoder));
|
|
5040
|
-
assert!(metrics.encoded_frames >= 2, "{metrics:?}");
|
|
5041
|
-
assert_eq!(
|
|
5042
|
-
metrics.key_frames, 1,
|
|
5043
|
-
"one IDR, then inter frames: {metrics:?}"
|
|
5044
|
-
);
|
|
5045
|
-
assert_eq!(metrics.refined_frames, 1, "a still desktop is refined once");
|
|
5046
|
-
assert_eq!(
|
|
5047
|
-
metrics.halved_frames, 0,
|
|
5048
|
-
"H.264 is always coded at full size"
|
|
5049
|
-
);
|
|
5050
|
-
pin.abort();
|
|
5051
|
-
inner.close("done", "closed").await;
|
|
5052
|
-
assert!(
|
|
5053
|
-
lock(&inner.encoder).is_none(),
|
|
5054
|
-
"closing releases the encoder"
|
|
5055
|
-
);
|
|
5056
|
-
}
|
|
5057
4046
|
}
|