CameraEnumerationAndroid: Make getSupportedFormats() an interface

Enumerating camera capabilities in the deprecated android.hardware.Camera interface is really slow because of the need to open and release the camera. By making getSupportedFormats() an interface, we allow apps the opportunity to inject their own implementation, such as storing the supported formats offline in the device's internal storage. It will also be possible to add an implementation of getSupportedFormats() using the new android.hardware.Camera2 interface in a follow-up CL.

R=tommi@webrtc.org

Review URL: https://codereview.webrtc.org/1321903002 .

Cr-Commit-Position: refs/heads/master@{#9819}
diff --git a/talk/app/webrtc/java/android/org/webrtc/CameraEnumerationAndroid.java b/talk/app/webrtc/java/android/org/webrtc/CameraEnumerationAndroid.java
index d6fa127..136c232 100644
--- a/talk/app/webrtc/java/android/org/webrtc/CameraEnumerationAndroid.java
+++ b/talk/app/webrtc/java/android/org/webrtc/CameraEnumerationAndroid.java
@@ -46,9 +46,23 @@
 @SuppressWarnings("deprecation")
 public class CameraEnumerationAndroid {
   private final static String TAG = "CameraEnumerationAndroid";
-  // List of formats supported by all cameras. This list is filled once in order
-  // to be able to switch cameras.
-  public static List<List<CaptureFormat>> supportedFormats;
+  // Synchronized on |CameraEnumerationAndroid.this|.
+  private static Enumerator enumerator = new CameraEnumerator();
+
+  public interface Enumerator {
+    /**
+     * Returns a list of supported CaptureFormats for the camera with index |cameraId|.
+     */
+    List<CaptureFormat> getSupportedFormats(int cameraId);
+  }
+
+  public static synchronized void setEnumerator(Enumerator enumerator) {
+    CameraEnumerationAndroid.enumerator = enumerator;
+  }
+
+  public static synchronized List<CaptureFormat> getSupportedFormats(int cameraId) {
+    return enumerator.getSupportedFormats(cameraId);
+  }
 
   public static class CaptureFormat {
     public final int width;
@@ -175,37 +189,8 @@
     return null;
   }
 
-  public static boolean initStatics() {
-    if (supportedFormats != null)
-      return true;
-    try {
-      Log.d(TAG, "Get supported formats.");
-      supportedFormats =
-          new ArrayList<List<CaptureFormat>>(Camera.getNumberOfCameras());
-      // Start requesting supported formats from camera with the highest index
-      // (back camera) first. If it fails then likely camera is in bad state.
-      for (int i = Camera.getNumberOfCameras() - 1; i >= 0; i--) {
-        ArrayList<CaptureFormat> supportedFormat = getSupportedFormats(i);
-        if (supportedFormat.size() == 0) {
-          Log.e(TAG, "Fail to get supported formats for camera " + i);
-          supportedFormats = null;
-          return false;
-        }
-        supportedFormats.add(supportedFormat);
-      }
-      // Reverse the list since it is filled in reverse order.
-      Collections.reverse(supportedFormats);
-      Log.d(TAG, "Get supported formats done.");
-      return true;
-    } catch (Exception e) {
-      supportedFormats = null;
-      Log.e(TAG, "InitStatics failed",e);
-    }
-    return false;
-  }
-
   public static String getSupportedFormatsAsJson(int id) throws JSONException {
-    List<CaptureFormat> formats = supportedFormats.get(id);
+    List<CaptureFormat> formats = getSupportedFormats(id);
     JSONArray json_formats = new JSONArray();
     for (CaptureFormat format : formats) {
       JSONObject json_format = new JSONObject();
@@ -219,42 +204,6 @@
     return json_formats.toString();
   }
 
-  // Returns a list of CaptureFormat for the camera with index id.
-  public static ArrayList<CaptureFormat> getSupportedFormats(int id) {
-    ArrayList<CaptureFormat> formatList = new ArrayList<CaptureFormat>();
-
-    Camera camera;
-    try {
-      Log.d(TAG, "Opening camera " + id);
-      camera = Camera.open(id);
-    } catch (Exception e) {
-      Log.e(TAG, "Open camera failed on id " + id, e);
-      return formatList;
-    }
-
-    try {
-      Camera.Parameters parameters;
-      parameters = camera.getParameters();
-      // getSupportedPreviewFpsRange returns a sorted list.
-      List<int[]> listFpsRange = parameters.getSupportedPreviewFpsRange();
-      int[] range = {0, 0};
-      if (listFpsRange != null)
-        range = listFpsRange.get(listFpsRange.size() -1);
-
-      List<Camera.Size> supportedSizes = parameters.getSupportedPreviewSizes();
-      for (Camera.Size size : supportedSizes) {
-        formatList.add(new CaptureFormat(size.width, size.height,
-            range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
-            range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]));
-      }
-    } catch (Exception e) {
-      Log.e(TAG, "getSupportedFormats failed on id " + id, e);
-    }
-    camera.release();
-    camera = null;
-    return formatList;
-  }
-
   // Helper class for finding the closest supported format for the two functions below.
   private static abstract class ClosestComparator<T> implements Comparator<T> {
     // Difference between supported and requested parameter.
diff --git a/talk/app/webrtc/java/android/org/webrtc/CameraEnumerator.java b/talk/app/webrtc/java/android/org/webrtc/CameraEnumerator.java
new file mode 100644
index 0000000..0e6b978
--- /dev/null
+++ b/talk/app/webrtc/java/android/org/webrtc/CameraEnumerator.java
@@ -0,0 +1,102 @@
+/*
+ * libjingle
+ * Copyright 2015 Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.webrtc;
+
+import android.hardware.Camera;
+import android.os.SystemClock;
+import android.util.Log;
+
+import org.webrtc.CameraEnumerationAndroid.CaptureFormat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SuppressWarnings("deprecation")
+public class CameraEnumerator implements CameraEnumerationAndroid.Enumerator {
+  private final static String TAG = "CameraEnumerator";
+  // Each entry contains the supported formats for corresponding camera index. The formats for all
+  // cameras are enumerated on the first call to getSupportedFormats(), and cached for future
+  // reference.
+  private List<List<CaptureFormat>> cachedSupportedFormats;
+
+  @Override
+  public List<CaptureFormat> getSupportedFormats(int cameraId) {
+    synchronized (this) {
+      if (cachedSupportedFormats == null) {
+        cachedSupportedFormats = new ArrayList<List<CaptureFormat>>();
+        for (int i = 0; i < CameraEnumerationAndroid.getDeviceCount(); ++i) {
+          cachedSupportedFormats.add(enumerateFormats(i));
+        }
+      }
+    }
+    return cachedSupportedFormats.get(cameraId);
+  }
+
+  private List<CaptureFormat> enumerateFormats(int cameraId) {
+    Log.d(TAG, "Get supported formats for camera index " + cameraId + ".");
+    final long startTimeMs = SystemClock.elapsedRealtime();
+    final Camera.Parameters parameters;
+    Camera camera = null;
+    try {
+      Log.d(TAG, "Opening camera with index " + cameraId);
+      camera = Camera.open(cameraId);
+      parameters = camera.getParameters();
+    } catch (RuntimeException e) {
+      Log.e(TAG, "Open camera failed on camera index " + cameraId, e);
+      return new ArrayList<CaptureFormat>();
+    } finally {
+      if (camera != null) {
+        camera.release();
+      }
+    }
+
+    final List<CaptureFormat> formatList = new ArrayList<CaptureFormat>();
+    try {
+      int minFps = 0;
+      int maxFps = 0;
+      final List<int[]> listFpsRange = parameters.getSupportedPreviewFpsRange();
+      if (listFpsRange != null) {
+        // getSupportedPreviewFpsRange() returns a sorted list. Take the fps range
+        // corresponding to the highest fps.
+        final int[] range = listFpsRange.get(listFpsRange.size() - 1);
+        minFps = range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX];
+        maxFps = range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX];
+      }
+      for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
+        formatList.add(new CaptureFormat(size.width, size.height, minFps, maxFps));
+      }
+    } catch (Exception e) {
+      Log.e(TAG, "getSupportedFormats() failed on camera index " + cameraId, e);
+    }
+
+    final long endTimeMs = SystemClock.elapsedRealtime();
+    Log.d(TAG, "Get supported formats for camera index " + cameraId + " done."
+        + " Time spent: " + (endTimeMs - startTimeMs) + " ms.");
+    return formatList;
+  }
+}
diff --git a/talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java b/talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java
index d4251f6..ffda916 100644
--- a/talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java
+++ b/talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java
@@ -220,7 +220,7 @@
   }
 
   public synchronized List<CaptureFormat> getSupportedFormats() {
-    return CameraEnumerationAndroid.supportedFormats.get(id);
+    return CameraEnumerationAndroid.getSupportedFormats(id);
   }
 
   // Return a list of timestamps for the frames that have been sent out, but not returned yet.
@@ -234,14 +234,11 @@
   }
 
   // Called by native code.
-  // Enumerates resolution and frame rates for all cameras to be able to switch
-  // cameras. Initializes local variables for the camera named |deviceName| and
-  // starts a thread to be used for capturing.
-  // If deviceName is empty, the first available device is used in order to be
-  // compatible with the generic VideoCapturer class.
+  // Initializes local variables for the camera named |deviceName|. If |deviceName| is empty, the
+  // first available device is used in order to be compatible with the generic VideoCapturer class.
   synchronized boolean init(String deviceName) {
     Log.d(TAG, "init: " + deviceName);
-    if (deviceName == null || !CameraEnumerationAndroid.initStatics())
+    if (deviceName == null)
       return false;
 
     boolean foundDevice = false;
diff --git a/talk/app/webrtc/java/jni/classreferenceholder.cc b/talk/app/webrtc/java/jni/classreferenceholder.cc
index 2c4f1e6..520b5a7 100644
--- a/talk/app/webrtc/java/jni/classreferenceholder.cc
+++ b/talk/app/webrtc/java/jni/classreferenceholder.cc
@@ -71,6 +71,8 @@
   LoadClass(jni, "org/webrtc/IceCandidate");
 #if defined(ANDROID) && !defined(WEBRTC_CHROMIUM_BUILD)
   LoadClass(jni, "android/graphics/SurfaceTexture");
+  LoadClass(jni, "org/webrtc/CameraEnumerator");
+  LoadClass(jni, "org/webrtc/CameraEnumerationAndroid");
   LoadClass(jni, "org/webrtc/VideoCapturerAndroid");
   LoadClass(jni, "org/webrtc/VideoCapturerAndroid$NativeObserver");
   LoadClass(jni, "org/webrtc/EglBase");