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, HEVCEncPicFields, HEVCEncSeqFields, HevcEncPicSccFields,
HevcEncSeqSccFields, HevcEncSliceFields, MappedCodedBuffer, Picture, PictureHEVC, Surface,
UsageHint, VA_FOURCC_NV12, VA_INVALID_SURFACE, VA_PICTURE_HEVC_INVALID,
VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE, VA_RC_CQP, VA_RT_FORMAT_YUV420, VAConfigAttrib,
VAConfigAttribType, VAEntrypoint,
};
use super::codec::video_profile;
use super::hevc_gop::{DpbSlot, FrameDecision, FrameRequest, GopState};
const SURFACE_POOL_SIZE: usize = super::gop::WORKSPACE_DPB_CAP;
const FIXED_QP: u8 = 26;
const GENERAL_PROFILE_IDC_MAIN: u8 = 1;
const GENERAL_TIER_FLAG_MAIN: u8 = 0;
const GENERAL_LEVEL_IDC: u8 = 93;
const CB_MIN_LOG2_MINUS3: u8 = 0; const CB_DIFF_LOG2: u8 = 2; const TB_MIN_LOG2_MINUS2: u8 = 0; const TB_DIFF_LOG2: u8 = 3; const TRANSFORM_HIERARCHY_DEPTH: u8 = 3; const CTU_SIZE: u32 = 32;
const CTU_MAX_BITSIZE_NO_LIMIT: u8 = 0xFF;
pub(crate) struct VaapiHevcVideoEncoder {
context: Rc<Context>,
_config: Config,
info: StreamInfo,
width: u32,
height: u32,
nv12_bytes: usize,
bits_per_second: u32,
surfaces: Vec<Option<Surface<()>>>,
gop: GopState,
effective_gop_size: u32,
pending: VecDeque<Packet>,
flushed: bool,
}
impl VaapiHevcVideoEncoder {
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 supports_p_frames = super::video::probe_supports_p_frames(&display, profile);
let effective_gop_size = if config.gop_size > 1 && supports_p_frames {
config.gop_size
} else {
1
};
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 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 = super::video::nv12_size(config.width, config.height)?;
Ok(Self {
context,
_config: vaconfig,
info: stream_info_from(config),
width: config.width,
height: config.height,
nv12_bytes,
bits_per_second: config.bitrate_bps,
surfaces: surfaces.into_iter().map(Some).collect(),
gop: GopState::new(effective_gop_size),
effective_gop_size,
pending: VecDeque::new(),
flushed: false,
})
}
fn encode_one(
&self,
surface: Surface<()>,
frame: &VideoFrame,
decision: &FrameDecision,
reference: Option<(cros_libva::VASurfaceID, DpbSlot)>,
) -> (Option<Surface<()>>, Result<Packet, EncodeError>) {
let Ok(pic_width) = u16::try_from(self.width) else {
return (Some(surface), Err(EncodeError::InvalidInput));
};
let Ok(pic_height) = u16::try_from(self.height) else {
return (Some(surface), Err(EncodeError::InvalidInput));
};
let num_ctu_in_slice = ctu_count(self.width, self.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_buf = if decision.is_idr {
let seq_params = build_seq_params(
pic_width,
pic_height,
self.bits_per_second,
self.effective_gop_size,
);
match self.context.create_buffer(BufferType::EncSequenceParameter(
EncSequenceParameter::HEVC(seq_params),
)) {
Ok(buf) => Some(buf),
Err(_) => return (Some(surface), Err(EncodeError::Backend)),
}
} else {
None
};
let pic_params = build_pic_params(surface_id, coded_buf.id(), decision, reference);
let slice_params = build_slice_params(num_ctu_in_slice, decision, reference);
let Ok(pic_buf) =
self.context
.create_buffer(BufferType::EncPictureParameter(EncPictureParameter::HEVC(
pic_params,
)))
else {
return (Some(surface), Err(EncodeError::Backend));
};
let Ok(slice_buf) =
self.context
.create_buffer(BufferType::EncSliceParameter(EncSliceParameter::HEVC(
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);
if let Some(seq_buf) = seq_buf {
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: decision.is_idr,
is_discard: false,
payload: Bytes::from(bytes),
};
(surface, Ok(packet))
}
}
impl VideoEncoder for VaapiHevcVideoEncoder {
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 decision = self.gop.decide(FrameRequest::Auto);
let reference = match decision.reference {
Some((ref_slot, ref_dpb_slot)) => {
let ref_surface = self.surfaces[ref_slot]
.as_ref()
.ok_or(EncodeError::Backend)?;
Some((ref_surface.id(), ref_dpb_slot))
}
None => None,
};
let slot = decision.setup_slot;
let surface = self.surfaces[slot].take().ok_or(EncodeError::Backend)?;
let surface = match super::video::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, &decision, reference);
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 build_seq_params(
pic_width: u16,
pic_height: u16,
bits_per_second: u32,
effective_gop_size: u32,
) -> cros_libva::EncSequenceParameterBufferHEVC {
let gop_active = effective_gop_size > 1;
let (intra_period, intra_idr_period, ip_period) = if gop_active {
(effective_gop_size, 1, 1)
} else {
(1, 1, 0)
};
let seq_fields = HEVCEncSeqFields::new(
1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
1, 0, );
cros_libva::EncSequenceParameterBufferHEVC::new(
GENERAL_PROFILE_IDC_MAIN,
GENERAL_LEVEL_IDC,
GENERAL_TIER_FLAG_MAIN,
intra_period,
intra_idr_period,
ip_period,
bits_per_second,
pic_width,
pic_height,
&seq_fields,
CB_MIN_LOG2_MINUS3,
CB_DIFF_LOG2,
TB_MIN_LOG2_MINUS2,
TB_DIFF_LOG2,
TRANSFORM_HIERARCHY_DEPTH, TRANSFORM_HIERARCHY_DEPTH, 0, 0, 0, 0, None, 0, 0, 0, 0, 0, 0, 0, 0, &HevcEncSeqSccFields::new(0), )
}
fn build_pic_params(
surface_id: cros_libva::VASurfaceID,
coded_buf_id: cros_libva::VABufferID,
decision: &FrameDecision,
reference: Option<(cros_libva::VASurfaceID, DpbSlot)>,
) -> cros_libva::EncPictureParameterBufferHEVC {
let decoded_curr_pic = PictureHEVC::new(surface_id, decision.poc, 0);
let mut reference_frames: [PictureHEVC; 15] = std::array::from_fn(|_| invalid_picture_hevc());
if let Some((ref_surface_id, ref_slot)) = reference {
reference_frames[0] = reference_picture_hevc(ref_surface_id, ref_slot);
}
let coding_type: u32 = if decision.is_idr { 1 } else { 2 };
let pic_fields = HEVCEncPicFields::new(
u32::from(decision.is_idr), coding_type,
1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, );
let nal_unit_type: u8 = if decision.is_idr { 19 } else { 1 };
cros_libva::EncPictureParameterBufferHEVC::new(
decoded_curr_pic,
reference_frames,
coded_buf_id,
0, 0, FIXED_QP,
0, 0, 0, 0, 0, [0u8; 19], [0u8; 21], 0, CTU_MAX_BITSIZE_NO_LIMIT,
0, 0, 0, nal_unit_type,
&pic_fields,
0, 0, &HevcEncPicSccFields::new(0), )
}
fn build_slice_params(
num_ctu_in_slice: u32,
decision: &FrameDecision,
reference: Option<(cros_libva::VASurfaceID, DpbSlot)>,
) -> cros_libva::EncSliceParameterBufferHEVC {
let mut ref_pic_list0: [PictureHEVC; 15] = std::array::from_fn(|_| invalid_picture_hevc());
let ref_pic_list1: [PictureHEVC; 15] = std::array::from_fn(|_| invalid_picture_hevc());
if let Some((ref_surface_id, ref_slot)) = reference {
ref_pic_list0[0] = reference_picture_hevc(ref_surface_id, ref_slot);
}
let slice_type: u8 = if decision.is_idr { 2 } else { 1 };
let slice_fields = HevcEncSliceFields::new(
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, );
cros_libva::EncSliceParameterBufferHEVC::new(
0, num_ctu_in_slice,
slice_type,
0, 0, 0, ref_pic_list0,
ref_pic_list1,
0, 0, [0i8; 15],
[0i8; 15],
[[0i8; 2]; 15],
[[0i8; 2]; 15],
[0i8; 15],
[0i8; 15],
[[0i8; 2]; 15],
[[0i8; 2]; 15],
5, 0, 0, 0, 0, 0, &slice_fields,
0, 0, )
}
fn reference_picture_hevc(surface_id: cros_libva::VASurfaceID, slot: DpbSlot) -> PictureHEVC {
PictureHEVC::new(surface_id, slot.poc, VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE)
}
fn invalid_picture_hevc() -> PictureHEVC {
PictureHEVC::new(VA_INVALID_SURFACE, 0, VA_PICTURE_HEVC_INVALID)
}
const fn ctu_count(width: u32, height: u32) -> u32 {
width.div_ceil(CTU_SIZE) * height.div_ceil(CTU_SIZE)
}
fn validate(config: &VideoEncoderConfig) -> Result<(), EncodeError> {
if config.codec != CodecKind::Hevc {
return Err(EncodeError::Unsupported);
}
if config.width == 0 || config.height == 0 {
return Err(EncodeError::InvalidInput);
}
if config.width > u32::from(u16::MAX) || config.height > u32::from(u16::MAX) {
return Err(EncodeError::Unsupported);
}
if !config.width.is_multiple_of(8) || !config.height.is_multiple_of(8) {
return Err(EncodeError::Unsupported);
}
if config.pixel_format != PixelFormat::Nv12 {
return Err(EncodeError::Unsupported);
}
if config.time_base.den == 0 {
return Err(EncodeError::InvalidInput);
}
if config.gop_size == 0 {
return Err(EncodeError::InvalidInput);
}
Ok(())
}
#[allow(clippy::missing_const_for_fn, reason = "StreamInfo holds Bytes")]
fn stream_info_from(config: &VideoEncoderConfig) -> StreamInfo {
StreamInfo::Video {
id: 0,
codec: CodecKind::Hevc,
time_base: config.time_base,
geometry: VideoGeometry {
width: config.width,
height: config.height,
},
extra_data: Bytes::new(),
}
}
#[cfg(test)]
#[path = "hevc_tests.rs"]
mod tests;