@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.
- package/NOTICE +23 -0
- package/README.md +78 -48
- package/bin/desklink-host.mjs +1 -1
- package/dist/bridge.d.ts +1 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +12 -4
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +1 -0
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +26 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolveEngine.d.ts +1 -1
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +22 -9
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +15 -7
- package/docs/PROTOCOL.md +150 -23
- package/engine/Cargo.lock +46 -1
- package/engine/Cargo.toml +12 -2
- package/engine/build.rs +28 -2
- package/engine/native/nvenc_shim.c +453 -0
- package/engine/native/vaapi_shim.c +727 -0
- package/engine/native/vpx_shim.c +54 -4
- package/engine/native/vt_shim.c +448 -0
- package/engine/src/capture.rs +75 -55
- package/engine/src/capture_mac.rs +24 -14
- package/engine/src/convert.rs +286 -61
- package/engine/src/encoder.rs +122 -13
- package/engine/src/h264.rs +188 -0
- package/engine/src/h264_nvenc.rs +308 -0
- package/engine/src/h264_openh264.rs +735 -0
- package/engine/src/h264_vaapi.rs +263 -0
- package/engine/src/h264_vt.rs +368 -0
- package/engine/src/mac.rs +6 -3
- package/engine/src/main.rs +22 -4
- package/engine/src/peer.rs +466 -28
- package/engine/src/protocol.rs +15 -2
- package/engine/src/session.rs +790 -93
- package/engine/src/x11.rs +210 -9
- package/engine/src/x11_mac.rs +12 -0
- package/engine/vendor/libva/COPYING +19 -0
- package/engine/vendor/libva/va/va.h +5450 -0
- package/engine/vendor/libva/va/va_dec_av1.h +695 -0
- package/engine/vendor/libva/va/va_dec_hevc.h +616 -0
- package/engine/vendor/libva/va/va_dec_jpeg.h +157 -0
- package/engine/vendor/libva/va/va_dec_vp8.h +249 -0
- package/engine/vendor/libva/va/va_dec_vp9.h +312 -0
- package/engine/vendor/libva/va/va_dec_vvc.h +688 -0
- package/engine/vendor/libva/va/va_drm.h +61 -0
- package/engine/vendor/libva/va/va_enc_av1.h +1010 -0
- package/engine/vendor/libva/va/va_enc_h264.h +659 -0
- package/engine/vendor/libva/va/va_enc_hevc.h +958 -0
- package/engine/vendor/libva/va/va_enc_jpeg.h +162 -0
- package/engine/vendor/libva/va/va_enc_mpeg2.h +308 -0
- package/engine/vendor/libva/va/va_enc_vp8.h +350 -0
- package/engine/vendor/libva/va/va_enc_vp9.h +639 -0
- package/engine/vendor/libva/va/va_fei.h +170 -0
- package/engine/vendor/libva/va/va_fei_h264.h +505 -0
- package/engine/vendor/libva/va/va_fei_hevc.h +254 -0
- package/engine/vendor/libva/va/va_prot.h +525 -0
- package/engine/vendor/libva/va/va_version.h +87 -0
- package/engine/vendor/libva/va/va_vpp.h +1590 -0
- package/engine/vendor/nv-codec-headers/LICENSE +24 -0
- package/engine/vendor/nv-codec-headers/README +7 -0
- package/engine/vendor/nv-codec-headers/include/ffnvcodec/nvEncodeAPI.h +3825 -0
- package/examples/reference-client.html +1 -1
- package/package.json +3 -2
package/engine/src/peer.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! The WebRTC handoff: one
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
.
|
|
636
|
-
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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();
|