pub mod camera;
pub mod capture;
pub mod error;
pub mod rtsp;
pub mod v4l2;
pub mod video;
pub use crate::stream::camera::{CameraCapture, CameraCaptureConfig};
pub use crate::stream::capture::StreamCapture;
pub use crate::stream::error::StreamCaptureError;
pub use crate::stream::rtsp::RTSPCameraConfig;
pub use crate::stream::v4l2::V4L2CameraConfig;
pub use crate::stream::video::VideoWriter;
use std::any::Any;
use std::sync::Arc;
pub(crate) fn quote_pipeline_value(value: &str) -> Result<String, StreamCaptureError> {
if value
.chars()
.any(|c| c == '"' || c == '\\' || c.is_control())
{
return Err(StreamCaptureError::InvalidConfig(format!(
"value {value:?} contains characters not allowed in a pipeline description"
)));
}
Ok(format!("\"{value}\""))
}
pub(crate) fn set_location_property(
pipeline: &gstreamer::Pipeline,
name: &str,
path: &std::path::Path,
) -> Result<(), StreamCaptureError> {
use gstreamer::prelude::*;
let element = pipeline
.by_name(name)
.ok_or(StreamCaptureError::GetElementByNameError)?;
let writable_string = element.find_property("location").is_some_and(|p| {
p.value_type() == String::static_type()
&& p.flags().contains(gstreamer::glib::ParamFlags::WRITABLE)
});
if !writable_string {
return Err(StreamCaptureError::InvalidConfig(format!(
"element {name:?} has no writable string `location` property"
)));
}
element.set_property("location", path.to_string_lossy().as_ref());
Ok(())
}
use kornia_image::Image;
use kornia_tensor::resource::{MemoryDomain, MemoryResource};
pub struct GstResource {
pub _map: gstreamer::buffer::MappedBuffer<gstreamer::buffer::Readable>,
}
unsafe impl Send for GstResource {}
unsafe impl Sync for GstResource {}
impl MemoryResource for GstResource {
fn as_ptr(&self) -> *mut u8 {
self._map.as_ptr() as *mut u8
}
fn len_bytes(&self) -> usize {
self._map.len()
}
fn domain(&self) -> MemoryDomain {
MemoryDomain::Host
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
pub(crate) fn image_from_gst_buffer(
size: kornia_image::ImageSize,
mapped_buffer: gstreamer::buffer::MappedBuffer<gstreamer::buffer::Readable>,
) -> Result<kornia_image::Image<u8, 3>, crate::stream::error::StreamCaptureError> {
let data_ptr: *const u8 = mapped_buffer.as_ptr();
let data_len: usize = mapped_buffer.len();
let expected_len = size
.width
.checked_mul(size.height)
.and_then(|n| n.checked_mul(3))
.ok_or_else(|| {
crate::stream::error::StreamCaptureError::InvalidImageFormat(format!(
"frame dimensions overflow: {}x{}",
size.width, size.height
))
})?;
if data_len < expected_len {
return Err(
crate::stream::error::StreamCaptureError::BufferSizeMismatch {
expected: expected_len,
got: data_len,
},
);
}
let resource = GstResource {
_map: mapped_buffer,
};
let keepalive: Arc<dyn Any + Send + Sync> = Arc::new(resource);
let image = unsafe {
Image::<u8, 3>::from_borrowed_host_readonly(size, data_ptr, keepalive)
.map_err(crate::stream::error::StreamCaptureError::ImageError)?
};
Ok(image)
}
#[cfg(test)]
mod tests {
use crate::stream::StreamCapture;
#[test]
fn quoted_value_cannot_inject_elements() -> Result<(), Box<dyn std::error::Error>> {
use gstreamer::prelude::*;
gstreamer::init()?;
let evil = "/tmp/a b ! fakesink name=injected location=x";
let desc = format!(
"filesrc name=src location={}",
super::quote_pipeline_value(evil)?
);
let bin = gstreamer::parse::launch(&desc)?
.dynamic_cast::<gstreamer::Bin>()
.ok();
assert!(bin.is_none(), "value was split into multiple elements");
let src = gstreamer::parse::launch(&desc)?;
assert_eq!(
src.property::<Option<String>>("location").as_deref(),
Some(evil)
);
assert!(super::quote_pipeline_value("a\" ! fakesink").is_err());
assert!(super::quote_pipeline_value("a\\").is_err());
assert!(super::quote_pipeline_value("a\nb").is_err());
Ok(())
}
#[test]
fn location_property_accepts_any_path() -> Result<(), Box<dyn std::error::Error>> {
use gstreamer::prelude::*;
gstreamer::init()?;
let path = std::path::Path::new(r#"C:\videos\my clip ! fakesink name=x "q".mp4"#);
let pipeline = gstreamer::parse::launch("filesrc name=src ! fakesink")?
.dynamic_cast::<gstreamer::Pipeline>()
.map_err(|_| "not a pipeline")?;
super::set_location_property(&pipeline, "src", path)?;
let src = pipeline.by_name("src").ok_or("missing src")?;
assert_eq!(
src.property::<Option<String>>("location").as_deref(),
path.to_str()
);
assert_eq!(pipeline.children().len(), 2);
assert!(super::set_location_property(&pipeline, "missing", path).is_err());
let fakesink = pipeline
.children()
.into_iter()
.find(|e| e.name() != "src")
.ok_or("missing fakesink")?;
assert!(super::set_location_property(&pipeline, &fakesink.name(), path).is_err());
Ok(())
}
#[test]
fn gst_resource_capture_n_frames_and_drop() -> Result<(), Box<dyn std::error::Error>> {
const N_FRAMES: usize = 5;
const WIDTH: usize = 8;
const HEIGHT: usize = 4;
if !gstreamer::INITIALIZED.load(std::sync::atomic::Ordering::Relaxed) {
gstreamer::init()?;
}
let pipeline_desc = format!(
"videotestsrc num-buffers={n} ! \
video/x-raw,format=RGB,width={w},height={h},framerate=30/1 ! \
appsink name=sink sync=false",
n = N_FRAMES,
w = WIDTH,
h = HEIGHT,
);
let mut capture = StreamCapture::new(&pipeline_desc)?;
capture.start()?;
let mut frames_received = 0usize;
let max_polls = N_FRAMES * 5;
for _ in 0..max_polls {
if let Some(image) = capture.grab_rgb8()? {
assert_eq!(image.width(), WIDTH, "frame width mismatch");
assert_eq!(image.height(), HEIGHT, "frame height mismatch");
assert_eq!(image.num_channels(), 3, "frame channels mismatch");
let slice = image.as_slice();
assert_eq!(
slice.len(),
WIDTH * HEIGHT * 3,
"frame pixel count mismatch"
);
let _ = slice[0];
let _ = slice[slice.len() - 1];
frames_received += 1;
}
if frames_received >= N_FRAMES {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
capture.close()?;
assert_eq!(
frames_received, N_FRAMES,
"expected {N_FRAMES} frames but received {frames_received}"
);
Ok(())
}
#[test]
fn gst_buffer_size_validation_arithmetic() {
assert_eq!(8usize * 4 * 3, 96, "8x4 RGB24 = 96 bytes");
assert_eq!(640usize * 480 * 3, 921_600, "640x480 RGB24 = 921600 bytes");
assert_eq!(
1920usize * 1080 * 3,
6_220_800,
"1920x1080 RGB24 = 6220800 bytes"
);
let width = 8usize;
let height = 4usize;
let expected = width * height * 3;
assert!(
(expected - 1) < expected,
"a buffer of size expected-1 is strictly smaller than expected"
);
}
}