From 232670147954f49164f0c9cba69f62bd0f4de5c8 Mon Sep 17 00:00:00 2001 From: vsem-azamat Date: Sun, 28 Dec 2025 15:54:15 +0100 Subject: [PATCH 01/29] feat(vdo): Add safe Rust bindings for VDO API Safe wrappers over vdo-sys with builder pattern for stream creation, iterator-based frame capture, and automatic resource cleanup. Tested on device: YUV, JPEG, H.264, H.265 formats work correctly. --- Cargo.lock | 15 +- Cargo.toml | 1 + apps/vdo_encode_client/Cargo.toml | 2 +- apps/vdo_encode_client/src/main.rs | 115 ++- crates/vdo/Cargo.toml | 23 + crates/vdo/examples/basic.rs | 42 + crates/vdo/src/lib.rs | 1359 ++++++++++++++++++++++++++++ 7 files changed, 1541 insertions(+), 16 deletions(-) create mode 100644 crates/vdo/Cargo.toml create mode 100644 crates/vdo/examples/basic.rs create mode 100644 crates/vdo/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ba9c3854..3a4ffe3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3157,6 +3157,19 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vdo" +version = "0.0.0" +dependencies = [ + "anyhow", + "env_logger", + "glib-sys", + "gobject-sys", + "log", + "thiserror 1.0.69", + "vdo-sys", +] + [[package]] name = "vdo-sys" version = "0.0.0" @@ -3173,7 +3186,7 @@ version = "0.0.0" dependencies = [ "acap-logging", "log", - "vdo-sys", + "vdo", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index df39627d..1501721c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ licensekey-sys = { path = "crates/licensekey-sys" } mdb = { path = "crates/mdb" } mdb-sys = { path = "crates/mdb-sys" } vdo-sys = { path = "crates/vdo-sys" } +vdo = { path = "crates/vdo" } [workspace.package] edition = "2021" diff --git a/apps/vdo_encode_client/Cargo.toml b/apps/vdo_encode_client/Cargo.toml index 03857640..da9a62cd 100644 --- a/apps/vdo_encode_client/Cargo.toml +++ b/apps/vdo_encode_client/Cargo.toml @@ -8,4 +8,4 @@ publish = false log = { workspace = true } acap-logging = { workspace = true } -vdo-sys = { workspace = true } +vdo = { workspace = true } diff --git a/apps/vdo_encode_client/src/main.rs b/apps/vdo_encode_client/src/main.rs index 25439cce..fc500ff8 100644 --- a/apps/vdo_encode_client/src/main.rs +++ b/apps/vdo_encode_client/src/main.rs @@ -1,20 +1,107 @@ -//! This application is a basic VDO type of application. +//! VDO Example Application //! -//! The application starts a VDO stream and illustrates how to continuously capture frames from the -//! VDO service, access the received buffer contents, as well as the frame metadata. +//! This application demonstrates the VDO (Video Capture) API by capturing +//! frames from the camera in various formats. //! -//! # Arguments -//! -//! - `format`: A string describing the video compression format. -//! Possible values are `h264` (default), `h265`, `jpeg`, `nv12`, and `y800`. -//! - `frames`: An integer specifying the number of captured frames. -//! - `output`: The output filename. -//! -use log::info; +//! It tests: +//! - Stream creation with different formats (YUV, JPEG, H.264) +//! - Frame capture and metadata access +//! - Proper resource cleanup + +use log::{error, info}; +use vdo::{Error, Stream, VdoFormat}; + +fn test_format(name: &str, format: VdoFormat, num_frames: usize) -> Result<(), Error> { + info!("=== Testing {} format ===", name); + + let mut stream = Stream::builder() + .channel(0) + .format(format) + .resolution(640, 480) + .framerate(15) + .build()?; + + info!("{}: Stream created successfully", name); + + // Get stream info + if let Ok(stream_info) = stream.info() { + info!("{}: Stream info:", name); + stream_info.dump(); + } + + let mut running = stream.start()?; + info!("{}: Stream started", name); + + for (i, buffer) in running.iter().take(num_frames).enumerate() { + let frame = buffer.frame()?; + let size = frame.size(); + let seq = frame.sequence_number(); + let ts = frame.timestamp(); + + info!( + "{}: Frame {}: {} bytes, seq={}, timestamp={}us", + name, i, size, seq, ts + ); + + // For JPEG, verify magic bytes + if format == VdoFormat::VDO_FORMAT_JPEG { + let data = buffer.as_slice()?; + if data.len() >= 2 && data[0] == 0xFF && data[1] == 0xD8 { + info!("{}: Frame {} has valid JPEG header", name, i); + } else { + error!("{}: Frame {} has INVALID JPEG header!", name, i); + } + } + } + + running.stop()?; + info!("{}: Stream stopped successfully", name); + info!(""); + + Ok(()) +} fn main() { acap_logging::init_logger(); - unsafe { assert!(!vdo_sys::vdo_map_new().is_null()) }; - info!("vdo map created"); - todo!("Implement the real example") + + info!("VDO Example Application starting..."); + info!("Testing VDO safe Rust bindings"); + info!(""); + + // Test YUV (most portable format) + match test_format("YUV", VdoFormat::VDO_FORMAT_YUV, 5) { + Ok(()) => info!("YUV test: PASSED"), + Err(e) => error!("YUV test: FAILED - {}", e), + } + + // Test JPEG + match test_format("JPEG", VdoFormat::VDO_FORMAT_JPEG, 5) { + Ok(()) => info!("JPEG test: PASSED"), + Err(e) => error!("JPEG test: FAILED - {}", e), + } + + // Test H.264 + match test_format("H.264", VdoFormat::VDO_FORMAT_H264, 10) { + Ok(()) => info!("H.264 test: PASSED"), + Err(e) => error!("H.264 test: FAILED - {}", e), + } + + // Test H.265 (might not be supported on all platforms) + match test_format("H.265", VdoFormat::VDO_FORMAT_H265, 5) { + Ok(()) => info!("H.265 test: PASSED"), + Err(e) => { + if let Error::Vdo(ref vdo_err) = e { + if vdo_err.code_name() == "VDO_ERROR_NOT_SUPPORTED" { + info!("H.265 test: SKIPPED (not supported on this platform)"); + } else { + error!("H.265 test: FAILED - {}", e); + } + } else { + error!("H.265 test: FAILED - {}", e); + } + } + } + + info!(""); + info!("VDO Example Application completed!"); } diff --git a/crates/vdo/Cargo.toml b/crates/vdo/Cargo.toml new file mode 100644 index 00000000..22b49739 --- /dev/null +++ b/crates/vdo/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "vdo" +version = "0.0.0" +edition.workspace = true +license = "MIT" +description = "Safe Rust bindings for the VDO (Video Capture) API" + +[dependencies] +vdo-sys = { workspace = true } +log = { workspace = true } +glib-sys = { workspace = true } +gobject-sys = { workspace = true } +thiserror = { workspace = true } + +[features] +device-tests = [] + +[dev-dependencies] +anyhow = { workspace = true } +env_logger = { workspace = true } + +[[example]] +name = "basic" diff --git a/crates/vdo/examples/basic.rs b/crates/vdo/examples/basic.rs new file mode 100644 index 00000000..87dd2101 --- /dev/null +++ b/crates/vdo/examples/basic.rs @@ -0,0 +1,42 @@ +//! Basic example of using VDO to capture video frames. +//! +//! This example creates a video stream, captures a few frames, and prints +//! information about each frame. + +use vdo::{Stream, VdoFormat}; + +fn main() -> Result<(), Box> { + // Initialize logging (optional) + env_logger::init(); + + println!("Creating video stream..."); + + // Create a stream with YUV format (most portable across platforms) + let mut stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(640, 480) + .framerate(15) + .build()?; + + println!("Starting stream..."); + let mut running = stream.start()?; + + println!("Capturing frames..."); + for (i, buffer) in running.iter().take(10).enumerate() { + let frame = buffer.frame()?; + println!( + "Frame {}: {} bytes, seq={}, timestamp={}us", + i, + frame.size(), + frame.sequence_number(), + frame.timestamp() + ); + } + + println!("Stopping stream..."); + running.stop()?; + + println!("Done!"); + Ok(()) +} diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs new file mode 100644 index 00000000..2cb066ec --- /dev/null +++ b/crates/vdo/src/lib.rs @@ -0,0 +1,1359 @@ +//! Safe Rust bindings for the [VDO (Video Capture) API](https://axiscommunications.github.io/acap-documentation/docs/api/src/api/vdostream/html/index.html). +//! +//! VDO provides access to video streams from Axis cameras, supporting various +//! video formats including H.264, H.265, JPEG, and raw YUV/RGB formats. +//! +//! # Platform Compatibility +//! +//! Video format support varies by hardware platform: +//! +//! | Format | Artpec-6 | Artpec-7 | Artpec-8 | Artpec-9 | Ambarella CV | +//! |--------|----------|----------|----------|----------|--------------| +//! | H.264 | Yes | Yes | Yes | Yes | Yes | +//! | H.265 | No | Yes | Yes | Yes | Yes | +//! | JPEG | Yes | Yes | Yes | Yes | Yes | +//! | YUV (NV12, Y800) | Yes | Yes | Yes | Yes | Yes | +//! | RGB | No | No | No | Yes | Yes | +//! | PLANAR_RGB | No | No | Yes | Yes | Yes | +//! | AV1 | No | No | No | Yes | No | +//! +//! For maximum portability, use `VdoFormat::VDO_FORMAT_YUV`. +//! +//! # Example +//! +//! ```no_run +//! use vdo::{Stream, VdoFormat}; +//! +//! let mut stream = Stream::builder() +//! .channel(0) +//! .format(VdoFormat::VDO_FORMAT_YUV) +//! .resolution(1920, 1080) +//! .build() +//! .expect("Failed to create stream"); +//! +//! let mut running = stream.start().expect("Failed to start stream"); +//! +//! for buffer in running.iter().take(10) { +//! let frame = buffer.frame().expect("Failed to get frame"); +//! println!("Frame size: {} bytes", frame.size()); +//! } +//! +//! running.stop().expect("Failed to stop stream"); +//! ``` +//! +//! # Known Issues +//! +//! - Image rotation may vary between platforms. Check the `rotation` property in stream info. +//! - Some formats (RGB, PLANAR_RGB) may produce upside-down images on certain platforms. + +use glib_sys::GError; +use gobject_sys::{g_object_unref, GObject}; +use std::ffi::{CStr, CString}; +use std::fmt::{Debug, Display}; +use std::marker::PhantomData; +use std::mem; +use std::ptr; +use vdo_sys::*; + +// Re-export commonly used types from vdo-sys +pub use vdo_sys::{ + VdoBufferStrategy, VdoFormat, VdoFrameType, VdoRateControlMode, VdoRateControlPriority, +}; + +/// Macro for calling VDO functions that take a GError** parameter. +/// Returns a tuple of (result, Option). +macro_rules! try_func { + ($func:ident $(,)?) => {{ + let mut error: *mut GError = ptr::null_mut(); + let success = $func(&mut error); + if error.is_null() { + (success, None) + } else { + (success, Some(Error::Vdo(VdoError::from_gerror(error)))) + } + }}; + ($func:ident, $($arg:expr),+ $(,)?) => {{ + let mut error: *mut GError = ptr::null_mut(); + let success = $func($( $arg ),+, &mut error); + if error.is_null() { + (success, None) + } else { + (success, Some(Error::Vdo(VdoError::from_gerror(error)))) + } + }}; +} + +// ============================================================================ +// Error types +// ============================================================================ + +/// Error type for VDO operations. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Error returned by the VDO library. + #[error(transparent)] + Vdo(#[from] VdoError), + /// VDO returned an unexpected null pointer. + #[error("VDO returned an unexpected null pointer")] + NullPointer, + /// Could not allocate memory for CString. + #[error("Could not allocate memory for CString")] + CStringAllocation, + /// Missing error data from VDO library. + #[error("Missing error data from VDO library")] + MissingVdoError, + /// No buffers are allocated for the stream. + #[error("No buffers are allocated for the stream")] + NoBuffersAllocated, +} + +/// Result type for VDO operations. +pub type Result = std::result::Result; + +/// Error from the VDO library. +#[derive(Default)] +pub struct VdoError { + code: i32, + message: String, +} + +impl VdoError { + fn from_gerror(gerror: *mut GError) -> Self { + if gerror.is_null() { + return VdoError::default(); + } + + let g_error = unsafe { *gerror }; + let message = if g_error.message.is_null() { + String::from("Unknown error") + } else { + unsafe { CStr::from_ptr(g_error.message) } + .to_str() + .unwrap_or("Invalid UTF-8 in error message") + .to_string() + }; + + // Free the GError + unsafe { glib_sys::g_error_free(gerror) }; + + VdoError { + code: g_error.code, + message, + } + } + + /// Returns the error code name. + pub fn code_name(&self) -> &'static str { + let code = self.code as u32; + match code { + x if x == VDO_ERROR_NOT_FOUND.0 => "VDO_ERROR_NOT_FOUND", + x if x == VDO_ERROR_EXISTS.0 => "VDO_ERROR_EXISTS", + x if x == VDO_ERROR_INVALID_ARGUMENT.0 => "VDO_ERROR_INVALID_ARGUMENT", + x if x == VDO_ERROR_PERMISSION_DENIED.0 => "VDO_ERROR_PERMISSION_DENIED", + x if x == VDO_ERROR_NOT_SUPPORTED.0 => "VDO_ERROR_NOT_SUPPORTED", + x if x == VDO_ERROR_CLOSED.0 => "VDO_ERROR_CLOSED", + x if x == VDO_ERROR_BUSY.0 => "VDO_ERROR_BUSY", + x if x == VDO_ERROR_IO.0 => "VDO_ERROR_IO", + x if x == VDO_ERROR_HAL.0 => "VDO_ERROR_HAL", + x if x == VDO_ERROR_DBUS.0 => "VDO_ERROR_DBUS", + x if x == VDO_ERROR_OOM.0 => "VDO_ERROR_OOM", + x if x == VDO_ERROR_IDLE.0 => "VDO_ERROR_IDLE", + x if x == VDO_ERROR_NO_DATA.0 => "VDO_ERROR_NO_DATA", + x if x == VDO_ERROR_NO_BUFFER_SPACE.0 => "VDO_ERROR_NO_BUFFER_SPACE", + x if x == VDO_ERROR_BUFFER_FAILURE.0 => "VDO_ERROR_BUFFER_FAILURE", + x if x == VDO_ERROR_INTERFACE_DOWN.0 => "VDO_ERROR_INTERFACE_DOWN", + x if x == VDO_ERROR_FAILED.0 => "VDO_ERROR_FAILED", + x if x == VDO_ERROR_FATAL.0 => "VDO_ERROR_FATAL", + x if x == VDO_ERROR_NOT_CONTROLLED.0 => "VDO_ERROR_NOT_CONTROLLED", + x if x == VDO_ERROR_NO_EVENT.0 => "VDO_ERROR_NO_EVENT", + _ => "VDO_ERROR_UNKNOWN", + } + } + + /// Returns the numeric error code. + pub fn code(&self) -> i32 { + self.code + } + + /// Returns the error message. + pub fn message(&self) -> &str { + &self.message + } +} + +impl Display for VdoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} ({}): {}", self.code_name(), self.code, self.message) + } +} + +impl Debug for VdoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VdoError") + .field("code", &self.code) + .field("code_name", &self.code_name()) + .field("message", &self.message) + .finish() + } +} + +impl std::error::Error for VdoError {} + +// ============================================================================ +// Map - VDO settings/configuration container +// ============================================================================ + +/// A key-value map for VDO settings. +/// +/// Used to configure stream parameters and retrieve stream information. +pub struct Map { + raw: *mut VdoMap, +} + +impl Map { + /// Creates a new empty map. + pub fn new() -> Result { + let map = unsafe { vdo_map_new() }; + if map.is_null() { + Err(Error::NullPointer) + } else { + Ok(Self { raw: map }) + } + } + + /// Sets a 32-bit unsigned integer value. + pub fn set_u32(&self, key: &str, value: u32) -> Result<()> { + let key_cstr = CString::new(key).map_err(|_| Error::CStringAllocation)?; + unsafe { + vdo_map_set_uint32(self.raw, key_cstr.as_ptr(), value); + } + Ok(()) + } + + /// Gets a 32-bit unsigned integer value. + pub fn get_u32(&self, key: &str, default: u32) -> Result { + let key_cstr = CString::new(key).map_err(|_| Error::CStringAllocation)?; + let value = unsafe { vdo_map_get_uint32(self.raw, key_cstr.as_ptr(), default) }; + Ok(value) + } + + /// Sets a string value. + pub fn set_string(&self, key: &str, value: &str) -> Result<()> { + let key_cstr = CString::new(key).map_err(|_| Error::CStringAllocation)?; + let value_cstr = CString::new(value).map_err(|_| Error::CStringAllocation)?; + unsafe { + vdo_map_set_string(self.raw, key_cstr.as_ptr(), value_cstr.as_ptr()); + } + Ok(()) + } + + /// Gets a string value, returning an owned copy. + /// + /// Returns `None` if the key doesn't exist or the value is null. + pub fn get_string(&self, key: &str) -> Result> { + let key_cstr = CString::new(key).map_err(|_| Error::CStringAllocation)?; + let ptr = unsafe { vdo_map_dup_string(self.raw, key_cstr.as_ptr(), ptr::null()) }; + if ptr.is_null() { + return Ok(None); + } + let cstr = unsafe { CStr::from_ptr(ptr) }; + let result = cstr.to_str().map(|s| s.to_owned()).ok(); + unsafe { glib_sys::g_free(ptr as *mut _) }; + Ok(result) + } + + /// Sets a boolean value. + pub fn set_bool(&self, key: &str, value: bool) -> Result<()> { + let key_cstr = CString::new(key).map_err(|_| Error::CStringAllocation)?; + let gvalue = if value { + glib_sys::GTRUE + } else { + glib_sys::GFALSE + }; + unsafe { + vdo_map_set_boolean(self.raw, key_cstr.as_ptr(), gvalue); + } + Ok(()) + } + + /// Gets a boolean value. + pub fn get_bool(&self, key: &str, default: bool) -> Result { + let key_cstr = CString::new(key).map_err(|_| Error::CStringAllocation)?; + let gdefault = if default { + glib_sys::GTRUE + } else { + glib_sys::GFALSE + }; + let value = unsafe { vdo_map_get_boolean(self.raw, key_cstr.as_ptr(), gdefault) }; + Ok(value != glib_sys::GFALSE) + } + + /// Dumps the map contents to stdout (for debugging). + pub fn dump(&self) { + unsafe { + vdo_map_dump(self.raw); + } + } + + /// Returns the raw pointer (for internal use). + pub(crate) fn as_ptr(&self) -> *mut VdoMap { + self.raw + } +} + +impl Drop for Map { + fn drop(&mut self) { + unsafe { g_object_unref(self.raw as *mut GObject) } + } +} + +// ============================================================================ +// StreamBuilder - Builder pattern for Stream +// ============================================================================ + +/// Builder for creating a video stream. +/// +/// Use [`Stream::builder()`] to create a new builder. +/// +/// # Example +/// +/// ```no_run +/// use vdo::{Stream, VdoFormat}; +/// +/// let stream = Stream::builder() +/// .channel(0) +/// .format(VdoFormat::VDO_FORMAT_H264) +/// .resolution(1920, 1080) +/// .framerate(30) +/// .build() +/// .expect("Failed to build stream"); +/// ``` +#[derive(Clone)] +pub struct StreamBuilder { + format: VdoFormat, + buffer_count: u32, + buffer_strategy: VdoBufferStrategy, + channel: u32, + width: u32, + height: u32, + framerate: u32, +} + +impl Default for StreamBuilder { + fn default() -> Self { + Self { + format: VdoFormat::VDO_FORMAT_H264, + buffer_count: 3, + buffer_strategy: VdoBufferStrategy::VDO_BUFFER_STRATEGY_INFINITE, + channel: 0, + width: 0, + height: 0, + framerate: 0, + } + } +} + +impl StreamBuilder { + /// Creates a new stream builder with default settings. + pub fn new() -> Self { + Self::default() + } + + /// Sets the video format. + /// + /// Default: `VdoFormat::VDO_FORMAT_H264` + /// + /// See the [platform compatibility table](crate#platform-compatibility) for supported formats. + pub fn format(mut self, format: VdoFormat) -> Self { + self.format = format; + self + } + + /// Sets the video channel. + /// + /// Default: 0 (main channel) + pub fn channel(mut self, channel: u32) -> Self { + self.channel = channel; + self + } + + /// Sets the video resolution. + /// + /// If width or height is 0, the camera's native resolution is used. + pub fn resolution(mut self, width: u32, height: u32) -> Self { + self.width = width; + self.height = height; + self + } + + /// Sets the framerate. + /// + /// If 0, the camera's default framerate is used. + pub fn framerate(mut self, framerate: u32) -> Self { + self.framerate = framerate; + self + } + + /// Sets the number of buffers. + /// + /// Default: 3 + /// + /// For YUV and RGB formats, this controls the number of frame buffers. + /// For compressed formats (H.264, H.265, JPEG), this is typically ignored. + pub fn buffers(mut self, count: u32) -> Self { + self.buffer_count = count; + self + } + + /// Sets the buffer strategy. + /// + /// Default: `VdoBufferStrategy::VDO_BUFFER_STRATEGY_INFINITE` + /// + /// - `VDO_BUFFER_STRATEGY_INFINITE`: VDO manages buffers internally (works for all formats) + /// - `VDO_BUFFER_STRATEGY_EXPLICIT`: Application manages buffers (only for YUV/RGB) + pub fn buffer_strategy(mut self, strategy: VdoBufferStrategy) -> Self { + self.buffer_strategy = strategy; + self + } + + /// Builds the stream. + /// + /// # Errors + /// + /// Returns an error if the stream could not be created (e.g., invalid format + /// for the platform, or camera not available). + pub fn build(self) -> Result { + let map = Map::new()?; + map.set_u32("channel", self.channel)?; + map.set_u32("format", self.format.0 as u32)?; + if self.width > 0 { + map.set_u32("width", self.width)?; + } + if self.height > 0 { + map.set_u32("height", self.height)?; + } + if self.framerate > 0 { + map.set_u32("framerate", self.framerate)?; + } + map.set_u32("buffer.count", self.buffer_count)?; + map.set_u32("buffer.strategy", self.buffer_strategy.0)?; + + let (stream_raw, maybe_error) = unsafe { try_func!(vdo_stream_new, map.as_ptr(), None) }; + + if stream_raw.is_null() { + return Err(maybe_error.unwrap_or(Error::MissingVdoError)); + } + + debug_assert!( + maybe_error.is_none(), + "vdo_stream_new returned a stream pointer AND an error" + ); + + Ok(Stream { + raw: stream_raw, + _buffers: Vec::new(), + }) + } +} + +// ============================================================================ +// Stream - Video stream handle +// ============================================================================ + +/// A video stream from a camera channel. +/// +/// Use [`Stream::builder()`] to create a stream, then call [`Stream::start()`] +/// to begin capturing frames. +/// +/// # Example +/// +/// ```no_run +/// use vdo::{Stream, VdoFormat}; +/// +/// let mut stream = Stream::builder() +/// .format(VdoFormat::VDO_FORMAT_JPEG) +/// .resolution(640, 480) +/// .build()?; +/// +/// let mut running = stream.start()?; +/// for buffer in running.iter().take(5) { +/// println!("Got frame: {} bytes", buffer.frame()?.size()); +/// } +/// running.stop()?; +/// # Ok::<(), vdo::Error>(()) +/// ``` +pub struct Stream { + raw: *mut VdoStream, + _buffers: Vec<*mut VdoBuffer>, +} + +// SAFETY: Stream can be sent between threads. +// The underlying VDO library uses GLib which is thread-safe. +unsafe impl Send for Stream {} + +impl Stream { + /// Creates a new stream builder. + pub fn builder() -> StreamBuilder { + StreamBuilder::new() + } + + /// Creates a stream with default settings (H.264 format). + /// + /// This is equivalent to `Stream::builder().build()`. The default format + /// is H.264, which may not be what you want. For other formats, use + /// [`Stream::builder()`] instead: + /// + /// ```no_run + /// # use vdo::{Stream, VdoFormat}; + /// let stream = Stream::builder() + /// .format(VdoFormat::VDO_FORMAT_YUV) + /// .build()?; + /// # Ok::<(), vdo::Error>(()) + /// ``` + pub fn new() -> Result { + StreamBuilder::new().build() + } + + /// Returns stream information as a map. + /// + /// The map contains properties like actual resolution, format, etc. + pub fn info(&self) -> Result { + let (map_raw, maybe_error) = unsafe { try_func!(vdo_stream_get_info, self.raw) }; + if map_raw.is_null() { + return Err(maybe_error.unwrap_or(Error::MissingVdoError)); + } + Ok(Map { raw: map_raw }) + } + + /// Returns stream settings as a map. + pub fn settings(&self) -> Result { + let (map_raw, maybe_error) = unsafe { try_func!(vdo_stream_get_settings, self.raw) }; + if map_raw.is_null() { + return Err(maybe_error.unwrap_or(Error::MissingVdoError)); + } + Ok(Map { raw: map_raw }) + } + + /// Starts the stream and returns a handle for accessing frames. + /// + /// The stream will begin capturing frames from the camera. Use the returned + /// [`RunningStream`] to iterate over frames. + /// + /// # Errors + /// + /// Returns an error if the stream could not be started. + pub fn start(&mut self) -> Result> { + let (success, maybe_error) = unsafe { try_func!(vdo_stream_start, self.raw) }; + if success != glib_sys::GTRUE { + return Err(maybe_error.unwrap_or(Error::MissingVdoError)); + } + Ok(RunningStream { stream: self }) + } +} + +impl Drop for Stream { + fn drop(&mut self) { + unsafe { + vdo_stream_stop(self.raw); + } + // Clean up any allocated buffers + for mut buffer in mem::take(&mut self._buffers) { + unsafe { + let _ = try_func!(vdo_stream_buffer_unref, self.raw, &mut buffer); + } + } + } +} + +// ============================================================================ +// RunningStream - A started stream that can be iterated +// ============================================================================ + +/// A running video stream that yields frame buffers. +/// +/// Created by calling [`Stream::start()`]. Use [`iter()`](RunningStream::iter) +/// to get an iterator over frames. +pub struct RunningStream<'a> { + stream: &'a mut Stream, +} + +impl RunningStream<'_> { + /// Returns an iterator over frame buffers. + /// + /// Each call to `next()` blocks until a new frame is available. + /// The iterator never ends naturally - use `.take(n)` to limit frames. + pub fn iter(&mut self) -> StreamIterator<'_> { + StreamIterator { + stream: self.stream, + } + } + + /// Stops the stream. + /// + /// After stopping, no more frames can be retrieved. + pub fn stop(&mut self) -> Result<()> { + unsafe { vdo_stream_stop(self.stream.raw) }; + Ok(()) + } +} + +// ============================================================================ +// StreamIterator - Iterator over stream buffers +// ============================================================================ + +/// Iterator that yields frame buffers from a running stream. +pub struct StreamIterator<'a> { + stream: &'a Stream, +} + +impl<'a> Iterator for StreamIterator<'a> { + type Item = StreamBuffer<'a>; + + fn next(&mut self) -> Option { + let (buffer_ptr, maybe_error) = + unsafe { try_func!(vdo_stream_get_buffer, self.stream.raw) }; + + if buffer_ptr.is_null() { + if let Some(err) = maybe_error { + log::error!("Error getting buffer: {}", err); + } + return None; + } + + Some(StreamBuffer { + raw: buffer_ptr, + stream: self.stream, + _phantom: PhantomData, + }) + } +} + +// ============================================================================ +// StreamBuffer - A frame buffer from a stream +// ============================================================================ + +/// A buffer containing a video frame. +/// +/// The buffer is automatically released when dropped. +pub struct StreamBuffer<'a> { + raw: *mut VdoBuffer, + stream: &'a Stream, + _phantom: PhantomData<&'a ()>, +} + +impl StreamBuffer<'_> { + /// Returns the buffer capacity in bytes. + pub fn capacity(&self) -> usize { + unsafe { vdo_buffer_get_capacity(self.raw) } + } + + /// Returns the frame data as a byte slice. + /// + /// The slice length is the buffer capacity, not the actual frame size. + /// Use [`Frame::size()`] to get the actual frame data size. + pub fn as_slice(&self) -> Result<&[u8]> { + let data = unsafe { vdo_buffer_get_data(self.raw) }; + if data.is_null() { + return Err(Error::NullPointer); + } + let slice = unsafe { std::slice::from_raw_parts(data as *const u8, self.capacity()) }; + Ok(slice) + } + + /// Returns the frame data as a mutable byte slice. + pub fn as_mut_slice(&mut self) -> Result<&mut [u8]> { + let data = unsafe { vdo_buffer_get_data(self.raw) }; + if data.is_null() { + return Err(Error::NullPointer); + } + let slice = unsafe { std::slice::from_raw_parts_mut(data as *mut u8, self.capacity()) }; + Ok(slice) + } + + /// Returns frame metadata for this buffer. + pub fn frame(&self) -> Result> { + let frame = unsafe { vdo_buffer_get_frame(self.raw) }; + if frame.is_null() { + return Err(Error::NullPointer); + } + Ok(Frame { + raw: frame, + _phantom: PhantomData, + }) + } +} + +impl Drop for StreamBuffer<'_> { + fn drop(&mut self) { + unsafe { + let _ = try_func!(vdo_stream_buffer_unref, self.stream.raw, &mut self.raw); + } + } +} + +// ============================================================================ +// Frame - Frame metadata +// ============================================================================ + +/// Metadata for a video frame. +/// +/// Contains information about frame timing, size, and type. +pub struct Frame<'a> { + raw: *mut VdoFrame, + _phantom: PhantomData<&'a StreamBuffer<'a>>, +} + +impl Frame<'_> { + /// Returns the frame type (I-frame, P-frame, etc.). + pub fn frame_type(&self) -> VdoFrameType { + unsafe { vdo_frame_get_frame_type(self.raw) } + } + + /// Returns the sequence number of the frame. + /// + /// Starts at 0 and increments with each frame. The wrap-around point is undefined. + pub fn sequence_number(&self) -> u32 { + unsafe { vdo_frame_get_sequence_nbr(self.raw) } + } + + /// Returns the timestamp in microseconds since boot. + pub fn timestamp(&self) -> u64 { + unsafe { vdo_frame_get_timestamp(self.raw) } + } + + /// Returns the custom timestamp. + pub fn custom_timestamp(&self) -> i64 { + unsafe { vdo_frame_get_custom_timestamp(self.raw) } + } + + /// Returns the frame data size in bytes. + /// + /// This is the actual size of the frame data, which may be less than + /// the buffer capacity. + pub fn size(&self) -> usize { + unsafe { vdo_frame_get_size(self.raw) } + } + + /// Returns the header size in bytes. + pub fn header_size(&self) -> isize { + unsafe { vdo_frame_get_header_size(self.raw) } + } + + /// Returns the file descriptor for the frame data. + /// + /// This can be used for zero-copy operations with other APIs. + pub fn file_descriptor(&self) -> std::os::fd::BorrowedFd<'_> { + unsafe { + let fd = vdo_frame_get_fd(self.raw); + std::os::fd::BorrowedFd::borrow_raw(fd) + } + } + + /// Returns whether this is the last buffer in a sequence. + pub fn is_last_buffer(&self) -> bool { + unsafe { vdo_frame_get_is_last_buffer(self.raw) != 0 } + } +} + +// ============================================================================ +// Unit Tests (no device required) +// ============================================================================ + +#[cfg(test)] +mod unit_tests { + use super::*; + + #[test] + fn error_code_names() { + // Test that error code names are correctly mapped + let err = VdoError { + code: VDO_ERROR_NOT_FOUND.0 as i32, + message: "test".to_string(), + }; + assert_eq!(err.code_name(), "VDO_ERROR_NOT_FOUND"); + + let err = VdoError { + code: VDO_ERROR_NOT_SUPPORTED.0 as i32, + message: "test".to_string(), + }; + assert_eq!(err.code_name(), "VDO_ERROR_NOT_SUPPORTED"); + + let err = VdoError { + code: 9999, + message: "test".to_string(), + }; + assert_eq!(err.code_name(), "VDO_ERROR_UNKNOWN"); + } + + #[test] + fn error_display() { + let err = VdoError { + code: VDO_ERROR_BUSY.0 as i32, + message: "Resource is busy".to_string(), + }; + let display = format!("{}", err); + assert!(display.contains("VDO_ERROR_BUSY")); + assert!(display.contains("Resource is busy")); + } + + #[test] + fn stream_builder_defaults() { + let builder = StreamBuilder::default(); + // Check default values match expected + assert_eq!(builder.format, VdoFormat::VDO_FORMAT_H264); + assert_eq!(builder.channel, 0); + assert_eq!(builder.buffer_count, 3); + assert_eq!( + builder.buffer_strategy, + VdoBufferStrategy::VDO_BUFFER_STRATEGY_INFINITE + ); + } + + #[test] + fn stream_builder_chaining() { + let builder = StreamBuilder::new() + .format(VdoFormat::VDO_FORMAT_JPEG) + .channel(1) + .resolution(1280, 720) + .framerate(30) + .buffers(5) + .buffer_strategy(VdoBufferStrategy::VDO_BUFFER_STRATEGY_EXPLICIT); + + assert_eq!(builder.format, VdoFormat::VDO_FORMAT_JPEG); + assert_eq!(builder.channel, 1); + assert_eq!(builder.width, 1280); + assert_eq!(builder.height, 720); + assert_eq!(builder.framerate, 30); + assert_eq!(builder.buffer_count, 5); + assert_eq!( + builder.buffer_strategy, + VdoBufferStrategy::VDO_BUFFER_STRATEGY_EXPLICIT + ); + } + + #[test] + fn stream_builder_clone() { + let builder1 = StreamBuilder::new() + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(640, 480); + + let builder2 = builder1.clone(); + + assert_eq!(builder1.format, builder2.format); + assert_eq!(builder1.width, builder2.width); + assert_eq!(builder1.height, builder2.height); + } + + #[test] + fn vdo_format_values() { + // Verify format values match expected C API values + assert_eq!(VdoFormat::VDO_FORMAT_H264.0, 0); + assert_eq!(VdoFormat::VDO_FORMAT_H265.0, 1); + assert_eq!(VdoFormat::VDO_FORMAT_JPEG.0, 2); + assert_eq!(VdoFormat::VDO_FORMAT_YUV.0, 3); + } + + #[test] + fn buffer_strategy_values() { + // Verify buffer strategy values + assert_eq!(VdoBufferStrategy::VDO_BUFFER_STRATEGY_NONE.0, 0); + assert_eq!(VdoBufferStrategy::VDO_BUFFER_STRATEGY_INFINITE.0, 4); + assert_eq!(VdoBufferStrategy::VDO_BUFFER_STRATEGY_EXPLICIT.0, 3); + } + + #[test] + fn error_is_send() { + // Verify Error can be sent across threads + fn assert_send() {} + assert_send::(); + } + + #[test] + fn vdo_error_default() { + let err = VdoError::default(); + assert_eq!(err.code(), 0); + assert!(err.message().is_empty()); + } + + #[test] + fn error_from_vdo_error() { + let vdo_err = VdoError { + code: 1, + message: "test".to_string(), + }; + let err: Error = Error::from(vdo_err); + match err { + Error::Vdo(e) => { + assert_eq!(e.code(), 1); + assert_eq!(e.message(), "test"); + } + _ => panic!("Expected Error::Vdo"), + } + } + + #[test] + fn all_error_variants_display() { + // Ensure all error variants have meaningful Display output + let errors = [ + Error::NullPointer, + Error::CStringAllocation, + Error::MissingVdoError, + Error::NoBuffersAllocated, + Error::Vdo(VdoError { + code: 1, + message: "test".to_string(), + }), + ]; + + for err in &errors { + let msg = format!("{}", err); + assert!(!msg.is_empty(), "Error display should not be empty"); + } + } +} + +// ============================================================================ +// Device Tests (require actual Axis camera) +// ============================================================================ + +#[cfg(all(test, target_arch = "aarch64", feature = "device-tests"))] +mod device_tests { + use super::*; + + fn init_logger() { + let _ = env_logger::builder().is_test(true).try_init(); + } + + #[test] + fn stream_starts_and_stops() -> std::result::Result<(), Box> { + init_logger(); + let mut stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(640, 480) + .build()?; + + let mut running = stream.start()?; + running.stop()?; + Ok(()) + } + + #[test] + fn stream_info_available() -> std::result::Result<(), Box> { + init_logger(); + let stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(640, 480) + .build()?; + + let info = stream.info()?; + // Just verify we can get info without error + info.dump(); + Ok(()) + } + + #[test] + fn stream_settings_available() -> std::result::Result<(), Box> { + init_logger(); + let stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(640, 480) + .build()?; + + let settings = stream.settings()?; + settings.dump(); + Ok(()) + } + + #[test] + fn capture_yuv_frames() -> std::result::Result<(), Box> { + init_logger(); + let mut stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(640, 480) + .build()?; + + let mut running = stream.start()?; + + for (i, buffer) in running.iter().take(5).enumerate() { + let frame = buffer.frame()?; + let size = frame.size(); + assert!(size > 0, "Frame {} size should be > 0", i); + + // YUV NV12: width * height * 1.5 bytes + let expected_min = (640 * 480) as usize; + assert!( + size >= expected_min, + "YUV frame too small: {} < {}", + size, + expected_min + ); + + log::info!( + "YUV frame {}: {} bytes, seq={}, ts={}", + i, + size, + frame.sequence_number(), + frame.timestamp() + ); + } + + running.stop()?; + Ok(()) + } + + #[test] + fn capture_jpeg_frames() -> std::result::Result<(), Box> { + init_logger(); + let mut stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_JPEG) + .resolution(640, 480) + .build()?; + + let mut running = stream.start()?; + + for (i, buffer) in running.iter().take(5).enumerate() { + let frame = buffer.frame()?; + assert!(frame.size() > 0, "Frame {} size should be > 0", i); + + // Verify JPEG magic bytes (SOI marker) + let data = buffer.as_slice()?; + assert!(data.len() >= 2, "Buffer too small for JPEG"); + assert_eq!(data[0], 0xFF, "Invalid JPEG SOI marker"); + assert_eq!(data[1], 0xD8, "Invalid JPEG SOI marker"); + + log::info!("JPEG frame {}: {} bytes", i, frame.size()); + } + + running.stop()?; + Ok(()) + } + + #[test] + fn capture_h264_frames() -> std::result::Result<(), Box> { + init_logger(); + let mut stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_H264) + .resolution(640, 480) + .build()?; + + let mut running = stream.start()?; + + let mut got_i_frame = false; + let mut got_p_frame = false; + + for buffer in running.iter().take(30) { + let frame = buffer.frame()?; + assert!(frame.size() > 0, "H.264 frame size should be > 0"); + + match frame.frame_type() { + VdoFrameType::VDO_FRAME_TYPE_I => got_i_frame = true, + VdoFrameType::VDO_FRAME_TYPE_P => got_p_frame = true, + _ => {} + } + + log::info!( + "H.264 frame: {} bytes, type={:?}, seq={}", + frame.size(), + frame.frame_type(), + frame.sequence_number() + ); + } + + // We should see at least an I-frame in 30 frames + assert!(got_i_frame, "Should have captured at least one I-frame"); + + running.stop()?; + Ok(()) + } + + #[test] + fn capture_h265_frames() -> std::result::Result<(), Box> { + init_logger(); + + // H.265 might not be supported on all platforms (e.g., Artpec-6) + let stream_result = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_H265) + .resolution(640, 480) + .build(); + + match stream_result { + Ok(mut stream) => { + let mut running = stream.start()?; + + for buffer in running.iter().take(10) { + let frame = buffer.frame()?; + assert!(frame.size() > 0, "H.265 frame size should be > 0"); + log::info!("H.265 frame: {} bytes", frame.size()); + } + + running.stop()?; + } + Err(Error::Vdo(e)) if e.code_name() == "VDO_ERROR_NOT_SUPPORTED" => { + log::info!("H.265 not supported on this platform, skipping"); + } + Err(e) => return Err(e.into()), + } + + Ok(()) + } + + #[test] + fn frame_timestamps_increase() -> std::result::Result<(), Box> { + init_logger(); + let mut stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(320, 240) + .framerate(15) + .build()?; + + let mut running = stream.start()?; + let mut prev_ts = 0u64; + let mut prev_seq = 0u32; + + for (i, buffer) in running.iter().take(10).enumerate() { + let frame = buffer.frame()?; + let ts = frame.timestamp(); + let seq = frame.sequence_number(); + + if i > 0 { + assert!( + ts > prev_ts, + "Timestamp should increase: {} <= {}", + ts, + prev_ts + ); + assert!( + seq > prev_seq, + "Sequence should increase: {} <= {}", + seq, + prev_seq + ); + } + + prev_ts = ts; + prev_seq = seq; + } + + running.stop()?; + Ok(()) + } + + #[test] + fn buffer_data_accessible() -> std::result::Result<(), Box> { + init_logger(); + let mut stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(320, 240) + .build()?; + + let mut running = stream.start()?; + + for buffer in running.iter().take(3) { + let frame = buffer.frame()?; + let capacity = buffer.capacity(); + let data = buffer.as_slice()?; + + assert_eq!(data.len(), capacity, "Slice length should match capacity"); + assert!(frame.size() <= capacity, "Frame size should be <= capacity"); + + // Verify we can read data without crashing + let _first_byte = data[0]; + let _last_byte = data[frame.size().saturating_sub(1)]; + } + + running.stop()?; + Ok(()) + } + + #[test] + fn multiple_streams_sequential() -> std::result::Result<(), Box> { + init_logger(); + + // Create and use first stream + { + let mut stream = Stream::builder() + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(320, 240) + .build()?; + + let mut running = stream.start()?; + let _ = running.iter().take(3).count(); + running.stop()?; + } + + // Create and use second stream + { + let mut stream = Stream::builder() + .format(VdoFormat::VDO_FORMAT_JPEG) + .resolution(320, 240) + .build()?; + + let mut running = stream.start()?; + let _ = running.iter().take(3).count(); + running.stop()?; + } + + Ok(()) + } + + // ======================================================================== + // Error Handling Tests + // ======================================================================== + + #[test] + fn invalid_channel_returns_error() { + init_logger(); + // Channel 999 should not exist on any camera + let result = Stream::builder() + .channel(999) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(640, 480) + .build(); + + assert!(result.is_err(), "Invalid channel should return error"); + if let Err(e) = result { + log::info!("Expected error for invalid channel: {}", e); + } + } + + #[test] + fn unsupported_format_returns_error() { + init_logger(); + // Try a format that might not be supported (platform-dependent) + // VDO_FORMAT_BAYER is often not supported + let result = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_BAYER) + .resolution(640, 480) + .build(); + + // This might succeed on some platforms, so just log the result + match result { + Ok(_) => log::info!("BAYER format is supported on this platform"), + Err(e) => log::info!("BAYER format not supported: {}", e), + } + } + + #[test] + fn invalid_resolution_handled() { + init_logger(); + // Try an unusual resolution that might not be supported + let result = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(12345, 6789) // Unusual resolution + .build(); + + // Camera might adjust resolution or return error + match result { + Ok(stream) => { + // Camera accepted - check actual resolution via info + if let Ok(info) = stream.info() { + log::info!("Camera accepted unusual resolution, check actual via info"); + info.dump(); + } + } + Err(e) => { + log::info!("Camera rejected unusual resolution: {}", e); + } + } + } + + #[test] + fn stream_stop_is_idempotent() -> std::result::Result<(), Box> { + init_logger(); + let mut stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(320, 240) + .build()?; + + let mut running = stream.start()?; + let _ = running.iter().take(1).count(); + + // Stop multiple times should not crash + running.stop()?; + running.stop()?; // Second stop should be safe + + Ok(()) + } + + #[test] + fn stream_dropped_without_stop() -> std::result::Result<(), Box> { + init_logger(); + + // Create stream, start it, get some frames, then drop without calling stop() + // This tests that Drop implementation properly cleans up + { + let mut stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(320, 240) + .build()?; + + let mut running = stream.start()?; + let _ = running.iter().take(2).count(); + // Intentionally NOT calling running.stop() + // Drop should handle cleanup + } + + // If we get here without crash, cleanup worked + log::info!("Stream dropped without explicit stop - cleanup successful"); + Ok(()) + } + + #[test] + fn error_message_is_descriptive() { + init_logger(); + + // Force an error and check that the message is helpful + let result = Stream::builder() + .channel(999) // Invalid + .build(); + + if let Err(Error::Vdo(e)) = result { + let msg = e.message(); + let code_name = e.code_name(); + log::info!( + "Error code: {}, name: {}, message: {}", + e.code(), + code_name, + msg + ); + + // Error should have some information + assert!(!code_name.is_empty(), "Error code name should not be empty"); + } + } + + #[test] + fn rapid_stream_creation_destruction() -> std::result::Result<(), Box> { + init_logger(); + + // Rapidly create and destroy streams to test for resource leaks + for i in 0..5 { + let mut stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(320, 240) + .build()?; + + let mut running = stream.start()?; + let _ = running.iter().take(1).count(); + running.stop()?; + + log::info!("Rapid cycle {} complete", i); + } + + Ok(()) + } +} From 2ca25d93eeaba1a3d8cd091843bce99aa05c97bb Mon Sep 17 00:00:00 2001 From: vsem-azamat Date: Tue, 10 Feb 2026 21:43:49 +0100 Subject: [PATCH 02/29] fix(vdo): Address all PR #223 review feedback Address all review comments. Fix GObject leak in Stream::drop. - Consume-by-value ownership: start(self), stop(self), unref(self) - Replace Iterator with next_buffer() -> Result - Merge Frame into StreamBuffer (VdoBuffer = VdoFrame) - Extract Map to separate module, keys as &CStr - Add Resolution enum, remove buffer_strategy, rename to custom_timestamp_us - Replace as_mut_slice with data_copy(), file_descriptor() returns Result Tested: cargo clippy clean, 33/33 tests pass on Artpec-9 device. --- Cargo.toml | 2 +- apps/vdo_encode_client/src/main.rs | 36 +- crates/vdo/examples/basic.rs | 23 +- crates/vdo/src/lib.rs | 1122 +++++++++++++--------------- crates/vdo/src/map.rs | 128 ++++ 5 files changed, 701 insertions(+), 610 deletions(-) create mode 100644 crates/vdo/src/map.rs diff --git a/Cargo.toml b/Cargo.toml index 1501721c..e8965429 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,8 +67,8 @@ licensekey = { path = "crates/licensekey" } licensekey-sys = { path = "crates/licensekey-sys" } mdb = { path = "crates/mdb" } mdb-sys = { path = "crates/mdb-sys" } -vdo-sys = { path = "crates/vdo-sys" } vdo = { path = "crates/vdo" } +vdo-sys = { path = "crates/vdo-sys" } [workspace.package] edition = "2021" diff --git a/apps/vdo_encode_client/src/main.rs b/apps/vdo_encode_client/src/main.rs index fc500ff8..5e18c4ad 100644 --- a/apps/vdo_encode_client/src/main.rs +++ b/apps/vdo_encode_client/src/main.rs @@ -8,16 +8,22 @@ //! - Frame capture and metadata access //! - Proper resource cleanup +// These format tests run on the device as an ACAP application rather than as +// unit tests because they require access to actual camera hardware via the VDO API. + use log::{error, info}; -use vdo::{Error, Stream, VdoFormat}; +use vdo::{Error, Resolution, Stream, VdoFormat}; -fn test_format(name: &str, format: VdoFormat, num_frames: usize) -> Result<(), Error> { +fn capture_format(name: &str, format: VdoFormat, num_frames: usize) -> Result<(), Error> { info!("=== Testing {} format ===", name); - let mut stream = Stream::builder() + let stream = Stream::builder() .channel(0) .format(format) - .resolution(640, 480) + .resolution(Resolution::Exact { + width: 640, + height: 480, + }) .framerate(15) .build()?; @@ -29,14 +35,14 @@ fn test_format(name: &str, format: VdoFormat, num_frames: usize) -> Result<(), E stream_info.dump(); } - let mut running = stream.start()?; + let running = stream.start()?; info!("{}: Stream started", name); - for (i, buffer) in running.iter().take(num_frames).enumerate() { - let frame = buffer.frame()?; - let size = frame.size(); - let seq = frame.sequence_number(); - let ts = frame.timestamp(); + for i in 0..num_frames { + let buffer = running.next_buffer()?; + let size = buffer.size(); + let seq = buffer.sequence_number(); + let ts = buffer.timestamp(); info!( "{}: Frame {}: {} bytes, seq={}, timestamp={}us", @@ -54,7 +60,7 @@ fn test_format(name: &str, format: VdoFormat, num_frames: usize) -> Result<(), E } } - running.stop()?; + running.stop(); info!("{}: Stream stopped successfully", name); info!(""); @@ -69,25 +75,25 @@ fn main() { info!(""); // Test YUV (most portable format) - match test_format("YUV", VdoFormat::VDO_FORMAT_YUV, 5) { + match capture_format("YUV", VdoFormat::VDO_FORMAT_YUV, 5) { Ok(()) => info!("YUV test: PASSED"), Err(e) => error!("YUV test: FAILED - {}", e), } // Test JPEG - match test_format("JPEG", VdoFormat::VDO_FORMAT_JPEG, 5) { + match capture_format("JPEG", VdoFormat::VDO_FORMAT_JPEG, 5) { Ok(()) => info!("JPEG test: PASSED"), Err(e) => error!("JPEG test: FAILED - {}", e), } // Test H.264 - match test_format("H.264", VdoFormat::VDO_FORMAT_H264, 10) { + match capture_format("H.264", VdoFormat::VDO_FORMAT_H264, 10) { Ok(()) => info!("H.264 test: PASSED"), Err(e) => error!("H.264 test: FAILED - {}", e), } // Test H.265 (might not be supported on all platforms) - match test_format("H.265", VdoFormat::VDO_FORMAT_H265, 5) { + match capture_format("H.265", VdoFormat::VDO_FORMAT_H265, 5) { Ok(()) => info!("H.265 test: PASSED"), Err(e) => { if let Error::Vdo(ref vdo_err) = e { diff --git a/crates/vdo/examples/basic.rs b/crates/vdo/examples/basic.rs index 87dd2101..6eeb2876 100644 --- a/crates/vdo/examples/basic.rs +++ b/crates/vdo/examples/basic.rs @@ -3,7 +3,7 @@ //! This example creates a video stream, captures a few frames, and prints //! information about each frame. -use vdo::{Stream, VdoFormat}; +use vdo::{Resolution, Stream, VdoFormat}; fn main() -> Result<(), Box> { // Initialize logging (optional) @@ -12,30 +12,33 @@ fn main() -> Result<(), Box> { println!("Creating video stream..."); // Create a stream with YUV format (most portable across platforms) - let mut stream = Stream::builder() + let stream = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(640, 480) + .resolution(Resolution::Exact { + width: 640, + height: 480, + }) .framerate(15) .build()?; println!("Starting stream..."); - let mut running = stream.start()?; + let running = stream.start()?; println!("Capturing frames..."); - for (i, buffer) in running.iter().take(10).enumerate() { - let frame = buffer.frame()?; + for i in 0..10 { + let buffer = running.next_buffer()?; println!( "Frame {}: {} bytes, seq={}, timestamp={}us", i, - frame.size(), - frame.sequence_number(), - frame.timestamp() + buffer.size(), + buffer.sequence_number(), + buffer.timestamp() ); } println!("Stopping stream..."); - running.stop()?; + running.stop(); println!("Done!"); Ok(()) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 2cb066ec..d01572ed 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -22,23 +22,23 @@ //! # Example //! //! ```no_run -//! use vdo::{Stream, VdoFormat}; +//! use vdo::{Resolution, Stream, VdoFormat}; //! -//! let mut stream = Stream::builder() +//! let stream = Stream::builder() //! .channel(0) //! .format(VdoFormat::VDO_FORMAT_YUV) -//! .resolution(1920, 1080) +//! .resolution(Resolution::Exact { width: 1920, height: 1080 }) //! .build() //! .expect("Failed to create stream"); //! -//! let mut running = stream.start().expect("Failed to start stream"); +//! let running = stream.start().expect("Failed to start stream"); //! -//! for buffer in running.iter().take(10) { -//! let frame = buffer.frame().expect("Failed to get frame"); -//! println!("Frame size: {} bytes", frame.size()); +//! for _ in 0..10 { +//! let buffer = running.next_buffer().expect("Failed to get buffer"); +//! println!("Frame size: {} bytes", buffer.size()); //! } //! -//! running.stop().expect("Failed to stop stream"); +//! running.stop(); //! ``` //! //! # Known Issues @@ -46,19 +46,17 @@ //! - Image rotation may vary between platforms. Check the `rotation` property in stream info. //! - Some formats (RGB, PLANAR_RGB) may produce upside-down images on certain platforms. +mod map; +pub use map::{CStringPtr, Map}; + use glib_sys::GError; use gobject_sys::{g_object_unref, GObject}; -use std::ffi::{CStr, CString}; use std::fmt::{Debug, Display}; -use std::marker::PhantomData; use std::mem; use std::ptr; use vdo_sys::*; -// Re-export commonly used types from vdo-sys -pub use vdo_sys::{ - VdoBufferStrategy, VdoFormat, VdoFrameType, VdoRateControlMode, VdoRateControlPriority, -}; +pub use vdo_sys::{VdoFormat, VdoFrameType, VdoRateControlMode, VdoRateControlPriority}; /// Macro for calling VDO functions that take a GError** parameter. /// Returns a tuple of (result, Option). @@ -83,33 +81,17 @@ macro_rules! try_func { }}; } -// ============================================================================ -// Error types -// ============================================================================ - /// Error type for VDO operations. #[derive(thiserror::Error, Debug)] pub enum Error { - /// Error returned by the VDO library. #[error(transparent)] Vdo(#[from] VdoError), - /// VDO returned an unexpected null pointer. #[error("VDO returned an unexpected null pointer")] NullPointer, - /// Could not allocate memory for CString. - #[error("Could not allocate memory for CString")] - CStringAllocation, - /// Missing error data from VDO library. #[error("Missing error data from VDO library")] MissingVdoError, - /// No buffers are allocated for the stream. - #[error("No buffers are allocated for the stream")] - NoBuffersAllocated, } -/// Result type for VDO operations. -pub type Result = std::result::Result; - /// Error from the VDO library. #[derive(Default)] pub struct VdoError { @@ -123,17 +105,18 @@ impl VdoError { return VdoError::default(); } + // SAFETY: gerror is non-null. We copy the struct and read the message pointer + // before calling g_error_free, which would invalidate both. let g_error = unsafe { *gerror }; let message = if g_error.message.is_null() { String::from("Unknown error") } else { - unsafe { CStr::from_ptr(g_error.message) } + unsafe { std::ffi::CStr::from_ptr(g_error.message) } .to_str() .unwrap_or("Invalid UTF-8 in error message") .to_string() }; - // Free the GError unsafe { glib_sys::g_error_free(gerror) }; VdoError { @@ -142,7 +125,7 @@ impl VdoError { } } - /// Returns the error code name. + /// Returns a human-readable name for the VDO error code. pub fn code_name(&self) -> &'static str { let code = self.code as u32; match code { @@ -170,12 +153,10 @@ impl VdoError { } } - /// Returns the numeric error code. pub fn code(&self) -> i32 { self.code } - /// Returns the error message. pub fn message(&self) -> &str { &self.message } @@ -199,118 +180,15 @@ impl Debug for VdoError { impl std::error::Error for VdoError {} -// ============================================================================ -// Map - VDO settings/configuration container -// ============================================================================ - -/// A key-value map for VDO settings. -/// -/// Used to configure stream parameters and retrieve stream information. -pub struct Map { - raw: *mut VdoMap, +/// Specifies the video resolution for a stream. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Resolution { + /// Use the camera's native resolution. + Native, + /// Use an exact resolution. + Exact { width: u32, height: u32 }, } -impl Map { - /// Creates a new empty map. - pub fn new() -> Result { - let map = unsafe { vdo_map_new() }; - if map.is_null() { - Err(Error::NullPointer) - } else { - Ok(Self { raw: map }) - } - } - - /// Sets a 32-bit unsigned integer value. - pub fn set_u32(&self, key: &str, value: u32) -> Result<()> { - let key_cstr = CString::new(key).map_err(|_| Error::CStringAllocation)?; - unsafe { - vdo_map_set_uint32(self.raw, key_cstr.as_ptr(), value); - } - Ok(()) - } - - /// Gets a 32-bit unsigned integer value. - pub fn get_u32(&self, key: &str, default: u32) -> Result { - let key_cstr = CString::new(key).map_err(|_| Error::CStringAllocation)?; - let value = unsafe { vdo_map_get_uint32(self.raw, key_cstr.as_ptr(), default) }; - Ok(value) - } - - /// Sets a string value. - pub fn set_string(&self, key: &str, value: &str) -> Result<()> { - let key_cstr = CString::new(key).map_err(|_| Error::CStringAllocation)?; - let value_cstr = CString::new(value).map_err(|_| Error::CStringAllocation)?; - unsafe { - vdo_map_set_string(self.raw, key_cstr.as_ptr(), value_cstr.as_ptr()); - } - Ok(()) - } - - /// Gets a string value, returning an owned copy. - /// - /// Returns `None` if the key doesn't exist or the value is null. - pub fn get_string(&self, key: &str) -> Result> { - let key_cstr = CString::new(key).map_err(|_| Error::CStringAllocation)?; - let ptr = unsafe { vdo_map_dup_string(self.raw, key_cstr.as_ptr(), ptr::null()) }; - if ptr.is_null() { - return Ok(None); - } - let cstr = unsafe { CStr::from_ptr(ptr) }; - let result = cstr.to_str().map(|s| s.to_owned()).ok(); - unsafe { glib_sys::g_free(ptr as *mut _) }; - Ok(result) - } - - /// Sets a boolean value. - pub fn set_bool(&self, key: &str, value: bool) -> Result<()> { - let key_cstr = CString::new(key).map_err(|_| Error::CStringAllocation)?; - let gvalue = if value { - glib_sys::GTRUE - } else { - glib_sys::GFALSE - }; - unsafe { - vdo_map_set_boolean(self.raw, key_cstr.as_ptr(), gvalue); - } - Ok(()) - } - - /// Gets a boolean value. - pub fn get_bool(&self, key: &str, default: bool) -> Result { - let key_cstr = CString::new(key).map_err(|_| Error::CStringAllocation)?; - let gdefault = if default { - glib_sys::GTRUE - } else { - glib_sys::GFALSE - }; - let value = unsafe { vdo_map_get_boolean(self.raw, key_cstr.as_ptr(), gdefault) }; - Ok(value != glib_sys::GFALSE) - } - - /// Dumps the map contents to stdout (for debugging). - pub fn dump(&self) { - unsafe { - vdo_map_dump(self.raw); - } - } - - /// Returns the raw pointer (for internal use). - pub(crate) fn as_ptr(&self) -> *mut VdoMap { - self.raw - } -} - -impl Drop for Map { - fn drop(&mut self) { - unsafe { g_object_unref(self.raw as *mut GObject) } - } -} - -// ============================================================================ -// StreamBuilder - Builder pattern for Stream -// ============================================================================ - /// Builder for creating a video stream. /// /// Use [`Stream::builder()`] to create a new builder. @@ -318,12 +196,12 @@ impl Drop for Map { /// # Example /// /// ```no_run -/// use vdo::{Stream, VdoFormat}; +/// use vdo::{Resolution, Stream, VdoFormat}; /// /// let stream = Stream::builder() /// .channel(0) /// .format(VdoFormat::VDO_FORMAT_H264) -/// .resolution(1920, 1080) +/// .resolution(Resolution::Exact { width: 1920, height: 1080 }) /// .framerate(30) /// .build() /// .expect("Failed to build stream"); @@ -332,10 +210,8 @@ impl Drop for Map { pub struct StreamBuilder { format: VdoFormat, buffer_count: u32, - buffer_strategy: VdoBufferStrategy, channel: u32, - width: u32, - height: u32, + resolution: Resolution, framerate: u32, } @@ -344,23 +220,18 @@ impl Default for StreamBuilder { Self { format: VdoFormat::VDO_FORMAT_H264, buffer_count: 3, - buffer_strategy: VdoBufferStrategy::VDO_BUFFER_STRATEGY_INFINITE, channel: 0, - width: 0, - height: 0, + resolution: Resolution::Native, framerate: 0, } } } impl StreamBuilder { - /// Creates a new stream builder with default settings. pub fn new() -> Self { Self::default() } - /// Sets the video format. - /// /// Default: `VdoFormat::VDO_FORMAT_H264` /// /// See the [platform compatibility table](crate#platform-compatibility) for supported formats. @@ -369,74 +240,53 @@ impl StreamBuilder { self } - /// Sets the video channel. - /// /// Default: 0 (main channel) pub fn channel(mut self, channel: u32) -> Self { self.channel = channel; self } - /// Sets the video resolution. - /// - /// If width or height is 0, the camera's native resolution is used. - pub fn resolution(mut self, width: u32, height: u32) -> Self { - self.width = width; - self.height = height; + /// Default: [`Resolution::Native`] + pub fn resolution(mut self, resolution: Resolution) -> Self { + self.resolution = resolution; self } - /// Sets the framerate. - /// /// If 0, the camera's default framerate is used. pub fn framerate(mut self, framerate: u32) -> Self { self.framerate = framerate; self } - /// Sets the number of buffers. - /// - /// Default: 3 - /// - /// For YUV and RGB formats, this controls the number of frame buffers. - /// For compressed formats (H.264, H.265, JPEG), this is typically ignored. + /// Default: 3. For YUV/RGB formats, controls frame buffer count. + /// For compressed formats (H.264, H.265, JPEG), typically ignored. pub fn buffers(mut self, count: u32) -> Self { self.buffer_count = count; self } - /// Sets the buffer strategy. - /// - /// Default: `VdoBufferStrategy::VDO_BUFFER_STRATEGY_INFINITE` - /// - /// - `VDO_BUFFER_STRATEGY_INFINITE`: VDO manages buffers internally (works for all formats) - /// - `VDO_BUFFER_STRATEGY_EXPLICIT`: Application manages buffers (only for YUV/RGB) - pub fn buffer_strategy(mut self, strategy: VdoBufferStrategy) -> Self { - self.buffer_strategy = strategy; - self - } - /// Builds the stream. /// - /// # Errors - /// /// Returns an error if the stream could not be created (e.g., invalid format /// for the platform, or camera not available). - pub fn build(self) -> Result { - let map = Map::new()?; - map.set_u32("channel", self.channel)?; - map.set_u32("format", self.format.0 as u32)?; - if self.width > 0 { - map.set_u32("width", self.width)?; - } - if self.height > 0 { - map.set_u32("height", self.height)?; + pub fn build(self) -> std::result::Result { + let mut map = Map::try_new()?; + map.set_u32(c"channel", self.channel); + map.set_u32(c"format", self.format.0 as u32); + if let Resolution::Exact { width, height } = self.resolution { + map.set_u32(c"width", width); + map.set_u32(c"height", height); } if self.framerate > 0 { - map.set_u32("framerate", self.framerate)?; + map.set_u32(c"framerate", self.framerate); } - map.set_u32("buffer.count", self.buffer_count)?; - map.set_u32("buffer.strategy", self.buffer_strategy.0)?; + map.set_u32(c"buffer.count", self.buffer_count); + // Always use INFINITE strategy; EXPLICIT is not exposed because it + // requires unsafe application-managed buffer allocation. + map.set_u32( + c"buffer.strategy", + VdoBufferStrategy::VDO_BUFFER_STRATEGY_INFINITE.0, + ); let (stream_raw, maybe_error) = unsafe { try_func!(vdo_stream_new, map.as_ptr(), None) }; @@ -449,100 +299,76 @@ impl StreamBuilder { "vdo_stream_new returned a stream pointer AND an error" ); - Ok(Stream { - raw: stream_raw, - _buffers: Vec::new(), - }) + Ok(Stream { raw: stream_raw }) } } -// ============================================================================ -// Stream - Video stream handle -// ============================================================================ - /// A video stream from a camera channel. /// /// Use [`Stream::builder()`] to create a stream, then call [`Stream::start()`] -/// to begin capturing frames. +/// to begin capturing frames. Starting consumes the `Stream` and returns a +/// [`RunningStream`]. /// /// # Example /// /// ```no_run -/// use vdo::{Stream, VdoFormat}; +/// use vdo::{Resolution, Stream, VdoFormat}; /// -/// let mut stream = Stream::builder() +/// let stream = Stream::builder() /// .format(VdoFormat::VDO_FORMAT_JPEG) -/// .resolution(640, 480) +/// .resolution(Resolution::Exact { width: 640, height: 480 }) /// .build()?; /// -/// let mut running = stream.start()?; -/// for buffer in running.iter().take(5) { -/// println!("Got frame: {} bytes", buffer.frame()?.size()); +/// let running = stream.start()?; +/// for _ in 0..5 { +/// let buffer = running.next_buffer()?; +/// println!("Got frame: {} bytes", buffer.size()); /// } -/// running.stop()?; +/// running.stop(); /// # Ok::<(), vdo::Error>(()) /// ``` +#[derive(Debug)] pub struct Stream { raw: *mut VdoStream, - _buffers: Vec<*mut VdoBuffer>, } -// SAFETY: Stream can be sent between threads. -// The underlying VDO library uses GLib which is thread-safe. +// SAFETY: We hold exclusive ownership of this GObject reference. +// Sync is NOT implemented because GLib objects are not safe for concurrent access. unsafe impl Send for Stream {} impl Stream { - /// Creates a new stream builder. pub fn builder() -> StreamBuilder { StreamBuilder::new() } - /// Creates a stream with default settings (H.264 format). - /// - /// This is equivalent to `Stream::builder().build()`. The default format - /// is H.264, which may not be what you want. For other formats, use - /// [`Stream::builder()`] instead: - /// - /// ```no_run - /// # use vdo::{Stream, VdoFormat}; - /// let stream = Stream::builder() - /// .format(VdoFormat::VDO_FORMAT_YUV) - /// .build()?; - /// # Ok::<(), vdo::Error>(()) - /// ``` - pub fn new() -> Result { + /// Equivalent to `Stream::builder().build()` (H.264 format, native resolution). + pub fn new() -> std::result::Result { StreamBuilder::new().build() } - /// Returns stream information as a map. - /// - /// The map contains properties like actual resolution, format, etc. - pub fn info(&self) -> Result { + /// Returns stream information (actual resolution, format, etc.) as a map. + pub fn info(&self) -> std::result::Result { let (map_raw, maybe_error) = unsafe { try_func!(vdo_stream_get_info, self.raw) }; if map_raw.is_null() { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); } - Ok(Map { raw: map_raw }) + // SAFETY: map_raw is non-null and freshly returned by VDO with ownership transferred. + Ok(unsafe { Map::from_raw(map_raw) }) } /// Returns stream settings as a map. - pub fn settings(&self) -> Result { + pub fn settings(&self) -> std::result::Result { let (map_raw, maybe_error) = unsafe { try_func!(vdo_stream_get_settings, self.raw) }; if map_raw.is_null() { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); } - Ok(Map { raw: map_raw }) + Ok(unsafe { Map::from_raw(map_raw) }) } - /// Starts the stream and returns a handle for accessing frames. - /// - /// The stream will begin capturing frames from the camera. Use the returned - /// [`RunningStream`] to iterate over frames. + /// Starts the stream, consuming `self` and returning a [`RunningStream`]. /// - /// # Errors - /// - /// Returns an error if the stream could not be started. - pub fn start(&mut self) -> Result> { + /// On failure, the underlying stream is automatically cleaned up. + pub fn start(self) -> std::result::Result { let (success, maybe_error) = unsafe { try_func!(vdo_stream_start, self.raw) }; if success != glib_sys::GTRUE { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); @@ -553,211 +379,164 @@ impl Stream { impl Drop for Stream { fn drop(&mut self) { - unsafe { - vdo_stream_stop(self.raw); - } - // Clean up any allocated buffers - for mut buffer in mem::take(&mut self._buffers) { - unsafe { - let _ = try_func!(vdo_stream_buffer_unref, self.raw, &mut buffer); - } - } + // vdo_stream_stop is idempotent (returns void), safe to call even if never started. + unsafe { vdo_stream_stop(self.raw) }; + // Release our GObject reference to avoid leaking. + unsafe { g_object_unref(self.raw as *mut GObject) }; } } -// ============================================================================ -// RunningStream - A started stream that can be iterated -// ============================================================================ - /// A running video stream that yields frame buffers. /// -/// Created by calling [`Stream::start()`]. Use [`iter()`](RunningStream::iter) -/// to get an iterator over frames. -pub struct RunningStream<'a> { - stream: &'a mut Stream, -} - -impl RunningStream<'_> { - /// Returns an iterator over frame buffers. - /// - /// Each call to `next()` blocks until a new frame is available. - /// The iterator never ends naturally - use `.take(n)` to limit frames. - pub fn iter(&mut self) -> StreamIterator<'_> { - StreamIterator { - stream: self.stream, - } - } - - /// Stops the stream. - /// - /// After stopping, no more frames can be retrieved. - pub fn stop(&mut self) -> Result<()> { - unsafe { vdo_stream_stop(self.stream.raw) }; - Ok(()) - } +/// Created by calling [`Stream::start()`]. Use [`next_buffer()`](RunningStream::next_buffer) +/// to retrieve frame buffers. Call [`stop()`](RunningStream::stop) or simply drop +/// this value for cleanup. +pub struct RunningStream { + stream: Stream, } -// ============================================================================ -// StreamIterator - Iterator over stream buffers -// ============================================================================ +// SAFETY: Owns a Stream (which is Send). +// Sync is NOT implemented; this ensures next_buffer(&self) cannot be called concurrently. +unsafe impl Send for RunningStream {} -/// Iterator that yields frame buffers from a running stream. -pub struct StreamIterator<'a> { - stream: &'a Stream, -} - -impl<'a> Iterator for StreamIterator<'a> { - type Item = StreamBuffer<'a>; - - fn next(&mut self) -> Option { +impl RunningStream { + /// Blocks until a new frame is available and returns it. + pub fn next_buffer(&self) -> std::result::Result, Error> { let (buffer_ptr, maybe_error) = unsafe { try_func!(vdo_stream_get_buffer, self.stream.raw) }; if buffer_ptr.is_null() { - if let Some(err) = maybe_error { - log::error!("Error getting buffer: {}", err); - } - return None; + return Err(maybe_error.unwrap_or(Error::MissingVdoError)); } - Some(StreamBuffer { + Ok(StreamBuffer { raw: buffer_ptr, - stream: self.stream, - _phantom: PhantomData, + stream: &self.stream, }) } -} -// ============================================================================ -// StreamBuffer - A frame buffer from a stream -// ============================================================================ + /// Stops the stream, consuming this handle. + pub fn stop(self) { + unsafe { vdo_stream_stop(self.stream.raw) }; + // self.stream dropped here -> Stream::drop calls g_object_unref + } +} -/// A buffer containing a video frame. +/// A buffer containing a video frame from a running stream. +/// +/// Since `VdoBuffer` and `VdoFrame` are the same type in the C API, all frame +/// metadata (size, timestamp, frame type, etc.) is accessed directly on this type. +/// +/// The buffer borrows from the [`RunningStream`] that produced it and is +/// automatically unreferenced when dropped. Use [`unref()`](StreamBuffer::unref) +/// to handle unref errors explicitly. /// -/// The buffer is automatically released when dropped. +/// # Buffer Validity +/// +/// The buffer data pointer and frame metadata remain valid until the buffer is +/// unreferenced (on drop or via [`unref()`](StreamBuffer::unref)). pub struct StreamBuffer<'a> { raw: *mut VdoBuffer, stream: &'a Stream, - _phantom: PhantomData<&'a ()>, } impl StreamBuffer<'_> { - /// Returns the buffer capacity in bytes. pub fn capacity(&self) -> usize { unsafe { vdo_buffer_get_capacity(self.raw) } } - /// Returns the frame data as a byte slice. + /// Returns the frame data as a byte slice of [`capacity()`](StreamBuffer::capacity) bytes. /// - /// The slice length is the buffer capacity, not the actual frame size. - /// Use [`Frame::size()`] to get the actual frame data size. - pub fn as_slice(&self) -> Result<&[u8]> { + /// Use [`size()`](StreamBuffer::size) to get the actual frame data size. + pub fn as_slice(&self) -> std::result::Result<&[u8], Error> { let data = unsafe { vdo_buffer_get_data(self.raw) }; if data.is_null() { return Err(Error::NullPointer); } + // SAFETY: VDO buffers are backed by mmap'd or allocated regions that are fully + // initialized at allocation time. Bytes beyond size() may be stale but are valid. let slice = unsafe { std::slice::from_raw_parts(data as *const u8, self.capacity()) }; Ok(slice) } - /// Returns the frame data as a mutable byte slice. - pub fn as_mut_slice(&mut self) -> Result<&mut [u8]> { + /// Returns a copy of exactly [`size()`](StreamBuffer::size) bytes of frame data. + pub fn data_copy(&self) -> std::result::Result, Error> { let data = unsafe { vdo_buffer_get_data(self.raw) }; if data.is_null() { return Err(Error::NullPointer); } - let slice = unsafe { std::slice::from_raw_parts_mut(data as *mut u8, self.capacity()) }; - Ok(slice) - } - - /// Returns frame metadata for this buffer. - pub fn frame(&self) -> Result> { - let frame = unsafe { vdo_buffer_get_frame(self.raw) }; - if frame.is_null() { - return Err(Error::NullPointer); - } - Ok(Frame { - raw: frame, - _phantom: PhantomData, - }) + let size = self.size(); + let slice = unsafe { std::slice::from_raw_parts(data as *const u8, size) }; + Ok(slice.to_vec()) } -} - -impl Drop for StreamBuffer<'_> { - fn drop(&mut self) { - unsafe { - let _ = try_func!(vdo_stream_buffer_unref, self.stream.raw, &mut self.raw); - } - } -} - -// ============================================================================ -// Frame - Frame metadata -// ============================================================================ -/// Metadata for a video frame. -/// -/// Contains information about frame timing, size, and type. -pub struct Frame<'a> { - raw: *mut VdoFrame, - _phantom: PhantomData<&'a StreamBuffer<'a>>, -} - -impl Frame<'_> { - /// Returns the frame type (I-frame, P-frame, etc.). pub fn frame_type(&self) -> VdoFrameType { unsafe { vdo_frame_get_frame_type(self.raw) } } - /// Returns the sequence number of the frame. - /// - /// Starts at 0 and increments with each frame. The wrap-around point is undefined. + /// Starts at 0 and increments with each frame. Wrap-around point is undefined. pub fn sequence_number(&self) -> u32 { unsafe { vdo_frame_get_sequence_nbr(self.raw) } } - /// Returns the timestamp in microseconds since boot. + /// Timestamp in microseconds since boot. pub fn timestamp(&self) -> u64 { unsafe { vdo_frame_get_timestamp(self.raw) } } - /// Returns the custom timestamp. - pub fn custom_timestamp(&self) -> i64 { + pub fn custom_timestamp_us(&self) -> i64 { unsafe { vdo_frame_get_custom_timestamp(self.raw) } } - /// Returns the frame data size in bytes. - /// - /// This is the actual size of the frame data, which may be less than - /// the buffer capacity. + /// Actual frame data size in bytes (may be less than [`capacity()`](StreamBuffer::capacity)). pub fn size(&self) -> usize { unsafe { vdo_frame_get_size(self.raw) } } - /// Returns the header size in bytes. pub fn header_size(&self) -> isize { unsafe { vdo_frame_get_header_size(self.raw) } } - /// Returns the file descriptor for the frame data. - /// - /// This can be used for zero-copy operations with other APIs. - pub fn file_descriptor(&self) -> std::os::fd::BorrowedFd<'_> { - unsafe { - let fd = vdo_frame_get_fd(self.raw); - std::os::fd::BorrowedFd::borrow_raw(fd) + /// Returns a borrowed file descriptor for the buffer's backing memory. + pub fn file_descriptor(&self) -> std::result::Result, Error> { + let fd = unsafe { vdo_buffer_get_fd(self.raw) }; + if fd < 0 { + return Err(Error::NullPointer); } + // SAFETY: fd is non-negative and valid for the lifetime of this buffer. + Ok(unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) }) } - /// Returns whether this is the last buffer in a sequence. pub fn is_last_buffer(&self) -> bool { unsafe { vdo_frame_get_is_last_buffer(self.raw) != 0 } } + + /// Explicitly unreferences this buffer, returning an error if the operation fails. + /// + /// Normally buffers are unreferenced on drop. Use this if you need error handling. + pub fn unref(self) -> std::result::Result<(), Error> { + let mut raw = self.raw; + let stream_raw = self.stream.raw; + mem::forget(self); + + let (success, maybe_error) = + unsafe { try_func!(vdo_stream_buffer_unref, stream_raw, &mut raw) }; + if success != glib_sys::GTRUE { + return Err(maybe_error.unwrap_or(Error::MissingVdoError)); + } + Ok(()) + } } -// ============================================================================ -// Unit Tests (no device required) -// ============================================================================ +impl Drop for StreamBuffer<'_> { + fn drop(&mut self) { + let (_, maybe_error) = + unsafe { try_func!(vdo_stream_buffer_unref, self.stream.raw, &mut self.raw) }; + if let Some(err) = maybe_error { + log::error!("Failed to unref buffer: {}", err); + } + } +} #[cfg(test)] mod unit_tests { @@ -765,7 +544,6 @@ mod unit_tests { #[test] fn error_code_names() { - // Test that error code names are correctly mapped let err = VdoError { code: VDO_ERROR_NOT_FOUND.0 as i32, message: "test".to_string(), @@ -799,14 +577,10 @@ mod unit_tests { #[test] fn stream_builder_defaults() { let builder = StreamBuilder::default(); - // Check default values match expected assert_eq!(builder.format, VdoFormat::VDO_FORMAT_H264); assert_eq!(builder.channel, 0); assert_eq!(builder.buffer_count, 3); - assert_eq!( - builder.buffer_strategy, - VdoBufferStrategy::VDO_BUFFER_STRATEGY_INFINITE - ); + assert_eq!(builder.resolution, Resolution::Native); } #[test] @@ -814,56 +588,28 @@ mod unit_tests { let builder = StreamBuilder::new() .format(VdoFormat::VDO_FORMAT_JPEG) .channel(1) - .resolution(1280, 720) + .resolution(Resolution::Exact { + width: 1280, + height: 720, + }) .framerate(30) - .buffers(5) - .buffer_strategy(VdoBufferStrategy::VDO_BUFFER_STRATEGY_EXPLICIT); + .buffers(5); assert_eq!(builder.format, VdoFormat::VDO_FORMAT_JPEG); assert_eq!(builder.channel, 1); - assert_eq!(builder.width, 1280); - assert_eq!(builder.height, 720); - assert_eq!(builder.framerate, 30); - assert_eq!(builder.buffer_count, 5); assert_eq!( - builder.buffer_strategy, - VdoBufferStrategy::VDO_BUFFER_STRATEGY_EXPLICIT + builder.resolution, + Resolution::Exact { + width: 1280, + height: 720 + } ); - } - - #[test] - fn stream_builder_clone() { - let builder1 = StreamBuilder::new() - .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(640, 480); - - let builder2 = builder1.clone(); - - assert_eq!(builder1.format, builder2.format); - assert_eq!(builder1.width, builder2.width); - assert_eq!(builder1.height, builder2.height); - } - - #[test] - fn vdo_format_values() { - // Verify format values match expected C API values - assert_eq!(VdoFormat::VDO_FORMAT_H264.0, 0); - assert_eq!(VdoFormat::VDO_FORMAT_H265.0, 1); - assert_eq!(VdoFormat::VDO_FORMAT_JPEG.0, 2); - assert_eq!(VdoFormat::VDO_FORMAT_YUV.0, 3); - } - - #[test] - fn buffer_strategy_values() { - // Verify buffer strategy values - assert_eq!(VdoBufferStrategy::VDO_BUFFER_STRATEGY_NONE.0, 0); - assert_eq!(VdoBufferStrategy::VDO_BUFFER_STRATEGY_INFINITE.0, 4); - assert_eq!(VdoBufferStrategy::VDO_BUFFER_STRATEGY_EXPLICIT.0, 3); + assert_eq!(builder.framerate, 30); + assert_eq!(builder.buffer_count, 5); } #[test] fn error_is_send() { - // Verify Error can be sent across threads fn assert_send() {} assert_send::(); } @@ -875,6 +621,13 @@ mod unit_tests { assert!(err.message().is_empty()); } + #[test] + fn vdo_error_from_null() { + let err = VdoError::from_gerror(ptr::null_mut()); + assert_eq!(err.code(), 0); + assert!(err.message().is_empty()); + } + #[test] fn error_from_vdo_error() { let vdo_err = VdoError { @@ -893,29 +646,24 @@ mod unit_tests { #[test] fn all_error_variants_display() { - // Ensure all error variants have meaningful Display output - let errors = [ - Error::NullPointer, - Error::CStringAllocation, - Error::MissingVdoError, - Error::NoBuffersAllocated, - Error::Vdo(VdoError { - code: 1, - message: "test".to_string(), - }), - ]; - - for err in &errors { - let msg = format!("{}", err); - assert!(!msg.is_empty(), "Error display should not be empty"); - } + assert_eq!( + format!("{}", Error::NullPointer), + "VDO returned an unexpected null pointer" + ); + assert_eq!( + format!("{}", Error::MissingVdoError), + "Missing error data from VDO library" + ); + let vdo = Error::Vdo(VdoError { + code: 1, + message: "test".to_string(), + }); + assert!(!format!("{}", vdo).is_empty()); } } -// ============================================================================ -// Device Tests (require actual Axis camera) -// ============================================================================ - +// These tests require the VDO shared library (libvdo.so) and actual camera hardware. +// Results depend on the specific camera model and firmware. #[cfg(all(test, target_arch = "aarch64", feature = "device-tests"))] mod device_tests { use super::*; @@ -927,14 +675,40 @@ mod device_tests { #[test] fn stream_starts_and_stops() -> std::result::Result<(), Box> { init_logger(); - let mut stream = Stream::builder() + let stream = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(640, 480) + .resolution(Resolution::Exact { + width: 640, + height: 480, + }) + .build()?; + + let running = stream.start()?; + running.stop(); + Ok(()) + } + + #[test] + fn stream_new_default() -> std::result::Result<(), Box> { + init_logger(); + let stream = Stream::new()?; + let _info = stream.info()?; + Ok(()) + } + + #[test] + fn native_resolution() -> std::result::Result<(), Box> { + init_logger(); + let stream = Stream::builder() + .format(VdoFormat::VDO_FORMAT_YUV) .build()?; - let mut running = stream.start()?; - running.stop()?; + let running = stream.start()?; + let buffer = running.next_buffer()?; + assert!(buffer.size() > 0); + drop(buffer); + running.stop(); Ok(()) } @@ -944,11 +718,13 @@ mod device_tests { let stream = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(640, 480) + .resolution(Resolution::Exact { + width: 640, + height: 480, + }) .build()?; let info = stream.info()?; - // Just verify we can get info without error info.dump(); Ok(()) } @@ -959,7 +735,10 @@ mod device_tests { let stream = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(640, 480) + .resolution(Resolution::Exact { + width: 640, + height: 480, + }) .build()?; let settings = stream.settings()?; @@ -970,17 +749,20 @@ mod device_tests { #[test] fn capture_yuv_frames() -> std::result::Result<(), Box> { init_logger(); - let mut stream = Stream::builder() + let stream = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(640, 480) + .resolution(Resolution::Exact { + width: 640, + height: 480, + }) .build()?; - let mut running = stream.start()?; + let running = stream.start()?; - for (i, buffer) in running.iter().take(5).enumerate() { - let frame = buffer.frame()?; - let size = frame.size(); + for i in 0..5 { + let buffer = running.next_buffer()?; + let size = buffer.size(); assert!(size > 0, "Frame {} size should be > 0", i); // YUV NV12: width * height * 1.5 bytes @@ -996,104 +778,111 @@ mod device_tests { "YUV frame {}: {} bytes, seq={}, ts={}", i, size, - frame.sequence_number(), - frame.timestamp() + buffer.sequence_number(), + buffer.timestamp() ); } - running.stop()?; + running.stop(); Ok(()) } #[test] fn capture_jpeg_frames() -> std::result::Result<(), Box> { init_logger(); - let mut stream = Stream::builder() + let stream = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_JPEG) - .resolution(640, 480) + .resolution(Resolution::Exact { + width: 640, + height: 480, + }) .build()?; - let mut running = stream.start()?; + let running = stream.start()?; - for (i, buffer) in running.iter().take(5).enumerate() { - let frame = buffer.frame()?; - assert!(frame.size() > 0, "Frame {} size should be > 0", i); + for i in 0..5 { + let buffer = running.next_buffer()?; + assert!(buffer.size() > 0, "Frame {} size should be > 0", i); - // Verify JPEG magic bytes (SOI marker) let data = buffer.as_slice()?; assert!(data.len() >= 2, "Buffer too small for JPEG"); assert_eq!(data[0], 0xFF, "Invalid JPEG SOI marker"); assert_eq!(data[1], 0xD8, "Invalid JPEG SOI marker"); - log::info!("JPEG frame {}: {} bytes", i, frame.size()); + log::info!("JPEG frame {}: {} bytes", i, buffer.size()); } - running.stop()?; + running.stop(); Ok(()) } #[test] fn capture_h264_frames() -> std::result::Result<(), Box> { init_logger(); - let mut stream = Stream::builder() + let stream = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_H264) - .resolution(640, 480) + .resolution(Resolution::Exact { + width: 640, + height: 480, + }) .build()?; - let mut running = stream.start()?; + let running = stream.start()?; let mut got_i_frame = false; - let mut got_p_frame = false; - for buffer in running.iter().take(30) { - let frame = buffer.frame()?; - assert!(frame.size() > 0, "H.264 frame size should be > 0"); + for _ in 0..30 { + let buffer = running.next_buffer()?; + assert!(buffer.size() > 0, "H.264 frame size should be > 0"); - match frame.frame_type() { - VdoFrameType::VDO_FRAME_TYPE_I => got_i_frame = true, - VdoFrameType::VDO_FRAME_TYPE_P => got_p_frame = true, + match buffer.frame_type() { + VdoFrameType::VDO_FRAME_TYPE_H264_IDR | VdoFrameType::VDO_FRAME_TYPE_H264_I => { + got_i_frame = true; + } _ => {} } log::info!( "H.264 frame: {} bytes, type={:?}, seq={}", - frame.size(), - frame.frame_type(), - frame.sequence_number() + buffer.size(), + buffer.frame_type(), + buffer.sequence_number() ); } - // We should see at least an I-frame in 30 frames assert!(got_i_frame, "Should have captured at least one I-frame"); - running.stop()?; + running.stop(); Ok(()) } + /// Skips gracefully on platforms without H.265 support (e.g., Artpec-6). #[test] fn capture_h265_frames() -> std::result::Result<(), Box> { init_logger(); - // H.265 might not be supported on all platforms (e.g., Artpec-6) let stream_result = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_H265) - .resolution(640, 480) + .resolution(Resolution::Exact { + width: 640, + height: 480, + }) .build(); match stream_result { - Ok(mut stream) => { - let mut running = stream.start()?; + Ok(stream) => { + let running = stream.start()?; - for buffer in running.iter().take(10) { - let frame = buffer.frame()?; - assert!(frame.size() > 0, "H.265 frame size should be > 0"); - log::info!("H.265 frame: {} bytes", frame.size()); + for _ in 0..10 { + let buffer = running.next_buffer()?; + assert!(buffer.size() > 0, "H.265 frame size should be > 0"); + log::info!("H.265 frame: {} bytes", buffer.size()); } - running.stop()?; + running.stop(); } Err(Error::Vdo(e)) if e.code_name() == "VDO_ERROR_NOT_SUPPORTED" => { log::info!("H.265 not supported on this platform, skipping"); @@ -1107,21 +896,24 @@ mod device_tests { #[test] fn frame_timestamps_increase() -> std::result::Result<(), Box> { init_logger(); - let mut stream = Stream::builder() + let stream = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(320, 240) + .resolution(Resolution::Exact { + width: 320, + height: 240, + }) .framerate(15) .build()?; - let mut running = stream.start()?; + let running = stream.start()?; let mut prev_ts = 0u64; let mut prev_seq = 0u32; - for (i, buffer) in running.iter().take(10).enumerate() { - let frame = buffer.frame()?; - let ts = frame.timestamp(); - let seq = frame.sequence_number(); + for i in 0..10 { + let buffer = running.next_buffer()?; + let ts = buffer.timestamp(); + let seq = buffer.sequence_number(); if i > 0 { assert!( @@ -1142,35 +934,144 @@ mod device_tests { prev_seq = seq; } - running.stop()?; + running.stop(); Ok(()) } #[test] fn buffer_data_accessible() -> std::result::Result<(), Box> { init_logger(); - let mut stream = Stream::builder() + let stream = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(320, 240) + .resolution(Resolution::Exact { + width: 320, + height: 240, + }) .build()?; - let mut running = stream.start()?; + let running = stream.start()?; - for buffer in running.iter().take(3) { - let frame = buffer.frame()?; + for _ in 0..3 { + let buffer = running.next_buffer()?; let capacity = buffer.capacity(); let data = buffer.as_slice()?; assert_eq!(data.len(), capacity, "Slice length should match capacity"); - assert!(frame.size() <= capacity, "Frame size should be <= capacity"); + assert!( + buffer.size() <= capacity, + "Frame size should be <= capacity" + ); - // Verify we can read data without crashing - let _first_byte = data[0]; - let _last_byte = data[frame.size().saturating_sub(1)]; + std::hint::black_box(data[0]); + std::hint::black_box(data[buffer.size().saturating_sub(1)]); } - running.stop()?; + running.stop(); + Ok(()) + } + + #[test] + fn data_copy_returns_frame_data() -> std::result::Result<(), Box> { + init_logger(); + let stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(Resolution::Exact { + width: 320, + height: 240, + }) + .build()?; + + let running = stream.start()?; + let buffer = running.next_buffer()?; + + let copy = buffer.data_copy()?; + assert_eq!(copy.len(), buffer.size()); + + // Verify copy matches the original slice + let slice = buffer.as_slice()?; + assert_eq!(©[..], &slice[..copy.len()]); + + drop(buffer); + running.stop(); + Ok(()) + } + + #[test] + fn file_descriptor_is_valid() -> std::result::Result<(), Box> { + init_logger(); + let stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(Resolution::Exact { + width: 320, + height: 240, + }) + .build()?; + + let running = stream.start()?; + let buffer = running.next_buffer()?; + + use std::os::fd::AsRawFd; + let fd = buffer.file_descriptor()?; + assert!( + fd.as_raw_fd() >= 0, + "File descriptor should be non-negative" + ); + + drop(buffer); + running.stop(); + Ok(()) + } + + #[test] + fn explicit_unref() -> std::result::Result<(), Box> { + init_logger(); + let stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(Resolution::Exact { + width: 320, + height: 240, + }) + .build()?; + + let running = stream.start()?; + let buffer = running.next_buffer()?; + buffer.unref()?; + + running.stop(); + Ok(()) + } + + #[test] + fn all_buffer_metadata_accessible() -> std::result::Result<(), Box> { + init_logger(); + let stream = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(Resolution::Exact { + width: 320, + height: 240, + }) + .build()?; + + let running = stream.start()?; + let buffer = running.next_buffer()?; + + // Exercise all metadata accessors + std::hint::black_box(buffer.size()); + std::hint::black_box(buffer.capacity()); + std::hint::black_box(buffer.frame_type()); + std::hint::black_box(buffer.sequence_number()); + std::hint::black_box(buffer.timestamp()); + std::hint::black_box(buffer.custom_timestamp_us()); + std::hint::black_box(buffer.header_size()); + std::hint::black_box(buffer.is_last_buffer()); + + drop(buffer); + running.stop(); Ok(()) } @@ -1178,45 +1079,89 @@ mod device_tests { fn multiple_streams_sequential() -> std::result::Result<(), Box> { init_logger(); - // Create and use first stream { - let mut stream = Stream::builder() + let stream = Stream::builder() .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(320, 240) + .resolution(Resolution::Exact { + width: 320, + height: 240, + }) .build()?; - let mut running = stream.start()?; - let _ = running.iter().take(3).count(); - running.stop()?; + let running = stream.start()?; + for _ in 0..3 { + let _buf = running.next_buffer()?; + } + running.stop(); } - // Create and use second stream { - let mut stream = Stream::builder() + let stream = Stream::builder() .format(VdoFormat::VDO_FORMAT_JPEG) - .resolution(320, 240) + .resolution(Resolution::Exact { + width: 320, + height: 240, + }) .build()?; - let mut running = stream.start()?; - let _ = running.iter().take(3).count(); - running.stop()?; + let running = stream.start()?; + for _ in 0..3 { + let _buf = running.next_buffer()?; + } + running.stop(); } Ok(()) } - // ======================================================================== - // Error Handling Tests - // ======================================================================== + /// Two streams open simultaneously, polled round-robin from one thread. + /// May fail if the camera doesn't support multiple concurrent streams. + #[test] + fn interleaved_streams() -> std::result::Result<(), Box> { + init_logger(); + + let stream1 = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_YUV) + .resolution(Resolution::Exact { + width: 320, + height: 240, + }) + .build()?; + + let stream2 = Stream::builder() + .channel(0) + .format(VdoFormat::VDO_FORMAT_JPEG) + .resolution(Resolution::Exact { + width: 320, + height: 240, + }) + .build()?; + + let running1 = stream1.start()?; + let running2 = stream2.start()?; + + for _ in 0..3 { + let _buf1 = running1.next_buffer()?; + let _buf2 = running2.next_buffer()?; + } + + running1.stop(); + running2.stop(); + + Ok(()) + } #[test] fn invalid_channel_returns_error() { init_logger(); - // Channel 999 should not exist on any camera let result = Stream::builder() .channel(999) .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(640, 480) + .resolution(Resolution::Exact { + width: 640, + height: 480, + }) .build(); assert!(result.is_err(), "Invalid channel should return error"); @@ -1225,38 +1170,40 @@ mod device_tests { } } + /// Observational test: result is platform-dependent, logged but not asserted. #[test] - fn unsupported_format_returns_error() { + fn unsupported_format_logged() { init_logger(); - // Try a format that might not be supported (platform-dependent) - // VDO_FORMAT_BAYER is often not supported let result = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_BAYER) - .resolution(640, 480) + .resolution(Resolution::Exact { + width: 640, + height: 480, + }) .build(); - // This might succeed on some platforms, so just log the result match result { Ok(_) => log::info!("BAYER format is supported on this platform"), Err(e) => log::info!("BAYER format not supported: {}", e), } } + /// Observational test: the camera may adjust or reject unusual resolutions. #[test] - fn invalid_resolution_handled() { + fn invalid_resolution_logged() { init_logger(); - // Try an unusual resolution that might not be supported let result = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(12345, 6789) // Unusual resolution + .resolution(Resolution::Exact { + width: 12345, + height: 6789, + }) .build(); - // Camera might adjust resolution or return error match result { Ok(stream) => { - // Camera accepted - check actual resolution via info if let Ok(info) = stream.info() { log::info!("Camera accepted unusual resolution, check actual via info"); info.dump(); @@ -1268,45 +1215,28 @@ mod device_tests { } } - #[test] - fn stream_stop_is_idempotent() -> std::result::Result<(), Box> { - init_logger(); - let mut stream = Stream::builder() - .channel(0) - .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(320, 240) - .build()?; - - let mut running = stream.start()?; - let _ = running.iter().take(1).count(); - - // Stop multiple times should not crash - running.stop()?; - running.stop()?; // Second stop should be safe - - Ok(()) - } - + /// Tests that dropping a RunningStream without calling stop() doesn't crash. #[test] fn stream_dropped_without_stop() -> std::result::Result<(), Box> { init_logger(); - // Create stream, start it, get some frames, then drop without calling stop() - // This tests that Drop implementation properly cleans up { - let mut stream = Stream::builder() + let stream = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(320, 240) + .resolution(Resolution::Exact { + width: 320, + height: 240, + }) .build()?; - let mut running = stream.start()?; - let _ = running.iter().take(2).count(); + let running = stream.start()?; + for _ in 0..2 { + let _buf = running.next_buffer()?; + } // Intentionally NOT calling running.stop() - // Drop should handle cleanup } - // If we get here without crash, cleanup worked log::info!("Stream dropped without explicit stop - cleanup successful"); Ok(()) } @@ -1315,23 +1245,20 @@ mod device_tests { fn error_message_is_descriptive() { init_logger(); - // Force an error and check that the message is helpful - let result = Stream::builder() - .channel(999) // Invalid - .build(); - - if let Err(Error::Vdo(e)) = result { - let msg = e.message(); - let code_name = e.code_name(); - log::info!( - "Error code: {}, name: {}, message: {}", - e.code(), - code_name, - msg - ); + let err = Stream::builder() + .channel(999) + .build() + .expect_err("Channel 999 should fail"); - // Error should have some information - assert!(!code_name.is_empty(), "Error code name should not be empty"); + match err { + Error::Vdo(e) => { + assert!( + !e.code_name().is_empty(), + "Error code name should not be empty" + ); + assert!(!e.message().is_empty(), "Error message should not be empty"); + } + other => panic!("Expected Error::Vdo, got: {:?}", other), } } @@ -1339,21 +1266,48 @@ mod device_tests { fn rapid_stream_creation_destruction() -> std::result::Result<(), Box> { init_logger(); - // Rapidly create and destroy streams to test for resource leaks for i in 0..5 { - let mut stream = Stream::builder() + let stream = Stream::builder() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(320, 240) + .resolution(Resolution::Exact { + width: 320, + height: 240, + }) .build()?; - let mut running = stream.start()?; - let _ = running.iter().take(1).count(); - running.stop()?; + let running = stream.start()?; + drop(running.next_buffer()?); + running.stop(); log::info!("Rapid cycle {} complete", i); } Ok(()) } + + // This test only requires libvdo.so, not camera hardware, but is placed here + // because the VDO library is only available on the device. + #[test] + fn map_get_set_operations() -> std::result::Result<(), Box> { + init_logger(); + + let mut map = Map::try_new()?; + + map.set_u32(c"test_u32", 42); + assert_eq!(map.get_u32(c"test_u32", 0), 42); + assert_eq!(map.get_u32(c"missing_key", 99), 99); + + map.set_bool(c"test_bool", true); + assert!(map.get_bool(c"test_bool", false)); + assert!(!map.get_bool(c"missing_bool", false)); + + map.set_string(c"test_str", c"hello"); + let value = map.get_string(c"test_str"); + assert!(value.is_some()); + assert_eq!(value.unwrap().as_c_str().to_str().unwrap(), "hello"); + assert!(map.get_string(c"missing_str").is_none()); + + Ok(()) + } } diff --git a/crates/vdo/src/map.rs b/crates/vdo/src/map.rs new file mode 100644 index 00000000..5e1d2729 --- /dev/null +++ b/crates/vdo/src/map.rs @@ -0,0 +1,128 @@ +//! Key-value map for VDO settings and a GLib-allocated C string type. + +use gobject_sys::{g_object_unref, GObject}; +use std::ffi::{c_char, c_void, CStr}; +use std::ptr::{self, NonNull}; +use vdo_sys::*; + +/// An owned pointer to a C string allocated by GLib. +/// +/// The string is freed with `g_free` when dropped. +#[repr(transparent)] +pub struct CStringPtr(NonNull); + +impl CStringPtr { + /// # Safety + /// + /// In addition to the safety preconditions for [`CStr::from_ptr`] the memory must have been + /// allocated in a manner compatible with [`glib_sys::g_free`] and there must be no other + /// users of this memory. + pub(crate) unsafe fn from_ptr(ptr: *mut c_char) -> Self { + debug_assert!(!ptr.is_null()); + Self(NonNull::new_unchecked(ptr)) + } + + pub fn as_c_str(&self) -> &CStr { + // SAFETY: The preconditions for instantiating this type include all preconditions + // for `CStr::from_ptr`. + unsafe { CStr::from_ptr(self.0.as_ptr() as *const c_char) } + } +} + +impl Drop for CStringPtr { + fn drop(&mut self) { + // SAFETY: We have full ownership, allocated in a manner compatible with `g_free`. + unsafe { + glib_sys::g_free(self.0.as_ptr() as *mut c_void); + } + } +} + +/// A key-value map for VDO settings. +/// +/// Used to configure stream parameters and retrieve stream information. +/// All methods assume `self.raw` is a valid `VdoMap` pointer, which is +/// guaranteed by the constructors. +pub struct Map { + raw: *mut VdoMap, +} + +impl Map { + pub fn try_new() -> std::result::Result { + let map = unsafe { vdo_map_new() }; + if map.is_null() { + Err(super::Error::NullPointer) + } else { + Ok(Self { raw: map }) + } + } + + /// # Safety + /// + /// `ptr` must be a non-null, valid `VdoMap` pointer with ownership + /// transferred to this `Map` (it will be unreferenced on drop). + pub(crate) unsafe fn from_raw(ptr: *mut VdoMap) -> Self { + debug_assert!(!ptr.is_null()); + Self { raw: ptr } + } + + pub fn set_u32(&mut self, key: &CStr, value: u32) { + unsafe { vdo_map_set_uint32(self.raw, key.as_ptr(), value) } + } + + pub fn get_u32(&self, key: &CStr, default: u32) -> u32 { + unsafe { vdo_map_get_uint32(self.raw, key.as_ptr(), default) } + } + + pub fn set_string(&mut self, key: &CStr, value: &CStr) { + unsafe { vdo_map_set_string(self.raw, key.as_ptr(), value.as_ptr()) } + } + + /// Returns `None` if the key doesn't exist or the value is null. + pub fn get_string(&self, key: &CStr) -> Option { + // Passing null as default so missing keys yield null -> None. + let ptr = unsafe { vdo_map_dup_string(self.raw, key.as_ptr(), ptr::null()) }; + if ptr.is_null() { + return None; + } + // SAFETY: ptr is non-null, allocated by g_malloc via vdo_map_dup_string, and we own it. + Some(unsafe { CStringPtr::from_ptr(ptr) }) + } + + pub fn set_bool(&mut self, key: &CStr, value: bool) { + let gvalue = if value { + glib_sys::GTRUE + } else { + glib_sys::GFALSE + }; + unsafe { vdo_map_set_boolean(self.raw, key.as_ptr(), gvalue) } + } + + pub fn get_bool(&self, key: &CStr, default: bool) -> bool { + let gdefault = if default { + glib_sys::GTRUE + } else { + glib_sys::GFALSE + }; + unsafe { vdo_map_get_boolean(self.raw, key.as_ptr(), gdefault) != glib_sys::GFALSE } + } + + /// Dumps the map contents to stdout (for debugging). + pub fn dump(&self) { + unsafe { vdo_map_dump(self.raw) } + } + + pub(crate) fn as_ptr(&self) -> *mut VdoMap { + self.raw + } +} + +// SAFETY: We hold exclusive ownership of the GObject reference. +// Sync is NOT implemented because GLib objects are not safe for concurrent access. +unsafe impl Send for Map {} + +impl Drop for Map { + fn drop(&mut self) { + unsafe { g_object_unref(self.raw as *mut GObject) } + } +} From 05638beeeb62a14fd2267c385a00e7e688572847 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Sat, 21 Feb 2026 13:13:44 +0100 Subject: [PATCH 03/29] refactor(vdo): Replace glob imports with qualified paths and add expect-test Replace `use vdo_sys::*` with explicit type imports and `vdo_sys::`-qualified function/constant calls at call sites. This addresses the PR review feedback about avoiding glob imports. Also adds the missing VDO_ERROR_NO_VIDEO error code and adopts expect-test for display/formatting tests. --- Cargo.lock | 19 ++++++- Cargo.toml | 1 + crates/vdo/Cargo.toml | 1 + crates/vdo/src/lib.rs | 118 ++++++++++++++++++++---------------------- crates/vdo/src/map.rs | 18 +++---- 5 files changed, 86 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a4ffe3b..fa1f90dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -829,6 +829,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "dissimilar" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8975ffdaa0ef3661bfe02dbdcc06c9f829dfafe6a3c474de366a8d5e44276921" + [[package]] name = "either" version = "1.11.0" @@ -920,6 +926,16 @@ dependencies = [ "log", ] +[[package]] +name = "expect-test" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63af43ff4431e848fb47472a920f14fa71c24de13255a5692e93d4e90302acb0" +dependencies = [ + "dissimilar", + "once_cell", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1634,7 +1650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -3163,6 +3179,7 @@ version = "0.0.0" dependencies = [ "anyhow", "env_logger", + "expect-test", "glib-sys", "gobject-sys", "log", diff --git a/Cargo.toml b/Cargo.toml index e8965429..b0033c9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ diqwest = "3.1.0" digest_auth = "0.3.1" dirs = "5.0.1" env_logger = { version = "0.11.3", default-features = false } +expect-test = "1.5.1" futures = "0.3.30" futures-lite = "2.6.0" futures-util = "0.3.30" diff --git a/crates/vdo/Cargo.toml b/crates/vdo/Cargo.toml index 22b49739..f5d2cb52 100644 --- a/crates/vdo/Cargo.toml +++ b/crates/vdo/Cargo.toml @@ -18,6 +18,7 @@ device-tests = [] [dev-dependencies] anyhow = { workspace = true } env_logger = { workspace = true } +expect-test = { workspace = true } [[example]] name = "basic" diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index d01572ed..5891e429 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -54,14 +54,14 @@ use gobject_sys::{g_object_unref, GObject}; use std::fmt::{Debug, Display}; use std::mem; use std::ptr; -use vdo_sys::*; +use vdo_sys::{VdoBuffer, VdoBufferStrategy, VdoStream}; pub use vdo_sys::{VdoFormat, VdoFrameType, VdoRateControlMode, VdoRateControlPriority}; /// Macro for calling VDO functions that take a GError** parameter. /// Returns a tuple of (result, Option). macro_rules! try_func { - ($func:ident $(,)?) => {{ + ($func:path $(,)?) => {{ let mut error: *mut GError = ptr::null_mut(); let success = $func(&mut error); if error.is_null() { @@ -70,7 +70,7 @@ macro_rules! try_func { (success, Some(Error::Vdo(VdoError::from_gerror(error)))) } }}; - ($func:ident, $($arg:expr),+ $(,)?) => {{ + ($func:path, $($arg:expr),+ $(,)?) => {{ let mut error: *mut GError = ptr::null_mut(); let success = $func($( $arg ),+, &mut error); if error.is_null() { @@ -129,26 +129,27 @@ impl VdoError { pub fn code_name(&self) -> &'static str { let code = self.code as u32; match code { - x if x == VDO_ERROR_NOT_FOUND.0 => "VDO_ERROR_NOT_FOUND", - x if x == VDO_ERROR_EXISTS.0 => "VDO_ERROR_EXISTS", - x if x == VDO_ERROR_INVALID_ARGUMENT.0 => "VDO_ERROR_INVALID_ARGUMENT", - x if x == VDO_ERROR_PERMISSION_DENIED.0 => "VDO_ERROR_PERMISSION_DENIED", - x if x == VDO_ERROR_NOT_SUPPORTED.0 => "VDO_ERROR_NOT_SUPPORTED", - x if x == VDO_ERROR_CLOSED.0 => "VDO_ERROR_CLOSED", - x if x == VDO_ERROR_BUSY.0 => "VDO_ERROR_BUSY", - x if x == VDO_ERROR_IO.0 => "VDO_ERROR_IO", - x if x == VDO_ERROR_HAL.0 => "VDO_ERROR_HAL", - x if x == VDO_ERROR_DBUS.0 => "VDO_ERROR_DBUS", - x if x == VDO_ERROR_OOM.0 => "VDO_ERROR_OOM", - x if x == VDO_ERROR_IDLE.0 => "VDO_ERROR_IDLE", - x if x == VDO_ERROR_NO_DATA.0 => "VDO_ERROR_NO_DATA", - x if x == VDO_ERROR_NO_BUFFER_SPACE.0 => "VDO_ERROR_NO_BUFFER_SPACE", - x if x == VDO_ERROR_BUFFER_FAILURE.0 => "VDO_ERROR_BUFFER_FAILURE", - x if x == VDO_ERROR_INTERFACE_DOWN.0 => "VDO_ERROR_INTERFACE_DOWN", - x if x == VDO_ERROR_FAILED.0 => "VDO_ERROR_FAILED", - x if x == VDO_ERROR_FATAL.0 => "VDO_ERROR_FATAL", - x if x == VDO_ERROR_NOT_CONTROLLED.0 => "VDO_ERROR_NOT_CONTROLLED", - x if x == VDO_ERROR_NO_EVENT.0 => "VDO_ERROR_NO_EVENT", + x if x == vdo_sys::VDO_ERROR_NOT_FOUND.0 => "VDO_ERROR_NOT_FOUND", + x if x == vdo_sys::VDO_ERROR_EXISTS.0 => "VDO_ERROR_EXISTS", + x if x == vdo_sys::VDO_ERROR_INVALID_ARGUMENT.0 => "VDO_ERROR_INVALID_ARGUMENT", + x if x == vdo_sys::VDO_ERROR_PERMISSION_DENIED.0 => "VDO_ERROR_PERMISSION_DENIED", + x if x == vdo_sys::VDO_ERROR_NOT_SUPPORTED.0 => "VDO_ERROR_NOT_SUPPORTED", + x if x == vdo_sys::VDO_ERROR_CLOSED.0 => "VDO_ERROR_CLOSED", + x if x == vdo_sys::VDO_ERROR_BUSY.0 => "VDO_ERROR_BUSY", + x if x == vdo_sys::VDO_ERROR_IO.0 => "VDO_ERROR_IO", + x if x == vdo_sys::VDO_ERROR_HAL.0 => "VDO_ERROR_HAL", + x if x == vdo_sys::VDO_ERROR_DBUS.0 => "VDO_ERROR_DBUS", + x if x == vdo_sys::VDO_ERROR_OOM.0 => "VDO_ERROR_OOM", + x if x == vdo_sys::VDO_ERROR_IDLE.0 => "VDO_ERROR_IDLE", + x if x == vdo_sys::VDO_ERROR_NO_DATA.0 => "VDO_ERROR_NO_DATA", + x if x == vdo_sys::VDO_ERROR_NO_BUFFER_SPACE.0 => "VDO_ERROR_NO_BUFFER_SPACE", + x if x == vdo_sys::VDO_ERROR_BUFFER_FAILURE.0 => "VDO_ERROR_BUFFER_FAILURE", + x if x == vdo_sys::VDO_ERROR_INTERFACE_DOWN.0 => "VDO_ERROR_INTERFACE_DOWN", + x if x == vdo_sys::VDO_ERROR_FAILED.0 => "VDO_ERROR_FAILED", + x if x == vdo_sys::VDO_ERROR_FATAL.0 => "VDO_ERROR_FATAL", + x if x == vdo_sys::VDO_ERROR_NOT_CONTROLLED.0 => "VDO_ERROR_NOT_CONTROLLED", + x if x == vdo_sys::VDO_ERROR_NO_EVENT.0 => "VDO_ERROR_NO_EVENT", + x if x == vdo_sys::VDO_ERROR_NO_VIDEO.0 => "VDO_ERROR_NO_VIDEO", _ => "VDO_ERROR_UNKNOWN", } } @@ -288,7 +289,7 @@ impl StreamBuilder { VdoBufferStrategy::VDO_BUFFER_STRATEGY_INFINITE.0, ); - let (stream_raw, maybe_error) = unsafe { try_func!(vdo_stream_new, map.as_ptr(), None) }; + let (stream_raw, maybe_error) = unsafe { try_func!(vdo_sys::vdo_stream_new, map.as_ptr(), None) }; if stream_raw.is_null() { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); @@ -348,7 +349,7 @@ impl Stream { /// Returns stream information (actual resolution, format, etc.) as a map. pub fn info(&self) -> std::result::Result { - let (map_raw, maybe_error) = unsafe { try_func!(vdo_stream_get_info, self.raw) }; + let (map_raw, maybe_error) = unsafe { try_func!(vdo_sys::vdo_stream_get_info, self.raw) }; if map_raw.is_null() { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); } @@ -358,7 +359,7 @@ impl Stream { /// Returns stream settings as a map. pub fn settings(&self) -> std::result::Result { - let (map_raw, maybe_error) = unsafe { try_func!(vdo_stream_get_settings, self.raw) }; + let (map_raw, maybe_error) = unsafe { try_func!(vdo_sys::vdo_stream_get_settings, self.raw) }; if map_raw.is_null() { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); } @@ -369,7 +370,7 @@ impl Stream { /// /// On failure, the underlying stream is automatically cleaned up. pub fn start(self) -> std::result::Result { - let (success, maybe_error) = unsafe { try_func!(vdo_stream_start, self.raw) }; + let (success, maybe_error) = unsafe { try_func!(vdo_sys::vdo_stream_start, self.raw) }; if success != glib_sys::GTRUE { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); } @@ -380,7 +381,7 @@ impl Stream { impl Drop for Stream { fn drop(&mut self) { // vdo_stream_stop is idempotent (returns void), safe to call even if never started. - unsafe { vdo_stream_stop(self.raw) }; + unsafe { vdo_sys::vdo_stream_stop(self.raw) }; // Release our GObject reference to avoid leaking. unsafe { g_object_unref(self.raw as *mut GObject) }; } @@ -403,7 +404,7 @@ impl RunningStream { /// Blocks until a new frame is available and returns it. pub fn next_buffer(&self) -> std::result::Result, Error> { let (buffer_ptr, maybe_error) = - unsafe { try_func!(vdo_stream_get_buffer, self.stream.raw) }; + unsafe { try_func!(vdo_sys::vdo_stream_get_buffer, self.stream.raw) }; if buffer_ptr.is_null() { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); @@ -417,7 +418,7 @@ impl RunningStream { /// Stops the stream, consuming this handle. pub fn stop(self) { - unsafe { vdo_stream_stop(self.stream.raw) }; + unsafe { vdo_sys::vdo_stream_stop(self.stream.raw) }; // self.stream dropped here -> Stream::drop calls g_object_unref } } @@ -442,14 +443,14 @@ pub struct StreamBuffer<'a> { impl StreamBuffer<'_> { pub fn capacity(&self) -> usize { - unsafe { vdo_buffer_get_capacity(self.raw) } + unsafe { vdo_sys::vdo_buffer_get_capacity(self.raw) } } /// Returns the frame data as a byte slice of [`capacity()`](StreamBuffer::capacity) bytes. /// /// Use [`size()`](StreamBuffer::size) to get the actual frame data size. pub fn as_slice(&self) -> std::result::Result<&[u8], Error> { - let data = unsafe { vdo_buffer_get_data(self.raw) }; + let data = unsafe { vdo_sys::vdo_buffer_get_data(self.raw) }; if data.is_null() { return Err(Error::NullPointer); } @@ -461,7 +462,7 @@ impl StreamBuffer<'_> { /// Returns a copy of exactly [`size()`](StreamBuffer::size) bytes of frame data. pub fn data_copy(&self) -> std::result::Result, Error> { - let data = unsafe { vdo_buffer_get_data(self.raw) }; + let data = unsafe { vdo_sys::vdo_buffer_get_data(self.raw) }; if data.is_null() { return Err(Error::NullPointer); } @@ -471,35 +472,35 @@ impl StreamBuffer<'_> { } pub fn frame_type(&self) -> VdoFrameType { - unsafe { vdo_frame_get_frame_type(self.raw) } + unsafe { vdo_sys::vdo_frame_get_frame_type(self.raw) } } /// Starts at 0 and increments with each frame. Wrap-around point is undefined. pub fn sequence_number(&self) -> u32 { - unsafe { vdo_frame_get_sequence_nbr(self.raw) } + unsafe { vdo_sys::vdo_frame_get_sequence_nbr(self.raw) } } /// Timestamp in microseconds since boot. pub fn timestamp(&self) -> u64 { - unsafe { vdo_frame_get_timestamp(self.raw) } + unsafe { vdo_sys::vdo_frame_get_timestamp(self.raw) } } pub fn custom_timestamp_us(&self) -> i64 { - unsafe { vdo_frame_get_custom_timestamp(self.raw) } + unsafe { vdo_sys::vdo_frame_get_custom_timestamp(self.raw) } } /// Actual frame data size in bytes (may be less than [`capacity()`](StreamBuffer::capacity)). pub fn size(&self) -> usize { - unsafe { vdo_frame_get_size(self.raw) } + unsafe { vdo_sys::vdo_frame_get_size(self.raw) } } pub fn header_size(&self) -> isize { - unsafe { vdo_frame_get_header_size(self.raw) } + unsafe { vdo_sys::vdo_frame_get_header_size(self.raw) } } /// Returns a borrowed file descriptor for the buffer's backing memory. pub fn file_descriptor(&self) -> std::result::Result, Error> { - let fd = unsafe { vdo_buffer_get_fd(self.raw) }; + let fd = unsafe { vdo_sys::vdo_buffer_get_fd(self.raw) }; if fd < 0 { return Err(Error::NullPointer); } @@ -508,7 +509,7 @@ impl StreamBuffer<'_> { } pub fn is_last_buffer(&self) -> bool { - unsafe { vdo_frame_get_is_last_buffer(self.raw) != 0 } + unsafe { vdo_sys::vdo_frame_get_is_last_buffer(self.raw) != 0 } } /// Explicitly unreferences this buffer, returning an error if the operation fails. @@ -520,7 +521,7 @@ impl StreamBuffer<'_> { mem::forget(self); let (success, maybe_error) = - unsafe { try_func!(vdo_stream_buffer_unref, stream_raw, &mut raw) }; + unsafe { try_func!(vdo_sys::vdo_stream_buffer_unref, stream_raw, &mut raw) }; if success != glib_sys::GTRUE { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); } @@ -531,7 +532,7 @@ impl StreamBuffer<'_> { impl Drop for StreamBuffer<'_> { fn drop(&mut self) { let (_, maybe_error) = - unsafe { try_func!(vdo_stream_buffer_unref, self.stream.raw, &mut self.raw) }; + unsafe { try_func!(vdo_sys::vdo_stream_buffer_unref, self.stream.raw, &mut self.raw) }; if let Some(err) = maybe_error { log::error!("Failed to unref buffer: {}", err); } @@ -541,37 +542,36 @@ impl Drop for StreamBuffer<'_> { #[cfg(test)] mod unit_tests { use super::*; + use expect_test::expect; #[test] fn error_code_names() { let err = VdoError { - code: VDO_ERROR_NOT_FOUND.0 as i32, + code: vdo_sys::VDO_ERROR_NOT_FOUND.0 as i32, message: "test".to_string(), }; - assert_eq!(err.code_name(), "VDO_ERROR_NOT_FOUND"); + expect!["VDO_ERROR_NOT_FOUND"].assert_eq(err.code_name()); let err = VdoError { - code: VDO_ERROR_NOT_SUPPORTED.0 as i32, + code: vdo_sys::VDO_ERROR_NOT_SUPPORTED.0 as i32, message: "test".to_string(), }; - assert_eq!(err.code_name(), "VDO_ERROR_NOT_SUPPORTED"); + expect!["VDO_ERROR_NOT_SUPPORTED"].assert_eq(err.code_name()); let err = VdoError { code: 9999, message: "test".to_string(), }; - assert_eq!(err.code_name(), "VDO_ERROR_UNKNOWN"); + expect!["VDO_ERROR_UNKNOWN"].assert_eq(err.code_name()); } #[test] fn error_display() { let err = VdoError { - code: VDO_ERROR_BUSY.0 as i32, + code: vdo_sys::VDO_ERROR_BUSY.0 as i32, message: "Resource is busy".to_string(), }; - let display = format!("{}", err); - assert!(display.contains("VDO_ERROR_BUSY")); - assert!(display.contains("Resource is busy")); + expect!["VDO_ERROR_BUSY (7): Resource is busy"].assert_eq(&format!("{err}")); } #[test] @@ -646,19 +646,15 @@ mod unit_tests { #[test] fn all_error_variants_display() { - assert_eq!( - format!("{}", Error::NullPointer), - "VDO returned an unexpected null pointer" - ); - assert_eq!( - format!("{}", Error::MissingVdoError), - "Missing error data from VDO library" - ); + expect!["VDO returned an unexpected null pointer"] + .assert_eq(&format!("{}", Error::NullPointer)); + expect!["Missing error data from VDO library"] + .assert_eq(&format!("{}", Error::MissingVdoError)); let vdo = Error::Vdo(VdoError { code: 1, message: "test".to_string(), }); - assert!(!format!("{}", vdo).is_empty()); + expect!["VDO_ERROR_NOT_FOUND (1): test"].assert_eq(&format!("{vdo}")); } } diff --git a/crates/vdo/src/map.rs b/crates/vdo/src/map.rs index 5e1d2729..20a48fef 100644 --- a/crates/vdo/src/map.rs +++ b/crates/vdo/src/map.rs @@ -3,7 +3,7 @@ use gobject_sys::{g_object_unref, GObject}; use std::ffi::{c_char, c_void, CStr}; use std::ptr::{self, NonNull}; -use vdo_sys::*; +use vdo_sys::VdoMap; /// An owned pointer to a C string allocated by GLib. /// @@ -49,7 +49,7 @@ pub struct Map { impl Map { pub fn try_new() -> std::result::Result { - let map = unsafe { vdo_map_new() }; + let map = unsafe { vdo_sys::vdo_map_new() }; if map.is_null() { Err(super::Error::NullPointer) } else { @@ -67,21 +67,21 @@ impl Map { } pub fn set_u32(&mut self, key: &CStr, value: u32) { - unsafe { vdo_map_set_uint32(self.raw, key.as_ptr(), value) } + unsafe { vdo_sys::vdo_map_set_uint32(self.raw, key.as_ptr(), value) } } pub fn get_u32(&self, key: &CStr, default: u32) -> u32 { - unsafe { vdo_map_get_uint32(self.raw, key.as_ptr(), default) } + unsafe { vdo_sys::vdo_map_get_uint32(self.raw, key.as_ptr(), default) } } pub fn set_string(&mut self, key: &CStr, value: &CStr) { - unsafe { vdo_map_set_string(self.raw, key.as_ptr(), value.as_ptr()) } + unsafe { vdo_sys::vdo_map_set_string(self.raw, key.as_ptr(), value.as_ptr()) } } /// Returns `None` if the key doesn't exist or the value is null. pub fn get_string(&self, key: &CStr) -> Option { // Passing null as default so missing keys yield null -> None. - let ptr = unsafe { vdo_map_dup_string(self.raw, key.as_ptr(), ptr::null()) }; + let ptr = unsafe { vdo_sys::vdo_map_dup_string(self.raw, key.as_ptr(), ptr::null()) }; if ptr.is_null() { return None; } @@ -95,7 +95,7 @@ impl Map { } else { glib_sys::GFALSE }; - unsafe { vdo_map_set_boolean(self.raw, key.as_ptr(), gvalue) } + unsafe { vdo_sys::vdo_map_set_boolean(self.raw, key.as_ptr(), gvalue) } } pub fn get_bool(&self, key: &CStr, default: bool) -> bool { @@ -104,12 +104,12 @@ impl Map { } else { glib_sys::GFALSE }; - unsafe { vdo_map_get_boolean(self.raw, key.as_ptr(), gdefault) != glib_sys::GFALSE } + unsafe { vdo_sys::vdo_map_get_boolean(self.raw, key.as_ptr(), gdefault) != glib_sys::GFALSE } } /// Dumps the map contents to stdout (for debugging). pub fn dump(&self) { - unsafe { vdo_map_dump(self.raw) } + unsafe { vdo_sys::vdo_map_dump(self.raw) } } pub(crate) fn as_ptr(&self) -> *mut VdoMap { From b89acc07ee531a866ef264a53948055a99c82083 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Sat, 21 Feb 2026 13:46:13 +0100 Subject: [PATCH 04/29] fix(vdo): Address code review and security audit findings - Clamp size to capacity in data_copy to prevent out-of-bounds reads - Replace debug_assert with assert in unsafe constructors (from_ptr, from_raw) - Document BorrowedFd lifetime constraints on file_descriptor() - Fix double vdo_stream_stop by delegating to Drop - Add Error::InvalidFd instead of overloading NullPointer for bad fds - Return Option from header_size() for negative (no header) values - Fix from_gerror safety comment to match actual code order - Add Debug for Map and CStringPtr, Deref for CStringPtr - Use explicit ptr::null::(), comment as_ptr GLib convention --- crates/vdo/src/lib.rs | 28 ++++++++++++++++++++-------- crates/vdo/src/map.rs | 29 ++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 5891e429..184f1fcc 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -88,6 +88,8 @@ pub enum Error { Vdo(#[from] VdoError), #[error("VDO returned an unexpected null pointer")] NullPointer, + #[error("VDO returned an invalid file descriptor")] + InvalidFd, #[error("Missing error data from VDO library")] MissingVdoError, } @@ -105,8 +107,9 @@ impl VdoError { return VdoError::default(); } - // SAFETY: gerror is non-null. We copy the struct and read the message pointer - // before calling g_error_free, which would invalidate both. + // SAFETY: gerror is non-null. We dereference the struct to copy its fields + // (code, message pointer), then read the message string, all before calling + // g_error_free which invalidates the GError and its contents. let g_error = unsafe { *gerror }; let message = if g_error.message.is_null() { String::from("Unknown error") @@ -418,8 +421,8 @@ impl RunningStream { /// Stops the stream, consuming this handle. pub fn stop(self) { - unsafe { vdo_sys::vdo_stream_stop(self.stream.raw) }; - // self.stream dropped here -> Stream::drop calls g_object_unref + // Dropping self triggers Stream::drop which calls vdo_stream_stop + g_object_unref. + drop(self); } } @@ -466,7 +469,8 @@ impl StreamBuffer<'_> { if data.is_null() { return Err(Error::NullPointer); } - let size = self.size(); + // Clamp size to capacity to avoid reading beyond the mapped region. + let size = self.size().min(self.capacity()); let slice = unsafe { std::slice::from_raw_parts(data as *const u8, size) }; Ok(slice.to_vec()) } @@ -494,15 +498,21 @@ impl StreamBuffer<'_> { unsafe { vdo_sys::vdo_frame_get_size(self.raw) } } - pub fn header_size(&self) -> isize { - unsafe { vdo_sys::vdo_frame_get_header_size(self.raw) } + /// Returns the header size in bytes, or `None` if the frame has no header. + pub fn header_size(&self) -> Option { + let size = unsafe { vdo_sys::vdo_frame_get_header_size(self.raw) }; + if size < 0 { None } else { Some(size as usize) } } /// Returns a borrowed file descriptor for the buffer's backing memory. + /// + /// The fd is only valid for the lifetime of this buffer. Do not convert it to + /// an `OwnedFd` (e.g. via `try_clone_to_owned`), as the underlying fd is closed + /// when the buffer is unreferenced. pub fn file_descriptor(&self) -> std::result::Result, Error> { let fd = unsafe { vdo_sys::vdo_buffer_get_fd(self.raw) }; if fd < 0 { - return Err(Error::NullPointer); + return Err(Error::InvalidFd); } // SAFETY: fd is non-negative and valid for the lifetime of this buffer. Ok(unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) }) @@ -648,6 +658,8 @@ mod unit_tests { fn all_error_variants_display() { expect!["VDO returned an unexpected null pointer"] .assert_eq(&format!("{}", Error::NullPointer)); + expect!["VDO returned an invalid file descriptor"] + .assert_eq(&format!("{}", Error::InvalidFd)); expect!["Missing error data from VDO library"] .assert_eq(&format!("{}", Error::MissingVdoError)); let vdo = Error::Vdo(VdoError { diff --git a/crates/vdo/src/map.rs b/crates/vdo/src/map.rs index 20a48fef..46e1392c 100644 --- a/crates/vdo/src/map.rs +++ b/crates/vdo/src/map.rs @@ -2,6 +2,8 @@ use gobject_sys::{g_object_unref, GObject}; use std::ffi::{c_char, c_void, CStr}; +use std::fmt; +use std::ops::Deref; use std::ptr::{self, NonNull}; use vdo_sys::VdoMap; @@ -18,7 +20,7 @@ impl CStringPtr { /// allocated in a manner compatible with [`glib_sys::g_free`] and there must be no other /// users of this memory. pub(crate) unsafe fn from_ptr(ptr: *mut c_char) -> Self { - debug_assert!(!ptr.is_null()); + assert!(!ptr.is_null(), "CStringPtr::from_ptr called with null"); Self(NonNull::new_unchecked(ptr)) } @@ -29,6 +31,20 @@ impl CStringPtr { } } +impl Deref for CStringPtr { + type Target = CStr; + + fn deref(&self) -> &CStr { + self.as_c_str() + } +} + +impl fmt::Debug for CStringPtr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.as_c_str()) + } +} + impl Drop for CStringPtr { fn drop(&mut self) { // SAFETY: We have full ownership, allocated in a manner compatible with `g_free`. @@ -62,7 +78,7 @@ impl Map { /// `ptr` must be a non-null, valid `VdoMap` pointer with ownership /// transferred to this `Map` (it will be unreferenced on drop). pub(crate) unsafe fn from_raw(ptr: *mut VdoMap) -> Self { - debug_assert!(!ptr.is_null()); + assert!(!ptr.is_null(), "Map::from_raw called with null"); Self { raw: ptr } } @@ -81,7 +97,7 @@ impl Map { /// Returns `None` if the key doesn't exist or the value is null. pub fn get_string(&self, key: &CStr) -> Option { // Passing null as default so missing keys yield null -> None. - let ptr = unsafe { vdo_sys::vdo_map_dup_string(self.raw, key.as_ptr(), ptr::null()) }; + let ptr = unsafe { vdo_sys::vdo_map_dup_string(self.raw, key.as_ptr(), ptr::null::()) }; if ptr.is_null() { return None; } @@ -112,11 +128,18 @@ impl Map { unsafe { vdo_sys::vdo_map_dump(self.raw) } } + // Returns *mut because GLib's C API takes *mut even for read-only operations. pub(crate) fn as_ptr(&self) -> *mut VdoMap { self.raw } } +impl fmt::Debug for Map { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Map").field("raw", &self.raw).finish() + } +} + // SAFETY: We hold exclusive ownership of the GObject reference. // Sync is NOT implemented because GLib objects are not safe for concurrent access. unsafe impl Send for Map {} From 87a708d7cc44df059456adab5fd022338313bda8 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Sat, 21 Feb 2026 13:47:54 +0100 Subject: [PATCH 05/29] style(vdo): Apply rustfmt --- crates/vdo/src/lib.rs | 21 ++++++++++++++++----- crates/vdo/src/map.rs | 7 +++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 184f1fcc..2a2db352 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -292,7 +292,8 @@ impl StreamBuilder { VdoBufferStrategy::VDO_BUFFER_STRATEGY_INFINITE.0, ); - let (stream_raw, maybe_error) = unsafe { try_func!(vdo_sys::vdo_stream_new, map.as_ptr(), None) }; + let (stream_raw, maybe_error) = + unsafe { try_func!(vdo_sys::vdo_stream_new, map.as_ptr(), None) }; if stream_raw.is_null() { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); @@ -362,7 +363,8 @@ impl Stream { /// Returns stream settings as a map. pub fn settings(&self) -> std::result::Result { - let (map_raw, maybe_error) = unsafe { try_func!(vdo_sys::vdo_stream_get_settings, self.raw) }; + let (map_raw, maybe_error) = + unsafe { try_func!(vdo_sys::vdo_stream_get_settings, self.raw) }; if map_raw.is_null() { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); } @@ -501,7 +503,11 @@ impl StreamBuffer<'_> { /// Returns the header size in bytes, or `None` if the frame has no header. pub fn header_size(&self) -> Option { let size = unsafe { vdo_sys::vdo_frame_get_header_size(self.raw) }; - if size < 0 { None } else { Some(size as usize) } + if size < 0 { + None + } else { + Some(size as usize) + } } /// Returns a borrowed file descriptor for the buffer's backing memory. @@ -541,8 +547,13 @@ impl StreamBuffer<'_> { impl Drop for StreamBuffer<'_> { fn drop(&mut self) { - let (_, maybe_error) = - unsafe { try_func!(vdo_sys::vdo_stream_buffer_unref, self.stream.raw, &mut self.raw) }; + let (_, maybe_error) = unsafe { + try_func!( + vdo_sys::vdo_stream_buffer_unref, + self.stream.raw, + &mut self.raw + ) + }; if let Some(err) = maybe_error { log::error!("Failed to unref buffer: {}", err); } diff --git a/crates/vdo/src/map.rs b/crates/vdo/src/map.rs index 46e1392c..cec988d8 100644 --- a/crates/vdo/src/map.rs +++ b/crates/vdo/src/map.rs @@ -97,7 +97,8 @@ impl Map { /// Returns `None` if the key doesn't exist or the value is null. pub fn get_string(&self, key: &CStr) -> Option { // Passing null as default so missing keys yield null -> None. - let ptr = unsafe { vdo_sys::vdo_map_dup_string(self.raw, key.as_ptr(), ptr::null::()) }; + let ptr = + unsafe { vdo_sys::vdo_map_dup_string(self.raw, key.as_ptr(), ptr::null::()) }; if ptr.is_null() { return None; } @@ -120,7 +121,9 @@ impl Map { } else { glib_sys::GFALSE }; - unsafe { vdo_sys::vdo_map_get_boolean(self.raw, key.as_ptr(), gdefault) != glib_sys::GFALSE } + unsafe { + vdo_sys::vdo_map_get_boolean(self.raw, key.as_ptr(), gdefault) != glib_sys::GFALSE + } } /// Dumps the map contents to stdout (for debugging). From 9f98b9ee65a7871269c8e7061a04009b26d7d734 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Sun, 1 Mar 2026 17:35:20 +0100 Subject: [PATCH 06/29] fix(vdo): Fix soundness hole in file_descriptor() API Return RawFd instead of BorrowedFd and mark the method unsafe. BorrowedFd allowed callers to call try_clone_to_owned() in safe code, which could lead to fd aliasing and use-after-close when VDO unrefs the buffer. --- crates/vdo/src/lib.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 2a2db352..68056a1a 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -510,18 +510,18 @@ impl StreamBuffer<'_> { } } - /// Returns a borrowed file descriptor for the buffer's backing memory. + /// Returns the raw file descriptor for the buffer's backing memory. /// - /// The fd is only valid for the lifetime of this buffer. Do not convert it to - /// an `OwnedFd` (e.g. via `try_clone_to_owned`), as the underlying fd is closed - /// when the buffer is unreferenced. - pub fn file_descriptor(&self) -> std::result::Result, Error> { - let fd = unsafe { vdo_sys::vdo_buffer_get_fd(self.raw) }; + /// # Safety + /// + /// The returned fd is owned by VDO and will be closed when this buffer is + /// unreferenced. The caller must not close or duplicate (`dup`) the fd. + pub unsafe fn file_descriptor(&self) -> std::result::Result { + let fd = vdo_sys::vdo_buffer_get_fd(self.raw); if fd < 0 { return Err(Error::InvalidFd); } - // SAFETY: fd is non-negative and valid for the lifetime of this buffer. - Ok(unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) }) + Ok(fd) } pub fn is_last_buffer(&self) -> bool { @@ -1032,12 +1032,9 @@ mod device_tests { let running = stream.start()?; let buffer = running.next_buffer()?; - use std::os::fd::AsRawFd; - let fd = buffer.file_descriptor()?; - assert!( - fd.as_raw_fd() >= 0, - "File descriptor should be non-negative" - ); + // SAFETY: We only read the fd value for the assertion; we do not close or dup it. + let fd = unsafe { buffer.file_descriptor()? }; + assert!(fd >= 0, "File descriptor should be non-negative"); drop(buffer); running.stop(); From 76ed6e43e146a279f77c44663f5a7df5035ba1f1 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Sun, 1 Mar 2026 17:43:07 +0100 Subject: [PATCH 07/29] docs(vdo): Separate Safety and Panics sections in unsafe fn docs --- crates/vdo/src/map.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/vdo/src/map.rs b/crates/vdo/src/map.rs index cec988d8..b4663938 100644 --- a/crates/vdo/src/map.rs +++ b/crates/vdo/src/map.rs @@ -16,9 +16,13 @@ pub struct CStringPtr(NonNull); impl CStringPtr { /// # Safety /// - /// In addition to the safety preconditions for [`CStr::from_ptr`] the memory must have been - /// allocated in a manner compatible with [`glib_sys::g_free`] and there must be no other + /// The memory must satisfy the preconditions for [`CStr::from_ptr`], must have been + /// allocated in a manner compatible with [`glib_sys::g_free`], and there must be no other /// users of this memory. + /// + /// # Panics + /// + /// Panics if `ptr` is null. pub(crate) unsafe fn from_ptr(ptr: *mut c_char) -> Self { assert!(!ptr.is_null(), "CStringPtr::from_ptr called with null"); Self(NonNull::new_unchecked(ptr)) @@ -75,8 +79,12 @@ impl Map { /// # Safety /// - /// `ptr` must be a non-null, valid `VdoMap` pointer with ownership + /// `ptr` must be a valid `VdoMap` pointer with ownership /// transferred to this `Map` (it will be unreferenced on drop). + /// + /// # Panics + /// + /// Panics if `ptr` is null. pub(crate) unsafe fn from_raw(ptr: *mut VdoMap) -> Self { assert!(!ptr.is_null(), "Map::from_raw called with null"); Self { raw: ptr } From b616613ab919622490cc158dfc4d9d2b707c73d2 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Sun, 1 Mar 2026 17:47:21 +0100 Subject: [PATCH 08/29] fix(vdo): Rewrite Send safety comments without GLib assumption --- crates/vdo/src/lib.rs | 8 ++++---- crates/vdo/src/map.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 68056a1a..0e6c1381 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -337,8 +337,8 @@ pub struct Stream { raw: *mut VdoStream, } -// SAFETY: We hold exclusive ownership of this GObject reference. -// Sync is NOT implemented because GLib objects are not safe for concurrent access. +// SAFETY: We hold exclusive ownership of the raw pointer. Since `Sync` is not +// implemented, only one thread can access the object at a time. unsafe impl Send for Stream {} impl Stream { @@ -401,8 +401,8 @@ pub struct RunningStream { stream: Stream, } -// SAFETY: Owns a Stream (which is Send). -// Sync is NOT implemented; this ensures next_buffer(&self) cannot be called concurrently. +// SAFETY: Owns a Stream (which is Send) and does not implement Sync, +// so only one thread can access the object at a time. unsafe impl Send for RunningStream {} impl RunningStream { diff --git a/crates/vdo/src/map.rs b/crates/vdo/src/map.rs index b4663938..309187b1 100644 --- a/crates/vdo/src/map.rs +++ b/crates/vdo/src/map.rs @@ -151,8 +151,8 @@ impl fmt::Debug for Map { } } -// SAFETY: We hold exclusive ownership of the GObject reference. -// Sync is NOT implemented because GLib objects are not safe for concurrent access. +// SAFETY: We hold exclusive ownership of the raw pointer. Since `Sync` is not +// implemented, only one thread can access the object at a time. unsafe impl Send for Map {} impl Drop for Map { From e5bdfa98065479435b9cdd1e0ed4bc053608e70d Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Sun, 1 Mar 2026 18:12:38 +0100 Subject: [PATCH 09/29] refactor(vdo): Remove redundant RunningStream::stop() method drop() achieves the same result and is idiomatic Rust. --- apps/vdo_encode_client/src/main.rs | 2 +- crates/vdo/examples/basic.rs | 2 +- crates/vdo/src/lib.rs | 50 +++++++++++++----------------- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/apps/vdo_encode_client/src/main.rs b/apps/vdo_encode_client/src/main.rs index 5e18c4ad..785ba134 100644 --- a/apps/vdo_encode_client/src/main.rs +++ b/apps/vdo_encode_client/src/main.rs @@ -60,7 +60,7 @@ fn capture_format(name: &str, format: VdoFormat, num_frames: usize) -> Result<() } } - running.stop(); + drop(running); info!("{}: Stream stopped successfully", name); info!(""); diff --git a/crates/vdo/examples/basic.rs b/crates/vdo/examples/basic.rs index 6eeb2876..7d72c9f8 100644 --- a/crates/vdo/examples/basic.rs +++ b/crates/vdo/examples/basic.rs @@ -38,7 +38,7 @@ fn main() -> Result<(), Box> { } println!("Stopping stream..."); - running.stop(); + drop(running); println!("Done!"); Ok(()) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 0e6c1381..d084d76c 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -38,7 +38,7 @@ //! println!("Frame size: {} bytes", buffer.size()); //! } //! -//! running.stop(); +//! drop(running); //! ``` //! //! # Known Issues @@ -329,7 +329,7 @@ impl StreamBuilder { /// let buffer = running.next_buffer()?; /// println!("Got frame: {} bytes", buffer.size()); /// } -/// running.stop(); +/// drop(running); /// # Ok::<(), vdo::Error>(()) /// ``` #[derive(Debug)] @@ -395,8 +395,7 @@ impl Drop for Stream { /// A running video stream that yields frame buffers. /// /// Created by calling [`Stream::start()`]. Use [`next_buffer()`](RunningStream::next_buffer) -/// to retrieve frame buffers. Call [`stop()`](RunningStream::stop) or simply drop -/// this value for cleanup. +/// to retrieve frame buffers. Drop this value to stop the stream. pub struct RunningStream { stream: Stream, } @@ -421,11 +420,6 @@ impl RunningStream { }) } - /// Stops the stream, consuming this handle. - pub fn stop(self) { - // Dropping self triggers Stream::drop which calls vdo_stream_stop + g_object_unref. - drop(self); - } } /// A buffer containing a video frame from a running stream. @@ -704,7 +698,7 @@ mod device_tests { .build()?; let running = stream.start()?; - running.stop(); + drop(running); Ok(()) } @@ -727,7 +721,7 @@ mod device_tests { let buffer = running.next_buffer()?; assert!(buffer.size() > 0); drop(buffer); - running.stop(); + drop(running); Ok(()) } @@ -802,7 +796,7 @@ mod device_tests { ); } - running.stop(); + drop(running); Ok(()) } @@ -832,7 +826,7 @@ mod device_tests { log::info!("JPEG frame {}: {} bytes", i, buffer.size()); } - running.stop(); + drop(running); Ok(()) } @@ -873,7 +867,7 @@ mod device_tests { assert!(got_i_frame, "Should have captured at least one I-frame"); - running.stop(); + drop(running); Ok(()) } @@ -901,7 +895,7 @@ mod device_tests { log::info!("H.265 frame: {} bytes", buffer.size()); } - running.stop(); + drop(running); } Err(Error::Vdo(e)) if e.code_name() == "VDO_ERROR_NOT_SUPPORTED" => { log::info!("H.265 not supported on this platform, skipping"); @@ -953,7 +947,7 @@ mod device_tests { prev_seq = seq; } - running.stop(); + drop(running); Ok(()) } @@ -986,7 +980,7 @@ mod device_tests { std::hint::black_box(data[buffer.size().saturating_sub(1)]); } - running.stop(); + drop(running); Ok(()) } @@ -1013,7 +1007,7 @@ mod device_tests { assert_eq!(©[..], &slice[..copy.len()]); drop(buffer); - running.stop(); + drop(running); Ok(()) } @@ -1037,7 +1031,7 @@ mod device_tests { assert!(fd >= 0, "File descriptor should be non-negative"); drop(buffer); - running.stop(); + drop(running); Ok(()) } @@ -1057,7 +1051,7 @@ mod device_tests { let buffer = running.next_buffer()?; buffer.unref()?; - running.stop(); + drop(running); Ok(()) } @@ -1087,7 +1081,7 @@ mod device_tests { std::hint::black_box(buffer.is_last_buffer()); drop(buffer); - running.stop(); + drop(running); Ok(()) } @@ -1108,7 +1102,7 @@ mod device_tests { for _ in 0..3 { let _buf = running.next_buffer()?; } - running.stop(); + drop(running); } { @@ -1124,7 +1118,7 @@ mod device_tests { for _ in 0..3 { let _buf = running.next_buffer()?; } - running.stop(); + drop(running); } Ok(()) @@ -1162,8 +1156,8 @@ mod device_tests { let _buf2 = running2.next_buffer()?; } - running1.stop(); - running2.stop(); + drop(running1); + drop(running2); Ok(()) } @@ -1231,7 +1225,7 @@ mod device_tests { } } - /// Tests that dropping a RunningStream without calling stop() doesn't crash. + /// Tests that dropping a RunningStream without explicit drop doesn't crash. #[test] fn stream_dropped_without_stop() -> std::result::Result<(), Box> { init_logger(); @@ -1250,7 +1244,7 @@ mod device_tests { for _ in 0..2 { let _buf = running.next_buffer()?; } - // Intentionally NOT calling running.stop() + // Intentionally NOT calling drop(running) } log::info!("Stream dropped without explicit stop - cleanup successful"); @@ -1294,7 +1288,7 @@ mod device_tests { let running = stream.start()?; drop(running.next_buffer()?); - running.stop(); + drop(running); log::info!("Rapid cycle {} complete", i); } From adb1eb9ca84a8b7691a40d7a02d2c6593ab1f57b Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Sun, 1 Mar 2026 18:31:12 +0100 Subject: [PATCH 10/29] refactor(vdo): Use NonNull::new instead of assert + new_unchecked --- crates/vdo/src/map.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/vdo/src/map.rs b/crates/vdo/src/map.rs index 309187b1..3d616a61 100644 --- a/crates/vdo/src/map.rs +++ b/crates/vdo/src/map.rs @@ -24,8 +24,7 @@ impl CStringPtr { /// /// Panics if `ptr` is null. pub(crate) unsafe fn from_ptr(ptr: *mut c_char) -> Self { - assert!(!ptr.is_null(), "CStringPtr::from_ptr called with null"); - Self(NonNull::new_unchecked(ptr)) + Self(NonNull::new(ptr).expect("CStringPtr::from_ptr called with null")) } pub fn as_c_str(&self) -> &CStr { From 6a5579c2087344f5b334582119d0862bfd3a3205 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Sun, 1 Mar 2026 21:25:17 +0100 Subject: [PATCH 11/29] fix(vdo): Match error codes as i32 to avoid wrapping negative GError codes --- crates/vdo/src/lib.rs | 57 +++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index d084d76c..399786e5 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -130,29 +130,31 @@ impl VdoError { /// Returns a human-readable name for the VDO error code. pub fn code_name(&self) -> &'static str { - let code = self.code as u32; - match code { - x if x == vdo_sys::VDO_ERROR_NOT_FOUND.0 => "VDO_ERROR_NOT_FOUND", - x if x == vdo_sys::VDO_ERROR_EXISTS.0 => "VDO_ERROR_EXISTS", - x if x == vdo_sys::VDO_ERROR_INVALID_ARGUMENT.0 => "VDO_ERROR_INVALID_ARGUMENT", - x if x == vdo_sys::VDO_ERROR_PERMISSION_DENIED.0 => "VDO_ERROR_PERMISSION_DENIED", - x if x == vdo_sys::VDO_ERROR_NOT_SUPPORTED.0 => "VDO_ERROR_NOT_SUPPORTED", - x if x == vdo_sys::VDO_ERROR_CLOSED.0 => "VDO_ERROR_CLOSED", - x if x == vdo_sys::VDO_ERROR_BUSY.0 => "VDO_ERROR_BUSY", - x if x == vdo_sys::VDO_ERROR_IO.0 => "VDO_ERROR_IO", - x if x == vdo_sys::VDO_ERROR_HAL.0 => "VDO_ERROR_HAL", - x if x == vdo_sys::VDO_ERROR_DBUS.0 => "VDO_ERROR_DBUS", - x if x == vdo_sys::VDO_ERROR_OOM.0 => "VDO_ERROR_OOM", - x if x == vdo_sys::VDO_ERROR_IDLE.0 => "VDO_ERROR_IDLE", - x if x == vdo_sys::VDO_ERROR_NO_DATA.0 => "VDO_ERROR_NO_DATA", - x if x == vdo_sys::VDO_ERROR_NO_BUFFER_SPACE.0 => "VDO_ERROR_NO_BUFFER_SPACE", - x if x == vdo_sys::VDO_ERROR_BUFFER_FAILURE.0 => "VDO_ERROR_BUFFER_FAILURE", - x if x == vdo_sys::VDO_ERROR_INTERFACE_DOWN.0 => "VDO_ERROR_INTERFACE_DOWN", - x if x == vdo_sys::VDO_ERROR_FAILED.0 => "VDO_ERROR_FAILED", - x if x == vdo_sys::VDO_ERROR_FATAL.0 => "VDO_ERROR_FATAL", - x if x == vdo_sys::VDO_ERROR_NOT_CONTROLLED.0 => "VDO_ERROR_NOT_CONTROLLED", - x if x == vdo_sys::VDO_ERROR_NO_EVENT.0 => "VDO_ERROR_NO_EVENT", - x if x == vdo_sys::VDO_ERROR_NO_VIDEO.0 => "VDO_ERROR_NO_VIDEO", + // Compare as i32 to avoid wrapping negative GError codes from non-VDO domains. + match self.code { + x if x == vdo_sys::VDO_ERROR_NOT_FOUND.0 as i32 => "VDO_ERROR_NOT_FOUND", + x if x == vdo_sys::VDO_ERROR_EXISTS.0 as i32 => "VDO_ERROR_EXISTS", + x if x == vdo_sys::VDO_ERROR_INVALID_ARGUMENT.0 as i32 => "VDO_ERROR_INVALID_ARGUMENT", + x if x == vdo_sys::VDO_ERROR_PERMISSION_DENIED.0 as i32 => { + "VDO_ERROR_PERMISSION_DENIED" + } + x if x == vdo_sys::VDO_ERROR_NOT_SUPPORTED.0 as i32 => "VDO_ERROR_NOT_SUPPORTED", + x if x == vdo_sys::VDO_ERROR_CLOSED.0 as i32 => "VDO_ERROR_CLOSED", + x if x == vdo_sys::VDO_ERROR_BUSY.0 as i32 => "VDO_ERROR_BUSY", + x if x == vdo_sys::VDO_ERROR_IO.0 as i32 => "VDO_ERROR_IO", + x if x == vdo_sys::VDO_ERROR_HAL.0 as i32 => "VDO_ERROR_HAL", + x if x == vdo_sys::VDO_ERROR_DBUS.0 as i32 => "VDO_ERROR_DBUS", + x if x == vdo_sys::VDO_ERROR_OOM.0 as i32 => "VDO_ERROR_OOM", + x if x == vdo_sys::VDO_ERROR_IDLE.0 as i32 => "VDO_ERROR_IDLE", + x if x == vdo_sys::VDO_ERROR_NO_DATA.0 as i32 => "VDO_ERROR_NO_DATA", + x if x == vdo_sys::VDO_ERROR_NO_BUFFER_SPACE.0 as i32 => "VDO_ERROR_NO_BUFFER_SPACE", + x if x == vdo_sys::VDO_ERROR_BUFFER_FAILURE.0 as i32 => "VDO_ERROR_BUFFER_FAILURE", + x if x == vdo_sys::VDO_ERROR_INTERFACE_DOWN.0 as i32 => "VDO_ERROR_INTERFACE_DOWN", + x if x == vdo_sys::VDO_ERROR_FAILED.0 as i32 => "VDO_ERROR_FAILED", + x if x == vdo_sys::VDO_ERROR_FATAL.0 as i32 => "VDO_ERROR_FATAL", + x if x == vdo_sys::VDO_ERROR_NOT_CONTROLLED.0 as i32 => "VDO_ERROR_NOT_CONTROLLED", + x if x == vdo_sys::VDO_ERROR_NO_EVENT.0 as i32 => "VDO_ERROR_NO_EVENT", + x if x == vdo_sys::VDO_ERROR_NO_VIDEO.0 as i32 => "VDO_ERROR_NO_VIDEO", _ => "VDO_ERROR_UNKNOWN", } } @@ -419,7 +421,6 @@ impl RunningStream { stream: &self.stream, }) } - } /// A buffer containing a video frame from a running stream. @@ -578,6 +579,14 @@ mod unit_tests { message: "test".to_string(), }; expect!["VDO_ERROR_UNKNOWN"].assert_eq(err.code_name()); + + // Negative codes (from non-VDO GError domains) should map to UNKNOWN, + // not wrap to a matching VDO constant. + let err = VdoError { + code: -1, + message: "test".to_string(), + }; + expect!["VDO_ERROR_UNKNOWN"].assert_eq(err.code_name()); } #[test] From 6274dae74f363e5e578acb194f4c2debc64baa82 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Sun, 1 Mar 2026 21:26:01 +0100 Subject: [PATCH 12/29] fix(vdo): Use == GFALSE for gboolean checks instead of != GTRUE GLib treats any nonzero gboolean as true, not just GTRUE (1). Using != GTRUE could misclassify a valid success value like 2 as failure. --- crates/vdo/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 399786e5..0f2e4f7c 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -378,7 +378,7 @@ impl Stream { /// On failure, the underlying stream is automatically cleaned up. pub fn start(self) -> std::result::Result { let (success, maybe_error) = unsafe { try_func!(vdo_sys::vdo_stream_start, self.raw) }; - if success != glib_sys::GTRUE { + if success == glib_sys::GFALSE { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); } Ok(RunningStream { stream: self }) @@ -533,7 +533,7 @@ impl StreamBuffer<'_> { let (success, maybe_error) = unsafe { try_func!(vdo_sys::vdo_stream_buffer_unref, stream_raw, &mut raw) }; - if success != glib_sys::GTRUE { + if success == glib_sys::GFALSE { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); } Ok(()) From e8500d3ec36e9d3bfffe16fd818ee3910a0582a1 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Sun, 1 Mar 2026 21:26:50 +0100 Subject: [PATCH 13/29] fix(vdo): Track stream started state to avoid stopping unstarted streams Previously Drop always called vdo_stream_stop, even on streams that were never started or where start() failed. This relied on undocumented VDO idempotency. Now a `started` flag ensures stop is only called on streams that were actually started successfully. --- crates/vdo/src/lib.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 0f2e4f7c..3bb8b786 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -306,7 +306,10 @@ impl StreamBuilder { "vdo_stream_new returned a stream pointer AND an error" ); - Ok(Stream { raw: stream_raw }) + Ok(Stream { + raw: stream_raw, + started: false, + }) } } @@ -337,6 +340,7 @@ impl StreamBuilder { #[derive(Debug)] pub struct Stream { raw: *mut VdoStream, + started: bool, } // SAFETY: We hold exclusive ownership of the raw pointer. Since `Sync` is not @@ -375,20 +379,23 @@ impl Stream { /// Starts the stream, consuming `self` and returning a [`RunningStream`]. /// - /// On failure, the underlying stream is automatically cleaned up. - pub fn start(self) -> std::result::Result { + /// On failure, the stream is consumed and cannot be reused; create a new + /// stream via [`Stream::builder()`] to retry. + pub fn start(mut self) -> std::result::Result { let (success, maybe_error) = unsafe { try_func!(vdo_sys::vdo_stream_start, self.raw) }; if success == glib_sys::GFALSE { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); } + self.started = true; Ok(RunningStream { stream: self }) } } impl Drop for Stream { fn drop(&mut self) { - // vdo_stream_stop is idempotent (returns void), safe to call even if never started. - unsafe { vdo_sys::vdo_stream_stop(self.raw) }; + if self.started { + unsafe { vdo_sys::vdo_stream_stop(self.raw) }; + } // Release our GObject reference to avoid leaking. unsafe { g_object_unref(self.raw as *mut GObject) }; } From a617cd77ebd2a095c2b80a81de2dc5f931cdf4e6 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Sun, 1 Mar 2026 21:28:46 +0100 Subject: [PATCH 14/29] fix(vdo): Fix buffer leak in StreamBuffer::unref() on error mem::forget(self) was called before the FFI unref call, so on failure the buffer was permanently leaked with Drop already suppressed. Move mem::forget after the success check so Drop can retry cleanup on error. --- crates/vdo/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 3bb8b786..98bf5b7f 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -534,15 +534,16 @@ impl StreamBuffer<'_> { /// /// Normally buffers are unreferenced on drop. Use this if you need error handling. pub fn unref(self) -> std::result::Result<(), Error> { + // Local copy so self.raw stays valid for Drop if unref fails. let mut raw = self.raw; let stream_raw = self.stream.raw; - mem::forget(self); let (success, maybe_error) = unsafe { try_func!(vdo_sys::vdo_stream_buffer_unref, stream_raw, &mut raw) }; if success == glib_sys::GFALSE { return Err(maybe_error.unwrap_or(Error::MissingVdoError)); } + mem::forget(self); // buffer already unreferenced, suppress Drop Ok(()) } } From 756af0d6180f199d1bd8a5912857e7e7cb025f18 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Sun, 1 Mar 2026 21:29:16 +0100 Subject: [PATCH 15/29] fix(vdo): Log StreamBuffer Drop failures even without GError Previously Drop only logged when a GError was present, silently ignoring GFALSE returns without an error object. Now logs in both cases. --- crates/vdo/src/lib.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 98bf5b7f..3218e1b7 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -550,15 +550,18 @@ impl StreamBuffer<'_> { impl Drop for StreamBuffer<'_> { fn drop(&mut self) { - let (_, maybe_error) = unsafe { + let (success, maybe_error) = unsafe { try_func!( vdo_sys::vdo_stream_buffer_unref, self.stream.raw, &mut self.raw ) }; - if let Some(err) = maybe_error { - log::error!("Failed to unref buffer: {}", err); + if success == glib_sys::GFALSE || maybe_error.is_some() { + match maybe_error { + Some(err) => log::error!("Failed to unref buffer: {}", err), + None => log::error!("Failed to unref buffer (no GError details)"), + } } } } From 35ae4174e673afb6b94caf3d06db66a50c205ce3 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Mon, 2 Mar 2026 09:36:48 +0100 Subject: [PATCH 16/29] refactor(vdo): Minor code review cleanups - Remove dead zero-arg arm from try_func! macro - Use != GFALSE instead of != 0 in is_last_buffer for consistency - Remove public Default derive from VdoError (code=0 misleadingly maps to VDO_ERROR_UNKNOWN; only used internally in from_gerror) - Add production warning to Map::dump() doc comment --- crates/vdo/src/lib.rs | 24 +++++------------------- crates/vdo/src/map.rs | 3 ++- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 3218e1b7..a7b0a73f 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -61,15 +61,6 @@ pub use vdo_sys::{VdoFormat, VdoFrameType, VdoRateControlMode, VdoRateControlPri /// Macro for calling VDO functions that take a GError** parameter. /// Returns a tuple of (result, Option). macro_rules! try_func { - ($func:path $(,)?) => {{ - let mut error: *mut GError = ptr::null_mut(); - let success = $func(&mut error); - if error.is_null() { - (success, None) - } else { - (success, Some(Error::Vdo(VdoError::from_gerror(error)))) - } - }}; ($func:path, $($arg:expr),+ $(,)?) => {{ let mut error: *mut GError = ptr::null_mut(); let success = $func($( $arg ),+, &mut error); @@ -95,7 +86,6 @@ pub enum Error { } /// Error from the VDO library. -#[derive(Default)] pub struct VdoError { code: i32, message: String, @@ -104,7 +94,10 @@ pub struct VdoError { impl VdoError { fn from_gerror(gerror: *mut GError) -> Self { if gerror.is_null() { - return VdoError::default(); + return VdoError { + code: 0, + message: String::new(), + }; } // SAFETY: gerror is non-null. We dereference the struct to copy its fields @@ -527,7 +520,7 @@ impl StreamBuffer<'_> { } pub fn is_last_buffer(&self) -> bool { - unsafe { vdo_sys::vdo_frame_get_is_last_buffer(self.raw) != 0 } + unsafe { vdo_sys::vdo_frame_get_is_last_buffer(self.raw) != glib_sys::GFALSE } } /// Explicitly unreferences this buffer, returning an error if the operation fails. @@ -649,13 +642,6 @@ mod unit_tests { assert_send::(); } - #[test] - fn vdo_error_default() { - let err = VdoError::default(); - assert_eq!(err.code(), 0); - assert!(err.message().is_empty()); - } - #[test] fn vdo_error_from_null() { let err = VdoError::from_gerror(ptr::null_mut()); diff --git a/crates/vdo/src/map.rs b/crates/vdo/src/map.rs index 3d616a61..d23fcfe7 100644 --- a/crates/vdo/src/map.rs +++ b/crates/vdo/src/map.rs @@ -133,7 +133,8 @@ impl Map { } } - /// Dumps the map contents to stdout (for debugging). + /// Dumps the map contents to stdout. Intended for debugging only; + /// may expose sensitive configuration values in production logs. pub fn dump(&self) { unsafe { vdo_sys::vdo_map_dump(self.raw) } } From a6ba0154a9e26777ee7a4d1442ce65517e2ed093 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Mon, 2 Mar 2026 09:45:04 +0100 Subject: [PATCH 17/29] fix(vdo): Use correct types for VdoFormat and fix Send safety comments VdoFormat wraps c_int (i32) but was stored via set_u32, which would wrap VDO_FORMAT_NONE (-1) to u32::MAX. Added Map::set_i32/get_i32 and switched the builder to use set_i32 for format. Reworded Send safety comments - the justification is exclusive ownership and no thread-pinning requirement, not absence of Sync. --- crates/vdo/src/lib.rs | 10 +++++----- crates/vdo/src/map.rs | 12 ++++++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index a7b0a73f..cb609695 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -271,7 +271,7 @@ impl StreamBuilder { pub fn build(self) -> std::result::Result { let mut map = Map::try_new()?; map.set_u32(c"channel", self.channel); - map.set_u32(c"format", self.format.0 as u32); + map.set_i32(c"format", self.format.0); if let Resolution::Exact { width, height } = self.resolution { map.set_u32(c"width", width); map.set_u32(c"height", height); @@ -336,8 +336,8 @@ pub struct Stream { started: bool, } -// SAFETY: We hold exclusive ownership of the raw pointer. Since `Sync` is not -// implemented, only one thread can access the object at a time. +// SAFETY: We hold exclusive ownership of the raw pointer and the VDO SDK +// does not require streams to be pinned to a specific thread. unsafe impl Send for Stream {} impl Stream { @@ -402,8 +402,8 @@ pub struct RunningStream { stream: Stream, } -// SAFETY: Owns a Stream (which is Send) and does not implement Sync, -// so only one thread can access the object at a time. +// SAFETY: Owns a Stream (which is Send) and the VDO SDK does not +// require streams to be pinned to a specific thread. unsafe impl Send for RunningStream {} impl RunningStream { diff --git a/crates/vdo/src/map.rs b/crates/vdo/src/map.rs index d23fcfe7..a31b67ed 100644 --- a/crates/vdo/src/map.rs +++ b/crates/vdo/src/map.rs @@ -97,6 +97,14 @@ impl Map { unsafe { vdo_sys::vdo_map_get_uint32(self.raw, key.as_ptr(), default) } } + pub fn set_i32(&mut self, key: &CStr, value: i32) { + unsafe { vdo_sys::vdo_map_set_int32(self.raw, key.as_ptr(), value) } + } + + pub fn get_i32(&self, key: &CStr, default: i32) -> i32 { + unsafe { vdo_sys::vdo_map_get_int32(self.raw, key.as_ptr(), default) } + } + pub fn set_string(&mut self, key: &CStr, value: &CStr) { unsafe { vdo_sys::vdo_map_set_string(self.raw, key.as_ptr(), value.as_ptr()) } } @@ -151,8 +159,8 @@ impl fmt::Debug for Map { } } -// SAFETY: We hold exclusive ownership of the raw pointer. Since `Sync` is not -// implemented, only one thread can access the object at a time. +// SAFETY: We hold exclusive ownership of the raw pointer and VdoMap +// does not require access from a specific thread. unsafe impl Send for Map {} impl Drop for Map { From 824eefff4a7064da2753038450bf7c83ec349d41 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Mon, 2 Mar 2026 10:09:31 +0100 Subject: [PATCH 18/29] fix(vdo): Revert format to set_u32 - VDO API expects uint32 VdoFormat is c_int in bindgen, but VDO actually expects uint32 for the format key. Using set_i32 causes VDO to ignore the value. --- crates/vdo/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index cb609695..77edbcaa 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -271,7 +271,7 @@ impl StreamBuilder { pub fn build(self) -> std::result::Result { let mut map = Map::try_new()?; map.set_u32(c"channel", self.channel); - map.set_i32(c"format", self.format.0); + map.set_u32(c"format", self.format.0 as u32); if let Resolution::Exact { width, height } = self.resolution { map.set_u32(c"width", width); map.set_u32(c"height", height); From f4337d8af1c733cd9851a8d6c72c557f17c4096a Mon Sep 17 00:00:00 2001 From: AP Ljungquist Date: Sat, 7 Mar 2026 14:08:50 +0100 Subject: [PATCH 19/29] f: consistent cfg https://github.com/AxisCommunications/acap-rs/pull/223#discussion_r2659585648 --- crates/vdo/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 77edbcaa..e4dd0129 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -683,7 +683,7 @@ mod unit_tests { // These tests require the VDO shared library (libvdo.so) and actual camera hardware. // Results depend on the specific camera model and firmware. -#[cfg(all(test, target_arch = "aarch64", feature = "device-tests"))] +#[cfg(not(any(target_arch = "x86_64", target_os = "macos")))] mod device_tests { use super::*; From 861856800f0a83cc09c48ab7ab3c2b88e64ceb62 Mon Sep 17 00:00:00 2001 From: AP Ljungquist Date: Sat, 7 Mar 2026 15:26:23 +0100 Subject: [PATCH 20/29] Proper tests module and cfg --- crates/vdo/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index e4dd0129..6371400f 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -684,7 +684,8 @@ mod unit_tests { // These tests require the VDO shared library (libvdo.so) and actual camera hardware. // Results depend on the specific camera model and firmware. #[cfg(not(any(target_arch = "x86_64", target_os = "macos")))] -mod device_tests { +#[cfg(test)] +mod tests { use super::*; fn init_logger() { From fdf2314bb5cfea78f7ddde8a532d74f4ece09d4b Mon Sep 17 00:00:00 2001 From: AP Ljungquist Date: Sat, 7 Mar 2026 15:31:02 +0100 Subject: [PATCH 21/29] update checksums --- apps-aarch64.checksum | 2 +- apps-aarch64.filesize | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps-aarch64.checksum b/apps-aarch64.checksum index b03fa947..d4c11590 100644 --- a/apps-aarch64.checksum +++ b/apps-aarch64.checksum @@ -15,4 +15,4 @@ bfb74d7d675b6adcd48fb0be3e542fcfdb0c888e target-aarch64/acap/licensekey_handler e01115210714bb7b83d89fd5f645d997b6bcae1d target-aarch64/acap/subscribe_to_event_1_0_0_aarch64.eap 9df8dead66b245aa19d3aafa758871118b8cc25f target-aarch64/acap/using_a_build_script_0_0_0_aarch64.eap 2d373306d132d68f7c3755bb93eb45b906347ae2 target-aarch64/acap/vapix_access_0_0_0_aarch64.eap -3cdc345e9a3db8c78a6866e80ced733a61f8307f target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap +20873e65c332b75e0203a5a917dc08efbd088cc6 target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap diff --git a/apps-aarch64.filesize b/apps-aarch64.filesize index b7d79e05..5ac02f06 100644 --- a/apps-aarch64.filesize +++ b/apps-aarch64.filesize @@ -15,4 +15,4 @@ 3417 target-aarch64/acap/subscribe_to_event_1_0_0_aarch64.eap 899 target-aarch64/acap/using_a_build_script_0_0_0_aarch64.eap 11327 target-aarch64/acap/vapix_access_0_0_0_aarch64.eap -1407 target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap +1492 target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap From 52edcd3303036db914c232c70c1819bb88841642 Mon Sep 17 00:00:00 2001 From: AP Ljungquist Date: Sat, 7 Mar 2026 17:32:35 +0100 Subject: [PATCH 22/29] fix `make check_docs` --- crates/vdo/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 6371400f..a3942668 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -59,7 +59,7 @@ use vdo_sys::{VdoBuffer, VdoBufferStrategy, VdoStream}; pub use vdo_sys::{VdoFormat, VdoFrameType, VdoRateControlMode, VdoRateControlPriority}; /// Macro for calling VDO functions that take a GError** parameter. -/// Returns a tuple of (result, Option). +/// Returns a tuple of `(result, Option)`. macro_rules! try_func { ($func:path, $($arg:expr),+ $(,)?) => {{ let mut error: *mut GError = ptr::null_mut(); From 5f9a46990ec924716c7a54abc8cea1fa88128c15 Mon Sep 17 00:00:00 2001 From: AP Ljungquist Date: Sun, 8 Mar 2026 10:07:40 +0100 Subject: [PATCH 23/29] don't attempt to run vdo tests on host --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index bcecc026..e36ee531 100644 --- a/Makefile +++ b/Makefile @@ -232,6 +232,7 @@ check_tests: --exclude bbox \ --exclude licensekey \ --exclude mdb \ + --exclude vdo \ --locked \ --workspace .PHONY: check_tests From 7489918184343c1b395cf26dd9b6d4038aa8fa8e Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Tue, 9 Jun 2026 21:15:55 +0200 Subject: [PATCH 24/29] address review comments --- Cargo.lock | 1 + apps/vdo_encode_client/src/main.rs | 4 +- crates/vdo/Cargo.toml | 1 + crates/vdo/examples/basic.rs | 4 +- crates/vdo/src/lib.rs | 179 ++++++++++++----------------- crates/vdo/src/map.rs | 46 ++++---- 6 files changed, 106 insertions(+), 129 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb984d8e..a2b2082f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3072,6 +3072,7 @@ dependencies = [ "anyhow", "env_logger", "expect-test", + "glib", "glib-sys", "gobject-sys", "log", diff --git a/apps/vdo_encode_client/src/main.rs b/apps/vdo_encode_client/src/main.rs index 785ba134..eefea219 100644 --- a/apps/vdo_encode_client/src/main.rs +++ b/apps/vdo_encode_client/src/main.rs @@ -12,12 +12,12 @@ // unit tests because they require access to actual camera hardware via the VDO API. use log::{error, info}; -use vdo::{Error, Resolution, Stream, VdoFormat}; +use vdo::{Error, Resolution, StreamBuilder, VdoFormat}; fn capture_format(name: &str, format: VdoFormat, num_frames: usize) -> Result<(), Error> { info!("=== Testing {} format ===", name); - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(format) .resolution(Resolution::Exact { diff --git a/crates/vdo/Cargo.toml b/crates/vdo/Cargo.toml index f5d2cb52..8cf99968 100644 --- a/crates/vdo/Cargo.toml +++ b/crates/vdo/Cargo.toml @@ -8,6 +8,7 @@ description = "Safe Rust bindings for the VDO (Video Capture) API" [dependencies] vdo-sys = { workspace = true } log = { workspace = true } +glib = { workspace = true } glib-sys = { workspace = true } gobject-sys = { workspace = true } thiserror = { workspace = true } diff --git a/crates/vdo/examples/basic.rs b/crates/vdo/examples/basic.rs index 7d72c9f8..78ca5c41 100644 --- a/crates/vdo/examples/basic.rs +++ b/crates/vdo/examples/basic.rs @@ -3,7 +3,7 @@ //! This example creates a video stream, captures a few frames, and prints //! information about each frame. -use vdo::{Resolution, Stream, VdoFormat}; +use vdo::{Resolution, StreamBuilder, VdoFormat}; fn main() -> Result<(), Box> { // Initialize logging (optional) @@ -12,7 +12,7 @@ fn main() -> Result<(), Box> { println!("Creating video stream..."); // Create a stream with YUV format (most portable across platforms) - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index a3942668..8b86605c 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -22,9 +22,9 @@ //! # Example //! //! ```no_run -//! use vdo::{Resolution, Stream, VdoFormat}; +//! use vdo::{Resolution, StreamBuilder, VdoFormat}; //! -//! let stream = Stream::builder() +//! let stream = StreamBuilder::new() //! .channel(0) //! .format(VdoFormat::VDO_FORMAT_YUV) //! .resolution(Resolution::Exact { width: 1920, height: 1080 }) @@ -47,15 +47,16 @@ //! - Some formats (RGB, PLANAR_RGB) may produce upside-down images on certain platforms. mod map; -pub use map::{CStringPtr, Map}; +use std::{ + fmt::{Debug, Display}, + os::fd::BorrowedFd, + ptr, +}; use glib_sys::GError; use gobject_sys::{g_object_unref, GObject}; -use std::fmt::{Debug, Display}; -use std::mem; -use std::ptr; +pub use map::{CStringPtr, Map}; use vdo_sys::{VdoBuffer, VdoBufferStrategy, VdoStream}; - pub use vdo_sys::{VdoFormat, VdoFrameType, VdoRateControlMode, VdoRateControlPriority}; /// Macro for calling VDO functions that take a GError** parameter. @@ -190,14 +191,14 @@ pub enum Resolution { /// Builder for creating a video stream. /// -/// Use [`Stream::builder()`] to create a new builder. +/// Use [`StreamBuilder::new()`] to create a new builder. /// /// # Example /// /// ```no_run -/// use vdo::{Resolution, Stream, VdoFormat}; +/// use vdo::{Resolution, StreamBuilder, VdoFormat}; /// -/// let stream = Stream::builder() +/// let stream = StreamBuilder::new() /// .channel(0) /// .format(VdoFormat::VDO_FORMAT_H264) /// .resolution(Resolution::Exact { width: 1920, height: 1080 }) @@ -269,7 +270,7 @@ impl StreamBuilder { /// Returns an error if the stream could not be created (e.g., invalid format /// for the platform, or camera not available). pub fn build(self) -> std::result::Result { - let mut map = Map::try_new()?; + let mut map = Map::new(); map.set_u32(c"channel", self.channel); map.set_u32(c"format", self.format.0 as u32); if let Resolution::Exact { width, height } = self.resolution { @@ -308,16 +309,16 @@ impl StreamBuilder { /// A video stream from a camera channel. /// -/// Use [`Stream::builder()`] to create a stream, then call [`Stream::start()`] +/// Use [`StreamBuilder`] to create a stream, then call [`Stream::start()`] /// to begin capturing frames. Starting consumes the `Stream` and returns a /// [`RunningStream`]. /// /// # Example /// /// ```no_run -/// use vdo::{Resolution, Stream, VdoFormat}; +/// use vdo::{Resolution, StreamBuilder, VdoFormat}; /// -/// let stream = Stream::builder() +/// let stream = StreamBuilder::new() /// .format(VdoFormat::VDO_FORMAT_JPEG) /// .resolution(Resolution::Exact { width: 640, height: 480 }) /// .build()?; @@ -341,11 +342,7 @@ pub struct Stream { unsafe impl Send for Stream {} impl Stream { - pub fn builder() -> StreamBuilder { - StreamBuilder::new() - } - - /// Equivalent to `Stream::builder().build()` (H.264 format, native resolution). + /// Equivalent to `StreamBuilder::new().build()` (H.264 format, native resolution). pub fn new() -> std::result::Result { StreamBuilder::new().build() } @@ -373,7 +370,7 @@ impl Stream { /// Starts the stream, consuming `self` and returning a [`RunningStream`]. /// /// On failure, the stream is consumed and cannot be reused; create a new - /// stream via [`Stream::builder()`] to retry. + /// stream via [`StreamBuilder`] to retry. pub fn start(mut self) -> std::result::Result { let (success, maybe_error) = unsafe { try_func!(vdo_sys::vdo_stream_start, self.raw) }; if success == glib_sys::GFALSE { @@ -429,13 +426,12 @@ impl RunningStream { /// metadata (size, timestamp, frame type, etc.) is accessed directly on this type. /// /// The buffer borrows from the [`RunningStream`] that produced it and is -/// automatically unreferenced when dropped. Use [`unref()`](StreamBuffer::unref) -/// to handle unref errors explicitly. +/// automatically unreferenced when dropped. /// /// # Buffer Validity /// /// The buffer data pointer and frame metadata remain valid until the buffer is -/// unreferenced (on drop or via [`unref()`](StreamBuffer::unref)). +/// unreferenced on drop. pub struct StreamBuffer<'a> { raw: *mut VdoBuffer, stream: &'a Stream, @@ -460,15 +456,25 @@ impl StreamBuffer<'_> { Ok(slice) } - /// Returns a copy of exactly [`size()`](StreamBuffer::size) bytes of frame data. + /// Returns a copy of the frame data, excluding the header if one is present. + /// + /// Use [`as_slice()`](StreamBuffer::as_slice) for a raw view of the whole buffer. pub fn data_copy(&self) -> std::result::Result, Error> { let data = unsafe { vdo_sys::vdo_buffer_get_data(self.raw) }; if data.is_null() { return Err(Error::NullPointer); } - // Clamp size to capacity to avoid reading beyond the mapped region. - let size = self.size().min(self.capacity()); - let slice = unsafe { std::slice::from_raw_parts(data as *const u8, size) }; + let offset = self.header_size().unwrap_or(0); + let size = self.size(); + assert!(offset <= size, "expect header to fit within frame size"); + assert!( + size <= self.capacity(), + "expect frame size to fit within buffer capacity" + ); + // SAFETY: offset..size lies within the mapped region of capacity bytes, + // which is fully initialized at allocation time. + let slice = + unsafe { std::slice::from_raw_parts((data as *const u8).add(offset), size - offset) }; Ok(slice.to_vec()) } @@ -505,40 +511,23 @@ impl StreamBuffer<'_> { } } - /// Returns the raw file descriptor for the buffer's backing memory. + /// Returns the file descriptor for the buffer's backing memory. /// - /// # Safety - /// - /// The returned fd is owned by VDO and will be closed when this buffer is - /// unreferenced. The caller must not close or duplicate (`dup`) the fd. - pub unsafe fn file_descriptor(&self) -> std::result::Result { - let fd = vdo_sys::vdo_buffer_get_fd(self.raw); + /// The fd is owned by VDO; the returned [`BorrowedFd`] is tied to the + /// lifetime of this buffer and cannot be closed through it. + pub fn file_descriptor(&self) -> std::result::Result, Error> { + let fd = unsafe { vdo_sys::vdo_buffer_get_fd(self.raw) }; if fd < 0 { return Err(Error::InvalidFd); } - Ok(fd) + // SAFETY: fd is a valid descriptor owned by VDO and remains open at + // least for the lifetime of this buffer, to which the BorrowedFd is tied. + Ok(unsafe { BorrowedFd::borrow_raw(fd) }) } pub fn is_last_buffer(&self) -> bool { unsafe { vdo_sys::vdo_frame_get_is_last_buffer(self.raw) != glib_sys::GFALSE } } - - /// Explicitly unreferences this buffer, returning an error if the operation fails. - /// - /// Normally buffers are unreferenced on drop. Use this if you need error handling. - pub fn unref(self) -> std::result::Result<(), Error> { - // Local copy so self.raw stays valid for Drop if unref fails. - let mut raw = self.raw; - let stream_raw = self.stream.raw; - - let (success, maybe_error) = - unsafe { try_func!(vdo_sys::vdo_stream_buffer_unref, stream_raw, &mut raw) }; - if success == glib_sys::GFALSE { - return Err(maybe_error.unwrap_or(Error::MissingVdoError)); - } - mem::forget(self); // buffer already unreferenced, suppress Drop - Ok(()) - } } impl Drop for StreamBuffer<'_> { @@ -561,9 +550,10 @@ impl Drop for StreamBuffer<'_> { #[cfg(test)] mod unit_tests { - use super::*; use expect_test::expect; + use super::*; + #[test] fn error_code_names() { let err = VdoError { @@ -695,7 +685,7 @@ mod tests { #[test] fn stream_starts_and_stops() -> std::result::Result<(), Box> { init_logger(); - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -720,7 +710,7 @@ mod tests { #[test] fn native_resolution() -> std::result::Result<(), Box> { init_logger(); - let stream = Stream::builder() + let stream = StreamBuilder::new() .format(VdoFormat::VDO_FORMAT_YUV) .build()?; @@ -735,7 +725,7 @@ mod tests { #[test] fn stream_info_available() -> std::result::Result<(), Box> { init_logger(); - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -752,7 +742,7 @@ mod tests { #[test] fn stream_settings_available() -> std::result::Result<(), Box> { init_logger(); - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -769,7 +759,7 @@ mod tests { #[test] fn capture_yuv_frames() -> std::result::Result<(), Box> { init_logger(); - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -810,7 +800,7 @@ mod tests { #[test] fn capture_jpeg_frames() -> std::result::Result<(), Box> { init_logger(); - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_JPEG) .resolution(Resolution::Exact { @@ -840,7 +830,7 @@ mod tests { #[test] fn capture_h264_frames() -> std::result::Result<(), Box> { init_logger(); - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_H264) .resolution(Resolution::Exact { @@ -883,7 +873,7 @@ mod tests { fn capture_h265_frames() -> std::result::Result<(), Box> { init_logger(); - let stream_result = Stream::builder() + let stream_result = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_H265) .resolution(Resolution::Exact { @@ -916,7 +906,7 @@ mod tests { #[test] fn frame_timestamps_increase() -> std::result::Result<(), Box> { init_logger(); - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -961,7 +951,7 @@ mod tests { #[test] fn buffer_data_accessible() -> std::result::Result<(), Box> { init_logger(); - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -994,7 +984,7 @@ mod tests { #[test] fn data_copy_returns_frame_data() -> std::result::Result<(), Box> { init_logger(); - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -1007,11 +997,12 @@ mod tests { let buffer = running.next_buffer()?; let copy = buffer.data_copy()?; - assert_eq!(copy.len(), buffer.size()); + let offset = buffer.header_size().unwrap_or(0); + assert_eq!(copy.len(), buffer.size() - offset); - // Verify copy matches the original slice + // Verify copy matches the original slice, past the header if any let slice = buffer.as_slice()?; - assert_eq!(©[..], &slice[..copy.len()]); + assert_eq!(©[..], &slice[offset..offset + copy.len()]); drop(buffer); drop(running); @@ -1021,7 +1012,7 @@ mod tests { #[test] fn file_descriptor_is_valid() -> std::result::Result<(), Box> { init_logger(); - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -1033,39 +1024,21 @@ mod tests { let running = stream.start()?; let buffer = running.next_buffer()?; - // SAFETY: We only read the fd value for the assertion; we do not close or dup it. - let fd = unsafe { buffer.file_descriptor()? }; - assert!(fd >= 0, "File descriptor should be non-negative"); + let fd = buffer.file_descriptor()?; + assert!( + std::os::fd::AsRawFd::as_raw_fd(&fd) >= 0, + "File descriptor should be non-negative" + ); drop(buffer); drop(running); Ok(()) } - #[test] - fn explicit_unref() -> std::result::Result<(), Box> { - init_logger(); - let stream = Stream::builder() - .channel(0) - .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(Resolution::Exact { - width: 320, - height: 240, - }) - .build()?; - - let running = stream.start()?; - let buffer = running.next_buffer()?; - buffer.unref()?; - - drop(running); - Ok(()) - } - #[test] fn all_buffer_metadata_accessible() -> std::result::Result<(), Box> { init_logger(); - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -1097,7 +1070,7 @@ mod tests { init_logger(); { - let stream = Stream::builder() + let stream = StreamBuilder::new() .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { width: 320, @@ -1113,7 +1086,7 @@ mod tests { } { - let stream = Stream::builder() + let stream = StreamBuilder::new() .format(VdoFormat::VDO_FORMAT_JPEG) .resolution(Resolution::Exact { width: 320, @@ -1137,7 +1110,7 @@ mod tests { fn interleaved_streams() -> std::result::Result<(), Box> { init_logger(); - let stream1 = Stream::builder() + let stream1 = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -1146,7 +1119,7 @@ mod tests { }) .build()?; - let stream2 = Stream::builder() + let stream2 = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_JPEG) .resolution(Resolution::Exact { @@ -1172,7 +1145,7 @@ mod tests { #[test] fn invalid_channel_returns_error() { init_logger(); - let result = Stream::builder() + let result = StreamBuilder::new() .channel(999) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -1191,7 +1164,7 @@ mod tests { #[test] fn unsupported_format_logged() { init_logger(); - let result = Stream::builder() + let result = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_BAYER) .resolution(Resolution::Exact { @@ -1210,7 +1183,7 @@ mod tests { #[test] fn invalid_resolution_logged() { init_logger(); - let result = Stream::builder() + let result = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -1238,7 +1211,7 @@ mod tests { init_logger(); { - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -1262,7 +1235,7 @@ mod tests { fn error_message_is_descriptive() { init_logger(); - let err = Stream::builder() + let err = StreamBuilder::new() .channel(999) .build() .expect_err("Channel 999 should fail"); @@ -1284,7 +1257,7 @@ mod tests { init_logger(); for i in 0..5 { - let stream = Stream::builder() + let stream = StreamBuilder::new() .channel(0) .format(VdoFormat::VDO_FORMAT_YUV) .resolution(Resolution::Exact { @@ -1309,7 +1282,7 @@ mod tests { fn map_get_set_operations() -> std::result::Result<(), Box> { init_logger(); - let mut map = Map::try_new()?; + let mut map = Map::new(); map.set_u32(c"test_u32", 42); assert_eq!(map.get_u32(c"test_u32", 0), 42); diff --git a/crates/vdo/src/map.rs b/crates/vdo/src/map.rs index a31b67ed..c8cd0cc8 100644 --- a/crates/vdo/src/map.rs +++ b/crates/vdo/src/map.rs @@ -1,10 +1,14 @@ //! Key-value map for VDO settings and a GLib-allocated C string type. +use std::{ + ffi::{c_char, c_void, CStr}, + fmt, + ops::Deref, + ptr::{self, NonNull}, +}; + +use glib::translate::{from_glib, IntoGlib}; use gobject_sys::{g_object_unref, GObject}; -use std::ffi::{c_char, c_void, CStr}; -use std::fmt; -use std::ops::Deref; -use std::ptr::{self, NonNull}; use vdo_sys::VdoMap; /// An owned pointer to a C string allocated by GLib. @@ -67,13 +71,11 @@ pub struct Map { } impl Map { - pub fn try_new() -> std::result::Result { + pub fn new() -> Self { + // `vdo_map_new` is a thin wrapper around `g_object_new`, which aborts + // the program if allocation fails, so the returned pointer is never null. let map = unsafe { vdo_sys::vdo_map_new() }; - if map.is_null() { - Err(super::Error::NullPointer) - } else { - Ok(Self { raw: map }) - } + Self { raw: map } } /// # Safety @@ -122,22 +124,16 @@ impl Map { } pub fn set_bool(&mut self, key: &CStr, value: bool) { - let gvalue = if value { - glib_sys::GTRUE - } else { - glib_sys::GFALSE - }; - unsafe { vdo_sys::vdo_map_set_boolean(self.raw, key.as_ptr(), gvalue) } + unsafe { vdo_sys::vdo_map_set_boolean(self.raw, key.as_ptr(), value.into_glib()) } } pub fn get_bool(&self, key: &CStr, default: bool) -> bool { - let gdefault = if default { - glib_sys::GTRUE - } else { - glib_sys::GFALSE - }; unsafe { - vdo_sys::vdo_map_get_boolean(self.raw, key.as_ptr(), gdefault) != glib_sys::GFALSE + from_glib(vdo_sys::vdo_map_get_boolean( + self.raw, + key.as_ptr(), + default.into_glib(), + )) } } @@ -153,6 +149,12 @@ impl Map { } } +impl Default for Map { + fn default() -> Self { + Self::new() + } +} + impl fmt::Debug for Map { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Map").field("raw", &self.raw).finish() From b2ccc7327625efea284b06adec39816d706360ba Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Tue, 9 Jun 2026 21:16:21 +0200 Subject: [PATCH 25/29] update checksums --- apps-aarch64.checksum | 36 ++++++++++++++++++------------------ apps-aarch64.filesize | 4 ++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apps-aarch64.checksum b/apps-aarch64.checksum index 33a3d003..86e8c8e5 100644 --- a/apps-aarch64.checksum +++ b/apps-aarch64.checksum @@ -1,18 +1,18 @@ -27c2f1c9ccb02741fd7c412793ced13550fd0ee3 target-aarch64/acap/Challenge_Build_Tools_1_0_0_all.eap -5250ef2bd726138fc5f242596587156eab5e64f4 target-aarch64/acap/axoverlay_example_0_0_0_aarch64.eap -36bb02351514cdab558e2dff77762bf8a9080590 target-aarch64/acap/axparameter_example_0_0_0_aarch64.eap -4ab515a362d34c42a1723bfedfbab583c5ebe2fc target-aarch64/acap/axstorage_example_0_0_0_aarch64.eap -7d29034492ff57f3b46e876618090b7ef6de1c2f target-aarch64/acap/bounding_box_example_0_0_0_aarch64.eap -e186594f4c8857d80cd18fce3d45892d6fba11a0 target-aarch64/acap/consume_analytics_metadata_0_0_0_aarch64.eap -e2cab11965f638c6c2409bdbc835b12cf6d84aef target-aarch64/acap/embedded_web_page_0_0_0_aarch64.eap -15d5eb7122fb03e6b633ea6ba76c88aac0794796 target-aarch64/acap/event_subscribe_1_0_0_aarch64.eap -a12f184fdcacfaf4c1d3b9f11f2dddc64ec21f14 target-aarch64/acap/hello_world_0_0_0_aarch64.eap -d4d351038d3af3d995ac5137f070afe7b56763ef target-aarch64/acap/inspect_env_0_0_0_aarch64.eap -babe2fd2009f796efe6529d240e8e81368c1982a target-aarch64/acap/licensekey_handler_0_0_0_aarch64.eap -387a1b17f14ec4103a582956ced1a9fe12fd0b91 target-aarch64/acap/object_detection_1_0_0_aarch64.eap -ee33a597448238ce7e483d590aad51945dd92637 target-aarch64/acap/reverse_proxy_0_0_0_aarch64.eap -d2365ffe408d5a77b326589d664d3647f4e99ac8 target-aarch64/acap/send_event_1_0_0_aarch64.eap -abfdb79508c4395f124c9cbe622e695515a8054f target-aarch64/acap/subscribe_to_event_1_0_0_aarch64.eap -a6e0ee05e0ea5b0e76308b6d91b06b61fb2d9029 target-aarch64/acap/using_a_build_script_0_0_0_aarch64.eap -61c0f96d5388ebb6a46a80f0fd2f57b86143a2f6 target-aarch64/acap/vapix_access_0_0_0_aarch64.eap -8f9dea1da7a39c1c37b99d6ab1a33d0abe0d643f target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap +8fff02275de11d5f3aba0e4aba4ab162a489200c target-aarch64/acap/Challenge_Build_Tools_1_0_0_all.eap +cfb0f32fbba9a2644bf0b0c8c97d8dbf7898f578 target-aarch64/acap/axoverlay_example_0_0_0_aarch64.eap +aa51111c2b470dde239175c039ed736e691ba692 target-aarch64/acap/axparameter_example_0_0_0_aarch64.eap +b80dd1a02f197fc8d43fefa8795456c1d62a0e02 target-aarch64/acap/axstorage_example_0_0_0_aarch64.eap +1a75409448af9b9e64426137654193d621cb3398 target-aarch64/acap/bounding_box_example_0_0_0_aarch64.eap +ef6969bce69114d11c846294f5d2cb3c614e56f4 target-aarch64/acap/consume_analytics_metadata_0_0_0_aarch64.eap +16310c12bb50db85d69c744d13c93ebf0c628953 target-aarch64/acap/embedded_web_page_0_0_0_aarch64.eap +43138b6b0db0c82b42a2304b48dce8d9ebe75e1b target-aarch64/acap/event_subscribe_1_0_0_aarch64.eap +ed67611ddb2bdcaa3437db9e37b3d670fbd390ef target-aarch64/acap/hello_world_0_0_0_aarch64.eap +2632d8e7bc0da4a8e0ab209b6a9032f100a01c6a target-aarch64/acap/inspect_env_0_0_0_aarch64.eap +af85150301e920bc2a1ebf8163a7c807ac78ba56 target-aarch64/acap/licensekey_handler_0_0_0_aarch64.eap +a405dd9022a4a505e4288bf2c8896fbfbddc4dbd target-aarch64/acap/object_detection_1_0_0_aarch64.eap +2e589a0d2ba6a156c2487c2f50477183a0a01174 target-aarch64/acap/reverse_proxy_0_0_0_aarch64.eap +eece6e8250c9b0f26ac36ed71995a26e10de9b46 target-aarch64/acap/send_event_1_0_0_aarch64.eap +90b325e010951cb6b1363cd933632d0a47e1ab31 target-aarch64/acap/subscribe_to_event_1_0_0_aarch64.eap +9df8dead66b245aa19d3aafa758871118b8cc25f target-aarch64/acap/using_a_build_script_0_0_0_aarch64.eap +8d4204ebfe3f6b5c7a979c90e08df6fc3d2e9171 target-aarch64/acap/vapix_access_0_0_0_aarch64.eap +31c0b61367ccc080d90e4c1d6344a7316063906f target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap diff --git a/apps-aarch64.filesize b/apps-aarch64.filesize index 0c20c747..fed9d31d 100644 --- a/apps-aarch64.filesize +++ b/apps-aarch64.filesize @@ -10,9 +10,9 @@ 1441 target-aarch64/acap/inspect_env_0_0_0_aarch64.eap 1430 target-aarch64/acap/licensekey_handler_0_0_0_aarch64.eap 1408 target-aarch64/acap/object_detection_1_0_0_aarch64.eap -10315 target-aarch64/acap/reverse_proxy_0_0_0_aarch64.eap +10316 target-aarch64/acap/reverse_proxy_0_0_0_aarch64.eap 3437 target-aarch64/acap/send_event_1_0_0_aarch64.eap 3418 target-aarch64/acap/subscribe_to_event_1_0_0_aarch64.eap 899 target-aarch64/acap/using_a_build_script_0_0_0_aarch64.eap 11330 target-aarch64/acap/vapix_access_0_0_0_aarch64.eap -1407 target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap +2837 target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap From ef3ca6a64ab2bfa479bd3699aa1989c6bfb8aca0 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Mon, 15 Jun 2026 11:07:05 +0200 Subject: [PATCH 26/29] document fd lifetime assumption --- crates/vdo/src/lib.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index 8b86605c..cbb0fd42 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -513,15 +513,22 @@ impl StreamBuffer<'_> { /// Returns the file descriptor for the buffer's backing memory. /// - /// The fd is owned by VDO; the returned [`BorrowedFd`] is tied to the - /// lifetime of this buffer and cannot be closed through it. + /// The fd is owned by VDO and backs the same memory as + /// [`as_slice()`](StreamBuffer::as_slice). The [VDO documentation] states + /// that this memory "is only valid for as long as the VdoBuffer itself is + /// valid", so the returned [`BorrowedFd`] is tied to this buffer's lifetime + /// and cannot be closed through it. + /// + /// [VDO documentation]: https://developer.axis.com/acap/api/src/api/vdostream/html/vdo-buffer_8h.html pub fn file_descriptor(&self) -> std::result::Result, Error> { let fd = unsafe { vdo_sys::vdo_buffer_get_fd(self.raw) }; if fd < 0 { return Err(Error::InvalidFd); } - // SAFETY: fd is a valid descriptor owned by VDO and remains open at - // least for the lifetime of this buffer, to which the BorrowedFd is tied. + // SAFETY: fd is owned by VDO and backs this buffer's memory, which VDO + // documents as valid for as long as the buffer is valid. The returned + // BorrowedFd borrows from `self`, so it cannot outlive the buffer, and + // it cannot be used to close the descriptor. Ok(unsafe { BorrowedFd::borrow_raw(fd) }) } From 17f0d266362439783b279155b0122140d717d1cd Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Mon, 15 Jun 2026 14:01:22 +0200 Subject: [PATCH 27/29] remove file_descriptor until there is a consumer --- crates/vdo/src/lib.rs | 52 ------------------------------------------- 1 file changed, 52 deletions(-) diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs index cbb0fd42..199998b5 100644 --- a/crates/vdo/src/lib.rs +++ b/crates/vdo/src/lib.rs @@ -49,7 +49,6 @@ mod map; use std::{ fmt::{Debug, Display}, - os::fd::BorrowedFd, ptr, }; @@ -80,8 +79,6 @@ pub enum Error { Vdo(#[from] VdoError), #[error("VDO returned an unexpected null pointer")] NullPointer, - #[error("VDO returned an invalid file descriptor")] - InvalidFd, #[error("Missing error data from VDO library")] MissingVdoError, } @@ -511,27 +508,6 @@ impl StreamBuffer<'_> { } } - /// Returns the file descriptor for the buffer's backing memory. - /// - /// The fd is owned by VDO and backs the same memory as - /// [`as_slice()`](StreamBuffer::as_slice). The [VDO documentation] states - /// that this memory "is only valid for as long as the VdoBuffer itself is - /// valid", so the returned [`BorrowedFd`] is tied to this buffer's lifetime - /// and cannot be closed through it. - /// - /// [VDO documentation]: https://developer.axis.com/acap/api/src/api/vdostream/html/vdo-buffer_8h.html - pub fn file_descriptor(&self) -> std::result::Result, Error> { - let fd = unsafe { vdo_sys::vdo_buffer_get_fd(self.raw) }; - if fd < 0 { - return Err(Error::InvalidFd); - } - // SAFETY: fd is owned by VDO and backs this buffer's memory, which VDO - // documents as valid for as long as the buffer is valid. The returned - // BorrowedFd borrows from `self`, so it cannot outlive the buffer, and - // it cannot be used to close the descriptor. - Ok(unsafe { BorrowedFd::borrow_raw(fd) }) - } - pub fn is_last_buffer(&self) -> bool { unsafe { vdo_sys::vdo_frame_get_is_last_buffer(self.raw) != glib_sys::GFALSE } } @@ -666,8 +642,6 @@ mod unit_tests { fn all_error_variants_display() { expect!["VDO returned an unexpected null pointer"] .assert_eq(&format!("{}", Error::NullPointer)); - expect!["VDO returned an invalid file descriptor"] - .assert_eq(&format!("{}", Error::InvalidFd)); expect!["Missing error data from VDO library"] .assert_eq(&format!("{}", Error::MissingVdoError)); let vdo = Error::Vdo(VdoError { @@ -1016,32 +990,6 @@ mod tests { Ok(()) } - #[test] - fn file_descriptor_is_valid() -> std::result::Result<(), Box> { - init_logger(); - let stream = StreamBuilder::new() - .channel(0) - .format(VdoFormat::VDO_FORMAT_YUV) - .resolution(Resolution::Exact { - width: 320, - height: 240, - }) - .build()?; - - let running = stream.start()?; - let buffer = running.next_buffer()?; - - let fd = buffer.file_descriptor()?; - assert!( - std::os::fd::AsRawFd::as_raw_fd(&fd) >= 0, - "File descriptor should be non-negative" - ); - - drop(buffer); - drop(running); - Ok(()) - } - #[test] fn all_buffer_metadata_accessible() -> std::result::Result<(), Box> { init_logger(); From f4c9497dfae4b177dbc1990247eb2d033e183737 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Mon, 15 Jun 2026 14:01:22 +0200 Subject: [PATCH 28/29] update checksums --- apps-aarch64.checksum | 2 +- apps-aarch64.filesize | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps-aarch64.checksum b/apps-aarch64.checksum index 86e8c8e5..dc91888f 100644 --- a/apps-aarch64.checksum +++ b/apps-aarch64.checksum @@ -15,4 +15,4 @@ eece6e8250c9b0f26ac36ed71995a26e10de9b46 target-aarch64/acap/send_event_1_0_0_a 90b325e010951cb6b1363cd933632d0a47e1ab31 target-aarch64/acap/subscribe_to_event_1_0_0_aarch64.eap 9df8dead66b245aa19d3aafa758871118b8cc25f target-aarch64/acap/using_a_build_script_0_0_0_aarch64.eap 8d4204ebfe3f6b5c7a979c90e08df6fc3d2e9171 target-aarch64/acap/vapix_access_0_0_0_aarch64.eap -31c0b61367ccc080d90e4c1d6344a7316063906f target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap +a287bf9a3d2a76be3e728b84363d495b1a23aebd target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap diff --git a/apps-aarch64.filesize b/apps-aarch64.filesize index fed9d31d..066de81a 100644 --- a/apps-aarch64.filesize +++ b/apps-aarch64.filesize @@ -15,4 +15,4 @@ 3418 target-aarch64/acap/subscribe_to_event_1_0_0_aarch64.eap 899 target-aarch64/acap/using_a_build_script_0_0_0_aarch64.eap 11330 target-aarch64/acap/vapix_access_0_0_0_aarch64.eap -2837 target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap +2836 target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap From db02c34bf04fb3a8e1be3d10cc251f1a09a2db76 Mon Sep 17 00:00:00 2001 From: Azamat Almazbek uulu Date: Thu, 2 Jul 2026 20:47:24 +0200 Subject: [PATCH 29/29] update checksums --- apps-aarch64.checksum | 36 ++++++++++++++++++------------------ apps-aarch64.filesize | 4 ++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apps-aarch64.checksum b/apps-aarch64.checksum index dc91888f..f597feba 100644 --- a/apps-aarch64.checksum +++ b/apps-aarch64.checksum @@ -1,18 +1,18 @@ -8fff02275de11d5f3aba0e4aba4ab162a489200c target-aarch64/acap/Challenge_Build_Tools_1_0_0_all.eap -cfb0f32fbba9a2644bf0b0c8c97d8dbf7898f578 target-aarch64/acap/axoverlay_example_0_0_0_aarch64.eap -aa51111c2b470dde239175c039ed736e691ba692 target-aarch64/acap/axparameter_example_0_0_0_aarch64.eap -b80dd1a02f197fc8d43fefa8795456c1d62a0e02 target-aarch64/acap/axstorage_example_0_0_0_aarch64.eap -1a75409448af9b9e64426137654193d621cb3398 target-aarch64/acap/bounding_box_example_0_0_0_aarch64.eap -ef6969bce69114d11c846294f5d2cb3c614e56f4 target-aarch64/acap/consume_analytics_metadata_0_0_0_aarch64.eap -16310c12bb50db85d69c744d13c93ebf0c628953 target-aarch64/acap/embedded_web_page_0_0_0_aarch64.eap -43138b6b0db0c82b42a2304b48dce8d9ebe75e1b target-aarch64/acap/event_subscribe_1_0_0_aarch64.eap -ed67611ddb2bdcaa3437db9e37b3d670fbd390ef target-aarch64/acap/hello_world_0_0_0_aarch64.eap -2632d8e7bc0da4a8e0ab209b6a9032f100a01c6a target-aarch64/acap/inspect_env_0_0_0_aarch64.eap -af85150301e920bc2a1ebf8163a7c807ac78ba56 target-aarch64/acap/licensekey_handler_0_0_0_aarch64.eap -a405dd9022a4a505e4288bf2c8896fbfbddc4dbd target-aarch64/acap/object_detection_1_0_0_aarch64.eap -2e589a0d2ba6a156c2487c2f50477183a0a01174 target-aarch64/acap/reverse_proxy_0_0_0_aarch64.eap -eece6e8250c9b0f26ac36ed71995a26e10de9b46 target-aarch64/acap/send_event_1_0_0_aarch64.eap -90b325e010951cb6b1363cd933632d0a47e1ab31 target-aarch64/acap/subscribe_to_event_1_0_0_aarch64.eap -9df8dead66b245aa19d3aafa758871118b8cc25f target-aarch64/acap/using_a_build_script_0_0_0_aarch64.eap -8d4204ebfe3f6b5c7a979c90e08df6fc3d2e9171 target-aarch64/acap/vapix_access_0_0_0_aarch64.eap -a287bf9a3d2a76be3e728b84363d495b1a23aebd target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap +27c2f1c9ccb02741fd7c412793ced13550fd0ee3 target-aarch64/acap/Challenge_Build_Tools_1_0_0_all.eap +5250ef2bd726138fc5f242596587156eab5e64f4 target-aarch64/acap/axoverlay_example_0_0_0_aarch64.eap +36bb02351514cdab558e2dff77762bf8a9080590 target-aarch64/acap/axparameter_example_0_0_0_aarch64.eap +4ab515a362d34c42a1723bfedfbab583c5ebe2fc target-aarch64/acap/axstorage_example_0_0_0_aarch64.eap +7d29034492ff57f3b46e876618090b7ef6de1c2f target-aarch64/acap/bounding_box_example_0_0_0_aarch64.eap +e186594f4c8857d80cd18fce3d45892d6fba11a0 target-aarch64/acap/consume_analytics_metadata_0_0_0_aarch64.eap +e2cab11965f638c6c2409bdbc835b12cf6d84aef target-aarch64/acap/embedded_web_page_0_0_0_aarch64.eap +15d5eb7122fb03e6b633ea6ba76c88aac0794796 target-aarch64/acap/event_subscribe_1_0_0_aarch64.eap +a12f184fdcacfaf4c1d3b9f11f2dddc64ec21f14 target-aarch64/acap/hello_world_0_0_0_aarch64.eap +d4d351038d3af3d995ac5137f070afe7b56763ef target-aarch64/acap/inspect_env_0_0_0_aarch64.eap +babe2fd2009f796efe6529d240e8e81368c1982a target-aarch64/acap/licensekey_handler_0_0_0_aarch64.eap +387a1b17f14ec4103a582956ced1a9fe12fd0b91 target-aarch64/acap/object_detection_1_0_0_aarch64.eap +ee33a597448238ce7e483d590aad51945dd92637 target-aarch64/acap/reverse_proxy_0_0_0_aarch64.eap +d2365ffe408d5a77b326589d664d3647f4e99ac8 target-aarch64/acap/send_event_1_0_0_aarch64.eap +abfdb79508c4395f124c9cbe622e695515a8054f target-aarch64/acap/subscribe_to_event_1_0_0_aarch64.eap +a6e0ee05e0ea5b0e76308b6d91b06b61fb2d9029 target-aarch64/acap/using_a_build_script_0_0_0_aarch64.eap +61c0f96d5388ebb6a46a80f0fd2f57b86143a2f6 target-aarch64/acap/vapix_access_0_0_0_aarch64.eap +a69abf2f8bdb3201f75a0cdcb52b44121f171f53 target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap diff --git a/apps-aarch64.filesize b/apps-aarch64.filesize index 066de81a..ee6e8ef4 100644 --- a/apps-aarch64.filesize +++ b/apps-aarch64.filesize @@ -10,9 +10,9 @@ 1441 target-aarch64/acap/inspect_env_0_0_0_aarch64.eap 1430 target-aarch64/acap/licensekey_handler_0_0_0_aarch64.eap 1408 target-aarch64/acap/object_detection_1_0_0_aarch64.eap -10316 target-aarch64/acap/reverse_proxy_0_0_0_aarch64.eap +10315 target-aarch64/acap/reverse_proxy_0_0_0_aarch64.eap 3437 target-aarch64/acap/send_event_1_0_0_aarch64.eap 3418 target-aarch64/acap/subscribe_to_event_1_0_0_aarch64.eap 899 target-aarch64/acap/using_a_build_script_0_0_0_aarch64.eap 11330 target-aarch64/acap/vapix_access_0_0_0_aarch64.eap -2836 target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap +2837 target-aarch64/acap/vdoencodeclient_1_0_0_aarch64.eap