use std::collections::VecDeque;
use std::rc::Rc;
use crate::{EncodeError, VideoEncoder, VideoEncoderConfig, VideoInputPreference};
use mediaway_common::{
Bytes, CodecKind, Packet, PixelFormat, StreamInfo, VideoFrame, VideoFrameStorage, VideoGeometry,
};
use cros_libva::{
BufferType, Config, Context, Display, EncPictureParameter, EncSequenceParameter,
EncSliceParameter, H264EncPicFields, H264EncSeqFields, Image, MappedCodedBuffer, Picture,
PictureH264, Surface, UsageHint, VA_FOURCC_NV12, VA_INVALID_ID, VA_LSB_FIRST, VA_RC_CQP,
VA_RT_FORMAT_YUV420, VAConfigAttrib, VAConfigAttribType, VAEntrypoint, VAImageFormat,
};
use super::codec::video_profile;
const SURFACE_POOL_SIZE: usize = 4;
const FIXED_QP: u8 = 26;
const LEVEL_IDC: u8 = 30;
pub(crate) struct VaapiVideoEncoder {
context: Rc<Context>,
_config: Config,
info: StreamInfo,
width: u32,
height: u32,
mb_width: u16,
mb_height: u16,
nv12_bytes: usize,
bits_per_second: u32,
surfaces: Vec<Option<Surface<()>>>,
next_surface: usize,
pending: VecDeque<Packet>,
flushed: bool,
}
impl VaapiVideoEncoder {
pub(crate) fn open(config: &VideoEncoderConfig) -> Result<Self, EncodeError> {
validate(config)?;
match config.input {
VideoInputPreference::CpuUploadOk => Self::open_cpu(config),
_ => Err(EncodeError::Unsupported),
}
}
fn open_cpu(config: &VideoEncoderConfig) -> Result<Self, EncodeError> {
let display: Rc<Display> = Display::open().ok_or(EncodeError::Backend)?;
let profile = video_profile(config.codec)?;
let attrs = vec![VAConfigAttrib {
type_: VAConfigAttribType::VAConfigAttribRateControl,
value: VA_RC_CQP,
}];
let vaconfig = display
.create_config(attrs, profile, VAEntrypoint::VAEntrypointEncSlice)
.map_err(|_| EncodeError::Backend)?;
let mb_width = mb_count(config.width)?;
let mb_height = mb_count(config.height)?;
let surfaces = display
.create_surfaces(
VA_RT_FORMAT_YUV420,
Some(VA_FOURCC_NV12),
config.width,
config.height,
Some(UsageHint::USAGE_HINT_ENCODER),
vec![(); SURFACE_POOL_SIZE],
)
.map_err(|_| EncodeError::Backend)?;
let context = display
.create_context(
&vaconfig,
config.width,
config.height,
Some(&surfaces),
true,
)
.map_err(|_| EncodeError::Backend)?;
let nv12_bytes = nv12_size(config.width, config.height)?;
Ok(Self {
context,
_config: vaconfig,
info: stream_info_from(config),
width: config.width,
height: config.height,
mb_width,
mb_height,
nv12_bytes,
bits_per_second: config.bitrate_bps,
surfaces: surfaces.into_iter().map(Some).collect(),
next_surface: 0,
pending: VecDeque::new(),
flushed: false,
})
}
fn encode_one(
&self,
surface: Surface<()>,
frame: &VideoFrame,
) -> (Option<Surface<()>>, Result<Packet, EncodeError>) {
let num_macroblocks = u32::from(self.mb_width) * u32::from(self.mb_height);
let coded_size = self.nv12_bytes.saturating_mul(2).max(4096);
let Ok(coded_buf) = self.context.create_enc_coded(coded_size) else {
return (Some(surface), Err(EncodeError::Backend));
};
let surface_id = surface.id();
let seq_params = build_seq_params(self.mb_width, self.mb_height, self.bits_per_second);
let pic_params = build_pic_params(surface_id, coded_buf.id());
let slice_params = build_slice_params(num_macroblocks);
let Ok(seq_buf) = self.context.create_buffer(BufferType::EncSequenceParameter(
EncSequenceParameter::H264(seq_params),
)) else {
return (Some(surface), Err(EncodeError::Backend));
};
let Ok(pic_buf) =
self.context
.create_buffer(BufferType::EncPictureParameter(EncPictureParameter::H264(
pic_params,
)))
else {
return (Some(surface), Err(EncodeError::Backend));
};
let Ok(slice_buf) =
self.context
.create_buffer(BufferType::EncSliceParameter(EncSliceParameter::H264(
slice_params,
)))
else {
return (Some(surface), Err(EncodeError::Backend));
};
let timestamp = u64::try_from(frame.pts).unwrap_or(0);
let mut picture = Picture::new::<()>(timestamp, Rc::clone(&self.context), surface);
picture.add_buffer(seq_buf);
picture.add_buffer(pic_buf);
picture.add_buffer(slice_buf);
let Ok(picture) = picture.begin::<()>() else {
return (None, Err(EncodeError::Backend));
};
let Ok(picture) = picture.render() else {
return (None, Err(EncodeError::Backend));
};
let Ok(picture) = picture.end() else {
return (None, Err(EncodeError::Backend));
};
let Ok(picture) = picture.sync::<()>() else {
return (None, Err(EncodeError::Backend));
};
let bytes = match MappedCodedBuffer::new(&coded_buf) {
Ok(mapped) => {
let mut bytes = Vec::new();
for segment in mapped.iter() {
bytes.extend_from_slice(segment.buf);
}
bytes
}
Err(_) => return (picture.take_surface().ok(), Err(EncodeError::Backend)),
};
let surface = picture.take_surface().ok();
let packet = Packet {
stream_id: 0,
pts: frame.pts,
dts: frame.pts,
duration: frame.duration,
is_keyframe: true,
is_discard: false,
payload: Bytes::from(bytes),
};
(surface, Ok(packet))
}
}
impl VideoEncoder for VaapiVideoEncoder {
fn stream_info(&self) -> &StreamInfo {
&self.info
}
fn push_frame(&mut self, frame: &VideoFrame) -> Result<(), EncodeError> {
if self.flushed {
return Err(EncodeError::Closed);
}
let VideoFrameStorage::Cpu { data } = &frame.storage else {
return Err(EncodeError::Unsupported);
};
if frame.width != self.width || frame.height != self.height {
return Err(EncodeError::InvalidInput);
}
if data.len() < self.nv12_bytes {
return Err(EncodeError::InvalidInput);
}
let slot = self.next_surface;
self.next_surface = (self.next_surface + 1) % self.surfaces.len();
let surface = self.surfaces[slot].take().ok_or(EncodeError::Backend)?;
let surface = match upload_cpu_nv12(&surface, data, self.width, self.height) {
Ok(()) => surface,
Err(e) => {
self.surfaces[slot] = Some(surface);
return Err(e);
}
};
let (returned_surface, result) = self.encode_one(surface, frame);
self.surfaces[slot] = returned_surface;
let packet = result?;
self.pending.push_back(packet);
Ok(())
}
fn poll_packet(&mut self) -> Result<Option<Packet>, EncodeError> {
Ok(self.pending.pop_front())
}
fn flush(&mut self) -> Result<(), EncodeError> {
self.flushed = true;
Ok(())
}
}
fn upload_cpu_nv12(
surface: &Surface<()>,
data: &[u8],
width: u32,
height: u32,
) -> Result<(), EncodeError> {
let format = VAImageFormat {
fourcc: VA_FOURCC_NV12,
byte_order: VA_LSB_FIRST,
bits_per_pixel: 12,
depth: 0,
red_mask: 0,
green_mask: 0,
blue_mask: 0,
alpha_mask: 0,
va_reserved: Default::default(),
};
let mut image = Image::create_from(surface, format, (width, height), (width, height))
.map_err(|_| EncodeError::Backend)?;
let va_image = *image.image();
let offsets = va_image.offsets;
let pitches = va_image.pitches;
let w = width as usize;
let h = height as usize;
let y_pitch = pitches[0] as usize;
let y_offset = offsets[0] as usize;
let uv_pitch = pitches[1] as usize;
let uv_offset = offsets[1] as usize;
let y_plane_bytes = w * h;
let uv_rows = h / 2;
let dst = image.as_mut();
if dst.len() < y_offset + y_pitch * h || dst.len() < uv_offset + uv_pitch * uv_rows {
return Err(EncodeError::Backend);
}
for row in 0..h {
let src = row * w;
let dst_off = y_offset + row * y_pitch;
dst[dst_off..dst_off + w].copy_from_slice(&data[src..src + w]);
}
for row in 0..uv_rows {
let src = y_plane_bytes + row * w;
let dst_off = uv_offset + row * uv_pitch;
dst[dst_off..dst_off + w].copy_from_slice(&data[src..src + w]);
}
Ok(())
}
fn build_seq_params(
mb_width: u16,
mb_height: u16,
bits_per_second: u32,
) -> cros_libva::EncSequenceParameterBufferH264 {
let seq_fields = H264EncSeqFields::new(
1, 1, 0, 0, 1, 4, 2, 0, 0, );
cros_libva::EncSequenceParameterBufferH264::new(
0, LEVEL_IDC,
1, 1, 0, bits_per_second,
1, mb_width,
mb_height,
&seq_fields,
0, 0, 0, 0, 0, [0i32; 256], None, None, 0, 0, 0, 0, 0, )
}
fn build_pic_params(
surface_id: cros_libva::VASurfaceID,
coded_buf_id: cros_libva::VABufferID,
) -> cros_libva::EncPictureParameterBufferH264 {
let curr_pic = PictureH264::new(surface_id, 0, 0, 0, 0);
let reference_frames: [PictureH264; 16] = std::array::from_fn(|_| invalid_picture_h264());
let pic_fields = H264EncPicFields::new(
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, );
cros_libva::EncPictureParameterBufferH264::new(
curr_pic,
reference_frames,
coded_buf_id,
0, 0, 0, 0, FIXED_QP,
0, 0, 0, 0, &pic_fields,
)
}
fn build_slice_params(num_macroblocks: u32) -> cros_libva::EncSliceParameterBufferH264 {
let ref_pic_list_0: [PictureH264; 32] = std::array::from_fn(|_| invalid_picture_h264());
let ref_pic_list_1: [PictureH264; 32] = std::array::from_fn(|_| invalid_picture_h264());
cros_libva::EncSliceParameterBufferH264::new(
0, num_macroblocks,
VA_INVALID_ID, 2, 0, 0, 0, 0, [0i32; 2], 0, 0, 0, 0, ref_pic_list_0,
ref_pic_list_1,
0, 0, 0, [0i16; 32],
[0i16; 32],
0, [[0i16; 2]; 32],
[[0i16; 2]; 32],
0, [0i16; 32],
[0i16; 32],
0, [[0i16; 2]; 32],
[[0i16; 2]; 32],
0, 0, 0, 0, 0, )
}
fn invalid_picture_h264() -> PictureH264 {
PictureH264::new(
cros_libva::VA_INVALID_SURFACE,
0,
cros_libva::VA_PICTURE_H264_INVALID,
0,
0,
)
}
fn validate(config: &VideoEncoderConfig) -> Result<(), EncodeError> {
if !super::codec::is_supported_video_codec(config.codec) {
return Err(EncodeError::Unsupported);
}
if config.width == 0 || config.height == 0 {
return Err(EncodeError::InvalidInput);
}
if config.width % 16 != 0 || config.height % 16 != 0 {
return Err(EncodeError::Unsupported);
}
if config.pixel_format != PixelFormat::Nv12 {
return Err(EncodeError::Unsupported);
}
if config.time_base.den == 0 {
return Err(EncodeError::InvalidInput);
}
Ok(())
}
fn mb_count(dim: u32) -> Result<u16, EncodeError> {
u16::try_from(dim / 16).map_err(|_| EncodeError::InvalidInput)
}
fn nv12_size(width: u32, height: u32) -> Result<usize, EncodeError> {
let w = usize::try_from(width).map_err(|_| EncodeError::InvalidInput)?;
let h = usize::try_from(height).map_err(|_| EncodeError::InvalidInput)?;
w.checked_mul(h)
.and_then(|y| y.checked_mul(3))
.and_then(|v| v.checked_div(2))
.ok_or(EncodeError::InvalidInput)
}
#[allow(clippy::missing_const_for_fn, reason = "StreamInfo holds Bytes")]
fn stream_info_from(config: &VideoEncoderConfig) -> StreamInfo {
StreamInfo::Video {
id: 0,
codec: CodecKind::H264,
time_base: config.time_base,
geometry: VideoGeometry {
width: config.width,
height: config.height,
},
extra_data: Bytes::new(),
}
}
#[cfg(test)]
#[path = "video_tests.rs"]
mod tests;