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;
+  }
 }