@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.
- package/NOTICE +23 -0
- package/README.md +107 -55
- package/bin/desklink-host.mjs +2 -2
- 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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/keeper.d.ts +52 -0
- package/dist/keeper.d.ts.map +1 -0
- package/dist/keeper.js +101 -0
- package/dist/keeper.js.map +1 -0
- 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 +3 -3
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +24 -11
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +20 -8
- package/docs/PROTOCOL.md +347 -24
- package/engine/Cargo.lock +47 -1
- package/engine/Cargo.toml +13 -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/keeper.rs +622 -0
- package/engine/src/mac.rs +9 -6
- package/engine/src/main.rs +64 -4
- package/engine/src/peer.rs +887 -42
- package/engine/src/protocol.rs +153 -4
- package/engine/src/session.rs +1547 -137
- 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,11 +12,14 @@
|
|
|
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;
|
|
19
|
-
use rtc::peer_connection::configuration::media_engine::{
|
|
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,
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
let _ =
|
|
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<
|
|
756
|
+
packetizer: Mutex<Packetizer>,
|
|
381
757
|
ssrc: u32,
|
|
382
758
|
control: Arc<dyn DataChannel>,
|
|
383
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
411
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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");
|