@desklink/host 0.1.1 → 0.2.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/README.md +29 -7
- package/bin/desklink-host.mjs +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/resolveEngine.d.ts +2 -2
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +4 -4
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +7 -3
- package/docs/PROTOCOL.md +205 -9
- package/engine/Cargo.lock +2 -1
- package/engine/Cargo.toml +2 -1
- package/engine/src/keeper.rs +622 -0
- package/engine/src/mac.rs +3 -3
- package/engine/src/main.rs +42 -0
- package/engine/src/peer.rs +429 -22
- package/engine/src/protocol.rs +138 -2
- package/engine/src/session.rs +768 -55
- package/package.json +2 -2
package/engine/src/peer.rs
CHANGED
|
@@ -16,7 +16,9 @@ use rtc::interceptor::{
|
|
|
16
16
|
};
|
|
17
17
|
use rtc::media_stream::MediaStreamTrack;
|
|
18
18
|
use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors;
|
|
19
|
-
use rtc::peer_connection::configuration::media_engine::{
|
|
19
|
+
use rtc::peer_connection::configuration::media_engine::{
|
|
20
|
+
MediaEngine, MIME_TYPE_H264, MIME_TYPE_VP9,
|
|
21
|
+
};
|
|
20
22
|
use rtc::peer_connection::configuration::RTCConfigurationBuilder;
|
|
21
23
|
use rtc::peer_connection::event::RTCPeerConnectionIceEvent;
|
|
22
24
|
use rtc::peer_connection::sdp::RTCSessionDescription;
|
|
@@ -25,8 +27,8 @@ use rtc::rtcp::payload_feedbacks::full_intra_request::FullIntraRequest;
|
|
|
25
27
|
use rtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication;
|
|
26
28
|
use rtc::rtcp::receiver_report::ReceiverReport;
|
|
27
29
|
use rtc::rtp_transceiver::rtp_sender::{
|
|
28
|
-
RTCRtpCodec, RTCRtpCodecParameters, RTCRtpCodingParameters,
|
|
29
|
-
RtpCodecKind,
|
|
30
|
+
RTCPFeedback, RTCRtpCodec, RTCRtpCodecParameters, RTCRtpCodingParameters,
|
|
31
|
+
RTCRtpEncodingParameters, RtpCodecKind,
|
|
30
32
|
};
|
|
31
33
|
use rtc::rtp_transceiver::PayloadType;
|
|
32
34
|
use rtc::sansio::Protocol;
|
|
@@ -49,6 +51,23 @@ const VP9_CLOCK_RATE: u32 = 90_000;
|
|
|
49
51
|
/// this one is the common convention and keeps packet captures readable.
|
|
50
52
|
pub const VP9_PAYLOAD_TYPE: PayloadType = 98;
|
|
51
53
|
|
|
54
|
+
/// H.264's RTP clock is 90 kHz (RFC 6184), the same as VP9's.
|
|
55
|
+
const H264_CLOCK_RATE: u32 = 90_000;
|
|
56
|
+
|
|
57
|
+
/// Payload type the engine offers H.264 on, again a dynamic-range convention.
|
|
58
|
+
pub const H264_PAYLOAD_TYPE: PayloadType = 102;
|
|
59
|
+
|
|
60
|
+
/// Which video track this peer carries.
|
|
61
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
62
|
+
pub enum VideoCodec {
|
|
63
|
+
/// VP9 in flexible mode, encoded by this engine from captured frames.
|
|
64
|
+
Vp9,
|
|
65
|
+
/// H.264 the consumer encoded and feeds. The profile-level-id is read from
|
|
66
|
+
/// the fed stream's own SPS, so a receiver's decoder is configured for the
|
|
67
|
+
/// profile actually arriving rather than for one the engine guessed.
|
|
68
|
+
H264 { profile_level_id: String },
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
fn vp9_codec() -> RTCRtpCodecParameters {
|
|
53
72
|
RTCRtpCodecParameters {
|
|
54
73
|
rtp_codec: RTCRtpCodec {
|
|
@@ -63,6 +82,91 @@ fn vp9_codec() -> RTCRtpCodecParameters {
|
|
|
63
82
|
}
|
|
64
83
|
}
|
|
65
84
|
|
|
85
|
+
/// H.264 with `packetization-mode=1`, which is what the device helpers emit and
|
|
86
|
+
/// what every WebRTC receiver implements. Key frame requests are negotiated so
|
|
87
|
+
/// the receiver has a defined way to ask; the engine turns one into a
|
|
88
|
+
/// `session.keyframeRequest` for the consumer that owns the encoder.
|
|
89
|
+
fn h264_codec(profile_level_id: &str) -> RTCRtpCodecParameters {
|
|
90
|
+
RTCRtpCodecParameters {
|
|
91
|
+
rtp_codec: RTCRtpCodec {
|
|
92
|
+
mime_type: MIME_TYPE_H264.to_owned(),
|
|
93
|
+
clock_rate: H264_CLOCK_RATE,
|
|
94
|
+
channels: 0,
|
|
95
|
+
sdp_fmtp_line: format!(
|
|
96
|
+
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id={profile_level_id}"
|
|
97
|
+
),
|
|
98
|
+
rtcp_feedback: [("nack", ""), ("nack", "pli"), ("ccm", "fir")]
|
|
99
|
+
.into_iter()
|
|
100
|
+
.map(|(typ, parameter)| RTCPFeedback {
|
|
101
|
+
typ: typ.to_owned(),
|
|
102
|
+
parameter: parameter.to_owned(),
|
|
103
|
+
})
|
|
104
|
+
.collect(),
|
|
105
|
+
},
|
|
106
|
+
payload_type: H264_PAYLOAD_TYPE,
|
|
107
|
+
..Default::default()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// The `profile-level-id` an H.264 Annex-B stream declares, as the three bytes
|
|
112
|
+
/// after an SPS NAL header: `profile_idc`, the constraint flags, `level_idc`.
|
|
113
|
+
///
|
|
114
|
+
/// This is the value the offer must carry, and it can only come from the stream
|
|
115
|
+
/// itself, so a peer for a fed stream is built after the first access unit
|
|
116
|
+
/// rather than at `session.open`.
|
|
117
|
+
pub fn h264_profile_level_id(annex_b: &[u8]) -> Option<String> {
|
|
118
|
+
for nalu in AnnexBNalUnits::new(annex_b) {
|
|
119
|
+
let Some(&first) = nalu.first() else {
|
|
120
|
+
continue;
|
|
121
|
+
};
|
|
122
|
+
if first & 0x1f == SPS_NAL_TYPE && nalu.len() >= 4 {
|
|
123
|
+
return Some(format!("{:02x}{:02x}{:02x}", nalu[1], nalu[2], nalu[3]));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
None
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// NAL type 7: sequence parameter set.
|
|
130
|
+
const SPS_NAL_TYPE: u8 = 7;
|
|
131
|
+
|
|
132
|
+
/// A byte stream's NAL units, delimited by the Annex-B start code
|
|
133
|
+
/// (`00 00 01`, optionally preceded by zero bytes).
|
|
134
|
+
struct AnnexBNalUnits<'a> {
|
|
135
|
+
rest: &'a [u8],
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
impl<'a> AnnexBNalUnits<'a> {
|
|
139
|
+
fn new(stream: &'a [u8]) -> Self {
|
|
140
|
+
Self { rest: stream }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn start_code(stream: &[u8]) -> Option<usize> {
|
|
144
|
+
stream
|
|
145
|
+
.windows(3)
|
|
146
|
+
.position(|window| window == [0x00, 0x00, 0x01])
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
impl<'a> Iterator for AnnexBNalUnits<'a> {
|
|
151
|
+
type Item = &'a [u8];
|
|
152
|
+
|
|
153
|
+
fn next(&mut self) -> Option<Self::Item> {
|
|
154
|
+
let start = Self::start_code(self.rest)? + 3;
|
|
155
|
+
let body = &self.rest[start..];
|
|
156
|
+
let end = Self::start_code(body).map_or(body.len(), |next| {
|
|
157
|
+
// A four-byte start code includes the zero the three-byte search
|
|
158
|
+
// left at the end of the previous unit.
|
|
159
|
+
if next > 0 && body[next - 1] == 0x00 {
|
|
160
|
+
next - 1
|
|
161
|
+
} else {
|
|
162
|
+
next
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
self.rest = &body[end..];
|
|
166
|
+
Some(&body[..end])
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
66
170
|
/// What the consumer needs to know about a live peer.
|
|
67
171
|
#[derive(Debug, Clone)]
|
|
68
172
|
pub enum PeerEvent {
|
|
@@ -80,9 +184,13 @@ pub enum PeerEvent {
|
|
|
80
184
|
ControlClosed,
|
|
81
185
|
/// A control message arrived from the client.
|
|
82
186
|
ControlMessage(String),
|
|
187
|
+
/// The far end asked for a fresh reference frame (RTCP PLI or FIR) on a
|
|
188
|
+
/// track this engine does not encode itself, so the consumer must supply one.
|
|
189
|
+
KeyframeRequest,
|
|
83
190
|
}
|
|
84
191
|
|
|
85
192
|
/// Transport settings the consumer chose for this session.
|
|
193
|
+
#[derive(Debug, Clone)]
|
|
86
194
|
pub struct TransportOptions {
|
|
87
195
|
pub ice_servers: Vec<(String, Option<String>, Option<String>)>,
|
|
88
196
|
/// Listen for ICE over TCP on the loopback too. A phone that reaches this
|
|
@@ -116,21 +224,33 @@ async fn serve_control(
|
|
|
116
224
|
channel: Arc<dyn DataChannel>,
|
|
117
225
|
events: tokio::sync::mpsc::UnboundedSender<PeerEvent>,
|
|
118
226
|
) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
227
|
+
// A heartbeat the client watches: ICE consent timers take seconds to call
|
|
228
|
+
// a dead path disconnected, while a missing heartbeat reads as stalled
|
|
229
|
+
// within about a second. Cheap on purpose — seventeen bytes twice a second.
|
|
230
|
+
let mut ping = tokio::time::interval(std::time::Duration::from_millis(500));
|
|
231
|
+
loop {
|
|
232
|
+
tokio::select! {
|
|
233
|
+
event = channel.poll() => {
|
|
234
|
+
match event {
|
|
235
|
+
Some(DataChannelEvent::OnOpen) => {
|
|
236
|
+
let _ = events.send(PeerEvent::ControlOpen);
|
|
237
|
+
}
|
|
238
|
+
Some(DataChannelEvent::OnMessage(message)) => {
|
|
239
|
+
if let Ok(text) = String::from_utf8(message.data.to_vec()) {
|
|
240
|
+
let _ = events.send(PeerEvent::ControlMessage(text));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
Some(DataChannelEvent::OnClose) => {
|
|
244
|
+
let _ = events.send(PeerEvent::ControlClosed);
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
Some(_) => {}
|
|
248
|
+
None => break,
|
|
127
249
|
}
|
|
128
250
|
}
|
|
129
|
-
|
|
130
|
-
let _ =
|
|
131
|
-
break;
|
|
251
|
+
_ = ping.tick() => {
|
|
252
|
+
let _ = channel.send_text(r#"{"kind":"ping"}"#).await;
|
|
132
253
|
}
|
|
133
|
-
_ => {}
|
|
134
254
|
}
|
|
135
255
|
}
|
|
136
256
|
let _ = events.send(PeerEvent::ControlClosed);
|
|
@@ -231,12 +351,14 @@ impl Interceptor for FeedbackForwarder {
|
|
|
231
351
|
|
|
232
352
|
/// Read the video sender's feedback until the peer closes it (by aborting this
|
|
233
353
|
/// task: the track's feedback channel outlives a closed peer): key frame
|
|
234
|
-
/// requests set `wants_keyframe`, receiver reports queue their loss fraction
|
|
354
|
+
/// requests set `wants_keyframe`, receiver reports queue their loss fraction,
|
|
355
|
+
/// and a track whose encoder lives in the consumer is told to ask for one.
|
|
235
356
|
async fn serve_feedback(
|
|
236
357
|
track: Arc<TrackLocalStaticRTP>,
|
|
237
358
|
ssrc: u32,
|
|
238
359
|
wants_keyframe: Arc<AtomicBool>,
|
|
239
360
|
loss: Arc<Mutex<Vec<u8>>>,
|
|
361
|
+
keyframe_requests: Option<tokio::sync::mpsc::UnboundedSender<PeerEvent>>,
|
|
240
362
|
) {
|
|
241
363
|
loop {
|
|
242
364
|
// The track is bound once the answer is applied; until then, and after
|
|
@@ -249,6 +371,9 @@ async fn serve_feedback(
|
|
|
249
371
|
let packet = packet.as_any();
|
|
250
372
|
if packet.is::<PictureLossIndication>() || packet.is::<FullIntraRequest>() {
|
|
251
373
|
wants_keyframe.store(true, Ordering::SeqCst);
|
|
374
|
+
if let Some(requests) = &keyframe_requests {
|
|
375
|
+
let _ = requests.send(PeerEvent::KeyframeRequest);
|
|
376
|
+
}
|
|
252
377
|
} else if let Some(report) = packet.downcast_ref::<ReceiverReport>() {
|
|
253
378
|
let mut queue = lock(&loss);
|
|
254
379
|
queue.extend(
|
|
@@ -270,6 +395,102 @@ async fn serve_feedback(
|
|
|
270
395
|
const RTP_MTU: usize = 1200;
|
|
271
396
|
const RTP_HEADER: usize = 12;
|
|
272
397
|
|
|
398
|
+
/// One encoded track's packetizer. The engine builds RTP headers itself either
|
|
399
|
+
/// way — the payloaders below only decide how a frame's bytes are split — so the
|
|
400
|
+
/// codec only changes which ones run.
|
|
401
|
+
enum Packetizer {
|
|
402
|
+
Vp9(Vp9Packetizer),
|
|
403
|
+
H264(H264Packetizer),
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
impl Packetizer {
|
|
407
|
+
fn packetize(
|
|
408
|
+
&mut self,
|
|
409
|
+
frame: &[u8],
|
|
410
|
+
keyframe: bool,
|
|
411
|
+
captured: Instant,
|
|
412
|
+
ssrc: u32,
|
|
413
|
+
payload_type: PayloadType,
|
|
414
|
+
) -> Result<Vec<rtc::rtp::Packet>> {
|
|
415
|
+
match self {
|
|
416
|
+
Self::Vp9(packetizer) => {
|
|
417
|
+
Ok(packetizer.packetize(frame, keyframe, captured, ssrc, payload_type))
|
|
418
|
+
}
|
|
419
|
+
Self::H264(packetizer) => packetizer.packetize(frame, captured, ssrc, payload_type),
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/// H.264 over RTP (RFC 6184), packetization mode 1: single NAL units, STAP-A for
|
|
425
|
+
/// the parameter sets, FU-A for anything over the MTU. The fragmentation itself
|
|
426
|
+
/// is webrtc-rs's own H.264 payloader (`RTCRtpCodec::payloader`), which already
|
|
427
|
+
/// implements those three payload shapes; only the RTP header is built here, so
|
|
428
|
+
/// the timestamp is the arrival time of the access unit rather than the moment
|
|
429
|
+
/// each fragment happened to be written.
|
|
430
|
+
struct H264Packetizer {
|
|
431
|
+
sequence: u16,
|
|
432
|
+
timestamp_base: u32,
|
|
433
|
+
started: Instant,
|
|
434
|
+
payloader: Box<dyn rtc::rtp::packetizer::Payloader>,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
impl H264Packetizer {
|
|
438
|
+
fn new(payloader: Box<dyn rtc::rtp::packetizer::Payloader>) -> Self {
|
|
439
|
+
Self {
|
|
440
|
+
sequence: rand::random(),
|
|
441
|
+
timestamp_base: rand::random(),
|
|
442
|
+
started: Instant::now(),
|
|
443
|
+
payloader,
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
fn packetize(
|
|
448
|
+
&mut self,
|
|
449
|
+
access_unit: &[u8],
|
|
450
|
+
arrived: Instant,
|
|
451
|
+
ssrc: u32,
|
|
452
|
+
payload_type: PayloadType,
|
|
453
|
+
) -> Result<Vec<rtc::rtp::Packet>> {
|
|
454
|
+
let payloads = self
|
|
455
|
+
.payloader
|
|
456
|
+
.payload(
|
|
457
|
+
RTP_MTU - RTP_HEADER,
|
|
458
|
+
&bytes::Bytes::copy_from_slice(access_unit),
|
|
459
|
+
)
|
|
460
|
+
.map_err(|error| {
|
|
461
|
+
anyhow::anyhow!("the H.264 payloader refused an access unit: {error}")
|
|
462
|
+
})?;
|
|
463
|
+
let ticks = (arrived
|
|
464
|
+
.saturating_duration_since(self.started)
|
|
465
|
+
.as_secs_f64()
|
|
466
|
+
* H264_CLOCK_RATE as f64) as u64 as u32;
|
|
467
|
+
let timestamp = self.timestamp_base.wrapping_add(ticks);
|
|
468
|
+
let last = payloads.len().saturating_sub(1);
|
|
469
|
+
Ok(payloads
|
|
470
|
+
.into_iter()
|
|
471
|
+
.enumerate()
|
|
472
|
+
.map(|(index, payload)| {
|
|
473
|
+
let sequence_number = self.sequence;
|
|
474
|
+
self.sequence = self.sequence.wrapping_add(1);
|
|
475
|
+
rtc::rtp::Packet {
|
|
476
|
+
header: rtc::rtp::Header {
|
|
477
|
+
version: 2,
|
|
478
|
+
// The marker ends an access unit, which is what a video
|
|
479
|
+
// receiver uses to hand a complete picture to its decoder.
|
|
480
|
+
marker: index == last,
|
|
481
|
+
payload_type,
|
|
482
|
+
sequence_number,
|
|
483
|
+
timestamp,
|
|
484
|
+
ssrc,
|
|
485
|
+
..Default::default()
|
|
486
|
+
},
|
|
487
|
+
payload,
|
|
488
|
+
}
|
|
489
|
+
})
|
|
490
|
+
.collect())
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
273
494
|
/// VP9 over RTP (RFC 9628) in flexible mode, one layer.
|
|
274
495
|
///
|
|
275
496
|
/// The payload descriptor is what a receiver builds its reference graph from,
|
|
@@ -377,7 +598,7 @@ pub struct VideoPeer {
|
|
|
377
598
|
/// True while the transport is connected: frames sent before it are lost.
|
|
378
599
|
connected: Arc<AtomicBool>,
|
|
379
600
|
track: Arc<TrackLocalStaticRTP>,
|
|
380
|
-
packetizer: Mutex<
|
|
601
|
+
packetizer: Mutex<Packetizer>,
|
|
381
602
|
ssrc: u32,
|
|
382
603
|
control: Arc<dyn DataChannel>,
|
|
383
604
|
payload_type: PayloadType,
|
|
@@ -401,14 +622,18 @@ impl VideoPeer {
|
|
|
401
622
|
pub async fn offer(
|
|
402
623
|
options: TransportOptions,
|
|
403
624
|
events: tokio::sync::mpsc::UnboundedSender<PeerEvent>,
|
|
625
|
+
codec: VideoCodec,
|
|
404
626
|
) -> Result<(Self, String)> {
|
|
405
627
|
let runtime = default_runtime().context("no WebRTC runtime is enabled")?;
|
|
406
628
|
|
|
407
629
|
let mut media_engine = MediaEngine::default();
|
|
408
|
-
let video_codec =
|
|
630
|
+
let video_codec = match &codec {
|
|
631
|
+
VideoCodec::Vp9 => vp9_codec(),
|
|
632
|
+
VideoCodec::H264 { profile_level_id } => h264_codec(profile_level_id),
|
|
633
|
+
};
|
|
409
634
|
media_engine
|
|
410
635
|
.register_codec(video_codec.clone(), RtpCodecKind::Video)
|
|
411
|
-
.
|
|
636
|
+
.with_context(|| format!("failed to offer {codec:?}"))?;
|
|
412
637
|
let registry = register_default_interceptors(Registry::new(), &mut media_engine)?
|
|
413
638
|
.with(
|
|
414
639
|
Slot::Pacer,
|
|
@@ -437,6 +662,10 @@ impl VideoPeer {
|
|
|
437
662
|
let wants_keyframe = Arc::new(AtomicBool::new(true));
|
|
438
663
|
let connected = Arc::new(AtomicBool::new(false));
|
|
439
664
|
let control_events = events.clone();
|
|
665
|
+
// A track this engine encodes itself needs no notification: the pipeline
|
|
666
|
+
// reads `wants_keyframe` directly. A fed track's encoder lives in the
|
|
667
|
+
// consumer, so a key frame request has to travel back as an event.
|
|
668
|
+
let keyframe_requests = matches!(codec, VideoCodec::H264 { .. }).then(|| events.clone());
|
|
440
669
|
let peer = PeerConnectionBuilder::<std::net::SocketAddr>::new()
|
|
441
670
|
.with_configuration(builder.build())
|
|
442
671
|
.with_media_engine(media_engine)
|
|
@@ -502,8 +731,19 @@ impl VideoPeer {
|
|
|
502
731
|
ssrc,
|
|
503
732
|
wants_keyframe.clone(),
|
|
504
733
|
loss.clone(),
|
|
734
|
+
keyframe_requests,
|
|
505
735
|
));
|
|
506
736
|
|
|
737
|
+
let packetizer = match &codec {
|
|
738
|
+
VideoCodec::Vp9 => Packetizer::Vp9(Vp9Packetizer::new()),
|
|
739
|
+
VideoCodec::H264 { .. } => Packetizer::H264(H264Packetizer::new(
|
|
740
|
+
video_codec
|
|
741
|
+
.rtp_codec
|
|
742
|
+
.payloader()
|
|
743
|
+
.context("the media engine has no H.264 payloader")?,
|
|
744
|
+
)),
|
|
745
|
+
};
|
|
746
|
+
|
|
507
747
|
let offer = peer.create_offer(None).await?;
|
|
508
748
|
peer.set_local_description(offer.clone()).await?;
|
|
509
749
|
|
|
@@ -513,7 +753,7 @@ impl VideoPeer {
|
|
|
513
753
|
wants_keyframe,
|
|
514
754
|
connected,
|
|
515
755
|
track,
|
|
516
|
-
packetizer: Mutex::new(
|
|
756
|
+
packetizer: Mutex::new(packetizer),
|
|
517
757
|
ssrc,
|
|
518
758
|
control,
|
|
519
759
|
payload_type,
|
|
@@ -547,6 +787,26 @@ impl VideoPeer {
|
|
|
547
787
|
Ok(applied)
|
|
548
788
|
}
|
|
549
789
|
|
|
790
|
+
/// Restart ICE on this peer: a fresh offer with a new ICE generation,
|
|
791
|
+
/// for the client to answer as usual. The engine stays the offerer, so
|
|
792
|
+
/// this is the same local-offer path as the initial negotiation.
|
|
793
|
+
pub async fn restart_ice(&self) -> Result<String> {
|
|
794
|
+
use rtc::peer_connection::configuration::RTCOfferOptions;
|
|
795
|
+
let offer = self
|
|
796
|
+
.peer
|
|
797
|
+
.create_offer(Some(RTCOfferOptions {
|
|
798
|
+
ice_restart: true,
|
|
799
|
+
..Default::default()
|
|
800
|
+
}))
|
|
801
|
+
.await
|
|
802
|
+
.context("the peer would not offer an ICE restart")?;
|
|
803
|
+
self.peer
|
|
804
|
+
.set_local_description(offer.clone())
|
|
805
|
+
.await
|
|
806
|
+
.context("the peer refused its restart offer")?;
|
|
807
|
+
Ok(offer.sdp)
|
|
808
|
+
}
|
|
809
|
+
|
|
550
810
|
pub async fn add_candidate(
|
|
551
811
|
&self,
|
|
552
812
|
candidate: String,
|
|
@@ -585,7 +845,8 @@ impl VideoPeer {
|
|
|
585
845
|
}
|
|
586
846
|
|
|
587
847
|
/// Packetize one encoded frame and hand it to the track, paced by the
|
|
588
|
-
/// interceptor chain. `captured` is when its picture was taken
|
|
848
|
+
/// interceptor chain. `captured` is when its picture was taken — for a fed
|
|
849
|
+
/// stream, when its access unit arrived.
|
|
589
850
|
pub async fn send_frame(&self, data: &[u8], keyframe: bool, captured: Instant) -> Result<()> {
|
|
590
851
|
let packets = lock(&self.packetizer).packetize(
|
|
591
852
|
data,
|
|
@@ -593,7 +854,7 @@ impl VideoPeer {
|
|
|
593
854
|
captured,
|
|
594
855
|
self.ssrc,
|
|
595
856
|
self.payload_type,
|
|
596
|
-
)
|
|
857
|
+
)?;
|
|
597
858
|
for packet in packets {
|
|
598
859
|
self.track
|
|
599
860
|
.write_rtp_with_extensions(packet, &[])
|
|
@@ -672,6 +933,98 @@ mod tests {
|
|
|
672
933
|
Arc::new(peer)
|
|
673
934
|
}
|
|
674
935
|
|
|
936
|
+
#[tokio::test]
|
|
937
|
+
async fn an_encoded_offer_carries_the_streams_own_profile_and_level() {
|
|
938
|
+
let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
939
|
+
let (_peer, offer) = VideoPeer::offer(
|
|
940
|
+
TransportOptions {
|
|
941
|
+
ice_servers: Vec::new(),
|
|
942
|
+
loopback_tcp: false,
|
|
943
|
+
pace_bps: 20_000_000.0,
|
|
944
|
+
},
|
|
945
|
+
events,
|
|
946
|
+
VideoCodec::H264 {
|
|
947
|
+
profile_level_id: String::from("42c029"),
|
|
948
|
+
},
|
|
949
|
+
)
|
|
950
|
+
.await
|
|
951
|
+
.expect("a peer connection");
|
|
952
|
+
assert!(
|
|
953
|
+
offer.contains("H264/90000"),
|
|
954
|
+
"the encoded track must be offered as H.264: {offer}",
|
|
955
|
+
);
|
|
956
|
+
assert!(
|
|
957
|
+
offer.contains("packetization-mode=1;profile-level-id=42c029"),
|
|
958
|
+
"the offer must name the fed SPS's own profile-level-id: {offer}",
|
|
959
|
+
);
|
|
960
|
+
assert!(
|
|
961
|
+
!offer.contains("VP9"),
|
|
962
|
+
"an encoded source must not also offer the codec it cannot encode: {offer}",
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
#[test]
|
|
967
|
+
fn the_profile_level_id_comes_from_the_sps_in_the_stream() {
|
|
968
|
+
// A leading zero byte, a four-byte start code and a three-byte one, so
|
|
969
|
+
// the scanner is exercised on both forms.
|
|
970
|
+
let mut stream = vec![0x00, 0x00, 0x00, 0x01];
|
|
971
|
+
stream.extend_from_slice(&[0x67, 0x42, 0xc0, 0x29, 0x8c, 0x8d]);
|
|
972
|
+
stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x68, 0xce, 0x3c, 0x80]);
|
|
973
|
+
stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x00]);
|
|
974
|
+
assert_eq!(
|
|
975
|
+
h264_profile_level_id(&stream).as_deref(),
|
|
976
|
+
Some("42c029"),
|
|
977
|
+
"profile_idc, constraint flags and level_idc, as the SDP needs them",
|
|
978
|
+
);
|
|
979
|
+
assert_eq!(
|
|
980
|
+
h264_profile_level_id(&[0x00, 0x00, 0x01, 0x65, 0x88]),
|
|
981
|
+
None,
|
|
982
|
+
"a stream with no SPS cannot start a session",
|
|
983
|
+
);
|
|
984
|
+
let mut adjacent = vec![0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01];
|
|
985
|
+
adjacent.extend_from_slice(&[0x67, 0x42, 0xc0, 0x29]);
|
|
986
|
+
assert_eq!(
|
|
987
|
+
h264_profile_level_id(&adjacent).as_deref(),
|
|
988
|
+
Some("42c029"),
|
|
989
|
+
"an empty NAL between start codes is skipped, not fatal to the scan",
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
#[test]
|
|
994
|
+
fn a_fed_keyframe_is_packetized_as_parameter_sets_then_a_slice() {
|
|
995
|
+
let mut stream = vec![0x00, 0x00, 0x00, 0x01];
|
|
996
|
+
stream.extend_from_slice(&[0x67, 0x42, 0xc0, 0x29, 0x8c, 0x8d]);
|
|
997
|
+
stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x68, 0xce, 0x3c, 0x80]);
|
|
998
|
+
stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x00]);
|
|
999
|
+
let payloader = h264_codec("42c029")
|
|
1000
|
+
.rtp_codec
|
|
1001
|
+
.payloader()
|
|
1002
|
+
.expect("the media engine has an H.264 payloader");
|
|
1003
|
+
let mut packetizer = H264Packetizer::new(payloader);
|
|
1004
|
+
let packets = packetizer
|
|
1005
|
+
.packetize(&stream, Instant::now(), 7, H264_PAYLOAD_TYPE)
|
|
1006
|
+
.expect("a keyframe packetizes");
|
|
1007
|
+
assert_eq!(
|
|
1008
|
+
packets.len(),
|
|
1009
|
+
2,
|
|
1010
|
+
"one STAP-A for the parameter sets, one slice"
|
|
1011
|
+
);
|
|
1012
|
+
assert_eq!(
|
|
1013
|
+
packets[0].payload[0], 0x78,
|
|
1014
|
+
"the parameter sets travel as a STAP-A",
|
|
1015
|
+
);
|
|
1016
|
+
assert_eq!(packets[1].payload[0], 0x65, "then the IDR slice itself");
|
|
1017
|
+
assert!(
|
|
1018
|
+
!packets[0].header.marker && packets[1].header.marker,
|
|
1019
|
+
"the marker ends the access unit, not the first fragment of it",
|
|
1020
|
+
);
|
|
1021
|
+
assert_eq!(
|
|
1022
|
+
packets[0].header.timestamp, packets[1].header.timestamp,
|
|
1023
|
+
"an access unit is one instant",
|
|
1024
|
+
);
|
|
1025
|
+
assert_eq!(packets[0].header.payload_type, H264_PAYLOAD_TYPE);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
675
1028
|
#[tokio::test]
|
|
676
1029
|
async fn a_candidate_that_arrives_before_the_answer_is_not_refused() {
|
|
677
1030
|
let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
@@ -682,6 +1035,7 @@ mod tests {
|
|
|
682
1035
|
pace_bps: 20_000_000.0,
|
|
683
1036
|
},
|
|
684
1037
|
events,
|
|
1038
|
+
VideoCodec::Vp9,
|
|
685
1039
|
)
|
|
686
1040
|
.await
|
|
687
1041
|
.expect("a peer connection");
|
|
@@ -707,6 +1061,7 @@ mod tests {
|
|
|
707
1061
|
pace_bps: 20_000_000.0,
|
|
708
1062
|
},
|
|
709
1063
|
events,
|
|
1064
|
+
VideoCodec::Vp9,
|
|
710
1065
|
)
|
|
711
1066
|
.await
|
|
712
1067
|
.expect("a peer connection");
|
|
@@ -733,6 +1088,57 @@ mod tests {
|
|
|
733
1088
|
assert_eq!(applied, 1, "the held candidate must reach the peer");
|
|
734
1089
|
}
|
|
735
1090
|
|
|
1091
|
+
#[tokio::test]
|
|
1092
|
+
async fn an_ice_restart_offers_fresh_credentials_on_the_same_peer() {
|
|
1093
|
+
let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
1094
|
+
let (peer, offer) = VideoPeer::offer(
|
|
1095
|
+
TransportOptions {
|
|
1096
|
+
ice_servers: Vec::new(),
|
|
1097
|
+
loopback_tcp: false,
|
|
1098
|
+
pace_bps: 20_000_000.0,
|
|
1099
|
+
},
|
|
1100
|
+
events,
|
|
1101
|
+
VideoCodec::Vp9,
|
|
1102
|
+
)
|
|
1103
|
+
.await
|
|
1104
|
+
.expect("a peer connection");
|
|
1105
|
+
|
|
1106
|
+
let other = answerer(&offer).await;
|
|
1107
|
+
let answer = other.create_answer(None).await.expect("an answer");
|
|
1108
|
+
other
|
|
1109
|
+
.set_local_description(answer.clone())
|
|
1110
|
+
.await
|
|
1111
|
+
.expect("a local description");
|
|
1112
|
+
peer.accept_answer(answer.sdp)
|
|
1113
|
+
.await
|
|
1114
|
+
.expect("the answer is accepted");
|
|
1115
|
+
|
|
1116
|
+
let ufrag = |sdp: &str| {
|
|
1117
|
+
sdp.lines()
|
|
1118
|
+
.find_map(|line| line.strip_prefix("a=ice-ufrag:"))
|
|
1119
|
+
.expect("an ICE username fragment")
|
|
1120
|
+
.to_owned()
|
|
1121
|
+
};
|
|
1122
|
+
let restart = peer.restart_ice().await.expect("a restart offer");
|
|
1123
|
+
assert!(
|
|
1124
|
+
!restart.is_empty() && ufrag(&restart) != ufrag(&offer),
|
|
1125
|
+
"the restart must turn over the ICE generation"
|
|
1126
|
+
);
|
|
1127
|
+
// The far end answers the restart as usual, on the same peer.
|
|
1128
|
+
other
|
|
1129
|
+
.set_remote_description(RTCSessionDescription::offer(restart).expect("a valid offer"))
|
|
1130
|
+
.await
|
|
1131
|
+
.expect("the restart offer applies");
|
|
1132
|
+
let reanswer = other.create_answer(None).await.expect("an answer");
|
|
1133
|
+
other
|
|
1134
|
+
.set_local_description(reanswer.clone())
|
|
1135
|
+
.await
|
|
1136
|
+
.expect("a local description");
|
|
1137
|
+
peer.accept_answer(reanswer.sdp)
|
|
1138
|
+
.await
|
|
1139
|
+
.expect("the restart answer is accepted");
|
|
1140
|
+
}
|
|
1141
|
+
|
|
736
1142
|
#[tokio::test]
|
|
737
1143
|
async fn a_forwarded_client_is_offered_ice_over_tcp_on_the_loopback() {
|
|
738
1144
|
let offered = |loopback_tcp: bool| async move {
|
|
@@ -744,6 +1150,7 @@ mod tests {
|
|
|
744
1150
|
pace_bps: 20_000_000.0,
|
|
745
1151
|
},
|
|
746
1152
|
events,
|
|
1153
|
+
VideoCodec::Vp9,
|
|
747
1154
|
)
|
|
748
1155
|
.await
|
|
749
1156
|
.expect("a peer connection");
|