1 /* <lambda>null2 * Copyright 2023 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.view 18 19 import android.annotation.SuppressLint 20 import android.content.Context 21 import android.content.pm.ActivityInfo 22 import android.os.Bundle 23 import android.view.OrientationEventListener 24 import android.widget.Button 25 import androidx.annotation.OptIn 26 import androidx.appcompat.app.AppCompatActivity 27 import androidx.camera.camera2.Camera2Config 28 import androidx.camera.camera2.pipe.integration.CameraPipeConfig 29 import androidx.camera.core.Camera 30 import androidx.camera.core.CameraSelector 31 import androidx.camera.core.CameraXConfig 32 import androidx.camera.core.ImageAnalysis 33 import androidx.camera.core.ImageCapture 34 import androidx.camera.core.Logger 35 import androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY 36 import androidx.camera.core.Preview 37 import androidx.camera.core.UseCase 38 import androidx.camera.core.impl.utils.executor.CameraXExecutors 39 import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration 40 import androidx.camera.lifecycle.ProcessCameraProvider 41 import androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore 42 import androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions 43 import androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions 44 import androidx.camera.testing.impl.FileUtil.writeTextToExternalFile 45 import androidx.camera.video.PendingRecording 46 import androidx.camera.video.Recorder 47 import androidx.camera.video.Recording 48 import androidx.camera.video.VideoCapture 49 import androidx.camera.video.VideoRecordEvent 50 import androidx.camera.view.PreviewView 51 import androidx.camera.view.PreviewView.ImplementationMode 52 import androidx.core.content.ContextCompat 53 import androidx.core.util.Consumer 54 55 private const val TAG = "StreamSharingActivity" 56 private const val PREFIX_INFORMATION = "test_information" 57 private const val PREFIX_VIDEO = "video" 58 private const val KEY_ORIENTATION = "device_orientation" 59 private const val KEY_STREAM_SHARING_STATE = "is_stream_sharing_enabled" 60 61 // Possible values for this intent key (case-insensitive): "portrait", "landscape". 62 private const val INTENT_SCREEN_ORIENTATION = "orientation" 63 private const val SCREEN_ORIENTATION_PORTRAIT = "portrait" 64 private const val SCREEN_ORIENTATION_LANDSCAPE = "landscape" 65 66 // Possible values for this intent key (case-insensitive): "back", "front". 67 private const val INTENT_EXTRA_CAMERA_DIRECTION = "camera_direction" 68 private const val CAMERA_DIRECTION_BACK = "back" 69 private const val CAMERA_DIRECTION_FRONT = "front" 70 71 // Possible values for this intent key (case-insensitive): "compatible", "performance". 72 private const val INTENT_PREVIEW_VIEW_MODE = "preview_view_mode" 73 private const val PREVIEW_VIEW_COMPATIBLE_MODE = "compatible" 74 private const val PREVIEW_VIEW_PERFORMANCE_MODE = "performance" 75 76 // Possible values for this intent key (case-insensitive): "camera2", "camera_pipe". 77 private const val INTENT_EXTRA_CAMERA_IMPLEMENTATION = "camera_implementation" 78 private const val CAMERA_IMPLEMENTATION_CAMERA2 = "camera2" 79 private const val CAMERA_IMPLEMENTATION_CAMERA_PIPE = "camera_pipe" 80 81 class StreamSharingActivity : AppCompatActivity() { 82 83 private lateinit var previewView: PreviewView 84 private lateinit var exportButton: Button 85 private lateinit var recordButton: Button 86 private lateinit var useCases: Array<UseCase> 87 private var cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA 88 private var cameraXConfig: CameraXConfig = Camera2Config.defaultConfig() 89 private var camera: Camera? = null 90 private var previewViewMode: ImplementationMode = ImplementationMode.PERFORMANCE 91 private var previewViewScaleType = PreviewView.ScaleType.FILL_CENTER 92 93 private var activeRecording: Recording? = null 94 private var isUseCasesBound: Boolean = false 95 private var deviceOrientation: Int = -1 96 private val orientationEventListener by lazy { 97 object : OrientationEventListener(applicationContext) { 98 override fun onOrientationChanged(orientation: Int) { 99 deviceOrientation = orientation 100 } 101 } 102 } 103 104 override fun onCreate(savedInstanceState: Bundle?) { 105 super.onCreate(savedInstanceState) 106 setContentView(R.layout.activity_stream_sharing) 107 108 // Apply settings from intent. 109 val bundle = intent.extras 110 if (bundle != null) { 111 parseScreenOrientationAndSetValueIfNeed(bundle) 112 parseCameraSelector(bundle) 113 parseCameraImplementation(bundle) 114 parsePreviewViewMode(bundle) 115 } 116 117 // Initial view objects. 118 previewView = findViewById(R.id.preview_view) 119 previewView.scaleType = previewViewScaleType 120 previewView.implementationMode = previewViewMode 121 exportButton = findViewById(R.id.export_button) 122 exportButton.setOnClickListener { exportTestInformation() } 123 recordButton = findViewById(R.id.record_button) 124 recordButton.setOnClickListener { 125 if (activeRecording == null) startRecording() else stopRecording() 126 } 127 128 if (!isCameraXConfigured) { 129 isCameraXConfigured = true 130 configureCameraProvider() 131 } 132 startCamera() 133 } 134 135 override fun onResume() { 136 super.onResume() 137 orientationEventListener.enable() 138 } 139 140 override fun onPause() { 141 super.onPause() 142 orientationEventListener.disable() 143 } 144 145 private fun parseScreenOrientationAndSetValueIfNeed(bundle: Bundle) { 146 val orientationString = bundle.getString(INTENT_SCREEN_ORIENTATION) 147 if (SCREEN_ORIENTATION_PORTRAIT.equals(orientationString, true)) { 148 requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT 149 } else if (SCREEN_ORIENTATION_LANDSCAPE.equals(orientationString, true)) { 150 requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE 151 } 152 } 153 154 private fun parseCameraSelector(bundle: Bundle) { 155 val cameraDirection = bundle.getString(INTENT_EXTRA_CAMERA_DIRECTION) 156 if (CAMERA_DIRECTION_BACK.equals(cameraDirection, true)) { 157 cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA 158 } else if (CAMERA_DIRECTION_FRONT.equals(cameraDirection, true)) { 159 cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA 160 } 161 } 162 163 private fun parseCameraImplementation(bundle: Bundle) { 164 val implementation = bundle.getString(INTENT_EXTRA_CAMERA_IMPLEMENTATION) 165 if (CAMERA_IMPLEMENTATION_CAMERA2.equals(implementation, true)) { 166 cameraXConfig = Camera2Config.defaultConfig() 167 } else if (CAMERA_IMPLEMENTATION_CAMERA_PIPE.equals(implementation, true)) { 168 cameraXConfig = CameraPipeConfig.defaultConfig() 169 } 170 } 171 172 private fun parsePreviewViewMode(bundle: Bundle) { 173 val mode = bundle.getString(INTENT_PREVIEW_VIEW_MODE) 174 if (PREVIEW_VIEW_COMPATIBLE_MODE.equals(mode, true)) { 175 previewViewMode = ImplementationMode.COMPATIBLE 176 } else if (PREVIEW_VIEW_PERFORMANCE_MODE.equals(mode, true)) { 177 previewViewMode = ImplementationMode.PERFORMANCE 178 } 179 } 180 181 @SuppressLint("NullAnnotationGroup") 182 @OptIn(ExperimentalCameraProviderConfiguration::class) 183 private fun configureCameraProvider() { 184 ProcessCameraProvider.configureInstance(cameraXConfig) 185 } 186 187 private fun startCamera() { 188 val cameraProviderFuture = ProcessCameraProvider.getInstance(applicationContext) 189 cameraProviderFuture.addListener( 190 { bindUseCases(cameraProviderFuture.get()) }, 191 ContextCompat.getMainExecutor(applicationContext) 192 ) 193 } 194 195 private fun bindUseCases(cameraProvider: ProcessCameraProvider) { 196 enableRecording(false) 197 isUseCasesBound = false 198 cameraProvider.unbindAll() 199 useCases = 200 arrayOf( 201 createPreview(), 202 createImageCapture(), 203 createImageAnalysis(), 204 createVideoCapture() 205 ) 206 isUseCasesBound = 207 try { 208 camera = cameraProvider.bindToLifecycle(this, cameraSelector, *useCases) 209 enableRecording(true) 210 true 211 } catch (exception: Exception) { 212 Logger.e(TAG, "Failed to bind use cases.", exception) 213 false 214 } 215 } 216 217 private fun createPreview(): Preview { 218 return Preview.Builder().build().apply { setSurfaceProvider(previewView.surfaceProvider) } 219 } 220 221 private fun createImageCapture(): ImageCapture { 222 return ImageCapture.Builder().build() 223 } 224 225 private fun createImageAnalysis(): ImageAnalysis { 226 return ImageAnalysis.Builder().build() 227 } 228 229 private fun createVideoCapture(): VideoCapture<Recorder> { 230 val recorder = Recorder.Builder().build() 231 return VideoCapture.Builder(recorder).setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY).build() 232 } 233 234 @Suppress("UNCHECKED_CAST") 235 private fun getVideoCapture(): VideoCapture<Recorder>? { 236 return findUseCase(VideoCapture::class.java) as VideoCapture<Recorder>? 237 } 238 239 private fun <T : UseCase?> findUseCase(useCaseSubclass: Class<T>): T? { 240 for (useCase in useCases) { 241 if (useCaseSubclass.isInstance(useCase)) { 242 return useCaseSubclass.cast(useCase) 243 } 244 } 245 return null 246 } 247 248 private fun isStreamSharingEnabled(): Boolean { 249 val isCombinationSupported = 250 camera != null && camera!!.isUseCasesCombinationSupportedByFramework(*useCases) 251 return !isCombinationSupported && isUseCasesBound 252 } 253 254 private fun enableRecording(enabled: Boolean) { 255 recordButton.isEnabled = enabled 256 } 257 258 @SuppressLint("MissingPermission") 259 private fun startRecording() { 260 recordButton.text = getString(R.string.btn_video_stop_recording) 261 activeRecording = 262 getVideoCapture()!!.let { 263 prepareRecording(applicationContext, it.output) 264 .withAudioEnabled() 265 .start(CameraXExecutors.directExecutor(), generateVideoRecordEventListener()) 266 } 267 } 268 269 private fun stopRecording() { 270 recordButton.text = getString(R.string.btn_video_record) 271 activeRecording!!.stop() 272 activeRecording = null 273 } 274 275 private fun prepareRecording(context: Context, recorder: Recorder): PendingRecording { 276 val fileName = generateFileName(PREFIX_VIDEO) 277 278 return if (canDeviceWriteToMediaStore()) { 279 recorder.prepareRecording( 280 context, 281 generateVideoMediaStoreOptions(context.contentResolver, fileName) 282 ) 283 } else { 284 recorder.prepareRecording(context, generateVideoFileOutputOptions(fileName)) 285 } 286 } 287 288 private fun exportTestInformation() { 289 val fileName = generateFileName(PREFIX_INFORMATION) 290 val information = 291 "$KEY_ORIENTATION:$deviceOrientation" + 292 "\n" + 293 "$KEY_STREAM_SHARING_STATE:${isStreamSharingEnabled()}" 294 295 writeTextToExternalFile(information, fileName) 296 } 297 298 private fun generateFileName(prefix: String? = null): String { 299 val timeMillis = System.currentTimeMillis() 300 return if (prefix != null) "${prefix}_$timeMillis" else "$timeMillis" 301 } 302 303 private fun generateVideoRecordEventListener(): Consumer<VideoRecordEvent> { 304 return Consumer<VideoRecordEvent> { event -> 305 if (event is VideoRecordEvent.Finalize) { 306 val uri = event.outputResults.outputUri 307 when (event.error) { 308 VideoRecordEvent.Finalize.ERROR_NONE, 309 VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED, 310 VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED, 311 VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE, 312 VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE -> 313 Logger.d(TAG, "Video saved to: $uri") 314 else -> 315 Logger.e(TAG, "Failed to save video: uri $uri with code (${event.error})") 316 } 317 } 318 } 319 } 320 321 companion object { 322 private var isCameraXConfigured: Boolean = false 323 } 324 } 325