diff --git a/README.md b/README.md index 3782d11..4ed52d6 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,23 @@ This is a native driver for [Webcam Capture](https://github.com/sarxos/webcam-capture) that is reliable, has very good performance, fast startup time and is able to correctly list the detailed capabilities of video devices such as resolutions and device IDs. -Currently it works on Windows only, with the `CaptureManagerDriver`, based on [CaptureManager-SDK](https://www.codeproject.com/Articles/1017223/CaptureManager-SDK-Capturing-Recording-and-Streami), which uses the MediaFoundation Windows API. +Currently it works on Windows and Mac. + +For Windows, it uses the `CaptureManagerDriver`, based on [CaptureManager-SDK](https://www.codeproject.com/Articles/1017223/CaptureManager-SDK-Capturing-Recording-and-Streami), which uses the MediaFoundation Windows API. +For Mac, it uses `AVFDriver`, based on a [custom library](https://github.com/eduramiba/libvideocapture-avfoundation) that uses [AVFoundation](https://developer.apple.com/av-foundation/). # How to use 1. Download this repository and run `mvn install` 2. Add `com.github.eduramiba:webcam-capture-driver-native:1.0.0-SNAPSHOT` dependency to your application. -3. Copy the DLLs of the `natives` folder for your system into the java library path. -4. Use the driver with `Webcam.setDriver(new CaptureManagerDriver())` -5. List the devices with `Webcam.getWebcams()` as normal and use the library in your preferred way. In JavaFX it's recommended to do it as in the example below. +3. Use the driver with `Webcam.setDriver(new NativeDriver())` +4. List the devices with `Webcam.getWebcams()` as normal and use the library in your preferred way. In JavaFX it's recommended to do it as in the example below. # Simple example with JavaFX ```java +import com.github.eduramiba.webcamcapture.drivers.NativeDriver; import com.github.eduramiba.webcamcapture.drivers.WebcamDeviceWithBufferOperations; -import com.github.eduramiba.webcamcapture.drivers.capturemanager.CaptureManagerDriver; import com.github.sarxos.webcam.Webcam; import com.github.sarxos.webcam.WebcamDevice; import java.util.concurrent.Executors; @@ -32,62 +34,73 @@ import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class TestCaptureManagerDriver extends Application { +public class TestDriver extends Application { + + private static final Logger LOG = LoggerFactory.getLogger(TestDriver.class); - private static final Logger LOG = LoggerFactory.getLogger(TestCaptureManagerDriver.class); - public static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(4); public static void main(String[] args) { - Webcam.setDriver(new CaptureManagerDriver()); + Webcam.setDriver(new NativeDriver()); launch(args); } @Override public void start(Stage stage) throws Exception { - ImageView imageView = new ImageView(); - HBox root = new HBox(); + final ImageView imageView = new ImageView(); + final HBox root = new HBox(); root.getChildren().add(imageView); Webcam.getWebcams().stream() - .findFirst() - .ifPresent((final Webcam camera) -> { - final WebcamDevice device = camera.getDevice(); - LOG.info("Found camera: {}, device = {}", camera, device); + .findFirst() + .ifPresent((final Webcam camera) -> { + final WebcamDevice device = camera.getDevice(); + LOG.info("Found camera: {}, device = {}", camera, device); - final int width = device.getResolution().width; - final int height = device.getResolution().height; - final WritableImage fxImage = new WritableImage(width, height); - Platform.runLater(() -> { - imageView.setImage(fxImage); - }); - - camera.open(); - if (device instanceof WebcamDeviceWithBufferOperations) { - EXECUTOR.scheduleAtFixedRate(() -> { - ((WebcamDeviceWithBufferOperations) device).updateFXIMage(fxImage); - }, 0, 16, TimeUnit.MILLISECONDS); - } + final int width = device.getResolution().width; + final int height = device.getResolution().height; + final WritableImage fxImage = new WritableImage(width, height); + Platform.runLater(() -> { + imageView.setImage(fxImage); + stage.setWidth(width); + stage.setHeight(height); + stage.centerOnScreen(); }); + camera.getLock().disable(); + camera.open(); + if (device instanceof WebcamDeviceWithBufferOperations) { + EXECUTOR.scheduleAtFixedRate(() -> { + ((WebcamDeviceWithBufferOperations) device).updateFXIMage(fxImage); + }, 0, 16, TimeUnit.MILLISECONDS); + } + }); + + stage.setOnCloseRequest(t -> { + Platform.exit(); + System.exit(0); + }); + // Create the Scene - Scene scene = new Scene(root); - // Add the scene to the Stage + final Scene scene = new Scene(root); stage.setScene(scene); - // Set the title of the Stage - stage.setTitle("Displaying an Image"); - // Display the Stage + stage.setTitle("Webcam example"); stage.show(); } } + ``` # Future work * Publish this as a maven central artifact. At the moment you will need to build it yourself. -* Implement MacOS and Linux native driver that uses LibUVC. +* Implement Linux driver # Notes -The source code in `natives` folder and `capturemanager` java package has been copied from [CaptureManager-SDK](https://www.codeproject.com/Articles/1017223/CaptureManager-SDK-Capturing-Recording-and-Streami) and slightly improved for this driver. \ No newline at end of file +The source code in `natives` folder and `capturemanager` java package has been copied from [CaptureManager-SDK](https://www.codeproject.com/Articles/1017223/CaptureManager-SDK-Capturing-Recording-and-Streami) and slightly improved for this driver. This code is not idiomatic java and needs improvement. +The DLLs for Windows can just be copied along with your program. + +The native dynamic libraries for Mac are on `src/main/resources` and loaded by JNA from inside the JAR. +Note that if you want to distribute a Mac app you will need to properly codesign the dylib files with entitlements, have an Info.plist, notarization... diff --git a/pom.xml b/pom.xml index 4831822..a315134 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,7 @@ 0.3.13-SNAPSHOT 1.3.2 + 5.12.1 19 @@ -55,6 +56,17 @@ ${driver.webcam-capture.version} + + net.java.dev.jna + jna + ${driver.jna.version} + + + net.java.dev.jna + jna-platform + ${driver.jna.version} + + com.fasterxml aalto-xml @@ -82,6 +94,15 @@ webcam-capture + + net.java.dev.jna + jna + + + net.java.dev.jna + jna-platform + + com.fasterxml aalto-xml diff --git a/src/main/java/com/github/eduramiba/webcamcapture/TestCaptureManagerDriver.java b/src/main/java/com/github/eduramiba/webcamcapture/TestDriver.java similarity index 74% rename from src/main/java/com/github/eduramiba/webcamcapture/TestCaptureManagerDriver.java rename to src/main/java/com/github/eduramiba/webcamcapture/TestDriver.java index 1dbcfbb..2811130 100644 --- a/src/main/java/com/github/eduramiba/webcamcapture/TestCaptureManagerDriver.java +++ b/src/main/java/com/github/eduramiba/webcamcapture/TestDriver.java @@ -1,7 +1,7 @@ package com.github.eduramiba.webcamcapture; +import com.github.eduramiba.webcamcapture.drivers.NativeDriver; import com.github.eduramiba.webcamcapture.drivers.WebcamDeviceWithBufferOperations; -import com.github.eduramiba.webcamcapture.drivers.capturemanager.CaptureManagerDriver; import com.github.sarxos.webcam.Webcam; import com.github.sarxos.webcam.WebcamDevice; import java.util.concurrent.Executors; @@ -17,22 +17,22 @@ import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class TestCaptureManagerDriver extends Application { +public class TestDriver extends Application { - private static final Logger LOG = LoggerFactory.getLogger(TestCaptureManagerDriver.class); + private static final Logger LOG = LoggerFactory.getLogger(TestDriver.class); public static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(4); public static void main(String[] args) { - Webcam.setDriver(new CaptureManagerDriver()); + Webcam.setDriver(new NativeDriver()); launch(args); } @Override public void start(Stage stage) throws Exception { - ImageView imageView = new ImageView(); - HBox root = new HBox(); + final ImageView imageView = new ImageView(); + final HBox root = new HBox(); root.getChildren().add(imageView); Webcam.getWebcams().stream() @@ -46,8 +46,12 @@ public class TestCaptureManagerDriver extends Application { final WritableImage fxImage = new WritableImage(width, height); Platform.runLater(() -> { imageView.setImage(fxImage); + stage.setWidth(width); + stage.setHeight(height); + stage.centerOnScreen(); }); + camera.getLock().disable(); camera.open(); if (device instanceof WebcamDeviceWithBufferOperations) { EXECUTOR.scheduleAtFixedRate(() -> { @@ -56,13 +60,15 @@ public class TestCaptureManagerDriver extends Application { } }); + stage.setOnCloseRequest(t -> { + Platform.exit(); + System.exit(0); + }); + // Create the Scene - Scene scene = new Scene(root); - // Add the scene to the Stage + final Scene scene = new Scene(root); stage.setScene(scene); - // Set the title of the Stage - stage.setTitle("Displaying an Image"); - // Display the Stage + stage.setTitle("Webcam example"); stage.show(); } } diff --git a/src/main/java/com/github/eduramiba/webcamcapture/drivers/NativeDriver.java b/src/main/java/com/github/eduramiba/webcamcapture/drivers/NativeDriver.java new file mode 100644 index 0000000..8473648 --- /dev/null +++ b/src/main/java/com/github/eduramiba/webcamcapture/drivers/NativeDriver.java @@ -0,0 +1,36 @@ +package com.github.eduramiba.webcamcapture.drivers; + +import java.util.List; +import java.util.Locale; + +import com.github.eduramiba.webcamcapture.drivers.avfoundation.driver.AVFDriver; +import com.github.eduramiba.webcamcapture.drivers.capturemanager.CaptureManagerDriver; +import com.github.sarxos.webcam.WebcamDevice; +import com.github.sarxos.webcam.WebcamDriver; + +public class NativeDriver implements WebcamDriver { + + private final WebcamDriver driver; + + public NativeDriver() { + final String os = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH); + + if ((os.indexOf("mac") >= 0) || (os.indexOf("darwin") >= 0)) { + this.driver = new AVFDriver(); + } else if (os.indexOf("win") >= 0) { + this.driver = new CaptureManagerDriver(); + } else { + throw new IllegalStateException("Unsupported OS = " + os); + } + } + + @Override + public List getDevices() { + return driver.getDevices(); + } + + @Override + public boolean isThreadSafe() { + return driver.isThreadSafe(); + } +} diff --git a/src/main/java/com/github/eduramiba/webcamcapture/drivers/avfoundation/driver/AVFDriver.java b/src/main/java/com/github/eduramiba/webcamcapture/drivers/avfoundation/driver/AVFDriver.java new file mode 100644 index 0000000..dbf969e --- /dev/null +++ b/src/main/java/com/github/eduramiba/webcamcapture/drivers/avfoundation/driver/AVFDriver.java @@ -0,0 +1,102 @@ +package com.github.eduramiba.webcamcapture.drivers.avfoundation.driver; + +import java.awt.*; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +import com.github.sarxos.webcam.WebcamDevice; +import com.github.sarxos.webcam.WebcamDriver; +import com.sun.jna.Native; + +public class AVFDriver implements WebcamDriver { + + private static final ByteBuffer buffer = ByteBuffer.allocateDirect(255); + + @Override + public synchronized List getDevices() { + final var lib = LibVideoCapture.INSTANCE; + + final List list = new ArrayList<>(); + + lib.vcavf_initialize(); + final int count = lib.vcavf_devices_count(); + + if (count < 1) { + return list; + } + + for (int devIndex = 0; devIndex < count; devIndex++) { + final String uniqueId = deviceUniqueId(devIndex); + final String name = deviceName(devIndex); + + final int formatCount = lib.vcavf_get_device_formats_count(devIndex); + + final Set resolutions = new LinkedHashSet<>(); + for (int formatIndex = 0; formatIndex < formatCount; formatIndex++) { + final String format = deviceFormat(devIndex, formatIndex); + + final Dimension resolution = formatToDimension(format); + if (resolution != null) { + resolutions.add(resolution); + } + } + + final AVFVideoDevice device = new AVFVideoDevice(devIndex, uniqueId, name, resolutions); + 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); + LibVideoCapture.INSTANCE.vcavf_get_device_unique_id(deviceIndex, bufferP, buffer.capacity()); + return bufferP.getString(0, StandardCharsets.UTF_8.name()); + } + + private static String deviceModelId(final int deviceIndex) { + final var bufferP = Native.getDirectBufferPointer(buffer); + LibVideoCapture.INSTANCE.vcavf_get_device_model_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); + LibVideoCapture.INSTANCE.vcavf_get_device_name(deviceIndex, bufferP, buffer.capacity()); + return bufferP.getString(0, StandardCharsets.UTF_8.name()); + } + + private static String deviceFormat(final int deviceIndex, final int formatIndex) { + final var bufferP = Native.getDirectBufferPointer(buffer); + LibVideoCapture.INSTANCE.vcavf_get_device_format(deviceIndex, formatIndex, bufferP, buffer.capacity()); + return bufferP.getString(0, StandardCharsets.UTF_8.name()); + } + + public static final Pattern RESOLUTION_PATTERN = Pattern.compile("[0-9]+x[0-9]+", Pattern.CASE_INSENSITIVE); + + private static Dimension formatToDimension(final String format) { + final String[] parts = format.split(";"); + if (parts.length > 0) { + final String resolution = parts[0].trim(); + + if (RESOLUTION_PATTERN.matcher(resolution).matches()) { + final String[] widthAndHeight = resolution.split("[Xx]"); + return new Dimension(Integer.parseInt(widthAndHeight[0]), Integer.parseInt(widthAndHeight[1])); + } + } + + return null; + } +} diff --git a/src/main/java/com/github/eduramiba/webcamcapture/drivers/avfoundation/driver/AVFVideoDevice.java b/src/main/java/com/github/eduramiba/webcamcapture/drivers/avfoundation/driver/AVFVideoDevice.java new file mode 100644 index 0000000..5b9e39d --- /dev/null +++ b/src/main/java/com/github/eduramiba/webcamcapture/drivers/avfoundation/driver/AVFVideoDevice.java @@ -0,0 +1,280 @@ +package com.github.eduramiba.webcamcapture.drivers.avfoundation.driver; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.ComponentSampleModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.Raster; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collection; + +import com.github.eduramiba.webcamcapture.drivers.WebcamDeviceWithBufferOperations; +import com.github.eduramiba.webcamcapture.drivers.WebcamDeviceWithId; +import com.github.sarxos.webcam.WebcamDevice; +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 static com.github.eduramiba.webcamcapture.drivers.avfoundation.driver.LibVideoCapture.STATUS_AUTHORIZED; + +public class AVFVideoDevice implements WebcamDevice, WebcamDevice.FPSSource, WebcamDevice.BufferAccess, WebcamDeviceWithId, WebcamDeviceWithBufferOperations { + private static final Logger LOG = LoggerFactory.getLogger(AVFVideoDevice.class); + + private final int deviceIndex; + private final String id; + private final String name; + private final Dimension[] resolutions; + private Dimension resolution; + + //State: + private boolean open = false; + private ByteBuffer imgBuffer = null; + private byte[] arrayByteBuffer = null; + private BufferedImage bufferedImage = null; + + public AVFVideoDevice(final int deviceIndex, final String id, final String name, final Collection resolutions) { + 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); + } + + 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 = LibVideoCapture.INSTANCE; + final int authStatus = lib.vcavf_has_videocapture_auth(); + + if (authStatus != STATUS_AUTHORIZED) { + LOG.warn("Capture auth status = {}", authStatus); + } + + if (authStatus != STATUS_AUTHORIZED) { + lib.vcavf_ask_videocapture_auth(); + } + + final int width = resolution.width; + final int height = resolution.height; + final int startResult = lib.vcavf_start_capture(deviceIndex, width, height); + + if (startResult < 0) { + LOG.warn("Capture start result for device {} = {}", id, startResult); + return; + } + + final var bufferSizeBytes = width * height * 3; + + this.open = true; + this.imgBuffer = ByteBuffer.allocateDirect(bufferSizeBytes); + this.bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR); + this.arrayByteBuffer = new byte[imgBuffer.capacity()]; + + LOG.info("Device {} opened successfully", id); + } + + @Override + public synchronized void close() { + if (isOpen()) { + LibVideoCapture.INSTANCE.vcavf_stop_capture(deviceIndex); + open = false; + imgBuffer = null; + arrayByteBuffer = null; + bufferedImage = null; + } + } + + @Override + public void dispose() { + } + + @Override + public boolean isOpen() { + return open; + } + + public static final int MAX_FPS = 30; + + @Override + public double getFPS() { + //TODO: Use actual FPS declared by stream + return MAX_FPS; + } + + @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 synchronized boolean updateFXIMage(WritableImage writableImage) { + if (!isOpen()) { + return false; + } + + updateBuffer(); + + return updateFXIMage(writableImage, imgBuffer); + } + + public boolean updateFXIMage(final WritableImage writableImage, final ByteBuffer byteBuffer) { + if (!isOpen()) { + return false; + } + + final int videoWidth = resolution.width; + final int videoHeight = resolution.height; + + final PixelWriter pw = writableImage.getPixelWriter(); + + byteBuffer.mark(); + byteBuffer.position(0); + pw.setPixels( + 0, 0, videoWidth, videoHeight, + PixelFormat.getByteRgbInstance(), byteBuffer, 3 * videoWidth + ); + + return true; + } + + private void updateBuffer() { + if (LibVideoCapture.INSTANCE.vcavf_has_new_frame(deviceIndex)) { + LibVideoCapture.INSTANCE.vcavf_grab_frame( + deviceIndex, + Native.getDirectBufferPointer(imgBuffer), imgBuffer.capacity()); + } + } + + private void updateBufferedImage() { + if (!isOpen()) { + return; + } + + final int videoWidth = resolution.width; + final int videoHeight = resolution.height; + + final ComponentSampleModel sampleModel = new ComponentSampleModel( + DataBuffer.TYPE_BYTE, videoWidth, videoHeight, 3, videoWidth * 3, + 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); + } +} diff --git a/src/main/java/com/github/eduramiba/webcamcapture/drivers/avfoundation/driver/LibVideoCapture.java b/src/main/java/com/github/eduramiba/webcamcapture/drivers/avfoundation/driver/LibVideoCapture.java new file mode 100644 index 0000000..167a03b --- /dev/null +++ b/src/main/java/com/github/eduramiba/webcamcapture/drivers/avfoundation/driver/LibVideoCapture.java @@ -0,0 +1,49 @@ +package com.github.eduramiba.webcamcapture.drivers.avfoundation.driver; + +import com.sun.jna.*; + +public interface LibVideoCapture extends Library { + String JNA_LIBRARY_NAME = "videocapture"; + NativeLibrary JNA_NATIVE_LIB = NativeLibrary.getInstance(LibVideoCapture.JNA_LIBRARY_NAME); + LibVideoCapture INSTANCE = Native.loadLibrary(LibVideoCapture.JNA_LIBRARY_NAME, LibVideoCapture.class); + + public static final int RESULT_OK = (0); + 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 STATUS_AUTHORIZED = (0); + public static final int STATUS_NOT_DETERMINED = (-2); + public static final int STATUS_DENIED = (-1); + + + boolean vcavf_initialize(); + + int vcavf_has_videocapture_auth(); + void vcavf_ask_videocapture_auth(); + + int vcavf_devices_count(); + + void vcavf_get_device_unique_id(int deviceIndex, Pointer pointer, int availableBytes); + + void vcavf_get_device_model_id(int deviceIndex, Pointer pointer, int availableBytes); + + void vcavf_get_device_name(int deviceIndex, Pointer pointer, int availableBytes); + + int vcavf_get_device_formats_count(int deviceIndex); + + void vcavf_get_device_format(int deviceIndex, int formatIndex, Pointer memory, int availableBytes); + + int vcavf_start_capture(int deviceIndex, int width, int height); + + int vcavf_stop_capture(int deviceIndex); + + boolean vcavf_has_new_frame(int deviceIndex); + + int vcavf_frame_width(int deviceIndex); + + int vcavf_frame_height(int deviceIndex); + + boolean vcavf_grab_frame(int deviceIndex, Pointer pointer, int availableBytes); +} diff --git a/src/main/resources/darwin-aarch64/libvideocapture.dylib b/src/main/resources/darwin-aarch64/libvideocapture.dylib new file mode 100755 index 0000000..6e242d5 Binary files /dev/null and b/src/main/resources/darwin-aarch64/libvideocapture.dylib differ diff --git a/src/main/resources/darwin-x86-64/libvideocapture.dylib b/src/main/resources/darwin-x86-64/libvideocapture.dylib new file mode 100755 index 0000000..fc6ca94 Binary files /dev/null and b/src/main/resources/darwin-x86-64/libvideocapture.dylib differ