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::{orient_i420, DeviceFormat, Normalizer, Orientation};
37use decode::{new_decoder, sniff_keyframe, Codec, DecodeError, Decoder};
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: "/tmp/selkies_webcam0.sock".into(),
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
103fn parse_pixel_format(name: &str) -> Option<u32> {
104 match name.trim().to_ascii_uppercase().as_str() {
105 "I420" | "YU12" | "YUV420" => Some(ring::V4L2_PIX_FMT_YUV420),
106 "NV12" => Some(ring::V4L2_PIX_FMT_NV12),
107 "YUYV" | "YUY2" => Some(ring::V4L2_PIX_FMT_YUYV),
108 "MJPEG" | "MJPG" | "JPEG" => Some(ring::V4L2_PIX_FMT_MJPEG),
109 _ => None,
110 }
111}
112
113#[derive(Default)]
115struct Stats {
116 pushed: AtomicU64,
117 decoded: AtomicU64,
118 published: AtomicU64,
119 passthrough: AtomicU64,
120 dropped: AtomicU64,
121 skipped: AtomicU64,
122 errors: AtomicU64,
123 input_width: AtomicU32,
124 input_height: AtomicU32,
125 input_codec: AtomicU32,
126 input_rotation: AtomicU32,
127 pipewire_streaming: AtomicBool,
131}
132
133struct Job {
134 codec: Codec,
135 keyframe: bool,
136 orientation: Orientation,
137 data: Vec<u8>,
138}
139
140struct QueueState {
141 jobs: VecDeque<Job>,
142 closed: bool,
143}
144
145struct Queue {
148 state: Mutex<QueueState>,
149 cv: Condvar,
150 capacity: usize,
151}
152
153impl Queue {
154 fn new(capacity: usize) -> Self {
155 Queue { state: Mutex::new(QueueState { jobs: VecDeque::new(), closed: false }), cv: Condvar::new(), capacity: capacity.max(1) }
156 }
157
158 fn push(&self, job: Job) -> Option<Job> {
160 let mut st = self.state.lock().unwrap_or_else(|e| e.into_inner());
161 let evicted = if st.jobs.len() >= self.capacity { st.jobs.pop_front() } else { None };
162 st.jobs.push_back(job);
163 drop(st);
164 self.cv.notify_one();
165 evicted
166 }
167
168 fn pop(&self) -> Option<Job> {
169 let mut st = self.state.lock().unwrap_or_else(|e| e.into_inner());
170 loop {
171 if let Some(j) = st.jobs.pop_front() {
172 return Some(j);
173 }
174 if st.closed {
175 return None;
176 }
177 st = self.cv.wait(st).unwrap_or_else(|e| e.into_inner());
178 }
179 }
180
181 fn close(&self) {
182 self.state.lock().unwrap_or_else(|e| e.into_inner()).closed = true;
183 self.cv.notify_all();
184 }
185}
186
187struct Pool(Mutex<Vec<Vec<u8>>>);
189
190impl Pool {
191 fn take(&self, len: usize) -> Vec<u8> {
192 let mut v = self.0.lock().unwrap_or_else(|e| e.into_inner()).pop().unwrap_or_default();
193 v.clear();
194 v.reserve(len);
195 v
196 }
197
198 fn put(&self, v: Vec<u8>) {
199 let mut pool = self.0.lock().unwrap_or_else(|e| e.into_inner());
200 if pool.len() < 8 {
201 pool.push(v);
202 }
203 }
204}
205
206struct Running {
207 queue: Arc<Queue>,
208 thread: Option<JoinHandle<()>>,
209 server: Arc<Server>,
210 keyframe_wanted: Arc<AtomicBool>,
211 pool: Arc<Pool>,
212 device_path: Arc<Mutex<String>>,
213 format: RingFormat,
214 pipewire: bool,
215}
216
217#[pyclass]
219pub struct VirtualCamera {
220 running: Mutex<Option<Running>>,
221 stats: Arc<Stats>,
222}
223
224fn monotonic_ns() -> u64 {
225 let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 };
226 unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) };
227 ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64
228}
229
230struct WorkerConfig {
231 queue: Arc<Queue>,
232 ring: Ring,
233 server: Arc<Server>,
234 stats: Arc<Stats>,
235 keyframe_wanted: Arc<AtomicBool>,
236 pool: Arc<Pool>,
237 v4l2out: Option<V4l2Output>,
238 device_path: Arc<Mutex<String>>,
239 pipewire: Option<PipeWireSink>,
240}
241
242const MJPEG_REENCODE_QUALITY: i32 = 85;
245
246struct MjpegEncoder {
249 comp: turbojpeg::Compressor,
250 i420: Vec<u8>,
251}
252
253impl MjpegEncoder {
254 fn new() -> Result<Self, String> {
255 let mut comp = turbojpeg::Compressor::new().map_err(|e| format!("turbojpeg compressor: {}", e))?;
256 comp.set_quality(MJPEG_REENCODE_QUALITY).map_err(|e| format!("turbojpeg quality: {}", e))?;
257 comp.set_subsamp(turbojpeg::Subsamp::Sub2x2).map_err(|e| format!("turbojpeg subsampling: {}", e))?;
258 Ok(MjpegEncoder { comp, i420: Vec::new() })
259 }
260
261 fn write_frame(&mut self, normalizer: &mut Normalizer, src: &convert::I420View<'_>, dev: &DeviceFormat, out: &mut [u8]) -> usize {
262 let need = dev.frame_bytes();
263 self.i420.resize(need, 0);
264 if normalizer.write_frame(src, dev, &mut self.i420) == 0 {
265 return 0;
266 }
267 let img = turbojpeg::YuvImage { pixels: &self.i420[..], width: dev.width, align: 1, height: dev.height, subsamp: turbojpeg::Subsamp::Sub2x2 };
268 self.comp.compress_yuv_to_slice(img, out).unwrap_or(0)
269 }
270}
271
272fn fan_out(ring: &Ring, server: &Server, v4l2out: &mut Option<V4l2Output>, device_path: &Mutex<String>, pipewire: &mut Option<PipeWireSink>, ts: u64, stats: &Stats) {
275 server.ring_doorbell();
276 if let Some(frame) = ring.latest_frame() {
277 if let Some(out) = v4l2out.as_mut() {
278 out.write_frame(frame);
279 if out.is_failed() {
280 device_path.lock().unwrap_or_else(|e| e.into_inner()).clear();
281 *v4l2out = None;
282 }
283 }
284 if let Some(pw) = pipewire.as_mut() {
285 pw.publish(frame, ts);
286 stats.pipewire_streaming.store(pw.is_streaming(), Ordering::Relaxed);
287 if pw.is_failed() {
288 eprintln!("[webcam] PipeWire node stopped; PipeWire sink disabled");
289 *pipewire = None;
290 stats.pipewire_streaming.store(false, Ordering::Relaxed);
291 }
292 }
293 }
294}
295
296fn worker(cfg: WorkerConfig) {
300 let WorkerConfig { queue, mut ring, server, stats, keyframe_wanted, pool, mut v4l2out, device_path, mut pipewire } = cfg;
301 let fmt = *ring.format();
302 let dev = DeviceFormat { width: fmt.width as usize, height: fmt.height as usize, fourcc: fmt.fourcc };
303 let mut normalizer = Normalizer::new();
304 let mut oriented = convert::I420Buffer::new(2, 2);
305 let mut decoder: Option<Box<dyn Decoder>> = None;
306 let mut need_keyframe = true;
307 let mut last_error_log = Instant::now() - Duration::from_secs(60);
308 let log_error = |msg: String, last: &mut Instant| {
309 if last.elapsed() >= Duration::from_secs(5) {
310 eprintln!("[webcam] {}", msg);
311 *last = Instant::now();
312 }
313 };
314 let mjpeg_device = dev.fourcc == ring::V4L2_PIX_FMT_MJPEG;
315 let mut jpeg_out = if mjpeg_device {
316 match MjpegEncoder::new() {
317 Ok(enc) => Some(enc),
318 Err(e) => {
319 eprintln!("[webcam] {}; only MJPEG frames of the device size can be served", e);
320 None
321 }
322 }
323 } else {
324 None
325 };
326
327 while let Some(job) = queue.pop() {
328 if mjpeg_device && job.codec == Codec::Mjpeg && job.orientation.is_upright() {
329 if let Ok(hdr) = turbojpeg::read_header(&job.data) {
332 stats.input_width.store(hdr.width as u32, Ordering::Relaxed);
333 stats.input_height.store(hdr.height as u32, Ordering::Relaxed);
334 let n = job.data.len();
335 if hdr.width == dev.width && hdr.height == dev.height && n <= fmt.sizeimage as usize {
336 let ts = monotonic_ns();
337 if ring.publish(ts, |slot| {
338 slot[..n].copy_from_slice(&job.data);
339 n
340 }) {
341 stats.passthrough.fetch_add(1, Ordering::Relaxed);
342 stats.published.fetch_add(1, Ordering::Relaxed);
343 fan_out(&ring, &server, &mut v4l2out, &device_path, &mut pipewire, ts, &stats);
344 }
345 pool.put(job.data);
346 continue;
347 }
348 }
349 }
350 if decoder.as_ref().map(|d| d.codec()) != Some(job.codec) {
351 match new_decoder(job.codec) {
352 Ok(d) => decoder = Some(d),
353 Err(e) => {
354 stats.errors.fetch_add(1, Ordering::Relaxed);
355 log_error(format!("no decoder for {}: {}", job.codec.name(), e), &mut last_error_log);
356 pool.put(job.data);
357 continue;
358 }
359 }
360 need_keyframe = true;
361 }
362 let dec = decoder.as_mut().expect("decoder present");
363 let keyframe = job.keyframe || sniff_keyframe(job.codec, &job.data).unwrap_or(false);
364 if need_keyframe && job.codec.is_inter_coded() && !keyframe {
365 stats.skipped.fetch_add(1, Ordering::Relaxed);
366 keyframe_wanted.store(true, Ordering::Relaxed);
367 pool.put(job.data);
368 continue;
369 }
370 match dec.decode(&job.data) {
371 Ok(true) => {
372 need_keyframe = false;
373 stats.decoded.fetch_add(1, Ordering::Relaxed);
374 if let Some(view) = dec.frame() {
375 stats.input_width.store(view.width as u32, Ordering::Relaxed);
376 stats.input_height.store(view.height as u32, Ordering::Relaxed);
377 let view = if job.orientation.is_upright() {
378 view
379 } else {
380 orient_i420(&view, job.orientation, &mut oriented);
381 oriented.view(view.full_range)
382 };
383 let ts = monotonic_ns();
384 let published = ring.publish(ts, |slot| match jpeg_out.as_mut() {
385 Some(enc) => enc.write_frame(&mut normalizer, &view, &dev, slot),
386 None if mjpeg_device => 0,
387 None => normalizer.write_frame(&view, &dev, slot),
388 });
389 if published {
390 stats.published.fetch_add(1, Ordering::Relaxed);
391 fan_out(&ring, &server, &mut v4l2out, &device_path, &mut pipewire, ts, &stats);
392 }
393 }
394 }
395 Ok(false) => {}
396 Err(DecodeError::Corrupt(e)) => {
397 stats.errors.fetch_add(1, Ordering::Relaxed);
398 if job.codec.is_inter_coded() {
399 need_keyframe = true;
400 keyframe_wanted.store(true, Ordering::Relaxed);
401 }
402 log_error(format!("{} decode error: {}", job.codec.name(), e), &mut last_error_log);
403 }
404 Err(DecodeError::Fatal(e)) => {
405 stats.errors.fetch_add(1, Ordering::Relaxed);
406 log_error(format!("{} decoder reset: {}", job.codec.name(), e), &mut last_error_log);
407 decoder = None;
408 need_keyframe = true;
409 keyframe_wanted.store(true, Ordering::Relaxed);
410 }
411 }
412 pool.put(job.data);
413 }
414}
415
416#[pymethods]
417impl VirtualCamera {
418 #[classattr]
419 const CODEC_MJPEG: u32 = Codec::Mjpeg as u32;
420 #[classattr]
421 const CODEC_H264: u32 = Codec::H264 as u32;
422 #[classattr]
423 const CODEC_VP8: u32 = Codec::Vp8 as u32;
424 #[classattr]
425 const CODEC_VP9: u32 = Codec::Vp9 as u32;
426 #[classattr]
427 const CODEC_AV1: u32 = Codec::Av1 as u32;
428 #[classattr]
429 const CODEC_HEVC: u32 = Codec::Hevc as u32;
430 #[classattr]
432 const KEYFRAME_WANTED: u32 = 1;
433
434 #[new]
435 fn new() -> Self {
436 VirtualCamera { running: Mutex::new(None), stats: Arc::new(Stats::default()) }
437 }
438
439 fn start(&self, py: Python<'_>, settings: &VirtualCameraSettings) -> PyResult<()> {
442 let fourcc = parse_pixel_format(&settings.pixel_format)
443 .ok_or_else(|| PyValueError::new_err(format!("unsupported pixel_format '{}'", settings.pixel_format)))?;
444 if settings.width < 2 || settings.height < 2 || settings.width > 8192 || settings.height > 8192 {
445 return Err(PyValueError::new_err("width/height out of range"));
446 }
447 if settings.fps_num == 0 || settings.fps_den == 0 {
448 return Err(PyValueError::new_err("fps_num/fps_den must be positive"));
449 }
450 let fmt = RingFormat::for_fourcc(fourcc, settings.width & !1, settings.height & !1, settings.fps_num, settings.fps_den)
451 .ok_or_else(|| PyValueError::new_err("unsupported pixel_format"))?;
452 let socket_path = settings.socket_path.clone();
453 let slots = settings.slots;
454 let queue_depth = settings.queue_depth.max(1) as usize;
455 let device_setting = settings.device_path.trim().to_string();
456 let want_pipewire = settings.pipewire;
457 let pipewire_node_name = settings.pipewire_node_name.trim().to_string();
458 let stats = self.stats.clone();
459
460 self.stop(py);
461
462 let started = py.detach(move || -> Result<Running, String> {
463 let ring = Ring::new(fmt, slots).map_err(|e| format!("ring allocation failed: {}", e))?;
464 let server = Arc::new(Server::bind(&socket_path, ring.config_bytes(), ring.fd())
465 .map_err(|e| format!("bind({}) failed: {}", socket_path, e))?);
466 let v4l2out = match device_setting.as_str() {
467 "" | "false" | "no" | "off" | "none" => None,
468 "auto" | "true" | "yes" | "on" => V4l2Output::find_loopback_device().and_then(|p| match V4l2Output::open(&p, &fmt) {
469 Ok(o) => Some(o),
470 Err(e) => {
471 eprintln!("[webcam] {}; kernel device sink disabled", e);
472 None
473 }
474 }),
475 path => match V4l2Output::open(path, &fmt) {
476 Ok(o) => Some(o),
477 Err(e) => {
478 eprintln!("[webcam] {}; kernel device sink disabled", e);
479 None
480 }
481 },
482 };
483 let device_path = Arc::new(Mutex::new(v4l2out.as_ref().map(|o| o.path().to_string()).unwrap_or_default()));
484 let pipewire = if want_pipewire {
485 match PipeWireSink::connect(&pipewire_node_name, "Selkies Virtual Camera", &fmt) {
486 Ok(sink) => Some(sink),
487 Err(e) => {
488 eprintln!("[webcam] PipeWire sink unavailable: {}", e);
489 None
490 }
491 }
492 } else {
493 None
494 };
495 let pipewire_on = pipewire.is_some();
496 let queue = Arc::new(Queue::new(queue_depth));
497 let keyframe_wanted = Arc::new(AtomicBool::new(true));
498 let pool = Arc::new(Pool(Mutex::new(Vec::new())));
499 let cfg = WorkerConfig {
500 queue: queue.clone(),
501 ring,
502 server: server.clone(),
503 stats,
504 keyframe_wanted: keyframe_wanted.clone(),
505 pool: pool.clone(),
506 v4l2out,
507 device_path: device_path.clone(),
508 pipewire,
509 };
510 let thread = thread::Builder::new()
511 .name("pixelflux-webcam".into())
512 .spawn(move || worker(cfg))
513 .map_err(|e| format!("decoder thread spawn failed: {}", e))?;
514 Ok(Running { queue, thread: Some(thread), server, keyframe_wanted, pool, device_path, format: fmt, pipewire: pipewire_on })
515 });
516 match started {
517 Ok(r) => {
518 *self.running.lock().unwrap_or_else(|e| e.into_inner()) = Some(r);
519 Ok(())
520 }
521 Err(e) => Err(PyRuntimeError::new_err(e)),
522 }
523 }
524
525 #[pyo3(signature = (data, codec, keyframe = false, offset = 0, rotation = 0, flip = false))]
533 fn push(&self, py: Python<'_>, data: PyBuffer<u8>, codec: u32, keyframe: bool, offset: usize, rotation: u32, flip: bool) -> PyResult<u32> {
534 let codec = Codec::from_id(codec).ok_or_else(|| PyValueError::new_err(format!("unknown codec id {}", codec)))?;
535 if rotation % 90 != 0 || rotation >= 360 {
536 return Err(PyValueError::new_err("rotation must be 0, 90, 180 or 270"));
537 }
538 let orientation = Orientation { quarter_turns: (rotation / 90) as u8, hflip: flip };
539 if !data.is_c_contiguous() {
540 return Err(PyValueError::new_err("frame buffer must be contiguous"));
541 }
542 let guard = self.running.lock().unwrap_or_else(|e| e.into_inner());
543 let running = guard.as_ref().ok_or_else(|| PyRuntimeError::new_err("virtual camera is not running"))?;
544 let total = data.len_bytes();
545 if offset >= total {
546 return Ok(running.keyframe_wanted.swap(false, Ordering::Relaxed) as u32);
547 }
548 let bytes = unsafe { std::slice::from_raw_parts(data.buf_ptr() as *const u8, total) };
549 let mut buf = running.pool.take(total - offset);
550 buf.extend_from_slice(&bytes[offset..]);
551 let _ = py;
552 self.stats.pushed.fetch_add(1, Ordering::Relaxed);
553 self.stats.input_codec.store(codec as u32, Ordering::Relaxed);
554 self.stats.input_rotation.store(rotation, Ordering::Relaxed);
555 if let Some(evicted) = running.queue.push(Job { codec, keyframe, orientation, data: buf }) {
556 self.stats.dropped.fetch_add(1, Ordering::Relaxed);
557 if evicted.codec.is_inter_coded() {
558 running.keyframe_wanted.store(true, Ordering::Relaxed);
559 }
560 running.pool.put(evicted.data);
561 }
562 Ok(running.keyframe_wanted.swap(false, Ordering::Relaxed) as u32)
563 }
564
565 fn stop(&self, py: Python<'_>) {
567 let running = self.running.lock().unwrap_or_else(|e| e.into_inner()).take();
568 if let Some(mut r) = running {
569 py.detach(move || {
570 r.queue.close();
571 if let Some(t) = r.thread.take() {
572 let _ = t.join();
573 }
574 drop(r.server);
575 });
576 }
577 }
578
579 #[getter]
580 fn is_running(&self) -> bool {
581 self.running.lock().unwrap_or_else(|e| e.into_inner()).is_some()
582 }
583
584 #[getter]
586 fn clients(&self) -> usize {
587 self.running.lock().unwrap_or_else(|e| e.into_inner()).as_ref().map(|r| r.server.client_count()).unwrap_or(0)
588 }
589
590 #[getter]
591 fn socket_path(&self) -> Option<String> {
592 self.running.lock().unwrap_or_else(|e| e.into_inner()).as_ref().map(|r| r.server.path().to_string())
593 }
594
595 #[getter]
597 fn device_path(&self) -> String {
598 self.running
599 .lock()
600 .unwrap_or_else(|e| e.into_inner())
601 .as_ref()
602 .map(|r| r.device_path.lock().unwrap_or_else(|e| e.into_inner()).clone())
603 .unwrap_or_default()
604 }
605
606 fn stats<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
608 let d = PyDict::new(py);
609 let s = &self.stats;
610 d.set_item("pushed", s.pushed.load(Ordering::Relaxed))?;
611 d.set_item("decoded", s.decoded.load(Ordering::Relaxed))?;
612 d.set_item("published", s.published.load(Ordering::Relaxed))?;
613 d.set_item("passthrough", s.passthrough.load(Ordering::Relaxed))?;
614 d.set_item("dropped", s.dropped.load(Ordering::Relaxed))?;
615 d.set_item("skipped", s.skipped.load(Ordering::Relaxed))?;
616 d.set_item("errors", s.errors.load(Ordering::Relaxed))?;
617 d.set_item("input_width", s.input_width.load(Ordering::Relaxed))?;
618 d.set_item("input_height", s.input_height.load(Ordering::Relaxed))?;
619 d.set_item("input_rotation", s.input_rotation.load(Ordering::Relaxed))?;
620 let codec = s.input_codec.load(Ordering::Relaxed);
621 d.set_item("input_codec", Codec::from_id(codec).map(|c| c.name()).unwrap_or(""))?;
622 let guard = self.running.lock().unwrap_or_else(|e| e.into_inner());
623 if let Some(r) = guard.as_ref() {
624 d.set_item("clients", r.server.client_count())?;
625 d.set_item("width", r.format.width)?;
626 d.set_item("height", r.format.height)?;
627 d.set_item("fps_num", r.format.fps_num)?;
628 d.set_item("fps_den", r.format.fps_den)?;
629 d.set_item("pixel_format", v4l2out::fourcc_str(r.format.fourcc))?;
630 d.set_item("socket_path", r.server.path())?;
631 d.set_item("device_path", r.device_path.lock().unwrap_or_else(|e| e.into_inner()).clone())?;
632 d.set_item("pipewire", r.pipewire)?;
633 d.set_item("pipewire_streaming", s.pipewire_streaming.load(Ordering::Relaxed))?;
634 } else {
635 d.set_item("clients", 0)?;
636 }
637 Ok(d)
638 }
639
640 #[staticmethod]
643 fn shm_layout<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
644 let d = PyDict::new(py);
645 d.set_item("magic", ring::SHM_MAGIC)?;
646 d.set_item("version", ring::SHM_VERSION)?;
647 d.set_item("ctrl_offset", ring::CTRL_OFFSET)?;
648 d.set_item("ctrl_stride", ring::CTRL_STRIDE)?;
649 d.set_item("data_offset", ring::DATA_OFFSET)?;
650 d.set_item("max_slots", ring::MAX_SLOTS)?;
651 d.set_item("config_size", ring::CONFIG_SIZE)?;
652 d.set_item(
653 "config_fields",
654 [
655 "magic", "version", "width", "height", "fourcc", "fps_num", "fps_den", "n_slots", "slot_size",
656 "data_offset", "ctrl_offset", "ctrl_stride", "bytesperline", "sizeimage",
657 ],
658 )?;
659 d.set_item(
660 "header_fields",
661 [
662 "magic", "version", "width", "height", "fourcc", "fps_num", "fps_den", "n_slots", "slot_size",
663 "data_offset", "bytesperline", "sizeimage", "latest_slot", "_pad",
664 ],
665 )?;
666 d.set_item("header_latest_frame_seq_offset", 56)?;
667 d.set_item("ctrl_fields", [("seq", 0, 4), ("bytesused", 4, 4), ("frame_seq", 8, 8), ("ts_ns", 16, 8)])?;
668 Ok(d)
669 }
670}
671
672impl Drop for VirtualCamera {
673 fn drop(&mut self) {
674 if let Some(mut r) = self.running.lock().unwrap_or_else(|e| e.into_inner()).take() {
675 r.queue.close();
676 if let Some(t) = r.thread.take() {
677 let _ = t.join();
678 }
679 }
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686
687 #[test]
688 fn queue_drops_oldest() {
689 let q = Queue::new(2);
690 assert!(q.push(Job { codec: Codec::H264, keyframe: true, orientation: Orientation::UPRIGHT, data: vec![1] }).is_none());
691 assert!(q.push(Job { codec: Codec::H264, keyframe: false, orientation: Orientation::UPRIGHT, data: vec![2] }).is_none());
692 let evicted = q.push(Job { codec: Codec::H264, keyframe: false, orientation: Orientation::UPRIGHT, data: vec![3] }).unwrap();
693 assert_eq!(evicted.data, vec![1]);
694 assert_eq!(q.pop().unwrap().data, vec![2]);
695 assert_eq!(q.pop().unwrap().data, vec![3]);
696 q.close();
697 assert!(q.pop().is_none());
698 }
699
700 #[test]
701 fn pixel_format_names() {
702 assert_eq!(parse_pixel_format("i420"), Some(ring::V4L2_PIX_FMT_YUV420));
703 assert_eq!(parse_pixel_format("NV12"), Some(ring::V4L2_PIX_FMT_NV12));
704 assert_eq!(parse_pixel_format(" yuyv "), Some(ring::V4L2_PIX_FMT_YUYV));
705 assert_eq!(parse_pixel_format("MJPG"), Some(ring::V4L2_PIX_FMT_MJPEG));
706 assert_eq!(parse_pixel_format("mjpeg"), Some(ring::V4L2_PIX_FMT_MJPEG));
707 assert_eq!(parse_pixel_format("rgb"), None);
708 let mjpeg = RingFormat::for_fourcc(ring::V4L2_PIX_FMT_MJPEG, 640, 480, 30, 1).unwrap();
709 assert_eq!((mjpeg.bytesperline, mjpeg.sizeimage), (0, 640 * 480 * 2));
710 }
711
712 #[test]
713 fn mjpeg_encoder_writes_a_jpeg_of_the_device_size() {
714 let dev = DeviceFormat { width: 64, height: 32, fourcc: ring::V4L2_PIX_FMT_MJPEG };
715 let mut src = convert::I420Buffer::new(32, 32);
716 src.data.fill(200);
717 let mut enc = MjpegEncoder::new().unwrap();
718 let mut normalizer = Normalizer::new();
719 let mut slot = vec![0u8; 64 * 32 * 2];
720 let n = enc.write_frame(&mut normalizer, &src.view(true), &dev, &mut slot);
721 assert!(n > 0 && n <= slot.len());
722 let hdr = turbojpeg::read_header(&slot[..n]).unwrap();
723 assert_eq!((hdr.width, hdr.height), (64, 32));
724 }
725}