Download environment/training-source/source-snapshot/src/camera.rs from mad-bot/dinovision: direct link, hf CLI and curl.
- Browser
- Download file 28.4 kB
-
https://huggingface.co/mad-bot/dinovision/resolve/main/environment/training-source/source-snapshot/src/camera.rs
- Command line
-
hf download hf://mad-bot/dinovision/environment/training-source/source-snapshot/src/camera.rs
-
curl -L -o camera.rs https://huggingface.co/mad-bot/dinovision/resolve/main/environment/training-source/source-snapshot/src/camera.rs
28.4 kB
| //! Quest passthrough camera, through the Camera2 NDK. | |
| //! | |
| //! Meta exposes the forward-facing cameras on Quest 3 and 3S as ordinary | |
| //! Camera2 devices, distinguished from the avatar cameras by vendor tags. | |
| //! Their own documentation covers the Kotlin path; this is the native one, | |
| //! because everything else here is Rust and bouncing frames through JNI to | |
| //! get them back into a Vulkan pipeline would be absurd. | |
| //! | |
| //! Requirements, all of which fail silently if missed: | |
| //! | |
| //! * `horizonos.permission.HEADSET_CAMERA` **granted at runtime**. Declaring | |
| //! it is not enough. For a POC: | |
| //! `adb shell pm grant rust.dinovision_xr horizonos.permission.HEADSET_CAMERA` | |
| //! * minSdk 34 and NDK r27 for the API-34 sysroot. Note that Horizon OS | |
| //! v205 does *not* export `ACameraManager_getTagFromName`, so Meta's | |
| //! vendor tags cannot be resolved by name; cameras are identified by | |
| //! lens facing and supported formats instead. | |
| //! * Passthrough enabled on the device. | |
| //! | |
| //! The camera delivers 1280×960 YUV420 at 60 Hz. The encoder wants a | |
| //! 224×224 RGB square, so [`FrameSource::next_frame`] centre-crops, | |
| //! box-downscales, and converts in one pass over the destination. | |
| use std::ffi::{c_char, c_int, c_void}; | |
| use std::sync::atomic::{AtomicBool, Ordering}; | |
| use std::time::{Duration, Instant}; | |
| use crate::source::FrameSource; | |
| /// How long to wait between reopen attempts. The camera stays disabled for | |
| /// as long as the headset is off, so retrying hard would just spin. | |
| const REOPEN_INTERVAL: Duration = Duration::from_secs(1); | |
| // --- Opaque handles ------------------------------------------------------- | |
| struct ACameraManager( [u8; 0]); | |
| struct ACameraDevice( [u8; 0]); | |
| struct ACameraMetadata( [u8; 0]); | |
| struct ACameraCaptureSession( [u8; 0]); | |
| struct ACaptureRequest( [u8; 0]); | |
| struct ACameraOutputTarget( [u8; 0]); | |
| struct ACaptureSessionOutput( [u8; 0]); | |
| struct ACaptureSessionOutputContainer( [u8; 0]); | |
| struct AImageReader( [u8; 0]); | |
| struct AImage( [u8; 0]); | |
| struct ANativeWindow( [u8; 0]); | |
| struct ACameraIdList { | |
| num_cameras: c_int, | |
| camera_ids: *mut *const c_char, | |
| } | |
| struct ACameraMetadataConstEntry { | |
| tag: u32, | |
| kind: u8, | |
| count: u32, | |
| data: *const u8, | |
| } | |
| struct ACameraDeviceStateCallbacks { | |
| context: *mut c_void, | |
| on_disconnected: extern "C" fn(*mut c_void, *mut ACameraDevice), | |
| on_error: extern "C" fn(*mut c_void, *mut ACameraDevice, c_int), | |
| } | |
| struct ACameraCaptureSessionStateCallbacks { | |
| context: *mut c_void, | |
| on_closed: extern "C" fn(*mut c_void, *mut ACameraCaptureSession), | |
| on_ready: extern "C" fn(*mut c_void, *mut ACameraCaptureSession), | |
| on_active: extern "C" fn(*mut c_void, *mut ACameraCaptureSession), | |
| } | |
| const AIMAGE_FORMAT_YUV_420_888: c_int = 0x23; | |
| const TEMPLATE_PREVIEW: c_int = 1; | |
| const ACAMERA_OK: c_int = 0; | |
| const AMEDIA_OK: c_int = 0; | |
| /// Standard metadata tags, section ordinal `<< 16` plus index. | |
| /// | |
| /// Meta's vendor tags would be nicer, but resolving a vendor tag by name | |
| /// needs `ACameraManager_getTagFromName`, and Horizon OS v205 does not | |
| /// export it — linking against it stops the whole library from loading | |
| /// with `UnsatisfiedLinkError`. Standard tags are all that is portable | |
| /// here, so the passthrough cameras get identified by what they can do | |
| /// rather than by what they are called. | |
| const ACAMERA_LENS_FACING: u32 = (8 << 16) + 5; | |
| const ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS: u32 = (13 << 16) + 10; | |
| /// The passthrough cameras look outward at the world, so they report | |
| /// `LENS_FACING_BACK`. The Quest also exposes a front-facing camera — | |
| /// the avatar one — which advertises the same resolutions but is | |
| /// separately restricted and denies `openCamera` outright. | |
| const LENS_FACING_BACK: u8 = 1; | |
| /// Camera2 error codes, so a failure says something rather than showing a | |
| /// bare negative number. | |
| fn camera_error(code: c_int) -> &'static str { | |
| match code { | |
| -10001 => "invalid parameter", | |
| -10002 => "camera disconnected", | |
| -10003 => "not enough memory", | |
| -10004 => "metadata not found", | |
| -10005 => "camera device error", | |
| -10006 => "camera service error", | |
| -10007 => "session closed", | |
| -10008 => "invalid operation", | |
| -10009 => "stream configure failed", | |
| -10010 => "camera in use", | |
| -10011 => "max cameras in use", | |
| -10012 => "camera disabled", | |
| -10013 => "permission denied", | |
| -10014 => "unsupported operation", | |
| _ => "unknown", | |
| } | |
| } | |
| unsafe extern "C" { | |
| fn ACameraManager_create() -> *mut ACameraManager; | |
| fn ACameraManager_delete(m: *mut ACameraManager); | |
| fn ACameraManager_getCameraIdList(m: *mut ACameraManager, out: *mut *mut ACameraIdList) -> c_int; | |
| fn ACameraManager_deleteCameraIdList(l: *mut ACameraIdList); | |
| fn ACameraManager_getCameraCharacteristics( | |
| m: *mut ACameraManager, | |
| id: *const c_char, | |
| out: *mut *mut ACameraMetadata, | |
| ) -> c_int; | |
| fn ACameraManager_openCamera( | |
| m: *mut ACameraManager, | |
| id: *const c_char, | |
| cb: *mut ACameraDeviceStateCallbacks, | |
| out: *mut *mut ACameraDevice, | |
| ) -> c_int; | |
| fn ACameraMetadata_getConstEntry( | |
| md: *const ACameraMetadata, | |
| tag: u32, | |
| entry: *mut ACameraMetadataConstEntry, | |
| ) -> c_int; | |
| fn ACameraMetadata_free(md: *mut ACameraMetadata); | |
| fn ACameraDevice_close(d: *mut ACameraDevice) -> c_int; | |
| fn ACameraDevice_createCaptureRequest( | |
| d: *mut ACameraDevice, | |
| template: c_int, | |
| out: *mut *mut ACaptureRequest, | |
| ) -> c_int; | |
| fn ACameraDevice_createCaptureSession( | |
| d: *mut ACameraDevice, | |
| outputs: *const ACaptureSessionOutputContainer, | |
| cb: *const ACameraCaptureSessionStateCallbacks, | |
| session: *mut *mut ACameraCaptureSession, | |
| ) -> c_int; | |
| fn ACaptureSessionOutputContainer_create( | |
| out: *mut *mut ACaptureSessionOutputContainer, | |
| ) -> c_int; | |
| fn ACaptureSessionOutputContainer_add( | |
| c: *mut ACaptureSessionOutputContainer, | |
| o: *const ACaptureSessionOutput, | |
| ) -> c_int; | |
| fn ACaptureSessionOutputContainer_free(c: *mut ACaptureSessionOutputContainer); | |
| fn ACaptureSessionOutput_create( | |
| w: *mut ANativeWindow, | |
| out: *mut *mut ACaptureSessionOutput, | |
| ) -> c_int; | |
| fn ACaptureSessionOutput_free(o: *mut ACaptureSessionOutput); | |
| fn ACameraOutputTarget_create(w: *mut ANativeWindow, out: *mut *mut ACameraOutputTarget) | |
| -> c_int; | |
| fn ACameraOutputTarget_free(t: *mut ACameraOutputTarget); | |
| fn ACaptureRequest_addTarget(r: *mut ACaptureRequest, t: *const ACameraOutputTarget) -> c_int; | |
| fn ACaptureRequest_free(r: *mut ACaptureRequest); | |
| fn ACameraCaptureSession_setRepeatingRequest( | |
| s: *mut ACameraCaptureSession, | |
| cb: *mut c_void, | |
| num: c_int, | |
| requests: *mut *mut ACaptureRequest, | |
| seq: *mut c_int, | |
| ) -> c_int; | |
| fn ACameraCaptureSession_close(s: *mut ACameraCaptureSession); | |
| } | |
| unsafe extern "C" { | |
| fn AImageReader_new( | |
| width: c_int, | |
| height: c_int, | |
| format: c_int, | |
| max_images: c_int, | |
| out: *mut *mut AImageReader, | |
| ) -> c_int; | |
| fn AImageReader_delete(r: *mut AImageReader); | |
| fn AImageReader_getWindow(r: *mut AImageReader, out: *mut *mut ANativeWindow) -> c_int; | |
| fn AImageReader_acquireLatestImage(r: *mut AImageReader, out: *mut *mut AImage) -> c_int; | |
| fn AImage_delete(i: *mut AImage); | |
| fn AImage_getWidth(i: *const AImage, out: *mut i32) -> c_int; | |
| fn AImage_getHeight(i: *const AImage, out: *mut i32) -> c_int; | |
| fn AImage_getPlaneRowStride(i: *const AImage, plane: c_int, out: *mut i32) -> c_int; | |
| fn AImage_getPlanePixelStride(i: *const AImage, plane: c_int, out: *mut i32) -> c_int; | |
| fn AImage_getPlaneData( | |
| i: *const AImage, | |
| plane: c_int, | |
| data: *mut *mut u8, | |
| len: *mut c_int, | |
| ) -> c_int; | |
| } | |
| /// Set from the device callbacks, cleared when the camera is reopened. | |
| /// | |
| /// A process-global rather than a per-camera flag because the callbacks | |
| /// take a raw context pointer and this app only ever opens one camera; | |
| /// threading an `Arc` through the FFI to support a second would be | |
| /// ceremony for a case that does not exist. | |
| static DEVICE_ERROR: AtomicBool = AtomicBool::new(false); | |
| extern "C" fn on_disconnected(_ctx: *mut c_void, _d: *mut ACameraDevice) { | |
| log::warn!("camera disconnected"); | |
| DEVICE_ERROR.store(true, Ordering::Release); | |
| } | |
| extern "C" fn on_error(_ctx: *mut c_void, _d: *mut ACameraDevice, err: c_int) { | |
| // Code 3 is ERROR_CAMERA_DISABLED, which Horizon OS raises whenever the | |
| // headset comes off. It is routine rather than exceptional here, so the | |
| // camera reopens instead of the view freezing on its last frame. | |
| log::warn!( | |
| "camera device error {err}{}", | |
| if err == 3 { " (disabled — headset removed?)" } else { "" } | |
| ); | |
| DEVICE_ERROR.store(true, Ordering::Release); | |
| } | |
| extern "C" fn on_session(_ctx: *mut c_void, _s: *mut ACameraCaptureSession) {} | |
| /// Which of the two forward-facing cameras to read. | |
| pub enum Eye { | |
| Left, | |
| Right, | |
| } | |
| impl Eye { | |
| fn position(self) -> u8 { | |
| match self { | |
| Eye::Left => 0, | |
| Eye::Right => 1, | |
| } | |
| } | |
| } | |
| /// A live passthrough camera delivering square RGB frames. | |
| pub struct PassthroughCamera { | |
| manager: *mut ACameraManager, | |
| device: *mut ACameraDevice, | |
| session: *mut ACameraCaptureSession, | |
| request: *mut ACaptureRequest, | |
| target: *mut ACameraOutputTarget, | |
| output: *mut ACaptureSessionOutput, | |
| container: *mut ACaptureSessionOutputContainer, | |
| reader: *mut AImageReader, | |
| size: usize, | |
| buf: Vec<u8>, | |
| have_frame: bool, | |
| /// Kept so the camera can be reopened after the device errors. | |
| eye: Eye, | |
| capture: (i32, i32), | |
| last_reopen: Instant, | |
| } | |
| // The handles are only touched from the thread that owns the struct; the | |
| // NDK does not pin them to a thread. | |
| unsafe impl Send for PassthroughCamera {} | |
| impl PassthroughCamera { | |
| /// Open the passthrough camera for one eye at `size × size` output. | |
| /// | |
| /// `capture` is the sensor resolution to request — 1280×960 is what the | |
| /// Quest 3/3S offer. | |
| pub fn new(size: usize, eye: Eye, capture: (i32, i32)) -> Result<Self, String> { | |
| unsafe { | |
| let manager = ACameraManager_create(); | |
| if manager.is_null() { | |
| return Err("ACameraManager_create returned null".into()); | |
| } | |
| let mut list: *mut ACameraIdList = std::ptr::null_mut(); | |
| if ACameraManager_getCameraIdList(manager, &mut list) != ACAMERA_OK || list.is_null() { | |
| ACameraManager_delete(manager); | |
| return Err("could not enumerate cameras — is HEADSET_CAMERA granted?".into()); | |
| } | |
| let ids = std::slice::from_raw_parts((*list).camera_ids, (*list).num_cameras as usize); | |
| log::info!("{} cameras visible", ids.len()); | |
| let candidates = Self::candidates(manager, ids, eye, capture); | |
| if candidates.is_empty() { | |
| ACameraManager_deleteCameraIdList(list); | |
| ACameraManager_delete(manager); | |
| return Err("no outward-facing camera offers the requested format".into()); | |
| } | |
| // --- Reader and its surface --- | |
| let mut reader: *mut AImageReader = std::ptr::null_mut(); | |
| if AImageReader_new( | |
| capture.0, | |
| capture.1, | |
| AIMAGE_FORMAT_YUV_420_888, | |
| // A small queue: we always take the newest frame and drop | |
| // the rest, so depth only adds latency. | |
| 4, | |
| &mut reader, | |
| ) != AMEDIA_OK | |
| { | |
| ACameraManager_deleteCameraIdList(list); | |
| ACameraManager_delete(manager); | |
| return Err("AImageReader_new failed".into()); | |
| } | |
| let mut window: *mut ANativeWindow = std::ptr::null_mut(); | |
| AImageReader_getWindow(reader, &mut window); | |
| // --- Open the device --- | |
| let mut callbacks = ACameraDeviceStateCallbacks { | |
| context: std::ptr::null_mut(), | |
| on_disconnected, | |
| on_error, | |
| }; | |
| // Try each candidate in preference order. Which camera id maps | |
| // to which physical sensor is not documented, and some are | |
| // restricted in ways that only surface at open time, so trying | |
| // beats predicting. | |
| let mut device: *mut ACameraDevice = std::ptr::null_mut(); | |
| let mut last = ACAMERA_OK; | |
| for &id in &candidates { | |
| let status = ACameraManager_openCamera(manager, id, &mut callbacks, &mut device); | |
| if status == ACAMERA_OK && !device.is_null() { | |
| log::info!("opened camera {:?}", std::ffi::CStr::from_ptr(id)); | |
| break; | |
| } | |
| log::warn!( | |
| "camera {:?} would not open: {} ({status})", | |
| std::ffi::CStr::from_ptr(id), | |
| camera_error(status) | |
| ); | |
| last = status; | |
| device = std::ptr::null_mut(); | |
| } | |
| ACameraManager_deleteCameraIdList(list); | |
| if device.is_null() { | |
| AImageReader_delete(reader); | |
| ACameraManager_delete(manager); | |
| return Err(format!( | |
| "no camera would open; last error {} ({last}){}", | |
| camera_error(last), | |
| if last == -10013 { | |
| " — HEADSET_CAMERA is a runtime permission and must be \ | |
| granted, and passthrough must be enabled" | |
| } else { | |
| "" | |
| } | |
| )); | |
| } | |
| // --- Session and repeating request --- | |
| let mut container: *mut ACaptureSessionOutputContainer = std::ptr::null_mut(); | |
| ACaptureSessionOutputContainer_create(&mut container); | |
| let mut output: *mut ACaptureSessionOutput = std::ptr::null_mut(); | |
| ACaptureSessionOutput_create(window, &mut output); | |
| ACaptureSessionOutputContainer_add(container, output); | |
| let session_cb = ACameraCaptureSessionStateCallbacks { | |
| context: std::ptr::null_mut(), | |
| on_closed: on_session, | |
| on_ready: on_session, | |
| on_active: on_session, | |
| }; | |
| let mut session: *mut ACameraCaptureSession = std::ptr::null_mut(); | |
| if ACameraDevice_createCaptureSession(device, container, &session_cb, &mut session) | |
| != ACAMERA_OK | |
| { | |
| return Err("createCaptureSession failed".into()); | |
| } | |
| let mut request: *mut ACaptureRequest = std::ptr::null_mut(); | |
| ACameraDevice_createCaptureRequest(device, TEMPLATE_PREVIEW, &mut request); | |
| let mut target: *mut ACameraOutputTarget = std::ptr::null_mut(); | |
| ACameraOutputTarget_create(window, &mut target); | |
| ACaptureRequest_addTarget(request, target); | |
| let mut requests = [request]; | |
| if ACameraCaptureSession_setRepeatingRequest( | |
| session, | |
| std::ptr::null_mut(), | |
| 1, | |
| requests.as_mut_ptr(), | |
| std::ptr::null_mut(), | |
| ) != ACAMERA_OK | |
| { | |
| return Err("setRepeatingRequest failed".into()); | |
| } | |
| log::info!( | |
| "passthrough camera streaming {}x{} -> {size}x{size}", | |
| capture.0, | |
| capture.1 | |
| ); | |
| Ok(Self { | |
| manager, | |
| device, | |
| session, | |
| request, | |
| target, | |
| output, | |
| container, | |
| reader, | |
| size, | |
| // Mid-grey until the first frame lands, so a stalled camera | |
| // is visibly "no data" rather than black. | |
| buf: vec![128; size * size * 3], | |
| have_frame: false, | |
| eye, | |
| capture, | |
| last_reopen: Instant::now(), | |
| }) | |
| } | |
| } | |
| /// Find the passthrough camera for the requested eye. | |
| /// | |
| /// Meta identifies these with vendor tags, but resolving a vendor tag | |
| /// needs `ACameraManager_getTagFromName`, which Horizon OS v205 does not | |
| /// export — and merely *linking* it prevents the library from loading at | |
| /// all. So the cameras are identified by capability instead: the | |
| /// passthrough pair are the ones offering the requested YUV420 size. | |
| /// Among those, the list order is left then right, matching how Meta | |
| /// numbers them. | |
| unsafe fn candidates( | |
| manager: *mut ACameraManager, | |
| ids: &[*const c_char], | |
| eye: Eye, | |
| capture: (i32, i32), | |
| ) -> Vec<*const c_char> { | |
| unsafe { | |
| let mut matching = Vec::new(); | |
| for &id in ids { | |
| let mut md: *mut ACameraMetadata = std::ptr::null_mut(); | |
| if ACameraManager_getCameraCharacteristics(manager, id, &mut md) != ACAMERA_OK { | |
| continue; | |
| } | |
| let mut entry = ACameraMetadataConstEntry { | |
| tag: 0, | |
| kind: 0, | |
| count: 0, | |
| data: std::ptr::null(), | |
| }; | |
| let facing = (ACameraMetadata_getConstEntry(md, ACAMERA_LENS_FACING, &mut entry) | |
| == ACAMERA_OK | |
| && entry.count > 0) | |
| .then(|| *entry.data); | |
| let mut supported = false; | |
| let mut entry = ACameraMetadataConstEntry { | |
| tag: 0, | |
| kind: 0, | |
| count: 0, | |
| data: std::ptr::null(), | |
| }; | |
| if ACameraMetadata_getConstEntry( | |
| md, | |
| ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, | |
| &mut entry, | |
| ) == ACAMERA_OK | |
| && !entry.data.is_null() | |
| { | |
| // int32[n * 4]: format, width, height, is-input. | |
| let values = | |
| std::slice::from_raw_parts(entry.data as *const i32, entry.count as usize); | |
| supported = values.chunks_exact(4).any(|c| { | |
| c[0] == AIMAGE_FORMAT_YUV_420_888 | |
| && c[1] == capture.0 | |
| && c[2] == capture.1 | |
| && c[3] == 0 | |
| }); | |
| } | |
| ACameraMetadata_free(md); | |
| log::info!( | |
| "camera {:?}: facing={facing:?} offers {}x{} YUV420: {supported}", | |
| std::ffi::CStr::from_ptr(id), | |
| capture.0, | |
| capture.1 | |
| ); | |
| if supported { | |
| matching.push((id, facing)); | |
| } | |
| } | |
| // Outward-facing first, and within that the requested eye | |
| // first — but keep the rest as fallbacks, since the mapping | |
| // from camera id to physical sensor is undocumented. | |
| let (mut outward, inward): (Vec<_>, Vec<_>) = matching | |
| .into_iter() | |
| .partition(|&(_, facing)| facing == Some(LENS_FACING_BACK)); | |
| let wanted = eye.position() as usize; | |
| if wanted < outward.len() { | |
| outward.swap(0, wanted); | |
| } | |
| let ordered: Vec<*const c_char> = outward | |
| .into_iter() | |
| .chain(inward) | |
| .map(|(id, _)| id) | |
| .collect(); | |
| if ordered.is_empty() { | |
| log::warn!( | |
| "no camera advertises {}x{} YUV420; trying all of them", | |
| capture.0, | |
| capture.1 | |
| ); | |
| return ids.to_vec(); | |
| } | |
| ordered | |
| } | |
| } | |
| /// Pull the newest frame, converting YUV420 to a square RGB crop. | |
| fn pump(&mut self) -> bool { | |
| unsafe { | |
| let mut image: *mut AImage = std::ptr::null_mut(); | |
| if AImageReader_acquireLatestImage(self.reader, &mut image) != AMEDIA_OK | |
| || image.is_null() | |
| { | |
| return false; | |
| } | |
| let (mut w, mut h) = (0i32, 0i32); | |
| AImage_getWidth(image, &mut w); | |
| AImage_getHeight(image, &mut h); | |
| let plane = |index: c_int| -> Option<(*mut u8, usize, usize)> { | |
| let (mut data, mut len) = (std::ptr::null_mut(), 0); | |
| if AImage_getPlaneData(image, index, &mut data, &mut len) != AMEDIA_OK { | |
| return None; | |
| } | |
| let (mut row, mut pixel) = (0i32, 0i32); | |
| AImage_getPlaneRowStride(image, index, &mut row); | |
| AImage_getPlanePixelStride(image, index, &mut pixel); | |
| Some((data, row as usize, pixel.max(1) as usize)) | |
| }; | |
| let (Some((y_data, y_row, _)), Some((u_data, u_row, u_pix)), Some((v_data, v_row, v_pix))) = | |
| (plane(0), plane(1), plane(2)) | |
| else { | |
| AImage_delete(image); | |
| return false; | |
| }; | |
| yuv420_to_square_rgb( | |
| YuvPlanes { | |
| y: y_data, | |
| y_row, | |
| u: u_data, | |
| u_row, | |
| u_pix, | |
| v: v_data, | |
| v_row, | |
| v_pix, | |
| }, | |
| w as usize, | |
| h as usize, | |
| self.size, | |
| &mut self.buf, | |
| ); | |
| AImage_delete(image); | |
| self.have_frame = true; | |
| true | |
| } | |
| } | |
| } | |
| impl Drop for PassthroughCamera { | |
| fn drop(&mut self) { | |
| unsafe { | |
| if !self.session.is_null() { | |
| ACameraCaptureSession_close(self.session); | |
| } | |
| if !self.request.is_null() { | |
| ACaptureRequest_free(self.request); | |
| } | |
| if !self.target.is_null() { | |
| ACameraOutputTarget_free(self.target); | |
| } | |
| if !self.container.is_null() { | |
| ACaptureSessionOutputContainer_free(self.container); | |
| } | |
| if !self.output.is_null() { | |
| ACaptureSessionOutput_free(self.output); | |
| } | |
| if !self.device.is_null() { | |
| ACameraDevice_close(self.device); | |
| } | |
| if !self.reader.is_null() { | |
| AImageReader_delete(self.reader); | |
| } | |
| if !self.manager.is_null() { | |
| ACameraManager_delete(self.manager); | |
| } | |
| } | |
| } | |
| } | |
| impl FrameSource for PassthroughCamera { | |
| fn size(&self) -> usize { | |
| self.size | |
| } | |
| fn next_frame(&mut self) -> Option<&[u8]> { | |
| // Taking the headset off disables the camera, which is a normal | |
| // thing to do rather than a fatal one. Reopen instead of freezing | |
| // on the last frame forever. | |
| if DEVICE_ERROR.load(Ordering::Acquire) | |
| && self.last_reopen.elapsed() >= REOPEN_INTERVAL | |
| { | |
| self.last_reopen = Instant::now(); | |
| match Self::new(self.size, self.eye, self.capture) { | |
| Ok(mut fresh) => { | |
| // Carry the last good frame across so the view does not | |
| // flash grey on every reopen. | |
| std::mem::swap(&mut fresh.buf, &mut self.buf); | |
| fresh.have_frame = self.have_frame; | |
| DEVICE_ERROR.store(false, Ordering::Release); | |
| // Dropping the old value closes the previous handles. | |
| *self = fresh; | |
| log::info!("camera reopened"); | |
| } | |
| Err(e) => log::warn!("camera reopen failed: {e}"), | |
| } | |
| } | |
| // A frame may not have arrived since the last call; showing the | |
| // previous one beats stalling the pipeline. | |
| self.pump(); | |
| self.have_frame.then_some(&self.buf[..]) | |
| } | |
| } | |
| struct YuvPlanes { | |
| y: *mut u8, | |
| y_row: usize, | |
| u: *mut u8, | |
| u_row: usize, | |
| u_pix: usize, | |
| v: *mut u8, | |
| v_row: usize, | |
| v_pix: usize, | |
| } | |
| /// Resample the *whole* YUV420 frame into a square RGB buffer. | |
| /// | |
| /// Deliberately not a centre crop. The encoder needs a square, but cropping | |
| /// 1280×960 to 960×960 throws away a quarter of the horizontal field of | |
| /// view, and the point of this app is to show what the camera saw. Squashing | |
| /// the full frame instead keeps all of it; the aspect is restored at display | |
| /// time by drawing the quad at the camera's real 4:3 shape, so nothing ends | |
| /// up stretched on screen. DINO sees a horizontally compressed image, which | |
| /// costs a little feature quality and is a far better trade than losing the | |
| /// periphery. | |
| /// | |
| /// Box-averages luma over each destination pixel's source footprint — | |
| /// point-sampling 960 down to 224 aliases badly enough to change what the | |
| /// patch embedding sees. Chroma is sampled at the centre, which is | |
| /// imperceptible after a 4× reduction and halves the work. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The plane pointers must be valid for the given strides and dimensions. | |
| unsafe fn yuv420_to_square_rgb( | |
| p: YuvPlanes, | |
| width: usize, | |
| height: usize, | |
| size: usize, | |
| out: &mut [u8], | |
| ) { | |
| for oy in 0..size { | |
| let sy0 = oy * height / size; | |
| let sy1 = ((oy + 1) * height / size).max(sy0 + 1).min(height); | |
| for ox in 0..size { | |
| let sx0 = ox * width / size; | |
| let sx1 = ((ox + 1) * width / size).max(sx0 + 1).min(width); | |
| let mut acc = 0u32; | |
| let mut n = 0u32; | |
| for sy in sy0..sy1 { | |
| let row = unsafe { p.y.add(sy * p.y_row) }; | |
| for sx in sx0..sx1 { | |
| acc += unsafe { *row.add(sx) } as u32; | |
| n += 1; | |
| } | |
| } | |
| let luma = (acc / n.max(1)) as f32; | |
| // Chroma planes are half resolution in both axes. | |
| let cx = ((sx0 + sx1) / 2) / 2; | |
| let cy = ((sy0 + sy1) / 2) / 2; | |
| let u = unsafe { *p.u.add(cy * p.u_row + cx * p.u_pix) } as f32 - 128.0; | |
| let v = unsafe { *p.v.add(cy * p.v_row + cx * p.v_pix) } as f32 - 128.0; | |
| // BT.601, which is what Camera2 delivers. | |
| let r = luma + 1.402 * v; | |
| let g = luma - 0.344_136 * u - 0.714_136 * v; | |
| let b = luma + 1.772 * u; | |
| let o = (oy * size + ox) * 3; | |
| out[o] = r.clamp(0.0, 255.0) as u8; | |
| out[o + 1] = g.clamp(0.0, 255.0) as u8; | |
| out[o + 2] = b.clamp(0.0, 255.0) as u8; | |
| } | |
| } | |
| } | |