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