bevy_iroh 0.4.0

Share Bevy entities between peers over iroh: replication, rooms, presence and typed messages, with optional voice and video
Documentation
//! Cameras through `bevy_v4l2`. Behind the `v4l2` feature, Linux only.
//!
//! [`Camera`] opens a device headless: no plane, no material, no render world, just the
//! capture thread and its colour tap feeding the encoder in planar 4:2:0 with the driver's
//! timestamps. [`VideoInput::camera`](super::VideoInput::camera) builds one from
//! [`MediaSettings::camera`](super::MediaSettings). An app that already runs bevy_v4l2's own
//! `Webcam` entity can share its capture instead: `VideoInput::new(feed.capture.color_tap())`.

use std::{sync::Arc, time::Duration};

use bevy_v4l2::{
    ColorTap,
    capture::{Capture, CaptureConfig, FOURCC_MJPG},
    select::{CameraFormat, FrameFormat, RequestedFormat, choose},
};

use super::{
    devices::CameraDevice,
    video::{Pixels, VideoFrame, VideoSource},
};

/// Every capture device under `/dev`, with the mode it would open at.
pub fn cameras() -> Vec<CameraDevice> {
    bevy_v4l2::controls::devices()
        .into_iter()
        .enumerate()
        .map(|(i, info)| {
            let detail = bevy_v4l2::capture::list_modes(&info.path)
                .ok()
                .and_then(|modes| choose(&modes, RequestedFormat::HighestResolution))
                .map(|c| format!("{}x{} {} @ {:.0} fps", c.width, c.height, c.fourcc, c.fps))
                .unwrap_or_else(|| "no usable mode".into());
            CameraDevice {
                id: info.path.to_string_lossy().into_owned(),
                name: info.name,
                is_default: i == 0,
                detail,
            }
        })
        .collect()
}

/// A running camera, as a video source.
pub struct Camera {
    capture: Arc<Capture>,
    tap: ColorTap,
    /// Present for compressed modes: it returns buffers to the driver and feeds the tap.
    _decoder: Option<bevy_v4l2::mjpeg::Decoder>,
    /// Raw modes hand every frame to us to return.
    raw: bool,
}

impl Camera {
    /// Open `id` (a `/dev/videoN` path; `None` is the first camera) at the mode nearest to
    /// `width`x`height` at `fps`, raw if a raw mode reaches the rate, MJPEG otherwise.
    pub fn open(id: Option<&str>, width: u32, height: u32, fps: f32) -> Result<Self, String> {
        let path = match id {
            Some(id) => std::path::PathBuf::from(id),
            None => bevy_v4l2::controls::devices()
                .into_iter()
                .next()
                .map(|d| d.path)
                .ok_or("no camera")?,
        };
        let modes = bevy_v4l2::capture::list_modes(&path)
            .map_err(|e| format!("cannot enumerate {}: {e}", path.display()))?;
        let want = CameraFormat::new(width, height, FrameFormat::Any, fps.round() as u32);
        let choice = choose(&modes, RequestedFormat::Closest(want))
            .ok_or_else(|| format!("{} offers nothing usable", path.display()))?;
        let config = CaptureConfig {
            device: path.clone(),
            width: choice.width,
            height: choice.height,
            fourcc: choice.fourcc,
            fps: Some(choice.fps.round() as u32),
            ..Default::default()
        };
        let capture = Arc::new(Capture::open(&config).map_err(|e| format!("camera: {e}"))?);
        let layout = capture.layout();
        let raw = layout.fourcc != FOURCC_MJPG;
        tracing::info!(
            "bevy_iroh: camera {} at {}x{} {} @ {:.0} fps ({})",
            path.display(),
            layout.width,
            layout.height,
            layout.fourcc,
            choice.fps,
            if raw { "raw" } else { "MJPEG" }
        );
        let tap = capture.color_tap();
        let decoder = (!raw).then(|| {
            bevy_v4l2::mjpeg::Decoder::start(capture.clone(), layout.width, layout.height)
        });
        Ok(Self {
            capture,
            tap,
            _decoder: decoder,
            raw,
        })
    }
}

impl VideoSource for Camera {
    fn next_frame(&mut self) -> Option<VideoFrame> {
        if self.raw {
            // The colour copy was taken on the capture thread; the buffer itself goes
            // straight back to the driver.
            while let Some(frame) = self.capture.try_recv_latest() {
                self.capture.requeue(frame.index);
            }
        }
        let frame = self.tap.recv_timeout(Duration::from_millis(5))?;
        // Newest wins, if more than one arrived.
        let frame = std::iter::from_fn(|| self.tap.try_recv_latest())
            .last()
            .unwrap_or(frame);
        Some(yuv_to_frame(frame))
    }
}

fn yuv_to_frame(frame: bevy_v4l2::capture::YuvFrame) -> VideoFrame {
    VideoFrame {
        width: frame.width,
        height: frame.height,
        pixels: Pixels::I420 {
            y: frame.y,
            u: frame.u,
            v: frame.v,
        },
        timestamp_ms: frame.timestamp.as_millis() as u64,
    }
}

/// A `bevy_v4l2` colour tap as a source, for sharing a `Webcam` entity's capture.
impl VideoSource for ColorTap {
    fn next_frame(&mut self) -> Option<VideoFrame> {
        self.try_recv_latest().map(yuv_to_frame)
    }
}