Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(37)

Unified Diff: media/base/android/java/src/org/chromium/media/VideoCaptureCamera2.java

Issue 1710373002: Revert Move VideoCapture* files from media/base/android/... to media/capture/video/android/java/... (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 4 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: media/base/android/java/src/org/chromium/media/VideoCaptureCamera2.java
diff --git a/media/base/android/java/src/org/chromium/media/VideoCaptureCamera2.java b/media/base/android/java/src/org/chromium/media/VideoCaptureCamera2.java
new file mode 100644
index 0000000000000000000000000000000000000000..0c66ede2ce363518717c80cbd6c153d7287113ed
--- /dev/null
+++ b/media/base/android/java/src/org/chromium/media/VideoCaptureCamera2.java
@@ -0,0 +1,503 @@
+// 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.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.params.StreamConfigurationMap;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Size;
+import android.view.Surface;
+
+import org.chromium.base.Log;
+import org.chromium.base.annotations.JNINamespace;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+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.LOLLIPOP)
+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) {
+ mCameraDevice = cameraDevice;
+ changeCameraStateAndNotify(CameraState.CONFIGURING);
+ if (!createCaptureObjects()) {
+ changeCameraStateAndNotify(CameraState.STOPPED);
+ nativeOnError(mNativeVideoCaptureDeviceAndroid,
+ "Error configuring camera");
+ }
+ }
+
+ @Override
+ public void onDisconnected(CameraDevice cameraDevice) {
+ cameraDevice.close();
+ mCameraDevice = null;
+ changeCameraStateAndNotify(CameraState.STOPPED);
+ }
+
+ @Override
+ public void onError(CameraDevice cameraDevice, int error) {
+ cameraDevice.close();
+ mCameraDevice = null;
+ changeCameraStateAndNotify(CameraState.STOPPED);
+ nativeOnError(mNativeVideoCaptureDeviceAndroid,
+ "Camera device error " + Integer.toString(error));
+ }
+ };
+
+ // Inner class to extend a Capture Session state change listener.
+ private class CrCaptureSessionListener extends CameraCaptureSession.StateCallback {
+ @Override
+ public void onConfigured(CameraCaptureSession cameraCaptureSession) {
+ Log.d(TAG, "onConfigured");
+ mCaptureSession = cameraCaptureSession;
+ createCaptureRequest();
+ changeCameraStateAndNotify(CameraState.STARTED);
+ }
+
+ @Override
+ public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
+ // TODO(mcasas): When signalling error, C++ will tear us down. Is there need for
+ // cleanup?
+ changeCameraStateAndNotify(CameraState.STOPPED);
+ nativeOnError(mNativeVideoCaptureDeviceAndroid,
+ "Camera session configuration error");
+ }
+ };
+
+ // Internal class implementing the ImageReader listener. Gets pinged when a
+ // new frame is been captured and downloaded to memory-backed buffers.
+ private class CrImageReaderListener implements ImageReader.OnImageAvailableListener {
+ @Override
+ public void onImageAvailable(ImageReader reader) {
+ Image image = null;
+ try {
+ image = reader.acquireLatestImage();
+ if (image == null) return;
+ if (image.getFormat() != ImageFormat.YUV_420_888
+ || image.getPlanes().length != 3) {
+ Log.e(TAG, "Unexpected image format: %d or #planes: %d",
+ image.getFormat(), image.getPlanes().length);
+ return;
+ }
+
+ if (reader.getWidth() != image.getWidth()
+ || reader.getHeight() != image.getHeight()) {
+ throw new IllegalStateException("ImageReader size " + reader.getWidth() + "x"
+ + reader.getHeight() + " did not match Image size " + image.getWidth()
+ + "x" + image.getHeight());
+ }
+ readImageIntoBuffer(image, mCapturedData);
+ nativeOnFrameAvailable(mNativeVideoCaptureDeviceAndroid,
+ mCapturedData,
+ mCapturedData.length,
+ getCameraRotation());
+ } catch (IllegalStateException ex) {
+ Log.e(TAG, "acquireLatestImage():" + ex);
+ return;
+ } finally {
+ if (image != null) {
+ image.close();
+ }
+ }
+ }
+ };
+
+ private byte[] mCapturedData;
+
+ private CameraDevice mCameraDevice = null;
+ private CaptureRequest.Builder mPreviewBuilder = null;
+ private CameraCaptureSession mCaptureSession = null;
+ private ImageReader mImageReader = null;
+
+ private static final double kNanoSecondsToFps = 1.0E-9;
+ private static final String TAG = "cr.media";
+
+ private static enum CameraState {OPENING, CONFIGURING, STARTED, STOPPED}
+ private CameraState mCameraState = CameraState.STOPPED;
+ private final Object mCameraStateLock = new Object();
+
+ // Service function to grab CameraCharacteristics and handle exceptions.
+ private static CameraCharacteristics getCameraCharacteristics(Context appContext, int id) {
+ final CameraManager manager =
+ (CameraManager) appContext.getSystemService(Context.CAMERA_SERVICE);
+ try {
+ return manager.getCameraCharacteristics(Integer.toString(id));
+ } catch (CameraAccessException ex) {
+ Log.e(TAG, "getNumberOfCameras: getCameraIdList(): " + ex);
+ }
+ return null;
+ }
+
+ private boolean createCaptureObjects() {
+ Log.d(TAG, "createCaptureObjects");
+ if (mCameraDevice == null) return false;
+
+ // Create an ImageReader and plug a thread looper into it to have
+ // readback take place on its own thread.
+ final int maxImages = 2;
+ mImageReader = ImageReader.newInstance(mCaptureFormat.getWidth(),
+ mCaptureFormat.getHeight(),
+ mCaptureFormat.getPixelFormat(),
+ maxImages);
+ HandlerThread thread = new HandlerThread("CameraPreview");
+ thread.start();
+ final Handler backgroundHandler = new Handler(thread.getLooper());
+ final CrImageReaderListener imageReaderListener = new CrImageReaderListener();
+ mImageReader.setOnImageAvailableListener(imageReaderListener,
+ backgroundHandler);
+
+ // The Preview template specifically means "high frame rate is given
+ // priority over the highest-quality post-processing".
+ try {
+ mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+ } catch (CameraAccessException ex) {
+ Log.e(TAG, "createCaptureRequest: " + ex);
+ return false;
+ } catch (IllegalArgumentException ex) {
+ Log.e(TAG, "createCaptureRequest: " + ex);
+ return false;
+ } catch (SecurityException ex) {
+ Log.e(TAG, "createCaptureRequest: " + ex);
+ return false;
+ }
+ if (mPreviewBuilder == null) {
+ Log.e(TAG, "mPreviewBuilder error");
+ return false;
+ }
+ // Construct an ImageReader Surface and plug it into our CaptureRequest.Builder.
+ mPreviewBuilder.addTarget(mImageReader.getSurface());
+
+ // A series of configuration options in the PreviewBuilder
+ mPreviewBuilder.set(CaptureRequest.CONTROL_MODE,
+ CameraMetadata.CONTROL_MODE_AUTO);
+ mPreviewBuilder.set(CaptureRequest.NOISE_REDUCTION_MODE,
+ CameraMetadata.NOISE_REDUCTION_MODE_FAST);
+ mPreviewBuilder.set(CaptureRequest.EDGE_MODE, CameraMetadata.EDGE_MODE_FAST);
+ mPreviewBuilder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
+ CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON);
+ // SENSOR_EXPOSURE_TIME ?
+
+ List<Surface> surfaceList = new ArrayList<Surface>(1);
+ surfaceList.add(mImageReader.getSurface());
+ final CrCaptureSessionListener captureSessionListener = new CrCaptureSessionListener();
+ try {
+ mCameraDevice.createCaptureSession(surfaceList, captureSessionListener, null);
+ } catch (CameraAccessException ex) {
+ Log.e(TAG, "createCaptureSession: " + ex);
+ return false;
+ } catch (IllegalArgumentException ex) {
+ Log.e(TAG, "createCaptureSession: " + ex);
+ return false;
+ } catch (SecurityException ex) {
+ Log.e(TAG, "createCaptureSession: " + ex);
+ return false;
+ }
+ // Wait for trigger on CrCaptureSessionListener.onConfigured();
+ return true;
+ }
+
+ private boolean createCaptureRequest() {
+ Log.d(TAG, "createCaptureRequest");
+ try {
+ // This line triggers the capture. No |listener| is registered, so
+ // we will not get notified of capture events, instead, ImageReader
+ // will trigger every time a downloaded image is ready. Since
+ //|handler| is null, we'll work on the current Thread Looper.
+ mCaptureSession.setRepeatingRequest(mPreviewBuilder.build(), null, null);
+ } catch (CameraAccessException ex) {
+ Log.e(TAG, "setRepeatingRequest: " + ex);
+ return false;
+ } catch (IllegalArgumentException ex) {
+ Log.e(TAG, "setRepeatingRequest: " + ex);
+ return false;
+ } catch (SecurityException ex) {
+ Log.e(TAG, "setRepeatingRequest: " + ex);
+ return false;
+ }
+ // Now wait for trigger on CrImageReaderListener.onImageAvailable();
+ return true;
+ }
+
+ private static void readImageIntoBuffer(Image image, byte[] data) {
+ final int imageWidth = image.getWidth();
+ final int imageHeight = image.getHeight();
+ final Image.Plane[] planes = image.getPlanes();
+
+ int offset = 0;
+ for (int plane = 0; plane < planes.length; ++plane) {
+ final ByteBuffer buffer = planes[plane].getBuffer();
+ final int rowStride = planes[plane].getRowStride();
+ // Experimentally, U and V planes have |pixelStride| = 2, which
+ // essentially means they are packed. That's silly, because we are
+ // forced to unpack here.
+ final int pixelStride = planes[plane].getPixelStride();
+ final int planeWidth = (plane == 0) ? imageWidth : imageWidth / 2;
+ final int planeHeight = (plane == 0) ? imageHeight : imageHeight / 2;
+
+ if (pixelStride == 1 && rowStride == planeWidth) {
+ // Copy whole plane from buffer into |data| at once.
+ buffer.get(data, offset, planeWidth * planeHeight);
+ offset += planeWidth * planeHeight;
+ } else {
+ // Copy pixels one by one respecting pixelStride and rowStride.
+ byte[] rowData = new byte[rowStride];
+ for (int row = 0; row < planeHeight - 1; ++row) {
+ buffer.get(rowData, 0, rowStride);
+ for (int col = 0; col < planeWidth; ++col) {
+ data[offset++] = rowData[col * pixelStride];
+ }
+ }
+
+ // Last row is special in some devices and may not contain the full
+ // |rowStride| bytes of data. See http://crbug.com/458701.
+ buffer.get(rowData, 0, Math.min(rowStride, buffer.remaining()));
+ for (int col = 0; col < planeWidth; ++col) {
+ data[offset++] = rowData[col * pixelStride];
+ }
+ }
+ }
+ }
+
+ private void changeCameraStateAndNotify(CameraState state) {
+ synchronized (mCameraStateLock) {
+ mCameraState = state;
+ mCameraStateLock.notifyAll();
+ }
+ }
+
+ static boolean isLegacyDevice(Context appContext, int id) {
+ final CameraCharacteristics cameraCharacteristics =
+ getCameraCharacteristics(appContext, id);
+ return cameraCharacteristics != null
+ && cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
+ == CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
+ }
+
+ static int getNumberOfCameras(Context appContext) {
+ final CameraManager manager =
+ (CameraManager) appContext.getSystemService(Context.CAMERA_SERVICE);
+ try {
+ return manager.getCameraIdList().length;
+ } catch (CameraAccessException ex) {
+ Log.e(TAG, "getNumberOfCameras: getCameraIdList(): " + ex);
+ return 0;
+ }
+ }
+
+ static int getCaptureApiType(int id, Context appContext) {
+ final CameraCharacteristics cameraCharacteristics =
+ getCameraCharacteristics(appContext, id);
+ if (cameraCharacteristics == null) {
+ return CaptureApiType.API_TYPE_UNKNOWN;
+ }
+
+ final int supportedHWLevel = cameraCharacteristics.get(
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
+ switch (supportedHWLevel) {
+ case CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY:
+ return CaptureApiType.API2_LEGACY;
+ case CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_FULL:
+ return CaptureApiType.API2_FULL;
+ case CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED:
+ return CaptureApiType.API2_LIMITED;
+ default:
+ return CaptureApiType.API2_LEGACY;
+ }
+ }
+
+ static String getName(int id, Context appContext) {
+ final CameraCharacteristics cameraCharacteristics =
+ getCameraCharacteristics(appContext, id);
+ if (cameraCharacteristics == null) return null;
+ final int facing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
+ return "camera2 " + id + ", facing "
+ + ((facing == CameraCharacteristics.LENS_FACING_FRONT) ? "front" : "back");
+ }
+
+ static VideoCaptureFormat[] getDeviceSupportedFormats(Context appContext, int id) {
+ final CameraCharacteristics cameraCharacteristics =
+ getCameraCharacteristics(appContext, id);
+ if (cameraCharacteristics == null) return null;
+
+ 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 minFrameDuration = streamMap.getOutputMinFrameDuration(format, size);
+ minFrameRate = (minFrameDuration == 0)
+ ? 0.0f
+ : (1.0 / kNanoSecondsToFps * minFrameDuration);
+ } 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, 0));
+ }
+ }
+ return formatList.toArray(new VideoCaptureFormat[formatList.size()]);
+ }
+
+ VideoCaptureCamera2(Context context,
+ int id,
+ long nativeVideoCaptureDeviceAndroid) {
+ super(context, id, nativeVideoCaptureDeviceAndroid);
+ }
+
+ @Override
+ public boolean allocate(int width, int height, int frameRate) {
+ Log.d(TAG, "allocate: requested (%d x %d) @%dfps", width, height, frameRate);
+ 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(mContext, mId);
+ final StreamConfigurationMap streamMap =
+ cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+
+ // Find closest supported size.
+ final Size[] supportedSizes = streamMap.getOutputSizes(ImageFormat.YUV_420_888);
+ if (supportedSizes == null) return false;
+ Size closestSupportedSize = null;
+ int minDiff = Integer.MAX_VALUE;
+ for (Size size : supportedSizes) {
+ final int diff =
+ Math.abs(size.getWidth() - width) + Math.abs(size.getHeight() - height);
+ if (diff < minDiff) {
+ minDiff = diff;
+ closestSupportedSize = size;
+ }
+ }
+ if (minDiff == Integer.MAX_VALUE) {
+ Log.e(TAG, "No supported resolutions.");
+ return false;
+ }
+ Log.d(TAG, "allocate: matched (%d x %d)", closestSupportedSize.getWidth(),
+ closestSupportedSize.getHeight());
+
+ // |mCaptureFormat| is also used to configure the ImageReader.
+ mCaptureFormat = new VideoCaptureFormat(closestSupportedSize.getWidth(),
+ closestSupportedSize.getHeight(), frameRate, ImageFormat.YUV_420_888);
+ int expectedFrameSize = mCaptureFormat.mWidth * mCaptureFormat.mHeight
+ * ImageFormat.getBitsPerPixel(mCaptureFormat.mPixelFormat) / 8;
+ mCapturedData = new byte[expectedFrameSize];
+ mCameraNativeOrientation =
+ cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+ // 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;
+ return true;
+ }
+
+ @Override
+ public boolean startCapture() {
+ Log.d(TAG, "startCapture");
+ changeCameraStateAndNotify(CameraState.OPENING);
+ final CameraManager manager =
+ (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
+ final Handler mainHandler = new Handler(mContext.getMainLooper());
+ final CrStateListener stateListener = new CrStateListener();
+ try {
+ manager.openCamera(Integer.toString(mId), stateListener, mainHandler);
+ } catch (CameraAccessException ex) {
+ Log.e(TAG, "allocate: manager.openCamera: " + ex);
+ return false;
+ } catch (IllegalArgumentException ex) {
+ Log.e(TAG, "allocate: manager.openCamera: " + ex);
+ return false;
+ } catch (SecurityException ex) {
+ Log.e(TAG, "allocate: manager.openCamera: " + ex);
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean stopCapture() {
+ Log.d(TAG, "stopCapture");
+
+ // 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;
+ }
+
+ try {
+ mCaptureSession.abortCaptures();
+ } catch (CameraAccessException ex) {
+ Log.e(TAG, "abortCaptures: " + ex);
+ return false;
+ } catch (IllegalStateException ex) {
+ Log.e(TAG, "abortCaptures: " + ex);
+ return false;
+ }
+ if (mCameraDevice == null) return false;
+ mCameraDevice.close();
+ changeCameraStateAndNotify(CameraState.STOPPED);
+ return true;
+ }
+
+ @Override
+ public void deallocate() {
+ Log.d(TAG, "deallocate");
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698