use std::{mem, time::Instant};
#[cfg(feature = "libcamera")]
use crate::capture_api::libcamera_backend::{ControlMessage, PendingControlState};
use crate::metrics::StageMetrics;
use crate::{BackendKind, ProbedBackend};
#[cfg(feature = "v4l2")]
use super::controls::{apply_v4l2_controls, read_v4l2_control};
#[cfg(feature = "file-backend")]
use super::file_backend;
#[cfg(feature = "libcamera")]
use super::libcamera_backend;
#[cfg(feature = "netcam")]
use super::netcam_backend;
use super::request::CaptureError;
#[cfg(feature = "v4l2")]
use super::v4l2_backend;
use super::virtual_backend;
use styx_capture::prelude::*;
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub enum ControlPlane {
None,
#[cfg(feature = "v4l2")]
V4l2 {
path: String,
},
#[cfg(feature = "libcamera")]
Libcamera {
tx: std::sync::mpsc::Sender<ControlMessage>,
pending: std::sync::Arc<std::sync::Mutex<PendingControlState>>,
},
#[cfg(feature = "file-backend")]
File {
state: file_backend::FileControlStateHandle,
},
Virtual,
}
pub struct CaptureHandle {
pub(crate) backend: BackendKind,
pub(crate) control: ControlPlane,
pub(crate) descriptor: CaptureDescriptor,
pub(crate) mode: Mode,
pub(crate) interval: Option<Interval>,
pub(crate) rx: BoundedRx<FrameLease>,
pub(crate) stop_tx: Option<std::sync::mpsc::Sender<()>>,
pub(super) worker: Option<WorkerHandle>,
pub(crate) metrics: StageMetrics,
}
pub enum WorkerHandle {
Thread(std::thread::JoinHandle<()>),
#[cfg(feature = "async")]
Async(tokio::task::JoinHandle<()>),
}
impl CaptureHandle {
pub fn recv(&self) -> RecvOutcome<FrameLease> {
let start = Instant::now();
match self.rx.recv() {
RecvOutcome::Data(frame) => {
self.metrics.record(start.elapsed());
RecvOutcome::Data(frame)
}
other => other,
}
}
#[cfg(feature = "async")]
pub async fn recv_async(&self) -> RecvOutcome<FrameLease> {
let start = Instant::now();
match self.rx.recv_async().await {
RecvOutcome::Data(frame) => {
self.metrics.record(start.elapsed());
RecvOutcome::Data(frame)
}
other => other,
}
}
pub fn recv_blocking(&self, wait: std::time::Duration) -> RecvOutcome<FrameLease> {
loop {
match self.recv() {
RecvOutcome::Empty => {
if !wait.is_zero() {
std::thread::sleep(wait);
} else {
std::thread::yield_now();
}
}
other => return other,
}
}
}
pub fn stop(mut self) {
self.teardown_in_place();
}
#[cfg(feature = "async")]
pub async fn stop_async(self) {
tokio::task::block_in_place(|| self.stop());
}
fn teardown_in_place(&mut self) {
if let Some(tx) = self.stop_tx.take() {
let _ = tx.send(());
}
self.rx.close();
if let Some(worker) = self.worker.take() {
match worker {
WorkerHandle::Thread(h) => {
let _ = h.join();
}
#[cfg(feature = "async")]
WorkerHandle::Async(h) => {
if let Ok(rt) = tokio::runtime::Handle::try_current() {
let _ = rt.block_on(h);
} else {
h.abort();
}
}
}
}
}
pub fn reconfigure(
self,
request: super::CaptureRequest<'_>,
) -> Result<CaptureHandle, CaptureError> {
let mut handle = self;
handle.reconfigure_in_place(request)?;
Ok(handle)
}
pub fn reconfigure_in_place(
&mut self,
request: super::CaptureRequest<'_>,
) -> Result<(), CaptureError> {
self.teardown_in_place();
let mut new_capture = request.start()?;
mem::swap(self, &mut new_capture);
Ok(())
}
pub fn backend(&self) -> BackendKind {
self.backend
}
pub fn mode(&self) -> &Mode {
&self.mode
}
pub fn interval(&self) -> Option<Interval> {
self.interval
}
pub fn metrics(&self) -> StageMetrics {
self.metrics.clone()
}
pub fn set_control(&self, _id: ControlId, _value: ControlValue) -> Result<(), CaptureError> {
match &self.control {
ControlPlane::None | ControlPlane::Virtual => Err(CaptureError::ControlUnsupported),
#[cfg(feature = "v4l2")]
ControlPlane::V4l2 { path } => apply_v4l2_controls(path, &[(_id, _value)]),
#[cfg(feature = "libcamera")]
ControlPlane::Libcamera { tx, pending } => {
{
let mut guard = pending.lock().map_err(|_| CaptureError::ControlApply("libcamera pending lock poisoned".into()))?;
if matches!(_value, ControlValue::None) {
guard.insert(_id, None);
} else {
guard.insert(_id, Some(_value));
}
}
tx.send(ControlMessage::Wake)
.map_err(|_| CaptureError::ControlApply("libcamera channel closed".into()))
}
#[cfg(feature = "file-backend")]
ControlPlane::File { state } => file_backend::apply_file_control(state, _id, _value),
}
}
#[cfg(feature = "async")]
pub async fn set_control_async(
&self,
id: ControlId,
value: ControlValue,
) -> Result<(), CaptureError> {
tokio::task::block_in_place(|| self.set_control(id, value))
}
pub fn get_control(&self, _id: ControlId) -> Result<ControlValue, CaptureError> {
match &self.control {
#[cfg(feature = "v4l2")]
ControlPlane::V4l2 { path } => read_v4l2_control(path, _id),
#[cfg(feature = "libcamera")]
ControlPlane::Libcamera { tx, .. } => {
let (resp_tx, resp_rx) = std::sync::mpsc::channel();
tx.send(ControlMessage::Get(_id, resp_tx))
.map_err(|_| CaptureError::ControlApply("libcamera channel closed".into()))?;
resp_rx
.recv()
.map_err(|_| CaptureError::ControlApply("libcamera response closed".into()))?
}
#[cfg(feature = "file-backend")]
ControlPlane::File { state } => file_backend::read_file_control(state, _id),
_ => Err(CaptureError::ControlUnsupported),
}
}
#[cfg(feature = "async")]
pub async fn get_control_async(&self, id: ControlId) -> Result<ControlValue, CaptureError> {
tokio::task::block_in_place(|| self.get_control(id))
}
}
impl Drop for CaptureHandle {
fn drop(&mut self) {
if self.worker.is_some() {
self.teardown_in_place();
}
}
}
impl CaptureSource for CaptureHandle {
fn descriptor(&self) -> &CaptureDescriptor {
&self.descriptor
}
fn next_frame(&self) -> Option<FrameLease> {
loop {
match self.rx.recv() {
RecvOutcome::Data(frame) => return Some(frame),
RecvOutcome::Closed => return None,
RecvOutcome::Empty => {
std::thread::yield_now();
continue;
}
}
}
}
}
pub(crate) fn start_backend(
backend: &ProbedBackend,
mode: Mode,
interval: Option<Interval>,
#[allow(unused_variables)] controls: Vec<(ControlId, ControlValue)>,
#[allow(unused_variables)] enable_tdn_output: bool,
) -> Result<CaptureHandle, CaptureError> {
let descriptor = backend.descriptor.clone();
match backend.kind {
BackendKind::Virtual => virtual_backend::start_virtual(mode, interval, descriptor),
#[cfg(feature = "v4l2")]
BackendKind::V4l2 => {
v4l2_backend::start_v4l2(backend, mode, interval, controls, descriptor)
}
#[cfg(not(feature = "v4l2"))]
BackendKind::V4l2 => Err(CaptureError::BackendMissing(BackendKind::V4l2)),
#[cfg(feature = "libcamera")]
BackendKind::Libcamera => {
libcamera_backend::start_libcamera(backend, mode, interval, controls, descriptor, enable_tdn_output)
}
#[cfg(not(feature = "libcamera"))]
BackendKind::Libcamera => Err(CaptureError::BackendMissing(BackendKind::Libcamera)),
#[cfg(feature = "netcam")]
BackendKind::Netcam => netcam_backend::start_netcam(backend, mode, interval, descriptor),
#[cfg(not(feature = "netcam"))]
BackendKind::Netcam => Err(CaptureError::BackendMissing(BackendKind::Netcam)),
#[cfg(feature = "file-backend")]
BackendKind::File => {
file_backend::start_file(backend, mode, interval, controls, descriptor)
}
#[cfg(not(feature = "file-backend"))]
BackendKind::File => Err(CaptureError::BackendMissing(BackendKind::File)),
}
}