feat(clients/android): OnFrameRendered display stage — HUD headline becomes capture→displayed
The long-deferred Android display stage (design/stats-unification.md; plan 4.1 of design/client-parity-and-network-resilience.md): AMediaCodec_setOnFrameRenderedCallback (API 26, under the minSdk-28 floor ⇒ hard-linked via ndk-sys) reports SurfaceFlinger's per-frame render timestamp, giving the HUD the spec's `display` = decoded→displayed term and the directly-measured capture→displayed end-to-end headline on both decode loops. Falls back per spec to the v1 capture→decoded endpoint on any window without render callbacks (the platform may drop them under load), and to it permanently if registration is refused. - The render timestamp arrives on CLOCK_MONOTONIC; it's re-based onto CLOCK_REALTIME against monotonic-now at callback time, which also cancels the (batchable) callback delivery lag. - The `ndk` crate exposes neither the callback nor the codec pointer needed to bind it raw, so the workspace pins `ndk` 0.9.0 to a vendored copy (clients/android/native/ vendor/ndk) whose ONLY change makes MediaCodec::as_ptr public — the "as_ptr patch". Workspace-excluded so host builds never compile it; drop when upstream exposes either. - nativeVideoStats grows to 26 doubles (22–25: dispValid, displayP50, e2eDispP50/P95; 0–21 unchanged for older readers); StatsOverlay moves headline endpoint + equation together so the equation always tiles the headline interval. Verified: host cargo check/test/clippy, aarch64-linux-android check/clippy, Kotlin app+kit+tests compile, roborazzi HUD render shows the full 4-term equation. Device verification rides plan 4.2's phone A/B. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,459 @@
|
||||
//! Bindings for [`AImageReader`] and [`AImage`]
|
||||
//!
|
||||
//! [`AImageReader`]: https://developer.android.com/ndk/reference/group/media#aimagereader
|
||||
//! [`AImage`]: https://developer.android.com/ndk/reference/group/media#aimage
|
||||
#![cfg(feature = "api-level-24")]
|
||||
|
||||
use crate::media_error::{construct, construct_never_null, MediaError, Result};
|
||||
use crate::native_window::NativeWindow;
|
||||
use crate::utils::abort_on_panic;
|
||||
use num_enum::{FromPrimitive, IntoPrimitive};
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
fmt::{self, Debug, Formatter},
|
||||
mem::MaybeUninit,
|
||||
ptr::NonNull,
|
||||
};
|
||||
|
||||
#[cfg(feature = "api-level-26")]
|
||||
use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd};
|
||||
|
||||
#[cfg(feature = "api-level-26")]
|
||||
use crate::hardware_buffer::{HardwareBuffer, HardwareBufferUsage};
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive, IntoPrimitive)]
|
||||
#[allow(non_camel_case_types)]
|
||||
#[non_exhaustive]
|
||||
pub enum ImageFormat {
|
||||
RGBA_8888 = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_RGBA_8888.0 as i32,
|
||||
RGBX_8888 = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_RGBX_8888.0 as i32,
|
||||
RGB_888 = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_RGB_888.0 as i32,
|
||||
RGB_565 = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_RGB_565.0 as i32,
|
||||
RGBA_FP16 = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_RGBA_FP16.0 as i32,
|
||||
YUV_420_888 = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_YUV_420_888.0 as i32,
|
||||
JPEG = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_JPEG.0 as i32,
|
||||
RAW16 = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_RAW16.0 as i32,
|
||||
RAW_PRIVATE = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_RAW_PRIVATE.0 as i32,
|
||||
RAW10 = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_RAW10.0 as i32,
|
||||
RAW12 = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_RAW12.0 as i32,
|
||||
DEPTH16 = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_DEPTH16.0 as i32,
|
||||
DEPTH_POINT_CLOUD = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_DEPTH_POINT_CLOUD.0 as i32,
|
||||
PRIVATE = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_PRIVATE.0 as i32,
|
||||
Y8 = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_Y8.0 as i32,
|
||||
HEIC = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_HEIC.0 as i32,
|
||||
DEPTH_JPEG = ffi::AIMAGE_FORMATS::AIMAGE_FORMAT_DEPTH_JPEG.0 as i32,
|
||||
|
||||
#[doc(hidden)]
|
||||
#[num_enum(catch_all)]
|
||||
__Unknown(i32),
|
||||
}
|
||||
|
||||
pub type ImageListener = Box<dyn FnMut(&ImageReader) + Send>;
|
||||
|
||||
#[cfg(feature = "api-level-26")]
|
||||
pub type BufferRemovedListener = Box<dyn FnMut(&ImageReader, &HardwareBuffer) + Send>;
|
||||
|
||||
/// Result returned by:
|
||||
/// - [`ImageReader::acquire_next_image()`]`
|
||||
/// - [`ImageReader::acquire_next_image_async()`]`
|
||||
/// - [`ImageReader::acquire_latest_image()`]`
|
||||
/// - [`ImageReader::acquire_latest_image_async()`]`
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum AcquireResult<T> {
|
||||
/// Returned if there is no buffers currently available in the reader queue.
|
||||
#[doc(alias = "AMEDIA_IMGREADER_NO_BUFFER_AVAILABLE")]
|
||||
NoBufferAvailable,
|
||||
/// Returned if the number of concurrently acquired images has reached the limit.
|
||||
#[doc(alias = "AMEDIA_IMGREADER_MAX_IMAGES_ACQUIRED")]
|
||||
MaxImagesAcquired,
|
||||
|
||||
/// Returned if an [`Image`] (optionally with fence) was successfully acquired.
|
||||
Image(T),
|
||||
}
|
||||
|
||||
impl<T> AcquireResult<T> {
|
||||
fn map<U>(self, f: impl FnOnce(T) -> U) -> AcquireResult<U> {
|
||||
match self {
|
||||
AcquireResult::Image(img) => AcquireResult::Image(f(img)),
|
||||
AcquireResult::NoBufferAvailable => AcquireResult::NoBufferAvailable,
|
||||
AcquireResult::MaxImagesAcquired => AcquireResult::MaxImagesAcquired,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AcquireResult<Image> {
|
||||
/// Inlined version of [`construct_never_null()`] with IMGREADER-specific result mapping.
|
||||
fn construct_never_null(
|
||||
with_ptr: impl FnOnce(*mut *mut ffi::AImage) -> ffi::media_status_t,
|
||||
) -> Result<Self> {
|
||||
let mut result = MaybeUninit::uninit();
|
||||
let status = with_ptr(result.as_mut_ptr());
|
||||
match status {
|
||||
ffi::media_status_t::AMEDIA_IMGREADER_NO_BUFFER_AVAILABLE => {
|
||||
Ok(Self::NoBufferAvailable)
|
||||
}
|
||||
ffi::media_status_t::AMEDIA_IMGREADER_MAX_IMAGES_ACQUIRED => {
|
||||
Ok(Self::MaxImagesAcquired)
|
||||
}
|
||||
status => MediaError::from_status(status).map(|()| {
|
||||
let result = unsafe { result.assume_init() };
|
||||
Self::Image(Image {
|
||||
inner: if cfg!(debug_assertions) {
|
||||
NonNull::new(result).expect("result should never be null")
|
||||
} else {
|
||||
unsafe { NonNull::new_unchecked(result) }
|
||||
},
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A native [`AImageReader *`]
|
||||
///
|
||||
/// [`AImageReader *`]: https://developer.android.com/ndk/reference/group/media#aimagereader
|
||||
pub struct ImageReader {
|
||||
inner: NonNull<ffi::AImageReader>,
|
||||
image_cb: Option<Box<ImageListener>>,
|
||||
#[cfg(feature = "api-level-26")]
|
||||
buffer_removed_cb: Option<Box<BufferRemovedListener>>,
|
||||
}
|
||||
|
||||
impl Debug for ImageReader {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ImageReader")
|
||||
.field("inner", &self.inner)
|
||||
.field(
|
||||
"image_cb",
|
||||
match &self.image_cb {
|
||||
Some(_) => &"Some(_)",
|
||||
None => &"None",
|
||||
},
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageReader {
|
||||
fn from_ptr(inner: NonNull<ffi::AImageReader>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
image_cb: None,
|
||||
#[cfg(feature = "api-level-26")]
|
||||
buffer_removed_cb: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_ptr(&self) -> *mut ffi::AImageReader {
|
||||
self.inner.as_ptr()
|
||||
}
|
||||
|
||||
pub fn new(width: i32, height: i32, format: ImageFormat, max_images: i32) -> Result<Self> {
|
||||
let inner = construct_never_null(|res| unsafe {
|
||||
ffi::AImageReader_new(width, height, format.into(), max_images, res)
|
||||
})?;
|
||||
|
||||
Ok(Self::from_ptr(inner))
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-26")]
|
||||
pub fn new_with_usage(
|
||||
width: i32,
|
||||
height: i32,
|
||||
format: ImageFormat,
|
||||
usage: HardwareBufferUsage,
|
||||
max_images: i32,
|
||||
) -> Result<Self> {
|
||||
let inner = construct_never_null(|res| unsafe {
|
||||
ffi::AImageReader_newWithUsage(
|
||||
width,
|
||||
height,
|
||||
format.into(),
|
||||
usage.bits(),
|
||||
max_images,
|
||||
res,
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Self::from_ptr(inner))
|
||||
}
|
||||
|
||||
#[doc(alias = "AImageReader_setImageListener")]
|
||||
pub fn set_image_listener(&mut self, listener: ImageListener) -> Result<()> {
|
||||
let mut boxed = Box::new(listener);
|
||||
let ptr: *mut ImageListener = &mut *boxed;
|
||||
|
||||
unsafe extern "C" fn on_image_available(
|
||||
context: *mut c_void,
|
||||
reader: *mut ffi::AImageReader,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
let reader = ImageReader::from_ptr(NonNull::new_unchecked(reader));
|
||||
let listener: *mut ImageListener = context.cast();
|
||||
(*listener)(&reader);
|
||||
std::mem::forget(reader);
|
||||
})
|
||||
}
|
||||
|
||||
let mut listener = ffi::AImageReader_ImageListener {
|
||||
context: ptr as _,
|
||||
onImageAvailable: Some(on_image_available),
|
||||
};
|
||||
let status = unsafe { ffi::AImageReader_setImageListener(self.as_ptr(), &mut listener) };
|
||||
|
||||
// keep listener alive until Drop or new listener is assigned
|
||||
self.image_cb = Some(boxed);
|
||||
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-26")]
|
||||
#[doc(alias = "AImageReader_setBufferRemovedListener")]
|
||||
pub fn set_buffer_removed_listener(&mut self, listener: BufferRemovedListener) -> Result<()> {
|
||||
let mut boxed = Box::new(listener);
|
||||
let ptr: *mut BufferRemovedListener = &mut *boxed;
|
||||
|
||||
unsafe extern "C" fn on_buffer_removed(
|
||||
context: *mut c_void,
|
||||
reader: *mut ffi::AImageReader,
|
||||
buffer: *mut ffi::AHardwareBuffer,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
let reader = ImageReader::from_ptr(NonNull::new_unchecked(reader));
|
||||
let buffer = HardwareBuffer::from_ptr(NonNull::new_unchecked(buffer));
|
||||
let listener: *mut BufferRemovedListener = context.cast();
|
||||
(*listener)(&reader, &buffer);
|
||||
std::mem::forget(reader);
|
||||
})
|
||||
}
|
||||
|
||||
let mut listener = ffi::AImageReader_BufferRemovedListener {
|
||||
context: ptr as _,
|
||||
onBufferRemoved: Some(on_buffer_removed),
|
||||
};
|
||||
let status =
|
||||
unsafe { ffi::AImageReader_setBufferRemovedListener(self.as_ptr(), &mut listener) };
|
||||
|
||||
// keep listener alive until Drop or new listener is assigned
|
||||
self.buffer_removed_cb = Some(boxed);
|
||||
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
|
||||
/// Get a [`NativeWindow`] that can be used to produce [`Image`]s for this [`ImageReader`].
|
||||
///
|
||||
/// <https://developer.android.com/ndk/reference/group/media#aimagereader_getwindow>
|
||||
#[doc(alias = "AImageReader_getWindow")]
|
||||
pub fn window(&self) -> Result<NativeWindow> {
|
||||
unsafe {
|
||||
let ptr = construct_never_null(|res| ffi::AImageReader_getWindow(self.as_ptr(), res))?;
|
||||
Ok(NativeWindow::clone_from_ptr(ptr))
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(alias = "AImageReader_getWidth")]
|
||||
pub fn width(&self) -> Result<i32> {
|
||||
construct(|res| unsafe { ffi::AImageReader_getWidth(self.as_ptr(), res) })
|
||||
}
|
||||
|
||||
#[doc(alias = "AImageReader_getHeight")]
|
||||
pub fn height(&self) -> Result<i32> {
|
||||
construct(|res| unsafe { ffi::AImageReader_getHeight(self.as_ptr(), res) })
|
||||
}
|
||||
|
||||
#[doc(alias = "AImageReader_getFormat")]
|
||||
pub fn format(&self) -> Result<ImageFormat> {
|
||||
let format = construct(|res| unsafe { ffi::AImageReader_getFormat(self.as_ptr(), res) })?;
|
||||
Ok(format.into())
|
||||
}
|
||||
|
||||
#[doc(alias = "AImageReader_getMaxImages")]
|
||||
pub fn max_images(&self) -> Result<i32> {
|
||||
construct(|res| unsafe { ffi::AImageReader_getMaxImages(self.as_ptr(), res) })
|
||||
}
|
||||
|
||||
#[doc(alias = "AImageReader_acquireNextImage")]
|
||||
pub fn acquire_next_image(&self) -> Result<AcquireResult<Image>> {
|
||||
AcquireResult::construct_never_null(|res| unsafe {
|
||||
ffi::AImageReader_acquireNextImage(self.as_ptr(), res)
|
||||
})
|
||||
}
|
||||
|
||||
/// Acquire the next [`Image`] from the image reader's queue asynchronously.
|
||||
///
|
||||
/// # Safety
|
||||
/// If the returned file descriptor is not [`None`], it must be awaited before attempting to
|
||||
/// access the [`Image`] returned.
|
||||
///
|
||||
/// <https://developer.android.com/ndk/reference/group/media#aimagereader_acquirenextimageasync>
|
||||
#[cfg(feature = "api-level-26")]
|
||||
#[doc(alias = "AImageReader_acquireNextImageAsync")]
|
||||
pub unsafe fn acquire_next_image_async(
|
||||
&self,
|
||||
) -> Result<AcquireResult<(Image, Option<OwnedFd>)>> {
|
||||
let mut fence = MaybeUninit::uninit();
|
||||
AcquireResult::construct_never_null(|res| {
|
||||
ffi::AImageReader_acquireNextImageAsync(self.as_ptr(), res, fence.as_mut_ptr())
|
||||
})
|
||||
.map(|result| {
|
||||
result.map(|image| match fence.assume_init() {
|
||||
-1 => (image, None),
|
||||
fence => (image, Some(unsafe { OwnedFd::from_raw_fd(fence) })),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(alias = "AImageReader_acquireLatestImage")]
|
||||
pub fn acquire_latest_image(&self) -> Result<AcquireResult<Image>> {
|
||||
AcquireResult::construct_never_null(|res| unsafe {
|
||||
ffi::AImageReader_acquireLatestImage(self.as_ptr(), res)
|
||||
})
|
||||
}
|
||||
|
||||
/// Acquire the latest [`Image`] from the image reader's queue asynchronously, dropping older images.
|
||||
///
|
||||
/// # Safety
|
||||
/// If the returned file descriptor is not [`None`], it must be awaited before attempting to
|
||||
/// access the [`Image`] returned.
|
||||
///
|
||||
/// <https://developer.android.com/ndk/reference/group/media#aimagereader_acquirelatestimageasync>
|
||||
#[cfg(feature = "api-level-26")]
|
||||
#[doc(alias = "AImageReader_acquireLatestImageAsync")]
|
||||
pub unsafe fn acquire_latest_image_async(
|
||||
&self,
|
||||
) -> Result<AcquireResult<(Image, Option<OwnedFd>)>> {
|
||||
let mut fence = MaybeUninit::uninit();
|
||||
AcquireResult::construct_never_null(|res| {
|
||||
ffi::AImageReader_acquireLatestImageAsync(self.as_ptr(), res, fence.as_mut_ptr())
|
||||
})
|
||||
.map(|result| {
|
||||
result.map(|image| match fence.assume_init() {
|
||||
-1 => (image, None),
|
||||
fence => (image, Some(unsafe { OwnedFd::from_raw_fd(fence) })),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ImageReader {
|
||||
#[doc(alias = "AImageReader_delete")]
|
||||
fn drop(&mut self) {
|
||||
unsafe { ffi::AImageReader_delete(self.as_ptr()) };
|
||||
}
|
||||
}
|
||||
|
||||
/// A native [`AImage *`]
|
||||
///
|
||||
/// [`AImage *`]: https://developer.android.com/ndk/reference/group/media#aimage
|
||||
#[derive(Debug)]
|
||||
#[doc(alias = "AImage")]
|
||||
pub struct Image {
|
||||
inner: NonNull<ffi::AImage>,
|
||||
}
|
||||
|
||||
#[doc(alias = "AImageCropRect")]
|
||||
pub type CropRect = ffi::AImageCropRect;
|
||||
|
||||
impl Image {
|
||||
fn as_ptr(&self) -> *mut ffi::AImage {
|
||||
self.inner.as_ptr()
|
||||
}
|
||||
|
||||
#[doc(alias = "AImage_getPlaneData")]
|
||||
pub fn plane_data(&self, plane_idx: i32) -> Result<&[u8]> {
|
||||
let mut result_ptr = MaybeUninit::uninit();
|
||||
let mut result_len = MaybeUninit::uninit();
|
||||
let status = unsafe {
|
||||
ffi::AImage_getPlaneData(
|
||||
self.as_ptr(),
|
||||
plane_idx,
|
||||
result_ptr.as_mut_ptr(),
|
||||
result_len.as_mut_ptr(),
|
||||
)
|
||||
};
|
||||
|
||||
MediaError::from_status(status).map(|()| unsafe {
|
||||
std::slice::from_raw_parts(result_ptr.assume_init(), result_len.assume_init() as _)
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(alias = "AImage_getPlanePixelStride")]
|
||||
pub fn plane_pixel_stride(&self, plane_idx: i32) -> Result<i32> {
|
||||
construct(|res| unsafe { ffi::AImage_getPlanePixelStride(self.as_ptr(), plane_idx, res) })
|
||||
}
|
||||
|
||||
#[doc(alias = "AImage_getPlaneRowStride")]
|
||||
pub fn plane_row_stride(&self, plane_idx: i32) -> Result<i32> {
|
||||
construct(|res| unsafe { ffi::AImage_getPlaneRowStride(self.as_ptr(), plane_idx, res) })
|
||||
}
|
||||
|
||||
#[doc(alias = "AImage_getCropRect")]
|
||||
pub fn crop_rect(&self) -> Result<CropRect> {
|
||||
construct(|res| unsafe { ffi::AImage_getCropRect(self.as_ptr(), res) })
|
||||
}
|
||||
|
||||
#[doc(alias = "AImage_getWidth")]
|
||||
pub fn width(&self) -> Result<i32> {
|
||||
construct(|res| unsafe { ffi::AImage_getWidth(self.as_ptr(), res) })
|
||||
}
|
||||
|
||||
#[doc(alias = "AImage_getHeight")]
|
||||
pub fn height(&self) -> Result<i32> {
|
||||
construct(|res| unsafe { ffi::AImage_getHeight(self.as_ptr(), res) })
|
||||
}
|
||||
|
||||
#[doc(alias = "AImage_getFormat")]
|
||||
pub fn format(&self) -> Result<ImageFormat> {
|
||||
let format = construct(|res| unsafe { ffi::AImage_getFormat(self.as_ptr(), res) })?;
|
||||
Ok(format.into())
|
||||
}
|
||||
|
||||
#[doc(alias = "AImage_getTimestamp")]
|
||||
pub fn timestamp(&self) -> Result<i64> {
|
||||
construct(|res| unsafe { ffi::AImage_getTimestamp(self.as_ptr(), res) })
|
||||
}
|
||||
|
||||
#[doc(alias = "AImage_getNumberOfPlanes")]
|
||||
pub fn number_of_planes(&self) -> Result<i32> {
|
||||
construct(|res| unsafe { ffi::AImage_getNumberOfPlanes(self.as_ptr(), res) })
|
||||
}
|
||||
|
||||
/// Get the hardware buffer handle of the input image intended for GPU and/or hardware access.
|
||||
///
|
||||
/// Note that no reference on the returned [`HardwareBuffer`] handle is acquired automatically.
|
||||
/// Once the [`Image`] or the parent [`ImageReader`] is deleted, the [`HardwareBuffer`] handle
|
||||
/// from previous [`Image::hardware_buffer()`] becomes invalid.
|
||||
///
|
||||
/// If the caller ever needs to hold on a reference to the [`HardwareBuffer`] handle after the
|
||||
/// [`Image`] or the parent [`ImageReader`] is deleted, it must call
|
||||
/// [`HardwareBuffer::acquire()`] to acquire an extra reference, and [`drop()`] it when
|
||||
/// finished using it in order to properly deallocate the underlying memory managed by
|
||||
/// [`HardwareBuffer`]. If the caller has acquired an extra reference on a [`HardwareBuffer`]
|
||||
/// returned from this function, it must also register a listener using
|
||||
/// [`ImageReader::set_buffer_removed_listener()`] to be notified when the buffer is no longer
|
||||
/// used by [`ImageReader`].
|
||||
#[cfg(feature = "api-level-26")]
|
||||
#[doc(alias = "AImage_getHardwareBuffer")]
|
||||
pub fn hardware_buffer(&self) -> Result<HardwareBuffer> {
|
||||
unsafe {
|
||||
let ptr =
|
||||
construct_never_null(|res| ffi::AImage_getHardwareBuffer(self.as_ptr(), res))?;
|
||||
Ok(HardwareBuffer::from_ptr(ptr))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-26")]
|
||||
#[doc(alias = "AImage_deleteAsync")]
|
||||
pub fn delete_async(self, release_fence_fd: OwnedFd) {
|
||||
unsafe { ffi::AImage_deleteAsync(self.as_ptr(), release_fence_fd.into_raw_fd()) };
|
||||
std::mem::forget(self);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Image {
|
||||
#[doc(alias = "AImage_delete")]
|
||||
fn drop(&mut self) {
|
||||
unsafe { ffi::AImage_delete(self.as_ptr()) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
//! Bindings for [`AMediaCodec`]
|
||||
//!
|
||||
//! [`AMediaCodec`]: https://developer.android.com/ndk/reference/group/media#amediacodec
|
||||
|
||||
#[deprecated = "MediaFormat should be referenced directly from the media_format module"]
|
||||
pub use super::media_format::MediaFormat;
|
||||
use crate::media_error::{MediaError, Result};
|
||||
use crate::native_window::NativeWindow;
|
||||
use crate::utils::abort_on_panic;
|
||||
use std::{
|
||||
ffi::{c_char, c_void, CStr, CString},
|
||||
fmt,
|
||||
mem::MaybeUninit,
|
||||
pin::Pin,
|
||||
ptr::{self, NonNull},
|
||||
slice,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum MediaCodecDirection {
|
||||
Decoder,
|
||||
Encoder,
|
||||
}
|
||||
|
||||
/// A native [`AMediaCodec *`]
|
||||
///
|
||||
/// [`AMediaCodec *`]: https://developer.android.com/ndk/reference/group/media#amediacodec
|
||||
#[derive(Debug)]
|
||||
pub struct MediaCodec {
|
||||
inner: NonNull<ffi::AMediaCodec>,
|
||||
async_notify_callback: Option<Pin<Box<AsyncNotifyCallback>>>,
|
||||
}
|
||||
|
||||
pub struct AsyncNotifyCallback {
|
||||
/// Called when an input buffer becomes available.
|
||||
///
|
||||
/// The specified index is the index of the available input buffer.
|
||||
pub on_input_available: Option<InputAvailableCallback>,
|
||||
|
||||
/// Called when an output buffer becomes available.
|
||||
///
|
||||
/// The specified index is the index of the available output buffer. The specified
|
||||
/// [`BufferInfo`] contains information regarding the available output buffer.
|
||||
pub on_output_available: Option<OutputAvailableCallback>,
|
||||
|
||||
/// Called when the output format has changed.
|
||||
///
|
||||
/// The specified format contains the new output format.
|
||||
pub on_format_changed: Option<FormatChangedCallback>,
|
||||
|
||||
/// Called when the [`MediaCodec`] encountered an error.
|
||||
///
|
||||
/// The specified [`ActionCode`] indicates the possible actions that client can take, and it can
|
||||
/// be checked by calling [`ActionCode::is_recoverable`] or [`ActionCode::is_transient`]. If
|
||||
/// both [`ActionCode::is_recoverable`] and [`ActionCode::is_transient`] return [`false`], then
|
||||
/// the codec error is fatal and the codec must be deleted. The specified detail string may
|
||||
/// contain more detailed messages about this error.
|
||||
pub on_error: Option<ErrorCallback>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for AsyncNotifyCallback {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("AsyncNotifyCallback")
|
||||
.field(
|
||||
"on_input_available",
|
||||
match &self.on_input_available {
|
||||
Some(_) => &"Some(_)",
|
||||
None => &"None",
|
||||
},
|
||||
)
|
||||
.field(
|
||||
"on_output_available",
|
||||
match &self.on_output_available {
|
||||
Some(_) => &"Some(_)",
|
||||
None => &"None",
|
||||
},
|
||||
)
|
||||
.field(
|
||||
"on_format_changed",
|
||||
match &self.on_format_changed {
|
||||
Some(_) => &"Some(_)",
|
||||
None => &"None",
|
||||
},
|
||||
)
|
||||
.field(
|
||||
"on_error",
|
||||
match &self.on_error {
|
||||
Some(_) => &"Some(_)",
|
||||
None => &"None",
|
||||
},
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub type InputAvailableCallback = Box<dyn FnMut(usize) + Send>;
|
||||
pub type OutputAvailableCallback = Box<dyn FnMut(usize, &BufferInfo) + Send>;
|
||||
pub type FormatChangedCallback = Box<dyn FnMut(&MediaFormat) + Send>;
|
||||
pub type ErrorCallback = Box<dyn FnMut(MediaError, ActionCode, &CStr) + Send>;
|
||||
|
||||
impl MediaCodec {
|
||||
/// [punktfunk vendored patch — the ONLY change to this crate] Public so callers can bind
|
||||
/// `AMediaCodec_*` entry points the wrapper doesn't expose yet (here:
|
||||
/// `AMediaCodec_setOnFrameRenderedCallback` for the HUD's display stage). The pointer is valid
|
||||
/// for `&self`'s lifetime; callers must not delete or re-configure the codec through it.
|
||||
pub fn as_ptr(&self) -> *mut ffi::AMediaCodec {
|
||||
self.inner.as_ptr()
|
||||
}
|
||||
|
||||
pub fn from_codec_name(name: &str) -> Option<Self> {
|
||||
let c_string = CString::new(name).unwrap();
|
||||
Some(Self {
|
||||
inner: NonNull::new(unsafe { ffi::AMediaCodec_createCodecByName(c_string.as_ptr()) })?,
|
||||
async_notify_callback: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_decoder_type(mime_type: &str) -> Option<Self> {
|
||||
let c_string = CString::new(mime_type).unwrap();
|
||||
Some(Self {
|
||||
inner: NonNull::new(unsafe {
|
||||
ffi::AMediaCodec_createDecoderByType(c_string.as_ptr())
|
||||
})?,
|
||||
async_notify_callback: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_encoder_type(mime_type: &str) -> Option<Self> {
|
||||
let c_string = CString::new(mime_type).unwrap();
|
||||
Some(Self {
|
||||
inner: NonNull::new(unsafe {
|
||||
ffi::AMediaCodec_createEncoderByType(c_string.as_ptr())
|
||||
})?,
|
||||
async_notify_callback: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set an asynchronous callback for actionable [`MediaCodec`] events.
|
||||
///
|
||||
/// When asynchronous callback is enabled, it is an error for the client to call
|
||||
/// [`MediaCodec::dequeue_input_buffer()`] or [`MediaCodec::dequeue_output_buffer()`].
|
||||
///
|
||||
/// [`MediaCodec::flush()`] behaves differently in asynchronous mode. After calling
|
||||
/// [`MediaCodec::flush()`], the client must call [`MediaCodec::start()`] to "resume" receiving
|
||||
/// input buffers. Even if the client does not receive
|
||||
/// [`AsyncNotifyCallback::on_input_available`] callbacks from video encoders configured with an
|
||||
/// input surface, the client still needs to call [`MediaCodec::start()`] to resume the input
|
||||
/// surface to send buffers to the encoders.
|
||||
///
|
||||
/// When called with [`None`] callback, this method unregisters any previously set callback.
|
||||
///
|
||||
/// Refer to the definition of [`AsyncNotifyCallback`] on how each callback function is called
|
||||
/// and what are specified.
|
||||
///
|
||||
/// Once the callback is unregistered or the codec is reset / released, the previously
|
||||
/// registered callback will not be called.
|
||||
///
|
||||
/// All callbacks are fired on one NDK internal thread.
|
||||
/// [`MediaCodec::set_async_notify_callback()`] should not be called on the callback thread. No
|
||||
/// heavy duty task should be performed on callback thread.
|
||||
#[cfg(feature = "api-level-28")]
|
||||
pub fn set_async_notify_callback(
|
||||
&mut self,
|
||||
callback: Option<AsyncNotifyCallback>,
|
||||
) -> Result<()> {
|
||||
unsafe extern "C" fn ffi_on_input_available(
|
||||
_codec: *mut ffi::AMediaCodec,
|
||||
user_data: *mut c_void,
|
||||
index: i32,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
let callback = &mut *(user_data as *mut AsyncNotifyCallback);
|
||||
if let Some(f) = callback.on_input_available.as_mut() {
|
||||
f(index as usize);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn ffi_on_output_available(
|
||||
_codec: *mut ffi::AMediaCodec,
|
||||
user_data: *mut c_void,
|
||||
index: i32,
|
||||
buffer_info: *mut ffi::AMediaCodecBufferInfo,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
let callback = &mut *(user_data as *mut AsyncNotifyCallback);
|
||||
if let Some(f) = callback.on_output_available.as_mut() {
|
||||
let buffer_info = BufferInfo {
|
||||
inner: *buffer_info,
|
||||
};
|
||||
f(index as usize, &buffer_info);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn ffi_on_format_changed(
|
||||
_codec: *mut ffi::AMediaCodec,
|
||||
user_data: *mut c_void,
|
||||
format: *mut ffi::AMediaFormat,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
// Ownership of the format is not documented, but the implementation allocates a new instance and does
|
||||
// not free it, so assume it is ok for us to do so
|
||||
// https://cs.android.com/android/platform/superproject/main/+/refs/heads/main:frameworks/av/media/ndk/NdkMediaCodec.cpp;l=248-254;drc=5e15c3e22f3fa32d64e57302201123ce41589adf
|
||||
let format = MediaFormat::from_ptr(NonNull::new_unchecked(format));
|
||||
|
||||
let callback = &mut *(user_data as *mut AsyncNotifyCallback);
|
||||
if let Some(f) = callback.on_format_changed.as_mut() {
|
||||
f(&format);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn ffi_on_error(
|
||||
_codec: *mut ffi::AMediaCodec,
|
||||
user_data: *mut c_void,
|
||||
error: ffi::media_status_t,
|
||||
action_code: i32,
|
||||
detail: *const c_char,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
let callback = &mut *(user_data as *mut AsyncNotifyCallback);
|
||||
if let Some(f) = callback.on_error.as_mut() {
|
||||
f(
|
||||
MediaError::from_status(error).unwrap_err(),
|
||||
ActionCode(action_code),
|
||||
CStr::from_ptr(detail),
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let (callback, ffi_callback, user_data) = if let Some(callback) = callback {
|
||||
// On Android 12 and earlier, due to faulty null checks, if a callback is not set, but at least one other
|
||||
// callback *is* set, then it will segfault in when trying to invoke the unset callback. See for example:
|
||||
// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r34:frameworks/av/media/ndk/NdkMediaCodec.cpp;l=161-162;drc=ef058464777739e2d9ffad5f00d0e57b186d9a13
|
||||
// To work around this we just enable all callbacks and do nothing if the corresponding callback is not set
|
||||
// in AsyncNotifyCallback
|
||||
let ffi_callback = ffi::AMediaCodecOnAsyncNotifyCallback {
|
||||
onAsyncInputAvailable: Some(ffi_on_input_available),
|
||||
onAsyncOutputAvailable: Some(ffi_on_output_available),
|
||||
onAsyncFormatChanged: Some(ffi_on_format_changed),
|
||||
onAsyncError: Some(ffi_on_error),
|
||||
};
|
||||
|
||||
let mut boxed = Box::pin(callback);
|
||||
let ptr: *mut AsyncNotifyCallback = &mut *boxed;
|
||||
|
||||
(Some(boxed), ffi_callback, ptr as *mut c_void)
|
||||
} else {
|
||||
let ffi_callback = ffi::AMediaCodecOnAsyncNotifyCallback {
|
||||
onAsyncInputAvailable: None,
|
||||
onAsyncOutputAvailable: None,
|
||||
onAsyncFormatChanged: None,
|
||||
onAsyncError: None,
|
||||
};
|
||||
|
||||
(None, ffi_callback, ptr::null_mut())
|
||||
};
|
||||
|
||||
let status = unsafe {
|
||||
ffi::AMediaCodec_setAsyncNotifyCallback(self.as_ptr(), ffi_callback, user_data)
|
||||
};
|
||||
let result = MediaError::from_status(status);
|
||||
|
||||
// This behavior is not documented, but the implementation always clears the callback on failure, so we must
|
||||
// clear any callback that may have been previously registered
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:frameworks/av/media/ndk/NdkMediaCodec.cpp;l=581-584;drc=8c4e619c7461ac1a8c20c55364643662e9185e4d
|
||||
if result.is_ok() {
|
||||
self.async_notify_callback = callback;
|
||||
} else {
|
||||
self.async_notify_callback = None;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn configure(
|
||||
&self,
|
||||
format: &MediaFormat,
|
||||
surface: Option<&NativeWindow>,
|
||||
direction: MediaCodecDirection,
|
||||
) -> Result<()> {
|
||||
let status = unsafe {
|
||||
ffi::AMediaCodec_configure(
|
||||
self.as_ptr(),
|
||||
format.as_ptr(),
|
||||
surface.map_or(ptr::null_mut(), |s| s.ptr().as_ptr()),
|
||||
ptr::null_mut(),
|
||||
if direction == MediaCodecDirection::Encoder {
|
||||
ffi::AMEDIACODEC_CONFIGURE_FLAG_ENCODE as u32
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
};
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-26")]
|
||||
pub fn create_input_surface(&self) -> Result<NativeWindow> {
|
||||
use crate::media_error::construct_never_null;
|
||||
unsafe {
|
||||
let ptr = construct_never_null(|res| {
|
||||
ffi::AMediaCodec_createInputSurface(self.as_ptr(), res)
|
||||
})?;
|
||||
Ok(NativeWindow::from_ptr(ptr))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-26")]
|
||||
pub fn create_persistent_input_surface() -> Result<NativeWindow> {
|
||||
use crate::media_error::construct_never_null;
|
||||
unsafe {
|
||||
let ptr =
|
||||
construct_never_null(|res| ffi::AMediaCodec_createPersistentInputSurface(res))?;
|
||||
Ok(NativeWindow::from_ptr(ptr))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dequeue_input_buffer(&self, timeout: Duration) -> Result<DequeuedInputBufferResult<'_>> {
|
||||
let result = unsafe {
|
||||
ffi::AMediaCodec_dequeueInputBuffer(
|
||||
self.as_ptr(),
|
||||
timeout
|
||||
.as_micros()
|
||||
.try_into()
|
||||
.expect("Supplied timeout is too large"),
|
||||
)
|
||||
};
|
||||
|
||||
if result == ffi::AMEDIACODEC_INFO_TRY_AGAIN_LATER as isize {
|
||||
Ok(DequeuedInputBufferResult::TryAgainLater)
|
||||
} else {
|
||||
let index = MediaError::from_status_if_negative(result)? as usize;
|
||||
Ok(DequeuedInputBufferResult::Buffer(InputBuffer {
|
||||
codec: self,
|
||||
index,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dequeue_output_buffer(
|
||||
&self,
|
||||
timeout: Duration,
|
||||
) -> Result<DequeuedOutputBufferInfoResult<'_>> {
|
||||
let mut info = MaybeUninit::uninit();
|
||||
|
||||
let result = unsafe {
|
||||
ffi::AMediaCodec_dequeueOutputBuffer(
|
||||
self.as_ptr(),
|
||||
info.as_mut_ptr(),
|
||||
timeout
|
||||
.as_micros()
|
||||
.try_into()
|
||||
.expect("Supplied timeout is too large"),
|
||||
)
|
||||
};
|
||||
|
||||
if result == ffi::AMEDIACODEC_INFO_TRY_AGAIN_LATER as isize {
|
||||
Ok(DequeuedOutputBufferInfoResult::TryAgainLater)
|
||||
} else if result == ffi::AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED as isize {
|
||||
Ok(DequeuedOutputBufferInfoResult::OutputFormatChanged)
|
||||
} else if result == ffi::AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED as isize {
|
||||
Ok(DequeuedOutputBufferInfoResult::OutputBuffersChanged)
|
||||
} else {
|
||||
let index = MediaError::from_status_if_negative(result)? as usize;
|
||||
Ok(DequeuedOutputBufferInfoResult::Buffer(OutputBuffer {
|
||||
codec: self,
|
||||
index,
|
||||
info: BufferInfo {
|
||||
inner: unsafe { info.assume_init() },
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flush(&self) -> Result<()> {
|
||||
let status = unsafe { ffi::AMediaCodec_flush(self.as_ptr()) };
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
|
||||
pub fn input_buffer(&self, index: usize) -> Option<&mut [MaybeUninit<u8>]> {
|
||||
unsafe {
|
||||
let mut out_size = 0;
|
||||
let buffer_ptr = ffi::AMediaCodec_getInputBuffer(self.as_ptr(), index, &mut out_size);
|
||||
if buffer_ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(slice::from_raw_parts_mut(buffer_ptr.cast(), out_size))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_buffer(&self, index: usize) -> Option<&[u8]> {
|
||||
unsafe {
|
||||
let mut out_size = 0;
|
||||
let buffer_ptr = ffi::AMediaCodec_getOutputBuffer(self.as_ptr(), index, &mut out_size);
|
||||
if buffer_ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(slice::from_raw_parts(buffer_ptr, out_size))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-28")]
|
||||
pub fn input_format(&self) -> MediaFormat {
|
||||
let inner = NonNull::new(unsafe { ffi::AMediaCodec_getInputFormat(self.as_ptr()) })
|
||||
.expect("AMediaCodec_getInputFormat returned NULL");
|
||||
unsafe { MediaFormat::from_ptr(inner) }
|
||||
}
|
||||
|
||||
pub fn output_format(&self) -> MediaFormat {
|
||||
let inner = NonNull::new(unsafe { ffi::AMediaCodec_getOutputFormat(self.as_ptr()) })
|
||||
.expect("AMediaCodec_getOutputFormat returned NULL");
|
||||
unsafe { MediaFormat::from_ptr(inner) }
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-28")]
|
||||
pub fn name(&self) -> Result<String> {
|
||||
use crate::media_error::construct;
|
||||
unsafe {
|
||||
let name_ptr = construct(|name| ffi::AMediaCodec_getName(self.as_ptr(), name))?;
|
||||
let name = CStr::from_ptr(name_ptr).to_str().unwrap().to_owned();
|
||||
ffi::AMediaCodec_releaseName(self.as_ptr(), name_ptr);
|
||||
|
||||
Ok(name)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn queue_input_buffer(
|
||||
&self,
|
||||
buffer: InputBuffer<'_>,
|
||||
offset: usize,
|
||||
size: usize,
|
||||
time: u64,
|
||||
flags: u32,
|
||||
) -> Result<()> {
|
||||
debug_assert!(ptr::eq(self, buffer.codec));
|
||||
self.queue_input_buffer_by_index(buffer.index, offset, size, time, flags)
|
||||
}
|
||||
|
||||
pub fn queue_input_buffer_by_index(
|
||||
&self,
|
||||
buffer_index: usize,
|
||||
offset: usize,
|
||||
size: usize,
|
||||
time: u64,
|
||||
flags: u32,
|
||||
) -> Result<()> {
|
||||
let status = unsafe {
|
||||
ffi::AMediaCodec_queueInputBuffer(
|
||||
self.as_ptr(),
|
||||
buffer_index,
|
||||
offset as ffi::off_t,
|
||||
size,
|
||||
time,
|
||||
flags,
|
||||
)
|
||||
};
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
|
||||
pub fn release_output_buffer(&self, buffer: OutputBuffer<'_>, render: bool) -> Result<()> {
|
||||
debug_assert!(ptr::eq(self, buffer.codec));
|
||||
self.release_output_buffer_by_index(buffer.index, render)
|
||||
}
|
||||
|
||||
pub fn release_output_buffer_by_index(&self, buffer_index: usize, render: bool) -> Result<()> {
|
||||
let status =
|
||||
unsafe { ffi::AMediaCodec_releaseOutputBuffer(self.as_ptr(), buffer_index, render) };
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
|
||||
pub fn release_output_buffer_at_time(
|
||||
&self,
|
||||
buffer: OutputBuffer<'_>,
|
||||
timestamp_ns: i64,
|
||||
) -> Result<()> {
|
||||
debug_assert!(ptr::eq(self, buffer.codec));
|
||||
self.release_output_buffer_at_time_by_index(buffer.index, timestamp_ns)
|
||||
}
|
||||
|
||||
pub fn release_output_buffer_at_time_by_index(
|
||||
&self,
|
||||
buffer_index: usize,
|
||||
timestamp_ns: i64,
|
||||
) -> Result<()> {
|
||||
let status = unsafe {
|
||||
ffi::AMediaCodec_releaseOutputBufferAtTime(self.as_ptr(), buffer_index, timestamp_ns)
|
||||
};
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-26")]
|
||||
pub fn set_input_surface(&self, surface: &NativeWindow) -> Result<()> {
|
||||
let status =
|
||||
unsafe { ffi::AMediaCodec_setInputSurface(self.as_ptr(), surface.ptr().as_ptr()) };
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
|
||||
pub fn set_output_surface(&self, surface: &NativeWindow) -> Result<()> {
|
||||
let status =
|
||||
unsafe { ffi::AMediaCodec_setOutputSurface(self.as_ptr(), surface.ptr().as_ptr()) };
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-26")]
|
||||
pub fn set_parameters(&self, params: MediaFormat) -> Result<()> {
|
||||
let status = unsafe { ffi::AMediaCodec_setParameters(self.as_ptr(), params.as_ptr()) };
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-26")]
|
||||
pub fn set_signal_end_of_input_stream(&self) -> Result<()> {
|
||||
let status = unsafe { ffi::AMediaCodec_signalEndOfInputStream(self.as_ptr()) };
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
|
||||
pub fn start(&self) -> Result<()> {
|
||||
let status = unsafe { ffi::AMediaCodec_start(self.as_ptr()) };
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<()> {
|
||||
let status = unsafe { ffi::AMediaCodec_stop(self.as_ptr()) };
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MediaCodec {
|
||||
fn drop(&mut self) {
|
||||
let status = unsafe { ffi::AMediaCodec_delete(self.as_ptr()) };
|
||||
MediaError::from_status(status).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InputBuffer<'a> {
|
||||
codec: &'a MediaCodec,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl InputBuffer<'_> {
|
||||
pub fn buffer_mut(&mut self) -> &mut [MaybeUninit<u8>] {
|
||||
self.codec.input_buffer(self.index).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"AMediaCodec_getInputBuffer returned NULL for index {}",
|
||||
self.index
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DequeuedInputBufferResult<'a> {
|
||||
Buffer(InputBuffer<'a>),
|
||||
TryAgainLater,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OutputBuffer<'a> {
|
||||
codec: &'a MediaCodec,
|
||||
index: usize,
|
||||
info: BufferInfo,
|
||||
}
|
||||
|
||||
impl OutputBuffer<'_> {
|
||||
pub fn buffer(&self) -> &[u8] {
|
||||
self.codec.output_buffer(self.index).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"AMediaCodec_getOutputBuffer returned NULL for index {}",
|
||||
self.index
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-28")]
|
||||
pub fn format(&self) -> MediaFormat {
|
||||
let inner = NonNull::new(unsafe {
|
||||
ffi::AMediaCodec_getBufferFormat(self.codec.as_ptr(), self.index)
|
||||
})
|
||||
.expect("AMediaCodec_getBufferFormat returned NULL");
|
||||
unsafe { MediaFormat::from_ptr(inner) }
|
||||
}
|
||||
|
||||
pub fn info(&self) -> &BufferInfo {
|
||||
&self.info
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DequeuedOutputBufferInfoResult<'a> {
|
||||
Buffer(OutputBuffer<'a>),
|
||||
TryAgainLater,
|
||||
OutputFormatChanged,
|
||||
OutputBuffersChanged,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct BufferInfo {
|
||||
inner: ffi::AMediaCodecBufferInfo,
|
||||
}
|
||||
|
||||
impl BufferInfo {
|
||||
pub fn offset(&self) -> i32 {
|
||||
self.inner.offset
|
||||
}
|
||||
|
||||
pub fn size(&self) -> i32 {
|
||||
self.inner.size
|
||||
}
|
||||
|
||||
pub fn presentation_time_us(&self) -> i64 {
|
||||
self.inner.presentationTimeUs
|
||||
}
|
||||
|
||||
pub fn flags(&self) -> u32 {
|
||||
self.inner.flags
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct ActionCode(pub i32);
|
||||
|
||||
impl ActionCode {
|
||||
pub fn is_recoverable(self) -> bool {
|
||||
unsafe { ffi::AMediaCodecActionCode_isRecoverable(self.0) }
|
||||
}
|
||||
|
||||
pub fn is_transient(self) -> bool {
|
||||
unsafe { ffi::AMediaCodecActionCode_isTransient(self.0) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
//! Bindings for [`AMediaFormat`]
|
||||
//!
|
||||
//! [`AMediaFormat`]: https://developer.android.com/ndk/reference/group/media#amediaformat
|
||||
|
||||
use std::{
|
||||
ffi::{CStr, CString},
|
||||
fmt,
|
||||
ptr::{self, NonNull},
|
||||
slice,
|
||||
};
|
||||
|
||||
use crate::media_error::{MediaError, Result};
|
||||
|
||||
/// A native [`AMediaFormat *`]
|
||||
///
|
||||
/// [`AMediaFormat *`]: https://developer.android.com/ndk/reference/group/media#amediaformat
|
||||
#[doc(alias = "AMediaFormat")]
|
||||
pub struct MediaFormat {
|
||||
inner: NonNull<ffi::AMediaFormat>,
|
||||
}
|
||||
|
||||
impl fmt::Display for MediaFormat {
|
||||
/// Human readable representation of the format.
|
||||
#[doc(alias = "AMediaFormat_toString")]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let c_str = unsafe { CStr::from_ptr(ffi::AMediaFormat_toString(self.as_ptr())) };
|
||||
f.write_str(c_str.to_str().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for MediaFormat {
|
||||
#[doc(alias = "AMediaFormat_toString")]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "MediaFormat({:?}: {})", self.inner, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MediaFormat {
|
||||
#[doc(alias = "AMediaFormat_new")]
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaFormat {
|
||||
/// Assumes ownership of `ptr`
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a valid pointer to an Android [`ffi::AMediaFormat`].
|
||||
pub unsafe fn from_ptr(ptr: NonNull<ffi::AMediaFormat>) -> Self {
|
||||
Self { inner: ptr }
|
||||
}
|
||||
|
||||
pub fn as_ptr(&self) -> *mut ffi::AMediaFormat {
|
||||
self.inner.as_ptr()
|
||||
}
|
||||
|
||||
#[doc(alias = "AMediaFormat_new")]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: NonNull::new(unsafe { ffi::AMediaFormat_new() }).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(alias = "AMediaFormat_getInt32")]
|
||||
pub fn i32(&self, key: &str) -> Option<i32> {
|
||||
let name = CString::new(key).unwrap();
|
||||
let mut out = 0;
|
||||
if unsafe { ffi::AMediaFormat_getInt32(self.as_ptr(), name.as_ptr(), &mut out) } {
|
||||
Some(out)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(alias = "AMediaFormat_getInt64")]
|
||||
pub fn i64(&self, key: &str) -> Option<i64> {
|
||||
let name = CString::new(key).unwrap();
|
||||
let mut out = 0;
|
||||
if unsafe { ffi::AMediaFormat_getInt64(self.as_ptr(), name.as_ptr(), &mut out) } {
|
||||
Some(out)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(alias = "AMediaFormat_getFloat")]
|
||||
pub fn f32(&self, key: &str) -> Option<f32> {
|
||||
let name = CString::new(key).unwrap();
|
||||
let mut out = 0.0;
|
||||
if unsafe { ffi::AMediaFormat_getFloat(self.as_ptr(), name.as_ptr(), &mut out) } {
|
||||
Some(out)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(alias = "AMediaFormat_getSize")]
|
||||
pub fn usize(&self, key: &str) -> Option<usize> {
|
||||
let name = CString::new(key).unwrap();
|
||||
let mut out = 0;
|
||||
if unsafe { ffi::AMediaFormat_getSize(self.as_ptr(), name.as_ptr(), &mut out) } {
|
||||
Some(out)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(alias = "AMediaFormat_getBuffer")]
|
||||
pub fn buffer(&self, key: &str) -> Option<&[u8]> {
|
||||
let name = CString::new(key).unwrap();
|
||||
let mut out_buffer = ptr::null_mut();
|
||||
let mut out_size = 0;
|
||||
unsafe {
|
||||
ffi::AMediaFormat_getBuffer(
|
||||
self.as_ptr(),
|
||||
name.as_ptr(),
|
||||
&mut out_buffer,
|
||||
&mut out_size,
|
||||
)
|
||||
}
|
||||
.then(|| unsafe { slice::from_raw_parts(out_buffer.cast(), out_size) })
|
||||
}
|
||||
|
||||
/// The returned `&str` borrow is only valid until the next call to [`MediaFormat::str()`] for
|
||||
/// the same key.
|
||||
#[doc(alias = "AMediaFormat_getString")]
|
||||
pub fn str(&mut self, key: &str) -> Option<&str> {
|
||||
let name = CString::new(key).unwrap();
|
||||
let mut out = ptr::null();
|
||||
unsafe { ffi::AMediaFormat_getString(self.as_ptr(), name.as_ptr(), &mut out) }
|
||||
.then(|| unsafe { CStr::from_ptr(out) }.to_str().unwrap())
|
||||
}
|
||||
|
||||
#[doc(alias = "AMediaFormat_setInt32")]
|
||||
pub fn set_i32(&mut self, key: &str, value: i32) {
|
||||
let name = CString::new(key).unwrap();
|
||||
unsafe { ffi::AMediaFormat_setInt32(self.as_ptr(), name.as_ptr(), value) }
|
||||
}
|
||||
|
||||
#[doc(alias = "AMediaFormat_setInt64")]
|
||||
pub fn set_i64(&mut self, key: &str, value: i64) {
|
||||
let name = CString::new(key).unwrap();
|
||||
unsafe { ffi::AMediaFormat_setInt64(self.as_ptr(), name.as_ptr(), value) }
|
||||
}
|
||||
|
||||
#[doc(alias = "AMediaFormat_setFloat")]
|
||||
pub fn set_f32(&mut self, key: &str, value: f32) {
|
||||
let name = CString::new(key).unwrap();
|
||||
unsafe { ffi::AMediaFormat_setFloat(self.as_ptr(), name.as_ptr(), value) }
|
||||
}
|
||||
|
||||
#[doc(alias = "AMediaFormat_setString")]
|
||||
pub fn set_str(&mut self, key: &str, value: &str) {
|
||||
let name = CString::new(key).unwrap();
|
||||
let c_string = CString::new(value).unwrap();
|
||||
unsafe { ffi::AMediaFormat_setString(self.as_ptr(), name.as_ptr(), c_string.as_ptr()) }
|
||||
}
|
||||
|
||||
#[doc(alias = "AMediaFormat_setBuffer")]
|
||||
pub fn set_buffer(&mut self, key: &str, value: &[u8]) {
|
||||
let name = CString::new(key).unwrap();
|
||||
unsafe {
|
||||
ffi::AMediaFormat_setBuffer(
|
||||
self.as_ptr(),
|
||||
name.as_ptr(),
|
||||
value.as_ptr().cast(),
|
||||
value.len(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-28")]
|
||||
#[doc(alias = "AMediaFormat_getDouble")]
|
||||
pub fn f64(&self, key: &str) -> Option<f64> {
|
||||
let name = CString::new(key).unwrap();
|
||||
let mut out = 0.0;
|
||||
if unsafe { ffi::AMediaFormat_getDouble(self.as_ptr(), name.as_ptr(), &mut out) } {
|
||||
Some(out)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns (left, top, right, bottom)
|
||||
#[cfg(feature = "api-level-28")]
|
||||
#[doc(alias = "AMediaFormat_getRect")]
|
||||
pub fn rect(&self, key: &str) -> Option<(i32, i32, i32, i32)> {
|
||||
let name = CString::new(key).unwrap();
|
||||
let mut left = 0;
|
||||
let mut top = 0;
|
||||
let mut right = 0;
|
||||
let mut bottom = 0;
|
||||
if unsafe {
|
||||
ffi::AMediaFormat_getRect(
|
||||
self.as_ptr(),
|
||||
name.as_ptr(),
|
||||
&mut left,
|
||||
&mut top,
|
||||
&mut right,
|
||||
&mut bottom,
|
||||
)
|
||||
} {
|
||||
Some((left, top, right, bottom))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-28")]
|
||||
#[doc(alias = "AMediaFormat_setDouble")]
|
||||
pub fn set_f64(&mut self, key: &str, value: f64) {
|
||||
let name = CString::new(key).unwrap();
|
||||
unsafe { ffi::AMediaFormat_setDouble(self.as_ptr(), name.as_ptr(), value) }
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-28")]
|
||||
#[doc(alias = "AMediaFormat_setRect")]
|
||||
pub fn set_rect(&mut self, key: &str, left: i32, top: i32, right: i32, bottom: i32) {
|
||||
let name = CString::new(key).unwrap();
|
||||
unsafe { ffi::AMediaFormat_setRect(self.as_ptr(), name.as_ptr(), left, top, right, bottom) }
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-level-28")]
|
||||
#[doc(alias = "AMediaFormat_setSize")]
|
||||
pub fn set_usize(&mut self, key: &str, value: usize) {
|
||||
let name = CString::new(key).unwrap();
|
||||
unsafe { ffi::AMediaFormat_setSize(self.as_ptr(), name.as_ptr(), value) }
|
||||
}
|
||||
|
||||
/// Copy one [`MediaFormat`] to another.
|
||||
#[cfg(feature = "api-level-29")]
|
||||
#[doc(alias = "AMediaFormat_copy")]
|
||||
pub fn copy(&self, to: &mut Self) -> Result<()> {
|
||||
let status = unsafe { ffi::AMediaFormat_copy(to.as_ptr(), self.as_ptr()) };
|
||||
MediaError::from_status(status)
|
||||
}
|
||||
|
||||
/// Clones this [`MediaFormat`] into a [`MediaFormat::new()`] object using
|
||||
/// [`MediaFormat::copy()`].
|
||||
#[cfg(feature = "api-level-29")]
|
||||
#[doc(alias = "AMediaFormat_new")]
|
||||
#[doc(alias = "AMediaFormat_copy")]
|
||||
pub fn try_clone(&self) -> Result<Self> {
|
||||
let mut copy = Self::new();
|
||||
self.copy(&mut copy)?;
|
||||
Ok(copy)
|
||||
}
|
||||
|
||||
/// Remove all key/value pairs from this [`MediaFormat`].
|
||||
#[cfg(feature = "api-level-29")]
|
||||
#[doc(alias = "AMediaFormat_clear")]
|
||||
pub fn clear(&mut self) {
|
||||
unsafe { ffi::AMediaFormat_clear(self.as_ptr()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MediaFormat {
|
||||
#[doc(alias = "AMediaFormat_delete")]
|
||||
fn drop(&mut self) {
|
||||
let status = unsafe { ffi::AMediaFormat_delete(self.as_ptr()) };
|
||||
MediaError::from_status(status).unwrap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//! Bindings for the NDK media classes.
|
||||
//!
|
||||
//! See also [the NDK docs](https://developer.android.com/ndk/reference/group/media)
|
||||
#![cfg(feature = "media")]
|
||||
|
||||
pub mod image_reader;
|
||||
pub mod media_codec;
|
||||
pub mod media_format;
|
||||
Reference in New Issue
Block a user