@desklink/host 0.5.7 → 0.5.9

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.
@@ -703,7 +703,7 @@ pub struct OpenRequest {
703
703
  pub max_height: usize,
704
704
  pub bitrate_kbps: u32,
705
705
  pub max_fps: u32,
706
- pub ice_servers: Vec<(String, Option<String>, Option<String>)>,
706
+ pub ice_servers: Vec<(Vec<String>, Option<String>, Option<String>)>,
707
707
  pub restore_token: Option<String>,
708
708
  pub ttl: Option<Duration>,
709
709
  /// Offer a passive ICE-TCP candidate on the loopback, for a forwarded client.
@@ -762,6 +762,13 @@ pub enum SessionEvent {
762
762
  KeyframeRequest {
763
763
  generation: u64,
764
764
  },
765
+ /// An ICE server URL the request asked for that the peer's configuration
766
+ /// does not carry: one the WebRTC stack cannot dial (TURN over anything
767
+ /// but UDP). Reported once per open, naming the URL, so a skipped URL is
768
+ /// a warning the consumer sees rather than a silent drop.
769
+ IceServerSkipped {
770
+ url: String,
771
+ },
765
772
  RestoreToken(String),
766
773
  CaptureStopped {
767
774
  reason: String,
@@ -821,6 +828,13 @@ struct Inner {
821
828
  /// closes its Hyprland event socket.
822
829
  #[cfg(target_os = "linux")]
823
830
  window: Mutex<Option<crate::hyprland::WindowLocator>>,
831
+ /// Which Hyprland output the agent cue helper draws on (a located window
832
+ /// share's desktop mode): the helper respawns only when a press lands
833
+ /// elsewhere. Same for the point helper.
834
+ #[cfg(target_os = "linux")]
835
+ cue_output: Mutex<Option<String>>,
836
+ #[cfg(target_os = "linux")]
837
+ point_output: Mutex<Option<String>>,
824
838
  capture: Mutex<Option<FrameSource>>,
825
839
  /// Absent for an encoded source: there is no local keyboard to translate a
826
840
  /// character through, because the client's keys are forwarded instead.
@@ -1267,9 +1281,19 @@ impl Session {
1267
1281
  let source_h = source.height.max(1) as usize;
1268
1282
  let (width, height) =
1269
1283
  fit(source_w, source_h, request.max_width, request.max_height);
1284
+ // A Hyprland window share's cues go to the desktop point each
1285
+ // press lands on, and its picture holds no layer surface to mask.
1286
+ #[cfg(target_os = "linux")]
1287
+ let desktop =
1288
+ share_watch.is_some() && source.source_type.as_deref() == Some("Window");
1270
1289
  #[cfg(target_os = "linux")]
1271
1290
  if request.agent_indicator {
1272
- indicator = crate::indicator::Indicator::start_wayland(source_w, source_h).ok();
1291
+ // No press has landed yet, so no output is chosen: the
1292
+ // compositor places this first helper, and the first
1293
+ // located press moves it onto the landed output.
1294
+ indicator =
1295
+ crate::indicator::Indicator::start_wayland(source_w, source_h, desktop, None)
1296
+ .ok();
1273
1297
  }
1274
1298
  #[cfg(target_os = "macos")]
1275
1299
  if request.agent_indicator {
@@ -1312,6 +1336,7 @@ impl Session {
1312
1336
  sink,
1313
1337
  indicator
1314
1338
  .as_ref()
1339
+ .filter(|_| !desktop)
1315
1340
  .map(crate::indicator::Indicator::position),
1316
1341
  Box::new({
1317
1342
  let status = capture_status.clone();
@@ -1485,6 +1510,10 @@ impl Session {
1485
1510
  },
1486
1511
  #[cfg(target_os = "linux")]
1487
1512
  window: Mutex::new(window),
1513
+ #[cfg(target_os = "linux")]
1514
+ cue_output: Mutex::new(None),
1515
+ #[cfg(target_os = "linux")]
1516
+ point_output: Mutex::new(None),
1488
1517
  capture: Mutex::new(Some(capture)),
1489
1518
  layout: Mutex::new(Some(layout)),
1490
1519
  #[cfg(target_os = "linux")]
@@ -1585,6 +1614,10 @@ impl Session {
1585
1614
  point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
1586
1615
  #[cfg(target_os = "linux")]
1587
1616
  window: Mutex::new(None),
1617
+ #[cfg(target_os = "linux")]
1618
+ cue_output: Mutex::new(None),
1619
+ #[cfg(target_os = "linux")]
1620
+ point_output: Mutex::new(None),
1588
1621
  capture: Mutex::new(None),
1589
1622
  layout: Mutex::new(None),
1590
1623
  #[cfg(target_os = "linux")]
@@ -1731,6 +1764,10 @@ impl Session {
1731
1764
  if let Some(helper) = overlay.take() {
1732
1765
  helper.stop();
1733
1766
  }
1767
+ #[cfg(target_os = "linux")]
1768
+ {
1769
+ *lock(&self.inner.point_output) = None;
1770
+ }
1734
1771
  return Ok(());
1735
1772
  }
1736
1773
  let (x, y) = params
@@ -1754,6 +1791,36 @@ impl Session {
1754
1791
  "timeout must be 1..120000 ms; label must be at most 96 printable ASCII bytes",
1755
1792
  ));
1756
1793
  }
1794
+ // A located window's point shows where a press there would land, on
1795
+ // the output holding it; the helper moves only when that output
1796
+ // changes, so repeats on one output reuse it with no new process.
1797
+ #[cfg(target_os = "linux")]
1798
+ let landed = match lock(&self.inner.window).as_ref() {
1799
+ Some(window) => {
1800
+ let (px, py) = window
1801
+ .locate(x, y)
1802
+ .map_err(|message| SessionError::new("indicator-unavailable", message))?;
1803
+ Some((px.floor() as i64, py.floor() as i64))
1804
+ }
1805
+ None => None,
1806
+ };
1807
+ #[cfg(target_os = "linux")]
1808
+ let landed_output: Option<String> = match landed {
1809
+ Some((px, py)) => {
1810
+ crate::hyprland::monitor_containing(px as f64, py as f64).unwrap_or(None)
1811
+ }
1812
+ None => None,
1813
+ };
1814
+ #[cfg(target_os = "linux")]
1815
+ if match (&landed, &landed_output) {
1816
+ (Some(_), output) => lock(&self.inner.point_output).clone() != *output,
1817
+ (None, _) => overlay.is_none(),
1818
+ } {
1819
+ if let Some(helper) = overlay.take() {
1820
+ helper.stop();
1821
+ }
1822
+ *lock(&self.inner.point_output) = landed_output.clone();
1823
+ }
1757
1824
  #[cfg(not(target_os = "windows"))]
1758
1825
  if overlay.is_none() {
1759
1826
  #[cfg(target_os = "linux")]
@@ -1762,7 +1829,10 @@ impl Session {
1762
1829
  PointDesktop::Wayland => crate::indicator::Indicator::start_point_wayland(
1763
1830
  source.width,
1764
1831
  source.height,
1765
- source.position,
1832
+ landed
1833
+ .map(|(x, y)| (x as i32, y as i32))
1834
+ .or(source.position),
1835
+ landed.is_some(),
1766
1836
  ),
1767
1837
  PointDesktop::Unavailable(_) => unreachable!("validated point target"),
1768
1838
  };
@@ -1784,6 +1854,8 @@ impl Session {
1784
1854
  }
1785
1855
  })?);
1786
1856
  }
1857
+ #[cfg(target_os = "linux")]
1858
+ let (x, y) = landed.unwrap_or((x, y));
1787
1859
  #[cfg(not(target_os = "windows"))]
1788
1860
  return overlay
1789
1861
  .as_mut()
@@ -2110,6 +2182,11 @@ impl Inner {
2110
2182
  if let Some(helper) = lock(&self.point_overlay).take() {
2111
2183
  helper.stop();
2112
2184
  }
2185
+ #[cfg(target_os = "linux")]
2186
+ {
2187
+ *lock(&self.cue_output) = None;
2188
+ *lock(&self.point_output) = None;
2189
+ }
2113
2190
  // Release first, then tear down: a stuck modifier is the one failure the
2114
2191
  // user cannot undo by reconnecting.
2115
2192
  if let Ok(mut input) = self.input.lock() {
@@ -2239,7 +2316,7 @@ impl Inner {
2239
2316
  }
2240
2317
  *last = seq;
2241
2318
  }
2242
- let feedback = match &message {
2319
+ let mut feedback = match &message {
2243
2320
  ControlMessage::Pointer { phase, x, y, .. } if lock(&self.encoded).is_none() => {
2244
2321
  let (x, y) = to_source_pixels(*x, *y, self.encoded_size(), &self.source);
2245
2322
  Some((
@@ -2271,11 +2348,6 @@ impl Inner {
2271
2348
  let located = lock(&self.window).is_some();
2272
2349
  #[cfg(not(target_os = "linux"))]
2273
2350
  let located = false;
2274
- let feedback = if located {
2275
- feedback.filter(|(kind, ..)| *kind == 'T')
2276
- } else {
2277
- feedback
2278
- };
2279
2351
  if self.source.source_type.as_deref() == Some("Window")
2280
2352
  && !located
2281
2353
  && matches!(&message, ControlMessage::Pointer { phase, .. } if !matches!(phase, PointerPhase::Cancel))
@@ -2294,6 +2366,10 @@ impl Inner {
2294
2366
  self.reject(seq, "input-unavailable", "no input backend");
2295
2367
  return;
2296
2368
  }
2369
+ // Only a located window-share press can move the cue helper: other
2370
+ // paths have no desktop point to choose an output from.
2371
+ #[cfg(target_os = "linux")]
2372
+ let located_press = located && matches!(&message, ControlMessage::Pointer { .. });
2297
2373
  let outcome = match message {
2298
2374
  ControlMessage::Pointer {
2299
2375
  phase,
@@ -2301,7 +2377,14 @@ impl Inner {
2301
2377
  y,
2302
2378
  button,
2303
2379
  ..
2304
- } => self.pointer(phase, x, y, button, seq),
2380
+ } => self.pointer(phase, x, y, button, seq).map(|landed| {
2381
+ // A located window's cue goes where the press landed, or nowhere.
2382
+ if located {
2383
+ feedback = feedback
2384
+ .zip(landed)
2385
+ .map(|((kind, ..), (x, y))| (kind, x, y));
2386
+ }
2387
+ }),
2305
2388
  ControlMessage::Wheel { dx, dy, .. } => self.wheel(dx, dy, seq),
2306
2389
  ControlMessage::Key {
2307
2390
  name,
@@ -2330,6 +2413,14 @@ impl Inner {
2330
2413
  };
2331
2414
  match outcome {
2332
2415
  Ok(()) => {
2416
+ #[cfg(target_os = "linux")]
2417
+ if located_press {
2418
+ // Onto the landed output before the cue is told where to
2419
+ // draw, so a cross-output tap draws there, not nowhere.
2420
+ if let Some((_, x, y)) = feedback {
2421
+ self.move_cue_to(x, y);
2422
+ }
2423
+ }
2333
2424
  if let Some((kind, x, y)) = feedback {
2334
2425
  if let Ok(mut indicator) = self.indicator.lock() {
2335
2426
  if let Some(indicator) = indicator.as_mut() {
@@ -2395,6 +2486,49 @@ impl Inner {
2395
2486
  }
2396
2487
  }
2397
2488
 
2489
+ /// Move the agent cue helper onto the output holding desktop point
2490
+ /// `(x, y)`, where a located press just landed. A press on the helper's
2491
+ /// own output reuses it with no new process; anything else respawns it
2492
+ /// with the landed origin, which selects that output through the point
2493
+ /// helper's xdg-output selection. Best-effort: when the output or the
2494
+ /// respawn is unavailable, the standing helper keeps the cue.
2495
+ #[cfg(target_os = "linux")]
2496
+ fn move_cue_to(&self, x: i64, y: i64) {
2497
+ let output = match crate::hyprland::monitor_containing(x as f64, y as f64) {
2498
+ Ok(output) => output,
2499
+ Err(error) => {
2500
+ log::warn!("agent cue stays on its output: {error}");
2501
+ return;
2502
+ }
2503
+ };
2504
+ if lock(&self.cue_output).clone() == output {
2505
+ return;
2506
+ }
2507
+ let Some(name) = output else { return };
2508
+ let mut indicator = lock(&self.indicator);
2509
+ if let Some(helper) = indicator.take() {
2510
+ helper.stop();
2511
+ }
2512
+ let (width, height) = (
2513
+ self.source.width.max(1) as usize,
2514
+ self.source.height.max(1) as usize,
2515
+ );
2516
+ match crate::indicator::Indicator::start_wayland(
2517
+ width,
2518
+ height,
2519
+ true,
2520
+ Some((x as i32, y as i32)),
2521
+ ) {
2522
+ Ok(helper) => {
2523
+ *indicator = Some(helper);
2524
+ *lock(&self.cue_output) = Some(name);
2525
+ }
2526
+ Err(error) => {
2527
+ log::warn!("agent cue stays on its output: the {name} helper did not start: {error:#}");
2528
+ }
2529
+ }
2530
+ }
2531
+
2398
2532
  fn pointer(
2399
2533
  &self,
2400
2534
  phase: PointerPhase,
@@ -2402,7 +2536,7 @@ impl Inner {
2402
2536
  y: i64,
2403
2537
  button: i64,
2404
2538
  _seq: u64,
2405
- ) -> std::result::Result<(), (&'static str, String)> {
2539
+ ) -> std::result::Result<Option<(i64, i64)>, (&'static str, String)> {
2406
2540
  let (width, height) = self.encoded_size();
2407
2541
  if !(0..width as i64).contains(&x) || !(0..height as i64).contains(&y) {
2408
2542
  return Err((
@@ -2421,19 +2555,21 @@ impl Inner {
2421
2555
  .place(x, y)
2422
2556
  .map_err(|message| ("input-unavailable", message))
2423
2557
  };
2558
+ // The desktop point the pointer landed on, where its cue belongs.
2424
2559
  return match phase {
2425
- PointerPhase::Move => place(),
2560
+ PointerPhase::Move => place().map(Some),
2426
2561
  PointerPhase::Down => {
2427
2562
  // Refused before the pointer moves: nothing could press.
2428
2563
  self.with_input(|_| Ok(()))?;
2429
- place()?;
2430
- self.with_input(|target| target.button(Button::from_number(button), true))
2564
+ let point = place()?;
2565
+ self.with_input(|target| target.button(Button::from_number(button), true))?;
2566
+ Ok(Some(point))
2431
2567
  }
2432
2568
  PointerPhase::Up => {
2433
2569
  self.with_input(|target| target.button(Button::from_number(button), false))?;
2434
- place()
2570
+ place().map(Some)
2435
2571
  }
2436
- PointerPhase::Cancel => self.with_input(InputTarget::release_all),
2572
+ PointerPhase::Cancel => self.with_input(InputTarget::release_all).map(|()| None),
2437
2573
  };
2438
2574
  }
2439
2575
  self.with_input(|target| match phase {
@@ -2448,6 +2584,7 @@ impl Inner {
2448
2584
  }
2449
2585
  PointerPhase::Cancel => target.release_all(),
2450
2586
  })
2587
+ .map(|()| None)
2451
2588
  }
2452
2589
 
2453
2590
  fn wheel(
@@ -2770,6 +2907,12 @@ fn auto_bitrate(width: usize, height: usize) -> u32 {
2770
2907
  const QUEUE_HIGH: Duration = Duration::from_millis(25);
2771
2908
  /// Below this the path has room again and the target may creep back up.
2772
2909
  const QUEUE_LOW: Duration = Duration::from_millis(10);
2910
+ /// The same thresholds for a TCP path. TCP never drops, so its queue is read
2911
+ /// as delay alone, and a relay's bridge adds 20-30 ms of delivery jitter that is
2912
+ /// not queueing: at 25/10 ms that jitter backs the target off to its floor
2913
+ /// with the link idle. Only a queue that stands over 100 ms is a backlog here.
2914
+ const QUEUE_HIGH_TCP: Duration = Duration::from_millis(100);
2915
+ const QUEUE_LOW_TCP: Duration = Duration::from_millis(60);
2773
2916
  /// How often queueing delay may back the target off: long enough for the last
2774
2917
  /// back-off to reach the far end and show in its feedback, short enough that a
2775
2918
  /// queue is drained in a few steps rather than a few seconds.
@@ -2791,6 +2934,8 @@ struct RateControl {
2791
2934
  last_change: Option<Instant>,
2792
2935
  /// The last queueing delay reported, while the path reports one.
2793
2936
  queue: Option<Duration>,
2937
+ queue_high: Duration,
2938
+ queue_low: Duration,
2794
2939
  }
2795
2940
 
2796
2941
  impl RateControl {
@@ -2809,9 +2954,21 @@ impl RateControl {
2809
2954
  target: ceiling,
2810
2955
  last_change: None,
2811
2956
  queue: None,
2957
+ queue_high: QUEUE_HIGH,
2958
+ queue_low: QUEUE_LOW,
2812
2959
  }
2813
2960
  }
2814
2961
 
2962
+ /// Read queueing delay against the thresholds for the transport the selected
2963
+ /// pair runs over.
2964
+ fn set_tcp(&mut self, tcp: bool) {
2965
+ (self.queue_high, self.queue_low) = if tcp {
2966
+ (QUEUE_HIGH_TCP, QUEUE_LOW_TCP)
2967
+ } else {
2968
+ (QUEUE_HIGH, QUEUE_LOW)
2969
+ };
2970
+ }
2971
+
2815
2972
  /// The new target when these loss reports (RFC 3550 fractions, /256) or
2816
2973
  /// this path report move it: down on a standing queue at most every
2817
2974
  /// `DELAY_BACKOFF_EVERY`, otherwise at most once a second.
@@ -2822,7 +2979,7 @@ impl RateControl {
2822
2979
  let since = self.last_change.map(|at| now.duration_since(at));
2823
2980
  let waited = |gap: Duration| since.is_none_or(|since| since >= gap);
2824
2981
  let floor = (self.ceiling / 8).max(500);
2825
- let next = if let Some(path) = path.filter(|path| path.queue_delay > QUEUE_HIGH) {
2982
+ let next = if let Some(path) = path.filter(|path| path.queue_delay > self.queue_high) {
2826
2983
  if !waited(DELAY_BACKOFF_EVERY) {
2827
2984
  return None;
2828
2985
  }
@@ -2834,7 +2991,7 @@ impl RateControl {
2834
2991
  return None;
2835
2992
  }
2836
2993
  let loss = worst as f64 / 256.0;
2837
- let roomy = self.queue.is_none_or(|queue| queue < QUEUE_LOW);
2994
+ let roomy = self.queue.is_none_or(|queue| queue < self.queue_low);
2838
2995
  if loss > 0.10 {
2839
2996
  ((self.target as f64 * 0.7) as u32).max(floor)
2840
2997
  } else if loss < 0.02 && roomy {
@@ -3083,6 +3240,9 @@ fn spawn_pipeline(
3083
3240
  }
3084
3241
  let reports = peer.take_loss_reports();
3085
3242
  let path = peer.take_path_report();
3243
+ if path.is_some() {
3244
+ rate.set_tcp(handle.block_on(peer.selected_pair_is_tcp()));
3245
+ }
3086
3246
  if let Some(kbps) = rate.update(&reports, path, Instant::now()) {
3087
3247
  let applied = inner.encoder.lock().map(|mut encoder| {
3088
3248
  encoder
@@ -3644,6 +3804,10 @@ mod tests {
3644
3804
  point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
3645
3805
  #[cfg(target_os = "linux")]
3646
3806
  window: Mutex::new(None),
3807
+ #[cfg(target_os = "linux")]
3808
+ cue_output: Mutex::new(None),
3809
+ #[cfg(target_os = "linux")]
3810
+ point_output: Mutex::new(None),
3647
3811
  capture: Mutex::new(None),
3648
3812
  layout: Mutex::new(None),
3649
3813
  #[cfg(target_os = "linux")]
@@ -3723,7 +3887,7 @@ mod tests {
3723
3887
  )
3724
3888
  .await
3725
3889
  .expect("a peer connection builds without a network");
3726
- test_inner_with(events, peer, 64, 64)
3890
+ test_inner_with(events, Arc::new(peer), 64, 64)
3727
3891
  }
3728
3892
 
3729
3893
  #[cfg(target_os = "linux")]
@@ -3858,7 +4022,7 @@ mod tests {
3858
4022
 
3859
4023
  fn test_inner_with(
3860
4024
  events: tokio_mpsc::UnboundedSender<Notice>,
3861
- peer: VideoPeer,
4025
+ peer: Arc<VideoPeer>,
3862
4026
  width: usize,
3863
4027
  height: usize,
3864
4028
  ) -> (Arc<Inner>, Arc<Mutex<Vec<(i16, bool)>>>) {
@@ -3886,7 +4050,7 @@ mod tests {
3886
4050
  latest: Arc::new(Mutex::new(None)),
3887
4051
  capture_error: Arc::new(Mutex::new(None)),
3888
4052
  frame_changes: tokio::sync::watch::channel(0).1,
3889
- peer: Mutex::new(Some(Arc::new(peer))),
4053
+ peer: Mutex::new(Some(peer)),
3890
4054
  encoder: Mutex::new(Some(VideoEncoder::Vp9(
3891
4055
  Encoder::new(width, height, 1000, 30, 1).expect("an encoder"),
3892
4056
  ))),
@@ -3905,6 +4069,10 @@ mod tests {
3905
4069
  point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
3906
4070
  #[cfg(target_os = "linux")]
3907
4071
  window: Mutex::new(None),
4072
+ #[cfg(target_os = "linux")]
4073
+ cue_output: Mutex::new(None),
4074
+ #[cfg(target_os = "linux")]
4075
+ point_output: Mutex::new(None),
3908
4076
  layout: Mutex::new(Some(Layout::from_environment().expect("a keymap"))),
3909
4077
  #[cfg(target_os = "linux")]
3910
4078
  audio: Mutex::new(None),
@@ -4484,7 +4652,7 @@ mod tests {
4484
4652
  let codec = crate::peer::tests::h264_receiver_codec();
4485
4653
  let (peer, _) = crate::peer::tests::answered(vec![codec.clone()]).await;
4486
4654
  let (events, mut received) = tokio_mpsc::unbounded_channel();
4487
- let (inner, _) = test_inner_with(events, peer, 64, 64);
4655
+ let (inner, _) = test_inner_with(events, Arc::new(peer), 64, 64);
4488
4656
  let session = Session { inner };
4489
4657
  let mut offers = Vec::new();
4490
4658
  for _ in 0..3 {
@@ -4656,6 +4824,72 @@ mod tests {
4656
4824
  assert_eq!((later.codec, later.encoder), ("vp9", "libvpx"));
4657
4825
  }
4658
4826
 
4827
+ /// The rate target after 3 s of delivery jitter on a connected pair: a queue
4828
+ /// that alternates between 5 and 30 ms with nothing lost, and 800 kbps
4829
+ /// delivered. The pipeline runs for real; only the path reports are fed.
4830
+ async fn target_under_bridge_jitter(tcp: bool) -> u32 {
4831
+ let peers = crate::peer::tests::connected(
4832
+ if tcp {
4833
+ Vec::new()
4834
+ } else {
4835
+ vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))]
4836
+ },
4837
+ if tcp {
4838
+ vec![std::net::SocketAddr::from(([127, 0, 0, 1], 0))]
4839
+ } else {
4840
+ Vec::new()
4841
+ },
4842
+ tcp,
4843
+ )
4844
+ .await;
4845
+ let engine = Arc::clone(&peers.engine);
4846
+ assert_eq!(
4847
+ engine.selected_pair_is_tcp().await,
4848
+ tcp,
4849
+ "the selected pair must run over {}",
4850
+ if tcp { "TCP" } else { "UDP" }
4851
+ );
4852
+ let (events, _events_rx) = tokio_mpsc::unbounded_channel();
4853
+ let (inner, _recorded) = test_inner_with(events, Arc::clone(&engine), 64, 64);
4854
+ let (_frame_tx, frame_rx) = latest_frame();
4855
+ spawn_pipeline(
4856
+ &inner,
4857
+ Arc::clone(&engine),
4858
+ frame_rx,
4859
+ Arc::new(AtomicBool::new(true)),
4860
+ 30,
4861
+ 1000,
4862
+ );
4863
+ for step in 0..300 {
4864
+ let queue_delay = Duration::from_millis(if step % 2 == 0 { 5 } else { 30 });
4865
+ engine.report_path(PathReport {
4866
+ queue_delay,
4867
+ delivered_kbps: 800,
4868
+ });
4869
+ tokio::time::sleep(Duration::from_millis(10)).await;
4870
+ }
4871
+ let target = inner.metrics.lock().unwrap().target_kbps;
4872
+ drop(peers);
4873
+ target
4874
+ }
4875
+
4876
+ #[tokio::test(flavor = "multi_thread")]
4877
+ async fn a_tcp_path_with_bridge_jitter_holds_its_rate_ceiling() {
4878
+ assert_eq!(
4879
+ target_under_bridge_jitter(true).await,
4880
+ 1000,
4881
+ "a 5-30 ms jitter that is not a queue must not back a TCP path off"
4882
+ );
4883
+ }
4884
+
4885
+ #[tokio::test(flavor = "multi_thread")]
4886
+ async fn a_udp_path_with_the_same_jitter_still_backs_off() {
4887
+ assert!(
4888
+ target_under_bridge_jitter(false).await < 1000,
4889
+ "a UDP path reads the same 30 ms as a standing queue and must back off"
4890
+ );
4891
+ }
4892
+
4659
4893
  #[tokio::test]
4660
4894
  async fn a_receiver_that_asks_for_h264_is_sent_h264_from_the_first_frame() {
4661
4895
  let Some(info) = tokio::task::spawn_blocking(crate::h264::probe)
@@ -4668,7 +4902,7 @@ mod tests {
4668
4902
  let (peer, _answer) =
4669
4903
  crate::peer::tests::answered(vec![crate::peer::tests::h264_receiver_codec()]).await;
4670
4904
  let (events, _events_rx) = tokio_mpsc::unbounded_channel();
4671
- let (inner, _recorded) = test_inner_with(events, peer, 320, 240);
4905
+ let (inner, _recorded) = test_inner_with(events, Arc::new(peer), 320, 240);
4672
4906
  // The answer is real, so ICE runs and, with no candidates, never
4673
4907
  // connects; each state it reports would take back `assume_connected`.
4674
4908
  let pinned = current_peer(&inner).expect("the test's peer");
@@ -97,8 +97,8 @@ No public API changed; no signalling or wire change.
97
97
 
98
98
  Do this once upstream rtc-ice keeps the previous selected pair across a restart
99
99
  on its own and nominates a freshly validated pair without waiting for the next
100
- check tick (an upstream PR is a follow-up; it needs the captain's word first,
101
- since it would carry his name).
100
+ check tick (an upstream PR is a follow-up; that needs a maintainer decision first,
101
+ since the contribution would carry its author's name).
102
102
 
103
103
  ## Dev-only measurement instrumentation (not part of the patch)
104
104
 
@@ -10,7 +10,7 @@ use stun::textattrs::*;
10
10
 
11
11
  use crate::attributes::{control::*, priority::*, use_candidate::*};
12
12
  use crate::candidate::{candidate_pair::*, *};
13
- // desklink scout trace (dl-roam-postnom-attr):
13
+ // desklink scout trace: dev-only ICE timeline instrumentation.
14
14
  use crate::dl_trace;
15
15
  use crate::dl_trace::dl_ice;
16
16
 
@@ -38,7 +38,7 @@ use stun::xoraddr::*;
38
38
 
39
39
  use crate::candidate::candidate_peer_reflexive::CandidatePeerReflexiveConfig;
40
40
  use crate::candidate::{candidate_pair::*, *};
41
- // desklink scout trace (dl-roam-postnom-attr):
41
+ // desklink scout trace: dev-only ICE timeline instrumentation.
42
42
  use crate::dl_trace;
43
43
  use crate::dl_trace::dl_ice;
44
44
  use crate::mdns::{MulticastDnsMode, create_multicast_dns, generate_multicast_dns_name};
@@ -1,7 +1,7 @@
1
1
  //! desklink scout instrumentation (dev-only, `DESKLINK_ICE_TRACE=1`).
2
2
  //!
3
3
  //! Timestamped stderr trace of the ICE agent's restart/check/nomination timeline
4
- //! for task dl-roam-postnom-attr. Not part of the shipped patch; see
4
+ //! for ICE restart diagnosis. Not part of the shipped patch; see
5
5
  //! DESKLINK-PATCH.md. Every call site is marked `// desklink scout trace:` so the
6
6
  //! whole diff is trivially droppable.
7
7
 
@@ -58,7 +58,7 @@
58
58
  /// without adding — and version-matching — a direct `rtc-crypto` dependency.
59
59
  pub use crypto;
60
60
 
61
- // desklink scout trace: dev-only ICE timeline instrumentation (dl-roam-postnom-attr).
61
+ // desklink scout trace: dev-only ICE timeline instrumentation.
62
62
  pub(crate) mod dl_trace;
63
63
 
64
64
  pub mod agent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desklink/host",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,7 +38,7 @@
38
38
  "ws": "8.21.3"
39
39
  },
40
40
  "optionalDependencies": {
41
- "@desklink/host-linux-x64-gnu": "0.5.7",
42
- "@desklink/host-darwin-arm64": "0.5.7"
41
+ "@desklink/host-linux-x64-gnu": "0.5.9",
42
+ "@desklink/host-darwin-arm64": "0.5.9"
43
43
  }
44
44
  }