1 /* 2 * Copyright (C) 2024 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.google.android.car.kitchensink.camera2; 18 19 import android.hardware.camera2.CameraAccessException; 20 import android.hardware.camera2.CameraCaptureSession; 21 import android.hardware.camera2.CameraDevice; 22 import android.hardware.camera2.CameraManager; 23 import android.hardware.camera2.CaptureRequest; 24 import android.hardware.camera2.TotalCaptureResult; 25 import android.hardware.camera2.params.OutputConfiguration; 26 import android.hardware.camera2.params.SessionConfiguration; 27 import android.media.MediaRecorder; 28 import android.os.Handler; 29 import android.os.HandlerThread; 30 import android.os.Looper; 31 import android.os.Message; 32 import android.util.Log; 33 import android.util.Size; 34 import android.view.SurfaceHolder; 35 import android.view.SurfaceView; 36 37 import androidx.annotation.NonNull; 38 39 import java.io.IOException; 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.concurrent.Executor; 43 import java.util.concurrent.atomic.AtomicLong; 44 45 final class CameraPreviewManager { 46 private static final String TAG = CameraPreviewManager.class.getSimpleName(); 47 private static final int MSG_SURFACE_READY = 1; 48 private static final int MSG_CAMERA_OPENED = 2; 49 private static final int MSG_SESSION_REQUESTED = 3; 50 private static final Size VIDEO_SIZE = new Size(/* width= */ 1280, /* height= */ 720); 51 private static final int VIDEO_FRAME_RATE = 30; 52 private static final int VIDEO_BIT_RATE = 10_000_000; 53 private final SessionStateListener mSessionStateListener; 54 private final CameraDeviceStateListener mCameraDeviceStateListener; 55 private final SessionCaptureListener mSessionCaptureListener; 56 private final String mCameraId; 57 private final SurfaceView mSurfaceView; 58 private final CameraManager mCameraManager; 59 private final Handler mSessionHandler; 60 private final Handler mConfigHandler; 61 private boolean mIsCameraConnected; 62 private boolean mIsSurfaceCreated; 63 private boolean mIsPreviewSessionRequested; 64 private boolean mIsPreviewSessionCreated; 65 private boolean mIsRecordingSessionRequested; 66 private boolean mIsRecordingSessionCreated; 67 private CameraDevice mCameraDevice; 68 private CaptureRequest mCaptureRequest; 69 private CameraCaptureSession mCaptureSession; 70 private MediaRecorder mRecorder; 71 private String mVideoFilePath; 72 73 /** 74 * CameraPreviewManager 75 * Class designed to create and manage a camera preview for a single camera device. 76 * 77 * @param cameraId Specifies for which camera this instance is managing the preview 78 * @param surfaceView The SurfaceView on which the preview will be shown 79 * @param cameraManager The system camera manager 80 */ CameraPreviewManager( String cameraId, SurfaceView surfaceView, CameraManager cameraManager, HandlerThread sessionHandlerThread)81 CameraPreviewManager( 82 String cameraId, SurfaceView surfaceView, 83 CameraManager cameraManager, HandlerThread sessionHandlerThread) { 84 mCameraId = cameraId; 85 mSurfaceView = surfaceView; 86 mCameraManager = cameraManager; 87 mSurfaceView.getHolder().addCallback(new SurfacePreviewListener()); 88 89 mCameraDeviceStateListener = new CameraDeviceStateListener(); 90 mSessionStateListener = new SessionStateListener(); 91 mSessionCaptureListener = new SessionCaptureListener(); 92 93 mSessionHandler = new Handler(sessionHandlerThread.getLooper()); 94 mConfigHandler = new Handler(Looper.getMainLooper(), new CameraSurfaceInitListener()); 95 } 96 openCamera()97 void openCamera() { 98 try { 99 Log.d(TAG, "Opening camera " + mCameraId); 100 mCameraManager.openCamera(mCameraId, mCameraDeviceStateListener, null); 101 } catch (CameraAccessException | IllegalStateException e) { 102 Log.e(TAG, "Failed to open camera " + mCameraId + ". Got:", e); 103 } 104 Log.d(TAG, "Initialize MediaRecorder."); 105 mRecorder = new MediaRecorder(); 106 } 107 closeCamera()108 void closeCamera() { 109 if (mCameraDevice != null) { 110 mCameraDevice.close(); 111 mCameraDevice = null; 112 mIsCameraConnected = false; 113 Log.d(TAG, "Closed camera " + mCameraId); 114 } 115 Log.d(TAG, "Release MediaRecorder."); 116 mRecorder.reset(); 117 mRecorder.release(); 118 } 119 startPreviewSession()120 void startPreviewSession() { 121 mIsPreviewSessionRequested = true; 122 mConfigHandler.sendEmptyMessage(MSG_SESSION_REQUESTED); 123 } 124 startRecordingSession(String filePrefix)125 void startRecordingSession(String filePrefix) { 126 mIsRecordingSessionRequested = true; 127 mVideoFilePath = String.format("%s_id_%s.mp4", filePrefix, mCameraId); 128 Log.d(TAG, String.format( 129 "Video recording path for camera %s set to %s", mCameraId, mVideoFilePath)); 130 setupMediaRecorder(); 131 mConfigHandler.sendEmptyMessage(MSG_SESSION_REQUESTED); 132 } 133 setupMediaRecorder()134 private void setupMediaRecorder() { 135 mRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); 136 mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); 137 mRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); 138 mRecorder.setVideoSize(VIDEO_SIZE.getWidth(), VIDEO_SIZE.getHeight()); 139 mRecorder.setVideoFrameRate(VIDEO_FRAME_RATE); 140 mRecorder.setVideoEncodingBitRate(VIDEO_BIT_RATE); 141 mRecorder.setOutputFile(mVideoFilePath); 142 try { 143 mRecorder.prepare(); 144 } catch (IllegalStateException | IOException e) { 145 Log.e(TAG, "Unable to prepare media recorder with camera " + mCameraId, e); 146 } 147 } 148 stopSession()149 void stopSession() { 150 if (mIsRecordingSessionRequested || mIsRecordingSessionCreated) { 151 Log.d(TAG, "Attempting to stop current recording session of camera " + mCameraId); 152 try { 153 mRecorder.stop(); 154 } catch (IllegalStateException e) { 155 Log.e(TAG, "Attempting to stop recording that wasn't started, got: ", e); 156 } catch (RuntimeException e) { 157 Log.e(TAG, "Received no data during recording, got: ", e); 158 } 159 mRecorder.reset(); 160 } else { 161 Log.d(TAG, "Attempting to stop current preview session of camera " + mCameraId); 162 } 163 try { 164 if (mCaptureSession != null) { 165 mCaptureSession.stopRepeating(); 166 mCaptureSession.close(); 167 mCaptureSession = null; 168 } 169 Log.d(TAG, "Stopped capture session " + mCameraId); 170 } catch (Exception e) { 171 Log.e(TAG, "Exception caught while stopping camera session " + mCameraId, e); 172 } 173 mIsPreviewSessionRequested = false; 174 mIsPreviewSessionCreated = false; 175 mIsRecordingSessionRequested = false; 176 mIsRecordingSessionCreated = false; 177 } 178 getFrameCountOfLastSession()179 long getFrameCountOfLastSession() { 180 return mSessionCaptureListener.getFrameCount(); 181 } 182 183 final class SurfacePreviewListener implements SurfaceHolder.Callback { 184 185 @Override surfaceCreated(@onNull SurfaceHolder holder)186 public void surfaceCreated(@NonNull SurfaceHolder holder) { 187 Log.d(TAG, "Surface created"); 188 mIsSurfaceCreated = true; 189 mConfigHandler.sendEmptyMessage(MSG_SURFACE_READY); 190 } 191 192 @Override surfaceChanged( @onNull SurfaceHolder holder, int format, int width, int height)193 public void surfaceChanged( 194 @NonNull SurfaceHolder holder, int format, int width, int height) { 195 Log.d(TAG, "Surface Changed to: " + width + "x" + height); 196 } 197 198 @Override surfaceDestroyed(@onNull SurfaceHolder holder)199 public void surfaceDestroyed(@NonNull SurfaceHolder holder) { 200 Log.d(TAG, "Surface destroyed"); 201 mIsSurfaceCreated = false; 202 } 203 } 204 205 static final class SessionExecutor implements Executor { 206 private final Handler mExecutorHandler; 207 SessionExecutor(Handler handler)208 SessionExecutor(Handler handler) { 209 mExecutorHandler = handler; 210 } 211 212 @Override execute(Runnable runCmd)213 public void execute(Runnable runCmd) { 214 mExecutorHandler.post(runCmd); 215 } 216 } 217 218 final class CameraDeviceStateListener extends CameraDevice.StateCallback { 219 220 @Override onOpened(@onNull CameraDevice camera)221 public void onOpened(@NonNull CameraDevice camera) { 222 Log.d(TAG, "Camera Opened"); 223 mCameraDevice = camera; 224 mIsCameraConnected = true; 225 mConfigHandler.sendEmptyMessage(MSG_CAMERA_OPENED); 226 } 227 228 @Override onDisconnected(@onNull CameraDevice camera)229 public void onDisconnected(@NonNull CameraDevice camera) { 230 mIsCameraConnected = false; 231 } 232 233 @Override onClosed(@onNull CameraDevice camera)234 public void onClosed(@NonNull CameraDevice camera) { 235 mIsCameraConnected = false; 236 } 237 238 @Override onError(@onNull CameraDevice camera, int error)239 public void onError(@NonNull CameraDevice camera, int error) {} 240 } 241 242 final class CameraSurfaceInitListener implements Handler.Callback { 243 @Override handleMessage(@onNull Message msg)244 public boolean handleMessage(@NonNull Message msg) { 245 switch(msg.what) { 246 case MSG_CAMERA_OPENED: 247 case MSG_SURFACE_READY: 248 case MSG_SESSION_REQUESTED: 249 if ((mCameraDevice != null) && mIsCameraConnected && mIsSurfaceCreated) { 250 // Camera and surfaces ready to start new capture session 251 if (mIsPreviewSessionRequested && !mIsPreviewSessionCreated) { 252 // Preview session requested but not created 253 Log.d(TAG, "All conditions satisfied, starting preview session."); 254 createPreviewSession(); 255 } else if (mIsRecordingSessionRequested && !mIsRecordingSessionCreated) { 256 // Recording session requested but not created 257 Log.d(TAG, "All conditions satisfied, starting recording session."); 258 createRecordingSession(); 259 } 260 } 261 } 262 return true; 263 } 264 } 265 createPreviewSession()266 private void createPreviewSession() { 267 try { 268 CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest( 269 CameraDevice.TEMPLATE_PREVIEW); 270 captureRequestBuilder.addTarget(mSurfaceView.getHolder().getSurface()); 271 mCaptureRequest = captureRequestBuilder.build(); 272 273 List<OutputConfiguration> outputs = new ArrayList<>(); 274 outputs.add(new OutputConfiguration(mSurfaceView.getHolder().getSurface())); 275 SessionConfiguration sessionConfig = new SessionConfiguration( 276 SessionConfiguration.SESSION_REGULAR, outputs, 277 new SessionExecutor(mSessionHandler), mSessionStateListener); 278 279 sessionConfig.setSessionParameters(mCaptureRequest); 280 mCameraDevice.createCaptureSession(sessionConfig); 281 mIsPreviewSessionCreated = true; 282 Log.d(TAG, "Created preview capture session for camera " + mCameraId); 283 } catch (CameraAccessException | IllegalStateException e) { 284 Log.e(TAG, "Failed to create preview capture session with camera " + mCameraId, e); 285 } 286 } 287 createRecordingSession()288 private void createRecordingSession() { 289 try { 290 291 Log.d(TAG, String.format( 292 "Recorder for camera %s is valid? %b", 293 mCameraId, mRecorder.getSurface().isValid())); 294 295 CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest( 296 CameraDevice.TEMPLATE_RECORD); 297 captureRequestBuilder.addTarget(mSurfaceView.getHolder().getSurface()); 298 captureRequestBuilder.addTarget(mRecorder.getSurface()); 299 mCaptureRequest = captureRequestBuilder.build(); 300 301 List<OutputConfiguration> outputs = new ArrayList<>(); 302 outputs.add(new OutputConfiguration(mSurfaceView.getHolder().getSurface())); 303 outputs.add(new OutputConfiguration(mRecorder.getSurface())); 304 SessionConfiguration sessionConfig = new SessionConfiguration( 305 SessionConfiguration.SESSION_REGULAR, outputs, 306 new SessionExecutor(mSessionHandler), mSessionStateListener); 307 308 sessionConfig.setSessionParameters(mCaptureRequest); 309 mCameraDevice.createCaptureSession(sessionConfig); 310 mIsRecordingSessionCreated = true; 311 Log.d(TAG, "Created recording capture session for camera " + mCameraId); 312 } catch (CameraAccessException | IllegalStateException e) { 313 Log.e(TAG, "Failed to create recording capture session with camera " + mCameraId, e); 314 } 315 } 316 317 318 final class SessionStateListener extends CameraCaptureSession.StateCallback { 319 @Override onConfigured(@onNull CameraCaptureSession session)320 public void onConfigured(@NonNull CameraCaptureSession session) { 321 mCaptureSession = session; 322 try { 323 mSessionCaptureListener.resetFrameCount(); 324 mCaptureSession.setRepeatingRequest( 325 mCaptureRequest, mSessionCaptureListener, mSessionHandler); 326 if (mIsPreviewSessionRequested) { 327 Log.d(TAG, "Successfully started recording session with camera " + mCameraId); 328 } else if (mIsRecordingSessionRequested) { 329 mRecorder.start(); 330 Log.d(TAG, "Successfully started recording session with camera " + mCameraId); 331 } 332 Log.d(TAG, "Successfully started recording session with camera " + mCameraId); 333 } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { 334 Log.e(TAG, "Failed to start camera preview with camera " + mCameraId, e); 335 } 336 } 337 338 @Override onConfigureFailed(@onNull CameraCaptureSession session)339 public void onConfigureFailed(@NonNull CameraCaptureSession session) { 340 Log.e(TAG, "Failed to configure session with camera " + mCameraId); 341 } 342 } 343 344 static final class SessionCaptureListener extends CameraCaptureSession.CaptureCallback { 345 private final AtomicLong mFrameCount = new AtomicLong(0); 346 347 @Override onCaptureCompleted( @onNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result)348 public void onCaptureCompleted( 349 @NonNull CameraCaptureSession session, 350 @NonNull CaptureRequest request, 351 @NonNull TotalCaptureResult result) { 352 mFrameCount.incrementAndGet(); 353 } 354 resetFrameCount()355 public void resetFrameCount() { 356 mFrameCount.set(0); 357 } 358 getFrameCount()359 public long getFrameCount() { 360 return mFrameCount.get(); 361 } 362 } 363 } 364