#![allow(unsafe_code)]
use std::collections::VecDeque;
use std::ffi::{CStr, c_void};
use std::os::raw::c_int;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use crate::camera::{CameraCapture, CameraCaptureConfig, CaptureOutputPreference};
use crate::{CaptureError, Select};
use mediaway_common::{
Bytes, CodecKind, PixelFormat, Rational, StreamInfo, VideoFrame, VideoFrameStorage,
VideoGeometry,
};
use ndk::media::image_reader::{AcquireResult, Image, ImageFormat, ImageReader};
const CAPTURE_WIDTH: i32 = 1280;
const CAPTURE_HEIGHT: i32 = 720;
const MAX_IMAGES: i32 = 4;
const FRAME_QUEUE_CAP: usize = 4;
const POLL_INTERVAL: Duration = Duration::from_millis(8);
struct FrameQueue {
frames: Mutex<VecDeque<VideoFrame>>,
}
struct CameraSession {
stream_info: StreamInfo,
queue: Arc<FrameQueue>,
stop: Arc<AtomicBool>,
device_lost: Arc<AtomicBool>,
worker: Option<JoinHandle<()>>,
}
pub struct AndroidCameraCapture {
inner: Option<CameraSession>,
}
impl AndroidCameraCapture {
pub fn open(config: &CameraCaptureConfig) -> Result<Self, CaptureError> {
if config.select != Select::Default {
return Err(CaptureError::Unsupported);
}
if config.output != CaptureOutputPreference::CpuFramesOk {
return Err(CaptureError::Unsupported);
}
let queue = Arc::new(FrameQueue {
frames: Mutex::new(VecDeque::new()),
});
let queue_worker = Arc::clone(&queue);
let stop = Arc::new(AtomicBool::new(false));
let stop_worker = Arc::clone(&stop);
let device_lost = Arc::new(AtomicBool::new(false));
let device_lost_worker = Arc::clone(&device_lost);
let time_base = config.time_base;
let (tx_info, rx_info) = std::sync::mpsc::sync_channel(1);
let worker = thread::Builder::new()
.name("mediaway-camera2-ndk".into())
.spawn(move || {
run_camera_worker(
time_base,
&queue_worker,
&stop_worker,
&device_lost_worker,
&tx_info,
);
})
.map_err(|_| CaptureError::Backend)?;
let stream_info = rx_info.recv().map_err(|_| CaptureError::Backend)??;
Ok(Self {
inner: Some(CameraSession {
stream_info,
queue,
stop,
device_lost,
worker: Some(worker),
}),
})
}
}
impl CameraCapture for AndroidCameraCapture {
fn stream_info(&self) -> &StreamInfo {
#[allow(
clippy::option_if_let_else,
reason = "map_or_else forces 'static vs 'self lifetime clash"
)]
if let Some(s) = self.inner.as_ref() {
&s.stream_info
} else {
closed_video_info()
}
}
fn poll_frame(&mut self) -> Result<Option<VideoFrame>, CaptureError> {
let Some(session) = self.inner.as_ref() else {
return Err(CaptureError::Closed);
};
if session.device_lost.load(Ordering::Relaxed) {
return Err(CaptureError::DeviceLost);
}
let mut q = session
.queue
.frames
.lock()
.map_err(|_| CaptureError::Backend)?;
Ok(q.pop_front())
}
fn release_frame(&mut self) -> Result<(), CaptureError> {
if self.inner.is_none() {
return Err(CaptureError::Closed);
}
Ok(())
}
fn close(&mut self) -> Result<(), CaptureError> {
let Some(mut session) = self.inner.take() else {
return Ok(());
};
session.stop.store(true, Ordering::SeqCst);
if let Some(h) = session.worker.take() {
let _ = h.join();
}
Ok(())
}
}
impl Drop for AndroidCameraCapture {
fn drop(&mut self) {
let _ = self.close();
}
}
fn closed_video_info() -> &'static StreamInfo {
use std::sync::OnceLock;
static INFO: OnceLock<StreamInfo> = OnceLock::new();
INFO.get_or_init(|| StreamInfo::Video {
id: 0,
codec: CodecKind::RawVideo,
time_base: Rational::new(1, 30),
geometry: VideoGeometry {
width: 0,
height: 0,
},
extra_data: Bytes::new(),
})
}
struct DeviceState {
lost: Arc<AtomicBool>,
}
unsafe extern "C" fn on_device_disconnected(
context: *mut c_void,
_device: *mut ndk_sys::ACameraDevice,
) {
if let Some(state) = unsafe { (context.cast::<DeviceState>()).as_ref() } {
state.lost.store(true, Ordering::SeqCst);
}
}
unsafe extern "C" fn on_device_error(
context: *mut c_void,
_device: *mut ndk_sys::ACameraDevice,
_error: c_int,
) {
if let Some(state) = unsafe { (context.cast::<DeviceState>()).as_ref() } {
state.lost.store(true, Ordering::SeqCst);
}
}
struct CameraResources {
manager: *mut ndk_sys::ACameraManager,
device: *mut ndk_sys::ACameraDevice,
session: *mut ndk_sys::ACameraCaptureSession,
request: *mut ndk_sys::ACaptureRequest,
output_target: *mut ndk_sys::ACameraOutputTarget,
output_container: *mut ndk_sys::ACaptureSessionOutputContainer,
session_output: *mut ndk_sys::ACaptureSessionOutput,
image_reader: Option<ImageReader>,
device_state: Option<Box<DeviceState>>,
}
impl Default for CameraResources {
fn default() -> Self {
Self {
manager: std::ptr::null_mut(),
device: std::ptr::null_mut(),
session: std::ptr::null_mut(),
request: std::ptr::null_mut(),
output_target: std::ptr::null_mut(),
output_container: std::ptr::null_mut(),
session_output: std::ptr::null_mut(),
image_reader: None,
device_state: None,
}
}
}
impl Drop for CameraResources {
fn drop(&mut self) {
unsafe {
if !self.request.is_null() {
ndk_sys::ACaptureRequest_free(self.request);
}
if !self.output_target.is_null() {
ndk_sys::ACameraOutputTarget_free(self.output_target);
}
if !self.session.is_null() {
ndk_sys::ACameraCaptureSession_close(self.session);
}
if !self.output_container.is_null() && !self.session_output.is_null() {
ndk_sys::ACaptureSessionOutputContainer_remove(
self.output_container,
self.session_output,
);
}
if !self.session_output.is_null() {
ndk_sys::ACaptureSessionOutput_free(self.session_output);
}
if !self.output_container.is_null() {
ndk_sys::ACaptureSessionOutputContainer_free(self.output_container);
}
if !self.device.is_null() {
ndk_sys::ACameraDevice_close(self.device);
}
if !self.manager.is_null() {
ndk_sys::ACameraManager_delete(self.manager);
}
}
}
}
fn run_camera_worker(
time_base: Rational,
queue: &FrameQueue,
stop: &AtomicBool,
device_lost: &Arc<AtomicBool>,
tx_info: &std::sync::mpsc::SyncSender<Result<StreamInfo, CaptureError>>,
) {
let (resources, stream_info, format) =
match open_camera_session(time_base, Arc::clone(device_lost)) {
Ok(v) => v,
Err(e) => {
let _ = tx_info.send(Err(e));
return;
}
};
let _ = tx_info.send(Ok(stream_info));
let Some(reader) = resources.image_reader.as_ref() else {
return;
};
let mut pts: i64 = 0;
while !stop.load(Ordering::Relaxed) {
match reader.acquire_latest_image() {
Ok(AcquireResult::Image(image)) => {
if let Some(data) = pack_image(&image, format) {
push_frame(queue, format, data, pts);
pts = pts.saturating_add(1);
}
}
Ok(_) | Err(_) => thread::sleep(POLL_INTERVAL),
}
}
}
fn open_camera_session(
time_base: Rational,
device_lost: Arc<AtomicBool>,
) -> Result<(CameraResources, StreamInfo, PixelFormat), CaptureError> {
let mut res = CameraResources::default();
open_camera_device(&mut res, device_lost)?;
let format = configure_capture_session(&mut res)?;
let info = StreamInfo::Video {
id: 0,
codec: CodecKind::RawVideo,
time_base,
geometry: VideoGeometry {
width: CAPTURE_WIDTH.unsigned_abs(),
height: CAPTURE_HEIGHT.unsigned_abs(),
},
extra_data: Bytes::new(),
};
Ok((res, info, format))
}
fn open_camera_device(
res: &mut CameraResources,
device_lost: Arc<AtomicBool>,
) -> Result<(), CaptureError> {
res.manager = unsafe { ndk_sys::ACameraManager_create() };
if res.manager.is_null() {
return Err(CaptureError::Backend);
}
let camera_id = first_camera_id(res.manager)?;
let device_state = Box::new(DeviceState { lost: device_lost });
let device_state_ptr: *mut DeviceState = (&raw const *device_state).cast_mut();
let mut state_callbacks = ndk_sys::ACameraDevice_StateCallbacks {
context: device_state_ptr.cast(),
onDisconnected: Some(on_device_disconnected),
onError: Some(on_device_error),
};
res.device_state = Some(device_state);
let status = unsafe {
ndk_sys::ACameraManager_openCamera(
res.manager,
camera_id.as_ptr(),
&raw mut state_callbacks,
&raw mut res.device,
)
};
if status.0 != 0 || res.device.is_null() {
return Err(map_camera_status(status));
}
Ok(())
}
fn configure_capture_session(res: &mut CameraResources) -> Result<PixelFormat, CaptureError> {
let reader = ImageReader::new(
CAPTURE_WIDTH,
CAPTURE_HEIGHT,
ImageFormat::YUV_420_888,
MAX_IMAGES,
)
.map_err(|_| CaptureError::Backend)?;
let window = reader.window().map_err(|_| CaptureError::Backend)?;
let anw: *mut ndk_sys::ANativeWindow = window.ptr().as_ptr();
drop(window);
res.image_reader = Some(reader);
let status =
unsafe { ndk_sys::ACaptureSessionOutputContainer_create(&raw mut res.output_container) };
if status.0 != 0 {
return Err(map_camera_status(status));
}
let status =
unsafe { ndk_sys::ACaptureSessionOutput_create(anw.cast(), &raw mut res.session_output) };
if status.0 != 0 {
return Err(map_camera_status(status));
}
let status = unsafe {
ndk_sys::ACaptureSessionOutputContainer_add(res.output_container, res.session_output)
};
if status.0 != 0 {
return Err(map_camera_status(status));
}
let session_callbacks = ndk_sys::ACameraCaptureSession_stateCallbacks {
context: std::ptr::null_mut(),
onClosed: None,
onReady: None,
onActive: None,
};
let status = unsafe {
ndk_sys::ACameraDevice_createCaptureSession(
res.device,
res.output_container,
&raw const session_callbacks,
&raw mut res.session,
)
};
if status.0 != 0 {
return Err(map_camera_status(status));
}
let status = unsafe {
ndk_sys::ACameraDevice_createCaptureRequest(
res.device,
ndk_sys::ACameraDevice_request_template::TEMPLATE_PREVIEW,
&raw mut res.request,
)
};
if status.0 != 0 {
return Err(map_camera_status(status));
}
let status =
unsafe { ndk_sys::ACameraOutputTarget_create(anw.cast(), &raw mut res.output_target) };
if status.0 != 0 {
return Err(map_camera_status(status));
}
let status = unsafe { ndk_sys::ACaptureRequest_addTarget(res.request, res.output_target) };
if status.0 != 0 {
return Err(map_camera_status(status));
}
let mut sequence_id: c_int = 0;
let mut requests = [res.request];
let status = unsafe {
ndk_sys::ACameraCaptureSession_setRepeatingRequest(
res.session,
std::ptr::null_mut(),
1,
requests.as_mut_ptr(),
&raw mut sequence_id,
)
};
if status.0 != 0 {
return Err(map_camera_status(status));
}
detect_first_image_format(res.image_reader.as_ref().ok_or(CaptureError::Backend)?)
}
fn detect_first_image_format(reader: &ImageReader) -> Result<PixelFormat, CaptureError> {
const MAX_ATTEMPTS: u32 = 250; for _ in 0..MAX_ATTEMPTS {
if let Ok(AcquireResult::Image(image)) = reader.acquire_latest_image() {
return detect_pixel_format(&image).ok_or(CaptureError::Unsupported);
}
thread::sleep(POLL_INTERVAL);
}
Err(CaptureError::Backend)
}
pub(super) fn camera_id_count() -> usize {
let manager = unsafe { ndk_sys::ACameraManager_create() };
if manager.is_null() {
return 0;
}
let mut list: *mut ndk_sys::ACameraIdList = std::ptr::null_mut();
let status = unsafe { ndk_sys::ACameraManager_getCameraIdList(manager, &raw mut list) };
let count = if status.0 == 0 && !list.is_null() {
let n = unsafe { (*list).numCameras };
usize::try_from(n).unwrap_or(0)
} else {
0
};
if !list.is_null() {
unsafe { ndk_sys::ACameraManager_deleteCameraIdList(list) };
}
unsafe { ndk_sys::ACameraManager_delete(manager) };
count
}
fn first_camera_id(
manager: *mut ndk_sys::ACameraManager,
) -> Result<std::ffi::CString, CaptureError> {
let mut list: *mut ndk_sys::ACameraIdList = std::ptr::null_mut();
let status = unsafe { ndk_sys::ACameraManager_getCameraIdList(manager, &raw mut list) };
if status.0 != 0 || list.is_null() {
return Err(map_camera_status(status));
}
let result = unsafe {
let list_ref = &*list;
if list_ref.numCameras <= 0 || list_ref.cameraIds.is_null() {
Err(CaptureError::InvalidInput)
} else {
let first = *list_ref.cameraIds;
if first.is_null() {
Err(CaptureError::Backend)
} else {
Ok(CStr::from_ptr(first).to_owned())
}
}
};
unsafe { ndk_sys::ACameraManager_deleteCameraIdList(list) };
result
}
const fn map_camera_status(status: ndk_sys::camera_status_t) -> CaptureError {
match status.0 {
-10013 => CaptureError::AccessDenied, -10002 => CaptureError::DeviceLost, -10001 => CaptureError::InvalidInput, _ => CaptureError::Backend,
}
}
fn detect_pixel_format(image: &Image) -> Option<PixelFormat> {
let u_stride = image.plane_pixel_stride(1).ok()?;
let v_stride = image.plane_pixel_stride(2).ok()?;
if u_stride == 1 && v_stride == 1 {
return Some(PixelFormat::I420);
}
if u_stride == 2 && v_stride == 2 {
let u = image.plane_data(1).ok()?;
let v = image.plane_data(2).ok()?;
if !u.is_empty() && v.as_ptr() as usize == (u.as_ptr() as usize).wrapping_add(1) {
return Some(PixelFormat::Nv12);
}
}
None
}
fn copy_rows(
src: &[u8],
out: &mut Vec<u8>,
row_bytes: usize,
rows: usize,
stride: usize,
) -> Option<()> {
for row in 0..rows {
let start = row.checked_mul(stride)?;
let end = start.checked_add(row_bytes)?;
out.extend_from_slice(src.get(start..end)?);
}
Some(())
}
fn pack_image(image: &Image, format: PixelFormat) -> Option<Bytes> {
let width = usize::try_from(image.width().ok()?).unwrap_or(0);
let height = usize::try_from(image.height().ok()?).unwrap_or(0);
if width == 0 || height == 0 {
return None;
}
let y = image.plane_data(0).ok()?;
let y_stride = usize::try_from(image.plane_row_stride(0).ok()?).unwrap_or(0);
if y_stride < width {
return None;
}
let mut out = Vec::new();
copy_rows(y, &mut out, width, height, y_stride)?;
match format {
PixelFormat::I420 => {
let u = image.plane_data(1).ok()?;
let v = image.plane_data(2).ok()?;
let chroma_stride = usize::try_from(image.plane_row_stride(1).ok()?).unwrap_or(0);
let chroma_w = width / 2;
let chroma_rows = height / 2;
if chroma_stride < chroma_w {
return None;
}
copy_rows(u, &mut out, chroma_w, chroma_rows, chroma_stride)?;
copy_rows(v, &mut out, chroma_w, chroma_rows, chroma_stride)?;
}
PixelFormat::Nv12 => {
let uv = image.plane_data(1).ok()?;
let uv_stride = usize::try_from(image.plane_row_stride(1).ok()?).unwrap_or(0);
let chroma_rows = height / 2;
if uv_stride < width {
return None;
}
copy_rows(uv, &mut out, width, chroma_rows, uv_stride)?;
}
_ => return None,
}
Some(Bytes::from(out))
}
fn push_frame(queue: &FrameQueue, format: PixelFormat, data: Bytes, pts: i64) {
let frame = VideoFrame {
pts,
duration: 1,
width: CAPTURE_WIDTH.unsigned_abs(),
height: CAPTURE_HEIGHT.unsigned_abs(),
format,
storage: VideoFrameStorage::Cpu { data },
};
if let Ok(mut q) = queue.frames.lock() {
if q.len() >= FRAME_QUEUE_CAP {
let _ = q.pop_front();
}
q.push_back(frame);
}
}
#[cfg(test)]
#[path = "camera_tests.rs"]
mod tests;