mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-23 20:36:44 +00:00
reduce camera cpu usage through sleeping webcam capture threads to match 10fps
This commit is contained in:
parent
d5830399b7
commit
8388a7fed5
5 changed files with 382 additions and 1 deletions
|
@ -0,0 +1,354 @@
|
|||
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.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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class WebcamScanDriver extends WebcamDefaultDriver {
|
||||
@Override
|
||||
public List<WebcamDevice> getDevices() {
|
||||
List<WebcamDevice> devices = super.getDevices();
|
||||
List<WebcamDevice> scanDevices = new ArrayList<>();
|
||||
for(WebcamDevice device : devices) {
|
||||
WebcamDefaultDevice defaultDevice = (WebcamDefaultDevice)device;
|
||||
scanDevices.add(new WebcamScanDevice(defaultDevice.getDeviceRef()));
|
||||
}
|
||||
|
||||
return scanDevices;
|
||||
}
|
||||
}
|
|
@ -34,6 +34,10 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
private Webcam cam;
|
||||
private long lastQrSampleTime;
|
||||
|
||||
static {
|
||||
Webcam.setDriver(new WebcamScanDriver());
|
||||
}
|
||||
|
||||
public WebcamService(WebcamResolution resolution, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
|
||||
this.resolution = resolution;
|
||||
this.listener = listener;
|
||||
|
|
|
@ -29,4 +29,5 @@ open module com.sparrowwallet.sparrow {
|
|||
requires jtorctl;
|
||||
requires javacsv;
|
||||
requires jul.to.slf4j;
|
||||
requires bridj;
|
||||
}
|
|
@ -85,7 +85,7 @@
|
|||
<SeparatorMenuItem />
|
||||
<CheckMenuItem fx:id="openWalletsInNewWindows" mnemonicParsing="false" text="Open Wallets in New Windows" onAction="#openWalletsInNewWindows"/>
|
||||
<CheckMenuItem fx:id="hideEmptyUsedAddresses" mnemonicParsing="false" text="Hide Empty Used Addresses" onAction="#hideEmptyUsedAddresses"/>
|
||||
<CheckMenuItem fx:id="useHdCameraResolution" mnemonicParsing="false" text="Use HD camera resolution" onAction="#useHdCameraResolution"/>
|
||||
<CheckMenuItem fx:id="useHdCameraResolution" mnemonicParsing="false" text="Use HD Camera Resolution" onAction="#useHdCameraResolution"/>
|
||||
<CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/>
|
||||
<SeparatorMenuItem />
|
||||
<MenuItem fx:id="minimizeToTray" mnemonicParsing="false" text="Minimize to System Tray" accelerator="Shortcut+Y" onAction="#minimizeToTray"/>
|
||||
|
|
Loading…
Reference in a new issue