Skip to content

Commit dfb14d9

Browse files
trekawekclaude
andcommitted
Use OpenCV for webcam capture so it works on Apple Silicon (#60)
The sarxos webcam-capture driver loads BridJ, whose bundled macOS native is x86_64-only, so on an arm64 Mac it fails with an UnsatisfiedLinkError ("incompatible architecture ... need 'arm64'"). Replace it with openpnp's OpenCV build, which ships native libraries for macOS arm64 (and x86_64), Linux and Windows, and drive the camera through OpenCV's VideoCapture. WebcamCameraSource now grabs frames on a daemon thread so getFrame() is a non-blocking read of the latest frame (the emulator polls it on every in-game capture and must not stall on the camera). Any failure - native library unavailable for the platform, no camera, or a read error - degrades to null so the camera falls back to the image file / test pattern and the emulator never crashes; the menu reports it couldn't open. Refs #60 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J1wLWscyUGS7CFwc3CjJR8
1 parent ec113b0 commit dfb14d9

2 files changed

Lines changed: 92 additions & 37 deletions

File tree

swing/pom.xml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,13 @@
5858
<artifactId>core</artifactId>
5959
<version>1.7.2-SNAPSHOT</version>
6060
</dependency>
61+
<!-- OpenCV with bundled natives for the webcam camera source; unlike the old
62+
sarxos/bridj stack this ships Apple-Silicon (osx arm64) natives, and Linux /
63+
Windows / x86_64, so a self-built jar captures on the machine that built it. -->
6164
<dependency>
62-
<groupId>com.github.sarxos</groupId>
63-
<artifactId>webcam-capture</artifactId>
64-
<version>0.3.12</version>
65+
<groupId>org.openpnp</groupId>
66+
<artifactId>opencv</artifactId>
67+
<version>4.9.0-0</version>
6568
</dependency>
6669
</dependencies>
6770
</project>
Lines changed: 86 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,127 @@
11
package eu.rekawek.coffeegb.swing.io;
22

3-
import com.github.sarxos.webcam.Webcam;
43
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;
57
import org.slf4j.Logger;
68
import org.slf4j.LoggerFactory;
79

8-
import java.awt.Dimension;
910
import java.awt.image.BufferedImage;
11+
import java.awt.image.DataBufferByte;
1012

1113
/**
1214
* 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.
1624
*/
1725
public class WebcamCameraSource implements CameraSource, AutoCloseable {
1826

1927
private static final Logger LOG = LoggerFactory.getLogger(WebcamCameraSource.class);
2028

21-
private final Webcam webcam;
29+
private static boolean nativeLoaded;
2230

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();
2544
}
2645

2746
/**
28-
* Opens the default webcam.
47+
* Loads the native library and opens the default camera (device 0).
2948
*
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
3151
*/
32-
public static WebcamCameraSource open() {
52+
public static synchronized WebcamCameraSource open() {
3353
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;
3857
}
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;
5263
}
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);
5666
} catch (Throwable t) {
5767
LOG.warn("Failed to open the webcam", t);
5868
return null;
5969
}
6070
}
6171

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+
6294
@Override
6395
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;
65110
}
66111

67112
@Override
68113
public void close() {
114+
running = false;
115+
grabThread.interrupt();
116+
try {
117+
grabThread.join(500);
118+
} catch (InterruptedException e) {
119+
Thread.currentThread().interrupt();
120+
}
69121
try {
70-
webcam.close();
122+
capture.release();
71123
} catch (Throwable t) {
72-
LOG.warn("Failed to close the webcam", t);
124+
LOG.warn("Failed to release the webcam", t);
73125
}
74126
}
75127
}

0 commit comments

Comments
 (0)