-// Copyright 2013 The Chromium Authors. All rights reserved.
+// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import android.content.Context;
import android.graphics.ImageFormat;
-import android.graphics.SurfaceTexture;
-import android.hardware.Camera;
-import android.hardware.Camera.PreviewCallback;
-import android.hardware.Camera.Size;
-import android.opengl.GLES20;
-import android.util.Log;
-import android.view.Surface;
-import android.view.WindowManager;
import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.locks.ReentrantLock;
-
-/** This class implements the listener interface for receiving copies of preview
- * frames from the camera, plus a series of methods to manipulate camera and its
- * capture from the C++ side. Objects of this class are created via
- * createVideoCapture() and are explicitly owned by the creator. All methods
- * are invoked by this owner, including the callback OnPreviewFrame().
+/**
+ * Video Capture Device base class, defines a set of methods that native code
+ * needs to use to configure, start capture, and to be reached by callbacks and
+ * provides some neccesary data type(s) with accessors.
**/
@JNINamespace("media")
-public class VideoCapture implements PreviewCallback {
- static class CaptureFormat {
+public abstract class VideoCapture {
+
+ protected static class CaptureFormat {
+ int mWidth;
+ int mHeight;
+ final int mFramerate;
+ final int mPixelFormat;
+
public CaptureFormat(
int width, int height, int framerate, int pixelformat) {
mWidth = width;
mFramerate = framerate;
mPixelFormat = pixelformat;
}
- public int mWidth;
- public int mHeight;
- public final int mFramerate;
- public final int mPixelFormat;
- @CalledByNative("CaptureFormat")
+
public int getWidth() {
return mWidth;
}
- @CalledByNative("CaptureFormat")
+
public int getHeight() {
return mHeight;
}
- @CalledByNative("CaptureFormat")
+
public int getFramerate() {
return mFramerate;
}
- @CalledByNative("CaptureFormat")
+
public int getPixelFormat() {
return mPixelFormat;
}
}
- // Some devices don't support YV12 format correctly, even with JELLY_BEAN or
- // newer OS. To work around the issues on those devices, we have to request
- // NV21. Some other devices have troubles with certain capture resolutions
- // under a given one: for those, the resolution is swapped with a known
- // good. Both are supposed to be temporary hacks.
- private static class BuggyDeviceHack {
- private static class IdAndSizes {
- IdAndSizes(String model, String device, int minWidth, int minHeight) {
- mModel = model;
- mDevice = device;
- mMinWidth = minWidth;
- mMinHeight = minHeight;
- }
- public final String mModel;
- public final String mDevice;
- public final int mMinWidth;
- public final int mMinHeight;
- }
- private static final IdAndSizes s_CAPTURESIZE_BUGGY_DEVICE_LIST[] = {
- new IdAndSizes("Nexus 7", "flo", 640, 480)
- };
-
- private static final String[] s_COLORSPACE_BUGGY_DEVICE_LIST = {
- "SAMSUNG-SGH-I747",
- "ODROID-U2",
- };
-
- static void applyMinDimensions(CaptureFormat format) {
- // NOTE: this can discard requested aspect ratio considerations.
- for (IdAndSizes buggyDevice : s_CAPTURESIZE_BUGGY_DEVICE_LIST) {
- if (buggyDevice.mModel.contentEquals(android.os.Build.MODEL) &&
- buggyDevice.mDevice.contentEquals(android.os.Build.DEVICE)) {
- format.mWidth = (buggyDevice.mMinWidth > format.mWidth)
- ? buggyDevice.mMinWidth
- : format.mWidth;
- format.mHeight = (buggyDevice.mMinHeight > format.mHeight)
- ? buggyDevice.mMinHeight
- : format.mHeight;
- }
- }
- }
-
- static int getImageFormat() {
- if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) {
- return ImageFormat.NV21;
- }
-
- for (String buggyDevice : s_COLORSPACE_BUGGY_DEVICE_LIST) {
- if (buggyDevice.contentEquals(android.os.Build.MODEL)) {
- return ImageFormat.NV21;
- }
- }
- return ImageFormat.YV12;
- }
- }
-
- private Camera mCamera;
- public ReentrantLock mPreviewBufferLock = new ReentrantLock();
- private Context mContext = null;
- // True when native code has started capture.
- private boolean mIsRunning = false;
-
- private static final int NUM_CAPTURE_BUFFERS = 3;
- private int mExpectedFrameSize = 0;
- private int mId = 0;
+ protected CaptureFormat mCaptureFormat = null;
+ protected final Context mContext;
+ protected final int mId;
// Native callback context variable.
- private long mNativeVideoCaptureDeviceAndroid = 0;
- private int[] mGlTextures = null;
- private SurfaceTexture mSurfaceTexture = null;
- private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;
-
- private int mCameraOrientation = 0;
- private int mCameraFacing = 0;
- private int mDeviceOrientation = 0;
-
- CaptureFormat mCaptureFormat = null;
- private static final String TAG = "VideoCapture";
-
- @CalledByNative
- public static VideoCapture createVideoCapture(
- Context context, int id, long nativeVideoCaptureDeviceAndroid) {
- return new VideoCapture(context, id, nativeVideoCaptureDeviceAndroid);
- }
-
- @CalledByNative
- public static CaptureFormat[] getDeviceSupportedFormats(int id) {
- Camera camera;
- try {
- camera = Camera.open(id);
- } catch (RuntimeException ex) {
- Log.e(TAG, "Camera.open: " + ex);
- return null;
- }
- Camera.Parameters parameters = getCameraParameters(camera);
- if (parameters == null) {
- return null;
- }
+ protected final long mNativeVideoCaptureDeviceAndroid;
- ArrayList<CaptureFormat> formatList = new ArrayList<CaptureFormat>();
- // getSupportedPreview{Formats,FpsRange,PreviewSizes}() returns Lists
- // with at least one element, but when the camera is in bad state, they
- // can return null pointers; in that case we use a 0 entry, so we can
- // retrieve as much information as possible.
- List<Integer> pixelFormats = parameters.getSupportedPreviewFormats();
- if (pixelFormats == null) {
- pixelFormats = new ArrayList<Integer>();
- }
- if (pixelFormats.size() == 0) {
- pixelFormats.add(ImageFormat.UNKNOWN);
- }
- for (Integer previewFormat : pixelFormats) {
- int pixelFormat =
- AndroidImageFormatList.ANDROID_IMAGEFORMAT_UNKNOWN;
- if (previewFormat == ImageFormat.YV12) {
- pixelFormat = AndroidImageFormatList.ANDROID_IMAGEFORMAT_YV12;
- } else if (previewFormat == ImageFormat.NV21) {
- continue;
- }
-
- List<int[]> listFpsRange = parameters.getSupportedPreviewFpsRange();
- if (listFpsRange == null) {
- listFpsRange = new ArrayList<int[]>();
- }
- if (listFpsRange.size() == 0) {
- listFpsRange.add(new int[] {0, 0});
- }
- for (int[] fpsRange : listFpsRange) {
- List<Camera.Size> supportedSizes =
- parameters.getSupportedPreviewSizes();
- if (supportedSizes == null) {
- supportedSizes = new ArrayList<Camera.Size>();
- }
- if (supportedSizes.size() == 0) {
- supportedSizes.add(camera.new Size(0, 0));
- }
- for (Camera.Size size : supportedSizes) {
- formatList.add(new CaptureFormat(size.width, size.height,
- (fpsRange[0] + 999 ) / 1000, pixelFormat));
- }
- }
- }
- camera.release();
- return formatList.toArray(new CaptureFormat[formatList.size()]);
- }
-
- public VideoCapture(
- Context context, int id, long nativeVideoCaptureDeviceAndroid) {
+ VideoCapture(Context context,
+ int id,
+ long nativeVideoCaptureDeviceAndroid) {
mContext = context;
mId = id;
mNativeVideoCaptureDeviceAndroid = nativeVideoCaptureDeviceAndroid;
}
- // Returns true on success, false otherwise.
@CalledByNative
- public boolean allocate(int width, int height, int frameRate) {
- Log.d(TAG, "allocate: requested (" + width + "x" + height + ")@" +
- frameRate + "fps");
- try {
- mCamera = Camera.open(mId);
- } catch (RuntimeException ex) {
- Log.e(TAG, "allocate: Camera.open: " + ex);
- return false;
- }
+ abstract boolean allocate(int width, int height, int frameRate);
- Camera.CameraInfo cameraInfo = getCameraInfo(mId);
- if (cameraInfo == null) {
- mCamera.release();
- mCamera = null;
- return false;
- }
-
- mCameraOrientation = cameraInfo.orientation;
- mCameraFacing = cameraInfo.facing;
- mDeviceOrientation = getDeviceOrientation();
- Log.d(TAG, "allocate: orientation dev=" + mDeviceOrientation +
- ", cam=" + mCameraOrientation + ", facing=" + mCameraFacing);
-
- Camera.Parameters parameters = getCameraParameters(mCamera);
- if (parameters == null) {
- mCamera = null;
- return false;
- }
-
- // getSupportedPreviewFpsRange() returns a List with at least one
- // element, but when camera is in bad state, it can return null pointer.
- List<int[]> listFpsRange = parameters.getSupportedPreviewFpsRange();
- if (listFpsRange == null || listFpsRange.size() == 0) {
- Log.e(TAG, "allocate: no fps range found");
- return false;
- }
- int frameRateInMs = frameRate * 1000;
- // Use the first range as default.
- int[] fpsMinMax = listFpsRange.get(0);
- int newFrameRate = (fpsMinMax[0] + 999) / 1000;
- for (int[] fpsRange : listFpsRange) {
- if (fpsRange[0] <= frameRateInMs && frameRateInMs <= fpsRange[1]) {
- fpsMinMax = fpsRange;
- newFrameRate = frameRate;
- break;
- }
- }
- frameRate = newFrameRate;
- Log.d(TAG, "allocate: fps set to " + frameRate);
-
- // Calculate size.
- List<Camera.Size> listCameraSize =
- parameters.getSupportedPreviewSizes();
- int minDiff = Integer.MAX_VALUE;
- int matchedWidth = width;
- int matchedHeight = height;
- for (Camera.Size size : listCameraSize) {
- int diff = Math.abs(size.width - width) +
- Math.abs(size.height - height);
- Log.d(TAG, "allocate: supported (" +
- size.width + ", " + size.height + "), diff=" + diff);
- // TODO(wjia): Remove this hack (forcing width to be multiple
- // of 32) by supporting stride in video frame buffer.
- // Right now, VideoCaptureController requires compact YV12
- // (i.e., with no padding).
- if (diff < minDiff && (size.width % 32 == 0)) {
- minDiff = diff;
- matchedWidth = size.width;
- matchedHeight = size.height;
- }
- }
- if (minDiff == Integer.MAX_VALUE) {
- Log.e(TAG, "allocate: can not find a multiple-of-32 resolution");
- return false;
- }
-
- mCaptureFormat = new CaptureFormat(
- matchedWidth, matchedHeight, frameRate,
- BuggyDeviceHack.getImageFormat());
- // Hack to avoid certain capture resolutions under a minimum one,
- // see http://crbug.com/305294
- BuggyDeviceHack.applyMinDimensions(mCaptureFormat);
- Log.d(TAG, "allocate: matched (" + mCaptureFormat.mWidth + "x" +
- mCaptureFormat.mHeight + ")");
-
- if (parameters.isVideoStabilizationSupported()) {
- Log.d(TAG, "Image stabilization supported, currently: "
- + parameters.getVideoStabilization() + ", setting it.");
- parameters.setVideoStabilization(true);
- } else {
- Log.d(TAG, "Image stabilization not supported.");
- }
- parameters.setPreviewSize(mCaptureFormat.mWidth,
- mCaptureFormat.mHeight);
- parameters.setPreviewFormat(mCaptureFormat.mPixelFormat);
- parameters.setPreviewFpsRange(fpsMinMax[0], fpsMinMax[1]);
- mCamera.setParameters(parameters);
-
- // Set SurfaceTexture. Android Capture needs a SurfaceTexture even if
- // it is not going to be used.
- mGlTextures = new int[1];
- // Generate one texture pointer and bind it as an external texture.
- GLES20.glGenTextures(1, mGlTextures, 0);
- GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, mGlTextures[0]);
- // No mip-mapping with camera source.
- GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES,
- GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
- GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES,
- GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
- // Clamp to edge is only option.
- GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES,
- GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
- GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES,
- GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
-
- mSurfaceTexture = new SurfaceTexture(mGlTextures[0]);
- mSurfaceTexture.setOnFrameAvailableListener(null);
+ @CalledByNative
+ abstract int startCapture();
- try {
- mCamera.setPreviewTexture(mSurfaceTexture);
- } catch (IOException ex) {
- Log.e(TAG, "allocate: " + ex);
- return false;
- }
+ @CalledByNative
+ abstract int stopCapture();
- int bufSize = mCaptureFormat.mWidth *
- mCaptureFormat.mHeight *
- ImageFormat.getBitsPerPixel(
- mCaptureFormat.mPixelFormat) / 8;
- for (int i = 0; i < NUM_CAPTURE_BUFFERS; i++) {
- byte[] buffer = new byte[bufSize];
- mCamera.addCallbackBuffer(buffer);
- }
- mExpectedFrameSize = bufSize;
+ @CalledByNative
+ abstract void deallocate();
- return true;
- }
+ // Local hook to allow derived classes to configure and plug capture
+ // buffers if needed.
+ abstract void allocateBuffers();
@CalledByNative
- public int queryWidth() {
+ public final int queryWidth() {
return mCaptureFormat.mWidth;
}
@CalledByNative
- public int queryHeight() {
+ public final int queryHeight() {
return mCaptureFormat.mHeight;
}
@CalledByNative
- public int queryFrameRate() {
+ public final int queryFrameRate() {
return mCaptureFormat.mFramerate;
}
@CalledByNative
- public int getColorspace() {
+ public final int getColorspace() {
switch (mCaptureFormat.mPixelFormat) {
case ImageFormat.YV12:
- return AndroidImageFormatList.ANDROID_IMAGEFORMAT_YV12;
+ return AndroidImageFormat.YV12;
case ImageFormat.NV21:
- return AndroidImageFormatList.ANDROID_IMAGEFORMAT_NV21;
+ return AndroidImageFormat.NV21;
case ImageFormat.UNKNOWN:
default:
- return AndroidImageFormatList.ANDROID_IMAGEFORMAT_UNKNOWN;
- }
- }
-
- @CalledByNative
- public int startCapture() {
- if (mCamera == null) {
- Log.e(TAG, "startCapture: camera is null");
- return -1;
- }
-
- mPreviewBufferLock.lock();
- try {
- if (mIsRunning) {
- return 0;
- }
- mIsRunning = true;
- } finally {
- mPreviewBufferLock.unlock();
- }
- mCamera.setPreviewCallbackWithBuffer(this);
- try {
- mCamera.startPreview();
- } catch (RuntimeException ex) {
- Log.e(TAG, "startCapture: Camera.startPreview: " + ex);
- return -1;
- }
-
- return 0;
- }
-
- @CalledByNative
- public int stopCapture() {
- if (mCamera == null) {
- Log.e(TAG, "stopCapture: camera is null");
- return 0;
- }
-
- mPreviewBufferLock.lock();
- try {
- if (!mIsRunning) {
- return 0;
- }
- mIsRunning = false;
- } finally {
- mPreviewBufferLock.unlock();
- }
-
- mCamera.stopPreview();
- mCamera.setPreviewCallbackWithBuffer(null);
- return 0;
- }
-
- @CalledByNative
- public void deallocate() {
- if (mCamera == null)
- return;
-
- stopCapture();
- try {
- mCamera.setPreviewTexture(null);
- if (mGlTextures != null)
- GLES20.glDeleteTextures(1, mGlTextures, 0);
- mCaptureFormat = null;
- mCamera.release();
- mCamera = null;
- } catch (IOException ex) {
- Log.e(TAG, "deallocate: failed to deallocate camera, " + ex);
- return;
- }
- }
-
- @Override
- public void onPreviewFrame(byte[] data, Camera camera) {
- mPreviewBufferLock.lock();
- try {
- if (!mIsRunning) {
- return;
- }
- if (data.length == mExpectedFrameSize) {
- int rotation = getDeviceOrientation();
- if (rotation != mDeviceOrientation) {
- mDeviceOrientation = rotation;
- Log.d(TAG,
- "onPreviewFrame: device orientation=" +
- mDeviceOrientation + ", camera orientation=" +
- mCameraOrientation);
- }
- if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_BACK) {
- rotation = 360 - rotation;
- }
- rotation = (mCameraOrientation + rotation) % 360;
- nativeOnFrameAvailable(mNativeVideoCaptureDeviceAndroid,
- data, mExpectedFrameSize, rotation);
- }
- } finally {
- mPreviewBufferLock.unlock();
- if (camera != null) {
- camera.addCallbackBuffer(data);
- }
+ return AndroidImageFormat.UNKNOWN;
}
}
- // TODO(wjia): investigate whether reading from texture could give better
- // performance and frame rate, using onFrameAvailable().
-
- private static class ChromiumCameraInfo {
- private final int mId;
- private final Camera.CameraInfo mCameraInfo;
-
- private ChromiumCameraInfo(int index) {
- mId = index;
- mCameraInfo = getCameraInfo(mId);
- }
-
- @CalledByNative("ChromiumCameraInfo")
- private static int getNumberOfCameras() {
- return Camera.getNumberOfCameras();
- }
-
- @CalledByNative("ChromiumCameraInfo")
- private static ChromiumCameraInfo getAt(int index) {
- return new ChromiumCameraInfo(index);
- }
-
- @CalledByNative("ChromiumCameraInfo")
- private int getId() {
- return mId;
- }
-
- @CalledByNative("ChromiumCameraInfo")
- private String getDeviceName() {
- if (mCameraInfo == null) {
- return "";
- }
- return "camera " + mId + ", facing " +
- (mCameraInfo.facing ==
- Camera.CameraInfo.CAMERA_FACING_FRONT ? "front" : "back");
- }
-
- @CalledByNative("ChromiumCameraInfo")
- private int getOrientation() {
- return (mCameraInfo == null ? 0 : mCameraInfo.orientation);
- }
- }
-
- private native void nativeOnFrameAvailable(
+ // Method for VideoCapture implementations to call back native code.
+ public native void nativeOnFrameAvailable(
long nativeVideoCaptureDeviceAndroid,
byte[] data,
int length,
int rotation);
-
- private int getDeviceOrientation() {
- int orientation = 0;
- if (mContext != null) {
- WindowManager wm = (WindowManager) mContext.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;
- }
-
- private static Camera.Parameters getCameraParameters(Camera camera) {
- Camera.Parameters parameters;
- try {
- parameters = camera.getParameters();
- } catch (RuntimeException ex) {
- Log.e(TAG, "getCameraParameters: Camera.getParameters: " + ex);
- camera.release();
- return null;
- }
- return parameters;
- }
-
- private static Camera.CameraInfo getCameraInfo(int id) {
- Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
- try {
- Camera.getCameraInfo(id, cameraInfo);
- } catch (RuntimeException ex) {
- Log.e(TAG, "getCameraInfo: Camera.getCameraInfo: " + ex);
- return null;
- }
- return cameraInfo;
- }
}