Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / media / base / android / java / src / org / chromium / media / VideoCapture.java
1 // Copyright 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
5 package org.chromium.media;
6
7 import android.content.Context;
8 import android.graphics.ImageFormat;
9 import android.graphics.SurfaceTexture;
10 import android.hardware.Camera;
11 import android.hardware.Camera.PreviewCallback;
12 import android.hardware.Camera.Size;
13 import android.opengl.GLES20;
14 import android.util.Log;
15 import android.view.Surface;
16 import android.view.WindowManager;
17
18 import org.chromium.base.CalledByNative;
19 import org.chromium.base.JNINamespace;
20
21 import java.io.IOException;
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.concurrent.locks.ReentrantLock;
25
26 /** This class implements the listener interface for receiving copies of preview
27  * frames from the camera, plus a series of methods to manipulate camera and its
28  * capture from the C++ side. Objects of this class are created via
29  * createVideoCapture() and are explicitly owned by the creator. All methods
30  * are invoked by this owner, including the callback OnPreviewFrame().
31  **/
32 @JNINamespace("media")
33 public class VideoCapture implements PreviewCallback {
34     static class CaptureFormat {
35         public CaptureFormat(
36                 int width, int height, int framerate, int pixelformat) {
37             mWidth = width;
38             mHeight = height;
39             mFramerate = framerate;
40             mPixelFormat = pixelformat;
41         }
42         public int mWidth;
43         public int mHeight;
44         public final int mFramerate;
45         public final int mPixelFormat;
46         @CalledByNative("CaptureFormat")
47         public int getWidth() {
48             return mWidth;
49         }
50         @CalledByNative("CaptureFormat")
51         public int getHeight() {
52             return mHeight;
53         }
54         @CalledByNative("CaptureFormat")
55         public int getFramerate() {
56             return mFramerate;
57         }
58         @CalledByNative("CaptureFormat")
59         public int getPixelFormat() {
60             return mPixelFormat;
61         }
62     }
63
64     // Some devices don't support YV12 format correctly, even with JELLY_BEAN or
65     // newer OS. To work around the issues on those devices, we have to request
66     // NV21. Some other devices have troubles with certain capture resolutions
67     // under a given one: for those, the resolution is swapped with a known
68     // good. Both are supposed to be temporary hacks.
69     private static class BuggyDeviceHack {
70         private static class IdAndSizes {
71             IdAndSizes(String model, String device, int minWidth, int minHeight) {
72                 mModel = model;
73                 mDevice = device;
74                 mMinWidth = minWidth;
75                 mMinHeight = minHeight;
76             }
77             public final String mModel;
78             public final String mDevice;
79             public final int mMinWidth;
80             public final int mMinHeight;
81         }
82         private static final IdAndSizes s_CAPTURESIZE_BUGGY_DEVICE_LIST[] = {
83             new IdAndSizes("Nexus 7", "flo", 640, 480)
84         };
85
86         private static final String[] s_COLORSPACE_BUGGY_DEVICE_LIST = {
87             "SAMSUNG-SGH-I747",
88             "ODROID-U2",
89         };
90
91         static void applyMinDimensions(CaptureFormat format) {
92             // NOTE: this can discard requested aspect ratio considerations.
93             for (IdAndSizes buggyDevice : s_CAPTURESIZE_BUGGY_DEVICE_LIST) {
94                 if (buggyDevice.mModel.contentEquals(android.os.Build.MODEL) &&
95                         buggyDevice.mDevice.contentEquals(android.os.Build.DEVICE)) {
96                     format.mWidth = (buggyDevice.mMinWidth > format.mWidth)
97                                         ? buggyDevice.mMinWidth
98                                         : format.mWidth;
99                     format.mHeight = (buggyDevice.mMinHeight > format.mHeight)
100                                          ? buggyDevice.mMinHeight
101                                          : format.mHeight;
102                 }
103             }
104         }
105
106         static int getImageFormat() {
107             if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) {
108                 return ImageFormat.NV21;
109             }
110
111             for (String buggyDevice : s_COLORSPACE_BUGGY_DEVICE_LIST) {
112                 if (buggyDevice.contentEquals(android.os.Build.MODEL)) {
113                     return ImageFormat.NV21;
114                 }
115             }
116             return ImageFormat.YV12;
117         }
118     }
119
120     private Camera mCamera;
121     public ReentrantLock mPreviewBufferLock = new ReentrantLock();
122     private Context mContext = null;
123     // True when native code has started capture.
124     private boolean mIsRunning = false;
125
126     private static final int NUM_CAPTURE_BUFFERS = 3;
127     private int mExpectedFrameSize = 0;
128     private int mId = 0;
129     // Native callback context variable.
130     private long mNativeVideoCaptureDeviceAndroid = 0;
131     private int[] mGlTextures = null;
132     private SurfaceTexture mSurfaceTexture = null;
133     private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;
134
135     private int mCameraOrientation = 0;
136     private int mCameraFacing = 0;
137     private int mDeviceOrientation = 0;
138
139     CaptureFormat mCaptureFormat = null;
140     private static final String TAG = "VideoCapture";
141
142     @CalledByNative
143     public static VideoCapture createVideoCapture(
144             Context context, int id, long nativeVideoCaptureDeviceAndroid) {
145         return new VideoCapture(context, id, nativeVideoCaptureDeviceAndroid);
146     }
147
148     @CalledByNative
149     public static CaptureFormat[] getDeviceSupportedFormats(int id) {
150         Camera camera;
151         try {
152              camera = Camera.open(id);
153         } catch (RuntimeException ex) {
154             Log.e(TAG, "Camera.open: " + ex);
155             return null;
156         }
157         Camera.Parameters parameters = camera.getParameters();
158
159         ArrayList<CaptureFormat> formatList = new ArrayList<CaptureFormat>();
160         // getSupportedPreview{Formats,FpsRange,PreviewSizes}() returns Lists
161         // with at least one element, but when the camera is in bad state, they
162         // can return null pointers; in that case we use a 0 entry, so we can
163         // retrieve as much information as possible.
164         List<Integer> pixelFormats = parameters.getSupportedPreviewFormats();
165         if (pixelFormats == null) {
166             pixelFormats = new ArrayList<Integer>();
167         }
168         if (pixelFormats.size() == 0) {
169             pixelFormats.add(ImageFormat.UNKNOWN);
170         }
171         for (Integer previewFormat : pixelFormats) {
172             int pixelFormat =
173                     AndroidImageFormatList.ANDROID_IMAGEFORMAT_UNKNOWN;
174             if (previewFormat == ImageFormat.YV12) {
175                 pixelFormat = AndroidImageFormatList.ANDROID_IMAGEFORMAT_YV12;
176             } else if (previewFormat == ImageFormat.NV21) {
177                 continue;
178             }
179
180             List<int[]> listFpsRange = parameters.getSupportedPreviewFpsRange();
181             if (listFpsRange == null) {
182                 listFpsRange = new ArrayList<int[]>();
183             }
184             if (listFpsRange.size() == 0) {
185                 listFpsRange.add(new int[] {0, 0});
186             }
187             for (int[] fpsRange : listFpsRange) {
188                 List<Camera.Size> supportedSizes =
189                         parameters.getSupportedPreviewSizes();
190                 if (supportedSizes == null) {
191                     supportedSizes = new ArrayList<Camera.Size>();
192                 }
193                 if (supportedSizes.size() == 0) {
194                     supportedSizes.add(camera.new Size(0, 0));
195                 }
196                 for (Camera.Size size : supportedSizes) {
197                     formatList.add(new CaptureFormat(size.width, size.height,
198                             (fpsRange[0] + 999 ) / 1000, pixelFormat));
199                 }
200             }
201         }
202         camera.release();
203         return formatList.toArray(new CaptureFormat[formatList.size()]);
204     }
205
206     public VideoCapture(
207             Context context, int id, long nativeVideoCaptureDeviceAndroid) {
208         mContext = context;
209         mId = id;
210         mNativeVideoCaptureDeviceAndroid = nativeVideoCaptureDeviceAndroid;
211     }
212
213     // Returns true on success, false otherwise.
214     @CalledByNative
215     public boolean allocate(int width, int height, int frameRate) {
216         Log.d(TAG, "allocate: requested (" + width + "x" + height + ")@" +
217                  frameRate + "fps");
218         try {
219             mCamera = Camera.open(mId);
220         } catch (RuntimeException ex) {
221             Log.e(TAG, "allocate: Camera.open: " + ex);
222             return false;
223         }
224
225         Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
226         Camera.getCameraInfo(mId, cameraInfo);
227         mCameraOrientation = cameraInfo.orientation;
228         mCameraFacing = cameraInfo.facing;
229         mDeviceOrientation = getDeviceOrientation();
230         Log.d(TAG, "allocate: orientation dev=" + mDeviceOrientation +
231                   ", cam=" + mCameraOrientation + ", facing=" + mCameraFacing);
232
233         Camera.Parameters parameters = mCamera.getParameters();
234
235         // getSupportedPreviewFpsRange() returns a List with at least one
236         // element, but when camera is in bad state, it can return null pointer.
237         List<int[]> listFpsRange = parameters.getSupportedPreviewFpsRange();
238         if (listFpsRange == null || listFpsRange.size() == 0) {
239             Log.e(TAG, "allocate: no fps range found");
240             return false;
241         }
242         int frameRateInMs = frameRate * 1000;
243         // Use the first range as default.
244         int[] fpsMinMax = listFpsRange.get(0);
245         int newFrameRate = (fpsMinMax[0] + 999) / 1000;
246         for (int[] fpsRange : listFpsRange) {
247             if (fpsRange[0] <= frameRateInMs && frameRateInMs <= fpsRange[1]) {
248                 fpsMinMax = fpsRange;
249                 newFrameRate = frameRate;
250                 break;
251             }
252         }
253         frameRate = newFrameRate;
254         Log.d(TAG, "allocate: fps set to " + frameRate);
255
256         // Calculate size.
257         List<Camera.Size> listCameraSize =
258                 parameters.getSupportedPreviewSizes();
259         int minDiff = Integer.MAX_VALUE;
260         int matchedWidth = width;
261         int matchedHeight = height;
262         for (Camera.Size size : listCameraSize) {
263             int diff = Math.abs(size.width - width) +
264                        Math.abs(size.height - height);
265             Log.d(TAG, "allocate: supported (" +
266                   size.width + ", " + size.height + "), diff=" + diff);
267             // TODO(wjia): Remove this hack (forcing width to be multiple
268             // of 32) by supporting stride in video frame buffer.
269             // Right now, VideoCaptureController requires compact YV12
270             // (i.e., with no padding).
271             if (diff < minDiff && (size.width % 32 == 0)) {
272                 minDiff = diff;
273                 matchedWidth = size.width;
274                 matchedHeight = size.height;
275             }
276         }
277         if (minDiff == Integer.MAX_VALUE) {
278             Log.e(TAG, "allocate: can not find a multiple-of-32 resolution");
279             return false;
280         }
281
282         mCaptureFormat = new CaptureFormat(
283                 matchedWidth, matchedHeight, frameRate,
284                 BuggyDeviceHack.getImageFormat());
285         // Hack to avoid certain capture resolutions under a minimum one,
286         // see http://crbug.com/305294
287         BuggyDeviceHack.applyMinDimensions(mCaptureFormat);
288         Log.d(TAG, "allocate: matched (" + mCaptureFormat.mWidth + "x" +
289                 mCaptureFormat.mHeight + ")");
290
291         if (parameters.isVideoStabilizationSupported()) {
292             Log.d(TAG, "Image stabilization supported, currently: "
293                   + parameters.getVideoStabilization() + ", setting it.");
294             parameters.setVideoStabilization(true);
295         } else {
296             Log.d(TAG, "Image stabilization not supported.");
297         }
298         parameters.setPreviewSize(mCaptureFormat.mWidth,
299                                   mCaptureFormat.mHeight);
300         parameters.setPreviewFormat(mCaptureFormat.mPixelFormat);
301         parameters.setPreviewFpsRange(fpsMinMax[0], fpsMinMax[1]);
302         mCamera.setParameters(parameters);
303
304         // Set SurfaceTexture. Android Capture needs a SurfaceTexture even if
305         // it is not going to be used.
306         mGlTextures = new int[1];
307         // Generate one texture pointer and bind it as an external texture.
308         GLES20.glGenTextures(1, mGlTextures, 0);
309         GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, mGlTextures[0]);
310         // No mip-mapping with camera source.
311         GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES,
312                 GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
313         GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES,
314                 GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
315         // Clamp to edge is only option.
316         GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES,
317                 GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
318         GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES,
319                 GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
320
321         mSurfaceTexture = new SurfaceTexture(mGlTextures[0]);
322         mSurfaceTexture.setOnFrameAvailableListener(null);
323
324         try {
325             mCamera.setPreviewTexture(mSurfaceTexture);
326         } catch (IOException ex) {
327             Log.e(TAG, "allocate: " + ex);
328             return false;
329         }
330
331         int bufSize = mCaptureFormat.mWidth *
332                       mCaptureFormat.mHeight *
333                       ImageFormat.getBitsPerPixel(
334                               mCaptureFormat.mPixelFormat) / 8;
335         for (int i = 0; i < NUM_CAPTURE_BUFFERS; i++) {
336             byte[] buffer = new byte[bufSize];
337             mCamera.addCallbackBuffer(buffer);
338         }
339         mExpectedFrameSize = bufSize;
340
341         return true;
342     }
343
344     @CalledByNative
345     public int queryWidth() {
346         return mCaptureFormat.mWidth;
347     }
348
349     @CalledByNative
350     public int queryHeight() {
351         return mCaptureFormat.mHeight;
352     }
353
354     @CalledByNative
355     public int queryFrameRate() {
356         return mCaptureFormat.mFramerate;
357     }
358
359     @CalledByNative
360     public int getColorspace() {
361         switch (mCaptureFormat.mPixelFormat) {
362             case ImageFormat.YV12:
363                 return AndroidImageFormatList.ANDROID_IMAGEFORMAT_YV12;
364             case ImageFormat.NV21:
365                 return AndroidImageFormatList.ANDROID_IMAGEFORMAT_NV21;
366             case ImageFormat.UNKNOWN:
367             default:
368                 return AndroidImageFormatList.ANDROID_IMAGEFORMAT_UNKNOWN;
369         }
370     }
371
372     @CalledByNative
373     public int startCapture() {
374         if (mCamera == null) {
375             Log.e(TAG, "startCapture: camera is null");
376             return -1;
377         }
378
379         mPreviewBufferLock.lock();
380         try {
381             if (mIsRunning) {
382                 return 0;
383             }
384             mIsRunning = true;
385         } finally {
386             mPreviewBufferLock.unlock();
387         }
388         mCamera.setPreviewCallbackWithBuffer(this);
389         mCamera.startPreview();
390         return 0;
391     }
392
393     @CalledByNative
394     public int stopCapture() {
395         if (mCamera == null) {
396             Log.e(TAG, "stopCapture: camera is null");
397             return 0;
398         }
399
400         mPreviewBufferLock.lock();
401         try {
402             if (!mIsRunning) {
403                 return 0;
404             }
405             mIsRunning = false;
406         } finally {
407             mPreviewBufferLock.unlock();
408         }
409
410         mCamera.stopPreview();
411         mCamera.setPreviewCallbackWithBuffer(null);
412         return 0;
413     }
414
415     @CalledByNative
416     public void deallocate() {
417         if (mCamera == null)
418             return;
419
420         stopCapture();
421         try {
422             mCamera.setPreviewTexture(null);
423             if (mGlTextures != null)
424                 GLES20.glDeleteTextures(1, mGlTextures, 0);
425             mCaptureFormat = null;
426             mCamera.release();
427             mCamera = null;
428         } catch (IOException ex) {
429             Log.e(TAG, "deallocate: failed to deallocate camera, " + ex);
430             return;
431         }
432     }
433
434     @Override
435     public void onPreviewFrame(byte[] data, Camera camera) {
436         mPreviewBufferLock.lock();
437         try {
438             if (!mIsRunning) {
439                 return;
440             }
441             if (data.length == mExpectedFrameSize) {
442                 int rotation = getDeviceOrientation();
443                 if (rotation != mDeviceOrientation) {
444                     mDeviceOrientation = rotation;
445                     Log.d(TAG,
446                           "onPreviewFrame: device orientation=" +
447                           mDeviceOrientation + ", camera orientation=" +
448                           mCameraOrientation);
449                 }
450                 if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_BACK) {
451                     rotation = 360 - rotation;
452                 }
453                 rotation = (mCameraOrientation + rotation) % 360;
454                 nativeOnFrameAvailable(mNativeVideoCaptureDeviceAndroid,
455                         data, mExpectedFrameSize, rotation);
456             }
457         } finally {
458             mPreviewBufferLock.unlock();
459             if (camera != null) {
460                 camera.addCallbackBuffer(data);
461             }
462         }
463     }
464
465     // TODO(wjia): investigate whether reading from texture could give better
466     // performance and frame rate, using onFrameAvailable().
467
468     private static class ChromiumCameraInfo {
469         private final int mId;
470         private final Camera.CameraInfo mCameraInfo;
471
472         private ChromiumCameraInfo(int index) {
473             mId = index;
474             mCameraInfo = new Camera.CameraInfo();
475             Camera.getCameraInfo(index, mCameraInfo);
476         }
477
478         @CalledByNative("ChromiumCameraInfo")
479         private static int getNumberOfCameras() {
480             return Camera.getNumberOfCameras();
481         }
482
483         @CalledByNative("ChromiumCameraInfo")
484         private static ChromiumCameraInfo getAt(int index) {
485             return new ChromiumCameraInfo(index);
486         }
487
488         @CalledByNative("ChromiumCameraInfo")
489         private int getId() {
490             return mId;
491         }
492
493         @CalledByNative("ChromiumCameraInfo")
494         private String getDeviceName() {
495             return  "camera " + mId + ", facing " +
496                     (mCameraInfo.facing ==
497                      Camera.CameraInfo.CAMERA_FACING_FRONT ? "front" : "back");
498         }
499
500         @CalledByNative("ChromiumCameraInfo")
501         private int getOrientation() {
502             return mCameraInfo.orientation;
503         }
504     }
505
506     private native void nativeOnFrameAvailable(
507             long nativeVideoCaptureDeviceAndroid,
508             byte[] data,
509             int length,
510             int rotation);
511
512     private int getDeviceOrientation() {
513         int orientation = 0;
514         if (mContext != null) {
515             WindowManager wm = (WindowManager) mContext.getSystemService(
516                     Context.WINDOW_SERVICE);
517             switch(wm.getDefaultDisplay().getRotation()) {
518                 case Surface.ROTATION_90:
519                     orientation = 90;
520                     break;
521                 case Surface.ROTATION_180:
522                     orientation = 180;
523                     break;
524                 case Surface.ROTATION_270:
525                     orientation = 270;
526                     break;
527                 case Surface.ROTATION_0:
528                 default:
529                     orientation = 0;
530                     break;
531             }
532         }
533         return orientation;
534     }
535 }