1// Copyright 2014 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.opengl.GLES20;
11import android.util.Log;
12import android.view.Surface;
13import android.view.WindowManager;
14
15import org.chromium.base.CalledByNative;
16import org.chromium.base.JNINamespace;
17
18import java.io.IOException;
19import java.util.List;
20import java.util.concurrent.locks.ReentrantLock;
21
22/**
23 * Video Capture Device base class to interface to native Chromium.
24 **/
25@JNINamespace("media")
26@SuppressWarnings("deprecation")
27public abstract class VideoCapture implements android.hardware.Camera.PreviewCallback {
28
29    protected static class CaptureFormat {
30        int mWidth;
31        int mHeight;
32        final int mFramerate;
33        final int mPixelFormat;
34
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
43        public int getWidth() {
44            return mWidth;
45        }
46
47        public int getHeight() {
48            return mHeight;
49        }
50
51        public int getFramerate() {
52            return mFramerate;
53        }
54
55        public int getPixelFormat() {
56            return mPixelFormat;
57        }
58    }
59
60    protected android.hardware.Camera mCamera;
61    protected CaptureFormat mCaptureFormat = null;
62    // Lock to mutually exclude execution of OnPreviewFrame {start/stop}Capture.
63    protected ReentrantLock mPreviewBufferLock = new ReentrantLock();
64    protected Context mContext = null;
65    // True when native code has started capture.
66    protected boolean mIsRunning = false;
67
68    protected int mId;
69    // Native callback context variable.
70    protected long mNativeVideoCaptureDeviceAndroid;
71    protected int[] mGlTextures = null;
72    protected SurfaceTexture mSurfaceTexture = null;
73    protected static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;
74
75    protected int mCameraOrientation;
76    protected int mCameraFacing;
77    protected int mDeviceOrientation;
78    private static final String TAG = "VideoCapture";
79
80    VideoCapture(Context context,
81                 int id,
82                 long nativeVideoCaptureDeviceAndroid) {
83        mContext = context;
84        mId = id;
85        mNativeVideoCaptureDeviceAndroid = nativeVideoCaptureDeviceAndroid;
86    }
87
88    @CalledByNative
89    boolean allocate(int width, int height, int frameRate) {
90        Log.d(TAG, "allocate: requested (" + width + "x" + height + ")@" +
91                frameRate + "fps");
92        try {
93            mCamera = android.hardware.Camera.open(mId);
94        } catch (RuntimeException ex) {
95            Log.e(TAG, "allocate: Camera.open: " + ex);
96            return false;
97        }
98
99        android.hardware.Camera.CameraInfo cameraInfo = getCameraInfo(mId);
100        if (cameraInfo == null) {
101            mCamera.release();
102            mCamera = null;
103            return false;
104        }
105
106        mCameraOrientation = cameraInfo.orientation;
107        mCameraFacing = cameraInfo.facing;
108        mDeviceOrientation = getDeviceOrientation();
109        Log.d(TAG, "allocate: orientation dev=" + mDeviceOrientation +
110                  ", cam=" + mCameraOrientation + ", facing=" + mCameraFacing);
111
112        android.hardware.Camera.Parameters parameters = getCameraParameters(mCamera);
113        if (parameters == null) {
114            mCamera = null;
115            return false;
116        }
117
118        // getSupportedPreviewFpsRange() returns a List with at least one
119        // element, but when camera is in bad state, it can return null pointer.
120        List<int[]> listFpsRange = parameters.getSupportedPreviewFpsRange();
121        if (listFpsRange == null || listFpsRange.size() == 0) {
122            Log.e(TAG, "allocate: no fps range found");
123            return false;
124        }
125        int frameRateInMs = frameRate * 1000;
126        // Use the first range as default.
127        int[] fpsMinMax = listFpsRange.get(0);
128        int newFrameRate = (fpsMinMax[0] + 999) / 1000;
129        for (int[] fpsRange : listFpsRange) {
130            if (fpsRange[0] <= frameRateInMs && frameRateInMs <= fpsRange[1]) {
131                fpsMinMax = fpsRange;
132                newFrameRate = frameRate;
133                break;
134            }
135        }
136        frameRate = newFrameRate;
137        Log.d(TAG, "allocate: fps set to " + frameRate);
138
139        // Calculate size.
140        List<android.hardware.Camera.Size> listCameraSize =
141                parameters.getSupportedPreviewSizes();
142        int minDiff = Integer.MAX_VALUE;
143        int matchedWidth = width;
144        int matchedHeight = height;
145        for (android.hardware.Camera.Size size : listCameraSize) {
146            int diff = Math.abs(size.width - width) +
147                       Math.abs(size.height - height);
148            Log.d(TAG, "allocate: supported (" +
149                  size.width + ", " + size.height + "), diff=" + diff);
150            // TODO(wjia): Remove this hack (forcing width to be multiple
151            // of 32) by supporting stride in video frame buffer.
152            // Right now, VideoCaptureController requires compact YV12
153            // (i.e., with no padding).
154            if (diff < minDiff && (size.width % 32 == 0)) {
155                minDiff = diff;
156                matchedWidth = size.width;
157                matchedHeight = size.height;
158            }
159        }
160        if (minDiff == Integer.MAX_VALUE) {
161            Log.e(TAG, "allocate: can not find a multiple-of-32 resolution");
162            return false;
163        }
164        Log.d(TAG, "allocate: matched (" + matchedWidth + "x" + matchedHeight + ")");
165
166        if (parameters.isVideoStabilizationSupported()) {
167            Log.d(TAG, "Image stabilization supported, currently: " +
168                  parameters.getVideoStabilization() + ", setting it.");
169            parameters.setVideoStabilization(true);
170        } else {
171            Log.d(TAG, "Image stabilization not supported.");
172        }
173
174        setCaptureParameters(matchedWidth, matchedHeight, frameRate, parameters);
175        parameters.setPreviewSize(mCaptureFormat.mWidth,
176                                  mCaptureFormat.mHeight);
177        parameters.setPreviewFpsRange(fpsMinMax[0], fpsMinMax[1]);
178        parameters.setPreviewFormat(mCaptureFormat.mPixelFormat);
179        try {
180            mCamera.setParameters(parameters);
181        } catch (RuntimeException ex) {
182            Log.e(TAG, "setParameters: " + ex);
183            return false;
184        }
185
186        // Set SurfaceTexture. Android Capture needs a SurfaceTexture even if
187        // it is not going to be used.
188        mGlTextures = new int[1];
189        // Generate one texture pointer and bind it as an external texture.
190        GLES20.glGenTextures(1, mGlTextures, 0);
191        GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, mGlTextures[0]);
192        // No mip-mapping with camera source.
193        GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES,
194                GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
195        GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES,
196                GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
197        // Clamp to edge is only option.
198        GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES,
199                GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
200        GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES,
201                GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
202
203        mSurfaceTexture = new SurfaceTexture(mGlTextures[0]);
204        mSurfaceTexture.setOnFrameAvailableListener(null);
205        try {
206            mCamera.setPreviewTexture(mSurfaceTexture);
207        } catch (IOException ex) {
208            Log.e(TAG, "allocate: " + ex);
209            return false;
210        }
211
212        allocateBuffers();
213        return true;
214    }
215
216    @CalledByNative
217    public int startCapture() {
218        if (mCamera == null) {
219            Log.e(TAG, "startCapture: camera is null");
220            return -1;
221        }
222
223        mPreviewBufferLock.lock();
224        try {
225            if (mIsRunning) {
226                return 0;
227            }
228            mIsRunning = true;
229        } finally {
230            mPreviewBufferLock.unlock();
231        }
232        setPreviewCallback(this);
233        try {
234            mCamera.startPreview();
235        } catch (RuntimeException ex) {
236            Log.e(TAG, "startCapture: Camera.startPreview: " + ex);
237            return -1;
238        }
239        return 0;
240    }
241
242    @CalledByNative
243    public int stopCapture() {
244        if (mCamera == null) {
245            Log.e(TAG, "stopCapture: camera is null");
246            return 0;
247        }
248
249        mPreviewBufferLock.lock();
250        try {
251            if (!mIsRunning) {
252                return 0;
253            }
254            mIsRunning = false;
255        } finally {
256            mPreviewBufferLock.unlock();
257        }
258
259        mCamera.stopPreview();
260        setPreviewCallback(null);
261        return 0;
262    }
263
264    @CalledByNative
265    public void deallocate() {
266        if (mCamera == null)
267            return;
268
269        stopCapture();
270        try {
271            mCamera.setPreviewTexture(null);
272            if (mGlTextures != null)
273                GLES20.glDeleteTextures(1, mGlTextures, 0);
274            mCaptureFormat = null;
275            mCamera.release();
276            mCamera = null;
277        } catch (IOException ex) {
278            Log.e(TAG, "deallocate: failed to deallocate camera, " + ex);
279            return;
280        }
281    }
282
283    // Local hook to allow derived classes to fill capture format and modify
284    // camera parameters as they see fit.
285    abstract void setCaptureParameters(
286            int width,
287            int height,
288            int frameRate,
289            android.hardware.Camera.Parameters cameraParameters);
290
291    // Local hook to allow derived classes to configure and plug capture
292    // buffers if needed.
293    abstract void allocateBuffers();
294
295    // Local method to be overriden with the particular setPreviewCallback to be
296    // used in the implementations.
297    abstract void setPreviewCallback(android.hardware.Camera.PreviewCallback cb);
298
299    @CalledByNative
300    public int queryWidth() {
301        return mCaptureFormat.mWidth;
302    }
303
304    @CalledByNative
305    public int queryHeight() {
306        return mCaptureFormat.mHeight;
307    }
308
309    @CalledByNative
310    public int queryFrameRate() {
311        return mCaptureFormat.mFramerate;
312    }
313
314    @CalledByNative
315    public int getColorspace() {
316        switch (mCaptureFormat.mPixelFormat) {
317            case ImageFormat.YV12:
318                return AndroidImageFormatList.ANDROID_IMAGEFORMAT_YV12;
319            case ImageFormat.NV21:
320                return AndroidImageFormatList.ANDROID_IMAGEFORMAT_NV21;
321            case ImageFormat.UNKNOWN:
322            default:
323                return AndroidImageFormatList.ANDROID_IMAGEFORMAT_UNKNOWN;
324        }
325    }
326
327    protected int getDeviceOrientation() {
328        int orientation = 0;
329        if (mContext != null) {
330            WindowManager wm = (WindowManager) mContext.getSystemService(
331                    Context.WINDOW_SERVICE);
332            switch(wm.getDefaultDisplay().getRotation()) {
333                case Surface.ROTATION_90:
334                    orientation = 90;
335                    break;
336                case Surface.ROTATION_180:
337                    orientation = 180;
338                    break;
339                case Surface.ROTATION_270:
340                    orientation = 270;
341                    break;
342                case Surface.ROTATION_0:
343                default:
344                    orientation = 0;
345                    break;
346            }
347        }
348        return orientation;
349    }
350
351    // Method for VideoCapture implementations to call back native code.
352    public native void nativeOnFrameAvailable(
353            long nativeVideoCaptureDeviceAndroid,
354            byte[] data,
355            int length,
356            int rotation);
357
358    protected static android.hardware.Camera.Parameters getCameraParameters(
359            android.hardware.Camera camera) {
360        android.hardware.Camera.Parameters parameters;
361        try {
362            parameters = camera.getParameters();
363        } catch (RuntimeException ex) {
364            Log.e(TAG, "getCameraParameters: android.hardware.Camera.getParameters: " + ex);
365            camera.release();
366            return null;
367        }
368        return parameters;
369    }
370
371    private android.hardware.Camera.CameraInfo getCameraInfo(int id) {
372        android.hardware.Camera.CameraInfo cameraInfo = new android.hardware.Camera.CameraInfo();
373        try {
374            android.hardware.Camera.getCameraInfo(id, cameraInfo);
375        } catch (RuntimeException ex) {
376            Log.e(TAG, "getCameraInfo: android.hardware.Camera.getCameraInfo: " + ex);
377            return null;
378        }
379        return cameraInfo;
380    }
381}
382