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