Skip to main content

media_pp/elements/source/
file_demuxer.rs

1use std::{path::Path, 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    pad::SrcPad,
13};
14
15/// Errors specific to `FileDemuxer`. Converts into the crate-wide `Error`
16/// via `?` (see [`crate::error::Error`]).
17#[derive(Debug, ThisError)]
18pub enum FileDemuxError {
19    /// FFmpeg rejected opening, reading, or seeking the input container.
20    #[error("ffmpeg error: {0}")]
21    Ffmpeg(#[from] ffmpeg::Error),
22}
23
24/// Metadata about one stream in an opened container, reported up front so
25/// callers can decide what to build downstream before the pipeline runs.
26#[derive(Debug, Clone, Copy)]
27pub struct StreamInfo {
28    /// Zero-based stream index used by the matching source pad.
29    pub index: usize,
30    /// Media kind reported by the container, such as audio or video.
31    pub kind: ffmpeg::media::Type,
32}
33
34/// Demuxes a file, exposing one src pad per container stream (indexed the
35/// same way as `StreamInfo::index`). Linking a pad "selects" that stream;
36/// leaving it unlinked just drops its packets. Real demuxer I/O is
37/// blocking, so this is meant to be run as the pipeline's source thread.
38///
39/// Fan-out (e.g. routing video and audio to separate branches) needs no
40/// separate "Tee" element here — it's just a matter of linking more than
41/// one of these pads.
42pub struct FileDemuxer {
43    pp_log: PpLog,
44    name: Arc<str>,
45    input: ffmpeg::format::context::Input,
46    pads: Vec<SrcPad>,
47    /// One packet read ahead of `run`'s own loop, set only by `seek` —
48    /// peeking a packet right after `Input::seek` is how it learns where
49    /// playback actually landed (see `seek`'s docs), and that packet still
50    /// needs to be delivered, not discarded, so it's stashed here for
51    /// `run`'s next iteration to pick up instead of reading a fresh one.
52    pending: Option<(usize, ffmpeg::Packet)>,
53}
54
55impl FileDemuxer {
56    /// Opens the file and returns it alongside every stream it contains,
57    /// so the caller can inspect them (count, media type, ...) before
58    /// deciding which of `src_pads()` to link.
59    pub fn open(
60        name: impl Into<String>,
61        path: impl AsRef<Path>,
62    ) -> Result<(Self, Vec<StreamInfo>), FileDemuxError> {
63        let input = ffmpeg::format::input(&path)?;
64
65        let streams: Vec<StreamInfo> = input
66            .streams()
67            .map(|s| StreamInfo {
68                index: s.index(),
69                kind: s.parameters().medium(),
70            })
71            .collect();
72
73        let pads = streams
74            .iter()
75            .map(|s| SrcPad::new(format!("src_{}", s.index)))
76            .collect();
77
78        let name: Arc<str> = name.into().into();
79        let pp_log = element_pp_log(ElementType::FileDemuxer, &name, None);
80        pp_info!(
81            pp_log: &pp_log,
82            "opened: path={}, {} stream(s)",
83            path.as_ref().display(),
84            streams.len()
85        );
86        Ok((
87            Self {
88                name,
89                pp_log,
90                input,
91                pads,
92                pending: None,
93            },
94            streams,
95        ))
96    }
97
98    /// Codec parameters for one of this file's streams — what you need to
99    /// construct a matching [`crate::elements::SwDecoder`] for it.
100    pub fn stream_parameters(&self, index: usize) -> Option<ffmpeg::codec::Parameters> {
101        self.stream(index).map(|s| s.parameters())
102    }
103
104    /// The unit decoded frame timestamps for this stream are expressed in —
105    /// what you need to construct a matching [`crate::elements::Pacer`] for
106    /// it.
107    pub fn stream_time_base(&self, index: usize) -> Option<ffmpeg::Rational> {
108        self.stream(index).map(|s| s.time_base())
109    }
110
111    fn stream(&self, index: usize) -> Option<ffmpeg::format::stream::Stream<'_>> {
112        self.input.streams().find(|s| s.index() == index)
113    }
114}
115
116impl Element for FileDemuxer {
117    fn name(&self) -> Arc<str> {
118        self.name.clone()
119    }
120
121    fn element_type(&self) -> ElementType {
122        ElementType::FileDemuxer
123    }
124
125    fn pp_log(&self) -> &PpLog {
126        &self.pp_log
127    }
128
129    fn pp_log_mut(&mut self) -> &mut PpLog {
130        &mut self.pp_log
131    }
132}
133
134impl Source for FileDemuxer {
135    fn src_pads(&mut self) -> &mut [SrcPad] {
136        &mut self.pads
137    }
138}
139
140impl SourceElement for FileDemuxer {
141    fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> crate::error::Result<()> {
142        pp_info!(self, "started");
143        // Deliberately re-creates `self.input.packets()` fresh every
144        // iteration (cheap — it's just a short-lived wrapper, not a
145        // stateful cursor of its own) instead of holding one `for` loop's
146        // iterator across the whole function, the way this used to read.
147        // That iterator borrows `input` for as long as it's alive; `Seek`
148        // needs `drain_control` to be able to call `self.seek()` — a
149        // *second* mutable borrow of `input` — in between reads, which a
150        // single loop-spanning iterator would rule out.
151        loop {
152            if drain_control(control, self, bus)?.stopped {
153                // Stop: abandon in place, no final Eos.
154                pp_info!(self, "stopped");
155                return Ok(());
156            }
157            // `seek` (called from within `drain_control`, above) already
158            // consumed one packet to find out where it landed — deliver
159            // that before reading a fresh one, or it'd be silently lost.
160            let next = match self.pending.take() {
161                Some(next) => Some(next),
162                None => self.input.packets().next().map(|(s, p)| (s.index(), p)),
163            };
164            let Some((index, packet)) = next else {
165                break;
166            };
167            if let Some(pad) = self.pads.get_mut(index) {
168                // A downstream failure drops just this one packet — same
169                // "report, don't die" contract `Queue`'s worker gives a
170                // failing `Sink` — rather than ending this whole source
171                // thread over it. `Pipeline::stop` is how a caller who
172                // decides an error is fatal actually ends things.
173                if let Err(error) = pad.push(MediaBuffer::Packet(Arc::new(packet))) {
174                    bus.post(
175                        &self.pp_log,
176                        BusEvent::Error {
177                            element_type: ElementType::FileDemuxer,
178                            name: self.name.clone(),
179                            error,
180                        },
181                    );
182                }
183            }
184        }
185        for pad in self.pads.iter_mut() {
186            pad.push_eos(&self.pp_log)?;
187        }
188        pp_info!(self, "event=eos phase=source_completed outcome=ok");
189        Ok(())
190    }
191
192    fn seek(&mut self, target: Duration) -> crate::error::Result<Duration> {
193        // `Input::seek` takes microseconds (`AV_TIME_BASE` units) when
194        // seeking the whole container (stream index -1, which is what it
195        // uses internally) rather than one specific stream — an unbounded
196        // range (`..`) just means "as close to `ts` as ffmpeg can manage",
197        // no extra min/max constraint. In practice that means *backward*
198        // to the nearest keyframe at or before `target`: never forward,
199        // and never onto a non-keyframe, since either would leave nothing
200        // downstream can decode/remux from. A sparse-keyframe file can
201        // make that keyframe well before `target` — e.g. a single
202        // 10-second file with keyframes only at 0s and 8.3s means every
203        // `target` under 8.3s lands back at 0s.
204        let ts = target.as_micros().min(i64::MAX as u128) as i64;
205        self.input.seek(ts, ..).inspect_err(|error| {
206            pp_error!(self, "seek to {target:?} failed: {error}");
207        })?;
208
209        // `avformat_seek_file` only reports success/failure, not where it
210        // landed — the one way to find out is to read the next packet and
211        // look at its own timestamp. That packet is real data (not a
212        // probe to throw away), so it's stashed in `pending` for `run`'s
213        // next iteration instead of being dropped here.
214        match self.input.packets().next() {
215            Some((stream, packet)) => {
216                let time_base = stream.time_base();
217                let landed = packet
218                    .pts()
219                    .or_else(|| packet.dts())
220                    .map(|ts| ts_to_duration(ts, time_base))
221                    .unwrap_or(Duration::ZERO);
222                self.pending = Some((stream.index(), packet));
223                Ok(landed)
224            }
225            // Nothing left to read right after seeking (`target` at/past
226            // EOF) — there's no packet to learn a real position from, so
227            // just report the request back as-is.
228            None => Ok(target),
229        }
230    }
231}
232
233fn ts_to_duration(ts: i64, time_base: ffmpeg::Rational) -> Duration {
234    let secs = ts as f64 * f64::from(time_base.numerator()) / f64::from(time_base.denominator());
235    Duration::from_secs_f64(secs.max(0.0))
236}
237
238#[cfg(test)]
239mod tests {
240    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
241
242    use super::*;
243    use crate::control;
244    use crate::test_support::try_test_video;
245
246    struct CountingSink {
247        pp_log: PpLog,
248        count: Arc<AtomicUsize>,
249        saw_eos: Arc<AtomicBool>,
250    }
251
252    impl Element for CountingSink {
253        fn name(&self) -> Arc<str> {
254            "counting-sink".into()
255        }
256
257        fn element_type(&self) -> ElementType {
258            ElementType::Other
259        }
260
261        fn pp_log(&self) -> &PpLog {
262            &self.pp_log
263        }
264
265        fn pp_log_mut(&mut self) -> &mut PpLog {
266            &mut self.pp_log
267        }
268    }
269
270    impl crate::element::Sink for CountingSink {
271        fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
272            match buf {
273                MediaBuffer::Eos => self.saw_eos.store(true, Ordering::SeqCst),
274                _ => {
275                    self.count.fetch_add(1, Ordering::SeqCst);
276                }
277            }
278            Ok(())
279        }
280
281        fn control(&mut self, _msg: crate::control::ControlMsg) -> crate::error::Result<()> {
282            Ok(())
283        }
284    }
285
286    #[test]
287    fn open_reports_stream_parameters_for_a_valid_index_and_none_out_of_range() {
288        let Some(path) = try_test_video() else { return };
289        let (demuxer, streams) = FileDemuxer::open("demux", &path).expect("open test video");
290        let video = streams
291            .iter()
292            .find(|s| s.kind == ffmpeg::media::Type::Video)
293            .expect("test video has a video stream");
294
295        assert!(demuxer.stream_parameters(video.index).is_some());
296        assert!(demuxer.stream_time_base(video.index).is_some());
297
298        let out_of_range = streams.len() + 1;
299        assert!(
300            demuxer.stream_parameters(out_of_range).is_none(),
301            "an out-of-range stream index must report nothing, not panic"
302        );
303        assert!(demuxer.stream_time_base(out_of_range).is_none());
304    }
305
306    /// Drives `FileDemuxer::run` directly (no `Pipeline`) to prove the
307    /// basic contract on its own: every packet on a linked pad's stream
308    /// arrives, and running off the end of the file delivers a final
309    /// `Eos` rather than just stopping silently.
310    #[test]
311    fn run_delivers_every_packet_on_a_linked_pad_then_eos() {
312        let Some(path) = try_test_video() else { return };
313        let (mut demuxer, streams) = FileDemuxer::open("demux", &path).expect("open test video");
314        let video = streams
315            .iter()
316            .find(|s| s.kind == ffmpeg::media::Type::Video)
317            .expect("test video has a video stream");
318
319        let count = Arc::new(AtomicUsize::new(0));
320        let saw_eos = Arc::new(AtomicBool::new(false));
321        demuxer.src_pads()[video.index].link(Box::new(CountingSink {
322            count: count.clone(),
323            saw_eos: saw_eos.clone(),
324            pp_log: element_pp_log(ElementType::Other, "counting-sink", None),
325        }));
326
327        let (bus, bus_rx) = Bus::new();
328        let (_tx, rx) = control::channel();
329        demuxer
330            .run(&rx, &bus)
331            .expect("run must reach eos cleanly, not error");
332
333        assert!(
334            count.load(Ordering::SeqCst) > 0,
335            "expected at least one packet delivered to the linked pad"
336        );
337        assert!(
338            saw_eos.load(Ordering::SeqCst),
339            "expected an Eos once the file is exhausted"
340        );
341        drop(bus);
342        assert!(
343            bus_rx.iter().all(|e| !matches!(e, BusEvent::Error { .. })),
344            "run must not report any errors demuxing a well-formed file"
345        );
346    }
347}