Android: Camera2 implementation and tests for it.
BUG=webrtc:5519
R=magjed@webrtc.org
Review URL: https://codereview.webrtc.org/2078473002 .
Cr-Commit-Position: refs/heads/master@{#13320}
diff --git a/webrtc/api/androidtests/src/org/webrtc/Camera2CapturerTest.java b/webrtc/api/androidtests/src/org/webrtc/Camera2CapturerTest.java
new file mode 100644
index 0000000..7ad60c0
--- /dev/null
+++ b/webrtc/api/androidtests/src/org/webrtc/Camera2CapturerTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc;
+
+import org.webrtc.CameraEnumerationAndroid.CaptureFormat;
+
+import android.content.Context;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraAccessException;
+import android.os.Handler;
+import android.os.Looper;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.util.concurrent.CountDownLatch;
+
+public class Camera2CapturerTest extends InstrumentationTestCase {
+ static final String TAG = "Camera2CapturerTest";
+
+ /**
+ * Simple camera2 implementation that only knows how to open the camera and close it.
+ */
+ private class SimpleCamera2 {
+ final CameraManager cameraManager;
+ final LooperThread looperThread;
+ final CountDownLatch openDoneSignal;
+ final Object cameraDeviceLock;
+ CameraDevice cameraDevice; // Guarded by cameraDeviceLock
+ boolean openSucceeded; // Guarded by cameraDeviceLock
+
+ private class LooperThread extends Thread {
+ final CountDownLatch startedSignal = new CountDownLatch(1);
+ private Handler handler;
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ handler = new Handler();
+ startedSignal.countDown();
+ Looper.loop();
+ }
+
+ public void waitToStart() {
+ ThreadUtils.awaitUninterruptibly(startedSignal);
+ }
+
+ public void requestStop() {
+ handler.getLooper().quit();
+ }
+
+ public Handler getHandler() {
+ return handler;
+ }
+ }
+
+ private class CameraStateCallback extends CameraDevice.StateCallback {
+ @Override
+ public void onClosed(CameraDevice cameraDevice) {
+ Logging.d(TAG, "Simple camera2 closed.");
+
+ synchronized (cameraDeviceLock) {
+ SimpleCamera2.this.cameraDevice = null;
+ }
+ }
+
+ @Override
+ public void onDisconnected(CameraDevice cameraDevice) {
+ Logging.d(TAG, "Simple camera2 disconnected.");
+
+ synchronized (cameraDeviceLock) {
+ SimpleCamera2.this.cameraDevice = null;
+ }
+ }
+
+ @Override
+ public void onError(CameraDevice cameraDevice, int errorCode) {
+ Logging.w(TAG, "Simple camera2 error: " + errorCode);
+
+ synchronized (cameraDeviceLock) {
+ SimpleCamera2.this.cameraDevice = cameraDevice;
+ openSucceeded = false;
+ }
+
+ openDoneSignal.countDown();
+ }
+
+ @Override
+ public void onOpened(CameraDevice cameraDevice) {
+ Logging.d(TAG, "Simple camera2 opened.");
+
+ synchronized (cameraDeviceLock) {
+ SimpleCamera2.this.cameraDevice = cameraDevice;
+ openSucceeded = true;
+ }
+
+ openDoneSignal.countDown();
+ }
+ }
+
+ SimpleCamera2(Context context, String deviceName) {
+ cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ looperThread = new LooperThread();
+ looperThread.start();
+ looperThread.waitToStart();
+ cameraDeviceLock = new Object();
+ openDoneSignal = new CountDownLatch(1);
+ cameraDevice = null;
+ Logging.d(TAG, "Opening simple camera2.");
+ try {
+ cameraManager.openCamera(deviceName, new CameraStateCallback(), looperThread.getHandler());
+ } catch (CameraAccessException e) {
+ fail("Simple camera2 CameraAccessException: " + e.getMessage());
+ }
+
+ Logging.d(TAG, "Waiting for simple camera2 to open.");
+ ThreadUtils.awaitUninterruptibly(openDoneSignal);
+ synchronized (cameraDeviceLock) {
+ if (!openSucceeded) {
+ fail("Opening simple camera2 failed.");
+ }
+ }
+ }
+
+ public void close() {
+ Logging.d(TAG, "Closing simple camera2.");
+ synchronized (cameraDeviceLock) {
+ if (cameraDevice != null) {
+ cameraDevice.close();
+ }
+ }
+
+ looperThread.requestStop();
+ ThreadUtils.joinUninterruptibly(looperThread);
+ }
+ }
+
+ private class TestObjectFactory
+ extends CameraVideoCapturerTestFixtures.TestObjectFactory {
+ @Override
+ public CameraEnumerator getCameraEnumerator() {
+ return new Camera2Enumerator(getAppContext());
+ }
+
+ @Override
+ public Context getAppContext() {
+ return getInstrumentation().getTargetContext();
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public Object rawOpenCamera(String cameraName) {
+ return new SimpleCamera2(getAppContext(), cameraName);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void rawCloseCamera(Object camera) {
+ ((SimpleCamera2) camera).close();
+ }
+ }
+
+ private CameraVideoCapturerTestFixtures fixtures;
+
+ @Override
+ protected void setUp() {
+ fixtures = new CameraVideoCapturerTestFixtures(new TestObjectFactory());
+ }
+
+ @Override
+ protected void tearDown() {
+ fixtures.dispose();
+ }
+
+ @SmallTest
+ public void testCreateAndDispose() {
+ fixtures.createCapturerAndDispose();
+ }
+
+ @SmallTest
+ public void testCreateNonExistingCamera() {
+ fixtures.createNonExistingCamera();
+ }
+
+ // This test that the camera can be started and that the frames are forwarded
+ // to a Java video renderer using a "default" capturer.
+ // It tests both the Java and the C++ layer.
+ @MediumTest
+ public void testCreateCapturerAndRender() throws InterruptedException {
+ fixtures.createCapturerAndRender();
+ }
+
+ // This test that the camera can be started and that the frames are forwarded
+ // to a Java video renderer using the front facing video capturer.
+ // It tests both the Java and the C++ layer.
+ @MediumTest
+ public void testStartFrontFacingVideoCapturer() throws InterruptedException {
+ fixtures.createFrontFacingCapturerAndRender();
+ }
+
+ // This test that the camera can be started and that the frames are forwarded
+ // to a Java video renderer using the back facing video capturer.
+ // It tests both the Java and the C++ layer.
+ @MediumTest
+ public void testStartBackFacingVideoCapturer() throws InterruptedException {
+ fixtures.createBackFacingCapturerAndRender();
+ }
+
+ // This test that the default camera can be started and that the camera can
+ // later be switched to another camera.
+ // It tests both the Java and the C++ layer.
+ @MediumTest
+ public void testSwitchVideoCapturer() throws InterruptedException {
+ fixtures.switchCamera();
+ }
+
+ @MediumTest
+ public void testCameraEvents() throws InterruptedException {
+ fixtures.cameraEventsInvoked();
+ }
+
+ // Test what happens when attempting to call e.g. switchCamera() after camera has been stopped.
+ @MediumTest
+ public void testCameraCallsAfterStop() throws InterruptedException {
+ fixtures.cameraCallsAfterStop();
+ }
+
+ // This test that the VideoSource that the CameraVideoCapturer is connected to can
+ // be stopped and restarted. It tests both the Java and the C++ layer.
+ @LargeTest
+ public void testStopRestartVideoSource() throws InterruptedException {
+ fixtures.stopRestartVideoSource();
+ }
+
+ // This test that the camera can be started at different resolutions.
+ // It does not test or use the C++ layer.
+ @LargeTest
+ public void testStartStopWithDifferentResolutions() throws InterruptedException {
+ fixtures.startStopWithDifferentResolutions();
+ }
+
+ // This test what happens if buffers are returned after the capturer have
+ // been stopped and restarted. It does not test or use the C++ layer.
+ @LargeTest
+ public void testReturnBufferLate() throws InterruptedException {
+ fixtures.returnBufferLateEndToEnd();
+ }
+
+ // This test that we can capture frames, keep the frames in a local renderer, stop capturing,
+ // and then return the frames. The difference between the test testReturnBufferLate() is that we
+ // also test the JNI and C++ AndroidVideoCapturer parts.
+ @MediumTest
+ public void testReturnBufferLateEndToEnd() throws InterruptedException {
+ fixtures.returnBufferLateEndToEnd();
+ }
+
+ // This test that CameraEventsHandler.onError is triggered if video buffers are not returned to
+ // the capturer.
+ @LargeTest
+ public void testCameraFreezedEventOnBufferStarvation() throws InterruptedException {
+ fixtures.cameraFreezedEventOnBufferStarvation();
+ }
+
+ // This test that frames forwarded to a renderer is scaled if onOutputFormatRequest is
+ // called. This test both Java and C++ parts of of the stack.
+ @MediumTest
+ public void testScaleCameraOutput() throws InterruptedException {
+ fixtures.scaleCameraOutput();
+ }
+
+ // This test that an error is reported if the camera is already opened
+ // when CameraVideoCapturer is started.
+ @LargeTest
+ public void testStartWhileCameraIsAlreadyOpen() throws InterruptedException {
+ fixtures.startWhileCameraIsAlreadyOpen();
+ }
+
+ // This test that CameraVideoCapturer can be started, even if the camera is already opened
+ // if the camera is closed while CameraVideoCapturer is re-trying to start.
+ @LargeTest
+ public void testStartWhileCameraIsAlreadyOpenAndCloseCamera() throws InterruptedException {
+ fixtures.startWhileCameraIsAlreadyOpenAndCloseCamera();
+ }
+
+ // This test that CameraVideoCapturer.stop can be called while CameraVideoCapturer is
+ // re-trying to start.
+ @MediumTest
+ public void testStartWhileCameraIsAlreadyOpenAndStop() throws InterruptedException {
+ fixtures.startWhileCameraIsAlreadyOpenAndStop();
+ }
+}
diff --git a/webrtc/api/java/android/org/webrtc/Camera1Enumerator.java b/webrtc/api/java/android/org/webrtc/Camera1Enumerator.java
index a553bfe..c10d974 100644
--- a/webrtc/api/java/android/org/webrtc/Camera1Enumerator.java
+++ b/webrtc/api/java/android/org/webrtc/Camera1Enumerator.java
@@ -128,7 +128,7 @@
}
// Convert from android.hardware.Camera.Size to Size.
- public static List<Size> convertSizes(List<android.hardware.Camera.Size> cameraSizes) {
+ static List<Size> convertSizes(List<android.hardware.Camera.Size> cameraSizes) {
final List<Size> sizes = new ArrayList<Size>();
for (android.hardware.Camera.Size size : cameraSizes) {
sizes.add(new Size(size.width, size.height));
@@ -137,7 +137,7 @@
}
// Convert from int[2] to CaptureFormat.FramerateRange.
- public static List<CaptureFormat.FramerateRange> convertFramerates(List<int[]> arrayRanges) {
+ static List<CaptureFormat.FramerateRange> convertFramerates(List<int[]> arrayRanges) {
final List<CaptureFormat.FramerateRange> ranges = new ArrayList<CaptureFormat.FramerateRange>();
for (int[] range : arrayRanges) {
ranges.add(new CaptureFormat.FramerateRange(
diff --git a/webrtc/api/java/android/org/webrtc/Camera2Capturer.java b/webrtc/api/java/android/org/webrtc/Camera2Capturer.java
new file mode 100644
index 0000000..8e44d69
--- /dev/null
+++ b/webrtc/api/java/android/org/webrtc/Camera2Capturer.java
@@ -0,0 +1,926 @@
+/*
+ * Copyright 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc;
+
+import org.webrtc.CameraEnumerationAndroid.CaptureFormat;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+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.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Build;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.Range;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Semaphore;
+
+@TargetApi(21)
+public class Camera2Capturer implements
+ CameraVideoCapturer,
+ SurfaceTextureHelper.OnTextureFrameAvailableListener {
+ private final static String TAG = "Camera2Capturer";
+
+ private final static int MAX_OPEN_CAMERA_ATTEMPTS = 3;
+ private final static int OPEN_CAMERA_DELAY_MS = 500;
+ private final static int STOP_TIMEOUT = 10000;
+ private final static int START_TIMEOUT = 10000;
+ private final static Object STOP_TIMEOUT_RUNNABLE_TOKEN = new Object();
+
+ // In the Camera2 API, starting a camera is inherently asynchronous, and this state is
+ // represented with 'STARTING'. Stopping is also asynchronous and this state is 'STOPPING'.
+ private static enum CameraState { IDLE, STARTING, RUNNING, STOPPING }
+
+ // Thread safe objects.
+ // --------------------
+ private final CameraManager cameraManager;
+ private final CameraEventsHandler eventsHandler;
+
+
+ // Shared state - guarded by cameraStateLock. Will only be edited from camera thread (when it is
+ // running).
+ // ---------------------------------------------------------------------------------------------
+ private final Object cameraStateLock = new Object();
+ private CameraState cameraState = CameraState.IDLE;
+ // |cameraThreadHandler| must be synchronized on |cameraStateLock| when not on the camera thread,
+ // or when modifying the reference. Use postOnCameraThread() instead of posting directly to
+ // the handler - this way all callbacks with a specifed token can be removed at once.
+ // |cameraThreadHandler| must be null if and only if CameraState is IDLE.
+ private Handler cameraThreadHandler;
+ // Remember the requested format in case we want to switch cameras.
+ private int requestedWidth;
+ private int requestedHeight;
+ private int requestedFramerate;
+
+ // Will only be edited while camera state is IDLE and cameraStateLock is acquired.
+ private String cameraName;
+ private boolean isFrontCamera;
+ private int cameraOrientation;
+
+ // Semaphore for allowing only one switch at a time.
+ private final Semaphore pendingCameraSwitchSemaphore = new Semaphore(1);
+ // Guarded by pendingCameraSwitchSemaphore
+ private CameraSwitchHandler switchEventsHandler;
+
+ // Internal state - must only be modified from camera thread
+ // ---------------------------------------------------------
+ private CaptureFormat captureFormat;
+ private Context applicationContext;
+ private CapturerObserver capturerObserver;
+ private CameraStatistics cameraStatistics;
+ private SurfaceTextureHelper surfaceTextureHelper;
+ private CameraCaptureSession captureSession;
+ private Surface surface;
+ private CameraDevice cameraDevice;
+ private CameraStateCallback cameraStateCallback;
+
+ // Factor to convert between Android framerates and CaptureFormat.FramerateRange. It will be
+ // either 1 or 1000.
+ private int fpsUnitFactor;
+ private boolean firstFrameReported;
+ private int consecutiveCameraOpenFailures;
+
+ public Camera2Capturer(
+ Context context, String cameraName, CameraEventsHandler eventsHandler) {
+ Logging.d(TAG, "Camera2Capturer ctor, camera name: " + cameraName);
+ this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ this.eventsHandler = eventsHandler;
+
+ setCameraName(cameraName);
+ }
+
+ /**
+ * Helper method for checking method is executed on camera thread. Also allows calls from other
+ * threads if camera is closed.
+ */
+ private void checkIsOnCameraThread() {
+ if (cameraState == CameraState.IDLE) {
+ return;
+ }
+
+ checkIsStrictlyOnCameraThread();
+ }
+
+ /**
+ * Like checkIsOnCameraThread but doesn't allow the camera to be stopped.
+ */
+ private void checkIsStrictlyOnCameraThread() {
+ if (cameraThreadHandler == null) {
+ throw new IllegalStateException("Camera is closed.");
+ }
+
+ if (Thread.currentThread() != cameraThreadHandler.getLooper().getThread()) {
+ throw new IllegalStateException("Wrong thread");
+ }
+ }
+
+ /**
+ * Checks method is not invoked on the camera thread. Used in functions waiting for the camera
+ * state to change since executing them on the camera thread would cause a deadlock.
+ */
+ private void checkNotOnCameraThread() {
+ if (cameraThreadHandler == null) {
+ return;
+ }
+
+ if (Thread.currentThread() == cameraThreadHandler.getLooper().getThread()) {
+ throw new IllegalStateException(
+ "Method waiting for camera state to change executed on camera thread");
+ }
+ }
+
+ private void waitForCameraToExitTransitionalState(
+ CameraState transitionalState, long timeoutMs) {
+ checkNotOnCameraThread();
+
+ // We probably should already have the lock when this is called but acquire it in case
+ // we don't have it.
+ synchronized (cameraStateLock) {
+ long timeoutAt = SystemClock.uptimeMillis() + timeoutMs;
+
+ while (cameraState == transitionalState) {
+ Logging.d(TAG, "waitForCameraToExitTransitionalState waiting: "
+ + cameraState);
+
+ long timeLeft = timeoutAt - SystemClock.uptimeMillis();
+
+ if (timeLeft <= 0) {
+ Logging.e(TAG, "Camera failed to exit transitional state " + transitionalState
+ + " within the time limit.");
+ break;
+ }
+
+ try {
+ cameraStateLock.wait(timeLeft);
+ } catch (InterruptedException e) {
+ Logging.w(TAG, "Trying to interrupt while waiting to exit transitional state "
+ + transitionalState + ", ignoring: " + e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Waits until camera state is not STOPPING.
+ */
+ private void waitForCameraToStopIfStopping() {
+ waitForCameraToExitTransitionalState(CameraState.STOPPING, STOP_TIMEOUT);
+ }
+
+ /**
+ * Wait until camera state is not STARTING.
+ */
+ private void waitForCameraToStartIfStarting() {
+ waitForCameraToExitTransitionalState(CameraState.STARTING, START_TIMEOUT);
+ }
+
+ /**
+ * Sets the name of the camera. Camera must be stopped or stopping when this is called.
+ */
+ private void setCameraName(String cameraName) {
+ final CameraCharacteristics characteristics;
+ try {
+ final String[] cameraIds = cameraManager.getCameraIdList();
+
+ if (cameraName.isEmpty() && cameraIds.length != 0) {
+ cameraName = cameraIds[0];
+ }
+
+ if (!Arrays.asList(cameraIds).contains(cameraName)) {
+ throw new IllegalArgumentException(
+ "Camera name: " + cameraName + " does not match any known camera device:");
+ }
+
+ characteristics = cameraManager.getCameraCharacteristics(cameraName);
+ } catch (CameraAccessException e) {
+ throw new RuntimeException("Camera access exception: " + e);
+ }
+
+ synchronized (cameraStateLock) {
+ waitForCameraToStopIfStopping();
+
+ if (cameraState != CameraState.IDLE) {
+ throw new RuntimeException("Changing camera name on running camera.");
+ }
+
+ // Note: Usually changing camera state from outside camera thread is not allowed. It is
+ // allowed here because camera is not running.
+ this.cameraName = cameraName;
+ isFrontCamera = characteristics.get(CameraCharacteristics.LENS_FACING)
+ == CameraMetadata.LENS_FACING_FRONT;
+
+ /*
+ * Clockwise angle through which the output image needs to be rotated to be upright on the
+ * device screen in its native orientation.
+ * Also defines the direction of rolling shutter readout, which is from top to bottom in the
+ * sensor's coordinate system.
+ * Units: Degrees of clockwise rotation; always a multiple of 90
+ */
+ cameraOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+ }
+ }
+
+ /**
+ * Triggers appropriate error handlers based on the camera state. Must be called on the camera
+ * thread and camera must not be stopped.
+ */
+ private void reportError(String errorDescription) {
+ checkIsStrictlyOnCameraThread();
+ Logging.e(TAG, "Error in camera at state " + cameraState + ": " + errorDescription);
+
+ if (switchEventsHandler != null) {
+ switchEventsHandler.onCameraSwitchError(errorDescription);
+ switchEventsHandler = null;
+ pendingCameraSwitchSemaphore.release();
+ }
+
+ switch (cameraState) {
+ case STARTING:
+ capturerObserver.onCapturerStarted(false /* success */);
+ // fall through
+ case RUNNING:
+ if (eventsHandler != null) {
+ eventsHandler.onCameraError(errorDescription);
+ }
+ break;
+ case STOPPING:
+ setCameraState(CameraState.IDLE);
+ Logging.e(TAG, "Closing camera failed: " + errorDescription);
+ return; // We don't want to call closeAndRelease in this case.
+ default:
+ throw new RuntimeException("Unknown camera state: " + cameraState);
+ }
+ closeAndRelease();
+ }
+
+ private void closeAndRelease() {
+ checkIsStrictlyOnCameraThread();
+
+ Logging.d(TAG, "Close and release.");
+ setCameraState(CameraState.STOPPING);
+
+ // Remove all pending Runnables posted from |this|.
+ cameraThreadHandler.removeCallbacksAndMessages(this /* token */);
+ applicationContext = null;
+ capturerObserver = null;
+ if (cameraStatistics != null) {
+ cameraStatistics.release();
+ cameraStatistics = null;
+ }
+ if (surfaceTextureHelper != null) {
+ surfaceTextureHelper.stopListening();
+ surfaceTextureHelper = null;
+ }
+ if (captureSession != null) {
+ captureSession.close();
+ captureSession = null;
+ }
+ if (surface != null) {
+ surface.release();
+ surface = null;
+ }
+ if (cameraDevice != null) {
+ // Add a timeout for stopping the camera.
+ cameraThreadHandler.postAtTime(new Runnable() {
+ @Override
+ public void run() {
+ Logging.e(TAG, "Camera failed to stop within the timeout. Force stopping.");
+ setCameraState(CameraState.IDLE);
+ if (eventsHandler != null) {
+ eventsHandler.onCameraError("Camera failed to stop (timeout).");
+ }
+ }
+ }, STOP_TIMEOUT_RUNNABLE_TOKEN, SystemClock.uptimeMillis() + STOP_TIMEOUT);
+
+ cameraDevice.close();
+ cameraDevice = null;
+ } else {
+ Logging.w(TAG, "closeAndRelease called while cameraDevice is null");
+ setCameraState(CameraState.IDLE);
+ }
+ this.cameraStateCallback = null;
+ }
+
+ /**
+ * Sets the camera state while ensuring constraints are followed.
+ */
+ private void setCameraState(CameraState newState) {
+ // State must only be modified on the camera thread. It can be edited from other threads
+ // if cameraState is IDLE since there is no camera thread.
+ checkIsOnCameraThread();
+
+ if (newState != CameraState.IDLE) {
+ if (cameraThreadHandler == null) {
+ throw new IllegalStateException(
+ "cameraThreadHandler must be null if and only if CameraState is IDLE.");
+ }
+ } else {
+ cameraThreadHandler = null;
+ }
+
+ switch (newState) {
+ case STARTING:
+ if (cameraState != CameraState.IDLE) {
+ throw new IllegalStateException("Only stopped camera can start.");
+ }
+ break;
+ case RUNNING:
+ if (cameraState != CameraState.STARTING) {
+ throw new IllegalStateException("Only starting camera can go to running state.");
+ }
+ break;
+ case STOPPING:
+ if (cameraState != CameraState.STARTING && cameraState != CameraState.RUNNING) {
+ throw new IllegalStateException("Only starting or running camera can stop.");
+ }
+ break;
+ case IDLE:
+ if (cameraState != CameraState.STOPPING) {
+ throw new IllegalStateException("Only stopping camera can go to idle state.");
+ }
+ break;
+ default:
+ throw new RuntimeException("Unknown camera state: " + newState);
+ }
+
+ synchronized (cameraStateLock) {
+ cameraState = newState;
+ cameraStateLock.notifyAll();
+ }
+ }
+
+ /**
+ * Internal method for opening the camera. Must be called on the camera thread.
+ */
+ private void openCamera() {
+ try {
+ checkIsStrictlyOnCameraThread();
+
+ if (cameraState != CameraState.STARTING) {
+ throw new IllegalStateException("Camera should be in state STARTING in openCamera.");
+ }
+
+ if (cameraThreadHandler == null) {
+ throw new RuntimeException("Someone set cameraThreadHandler to null while the camera "
+ + "state was STARTING. This should never happen");
+ }
+
+ // Camera is in state STARTING so cameraName will not be edited.
+ cameraManager.openCamera(cameraName, cameraStateCallback, cameraThreadHandler);
+ } catch (CameraAccessException e) {
+ reportError("Failed to open camera: " + e);
+ }
+ }
+
+ private void startCaptureOnCameraThread(
+ final int requestedWidth, final int requestedHeight, final int requestedFramerate,
+ final SurfaceTextureHelper surfaceTextureHelper, final Context applicationContext,
+ final CapturerObserver capturerObserver) {
+ checkIsStrictlyOnCameraThread();
+
+ firstFrameReported = false;
+ consecutiveCameraOpenFailures = 0;
+
+ this.applicationContext = applicationContext;
+ this.capturerObserver = capturerObserver;
+ this.surfaceTextureHelper = surfaceTextureHelper;
+ this.cameraStateCallback = new CameraStateCallback();
+
+ synchronized (cameraStateLock) {
+ // Remember the requested format in case we want to switch cameras.
+ this.requestedWidth = requestedWidth;
+ this.requestedHeight = requestedHeight;
+ this.requestedFramerate = requestedFramerate;
+ }
+
+ final CameraCharacteristics cameraCharacteristics;
+ try {
+ // Camera is in state STARTING so cameraName will not be edited.
+ cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName);
+ } catch (CameraAccessException e) {
+ reportError("getCameraCharacteristics(): " + e.getMessage());
+ return;
+ }
+
+ List<CaptureFormat.FramerateRange> framerateRanges =
+ Camera2Enumerator.getSupportedFramerateRanges(cameraCharacteristics);
+ List<Size> sizes = Camera2Enumerator.getSupportedSizes(cameraCharacteristics);
+
+ if (framerateRanges.isEmpty() || sizes.isEmpty()) {
+ reportError("No supported capture formats.");
+ }
+
+ // Some LEGACY camera implementations use fps rates that are multiplied with 1000. Make sure
+ // all values are multiplied with 1000 for consistency.
+ this.fpsUnitFactor = (framerateRanges.get(0).max > 1000) ? 1 : 1000;
+
+ final CaptureFormat.FramerateRange bestFpsRange =
+ CameraEnumerationAndroid.getClosestSupportedFramerateRange(
+ framerateRanges, requestedFramerate);
+
+ final Size bestSize = CameraEnumerationAndroid.getClosestSupportedSize(
+ sizes, requestedWidth, requestedHeight);
+
+ this.captureFormat = new CaptureFormat(bestSize.width, bestSize.height, bestFpsRange);
+ Logging.d(TAG, "Using capture format: " + captureFormat);
+
+ Logging.d(TAG, "Opening camera " + cameraName);
+ if (eventsHandler != null) {
+ int cameraIndex = -1;
+ try {
+ cameraIndex = Integer.parseInt(cameraName);
+ } catch (NumberFormatException e) {
+ Logging.d(TAG, "External camera with non-int identifier: " + cameraName);
+ }
+ eventsHandler.onCameraOpening(cameraIndex);
+ }
+
+ openCamera();
+ }
+
+ /**
+ * Starts capture using specified settings. This is automatically called for you by
+ * VideoCapturerTrackSource if you are just using the camera as source for video track.
+ */
+ @Override
+ public void startCapture(
+ final int requestedWidth, final int requestedHeight, final int requestedFramerate,
+ final SurfaceTextureHelper surfaceTextureHelper, final Context applicationContext,
+ final CapturerObserver capturerObserver) {
+ Logging.d(TAG, "startCapture requested: " + requestedWidth + "x" + requestedHeight
+ + "@" + requestedFramerate);
+ if (surfaceTextureHelper == null) {
+ throw new IllegalArgumentException("surfaceTextureHelper not set.");
+ }
+ if (applicationContext == null) {
+ throw new IllegalArgumentException("applicationContext not set.");
+ }
+ if (capturerObserver == null) {
+ throw new IllegalArgumentException("capturerObserver not set.");
+ }
+
+ synchronized (cameraStateLock) {
+ waitForCameraToStopIfStopping();
+ if (cameraState != CameraState.IDLE) {
+ Logging.e(TAG, "Unexpected camera state for startCapture: " + cameraState);
+ return;
+ }
+ this.cameraThreadHandler = surfaceTextureHelper.getHandler();
+ setCameraState(CameraState.STARTING);
+ }
+
+ postOnCameraThread(new Runnable() {
+ @Override
+ public void run() {
+ startCaptureOnCameraThread(requestedWidth, requestedHeight, requestedFramerate,
+ surfaceTextureHelper, applicationContext, capturerObserver);
+ }
+ });
+ }
+
+ final class CameraStateCallback extends CameraDevice.StateCallback {
+ private String getErrorDescription(int errorCode) {
+ switch (errorCode) {
+ case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE:
+ return "Camera device has encountered a fatal error.";
+ case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED:
+ return "Camera device could not be opened due to a device policy.";
+ case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE:
+ return "Camera device is in use already.";
+ case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE:
+ return "Camera service has encountered a fatal error.";
+ case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE:
+ return "Camera device could not be opened because"
+ + " there are too many other open camera devices.";
+ default:
+ return "Unknown camera error: " + errorCode;
+ }
+ }
+
+ @Override
+ public void onDisconnected(CameraDevice camera) {
+ checkIsStrictlyOnCameraThread();
+ cameraDevice = camera;
+ reportError("Camera disconnected.");
+ }
+
+ @Override
+ public void onError(CameraDevice camera, int errorCode) {
+ checkIsStrictlyOnCameraThread();
+ cameraDevice = camera;
+
+ if (cameraState == CameraState.STARTING && (
+ errorCode == CameraDevice.StateCallback.ERROR_CAMERA_IN_USE ||
+ errorCode == CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE)) {
+ consecutiveCameraOpenFailures++;
+
+ if (consecutiveCameraOpenFailures < MAX_OPEN_CAMERA_ATTEMPTS) {
+ Logging.w(TAG, "Opening camera failed, trying again: " + getErrorDescription(errorCode));
+
+ postDelayedOnCameraThread(OPEN_CAMERA_DELAY_MS, new Runnable() {
+ public void run() {
+ openCamera();
+ }
+ });
+ return;
+ } else {
+ Logging.e(TAG, "Opening camera failed too many times. Passing the error.");
+ }
+ }
+
+ reportError(getErrorDescription(errorCode));
+ }
+
+ @Override
+ public void onOpened(CameraDevice camera) {
+ checkIsStrictlyOnCameraThread();
+
+ Logging.d(TAG, "Camera opened.");
+ if (cameraState != CameraState.STARTING) {
+ throw new IllegalStateException("Unexpected state when camera opened: " + cameraState);
+ }
+
+ cameraDevice = camera;
+ final SurfaceTexture surfaceTexture = surfaceTextureHelper.getSurfaceTexture();
+ surfaceTexture.setDefaultBufferSize(captureFormat.width, captureFormat.height);
+ surface = new Surface(surfaceTexture);
+ try {
+ camera.createCaptureSession(
+ Arrays.asList(surface), new CaptureSessionCallback(), cameraThreadHandler);
+ } catch (CameraAccessException e) {
+ reportError("Failed to create capture session. " + e);
+ }
+ }
+
+ @Override
+ public void onClosed(CameraDevice camera) {
+ checkIsStrictlyOnCameraThread();
+
+ Logging.d(TAG, "Camera device closed.");
+
+ if (cameraState != CameraState.STOPPING) {
+ Logging.e(TAG, "Camera state was not STOPPING in onClosed. Most likely camera didn't stop "
+ + "within timelimit and this method was invoked twice.");
+ return;
+ }
+
+ cameraThreadHandler.removeCallbacksAndMessages(STOP_TIMEOUT_RUNNABLE_TOKEN);
+ setCameraState(CameraState.IDLE);
+ if (eventsHandler != null) {
+ eventsHandler.onCameraClosed();
+ }
+ }
+ }
+
+ final class CaptureSessionCallback extends CameraCaptureSession.StateCallback {
+ @Override
+ public void onConfigureFailed(CameraCaptureSession session) {
+ checkIsStrictlyOnCameraThread();
+ captureSession = session;
+ reportError("Failed to configure capture session.");
+ }
+
+ @Override
+ public void onConfigured(CameraCaptureSession session) {
+ checkIsStrictlyOnCameraThread();
+ Logging.d(TAG, "Camera capture session configured.");
+ captureSession = session;
+ try {
+ /*
+ * The viable options for video capture requests are:
+ * TEMPLATE_PREVIEW: High frame rate is given priority over the highest-quality
+ * post-processing.
+ * TEMPLATE_RECORD: Stable frame rate is used, and post-processing is set for recording
+ * quality.
+ */
+ final CaptureRequest.Builder captureRequestBuilder =
+ cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
+ // Set auto exposure fps range.
+ captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<Integer>(
+ captureFormat.framerate.min / fpsUnitFactor,
+ captureFormat.framerate.max / fpsUnitFactor));
+ captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON);
+ captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false);
+
+ captureRequestBuilder.addTarget(surface);
+ session.setRepeatingRequest(
+ captureRequestBuilder.build(), new CameraCaptureCallback(), cameraThreadHandler);
+ } catch (CameraAccessException e) {
+ reportError("Failed to start capture request. " + e);
+ return;
+ }
+
+ Logging.d(TAG, "Camera device successfully started.");
+ surfaceTextureHelper.startListening(Camera2Capturer.this);
+ capturerObserver.onCapturerStarted(true /* success */);
+ cameraStatistics = new CameraStatistics(surfaceTextureHelper, eventsHandler);
+ setCameraState(CameraState.RUNNING);
+
+ if (switchEventsHandler != null) {
+ switchEventsHandler.onCameraSwitchDone(isFrontCamera);
+ switchEventsHandler = null;
+ pendingCameraSwitchSemaphore.release();
+ }
+ }
+ }
+
+ final class CameraCaptureCallback extends CameraCaptureSession.CaptureCallback {
+ static final int MAX_CONSECUTIVE_CAMERA_CAPTURE_FAILURES = 10;
+ int consecutiveCameraCaptureFailures;
+
+ @Override
+ public void onCaptureFailed(
+ CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
+ checkIsOnCameraThread();
+ ++consecutiveCameraCaptureFailures;
+ if (consecutiveCameraCaptureFailures > MAX_CONSECUTIVE_CAMERA_CAPTURE_FAILURES) {
+ reportError("Capture failed " + consecutiveCameraCaptureFailures + " consecutive times.");
+ }
+ }
+
+ @Override
+ public void onCaptureCompleted(
+ CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
+ // TODO(sakal): This sometimes gets called after camera has stopped, investigate
+ checkIsOnCameraThread();
+ consecutiveCameraCaptureFailures = 0;
+ }
+ }
+
+
+
+ // Switch camera to the next valid camera id. This can only be called while
+ // the camera is running.
+ @Override
+ public void switchCamera(final CameraSwitchHandler switchEventsHandler) {
+ final String[] cameraIds;
+ try {
+ cameraIds = cameraManager.getCameraIdList();
+ } catch (CameraAccessException e) {
+ if (switchEventsHandler != null) {
+ switchEventsHandler.onCameraSwitchError("Could not get camera names: " + e);
+ }
+ return;
+ }
+ if (cameraIds.length < 2) {
+ if (switchEventsHandler != null) {
+ switchEventsHandler.onCameraSwitchError("No camera to switch to.");
+ }
+ return;
+ }
+ // Do not handle multiple camera switch request to avoid blocking camera thread by handling too
+ // many switch request from a queue. We have to be careful to always release this.
+ if (!pendingCameraSwitchSemaphore.tryAcquire()) {
+ Logging.w(TAG, "Ignoring camera switch request.");
+ if (switchEventsHandler != null) {
+ switchEventsHandler.onCameraSwitchError("Pending camera switch already in progress.");
+ }
+ return;
+ }
+
+ final String newCameraId;
+ final SurfaceTextureHelper surfaceTextureHelper;
+ final Context applicationContext;
+ final CapturerObserver capturerObserver;
+ final int requestedWidth;
+ final int requestedHeight;
+ final int requestedFramerate;
+
+ synchronized (cameraStateLock) {
+ waitForCameraToStartIfStarting();
+
+ if (cameraState != CameraState.RUNNING) {
+ Logging.e(TAG, "Calling swithCamera() on stopped camera.");
+ if (switchEventsHandler != null) {
+ switchEventsHandler.onCameraSwitchError("Camera is stopped.");
+ }
+ pendingCameraSwitchSemaphore.release();
+ return;
+ }
+
+ // Calculate new camera index and camera id. Camera is in state RUNNING so cameraName will
+ // not be edited.
+ final int currentCameraIndex = Arrays.asList(cameraIds).indexOf(cameraName);
+ if (currentCameraIndex == -1) {
+ Logging.e(TAG, "Couldn't find current camera id " + cameraName
+ + " in list of camera ids: " + Arrays.toString(cameraIds));
+ }
+ final int newCameraIndex = (currentCameraIndex + 1) % cameraIds.length;
+ newCameraId = cameraIds[newCameraIndex];
+
+ // Remember parameters. These are not null since camera is in RUNNING state. They aren't
+ // edited either while camera is in RUNNING state.
+ surfaceTextureHelper = this.surfaceTextureHelper;
+ applicationContext = this.applicationContext;
+ capturerObserver = this.capturerObserver;
+ requestedWidth = this.requestedWidth;
+ requestedHeight = this.requestedHeight;
+ requestedFramerate = this.requestedFramerate;
+ this.switchEventsHandler = switchEventsHandler;
+ }
+
+ // Make the switch.
+ stopCapture();
+ setCameraName(newCameraId);
+ startCapture(requestedWidth, requestedHeight, requestedFramerate, surfaceTextureHelper,
+ applicationContext, capturerObserver);
+
+ // Note: switchEventsHandler will be called from onConfigured / reportError.
+ }
+
+ // Requests a new output format from the video capturer. Captured frames
+ // by the camera will be scaled/or dropped by the video capturer.
+ // It does not matter if width and height are flipped. I.E, |width| = 640, |height| = 480 produce
+ // the same result as |width| = 480, |height| = 640.
+ // TODO(magjed/perkj): Document what this function does. Change name?
+ @Override
+ public void onOutputFormatRequest(final int width, final int height, final int framerate) {
+ postOnCameraThread(new Runnable() {
+ @Override
+ public void run() {
+ if (capturerObserver == null) {
+ Logging.e(TAG, "Calling onOutputFormatRequest() on stopped camera.");
+ return;
+ }
+ Logging.d(TAG,
+ "onOutputFormatRequestOnCameraThread: " + width + "x" + height + "@" + framerate);
+ capturerObserver.onOutputFormatRequest(width, height, framerate);
+ }
+ });
+ }
+
+ // Reconfigure the camera to capture in a new format. This should only be called while the camera
+ // is running.
+ @Override
+ public void changeCaptureFormat(final int width, final int height, final int framerate) {
+ final SurfaceTextureHelper surfaceTextureHelper;
+ final Context applicationContext;
+ final CapturerObserver capturerObserver;
+
+ synchronized (cameraStateLock) {
+ waitForCameraToStartIfStarting();
+
+ if (cameraState != CameraState.RUNNING) {
+ Logging.e(TAG, "Calling changeCaptureFormat() on stopped camera.");
+ return;
+ }
+
+ requestedWidth = width;
+ requestedHeight = height;
+ requestedFramerate = framerate;
+
+ surfaceTextureHelper = this.surfaceTextureHelper;
+ applicationContext = this.applicationContext;
+ capturerObserver = this.capturerObserver;
+ }
+
+ // Make the switch.
+ stopCapture();
+ // TODO(magjed/sakal): Just recreate session.
+ startCapture(width, height, framerate,
+ surfaceTextureHelper, applicationContext, capturerObserver);
+ }
+
+ @Override
+ public List<CaptureFormat> getSupportedFormats() {
+ synchronized (cameraState) {
+ return Camera2Enumerator.getSupportedFormats(this.cameraManager, cameraName);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ synchronized (cameraStateLock) {
+ waitForCameraToStopIfStopping();
+
+ if (cameraState != CameraState.IDLE) {
+ throw new IllegalStateException("Unexpected camera state for dispose: " + cameraState);
+ }
+ }
+ }
+
+ // Blocks until camera is known to be stopped.
+ @Override
+ public void stopCapture() {
+ final CountDownLatch cameraStoppingLatch = new CountDownLatch(1);
+
+ Logging.d(TAG, "stopCapture");
+ checkNotOnCameraThread();
+
+ synchronized (cameraStateLock) {
+ waitForCameraToStartIfStarting();
+
+ if (cameraState != CameraState.RUNNING) {
+ Logging.w(TAG, "stopCapture called for already stopped camera.");
+ return;
+ }
+
+ postOnCameraThread(new Runnable() {
+ @Override
+ public void run() {
+ Logging.d(TAG, "stopCaptureOnCameraThread");
+
+ // Stop capture.
+ closeAndRelease();
+ cameraStoppingLatch.countDown();
+ }
+ });
+ }
+
+ // Wait for the stopping to start
+ ThreadUtils.awaitUninterruptibly(cameraStoppingLatch);
+
+ Logging.d(TAG, "stopCapture done");
+ }
+
+ private void postOnCameraThread(Runnable runnable) {
+ postDelayedOnCameraThread(0 /* delayMs */, runnable);
+ }
+
+ private void postDelayedOnCameraThread(int delayMs, Runnable runnable) {
+ synchronized (cameraStateLock) {
+ if ((cameraState != CameraState.STARTING && cameraState != CameraState.RUNNING)
+ || !cameraThreadHandler.postAtTime(
+ runnable, this /* token */, SystemClock.uptimeMillis() + delayMs)) {
+ Logging.w(TAG, "Runnable not scheduled even though it was requested.");
+ }
+ }
+ }
+
+ private int getDeviceOrientation() {
+ int orientation = 0;
+
+ WindowManager wm = (WindowManager) applicationContext.getSystemService(
+ Context.WINDOW_SERVICE);
+ switch(wm.getDefaultDisplay().getRotation()) {
+ case Surface.ROTATION_90:
+ orientation = 90;
+ break;
+ case Surface.ROTATION_180:
+ orientation = 180;
+ break;
+ case Surface.ROTATION_270:
+ orientation = 270;
+ break;
+ case Surface.ROTATION_0:
+ default:
+ orientation = 0;
+ break;
+ }
+ return orientation;
+ }
+
+ @Override
+ public void onTextureFrameAvailable(
+ int oesTextureId, float[] transformMatrix, long timestampNs) {
+ checkIsStrictlyOnCameraThread();
+
+ if (eventsHandler != null && !firstFrameReported) {
+ eventsHandler.onFirstFrameAvailable();
+ firstFrameReported = true;
+ }
+
+ int rotation;
+ if (isFrontCamera) {
+ // Undo the mirror that the OS "helps" us with.
+ // http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
+ rotation = cameraOrientation + getDeviceOrientation();
+ transformMatrix =
+ RendererCommon.multiplyMatrices(transformMatrix, RendererCommon.horizontalFlipMatrix());
+ } else {
+ rotation = cameraOrientation - getDeviceOrientation();
+ }
+ // Make sure |rotation| is between 0 and 360.
+ rotation = (360 + rotation % 360) % 360;
+
+ // Undo camera orientation - we report it as rotation instead.
+ transformMatrix = RendererCommon.rotateTextureMatrix(transformMatrix, -cameraOrientation);
+
+ cameraStatistics.addFrame();
+ capturerObserver.onTextureFrameCaptured(captureFormat.width, captureFormat.height, oesTextureId,
+ transformMatrix, rotation, timestampNs);
+ }
+}
diff --git a/webrtc/api/java/android/org/webrtc/Camera2Enumerator.java b/webrtc/api/java/android/org/webrtc/Camera2Enumerator.java
index 8c342c5..fe2b259 100644
--- a/webrtc/api/java/android/org/webrtc/Camera2Enumerator.java
+++ b/webrtc/api/java/android/org/webrtc/Camera2Enumerator.java
@@ -10,20 +10,20 @@
package org.webrtc;
+import org.webrtc.CameraEnumerationAndroid.CaptureFormat;
+
import android.annotation.TargetApi;
import android.content.Context;
-
import android.graphics.ImageFormat;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.os.Build;
import android.os.SystemClock;
import android.util.Range;
-import android.util.Size;
-
-import org.webrtc.CameraEnumerationAndroid.CaptureFormat;
-import org.webrtc.Logging;
import java.util.ArrayList;
import java.util.HashMap;
@@ -31,7 +31,7 @@
import java.util.Map;
@TargetApi(21)
-public class Camera2Enumerator {
+public class Camera2Enumerator implements CameraEnumerator {
private final static String TAG = "Camera2Enumerator";
private final static double NANO_SECONDS_PER_SECOND = 1.0e9;
@@ -40,21 +40,104 @@
private static final Map<String, List<CaptureFormat>> cachedSupportedFormats =
new HashMap<String, List<CaptureFormat>>();
+ final Context context;
+ final CameraManager cameraManager;
+
+ public Camera2Enumerator(Context context) {
+ this.context = context;
+ this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ }
+
+ @Override
+ public String[] getDeviceNames() {
+ try {
+ return cameraManager.getCameraIdList();
+ } catch (CameraAccessException e) {
+ Logging.e(TAG, "Camera access exception: " + e);
+ return new String[] {};
+ }
+ }
+
+ @Override
+ public boolean isFrontFacing(String deviceName) {
+ CameraCharacteristics characteristics
+ = getCameraCharacteristics(deviceName);
+
+ return characteristics != null
+ && characteristics.get(CameraCharacteristics.LENS_FACING)
+ == CameraMetadata.LENS_FACING_FRONT;
+ }
+
+ @Override
+ public boolean isBackFacing(String deviceName) {
+ CameraCharacteristics characteristics
+ = getCameraCharacteristics(deviceName);
+
+ return characteristics != null
+ && characteristics.get(CameraCharacteristics.LENS_FACING)
+ == CameraMetadata.LENS_FACING_BACK;
+ }
+
+ @Override
+ public CameraVideoCapturer createCapturer(String deviceName,
+ CameraVideoCapturer.CameraEventsHandler eventsHandler) {
+ return new Camera2Capturer(context, deviceName, eventsHandler);
+ }
+
+ private CameraCharacteristics getCameraCharacteristics(String deviceName) {
+ try {
+ return cameraManager.getCameraCharacteristics(deviceName);
+ } catch (CameraAccessException e) {
+ Logging.e(TAG, "Camera access exception: " + e);
+ return null;
+ }
+ }
+
public static boolean isSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
- public static List<CaptureFormat> getSupportedFormats(Context context, String cameraId) {
+ static List<CaptureFormat.FramerateRange> getSupportedFramerateRanges(
+ CameraCharacteristics cameraCharacteristics) {
+ final Range<Integer>[] fpsRanges =
+ cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
+
+ if (fpsRanges == null) {
+ return new ArrayList<CaptureFormat.FramerateRange>();
+ }
+
+ int maxFps = 0;
+ for (Range<Integer> fpsRange : fpsRanges) {
+ maxFps = Math.max(maxFps, fpsRange.getUpper());
+ }
+ int unitFactor = maxFps < 1000 ? 1000 : 1;
+ return convertFramerates(fpsRanges, unitFactor);
+ }
+
+ static List<Size> getSupportedSizes(
+ CameraCharacteristics cameraCharacteristics) {
+ final StreamConfigurationMap streamMap =
+ cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+ final android.util.Size[] sizes = streamMap.getOutputSizes(SurfaceTexture.class);
+ if (sizes == null) {
+ Logging.e(TAG, "No supported camera output sizes.");
+ return new ArrayList<Size>();
+ }
+ return convertSizes(sizes);
+ }
+
+ static List<CaptureFormat> getSupportedFormats(Context context, String cameraId) {
return getSupportedFormats(
(CameraManager) context.getSystemService(Context.CAMERA_SERVICE), cameraId);
}
- public static List<CaptureFormat> getSupportedFormats(
+ static List<CaptureFormat> getSupportedFormats(
CameraManager cameraManager, String cameraId) {
synchronized (cachedSupportedFormats) {
if (cachedSupportedFormats.containsKey(cameraId)) {
return cachedSupportedFormats.get(cameraId);
}
+
Logging.d(TAG, "Get supported formats for camera index " + cameraId + ".");
final long startTimeMs = SystemClock.elapsedRealtime();
@@ -66,35 +149,34 @@
return new ArrayList<CaptureFormat>();
}
- // Calculate default max fps from auto-exposure ranges in case getOutputMinFrameDuration() is
- // not supported.
- final Range<Integer>[] fpsRanges =
- cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
- int defaultMaxFps = 0;
- for (Range<Integer> fpsRange : fpsRanges) {
- defaultMaxFps = Math.max(defaultMaxFps, fpsRange.getUpper());
- }
-
final StreamConfigurationMap streamMap =
cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
- final Size[] sizes = streamMap.getOutputSizes(ImageFormat.YUV_420_888);
- if (sizes == null) {
- throw new RuntimeException("ImageFormat.YUV_420_888 not supported.");
+
+ List<CaptureFormat.FramerateRange> framerateRanges = getSupportedFramerateRanges(
+ cameraCharacteristics);
+ List<Size> sizes = getSupportedSizes(cameraCharacteristics);
+
+ int defaultMaxFps = 0;
+ for (CaptureFormat.FramerateRange framerateRange : framerateRanges) {
+ defaultMaxFps = Math.max(defaultMaxFps, framerateRange.max);
}
final List<CaptureFormat> formatList = new ArrayList<CaptureFormat>();
for (Size size : sizes) {
long minFrameDurationNs = 0;
try {
- minFrameDurationNs = streamMap.getOutputMinFrameDuration(ImageFormat.YUV_420_888, size);
+ minFrameDurationNs = streamMap.getOutputMinFrameDuration(SurfaceTexture.class,
+ new android.util.Size(size.width, size.height));
} catch (Exception e) {
// getOutputMinFrameDuration() is not supported on all devices. Ignore silently.
}
final int maxFps = (minFrameDurationNs == 0)
- ? defaultMaxFps
- : (int) Math.round(NANO_SECONDS_PER_SECOND / minFrameDurationNs);
- formatList.add(new CaptureFormat(size.getWidth(), size.getHeight(), 0, maxFps * 1000));
+ ? defaultMaxFps
+ : (int) Math.round(NANO_SECONDS_PER_SECOND / minFrameDurationNs) * 1000;
+ formatList.add(new CaptureFormat(size.width, size.height, 0, maxFps));
+ Logging.d(TAG, "Format: " + size.width + "x" + size.height + "@" + maxFps);
}
+
cachedSupportedFormats.put(cameraId, formatList);
final long endTimeMs = SystemClock.elapsedRealtime();
Logging.d(TAG, "Get supported formats for camera index " + cameraId + " done."
@@ -102,4 +184,25 @@
return formatList;
}
}
+
+ // Convert from android.util.Size to Size.
+ private static List<Size> convertSizes(android.util.Size[] cameraSizes) {
+ final List<Size> sizes = new ArrayList<Size>();
+ for (android.util.Size size : cameraSizes) {
+ sizes.add(new Size(size.getWidth(), size.getHeight()));
+ }
+ return sizes;
+ }
+
+ // Convert from android.util.Range<Integer> to CaptureFormat.FramerateRange.
+ private static List<CaptureFormat.FramerateRange> convertFramerates(
+ Range<Integer>[] arrayRanges, int unitFactor) {
+ final List<CaptureFormat.FramerateRange> ranges = new ArrayList<CaptureFormat.FramerateRange>();
+ for (Range<Integer> range : arrayRanges) {
+ ranges.add(new CaptureFormat.FramerateRange(
+ range.getLower() * unitFactor,
+ range.getUpper() * unitFactor));
+ }
+ return ranges;
+ }
}