@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
@@ -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,11 +12,14 @@
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;
19
- use rtc::peer_connection::configuration::media_engine::{MediaEngine, MIME_TYPE_VP9};
20
+ use rtc::peer_connection::configuration::media_engine::{
21
+ MediaEngine, MIME_TYPE_H264, MIME_TYPE_VP9,
22
+ };
20
23
  use rtc::peer_connection::configuration::RTCConfigurationBuilder;
21
24
  use rtc::peer_connection::event::RTCPeerConnectionIceEvent;
22
25
  use rtc::peer_connection::sdp::RTCSessionDescription;
@@ -24,9 +27,11 @@ use rtc::peer_connection::transport::{RTCIceCandidateInit, RTCIceServer};
24
27
  use rtc::rtcp::payload_feedbacks::full_intra_request::FullIntraRequest;
25
28
  use rtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication;
26
29
  use rtc::rtcp::receiver_report::ReceiverReport;
30
+ use rtc::rtp::extension::playout_delay_extension::PlayoutDelayExtension;
31
+ use rtc::rtp::extension::HeaderExtension;
27
32
  use rtc::rtp_transceiver::rtp_sender::{
28
- RTCRtpCodec, RTCRtpCodecParameters, RTCRtpCodingParameters, RTCRtpEncodingParameters,
29
- RtpCodecKind,
33
+ RTCPFeedback, RTCRtpCodec, RTCRtpCodecParameters, RTCRtpCodingParameters,
34
+ RTCRtpEncodingParameters, RTCRtpHeaderExtensionCapability, RtpCodecKind,
30
35
  };
31
36
  use rtc::rtp_transceiver::PayloadType;
32
37
  use rtc::sansio::Protocol;
@@ -49,6 +54,27 @@ const VP9_CLOCK_RATE: u32 = 90_000;
49
54
  /// this one is the common convention and keeps packet captures readable.
50
55
  pub const VP9_PAYLOAD_TYPE: PayloadType = 98;
51
56
 
57
+ /// H.264's RTP clock is 90 kHz (RFC 6184), the same as VP9's.
58
+ const H264_CLOCK_RATE: u32 = 90_000;
59
+
60
+ /// Payload type the engine offers H.264 on, again a dynamic-range convention.
61
+ pub const H264_PAYLOAD_TYPE: PayloadType = 102;
62
+
63
+ /// Which video track this peer carries.
64
+ #[derive(Debug, Clone, PartialEq, Eq)]
65
+ pub enum VideoCodec {
66
+ /// VP9 in flexible mode, encoded by this engine from captured frames.
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,
72
+ /// H.264 the consumer encoded and feeds. The profile-level-id is read from
73
+ /// the fed stream's own SPS, so a receiver's decoder is configured for the
74
+ /// profile actually arriving rather than for one the engine guessed.
75
+ H264 { profile_level_id: String },
76
+ }
77
+
52
78
  fn vp9_codec() -> RTCRtpCodecParameters {
53
79
  RTCRtpCodecParameters {
54
80
  rtp_codec: RTCRtpCodec {
@@ -63,6 +89,123 @@ fn vp9_codec() -> RTCRtpCodecParameters {
63
89
  }
64
90
  }
65
91
 
92
+ /// H.264 with `packetization-mode=1`, which is what the device helpers emit and
93
+ /// what every WebRTC receiver implements. Key frame requests are negotiated so
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.
96
+ fn h264_codec(profile_level_id: &str) -> RTCRtpCodecParameters {
97
+ RTCRtpCodecParameters {
98
+ rtp_codec: RTCRtpCodec {
99
+ mime_type: MIME_TYPE_H264.to_owned(),
100
+ clock_rate: H264_CLOCK_RATE,
101
+ channels: 0,
102
+ sdp_fmtp_line: format!(
103
+ "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id={profile_level_id}"
104
+ ),
105
+ rtcp_feedback: [("nack", ""), ("nack", "pli"), ("ccm", "fir")]
106
+ .into_iter()
107
+ .map(|(typ, parameter)| RTCPFeedback {
108
+ typ: typ.to_owned(),
109
+ parameter: parameter.to_owned(),
110
+ })
111
+ .collect(),
112
+ },
113
+ payload_type: H264_PAYLOAD_TYPE,
114
+ ..Default::default()
115
+ }
116
+ }
117
+
118
+ /// The `profile-level-id` an H.264 Annex-B stream declares, as the three bytes
119
+ /// after an SPS NAL header: `profile_idc`, the constraint flags, `level_idc`.
120
+ ///
121
+ /// This is the value the offer must carry, and it can only come from the stream
122
+ /// itself, so a peer for a fed stream is built after the first access unit
123
+ /// rather than at `session.open`.
124
+ pub fn h264_profile_level_id(annex_b: &[u8]) -> Option<String> {
125
+ for nalu in AnnexBNalUnits::new(annex_b) {
126
+ let Some(&first) = nalu.first() else {
127
+ continue;
128
+ };
129
+ if first & 0x1f == SPS_NAL_TYPE && nalu.len() >= 4 {
130
+ return Some(format!("{:02x}{:02x}{:02x}", nalu[1], nalu[2], nalu[3]));
131
+ }
132
+ }
133
+ None
134
+ }
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
+
168
+ /// NAL type 7: sequence parameter set.
169
+ const SPS_NAL_TYPE: u8 = 7;
170
+
171
+ /// A byte stream's NAL units, delimited by the Annex-B start code
172
+ /// (`00 00 01`, optionally preceded by zero bytes).
173
+ struct AnnexBNalUnits<'a> {
174
+ rest: &'a [u8],
175
+ }
176
+
177
+ impl<'a> AnnexBNalUnits<'a> {
178
+ fn new(stream: &'a [u8]) -> Self {
179
+ Self { rest: stream }
180
+ }
181
+
182
+ fn start_code(stream: &[u8]) -> Option<usize> {
183
+ stream
184
+ .windows(3)
185
+ .position(|window| window == [0x00, 0x00, 0x01])
186
+ }
187
+ }
188
+
189
+ impl<'a> Iterator for AnnexBNalUnits<'a> {
190
+ type Item = &'a [u8];
191
+
192
+ fn next(&mut self) -> Option<Self::Item> {
193
+ let start = Self::start_code(self.rest)? + 3;
194
+ let body = &self.rest[start..];
195
+ let end = Self::start_code(body).map_or(body.len(), |next| {
196
+ // A four-byte start code includes the zero the three-byte search
197
+ // left at the end of the previous unit.
198
+ if next > 0 && body[next - 1] == 0x00 {
199
+ next - 1
200
+ } else {
201
+ next
202
+ }
203
+ });
204
+ self.rest = &body[end..];
205
+ Some(&body[..end])
206
+ }
207
+ }
208
+
66
209
  /// What the consumer needs to know about a live peer.
67
210
  #[derive(Debug, Clone)]
68
211
  pub enum PeerEvent {
@@ -80,9 +223,13 @@ pub enum PeerEvent {
80
223
  ControlClosed,
81
224
  /// A control message arrived from the client.
82
225
  ControlMessage(String),
226
+ /// The far end asked for a fresh reference frame (RTCP PLI or FIR) on a
227
+ /// track this engine does not encode itself, so the consumer must supply one.
228
+ KeyframeRequest,
83
229
  }
84
230
 
85
231
  /// Transport settings the consumer chose for this session.
232
+ #[derive(Debug, Clone)]
86
233
  pub struct TransportOptions {
87
234
  pub ice_servers: Vec<(String, Option<String>, Option<String>)>,
88
235
  /// Listen for ICE over TCP on the loopback too. A phone that reaches this
@@ -99,6 +246,118 @@ pub struct TransportOptions {
99
246
  /// a loop that never shows a sharp picture.
100
247
  const PACE_BURST_BITS: f64 = 256_000.0;
101
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
+
102
361
  struct Handler {
103
362
  events: tokio::sync::mpsc::UnboundedSender<PeerEvent>,
104
363
  wants_keyframe: Arc<AtomicBool>,
@@ -116,21 +375,33 @@ async fn serve_control(
116
375
  channel: Arc<dyn DataChannel>,
117
376
  events: tokio::sync::mpsc::UnboundedSender<PeerEvent>,
118
377
  ) {
119
- while let Some(event) = channel.poll().await {
120
- match event {
121
- DataChannelEvent::OnOpen => {
122
- let _ = events.send(PeerEvent::ControlOpen);
123
- }
124
- DataChannelEvent::OnMessage(message) => {
125
- if let Ok(text) = String::from_utf8(message.data.to_vec()) {
126
- let _ = events.send(PeerEvent::ControlMessage(text));
378
+ // A heartbeat the client watches: ICE consent timers take seconds to call
379
+ // a dead path disconnected, while a missing heartbeat reads as stalled
380
+ // within about a second. Cheap on purpose — seventeen bytes twice a second.
381
+ let mut ping = tokio::time::interval(std::time::Duration::from_millis(500));
382
+ loop {
383
+ tokio::select! {
384
+ event = channel.poll() => {
385
+ match event {
386
+ Some(DataChannelEvent::OnOpen) => {
387
+ let _ = events.send(PeerEvent::ControlOpen);
388
+ }
389
+ Some(DataChannelEvent::OnMessage(message)) => {
390
+ if let Ok(text) = String::from_utf8(message.data.to_vec()) {
391
+ let _ = events.send(PeerEvent::ControlMessage(text));
392
+ }
393
+ }
394
+ Some(DataChannelEvent::OnClose) => {
395
+ let _ = events.send(PeerEvent::ControlClosed);
396
+ break;
397
+ }
398
+ Some(_) => {}
399
+ None => break,
127
400
  }
128
401
  }
129
- DataChannelEvent::OnClose => {
130
- let _ = events.send(PeerEvent::ControlClosed);
131
- break;
402
+ _ = ping.tick() => {
403
+ let _ = channel.send_text(r#"{"kind":"ping"}"#).await;
132
404
  }
133
- _ => {}
134
405
  }
135
406
  }
136
407
  let _ = events.send(PeerEvent::ControlClosed);
@@ -231,12 +502,14 @@ impl Interceptor for FeedbackForwarder {
231
502
 
232
503
  /// Read the video sender's feedback until the peer closes it (by aborting this
233
504
  /// task: the track's feedback channel outlives a closed peer): key frame
234
- /// requests set `wants_keyframe`, receiver reports queue their loss fraction.
505
+ /// requests set `wants_keyframe`, receiver reports queue their loss fraction,
506
+ /// and a track whose encoder lives in the consumer is told to ask for one.
235
507
  async fn serve_feedback(
236
508
  track: Arc<TrackLocalStaticRTP>,
237
509
  ssrc: u32,
238
510
  wants_keyframe: Arc<AtomicBool>,
239
511
  loss: Arc<Mutex<Vec<u8>>>,
512
+ keyframe_requests: Option<tokio::sync::mpsc::UnboundedSender<PeerEvent>>,
240
513
  ) {
241
514
  loop {
242
515
  // The track is bound once the answer is applied; until then, and after
@@ -249,6 +522,9 @@ async fn serve_feedback(
249
522
  let packet = packet.as_any();
250
523
  if packet.is::<PictureLossIndication>() || packet.is::<FullIntraRequest>() {
251
524
  wants_keyframe.store(true, Ordering::SeqCst);
525
+ if let Some(requests) = &keyframe_requests {
526
+ let _ = requests.send(PeerEvent::KeyframeRequest);
527
+ }
252
528
  } else if let Some(report) = packet.downcast_ref::<ReceiverReport>() {
253
529
  let mut queue = lock(&loss);
254
530
  queue.extend(
@@ -270,6 +546,102 @@ async fn serve_feedback(
270
546
  const RTP_MTU: usize = 1200;
271
547
  const RTP_HEADER: usize = 12;
272
548
 
549
+ /// One encoded track's packetizer. The engine builds RTP headers itself either
550
+ /// way — the payloaders below only decide how a frame's bytes are split — so the
551
+ /// codec only changes which ones run.
552
+ enum Packetizer {
553
+ Vp9(Vp9Packetizer),
554
+ H264(H264Packetizer),
555
+ }
556
+
557
+ impl Packetizer {
558
+ fn packetize(
559
+ &mut self,
560
+ frame: &[u8],
561
+ keyframe: bool,
562
+ captured: Instant,
563
+ ssrc: u32,
564
+ payload_type: PayloadType,
565
+ ) -> Result<Vec<rtc::rtp::Packet>> {
566
+ match self {
567
+ Self::Vp9(packetizer) => {
568
+ Ok(packetizer.packetize(frame, keyframe, captured, ssrc, payload_type))
569
+ }
570
+ Self::H264(packetizer) => packetizer.packetize(frame, captured, ssrc, payload_type),
571
+ }
572
+ }
573
+ }
574
+
575
+ /// H.264 over RTP (RFC 6184), packetization mode 1: single NAL units, STAP-A for
576
+ /// the parameter sets, FU-A for anything over the MTU. The fragmentation itself
577
+ /// is webrtc-rs's own H.264 payloader (`RTCRtpCodec::payloader`), which already
578
+ /// implements those three payload shapes; only the RTP header is built here, so
579
+ /// the timestamp is the arrival time of the access unit rather than the moment
580
+ /// each fragment happened to be written.
581
+ struct H264Packetizer {
582
+ sequence: u16,
583
+ timestamp_base: u32,
584
+ started: Instant,
585
+ payloader: Box<dyn rtc::rtp::packetizer::Payloader>,
586
+ }
587
+
588
+ impl H264Packetizer {
589
+ fn new(payloader: Box<dyn rtc::rtp::packetizer::Payloader>) -> Self {
590
+ Self {
591
+ sequence: rand::random(),
592
+ timestamp_base: rand::random(),
593
+ started: Instant::now(),
594
+ payloader,
595
+ }
596
+ }
597
+
598
+ fn packetize(
599
+ &mut self,
600
+ access_unit: &[u8],
601
+ arrived: Instant,
602
+ ssrc: u32,
603
+ payload_type: PayloadType,
604
+ ) -> Result<Vec<rtc::rtp::Packet>> {
605
+ let payloads = self
606
+ .payloader
607
+ .payload(
608
+ RTP_MTU - RTP_HEADER,
609
+ &bytes::Bytes::copy_from_slice(access_unit),
610
+ )
611
+ .map_err(|error| {
612
+ anyhow::anyhow!("the H.264 payloader refused an access unit: {error}")
613
+ })?;
614
+ let ticks = (arrived
615
+ .saturating_duration_since(self.started)
616
+ .as_secs_f64()
617
+ * H264_CLOCK_RATE as f64) as u64 as u32;
618
+ let timestamp = self.timestamp_base.wrapping_add(ticks);
619
+ let last = payloads.len().saturating_sub(1);
620
+ Ok(payloads
621
+ .into_iter()
622
+ .enumerate()
623
+ .map(|(index, payload)| {
624
+ let sequence_number = self.sequence;
625
+ self.sequence = self.sequence.wrapping_add(1);
626
+ rtc::rtp::Packet {
627
+ header: rtc::rtp::Header {
628
+ version: 2,
629
+ // The marker ends an access unit, which is what a video
630
+ // receiver uses to hand a complete picture to its decoder.
631
+ marker: index == last,
632
+ payload_type,
633
+ sequence_number,
634
+ timestamp,
635
+ ssrc,
636
+ ..Default::default()
637
+ },
638
+ payload,
639
+ }
640
+ })
641
+ .collect())
642
+ }
643
+ }
644
+
273
645
  /// VP9 over RTP (RFC 9628) in flexible mode, one layer.
274
646
  ///
275
647
  /// The payload descriptor is what a receiver builds its reference graph from,
@@ -376,13 +748,25 @@ pub struct VideoPeer {
376
748
  wants_keyframe: Arc<AtomicBool>,
377
749
  /// True while the transport is connected: frames sent before it are lost.
378
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,
379
755
  track: Arc<TrackLocalStaticRTP>,
380
- packetizer: Mutex<Vp9Packetizer>,
756
+ packetizer: Mutex<Packetizer>,
381
757
  ssrc: u32,
382
758
  control: Arc<dyn DataChannel>,
383
- 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,
384
766
  /// Loss fractions (RFC 3550, /256) the far end reported, oldest first.
385
767
  loss: Arc<Mutex<Vec<u8>>>,
768
+ /// The latest queueing delay transport-wide feedback showed, if any.
769
+ path: Arc<Mutex<Option<PathReport>>>,
386
770
  feedback: tokio::task::JoinHandle<()>,
387
771
  /// The engine is the offerer, so a candidate can arrive before the answer
388
772
  /// that supplies its remote description; those are held here until it does.
@@ -401,14 +785,34 @@ impl VideoPeer {
401
785
  pub async fn offer(
402
786
  options: TransportOptions,
403
787
  events: tokio::sync::mpsc::UnboundedSender<PeerEvent>,
788
+ codec: VideoCodec,
404
789
  ) -> Result<(Self, String)> {
405
790
  let runtime = default_runtime().context("no WebRTC runtime is enabled")?;
406
791
 
407
792
  let mut media_engine = MediaEngine::default();
408
- let video_codec = vp9_codec();
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)],
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
+ }
409
806
  media_engine
410
- .register_codec(video_codec.clone(), RtpCodecKind::Video)
411
- .context("failed to offer VP9")?;
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));
412
816
  let registry = register_default_interceptors(Registry::new(), &mut media_engine)?
413
817
  .with(
414
818
  Slot::Pacer,
@@ -417,6 +821,14 @@ impl VideoPeer {
417
821
  .with_burst_bits(PACE_BURST_BITS)
418
822
  .build(),
419
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
+ )
420
832
  // Last, so every interceptor has seen the whole of the inbound RTCP.
421
833
  .with(Slot::from(14_000), FeedbackForwarder::default());
422
834
 
@@ -437,6 +849,10 @@ impl VideoPeer {
437
849
  let wants_keyframe = Arc::new(AtomicBool::new(true));
438
850
  let connected = Arc::new(AtomicBool::new(false));
439
851
  let control_events = events.clone();
852
+ // A track this engine encodes itself needs no notification: the pipeline
853
+ // reads `wants_keyframe` directly. A fed track's encoder lives in the
854
+ // consumer, so a key frame request has to travel back as an event.
855
+ let keyframe_requests = matches!(codec, VideoCodec::H264 { .. }).then(|| events.clone());
440
856
  let peer = PeerConnectionBuilder::<std::net::SocketAddr>::new()
441
857
  .with_configuration(builder.build())
442
858
  .with_media_engine(media_engine)
@@ -468,14 +884,19 @@ impl VideoPeer {
468
884
  String::from("desktop"),
469
885
  String::from("desktop"),
470
886
  RtpCodecKind::Video,
471
- vec![RTCRtpEncodingParameters {
472
- rtp_coding_parameters: RTCRtpCodingParameters {
473
- 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(),
474
897
  ..Default::default()
475
- },
476
- codec: video_codec.rtp_codec.clone(),
477
- ..Default::default()
478
- }],
898
+ })
899
+ .collect(),
479
900
  )));
480
901
  let sender = peer
481
902
  .add_track(Arc::clone(&track) as Arc<dyn TrackLocal>)
@@ -502,8 +923,19 @@ impl VideoPeer {
502
923
  ssrc,
503
924
  wants_keyframe.clone(),
504
925
  loss.clone(),
926
+ keyframe_requests,
505
927
  ));
506
928
 
929
+ let packetizer = match &codec {
930
+ VideoCodec::Vp9 | VideoCodec::Vp9OrH264 => Packetizer::Vp9(Vp9Packetizer::new()),
931
+ VideoCodec::H264 { .. } => Packetizer::H264(H264Packetizer::new(
932
+ video_codec
933
+ .rtp_codec
934
+ .payloader()
935
+ .context("the media engine has no H.264 payloader")?,
936
+ )),
937
+ };
938
+
507
939
  let offer = peer.create_offer(None).await?;
508
940
  peer.set_local_description(offer.clone()).await?;
509
941
 
@@ -512,12 +944,16 @@ impl VideoPeer {
512
944
  peer,
513
945
  wants_keyframe,
514
946
  connected,
947
+ playout_delay: AtomicBool::new(false),
515
948
  track,
516
- packetizer: Mutex::new(Vp9Packetizer::new()),
949
+ packetizer: Mutex::new(packetizer),
517
950
  ssrc,
518
951
  control,
519
- payload_type,
952
+ payload_type: std::sync::atomic::AtomicU8::new(payload_type),
953
+ offers_h264: codec == VideoCodec::Vp9OrH264,
954
+ sends_h264: AtomicBool::new(false),
520
955
  loss,
956
+ path,
521
957
  feedback,
522
958
  candidates: Mutex::new(PendingCandidates::default()),
523
959
  },
@@ -527,15 +963,40 @@ impl VideoPeer {
527
963
 
528
964
  pub async fn accept_answer(&self, sdp: String) -> Result<usize> {
529
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);
530
967
  self.peer
531
968
  .set_remote_description(answer)
532
969
  .await
533
970
  .context("the peer refused the answer")?;
534
- 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) = {
535
984
  let mut state = lock(&self.candidates);
985
+ let first = !state.ready;
536
986
  state.ready = true;
537
- std::mem::take(&mut state.held)
987
+ (first, std::mem::take(&mut state.held))
538
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
+ }
539
1000
  let mut applied = 0;
540
1001
  for candidate in held {
541
1002
  // Each was acknowledged when it arrived; a bad one must not turn the
@@ -547,6 +1008,26 @@ impl VideoPeer {
547
1008
  Ok(applied)
548
1009
  }
549
1010
 
1011
+ /// Restart ICE on this peer: a fresh offer with a new ICE generation,
1012
+ /// for the client to answer as usual. The engine stays the offerer, so
1013
+ /// this is the same local-offer path as the initial negotiation.
1014
+ pub async fn restart_ice(&self) -> Result<String> {
1015
+ use rtc::peer_connection::configuration::RTCOfferOptions;
1016
+ let offer = self
1017
+ .peer
1018
+ .create_offer(Some(RTCOfferOptions {
1019
+ ice_restart: true,
1020
+ ..Default::default()
1021
+ }))
1022
+ .await
1023
+ .context("the peer would not offer an ICE restart")?;
1024
+ self.peer
1025
+ .set_local_description(offer.clone())
1026
+ .await
1027
+ .context("the peer refused its restart offer")?;
1028
+ Ok(offer.sdp)
1029
+ }
1030
+
550
1031
  pub async fn add_candidate(
551
1032
  &self,
552
1033
  candidate: String,
@@ -573,6 +1054,11 @@ impl VideoPeer {
573
1054
  .context("the peer refused an ICE candidate")
574
1055
  }
575
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
+
576
1062
  /// Whether the transport is connected; a frame sent before it is lost.
577
1063
  pub fn is_connected(&self) -> bool {
578
1064
  self.connected.load(Ordering::SeqCst)
@@ -585,18 +1071,24 @@ impl VideoPeer {
585
1071
  }
586
1072
 
587
1073
  /// Packetize one encoded frame and hand it to the track, paced by the
588
- /// interceptor chain. `captured` is when its picture was taken.
1074
+ /// interceptor chain. `captured` is when its picture was taken — for a fed
1075
+ /// stream, when its access unit arrived.
589
1076
  pub async fn send_frame(&self, data: &[u8], keyframe: bool, captured: Instant) -> Result<()> {
590
1077
  let packets = lock(&self.packetizer).packetize(
591
1078
  data,
592
1079
  keyframe,
593
1080
  captured,
594
1081
  self.ssrc,
595
- self.payload_type,
596
- );
1082
+ self.payload_type.load(Ordering::SeqCst),
1083
+ )?;
1084
+ let extensions: &[HeaderExtension] = if self.playout_delay.load(Ordering::SeqCst) {
1085
+ &[NO_PLAYOUT_DELAY]
1086
+ } else {
1087
+ &[]
1088
+ };
597
1089
  for packet in packets {
598
1090
  self.track
599
- .write_rtp_with_extensions(packet, &[])
1091
+ .write_rtp_with_extensions(packet, extensions)
600
1092
  .await
601
1093
  .context("failed to hand a packet to the track")?;
602
1094
  }
@@ -608,6 +1100,12 @@ impl VideoPeer {
608
1100
  std::mem::take(&mut *lock(&self.loss))
609
1101
  }
610
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
+
611
1109
  /// Take the pending "the far end needs a reference frame" request, if any.
612
1110
  pub fn take_keyframe_request(&self) -> bool {
613
1111
  self.wants_keyframe.swap(false, Ordering::SeqCst)
@@ -641,18 +1139,72 @@ fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
641
1139
  }
642
1140
 
643
1141
  #[cfg(test)]
644
- mod tests {
1142
+ pub(crate) mod tests {
645
1143
  use super::*;
646
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
+
647
1189
  struct NoopHandler;
648
1190
 
649
1191
  impl PeerConnectionEventHandler for NoopHandler {}
650
1192
 
651
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> {
652
1202
  let mut media_engine = MediaEngine::default();
653
- media_engine
654
- .register_codec(vp9_codec(), RtpCodecKind::Video)
655
- .expect("VP9 registers");
1203
+ for codec in codecs {
1204
+ media_engine
1205
+ .register_codec(codec, RtpCodecKind::Video)
1206
+ .expect("the codec registers");
1207
+ }
656
1208
  let registry = register_default_interceptors(Registry::new(), &mut media_engine)
657
1209
  .expect("interceptors register");
658
1210
  let peer = PeerConnectionBuilder::<std::net::SocketAddr>::new()
@@ -672,6 +1224,206 @@ mod tests {
672
1224
  Arc::new(peer)
673
1225
  }
674
1226
 
1227
+ #[tokio::test]
1228
+ async fn an_encoded_offer_carries_the_streams_own_profile_and_level() {
1229
+ let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
1230
+ let (_peer, offer) = VideoPeer::offer(
1231
+ TransportOptions {
1232
+ ice_servers: Vec::new(),
1233
+ loopback_tcp: false,
1234
+ pace_bps: 20_000_000.0,
1235
+ },
1236
+ events,
1237
+ VideoCodec::H264 {
1238
+ profile_level_id: String::from("42c029"),
1239
+ },
1240
+ )
1241
+ .await
1242
+ .expect("a peer connection");
1243
+ assert!(
1244
+ offer.contains("H264/90000"),
1245
+ "the encoded track must be offered as H.264: {offer}",
1246
+ );
1247
+ assert!(
1248
+ offer.contains("packetization-mode=1;profile-level-id=42c029"),
1249
+ "the offer must name the fed SPS's own profile-level-id: {offer}",
1250
+ );
1251
+ assert!(
1252
+ !offer.contains("VP9"),
1253
+ "an encoded source must not also offer the codec it cannot encode: {offer}",
1254
+ );
1255
+ }
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
+
1365
+ #[test]
1366
+ fn the_profile_level_id_comes_from_the_sps_in_the_stream() {
1367
+ // A leading zero byte, a four-byte start code and a three-byte one, so
1368
+ // the scanner is exercised on both forms.
1369
+ let mut stream = vec![0x00, 0x00, 0x00, 0x01];
1370
+ stream.extend_from_slice(&[0x67, 0x42, 0xc0, 0x29, 0x8c, 0x8d]);
1371
+ stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x68, 0xce, 0x3c, 0x80]);
1372
+ stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x00]);
1373
+ assert_eq!(
1374
+ h264_profile_level_id(&stream).as_deref(),
1375
+ Some("42c029"),
1376
+ "profile_idc, constraint flags and level_idc, as the SDP needs them",
1377
+ );
1378
+ assert_eq!(
1379
+ h264_profile_level_id(&[0x00, 0x00, 0x01, 0x65, 0x88]),
1380
+ None,
1381
+ "a stream with no SPS cannot start a session",
1382
+ );
1383
+ let mut adjacent = vec![0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01];
1384
+ adjacent.extend_from_slice(&[0x67, 0x42, 0xc0, 0x29]);
1385
+ assert_eq!(
1386
+ h264_profile_level_id(&adjacent).as_deref(),
1387
+ Some("42c029"),
1388
+ "an empty NAL between start codes is skipped, not fatal to the scan",
1389
+ );
1390
+ }
1391
+
1392
+ #[test]
1393
+ fn a_fed_keyframe_is_packetized_as_parameter_sets_then_a_slice() {
1394
+ let mut stream = vec![0x00, 0x00, 0x00, 0x01];
1395
+ stream.extend_from_slice(&[0x67, 0x42, 0xc0, 0x29, 0x8c, 0x8d]);
1396
+ stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x68, 0xce, 0x3c, 0x80]);
1397
+ stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x00]);
1398
+ let payloader = h264_codec("42c029")
1399
+ .rtp_codec
1400
+ .payloader()
1401
+ .expect("the media engine has an H.264 payloader");
1402
+ let mut packetizer = H264Packetizer::new(payloader);
1403
+ let packets = packetizer
1404
+ .packetize(&stream, Instant::now(), 7, H264_PAYLOAD_TYPE)
1405
+ .expect("a keyframe packetizes");
1406
+ assert_eq!(
1407
+ packets.len(),
1408
+ 2,
1409
+ "one STAP-A for the parameter sets, one slice"
1410
+ );
1411
+ assert_eq!(
1412
+ packets[0].payload[0], 0x78,
1413
+ "the parameter sets travel as a STAP-A",
1414
+ );
1415
+ assert_eq!(packets[1].payload[0], 0x65, "then the IDR slice itself");
1416
+ assert!(
1417
+ !packets[0].header.marker && packets[1].header.marker,
1418
+ "the marker ends the access unit, not the first fragment of it",
1419
+ );
1420
+ assert_eq!(
1421
+ packets[0].header.timestamp, packets[1].header.timestamp,
1422
+ "an access unit is one instant",
1423
+ );
1424
+ assert_eq!(packets[0].header.payload_type, H264_PAYLOAD_TYPE);
1425
+ }
1426
+
675
1427
  #[tokio::test]
676
1428
  async fn a_candidate_that_arrives_before_the_answer_is_not_refused() {
677
1429
  let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
@@ -682,6 +1434,7 @@ mod tests {
682
1434
  pace_bps: 20_000_000.0,
683
1435
  },
684
1436
  events,
1437
+ VideoCodec::Vp9,
685
1438
  )
686
1439
  .await
687
1440
  .expect("a peer connection");
@@ -697,6 +1450,45 @@ mod tests {
697
1450
  .expect("a candidate that arrives before the answer is buffered");
698
1451
  }
699
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
+
700
1492
  #[tokio::test]
701
1493
  async fn held_candidates_reach_the_peer_when_the_answer_arrives() {
702
1494
  let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
@@ -707,6 +1499,7 @@ mod tests {
707
1499
  pace_bps: 20_000_000.0,
708
1500
  },
709
1501
  events,
1502
+ VideoCodec::Vp9,
710
1503
  )
711
1504
  .await
712
1505
  .expect("a peer connection");
@@ -733,6 +1526,57 @@ mod tests {
733
1526
  assert_eq!(applied, 1, "the held candidate must reach the peer");
734
1527
  }
735
1528
 
1529
+ #[tokio::test]
1530
+ async fn an_ice_restart_offers_fresh_credentials_on_the_same_peer() {
1531
+ let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
1532
+ let (peer, offer) = VideoPeer::offer(
1533
+ TransportOptions {
1534
+ ice_servers: Vec::new(),
1535
+ loopback_tcp: false,
1536
+ pace_bps: 20_000_000.0,
1537
+ },
1538
+ events,
1539
+ VideoCodec::Vp9,
1540
+ )
1541
+ .await
1542
+ .expect("a peer connection");
1543
+
1544
+ let other = answerer(&offer).await;
1545
+ let answer = other.create_answer(None).await.expect("an answer");
1546
+ other
1547
+ .set_local_description(answer.clone())
1548
+ .await
1549
+ .expect("a local description");
1550
+ peer.accept_answer(answer.sdp)
1551
+ .await
1552
+ .expect("the answer is accepted");
1553
+
1554
+ let ufrag = |sdp: &str| {
1555
+ sdp.lines()
1556
+ .find_map(|line| line.strip_prefix("a=ice-ufrag:"))
1557
+ .expect("an ICE username fragment")
1558
+ .to_owned()
1559
+ };
1560
+ let restart = peer.restart_ice().await.expect("a restart offer");
1561
+ assert!(
1562
+ !restart.is_empty() && ufrag(&restart) != ufrag(&offer),
1563
+ "the restart must turn over the ICE generation"
1564
+ );
1565
+ // The far end answers the restart as usual, on the same peer.
1566
+ other
1567
+ .set_remote_description(RTCSessionDescription::offer(restart).expect("a valid offer"))
1568
+ .await
1569
+ .expect("the restart offer applies");
1570
+ let reanswer = other.create_answer(None).await.expect("an answer");
1571
+ other
1572
+ .set_local_description(reanswer.clone())
1573
+ .await
1574
+ .expect("a local description");
1575
+ peer.accept_answer(reanswer.sdp)
1576
+ .await
1577
+ .expect("the restart answer is accepted");
1578
+ }
1579
+
736
1580
  #[tokio::test]
737
1581
  async fn a_forwarded_client_is_offered_ice_over_tcp_on_the_loopback() {
738
1582
  let offered = |loopback_tcp: bool| async move {
@@ -744,6 +1588,7 @@ mod tests {
744
1588
  pace_bps: 20_000_000.0,
745
1589
  },
746
1590
  events,
1591
+ VideoCodec::Vp9,
747
1592
  )
748
1593
  .await
749
1594
  .expect("a peer connection");