feat: 初步增加 Windows 支持

This commit is contained in:
mofeng-git
2026-05-18 22:43:28 +08:00
parent 0b9d94f53f
commit 935fa823f2
163 changed files with 11419 additions and 7581 deletions

865
src/video/codec/convert.rs Normal file
View File

@@ -0,0 +1,865 @@
//! Pixel format conversion utilities
//!
//! This module provides SIMD-accelerated color space conversion using libyuv.
//! Primary use case: YUYV (from V4L2 capture) → YUV420P/NV12 (for H264 encoding)
use crate::error::{AppError, Result};
use crate::video::format::{PixelFormat, Resolution};
/// YUV420P buffer with separate Y, U, V planes
pub struct Yuv420pBuffer {
/// Raw buffer containing all planes
data: Vec<u8>,
/// Width of the frame
width: u32,
/// Height of the frame
height: u32,
/// Y plane offset (always 0)
y_offset: usize,
/// U plane offset
u_offset: usize,
/// V plane offset
v_offset: usize,
}
impl Yuv420pBuffer {
/// Create a new YUV420P buffer for the given resolution
pub fn new(resolution: Resolution) -> Self {
let width = resolution.width;
let height = resolution.height;
// YUV420P: Y = width*height, U = width*height/4, V = width*height/4
let y_size = (width * height) as usize;
let uv_size = y_size / 4;
let total_size = y_size + uv_size * 2;
Self {
data: vec![0u8; total_size],
width,
height,
y_offset: 0,
u_offset: y_size,
v_offset: y_size + uv_size,
}
}
/// Get the raw buffer as bytes
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
/// Get the raw buffer as mutable bytes
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
&mut self.data
}
/// Get Y plane
pub fn y_plane(&self) -> &[u8] {
&self.data[self.y_offset..self.u_offset]
}
/// Get Y plane mutable
pub fn y_plane_mut(&mut self) -> &mut [u8] {
let u_offset = self.u_offset;
&mut self.data[self.y_offset..u_offset]
}
/// Get U plane
pub fn u_plane(&self) -> &[u8] {
&self.data[self.u_offset..self.v_offset]
}
/// Get U plane mutable
pub fn u_plane_mut(&mut self) -> &mut [u8] {
let v_offset = self.v_offset;
let u_offset = self.u_offset;
&mut self.data[u_offset..v_offset]
}
/// Get V plane
pub fn v_plane(&self) -> &[u8] {
&self.data[self.v_offset..]
}
/// Get V plane mutable
pub fn v_plane_mut(&mut self) -> &mut [u8] {
let v_offset = self.v_offset;
&mut self.data[v_offset..]
}
/// Get buffer length
pub fn len(&self) -> usize {
self.data.len()
}
/// Check if buffer is empty
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
/// Get resolution
pub fn resolution(&self) -> Resolution {
Resolution::new(self.width, self.height)
}
}
/// NV12 buffer with Y plane and interleaved UV plane
pub struct Nv12Buffer {
/// Raw buffer containing Y plane followed by interleaved UV plane
data: Vec<u8>,
/// Width of the frame
width: u32,
/// Height of the frame
height: u32,
}
impl Nv12Buffer {
/// Create a new NV12 buffer for the given resolution
pub fn new(resolution: Resolution) -> Self {
let width = resolution.width;
let height = resolution.height;
// NV12: Y = width*height, UV = width*height/2 (interleaved)
let y_size = (width * height) as usize;
let uv_size = y_size / 2;
let total_size = y_size + uv_size;
Self {
data: vec![0u8; total_size],
width,
height,
}
}
/// Get the raw buffer as bytes
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
/// Get the raw buffer as mutable bytes
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
&mut self.data
}
/// Get Y plane
pub fn y_plane(&self) -> &[u8] {
let y_size = (self.width * self.height) as usize;
&self.data[..y_size]
}
/// Get Y plane mutable
pub fn y_plane_mut(&mut self) -> &mut [u8] {
let y_size = (self.width * self.height) as usize;
&mut self.data[..y_size]
}
/// Get UV plane (interleaved)
pub fn uv_plane(&self) -> &[u8] {
let y_size = (self.width * self.height) as usize;
&self.data[y_size..]
}
/// Get UV plane mutable
pub fn uv_plane_mut(&mut self) -> &mut [u8] {
let y_size = (self.width * self.height) as usize;
&mut self.data[y_size..]
}
/// Get buffer length
pub fn len(&self) -> usize {
self.data.len()
}
/// Check if buffer is empty
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
/// Get resolution
pub fn resolution(&self) -> Resolution {
Resolution::new(self.width, self.height)
}
}
/// Pixel format converter using libyuv (SIMD accelerated)
pub struct PixelConverter {
/// Source format
src_format: PixelFormat,
/// Destination format
dst_format: PixelFormat,
/// Frame resolution
resolution: Resolution,
/// Output buffer (reused across conversions)
output_buffer: Yuv420pBuffer,
/// Scratch buffer for split chroma planes when converting semiplanar 4:2:2 / 4:4:4 input.
uv_split_buffer: Vec<u8>,
}
impl PixelConverter {
fn new(src_format: PixelFormat, dst_format: PixelFormat, resolution: Resolution) -> Self {
let max_uv_plane_size = (resolution.width * resolution.height) as usize;
Self {
src_format,
dst_format,
resolution,
output_buffer: Yuv420pBuffer::new(resolution),
uv_split_buffer: vec![0u8; max_uv_plane_size * 2],
}
}
/// Create a new converter for YUYV → YUV420P
pub fn yuyv_to_yuv420p(resolution: Resolution) -> Self {
Self::new(PixelFormat::Yuyv, PixelFormat::Yuv420, resolution)
}
/// Create a new converter for UYVY → YUV420P
pub fn uyvy_to_yuv420p(resolution: Resolution) -> Self {
Self::new(PixelFormat::Uyvy, PixelFormat::Yuv420, resolution)
}
/// Create a new converter for YVYU → YUV420P
pub fn yvyu_to_yuv420p(resolution: Resolution) -> Self {
Self::new(PixelFormat::Yvyu, PixelFormat::Yuv420, resolution)
}
/// Create a new converter for NV12 → YUV420P
pub fn nv12_to_yuv420p(resolution: Resolution) -> Self {
Self::new(PixelFormat::Nv12, PixelFormat::Yuv420, resolution)
}
/// Create a new converter for NV21 → YUV420P
pub fn nv21_to_yuv420p(resolution: Resolution) -> Self {
Self::new(PixelFormat::Nv21, PixelFormat::Yuv420, resolution)
}
/// Create a new converter for NV16 → YUV420P
pub fn nv16_to_yuv420p(resolution: Resolution) -> Self {
Self::new(PixelFormat::Nv16, PixelFormat::Yuv420, resolution)
}
/// Create a new converter for NV24 → YUV420P
pub fn nv24_to_yuv420p(resolution: Resolution) -> Self {
Self::new(PixelFormat::Nv24, PixelFormat::Yuv420, resolution)
}
/// Create a new converter for YVU420 → YUV420P (swap U and V planes)
pub fn yvu420_to_yuv420p(resolution: Resolution) -> Self {
Self::new(PixelFormat::Yvu420, PixelFormat::Yuv420, resolution)
}
/// Create a new converter for RGB24 → YUV420P
pub fn rgb24_to_yuv420p(resolution: Resolution) -> Self {
Self::new(PixelFormat::Rgb24, PixelFormat::Yuv420, resolution)
}
/// Create a new converter for BGR24 → YUV420P
pub fn bgr24_to_yuv420p(resolution: Resolution) -> Self {
Self::new(PixelFormat::Bgr24, PixelFormat::Yuv420, resolution)
}
/// Convert a frame and return reference to the output buffer
pub fn convert(&mut self, input: &[u8]) -> Result<&[u8]> {
let width = self.resolution.width as i32;
let height = self.resolution.height as i32;
let expected_size = self.output_buffer.len();
match (self.src_format, self.dst_format) {
(PixelFormat::Yuyv, PixelFormat::Yuv420) => {
libyuv::yuy2_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
.map_err(|e| {
AppError::VideoError(format!("libyuv conversion failed: {}", e))
})?;
}
(PixelFormat::Uyvy, PixelFormat::Yuv420) => {
libyuv::uyvy_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
.map_err(|e| {
AppError::VideoError(format!("libyuv conversion failed: {}", e))
})?;
}
(PixelFormat::Nv12, PixelFormat::Yuv420) => {
libyuv::nv12_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
.map_err(|e| {
AppError::VideoError(format!("libyuv conversion failed: {}", e))
})?;
}
(PixelFormat::Nv21, PixelFormat::Yuv420) => {
libyuv::nv21_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
.map_err(|e| {
AppError::VideoError(format!("libyuv conversion failed: {}", e))
})?;
}
(PixelFormat::Nv16, PixelFormat::Yuv420) => {
self.convert_nv16_to_yuv420p(input)?;
}
(PixelFormat::Nv24, PixelFormat::Yuv420) => {
self.convert_nv24_to_yuv420p(input)?;
}
(PixelFormat::Rgb24, PixelFormat::Yuv420) => {
libyuv::rgb24_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
.map_err(|e| {
AppError::VideoError(format!("libyuv conversion failed: {}", e))
})?;
}
(PixelFormat::Bgr24, PixelFormat::Yuv420) => {
libyuv::bgr24_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
.map_err(|e| {
AppError::VideoError(format!("libyuv conversion failed: {}", e))
})?;
}
(PixelFormat::Yvyu, PixelFormat::Yuv420) => {
// YVYU is not directly supported by libyuv, use software conversion
self.convert_yvyu_to_yuv420p_sw(input)?;
}
(PixelFormat::Yvu420, PixelFormat::Yuv420) => {
// YVU420 just swaps U and V planes
self.convert_yvu420_to_yuv420p_sw(input)?;
}
(PixelFormat::Yuv420, PixelFormat::Yuv420) => {
// No conversion needed, just copy
if input.len() < expected_size {
return Err(AppError::VideoError(format!(
"Input buffer too small: {} < {}",
input.len(),
expected_size
)));
}
self.output_buffer
.as_bytes_mut()
.copy_from_slice(&input[..expected_size]);
}
_ => {
return Err(AppError::VideoError(format!(
"Unsupported conversion: {}{}",
self.src_format, self.dst_format
)));
}
};
Ok(self.output_buffer.as_bytes())
}
/// Get output buffer length
pub fn output_len(&self) -> usize {
self.output_buffer.len()
}
/// Get resolution
pub fn resolution(&self) -> Resolution {
self.resolution
}
/// Software conversion for YVYU (not supported by libyuv)
fn convert_yvyu_to_yuv420p_sw(&mut self, yvyu: &[u8]) -> Result<()> {
let width = self.resolution.width as usize;
let height = self.resolution.height as usize;
let y_size = width * height;
let uv_size = y_size / 4;
let half_width = width / 2;
let data = self.output_buffer.as_bytes_mut();
let (y_plane, uv_planes) = data.split_at_mut(y_size);
let (u_plane, v_plane) = uv_planes.split_at_mut(uv_size);
for row in (0..height).step_by(2) {
let yvyu_row0_offset = row * width * 2;
let yvyu_row1_offset = (row + 1) * width * 2;
let y_row0_offset = row * width;
let y_row1_offset = (row + 1) * width;
let uv_row_offset = (row / 2) * half_width;
for col in (0..width).step_by(2) {
let yvyu_offset0 = yvyu_row0_offset + col * 2;
let yvyu_offset1 = yvyu_row1_offset + col * 2;
// YVYU: Y0, V0, Y1, U0
let y0_0 = yvyu[yvyu_offset0];
let v0 = yvyu[yvyu_offset0 + 1];
let y0_1 = yvyu[yvyu_offset0 + 2];
let u0 = yvyu[yvyu_offset0 + 3];
let y1_0 = yvyu[yvyu_offset1];
let v1 = yvyu[yvyu_offset1 + 1];
let y1_1 = yvyu[yvyu_offset1 + 2];
let u1 = yvyu[yvyu_offset1 + 3];
y_plane[y_row0_offset + col] = y0_0;
y_plane[y_row0_offset + col + 1] = y0_1;
y_plane[y_row1_offset + col] = y1_0;
y_plane[y_row1_offset + col + 1] = y1_1;
let uv_idx = uv_row_offset + col / 2;
u_plane[uv_idx] = ((u0 as u16 + u1 as u16) / 2) as u8;
v_plane[uv_idx] = ((v0 as u16 + v1 as u16) / 2) as u8;
}
}
Ok(())
}
/// Software conversion for YVU420 (just swap U and V)
fn convert_yvu420_to_yuv420p_sw(&mut self, yvu420: &[u8]) -> Result<()> {
let width = self.resolution.width as usize;
let height = self.resolution.height as usize;
let y_size = width * height;
let uv_size = y_size / 4;
let data = self.output_buffer.as_bytes_mut();
let (y_plane, uv_planes) = data.split_at_mut(y_size);
let (u_plane, v_plane) = uv_planes.split_at_mut(uv_size);
// Copy Y plane directly
y_plane.copy_from_slice(&yvu420[..y_size]);
// In YVU420, V comes before U
let v_src = &yvu420[y_size..y_size + uv_size];
let u_src = &yvu420[y_size + uv_size..];
// Swap U and V
u_plane.copy_from_slice(u_src);
v_plane.copy_from_slice(v_src);
Ok(())
}
/// Convert NV16 (4:2:2 semiplanar) → YUV420P using libyuv split + I422 downsample
fn convert_nv16_to_yuv420p(&mut self, nv16: &[u8]) -> Result<()> {
let width = self.resolution.width as usize;
let height = self.resolution.height as usize;
let y_size = width * height;
let uv_size = y_size;
if nv16.len() < y_size + uv_size {
return Err(AppError::VideoError(format!(
"NV16 data too small: {} < {}",
nv16.len(),
y_size + uv_size
)));
}
let src_uv = &nv16[y_size..y_size + uv_size];
let chroma_plane_size = y_size / 2;
let (u_plane_422, rest) = self.uv_split_buffer.split_at_mut(chroma_plane_size);
let (v_plane_422, _) = rest.split_at_mut(chroma_plane_size);
libyuv::split_uv_plane(
src_uv,
width as i32,
u_plane_422,
(width / 2) as i32,
v_plane_422,
(width / 2) as i32,
(width / 2) as i32,
height as i32,
)
.map_err(|e| AppError::VideoError(format!("libyuv NV16 split failed: {}", e)))?;
libyuv::i422_to_i420_planar(
&nv16[..y_size],
width as i32,
u_plane_422,
(width / 2) as i32,
v_plane_422,
(width / 2) as i32,
self.output_buffer.as_bytes_mut(),
width as i32,
height as i32,
)
.map_err(|e| AppError::VideoError(format!("libyuv NV16→I420 failed: {}", e)))?;
Ok(())
}
/// Convert NV24 (4:4:4 semiplanar) → YUV420P using libyuv split + I444 downsample
fn convert_nv24_to_yuv420p(&mut self, nv24: &[u8]) -> Result<()> {
let width = self.resolution.width as usize;
let height = self.resolution.height as usize;
let y_size = width * height;
let uv_size = y_size * 2;
if nv24.len() < y_size + uv_size {
return Err(AppError::VideoError(format!(
"NV24 data too small: {} < {}",
nv24.len(),
y_size + uv_size
)));
}
let src_uv = &nv24[y_size..y_size + uv_size];
let chroma_plane_size = y_size;
let (u_plane_444, rest) = self.uv_split_buffer.split_at_mut(chroma_plane_size);
let (v_plane_444, _) = rest.split_at_mut(chroma_plane_size);
libyuv::split_uv_plane(
src_uv,
(width * 2) as i32,
u_plane_444,
width as i32,
v_plane_444,
width as i32,
width as i32,
height as i32,
)
.map_err(|e| AppError::VideoError(format!("libyuv NV24 split failed: {}", e)))?;
libyuv::i444_to_i420_planar(
&nv24[..y_size],
width as i32,
u_plane_444,
width as i32,
v_plane_444,
width as i32,
self.output_buffer.as_bytes_mut(),
width as i32,
height as i32,
)
.map_err(|e| AppError::VideoError(format!("libyuv NV24→I420 failed: {}", e)))?;
Ok(())
}
}
/// Calculate YUV420P buffer size for a given resolution
pub fn yuv420p_buffer_size(resolution: Resolution) -> usize {
let pixels = (resolution.width * resolution.height) as usize;
pixels + pixels / 2
}
/// Calculate YUYV buffer size for a given resolution
pub fn yuyv_buffer_size(resolution: Resolution) -> usize {
(resolution.width * resolution.height * 2) as usize
}
// ============================================================================
// NV12 Converter for VAAPI encoder (using libyuv)
// ============================================================================
/// Pixel format converter that outputs NV12 (for VAAPI encoders)
pub struct Nv12Converter {
/// Source format
src_format: PixelFormat,
/// Frame resolution
resolution: Resolution,
/// Output buffer (reused across conversions)
output_buffer: Nv12Buffer,
/// Optional I420 buffer for intermediate conversions
i420_buffer: Option<Yuv420pBuffer>,
}
impl Nv12Converter {
/// Create a new converter for BGR24 → NV12
pub fn bgr24_to_nv12(resolution: Resolution) -> Self {
Self {
src_format: PixelFormat::Bgr24,
resolution,
output_buffer: Nv12Buffer::new(resolution),
i420_buffer: None,
}
}
/// Create a new converter for RGB24 → NV12
pub fn rgb24_to_nv12(resolution: Resolution) -> Self {
Self {
src_format: PixelFormat::Rgb24,
resolution,
output_buffer: Nv12Buffer::new(resolution),
i420_buffer: None,
}
}
/// Create a new converter for YUYV → NV12
pub fn yuyv_to_nv12(resolution: Resolution) -> Self {
Self {
src_format: PixelFormat::Yuyv,
resolution,
output_buffer: Nv12Buffer::new(resolution),
i420_buffer: None,
}
}
/// Create a new converter for YUV420P (I420) → NV12
pub fn yuv420_to_nv12(resolution: Resolution) -> Self {
Self {
src_format: PixelFormat::Yuv420,
resolution,
output_buffer: Nv12Buffer::new(resolution),
i420_buffer: None,
}
}
/// Create a new converter for NV21 → NV12
pub fn nv21_to_nv12(resolution: Resolution) -> Self {
Self {
src_format: PixelFormat::Nv21,
resolution,
output_buffer: Nv12Buffer::new(resolution),
i420_buffer: Some(Yuv420pBuffer::new(resolution)),
}
}
/// Create a new converter for NV16 → NV12 (downsample chroma vertically)
pub fn nv16_to_nv12(resolution: Resolution) -> Self {
Self {
src_format: PixelFormat::Nv16,
resolution,
output_buffer: Nv12Buffer::new(resolution),
i420_buffer: None,
}
}
/// Create a new converter for NV24 → NV12
pub fn nv24_to_nv12(resolution: Resolution) -> Self {
Self {
src_format: PixelFormat::Nv24,
resolution,
output_buffer: Nv12Buffer::new(resolution),
i420_buffer: None,
}
}
/// Convert a frame and return reference to the output buffer
pub fn convert(&mut self, input: &[u8]) -> Result<&[u8]> {
let width = self.resolution.width as i32;
let height = self.resolution.height as i32;
// Handle formats that need custom conversion without holding dst borrow
match self.src_format {
PixelFormat::Nv21 => {
let mut i420 = self.i420_buffer.take().ok_or_else(|| {
AppError::VideoError("NV21 I420 buffer not initialized".to_string())
})?;
{
let dst = self.output_buffer.as_bytes_mut();
Self::convert_nv21_to_nv12_with_dims(
self.resolution.width as usize,
self.resolution.height as usize,
input,
dst,
&mut i420,
)?;
}
self.i420_buffer = Some(i420);
return Ok(self.output_buffer.as_bytes());
}
PixelFormat::Nv16 => {
let dst = self.output_buffer.as_bytes_mut();
Self::convert_nv16_to_nv12_with_dims(
self.resolution.width as usize,
self.resolution.height as usize,
input,
dst,
)?;
return Ok(self.output_buffer.as_bytes());
}
PixelFormat::Nv24 => {
let dst = self.output_buffer.as_bytes_mut();
Self::convert_nv24_to_nv12_with_dims(
self.resolution.width as usize,
self.resolution.height as usize,
input,
dst,
)?;
return Ok(self.output_buffer.as_bytes());
}
_ => {}
}
let dst = self.output_buffer.as_bytes_mut();
let result = match self.src_format {
PixelFormat::Bgr24 => libyuv::bgr24_to_nv12(input, dst, width, height),
PixelFormat::Rgb24 => libyuv::rgb24_to_nv12(input, dst, width, height),
PixelFormat::Yuyv => libyuv::yuy2_to_nv12(input, dst, width, height),
PixelFormat::Yuv420 => libyuv::i420_to_nv12(input, dst, width, height),
_ => {
return Err(AppError::VideoError(format!(
"Unsupported conversion to NV12: {}",
self.src_format
)));
}
};
result
.map_err(|e| AppError::VideoError(format!("libyuv NV12 conversion failed: {}", e)))?;
Ok(self.output_buffer.as_bytes())
}
fn convert_nv21_to_nv12_with_dims(
width: usize,
height: usize,
input: &[u8],
dst: &mut [u8],
yuv: &mut Yuv420pBuffer,
) -> Result<()> {
libyuv::nv21_to_i420(input, yuv.as_bytes_mut(), width as i32, height as i32)
.map_err(|e| AppError::VideoError(format!("libyuv NV21->I420 failed: {}", e)))?;
libyuv::i420_to_nv12(yuv.as_bytes(), dst, width as i32, height as i32)
.map_err(|e| AppError::VideoError(format!("libyuv I420->NV12 failed: {}", e)))?;
Ok(())
}
fn convert_nv16_to_nv12_with_dims(
width: usize,
height: usize,
input: &[u8],
dst: &mut [u8],
) -> Result<()> {
let y_size = width * height;
let uv_size_nv16 = y_size; // NV16 chroma plane is full height
let uv_size_nv12 = y_size / 2;
if input.len() < y_size + uv_size_nv16 {
return Err(AppError::VideoError(format!(
"NV16 data too small: {} < {}",
input.len(),
y_size + uv_size_nv16
)));
}
// Copy Y plane as-is
dst[..y_size].copy_from_slice(&input[..y_size]);
// Downsample chroma vertically: average pairs of rows
let src_uv = &input[y_size..y_size + uv_size_nv16];
let dst_uv = &mut dst[y_size..y_size + uv_size_nv12];
let src_row_bytes = width;
let dst_row_bytes = width;
let dst_rows = height / 2;
for row in 0..dst_rows {
let src_row0 =
&src_uv[row * 2 * src_row_bytes..row * 2 * src_row_bytes + src_row_bytes];
let src_row1 = &src_uv
[(row * 2 + 1) * src_row_bytes..(row * 2 + 1) * src_row_bytes + src_row_bytes];
let dst_row = &mut dst_uv[row * dst_row_bytes..row * dst_row_bytes + dst_row_bytes];
for i in 0..dst_row_bytes {
let sum = src_row0[i] as u16 + src_row1[i] as u16;
dst_row[i] = (sum / 2) as u8;
}
}
Ok(())
}
fn convert_nv24_to_nv12_with_dims(
width: usize,
height: usize,
input: &[u8],
dst: &mut [u8],
) -> Result<()> {
let y_size = width * height;
let uv_size_nv24 = y_size * 2;
let uv_size_nv12 = y_size / 2;
if input.len() < y_size + uv_size_nv24 {
return Err(AppError::VideoError(format!(
"NV24 data too small: {} < {}",
input.len(),
y_size + uv_size_nv24
)));
}
dst[..y_size].copy_from_slice(&input[..y_size]);
let src_uv = &input[y_size..y_size + uv_size_nv24];
let dst_uv = &mut dst[y_size..y_size + uv_size_nv12];
let dst_rows = height / 2;
for row in 0..dst_rows {
let src_row0 = &src_uv[row * 2 * width * 2..row * 2 * width * 2 + width * 2];
let src_row1 =
&src_uv[(row * 2 + 1) * width * 2..(row * 2 + 1) * width * 2 + width * 2];
let dst_row = &mut dst_uv[row * width..row * width + width];
for pair in 0..(width / 2) {
let src_idx0 = pair * 4;
let src_idx1 = src_idx0 + 2;
let dst_idx = pair * 2;
dst_row[dst_idx] = ((src_row0[src_idx0] as u32
+ src_row0[src_idx1] as u32
+ src_row1[src_idx0] as u32
+ src_row1[src_idx1] as u32)
/ 4) as u8;
dst_row[dst_idx + 1] = ((src_row0[src_idx0 + 1] as u32
+ src_row0[src_idx1 + 1] as u32
+ src_row1[src_idx0 + 1] as u32
+ src_row1[src_idx1 + 1] as u32)
/ 4) as u8;
}
}
Ok(())
}
/// Get output buffer length
pub fn output_len(&self) -> usize {
self.output_buffer.len()
}
/// Get resolution
pub fn resolution(&self) -> Resolution {
self.resolution
}
}
// ============================================================================
// Standalone conversion functions (using libyuv)
// ============================================================================
/// Convert BGR24 to NV12 using libyuv
pub fn bgr_to_nv12(bgr: &[u8], nv12: &mut [u8], width: usize, height: usize) {
if let Err(e) = libyuv::bgr24_to_nv12(bgr, nv12, width as i32, height as i32) {
tracing::error!("libyuv BGR24→NV12 conversion failed: {}", e);
}
}
/// Convert RGB24 to NV12 using libyuv
pub fn rgb_to_nv12(rgb: &[u8], nv12: &mut [u8], width: usize, height: usize) {
if let Err(e) = libyuv::rgb24_to_nv12(rgb, nv12, width as i32, height as i32) {
tracing::error!("libyuv RGB24→NV12 conversion failed: {}", e);
}
}
/// Convert YUYV to NV12 using libyuv
pub fn yuyv_to_nv12(yuyv: &[u8], nv12: &mut [u8], width: usize, height: usize) {
if let Err(e) = libyuv::yuy2_to_nv12(yuyv, nv12, width as i32, height as i32) {
tracing::error!("libyuv YUYV→NV12 conversion failed: {}", e);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_yuv420p_buffer_creation() {
let buffer = Yuv420pBuffer::new(Resolution::HD720);
assert_eq!(buffer.len(), 1280 * 720 * 3 / 2);
assert_eq!(buffer.y_plane().len(), 1280 * 720);
assert_eq!(buffer.u_plane().len(), 1280 * 720 / 4);
assert_eq!(buffer.v_plane().len(), 1280 * 720 / 4);
}
#[test]
fn test_nv12_buffer_creation() {
let buffer = Nv12Buffer::new(Resolution::HD720);
assert_eq!(buffer.len(), 1280 * 720 * 3 / 2);
assert_eq!(buffer.y_plane().len(), 1280 * 720);
assert_eq!(buffer.uv_plane().len(), 1280 * 720 / 2);
}
#[test]
fn test_yuyv_to_yuv420p_conversion() {
let resolution = Resolution::new(4, 4);
let mut converter = PixelConverter::yuyv_to_yuv420p(resolution);
// Create YUYV data (4x4 = 32 bytes)
let yuyv = vec![
16, 128, 17, 129, 18, 130, 19, 131, 20, 132, 21, 133, 22, 134, 23, 135, 24, 136, 25,
137, 26, 138, 27, 139, 28, 140, 29, 141, 30, 142, 31, 143,
];
let result = converter.convert(&yuyv).unwrap();
assert_eq!(result.len(), 24); // 4*4 + 2*2 + 2*2 = 24 bytes
}
}

528
src/video/codec/h264.rs Normal file
View File

@@ -0,0 +1,528 @@
//! H.264 encoder using hwcodec (rustdesk's FFmpeg wrapper)
//!
//! Supports multiple encoder backends via FFmpeg:
//! - VAAPI (Intel/AMD/NVIDIA on Linux)
//! - NVENC (NVIDIA)
//! - AMF (AMD)
//! - Software (libx264)
//!
//! The encoder is selected automatically based on availability.
use bytes::Bytes;
use std::sync::Once;
use tracing::{debug, error, info, warn};
use hwcodec::common::{Quality, RateControl};
use hwcodec::ffmpeg::{resolve_pixel_format, AVPixelFormat};
use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder};
use hwcodec::ffmpeg_ram::CodecInfo;
use super::detect_best_codec_for_format;
use super::registry::EncoderBackend;
use super::traits::{EncodedFormat, EncodedFrame, Encoder, EncoderConfig};
use crate::error::{AppError, Result};
use crate::video::format::{PixelFormat, Resolution};
static INIT_LOGGING: Once = Once::new();
/// Initialize hwcodec logging (only once)
fn init_hwcodec_logging() {
INIT_LOGGING.call_once(|| {
// hwcodec uses the `log` crate, which will work with our tracing subscriber
debug!("hwcodec logging initialized");
});
}
/// H.264 encoder type (detected from hwcodec)
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum H264EncoderType {
/// NVIDIA NVENC
Nvenc,
/// Intel Quick Sync (QSV)
Qsv,
/// AMD AMF
Amf,
/// VAAPI (Linux generic)
Vaapi,
/// RKMPP (Rockchip) - requires hwcodec extension
Rkmpp,
/// V4L2 M2M (ARM generic) - requires hwcodec extension
V4l2M2m,
/// Software encoding (libx264/openh264)
Software,
/// No encoder available
#[default]
None,
}
impl std::fmt::Display for H264EncoderType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
H264EncoderType::Nvenc => write!(f, "NVENC"),
H264EncoderType::Qsv => write!(f, "QSV"),
H264EncoderType::Amf => write!(f, "AMF"),
H264EncoderType::Vaapi => write!(f, "VAAPI"),
H264EncoderType::Rkmpp => write!(f, "RKMPP"),
H264EncoderType::V4l2M2m => write!(f, "V4L2 M2M"),
H264EncoderType::Software => write!(f, "Software"),
H264EncoderType::None => write!(f, "None"),
}
}
}
/// Map codec name to encoder type
impl From<EncoderBackend> for H264EncoderType {
fn from(backend: EncoderBackend) -> Self {
match backend {
EncoderBackend::Nvenc => H264EncoderType::Nvenc,
EncoderBackend::Qsv => H264EncoderType::Qsv,
EncoderBackend::Amf => H264EncoderType::Amf,
EncoderBackend::Vaapi => H264EncoderType::Vaapi,
EncoderBackend::Rkmpp => H264EncoderType::Rkmpp,
EncoderBackend::V4l2m2m => H264EncoderType::V4l2M2m,
EncoderBackend::Software => H264EncoderType::Software,
}
}
}
/// Input pixel format for H264 encoder
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum H264InputFormat {
/// YUV420P (I420) - planar Y, U, V
Yuv420p,
/// NV12 - Y plane + interleaved UV plane (optimal for VAAPI)
#[default]
Nv12,
/// NV21 - Y plane + interleaved VU plane
Nv21,
/// NV16 - Y plane + interleaved UV plane (4:2:2)
Nv16,
/// NV24 - Y plane + interleaved UV plane (4:4:4)
Nv24,
/// YUYV422 - packed YUV 4:2:2 format (optimal for RKMPP direct input)
Yuyv422,
/// RGB24 - packed RGB format (RKMPP direct input)
Rgb24,
/// BGR24 - packed BGR format (RKMPP direct input)
Bgr24,
}
/// H.264 encoder configuration
#[derive(Debug, Clone)]
pub struct H264Config {
/// Base encoder config
pub base: EncoderConfig,
/// Target bitrate in kbps
pub bitrate_kbps: u32,
/// GOP size (keyframe interval)
pub gop_size: u32,
/// Frame rate
pub fps: u32,
/// Input pixel format
pub input_format: H264InputFormat,
}
impl Default for H264Config {
fn default() -> Self {
Self {
base: EncoderConfig::default(),
bitrate_kbps: 1000,
gop_size: 30,
fps: 30,
input_format: H264InputFormat::Nv12,
}
}
}
impl H264Config {
/// Create config for low latency streaming with NV12 input (optimal for VAAPI)
pub fn low_latency(resolution: Resolution, bitrate_kbps: u32) -> Self {
Self {
base: EncoderConfig::h264(resolution, bitrate_kbps),
bitrate_kbps,
gop_size: 30,
fps: 30,
input_format: H264InputFormat::Nv12,
}
}
/// Create config for low latency streaming with YUV420P input
pub fn low_latency_yuv420p(resolution: Resolution, bitrate_kbps: u32) -> Self {
Self {
base: EncoderConfig::h264(resolution, bitrate_kbps),
bitrate_kbps,
gop_size: 30,
fps: 30,
input_format: H264InputFormat::Yuv420p,
}
}
/// Create config for low latency streaming with YUYV422 input (optimal for RKMPP direct input)
pub fn low_latency_yuyv422(resolution: Resolution, bitrate_kbps: u32) -> Self {
Self {
base: EncoderConfig::h264(resolution, bitrate_kbps),
bitrate_kbps,
gop_size: 30,
fps: 30,
input_format: H264InputFormat::Yuyv422,
}
}
/// Create config for quality streaming
pub fn quality(resolution: Resolution, bitrate_kbps: u32) -> Self {
Self {
base: EncoderConfig::h264(resolution, bitrate_kbps),
bitrate_kbps,
gop_size: 60,
fps: 30,
input_format: H264InputFormat::Nv12,
}
}
/// Set input format
pub fn with_input_format(mut self, format: H264InputFormat) -> Self {
self.input_format = format;
self
}
}
/// Get available H264 encoders from hwcodec
pub fn get_available_encoders(width: u32, height: u32) -> Vec<CodecInfo> {
init_hwcodec_logging();
let ctx = EncodeContext {
name: String::new(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt: resolve_pixel_format("yuv420p", AVPixelFormat::AV_PIX_FMT_YUV420P),
align: 1,
fps: 30,
gop: 30,
rc: RateControl::RC_CBR,
quality: Quality::Quality_Low, // Use low quality preset for fastest encoding (ultrafast)
kbs: 2000,
q: 23,
thread_count: 4,
};
HwEncoder::available_encoders(ctx, None)
}
/// Detect best available H.264 encoder
pub fn detect_best_encoder(width: u32, height: u32) -> (H264EncoderType, Option<String>) {
let encoders = get_available_encoders(width, height);
if let Some((encoder_type, codec_name)) =
detect_best_codec_for_format(&encoders, hwcodec::common::DataFormat::H264, |_| true)
{
info!("Best H.264 encoder: {} ({})", codec_name, encoder_type);
(encoder_type, Some(codec_name))
} else {
warn!("No H.264 encoders available from hwcodec");
(H264EncoderType::None, None)
}
}
/// Encoded frame from hwcodec (cloned for ownership)
#[derive(Debug, Clone)]
pub struct HwEncodeFrame {
pub data: Vec<u8>,
pub pts: i64,
pub key: i32,
}
/// H.264 encoder using hwcodec
pub struct H264Encoder {
/// hwcodec encoder instance
inner: HwEncoder,
/// Encoder configuration
config: H264Config,
/// Detected encoder type
encoder_type: H264EncoderType,
/// Codec name
codec_name: String,
/// Frame counter
frame_count: u64,
/// Required YUV buffer length from hwcodec
yuv_length: i32,
}
impl H264Encoder {
/// Create a new H.264 encoder with automatic codec detection
pub fn new(config: H264Config) -> Result<Self> {
init_hwcodec_logging();
let width = config.base.resolution.width;
let height = config.base.resolution.height;
// Detect best encoder
let (_encoder_type, codec_name) = detect_best_encoder(width, height);
let codec_name = codec_name
.ok_or_else(|| AppError::VideoError("No H.264 encoder available".to_string()))?;
Self::with_codec(config, &codec_name)
}
/// Create encoder with specific codec name
pub fn with_codec(config: H264Config, codec_name: &str) -> Result<Self> {
init_hwcodec_logging();
let width = config.base.resolution.width;
let height = config.base.resolution.height;
// Select pixel format based on config
let (pixfmt_name, pixfmt_fallback) = match config.input_format {
H264InputFormat::Nv12 => ("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
H264InputFormat::Nv21 => ("nv21", AVPixelFormat::AV_PIX_FMT_NV21),
H264InputFormat::Nv16 => ("nv16", AVPixelFormat::AV_PIX_FMT_NV16),
H264InputFormat::Nv24 => ("nv24", AVPixelFormat::AV_PIX_FMT_NV24),
H264InputFormat::Yuv420p => ("yuv420p", AVPixelFormat::AV_PIX_FMT_YUV420P),
H264InputFormat::Yuyv422 => ("yuyv422", AVPixelFormat::AV_PIX_FMT_YUYV422),
H264InputFormat::Rgb24 => ("rgb24", AVPixelFormat::AV_PIX_FMT_RGB24),
H264InputFormat::Bgr24 => ("bgr24", AVPixelFormat::AV_PIX_FMT_BGR24),
};
let pixfmt = resolve_pixel_format(pixfmt_name, pixfmt_fallback);
info!(
"Creating H.264 encoder: {} at {}x{} @ {} kbps (input: {:?})",
codec_name, width, height, config.bitrate_kbps, config.input_format
);
let ctx = EncodeContext {
name: codec_name.to_string(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt,
align: 1,
fps: config.fps as i32,
gop: config.gop_size as i32,
rc: RateControl::RC_CBR,
quality: Quality::Quality_Low, // Use low quality preset for fastest encoding (lowest latency)
kbs: config.bitrate_kbps as i32,
q: 23,
thread_count: 4, // Use 4 threads for better performance
};
let inner = HwEncoder::new(ctx).map_err(|_| {
AppError::VideoError(format!("Failed to create encoder: {}", codec_name))
})?;
let yuv_length = inner.length;
let encoder_type = H264EncoderType::from(EncoderBackend::from_codec_name(codec_name));
info!(
"H.264 encoder created: {} (type: {}, buffer_length: {}, input_format: {:?})",
codec_name, encoder_type, yuv_length, config.input_format
);
Ok(Self {
inner,
config,
encoder_type,
codec_name: codec_name.to_string(),
frame_count: 0,
yuv_length,
})
}
/// Create with auto-detected encoder
pub fn auto(resolution: Resolution, bitrate_kbps: u32) -> Result<Self> {
let config = H264Config::low_latency(resolution, bitrate_kbps);
Self::new(config)
}
/// Get encoder type
pub fn encoder_type(&self) -> &H264EncoderType {
&self.encoder_type
}
/// Get codec name
pub fn codec_name(&self) -> &str {
&self.codec_name
}
/// Update bitrate dynamically
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
self.inner
.set_bitrate(bitrate_kbps as i32)
.map_err(|_| AppError::VideoError("Failed to set bitrate".to_string()))?;
self.config.bitrate_kbps = bitrate_kbps;
debug!("Bitrate updated to {} kbps", bitrate_kbps);
Ok(())
}
/// Request next frame to be a keyframe (IDR)
pub fn request_keyframe(&mut self) {
self.inner.request_keyframe();
debug!("H264 keyframe requested");
}
/// Encode raw frame data (YUV420P or NV12 depending on config)
pub fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
if data.len() < self.yuv_length as usize {
return Err(AppError::VideoError(format!(
"Frame data too small: {} < {}",
data.len(),
self.yuv_length
)));
}
self.frame_count += 1;
match self.inner.encode(data, pts_ms) {
Ok(frames) => {
// Zero-copy: drain frames from hwcodec buffer instead of cloning
// hwcodec returns &mut Vec, so we can take ownership via drain
let owned_frames: Vec<HwEncodeFrame> = frames
.drain(..)
.map(|f| HwEncodeFrame {
data: f.data, // Move, not clone
pts: f.pts,
key: f.key,
})
.collect();
Ok(owned_frames)
}
Err(e) => {
error!("Encode failed: {}", e);
Err(AppError::VideoError(format!("Encode failed: {}", e)))
}
}
}
/// Encode YUV420P data (legacy method, use encode_raw for new code)
pub fn encode_yuv420p(&mut self, yuv_data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
self.encode_raw(yuv_data, pts_ms)
}
/// Encode NV12 data
pub fn encode_nv12(&mut self, nv12_data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
self.encode_raw(nv12_data, pts_ms)
}
/// Get input format
pub fn input_format(&self) -> H264InputFormat {
self.config.input_format
}
/// Get YUV buffer info (linesize, offset, length)
pub fn yuv_info(&self) -> (Vec<i32>, Vec<i32>, i32) {
(
self.inner.linesize.clone(),
self.inner.offset.clone(),
self.inner.length,
)
}
}
// SAFETY: H264Encoder contains hwcodec::ffmpeg_ram::encode::Encoder which has raw pointers
// that are not Send by default. However, we ensure that H264Encoder is only used from
// a single task/thread at a time (encoding is sequential), so this is safe.
// The raw pointers are internal FFmpeg context that doesn't escape the encoder.
unsafe impl Send for H264Encoder {}
impl Encoder for H264Encoder {
fn name(&self) -> &str {
&self.codec_name
}
fn output_format(&self) -> EncodedFormat {
EncodedFormat::H264
}
fn encode(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
// Assume input is YUV420P
let pts_ms = (sequence * 1000 / self.config.fps as u64) as i64;
let mut frames = self.encode_yuv420p(data, pts_ms)?;
if frames.is_empty() {
// Encoder needs more frames (shouldn't happen with our config)
warn!("Encoder returned no frames");
return Err(AppError::VideoError(
"Encoder returned no frames".to_string(),
));
}
// Take ownership of the first frame (zero-copy)
let frame = frames.remove(0);
let key_frame = frame.key == 1;
Ok(EncodedFrame::h264(
Bytes::from(frame.data), // Move Vec into Bytes (zero-copy)
self.config.base.resolution,
key_frame,
sequence,
frame.pts as u64,
frame.pts as u64,
))
}
fn flush(&mut self) -> Result<Vec<EncodedFrame>> {
// hwcodec doesn't have explicit flush, return empty
Ok(vec![])
}
fn reset(&mut self) -> Result<()> {
self.frame_count = 0;
Ok(())
}
fn config(&self) -> &EncoderConfig {
&self.config.base
}
fn supports_format(&self, format: PixelFormat) -> bool {
// Check if the format matches our configured input format
match self.config.input_format {
H264InputFormat::Nv12 => matches!(format, PixelFormat::Nv12),
H264InputFormat::Nv21 => matches!(format, PixelFormat::Nv21),
H264InputFormat::Nv16 => matches!(format, PixelFormat::Nv16),
H264InputFormat::Nv24 => matches!(format, PixelFormat::Nv24),
H264InputFormat::Yuv420p => matches!(format, PixelFormat::Yuv420),
H264InputFormat::Yuyv422 => matches!(format, PixelFormat::Yuyv),
H264InputFormat::Rgb24 => matches!(format, PixelFormat::Rgb24),
H264InputFormat::Bgr24 => matches!(format, PixelFormat::Bgr24),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_encoder() {
let (encoder_type, codec_name) = detect_best_encoder(1280, 720);
println!("Detected encoder: {:?} ({:?})", encoder_type, codec_name);
}
#[test]
fn test_available_encoders() {
let encoders = get_available_encoders(1280, 720);
println!("Available encoders:");
for enc in &encoders {
println!(" - {} ({:?})", enc.name, enc.format);
}
}
#[test]
fn test_create_encoder() {
let config = H264Config::low_latency(Resolution::HD720, 2000);
match H264Encoder::new(config) {
Ok(encoder) => {
println!(
"Created encoder: {} ({})",
encoder.codec_name(),
encoder.encoder_type()
);
}
Err(e) => {
println!("Failed to create encoder: {}", e);
}
}
}
}

View File

@@ -0,0 +1,299 @@
//! H.264 Annex-B/AVCC bitstream helpers shared by WebRTC, RTSP and RustDesk.
pub const FALLBACK_WEBRTC_PROFILE_LEVEL_ID: &str = "42e01f";
pub fn webrtc_fmtp_line(profile_level_id: &str) -> String {
format!(
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id={}",
profile_level_id
)
}
pub fn fallback_webrtc_fmtp_line() -> String {
webrtc_fmtp_line(FALLBACK_WEBRTC_PROFILE_LEVEL_ID)
}
pub fn strip_aud_nal_units(data: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(data.len());
let mut i = 0;
while i < data.len() {
let (start_code_pos, start_code_len) = if i + 4 <= data.len()
&& data[i] == 0
&& data[i + 1] == 0
&& data[i + 2] == 0
&& data[i + 3] == 1
{
(i, 4)
} else if i + 3 <= data.len() && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
(i, 3)
} else {
i += 1;
continue;
};
let nal_start = start_code_pos + start_code_len;
if nal_start >= data.len() {
break;
}
let nal_type = data[nal_start] & 0x1F;
let mut nal_end = data.len();
let mut j = nal_start + 1;
while j + 3 <= data.len() {
if (data[j] == 0 && data[j + 1] == 0 && data[j + 2] == 1)
|| (j + 4 <= data.len()
&& data[j] == 0
&& data[j + 1] == 0
&& data[j + 2] == 0
&& data[j + 3] == 1)
{
nal_end = j;
break;
}
j += 1;
}
if nal_type != 9 && nal_type != 12 {
result.extend_from_slice(&data[start_code_pos..nal_end]);
}
i = nal_end;
}
if result.is_empty() && !data.is_empty() {
return data.to_vec();
}
result
}
pub fn extract_sps_pps(data: &[u8]) -> (Option<Vec<u8>>, Option<Vec<u8>>) {
let mut sps: Option<Vec<u8>> = None;
let mut pps: Option<Vec<u8>> = None;
let mut i = 0;
while i < data.len() {
let start_code_len = if i + 4 <= data.len()
&& data[i] == 0
&& data[i + 1] == 0
&& data[i + 2] == 0
&& data[i + 3] == 1
{
4
} else if i + 3 <= data.len() && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
3
} else {
i += 1;
continue;
};
let nal_start = i + start_code_len;
if nal_start >= data.len() {
break;
}
let nal_type = data[nal_start] & 0x1F;
let mut nal_end = data.len();
let mut j = nal_start + 1;
while j + 3 <= data.len() {
if (data[j] == 0 && data[j + 1] == 0 && data[j + 2] == 1)
|| (j + 4 <= data.len()
&& data[j] == 0
&& data[j + 1] == 0
&& data[j + 2] == 0
&& data[j + 3] == 1)
{
nal_end = j;
break;
}
j += 1;
}
match nal_type {
7 => {
sps = Some(data[nal_start..nal_end].to_vec());
}
8 => {
pps = Some(data[nal_start..nal_end].to_vec());
}
_ => {}
}
i = nal_end;
}
(sps, pps)
}
pub fn has_sps_pps(data: &[u8]) -> bool {
let mut has_sps = false;
let mut has_pps = false;
let mut i = 0;
while i < data.len() {
let start_code_len = if i + 4 <= data.len()
&& data[i] == 0
&& data[i + 1] == 0
&& data[i + 2] == 0
&& data[i + 3] == 1
{
4
} else if i + 3 <= data.len() && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
3
} else {
i += 1;
continue;
};
let nal_start = i + start_code_len;
if nal_start >= data.len() {
break;
}
let nal_type = data[nal_start] & 0x1F;
match nal_type {
7 => has_sps = true,
8 => has_pps = true,
_ => {}
}
if has_sps && has_pps {
return true;
}
i = nal_start + 1;
}
has_sps && has_pps
}
pub fn is_keyframe(data: &[u8]) -> bool {
let mut i = 0;
while i < data.len() {
if i + 3 < data.len() && data[i] == 0 && data[i + 1] == 0 {
let nal_start = if data[i + 2] == 1 {
i + 3
} else if i + 4 < data.len() && data[i + 2] == 0 && data[i + 3] == 1 {
i + 4
} else {
i += 1;
continue;
};
if nal_start < data.len() {
let nal_type = data[nal_start] & 0x1F;
if nal_type == 5 {
return true;
}
}
i = nal_start;
} else {
i += 1;
}
}
false
}
/// `profile-level-id` hex for SDP (`42001f` etc.); expects SPS NAL without start code.
pub fn parse_profile_level_id_from_sps(sps: &[u8]) -> Option<String> {
if sps.len() < 4 {
return None;
}
let profile_idc = sps[1];
let constraint_set_flags = sps[2];
let level_idc = sps[3];
Some(format!(
"{:02x}{:02x}{:02x}",
profile_idc, constraint_set_flags, level_idc
))
}
pub fn extract_profile_level_id(data: &[u8]) -> Option<String> {
let (sps, _) = extract_sps_pps(data);
sps.and_then(|sps_data| parse_profile_level_id_from_sps(&sps_data))
}
pub fn is_annex_b(data: &[u8]) -> bool {
data.starts_with(&[0, 0, 1]) || data.starts_with(&[0, 0, 0, 1])
}
pub fn avcc_to_annex_b(data: &[u8]) -> Option<Vec<u8>> {
let mut pos = 0;
let mut output = Vec::with_capacity(data.len() + 16);
let mut nalu_count = 0usize;
while pos + 4 <= data.len() {
let nalu_len =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
pos += 4;
if nalu_len == 0 || pos + nalu_len > data.len() {
return None;
}
let nal_type = data[pos] & 0x1F;
if nal_type != 9 && nal_type != 12 {
output.extend_from_slice(&[0, 0, 0, 1]);
output.extend_from_slice(&data[pos..pos + nalu_len]);
}
nalu_count += 1;
pos += nalu_len;
}
if pos == data.len() && nalu_count > 0 && !output.is_empty() {
Some(output)
} else {
None
}
}
pub fn normalize_for_webrtc(data: &[u8]) -> Vec<u8> {
if is_annex_b(data) {
return strip_aud_nal_units(data);
}
if let Some(annex_b) = avcc_to_annex_b(data) {
return strip_aud_nal_units(&annex_b);
}
data.to_vec()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_h264_keyframes() {
let idr_frame = vec![0x00, 0x00, 0x00, 0x01, 0x65];
assert!(is_keyframe(&idr_frame));
let idr_frame_3 = vec![0x00, 0x00, 0x01, 0x65];
assert!(is_keyframe(&idr_frame_3));
let p_frame = vec![0x00, 0x00, 0x00, 0x01, 0x41];
assert!(!is_keyframe(&p_frame));
let sps = vec![0x00, 0x00, 0x00, 0x01, 0x67];
assert!(!is_keyframe(&sps));
let multi_nal = vec![
0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x01, 0x68, 0xce,
0x38, 0x80, 0x00, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84,
];
assert!(is_keyframe(&multi_nal));
}
#[test]
fn parses_profile_level_id_from_sps() {
assert_eq!(
parse_profile_level_id_from_sps(&[0x67, 0x42, 0x40, 0x2a]),
Some("42402a".to_string())
);
}
}

635
src/video/codec/h265.rs Normal file
View File

@@ -0,0 +1,635 @@
//! H.265/HEVC encoder using hwcodec (FFmpeg wrapper)
//!
//! Supports both hardware and software encoding:
//! - Hardware: VAAPI, NVENC, QSV, AMF, RKMPP, V4L2 M2M
//! - Software: libx265 (CPU-based, high CPU usage)
//!
//! Hardware encoding is preferred when available for better performance.
use bytes::Bytes;
use std::sync::Once;
use tracing::{debug, error, info, warn};
use hwcodec::common::{DataFormat, Quality, RateControl};
use hwcodec::ffmpeg::{resolve_pixel_format, AVPixelFormat};
use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder};
use hwcodec::ffmpeg_ram::CodecInfo;
use super::detect_best_codec_for_format;
use super::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType};
use super::traits::{EncodedFormat, EncodedFrame, Encoder, EncoderConfig};
use crate::error::{AppError, Result};
use crate::video::format::{PixelFormat, Resolution};
static INIT_LOGGING: Once = Once::new();
/// Initialize hwcodec logging (only once)
fn init_hwcodec_logging() {
INIT_LOGGING.call_once(|| {
debug!("hwcodec logging initialized for H265");
});
}
/// H.265 encoder type (detected from hwcodec)
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum H265EncoderType {
/// NVIDIA NVENC
Nvenc,
/// Intel Quick Sync (QSV)
Qsv,
/// AMD AMF
Amf,
/// VAAPI (Linux generic)
Vaapi,
/// RKMPP (Rockchip)
Rkmpp,
/// V4L2 M2M (ARM generic)
V4l2M2m,
/// Software encoder (libx265)
Software,
/// No encoder available
#[default]
None,
}
impl std::fmt::Display for H265EncoderType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
H265EncoderType::Nvenc => write!(f, "NVENC"),
H265EncoderType::Qsv => write!(f, "QSV"),
H265EncoderType::Amf => write!(f, "AMF"),
H265EncoderType::Vaapi => write!(f, "VAAPI"),
H265EncoderType::Rkmpp => write!(f, "RKMPP"),
H265EncoderType::V4l2M2m => write!(f, "V4L2 M2M"),
H265EncoderType::Software => write!(f, "Software"),
H265EncoderType::None => write!(f, "None"),
}
}
}
impl From<EncoderBackend> for H265EncoderType {
fn from(backend: EncoderBackend) -> Self {
match backend {
EncoderBackend::Nvenc => H265EncoderType::Nvenc,
EncoderBackend::Qsv => H265EncoderType::Qsv,
EncoderBackend::Amf => H265EncoderType::Amf,
EncoderBackend::Vaapi => H265EncoderType::Vaapi,
EncoderBackend::Rkmpp => H265EncoderType::Rkmpp,
EncoderBackend::V4l2m2m => H265EncoderType::V4l2M2m,
EncoderBackend::Software => H265EncoderType::Software,
}
}
}
/// Input pixel format for H265 encoder
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum H265InputFormat {
/// YUV420P (I420) - planar Y, U, V
Yuv420p,
/// NV12 - Y plane + interleaved UV plane (optimal for hardware encoders)
#[default]
Nv12,
/// NV21 - Y plane + interleaved VU plane
Nv21,
/// NV16 - Y plane + interleaved UV plane (4:2:2)
Nv16,
/// NV24 - Y plane + interleaved UV plane (4:4:4)
Nv24,
/// YUYV422 - packed YUV 4:2:2 format (optimal for RKMPP direct input)
Yuyv422,
/// RGB24 - packed RGB format (RKMPP direct input)
Rgb24,
/// BGR24 - packed BGR format (RKMPP direct input)
Bgr24,
}
/// H.265 encoder configuration
#[derive(Debug, Clone)]
pub struct H265Config {
/// Base encoder config
pub base: EncoderConfig,
/// Target bitrate in kbps
pub bitrate_kbps: u32,
/// GOP size (keyframe interval)
pub gop_size: u32,
/// Frame rate
pub fps: u32,
/// Input pixel format
pub input_format: H265InputFormat,
}
impl Default for H265Config {
fn default() -> Self {
Self {
base: EncoderConfig::default(),
bitrate_kbps: 8000,
gop_size: 30,
fps: 30,
input_format: H265InputFormat::Nv12,
}
}
}
impl H265Config {
/// Create config for low latency streaming with NV12 input
pub fn low_latency(resolution: Resolution, bitrate_kbps: u32) -> Self {
Self {
base: EncoderConfig {
resolution,
input_format: PixelFormat::Nv12,
quality: bitrate_kbps,
fps: 30,
gop_size: 30,
},
bitrate_kbps,
gop_size: 30,
fps: 30,
input_format: H265InputFormat::Nv12,
}
}
/// Create config for low latency streaming with YUYV422 input (optimal for RKMPP direct input)
pub fn low_latency_yuyv422(resolution: Resolution, bitrate_kbps: u32) -> Self {
Self {
base: EncoderConfig {
resolution,
input_format: PixelFormat::Yuyv,
quality: bitrate_kbps,
fps: 30,
gop_size: 30,
},
bitrate_kbps,
gop_size: 30,
fps: 30,
input_format: H265InputFormat::Yuyv422,
}
}
/// Create config for quality streaming
pub fn quality(resolution: Resolution, bitrate_kbps: u32) -> Self {
Self {
base: EncoderConfig {
resolution,
input_format: PixelFormat::Nv12,
quality: bitrate_kbps,
fps: 30,
gop_size: 60,
},
bitrate_kbps,
gop_size: 60,
fps: 30,
input_format: H265InputFormat::Nv12,
}
}
/// Set input format
pub fn with_input_format(mut self, format: H265InputFormat) -> Self {
self.input_format = format;
self
}
}
/// Get available H265 hardware encoders from hwcodec
pub fn get_available_h265_encoders(width: u32, height: u32) -> Vec<CodecInfo> {
init_hwcodec_logging();
let ctx = EncodeContext {
name: String::new(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
align: 1,
fps: 30,
gop: 30,
rc: RateControl::RC_CBR,
quality: Quality::Quality_Default,
kbs: 2000,
q: 23,
thread_count: 1,
};
let all_encoders = HwEncoder::available_encoders(ctx, None);
// Include both hardware and software H265 encoders
all_encoders
.into_iter()
.filter(|e| e.format == DataFormat::H265)
.collect()
}
/// Detect best available H.265 encoder (hardware preferred, software fallback)
pub fn detect_best_h265_encoder(width: u32, height: u32) -> (H265EncoderType, Option<String>) {
let encoders = get_available_h265_encoders(width, height);
// Prefer hardware encoders over software (libx265)
// Hardware priority: NVENC > QSV > AMF > VAAPI > RKMPP > V4L2 M2M > Software
if let Some((encoder_type, codec_name)) =
detect_best_codec_for_format(&encoders, DataFormat::H265, |codec| {
!codec.name.contains("libx265")
})
{
info!("Selected H.265 encoder: {} ({})", codec_name, encoder_type);
(encoder_type, Some(codec_name))
} else {
warn!("No H.265 encoders available");
(H265EncoderType::None, None)
}
}
/// Check if H265 hardware encoding is available
pub fn is_h265_available() -> bool {
let registry = EncoderRegistry::global();
registry.is_codec_available(VideoEncoderType::H265)
}
/// Encoded frame from hwcodec (cloned for ownership)
#[derive(Debug, Clone)]
pub struct HwEncodeFrame {
pub data: Vec<u8>,
pub pts: i64,
pub key: i32,
}
/// H.265 encoder using hwcodec
pub struct H265Encoder {
/// hwcodec encoder instance
inner: HwEncoder,
/// Encoder configuration
config: H265Config,
/// Detected encoder type
encoder_type: H265EncoderType,
/// Codec name
codec_name: String,
/// Frame counter
frame_count: u64,
/// Required buffer length from hwcodec
buffer_length: i32,
}
impl H265Encoder {
/// Create a new H.265 encoder with automatic hardware codec detection
///
/// Returns an error if no hardware encoder is available.
pub fn new(config: H265Config) -> Result<Self> {
init_hwcodec_logging();
let width = config.base.resolution.width;
let height = config.base.resolution.height;
// Detect best hardware encoder
let (encoder_type, codec_name) = detect_best_h265_encoder(width, height);
if encoder_type == H265EncoderType::None {
return Err(AppError::VideoError(
"No H.265 encoder available. Please ensure FFmpeg is built with libx265 support."
.to_string(),
));
}
let codec_name = codec_name.unwrap();
Self::with_codec(config, &codec_name)
}
/// Create encoder with specific codec name
pub fn with_codec(config: H265Config, codec_name: &str) -> Result<Self> {
init_hwcodec_logging();
// Determine if this is a software encoder
let is_software = codec_name.contains("libx265");
// Warn about software encoder performance
if is_software {
warn!(
"Using software H.265 encoder (libx265) - high CPU usage expected. \
Hardware encoder is recommended for better performance."
);
}
let width = config.base.resolution.width;
let height = config.base.resolution.height;
// Software encoders (libx265) require YUV420P, hardware encoders use NV12 or YUYV422
let (pixfmt_name, pixfmt_fallback, actual_input_format) = if is_software {
(
"yuv420p",
AVPixelFormat::AV_PIX_FMT_YUV420P,
H265InputFormat::Yuv420p,
)
} else {
match config.input_format {
H265InputFormat::Nv12 => (
"nv12",
AVPixelFormat::AV_PIX_FMT_NV12,
H265InputFormat::Nv12,
),
H265InputFormat::Nv21 => (
"nv21",
AVPixelFormat::AV_PIX_FMT_NV21,
H265InputFormat::Nv21,
),
H265InputFormat::Nv16 => (
"nv16",
AVPixelFormat::AV_PIX_FMT_NV16,
H265InputFormat::Nv16,
),
H265InputFormat::Nv24 => (
"nv24",
AVPixelFormat::AV_PIX_FMT_NV24,
H265InputFormat::Nv24,
),
H265InputFormat::Yuv420p => (
"yuv420p",
AVPixelFormat::AV_PIX_FMT_YUV420P,
H265InputFormat::Yuv420p,
),
H265InputFormat::Yuyv422 => (
"yuyv422",
AVPixelFormat::AV_PIX_FMT_YUYV422,
H265InputFormat::Yuyv422,
),
H265InputFormat::Rgb24 => (
"rgb24",
AVPixelFormat::AV_PIX_FMT_RGB24,
H265InputFormat::Rgb24,
),
H265InputFormat::Bgr24 => (
"bgr24",
AVPixelFormat::AV_PIX_FMT_BGR24,
H265InputFormat::Bgr24,
),
}
};
let pixfmt = resolve_pixel_format(pixfmt_name, pixfmt_fallback);
info!(
"Creating H.265 encoder: {} at {}x{} @ {} kbps (input: {:?})",
codec_name, width, height, config.bitrate_kbps, actual_input_format
);
let ctx = EncodeContext {
name: codec_name.to_string(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt,
align: 1,
fps: config.fps as i32,
gop: config.gop_size as i32,
rc: RateControl::RC_CBR,
quality: Quality::Quality_Default,
kbs: config.bitrate_kbps as i32,
q: 23,
thread_count: 1,
};
let inner = HwEncoder::new(ctx).map_err(|_| {
AppError::VideoError(format!("Failed to create H.265 encoder: {}", codec_name))
})?;
let buffer_length = inner.length;
let backend = EncoderBackend::from_codec_name(codec_name);
let encoder_type = H265EncoderType::from(backend);
// Update config to reflect actual input format used
let mut config = config;
config.input_format = actual_input_format;
info!(
"H.265 encoder created: {} (type: {}, buffer_length: {})",
codec_name, encoder_type, buffer_length
);
Ok(Self {
inner,
config,
encoder_type,
codec_name: codec_name.to_string(),
frame_count: 0,
buffer_length,
})
}
/// Create with auto-detected encoder
pub fn auto(resolution: Resolution, bitrate_kbps: u32) -> Result<Self> {
let config = H265Config::low_latency(resolution, bitrate_kbps);
Self::new(config)
}
/// Get encoder type
pub fn encoder_type(&self) -> &H265EncoderType {
&self.encoder_type
}
/// Get codec name
pub fn codec_name(&self) -> &str {
&self.codec_name
}
/// Update bitrate dynamically
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
self.inner
.set_bitrate(bitrate_kbps as i32)
.map_err(|_| AppError::VideoError("Failed to set H.265 bitrate".to_string()))?;
self.config.bitrate_kbps = bitrate_kbps;
debug!("H.265 bitrate updated to {} kbps", bitrate_kbps);
Ok(())
}
/// Request next frame to be a keyframe (IDR)
pub fn request_keyframe(&mut self) {
self.inner.request_keyframe();
debug!("H265 keyframe requested");
}
/// Encode raw frame data (NV12 or YUV420P depending on config)
pub fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
if data.len() < self.buffer_length as usize {
return Err(AppError::VideoError(format!(
"Frame data too small: {} < {}",
data.len(),
self.buffer_length
)));
}
self.frame_count += 1;
// Debug log every 30 frames (1 second at 30fps)
if self.frame_count % 30 == 1 {
debug!(
"[H265] Encoding frame #{}: input_size={}, pts_ms={}, codec={}",
self.frame_count,
data.len(),
pts_ms,
self.codec_name
);
}
match self.inner.encode(data, pts_ms) {
Ok(frames) => {
// Zero-copy: drain frames from hwcodec buffer instead of cloning
let owned_frames: Vec<HwEncodeFrame> = frames
.drain(..)
.map(|f| HwEncodeFrame {
data: f.data, // Move, not clone
pts: f.pts,
key: f.key,
})
.collect();
// Log encoded output
if !owned_frames.is_empty() {
let total_size: usize = owned_frames.iter().map(|f| f.data.len()).sum();
let keyframe = owned_frames.iter().any(|f| f.key == 1);
if keyframe || self.frame_count % 30 == 1 {
debug!(
"[H265] Encoded frame #{}: output_size={}, keyframe={}, frame_count={}",
self.frame_count,
total_size,
keyframe,
owned_frames.len()
);
// Log first few bytes of keyframe for debugging
if keyframe && !owned_frames[0].data.is_empty() {
let preview_len = owned_frames[0].data.len().min(32);
debug!(
"[H265] Keyframe data preview: {:02x?}",
&owned_frames[0].data[..preview_len]
);
}
}
} else {
warn!(
"[H265] Encoder returned empty frame list for frame #{}",
self.frame_count
);
}
Ok(owned_frames)
}
Err(e) => {
error!("[H265] Encode failed at frame #{}: {}", self.frame_count, e);
Err(AppError::VideoError(format!("H.265 encode failed: {}", e)))
}
}
}
/// Encode NV12 data
pub fn encode_nv12(&mut self, nv12_data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
self.encode_raw(nv12_data, pts_ms)
}
/// Get input format
pub fn input_format(&self) -> H265InputFormat {
self.config.input_format
}
/// Get buffer info (linesize, offset, length)
pub fn buffer_info(&self) -> (Vec<i32>, Vec<i32>, i32) {
(
self.inner.linesize.clone(),
self.inner.offset.clone(),
self.inner.length,
)
}
}
// SAFETY: H265Encoder contains hwcodec::ffmpeg_ram::encode::Encoder which has raw pointers
// that are not Send by default. However, we ensure that H265Encoder is only used from
// a single task/thread at a time (encoding is sequential), so this is safe.
unsafe impl Send for H265Encoder {}
impl Encoder for H265Encoder {
fn name(&self) -> &str {
&self.codec_name
}
fn output_format(&self) -> EncodedFormat {
EncodedFormat::H265
}
fn encode(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
let pts_ms = (sequence * 1000 / self.config.fps as u64) as i64;
let mut frames = self.encode_raw(data, pts_ms)?;
if frames.is_empty() {
warn!("H.265 encoder returned no frames");
return Err(AppError::VideoError(
"H.265 encoder returned no frames".to_string(),
));
}
// Take ownership of the first frame (zero-copy)
let frame = frames.remove(0);
let key_frame = frame.key == 1;
Ok(EncodedFrame {
data: Bytes::from(frame.data), // Move Vec into Bytes (zero-copy)
format: EncodedFormat::H265,
resolution: self.config.base.resolution,
key_frame,
sequence,
timestamp: std::time::Instant::now(),
pts: frame.pts as u64,
dts: frame.pts as u64,
})
}
fn flush(&mut self) -> Result<Vec<EncodedFrame>> {
Ok(vec![])
}
fn reset(&mut self) -> Result<()> {
self.frame_count = 0;
Ok(())
}
fn config(&self) -> &EncoderConfig {
&self.config.base
}
fn supports_format(&self, format: PixelFormat) -> bool {
match self.config.input_format {
H265InputFormat::Nv12 => matches!(format, PixelFormat::Nv12),
H265InputFormat::Nv21 => matches!(format, PixelFormat::Nv21),
H265InputFormat::Nv16 => matches!(format, PixelFormat::Nv16),
H265InputFormat::Nv24 => matches!(format, PixelFormat::Nv24),
H265InputFormat::Yuv420p => matches!(format, PixelFormat::Yuv420),
H265InputFormat::Yuyv422 => matches!(format, PixelFormat::Yuyv),
H265InputFormat::Rgb24 => matches!(format, PixelFormat::Rgb24),
H265InputFormat::Bgr24 => matches!(format, PixelFormat::Bgr24),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_h265_encoder() {
let (encoder_type, codec_name) = detect_best_h265_encoder(1280, 720);
println!(
"Detected H.265 encoder: {:?} ({:?})",
encoder_type, codec_name
);
}
#[test]
fn test_available_h265_encoders() {
let encoders = get_available_h265_encoders(1280, 720);
println!("Available H.265 hardware encoders:");
for enc in &encoders {
println!(" - {} ({:?})", enc.name, enc.format);
}
}
#[test]
fn test_h265_availability() {
let available = is_h265_available();
println!("H.265 hardware encoding available: {}", available);
}
}

404
src/video/codec/jpeg.rs Normal file
View File

@@ -0,0 +1,404 @@
//! JPEG encoder implementation
//!
//! Provides JPEG encoding for raw video frames (YUYV, NV12, NV16, NV24, RGB, BGR)
//! Uses libyuv for SIMD-accelerated color space conversion to I420,
//! then turbojpeg for direct YUV encoding (skips internal color conversion).
use bytes::Bytes;
use super::traits::{EncodedFormat, EncodedFrame, EncoderConfig};
use crate::error::{AppError, Result};
use crate::video::format::{PixelFormat, Resolution};
/// JPEG encoder using libyuv + turbojpeg
///
/// Encoding pipeline (all SIMD accelerated):
/// ```text
/// YUYV/NV12/NV16/NV24/BGR24/RGB24 ──libyuv──> I420 ──turbojpeg──> JPEG
/// ```
///
/// Note: This encoder is NOT thread-safe due to turbojpeg limitations.
/// Use it from a single thread or wrap in a Mutex.
pub struct JpegEncoder {
config: EncoderConfig,
compressor: turbojpeg::Compressor,
/// I420 buffer for YUV encoding (Y + U + V planes)
i420_buffer: Vec<u8>,
/// Scratch buffer for split chroma planes when converting semiplanar 4:2:2 / 4:4:4 input.
uv_split_buffer: Vec<u8>,
/// BGRA buffer used when a source format needs explicit YUV matrix expansion before JPEG.
bgra_buffer: Vec<u8>,
}
impl JpegEncoder {
/// Create a new JPEG encoder
pub fn new(config: EncoderConfig) -> Result<Self> {
let resolution = config.resolution;
let width = resolution.width as usize;
let height = resolution.height as usize;
// I420: Y = width*height, U = width*height/4, V = width*height/4
let i420_size = width * height * 3 / 2;
let max_uv_plane_size = width * height;
let bgra_size = width * height * 4;
let mut compressor = turbojpeg::Compressor::new().map_err(|e| {
AppError::VideoError(format!("Failed to create turbojpeg compressor: {}", e))
})?;
compressor
.set_quality(config.quality.min(100) as i32)
.map_err(|e| AppError::VideoError(format!("Failed to set JPEG quality: {}", e)))?;
Ok(Self {
config,
compressor,
i420_buffer: vec![0u8; i420_size],
uv_split_buffer: vec![0u8; max_uv_plane_size * 2],
bgra_buffer: vec![0u8; bgra_size],
})
}
/// Create with specific quality
pub fn with_quality(resolution: Resolution, quality: u32) -> Result<Self> {
let config = EncoderConfig::jpeg(resolution, quality);
Self::new(config)
}
/// Set JPEG quality (1-100)
pub fn set_quality(&mut self, quality: u32) -> Result<()> {
self.compressor
.set_quality(quality.min(100) as i32)
.map_err(|e| AppError::VideoError(format!("Failed to set JPEG quality: {}", e)))?;
self.config.quality = quality;
Ok(())
}
/// Encode I420 buffer to JPEG using turbojpeg's YUV encoder
#[inline]
fn encode_i420_to_jpeg(&mut self, sequence: u64) -> Result<EncodedFrame> {
let width = self.config.resolution.width as usize;
let height = self.config.resolution.height as usize;
// Create YuvImage for turbojpeg (I420 = YUV420 = Sub2x2)
let yuv_image = turbojpeg::YuvImage {
pixels: self.i420_buffer.as_slice(),
width,
height,
align: 1, // No padding between rows
subsamp: turbojpeg::Subsamp::Sub2x2, // YUV 4:2:0
};
// Compress YUV directly to JPEG (skips color space conversion!)
let jpeg_data = self
.compressor
.compress_yuv_to_vec(yuv_image)
.map_err(|e| AppError::VideoError(format!("JPEG compression failed: {}", e)))?;
Ok(EncodedFrame::jpeg(
Bytes::from(jpeg_data),
self.config.resolution,
sequence,
))
}
/// Encode BGRA buffer to JPEG using turbojpeg's RGB path.
#[inline]
fn encode_bgra_to_jpeg(&mut self, sequence: u64) -> Result<EncodedFrame> {
let width = self.config.resolution.width as usize;
let height = self.config.resolution.height as usize;
self.compressor
.set_subsamp(turbojpeg::Subsamp::Sub2x2)
.map_err(|e| AppError::VideoError(format!("Failed to set JPEG subsampling: {}", e)))?;
let image = turbojpeg::Image {
pixels: self.bgra_buffer.as_slice(),
width,
pitch: width * 4,
height,
format: turbojpeg::PixelFormat::BGRA,
};
let jpeg_data = self
.compressor
.compress_to_vec(image)
.map_err(|e| AppError::VideoError(format!("JPEG compression failed: {}", e)))?;
Ok(EncodedFrame::jpeg(
Bytes::from(jpeg_data),
self.config.resolution,
sequence,
))
}
/// Encode YUYV (YUV422) frame to JPEG
pub fn encode_yuyv(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
let width = self.config.resolution.width as usize;
let height = self.config.resolution.height as usize;
let expected_size = width * height * 2;
if data.len() < expected_size {
return Err(AppError::VideoError(format!(
"YUYV data too small: {} < {}",
data.len(),
expected_size
)));
}
// Convert YUYV to I420 using libyuv (SIMD accelerated)
libyuv::yuy2_to_i420(data, &mut self.i420_buffer, width as i32, height as i32)
.map_err(|e| AppError::VideoError(format!("libyuv YUYV→I420 failed: {}", e)))?;
self.encode_i420_to_jpeg(sequence)
}
/// YVYU → swap chroma to YUYV in scratch, then same as [`Self::encode_yuyv`].
pub fn encode_yvyu(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
let width = self.config.resolution.width as usize;
let height = self.config.resolution.height as usize;
let expected_size = width * height * 2;
if data.len() < expected_size {
return Err(AppError::VideoError(format!(
"YVYU data too small: {} < {}",
data.len(),
expected_size
)));
}
// Reuse bgra_buffer as scratch for the swapped YUYV data.
if self.bgra_buffer.len() < expected_size {
self.bgra_buffer.resize(expected_size, 0);
}
let dst = &mut self.bgra_buffer[..expected_size];
let src = &data[..expected_size];
// Swap bytes [1] and [3] in every 4-byte macropixel: Y0 V0 Y1 U0 → Y0 U0 Y1 V0
for (chunk_dst, chunk_src) in dst.chunks_exact_mut(4).zip(src.chunks_exact(4)) {
chunk_dst[0] = chunk_src[0]; // Y0
chunk_dst[1] = chunk_src[3]; // U0
chunk_dst[2] = chunk_src[2]; // Y1
chunk_dst[3] = chunk_src[1]; // V0
}
libyuv::yuy2_to_i420(dst, &mut self.i420_buffer, width as i32, height as i32)
.map_err(|e| AppError::VideoError(format!("libyuv YVYU→I420 failed: {}", e)))?;
self.encode_i420_to_jpeg(sequence)
}
/// Encode NV12 frame to JPEG
pub fn encode_nv12(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
let width = self.config.resolution.width as usize;
let height = self.config.resolution.height as usize;
let expected_size = width * height * 3 / 2;
if data.len() < expected_size {
return Err(AppError::VideoError(format!(
"NV12 data too small: {} < {}",
data.len(),
expected_size
)));
}
// Convert NV12 to I420 using libyuv (SIMD accelerated)
libyuv::nv12_to_i420(data, &mut self.i420_buffer, width as i32, height as i32)
.map_err(|e| AppError::VideoError(format!("libyuv NV12→I420 failed: {}", e)))?;
self.encode_i420_to_jpeg(sequence)
}
/// Encode NV16 frame to JPEG
pub fn encode_nv16(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
let width = self.config.resolution.width as usize;
let height = self.config.resolution.height as usize;
let y_size = width * height;
let uv_size = y_size;
let expected_size = y_size + uv_size;
if data.len() < expected_size {
return Err(AppError::VideoError(format!(
"NV16 data too small: {} < {}",
data.len(),
expected_size
)));
}
let src_uv = &data[y_size..expected_size];
let chroma_plane_size = y_size / 2;
let (u_plane_422, rest) = self.uv_split_buffer.split_at_mut(chroma_plane_size);
let (v_plane_422, _) = rest.split_at_mut(chroma_plane_size);
libyuv::split_uv_plane(
src_uv,
width as i32,
u_plane_422,
(width / 2) as i32,
v_plane_422,
(width / 2) as i32,
(width / 2) as i32,
height as i32,
)
.map_err(|e| AppError::VideoError(format!("libyuv NV16 split failed: {}", e)))?;
libyuv::i422_to_i420_planar(
&data[..y_size],
width as i32,
u_plane_422,
(width / 2) as i32,
v_plane_422,
(width / 2) as i32,
&mut self.i420_buffer,
width as i32,
height as i32,
)
.map_err(|e| AppError::VideoError(format!("libyuv NV16→I420 failed: {}", e)))?;
self.encode_i420_to_jpeg(sequence)
}
/// Encode NV24 frame to JPEG
pub fn encode_nv24(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
let width = self.config.resolution.width as usize;
let height = self.config.resolution.height as usize;
let y_size = width * height;
let uv_size = y_size * 2;
let expected_size = y_size + uv_size;
if data.len() < expected_size {
return Err(AppError::VideoError(format!(
"NV24 data too small: {} < {}",
data.len(),
expected_size
)));
}
let src_uv = &data[y_size..expected_size];
let chroma_plane_size = y_size;
let (u_plane_444, rest) = self.uv_split_buffer.split_at_mut(chroma_plane_size);
let (v_plane_444, _) = rest.split_at_mut(chroma_plane_size);
libyuv::split_uv_plane(
src_uv,
(width * 2) as i32,
u_plane_444,
width as i32,
v_plane_444,
width as i32,
width as i32,
height as i32,
)
.map_err(|e| AppError::VideoError(format!("libyuv NV24 split failed: {}", e)))?;
libyuv::h444_to_bgra(
&data[..y_size],
u_plane_444,
v_plane_444,
&mut self.bgra_buffer,
width as i32,
height as i32,
)
.map_err(|e| AppError::VideoError(format!("libyuv NV24(H444)→BGRA failed: {}", e)))?;
self.encode_bgra_to_jpeg(sequence)
}
/// Encode RGB24 frame to JPEG
pub fn encode_rgb(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
let width = self.config.resolution.width as usize;
let height = self.config.resolution.height as usize;
let expected_size = width * height * 3;
if data.len() < expected_size {
return Err(AppError::VideoError(format!(
"RGB data too small: {} < {}",
data.len(),
expected_size
)));
}
// Convert RGB24 to I420 using libyuv (SIMD accelerated)
libyuv::rgb24_to_i420(data, &mut self.i420_buffer, width as i32, height as i32)
.map_err(|e| AppError::VideoError(format!("libyuv RGB24→I420 failed: {}", e)))?;
self.encode_i420_to_jpeg(sequence)
}
/// Encode BGR24 frame to JPEG
pub fn encode_bgr(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
let width = self.config.resolution.width as usize;
let height = self.config.resolution.height as usize;
let expected_size = width * height * 3;
if data.len() < expected_size {
return Err(AppError::VideoError(format!(
"BGR data too small: {} < {}",
data.len(),
expected_size
)));
}
// Convert BGR24 to I420 using libyuv (SIMD accelerated)
// Note: libyuv's RAWToI420 is BGR24 → I420
libyuv::bgr24_to_i420(data, &mut self.i420_buffer, width as i32, height as i32)
.map_err(|e| AppError::VideoError(format!("libyuv BGR24→I420 failed: {}", e)))?;
self.encode_i420_to_jpeg(sequence)
}
}
impl crate::video::codec::traits::Encoder for JpegEncoder {
fn name(&self) -> &str {
"JPEG (libyuv+turbojpeg)"
}
fn output_format(&self) -> EncodedFormat {
EncodedFormat::Jpeg
}
fn encode(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
match self.config.input_format {
PixelFormat::Yuyv => self.encode_yuyv(data, sequence),
PixelFormat::Yvyu => self.encode_yvyu(data, sequence),
PixelFormat::Nv12 => self.encode_nv12(data, sequence),
PixelFormat::Nv16 => self.encode_nv16(data, sequence),
PixelFormat::Nv24 => self.encode_nv24(data, sequence),
PixelFormat::Rgb24 => self.encode_rgb(data, sequence),
PixelFormat::Bgr24 => self.encode_bgr(data, sequence),
_ => Err(AppError::VideoError(format!(
"Unsupported input format for JPEG: {}",
self.config.input_format
))),
}
}
fn config(&self) -> &EncoderConfig {
&self.config
}
fn supports_format(&self, format: PixelFormat) -> bool {
matches!(
format,
PixelFormat::Yuyv
| PixelFormat::Yvyu
| PixelFormat::Nv12
| PixelFormat::Nv16
| PixelFormat::Nv24
| PixelFormat::Rgb24
| PixelFormat::Bgr24
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_i420_buffer_size() {
// 1920x1080 I420 = 1920*1080 + 960*540 + 960*540 = 3110400 bytes
let config = EncoderConfig::jpeg(Resolution::HD1080, 80);
let encoder = JpegEncoder::new(config).unwrap();
assert_eq!(encoder.i420_buffer.len(), 1920 * 1080 * 3 / 2);
}
}

View File

@@ -0,0 +1,98 @@
//! MJPEG decoder using RKMPP via hwcodec (FFmpeg RAM).
use hwcodec::ffmpeg::AVPixelFormat;
use hwcodec::ffmpeg_ram::decode::{DecodeContext, Decoder};
use tracing::{info, warn};
use crate::error::{AppError, Result};
use crate::video::codec::convert::Nv12Converter;
use crate::video::format::Resolution;
pub struct MjpegRkmppDecoder {
decoder: Decoder,
resolution: Resolution,
nv16_to_nv12: Option<Nv12Converter>,
last_pixfmt: Option<AVPixelFormat>,
}
impl MjpegRkmppDecoder {
pub fn new(resolution: Resolution) -> Result<Self> {
let ctx = DecodeContext {
name: "mjpeg_rkmpp".to_string(),
width: resolution.width as i32,
height: resolution.height as i32,
sw_pixfmt: AVPixelFormat::AV_PIX_FMT_NV12,
thread_count: 1,
};
let decoder = Decoder::new(ctx).map_err(|_| {
AppError::VideoError("Failed to create mjpeg_rkmpp decoder".to_string())
})?;
Ok(Self {
decoder,
resolution,
nv16_to_nv12: None,
last_pixfmt: None,
})
}
pub fn decode_to_nv12(&mut self, mjpeg: &[u8]) -> Result<Vec<u8>> {
let frames = self
.decoder
.decode(mjpeg)
.map_err(|e| AppError::VideoError(format!("mjpeg_rkmpp decode failed: {}", e)))?;
if frames.is_empty() {
return Err(AppError::VideoError(
"mjpeg_rkmpp decode returned no frames".to_string(),
));
}
if frames.len() > 1 {
warn!(
"mjpeg_rkmpp decode returned {} frames, using last",
frames.len()
);
}
let frame = frames
.pop()
.ok_or_else(|| AppError::VideoError("mjpeg_rkmpp decode returned empty".to_string()))?;
if frame.width as u32 != self.resolution.width
|| frame.height as u32 != self.resolution.height
{
warn!(
"mjpeg_rkmpp output size {}x{} differs from expected {}x{}",
frame.width, frame.height, self.resolution.width, self.resolution.height
);
}
if let Some(last) = self.last_pixfmt {
if frame.pixfmt != last {
warn!(
"mjpeg_rkmpp output pixfmt changed from {:?} to {:?}",
last, frame.pixfmt
);
}
} else {
if frame.pixfmt == AVPixelFormat::AV_PIX_FMT_NV16 {
info!("mjpeg_rkmpp output pixfmt NV16 on first frame; converting to NV12");
}
self.last_pixfmt = Some(frame.pixfmt);
}
let pixfmt = self.last_pixfmt.unwrap_or(frame.pixfmt);
match pixfmt {
AVPixelFormat::AV_PIX_FMT_NV12 => Ok(frame.data),
AVPixelFormat::AV_PIX_FMT_NV16 => {
if self.nv16_to_nv12.is_none() {
self.nv16_to_nv12 = Some(Nv12Converter::nv16_to_nv12(self.resolution));
}
let conv = self.nv16_to_nv12.as_mut().unwrap();
let nv12 = conv.convert(&frame.data)?;
Ok(nv12.to_vec())
}
other => Err(AppError::VideoError(format!(
"mjpeg_rkmpp output pixfmt {:?} (expected NV12/NV16)",
other
))),
}
}
}

View File

@@ -0,0 +1,54 @@
//! MJPEG decoder using TurboJPEG (software) -> RGB24.
use turbojpeg::{Decompressor, Image, PixelFormat as TJPixelFormat};
use crate::error::{AppError, Result};
use crate::video::format::Resolution;
pub struct MjpegTurboDecoder {
decompressor: Decompressor,
resolution: Resolution,
}
impl MjpegTurboDecoder {
pub fn new(resolution: Resolution) -> Result<Self> {
let decompressor = Decompressor::new().map_err(|e| {
AppError::VideoError(format!("Failed to create turbojpeg decoder: {}", e))
})?;
Ok(Self {
decompressor,
resolution,
})
}
pub fn decode_to_rgb(&mut self, mjpeg: &[u8]) -> Result<Vec<u8>> {
let header = self
.decompressor
.read_header(mjpeg)
.map_err(|e| AppError::VideoError(format!("turbojpeg read_header failed: {}", e)))?;
if header.width as u32 != self.resolution.width
|| header.height as u32 != self.resolution.height
{
return Err(AppError::VideoError(format!(
"turbojpeg size mismatch: {}x{} (expected {}x{})",
header.width, header.height, self.resolution.width, self.resolution.height
)));
}
let pitch = header.width * 3;
let mut image = Image {
pixels: vec![0u8; header.height * pitch],
width: header.width,
pitch,
height: header.height,
format: TJPixelFormat::RGB,
};
self.decompressor
.decompress(mjpeg, image.as_deref_mut())
.map_err(|e| AppError::VideoError(format!("turbojpeg decode failed: {}", e)))?;
Ok(image.pixels)
}
}

72
src/video/codec/mod.rs Normal file
View File

@@ -0,0 +1,72 @@
//! Video codec, conversion, encoding, and decoding implementations.
use hwcodec::common::DataFormat;
use hwcodec::ffmpeg_ram::CodecInfo;
pub mod convert;
pub mod h264;
pub mod h264_bitstream;
pub mod h265;
pub mod jpeg;
pub mod registry;
pub mod self_check;
pub mod traits;
pub mod video_codec;
pub mod vp8;
pub mod vp9;
pub mod mjpeg_turbo;
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
pub mod mjpeg_rkmpp;
pub use convert::{PixelConverter, Yuv420pBuffer};
pub use h264::{H264Config, H264Encoder, H264EncoderType, H264InputFormat};
pub use h265::{H265Config, H265Encoder, H265EncoderType, H265InputFormat};
pub use jpeg::JpegEncoder;
pub use mjpeg_turbo::MjpegTurboDecoder;
pub use registry::{AvailableEncoder, EncoderBackend, EncoderRegistry, VideoEncoderType};
pub use self_check::{
build_hardware_self_check_runtime_error, run_hardware_self_check, VideoEncoderSelfCheckCell,
VideoEncoderSelfCheckCodec, VideoEncoderSelfCheckResponse, VideoEncoderSelfCheckRow,
};
pub use traits::{
BitratePreset, EncodedFormat, EncodedFrame, Encoder, EncoderConfig, EncoderFactory,
};
pub use video_codec::{
CodecFrame, VideoCodec, VideoCodecConfig, VideoCodecFactory, VideoCodecType,
};
pub use vp8::{VP8Config, VP8Encoder, VP8EncoderType, VP8InputFormat};
pub use vp9::{VP9Config, VP9Encoder, VP9EncoderType, VP9InputFormat};
pub(crate) fn select_codec_for_format<F>(
encoders: &[CodecInfo],
format: DataFormat,
preferred: F,
) -> Option<&CodecInfo>
where
F: Fn(&CodecInfo) -> bool,
{
encoders
.iter()
.find(|codec| codec.format == format && preferred(codec))
.or_else(|| encoders.iter().find(|codec| codec.format == format))
}
pub(crate) fn detect_best_codec_for_format<T, F>(
encoders: &[CodecInfo],
format: DataFormat,
preferred: F,
) -> Option<(T, String)>
where
T: From<EncoderBackend>,
F: Fn(&CodecInfo) -> bool,
{
select_codec_for_format(encoders, format, preferred).map(|codec| {
(
T::from(EncoderBackend::from_codec_name(&codec.name)),
codec.name.clone(),
)
})
}

566
src/video/codec/registry.rs Normal file
View File

@@ -0,0 +1,566 @@
//! Encoder registry - Detection and management of available video encoders
//!
//! This module provides:
//! - Automatic detection of available hardware/software encoders
//! - Encoder selection based on format and priority
//! - Global registry for encoder availability queries
use std::collections::HashMap;
use std::sync::OnceLock;
use std::time::Duration;
use tracing::{debug, info, warn};
use hwcodec::common::{DataFormat, Quality, RateControl};
use hwcodec::ffmpeg::{resolve_pixel_format, AVPixelFormat};
use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder};
use hwcodec::ffmpeg_ram::CodecInfo;
/// Video encoder format type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VideoEncoderType {
/// H.264/AVC
H264,
/// H.265/HEVC
H265,
/// VP8
VP8,
/// VP9
VP9,
}
impl VideoEncoderType {
pub const fn ordered() -> [Self; 4] {
[Self::H264, Self::H265, Self::VP8, Self::VP9]
}
/// Convert to hwcodec DataFormat
pub fn to_data_format(&self) -> DataFormat {
match self {
VideoEncoderType::H264 => DataFormat::H264,
VideoEncoderType::H265 => DataFormat::H265,
VideoEncoderType::VP8 => DataFormat::VP8,
VideoEncoderType::VP9 => DataFormat::VP9,
}
}
/// Create from hwcodec DataFormat
pub fn from_data_format(format: DataFormat) -> Option<Self> {
match format {
DataFormat::H264 => Some(VideoEncoderType::H264),
DataFormat::H265 => Some(VideoEncoderType::H265),
DataFormat::VP8 => Some(VideoEncoderType::VP8),
DataFormat::VP9 => Some(VideoEncoderType::VP9),
_ => None,
}
}
/// Get codec name prefix for FFmpeg
pub fn codec_prefix(&self) -> &'static str {
match self {
VideoEncoderType::H264 => "h264",
VideoEncoderType::H265 => "hevc",
VideoEncoderType::VP8 => "vp8",
VideoEncoderType::VP9 => "vp9",
}
}
/// Get display name
pub fn display_name(&self) -> &'static str {
match self {
VideoEncoderType::H264 => "H.264",
VideoEncoderType::H265 => "H.265/HEVC",
VideoEncoderType::VP8 => "VP8",
VideoEncoderType::VP9 => "VP9",
}
}
}
impl std::fmt::Display for VideoEncoderType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.display_name())
}
}
/// Encoder backend type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EncoderBackend {
/// Intel/AMD/NVIDIA VAAPI (Linux)
Vaapi,
/// NVIDIA NVENC
Nvenc,
/// Intel Quick Sync Video
Qsv,
/// AMD AMF
Amf,
/// Rockchip MPP
Rkmpp,
/// V4L2 Memory-to-Memory (ARM)
V4l2m2m,
/// Software encoding (libx264, libx265, libvpx)
Software,
}
impl EncoderBackend {
/// Detect backend from codec name
pub fn from_codec_name(name: &str) -> Self {
if name.contains("vaapi") {
EncoderBackend::Vaapi
} else if name.contains("nvenc") {
EncoderBackend::Nvenc
} else if name.contains("qsv") {
EncoderBackend::Qsv
} else if name.contains("amf") {
EncoderBackend::Amf
} else if name.contains("rkmpp") {
EncoderBackend::Rkmpp
} else if name.contains("v4l2m2m") {
EncoderBackend::V4l2m2m
} else {
EncoderBackend::Software
}
}
/// Check if this is a hardware backend
pub fn is_hardware(&self) -> bool {
!matches!(self, EncoderBackend::Software)
}
/// Get display name
pub fn display_name(&self) -> &'static str {
match self {
EncoderBackend::Vaapi => "VAAPI",
EncoderBackend::Nvenc => "NVENC",
EncoderBackend::Qsv => "QSV",
EncoderBackend::Amf => "AMF",
EncoderBackend::Rkmpp => "RKMPP",
EncoderBackend::V4l2m2m => "V4L2 M2M",
EncoderBackend::Software => "Software",
}
}
/// Parse from string (case-insensitive)
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"vaapi" => Some(EncoderBackend::Vaapi),
"nvenc" => Some(EncoderBackend::Nvenc),
"qsv" => Some(EncoderBackend::Qsv),
"amf" => Some(EncoderBackend::Amf),
"rkmpp" => Some(EncoderBackend::Rkmpp),
"v4l2m2m" | "v4l2" => Some(EncoderBackend::V4l2m2m),
"software" | "cpu" => Some(EncoderBackend::Software),
_ => None,
}
}
}
impl std::fmt::Display for EncoderBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.display_name())
}
}
/// Information about an available encoder
#[derive(Debug, Clone)]
pub struct AvailableEncoder {
/// Encoder format type
pub format: VideoEncoderType,
/// FFmpeg codec name (e.g., "h264_vaapi", "hevc_nvenc")
pub codec_name: String,
/// Backend type
pub backend: EncoderBackend,
/// Priority (lower is better)
pub priority: i32,
/// Whether this is a hardware encoder
pub is_hardware: bool,
}
impl AvailableEncoder {
/// Create from hwcodec CodecInfo
pub fn from_codec_info(info: &CodecInfo) -> Option<Self> {
let format = VideoEncoderType::from_data_format(info.format)?;
let backend = EncoderBackend::from_codec_name(&info.name);
let is_hardware = backend.is_hardware();
Some(Self {
format,
codec_name: info.name.clone(),
backend,
priority: info.priority,
is_hardware,
})
}
}
/// Global encoder registry
///
/// Detects and caches available encoders at startup.
/// Use `EncoderRegistry::global()` to access the singleton instance.
pub struct EncoderRegistry {
/// Available encoders grouped by format
encoders: HashMap<VideoEncoderType, Vec<AvailableEncoder>>,
/// Detection resolution (used for testing)
detection_resolution: (u32, u32),
}
impl EncoderRegistry {
fn detect_encoders_with_timeout(ctx: EncodeContext, timeout: Duration) -> Vec<CodecInfo> {
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
let handle = std::thread::Builder::new()
.name("ffmpeg-encoder-detect".to_string())
.spawn(move || {
let result = HwEncoder::available_encoders(ctx, None);
let _ = tx.send(result);
});
let Ok(handle) = handle else {
warn!("Failed to spawn encoder detection thread");
return Vec::new();
};
match rx.recv_timeout(timeout) {
Ok(encoders) => {
let _ = handle.join();
encoders
}
Err(mpsc::RecvTimeoutError::Timeout) => {
warn!(
"Encoder detection timed out after {}ms, skipping hardware detection",
timeout.as_millis()
);
std::thread::spawn(move || {
let _ = handle.join();
});
Vec::new()
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
let _ = handle.join();
warn!("Encoder detection thread exited unexpectedly");
Vec::new()
}
}
}
fn register_software_fallbacks(&mut self) {
info!("Registering software encoders...");
for format in VideoEncoderType::ordered() {
let encoders = self.encoders.entry(format).or_default();
if encoders.iter().any(|encoder| !encoder.is_hardware) {
continue;
}
let codec_name = match format {
VideoEncoderType::H264 => "libx264",
VideoEncoderType::H265 => "libx265",
VideoEncoderType::VP8 => "libvpx",
VideoEncoderType::VP9 => "libvpx-vp9",
};
encoders.push(AvailableEncoder {
format,
codec_name: codec_name.to_string(),
backend: EncoderBackend::Software,
priority: 100,
is_hardware: false,
});
debug!(
"Registered software encoder: {} for {} (priority: {})",
codec_name, format, 100
);
}
}
/// Get the global registry instance
///
/// The registry is initialized lazily on first access with 1280x720 detection.
pub fn global() -> &'static Self {
static INSTANCE: OnceLock<EncoderRegistry> = OnceLock::new();
INSTANCE.get_or_init(|| {
let mut registry = EncoderRegistry::new();
registry.detect_encoders(1280, 720);
registry
})
}
/// Create a new empty registry
pub fn new() -> Self {
Self {
encoders: HashMap::new(),
detection_resolution: (0, 0),
}
}
/// Detect all available encoders
///
/// This queries hwcodec/FFmpeg for available encoders and populates the registry.
pub fn detect_encoders(&mut self, width: u32, height: u32) {
info!("Detecting available video encoders at {}x{}", width, height);
self.encoders.clear();
self.detection_resolution = (width, height);
// Create test context for encoder detection
let ctx = EncodeContext {
name: String::new(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
align: 1,
fps: 30,
gop: 30,
rc: RateControl::RC_CBR,
quality: Quality::Quality_Default,
kbs: 2000,
q: 23,
thread_count: 1,
};
const DETECT_TIMEOUT_MS: u64 = 5000;
info!("Encoder detection timeout: {}ms", DETECT_TIMEOUT_MS);
let all_encoders = Self::detect_encoders_with_timeout(
ctx.clone(),
Duration::from_millis(DETECT_TIMEOUT_MS),
);
info!("Found {} encoders from hwcodec", all_encoders.len());
for codec_info in &all_encoders {
if let Some(encoder) = AvailableEncoder::from_codec_info(codec_info) {
debug!(
"Detected encoder: {} ({}) - {} priority={}",
encoder.codec_name, encoder.format, encoder.backend, encoder.priority
);
self.encoders
.entry(encoder.format)
.or_default()
.push(encoder);
}
}
// Sort encoders by priority (lower is better)
for encoders in self.encoders.values_mut() {
encoders.sort_by_key(|e| e.priority);
}
self.register_software_fallbacks();
// Log summary
for (format, encoders) in &self.encoders {
let hw_count = encoders.iter().filter(|e| e.is_hardware).count();
let sw_count = encoders.len() - hw_count;
info!(
"{}: {} encoders ({} hardware, {} software)",
format,
encoders.len(),
hw_count,
sw_count
);
}
}
/// Get the best encoder for a format
///
/// # Arguments
/// * `format` - The video format to encode
/// * `hardware_only` - If true, only return hardware encoders
///
/// # Returns
/// The best available encoder, or None if no suitable encoder is found
pub fn best_encoder(
&self,
format: VideoEncoderType,
hardware_only: bool,
) -> Option<&AvailableEncoder> {
self.encoders.get(&format)?.iter().find(
|e| {
if hardware_only {
e.is_hardware
} else {
true
}
},
)
}
pub fn best_available_encoder(&self, format: VideoEncoderType) -> Option<&AvailableEncoder> {
self.best_encoder(format, false)
}
/// Get all encoders for a format
pub fn encoders_for_format(&self, format: VideoEncoderType) -> &[AvailableEncoder] {
self.encoders
.get(&format)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
/// Get all available formats
///
/// # Arguments
/// * `hardware_only` - If true, only return formats with hardware encoders
pub fn available_formats(&self, hardware_only: bool) -> Vec<VideoEncoderType> {
self.encoders
.iter()
.filter(|(_, encoders)| {
if hardware_only {
encoders.iter().any(|e| e.is_hardware)
} else {
!encoders.is_empty()
}
})
.map(|(format, _)| *format)
.collect()
}
/// Check if a format is available
///
/// # Arguments
/// * `format` - The video format to check
/// * `hardware_only` - If true, only check for hardware encoders
pub fn is_format_available(&self, format: VideoEncoderType, hardware_only: bool) -> bool {
self.best_encoder(format, hardware_only).is_some()
}
pub fn is_codec_available(&self, format: VideoEncoderType) -> bool {
self.best_available_encoder(format).is_some()
}
/// Get available formats for user selection
///
pub fn selectable_formats(&self) -> Vec<VideoEncoderType> {
VideoEncoderType::ordered()
.into_iter()
.filter(|format| self.is_codec_available(*format))
.collect()
}
/// Get detection resolution
pub fn detection_resolution(&self) -> (u32, u32) {
self.detection_resolution
}
/// Get all available backend types
pub fn available_backends(&self) -> Vec<EncoderBackend> {
use std::collections::HashSet;
let mut backends = HashSet::new();
for encoders in self.encoders.values() {
for encoder in encoders {
backends.insert(encoder.backend);
}
}
let mut result: Vec<_> = backends.into_iter().collect();
// Sort: hardware backends first, software last
result.sort_by_key(|b| if b.is_hardware() { 0 } else { 1 });
result
}
/// Get formats supported by a specific backend
pub fn formats_for_backend(&self, backend: EncoderBackend) -> Vec<VideoEncoderType> {
let mut formats = Vec::new();
for (format, encoders) in &self.encoders {
if encoders.iter().any(|e| e.backend == backend) {
formats.push(*format);
}
}
formats
}
/// Get encoder for a format with specific backend
pub fn encoder_with_backend(
&self,
format: VideoEncoderType,
backend: EncoderBackend,
) -> Option<&AvailableEncoder> {
self.encoders
.get(&format)?
.iter()
.find(|e| e.backend == backend)
}
/// Get encoders grouped by backend for a format
pub fn encoders_by_backend(
&self,
format: VideoEncoderType,
) -> HashMap<EncoderBackend, Vec<&AvailableEncoder>> {
let mut grouped = HashMap::new();
if let Some(encoders) = self.encoders.get(&format) {
for encoder in encoders {
grouped
.entry(encoder.backend)
.or_insert_with(Vec::new)
.push(encoder);
}
}
grouped
}
}
impl Default for EncoderRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_video_encoder_type_display() {
assert_eq!(VideoEncoderType::H264.display_name(), "H.264");
assert_eq!(VideoEncoderType::H265.display_name(), "H.265/HEVC");
assert_eq!(VideoEncoderType::VP8.display_name(), "VP8");
assert_eq!(VideoEncoderType::VP9.display_name(), "VP9");
}
#[test]
fn test_encoder_backend_detection() {
assert_eq!(
EncoderBackend::from_codec_name("h264_vaapi"),
EncoderBackend::Vaapi
);
assert_eq!(
EncoderBackend::from_codec_name("hevc_nvenc"),
EncoderBackend::Nvenc
);
assert_eq!(
EncoderBackend::from_codec_name("h264_qsv"),
EncoderBackend::Qsv
);
assert_eq!(
EncoderBackend::from_codec_name("libx264"),
EncoderBackend::Software
);
}
#[test]
fn test_codec_ordering() {
assert_eq!(
VideoEncoderType::ordered(),
[
VideoEncoderType::H264,
VideoEncoderType::H265,
VideoEncoderType::VP8,
VideoEncoderType::VP9,
]
);
}
#[test]
fn test_registry_detection() {
let mut registry = EncoderRegistry::new();
registry.detect_encoders(1280, 720);
// Should have detected at least H264 (software fallback available)
println!("Available formats: {:?}", registry.available_formats(false));
println!("Selectable formats: {:?}", registry.selectable_formats());
}
}

View File

@@ -0,0 +1,335 @@
use serde::Serialize;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use super::{
EncoderRegistry, H264Config, H264Encoder, H265Config, H265Encoder, VP8Config, VP8Encoder,
VP9Config, VP9Encoder, VideoEncoderType,
};
use crate::error::{AppError, Result};
use crate::video::format::{PixelFormat, Resolution};
const SELF_CHECK_TIMEOUT: Duration = Duration::from_secs(5);
const SELF_CHECK_FRAME_ATTEMPTS: u64 = 3;
#[derive(Serialize)]
pub struct VideoEncoderSelfCheckCodec {
pub id: &'static str,
pub name: &'static str,
}
#[derive(Serialize)]
pub struct VideoEncoderSelfCheckCell {
pub codec_id: &'static str,
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub elapsed_ms: Option<u64>,
}
#[derive(Serialize)]
pub struct VideoEncoderSelfCheckRow {
pub resolution_id: &'static str,
pub resolution_label: &'static str,
pub width: u32,
pub height: u32,
pub cells: Vec<VideoEncoderSelfCheckCell>,
}
#[derive(Serialize)]
pub struct VideoEncoderSelfCheckResponse {
pub current_hardware_encoder: String,
pub codecs: Vec<VideoEncoderSelfCheckCodec>,
pub rows: Vec<VideoEncoderSelfCheckRow>,
}
pub fn run_hardware_self_check() -> VideoEncoderSelfCheckResponse {
let registry = EncoderRegistry::global();
let codecs = codec_columns();
let mut rows = Vec::new();
for (resolution_id, resolution_label, resolution) in test_resolutions() {
let mut cells = Vec::new();
for codec in test_codecs() {
let cell = match registry.best_encoder(codec, true) {
Some(encoder) => run_single_check(codec, resolution, encoder.codec_name.clone()),
None => unsupported_cell(codec),
};
cells.push(cell);
}
rows.push(VideoEncoderSelfCheckRow {
resolution_id,
resolution_label,
width: resolution.width,
height: resolution.height,
cells,
});
}
VideoEncoderSelfCheckResponse {
current_hardware_encoder: current_hardware_encoder(registry),
codecs,
rows,
}
}
pub fn build_hardware_self_check_runtime_error() -> VideoEncoderSelfCheckResponse {
let codecs = codec_columns();
let mut rows = Vec::new();
for (resolution_id, resolution_label, resolution) in test_resolutions() {
let cells = test_codecs()
.into_iter()
.map(|codec| VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: false,
elapsed_ms: None,
})
.collect();
rows.push(VideoEncoderSelfCheckRow {
resolution_id,
resolution_label,
width: resolution.width,
height: resolution.height,
cells,
});
}
VideoEncoderSelfCheckResponse {
current_hardware_encoder: "None".to_string(),
codecs,
rows,
}
}
fn codec_columns() -> Vec<VideoEncoderSelfCheckCodec> {
test_codecs()
.into_iter()
.map(|codec| VideoEncoderSelfCheckCodec {
id: codec_id(codec),
name: match codec {
VideoEncoderType::H265 => "H.265",
_ => codec.display_name(),
},
})
.collect()
}
fn test_codecs() -> [VideoEncoderType; 4] {
[
VideoEncoderType::H264,
VideoEncoderType::H265,
VideoEncoderType::VP8,
VideoEncoderType::VP9,
]
}
fn test_resolutions() -> [(&'static str, &'static str, Resolution); 4] {
[
("720p", "720p", Resolution::HD720),
("1080p", "1080p", Resolution::HD1080),
("2k", "2K", Resolution::new(2560, 1440)),
("4k", "4K", Resolution::UHD4K),
]
}
fn codec_id(codec: VideoEncoderType) -> &'static str {
match codec {
VideoEncoderType::H264 => "h264",
VideoEncoderType::H265 => "h265",
VideoEncoderType::VP8 => "vp8",
VideoEncoderType::VP9 => "vp9",
}
}
fn unsupported_cell(codec: VideoEncoderType) -> VideoEncoderSelfCheckCell {
VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: false,
elapsed_ms: None,
}
}
fn run_single_check(
codec: VideoEncoderType,
resolution: Resolution,
codec_name_ffmpeg: String,
) -> VideoEncoderSelfCheckCell {
let started = Instant::now();
let (tx, rx) = mpsc::channel();
let thread_codec_name = codec_name_ffmpeg.clone();
let spawn_result = std::thread::Builder::new()
.name(format!(
"encoder-self-check-{}-{}x{}",
codec_id(codec),
resolution.width,
resolution.height
))
.spawn(move || {
let _ = tx.send(run_smoke_test(codec, resolution, &thread_codec_name));
});
if let Err(e) = spawn_result {
let _ = e;
return VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: false,
elapsed_ms: Some(started.elapsed().as_millis() as u64),
};
}
match rx.recv_timeout(SELF_CHECK_TIMEOUT) {
Ok(Ok(())) => VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: true,
elapsed_ms: Some(started.elapsed().as_millis() as u64),
},
Ok(Err(_)) => VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: false,
elapsed_ms: Some(started.elapsed().as_millis() as u64),
},
Err(mpsc::RecvTimeoutError::Timeout) => VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: false,
elapsed_ms: Some(started.elapsed().as_millis() as u64),
},
Err(mpsc::RecvTimeoutError::Disconnected) => VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: false,
elapsed_ms: Some(started.elapsed().as_millis() as u64),
},
}
}
fn current_hardware_encoder(registry: &EncoderRegistry) -> String {
let backends = registry
.available_backends()
.into_iter()
.filter(|backend| backend.is_hardware())
.map(|backend| backend.display_name().to_string())
.collect::<Vec<_>>();
if backends.is_empty() {
"None".to_string()
} else {
backends.join("/")
}
}
fn run_smoke_test(
codec: VideoEncoderType,
resolution: Resolution,
codec_name_ffmpeg: &str,
) -> Result<()> {
match codec {
VideoEncoderType::H264 => run_h264_smoke_test(resolution, codec_name_ffmpeg),
VideoEncoderType::H265 => run_h265_smoke_test(resolution, codec_name_ffmpeg),
VideoEncoderType::VP8 => run_vp8_smoke_test(resolution, codec_name_ffmpeg),
VideoEncoderType::VP9 => run_vp9_smoke_test(resolution, codec_name_ffmpeg),
}
}
fn run_h264_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> {
let mut encoder = H264Encoder::with_codec(
H264Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)),
codec_name_ffmpeg,
)?;
encoder.request_keyframe();
let frame = build_nv12_test_frame(resolution, encoder.yuv_info().2 as usize);
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
if frames.iter().any(|frame| !frame.data.is_empty()) {
return Ok(());
}
}
Err(AppError::VideoError(
"Encoder produced no output after multiple frames".to_string(),
))
}
fn run_h265_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> {
let mut encoder = H265Encoder::with_codec(
H265Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)),
codec_name_ffmpeg,
)?;
encoder.request_keyframe();
let frame = build_nv12_test_frame(resolution, encoder.buffer_info().2 as usize);
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
if frames.iter().any(|frame| !frame.data.is_empty()) {
return Ok(());
}
}
Err(AppError::VideoError(
"Encoder produced no output after multiple frames".to_string(),
))
}
fn run_vp8_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> {
let mut encoder = VP8Encoder::with_codec(
VP8Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)),
codec_name_ffmpeg,
)?;
let frame = build_nv12_test_frame(resolution, encoder.buffer_info().2 as usize);
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
if frames.iter().any(|frame| !frame.data.is_empty()) {
return Ok(());
}
}
Err(AppError::VideoError(
"Encoder produced no output after multiple frames".to_string(),
))
}
fn run_vp9_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> {
let mut encoder = VP9Encoder::with_codec(
VP9Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)),
codec_name_ffmpeg,
)?;
let frame = build_nv12_test_frame(resolution, encoder.buffer_info().2 as usize);
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
if frames.iter().any(|frame| !frame.data.is_empty()) {
return Ok(());
}
}
Err(AppError::VideoError(
"Encoder produced no output after multiple frames".to_string(),
))
}
fn build_nv12_test_frame(resolution: Resolution, buffer_length: usize) -> Vec<u8> {
let minimum_length = PixelFormat::Nv12.frame_size(resolution).unwrap_or(0);
let mut frame = vec![0x80; buffer_length.max(minimum_length)];
let y_plane_len = (resolution.width * resolution.height) as usize;
let fill_len = y_plane_len.min(frame.len());
frame[..fill_len].fill(0x10);
frame
}
fn bitrate_kbps_for_resolution(resolution: Resolution) -> u32 {
match resolution.width {
0..=1280 => 4_000,
1281..=1920 => 8_000,
1921..=2560 => 12_000,
_ => 20_000,
}
}
fn pts_ms(sequence: u64) -> i64 {
((sequence * 1000) / 30) as i64
}

191
src/video/codec/traits.rs Normal file
View File

@@ -0,0 +1,191 @@
//! Encoder traits and common types
use bytes::Bytes;
use std::time::Instant;
use crate::error::Result;
use crate::video::format::{PixelFormat, Resolution};
/// Defined in `config::schema` (typeshare + serde). Re-export for encoder users.
pub use crate::config::BitratePreset;
/// Encoder configuration
#[derive(Debug, Clone)]
pub struct EncoderConfig {
/// Target resolution
pub resolution: Resolution,
/// Input pixel format
pub input_format: PixelFormat,
/// Output quality (1-100 for JPEG, bitrate kbps for H264)
pub quality: u32,
/// Target frame rate
pub fps: u32,
/// Keyframe interval (for H264)
pub gop_size: u32,
}
impl Default for EncoderConfig {
fn default() -> Self {
Self {
resolution: Resolution::HD1080,
input_format: PixelFormat::Yuyv,
quality: 80,
fps: 30,
gop_size: 30,
}
}
}
impl EncoderConfig {
pub fn jpeg(resolution: Resolution, quality: u32) -> Self {
Self {
resolution,
input_format: PixelFormat::Yuyv,
quality,
fps: 30,
gop_size: 1,
}
}
pub fn h264(resolution: Resolution, bitrate_kbps: u32) -> Self {
Self {
resolution,
input_format: PixelFormat::Yuyv,
quality: bitrate_kbps,
fps: 30,
gop_size: 30,
}
}
}
/// Encoded frame output
#[derive(Debug, Clone)]
pub struct EncodedFrame {
/// Encoded data
pub data: Bytes,
/// Output format (JPEG, H264, etc.)
pub format: EncodedFormat,
/// Resolution
pub resolution: Resolution,
/// Whether this is a key frame
pub key_frame: bool,
/// Frame sequence number
pub sequence: u64,
/// Encoding timestamp
pub timestamp: Instant,
/// Presentation timestamp (for video sync)
pub pts: u64,
/// Decode timestamp (for B-frames)
pub dts: u64,
}
impl EncodedFrame {
pub fn jpeg(data: Bytes, resolution: Resolution, sequence: u64) -> Self {
Self {
data,
format: EncodedFormat::Jpeg,
resolution,
key_frame: true,
sequence,
timestamp: Instant::now(),
pts: sequence,
dts: sequence,
}
}
pub fn h264(
data: Bytes,
resolution: Resolution,
key_frame: bool,
sequence: u64,
pts: u64,
dts: u64,
) -> Self {
Self {
data,
format: EncodedFormat::H264,
resolution,
key_frame,
sequence,
timestamp: Instant::now(),
pts,
dts,
}
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
/// Encoded output format
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncodedFormat {
Jpeg,
H264,
H265,
Vp8,
Vp9,
Av1,
}
impl std::fmt::Display for EncodedFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EncodedFormat::Jpeg => write!(f, "JPEG"),
EncodedFormat::H264 => write!(f, "H.264"),
EncodedFormat::H265 => write!(f, "H.265"),
EncodedFormat::Vp8 => write!(f, "VP8"),
EncodedFormat::Vp9 => write!(f, "VP9"),
EncodedFormat::Av1 => write!(f, "AV1"),
}
}
}
/// Generic encoder trait
/// Note: Not Sync because some encoders (like turbojpeg) are not thread-safe
pub trait Encoder: Send {
/// Get encoder name
fn name(&self) -> &str;
/// Get output format
fn output_format(&self) -> EncodedFormat;
/// Encode a raw frame
fn encode(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame>;
/// Flush any pending frames
fn flush(&mut self) -> Result<Vec<EncodedFrame>> {
Ok(vec![])
}
/// Reset encoder state
fn reset(&mut self) -> Result<()> {
Ok(())
}
/// Get current configuration
fn config(&self) -> &EncoderConfig;
/// Check if encoder supports the given input format
fn supports_format(&self, format: PixelFormat) -> bool;
}
/// Encoder factory for creating encoders
pub trait EncoderFactory: Send + Sync {
/// Create an encoder with the given configuration
fn create(&self, config: EncoderConfig) -> Result<Box<dyn Encoder>>;
/// Get encoder type name
fn encoder_type(&self) -> &str;
/// Check if this encoder is available on the system
fn is_available(&self) -> bool;
/// Get encoder priority (higher = preferred)
fn priority(&self) -> u32;
}

View File

@@ -0,0 +1,370 @@
//! WebRTC Video Codec abstraction layer
//!
//! This module provides a unified interface for video codecs used in WebRTC streaming.
//! It supports multiple codec types (H264, VP8, VP9, H265) with a common API.
//!
//! # Architecture
//!
//! ```text
//! VideoCodec (trait)
//! |
//! +-- H264Codec (current implementation)
//! +-- VP8Codec (reserved)
//! +-- VP9Codec (reserved)
//! +-- H265Codec (reserved)
//! ```
use bytes::Bytes;
use std::time::Duration;
use crate::error::Result;
use crate::video::format::Resolution;
/// Supported video codec types for WebRTC
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VideoCodecType {
/// H.264/AVC - widely supported, good compression
H264,
/// VP8 - royalty-free, good browser support
VP8,
/// VP9 - better compression than VP8
VP9,
/// H.265/HEVC - best compression, limited browser support
H265,
}
impl VideoCodecType {
/// Get the codec name for SDP
pub fn sdp_name(&self) -> &'static str {
match self {
VideoCodecType::H264 => "H264",
VideoCodecType::VP8 => "VP8",
VideoCodecType::VP9 => "VP9",
VideoCodecType::H265 => "H265",
}
}
/// Get the default RTP payload type
pub fn default_payload_type(&self) -> u8 {
match self {
VideoCodecType::H264 => 96,
VideoCodecType::VP8 => 97,
VideoCodecType::VP9 => 98,
VideoCodecType::H265 => 99,
}
}
/// Get the RTP clock rate (always 90000 for video)
pub fn clock_rate(&self) -> u32 {
90000
}
/// Get the MIME type
pub fn mime_type(&self) -> &'static str {
match self {
VideoCodecType::H264 => "video/H264",
VideoCodecType::VP8 => "video/VP8",
VideoCodecType::VP9 => "video/VP9",
VideoCodecType::H265 => "video/H265",
}
}
}
impl std::fmt::Display for VideoCodecType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.sdp_name())
}
}
/// Encoded video frame for WebRTC transmission
#[derive(Debug, Clone)]
pub struct CodecFrame {
/// Encoded data (Annex B format for H264/H265, raw for VP8/VP9)
pub data: Bytes,
/// Presentation timestamp in milliseconds
pub pts_ms: i64,
/// Whether this is a keyframe (IDR for H264, key frame for VP8/VP9)
pub is_keyframe: bool,
/// Codec type
pub codec: VideoCodecType,
/// Frame sequence number
pub sequence: u64,
/// Frame duration
pub duration: Duration,
}
impl CodecFrame {
/// Create a new H264 frame
pub fn h264(data: Bytes, pts_ms: i64, is_keyframe: bool, sequence: u64, fps: u32) -> Self {
Self {
data,
pts_ms,
is_keyframe,
codec: VideoCodecType::H264,
sequence,
duration: Duration::from_millis(1000 / fps as u64),
}
}
/// Create a new VP8 frame
pub fn vp8(data: Bytes, pts_ms: i64, is_keyframe: bool, sequence: u64, fps: u32) -> Self {
Self {
data,
pts_ms,
is_keyframe,
codec: VideoCodecType::VP8,
sequence,
duration: Duration::from_millis(1000 / fps as u64),
}
}
/// Create a new VP9 frame
pub fn vp9(data: Bytes, pts_ms: i64, is_keyframe: bool, sequence: u64, fps: u32) -> Self {
Self {
data,
pts_ms,
is_keyframe,
codec: VideoCodecType::VP9,
sequence,
duration: Duration::from_millis(1000 / fps as u64),
}
}
/// Create a new H265 frame
pub fn h265(data: Bytes, pts_ms: i64, is_keyframe: bool, sequence: u64, fps: u32) -> Self {
Self {
data,
pts_ms,
is_keyframe,
codec: VideoCodecType::H265,
sequence,
duration: Duration::from_millis(1000 / fps as u64),
}
}
/// Get frame size in bytes
pub fn len(&self) -> usize {
self.data.len()
}
/// Check if frame is empty
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
/// Video codec configuration
#[derive(Debug, Clone)]
pub struct VideoCodecConfig {
/// Codec type
pub codec: VideoCodecType,
/// Target resolution
pub resolution: Resolution,
/// Target bitrate in kbps
pub bitrate_kbps: u32,
/// Target FPS
pub fps: u32,
/// GOP size (keyframe interval in frames)
pub gop_size: u32,
/// Profile (codec-specific)
pub profile: Option<String>,
/// Level (codec-specific)
pub level: Option<String>,
}
impl Default for VideoCodecConfig {
fn default() -> Self {
Self {
codec: VideoCodecType::H264,
resolution: Resolution::HD720,
bitrate_kbps: 8000,
fps: 30,
gop_size: 30,
profile: None,
level: None,
}
}
}
impl VideoCodecConfig {
/// Create H264 config with common settings
pub fn h264(resolution: Resolution, bitrate_kbps: u32, fps: u32) -> Self {
Self {
codec: VideoCodecType::H264,
resolution,
bitrate_kbps,
fps,
gop_size: fps, // 1 second GOP
profile: Some("baseline".to_string()),
level: Some("3.1".to_string()),
}
}
/// Create VP8 config
pub fn vp8(resolution: Resolution, bitrate_kbps: u32, fps: u32) -> Self {
Self {
codec: VideoCodecType::VP8,
resolution,
bitrate_kbps,
fps,
gop_size: fps,
profile: None,
level: None,
}
}
/// Create VP9 config
pub fn vp9(resolution: Resolution, bitrate_kbps: u32, fps: u32) -> Self {
Self {
codec: VideoCodecType::VP9,
resolution,
bitrate_kbps,
fps,
gop_size: fps,
profile: None,
level: None,
}
}
/// Create H265 config
pub fn h265(resolution: Resolution, bitrate_kbps: u32, fps: u32) -> Self {
Self {
codec: VideoCodecType::H265,
resolution,
bitrate_kbps,
fps,
gop_size: fps,
profile: Some("main".to_string()),
level: Some("4.0".to_string()),
}
}
}
/// WebRTC video codec trait
///
/// This trait defines the interface for video codecs used in WebRTC streaming.
/// Implementations should handle format conversion internally if needed.
pub trait VideoCodec: Send {
/// Get codec type
fn codec_type(&self) -> VideoCodecType;
/// Get codec name for display
fn codec_name(&self) -> &'static str;
/// Get RTP payload type
fn payload_type(&self) -> u8 {
self.codec_type().default_payload_type()
}
/// Get SDP fmtp parameters (codec-specific)
///
/// For H264: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=<sps>"
/// For VP8/VP9: None or empty
fn sdp_fmtp(&self) -> Option<String>;
/// Encode a raw frame (NV12 format expected)
///
/// # Arguments
/// * `frame` - Raw frame data in NV12 format
/// * `pts_ms` - Presentation timestamp in milliseconds
///
/// # Returns
/// * `Ok(Some(frame))` - Encoded frame
/// * `Ok(None)` - Encoder is buffering (no output yet)
/// * `Err(e)` - Encoding error
fn encode(&mut self, frame: &[u8], pts_ms: i64) -> Result<Option<CodecFrame>>;
/// Set target bitrate dynamically
fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()>;
/// Request a keyframe on next encode
fn request_keyframe(&mut self);
/// Get current configuration
fn config(&self) -> &VideoCodecConfig;
/// Flush any pending frames
fn flush(&mut self) -> Result<Vec<CodecFrame>> {
Ok(vec![])
}
/// Reset encoder state
fn reset(&mut self) -> Result<()> {
Ok(())
}
}
/// Video codec factory trait
///
/// Used to create codec instances and query available codecs.
pub trait VideoCodecFactory: Send + Sync {
/// Create a codec with the given configuration
fn create(&self, config: VideoCodecConfig) -> Result<Box<dyn VideoCodec>>;
/// Get supported codec types
fn supported_codecs(&self) -> Vec<VideoCodecType>;
/// Check if a specific codec is available
fn is_codec_available(&self, codec: VideoCodecType) -> bool {
self.supported_codecs().contains(&codec)
}
/// Get the best available codec (based on priority)
fn best_codec(&self) -> Option<VideoCodecType> {
// Priority: H264 > VP8 > VP9 > H265
let supported = self.supported_codecs();
if supported.contains(&VideoCodecType::H264) {
Some(VideoCodecType::H264)
} else if supported.contains(&VideoCodecType::VP8) {
Some(VideoCodecType::VP8)
} else if supported.contains(&VideoCodecType::VP9) {
Some(VideoCodecType::VP9)
} else if supported.contains(&VideoCodecType::H265) {
Some(VideoCodecType::H265)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_codec_type_properties() {
assert_eq!(VideoCodecType::H264.sdp_name(), "H264");
assert_eq!(VideoCodecType::H264.default_payload_type(), 96);
assert_eq!(VideoCodecType::H264.clock_rate(), 90000);
assert_eq!(VideoCodecType::H264.mime_type(), "video/H264");
}
#[test]
fn test_codec_frame_creation() {
let data = Bytes::from(vec![0x00, 0x00, 0x00, 0x01, 0x65]);
let frame = CodecFrame::h264(data.clone(), 1000, true, 1, 30);
assert_eq!(frame.codec, VideoCodecType::H264);
assert!(frame.is_keyframe);
assert_eq!(frame.pts_ms, 1000);
assert_eq!(frame.sequence, 1);
assert_eq!(frame.len(), 5);
}
#[test]
fn test_codec_config_default() {
let config = VideoCodecConfig::default();
assert_eq!(config.codec, VideoCodecType::H264);
assert_eq!(config.bitrate_kbps, 8000);
assert_eq!(config.fps, 30);
}
#[test]
fn test_codec_config_h264() {
let config = VideoCodecConfig::h264(Resolution::HD1080, 4000, 60);
assert_eq!(config.codec, VideoCodecType::H264);
assert_eq!(config.bitrate_kbps, 4000);
assert_eq!(config.fps, 60);
assert_eq!(config.gop_size, 60);
}
}

483
src/video/codec/vp8.rs Normal file
View File

@@ -0,0 +1,483 @@
//! VP8 encoder using hwcodec (FFmpeg wrapper)
//!
//! Supports both hardware and software encoding:
//! - Hardware: VAAPI (Intel on Linux)
//! - Software: libvpx (CPU-based, high CPU usage)
//!
//! Hardware encoding is preferred when available for better performance.
use bytes::Bytes;
use std::sync::Once;
use tracing::{debug, error, info, warn};
use hwcodec::common::{DataFormat, Quality, RateControl};
use hwcodec::ffmpeg::{resolve_pixel_format, AVPixelFormat};
use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder};
use hwcodec::ffmpeg_ram::CodecInfo;
use super::detect_best_codec_for_format;
use super::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType};
use super::traits::{EncodedFormat, EncodedFrame, Encoder, EncoderConfig};
use crate::error::{AppError, Result};
use crate::video::format::{PixelFormat, Resolution};
static INIT_LOGGING: Once = Once::new();
/// Initialize hwcodec logging (only once)
fn init_hwcodec_logging() {
INIT_LOGGING.call_once(|| {
debug!("hwcodec logging initialized for VP8");
});
}
/// VP8 encoder type (detected from hwcodec)
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum VP8EncoderType {
/// VAAPI (Intel on Linux)
Vaapi,
/// Software encoder (libvpx)
Software,
/// No encoder available
#[default]
None,
}
impl std::fmt::Display for VP8EncoderType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VP8EncoderType::Vaapi => write!(f, "VAAPI"),
VP8EncoderType::Software => write!(f, "Software"),
VP8EncoderType::None => write!(f, "None"),
}
}
}
impl From<EncoderBackend> for VP8EncoderType {
fn from(backend: EncoderBackend) -> Self {
match backend {
EncoderBackend::Vaapi => VP8EncoderType::Vaapi,
EncoderBackend::Software => VP8EncoderType::Software,
_ => VP8EncoderType::None,
}
}
}
/// Input pixel format for VP8 encoder
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VP8InputFormat {
/// YUV420P (I420) - planar Y, U, V
Yuv420p,
/// NV12 - Y plane + interleaved UV plane
#[default]
Nv12,
}
/// VP8 encoder configuration
#[derive(Debug, Clone)]
pub struct VP8Config {
/// Base encoder config
pub base: EncoderConfig,
/// Target bitrate in kbps
pub bitrate_kbps: u32,
/// GOP size (keyframe interval)
pub gop_size: u32,
/// Frame rate
pub fps: u32,
/// Input pixel format
pub input_format: VP8InputFormat,
}
impl Default for VP8Config {
fn default() -> Self {
Self {
base: EncoderConfig::default(),
bitrate_kbps: 8000,
gop_size: 30,
fps: 30,
input_format: VP8InputFormat::Nv12,
}
}
}
impl VP8Config {
/// Create config for low latency streaming with NV12 input
pub fn low_latency(resolution: Resolution, bitrate_kbps: u32) -> Self {
Self {
base: EncoderConfig {
resolution,
input_format: PixelFormat::Nv12,
quality: bitrate_kbps,
fps: 30,
gop_size: 30,
},
bitrate_kbps,
gop_size: 30,
fps: 30,
input_format: VP8InputFormat::Nv12,
}
}
/// Set input format
pub fn with_input_format(mut self, format: VP8InputFormat) -> Self {
self.input_format = format;
self
}
}
/// Get available VP8 hardware encoders from hwcodec
pub fn get_available_vp8_encoders(width: u32, height: u32) -> Vec<CodecInfo> {
init_hwcodec_logging();
let ctx = EncodeContext {
name: String::new(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
align: 1,
fps: 30,
gop: 30,
rc: RateControl::RC_CBR,
quality: Quality::Quality_Default,
kbs: 2000,
q: 23,
thread_count: 1,
};
let all_encoders = HwEncoder::available_encoders(ctx, None);
// Include both hardware and software VP8 encoders
all_encoders
.into_iter()
.filter(|e| e.format == DataFormat::VP8)
.collect()
}
/// Detect best available VP8 encoder (hardware preferred, software fallback)
pub fn detect_best_vp8_encoder(width: u32, height: u32) -> (VP8EncoderType, Option<String>) {
let encoders = get_available_vp8_encoders(width, height);
// Prefer hardware encoders (VAAPI) over software (libvpx)
if let Some((encoder_type, codec_name)) =
detect_best_codec_for_format(&encoders, DataFormat::VP8, |codec| {
codec.name.contains("vaapi")
})
{
info!("Selected VP8 encoder: {} ({})", codec_name, encoder_type);
(encoder_type, Some(codec_name))
} else {
warn!("No VP8 encoders available");
(VP8EncoderType::None, None)
}
}
/// Check if VP8 hardware encoding is available
pub fn is_vp8_available() -> bool {
let registry = EncoderRegistry::global();
registry.is_codec_available(VideoEncoderType::VP8)
}
/// Encoded frame from hwcodec (cloned for ownership)
#[derive(Debug, Clone)]
pub struct HwEncodeFrame {
pub data: Vec<u8>,
pub pts: i64,
pub key: i32,
}
/// VP8 encoder using hwcodec
pub struct VP8Encoder {
/// hwcodec encoder instance
inner: HwEncoder,
/// Encoder configuration
config: VP8Config,
/// Detected encoder type
encoder_type: VP8EncoderType,
/// Codec name
codec_name: String,
/// Frame counter
frame_count: u64,
/// Required buffer length from hwcodec
buffer_length: i32,
}
impl VP8Encoder {
/// Create a new VP8 encoder with automatic hardware codec detection
///
/// Returns an error if no hardware encoder is available.
/// VP8 hardware encoding requires Intel VAAPI support.
pub fn new(config: VP8Config) -> Result<Self> {
init_hwcodec_logging();
let width = config.base.resolution.width;
let height = config.base.resolution.height;
let (encoder_type, codec_name) = detect_best_vp8_encoder(width, height);
if encoder_type == VP8EncoderType::None {
return Err(AppError::VideoError(
"No VP8 encoder available. Please ensure FFmpeg is built with libvpx support."
.to_string(),
));
}
let codec_name = codec_name.unwrap();
Self::with_codec(config, &codec_name)
}
/// Create encoder with specific codec name
pub fn with_codec(config: VP8Config, codec_name: &str) -> Result<Self> {
init_hwcodec_logging();
// Determine if this is a software encoder
let is_software = codec_name.contains("libvpx");
// Warn about software encoder performance
if is_software {
warn!(
"Using software VP8 encoder (libvpx) - high CPU usage expected. \
Hardware encoder is recommended for better performance."
);
}
let width = config.base.resolution.width;
let height = config.base.resolution.height;
// Software encoders (libvpx) require YUV420P, hardware (VAAPI) uses NV12
let (pixfmt_name, pixfmt_fallback, actual_input_format) = if is_software {
(
"yuv420p",
AVPixelFormat::AV_PIX_FMT_YUV420P,
VP8InputFormat::Yuv420p,
)
} else {
match config.input_format {
VP8InputFormat::Nv12 => {
("nv12", AVPixelFormat::AV_PIX_FMT_NV12, VP8InputFormat::Nv12)
}
VP8InputFormat::Yuv420p => (
"yuv420p",
AVPixelFormat::AV_PIX_FMT_YUV420P,
VP8InputFormat::Yuv420p,
),
}
};
let pixfmt = resolve_pixel_format(pixfmt_name, pixfmt_fallback);
info!(
"Creating VP8 encoder: {} at {}x{} @ {} kbps (input: {:?})",
codec_name, width, height, config.bitrate_kbps, actual_input_format
);
let ctx = EncodeContext {
name: codec_name.to_string(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt,
align: 1,
fps: config.fps as i32,
gop: config.gop_size as i32,
rc: RateControl::RC_CBR,
quality: Quality::Quality_Default,
kbs: config.bitrate_kbps as i32,
q: 23,
thread_count: 1,
};
let inner = HwEncoder::new(ctx).map_err(|_| {
AppError::VideoError(format!("Failed to create VP8 encoder: {}", codec_name))
})?;
let buffer_length = inner.length;
let backend = EncoderBackend::from_codec_name(codec_name);
let encoder_type = VP8EncoderType::from(backend);
// Update config to reflect actual input format used
let mut config = config;
config.input_format = actual_input_format;
info!(
"VP8 encoder created: {} (type: {}, buffer_length: {})",
codec_name, encoder_type, buffer_length
);
Ok(Self {
inner,
config,
encoder_type,
codec_name: codec_name.to_string(),
frame_count: 0,
buffer_length,
})
}
/// Create with auto-detected encoder
pub fn auto(resolution: Resolution, bitrate_kbps: u32) -> Result<Self> {
let config = VP8Config::low_latency(resolution, bitrate_kbps);
Self::new(config)
}
/// Get encoder type
pub fn encoder_type(&self) -> &VP8EncoderType {
&self.encoder_type
}
/// Get codec name
pub fn codec_name(&self) -> &str {
&self.codec_name
}
/// Update bitrate dynamically
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
self.inner
.set_bitrate(bitrate_kbps as i32)
.map_err(|_| AppError::VideoError("Failed to set VP8 bitrate".to_string()))?;
self.config.bitrate_kbps = bitrate_kbps;
debug!("VP8 bitrate updated to {} kbps", bitrate_kbps);
Ok(())
}
/// Encode raw frame data
pub fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
if data.len() < self.buffer_length as usize {
return Err(AppError::VideoError(format!(
"Frame data too small: {} < {}",
data.len(),
self.buffer_length
)));
}
self.frame_count += 1;
match self.inner.encode(data, pts_ms) {
Ok(frames) => {
// Zero-copy: drain frames from hwcodec buffer instead of cloning
let owned_frames: Vec<HwEncodeFrame> = frames
.drain(..)
.map(|f| HwEncodeFrame {
data: f.data, // Move, not clone
pts: f.pts,
key: f.key,
})
.collect();
Ok(owned_frames)
}
Err(e) => {
error!("VP8 encode failed: {}", e);
Err(AppError::VideoError(format!("VP8 encode failed: {}", e)))
}
}
}
/// Encode NV12 data
pub fn encode_nv12(&mut self, nv12_data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
self.encode_raw(nv12_data, pts_ms)
}
/// Get input format
pub fn input_format(&self) -> VP8InputFormat {
self.config.input_format
}
/// Get buffer info
pub fn buffer_info(&self) -> (Vec<i32>, Vec<i32>, i32) {
(
self.inner.linesize.clone(),
self.inner.offset.clone(),
self.inner.length,
)
}
}
// SAFETY: VP8Encoder contains hwcodec::ffmpeg_ram::encode::Encoder which has raw pointers
// that are not Send by default. However, we ensure that VP8Encoder is only used from
// a single task/thread at a time (encoding is sequential), so this is safe.
unsafe impl Send for VP8Encoder {}
impl Encoder for VP8Encoder {
fn name(&self) -> &str {
&self.codec_name
}
fn output_format(&self) -> EncodedFormat {
EncodedFormat::Vp8
}
fn encode(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
let pts_ms = (sequence * 1000 / self.config.fps as u64) as i64;
let mut frames = self.encode_raw(data, pts_ms)?;
if frames.is_empty() {
warn!("VP8 encoder returned no frames");
return Err(AppError::VideoError(
"VP8 encoder returned no frames".to_string(),
));
}
// Take ownership of the first frame (zero-copy)
let frame = frames.remove(0);
let key_frame = frame.key == 1;
Ok(EncodedFrame {
data: Bytes::from(frame.data), // Move Vec into Bytes (zero-copy)
format: EncodedFormat::Vp8,
resolution: self.config.base.resolution,
key_frame,
sequence,
timestamp: std::time::Instant::now(),
pts: frame.pts as u64,
dts: frame.pts as u64,
})
}
fn flush(&mut self) -> Result<Vec<EncodedFrame>> {
Ok(vec![])
}
fn reset(&mut self) -> Result<()> {
self.frame_count = 0;
Ok(())
}
fn config(&self) -> &EncoderConfig {
&self.config.base
}
fn supports_format(&self, format: PixelFormat) -> bool {
match self.config.input_format {
VP8InputFormat::Nv12 => matches!(format, PixelFormat::Nv12),
VP8InputFormat::Yuv420p => matches!(format, PixelFormat::Yuv420),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_vp8_encoder() {
let (encoder_type, codec_name) = detect_best_vp8_encoder(1280, 720);
println!(
"Detected VP8 encoder: {:?} ({:?})",
encoder_type, codec_name
);
}
#[test]
fn test_available_vp8_encoders() {
let encoders = get_available_vp8_encoders(1280, 720);
println!("Available VP8 hardware encoders:");
for enc in &encoders {
println!(" - {} ({:?})", enc.name, enc.format);
}
}
#[test]
fn test_vp8_availability() {
let available = is_vp8_available();
println!("VP8 hardware encoding available: {}", available);
}
}

483
src/video/codec/vp9.rs Normal file
View File

@@ -0,0 +1,483 @@
//! VP9 encoder using hwcodec (FFmpeg wrapper)
//!
//! Supports both hardware and software encoding:
//! - Hardware: VAAPI (Intel on Linux)
//! - Software: libvpx-vp9 (CPU-based, high CPU usage)
//!
//! Hardware encoding is preferred when available for better performance.
use bytes::Bytes;
use std::sync::Once;
use tracing::{debug, error, info, warn};
use hwcodec::common::{DataFormat, Quality, RateControl};
use hwcodec::ffmpeg::{resolve_pixel_format, AVPixelFormat};
use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder};
use hwcodec::ffmpeg_ram::CodecInfo;
use super::detect_best_codec_for_format;
use super::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType};
use super::traits::{EncodedFormat, EncodedFrame, Encoder, EncoderConfig};
use crate::error::{AppError, Result};
use crate::video::format::{PixelFormat, Resolution};
static INIT_LOGGING: Once = Once::new();
/// Initialize hwcodec logging (only once)
fn init_hwcodec_logging() {
INIT_LOGGING.call_once(|| {
debug!("hwcodec logging initialized for VP9");
});
}
/// VP9 encoder type (detected from hwcodec)
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum VP9EncoderType {
/// VAAPI (Intel on Linux)
Vaapi,
/// Software encoder (libvpx-vp9)
Software,
/// No encoder available
#[default]
None,
}
impl std::fmt::Display for VP9EncoderType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VP9EncoderType::Vaapi => write!(f, "VAAPI"),
VP9EncoderType::Software => write!(f, "Software"),
VP9EncoderType::None => write!(f, "None"),
}
}
}
impl From<EncoderBackend> for VP9EncoderType {
fn from(backend: EncoderBackend) -> Self {
match backend {
EncoderBackend::Vaapi => VP9EncoderType::Vaapi,
EncoderBackend::Software => VP9EncoderType::Software,
_ => VP9EncoderType::None,
}
}
}
/// Input pixel format for VP9 encoder
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VP9InputFormat {
/// YUV420P (I420) - planar Y, U, V
Yuv420p,
/// NV12 - Y plane + interleaved UV plane
#[default]
Nv12,
}
/// VP9 encoder configuration
#[derive(Debug, Clone)]
pub struct VP9Config {
/// Base encoder config
pub base: EncoderConfig,
/// Target bitrate in kbps
pub bitrate_kbps: u32,
/// GOP size (keyframe interval)
pub gop_size: u32,
/// Frame rate
pub fps: u32,
/// Input pixel format
pub input_format: VP9InputFormat,
}
impl Default for VP9Config {
fn default() -> Self {
Self {
base: EncoderConfig::default(),
bitrate_kbps: 8000,
gop_size: 30,
fps: 30,
input_format: VP9InputFormat::Nv12,
}
}
}
impl VP9Config {
/// Create config for low latency streaming with NV12 input
pub fn low_latency(resolution: Resolution, bitrate_kbps: u32) -> Self {
Self {
base: EncoderConfig {
resolution,
input_format: PixelFormat::Nv12,
quality: bitrate_kbps,
fps: 30,
gop_size: 30,
},
bitrate_kbps,
gop_size: 30,
fps: 30,
input_format: VP9InputFormat::Nv12,
}
}
/// Set input format
pub fn with_input_format(mut self, format: VP9InputFormat) -> Self {
self.input_format = format;
self
}
}
/// Get available VP9 hardware encoders from hwcodec
pub fn get_available_vp9_encoders(width: u32, height: u32) -> Vec<CodecInfo> {
init_hwcodec_logging();
let ctx = EncodeContext {
name: String::new(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
align: 1,
fps: 30,
gop: 30,
rc: RateControl::RC_CBR,
quality: Quality::Quality_Default,
kbs: 2000,
q: 23,
thread_count: 1,
};
let all_encoders = HwEncoder::available_encoders(ctx, None);
// Include both hardware and software VP9 encoders
all_encoders
.into_iter()
.filter(|e| e.format == DataFormat::VP9)
.collect()
}
/// Detect best available VP9 encoder (hardware preferred, software fallback)
pub fn detect_best_vp9_encoder(width: u32, height: u32) -> (VP9EncoderType, Option<String>) {
let encoders = get_available_vp9_encoders(width, height);
// Prefer hardware encoders (VAAPI) over software (libvpx-vp9)
if let Some((encoder_type, codec_name)) =
detect_best_codec_for_format(&encoders, DataFormat::VP9, |codec| {
codec.name.contains("vaapi")
})
{
info!("Selected VP9 encoder: {} ({})", codec_name, encoder_type);
(encoder_type, Some(codec_name))
} else {
warn!("No VP9 encoders available");
(VP9EncoderType::None, None)
}
}
/// Check if VP9 hardware encoding is available
pub fn is_vp9_available() -> bool {
let registry = EncoderRegistry::global();
registry.is_codec_available(VideoEncoderType::VP9)
}
/// Encoded frame from hwcodec (cloned for ownership)
#[derive(Debug, Clone)]
pub struct HwEncodeFrame {
pub data: Vec<u8>,
pub pts: i64,
pub key: i32,
}
/// VP9 encoder using hwcodec
pub struct VP9Encoder {
/// hwcodec encoder instance
inner: HwEncoder,
/// Encoder configuration
config: VP9Config,
/// Detected encoder type
encoder_type: VP9EncoderType,
/// Codec name
codec_name: String,
/// Frame counter
frame_count: u64,
/// Required buffer length from hwcodec
buffer_length: i32,
}
impl VP9Encoder {
/// Create a new VP9 encoder with automatic hardware codec detection
///
/// Returns an error if no hardware encoder is available.
/// VP9 hardware encoding requires Intel VAAPI support.
pub fn new(config: VP9Config) -> Result<Self> {
init_hwcodec_logging();
let width = config.base.resolution.width;
let height = config.base.resolution.height;
let (encoder_type, codec_name) = detect_best_vp9_encoder(width, height);
if encoder_type == VP9EncoderType::None {
return Err(AppError::VideoError(
"No VP9 encoder available. Please ensure FFmpeg is built with libvpx support."
.to_string(),
));
}
let codec_name = codec_name.unwrap();
Self::with_codec(config, &codec_name)
}
/// Create encoder with specific codec name
pub fn with_codec(config: VP9Config, codec_name: &str) -> Result<Self> {
init_hwcodec_logging();
// Determine if this is a software encoder
let is_software = codec_name.contains("libvpx");
// Warn about software encoder performance
if is_software {
warn!(
"Using software VP9 encoder (libvpx-vp9) - high CPU usage expected. \
Hardware encoder is recommended for better performance."
);
}
let width = config.base.resolution.width;
let height = config.base.resolution.height;
// Software encoders (libvpx-vp9) require YUV420P, hardware (VAAPI) uses NV12
let (pixfmt_name, pixfmt_fallback, actual_input_format) = if is_software {
(
"yuv420p",
AVPixelFormat::AV_PIX_FMT_YUV420P,
VP9InputFormat::Yuv420p,
)
} else {
match config.input_format {
VP9InputFormat::Nv12 => {
("nv12", AVPixelFormat::AV_PIX_FMT_NV12, VP9InputFormat::Nv12)
}
VP9InputFormat::Yuv420p => (
"yuv420p",
AVPixelFormat::AV_PIX_FMT_YUV420P,
VP9InputFormat::Yuv420p,
),
}
};
let pixfmt = resolve_pixel_format(pixfmt_name, pixfmt_fallback);
info!(
"Creating VP9 encoder: {} at {}x{} @ {} kbps (input: {:?})",
codec_name, width, height, config.bitrate_kbps, actual_input_format
);
let ctx = EncodeContext {
name: codec_name.to_string(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt,
align: 1,
fps: config.fps as i32,
gop: config.gop_size as i32,
rc: RateControl::RC_CBR,
quality: Quality::Quality_Default,
kbs: config.bitrate_kbps as i32,
q: 31,
thread_count: 4, // VP9 benefits from multi-threading
};
let inner = HwEncoder::new(ctx).map_err(|_| {
AppError::VideoError(format!("Failed to create VP9 encoder: {}", codec_name))
})?;
let buffer_length = inner.length;
let backend = EncoderBackend::from_codec_name(codec_name);
let encoder_type = VP9EncoderType::from(backend);
// Update config to reflect actual input format used
let mut config = config;
config.input_format = actual_input_format;
info!(
"VP9 encoder created: {} (type: {}, buffer_length: {})",
codec_name, encoder_type, buffer_length
);
Ok(Self {
inner,
config,
encoder_type,
codec_name: codec_name.to_string(),
frame_count: 0,
buffer_length,
})
}
/// Create with auto-detected encoder
pub fn auto(resolution: Resolution, bitrate_kbps: u32) -> Result<Self> {
let config = VP9Config::low_latency(resolution, bitrate_kbps);
Self::new(config)
}
/// Get encoder type
pub fn encoder_type(&self) -> &VP9EncoderType {
&self.encoder_type
}
/// Get codec name
pub fn codec_name(&self) -> &str {
&self.codec_name
}
/// Update bitrate dynamically
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
self.inner
.set_bitrate(bitrate_kbps as i32)
.map_err(|_| AppError::VideoError("Failed to set VP9 bitrate".to_string()))?;
self.config.bitrate_kbps = bitrate_kbps;
debug!("VP9 bitrate updated to {} kbps", bitrate_kbps);
Ok(())
}
/// Encode raw frame data
pub fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
if data.len() < self.buffer_length as usize {
return Err(AppError::VideoError(format!(
"Frame data too small: {} < {}",
data.len(),
self.buffer_length
)));
}
self.frame_count += 1;
match self.inner.encode(data, pts_ms) {
Ok(frames) => {
// Zero-copy: drain frames from hwcodec buffer instead of cloning
let owned_frames: Vec<HwEncodeFrame> = frames
.drain(..)
.map(|f| HwEncodeFrame {
data: f.data, // Move, not clone
pts: f.pts,
key: f.key,
})
.collect();
Ok(owned_frames)
}
Err(e) => {
error!("VP9 encode failed: {}", e);
Err(AppError::VideoError(format!("VP9 encode failed: {}", e)))
}
}
}
/// Encode NV12 data
pub fn encode_nv12(&mut self, nv12_data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
self.encode_raw(nv12_data, pts_ms)
}
/// Get input format
pub fn input_format(&self) -> VP9InputFormat {
self.config.input_format
}
/// Get buffer info
pub fn buffer_info(&self) -> (Vec<i32>, Vec<i32>, i32) {
(
self.inner.linesize.clone(),
self.inner.offset.clone(),
self.inner.length,
)
}
}
// SAFETY: VP9Encoder contains hwcodec::ffmpeg_ram::encode::Encoder which has raw pointers
// that are not Send by default. However, we ensure that VP9Encoder is only used from
// a single task/thread at a time (encoding is sequential), so this is safe.
unsafe impl Send for VP9Encoder {}
impl Encoder for VP9Encoder {
fn name(&self) -> &str {
&self.codec_name
}
fn output_format(&self) -> EncodedFormat {
EncodedFormat::Vp9
}
fn encode(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
let pts_ms = (sequence * 1000 / self.config.fps as u64) as i64;
let mut frames = self.encode_raw(data, pts_ms)?;
if frames.is_empty() {
warn!("VP9 encoder returned no frames");
return Err(AppError::VideoError(
"VP9 encoder returned no frames".to_string(),
));
}
// Take ownership of the first frame (zero-copy)
let frame = frames.remove(0);
let key_frame = frame.key == 1;
Ok(EncodedFrame {
data: Bytes::from(frame.data), // Move Vec into Bytes (zero-copy)
format: EncodedFormat::Vp9,
resolution: self.config.base.resolution,
key_frame,
sequence,
timestamp: std::time::Instant::now(),
pts: frame.pts as u64,
dts: frame.pts as u64,
})
}
fn flush(&mut self) -> Result<Vec<EncodedFrame>> {
Ok(vec![])
}
fn reset(&mut self) -> Result<()> {
self.frame_count = 0;
Ok(())
}
fn config(&self) -> &EncoderConfig {
&self.config.base
}
fn supports_format(&self, format: PixelFormat) -> bool {
match self.config.input_format {
VP9InputFormat::Nv12 => matches!(format, PixelFormat::Nv12),
VP9InputFormat::Yuv420p => matches!(format, PixelFormat::Yuv420),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_vp9_encoder() {
let (encoder_type, codec_name) = detect_best_vp9_encoder(1280, 720);
println!(
"Detected VP9 encoder: {:?} ({:?})",
encoder_type, codec_name
);
}
#[test]
fn test_available_vp9_encoders() {
let encoders = get_available_vp9_encoders(1280, 720);
println!("Available VP9 hardware encoders:");
for enc in &encoders {
println!(" - {} ({:?})", enc.name, enc.format);
}
}
#[test]
fn test_vp9_availability() {
let available = is_vp9_available();
println!("VP9 hardware encoding available: {}", available);
}
}