scuffle_ffmpeg/
scaler.rs

1use crate::AVPixelFormat;
2use crate::error::{FfmpegError, FfmpegErrorCode};
3use crate::ffi::*;
4use crate::frame::VideoFrame;
5use crate::smart_object::SmartPtr;
6
7/// A scaler is a wrapper around an [`SwsContext`]. Which is used to scale or transform video frames.
8pub struct VideoScaler {
9    ptr: SmartPtr<SwsContext>,
10    frame: VideoFrame,
11    pixel_format: AVPixelFormat,
12    width: i32,
13    height: i32,
14}
15
16/// Safety: `Scaler` is safe to send between threads.
17unsafe impl Send for VideoScaler {}
18
19impl VideoScaler {
20    /// Creates a new `Scaler` instance.
21    pub fn new(
22        input_width: i32,
23        input_height: i32,
24        incoming_pixel_fmt: AVPixelFormat,
25        width: i32,
26        height: i32,
27        pixel_format: AVPixelFormat,
28    ) -> Result<Self, FfmpegError> {
29        // Safety: `sws_getContext` is safe to call, and the pointer returned is valid.
30        let ptr = unsafe {
31            sws_getContext(
32                input_width,
33                input_height,
34                incoming_pixel_fmt.into(),
35                width,
36                height,
37                pixel_format.into(),
38                SWS_BILINEAR as i32,
39                std::ptr::null_mut(),
40                std::ptr::null_mut(),
41                std::ptr::null(),
42            )
43        };
44
45        let destructor = |ptr: &mut *mut SwsContext| {
46            // Safety: `sws_freeContext` is safe to call.
47            unsafe {
48                sws_freeContext(*ptr);
49            }
50
51            *ptr = std::ptr::null_mut();
52        };
53
54        // Safety: `ptr` is a valid pointer & `destructor` has been setup to free the context.
55        let ptr = unsafe { SmartPtr::wrap_non_null(ptr, destructor) }.ok_or(FfmpegError::Alloc)?;
56
57        let frame = VideoFrame::builder()
58            .width(width)
59            .height(height)
60            .pix_fmt(pixel_format)
61            .build()?;
62
63        Ok(Self {
64            ptr,
65            frame,
66            pixel_format,
67            width,
68            height,
69        })
70    }
71
72    /// Returns the pixel format of the scalar.
73    pub const fn pixel_format(&self) -> AVPixelFormat {
74        self.pixel_format
75    }
76
77    /// Returns the width of the scalar.
78    pub const fn width(&self) -> i32 {
79        self.width
80    }
81
82    /// Returns the height of the scalar.
83    pub const fn height(&self) -> i32 {
84        self.height
85    }
86
87    /// Processes a frame through the scalar.
88    pub fn process<'a>(&'a mut self, frame: &VideoFrame) -> Result<&'a VideoFrame, FfmpegError> {
89        // Safety: `frame` is a valid pointer, and `self.ptr` is a valid pointer.
90        let frame_ptr = unsafe { frame.as_ptr().as_ref().unwrap() };
91        // Safety: `self.frame` is a valid pointer.
92        let self_frame_ptr = unsafe { self.frame.as_ptr().as_ref().unwrap() };
93
94        // Safety: `sws_scale` is safe to call.
95        FfmpegErrorCode(unsafe {
96            sws_scale(
97                self.ptr.as_mut_ptr(),
98                frame_ptr.data.as_ptr() as *const *const u8,
99                frame_ptr.linesize.as_ptr(),
100                0,
101                frame_ptr.height,
102                self_frame_ptr.data.as_ptr(),
103                self_frame_ptr.linesize.as_ptr(),
104            )
105        })
106        .result()?;
107
108        // Copy the other fields from the input frame to the output frame.
109        self.frame.set_dts(frame.dts());
110        self.frame.set_pts(frame.pts());
111        self.frame.set_duration(frame.duration());
112        self.frame.set_time_base(frame.time_base());
113
114        Ok(&self.frame)
115    }
116}
117
118#[cfg(test)]
119#[cfg_attr(all(test, coverage_nightly), coverage(off))]
120mod tests {
121    use insta::assert_debug_snapshot;
122    use rand::Rng;
123
124    use crate::frame::VideoFrame;
125    use crate::scaler::{AVPixelFormat, VideoScaler};
126
127    #[test]
128    fn test_scalar_new() {
129        let input_width = 1920;
130        let input_height = 1080;
131        let incoming_pixel_fmt = AVPixelFormat::Yuv420p;
132        let output_width = 1280;
133        let output_height = 720;
134        let output_pixel_fmt = AVPixelFormat::Rgb24;
135        let scalar = VideoScaler::new(
136            input_width,
137            input_height,
138            incoming_pixel_fmt,
139            output_width,
140            output_height,
141            output_pixel_fmt,
142        );
143
144        assert!(scalar.is_ok(), "Expected Scalar::new to succeed");
145        let scalar = scalar.unwrap();
146
147        assert_eq!(
148            scalar.width(),
149            output_width,
150            "Expected Scalar width to match the output width"
151        );
152        assert_eq!(
153            scalar.height(),
154            output_height,
155            "Expected Scalar height to match the output height"
156        );
157        assert_eq!(
158            scalar.pixel_format(),
159            output_pixel_fmt,
160            "Expected Scalar pixel format to match the output pixel format"
161        );
162    }
163
164    #[test]
165    fn test_scalar_process() {
166        let input_width = 1920;
167        let input_height = 1080;
168        let incoming_pixel_fmt = AVPixelFormat::Yuv420p;
169        let output_width = 1280;
170        let output_height = 720;
171        let output_pixel_fmt = AVPixelFormat::Rgb24;
172
173        let mut scalar = VideoScaler::new(
174            input_width,
175            input_height,
176            incoming_pixel_fmt,
177            output_width,
178            output_height,
179            output_pixel_fmt,
180        )
181        .expect("Failed to create Scalar");
182
183        let mut input_frame = VideoFrame::builder()
184            .width(input_width)
185            .height(input_height)
186            .pix_fmt(incoming_pixel_fmt)
187            .build()
188            .expect("Failed to create VideoFrame");
189
190        // We need to fill the buffer with random data otherwise the result will be based off uninitialized data.
191        let mut rng = rand::rng();
192
193        for data_idx in 0..rusty_ffmpeg::ffi::AV_NUM_DATA_POINTERS {
194            if let Some(mut data_buf) = input_frame.data_mut(data_idx as usize) {
195                for row_idx in 0..data_buf.height() {
196                    let row = data_buf.get_row_mut(row_idx as usize).unwrap();
197                    rng.fill(row);
198                }
199            }
200        }
201
202        let result = scalar.process(&input_frame);
203
204        assert!(
205            result.is_ok(),
206            "Expected Scalar::process to succeed, but got error: {result:?}"
207        );
208
209        let output_frame = result.unwrap();
210        assert_debug_snapshot!(output_frame, @r"
211        VideoFrame {
212            width: 1280,
213            height: 720,
214            sample_aspect_ratio: Rational {
215                numerator: 1,
216                denominator: 1,
217            },
218            pts: None,
219            dts: None,
220            duration: Some(
221                0,
222            ),
223            best_effort_timestamp: None,
224            time_base: Rational {
225                numerator: 0,
226                denominator: 1,
227            },
228            format: AVPixelFormat::Rgb24,
229            is_audio: false,
230            is_video: true,
231            is_keyframe: false,
232        }
233        ");
234    }
235}