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; 18 19 import android.content.Context; 20 import android.graphics.ImageFormat; 21 import android.hardware.camera2.CameraAccessException; 22 import android.hardware.camera2.CameraDevice; 23 import android.hardware.camera2.CameraManager; 24 import android.hardware.camera2.CameraMetadata; 25 import android.hardware.camera2.CameraCharacteristics; 26 import android.hardware.camera2.CaptureRequest; 27 import android.hardware.camera2.Size; 28 import android.media.Image; 29 import android.media.ImageReader; 30 import android.os.Handler; 31 import android.os.HandlerThread; 32 import android.os.Looper; 33 import android.os.Message; 34 import android.util.Log; 35 import android.view.Surface; 36 import android.view.SurfaceHolder; 37 38 import com.android.ex.camera2.blocking.BlockingCameraManager; 39 import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException; 40 import com.android.ex.camera2.blocking.BlockingStateListener; 41 42 import java.util.ArrayList; 43 import java.util.Arrays; 44 import 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 */ 50 public 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