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}