diff --git a/pom.xml b/pom.xml
index 3faba6f..9703f41 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
io.github.eduramiba
webcam-capture-driver-native
- 1.0.1
+ 1.1.0
4.0.0
jar
@@ -369,7 +369,11 @@
without-natives
darwin-*
+ linux-*
+ win32-*
**/*.dylib
+ **/*.so
+ **/*.dll
diff --git a/src/main/java/com/github/eduramiba/webcamcapture/drivers/NativeDriver.java b/src/main/java/com/github/eduramiba/webcamcapture/drivers/NativeDriver.java
index ce77766..727b555 100644
--- a/src/main/java/com/github/eduramiba/webcamcapture/drivers/NativeDriver.java
+++ b/src/main/java/com/github/eduramiba/webcamcapture/drivers/NativeDriver.java
@@ -1,44 +1,35 @@
package com.github.eduramiba.webcamcapture.drivers;
import com.github.eduramiba.webcamcapture.drivers.avfoundation.driver.AVFDriver;
-import com.github.eduramiba.webcamcapture.drivers.capturemanager.CaptureManagerDriver;
+import com.github.eduramiba.webcamcapture.drivers.nokhwa.NokhwaDriver;
import com.github.sarxos.webcam.WebcamDevice;
import com.github.sarxos.webcam.WebcamDiscoverySupport;
import com.github.sarxos.webcam.WebcamDriver;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import java.util.Collections;
import java.util.List;
import java.util.Locale;
public class NativeDriver implements WebcamDriver, WebcamDiscoverySupport {
- private static final Logger LOG = LoggerFactory.getLogger(NativeDriver.class);
private final WebcamDriver driver;
+ private final boolean supportScan;
public NativeDriver() {
+ this(true);
+ }
+
+ public NativeDriver(boolean supportScan) {
final String os = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
if ((os.contains("mac")) || (os.contains("darwin"))) {
this.driver = new AVFDriver();
} else if (os.contains("win")) {
- this.driver = new CaptureManagerDriver();
+ this.driver = new NokhwaDriver();
} else {
- // TODO support at least Linux and Raspberry
- LOG.warn("Unsupported OS {}. No devices will be returned!", os);
- this.driver = new WebcamDriver() {
- @Override
- public List getDevices() {
- return Collections.emptyList();
- }
-
- @Override
- public boolean isThreadSafe() {
- return true;
- }
- };
+ this.driver = new NokhwaDriver();
}
+
+ this.supportScan = supportScan;
}
@Override
@@ -58,6 +49,8 @@ public class NativeDriver implements WebcamDriver, WebcamDiscoverySupport {
@Override
public boolean isScanPossible() {
- return true;
+ return supportScan;
}
+
+
}
diff --git a/src/main/java/com/github/eduramiba/webcamcapture/drivers/nokhwa/LibNokhwa.java b/src/main/java/com/github/eduramiba/webcamcapture/drivers/nokhwa/LibNokhwa.java
new file mode 100644
index 0000000..1fb0b56
--- /dev/null
+++ b/src/main/java/com/github/eduramiba/webcamcapture/drivers/nokhwa/LibNokhwa.java
@@ -0,0 +1,72 @@
+package com.github.eduramiba.webcamcapture.drivers.nokhwa;
+
+import com.sun.jna.*;
+
+public interface LibNokhwa extends Library {
+
+ String JNA_LIBRARY_NAME = "cnokhwa";
+ NativeLibrary JNA_NATIVE_LIB = NativeLibrary.getInstance(LibNokhwa.JNA_LIBRARY_NAME);
+ LibNokhwa INSTANCE = Native.loadLibrary(LibNokhwa.JNA_LIBRARY_NAME, LibNokhwa.class);
+
+ // Results:
+ public static final int RESULT_OK = (0);
+ public static final int RESULT_YES = (0);
+ public static final int RESULT_NO = (-256);
+
+ // Errors:
+ public static final int ERROR_DEVICE_NOT_FOUND = (-1);
+ public static final int ERROR_FORMAT_NOT_FOUND = (-2);
+ public static final int ERROR_OPENING_DEVICE = (-3);
+ public static final int ERROR_SESSION_ALREADY_STARTED = (-4);
+ public static final int ERROR_SESSION_NOT_STARTED = (-5);
+ public static final int ERROR_STATE_NOT_INITIALIZED = (-6);
+ public static final int ERROR_READING_CAMERA_SESSION = (-7);
+ public static final int ERROR_READING_FRAME = (-8);
+ public static final int ERROR_DECODING_FRAME = (-9);
+ public static final int ERROR_NO_FRAME_AVAILABLE = (-10);
+ public static final int ERROR_BUFFER_NULL = (-11);
+ public static final int ERROR_BUFFER_NOT_ENOUGH_CAPACITY = (-12);
+
+ // Auth status:
+ public static final int STATUS_AUTHORIZED = (0);
+ public static final int STATUS_DENIED = (-1);
+ public static final int STATUS_NOT_DETERMINED = (-2);
+
+ int cnokhwa_initialize();
+
+ int cnokhwa_has_videocapture_auth();
+
+ void cnokhwa_ask_videocapture_auth();
+
+ int cnokhwa_devices_count();
+
+ int cnokhwa_device_unique_id(int deviceIndex, Pointer pointer, int availableBytes);
+
+ int cnokhwa_device_model_id(int deviceIndex, Pointer pointer, int availableBytes);
+
+ int cnokhwa_device_name(int deviceIndex, Pointer pointer, int availableBytes);
+
+ int cnokhwa_device_formats_count(int deviceIndex);
+
+ int cnokhwa_device_format_width(int deviceIndex, int formatIndex);
+
+ int cnokhwa_device_format_height(int deviceIndex, int formatIndex);
+
+ int cnokhwa_device_format_frame_rate(int deviceIndex, int formatIndex);
+
+ int cnokhwa_device_format_type(int deviceIndex, int formatIndex, Pointer pointer, int availableBytes);
+
+ int cnokhwa_start_capture(int deviceIndex, int width, int height);
+
+ int cnokhwa_stop_capture(int deviceIndex);
+
+ int cnokhwa_has_new_frame(int deviceIndex);
+
+ int cnokhwa_frame_width(int deviceIndex);
+
+ int cnokhwa_frame_height(int deviceIndex);
+
+ int cnokhwa_frame_bytes_per_row(int deviceIndex);
+
+ int cnokhwa_grab_frame(int deviceIndex, Pointer pointer, int availableBytes);
+}
diff --git a/src/main/java/com/github/eduramiba/webcamcapture/drivers/nokhwa/NokhwaDriver.java b/src/main/java/com/github/eduramiba/webcamcapture/drivers/nokhwa/NokhwaDriver.java
new file mode 100644
index 0000000..5d3fd51
--- /dev/null
+++ b/src/main/java/com/github/eduramiba/webcamcapture/drivers/nokhwa/NokhwaDriver.java
@@ -0,0 +1,105 @@
+package com.github.eduramiba.webcamcapture.drivers.nokhwa;
+
+import com.github.sarxos.webcam.WebcamDevice;
+import com.github.sarxos.webcam.WebcamDriver;
+import com.sun.jna.Native;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.*;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.List;
+
+import static com.github.eduramiba.webcamcapture.drivers.nokhwa.LibNokhwa.*;
+
+/**
+ * Driver based on https://github.com/l1npengtul/nokhwa exported through https://github.com/eduramiba/lib-cnokhwa
+ */
+public class NokhwaDriver implements WebcamDriver {
+
+ private static final Logger LOG = LoggerFactory.getLogger(NokhwaDriver.class);
+
+ private static final ByteBuffer buffer = ByteBuffer.allocateDirect(255);
+
+ @Override
+ public synchronized List getDevices() {
+ final var lib = LibNokhwa.INSTANCE;
+
+ final List list = new ArrayList<>();
+
+ if (lib.cnokhwa_initialize() != RESULT_OK) {
+ LOG.error("Error initializing native library");
+ return list;
+ }
+
+ final int devicesCount = lib.cnokhwa_devices_count();
+
+ LOG.info("Available devices: {}", devicesCount);
+
+ if (devicesCount < 1) {
+ return list;
+ }
+
+ final Set availableFormats = new LinkedHashSet<>();
+
+ for (int devIndex = 0; devIndex < devicesCount; devIndex++) {
+ final String uniqueId = deviceUniqueId(devIndex);
+ final String name = deviceName(devIndex);
+
+ final int formatCount = lib.cnokhwa_device_formats_count(devIndex);
+
+ final Set resolutions = new LinkedHashSet<>();
+ int maxFps = 0;
+ for (int formatIndex = 0; formatIndex < formatCount; formatIndex++) {
+ final String formatType = deviceFormatType(devIndex, formatIndex);
+ final int formatWidth = lib.cnokhwa_device_format_width(devIndex, formatIndex);
+ final int formatHeight = lib.cnokhwa_device_format_height(devIndex, formatIndex);
+ final int formatFps = lib.cnokhwa_device_format_frame_rate(devIndex, formatIndex);
+
+ availableFormats.add(String.format("%dx%d %s (%d fps)", formatWidth, formatHeight, formatType, formatFps));
+
+ final Dimension resolution = new Dimension(formatWidth, formatHeight);
+
+ resolutions.add(resolution);
+
+ if (formatFps > maxFps) {
+ maxFps = formatFps;
+ }
+ }
+
+ LOG.info("Found camera {} (id {}) with available formats: {}", name, uniqueId, availableFormats);
+
+ final NokhwaVideoDevice device = new NokhwaVideoDevice(devIndex, uniqueId, name, resolutions, maxFps);
+ if (device.isValid()) {
+ list.add(device);
+ }
+ }
+
+ return list;
+ }
+
+ @Override
+ public boolean isThreadSafe() {
+ return true;
+ }
+
+ private static String deviceUniqueId(final int deviceIndex) {
+ final var bufferP = Native.getDirectBufferPointer(buffer);
+ LibNokhwa.INSTANCE.cnokhwa_device_unique_id(deviceIndex, bufferP, buffer.capacity());
+ return bufferP.getString(0, StandardCharsets.UTF_8.name());
+ }
+
+ private static String deviceName(final int deviceIndex) {
+ final var bufferP = Native.getDirectBufferPointer(buffer);
+ LibNokhwa.INSTANCE.cnokhwa_device_name(deviceIndex, bufferP, buffer.capacity());
+ return bufferP.getString(0, StandardCharsets.UTF_8.name());
+ }
+
+ private static String deviceFormatType(final int deviceIndex, final int formatIndex) {
+ final var bufferP = Native.getDirectBufferPointer(buffer);
+ LibNokhwa.INSTANCE.cnokhwa_device_format_type(deviceIndex, formatIndex, bufferP, buffer.capacity());
+ return bufferP.getString(0, StandardCharsets.UTF_8.name());
+ }
+}
diff --git a/src/main/java/com/github/eduramiba/webcamcapture/drivers/nokhwa/NokhwaVideoDevice.java b/src/main/java/com/github/eduramiba/webcamcapture/drivers/nokhwa/NokhwaVideoDevice.java
new file mode 100644
index 0000000..9ffa08b
--- /dev/null
+++ b/src/main/java/com/github/eduramiba/webcamcapture/drivers/nokhwa/NokhwaVideoDevice.java
@@ -0,0 +1,316 @@
+package com.github.eduramiba.webcamcapture.drivers.nokhwa;
+
+import com.github.eduramiba.webcamcapture.drivers.WebcamDeviceExtended;
+import com.sun.jna.Native;
+import javafx.scene.image.PixelFormat;
+import javafx.scene.image.PixelWriter;
+import javafx.scene.image.WritableImage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.*;
+import java.awt.image.*;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collection;
+
+import static com.github.eduramiba.webcamcapture.drivers.nokhwa.LibNokhwa.*;
+
+
+public class NokhwaVideoDevice implements WebcamDeviceExtended {
+ private static final Logger LOG = LoggerFactory.getLogger(NokhwaVideoDevice.class);
+
+ private final int deviceIndex;
+ private final String id;
+ private final String name;
+ private final Dimension[] resolutions;
+ private Dimension resolution;
+ private final int maxFps;
+
+ //State:
+ private boolean open = false;
+ private int bytesPerRow = -1;
+ private ByteBuffer imgBuffer = null;
+ private byte[] arrayByteBuffer = null;
+ private BufferedImage bufferedImage = null;
+ private long lastFrameTimestamp = -1;
+
+ public NokhwaVideoDevice(final int deviceIndex, final String id, final String name, final Collection resolutions, final int maxFps) {
+ this.deviceIndex = deviceIndex;
+ this.id = id;
+ this.name = name;
+ this.resolutions = resolutions != null ? resolutions.toArray(new Dimension[0]) : new Dimension[0];
+ this.resolution = bestResolution(this.resolutions);
+ this.maxFps = maxFps;
+ }
+
+ public boolean isValid() {
+ return resolution != null && resolution.width > 0 && resolution.height > 0;
+ }
+
+ private Dimension bestResolution(final Dimension[] resolutions) {
+ Dimension best = null;
+ int bestPixels = 0;
+
+ for (Dimension dim : resolutions) {
+ int px = dim.width * dim.height;
+
+ if (px > bestPixels) {
+ best = dim;
+ bestPixels = px;
+ }
+ }
+
+ return best;
+ }
+
+ public int getDeviceIndex() {
+ return deviceIndex;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Dimension[] getResolutions() {
+ return resolutions;
+ }
+
+ @Override
+ public Dimension getResolution() {
+ return resolution;
+ }
+
+ @Override
+ public void setResolution(Dimension resolution) {
+ if (isOpen()) {
+ return;
+ }
+
+ this.resolution = resolution;
+ }
+
+ @Override
+ public BufferedImage getImage() {
+ return getImage(imgBuffer);
+ }
+
+ @Override
+ public synchronized void open() {
+ if (isOpen()) {
+ return;
+ }
+
+ final var lib = LibNokhwa.INSTANCE;
+ final int authStatus = lib.cnokhwa_has_videocapture_auth();
+
+ if (authStatus != STATUS_AUTHORIZED) {
+ LOG.warn("Capture auth status = {}", authStatus);
+ }
+
+ if (authStatus != STATUS_AUTHORIZED) {
+ lib.cnokhwa_ask_videocapture_auth();
+ }
+
+ final int width = resolution.width;
+ final int height = resolution.height;
+ final int startResult = lib.cnokhwa_start_capture(deviceIndex, width, height);
+
+ if (startResult < 0) {
+ LOG.error("Error capture start result for device {} = {}", id, startResult);
+ return;
+ }
+
+ this.open = true;
+ this.bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
+
+ LOG.info("Device {} opened successfully", id);
+ }
+
+ @Override
+ public synchronized void close() {
+ if (isOpen()) {
+ LibNokhwa.INSTANCE.cnokhwa_stop_capture(deviceIndex);
+ open = false;
+ bytesPerRow = -1;
+ imgBuffer = null;
+ arrayByteBuffer = null;
+ bufferedImage = null;
+ }
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public boolean isOpen() {
+ return open;
+ }
+
+ @Override
+ public long getLastFrameTimestamp() {
+ return lastFrameTimestamp;
+ }
+
+ @Override
+ public double getFPS() {
+ return maxFps;
+ }
+
+ @Override
+ public synchronized ByteBuffer getImageBytes() {
+ if (!isOpen()) {
+ return null;
+ }
+
+ updateBuffer();
+
+ return imgBuffer;
+ }
+
+ @Override
+ public synchronized void getImageBytes(final ByteBuffer target) {
+ if (!isOpen()) {
+ return;
+ }
+
+ if (target.remaining() < imgBuffer.capacity()) {
+ LOG.error("At least {} bytes needed but passed buffer has only {} remaining size", imgBuffer.capacity(), target.capacity());
+ return;
+ }
+
+ updateBuffer();
+ imgBuffer.rewind();
+ target.put(imgBuffer);
+ }
+
+ @Override
+ public String toString() {
+ return "AVFVideoDevice{" +
+ "deviceIndex=" + deviceIndex +
+ ", id='" + id + '\'' +
+ ", name='" + name + '\'' +
+ ", resolutions=" + Arrays.toString(resolutions) +
+ '}';
+ }
+
+ @Override
+ public synchronized BufferedImage getImage(ByteBuffer byteBuffer) {
+ if (!isOpen()) {
+ return null;
+ }
+
+ updateBuffer();
+ updateBufferedImage();
+
+ return bufferedImage;
+ }
+
+ @Override
+ public boolean updateFXIMage(WritableImage writableImage) {
+ return updateFXIMage(writableImage, -1);
+ }
+
+ @Override
+ public synchronized boolean updateFXIMage(final WritableImage writableImage, final long lastFrameTimestamp) {
+ if (!isOpen()) {
+ return false;
+ }
+
+ updateBuffer();
+
+ if (imgBuffer == null) {
+ return false;
+ }
+
+ if (this.lastFrameTimestamp <= lastFrameTimestamp) {
+ return false;
+ }
+
+ final int videoWidth = resolution.width;
+ final int videoHeight = resolution.height;
+
+ final PixelWriter pw = writableImage.getPixelWriter();
+
+ imgBuffer.mark();
+ imgBuffer.position(0);
+ pw.setPixels(
+ 0, 0, videoWidth, videoHeight,
+ PixelFormat.getByteRgbInstance(), imgBuffer, bytesPerRow
+ );
+
+ return true;
+ }
+
+ private void updateBuffer() {
+ final int hasFrameResult = LibNokhwa.INSTANCE.cnokhwa_has_new_frame(deviceIndex);
+
+ if (hasFrameResult == RESULT_YES) {
+ if (imgBuffer == null) {
+ // Init buffer if still not initialized:
+ this.bytesPerRow = LibNokhwa.INSTANCE.cnokhwa_frame_bytes_per_row(deviceIndex);
+
+ final var bufferSizeBytes = bytesPerRow * resolution.height;
+ this.imgBuffer = ByteBuffer.allocateDirect(bufferSizeBytes);
+ this.arrayByteBuffer = new byte[imgBuffer.capacity()];
+ }
+
+ final int grabResult = LibNokhwa.INSTANCE.cnokhwa_grab_frame(
+ deviceIndex,
+ Native.getDirectBufferPointer(imgBuffer), imgBuffer.capacity()
+ );
+
+ if (grabResult == RESULT_OK) {
+ lastFrameTimestamp = System.currentTimeMillis();
+ } else {
+ LOG.error("Error grabbing frame = {}", grabResult);
+ }
+ } else {
+ if (hasFrameResult != RESULT_NO) {
+ LOG.error("Error checking for new frame = {}", hasFrameResult);
+ }
+ }
+ }
+
+ private void updateBufferedImage() {
+ if (!isOpen() || imgBuffer == null) {
+ return;
+ }
+
+ final int videoWidth = resolution.width;
+ final int videoHeight = resolution.height;
+
+ final ComponentSampleModel sampleModel = new ComponentSampleModel(
+ DataBuffer.TYPE_BYTE, videoWidth, videoHeight, 3, bytesPerRow,
+ new int[]{0, 1, 2}
+ );
+
+ imgBuffer.mark();
+ imgBuffer.position(0);
+ imgBuffer.get(arrayByteBuffer, 0, imgBuffer.capacity());
+ imgBuffer.reset();
+
+ final DataBuffer dataBuffer = new DataBufferByte(arrayByteBuffer, arrayByteBuffer.length);
+ final Raster raster = Raster.createRaster(sampleModel, dataBuffer, null);
+ bufferedImage.setData(raster);
+ }
+
+ @Override
+ public void addCustomEventsListener(Listener listener) {
+ // NOOP. To be improved with custom events from nokhwa lib
+ }
+
+ @Override
+ public boolean removeCustomEventsListener(Listener listener) {
+ // NOOP
+ return true;
+ }
+}
diff --git a/src/main/resources/linux-x86-64/libcnokhwa.so b/src/main/resources/linux-x86-64/libcnokhwa.so
new file mode 100755
index 0000000..d96c6a6
Binary files /dev/null and b/src/main/resources/linux-x86-64/libcnokhwa.so differ
diff --git a/src/main/resources/linux-x86/libcnokhwa.so b/src/main/resources/linux-x86/libcnokhwa.so
new file mode 100755
index 0000000..fa8049a
Binary files /dev/null and b/src/main/resources/linux-x86/libcnokhwa.so differ