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}