Skip to content

Commit

Permalink
(feat) flutter: take picture
Browse files Browse the repository at this point in the history
  • Loading branch information
alexey-pelykh committed Dec 17, 2024
1 parent 85fc35a commit 19f6ef0
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.uvccamera.flutter;

/**
* Handle to be notified when the device permission request result is available.
* Handler to be notified when the device permission request result is available.
*/
@FunctionalInterface
/* package-private */ interface UvcCameraDevicePermissionRequestResultHandler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ public UvcCameraNativeMethodCallHandler(final UvcCameraPlatform uvcCameraPlatfor

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Log.v(TAG, "onMethodCall: " + call + ", result=" + result);
Log.v(TAG, "onMethodCall"
+ ": call=" + call
+ ", result=" + result
);

switch (call.method) {
case "isSupported" -> {
Expand Down Expand Up @@ -370,6 +373,28 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result

result.success(null);
}
case "takePicture" -> {
final var cameraId = call.<Integer>argument("cameraId");
if (cameraId == null) {
result.error("InvalidArgument", "cameraId is required", null);
return;
}

try {
uvcCameraPlatform.takePicture(
cameraId,
(pictureFile, error) -> mainLooperHandler.post(() -> {
if (error != null) {
result.error(error.getClass().getSimpleName(), error.getMessage(), null);
} else {
result.success(pictureFile.getAbsolutePath());
}
})
);
} catch (final Exception e) {
result.error(e.getClass().getSimpleName(), e.getMessage(), null);
}
}
case "startVideoRecording" -> {
final var cameraId = call.<Integer>argument("cameraId");
if (cameraId == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.usb.UsbDevice;
import android.media.MediaRecorder;
import android.os.Handler;
Expand All @@ -17,6 +20,7 @@
import com.serenegiant.usb.UVCCamera;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
Expand Down Expand Up @@ -851,6 +855,145 @@ public void setPreviewSize(
);
}

/**
* Takes a picture for the specified camera
*
* @param cameraId the camera ID
* @param resultHandler the handler to be notified when the picture is taken
*/
public void takePicture(final int cameraId, UvcCameraTakePictureResultHandler resultHandler) {
Log.v(TAG, "takePicture"
+ ": cameraId=" + cameraId
);

final var cameraResources = camerasResources.get(cameraId);
if (cameraResources == null) {
throw new IllegalArgumentException("Camera resources not found: " + cameraId);
}

final var applicationContext = this.applicationContext.get();
if (applicationContext == null) {
throw new IllegalStateException("applicationContext reference has expired");
}

final var outputDir = applicationContext.getCacheDir();
final File outputFile;
try {
outputFile = File.createTempFile("PIC", ".jpg", outputDir);
} catch (IOException | SecurityException e) {
throw new IllegalStateException("Failed to create picture file", e);
}

cameraResources.camera().setFrameCallback(
new UvcCameraTakePictureFrameCallback(
this,
cameraId,
outputFile,
resultHandler
),
UVCCamera.PIXEL_FORMAT_NV21
);
}

/**
* Handles the taken picture
*
* @param cameraId the camera ID
* @param outputFile the output file
* @param frame the frame
* @param resultHandler the result handler
*/
/* package-private */ void handleTakenPicture(
final int cameraId,
final File outputFile,
final ByteBuffer frame,
final UvcCameraTakePictureResultHandler resultHandler
) {
Log.v(TAG, "handleTakenPicture"
+ ": cameraId=" + cameraId
+ ", outputFile=" + outputFile
+ ", frame=" + frame
+ ", resultHandler=" + resultHandler
);

final var cameraResources = camerasResources.get(cameraId);
if (cameraResources == null) {
throw new IllegalArgumentException("Camera resources not found: " + cameraId);
}

// Create copy of the frame data as the frame buffer is owned by the native side (libuvc)
final var frameData = new byte[frame.remaining()];
frame.get(frameData);

// NOTE: The frame callback should've been detached here yet that will cause a deadlock

// Save the taken picture to the file using the worker looper
mainLooperHandler.post(() -> {
// Detach the frame callback
cameraResources.camera().setFrameCallback(null, 0);

try {
saveTakenPictureToFile(cameraId, outputFile, frameData);
resultHandler.onResult(outputFile, null);
} catch(final Exception e) {
Log.e(TAG, "Failed to save taken picture to file", e);
resultHandler.onResult(null, e);
}
});
}

/**
* Saves the taken picture to the specified file
*
* @param cameraId the camera ID
* @param outputFile the output file
* @param frameData the frame data
*/
private void saveTakenPictureToFile(final int cameraId, final File outputFile, final byte[] frameData) {
Log.v(TAG, "saveTakenPictureToFile"
+ ": cameraId=" + cameraId
+ ", outputFile=" + outputFile
+ ", frameData=[... " + frameData.length + " byte(s) ...]"
);

final var cameraResources = camerasResources.get(cameraId);
if (cameraResources == null) {
throw new IllegalArgumentException("Camera resources not found: " + cameraId);
}

final var previewSize = cameraResources.camera().getPreviewSize();
final var yuvImage = new YuvImage(
frameData,
ImageFormat.NV21,
previewSize.width,
previewSize.height,
null
);

final FileOutputStream outputFileStream;
try {
outputFileStream = new FileOutputStream(outputFile);
} catch (IOException e) {
throw new IllegalStateException("Failed to open picture file output stream", e);
}

try {
yuvImage.compressToJpeg(
new Rect(0, 0, previewSize.width, previewSize.height),
100,
outputFileStream
);
} catch (Exception e) {
throw new IllegalStateException("Failed to write picture file", e);
} finally {
try {
outputFileStream.close();
} catch (IOException e) {
Log.w(TAG, "Failed to close picture file output stream", e);
}
}
}

/**
* Starts video recording for the specified camera
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.uvccamera.flutter;

import android.util.Log;

import com.serenegiant.usb.IFrameCallback;

import java.io.File;
import java.nio.ByteBuffer;

/* package-private */ class UvcCameraTakePictureFrameCallback implements IFrameCallback {

/**
* Log tag
*/
private static final String TAG = UvcCameraTakePictureFrameCallback.class.getCanonicalName();

/**
* The UVC camera platform
*/
private final UvcCameraPlatform uvcCameraPlatform;

/**
* The camera ID
*/
private final int cameraId;

/**
* Output file to which the picture is saved.
*/
private final File outputFile;

/**
* The result handler
*/
private final UvcCameraTakePictureResultHandler resultHandler;

/**
* Creates a new instance of {@link UvcCameraTakePictureFrameCallback}.
*
* @param uvcCameraPlatform the UVC camera platform
* @param cameraId the camera ID
* @param outputFile the output file
* @param resultHandler the result handler
*/
public UvcCameraTakePictureFrameCallback(
final UvcCameraPlatform uvcCameraPlatform,
final int cameraId,
final File outputFile,
final UvcCameraTakePictureResultHandler resultHandler
) {
this.uvcCameraPlatform = uvcCameraPlatform;
this.cameraId = cameraId;
this.outputFile = outputFile;
this.resultHandler = resultHandler;
}

@Override
public void onFrame(ByteBuffer frame) {
Log.v(TAG, "onFrame"
+ ": frame=" + frame
);

uvcCameraPlatform.handleTakenPicture(cameraId, outputFile, frame, resultHandler);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.uvccamera.flutter;

import java.io.File;

/**
* Handler to be notified when the take-picture result is available.
*/
@FunctionalInterface
/* package-private */ interface UvcCameraTakePictureResultHandler {

/**
* Called when the take-picture result is available
*
* @param outputFile the output file to which the picture is saved
* or null if the picture could not be taken
* @param error the error that occurred while taking the picture
*/
void onResult(File outputFile, Exception error);

}
17 changes: 17 additions & 0 deletions flutter/example/lib/uvccamera_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ class _UvcCameraWidgetState extends State<UvcCameraWidget> {
await _cameraController!.startVideoRecording(videoRecordingMode);
}

Future<void> _takePicture() async {
final XFile outputFile = await _cameraController!.takePicture();

outputFile.length().then((length) {
setState(() {
_log = 'image file: ${outputFile.path} ($length bytes)\n$_log';
});
});
}

Future<void> _stopVideoRecording() async {
final XFile outputFile = await _cameraController!.stopVideoRecording();

Expand Down Expand Up @@ -245,6 +255,13 @@ class _UvcCameraWidgetState extends State<UvcCameraWidget> {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FloatingActionButton(
backgroundColor: Colors.white,
onPressed: () async => {
await _takePicture(),
},
child: Icon(Icons.camera_alt, color: Colors.black),
),
FloatingActionButton(
backgroundColor: value.isRecordingVideo ? Colors.red : Colors.white,
onPressed: () async {
Expand Down
11 changes: 11 additions & 0 deletions flutter/lib/src/uvccamera_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,17 @@ class UvcCameraController extends ValueNotifier<UvcCameraControllerState> {
return _cameraButtonEventStream!;
}

/// Takes a picture.
Future<XFile> takePicture() async {
_ensureInitializedNotDisposed();

final XFile pictureFile = await UvcCameraPlatformInterface.instance.takePicture(
_cameraId!,
);

return pictureFile;
}

/// Starts video recording.
Future<void> startVideoRecording(UvcCameraMode videoRecordingMode) async {
_ensureInitializedNotDisposed();
Expand Down
16 changes: 16 additions & 0 deletions flutter/lib/src/uvccamera_platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,22 @@ class UvcCameraPlatform extends UvcCameraPlatformInterface {
});
}

@override
Future<XFile> takePicture(int cameraId) async {
final result = await _nativeMethodChannel.invokeMethod<String>('takePicture', {
'cameraId': cameraId,
});

if (result == null) {
throw PlatformException(
code: 'UNKNOWN',
message: 'Unable to take picture for camera: $cameraId',
);
}

return XFile(result);
}

@override
Future<XFile> startVideoRecording(int cameraId, UvcCameraMode videoRecordingMode) async {
final result = await _nativeMethodChannel.invokeMethod<String>('startVideoRecording', {
Expand Down
4 changes: 4 additions & 0 deletions flutter/lib/src/uvccamera_platform_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ abstract class UvcCameraPlatformInterface extends PlatformInterface {
throw UnimplementedError('setPreviewMode() has not been implemented.');
}

Future<XFile> takePicture(int cameraId) {
throw UnimplementedError('takePicture() has not been implemented.');
}

Future<XFile> startVideoRecording(int cameraId, UvcCameraMode videoRecordingMode) {
throw UnimplementedError('startVideoRecording() has not been implemented.');
}
Expand Down

0 comments on commit 19f6ef0

Please sign in to comment.