1pub 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#[derive(Clone, Debug, PartialEq)]
42pub struct VirtualCameraSettings {
43 pub socket_path: String,
45 pub width: u32,
47 pub height: u32,
48 pub fps_num: u32,
50 pub fps_den: u32,
51 pub pixel_format: String,
57 pub slots: u32,
59 pub queue_depth: u32,
61 pub device_path: String,
63 pub pipewire: bool,
65 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
87pub 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#[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 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
139struct 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 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
192struct 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
227pub 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
254const MJPEG_REENCODE_QUALITY: i32 = 85;
257
258struct 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
302fn 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
322fn 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
359fn 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 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 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 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 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 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 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 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 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 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 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#[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 pub input_codec: &'static str,
850 pub running: Option<CameraRunning>,
851}
852
853#[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 #[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}