use crate::image::{Image, ImageSize};
use anyhow::Result;
use gst::prelude::*;
pub struct WebcamCaptureBuilder {
camera_id: usize,
size: Option<ImageSize>,
}
impl WebcamCaptureBuilder {
pub fn new() -> Self {
Self {
camera_id: 0,
size: None,
}
}
pub fn camera_id(mut self, camera_id: usize) -> Self {
self.camera_id = camera_id;
self
}
pub fn with_size(mut self, size: ImageSize) -> Self {
self.size = Some(size);
self
}
pub fn build(self) -> Result<WebcamCapture> {
WebcamCapture::new(self.camera_id, self.size)
}
}
impl Default for WebcamCaptureBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct WebcamCapture {
pipeline: gst::Pipeline,
receiver: tokio::sync::mpsc::Receiver<Image<u8, 3>>,
handle: Vec<tokio::task::JoinHandle<()>>,
}
impl WebcamCapture {
fn new(camera_id: usize, size: Option<ImageSize>) -> Result<Self> {
gst::init()?;
let pipeline_str = Self::gst_pipeline_string(camera_id, size);
let pipeline = gst::parse::launch(&pipeline_str)?
.downcast::<gst::Pipeline>()
.map_err(|_| anyhow::anyhow!("Failed to downcast pipeline"))?;
let appsink = pipeline
.by_name("sink")
.ok_or_else(|| anyhow::anyhow!("Failed to get sink"))?
.dynamic_cast::<gst_app::AppSink>()
.map_err(|_| anyhow::anyhow!("Failed to cast to AppSink"))?;
let (tx, rx) = tokio::sync::mpsc::channel(50);
appsink.set_callbacks(
gst_app::AppSinkCallbacks::builder()
.new_sample(move |sink| match Self::extract_image_frame(sink) {
Ok(frame) => {
if tx.blocking_send(frame).is_err() {
Err(gst::FlowError::Error)
} else {
Ok(gst::FlowSuccess::Ok)
}
}
Err(_) => Err(gst::FlowError::Error),
})
.build(),
);
Ok(Self {
pipeline,
receiver: rx,
handle: vec![],
})
}
pub async fn run<F>(&mut self, f: F) -> Result<()>
where
F: Fn(Image<u8, 3>) -> Result<()>,
{
let pipeline = &self.pipeline;
pipeline.set_state(gst::State::Playing)?;
let bus = pipeline
.bus()
.ok_or_else(|| anyhow::anyhow!("Failed to get bus"))?;
let handle = tokio::task::spawn(async move {
for msg in bus.iter_timed(gst::ClockTime::NONE) {
use gst::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
eprintln!(
"Error from {:?}: {} ({:?})",
msg.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
break;
}
_ => (),
}
}
});
self.handle.push(handle);
while let Some(img) = self.receiver.recv().await {
f(img)?;
}
Ok(())
}
pub async fn close(&mut self) -> Result<()> {
self.pipeline.send_event(gst::event::Eos::new());
while let Some(h) = self.handle.pop() {
h.await?;
}
self.pipeline.set_state(gst::State::Null)?;
Ok(())
}
fn gst_pipeline_string(camera_id: usize, size: Option<ImageSize>) -> String {
let video_resize = if let Some(size) = size {
format!(
" ! video/x-raw,width={},height={},framerate=30/1",
size.width, size.height
)
} else {
"".to_string()
};
format!(
"v4l2src device=/dev/video{} {}! videoconvert ! videoscale ! video/x-raw,format=RGB ! appsink name=sink",
camera_id, video_resize
)
}
fn extract_image_frame(appsink: &gst_app::AppSink) -> Result<Image<u8, 3>> {
let sample = appsink.pull_sample()?;
let caps = sample
.caps()
.ok_or_else(|| anyhow::anyhow!("Failed to get caps from sample"))?;
let structure = caps
.structure(0)
.ok_or_else(|| anyhow::anyhow!("Failed to get structure"))?;
let height = structure.get::<i32>("height")? as usize;
let width = structure.get::<i32>("width")? as usize;
let buffer = sample
.buffer()
.ok_or_else(|| anyhow::anyhow!("Failed to get buffer from sample"))?;
let map = buffer.map_readable()?;
Image::<u8, 3>::new(ImageSize { width, height }, map.as_slice().to_vec())
}
}