Skip to main content

media_pp/elements/sink/
app_sink.rs

1use std::sync::Arc;
2
3use crate::pp_log::{PpLog, pp_info};
4
5use crate::{
6    buffer::MediaBuffer,
7    contract::InputContract,
8    control::ControlMsg,
9    element::{Element, ElementType, Sink, element_pp_log},
10    error::Result,
11};
12
13/// Terminal sink that hands every buffer (and, optionally, every control
14/// message) to a plain closure instead of requiring a bespoke `struct` +
15/// `Element`/`Sink` impl — the equivalent of GStreamer's `appsink`: the
16/// pipeline's job ends here, and whatever the caller does with the data
17/// (run inference, forward it to a channel, write it out, ...) is none of
18/// this crate's concern.
19///
20/// `FrameCounter`/`PacketCounter` are what a one-off consumer looked
21/// like *before* this existed — this is the general case of the same
22/// pattern, for when a whole new type per use site is more ceremony than
23/// the actual logic warrants:
24///
25/// ```
26/// # use media_pp::{buffer::MediaBuffer, elements::AppSink};
27/// let mut count = 0usize;
28/// let sink = AppSink::new("counter", move |buf: MediaBuffer| {
29///     if matches!(buf, MediaBuffer::Video(_)) {
30///         count += 1;
31///     }
32///     Ok(())
33/// });
34/// ```
35pub struct AppSink<F, C> {
36    pp_log: PpLog,
37    name: Arc<str>,
38    consume: F,
39    control: C,
40}
41
42impl<F> AppSink<F, fn(ControlMsg) -> Result<()>>
43where
44    F: FnMut(MediaBuffer) -> Result<()> + Send + 'static,
45{
46    /// `consume` is the only thing this reacts to — every `ControlMsg`
47    /// (`Pause`/`Resume`/`Stop`/`Seek`) is silently ignored, the same as
48    /// `FrameCounter`/`PacketCounter`. Reach for
49    /// [`AppSink::with_control`] instead if the closure needs to know
50    /// about one of those — e.g. resetting a tracker's history, or a
51    /// batch buffer, on `Seek`, the same way `SwDecoder`/`Pacer` react to
52    /// it internally.
53    pub fn new(name: impl Into<String>, consume: F) -> Self {
54        Self::with_control(name, consume, |_| Ok(()))
55    }
56}
57
58impl<F, C> AppSink<F, C>
59where
60    F: FnMut(MediaBuffer) -> Result<()> + Send + 'static,
61    C: FnMut(ControlMsg) -> Result<()> + Send + 'static,
62{
63    /// Same as [`AppSink::new`], but also hands every [`ControlMsg`] to
64    /// `control` instead of silently dropping it.
65    ///
66    /// ```
67    /// # use media_pp::{control::ControlMsg, elements::AppSink};
68    /// let sink = AppSink::with_control(
69    ///     "detector",
70    ///     |_buf| Ok(()),
71    ///     |msg| {
72    ///         if let ControlMsg::Seek(_) = msg {
73    ///             // e.g. clear a tracker's history here
74    ///         }
75    ///         Ok(())
76    ///     },
77    /// );
78    /// ```
79    pub fn with_control(name: impl Into<String>, consume: F, control: C) -> Self {
80        let name: Arc<str> = name.into().into();
81        let pp_log = element_pp_log(ElementType::AppSink, &name, None);
82        pp_info!(pp_log: &pp_log, "created");
83        Self {
84            name,
85            pp_log,
86            consume,
87            control,
88        }
89    }
90}
91
92impl<F, C> Element for AppSink<F, C>
93where
94    F: FnMut(MediaBuffer) -> Result<()> + Send + 'static,
95    C: FnMut(ControlMsg) -> Result<()> + Send + 'static,
96{
97    fn name(&self) -> Arc<str> {
98        self.name.clone()
99    }
100
101    fn element_type(&self) -> ElementType {
102        ElementType::AppSink
103    }
104
105    fn pp_log(&self) -> &PpLog {
106        &self.pp_log
107    }
108
109    fn pp_log_mut(&mut self) -> &mut PpLog {
110        &mut self.pp_log
111    }
112}
113
114impl<F, C> Sink for AppSink<F, C>
115where
116    F: FnMut(MediaBuffer) -> Result<()> + Send + 'static,
117    C: FnMut(ControlMsg) -> Result<()> + Send + 'static,
118{
119    /// Every buffer reaches the closure verbatim, so this element never
120    /// rejects one itself. It is a claim about this sink, not about the
121    /// closure: one that only understands packets still returns its own
122    /// error for a frame, which is application behavior a link check
123    /// cannot see.
124    fn input_contract(&self) -> InputContract {
125        InputContract::Any
126    }
127
128    fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
129        (self.consume)(buf)
130    }
131
132    fn control(&mut self, msg: ControlMsg) -> Result<()> {
133        (self.control)(msg)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use std::{
140        sync::{Arc, Mutex},
141        time::Duration,
142    };
143
144    use ffmpeg_next as ffmpeg;
145
146    use super::*;
147
148    fn control_messages() -> [ControlMsg; 4] {
149        [
150            ControlMsg::Pause,
151            ControlMsg::Resume,
152            ControlMsg::Stop,
153            ControlMsg::Seek(Duration::from_secs(1)),
154        ]
155    }
156
157    /// `AppSink::new`'s docs promise every `ControlMsg` is *silently*
158    /// ignored — accepting it and doing nothing, not failing the control
159    /// cascade the way an `Err` here would.
160    #[test]
161    fn new_accepts_and_ignores_every_control_message() {
162        let mut sink = AppSink::new("counter", |_buf| Ok(()));
163
164        for msg in control_messages() {
165            sink.control(msg).unwrap();
166        }
167    }
168
169    /// The whole point of `with_control` over `new`: no variant is
170    /// filtered out on the way to the closure.
171    #[test]
172    fn with_control_forwards_every_control_message() {
173        let seen = Arc::new(Mutex::new(Vec::new()));
174        let recorded = seen.clone();
175        let mut sink = AppSink::with_control(
176            "detector",
177            |_buf| Ok(()),
178            move |msg| {
179                recorded.lock().unwrap().push(msg);
180                Ok(())
181            },
182        );
183
184        for msg in control_messages() {
185            sink.control(msg).unwrap();
186        }
187
188        assert_eq!(&*seen.lock().unwrap(), &control_messages());
189    }
190
191    /// A terminal `Sink`'s error has to come back out of `consume`
192    /// unchanged: that return value is what a direct caller propagates
193    /// with `?`, and what a `Queue` worker turns into `BusEvent::Error`.
194    /// Swallowing it here would make both silently impossible.
195    #[test]
196    fn consume_error_propagates_to_the_caller() {
197        let mut sink = AppSink::new("failing", |_buf| {
198            Err(crate::error::Error::Other("closure failed".into()))
199        });
200
201        let error = sink.consume(MediaBuffer::Eos).unwrap_err();
202
203        assert!(error.to_string().contains("closure failed"));
204    }
205
206    /// `Eos` reaches the closure like any other buffer rather than being
207    /// consumed by the sink itself — a caller that finalizes on EOS (a
208    /// muxer wrapper, a channel it closes) only ever learns about it here.
209    #[test]
210    fn every_buffer_including_eos_reaches_the_closure() {
211        let seen = Arc::new(Mutex::new(Vec::new()));
212        let recorded = seen.clone();
213        let mut sink = AppSink::new("recorder", move |buf| {
214            recorded
215                .lock()
216                .unwrap()
217                .push(matches!(buf, MediaBuffer::Eos));
218            Ok(())
219        });
220
221        sink.consume(MediaBuffer::Audio(Arc::new(ffmpeg::frame::Audio::empty())))
222            .unwrap();
223        sink.consume(MediaBuffer::Eos).unwrap();
224
225        assert_eq!(&*seen.lock().unwrap(), &[false, true]);
226    }
227}