@desklink/host 0.2.0 → 0.3.0

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.
Files changed (69) hide show
  1. package/NOTICE +23 -0
  2. package/README.md +78 -48
  3. package/bin/desklink-host.mjs +1 -1
  4. package/dist/bridge.d.ts +1 -0
  5. package/dist/bridge.d.ts.map +1 -1
  6. package/dist/bridge.js +12 -4
  7. package/dist/bridge.js.map +1 -1
  8. package/dist/engineProcess.d.ts.map +1 -1
  9. package/dist/engineProcess.js +1 -0
  10. package/dist/engineProcess.js.map +1 -1
  11. package/dist/protocol.d.ts +26 -0
  12. package/dist/protocol.d.ts.map +1 -1
  13. package/dist/protocol.js.map +1 -1
  14. package/dist/resolveEngine.d.ts +1 -1
  15. package/dist/resolveEngine.d.ts.map +1 -1
  16. package/dist/resolveEngine.js +22 -9
  17. package/dist/resolveEngine.js.map +1 -1
  18. package/docs/BOUNDARY.md +15 -7
  19. package/docs/PROTOCOL.md +150 -23
  20. package/engine/Cargo.lock +46 -1
  21. package/engine/Cargo.toml +12 -2
  22. package/engine/build.rs +28 -2
  23. package/engine/native/nvenc_shim.c +453 -0
  24. package/engine/native/vaapi_shim.c +727 -0
  25. package/engine/native/vpx_shim.c +54 -4
  26. package/engine/native/vt_shim.c +448 -0
  27. package/engine/src/capture.rs +75 -55
  28. package/engine/src/capture_mac.rs +24 -14
  29. package/engine/src/convert.rs +286 -61
  30. package/engine/src/encoder.rs +122 -13
  31. package/engine/src/h264.rs +188 -0
  32. package/engine/src/h264_nvenc.rs +308 -0
  33. package/engine/src/h264_openh264.rs +735 -0
  34. package/engine/src/h264_vaapi.rs +263 -0
  35. package/engine/src/h264_vt.rs +368 -0
  36. package/engine/src/mac.rs +6 -3
  37. package/engine/src/main.rs +22 -4
  38. package/engine/src/peer.rs +466 -28
  39. package/engine/src/protocol.rs +15 -2
  40. package/engine/src/session.rs +790 -93
  41. package/engine/src/x11.rs +210 -9
  42. package/engine/src/x11_mac.rs +12 -0
  43. package/engine/vendor/libva/COPYING +19 -0
  44. package/engine/vendor/libva/va/va.h +5450 -0
  45. package/engine/vendor/libva/va/va_dec_av1.h +695 -0
  46. package/engine/vendor/libva/va/va_dec_hevc.h +616 -0
  47. package/engine/vendor/libva/va/va_dec_jpeg.h +157 -0
  48. package/engine/vendor/libva/va/va_dec_vp8.h +249 -0
  49. package/engine/vendor/libva/va/va_dec_vp9.h +312 -0
  50. package/engine/vendor/libva/va/va_dec_vvc.h +688 -0
  51. package/engine/vendor/libva/va/va_drm.h +61 -0
  52. package/engine/vendor/libva/va/va_enc_av1.h +1010 -0
  53. package/engine/vendor/libva/va/va_enc_h264.h +659 -0
  54. package/engine/vendor/libva/va/va_enc_hevc.h +958 -0
  55. package/engine/vendor/libva/va/va_enc_jpeg.h +162 -0
  56. package/engine/vendor/libva/va/va_enc_mpeg2.h +308 -0
  57. package/engine/vendor/libva/va/va_enc_vp8.h +350 -0
  58. package/engine/vendor/libva/va/va_enc_vp9.h +639 -0
  59. package/engine/vendor/libva/va/va_fei.h +170 -0
  60. package/engine/vendor/libva/va/va_fei_h264.h +505 -0
  61. package/engine/vendor/libva/va/va_fei_hevc.h +254 -0
  62. package/engine/vendor/libva/va/va_prot.h +525 -0
  63. package/engine/vendor/libva/va/va_version.h +87 -0
  64. package/engine/vendor/libva/va/va_vpp.h +1590 -0
  65. package/engine/vendor/nv-codec-headers/LICENSE +24 -0
  66. package/engine/vendor/nv-codec-headers/README +7 -0
  67. package/engine/vendor/nv-codec-headers/include/ffnvcodec/nvEncodeAPI.h +3825 -0
  68. package/examples/reference-client.html +1 -1
  69. package/package.json +3 -2
@@ -8,16 +8,18 @@
8
8
  use crate::capture::{self, Capture};
9
9
  use crate::clipboard;
10
10
  use crate::convert::{fit, I420};
11
- use crate::encoder::Encoder;
11
+ use crate::encoder::{Encoder, VideoEncoder};
12
12
  use crate::input::{Button, HeldState, InputDevices};
13
13
  use crate::keymap::{self, Layout};
14
- use crate::peer::{h264_profile_level_id, PeerEvent, TransportOptions, VideoCodec, VideoPeer};
14
+ use crate::peer::{
15
+ h264_profile_level_id, PathReport, PeerEvent, TransportOptions, VideoCodec, VideoPeer,
16
+ };
15
17
  use crate::portal::{self, SelectedSource};
16
18
  use crate::protocol::{
17
19
  ControlMessage, ControlReply, EncodedCodec, Permission, PointerPhase, SourceRequest,
18
20
  };
19
21
  use crate::x11::X11Desktop;
20
- use anyhow::Result;
22
+ use anyhow::{Context, Result};
21
23
  use std::sync::atomic::{AtomicBool, Ordering};
22
24
  use std::sync::{Arc, Condvar, Mutex};
23
25
  use std::time::{Duration, Instant};
@@ -51,14 +53,41 @@ pub struct Metrics {
51
53
  pub dropped_frames: u64,
52
54
  pub encoded_frames: u64,
53
55
  pub encoded_bytes: u64,
54
- /// Key frames sent: the first, and one per receiver request.
56
+ /// Key frames sent: the first, one per receiver request, and one each time
57
+ /// motion starts or stops being coded at half size.
55
58
  pub key_frames: u64,
56
59
  /// Refinement passes: a still desktop re-coded once, sharp.
57
60
  pub refined_frames: u64,
61
+ /// Motion frames coded at half size because full size could not keep up.
62
+ pub halved_frames: u64,
63
+ /// Fresh captures coded at the pace of change: a motion pass on a frame
64
+ /// the encoder had not coded yet — the population `queue_micros` is over.
65
+ pub motion_frames: u64,
58
66
  /// Time spent in the encoder, in microseconds, over every encoded frame.
59
67
  pub encode_micros: u64,
68
+ /// Time spent grabbing and converting a frame on the capture thread, in
69
+ /// microseconds, over every frame forwarded to the encoder. On X11 this
70
+ /// is `GetImage` plus the downscale to I420; unchanged polls are hashed,
71
+ /// recognised, and never forwarded, so their grab cost stays out; the lab
72
+ /// harness reads it as the grab cost L1 owns.
73
+ pub capture_micros: u64,
74
+ /// Time spent hashing a captured frame and working out its damage on the
75
+ /// capture thread, in microseconds, over every forwarded frame; the hash
76
+ /// branch runs only when `local_frames` is on.
77
+ pub convert_micros: u64,
78
+ /// Time a freshly captured frame waited between the capture handoff and
79
+ /// the start of its encode, in microseconds, over motion frames.
80
+ pub queue_micros: u64,
81
+ /// Time spent packetizing a frame and handing it to the transport, in
82
+ /// microseconds, over every sent frame.
83
+ pub send_micros: u64,
60
84
  /// The current rate target, after any loss back-off.
61
85
  pub target_kbps: u32,
86
+ /// The codec frames go out in, `vp9` or `h264`, and what codes them
87
+ /// (`libvpx`, `nvenc`, `vaapi`, `openh264`, `videotoolbox`); empty for an
88
+ /// encoded source, whose consumer is the encoder.
89
+ pub codec: &'static str,
90
+ pub encoder: &'static str,
62
91
  pub input_applied: u64,
63
92
  pub input_rejected: u64,
64
93
  /// Access units the consumer fed to an encoded source.
@@ -185,6 +214,7 @@ fn select_x11(
185
214
  let desktop = Arc::new(Mutex::new(X11Desktop::connect(display)?));
186
215
  let (width, height) = {
187
216
  let desktop = lock(&desktop);
217
+ eprintln!("x11 capture path: {}", desktop.capture_path());
188
218
  desktop.screen_size()
189
219
  };
190
220
  let source = SelectedSource {
@@ -207,26 +237,75 @@ fn select_x11(
207
237
  .spawn(move || {
208
238
  let interval = frame_interval(max_fps);
209
239
  let mut sequence = 0u64;
210
- // An X server has no damage signal here, so an unchanged screen
211
- // is recognised by its pixels: handing it on would keep the
212
- // encoder busy and a still desktop would never be refined.
240
+ let mut last_sent: Option<Instant> = None;
241
+ // Without XDamage there is no change signal, so an unchanged
242
+ // screen is recognised by its pixels exactly as before.
243
+ let hashed = !lock(&desktop).damage_armed();
213
244
  let mut last_hash = None;
245
+ // The first frame is always grabbed; afterwards Damage wakes
246
+ // the loop, so a still screen costs no grab, no conversion
247
+ // and no hash. The fps cap stays a minimum spacing.
248
+ let mut first = true;
214
249
  while !stop.load(Ordering::SeqCst) {
250
+ if !first {
251
+ let changed = lock(&desktop).take_damage();
252
+ if !changed {
253
+ std::thread::sleep(interval);
254
+ continue;
255
+ }
256
+ if let Some(sent) = last_sent {
257
+ let elapsed = sent.elapsed();
258
+ if elapsed < interval {
259
+ std::thread::sleep(interval - elapsed);
260
+ }
261
+ }
262
+ }
215
263
  let started = Instant::now();
264
+ let grabbed = Instant::now();
216
265
  let frame = {
217
266
  let mut desktop = lock(&desktop);
218
267
  desktop.capture_with_pixels(max_width, max_height)
219
268
  };
269
+ let grab_micros = grabbed.elapsed().as_micros() as u64;
220
270
  match frame {
221
271
  Ok((frame, raw)) => {
222
- use std::hash::{Hash, Hasher};
223
- let mut hasher = std::collections::hash_map::DefaultHasher::new();
224
- raw.hash(&mut hasher);
225
- let hash = hasher.finish();
226
- if last_hash != Some(hash) {
227
- last_hash = Some(hash);
228
- sink(frame, sequence, raw);
272
+ let changed = if hashed {
273
+ use std::hash::{Hash, Hasher};
274
+ let mut hasher = std::collections::hash_map::DefaultHasher::new();
275
+ raw.hash(&mut hasher);
276
+ let hash = hasher.finish();
277
+ if last_hash == Some(hash) {
278
+ false
279
+ } else {
280
+ last_hash = Some(hash);
281
+ true
282
+ }
283
+ } else {
284
+ true
285
+ };
286
+ if changed {
287
+ // Handing an unchanged frame on would keep the
288
+ // encoder busy and a still desktop would never
289
+ // be refined.
290
+ if let Ok(mut m) = captured.lock() {
291
+ m.capture_micros += grab_micros;
292
+ }
293
+ let (w, h) = (frame.width, frame.height);
294
+ let (screen_w, screen_h) = (width as usize, height as usize);
295
+ sink(frame, sequence, &mut || {
296
+ crate::convert::to_bgrx(
297
+ &raw,
298
+ screen_w,
299
+ screen_h,
300
+ screen_w * 4,
301
+ crate::convert::PixelFormat::Bgrx,
302
+ w,
303
+ h,
304
+ )
305
+ });
229
306
  sequence += 1;
307
+ last_sent = Some(started);
308
+ first = false;
230
309
  }
231
310
  }
232
311
  Err(error) => {
@@ -239,11 +318,21 @@ fn select_x11(
239
318
  on_stop(format!("X11 capture stopped: {error}"));
240
319
  break;
241
320
  }
321
+ if !hashed {
322
+ let elapsed = started.elapsed();
323
+ if elapsed < interval {
324
+ std::thread::sleep(interval - elapsed);
325
+ }
326
+ }
242
327
  }
243
328
  }
244
- let elapsed = started.elapsed();
245
- if elapsed < interval {
246
- std::thread::sleep(interval - elapsed);
329
+ // With Damage the fps cap paced the grab above; without
330
+ // it the loop keeps its old trailing sleep.
331
+ if hashed {
332
+ let elapsed = started.elapsed();
333
+ if elapsed < interval {
334
+ std::thread::sleep(interval - elapsed);
335
+ }
247
336
  }
248
337
  }
249
338
  })
@@ -506,6 +595,9 @@ pub struct OpenRequest {
506
595
  /// Offer a passive ICE-TCP candidate on the loopback, for a forwarded client.
507
596
  pub loopback_tcp: bool,
508
597
  pub agent_indicator: bool,
598
+ /// Keep the latest frame and its tile hashes for `session.frame` and
599
+ /// `session.frame.changed`; off, a frame is converted once, for the encoder.
600
+ pub local_frames: bool,
509
601
  }
510
602
 
511
603
  /// Events a session raises for its consumer.
@@ -546,8 +638,13 @@ pub enum SessionEvent {
546
638
  CaptureStopped {
547
639
  reason: String,
548
640
  },
641
+ /// The session ended on the engine's side. `code` says whether a new
642
+ /// session may recover it: `transport` is the path to the consumer's peer
643
+ /// lost, which a fresh `session.open` can heal; `lease` and `error` are
644
+ /// the session's authority ending or a fault, which it cannot.
549
645
  Revoked {
550
646
  reason: String,
647
+ code: &'static str,
551
648
  },
552
649
  }
553
650
 
@@ -579,7 +676,7 @@ struct Inner {
579
676
  /// offer has to name the profile the stream's own SPS declares.
580
677
  peer: Mutex<Option<Arc<VideoPeer>>>,
581
678
  /// Absent for an encoded source: the consumer is the encoder.
582
- encoder: Mutex<Option<Encoder>>,
679
+ encoder: Mutex<Option<VideoEncoder>>,
583
680
  input: Mutex<Option<InputTarget>>,
584
681
  indicator: Mutex<Option<crate::indicator::Indicator>>,
585
682
  capture: Mutex<Option<FrameSource>>,
@@ -595,6 +692,8 @@ struct Inner {
595
692
  events: tokio_mpsc::UnboundedSender<Notice>,
596
693
  /// Present only for an encoded source.
597
694
  encoded: Mutex<Option<Encoded>>,
695
+ /// `OpenRequest::local_frames`: whether `latest` is kept at all.
696
+ local_frames: bool,
598
697
  }
599
698
 
600
699
  /// A source whose video arrives already encoded.
@@ -675,7 +774,7 @@ pub fn capabilities() -> serde_json::Value {
675
774
  "cursor": "embedded",
676
775
  "audio": false,
677
776
  },
678
- "encode": { "codecs": ["vp9"], "hardware": false },
777
+ "encode": encode_capabilities(),
679
778
  "input": {
680
779
  "mechanism": "inputtino/uinput",
681
780
  "pointer": input,
@@ -733,7 +832,7 @@ pub fn capabilities() -> serde_json::Value {
733
832
  "remedy": format!("Allow {} in System Settings › Privacy & Security › Screen & System Audio Recording, then reconnect.", crate::mac::tcc_responsible_app_name())
734
833
  }) }
735
834
  },
736
- "encode": { "codecs": ["vp9"], "hardware": false },
835
+ "encode": encode_capabilities(),
737
836
  "input": {
738
837
  "mechanism": "quartz-cgevent",
739
838
  "pointer": input,
@@ -750,6 +849,24 @@ pub fn capabilities() -> serde_json::Value {
750
849
  })
751
850
  }
752
851
 
852
+ /// `capabilities.encode`: VP9 always, H.264 when an encoder for it starts
853
+ /// here now. `hardware` describes VP9, the default.
854
+ fn encode_capabilities() -> serde_json::Value {
855
+ let h264 = crate::h264::probe();
856
+ let mut codecs = vec!["vp9"];
857
+ if h264.is_some() {
858
+ codecs.push("h264");
859
+ }
860
+ serde_json::json!({
861
+ "codecs": codecs,
862
+ "hardware": false,
863
+ "h264": {
864
+ "encoder": h264.map(|info| info.encoder),
865
+ "hardware": h264.is_some_and(|info| info.hardware),
866
+ },
867
+ })
868
+ }
869
+
753
870
  impl Session {
754
871
  pub async fn open(
755
872
  request: OpenRequest,
@@ -832,9 +949,18 @@ impl Session {
832
949
  let capture_status = Arc::new(on_capture_status);
833
950
  let frame_events = events.clone();
834
951
  let frame_session_id = id.clone();
835
- let sink = Box::new(move |frame: I420, _seq: u64, raw: Vec<u8>| {
836
- let hashes = tile_hashes(&raw, frame.width, frame.height);
837
- if let Ok(mut held) = observed.lock() {
952
+ let local_frames = request.local_frames;
953
+ let sink = Box::new(move |frame: I420, _seq: u64, pixels: capture::Pixels| {
954
+ // The frame's RTP time: when capture handed it over, not when it
955
+ // was coded, so the receiver never sees encode jitter as cadence.
956
+ // ponytail: stamped after the backend's grab and conversion; a
957
+ // grab-start instant from each backend needs a FrameSink change.
958
+ let captured_at = Instant::now();
959
+ let raw = if local_frames { pixels() } else { None };
960
+ let hashes = raw
961
+ .as_ref()
962
+ .map(|raw| tile_hashes(raw, frame.width, frame.height));
963
+ if let (Some(raw), Some(hashes), Ok(mut held)) = (raw, hashes, observed.lock()) {
838
964
  let unchanged =
839
965
  held.as_ref()
840
966
  .is_some_and(|(_, _, width, height, _, current, _)| {
@@ -865,10 +991,11 @@ impl Session {
865
991
  }
866
992
  if let Ok(mut m) = captured.lock() {
867
993
  m.captured_frames += 1;
994
+ m.convert_micros += captured_at.elapsed().as_micros() as u64;
868
995
  }
869
996
  // An encoder that is behind gets the newest frame, not a queue: a
870
997
  // desktop stream is live, and the last state is the one that counts.
871
- if frame_tx.put(frame) {
998
+ if frame_tx.put(frame, captured_at) {
872
999
  if let Ok(mut m) = captured.lock() {
873
1000
  m.dropped_frames += 1;
874
1001
  }
@@ -1046,6 +1173,16 @@ impl Session {
1046
1173
  SessionError::new("input", format!("no keyboard layout: {error:#}"))
1047
1174
  })?;
1048
1175
 
1176
+ // H.264 is offered beside VP9 only when an encoder for it starts on
1177
+ // this machine; the answer then decides which one is coded.
1178
+ let codec = if !crate::h264::fits(width, height) {
1179
+ VideoCodec::Vp9
1180
+ } else {
1181
+ match tokio::task::spawn_blocking(crate::h264::prepare).await {
1182
+ Ok(Some(_)) => VideoCodec::Vp9OrH264,
1183
+ _ => VideoCodec::Vp9,
1184
+ }
1185
+ };
1049
1186
  let (peer_events_tx, peer_events_rx) = tokio_mpsc::unbounded_channel::<PeerEvent>();
1050
1187
  let (peer, offer) = VideoPeer::offer(
1051
1188
  TransportOptions {
@@ -1056,7 +1193,7 @@ impl Session {
1056
1193
  pace_bps: (bitrate_kbps as f64 * 3_000.0).max(20_000_000.0),
1057
1194
  },
1058
1195
  peer_events_tx,
1059
- VideoCodec::Vp9,
1196
+ codec,
1060
1197
  )
1061
1198
  .await
1062
1199
  .map_err(|error| SessionError::new("transport", format!("{error:#}")))?;
@@ -1081,7 +1218,7 @@ impl Session {
1081
1218
  capture_error,
1082
1219
  frame_changes,
1083
1220
  peer: Mutex::new(Some(peer.clone())),
1084
- encoder: Mutex::new(Some(encoder)),
1221
+ encoder: Mutex::new(Some(VideoEncoder::Vp9(encoder))),
1085
1222
  input: Mutex::new(input),
1086
1223
  indicator: Mutex::new(indicator),
1087
1224
  capture: Mutex::new(Some(capture)),
@@ -1092,6 +1229,7 @@ impl Session {
1092
1229
  pipeline: pipeline.clone(),
1093
1230
  events: events.clone(),
1094
1231
  encoded: Mutex::new(None),
1232
+ local_frames: request.local_frames,
1095
1233
  });
1096
1234
 
1097
1235
  inner.notify(SessionEvent::Description {
@@ -1189,6 +1327,7 @@ impl Session {
1189
1327
  peer_events,
1190
1328
  feed,
1191
1329
  })),
1330
+ local_frames: false,
1192
1331
  });
1193
1332
  inner.notify(SessionEvent::State {
1194
1333
  capture: "consented",
@@ -1266,6 +1405,7 @@ impl Session {
1266
1405
  "this session forwards an encoded stream and keeps no frame",
1267
1406
  ));
1268
1407
  }
1408
+ self.keeps_frames()?;
1269
1409
  let mut changes = self.inner.frame_changes.clone();
1270
1410
  let deadline = tokio::time::Instant::now() + Duration::from_millis(timeout_ms.min(120_000));
1271
1411
  loop {
@@ -1302,6 +1442,16 @@ impl Session {
1302
1442
  }
1303
1443
  }
1304
1444
 
1445
+ fn keeps_frames(&self) -> std::result::Result<(), SessionError> {
1446
+ if self.inner.local_frames {
1447
+ return Ok(());
1448
+ }
1449
+ Err(SessionError::new(
1450
+ "operation",
1451
+ "this session was opened with local_frames off and keeps no frame",
1452
+ ))
1453
+ }
1454
+
1305
1455
  /// Write only the requested lossless frame bytes; JSON carries metadata, never pixels.
1306
1456
  pub fn frame(
1307
1457
  &self,
@@ -1321,6 +1471,7 @@ impl Session {
1321
1471
  "this session forwards an encoded stream and keeps no frame",
1322
1472
  ));
1323
1473
  }
1474
+ self.keeps_frames()?;
1324
1475
  if let Some(reason) = lock(&self.inner.capture_error).clone() {
1325
1476
  return Err(SessionError::new("stream-stopped", reason));
1326
1477
  }
@@ -1449,7 +1600,7 @@ impl Session {
1449
1600
  }
1450
1601
 
1451
1602
  pub async fn close(&self, reason: &str) {
1452
- self.inner.close(reason).await;
1603
+ self.inner.close(reason, "closed").await;
1453
1604
  }
1454
1605
  }
1455
1606
 
@@ -1464,13 +1615,18 @@ impl Inner {
1464
1615
 
1465
1616
  /// Stop everything and release whatever input this session held. Idempotent,
1466
1617
  /// and the one teardown both an explicit close and a lease expiry take.
1467
- async fn close(&self, reason: &str) {
1618
+ async fn close(&self, reason: &str, code: &str) {
1468
1619
  if self.closed.swap(true, Ordering::SeqCst) {
1469
1620
  return;
1470
1621
  }
1471
1622
  self.control_open.store(false, Ordering::SeqCst);
1472
1623
  self.pipeline.store(false, Ordering::SeqCst);
1473
1624
  drop(lock(&self.encoded).take());
1625
+ // The encoder goes now, not with the last handle: a hardware session
1626
+ // still open when the process exits can hang the driver's exit
1627
+ // handlers, and `shutdown` exits right after this. The encode thread
1628
+ // finds it gone and stops.
1629
+ drop(lock(&self.encoder).take());
1474
1630
  if let Ok(mut indicator) = self.indicator.lock() {
1475
1631
  indicator.take();
1476
1632
  }
@@ -1485,8 +1641,8 @@ impl Inner {
1485
1641
  if let Some(peer) = self.peer.lock().ok().and_then(|peer| peer.clone()) {
1486
1642
  let _ = peer
1487
1643
  .send_control(
1488
- &serde_json::to_string(&ControlReply::Revoked { reason }).unwrap_or_else(
1489
- |_| String::from(r#"{"kind":"revoked","reason":"closed"}"#),
1644
+ &serde_json::to_string(&ControlReply::Revoked { reason, code }).unwrap_or_else(
1645
+ |_| String::from(r#"{"kind":"revoked","reason":"closed","code":"closed"}"#),
1490
1646
  ),
1491
1647
  )
1492
1648
  .await;
@@ -1937,11 +2093,13 @@ fn frame_interval(max_fps: u32) -> Duration {
1937
2093
  Duration::from_secs_f64(1.0 / max_fps.max(1) as f64)
1938
2094
  }
1939
2095
 
1940
- /// The newest captured frame, handed from the capture thread to the encoder. A
1941
- /// newer frame replaces one the encoder has not taken, so however far behind
1942
- /// it falls, what it codes next is the desktop as it is now.
2096
+ /// The newest captured frame and when it was captured, handed from the capture
2097
+ /// thread to the encoder. A newer frame replaces one the encoder has not taken,
2098
+ /// so however far behind it falls, what it codes next is the desktop as it is
2099
+ /// now. The instant travels with the picture so the pipeline can report how
2100
+ /// long a coded frame waited for its encode.
1943
2101
  struct FrameSlot {
1944
- frame: Mutex<(Option<I420>, bool)>,
2102
+ frame: Mutex<(Option<(I420, Instant)>, bool)>,
1945
2103
  ready: Condvar,
1946
2104
  }
1947
2105
 
@@ -1958,13 +2116,13 @@ fn latest_frame() -> (FrameSender, FrameReceiver) {
1958
2116
 
1959
2117
  impl FrameSender {
1960
2118
  /// Hand over a frame; true when it replaced one the encoder never took.
1961
- fn put(&self, frame: I420) -> bool {
2119
+ fn put(&self, frame: I420, captured: Instant) -> bool {
1962
2120
  let mut held = self
1963
2121
  .0
1964
2122
  .frame
1965
2123
  .lock()
1966
2124
  .unwrap_or_else(|poisoned| poisoned.into_inner());
1967
- let replaced = held.0.replace(frame).is_some();
2125
+ let replaced = held.0.replace((frame, captured)).is_some();
1968
2126
  self.0.ready.notify_one();
1969
2127
  replaced
1970
2128
  }
@@ -1984,12 +2142,22 @@ impl Drop for FrameSender {
1984
2142
  }
1985
2143
 
1986
2144
  enum Taken {
1987
- Frame(I420),
2145
+ Frame(I420, Instant),
1988
2146
  Timeout,
1989
2147
  Ended,
1990
2148
  }
1991
2149
 
1992
2150
  impl FrameReceiver {
2151
+ /// Whether a frame is waiting to be taken.
2152
+ fn waiting(&self) -> bool {
2153
+ self.0
2154
+ .frame
2155
+ .lock()
2156
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
2157
+ .0
2158
+ .is_some()
2159
+ }
2160
+
1993
2161
  fn take(&self, wait: Duration) -> Taken {
1994
2162
  let held = self
1995
2163
  .0
@@ -2002,7 +2170,7 @@ impl FrameReceiver {
2002
2170
  .wait_timeout_while(held, wait, |(frame, ended)| frame.is_none() && !*ended)
2003
2171
  .unwrap_or_else(|poisoned| poisoned.into_inner());
2004
2172
  match held.0.take() {
2005
- Some(frame) => Taken::Frame(frame),
2173
+ Some((frame, captured)) => Taken::Frame(frame, captured),
2006
2174
  None if held.1 => Taken::Ended,
2007
2175
  None => Taken::Timeout,
2008
2176
  }
@@ -2028,12 +2196,32 @@ fn auto_bitrate(width: usize, height: usize) -> u32 {
2028
2196
  (1500 + (width * height * 18 / 10_000) as u32).min(25_000)
2029
2197
  }
2030
2198
 
2031
- /// Loss-based rate target: back off quickly when the far end reports loss,
2032
- /// creep back when it reports none, never above what the session asked for.
2199
+ /// Queueing delay that means the link carries more than it drains: well under
2200
+ /// the latency budget, well over the noise of a busy host's scheduling.
2201
+ const QUEUE_HIGH: Duration = Duration::from_millis(25);
2202
+ /// Below this the path has room again and the target may creep back up.
2203
+ const QUEUE_LOW: Duration = Duration::from_millis(10);
2204
+ /// How often queueing delay may back the target off: long enough for the last
2205
+ /// back-off to reach the far end and show in its feedback, short enough that a
2206
+ /// queue is drained in a few steps rather than a few seconds.
2207
+ const DELAY_BACKOFF_EVERY: Duration = Duration::from_millis(300);
2208
+
2209
+ /// The rate target: back off when the far end reports a standing queue or
2210
+ /// loss, creep back when it reports neither, never above what the session
2211
+ /// asked for.
2212
+ ///
2213
+ /// Delay leads. A link with a deep buffer (most Wi-Fi and cellular ones) never
2214
+ /// loses a packet it can queue, so loss alone lets queueing delay grow to
2215
+ /// seconds before anything reacts. A standing queue backs the target off to
2216
+ /// 85% of what the path delivered, which drains it; never more than halving
2217
+ /// per step, so a stream that was idle and sent little is not taken for a
2218
+ /// slow link. Receivers without transport-wide feedback leave it loss-only.
2033
2219
  struct RateControl {
2034
2220
  ceiling: u32,
2035
2221
  target: u32,
2036
2222
  last_change: Option<Instant>,
2223
+ /// The last queueing delay reported, while the path reports one.
2224
+ queue: Option<Duration>,
2037
2225
  }
2038
2226
 
2039
2227
  impl RateControl {
@@ -2042,26 +2230,40 @@ impl RateControl {
2042
2230
  ceiling,
2043
2231
  target: ceiling,
2044
2232
  last_change: None,
2233
+ queue: None,
2045
2234
  }
2046
2235
  }
2047
2236
 
2048
- /// The new target when these reports move it, at most once a second.
2049
- fn update(&mut self, reports: &[u8], now: Instant) -> Option<u32> {
2050
- let worst = *reports.iter().max()?;
2051
- if self
2052
- .last_change
2053
- .is_some_and(|at| now.duration_since(at) < Duration::from_secs(1))
2054
- {
2055
- return None;
2237
+ /// The new target when these loss reports (RFC 3550 fractions, /256) or
2238
+ /// this path report move it: down on a standing queue at most every
2239
+ /// `DELAY_BACKOFF_EVERY`, otherwise at most once a second.
2240
+ fn update(&mut self, reports: &[u8], path: Option<PathReport>, now: Instant) -> Option<u32> {
2241
+ if let Some(path) = path {
2242
+ self.queue = Some(path.queue_delay);
2056
2243
  }
2057
- let loss = worst as f64 / 256.0;
2244
+ let since = self.last_change.map(|at| now.duration_since(at));
2245
+ let waited = |gap: Duration| since.is_none_or(|since| since >= gap);
2058
2246
  let floor = (self.ceiling / 8).max(500);
2059
- let next = if loss > 0.10 {
2060
- ((self.target as f64 * 0.7) as u32).max(floor)
2061
- } else if loss < 0.02 {
2062
- ((self.target as f64 * 1.08) as u32 + 64).min(self.ceiling)
2247
+ let next = if let Some(path) = path.filter(|path| path.queue_delay > QUEUE_HIGH) {
2248
+ if !waited(DELAY_BACKOFF_EVERY) {
2249
+ return None;
2250
+ }
2251
+ let delivered = path.delivered_kbps.clamp(self.target / 2, self.target);
2252
+ ((delivered as f64 * 0.85) as u32).max(floor)
2063
2253
  } else {
2064
- self.target
2254
+ let worst = *reports.iter().max()?;
2255
+ if !waited(Duration::from_secs(1)) {
2256
+ return None;
2257
+ }
2258
+ let loss = worst as f64 / 256.0;
2259
+ let roomy = self.queue.is_none_or(|queue| queue < QUEUE_LOW);
2260
+ if loss > 0.10 {
2261
+ ((self.target as f64 * 0.7) as u32).max(floor)
2262
+ } else if loss < 0.02 && roomy {
2263
+ ((self.target as f64 * 1.08) as u32 + 64).min(self.ceiling)
2264
+ } else {
2265
+ self.target
2266
+ }
2065
2267
  };
2066
2268
  if next == self.target {
2067
2269
  return None;
@@ -2072,9 +2274,127 @@ impl RateControl {
2072
2274
  }
2073
2275
  }
2074
2276
 
2277
+ /// How far the stream may run ahead of its rate target before the next frame
2278
+ /// waits: one large frame (a key frame, a refinement) goes at once, but the
2279
+ /// frames behind it hold until the link has had time to carry it, instead of
2280
+ /// queueing behind it. The frame that goes then is the newest one.
2281
+ const FRAME_BURST: Duration = Duration::from_millis(60);
2282
+
2283
+ /// How much faster than the target the budget drains. A rate-controlled
2284
+ /// encoder averages its target but overshoots it frame to frame; drained at
2285
+ /// the target itself, that ordinary overshoot would add up and hold frames on
2286
+ /// a link with room to spare. The rate control, not this, keeps the average
2287
+ /// under what the path carries.
2288
+ const BUDGET_HEADROOM: f64 = 1.25;
2289
+
2290
+ /// What has been sent beyond the rate target, draining a little faster than
2291
+ /// it: a leaky bucket that bounds a burst to `FRAME_BURST` plus one frame.
2292
+ #[derive(Default)]
2293
+ struct SendBudget {
2294
+ bits: f64,
2295
+ at: Option<Instant>,
2296
+ }
2297
+
2298
+ impl SendBudget {
2299
+ /// The drain rate, in bits per second.
2300
+ fn rate(kbps: u32) -> f64 {
2301
+ kbps.max(1) as f64 * 1000.0 * BUDGET_HEADROOM
2302
+ }
2303
+
2304
+ fn drained(&self, now: Instant, kbps: u32) -> f64 {
2305
+ let elapsed = self
2306
+ .at
2307
+ .map_or(0.0, |at| now.saturating_duration_since(at).as_secs_f64());
2308
+ (self.bits - elapsed * Self::rate(kbps)).max(0.0)
2309
+ }
2310
+
2311
+ /// How long until a frame may go; zero when it may go now.
2312
+ fn wait(&self, now: Instant, kbps: u32) -> Duration {
2313
+ let rate = Self::rate(kbps);
2314
+ let over = self.drained(now, kbps) - FRAME_BURST.as_secs_f64() * rate;
2315
+ Duration::from_secs_f64((over / rate).max(0.0))
2316
+ }
2317
+
2318
+ fn sent(&mut self, now: Instant, bytes: usize, kbps: u32) {
2319
+ self.bits = self.drained(now, kbps) + bytes as f64 * 8.0;
2320
+ self.at = Some(now);
2321
+ }
2322
+ }
2323
+
2324
+ /// Whether motion is coded at half size. A full-size frame is slow when it
2325
+ /// took longer to code than a frame interval and the source already had a
2326
+ /// newer frame waiting: the encoder, not the source, set the pace. Content that
2327
+ /// arrives slower than the frame cap (30 fps into a 60 fps session) is never
2328
+ /// slow however long a frame takes, as long as it is done before the next one.
2329
+ /// Several slow frames that each change much of the picture (a packet over a
2330
+ /// quarter of the per-frame rate budget) mean full size cannot reach the frame
2331
+ /// rate, so the rest of the motion is coded at half size, until it stops (the
2332
+ /// refinement pass is always full size) or a run of frames that are both small
2333
+ /// and not slow shows full size would keep up again. Packet size or coding time
2334
+ /// alone never returns it to full size, so heavy motion does not flap between
2335
+ /// sizes; each change costs a key frame. Both directions need a run of frames
2336
+ /// rather than one, so a slow key frame or a busy moment on the host does not
2337
+ /// flip it, and typing never halves.
2338
+ #[derive(Default)]
2339
+ struct MotionSize {
2340
+ half: bool,
2341
+ run: u32,
2342
+ }
2343
+
2344
+ impl MotionSize {
2345
+ const SLOW_RUN: u32 = 5;
2346
+ const SMALL_RUN: u32 = 30;
2347
+
2348
+ /// Account for a motion frame coded at the current size in `took`; `large`
2349
+ /// when its packet was over a quarter of the per-frame rate budget, `behind`
2350
+ /// when a newer frame was already waiting once it was coded.
2351
+ fn coded(&mut self, took: Duration, large: bool, behind: bool, interval: Duration) {
2352
+ let slow = took > interval && behind;
2353
+ let (counts, needed) = if self.half {
2354
+ (!large && !slow, Self::SMALL_RUN)
2355
+ } else {
2356
+ (large && slow, Self::SLOW_RUN)
2357
+ };
2358
+ self.run = if counts { self.run + 1 } else { 0 };
2359
+ if self.run >= needed {
2360
+ self.half = !self.half;
2361
+ self.run = 0;
2362
+ }
2363
+ }
2364
+ }
2365
+
2366
+ /// The rate cap as a deadline rather than a minimum gap. `due` is where the
2367
+ /// next frame falls on the `max_fps` cadence the stream has kept so far, and a
2368
+ /// frame may go up to one interval before it, so a late frame's lost time is
2369
+ /// made up by the next. A capture clock that jitters around the cap is then
2370
+ /// coded the moment each frame arrives, instead of beating against a second
2371
+ /// clock that delays a frame or lets the next one supersede it; the sustained
2372
+ /// rate still never exceeds the cap, and no window holds more than one frame
2373
+ /// over it.
2374
+ struct Cadence {
2375
+ interval: Duration,
2376
+ due: Instant,
2377
+ }
2378
+
2379
+ impl Cadence {
2380
+ fn new(interval: Duration, now: Instant) -> Self {
2381
+ Self { interval, due: now }
2382
+ }
2383
+
2384
+ /// How long until a frame may go; zero when it may go now.
2385
+ fn wait(&self, now: Instant) -> Duration {
2386
+ self.due.saturating_duration_since(now + self.interval)
2387
+ }
2388
+
2389
+ fn sent(&mut self, now: Instant) {
2390
+ self.due = self.due.max(now) + self.interval;
2391
+ }
2392
+ }
2393
+
2075
2394
  enum Pass {
2076
2395
  Motion {
2077
2396
  keyframe: bool,
2397
+ half: bool,
2078
2398
  },
2079
2399
  Refine,
2080
2400
  /// The same picture again, so a still stream never looks stalled.
@@ -2107,8 +2427,11 @@ fn spawn_pipeline(
2107
2427
  let mut rate = RateControl::new(bitrate_kbps);
2108
2428
  if let Ok(mut m) = inner.metrics.lock() {
2109
2429
  m.target_kbps = bitrate_kbps;
2430
+ (m.codec, m.encoder) = ("vp9", "libvpx");
2110
2431
  }
2111
- let mut latest: Option<I420> = None;
2432
+ // Whether motion may be coded at half size: VP9's alone.
2433
+ let mut halves = true;
2434
+ let mut latest: Option<(I420, Instant)> = None;
2112
2435
  // `pending`: the latest frame has not been coded. `refined`: it has
2113
2436
  // had its refinement pass, or there is nothing to refine.
2114
2437
  let mut pending = false;
@@ -2116,11 +2439,15 @@ fn spawn_pipeline(
2116
2439
  let mut keyframe = true;
2117
2440
  let mut still_since = Instant::now();
2118
2441
  let mut last_sent: Option<Instant> = None;
2442
+ let mut size = MotionSize::default();
2443
+ let mut cadence = Cadence::new(interval, Instant::now());
2444
+ let mut budget = SendBudget::default();
2445
+ // What the last frame was stamped with: the RTP clock never runs
2446
+ // backwards, even for a re-coded picture.
2447
+ let mut last_stamp: Option<Instant> = None;
2119
2448
  loop {
2120
2449
  let now = Instant::now();
2121
- let slot = last_sent.map_or(Duration::ZERO, |at| {
2122
- interval.saturating_sub(now.duration_since(at))
2123
- });
2450
+ let slot = cadence.wait(now).max(budget.wait(now, rate.target));
2124
2451
  let connected = peer.is_connected();
2125
2452
  let wait = if !connected {
2126
2453
  // Nothing sent before the transport connects arrives; the
@@ -2140,13 +2467,13 @@ fn spawn_pipeline(
2140
2467
  .clamp(Duration::from_millis(10), Duration::from_millis(100))
2141
2468
  };
2142
2469
  match frame_rx.take(wait) {
2143
- Taken::Frame(frame) => {
2470
+ Taken::Frame(frame, captured) => {
2144
2471
  if pending {
2145
2472
  if let Ok(mut m) = inner.metrics.lock() {
2146
2473
  m.dropped_frames += 1;
2147
2474
  }
2148
2475
  }
2149
- latest = Some(frame);
2476
+ latest = Some((frame, captured));
2150
2477
  pending = true;
2151
2478
  refined = false;
2152
2479
  still_since = Instant::now();
@@ -2161,7 +2488,8 @@ fn spawn_pipeline(
2161
2488
  keyframe = true;
2162
2489
  }
2163
2490
  let reports = peer.take_loss_reports();
2164
- if let Some(kbps) = rate.update(&reports, Instant::now()) {
2491
+ let path = peer.take_path_report();
2492
+ if let Some(kbps) = rate.update(&reports, path, Instant::now()) {
2165
2493
  let applied = inner.encoder.lock().map(|mut encoder| {
2166
2494
  encoder
2167
2495
  .as_mut()
@@ -2174,18 +2502,45 @@ fn spawn_pipeline(
2174
2502
  }
2175
2503
  }
2176
2504
  }
2177
- let Some(frame) = latest.as_ref() else {
2505
+ let Some((frame, captured)) = latest.as_ref() else {
2178
2506
  continue;
2179
2507
  };
2180
2508
  if !connected {
2181
2509
  continue;
2182
2510
  }
2183
2511
  let now = Instant::now();
2184
- if last_sent.is_some_and(|at| now.duration_since(at) < interval) {
2512
+ if !cadence.wait(now).is_zero() || !budget.wait(now, rate.target).is_zero() {
2185
2513
  continue;
2186
2514
  }
2515
+ if halves && peer.sends_h264() {
2516
+ // The answer asked for H.264: nothing has been sent yet,
2517
+ // since frames wait for the transport, which waits for it.
2518
+ match start_h264(&inner, rate.target, max_fps) {
2519
+ Ok(()) => {
2520
+ halves = false;
2521
+ size = MotionSize::default();
2522
+ keyframe = true;
2523
+ }
2524
+ Err(error) => {
2525
+ eprintln!("{error:#}");
2526
+ let reason = String::from("no H.264 encoder could start");
2527
+ inner.notify(SessionEvent::Revoked {
2528
+ reason: reason.clone(),
2529
+ code: "error",
2530
+ });
2531
+ let target = inner.clone();
2532
+ handle.spawn(async move {
2533
+ target.close(&reason, "error").await;
2534
+ });
2535
+ break;
2536
+ }
2537
+ }
2538
+ }
2187
2539
  let pass = if pending || keyframe {
2188
- Pass::Motion { keyframe }
2540
+ Pass::Motion {
2541
+ keyframe,
2542
+ half: size.half && halves,
2543
+ }
2189
2544
  } else if !refined && now.duration_since(still_since) >= REFINE_AFTER {
2190
2545
  Pass::Refine
2191
2546
  } else if refined
@@ -2206,9 +2561,9 @@ fn spawn_pipeline(
2206
2561
  break;
2207
2562
  };
2208
2563
  match pass {
2209
- Pass::Motion { keyframe } => encoder.encode(frame, keyframe),
2564
+ Pass::Motion { keyframe, half } => encoder.encode(frame, keyframe, half),
2210
2565
  Pass::Refine => encoder.refine(frame),
2211
- Pass::Keepalive => encoder.encode(frame, false),
2566
+ Pass::Keepalive => encoder.encode(frame, false, false),
2212
2567
  }
2213
2568
  };
2214
2569
  let packet = match packet {
@@ -2226,22 +2581,29 @@ fn spawn_pipeline(
2226
2581
  let reason = String::from("the encoder rejected a frame");
2227
2582
  inner.notify(SessionEvent::Revoked {
2228
2583
  reason: reason.clone(),
2584
+ code: "error",
2229
2585
  });
2230
2586
  let target = inner.clone();
2231
2587
  handle.spawn(async move {
2232
- target.close(&reason).await;
2588
+ target.close(&reason, "error").await;
2233
2589
  });
2234
2590
  break;
2235
2591
  }
2236
2592
  };
2593
+ let took = started.elapsed();
2237
2594
  if let Ok(mut m) = inner.metrics.lock() {
2238
2595
  m.encoded_frames += 1;
2239
2596
  m.encoded_bytes += packet.data.len() as u64;
2240
- m.encode_micros += started.elapsed().as_micros() as u64;
2597
+ m.encode_micros += took.as_micros() as u64;
2598
+ m.key_frames += packet.keyframe as u64;
2599
+ if matches!(pass, Pass::Motion { .. }) && pending {
2600
+ m.queue_micros += started.duration_since(*captured).as_micros() as u64;
2601
+ m.motion_frames += 1;
2602
+ }
2241
2603
  match pass {
2242
- Pass::Motion { keyframe: true } => m.key_frames += 1,
2604
+ Pass::Motion { half: true, .. } => m.halved_frames += 1,
2243
2605
  Pass::Refine => m.refined_frames += 1,
2244
- Pass::Motion { keyframe: false } | Pass::Keepalive => {}
2606
+ Pass::Motion { half: false, .. } | Pass::Keepalive => {}
2245
2607
  }
2246
2608
  }
2247
2609
  match pass {
@@ -2250,13 +2612,34 @@ fn spawn_pipeline(
2250
2612
  keyframe = false;
2251
2613
  refined = false;
2252
2614
  still_since = now;
2615
+ let budget = rate.target as u64 * 1000 / 8 / max_fps.max(1) as u64;
2616
+ let large = packet.data.len() as u64 * 4 > budget;
2617
+ if halves {
2618
+ size.coded(took, large, frame_rx.waiting(), interval);
2619
+ }
2620
+ }
2621
+ Pass::Refine => {
2622
+ refined = true;
2623
+ size = MotionSize::default();
2253
2624
  }
2254
- Pass::Refine => refined = true,
2255
2625
  Pass::Keepalive => {}
2256
2626
  }
2257
2627
  last_sent = Some(now);
2628
+ cadence.sent(now);
2629
+ budget.sent(now, packet.data.len(), rate.target);
2630
+ // A new picture carries the moment it was captured, so the
2631
+ // receiver paces playout by the desktop's clock rather than by
2632
+ // encode time; a re-coded one (refinement, keepalive) is new
2633
+ // content now.
2634
+ let stamp = match pass {
2635
+ Pass::Motion { .. } => *captured,
2636
+ Pass::Refine | Pass::Keepalive => now,
2637
+ };
2638
+ let stamp = last_stamp.map_or(stamp, |at| stamp.max(at + Duration::from_millis(1)));
2639
+ last_stamp = Some(stamp);
2640
+ let sending = Instant::now();
2258
2641
  if let Err(error) =
2259
- handle.block_on(peer.send_frame(&packet.data, packet.keyframe, now))
2642
+ handle.block_on(peer.send_frame(&packet.data, packet.keyframe, stamp))
2260
2643
  {
2261
2644
  // The transport is gone. End the session rather than leave
2262
2645
  // the desktop captured behind a picture that stopped.
@@ -2264,18 +2647,41 @@ fn spawn_pipeline(
2264
2647
  let reason = String::from("the connection to the phone was lost");
2265
2648
  inner.notify(SessionEvent::Revoked {
2266
2649
  reason: reason.clone(),
2650
+ code: "transport",
2267
2651
  });
2268
2652
  let target = inner.clone();
2269
2653
  handle.spawn(async move {
2270
- target.close(&reason).await;
2654
+ target.close(&reason, "transport").await;
2271
2655
  });
2272
2656
  break;
2273
2657
  }
2658
+ if let Ok(mut m) = inner.metrics.lock() {
2659
+ m.send_micros += sending.elapsed().as_micros() as u64;
2660
+ }
2274
2661
  }
2275
2662
  })
2276
2663
  .ok();
2277
2664
  }
2278
2665
 
2666
+ /// Replace the session's VP9 encoder with an H.264 one of the same size and
2667
+ /// rate target, on the encode thread that will drive it.
2668
+ fn start_h264(inner: &Arc<Inner>, bitrate_kbps: u32, fps: u32) -> Result<()> {
2669
+ let mut held = lock(&inner.encoder);
2670
+ let (width, height) = held
2671
+ .as_ref()
2672
+ .map(VideoEncoder::size)
2673
+ .context("the session has no encoder")?;
2674
+ let (backend, info) = crate::h264::open(width, height, bitrate_kbps, fps)?;
2675
+ *held = Some(VideoEncoder::H264 {
2676
+ backend,
2677
+ size: (width, height),
2678
+ });
2679
+ if let Ok(mut m) = inner.metrics.lock() {
2680
+ (m.codec, m.encoder) = ("h264", info.encoder);
2681
+ }
2682
+ Ok(())
2683
+ }
2684
+
2279
2685
  /// Packetize what a consumer feeds an encoded source, building the peer on the
2280
2686
  /// first access unit that carries an SPS.
2281
2687
  ///
@@ -2338,6 +2744,7 @@ fn spawn_encoded(inner: &Arc<Inner>, mut feed: tokio_mpsc::Receiver<FedAccessUni
2338
2744
  revoke(
2339
2745
  &inner,
2340
2746
  format!("the transport refused the encoded stream: {error:#}"),
2747
+ "error",
2341
2748
  )
2342
2749
  .await;
2343
2750
  return;
@@ -2362,7 +2769,12 @@ fn spawn_encoded(inner: &Arc<Inner>, mut feed: tokio_mpsc::Receiver<FedAccessUni
2362
2769
  }
2363
2770
  Err(error) => {
2364
2771
  eprintln!("the video track refused a fed access unit: {error:#}");
2365
- revoke(&inner, String::from("the connection to the phone was lost")).await;
2772
+ revoke(
2773
+ &inner,
2774
+ String::from("the connection to the phone was lost"),
2775
+ "transport",
2776
+ )
2777
+ .await;
2366
2778
  return;
2367
2779
  }
2368
2780
  }
@@ -2376,11 +2788,12 @@ fn current_peer(inner: &Arc<Inner>) -> Option<Arc<VideoPeer>> {
2376
2788
 
2377
2789
  /// End the session with a reason the consumer can show, exactly as the encode
2378
2790
  /// pipeline does when its transport goes away.
2379
- async fn revoke(inner: &Arc<Inner>, reason: String) {
2791
+ async fn revoke(inner: &Arc<Inner>, reason: String, code: &'static str) {
2380
2792
  inner.notify(SessionEvent::Revoked {
2381
2793
  reason: reason.clone(),
2794
+ code,
2382
2795
  });
2383
- inner.close(&reason).await;
2796
+ inner.close(&reason, code).await;
2384
2797
  }
2385
2798
 
2386
2799
  fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiver<PeerEvent>) {
@@ -2428,8 +2841,9 @@ fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiv
2428
2841
  let reason = String::from("the connection to the phone was lost");
2429
2842
  inner.notify(SessionEvent::Revoked {
2430
2843
  reason: reason.clone(),
2844
+ code: "transport",
2431
2845
  });
2432
- inner.close(&reason).await;
2846
+ inner.close(&reason, "transport").await;
2433
2847
  continue;
2434
2848
  }
2435
2849
  inner.notify(SessionEvent::State {
@@ -2487,10 +2901,11 @@ fn spawn_lease(inner: &Arc<Inner>, ttl: Duration) {
2487
2901
  let reason = String::from("the session lease expired");
2488
2902
  inner.notify(SessionEvent::Revoked {
2489
2903
  reason: reason.clone(),
2904
+ code: "lease",
2490
2905
  });
2491
2906
  // An expiry ends the session exactly as an explicit close does; the
2492
2907
  // notification above is not a substitute for stopping the desktop.
2493
- inner.close(&reason).await;
2908
+ inner.close(&reason, "lease").await;
2494
2909
  });
2495
2910
  }
2496
2911
 
@@ -2575,10 +2990,11 @@ mod tests {
2575
2990
  peer_events,
2576
2991
  feed,
2577
2992
  })),
2993
+ local_frames: false,
2578
2994
  });
2579
2995
  spawn_peer_events(&inner, peer_events_rx);
2580
2996
  spawn_encoded(&inner, feed_rx);
2581
- inner.close("test").await;
2997
+ inner.close("test", "closed").await;
2582
2998
  for _ in 0..100 {
2583
2999
  if Arc::strong_count(&inner) == 1 {
2584
3000
  break;
@@ -2600,6 +3016,41 @@ mod tests {
2600
3016
  );
2601
3017
  }
2602
3018
 
3019
+ #[test]
3020
+ fn frames_that_jitter_around_the_cap_go_at_once_and_a_flood_stays_capped() {
3021
+ let interval = Duration::from_millis(20);
3022
+ let start = Instant::now();
3023
+ let mut cadence = Cadence::new(interval, start);
3024
+ // Captured at the cap, each a few ms early or late: none waits, so
3025
+ // none is delayed or superseded by the next.
3026
+ for (n, jitter) in [0i64, -6, 5, -8, 7, -3, 0, -9, 9, -5].iter().enumerate() {
3027
+ let at = start + interval * n as u32 + Duration::from_millis(9);
3028
+ let at = if *jitter < 0 {
3029
+ at - Duration::from_millis(jitter.unsigned_abs())
3030
+ } else {
3031
+ at + Duration::from_millis(*jitter as u64)
3032
+ };
3033
+ assert_eq!(cadence.wait(at), Duration::ZERO, "frame {n} waited");
3034
+ cadence.sent(at);
3035
+ }
3036
+
3037
+ // A source far above the cap: one second of 1 ms frames sends at most
3038
+ // one frame over the cap.
3039
+ let start = start + Duration::from_secs(10);
3040
+ let mut cadence = Cadence::new(interval, start);
3041
+ let sent = (0..1000)
3042
+ .map(|ms| start + Duration::from_millis(ms))
3043
+ .filter(|&at| {
3044
+ let go = cadence.wait(at).is_zero();
3045
+ if go {
3046
+ cadence.sent(at);
3047
+ }
3048
+ go
3049
+ })
3050
+ .count();
3051
+ assert!((50..=51).contains(&sent), "sent {sent} at a 50 fps cap");
3052
+ }
3053
+
2603
3054
  #[test]
2604
3055
  fn a_small_source_is_never_upscaled() {
2605
3056
  assert_eq!(fit(1000, 600, 1280, 800), (1000, 600));
@@ -2658,6 +3109,15 @@ mod tests {
2658
3109
  )
2659
3110
  .await
2660
3111
  .expect("a peer connection builds without a network");
3112
+ test_inner_with(events, peer, 64, 64)
3113
+ }
3114
+
3115
+ fn test_inner_with(
3116
+ events: tokio_mpsc::UnboundedSender<Notice>,
3117
+ peer: VideoPeer,
3118
+ width: usize,
3119
+ height: usize,
3120
+ ) -> (Arc<Inner>, Arc<Mutex<Vec<(i16, bool)>>>) {
2661
3121
  // The pipeline codes nothing before the transport connects.
2662
3122
  peer.assume_connected();
2663
3123
  let recorded = Arc::new(Mutex::new(Vec::new()));
@@ -2684,7 +3144,9 @@ mod tests {
2684
3144
  capture_error: Arc::new(Mutex::new(None)),
2685
3145
  frame_changes: tokio::sync::watch::channel(0).1,
2686
3146
  peer: Mutex::new(Some(Arc::new(peer))),
2687
- encoder: Mutex::new(Some(Encoder::new(64, 64, 1000, 30, 1).expect("an encoder"))),
3147
+ encoder: Mutex::new(Some(VideoEncoder::Vp9(
3148
+ Encoder::new(width, height, 1000, 30, 1).expect("an encoder"),
3149
+ ))),
2688
3150
  input: Mutex::new(Some(InputTarget {
2689
3151
  applier: Applier::Recording(recorded.clone()),
2690
3152
  held: HeldState::default(),
@@ -2702,6 +3164,7 @@ mod tests {
2702
3164
  pipeline: Arc::new(AtomicBool::new(true)),
2703
3165
  events,
2704
3166
  encoded: Mutex::new(None),
3167
+ local_frames: true,
2705
3168
  });
2706
3169
  (inner, recorded)
2707
3170
  }
@@ -2742,11 +3205,11 @@ mod tests {
2742
3205
  matches!(
2743
3206
  received.try_recv(),
2744
3207
  Ok(Notice {
2745
- event: SessionEvent::Revoked { .. },
3208
+ event: SessionEvent::Revoked { code: "lease", .. },
2746
3209
  ..
2747
3210
  })
2748
3211
  ),
2749
- "the consumer is still told why the session ended",
3212
+ "the consumer is still told the session's authority ended",
2750
3213
  );
2751
3214
 
2752
3215
  let rejected = inner.metrics.lock().unwrap().input_rejected;
@@ -3020,20 +3483,24 @@ mod tests {
3020
3483
 
3021
3484
  // The encoder is 64x64; a 32x32 frame is the dimension mismatch that
3022
3485
  // used to be counted and dropped behind a permanently black picture.
3023
- frame_tx.put(I420 {
3024
- width: 32,
3025
- height: 32,
3026
- data: vec![128u8; 32 * 32 + 2 * 16 * 16],
3027
- });
3486
+ frame_tx.put(
3487
+ I420 {
3488
+ width: 32,
3489
+ height: 32,
3490
+ data: vec![128u8; 32 * 32 + 2 * 16 * 16],
3491
+ },
3492
+ Instant::now(),
3493
+ );
3028
3494
  tokio::time::sleep(Duration::from_millis(150)).await;
3029
3495
 
3030
3496
  let mut reason = None;
3031
3497
  while let Ok(event) = received.try_recv() {
3032
3498
  if let Notice {
3033
- event: SessionEvent::Revoked { reason: why },
3499
+ event: SessionEvent::Revoked { reason: why, code },
3034
3500
  ..
3035
3501
  } = event
3036
3502
  {
3503
+ assert_eq!(code, "error", "a refused frame is a fault, not a lost path");
3037
3504
  reason = Some(why);
3038
3505
  }
3039
3506
  }
@@ -3074,6 +3541,7 @@ mod tests {
3074
3541
  ttl: Some(Duration::from_secs(30)),
3075
3542
  loopback_tcp: false,
3076
3543
  agent_indicator: false,
3544
+ local_frames: true,
3077
3545
  }
3078
3546
  }
3079
3547
 
@@ -3241,6 +3709,7 @@ mod tests {
3241
3709
  ttl: None,
3242
3710
  loopback_tcp: false,
3243
3711
  agent_indicator: false,
3712
+ local_frames: true,
3244
3713
  },
3245
3714
  events,
3246
3715
  )
@@ -3282,8 +3751,17 @@ mod tests {
3282
3751
  );
3283
3752
 
3284
3753
  peer_tx.send(PeerEvent::State(State::Failed)).unwrap();
3754
+ let mut transport_lost = false;
3285
3755
  tokio::time::timeout(Duration::from_secs(2), async {
3286
3756
  while let Some(notice) = received.recv().await {
3757
+ // A lost path is the one revocation a new session can heal.
3758
+ transport_lost |= matches!(
3759
+ notice.event,
3760
+ SessionEvent::Revoked {
3761
+ code: "transport",
3762
+ ..
3763
+ }
3764
+ );
3287
3765
  if matches!(
3288
3766
  notice.event,
3289
3767
  SessionEvent::State {
@@ -3298,11 +3776,158 @@ mod tests {
3298
3776
  })
3299
3777
  .await
3300
3778
  .unwrap();
3779
+ assert!(
3780
+ transport_lost,
3781
+ "a failed transport must revoke with code transport"
3782
+ );
3301
3783
  assert!(inner.revoked_reason().is_some());
3302
3784
  assert!(!inner.pipeline.load(Ordering::SeqCst));
3303
3785
  assert!(inner.input.lock().unwrap().is_none());
3304
3786
  }
3305
3787
 
3788
+ #[test]
3789
+ fn a_standing_queue_backs_the_rate_off_to_what_the_path_delivers() {
3790
+ let start = Instant::now();
3791
+ let queued = |ms: u64, delivered_kbps: u32| {
3792
+ Some(PathReport {
3793
+ queue_delay: Duration::from_millis(ms),
3794
+ delivered_kbps,
3795
+ })
3796
+ };
3797
+ let mut rate = RateControl::new(8000);
3798
+ // A burst's worth of queue and no loss moves nothing.
3799
+ assert_eq!(rate.update(&[], queued(15, 3000), start), None);
3800
+ // A standing queue on a 4 Mbps link: 85% of what got through, at once,
3801
+ // with no loss reported at all.
3802
+ assert_eq!(rate.update(&[], queued(80, 4000), start), Some(3400));
3803
+ // Not again until the back-off has had time to show in the feedback.
3804
+ assert_eq!(
3805
+ rate.update(&[], queued(80, 3400), start + Duration::from_millis(100)),
3806
+ None
3807
+ );
3808
+ assert_eq!(
3809
+ rate.update(&[], queued(60, 3400), start + Duration::from_millis(300)),
3810
+ Some(2890)
3811
+ );
3812
+ // A stream that sent little is not taken for a slow link: at most half.
3813
+ assert_eq!(
3814
+ rate.update(&[], queued(60, 100), start + Duration::from_millis(600)),
3815
+ Some(1228)
3816
+ );
3817
+ // Clean loss reports do not raise it while the queue has not drained...
3818
+ assert_eq!(
3819
+ rate.update(&[0], queued(15, 1200), start + Duration::from_secs(2)),
3820
+ None
3821
+ );
3822
+ // ...and do once it has.
3823
+ assert_eq!(
3824
+ rate.update(&[0], queued(2, 1200), start + Duration::from_secs(3)),
3825
+ Some(1390)
3826
+ );
3827
+ // Nothing drops below the floor.
3828
+ let mut rate = RateControl::new(8000);
3829
+ for step in 0..20u64 {
3830
+ rate.update(
3831
+ &[],
3832
+ queued(200, 1),
3833
+ start + DELAY_BACKOFF_EVERY * step as u32,
3834
+ );
3835
+ }
3836
+ assert_eq!(rate.target, 1000);
3837
+ }
3838
+
3839
+ #[test]
3840
+ fn without_transport_feedback_the_rate_follows_loss_alone() {
3841
+ let start = Instant::now();
3842
+ let mut rate = RateControl::new(8000);
3843
+ assert_eq!(rate.update(&[], None, start), None);
3844
+ assert_eq!(rate.update(&[64], None, start), Some(5600));
3845
+ assert_eq!(
3846
+ rate.update(&[64], None, start + Duration::from_millis(500)),
3847
+ None
3848
+ );
3849
+ assert_eq!(
3850
+ rate.update(&[10], None, start + Duration::from_secs(1)),
3851
+ None
3852
+ );
3853
+ assert_eq!(
3854
+ rate.update(&[0], None, start + Duration::from_secs(2)),
3855
+ Some(6112)
3856
+ );
3857
+ }
3858
+
3859
+ #[test]
3860
+ fn a_large_frame_holds_the_next_until_the_link_has_carried_it() {
3861
+ let start = Instant::now();
3862
+ let mut budget = SendBudget::default();
3863
+ // Frames at the target, one per 20 ms at 4000 kbps, never wait, and
3864
+ // neither does the ordinary overshoot of a frame at twice the budget.
3865
+ for n in 0..50u32 {
3866
+ let at = start + Duration::from_millis(20) * n;
3867
+ assert_eq!(budget.wait(at, 4000), Duration::ZERO, "frame {n} waited");
3868
+ budget.sent(at, if n % 5 == 0 { 20_000 } else { 10_000 }, 4000);
3869
+ }
3870
+ // A 100 kB key frame drains in 160 ms (4000 kbps and a quarter): it
3871
+ // goes at once, and the next waits out all of it past the 60 ms burst
3872
+ // allowance.
3873
+ let at = start + Duration::from_secs(1);
3874
+ assert_eq!(budget.wait(at, 4000), Duration::ZERO);
3875
+ budget.sent(at, 100_000, 4000);
3876
+ let wait = budget.wait(at, 4000);
3877
+ assert!(
3878
+ (Duration::from_millis(99)..=Duration::from_millis(101)).contains(&wait),
3879
+ "waited {wait:?}"
3880
+ );
3881
+ assert_eq!(budget.wait(at + wait, 4000), Duration::ZERO);
3882
+ }
3883
+
3884
+ #[test]
3885
+ fn motion_halves_only_when_the_encoder_sets_the_pace_and_does_not_flap() {
3886
+ let interval = Duration::from_millis(16);
3887
+ let (slow, fast) = (Duration::from_millis(30), Duration::from_millis(2));
3888
+ let mut size = MotionSize::default();
3889
+ for _ in 0..MotionSize::SLOW_RUN * 4 {
3890
+ size.coded(slow, false, true, interval);
3891
+ }
3892
+ assert!(
3893
+ !size.half,
3894
+ "slow small frames (typing on a busy host) keep full size"
3895
+ );
3896
+ for _ in 0..MotionSize::SLOW_RUN * 4 {
3897
+ size.coded(slow, true, false, interval);
3898
+ }
3899
+ assert!(
3900
+ !size.half,
3901
+ "a source slower than the cap (30 fps, 30 ms encode) never halves"
3902
+ );
3903
+ for _ in 0..MotionSize::SLOW_RUN - 1 {
3904
+ size.coded(slow, true, true, interval);
3905
+ }
3906
+ size.coded(fast, true, true, interval);
3907
+ assert!(!size.half, "a broken run of slow frames keeps full size");
3908
+ for _ in 0..MotionSize::SLOW_RUN {
3909
+ size.coded(slow, true, true, interval);
3910
+ }
3911
+ assert!(size.half, "a run of slow large frames halves");
3912
+ // The marginal band: half-size frames that are small but still slow,
3913
+ // between large ones. Neither may send it back to full size.
3914
+ for i in 0..MotionSize::SMALL_RUN * 10 {
3915
+ size.coded(slow, i % 7 == 0, true, interval);
3916
+ assert!(size.half, "marginal half-size motion must not flap back");
3917
+ }
3918
+ for _ in 0..MotionSize::SMALL_RUN * 2 {
3919
+ size.coded(fast, true, true, interval);
3920
+ }
3921
+ assert!(size.half, "large half-size frames stay half, however fast");
3922
+ for _ in 0..MotionSize::SMALL_RUN {
3923
+ size.coded(fast, false, true, interval);
3924
+ }
3925
+ assert!(
3926
+ !size.half,
3927
+ "a run of small, fast half-size frames returns to full size"
3928
+ );
3929
+ }
3930
+
3306
3931
  #[tokio::test]
3307
3932
  async fn the_pipeline_caps_the_rate_codes_the_last_frame_and_refines_a_still_desktop_once() {
3308
3933
  let (events, _events_rx) = tokio_mpsc::unbounded_channel();
@@ -3319,11 +3944,14 @@ mod tests {
3319
3944
 
3320
3945
  // 40 frames over 200 ms is 200 fps; the requested 20 fps caps what leaves.
3321
3946
  for _ in 0..40 {
3322
- frame_tx.put(I420 {
3323
- width: 64,
3324
- height: 64,
3325
- data: vec![128u8; 64 * 64 + 2 * 32 * 32],
3326
- });
3947
+ frame_tx.put(
3948
+ I420 {
3949
+ width: 64,
3950
+ height: 64,
3951
+ data: vec![128u8; 64 * 64 + 2 * 32 * 32],
3952
+ },
3953
+ Instant::now(),
3954
+ );
3327
3955
  tokio::time::sleep(Duration::from_millis(5)).await;
3328
3956
  }
3329
3957
  let motion = inner.metrics.lock().unwrap().encoded_frames;
@@ -3362,5 +3990,74 @@ mod tests {
3362
3990
  "a still stream still sends"
3363
3991
  );
3364
3992
  assert_eq!((later.key_frames, later.refined_frames), (1, 1));
3993
+ assert_eq!((later.codec, later.encoder), ("vp9", "libvpx"));
3994
+ }
3995
+
3996
+ #[tokio::test]
3997
+ async fn a_receiver_that_asks_for_h264_is_sent_h264_from_the_first_frame() {
3998
+ let Some(info) = tokio::task::spawn_blocking(crate::h264::probe)
3999
+ .await
4000
+ .unwrap()
4001
+ else {
4002
+ eprintln!("skipped: no H.264 encoder starts on this machine");
4003
+ return;
4004
+ };
4005
+ let (peer, _answer) =
4006
+ crate::peer::tests::answered(vec![crate::peer::tests::h264_receiver_codec()]).await;
4007
+ let (events, _events_rx) = tokio_mpsc::unbounded_channel();
4008
+ let (inner, _recorded) = test_inner_with(events, peer, 320, 240);
4009
+ // The answer is real, so ICE runs and, with no candidates, never
4010
+ // connects; each state it reports would take back `assume_connected`.
4011
+ let pinned = current_peer(&inner).expect("the test's peer");
4012
+ let pin = tokio::spawn(async move {
4013
+ loop {
4014
+ pinned.assume_connected();
4015
+ tokio::time::sleep(Duration::from_millis(5)).await;
4016
+ }
4017
+ });
4018
+ let (frame_tx, frame_rx) = latest_frame();
4019
+ spawn_pipeline(
4020
+ &inner,
4021
+ current_peer(&inner).expect("the test's peer"),
4022
+ frame_rx,
4023
+ Arc::new(AtomicBool::new(true)),
4024
+ 20,
4025
+ 1000,
4026
+ );
4027
+ for shade in 0..10u8 {
4028
+ frame_tx.put(
4029
+ I420 {
4030
+ width: 320,
4031
+ height: 240,
4032
+ data: vec![shade * 20; 320 * 240 * 3 / 2],
4033
+ },
4034
+ Instant::now(),
4035
+ );
4036
+ tokio::time::sleep(Duration::from_millis(50)).await;
4037
+ }
4038
+ // Opening a hardware session can take a good part of a second, more
4039
+ // with other tests holding the GPU: wait for the still to be refined.
4040
+ let deadline = Instant::now() + Duration::from_secs(10);
4041
+ while inner.metrics.lock().unwrap().refined_frames == 0 && Instant::now() < deadline {
4042
+ tokio::time::sleep(Duration::from_millis(50)).await;
4043
+ }
4044
+ let metrics = inner.metrics.lock().unwrap().clone();
4045
+ assert_eq!((metrics.codec, metrics.encoder), ("h264", info.encoder));
4046
+ assert!(metrics.encoded_frames >= 2, "{metrics:?}");
4047
+ assert_eq!(
4048
+ metrics.key_frames, 1,
4049
+ "one IDR, then inter frames: {metrics:?}"
4050
+ );
4051
+ assert_eq!(metrics.refined_frames, 1, "a still desktop is refined once");
4052
+ assert_eq!(
4053
+ metrics.halved_frames, 0,
4054
+ "H.264 is always coded at full size"
4055
+ );
4056
+ pin.abort();
4057
+ inner.close("done", "closed").await;
4058
+ assert!(
4059
+ lock(&inner.encoder).is_none(),
4060
+ "closing releases the encoder"
4061
+ );
3365
4062
  }
3366
4063
  }