|
1 | 1 | package eu.rekawek.coffeegb.swing.io; |
2 | 2 |
|
3 | | -import com.github.sarxos.webcam.Webcam; |
4 | 3 | import eu.rekawek.coffeegb.core.memory.cart.type.CameraSource; |
| 4 | +import nu.pattern.OpenCV; |
| 5 | +import org.opencv.core.Mat; |
| 6 | +import org.opencv.videoio.VideoCapture; |
5 | 7 | import org.slf4j.Logger; |
6 | 8 | import org.slf4j.LoggerFactory; |
7 | 9 |
|
8 | | -import java.awt.Dimension; |
9 | 10 | import java.awt.image.BufferedImage; |
| 11 | +import java.awt.image.DataBufferByte; |
10 | 12 |
|
11 | 13 | /** |
12 | 14 | * A live {@link CameraSource} for the Game Boy (Pocket) Camera, backed by a real webcam |
13 | | - * through the sarxos webcam-capture library. The webcam runs in async mode: a background |
14 | | - * thread keeps the latest frame buffered, so {@link #getFrame()} is a cheap non-blocking |
15 | | - * read the emulator can call on every in-game capture. |
| 15 | + * through OpenCV's {@code VideoCapture} (openpnp's OpenCV build, which bundles native |
| 16 | + * libraries for Linux, Windows and macOS including Apple Silicon - unlike the old |
| 17 | + * sarxos/BridJ stack that only had x86_64 natives). |
| 18 | + * |
| 19 | + * <p>A daemon thread grabs frames continuously so {@link #getFrame()} is a cheap |
| 20 | + * non-blocking read of the latest frame - the emulator calls it on every in-game capture |
| 21 | + * and must never stall on the camera. Any failure to load the native library, open the |
| 22 | + * device or read a frame degrades to {@code null} (the camera then falls back to the image |
| 23 | + * file / test pattern) rather than crashing the emulator. |
16 | 24 | */ |
17 | 25 | public class WebcamCameraSource implements CameraSource, AutoCloseable { |
18 | 26 |
|
19 | 27 | private static final Logger LOG = LoggerFactory.getLogger(WebcamCameraSource.class); |
20 | 28 |
|
21 | | - private final Webcam webcam; |
| 29 | + private static boolean nativeLoaded; |
22 | 30 |
|
23 | | - private WebcamCameraSource(Webcam webcam) { |
24 | | - this.webcam = webcam; |
| 31 | + private final VideoCapture capture; |
| 32 | + |
| 33 | + private final Thread grabThread; |
| 34 | + |
| 35 | + private volatile boolean running = true; |
| 36 | + |
| 37 | + private volatile BufferedImage latest; |
| 38 | + |
| 39 | + private WebcamCameraSource(VideoCapture capture) { |
| 40 | + this.capture = capture; |
| 41 | + this.grabThread = new Thread(this::grabLoop, "webcam-grab"); |
| 42 | + this.grabThread.setDaemon(true); |
| 43 | + this.grabThread.start(); |
25 | 44 | } |
26 | 45 |
|
27 | 46 | /** |
28 | | - * Opens the default webcam. |
| 47 | + * Loads the native library and opens the default camera (device 0). |
29 | 48 | * |
30 | | - * @return the source, or {@code null} if no webcam is available or it cannot be opened |
| 49 | + * @return the source, or {@code null} if the native library is unavailable (e.g. an |
| 50 | + * unsupported architecture) or no camera can be opened |
31 | 51 | */ |
32 | | - public static WebcamCameraSource open() { |
| 52 | + public static synchronized WebcamCameraSource open() { |
33 | 53 | try { |
34 | | - Webcam webcam = Webcam.getDefault(); |
35 | | - if (webcam == null) { |
36 | | - LOG.warn("No webcam detected"); |
37 | | - return null; |
| 54 | + if (!nativeLoaded) { |
| 55 | + OpenCV.loadLocally(); |
| 56 | + nativeLoaded = true; |
38 | 57 | } |
39 | | - // the frame is downscaled to the 128x112 sensor anyway, so prefer the smallest |
40 | | - // supported resolution that still covers it (lower latency and CPU) |
41 | | - Dimension best = null; |
42 | | - for (Dimension d : webcam.getViewSizes()) { |
43 | | - if (d.width < 128 || d.height < 112) { |
44 | | - continue; |
45 | | - } |
46 | | - if (best == null || (long) d.width * d.height < (long) best.width * best.height) { |
47 | | - best = d; |
48 | | - } |
49 | | - } |
50 | | - if (best != null) { |
51 | | - webcam.setViewSize(best); |
| 58 | + VideoCapture capture = new VideoCapture(0); |
| 59 | + if (!capture.isOpened()) { |
| 60 | + LOG.warn("No webcam could be opened (device 0)"); |
| 61 | + capture.release(); |
| 62 | + return null; |
52 | 63 | } |
53 | | - webcam.open(true); |
54 | | - LOG.info("Opened webcam {} at {}", webcam.getName(), webcam.getViewSize()); |
55 | | - return new WebcamCameraSource(webcam); |
| 64 | + LOG.info("Opened webcam device 0 via OpenCV"); |
| 65 | + return new WebcamCameraSource(capture); |
56 | 66 | } catch (Throwable t) { |
57 | 67 | LOG.warn("Failed to open the webcam", t); |
58 | 68 | return null; |
59 | 69 | } |
60 | 70 | } |
61 | 71 |
|
| 72 | + private void grabLoop() { |
| 73 | + Mat mat = new Mat(); |
| 74 | + while (running) { |
| 75 | + try { |
| 76 | + if (capture.read(mat) && !mat.empty()) { |
| 77 | + latest = matToBufferedImage(mat); |
| 78 | + } else { |
| 79 | + Thread.sleep(50); |
| 80 | + } |
| 81 | + } catch (InterruptedException e) { |
| 82 | + return; |
| 83 | + } catch (Throwable t) { |
| 84 | + LOG.warn("Webcam grab failed", t); |
| 85 | + try { |
| 86 | + Thread.sleep(200); |
| 87 | + } catch (InterruptedException e) { |
| 88 | + return; |
| 89 | + } |
| 90 | + } |
| 91 | + } |
| 92 | + } |
| 93 | + |
62 | 94 | @Override |
63 | 95 | public BufferedImage getFrame() { |
64 | | - return webcam.isOpen() ? webcam.getImage() : null; |
| 96 | + return latest; |
| 97 | + } |
| 98 | + |
| 99 | + private static BufferedImage matToBufferedImage(Mat mat) { |
| 100 | + int width = mat.cols(); |
| 101 | + int height = mat.rows(); |
| 102 | + int channels = mat.channels(); |
| 103 | + byte[] data = new byte[width * height * channels]; |
| 104 | + mat.get(0, 0, data); |
| 105 | + int type = channels == 1 ? BufferedImage.TYPE_BYTE_GRAY : BufferedImage.TYPE_3BYTE_BGR; |
| 106 | + BufferedImage image = new BufferedImage(width, height, type); |
| 107 | + byte[] target = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); |
| 108 | + System.arraycopy(data, 0, target, 0, Math.min(data.length, target.length)); |
| 109 | + return image; |
65 | 110 | } |
66 | 111 |
|
67 | 112 | @Override |
68 | 113 | public void close() { |
| 114 | + running = false; |
| 115 | + grabThread.interrupt(); |
| 116 | + try { |
| 117 | + grabThread.join(500); |
| 118 | + } catch (InterruptedException e) { |
| 119 | + Thread.currentThread().interrupt(); |
| 120 | + } |
69 | 121 | try { |
70 | | - webcam.close(); |
| 122 | + capture.release(); |
71 | 123 | } catch (Throwable t) { |
72 | | - LOG.warn("Failed to close the webcam", t); |
| 124 | + LOG.warn("Failed to release the webcam", t); |
73 | 125 | } |
74 | 126 | } |
75 | 127 | } |
0 commit comments