diff --git a/webcam-capture-drivers/driver-ffmpeg-cli/src/main/java/com/github/sarxos/webcam/ds/ffmpegcli/FFmpegCliDevice.java b/webcam-capture-drivers/driver-ffmpeg-cli/src/main/java/com/github/sarxos/webcam/ds/ffmpegcli/FFmpegCliDevice.java index 6c8e1887..c27c8b96 100644 --- a/webcam-capture-drivers/driver-ffmpeg-cli/src/main/java/com/github/sarxos/webcam/ds/ffmpegcli/FFmpegCliDevice.java +++ b/webcam-capture-drivers/driver-ffmpeg-cli/src/main/java/com/github/sarxos/webcam/ds/ffmpegcli/FFmpegCliDevice.java @@ -138,6 +138,14 @@ public synchronized ByteBuffer getImageBytes() { return ByteBuffer.wrap(readBytes()); } + @Override + public void getImageBytes(ByteBuffer buffer) { + if (!open.get()) { + return; + } + buffer.put(readBytes()); + } + @Override public BufferedImage getImage() { diff --git a/webcam-capture/src/main/java/com/github/sarxos/webcam/Webcam.java b/webcam-capture/src/main/java/com/github/sarxos/webcam/Webcam.java index 8533a2a2..f59b45a0 100644 --- a/webcam-capture/src/main/java/com/github/sarxos/webcam/Webcam.java +++ b/webcam-capture/src/main/java/com/github/sarxos/webcam/Webcam.java @@ -24,10 +24,10 @@ import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDriver; import com.github.sarxos.webcam.ds.cgt.WebcamCloseTask; import com.github.sarxos.webcam.ds.cgt.WebcamDisposeTask; +import com.github.sarxos.webcam.ds.cgt.WebcamGetBufferTask; +import com.github.sarxos.webcam.ds.cgt.WebcamGetImageTask; import com.github.sarxos.webcam.ds.cgt.WebcamOpenTask; import com.github.sarxos.webcam.ds.cgt.WebcamReadBufferTask; -import com.github.sarxos.webcam.ds.cgt.WebcamReadImageTask; -import com.github.sarxos.webcam.util.ImageUtils; /** @@ -627,7 +627,7 @@ public BufferedImage getImage() { // get image t1 = System.currentTimeMillis(); - BufferedImage image = transform(new WebcamReadImageTask(driver, device).getImage()); + BufferedImage image = transform(new WebcamGetImageTask(driver, device).getImage()); t2 = System.currentTimeMillis(); if (image == null) { @@ -694,14 +694,43 @@ public ByteBuffer getImageBytes() { // buffers, just convert image to RGB byte array if (device instanceof BufferAccess) { - return new WebcamReadBufferTask(driver, device).getBuffer(); + return new WebcamGetBufferTask(driver, device).getBuffer(); } else { - BufferedImage image = getImage(); - if (image != null) { - return ByteBuffer.wrap(ImageUtils.toRawByteArray(image)); - } else { - return null; - } + throw new IllegalStateException(String.format("Driver %s does not support buffer access", driver.getClass().getName())); + } + } + + /** + * Get RAW image ByteBuffer. It will always return buffer with 3 x 1 bytes + * per each pixel, where RGB components are on (0, 1, 2) and color space is + * sRGB.
+ *
+ * + * IMPORTANT!
+ * Some drivers can return direct ByteBuffer, so there is no guarantee that + * underlying bytes will not be released in next read image operation. + * Therefore, to avoid potential bugs you should convert this ByteBuffer to + * bytes array before you fetch next image. + * + * @return Byte buffer + */ + public void getImageBytes(ByteBuffer target) { + + if (!isReady()) { + return; + } + + assert driver != null; + assert device != null; + + // some devices can support direct image buffers, and for those call + // processor task, and for those which does not support direct image + // buffers, just convert image to RGB byte array + + if (device instanceof BufferAccess) { + new WebcamReadBufferTask(driver, device, target).readBuffer(); + } else { + throw new IllegalStateException(String.format("Driver %s does not support buffer access", driver.getClass().getName())); } } diff --git a/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamDevice.java b/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamDevice.java index f4720b7b..2211032e 100644 --- a/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamDevice.java +++ b/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamDevice.java @@ -22,13 +22,27 @@ public interface WebcamDevice { public static interface BufferAccess { /** - * Get image in form of raw bytes. Do not use this buffer to set - * bytes value, it should be used only for read purpose! + * Read the underlying image memory buffer. This method will return new + * reference to pre-allocated off-heap memory where image bytes are + * stored. The size of this buffer is image width * height * 3 bytes.
+ *
+ * + * NOTE! Do not use this buffer to set bytes value. It + * should be used only for read purpose! * * @return Bytes buffer */ ByteBuffer getImageBytes(); + /** + * Copy the underlying image memory into the target buffer passed as the + * argument.The remaining capacity of the target buffer needs to be at + * least image width * height * 3 bytes. + * + * @param target the buffer to which image data should be copied + */ + void getImageBytes(ByteBuffer target); + } public static interface FPSSource { diff --git a/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamUpdater.java b/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamUpdater.java index 093a619d..14085f49 100644 --- a/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamUpdater.java +++ b/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamUpdater.java @@ -13,7 +13,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.github.sarxos.webcam.ds.cgt.WebcamReadImageTask; +import com.github.sarxos.webcam.ds.cgt.WebcamGetImageTask; /** @@ -99,7 +99,7 @@ public void start() { if (running.compareAndSet(false, true)) { - image.set(new WebcamReadImageTask(Webcam.getDriver(), webcam.getDevice()).getImage()); + image.set(new WebcamGetImageTask(Webcam.getDriver(), webcam.getDevice()).getImage()); executor = Executors.newSingleThreadScheduledExecutor(THREAD_FACTORY); executor.execute(this); @@ -166,7 +166,7 @@ private void tick() { BufferedImage img = null; t1 = System.currentTimeMillis(); - img = webcam.transform(new WebcamReadImageTask(driver, device).getImage()); + img = webcam.transform(new WebcamGetImageTask(driver, device).getImage()); t2 = System.currentTimeMillis(); image.set(img); diff --git a/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/buildin/WebcamDefaultDevice.java b/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/buildin/WebcamDefaultDevice.java index a96b1260..790e9685 100644 --- a/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/buildin/WebcamDefaultDevice.java +++ b/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/buildin/WebcamDefaultDevice.java @@ -14,7 +14,6 @@ import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; import org.bridj.Pointer; import org.slf4j.Logger; @@ -75,6 +74,7 @@ protected void handle() { grabber.setTimeout(timeout); result.set(grabber.nextFrame()); + fresh.set(true); } } @@ -119,9 +119,9 @@ protected void handle() { private final AtomicBoolean open = new AtomicBoolean(false); /** - * When last frame was requested. + * Is the last image fresh one. */ - private final AtomicLong timestamp = new AtomicLong(-1); + private final AtomicBoolean fresh = new AtomicBoolean(false); private Thread refresher = null; @@ -132,6 +132,9 @@ protected void handle() { private long t1 = -1; private long t2 = -1; + /** + * Current FPS. + */ private volatile double fps = 0; protected WebcamDefaultDevice(Device device) { @@ -180,17 +183,24 @@ public ByteBuffer getImageBytes() { LOG.debug("Webcam is disposed, image will be null"); return null; } - if (!open.get()) { LOG.debug("Webcam is closed, image will be null"); return null; } - LOG.trace("Webcam device get image (next frame)"); + // if image is not fresh, update it + + if (fresh.compareAndSet(false, true)) { + updateFrameBuffer(); + } // get image buffer + LOG.trace("Webcam grabber get image pointer"); + Pointer image = grabber.getImage(); + fresh.set(false); + if (image == null) { LOG.warn("Null array pointer found instead of image"); return null; @@ -203,6 +213,50 @@ public ByteBuffer getImageBytes() { return image.getByteBuffer(length); } + @Override + public void getImageBytes(ByteBuffer target) { + + if (disposed.get()) { + LOG.debug("Webcam is disposed, image will be null"); + return; + } + if (!open.get()) { + LOG.debug("Webcam is closed, image will be null"); + return; + } + + int minSize = size.width * 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)); + } + + // if image is not fresh, update it + + if (fresh.compareAndSet(false, true)) { + updateFrameBuffer(); + } + + // get image buffer + + LOG.trace("Webcam grabber get image pointer"); + + Pointer image = grabber.getImage(); + fresh.set(false); + + if (image == null) { + LOG.warn("Null array pointer found instead of image"); + return; + } + + LOG.trace("Webcam device read buffer {} bytes", minSize); + + image = image.validBytes(minSize); + image.getBytes(target); + + } + @Override public BufferedImage getImage() { @@ -301,8 +355,6 @@ public void open() { } while (++i < 3); - timestamp.set(System.currentTimeMillis()); - LOG.debug("Webcam device {} is now open", this); open.set(true); @@ -370,11 +422,35 @@ public void setTimeout(int timeout) { this.timeout = timeout; } + /** + * Update underlying memory buffer and fetch new frame. + */ + private void updateFrameBuffer() { + + LOG.trace("Next frame"); + + if (t1 == -1 || t2 == -1) { + t1 = System.currentTimeMillis(); + t2 = System.currentTimeMillis(); + } + + int result = new NextFrameTask(this).nextFrame(); + + t1 = t2; + t2 = System.currentTimeMillis(); + + fps = (4 * fps + 1000 / (t2 - t1 + 1)) / 5; + + if (result == -1) { + LOG.error("Timeout when requesting image!"); + } else if (result < -1) { + LOG.error("Error requesting new frame!"); + } + } + @Override public void run() { - int result = -1; - do { if (Thread.interrupted()) { @@ -387,27 +463,7 @@ public void run() { return; } - LOG.trace("Next frame"); - - if (t1 == -1 || t2 == -1) { - t1 = System.currentTimeMillis(); - t2 = System.currentTimeMillis(); - } - - result = new NextFrameTask(this).nextFrame(); - - t1 = t2; - t2 = System.currentTimeMillis(); - - fps = (4 * fps + 1000 / (t2 - t1 + 1)) / 5; - - if (result == -1) { - LOG.error("Timeout when requesting image!"); - } else if (result < -1) { - LOG.error("Error requesting new frame!"); - } - - timestamp.set(System.currentTimeMillis()); + updateFrameBuffer(); } while (open.get()); } diff --git a/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/cgt/WebcamGetBufferTask.java b/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/cgt/WebcamGetBufferTask.java new file mode 100644 index 00000000..1498e4d6 --- /dev/null +++ b/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/cgt/WebcamGetBufferTask.java @@ -0,0 +1,48 @@ +package com.github.sarxos.webcam.ds.cgt; + +import java.nio.ByteBuffer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.sarxos.webcam.WebcamDevice; +import com.github.sarxos.webcam.WebcamDevice.BufferAccess; +import com.github.sarxos.webcam.WebcamDriver; +import com.github.sarxos.webcam.WebcamTask; + + +public class WebcamGetBufferTask extends WebcamTask { + + private static final Logger LOG = LoggerFactory.getLogger(WebcamGetBufferTask.class); + + private volatile ByteBuffer buffer = null; + + public WebcamGetBufferTask(WebcamDriver driver, WebcamDevice device) { + super(driver, device); + } + + public ByteBuffer getBuffer() { + try { + process(); + } catch (InterruptedException e) { + LOG.debug("Image buffer request interrupted", e); + return null; + } + return buffer; + } + + @Override + protected void handle() { + + WebcamDevice device = getDevice(); + if (!device.isOpen()) { + return; + } + + if (!(device instanceof BufferAccess)) { + return; + } + + buffer = ((BufferAccess) device).getImageBytes(); + } +} diff --git a/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/cgt/WebcamReadImageTask.java b/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/cgt/WebcamGetImageTask.java similarity index 83% rename from webcam-capture/src/main/java/com/github/sarxos/webcam/ds/cgt/WebcamReadImageTask.java rename to webcam-capture/src/main/java/com/github/sarxos/webcam/ds/cgt/WebcamGetImageTask.java index 903375fb..bf09ef7b 100644 --- a/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/cgt/WebcamReadImageTask.java +++ b/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/cgt/WebcamGetImageTask.java @@ -10,13 +10,13 @@ import com.github.sarxos.webcam.WebcamTask; -public class WebcamReadImageTask extends WebcamTask { +public class WebcamGetImageTask extends WebcamTask { - private static final Logger LOG = LoggerFactory.getLogger(WebcamReadImageTask.class); + private static final Logger LOG = LoggerFactory.getLogger(WebcamGetImageTask.class); private volatile BufferedImage image = null; - public WebcamReadImageTask(WebcamDriver driver, WebcamDevice device) { + public WebcamGetImageTask(WebcamDriver driver, WebcamDevice device) { super(driver, device); } diff --git a/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/cgt/WebcamReadBufferTask.java b/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/cgt/WebcamReadBufferTask.java index f7e643f8..343dc43b 100644 --- a/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/cgt/WebcamReadBufferTask.java +++ b/webcam-capture/src/main/java/com/github/sarxos/webcam/ds/cgt/WebcamReadBufferTask.java @@ -2,9 +2,6 @@ import java.nio.ByteBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.github.sarxos.webcam.WebcamDevice; import com.github.sarxos.webcam.WebcamDevice.BufferAccess; import com.github.sarxos.webcam.WebcamDriver; @@ -13,22 +10,20 @@ public class WebcamReadBufferTask extends WebcamTask { - private static final Logger LOG = LoggerFactory.getLogger(WebcamReadBufferTask.class); - - private volatile ByteBuffer buffer = null; + private volatile ByteBuffer target = null; - public WebcamReadBufferTask(WebcamDriver driver, WebcamDevice device) { + public WebcamReadBufferTask(WebcamDriver driver, WebcamDevice device, ByteBuffer target) { super(driver, device); + this.target = target; } - public ByteBuffer getBuffer() { + public ByteBuffer readBuffer() { try { process(); } catch (InterruptedException e) { - LOG.debug("Image buffer request interrupted", e); return null; } - return buffer; + return target; } @Override @@ -43,6 +38,6 @@ protected void handle() { return; } - buffer = ((BufferAccess) device).getImageBytes(); + ((BufferAccess) device).getImageBytes(target); } } diff --git a/webcam-capture/src/main/java/com/github/sarxos/webcam/util/ImageUtils.java b/webcam-capture/src/main/java/com/github/sarxos/webcam/util/ImageUtils.java index 8b70f4aa..7ee2980a 100644 --- a/webcam-capture/src/main/java/com/github/sarxos/webcam/util/ImageUtils.java +++ b/webcam-capture/src/main/java/com/github/sarxos/webcam/util/ImageUtils.java @@ -1,8 +1,6 @@ package com.github.sarxos.webcam.util; import java.awt.image.BufferedImage; -import java.awt.image.DataBuffer; -import java.awt.image.DataBufferByte; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -65,38 +63,4 @@ public static byte[] toByteArray(BufferedImage image, String format) { return bytes; } - - public static byte[] toRawByteArray(BufferedImage image) { - - DataBuffer dbuf = image.getRaster().getDataBuffer(); - - if (dbuf instanceof DataBufferByte) { - - return ((DataBufferByte) dbuf).getData(); - - } else { - - int w = image.getWidth(); - int h = image.getHeight(); - int n = w * h; - - byte[] bytes = new byte[n * 3]; - - int i, x, y, rgb; - - for (i = 0; i < n; i++) { - - x = i % w; - y = i / h; - - rgb = image.getRGB(x, y); - - bytes[i * 3 + 0] = (byte) ((rgb >> 16) & 0xff); - bytes[i * 3 + 1] = (byte) ((rgb >> 8) & 0xff); - bytes[i * 3 + 2] = (byte) (rgb & 0xff); - } - - return bytes; - } - } }