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