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