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