media_pp\elements\filter/
scaler.rs1use std::sync::Arc;
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 control::ControlMsg,
10 element::{Element, ElementType, Sink, Source, element_pp_log},
11 error::Result,
12 pad::SrcPad,
13 pool::UnboundObjectPool,
14};
15
16const POOL_SIZE: usize = 4;
26
27#[derive(Debug, ThisError)]
30pub enum ScalerError {
31 #[error("ffmpeg error: {0}")]
32 Ffmpeg(#[from] ffmpeg::Error),
33
34 #[error(
35 "Scaler only converts/resizes decoded Video frames, got a {0}; \
36 link it straight after a decoder, not a demuxer"
37 )]
38 UnsupportedBuffer(&'static str),
39}
40
41pub struct Scaler {
51 pp_log: PpLog,
52 name: Arc<str>,
53 dst_format: ffmpeg::format::Pixel,
54 dst_width: u32,
55 dst_height: u32,
56 flags: ffmpeg::software::scaling::Flags,
57 context: Option<ffmpeg::software::scaling::Context>,
64 pool: UnboundObjectPool<ffmpeg::frame::Video>,
70 pad: SrcPad,
71}
72
73unsafe impl Send for Scaler {}
81
82impl Scaler {
83 pub fn new(
89 name: impl Into<String>,
90 dst_format: ffmpeg::format::Pixel,
91 dst_width: u32,
92 dst_height: u32,
93 flags: ffmpeg::software::scaling::Flags,
94 ) -> Self {
95 let name: Arc<str> = name.into().into();
96 let pp_log = element_pp_log(ElementType::Scaler, &name, None);
97 pp_info!(
98 pp_log: &pp_log,
99 "created: dst_format={dst_format:?}, dst={dst_width}x{dst_height}"
100 );
101 let pad = SrcPad::new(format!("{name}_src"));
102 let pool = UnboundObjectPool::new(
103 POOL_SIZE,
104 move || ffmpeg::frame::Video::new(dst_format, dst_width, dst_height),
105 |_| {},
106 );
107 Self {
108 name,
109 pp_log,
110 dst_format,
111 dst_width,
112 dst_height,
113 flags,
114 context: None,
115 pool,
116 pad,
117 }
118 }
119
120 fn context_matches(&self, frame: &ffmpeg::frame::Video) -> bool {
124 match &self.context {
125 Some(context) => {
126 let input = context.input();
127 input.format == frame.format()
128 && input.width == frame.width()
129 && input.height == frame.height()
130 }
131 None => false,
132 }
133 }
134}
135
136impl Element for Scaler {
137 fn name(&self) -> Arc<str> {
138 self.name.clone()
139 }
140
141 fn element_type(&self) -> ElementType {
142 ElementType::Scaler
143 }
144
145 fn pp_log(&self) -> &PpLog {
146 &self.pp_log
147 }
148
149 fn pp_log_mut(&mut self) -> &mut PpLog {
150 &mut self.pp_log
151 }
152}
153
154impl Source for Scaler {
155 fn src_pads(&mut self) -> &mut [SrcPad] {
156 std::slice::from_mut(&mut self.pad)
157 }
158}
159
160impl Sink for Scaler {
161 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
162 match buf {
163 MediaBuffer::Video(frame) => {
164 if !self.context_matches(&frame) {
165 match &mut self.context {
166 Some(context) => context.cached(
167 frame.format(),
168 frame.width(),
169 frame.height(),
170 self.dst_format,
171 self.dst_width,
172 self.dst_height,
173 self.flags,
174 ),
175 None => {
176 self.context = Some(
177 ffmpeg::software::scaling::Context::get(
178 frame.format(),
179 frame.width(),
180 frame.height(),
181 self.dst_format,
182 self.dst_width,
183 self.dst_height,
184 self.flags,
185 )
186 .inspect_err(|error| {
187 pp_error!(self, "failed to build scaling context: {error}")
188 })
189 .map_err(ScalerError::from)?,
190 );
191 }
192 }
193 }
194
195 let mut output = self.pool.get();
199 self.context
200 .as_mut()
201 .expect("built or confirmed matching above")
202 .run(&frame, &mut output)
203 .inspect_err(|error| pp_error!(self, "scale failed: {error}"))
204 .map_err(ScalerError::from)?;
205 output.set_pts(frame.pts());
209
210 self.pad.push(MediaBuffer::Video(Arc::new(output)))
211 }
212 MediaBuffer::Eos => self.pad.push(MediaBuffer::Eos),
213 MediaBuffer::Packet(_) => {
214 pp_error!(self, "unsupported buffer: Packet");
215 Err(ScalerError::UnsupportedBuffer("Packet").into())
216 }
217 MediaBuffer::Audio(_) => {
218 pp_error!(self, "unsupported buffer: Audio");
219 Err(ScalerError::UnsupportedBuffer("Audio").into())
220 }
221 }
222 }
223
224 fn control(&mut self, msg: ControlMsg) -> Result<()> {
225 self.pad.control(msg)
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use std::sync::Mutex;
236
237 use super::*;
238
239 struct CapturingSink {
240 pp_log: PpLog,
241 received: Arc<Mutex<Vec<MediaBuffer>>>,
242 }
243
244 impl Element for CapturingSink {
245 fn name(&self) -> Arc<str> {
246 "capture".into()
247 }
248
249 fn element_type(&self) -> ElementType {
250 ElementType::Other
251 }
252
253 fn pp_log(&self) -> &PpLog {
254 &self.pp_log
255 }
256
257 fn pp_log_mut(&mut self) -> &mut PpLog {
258 &mut self.pp_log
259 }
260 }
261
262 impl Sink for CapturingSink {
263 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
264 self.received.lock().unwrap().push(buf);
265 Ok(())
266 }
267
268 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
269 Ok(())
270 }
271 }
272
273 fn video_frame(
274 format: ffmpeg::format::Pixel,
275 width: u32,
276 height: u32,
277 pts: i64,
278 ) -> MediaBuffer {
279 let pool = UnboundObjectPool::new(
280 0,
281 move || ffmpeg::frame::Video::new(format, width, height),
282 |_| {},
283 );
284 let mut frame = pool.get();
285 frame.set_pts(Some(pts));
286 MediaBuffer::Video(Arc::new(frame))
287 }
288
289 fn new_scaler(
290 dst_format: ffmpeg::format::Pixel,
291 dst_width: u32,
292 dst_height: u32,
293 ) -> (Scaler, Arc<Mutex<Vec<MediaBuffer>>>) {
294 let mut scaler = Scaler::new(
295 "scaler",
296 dst_format,
297 dst_width,
298 dst_height,
299 ffmpeg::software::scaling::Flags::BILINEAR,
300 );
301 let received = Arc::new(Mutex::new(Vec::new()));
302 scaler.src_pads()[0].link(Box::new(CapturingSink {
303 received: received.clone(),
304 pp_log: element_pp_log(ElementType::Other, "capture", None),
305 }));
306 (scaler, received)
307 }
308
309 #[test]
310 fn converts_pixel_format_and_size_while_preserving_pts() {
311 let (mut scaler, received) = new_scaler(ffmpeg::format::Pixel::RGB24, 80, 60);
312 scaler
313 .consume(video_frame(ffmpeg::format::Pixel::YUV420P, 160, 120, 4242))
314 .expect("scale must succeed");
315
316 let received = received.lock().unwrap();
317 assert_eq!(received.len(), 1);
318 let MediaBuffer::Video(frame) = &received[0] else {
319 panic!("expected a Video buffer");
320 };
321 assert_eq!(frame.format(), ffmpeg::format::Pixel::RGB24);
322 assert_eq!(frame.width(), 80);
323 assert_eq!(frame.height(), 60);
324 assert_eq!(frame.pts(), Some(4242));
325 }
326
327 #[test]
331 fn rebuilds_its_scaling_context_when_input_dimensions_change_mid_stream() {
332 let (mut scaler, received) = new_scaler(ffmpeg::format::Pixel::RGB24, 80, 60);
333 scaler
334 .consume(video_frame(ffmpeg::format::Pixel::YUV420P, 160, 120, 0))
335 .expect("first frame must scale");
336 scaler
337 .consume(video_frame(ffmpeg::format::Pixel::YUV420P, 320, 240, 1))
338 .expect("a differently-sized second frame must still scale, not reuse a stale context");
339
340 let received = received.lock().unwrap();
341 assert_eq!(received.len(), 2);
342 for buf in received.iter() {
343 let MediaBuffer::Video(frame) = buf else {
344 panic!("expected a Video buffer");
345 };
346 assert_eq!(frame.width(), 80);
347 assert_eq!(frame.height(), 60);
348 }
349 }
350
351 #[test]
352 fn eos_forwards_downstream() {
353 let (mut scaler, received) = new_scaler(ffmpeg::format::Pixel::RGB24, 80, 60);
354 scaler
355 .consume(MediaBuffer::Eos)
356 .expect("eos must forward cleanly");
357 assert!(matches!(
358 received.lock().unwrap().as_slice(),
359 [MediaBuffer::Eos]
360 ));
361 }
362
363 #[test]
364 fn rejects_packet_and_audio_buffers_with_a_clean_error_instead_of_scaling_garbage() {
365 let (mut scaler, _received) = new_scaler(ffmpeg::format::Pixel::RGB24, 80, 60);
366
367 let packet = MediaBuffer::Packet(Arc::new(ffmpeg::Packet::empty()));
368 assert!(
369 scaler.consume(packet).is_err(),
370 "Packet must be rejected, not silently accepted"
371 );
372
373 let audio = MediaBuffer::Audio(Arc::new(ffmpeg::frame::Audio::empty()));
374 assert!(
375 scaler.consume(audio).is_err(),
376 "Audio must be rejected, not silently accepted"
377 );
378 }
379}