CameraOps.java revision a04e462a07854595122f19b1df9f19c78e5dc030
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.testingcamera2;
18
19import android.content.Context;
20import android.graphics.ImageFormat;
21import android.hardware.camera2.CameraAccessException;
22import android.hardware.camera2.CameraDevice;
23import android.hardware.camera2.CameraManager;
24import android.hardware.camera2.CameraMetadata;
25import android.hardware.camera2.CameraProperties;
26import android.hardware.camera2.CaptureRequest;
27import android.hardware.camera2.Size;
28import android.media.Image;
29import android.media.ImageReader;
30import android.os.Handler;
31import android.os.Looper;
32import android.os.Message;
33import android.util.Log;
34import android.view.Surface;
35import android.view.SurfaceHolder;
36
37import java.util.ArrayList;
38import java.util.Arrays;
39import java.util.List;
40
41/**
42 * A camera controller class that runs in its own thread, to
43 * move camera ops off the UI. Generally thread-safe.
44 */
45public class CameraOps {
46
47    private static final String TAG = "CameraOps";
48
49    private Thread mOpsThread;
50    private Handler mOpsHandler;
51
52    private CameraManager mCameraManager;
53    private CameraDevice mCamera;
54
55    private ImageReader mCaptureReader;
56    private CameraProperties mCameraProperties;
57
58    private int mEncodingBitRate;
59
60    private CaptureRequest.Builder mPreviewRequestBuilder;
61    private CaptureRequest.Builder mRecordingRequestBuilder;
62    List<Surface> mOutputSurfaces = new ArrayList<Surface>(2);
63    private Surface mPreviewSurface;
64    // How many JPEG buffers do we want to hold on to at once
65    private static final int MAX_CONCURRENT_JPEGS = 2;
66
67    private static final int STATUS_ERROR = 0;
68    private static final int STATUS_UNINITIALIZED = 1;
69    private static final int STATUS_OK = 2;
70    // low encoding bitrate(bps), used by small resolution like 640x480.
71    private static final int ENC_BIT_RATE_LOW = 2000000;
72    // high encoding bitrate(bps), used by large resolution like 1080p.
73    private static final int ENC_BIT_RATE_HIGH = 10000000;
74    private static final Size DEFAULT_SIZE = new Size(640, 480);
75    private static final Size HIGH_RESOLUTION_SIZE = new Size(1920, 1080);
76
77    private int mStatus = STATUS_UNINITIALIZED;
78
79    CameraRecordingStream mRecordingStream;
80
81    private void checkOk() {
82        if (mStatus < STATUS_OK) {
83            throw new IllegalStateException(String.format("Device not OK: %d", mStatus ));
84        }
85    }
86
87    private class OpsHandler extends Handler {
88        @Override
89        public void handleMessage(Message msg) {
90
91        }
92    }
93
94    private CameraOps(Context ctx) throws ApiFailureException {
95        mCameraManager = (CameraManager) ctx.getSystemService(Context.CAMERA_SERVICE);
96        if (mCameraManager == null) {
97            throw new ApiFailureException("Can't connect to camera manager!");
98        }
99
100        mOpsThread = new Thread(new Runnable() {
101            @Override
102            public void run() {
103                Looper.prepare();
104                mOpsHandler = new OpsHandler();
105                Looper.loop();
106            }
107        }, "CameraOpsThread");
108        mOpsThread.start();
109
110        mRecordingStream = new CameraRecordingStream();
111        mStatus = STATUS_OK;
112    }
113
114    static public CameraOps create(Context ctx) throws ApiFailureException {
115        return new CameraOps(ctx);
116    }
117
118    public String[] getDevices() throws ApiFailureException{
119        checkOk();
120        try {
121            return mCameraManager.getCameraIdList();
122        } catch (CameraAccessException e) {
123            throw new ApiFailureException("Can't query device set", e);
124        }
125    }
126
127    public void registerCameraListener(CameraManager.AvailabilityListener listener)
128            throws ApiFailureException {
129        checkOk();
130        mCameraManager.addAvailabilityListener(listener, mOpsHandler);
131    }
132
133    public CameraProperties getCameraProperties() {
134        checkOk();
135        if (mCameraProperties == null) {
136            throw new IllegalStateException("CameraProperties is not available");
137        }
138        return mCameraProperties;
139    }
140
141    public void openDevice(String cameraId)
142            throws CameraAccessException, ApiFailureException {
143        checkOk();
144
145        if (mCamera != null) {
146            throw new IllegalStateException("Already have open camera device");
147        }
148
149        mCamera = mCameraManager.openCamera(cameraId);
150    }
151
152    public void closeDevice()
153            throws ApiFailureException {
154        checkOk();
155        mCameraProperties = null;
156
157        if (mCamera == null) return;
158
159        try {
160            mCamera.close();
161        } catch (Exception e) {
162            throw new ApiFailureException("can't close device!", e);
163        }
164
165        mCamera = null;
166    }
167
168    private void minimalOpenCamera() throws ApiFailureException {
169        if (mCamera == null) {
170            try {
171                String[] devices = mCameraManager.getCameraIdList();
172                if (devices == null || devices.length == 0) {
173                    throw new ApiFailureException("no devices");
174                }
175                mCamera = mCameraManager.openCamera(devices[0]);
176                mCameraProperties = mCamera.getProperties();
177            } catch (CameraAccessException e) {
178                throw new ApiFailureException("open failure", e);
179            }
180        }
181
182        mStatus = STATUS_OK;
183    }
184
185    /**
186     * Set up SurfaceView dimensions for camera preview
187     */
188    public void minimalPreviewConfig(SurfaceHolder previewHolder) throws ApiFailureException {
189
190        minimalOpenCamera();
191        try {
192            CameraProperties properties = mCamera.getProperties();
193
194            Size[] previewSizes = null;
195            Size sz = DEFAULT_SIZE;
196            if (properties != null) {
197                previewSizes = properties.get(
198                        CameraProperties.SCALER_AVAILABLE_PROCESSED_SIZES);
199            }
200
201            if (previewSizes != null && previewSizes.length != 0 &&
202                    Arrays.asList(previewSizes).contains(HIGH_RESOLUTION_SIZE)) {
203                sz = HIGH_RESOLUTION_SIZE;
204            }
205            Log.i(TAG, "Set preview size to " + sz.toString());
206            previewHolder.setFixedSize(sz.getWidth(), sz.getHeight());
207            mPreviewSurface = previewHolder.getSurface();
208        }  catch (CameraAccessException e) {
209            throw new ApiFailureException("Error setting up minimal preview", e);
210        }
211    }
212
213
214    /**
215     * Update current preview with manual control inputs.
216     */
217    public void updatePreview(CameraControls manualCtrl) {
218        updateCaptureRequest(mPreviewRequestBuilder, manualCtrl);
219
220        try {
221            // TODO: add capture result listener
222            mCamera.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null);
223        } catch (CameraAccessException e) {
224            Log.e(TAG, "Update camera preview failed");
225        }
226    }
227
228    /**
229     * Configure streams and run minimal preview
230     */
231    public void minimalPreview(SurfaceHolder previewHolder) throws ApiFailureException {
232
233        minimalOpenCamera();
234        if (mPreviewSurface == null) {
235            throw new ApiFailureException("Preview surface is not created");
236        }
237        try {
238            mCamera.stopRepeating();
239            mCamera.waitUntilIdle();
240
241            List<Surface> outputSurfaces = new ArrayList(1);
242            outputSurfaces.add(mPreviewSurface);
243
244            mCamera.configureOutputs(outputSurfaces);
245
246            CaptureRequest.Builder previewBuilder;
247            mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
248
249            mPreviewRequestBuilder.addTarget(mPreviewSurface);
250
251            mCamera.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null);
252        } catch (CameraAccessException e) {
253            throw new ApiFailureException("Error setting up minimal preview", e);
254        }
255    }
256
257    public void minimalJpegCapture(final CaptureListener listener, CaptureResultListener l,
258            Handler h, CameraControls cameraControl) throws ApiFailureException {
259        minimalOpenCamera();
260
261        try {
262            mCamera.stopRepeating();
263            mCamera.waitUntilIdle();
264
265            CameraProperties properties = mCamera.getProperties();
266            Size[] jpegSizes = null;
267            if (properties != null) {
268                jpegSizes = properties.get(
269                        CameraProperties.SCALER_AVAILABLE_JPEG_SIZES);
270            }
271            int width = 640;
272            int height = 480;
273
274            if (jpegSizes != null && jpegSizes.length > 0) {
275                width = jpegSizes[0].getWidth();
276                height = jpegSizes[0].getHeight();
277            }
278
279            if (mCaptureReader == null || mCaptureReader.getWidth() != width ||
280                    mCaptureReader.getHeight() != height) {
281                if (mCaptureReader != null) {
282                    mCaptureReader.close();
283                }
284                mCaptureReader = new ImageReader(width, height,
285                        ImageFormat.JPEG, MAX_CONCURRENT_JPEGS);
286            }
287
288            List<Surface> outputSurfaces = new ArrayList(1);
289            outputSurfaces.add(mCaptureReader.getSurface());
290
291            mCamera.configureOutputs(outputSurfaces);
292
293            CaptureRequest.Builder captureBuilder =
294                    mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
295
296            captureBuilder.addTarget(mCaptureReader.getSurface());
297
298            updateCaptureRequest(captureBuilder, cameraControl);
299
300            ImageReader.OnImageAvailableListener readerListener =
301                    new ImageReader.OnImageAvailableListener() {
302                @Override
303                public void onImageAvailable(ImageReader reader) {
304                    Image i = reader.getNextImage();
305                    listener.onCaptureAvailable(i);
306                    i.close();
307                }
308            };
309            mCaptureReader.setImageAvailableListener(readerListener, h);
310
311            mCamera.capture(captureBuilder.build(), l, mOpsHandler);
312
313        } catch (CameraAccessException e) {
314            throw new ApiFailureException("Error in minimal JPEG capture", e);
315        }
316    }
317
318    public void startRecording(boolean useMediaCodec) throws ApiFailureException {
319        minimalOpenCamera();
320        Size recordingSize = getRecordingSize();
321        CaptureRequest request;
322        try {
323            if (mRecordingRequestBuilder == null) {
324                mRecordingRequestBuilder =
325                        mCamera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
326            }
327            // Setup output stream first
328            mRecordingStream.configure(recordingSize, useMediaCodec, mEncodingBitRate);
329            mRecordingStream.onConfiguringOutputs(mOutputSurfaces, /* detach */false);
330            mRecordingStream.onConfiguringRequest(mRecordingRequestBuilder, /* detach */false);
331
332            // TODO: For preview, create preview stream class, and do the same thing like recording.
333            mOutputSurfaces.add(mPreviewSurface);
334            mRecordingRequestBuilder.addTarget(mPreviewSurface);
335
336            // Start camera streaming and recording.
337            mCamera.configureOutputs(mOutputSurfaces);
338            mCamera.setRepeatingRequest(mRecordingRequestBuilder.build(), null, null);
339            mRecordingStream.start();
340        } catch (CameraAccessException e) {
341            throw new ApiFailureException("Error start recording", e);
342        }
343    }
344
345    public void stopRecording() throws ApiFailureException {
346        try {
347            /**
348             * <p>
349             * Only stop camera recording stream.
350             * </p>
351             * <p>
352             * FIXME: There is a race condition to be fixed in CameraDevice.
353             * Basically, when stream closes, encoder and its surface is
354             * released, while it still takes some time for camera to finish the
355             * output to that surface. Then it cause camera in bad state.
356             * </p>
357             */
358            mRecordingStream.onConfiguringRequest(mRecordingRequestBuilder, /* detach */true);
359            mRecordingStream.onConfiguringOutputs(mOutputSurfaces, /* detach */true);
360            mCamera.stopRepeating();
361            mCamera.waitUntilIdle();
362            mRecordingStream.stop();
363
364            mCamera.configureOutputs(mOutputSurfaces);
365            mCamera.setRepeatingRequest(mRecordingRequestBuilder.build(), null, null);
366        } catch (CameraAccessException e) {
367            throw new ApiFailureException("Error stop recording", e);
368        }
369    }
370
371    private Size getRecordingSize() throws ApiFailureException {
372        try {
373            CameraProperties properties = mCamera.getProperties();
374
375            Size[] recordingSizes = null;
376            if (properties != null) {
377                recordingSizes = properties.get(
378                        CameraProperties.SCALER_AVAILABLE_PROCESSED_SIZES);
379            }
380
381            mEncodingBitRate = ENC_BIT_RATE_LOW;
382            if (recordingSizes == null || recordingSizes.length == 0) {
383                Log.w(TAG, "Unable to get recording sizes, default to 640x480");
384                return DEFAULT_SIZE;
385            } else {
386                /**
387                 * TODO: create resolution selection widget on UI, then use the
388                 * select size. For now, return HIGH_RESOLUTION_SIZE if it
389                 * exists in the processed size list, otherwise return default
390                 * size
391                 */
392                if (Arrays.asList(recordingSizes).contains(HIGH_RESOLUTION_SIZE)) {
393                    mEncodingBitRate = ENC_BIT_RATE_HIGH;
394                    return HIGH_RESOLUTION_SIZE;
395                } else {
396                    // Fallback to default size when HD size is not found.
397                    Log.w(TAG,
398                            "Unable to find the requested size " + HIGH_RESOLUTION_SIZE.toString()
399                            + " Fallback to " + DEFAULT_SIZE.toString());
400                    return DEFAULT_SIZE;
401                }
402            }
403        } catch (CameraAccessException e) {
404            throw new ApiFailureException("Error setting up video recording", e);
405        }
406    }
407
408    private void updateCaptureRequest(CaptureRequest.Builder builder, CameraControls cameraControl) {
409        if (cameraControl != null) {
410            // Update the manual control metadata for capture request
411            // Disable 3A routines.
412            if (cameraControl.isManualControlEnabled()) {
413                Log.e(TAG, "update request: " + cameraControl.getSensitivity());
414                builder.set(CaptureRequest.CONTROL_MODE,
415                        CameraMetadata.CONTROL_MODE_OFF);
416                builder.set(CaptureRequest.SENSOR_SENSITIVITY,
417                        cameraControl.getSensitivity());
418                builder.set(CaptureRequest.SENSOR_FRAME_DURATION,
419                        cameraControl.getFrameDuration());
420                builder.set(CaptureRequest.SENSOR_EXPOSURE_TIME,
421                        cameraControl.getExposure());
422            } else {
423                builder.set(CaptureRequest.CONTROL_MODE,
424                        CameraMetadata.CONTROL_MODE_AUTO);
425            }
426        }
427    }
428
429    public interface CaptureListener {
430        void onCaptureAvailable(Image capture);
431    }
432
433    public static abstract class CaptureResultListener extends CameraDevice.CaptureListener {}
434}
435