Add driver based in nokhwa library
This commit is contained in:
parent
ae8c94587b
commit
740d19099f
6
pom.xml
6
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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.
Loading…
Reference in New Issue