1 /*
<lambda>null2  * Copyright 2022 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 androidx.camera.integration.avsync.model
18 
19 import android.annotation.SuppressLint
20 import android.content.Context
21 import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
22 import android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
23 import android.hardware.camera2.CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE
24 import android.util.Log
25 import android.util.Range
26 import androidx.annotation.MainThread
27 import androidx.annotation.OptIn
28 import androidx.camera.camera2.interop.Camera2CameraInfo as C2CameraInfo
29 import androidx.camera.camera2.interop.Camera2Interop as C2Interop
30 import androidx.camera.camera2.interop.ExperimentalCamera2Interop
31 import androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo as CPCameraInfo
32 import androidx.camera.camera2.pipe.integration.interop.Camera2Interop as CPInterop
33 import androidx.camera.core.CameraInfo
34 import androidx.camera.core.CameraSelector
35 import androidx.camera.lifecycle.ProcessCameraProvider
36 import androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore
37 import androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions
38 import androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions
39 import androidx.camera.video.PendingRecording
40 import androidx.camera.video.Recorder
41 import androidx.camera.video.Recording
42 import androidx.camera.video.VideoCapture
43 import androidx.camera.video.VideoRecordEvent
44 import androidx.concurrent.futures.await
45 import androidx.core.content.ContextCompat
46 import androidx.core.util.Consumer
47 import androidx.lifecycle.LifecycleOwner
48 
49 private const val TAG = "CameraHelper"
50 
51 class CameraHelper(private val cameraImplementation: CameraImplementation) {
52 
53     private val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
54     private var videoCapture: VideoCapture<Recorder>? = null
55     private var activeRecording: Recording? = null
56 
57     @MainThread
58     suspend fun bindCamera(context: Context, lifecycleOwner: LifecycleOwner): Boolean {
59         val cameraProvider = ProcessCameraProvider.getInstance(context).await()
60 
61         return try {
62             // Binds to lifecycle without use cases to get camera info for necessary checks.
63             val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector)
64             videoCapture = createVideoCapture(camera.cameraInfo)
65 
66             cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, videoCapture)
67             true
68         } catch (exception: Exception) {
69             Log.e(TAG, "Camera binding failed", exception)
70             videoCapture = null
71             false
72         }
73     }
74 
75     /**
76      * Start video recording.
77      *
78      * <p> For E2E test, permissions will be handled by the launch script.
79      */
80     @SuppressLint("MissingPermission")
81     fun startRecording(context: Context, eventListener: Consumer<VideoRecordEvent>? = null) {
82         activeRecording =
83             videoCapture!!.let {
84                 val listener = eventListener ?: generateVideoRecordEventListener()
85                 prepareRecording(context, it.output)
86                     .withAudioEnabled()
87                     .start(ContextCompat.getMainExecutor(context), listener)
88             }
89     }
90 
91     fun stopRecording() {
92         activeRecording!!.stop()
93         activeRecording = null
94     }
95 
96     fun pauseRecording() {
97         activeRecording!!.pause()
98     }
99 
100     fun resumeRecording() {
101         activeRecording!!.resume()
102     }
103 
104     private fun createVideoCapture(cameraInfo: CameraInfo): VideoCapture<Recorder> {
105         val recorder = Recorder.Builder().build()
106         val videoCaptureBuilder = VideoCapture.Builder<Recorder>(recorder)
107         if (isLegacyDevice(cameraInfo, cameraImplementation)) {
108             // Set target FPS to 30 on legacy devices. Legacy devices use lower FPS to
109             // workaround exposure issues, but this makes the video timestamp info become
110             // fewer and causes A/V sync test to false alarm. See AeFpsRangeLegacyQuirk.
111             videoCaptureBuilder.setTargetFpsRange(FPS_30, cameraImplementation)
112         }
113 
114         return videoCaptureBuilder.build()
115     }
116 
117     companion object {
118         enum class CameraImplementation {
119             CAMERA2,
120             CAMERA_PIPE
121         }
122 
123         private val FPS_30 = Range(30, 30)
124 
125         @SuppressLint("NullAnnotationGroup")
126         @OptIn(ExperimentalCamera2Interop::class)
127         @kotlin.OptIn(
128             androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop::class
129         )
130         private fun VideoCapture.Builder<Recorder>.setTargetFpsRange(
131             range: Range<Int>,
132             cameraImplementation: CameraImplementation
133         ): VideoCapture.Builder<Recorder> {
134             Log.d(TAG, "Set target fps to $range")
135             when (cameraImplementation) {
136                 CameraImplementation.CAMERA2 -> {
137                     C2Interop.Extender(this)
138                         .setCaptureRequestOption(CONTROL_AE_TARGET_FPS_RANGE, range)
139                 }
140                 CameraImplementation.CAMERA_PIPE -> {
141                     CPInterop.Extender(this)
142                         .setCaptureRequestOption(CONTROL_AE_TARGET_FPS_RANGE, range)
143                 }
144             }
145 
146             return this
147         }
148 
149         @SuppressLint("NullAnnotationGroup")
150         @OptIn(ExperimentalCamera2Interop::class)
151         @kotlin.OptIn(
152             androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop::class
153         )
154         private fun isLegacyDevice(
155             cameraInfo: CameraInfo,
156             cameraImplementation: CameraImplementation
157         ): Boolean {
158             val hardwareLevel =
159                 when (cameraImplementation) {
160                     CameraImplementation.CAMERA2 ->
161                         C2CameraInfo.from(cameraInfo)
162                             .getCameraCharacteristic(INFO_SUPPORTED_HARDWARE_LEVEL)
163                     CameraImplementation.CAMERA_PIPE ->
164                         CPCameraInfo.from(cameraInfo)
165                             .getCameraCharacteristic(INFO_SUPPORTED_HARDWARE_LEVEL)
166                 }
167 
168             return hardwareLevel == INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
169         }
170 
171         private fun prepareRecording(context: Context, recorder: Recorder): PendingRecording {
172             val fileName = generateVideoFileName()
173             return if (canDeviceWriteToMediaStore()) {
174                 recorder.prepareRecording(
175                     context,
176                     generateVideoMediaStoreOptions(context.contentResolver, fileName)
177                 )
178             } else {
179                 recorder.prepareRecording(context, generateVideoFileOutputOptions(fileName))
180             }
181         }
182 
183         private fun generateVideoFileName(): String {
184             return "video_" + System.currentTimeMillis()
185         }
186 
187         private fun generateVideoRecordEventListener(): Consumer<VideoRecordEvent> {
188             return Consumer { videoRecordEvent ->
189                 if (videoRecordEvent is VideoRecordEvent.Finalize) {
190                     val uri = videoRecordEvent.outputResults.outputUri
191                     if (videoRecordEvent.error == VideoRecordEvent.Finalize.ERROR_NONE) {
192                         Log.d(TAG, "Video saved to: $uri")
193                     } else {
194                         val msg = "save to uri $uri with error code (${videoRecordEvent.error})"
195                         Log.e(TAG, "Failed to save video: $msg")
196                     }
197                 }
198             }
199         }
200     }
201 }
202