- Replace buffer.index() with buffer.buffer_mut()/buffer.buffer() (ndk 0.9 RAII API) - Replace queue_input_buffer_by_index/release_output_buffer_by_index with queue_input_buffer/release_output_buffer taking buffer objects - Fix MaybeUninit<u8> copy using .write() instead of copy_from_slice - Add BITRATE_MODE_CBR and AMEDIACODEC_BUFFER_FLAG_KEY_FRAME local constants (removes ndk_sys dependency for these values) - Add unsafe impl Send for all six MediaCodec wrapper structs - Pin @tauri-apps/api to ^2.11 to match Cargo.lock tauri 2.11.1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1293 lines
45 KiB
Rust
1293 lines
45 KiB
Rust
//! Android MediaCodec H.264 / H.265 encoder / decoder (Android only).
|
|
//!
|
|
//! On Android targets this uses the `ndk` crate's safe bindings around
|
|
//! `AMediaCodec`. On non-Android targets all methods return
|
|
//! [`VideoError::NotInitialized`].
|
|
|
|
use crate::decoder::VideoDecoder;
|
|
use crate::encoder::{VideoEncoder, VideoError, VideoFrame};
|
|
|
|
#[cfg(target_os = "android")]
|
|
mod imp {
|
|
pub use ndk::media::media_codec::{MediaCodec, MediaCodecDirection};
|
|
pub use ndk::media::media_format::MediaFormat;
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
use imp::*;
|
|
|
|
/// Android MediaCodec H.264 encoder.
|
|
///
|
|
/// Full implementation requires an Android build environment (NDK).
|
|
/// On non-Android targets this is a compile-safe placeholder.
|
|
pub struct MediaCodecEncoder {
|
|
#[cfg(target_os = "android")]
|
|
codec: MediaCodec,
|
|
#[cfg(target_os = "android")]
|
|
width: u32,
|
|
#[cfg(target_os = "android")]
|
|
height: u32,
|
|
force_keyframe: bool,
|
|
#[cfg(not(target_os = "android"))]
|
|
_width: u32,
|
|
#[cfg(not(target_os = "android"))]
|
|
_height: u32,
|
|
#[cfg(not(target_os = "android"))]
|
|
_bitrate_bps: u32,
|
|
}
|
|
|
|
/// Android color format constant: YUV 4:2:0 planar (I420).
|
|
#[cfg(target_os = "android")]
|
|
const COLOR_FORMAT_YUV420_PLANAR: i32 = 19;
|
|
/// Android MediaCodec CBR bitrate mode (MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR).
|
|
#[cfg(target_os = "android")]
|
|
const BITRATE_MODE_CBR: i32 = 2;
|
|
/// AMediaCodec keyframe buffer flag.
|
|
#[cfg(target_os = "android")]
|
|
const AMEDIACODEC_BUFFER_FLAG_KEY_FRAME: u32 = 1;
|
|
|
|
// AMediaCodec is thread-safe; the NonNull inside MediaCodec suppresses auto-Send.
|
|
#[cfg(target_os = "android")]
|
|
unsafe impl Send for MediaCodecEncoder {}
|
|
|
|
impl MediaCodecEncoder {
|
|
/// Create a new encoder.
|
|
pub fn new(width: u32, height: u32, bitrate_bps: u32) -> Result<Self, VideoError> {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
let mut format = MediaFormat::new();
|
|
format.set_str("mime", "video/avc");
|
|
format.set_i32("width", width as i32);
|
|
format.set_i32("height", height as i32);
|
|
format.set_i32("bitrate", bitrate_bps as i32);
|
|
format.set_i32("frame-rate", 30);
|
|
format.set_i32("i-frame-interval", 1);
|
|
format.set_i32("color-format", COLOR_FORMAT_YUV420_PLANAR);
|
|
|
|
let codec = MediaCodec::from_encoder_type("video/avc").ok_or_else(|| {
|
|
VideoError::PlatformError("AMediaCodec_createEncoderByType failed".into())
|
|
})?;
|
|
|
|
codec
|
|
.configure(&format, None, MediaCodecDirection::Encoder)
|
|
.map_err(|e| VideoError::PlatformError(format!("configure failed: {e}")))?;
|
|
|
|
codec
|
|
.start()
|
|
.map_err(|e| VideoError::PlatformError(format!("start failed: {e}")))?;
|
|
|
|
Ok(Self {
|
|
codec,
|
|
width,
|
|
height,
|
|
force_keyframe: false,
|
|
})
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let _ = (width, height, bitrate_bps);
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl VideoEncoder for MediaCodecEncoder {
|
|
fn encode(&mut self, frame: &VideoFrame) -> Result<Vec<u8>, VideoError> {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
let y_size = (self.width * self.height) as usize;
|
|
let uv_size = y_size / 4;
|
|
let expected = y_size + uv_size * 2;
|
|
if frame.data.len() < expected {
|
|
return Err(VideoError::InvalidInput(format!(
|
|
"I420 frame too small: {} bytes, expected {expected}",
|
|
frame.data.len()
|
|
)));
|
|
}
|
|
|
|
// Drain any pending output before feeding new input.
|
|
let mut annex_b = self.drain_output()?;
|
|
|
|
// Feed the new frame.
|
|
match self
|
|
.codec
|
|
.dequeue_input_buffer(std::time::Duration::from_millis(10))
|
|
{
|
|
Ok(ndk::media::media_codec::DequeuedInputBufferResult::Buffer(mut buffer)) => {
|
|
let flags = if self.force_keyframe {
|
|
AMEDIACODEC_BUFFER_FLAG_KEY_FRAME
|
|
} else {
|
|
0
|
|
};
|
|
let to_copy = {
|
|
let buf = buffer.buffer_mut();
|
|
let n = frame.data.len().min(buf.len());
|
|
for (d, &s) in buf[..n].iter_mut().zip(frame.data[..n].iter()) {
|
|
d.write(s);
|
|
}
|
|
n
|
|
};
|
|
self.codec
|
|
.queue_input_buffer(buffer, 0, to_copy, frame.timestamp_ms as u64 * 1000, flags)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!("queue_input_buffer failed: {e}"))
|
|
})?;
|
|
}
|
|
Ok(ndk::media::media_codec::DequeuedInputBufferResult::TryAgainLater) => {}
|
|
Err(e) => {
|
|
return Err(VideoError::PlatformError(format!(
|
|
"dequeue_input_buffer failed: {e}"
|
|
)));
|
|
}
|
|
}
|
|
|
|
// Drain output again to collect the encoded frame.
|
|
annex_b.extend_from_slice(&self.drain_output()?);
|
|
Ok(annex_b)
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let _ = frame;
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
|
|
fn request_keyframe(&mut self) {
|
|
self.force_keyframe = true;
|
|
}
|
|
|
|
fn is_keyframe(&self, packet: &[u8]) -> bool {
|
|
if packet.is_empty() {
|
|
return false;
|
|
}
|
|
let nal_type = packet[0] & 0x1F;
|
|
nal_type == 5
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
impl MediaCodecEncoder {
|
|
/// Drain all available output buffers and convert from AVCC to Annex-B.
|
|
fn drain_output(&mut self) -> Result<Vec<u8>, VideoError> {
|
|
let mut output = Vec::new();
|
|
loop {
|
|
match self
|
|
.codec
|
|
.dequeue_output_buffer(std::time::Duration::from_millis(0))
|
|
{
|
|
Ok(ndk::media::media_codec::DequeuedOutputBufferInfoResult::Buffer(buffer)) => {
|
|
let is_keyframe =
|
|
(buffer.info().flags() & AMEDIACODEC_BUFFER_FLAG_KEY_FRAME) != 0;
|
|
if is_keyframe {
|
|
self.force_keyframe = false;
|
|
}
|
|
let data = buffer.buffer().to_vec();
|
|
output.extend_from_slice(&avcc_to_annexb(&data));
|
|
self.codec
|
|
.release_output_buffer(buffer, false)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!("release_output_buffer failed: {e}"))
|
|
})?;
|
|
}
|
|
Ok(
|
|
ndk::media::media_codec::DequeuedOutputBufferInfoResult::OutputFormatChanged,
|
|
) => continue,
|
|
Ok(
|
|
ndk::media::media_codec::DequeuedOutputBufferInfoResult::OutputBuffersChanged,
|
|
) => continue,
|
|
Ok(ndk::media::media_codec::DequeuedOutputBufferInfoResult::TryAgainLater) => break,
|
|
Err(e) => {
|
|
return Err(VideoError::PlatformError(format!(
|
|
"dequeue_output_buffer failed: {e}"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
Ok(output)
|
|
}
|
|
}
|
|
|
|
/// Android MediaCodec H.264 decoder.
|
|
///
|
|
/// Full implementation requires an Android build environment (NDK).
|
|
/// On non-Android targets this is a compile-safe placeholder.
|
|
pub struct MediaCodecDecoder {
|
|
#[cfg(target_os = "android")]
|
|
codec: Option<MediaCodec>,
|
|
#[cfg(target_os = "android")]
|
|
width: u32,
|
|
#[cfg(target_os = "android")]
|
|
height: u32,
|
|
#[cfg(not(target_os = "android"))]
|
|
_width: u32,
|
|
#[cfg(not(target_os = "android"))]
|
|
_height: u32,
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
unsafe impl Send for MediaCodecDecoder {}
|
|
|
|
impl MediaCodecDecoder {
|
|
/// Create a new decoder.
|
|
pub fn new(width: u32, height: u32) -> Result<Self, VideoError> {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
Ok(Self {
|
|
codec: None,
|
|
width,
|
|
height,
|
|
})
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let _ = (width, height);
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl VideoDecoder for MediaCodecDecoder {
|
|
fn decode(&mut self, access_unit: &[u8]) -> Result<Option<VideoFrame>, VideoError> {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
if access_unit.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
|
|
// Lazily create the decoder when we see the first SPS/PPS.
|
|
if self.codec.is_none() {
|
|
let (sps, pps) = extract_sps_pps(access_unit);
|
|
let (sps, pps) = match (sps, pps) {
|
|
(Some(s), Some(p)) => (s, p),
|
|
_ => return Ok(None), // need parameter sets before we can init decoder
|
|
};
|
|
|
|
let mut format = MediaFormat::new();
|
|
format.set_str("mime", "video/avc");
|
|
format.set_i32("width", self.width as i32);
|
|
format.set_i32("height", self.height as i32);
|
|
format.set_buffer("csd-0", &sps);
|
|
format.set_buffer("csd-1", &pps);
|
|
|
|
let codec = MediaCodec::from_decoder_type("video/avc").ok_or_else(|| {
|
|
VideoError::PlatformError("AMediaCodec_createDecoderByType failed".into())
|
|
})?;
|
|
|
|
codec
|
|
.configure(&format, None, MediaCodecDirection::Decoder)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!("decoder configure failed: {e}"))
|
|
})?;
|
|
|
|
codec
|
|
.start()
|
|
.map_err(|e| VideoError::PlatformError(format!("decoder start failed: {e}")))?;
|
|
|
|
self.codec = Some(codec);
|
|
}
|
|
|
|
let codec = self.codec.as_mut().ok_or(VideoError::NotInitialized)?;
|
|
|
|
// Feed input.
|
|
match codec.dequeue_input_buffer(std::time::Duration::from_millis(10)) {
|
|
Ok(ndk::media::media_codec::DequeuedInputBufferResult::Buffer(mut buffer)) => {
|
|
let to_copy = {
|
|
let buf = buffer.buffer_mut();
|
|
let n = access_unit.len().min(buf.len());
|
|
for (d, &s) in buf[..n].iter_mut().zip(access_unit[..n].iter()) {
|
|
d.write(s);
|
|
}
|
|
n
|
|
};
|
|
codec
|
|
.queue_input_buffer(buffer, 0, to_copy, 0, 0)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!(
|
|
"decoder queue_input_buffer failed: {e}"
|
|
))
|
|
})?;
|
|
}
|
|
Ok(ndk::media::media_codec::DequeuedInputBufferResult::TryAgainLater) => {}
|
|
Err(e) => {
|
|
return Err(VideoError::PlatformError(format!(
|
|
"decoder dequeue_input_buffer failed: {e}"
|
|
)));
|
|
}
|
|
}
|
|
|
|
// Drain output.
|
|
match codec.dequeue_output_buffer(std::time::Duration::from_millis(10)) {
|
|
Ok(ndk::media::media_codec::DequeuedOutputBufferInfoResult::Buffer(buffer)) => {
|
|
let data = buffer.buffer().to_vec();
|
|
codec
|
|
.release_output_buffer(buffer, false)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!(
|
|
"decoder release_output_buffer failed: {e}"
|
|
))
|
|
})?;
|
|
Ok(Some(VideoFrame {
|
|
width: self.width,
|
|
height: self.height,
|
|
data,
|
|
timestamp_ms: 0,
|
|
}))
|
|
}
|
|
Ok(_) => Ok(None),
|
|
Err(e) => Err(VideoError::PlatformError(format!(
|
|
"decoder dequeue_output_buffer failed: {e}"
|
|
))),
|
|
}
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let _ = access_unit;
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// H.265 / HEVC
|
|
// ============================================================================
|
|
|
|
/// Android MediaCodec H.265 encoder.
|
|
///
|
|
/// On non-Android targets this is a compile-safe placeholder.
|
|
pub struct MediaCodecHevcEncoder {
|
|
#[cfg(target_os = "android")]
|
|
codec: MediaCodec,
|
|
#[cfg(target_os = "android")]
|
|
width: u32,
|
|
#[cfg(target_os = "android")]
|
|
height: u32,
|
|
force_keyframe: bool,
|
|
#[cfg(not(target_os = "android"))]
|
|
_width: u32,
|
|
#[cfg(not(target_os = "android"))]
|
|
_height: u32,
|
|
#[cfg(not(target_os = "android"))]
|
|
_bitrate_bps: u32,
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
unsafe impl Send for MediaCodecHevcEncoder {}
|
|
|
|
impl MediaCodecHevcEncoder {
|
|
pub fn new(width: u32, height: u32, bitrate_bps: u32) -> Result<Self, VideoError> {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
let mut format = MediaFormat::new();
|
|
format.set_str("mime", "video/hevc");
|
|
format.set_i32("width", width as i32);
|
|
format.set_i32("height", height as i32);
|
|
format.set_i32("bitrate", bitrate_bps as i32);
|
|
format.set_i32("frame-rate", 30);
|
|
format.set_i32("i-frame-interval", 1);
|
|
format.set_i32("color-format", COLOR_FORMAT_YUV420_PLANAR);
|
|
|
|
let codec = MediaCodec::from_encoder_type("video/hevc").ok_or_else(|| {
|
|
VideoError::PlatformError("AMediaCodec_createEncoderByType (HEVC) failed".into())
|
|
})?;
|
|
|
|
codec
|
|
.configure(&format, None, MediaCodecDirection::Encoder)
|
|
.map_err(|e| VideoError::PlatformError(format!("configure failed: {e}")))?;
|
|
|
|
codec
|
|
.start()
|
|
.map_err(|e| VideoError::PlatformError(format!("start failed: {e}")))?;
|
|
|
|
Ok(Self {
|
|
codec,
|
|
width,
|
|
height,
|
|
force_keyframe: false,
|
|
})
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let _ = (width, height, bitrate_bps);
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl VideoEncoder for MediaCodecHevcEncoder {
|
|
fn encode(&mut self, frame: &VideoFrame) -> Result<Vec<u8>, VideoError> {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
let y_size = (self.width * self.height) as usize;
|
|
let uv_size = y_size / 4;
|
|
let expected = y_size + uv_size * 2;
|
|
if frame.data.len() < expected {
|
|
return Err(VideoError::InvalidInput(format!(
|
|
"I420 frame too small: {} bytes, expected {expected}",
|
|
frame.data.len()
|
|
)));
|
|
}
|
|
|
|
let mut annex_b = self.drain_output()?;
|
|
|
|
match self
|
|
.codec
|
|
.dequeue_input_buffer(std::time::Duration::from_millis(10))
|
|
{
|
|
Ok(ndk::media::media_codec::DequeuedInputBufferResult::Buffer(mut buffer)) => {
|
|
let flags = if self.force_keyframe { AMEDIACODEC_BUFFER_FLAG_KEY_FRAME } else { 0 };
|
|
let to_copy = {
|
|
let buf = buffer.buffer_mut();
|
|
let n = frame.data.len().min(buf.len());
|
|
for (d, &s) in buf[..n].iter_mut().zip(frame.data[..n].iter()) {
|
|
d.write(s);
|
|
}
|
|
n
|
|
};
|
|
self.codec
|
|
.queue_input_buffer(buffer, 0, to_copy, frame.timestamp_ms as u64 * 1000, flags)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!("queue_input_buffer failed: {e}"))
|
|
})?;
|
|
}
|
|
Ok(ndk::media::media_codec::DequeuedInputBufferResult::TryAgainLater) => {}
|
|
Err(e) => {
|
|
return Err(VideoError::PlatformError(format!(
|
|
"dequeue_input_buffer failed: {e}"
|
|
)));
|
|
}
|
|
}
|
|
|
|
annex_b.extend_from_slice(&self.drain_output()?);
|
|
Ok(annex_b)
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let _ = frame;
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
|
|
fn request_keyframe(&mut self) {
|
|
self.force_keyframe = true;
|
|
}
|
|
|
|
fn is_keyframe(&self, packet: &[u8]) -> bool {
|
|
if packet.len() < 2 {
|
|
return false;
|
|
}
|
|
let nal_type = (packet[0] >> 1) & 0x3F;
|
|
nal_type == 19 || nal_type == 20
|
|
}
|
|
}
|
|
|
|
/// Android MediaCodec AV1 encoder.
|
|
///
|
|
/// On non-Android targets this is a compile-safe placeholder.
|
|
pub struct MediaCodecAv1Encoder {
|
|
#[cfg(target_os = "android")]
|
|
codec: MediaCodec,
|
|
#[cfg(target_os = "android")]
|
|
width: u32,
|
|
#[cfg(target_os = "android")]
|
|
height: u32,
|
|
force_keyframe: bool,
|
|
#[cfg(not(target_os = "android"))]
|
|
_width: u32,
|
|
#[cfg(not(target_os = "android"))]
|
|
_height: u32,
|
|
#[cfg(not(target_os = "android"))]
|
|
_bitrate_bps: u32,
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
unsafe impl Send for MediaCodecAv1Encoder {}
|
|
|
|
impl MediaCodecAv1Encoder {
|
|
pub fn new(width: u32, height: u32, bitrate_bps: u32) -> Result<Self, VideoError> {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
let mut format = MediaFormat::new();
|
|
format.set_str("mime", "video/av01");
|
|
format.set_i32("width", width as i32);
|
|
format.set_i32("height", height as i32);
|
|
format.set_i32("bitrate", bitrate_bps as i32);
|
|
format.set_i32("frame-rate", 30);
|
|
format.set_i32("color-format", COLOR_FORMAT_YUV420_PLANAR);
|
|
format.set_i32("bitrate-mode", BITRATE_MODE_CBR);
|
|
format.set_i32("i-frame-interval", 2);
|
|
|
|
let codec = MediaCodec::from_encoder_type("video/av01").ok_or_else(|| {
|
|
VideoError::PlatformError("AMediaCodec_createEncoderByType (AV1) failed".into())
|
|
})?;
|
|
|
|
codec
|
|
.configure(&format, None, MediaCodecDirection::Encoder)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!("AV1 encoder configure failed: {e}"))
|
|
})?;
|
|
|
|
codec
|
|
.start()
|
|
.map_err(|e| VideoError::PlatformError(format!("AV1 encoder start failed: {e}")))?;
|
|
|
|
Ok(Self {
|
|
codec,
|
|
width,
|
|
height,
|
|
force_keyframe: false,
|
|
})
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let _ = (width, height, bitrate_bps);
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl VideoEncoder for MediaCodecAv1Encoder {
|
|
fn encode(&mut self, frame: &VideoFrame) -> Result<Vec<u8>, VideoError> {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
let mut output = Vec::new();
|
|
|
|
match self
|
|
.codec
|
|
.dequeue_input_buffer(std::time::Duration::from_millis(0))
|
|
{
|
|
Ok(ndk::media::media_codec::DequeuedInputBufferResult::Buffer(mut buffer)) => {
|
|
let flags = if self.force_keyframe { AMEDIACODEC_BUFFER_FLAG_KEY_FRAME } else { 0 };
|
|
let to_copy = {
|
|
let buf = buffer.buffer_mut();
|
|
let n = frame.data.len().min(buf.len());
|
|
for (d, &s) in buf[..n].iter_mut().zip(frame.data[..n].iter()) {
|
|
d.write(s);
|
|
}
|
|
n
|
|
};
|
|
self.codec
|
|
.queue_input_buffer(buffer, 0, to_copy, frame.timestamp_ms as u64 * 1000, flags)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!(
|
|
"AV1 encoder queue_input_buffer failed: {e}"
|
|
))
|
|
})?;
|
|
}
|
|
Ok(ndk::media::media_codec::DequeuedInputBufferResult::TryAgainLater) => {}
|
|
Err(e) => {
|
|
return Err(VideoError::PlatformError(format!(
|
|
"AV1 encoder dequeue_input_buffer failed: {e}"
|
|
)));
|
|
}
|
|
}
|
|
|
|
output.extend_from_slice(&self.drain_output()?);
|
|
Ok(output)
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let _ = frame;
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
|
|
fn request_keyframe(&mut self) {
|
|
self.force_keyframe = true;
|
|
}
|
|
|
|
fn is_keyframe(&self, packet: &[u8]) -> bool {
|
|
crate::av1_obu::is_keyframe_obu(packet)
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
impl MediaCodecHevcEncoder {
|
|
fn drain_output(&mut self) -> Result<Vec<u8>, VideoError> {
|
|
let mut output = Vec::new();
|
|
loop {
|
|
match self
|
|
.codec
|
|
.dequeue_output_buffer(std::time::Duration::from_millis(0))
|
|
{
|
|
Ok(ndk::media::media_codec::DequeuedOutputBufferInfoResult::Buffer(buffer)) => {
|
|
let is_keyframe =
|
|
(buffer.info().flags() & AMEDIACODEC_BUFFER_FLAG_KEY_FRAME) != 0;
|
|
if is_keyframe {
|
|
self.force_keyframe = false;
|
|
}
|
|
let data = buffer.buffer().to_vec();
|
|
output.extend_from_slice(&avcc_to_annexb(&data));
|
|
self.codec
|
|
.release_output_buffer(buffer, false)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!("release_output_buffer failed: {e}"))
|
|
})?;
|
|
}
|
|
Ok(
|
|
ndk::media::media_codec::DequeuedOutputBufferInfoResult::OutputFormatChanged,
|
|
) => continue,
|
|
Ok(
|
|
ndk::media::media_codec::DequeuedOutputBufferInfoResult::OutputBuffersChanged,
|
|
) => continue,
|
|
Ok(ndk::media::media_codec::DequeuedOutputBufferInfoResult::TryAgainLater) => break,
|
|
Err(e) => {
|
|
return Err(VideoError::PlatformError(format!(
|
|
"dequeue_output_buffer failed: {e}"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
Ok(output)
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
impl MediaCodecAv1Encoder {
|
|
fn drain_output(&mut self) -> Result<Vec<u8>, VideoError> {
|
|
let mut output = Vec::new();
|
|
loop {
|
|
match self
|
|
.codec
|
|
.dequeue_output_buffer(std::time::Duration::from_millis(0))
|
|
{
|
|
Ok(ndk::media::media_codec::DequeuedOutputBufferInfoResult::Buffer(buffer)) => {
|
|
let is_keyframe =
|
|
(buffer.info().flags() & AMEDIACODEC_BUFFER_FLAG_KEY_FRAME) != 0;
|
|
if is_keyframe {
|
|
self.force_keyframe = false;
|
|
}
|
|
// AV1 output from MediaCodec is already in OBU format.
|
|
let data = buffer.buffer().to_vec();
|
|
output.extend_from_slice(&data);
|
|
self.codec
|
|
.release_output_buffer(buffer, false)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!(
|
|
"AV1 encoder release_output_buffer failed: {e}"
|
|
))
|
|
})?;
|
|
}
|
|
Ok(
|
|
ndk::media::media_codec::DequeuedOutputBufferInfoResult::OutputFormatChanged,
|
|
) => continue,
|
|
Ok(
|
|
ndk::media::media_codec::DequeuedOutputBufferInfoResult::OutputBuffersChanged,
|
|
) => continue,
|
|
Ok(ndk::media::media_codec::DequeuedOutputBufferInfoResult::TryAgainLater) => break,
|
|
Err(e) => {
|
|
return Err(VideoError::PlatformError(format!(
|
|
"AV1 encoder dequeue_output_buffer failed: {e}"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
Ok(output)
|
|
}
|
|
}
|
|
|
|
/// Android MediaCodec H.265 decoder.
|
|
///
|
|
/// On non-Android targets this is a compile-safe placeholder.
|
|
pub struct MediaCodecHevcDecoder {
|
|
#[cfg(target_os = "android")]
|
|
codec: Option<MediaCodec>,
|
|
#[cfg(target_os = "android")]
|
|
width: u32,
|
|
#[cfg(target_os = "android")]
|
|
height: u32,
|
|
#[cfg(not(target_os = "android"))]
|
|
_width: u32,
|
|
#[cfg(not(target_os = "android"))]
|
|
_height: u32,
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
unsafe impl Send for MediaCodecHevcDecoder {}
|
|
|
|
impl MediaCodecHevcDecoder {
|
|
pub fn new(width: u32, height: u32) -> Result<Self, VideoError> {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
Ok(Self {
|
|
codec: None,
|
|
width,
|
|
height,
|
|
})
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let _ = (width, height);
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl VideoDecoder for MediaCodecHevcDecoder {
|
|
fn decode(&mut self, access_unit: &[u8]) -> Result<Option<VideoFrame>, VideoError> {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
if access_unit.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
|
|
// Lazily create decoder when we see VPS/SPS/PPS.
|
|
if self.codec.is_none() {
|
|
let (vps, sps, pps) = extract_vps_sps_pps(access_unit);
|
|
let (vps, sps, pps) = match (vps, sps, pps) {
|
|
(Some(v), Some(s), Some(p)) => (v, s, p),
|
|
_ => return Ok(None),
|
|
};
|
|
|
|
let mut format = MediaFormat::new();
|
|
format.set_str("mime", "video/hevc");
|
|
format.set_i32("width", self.width as i32);
|
|
format.set_i32("height", self.height as i32);
|
|
format.set_buffer("csd-0", &vps);
|
|
format.set_buffer("csd-1", &sps);
|
|
format.set_buffer("csd-2", &pps);
|
|
|
|
let codec = MediaCodec::from_decoder_type("video/hevc").ok_or_else(|| {
|
|
VideoError::PlatformError(
|
|
"AMediaCodec_createDecoderByType (HEVC) failed".into(),
|
|
)
|
|
})?;
|
|
|
|
codec
|
|
.configure(&format, None, MediaCodecDirection::Decoder)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!("decoder configure failed: {e}"))
|
|
})?;
|
|
|
|
codec
|
|
.start()
|
|
.map_err(|e| VideoError::PlatformError(format!("decoder start failed: {e}")))?;
|
|
|
|
self.codec = Some(codec);
|
|
}
|
|
|
|
let codec = self.codec.as_mut().ok_or(VideoError::NotInitialized)?;
|
|
|
|
match codec.dequeue_input_buffer(std::time::Duration::from_millis(10)) {
|
|
Ok(ndk::media::media_codec::DequeuedInputBufferResult::Buffer(mut buffer)) => {
|
|
let to_copy = {
|
|
let buf = buffer.buffer_mut();
|
|
let n = access_unit.len().min(buf.len());
|
|
for (d, &s) in buf[..n].iter_mut().zip(access_unit[..n].iter()) {
|
|
d.write(s);
|
|
}
|
|
n
|
|
};
|
|
codec
|
|
.queue_input_buffer(buffer, 0, to_copy, 0, 0)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!(
|
|
"decoder queue_input_buffer failed: {e}"
|
|
))
|
|
})?;
|
|
}
|
|
Ok(ndk::media::media_codec::DequeuedInputBufferResult::TryAgainLater) => {}
|
|
Err(e) => {
|
|
return Err(VideoError::PlatformError(format!(
|
|
"decoder dequeue_input_buffer failed: {e}"
|
|
)));
|
|
}
|
|
}
|
|
|
|
match codec.dequeue_output_buffer(std::time::Duration::from_millis(10)) {
|
|
Ok(ndk::media::media_codec::DequeuedOutputBufferInfoResult::Buffer(buffer)) => {
|
|
let data = buffer.buffer().to_vec();
|
|
codec
|
|
.release_output_buffer(buffer, false)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!(
|
|
"decoder release_output_buffer failed: {e}"
|
|
))
|
|
})?;
|
|
Ok(Some(VideoFrame {
|
|
width: self.width,
|
|
height: self.height,
|
|
data,
|
|
timestamp_ms: 0,
|
|
}))
|
|
}
|
|
Ok(_) => Ok(None),
|
|
Err(e) => Err(VideoError::PlatformError(format!(
|
|
"decoder dequeue_output_buffer failed: {e}"
|
|
))),
|
|
}
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let _ = access_unit;
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Android MediaCodec AV1 decoder.
|
|
///
|
|
/// On non-Android targets this is a compile-safe placeholder.
|
|
pub struct MediaCodecAv1Decoder {
|
|
#[cfg(target_os = "android")]
|
|
codec: Option<MediaCodec>,
|
|
#[cfg(target_os = "android")]
|
|
width: u32,
|
|
#[cfg(target_os = "android")]
|
|
height: u32,
|
|
#[cfg(not(target_os = "android"))]
|
|
_width: u32,
|
|
#[cfg(not(target_os = "android"))]
|
|
_height: u32,
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
unsafe impl Send for MediaCodecAv1Decoder {}
|
|
|
|
impl MediaCodecAv1Decoder {
|
|
pub fn new(width: u32, height: u32) -> Result<Self, VideoError> {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
Ok(Self {
|
|
codec: None,
|
|
width,
|
|
height,
|
|
})
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let _ = (width, height);
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl VideoDecoder for MediaCodecAv1Decoder {
|
|
fn decode(&mut self, access_unit: &[u8]) -> Result<Option<VideoFrame>, VideoError> {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
if access_unit.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
|
|
// Lazily create decoder when we see a sequence header OBU.
|
|
if self.codec.is_none() {
|
|
let seq_header = extract_sequence_header_obu(access_unit);
|
|
let seq_header = match seq_header {
|
|
Some(sh) => sh,
|
|
_ => return Ok(None),
|
|
};
|
|
|
|
let mut format = MediaFormat::new();
|
|
format.set_str("mime", "video/av01");
|
|
format.set_i32("width", self.width as i32);
|
|
format.set_i32("height", self.height as i32);
|
|
format.set_buffer("csd-0", &seq_header);
|
|
|
|
let codec = MediaCodec::from_decoder_type("video/av01").ok_or_else(|| {
|
|
VideoError::PlatformError("AMediaCodec_createDecoderByType (AV1) failed".into())
|
|
})?;
|
|
|
|
codec
|
|
.configure(&format, None, MediaCodecDirection::Decoder)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!("AV1 decoder configure failed: {e}"))
|
|
})?;
|
|
|
|
codec.start().map_err(|e| {
|
|
VideoError::PlatformError(format!("AV1 decoder start failed: {e}"))
|
|
})?;
|
|
|
|
self.codec = Some(codec);
|
|
}
|
|
|
|
let codec = self.codec.as_mut().ok_or(VideoError::NotInitialized)?;
|
|
|
|
match codec.dequeue_input_buffer(std::time::Duration::from_millis(10)) {
|
|
Ok(ndk::media::media_codec::DequeuedInputBufferResult::Buffer(mut buffer)) => {
|
|
let to_copy = {
|
|
let buf = buffer.buffer_mut();
|
|
let n = access_unit.len().min(buf.len());
|
|
for (d, &s) in buf[..n].iter_mut().zip(access_unit[..n].iter()) {
|
|
d.write(s);
|
|
}
|
|
n
|
|
};
|
|
codec
|
|
.queue_input_buffer(buffer, 0, to_copy, 0, 0)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!(
|
|
"AV1 decoder queue_input_buffer failed: {e}"
|
|
))
|
|
})?;
|
|
}
|
|
Ok(ndk::media::media_codec::DequeuedInputBufferResult::TryAgainLater) => {}
|
|
Err(e) => {
|
|
return Err(VideoError::PlatformError(format!(
|
|
"AV1 decoder dequeue_input_buffer failed: {e}"
|
|
)));
|
|
}
|
|
}
|
|
|
|
match codec.dequeue_output_buffer(std::time::Duration::from_millis(10)) {
|
|
Ok(ndk::media::media_codec::DequeuedOutputBufferInfoResult::Buffer(buffer)) => {
|
|
let data = buffer.buffer().to_vec();
|
|
codec
|
|
.release_output_buffer(buffer, false)
|
|
.map_err(|e| {
|
|
VideoError::PlatformError(format!(
|
|
"AV1 decoder release_output_buffer failed: {e}"
|
|
))
|
|
})?;
|
|
Ok(Some(VideoFrame {
|
|
width: self.width,
|
|
height: self.height,
|
|
data,
|
|
timestamp_ms: 0,
|
|
}))
|
|
}
|
|
Ok(_) => Ok(None),
|
|
Err(e) => Err(VideoError::PlatformError(format!(
|
|
"AV1 decoder dequeue_output_buffer failed: {e}"
|
|
))),
|
|
}
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let _ = access_unit;
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Type alias for HEVC parameter-set triple returned by `extract_vps_sps_pps`.
|
|
type HevcParameterSets = (Option<Vec<u8>>, Option<Vec<u8>>, Option<Vec<u8>>);
|
|
|
|
/// Parse an Annex-B access unit and return the first VPS, SPS and PPS found (HEVC).
|
|
#[allow(dead_code)]
|
|
fn extract_vps_sps_pps(annex_b: &[u8]) -> HevcParameterSets {
|
|
let nals = split_annex_b(annex_b);
|
|
let mut vps = None;
|
|
let mut sps = None;
|
|
let mut pps = None;
|
|
for nal in nals {
|
|
if nal.len() < 2 {
|
|
continue;
|
|
}
|
|
let nal_type = (nal[0] >> 1) & 0x3F;
|
|
if nal_type == 32 && vps.is_none() {
|
|
vps = Some(nal.to_vec());
|
|
} else if nal_type == 33 && sps.is_none() {
|
|
sps = Some(nal.to_vec());
|
|
} else if nal_type == 34 && pps.is_none() {
|
|
pps = Some(nal.to_vec());
|
|
}
|
|
}
|
|
(vps, sps, pps)
|
|
}
|
|
|
|
/// Convert an AVCC blob (4-byte big-endian length prefixes) to Annex-B
|
|
/// (4-byte start codes `0x00 0x00 0x00 0x01`).
|
|
#[allow(dead_code)]
|
|
fn avcc_to_annexb(data: &[u8]) -> Vec<u8> {
|
|
let mut out = Vec::with_capacity(data.len() + data.len() / 4);
|
|
let mut offset = 0;
|
|
while offset + 4 <= data.len() {
|
|
let nal_len = u32::from_be_bytes([
|
|
data[offset],
|
|
data[offset + 1],
|
|
data[offset + 2],
|
|
data[offset + 3],
|
|
]) as usize;
|
|
offset += 4;
|
|
if offset + nal_len > data.len() {
|
|
break;
|
|
}
|
|
out.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
|
|
out.extend_from_slice(&data[offset..offset + nal_len]);
|
|
offset += nal_len;
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Parse an Annex-B access unit and return the first SPS and PPS found.
|
|
#[allow(dead_code)]
|
|
fn extract_sps_pps(annex_b: &[u8]) -> (Option<Vec<u8>>, Option<Vec<u8>>) {
|
|
let nals = split_annex_b(annex_b);
|
|
let mut sps = None;
|
|
let mut pps = None;
|
|
for nal in nals {
|
|
if nal.is_empty() {
|
|
continue;
|
|
}
|
|
let nal_type = nal[0] & 0x1F;
|
|
if nal_type == 7 && sps.is_none() {
|
|
sps = Some(nal.to_vec());
|
|
} else if nal_type == 8 && pps.is_none() {
|
|
pps = Some(nal.to_vec());
|
|
}
|
|
}
|
|
(sps, pps)
|
|
}
|
|
|
|
/// Split an Annex-B byte stream into individual NAL units (without start codes).
|
|
#[allow(dead_code)]
|
|
fn split_annex_b(data: &[u8]) -> Vec<&[u8]> {
|
|
let mut nals = Vec::new();
|
|
let mut i = 0;
|
|
while i < data.len() {
|
|
if i + 3 <= data.len() && data[i..i + 3] == [0x00, 0x00, 0x01] {
|
|
i += 3;
|
|
} else if i + 4 <= data.len() && data[i..i + 4] == [0x00, 0x00, 0x00, 0x01] {
|
|
i += 4;
|
|
} else {
|
|
i += 1;
|
|
continue;
|
|
}
|
|
let start = i;
|
|
while i < data.len() {
|
|
if i + 3 <= data.len() && data[i..i + 3] == [0x00, 0x00, 0x01] {
|
|
break;
|
|
}
|
|
if i + 4 <= data.len() && data[i..i + 4] == [0x00, 0x00, 0x00, 0x01] {
|
|
break;
|
|
}
|
|
i += 1;
|
|
}
|
|
nals.push(&data[start..i]);
|
|
}
|
|
nals
|
|
}
|
|
|
|
/// Extract the first sequence header OBU from an AV1 OBU stream.
|
|
///
|
|
/// Returns the raw OBU bytes (header + size field + payload) for use as
|
|
/// Android MediaCodec `csd-0`.
|
|
#[allow(dead_code)]
|
|
fn extract_sequence_header_obu(data: &[u8]) -> Option<Vec<u8>> {
|
|
use crate::av1_obu::{ObuHeader, read_leb128};
|
|
let mut i = 0usize;
|
|
while i < data.len() {
|
|
let header = ObuHeader::from_byte(data[i]);
|
|
i += 1;
|
|
|
|
if header.extension_flag {
|
|
if i >= data.len() {
|
|
break;
|
|
}
|
|
i += 1;
|
|
}
|
|
|
|
let payload_len = if header.has_size_field {
|
|
let (size, consumed) = read_leb128(data, i)?;
|
|
i += consumed;
|
|
size as usize
|
|
} else {
|
|
// OBU runs to end of stream — not useful for extraction.
|
|
break;
|
|
};
|
|
|
|
if header.obu_type == crate::av1_obu::obu_type::SEQUENCE_HEADER {
|
|
let obu_end = i + payload_len;
|
|
if obu_end > data.len() {
|
|
break;
|
|
}
|
|
// Return the full OBU including header, size field, and payload.
|
|
return Some(data[..obu_end].to_vec());
|
|
}
|
|
|
|
i += payload_len;
|
|
}
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn mediacodec_encoder_returns_not_initialized_on_non_android() {
|
|
let enc = MediaCodecEncoder::new(1280, 720, 2_000_000);
|
|
assert!(matches!(enc, Err(VideoError::NotInitialized)));
|
|
}
|
|
|
|
#[test]
|
|
fn mediacodec_decoder_returns_not_initialized_on_non_android() {
|
|
let dec = MediaCodecDecoder::new(1280, 720);
|
|
assert!(matches!(dec, Err(VideoError::NotInitialized)));
|
|
}
|
|
|
|
#[test]
|
|
fn is_keyframe_detects_idr() {
|
|
let enc = MediaCodecEncoder {
|
|
#[cfg(target_os = "android")]
|
|
codec: unreachable!(),
|
|
#[cfg(target_os = "android")]
|
|
width: 1280,
|
|
#[cfg(target_os = "android")]
|
|
height: 720,
|
|
force_keyframe: false,
|
|
#[cfg(not(target_os = "android"))]
|
|
_width: 1280,
|
|
#[cfg(not(target_os = "android"))]
|
|
_height: 720,
|
|
#[cfg(not(target_os = "android"))]
|
|
_bitrate_bps: 2_000_000,
|
|
};
|
|
assert!(enc.is_keyframe(&[0x65, 0x01]));
|
|
assert!(!enc.is_keyframe(&[0x41, 0x01]));
|
|
}
|
|
|
|
#[test]
|
|
fn avcc_to_annexb_roundtrip() {
|
|
let nal1 = vec![0x67, 0x42, 0xC0, 0x1E];
|
|
let nal2 = vec![0x68, 0xCE, 0x3C, 0x80];
|
|
let mut avcc = Vec::new();
|
|
avcc.extend_from_slice(&(nal1.len() as u32).to_be_bytes());
|
|
avcc.extend_from_slice(&nal1);
|
|
avcc.extend_from_slice(&(nal2.len() as u32).to_be_bytes());
|
|
avcc.extend_from_slice(&nal2);
|
|
|
|
let annex_b = avcc_to_annexb(&avcc);
|
|
let expected = vec![
|
|
0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0xC0, 0x1E, 0x00, 0x00, 0x00, 0x01, 0x68, 0xCE,
|
|
0x3C, 0x80,
|
|
];
|
|
assert_eq!(annex_b, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn hevc_mediacodec_encoder_returns_not_initialized_on_non_android() {
|
|
let enc = MediaCodecHevcEncoder::new(1280, 720, 2_000_000);
|
|
assert!(matches!(enc, Err(VideoError::NotInitialized)));
|
|
}
|
|
|
|
#[test]
|
|
fn hevc_mediacodec_decoder_returns_not_initialized_on_non_android() {
|
|
let dec = MediaCodecHevcDecoder::new(1280, 720);
|
|
assert!(matches!(dec, Err(VideoError::NotInitialized)));
|
|
}
|
|
|
|
#[test]
|
|
fn hevc_is_keyframe_detects_idr() {
|
|
let enc = MediaCodecHevcEncoder {
|
|
#[cfg(target_os = "android")]
|
|
codec: unreachable!(),
|
|
#[cfg(target_os = "android")]
|
|
width: 1280,
|
|
#[cfg(target_os = "android")]
|
|
height: 720,
|
|
force_keyframe: false,
|
|
#[cfg(not(target_os = "android"))]
|
|
_width: 1280,
|
|
#[cfg(not(target_os = "android"))]
|
|
_height: 720,
|
|
#[cfg(not(target_os = "android"))]
|
|
_bitrate_bps: 2_000_000,
|
|
};
|
|
// NAL type 19 (IDR_W_RADL): first byte = 0b0_010011_0 = 0x26
|
|
assert!(enc.is_keyframe(&[0x26, 0x01]));
|
|
// NAL type 20 (IDR_N_LP): first byte = 0b0_010100_0 = 0x28
|
|
assert!(enc.is_keyframe(&[0x28, 0x01]));
|
|
// NAL type 1 (TRAIL_R)
|
|
assert!(!enc.is_keyframe(&[0x02, 0x01]));
|
|
}
|
|
|
|
#[test]
|
|
fn av1_mediacodec_encoder_returns_not_initialized_on_non_android() {
|
|
let enc = MediaCodecAv1Encoder::new(1280, 720, 2_000_000);
|
|
assert!(matches!(enc, Err(VideoError::NotInitialized)));
|
|
}
|
|
|
|
#[test]
|
|
fn av1_mediacodec_decoder_returns_not_initialized_on_non_android() {
|
|
let dec = MediaCodecAv1Decoder::new(1280, 720);
|
|
assert!(matches!(dec, Err(VideoError::NotInitialized)));
|
|
}
|
|
|
|
#[test]
|
|
fn av1_is_keyframe_detects_keyframe() {
|
|
let enc = MediaCodecAv1Encoder {
|
|
#[cfg(target_os = "android")]
|
|
codec: unreachable!(),
|
|
#[cfg(target_os = "android")]
|
|
width: 1280,
|
|
#[cfg(target_os = "android")]
|
|
height: 720,
|
|
force_keyframe: false,
|
|
#[cfg(not(target_os = "android"))]
|
|
_width: 1280,
|
|
#[cfg(not(target_os = "android"))]
|
|
_height: 720,
|
|
#[cfg(not(target_os = "android"))]
|
|
_bitrate_bps: 2_000_000,
|
|
};
|
|
// Frame header with show_existing_frame=0, frame_type=0 (KEY_FRAME)
|
|
let mut key_obu = Vec::new();
|
|
let header = crate::av1_obu::ObuHeader {
|
|
obu_type: crate::av1_obu::obu_type::FRAME_HEADER,
|
|
has_size_field: true,
|
|
extension_flag: false,
|
|
};
|
|
key_obu.push(header.to_byte());
|
|
crate::av1_obu::write_leb128(2, &mut key_obu);
|
|
key_obu.extend_from_slice(&[0x00, 0x00]); // show_existing=0, frame_type=0
|
|
assert!(enc.is_keyframe(&key_obu));
|
|
|
|
// Frame header with show_existing_frame=0, frame_type=1 (INTER)
|
|
let mut inter_obu = Vec::new();
|
|
let header = crate::av1_obu::ObuHeader {
|
|
obu_type: crate::av1_obu::obu_type::FRAME_HEADER,
|
|
has_size_field: true,
|
|
extension_flag: false,
|
|
};
|
|
inter_obu.push(header.to_byte());
|
|
crate::av1_obu::write_leb128(2, &mut inter_obu);
|
|
inter_obu.extend_from_slice(&[0x40, 0x00]); // show_existing=0, frame_type=1
|
|
assert!(!enc.is_keyframe(&inter_obu));
|
|
}
|
|
|
|
#[test]
|
|
fn extract_sequence_header_obu_finds_first_seq_header() {
|
|
let mut data = Vec::new();
|
|
// Sequence header OBU
|
|
let sh_header = crate::av1_obu::ObuHeader {
|
|
obu_type: crate::av1_obu::obu_type::SEQUENCE_HEADER,
|
|
has_size_field: true,
|
|
extension_flag: false,
|
|
};
|
|
data.push(sh_header.to_byte());
|
|
crate::av1_obu::write_leb128(5, &mut data);
|
|
data.extend_from_slice(&[0xAA; 5]);
|
|
|
|
// Frame OBU
|
|
let fh_header = crate::av1_obu::ObuHeader {
|
|
obu_type: crate::av1_obu::obu_type::FRAME,
|
|
has_size_field: true,
|
|
extension_flag: false,
|
|
};
|
|
data.push(fh_header.to_byte());
|
|
crate::av1_obu::write_leb128(3, &mut data);
|
|
data.extend_from_slice(&[0xBB; 3]);
|
|
|
|
let seq = extract_sequence_header_obu(&data).unwrap();
|
|
// Should contain header byte + leb128(5) + 5 payload bytes
|
|
assert_eq!(seq.len(), 1 + 1 + 5);
|
|
assert_eq!(seq[0], sh_header.to_byte());
|
|
}
|
|
|
|
#[test]
|
|
fn extract_sequence_header_obu_returns_none_without_seq_header() {
|
|
let mut data = Vec::new();
|
|
let fh_header = crate::av1_obu::ObuHeader {
|
|
obu_type: crate::av1_obu::obu_type::FRAME,
|
|
has_size_field: true,
|
|
extension_flag: false,
|
|
};
|
|
data.push(fh_header.to_byte());
|
|
crate::av1_obu::write_leb128(3, &mut data);
|
|
data.extend_from_slice(&[0xBB; 3]);
|
|
|
|
assert!(extract_sequence_header_obu(&data).is_none());
|
|
}
|
|
}
|