Skip to main content

media_pp/elements/source/
rtsp_source.rs

1use std::{sync::Arc, time::Duration};
2
3use crate::pp_log::{PpLog, pp_error, pp_info};
4use ffmpeg_next as ffmpeg;
5use thiserror::Error as ThisError;
6
7use crate::{
8    buffer::MediaBuffer,
9    bus::{Bus, BusEvent},
10    control::{ControlReceiver, drain_control},
11    element::{Element, ElementType, Source, SourceElement, element_pp_log},
12    elements::RtspTransport,
13    error::Result,
14    pad::SrcPad,
15};
16
17use super::file_demuxer::StreamInfo;
18
19/// Errors specific to `RtspSource`. Converts into the crate-wide `Error`
20/// via `?` (see [`crate::error::Error`]).
21#[derive(Debug, ThisError)]
22pub enum RtspSourceError {
23    /// FFmpeg rejected connection setup, stream reading, or shutdown.
24    #[error("ffmpeg error: {0}")]
25    Ffmpeg(#[from] ffmpeg::Error),
26    /// Seeking was requested on a live RTSP stream.
27    #[error("RtspSource doesn't support seeking a live stream")]
28    SeekUnsupported,
29}
30
31/// Construction-time options for [`RtspSource::open`].
32#[derive(Debug, Clone, Copy)]
33pub struct RtspOptions {
34    /// Transport used for RTSP media delivery.
35    pub transport: RtspTransport,
36    /// Socket I/O timeout — ffmpeg's own `timeout` RTSP demuxer option,
37    /// which covers the initial connect/handshake reads too, not just
38    /// steady-state ones. Without this, ffmpeg's own default is *no
39    /// timeout at all*, meaning [`RtspSource::open`] can hang forever
40    /// against an unreachable or dead server.
41    pub timeout: Duration,
42}
43
44impl Default for RtspOptions {
45    fn default() -> Self {
46        Self {
47            transport: RtspTransport::Tcp,
48            timeout: Duration::from_secs(5),
49        }
50    }
51}
52
53/// Demuxes a live RTSP stream — the client/receive counterpart to
54/// [`crate::elements::RtspSink`] (which publishes). One src pad per
55/// stream the server advertises, same shape as
56/// [`crate::elements::FileDemuxer`].
57///
58/// Deliberately does **not** retry or reconnect internally: a read failure
59/// (dropped connection, camera reboot, ...) ends this source's thread with
60/// `Err`, the same way any other fatal [`SourceElement::run`] failure
61/// does, instead of looping forever inside `run()`. Reconnecting means
62/// building a fresh `RtspSource`/[`crate::pipeline::Pipeline`] — mirrors
63/// `Pipeline` itself not being reusable once it ends: watch
64/// [`crate::pipeline::Pipeline::bus`], and on error, call
65/// [`RtspSource::open`] again.
66///
67/// Uses `Packet::read` directly instead of `Input::packets()` — the
68/// latter silently retries forever inside its own `next()` on any non-EOF
69/// error (network timeout, connection reset, ...), which would make a
70/// stuck connection un-`Stop`-able (`drain_control` never gets a turn)
71/// and this element's "fail fast, don't retry" contract impossible to
72/// keep.
73pub struct RtspSource {
74    pp_log: PpLog,
75    name: Arc<str>,
76    input: ffmpeg::format::context::Input,
77    pads: Vec<SrcPad>,
78}
79
80impl RtspSource {
81    /// Connects to `url` (e.g. `rtsp://host:port/path`) and returns the
82    /// element alongside every stream the server advertised, so the
83    /// caller can inspect them before deciding which of `src_pads()` to
84    /// link — same pattern as `FileDemuxer::open`.
85    pub fn open(
86        name: impl Into<String>,
87        url: impl AsRef<str>,
88        options: RtspOptions,
89    ) -> std::result::Result<(Self, Vec<StreamInfo>), RtspSourceError> {
90        let mut dict = ffmpeg::Dictionary::new();
91        dict.set("rtsp_transport", options.transport.as_ffmpeg_option());
92        dict.set("timeout", &options.timeout.as_micros().to_string());
93
94        let input = ffmpeg::format::input_with_dictionary(url.as_ref(), dict)?;
95
96        let streams: Vec<StreamInfo> = input
97            .streams()
98            .map(|s| StreamInfo {
99                index: s.index(),
100                kind: s.parameters().medium(),
101            })
102            .collect();
103
104        let pads = streams
105            .iter()
106            .map(|s| SrcPad::new(format!("src_{}", s.index)))
107            .collect();
108
109        let name: Arc<str> = name.into().into();
110        let pp_log = element_pp_log(ElementType::RtspSource, &name, None);
111        pp_info!(
112            pp_log: &pp_log,
113            "opened: url={}, transport={:?}, {} stream(s)",
114            url.as_ref(),
115            options.transport,
116            streams.len()
117        );
118        Ok((
119            Self {
120                name,
121                pp_log,
122                input,
123                pads,
124            },
125            streams,
126        ))
127    }
128
129    /// Codec parameters for one of this stream's streams — what you need
130    /// to construct a matching [`crate::elements::SwDecoder`] for it.
131    pub fn stream_parameters(&self, index: usize) -> Option<ffmpeg::codec::Parameters> {
132        self.stream(index).map(|s| s.parameters())
133    }
134
135    /// The unit decoded frame timestamps for this stream are expressed in
136    /// — what you need to construct a matching [`crate::elements::Pacer`]
137    /// for it.
138    pub fn stream_time_base(&self, index: usize) -> Option<ffmpeg::Rational> {
139        self.stream(index).map(|s| s.time_base())
140    }
141
142    fn stream(&self, index: usize) -> Option<ffmpeg::format::stream::Stream<'_>> {
143        self.input.streams().find(|s| s.index() == index)
144    }
145}
146
147impl Element for RtspSource {
148    fn name(&self) -> Arc<str> {
149        self.name.clone()
150    }
151
152    fn element_type(&self) -> ElementType {
153        ElementType::RtspSource
154    }
155
156    fn pp_log(&self) -> &PpLog {
157        &self.pp_log
158    }
159
160    fn pp_log_mut(&mut self) -> &mut PpLog {
161        &mut self.pp_log
162    }
163}
164
165impl Source for RtspSource {
166    fn src_pads(&mut self) -> &mut [SrcPad] {
167        &mut self.pads
168    }
169}
170
171impl SourceElement for RtspSource {
172    fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<()> {
173        pp_info!(self, "started");
174        loop {
175            if drain_control(control, self, bus)?.stopped {
176                pp_info!(self, "stopped");
177                return Ok(());
178            }
179
180            let mut packet = ffmpeg::Packet::empty();
181            match packet.read(&mut self.input) {
182                Ok(()) => {
183                    let index = packet.stream();
184                    if let Some(pad) = self.pads.get_mut(index) {
185                        // A downstream failure drops just this one packet
186                        // — same "report, don't die" contract `Queue`'s
187                        // worker gives a failing `Sink` — rather than
188                        // ending this whole source thread over it.
189                        if let Err(error) = pad.push(MediaBuffer::Packet(Arc::new(packet))) {
190                            bus.post(
191                                &self.pp_log,
192                                BusEvent::Error {
193                                    element_type: ElementType::RtspSource,
194                                    name: self.name.clone(),
195                                    error,
196                                },
197                            );
198                        }
199                    }
200                }
201                // A real on-demand RTSP stream can send a clean EOF; a
202                // live camera essentially never will, but treat it the
203                // same way `FileDemuxer` treats running out of packets.
204                Err(ffmpeg::Error::Eof) => break,
205                // Anything else (connection reset, socket timeout, ...) is
206                // fatal — reported and this thread ends, rather than
207                // retried. See this type's own docs on why: retrying
208                // belongs to whoever's watching the bus, building a fresh
209                // `RtspSource` to reconnect with.
210                Err(error) => {
211                    pp_error!(self, "read failed: {error}");
212                    return Err(RtspSourceError::Ffmpeg(error).into());
213                }
214            }
215        }
216        for pad in self.pads.iter_mut() {
217            pad.push_eos(&self.pp_log)?;
218        }
219        pp_info!(self, "event=eos phase=source_completed outcome=ok");
220        Ok(())
221    }
222
223    fn seek(&mut self, _target: Duration) -> Result<Duration> {
224        Err(RtspSourceError::SeekUnsupported.into())
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use std::time::Instant;
231
232    use super::*;
233
234    /// `RtspOptions::timeout`'s whole reason for existing: without it
235    /// ffmpeg applies *no* timeout at all and `open` blocks forever against
236    /// a server that never answers. `192.0.2.1` is RFC 5737 TEST-NET-1,
237    /// reserved for documentation and guaranteed not to be routed, so this
238    /// exercises the "no answer" path rather than a fast connection refusal.
239    ///
240    /// Only the upper bound is asserted: a network that replies with an ICMP
241    /// unreachable makes this fail even sooner, which is equally correct. The
242    /// bound sits well under the OS-level TCP connect timeout (~21s on
243    /// Windows, far longer on Linux), so a regression that stops passing the
244    /// option through is what actually trips it.
245    #[test]
246    fn open_gives_up_within_the_configured_timeout_instead_of_hanging() {
247        let options = RtspOptions {
248            timeout: Duration::from_millis(500),
249            ..Default::default()
250        };
251
252        let started = Instant::now();
253        let result = RtspSource::open("rtsp", "rtsp://192.0.2.1:554/none", options);
254        let elapsed = started.elapsed();
255
256        assert!(
257            result.is_err(),
258            "opening an unroutable address must not succeed"
259        );
260        assert!(
261            elapsed < Duration::from_secs(10),
262            "open took {elapsed:?} — the configured timeout is not reaching ffmpeg"
263        );
264    }
265
266    /// A caller that never touches `RtspOptions` still has to get a bounded
267    /// `open`, since the default this type supplies is the only thing
268    /// standing between them and ffmpeg's unbounded one.
269    #[test]
270    fn the_default_options_still_bound_the_connection() {
271        let options = RtspOptions::default();
272
273        assert_eq!(options.transport, RtspTransport::Tcp);
274        assert!(
275            options.timeout > Duration::ZERO,
276            "the default timeout must not be ffmpeg's unbounded one"
277        );
278    }
279}