diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index f1d771496dde..1e8789b9afda 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.8.1
+
+* Complete rewrite of Android plugin to fix all capture, focus, flash, and exposure issues.
+* Fixed crash when opening front-facing cameras on some legacy android devices like Sony XZ.
+* Android Flash mode works with full precapture sequence.
+
## 0.8.0
* Stable null safety release.
diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle
index 0b88fd10fb71..fa981d738015 100644
--- a/packages/camera/camera/android/build.gradle
+++ b/packages/camera/camera/android/build.gradle
@@ -49,7 +49,7 @@ android {
dependencies {
compileOnly 'androidx.annotation:annotation:1.1.0'
testImplementation 'junit:junit:4.12'
- testImplementation 'org.mockito:mockito-core:3.5.13'
+ testImplementation 'org.mockito:mockito-inline:3.5.13'
testImplementation 'androidx.test:core:1.3.0'
testImplementation 'org.robolectric:robolectric:4.3'
}
diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
index 5169a3babb74..91f0df33476a 100644
--- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
+++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
@@ -4,27 +4,20 @@
package io.flutter.plugins.camera;
-import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize;
-
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.ImageFormat;
-import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
-import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
-import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
-import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
-import android.hardware.camera2.params.MeteringRectangle;
import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.params.SessionConfiguration;
import android.media.CamcorderProfile;
@@ -35,27 +28,43 @@
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Handler;
+import android.os.HandlerThread;
import android.os.Looper;
-import android.os.SystemClock;
import android.util.Log;
-import android.util.Range;
-import android.util.Rational;
import android.util.Size;
+import android.util.SparseIntArray;
+import android.view.Display;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodChannel.Result;
-import io.flutter.plugins.camera.PictureCaptureRequest.State;
+import io.flutter.plugins.camera.features.CameraFeature;
+import io.flutter.plugins.camera.features.CameraFeatureFactory;
+import io.flutter.plugins.camera.features.CameraFeatures;
+import io.flutter.plugins.camera.features.Point;
+import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature;
+import io.flutter.plugins.camera.features.autofocus.FocusMode;
+import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature;
+import io.flutter.plugins.camera.features.exposurelock.ExposureMode;
+import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature;
+import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetValue;
+import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature;
+import io.flutter.plugins.camera.features.flash.FlashFeature;
+import io.flutter.plugins.camera.features.flash.FlashMode;
+import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature;
+import io.flutter.plugins.camera.features.regionboundaries.CameraRegions;
+import io.flutter.plugins.camera.features.regionboundaries.RegionBoundariesFeature;
+import io.flutter.plugins.camera.features.resolution.ResolutionFeature;
+import io.flutter.plugins.camera.features.resolution.ResolutionPreset;
+import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager;
+import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature;
+import io.flutter.plugins.camera.features.zoomlevel.CameraZoom;
+import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature;
import io.flutter.plugins.camera.media.MediaRecorderBuilder;
-import io.flutter.plugins.camera.types.ExposureMode;
-import io.flutter.plugins.camera.types.FlashMode;
-import io.flutter.plugins.camera.types.FocusMode;
-import io.flutter.plugins.camera.types.ResolutionPreset;
import io.flutter.view.TextureRegistry.SurfaceTextureEntry;
import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -71,164 +80,251 @@ interface ErrorCallback {
void onError(String errorCode, String errorMessage);
}
-public class Camera {
+/**
+ * Note: at this time we do not implement zero shutter lag (ZSL) capture. This is a potential
+ * improvement we can use in the future. The idea is in a TEMPLATE_ZERO_SHUTTER_LAG capture session,
+ * the system maintains a ring buffer of images from the preview. It must be in full auto moved
+ * (flash, ae, focus, etc). When you capture an image, it simply picks one out of the ring buffer,
+ * thus capturing an image with zero shutter lag.
+ *
+ *
This is a potential improvement for the future. A good example is the AOSP camera here:
+ * https://android.googlesource.com/platform/packages/apps/Camera2/+/9c94ab3/src/com/android/camera/one/v2/OneCameraZslImpl.java
+ *
+ *
But one note- they mention sometimes ZSL captures can be very low quality so it might not be
+ * preferred on some devices. If we do add support for this in the future, we should allow it to be
+ * enabled from dart.
+ */
+class Camera implements CameraCaptureCallback.CameraCaptureStateListener {
private static final String TAG = "Camera";
- /** Timeout for the pre-capture sequence. */
- private static final long PRECAPTURE_TIMEOUT_MS = 1000;
+ /** Conversion from screen rotation to JPEG orientation. */
+ private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
+
+ private static final HashMap supportedImageFormats;
+
+ static {
+ ORIENTATIONS.append(Surface.ROTATION_0, 90);
+ ORIENTATIONS.append(Surface.ROTATION_90, 0);
+ ORIENTATIONS.append(Surface.ROTATION_180, 270);
+ ORIENTATIONS.append(Surface.ROTATION_270, 180);
+ }
+
+ // Current supported outputs
+ static {
+ supportedImageFormats = new HashMap<>();
+ supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888);
+ supportedImageFormats.put("jpeg", ImageFormat.JPEG);
+ }
+
+ /**
+ * Holds all of the camera features/settings and will be used to update the request builder when
+ * one changes.
+ */
+ private final Map cameraFeatures;
private final SurfaceTextureEntry flutterTexture;
- private final CameraManager cameraManager;
- private final DeviceOrientationManager deviceOrientationListener;
- private final boolean isFrontFacing;
- private final int sensorOrientation;
- private final String cameraName;
- private final Size captureSize;
- private final Size previewSize;
private final boolean enableAudio;
private final Context applicationContext;
- private final CamcorderProfile recordingProfile;
private final DartMessenger dartMessenger;
- private final CameraZoom cameraZoom;
- private final CameraCharacteristics cameraCharacteristics;
+ private final CameraProperties cameraProperties;
+ private final CameraFeatureFactory cameraFeatureFactory;
+ private final Activity activity;
+ /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */
+ private final CameraCaptureCallback cameraCaptureCallback;
+ /** A {@link Handler} for running tasks in the background. */
+ private Handler backgroundHandler;
+ /**
+ * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a
+ * still image is ready to be saved.
+ */
+ private final ImageReader.OnImageAvailableListener onImageAvailableListener =
+ new ImageReader.OnImageAvailableListener() {
+ @Override
+ public void onImageAvailable(ImageReader reader) {
+ Log.i(TAG, "onImageAvailable");
+
+ // Use acquireNextImage since our image reader is only for 1 image.
+ backgroundHandler.post(
+ new ImageSaver(
+ reader.acquireNextImage(), pictureCaptureRequest.file, pictureCaptureRequest));
+ cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW);
+ }
+ };
+ /** An additional thread for running tasks that shouldn't block the UI. */
+ private HandlerThread backgroundHandlerThread;
private CameraDevice cameraDevice;
- private CameraCaptureSession cameraCaptureSession;
+ private CameraCaptureSession captureSession;
private ImageReader pictureImageReader;
private ImageReader imageStreamReader;
- private CaptureRequest.Builder captureRequestBuilder;
+ /** {@link CaptureRequest.Builder} for the camera preview */
+ private CaptureRequest.Builder previewRequestBuilder;
+
private MediaRecorder mediaRecorder;
+ /** True when recording video. */
private boolean recordingVideo;
+
private File videoRecordingFile;
- private FlashMode flashMode;
- private ExposureMode exposureMode;
- private FocusMode focusMode;
- private PictureCaptureRequest pictureCaptureRequest;
- private CameraRegions cameraRegions;
- private int exposureOffset;
- private boolean useAutoFocus = true;
- private Range fpsRange;
- private PlatformChannel.DeviceOrientation lockedCaptureOrientation;
- private long preCaptureStartTime;
- private static final HashMap supportedImageFormats;
- // Current supported outputs
- static {
- supportedImageFormats = new HashMap<>();
- supportedImageFormats.put("yuv420", 35);
- supportedImageFormats.put("jpeg", 256);
- }
+ /** Holds the current picture capture request and its related timeouts */
+ private PictureCaptureRequest pictureCaptureRequest;
public Camera(
final Activity activity,
final SurfaceTextureEntry flutterTexture,
+ final CameraFeatureFactory cameraFeatureFactory,
final DartMessenger dartMessenger,
- final String cameraName,
- final String resolutionPreset,
- final boolean enableAudio)
- throws CameraAccessException {
+ final CameraProperties cameraProperties,
+ final ResolutionPreset resolutionPreset,
+ final boolean enableAudio) {
+
if (activity == null) {
throw new IllegalStateException("No activity available!");
}
- this.cameraName = cameraName;
+ this.activity = activity;
this.enableAudio = enableAudio;
this.flutterTexture = flutterTexture;
this.dartMessenger = dartMessenger;
- this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
this.applicationContext = activity.getApplicationContext();
- this.flashMode = FlashMode.auto;
- this.exposureMode = ExposureMode.auto;
- this.focusMode = FocusMode.auto;
- this.exposureOffset = 0;
-
- cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName);
- initFps(cameraCharacteristics);
- sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
- isFrontFacing =
- cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)
- == CameraMetadata.LENS_FACING_FRONT;
- ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset);
- recordingProfile =
- CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset);
- captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);
- previewSize = computeBestPreviewSize(cameraName, preset);
- cameraZoom =
- new CameraZoom(
- cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE),
- cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM));
-
- deviceOrientationListener =
- new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation);
- deviceOrientationListener.start();
+ this.cameraProperties = cameraProperties;
+ this.cameraFeatureFactory = cameraFeatureFactory;
+
+ // Setup camera features
+ this.cameraFeatures =
+ new HashMap() {
+ {
+ put(
+ CameraFeatures.resolution,
+ cameraFeatureFactory.createResolutionFeature(
+ cameraProperties, resolutionPreset, cameraProperties.getCameraName()));
+ put(
+ CameraFeatures.autoFocus,
+ cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false));
+ put(
+ CameraFeatures.sensorOrientation,
+ cameraFeatureFactory.createSensorOrientationFeature(
+ cameraProperties, activity, dartMessenger));
+ put(
+ CameraFeatures.exposureLock,
+ cameraFeatureFactory.createExposureLockFeature(cameraProperties));
+ put(
+ CameraFeatures.exposureOffset,
+ cameraFeatureFactory.createExposureOffsetFeature(cameraProperties));
+ put(
+ CameraFeatures.exposurePoint,
+ cameraFeatureFactory.createExposurePointFeature(
+ cameraProperties, () -> getCameraRegions()));
+ put(
+ CameraFeatures.focusPoint,
+ cameraFeatureFactory.createFocusPointFeature(
+ cameraProperties, () -> getCameraRegions()));
+ put(CameraFeatures.flash, cameraFeatureFactory.createFlashFeature(cameraProperties));
+ put(
+ CameraFeatures.fpsRange,
+ cameraFeatureFactory.createFpsRangeFeature(cameraProperties));
+ put(
+ CameraFeatures.noiseReduction,
+ cameraFeatureFactory.createNoiseReductionFeature(cameraProperties));
+ put(
+ CameraFeatures.zoomLevel,
+ cameraFeatureFactory.createZoomLevelFeature(cameraProperties));
+
+ // Note: CameraFeatures.regionBoundaries will be created in CreateCaptureSession.
+ }
+ };
+
+ // Create capture callback
+ cameraCaptureCallback = CameraCaptureCallback.create(this);
+
+ // Start background thread.
+ startBackgroundThread();
}
- private void initFps(CameraCharacteristics cameraCharacteristics) {
- try {
- Range[] ranges =
- cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
- if (ranges != null) {
- for (Range range : ranges) {
- int upper = range.getUpper();
- Log.i("Camera", "[FPS Range Available] is:" + range);
- if (upper >= 10) {
- if (fpsRange == null || upper > fpsRange.getUpper()) {
- fpsRange = range;
- }
- }
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
+ @Override
+ public void onConverged() {
+ takePictureAfterPrecapture();
+ }
+
+ @Override
+ public void onPrecapture() {
+ runPrecaptureSequence();
+ }
+
+ /**
+ * Update the builder settings with all of our available features.
+ *
+ * @param requestBuilder request builder to update.
+ */
+ private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) {
+ for (Map.Entry feature : cameraFeatures.entrySet()) {
+ Log.i(TAG, "Updating builder with feature: " + feature.getValue().getDebugName());
+ feature.getValue().updateBuilder(requestBuilder);
}
- Log.i("Camera", "[FPS Range] is:" + fpsRange);
}
private void prepareMediaRecorder(String outputFilePath) throws IOException {
+ Log.i(TAG, "prepareMediaRecorder");
+
if (mediaRecorder != null) {
mediaRecorder.release();
}
+ final PlatformChannel.DeviceOrientation lockedOrientation =
+ ((SensorOrientationFeature) cameraFeatures.get(CameraFeatures.sensorOrientation))
+ .getLockedCaptureOrientation();
+
mediaRecorder =
- new MediaRecorderBuilder(recordingProfile, outputFilePath)
+ new MediaRecorderBuilder(getRecordingProfile(), outputFilePath)
.setEnableAudio(enableAudio)
.setMediaOrientation(
- lockedCaptureOrientation == null
- ? deviceOrientationListener.getMediaOrientation()
- : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation))
+ lockedOrientation == null
+ ? getDeviceOrientationManager().getMediaOrientation()
+ : getDeviceOrientationManager().getMediaOrientation(lockedOrientation))
.build();
}
@SuppressLint("MissingPermission")
public void open(String imageFormatGroup) throws CameraAccessException {
+ // We always capture using JPEG format.
pictureImageReader =
ImageReader.newInstance(
- captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2);
+ getCaptureSize().getWidth(), getCaptureSize().getHeight(), ImageFormat.JPEG, 1);
+ // For image streaming, we use the provided image format or fall back to YUV420.
Integer imageFormat = supportedImageFormats.get(imageFormatGroup);
if (imageFormat == null) {
Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420");
imageFormat = ImageFormat.YUV_420_888;
}
-
- // Used to steam image byte data to dart side.
imageStreamReader =
- ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2);
+ ImageReader.newInstance(
+ getPreviewSize().getWidth(), getPreviewSize().getHeight(), imageFormat, 1);
+ // Open the camera now
+ CameraManager cameraManager = CameraUtils.getCameraManager(activity);
cameraManager.openCamera(
- cameraName,
+ cameraProperties.getCameraName(),
new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice device) {
+ Log.i(TAG, "open | onOpened");
+
cameraDevice = device;
try {
startPreview();
+
+ final boolean isExposurePointSupported =
+ cameraFeatures.get(CameraFeatures.exposurePoint).checkIsSupported();
+ final boolean isFocusPointSupported =
+ cameraFeatures.get(CameraFeatures.focusPoint).checkIsSupported();
+
dartMessenger.sendCameraInitializedEvent(
- previewSize.getWidth(),
- previewSize.getHeight(),
- exposureMode,
- focusMode,
- isExposurePointSupported(),
- isFocusPointSupported());
+ getPreviewSize().getWidth(),
+ getPreviewSize().getHeight(),
+ (ExposureMode) cameraFeatures.get(CameraFeatures.exposureLock).getValue(),
+ (FocusMode) cameraFeatures.get(CameraFeatures.autoFocus).getValue(),
+ isExposurePointSupported,
+ isFocusPointSupported);
} catch (CameraAccessException e) {
dartMessenger.sendCameraErrorEvent(e.getMessage());
close();
@@ -237,18 +333,24 @@ public void onOpened(@NonNull CameraDevice device) {
@Override
public void onClosed(@NonNull CameraDevice camera) {
+ Log.i(TAG, "open | onClosed");
+
dartMessenger.sendCameraClosingEvent();
super.onClosed(camera);
}
@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
+ Log.i(TAG, "open | onDisconnected");
+
close();
dartMessenger.sendCameraErrorEvent("The camera was disconnected.");
}
@Override
public void onError(@NonNull CameraDevice cameraDevice, int errorCode) {
+ Log.i(TAG, "open | onError");
+
close();
String errorDescription;
switch (errorCode) {
@@ -273,7 +375,7 @@ public void onError(@NonNull CameraDevice cameraDevice, int errorCode) {
dartMessenger.sendCameraErrorEvent(errorDescription);
}
},
- null);
+ backgroundHandler);
}
private void createCaptureSession(int templateType, Surface... surfaces)
@@ -284,43 +386,48 @@ private void createCaptureSession(int templateType, Surface... surfaces)
private void createCaptureSession(
int templateType, Runnable onSuccessCallback, Surface... surfaces)
throws CameraAccessException {
+ Log.i(TAG, "createCaptureSession");
+
// Close any existing capture session.
closeCaptureSession();
// Create a new capture builder.
- captureRequestBuilder = cameraDevice.createCaptureRequest(templateType);
+ previewRequestBuilder = cameraDevice.createCaptureRequest(templateType);
// Build Flutter surface to render to
SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture();
- surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
+ surfaceTexture.setDefaultBufferSize(getPreviewSize().getWidth(), getPreviewSize().getHeight());
Surface flutterSurface = new Surface(surfaceTexture);
- captureRequestBuilder.addTarget(flutterSurface);
+ previewRequestBuilder.addTarget(flutterSurface);
List remainingSurfaces = Arrays.asList(surfaces);
if (templateType != CameraDevice.TEMPLATE_PREVIEW) {
// If it is not preview mode, add all surfaces as targets.
for (Surface surface : remainingSurfaces) {
- captureRequestBuilder.addTarget(surface);
+ previewRequestBuilder.addTarget(surface);
}
}
- cameraRegions = new CameraRegions(getRegionBoundaries());
+ // Update camera regions
+ cameraFeatures.put(
+ CameraFeatures.regionBoundaries,
+ cameraFeatureFactory.createRegionBoundariesFeature(
+ cameraProperties, previewRequestBuilder));
// Prepare the callback
CameraCaptureSession.StateCallback callback =
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
+ // Camera was already closed.
if (cameraDevice == null) {
dartMessenger.sendCameraErrorEvent("The camera was closed during configuration.");
return;
}
- cameraCaptureSession = session;
+ captureSession = session;
- updateFpsRange();
- updateFocus(focusMode);
- updateFlash(flashMode);
- updateExposure(exposureMode);
+ Log.i(TAG, "Updating builder settings");
+ updateBuilderSettings(previewRequestBuilder);
refreshPreviewCaptureSession(
onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message));
@@ -367,252 +474,264 @@ private void createCaptureSessionWithSessionConfig(
private void createCaptureSession(
List surfaces, CameraCaptureSession.StateCallback callback)
throws CameraAccessException {
- cameraDevice.createCaptureSession(surfaces, callback, null);
+ cameraDevice.createCaptureSession(surfaces, callback, backgroundHandler);
}
+ // Send a repeating request to refresh our capture session.
private void refreshPreviewCaptureSession(
@Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) {
- if (cameraCaptureSession == null) {
+ Log.i(TAG, "refreshPreviewCaptureSession");
+ if (captureSession == null) {
+ Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning");
return;
}
try {
- cameraCaptureSession.setRepeatingRequest(
- captureRequestBuilder.build(),
- pictureCaptureCallback,
- new Handler(Looper.getMainLooper()));
+ captureSession.setRepeatingRequest(
+ previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler);
if (onSuccessCallback != null) {
onSuccessCallback.run();
}
+
} catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) {
onErrorCallback.onError("cameraAccess", e.getMessage());
}
}
- private void writeToFile(ByteBuffer buffer, File file) throws IOException {
- try (FileOutputStream outputStream = new FileOutputStream(file)) {
- while (0 < buffer.remaining()) {
- outputStream.getChannel().write(buffer);
- }
- }
- }
-
public void takePicture(@NonNull final Result result) {
- // Only take 1 picture at a time
- if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) {
+ Log.i(
+ TAG,
+ "takePicture | useAutoFocus: " + cameraFeatures.get(CameraFeatures.autoFocus).getValue());
+
+ // Only take one 1 picture at a time.
+ if (cameraCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) {
result.error("captureAlreadyActive", "Picture is currently already being captured", null);
return;
}
- // Store the result
- this.pictureCaptureRequest = new PictureCaptureRequest(result);
// Create temporary file
final File outputDir = applicationContext.getCacheDir();
- final File file;
try {
- file = File.createTempFile("CAP", ".jpg", outputDir);
+ final File file = File.createTempFile("CAP", ".jpg", outputDir);
+
+ // Start a new capture
+ pictureCaptureRequest = PictureCaptureRequest.create(result, file, 3000, 3000);
+ cameraCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest);
} catch (IOException | SecurityException e) {
pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null);
return;
}
// Listen for picture being taken
- pictureImageReader.setOnImageAvailableListener(
- reader -> {
- try (Image image = reader.acquireLatestImage()) {
- ByteBuffer buffer = image.getPlanes()[0].getBuffer();
- writeToFile(buffer, file);
- pictureCaptureRequest.finish(file.getAbsolutePath());
- } catch (IOException e) {
- pictureCaptureRequest.error("IOError", "Failed saving image", null);
- }
- },
- null);
+ pictureImageReader.setOnImageAvailableListener(onImageAvailableListener, backgroundHandler);
- if (useAutoFocus) {
+ final AutoFocusFeature autoFocusFeature =
+ (AutoFocusFeature) cameraFeatures.get(CameraFeatures.autoFocus);
+ final boolean isAutoFocusSupported = autoFocusFeature.checkIsSupported();
+ if (isAutoFocusSupported && autoFocusFeature.getValue() == FocusMode.auto) {
runPictureAutoFocus();
} else {
- runPicturePreCapture();
+ runPrecaptureSequence();
}
}
- private final CameraCaptureSession.CaptureCallback pictureCaptureCallback =
- new CameraCaptureSession.CaptureCallback() {
- @Override
- public void onCaptureCompleted(
- @NonNull CameraCaptureSession session,
- @NonNull CaptureRequest request,
- @NonNull TotalCaptureResult result) {
- processCapture(result);
- }
-
- @Override
- public void onCaptureProgressed(
- @NonNull CameraCaptureSession session,
- @NonNull CaptureRequest request,
- @NonNull CaptureResult partialResult) {
- processCapture(partialResult);
- }
-
- @Override
- public void onCaptureFailed(
- @NonNull CameraCaptureSession session,
- @NonNull CaptureRequest request,
- @NonNull CaptureFailure failure) {
- if (pictureCaptureRequest == null || pictureCaptureRequest.isFinished()) {
- return;
- }
- String reason;
- boolean fatalFailure = false;
- switch (failure.getReason()) {
- case CaptureFailure.REASON_ERROR:
- reason = "An error happened in the framework";
- break;
- case CaptureFailure.REASON_FLUSHED:
- reason = "The capture has failed due to an abortCaptures() call";
- fatalFailure = true;
- break;
- default:
- reason = "Unknown reason";
- }
- Log.w("Camera", "pictureCaptureCallback.onCaptureFailed(): " + reason);
- if (fatalFailure) pictureCaptureRequest.error("captureFailure", reason, null);
- }
+ /**
+ * Run the precapture sequence for capturing a still image. This method should be called when we
+ * get a response in {@link #cameraCaptureCallback} from lockFocus().
+ */
+ private void runPrecaptureSequence() {
+ Log.i(TAG, "runPrecaptureSequence");
+ try {
+ // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE_START
+ previewRequestBuilder.set(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);
+ captureSession.capture(
+ previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler);
+
+ // Repeating request to refresh preview session
+ refreshPreviewCaptureSession(
+ null, (code, message) -> pictureCaptureRequest.error("cameraAccess", message, null));
- private void processCapture(CaptureResult result) {
- if (pictureCaptureRequest == null) {
- return;
- }
+ // Start precapture now
+ cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_PRECAPTURE_START);
- Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
- Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
- switch (pictureCaptureRequest.getState()) {
- case focusing:
- if (afState == null) {
- return;
- } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
- || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
- // Some devices might return null here, in which case we will also continue.
- if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
- runPictureCapture();
- } else {
- runPicturePreCapture();
- }
- }
- break;
- case preCapture:
- // Some devices might return null here, in which case we will also continue.
- if (aeState == null
- || aeState == CaptureRequest.CONTROL_AE_STATE_PRECAPTURE
- || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED
- || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) {
- pictureCaptureRequest.setState(State.waitingPreCaptureReady);
- setPreCaptureStartTime();
- }
- break;
- case waitingPreCaptureReady:
- if (aeState == null || aeState != CaptureRequest.CONTROL_AE_STATE_PRECAPTURE) {
- runPictureCapture();
- } else {
- if (hitPreCaptureTimeout()) {
- unlockAutoFocus();
- }
- }
- }
- }
- };
+ previewRequestBuilder.set(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
- private void runPictureAutoFocus() {
- assert (pictureCaptureRequest != null);
+ // Trigger one capture to start AE sequence
+ captureSession.capture(
+ previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler);
- pictureCaptureRequest.setState(PictureCaptureRequest.State.focusing);
- lockAutoFocus(pictureCaptureCallback);
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
}
- private void runPicturePreCapture() {
- assert (pictureCaptureRequest != null);
- pictureCaptureRequest.setState(PictureCaptureRequest.State.preCapture);
-
- captureRequestBuilder.set(
- CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
- CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
-
- refreshPreviewCaptureSession(
- () ->
- captureRequestBuilder.set(
- CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
- CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE),
- (code, message) -> pictureCaptureRequest.error(code, message, null));
- }
+ /**
+ * Capture a still picture. This method should be called when we get a response in {@link
+ * #cameraCaptureCallback} from both lockFocus().
+ */
+ private void takePictureAfterPrecapture() {
+ Log.i(TAG, "captureStillPicture");
+ cameraCaptureCallback.setCameraState(CameraState.STATE_CAPTURING);
- private void runPictureCapture() {
- assert (pictureCaptureRequest != null);
- pictureCaptureRequest.setState(PictureCaptureRequest.State.capturing);
try {
- final CaptureRequest.Builder captureBuilder =
+ if (null == cameraDevice) {
+ return;
+ }
+ // This is the CaptureRequest.Builder that we use to take a picture.
+ final CaptureRequest.Builder stillBuilder =
cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
- captureBuilder.addTarget(pictureImageReader.getSurface());
- captureBuilder.set(
+ stillBuilder.addTarget(pictureImageReader.getSurface());
+
+ // Zoom
+ stillBuilder.set(
CaptureRequest.SCALER_CROP_REGION,
- captureRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION));
- captureBuilder.set(
- CaptureRequest.JPEG_ORIENTATION,
- lockedCaptureOrientation == null
- ? deviceOrientationListener.getMediaOrientation()
- : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation));
-
- switch (flashMode) {
- case off:
- captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
- captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
- break;
- case auto:
- captureBuilder.set(
- CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
- break;
- case always:
- default:
- captureBuilder.set(
- CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
- break;
- }
- cameraCaptureSession.stopRepeating();
- cameraCaptureSession.capture(
- captureBuilder.build(),
+ previewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION));
+
+ // Update builder settings
+ updateBuilderSettings(stillBuilder);
+
+ // Orientation
+ int rotation = getDefaultDisplay().getRotation();
+ stillBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
+
+ CameraCaptureSession.CaptureCallback captureCallback =
new CameraCaptureSession.CaptureCallback() {
+
+ @Override
+ public void onCaptureStarted(
+ @NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ long timestamp,
+ long frameNumber) {
+ Log.i(TAG, "onCaptureStarted");
+ }
+
+ @Override
+ public void onCaptureProgressed(
+ @NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull CaptureResult partialResult) {
+ Log.i(TAG, "onCaptureProgressed");
+ }
+
@Override
public void onCaptureCompleted(
@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
+ Log.i(TAG, "onCaptureCompleted");
unlockAutoFocus();
}
- },
- null);
+ };
+
+ captureSession.stopRepeating();
+ captureSession.abortCaptures();
+ Log.i(TAG, "sending capture request");
+ captureSession.capture(stillBuilder.build(), captureCallback, backgroundHandler);
} catch (CameraAccessException e) {
pictureCaptureRequest.error("cameraAccess", e.getMessage(), null);
}
}
- private void lockAutoFocus(CaptureCallback callback) {
- captureRequestBuilder.set(
+ @SuppressWarnings("deprecation")
+ private Display getDefaultDisplay() {
+ return activity.getWindowManager().getDefaultDisplay();
+ }
+
+ /** Starts a background thread and its {@link Handler}. TODO: call when activity resumed */
+ private void startBackgroundThread() {
+ backgroundHandlerThread = new HandlerThread("CameraBackground");
+ backgroundHandlerThread.start();
+ backgroundHandler = new Handler(backgroundHandlerThread.getLooper());
+ }
+
+ /** Stops the background thread and its {@link Handler}. TODO: call when activity paused */
+ private void stopBackgroundThread() {
+ try {
+ if (backgroundHandlerThread != null) {
+ backgroundHandlerThread.quitSafely();
+ backgroundHandlerThread.join();
+ backgroundHandlerThread = null;
+ }
+
+ backgroundHandler = null;
+ } catch (InterruptedException e) {
+ pictureCaptureRequest.error("cameraAccess", e.getMessage(), null);
+ }
+ }
+
+ /**
+ * Retrieves the JPEG orientation from the specified screen rotation.
+ *
+ * @param rotation The screen rotation.
+ * @return The JPEG orientation (one of 0, 90, 270, and 360)
+ */
+ private int getOrientation(int rotation) {
+ // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X)
+ // We have to take that into account and rotate JPEG properly.
+ // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS.
+ // For devices with orientation of 270, we need to rotate the JPEG 180 degrees.
+ final Integer sensorOrientation =
+ (Integer) cameraFeatures.get(CameraFeatures.sensorOrientation).getValue();
+ return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360;
+ }
+
+ /** Start capturing a picture, doing autofocus first. */
+ private void runPictureAutoFocus() {
+ Log.i(TAG, "runPictureAutoFocus");
+ assert (pictureCaptureRequest != null);
+
+ cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS);
+ lockAutoFocus();
+ }
+
+ /**
+ * Start the autofocus routine on the current capture request and calls the onCompleted callback.
+ */
+ private void lockAutoFocus() {
+ Log.i(TAG, "lockAutoFocus");
+
+ // Trigger AF to start
+ previewRequestBuilder.set(
CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START);
- refreshPreviewCaptureSession(
- null, (code, message) -> pictureCaptureRequest.error(code, message, null));
+ try {
+ captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler);
+ } catch (CameraAccessException e) {
+ Log.i(TAG, "Error unlocking focus: " + e.getMessage());
+ dartMessenger.sendCameraErrorEvent(e.getMessage());
+ return;
+ }
}
+ /** Cancel and reset auto focus state and refresh the preview session. */
private void unlockAutoFocus() {
- captureRequestBuilder.set(
- CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
- updateFocus(focusMode);
+ Log.i(TAG, "unlockAutoFocus");
try {
- cameraCaptureSession.capture(captureRequestBuilder.build(), null, null);
- } catch (CameraAccessException ignored) {
+ if (captureSession == null) {
+ Log.i(TAG, "[unlockAutoFocus] captureSession null, returning");
+ return;
+ }
+ // Cancel existing AF state
+ previewRequestBuilder.set(
+ CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
+ captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler);
+
+ // Set AF state to idle again
+ previewRequestBuilder.set(
+ CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE);
+
+ captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler);
+ } catch (CameraAccessException e) {
+ Log.i(TAG, "Error unlocking focus: " + e.getMessage());
+ dartMessenger.sendCameraErrorEvent(e.getMessage());
+ return;
}
- captureRequestBuilder.set(
- CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
refreshPreviewCaptureSession(
null,
@@ -630,7 +749,11 @@ public void startVideoRecording(Result result) {
try {
prepareMediaRecorder(videoRecordingFile.getAbsolutePath());
+
+ // Re-create autofocus feature so it's using video focus mode now
+ cameraFeatures.put(CameraFeatures.autoFocus, new AutoFocusFeature(cameraProperties, true));
recordingVideo = true;
+
createCaptureSession(
CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface());
result.success(null);
@@ -648,10 +771,12 @@ public void stopVideoRecording(@NonNull final Result result) {
}
try {
+ // Re-create autofocus feature so it's using continuous capture focus mode now
+ cameraFeatures.put(CameraFeatures.autoFocus, new AutoFocusFeature(cameraProperties, false));
recordingVideo = false;
try {
- cameraCaptureSession.abortCaptures();
+ captureSession.abortCaptures();
mediaRecorder.stop();
} catch (CameraAccessException | IllegalStateException e) {
// Ignore exceptions and try to continue (changes are camera session already aborted capture)
@@ -709,258 +834,205 @@ public void resumeVideoRecording(@NonNull final Result result) {
result.success(null);
}
- public void setFlashMode(@NonNull final Result result, FlashMode mode)
- throws CameraAccessException {
- // Get the flash availability
- Boolean flashAvailable =
- cameraManager
- .getCameraCharacteristics(cameraDevice.getId())
- .get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
-
- // Check if flash is available.
- if (flashAvailable == null || !flashAvailable) {
- result.error("setFlashModeFailed", "Device does not have flash capabilities", null);
- return;
- }
+ /**
+ * Dart handler when it's time to set a new flash mode. This will try to set a new flash mode to
+ * the current camera.
+ *
+ * @param result Flutter result.
+ * @param newMode new mode.
+ */
+ public void setFlashMode(@NonNull final Result result, FlashMode newMode) {
+ // Save the new flash mode setting
+ ((FlashFeature) cameraFeatures.get(CameraFeatures.flash)).setValue(newMode);
+ cameraFeatures.get(CameraFeatures.flash).updateBuilder(previewRequestBuilder);
- // If switching directly from torch to auto or on, make sure we turn off the torch.
- if (flashMode == FlashMode.torch && mode != FlashMode.torch && mode != FlashMode.off) {
- updateFlash(FlashMode.off);
+ refreshPreviewCaptureSession(
+ () -> result.success(null),
+ (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null));
+ }
- this.cameraCaptureSession.setRepeatingRequest(
- captureRequestBuilder.build(),
- new CaptureCallback() {
- private boolean isFinished = false;
+ /**
+ * Dart handler for setting new exposure mode setting.
+ *
+ * @param result Flutter result.
+ * @param newMode new mode.
+ */
+ public void setExposureMode(@NonNull final Result result, ExposureMode newMode) {
+ ((ExposureLockFeature) cameraFeatures.get(CameraFeatures.exposureLock)).setValue(newMode);
+ cameraFeatures.get(CameraFeatures.exposureLock).updateBuilder(previewRequestBuilder);
- @Override
- public void onCaptureCompleted(
- @NonNull CameraCaptureSession session,
- @NonNull CaptureRequest request,
- @NonNull TotalCaptureResult captureResult) {
- if (isFinished) {
- return;
- }
-
- updateFlash(mode);
- refreshPreviewCaptureSession(
- () -> {
- result.success(null);
- isFinished = true;
- },
- (code, message) ->
- result.error("setFlashModeFailed", "Could not set flash mode.", null));
- }
+ refreshPreviewCaptureSession(
+ () -> result.success(null),
+ (code, message) ->
+ result.error("setExposureModeFailed", "Could not set exposure mode.", null));
+ }
- @Override
- public void onCaptureFailed(
- @NonNull CameraCaptureSession session,
- @NonNull CaptureRequest request,
- @NonNull CaptureFailure failure) {
- if (isFinished) {
- return;
- }
+ /**
+ * Get the current camera regions. Used in ExposurePoint feature so it can always get a reference
+ * to the latest camera regions instance here.
+ *
+ * The CameraRegions will be replaced every time a new capture session is started.
+ *
+ * @return camera region object.
+ */
+ public CameraRegions getCameraRegions() {
+ final RegionBoundariesFeature regionBoundaries =
+ (RegionBoundariesFeature) cameraFeatures.get(CameraFeatures.regionBoundaries);
+ return regionBoundaries.getCameraRegions();
+ }
- result.error("setFlashModeFailed", "Could not set flash mode.", null);
- isFinished = true;
- }
- },
- null);
- } else {
- updateFlash(mode);
+ /**
+ * Set new exposure point from dart.
+ *
+ * @param result Flutter result.
+ * @param point The exposure point.
+ */
+ public void setExposurePoint(@NonNull final Result result, Point point) {
+ ((ExposurePointFeature) cameraFeatures.get(CameraFeatures.exposurePoint)).setValue(point);
+ cameraFeatures.get(CameraFeatures.exposurePoint).updateBuilder(previewRequestBuilder);
- refreshPreviewCaptureSession(
- () -> result.success(null),
- (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null));
- }
+ refreshPreviewCaptureSession(
+ () -> result.success(null),
+ (code, message) ->
+ result.error("setExposurePointFailed", "Could not set exposure point.", null));
}
- public void setExposureMode(@NonNull final Result result, ExposureMode mode)
- throws CameraAccessException {
- updateExposure(mode);
- cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
- result.success(null);
+ /** Return the max exposure offset value supported by the camera to dart. */
+ public double getMaxExposureOffset() {
+ final ExposureOffsetValue val =
+ (ExposureOffsetValue) cameraFeatures.get(CameraFeatures.exposureOffset).getValue();
+ return val.max;
}
- public void setExposurePoint(@NonNull final Result result, Double x, Double y)
- throws CameraAccessException {
- // Check if exposure point functionality is available.
- if (!isExposurePointSupported()) {
- result.error(
- "setExposurePointFailed", "Device does not have exposure point capabilities", null);
- return;
- }
- // Check if the current region boundaries are known
- if (cameraRegions.getMaxBoundaries() == null) {
- result.error("setExposurePointFailed", "Could not determine max region boundaries", null);
- return;
- }
- // Set the metering rectangle
- if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle();
- else cameraRegions.setAutoExposureMeteringRectangleFromPoint(y, 1 - x);
- // Apply it
- updateExposure(exposureMode);
- refreshPreviewCaptureSession(
- () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null));
+ /** Return the min exposure offset value supported by the camera to dart. */
+ public double getMinExposureOffset() {
+ final ExposureOffsetValue val =
+ (ExposureOffsetValue) cameraFeatures.get(CameraFeatures.exposureOffset).getValue();
+ return val.min;
}
- public void setFocusMode(@NonNull final Result result, FocusMode mode)
- throws CameraAccessException {
- this.focusMode = mode;
+ /** Return the exposure offset step size to dart. */
+ public double getExposureOffsetStepSize() {
+ final ExposureOffsetFeature val =
+ (ExposureOffsetFeature) cameraFeatures.get(CameraFeatures.exposureOffset);
+ return val.getExposureOffsetStepSize();
+ }
- updateFocus(mode);
+ /**
+ * Set new focus mode from dart.
+ *
+ * @param result Flutter result.
+ * @param newMode New mode.
+ */
+ public void setFocusMode(@NonNull final Result result, FocusMode newMode) {
+ ((AutoFocusFeature) cameraFeatures.get(CameraFeatures.autoFocus)).setValue(newMode);
- switch (mode) {
- case auto:
- refreshPreviewCaptureSession(
- null, (code, message) -> result.error("setFocusMode", message, null));
- break;
+ cameraFeatures.get(CameraFeatures.autoFocus).updateBuilder(previewRequestBuilder);
+
+ /*
+ * For focus mode we need to do an extra step of actually locking/unlocking the
+ * focus in order to ensure it goes into the correct state.
+ */
+ switch (newMode) {
case locked:
- lockAutoFocus(
- new CaptureCallback() {
- @Override
- public void onCaptureCompleted(
- @NonNull CameraCaptureSession session,
- @NonNull CaptureRequest request,
- @NonNull TotalCaptureResult result) {
- unlockAutoFocus();
- }
- });
+ // Perform a single focus trigger
+ lockAutoFocus();
+
+ // Set AF state to idle again
+ previewRequestBuilder.set(
+ CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE);
+
+ try {
+ captureSession.setRepeatingRequest(
+ previewRequestBuilder.build(), null, backgroundHandler);
+ } catch (CameraAccessException e) {
+ result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null);
+ }
break;
- }
- result.success(null);
- }
-
- public void setFocusPoint(@NonNull final Result result, Double x, Double y)
- throws CameraAccessException {
- // Check if focus point functionality is available.
- if (!isFocusPointSupported()) {
- result.error("setFocusPointFailed", "Device does not have focus point capabilities", null);
- return;
- }
- // Check if the current region boundaries are known
- if (cameraRegions.getMaxBoundaries() == null) {
- result.error("setFocusPointFailed", "Could not determine max region boundaries", null);
- return;
- }
-
- // Set the metering rectangle
- if (x == null || y == null) {
- cameraRegions.resetAutoFocusMeteringRectangle();
- } else {
- cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x);
+ case auto:
+ // Cancel current AF trigger and set AF to idle again
+ unlockAutoFocus();
+ break;
}
- // Apply the new metering rectangle
- setFocusMode(result, focusMode);
+ result.success(null);
}
- @TargetApi(VERSION_CODES.P)
- private boolean supportsDistortionCorrection() throws CameraAccessException {
- int[] availableDistortionCorrectionModes =
- cameraManager
- .getCameraCharacteristics(cameraDevice.getId())
- .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES);
- if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0];
- long nonOffModesSupported =
- Arrays.stream(availableDistortionCorrectionModes)
- .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF)
- .count();
- return nonOffModesSupported > 0;
- }
+ /**
+ * Sets new focus point from dart.
+ *
+ * @param result Flutter result.
+ * @param x new x.
+ * @param y new y.
+ */
+ public void setFocusPoint(@NonNull final Result result, Point point) {
+ ((FocusPointFeature) cameraFeatures.get(CameraFeatures.focusPoint)).setValue(point);
+ cameraFeatures.get(CameraFeatures.focusPoint).updateBuilder(previewRequestBuilder);
- private Size getRegionBoundaries() throws CameraAccessException {
- // No distortion correction support
- if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) {
- return cameraManager
- .getCameraCharacteristics(cameraDevice.getId())
- .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);
- }
- // Get the current distortion correction mode
- Integer distortionCorrectionMode =
- captureRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE);
- // Return the correct boundaries depending on the mode
- android.graphics.Rect rect;
- if (distortionCorrectionMode == null
- || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) {
- rect =
- cameraManager
- .getCameraCharacteristics(cameraDevice.getId())
- .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE);
- } else {
- rect =
- cameraManager
- .getCameraCharacteristics(cameraDevice.getId())
- .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
- }
- return rect == null ? null : new Size(rect.width(), rect.height());
+ refreshPreviewCaptureSession(
+ () -> result.success(null),
+ (code, message) -> result.error("setFocusPointFailed", "Could not set focus point.", null));
}
- private boolean isExposurePointSupported() throws CameraAccessException {
- Integer supportedRegions =
- cameraManager
- .getCameraCharacteristics(cameraDevice.getId())
- .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE);
- return supportedRegions != null && supportedRegions > 0;
- }
+ /**
+ * Set a new exposure offset from dart. From dart the offset comes as a double, like +1.3 or -1.3.
+ *
+ * @param result flutter result.
+ * @param offset new value.
+ */
+ public void setExposureOffset(@NonNull final Result result, double offset) {
+ ((ExposureOffsetFeature) cameraFeatures.get(CameraFeatures.exposureOffset))
+ .setValue(new ExposureOffsetValue(offset));
+ cameraFeatures.get(CameraFeatures.exposureOffset).updateBuilder(previewRequestBuilder);
- private boolean isFocusPointSupported() throws CameraAccessException {
- Integer supportedRegions =
- cameraManager
- .getCameraCharacteristics(cameraDevice.getId())
- .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF);
- return supportedRegions != null && supportedRegions > 0;
+ refreshPreviewCaptureSession(
+ () -> result.success(null),
+ (code, message) -> result.error("setFocusModeFailed", "Could not set focus mode.", null));
}
- public double getMinExposureOffset() throws CameraAccessException {
- Range range =
- cameraManager
- .getCameraCharacteristics(cameraDevice.getId())
- .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
- double minStepped = range == null ? 0 : range.getLower();
- double stepSize = getExposureOffsetStepSize();
- return minStepped * stepSize;
+ public float getMaxZoomLevel() {
+ final ZoomLevelFeature zoomLevel =
+ (ZoomLevelFeature) cameraFeatures.get(CameraFeatures.zoomLevel);
+ return zoomLevel.getCameraZoom().maxZoom;
}
- public double getMaxExposureOffset() throws CameraAccessException {
- Range range =
- cameraManager
- .getCameraCharacteristics(cameraDevice.getId())
- .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
- double maxStepped = range == null ? 0 : range.getUpper();
- double stepSize = getExposureOffsetStepSize();
- return maxStepped * stepSize;
+ public float getMinZoomLevel() {
+ return CameraZoom.DEFAULT_ZOOM_FACTOR;
}
- public double getExposureOffsetStepSize() throws CameraAccessException {
- Rational stepSize =
- cameraManager
- .getCameraCharacteristics(cameraDevice.getId())
- .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP);
- return stepSize == null ? 0.0 : stepSize.doubleValue();
+ /** Shortcut to get current preview size. */
+ Size getPreviewSize() {
+ return ((ResolutionFeature) cameraFeatures.get(CameraFeatures.resolution)).getPreviewSize();
}
- public void setExposureOffset(@NonNull final Result result, double offset)
- throws CameraAccessException {
- // Set the exposure offset
- double stepSize = getExposureOffsetStepSize();
- exposureOffset = (int) (offset / stepSize);
- // Apply it
- updateExposure(exposureMode);
- this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
- result.success(offset);
+ /** Shortcut to get current capture size. */
+ Size getCaptureSize() {
+ return ((ResolutionFeature) cameraFeatures.get(CameraFeatures.resolution)).getCaptureSize();
}
- public float getMaxZoomLevel() {
- return cameraZoom.maxZoom;
+ /** Shortcut to get current recording profile. */
+ CamcorderProfile getRecordingProfile() {
+ return ((ResolutionFeature) cameraFeatures.get(CameraFeatures.resolution))
+ .getRecordingProfile();
}
- public float getMinZoomLevel() {
- return CameraZoom.DEFAULT_ZOOM_FACTOR;
+ /** Shortut to get deviceOrientationListener. */
+ DeviceOrientationManager getDeviceOrientationManager() {
+ return ((SensorOrientationFeature) cameraFeatures.get(CameraFeatures.sensorOrientation))
+ .getDeviceOrientationManager();
}
+ /**
+ * Set zoom level from dart.
+ *
+ * @param result Flutter result.
+ * @param zoom new value.
+ */
public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException {
- float maxZoom = cameraZoom.maxZoom;
+ final ZoomLevelFeature zoomLevel =
+ (ZoomLevelFeature) cameraFeatures.get(CameraFeatures.zoomLevel);
+ float maxZoom = zoomLevel.getCameraZoom().maxZoom;
float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR;
if (zoom > maxZoom || zoom < minZoom) {
@@ -974,122 +1046,33 @@ public void setZoomLevel(@NonNull final Result result, float zoom) throws Camera
return;
}
- //Zoom area is calculated relative to sensor area (activeRect)
- if (captureRequestBuilder != null) {
- final Rect computedZoom = cameraZoom.computeZoom(zoom);
- captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom);
- cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
- }
+ zoomLevel.setValue(zoom);
+ zoomLevel.updateBuilder(previewRequestBuilder);
- result.success(null);
+ refreshPreviewCaptureSession(
+ () -> result.success(null),
+ (code, message) -> result.error("setZoomLevelFailed", "Could not set zoom level.", null));
}
+ /**
+ * Lock capture orientation from dart.
+ *
+ * @param orientation new orientation.
+ */
public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) {
- this.lockedCaptureOrientation = orientation;
+ ((SensorOrientationFeature) cameraFeatures.get(CameraFeatures.sensorOrientation))
+ .lockCaptureOrientation(orientation);
}
+ /** Unlock capture orientation from dart. */
public void unlockCaptureOrientation() {
- this.lockedCaptureOrientation = null;
- }
-
- private void updateFpsRange() {
- if (fpsRange == null) {
- return;
- }
-
- captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
- }
-
- private void updateFocus(FocusMode mode) {
- if (useAutoFocus) {
- int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
- // Auto focus is not supported
- if (modes == null
- || modes.length == 0
- || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) {
- useAutoFocus = false;
- captureRequestBuilder.set(
- CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF);
- } else {
- // Applying auto focus
- switch (mode) {
- case locked:
- captureRequestBuilder.set(
- CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
- break;
- case auto:
- captureRequestBuilder.set(
- CaptureRequest.CONTROL_AF_MODE,
- recordingVideo
- ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO
- : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
- default:
- break;
- }
- MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle();
- captureRequestBuilder.set(
- CaptureRequest.CONTROL_AF_REGIONS,
- afRect == null ? null : new MeteringRectangle[] {afRect});
- }
- } else {
- captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF);
- }
- }
-
- private void updateExposure(ExposureMode mode) {
- exposureMode = mode;
-
- // Applying auto exposure
- MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle();
- captureRequestBuilder.set(
- CaptureRequest.CONTROL_AE_REGIONS,
- aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()});
-
- switch (mode) {
- case locked:
- captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true);
- break;
- case auto:
- default:
- captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false);
- break;
- }
-
- captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset);
- }
-
- private void updateFlash(FlashMode mode) {
- // Get flash
- flashMode = mode;
-
- // Applying flash modes
- switch (flashMode) {
- case off:
- captureRequestBuilder.set(
- CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
- captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
- break;
- case auto:
- captureRequestBuilder.set(
- CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
- captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
- break;
- case always:
- captureRequestBuilder.set(
- CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
- captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
- break;
- case torch:
- default:
- captureRequestBuilder.set(
- CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
- captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
- break;
- }
+ ((SensorOrientationFeature) cameraFeatures.get(CameraFeatures.sensorOrientation))
+ .unlockCaptureOrientation();
}
public void startPreview() throws CameraAccessException {
if (pictureImageReader == null || pictureImageReader.getSurface() == null) return;
+ Log.i(TAG, "startPreview");
createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface());
}
@@ -1097,6 +1080,7 @@ public void startPreview() throws CameraAccessException {
public void startPreviewWithImageStream(EventChannel imageStreamChannel)
throws CameraAccessException {
createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface());
+ Log.i(TAG, "startPreviewWithImageStream");
imageStreamChannel.setStreamHandler(
new EventChannel.StreamHandler() {
@@ -1107,7 +1091,7 @@ public void onListen(Object o, EventChannel.EventSink imageStreamSink) {
@Override
public void onCancel(Object o) {
- imageStreamReader.setOnImageAvailableListener(null, null);
+ imageStreamReader.setOnImageAvailableListener(null, backgroundHandler);
}
});
}
@@ -1115,7 +1099,8 @@ public void onCancel(Object o) {
private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) {
imageStreamReader.setOnImageAvailableListener(
reader -> {
- Image img = reader.acquireLatestImage();
+ // Use acquireNextImage since our image reader is only for 1 image.
+ Image img = reader.acquireNextImage();
if (img == null) return;
List