Skip to main content

rtsp_runtime/
transport.rs

1//! Typed `Transport` header — RFC 2326 §12.39.
2//!
3//! Parses and serializes the RTSP `Transport` header value, per the ABNF
4//! transcribed in [`docs/transport-header.md`](../docs/transport-header.md).
5//! A single header value is a comma-separated list of transport-specs in the
6//! client's order of preference; each spec is a `transport/profile[/lower]`
7//! triple followed by semicolon-separated parameters
8//! (`unicast`/`multicast`, `interleaved=lo-hi`, `client_port=lo-hi`,
9//! `server_port=lo-hi`, `port=lo-hi`, `mode`, `ssrc`, `destination`, `source`,
10//! `ttl`, `layers`, `append`).
11//!
12//! The types are round-trippable: `parse` → [`TransportSpec::to_header_value`]
13//! preserves the transport triple and every recognised parameter.
14
15use crate::error::{Error, Result};
16
17/// Transport protocol — currently only `RTP` is defined by RFC 2326 §12.39.
18const PROTO_RTP: &str = "RTP";
19/// Profile — currently only `AVP` is defined.
20const PROFILE_AVP: &str = "AVP";
21
22/// Lower-layer transport for an RTP/AVP spec (RFC 2326 §12.39).
23///
24/// For `RTP/AVP`, the default lower-transport is UDP when omitted.
25#[non_exhaustive]
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub enum LowerTransport {
29    /// `UDP` — the default when no lower-transport token is present.
30    Udp,
31    /// `TCP` — used for interleaved (`$`-framed) delivery.
32    Tcp,
33}
34
35impl LowerTransport {
36    /// The RFC 2326 token for this lower transport.
37    pub fn name(&self) -> &'static str {
38        match self {
39            LowerTransport::Udp => "UDP",
40            LowerTransport::Tcp => "TCP",
41        }
42    }
43}
44
45impl core::fmt::Display for LowerTransport {
46    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47        f.write_str(self.name())
48    }
49}
50
51/// Delivery mode: unicast or multicast (RFC 2326 §12.39). Mutually exclusive.
52#[non_exhaustive]
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub enum Delivery {
56    /// `unicast` delivery.
57    Unicast,
58    /// `multicast` delivery (the RFC default when neither token is present).
59    Multicast,
60}
61
62impl Delivery {
63    /// The RFC 2326 token for this delivery mode.
64    pub fn name(&self) -> &'static str {
65        match self {
66            Delivery::Unicast => "unicast",
67            Delivery::Multicast => "multicast",
68        }
69    }
70}
71
72impl core::fmt::Display for Delivery {
73    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
74        f.write_str(self.name())
75    }
76}
77
78/// A single parsed transport-spec from a `Transport` header (RFC 2326 §12.39).
79///
80/// Only `RTP/AVP` (with optional `/TCP` or `/UDP`) is modelled with typed
81/// parameters. The transport triple is fixed to `RTP/AVP`; the lower transport
82/// and each recognised parameter are optional.
83#[derive(Debug, Clone, PartialEq, Eq, Default)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85pub struct TransportSpec {
86    /// Lower-layer transport. `None` means the token was absent → UDP default.
87    pub lower_transport: Option<LowerTransport>,
88    /// `unicast` / `multicast`.
89    pub delivery: Option<Delivery>,
90    /// `interleaved=lo-hi` — the `$`-framing channel pair (RFC 2326 §10.12).
91    pub interleaved: Option<(u8, u8)>,
92    /// `client_port=lo-hi` — unicast RTP/RTCP port pair chosen by the client.
93    pub client_port: Option<(u16, u16)>,
94    /// `server_port=lo-hi` — unicast RTP/RTCP port pair chosen by the server.
95    pub server_port: Option<(u16, u16)>,
96    /// `port=lo-hi` — multicast RTP/RTCP port pair.
97    pub port: Option<(u16, u16)>,
98    /// `ttl=N` — multicast time-to-live.
99    pub ttl: Option<u8>,
100    /// `layers=N` — number of multicast layers.
101    pub layers: Option<u32>,
102    /// `ssrc=HHHHHHHH` — 32-bit RTP SSRC (unicast only).
103    pub ssrc: Option<u32>,
104    /// `destination[=addr]`.
105    pub destination: Option<String>,
106    /// `source=addr`.
107    pub source: Option<String>,
108    /// `mode` — quoted or bare method(s); `PLAY` or `RECORD`.
109    pub mode: Option<String>,
110    /// `append` flag (RECORD mode).
111    pub append: bool,
112}
113
114impl TransportSpec {
115    /// A fresh RTP/AVP/TCP interleaved spec on the given channel range — the
116    /// common client SETUP for TCP tunnelling (RFC 2326 §10.12).
117    pub fn rtp_avp_tcp_interleaved(lo: u8, hi: u8) -> Self {
118        TransportSpec {
119            lower_transport: Some(LowerTransport::Tcp),
120            delivery: Some(Delivery::Unicast),
121            interleaved: Some((lo, hi)),
122            ..Default::default()
123        }
124    }
125
126    /// Parses one transport-spec (no comma) from its textual form.
127    fn parse_spec(spec: &str) -> Result<Self> {
128        let mut parts = spec.split(';');
129        let head = parts
130            .next()
131            .ok_or_else(|| Error::TransportParse("empty transport-spec".into()))?
132            .trim();
133
134        // transport-protocol / profile [ / lower-transport ]
135        let mut triple = head.split('/');
136        let proto = triple.next().unwrap_or("").trim();
137        if !proto.eq_ignore_ascii_case(PROTO_RTP) {
138            return Err(Error::TransportParse(format!(
139                "unsupported transport protocol {proto:?} (only RTP)"
140            )));
141        }
142        let profile = triple
143            .next()
144            .ok_or_else(|| Error::TransportParse("missing profile".into()))?
145            .trim();
146        if !profile.eq_ignore_ascii_case(PROFILE_AVP) {
147            return Err(Error::TransportParse(format!(
148                "unsupported profile {profile:?} (only AVP)"
149            )));
150        }
151        let lower_transport = match triple.next() {
152            None => None,
153            Some(t) => match t.trim() {
154                s if s.eq_ignore_ascii_case("TCP") => Some(LowerTransport::Tcp),
155                s if s.eq_ignore_ascii_case("UDP") => Some(LowerTransport::Udp),
156                other => {
157                    return Err(Error::TransportParse(format!(
158                        "unknown lower-transport {other:?}"
159                    )));
160                }
161            },
162        };
163
164        let mut out = TransportSpec {
165            lower_transport,
166            ..Default::default()
167        };
168
169        for raw in parts {
170            let param = raw.trim();
171            if param.is_empty() {
172                continue;
173            }
174            let (key, value) = match param.split_once('=') {
175                Some((k, v)) => (k.trim(), Some(v.trim())),
176                None => (param, None),
177            };
178            match key.to_ascii_lowercase().as_str() {
179                "unicast" => out.delivery = Some(Delivery::Unicast),
180                "multicast" => out.delivery = Some(Delivery::Multicast),
181                "append" => out.append = true,
182                "interleaved" => {
183                    out.interleaved = Some(parse_u8_range(value, "interleaved")?);
184                }
185                "client_port" => out.client_port = Some(parse_u16_range(value, "client_port")?),
186                "server_port" => out.server_port = Some(parse_u16_range(value, "server_port")?),
187                "port" => out.port = Some(parse_u16_range(value, "port")?),
188                "ttl" => out.ttl = Some(parse_scalar(value, "ttl")?),
189                "layers" => out.layers = Some(parse_scalar(value, "layers")?),
190                "ssrc" => {
191                    let v = value
192                        .ok_or_else(|| Error::TransportParse("ssrc requires a value".into()))?;
193                    out.ssrc = Some(
194                        u32::from_str_radix(v.trim_matches('"'), 16)
195                            .map_err(|e| Error::TransportParse(format!("bad ssrc {v:?}: {e}")))?,
196                    );
197                }
198                "destination" => out.destination = value.map(|s| s.trim_matches('"').to_string()),
199                "source" => out.source = value.map(|s| s.trim_matches('"').to_string()),
200                "mode" => {
201                    out.mode = value.map(|s| s.trim_matches('"').to_string());
202                }
203                // Unknown parameters are ignored per the extensible header grammar.
204                _ => {}
205            }
206        }
207        Ok(out)
208    }
209
210    /// Serializes this spec to its `Transport` header textual form (no comma).
211    pub fn to_header_value(&self) -> String {
212        let mut s = String::new();
213        s.push_str(PROTO_RTP);
214        s.push('/');
215        s.push_str(PROFILE_AVP);
216        if let Some(lt) = self.lower_transport {
217            s.push('/');
218            s.push_str(lt.name());
219        }
220        if let Some(d) = self.delivery {
221            s.push(';');
222            s.push_str(d.name());
223        }
224        if let Some(dest) = &self.destination {
225            s.push_str(";destination=");
226            s.push_str(dest);
227        }
228        if let Some(src) = &self.source {
229            s.push_str(";source=");
230            s.push_str(src);
231        }
232        if let Some((lo, hi)) = self.interleaved {
233            s.push_str(&format!(";interleaved={lo}-{hi}"));
234        }
235        if let Some(ttl) = self.ttl {
236            s.push_str(&format!(";ttl={ttl}"));
237        }
238        if let Some(layers) = self.layers {
239            s.push_str(&format!(";layers={layers}"));
240        }
241        if let Some((lo, hi)) = self.port {
242            s.push_str(&format!(";port={lo}-{hi}"));
243        }
244        if let Some((lo, hi)) = self.client_port {
245            s.push_str(&format!(";client_port={lo}-{hi}"));
246        }
247        if let Some((lo, hi)) = self.server_port {
248            s.push_str(&format!(";server_port={lo}-{hi}"));
249        }
250        if let Some(ssrc) = self.ssrc {
251            s.push_str(&format!(";ssrc={ssrc:08X}"));
252        }
253        if let Some(mode) = &self.mode {
254            s.push_str(&format!(";mode=\"{mode}\""));
255        }
256        if self.append {
257            s.push_str(";append");
258        }
259        s
260    }
261}
262
263/// A `Transport` header value: one or more transport-specs in preference order
264/// (RFC 2326 §12.39).
265#[derive(Debug, Clone, PartialEq, Eq, Default)]
266#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
267pub struct Transport {
268    /// The transport-specs, in the order they appear (preference order).
269    pub specs: Vec<TransportSpec>,
270}
271
272impl Transport {
273    /// Constructs a `Transport` from a single spec.
274    pub fn single(spec: TransportSpec) -> Self {
275        Transport { specs: vec![spec] }
276    }
277
278    /// Parses a full `Transport` header value (comma-separated specs).
279    pub fn parse(value: &str) -> Result<Self> {
280        let specs = value
281            .split(',')
282            .map(str::trim)
283            .filter(|s| !s.is_empty())
284            .map(TransportSpec::parse_spec)
285            .collect::<Result<Vec<_>>>()?;
286        if specs.is_empty() {
287            return Err(Error::TransportParse("no transport-specs".into()));
288        }
289        Ok(Transport { specs })
290    }
291
292    /// Serializes to a `Transport` header value.
293    pub fn to_header_value(&self) -> String {
294        self.specs
295            .iter()
296            .map(TransportSpec::to_header_value)
297            .collect::<Vec<_>>()
298            .join(",")
299    }
300
301    /// The first spec, if any (the negotiated/preferred transport).
302    pub fn first(&self) -> Option<&TransportSpec> {
303        self.specs.first()
304    }
305}
306
307fn parse_scalar<T: core::str::FromStr>(value: Option<&str>, what: &str) -> Result<T>
308where
309    T::Err: core::fmt::Display,
310{
311    let v = value.ok_or_else(|| Error::TransportParse(format!("{what} requires a value")))?;
312    v.trim()
313        .parse::<T>()
314        .map_err(|e| Error::TransportParse(format!("bad {what} {v:?}: {e}")))
315}
316
317fn parse_u8_range(value: Option<&str>, what: &str) -> Result<(u8, u8)> {
318    let (lo, hi) = split_range(value, what)?;
319    let lo: u8 = lo
320        .parse()
321        .map_err(|e| Error::TransportParse(format!("bad {what} low {lo:?}: {e}")))?;
322    let hi: u8 = match hi {
323        Some(h) => h
324            .parse()
325            .map_err(|e| Error::TransportParse(format!("bad {what} high {h:?}: {e}")))?,
326        None => lo,
327    };
328    Ok((lo, hi))
329}
330
331fn parse_u16_range(value: Option<&str>, what: &str) -> Result<(u16, u16)> {
332    let (lo, hi) = split_range(value, what)?;
333    let lo: u16 = lo
334        .parse()
335        .map_err(|e| Error::TransportParse(format!("bad {what} low {lo:?}: {e}")))?;
336    let hi: u16 = match hi {
337        Some(h) => h
338            .parse()
339            .map_err(|e| Error::TransportParse(format!("bad {what} high {h:?}: {e}")))?,
340        None => lo,
341    };
342    Ok((lo, hi))
343}
344
345fn split_range<'a>(value: Option<&'a str>, what: &str) -> Result<(&'a str, Option<&'a str>)> {
346    let v = value
347        .ok_or_else(|| Error::TransportParse(format!("{what} requires a value")))?
348        .trim();
349    match v.split_once('-') {
350        Some((lo, hi)) => Ok((lo.trim(), Some(hi.trim()))),
351        None => Ok((v, None)),
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn parse_tcp_interleaved_round_trip() {
361        let t = Transport::parse("RTP/AVP/TCP;interleaved=0-1").unwrap();
362        let spec = t.first().unwrap();
363        assert_eq!(spec.lower_transport, Some(LowerTransport::Tcp));
364        assert_eq!(spec.interleaved, Some((0, 1)));
365        // re-serialize and re-parse must be equal
366        let s = t.to_header_value();
367        let t2 = Transport::parse(&s).unwrap();
368        assert_eq!(t, t2);
369    }
370
371    #[test]
372    fn parse_udp_unicast_client_port_round_trip() {
373        let t = Transport::parse("RTP/AVP;unicast;client_port=8000-8001").unwrap();
374        let spec = t.first().unwrap();
375        assert_eq!(spec.lower_transport, None);
376        assert_eq!(spec.delivery, Some(Delivery::Unicast));
377        assert_eq!(spec.client_port, Some((8000, 8001)));
378        let t2 = Transport::parse(&t.to_header_value()).unwrap();
379        assert_eq!(t, t2);
380    }
381
382    #[test]
383    fn parse_fixture_setup_transport() {
384        let t = Transport::parse("RTP/AVP/TCP;unicast;interleaved=0-1").unwrap();
385        let spec = t.first().unwrap();
386        assert_eq!(spec.delivery, Some(Delivery::Unicast));
387        assert_eq!(spec.interleaved, Some((0, 1)));
388    }
389
390    #[test]
391    fn ssrc_round_trips_as_hex() {
392        let t = Transport::parse("RTP/AVP;unicast;ssrc=DEADBEEF").unwrap();
393        assert_eq!(t.first().unwrap().ssrc, Some(0xDEAD_BEEF));
394        let t2 = Transport::parse(&t.to_header_value()).unwrap();
395        assert_eq!(t, t2);
396    }
397
398    #[test]
399    fn constructed_spec_serializes_fields() {
400        // Build from typed fields (not parsed), serialize, and assert the
401        // mutated field appears — rules out a fixture-only round-trip.
402        let spec = TransportSpec {
403            interleaved: Some((2, 3)),
404            delivery: Some(Delivery::Unicast),
405            lower_transport: Some(LowerTransport::Tcp),
406            ..Default::default()
407        };
408        let t = Transport::single(spec);
409        let s = t.to_header_value();
410        assert!(s.contains("interleaved=2-3"), "serialized: {s}");
411        assert!(s.contains("unicast"), "serialized: {s}");
412        // round-trips back to the same typed value
413        let back = Transport::parse(&s).unwrap();
414        assert_eq!(back.first().unwrap().interleaved, Some((2, 3)));
415    }
416
417    #[test]
418    fn rejects_non_rtp() {
419        assert!(Transport::parse("XYZ/AVP").is_err());
420    }
421}