Skip to main content

pixelflux/webcam/
mod.rs

1//! Virtual camera: the browser's webcam, delivered to applications as a V4L2 capture device.
2//!
3//! The client encodes its camera (H.264/VP8/… over the WebRTC media track or WebCodecs over the
4//! WebSocket, MJPEG as the last-resort canvas path) and Selkies hands each encoded frame to
5//! [`VirtualCamera::push`], which returns at once. A worker thread decodes, fits the picture into the
6//! device's fixed raw format, and publishes it to every sink at once:
7//!
8//! - the shared-memory ring served over a Unix socket to the Selkies V4L2 interposer
9//!   (`LD_PRELOAD`, no privileges, no kernel module), see [`ring`] and [`server`];
10//! - a v4l2loopback output device where one is configured or found, see [`v4l2out`] — the kernel
11//!   path for hosts and privileged containers, as `/dev/uinput` is for gamepads;
12//! - a PipeWire `Video/Source` node where a daemon is reachable, see [`pipewire`] — for
13//!   PipeWire-native consumers and the `pipewire-v4l2` wrapper.
14//!
15//! Nothing on the hot path copies more than the encoded bytes into a pooled buffer, and no
16//! worker thread ever calls back into the consumer.
17
18pub mod convert;
19pub mod decode;
20pub mod pipewire;
21pub mod ring;
22pub mod server;
23pub mod v4l2out;
24
25use std::collections::VecDeque;
26use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
27use std::sync::{Arc, Condvar, Mutex};
28use std::thread::{self, JoinHandle};
29use std::time::{Duration, Instant};
30
31use crate::Error;
32
33use convert::{DeviceFormat, Normalizer, Orientation, orient_i420};
34use decode::{Codec, DecodeError, Decoder, new_decoder, sniff_keyframe};
35use pipewire::PipeWireSink;
36use ring::{Ring, RingFormat};
37use server::Server;
38use v4l2out::V4l2Output;
39
40/// Configuration for [`VirtualCamera::start`].
41#[derive(Clone, Debug, PartialEq)]
42pub struct VirtualCameraSettings {
43    /// Unix socket the interposer connects to.
44    pub socket_path: String,
45    /// Advertised frame size; incoming frames are scaled and letterboxed to fit.
46    pub width: u32,
47    pub height: u32,
48    /// Advertised frame rate as a fraction.
49    pub fps_num: u32,
50    pub fps_den: u32,
51    /// Raw device pixel format: "I420", "NV12", or "YUYV".
52    /// Device pixel format: "I420" (the default; the browsers' preference), "NV12", "YUYV",
53    /// or "MJPEG" — a compressed device that carries an MJPEG uplink's frames as received
54    /// (no decode, one copy) and re-encodes only frames that must be fitted into the device
55    /// size or arrive in another codec.
56    pub pixel_format: String,
57    /// Ring slots (2..4); more slots tolerate slower readers.
58    pub slots: u32,
59    /// Encoded frames buffered ahead of the decoder before the oldest is dropped.
60    pub queue_depth: u32,
61    /// v4l2loopback output device: "" for none, "auto" to use the first one found, or a path.
62    pub device_path: String,
63    /// Also publish the camera as a PipeWire `Video/Source` node when a daemon is reachable.
64    pub pipewire: bool,
65    /// PipeWire node name (the description shown to users is fixed).
66    pub pipewire_node_name: String,
67}
68
69impl Default for VirtualCameraSettings {
70    fn default() -> Self {
71        VirtualCameraSettings {
72            socket_path: format!("{}/selkies_webcam0.sock", crate::webcam::socket_dir()),
73            width: 1280,
74            height: 720,
75            fps_num: 30,
76            fps_den: 1,
77            pixel_format: "I420".into(),
78            slots: 3,
79            queue_depth: 4,
80            device_path: "auto".into(),
81            pipewire: true,
82            pipewire_node_name: "selkies-webcam".into(),
83        }
84    }
85}
86
87/// Where the camera socket lives unless a caller names one: the session's private runtime
88/// directory, where no other account can take the name first, else `/tmp`. The v4l2
89/// interposer looks in the same place.
90pub fn socket_dir() -> String {
91    std::env::var("XDG_RUNTIME_DIR")
92        .ok()
93        .filter(|d| !d.is_empty())
94        .unwrap_or_else(|| "/tmp".into())
95}
96
97fn parse_pixel_format(name: &str) -> Option<u32> {
98    match name.trim().to_ascii_uppercase().as_str() {
99        "I420" | "YU12" | "YUV420" => Some(ring::V4L2_PIX_FMT_YUV420),
100        "NV12" => Some(ring::V4L2_PIX_FMT_NV12),
101        "YUYV" | "YUY2" => Some(ring::V4L2_PIX_FMT_YUYV),
102        "MJPEG" | "MJPG" | "JPEG" => Some(ring::V4L2_PIX_FMT_MJPEG),
103        _ => None,
104    }
105}
106
107/// Counters read by `VirtualCamera.stats()`.
108#[derive(Default)]
109struct Stats {
110    pushed: AtomicU64,
111    decoded: AtomicU64,
112    published: AtomicU64,
113    passthrough: AtomicU64,
114    dropped: AtomicU64,
115    skipped: AtomicU64,
116    errors: AtomicU64,
117    input_width: AtomicU32,
118    input_height: AtomicU32,
119    input_codec: AtomicU32,
120    input_rotation: AtomicU32,
121    /// Whether a PipeWire consumer was linked at the last fan-out. The interposer counts its
122    /// own clients and a kernel device's openers are the kernel's to know, so this is the one
123    /// sink whose consumers only it can see.
124    pipewire_streaming: AtomicBool,
125}
126
127struct Job {
128    codec: Codec,
129    keyframe: bool,
130    orientation: Orientation,
131    data: Vec<u8>,
132}
133
134struct QueueState {
135    jobs: VecDeque<Job>,
136    closed: bool,
137}
138
139/// Bounded hand-off to the decoder thread that drops the oldest frame when the decoder falls
140/// behind: a camera must stay live, never grow latency.
141struct Queue {
142    state: Mutex<QueueState>,
143    cv: Condvar,
144    capacity: usize,
145}
146
147impl Queue {
148    fn new(capacity: usize) -> Self {
149        Queue {
150            state: Mutex::new(QueueState {
151                jobs: VecDeque::new(),
152                closed: false,
153            }),
154            cv: Condvar::new(),
155            capacity: capacity.max(1),
156        }
157    }
158
159    /// Returns the job evicted to make room, if any.
160    fn push(&self, job: Job) -> Option<Job> {
161        let mut st = self.state.lock().unwrap_or_else(|e| e.into_inner());
162        let evicted = if st.jobs.len() >= self.capacity {
163            st.jobs.pop_front()
164        } else {
165            None
166        };
167        st.jobs.push_back(job);
168        drop(st);
169        self.cv.notify_one();
170        evicted
171    }
172
173    fn pop(&self) -> Option<Job> {
174        let mut st = self.state.lock().unwrap_or_else(|e| e.into_inner());
175        loop {
176            if let Some(j) = st.jobs.pop_front() {
177                return Some(j);
178            }
179            if st.closed {
180                return None;
181            }
182            st = self.cv.wait(st).unwrap_or_else(|e| e.into_inner());
183        }
184    }
185
186    fn close(&self) {
187        self.state.lock().unwrap_or_else(|e| e.into_inner()).closed = true;
188        self.cv.notify_all();
189    }
190}
191
192/// Recycled encoded-frame buffers so the steady state allocates nothing per frame.
193struct Pool(Mutex<Vec<Vec<u8>>>);
194
195impl Pool {
196    fn take(&self, len: usize) -> Vec<u8> {
197        let mut v = self
198            .0
199            .lock()
200            .unwrap_or_else(|e| e.into_inner())
201            .pop()
202            .unwrap_or_default();
203        v.clear();
204        v.reserve(len);
205        v
206    }
207
208    fn put(&self, v: Vec<u8>) {
209        let mut pool = self.0.lock().unwrap_or_else(|e| e.into_inner());
210        if pool.len() < 8 {
211            pool.push(v);
212        }
213    }
214}
215
216struct Running {
217    queue: Arc<Queue>,
218    thread: Option<JoinHandle<()>>,
219    server: Arc<Server>,
220    keyframe_wanted: Arc<AtomicBool>,
221    pool: Arc<Pool>,
222    device_path: Arc<Mutex<String>>,
223    format: RingFormat,
224    pipewire: bool,
225}
226
227/// A virtual camera fed with encoded frames by the consumer.
228pub struct VirtualCamera {
229    running: Mutex<Option<Running>>,
230    stats: Arc<Stats>,
231}
232
233fn monotonic_ns() -> u64 {
234    let mut ts = libc::timespec {
235        tv_sec: 0,
236        tv_nsec: 0,
237    };
238    unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) };
239    ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64
240}
241
242struct WorkerConfig {
243    queue: Arc<Queue>,
244    ring: Ring,
245    server: Arc<Server>,
246    stats: Arc<Stats>,
247    keyframe_wanted: Arc<AtomicBool>,
248    pool: Arc<Pool>,
249    v4l2out: Option<V4l2Output>,
250    device_path: Arc<Mutex<String>>,
251    pipewire: Option<PipeWireSink>,
252}
253
254/// Re-encode quality for frames that must be fitted into an MJPEG device (a camera of another
255/// size, an H.264/VP8 uplink); frames of the device's own size pass through as received.
256const MJPEG_REENCODE_QUALITY: i32 = 85;
257
258/// JPEG writer for the MJPEG device: the fitted, full-range I420 picture compressed straight into
259/// the ring slot.
260struct MjpegEncoder {
261    comp: turbojpeg::Compressor,
262    i420: Vec<u8>,
263}
264
265impl MjpegEncoder {
266    fn new() -> Result<Self, String> {
267        let mut comp =
268            turbojpeg::Compressor::new().map_err(|e| format!("turbojpeg compressor: {}", e))?;
269        comp.set_quality(MJPEG_REENCODE_QUALITY)
270            .map_err(|e| format!("turbojpeg quality: {}", e))?;
271        comp.set_subsamp(turbojpeg::Subsamp::Sub2x2)
272            .map_err(|e| format!("turbojpeg subsampling: {}", e))?;
273        Ok(MjpegEncoder {
274            comp,
275            i420: Vec::new(),
276        })
277    }
278
279    fn write_frame(
280        &mut self,
281        normalizer: &mut Normalizer,
282        src: &convert::I420View<'_>,
283        dev: &DeviceFormat,
284        out: &mut [u8],
285    ) -> usize {
286        let need = dev.frame_bytes();
287        self.i420.resize(need, 0);
288        if normalizer.write_frame(src, dev, &mut self.i420) == 0 {
289            return 0;
290        }
291        let img = turbojpeg::YuvImage {
292            pixels: &self.i420[..],
293            width: dev.width,
294            align: 1,
295            height: dev.height,
296            subsamp: turbojpeg::Subsamp::Sub2x2,
297        };
298        self.comp.compress_yuv_to_slice(img, out).unwrap_or(0)
299    }
300}
301
302/// A black frame in `dev`'s format, at most `capacity` bytes; `None` for an MJPEG device without
303/// its encoder.
304fn black_frame(
305    dev: &DeviceFormat,
306    normalizer: &mut Normalizer,
307    jpeg_out: Option<&mut MjpegEncoder>,
308    capacity: u32,
309) -> Option<Vec<u8>> {
310    let mut black = convert::I420Buffer::new(dev.width, dev.height);
311    black.fill_black(false);
312    let mut frame = vec![0u8; (capacity as usize).max(dev.frame_bytes())];
313    let n = match jpeg_out {
314        Some(enc) => enc.write_frame(normalizer, &black.view(false), dev, &mut frame),
315        None if dev.fourcc == ring::V4L2_PIX_FMT_MJPEG => 0,
316        None => normalizer.write_frame(&black.view(false), dev, &mut frame),
317    };
318    frame.truncate(n);
319    (n > 0).then_some(frame)
320}
321
322/// After a publish: wake the socket clients and mirror the frame into the kernel device and the
323/// PipeWire node where those sinks are up, retiring a sink that failed.
324fn fan_out(
325    ring: &Ring,
326    server: &Server,
327    v4l2out: &mut Option<V4l2Output>,
328    device_path: &Mutex<String>,
329    pipewire: &mut Option<PipeWireSink>,
330    ts: u64,
331    stats: &Stats,
332) {
333    server.ring_doorbell();
334    if let Some(frame) = ring.latest_frame() {
335        if let Some(out) = v4l2out.as_mut() {
336            out.write_frame(frame);
337            if out.is_failed() {
338                device_path
339                    .lock()
340                    .unwrap_or_else(|e| e.into_inner())
341                    .clear();
342                *v4l2out = None;
343            }
344        }
345        if let Some(pw) = pipewire.as_mut() {
346            pw.publish(frame, ts);
347            stats
348                .pipewire_streaming
349                .store(pw.is_streaming(), Ordering::Relaxed);
350            if pw.is_failed() {
351                eprintln!("[webcam] PipeWire node stopped; PipeWire sink disabled");
352                *pipewire = None;
353                stats.pipewire_streaming.store(false, Ordering::Relaxed);
354            }
355        }
356    }
357}
358
359/// Decoder thread body: decode, fit, publish, wake clients. A dropped or undecodable inter-coded
360/// frame parks the stream until the next keyframe and asks the client for one, so a consumer never
361/// sees the smear of predictions built on a missing reference.
362fn worker(cfg: WorkerConfig) {
363    let WorkerConfig {
364        queue,
365        mut ring,
366        server,
367        stats,
368        keyframe_wanted,
369        pool,
370        mut v4l2out,
371        device_path,
372        mut pipewire,
373    } = cfg;
374    let fmt = *ring.format();
375    let dev = DeviceFormat {
376        width: fmt.width as usize,
377        height: fmt.height as usize,
378        fourcc: fmt.fourcc,
379    };
380    let mut normalizer = Normalizer::new();
381    let mut oriented = convert::I420Buffer::new(2, 2);
382    let mut decoder: Option<Box<dyn Decoder>> = None;
383    let mut need_keyframe = true;
384    let mut last_error_log = Instant::now() - Duration::from_secs(60);
385    let log_error = |msg: String, last: &mut Instant| {
386        if last.elapsed() >= Duration::from_secs(5) {
387            eprintln!("[webcam] {}", msg);
388            *last = Instant::now();
389        }
390    };
391    let mjpeg_device = dev.fourcc == ring::V4L2_PIX_FMT_MJPEG;
392    let mut jpeg_out = if mjpeg_device {
393        match MjpegEncoder::new() {
394            Ok(enc) => Some(enc),
395            Err(e) => {
396                eprintln!(
397                    "[webcam] {}; only MJPEG frames of the device size can be served",
398                    e
399                );
400                None
401            }
402        }
403    } else {
404        None
405    };
406    // A loopback device created with `exclusive_caps` announces capture only once its producer
407    // has written a frame: until then no application finds it, and a camera started on demand
408    // waits for an application to open it.
409    if let Some(out) = v4l2out.as_mut()
410        && let Some(frame) = black_frame(&dev, &mut normalizer, jpeg_out.as_mut(), fmt.sizeimage)
411    {
412        out.write_frame(&frame);
413    }
414
415    while let Some(job) = queue.pop() {
416        if mjpeg_device && job.codec == Codec::Jpeg && job.orientation.is_upright() {
417            // The device speaks the uplink's own format: an upright frame of the device's
418            // size is published as received, undecoded; any other goes through decode and fit.
419            if let Ok(hdr) = turbojpeg::read_header(&job.data) {
420                stats.input_width.store(hdr.width as u32, Ordering::Relaxed);
421                stats
422                    .input_height
423                    .store(hdr.height as u32, Ordering::Relaxed);
424                let n = job.data.len();
425                if hdr.width == dev.width && hdr.height == dev.height && n <= fmt.sizeimage as usize
426                {
427                    let ts = monotonic_ns();
428                    if ring.publish(ts, |slot| {
429                        slot[..n].copy_from_slice(&job.data);
430                        n
431                    }) {
432                        stats.passthrough.fetch_add(1, Ordering::Relaxed);
433                        stats.published.fetch_add(1, Ordering::Relaxed);
434                        fan_out(
435                            &ring,
436                            &server,
437                            &mut v4l2out,
438                            &device_path,
439                            &mut pipewire,
440                            ts,
441                            &stats,
442                        );
443                    }
444                    pool.put(job.data);
445                    continue;
446                }
447            }
448        }
449        if decoder.as_ref().map(|d| d.codec()) != Some(job.codec) {
450            match new_decoder(job.codec) {
451                Ok(d) => decoder = Some(d),
452                Err(e) => {
453                    stats.errors.fetch_add(1, Ordering::Relaxed);
454                    log_error(
455                        format!("no decoder for {}: {}", job.codec.name(), e),
456                        &mut last_error_log,
457                    );
458                    pool.put(job.data);
459                    continue;
460                }
461            }
462            need_keyframe = true;
463        }
464        let dec = decoder.as_mut().expect("decoder present");
465        let keyframe = job.keyframe || sniff_keyframe(job.codec, &job.data).unwrap_or(false);
466        if need_keyframe && job.codec.is_inter_coded() && !keyframe {
467            stats.skipped.fetch_add(1, Ordering::Relaxed);
468            keyframe_wanted.store(true, Ordering::Relaxed);
469            pool.put(job.data);
470            continue;
471        }
472        match dec.decode(&job.data) {
473            Ok(true) => {
474                need_keyframe = false;
475                stats.decoded.fetch_add(1, Ordering::Relaxed);
476                if let Some(view) = dec.frame() {
477                    stats
478                        .input_width
479                        .store(view.width as u32, Ordering::Relaxed);
480                    stats
481                        .input_height
482                        .store(view.height as u32, Ordering::Relaxed);
483                    let view = if job.orientation.is_upright() {
484                        view
485                    } else {
486                        orient_i420(&view, job.orientation, &mut oriented);
487                        oriented.view(view.full_range)
488                    };
489                    let ts = monotonic_ns();
490                    let published = ring.publish(ts, |slot| match jpeg_out.as_mut() {
491                        Some(enc) => enc.write_frame(&mut normalizer, &view, &dev, slot),
492                        None if mjpeg_device => 0,
493                        None => normalizer.write_frame(&view, &dev, slot),
494                    });
495                    if published {
496                        stats.published.fetch_add(1, Ordering::Relaxed);
497                        fan_out(
498                            &ring,
499                            &server,
500                            &mut v4l2out,
501                            &device_path,
502                            &mut pipewire,
503                            ts,
504                            &stats,
505                        );
506                    }
507                }
508            }
509            Ok(false) => {}
510            Err(DecodeError::Corrupt(e)) => {
511                stats.errors.fetch_add(1, Ordering::Relaxed);
512                if job.codec.is_inter_coded() {
513                    need_keyframe = true;
514                    keyframe_wanted.store(true, Ordering::Relaxed);
515                }
516                log_error(
517                    format!("{} decode error: {}", job.codec.name(), e),
518                    &mut last_error_log,
519                );
520            }
521            Err(DecodeError::Fatal(e)) => {
522                stats.errors.fetch_add(1, Ordering::Relaxed);
523                log_error(
524                    format!("{} decoder reset: {}", job.codec.name(), e),
525                    &mut last_error_log,
526                );
527                decoder = None;
528                need_keyframe = true;
529                keyframe_wanted.store(true, Ordering::Relaxed);
530            }
531        }
532        pool.put(job.data);
533    }
534}
535
536impl Default for VirtualCamera {
537    fn default() -> Self {
538        Self::new()
539    }
540}
541
542impl VirtualCamera {
543    /// The codec ids `push` takes, as the client names a frame's codec on the wire.
544    pub const CODEC_MJPEG: u32 = Codec::Jpeg.id();
545    pub const CODEC_H264: u32 = Codec::H264.id();
546    pub const CODEC_VP8: u32 = Codec::Vp8.id();
547    pub const CODEC_VP9: u32 = Codec::Vp9.id();
548    pub const CODEC_AV1: u32 = Codec::Av1.id();
549    pub const CODEC_HEVC: u32 = Codec::H265.id();
550    /// `push()` result bit: the decoder needs a keyframe from the client to resume.
551    pub const KEYFRAME_WANTED: u32 = 1;
552
553    pub fn new() -> Self {
554        VirtualCamera {
555            running: Mutex::new(None),
556            stats: Arc::new(Stats::default()),
557        }
558    }
559
560    /// Bind the socket, allocate the ring, open the kernel device if configured, and start the
561    /// decoder thread. Restarting a running camera stops it first.
562    pub fn start(&self, settings: &VirtualCameraSettings) -> Result<(), Error> {
563        let fourcc = parse_pixel_format(&settings.pixel_format).ok_or_else(|| {
564            Error::Settings(format!(
565                "unsupported pixel_format '{}'",
566                settings.pixel_format
567            ))
568        })?;
569        if settings.width < 2
570            || settings.height < 2
571            || settings.width > 8192
572            || settings.height > 8192
573        {
574            return Err(Error::Settings("width/height out of range".into()));
575        }
576        if settings.fps_num == 0 || settings.fps_den == 0 {
577            return Err(Error::Settings("fps_num/fps_den must be positive".into()));
578        }
579        let fmt = RingFormat::for_fourcc(
580            fourcc,
581            settings.width & !1,
582            settings.height & !1,
583            settings.fps_num,
584            settings.fps_den,
585        )
586        .ok_or_else(|| Error::Settings("unsupported pixel_format".into()))?;
587        let socket_path = settings.socket_path.clone();
588        let slots = settings.slots;
589        let queue_depth = settings.queue_depth.max(1) as usize;
590        let device_setting = settings.device_path.trim().to_string();
591        let want_pipewire = settings.pipewire;
592        let pipewire_node_name = settings.pipewire_node_name.trim().to_string();
593        let stats = self.stats.clone();
594
595        self.stop();
596
597        let started = (move || -> Result<Running, String> {
598            let ring =
599                Ring::new(fmt, slots).map_err(|e| format!("ring allocation failed: {}", e))?;
600            let server = Arc::new(
601                Server::bind(&socket_path, ring.config_bytes(), ring.fd())
602                    .map_err(|e| format!("bind({}) failed: {}", socket_path, e))?,
603            );
604            let v4l2out = match device_setting.as_str() {
605                "" | "false" | "no" | "off" | "none" => None,
606                "auto" | "true" | "yes" | "on" => {
607                    V4l2Output::find_loopback_device().and_then(|p| {
608                        match V4l2Output::open(&p, &fmt) {
609                            Ok(o) => Some(o),
610                            Err(e) => {
611                                eprintln!("[webcam] {}; kernel device sink disabled", e);
612                                None
613                            }
614                        }
615                    })
616                }
617                path => match V4l2Output::open(path, &fmt) {
618                    Ok(o) => Some(o),
619                    Err(e) => {
620                        eprintln!("[webcam] {}; kernel device sink disabled", e);
621                        None
622                    }
623                },
624            };
625            let device_path = Arc::new(Mutex::new(
626                v4l2out
627                    .as_ref()
628                    .map(|o| o.path().to_string())
629                    .unwrap_or_default(),
630            ));
631            let pipewire = if want_pipewire {
632                match PipeWireSink::connect(&pipewire_node_name, "Selkies Virtual Camera", &fmt) {
633                    Ok(sink) => Some(sink),
634                    Err(e) => {
635                        eprintln!("[webcam] PipeWire sink unavailable: {}", e);
636                        None
637                    }
638                }
639            } else {
640                None
641            };
642            let pipewire_on = pipewire.is_some();
643            let queue = Arc::new(Queue::new(queue_depth));
644            let keyframe_wanted = Arc::new(AtomicBool::new(true));
645            let pool = Arc::new(Pool(Mutex::new(Vec::new())));
646            let cfg = WorkerConfig {
647                queue: queue.clone(),
648                ring,
649                server: server.clone(),
650                stats,
651                keyframe_wanted: keyframe_wanted.clone(),
652                pool: pool.clone(),
653                v4l2out,
654                device_path: device_path.clone(),
655                pipewire,
656            };
657            let thread = thread::Builder::new()
658                .name("pixelflux-webcam".into())
659                .spawn(move || worker(cfg))
660                .map_err(|e| format!("decoder thread spawn failed: {}", e))?;
661            Ok(Running {
662                queue,
663                thread: Some(thread),
664                server,
665                keyframe_wanted,
666                pool,
667                device_path,
668                format: fmt,
669                pipewire: pipewire_on,
670            })
671        })();
672        match started {
673            Ok(r) => {
674                *self.running.lock().unwrap_or_else(|e| e.into_inner()) = Some(r);
675                Ok(())
676            }
677            Err(e) => Err(Error::Capture(e)),
678        }
679    }
680
681    /// Hand one encoded frame to the decoder. `data` is the encoded payload; empty data pushes
682    /// nothing and only reports. `rotation` (clockwise degrees: 0, 90, 180, or 270) and `flip`
683    /// (a horizontal mirror applied after the rotation) carry the frame's upright transform when
684    /// the client's encoder left it as metadata instead of baking it into the pixels; the decoded
685    /// picture is oriented before it is fitted. Returns a bit set (`KEYFRAME_WANTED`) the caller
686    /// relays to the client. Fails when the camera is not running, the codec id is unknown, or the
687    /// rotation is not a quarter turn.
688    pub fn push(
689        &self,
690        data: &[u8],
691        codec: u32,
692        keyframe: bool,
693        rotation: u32,
694        flip: bool,
695    ) -> Result<u32, Error> {
696        let codec = Codec::from_id(codec)
697            .ok_or_else(|| Error::Settings(format!("unknown codec id {}", codec)))?;
698        if !rotation.is_multiple_of(90) || rotation >= 360 {
699            return Err(Error::Settings(
700                "rotation must be 0, 90, 180, or 270".into(),
701            ));
702        }
703        let orientation = Orientation {
704            quarter_turns: (rotation / 90) as u8,
705            hflip: flip,
706        };
707        let guard = self.running.lock().unwrap_or_else(|e| e.into_inner());
708        let running = guard
709            .as_ref()
710            .ok_or_else(|| Error::Capture("virtual camera is not running".into()))?;
711        if data.is_empty() {
712            return Ok(u32::from(
713                running.keyframe_wanted.swap(false, Ordering::Relaxed),
714            ));
715        }
716        let mut buf = running.pool.take(data.len());
717        buf.extend_from_slice(data);
718        self.stats.pushed.fetch_add(1, Ordering::Relaxed);
719        self.stats
720            .input_codec
721            .store(codec as u32, Ordering::Relaxed);
722        self.stats.input_rotation.store(rotation, Ordering::Relaxed);
723        if let Some(evicted) = running.queue.push(Job {
724            codec,
725            keyframe,
726            orientation,
727            data: buf,
728        }) {
729            self.stats.dropped.fetch_add(1, Ordering::Relaxed);
730            if evicted.codec.is_inter_coded() {
731                running.keyframe_wanted.store(true, Ordering::Relaxed);
732            }
733            running.pool.put(evicted.data);
734        }
735        Ok(u32::from(
736            running.keyframe_wanted.swap(false, Ordering::Relaxed),
737        ))
738    }
739
740    /// Stop the decoder thread, close every interposer client, and remove the socket.
741    pub fn stop(&self) {
742        let running = self
743            .running
744            .lock()
745            .unwrap_or_else(|e| e.into_inner())
746            .take();
747        if let Some(mut r) = running {
748            r.queue.close();
749            if let Some(t) = r.thread.take() {
750                let _ = t.join();
751            }
752            drop(r.server);
753        }
754    }
755
756    pub fn is_running(&self) -> bool {
757        self.running
758            .lock()
759            .unwrap_or_else(|e| e.into_inner())
760            .is_some()
761    }
762
763    /// Interposer clients that completed the handshake.
764    pub fn clients(&self) -> usize {
765        self.running
766            .lock()
767            .unwrap_or_else(|e| e.into_inner())
768            .as_ref()
769            .map(|r| r.server.client_count())
770            .unwrap_or(0)
771    }
772
773    pub fn socket_path(&self) -> Option<String> {
774        self.running
775            .lock()
776            .unwrap_or_else(|e| e.into_inner())
777            .as_ref()
778            .map(|r| r.server.path().to_string())
779    }
780
781    /// Kernel device currently mirrored, or "" when none is in use.
782    pub fn device_path(&self) -> String {
783        self.running
784            .lock()
785            .unwrap_or_else(|e| e.into_inner())
786            .as_ref()
787            .map(|r| {
788                r.device_path
789                    .lock()
790                    .unwrap_or_else(|e| e.into_inner())
791                    .clone()
792            })
793            .unwrap_or_default()
794    }
795
796    /// Counters and the negotiated geometry.
797    pub fn stats(&self) -> CameraStats {
798        let s = &self.stats;
799        let guard = self.running.lock().unwrap_or_else(|e| e.into_inner());
800        CameraStats {
801            pushed: s.pushed.load(Ordering::Relaxed),
802            decoded: s.decoded.load(Ordering::Relaxed),
803            published: s.published.load(Ordering::Relaxed),
804            passthrough: s.passthrough.load(Ordering::Relaxed),
805            dropped: s.dropped.load(Ordering::Relaxed),
806            skipped: s.skipped.load(Ordering::Relaxed),
807            errors: s.errors.load(Ordering::Relaxed),
808            input_width: s.input_width.load(Ordering::Relaxed),
809            input_height: s.input_height.load(Ordering::Relaxed),
810            input_rotation: s.input_rotation.load(Ordering::Relaxed),
811            input_codec: Codec::from_id(s.input_codec.load(Ordering::Relaxed))
812                .map(|c| c.name())
813                .unwrap_or(""),
814            running: guard.as_ref().map(|r| CameraRunning {
815                clients: r.server.client_count(),
816                width: r.format.width,
817                height: r.format.height,
818                fps_num: r.format.fps_num,
819                fps_den: r.format.fps_den,
820                pixel_format: v4l2out::fourcc_str(r.format.fourcc),
821                socket_path: r.server.path().to_string(),
822                device_path: r
823                    .device_path
824                    .lock()
825                    .unwrap_or_else(|e| e.into_inner())
826                    .clone(),
827                pipewire: r.pipewire,
828                pipewire_streaming: s.pipewire_streaming.load(Ordering::Relaxed),
829            }),
830        }
831    }
832}
833
834/// What `VirtualCamera::stats` reports: the counters since the camera was created, what the
835/// client last sent, and the running camera's geometry and sinks where one runs.
836#[derive(Clone, Debug, PartialEq)]
837pub struct CameraStats {
838    pub pushed: u64,
839    pub decoded: u64,
840    pub published: u64,
841    pub passthrough: u64,
842    pub dropped: u64,
843    pub skipped: u64,
844    pub errors: u64,
845    pub input_width: u32,
846    pub input_height: u32,
847    pub input_rotation: u32,
848    /// The codec of the last frame pushed, empty before the first.
849    pub input_codec: &'static str,
850    pub running: Option<CameraRunning>,
851}
852
853/// The running camera's negotiated format and sinks (`CameraStats::running`).
854#[derive(Clone, Debug, PartialEq)]
855pub struct CameraRunning {
856    pub clients: usize,
857    pub width: u32,
858    pub height: u32,
859    pub fps_num: u32,
860    pub fps_den: u32,
861    pub pixel_format: String,
862    pub socket_path: String,
863    pub device_path: String,
864    pub pipewire: bool,
865    pub pipewire_streaming: bool,
866}
867
868impl Drop for VirtualCamera {
869    fn drop(&mut self) {
870        if let Some(mut r) = self
871            .running
872            .lock()
873            .unwrap_or_else(|e| e.into_inner())
874            .take()
875        {
876            r.queue.close();
877            if let Some(t) = r.thread.take() {
878                let _ = t.join();
879            }
880        }
881    }
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887
888    /// The frame a loopback device is primed with is black in each format the ring serves.
889    #[test]
890    fn the_priming_frame_is_black_in_the_devices_format() {
891        let mut normalizer = Normalizer::new();
892        let (w, h) = (64usize, 48usize);
893        let luma = w * h;
894        let dev = |fourcc| DeviceFormat {
895            width: w,
896            height: h,
897            fourcc,
898        };
899        let i420 = black_frame(&dev(ring::V4L2_PIX_FMT_YUV420), &mut normalizer, None, 0).unwrap();
900        assert_eq!(i420.len(), luma * 3 / 2);
901        assert!(i420[..luma].iter().all(|&b| b == 16));
902        assert!(i420[luma..].iter().all(|&b| b == 128));
903        let nv12 = black_frame(&dev(ring::V4L2_PIX_FMT_NV12), &mut normalizer, None, 0).unwrap();
904        assert_eq!(nv12, i420);
905        let yuyv = black_frame(&dev(ring::V4L2_PIX_FMT_YUYV), &mut normalizer, None, 0).unwrap();
906        assert_eq!(yuyv.len(), luma * 2);
907        assert!(yuyv.chunks(2).all(|p| p == [16, 128]));
908        let mjpeg = dev(ring::V4L2_PIX_FMT_MJPEG);
909        assert!(black_frame(&mjpeg, &mut normalizer, None, 0).is_none());
910        let mut enc = MjpegEncoder::new().unwrap();
911        let jpeg = black_frame(&mjpeg, &mut normalizer, Some(&mut enc), 65536).unwrap();
912        assert_eq!(&jpeg[..2], [0xff, 0xd8]);
913    }
914
915    #[test]
916    fn queue_drops_oldest() {
917        let q = Queue::new(2);
918        assert!(
919            q.push(Job {
920                codec: Codec::H264,
921                keyframe: true,
922                orientation: Orientation::UPRIGHT,
923                data: vec![1]
924            })
925            .is_none()
926        );
927        assert!(
928            q.push(Job {
929                codec: Codec::H264,
930                keyframe: false,
931                orientation: Orientation::UPRIGHT,
932                data: vec![2]
933            })
934            .is_none()
935        );
936        let evicted = q
937            .push(Job {
938                codec: Codec::H264,
939                keyframe: false,
940                orientation: Orientation::UPRIGHT,
941                data: vec![3],
942            })
943            .unwrap();
944        assert_eq!(evicted.data, vec![1]);
945        assert_eq!(q.pop().unwrap().data, vec![2]);
946        assert_eq!(q.pop().unwrap().data, vec![3]);
947        q.close();
948        assert!(q.pop().is_none());
949    }
950
951    #[test]
952    fn pixel_format_names() {
953        assert_eq!(parse_pixel_format("i420"), Some(ring::V4L2_PIX_FMT_YUV420));
954        assert_eq!(parse_pixel_format("NV12"), Some(ring::V4L2_PIX_FMT_NV12));
955        assert_eq!(parse_pixel_format(" yuyv "), Some(ring::V4L2_PIX_FMT_YUYV));
956        assert_eq!(parse_pixel_format("MJPG"), Some(ring::V4L2_PIX_FMT_MJPEG));
957        assert_eq!(parse_pixel_format("mjpeg"), Some(ring::V4L2_PIX_FMT_MJPEG));
958        assert_eq!(parse_pixel_format("rgb"), None);
959        let mjpeg = RingFormat::for_fourcc(ring::V4L2_PIX_FMT_MJPEG, 640, 480, 30, 1).unwrap();
960        assert_eq!((mjpeg.bytesperline, mjpeg.sizeimage), (0, 640 * 480 * 2));
961    }
962
963    #[test]
964    fn mjpeg_encoder_writes_a_jpeg_of_the_device_size() {
965        let dev = DeviceFormat {
966            width: 64,
967            height: 32,
968            fourcc: ring::V4L2_PIX_FMT_MJPEG,
969        };
970        let mut src = convert::I420Buffer::new(32, 32);
971        src.data.fill(200);
972        let mut enc = MjpegEncoder::new().unwrap();
973        let mut normalizer = Normalizer::new();
974        let mut slot = vec![0u8; 64 * 32 * 2];
975        let n = enc.write_frame(&mut normalizer, &src.view(true), &dev, &mut slot);
976        assert!(n > 0 && n <= slot.len());
977        let hdr = turbojpeg::read_header(&slot[..n]).unwrap();
978        assert_eq!((hdr.width, hdr.height), (64, 32));
979    }
980}