| // 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.camera; |
| |
| import android.annotation.SuppressLint; |
| import android.annotation.TargetApi; |
| import android.app.Activity; |
| import android.content.Context; |
| import android.graphics.ImageFormat; |
| import android.graphics.SurfaceTexture; |
| import android.hardware.camera2.CameraAccessException; |
| import android.hardware.camera2.CameraCaptureSession; |
| import android.hardware.camera2.CameraDevice; |
| import android.hardware.camera2.CameraManager; |
| import android.hardware.camera2.CameraMetadata; |
| import android.hardware.camera2.CaptureRequest; |
| import android.hardware.camera2.TotalCaptureResult; |
| import android.hardware.camera2.params.OutputConfiguration; |
| import android.hardware.camera2.params.SessionConfiguration; |
| import android.media.CamcorderProfile; |
| import android.media.Image; |
| import android.media.ImageReader; |
| import android.media.MediaRecorder; |
| import android.os.Build; |
| 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.util.Log; |
| import android.util.Size; |
| import android.view.Display; |
| import android.view.Surface; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.lifecycle.Lifecycle; |
| import androidx.lifecycle.LifecycleObserver; |
| import androidx.lifecycle.OnLifecycleEvent; |
| import io.flutter.embedding.engine.systemchannels.PlatformChannel; |
| import io.flutter.plugin.common.EventChannel; |
| import io.flutter.plugin.common.MethodChannel; |
| import io.flutter.plugin.common.MethodChannel.Result; |
| 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.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.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.ZoomLevelFeature; |
| import io.flutter.plugins.camera.media.MediaRecorderBuilder; |
| import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; |
| import io.flutter.view.TextureRegistry.SurfaceTextureEntry; |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.concurrent.Executors; |
| |
| @FunctionalInterface |
| interface ErrorCallback { |
| void onError(String errorCode, String errorMessage); |
| } |
| |
| class Camera |
| implements CameraCaptureCallback.CameraCaptureStateListener, |
| ImageReader.OnImageAvailableListener, |
| LifecycleObserver { |
| private static final String TAG = "Camera"; |
| |
| private static final HashMap<String, Integer> supportedImageFormats; |
| |
| // 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 CameraFeatures cameraFeatures; |
| |
| private final SurfaceTextureEntry flutterTexture; |
| private final boolean enableAudio; |
| private final Context applicationContext; |
| private final DartMessenger dartMessenger; |
| 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; |
| |
| /** An additional thread for running tasks that shouldn't block the UI. */ |
| private HandlerThread backgroundHandlerThread; |
| |
| private CameraDevice cameraDevice; |
| private CameraCaptureSession captureSession; |
| private ImageReader pictureImageReader; |
| private ImageReader imageStreamReader; |
| /** {@link CaptureRequest.Builder} for the camera preview */ |
| private CaptureRequest.Builder previewRequestBuilder; |
| |
| private MediaRecorder mediaRecorder; |
| /** True when recording video. */ |
| private boolean recordingVideo; |
| |
| private File captureFile; |
| |
| /** Holds the current capture timeouts */ |
| private CaptureTimeoutsWrapper captureTimeouts; |
| |
| private MethodChannel.Result flutterResult; |
| |
| public Camera( |
| final Activity activity, |
| final SurfaceTextureEntry flutterTexture, |
| final CameraFeatureFactory cameraFeatureFactory, |
| final DartMessenger dartMessenger, |
| final CameraProperties cameraProperties, |
| final ResolutionPreset resolutionPreset, |
| final boolean enableAudio) { |
| |
| if (activity == null) { |
| throw new IllegalStateException("No activity available!"); |
| } |
| this.activity = activity; |
| this.enableAudio = enableAudio; |
| this.flutterTexture = flutterTexture; |
| this.dartMessenger = dartMessenger; |
| this.applicationContext = activity.getApplicationContext(); |
| this.cameraProperties = cameraProperties; |
| this.cameraFeatureFactory = cameraFeatureFactory; |
| this.cameraFeatures = |
| CameraFeatures.init( |
| cameraFeatureFactory, cameraProperties, activity, dartMessenger, resolutionPreset); |
| |
| // Create capture callback. |
| captureTimeouts = new CaptureTimeoutsWrapper(3000, 3000); |
| cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts); |
| |
| startBackgroundThread(); |
| } |
| |
| @Override |
| public void onConverged() { |
| takePictureAfterPrecapture(); |
| } |
| |
| @Override |
| public void onPrecapture() { |
| runPrecaptureSequence(); |
| } |
| |
| /** |
| * Updates the builder settings with all of the available features. |
| * |
| * @param requestBuilder request builder to update. |
| */ |
| private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { |
| for (CameraFeature feature : cameraFeatures.getAllFeatures()) { |
| Log.d(TAG, "Updating builder with feature: " + feature.getDebugName()); |
| feature.updateBuilder(requestBuilder); |
| } |
| } |
| |
| private void prepareMediaRecorder(String outputFilePath) throws IOException { |
| Log.i(TAG, "prepareMediaRecorder"); |
| |
| if (mediaRecorder != null) { |
| mediaRecorder.release(); |
| } |
| |
| final PlatformChannel.DeviceOrientation lockedOrientation = |
| ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) |
| .getLockedCaptureOrientation(); |
| |
| mediaRecorder = |
| new MediaRecorderBuilder(getRecordingProfile(), outputFilePath) |
| .setEnableAudio(enableAudio) |
| .setMediaOrientation( |
| lockedOrientation == null |
| ? getDeviceOrientationManager().getVideoOrientation() |
| : getDeviceOrientationManager().getVideoOrientation(lockedOrientation)) |
| .build(); |
| } |
| |
| @SuppressLint("MissingPermission") |
| public void open(String imageFormatGroup) throws CameraAccessException { |
| final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); |
| |
| if (!resolutionFeature.checkIsSupported()) { |
| // Tell the user that the camera they are trying to open is not supported, |
| // as its {@link android.media.CamcorderProfile} cannot be fetched due to the name |
| // not being a valid parsable integer. |
| dartMessenger.sendCameraErrorEvent( |
| "Camera with name \"" |
| + cameraProperties.getCameraName() |
| + "\" is not supported by this plugin."); |
| return; |
| } |
| |
| // Always capture using JPEG format. |
| pictureImageReader = |
| ImageReader.newInstance( |
| resolutionFeature.getCaptureSize().getWidth(), |
| resolutionFeature.getCaptureSize().getHeight(), |
| ImageFormat.JPEG, |
| 1); |
| |
| // For image streaming, 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; |
| } |
| imageStreamReader = |
| ImageReader.newInstance( |
| resolutionFeature.getPreviewSize().getWidth(), |
| resolutionFeature.getPreviewSize().getHeight(), |
| imageFormat, |
| 1); |
| |
| // Open the camera. |
| CameraManager cameraManager = CameraUtils.getCameraManager(activity); |
| cameraManager.openCamera( |
| cameraProperties.getCameraName(), |
| new CameraDevice.StateCallback() { |
| @Override |
| public void onOpened(@NonNull CameraDevice device) { |
| cameraDevice = device; |
| try { |
| startPreview(); |
| dartMessenger.sendCameraInitializedEvent( |
| resolutionFeature.getPreviewSize().getWidth(), |
| resolutionFeature.getPreviewSize().getHeight(), |
| cameraFeatures.getExposureLock().getValue(), |
| cameraFeatures.getAutoFocus().getValue(), |
| cameraFeatures.getExposurePoint().checkIsSupported(), |
| cameraFeatures.getFocusPoint().checkIsSupported()); |
| } catch (CameraAccessException e) { |
| dartMessenger.sendCameraErrorEvent(e.getMessage()); |
| close(); |
| } |
| } |
| |
| @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) { |
| case ERROR_CAMERA_IN_USE: |
| errorDescription = "The camera device is in use already."; |
| break; |
| case ERROR_MAX_CAMERAS_IN_USE: |
| errorDescription = "Max cameras in use"; |
| break; |
| case ERROR_CAMERA_DISABLED: |
| errorDescription = "The camera device could not be opened due to a device policy."; |
| break; |
| case ERROR_CAMERA_DEVICE: |
| errorDescription = "The camera device has encountered a fatal error"; |
| break; |
| case ERROR_CAMERA_SERVICE: |
| errorDescription = "The camera service has encountered a fatal error."; |
| break; |
| default: |
| errorDescription = "Unknown camera error"; |
| } |
| dartMessenger.sendCameraErrorEvent(errorDescription); |
| } |
| }, |
| backgroundHandler); |
| } |
| |
| private void createCaptureSession(int templateType, Surface... surfaces) |
| throws CameraAccessException { |
| createCaptureSession(templateType, null, surfaces); |
| } |
| |
| private void createCaptureSession( |
| int templateType, Runnable onSuccessCallback, Surface... surfaces) |
| throws CameraAccessException { |
| // Close any existing capture session. |
| closeCaptureSession(); |
| |
| // Create a new capture builder. |
| previewRequestBuilder = cameraDevice.createCaptureRequest(templateType); |
| |
| // Build Flutter surface to render to. |
| ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); |
| SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); |
| surfaceTexture.setDefaultBufferSize( |
| resolutionFeature.getPreviewSize().getWidth(), |
| resolutionFeature.getPreviewSize().getHeight()); |
| Surface flutterSurface = new Surface(surfaceTexture); |
| previewRequestBuilder.addTarget(flutterSurface); |
| |
| List<Surface> remainingSurfaces = Arrays.asList(surfaces); |
| if (templateType != CameraDevice.TEMPLATE_PREVIEW) { |
| // If it is not preview mode, add all surfaces as targets. |
| for (Surface surface : remainingSurfaces) { |
| previewRequestBuilder.addTarget(surface); |
| } |
| } |
| |
| // Update camera regions. |
| Size cameraBoundaries = |
| CameraRegionUtils.getCameraBoundaries(cameraProperties, previewRequestBuilder); |
| cameraFeatures.getExposurePoint().setCameraBoundaries(cameraBoundaries); |
| cameraFeatures.getFocusPoint().setCameraBoundaries(cameraBoundaries); |
| |
| // 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; |
| } |
| captureSession = session; |
| |
| Log.i(TAG, "Updating builder settings"); |
| updateBuilderSettings(previewRequestBuilder); |
| |
| refreshPreviewCaptureSession( |
| onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); |
| } |
| |
| @Override |
| public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { |
| dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); |
| } |
| }; |
| |
| // Start the session. |
| if (VERSION.SDK_INT >= VERSION_CODES.P) { |
| // Collect all surfaces to render to. |
| List<OutputConfiguration> configs = new ArrayList<>(); |
| configs.add(new OutputConfiguration(flutterSurface)); |
| for (Surface surface : remainingSurfaces) { |
| configs.add(new OutputConfiguration(surface)); |
| } |
| createCaptureSessionWithSessionConfig(configs, callback); |
| } else { |
| // Collect all surfaces to render to. |
| List<Surface> surfaceList = new ArrayList<>(); |
| surfaceList.add(flutterSurface); |
| surfaceList.addAll(remainingSurfaces); |
| createCaptureSession(surfaceList, callback); |
| } |
| } |
| |
| @TargetApi(VERSION_CODES.P) |
| private void createCaptureSessionWithSessionConfig( |
| List<OutputConfiguration> outputConfigs, CameraCaptureSession.StateCallback callback) |
| throws CameraAccessException { |
| cameraDevice.createCaptureSession( |
| new SessionConfiguration( |
| SessionConfiguration.SESSION_REGULAR, |
| outputConfigs, |
| Executors.newSingleThreadExecutor(), |
| callback)); |
| } |
| |
| @TargetApi(VERSION_CODES.LOLLIPOP) |
| @SuppressWarnings("deprecation") |
| private void createCaptureSession( |
| List<Surface> surfaces, CameraCaptureSession.StateCallback callback) |
| throws CameraAccessException { |
| cameraDevice.createCaptureSession(surfaces, callback, backgroundHandler); |
| } |
| |
| // Send a repeating request to refresh capture session. |
| private void refreshPreviewCaptureSession( |
| @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { |
| if (captureSession == null) { |
| Log.i( |
| TAG, |
| "[refreshPreviewCaptureSession] captureSession not yet initialized, " |
| + "skipping preview capture session refresh."); |
| return; |
| } |
| |
| try { |
| captureSession.setRepeatingRequest( |
| previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); |
| |
| if (onSuccessCallback != null) { |
| onSuccessCallback.run(); |
| } |
| |
| } catch (CameraAccessException e) { |
| onErrorCallback.onError("cameraAccess", e.getMessage()); |
| } |
| } |
| |
| public void takePicture(@NonNull final Result result) { |
| // Only take one picture at a time. |
| if (cameraCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) { |
| result.error("captureAlreadyActive", "Picture is currently already being captured", null); |
| return; |
| } |
| |
| flutterResult = result; |
| |
| // Create temporary file. |
| final File outputDir = applicationContext.getCacheDir(); |
| try { |
| captureFile = File.createTempFile("CAP", ".jpg", outputDir); |
| captureTimeouts.reset(); |
| } catch (IOException | SecurityException e) { |
| dartMessenger.error(flutterResult, "cannotCreateFile", e.getMessage(), null); |
| return; |
| } |
| |
| // Listen for picture being taken. |
| pictureImageReader.setOnImageAvailableListener(this, backgroundHandler); |
| |
| final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); |
| final boolean isAutoFocusSupported = autoFocusFeature.checkIsSupported(); |
| if (isAutoFocusSupported && autoFocusFeature.getValue() == FocusMode.auto) { |
| runPictureAutoFocus(); |
| } else { |
| runPrecaptureSequence(); |
| } |
| } |
| |
| /** |
| * Run the precapture sequence for capturing a still image. This method should be called when a |
| * response is received 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) -> dartMessenger.error(flutterResult, "cameraAccess", message, null)); |
| |
| // Start precapture. |
| cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_PRECAPTURE_START); |
| |
| previewRequestBuilder.set( |
| CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, |
| CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); |
| |
| // Trigger one capture to start AE sequence. |
| captureSession.capture( |
| previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); |
| |
| } catch (CameraAccessException e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| /** |
| * Capture a still picture. This method should be called when a response is received {@link |
| * #cameraCaptureCallback} from both lockFocus(). |
| */ |
| private void takePictureAfterPrecapture() { |
| Log.i(TAG, "captureStillPicture"); |
| cameraCaptureCallback.setCameraState(CameraState.STATE_CAPTURING); |
| |
| if (cameraDevice == null) { |
| return; |
| } |
| // This is the CaptureRequest.Builder that is used to take a picture. |
| CaptureRequest.Builder stillBuilder; |
| try { |
| stillBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); |
| } catch (CameraAccessException e) { |
| dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); |
| return; |
| } |
| stillBuilder.addTarget(pictureImageReader.getSurface()); |
| |
| // Zoom. |
| stillBuilder.set( |
| CaptureRequest.SCALER_CROP_REGION, |
| previewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); |
| |
| // Have all features update the builder. |
| updateBuilderSettings(stillBuilder); |
| |
| // Orientation. |
| final PlatformChannel.DeviceOrientation lockedOrientation = |
| ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) |
| .getLockedCaptureOrientation(); |
| stillBuilder.set( |
| CaptureRequest.JPEG_ORIENTATION, |
| lockedOrientation == null |
| ? getDeviceOrientationManager().getPhotoOrientation() |
| : getDeviceOrientationManager().getPhotoOrientation(lockedOrientation)); |
| |
| CameraCaptureSession.CaptureCallback captureCallback = |
| new CameraCaptureSession.CaptureCallback() { |
| @Override |
| public void onCaptureCompleted( |
| @NonNull CameraCaptureSession session, |
| @NonNull CaptureRequest request, |
| @NonNull TotalCaptureResult result) { |
| unlockAutoFocus(); |
| } |
| }; |
| |
| try { |
| captureSession.stopRepeating(); |
| captureSession.abortCaptures(); |
| Log.i(TAG, "sending capture request"); |
| captureSession.capture(stillBuilder.build(), captureCallback, backgroundHandler); |
| } catch (CameraAccessException e) { |
| dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); |
| } |
| } |
| |
| @SuppressWarnings("deprecation") |
| private Display getDefaultDisplay() { |
| return activity.getWindowManager().getDefaultDisplay(); |
| } |
| |
| /** Starts a background thread and its {@link Handler}. */ |
| @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) |
| public void startBackgroundThread() { |
| backgroundHandlerThread = new HandlerThread("CameraBackground"); |
| try { |
| backgroundHandlerThread.start(); |
| } catch (IllegalThreadStateException e) { |
| // Ignore exception in case the thread has already started. |
| } |
| backgroundHandler = new Handler(backgroundHandlerThread.getLooper()); |
| } |
| |
| /** Stops the background thread and its {@link Handler}. */ |
| @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) |
| public void stopBackgroundThread() { |
| if (backgroundHandlerThread != null) { |
| backgroundHandlerThread.quitSafely(); |
| try { |
| backgroundHandlerThread.join(); |
| } catch (InterruptedException e) { |
| dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); |
| } |
| } |
| backgroundHandlerThread = null; |
| backgroundHandler = null; |
| } |
| |
| /** Start capturing a picture, doing autofocus first. */ |
| private void runPictureAutoFocus() { |
| Log.i(TAG, "runPictureAutoFocus"); |
| |
| cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); |
| lockAutoFocus(); |
| } |
| |
| private void lockAutoFocus() { |
| Log.i(TAG, "lockAutoFocus"); |
| if (captureSession == null) { |
| Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); |
| return; |
| } |
| |
| // Trigger AF to start. |
| previewRequestBuilder.set( |
| CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); |
| |
| try { |
| captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); |
| } catch (CameraAccessException e) { |
| dartMessenger.sendCameraErrorEvent(e.getMessage()); |
| } |
| } |
| |
| /** Cancel and reset auto focus state and refresh the preview session. */ |
| private void unlockAutoFocus() { |
| Log.i(TAG, "unlockAutoFocus"); |
| if (captureSession == null) { |
| Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); |
| return; |
| } |
| try { |
| // 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) { |
| dartMessenger.sendCameraErrorEvent(e.getMessage()); |
| return; |
| } |
| |
| refreshPreviewCaptureSession( |
| null, |
| (errorCode, errorMessage) -> |
| dartMessenger.error(flutterResult, errorCode, errorMessage, null)); |
| } |
| |
| public void startVideoRecording(@NonNull Result result) { |
| final File outputDir = applicationContext.getCacheDir(); |
| try { |
| captureFile = File.createTempFile("REC", ".mp4", outputDir); |
| } catch (IOException | SecurityException e) { |
| result.error("cannotCreateFile", e.getMessage(), null); |
| return; |
| } |
| try { |
| prepareMediaRecorder(captureFile.getAbsolutePath()); |
| } catch (IOException e) { |
| recordingVideo = false; |
| captureFile = null; |
| result.error("videoRecordingFailed", e.getMessage(), null); |
| return; |
| } |
| // Re-create autofocus feature so it's using video focus mode now. |
| cameraFeatures.setAutoFocus( |
| cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); |
| recordingVideo = true; |
| try { |
| createCaptureSession( |
| CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); |
| result.success(null); |
| } catch (CameraAccessException e) { |
| recordingVideo = false; |
| captureFile = null; |
| result.error("videoRecordingFailed", e.getMessage(), null); |
| } |
| } |
| |
| public void stopVideoRecording(@NonNull final Result result) { |
| if (!recordingVideo) { |
| result.success(null); |
| return; |
| } |
| // Re-create autofocus feature so it's using continuous capture focus mode now. |
| cameraFeatures.setAutoFocus( |
| cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); |
| recordingVideo = false; |
| try { |
| captureSession.abortCaptures(); |
| mediaRecorder.stop(); |
| } catch (CameraAccessException | IllegalStateException e) { |
| // Ignore exceptions and try to continue (changes are camera session already aborted capture). |
| } |
| mediaRecorder.reset(); |
| try { |
| startPreview(); |
| } catch (CameraAccessException | IllegalStateException e) { |
| result.error("videoRecordingFailed", e.getMessage(), null); |
| return; |
| } |
| result.success(captureFile.getAbsolutePath()); |
| captureFile = null; |
| } |
| |
| public void pauseVideoRecording(@NonNull final Result result) { |
| if (!recordingVideo) { |
| result.success(null); |
| return; |
| } |
| |
| try { |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { |
| mediaRecorder.pause(); |
| } else { |
| result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); |
| return; |
| } |
| } catch (IllegalStateException e) { |
| result.error("videoRecordingFailed", e.getMessage(), null); |
| return; |
| } |
| |
| result.success(null); |
| } |
| |
| public void resumeVideoRecording(@NonNull final Result result) { |
| if (!recordingVideo) { |
| result.success(null); |
| return; |
| } |
| |
| try { |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { |
| mediaRecorder.resume(); |
| } else { |
| result.error( |
| "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); |
| return; |
| } |
| } catch (IllegalStateException e) { |
| result.error("videoRecordingFailed", e.getMessage(), null); |
| return; |
| } |
| |
| result.success(null); |
| } |
| |
| /** |
| * Method handler for setting new flash modes. |
| * |
| * @param result Flutter result. |
| * @param newMode new mode. |
| */ |
| public void setFlashMode(@NonNull final Result result, @NonNull FlashMode newMode) { |
| // Save the new flash mode setting. |
| final FlashFeature flashFeature = cameraFeatures.getFlash(); |
| flashFeature.setValue(newMode); |
| flashFeature.updateBuilder(previewRequestBuilder); |
| |
| refreshPreviewCaptureSession( |
| () -> result.success(null), |
| (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); |
| } |
| |
| /** |
| * Method handler for setting new exposure modes. |
| * |
| * @param result Flutter result. |
| * @param newMode new mode. |
| */ |
| public void setExposureMode(@NonNull final Result result, @NonNull ExposureMode newMode) { |
| final ExposureLockFeature exposureLockFeature = cameraFeatures.getExposureLock(); |
| exposureLockFeature.setValue(newMode); |
| exposureLockFeature.updateBuilder(previewRequestBuilder); |
| |
| refreshPreviewCaptureSession( |
| () -> result.success(null), |
| (code, message) -> |
| result.error("setExposureModeFailed", "Could not set exposure mode.", null)); |
| } |
| |
| /** |
| * Sets new exposure point from dart. |
| * |
| * @param result Flutter result. |
| * @param point The exposure point. |
| */ |
| public void setExposurePoint(@NonNull final Result result, @Nullable Point point) { |
| final ExposurePointFeature exposurePointFeature = cameraFeatures.getExposurePoint(); |
| exposurePointFeature.setValue(point); |
| exposurePointFeature.updateBuilder(previewRequestBuilder); |
| |
| refreshPreviewCaptureSession( |
| () -> result.success(null), |
| (code, message) -> |
| result.error("setExposurePointFailed", "Could not set exposure point.", null)); |
| } |
| |
| /** Return the max exposure offset value supported by the camera to dart. */ |
| public double getMaxExposureOffset() { |
| return cameraFeatures.getExposureOffset().getMaxExposureOffset(); |
| } |
| |
| /** Return the min exposure offset value supported by the camera to dart. */ |
| public double getMinExposureOffset() { |
| return cameraFeatures.getExposureOffset().getMinExposureOffset(); |
| } |
| |
| /** Return the exposure offset step size to dart. */ |
| public double getExposureOffsetStepSize() { |
| return cameraFeatures.getExposureOffset().getExposureOffsetStepSize(); |
| } |
| |
| /** |
| * Sets new focus mode from dart. |
| * |
| * @param result Flutter result. |
| * @param newMode New mode. |
| */ |
| public void setFocusMode(final Result result, @NonNull FocusMode newMode) { |
| final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); |
| autoFocusFeature.setValue(newMode); |
| autoFocusFeature.updateBuilder(previewRequestBuilder); |
| |
| /* |
| * For focus mode an extra step of actually locking/unlocking the |
| * focus has to be done, in order to ensure it goes into the correct state. |
| */ |
| switch (newMode) { |
| case locked: |
| // Perform a single focus trigger. |
| lockAutoFocus(); |
| if (captureSession == null) { |
| Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); |
| return; |
| } |
| |
| // 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) { |
| if (result != null) { |
| result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); |
| } |
| return; |
| } |
| break; |
| case auto: |
| // Cancel current AF trigger and set AF to idle again. |
| unlockAutoFocus(); |
| break; |
| } |
| |
| if (result != null) { |
| result.success(null); |
| } |
| } |
| |
| /** |
| * Sets new focus point from dart. |
| * |
| * @param result Flutter result. |
| * @param point the new coordinates. |
| */ |
| public void setFocusPoint(@NonNull final Result result, @Nullable Point point) { |
| final FocusPointFeature focusPointFeature = cameraFeatures.getFocusPoint(); |
| focusPointFeature.setValue(point); |
| focusPointFeature.updateBuilder(previewRequestBuilder); |
| |
| refreshPreviewCaptureSession( |
| () -> result.success(null), |
| (code, message) -> result.error("setFocusPointFailed", "Could not set focus point.", null)); |
| |
| this.setFocusMode(null, cameraFeatures.getAutoFocus().getValue()); |
| } |
| |
| /** |
| * Sets 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) { |
| final ExposureOffsetFeature exposureOffsetFeature = cameraFeatures.getExposureOffset(); |
| exposureOffsetFeature.setValue(offset); |
| exposureOffsetFeature.updateBuilder(previewRequestBuilder); |
| |
| refreshPreviewCaptureSession( |
| () -> result.success(null), |
| (code, message) -> |
| result.error("setExposureOffsetFailed", "Could not set exposure offset.", null)); |
| } |
| |
| public float getMaxZoomLevel() { |
| return cameraFeatures.getZoomLevel().getMaximumZoomLevel(); |
| } |
| |
| public float getMinZoomLevel() { |
| return cameraFeatures.getZoomLevel().getMinimumZoomLevel(); |
| } |
| |
| /** Shortcut to get current recording profile. */ |
| CamcorderProfile getRecordingProfile() { |
| return cameraFeatures.getResolution().getRecordingProfile(); |
| } |
| |
| /** Shortut to get deviceOrientationListener. */ |
| DeviceOrientationManager getDeviceOrientationManager() { |
| return cameraFeatures.getSensorOrientation().getDeviceOrientationManager(); |
| } |
| |
| /** |
| * Sets zoom level from dart. |
| * |
| * @param result Flutter result. |
| * @param zoom new value. |
| */ |
| public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { |
| final ZoomLevelFeature zoomLevel = cameraFeatures.getZoomLevel(); |
| float maxZoom = zoomLevel.getMaximumZoomLevel(); |
| float minZoom = zoomLevel.getMinimumZoomLevel(); |
| |
| if (zoom > maxZoom || zoom < minZoom) { |
| String errorMessage = |
| String.format( |
| Locale.ENGLISH, |
| "Zoom level out of bounds (zoom level should be between %f and %f).", |
| minZoom, |
| maxZoom); |
| result.error("ZOOM_ERROR", errorMessage, null); |
| return; |
| } |
| |
| zoomLevel.setValue(zoom); |
| zoomLevel.updateBuilder(previewRequestBuilder); |
| |
| 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) { |
| cameraFeatures.getSensorOrientation().lockCaptureOrientation(orientation); |
| } |
| |
| /** Unlock capture orientation from dart. */ |
| public void unlockCaptureOrientation() { |
| cameraFeatures.getSensorOrientation().unlockCaptureOrientation(); |
| } |
| |
| public void startPreview() throws CameraAccessException { |
| if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; |
| Log.i(TAG, "startPreview"); |
| |
| createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); |
| } |
| |
| public void startPreviewWithImageStream(EventChannel imageStreamChannel) |
| throws CameraAccessException { |
| createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); |
| Log.i(TAG, "startPreviewWithImageStream"); |
| |
| imageStreamChannel.setStreamHandler( |
| new EventChannel.StreamHandler() { |
| @Override |
| public void onListen(Object o, EventChannel.EventSink imageStreamSink) { |
| setImageStreamImageAvailableListener(imageStreamSink); |
| } |
| |
| @Override |
| public void onCancel(Object o) { |
| imageStreamReader.setOnImageAvailableListener(null, backgroundHandler); |
| } |
| }); |
| } |
| |
| /** |
| * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a |
| * still image is ready to be saved. |
| */ |
| @Override |
| public void onImageAvailable(ImageReader reader) { |
| Log.i(TAG, "onImageAvailable"); |
| |
| backgroundHandler.post( |
| new ImageSaver( |
| // Use acquireNextImage since image reader is only for one image. |
| reader.acquireNextImage(), |
| captureFile, |
| new ImageSaver.Callback() { |
| @Override |
| public void onComplete(String absolutePath) { |
| dartMessenger.finish(flutterResult, absolutePath); |
| } |
| |
| @Override |
| public void onError(String errorCode, String errorMessage) { |
| dartMessenger.error(flutterResult, errorCode, errorMessage, null); |
| } |
| })); |
| cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW); |
| } |
| |
| private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { |
| imageStreamReader.setOnImageAvailableListener( |
| reader -> { |
| // Use acquireNextImage since image reader is only for one image. |
| Image img = reader.acquireNextImage(); |
| if (img == null) return; |
| |
| List<Map<String, Object>> planes = new ArrayList<>(); |
| for (Image.Plane plane : img.getPlanes()) { |
| ByteBuffer buffer = plane.getBuffer(); |
| |
| byte[] bytes = new byte[buffer.remaining()]; |
| buffer.get(bytes, 0, bytes.length); |
| |
| Map<String, Object> planeBuffer = new HashMap<>(); |
| planeBuffer.put("bytesPerRow", plane.getRowStride()); |
| planeBuffer.put("bytesPerPixel", plane.getPixelStride()); |
| planeBuffer.put("bytes", bytes); |
| |
| planes.add(planeBuffer); |
| } |
| |
| Map<String, Object> imageBuffer = new HashMap<>(); |
| imageBuffer.put("width", img.getWidth()); |
| imageBuffer.put("height", img.getHeight()); |
| imageBuffer.put("format", img.getFormat()); |
| imageBuffer.put("planes", planes); |
| |
| final Handler handler = new Handler(Looper.getMainLooper()); |
| handler.post(() -> imageStreamSink.success(imageBuffer)); |
| img.close(); |
| }, |
| backgroundHandler); |
| } |
| |
| private void closeCaptureSession() { |
| if (captureSession != null) { |
| Log.i(TAG, "closeCaptureSession"); |
| |
| captureSession.close(); |
| captureSession = null; |
| } |
| } |
| |
| public void close() { |
| Log.i(TAG, "close"); |
| closeCaptureSession(); |
| |
| if (cameraDevice != null) { |
| cameraDevice.close(); |
| cameraDevice = null; |
| } |
| if (pictureImageReader != null) { |
| pictureImageReader.close(); |
| pictureImageReader = null; |
| } |
| if (imageStreamReader != null) { |
| imageStreamReader.close(); |
| imageStreamReader = null; |
| } |
| if (mediaRecorder != null) { |
| mediaRecorder.reset(); |
| mediaRecorder.release(); |
| mediaRecorder = null; |
| } |
| |
| stopBackgroundThread(); |
| } |
| |
| public void dispose() { |
| Log.i(TAG, "dispose"); |
| |
| close(); |
| flutterTexture.release(); |
| getDeviceOrientationManager().stop(); |
| } |
| } |