arcly_stream/protocol/srt/
mod.rs1mod arq;
44#[cfg(feature = "srt-encrypt")]
45mod crypto;
46mod egress;
47mod handshake;
48#[cfg(feature = "srt-encrypt")]
49mod keymaterial;
50mod packet;
51
52pub use egress::SrtCaller;
53pub use handshake::{HandshakeType, SrtHandshake};
54pub use packet::{ControlType, SrtPacket};
55pub use crate::protocol::tsdemux::{TsDemuxer, TsPayload, TsTrackKind};
58
59use crate::inbound::{InboundProtocol, IngestContext, PublishSession};
60use crate::{CodecId, MediaFrame, Result, StreamKey};
61use async_trait::async_trait;
62use std::net::SocketAddr;
63use std::time::{Duration, Instant};
64use tokio_util::sync::CancellationToken;
65use tracing::{debug, info, warn};
66
67use arq::Receiver;
68
69const ARQ_WINDOW: usize = 1024;
71const CONTROL_INTERVAL: Duration = Duration::from_millis(10);
73
74#[derive(Debug, Clone)]
76pub struct SrtHandler {
77 bind: SocketAddr,
78 key: StreamKey,
79 #[cfg(feature = "srt-encrypt")]
81 passphrase: Option<String>,
82}
83
84impl SrtHandler {
85 pub fn new(bind: SocketAddr, key: StreamKey) -> Self {
87 Self {
88 bind,
89 key,
90 #[cfg(feature = "srt-encrypt")]
91 passphrase: None,
92 }
93 }
94
95 #[cfg(feature = "srt-encrypt")]
98 pub fn with_passphrase(mut self, passphrase: impl Into<String>) -> Self {
99 self.passphrase = Some(passphrase.into());
100 self
101 }
102
103 #[cfg(feature = "srt-encrypt")]
106 fn answer(
107 &self,
108 datagram: &[u8],
109 km: &mut Option<keymaterial::KeyMaterial>,
110 ) -> Option<Vec<u8>> {
111 match &self.passphrase {
112 Some(pass) => {
113 let (reply, got) = handshake::respond_with_km(datagram, pass.as_bytes())?;
114 if got.is_some() {
115 *km = got;
116 }
117 Some(reply)
118 }
119 None => handshake::respond(datagram),
120 }
121 }
122
123 #[cfg(not(feature = "srt-encrypt"))]
125 fn answer(&self, datagram: &[u8]) -> Option<Vec<u8>> {
126 handshake::respond(datagram)
127 }
128}
129
130fn publish_payload(demux: &mut TsDemuxer, sess: &PublishSession, payload: &[u8]) -> Result<()> {
132 for au in demux.push(payload) {
133 if au.codec == CodecId::Unknown {
134 continue;
135 }
136 let pts = au.pts_ms;
137 let mut frame = match au.kind {
138 TsTrackKind::Video => MediaFrame::new_video(pts, pts, au.data, au.codec, au.keyframe),
139 TsTrackKind::Audio => MediaFrame::new_audio(pts, au.data, au.codec),
140 };
141 if au.is_config {
142 frame.flags |= crate::FrameFlags::CONFIG;
143 }
144 sess.publish_frame(frame)?;
145 }
146 Ok(())
147}
148
149#[async_trait]
150impl InboundProtocol for SrtHandler {
151 fn name(&self) -> &'static str {
152 "srt"
153 }
154
155 async fn serve(&self, ctx: IngestContext, shutdown: CancellationToken) -> Result<()> {
156 use tokio::net::UdpSocket;
157
158 let socket = UdpSocket::bind(self.bind).await?;
159 info!(bind = %self.bind, "srt listener bound");
160 let mut buf = vec![0u8; 1500];
161 let mut peer: Option<SocketAddr> = None;
162 let mut caller_id: u32 = 0;
163 let mut requested_sid: Option<String> = None;
166 let mut session: Option<PublishSession> = None;
167 let mut demux = TsDemuxer::new();
168 let mut receiver = Receiver::new(ARQ_WINDOW);
169 let mut ack_no: u32 = 0;
170 let start = Instant::now();
171 let mut control = tokio::time::interval(CONTROL_INTERVAL);
172 control.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
173 #[cfg(feature = "srt-encrypt")]
174 let mut key_material: Option<keymaterial::KeyMaterial> = None;
175
176 loop {
177 tokio::select! {
178 _ = shutdown.cancelled() => break,
179
180 _ = control.tick() => {
183 if let Some(p) = peer {
184 let ts = start.elapsed().as_micros() as u32;
185 if let Some(ack) = receiver.ack_seq() {
186 let _ = socket.send_to(&arq::build_ack(ack_no, ack, ts, caller_id), p).await;
187 ack_no = ack_no.wrapping_add(1);
188 }
189 let missing = receiver.missing();
190 if !missing.is_empty() {
191 let _ = socket.send_to(&arq::build_nak(&missing, ts, caller_id), p).await;
192 }
193 if let Some(sess) = session.as_ref() {
194 for ready in receiver.relieve() {
195 publish_payload(&mut demux, sess, &ready)?;
196 }
197 }
198 }
199 }
200
201 r = socket.recv_from(&mut buf) => {
202 let (n, from) = match r {
203 Ok(v) => v,
204 Err(e) => { warn!(error = %e, "srt recv failed"); continue; }
205 };
206 let datagram = &buf[..n];
207 let Some(pkt) = SrtPacket::parse(datagram) else { continue; };
208
209 match pkt {
210 SrtPacket::Control { control_type, .. } => {
211 if control_type == ControlType::Handshake {
214 #[cfg(feature = "srt-encrypt")]
215 let reply = self.answer(datagram, &mut key_material);
216 #[cfg(not(feature = "srt-encrypt"))]
217 let reply = self.answer(datagram);
218 if let Some(reply) = reply {
219 let _ = socket.send_to(&reply, from).await;
220 peer = Some(from);
221 if let Some(h) = SrtHandshake::parse(datagram) {
222 caller_id = h.socket_id;
223 }
224 if let Some(sid) = handshake::stream_id(datagram) {
227 requested_sid = Some(sid);
228 }
229 debug!(%from, "srt handshake answered");
230 }
231 }
232 }
233 SrtPacket::Data { sequence, key_flag, payload_offset, .. } => {
234 if peer != Some(from) {
235 continue; }
237 if session.is_none() {
238 let key = match &requested_sid {
241 Some(sid) => {
242 StreamKey::new(self.key.app.clone(), sid.as_str())
243 }
244 None => self.key.clone(),
245 };
246 session = Some(ctx.open_publish(key).await?);
247 }
248 let sess = session.as_ref().unwrap();
249 let payload = datagram[payload_offset..].to_vec();
250 #[cfg(feature = "srt-encrypt")]
253 let payload = {
254 let mut payload = payload;
255 if key_flag != 0 {
256 match &key_material {
257 Some(km) => km.transform(sequence, &mut payload),
258 None => continue, }
260 }
261 payload
262 };
263 #[cfg(not(feature = "srt-encrypt"))]
264 if key_flag != 0 {
265 continue; }
267 for ready in receiver.push(sequence, payload) {
269 publish_payload(&mut demux, sess, &ready)?;
270 }
271 }
272 }
273 }
274 }
275 }
276
277 if let Some(sess) = session {
278 sess.finish().await?;
279 }
280 Ok(())
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn handler_reports_name_and_key() {
290 let h = SrtHandler::new(
291 "127.0.0.1:9000".parse().unwrap(),
292 StreamKey::new("live", "feed"),
293 );
294 assert_eq!(h.name(), "srt");
295 assert_eq!(h.key.stream_id.as_str(), "feed");
296 }
297
298 use std::collections::HashSet;
299 use std::time::Duration;
300 use tokio::net::UdpSocket;
301 use tokio::sync::mpsc;
302 use tokio::time::timeout;
303
304 #[tokio::test]
308 async fn arq_recovers_dropped_packets() {
309 let listener = UdpSocket::bind("127.0.0.1:0").await.unwrap();
310 let addr = listener.local_addr().unwrap();
311 let (tx, rx) = mpsc::channel(32);
312 let shutdown = CancellationToken::new();
313 let caller_task = tokio::spawn(SrtCaller::new(addr).run(rx, shutdown.clone()));
314
315 let mut buf = [0u8; 2048];
317 let (n, peer) = listener.recv_from(&mut buf).await.unwrap();
318 let dest = SrtHandshake::parse(&buf[..n]).unwrap().socket_id;
319 listener
320 .send_to(&handshake::respond(&buf[..n]).unwrap(), peer)
321 .await
322 .unwrap();
323 let (n, peer) = listener.recv_from(&mut buf).await.unwrap();
324 listener
325 .send_to(&handshake::respond(&buf[..n]).unwrap(), peer)
326 .await
327 .unwrap();
328
329 let count = 12usize;
331 for i in 0..count {
332 tx.send(bytes::Bytes::from(vec![i as u8; 100]))
333 .await
334 .unwrap();
335 }
336
337 let mut receiver = Receiver::new(64);
338 let mut delivered: Vec<Vec<u8>> = Vec::new();
339 let mut dropped_once: HashSet<u32> = HashSet::new();
340 while delivered.len() < count {
341 let (n, from) = timeout(Duration::from_secs(5), listener.recv_from(&mut buf))
342 .await
343 .expect("packet within timeout")
344 .unwrap();
345 let SrtPacket::Data {
346 sequence,
347 payload_offset,
348 ..
349 } = SrtPacket::parse(&buf[..n]).unwrap()
350 else {
351 continue;
352 };
353 if (sequence == 3 || sequence == 8) && dropped_once.insert(sequence) {
355 let nak = arq::build_nak(&[(sequence, sequence)], 0, dest);
356 listener.send_to(&nak, from).await.unwrap();
357 continue;
358 }
359 let payload = buf[payload_offset..n].to_vec();
360 delivered.extend(receiver.push(sequence, payload));
361 }
362
363 shutdown.cancel();
364 let _ = caller_task.await;
365
366 let expected: Vec<Vec<u8>> = (0..count).map(|i| vec![i as u8; 100]).collect();
367 assert_eq!(delivered, expected, "every payload recovered, in order");
368 assert_eq!(dropped_once.len(), 2, "both losses were actually injected");
369 }
370
371 #[cfg(feature = "srt-encrypt")]
374 #[tokio::test]
375 async fn encrypted_caller_payload_decrypts_with_recovered_km() {
376 let pass = "swordfish-correct-horse";
377 let listener = UdpSocket::bind("127.0.0.1:0").await.unwrap();
378 let addr = listener.local_addr().unwrap();
379 let (tx, rx) = mpsc::channel(8);
380 let shutdown = CancellationToken::new();
381 let caller_task = tokio::spawn(
382 SrtCaller::new(addr)
383 .with_passphrase(pass)
384 .run(rx, shutdown.clone()),
385 );
386
387 let mut buf = [0u8; 2048];
388 let (n, peer) = listener.recv_from(&mut buf).await.unwrap();
390 let (reply, none) = handshake::respond_with_km(&buf[..n], pass.as_bytes()).unwrap();
391 assert!(none.is_none(), "induction carries no key material");
392 listener.send_to(&reply, peer).await.unwrap();
393 let (n, peer) = listener.recv_from(&mut buf).await.unwrap();
395 let (reply, km) = handshake::respond_with_km(&buf[..n], pass.as_bytes()).unwrap();
396 let km = km.expect("conclusion KMREQ yields key material");
397 assert!(handshake::respond_with_km(&buf[..n], b"wrong").is_none());
399 listener.send_to(&reply, peer).await.unwrap();
400
401 let plain = vec![0x47u8; TS_BYTES_PER_DATAGRAM_TEST];
402 tx.send(bytes::Bytes::from(plain.clone())).await.unwrap();
403
404 let (n, _) = timeout(Duration::from_secs(5), listener.recv_from(&mut buf))
405 .await
406 .expect("data packet")
407 .unwrap();
408 let SrtPacket::Data {
409 sequence,
410 key_flag,
411 payload_offset,
412 ..
413 } = SrtPacket::parse(&buf[..n]).unwrap()
414 else {
415 panic!("expected a data packet");
416 };
417 assert_eq!(key_flag, 1, "payload flagged as even-key encrypted");
418 let mut wire = buf[payload_offset..n].to_vec();
419 assert_ne!(wire, plain, "payload is ciphertext on the wire");
420 km.transform(sequence, &mut wire);
421 assert_eq!(wire, plain, "recovered key material decrypts the payload");
422
423 shutdown.cancel();
424 let _ = caller_task.await;
425 }
426
427 #[cfg(feature = "srt-encrypt")]
429 const TS_BYTES_PER_DATAGRAM_TEST: usize = 7 * 188;
430}