@desklink/host 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/NOTICE +23 -0
  2. package/README.md +78 -48
  3. package/bin/desklink-host.mjs +1 -1
  4. package/dist/bridge.d.ts +1 -0
  5. package/dist/bridge.d.ts.map +1 -1
  6. package/dist/bridge.js +12 -4
  7. package/dist/bridge.js.map +1 -1
  8. package/dist/engineProcess.d.ts.map +1 -1
  9. package/dist/engineProcess.js +1 -0
  10. package/dist/engineProcess.js.map +1 -1
  11. package/dist/protocol.d.ts +26 -0
  12. package/dist/protocol.d.ts.map +1 -1
  13. package/dist/protocol.js.map +1 -1
  14. package/dist/resolveEngine.d.ts +1 -1
  15. package/dist/resolveEngine.d.ts.map +1 -1
  16. package/dist/resolveEngine.js +22 -9
  17. package/dist/resolveEngine.js.map +1 -1
  18. package/docs/BOUNDARY.md +15 -7
  19. package/docs/PROTOCOL.md +150 -23
  20. package/engine/Cargo.lock +46 -1
  21. package/engine/Cargo.toml +12 -2
  22. package/engine/build.rs +28 -2
  23. package/engine/native/nvenc_shim.c +453 -0
  24. package/engine/native/vaapi_shim.c +727 -0
  25. package/engine/native/vpx_shim.c +54 -4
  26. package/engine/native/vt_shim.c +448 -0
  27. package/engine/src/capture.rs +75 -55
  28. package/engine/src/capture_mac.rs +24 -14
  29. package/engine/src/convert.rs +286 -61
  30. package/engine/src/encoder.rs +122 -13
  31. package/engine/src/h264.rs +188 -0
  32. package/engine/src/h264_nvenc.rs +308 -0
  33. package/engine/src/h264_openh264.rs +735 -0
  34. package/engine/src/h264_vaapi.rs +263 -0
  35. package/engine/src/h264_vt.rs +368 -0
  36. package/engine/src/mac.rs +6 -3
  37. package/engine/src/main.rs +22 -4
  38. package/engine/src/peer.rs +466 -28
  39. package/engine/src/protocol.rs +15 -2
  40. package/engine/src/session.rs +790 -93
  41. package/engine/src/x11.rs +210 -9
  42. package/engine/src/x11_mac.rs +12 -0
  43. package/engine/vendor/libva/COPYING +19 -0
  44. package/engine/vendor/libva/va/va.h +5450 -0
  45. package/engine/vendor/libva/va/va_dec_av1.h +695 -0
  46. package/engine/vendor/libva/va/va_dec_hevc.h +616 -0
  47. package/engine/vendor/libva/va/va_dec_jpeg.h +157 -0
  48. package/engine/vendor/libva/va/va_dec_vp8.h +249 -0
  49. package/engine/vendor/libva/va/va_dec_vp9.h +312 -0
  50. package/engine/vendor/libva/va/va_dec_vvc.h +688 -0
  51. package/engine/vendor/libva/va/va_drm.h +61 -0
  52. package/engine/vendor/libva/va/va_enc_av1.h +1010 -0
  53. package/engine/vendor/libva/va/va_enc_h264.h +659 -0
  54. package/engine/vendor/libva/va/va_enc_hevc.h +958 -0
  55. package/engine/vendor/libva/va/va_enc_jpeg.h +162 -0
  56. package/engine/vendor/libva/va/va_enc_mpeg2.h +308 -0
  57. package/engine/vendor/libva/va/va_enc_vp8.h +350 -0
  58. package/engine/vendor/libva/va/va_enc_vp9.h +639 -0
  59. package/engine/vendor/libva/va/va_fei.h +170 -0
  60. package/engine/vendor/libva/va/va_fei_h264.h +505 -0
  61. package/engine/vendor/libva/va/va_fei_hevc.h +254 -0
  62. package/engine/vendor/libva/va/va_prot.h +525 -0
  63. package/engine/vendor/libva/va/va_version.h +87 -0
  64. package/engine/vendor/libva/va/va_vpp.h +1590 -0
  65. package/engine/vendor/nv-codec-headers/LICENSE +24 -0
  66. package/engine/vendor/nv-codec-headers/README +7 -0
  67. package/engine/vendor/nv-codec-headers/include/ffnvcodec/nvEncodeAPI.h +3825 -0
  68. package/examples/reference-client.html +1 -1
  69. package/package.json +3 -2
@@ -1,4 +1,4 @@
1
- //! The WebRTC handoff: one VP9 video track out, one control data channel in.
1
+ //! The WebRTC handoff: one video track out, one control data channel in.
2
2
  //!
3
3
  //! The engine is the offerer. The consumer's own authenticated channel carries
4
4
  //! the SDP and ICE candidates to the client and brings the answer back; this
@@ -12,7 +12,8 @@
12
12
 
13
13
  use anyhow::{Context, Result};
14
14
  use rtc::interceptor::{
15
- Attribute, Interceptor, PacerBuilder, Packet, Registry, Slot, StreamInfo, TaggedPacket,
15
+ Attribute, BandwidthEstimator, CongestionControlBuilder, Interceptor, PacerBuilder, Packet,
16
+ PacketReport, Registry, Slot, StreamInfo, TaggedPacket, TwccSenderBuilder,
16
17
  };
17
18
  use rtc::media_stream::MediaStreamTrack;
18
19
  use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors;
@@ -26,9 +27,11 @@ use rtc::peer_connection::transport::{RTCIceCandidateInit, RTCIceServer};
26
27
  use rtc::rtcp::payload_feedbacks::full_intra_request::FullIntraRequest;
27
28
  use rtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication;
28
29
  use rtc::rtcp::receiver_report::ReceiverReport;
30
+ use rtc::rtp::extension::playout_delay_extension::PlayoutDelayExtension;
31
+ use rtc::rtp::extension::HeaderExtension;
29
32
  use rtc::rtp_transceiver::rtp_sender::{
30
33
  RTCPFeedback, RTCRtpCodec, RTCRtpCodecParameters, RTCRtpCodingParameters,
31
- RTCRtpEncodingParameters, RtpCodecKind,
34
+ RTCRtpEncodingParameters, RTCRtpHeaderExtensionCapability, RtpCodecKind,
32
35
  };
33
36
  use rtc::rtp_transceiver::PayloadType;
34
37
  use rtc::sansio::Protocol;
@@ -62,6 +65,10 @@ pub const H264_PAYLOAD_TYPE: PayloadType = 102;
62
65
  pub enum VideoCodec {
63
66
  /// VP9 in flexible mode, encoded by this engine from captured frames.
64
67
  Vp9,
68
+ /// Captured frames again, offered as VP9 first and H.264 second: the
69
+ /// receiver's answer picks, so one that states no preference keeps VP9,
70
+ /// and one whose platform decodes only H.264 in hardware can ask for it.
71
+ Vp9OrH264,
65
72
  /// H.264 the consumer encoded and feeds. The profile-level-id is read from
66
73
  /// the fed stream's own SPS, so a receiver's decoder is configured for the
67
74
  /// profile actually arriving rather than for one the engine guessed.
@@ -84,8 +91,8 @@ fn vp9_codec() -> RTCRtpCodecParameters {
84
91
 
85
92
  /// H.264 with `packetization-mode=1`, which is what the device helpers emit and
86
93
  /// what every WebRTC receiver implements. Key frame requests are negotiated so
87
- /// the receiver has a defined way to ask; the engine turns one into a
88
- /// `session.keyframeRequest` for the consumer that owns the encoder.
94
+ /// the receiver has a defined way to ask; for a fed stream the engine turns one
95
+ /// into a `session.keyframeRequest` for the consumer that owns the encoder.
89
96
  fn h264_codec(profile_level_id: &str) -> RTCRtpCodecParameters {
90
97
  RTCRtpCodecParameters {
91
98
  rtp_codec: RTCRtpCodec {
@@ -126,6 +133,38 @@ pub fn h264_profile_level_id(annex_b: &[u8]) -> Option<String> {
126
133
  None
127
134
  }
128
135
 
136
+ /// Whether an answer puts H.264 ahead of VP9 on its video section: the
137
+ /// receiver's preference, which the codec it is sent follows.
138
+ fn answer_prefers_h264(sdp: &str) -> bool {
139
+ let Some(video) = sdp
140
+ .split("\nm=")
141
+ .find(|section| section.trim_start_matches("m=").starts_with("video "))
142
+ else {
143
+ return false;
144
+ };
145
+ let mut lines = video.lines();
146
+ let formats: Vec<&str> = lines
147
+ .next()
148
+ .map(|line| line.split_whitespace().skip(3).collect())
149
+ .unwrap_or_default();
150
+ let names: Vec<(&str, &str)> = lines
151
+ .filter_map(|line| line.trim_end().strip_prefix("a=rtpmap:"))
152
+ .filter_map(|map| map.split_once(' '))
153
+ .collect();
154
+ formats
155
+ .iter()
156
+ .find_map(|format| {
157
+ let (_, name) = names.iter().find(|(pt, _)| pt == format)?;
158
+ let name = name.split('/').next()?.to_ascii_uppercase();
159
+ match name.as_str() {
160
+ "H264" => Some(true),
161
+ "VP9" => Some(false),
162
+ _ => None,
163
+ }
164
+ })
165
+ .unwrap_or(false)
166
+ }
167
+
129
168
  /// NAL type 7: sequence parameter set.
130
169
  const SPS_NAL_TYPE: u8 = 7;
131
170
 
@@ -207,6 +246,118 @@ pub struct TransportOptions {
207
246
  /// a loop that never shows a sharp picture.
208
247
  const PACE_BURST_BITS: f64 = 256_000.0;
209
248
 
249
+ /// The receiver's playout delay, stated on every video packet: none. A
250
+ /// remote desktop answers the hand that is driving it, so a frame should be
251
+ /// shown when it is decoded rather than held in a jitter buffer to smooth
252
+ /// playback; a browser otherwise adds about 10 ms of buffering and paces
253
+ /// presentation to its own cadence, costing both latency and frames.
254
+ const PLAYOUT_DELAY_URI: &str = "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay";
255
+ const NO_PLAYOUT_DELAY: HeaderExtension = HeaderExtension::PlayoutDelay(PlayoutDelayExtension {
256
+ min_delay: 0,
257
+ max_delay: 0,
258
+ });
259
+ /// What the far end's transport-wide feedback says about the path.
260
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
261
+ pub struct PathReport {
262
+ /// The queueing delay no packet escaped over the last `STANDING_WINDOW`:
263
+ /// the lowest one-way delay in it above the lowest the path has shown. A
264
+ /// burst that one frame queues up lifts some packets, not all of them; a
265
+ /// link carrying more than it can drain lifts every one.
266
+ pub queue_delay: Duration,
267
+ /// What arrived over the last `DELIVERED_WINDOW`, in kbps.
268
+ pub delivered_kbps: u32,
269
+ }
270
+
271
+ /// How long the lowest one-way delay is remembered: a path's base delay is the
272
+ /// lowest of the last one to two of these, so a queue that stands for a while
273
+ /// is still measured against the empty path, and a route change is learnt.
274
+ const FLOOR_WINDOW: Duration = Duration::from_secs(15);
275
+ const STANDING_WINDOW: Duration = Duration::from_millis(200);
276
+ const DELIVERED_WINDOW: Duration = Duration::from_millis(500);
277
+
278
+ /// Reads the queueing delay out of transport-wide congestion feedback (every
279
+ /// packet's departure here against its arrival there) for the session's rate
280
+ /// control to act on. The rate decision stays in the session, beside the
281
+ /// encoder it drives; this only measures. Its own "target" is the constant
282
+ /// pacing rate, so the pacer is never retargeted from here.
283
+ struct QueueDelay {
284
+ report: Arc<Mutex<Option<PathReport>>>,
285
+ pace_bps: f64,
286
+ epoch: Option<Instant>,
287
+ /// The lowest one-way delay in the previous and the current window, in µs
288
+ /// on an arbitrary offset (the two clocks are not synchronized).
289
+ floor: [i64; 2],
290
+ floor_since: Option<Instant>,
291
+ /// Recent arrivals: arrival and one-way delay in µs, size in bytes.
292
+ recent: VecDeque<(i64, i64, usize)>,
293
+ }
294
+
295
+ impl QueueDelay {
296
+ fn new(report: Arc<Mutex<Option<PathReport>>>, pace_bps: f64) -> Self {
297
+ Self {
298
+ report,
299
+ pace_bps,
300
+ epoch: None,
301
+ floor: [i64::MAX; 2],
302
+ floor_since: None,
303
+ recent: VecDeque::new(),
304
+ }
305
+ }
306
+ }
307
+
308
+ impl BandwidthEstimator for QueueDelay {
309
+ fn on_reports(&mut self, now: Instant, reports: &[PacketReport]) {
310
+ let Some(first) = reports.first() else {
311
+ return;
312
+ };
313
+ let epoch = *self.epoch.get_or_insert(first.departure);
314
+ let before = self.recent.len();
315
+ for report in reports {
316
+ let Some(arrival) = report.arrival.filter(|_| report.arrived) else {
317
+ continue;
318
+ };
319
+ let arrival = arrival.as_micros() as i64;
320
+ let delay = arrival
321
+ - report
322
+ .departure
323
+ .saturating_duration_since(epoch)
324
+ .as_micros() as i64;
325
+ self.floor[1] = self.floor[1].min(delay);
326
+ self.recent.push_back((arrival, delay, report.size));
327
+ }
328
+ let since = *self.floor_since.get_or_insert(now);
329
+ if now.saturating_duration_since(since) >= FLOOR_WINDOW {
330
+ self.floor = [self.floor[1], i64::MAX];
331
+ self.floor_since = Some(now);
332
+ }
333
+ if self.recent.len() == before {
334
+ return;
335
+ }
336
+ let latest = self.recent.iter().map(|(at, _, _)| *at).max().unwrap_or(0);
337
+ let delivered_since = latest - DELIVERED_WINDOW.as_micros() as i64;
338
+ self.recent.retain(|(at, _, _)| *at >= delivered_since);
339
+ let standing_since = latest - STANDING_WINDOW.as_micros() as i64;
340
+ let standing = self
341
+ .recent
342
+ .iter()
343
+ .filter(|(at, _, _)| *at >= standing_since)
344
+ .map(|(_, delay, _)| *delay)
345
+ .min()
346
+ .unwrap_or(0);
347
+ let base = self.floor[0].min(self.floor[1]);
348
+ let bytes: usize = self.recent.iter().map(|(_, _, size)| size).sum();
349
+ *lock(&self.report) = Some(PathReport {
350
+ queue_delay: Duration::from_micros(standing.saturating_sub(base).max(0) as u64),
351
+ delivered_kbps: (bytes as u64 * 8 * 1000 / DELIVERED_WINDOW.as_micros() as u64)
352
+ .min(u32::MAX as u64) as u32,
353
+ });
354
+ }
355
+
356
+ fn target_bitrate(&self) -> f64 {
357
+ self.pace_bps
358
+ }
359
+ }
360
+
210
361
  struct Handler {
211
362
  events: tokio::sync::mpsc::UnboundedSender<PeerEvent>,
212
363
  wants_keyframe: Arc<AtomicBool>,
@@ -597,13 +748,25 @@ pub struct VideoPeer {
597
748
  wants_keyframe: Arc<AtomicBool>,
598
749
  /// True while the transport is connected: frames sent before it are lost.
599
750
  connected: Arc<AtomicBool>,
751
+ /// True when the answer accepted the playout-delay extension. A packet
752
+ /// carrying an extension the answer declined is dropped whole by the
753
+ /// sender, so a receiver that declines it would get no picture at all.
754
+ playout_delay: AtomicBool,
600
755
  track: Arc<TrackLocalStaticRTP>,
601
756
  packetizer: Mutex<Packetizer>,
602
757
  ssrc: u32,
603
758
  control: Arc<dyn DataChannel>,
604
- payload_type: PayloadType,
759
+ /// The payload type frames go out on; H.264's once an answer to a
760
+ /// `Vp9OrH264` offer prefers it.
761
+ payload_type: std::sync::atomic::AtomicU8,
762
+ /// Offered H.264 beside VP9, for the answer to pick.
763
+ offers_h264: bool,
764
+ /// The answer picked H.264: the encoder has to follow.
765
+ sends_h264: AtomicBool,
605
766
  /// Loss fractions (RFC 3550, /256) the far end reported, oldest first.
606
767
  loss: Arc<Mutex<Vec<u8>>>,
768
+ /// The latest queueing delay transport-wide feedback showed, if any.
769
+ path: Arc<Mutex<Option<PathReport>>>,
607
770
  feedback: tokio::task::JoinHandle<()>,
608
771
  /// The engine is the offerer, so a candidate can arrive before the answer
609
772
  /// that supplies its remote description; those are held here until it does.
@@ -627,13 +790,29 @@ impl VideoPeer {
627
790
  let runtime = default_runtime().context("no WebRTC runtime is enabled")?;
628
791
 
629
792
  let mut media_engine = MediaEngine::default();
630
- let video_codec = match &codec {
631
- VideoCodec::Vp9 => vp9_codec(),
632
- VideoCodec::H264 { profile_level_id } => h264_codec(profile_level_id),
793
+ // In preference order: the offer lists them so, and a receiver that
794
+ // states no preference of its own answers with the first.
795
+ let video_codecs = match &codec {
796
+ VideoCodec::Vp9 => vec![vp9_codec()],
797
+ VideoCodec::Vp9OrH264 => vec![vp9_codec(), h264_codec(crate::h264::PROFILE_LEVEL_ID)],
798
+ VideoCodec::H264 { profile_level_id } => vec![h264_codec(profile_level_id)],
633
799
  };
800
+ let video_codec = video_codecs[0].clone();
801
+ for offered in &video_codecs {
802
+ media_engine
803
+ .register_codec(offered.clone(), RtpCodecKind::Video)
804
+ .with_context(|| format!("failed to offer {codec:?}"))?;
805
+ }
634
806
  media_engine
635
- .register_codec(video_codec.clone(), RtpCodecKind::Video)
636
- .with_context(|| format!("failed to offer {codec:?}"))?;
807
+ .register_header_extension(
808
+ RTCRtpHeaderExtensionCapability {
809
+ uri: String::from(PLAYOUT_DELAY_URI),
810
+ },
811
+ RtpCodecKind::Video,
812
+ None,
813
+ )
814
+ .context("failed to offer the playout-delay extension")?;
815
+ let path = Arc::new(Mutex::new(None));
637
816
  let registry = register_default_interceptors(Registry::new(), &mut media_engine)?
638
817
  .with(
639
818
  Slot::Pacer,
@@ -642,6 +821,14 @@ impl VideoPeer {
642
821
  .with_burst_bits(PACE_BURST_BITS)
643
822
  .build(),
644
823
  )
824
+ // Number every packet transport-wide, so the receiver reports when
825
+ // each one arrived, and read those reports for the queueing delay.
826
+ .with(Slot::TwccSender, TwccSenderBuilder::new().build())
827
+ .with(
828
+ Slot::CongestionControl,
829
+ CongestionControlBuilder::new(QueueDelay::new(path.clone(), options.pace_bps))
830
+ .build(),
831
+ )
645
832
  // Last, so every interceptor has seen the whole of the inbound RTCP.
646
833
  .with(Slot::from(14_000), FeedbackForwarder::default());
647
834
 
@@ -697,14 +884,19 @@ impl VideoPeer {
697
884
  String::from("desktop"),
698
885
  String::from("desktop"),
699
886
  RtpCodecKind::Video,
700
- vec![RTCRtpEncodingParameters {
701
- rtp_coding_parameters: RTCRtpCodingParameters {
702
- ssrc: Some(ssrc),
887
+ // One encoding per offered codec is how a track states its
888
+ // codec preferences; one stream is sent, on the first.
889
+ video_codecs
890
+ .iter()
891
+ .map(|offered| RTCRtpEncodingParameters {
892
+ rtp_coding_parameters: RTCRtpCodingParameters {
893
+ ssrc: Some(ssrc),
894
+ ..Default::default()
895
+ },
896
+ codec: offered.rtp_codec.clone(),
703
897
  ..Default::default()
704
- },
705
- codec: video_codec.rtp_codec.clone(),
706
- ..Default::default()
707
- }],
898
+ })
899
+ .collect(),
708
900
  )));
709
901
  let sender = peer
710
902
  .add_track(Arc::clone(&track) as Arc<dyn TrackLocal>)
@@ -735,7 +927,7 @@ impl VideoPeer {
735
927
  ));
736
928
 
737
929
  let packetizer = match &codec {
738
- VideoCodec::Vp9 => Packetizer::Vp9(Vp9Packetizer::new()),
930
+ VideoCodec::Vp9 | VideoCodec::Vp9OrH264 => Packetizer::Vp9(Vp9Packetizer::new()),
739
931
  VideoCodec::H264 { .. } => Packetizer::H264(H264Packetizer::new(
740
932
  video_codec
741
933
  .rtp_codec
@@ -752,12 +944,16 @@ impl VideoPeer {
752
944
  peer,
753
945
  wants_keyframe,
754
946
  connected,
947
+ playout_delay: AtomicBool::new(false),
755
948
  track,
756
949
  packetizer: Mutex::new(packetizer),
757
950
  ssrc,
758
951
  control,
759
- payload_type,
952
+ payload_type: std::sync::atomic::AtomicU8::new(payload_type),
953
+ offers_h264: codec == VideoCodec::Vp9OrH264,
954
+ sends_h264: AtomicBool::new(false),
760
955
  loss,
956
+ path,
761
957
  feedback,
762
958
  candidates: Mutex::new(PendingCandidates::default()),
763
959
  },
@@ -767,15 +963,40 @@ impl VideoPeer {
767
963
 
768
964
  pub async fn accept_answer(&self, sdp: String) -> Result<usize> {
769
965
  let answer = RTCSessionDescription::answer(sdp).context("the answer is not valid SDP")?;
966
+ let prefers_h264 = self.offers_h264 && answer_prefers_h264(&answer.sdp);
770
967
  self.peer
771
968
  .set_remote_description(answer)
772
969
  .await
773
970
  .context("the peer refused the answer")?;
774
- let held = {
971
+ // What the sender negotiated is what it checks each packet against.
972
+ let mut playout_delay = false;
973
+ for sender in self.peer.get_senders().await {
974
+ if let Ok(parameters) = sender.get_parameters().await {
975
+ playout_delay |= parameters
976
+ .rtp_parameters
977
+ .header_extensions
978
+ .iter()
979
+ .any(|extension| extension.uri == PLAYOUT_DELAY_URI);
980
+ }
981
+ }
982
+ self.playout_delay.store(playout_delay, Ordering::SeqCst);
983
+ let (first, held) = {
775
984
  let mut state = lock(&self.candidates);
985
+ let first = !state.ready;
776
986
  state.ready = true;
777
- std::mem::take(&mut state.held)
987
+ (first, std::mem::take(&mut state.held))
778
988
  };
989
+ // The first answer picks the codec, before any frame has gone out; an
990
+ // ICE restart's answer comes from the same receiver and keeps it.
991
+ if first && prefers_h264 {
992
+ let payloader = h264_codec(crate::h264::PROFILE_LEVEL_ID)
993
+ .rtp_codec
994
+ .payloader()
995
+ .context("the media engine has no H.264 payloader")?;
996
+ *lock(&self.packetizer) = Packetizer::H264(H264Packetizer::new(payloader));
997
+ self.payload_type.store(H264_PAYLOAD_TYPE, Ordering::SeqCst);
998
+ self.sends_h264.store(true, Ordering::SeqCst);
999
+ }
779
1000
  let mut applied = 0;
780
1001
  for candidate in held {
781
1002
  // Each was acknowledged when it arrived; a bad one must not turn the
@@ -833,6 +1054,11 @@ impl VideoPeer {
833
1054
  .context("the peer refused an ICE candidate")
834
1055
  }
835
1056
 
1057
+ /// Whether the answer asked for H.264 rather than VP9.
1058
+ pub fn sends_h264(&self) -> bool {
1059
+ self.sends_h264.load(Ordering::SeqCst)
1060
+ }
1061
+
836
1062
  /// Whether the transport is connected; a frame sent before it is lost.
837
1063
  pub fn is_connected(&self) -> bool {
838
1064
  self.connected.load(Ordering::SeqCst)
@@ -853,11 +1079,16 @@ impl VideoPeer {
853
1079
  keyframe,
854
1080
  captured,
855
1081
  self.ssrc,
856
- self.payload_type,
1082
+ self.payload_type.load(Ordering::SeqCst),
857
1083
  )?;
1084
+ let extensions: &[HeaderExtension] = if self.playout_delay.load(Ordering::SeqCst) {
1085
+ &[NO_PLAYOUT_DELAY]
1086
+ } else {
1087
+ &[]
1088
+ };
858
1089
  for packet in packets {
859
1090
  self.track
860
- .write_rtp_with_extensions(packet, &[])
1091
+ .write_rtp_with_extensions(packet, extensions)
861
1092
  .await
862
1093
  .context("failed to hand a packet to the track")?;
863
1094
  }
@@ -869,6 +1100,12 @@ impl VideoPeer {
869
1100
  std::mem::take(&mut *lock(&self.loss))
870
1101
  }
871
1102
 
1103
+ /// Take the path report that arrived since the last call, if any; `None`
1104
+ /// also when the receiver sends no transport-wide feedback.
1105
+ pub fn take_path_report(&self) -> Option<PathReport> {
1106
+ lock(&self.path).take()
1107
+ }
1108
+
872
1109
  /// Take the pending "the far end needs a reference frame" request, if any.
873
1110
  pub fn take_keyframe_request(&self) -> bool {
874
1111
  self.wants_keyframe.swap(false, Ordering::SeqCst)
@@ -902,18 +1139,72 @@ fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
902
1139
  }
903
1140
 
904
1141
  #[cfg(test)]
905
- mod tests {
1142
+ pub(crate) mod tests {
906
1143
  use super::*;
907
1144
 
1145
+ #[test]
1146
+ fn a_standing_queue_is_measured_and_one_frames_burst_is_not() {
1147
+ let epoch = Instant::now();
1148
+ let report = Arc::new(Mutex::new(None));
1149
+ let mut estimator = QueueDelay::new(report.clone(), 20_000_000.0);
1150
+ let mut id = 0u64;
1151
+ // One 1200-byte packet every 10 ms, sent at `ms` and `extra` ms late
1152
+ // over a path with 20 ms of base delay.
1153
+ let mut feed = |from: u64, to: u64, extra: &dyn Fn(u64) -> u64| {
1154
+ let reports: Vec<PacketReport> = (from..to)
1155
+ .step_by(10)
1156
+ .map(|ms| {
1157
+ id += 1;
1158
+ PacketReport {
1159
+ ssrc: 1,
1160
+ id,
1161
+ rtp_sequence_number: id as u16,
1162
+ is_twcc: true,
1163
+ twcc_sequence_number: id as u16,
1164
+ size: 1200,
1165
+ arrived: true,
1166
+ departure: epoch + Duration::from_millis(ms),
1167
+ arrival: Some(Duration::from_millis(1_000 + ms + 20 + extra(ms))),
1168
+ ecn: Default::default(),
1169
+ }
1170
+ })
1171
+ .collect();
1172
+ estimator.on_reports(epoch + Duration::from_millis(to + 20), &reports);
1173
+ lock(&report).take().expect("a report")
1174
+ };
1175
+
1176
+ let quiet = feed(0, 1000, &|_| 0);
1177
+ assert_eq!(quiet.queue_delay, Duration::ZERO);
1178
+ // 51 packets of 1200 bytes arrived in the last 500 ms, both ends in.
1179
+ assert_eq!(quiet.delivered_kbps, 979);
1180
+ // A frame's burst lifts the packets at its tail, not the ones before.
1181
+ let burst = feed(1000, 1100, &|ms| if ms >= 1070 { 40 } else { 0 });
1182
+ assert_eq!(burst.queue_delay, Duration::ZERO);
1183
+ // A link that drains slower than it is fed lifts every packet: the
1184
+ // queue is the least any packet of the last 200 ms waited.
1185
+ let standing = feed(1100, 1400, &|ms| 30 + (ms - 1100) / 10);
1186
+ assert_eq!(standing.queue_delay, Duration::from_millis(41));
1187
+ }
1188
+
908
1189
  struct NoopHandler;
909
1190
 
910
1191
  impl PeerConnectionEventHandler for NoopHandler {}
911
1192
 
912
1193
  async fn answerer(offer: &str) -> Arc<dyn PeerConnection> {
1194
+ answerer_with(offer, vec![vp9_codec()]).await
1195
+ }
1196
+
1197
+ /// A receiver that decodes `codecs`, in its order of preference.
1198
+ pub(crate) async fn answerer_with(
1199
+ offer: &str,
1200
+ codecs: Vec<RTCRtpCodecParameters>,
1201
+ ) -> Arc<dyn PeerConnection> {
913
1202
  let mut media_engine = MediaEngine::default();
914
- media_engine
915
- .register_codec(vp9_codec(), RtpCodecKind::Video)
916
- .expect("VP9 registers");
1203
+ for codec in codecs {
1204
+ media_engine
1205
+ .register_codec(codec, RtpCodecKind::Video)
1206
+ .expect("the codec registers");
1207
+ }
917
1208
  let registry = register_default_interceptors(Registry::new(), &mut media_engine)
918
1209
  .expect("interceptors register");
919
1210
  let peer = PeerConnectionBuilder::<std::net::SocketAddr>::new()
@@ -963,6 +1254,114 @@ mod tests {
963
1254
  );
964
1255
  }
965
1256
 
1257
+ fn transport() -> TransportOptions {
1258
+ TransportOptions {
1259
+ ice_servers: Vec::new(),
1260
+ loopback_tcp: false,
1261
+ pace_bps: 20_000_000.0,
1262
+ }
1263
+ }
1264
+
1265
+ /// The answer `codecs` make to a `Vp9OrH264` offer, applied to its peer.
1266
+ pub(crate) async fn answered(codecs: Vec<RTCRtpCodecParameters>) -> (VideoPeer, String) {
1267
+ let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
1268
+ let (peer, offer) = VideoPeer::offer(transport(), events, VideoCodec::Vp9OrH264)
1269
+ .await
1270
+ .expect("a peer connection");
1271
+ let other = answerer_with(&offer, codecs).await;
1272
+ let answer = other.create_answer(None).await.expect("an answer");
1273
+ other
1274
+ .set_local_description(answer.clone())
1275
+ .await
1276
+ .expect("a local description");
1277
+ peer.accept_answer(answer.sdp.clone())
1278
+ .await
1279
+ .expect("the answer is accepted");
1280
+ (peer, answer.sdp)
1281
+ }
1282
+
1283
+ pub(crate) fn h264_receiver_codec() -> RTCRtpCodecParameters {
1284
+ h264_codec(crate::h264::PROFILE_LEVEL_ID)
1285
+ }
1286
+
1287
+ #[tokio::test]
1288
+ async fn a_captured_offer_puts_vp9_first_and_h264_second() {
1289
+ let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
1290
+ let (_peer, offer) = VideoPeer::offer(transport(), events, VideoCodec::Vp9OrH264)
1291
+ .await
1292
+ .expect("a peer connection");
1293
+ let video = offer
1294
+ .lines()
1295
+ .find(|line| line.starts_with("m=video "))
1296
+ .expect("a video section");
1297
+ assert!(
1298
+ video.ends_with(&format!(" {VP9_PAYLOAD_TYPE} {H264_PAYLOAD_TYPE}")),
1299
+ "VP9 is the default a receiver with no preference answers with: {video}",
1300
+ );
1301
+ assert!(
1302
+ offer.contains(&format!(
1303
+ "a=fmtp:{H264_PAYLOAD_TYPE} level-asymmetry-allowed=1;packetization-mode=1;profile-level-id={}",
1304
+ crate::h264::PROFILE_LEVEL_ID
1305
+ )),
1306
+ "H.264 is offered as the Constrained Baseline every backend emits: {offer}",
1307
+ );
1308
+ }
1309
+
1310
+ #[tokio::test]
1311
+ async fn the_receivers_first_codec_is_the_one_sent() {
1312
+ let (peer, answer) = answered(vec![vp9_codec(), h264_receiver_codec()]).await;
1313
+ assert!(
1314
+ !peer.sends_h264(),
1315
+ "a receiver that keeps the offer's order gets VP9: {answer}"
1316
+ );
1317
+ let (peer, answer) = answered(vec![h264_receiver_codec()]).await;
1318
+ assert!(peer.sends_h264(), "an H.264 receiver gets H.264: {answer}");
1319
+ assert_eq!(
1320
+ peer.payload_type.load(Ordering::SeqCst),
1321
+ H264_PAYLOAD_TYPE,
1322
+ "frames go out on the payload type the answer kept",
1323
+ );
1324
+ }
1325
+
1326
+ #[test]
1327
+ fn an_answer_is_read_for_its_video_sections_first_known_codec() {
1328
+ let answer = |formats: &str| {
1329
+ format!(
1330
+ "v=0\r\no=- 1 1 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\n\
1331
+ m=video 9 UDP/TLS/RTP/SAVPF {formats}\r\nc=IN IP4 0.0.0.0\r\n\
1332
+ a=rtpmap:96 rtx/90000\r\na=rtpmap:98 VP9/90000\r\na=rtpmap:102 H264/90000\r\n\
1333
+ m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\na=rtpmap:5 H264/90000\r\n"
1334
+ )
1335
+ };
1336
+ assert!(answer_prefers_h264(&answer("102 98")));
1337
+ assert!(answer_prefers_h264(&answer("96 102 98")), "rtx is skipped");
1338
+ assert!(!answer_prefers_h264(&answer("98 102")));
1339
+ assert!(!answer_prefers_h264(&answer("98")));
1340
+ assert!(!answer_prefers_h264("v=0\r\n"), "no video, no H.264");
1341
+ }
1342
+
1343
+ #[tokio::test]
1344
+ async fn the_offer_asks_the_receiver_to_show_frames_without_playout_delay() {
1345
+ let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
1346
+ let (_peer, offer) = VideoPeer::offer(
1347
+ TransportOptions {
1348
+ ice_servers: Vec::new(),
1349
+ loopback_tcp: false,
1350
+ pace_bps: 20_000_000.0,
1351
+ },
1352
+ events,
1353
+ VideoCodec::Vp9,
1354
+ )
1355
+ .await
1356
+ .expect("a peer connection");
1357
+ assert!(
1358
+ offer
1359
+ .lines()
1360
+ .any(|line| line.starts_with("a=extmap:") && line.ends_with(PLAYOUT_DELAY_URI)),
1361
+ "the video must negotiate the playout-delay extension its packets carry: {offer}",
1362
+ );
1363
+ }
1364
+
966
1365
  #[test]
967
1366
  fn the_profile_level_id_comes_from_the_sps_in_the_stream() {
968
1367
  // A leading zero byte, a four-byte start code and a three-byte one, so
@@ -1051,6 +1450,45 @@ mod tests {
1051
1450
  .expect("a candidate that arrives before the answer is buffered");
1052
1451
  }
1053
1452
 
1453
+ #[tokio::test]
1454
+ async fn packets_ask_for_no_playout_delay_only_when_the_answer_accepts_it() {
1455
+ for accepts in [false, true] {
1456
+ let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
1457
+ let (peer, offer) = VideoPeer::offer(
1458
+ TransportOptions {
1459
+ ice_servers: Vec::new(),
1460
+ loopback_tcp: false,
1461
+ pace_bps: 20_000_000.0,
1462
+ },
1463
+ events,
1464
+ VideoCodec::Vp9,
1465
+ )
1466
+ .await
1467
+ .expect("a peer connection");
1468
+ // This answerer knows no playout-delay extension, so it declines it.
1469
+ let other = answerer(&offer).await;
1470
+ let answer = other.create_answer(None).await.expect("an answer");
1471
+ let sdp = if accepts {
1472
+ answer.sdp.replacen(
1473
+ "a=mid:0\r\n",
1474
+ &format!("a=mid:0\r\na=extmap:1 {PLAYOUT_DELAY_URI}\r\n"),
1475
+ 1,
1476
+ )
1477
+ } else {
1478
+ answer.sdp
1479
+ };
1480
+ assert_eq!(sdp.contains(PLAYOUT_DELAY_URI), accepts);
1481
+ peer.accept_answer(sdp)
1482
+ .await
1483
+ .expect("the answer is accepted");
1484
+ assert_eq!(
1485
+ peer.playout_delay.load(Ordering::SeqCst),
1486
+ accepts,
1487
+ "a packet may carry the extension only when the answer accepted it",
1488
+ );
1489
+ }
1490
+ }
1491
+
1054
1492
  #[tokio::test]
1055
1493
  async fn held_candidates_reach_the_peer_when_the_answer_arrives() {
1056
1494
  let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();