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