@desklink/host 0.1.1 → 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 (78) hide show
  1. package/NOTICE +23 -0
  2. package/README.md +107 -55
  3. package/bin/desklink-host.mjs +2 -2
  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/index.d.ts +1 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/keeper.d.ts +52 -0
  16. package/dist/keeper.d.ts.map +1 -0
  17. package/dist/keeper.js +101 -0
  18. package/dist/keeper.js.map +1 -0
  19. package/dist/protocol.d.ts +26 -0
  20. package/dist/protocol.d.ts.map +1 -1
  21. package/dist/protocol.js.map +1 -1
  22. package/dist/resolveEngine.d.ts +3 -3
  23. package/dist/resolveEngine.d.ts.map +1 -1
  24. package/dist/resolveEngine.js +24 -11
  25. package/dist/resolveEngine.js.map +1 -1
  26. package/docs/BOUNDARY.md +20 -8
  27. package/docs/PROTOCOL.md +347 -24
  28. package/engine/Cargo.lock +47 -1
  29. package/engine/Cargo.toml +13 -2
  30. package/engine/build.rs +28 -2
  31. package/engine/native/nvenc_shim.c +453 -0
  32. package/engine/native/vaapi_shim.c +727 -0
  33. package/engine/native/vpx_shim.c +54 -4
  34. package/engine/native/vt_shim.c +448 -0
  35. package/engine/src/capture.rs +75 -55
  36. package/engine/src/capture_mac.rs +24 -14
  37. package/engine/src/convert.rs +286 -61
  38. package/engine/src/encoder.rs +122 -13
  39. package/engine/src/h264.rs +188 -0
  40. package/engine/src/h264_nvenc.rs +308 -0
  41. package/engine/src/h264_openh264.rs +735 -0
  42. package/engine/src/h264_vaapi.rs +263 -0
  43. package/engine/src/h264_vt.rs +368 -0
  44. package/engine/src/keeper.rs +622 -0
  45. package/engine/src/mac.rs +9 -6
  46. package/engine/src/main.rs +64 -4
  47. package/engine/src/peer.rs +887 -42
  48. package/engine/src/protocol.rs +153 -4
  49. package/engine/src/session.rs +1547 -137
  50. package/engine/src/x11.rs +210 -9
  51. package/engine/src/x11_mac.rs +12 -0
  52. package/engine/vendor/libva/COPYING +19 -0
  53. package/engine/vendor/libva/va/va.h +5450 -0
  54. package/engine/vendor/libva/va/va_dec_av1.h +695 -0
  55. package/engine/vendor/libva/va/va_dec_hevc.h +616 -0
  56. package/engine/vendor/libva/va/va_dec_jpeg.h +157 -0
  57. package/engine/vendor/libva/va/va_dec_vp8.h +249 -0
  58. package/engine/vendor/libva/va/va_dec_vp9.h +312 -0
  59. package/engine/vendor/libva/va/va_dec_vvc.h +688 -0
  60. package/engine/vendor/libva/va/va_drm.h +61 -0
  61. package/engine/vendor/libva/va/va_enc_av1.h +1010 -0
  62. package/engine/vendor/libva/va/va_enc_h264.h +659 -0
  63. package/engine/vendor/libva/va/va_enc_hevc.h +958 -0
  64. package/engine/vendor/libva/va/va_enc_jpeg.h +162 -0
  65. package/engine/vendor/libva/va/va_enc_mpeg2.h +308 -0
  66. package/engine/vendor/libva/va/va_enc_vp8.h +350 -0
  67. package/engine/vendor/libva/va/va_enc_vp9.h +639 -0
  68. package/engine/vendor/libva/va/va_fei.h +170 -0
  69. package/engine/vendor/libva/va/va_fei_h264.h +505 -0
  70. package/engine/vendor/libva/va/va_fei_hevc.h +254 -0
  71. package/engine/vendor/libva/va/va_prot.h +525 -0
  72. package/engine/vendor/libva/va/va_version.h +87 -0
  73. package/engine/vendor/libva/va/va_vpp.h +1590 -0
  74. package/engine/vendor/nv-codec-headers/LICENSE +24 -0
  75. package/engine/vendor/nv-codec-headers/README +7 -0
  76. package/engine/vendor/nv-codec-headers/include/ffnvcodec/nvEncodeAPI.h +3825 -0
  77. package/examples/reference-client.html +1 -1
  78. package/package.json +3 -2
@@ -8,14 +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::{PeerEvent, TransportOptions, VideoPeer};
14
+ use crate::peer::{
15
+ h264_profile_level_id, PathReport, PeerEvent, TransportOptions, VideoCodec, VideoPeer,
16
+ };
15
17
  use crate::portal::{self, SelectedSource};
16
- use crate::protocol::{ControlMessage, ControlReply, Permission, PointerPhase, SourceRequest};
18
+ use crate::protocol::{
19
+ ControlMessage, ControlReply, EncodedCodec, Permission, PointerPhase, SourceRequest,
20
+ };
17
21
  use crate::x11::X11Desktop;
18
- use anyhow::Result;
22
+ use anyhow::{Context, Result};
19
23
  use std::sync::atomic::{AtomicBool, Ordering};
20
24
  use std::sync::{Arc, Condvar, Mutex};
21
25
  use std::time::{Duration, Instant};
@@ -49,16 +53,48 @@ pub struct Metrics {
49
53
  pub dropped_frames: u64,
50
54
  pub encoded_frames: u64,
51
55
  pub encoded_bytes: u64,
52
- /// 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.
53
58
  pub key_frames: u64,
54
59
  /// Refinement passes: a still desktop re-coded once, sharp.
55
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,
56
66
  /// Time spent in the encoder, in microseconds, over every encoded frame.
57
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,
58
84
  /// The current rate target, after any loss back-off.
59
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,
60
91
  pub input_applied: u64,
61
92
  pub input_rejected: u64,
93
+ /// Access units the consumer fed to an encoded source.
94
+ pub fed_frames: u64,
95
+ /// Control messages forwarded back to the consumer that owns the device,
96
+ /// instead of being applied to a desktop this engine holds.
97
+ pub input_forwarded: u64,
62
98
  }
63
99
 
64
100
  fn tile_hashes(raw: &[u8], width: usize, height: usize) -> Vec<u64> {
@@ -178,6 +214,7 @@ fn select_x11(
178
214
  let desktop = Arc::new(Mutex::new(X11Desktop::connect(display)?));
179
215
  let (width, height) = {
180
216
  let desktop = lock(&desktop);
217
+ eprintln!("x11 capture path: {}", desktop.capture_path());
181
218
  desktop.screen_size()
182
219
  };
183
220
  let source = SelectedSource {
@@ -200,26 +237,75 @@ fn select_x11(
200
237
  .spawn(move || {
201
238
  let interval = frame_interval(max_fps);
202
239
  let mut sequence = 0u64;
203
- // An X server has no damage signal here, so an unchanged screen
204
- // is recognised by its pixels: handing it on would keep the
205
- // 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();
206
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;
207
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
+ }
208
263
  let started = Instant::now();
264
+ let grabbed = Instant::now();
209
265
  let frame = {
210
266
  let mut desktop = lock(&desktop);
211
267
  desktop.capture_with_pixels(max_width, max_height)
212
268
  };
269
+ let grab_micros = grabbed.elapsed().as_micros() as u64;
213
270
  match frame {
214
271
  Ok((frame, raw)) => {
215
- use std::hash::{Hash, Hasher};
216
- let mut hasher = std::collections::hash_map::DefaultHasher::new();
217
- raw.hash(&mut hasher);
218
- let hash = hasher.finish();
219
- if last_hash != Some(hash) {
220
- last_hash = Some(hash);
221
- 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
+ });
222
306
  sequence += 1;
307
+ last_sent = Some(started);
308
+ first = false;
223
309
  }
224
310
  }
225
311
  Err(error) => {
@@ -232,11 +318,21 @@ fn select_x11(
232
318
  on_stop(format!("X11 capture stopped: {error}"));
233
319
  break;
234
320
  }
321
+ if !hashed {
322
+ let elapsed = started.elapsed();
323
+ if elapsed < interval {
324
+ std::thread::sleep(interval - elapsed);
325
+ }
326
+ }
235
327
  }
236
328
  }
237
- let elapsed = started.elapsed();
238
- if elapsed < interval {
239
- 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
+ }
240
336
  }
241
337
  }
242
338
  })
@@ -460,7 +556,7 @@ fn x11_button(button: Button) -> u8 {
460
556
  }
461
557
  }
462
558
 
463
- fn lock<T>(mutex: &Arc<Mutex<T>>) -> std::sync::MutexGuard<'_, T> {
559
+ fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
464
560
  // A poisoned lock means a previous input call panicked while applying; the
465
561
  // desktop is then in unknown state, so the session is closed rather than
466
562
  // continuing to drive it.
@@ -470,6 +566,7 @@ fn lock<T>(mutex: &Arc<Mutex<T>>) -> std::sync::MutexGuard<'_, T> {
470
566
  }
471
567
 
472
568
  /// Why a session could not do what was asked, in the protocol's own vocabulary.
569
+ #[derive(Debug)]
473
570
  pub struct SessionError {
474
571
  pub code: &'static str,
475
572
  pub message: String,
@@ -498,6 +595,9 @@ pub struct OpenRequest {
498
595
  /// Offer a passive ICE-TCP candidate on the loopback, for a forwarded client.
499
596
  pub loopback_tcp: bool,
500
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,
501
601
  }
502
602
 
503
603
  /// Events a session raises for its consumer.
@@ -522,10 +622,29 @@ pub enum SessionEvent {
522
622
  seq: u64,
523
623
  damage: Vec<[usize; 4]>,
524
624
  },
625
+ /// One control-channel message from the client, for a source whose device
626
+ /// the consumer drives. The engine applied nothing locally.
627
+ Input {
628
+ input: serde_json::Value,
629
+ },
630
+ /// The far end needs a fresh reference frame and only the consumer can make
631
+ /// one: the encoder for this track is on its side. Raised by an RTCP PLI or
632
+ /// FIR, and once when the transport connects, because everything sent before
633
+ /// that was dropped.
634
+ KeyframeRequest {
635
+ generation: u64,
636
+ },
525
637
  RestoreToken(String),
526
- CaptureStopped { reason: String },
638
+ CaptureStopped {
639
+ reason: String,
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.
527
645
  Revoked {
528
646
  reason: String,
647
+ code: &'static str,
529
648
  },
530
649
  }
531
650
 
@@ -552,12 +671,18 @@ struct Inner {
552
671
  latest: Arc<Mutex<Option<FrameSnapshot>>>,
553
672
  capture_error: Arc<Mutex<Option<String>>>,
554
673
  frame_changes: tokio::sync::watch::Receiver<u64>,
555
- peer: Arc<VideoPeer>,
556
- encoder: Mutex<Encoder>,
674
+ /// Built when the codec is known: with `session.open` for a captured
675
+ /// desktop, and with the first fed access unit for an encoded source, whose
676
+ /// offer has to name the profile the stream's own SPS declares.
677
+ peer: Mutex<Option<Arc<VideoPeer>>>,
678
+ /// Absent for an encoded source: the consumer is the encoder.
679
+ encoder: Mutex<Option<VideoEncoder>>,
557
680
  input: Mutex<Option<InputTarget>>,
558
681
  indicator: Mutex<Option<crate::indicator::Indicator>>,
559
682
  capture: Mutex<Option<FrameSource>>,
560
- layout: Mutex<Layout>,
683
+ /// Absent for an encoded source: there is no local keyboard to translate a
684
+ /// character through, because the client's keys are forwarded instead.
685
+ layout: Mutex<Option<Layout>>,
561
686
  last_seq: Mutex<u64>,
562
687
  control_open: AtomicBool,
563
688
  closed: AtomicBool,
@@ -565,8 +690,37 @@ struct Inner {
565
690
  /// from the lease as well as from the consumer.
566
691
  pipeline: Arc<AtomicBool>,
567
692
  events: tokio_mpsc::UnboundedSender<Notice>,
693
+ /// Present only for an encoded source.
694
+ encoded: Mutex<Option<Encoded>>,
695
+ /// `OpenRequest::local_frames`: whether `latest` is kept at all.
696
+ local_frames: bool,
697
+ }
698
+
699
+ /// A source whose video arrives already encoded.
700
+ struct Encoded {
701
+ /// What the consumer said it would feed. Only H.264 exists today; the offer
702
+ /// is built from this rather than assumed, so a second codec is one arm here.
703
+ codec: EncodedCodec,
704
+ /// The transport settings the consumer chose in `session.open`, kept until
705
+ /// the peer that uses them can be built.
706
+ transport: TransportOptions,
707
+ /// Where the peer's events go once it exists.
708
+ peer_events: tokio_mpsc::UnboundedSender<PeerEvent>,
709
+ /// Access units waiting to be packetized.
710
+ feed: tokio_mpsc::Sender<FedAccessUnit>,
568
711
  }
569
712
 
713
+ /// One access unit a consumer fed, with the keyframe flag it declared.
714
+ struct FedAccessUnit {
715
+ keyframe: bool,
716
+ data: Vec<u8>,
717
+ }
718
+
719
+ /// How many fed access units may wait. A live stream drops the newest rather
720
+ /// than queue: the packetizer is the only consumer, and a backlog of
721
+ /// milliseconds-old pictures is worse than a gap the receiver asks to repair.
722
+ const FEED_BACKLOG: usize = 8;
723
+
570
724
  pub struct Session {
571
725
  inner: Arc<Inner>,
572
726
  }
@@ -620,7 +774,7 @@ pub fn capabilities() -> serde_json::Value {
620
774
  "cursor": "embedded",
621
775
  "audio": false,
622
776
  },
623
- "encode": { "codecs": ["vp9"], "hardware": false },
777
+ "encode": encode_capabilities(),
624
778
  "input": {
625
779
  "mechanism": "inputtino/uinput",
626
780
  "pointer": input,
@@ -633,6 +787,10 @@ pub fn capabilities() -> serde_json::Value {
633
787
  },
634
788
  "clipboard": { "read": clipboard, "write": clipboard && clipboard::writer_available(), "mime": ["text/plain;charset=utf-8"],
635
789
  "maxBytes": clipboard::MAX_CLIPBOARD_BYTES },
790
+ // Video this engine does not capture or encode but can carry, so a
791
+ // consumer can tell an old engine from one that takes `session.feed`
792
+ // without guessing from a version string.
793
+ "encoded": { "codecs": ["h264"] },
636
794
  })
637
795
  }
638
796
 
@@ -674,7 +832,7 @@ pub fn capabilities() -> serde_json::Value {
674
832
  "remedy": format!("Allow {} in System Settings › Privacy & Security › Screen & System Audio Recording, then reconnect.", crate::mac::tcc_responsible_app_name())
675
833
  }) }
676
834
  },
677
- "encode": { "codecs": ["vp9"], "hardware": false },
835
+ "encode": encode_capabilities(),
678
836
  "input": {
679
837
  "mechanism": "quartz-cgevent",
680
838
  "pointer": input,
@@ -687,6 +845,25 @@ pub fn capabilities() -> serde_json::Value {
687
845
  },
688
846
  "clipboard": { "read": true, "write": true, "mime": ["text/plain;charset=utf-8"],
689
847
  "maxBytes": clipboard::MAX_CLIPBOARD_BYTES },
848
+ "encoded": { "codecs": ["h264"] },
849
+ })
850
+ }
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
+ },
690
867
  })
691
868
  }
692
869
 
@@ -705,6 +882,17 @@ impl Session {
705
882
  let wants_control = request.permissions.contains(&Permission::Control);
706
883
  let wants_x11 = matches!(request.source, Some(SourceRequest::X11 { .. }));
707
884
  let max_fps = request.max_fps.clamp(1, 60);
885
+ // A source the consumer feeds needs no capture backend, no encoder and
886
+ // no local input device: the device the client drives is the consumer's,
887
+ // and the offer has to wait for the stream's own SPS.
888
+ if let Some(SourceRequest::Encoded {
889
+ codec,
890
+ width,
891
+ height,
892
+ }) = request.source.clone()
893
+ {
894
+ return Self::open_encoded(request, codec, width, height, events).await;
895
+ }
708
896
  // Which desktop decides which input path is even available: an X display
709
897
  // takes XTest, which cannot reach any other session, while a portal
710
898
  // desktop needs kernel input access.
@@ -738,19 +926,41 @@ impl Session {
738
926
  let captured = metrics.clone();
739
927
  let stop_signal = frame_tx_signal.clone();
740
928
  let on_capture_status = move |running: bool, reason: String| {
741
- if let Ok(mut error) = stopped_error.lock() { *error = if running { None } else { Some(reason.clone()) }; }
742
- if let Ok(mut frame) = stopped_latest.lock() { *frame = None; }
929
+ if let Ok(mut error) = stopped_error.lock() {
930
+ *error = if running { None } else { Some(reason.clone()) };
931
+ }
932
+ if let Ok(mut frame) = stopped_latest.lock() {
933
+ *frame = None;
934
+ }
743
935
  stop_signal.send_replace(0);
744
- let _ = stopped_events.send(Notice { session_id: stopped_id.clone(),
745
- event: if running { SessionEvent::State { capture: "streaming", transport: String::from("connected"), first_frame: false } }
746
- else { SessionEvent::CaptureStopped { reason } } });
936
+ let _ = stopped_events.send(Notice {
937
+ session_id: stopped_id.clone(),
938
+ event: if running {
939
+ SessionEvent::State {
940
+ capture: "streaming",
941
+ transport: String::from("connected"),
942
+ first_frame: false,
943
+ }
944
+ } else {
945
+ SessionEvent::CaptureStopped { reason }
946
+ },
947
+ });
747
948
  };
748
949
  let capture_status = Arc::new(on_capture_status);
749
950
  let frame_events = events.clone();
750
951
  let frame_session_id = id.clone();
751
- let sink = Box::new(move |frame: I420, _seq: u64, raw: Vec<u8>| {
752
- let hashes = tile_hashes(&raw, frame.width, frame.height);
753
- 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()) {
754
964
  let unchanged =
755
965
  held.as_ref()
756
966
  .is_some_and(|(_, _, width, height, _, current, _)| {
@@ -781,10 +991,11 @@ impl Session {
781
991
  }
782
992
  if let Ok(mut m) = captured.lock() {
783
993
  m.captured_frames += 1;
994
+ m.convert_micros += captured_at.elapsed().as_micros() as u64;
784
995
  }
785
996
  // An encoder that is behind gets the newest frame, not a queue: a
786
997
  // desktop stream is live, and the last state is the one that counts.
787
- if frame_tx.put(frame) {
998
+ if frame_tx.put(frame, captured_at) {
788
999
  if let Ok(mut m) = captured.lock() {
789
1000
  m.dropped_frames += 1;
790
1001
  }
@@ -803,7 +1014,10 @@ impl Session {
803
1014
  max_fps,
804
1015
  metrics.clone(),
805
1016
  sink,
806
- Box::new({ let status = capture_status.clone(); move |reason| status(false, reason) }),
1017
+ Box::new({
1018
+ let status = capture_status.clone();
1019
+ move |reason| status(false, reason)
1020
+ }),
807
1021
  )
808
1022
  .map_err(|error| SessionError::new("source", format!("{error:#}")))?,
809
1023
  _ => {
@@ -852,7 +1066,10 @@ impl Session {
852
1066
  max_fps,
853
1067
  sink,
854
1068
  indicator.as_ref().map(crate::indicator::Indicator::pid),
855
- Box::new({ let status = capture_status.clone(); move |running, reason| status(running, reason) }),
1069
+ Box::new({
1070
+ let status = capture_status.clone();
1071
+ move |running, reason| status(running, reason)
1072
+ }),
856
1073
  )
857
1074
  .map_err(|error| SessionError::new("source", format!("{error:#}")))?;
858
1075
  #[cfg(target_os = "linux")]
@@ -865,7 +1082,10 @@ impl Session {
865
1082
  indicator
866
1083
  .as_ref()
867
1084
  .map(crate::indicator::Indicator::position),
868
- Box::new({ let status = capture_status.clone(); move |reason| status(false, reason) }),
1085
+ Box::new({
1086
+ let status = capture_status.clone();
1087
+ move |reason| status(false, reason)
1088
+ }),
869
1089
  )
870
1090
  .map_err(|error| SessionError::new("source", format!("{error:#}")))?;
871
1091
  #[cfg(target_os = "linux")]
@@ -953,6 +1173,16 @@ impl Session {
953
1173
  SessionError::new("input", format!("no keyboard layout: {error:#}"))
954
1174
  })?;
955
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
+ };
956
1186
  let (peer_events_tx, peer_events_rx) = tokio_mpsc::unbounded_channel::<PeerEvent>();
957
1187
  let (peer, offer) = VideoPeer::offer(
958
1188
  TransportOptions {
@@ -963,9 +1193,11 @@ impl Session {
963
1193
  pace_bps: (bitrate_kbps as f64 * 3_000.0).max(20_000_000.0),
964
1194
  },
965
1195
  peer_events_tx,
1196
+ codec,
966
1197
  )
967
1198
  .await
968
1199
  .map_err(|error| SessionError::new("transport", format!("{error:#}")))?;
1200
+ let peer = Arc::new(peer);
969
1201
 
970
1202
  let generation = 1u64;
971
1203
  let geometry = serde_json::json!({
@@ -985,17 +1217,19 @@ impl Session {
985
1217
  latest,
986
1218
  capture_error,
987
1219
  frame_changes,
988
- peer: Arc::new(peer),
989
- encoder: Mutex::new(encoder),
1220
+ peer: Mutex::new(Some(peer.clone())),
1221
+ encoder: Mutex::new(Some(VideoEncoder::Vp9(encoder))),
990
1222
  input: Mutex::new(input),
991
1223
  indicator: Mutex::new(indicator),
992
1224
  capture: Mutex::new(Some(capture)),
993
- layout: Mutex::new(layout),
1225
+ layout: Mutex::new(Some(layout)),
994
1226
  last_seq: Mutex::new(0),
995
1227
  control_open: AtomicBool::new(false),
996
1228
  closed: AtomicBool::new(false),
997
1229
  pipeline: pipeline.clone(),
998
1230
  events: events.clone(),
1231
+ encoded: Mutex::new(None),
1232
+ local_frames: request.local_frames,
999
1233
  });
1000
1234
 
1001
1235
  inner.notify(SessionEvent::Description {
@@ -1008,13 +1242,141 @@ impl Session {
1008
1242
  first_frame: false,
1009
1243
  });
1010
1244
 
1011
- spawn_pipeline(&inner, frame_rx, pipeline, max_fps, bitrate_kbps);
1245
+ spawn_pipeline(&inner, peer, frame_rx, pipeline, max_fps, bitrate_kbps);
1012
1246
  spawn_peer_events(&inner, peer_events_rx);
1013
1247
  spawn_lease(&inner, request.ttl.unwrap_or(Duration::from_secs(3600)));
1014
1248
 
1015
1249
  Ok(Self { inner })
1016
1250
  }
1017
1251
 
1252
+ /// Open a session over video the consumer already encoded.
1253
+ ///
1254
+ /// Nothing is captured, encoded or driven locally: the engine packetizes
1255
+ /// what it is fed and sends the client's control messages back to the
1256
+ /// consumer, which owns the device. The peer is deliberately absent here —
1257
+ /// the offer must name the profile-level-id of the stream's own SPS, and
1258
+ /// that only exists once the first access unit arrives.
1259
+ async fn open_encoded(
1260
+ request: OpenRequest,
1261
+ codec: EncodedCodec,
1262
+ width: usize,
1263
+ height: usize,
1264
+ events: tokio_mpsc::UnboundedSender<Notice>,
1265
+ ) -> std::result::Result<Self, SessionError> {
1266
+ if width == 0 || height == 0 || width > 7680 || height > 4320 {
1267
+ return Err(SessionError::new(
1268
+ "source",
1269
+ format!(
1270
+ "an encoded surface must be 1..7680 by 1..4320 pixels, not {width}x{height}"
1271
+ ),
1272
+ ));
1273
+ }
1274
+ let id = opaque_id();
1275
+ let generation = 1u64;
1276
+ let metrics = Arc::new(Mutex::new(Metrics::default()));
1277
+ // Nothing raises it, and nothing waits for it: an encoded source has no
1278
+ // frame to read, only an encoded stream to forward.
1279
+ let (_frame_signal, frame_changes) = tokio::sync::watch::channel(0u64);
1280
+ let (peer_events, peer_events_rx) = tokio_mpsc::unbounded_channel::<PeerEvent>();
1281
+ let (feed, feed_rx) = tokio_mpsc::channel::<FedAccessUnit>(FEED_BACKLOG);
1282
+ let geometry = serde_json::json!({
1283
+ "source": { "width": width, "height": height },
1284
+ // Nothing is scaled: the client aims at the stream's own pixels, and
1285
+ // the consumer maps them to its device.
1286
+ "encoded": { "width": width, "height": height },
1287
+ "origin": { "x": 0, "y": 0 },
1288
+ });
1289
+ let inner = Arc::new(Inner {
1290
+ id,
1291
+ generation,
1292
+ permissions: request.permissions,
1293
+ source: SelectedSource {
1294
+ node_id: 0,
1295
+ width: width as i32,
1296
+ height: height as i32,
1297
+ position: Some((0, 0)),
1298
+ source_type: Some(String::from("encoded")),
1299
+ origin_x: 0,
1300
+ origin_y: 0,
1301
+ },
1302
+ geometry,
1303
+ metrics,
1304
+ latest: Arc::new(Mutex::new(None)),
1305
+ capture_error: Arc::new(Mutex::new(None)),
1306
+ frame_changes,
1307
+ peer: Mutex::new(None),
1308
+ encoder: Mutex::new(None),
1309
+ input: Mutex::new(None),
1310
+ indicator: Mutex::new(None),
1311
+ capture: Mutex::new(None),
1312
+ layout: Mutex::new(None),
1313
+ last_seq: Mutex::new(0),
1314
+ control_open: AtomicBool::new(false),
1315
+ closed: AtomicBool::new(false),
1316
+ pipeline: Arc::new(AtomicBool::new(false)),
1317
+ events: events.clone(),
1318
+ encoded: Mutex::new(Some(Encoded {
1319
+ codec,
1320
+ transport: TransportOptions {
1321
+ ice_servers: request.ice_servers,
1322
+ loopback_tcp: request.loopback_tcp,
1323
+ // A device stream is a few megabits; pacing exists to spread
1324
+ // a large frame, not to shape a rate this engine does not set.
1325
+ pace_bps: 20_000_000.0,
1326
+ },
1327
+ peer_events,
1328
+ feed,
1329
+ })),
1330
+ local_frames: false,
1331
+ });
1332
+ inner.notify(SessionEvent::State {
1333
+ capture: "consented",
1334
+ transport: String::from("new"),
1335
+ first_frame: false,
1336
+ });
1337
+ spawn_peer_events(&inner, peer_events_rx);
1338
+ spawn_encoded(&inner, feed_rx);
1339
+ spawn_lease(&inner, request.ttl.unwrap_or(Duration::from_secs(3600)));
1340
+ Ok(Self { inner })
1341
+ }
1342
+
1343
+ /// Take one access unit of an encoded source.
1344
+ ///
1345
+ /// The reply acknowledges the hand-off, not the picture: the unit is
1346
+ /// packetized behind this request, and `session.metrics` reports what was
1347
+ /// sent and what was dropped.
1348
+ pub fn feed(&self, keyframe: bool, data: Vec<u8>) -> std::result::Result<(), SessionError> {
1349
+ if self.inner.closed.load(Ordering::Relaxed) {
1350
+ return Err(SessionError::new("session", "the session has ended"));
1351
+ }
1352
+ let encoded = lock(&self.inner.encoded);
1353
+ let Some(encoded) = encoded.as_ref() else {
1354
+ return Err(SessionError::new(
1355
+ "operation",
1356
+ "this session has no encoded source to feed",
1357
+ ));
1358
+ };
1359
+ if data.is_empty() {
1360
+ return Err(SessionError::new(
1361
+ "operation",
1362
+ "an access unit cannot be empty",
1363
+ ));
1364
+ }
1365
+ if let Ok(mut m) = self.inner.metrics.lock() {
1366
+ m.fed_frames += 1;
1367
+ }
1368
+ if encoded
1369
+ .feed
1370
+ .try_send(FedAccessUnit { keyframe, data })
1371
+ .is_err()
1372
+ {
1373
+ if let Ok(mut m) = self.inner.metrics.lock() {
1374
+ m.dropped_frames += 1;
1375
+ }
1376
+ }
1377
+ Ok(())
1378
+ }
1379
+
1018
1380
  pub fn id(&self) -> &str {
1019
1381
  &self.inner.id
1020
1382
  }
@@ -1037,6 +1399,13 @@ impl Session {
1037
1399
  still_ms: Option<u64>,
1038
1400
  timeout_ms: u64,
1039
1401
  ) -> std::result::Result<(), SessionError> {
1402
+ if lock(&self.inner.encoded).is_some() {
1403
+ return Err(SessionError::new(
1404
+ "operation",
1405
+ "this session forwards an encoded stream and keeps no frame",
1406
+ ));
1407
+ }
1408
+ self.keeps_frames()?;
1040
1409
  let mut changes = self.inner.frame_changes.clone();
1041
1410
  let deadline = tokio::time::Instant::now() + Duration::from_millis(timeout_ms.min(120_000));
1042
1411
  loop {
@@ -1073,6 +1442,16 @@ impl Session {
1073
1442
  }
1074
1443
  }
1075
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
+
1076
1455
  /// Write only the requested lossless frame bytes; JSON carries metadata, never pixels.
1077
1456
  pub fn frame(
1078
1457
  &self,
@@ -1086,6 +1465,13 @@ impl Session {
1086
1465
  "view permission is required",
1087
1466
  ));
1088
1467
  }
1468
+ if lock(&self.inner.encoded).is_some() {
1469
+ return Err(SessionError::new(
1470
+ "operation",
1471
+ "this session forwards an encoded stream and keeps no frame",
1472
+ ));
1473
+ }
1474
+ self.keeps_frames()?;
1089
1475
  if let Some(reason) = lock(&self.inner.capture_error).clone() {
1090
1476
  return Err(SessionError::new("stream-stopped", reason));
1091
1477
  }
@@ -1140,7 +1526,32 @@ impl Session {
1140
1526
  }
1141
1527
 
1142
1528
  pub async fn accept_answer(&self, sdp: String) -> Result<()> {
1143
- self.inner.peer.accept_answer(sdp).await.map(|_applied| ())
1529
+ self.current_peer()?
1530
+ .accept_answer(sdp)
1531
+ .await
1532
+ .map(|_applied| ())
1533
+ }
1534
+
1535
+ /// The peer for this session, which an encoded source does not have until
1536
+ /// its first access unit has named the codec.
1537
+ fn current_peer(&self) -> Result<Arc<VideoPeer>> {
1538
+ self.inner
1539
+ .peer
1540
+ .lock()
1541
+ .ok()
1542
+ .and_then(|peer| peer.clone())
1543
+ .ok_or_else(|| anyhow::anyhow!("no offer has been made for this session yet"))
1544
+ }
1545
+
1546
+ /// Restart ICE: a fresh offer on the same peer, announced as a new
1547
+ /// `session.description` event for the client to answer as usual.
1548
+ pub async fn restart_ice(&self) -> Result<()> {
1549
+ let offer = self.current_peer()?.restart_ice().await?;
1550
+ self.inner.notify(SessionEvent::Description {
1551
+ generation: self.inner.generation,
1552
+ sdp: offer,
1553
+ });
1554
+ Ok(())
1144
1555
  }
1145
1556
 
1146
1557
  pub async fn add_candidate(
@@ -1149,8 +1560,7 @@ impl Session {
1149
1560
  sdp_mid: Option<String>,
1150
1561
  sdp_m_line_index: Option<u16>,
1151
1562
  ) -> Result<()> {
1152
- self.inner
1153
- .peer
1563
+ self.current_peer()?
1154
1564
  .add_candidate(candidate, sdp_mid, sdp_m_line_index)
1155
1565
  .await
1156
1566
  }
@@ -1190,7 +1600,7 @@ impl Session {
1190
1600
  }
1191
1601
 
1192
1602
  pub async fn close(&self, reason: &str) {
1193
- self.inner.close(reason).await;
1603
+ self.inner.close(reason, "closed").await;
1194
1604
  }
1195
1605
  }
1196
1606
 
@@ -1205,12 +1615,18 @@ impl Inner {
1205
1615
 
1206
1616
  /// Stop everything and release whatever input this session held. Idempotent,
1207
1617
  /// and the one teardown both an explicit close and a lease expiry take.
1208
- async fn close(&self, reason: &str) {
1618
+ async fn close(&self, reason: &str, code: &str) {
1209
1619
  if self.closed.swap(true, Ordering::SeqCst) {
1210
1620
  return;
1211
1621
  }
1212
1622
  self.control_open.store(false, Ordering::SeqCst);
1213
1623
  self.pipeline.store(false, Ordering::SeqCst);
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());
1214
1630
  if let Ok(mut indicator) = self.indicator.lock() {
1215
1631
  indicator.take();
1216
1632
  }
@@ -1222,14 +1638,16 @@ impl Inner {
1222
1638
  }
1223
1639
  *input = None;
1224
1640
  }
1225
- let _ = self
1226
- .peer
1227
- .send_control(
1228
- &serde_json::to_string(&ControlReply::Revoked { reason })
1229
- .unwrap_or_else(|_| String::from(r#"{"kind":"revoked","reason":"closed"}"#)),
1230
- )
1231
- .await;
1232
- self.peer.close().await;
1641
+ if let Some(peer) = self.peer.lock().ok().and_then(|peer| peer.clone()) {
1642
+ let _ = peer
1643
+ .send_control(
1644
+ &serde_json::to_string(&ControlReply::Revoked { reason, code }).unwrap_or_else(
1645
+ |_| String::from(r#"{"kind":"revoked","reason":"closed","code":"closed"}"#),
1646
+ ),
1647
+ )
1648
+ .await;
1649
+ peer.close().await;
1650
+ }
1233
1651
  // Dropping the capture stops the PipeWire stream and joins its thread,
1234
1652
  // which is what releases the compositor's consent for this session. It
1235
1653
  // has to happen synchronously, not when the last handle happens to fall
@@ -1256,7 +1674,10 @@ impl Inner {
1256
1674
  }
1257
1675
 
1258
1676
  fn clipboard_refusal(&self) -> Option<(&'static str, &'static str)> {
1259
- if self.source.source_type.as_deref() == Some("x11-root") || !wayland_clipboard_available()
1677
+ if matches!(
1678
+ self.source.source_type.as_deref(),
1679
+ Some("x11-root") | Some("encoded")
1680
+ ) || !wayland_clipboard_available()
1260
1681
  {
1261
1682
  return Some((
1262
1683
  "clipboard-unsupported",
@@ -1279,9 +1700,14 @@ impl Inner {
1279
1700
 
1280
1701
  fn reply(&self, payload: &ControlReply<'_>) -> Result<()> {
1281
1702
  let text = serde_json::to_string(payload)?;
1703
+ let Some(peer) = self.peer.lock().ok().and_then(|peer| peer.clone()) else {
1704
+ // An encoded source has no peer until its first access unit, so a
1705
+ // rejection raised before then has nowhere to go; the request that
1706
+ // caused it is refused in the same breath.
1707
+ return Ok(());
1708
+ };
1282
1709
  // The channel is asynchronous; a failure here means the peer is gone,
1283
1710
  // which the connection-state handler closes anyway.
1284
- let peer = self.peer.clone();
1285
1711
  tokio::spawn(async move {
1286
1712
  let _ = peer.send_control(&text).await;
1287
1713
  });
@@ -1318,7 +1744,7 @@ impl Inner {
1318
1744
  *last = seq;
1319
1745
  }
1320
1746
  let feedback = match &message {
1321
- ControlMessage::Pointer { phase, x, y, .. } => {
1747
+ ControlMessage::Pointer { phase, x, y, .. } if lock(&self.encoded).is_none() => {
1322
1748
  let (x, y) = to_source_pixels(*x, *y, self.encoded_size(), &self.source);
1323
1749
  Some((
1324
1750
  if matches!(phase, PointerPhase::Down) {
@@ -1334,6 +1760,13 @@ impl Inner {
1334
1760
  ControlMessage::Key { down: true, .. } => Some(('T', -1, -1)),
1335
1761
  _ => None,
1336
1762
  };
1763
+ // A source the consumer feeds is a device the consumer drives. Nothing is
1764
+ // applied to this machine: the message travels back as an event, and the
1765
+ // consumer injects it where the picture actually came from.
1766
+ if lock(&self.encoded).is_some() {
1767
+ self.forward(message);
1768
+ return;
1769
+ }
1337
1770
  let outcome = match message {
1338
1771
  ControlMessage::Pointer {
1339
1772
  phase,
@@ -1386,6 +1819,41 @@ impl Inner {
1386
1819
  }
1387
1820
  }
1388
1821
 
1822
+ /// Hand one client action back to the consumer, which owns the device this
1823
+ /// stream came from. The audit trail is the event itself and the count in
1824
+ /// `session.metrics`; nothing was applied here.
1825
+ fn forward(&self, message: ControlMessage) {
1826
+ let seq = message.seq();
1827
+ // The clipboard belongs to the device the consumer drives, and this
1828
+ // engine has none for this source; saying so is better than a request
1829
+ // that never answers.
1830
+ match &message {
1831
+ ControlMessage::ClipboardRead { request, .. }
1832
+ | ControlMessage::ClipboardWrite { request, .. } => {
1833
+ let _ = self.reply(&ControlReply::Clipboard {
1834
+ request,
1835
+ text: String::new(),
1836
+ truncated: false,
1837
+ error: Some("this source's clipboard belongs to the consumer"),
1838
+ });
1839
+ return;
1840
+ }
1841
+ _ => {}
1842
+ }
1843
+ let input = match serde_json::to_value(&message) {
1844
+ Ok(input) => input,
1845
+ Err(_) => {
1846
+ self.reject(seq, "operation", "unrecognised control message");
1847
+ return;
1848
+ }
1849
+ };
1850
+ self.notify(SessionEvent::Input { input });
1851
+ if let Ok(mut m) = self.metrics.lock() {
1852
+ m.input_forwarded += 1;
1853
+ }
1854
+ let _ = self.reply(&ControlReply::Ack { seq });
1855
+ }
1856
+
1389
1857
  fn with_input<T>(
1390
1858
  &self,
1391
1859
  action: impl FnOnce(&mut InputTarget) -> Result<T>,
@@ -1462,10 +1930,13 @@ impl Inner {
1462
1930
  }
1463
1931
 
1464
1932
  let stroke = {
1465
- let layout = self
1933
+ let held = self
1466
1934
  .layout
1467
1935
  .lock()
1468
1936
  .map_err(|_| ("session", String::from("no keyboard layout")))?;
1937
+ let layout = held
1938
+ .as_ref()
1939
+ .ok_or(("session", String::from("no keyboard layout")))?;
1469
1940
  if let Some(name) = name {
1470
1941
  keymap::named_key(&name)
1471
1942
  .ok_or(("text-unsupported", format!("unknown key {name}")))?
@@ -1512,12 +1983,15 @@ impl Inner {
1512
1983
  }
1513
1984
  #[cfg(target_os = "linux")]
1514
1985
  {
1515
- let layout = self
1986
+ let held = self
1516
1987
  .layout
1517
1988
  .lock()
1518
1989
  .map_err(|_| ("session", String::from("no keyboard layout")))?;
1990
+ let layout = held
1991
+ .as_ref()
1992
+ .ok_or(("session", String::from("no keyboard layout")))?;
1519
1993
  let (plan, unreachable) = layout.plan_text(text);
1520
- drop(layout);
1994
+ drop(held);
1521
1995
  if !unreachable.is_empty() {
1522
1996
  return Err((
1523
1997
  "text-unsupported",
@@ -1619,11 +2093,13 @@ fn frame_interval(max_fps: u32) -> Duration {
1619
2093
  Duration::from_secs_f64(1.0 / max_fps.max(1) as f64)
1620
2094
  }
1621
2095
 
1622
- /// The newest captured frame, handed from the capture thread to the encoder. A
1623
- /// newer frame replaces one the encoder has not taken, so however far behind
1624
- /// 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.
1625
2101
  struct FrameSlot {
1626
- frame: Mutex<(Option<I420>, bool)>,
2102
+ frame: Mutex<(Option<(I420, Instant)>, bool)>,
1627
2103
  ready: Condvar,
1628
2104
  }
1629
2105
 
@@ -1640,13 +2116,13 @@ fn latest_frame() -> (FrameSender, FrameReceiver) {
1640
2116
 
1641
2117
  impl FrameSender {
1642
2118
  /// Hand over a frame; true when it replaced one the encoder never took.
1643
- fn put(&self, frame: I420) -> bool {
2119
+ fn put(&self, frame: I420, captured: Instant) -> bool {
1644
2120
  let mut held = self
1645
2121
  .0
1646
2122
  .frame
1647
2123
  .lock()
1648
2124
  .unwrap_or_else(|poisoned| poisoned.into_inner());
1649
- let replaced = held.0.replace(frame).is_some();
2125
+ let replaced = held.0.replace((frame, captured)).is_some();
1650
2126
  self.0.ready.notify_one();
1651
2127
  replaced
1652
2128
  }
@@ -1666,12 +2142,22 @@ impl Drop for FrameSender {
1666
2142
  }
1667
2143
 
1668
2144
  enum Taken {
1669
- Frame(I420),
2145
+ Frame(I420, Instant),
1670
2146
  Timeout,
1671
2147
  Ended,
1672
2148
  }
1673
2149
 
1674
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
+
1675
2161
  fn take(&self, wait: Duration) -> Taken {
1676
2162
  let held = self
1677
2163
  .0
@@ -1684,7 +2170,7 @@ impl FrameReceiver {
1684
2170
  .wait_timeout_while(held, wait, |(frame, ended)| frame.is_none() && !*ended)
1685
2171
  .unwrap_or_else(|poisoned| poisoned.into_inner());
1686
2172
  match held.0.take() {
1687
- Some(frame) => Taken::Frame(frame),
2173
+ Some((frame, captured)) => Taken::Frame(frame, captured),
1688
2174
  None if held.1 => Taken::Ended,
1689
2175
  None => Taken::Timeout,
1690
2176
  }
@@ -1710,12 +2196,32 @@ fn auto_bitrate(width: usize, height: usize) -> u32 {
1710
2196
  (1500 + (width * height * 18 / 10_000) as u32).min(25_000)
1711
2197
  }
1712
2198
 
1713
- /// Loss-based rate target: back off quickly when the far end reports loss,
1714
- /// 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.
1715
2219
  struct RateControl {
1716
2220
  ceiling: u32,
1717
2221
  target: u32,
1718
2222
  last_change: Option<Instant>,
2223
+ /// The last queueing delay reported, while the path reports one.
2224
+ queue: Option<Duration>,
1719
2225
  }
1720
2226
 
1721
2227
  impl RateControl {
@@ -1724,26 +2230,40 @@ impl RateControl {
1724
2230
  ceiling,
1725
2231
  target: ceiling,
1726
2232
  last_change: None,
2233
+ queue: None,
1727
2234
  }
1728
2235
  }
1729
2236
 
1730
- /// The new target when these reports move it, at most once a second.
1731
- fn update(&mut self, reports: &[u8], now: Instant) -> Option<u32> {
1732
- let worst = *reports.iter().max()?;
1733
- if self
1734
- .last_change
1735
- .is_some_and(|at| now.duration_since(at) < Duration::from_secs(1))
1736
- {
1737
- 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);
1738
2243
  }
1739
- 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);
1740
2246
  let floor = (self.ceiling / 8).max(500);
1741
- let next = if loss > 0.10 {
1742
- ((self.target as f64 * 0.7) as u32).max(floor)
1743
- } else if loss < 0.02 {
1744
- ((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)
1745
2253
  } else {
1746
- 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
+ }
1747
2267
  };
1748
2268
  if next == self.target {
1749
2269
  return None;
@@ -1754,9 +2274,127 @@ impl RateControl {
1754
2274
  }
1755
2275
  }
1756
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
+
1757
2394
  enum Pass {
1758
2395
  Motion {
1759
2396
  keyframe: bool,
2397
+ half: bool,
1760
2398
  },
1761
2399
  Refine,
1762
2400
  /// The same picture again, so a still stream never looks stalled.
@@ -1774,6 +2412,7 @@ enum Pass {
1774
2412
  /// the desktop goes still the last frame is coded once more, sharp.
1775
2413
  fn spawn_pipeline(
1776
2414
  inner: &Arc<Inner>,
2415
+ peer: Arc<VideoPeer>,
1777
2416
  frame_rx: FrameReceiver,
1778
2417
  running: Arc<AtomicBool>,
1779
2418
  max_fps: u32,
@@ -1788,8 +2427,11 @@ fn spawn_pipeline(
1788
2427
  let mut rate = RateControl::new(bitrate_kbps);
1789
2428
  if let Ok(mut m) = inner.metrics.lock() {
1790
2429
  m.target_kbps = bitrate_kbps;
2430
+ (m.codec, m.encoder) = ("vp9", "libvpx");
1791
2431
  }
1792
- 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;
1793
2435
  // `pending`: the latest frame has not been coded. `refined`: it has
1794
2436
  // had its refinement pass, or there is nothing to refine.
1795
2437
  let mut pending = false;
@@ -1797,12 +2439,16 @@ fn spawn_pipeline(
1797
2439
  let mut keyframe = true;
1798
2440
  let mut still_since = Instant::now();
1799
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;
1800
2448
  loop {
1801
2449
  let now = Instant::now();
1802
- let slot = last_sent.map_or(Duration::ZERO, |at| {
1803
- interval.saturating_sub(now.duration_since(at))
1804
- });
1805
- let connected = inner.peer.is_connected();
2450
+ let slot = cadence.wait(now).max(budget.wait(now, rate.target));
2451
+ let connected = peer.is_connected();
1806
2452
  let wait = if !connected {
1807
2453
  // Nothing sent before the transport connects arrives; the
1808
2454
  // first frame after it is a key frame.
@@ -1821,13 +2467,13 @@ fn spawn_pipeline(
1821
2467
  .clamp(Duration::from_millis(10), Duration::from_millis(100))
1822
2468
  };
1823
2469
  match frame_rx.take(wait) {
1824
- Taken::Frame(frame) => {
2470
+ Taken::Frame(frame, captured) => {
1825
2471
  if pending {
1826
2472
  if let Ok(mut m) = inner.metrics.lock() {
1827
2473
  m.dropped_frames += 1;
1828
2474
  }
1829
2475
  }
1830
- latest = Some(frame);
2476
+ latest = Some((frame, captured));
1831
2477
  pending = true;
1832
2478
  refined = false;
1833
2479
  still_since = Instant::now();
@@ -1838,33 +2484,63 @@ fn spawn_pipeline(
1838
2484
  if !running.load(Ordering::Relaxed) {
1839
2485
  break;
1840
2486
  }
1841
- if inner.peer.take_keyframe_request() || !connected {
2487
+ if peer.take_keyframe_request() || !connected {
1842
2488
  keyframe = true;
1843
2489
  }
1844
- let reports = inner.peer.take_loss_reports();
1845
- if let Some(kbps) = rate.update(&reports, Instant::now()) {
1846
- let applied = inner
1847
- .encoder
1848
- .lock()
1849
- .map(|mut encoder| encoder.set_bitrate(kbps));
2490
+ let reports = peer.take_loss_reports();
2491
+ let path = peer.take_path_report();
2492
+ if let Some(kbps) = rate.update(&reports, path, Instant::now()) {
2493
+ let applied = inner.encoder.lock().map(|mut encoder| {
2494
+ encoder
2495
+ .as_mut()
2496
+ .map(|encoder| encoder.set_bitrate(kbps))
2497
+ .unwrap_or(Ok(()))
2498
+ });
1850
2499
  if matches!(applied, Ok(Ok(()))) {
1851
2500
  if let Ok(mut m) = inner.metrics.lock() {
1852
2501
  m.target_kbps = kbps;
1853
2502
  }
1854
2503
  }
1855
2504
  }
1856
- let Some(frame) = latest.as_ref() else {
2505
+ let Some((frame, captured)) = latest.as_ref() else {
1857
2506
  continue;
1858
2507
  };
1859
2508
  if !connected {
1860
2509
  continue;
1861
2510
  }
1862
2511
  let now = Instant::now();
1863
- 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() {
1864
2513
  continue;
1865
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
+ }
1866
2539
  let pass = if pending || keyframe {
1867
- Pass::Motion { keyframe }
2540
+ Pass::Motion {
2541
+ keyframe,
2542
+ half: size.half && halves,
2543
+ }
1868
2544
  } else if !refined && now.duration_since(still_since) >= REFINE_AFTER {
1869
2545
  Pass::Refine
1870
2546
  } else if refined
@@ -1877,14 +2553,17 @@ fn spawn_pipeline(
1877
2553
 
1878
2554
  let started = Instant::now();
1879
2555
  let packet = {
1880
- let mut encoder = match inner.encoder.lock() {
1881
- Ok(encoder) => encoder,
2556
+ let mut held = match inner.encoder.lock() {
2557
+ Ok(held) => held,
1882
2558
  Err(_) => break,
1883
2559
  };
2560
+ let Some(encoder) = held.as_mut() else {
2561
+ break;
2562
+ };
1884
2563
  match pass {
1885
- Pass::Motion { keyframe } => encoder.encode(frame, keyframe),
2564
+ Pass::Motion { keyframe, half } => encoder.encode(frame, keyframe, half),
1886
2565
  Pass::Refine => encoder.refine(frame),
1887
- Pass::Keepalive => encoder.encode(frame, false),
2566
+ Pass::Keepalive => encoder.encode(frame, false, false),
1888
2567
  }
1889
2568
  };
1890
2569
  let packet = match packet {
@@ -1902,22 +2581,29 @@ fn spawn_pipeline(
1902
2581
  let reason = String::from("the encoder rejected a frame");
1903
2582
  inner.notify(SessionEvent::Revoked {
1904
2583
  reason: reason.clone(),
2584
+ code: "error",
1905
2585
  });
1906
2586
  let target = inner.clone();
1907
2587
  handle.spawn(async move {
1908
- target.close(&reason).await;
2588
+ target.close(&reason, "error").await;
1909
2589
  });
1910
2590
  break;
1911
2591
  }
1912
2592
  };
2593
+ let took = started.elapsed();
1913
2594
  if let Ok(mut m) = inner.metrics.lock() {
1914
2595
  m.encoded_frames += 1;
1915
2596
  m.encoded_bytes += packet.data.len() as u64;
1916
- 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
+ }
1917
2603
  match pass {
1918
- Pass::Motion { keyframe: true } => m.key_frames += 1,
2604
+ Pass::Motion { half: true, .. } => m.halved_frames += 1,
1919
2605
  Pass::Refine => m.refined_frames += 1,
1920
- Pass::Motion { keyframe: false } | Pass::Keepalive => {}
2606
+ Pass::Motion { half: false, .. } | Pass::Keepalive => {}
1921
2607
  }
1922
2608
  }
1923
2609
  match pass {
@@ -1926,14 +2612,34 @@ fn spawn_pipeline(
1926
2612
  keyframe = false;
1927
2613
  refined = false;
1928
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();
1929
2624
  }
1930
- Pass::Refine => refined = true,
1931
2625
  Pass::Keepalive => {}
1932
2626
  }
1933
2627
  last_sent = Some(now);
1934
- let peer = inner.peer.clone();
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();
1935
2641
  if let Err(error) =
1936
- handle.block_on(peer.send_frame(&packet.data, packet.keyframe, now))
2642
+ handle.block_on(peer.send_frame(&packet.data, packet.keyframe, stamp))
1937
2643
  {
1938
2644
  // The transport is gone. End the session rather than leave
1939
2645
  // the desktop captured behind a picture that stopped.
@@ -1941,18 +2647,155 @@ fn spawn_pipeline(
1941
2647
  let reason = String::from("the connection to the phone was lost");
1942
2648
  inner.notify(SessionEvent::Revoked {
1943
2649
  reason: reason.clone(),
2650
+ code: "transport",
1944
2651
  });
1945
2652
  let target = inner.clone();
1946
2653
  handle.spawn(async move {
1947
- target.close(&reason).await;
2654
+ target.close(&reason, "transport").await;
1948
2655
  });
1949
2656
  break;
1950
2657
  }
2658
+ if let Ok(mut m) = inner.metrics.lock() {
2659
+ m.send_micros += sending.elapsed().as_micros() as u64;
2660
+ }
1951
2661
  }
1952
2662
  })
1953
2663
  .ok();
1954
2664
  }
1955
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
+
2685
+ /// Packetize what a consumer feeds an encoded source, building the peer on the
2686
+ /// first access unit that carries an SPS.
2687
+ ///
2688
+ /// That order is forced by the contract: the offer must name the profile the
2689
+ /// stream actually is, and the only place that is written down is the stream's
2690
+ /// own SPS. Until one arrives there is nothing that could be offered.
2691
+ fn spawn_encoded(inner: &Arc<Inner>, mut feed: tokio_mpsc::Receiver<FedAccessUnit>) {
2692
+ let inner = inner.clone();
2693
+ tokio::spawn(async move {
2694
+ while let Some(unit) = feed.recv().await {
2695
+ if inner.closed.load(Ordering::SeqCst) {
2696
+ return;
2697
+ }
2698
+ if current_peer(&inner).is_none() {
2699
+ let Some(profile) = h264_profile_level_id(&unit.data) else {
2700
+ if let Ok(mut m) = inner.metrics.lock() {
2701
+ m.dropped_frames += 1;
2702
+ }
2703
+ continue;
2704
+ };
2705
+ let (transport, peer_events, codec) = {
2706
+ let encoded = lock(&inner.encoded);
2707
+ let Some(encoded) = encoded.as_ref() else {
2708
+ return;
2709
+ };
2710
+ let codec = match encoded.codec {
2711
+ EncodedCodec::H264 => VideoCodec::H264 {
2712
+ profile_level_id: profile,
2713
+ },
2714
+ };
2715
+ (
2716
+ encoded.transport.clone(),
2717
+ encoded.peer_events.clone(),
2718
+ codec,
2719
+ )
2720
+ };
2721
+ let offered = VideoPeer::offer(transport, peer_events, codec).await;
2722
+ match offered {
2723
+ Ok((peer, offer)) => {
2724
+ let peer = Arc::new(peer);
2725
+ let stored = {
2726
+ let mut held = lock(&inner.peer);
2727
+ if inner.closed.load(Ordering::SeqCst) {
2728
+ false
2729
+ } else {
2730
+ *held = Some(peer.clone());
2731
+ true
2732
+ }
2733
+ };
2734
+ if !stored {
2735
+ peer.close().await;
2736
+ return;
2737
+ }
2738
+ inner.notify(SessionEvent::Description {
2739
+ generation: inner.generation,
2740
+ sdp: offer,
2741
+ });
2742
+ }
2743
+ Err(error) => {
2744
+ revoke(
2745
+ &inner,
2746
+ format!("the transport refused the encoded stream: {error:#}"),
2747
+ "error",
2748
+ )
2749
+ .await;
2750
+ return;
2751
+ }
2752
+ }
2753
+ }
2754
+ let Some(peer) = current_peer(&inner) else {
2755
+ return;
2756
+ };
2757
+ let started = Instant::now();
2758
+ let sent = peer.send_frame(&unit.data, unit.keyframe, started).await;
2759
+ match sent {
2760
+ Ok(()) => {
2761
+ if let Ok(mut m) = inner.metrics.lock() {
2762
+ m.encoded_frames += 1;
2763
+ m.encoded_bytes += unit.data.len() as u64;
2764
+ m.encode_micros += started.elapsed().as_micros() as u64;
2765
+ if unit.keyframe {
2766
+ m.key_frames += 1;
2767
+ }
2768
+ }
2769
+ }
2770
+ Err(error) => {
2771
+ eprintln!("the video track refused a fed access unit: {error:#}");
2772
+ revoke(
2773
+ &inner,
2774
+ String::from("the connection to the phone was lost"),
2775
+ "transport",
2776
+ )
2777
+ .await;
2778
+ return;
2779
+ }
2780
+ }
2781
+ }
2782
+ });
2783
+ }
2784
+
2785
+ fn current_peer(inner: &Arc<Inner>) -> Option<Arc<VideoPeer>> {
2786
+ inner.peer.lock().ok().and_then(|peer| peer.clone())
2787
+ }
2788
+
2789
+ /// End the session with a reason the consumer can show, exactly as the encode
2790
+ /// pipeline does when its transport goes away.
2791
+ async fn revoke(inner: &Arc<Inner>, reason: String, code: &'static str) {
2792
+ inner.notify(SessionEvent::Revoked {
2793
+ reason: reason.clone(),
2794
+ code,
2795
+ });
2796
+ inner.close(&reason, code).await;
2797
+ }
2798
+
1956
2799
  fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiver<PeerEvent>) {
1957
2800
  let inner = inner.clone();
1958
2801
  tokio::spawn(async move {
@@ -1975,6 +2818,16 @@ fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiv
1975
2818
  }
1976
2819
  PeerEvent::State(state) => {
1977
2820
  use webrtc::peer_connection::RTCPeerConnectionState as State;
2821
+ // The far end needs a reference frame to start from, and only
2822
+ // the consumer can make one for a stream it encodes. A
2823
+ // captured desktop manages this in its encode loop; a fed one
2824
+ // has to ask — and everything sent before the transport
2825
+ // connected was dropped, so this is not optional.
2826
+ if lock(&inner.encoded).is_some() && matches!(state, State::Connected) {
2827
+ inner.notify(SessionEvent::KeyframeRequest {
2828
+ generation: inner.generation,
2829
+ });
2830
+ }
1978
2831
  // ICE loss need not close SCTP. Never leave a drag or chord
1979
2832
  // held while waiting for that independent notification.
1980
2833
  if matches!(state, State::Disconnected | State::Failed | State::Closed) {
@@ -1988,8 +2841,9 @@ fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiv
1988
2841
  let reason = String::from("the connection to the phone was lost");
1989
2842
  inner.notify(SessionEvent::Revoked {
1990
2843
  reason: reason.clone(),
2844
+ code: "transport",
1991
2845
  });
1992
- inner.close(&reason).await;
2846
+ inner.close(&reason, "transport").await;
1993
2847
  continue;
1994
2848
  }
1995
2849
  inner.notify(SessionEvent::State {
@@ -2021,6 +2875,11 @@ fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiv
2021
2875
  }
2022
2876
  }
2023
2877
  }
2878
+ PeerEvent::KeyframeRequest => {
2879
+ inner.notify(SessionEvent::KeyframeRequest {
2880
+ generation: inner.generation,
2881
+ });
2882
+ }
2024
2883
  PeerEvent::ControlMessage(text) => {
2025
2884
  match serde_json::from_str::<ControlMessage>(&text) {
2026
2885
  Ok(message) => inner.apply(message),
@@ -2042,10 +2901,11 @@ fn spawn_lease(inner: &Arc<Inner>, ttl: Duration) {
2042
2901
  let reason = String::from("the session lease expired");
2043
2902
  inner.notify(SessionEvent::Revoked {
2044
2903
  reason: reason.clone(),
2904
+ code: "lease",
2045
2905
  });
2046
2906
  // An expiry ends the session exactly as an explicit close does; the
2047
2907
  // notification above is not a substitute for stopping the desktop.
2048
- inner.close(&reason).await;
2908
+ inner.close(&reason, "lease").await;
2049
2909
  });
2050
2910
  }
2051
2911
 
@@ -2082,6 +2942,72 @@ fn opaque_id() -> String {
2082
2942
  mod tests {
2083
2943
  use super::*;
2084
2944
 
2945
+ #[tokio::test]
2946
+ async fn a_closed_encoded_session_ends_the_tasks_its_channels_park() {
2947
+ let (events, _received) = tokio_mpsc::unbounded_channel();
2948
+ let (peer_events, peer_events_rx) = tokio_mpsc::unbounded_channel::<PeerEvent>();
2949
+ let (feed, feed_rx) = tokio_mpsc::channel::<FedAccessUnit>(FEED_BACKLOG);
2950
+ let inner = Arc::new(Inner {
2951
+ id: String::from("encoded-session"),
2952
+ generation: 1,
2953
+ permissions: Vec::new(),
2954
+ source: SelectedSource {
2955
+ node_id: 0,
2956
+ width: 640,
2957
+ height: 480,
2958
+ position: None,
2959
+ source_type: Some(String::from("encoded")),
2960
+ origin_x: 0,
2961
+ origin_y: 0,
2962
+ },
2963
+ geometry: serde_json::json!({
2964
+ "source": { "width": 640, "height": 480 },
2965
+ "encoded": { "width": 640, "height": 480 },
2966
+ "origin": { "x": 0, "y": 0 },
2967
+ }),
2968
+ metrics: Arc::new(Mutex::new(Metrics::default())),
2969
+ latest: Arc::new(Mutex::new(None)),
2970
+ capture_error: Arc::new(Mutex::new(None)),
2971
+ frame_changes: tokio::sync::watch::channel(0).1,
2972
+ peer: Mutex::new(None),
2973
+ encoder: Mutex::new(None),
2974
+ input: Mutex::new(None),
2975
+ indicator: Mutex::new(None),
2976
+ capture: Mutex::new(None),
2977
+ layout: Mutex::new(None),
2978
+ last_seq: Mutex::new(0),
2979
+ control_open: AtomicBool::new(false),
2980
+ closed: AtomicBool::new(false),
2981
+ pipeline: Arc::new(AtomicBool::new(false)),
2982
+ events,
2983
+ encoded: Mutex::new(Some(Encoded {
2984
+ codec: EncodedCodec::H264,
2985
+ transport: TransportOptions {
2986
+ ice_servers: Vec::new(),
2987
+ loopback_tcp: false,
2988
+ pace_bps: 20_000_000.0,
2989
+ },
2990
+ peer_events,
2991
+ feed,
2992
+ })),
2993
+ local_frames: false,
2994
+ });
2995
+ spawn_peer_events(&inner, peer_events_rx);
2996
+ spawn_encoded(&inner, feed_rx);
2997
+ inner.close("test", "closed").await;
2998
+ for _ in 0..100 {
2999
+ if Arc::strong_count(&inner) == 1 {
3000
+ break;
3001
+ }
3002
+ tokio::time::sleep(Duration::from_millis(5)).await;
3003
+ }
3004
+ assert_eq!(
3005
+ Arc::strong_count(&inner),
3006
+ 1,
3007
+ "closing must end the tasks whose channels the close takes away",
3008
+ );
3009
+ }
3010
+
2085
3011
  #[test]
2086
3012
  fn tile_damage_at_origin_does_not_underflow_or_hide_other_tiles() {
2087
3013
  assert_eq!(
@@ -2090,6 +3016,41 @@ mod tests {
2090
3016
  );
2091
3017
  }
2092
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
+
2093
3054
  #[test]
2094
3055
  fn a_small_source_is_never_upscaled() {
2095
3056
  assert_eq!(fit(1000, 600, 1280, 800), (1000, 600));
@@ -2144,9 +3105,19 @@ mod tests {
2144
3105
  pace_bps: 20_000_000.0,
2145
3106
  },
2146
3107
  peer_events,
3108
+ VideoCodec::Vp9,
2147
3109
  )
2148
3110
  .await
2149
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)>>>) {
2150
3121
  // The pipeline codes nothing before the transport connects.
2151
3122
  peer.assume_connected();
2152
3123
  let recorded = Arc::new(Mutex::new(Vec::new()));
@@ -2172,8 +3143,10 @@ mod tests {
2172
3143
  latest: Arc::new(Mutex::new(None)),
2173
3144
  capture_error: Arc::new(Mutex::new(None)),
2174
3145
  frame_changes: tokio::sync::watch::channel(0).1,
2175
- peer: Arc::new(peer),
2176
- encoder: Mutex::new(Encoder::new(64, 64, 1000, 30, 1).expect("an encoder")),
3146
+ peer: Mutex::new(Some(Arc::new(peer))),
3147
+ encoder: Mutex::new(Some(VideoEncoder::Vp9(
3148
+ Encoder::new(width, height, 1000, 30, 1).expect("an encoder"),
3149
+ ))),
2177
3150
  input: Mutex::new(Some(InputTarget {
2178
3151
  applier: Applier::Recording(recorded.clone()),
2179
3152
  held: HeldState::default(),
@@ -2184,12 +3157,14 @@ mod tests {
2184
3157
  })),
2185
3158
  capture: Mutex::new(None),
2186
3159
  indicator: Mutex::new(None),
2187
- layout: Mutex::new(Layout::from_environment().expect("a keymap")),
3160
+ layout: Mutex::new(Some(Layout::from_environment().expect("a keymap"))),
2188
3161
  last_seq: Mutex::new(0),
2189
3162
  control_open: AtomicBool::new(true),
2190
3163
  closed: AtomicBool::new(false),
2191
3164
  pipeline: Arc::new(AtomicBool::new(true)),
2192
3165
  events,
3166
+ encoded: Mutex::new(None),
3167
+ local_frames: true,
2193
3168
  });
2194
3169
  (inner, recorded)
2195
3170
  }
@@ -2230,11 +3205,11 @@ mod tests {
2230
3205
  matches!(
2231
3206
  received.try_recv(),
2232
3207
  Ok(Notice {
2233
- event: SessionEvent::Revoked { .. },
3208
+ event: SessionEvent::Revoked { code: "lease", .. },
2234
3209
  ..
2235
3210
  })
2236
3211
  ),
2237
- "the consumer is still told why the session ended",
3212
+ "the consumer is still told the session's authority ended",
2238
3213
  );
2239
3214
 
2240
3215
  let rejected = inner.metrics.lock().unwrap().input_rejected;
@@ -2474,12 +3449,22 @@ mod tests {
2474
3449
  async fn a_stopped_capture_never_serves_the_last_frame() {
2475
3450
  let (events, _) = tokio_mpsc::unbounded_channel();
2476
3451
  let (mut inner, _) = test_inner(events).await;
2477
- Arc::get_mut(&mut inner).unwrap().permissions.push(Permission::View);
3452
+ Arc::get_mut(&mut inner)
3453
+ .unwrap()
3454
+ .permissions
3455
+ .push(Permission::View);
2478
3456
  let session = Session { inner };
2479
- *lock(&session.inner.latest) = Some((1, Instant::now(), 2, 2, vec![0;16], vec![1], vec![]));
3457
+ *lock(&session.inner.latest) =
3458
+ Some((1, Instant::now(), 2, 2, vec![0; 16], vec![1], vec![]));
2480
3459
  *lock(&session.inner.capture_error) = Some(String::from("SCStreamErrorDomain -3821"));
2481
- assert_eq!(session.frame(None, "", None).unwrap_err().code, "stream-stopped");
2482
- assert_eq!(session.wait_frame(None, None, 10).await.unwrap_err().code, "stream-stopped");
3460
+ assert_eq!(
3461
+ session.frame(None, "", None).unwrap_err().code,
3462
+ "stream-stopped"
3463
+ );
3464
+ assert_eq!(
3465
+ session.wait_frame(None, None, 10).await.unwrap_err().code,
3466
+ "stream-stopped"
3467
+ );
2483
3468
  }
2484
3469
 
2485
3470
  #[tokio::test]
@@ -2487,24 +3472,35 @@ mod tests {
2487
3472
  let (events, mut received) = tokio_mpsc::unbounded_channel();
2488
3473
  let (inner, _recorded) = test_inner(events).await;
2489
3474
  let (frame_tx, frame_rx) = latest_frame();
2490
- spawn_pipeline(&inner, frame_rx, Arc::new(AtomicBool::new(true)), 30, 1000);
3475
+ spawn_pipeline(
3476
+ &inner,
3477
+ current_peer(&inner).expect("the test's peer"),
3478
+ frame_rx,
3479
+ Arc::new(AtomicBool::new(true)),
3480
+ 30,
3481
+ 1000,
3482
+ );
2491
3483
 
2492
3484
  // The encoder is 64x64; a 32x32 frame is the dimension mismatch that
2493
3485
  // used to be counted and dropped behind a permanently black picture.
2494
- frame_tx.put(I420 {
2495
- width: 32,
2496
- height: 32,
2497
- data: vec![128u8; 32 * 32 + 2 * 16 * 16],
2498
- });
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
+ );
2499
3494
  tokio::time::sleep(Duration::from_millis(150)).await;
2500
3495
 
2501
3496
  let mut reason = None;
2502
3497
  while let Ok(event) = received.try_recv() {
2503
3498
  if let Notice {
2504
- event: SessionEvent::Revoked { reason: why },
3499
+ event: SessionEvent::Revoked { reason: why, code },
2505
3500
  ..
2506
3501
  } = event
2507
3502
  {
3503
+ assert_eq!(code, "error", "a refused frame is a fault, not a lost path");
2508
3504
  reason = Some(why);
2509
3505
  }
2510
3506
  }
@@ -2519,6 +3515,184 @@ mod tests {
2519
3515
  );
2520
3516
  }
2521
3517
 
3518
+ /// A consumer-fed stream: a keyframe carrying SPS, PPS and an IDR slice.
3519
+ fn fed_keyframe() -> Vec<u8> {
3520
+ let mut stream = vec![0x00, 0x00, 0x00, 0x01];
3521
+ stream.extend_from_slice(&[0x67, 0x42, 0xc0, 0x29, 0x8c, 0x8d]);
3522
+ stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x68, 0xce, 0x3c, 0x80]);
3523
+ stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x00]);
3524
+ stream
3525
+ }
3526
+
3527
+ fn encoded_open() -> OpenRequest {
3528
+ OpenRequest {
3529
+ source: Some(SourceRequest::Encoded {
3530
+ codec: EncodedCodec::H264,
3531
+ width: 486,
3532
+ height: 1080,
3533
+ }),
3534
+ permissions: vec![Permission::View, Permission::Control],
3535
+ max_width: 3840,
3536
+ max_height: 2160,
3537
+ bitrate_kbps: 0,
3538
+ max_fps: 30,
3539
+ ice_servers: Vec::new(),
3540
+ restore_token: None,
3541
+ ttl: Some(Duration::from_secs(30)),
3542
+ loopback_tcp: false,
3543
+ agent_indicator: false,
3544
+ local_frames: true,
3545
+ }
3546
+ }
3547
+
3548
+ async fn next_notice(received: &mut tokio_mpsc::UnboundedReceiver<Notice>) -> Notice {
3549
+ tokio::time::timeout(Duration::from_secs(5), received.recv())
3550
+ .await
3551
+ .expect("a notification arrives")
3552
+ .expect("the channel stays open")
3553
+ }
3554
+
3555
+ #[tokio::test]
3556
+ async fn an_encoded_session_opens_without_capture_encoder_or_input() {
3557
+ let (events, _received) = tokio_mpsc::unbounded_channel();
3558
+ let session = Session::open(encoded_open(), events)
3559
+ .await
3560
+ .expect("an encoded source needs no display and no input device");
3561
+ let inner = &session.inner;
3562
+ assert_eq!(session.restore_source(), "encoded");
3563
+ assert_eq!(session.geometry()["encoded"]["width"], 486);
3564
+ assert_eq!(session.geometry()["encoded"]["height"], 1080);
3565
+ assert!(
3566
+ inner.encoder.lock().unwrap().is_none(),
3567
+ "the consumer is the encoder for this source"
3568
+ );
3569
+ assert!(
3570
+ inner.capture.lock().unwrap().is_none(),
3571
+ "there is nothing to capture"
3572
+ );
3573
+ assert!(
3574
+ inner.input.lock().unwrap().is_none(),
3575
+ "the client's input belongs to the consumer's device"
3576
+ );
3577
+ assert!(
3578
+ inner.peer.lock().unwrap().is_none(),
3579
+ "the offer waits for the stream's own SPS"
3580
+ );
3581
+ }
3582
+
3583
+ #[tokio::test]
3584
+ async fn the_offer_for_a_fed_stream_names_that_streams_own_profile() {
3585
+ let (events, mut received) = tokio_mpsc::unbounded_channel();
3586
+ let session = Session::open(encoded_open(), events)
3587
+ .await
3588
+ .expect("an encoded session");
3589
+ let first = next_notice(&mut received).await;
3590
+ assert!(
3591
+ matches!(
3592
+ first.event,
3593
+ SessionEvent::State {
3594
+ capture: "consented",
3595
+ ..
3596
+ }
3597
+ ),
3598
+ "the session is open before any offer exists: {:?}",
3599
+ first.event,
3600
+ );
3601
+
3602
+ session
3603
+ .feed(true, fed_keyframe())
3604
+ .expect("a keyframe is taken");
3605
+ let offered = loop {
3606
+ let notice = next_notice(&mut received).await;
3607
+ if let SessionEvent::Description { sdp, .. } = notice.event {
3608
+ break sdp;
3609
+ }
3610
+ };
3611
+ assert!(
3612
+ offered.contains("H264/90000"),
3613
+ "the fed track is offered as H.264: {offered}"
3614
+ );
3615
+ assert!(
3616
+ offered.contains("profile-level-id=42c029"),
3617
+ "the profile-level-id comes from the fed SPS, not a default: {offered}"
3618
+ );
3619
+ assert_eq!(session.metrics().fed_frames, 1);
3620
+ }
3621
+
3622
+ #[tokio::test]
3623
+ async fn a_fed_stream_forwards_its_controls_instead_of_applying_them() {
3624
+ let (events, mut received) = tokio_mpsc::unbounded_channel();
3625
+ let session = Session::open(encoded_open(), events)
3626
+ .await
3627
+ .expect("an encoded session");
3628
+ session.inner.control_open.store(true, Ordering::SeqCst);
3629
+
3630
+ session.inner.apply(ControlMessage::Pointer {
3631
+ phase: PointerPhase::Down,
3632
+ x: 10,
3633
+ y: 20,
3634
+ button: 1,
3635
+ seq: 1,
3636
+ });
3637
+
3638
+ let forwarded = loop {
3639
+ let notice = next_notice(&mut received).await;
3640
+ if let SessionEvent::Input { input } = notice.event {
3641
+ break input;
3642
+ }
3643
+ };
3644
+ assert_eq!(forwarded["kind"], "pointer");
3645
+ assert_eq!(forwarded["phase"], "down");
3646
+ assert_eq!(forwarded["x"], 10);
3647
+ assert_eq!(forwarded["seq"], 1);
3648
+ assert_eq!(session.metrics().input_forwarded, 1);
3649
+ assert_eq!(
3650
+ session.metrics().input_applied,
3651
+ 0,
3652
+ "nothing was applied to this machine"
3653
+ );
3654
+
3655
+ // The stream's own coordinates are what travels, so an encoded surface
3656
+ // cannot be driven by a coordinate the far end never rendered.
3657
+ session.inner.apply(ControlMessage::Pointer {
3658
+ phase: PointerPhase::Move,
3659
+ x: 9_000,
3660
+ y: 20,
3661
+ button: 1,
3662
+ seq: 2,
3663
+ });
3664
+ assert_eq!(session.metrics().input_forwarded, 2);
3665
+ }
3666
+
3667
+ #[tokio::test]
3668
+ async fn a_capture_session_refuses_a_fed_stream() {
3669
+ let (events, _received) = tokio_mpsc::unbounded_channel();
3670
+ let (inner, _recorded) = test_inner(events).await;
3671
+ let session = Session { inner };
3672
+ assert_eq!(
3673
+ session.feed(true, fed_keyframe()).unwrap_err().code,
3674
+ "operation",
3675
+ "only an encoded source takes access units",
3676
+ );
3677
+ }
3678
+
3679
+ #[tokio::test]
3680
+ async fn an_encoded_session_has_no_frame_to_read() {
3681
+ let (events, _received) = tokio_mpsc::unbounded_channel();
3682
+ let session = Session::open(encoded_open(), events)
3683
+ .await
3684
+ .expect("an encoded session");
3685
+ assert_eq!(session.frame(None, "", None).unwrap_err().code, "operation");
3686
+ assert_eq!(
3687
+ session.wait_frame(None, None, 10).await.unwrap_err().code,
3688
+ "operation"
3689
+ );
3690
+ assert_eq!(
3691
+ session.read_clipboard().await.unwrap_err(),
3692
+ "clipboard is unavailable for this desktop source",
3693
+ );
3694
+ }
3695
+
2522
3696
  #[tokio::test]
2523
3697
  async fn opening_without_view_permission_is_refused_before_capture() {
2524
3698
  let (events, _received) = tokio_mpsc::unbounded_channel();
@@ -2535,6 +3709,7 @@ mod tests {
2535
3709
  ttl: None,
2536
3710
  loopback_tcp: false,
2537
3711
  agent_indicator: false,
3712
+ local_frames: true,
2538
3713
  },
2539
3714
  events,
2540
3715
  )
@@ -2576,8 +3751,17 @@ mod tests {
2576
3751
  );
2577
3752
 
2578
3753
  peer_tx.send(PeerEvent::State(State::Failed)).unwrap();
3754
+ let mut transport_lost = false;
2579
3755
  tokio::time::timeout(Duration::from_secs(2), async {
2580
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
+ );
2581
3765
  if matches!(
2582
3766
  notice.event,
2583
3767
  SessionEvent::State {
@@ -2592,25 +3776,182 @@ mod tests {
2592
3776
  })
2593
3777
  .await
2594
3778
  .unwrap();
3779
+ assert!(
3780
+ transport_lost,
3781
+ "a failed transport must revoke with code transport"
3782
+ );
2595
3783
  assert!(inner.revoked_reason().is_some());
2596
3784
  assert!(!inner.pipeline.load(Ordering::SeqCst));
2597
3785
  assert!(inner.input.lock().unwrap().is_none());
2598
3786
  }
2599
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
+
2600
3931
  #[tokio::test]
2601
3932
  async fn the_pipeline_caps_the_rate_codes_the_last_frame_and_refines_a_still_desktop_once() {
2602
3933
  let (events, _events_rx) = tokio_mpsc::unbounded_channel();
2603
3934
  let (inner, _recorded) = test_inner(events).await;
2604
3935
  let (frame_tx, frame_rx) = latest_frame();
2605
- spawn_pipeline(&inner, frame_rx, Arc::new(AtomicBool::new(true)), 20, 1000);
3936
+ spawn_pipeline(
3937
+ &inner,
3938
+ current_peer(&inner).expect("the test's peer"),
3939
+ frame_rx,
3940
+ Arc::new(AtomicBool::new(true)),
3941
+ 20,
3942
+ 1000,
3943
+ );
2606
3944
 
2607
3945
  // 40 frames over 200 ms is 200 fps; the requested 20 fps caps what leaves.
2608
3946
  for _ in 0..40 {
2609
- frame_tx.put(I420 {
2610
- width: 64,
2611
- height: 64,
2612
- data: vec![128u8; 64 * 64 + 2 * 32 * 32],
2613
- });
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
+ );
2614
3955
  tokio::time::sleep(Duration::from_millis(5)).await;
2615
3956
  }
2616
3957
  let motion = inner.metrics.lock().unwrap().encoded_frames;
@@ -2649,5 +3990,74 @@ mod tests {
2649
3990
  "a still stream still sends"
2650
3991
  );
2651
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
+ );
2652
4062
  }
2653
4063
  }