use std::collections::VecDeque;
use std::time::Duration;
use crate::{EncodeError, VideoEncoder, VideoEncoderConfig, VideoInputPreference};
use mediaway_common::{
Bytes, CodecKind, Packet, PixelFormat, StreamInfo, VideoFrame, VideoFrameStorage, VideoGeometry,
};
use ndk::media::media_codec::{
DequeuedInputBufferResult, DequeuedOutputBufferInfoResult, MediaCodec, MediaCodecDirection,
};
use ndk::media::media_format::MediaFormat;
use super::codec::mime_type;
const COLOR_FORMAT_YUV420_SEMI_PLANAR: i32 = 21;
const BUFFER_FLAG_END_OF_STREAM: u32 = 4;
const BUFFER_FLAG_CODEC_CONFIG: u32 = 2;
const BUFFER_FLAG_KEY_FRAME: u32 = 1;
const INPUT_DEQUEUE_TIMEOUT: Duration = Duration::from_millis(20);
const MAX_INPUT_RETRIES: usize = 50;
const OUTPUT_DEQUEUE_TIMEOUT: Duration = Duration::from_millis(5);
const MAX_OPPORTUNISTIC_OUTPUT_DRAIN: usize = 4;
const MAX_FLUSH_DRAIN_RETRIES: usize = 400;
pub(crate) struct AmediaCodecVideoEncoder {
codec: MediaCodec,
info: StreamInfo,
width: u32,
height: u32,
yuv420_bytes: usize,
pending: VecDeque<Packet>,
flushed: bool,
}
impl AmediaCodecVideoEncoder {
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 mime = mime_type(config.codec)?;
let codec = MediaCodec::from_encoder_type(mime).ok_or(EncodeError::Backend)?;
let mut format = MediaFormat::new();
format.set_str("mime", mime);
format.set_i32(
"width",
i32::try_from(config.width).map_err(|_| EncodeError::InvalidInput)?,
);
format.set_i32(
"height",
i32::try_from(config.height).map_err(|_| EncodeError::InvalidInput)?,
);
format.set_i32("color-format", COLOR_FORMAT_YUV420_SEMI_PLANAR);
format.set_i32(
"bitrate",
i32::try_from(config.bitrate_bps).unwrap_or(i32::MAX),
);
let frame_rate = frame_rate_hint(config.time_base);
format.set_i32("frame-rate", frame_rate);
format.set_f32(
"i-frame-interval",
i_frame_interval_secs(config.gop_size, frame_rate),
);
codec
.configure(&format, None, MediaCodecDirection::Encoder)
.map_err(|_| EncodeError::Backend)?;
codec.start().map_err(|_| EncodeError::Backend)?;
let yuv420_bytes = yuv420_size(config.width, config.height)?;
Ok(Self {
codec,
info: stream_info_from(config),
width: config.width,
height: config.height,
yuv420_bytes,
pending: VecDeque::new(),
flushed: false,
})
}
fn upload_and_queue(&self, data: &[u8], time_us: u64, flags: u32) -> Result<(), EncodeError> {
for _ in 0..MAX_INPUT_RETRIES {
match self
.codec
.dequeue_input_buffer(INPUT_DEQUEUE_TIMEOUT)
.map_err(|_| EncodeError::Backend)?
{
DequeuedInputBufferResult::Buffer(mut input_buffer) => {
let dst = input_buffer.buffer_mut();
let n = data.len().min(dst.len());
for (d, &s) in dst[..n].iter_mut().zip(data[..n].iter()) {
d.write(s);
}
self.codec
.queue_input_buffer(input_buffer, 0, n, time_us, flags)
.map_err(|_| EncodeError::Backend)?;
return Ok(());
}
DequeuedInputBufferResult::TryAgainLater => {}
}
}
Err(EncodeError::Backend)
}
fn drain_output(&mut self, max_attempts: usize) -> Result<(), EncodeError> {
for _ in 0..max_attempts {
match self
.codec
.dequeue_output_buffer(OUTPUT_DEQUEUE_TIMEOUT)
.map_err(|_| EncodeError::Backend)?
{
DequeuedOutputBufferInfoResult::Buffer(output_buffer) => {
let info = *output_buffer.info();
let flags = info.flags();
if flags & BUFFER_FLAG_CODEC_CONFIG != 0 {
let start = usize::try_from(info.offset()).unwrap_or(0);
let len = usize::try_from(info.size()).unwrap_or(0);
let full = output_buffer.buffer();
let end = start.saturating_add(len).min(full.len());
if let Some(avcc) =
extract_avcc_extra_data(full.get(start..end).unwrap_or(&[]))
&& let StreamInfo::Video { extra_data, .. } = &mut self.info
{
*extra_data = avcc;
}
self.codec
.release_output_buffer(output_buffer, false)
.map_err(|_| EncodeError::Backend)?;
continue;
}
let start = usize::try_from(info.offset()).unwrap_or(0);
let len = usize::try_from(info.size()).unwrap_or(0);
let full = output_buffer.buffer();
let end = start.saturating_add(len).min(full.len());
let payload = full.get(start..end).unwrap_or(&[]).to_vec();
let pts = info.presentation_time_us();
let is_eos = flags & BUFFER_FLAG_END_OF_STREAM != 0;
self.codec
.release_output_buffer(output_buffer, false)
.map_err(|_| EncodeError::Backend)?;
if !payload.is_empty() {
self.pending.push_back(Packet {
stream_id: 0,
pts,
dts: pts,
duration: 0,
is_keyframe: flags & BUFFER_FLAG_KEY_FRAME != 0,
is_discard: false,
payload: Bytes::from(payload),
});
}
if is_eos {
return Ok(());
}
}
DequeuedOutputBufferInfoResult::TryAgainLater => return Ok(()),
DequeuedOutputBufferInfoResult::OutputFormatChanged
| DequeuedOutputBufferInfoResult::OutputBuffersChanged => {}
}
}
Ok(())
}
}
impl VideoEncoder for AmediaCodecVideoEncoder {
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.yuv420_bytes {
return Err(EncodeError::InvalidInput);
}
let time_us = u64::try_from(frame.pts).unwrap_or(0);
self.upload_and_queue(&data[..self.yuv420_bytes], time_us, 0)?;
self.drain_output(MAX_OPPORTUNISTIC_OUTPUT_DRAIN)
}
fn poll_packet(&mut self) -> Result<Option<Packet>, EncodeError> {
if self.pending.is_empty() {
self.drain_output(1)?;
}
Ok(self.pending.pop_front())
}
fn flush(&mut self) -> Result<(), EncodeError> {
if self.flushed {
return Ok(());
}
self.upload_and_queue(&[], 0, BUFFER_FLAG_END_OF_STREAM)?;
self.drain_output(MAX_FLUSH_DRAIN_RETRIES)?;
self.flushed = true;
Ok(())
}
}
#[allow(
clippy::cast_precision_loss,
reason = "gop_size/frame_rate are small encoder-config integers, always exact in f32"
)]
fn i_frame_interval_secs(gop_size: u32, frame_rate: i32) -> f32 {
if gop_size <= 1 {
0.0
} else {
gop_size as f32 / frame_rate.max(1) as f32
}
}
fn frame_rate_hint(time_base: mediaway_common::Rational) -> i32 {
if time_base.num == 0 {
return 30;
}
i32::try_from(u64::from(time_base.den) / time_base.num)
.unwrap_or(30)
.max(1)
}
fn extract_avcc_extra_data(annex_b: &[u8]) -> Option<Bytes> {
iso_bmff::bitstream::avc::to_avcc(annex_b).avcc
}
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.pixel_format != PixelFormat::Nv12 {
return Err(EncodeError::Unsupported);
}
if config.time_base.den == 0 {
return Err(EncodeError::InvalidInput);
}
Ok(())
}
fn yuv420_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;