reduce camera cpu usage through sleeping webcam capture threads to match 10fps

This commit is contained in:
Craig Raw 2021-04-07 13:34:06 +02:00
parent d5830399b7
commit 8388a7fed5
5 changed files with 382 additions and 1 deletions

View file

@ -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);
}
}
}
}

View file

@ -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;
}
}

View file

@ -34,6 +34,10 @@ public class WebcamService extends ScheduledService<Image> {
private Webcam cam; private Webcam cam;
private long lastQrSampleTime; private long lastQrSampleTime;
static {
Webcam.setDriver(new WebcamScanDriver());
}
public WebcamService(WebcamResolution resolution, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) { public WebcamService(WebcamResolution resolution, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
this.resolution = resolution; this.resolution = resolution;
this.listener = listener; this.listener = listener;

View file

@ -29,4 +29,5 @@ open module com.sparrowwallet.sparrow {
requires jtorctl; requires jtorctl;
requires javacsv; requires javacsv;
requires jul.to.slf4j; requires jul.to.slf4j;
requires bridj;
} }

View file

@ -85,7 +85,7 @@
<SeparatorMenuItem /> <SeparatorMenuItem />
<CheckMenuItem fx:id="openWalletsInNewWindows" mnemonicParsing="false" text="Open Wallets in New Windows" onAction="#openWalletsInNewWindows"/> <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="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"/> <CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/>
<SeparatorMenuItem /> <SeparatorMenuItem />
<MenuItem fx:id="minimizeToTray" mnemonicParsing="false" text="Minimize to System Tray" accelerator="Shortcut+Y" onAction="#minimizeToTray"/> <MenuItem fx:id="minimizeToTray" mnemonicParsing="false" text="Minimize to System Tray" accelerator="Shortcut+Y" onAction="#minimizeToTray"/>