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 
17 package com.android.testingcamera2.v1;
18 
19 import android.content.Context;
20 import android.graphics.ImageFormat;
21 import android.hardware.camera2.CameraAccessException;
22 import android.hardware.camera2.CameraCaptureSession;
23 import android.hardware.camera2.CameraDevice;
24 import android.hardware.camera2.CameraManager;
25 import android.hardware.camera2.CameraMetadata;
26 import android.hardware.camera2.CameraCharacteristics;
27 import android.hardware.camera2.CaptureRequest;
28 import android.hardware.camera2.CaptureRequest.Builder;
29 import android.util.Size;
30 import android.media.Image;
31 import android.media.ImageReader;
32 import android.media.MediaCodec;
33 import android.os.Handler;
34 import android.os.HandlerThread;
35 import android.util.Log;
36 import android.util.Size;
37 import android.view.Surface;
38 import android.view.SurfaceHolder;
39 
40 import com.android.ex.camera2.blocking.BlockingCameraManager;
41 import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException;
42 import com.android.ex.camera2.blocking.BlockingStateCallback;
43 import com.android.ex.camera2.blocking.BlockingSessionCallback;
44 
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.List;
48 
49 /**
50  * A camera controller class that runs in its own thread, to
51  * move camera ops off the UI. Generally thread-safe.
52  */
53 public class CameraOps {
54 
55     public static interface Listener {
onCameraOpened(String cameraId, CameraCharacteristics characteristics)56         void onCameraOpened(String cameraId, CameraCharacteristics characteristics);
57     }
58 
59     private static final String TAG = "CameraOps";
60     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
61 
62     private final HandlerThread mOpsThread;
63     private final Handler mOpsHandler;
64 
65     private final CameraManager mCameraManager;
66     private final BlockingCameraManager mBlockingCameraManager;
67     private final BlockingStateCallback mDeviceListener =
68             new BlockingStateCallback();
69 
70     private CameraDevice mCamera;
71     private CameraCaptureSession mSession;
72 
73     private ImageReader mCaptureReader;
74     private CameraCharacteristics mCameraCharacteristics;
75 
76     private int mEncodingBitRate;
77     private int mDeviceOrientation;
78 
79     private CaptureRequest.Builder mPreviewRequestBuilder;
80     private CaptureRequest.Builder mRecordingRequestBuilder;
81     List<Surface> mOutputSurfaces = new ArrayList<Surface>(2);
82     private Surface mPreviewSurface;
83     // How many JPEG buffers do we want to hold on to at once
84     private static final int MAX_CONCURRENT_JPEGS = 2;
85 
86     private static final int STATUS_ERROR = 0;
87     private static final int STATUS_UNINITIALIZED = 1;
88     private static final int STATUS_OK = 2;
89     // low encoding bitrate(bps), used by small resolution like 640x480.
90     private static final int ENC_BIT_RATE_LOW = 2000000;
91     // high encoding bitrate(bps), used by large resolution like 1080p.
92     private static final int ENC_BIT_RATE_HIGH = 10000000;
93     private static final Size DEFAULT_SIZE = new Size(640, 480);
94     private static final Size HIGH_RESOLUTION_SIZE = new Size(1920, 1080);
95 
96     private static final long IDLE_WAIT_MS = 2000;
97     // General short wait timeout for most state transitions
98     private static final long STATE_WAIT_MS = 500;
99 
100     private int mStatus = STATUS_UNINITIALIZED;
101 
102     CameraRecordingStream mRecordingStream;
103     private final Listener mListener;
104     private final Handler mListenerHandler;
105 
checkOk()106     private void checkOk() {
107         if (mStatus < STATUS_OK) {
108             throw new IllegalStateException(String.format("Device not OK: %d", mStatus ));
109         }
110     }
111 
CameraOps(Context ctx, Listener listener, Handler handler)112     private CameraOps(Context ctx, Listener listener, Handler handler) throws ApiFailureException {
113         mCameraManager = (CameraManager) ctx.getSystemService(Context.CAMERA_SERVICE);
114         if (mCameraManager == null) {
115             throw new ApiFailureException("Can't connect to camera manager!");
116         }
117         mBlockingCameraManager = new BlockingCameraManager(mCameraManager);
118 
119         mOpsThread = new HandlerThread("CameraOpsThread");
120         mOpsThread.start();
121         mOpsHandler = new Handler(mOpsThread.getLooper());
122 
123         mRecordingStream = new CameraRecordingStream();
124         mStatus = STATUS_OK;
125 
126         mListener = listener;
127         mListenerHandler = handler;
128     }
129 
create(Context ctx, Listener listener, Handler handler)130     static public CameraOps create(Context ctx, Listener listener, Handler handler)
131             throws ApiFailureException {
132         return new CameraOps(ctx, listener, handler);
133     }
134 
getDevices()135     public String[] getDevices() throws ApiFailureException{
136         checkOk();
137         try {
138             return mCameraManager.getCameraIdList();
139         } catch (CameraAccessException e) {
140             throw new ApiFailureException("Can't query device set", e);
141         }
142     }
143 
registerCameraListener(CameraManager.AvailabilityCallback listener)144     public void registerCameraListener(CameraManager.AvailabilityCallback listener)
145             throws ApiFailureException {
146         checkOk();
147         mCameraManager.registerAvailabilityCallback(listener, mOpsHandler);
148     }
149 
getCameraCharacteristics()150     public CameraCharacteristics getCameraCharacteristics() {
151         checkOk();
152         if (mCameraCharacteristics == null) {
153             throw new IllegalStateException("CameraCharacteristics is not available");
154         }
155         return mCameraCharacteristics;
156     }
157 
closeDevice()158     public void closeDevice()
159             throws ApiFailureException {
160         checkOk();
161         mCameraCharacteristics = null;
162 
163         if (mCamera == null) return;
164 
165         try {
166             mCamera.close();
167         } catch (Exception e) {
168             throw new ApiFailureException("can't close device!", e);
169         }
170 
171         mCamera = null;
172         mSession = null;
173     }
174 
minimalOpenCamera()175     private void minimalOpenCamera() throws ApiFailureException {
176         if (mCamera == null) {
177             final String[] devices;
178             final CameraCharacteristics characteristics;
179 
180             try {
181                 devices = mCameraManager.getCameraIdList();
182                 if (devices == null || devices.length == 0) {
183                     throw new ApiFailureException("no devices");
184                 }
185                 mCamera = mBlockingCameraManager.openCamera(devices[0],
186                         mDeviceListener, mOpsHandler);
187                 mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCamera.getId());
188                 characteristics = mCameraCharacteristics;
189             } catch (CameraAccessException e) {
190                 throw new ApiFailureException("open failure", e);
191             } catch (BlockingOpenException e) {
192                 throw new ApiFailureException("open async failure", e);
193             }
194 
195             // Dispatch listener event
196             if (mListener != null && mListenerHandler != null) {
197                 mListenerHandler.post(new Runnable() {
198                     @Override
199                     public void run() {
200                         mListener.onCameraOpened(devices[0], characteristics);
201                     }
202                 });
203             }
204         }
205 
206         mStatus = STATUS_OK;
207     }
208 
configureOutputs(List<Surface> outputs)209     private void configureOutputs(List<Surface> outputs) throws CameraAccessException {
210         BlockingSessionCallback sessionListener = new BlockingSessionCallback();
211         mCamera.createCaptureSession(outputs, sessionListener, mOpsHandler);
212         mSession = sessionListener.waitAndGetSession(IDLE_WAIT_MS);
213     }
214 
215     /**
216      * Set up SurfaceView dimensions for camera preview
217      */
minimalPreviewConfig(SurfaceHolder previewHolder)218     public void minimalPreviewConfig(SurfaceHolder previewHolder) throws ApiFailureException {
219 
220         minimalOpenCamera();
221         try {
222             CameraCharacteristics properties =
223                     mCameraManager.getCameraCharacteristics(mCamera.getId());
224 
225             Size[] previewSizes = null;
226             Size sz = DEFAULT_SIZE;
227             if (properties != null) {
228                 previewSizes =
229                         properties.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).
230                         getOutputSizes(previewHolder.getClass());
231             }
232 
233             if (previewSizes != null && previewSizes.length != 0 &&
234                     Arrays.asList(previewSizes).contains(HIGH_RESOLUTION_SIZE)) {
235                 sz = HIGH_RESOLUTION_SIZE;
236             }
237             Log.i(TAG, "Set preview size to " + sz.toString());
238             previewHolder.setFixedSize(sz.getWidth(), sz.getHeight());
239             mPreviewSurface = previewHolder.getSurface();
240         }  catch (CameraAccessException e) {
241             throw new ApiFailureException("Error setting up minimal preview", e);
242         }
243     }
244 
245 
246     /**
247      * Update current preview with user-specified control inputs.
248      */
updatePreview(CameraControls controls)249     public void updatePreview(CameraControls controls) {
250         if (VERBOSE) {
251             Log.v(TAG, "updatePreview - begin");
252         }
253 
254         updateCaptureRequest(mPreviewRequestBuilder, controls);
255 
256         try {
257             // Insert a one-time request if any triggers were set into the request
258             if (hasTriggers(mPreviewRequestBuilder)) {
259                 mSession.capture(mPreviewRequestBuilder.build(), /*listener*/null, /*handler*/null);
260                 removeTriggers(mPreviewRequestBuilder);
261 
262                 if (VERBOSE) {
263                     Log.v(TAG, "updatePreview - submitted extra one-shot capture with triggers");
264                 }
265             } else {
266                 if (VERBOSE) {
267                     Log.v(TAG, "updatePreview - no triggers, regular repeating request");
268                 }
269             }
270 
271             // TODO: add capture result listener
272             mSession.setRepeatingRequest(mPreviewRequestBuilder.build(),
273                     /*listener*/null, /*handler*/null);
274         } catch (CameraAccessException e) {
275             Log.e(TAG, "Update camera preview failed");
276         }
277 
278         if (VERBOSE) {
279             Log.v(TAG, "updatePreview - end");
280         }
281     }
282 
hasTriggers(Builder requestBuilder)283     private static boolean hasTriggers(Builder requestBuilder) {
284         if (requestBuilder == null) {
285             return false;
286         }
287 
288         Integer afTrigger = requestBuilder.get(CaptureRequest.CONTROL_AF_TRIGGER);
289         Integer aePrecaptureTrigger = requestBuilder.get(
290                 CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER);
291 
292         if (VERBOSE) {
293             Log.v(TAG, String.format("hasTriggers - afTrigger = %s, aePreCaptureTrigger = %s",
294                     afTrigger, aePrecaptureTrigger));
295         }
296 
297 
298         if (afTrigger != null && afTrigger != CaptureRequest.CONTROL_AF_TRIGGER_IDLE) {
299             return true;
300         }
301 
302 
303         if (aePrecaptureTrigger != null
304                 && aePrecaptureTrigger != CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE) {
305             return true;
306         }
307 
308         return false;
309     }
310 
removeTriggers(Builder requestBuilder)311     private static void removeTriggers(Builder requestBuilder) {
312         if (requestBuilder == null) {
313             return;
314         }
315 
316         requestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
317                 CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
318         requestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
319                 CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);
320     }
321 
322     /**
323      * Update current device orientation (0~360 degrees)
324      */
updateOrientation(int orientation)325     public void updateOrientation(int orientation) {
326         mDeviceOrientation = orientation;
327     }
328 
329     /**
330      * Configure streams and run minimal preview
331      */
minimalPreview(SurfaceHolder previewHolder, CameraControls camCtl)332     public void minimalPreview(SurfaceHolder previewHolder, CameraControls camCtl)
333             throws ApiFailureException {
334 
335         minimalOpenCamera();
336 
337         if (mPreviewSurface == null) {
338             throw new ApiFailureException("Preview surface is not created");
339         }
340         try {
341             List<Surface> outputSurfaces = new ArrayList<Surface>(/*capacity*/1);
342             outputSurfaces.add(mPreviewSurface);
343 
344             configureOutputs(outputSurfaces);
345 
346             mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
347             updateCaptureRequest(mPreviewRequestBuilder, camCtl);
348 
349             mPreviewRequestBuilder.addTarget(mPreviewSurface);
350 
351             mSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null);
352         } catch (CameraAccessException e) {
353             throw new ApiFailureException("Error setting up minimal preview", e);
354         }
355     }
356 
minimalJpegCapture(final CaptureCallback listener, CaptureResultListener l, Handler h, CameraControls cameraControl)357     public void minimalJpegCapture(final CaptureCallback listener, CaptureResultListener l,
358             Handler h, CameraControls cameraControl) throws ApiFailureException {
359         minimalOpenCamera();
360 
361         try {
362             CameraCharacteristics properties =
363                     mCameraManager.getCameraCharacteristics(mCamera.getId());
364             Size[] jpegSizes = null;
365             if (properties != null) {
366                 jpegSizes = properties.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).
367                         getOutputSizes(ImageFormat.JPEG);
368             }
369             int width = 640;
370             int height = 480;
371 
372             if (jpegSizes != null && jpegSizes.length > 0) {
373                 width = jpegSizes[0].getWidth();
374                 height = jpegSizes[0].getHeight();
375             }
376 
377             if (mCaptureReader == null || mCaptureReader.getWidth() != width ||
378                     mCaptureReader.getHeight() != height) {
379                 if (mCaptureReader != null) {
380                     mCaptureReader.close();
381                 }
382                 mCaptureReader = ImageReader.newInstance(width, height,
383                         ImageFormat.JPEG, MAX_CONCURRENT_JPEGS);
384             }
385 
386             List<Surface> outputSurfaces = new ArrayList<Surface>(/*capacity*/1);
387             outputSurfaces.add(mCaptureReader.getSurface());
388 
389             configureOutputs(outputSurfaces);
390 
391             CaptureRequest.Builder captureBuilder =
392                     mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
393             captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientationHint());
394 
395             captureBuilder.addTarget(mCaptureReader.getSurface());
396 
397             updateCaptureRequest(captureBuilder, cameraControl);
398 
399             ImageReader.OnImageAvailableListener readerListener =
400                     new ImageReader.OnImageAvailableListener() {
401                 @Override
402                 public void onImageAvailable(ImageReader reader) {
403                     Image i = null;
404                     try {
405                         i = reader.acquireNextImage();
406                         listener.onCaptureAvailable(i);
407                     } finally {
408                         if (i != null) {
409                             i.close();
410                         }
411                     }
412                 }
413             };
414             mCaptureReader.setOnImageAvailableListener(readerListener, h);
415 
416             mSession.capture(captureBuilder.build(), l, mOpsHandler);
417         } catch (CameraAccessException e) {
418             throw new ApiFailureException("Error in minimal JPEG capture", e);
419         }
420     }
421 
startRecording(Context applicationContext, boolean useMediaCodec, int outputFormat)422     public void startRecording(Context applicationContext, boolean useMediaCodec, int outputFormat)
423             throws ApiFailureException {
424         minimalOpenCamera();
425         Size recordingSize = getRecordingSize();
426         int orientationHint = getOrientationHint();
427         try {
428             if (mRecordingRequestBuilder == null) {
429                 mRecordingRequestBuilder =
430                         mCamera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
431             }
432             // Setup output stream first
433             mRecordingStream.configure(
434                     applicationContext, recordingSize, useMediaCodec, mEncodingBitRate,
435                     orientationHint, outputFormat);
436             mRecordingStream.onConfiguringOutputs(mOutputSurfaces, /* detach */false);
437             mRecordingStream.onConfiguringRequest(mRecordingRequestBuilder, /* detach */false);
438 
439             // TODO: For preview, create preview stream class, and do the same thing like recording.
440             mOutputSurfaces.add(mPreviewSurface);
441             mRecordingRequestBuilder.addTarget(mPreviewSurface);
442 
443             // Start camera streaming and recording.
444             configureOutputs(mOutputSurfaces);
445             mSession.setRepeatingRequest(mRecordingRequestBuilder.build(), null, null);
446             mRecordingStream.start();
447         } catch (CameraAccessException e) {
448             throw new ApiFailureException("Error start recording", e);
449         }
450     }
451 
stopRecording(Context ctx)452     public void stopRecording(Context ctx) throws ApiFailureException {
453         try {
454             /**
455              * <p>
456              * Only stop camera recording stream.
457              * </p>
458              * <p>
459              * FIXME: There is a race condition to be fixed in CameraDevice.
460              * Basically, when stream closes, encoder and its surface is
461              * released, while it still takes some time for camera to finish the
462              * output to that surface. Then it cause camera in bad state.
463              * </p>
464              */
465             mRecordingStream.onConfiguringRequest(mRecordingRequestBuilder, /* detach */true);
466             mRecordingStream.onConfiguringOutputs(mOutputSurfaces, /* detach */true);
467 
468             // Remove recording surface before calling RecordingStream.stop,
469             // since that invalidates the surface.
470             configureOutputs(mOutputSurfaces);
471 
472             mRecordingStream.stop(ctx);
473 
474             mSession.setRepeatingRequest(mRecordingRequestBuilder.build(), null, null);
475         } catch (CameraAccessException e) {
476             throw new ApiFailureException("Error stop recording", e);
477         }
478     }
479 
480     /**
481      * Flush all current requests and in-progress work
482      */
flush()483     public void flush() throws ApiFailureException {
484         minimalOpenCamera();
485         try {
486             mSession.abortCaptures();
487         } catch (CameraAccessException e) {
488             throw new ApiFailureException("Error flushing", e);
489         }
490     }
491 
getOrientationHint()492     private int getOrientationHint() {
493         // snap to {0, 90, 180, 270}
494         int orientation = ((int)Math.round(mDeviceOrientation/90.0)*90) % 360;
495 
496         CameraCharacteristics properties = getCameraCharacteristics();
497         int sensorOrientation = properties.get(CameraCharacteristics.SENSOR_ORIENTATION);
498 
499         // TODO: below calculation is for back-facing camera only
500         // front-facing camera should use:
501         // return (sensorOrientation - orientation +360) % 360;
502         return (sensorOrientation + orientation) % 360;
503     }
504 
getRecordingSize()505     private Size getRecordingSize() throws ApiFailureException {
506         try {
507             CameraCharacteristics properties =
508                     mCameraManager.getCameraCharacteristics(mCamera.getId());
509 
510             Size[] recordingSizes = null;
511             if (properties != null) {
512                 recordingSizes =
513                         properties.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).
514                         getOutputSizes(MediaCodec.class);
515             }
516 
517             mEncodingBitRate = ENC_BIT_RATE_LOW;
518             if (recordingSizes == null || recordingSizes.length == 0) {
519                 Log.w(TAG, "Unable to get recording sizes, default to 640x480");
520                 return DEFAULT_SIZE;
521             } else {
522                 /**
523                  * TODO: create resolution selection widget on UI, then use the
524                  * select size. For now, return HIGH_RESOLUTION_SIZE if it
525                  * exists in the processed size list, otherwise return default
526                  * size
527                  */
528                 if (Arrays.asList(recordingSizes).contains(HIGH_RESOLUTION_SIZE)) {
529                     mEncodingBitRate = ENC_BIT_RATE_HIGH;
530                     return HIGH_RESOLUTION_SIZE;
531                 } else {
532                     // Fallback to default size when HD size is not found.
533                     Log.w(TAG,
534                             "Unable to find the requested size " + HIGH_RESOLUTION_SIZE.toString()
535                             + " Fallback to " + DEFAULT_SIZE.toString());
536                     return DEFAULT_SIZE;
537                 }
538             }
539         } catch (CameraAccessException e) {
540             throw new ApiFailureException("Error setting up video recording", e);
541         }
542     }
543 
updateCaptureRequest(CaptureRequest.Builder builder, CameraControls cameraControl)544     private void updateCaptureRequest(CaptureRequest.Builder builder,
545             CameraControls cameraControl) {
546         if (cameraControl != null) {
547             // Update the manual control metadata for capture request
548             // may disable 3A routines.
549             updateCaptureRequest(builder, cameraControl.getManualControls());
550             // Update the AF control metadata for capture request (if manual is not used)
551             updateCaptureRequest(builder, cameraControl.getAfControls());
552         }
553     }
554 
updateCaptureRequest(CaptureRequest.Builder builder, CameraManualControls manualControls)555     private void updateCaptureRequest(CaptureRequest.Builder builder,
556             CameraManualControls manualControls) {
557         if (manualControls == null) {
558             return;
559         }
560 
561         if (manualControls.isManualControlEnabled()) {
562             Log.e(TAG, "update request: " + manualControls.getSensitivity());
563             builder.set(CaptureRequest.CONTROL_MODE,
564                     CameraMetadata.CONTROL_MODE_OFF);
565             builder.set(CaptureRequest.SENSOR_SENSITIVITY,
566                     manualControls.getSensitivity());
567             builder.set(CaptureRequest.SENSOR_FRAME_DURATION,
568                     manualControls.getFrameDuration());
569             builder.set(CaptureRequest.SENSOR_EXPOSURE_TIME,
570                     manualControls.getExposure());
571 
572             if (VERBOSE) {
573                 Log.v(TAG, "updateCaptureRequest - manual - control.mode = OFF");
574             }
575         } else {
576             builder.set(CaptureRequest.CONTROL_MODE,
577                     CameraMetadata.CONTROL_MODE_AUTO);
578 
579             if (VERBOSE) {
580                 Log.v(TAG, "updateCaptureRequest - manual - control.mode = AUTO");
581             }
582         }
583     }
584 
updateCaptureRequest(CaptureRequest.Builder builder, CameraAutoFocusControls cameraAfControl)585     private void updateCaptureRequest(CaptureRequest.Builder builder,
586             CameraAutoFocusControls cameraAfControl) {
587         if (cameraAfControl == null) {
588             return;
589         }
590 
591         if (cameraAfControl.isAfControlEnabled()) {
592             builder.set(CaptureRequest.CONTROL_AF_MODE, cameraAfControl.getAfMode());
593 
594             Integer afTrigger = cameraAfControl.consumePendingTrigger();
595 
596             if (afTrigger != null) {
597                 builder.set(CaptureRequest.CONTROL_AF_TRIGGER, afTrigger);
598             }
599 
600             if (VERBOSE) {
601                 Log.v(TAG, "updateCaptureRequest - AF - set trigger to " + afTrigger);
602             }
603         }
604     }
605 
606     public interface CaptureCallback {
onCaptureAvailable(Image capture)607         void onCaptureAvailable(Image capture);
608     }
609 
610     public static abstract class CaptureResultListener
611             extends CameraCaptureSession.CaptureCallback {}
612 }
613