media_pp/elements/source/
file_demuxer.rs1use 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#[derive(Debug, ThisError)]
18pub enum FileDemuxError {
19 #[error("ffmpeg error: {0}")]
21 Ffmpeg(#[from] ffmpeg::Error),
22}
23
24#[derive(Debug, Clone, Copy)]
27pub struct StreamInfo {
28 pub index: usize,
30 pub kind: ffmpeg::media::Type,
32}
33
34pub struct FileDemuxer {
43 pp_log: PpLog,
44 name: Arc<str>,
45 input: ffmpeg::format::context::Input,
46 pads: Vec<SrcPad>,
47 pending: Option<(usize, ffmpeg::Packet)>,
53}
54
55impl FileDemuxer {
56 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 pub fn stream_parameters(&self, index: usize) -> Option<ffmpeg::codec::Parameters> {
101 self.stream(index).map(|s| s.parameters())
102 }
103
104 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 loop {
152 if drain_control(control, self, bus)?.stopped {
153 pp_info!(self, "stopped");
155 return Ok(());
156 }
157 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 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 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 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 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 #[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}