Add driver based in nokhwa library

This commit is contained in:
Eduardo Ramos 2024-11-25 12:40:32 +01:00
parent ae8c94587b
commit 740d19099f
7 changed files with 511 additions and 21 deletions

View File

@ -3,7 +3,7 @@
<groupId>io.github.eduramiba</groupId>
<artifactId>webcam-capture-driver-native</artifactId>
<version>1.0.1</version>
<version>1.1.0</version>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
@ -369,7 +369,11 @@
<classifier>without-natives</classifier>
<excludes>
<exclude>darwin-*</exclude>
<exclude>linux-*</exclude>
<exclude>win32-*</exclude>
<exclude>**/*.dylib</exclude>
<exclude>**/*.so</exclude>
<exclude>**/*.dll</exclude>
</excludes>
</configuration>
</execution>

View File

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

View File

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

View File

@ -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<WebcamDevice> getDevices() {
final var lib = LibNokhwa.INSTANCE;
final List<WebcamDevice> 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<String> 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<Dimension> 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());
}
}

View File

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

Binary file not shown.

Binary file not shown.