@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.
@@ -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
- let (input, grant_state, unavailable) = match crate::input::probe() {
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 devices = InputDevices::create(source_w as i32, source_h as i32);
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 devices = InputDevices::create_for_display(
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
- devices.ok().map(Applier::Uinput)
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
  }