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 pyo3::buffer::PyBuffer;
32use pyo3::exceptions::{PyRuntimeError, PyValueError};
33use pyo3::prelude::*;
34use pyo3::types::PyDict;
35
36use convert::{DeviceFormat, Normalizer, Orientation, orient_i420};
37use decode::{Codec, DecodeError, Decoder, new_decoder, sniff_keyframe};
38use pipewire::PipeWireSink;
39use ring::{Ring, RingFormat};
40use server::Server;
41use v4l2out::V4l2Output;
42
43#[pyclass(dict)]
45pub struct VirtualCameraSettings {
46 #[pyo3(get, set)]
48 pub socket_path: String,
49 #[pyo3(get, set)]
51 pub width: u32,
52 #[pyo3(get, set)]
53 pub height: u32,
54 #[pyo3(get, set)]
56 pub fps_num: u32,
57 #[pyo3(get, set)]
58 pub fps_den: u32,
59 #[pyo3(get, set)]
61 pub pixel_format: String,
66 #[pyo3(get, set)]
68 pub slots: u32,
69 #[pyo3(get, set)]
71 pub queue_depth: u32,
72 #[pyo3(get, set)]
74 pub device_path: String,
75 #[pyo3(get, set)]
77 pub pipewire: bool,
78 #[pyo3(get, set)]
80 pub pipewire_node_name: String,
81}
82
83#[pymethods]
84impl VirtualCameraSettings {
85 #[new]
86 fn new() -> Self {
87 VirtualCameraSettings {
88 socket_path: format!("{}/selkies_webcam0.sock", crate::webcam::socket_dir()),
89 width: 1280,
90 height: 720,
91 fps_num: 30,
92 fps_den: 1,
93 pixel_format: "I420".into(),
94 slots: 3,
95 queue_depth: 4,
96 device_path: "auto".into(),
97 pipewire: true,
98 pipewire_node_name: "selkies-webcam".into(),
99 }
100 }
101}
102
103pub fn socket_dir() -> String {
107 std::env::var("XDG_RUNTIME_DIR")
108 .ok()
109 .filter(|d| !d.is_empty())
110 .unwrap_or_else(|| "/tmp".into())
111}
112
113fn parse_pixel_format(name: &str) -> Option<u32> {
114 match name.trim().to_ascii_uppercase().as_str() {
115 "I420" | "YU12" | "YUV420" => Some(ring::V4L2_PIX_FMT_YUV420),
116 "NV12" => Some(ring::V4L2_PIX_FMT_NV12),
117 "YUYV" | "YUY2" => Some(ring::V4L2_PIX_FMT_YUYV),
118 "MJPEG" | "MJPG" | "JPEG" => Some(ring::V4L2_PIX_FMT_MJPEG),
119 _ => None,
120 }
121}
122
123#[derive(Default)]
125struct Stats {
126 pushed: AtomicU64,
127 decoded: AtomicU64,
128 published: AtomicU64,
129 passthrough: AtomicU64,
130 dropped: AtomicU64,
131 skipped: AtomicU64,
132 errors: AtomicU64,
133 input_width: AtomicU32,
134 input_height: AtomicU32,
135 input_codec: AtomicU32,
136 input_rotation: AtomicU32,
137 pipewire_streaming: AtomicBool,
141}
142
143struct Job {
144 codec: Codec,
145 keyframe: bool,
146 orientation: Orientation,
147 data: Vec<u8>,
148}
149
150struct QueueState {
151 jobs: VecDeque<Job>,
152 closed: bool,
153}
154
155struct Queue {
158 state: Mutex<QueueState>,
159 cv: Condvar,
160 capacity: usize,
161}
162
163impl Queue {
164 fn new(capacity: usize) -> Self {
165 Queue {
166 state: Mutex::new(QueueState {
167 jobs: VecDeque::new(),
168 closed: false,
169 }),
170 cv: Condvar::new(),
171 capacity: capacity.max(1),
172 }
173 }
174
175 fn push(&self, job: Job) -> Option<Job> {
177 let mut st = self.state.lock().unwrap_or_else(|e| e.into_inner());
178 let evicted = if st.jobs.len() >= self.capacity {
179 st.jobs.pop_front()
180 } else {
181 None
182 };
183 st.jobs.push_back(job);
184 drop(st);
185 self.cv.notify_one();
186 evicted
187 }
188
189 fn pop(&self) -> Option<Job> {
190 let mut st = self.state.lock().unwrap_or_else(|e| e.into_inner());
191 loop {
192 if let Some(j) = st.jobs.pop_front() {
193 return Some(j);
194 }
195 if st.closed {
196 return None;
197 }
198 st = self.cv.wait(st).unwrap_or_else(|e| e.into_inner());
199 }
200 }
201
202 fn close(&self) {
203 self.state.lock().unwrap_or_else(|e| e.into_inner()).closed = true;
204 self.cv.notify_all();
205 }
206}
207
208struct Pool(Mutex<Vec<Vec<u8>>>);
210
211impl Pool {
212 fn take(&self, len: usize) -> Vec<u8> {
213 let mut v = self
214 .0
215 .lock()
216 .unwrap_or_else(|e| e.into_inner())
217 .pop()
218 .unwrap_or_default();
219 v.clear();
220 v.reserve(len);
221 v
222 }
223
224 fn put(&self, v: Vec<u8>) {
225 let mut pool = self.0.lock().unwrap_or_else(|e| e.into_inner());
226 if pool.len() < 8 {
227 pool.push(v);
228 }
229 }
230}
231
232struct Running {
233 queue: Arc<Queue>,
234 thread: Option<JoinHandle<()>>,
235 server: Arc<Server>,
236 keyframe_wanted: Arc<AtomicBool>,
237 pool: Arc<Pool>,
238 device_path: Arc<Mutex<String>>,
239 format: RingFormat,
240 pipewire: bool,
241}
242
243#[pyclass]
245pub struct VirtualCamera {
246 running: Mutex<Option<Running>>,
247 stats: Arc<Stats>,
248}
249
250fn monotonic_ns() -> u64 {
251 let mut ts = libc::timespec {
252 tv_sec: 0,
253 tv_nsec: 0,
254 };
255 unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) };
256 ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64
257}
258
259struct WorkerConfig {
260 queue: Arc<Queue>,
261 ring: Ring,
262 server: Arc<Server>,
263 stats: Arc<Stats>,
264 keyframe_wanted: Arc<AtomicBool>,
265 pool: Arc<Pool>,
266 v4l2out: Option<V4l2Output>,
267 device_path: Arc<Mutex<String>>,
268 pipewire: Option<PipeWireSink>,
269}
270
271const MJPEG_REENCODE_QUALITY: i32 = 85;
274
275struct MjpegEncoder {
278 comp: turbojpeg::Compressor,
279 i420: Vec<u8>,
280}
281
282impl MjpegEncoder {
283 fn new() -> Result<Self, String> {
284 let mut comp =
285 turbojpeg::Compressor::new().map_err(|e| format!("turbojpeg compressor: {}", e))?;
286 comp.set_quality(MJPEG_REENCODE_QUALITY)
287 .map_err(|e| format!("turbojpeg quality: {}", e))?;
288 comp.set_subsamp(turbojpeg::Subsamp::Sub2x2)
289 .map_err(|e| format!("turbojpeg subsampling: {}", e))?;
290 Ok(MjpegEncoder {
291 comp,
292 i420: Vec::new(),
293 })
294 }
295
296 fn write_frame(
297 &mut self,
298 normalizer: &mut Normalizer,
299 src: &convert::I420View<'_>,
300 dev: &DeviceFormat,
301 out: &mut [u8],
302 ) -> usize {
303 let need = dev.frame_bytes();
304 self.i420.resize(need, 0);
305 if normalizer.write_frame(src, dev, &mut self.i420) == 0 {
306 return 0;
307 }
308 let img = turbojpeg::YuvImage {
309 pixels: &self.i420[..],
310 width: dev.width,
311 align: 1,
312 height: dev.height,
313 subsamp: turbojpeg::Subsamp::Sub2x2,
314 };
315 self.comp.compress_yuv_to_slice(img, out).unwrap_or(0)
316 }
317}
318
319fn fan_out(
322 ring: &Ring,
323 server: &Server,
324 v4l2out: &mut Option<V4l2Output>,
325 device_path: &Mutex<String>,
326 pipewire: &mut Option<PipeWireSink>,
327 ts: u64,
328 stats: &Stats,
329) {
330 server.ring_doorbell();
331 if let Some(frame) = ring.latest_frame() {
332 if let Some(out) = v4l2out.as_mut() {
333 out.write_frame(frame);
334 if out.is_failed() {
335 device_path
336 .lock()
337 .unwrap_or_else(|e| e.into_inner())
338 .clear();
339 *v4l2out = None;
340 }
341 }
342 if let Some(pw) = pipewire.as_mut() {
343 pw.publish(frame, ts);
344 stats
345 .pipewire_streaming
346 .store(pw.is_streaming(), Ordering::Relaxed);
347 if pw.is_failed() {
348 eprintln!("[webcam] PipeWire node stopped; PipeWire sink disabled");
349 *pipewire = None;
350 stats.pipewire_streaming.store(false, Ordering::Relaxed);
351 }
352 }
353 }
354}
355
356fn worker(cfg: WorkerConfig) {
360 let WorkerConfig {
361 queue,
362 mut ring,
363 server,
364 stats,
365 keyframe_wanted,
366 pool,
367 mut v4l2out,
368 device_path,
369 mut pipewire,
370 } = cfg;
371 let fmt = *ring.format();
372 let dev = DeviceFormat {
373 width: fmt.width as usize,
374 height: fmt.height as usize,
375 fourcc: fmt.fourcc,
376 };
377 let mut normalizer = Normalizer::new();
378 let mut oriented = convert::I420Buffer::new(2, 2);
379 let mut decoder: Option<Box<dyn Decoder>> = None;
380 let mut need_keyframe = true;
381 let mut last_error_log = Instant::now() - Duration::from_secs(60);
382 let log_error = |msg: String, last: &mut Instant| {
383 if last.elapsed() >= Duration::from_secs(5) {
384 eprintln!("[webcam] {}", msg);
385 *last = Instant::now();
386 }
387 };
388 let mjpeg_device = dev.fourcc == ring::V4L2_PIX_FMT_MJPEG;
389 let mut jpeg_out = if mjpeg_device {
390 match MjpegEncoder::new() {
391 Ok(enc) => Some(enc),
392 Err(e) => {
393 eprintln!(
394 "[webcam] {}; only MJPEG frames of the device size can be served",
395 e
396 );
397 None
398 }
399 }
400 } else {
401 None
402 };
403
404 while let Some(job) = queue.pop() {
405 if mjpeg_device && job.codec == Codec::Jpeg && job.orientation.is_upright() {
406 if let Ok(hdr) = turbojpeg::read_header(&job.data) {
409 stats.input_width.store(hdr.width as u32, Ordering::Relaxed);
410 stats
411 .input_height
412 .store(hdr.height as u32, Ordering::Relaxed);
413 let n = job.data.len();
414 if hdr.width == dev.width && hdr.height == dev.height && n <= fmt.sizeimage as usize
415 {
416 let ts = monotonic_ns();
417 if ring.publish(ts, |slot| {
418 slot[..n].copy_from_slice(&job.data);
419 n
420 }) {
421 stats.passthrough.fetch_add(1, Ordering::Relaxed);
422 stats.published.fetch_add(1, Ordering::Relaxed);
423 fan_out(
424 &ring,
425 &server,
426 &mut v4l2out,
427 &device_path,
428 &mut pipewire,
429 ts,
430 &stats,
431 );
432 }
433 pool.put(job.data);
434 continue;
435 }
436 }
437 }
438 if decoder.as_ref().map(|d| d.codec()) != Some(job.codec) {
439 match new_decoder(job.codec) {
440 Ok(d) => decoder = Some(d),
441 Err(e) => {
442 stats.errors.fetch_add(1, Ordering::Relaxed);
443 log_error(
444 format!("no decoder for {}: {}", job.codec.name(), e),
445 &mut last_error_log,
446 );
447 pool.put(job.data);
448 continue;
449 }
450 }
451 need_keyframe = true;
452 }
453 let dec = decoder.as_mut().expect("decoder present");
454 let keyframe = job.keyframe || sniff_keyframe(job.codec, &job.data).unwrap_or(false);
455 if need_keyframe && job.codec.is_inter_coded() && !keyframe {
456 stats.skipped.fetch_add(1, Ordering::Relaxed);
457 keyframe_wanted.store(true, Ordering::Relaxed);
458 pool.put(job.data);
459 continue;
460 }
461 match dec.decode(&job.data) {
462 Ok(true) => {
463 need_keyframe = false;
464 stats.decoded.fetch_add(1, Ordering::Relaxed);
465 if let Some(view) = dec.frame() {
466 stats
467 .input_width
468 .store(view.width as u32, Ordering::Relaxed);
469 stats
470 .input_height
471 .store(view.height as u32, Ordering::Relaxed);
472 let view = if job.orientation.is_upright() {
473 view
474 } else {
475 orient_i420(&view, job.orientation, &mut oriented);
476 oriented.view(view.full_range)
477 };
478 let ts = monotonic_ns();
479 let published = ring.publish(ts, |slot| match jpeg_out.as_mut() {
480 Some(enc) => enc.write_frame(&mut normalizer, &view, &dev, slot),
481 None if mjpeg_device => 0,
482 None => normalizer.write_frame(&view, &dev, slot),
483 });
484 if published {
485 stats.published.fetch_add(1, Ordering::Relaxed);
486 fan_out(
487 &ring,
488 &server,
489 &mut v4l2out,
490 &device_path,
491 &mut pipewire,
492 ts,
493 &stats,
494 );
495 }
496 }
497 }
498 Ok(false) => {}
499 Err(DecodeError::Corrupt(e)) => {
500 stats.errors.fetch_add(1, Ordering::Relaxed);
501 if job.codec.is_inter_coded() {
502 need_keyframe = true;
503 keyframe_wanted.store(true, Ordering::Relaxed);
504 }
505 log_error(
506 format!("{} decode error: {}", job.codec.name(), e),
507 &mut last_error_log,
508 );
509 }
510 Err(DecodeError::Fatal(e)) => {
511 stats.errors.fetch_add(1, Ordering::Relaxed);
512 log_error(
513 format!("{} decoder reset: {}", job.codec.name(), e),
514 &mut last_error_log,
515 );
516 decoder = None;
517 need_keyframe = true;
518 keyframe_wanted.store(true, Ordering::Relaxed);
519 }
520 }
521 pool.put(job.data);
522 }
523}
524
525#[pymethods]
526impl VirtualCamera {
527 #[classattr]
528 const CODEC_MJPEG: u32 = Codec::Jpeg.id();
529 #[classattr]
530 const CODEC_H264: u32 = Codec::H264.id();
531 #[classattr]
532 const CODEC_VP8: u32 = Codec::Vp8.id();
533 #[classattr]
534 const CODEC_VP9: u32 = Codec::Vp9.id();
535 #[classattr]
536 const CODEC_AV1: u32 = Codec::Av1.id();
537 #[classattr]
538 const CODEC_HEVC: u32 = Codec::H265.id();
539 #[classattr]
541 const KEYFRAME_WANTED: u32 = 1;
542
543 #[new]
544 fn new() -> Self {
545 VirtualCamera {
546 running: Mutex::new(None),
547 stats: Arc::new(Stats::default()),
548 }
549 }
550
551 fn start(&self, py: Python<'_>, settings: &VirtualCameraSettings) -> PyResult<()> {
554 let fourcc = parse_pixel_format(&settings.pixel_format).ok_or_else(|| {
555 PyValueError::new_err(format!(
556 "unsupported pixel_format '{}'",
557 settings.pixel_format
558 ))
559 })?;
560 if settings.width < 2
561 || settings.height < 2
562 || settings.width > 8192
563 || settings.height > 8192
564 {
565 return Err(PyValueError::new_err("width/height out of range"));
566 }
567 if settings.fps_num == 0 || settings.fps_den == 0 {
568 return Err(PyValueError::new_err("fps_num/fps_den must be positive"));
569 }
570 let fmt = RingFormat::for_fourcc(
571 fourcc,
572 settings.width & !1,
573 settings.height & !1,
574 settings.fps_num,
575 settings.fps_den,
576 )
577 .ok_or_else(|| PyValueError::new_err("unsupported pixel_format"))?;
578 let socket_path = settings.socket_path.clone();
579 let slots = settings.slots;
580 let queue_depth = settings.queue_depth.max(1) as usize;
581 let device_setting = settings.device_path.trim().to_string();
582 let want_pipewire = settings.pipewire;
583 let pipewire_node_name = settings.pipewire_node_name.trim().to_string();
584 let stats = self.stats.clone();
585
586 self.stop(py);
587
588 let started = py.detach(move || -> Result<Running, String> {
589 let ring =
590 Ring::new(fmt, slots).map_err(|e| format!("ring allocation failed: {}", e))?;
591 let server = Arc::new(
592 Server::bind(&socket_path, ring.config_bytes(), ring.fd())
593 .map_err(|e| format!("bind({}) failed: {}", socket_path, e))?,
594 );
595 let v4l2out = match device_setting.as_str() {
596 "" | "false" | "no" | "off" | "none" => None,
597 "auto" | "true" | "yes" | "on" => {
598 V4l2Output::find_loopback_device().and_then(|p| {
599 match V4l2Output::open(&p, &fmt) {
600 Ok(o) => Some(o),
601 Err(e) => {
602 eprintln!("[webcam] {}; kernel device sink disabled", e);
603 None
604 }
605 }
606 })
607 }
608 path => match V4l2Output::open(path, &fmt) {
609 Ok(o) => Some(o),
610 Err(e) => {
611 eprintln!("[webcam] {}; kernel device sink disabled", e);
612 None
613 }
614 },
615 };
616 let device_path = Arc::new(Mutex::new(
617 v4l2out
618 .as_ref()
619 .map(|o| o.path().to_string())
620 .unwrap_or_default(),
621 ));
622 let pipewire = if want_pipewire {
623 match PipeWireSink::connect(&pipewire_node_name, "Selkies Virtual Camera", &fmt) {
624 Ok(sink) => Some(sink),
625 Err(e) => {
626 eprintln!("[webcam] PipeWire sink unavailable: {}", e);
627 None
628 }
629 }
630 } else {
631 None
632 };
633 let pipewire_on = pipewire.is_some();
634 let queue = Arc::new(Queue::new(queue_depth));
635 let keyframe_wanted = Arc::new(AtomicBool::new(true));
636 let pool = Arc::new(Pool(Mutex::new(Vec::new())));
637 let cfg = WorkerConfig {
638 queue: queue.clone(),
639 ring,
640 server: server.clone(),
641 stats,
642 keyframe_wanted: keyframe_wanted.clone(),
643 pool: pool.clone(),
644 v4l2out,
645 device_path: device_path.clone(),
646 pipewire,
647 };
648 let thread = thread::Builder::new()
649 .name("pixelflux-webcam".into())
650 .spawn(move || worker(cfg))
651 .map_err(|e| format!("decoder thread spawn failed: {}", e))?;
652 Ok(Running {
653 queue,
654 thread: Some(thread),
655 server,
656 keyframe_wanted,
657 pool,
658 device_path,
659 format: fmt,
660 pipewire: pipewire_on,
661 })
662 });
663 match started {
664 Ok(r) => {
665 *self.running.lock().unwrap_or_else(|e| e.into_inner()) = Some(r);
666 Ok(())
667 }
668 Err(e) => Err(PyRuntimeError::new_err(e)),
669 }
670 }
671
672 #[allow(clippy::too_many_arguments)]
680 #[pyo3(signature = (data, codec, keyframe = false, offset = 0, rotation = 0, flip = false))]
681 #[allow(clippy::too_many_arguments)]
682 fn push(
683 &self,
684 py: Python<'_>,
685 data: PyBuffer<u8>,
686 codec: u32,
687 keyframe: bool,
688 offset: usize,
689 rotation: u32,
690 flip: bool,
691 ) -> PyResult<u32> {
692 let codec = Codec::from_id(codec)
693 .ok_or_else(|| PyValueError::new_err(format!("unknown codec id {}", codec)))?;
694 if !rotation.is_multiple_of(90) || rotation >= 360 {
695 return Err(PyValueError::new_err("rotation must be 0, 90, 180, or 270"));
696 }
697 let orientation = Orientation {
698 quarter_turns: (rotation / 90) as u8,
699 hflip: flip,
700 };
701 if !data.is_c_contiguous() {
702 return Err(PyValueError::new_err("frame buffer must be contiguous"));
703 }
704 let guard = self.running.lock().unwrap_or_else(|e| e.into_inner());
705 let running = guard
706 .as_ref()
707 .ok_or_else(|| PyRuntimeError::new_err("virtual camera is not running"))?;
708 let total = data.len_bytes();
709 if offset >= total {
710 return Ok(running.keyframe_wanted.swap(false, Ordering::Relaxed) as u32);
711 }
712 let bytes = unsafe { std::slice::from_raw_parts(data.buf_ptr() as *const u8, total) };
713 let mut buf = running.pool.take(total - offset);
714 buf.extend_from_slice(&bytes[offset..]);
715 let _ = py;
716 self.stats.pushed.fetch_add(1, Ordering::Relaxed);
717 self.stats
718 .input_codec
719 .store(codec as u32, Ordering::Relaxed);
720 self.stats.input_rotation.store(rotation, Ordering::Relaxed);
721 if let Some(evicted) = running.queue.push(Job {
722 codec,
723 keyframe,
724 orientation,
725 data: buf,
726 }) {
727 self.stats.dropped.fetch_add(1, Ordering::Relaxed);
728 if evicted.codec.is_inter_coded() {
729 running.keyframe_wanted.store(true, Ordering::Relaxed);
730 }
731 running.pool.put(evicted.data);
732 }
733 Ok(running.keyframe_wanted.swap(false, Ordering::Relaxed) as u32)
734 }
735
736 fn stop(&self, py: Python<'_>) {
738 let running = self
739 .running
740 .lock()
741 .unwrap_or_else(|e| e.into_inner())
742 .take();
743 if let Some(mut r) = running {
744 py.detach(move || {
745 r.queue.close();
746 if let Some(t) = r.thread.take() {
747 let _ = t.join();
748 }
749 drop(r.server);
750 });
751 }
752 }
753
754 #[getter]
755 fn is_running(&self) -> bool {
756 self.running
757 .lock()
758 .unwrap_or_else(|e| e.into_inner())
759 .is_some()
760 }
761
762 #[getter]
764 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 #[getter]
774 fn socket_path(&self) -> Option<String> {
775 self.running
776 .lock()
777 .unwrap_or_else(|e| e.into_inner())
778 .as_ref()
779 .map(|r| r.server.path().to_string())
780 }
781
782 #[getter]
784 fn device_path(&self) -> String {
785 self.running
786 .lock()
787 .unwrap_or_else(|e| e.into_inner())
788 .as_ref()
789 .map(|r| {
790 r.device_path
791 .lock()
792 .unwrap_or_else(|e| e.into_inner())
793 .clone()
794 })
795 .unwrap_or_default()
796 }
797
798 fn stats<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
800 let d = PyDict::new(py);
801 let s = &self.stats;
802 d.set_item("pushed", s.pushed.load(Ordering::Relaxed))?;
803 d.set_item("decoded", s.decoded.load(Ordering::Relaxed))?;
804 d.set_item("published", s.published.load(Ordering::Relaxed))?;
805 d.set_item("passthrough", s.passthrough.load(Ordering::Relaxed))?;
806 d.set_item("dropped", s.dropped.load(Ordering::Relaxed))?;
807 d.set_item("skipped", s.skipped.load(Ordering::Relaxed))?;
808 d.set_item("errors", s.errors.load(Ordering::Relaxed))?;
809 d.set_item("input_width", s.input_width.load(Ordering::Relaxed))?;
810 d.set_item("input_height", s.input_height.load(Ordering::Relaxed))?;
811 d.set_item("input_rotation", s.input_rotation.load(Ordering::Relaxed))?;
812 let codec = s.input_codec.load(Ordering::Relaxed);
813 d.set_item(
814 "input_codec",
815 Codec::from_id(codec).map(|c| c.name()).unwrap_or(""),
816 )?;
817 let guard = self.running.lock().unwrap_or_else(|e| e.into_inner());
818 if let Some(r) = guard.as_ref() {
819 d.set_item("clients", r.server.client_count())?;
820 d.set_item("width", r.format.width)?;
821 d.set_item("height", r.format.height)?;
822 d.set_item("fps_num", r.format.fps_num)?;
823 d.set_item("fps_den", r.format.fps_den)?;
824 d.set_item("pixel_format", v4l2out::fourcc_str(r.format.fourcc))?;
825 d.set_item("socket_path", r.server.path())?;
826 d.set_item(
827 "device_path",
828 r.device_path
829 .lock()
830 .unwrap_or_else(|e| e.into_inner())
831 .clone(),
832 )?;
833 d.set_item("pipewire", r.pipewire)?;
834 d.set_item(
835 "pipewire_streaming",
836 s.pipewire_streaming.load(Ordering::Relaxed),
837 )?;
838 } else {
839 d.set_item("clients", 0)?;
840 }
841 Ok(d)
842 }
843
844 #[staticmethod]
847 fn shm_layout<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
848 let d = PyDict::new(py);
849 d.set_item("magic", ring::SHM_MAGIC)?;
850 d.set_item("version", ring::SHM_VERSION)?;
851 d.set_item("ctrl_offset", ring::CTRL_OFFSET)?;
852 d.set_item("ctrl_stride", ring::CTRL_STRIDE)?;
853 d.set_item("data_offset", ring::DATA_OFFSET)?;
854 d.set_item("max_slots", ring::MAX_SLOTS)?;
855 d.set_item("config_size", ring::CONFIG_SIZE)?;
856 d.set_item(
857 "config_fields",
858 [
859 "magic",
860 "version",
861 "width",
862 "height",
863 "fourcc",
864 "fps_num",
865 "fps_den",
866 "n_slots",
867 "slot_size",
868 "data_offset",
869 "ctrl_offset",
870 "ctrl_stride",
871 "bytesperline",
872 "sizeimage",
873 ],
874 )?;
875 d.set_item(
876 "header_fields",
877 [
878 "magic",
879 "version",
880 "width",
881 "height",
882 "fourcc",
883 "fps_num",
884 "fps_den",
885 "n_slots",
886 "slot_size",
887 "data_offset",
888 "bytesperline",
889 "sizeimage",
890 "latest_slot",
891 "_pad",
892 ],
893 )?;
894 d.set_item("header_latest_frame_seq_offset", 56)?;
895 d.set_item(
896 "ctrl_fields",
897 [
898 ("seq", 0, 4),
899 ("bytesused", 4, 4),
900 ("frame_seq", 8, 8),
901 ("ts_ns", 16, 8),
902 ],
903 )?;
904 Ok(d)
905 }
906}
907
908impl Drop for VirtualCamera {
909 fn drop(&mut self) {
910 if let Some(mut r) = self
911 .running
912 .lock()
913 .unwrap_or_else(|e| e.into_inner())
914 .take()
915 {
916 r.queue.close();
917 if let Some(t) = r.thread.take() {
918 let _ = t.join();
919 }
920 }
921 }
922}
923
924#[cfg(test)]
925mod tests {
926 use super::*;
927
928 #[test]
929 fn queue_drops_oldest() {
930 let q = Queue::new(2);
931 assert!(
932 q.push(Job {
933 codec: Codec::H264,
934 keyframe: true,
935 orientation: Orientation::UPRIGHT,
936 data: vec![1]
937 })
938 .is_none()
939 );
940 assert!(
941 q.push(Job {
942 codec: Codec::H264,
943 keyframe: false,
944 orientation: Orientation::UPRIGHT,
945 data: vec![2]
946 })
947 .is_none()
948 );
949 let evicted = q
950 .push(Job {
951 codec: Codec::H264,
952 keyframe: false,
953 orientation: Orientation::UPRIGHT,
954 data: vec![3],
955 })
956 .unwrap();
957 assert_eq!(evicted.data, vec![1]);
958 assert_eq!(q.pop().unwrap().data, vec![2]);
959 assert_eq!(q.pop().unwrap().data, vec![3]);
960 q.close();
961 assert!(q.pop().is_none());
962 }
963
964 #[test]
965 fn pixel_format_names() {
966 assert_eq!(parse_pixel_format("i420"), Some(ring::V4L2_PIX_FMT_YUV420));
967 assert_eq!(parse_pixel_format("NV12"), Some(ring::V4L2_PIX_FMT_NV12));
968 assert_eq!(parse_pixel_format(" yuyv "), Some(ring::V4L2_PIX_FMT_YUYV));
969 assert_eq!(parse_pixel_format("MJPG"), Some(ring::V4L2_PIX_FMT_MJPEG));
970 assert_eq!(parse_pixel_format("mjpeg"), Some(ring::V4L2_PIX_FMT_MJPEG));
971 assert_eq!(parse_pixel_format("rgb"), None);
972 let mjpeg = RingFormat::for_fourcc(ring::V4L2_PIX_FMT_MJPEG, 640, 480, 30, 1).unwrap();
973 assert_eq!((mjpeg.bytesperline, mjpeg.sizeimage), (0, 640 * 480 * 2));
974 }
975
976 #[test]
977 fn mjpeg_encoder_writes_a_jpeg_of_the_device_size() {
978 let dev = DeviceFormat {
979 width: 64,
980 height: 32,
981 fourcc: ring::V4L2_PIX_FMT_MJPEG,
982 };
983 let mut src = convert::I420Buffer::new(32, 32);
984 src.data.fill(200);
985 let mut enc = MjpegEncoder::new().unwrap();
986 let mut normalizer = Normalizer::new();
987 let mut slot = vec![0u8; 64 * 32 * 2];
988 let n = enc.write_frame(&mut normalizer, &src.view(true), &dev, &mut slot);
989 assert!(n > 0 && n <= slot.len());
990 let hdr = turbojpeg::read_header(&slot[..n]).unwrap();
991 assert_eq!((hdr.width, hdr.height), (64, 32));
992 }
993}