diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java index f6d744be..d01212bb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java @@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control; import com.github.sarxos.webcam.WebcamEvent; import com.github.sarxos.webcam.WebcamListener; import com.github.sarxos.webcam.WebcamResolution; +import com.github.sarxos.webcam.WebcamUpdater; import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.OutputDescriptor; @@ -39,6 +40,7 @@ import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.layout.*; +import javafx.util.Duration; import org.controlsfx.glyphfont.Glyph; import org.controlsfx.tools.Borders; import org.slf4j.Logger; @@ -67,6 +69,7 @@ public class QRScanDialog extends Dialog { private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)"); + private static final int SCAN_PERIOD_MILLIS = 100; private final ObjectProperty webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.VGA); private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0); @@ -79,7 +82,9 @@ public class QRScanDialog extends Dialog { webcamResolutionProperty.set(WebcamResolution.HD); } - this.webcamService = new WebcamService(webcamResolutionProperty.get(), new QRScanListener()); + this.webcamService = new WebcamService(webcamResolutionProperty.get(), new QRScanListener(), new ScanDelayCalculator()); + webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS)); + webcamService.setRestartOnFailure(false); WebcamView webcamView = new WebcamView(webcamService); final DialogPane dialogPane = new QRScanDialogPane(); @@ -703,4 +708,10 @@ public class QRScanDialog extends Dialog { 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); + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java index 59ef8ed3..467b391e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java @@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control; import com.github.sarxos.webcam.Webcam; import com.github.sarxos.webcam.WebcamListener; import com.github.sarxos.webcam.WebcamResolution; +import com.github.sarxos.webcam.WebcamUpdater; import com.google.zxing.*; import com.google.zxing.client.j2se.BufferedImageLuminanceSource; import com.google.zxing.common.HybridBinarizer; @@ -10,7 +11,7 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; -import javafx.concurrent.Service; +import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; import javafx.embed.swing.SwingFXUtils; import javafx.scene.image.Image; @@ -19,16 +20,24 @@ import java.awt.image.BufferedImage; import java.util.Arrays; import java.util.concurrent.TimeUnit; -public class WebcamService extends Service { +public class WebcamService extends ScheduledService { private WebcamResolution resolution; private final WebcamListener listener; - private BooleanProperty opening = new SimpleBooleanProperty(false); + private final WebcamUpdater.DelayCalculator delayCalculator; + private final BooleanProperty opening = new SimpleBooleanProperty(false); private final ObjectProperty resultProperty = new SimpleObjectProperty<>(null); - public WebcamService(WebcamResolution resolution, WebcamListener listener) { + private static final int QR_SAMPLE_PERIOD_MILLIS = 400; + + private Webcam cam; + private long lastQrSampleTime; + + public WebcamService(WebcamResolution resolution, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) { this.resolution = resolution; this.listener = listener; + this.delayCalculator = delayCalculator; + this.lastQrSampleTime = System.currentTimeMillis(); } @Override @@ -36,35 +45,52 @@ public class WebcamService extends Service { return new Task() { @Override protected Image call() throws Exception { - Webcam cam = Webcam.getWebcams(1, TimeUnit.MINUTES).get(0); try { - cam.setCustomViewSizes(resolution.getSize()); - cam.setViewSize(resolution.getSize()); - if(!Arrays.asList(cam.getWebcamListeners()).contains(listener)) { - cam.addWebcamListener(listener); + if(cam == null) { + cam = Webcam.getWebcams(1, TimeUnit.MINUTES).get(0); + cam.setCustomViewSizes(resolution.getSize()); + cam.setViewSize(resolution.getSize()); + if(!Arrays.asList(cam.getWebcamListeners()).contains(listener)) { + cam.addWebcamListener(listener); + } + + opening.set(true); + cam.open(true, delayCalculator); + opening.set(false); } - opening.set(true); - cam.open(); - opening.set(false); - while(!isCancelled()) { - if(cam.isImageNew()) { - BufferedImage bimg = cam.getImage(); - updateValue(SwingFXUtils.toFXImage(bimg, null)); - readQR(bimg); - } + BufferedImage bimg = cam.getImage(); + Image image = SwingFXUtils.toFXImage(bimg, null); + updateValue(image); + + if(System.currentTimeMillis() > (lastQrSampleTime + QR_SAMPLE_PERIOD_MILLIS)) { + readQR(bimg); + lastQrSampleTime = System.currentTimeMillis(); } - return getValue(); + + return image; } finally { opening.set(false); - if(!cam.close()) { - cam.close(); - } } } }; } + @Override + public void reset() { + cam = null; + super.reset(); + } + + @Override + public boolean cancel() { + if(cam != null && !cam.close()) { + cam.close(); + } + + return super.cancel(); + } + private void readQR(BufferedImage bufferedImage) { LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamView.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamView.java index cb160662..f5ce9891 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamView.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamView.java @@ -1,7 +1,10 @@ package com.sparrowwallet.sparrow.control; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.Node; import javafx.scene.control.Label; +import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.Region; import org.slf4j.Logger; @@ -14,7 +17,9 @@ public class WebcamView { private final WebcamService service; private final Region view; - private final Label statusPlaceholder ; + private final Label statusPlaceholder; + + private final ObjectProperty imageProperty = new SimpleObjectProperty<>(null); public WebcamView(WebcamService service) { this.service = service ; @@ -23,22 +28,32 @@ public class WebcamView { // make the cam behave like a mirror: imageView.setScaleX(-1); + service.valueProperty().addListener((observable, oldValue, newValue) -> { + if(newValue != null) { + imageProperty.set(newValue); + } + }); + this.statusPlaceholder = new Label(); this.view = new Region() { { service.stateProperty().addListener((obs, oldState, newState) -> { switch (newState) { case READY: - statusPlaceholder.setText("Initializing"); - getChildren().setAll(statusPlaceholder); + if(imageProperty.get() == null) { + statusPlaceholder.setText("Initializing"); + getChildren().setAll(statusPlaceholder); + } break ; case SCHEDULED: - statusPlaceholder.setText("Waiting"); - getChildren().setAll(statusPlaceholder); + if(imageProperty.get() == null) { + statusPlaceholder.setText("Waiting"); + getChildren().setAll(statusPlaceholder); + } break ; case RUNNING: imageView.imageProperty().unbind(); - imageView.imageProperty().bind(service.valueProperty()); + imageView.imageProperty().bind(imageProperty); getChildren().setAll(imageView); break ; case CANCELLED: