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