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.util.Log;
10
11import java.util.ArrayList;
12import java.util.List;
13
14/**
15 * This class extends the VideoCapture base class for manipulating normal video
16 * capture devices in Android, including receiving copies of preview frames via
17 * Java-allocated buffers. It also includes class BuggyDeviceHack to deal with
18 * troublesome devices.
19 **/
20@SuppressWarnings("deprecation")
21public class VideoCaptureAndroid extends VideoCapture {
22
23    // Some devices don't support YV12 format correctly, even with JELLY_BEAN or
24    // newer OS. To work around the issues on those devices, we have to request
25    // NV21. Some other devices have troubles with certain capture resolutions
26    // under a given one: for those, the resolution is swapped with a known
27    // good. Both are supposed to be temporary hacks.
28    private static class BuggyDeviceHack {
29        private static class IdAndSizes {
30            IdAndSizes(String model, String device, int minWidth, int minHeight) {
31                mModel = model;
32                mDevice = device;
33                mMinWidth = minWidth;
34                mMinHeight = minHeight;
35            }
36            public final String mModel;
37            public final String mDevice;
38            public final int mMinWidth;
39            public final int mMinHeight;
40        }
41
42        private static final IdAndSizes CAPTURESIZE_BUGGY_DEVICE_LIST[] = {
43            new IdAndSizes("Nexus 7", "flo", 640, 480)
44        };
45
46        private static final String[] COLORSPACE_BUGGY_DEVICE_LIST = {
47            "SAMSUNG-SGH-I747",
48            "ODROID-U2",
49        };
50
51        static void applyMinDimensions(CaptureFormat format) {
52            // NOTE: this can discard requested aspect ratio considerations.
53            for (IdAndSizes buggyDevice : CAPTURESIZE_BUGGY_DEVICE_LIST) {
54                if (buggyDevice.mModel.contentEquals(android.os.Build.MODEL) &&
55                        buggyDevice.mDevice.contentEquals(android.os.Build.DEVICE)) {
56                    format.mWidth = (buggyDevice.mMinWidth > format.mWidth)
57                            ? buggyDevice.mMinWidth : format.mWidth;
58                    format.mHeight = (buggyDevice.mMinHeight > format.mHeight)
59                            ? buggyDevice.mMinHeight : format.mHeight;
60                }
61            }
62        }
63
64        static int getImageFormat() {
65            if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) {
66                return ImageFormat.NV21;
67            }
68
69            for (String buggyDevice : COLORSPACE_BUGGY_DEVICE_LIST) {
70                if (buggyDevice.contentEquals(android.os.Build.MODEL)) {
71                    return ImageFormat.NV21;
72                }
73            }
74            return ImageFormat.YV12;
75        }
76    }
77
78    private int mExpectedFrameSize;
79    private static final int NUM_CAPTURE_BUFFERS = 3;
80    private static final String TAG = "VideoCaptureAndroid";
81
82    static CaptureFormat[] getDeviceSupportedFormats(int id) {
83        android.hardware.Camera camera;
84        try {
85             camera = android.hardware.Camera.open(id);
86        } catch (RuntimeException ex) {
87            Log.e(TAG, "Camera.open: " + ex);
88            return null;
89        }
90        android.hardware.Camera.Parameters parameters = getCameraParameters(camera);
91        if (parameters == null) {
92            return null;
93        }
94
95        ArrayList<CaptureFormat> formatList = new ArrayList<CaptureFormat>();
96        // getSupportedPreview{Formats,FpsRange,PreviewSizes}() returns Lists
97        // with at least one element, but when the camera is in bad state, they
98        // can return null pointers; in that case we use a 0 entry, so we can
99        // retrieve as much information as possible.
100        List<Integer> pixelFormats = parameters.getSupportedPreviewFormats();
101        if (pixelFormats == null) {
102            pixelFormats = new ArrayList<Integer>();
103        }
104        if (pixelFormats.size() == 0) {
105            pixelFormats.add(ImageFormat.UNKNOWN);
106        }
107        for (Integer previewFormat : pixelFormats) {
108            int pixelFormat =
109                    AndroidImageFormatList.ANDROID_IMAGEFORMAT_UNKNOWN;
110            if (previewFormat == ImageFormat.YV12) {
111                pixelFormat = AndroidImageFormatList.ANDROID_IMAGEFORMAT_YV12;
112            } else if (previewFormat == ImageFormat.NV21) {
113                continue;
114            }
115
116            List<int[]> listFpsRange = parameters.getSupportedPreviewFpsRange();
117            if (listFpsRange == null) {
118                listFpsRange = new ArrayList<int[]>();
119            }
120            if (listFpsRange.size() == 0) {
121                listFpsRange.add(new int[] {0, 0});
122            }
123            for (int[] fpsRange : listFpsRange) {
124                List<android.hardware.Camera.Size> supportedSizes =
125                        parameters.getSupportedPreviewSizes();
126                if (supportedSizes == null) {
127                    supportedSizes = new ArrayList<android.hardware.Camera.Size>();
128                }
129                if (supportedSizes.size() == 0) {
130                    supportedSizes.add(camera.new Size(0, 0));
131                }
132                for (android.hardware.Camera.Size size : supportedSizes) {
133                    formatList.add(new CaptureFormat(size.width,
134                                                     size.height,
135                                                     (fpsRange[1] + 999) / 1000,
136                                                     pixelFormat));
137                }
138            }
139        }
140        camera.release();
141        return formatList.toArray(new CaptureFormat[formatList.size()]);
142    }
143
144    VideoCaptureAndroid(Context context,
145                        int id,
146                        long nativeVideoCaptureDeviceAndroid) {
147        super(context, id, nativeVideoCaptureDeviceAndroid);
148    }
149
150    @Override
151    protected void setCaptureParameters(
152            int width,
153            int height,
154            int frameRate,
155            android.hardware.Camera.Parameters cameraParameters) {
156        mCaptureFormat = new CaptureFormat(
157                width, height, frameRate, BuggyDeviceHack.getImageFormat());
158        // Hack to avoid certain capture resolutions under a minimum one,
159        // see http://crbug.com/305294.
160        BuggyDeviceHack.applyMinDimensions(mCaptureFormat);
161    }
162
163    @Override
164    protected void allocateBuffers() {
165        mExpectedFrameSize = mCaptureFormat.mWidth * mCaptureFormat.mHeight *
166                ImageFormat.getBitsPerPixel(mCaptureFormat.mPixelFormat) / 8;
167        for (int i = 0; i < NUM_CAPTURE_BUFFERS; i++) {
168            byte[] buffer = new byte[mExpectedFrameSize];
169            mCamera.addCallbackBuffer(buffer);
170        }
171    }
172
173    @Override
174    protected void setPreviewCallback(android.hardware.Camera.PreviewCallback cb) {
175        mCamera.setPreviewCallbackWithBuffer(cb);
176    }
177
178    @Override
179    public void onPreviewFrame(byte[] data, android.hardware.Camera camera) {
180        mPreviewBufferLock.lock();
181        try {
182            if (!mIsRunning) {
183                return;
184            }
185            if (data.length == mExpectedFrameSize) {
186                int rotation = getDeviceOrientation();
187                if (rotation != mDeviceOrientation) {
188                    mDeviceOrientation = rotation;
189                }
190                if (mCameraFacing == android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK) {
191                    rotation = 360 - rotation;
192                }
193                rotation = (mCameraOrientation + rotation) % 360;
194                nativeOnFrameAvailable(mNativeVideoCaptureDeviceAndroid,
195                        data, mExpectedFrameSize, rotation);
196            }
197        } finally {
198            mPreviewBufferLock.unlock();
199            if (camera != null) {
200                camera.addCallbackBuffer(data);
201            }
202        }
203    }
204
205    // TODO(wjia): investigate whether reading from texture could give better
206    // performance and frame rate, using onFrameAvailable().
207}
208