Skip to content

Commit

Permalink
[camerax] Implement image capture (flutter#3287)
Browse files Browse the repository at this point in the history
[camerax] Implement image capture
  • Loading branch information
camsim99 authored Mar 22, 2023
1 parent 0f99306 commit 19d19b5
Show file tree
Hide file tree
Showing 23 changed files with 1,460 additions and 47 deletions.
3 changes: 2 additions & 1 deletion packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
* Adds implementation of availableCameras().
* Implements camera preview, createCamera, initializeCamera, onCameraError, onDeviceOrientationChanged, and onCameraInitialized.
* Adds integration test to plugin.
* Fixes instance manager hot restart behavior and fixes Java casting issue.
* Bump CameraX version to 1.3.0-alpha04.
* Fixes instance manager hot restart behavior and fixes Java casting issue.
* Implements image capture.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
private InstanceManager instanceManager;
private FlutterPluginBinding pluginBinding;
private ProcessCameraProviderHostApiImpl processCameraProviderHostApi;
private ImageCaptureHostApiImpl imageCaptureHostApi;
public SystemServicesHostApiImpl systemServicesHostApi;

/**
Expand Down Expand Up @@ -53,6 +54,8 @@ void setUp(BinaryMessenger binaryMessenger, Context context, TextureRegistry tex
GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApi);
GeneratedCameraXLibrary.PreviewHostApi.setup(
binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry));
imageCaptureHostApi = new ImageCaptureHostApiImpl(binaryMessenger, instanceManager, context);
GeneratedCameraXLibrary.ImageCaptureHostApi.setup(binaryMessenger, imageCaptureHostApi);
}

@Override
Expand Down Expand Up @@ -107,5 +110,8 @@ public void updateContext(Context context) {
if (processCameraProviderHostApi != null) {
processCameraProviderHostApi.setContext(context);
}
if (imageCaptureHostApi != null) {
processCameraProviderHostApi.setContext(context);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.Preview;
import io.flutter.plugin.common.BinaryMessenger;
import java.io.File;

/** Utility class used to create CameraX-related objects primarily for testing purposes. */
public class CameraXProxy {
Expand Down Expand Up @@ -48,4 +50,15 @@ public SystemServicesFlutterApiImpl createSystemServicesFlutterApiImpl(
@NonNull BinaryMessenger binaryMessenger) {
return new SystemServicesFlutterApiImpl(binaryMessenger);
}

public ImageCapture.Builder createImageCaptureBuilder() {
return new ImageCapture.Builder();
}

/**
* Creates an {@link ImageCapture.OutputFileOptions} to configure where to save a captured image.
*/
public ImageCapture.OutputFileOptions createImageCaptureOutputFileOptions(@NonNull File file) {
return new ImageCapture.OutputFileOptions.Builder(file).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,9 @@ Long bindToLifecycle(
@NonNull Long cameraSelectorIdentifier,
@NonNull List<Long> useCaseIds);

@NonNull
Boolean isBound(@NonNull Long identifier, @NonNull Long useCaseIdentifier);

void unbind(@NonNull Long identifier, @NonNull List<Long> useCaseIds);

void unbindAll(@NonNull Long identifier);
Expand Down Expand Up @@ -650,6 +653,40 @@ public void error(Throwable error) {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.ProcessCameraProviderHostApi.isBound",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
Map<String, Object> wrapped = new HashMap<>();
try {
ArrayList<Object> args = (ArrayList<Object>) message;
Number identifierArg = (Number) args.get(0);
if (identifierArg == null) {
throw new NullPointerException("identifierArg unexpectedly null.");
}
Number useCaseIdentifierArg = (Number) args.get(1);
if (useCaseIdentifierArg == null) {
throw new NullPointerException("useCaseIdentifierArg unexpectedly null.");
}
Boolean output =
api.isBound(
(identifierArg == null) ? null : identifierArg.longValue(),
(useCaseIdentifierArg == null) ? null : useCaseIdentifierArg.longValue());
wrapped.put("result", output);
} catch (Error | RuntimeException exception) {
wrapped.put("error", wrapError(exception));
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
Expand Down Expand Up @@ -1143,6 +1180,156 @@ static void setup(BinaryMessenger binaryMessenger, PreviewHostApi api) {
}
}

private static class ImageCaptureHostApiCodec extends StandardMessageCodec {
public static final ImageCaptureHostApiCodec INSTANCE = new ImageCaptureHostApiCodec();

private ImageCaptureHostApiCodec() {}

@Override
protected Object readValueOfType(byte type, ByteBuffer buffer) {
switch (type) {
case (byte) 128:
return ResolutionInfo.fromMap((Map<String, Object>) readValue(buffer));

default:
return super.readValueOfType(type, buffer);
}
}

@Override
protected void writeValue(ByteArrayOutputStream stream, Object value) {
if (value instanceof ResolutionInfo) {
stream.write(128);
writeValue(stream, ((ResolutionInfo) value).toMap());
} else {
super.writeValue(stream, value);
}
}
}

/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
public interface ImageCaptureHostApi {
void create(
@NonNull Long identifier,
@Nullable Long flashMode,
@Nullable ResolutionInfo targetResolution);

void setFlashMode(@NonNull Long identifier, @NonNull Long flashMode);

void takePicture(@NonNull Long identifier, Result<String> result);

/** The codec used by ImageCaptureHostApi. */
static MessageCodec<Object> getCodec() {
return ImageCaptureHostApiCodec.INSTANCE;
}

/**
* Sets up an instance of `ImageCaptureHostApi` to handle messages through the
* `binaryMessenger`.
*/
static void setup(BinaryMessenger binaryMessenger, ImageCaptureHostApi api) {
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.ImageCaptureHostApi.create", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
Map<String, Object> wrapped = new HashMap<>();
try {
ArrayList<Object> args = (ArrayList<Object>) message;
Number identifierArg = (Number) args.get(0);
if (identifierArg == null) {
throw new NullPointerException("identifierArg unexpectedly null.");
}
Number flashModeArg = (Number) args.get(1);
ResolutionInfo targetResolutionArg = (ResolutionInfo) args.get(2);
api.create(
(identifierArg == null) ? null : identifierArg.longValue(),
(flashModeArg == null) ? null : flashModeArg.longValue(),
targetResolutionArg);
wrapped.put("result", null);
} catch (Error | RuntimeException exception) {
wrapped.put("error", wrapError(exception));
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.ImageCaptureHostApi.setFlashMode", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
Map<String, Object> wrapped = new HashMap<>();
try {
ArrayList<Object> args = (ArrayList<Object>) message;
Number identifierArg = (Number) args.get(0);
if (identifierArg == null) {
throw new NullPointerException("identifierArg unexpectedly null.");
}
Number flashModeArg = (Number) args.get(1);
if (flashModeArg == null) {
throw new NullPointerException("flashModeArg unexpectedly null.");
}
api.setFlashMode(
(identifierArg == null) ? null : identifierArg.longValue(),
(flashModeArg == null) ? null : flashModeArg.longValue());
wrapped.put("result", null);
} catch (Error | RuntimeException exception) {
wrapped.put("error", wrapError(exception));
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.ImageCaptureHostApi.takePicture", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
Map<String, Object> wrapped = new HashMap<>();
try {
ArrayList<Object> args = (ArrayList<Object>) message;
Number identifierArg = (Number) args.get(0);
if (identifierArg == null) {
throw new NullPointerException("identifierArg unexpectedly null.");
}
Result<String> resultCallback =
new Result<String>() {
public void success(String result) {
wrapped.put("result", result);
reply.reply(wrapped);
}

public void error(Throwable error) {
wrapped.put("error", wrapError(error));
reply.reply(wrapped);
}
};

api.takePicture(
(identifierArg == null) ? null : identifierArg.longValue(), resultCallback);
} catch (Error | RuntimeException exception) {
wrapped.put("error", wrapError(exception));
reply.reply(wrapped);
}
});
} else {
channel.setMessageHandler(null);
}
}
}
}

private static Map<String, Object> wrapError(Throwable exception) {
Map<String, Object> errorMap = new HashMap<>();
errorMap.put("message", exception.toString());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.plugins.camerax;

import android.content.Context;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ImageCaptureHostApi;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.Executors;

public class ImageCaptureHostApiImpl implements ImageCaptureHostApi {
private final BinaryMessenger binaryMessenger;
private final InstanceManager instanceManager;

private Context context;
private SystemServicesFlutterApiImpl systemServicesFlutterApiImpl;

public static final String TEMPORARY_FILE_NAME = "CAP";
public static final String JPG_FILE_TYPE = ".jpg";

@VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy();

public ImageCaptureHostApiImpl(
@NonNull BinaryMessenger binaryMessenger,
@NonNull InstanceManager instanceManager,
@NonNull Context context) {
this.binaryMessenger = binaryMessenger;
this.instanceManager = instanceManager;
this.context = context;
}

/**
* Sets the context that the {@link ImageCapture} will use to find a location to save a captured
* image.
*/
public void setContext(Context context) {
this.context = context;
}

/**
* Creates an {@link ImageCapture} with the requested flash mode and target resolution if
* specified.
*/
@Override
public void create(
@NonNull Long identifier,
@Nullable Long flashMode,
@Nullable GeneratedCameraXLibrary.ResolutionInfo targetResolution) {
ImageCapture.Builder imageCaptureBuilder = cameraXProxy.createImageCaptureBuilder();
if (flashMode != null) {
// This sets the requested flash mode, but may fail silently.
imageCaptureBuilder.setFlashMode(flashMode.intValue());
}
if (targetResolution != null) {
imageCaptureBuilder.setTargetResolution(
new Size(
targetResolution.getWidth().intValue(), targetResolution.getHeight().intValue()));
}
ImageCapture imageCapture = imageCaptureBuilder.build();
instanceManager.addDartCreatedInstance(imageCapture, identifier);
}

/** Sets the flash mode of the {@link ImageCapture} instance with the specified identifier. */
@Override
public void setFlashMode(@NonNull Long identifier, @NonNull Long flashMode) {
ImageCapture imageCapture =
(ImageCapture) Objects.requireNonNull(instanceManager.getInstance(identifier));
imageCapture.setFlashMode(flashMode.intValue());
}

/** Captures a still image and uses the result to return its absolute path in memory. */
@Override
public void takePicture(
@NonNull Long identifier, @NonNull GeneratedCameraXLibrary.Result<String> result) {
ImageCapture imageCapture =
(ImageCapture) Objects.requireNonNull(instanceManager.getInstance(identifier));
final File outputDir = context.getCacheDir();
File temporaryCaptureFile;
try {
temporaryCaptureFile = File.createTempFile(TEMPORARY_FILE_NAME, JPG_FILE_TYPE, outputDir);
} catch (IOException | SecurityException e) {
result.error(e);
return;
}

ImageCapture.OutputFileOptions outputFileOptions =
cameraXProxy.createImageCaptureOutputFileOptions(temporaryCaptureFile);
ImageCapture.OnImageSavedCallback onImageSavedCallback =
createOnImageSavedCallback(temporaryCaptureFile, result);

imageCapture.takePicture(
outputFileOptions, Executors.newSingleThreadExecutor(), onImageSavedCallback);
}

/** Creates a callback used when saving a captured image. */
@VisibleForTesting
public ImageCapture.OnImageSavedCallback createOnImageSavedCallback(
@NonNull File file, @NonNull GeneratedCameraXLibrary.Result<String> result) {
return new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
result.success(file.getAbsolutePath());
}

@Override
public void onError(@NonNull ImageCaptureException exception) {
result.error(exception);
}
};
}
}
Loading

0 comments on commit 19d19b5

Please sign in to comment.