| // Copyright 2014 The Chromium 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 org.chromium.media; |
| |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.graphics.ImageFormat; |
| import android.graphics.Rect; |
| import android.hardware.camera2.CameraAccessException; |
| import android.hardware.camera2.CameraCaptureSession; |
| import android.hardware.camera2.CameraCharacteristics; |
| import android.hardware.camera2.CameraDevice; |
| import android.hardware.camera2.CameraManager; |
| import android.hardware.camera2.CameraMetadata; |
| 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.StreamConfigurationMap; |
| import android.media.Image; |
| import android.media.ImageReader; |
| import android.os.Build; |
| import android.os.ConditionVariable; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.util.Range; |
| import android.util.Size; |
| import android.util.SparseIntArray; |
| import android.view.Surface; |
| |
| import androidx.annotation.IntDef; |
| |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.Log; |
| import org.chromium.base.TraceEvent; |
| import org.chromium.base.annotations.JNINamespace; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * This class implements Video Capture using Camera2 API, introduced in Android |
| * API 21 (L Release). Capture takes place in the current Looper, while pixel |
| * download takes place in another thread used by ImageReader. A number of |
| * static methods are provided to retrieve information on current system cameras |
| * and their capabilities, using android.hardware.camera2.CameraManager. |
| **/ |
| @JNINamespace("media") |
| @TargetApi(Build.VERSION_CODES.M) |
| public class VideoCaptureCamera2 extends VideoCapture { |
| // Inner class to extend a CameraDevice state change listener. |
| private class CrStateListener extends CameraDevice.StateCallback { |
| @Override |
| public void onOpened(CameraDevice cameraDevice) { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| |
| Log.e(TAG, "CameraDevice.StateCallback onOpened"); |
| mCameraDevice = cameraDevice; |
| mWaitForDeviceClosedConditionVariable.close(); |
| changeCameraStateAndNotify(CameraState.CONFIGURING); |
| createPreviewObjectsAndStartPreviewOrFailWith( |
| AndroidVideoCaptureError.ANDROID_API_2_ERROR_CONFIGURING_CAMERA); |
| } |
| |
| @Override |
| public void onDisconnected(CameraDevice cameraDevice) { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| Log.e(TAG, "cameraDevice was closed unexpectedly"); |
| |
| cameraDevice.close(); |
| mCameraDevice = null; |
| changeCameraStateAndNotify(CameraState.STOPPED); |
| } |
| |
| @Override |
| public void onError(CameraDevice cameraDevice, int error) { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| Log.e(TAG, "cameraDevice encountered an error"); |
| |
| cameraDevice.close(); |
| mCameraDevice = null; |
| changeCameraStateAndNotify(CameraState.STOPPED); |
| VideoCaptureJni.get().onError(mNativeVideoCaptureDeviceAndroid, |
| VideoCaptureCamera2.this, |
| AndroidVideoCaptureError.ANDROID_API_2_CAMERA_DEVICE_ERROR_RECEIVED, |
| "Camera device error " + Integer.toString(error)); |
| } |
| |
| @Override |
| public void onClosed(CameraDevice camera) { |
| Log.d(TAG, "cameraDevice closed"); |
| // If we called mCameraDevice.close() while mPreviewSession was running, |
| // mPreviewSession will get closed, but the corresponding CrPreviewSessionListener |
| // will not receive a callback to onClosed(). Therefore we have to clean up |
| // the reference to mPreviewSession here. |
| if (mPreviewSession != null) { |
| mPreviewSession = null; |
| } |
| |
| mWaitForDeviceClosedConditionVariable.open(); |
| } |
| }; |
| |
| // Inner class to extend a Capture Session state change listener. |
| private class CrPreviewSessionListener extends CameraCaptureSession.StateCallback { |
| private final CaptureRequest mPreviewRequest; |
| CrPreviewSessionListener(CaptureRequest previewRequest) { |
| mPreviewRequest = previewRequest; |
| } |
| |
| @Override |
| public void onConfigured(CameraCaptureSession cameraCaptureSession) { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| |
| Log.d(TAG, "CrPreviewSessionListener.onConfigured"); |
| mPreviewSession = cameraCaptureSession; |
| try { |
| // This line triggers the preview. A |listener| is registered to receive the actual |
| // capture result details. A CrImageReaderListener will be triggered every time a |
| // downloaded image is ready. Since |handler| is null, we'll work on the current |
| // Thread Looper. |
| mPreviewSession.setRepeatingRequest( |
| mPreviewRequest, new CameraCaptureSession.CaptureCallback() { |
| @Override |
| public void onCaptureCompleted(CameraCaptureSession session, |
| CaptureRequest request, TotalCaptureResult result) { |
| // Since |result| is not guaranteed to contain a value for |
| // key |SENSOR_EXPOSURE_TIME| we have to check for null. |
| Long exposure_time_value = |
| result.get(CaptureResult.SENSOR_EXPOSURE_TIME); |
| if (exposure_time_value == null) return; |
| mLastExposureTimeNs = exposure_time_value; |
| } |
| }, null); |
| |
| } catch (CameraAccessException | SecurityException | IllegalStateException |
| | IllegalArgumentException ex) { |
| Log.e(TAG, "setRepeatingRequest: ", ex); |
| return; |
| } |
| |
| changeCameraStateAndNotify(CameraState.STARTED); |
| VideoCaptureJni.get().onStarted( |
| mNativeVideoCaptureDeviceAndroid, VideoCaptureCamera2.this); |
| |
| // Frames will be arriving at CrPreviewReaderListener.onImageAvailable(); |
| } |
| |
| @Override |
| public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| Log.d(TAG, "CrPreviewSessionListener.onConfigureFailed"); |
| |
| // TODO(mcasas): When signalling error, C++ will tear us down. Is there need for |
| // cleanup? |
| changeCameraStateAndNotify(CameraState.STOPPED); |
| mPreviewSession = null; |
| VideoCaptureJni.get().onError(mNativeVideoCaptureDeviceAndroid, |
| VideoCaptureCamera2.this, |
| AndroidVideoCaptureError.ANDROID_API_2_CAPTURE_SESSION_CONFIGURE_FAILED, |
| "Camera session configuration error"); |
| } |
| |
| @Override |
| public void onClosed(CameraCaptureSession cameraCaptureSession) { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| Log.d(TAG, "CrPreviewSessionListener.onClosed"); |
| |
| // The preview session gets closed temporarily when a takePhoto |
| // request is being processed. A new preview session will be |
| // started after that. |
| mPreviewSession = null; |
| } |
| }; |
| |
| // Internal class implementing an ImageReader listener for Preview frames. Gets pinged when a |
| // new frame is been captured and downloads it to memory-backed buffers. |
| private class CrPreviewReaderListener implements ImageReader.OnImageAvailableListener { |
| @Override |
| public void onImageAvailable(ImageReader reader) { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| |
| try (Image image = reader.acquireLatestImage()) { |
| if (image == null) { |
| VideoCaptureJni.get().onFrameDropped(mNativeVideoCaptureDeviceAndroid, |
| VideoCaptureCamera2.this, |
| AndroidVideoCaptureFrameDropReason |
| .ANDROID_API_2_ACQUIRED_IMAGE_IS_NULL); |
| return; |
| } |
| |
| if (image.getFormat() != ImageFormat.YUV_420_888 || image.getPlanes().length != 3) { |
| VideoCaptureJni.get().onError(mNativeVideoCaptureDeviceAndroid, |
| VideoCaptureCamera2.this, |
| AndroidVideoCaptureError |
| .ANDROID_API_2_IMAGE_READER_UNEXPECTED_IMAGE_FORMAT, |
| "Unexpected image format: " + image.getFormat() |
| + " or #planes: " + image.getPlanes().length); |
| throw new IllegalStateException(); |
| } |
| |
| if (reader.getWidth() != image.getWidth() |
| || reader.getHeight() != image.getHeight()) { |
| VideoCaptureJni.get().onError(mNativeVideoCaptureDeviceAndroid, |
| VideoCaptureCamera2.this, |
| AndroidVideoCaptureError |
| .ANDROID_API_2_IMAGE_READER_SIZE_DID_NOT_MATCH_IMAGE_SIZE, |
| "ImageReader size (" + reader.getWidth() + "x" + reader.getHeight() |
| + ") did not match Image size (" + image.getWidth() + "x" |
| + image.getHeight() + ")"); |
| throw new IllegalStateException(); |
| } |
| |
| VideoCaptureJni.get().onI420FrameAvailable(mNativeVideoCaptureDeviceAndroid, |
| VideoCaptureCamera2.this, image.getPlanes()[0].getBuffer(), |
| image.getPlanes()[0].getRowStride(), image.getPlanes()[1].getBuffer(), |
| image.getPlanes()[2].getBuffer(), image.getPlanes()[1].getRowStride(), |
| image.getPlanes()[1].getPixelStride(), image.getWidth(), image.getHeight(), |
| getCameraRotation(), image.getTimestamp()); |
| } catch (IllegalStateException ex) { |
| Log.e(TAG, "acquireLatestImage():", ex); |
| } |
| } |
| }; |
| |
| // Inner class to extend a Photo Session state change listener. |
| // Error paths must signal notifyTakePhotoError(). |
| private class CrPhotoSessionListener extends CameraCaptureSession.StateCallback { |
| private final ImageReader mImageReader; |
| private final CaptureRequest mPhotoRequest; |
| private final long mCallbackId; |
| CrPhotoSessionListener( |
| ImageReader imageReader, CaptureRequest photoRequest, long callbackId) { |
| mImageReader = imageReader; |
| mPhotoRequest = photoRequest; |
| mCallbackId = callbackId; |
| } |
| |
| @Override |
| public void onConfigured(CameraCaptureSession session) { |
| TraceEvent.instant("VideoCaptureCamera2.java", "CrPhotoSessionListener.onConfigured"); |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| |
| Log.d(TAG, "CrPhotoSessionListener.onConfigured"); |
| try { |
| TraceEvent.instant( |
| "VideoCaptureCamera2.java", "Calling CameraCaptureSession.capture()"); |
| // This line triggers a single photo capture. No |listener| is registered, so we |
| // will get notified via a CrPhotoSessionListener. Since |handler| is null, we'll |
| // work on the current Thread Looper. |
| session.capture(mPhotoRequest, null, null); |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "capture() CameraAccessException", ex); |
| notifyTakePhotoError(mCallbackId); |
| return; |
| } catch (IllegalStateException ex) { |
| Log.e(TAG, "capture() IllegalStateException", ex); |
| notifyTakePhotoError(mCallbackId); |
| return; |
| } |
| } |
| |
| @Override |
| public void onConfigureFailed(CameraCaptureSession session) { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| |
| Log.e(TAG, "failed configuring capture session"); |
| notifyTakePhotoError(mCallbackId); |
| return; |
| } |
| |
| @Override |
| public void onClosed(CameraCaptureSession session) { |
| mImageReader.close(); |
| } |
| }; |
| |
| // Internal class implementing an ImageReader listener for encoded Photos. |
| // Gets pinged when a new Image is been captured. |
| private class CrPhotoReaderListener implements ImageReader.OnImageAvailableListener { |
| private final long mCallbackId; |
| CrPhotoReaderListener(long callbackId) { |
| mCallbackId = callbackId; |
| } |
| |
| private byte[] readCapturedData(Image image) { |
| byte[] capturedData = null; |
| try { |
| capturedData = image.getPlanes()[0].getBuffer().array(); |
| } catch (UnsupportedOperationException ex) { |
| // Try reading the pixels in a different way. |
| final ByteBuffer buffer = image.getPlanes()[0].getBuffer(); |
| capturedData = new byte[buffer.remaining()]; |
| buffer.get(capturedData); |
| } finally { |
| return capturedData; |
| } |
| } |
| |
| @Override |
| public void onImageAvailable(ImageReader reader) { |
| TraceEvent.instant( |
| "VideoCaptureCamera2.java", "CrPhotoReaderListener.onImageAvailable"); |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| |
| try (Image image = reader.acquireLatestImage()) { |
| if (image == null) { |
| throw new IllegalStateException(); |
| } |
| |
| if (image.getFormat() != ImageFormat.JPEG) { |
| Log.e(TAG, "Unexpected image format: %d", image.getFormat()); |
| throw new IllegalStateException(); |
| } |
| |
| final byte[] capturedData = readCapturedData(image); |
| VideoCaptureJni.get().onPhotoTaken(mNativeVideoCaptureDeviceAndroid, |
| VideoCaptureCamera2.this, mCallbackId, capturedData); |
| |
| } catch (IllegalStateException ex) { |
| notifyTakePhotoError(mCallbackId); |
| return; |
| } |
| |
| createPreviewObjectsAndStartPreviewOrFailWith( |
| AndroidVideoCaptureError.ANDROID_API_2_ERROR_RESTARTING_PREVIEW); |
| } |
| }; |
| |
| private class StopCaptureTask implements Runnable { |
| @Override |
| public void run() { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| |
| if (mCameraDevice == null) return; |
| |
| // As per Android API documentation, this will automatically abort captures |
| // pending for mPreviewSession, but it will not lead to callbacks such as |
| // onClosed() to the corresponding CrPreviewSessionListener. |
| // Different from what the Android API documentation says, pending frames |
| // may still get delivered after this call. Therefore, we have to wait for |
| // CrStateListener.onClosed() in order to have a guarantee that no more |
| // frames are delivered. |
| mCameraDevice.close(); |
| |
| changeCameraStateAndNotify(CameraState.STOPPED); |
| mCropRect = new Rect(); |
| } |
| } |
| |
| private class GetPhotoCapabilitiesTask implements Runnable { |
| private final long mCallbackId; |
| public GetPhotoCapabilitiesTask(long callbackId) { |
| mCallbackId = callbackId; |
| } |
| |
| @Override |
| public void run() { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| |
| final CameraCharacteristics cameraCharacteristics = getCameraCharacteristics(mId); |
| PhotoCapabilities.Builder builder = new PhotoCapabilities.Builder(); |
| if (cameraCharacteristics == null) { |
| VideoCaptureJni.get().onGetPhotoCapabilitiesReply(mNativeVideoCaptureDeviceAndroid, |
| VideoCaptureCamera2.this, mCallbackId, builder.build()); |
| return; |
| } |
| int minIso = 0; |
| int maxIso = 0; |
| final Range<Integer> iso_range = |
| cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE); |
| if (iso_range != null) { |
| minIso = iso_range.getLower(); |
| maxIso = iso_range.getUpper(); |
| } |
| builder.setInt(PhotoCapabilityInt.MIN_ISO, minIso) |
| .setInt(PhotoCapabilityInt.MAX_ISO, maxIso) |
| .setInt(PhotoCapabilityInt.STEP_ISO, 1); |
| if (mPreviewRequest.get(CaptureRequest.SENSOR_SENSITIVITY) != null) { |
| builder.setInt(PhotoCapabilityInt.CURRENT_ISO, |
| mPreviewRequest.get(CaptureRequest.SENSOR_SENSITIVITY)); |
| } |
| |
| final StreamConfigurationMap streamMap = cameraCharacteristics.get( |
| CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); |
| final Size[] supportedSizes = streamMap.getOutputSizes(ImageFormat.JPEG); |
| int minWidth = Integer.MAX_VALUE; |
| int minHeight = Integer.MAX_VALUE; |
| int maxWidth = 0; |
| int maxHeight = 0; |
| for (Size size : supportedSizes) { |
| if (size.getWidth() < minWidth) minWidth = size.getWidth(); |
| if (size.getHeight() < minHeight) minHeight = size.getHeight(); |
| if (size.getWidth() > maxWidth) maxWidth = size.getWidth(); |
| if (size.getHeight() > maxHeight) maxHeight = size.getHeight(); |
| } |
| builder.setInt(PhotoCapabilityInt.MIN_HEIGHT, minHeight) |
| .setInt(PhotoCapabilityInt.MAX_HEIGHT, maxHeight) |
| .setInt(PhotoCapabilityInt.STEP_HEIGHT, 1) |
| .setInt(PhotoCapabilityInt.CURRENT_HEIGHT, |
| (mPhotoHeight > 0) ? mPhotoHeight : mCaptureFormat.getHeight()) |
| .setInt(PhotoCapabilityInt.MIN_WIDTH, minWidth) |
| .setInt(PhotoCapabilityInt.MAX_WIDTH, maxWidth) |
| .setInt(PhotoCapabilityInt.STEP_WIDTH, 1) |
| .setInt(PhotoCapabilityInt.CURRENT_WIDTH, |
| (mPhotoWidth > 0) ? mPhotoWidth : mCaptureFormat.getWidth()); |
| |
| float currentZoom = 1.0f; |
| if (cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE) |
| != null |
| && mPreviewRequest.get(CaptureRequest.SCALER_CROP_REGION) != null) { |
| currentZoom = cameraCharacteristics |
| .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE) |
| .width() |
| / (float) mPreviewRequest.get(CaptureRequest.SCALER_CROP_REGION).width(); |
| } |
| // There is no min-zoom per se, so clamp it to always 1. |
| builder.setDouble(PhotoCapabilityDouble.MIN_ZOOM, 1.0) |
| .setDouble(PhotoCapabilityDouble.MAX_ZOOM, mMaxZoom) |
| .setDouble(PhotoCapabilityDouble.CURRENT_ZOOM, currentZoom) |
| .setDouble(PhotoCapabilityDouble.STEP_ZOOM, 0.1); |
| |
| // Classify the Focus capabilities. In CONTINUOUS and SINGLE_SHOT, we can call |
| // autoFocus(AutoFocusCallback) to configure region(s) to focus onto. |
| final int[] jniFocusModes = |
| cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); |
| ArrayList<Integer> focusModes = new ArrayList<Integer>(3); |
| |
| // Android reports the focus metadata in units of diopters (1/meter), so |
| // 0.0f represents focusing at infinity, and increasing positive numbers represent |
| // focusing closer and closer to the camera device. |
| float minFocusDistance = 0; // >= 0 |
| float maxFocusDistance = 0; // (0.0f, android.lens.info.minimumFocusDistance] |
| if (cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE) |
| != null) { |
| minFocusDistance = cameraCharacteristics.get( |
| CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); |
| if (minFocusDistance == 0) { |
| Log.d(TAG, "lens is fixed-focus"); |
| } else if (minFocusDistance > 0) { |
| // Android provides focusDistance in diopters, but specs is in SI units |
| // (meters). |
| minFocusDistance = 1 / minFocusDistance; |
| } |
| } else { // null value |
| Log.d(TAG, "LENS_INFO_MINIMUM_FOCUS_DISTANCE is null"); |
| } |
| if (cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE) |
| != null) { |
| maxFocusDistance = cameraCharacteristics.get( |
| CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); |
| if (maxFocusDistance == 0) { |
| maxFocusDistance = (long) Double.POSITIVE_INFINITY; |
| } else if (maxFocusDistance > 0) { |
| // Android provides focusDistance in diopters, but specs is in SI units |
| // (meters). |
| maxFocusDistance = 1 / maxFocusDistance; |
| } |
| } else { // null value |
| Log.d(TAG, "LENS_INFO_HYPERFOCAL_DISTANCE is null"); |
| } |
| if (mPreviewRequest.get(CaptureRequest.LENS_FOCUS_DISTANCE) != null) { |
| mCurrentFocusDistance = mPreviewRequest.get(CaptureRequest.LENS_FOCUS_DISTANCE); |
| |
| // LENS_FOCUS_DISTANCE is in the range [0.0f, |
| // android.lens.info.minimumFocusDistance] Android provides focusDistance in |
| // diopters, but specs is in SI units (meters). |
| if (mCurrentFocusDistance == 0) { |
| Log.d(TAG, "infinity focus."); |
| mCurrentFocusDistance = (long) Double.POSITIVE_INFINITY; |
| } else if (mCurrentFocusDistance > 0) { |
| builder.setDouble(PhotoCapabilityDouble.CURRENT_FOCUS_DISTANCE, |
| 1 / mCurrentFocusDistance); |
| } |
| } else { // null value |
| Log.d(TAG, "LENS_FOCUS_DISTANCE is null"); |
| } |
| |
| for (int mode : jniFocusModes) { |
| if (mode == CameraMetadata.CONTROL_AF_MODE_OFF) { |
| focusModes.add(Integer.valueOf(AndroidMeteringMode.FIXED)); |
| // Smallest step by which focus distance can be changed. This value is not |
| // exposed by Android. |
| float mStepFocusDistance = 0.01f; |
| builder.setDouble(PhotoCapabilityDouble.MIN_FOCUS_DISTANCE, minFocusDistance) |
| .setDouble(PhotoCapabilityDouble.MAX_FOCUS_DISTANCE, maxFocusDistance) |
| .setDouble( |
| PhotoCapabilityDouble.STEP_FOCUS_DISTANCE, mStepFocusDistance); |
| } else if (mode == CameraMetadata.CONTROL_AF_MODE_AUTO |
| || mode == CameraMetadata.CONTROL_AF_MODE_MACRO) { |
| // CONTROL_AF_MODE_{AUTO,MACRO} do not imply continuously focusing. |
| if (!focusModes.contains(Integer.valueOf(AndroidMeteringMode.SINGLE_SHOT))) { |
| focusModes.add(Integer.valueOf(AndroidMeteringMode.SINGLE_SHOT)); |
| } |
| } else if (mode == CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO |
| || mode == CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE |
| || mode == CameraMetadata.CONTROL_AF_MODE_EDOF) { |
| if (!focusModes.contains(Integer.valueOf(AndroidMeteringMode.CONTINUOUS))) { |
| focusModes.add(Integer.valueOf(AndroidMeteringMode.CONTINUOUS)); |
| } |
| } |
| } |
| builder.setMeteringModeArray( |
| MeteringModeType.FOCUS, integerArrayListToArray(focusModes)); |
| |
| int jniFocusMode = AndroidMeteringMode.NONE; |
| if (mPreviewRequest.get(CaptureRequest.CONTROL_AF_MODE) != null) { |
| final int focusMode = mPreviewRequest.get(CaptureRequest.CONTROL_AF_MODE); |
| if (focusMode == CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO |
| || focusMode == CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE) { |
| jniFocusMode = AndroidMeteringMode.CONTINUOUS; |
| } else if (focusMode == CameraMetadata.CONTROL_AF_MODE_AUTO |
| || focusMode == CameraMetadata.CONTROL_AF_MODE_MACRO) { |
| jniFocusMode = AndroidMeteringMode.SINGLE_SHOT; |
| } else if (focusMode == CameraMetadata.CONTROL_AF_MODE_OFF) { |
| jniFocusMode = AndroidMeteringMode.FIXED; |
| // Set focus distance here. |
| if (mCurrentFocusDistance > 0) { |
| builder.setDouble(PhotoCapabilityDouble.CURRENT_FOCUS_DISTANCE, |
| 1 / mCurrentFocusDistance); |
| } |
| } else { |
| assert jniFocusMode == CameraMetadata.CONTROL_AF_MODE_EDOF; |
| } |
| } |
| builder.setMeteringMode(MeteringModeType.FOCUS, jniFocusMode); |
| |
| // Auto Exposure is the usual capability and state, unless AE is not available at all, |
| // which is signalled by an empty CONTROL_AE_AVAILABLE_MODES list. Exposure Compensation |
| // can also support or be locked, this is equivalent to AndroidMeteringMode.FIXED. |
| final int[] jniExposureModes = |
| cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES); |
| ArrayList<Integer> exposureModes = new ArrayList<Integer>(1); |
| for (int mode : jniExposureModes) { |
| if (mode == CameraMetadata.CONTROL_AE_MODE_ON |
| || mode == CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH |
| || mode == CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH |
| || mode == CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE) { |
| exposureModes.add(Integer.valueOf(AndroidMeteringMode.CONTINUOUS)); |
| break; |
| } else { |
| // Exposure mode is Manual. Here we can set exposure time. |
| // All exposure time values from Android are in nano seconds. |
| // Spec (https://w3c.github.io/mediacapture-image/#exposure-time) |
| // expects exposureTime to be in 100 microsecond units. |
| // A value of 1.0 means an exposure time of 1/10000th of a second |
| // and a value of 10000.0 means an exposure time of 1 second. |
| if (cameraCharacteristics.get( |
| CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE) |
| != null) { |
| // The minimum exposure time will be less than 100 micro-seconds. |
| // For FULL capability devices (android.info.supportedHardwareLevel == |
| // FULL), the maximum exposure time will be greater than 100 millisecond. |
| Range<Long> range = cameraCharacteristics.get( |
| CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE); |
| final long minExposureTime = range.getLower(); |
| final long maxExposureTime = range.getUpper(); |
| |
| if (minExposureTime != 0 && maxExposureTime != 0) { |
| builder.setDouble(PhotoCapabilityDouble.MAX_EXPOSURE_TIME, |
| maxExposureTime / kNanosecondsPer100Microsecond) |
| .setDouble(PhotoCapabilityDouble.MIN_EXPOSURE_TIME, |
| minExposureTime / kNanosecondsPer100Microsecond); |
| } |
| // Smallest step by which exposure time can be changed. This value is not |
| // exposed by Android. |
| builder.setDouble(PhotoCapabilityDouble.STEP_EXPOSURE_TIME, |
| 10000 / kNanosecondsPer100Microsecond) |
| .setDouble(PhotoCapabilityDouble.CURRENT_EXPOSURE_TIME, |
| mLastExposureTimeNs / kNanosecondsPer100Microsecond); |
| } |
| } |
| } |
| try { |
| Boolean ae_lock_available = |
| cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_LOCK_AVAILABLE); |
| if (ae_lock_available != null && ae_lock_available.booleanValue()) { |
| exposureModes.add(Integer.valueOf(AndroidMeteringMode.FIXED)); |
| } |
| } catch (NoSuchFieldError e) { |
| // Ignore this exception, it means CONTROL_AE_LOCK_AVAILABLE is not known. |
| } |
| builder.setMeteringModeArray( |
| MeteringModeType.EXPOSURE, integerArrayListToArray(exposureModes)); |
| |
| int jniExposureMode = AndroidMeteringMode.CONTINUOUS; |
| if ((mPreviewRequest.get(CaptureRequest.CONTROL_AE_MODE) != null) |
| && mPreviewRequest.get(CaptureRequest.CONTROL_AE_MODE) |
| == CameraMetadata.CONTROL_AE_MODE_OFF) { |
| jniExposureMode = AndroidMeteringMode.NONE; |
| } |
| if (mPreviewRequest.get(CaptureRequest.CONTROL_AE_LOCK) != null |
| && mPreviewRequest.get(CaptureRequest.CONTROL_AE_LOCK)) { |
| jniExposureMode = AndroidMeteringMode.FIXED; |
| } |
| builder.setMeteringMode(MeteringModeType.EXPOSURE, jniExposureMode); |
| |
| final float step = |
| cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP) |
| .floatValue(); |
| builder.setDouble(PhotoCapabilityDouble.STEP_EXPOSURE_COMPENSATION, step); |
| final Range<Integer> exposureCompensationRange = |
| cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); |
| builder.setDouble(PhotoCapabilityDouble.MIN_EXPOSURE_COMPENSATION, |
| exposureCompensationRange.getLower() * step) |
| .setDouble(PhotoCapabilityDouble.MAX_EXPOSURE_COMPENSATION, |
| exposureCompensationRange.getUpper() * step); |
| if (mPreviewRequest.get(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION) != null) { |
| builder.setDouble(PhotoCapabilityDouble.CURRENT_EXPOSURE_COMPENSATION, |
| mPreviewRequest.get(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION) |
| * step); |
| } |
| |
| final int[] jniWhiteBalanceMode = |
| cameraCharacteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES); |
| ArrayList<Integer> whiteBalanceModes = new ArrayList<Integer>(1); |
| for (int mode : jniWhiteBalanceMode) { |
| if (mode == CameraMetadata.CONTROL_AWB_MODE_AUTO) { |
| whiteBalanceModes.add(Integer.valueOf(AndroidMeteringMode.CONTINUOUS)); |
| break; |
| } |
| } |
| try { |
| Boolean awb_lock_available = |
| cameraCharacteristics.get(CameraCharacteristics.CONTROL_AWB_LOCK_AVAILABLE); |
| if (awb_lock_available != null && awb_lock_available.booleanValue()) { |
| whiteBalanceModes.add(Integer.valueOf(AndroidMeteringMode.FIXED)); |
| } |
| } catch (NoSuchFieldError e) { |
| // Ignore this exception, it means CONTROL_AWB_LOCK_AVAILABLE is not known. |
| } |
| builder.setMeteringModeArray( |
| MeteringModeType.WHITE_BALANCE, integerArrayListToArray(whiteBalanceModes)); |
| |
| int whiteBalanceMode = CameraMetadata.CONTROL_AWB_MODE_AUTO; |
| if (mPreviewRequest.get(CaptureRequest.CONTROL_AWB_MODE) != null) { |
| whiteBalanceMode = mPreviewRequest.get(CaptureRequest.CONTROL_AWB_MODE); |
| if (whiteBalanceMode == CameraMetadata.CONTROL_AWB_MODE_OFF) { |
| builder.setMeteringMode( |
| MeteringModeType.WHITE_BALANCE, AndroidMeteringMode.NONE); |
| } else { |
| builder.setMeteringMode(MeteringModeType.WHITE_BALANCE, |
| whiteBalanceMode == CameraMetadata.CONTROL_AWB_MODE_AUTO |
| ? AndroidMeteringMode.CONTINUOUS |
| : AndroidMeteringMode.FIXED); |
| } |
| } |
| builder.setInt(PhotoCapabilityInt.MIN_COLOR_TEMPERATURE, |
| COLOR_TEMPERATURES_MAP.keyAt(0)) |
| .setInt(PhotoCapabilityInt.MAX_COLOR_TEMPERATURE, |
| COLOR_TEMPERATURES_MAP.keyAt(COLOR_TEMPERATURES_MAP.size() - 1)) |
| .setInt(PhotoCapabilityInt.STEP_COLOR_TEMPERATURE, 50); |
| final int index = COLOR_TEMPERATURES_MAP.indexOfValue(whiteBalanceMode); |
| if (index >= 0) { |
| builder.setInt(PhotoCapabilityInt.CURRENT_COLOR_TEMPERATURE, |
| COLOR_TEMPERATURES_MAP.keyAt(index)); |
| } |
| |
| if (!cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE)) { |
| builder.setBool(PhotoCapabilityBool.SUPPORTS_TORCH, false) |
| .setBool(PhotoCapabilityBool.RED_EYE_REDUCTION, false); |
| } else { |
| // There's no way to query if torch and/or red eye reduction modes are available |
| // using Camera2 API but since there's a Flash unit, we assume so. |
| builder.setBool(PhotoCapabilityBool.SUPPORTS_TORCH, true) |
| .setBool(PhotoCapabilityBool.RED_EYE_REDUCTION, true); |
| |
| if (mPreviewRequest.get(CaptureRequest.FLASH_MODE) != null) { |
| builder.setBool(PhotoCapabilityBool.TORCH, |
| mPreviewRequest.get(CaptureRequest.FLASH_MODE) |
| == CameraMetadata.FLASH_MODE_TORCH); |
| } |
| |
| final int[] flashModes = |
| cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES); |
| ArrayList<Integer> modes = new ArrayList<Integer>(0); |
| for (int flashMode : flashModes) { |
| if (flashMode == CameraMetadata.FLASH_MODE_OFF) { |
| modes.add(Integer.valueOf(AndroidFillLightMode.OFF)); |
| } else if (flashMode == CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH) { |
| modes.add(Integer.valueOf(AndroidFillLightMode.AUTO)); |
| } else if (flashMode == CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH) { |
| modes.add(Integer.valueOf(AndroidFillLightMode.FLASH)); |
| } |
| } |
| builder.setFillLightModeArray(integerArrayListToArray(modes)); |
| } |
| |
| VideoCaptureJni.get().onGetPhotoCapabilitiesReply(mNativeVideoCaptureDeviceAndroid, |
| VideoCaptureCamera2.this, mCallbackId, builder.build()); |
| } |
| } |
| |
| private class PhotoOptions { |
| public final double zoom; |
| public final int focusMode; |
| public final double currentFocusDistance; |
| public final int exposureMode; |
| public final double width; |
| public final double height; |
| public final double[] pointsOfInterest2D; |
| public final boolean hasExposureCompensation; |
| public final double exposureCompensation; |
| public final double exposureTime; |
| public final int whiteBalanceMode; |
| public final double iso; |
| public final boolean hasRedEyeReduction; |
| public final boolean redEyeReduction; |
| public final int fillLightMode; |
| public final boolean hasTorch; |
| public final boolean torch; |
| public final double colorTemperature; |
| |
| public PhotoOptions(double zoom, int focusMode, double currentFocusDistance, |
| int exposureMode, double width, double height, double[] pointsOfInterest2D, |
| boolean hasExposureCompensation, double exposureCompensation, double exposureTime, |
| int whiteBalanceMode, double iso, boolean hasRedEyeReduction, |
| boolean redEyeReduction, int fillLightMode, boolean hasTorch, boolean torch, |
| double colorTemperature) { |
| this.zoom = zoom; |
| this.focusMode = focusMode; |
| this.currentFocusDistance = currentFocusDistance; |
| this.exposureMode = exposureMode; |
| this.width = width; |
| this.height = height; |
| this.pointsOfInterest2D = pointsOfInterest2D; |
| this.hasExposureCompensation = hasExposureCompensation; |
| this.exposureCompensation = exposureCompensation; |
| this.exposureTime = exposureTime; |
| this.whiteBalanceMode = whiteBalanceMode; |
| this.iso = iso; |
| this.hasRedEyeReduction = hasRedEyeReduction; |
| this.redEyeReduction = redEyeReduction; |
| this.fillLightMode = fillLightMode; |
| this.hasTorch = hasTorch; |
| this.torch = torch; |
| this.colorTemperature = colorTemperature; |
| } |
| } |
| |
| private class SetPhotoOptionsTask implements Runnable { |
| private final PhotoOptions mOptions; |
| |
| public SetPhotoOptionsTask(PhotoOptions options) { |
| mOptions = options; |
| } |
| |
| @Override |
| public void run() { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| |
| final CameraCharacteristics cameraCharacteristics = getCameraCharacteristics(mId); |
| if (cameraCharacteristics == null) return; |
| final Rect canvas = |
| cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); |
| |
| if (mOptions.zoom != 0) { |
| final float normalizedZoom = |
| Math.max(1.0f, Math.min((float) mOptions.zoom, mMaxZoom)); |
| final float cropFactor = (normalizedZoom - 1) / (2 * normalizedZoom); |
| |
| mCropRect = new Rect(Math.round(canvas.width() * cropFactor), |
| Math.round(canvas.height() * cropFactor), |
| Math.round(canvas.width() * (1 - cropFactor)), |
| Math.round(canvas.height() * (1 - cropFactor))); |
| Log.d(TAG, "zoom level %f, rectangle: %s", normalizedZoom, mCropRect.toString()); |
| } |
| |
| if (mOptions.focusMode != AndroidMeteringMode.NOT_SET) mFocusMode = mOptions.focusMode; |
| if (mOptions.currentFocusDistance != 0) { |
| mCurrentFocusDistance = (float) mOptions.currentFocusDistance; |
| } |
| if (mOptions.exposureMode != AndroidMeteringMode.NOT_SET) { |
| mExposureMode = mOptions.exposureMode; |
| } |
| if (mOptions.exposureTime != 0) { |
| // The web API (https://w3c.github.io/mediacapture-image/#exposure-time) provides |
| // exposureTime in 100 microsecond units. |
| mLastExposureTimeNs = |
| (long) (mOptions.exposureTime * kNanosecondsPer100Microsecond); |
| } |
| if (mOptions.whiteBalanceMode != AndroidMeteringMode.NOT_SET) { |
| mWhiteBalanceMode = mOptions.whiteBalanceMode; |
| } |
| if (mOptions.width > 0) mPhotoWidth = (int) Math.round(mOptions.width); |
| if (mOptions.height > 0) mPhotoHeight = (int) Math.round(mOptions.height); |
| |
| // Upon new |zoom| configuration, clear up the previous |mAreaOfInterest| if any. |
| if (mAreaOfInterest != null && !mAreaOfInterest.getRect().isEmpty() |
| && mOptions.zoom > 0) { |
| mAreaOfInterest = null; |
| } |
| // Also clear |mAreaOfInterest| if the user sets it as NONE. |
| if (mFocusMode == AndroidMeteringMode.NONE |
| || mExposureMode == AndroidMeteringMode.NONE) { |
| mAreaOfInterest = null; |
| } |
| // Update |mAreaOfInterest| if the camera supports and there are |pointsOfInterest2D|. |
| final boolean pointsOfInterestSupported = |
| cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) > 0 |
| || cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) > 0 |
| || cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB) > 0; |
| if (pointsOfInterestSupported && mOptions.pointsOfInterest2D.length > 0) { |
| assert mOptions.pointsOfInterest2D.length |
| == 2 : "Only 1 point of interest supported"; |
| assert mOptions.pointsOfInterest2D[0] <= 1.0 |
| && mOptions.pointsOfInterest2D[0] >= 0.0; |
| assert mOptions.pointsOfInterest2D[1] <= 1.0 |
| && mOptions.pointsOfInterest2D[1] >= 0.0; |
| // Calculate a Rect of 1/8 the |visibleRect| dimensions, and center it w.r.t. |
| // |canvas|. |
| final Rect visibleRect = (mCropRect.isEmpty()) ? canvas : mCropRect; |
| int centerX = |
| (int) Math.round(mOptions.pointsOfInterest2D[0] * visibleRect.width()); |
| int centerY = |
| (int) Math.round(mOptions.pointsOfInterest2D[1] * visibleRect.height()); |
| if (visibleRect.equals(mCropRect)) { |
| centerX += (canvas.width() - visibleRect.width()) / 2; |
| centerY += (canvas.height() - visibleRect.height()) / 2; |
| } |
| final int regionWidth = visibleRect.width() / 8; |
| final int regionHeight = visibleRect.height() / 8; |
| |
| mAreaOfInterest = new MeteringRectangle(Math.max(0, centerX - regionWidth / 2), |
| Math.max(0, centerY - regionHeight / 2), regionWidth, regionHeight, |
| MeteringRectangle.METERING_WEIGHT_MAX); |
| |
| Log.d(TAG, "Calculating (%.2fx%.2f) wrt to %s (canvas being %s)", |
| mOptions.pointsOfInterest2D[0], mOptions.pointsOfInterest2D[1], |
| visibleRect.toString(), canvas.toString()); |
| Log.d(TAG, "Area of interest %s", mAreaOfInterest.toString()); |
| } |
| |
| if (mOptions.hasExposureCompensation) { |
| mExposureCompensation = (int) Math.round(mOptions.exposureCompensation |
| / cameraCharacteristics |
| .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP) |
| .floatValue()); |
| } |
| if (mOptions.iso > 0) mIso = (int) Math.round(mOptions.iso); |
| if (mOptions.colorTemperature > 0) { |
| mColorTemperature = (int) Math.round(mOptions.colorTemperature); |
| } |
| if (mOptions.hasRedEyeReduction) mRedEyeReduction = mOptions.redEyeReduction; |
| if (mOptions.fillLightMode != AndroidFillLightMode.NOT_SET) { |
| mFillLightMode = mOptions.fillLightMode; |
| } |
| if (mOptions.hasTorch) mTorch = mOptions.torch; |
| |
| if (mPreviewSession != null) { |
| assert mPreviewRequestBuilder != null : "preview request builder"; |
| |
| // Reuse most of |mPreviewRequestBuilder| since it has expensive items inside that |
| // have to do with preview, e.g. the ImageReader and its associated Surface. |
| configureCommonCaptureSettings(mPreviewRequestBuilder); |
| |
| if (mOptions.fillLightMode != AndroidFillLightMode.NOT_SET) { |
| // Run the precapture sequence for capturing a still image. |
| mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, |
| CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); |
| } |
| |
| mPreviewRequest = mPreviewRequestBuilder.build(); |
| |
| try { |
| mPreviewSession.setRepeatingRequest(mPreviewRequest, null, null); |
| } catch (CameraAccessException | SecurityException | IllegalStateException |
| | IllegalArgumentException ex) { |
| Log.e(TAG, "setRepeatingRequest: ", ex); |
| } |
| } |
| } |
| } |
| |
| private class TakePhotoTask implements Runnable { |
| private final long mCallbackId; |
| public TakePhotoTask(long callbackId) { |
| mCallbackId = callbackId; |
| } |
| |
| @Override |
| public void run() { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| TraceEvent.instant("VideoCaptureCamera2.java", "TakePhotoTask.run"); |
| |
| if (mCameraDevice == null || mCameraState != CameraState.STARTED) { |
| Log.e(TAG, |
| "TakePhoto failed because mCameraDevice == null || " |
| + "mCameraState != CameraState.STARTED"); |
| notifyTakePhotoError(mCallbackId); |
| return; |
| } |
| |
| final CameraCharacteristics cameraCharacteristics = getCameraCharacteristics(mId); |
| if (cameraCharacteristics == null) { |
| Log.e(TAG, "cameraCharacteristics error"); |
| notifyTakePhotoError(mCallbackId); |
| return; |
| } |
| final StreamConfigurationMap streamMap = cameraCharacteristics.get( |
| CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); |
| final Size[] supportedSizes = streamMap.getOutputSizes(ImageFormat.JPEG); |
| final Size closestSize = |
| findClosestSizeInArray(supportedSizes, mPhotoWidth, mPhotoHeight); |
| |
| Log.d(TAG, "requested resolution: (%dx%d)", mPhotoWidth, mPhotoHeight); |
| if (closestSize != null) { |
| Log.d(TAG, " matched (%dx%d)", closestSize.getWidth(), closestSize.getHeight()); |
| } |
| TraceEvent.instant( |
| "VideoCaptureCamera2.java", "TakePhotoTask.run creating ImageReader"); |
| final ImageReader imageReader = ImageReader.newInstance( |
| (closestSize != null) ? closestSize.getWidth() : mCaptureFormat.getWidth(), |
| (closestSize != null) ? closestSize.getHeight() : mCaptureFormat.getHeight(), |
| ImageFormat.JPEG, 1 /* maxImages */); |
| |
| final CrPhotoReaderListener photoReaderListener = |
| new CrPhotoReaderListener(mCallbackId); |
| imageReader.setOnImageAvailableListener(photoReaderListener, mCameraThreadHandler); |
| |
| final List<Surface> surfaceList = new ArrayList<Surface>(1); |
| // TODO(mcasas): release this Surface when not needed, https://crbug.com/643884. |
| surfaceList.add(imageReader.getSurface()); |
| |
| CaptureRequest.Builder photoRequestBuilder = null; |
| try { |
| photoRequestBuilder = |
| mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "createCaptureRequest() error ", ex); |
| notifyTakePhotoError(mCallbackId); |
| return; |
| } |
| if (photoRequestBuilder == null) { |
| Log.e(TAG, "photoRequestBuilder error"); |
| notifyTakePhotoError(mCallbackId); |
| return; |
| } |
| photoRequestBuilder.addTarget(imageReader.getSurface()); |
| photoRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getCameraRotation()); |
| |
| TraceEvent.instant("VideoCaptureCamera2.java", |
| "TakePhotoTask.run calling configureCommonCaptureSettings"); |
| configureCommonCaptureSettings(photoRequestBuilder); |
| |
| TraceEvent.instant("VideoCaptureCamera2.java", |
| "TakePhotoTask.run calling photoRequestBuilder.build()"); |
| final CaptureRequest photoRequest = photoRequestBuilder.build(); |
| final CrPhotoSessionListener sessionListener = |
| new CrPhotoSessionListener(imageReader, photoRequest, mCallbackId); |
| try { |
| TraceEvent.instant("VideoCaptureCamera2.java", |
| "TakePhotoTask.run calling mCameraDevice.createCaptureSession()"); |
| mCameraDevice.createCaptureSession( |
| surfaceList, sessionListener, mCameraThreadHandler); |
| } catch (CameraAccessException | IllegalArgumentException | SecurityException ex) { |
| Log.e(TAG, "createCaptureSession: " + ex); |
| notifyTakePhotoError(mCallbackId); |
| } |
| } |
| } |
| |
| private static final double kNanosecondsPerSecond = 1000000000; |
| private static final long kNanosecondsPer100Microsecond = 100000; |
| private static final String TAG = "VideoCapture"; |
| |
| private static final String[] AE_TARGET_FPS_RANGE_BUGGY_DEVICE_LIST = { |
| // See https://crbug.com/913203 for more info. |
| "Pixel 3", |
| "Pixel 3 XL", |
| }; |
| |
| // Map of the equivalent color temperature in Kelvin for the White Balance setting. The |
| // values are a mixture of educated guesses and data from Android's Camera2 API. The |
| // temperatures must be ordered increasingly. |
| private static final SparseIntArray COLOR_TEMPERATURES_MAP; |
| static { |
| COLOR_TEMPERATURES_MAP = new SparseIntArray(); |
| COLOR_TEMPERATURES_MAP.append(2850, CameraMetadata.CONTROL_AWB_MODE_INCANDESCENT); |
| COLOR_TEMPERATURES_MAP.append(2950, CameraMetadata.CONTROL_AWB_MODE_WARM_FLUORESCENT); |
| COLOR_TEMPERATURES_MAP.append(4250, CameraMetadata.CONTROL_AWB_MODE_FLUORESCENT); |
| COLOR_TEMPERATURES_MAP.append(4600, CameraMetadata.CONTROL_AWB_MODE_TWILIGHT); |
| COLOR_TEMPERATURES_MAP.append(5000, CameraMetadata.CONTROL_AWB_MODE_DAYLIGHT); |
| COLOR_TEMPERATURES_MAP.append(6000, CameraMetadata.CONTROL_AWB_MODE_CLOUDY_DAYLIGHT); |
| COLOR_TEMPERATURES_MAP.append(7000, CameraMetadata.CONTROL_AWB_MODE_SHADE); |
| }; |
| |
| @IntDef({CameraState.OPENING, CameraState.CONFIGURING, CameraState.STARTED, |
| CameraState.STOPPED}) |
| @Retention(RetentionPolicy.SOURCE) |
| private @interface CameraState { |
| int OPENING = 0; |
| int CONFIGURING = 1; |
| int STARTED = 2; |
| int STOPPED = 3; |
| } |
| |
| private final Object mCameraStateLock = new Object(); |
| |
| private CameraDevice mCameraDevice; |
| private CameraCaptureSession mPreviewSession; |
| private CaptureRequest mPreviewRequest; |
| private CaptureRequest.Builder mPreviewRequestBuilder; |
| private ImageReader mImageReader; |
| // We create a dedicated HandlerThread for operating the camera on. This |
| // is needed, because the camera APIs requires a Looper for posting |
| // asynchronous callbacks to. The native thread that calls the constructor |
| // and public API cannot be used for this, because it does not have a |
| // Looper. |
| private Handler mCameraThreadHandler; |
| private ConditionVariable mWaitForDeviceClosedConditionVariable = new ConditionVariable(); |
| |
| private Range<Integer> mAeFpsRange; |
| private @CameraState int mCameraState = CameraState.STOPPED; |
| private float mMaxZoom = 1.0f; |
| private Rect mCropRect = new Rect(); |
| private int mPhotoWidth; |
| private int mPhotoHeight; |
| private int mFocusMode = AndroidMeteringMode.CONTINUOUS; |
| private float mCurrentFocusDistance = 1.0f; |
| private int mExposureMode = AndroidMeteringMode.CONTINUOUS; |
| private long mLastExposureTimeNs; |
| private MeteringRectangle mAreaOfInterest; |
| private int mExposureCompensation; |
| private int mWhiteBalanceMode = AndroidMeteringMode.CONTINUOUS; |
| private int mColorTemperature = -1; |
| private int mIso; |
| private boolean mRedEyeReduction; |
| private int mFillLightMode = AndroidFillLightMode.OFF; |
| private boolean mTorch; |
| private boolean mEnableFaceDetection; |
| |
| // Service function to grab CameraCharacteristics and handle exceptions. |
| private static CameraCharacteristics getCameraCharacteristics(int id) { |
| final CameraManager manager = |
| (CameraManager) ContextUtils.getApplicationContext().getSystemService( |
| Context.CAMERA_SERVICE); |
| try { |
| final String str_id = String.valueOf(id); |
| return manager.getCameraCharacteristics(str_id); |
| } catch (CameraAccessException | IllegalArgumentException | AssertionError |
| | NullPointerException ex) { |
| Log.e(TAG, "getCameraCharacteristics: ", ex); |
| } |
| return null; |
| } |
| |
| private void createPreviewObjectsAndStartPreviewOrFailWith(int androidVideoCaptureError) { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| |
| if (createPreviewObjectsAndStartPreview()) return; |
| |
| changeCameraStateAndNotify(CameraState.STOPPED); |
| VideoCaptureJni.get().onError(mNativeVideoCaptureDeviceAndroid, VideoCaptureCamera2.this, |
| androidVideoCaptureError, "Error starting or restarting preview"); |
| } |
| |
| private boolean createPreviewObjectsAndStartPreview() { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| if (mCameraDevice == null) return false; |
| |
| try (TraceEvent trace_event = TraceEvent.scoped( |
| "VideoCaptureCamera2.createPreviewObjectsAndStartPreview")) { |
| // Create an ImageReader and plug a thread looper into it to have |
| // readback take place on its own thread. |
| mImageReader = ImageReader.newInstance(mCaptureFormat.getWidth(), |
| mCaptureFormat.getHeight(), mCaptureFormat.getPixelFormat(), 2 /* maxImages */); |
| final CrPreviewReaderListener imageReaderListener = new CrPreviewReaderListener(); |
| mImageReader.setOnImageAvailableListener(imageReaderListener, mCameraThreadHandler); |
| |
| try { |
| // TEMPLATE_PREVIEW specifically means "high frame rate is given |
| // priority over the highest-quality post-processing". |
| mPreviewRequestBuilder = |
| mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); |
| } catch (CameraAccessException | IllegalArgumentException | SecurityException ex) { |
| Log.e(TAG, "createCaptureRequest: ", ex); |
| return false; |
| } |
| |
| if (mPreviewRequestBuilder == null) { |
| Log.e(TAG, "mPreviewRequestBuilder error"); |
| return false; |
| } |
| |
| // Construct an ImageReader Surface and plug it into our CaptureRequest.Builder. |
| mPreviewRequestBuilder.addTarget(mImageReader.getSurface()); |
| |
| // A series of configuration options in the PreviewBuilder |
| mPreviewRequestBuilder.set( |
| CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); |
| mPreviewRequestBuilder.set( |
| CaptureRequest.NOISE_REDUCTION_MODE, CameraMetadata.NOISE_REDUCTION_MODE_FAST); |
| mPreviewRequestBuilder.set(CaptureRequest.EDGE_MODE, CameraMetadata.EDGE_MODE_FAST); |
| |
| // Depending on the resolution and other parameters, stabilization might not be |
| // available, see https://crbug.com/718387. |
| // https://developer.android.com/reference/android/hardware/camera2/CaptureRequest.html#CONTROL_VIDEO_STABILIZATION_MODE |
| final CameraCharacteristics cameraCharacteristics = getCameraCharacteristics(mId); |
| if (cameraCharacteristics == null) return false; |
| final int[] stabilizationModes = cameraCharacteristics.get( |
| CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES); |
| for (int mode : stabilizationModes) { |
| if (mode == CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON) { |
| mPreviewRequestBuilder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, |
| CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON); |
| break; |
| } |
| } |
| |
| configureCommonCaptureSettings(mPreviewRequestBuilder); |
| |
| // Overwrite settings to enable face detection. |
| if (mEnableFaceDetection) { |
| mPreviewRequestBuilder.set( |
| CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_USE_SCENE_MODE); |
| mPreviewRequestBuilder.set(CaptureRequest.CONTROL_SCENE_MODE, |
| CameraMetadata.CONTROL_SCENE_MODE_FACE_PRIORITY); |
| } |
| |
| List<Surface> surfaceList = new ArrayList<Surface>(1); |
| // TODO(mcasas): release this Surface when not needed, https://crbug.com/643884. |
| surfaceList.add(mImageReader.getSurface()); |
| |
| mPreviewRequest = mPreviewRequestBuilder.build(); |
| |
| try { |
| mCameraDevice.createCaptureSession( |
| surfaceList, new CrPreviewSessionListener(mPreviewRequest), null); |
| } catch (CameraAccessException | IllegalArgumentException | SecurityException ex) { |
| Log.e(TAG, "createCaptureSession: ", ex); |
| return false; |
| } |
| // Wait for trigger on CrPreviewSessionListener.onConfigured(); |
| return true; |
| } |
| } |
| |
| private void configureCommonCaptureSettings(CaptureRequest.Builder requestBuilder) { |
| assert mCameraThreadHandler.getLooper() == Looper.myLooper() : "called on wrong thread"; |
| try (TraceEvent trace_event = |
| TraceEvent.scoped("VideoCaptureCamera2.configureCommonCaptureSettings")) { |
| final CameraCharacteristics cameraCharacteristics = getCameraCharacteristics(mId); |
| |
| // |mFocusMode| indicates if we're in auto/continuous, single-shot or manual mode. |
| // AndroidMeteringMode.SINGLE_SHOT is dealt with independently since it needs to be |
| // triggered by a capture. |
| if (mFocusMode == AndroidMeteringMode.CONTINUOUS) { |
| requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, |
| CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE); |
| requestBuilder.set( |
| CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); |
| } else if (mFocusMode == AndroidMeteringMode.FIXED) { |
| requestBuilder.set( |
| CaptureRequest.CONTROL_AF_MODE, CameraMetadata.CONTROL_AF_MODE_OFF); |
| requestBuilder.set( |
| CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); |
| requestBuilder.set(CaptureRequest.LENS_FOCUS_DISTANCE, 1 / mCurrentFocusDistance); |
| } |
| |
| // |mExposureMode|, |mFillLightMode| and |mTorch| interact to configure the AE and Flash |
| // modes. In a nutshell, FLASH_MODE is only effective if the auto-exposure is ON/OFF, |
| // otherwise the auto-exposure related flash control (ON_{AUTO,ALWAYS}_FLASH{_REDEYE) |
| // takes priority. |mTorch| mode overrides any previous |mFillLightMode| flash control. |
| if (mExposureMode == AndroidMeteringMode.NONE |
| || mExposureMode == AndroidMeteringMode.FIXED) { |
| requestBuilder.set( |
| CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_OFF); |
| |
| // We need to configure by hand the exposure time when AE mode is off. Set it to |
| // the last known exposure interval if known, otherwise set it to the middle of the |
| // allowed range. Further tuning will be done via |mIso| and |
| // |mExposureCompensation|. |
| if (mLastExposureTimeNs != 0) { |
| requestBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, mLastExposureTimeNs); |
| } else if (cameraCharacteristics != null) { |
| Range<Long> range = cameraCharacteristics.get( |
| CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE); |
| requestBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, |
| range.getLower() + (range.getUpper() + range.getLower()) / 2); |
| } |
| |
| } else { |
| requestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO); |
| requestBuilder.set( |
| CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); |
| if (!shouldSkipSettingAeTargetFpsRange()) { |
| requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, mAeFpsRange); |
| } |
| } |
| |
| if (mTorch) { |
| requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, |
| mExposureMode == AndroidMeteringMode.CONTINUOUS |
| ? CameraMetadata.CONTROL_AE_MODE_ON |
| : CameraMetadata.CONTROL_AE_MODE_OFF); |
| requestBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH); |
| } else { |
| switch (mFillLightMode) { |
| case AndroidFillLightMode.OFF: |
| requestBuilder.set( |
| CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF); |
| break; |
| case AndroidFillLightMode.AUTO: |
| // Setting the AE to CONTROL_AE_MODE_ON_AUTO_FLASH[_REDEYE] overrides |
| // FLASH_MODE. |
| requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, |
| mRedEyeReduction |
| ? CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE |
| : CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH); |
| break; |
| case AndroidFillLightMode.FLASH: |
| // Setting the AE to CONTROL_AE_MODE_ON_ALWAYS_FLASH overrides FLASH_MODE. |
| requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, |
| CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH); |
| requestBuilder.set( |
| CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_SINGLE); |
| break; |
| default: |
| } |
| requestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, |
| CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); |
| } |
| |
| requestBuilder.set( |
| CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, mExposureCompensation); |
| |
| // White Balance mode AndroidMeteringMode.SINGLE_SHOT is not supported. |
| if (mWhiteBalanceMode == AndroidMeteringMode.CONTINUOUS) { |
| requestBuilder.set(CaptureRequest.CONTROL_AWB_LOCK, false); |
| requestBuilder.set( |
| CaptureRequest.CONTROL_AWB_MODE, CameraMetadata.CONTROL_AWB_MODE_AUTO); |
| // TODO(mcasas): support different luminant color temperatures, e.g. DAYLIGHT, |
| // SHADE. https://crbug.com/518807 |
| } else if (mWhiteBalanceMode == AndroidMeteringMode.NONE) { |
| requestBuilder.set(CaptureRequest.CONTROL_AWB_LOCK, false); |
| requestBuilder.set( |
| CaptureRequest.CONTROL_AWB_MODE, CameraMetadata.CONTROL_AWB_MODE_OFF); |
| } else if (mWhiteBalanceMode == AndroidMeteringMode.FIXED) { |
| requestBuilder.set(CaptureRequest.CONTROL_AWB_LOCK, true); |
| } |
| if (mColorTemperature > 0) { |
| int colorSetting = -1; |
| if (cameraCharacteristics != null) { |
| colorSetting = getClosestWhiteBalance(mColorTemperature, |
| cameraCharacteristics.get( |
| CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES)); |
| } |
| Log.d(TAG, " Color temperature (%d ==> %d)", mColorTemperature, colorSetting); |
| if (colorSetting != -1) { |
| requestBuilder.set(CaptureRequest.CONTROL_AWB_MODE, colorSetting); |
| } |
| } |
| |
| if (mAreaOfInterest != null) { |
| MeteringRectangle[] array = {mAreaOfInterest}; |
| Log.d(TAG, "Area of interest %s", mAreaOfInterest.toString()); |
| requestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, array); |
| requestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, array); |
| requestBuilder.set(CaptureRequest.CONTROL_AWB_REGIONS, array); |
| } |
| |
| if (!mCropRect.isEmpty()) { |
| requestBuilder.set(CaptureRequest.SCALER_CROP_REGION, mCropRect); |
| } |
| |
| if (mIso > 0) requestBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, mIso); |
| } |
| } |
| |
| private void changeCameraStateAndNotify(@CameraState int state) { |
| synchronized (mCameraStateLock) { |
| mCameraState = state; |
| mCameraStateLock.notifyAll(); |
| } |
| } |
| |
| private static boolean shouldSkipSettingAeTargetFpsRange() { |
| for (String buggyDevice : AE_TARGET_FPS_RANGE_BUGGY_DEVICE_LIST) { |
| if (buggyDevice.contentEquals(android.os.Build.MODEL)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Finds the closest Size to (|width|x|height|) in |sizes|, and returns it or null. |
| // Ignores |width| or |height| if either is zero (== don't care). |
| private static Size findClosestSizeInArray(Size[] sizes, int width, int height) { |
| if (sizes == null) return null; |
| Size closestSize = null; |
| int minDiff = Integer.MAX_VALUE; |
| for (Size size : sizes) { |
| final int diff = ((width > 0) ? Math.abs(size.getWidth() - width) : 0) |
| + ((height > 0) ? Math.abs(size.getHeight() - height) : 0); |
| if (diff < minDiff) { |
| minDiff = diff; |
| closestSize = size; |
| } |
| } |
| if (minDiff == Integer.MAX_VALUE) { |
| Log.e(TAG, "Couldn't find resolution close to (%dx%d)", width, height); |
| return null; |
| } |
| return closestSize; |
| } |
| |
| private static int findInIntArray(int[] hayStack, int needle) { |
| for (int i = 0; i < hayStack.length; ++i) { |
| if (needle == hayStack[i]) return i; |
| } |
| return -1; |
| } |
| |
| private static int getClosestWhiteBalance(int colorTemperature, int[] supportedTemperatures) { |
| int minDiff = Integer.MAX_VALUE; |
| int matchedTemperature = -1; |
| |
| for (int i = 0; i < COLOR_TEMPERATURES_MAP.size(); ++i) { |
| if (findInIntArray(supportedTemperatures, COLOR_TEMPERATURES_MAP.valueAt(i)) == -1) { |
| continue; |
| } |
| final int diff = Math.abs(colorTemperature - COLOR_TEMPERATURES_MAP.keyAt(i)); |
| if (diff >= minDiff) continue; |
| minDiff = diff; |
| matchedTemperature = COLOR_TEMPERATURES_MAP.valueAt(i); |
| } |
| return matchedTemperature; |
| } |
| |
| public static boolean isLegacyDevice(int id) { |
| final CameraCharacteristics cameraCharacteristics = getCameraCharacteristics(id); |
| return cameraCharacteristics != null |
| && cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) |
| == CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; |
| } |
| |
| public static int getNumberOfCameras() { |
| CameraManager manager = null; |
| try { |
| manager = (CameraManager) ContextUtils.getApplicationContext().getSystemService( |
| Context.CAMERA_SERVICE); |
| } catch (IllegalArgumentException ex) { |
| Log.e(TAG, "getSystemService(Context.CAMERA_SERVICE): ", ex); |
| return 0; |
| } |
| if (manager == null) return 0; |
| try { |
| return manager.getCameraIdList().length; |
| } catch (CameraAccessException | SecurityException | AssertionError ex) { |
| // SecurityException is undocumented but seen in the wild: https://crbug/605424. |
| Log.e(TAG, "getNumberOfCameras: getCameraIdList(): ", ex); |
| return 0; |
| } |
| } |
| |
| public static int getCaptureApiType(int index) { |
| final CameraCharacteristics cameraCharacteristics = |
| getCameraCharacteristics(getDeviceIdInt(index)); |
| if (cameraCharacteristics == null) { |
| return VideoCaptureApi.UNKNOWN; |
| } |
| |
| final int supportedHWLevel = |
| cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); |
| |
| // https://crbug.com/1155568: We must explicitly check for |
| // BACKWARD_COMPATIBLE, except for LEGACY, where it's implied. See also |
| // https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY |
| if (supportedHWLevel == CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) { |
| return VideoCaptureApi.ANDROID_API2_LEGACY; |
| } |
| final int[] capabilities = |
| cameraCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES); |
| boolean backwardCompatible = false; |
| for (int cap : capabilities) { |
| if (cap == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) { |
| backwardCompatible = true; |
| break; |
| } |
| } |
| if (!backwardCompatible) { |
| return VideoCaptureApi.UNKNOWN; |
| } |
| |
| switch (supportedHWLevel) { |
| case CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_FULL: |
| return VideoCaptureApi.ANDROID_API2_FULL; |
| case CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED: |
| return VideoCaptureApi.ANDROID_API2_LIMITED; |
| default: |
| return VideoCaptureApi.ANDROID_API2_LEGACY; |
| } |
| } |
| |
| public static boolean isZoomSupported(int index) { |
| final CameraCharacteristics cameraCharacteristics = |
| getCameraCharacteristics(getDeviceIdInt(index)); |
| if (cameraCharacteristics == null) { |
| return false; |
| } |
| |
| final float maxZoom = |
| cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); |
| final boolean isZoomSupported = maxZoom > 1.0f; |
| return isZoomSupported; |
| } |
| |
| public static int getFacingMode(int index) { |
| final CameraCharacteristics cameraCharacteristics = |
| getCameraCharacteristics(getDeviceIdInt(index)); |
| if (cameraCharacteristics == null) { |
| return VideoFacingMode.MEDIA_VIDEO_FACING_NONE; |
| } |
| |
| final int facing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING); |
| switch (facing) { |
| case CameraCharacteristics.LENS_FACING_FRONT: |
| return VideoFacingMode.MEDIA_VIDEO_FACING_USER; |
| case CameraCharacteristics.LENS_FACING_BACK: |
| return VideoFacingMode.MEDIA_VIDEO_FACING_ENVIRONMENT; |
| default: |
| return VideoFacingMode.MEDIA_VIDEO_FACING_NONE; |
| } |
| } |
| |
| public static String getName(int index) { |
| final CameraCharacteristics cameraCharacteristics = |
| getCameraCharacteristics(getDeviceIdInt(index)); |
| if (cameraCharacteristics == null) return null; |
| final int facing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING); |
| |
| boolean isInfrared = false; |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { |
| final Integer infoColor = cameraCharacteristics.get( |
| CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT); |
| isInfrared = infoColor != null |
| && infoColor.equals( |
| CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_NIR); |
| } |
| return "camera2 " + index + ", facing " |
| + ((facing == CameraCharacteristics.LENS_FACING_FRONT) ? "front" : "back") |
| + (isInfrared ? " infrared" : ""); |
| } |
| |
| // Retrieves the index within the camera ID list for the specified camera ID; returns |
| // -1 if the specified camera ID is not found |
| public static int getDeviceIndex(int id) { |
| final CameraManager manager = |
| (CameraManager) ContextUtils.getApplicationContext().getSystemService( |
| Context.CAMERA_SERVICE); |
| try { |
| final String[] cameraIdList = manager.getCameraIdList(); |
| for (int index = 0; index < cameraIdList.length; ++index) { |
| try { |
| if (Integer.parseInt(cameraIdList[index]) == id) { |
| return index; |
| } |
| } catch (NumberFormatException e) { |
| continue; |
| } |
| } |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "manager.getCameraIdList: ", ex); |
| } |
| return -1; |
| } |
| |
| // Helper to retrieve the camera device ID, as an integer, at the specified |
| // index within the camera ID list; returns -1 if camera does not exist at the |
| // specified index |
| private static int getDeviceIdInt(int index) { |
| try { |
| return Integer.parseInt(getDeviceId(index)); |
| } catch (NumberFormatException ex) { |
| Log.e(TAG, "Invalid camera index: ", index); |
| return -1; |
| } |
| } |
| |
| static String getDeviceId(int index) { |
| final CameraManager manager = |
| (CameraManager) ContextUtils.getApplicationContext().getSystemService( |
| Context.CAMERA_SERVICE); |
| try { |
| final String[] cameraIdList = manager.getCameraIdList(); |
| if (index >= cameraIdList.length) { |
| Log.e(TAG, "Invalid camera index: ", index); |
| return null; |
| } |
| return cameraIdList[index]; |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "manager.getCameraIdList: ", ex); |
| return null; |
| } |
| } |
| |
| public static VideoCaptureFormat[] getDeviceSupportedFormats(int index) { |
| final CameraCharacteristics cameraCharacteristics = |
| getCameraCharacteristics(getDeviceIdInt(index)); |
| if (cameraCharacteristics == null) return null; |
| |
| try { |
| final int[] capabilities = |
| cameraCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES); |
| // Per-format frame rate via getOutputMinFrameDuration() is only available if the |
| // property REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR is set. |
| boolean minFrameDurationAvailable = false; |
| for (int cap : capabilities) { |
| if (cap == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR) { |
| minFrameDurationAvailable = true; |
| break; |
| } |
| } |
| |
| ArrayList<VideoCaptureFormat> formatList = new ArrayList<VideoCaptureFormat>(); |
| final StreamConfigurationMap streamMap = cameraCharacteristics.get( |
| CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); |
| final int[] formats = streamMap.getOutputFormats(); |
| for (int format : formats) { |
| final Size[] sizes = streamMap.getOutputSizes(format); |
| if (sizes == null) continue; |
| for (Size size : sizes) { |
| double minFrameRate = 0.0f; |
| if (minFrameDurationAvailable) { |
| final long minFrameDurationInNanoseconds = |
| streamMap.getOutputMinFrameDuration(format, size); |
| minFrameRate = (minFrameDurationInNanoseconds == 0) |
| ? 0.0f |
| : (kNanosecondsPerSecond / minFrameDurationInNanoseconds); |
| } else { |
| // TODO(mcasas): find out where to get the info from in this case. |
| // Hint: perhaps using SCALER_AVAILABLE_PROCESSED_MIN_DURATIONS. |
| minFrameRate = 0.0; |
| } |
| formatList.add(new VideoCaptureFormat( |
| size.getWidth(), size.getHeight(), (int) minFrameRate, format)); |
| } |
| } |
| return formatList.toArray(new VideoCaptureFormat[formatList.size()]); |
| } catch (Exception e) { |
| Log.e(TAG, "Unable to catch device supported video formats: ", e); |
| return null; |
| } |
| } |
| |
| VideoCaptureCamera2(int id, long nativeVideoCaptureDeviceAndroid) { |
| super(id, nativeVideoCaptureDeviceAndroid); |
| |
| VideoCaptureJni.get().dCheckCurrentlyOnIncomingTaskRunner( |
| mNativeVideoCaptureDeviceAndroid, VideoCaptureCamera2.this); |
| |
| HandlerThread thread = new HandlerThread("VideoCaptureCamera2_CameraThread"); |
| thread.start(); |
| mCameraThreadHandler = new Handler(thread.getLooper()); |
| |
| final CameraCharacteristics cameraCharacteristics = getCameraCharacteristics(id); |
| if (cameraCharacteristics != null) { |
| mMaxZoom = cameraCharacteristics.get( |
| CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); |
| } |
| } |
| |
| @Override |
| public void finalize() { |
| mCameraThreadHandler.getLooper().quit(); |
| } |
| |
| @Override |
| public boolean allocate(int width, int height, int frameRate, boolean enableFaceDetection) { |
| Log.d(TAG, "allocate: requested (%d x %d) @%dfps", width, height, frameRate); |
| VideoCaptureJni.get().dCheckCurrentlyOnIncomingTaskRunner( |
| mNativeVideoCaptureDeviceAndroid, VideoCaptureCamera2.this); |
| synchronized (mCameraStateLock) { |
| if (mCameraState == CameraState.OPENING || mCameraState == CameraState.CONFIGURING) { |
| Log.e(TAG, "allocate() invoked while Camera is busy opening/configuring."); |
| return false; |
| } |
| } |
| final CameraCharacteristics cameraCharacteristics = getCameraCharacteristics(mId); |
| if (cameraCharacteristics == null) return false; |
| final StreamConfigurationMap streamMap = |
| cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); |
| |
| mCameraNativeOrientation = |
| cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); |
| |
| // Update the capture width and height based on the camera orientation. |
| // With device's native orientation being Portrait for Android devices, |
| // for cameras that are mounted 0 or 180 degrees in respect to device's |
| // native orientation, we will need to swap the width and height in |
| // order to capture upright frames in respect to device's current |
| // orientation. |
| int capture_width = width; |
| int capture_height = height; |
| if (mCameraNativeOrientation == 0 || mCameraNativeOrientation == 180) { |
| Log.d(TAG, |
| "Flipping capture width and height to match device's " |
| + "natural orientation"); |
| capture_width = height; |
| capture_height = width; |
| } |
| |
| // Find closest supported size. |
| final Size[] supportedSizes = streamMap.getOutputSizes(ImageFormat.YUV_420_888); |
| final Size closestSupportedSize = |
| findClosestSizeInArray(supportedSizes, capture_width, capture_height); |
| if (closestSupportedSize == null) { |
| Log.e(TAG, "No supported resolutions."); |
| return false; |
| } |
| final List<Range<Integer>> fpsRanges = Arrays.asList(cameraCharacteristics.get( |
| CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)); |
| if (fpsRanges.isEmpty()) { |
| Log.e(TAG, "No supported framerate ranges."); |
| return false; |
| } |
| final List<FramerateRange> framerateRanges = |
| new ArrayList<FramerateRange>(fpsRanges.size()); |
| // On some legacy implementations FPS values are multiplied by 1000. Multiply by 1000 |
| // everywhere for consistency. Set fpsUnitFactor to 1 if fps ranges are already multiplied |
| // by 1000. |
| final int fpsUnitFactor = fpsRanges.get(0).getUpper() > 1000 ? 1 : 1000; |
| for (Range<Integer> range : fpsRanges) { |
| framerateRanges.add(new FramerateRange( |
| range.getLower() * fpsUnitFactor, range.getUpper() * fpsUnitFactor)); |
| } |
| final FramerateRange aeFramerateRange = |
| getClosestFramerateRange(framerateRanges, frameRate * 1000); |
| mAeFpsRange = new Range<Integer>( |
| aeFramerateRange.min / fpsUnitFactor, aeFramerateRange.max / fpsUnitFactor); |
| Log.d(TAG, "allocate: matched (%d x %d) @[%d - %d]", closestSupportedSize.getWidth(), |
| closestSupportedSize.getHeight(), mAeFpsRange.getLower(), mAeFpsRange.getUpper()); |
| |
| // |mCaptureFormat| is also used to configure the ImageReader. |
| mCaptureFormat = new VideoCaptureFormat(closestSupportedSize.getWidth(), |
| closestSupportedSize.getHeight(), frameRate, ImageFormat.YUV_420_888); |
| |
| // TODO(mcasas): The following line is correct for N5 with prerelease Build, |
| // but NOT for N7 with a dev Build. Figure out which one to support. |
| mInvertDeviceOrientationReadings = |
| cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) |
| == CameraCharacteristics.LENS_FACING_BACK; |
| |
| mEnableFaceDetection = enableFaceDetection; |
| return true; |
| } |
| |
| @Override |
| public boolean startCaptureMaybeAsync() { |
| VideoCaptureJni.get().dCheckCurrentlyOnIncomingTaskRunner( |
| mNativeVideoCaptureDeviceAndroid, VideoCaptureCamera2.this); |
| |
| changeCameraStateAndNotify(CameraState.OPENING); |
| final CameraManager manager = |
| (CameraManager) ContextUtils.getApplicationContext().getSystemService( |
| Context.CAMERA_SERVICE); |
| |
| final CrStateListener stateListener = new CrStateListener(); |
| try { |
| final String[] cameraIdList = manager.getCameraIdList(); |
| final int cameraIndex = getDeviceIndex(mId); |
| if (cameraIndex < 0) { |
| Log.e(TAG, "Invalid camera Id: ", mId); |
| return false; |
| } |
| TraceEvent.instant("VideoCaptureCamera2.java", |
| "VideoCaptureCamera2.startCaptureMaybeAsync calling manager.openCamera"); |
| manager.openCamera(cameraIdList[cameraIndex], stateListener, mCameraThreadHandler); |
| } catch (CameraAccessException | IllegalArgumentException | SecurityException ex) { |
| Log.e(TAG, "allocate: manager.openCamera: ", ex); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| @Override |
| public boolean stopCaptureAndBlockUntilStopped() { |
| VideoCaptureJni.get().dCheckCurrentlyOnIncomingTaskRunner( |
| mNativeVideoCaptureDeviceAndroid, VideoCaptureCamera2.this); |
| try (TraceEvent trace_event = |
| TraceEvent.scoped("VideoCaptureCamera2.stopCaptureAndBlockUntilStopped")) { |
| // With Camera2 API, the capture is started asynchronously, which will cause problem if |
| // stopCapture comes too quickly. Without stopping the previous capture properly, the |
| // next startCapture will fail and make Chrome no-responding. So wait camera to be |
| // STARTED. |
| synchronized (mCameraStateLock) { |
| while (mCameraState != CameraState.STARTED && mCameraState != CameraState.STOPPED) { |
| try { |
| mCameraStateLock.wait(); |
| } catch (InterruptedException ex) { |
| Log.e(TAG, "CaptureStartedEvent: ", ex); |
| } |
| } |
| if (mCameraState == CameraState.STOPPED) return true; |
| } |
| |
| mCameraThreadHandler.post(new StopCaptureTask()); |
| mWaitForDeviceClosedConditionVariable.block(); |
| |
| return true; |
| } |
| } |
| |
| @Override |
| public void getPhotoCapabilitiesAsync(long callbackId) { |
| VideoCaptureJni.get().dCheckCurrentlyOnIncomingTaskRunner( |
| mNativeVideoCaptureDeviceAndroid, VideoCaptureCamera2.this); |
| mCameraThreadHandler.post(new GetPhotoCapabilitiesTask(callbackId)); |
| } |
| |
| @Override |
| public void setPhotoOptions(double zoom, int focusMode, double currentFocusDistance, |
| int exposureMode, double width, double height, double[] pointsOfInterest2D, |
| boolean hasExposureCompensation, double exposureCompensation, double exposureTime, |
| int whiteBalanceMode, double iso, boolean hasRedEyeReduction, boolean redEyeReduction, |
| int fillLightMode, boolean hasTorch, boolean torch, double colorTemperature) { |
| VideoCaptureJni.get().dCheckCurrentlyOnIncomingTaskRunner( |
| mNativeVideoCaptureDeviceAndroid, VideoCaptureCamera2.this); |
| mCameraThreadHandler.post(new SetPhotoOptionsTask( |
| new PhotoOptions(zoom, focusMode, currentFocusDistance, exposureMode, width, height, |
| pointsOfInterest2D, hasExposureCompensation, exposureCompensation, |
| exposureTime, whiteBalanceMode, iso, hasRedEyeReduction, redEyeReduction, |
| fillLightMode, hasTorch, torch, colorTemperature))); |
| } |
| |
| @Override |
| public void takePhotoAsync(long callbackId) { |
| VideoCaptureJni.get().dCheckCurrentlyOnIncomingTaskRunner( |
| mNativeVideoCaptureDeviceAndroid, VideoCaptureCamera2.this); |
| TraceEvent.instant("VideoCaptureCamera2.java", "takePhotoAsync"); |
| |
| mCameraThreadHandler.post(new TakePhotoTask(callbackId)); |
| } |
| |
| @Override |
| public void deallocate() { |
| Log.d(TAG, "deallocate"); |
| } |
| } |