Skip to main content

arcly_stream/protocol/srt/
mod.rs

1//! Native SRT ingest handler (feature `srt`).
2//!
3//! Accepts SRT (Secure Reliable Transport) connections in **listener** mode,
4//! parses the control/data packet stream, and extracts the encapsulated
5//! MPEG-TS payload into elementary media frames that flow onto the engine bus
6//! through the standard [`InboundProtocol`] seam.
7//!
8//! [`InboundProtocol`]: crate::inbound::InboundProtocol
9//!
10//! # Pipeline
11//!
12//! ```text
13//!  UDP datagram ─▶ SrtPacket::parse ─▶ data payload ─▶ TsDemuxer ─▶ AccessUnit
14//!                       │                                              │
15//!                       └─ handshake (induction/conclusion)           ▼
16//!                                                              PublishSession
17//! ```
18//!
19//! The SRT [packet header][SrtPacket] (SRT RFC, draft-sharabayko-srt) and the
20//! [`SrtHandshake`] induction/conclusion exchange are parsed here; the MPEG-TS
21//! bytes ride in SRT *data* packets and are demuxed by [`TsDemuxer`].
22//!
23//! # Scope
24//!
25//! Loss recovery is modeled with a full **ARQ window**: the listener reorders by
26//! sequence number, NAKs gaps, and periodically ACKs; the caller keeps a
27//! retransmission buffer and replays NAK'd packets. With the `srt-encrypt`
28//! feature, **AES-CTR** media encryption and the `KMREQ`/`KMRSP` key-material
29//! exchange are supported via a pure-Rust crypto core — no dependency, still
30//! `#![forbid(unsafe_code)]`. Without that feature an encrypted feed is rejected
31//! at handshake.
32//!
33//! Interop note: the handshake/ARQ/KM wire formats follow draft-sharabayko-srt
34//! and interoperate with this crate's own caller/listener; exhaustive interop
35//! with third-party SRT stacks (libsrt option negotiation) is a follow-up.
36
37mod 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};
49// The MPEG-TS demuxer now lives in the shared `protocol::tsdemux` module (also
50// used by `udp` ingest); re-exported here so existing paths keep working.
51pub 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
63/// Out-of-order reorder window before a stuck gap is force-skipped.
64const ARQ_WINDOW: usize = 1024;
65/// Cadence of ACK/NAK control feedback and buffer-relief checks.
66const CONTROL_INTERVAL: Duration = Duration::from_millis(10);
67
68/// SRT ingest worker — binds a UDP listener and demuxes one MPEG-TS feed.
69#[derive(Debug, Clone)]
70pub struct SrtHandler {
71    bind: SocketAddr,
72    key: StreamKey,
73    /// Pre-shared passphrase enabling AES-CTR decryption (feature `srt-encrypt`).
74    #[cfg(feature = "srt-encrypt")]
75    passphrase: Option<String>,
76}
77
78impl SrtHandler {
79    /// A listener bound to `bind` that publishes the received feed as `key`.
80    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    /// Accept an **encrypted** feed, decrypting with `passphrase` (the SRT
90    /// `passphrase` the caller was configured with). Feature `srt-encrypt`.
91    #[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    /// Answer a handshake datagram, recovering key material on an encrypted
98    /// conclusion (feature `srt-encrypt`).
99    #[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    /// Answer a handshake datagram (plaintext build).
118    #[cfg(not(feature = "srt-encrypt"))]
119    fn answer(&self, datagram: &[u8]) -> Option<Vec<u8>> {
120        handshake::respond(datagram)
121    }
122}
123
124/// Demux a (reassembled, decrypted) TS payload and publish any access units.
125fn 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                // Periodic ARQ feedback: ACK in-order progress, NAK any gaps, and
172                // relieve a stuck reorder buffer so permanent loss can't wedge it.
173                _ = 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                            // Bring the session up on the handshake; the caller's
203                            // ACKACK and keepalives need no action here.
204                            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; // data before a handshake from this peer
222                            }
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                            // Decrypt in place (with the packet's own sequence)
229                            // before the reorder buffer, so reordering sees plaintext.
230                            #[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, // encrypted but no key yet
237                                    }
238                                }
239                                payload
240                            };
241                            #[cfg(not(feature = "srt-encrypt"))]
242                            if key_flag != 0 {
243                                continue; // encrypted feed, no crypto support built
244                            }
245                            // Reorder through the ARQ window, then demux in order.
246                            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    /// End-to-end lossy-link recovery: a caller streams packets through a test
283    /// listener that drops two of them on first sight and NAKs; the caller must
284    /// retransmit so the receiver delivers every payload, in order.
285    #[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        // Handshake: induction then conclusion, both echoed by `respond`.
294        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        // Feed 12 uniquely-tagged payloads.
308        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            // Drop seq 3 and 8 the first time they appear, and NAK them.
332            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    /// End-to-end encryption: an encrypted caller completes the KMREQ exchange,
350    /// and its on-wire payloads decrypt (only) with the recovered key material.
351    #[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        // Induction (no KM yet).
367        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        // Conclusion carries KMREQ; recover the SEK with the right passphrase.
372        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        // A wrong passphrase must fail to unwrap the very same handshake.
376        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    /// One full SRT datagram of TS payload (7 × 188).
406    #[cfg(feature = "srt-encrypt")]
407    const TS_BYTES_PER_DATAGRAM_TEST: usize = 7 * 188;
408}