Chromium Code Reviews| Index: media/base/android/java/src/org/chromium/media/VideoCapture.java |
| =================================================================== |
| --- media/base/android/java/src/org/chromium/media/VideoCapture.java (revision 0) |
| +++ media/base/android/java/src/org/chromium/media/VideoCapture.java (revision 0) |
| @@ -0,0 +1,331 @@ |
| +// Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +package org.chromium.media; |
| + |
| +import android.content.Context; |
| +import android.graphics.ImageFormat; |
| +import android.graphics.SurfaceTexture; |
| +import android.graphics.SurfaceTexture.OnFrameAvailableListener; |
| +import android.hardware.Camera; |
| +import android.hardware.Camera.PreviewCallback; |
| +import android.opengl.GLES20; |
| +import android.util.Log; |
| +import android.view.Surface; |
| +import android.view.WindowManager; |
| + |
| +import java.io.IOException; |
| +import java.lang.RuntimeException; |
| +import java.util.concurrent.locks.ReentrantLock; |
| +import java.util.Iterator; |
| +import java.util.List; |
| + |
| +import org.chromium.base.CalledByNative; |
| +import org.chromium.base.JNINamespace; |
| + |
| +@JNINamespace("media") |
| +public class VideoCapture implements PreviewCallback, OnFrameAvailableListener { |
| + static class CaptureCapability { |
| + public int mWidth = 0; |
| + public int mHeight = 0; |
| + public int mDesiredFps = 0; |
| + } |
| + |
| + private Camera mCamera; |
| + public ReentrantLock mPreviewBufferLock = new ReentrantLock(); |
| + private int mPixelFormat = ImageFormat.YV12; |
| + 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; |
| + // Native callback context variable. |
| + private int 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; |
| + |
| + CaptureCapability mCurrentCapability = null; |
| + private static final String TAG = "VideoCapture"; |
| + |
| + // Returns an instance of VideoCapture. |
| + @CalledByNative |
| + public static VideoCapture createVideoCapture(Context context, int id, |
| + int nativeVideoCaptureDeviceAndroid) { |
| + return new VideoCapture(context, id, nativeVideoCaptureDeviceAndroid); |
| + } |
| + |
| + public VideoCapture(Context context, int id, |
| + int nativeVideoCaptureDeviceAndroid) { |
| + mContext = context; |
| + mId = id; |
| + mNativeVideoCaptureDeviceAndroid = nativeVideoCaptureDeviceAndroid; |
| + } |
| + |
| + // Returns 0 on success, -1 otherwise. |
|
Ami GONE FROM CHROMIUM
2013/01/30 19:46:23
Return boolean instead, true on success?
wjia(left Chromium)
2013/02/06 00:45:34
Done.
|
| + @CalledByNative |
| + public int allocate(int width, int height, int frameRate) { |
| + Log.d(TAG, "allocate: requested width=" + width + |
| + ", height=" + height + ", frameRate=" + frameRate); |
| + try { |
| + mCamera = Camera.open(mId); |
| + Camera.CameraInfo camera_info = new Camera.CameraInfo(); |
| + Camera.getCameraInfo(mId, camera_info); |
| + mCameraOrientation = camera_info.orientation; |
| + mCameraFacing = camera_info.facing; |
| + mDeviceOrientation = getDeviceOrientation(); |
| + Log.d(TAG, "allocate: device orientation=" + mDeviceOrientation + |
| + ", camera orientation=" + mCameraOrientation + |
| + ", facing=" + mCameraFacing); |
| + |
| + Camera.Parameters parameters = mCamera.getParameters(); |
| + |
| + // Calculate fps. |
| + List<int[]> listFpsRange = parameters.getSupportedPreviewFpsRange(); |
| + int frameRateInMs = frameRate * 1000; |
| + boolean fpsIsSupported = false; |
| + int fpsMin = 0; |
| + int fpsMax = 0; |
| + Iterator itFpsRange = listFpsRange.iterator(); |
| + while (itFpsRange.hasNext()) { |
| + int[] fpsRange = (int[])itFpsRange.next(); |
| + if (fpsRange[0] <= frameRateInMs && |
| + frameRateInMs <= fpsRange[1]) { |
| + fpsIsSupported = true; |
| + fpsMin = fpsRange[0]; |
| + fpsMax = fpsRange[1]; |
| + break; |
| + } |
| + } |
| + |
| + if (!fpsIsSupported) { |
| + Log.e(TAG, "allocate: fps " + frameRate + " is not supported"); |
| + return -1; |
| + } |
| + |
| + mCurrentCapability = new CaptureCapability(); |
| + mCurrentCapability.mDesiredFps = frameRate; |
| + |
| + // Calculate size. |
| + List<Camera.Size> listCameraSize = |
| + parameters.getSupportedPreviewSizes(); |
| + int minDiff = Integer.MAX_VALUE; |
| + int matchedWidth = width; |
| + int matchedHeight = height; |
| + Iterator itCameraSize = listCameraSize.iterator(); |
| + while (itCameraSize.hasNext()) { |
| + Camera.Size size = (Camera.Size)itCameraSize.next(); |
| + int diff = Math.abs(size.width - width) + |
| + Math.abs(size.height - height); |
| + Log.d(TAG, "allocate: support resolution (" + |
| + size.width + ", " + size.height + "), diff=" + diff); |
| + if (diff < minDiff) { |
| + minDiff = diff; |
| + matchedWidth = size.width; |
| + matchedHeight = size.height; |
| + } |
| + } |
| + mCurrentCapability.mWidth = matchedWidth; |
| + mCurrentCapability.mHeight = matchedHeight; |
| + Log.d(TAG, "allocate: matched width=" + matchedWidth + |
| + ", height=" + matchedHeight); |
| + |
| + parameters.setPreviewSize(matchedWidth, matchedHeight); |
| + parameters.setPreviewFormat(mPixelFormat); |
| + parameters.setPreviewFpsRange(fpsMin, fpsMax); |
| + mCamera.setParameters(parameters); |
| + |
| + // Set SurfaceTexture. |
| + 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); |
| + |
| + mCamera.setPreviewTexture(mSurfaceTexture); |
| + |
| + int bufSize = matchedWidth * matchedHeight * |
| + ImageFormat.getBitsPerPixel(mPixelFormat) / 8; |
| + for (int i = 0; i < NUM_CAPTURE_BUFFERS; i++) { |
| + byte[] buffer = new byte[bufSize]; |
| + mCamera.addCallbackBuffer(buffer); |
| + } |
| + mExpectedFrameSize = bufSize; |
| + } catch (IOException ex) { |
| + Log.e(TAG, "allocate: IOException"); |
| + return -1; |
| + } catch (RuntimeException ex) { |
| + Log.e(TAG, "allocate: RuntimeException"); |
| + return -1; |
| + } |
| + |
| + return 0; |
| + } |
| + |
| + @CalledByNative |
| + public int queryWidth() { |
| + return mCurrentCapability.mWidth; |
| + } |
| + |
| + @CalledByNative |
| + public int queryHeight() { |
| + return mCurrentCapability.mHeight; |
| + } |
| + |
| + @CalledByNative |
| + public int queryFrameRate() { |
| + return mCurrentCapability.mDesiredFps; |
| + } |
| + |
| + @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); |
| + mCamera.startPreview(); |
| + return 0; |
| + } |
| + |
| + @CalledByNative |
| + public int stopCapture() { |
| + if (mCamera == null) { |
| + Log.d(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); |
| + mSurfaceTexture.setOnFrameAvailableListener(null); |
| + GLES20.glDeleteTextures(1, mGlTextures, 0); |
| + mCurrentCapability = null; |
| + mCamera.release(); |
| + mCamera = null; |
| + } catch (Exception ex) { |
| + Log.e(TAG, "deallocate: failed to deallocate camera"); |
| + 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); |
| + } |
| + boolean flipVertical = false; |
| + boolean flipHorizontal = false; |
| + if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) { |
| + rotation = (mCameraOrientation + rotation) % 360; |
| + rotation = (360 - rotation) % 360; |
| + flipHorizontal = (rotation == 180 || rotation == 0); |
| + flipVertical = !flipHorizontal; |
| + } else { |
| + rotation = (mCameraOrientation - rotation + 360) % 360; |
| + } |
| + nativeOnFrameAvailable(mNativeVideoCaptureDeviceAndroid, |
| + data, mExpectedFrameSize, |
| + rotation, flipVertical, flipHorizontal); |
| + } |
| + } finally { |
| + mPreviewBufferLock.unlock(); |
| + if (camera != null) { |
| + camera.addCallbackBuffer(data); |
| + } |
| + } |
| + } |
| + |
| + // TODO(wjia): investigate whether reading from texture could give better |
| + // performance and frame rate. |
| + @Override |
| + public void onFrameAvailable(SurfaceTexture surfaceTexture) { } |
| + |
| + private native void nativeOnFrameAvailable(int nativeVideoCaptureDeviceAndroid, |
| + byte[] data, int length, int rotation, |
| + boolean flipVertical, boolean flipHorizontal); |
| + |
| + 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; |
| + } |
| +} |