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