VideoCapture.java revision 3551c9c881056c480085172ff9840cab31610854
1// Copyright (c) 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.media;
6
7import android.content.Context;
8import android.graphics.ImageFormat;
9import android.graphics.SurfaceTexture;
10import android.graphics.SurfaceTexture.OnFrameAvailableListener;
11import android.hardware.Camera;
12import android.hardware.Camera.PreviewCallback;
13import android.opengl.GLES20;
14import android.util.Log;
15import android.view.Surface;
16import android.view.WindowManager;
17
18import java.io.IOException;
19import java.util.concurrent.locks.ReentrantLock;
20import java.util.Iterator;
21import java.util.List;
22
23import org.chromium.base.CalledByNative;
24import org.chromium.base.JNINamespace;
25
26@JNINamespace("media")
27public class VideoCapture implements PreviewCallback, OnFrameAvailableListener {
28    static class CaptureCapability {
29        public int mWidth = 0;
30        public int mHeight = 0;
31        public int mDesiredFps = 0;
32    }
33
34    // Some devices with OS older than JELLY_BEAN don't support YV12 format correctly.
35    // Some devices don't support YV12 format correctly even with JELLY_BEAN or newer OS.
36    // To work around the issues on those devices, we'd have to request NV21.
37    // This is a temporary hack till device manufacturers fix the problem or
38    // we don't need to support those devices any more.
39    private static class DeviceImageFormatHack {
40        private static final String[] sBUGGY_DEVICE_LIST = {
41            "SAMSUNG-SGH-I747",
42        };
43
44        static int getImageFormat() {
45            if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) {
46                return ImageFormat.NV21;
47            }
48
49            for (String buggyDevice : sBUGGY_DEVICE_LIST) {
50                if (buggyDevice.contentEquals(android.os.Build.MODEL)) {
51                    return ImageFormat.NV21;
52                }
53            }
54
55            return ImageFormat.YV12;
56        }
57    }
58
59    private Camera mCamera;
60    public ReentrantLock mPreviewBufferLock = new ReentrantLock();
61    private int mImageFormat = ImageFormat.YV12;
62    private byte[] mColorPlane = null;
63    private Context mContext = null;
64    // True when native code has started capture.
65    private boolean mIsRunning = false;
66
67    private static final int NUM_CAPTURE_BUFFERS = 3;
68    private int mExpectedFrameSize = 0;
69    private int mId = 0;
70    // Native callback context variable.
71    private int mNativeVideoCaptureDeviceAndroid = 0;
72    private int[] mGlTextures = null;
73    private SurfaceTexture mSurfaceTexture = null;
74    private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;
75
76    private int mCameraOrientation = 0;
77    private int mCameraFacing = 0;
78    private int mDeviceOrientation = 0;
79
80    CaptureCapability mCurrentCapability = null;
81    private static final String TAG = "VideoCapture";
82
83    @CalledByNative
84    public static VideoCapture createVideoCapture(
85            Context context, int id, int nativeVideoCaptureDeviceAndroid) {
86        return new VideoCapture(context, id, nativeVideoCaptureDeviceAndroid);
87    }
88
89    public VideoCapture(
90            Context context, int id, int nativeVideoCaptureDeviceAndroid) {
91        mContext = context;
92        mId = id;
93        mNativeVideoCaptureDeviceAndroid = nativeVideoCaptureDeviceAndroid;
94    }
95
96    // Returns true on success, false otherwise.
97    @CalledByNative
98    public boolean allocate(int width, int height, int frameRate) {
99        Log.d(TAG, "allocate: requested width=" + width +
100              ", height=" + height + ", frameRate=" + frameRate);
101        try {
102            mCamera = Camera.open(mId);
103        } catch (RuntimeException ex) {
104            Log.e(TAG, "allocate:Camera.open: " + ex);
105            return false;
106        }
107
108        try {
109            Camera.CameraInfo camera_info = new Camera.CameraInfo();
110            Camera.getCameraInfo(mId, camera_info);
111            mCameraOrientation = camera_info.orientation;
112            mCameraFacing = camera_info.facing;
113            mDeviceOrientation = getDeviceOrientation();
114            Log.d(TAG, "allocate: device orientation=" + mDeviceOrientation +
115                  ", camera orientation=" + mCameraOrientation +
116                  ", facing=" + mCameraFacing);
117
118            Camera.Parameters parameters = mCamera.getParameters();
119
120            // Calculate fps.
121            List<int[]> listFpsRange = parameters.getSupportedPreviewFpsRange();
122            if (listFpsRange.size() == 0) {
123                Log.e(TAG, "allocate: no fps range found");
124                return false;
125            }
126            int frameRateInMs = frameRate * 1000;
127            Iterator itFpsRange = listFpsRange.iterator();
128            int[] fpsRange = (int[])itFpsRange.next();
129            // Use the first range as default.
130            int fpsMin = fpsRange[0];
131            int fpsMax = fpsRange[1];
132            int newFrameRate = (fpsMin + 999) / 1000;
133            while (itFpsRange.hasNext()) {
134                fpsRange = (int[])itFpsRange.next();
135                if (fpsRange[0] <= frameRateInMs &&
136                    frameRateInMs <= fpsRange[1]) {
137                    fpsMin = fpsRange[0];
138                    fpsMax = fpsRange[1];
139                    newFrameRate = frameRate;
140                    break;
141                }
142            }
143            frameRate = newFrameRate;
144            Log.d(TAG, "allocate: fps set to " + frameRate);
145
146            mCurrentCapability = new CaptureCapability();
147            mCurrentCapability.mDesiredFps = frameRate;
148
149            // Calculate size.
150            List<Camera.Size> listCameraSize =
151                    parameters.getSupportedPreviewSizes();
152            int minDiff = Integer.MAX_VALUE;
153            int matchedWidth = width;
154            int matchedHeight = height;
155            Iterator itCameraSize = listCameraSize.iterator();
156            while (itCameraSize.hasNext()) {
157                Camera.Size size = (Camera.Size)itCameraSize.next();
158                int diff = Math.abs(size.width - width) +
159                           Math.abs(size.height - height);
160                Log.d(TAG, "allocate: support resolution (" +
161                      size.width + ", " + size.height + "), diff=" + diff);
162                // TODO(wjia): Remove this hack (forcing width to be multiple
163                // of 32) by supporting stride in video frame buffer.
164                // Right now, VideoCaptureController requires compact YV12
165                // (i.e., with no padding).
166                if (diff < minDiff && (size.width % 32 == 0)) {
167                    minDiff = diff;
168                    matchedWidth = size.width;
169                    matchedHeight = size.height;
170                }
171            }
172            if (minDiff == Integer.MAX_VALUE) {
173                Log.e(TAG, "allocate: can not find a resolution whose width " +
174                           "is multiple of 32");
175                return false;
176            }
177            mCurrentCapability.mWidth = matchedWidth;
178            mCurrentCapability.mHeight = matchedHeight;
179            Log.d(TAG, "allocate: matched width=" + matchedWidth +
180                  ", height=" + matchedHeight);
181
182            calculateImageFormat(matchedWidth, matchedHeight);
183
184            parameters.setPreviewSize(matchedWidth, matchedHeight);
185            parameters.setPreviewFormat(mImageFormat);
186            parameters.setPreviewFpsRange(fpsMin, fpsMax);
187            mCamera.setParameters(parameters);
188
189            // Set SurfaceTexture.
190            mGlTextures = new int[1];
191            // Generate one texture pointer and bind it as an external texture.
192            GLES20.glGenTextures(1, mGlTextures, 0);
193            GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, mGlTextures[0]);
194            // No mip-mapping with camera source.
195            GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES,
196                    GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
197            GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES,
198                    GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
199            // Clamp to edge is only option.
200            GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES,
201                    GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
202            GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES,
203                    GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
204
205            mSurfaceTexture = new SurfaceTexture(mGlTextures[0]);
206            mSurfaceTexture.setOnFrameAvailableListener(null);
207
208            mCamera.setPreviewTexture(mSurfaceTexture);
209
210            int bufSize = matchedWidth * matchedHeight *
211                          ImageFormat.getBitsPerPixel(mImageFormat) / 8;
212            for (int i = 0; i < NUM_CAPTURE_BUFFERS; i++) {
213                byte[] buffer = new byte[bufSize];
214                mCamera.addCallbackBuffer(buffer);
215            }
216            mExpectedFrameSize = bufSize;
217        } catch (IOException ex) {
218            Log.e(TAG, "allocate: " + ex);
219            return false;
220        }
221
222        return true;
223    }
224
225    @CalledByNative
226    public int queryWidth() {
227        return mCurrentCapability.mWidth;
228    }
229
230    @CalledByNative
231    public int queryHeight() {
232        return mCurrentCapability.mHeight;
233    }
234
235    @CalledByNative
236    public int queryFrameRate() {
237        return mCurrentCapability.mDesiredFps;
238    }
239
240    @CalledByNative
241    public int startCapture() {
242        if (mCamera == null) {
243            Log.e(TAG, "startCapture: camera is null");
244            return -1;
245        }
246
247        mPreviewBufferLock.lock();
248        try {
249            if (mIsRunning) {
250                return 0;
251            }
252            mIsRunning = true;
253        } finally {
254            mPreviewBufferLock.unlock();
255        }
256        mCamera.setPreviewCallbackWithBuffer(this);
257        mCamera.startPreview();
258        return 0;
259    }
260
261    @CalledByNative
262    public int stopCapture() {
263        if (mCamera == null) {
264            Log.e(TAG, "stopCapture: camera is null");
265            return 0;
266        }
267
268        mPreviewBufferLock.lock();
269        try {
270            if (!mIsRunning) {
271                return 0;
272            }
273            mIsRunning = false;
274        } finally {
275            mPreviewBufferLock.unlock();
276        }
277
278        mCamera.stopPreview();
279        mCamera.setPreviewCallbackWithBuffer(null);
280        return 0;
281    }
282
283    @CalledByNative
284    public void deallocate() {
285        if (mCamera == null)
286            return;
287
288        stopCapture();
289        try {
290            mCamera.setPreviewTexture(null);
291            if (mGlTextures != null)
292                GLES20.glDeleteTextures(1, mGlTextures, 0);
293            mCurrentCapability = null;
294            mCamera.release();
295            mCamera = null;
296        } catch (IOException ex) {
297            Log.e(TAG, "deallocate: failed to deallocate camera, " + ex);
298            return;
299        }
300    }
301
302    @Override
303    public void onPreviewFrame(byte[] data, Camera camera) {
304        mPreviewBufferLock.lock();
305        try {
306            if (!mIsRunning) {
307                return;
308            }
309            if (data.length == mExpectedFrameSize) {
310                int rotation = getDeviceOrientation();
311                if (rotation != mDeviceOrientation) {
312                    mDeviceOrientation = rotation;
313                    Log.d(TAG,
314                          "onPreviewFrame: device orientation=" +
315                          mDeviceOrientation + ", camera orientation=" +
316                          mCameraOrientation);
317                }
318                boolean flipVertical = false;
319                boolean flipHorizontal = false;
320                if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
321                    rotation = (mCameraOrientation + rotation) % 360;
322                    rotation = (360 - rotation) % 360;
323                    flipHorizontal = (rotation == 270 || rotation == 90);
324                    flipVertical = flipHorizontal;
325                } else {
326                    rotation = (mCameraOrientation - rotation + 360) % 360;
327                }
328                if (mImageFormat == ImageFormat.NV21) {
329                    convertNV21ToYV12(data);
330                }
331                nativeOnFrameAvailable(mNativeVideoCaptureDeviceAndroid,
332                        data, mExpectedFrameSize,
333                        rotation, flipVertical, flipHorizontal);
334            }
335        } finally {
336            mPreviewBufferLock.unlock();
337            if (camera != null) {
338                camera.addCallbackBuffer(data);
339            }
340        }
341    }
342
343    // TODO(wjia): investigate whether reading from texture could give better
344    // performance and frame rate.
345    @Override
346    public void onFrameAvailable(SurfaceTexture surfaceTexture) { }
347
348    private static class ChromiumCameraInfo {
349        private final int mId;
350        private final Camera.CameraInfo mCameraInfo;
351
352        private ChromiumCameraInfo(int index) {
353            mId = index;
354            mCameraInfo = new Camera.CameraInfo();
355            Camera.getCameraInfo(index, mCameraInfo);
356        }
357
358        @CalledByNative("ChromiumCameraInfo")
359        private static int getNumberOfCameras() {
360            return Camera.getNumberOfCameras();
361        }
362
363        @CalledByNative("ChromiumCameraInfo")
364        private static ChromiumCameraInfo getAt(int index) {
365            return new ChromiumCameraInfo(index);
366        }
367
368        @CalledByNative("ChromiumCameraInfo")
369        private int getId() {
370            return mId;
371        }
372
373        @CalledByNative("ChromiumCameraInfo")
374        private String getDeviceName() {
375            return  "camera " + mId + ", facing " +
376                    (mCameraInfo.facing ==
377                     Camera.CameraInfo.CAMERA_FACING_FRONT ? "front" : "back");
378        }
379
380        @CalledByNative("ChromiumCameraInfo")
381        private int getOrientation() {
382            return mCameraInfo.orientation;
383        }
384    }
385
386    private native void nativeOnFrameAvailable(
387            int nativeVideoCaptureDeviceAndroid,
388            byte[] data,
389            int length,
390            int rotation,
391            boolean flipVertical,
392            boolean flipHorizontal);
393
394    private int getDeviceOrientation() {
395        int orientation = 0;
396        if (mContext != null) {
397            WindowManager wm = (WindowManager)mContext.getSystemService(
398                    Context.WINDOW_SERVICE);
399            switch(wm.getDefaultDisplay().getRotation()) {
400                case Surface.ROTATION_90:
401                    orientation = 90;
402                    break;
403                case Surface.ROTATION_180:
404                    orientation = 180;
405                    break;
406                case Surface.ROTATION_270:
407                    orientation = 270;
408                    break;
409                case Surface.ROTATION_0:
410                default:
411                    orientation = 0;
412                    break;
413            }
414        }
415        return orientation;
416    }
417
418    private void calculateImageFormat(int width, int height) {
419        mImageFormat = DeviceImageFormatHack.getImageFormat();
420        if (mImageFormat == ImageFormat.NV21) {
421            mColorPlane = new byte[width * height / 4];
422        }
423    }
424
425    private void convertNV21ToYV12(byte[] data) {
426        final int ySize = mCurrentCapability.mWidth * mCurrentCapability.mHeight;
427        final int uvSize = ySize / 4;
428        for (int i = 0; i < uvSize; i++) {
429            final int index = ySize + i * 2;
430            data[ySize + i] = data[index];
431            mColorPlane[i] = data[index + 1];
432        }
433        System.arraycopy(mColorPlane, 0, data, ySize + uvSize, uvSize);
434    }
435}
436