Skip to main content

arcly_stream/protocol/srt/
egress.rs

1//! SRT **caller egress** — dial an SRT listener and push an MPEG-TS feed.
2//!
3//! The egress counterpart to the [listener ingest](super::SrtHandler):
4//! [`SrtCaller`] connects to a remote SRT listener (`srt://host:port` in caller
5//! mode), runs the caller handshake (`INDUCTION` → `CONCLUSION`), then wraps an
6//! MPEG-TS byte stream into SRT data packets and sends them.
7//!
8//! The transport stays **MPEG-TS-agnostic of the engine**: the caller consumes
9//! already-muxed TS bytes from an [`mpsc`] channel, so the host pairs it with a
10//! [`MpegTsMuxer`](crate::packager::MpegTsMuxer) to get a full frame → SRT path
11//! without coupling this module to the packager.
12//!
13//! Scope: unencrypted, and the minimal HSv5 handshake (no `HSREQ`/`KMREQ`
14//! extension blocks) — it interoperates with this crate's own listener; full
15//! extension interop with third-party SRT stacks is a follow-up.
16
17use std::net::SocketAddr;
18use std::time::{Duration, Instant};
19
20use tokio::net::UdpSocket;
21use tokio::sync::mpsc;
22use tokio::time::timeout;
23use tokio_util::sync::CancellationToken;
24use tracing::{debug, info};
25
26use super::arq::{self, SendBuffer};
27use super::handshake::{caller_conclusion, caller_induction, SrtHandshake};
28use super::packet::{build_data_packet, ControlType};
29use crate::{Result, StreamError};
30
31/// 7 × 188-byte TS packets = the classic 1316-byte SRT payload.
32const TS_BYTES_PER_DATAGRAM: usize = 7 * 188;
33/// Bound the handshake so a dead listener can't wedge the caller.
34const SETUP_TIMEOUT: Duration = Duration::from_secs(5);
35/// Retransmission window: packets kept available for NAK-driven replay.
36const SEND_WINDOW: usize = 2048;
37
38/// Pushes an MPEG-TS feed to a remote SRT listener in caller mode.
39pub struct SrtCaller {
40    addr: SocketAddr,
41    socket_id: u32,
42    /// Passphrase enabling AES-CTR encryption (feature `srt-encrypt`).
43    #[cfg(feature = "srt-encrypt")]
44    passphrase: Option<String>,
45    /// SEK length in bytes (16/24/32) when encrypting (feature `srt-encrypt`).
46    #[cfg(feature = "srt-encrypt")]
47    key_len: usize,
48}
49
50impl SrtCaller {
51    /// A caller targeting the listener at `addr`.
52    pub fn new(addr: SocketAddr) -> Self {
53        // A non-zero, stable-ish local socket id (real callers randomize).
54        let socket_id = (0x4152_434C ^ (addr.port() as u32).rotate_left(16)) | 1;
55        Self {
56            addr,
57            socket_id,
58            #[cfg(feature = "srt-encrypt")]
59            passphrase: None,
60            #[cfg(feature = "srt-encrypt")]
61            key_len: 16,
62        }
63    }
64
65    /// Encrypt the feed with AES-CTR under `passphrase` (default AES-128; pair
66    /// with [`with_key_len`](Self::with_key_len)). Feature `srt-encrypt`.
67    #[cfg(feature = "srt-encrypt")]
68    pub fn with_passphrase(mut self, passphrase: impl Into<String>) -> Self {
69        self.passphrase = Some(passphrase.into());
70        self
71    }
72
73    /// Set the AES key length in bytes (16, 24, or 32). Feature `srt-encrypt`.
74    #[cfg(feature = "srt-encrypt")]
75    pub fn with_key_len(mut self, key_len: usize) -> Self {
76        self.key_len = key_len;
77        self
78    }
79
80    /// Connect, handshake, and forward MPEG-TS chunks from `ts` until the channel
81    /// closes or `shutdown` fires. Each inbound chunk is split into
82    /// 1316-byte SRT datagrams.
83    pub async fn run(
84        self,
85        mut ts: mpsc::Receiver<bytes::Bytes>,
86        shutdown: CancellationToken,
87    ) -> Result<()> {
88        let sock = UdpSocket::bind(("0.0.0.0", 0)).await?;
89        sock.connect(self.addr).await?;
90        let mut buf = [0u8; 1500];
91
92        // INDUCTION → cookie.
93        sock.send(&caller_induction(self.socket_id, 0)).await?;
94        let n = timeout(SETUP_TIMEOUT, sock.recv(&mut buf))
95            .await
96            .map_err(|_| StreamError::protocol("srt induction timed out"))??;
97        let induction = SrtHandshake::parse(&buf[..n])
98            .ok_or_else(|| StreamError::protocol("malformed srt induction response"))?;
99
100        // Optional encryption: generate key material and send it in the
101        // CONCLUSION's KMREQ extension; the listener unwraps it with the same
102        // passphrase. Each payload is then AES-CTR'd before framing.
103        #[cfg(feature = "srt-encrypt")]
104        let key_material = self
105            .passphrase
106            .as_ref()
107            .map(|_| super::keymaterial::KeyMaterial::generate(self.key_len));
108
109        // CONCLUSION (echo the cookie, carry KMREQ if encrypting) → agreement.
110        #[cfg(feature = "srt-encrypt")]
111        let conclusion = match (&self.passphrase, &key_material) {
112            (Some(pass), Some(km)) => super::handshake::caller_conclusion_encrypted(
113                self.socket_id,
114                0,
115                induction.cookie,
116                self.key_len,
117                &km.to_message(pass.as_bytes()),
118            ),
119            _ => caller_conclusion(self.socket_id, 0, induction.cookie),
120        };
121        #[cfg(not(feature = "srt-encrypt"))]
122        let conclusion = caller_conclusion(self.socket_id, 0, induction.cookie);
123
124        sock.send(&conclusion).await?;
125        let n = timeout(SETUP_TIMEOUT, sock.recv(&mut buf))
126            .await
127            .map_err(|_| StreamError::protocol("srt conclusion timed out"))??;
128        let agreement = SrtHandshake::parse(&buf[..n])
129            .ok_or_else(|| StreamError::protocol("malformed srt conclusion response"))?;
130        // The data packets' destination is the socket id the listener returned.
131        let dest = agreement.socket_id;
132        info!(addr = %self.addr, dest, "srt caller connected");
133
134        // The key flag (`KK`) stamped on every data packet: 1 (even key) when
135        // encrypting, 0 otherwise.
136        #[cfg(feature = "srt-encrypt")]
137        let key_flag: u8 = if key_material.is_some() { 1 } else { 0 };
138        #[cfg(not(feature = "srt-encrypt"))]
139        let key_flag: u8 = 0;
140
141        let start = Instant::now();
142        let mut seq = 0u32;
143        let mut msg = 0u32;
144        let mut send_buf = SendBuffer::new(SEND_WINDOW);
145        loop {
146            tokio::select! {
147                _ = shutdown.cancelled() => break,
148
149                // Control feedback from the listener: retransmit on NAK, release
150                // the send buffer on ACK.
151                r = sock.recv(&mut buf) => {
152                    let Ok(n) = r else { continue };
153                    let dg = &buf[..n];
154                    match arq::control_type(dg) {
155                        Some(ControlType::Nak) => {
156                            for pkt in send_buf.retransmit(&arq::parse_nak(dg)) {
157                                let _ = sock.send(&pkt).await;
158                            }
159                        }
160                        Some(ControlType::Ack) => {
161                            if let Some(ack) = arq::parse_ack(dg) {
162                                send_buf.acknowledge(ack);
163                            }
164                            // ACKACK back so the listener can retire the ACK.
165                            if let Some(ack_no) = arq::ack_seqno(dg) {
166                                let ts_us = start.elapsed().as_micros() as u32;
167                                let _ = sock.send(&arq::build_ackack(ack_no, ts_us, dest)).await;
168                            }
169                        }
170                        _ => {}
171                    }
172                }
173
174                chunk = ts.recv() => match chunk {
175                    Some(bytes) => {
176                        for piece in bytes.chunks(TS_BYTES_PER_DATAGRAM) {
177                            let ts_us = start.elapsed().as_micros() as u32;
178                            let payload = piece.to_vec();
179                            #[cfg(feature = "srt-encrypt")]
180                            let mut payload = payload;
181                            #[cfg(feature = "srt-encrypt")]
182                            if let Some(km) = &key_material {
183                                km.transform(seq, &mut payload);
184                            }
185                            let pkt =
186                                build_data_packet(seq, msg, ts_us, dest, key_flag, false, &payload);
187                            if sock.send(&pkt).await.is_err() {
188                                return Ok(()); // listener gone
189                            }
190                            send_buf.record(seq, pkt);
191                            seq = seq.wrapping_add(1) & 0x7FFF_FFFF;
192                            msg = msg.wrapping_add(1) & 0x03FF_FFFF;
193                        }
194                    }
195                    None => break, // source closed
196                }
197            }
198        }
199        debug!(addr = %self.addr, "srt caller finished");
200        Ok(())
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use crate::protocol::srt::{handshake::respond, SrtPacket};
208
209    #[tokio::test]
210    async fn caller_handshakes_and_sends_data_over_loopback() {
211        // A fake listener: this crate's own `respond` for the handshake, then it
212        // captures the first data packet — a real end-to-end UDP loopback of the
213        // caller flow (handshake + data framing).
214        let listener = UdpSocket::bind("127.0.0.1:0").await.unwrap();
215        let addr = listener.local_addr().unwrap();
216
217        let (tx, rx) = mpsc::channel(4);
218        let shutdown = CancellationToken::new();
219        let sh = shutdown.clone();
220        let caller = SrtCaller::new(addr);
221        let caller_task = tokio::spawn(async move { caller.run(rx, sh).await });
222
223        let mut buf = [0u8; 1500];
224        // Induction.
225        let (n, peer) = listener.recv_from(&mut buf).await.unwrap();
226        let reply = respond(&buf[..n]).unwrap();
227        listener.send_to(&reply, peer).await.unwrap();
228        // Conclusion.
229        let (n, peer) = listener.recv_from(&mut buf).await.unwrap();
230        let reply = respond(&buf[..n]).unwrap();
231        listener.send_to(&reply, peer).await.unwrap();
232
233        // Feed one TS datagram; expect an SRT data packet on the wire.
234        tx.send(bytes::Bytes::from(vec![0x47u8; TS_BYTES_PER_DATAGRAM]))
235            .await
236            .unwrap();
237        let (n, _) = timeout(Duration::from_secs(5), listener.recv_from(&mut buf))
238            .await
239            .expect("data packet arrived")
240            .unwrap();
241        assert!(
242            matches!(SrtPacket::parse(&buf[..n]), Some(SrtPacket::Data { .. })),
243            "caller sent an SRT data packet after the handshake"
244        );
245
246        shutdown.cancel();
247        let _ = caller_task.await;
248    }
249}