@desklink/host 0.5.14 → 0.5.16

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