Export speed #2: cache the RGBA→YUV swscale context across frames
CpuYuvConverter::convert rebuilt the swscale context + both ffmpeg frames on every call (per output frame), despite the converter persisting for the whole export. Now the scaler + reusable source/dest frames are built once in new() and reused each convert(). (convert is &mut self; the caller already held it mutably.)
This commit is contained in:
parent
0a0d7cd0a9
commit
4e9cacb789
|
|
@ -13,6 +13,11 @@ use ffmpeg_next as ffmpeg;
|
||||||
pub struct CpuYuvConverter {
|
pub struct CpuYuvConverter {
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
|
/// swscale context + reusable source/dest frames, built once and reused every frame
|
||||||
|
/// (creating them per call was a measurable per-output-frame export cost).
|
||||||
|
scaler: ffmpeg::software::scaling::Context,
|
||||||
|
rgba_frame: ffmpeg::frame::Video,
|
||||||
|
yuv_frame: ffmpeg::frame::Video,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CpuYuvConverter {
|
impl CpuYuvConverter {
|
||||||
|
|
@ -22,7 +27,20 @@ impl CpuYuvConverter {
|
||||||
/// * `width` - Frame width in pixels
|
/// * `width` - Frame width in pixels
|
||||||
/// * `height` - Frame height in pixels
|
/// * `height` - Frame height in pixels
|
||||||
pub fn new(width: u32, height: u32) -> Result<Self, String> {
|
pub fn new(width: u32, height: u32) -> Result<Self, String> {
|
||||||
Ok(Self { width, height })
|
// BT.709 (HD) RGBA→YUV420p context, created once.
|
||||||
|
let scaler = ffmpeg::software::scaling::Context::get(
|
||||||
|
ffmpeg::format::Pixel::RGBA,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
ffmpeg::format::Pixel::YUV420P,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
ffmpeg::software::scaling::Flags::BILINEAR,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Failed to create swscale context: {}", e))?;
|
||||||
|
let rgba_frame = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::RGBA, width, height);
|
||||||
|
let yuv_frame = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::YUV420P, width, height);
|
||||||
|
Ok(Self { width, height, scaler, rgba_frame, yuv_frame })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert RGBA data to YUV420p planes
|
/// Convert RGBA data to YUV420p planes
|
||||||
|
|
@ -40,7 +58,7 @@ impl CpuYuvConverter {
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if rgba_data length doesn't match width * height * 4
|
/// Panics if rgba_data length doesn't match width * height * 4
|
||||||
pub fn convert(&self, rgba_data: &[u8]) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>), String> {
|
pub fn convert(&mut self, rgba_data: &[u8]) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>), String> {
|
||||||
let expected_size = (self.width * self.height * 4) as usize;
|
let expected_size = (self.width * self.height * 4) as usize;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rgba_data.len(),
|
rgba_data.len(),
|
||||||
|
|
@ -50,51 +68,17 @@ impl CpuYuvConverter {
|
||||||
rgba_data.len()
|
rgba_data.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create source RGBA frame
|
// Copy RGBA into the reused source frame, run the reused scaler into the reused
|
||||||
let mut rgba_frame = ffmpeg::frame::Video::new(
|
// dest frame (SIMD-optimized), then extract planes.
|
||||||
ffmpeg::format::Pixel::RGBA,
|
self.rgba_frame.data_mut(0).copy_from_slice(rgba_data);
|
||||||
self.width,
|
self.scaler
|
||||||
self.height,
|
.run(&self.rgba_frame, &mut self.yuv_frame)
|
||||||
);
|
|
||||||
|
|
||||||
// Copy RGBA data into source frame
|
|
||||||
// ffmpeg-next provides mutable access to the frame data
|
|
||||||
let frame_data = rgba_frame.data_mut(0);
|
|
||||||
frame_data.copy_from_slice(rgba_data);
|
|
||||||
|
|
||||||
// Create destination YUV420p frame
|
|
||||||
let mut yuv_frame = ffmpeg::frame::Video::new(
|
|
||||||
ffmpeg::format::Pixel::YUV420P,
|
|
||||||
self.width,
|
|
||||||
self.height,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create swscale context for RGBA→YUV420p conversion
|
|
||||||
// Uses BT.709 color matrix (HD standard)
|
|
||||||
let mut scaler = ffmpeg::software::scaling::Context::get(
|
|
||||||
ffmpeg::format::Pixel::RGBA,
|
|
||||||
self.width,
|
|
||||||
self.height,
|
|
||||||
ffmpeg::format::Pixel::YUV420P,
|
|
||||||
self.width,
|
|
||||||
self.height,
|
|
||||||
ffmpeg::software::scaling::Flags::BILINEAR,
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to create swscale context: {}", e))?;
|
|
||||||
|
|
||||||
// Perform the conversion (SIMD-optimized)
|
|
||||||
scaler
|
|
||||||
.run(&rgba_frame, &mut yuv_frame)
|
|
||||||
.map_err(|e| format!("swscale conversion failed: {}", e))?;
|
.map_err(|e| format!("swscale conversion failed: {}", e))?;
|
||||||
|
|
||||||
// Extract planar YUV data
|
// YUV420p planes: Y full-res, U/V quarter-res (2×2 subsampled).
|
||||||
// YUV420p has 3 planes:
|
let y_plane = self.yuv_frame.data(0).to_vec();
|
||||||
// - Y: full resolution (width × height)
|
let u_plane = self.yuv_frame.data(1).to_vec();
|
||||||
// - U: quarter resolution (width/2 × height/2)
|
let v_plane = self.yuv_frame.data(2).to_vec();
|
||||||
// - V: quarter resolution (width/2 × height/2)
|
|
||||||
let y_plane = yuv_frame.data(0).to_vec();
|
|
||||||
let u_plane = yuv_frame.data(1).to_vec();
|
|
||||||
let v_plane = yuv_frame.data(2).to_vec();
|
|
||||||
|
|
||||||
Ok((y_plane, u_plane, v_plane))
|
Ok((y_plane, u_plane, v_plane))
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +96,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conversion_output_sizes() {
|
fn test_conversion_output_sizes() {
|
||||||
let converter = CpuYuvConverter::new(1920, 1080).unwrap();
|
let mut converter = CpuYuvConverter::new(1920, 1080).unwrap();
|
||||||
|
|
||||||
// Create dummy RGBA data (all black)
|
// Create dummy RGBA data (all black)
|
||||||
let rgba_data = vec![0u8; 1920 * 1080 * 4];
|
let rgba_data = vec![0u8; 1920 * 1080 * 4];
|
||||||
|
|
@ -133,7 +117,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "RGBA data size mismatch")]
|
#[should_panic(expected = "RGBA data size mismatch")]
|
||||||
fn test_wrong_input_size_panics() {
|
fn test_wrong_input_size_panics() {
|
||||||
let converter = CpuYuvConverter::new(1920, 1080).unwrap();
|
let mut converter = CpuYuvConverter::new(1920, 1080).unwrap();
|
||||||
|
|
||||||
// Wrong size input
|
// Wrong size input
|
||||||
let rgba_data = vec![0u8; 1000];
|
let rgba_data = vec![0u8; 1000];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue