diff --git a/Cargo.lock b/Cargo.lock index ba9c385..66df653 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3157,6 +3157,16 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vdo" +version = "0.0.0" +dependencies = [ + "glib-sys", + "gobject-sys", + "log", + "vdo-sys", +] + [[package]] name = "vdo-sys" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index df39627..e896542 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ licensekey = { path = "crates/licensekey" } licensekey-sys = { path = "crates/licensekey-sys" } mdb = { path = "crates/mdb" } mdb-sys = { path = "crates/mdb-sys" } +vdo = { path = "crates/vdo" } vdo-sys = { path = "crates/vdo-sys" } [workspace.package] diff --git a/README.md b/README.md index 5000de7..ceef49c 100644 --- a/README.md +++ b/README.md @@ -156,8 +156,8 @@ Each crate has a corresponding `*-sys`, which is omitted for brevity. - Status: ⚠️ Alpha - Documentation: [Source code](crates/mdb/src/lib.rs) - `vdo`: Bindings for the Video Capture API. - - Status: 💡 Started - - Documentation: [Pull request](https://github.com/AxisCommunications/acap-rs/pull/153) + - Status: ⚠️ Alpha + - Documentation: [Source code](crates/vdo/src/lib.rs) ### VAPIX API bindings diff --git a/crates/vdo/Cargo.toml b/crates/vdo/Cargo.toml new file mode 100644 index 0000000..ee15daf --- /dev/null +++ b/crates/vdo/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "vdo" +version = "0.0.0" +edition.workspace = true +license = "MIT" +description = "Safe Rust bindings for the Axis VDO (Video Capture) API" + +[dependencies] +log = { workspace = true } +vdo-sys = { workspace = true } +glib-sys = { workspace = true } +gobject-sys = { workspace = true } diff --git a/crates/vdo/src/buffer.rs b/crates/vdo/src/buffer.rs new file mode 100644 index 0000000..8410111 --- /dev/null +++ b/crates/vdo/src/buffer.rs @@ -0,0 +1,228 @@ +//! VDO Buffer - video frame buffer with RAII memory management. + +use std::ptr; +use std::slice; + +use vdo_sys::{VdoBuffer as RawVdoBuffer, VdoStream as RawVdoStream}; + +use crate::format::FrameType; +use crate::map::Map; + +/// A video buffer containing frame data. +pub struct Buffer { + ptr: *mut RawVdoBuffer, + stream_ptr: *mut RawVdoStream, +} + +impl Buffer { + pub(crate) unsafe fn from_raw( + buffer_ptr: *mut RawVdoBuffer, + stream_ptr: *mut RawVdoStream, + ) -> Option { + if buffer_ptr.is_null() { + None + } else { + Some(Self { + ptr: buffer_ptr, + stream_ptr, + }) + } + } + + pub fn id(&self) -> u32 { + unsafe { vdo_sys::vdo_buffer_get_id(self.ptr) } + } + + pub fn fd(&self) -> i32 { + unsafe { vdo_sys::vdo_buffer_get_fd(self.ptr) } + } + + pub fn capacity(&self) -> usize { + unsafe { vdo_sys::vdo_buffer_get_capacity(self.ptr) } + } + + pub fn size(&self) -> usize { + unsafe { vdo_sys::vdo_frame_get_size(self.ptr) } + } + + pub fn offset(&self) -> i64 { + unsafe { vdo_sys::vdo_buffer_get_offset(self.ptr) } + } + + pub fn is_complete(&self) -> bool { + unsafe { vdo_sys::vdo_buffer_is_complete(self.ptr) != 0 } + } + + pub fn frame_type(&self) -> Option { + let raw = unsafe { vdo_sys::vdo_frame_get_frame_type(self.ptr) }; + FrameType::from_raw(raw) + } + + pub fn is_keyframe(&self) -> bool { + unsafe { vdo_sys::vdo_frame_is_key(self.ptr) != 0 } + } + + pub fn sequence_number(&self) -> u32 { + unsafe { vdo_sys::vdo_frame_get_sequence_nbr(self.ptr) } + } + + pub fn timestamp(&self) -> u64 { + unsafe { vdo_sys::vdo_frame_get_timestamp(self.ptr) } + } + + pub fn custom_timestamp(&self) -> i64 { + unsafe { vdo_sys::vdo_frame_get_custom_timestamp(self.ptr) } + } + + pub fn is_last(&self) -> bool { + unsafe { vdo_sys::vdo_frame_get_is_last_buffer(self.ptr) != 0 } + } + + pub fn extra_info(&self) -> Option { + let ptr = unsafe { vdo_sys::vdo_frame_get_extra_info(self.ptr) }; + unsafe { Map::from_raw(ptr) } + } + + /// Returns a slice to the buffer data. Uses cached mmap internally. + pub fn data(&self) -> Option<&[u8]> { + let data_ptr = unsafe { vdo_sys::vdo_buffer_get_data(self.ptr) }; + if data_ptr.is_null() { + return None; + } + + let size = self.size(); + if size == 0 { + return Some(&[]); + } + + Some(unsafe { slice::from_raw_parts(data_ptr as *const u8, size) }) + } + + /// Returns a mutable slice to the buffer data. + pub fn data_mut(&mut self) -> Option<&mut [u8]> { + let data_ptr = unsafe { vdo_sys::vdo_buffer_get_data(self.ptr) }; + if data_ptr.is_null() { + return None; + } + + let size = self.size(); + if size == 0 { + return Some(&mut []); + } + + Some(unsafe { slice::from_raw_parts_mut(data_ptr as *mut u8, size) }) + } +} + +impl Drop for Buffer { + fn drop(&mut self) { + if !self.ptr.is_null() && !self.stream_ptr.is_null() { + let mut ptr = self.ptr; + let mut error: *mut glib_sys::GError = ptr::null_mut(); + unsafe { + let _ = vdo_sys::vdo_stream_buffer_unref(self.stream_ptr, &mut ptr, &mut error); + if !error.is_null() { + glib_sys::g_error_free(error); + } + } + } + } +} + +unsafe impl Send for Buffer {} + +impl std::fmt::Debug for Buffer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Buffer") + .field("id", &self.id()) + .field("size", &self.size()) + .field("capacity", &self.capacity()) + .field("sequence_number", &self.sequence_number()) + .field("is_keyframe", &self.is_keyframe()) + .finish() + } +} + +/// A standalone buffer not associated with a stream, used for snapshots. +pub struct StandaloneBuffer { + ptr: *mut RawVdoBuffer, +} + +impl StandaloneBuffer { + pub(crate) unsafe fn from_raw(ptr: *mut RawVdoBuffer) -> Option { + if ptr.is_null() { + None + } else { + Some(Self { ptr }) + } + } + + pub fn id(&self) -> u32 { + unsafe { vdo_sys::vdo_buffer_get_id(self.ptr) } + } + + pub fn fd(&self) -> i32 { + unsafe { vdo_sys::vdo_buffer_get_fd(self.ptr) } + } + + pub fn capacity(&self) -> usize { + unsafe { vdo_sys::vdo_buffer_get_capacity(self.ptr) } + } + + pub fn size(&self) -> usize { + unsafe { vdo_sys::vdo_frame_get_size(self.ptr) } + } + + pub fn frame_type(&self) -> Option { + let raw = unsafe { vdo_sys::vdo_frame_get_frame_type(self.ptr) }; + FrameType::from_raw(raw) + } + + pub fn is_keyframe(&self) -> bool { + unsafe { vdo_sys::vdo_frame_is_key(self.ptr) != 0 } + } + + pub fn sequence_number(&self) -> u32 { + unsafe { vdo_sys::vdo_frame_get_sequence_nbr(self.ptr) } + } + + pub fn timestamp(&self) -> u64 { + unsafe { vdo_sys::vdo_frame_get_timestamp(self.ptr) } + } + + pub fn data(&self) -> Option<&[u8]> { + let data_ptr = unsafe { vdo_sys::vdo_buffer_get_data(self.ptr) }; + if data_ptr.is_null() { + return None; + } + + let size = self.size(); + if size == 0 { + return Some(&[]); + } + + Some(unsafe { slice::from_raw_parts(data_ptr as *const u8, size) }) + } +} + +impl Drop for StandaloneBuffer { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { + gobject_sys::g_object_unref(self.ptr as *mut _); + } + } + } +} + +unsafe impl Send for StandaloneBuffer {} + +impl std::fmt::Debug for StandaloneBuffer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StandaloneBuffer") + .field("id", &self.id()) + .field("size", &self.size()) + .field("capacity", &self.capacity()) + .finish() + } +} diff --git a/crates/vdo/src/error.rs b/crates/vdo/src/error.rs new file mode 100644 index 0000000..9236009 --- /dev/null +++ b/crates/vdo/src/error.rs @@ -0,0 +1,160 @@ +//! Error types for VDO operations. + +use std::ffi::CStr; +use std::fmt; + +use glib_sys::GError; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum ErrorCode { + NotFound, + Exists, + InvalidArgument, + PermissionDenied, + NotSupported, + Closed, + Busy, + Io, + Hal, + Dbus, + Oom, + Idle, + NoData, + NoBufferSpace, + BufferFailure, + InterfaceDown, + Failed, + Fatal, + NotControlled, + NoEvent, + NoVideo, + Unknown(u32), +} + +impl ErrorCode { + fn from_raw(code: u32) -> Self { + match code { + 1 => Self::NotFound, + 2 => Self::Exists, + 3 => Self::InvalidArgument, + 4 => Self::PermissionDenied, + 5 => Self::NotSupported, + 6 => Self::Closed, + 7 => Self::Busy, + 8 => Self::Io, + 9 => Self::Hal, + 10 => Self::Dbus, + 11 => Self::Oom, + 12 => Self::Idle, + 13 => Self::NoData, + 14 => Self::NoBufferSpace, + 15 => Self::BufferFailure, + 16 => Self::InterfaceDown, + 17 => Self::Failed, + 18 => Self::Fatal, + 19 => Self::NotControlled, + 20 => Self::NoEvent, + 21 => Self::NoVideo, + _ => Self::Unknown(code), + } + } +} + +impl fmt::Display for ErrorCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NotFound => write!(f, "not found"), + Self::Exists => write!(f, "already exists"), + Self::InvalidArgument => write!(f, "invalid argument"), + Self::PermissionDenied => write!(f, "permission denied"), + Self::NotSupported => write!(f, "not supported"), + Self::Closed => write!(f, "closed"), + Self::Busy => write!(f, "busy"), + Self::Io => write!(f, "I/O error"), + Self::Hal => write!(f, "HAL error"), + Self::Dbus => write!(f, "D-Bus error"), + Self::Oom => write!(f, "out of memory"), + Self::Idle => write!(f, "idle"), + Self::NoData => write!(f, "no data"), + Self::NoBufferSpace => write!(f, "no buffer space"), + Self::BufferFailure => write!(f, "buffer failure"), + Self::InterfaceDown => write!(f, "interface down"), + Self::Failed => write!(f, "failed"), + Self::Fatal => write!(f, "fatal error"), + Self::NotControlled => write!(f, "not controlled"), + Self::NoEvent => write!(f, "no event"), + Self::NoVideo => write!(f, "no video"), + Self::Unknown(code) => write!(f, "unknown error ({})", code), + } + } +} + +#[derive(Debug)] +pub struct Error { + code: ErrorCode, + message: Option, +} + +impl Error { + pub(crate) unsafe fn from_gerror(error: *mut GError) -> Option { + if error.is_null() { + return None; + } + + let gerror = &*error; + let code = ErrorCode::from_raw(gerror.code as u32); + let message = if gerror.message.is_null() { + None + } else { + Some( + CStr::from_ptr(gerror.message) + .to_string_lossy() + .into_owned(), + ) + }; + + glib_sys::g_error_free(error); + + Some(Self { code, message }) + } + + pub(crate) fn new(code: ErrorCode, message: impl Into) -> Self { + Self { + code, + message: Some(message.into()), + } + } + + pub fn code(&self) -> ErrorCode { + self.code + } + + pub fn message(&self) -> Option<&str> { + self.message.as_deref() + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.message { + Some(msg) => write!(f, "{}: {}", self.code, msg), + None => write!(f, "{}", self.code), + } + } +} + +impl std::error::Error for Error {} + +pub type Result = std::result::Result; + +macro_rules! check_gerror { + ($error:expr) => {{ + let err = unsafe { $crate::error::Error::from_gerror($error) }; + if let Some(e) = err { + return Err(e); + } + }}; +} + +pub(crate) use check_gerror; diff --git a/crates/vdo/src/format.rs b/crates/vdo/src/format.rs new file mode 100644 index 0000000..efd9083 --- /dev/null +++ b/crates/vdo/src/format.rs @@ -0,0 +1,148 @@ +//! Video format types. + +use vdo_sys::VdoFormat as RawVdoFormat; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum Format { + H264, + H265, + Jpeg, + Yuv, + Bayer, + Ivs, + Raw, + Rgba, + Rgb, + PlanarRgb, + Av1, +} + +impl Format { + pub(crate) fn to_raw(self) -> RawVdoFormat { + match self { + Self::H264 => RawVdoFormat::VDO_FORMAT_H264, + Self::H265 => RawVdoFormat::VDO_FORMAT_H265, + Self::Jpeg => RawVdoFormat::VDO_FORMAT_JPEG, + Self::Yuv => RawVdoFormat::VDO_FORMAT_YUV, + Self::Bayer => RawVdoFormat::VDO_FORMAT_BAYER, + Self::Ivs => RawVdoFormat::VDO_FORMAT_IVS, + Self::Raw => RawVdoFormat::VDO_FORMAT_RAW, + Self::Rgba => RawVdoFormat::VDO_FORMAT_RGBA, + Self::Rgb => RawVdoFormat::VDO_FORMAT_RGB, + Self::PlanarRgb => RawVdoFormat::VDO_FORMAT_PLANAR_RGB, + Self::Av1 => RawVdoFormat::VDO_FORMAT_AV1, + } + } + + pub(crate) fn as_i32(self) -> i32 { + self.to_raw().0 + } + + #[allow(dead_code)] + pub(crate) fn from_raw(raw: RawVdoFormat) -> Option { + match raw { + RawVdoFormat::VDO_FORMAT_H264 => Some(Self::H264), + RawVdoFormat::VDO_FORMAT_H265 => Some(Self::H265), + RawVdoFormat::VDO_FORMAT_JPEG => Some(Self::Jpeg), + RawVdoFormat::VDO_FORMAT_YUV => Some(Self::Yuv), + RawVdoFormat::VDO_FORMAT_BAYER => Some(Self::Bayer), + RawVdoFormat::VDO_FORMAT_IVS => Some(Self::Ivs), + RawVdoFormat::VDO_FORMAT_RAW => Some(Self::Raw), + RawVdoFormat::VDO_FORMAT_RGBA => Some(Self::Rgba), + RawVdoFormat::VDO_FORMAT_RGB => Some(Self::Rgb), + RawVdoFormat::VDO_FORMAT_PLANAR_RGB => Some(Self::PlanarRgb), + RawVdoFormat::VDO_FORMAT_AV1 => Some(Self::Av1), + _ => None, + } + } +} + +impl std::fmt::Display for Format { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = match self { + Self::H264 => "H.264", + Self::H265 => "H.265", + Self::Jpeg => "JPEG", + Self::Yuv => "YUV", + Self::Bayer => "Bayer", + Self::Ivs => "IVS", + Self::Raw => "Raw", + Self::Rgba => "RGBA", + Self::Rgb => "RGB", + Self::PlanarRgb => "Planar RGB", + Self::Av1 => "AV1", + }; + write!(f, "{}", name) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum FrameType { + None, + H264Sps, + H264Pps, + H264Sei, + H264Idr, + H264I, + H264P, + H264B, + H265Sps, + H265Pps, + H265Vps, + H265Sei, + H265Idr, + H265I, + H265P, + H265B, + Jpeg, + Yuv, + Raw, + Rgba, + Rgb, + PlanarRgb, + Av1Key, + Av1Inter, + Av1Bidi, +} + +impl FrameType { + pub fn is_keyframe(&self) -> bool { + matches!( + self, + Self::H264Idr | Self::H264I | Self::H265Idr | Self::H265I | Self::Av1Key | Self::Jpeg + ) + } + + pub(crate) fn from_raw(raw: vdo_sys::VdoFrameType) -> Option { + match raw { + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_NONE => Some(Self::None), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H264_SPS => Some(Self::H264Sps), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H264_PPS => Some(Self::H264Pps), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H264_SEI => Some(Self::H264Sei), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H264_IDR => Some(Self::H264Idr), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H264_I => Some(Self::H264I), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H264_P => Some(Self::H264P), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H264_B => Some(Self::H264B), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H265_SPS => Some(Self::H265Sps), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H265_PPS => Some(Self::H265Pps), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H265_VPS => Some(Self::H265Vps), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H265_SEI => Some(Self::H265Sei), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H265_IDR => Some(Self::H265Idr), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H265_I => Some(Self::H265I), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H265_P => Some(Self::H265P), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_H265_B => Some(Self::H265B), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_JPEG => Some(Self::Jpeg), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_YUV => Some(Self::Yuv), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_RAW => Some(Self::Raw), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_RGBA => Some(Self::Rgba), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_RGB => Some(Self::Rgb), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_PLANAR_RGB => Some(Self::PlanarRgb), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_AV1_KEY => Some(Self::Av1Key), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_AV1_INTER => Some(Self::Av1Inter), + vdo_sys::VdoFrameType::VDO_FRAME_TYPE_AV1_BIDI => Some(Self::Av1Bidi), + _ => None, + } + } +} diff --git a/crates/vdo/src/lib.rs b/crates/vdo/src/lib.rs new file mode 100644 index 0000000..dc97b3f --- /dev/null +++ b/crates/vdo/src/lib.rs @@ -0,0 +1,13 @@ +//! Safe Rust bindings for the Axis VDO (Video Capture) API. + +mod buffer; +mod error; +mod format; +mod map; +mod stream; + +pub use buffer::{Buffer, StandaloneBuffer}; +pub use error::{Error, ErrorCode, Result}; +pub use format::{Format, FrameType}; +pub use map::Map; +pub use stream::{snapshot, Stream, StreamSettings}; diff --git a/crates/vdo/src/map.rs b/crates/vdo/src/map.rs new file mode 100644 index 0000000..0e8930a --- /dev/null +++ b/crates/vdo/src/map.rs @@ -0,0 +1,228 @@ +//! VDO Map - key-value configuration container. + +use std::ffi::{CStr, CString}; +use std::ptr; + +use vdo_sys::VdoMap as RawVdoMap; + +use crate::error::{Error, ErrorCode, Result}; + +/// A key-value map for VDO configuration. +pub struct Map { + ptr: *mut RawVdoMap, +} + +impl Map { + pub fn new() -> Result { + let ptr = unsafe { vdo_sys::vdo_map_new() }; + if ptr.is_null() { + return Err(Error::new(ErrorCode::Oom, "Failed to create VdoMap")); + } + Ok(Self { ptr }) + } + + pub(crate) fn as_ptr(&self) -> *mut RawVdoMap { + self.ptr + } + + pub(crate) unsafe fn from_raw(ptr: *mut RawVdoMap) -> Option { + if ptr.is_null() { + None + } else { + Some(Self { ptr }) + } + } + + pub fn is_empty(&self) -> bool { + unsafe { vdo_sys::vdo_map_empty(self.ptr) != 0 } + } + + pub fn len(&self) -> usize { + unsafe { vdo_sys::vdo_map_size(self.ptr) } + } + + pub fn contains(&self, key: &str) -> bool { + let c_key = match CString::new(key) { + Ok(s) => s, + Err(_) => return false, + }; + unsafe { vdo_sys::vdo_map_contains(self.ptr, c_key.as_ptr()) != 0 } + } + + pub fn remove(&mut self, key: &str) { + if let Ok(c_key) = CString::new(key) { + unsafe { vdo_sys::vdo_map_remove(self.ptr, c_key.as_ptr()) } + } + } + + pub fn clear(&mut self) { + unsafe { vdo_sys::vdo_map_clear(self.ptr) } + } + + pub fn set_boolean(&mut self, key: &str, value: bool) -> Result<()> { + let c_key = CString::new(key) + .map_err(|_| Error::new(ErrorCode::InvalidArgument, "Invalid key string"))?; + unsafe { + vdo_sys::vdo_map_set_boolean(self.ptr, c_key.as_ptr(), if value { 1 } else { 0 }); + } + Ok(()) + } + + pub fn get_boolean(&self, key: &str, default: bool) -> bool { + let c_key = match CString::new(key) { + Ok(s) => s, + Err(_) => return default, + }; + let raw_default = if default { 1 } else { 0 }; + unsafe { vdo_sys::vdo_map_get_boolean(self.ptr, c_key.as_ptr(), raw_default) != 0 } + } + + pub fn set_int32(&mut self, key: &str, value: i32) -> Result<()> { + let c_key = CString::new(key) + .map_err(|_| Error::new(ErrorCode::InvalidArgument, "Invalid key string"))?; + unsafe { + vdo_sys::vdo_map_set_int32(self.ptr, c_key.as_ptr(), value); + } + Ok(()) + } + + pub fn get_int32(&self, key: &str, default: i32) -> i32 { + let c_key = match CString::new(key) { + Ok(s) => s, + Err(_) => return default, + }; + unsafe { vdo_sys::vdo_map_get_int32(self.ptr, c_key.as_ptr(), default) } + } + + pub fn set_uint32(&mut self, key: &str, value: u32) -> Result<()> { + let c_key = CString::new(key) + .map_err(|_| Error::new(ErrorCode::InvalidArgument, "Invalid key string"))?; + unsafe { + vdo_sys::vdo_map_set_uint32(self.ptr, c_key.as_ptr(), value); + } + Ok(()) + } + + pub fn get_uint32(&self, key: &str, default: u32) -> u32 { + let c_key = match CString::new(key) { + Ok(s) => s, + Err(_) => return default, + }; + unsafe { vdo_sys::vdo_map_get_uint32(self.ptr, c_key.as_ptr(), default) } + } + + pub fn set_int64(&mut self, key: &str, value: i64) -> Result<()> { + let c_key = CString::new(key) + .map_err(|_| Error::new(ErrorCode::InvalidArgument, "Invalid key string"))?; + unsafe { + vdo_sys::vdo_map_set_int64(self.ptr, c_key.as_ptr(), value); + } + Ok(()) + } + + pub fn get_int64(&self, key: &str, default: i64) -> i64 { + let c_key = match CString::new(key) { + Ok(s) => s, + Err(_) => return default, + }; + unsafe { vdo_sys::vdo_map_get_int64(self.ptr, c_key.as_ptr(), default) } + } + + pub fn set_uint64(&mut self, key: &str, value: u64) -> Result<()> { + let c_key = CString::new(key) + .map_err(|_| Error::new(ErrorCode::InvalidArgument, "Invalid key string"))?; + unsafe { + vdo_sys::vdo_map_set_uint64(self.ptr, c_key.as_ptr(), value); + } + Ok(()) + } + + pub fn get_uint64(&self, key: &str, default: u64) -> u64 { + let c_key = match CString::new(key) { + Ok(s) => s, + Err(_) => return default, + }; + unsafe { vdo_sys::vdo_map_get_uint64(self.ptr, c_key.as_ptr(), default) } + } + + pub fn set_double(&mut self, key: &str, value: f64) -> Result<()> { + let c_key = CString::new(key) + .map_err(|_| Error::new(ErrorCode::InvalidArgument, "Invalid key string"))?; + unsafe { + vdo_sys::vdo_map_set_double(self.ptr, c_key.as_ptr(), value); + } + Ok(()) + } + + pub fn get_double(&self, key: &str, default: f64) -> f64 { + let c_key = match CString::new(key) { + Ok(s) => s, + Err(_) => return default, + }; + unsafe { vdo_sys::vdo_map_get_double(self.ptr, c_key.as_ptr(), default) } + } + + pub fn set_string(&mut self, key: &str, value: &str) -> Result<()> { + let c_key = CString::new(key) + .map_err(|_| Error::new(ErrorCode::InvalidArgument, "Invalid key string"))?; + let c_value = CString::new(value) + .map_err(|_| Error::new(ErrorCode::InvalidArgument, "Invalid value string"))?; + unsafe { + vdo_sys::vdo_map_set_string(self.ptr, c_key.as_ptr(), c_value.as_ptr()); + } + Ok(()) + } + + pub fn get_string(&self, key: &str) -> Option { + let c_key = CString::new(key).ok()?; + let ptr = unsafe { + vdo_sys::vdo_map_get_string(self.ptr, c_key.as_ptr(), ptr::null_mut(), ptr::null()) + }; + if ptr.is_null() { + return None; + } + unsafe { CStr::from_ptr(ptr).to_str().ok().map(|s| s.to_owned()) } + } + + pub fn merge(&mut self, other: &Map) { + unsafe { + vdo_sys::vdo_map_merge(self.ptr, other.ptr); + } + } +} + +impl Drop for Map { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { + gobject_sys::g_object_unref(self.ptr as *mut _); + } + } + } +} + +unsafe impl Send for Map {} + +impl Clone for Map { + fn clone(&self) -> Self { + unsafe { + gobject_sys::g_object_ref(self.ptr as *mut _); + Self { ptr: self.ptr } + } + } +} + +impl Default for Map { + fn default() -> Self { + Self::new().expect("Failed to create default VdoMap") + } +} + +impl std::fmt::Debug for Map { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Map") + .field("len", &self.len()) + .field("ptr", &self.ptr) + .finish() + } +} diff --git a/crates/vdo/src/stream.rs b/crates/vdo/src/stream.rs new file mode 100644 index 0000000..c05b45e --- /dev/null +++ b/crates/vdo/src/stream.rs @@ -0,0 +1,248 @@ +//! VDO Stream - video stream management with RAII. + +use std::ptr; + +use glib_sys::GError; +use log::warn; +use vdo_sys::VdoStream as RawVdoStream; + +use crate::buffer::{Buffer, StandaloneBuffer}; +use crate::error::{check_gerror, Error, ErrorCode, Result}; +use crate::format::Format; +use crate::map::Map; + +/// A video stream for capturing frames from a camera. +pub struct Stream { + ptr: *mut RawVdoStream, +} + +impl Stream { + /// Creates a new video stream with the given settings. + pub fn new(settings: &StreamSettings) -> Result { + let mut error: *mut GError = ptr::null_mut(); + let ptr = unsafe { vdo_sys::vdo_stream_new(settings.map.as_ptr(), None, &mut error) }; + + check_gerror!(error); + + if ptr.is_null() { + return Err(Error::new(ErrorCode::Failed, "Failed to create stream")); + } + + Ok(Self { ptr }) + } + + pub fn id(&self) -> u32 { + unsafe { vdo_sys::vdo_stream_get_id(self.ptr) } + } + + pub fn fd(&self) -> Result { + let mut error: *mut GError = ptr::null_mut(); + let fd = unsafe { vdo_sys::vdo_stream_get_fd(self.ptr, &mut error) }; + check_gerror!(error); + Ok(fd) + } + + pub fn info(&self) -> Result { + let mut error: *mut GError = ptr::null_mut(); + let ptr = unsafe { vdo_sys::vdo_stream_get_info(self.ptr, &mut error) }; + check_gerror!(error); + + unsafe { Map::from_raw(ptr) } + .ok_or_else(|| Error::new(ErrorCode::Failed, "Failed to get stream info")) + } + + pub fn settings(&self) -> Result { + let mut error: *mut GError = ptr::null_mut(); + let ptr = unsafe { vdo_sys::vdo_stream_get_settings(self.ptr, &mut error) }; + check_gerror!(error); + + unsafe { Map::from_raw(ptr) } + .ok_or_else(|| Error::new(ErrorCode::Failed, "Failed to get stream settings")) + } + + pub fn set_settings(&mut self, settings: &Map) -> Result<()> { + let mut error: *mut GError = ptr::null_mut(); + let success = + unsafe { vdo_sys::vdo_stream_set_settings(self.ptr, settings.as_ptr(), &mut error) }; + check_gerror!(error); + + if success == 0 { + return Err(Error::new( + ErrorCode::Failed, + "Failed to set stream settings", + )); + } + Ok(()) + } + + pub fn set_framerate(&mut self, framerate: f64) -> Result<()> { + let mut error: *mut GError = ptr::null_mut(); + let success = unsafe { vdo_sys::vdo_stream_set_framerate(self.ptr, framerate, &mut error) }; + check_gerror!(error); + + if success == 0 { + return Err(Error::new(ErrorCode::Failed, "Failed to set framerate")); + } + Ok(()) + } + + pub fn start(&mut self) -> Result<()> { + let mut error: *mut GError = ptr::null_mut(); + let success = unsafe { vdo_sys::vdo_stream_start(self.ptr, &mut error) }; + check_gerror!(error); + + if success == 0 { + return Err(Error::new(ErrorCode::Failed, "Failed to start stream")); + } + Ok(()) + } + + pub fn stop(&mut self) { + unsafe { vdo_sys::vdo_stream_stop(self.ptr) }; + } + + pub fn force_keyframe(&mut self) -> Result<()> { + let mut error: *mut GError = ptr::null_mut(); + let success = unsafe { vdo_sys::vdo_stream_force_key_frame(self.ptr, &mut error) }; + check_gerror!(error); + + if success == 0 { + return Err(Error::new(ErrorCode::Failed, "Failed to force keyframe")); + } + Ok(()) + } + + /// Gets the next buffer from the stream. Blocks until a buffer is available. + pub fn get_buffer(&mut self) -> Result { + let mut error: *mut GError = ptr::null_mut(); + let buffer_ptr = unsafe { vdo_sys::vdo_stream_get_buffer(self.ptr, &mut error) }; + check_gerror!(error); + + unsafe { Buffer::from_raw(buffer_ptr, self.ptr) } + .ok_or_else(|| Error::new(ErrorCode::NoData, "No buffer available")) + } + + pub fn allocate_buffer(&mut self) -> Result { + let mut error: *mut GError = ptr::null_mut(); + let buffer_ptr = + unsafe { vdo_sys::vdo_stream_buffer_alloc(self.ptr, ptr::null_mut(), &mut error) }; + check_gerror!(error); + + unsafe { Buffer::from_raw(buffer_ptr, self.ptr) } + .ok_or_else(|| Error::new(ErrorCode::Oom, "Failed to allocate buffer")) + } +} + +impl Drop for Stream { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { vdo_sys::vdo_stream_stop(self.ptr) }; + unsafe { gobject_sys::g_object_unref(self.ptr as *mut _) }; + } + } +} + +unsafe impl Send for Stream {} + +impl std::fmt::Debug for Stream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Stream") + .field("id", &self.id()) + .field("ptr", &self.ptr) + .finish() + } +} + +/// Builder for stream settings. +pub struct StreamSettings { + pub(crate) map: Map, +} + +impl StreamSettings { + pub fn new() -> Self { + Self { + map: Map::new().expect("Failed to create settings map"), + } + } + + pub fn format(mut self, format: Format) -> Self { + if let Err(e) = self.map.set_int32("format", format.as_i32()) { + warn!("Failed to set format: {}", e); + } + self + } + + /// Sets both width and height in one call to prevent ill-formed configurations. + pub fn resolution(mut self, width: u32, height: u32) -> Self { + if let Err(e) = self.map.set_uint32("width", width) { + warn!("Failed to set width: {}", e); + } + if let Err(e) = self.map.set_uint32("height", height) { + warn!("Failed to set height: {}", e); + } + self + } + + pub fn framerate(mut self, fps: f64) -> Self { + if let Err(e) = self.map.set_double("framerate", fps) { + warn!("Failed to set framerate: {}", e); + } + self + } + + pub fn buffer_count(mut self, count: u32) -> Self { + if let Err(e) = self.map.set_uint32("buffer.count", count) { + warn!("Failed to set buffer count: {}", e); + } + self + } + + pub fn channel(mut self, channel: u32) -> Self { + if let Err(e) = self.map.set_uint32("channel", channel) { + warn!("Failed to set channel: {}", e); + } + self + } + + pub fn set(mut self, key: &str, value: u32) -> Self { + if let Err(e) = self.map.set_uint32(key, value) { + warn!("Failed to set {}: {}", key, e); + } + self + } + + pub fn set_string(mut self, key: &str, value: &str) -> Self { + if let Err(e) = self.map.set_string(key, value) { + warn!("Failed to set {}: {}", key, e); + } + self + } + + pub fn as_map(&self) -> &Map { + &self.map + } +} + +impl Default for StreamSettings { + fn default() -> Self { + Self::new() + } +} + +impl std::fmt::Debug for StreamSettings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StreamSettings") + .field("map", &self.map) + .finish() + } +} + +/// Captures a single snapshot frame. +pub fn snapshot(settings: &StreamSettings) -> Result { + let mut error: *mut GError = ptr::null_mut(); + let buffer_ptr = unsafe { vdo_sys::vdo_stream_snapshot(settings.map.as_ptr(), &mut error) }; + check_gerror!(error); + + unsafe { StandaloneBuffer::from_raw(buffer_ptr) } + .ok_or_else(|| Error::new(ErrorCode::Failed, "Failed to capture snapshot")) +}