diff --git a/crates/qrcode/keyfork-qrcode/src/lib.rs b/crates/qrcode/keyfork-qrcode/src/lib.rs index 08aa972..96bbe01 100644 --- a/crates/qrcode/keyfork-qrcode/src/lib.rs +++ b/crates/qrcode/keyfork-qrcode/src/lib.rs @@ -1,11 +1,15 @@ //! Encoding and decoding QR codes. +#![allow(clippy::expect_fun_call)] + use keyfork_bug as bug; +use bug::POISONED_MUTEX; use image::{ImageBuffer, ImageReader, Luma}; use std::{ io::{Cursor, Write}, process::{Command, Stdio}, + sync::{mpsc::channel, Arc, Condvar, Mutex}, time::{Duration, Instant}, }; use v4l::{ @@ -15,6 +19,8 @@ use v4l::{ Device, FourCC, }; +type Image = ImageBuffer, Vec>; + /// A QR code could not be generated. #[derive(thiserror::Error, Debug)] pub enum QRGenerationError { @@ -102,12 +108,12 @@ pub fn qrencode( const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format"; trait Scanner { - fn scan_image(&mut self, image: ImageBuffer, Vec>) -> Option; + fn scan_image(&mut self, image: Image) -> Option; } #[cfg(feature = "decode-backend-zbar")] mod zbar { - use super::{ImageBuffer, Luma, Scanner}; + use super::{Image, Scanner}; pub struct Zbar { scanner: keyfork_zbar::image_scanner::ImageScanner, @@ -129,29 +135,25 @@ mod zbar { } impl Scanner for Zbar { - fn scan_image( - &mut self, - image: ImageBuffer, Vec>, - ) -> Option { + fn scan_image(&mut self, image: Image) -> Option { let image = keyfork_zbar::image::Image::from(image); - self.scanner.scan_image(&image).into_iter().next().map(|symbol| { - String::from_utf8_lossy(symbol.data()).into() - }) + self.scanner + .scan_image(&image) + .into_iter() + .next() + .map(|symbol| String::from_utf8_lossy(symbol.data()).into()) } } } #[cfg(feature = "decode-backend-rqrr")] mod rqrr { - use super::{ImageBuffer, Luma, Scanner}; + use super::{Image, Scanner}; pub struct Rqrr; impl Scanner for Rqrr { - fn scan_image( - &mut self, - image: ImageBuffer, Vec>, - ) -> Option { + fn scan_image(&mut self, image: Image) -> Option { let mut image = rqrr::PreparedImage::prepare(image); for grid in image.detect_grids() { if let Ok((_, content)) = grid.decode() { @@ -168,6 +170,14 @@ fn dbg_elapsed(count: u64, instant: Instant) { let elapsed = instant.elapsed().as_secs(); let framerate = count as f64 / elapsed as f64; eprintln!("framerate: {count}/{elapsed} = {framerate}"); + std::thread::sleep(std::time::Duration::from_secs(5)); + +} + +#[derive(Debug)] +struct ScanQueue { + shutdown: bool, + images: Vec, } /// Continuously scan the `index`-th camera for a QR code. @@ -176,6 +186,11 @@ fn dbg_elapsed(count: u64, instant: Instant) { /// /// The function may return an error if the hardware is unable to scan video or if an image could /// not be decoded. +/// +/// # Panics +/// +/// The function may panic if a mutex is poisoned by a thread panicking, which should +/// only happen during a mutex, or if it can't send a message over the mpsc channel. pub fn scan_camera(timeout: Duration, index: usize) -> Result, QRCodeScanError> { let device = Device::new(index)?; let mut fmt = device @@ -185,33 +200,100 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result, QR device.set_format(&fmt)?; let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?; let start = Instant::now(); - cfg_if::cfg_if! { - if #[cfg(feature = "decode-backend-zbar")] { - let mut scanner = zbar::Zbar::default(); - } else if #[cfg(feature = "decode-backend-rqrr")] { - let mut scanner = rqrr::Rqrr; - } else { - unimplemented!("neither decode-backend-zbar nor decode-backend-rqrr were selected") - } - }; #[allow(unused)] let mut count = 0; - while Instant::now().duration_since(start) < timeout { - count += 1; - let (buffer, _) = stream.next()?; - let image = ImageReader::new(Cursor::new(buffer)) - .with_guessed_format()? - .decode()? - .to_luma8(); - if let Some(content) = scanner.scan_image(image) { - // dbg_elapsed(count, start); - return Ok(Some(content)); + let thread_count = 4; + + std::thread::scope(|scope| { + let scan_queue = ScanQueue { + shutdown: false, + images: vec![], + }; + + let arced = Arc::new((Mutex::new(scan_queue), Condvar::new())); + let (tx, rx) = channel(); + + for i in 0..thread_count { + let tx = tx.clone(); + let arced = arced.clone(); + + scope.spawn(move || { + cfg_if::cfg_if! { + if #[cfg(feature = "decode-backend-zbar")] { + let mut scanner = zbar::Zbar::default(); + } else if #[cfg(feature = "decode-backend-rqrr")] { + let mut scanner = rqrr::Rqrr; + } else { + unimplemented!("neither decode-backend-zbar nor decode-backend-rqrr were selected") + } + }; + + let (queue_mutex, condvar) = &*arced; + loop { + // NOTE: Carrying the `queue` variable through the loop, so we can + // pass it through without re-locking, means that we don't drop the + // lock on the mutex. Therefore, we unlock, then immediately + // re-lock when we pass the value to wait_while(). + // + // By holding onto the queue until we pass it back to the Condvar, + // and checking shutdown, we ensure that there's no way we miss the + // shutdown being set before we release the guard on the queue. + let queue = queue_mutex.lock().expect(bug::bug!(POISONED_MUTEX)); + if queue.shutdown { + break; + } + let mut queue = condvar + .wait_while(queue, |queue| { + queue.images.is_empty() && !queue.shutdown + }) + .expect(bug::bug!(POISONED_MUTEX)); + if let Some(image) = queue.images.pop() { + // drop the queue here since this is what's going to take time + drop(queue); + if let Some(content) = scanner.scan_image(image) { + eprintln!("scanned: {content}"); + if tx.send(content).is_err() { + break; + } + } + } + } + eprintln!("shutting down thread {i}"); + }); } - } - // dbg_elapsed(count, start); + while Instant::now().duration_since(start) < timeout { + if let Ok(content) = rx.try_recv() { + arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true; + eprintln!("shutting down threads (good)"); + arced.1.notify_all(); + return Ok(Some(content)); + } - Ok(None) + count += 1; + let (buffer, _) = stream.next()?; + let image = ImageReader::new(Cursor::new(buffer)) + .with_guessed_format()? + .decode()? + .to_luma8(); + + arced + .0 + .lock() + .expect(bug::bug!(POISONED_MUTEX)) + .images + .push(image); + arced.1.notify_one(); + } + + dbg_elapsed(count, start); + + arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true; + eprintln!("shutting down threads (bad)"); + arced.1.notify_all(); + + Ok(None) + }) }