@desklink/host 0.4.0 → 0.5.1

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.
@@ -895,7 +895,7 @@ pub fn capabilities() -> serde_json::Value {
895
895
  "formats": ["bgrx", "bgra", "rgbx", "rgba"],
896
896
  "cursor": "embedded",
897
897
  "cursor_modes": ["embedded", "hidden"],
898
- "cursor_positions": {"x11": true, "portal": "negotiated"},
898
+ "cursor_positions": {"x11": true, "wayland": "ext-image-copy-capture-v1"},
899
899
  "audio": false,
900
900
  },
901
901
  "encode": encode_capabilities(),
@@ -1205,8 +1205,8 @@ impl Session {
1205
1205
  }
1206
1206
  .map_err(|error| {
1207
1207
  let message = format!("{error:#}");
1208
- let code = if message.contains("cursor_positions_unavailable:") {
1209
- "cursor_positions_unavailable"
1208
+ let code = if message.contains("cursor-unavailable:") {
1209
+ "cursor-unavailable"
1210
1210
  } else {
1211
1211
  "source"
1212
1212
  };
@@ -1264,12 +1264,11 @@ impl Session {
1264
1264
  )
1265
1265
  .map_err(|error| SessionError::new("source", format!("{error:#}")))?;
1266
1266
  #[cfg(target_os = "linux")]
1267
- let capture = capture::start(
1267
+ let mut capture = capture::start(
1268
1268
  portal,
1269
1269
  width,
1270
1270
  height,
1271
1271
  max_fps,
1272
- cursor_sink,
1273
1272
  sink,
1274
1273
  indicator
1275
1274
  .as_ref()
@@ -1281,16 +1280,31 @@ impl Session {
1281
1280
  )
1282
1281
  .map_err(|error| SessionError::new("source", format!("{error:#}")))?;
1283
1282
  #[cfg(target_os = "linux")]
1284
- {
1285
- if request.cursor == crate::protocol::CursorMode::Hidden
1286
- && !capture.cursor_positions()
1287
- {
1288
- return Err(SessionError::new(
1289
- "cursor_positions_unavailable",
1290
- "portal stream did not provide SPA cursor metadata",
1291
- ));
1283
+ if let Some(cursor_sink) = cursor_sink {
1284
+ // Positions are an addition to hidden capture, never a
1285
+ // condition of it: without them the stream still runs and
1286
+ // the reply says why the receiver gets no cursor.
1287
+ cursor_info["source"] = serde_json::json!(crate::wl_cursor::PROTOCOL);
1288
+ let limitation = match crate::wl_cursor::start(
1289
+ (source.origin_x, source.origin_y),
1290
+ (source_w, source_h),
1291
+ cursor_sink,
1292
+ Duration::from_secs(1),
1293
+ ) {
1294
+ Ok((tracker, observed)) => {
1295
+ capture.cursor = Some(tracker);
1296
+ cursor_info["positions"] = serde_json::json!(observed);
1297
+ (!observed).then_some("the compositor has sent no cursor position yet: the cursor is off the captured output, the seat has no pointer, or the compositor withholds positions from this engine")
1298
+ }
1299
+ Err(error) => {
1300
+ log::warn!("hidden cursor capture has no cursor positions: {error:#}");
1301
+ Some("this compositor does not offer ext-image-copy-capture-v1 cursor sessions, so the cursor is hidden and its position is unknown")
1302
+ }
1303
+ };
1304
+ if let Some(limitation) = limitation {
1305
+ log::warn!("{limitation}");
1306
+ cursor_info["limitation"] = serde_json::json!(limitation);
1292
1307
  }
1293
- cursor_info["positions"] = serde_json::json!(capture.cursor_positions());
1294
1308
  }
1295
1309
  #[cfg(target_os = "linux")]
1296
1310
  if let Some(indicator) = indicator.as_mut() {
@@ -1319,7 +1333,7 @@ impl Session {
1319
1333
  let encoder = Encoder::new(
1320
1334
  width,
1321
1335
  height,
1322
- bitrate_kbps,
1336
+ RateControl::starting(bitrate_kbps).target,
1323
1337
  max_fps,
1324
1338
  available_parallelism().min(8) as u32,
1325
1339
  )
@@ -2563,6 +2577,15 @@ struct RateControl {
2563
2577
  }
2564
2578
 
2565
2579
  impl RateControl {
2580
+ /// Begin below an unknown weak link's capacity, then use the existing
2581
+ /// loss/delay feedback growth to approach the requested ceiling.
2582
+ fn starting(ceiling: u32) -> Self {
2583
+ Self {
2584
+ target: ceiling.min(3000),
2585
+ ..Self::new(ceiling)
2586
+ }
2587
+ }
2588
+
2566
2589
  fn new(ceiling: u32) -> Self {
2567
2590
  Self {
2568
2591
  ceiling,
@@ -2659,9 +2682,9 @@ impl SendBudget {
2659
2682
  }
2660
2683
  }
2661
2684
 
2662
- /// Whether motion is coded at half size. A full-size frame is slow when it
2663
- /// took longer to code than a frame interval and the source already had a
2664
- /// newer frame waiting: the encoder, not the source, set the pace. Content that
2685
+ /// Whether motion is coded at half size. A full-size frame is slow when its
2686
+ /// encoding or constrained-link drain time exceeds a frame interval and a
2687
+ /// newer frame or send-budget debt is waiting. Content that
2665
2688
  /// arrives slower than the frame cap (30 fps into a 60 fps session) is never
2666
2689
  /// slow however long a frame takes, as long as it is done before the next one.
2667
2690
  /// Several slow frames that each change much of the picture (a packet over a
@@ -2677,15 +2700,31 @@ impl SendBudget {
2677
2700
  struct MotionSize {
2678
2701
  half: bool,
2679
2702
  run: u32,
2703
+ link_run: u32,
2680
2704
  }
2681
2705
 
2682
2706
  impl MotionSize {
2683
2707
  const SLOW_RUN: u32 = 5;
2684
2708
  const SMALL_RUN: u32 = 30;
2685
2709
 
2686
- /// Account for a motion frame coded at the current size in `took`; `large`
2710
+ /// Persistent send debt needs a quicker response than a transient slow
2711
+ /// encode: every extra full-size motion frame extends the remote queue.
2712
+ fn queued(&mut self, drain: Duration, debt: bool, interval: Duration) {
2713
+ self.link_run = if !self.half && debt && drain > interval {
2714
+ self.link_run + 1
2715
+ } else {
2716
+ 0
2717
+ };
2718
+ if self.link_run >= 2 {
2719
+ self.half = true;
2720
+ self.run = 0;
2721
+ self.link_run = 0;
2722
+ }
2723
+ }
2724
+
2725
+ /// Account for a motion frame serviced at the current size in `took`; `large`
2687
2726
  /// when its packet was over a quarter of the per-frame rate budget, `behind`
2688
- /// when a newer frame was already waiting once it was coded.
2727
+ /// when a newer frame or send-budget debt was waiting once it was coded.
2689
2728
  fn coded(&mut self, took: Duration, large: bool, behind: bool, interval: Duration) {
2690
2729
  let slow = took > interval && behind;
2691
2730
  let (counts, needed) = if self.half {
@@ -2762,9 +2801,9 @@ fn spawn_pipeline(
2762
2801
  .name("desklink-encode".into())
2763
2802
  .spawn(move || {
2764
2803
  let interval = frame_interval(max_fps);
2765
- let mut rate = RateControl::new(bitrate_kbps);
2804
+ let mut rate = RateControl::starting(bitrate_kbps);
2766
2805
  if let Ok(mut m) = inner.metrics.lock() {
2767
- m.target_kbps = bitrate_kbps;
2806
+ m.target_kbps = rate.target;
2768
2807
  (m.codec, m.encoder) = ("vp9", "libvpx");
2769
2808
  }
2770
2809
  // Whether motion may be coded at half size: VP9's alone.
@@ -2944,16 +2983,26 @@ fn spawn_pipeline(
2944
2983
  Pass::Motion { half: false, .. } | Pass::Keepalive => {}
2945
2984
  }
2946
2985
  }
2986
+ budget.sent(now, packet.data.len(), rate.target);
2947
2987
  match pass {
2948
2988
  Pass::Motion { .. } => {
2949
2989
  pending = false;
2950
2990
  keyframe = false;
2951
2991
  refined = false;
2952
2992
  still_since = now;
2953
- let budget = rate.target as u64 * 1000 / 8 / max_fps.max(1) as u64;
2954
- let large = packet.data.len() as u64 * 4 > budget;
2993
+ let frame_budget = rate.target as u64 * 1000 / 8 / max_fps.max(1) as u64;
2994
+ let large = packet.data.len() as u64 * 4 > frame_budget;
2955
2995
  if halves {
2996
+ // A fast encoder can still outrun a constrained link.
2997
+ // Include the packet's drain time once the send
2998
+ // budget is holding frames, so sustained motion
2999
+ // reduces its size before a backlog builds.
3000
+ let drain = Duration::from_secs_f64(
3001
+ packet.data.len() as f64 * 8.0 / SendBudget::rate(rate.target),
3002
+ );
3003
+ let link_behind = !budget.wait(Instant::now(), rate.target).is_zero();
2956
3004
  size.coded(took, large, frame_rx.waiting(), interval);
3005
+ size.queued(drain, link_behind, interval);
2957
3006
  }
2958
3007
  }
2959
3008
  Pass::Refine => {
@@ -2964,7 +3013,6 @@ fn spawn_pipeline(
2964
3013
  }
2965
3014
  last_sent = Some(now);
2966
3015
  cadence.sent(now);
2967
- budget.sent(now, packet.data.len(), rate.target);
2968
3016
  // A new picture carries the moment it was captured, so the
2969
3017
  // receiver paces playout by the desktop's clock rather than by
2970
3018
  // encode time; a re-coded one (refinement, keepalive) is new
@@ -3403,66 +3451,6 @@ mod tests {
3403
3451
  );
3404
3452
  }
3405
3453
 
3406
- #[test]
3407
- fn tile_damage_at_origin_does_not_underflow_or_hide_other_tiles() {
3408
- assert_eq!(
3409
- dirty_regions(&[1, 0, 0, 1], &[0; 4], 64, 64),
3410
- vec![[0, 0, 32, 32], [32, 32, 32, 32]]
3411
- );
3412
- }
3413
-
3414
- #[test]
3415
- fn frames_that_jitter_around_the_cap_go_at_once_and_a_flood_stays_capped() {
3416
- let interval = Duration::from_millis(20);
3417
- let start = Instant::now();
3418
- let mut cadence = Cadence::new(interval, start);
3419
- // Captured at the cap, each a few ms early or late: none waits, so
3420
- // none is delayed or superseded by the next.
3421
- for (n, jitter) in [0i64, -6, 5, -8, 7, -3, 0, -9, 9, -5].iter().enumerate() {
3422
- let at = start + interval * n as u32 + Duration::from_millis(9);
3423
- let at = if *jitter < 0 {
3424
- at - Duration::from_millis(jitter.unsigned_abs())
3425
- } else {
3426
- at + Duration::from_millis(*jitter as u64)
3427
- };
3428
- assert_eq!(cadence.wait(at), Duration::ZERO, "frame {n} waited");
3429
- cadence.sent(at);
3430
- }
3431
-
3432
- // A source far above the cap: one second of 1 ms frames sends at most
3433
- // one frame over the cap.
3434
- let start = start + Duration::from_secs(10);
3435
- let mut cadence = Cadence::new(interval, start);
3436
- let sent = (0..1000)
3437
- .map(|ms| start + Duration::from_millis(ms))
3438
- .filter(|&at| {
3439
- let go = cadence.wait(at).is_zero();
3440
- if go {
3441
- cadence.sent(at);
3442
- }
3443
- go
3444
- })
3445
- .count();
3446
- assert!((50..=51).contains(&sent), "sent {sent} at a 50 fps cap");
3447
- }
3448
-
3449
- #[test]
3450
- fn a_small_source_is_never_upscaled() {
3451
- assert_eq!(fit(1000, 600, 1280, 800), (1000, 600));
3452
- }
3453
-
3454
- #[test]
3455
- fn a_large_source_is_fitted_into_the_box_with_even_dimensions() {
3456
- let (w, h) = fit(2560, 1440, 1280, 800);
3457
- assert_eq!((w, h), (1280, 720));
3458
- assert_eq!((w % 2, h % 2), (0, 0));
3459
- }
3460
-
3461
- #[test]
3462
- fn fit_keeps_the_aspect_ratio_when_the_box_is_the_constraint() {
3463
- assert_eq!(fit(2560, 1440, 1200, 600), (1066, 600));
3464
- }
3465
-
3466
3454
  #[test]
3467
3455
  fn an_encoded_surface_pixel_maps_to_the_sources_own_pixel() {
3468
3456
  let source = SelectedSource {
@@ -4324,149 +4312,6 @@ mod tests {
4324
4312
  assert!(inner.input.lock().unwrap().is_none());
4325
4313
  }
4326
4314
 
4327
- #[test]
4328
- fn a_standing_queue_backs_the_rate_off_to_what_the_path_delivers() {
4329
- let start = Instant::now();
4330
- let queued = |ms: u64, delivered_kbps: u32| {
4331
- Some(PathReport {
4332
- queue_delay: Duration::from_millis(ms),
4333
- delivered_kbps,
4334
- })
4335
- };
4336
- let mut rate = RateControl::new(8000);
4337
- // A burst's worth of queue and no loss moves nothing.
4338
- assert_eq!(rate.update(&[], queued(15, 3000), start), None);
4339
- // A standing queue on a 4 Mbps link: 85% of what got through, at once,
4340
- // with no loss reported at all.
4341
- assert_eq!(rate.update(&[], queued(80, 4000), start), Some(3400));
4342
- // Not again until the back-off has had time to show in the feedback.
4343
- assert_eq!(
4344
- rate.update(&[], queued(80, 3400), start + Duration::from_millis(100)),
4345
- None
4346
- );
4347
- assert_eq!(
4348
- rate.update(&[], queued(60, 3400), start + Duration::from_millis(300)),
4349
- Some(2890)
4350
- );
4351
- // A stream that sent little is not taken for a slow link: at most half.
4352
- assert_eq!(
4353
- rate.update(&[], queued(60, 100), start + Duration::from_millis(600)),
4354
- Some(1228)
4355
- );
4356
- // Clean loss reports do not raise it while the queue has not drained...
4357
- assert_eq!(
4358
- rate.update(&[0], queued(15, 1200), start + Duration::from_secs(2)),
4359
- None
4360
- );
4361
- // ...and do once it has.
4362
- assert_eq!(
4363
- rate.update(&[0], queued(2, 1200), start + Duration::from_secs(3)),
4364
- Some(1390)
4365
- );
4366
- // Nothing drops below the floor.
4367
- let mut rate = RateControl::new(8000);
4368
- for step in 0..20u64 {
4369
- rate.update(
4370
- &[],
4371
- queued(200, 1),
4372
- start + DELAY_BACKOFF_EVERY * step as u32,
4373
- );
4374
- }
4375
- assert_eq!(rate.target, 1000);
4376
- }
4377
-
4378
- #[test]
4379
- fn without_transport_feedback_the_rate_follows_loss_alone() {
4380
- let start = Instant::now();
4381
- let mut rate = RateControl::new(8000);
4382
- assert_eq!(rate.update(&[], None, start), None);
4383
- assert_eq!(rate.update(&[64], None, start), Some(5600));
4384
- assert_eq!(
4385
- rate.update(&[64], None, start + Duration::from_millis(500)),
4386
- None
4387
- );
4388
- assert_eq!(
4389
- rate.update(&[10], None, start + Duration::from_secs(1)),
4390
- None
4391
- );
4392
- assert_eq!(
4393
- rate.update(&[0], None, start + Duration::from_secs(2)),
4394
- Some(6112)
4395
- );
4396
- }
4397
-
4398
- #[test]
4399
- fn a_large_frame_holds_the_next_until_the_link_has_carried_it() {
4400
- let start = Instant::now();
4401
- let mut budget = SendBudget::default();
4402
- // Frames at the target, one per 20 ms at 4000 kbps, never wait, and
4403
- // neither does the ordinary overshoot of a frame at twice the budget.
4404
- for n in 0..50u32 {
4405
- let at = start + Duration::from_millis(20) * n;
4406
- assert_eq!(budget.wait(at, 4000), Duration::ZERO, "frame {n} waited");
4407
- budget.sent(at, if n % 5 == 0 { 20_000 } else { 10_000 }, 4000);
4408
- }
4409
- // A 100 kB key frame drains in 160 ms (4000 kbps and a quarter): it
4410
- // goes at once, and the next waits out all of it past the 60 ms burst
4411
- // allowance.
4412
- let at = start + Duration::from_secs(1);
4413
- assert_eq!(budget.wait(at, 4000), Duration::ZERO);
4414
- budget.sent(at, 100_000, 4000);
4415
- let wait = budget.wait(at, 4000);
4416
- assert!(
4417
- (Duration::from_millis(99)..=Duration::from_millis(101)).contains(&wait),
4418
- "waited {wait:?}"
4419
- );
4420
- assert_eq!(budget.wait(at + wait, 4000), Duration::ZERO);
4421
- }
4422
-
4423
- #[test]
4424
- fn motion_halves_only_when_the_encoder_sets_the_pace_and_does_not_flap() {
4425
- let interval = Duration::from_millis(16);
4426
- let (slow, fast) = (Duration::from_millis(30), Duration::from_millis(2));
4427
- let mut size = MotionSize::default();
4428
- for _ in 0..MotionSize::SLOW_RUN * 4 {
4429
- size.coded(slow, false, true, interval);
4430
- }
4431
- assert!(
4432
- !size.half,
4433
- "slow small frames (typing on a busy host) keep full size"
4434
- );
4435
- for _ in 0..MotionSize::SLOW_RUN * 4 {
4436
- size.coded(slow, true, false, interval);
4437
- }
4438
- assert!(
4439
- !size.half,
4440
- "a source slower than the cap (30 fps, 30 ms encode) never halves"
4441
- );
4442
- for _ in 0..MotionSize::SLOW_RUN - 1 {
4443
- size.coded(slow, true, true, interval);
4444
- }
4445
- size.coded(fast, true, true, interval);
4446
- assert!(!size.half, "a broken run of slow frames keeps full size");
4447
- for _ in 0..MotionSize::SLOW_RUN {
4448
- size.coded(slow, true, true, interval);
4449
- }
4450
- assert!(size.half, "a run of slow large frames halves");
4451
- // The marginal band: half-size frames that are small but still slow,
4452
- // between large ones. Neither may send it back to full size.
4453
- for i in 0..MotionSize::SMALL_RUN * 10 {
4454
- size.coded(slow, i % 7 == 0, true, interval);
4455
- assert!(size.half, "marginal half-size motion must not flap back");
4456
- }
4457
- for _ in 0..MotionSize::SMALL_RUN * 2 {
4458
- size.coded(fast, true, true, interval);
4459
- }
4460
- assert!(size.half, "large half-size frames stay half, however fast");
4461
- for _ in 0..MotionSize::SMALL_RUN {
4462
- size.coded(fast, false, true, interval);
4463
- }
4464
- assert!(
4465
- !size.half,
4466
- "a run of small, fast half-size frames returns to full size"
4467
- );
4468
- }
4469
-
4470
4315
  #[tokio::test]
4471
4316
  async fn the_pipeline_caps_the_rate_codes_the_last_frame_and_refines_a_still_desktop_once() {
4472
4317
  let (events, _events_rx) = tokio_mpsc::unbounded_channel();