replace sarxos/openimaj library with openpnp-capture library

This commit is contained in:
Craig Raw 2025-03-11 16:21:27 +02:00
parent 289a4453a4
commit 3b9551a8c6
9 changed files with 199 additions and 554 deletions

View File

@ -11,13 +11,11 @@ def osName = os.getFamilyName()
if(os.macOsX) {
osName = "osx"
}
def targetName = ""
def osArch = "x64"
def releaseArch = "x86_64"
if(System.getProperty("os.arch") == "aarch64") {
osArch = "aarch64"
releaseArch = "aarch64"
targetName = "-" + osArch
}
def headless = "true".equals(System.getProperty("java.awt.headless"))
@ -89,12 +87,7 @@ dependencies {
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
implementation('com.sparrowwallet:hummingbird:1.7.4')
implementation('co.nstant.in:cbor:0.9')
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
exclude group: 'com.google.android.tools', module: 'dx'
}
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation('org.openpnp:openpnp-capture-java:0.0.28-2')
implementation("io.matthewnelson.kotlin-components:kmp-tor:${vTor}-${vKmpTor}") {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
@ -378,18 +371,11 @@ extraJavaModuleInfo {
requires('org.slf4j')
requires('com.fasterxml.jackson.databind')
}
module("com.nativelibs4java:bridj${targetName}", 'com.nativelibs4java.bridj') {
exports('org.bridj')
exports('org.bridj.cpp')
requires('java.logging')
}
module("com.github.sarxos:webcam-capture${targetName}", 'com.github.sarxos.webcam.capture') {
exports('com.github.sarxos.webcam')
exports('com.github.sarxos.webcam.ds.buildin')
exports('com.github.sarxos.webcam.ds.buildin.natives')
module('org.openpnp:openpnp-capture-java', 'openpnp.capture.java') {
exports('org.openpnp.capture')
exports('org.openpnp.capture.library')
requires('java.desktop')
requires('com.nativelibs4java.bridj')
requires('org.slf4j')
requires('com.sun.jna')
}
module('de.codecentric.centerdevice:centerdevice-nsmenufx', 'centerdevice.nsmenufx') {
exports('de.codecentric.centerdevice')

2
drongo

@ -1 +1 @@
Subproject commit 2468578e723653344579aac857ee76d1a69fecde
Subproject commit e42931cd55bb99b19472499d27f13c7b5b6f6f82

View File

@ -1,6 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.*;
import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.P2PKHAddress;
@ -47,6 +47,7 @@ import javafx.util.Duration;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders;
import org.openpnp.capture.CaptureDevice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -80,7 +81,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
private final ObjectProperty<CaptureDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
public QRScanDialog() {
this.urDecoder = new URDecoder();
@ -91,7 +92,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
webcamResolutionProperty.set(WebcamResolution.HD);
}
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null);
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
webcamService.setRestartOnFailure(false);
WebcamView webcamView = new WebcamView(webcamService, Config.get().isMirrorCapture());
@ -109,13 +110,13 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
progressBar.setPadding(new Insets(0, 10, 0, 10));
progressBar.setPrefWidth(Integer.MAX_VALUE);
progressBar.progressProperty().bind(percentComplete);
webcamService.openingProperty().addListener((observable, oldValue, newValue) -> {
webcamService.openingProperty().addListener((_, _, newValue) -> {
if(percentComplete.get() <= 0.0) {
Platform.runLater(() -> percentComplete.set(newValue ? 0.0 : -1.0));
}
Platform.runLater(() -> {
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
for(WebcamDevice device : WebcamScanDriver.getFoundDevices()) {
for(CaptureDevice device : webcamService.getFoundDevices()) {
if(device.getName().equals(Config.get().getWebcamDevice())) {
webcamDeviceProperty.set(device);
}
@ -123,6 +124,18 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
});
});
webcamService.closedProperty().addListener((_, _, closed) -> {
if(closed && webcamResolutionProperty.get() != null) {
webcamService.setResolution(webcamResolutionProperty.get());
webcamService.setDevice(webcamDeviceProperty.get());
Platform.runLater(() -> {
if(!webcamService.isRunning()) {
webcamService.reset();
webcamService.start();
}
});
}
});
VBox vBox = new VBox(20);
vBox.getChildren().addAll(wrappedView, progressBar);
@ -131,45 +144,34 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
webcamService.resultProperty().addListener(new QRResultListener());
webcamService.setOnFailed(failedEvent -> {
Throwable exception = failedEvent.getSource().getException();
Throwable nested = exception;
while(nested.getCause() != null) {
nested = nested.getCause();
}
if(OsType.getCurrent() == OsType.WINDOWS &&
nested.getMessage().startsWith("Library 'OpenIMAJGrabber' was not loaded successfully from file")) {
exception = new WebcamDependencyException("Your system is missing a dependency required for the webcam. Follow the link below for more details.\n\n[https://sparrowwallet.com/docs/faq.html#your-system-is-missing-a-dependency-for-the-webcam]", exception);
} else if(nested.getMessage().startsWith("Cannot start native grabber") && Config.get().getWebcamDevice() != null) {
exception = new WebcamOpenException("Cannot open configured webcam " + Config.get().getWebcamDevice() + ", reverting to the default webcam");
Config.get().setWebcamDevice(null);
}
final Throwable result = exception;
Platform.runLater(() -> setResult(new Result(result)));
Throwable exception = Throwables.getRootCause(failedEvent.getSource().getException());
Platform.runLater(() -> setResult(new Result(exception)));
});
webcamService.start();
webcamResolutionProperty.addListener((observable, oldValue, newResolution) -> {
webcamResolutionProperty.addListener((_, _, newResolution) -> {
if(newResolution != null) {
setHeight(newResolution == WebcamResolution.HD ? (getHeight() - 100) : (getHeight() + 100));
EventManager.get().post(new WebcamResolutionChangedEvent(newResolution == WebcamResolution.HD));
}
webcamService.cancel();
});
webcamDeviceProperty.addListener((observable, oldValue, newValue) -> {
webcamDeviceProperty.addListener((_, _, newValue) -> {
Config.get().setWebcamDevice(newValue.getName());
if(!Objects.equals(webcamService.getDevice(), newValue)) {
webcamService.cancel();
}
});
setOnCloseRequest(event -> {
setOnCloseRequest(_ -> {
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
if(Config.get().isHdCapture() != isHdCapture) {
Config.get().setHdCapture(isHdCapture);
}
Platform.runLater(() -> webcamResolutionProperty.set(null));
Platform.runLater(() -> {
webcamResolutionProperty.set(null);
webcamService.close();
});
});
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
@ -685,37 +687,6 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
}
private class QRScanListener implements WebcamListener {
@Override
public void webcamOpen(WebcamEvent webcamEvent) {
}
@Override
public void webcamClosed(WebcamEvent webcamEvent) {
if(webcamResolutionProperty.get() != null) {
webcamService.setResolution(webcamResolutionProperty.get());
webcamService.setDevice(webcamDeviceProperty.get());
Platform.runLater(() -> {
if(!webcamService.isRunning()) {
webcamService.reset();
webcamService.start();
}
});
}
}
@Override
public void webcamDisposed(WebcamEvent webcamEvent) {
}
@Override
public void webcamImageObtained(WebcamEvent webcamEvent) {
}
}
private class QRScanDialogPane extends DialogPane {
@Override
protected Node createButton(ButtonType buttonType) {
@ -735,15 +706,15 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
button = hd;
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
ComboBox<WebcamDevice> devicesCombo = new ComboBox<>(WebcamScanDriver.getFoundDevices());
ComboBox<CaptureDevice> devicesCombo = new ComboBox<>(webcamService.getFoundDevices());
devicesCombo.setConverter(new StringConverter<>() {
@Override
public String toString(WebcamDevice device) {
return device instanceof WebcamScanDevice ? ((WebcamScanDevice)device).getDeviceName() : "Default Camera";
public String toString(CaptureDevice device) {
return device != null && device.getName() != null ? device.getName().replaceAll(" \\(.*\\)", "") : "Default Camera";
}
@Override
public WebcamDevice fromString(String string) {
public CaptureDevice fromString(String string) {
throw new UnsupportedOperationException();
}
});
@ -993,10 +964,4 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
super(message, cause);
}
}
public static class ScanDelayCalculator implements WebcamUpdater.DelayCalculator {
public long calculateDelay(long snapshotDuration, double deviceFps) {
return Math.max(SCAN_PERIOD_MILLIS - snapshotDuration, 0L);
}
}
}

View File

@ -0,0 +1,54 @@
package com.sparrowwallet.sparrow.control;
import org.openpnp.capture.CaptureFormat;
public enum WebcamResolution {
VGA(640, 480),
HD(1280, 720);
private final int width;
private final int height;
WebcamResolution(int width, int height) {
this.width = width;
this.height = height;
}
public int getPixelsCount() {
return this.width * this.height;
}
public int[] getAspectRatio() {
int factor = this.getCommonFactor(this.width, this.height);
int wr = this.width / factor;
int hr = this.height / factor;
return new int[] {wr, hr};
}
private int getCommonFactor(int width, int height) {
return height == 0 ? width : this.getCommonFactor(height, width % height);
}
public int getWidth() {
return this.width;
}
public int getHeight() {
return this.height;
}
public String toString() {
int[] ratio = this.getAspectRatio();
return super.toString() + ' ' + this.width + 'x' + this.height + " (" + ratio[0] + ':' + ratio[1] + ')';
}
public static WebcamResolution from(CaptureFormat captureFormat) {
for(WebcamResolution resolution : values()) {
if(captureFormat.getFormatInfo().width == resolution.width && captureFormat.getFormatInfo().height == resolution.height) {
return resolution;
}
}
return null;
}
}

View File

@ -1,372 +0,0 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.*;
import com.github.sarxos.webcam.ds.buildin.natives.Device;
import com.github.sarxos.webcam.ds.buildin.natives.DeviceList;
import com.github.sarxos.webcam.ds.buildin.natives.OpenIMAJGrabber;
import org.bridj.Pointer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.nio.ByteBuffer;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@SuppressWarnings("deprecation")
public class WebcamScanDevice implements WebcamDevice, WebcamDevice.BufferAccess, Runnable, WebcamDevice.FPSSource {
private static final Logger LOG = LoggerFactory.getLogger(WebcamScanDevice.class);
private static final int DEVICE_BUFFER_SIZE = 5;
private static final Dimension[] DIMENSIONS;
private static final int[] BAND_OFFSETS;
private static final int[] BITS;
private static final int[] OFFSET;
private static final int DATA_TYPE = 0;
private static final ColorSpace COLOR_SPACE;
public static final int SCAN_LOOP_WAIT_MILLIS = 100;
private int timeout = 5000;
private OpenIMAJGrabber grabber = null;
private Device device = null;
private Dimension size = null;
private ComponentSampleModel smodel = null;
private ColorModel cmodel = null;
private boolean failOnSizeMismatch = false;
private final AtomicBoolean disposed = new AtomicBoolean(false);
private final AtomicBoolean open = new AtomicBoolean(false);
private final AtomicBoolean fresh = new AtomicBoolean(false);
private Thread refresher = null;
private String name = null;
private String id = null;
private String fullname = null;
private long t1 = -1L;
private long t2 = -1L;
private volatile double fps = 0.0D;
protected WebcamScanDevice(Device device) {
this.device = device;
this.name = device.getNameStr();
this.id = device.getIdentifierStr();
this.fullname = String.format("%s %s", this.name, this.id);
}
public String getName() {
return this.fullname;
}
public String getDeviceName() {
return this.name;
}
public String getDeviceId() {
return this.id;
}
public Device getDeviceRef() {
return this.device;
}
public Dimension[] getResolutions() {
return DIMENSIONS;
}
public Dimension getResolution() {
if (this.size == null) {
this.size = this.getResolutions()[0];
}
return this.size;
}
public void setResolution(Dimension size) {
if (size == null) {
throw new IllegalArgumentException("Size cannot be null");
} else if (this.open.get()) {
throw new IllegalStateException("Cannot change resolution when webcam is open, please close it first");
} else {
this.size = size;
}
}
public ByteBuffer getImageBytes() {
if (this.disposed.get()) {
LOG.debug("Webcam is disposed, image will be null");
return null;
} else if (!this.open.get()) {
LOG.debug("Webcam is closed, image will be null");
return null;
} else {
if (this.fresh.compareAndSet(false, true)) {
this.updateFrameBuffer();
}
LOG.trace("Webcam grabber get image pointer");
Pointer<Byte> image = this.grabber.getImage();
this.fresh.set(false);
if (image == null) {
LOG.warn("Null array pointer found instead of image");
return null;
} else {
int length = this.size.width * this.size.height * 3;
LOG.trace("Webcam device get buffer, read {} bytes", length);
return image.getByteBuffer((long)length);
}
}
}
public void getImageBytes(ByteBuffer target) {
if (this.disposed.get()) {
LOG.debug("Webcam is disposed, image will be null");
} else if (!this.open.get()) {
LOG.debug("Webcam is closed, image will be null");
} else {
int minSize = this.size.width * this.size.height * 3;
int curSize = target.remaining();
if (minSize > curSize) {
throw new IllegalArgumentException(String.format("Not enough remaining space in target buffer (%d necessary vs %d remaining)", minSize, curSize));
} else {
if (this.fresh.compareAndSet(false, true)) {
this.updateFrameBuffer();
}
LOG.trace("Webcam grabber get image pointer");
Pointer<Byte> image = this.grabber.getImage();
this.fresh.set(false);
if (image == null) {
LOG.warn("Null array pointer found instead of image");
} else {
LOG.trace("Webcam device read buffer {} bytes", minSize);
image = image.validBytes((long)minSize);
image.getBytes(target);
}
}
}
}
public BufferedImage getImage() {
ByteBuffer buffer = this.getImageBytes();
if (buffer == null) {
LOG.error("Images bytes buffer is null!");
return null;
} else {
byte[] bytes = new byte[this.size.width * this.size.height * 3];
byte[][] data = new byte[][]{bytes};
buffer.get(bytes);
DataBufferByte dbuf = new DataBufferByte(data, bytes.length, OFFSET);
WritableRaster raster = Raster.createWritableRaster(this.smodel, dbuf, (Point)null);
BufferedImage bi = new BufferedImage(this.cmodel, raster, false, (Hashtable)null);
bi.flush();
return bi;
}
}
public void open() {
if (!this.disposed.get()) {
LOG.debug("Opening webcam device {}", this.getName());
if (this.size == null) {
this.size = this.getResolutions()[0];
}
if (this.size == null) {
throw new RuntimeException("The resolution size cannot be null");
} else {
LOG.debug("Webcam device {} starting session, size {}", this.device.getIdentifierStr(), this.size);
this.grabber = new OpenIMAJGrabber();
DeviceList list = (DeviceList)this.grabber.getVideoDevices().get();
Iterator var2 = list.asArrayList().iterator();
while(var2.hasNext()) {
Device d = (Device)var2.next();
d.getNameStr();
d.getIdentifierStr();
}
boolean started = this.grabber.startSession(this.size.width, this.size.height, 50, Pointer.pointerTo(this.device));
if (!started) {
throw new WebcamException("Cannot start native grabber!");
} else {
this.grabber.setTimeout(this.timeout);
LOG.debug("Webcam device session started");
Dimension size2 = new Dimension(this.grabber.getWidth(), this.grabber.getHeight());
int w1 = this.size.width;
int w2 = size2.width;
int h1 = this.size.height;
int h2 = size2.height;
if (w1 != w2 || h1 != h2) {
if (this.failOnSizeMismatch) {
throw new WebcamException(String.format("Different size obtained vs requested - [%dx%d] vs [%dx%d]", w1, h1, w2, h2));
}
Object[] args = new Object[]{w1, h1, w2, h2, w2, h2};
LOG.warn("Different size obtained vs requested - [{}x{}] vs [{}x{}]. Setting correct one. New size is [{}x{}]", args);
this.size = new Dimension(w2, h2);
}
this.smodel = new ComponentSampleModel(0, this.size.width, this.size.height, 3, this.size.width * 3, BAND_OFFSETS);
this.cmodel = new ComponentColorModel(COLOR_SPACE, BITS, false, false, 1, 0);
LOG.debug("Clear memory buffer");
this.clearMemoryBuffer();
LOG.debug("Webcam device {} is now open", this);
this.open.set(true);
this.refresher = this.startFramesRefresher();
}
}
}
}
private void clearMemoryBuffer() {
for(int i = 0; i < 5; ++i) {
this.grabber.nextFrame();
}
}
private Thread startFramesRefresher() {
Thread refresher = new Thread(this, String.format("frames-refresher-[%s]", this.id));
refresher.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
refresher.setDaemon(true);
refresher.start();
return refresher;
}
public void close() {
if (this.open.compareAndSet(true, false)) {
LOG.debug("Closing webcam device");
this.grabber.stopSession();
}
}
public void dispose() {
if (this.disposed.compareAndSet(false, true)) {
LOG.debug("Disposing webcam device {}", this.getName());
this.close();
}
}
public void setFailOnSizeMismatch(boolean fail) {
this.failOnSizeMismatch = fail;
}
public boolean isOpen() {
return this.open.get();
}
public int getTimeout() {
return this.timeout;
}
public void setTimeout(int timeout) {
if (this.isOpen()) {
throw new WebcamException("Timeout must be set before webcam is open");
} else {
this.timeout = timeout;
}
}
private void updateFrameBuffer() {
LOG.trace("Next frame");
if (this.t1 == -1L || this.t2 == -1L) {
this.t1 = System.currentTimeMillis();
this.t2 = System.currentTimeMillis();
}
int result = (new WebcamScanDevice.NextFrameTask(this)).nextFrame();
this.t1 = this.t2;
this.t2 = System.currentTimeMillis();
this.fps = (4.0D * this.fps + (double)(1000L / (this.t2 - this.t1 + 1L))) / 5.0D;
if (result == -1) {
LOG.error("Timeout when requesting image!");
} else if (result < -1) {
LOG.error("Error requesting new frame!");
}
}
public void run() {
do {
try {
Thread.sleep(SCAN_LOOP_WAIT_MILLIS);
} catch(InterruptedException e) {
//ignore
}
if (Thread.interrupted()) {
LOG.debug("Refresher has been interrupted");
return;
}
if (!this.open.get()) {
LOG.debug("Cancelling refresher");
return;
}
this.updateFrameBuffer();
} while(this.open.get());
}
public double getFPS() {
return this.fps;
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
WebcamScanDevice that = (WebcamScanDevice) o;
return Objects.equals(fullname, that.fullname);
}
@Override
public int hashCode() {
return Objects.hash(fullname);
}
static {
DIMENSIONS = new Dimension[]{WebcamResolution.QQVGA.getSize(), WebcamResolution.QVGA.getSize(), WebcamResolution.VGA.getSize()};
BAND_OFFSETS = new int[]{0, 1, 2};
BITS = new int[]{8, 8, 8};
OFFSET = new int[]{0};
COLOR_SPACE = ColorSpace.getInstance(1000);
}
private class NextFrameTask extends WebcamTask {
private final AtomicInteger result = new AtomicInteger(0);
public NextFrameTask(WebcamDevice device) {
super(device);
}
public int nextFrame() {
try {
this.process();
} catch (InterruptedException var2) {
WebcamScanDevice.LOG.debug("Image buffer request interrupted", var2);
}
return this.result.get();
}
protected void handle() {
WebcamScanDevice device = (WebcamScanDevice)this.getDevice();
if (device.isOpen()) {
try {
Thread.sleep(SCAN_LOOP_WAIT_MILLIS);
} catch(InterruptedException e) {
//ignore
}
this.result.set(WebcamScanDevice.this.grabber.nextFrame());
WebcamScanDevice.this.fresh.set(true);
}
}
}
}

View File

@ -1,45 +0,0 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.WebcamDevice;
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDevice;
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDriver;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.List;
public class WebcamScanDriver extends WebcamDefaultDriver {
private static final ObservableList<WebcamDevice> webcamDevices = FXCollections.observableArrayList();
private static boolean rescan;
@Override
public List<WebcamDevice> getDevices() {
if(rescan || webcamDevices.isEmpty()) {
List<WebcamDevice> devices = super.getDevices();
List<WebcamDevice> scanDevices = new ArrayList<>();
for(WebcamDevice device : devices) {
WebcamDefaultDevice defaultDevice = (WebcamDefaultDevice)device;
WebcamScanDevice scanDevice = new WebcamScanDevice(defaultDevice.getDeviceRef());
if(scanDevices.stream().noneMatch(dev -> ((WebcamScanDevice)dev).getDeviceName().equals(scanDevice.getDeviceName()))) {
scanDevices.add(scanDevice);
}
}
List<WebcamDevice> newDevices = new ArrayList<>(scanDevices);
newDevices.removeAll(webcamDevices);
webcamDevices.addAll(newDevices);
webcamDevices.removeIf(device -> !scanDevices.contains(device));
}
return webcamDevices;
}
public static ObservableList<WebcamDevice> getFoundDevices() {
return webcamDevices;
}
public static void rescan() {
rescan = true;
}
}

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.*;
import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;
@ -11,11 +10,18 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import net.sourceforge.zbar.ZBar;
import org.openpnp.capture.CaptureDevice;
import org.openpnp.capture.CaptureFormat;
import org.openpnp.capture.CaptureStream;
import org.openpnp.capture.OpenPnpCapture;
import org.openpnp.capture.library.OpenpnpCaptureLibrary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -23,38 +29,59 @@ import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
public class WebcamService extends ScheduledService<Image> {
private static final Logger log = LoggerFactory.getLogger(WebcamService.class);
private WebcamResolution resolution;
private WebcamDevice device;
private final WebcamListener listener;
private final WebcamUpdater.DelayCalculator delayCalculator;
private CaptureDevice device;
private final BooleanProperty opening = new SimpleBooleanProperty(false);
private final BooleanProperty closed = new SimpleBooleanProperty(false);
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
private static final int QR_SAMPLE_PERIOD_MILLIS = 200;
private Webcam cam;
private final OpenPnpCapture capture;
private CaptureStream stream;
private long lastQrSampleTime;
private final ObservableList<CaptureDevice> foundDevices = FXCollections.observableList(new ArrayList<>());
private final Reader qrReader;
private final Bokmakierie bokmakierie;
static {
Webcam.setDriver(new WebcamScanDriver());
OpenpnpCaptureLibrary.INSTANCE.Cap_installCustomLogFunction((level, ptr) -> {
switch(level) {
case 0:
case 1:
case 2:
case 3:
log.error(ptr.getString(0).trim());
break;
case 4:
case 5:
case 6:
log.info(ptr.getString(0).trim());
break;
case 7:
log.debug(ptr.getString(0).trim());
break;
case 8:
log.trace(ptr.getString(0).trim());
break;
}
});
}
public WebcamService(WebcamResolution resolution, WebcamDevice device, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
this.resolution = resolution;
this.device = device;
this.listener = listener;
this.delayCalculator = delayCalculator;
public WebcamService(WebcamResolution requestedResolution, CaptureDevice requestedDevice) {
this.capture = new OpenPnpCapture();
this.resolution = requestedResolution;
this.device = requestedDevice;
this.lastQrSampleTime = System.currentTimeMillis();
this.qrReader = new QRCodeReader();
this.bokmakierie = new Bokmakierie();
@ -62,50 +89,66 @@ public class WebcamService extends ScheduledService<Image> {
@Override
public Task<Image> createTask() {
return new Task<Image>() {
return new Task<>() {
@Override
protected Image call() throws Exception {
try {
if(cam == null) {
List<Webcam> webcams = Webcam.getWebcams(1, TimeUnit.MINUTES);
if(webcams.isEmpty()) {
throw new UnsupportedOperationException("No camera available.");
if(stream == null) {
List<CaptureDevice> devices = capture.getDevices();
List<CaptureDevice> newDevices = new ArrayList<>(devices);
newDevices.removeAll(foundDevices);
foundDevices.addAll(newDevices);
foundDevices.removeIf(device -> !devices.contains(device));
if(foundDevices.isEmpty()) {
throw new UnsupportedOperationException("No cameras available");
}
cam = webcams.get(0);
CaptureDevice selectedDevice = foundDevices.getFirst();
if(device != null) {
for(Webcam webcam : webcams) {
if(webcam.getDevice().getName().equals(device.getName())) {
cam = webcam;
for(CaptureDevice webcam : foundDevices) {
if(webcam.getName().equals(device.getName())) {
selectedDevice = webcam;
}
}
} else if(Config.get().getWebcamDevice() != null) {
for(Webcam webcam : webcams) {
if(webcam.getDevice().getName().equals(Config.get().getWebcamDevice())) {
cam = webcam;
for(CaptureDevice webcam : foundDevices) {
if(webcam.getName().equals(Config.get().getWebcamDevice())) {
selectedDevice = webcam;
}
}
}
device = cam.getDevice();
device = selectedDevice;
cam.setCustomViewSizes(resolution.getSize());
cam.setViewSize(resolution.getSize());
if(!Arrays.asList(cam.getWebcamListeners()).contains(listener)) {
cam.addWebcamListener(listener);
if(device.getFormats().isEmpty()) {
throw new UnsupportedOperationException("No resolutions supported by camera " + device.getName());
}
Map<WebcamResolution, CaptureFormat> supportedResolutions = device.getFormats().stream()
.filter(f -> WebcamResolution.from(f) != null)
.collect(Collectors.toMap(WebcamResolution::from, Function.identity(), (u, v) -> u));
CaptureFormat format = supportedResolutions.get(resolution);
if(format == null) {
if(!supportedResolutions.isEmpty()) {
format = supportedResolutions.values().iterator().next();
} else {
format = device.getFormats().getFirst();
}
log.warn("Could not get requested capture resolution, using " + format.getFormatInfo().width + "x" + format.getFormatInfo().height);
}
opening.set(true);
cam.open(true, delayCalculator);
stream = device.openStream(format);
opening.set(false);
closed.set(false);
}
BufferedImage originalImage = cam.getImage();
if(originalImage == null) {
return null;
}
BufferedImage originalImage = stream.capture();
CroppedDimension cropped = getCroppedDimension(originalImage);
BufferedImage croppedImage = originalImage.getSubimage(cropped.x, cropped.y, cropped.length, cropped.length);
BufferedImage framedImage = getFramedImage(originalImage, cropped);
@ -128,19 +171,24 @@ public class WebcamService extends ScheduledService<Image> {
@Override
public void reset() {
cam = null;
stream = null;
super.reset();
}
@Override
public boolean cancel() {
if(cam != null && !cam.close()) {
cam.close();
if(stream != null) {
stream.close();
closed.set(true);
}
return super.cancel();
}
public void close() {
capture.close();
}
private void readQR(BufferedImage wideImage, BufferedImage croppedImage) {
Result result = readQR(wideImage);
if(result == null) {
@ -235,33 +283,46 @@ public class WebcamService extends ScheduledService<Image> {
}
public int getCamWidth() {
return resolution.getSize().width;
return resolution.getWidth();
}
public int getCamHeight() {
return resolution.getSize().height;
return resolution.getHeight();
}
public void setResolution(WebcamResolution resolution) {
this.resolution = resolution;
}
public WebcamDevice getDevice() {
public CaptureDevice getDevice() {
return device;
}
public void setDevice(WebcamDevice device) {
public void setDevice(CaptureDevice device) {
this.device = device;
}
public boolean isOpening() {
return opening.get();
public ObservableList<CaptureDevice> getFoundDevices() {
return foundDevices;
}
public BooleanProperty openingProperty() {
return opening;
}
public BooleanProperty closedProperty() {
return closed;
}
public static String fourCCToString(int fourCC) {
return new String(new char[] {
(char) (fourCC >> 24 & 0xFF),
(char) ((fourCC >> 16) & 0xFF),
(char) ((fourCC >> 8) & 0xFF),
(char) ((fourCC) & 0xFF)
});
}
private static class CroppedDimension {
public int x;
public int y;

View File

@ -42,12 +42,11 @@ open module com.sparrowwallet.sparrow {
requires com.h2database;
requires com.sparrowwallet.hummingbird;
requires org.fxmisc.flowless;
requires com.github.sarxos.webcam.capture;
requires openpnp.capture.java;
requires centerdevice.nsmenufx;
requires org.jcommander;
requires jul.to.slf4j;
requires net.sourceforge.javacsv;
requires com.nativelibs4java.bridj;
requires org.reactfx.reactfx;
requires dev.bwt.jni;
requires io.reactivex.rxjava2;
@ -66,4 +65,5 @@ open module com.sparrowwallet.sparrow {
requires com.jcraft.jzlib;
requires com.sparrowwallet.tern;
requires com.sparrowwallet.lark;
requires com.sun.jna;
}

View File

@ -1,9 +1,6 @@
<configuration>
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<logger name="com.github.sarxos.webcam.Webcam" level="OFF"/>
<logger name="com.github.sarxos.webcam.ds.cgt.WebcamOpenTask" level="OFF"/>
<logger name="com.github.sarxos.webcam.ds.cgt.WebcamCloseTask" level="OFF"/>
<logger name="javafx.css" level="ERROR"/>
<logger name="javafx.scene.focus" level="INFO"/>
<logger name="sun.net.www.protocol.http.HttpURLConnection" level="INFO" />
@ -34,7 +31,6 @@
<logger name="org.eclipse.jetty.http.HttpParser" level="OFF" />
<logger name="org.eclipse.jetty.util.log" level="OFF" />
<logger name="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler" level="OFF" />
<logger name="org.bitcoinj.crypto.MnemonicCode" level="OFF" />
<logger name="org.springframework.core.KotlinDetector" level="OFF" />
<logger name="org.springframework.http.converter.json.Jackson2ObjectMapperBuilder" level="OFF" />
<logger name="org.springframework.web.HttpLogging" level="OFF" />