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