Merge "Remove the legacy core.VideoCapture API" into androidx-main
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 2249cca..da13be6 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -714,39 +714,6 @@
}
@Test
- fun legacyVideo_suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, mockCameraMetadata, cameraId,
- mockCamcorderProfileAdapter
- )
- val imageCapture = ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val preview = Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(imageCapture)
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- assertThrows(IllegalArgumentException::class.java) {
- supportedSurfaceCombination.getSuggestedResolutions(
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
- }
- }
-
- @Test
fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
val supportedSurfaceCombination = SupportedSurfaceCombination(
@@ -778,46 +745,6 @@
}
@Test
- fun legacyVideo_suggestedResForCustomizeResolutionsNotSupportedInLegacyDevice() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, mockCameraMetadata, cameraId,
- mockCamcorderProfileAdapter
- )
-
- // Legacy camera only support (PRIV, PREVIEW) + (PRIV, PREVIEW)
- val videoResolutionsPairs = listOf(
- Pair.create(ImageFormat.PRIVATE, arrayOf(recordSize))
- )
- val previewResolutionsPairs = listOf(
- Pair.create(ImageFormat.PRIVATE, arrayOf(previewSize))
- )
- // Override the default max resolution in VideoCapture
- val videoCapture =
- androidx.camera.core.VideoCapture.Builder()
- .setMaxResolution(recordSize)
- .setSupportedResolutions(videoResolutionsPairs)
- .build()
- val preview = Preview.Builder()
- .setSupportedResolutions(previewResolutionsPairs)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- assertThrows(IllegalArgumentException::class.java) {
- supportedSurfaceCombination.getSuggestedResolutions(
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
- }
- }
-
- @Test
fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice() {
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
val supportedSurfaceCombination = SupportedSurfaceCombination(
@@ -850,52 +777,6 @@
}
}
- @Test
- fun legacyVideo_getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, mockCameraMetadata, cameraId,
- mockCamcorderProfileAdapter
- )
- val imageCapture = ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val preview = Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(imageCapture)
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
- supportedSurfaceCombination.getSuggestedResolutions(
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
-
- // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
- Truth.assertThat(suggestedResolutionMap).containsEntry(
- useCaseToConfigMap[imageCapture],
- recordSize
- )
- Truth.assertThat(suggestedResolutionMap).containsEntry(
- useCaseToConfigMap[videoCapture],
- legacyVideoMaximumVideoSize
- )
- Truth.assertThat(suggestedResolutionMap).containsEntry(
- useCaseToConfigMap[preview],
- previewSize
- )
- }
-
// (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
@Test
fun suggestedResolutionsForMixedUseCaseInLimitedDevice() {
@@ -1129,60 +1010,6 @@
}
@Test
- fun legacyVideo_getSuggestedResolutionsForCustomizedSupportedResolutions() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, mockCameraMetadata, cameraId,
- mockCamcorderProfileAdapter
- )
- val formatResolutionsPairList: MutableList<Pair<Int, Array<Size>>> = ArrayList()
- formatResolutionsPairList.add(Pair.create(ImageFormat.JPEG, arrayOf(vgaSize)))
- formatResolutionsPairList.add(
- Pair.create(ImageFormat.YUV_420_888, arrayOf(vgaSize))
- )
- formatResolutionsPairList.add(Pair.create(ImageFormat.PRIVATE, arrayOf(vgaSize)))
-
- // Sets use cases customized supported resolutions to 640x480 only.
- val imageCapture = ImageCapture.Builder()
- .setSupportedResolutions(formatResolutionsPairList)
- .build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder()
- .setSupportedResolutions(formatResolutionsPairList)
- .build()
- val preview = Preview.Builder()
- .setSupportedResolutions(formatResolutionsPairList)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(imageCapture)
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
- supportedSurfaceCombination.getSuggestedResolutions(
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
-
- // Checks all suggested resolutions will become 640x480.
- Truth.assertThat(suggestedResolutionMap).containsEntry(
- useCaseToConfigMap[imageCapture],
- vgaSize
- )
- Truth.assertThat(suggestedResolutionMap).containsEntry(
- useCaseToConfigMap[videoCapture],
- vgaSize
- )
- Truth.assertThat(suggestedResolutionMap).containsEntry(
- useCaseToConfigMap[preview],
- vgaSize
- )
- }
-
- @Test
fun suggestedResolutionsForCustomizedSupportedResolutions() {
// Checks all suggested resolutions will become 640x480.
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.kt
deleted file mode 100644
index b8b5535..0000000
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.kt
+++ /dev/null
@@ -1,376 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2
-
-import android.Manifest
-import android.content.ContentResolver
-import android.content.ContentValues
-import android.content.Context
-import android.graphics.SurfaceTexture
-import android.media.MediaRecorder
-import android.os.Build
-import android.os.ParcelFileDescriptor
-import android.provider.MediaStore
-import android.util.Size
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.Logger
-import androidx.camera.core.Preview
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.internal.CameraUseCaseAdapter
-import androidx.camera.testing.AudioUtil
-import androidx.camera.testing.CameraUtil
-import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
-import androidx.camera.testing.SurfaceTextureProvider.createSurfaceTextureProvider
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.GrantPermissionRule
-import androidx.testutils.assertThrows
-import com.google.common.truth.Truth.assertThat
-import java.io.File
-import java.util.concurrent.TimeUnit
-import org.junit.After
-import org.junit.Assume.assumeFalse
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.timeout
-import org.mockito.Mockito.verify
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-@Suppress("DEPRECATION")
-@SdkSuppress(minSdkVersion = 21)
-class VideoCaptureTest {
- companion object {
- private const val TAG = "VideoCaptureTest"
- }
-
- @get:Rule
- val useRecordingResource = CameraUtil.checkVideoRecordingResource()
-
- @get:Rule
- val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
- PreTestCameraIdList(Camera2Config.defaultConfig())
- )
-
- @get:Rule
- val permissionRule: GrantPermissionRule =
- GrantPermissionRule.grant(
- Manifest.permission.WRITE_EXTERNAL_STORAGE,
- Manifest.permission.RECORD_AUDIO
- )
-
- private val instrumentation = InstrumentationRegistry.getInstrumentation()
-
- private val context = ApplicationProvider.getApplicationContext<Context>()
-
- private lateinit var cameraSelector: CameraSelector
-
- private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
-
- private lateinit var contentResolver: ContentResolver
-
- @Before
- fun setUp() {
- // TODO(b/168175357): Fix VideoCaptureTest problems on CuttleFish API 29
- assumeFalse(
- "Cuttlefish has MediaCodec dequeueInput/Output buffer fails issue. Unable to test.",
- Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
- )
-
- assumeTrue(CameraUtil.deviceHasCamera())
- assumeTrue(AudioUtil.canStartAudioRecord(MediaRecorder.AudioSource.CAMCORDER))
-
- cameraSelector = if (CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK)) {
- CameraSelector.DEFAULT_BACK_CAMERA
- } else {
- CameraSelector.DEFAULT_FRONT_CAMERA
- }
-
- CameraXUtil.initialize(
- context,
- Camera2Config.defaultConfig()
- ).get()
- cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
-
- contentResolver = context.contentResolver
- }
-
- @After
- fun tearDown() {
- instrumentation.runOnMainSync {
- if (this::cameraUseCaseAdapter.isInitialized) {
- cameraUseCaseAdapter.removeUseCases(cameraUseCaseAdapter.useCases)
- }
- }
-
- CameraXUtil.shutdown().get(10000, TimeUnit.MILLISECONDS)
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 25)
- fun buildFileOutputOptionsWithFileDescriptor_throwExceptionWhenAPILevelSmallerThan26() {
- val file = File.createTempFile("CameraX", ".tmp").apply {
- deleteOnExit()
- }
-
- val fileDescriptor =
- ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE).fileDescriptor
-
- assertThrows<IllegalArgumentException> {
- androidx.camera.core.VideoCapture.OutputFileOptions.Builder(fileDescriptor).build()
- }
-
- file.delete()
- }
-
- @Test(timeout = 30000)
- @SdkSuppress(minSdkVersion = 26)
- fun startRecordingWithFileDescriptor_whenAPILevelLargerThan26() {
- val file = File.createTempFile("CameraX", ".tmp").apply {
- deleteOnExit()
- }
-
- // It's needed to have a variable here to hold the parcel file descriptor reference which
- // returned from ParcelFileDescriptor.open(), the returned parcel descriptor reference might
- // be garbage collected unexpectedly. That will caused an "invalid file descriptor" issue.
- val parcelFileDescriptor =
- ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
- val fileDescriptor = parcelFileDescriptor.fileDescriptor
-
- val preview = Preview.Builder().build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
- assumeTrue(
- "This combination (videoCapture, preview) is not supported.",
- cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
- )
-
- instrumentation.runOnMainSync {
- preview.setSurfaceProvider(
- CameraXExecutors.mainThreadExecutor(),
- getSurfaceProvider()
- )
- // b/168187087 if there is only VideoCapture , VideoCapture will failed when setting the
- // repeating request with the surface, the workaround is binding one more useCase
- // Preview.
- cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
- }
-
- val outputFileOptions =
- androidx.camera.core.VideoCapture.OutputFileOptions.Builder(fileDescriptor).build()
-
- val callback = mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
-
- // Start recording with FileDescriptor
- videoCapture.startRecording(
- outputFileOptions,
- CameraXExecutors.mainThreadExecutor(),
- callback
- )
-
- // Recording for seconds
- recordingUntilKeyFrameArrived(videoCapture)
-
- // Stop recording
- videoCapture.stopRecording()
-
- verify(callback, timeout(10000)).onVideoSaved(any())
- parcelFileDescriptor.close()
- file.delete()
- }
-
- @FlakyTest // b/182165222
- @Test(timeout = 30000)
- fun unbind_shouldStopRecording() {
- val file = File.createTempFile("CameraX", ".tmp").apply {
- deleteOnExit()
- }
-
- val preview = Preview.Builder().build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
- assumeTrue(
- "This combination (videoCapture, preview) is not supported.",
- cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
- )
- instrumentation.runOnMainSync {
- preview.setSurfaceProvider(
- CameraXExecutors.mainThreadExecutor(),
- getSurfaceProvider()
- )
- cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
- }
-
- val outputFileOptions =
- androidx.camera.core.VideoCapture.OutputFileOptions.Builder(file).build()
-
- val callback = mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
-
- videoCapture.startRecording(
- outputFileOptions,
- CameraXExecutors.mainThreadExecutor(),
- callback
- )
-
- recordingUntilKeyFrameArrived(videoCapture)
-
- instrumentation.runOnMainSync {
- cameraUseCaseAdapter.removeUseCases(listOf(videoCapture, preview))
- }
-
- verify(callback, timeout(10000)).onVideoSaved(any())
- file.delete()
- }
-
- @Test(timeout = 30000)
- @SdkSuppress(minSdkVersion = 26)
- fun startRecordingWithUri_whenAPILevelLargerThan26() {
- val preview = Preview.Builder().build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
- assumeTrue(
- "This combination (videoCapture, preview) is not supported.",
- cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
- )
- instrumentation.runOnMainSync {
- preview.setSurfaceProvider(
- CameraXExecutors.mainThreadExecutor(),
- getSurfaceProvider()
- )
- cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
- }
-
- val callback = mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
- videoCapture.startRecording(
- getNewVideoOutputFileOptions(contentResolver),
- CameraXExecutors.mainThreadExecutor(),
- callback
- )
- recordingUntilKeyFrameArrived(videoCapture)
-
- videoCapture.stopRecording()
-
- // Assert: Wait for the signal that the image has been saved.
- val outputFileResultsArgumentCaptor =
- ArgumentCaptor.forClass(
- androidx.camera.core.VideoCapture.OutputFileResults::class.java
- )
- verify(callback, timeout(10000)).onVideoSaved(outputFileResultsArgumentCaptor.capture())
-
- // get file path to remove it
- val saveLocationUri =
- outputFileResultsArgumentCaptor.value.savedUri
- assertThat(saveLocationUri).isNotNull()
-
- // Remove temp test file
- contentResolver.delete(saveLocationUri!!, null, null)
- }
-
- @Test(timeout = 30000)
- fun videoCapture_saveResultToFile() {
- val file = File.createTempFile("CameraX", ".tmp").apply {
- deleteOnExit()
- }
-
- val preview = Preview.Builder().build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
- assumeTrue(
- "This combination (videoCapture, preview) is not supported.",
- cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
- )
- instrumentation.runOnMainSync {
- preview.setSurfaceProvider(
- CameraXExecutors.mainThreadExecutor(),
- getSurfaceProvider()
- )
- cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
- }
-
- val callback = mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
- videoCapture.startRecording(
- androidx.camera.core.VideoCapture.OutputFileOptions.Builder(file).build(),
- CameraXExecutors.mainThreadExecutor(),
- callback
- )
-
- recordingUntilKeyFrameArrived(videoCapture)
- videoCapture.stopRecording()
-
- // Wait for the signal that the video has been saved.
- verify(callback, timeout(10000)).onVideoSaved(any())
- file.delete()
- }
-
- /** Return a VideoOutputFileOption which is used to save a video. */
- private fun getNewVideoOutputFileOptions(
- resolver: ContentResolver
- ): androidx.camera.core.VideoCapture.OutputFileOptions {
- val videoFileName = "video_" + System.currentTimeMillis()
- val contentValues = ContentValues().apply {
- put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
- put(MediaStore.Video.Media.TITLE, videoFileName)
- put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName)
- }
-
- return androidx.camera.core.VideoCapture.OutputFileOptions.Builder(
- resolver,
- MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues
- ).build()
- }
-
- private fun getSurfaceProvider(): Preview.SurfaceProvider {
- return createSurfaceTextureProvider(object : SurfaceTextureCallback {
- override fun onSurfaceTextureReady(surfaceTexture: SurfaceTexture, resolution: Size) {
- // No-op
- }
-
- override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
- surfaceTexture.release()
- }
- })
- }
-
- private fun recordingUntilKeyFrameArrived(videoCapture: androidx.camera.core.VideoCapture) {
- Logger.i(TAG, "recordingUntilKeyFrameArrived begins: " + System.nanoTime() / 1000)
- while (true) {
- if (videoCapture.mIsFirstVideoKeyFrameWrite.get() && videoCapture
- .mIsFirstAudioSampleWrite.get()
- ) {
- Logger.i(
- TAG,
- "Video Key Frame and audio frame Arrived: " + System.nanoTime() / 1000
- )
- break
- }
- Thread.sleep(100)
- }
- Logger.i(TAG, "recordingUntilKeyFrameArrived ends: " + System.nanoTime() / 1000)
- }
-}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTestWithoutAudioPermissionTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTestWithoutAudioPermissionTest.kt
deleted file mode 100644
index 64a717c..0000000
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTestWithoutAudioPermissionTest.kt
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2
-
-import android.Manifest
-import android.content.ContentResolver
-import android.content.Context
-import android.content.pm.PackageManager
-import android.graphics.SurfaceTexture
-import android.media.MediaMetadataRetriever
-import android.net.Uri
-import android.os.Build
-import android.util.Size
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.Logger
-import androidx.camera.core.Preview
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.internal.CameraUseCaseAdapter
-import androidx.camera.testing.CameraUtil
-import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.SurfaceTextureProvider
-import androidx.core.content.ContextCompat
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.GrantPermissionRule
-import com.google.common.truth.Truth.assertThat
-import java.io.File
-import java.util.concurrent.TimeUnit
-import org.junit.After
-import org.junit.Assume
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
-import org.mockito.Mockito
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 21)
-class VideoCaptureTestWithoutAudioPermissionTest {
- companion object {
- const val TAG: String = "VideoCaptureTestWithoutAudioPermission"
- }
- @get:Rule
- val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
- PreTestCameraIdList(Camera2Config.defaultConfig())
- )
-
- @get:Rule
- val permissionRule: GrantPermissionRule =
- GrantPermissionRule.grant(
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- // Don't grant Manifest.permission.RECORD_AUDIO
- )
-
- private val instrumentation = InstrumentationRegistry.getInstrumentation()
-
- private val context = ApplicationProvider.getApplicationContext<Context>()
-
- private lateinit var cameraSelector: CameraSelector
-
- private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
-
- private lateinit var contentResolver: ContentResolver
-
- @Before
- fun setUp() {
- // TODO(b/168175357): Fix VideoCaptureTest problems on CuttleFish API 29
- Assume.assumeFalse(
- "Cuttlefish has MediaCodec dequeueInput/Output buffer fails issue. Unable to test.",
- Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
- )
-
- assumeTrue(CameraUtil.deviceHasCamera())
-
- cameraSelector = if (CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK)) {
- CameraSelector.DEFAULT_BACK_CAMERA
- } else {
- CameraSelector.DEFAULT_FRONT_CAMERA
- }
-
- CameraXUtil.initialize(
- context,
- Camera2Config.defaultConfig()
- ).get()
- cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
-
- contentResolver = context.contentResolver
- }
-
- @After
- fun tearDown() {
- instrumentation.runOnMainSync {
- if (this::cameraUseCaseAdapter.isInitialized) {
- cameraUseCaseAdapter.removeUseCases(cameraUseCaseAdapter.useCases)
- }
- }
-
- CameraXUtil.shutdown().get(10000, TimeUnit.MILLISECONDS)
- }
-
- /**
- * This test intends to test recording features without audio permission (RECORD_AUDIO).
- * Currently we cannot guarantee test cases' running sequence, the audio permission might be
- * granted by previous tests.
- * And if we revoke audio permission on the runtime it will cause the test crash.
- * That makes it necessary to check if the audio permission is denied or not before the test.
- * It's conceivable this test will be skipped because it's not the first case to test.
- */
- @Test
- @Suppress("DEPRECATION")
- fun videoCapture_saveResultToFileWithoutAudioPermission() {
- val checkPermissionResult =
- ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)
-
- Logger.i(TAG, "checkSelfPermission RECORD_AUDIO: $checkPermissionResult")
-
- // This test is only for audio permission does not granted case.
- assumeTrue(checkPermissionResult == PackageManager.PERMISSION_DENIED)
-
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
- val preview = Preview.Builder().build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
- assumeTrue(
- "This combination (videoCapture, preview) is not supported.",
- cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
- )
- instrumentation.runOnMainSync {
- preview.setSurfaceProvider(
- CameraXExecutors.mainThreadExecutor(),
- getSurfaceProvider()
- )
- cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
- }
-
- val callback =
- Mockito.mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
- videoCapture.startRecording(
- androidx.camera.core.VideoCapture.OutputFileOptions.Builder(file).build(),
- CameraXExecutors.mainThreadExecutor(),
- callback
- )
-
- Thread.sleep(3000)
-
- videoCapture.stopRecording()
-
- // Wait for the signal that the video has been saved.
- Mockito.verify(callback, Mockito.timeout(10000)).onVideoSaved(ArgumentMatchers.any())
-
- val mediaRetriever = MediaMetadataRetriever()
-
- mediaRetriever.apply {
- setDataSource(context, Uri.fromFile(file))
- val hasAudio = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
- val numOfTracks = extractMetadata(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS)
-
- // In most of case and test environment, the RECORD_AUDIO permission is granted.
- // But if there is any audio permission denied cases, the recording should be keeps
- // going and only video recorded.
- assertThat(hasAudio).isNull()
- assertThat(numOfTracks).isEqualTo("1")
- }
-
- file.delete()
- }
-
- private fun getSurfaceProvider(): Preview.SurfaceProvider {
- return SurfaceTextureProvider.createSurfaceTextureProvider(object :
- SurfaceTextureProvider.SurfaceTextureCallback {
- override fun onSurfaceTextureReady(
- surfaceTexture: SurfaceTexture,
- resolution: Size,
- ) {
- // No-op
- }
-
- override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
- surfaceTexture.release()
- }
- }
- )
- }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
index 29cce4f..980b398 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
@@ -80,7 +80,6 @@
import androidx.camera.core.impl.Quirks;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfig;
-import androidx.camera.core.impl.VideoCaptureConfig;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
@@ -383,33 +382,6 @@
== CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE);
}
- @SdkSuppress(minSdkVersion = 33)
- @Test
- public void getStreamUseCaseFromUseCaseConfigsVideoCapture() {
- Collection<UseCaseConfig<?>> useCaseConfigs = new ArrayList<>();
- VideoCaptureConfig videoCaptureConfig =
- new VideoCaptureConfig(MutableOptionsBundle.create());
- useCaseConfigs.add(videoCaptureConfig);
- assertTrue(StreamUseCaseUtil.getStreamUseCaseFromUseCaseConfigs(useCaseConfigs,
- new ArrayList<>())
- == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD);
- }
-
- @SdkSuppress(minSdkVersion = 33)
- @Test
- public void getStreamUseCaseFromUseCaseConfigsVideoAndImageCapture() {
- Collection<UseCaseConfig<?>> useCaseConfigs = new ArrayList<>();
- VideoCaptureConfig videoCaptureConfig =
- new VideoCaptureConfig(MutableOptionsBundle.create());
- useCaseConfigs.add(videoCaptureConfig);
- ImageCaptureConfig imageCaptureConfig = new ImageCaptureConfig(
- MutableOptionsBundle.create());
- useCaseConfigs.add(imageCaptureConfig);
- assertTrue(StreamUseCaseUtil.getStreamUseCaseFromUseCaseConfigs(useCaseConfigs,
- new ArrayList<>())
- == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL);
- }
-
// Sharing surface of YUV format is supported since API 28
@SdkSuppress(minSdkVersion = 28)
@Test
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
index 17881be..7af6200 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
@@ -27,7 +27,6 @@
import androidx.camera.core.impl.PreviewConfig;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfig;
-import androidx.camera.core.impl.VideoCaptureConfig;
import java.util.Collection;
@@ -89,15 +88,10 @@
}
- if (useCaseConfig instanceof VideoCaptureConfig) {
- if (hasImageCapture) {
- // If has both image and video capture, return preview video still case.
- return CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL;
- }
- hasVideoCapture = true;
- continue;
-
- }
+ // TODO: Need to handle "hasVideoCapture". The original statement was removed in
+ // aosp/2299682 because it uses the legacy core.VideoCapture API, which means the
+ // statement's content will never be run. The new video.VideoCapture API should
+ // used.
}
if (hasImageCapture) {
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
index 665019a..d10baed 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
@@ -16,8 +16,6 @@
package androidx.camera.camera2.internal;
-import static com.google.common.truth.Truth.assertThat;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -39,26 +37,20 @@
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
-import androidx.camera.core.AspectRatio;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.CameraUnavailableException;
import androidx.camera.core.CameraX;
import androidx.camera.core.CameraXConfig;
-import androidx.camera.core.ImageCapture;
import androidx.camera.core.InitializationException;
-import androidx.camera.core.Preview;
-import androidx.camera.core.UseCase;
import androidx.camera.core.impl.CameraDeviceSurfaceManager;
import androidx.camera.core.impl.ImageFormatConstants;
import androidx.camera.core.impl.SurfaceCombination;
import androidx.camera.core.impl.SurfaceConfig;
import androidx.camera.core.impl.SurfaceConfig.ConfigSize;
import androidx.camera.core.impl.SurfaceConfig.ConfigType;
-import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.testing.CameraUtil;
import androidx.camera.testing.CameraXUtil;
-import androidx.camera.testing.Configs;
import androidx.camera.testing.fakes.FakeCamera;
import androidx.camera.testing.fakes.FakeCameraFactory;
import androidx.test.core.app.ApplicationProvider;
@@ -77,10 +69,7 @@
import org.robolectric.shadows.ShadowCameraCharacteristics;
import org.robolectric.shadows.ShadowCameraManager;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
-import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -360,74 +349,6 @@
}
}
- @SuppressWarnings("deprecation")
- @Test(expected = IllegalArgumentException.class)
- public void suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
- ImageCapture imageCapture = new ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build();
- androidx.camera.core.VideoCapture videoCapture =
- new androidx.camera.core.VideoCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build();
- Preview preview = new Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build();
-
- List<UseCase> useCases = new ArrayList<>();
- useCases.add(imageCapture);
- useCases.add(videoCapture);
- useCases.add(preview);
-
- Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- mCameraFactory.getCamera(LEGACY_CAMERA_ID).getCameraInfoInternal(),
- useCases,
- mUseCaseConfigFactory);
- // A legacy level camera device can't support JPEG (ImageCapture) + PRIV (VideoCapture) +
- // PRIV (Preview) combination. An IllegalArgumentException will be thrown when trying to
- // bind these use cases at the same time.
- mSurfaceManager.getSuggestedResolutions(LEGACY_CAMERA_ID, Collections.emptyList(),
- new ArrayList<>(useCaseToConfigMap.values()));
- }
-
- @SuppressWarnings("deprecation")
- @Test
- public void getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
- ImageCapture imageCapture = new ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build();
- androidx.camera.core.VideoCapture videoCapture =
- new androidx.camera.core.VideoCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build();
- Preview preview = new Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build();
-
- List<UseCase> useCases = new ArrayList<>();
- useCases.add(imageCapture);
- useCases.add(videoCapture);
- useCases.add(preview);
-
- Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- mCameraFactory.getCamera(LIMITED_CAMERA_ID).getCameraInfoInternal(),
- useCases,
- mUseCaseConfigFactory);
- Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
- mSurfaceManager.getSuggestedResolutions(LIMITED_CAMERA_ID, Collections.emptyList(),
- new ArrayList<>(useCaseToConfigMap.values()));
-
- // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
- assertThat(suggestedResolutionMap).containsEntry(useCaseToConfigMap.get(imageCapture),
- mRecordSize);
- assertThat(suggestedResolutionMap).containsEntry(useCaseToConfigMap.get(videoCapture),
- mMaximumVideoSize);
- assertThat(suggestedResolutionMap).containsEntry(useCaseToConfigMap.get(preview),
- mPreviewSize);
- }
-
@Test
public void transformSurfaceConfigWithYUVAnalysisSize() {
SurfaceConfig surfaceConfig = mSurfaceManager.transformSurfaceConfig(
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index 1d8366d5..c4a3072 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -2786,11 +2786,11 @@
}
/**
- * Creates [Preview], [ImageCapture], [ImageAnalysis], [androidx.camera.core.VideoCapture] or
- * FakeUseCase according to the specified settings.
+ * Creates [Preview], [ImageCapture], [ImageAnalysis] or FakeUseCase according to the specified
+ * settings.
*
- * @param useCaseType Which of [Preview], [ImageCapture], [ImageAnalysis],
- * [androidx.camera.core.VideoCapture] and FakeUseCase should be created.
+ * @param useCaseType Which of [Preview], [ImageCapture], [ImageAnalysis] and FakeUseCase should
+ * be created.
* @param targetRotation the target rotation setting. Default is UNKNOWN_ROTATION and no target
* rotation will be set to the created use case.
* @param targetAspectRatio the target aspect ratio setting. Default is UNKNOWN_ASPECT_RATIO
@@ -2802,7 +2802,6 @@
* @param defaultResolution the default resolution setting. Default is null.
* @param supportedResolutions the customized supported resolutions. Default is null.
*/
- @Suppress("DEPRECATION")
private fun createUseCaseByLegacyApi(
useCaseType: Int,
targetRotation: Int = UNKNOWN_ROTATION,
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/SessionConfigTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/SessionConfigTest.java
index 9a165a8..38961da6 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/SessionConfigTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/SessionConfigTest.java
@@ -369,17 +369,11 @@
return deferrableSurface;
}
- @SuppressWarnings("deprecation")
@Test
public void combineTwoSessionsSurfaces() {
DeferrableSurface previewSurface = createSurface(Preview.class);
- DeferrableSurface videoSurface = createSurface(androidx.camera.core.VideoCapture.class);
DeferrableSurface imageCaptureSurface = createSurface(ImageCapture.class);
- SessionConfig.Builder builder0 = new SessionConfig.Builder();
- builder0.addSurface(videoSurface);
- builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
-
SessionConfig.Builder builder1 = new SessionConfig.Builder();
builder1.addSurface(previewSurface);
builder1.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
@@ -389,7 +383,6 @@
builder2.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
SessionConfig.ValidatingBuilder validatingBuilder = new SessionConfig.ValidatingBuilder();
- validatingBuilder.add(builder0.build());
validatingBuilder.add(builder1.build());
validatingBuilder.add(builder2.build());
@@ -397,8 +390,7 @@
List<DeferrableSurface> surfaces = sessionConfig.getSurfaces();
// Ensures the surfaces are all added and sorted correctly.
- assertThat(surfaces)
- .containsExactly(previewSurface, imageCaptureSurface, videoSurface).inOrder();
+ assertThat(surfaces).containsExactly(previewSurface, imageCaptureSurface).inOrder();
}
@Test
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
deleted file mode 100644
index bd6734b..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
+++ /dev/null
@@ -1,2112 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_AUDIO_BIT_RATE;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_AUDIO_CHANNEL_COUNT;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_AUDIO_MIN_BUFFER_SIZE;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_AUDIO_SAMPLE_RATE;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_BIT_RATE;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_INTRA_FRAME_INTERVAL;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_VIDEO_FRAME_RATE;
-import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS;
-import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME;
-import static androidx.camera.core.internal.ThreadConfig.OPTION_BACKGROUND_EXECUTOR;
-import static androidx.camera.core.internal.UseCaseEventConfig.OPTION_USE_CASE_EVENT_CALLBACK;
-
-import android.Manifest;
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.location.Location;
-import android.media.AudioFormat;
-import android.media.AudioRecord;
-import android.media.CamcorderProfile;
-import android.media.MediaCodec;
-import android.media.MediaCodec.BufferInfo;
-import android.media.MediaCodecInfo;
-import android.media.MediaCodecInfo.CodecCapabilities;
-import android.media.MediaFormat;
-import android.media.MediaMuxer;
-import android.media.MediaRecorder.AudioSource;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.ParcelFileDescriptor;
-import android.provider.MediaStore;
-import android.util.Pair;
-import android.util.Size;
-import android.view.Display;
-import android.view.Surface;
-
-import androidx.annotation.DoNotInline;
-import androidx.annotation.GuardedBy;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RequiresPermission;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.RestrictTo.Scope;
-import androidx.annotation.UiThread;
-import androidx.annotation.VisibleForTesting;
-import androidx.camera.core.impl.CameraInternal;
-import androidx.camera.core.impl.CaptureConfig;
-import androidx.camera.core.impl.Config;
-import androidx.camera.core.impl.ConfigProvider;
-import androidx.camera.core.impl.DeferrableSurface;
-import androidx.camera.core.impl.ImageOutputConfig;
-import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
-import androidx.camera.core.impl.ImmediateSurface;
-import androidx.camera.core.impl.MutableConfig;
-import androidx.camera.core.impl.MutableOptionsBundle;
-import androidx.camera.core.impl.OptionsBundle;
-import androidx.camera.core.impl.SessionConfig;
-import androidx.camera.core.impl.UseCaseConfig;
-import androidx.camera.core.impl.UseCaseConfigFactory;
-import androidx.camera.core.impl.VideoCaptureConfig;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.internal.ThreadConfig;
-import androidx.camera.core.internal.utils.VideoUtil;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
-import androidx.core.util.Preconditions;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.nio.ByteBuffer;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * A use case for taking a video.
- *
- * <p>This class is designed for simple video capturing. It gives basic configuration of the
- * recorded video such as resolution and file format.
- *
- * @deprecated Use {@link androidx.camera.video.VideoCapture} instead.
- *
- * @hide
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@Deprecated
-@RestrictTo(Scope.LIBRARY_GROUP)
-public final class VideoCapture extends UseCase {
-
- ////////////////////////////////////////////////////////////////////////////////////////////
- // [UseCase lifetime constant] - Stays constant for the lifetime of the UseCase. Which means
- // they could be created in the constructor.
- ////////////////////////////////////////////////////////////////////////////////////////////
-
- /**
- * An unknown error occurred.
- *
- * <p>See message parameter in onError callback or log for more details.
- */
- public static final int ERROR_UNKNOWN = 0;
- /**
- * An error occurred with encoder state, either when trying to change state or when an
- * unexpected state change occurred.
- */
- public static final int ERROR_ENCODER = 1;
- /** An error with muxer state such as during creation or when stopping. */
- public static final int ERROR_MUXER = 2;
- /**
- * An error indicating start recording was called when video recording is still in progress.
- */
- public static final int ERROR_RECORDING_IN_PROGRESS = 3;
- /**
- * An error indicating the file saving operations.
- */
- public static final int ERROR_FILE_IO = 4;
- /**
- * An error indicating this VideoCapture is not bound to a camera.
- */
- public static final int ERROR_INVALID_CAMERA = 5;
- /**
- * An error indicating the video file is too short.
- * <p> The output file will be deleted if the OutputFileOptions is backed by File or uri.
- */
- public static final int ERROR_RECORDING_TOO_SHORT = 6;
-
- /**
- * Provides a static configuration with implementation-agnostic options.
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- public static final Defaults DEFAULT_CONFIG = new Defaults();
- private static final String TAG = "VideoCapture";
- /** Amount of time to wait for dequeuing a buffer from the videoEncoder. */
- private static final int DEQUE_TIMEOUT_USEC = 10000;
- /** Android preferred mime type for AVC video. */
- private static final String VIDEO_MIME_TYPE = "video/avc";
- private static final String AUDIO_MIME_TYPE = "audio/mp4a-latm";
- /** Camcorder profiles quality list */
- private static final int[] CamcorderQuality = {
- CamcorderProfile.QUALITY_2160P,
- CamcorderProfile.QUALITY_1080P,
- CamcorderProfile.QUALITY_720P,
- CamcorderProfile.QUALITY_480P
- };
-
- private final BufferInfo mVideoBufferInfo = new BufferInfo();
- private final Object mMuxerLock = new Object();
- private final AtomicBoolean mEndOfVideoStreamSignal = new AtomicBoolean(true);
- private final AtomicBoolean mEndOfAudioStreamSignal = new AtomicBoolean(true);
- private final AtomicBoolean mEndOfAudioVideoSignal = new AtomicBoolean(true);
- private final BufferInfo mAudioBufferInfo = new BufferInfo();
- /** For record the first sample written time. */
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- public final AtomicBoolean mIsFirstVideoKeyFrameWrite = new AtomicBoolean(false);
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- public final AtomicBoolean mIsFirstAudioSampleWrite = new AtomicBoolean(false);
-
- ////////////////////////////////////////////////////////////////////////////////////////////
- // [UseCase attached constant] - Is only valid when the UseCase is attached to a camera.
- ////////////////////////////////////////////////////////////////////////////////////////////
-
- /** Thread on which all encoding occurs. */
- private HandlerThread mVideoHandlerThread;
- private Handler mVideoHandler;
- /** Thread on which audio encoding occurs. */
- private HandlerThread mAudioHandlerThread;
- private Handler mAudioHandler;
-
- @NonNull
- MediaCodec mVideoEncoder;
- @NonNull
- private MediaCodec mAudioEncoder;
- @Nullable
- private ListenableFuture<Void> mRecordingFuture = null;
- @NonNull
- private SessionConfig.Builder mSessionConfigBuilder = new SessionConfig.Builder();
-
- ////////////////////////////////////////////////////////////////////////////////////////////
- // [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
- ////////////////////////////////////////////////////////////////////////////////////////////
-
- /** The muxer that writes the encoding data to file. */
- @GuardedBy("mMuxerLock")
- private MediaMuxer mMuxer;
- private final AtomicBoolean mMuxerStarted = new AtomicBoolean(false);
- /** The index of the video track used by the muxer. */
- @GuardedBy("mMuxerLock")
- private int mVideoTrackIndex;
- /** The index of the audio track used by the muxer. */
- @GuardedBy("mMuxerLock")
- private int mAudioTrackIndex;
- /** Surface the camera writes to, which the videoEncoder uses as input. */
- Surface mCameraSurface;
-
- /** audio raw data */
- @Nullable
- private volatile AudioRecord mAudioRecorder;
- private volatile int mAudioBufferSize;
- private volatile boolean mIsRecording = false;
- private int mAudioChannelCount;
- private int mAudioSampleRate;
- private int mAudioBitRate;
- private DeferrableSurface mDeferrableSurface;
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
- volatile Uri mSavedVideoUri;
- private volatile ParcelFileDescriptor mParcelFileDescriptor;
- private final AtomicBoolean mIsAudioEnabled = new AtomicBoolean(true);
-
- private VideoEncoderInitStatus mVideoEncoderInitStatus =
- VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_UNINITIALIZED;
- @Nullable
- private Throwable mVideoEncoderErrorMessage;
-
- /**
- * Creates a new video capture use case from the given configuration.
- *
- * @param config for this use case instance
- */
- VideoCapture(@NonNull VideoCaptureConfig config) {
- super(config);
- }
-
- /** Creates a {@link MediaFormat} using parameters from the configuration */
- private static MediaFormat createVideoMediaFormat(VideoCaptureConfig config, Size resolution) {
- MediaFormat format =
- MediaFormat.createVideoFormat(
- VIDEO_MIME_TYPE, resolution.getWidth(), resolution.getHeight());
- format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
- format.setInteger(MediaFormat.KEY_BIT_RATE, config.getBitRate());
- format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoFrameRate());
- format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, config.getIFrameInterval());
-
- return format;
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @Nullable
- public UseCaseConfig<?> getDefaultConfig(boolean applyDefaultConfig,
- @NonNull UseCaseConfigFactory factory) {
- Config captureConfig = factory.getConfig(
- UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
- ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY);
-
- if (applyDefaultConfig) {
- captureConfig = Config.mergeConfigs(captureConfig, DEFAULT_CONFIG.getConfig());
- }
-
- return captureConfig == null ? null :
- getUseCaseConfigBuilder(captureConfig).getUseCaseConfig();
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @SuppressWarnings("WrongConstant")
- @Override
- @RestrictTo(Scope.LIBRARY_GROUP)
- public void onAttached() {
- mVideoHandlerThread = new HandlerThread(CameraXThreads.TAG + "video encoding thread");
- mAudioHandlerThread = new HandlerThread(CameraXThreads.TAG + "audio encoding thread");
-
- // video thread start
- mVideoHandlerThread.start();
- mVideoHandler = new Handler(mVideoHandlerThread.getLooper());
-
- // audio thread start
- mAudioHandlerThread.start();
- mAudioHandler = new Handler(mAudioHandlerThread.getLooper());
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @Override
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
- if (mCameraSurface != null) {
- mVideoEncoder.stop();
- mVideoEncoder.release();
- mAudioEncoder.stop();
- mAudioEncoder.release();
- releaseCameraSurface(false);
- }
-
- try {
- mVideoEncoder = MediaCodec.createEncoderByType(VIDEO_MIME_TYPE);
- mAudioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE);
- } catch (IOException e) {
- throw new IllegalStateException("Unable to create MediaCodec due to: " + e.getCause());
- }
-
- setupEncoder(getCameraId(), suggestedResolution);
- // VideoCapture has to be active to apply SessionConfig's template type.
- notifyActive();
- return suggestedResolution;
- }
-
- /**
- * Starts recording video, which continues until {@link VideoCapture#stopRecording()} is
- * called.
- *
- * <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
- * {@link OnVideoSavedCallback#onError(int, String, Throwable)}.
- *
- * @param outputFileOptions Location to save the video capture
- * @param executor The executor in which the callback methods will be run.
- * @param callback Callback for when the recorded video saving completion or failure.
- */
- @SuppressWarnings("ObjectToString")
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
- public void startRecording(
- @NonNull OutputFileOptions outputFileOptions, @NonNull Executor executor,
- @NonNull OnVideoSavedCallback callback) {
- if (Looper.getMainLooper() != Looper.myLooper()) {
- CameraXExecutors.mainThreadExecutor().execute(() -> startRecording(outputFileOptions,
- executor, callback));
- return;
- }
- Logger.i(TAG, "startRecording");
- mIsFirstVideoKeyFrameWrite.set(false);
- mIsFirstAudioSampleWrite.set(false);
-
- OnVideoSavedCallback postListener = new VideoSavedListenerWrapper(executor, callback);
-
- CameraInternal attachedCamera = getCamera();
- if (attachedCamera == null) {
- // Not bound. Notify callback.
- postListener.onError(ERROR_INVALID_CAMERA,
- "Not bound to a Camera [" + VideoCapture.this + "]", null);
- return;
- }
-
- // Check video encoder initialization status, if there is any error happened
- // return error callback directly.
- if (mVideoEncoderInitStatus
- == VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INSUFFICIENT_RESOURCE
- || mVideoEncoderInitStatus
- == VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INITIALIZED_FAILED
- || mVideoEncoderInitStatus
- == VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_RESOURCE_RECLAIMED) {
- postListener.onError(ERROR_ENCODER, "Video encoder initialization failed before start"
- + " recording ", mVideoEncoderErrorMessage);
- return;
- }
-
- if (!mEndOfAudioVideoSignal.get()) {
- postListener.onError(
- ERROR_RECORDING_IN_PROGRESS, "It is still in video recording!",
- null);
- return;
- }
-
- if (mIsAudioEnabled.get()) {
- try {
- // Audio input start
- if (mAudioRecorder.getState() == AudioRecord.STATE_INITIALIZED) {
- mAudioRecorder.startRecording();
- }
- } catch (IllegalStateException e) {
- // Disable the audio if the audio input cannot start. And Continue the recording
- // without audio.
- Logger.i(TAG,
- "AudioRecorder cannot start recording, disable audio." + e.getMessage());
- mIsAudioEnabled.set(false);
- releaseAudioInputResource();
- }
-
- // Gets the AudioRecorder's state
- if (mAudioRecorder.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
- Logger.i(TAG,
- "AudioRecorder startRecording failed - incorrect state: "
- + mAudioRecorder.getRecordingState());
- mIsAudioEnabled.set(false);
- releaseAudioInputResource();
- }
- }
-
- AtomicReference<Completer<Void>> recordingCompleterRef = new AtomicReference<>();
- mRecordingFuture = CallbackToFutureAdapter.getFuture(
- completer -> {
- recordingCompleterRef.set(completer);
- return "startRecording";
- });
- Completer<Void> recordingCompleter =
- Preconditions.checkNotNull(recordingCompleterRef.get());
-
- mRecordingFuture.addListener(() -> {
- mRecordingFuture = null;
- // Do the setup of the videoEncoder at the end of video recording instead of at the
- // start of recording because it requires attaching a new Surface. This causes a
- // glitch so we don't want that to incur latency at the start of capture.
- if (getCamera() != null) {
- // Ensure the use case is bound. Asynchronous stopping procedure may occur after
- // the use case is unbound, i.e. after onDetached().
- setupEncoder(getCameraId(), getAttachedSurfaceResolution());
- notifyReset();
- }
- }, CameraXExecutors.mainThreadExecutor());
-
- try {
- // video encoder start
- Logger.i(TAG, "videoEncoder start");
- mVideoEncoder.start();
-
- // audio encoder start
- if (mIsAudioEnabled.get()) {
- Logger.i(TAG, "audioEncoder start");
- mAudioEncoder.start();
- }
- } catch (IllegalStateException e) {
- recordingCompleter.set(null);
- postListener.onError(ERROR_ENCODER, "Audio/Video encoder start fail", e);
- return;
- }
-
- try {
- synchronized (mMuxerLock) {
- mMuxer = initMediaMuxer(outputFileOptions);
- Preconditions.checkNotNull(mMuxer);
- mMuxer.setOrientationHint(getRelativeRotation(attachedCamera));
-
- Metadata metadata = outputFileOptions.getMetadata();
- if (metadata != null && metadata.location != null) {
- mMuxer.setLocation(
- (float) metadata.location.getLatitude(),
- (float) metadata.location.getLongitude());
- }
- }
- } catch (IOException e) {
- recordingCompleter.set(null);
- postListener.onError(ERROR_MUXER, "MediaMuxer creation failed!", e);
- return;
- }
-
- mEndOfVideoStreamSignal.set(false);
- mEndOfAudioStreamSignal.set(false);
- mEndOfAudioVideoSignal.set(false);
- mIsRecording = true;
-
- // Attach Surface to repeating request.
- mSessionConfigBuilder.clearSurfaces();
- mSessionConfigBuilder.addSurface(mDeferrableSurface);
- updateSessionConfig(mSessionConfigBuilder.build());
- notifyUpdated();
-
- if (mIsAudioEnabled.get()) {
- mAudioHandler.post(() -> audioEncode(postListener));
- }
-
- String cameraId = getCameraId();
- Size resolution = getAttachedSurfaceResolution();
- mVideoHandler.post(
- () -> {
- boolean errorOccurred = videoEncode(postListener, cameraId, resolution,
- outputFileOptions);
- if (!errorOccurred) {
- postListener.onVideoSaved(new OutputFileResults(mSavedVideoUri));
- mSavedVideoUri = null;
- }
- recordingCompleter.set(null);
- });
- }
-
- /**
- * Stops recording video, this must be called after {@link
- * VideoCapture#startRecording(OutputFileOptions, Executor, OnVideoSavedCallback)} is
- * called.
- *
- * <p>stopRecording() is asynchronous API. User need to check if {@link
- * OnVideoSavedCallback#onVideoSaved(OutputFileResults)} or
- * {@link OnVideoSavedCallback#onError(int, String, Throwable)} be called
- * before startRecording.
- */
- public void stopRecording() {
- if (Looper.getMainLooper() != Looper.myLooper()) {
- CameraXExecutors.mainThreadExecutor().execute(() -> stopRecording());
- return;
- }
- Logger.i(TAG, "stopRecording");
-
- mSessionConfigBuilder.clearSurfaces();
- mSessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface);
- updateSessionConfig(mSessionConfigBuilder.build());
- notifyUpdated();
-
- if (mIsRecording) {
- if (mIsAudioEnabled.get()) {
- // Stop audio encoder thread, and wait video encoder and muxer stop.
- mEndOfAudioStreamSignal.set(true);
- } else {
- // Audio is disabled, stop video encoder thread directly.
- mEndOfVideoStreamSignal.set(true);
- }
- }
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- public void onDetached() {
- stopRecording();
-
- if (mRecordingFuture != null) {
- mRecordingFuture.addListener(() -> releaseResources(),
- CameraXExecutors.mainThreadExecutor());
- } else {
- releaseResources();
- }
- }
-
- private void releaseResources() {
- mVideoHandlerThread.quitSafely();
-
- // audio encoder release
- releaseAudioInputResource();
-
- if (mCameraSurface != null) {
- releaseCameraSurface(true);
- }
- }
-
- private void releaseAudioInputResource() {
- mAudioHandlerThread.quitSafely();
- if (mAudioEncoder != null) {
- mAudioEncoder.release();
- mAudioEncoder = null;
- }
-
- if (mAudioRecorder != null) {
- mAudioRecorder.release();
- mAudioRecorder = null;
- }
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @NonNull
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- public UseCaseConfig.Builder<?, ?, ?> getUseCaseConfigBuilder(@NonNull Config config) {
- return Builder.fromConfig(config);
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @UiThread
- @Override
- public void onStateDetached() {
- stopRecording();
- }
-
- @UiThread
- private void releaseCameraSurface(final boolean releaseVideoEncoder) {
- if (mDeferrableSurface == null) {
- return;
- }
-
- final MediaCodec videoEncoder = mVideoEncoder;
-
- // Calling close should allow termination future to complete and close the surface with
- // the listener that was added after constructing the DeferrableSurface.
- mDeferrableSurface.close();
- mDeferrableSurface.getTerminationFuture().addListener(
- () -> {
- if (releaseVideoEncoder && videoEncoder != null) {
- videoEncoder.release();
- }
- }, CameraXExecutors.mainThreadExecutor());
-
- if (releaseVideoEncoder) {
- mVideoEncoder = null;
- }
- mCameraSurface = null;
- mDeferrableSurface = null;
- }
-
- /**
- * Sets the desired rotation of the output video.
- *
- * <p>In most cases this should be set to the current rotation returned by {@link
- * Display#getRotation()}.
- *
- * @param rotation Desired rotation of the output video.
- */
- public void setTargetRotation(@RotationValue int rotation) {
- setTargetRotationInternal(rotation);
- }
-
- /**
- * Setup the {@link MediaCodec} for encoding video from a camera {@link Surface} and encoding
- * audio from selected audio source.
- */
- @UiThread
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
- void setupEncoder(@NonNull String cameraId, @NonNull Size resolution) {
- VideoCaptureConfig config = (VideoCaptureConfig) getCurrentConfig();
-
- // video encoder setup
- mVideoEncoder.reset();
- mVideoEncoderInitStatus = VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_UNINITIALIZED;
-
- // Configures a Video encoder, if there is any exception, will abort follow up actions
- try {
- mVideoEncoder.configure(
- createVideoMediaFormat(config, resolution), /*surface*/
- null, /*crypto*/
- null,
- MediaCodec.CONFIGURE_FLAG_ENCODE);
- } catch (MediaCodec.CodecException e) {
- int errorCode = 0;
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
- errorCode = Api23Impl.getCodecExceptionErrorCode(e);
- String diagnosticInfo = e.getDiagnosticInfo();
- if (errorCode == MediaCodec.CodecException.ERROR_INSUFFICIENT_RESOURCE) {
- Logger.i(TAG,
- "CodecException: code: " + errorCode + " diagnostic: "
- + diagnosticInfo);
- mVideoEncoderInitStatus =
- VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INSUFFICIENT_RESOURCE;
- } else if (errorCode == MediaCodec.CodecException.ERROR_RECLAIMED) {
- Logger.i(TAG,
- "CodecException: code: " + errorCode + " diagnostic: "
- + diagnosticInfo);
- mVideoEncoderInitStatus =
- VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_RESOURCE_RECLAIMED;
- }
- } else {
- mVideoEncoderInitStatus =
- VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INITIALIZED_FAILED;
- }
- mVideoEncoderErrorMessage = e;
- return;
- } catch (IllegalArgumentException | IllegalStateException e) {
- mVideoEncoderInitStatus =
- VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INITIALIZED_FAILED;
- mVideoEncoderErrorMessage = e;
- return;
- }
-
- if (mCameraSurface != null) {
- releaseCameraSurface(false);
- }
- Surface cameraSurface = mVideoEncoder.createInputSurface();
- mCameraSurface = cameraSurface;
-
- mSessionConfigBuilder = SessionConfig.Builder.createFrom(config);
-
- if (mDeferrableSurface != null) {
- mDeferrableSurface.close();
- }
- mDeferrableSurface = new ImmediateSurface(mCameraSurface, resolution, getImageFormat());
- mDeferrableSurface.getTerminationFuture().addListener(
- cameraSurface::release, CameraXExecutors.mainThreadExecutor()
- );
-
- mSessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface);
-
- mSessionConfigBuilder.addErrorListener(new SessionConfig.ErrorListener() {
- @Override
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
- public void onError(@NonNull SessionConfig sessionConfig,
- @NonNull SessionConfig.SessionError error) {
- // Ensure the attached camera has not changed before calling setupEncoder.
- // TODO(b/143915543): Ensure this never gets called by a camera that is not attached
- // to this use case so we don't need to do this check.
- if (isCurrentCamera(cameraId)) {
- // Only reset the pipeline when the bound camera is the same.
- setupEncoder(cameraId, resolution);
- notifyReset();
- }
- }
- });
-
- updateSessionConfig(mSessionConfigBuilder.build());
-
- // audio encoder setup
- // reset audio inout flag
- mIsAudioEnabled.set(true);
-
- setAudioParametersByCamcorderProfile(resolution, cameraId);
- mAudioEncoder.reset();
- mAudioEncoder.configure(
- createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
-
- if (mAudioRecorder != null) {
- mAudioRecorder.release();
- }
- mAudioRecorder = autoConfigAudioRecordSource(config);
- // check mAudioRecorder
- if (mAudioRecorder == null) {
- Logger.e(TAG, "AudioRecord object cannot initialized correctly!");
- mIsAudioEnabled.set(false);
- }
-
- synchronized (mMuxerLock) {
- mVideoTrackIndex = -1;
- mAudioTrackIndex = -1;
- }
- mIsRecording = false;
- }
-
- /**
- * Write a buffer that has been encoded to file.
- *
- * @param bufferIndex the index of the buffer in the videoEncoder that has available data
- * @return returns true if this buffer is the end of the stream
- */
- private boolean writeVideoEncodedBuffer(int bufferIndex) {
- if (bufferIndex < 0) {
- Logger.e(TAG, "Output buffer should not have negative index: " + bufferIndex);
- return false;
- }
- // Get data from buffer
- ByteBuffer outputBuffer = mVideoEncoder.getOutputBuffer(bufferIndex);
-
- // Check if buffer is valid, if not then return
- if (outputBuffer == null) {
- Logger.d(TAG, "OutputBuffer was null.");
- return false;
- }
-
- // Write data to mMuxer if available
- if (mMuxerStarted.get()) {
- if (mVideoBufferInfo.size > 0) {
- outputBuffer.position(mVideoBufferInfo.offset);
- outputBuffer.limit(mVideoBufferInfo.offset + mVideoBufferInfo.size);
- mVideoBufferInfo.presentationTimeUs = (System.nanoTime() / 1000);
-
- synchronized (mMuxerLock) {
- if (!mIsFirstVideoKeyFrameWrite.get()) {
- boolean isKeyFrame =
- (mVideoBufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
- if (isKeyFrame) {
- Logger.i(TAG,
- "First video key frame written.");
- mIsFirstVideoKeyFrameWrite.set(true);
- } else {
- // Request a sync frame immediately
- final Bundle syncFrame = new Bundle();
- syncFrame.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
- mVideoEncoder.setParameters(syncFrame);
- }
- }
- mMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, mVideoBufferInfo);
- }
- } else {
- Logger.i(TAG, "mVideoBufferInfo.size <= 0, index " + bufferIndex);
- }
- }
-
- // Release data
- mVideoEncoder.releaseOutputBuffer(bufferIndex, false);
-
- // Return true if EOS is set
- return (mVideoBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
- }
-
- private boolean writeAudioEncodedBuffer(int bufferIndex) {
- ByteBuffer buffer = getOutputBuffer(mAudioEncoder, bufferIndex);
- buffer.position(mAudioBufferInfo.offset);
- if (mMuxerStarted.get()) {
- try {
- if (mAudioBufferInfo.size > 0 && mAudioBufferInfo.presentationTimeUs > 0) {
- synchronized (mMuxerLock) {
- if (!mIsFirstAudioSampleWrite.get()) {
- Logger.i(TAG, "First audio sample written.");
- mIsFirstAudioSampleWrite.set(true);
- }
- mMuxer.writeSampleData(mAudioTrackIndex, buffer, mAudioBufferInfo);
- }
- } else {
- Logger.i(TAG, "mAudioBufferInfo size: " + mAudioBufferInfo.size + " "
- + "presentationTimeUs: " + mAudioBufferInfo.presentationTimeUs);
- }
- } catch (Exception e) {
- Logger.e(
- TAG,
- "audio error:size="
- + mAudioBufferInfo.size
- + "/offset="
- + mAudioBufferInfo.offset
- + "/timeUs="
- + mAudioBufferInfo.presentationTimeUs);
- e.printStackTrace();
- }
- }
- mAudioEncoder.releaseOutputBuffer(bufferIndex, false);
- return (mAudioBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
- }
-
- /**
- * Encoding which runs indefinitely until end of stream is signaled. This should not run on the
- * main thread otherwise it will cause the application to block.
- *
- * @return returns {@code true} if an error condition occurred, otherwise returns {@code false}
- */
- boolean videoEncode(@NonNull OnVideoSavedCallback videoSavedCallback, @NonNull String cameraId,
- @NonNull Size resolution,
- @NonNull OutputFileOptions outputFileOptions) {
- // Main encoding loop. Exits on end of stream.
- boolean errorOccurred = false;
- boolean videoEos = false;
- while (!videoEos && !errorOccurred) {
- // Check for end of stream from main thread
- if (mEndOfVideoStreamSignal.get()) {
- mVideoEncoder.signalEndOfInputStream();
- mEndOfVideoStreamSignal.set(false);
- }
-
- // Deque buffer to check for processing step
- int outputBufferId =
- mVideoEncoder.dequeueOutputBuffer(mVideoBufferInfo, DEQUE_TIMEOUT_USEC);
- switch (outputBufferId) {
- case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
- if (mMuxerStarted.get()) {
- videoSavedCallback.onError(
- ERROR_ENCODER,
- "Unexpected change in video encoding format.",
- null);
- errorOccurred = true;
- }
-
- synchronized (mMuxerLock) {
- mVideoTrackIndex = mMuxer.addTrack(mVideoEncoder.getOutputFormat());
-
- if ((mIsAudioEnabled.get() && mAudioTrackIndex >= 0
- && mVideoTrackIndex >= 0)
- || (!mIsAudioEnabled.get() && mVideoTrackIndex >= 0)) {
- Logger.i(TAG, "MediaMuxer started on video encode thread and audio "
- + "enabled: " + mIsAudioEnabled);
- mMuxer.start();
- mMuxerStarted.set(true);
- }
- }
- break;
- case MediaCodec.INFO_TRY_AGAIN_LATER:
- // Timed out. Just wait until next attempt to deque.
- break;
- default:
- videoEos = writeVideoEncodedBuffer(outputBufferId);
- }
- }
-
- try {
- Logger.i(TAG, "videoEncoder stop");
- mVideoEncoder.stop();
- } catch (IllegalStateException e) {
- videoSavedCallback.onError(ERROR_ENCODER,
- "Video encoder stop failed!", e);
- errorOccurred = true;
- }
-
- try {
- // new MediaMuxer instance required for each new file written, and release current one.
- synchronized (mMuxerLock) {
- if (mMuxer != null) {
- if (mMuxerStarted.get()) {
- Logger.i(TAG, "Muxer already started");
- mMuxer.stop();
- }
- mMuxer.release();
- mMuxer = null;
- }
- }
-
- // A final checking for recording result, if the recorded file has no key
- // frame, then the video file is not playable, needs to call
- // onError() and will be removed.
-
- boolean checkResult = removeRecordingResultIfNoVideoKeyFrameArrived(outputFileOptions);
-
- if (!checkResult) {
- videoSavedCallback.onError(ERROR_RECORDING_TOO_SHORT,
- "The file has no video key frame.", null);
- errorOccurred = true;
- }
- } catch (IllegalStateException e) {
- // The video encoder has not got the key frame yet.
- Logger.i(TAG, "muxer stop IllegalStateException: " + System.currentTimeMillis());
- Logger.i(TAG,
- "muxer stop exception, mIsFirstVideoKeyFrameWrite: "
- + mIsFirstVideoKeyFrameWrite.get());
- if (mIsFirstVideoKeyFrameWrite.get()) {
- // If muxer throws IllegalStateException at this moment and also the key frame
- // has received, this will reported as a Muxer stop failed.
- // Otherwise, this error will be ERROR_RECORDING_TOO_SHORT.
- videoSavedCallback.onError(ERROR_MUXER, "Muxer stop failed!", e);
- } else {
- videoSavedCallback.onError(ERROR_RECORDING_TOO_SHORT,
- "The file has no video key frame.", null);
- }
- errorOccurred = true;
- }
-
- if (mParcelFileDescriptor != null) {
- try {
- mParcelFileDescriptor.close();
- mParcelFileDescriptor = null;
- } catch (IOException e) {
- videoSavedCallback.onError(ERROR_MUXER, "File descriptor close failed!", e);
- errorOccurred = true;
- }
- }
-
- mMuxerStarted.set(false);
-
- // notify the UI thread that the video recording has finished
- mEndOfAudioVideoSignal.set(true);
- mIsFirstVideoKeyFrameWrite.set(false);
-
- Logger.i(TAG, "Video encode thread end.");
- return errorOccurred;
- }
-
- boolean audioEncode(OnVideoSavedCallback videoSavedCallback) {
- // Audio encoding loop. Exits on end of stream.
- boolean audioEos = false;
- int outIndex;
- long lastAudioTimestamp = 0;
- while (!audioEos && mIsRecording) {
- // Check for end of stream from main thread
- if (mEndOfAudioStreamSignal.get()) {
- mEndOfAudioStreamSignal.set(false);
- mIsRecording = false;
- }
-
- // get audio deque input buffer
- if (mAudioEncoder != null && mAudioRecorder != null) {
- try {
- int index = mAudioEncoder.dequeueInputBuffer(-1);
- if (index >= 0) {
- final ByteBuffer buffer = getInputBuffer(mAudioEncoder, index);
- buffer.clear();
- int length = mAudioRecorder.read(buffer, mAudioBufferSize);
- if (length > 0) {
- mAudioEncoder.queueInputBuffer(
- index,
- 0,
- length,
- (System.nanoTime() / 1000),
- mIsRecording ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
- }
- }
- } catch (MediaCodec.CodecException e) {
- Logger.i(TAG, "audio dequeueInputBuffer CodecException " + e.getMessage());
- } catch (IllegalStateException e) {
- Logger.i(TAG,
- "audio dequeueInputBuffer IllegalStateException " + e.getMessage());
- }
-
- // start to dequeue audio output buffer
- do {
- outIndex = mAudioEncoder.dequeueOutputBuffer(mAudioBufferInfo, 0);
- switch (outIndex) {
- case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
- synchronized (mMuxerLock) {
- mAudioTrackIndex = mMuxer.addTrack(mAudioEncoder.getOutputFormat());
- if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0) {
- Logger.i(TAG, "MediaMuxer start on audio encoder thread.");
- mMuxer.start();
- mMuxerStarted.set(true);
- }
- }
- break;
- case MediaCodec.INFO_TRY_AGAIN_LATER:
- break;
- default:
- // Drops out of order audio frame if the frame's earlier than last
- // frame.
- if (mAudioBufferInfo.presentationTimeUs > lastAudioTimestamp) {
- audioEos = writeAudioEncodedBuffer(outIndex);
- lastAudioTimestamp = mAudioBufferInfo.presentationTimeUs;
- } else {
- Logger.w(TAG,
- "Drops frame, current frame's timestamp "
- + mAudioBufferInfo.presentationTimeUs
- + " is earlier that last frame "
- + lastAudioTimestamp);
- // Releases this frame from output buffer
- mAudioEncoder.releaseOutputBuffer(outIndex, false);
- }
- }
- } while (outIndex >= 0 && !audioEos); // end of dequeue output buffer
- }
- } // end of while loop
-
- // Audio Stop
- try {
- Logger.i(TAG, "audioRecorder stop");
- mAudioRecorder.stop();
- } catch (IllegalStateException e) {
- videoSavedCallback.onError(
- ERROR_ENCODER, "Audio recorder stop failed!", e);
- }
-
- try {
- mAudioEncoder.stop();
- } catch (IllegalStateException e) {
- videoSavedCallback.onError(ERROR_ENCODER,
- "Audio encoder stop failed!", e);
- }
-
- Logger.i(TAG, "Audio encode thread end");
- // Use AtomicBoolean to signal because MediaCodec.signalEndOfInputStream() is not thread
- // safe
- mEndOfVideoStreamSignal.set(true);
-
- return false;
- }
-
- private ByteBuffer getInputBuffer(MediaCodec codec, int index) {
- return codec.getInputBuffer(index);
- }
-
- private ByteBuffer getOutputBuffer(MediaCodec codec, int index) {
- return codec.getOutputBuffer(index);
- }
-
- /** Creates a {@link MediaFormat} using parameters for audio from the configuration */
- private MediaFormat createAudioMediaFormat() {
- MediaFormat format =
- MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, mAudioSampleRate,
- mAudioChannelCount);
- format.setInteger(
- MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
- format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitRate);
-
- return format;
- }
-
- /** Create a AudioRecord object to get raw data */
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
- private AudioRecord autoConfigAudioRecordSource(VideoCaptureConfig config) {
- // Use channel count to determine stereo vs mono
- int channelConfig =
- mAudioChannelCount == 1
- ? AudioFormat.CHANNEL_IN_MONO
- : AudioFormat.CHANNEL_IN_STEREO;
-
- try {
- // Use only ENCODING_PCM_16BIT because it mandatory supported.
- int bufferSize =
- AudioRecord.getMinBufferSize(mAudioSampleRate, channelConfig,
- AudioFormat.ENCODING_PCM_16BIT);
-
- if (bufferSize <= 0) {
- bufferSize = config.getAudioMinBufferSize();
- }
-
- AudioRecord recorder =
- new AudioRecord(
- AudioSource.CAMCORDER,
- mAudioSampleRate,
- channelConfig,
- AudioFormat.ENCODING_PCM_16BIT,
- bufferSize * 2);
-
- if (recorder.getState() == AudioRecord.STATE_INITIALIZED) {
- mAudioBufferSize = bufferSize;
- Logger.i(
- TAG,
- "source: "
- + AudioSource.CAMCORDER
- + " audioSampleRate: "
- + mAudioSampleRate
- + " channelConfig: "
- + channelConfig
- + " audioFormat: "
- + AudioFormat.ENCODING_PCM_16BIT
- + " bufferSize: "
- + bufferSize);
- return recorder;
- }
- } catch (Exception e) {
- Logger.e(TAG, "Exception, keep trying.", e);
- }
- return null;
- }
-
- /** Set audio record parameters by CamcorderProfile */
- @SuppressWarnings("deprecation")
- private void setAudioParametersByCamcorderProfile(Size currentResolution, String cameraId) {
- CamcorderProfile profile;
- boolean isCamcorderProfileFound = false;
-
- try {
- for (int quality : CamcorderQuality) {
- if (CamcorderProfile.hasProfile(Integer.parseInt(cameraId), quality)) {
- profile = CamcorderProfile.get(Integer.parseInt(cameraId), quality);
- if (currentResolution.getWidth() == profile.videoFrameWidth
- && currentResolution.getHeight() == profile.videoFrameHeight) {
- mAudioChannelCount = profile.audioChannels;
- mAudioSampleRate = profile.audioSampleRate;
- mAudioBitRate = profile.audioBitRate;
- isCamcorderProfileFound = true;
- break;
- }
- }
- }
- } catch (NumberFormatException e) {
- Logger.i(TAG, "The camera Id is not an integer because the camera may be a removable "
- + "device. Use the default values for the audio related settings.");
- }
-
- // In case no corresponding camcorder profile can be founded, * get default value from
- // VideoCaptureConfig.
- if (!isCamcorderProfileFound) {
- VideoCaptureConfig config = (VideoCaptureConfig) getCurrentConfig();
- mAudioChannelCount = config.getAudioChannelCount();
- mAudioSampleRate = config.getAudioSampleRate();
- mAudioBitRate = config.getAudioBitRate();
- }
- }
-
- private boolean removeRecordingResultIfNoVideoKeyFrameArrived(
- @NonNull OutputFileOptions outputFileOptions) {
- boolean checkKeyFrame;
-
- // 1. There should be one video key frame at least.
- Logger.i(TAG,
- "check Recording Result First Video Key Frame Write: "
- + mIsFirstVideoKeyFrameWrite.get());
- if (!mIsFirstVideoKeyFrameWrite.get()) {
- Logger.i(TAG, "The recording result has no key frame.");
- checkKeyFrame = false;
- } else {
- checkKeyFrame = true;
- }
-
- // 2. If no key frame, remove file except the target is a file descriptor case.
- if (outputFileOptions.isSavingToFile()) {
- File outputFile = outputFileOptions.getFile();
- if (!checkKeyFrame) {
- Logger.i(TAG, "Delete file.");
- outputFile.delete();
- }
- } else if (outputFileOptions.isSavingToMediaStore()) {
- if (!checkKeyFrame) {
- Logger.i(TAG, "Delete file.");
- if (mSavedVideoUri != null) {
- ContentResolver contentResolver = outputFileOptions.getContentResolver();
- contentResolver.delete(mSavedVideoUri, null, null);
- }
- }
- }
-
- return checkKeyFrame;
- }
-
- @NonNull
- private MediaMuxer initMediaMuxer(@NonNull OutputFileOptions outputFileOptions)
- throws IOException {
- MediaMuxer mediaMuxer;
-
- if (outputFileOptions.isSavingToFile()) {
- File savedVideoFile = outputFileOptions.getFile();
- mSavedVideoUri = Uri.fromFile(outputFileOptions.getFile());
-
- mediaMuxer = new MediaMuxer(savedVideoFile.getAbsolutePath(),
- MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
- } else if (outputFileOptions.isSavingToFileDescriptor()) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
- throw new IllegalArgumentException("Using a FileDescriptor to record a video is "
- + "only supported for Android 8.0 or above.");
- }
-
- mediaMuxer = Api26Impl.createMediaMuxer(outputFileOptions.getFileDescriptor(),
- MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
- } else if (outputFileOptions.isSavingToMediaStore()) {
- ContentValues values = outputFileOptions.getContentValues() != null
- ? new ContentValues(outputFileOptions.getContentValues())
- : new ContentValues();
-
- mSavedVideoUri = outputFileOptions.getContentResolver().insert(
- outputFileOptions.getSaveCollection(), values);
-
- if (mSavedVideoUri == null) {
- throw new IOException("Invalid Uri!");
- }
-
- // Sine API 26, media muxer could be initiated by a FileDescriptor.
- try {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
- String savedLocationPath = VideoUtil.getAbsolutePathFromUri(
- outputFileOptions.getContentResolver(), mSavedVideoUri);
-
- Logger.i(TAG, "Saved Location Path: " + savedLocationPath);
- mediaMuxer = new MediaMuxer(savedLocationPath,
- MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
- } else {
- mParcelFileDescriptor =
- outputFileOptions.getContentResolver().openFileDescriptor(
- mSavedVideoUri, "rw");
- mediaMuxer = Api26Impl.createMediaMuxer(
- mParcelFileDescriptor.getFileDescriptor(),
- MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
- }
- } catch (IOException e) {
- mSavedVideoUri = null;
- throw e;
- }
- } else {
- throw new IllegalArgumentException(
- "The OutputFileOptions should assign before recording");
- }
-
- return mediaMuxer;
- }
-
- /**
- * Describes the error that occurred during video capture operations.
- *
- * <p>This is a parameter sent to the error callback functions set in listeners such as {@link
- * VideoCapture.OnVideoSavedCallback#onError(int, String, Throwable)}.
- *
- * <p>See message parameter in onError callback or log for more details.
- *
- * @hide
- */
- @IntDef({ERROR_UNKNOWN, ERROR_ENCODER, ERROR_MUXER, ERROR_RECORDING_IN_PROGRESS,
- ERROR_FILE_IO, ERROR_INVALID_CAMERA, ERROR_RECORDING_TOO_SHORT})
- @Retention(RetentionPolicy.SOURCE)
- @RestrictTo(Scope.LIBRARY_GROUP)
- public @interface VideoCaptureError {
- }
-
- enum VideoEncoderInitStatus {
- VIDEO_ENCODER_INIT_STATUS_UNINITIALIZED,
- VIDEO_ENCODER_INIT_STATUS_INITIALIZED_FAILED,
- VIDEO_ENCODER_INIT_STATUS_INSUFFICIENT_RESOURCE,
- VIDEO_ENCODER_INIT_STATUS_RESOURCE_RECLAIMED,
- }
-
- /** Listener containing callbacks for video file I/O events. */
- public interface OnVideoSavedCallback {
- /** Called when the video has been successfully saved. */
- void onVideoSaved(@NonNull OutputFileResults outputFileResults);
-
- /** Called when an error occurs while attempting to save the video. */
- void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
- @Nullable Throwable cause);
- }
-
- /**
- * Provides a base static default configuration for the VideoCapture
- *
- * <p>These values may be overridden by the implementation. They only provide a minimum set of
- * defaults that are implementation independent.
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- public static final class Defaults
- implements ConfigProvider<VideoCaptureConfig> {
- private static final int DEFAULT_VIDEO_FRAME_RATE = 30;
- /** 8Mb/s the recommend rate for 30fps 1080p */
- private static final int DEFAULT_BIT_RATE = 8 * 1024 * 1024;
- /** Seconds between each key frame */
- private static final int DEFAULT_INTRA_FRAME_INTERVAL = 1;
- /** audio bit rate */
- private static final int DEFAULT_AUDIO_BIT_RATE = 64000;
- /** audio sample rate */
- private static final int DEFAULT_AUDIO_SAMPLE_RATE = 8000;
- /** audio channel count */
- private static final int DEFAULT_AUDIO_CHANNEL_COUNT = 1;
- /** audio default minimum buffer size */
- private static final int DEFAULT_AUDIO_MIN_BUFFER_SIZE = 1024;
- /** Current max resolution of VideoCapture is set as FHD */
- private static final Size DEFAULT_MAX_RESOLUTION = new Size(1920, 1080);
- /** Surface occupancy priority to this use case */
- private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 3;
- private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_16_9;
-
- private static final VideoCaptureConfig DEFAULT_CONFIG;
-
- static {
- Builder builder = new Builder()
- .setVideoFrameRate(DEFAULT_VIDEO_FRAME_RATE)
- .setBitRate(DEFAULT_BIT_RATE)
- .setIFrameInterval(DEFAULT_INTRA_FRAME_INTERVAL)
- .setAudioBitRate(DEFAULT_AUDIO_BIT_RATE)
- .setAudioSampleRate(DEFAULT_AUDIO_SAMPLE_RATE)
- .setAudioChannelCount(DEFAULT_AUDIO_CHANNEL_COUNT)
- .setAudioMinBufferSize(DEFAULT_AUDIO_MIN_BUFFER_SIZE)
- .setMaxResolution(DEFAULT_MAX_RESOLUTION)
- .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
- .setTargetAspectRatio(DEFAULT_ASPECT_RATIO);
-
- DEFAULT_CONFIG = builder.getUseCaseConfig();
- }
-
- @NonNull
- @Override
- public VideoCaptureConfig getConfig() {
- return DEFAULT_CONFIG;
- }
- }
-
- /** Holder class for metadata that should be saved alongside captured video. */
- public static final class Metadata {
- /** Data representing a geographic location. */
- @Nullable
- public Location location;
- }
-
- private static final class VideoSavedListenerWrapper implements OnVideoSavedCallback {
-
- @NonNull
- Executor mExecutor;
- @NonNull
- OnVideoSavedCallback mOnVideoSavedCallback;
-
- VideoSavedListenerWrapper(@NonNull Executor executor,
- @NonNull OnVideoSavedCallback onVideoSavedCallback) {
- mExecutor = executor;
- mOnVideoSavedCallback = onVideoSavedCallback;
- }
-
- @Override
- public void onVideoSaved(@NonNull OutputFileResults outputFileResults) {
- try {
- mExecutor.execute(() -> mOnVideoSavedCallback.onVideoSaved(outputFileResults));
- } catch (RejectedExecutionException e) {
- Logger.e(TAG, "Unable to post to the supplied executor.");
- }
- }
-
- @Override
- public void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
- @Nullable Throwable cause) {
- try {
- mExecutor.execute(
- () -> mOnVideoSavedCallback.onError(videoCaptureError, message, cause));
- } catch (RejectedExecutionException e) {
- Logger.e(TAG, "Unable to post to the supplied executor.");
- }
- }
-
- }
-
- /** Builder for a {@link VideoCapture}. */
- @SuppressWarnings("ObjectToString")
- public static final class Builder
- implements
- UseCaseConfig.Builder<VideoCapture, VideoCaptureConfig, Builder>,
- ImageOutputConfig.Builder<Builder>,
- ThreadConfig.Builder<Builder> {
-
- private final MutableOptionsBundle mMutableConfig;
-
- /** Creates a new Builder object. */
- public Builder() {
- this(MutableOptionsBundle.create());
- }
-
- private Builder(@NonNull MutableOptionsBundle mutableConfig) {
- mMutableConfig = mutableConfig;
-
- Class<?> oldConfigClass =
- mutableConfig.retrieveOption(OPTION_TARGET_CLASS, null);
- if (oldConfigClass != null && !oldConfigClass.equals(VideoCapture.class)) {
- throw new IllegalArgumentException(
- "Invalid target class configuration for "
- + Builder.this
- + ": "
- + oldConfigClass);
- }
-
- setTargetClass(VideoCapture.class);
- }
-
- /**
- * Generates a Builder from another Config object.
- *
- * @param configuration An immutable configuration to pre-populate this builder.
- * @return The new Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- static Builder fromConfig(@NonNull Config configuration) {
- return new Builder(MutableOptionsBundle.from(configuration));
- }
-
-
- /**
- * Generates a Builder from another Config object
- *
- * @param configuration An immutable configuration to pre-populate this builder.
- * @return The new Builder.
- */
- @NonNull
- public static Builder fromConfig(@NonNull VideoCaptureConfig configuration) {
- return new Builder(MutableOptionsBundle.from(configuration));
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public MutableConfig getMutableConfig() {
- return mMutableConfig;
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public VideoCaptureConfig getUseCaseConfig() {
- return new VideoCaptureConfig(OptionsBundle.from(mMutableConfig));
- }
-
- /**
- * Builds an immutable {@link VideoCaptureConfig} from the current state.
- *
- * @return A {@link VideoCaptureConfig} populated with the current state.
- */
- @Override
- @NonNull
- public VideoCapture build() {
- VideoCaptureConfig videoCaptureConfig = getUseCaseConfig();
- ImageOutputConfig.validateConfig(videoCaptureConfig);
- return new VideoCapture(videoCaptureConfig);
- }
-
- /**
- * Sets the recording frames per second.
- *
- * @param videoFrameRate The requested interval in seconds.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setVideoFrameRate(int videoFrameRate) {
- getMutableConfig().insertOption(OPTION_VIDEO_FRAME_RATE, videoFrameRate);
- return this;
- }
-
- /**
- * Sets the encoding bit rate.
- *
- * @param bitRate The requested bit rate in bits per second.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setBitRate(int bitRate) {
- getMutableConfig().insertOption(OPTION_BIT_RATE, bitRate);
- return this;
- }
-
- /**
- * Sets number of seconds between each key frame in seconds.
- *
- * @param interval The requested interval in seconds.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setIFrameInterval(int interval) {
- getMutableConfig().insertOption(OPTION_INTRA_FRAME_INTERVAL, interval);
- return this;
- }
-
- /**
- * Sets the bit rate of the audio stream.
- *
- * @param bitRate The requested bit rate in bits/s.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setAudioBitRate(int bitRate) {
- getMutableConfig().insertOption(OPTION_AUDIO_BIT_RATE, bitRate);
- return this;
- }
-
- /**
- * Sets the sample rate of the audio stream.
- *
- * @param sampleRate The requested sample rate in bits/s.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setAudioSampleRate(int sampleRate) {
- getMutableConfig().insertOption(OPTION_AUDIO_SAMPLE_RATE, sampleRate);
- return this;
- }
-
- /**
- * Sets the number of audio channels.
- *
- * @param channelCount The requested number of audio channels.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setAudioChannelCount(int channelCount) {
- getMutableConfig().insertOption(OPTION_AUDIO_CHANNEL_COUNT, channelCount);
- return this;
- }
-
- /**
- * Sets the audio min buffer size.
- *
- * @param minBufferSize The requested audio minimum buffer size, in bytes.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setAudioMinBufferSize(int minBufferSize) {
- getMutableConfig().insertOption(OPTION_AUDIO_MIN_BUFFER_SIZE, minBufferSize);
- return this;
- }
-
- // Implementations of TargetConfig.Builder default methods
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setTargetClass(@NonNull Class<VideoCapture> targetClass) {
- getMutableConfig().insertOption(OPTION_TARGET_CLASS, targetClass);
-
- // If no name is set yet, then generate a unique name
- if (null == getMutableConfig().retrieveOption(OPTION_TARGET_NAME, null)) {
- String targetName = targetClass.getCanonicalName() + "-" + UUID.randomUUID();
- setTargetName(targetName);
- }
-
- return this;
- }
-
- /**
- * Sets the name of the target object being configured, used only for debug logging.
- *
- * <p>The name should be a value that can uniquely identify an instance of the object being
- * configured.
- *
- * <p>If not set, the target name will default to an unique name automatically generated
- * with the class canonical name and random UUID.
- *
- * @param targetName A unique string identifier for the instance of the class being
- * configured.
- * @return the current Builder.
- */
- @Override
- @NonNull
- public Builder setTargetName(@NonNull String targetName) {
- getMutableConfig().insertOption(OPTION_TARGET_NAME, targetName);
- return this;
- }
-
- // Implementations of ImageOutputConfig.Builder default methods
-
- /**
- * Sets the aspect ratio of the intended target for images from this configuration.
- *
- * <p>It is not allowed to set both target aspect ratio and target resolution on the same
- * use case.
- *
- * <p>The target aspect ratio is used as a hint when determining the resulting output aspect
- * ratio which may differ from the request, possibly due to device constraints.
- * Application code should check the resulting output's resolution.
- *
- * <p>If not set, resolutions with aspect ratio 4:3 will be considered in higher
- * priority.
- *
- * @param aspectRatio A {@link AspectRatio} representing the ratio of the
- * target's width and height.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setTargetAspectRatio(@AspectRatio.Ratio int aspectRatio) {
- getMutableConfig().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
- return this;
- }
-
- /**
- * Sets the rotation of the intended target for images from this configuration.
- *
- * <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link
- * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
- * Rotation values are relative to the "natural" rotation, {@link Surface#ROTATION_0}.
- *
- * <p>If not set, the target rotation will default to the value of
- * {@link Display#getRotation()} of the default display at the time the use case is
- * created. The use case is fully created once it has been attached to a camera.
- *
- * @param rotation The rotation of the intended target.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setTargetRotation(@RotationValue int rotation) {
- getMutableConfig().insertOption(OPTION_TARGET_ROTATION, rotation);
- return this;
- }
-
- /**
- * Sets the resolution of the intended target from this configuration.
- *
- * <p>The target resolution attempts to establish a minimum bound for the image resolution.
- * The actual image resolution will be the closest available resolution in size that is not
- * smaller than the target resolution, as determined by the Camera implementation. However,
- * if no resolution exists that is equal to or larger than the target resolution, the
- * nearest available resolution smaller than the target resolution will be chosen.
- *
- * <p>It is not allowed to set both target aspect ratio and target resolution on the same
- * use case.
- *
- * <p>The target aspect ratio will also be set the same as the aspect ratio of the provided
- * {@link Size}. Make sure to set the target resolution with the correct orientation.
- *
- * @param resolution The target resolution to choose from supported output sizes list.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setTargetResolution(@NonNull Size resolution) {
- getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION, resolution);
- return this;
- }
-
- /**
- * Sets the default resolution of the intended target from this configuration.
- *
- * @param resolution The default resolution to choose from supported output sizes list.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setDefaultResolution(@NonNull Size resolution) {
- getMutableConfig().insertOption(OPTION_DEFAULT_RESOLUTION, resolution);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setMaxResolution(@NonNull Size resolution) {
- getMutableConfig().insertOption(OPTION_MAX_RESOLUTION, resolution);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setSupportedResolutions(@NonNull List<Pair<Integer, Size[]>> resolutions) {
- getMutableConfig().insertOption(OPTION_SUPPORTED_RESOLUTIONS, resolutions);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
- getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
- return this;
- }
-
- // Implementations of ThreadConfig.Builder default methods
-
- /**
- * Sets the default executor that will be used for background tasks.
- *
- * <p>If not set, the background executor will default to an automatically generated
- * {@link Executor}.
- *
- * @param executor The executor which will be used for background tasks.
- * @return the current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setBackgroundExecutor(@NonNull Executor executor) {
- getMutableConfig().insertOption(OPTION_BACKGROUND_EXECUTOR, executor);
- return this;
- }
-
- // Implementations of UseCaseConfig.Builder default methods
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setDefaultSessionConfig(@NonNull SessionConfig sessionConfig) {
- getMutableConfig().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setDefaultCaptureConfig(@NonNull CaptureConfig captureConfig) {
- getMutableConfig().insertOption(OPTION_DEFAULT_CAPTURE_CONFIG, captureConfig);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setSessionOptionUnpacker(
- @NonNull SessionConfig.OptionUnpacker optionUnpacker) {
- getMutableConfig().insertOption(OPTION_SESSION_CONFIG_UNPACKER, optionUnpacker);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setCaptureOptionUnpacker(
- @NonNull CaptureConfig.OptionUnpacker optionUnpacker) {
- getMutableConfig().insertOption(OPTION_CAPTURE_CONFIG_UNPACKER, optionUnpacker);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setSurfaceOccupancyPriority(int priority) {
- getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY)
- @Override
- @NonNull
- public Builder setCameraSelector(@NonNull CameraSelector cameraSelector) {
- getMutableConfig().insertOption(OPTION_CAMERA_SELECTOR, cameraSelector);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setUseCaseEventCallback(
- @NonNull UseCase.EventCallback useCaseEventCallback) {
- getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_CALLBACK, useCaseEventCallback);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setZslDisabled(boolean disabled) {
- getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setHighResolutionDisabled(boolean disabled) {
- getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
- return this;
- }
- }
-
- /**
- * Info about the saved video file.
- */
- public static class OutputFileResults {
- @Nullable
- private Uri mSavedUri;
-
- OutputFileResults(@Nullable Uri savedUri) {
- mSavedUri = savedUri;
- }
-
- /**
- * Returns the {@link Uri} of the saved video file.
- *
- * <p> This field is only returned if the {@link VideoCapture.OutputFileOptions} is
- * backed by {@link MediaStore} constructed with
- * {@link androidx.camera.core.VideoCapture.OutputFileOptions}.
- */
- @Nullable
- public Uri getSavedUri() {
- return mSavedUri;
- }
- }
-
- /**
- * Options for saving newly captured video.
- *
- * <p> this class is used to configure save location and metadata. Save location can be
- * either a {@link File}, {@link MediaStore}. The metadata will be
- * stored with the saved video.
- */
- public static final class OutputFileOptions {
-
- // Empty metadata object used as a placeholder for no user-supplied metadata.
- // Should be initialized to all default values.
- private static final Metadata EMPTY_METADATA = new Metadata();
-
- @Nullable
- private final File mFile;
- @Nullable
- private final FileDescriptor mFileDescriptor;
- @Nullable
- private final ContentResolver mContentResolver;
- @Nullable
- private final Uri mSaveCollection;
- @Nullable
- private final ContentValues mContentValues;
- @Nullable
- private final Metadata mMetadata;
-
- OutputFileOptions(@Nullable File file,
- @Nullable FileDescriptor fileDescriptor,
- @Nullable ContentResolver contentResolver,
- @Nullable Uri saveCollection,
- @Nullable ContentValues contentValues,
- @Nullable Metadata metadata) {
- mFile = file;
- mFileDescriptor = fileDescriptor;
- mContentResolver = contentResolver;
- mSaveCollection = saveCollection;
- mContentValues = contentValues;
- mMetadata = metadata == null ? EMPTY_METADATA : metadata;
- }
-
- /** Returns the File object which is set by the {@link OutputFileOptions.Builder}. */
- @Nullable
- File getFile() {
- return mFile;
- }
-
- /**
- * Returns the FileDescriptor object which is set by the {@link OutputFileOptions.Builder}.
- */
- @Nullable
- FileDescriptor getFileDescriptor() {
- return mFileDescriptor;
- }
-
- /** Returns the content resolver which is set by the {@link OutputFileOptions.Builder}. */
- @Nullable
- ContentResolver getContentResolver() {
- return mContentResolver;
- }
-
- /** Returns the URI which is set by the {@link OutputFileOptions.Builder}. */
- @Nullable
- Uri getSaveCollection() {
- return mSaveCollection;
- }
-
- /** Returns the content values which is set by the {@link OutputFileOptions.Builder}. */
- @Nullable
- ContentValues getContentValues() {
- return mContentValues;
- }
-
- /** Return the metadata which is set by the {@link OutputFileOptions.Builder}.. */
- @Nullable
- Metadata getMetadata() {
- return mMetadata;
- }
-
- /** Checking the caller wants to save video to MediaStore. */
- boolean isSavingToMediaStore() {
- return getSaveCollection() != null && getContentResolver() != null
- && getContentValues() != null;
- }
-
- /** Checking the caller wants to save video to a File. */
- boolean isSavingToFile() {
- return getFile() != null;
- }
-
- /** Checking the caller wants to save video to a FileDescriptor. */
- boolean isSavingToFileDescriptor() {
- return getFileDescriptor() != null;
- }
-
- /**
- * Builder class for {@link OutputFileOptions}.
- */
- public static final class Builder {
- @Nullable
- private File mFile;
- @Nullable
- private FileDescriptor mFileDescriptor;
- @Nullable
- private ContentResolver mContentResolver;
- @Nullable
- private Uri mSaveCollection;
- @Nullable
- private ContentValues mContentValues;
- @Nullable
- private Metadata mMetadata;
-
- /**
- * Creates options to write captured video to a {@link File}.
- *
- * @param file save location of the video.
- */
- public Builder(@NonNull File file) {
- mFile = file;
- }
-
- /**
- * Creates options to write captured video to a {@link FileDescriptor}.
- *
- * <p>Using a FileDescriptor to record a video is only supported for Android 8.0 or
- * above.
- *
- * @param fileDescriptor to save the video.
- * @throws IllegalArgumentException when the device is not running Android 8.0 or above.
- */
- public Builder(@NonNull FileDescriptor fileDescriptor) {
- Preconditions.checkArgument(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O,
- "Using a FileDescriptor to record a video is only supported for Android 8"
- + ".0 or above.");
-
- mFileDescriptor = fileDescriptor;
- }
-
- /**
- * Creates options to write captured video to {@link MediaStore}.
- *
- * Example:
- *
- * <pre>{@code
- *
- * ContentValues contentValues = new ContentValues();
- * contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "NEW_VIDEO");
- * contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
- *
- * OutputFileOptions options = new OutputFileOptions.Builder(
- * getContentResolver(),
- * MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
- * contentValues).build();
- *
- * }</pre>
- *
- * @param contentResolver to access {@link MediaStore}
- * @param saveCollection The URL of the table to insert into.
- * @param contentValues to be included in the created video file.
- */
- public Builder(@NonNull ContentResolver contentResolver,
- @NonNull Uri saveCollection,
- @NonNull ContentValues contentValues) {
- mContentResolver = contentResolver;
- mSaveCollection = saveCollection;
- mContentValues = contentValues;
- }
-
- /**
- * Sets the metadata to be stored with the saved video.
- *
- * @param metadata Metadata to be stored with the saved video.
- */
- @NonNull
- public Builder setMetadata(@NonNull Metadata metadata) {
- mMetadata = metadata;
- return this;
- }
-
- /**
- * Builds {@link OutputFileOptions}.
- */
- @NonNull
- public OutputFileOptions build() {
- return new OutputFileOptions(mFile, mFileDescriptor, mContentResolver,
- mSaveCollection, mContentValues, mMetadata);
- }
- }
- }
-
- /**
- * Nested class to avoid verification errors for methods introduced in Android 8.0 (API 26).
- */
- @RequiresApi(26)
- private static class Api26Impl {
-
- private Api26Impl() {
- }
-
- @DoNotInline
- @NonNull
- static MediaMuxer createMediaMuxer(@NonNull FileDescriptor fileDescriptor, int format)
- throws IOException {
- return new MediaMuxer(fileDescriptor, format);
- }
- }
-
- /**
- * Nested class to avoid verification errors for methods introduced in Android 6.0 (API 23).
- */
- @RequiresApi(23)
- private static class Api23Impl {
-
- private Api23Impl() {
- }
-
- @DoNotInline
- static int getCodecExceptionErrorCode(MediaCodec.CodecException e) {
- return e.getErrorCode();
- }
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/VideoCaptureConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/VideoCaptureConfig.java
deleted file mode 100644
index db1c480..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/VideoCaptureConfig.java
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core.impl;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.internal.ThreadConfig;
-
-/**
- * Config for a video capture use case.
- *
- * <p>In the earlier stage, the VideoCapture is deprioritized.
- */
-@SuppressWarnings("deprecation")
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public final class VideoCaptureConfig
- implements UseCaseConfig<androidx.camera.core.VideoCapture>,
- ImageOutputConfig,
- ThreadConfig {
-
- // Option Declarations:
- // *********************************************************************************************
-
- public static final Option<Integer> OPTION_VIDEO_FRAME_RATE =
- Option.create("camerax.core.videoCapture.recordingFrameRate", int.class);
- public static final Option<Integer> OPTION_BIT_RATE =
- Option.create("camerax.core.videoCapture.bitRate", int.class);
- public static final Option<Integer> OPTION_INTRA_FRAME_INTERVAL =
- Option.create("camerax.core.videoCapture.intraFrameInterval", int.class);
- public static final Option<Integer> OPTION_AUDIO_BIT_RATE =
- Option.create("camerax.core.videoCapture.audioBitRate", int.class);
- public static final Option<Integer> OPTION_AUDIO_SAMPLE_RATE =
- Option.create("camerax.core.videoCapture.audioSampleRate", int.class);
- public static final Option<Integer> OPTION_AUDIO_CHANNEL_COUNT =
- Option.create("camerax.core.videoCapture.audioChannelCount", int.class);
- public static final Option<Integer> OPTION_AUDIO_MIN_BUFFER_SIZE =
- Option.create("camerax.core.videoCapture.audioMinBufferSize", int.class);
-
- // *********************************************************************************************
-
- private final OptionsBundle mConfig;
-
- public VideoCaptureConfig(@NonNull OptionsBundle config) {
- mConfig = config;
- }
-
- /**
- * Returns the recording frames per second.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getVideoFrameRate(int valueIfMissing) {
- return retrieveOption(OPTION_VIDEO_FRAME_RATE, valueIfMissing);
- }
-
- /**
- * Returns the recording frames per second.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getVideoFrameRate() {
- return retrieveOption(OPTION_VIDEO_FRAME_RATE);
- }
-
- /**
- * Returns the encoding bit rate.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getBitRate(int valueIfMissing) {
- return retrieveOption(OPTION_BIT_RATE, valueIfMissing);
- }
-
- /**
- * Returns the encoding bit rate.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getBitRate() {
- return retrieveOption(OPTION_BIT_RATE);
- }
-
- /**
- * Returns the number of seconds between each key frame.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getIFrameInterval(int valueIfMissing) {
- return retrieveOption(OPTION_INTRA_FRAME_INTERVAL, valueIfMissing);
- }
-
- /**
- * Returns the number of seconds between each key frame.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getIFrameInterval() {
- return retrieveOption(OPTION_INTRA_FRAME_INTERVAL);
- }
-
- /**
- * Returns the audio encoding bit rate.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getAudioBitRate(int valueIfMissing) {
- return retrieveOption(OPTION_AUDIO_BIT_RATE, valueIfMissing);
- }
-
- /**
- * Returns the audio encoding bit rate.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getAudioBitRate() {
- return retrieveOption(OPTION_AUDIO_BIT_RATE);
- }
-
- /**
- * Returns the audio sample rate.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getAudioSampleRate(int valueIfMissing) {
- return retrieveOption(OPTION_AUDIO_SAMPLE_RATE, valueIfMissing);
- }
-
- /**
- * Returns the audio sample rate.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getAudioSampleRate() {
- return retrieveOption(OPTION_AUDIO_SAMPLE_RATE);
- }
-
- /**
- * Returns the audio channel count.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getAudioChannelCount(int valueIfMissing) {
- return retrieveOption(OPTION_AUDIO_CHANNEL_COUNT, valueIfMissing);
- }
-
- /**
- * Returns the audio channel count.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getAudioChannelCount() {
- return retrieveOption(OPTION_AUDIO_CHANNEL_COUNT);
- }
-
- /**
- * Returns the audio minimum buffer size, in bytes.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getAudioMinBufferSize(int valueIfMissing) {
- return retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE, valueIfMissing);
- }
-
- /**
- * Returns the audio minimum buffer size, in bytes.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getAudioMinBufferSize() {
- return retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE);
- }
-
- /**
- * Retrieves the format of the image that is fed as input.
- *
- * <p>This should always be PRIVATE for VideoCapture.
- */
- @Override
- public int getInputFormat() {
- return ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
- }
-
- @NonNull
- @Override
- public Config getConfig() {
- return mConfig;
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/SurfaceSorter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/SurfaceSorter.java
index 9cce3f6..5d79905 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/SurfaceSorter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/SurfaceSorter.java
@@ -60,10 +60,8 @@
});
}
- @SuppressWarnings("deprecation")
private int getSurfacePriority(@NonNull DeferrableSurface surface) {
- if (surface.getContainerClass() == MediaCodec.class
- || surface.getContainerClass() == androidx.camera.core.VideoCapture.class) {
+ if (surface.getContainerClass() == MediaCodec.class) {
return PRIORITY_MEDIA_CODEC_SURFACE;
} else if (surface.getContainerClass() == Preview.class) {
return PRIORITY_PREVIEW_SURFACE;
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
deleted file mode 100644
index 184b2b8..0000000
--- a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core
-
-import android.content.Context
-import android.os.Build
-import android.os.Looper
-import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.internal.utils.SizeUtil
-import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.fakes.FakeAppConfig
-import androidx.camera.testing.fakes.FakeCamera
-import androidx.camera.testing.fakes.FakeCameraFactory
-import androidx.test.core.app.ApplicationProvider
-import java.io.File
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.Shadows.shadowOf
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.internal.DoNotInstrument
-import org.junit.Assert
-
-@RunWith(RobolectricTestRunner::class)
-@Suppress("DEPRECATION")
-@DoNotInstrument
-@Config(
- minSdk = Build.VERSION_CODES.LOLLIPOP
-)
-class VideoCaptureTest {
- @Before
- fun setUp() {
- val camera = FakeCamera()
-
- val cameraFactoryProvider =
- CameraFactory.Provider { _, _, _ ->
- val cameraFactory = FakeCameraFactory()
- cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
- camera
- }
- cameraFactory
- }
- val cameraXConfig = CameraXConfig.Builder.fromConfig(FakeAppConfig.create())
- .setCameraFactoryProvider(cameraFactoryProvider)
- .build()
- val context = ApplicationProvider.getApplicationContext<Context>()
- CameraXUtil.initialize(context, cameraXConfig).get()
- }
-
- @After
- fun tearDown() {
- CameraXUtil.shutdown().get()
- }
-
- @Test
- fun startRecording_beforeUseCaseIsBound() {
- val videoCapture = VideoCapture.Builder().build()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
- val outputFileOptions = VideoCapture.OutputFileOptions.Builder(file).build()
- val callback = mock(VideoCapture.OnVideoSavedCallback::class.java)
- videoCapture.startRecording(
- outputFileOptions,
- CameraXExecutors.mainThreadExecutor(),
- callback
- )
- shadowOf(Looper.getMainLooper()).idle()
-
- verify(callback).onError(eq(VideoCapture.ERROR_INVALID_CAMERA), anyString(), any())
- }
-
- @Test
- fun throwException_whenSetBothTargetResolutionAndAspectRatio() {
- Assert.assertThrows(IllegalArgumentException::class.java) {
- VideoCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
- .setTargetAspectRatio(AspectRatio.RATIO_4_3).build()
- }
- }
-
- @Test
- fun throwException_whenSetTargetResolutionWithResolutionSelector() {
- Assert.assertThrows(IllegalArgumentException::class.java) {
- VideoCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
- .setResolutionSelector(ResolutionSelector.Builder().build())
- .build()
- }
- }
-
- @Test
- fun throwException_whenSetTargetAspectRatioWithResolutionSelector() {
- Assert.assertThrows(IllegalArgumentException::class.java) {
- VideoCapture.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3)
- .setResolutionSelector(ResolutionSelector.Builder().build())
- .build()
- }
- }
-}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/SurfaceSorterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/SurfaceSorterTest.kt
index 2f2c339..92da45c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/SurfaceSorterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/SurfaceSorterTest.kt
@@ -88,50 +88,6 @@
assertThat(outputConfigs6.last()).isEqualTo(videoOutput)
}
- @Suppress("DEPRECATION")
- @Test
- fun sort_previewSurfaceIsInTheFirstAndVideoCaptureSurfaceIsInTheLast() {
- // Arrange.
- val videoOutput = SessionConfig.OutputConfig.builder(
- createSurface(containerClass = androidx.camera.core.VideoCapture::class.java)).build()
- val previewOutput = SessionConfig.OutputConfig.builder(
- createSurface(containerClass = Preview::class.java)).build()
- val imageOutput = SessionConfig.OutputConfig.builder(
- createSurface(containerClass = ImageCapture::class.java)).build()
- val surfaceSorter = SurfaceSorter()
-
- // All combinations
- val outputConfigs1 = mutableListOf(previewOutput, videoOutput, imageOutput)
- val outputConfigs2 = mutableListOf(previewOutput, imageOutput, videoOutput)
- val outputConfigs3 = mutableListOf(videoOutput, previewOutput, imageOutput)
- val outputConfigs4 = mutableListOf(videoOutput, imageOutput, previewOutput)
- val outputConfigs5 = mutableListOf(imageOutput, videoOutput, previewOutput)
- val outputConfigs6 = mutableListOf(imageOutput, previewOutput, videoOutput)
-
- // Act.
- surfaceSorter.sort(outputConfigs1)
- surfaceSorter.sort(outputConfigs2)
- surfaceSorter.sort(outputConfigs3)
- surfaceSorter.sort(outputConfigs4)
- surfaceSorter.sort(outputConfigs5)
- surfaceSorter.sort(outputConfigs6)
-
- // Assert.
- assertThat(outputConfigs1.first()).isEqualTo(previewOutput)
- assertThat(outputConfigs2.first()).isEqualTo(previewOutput)
- assertThat(outputConfigs3.first()).isEqualTo(previewOutput)
- assertThat(outputConfigs4.first()).isEqualTo(previewOutput)
- assertThat(outputConfigs5.first()).isEqualTo(previewOutput)
- assertThat(outputConfigs6.first()).isEqualTo(previewOutput)
-
- assertThat(outputConfigs1.last()).isEqualTo(videoOutput)
- assertThat(outputConfigs2.last()).isEqualTo(videoOutput)
- assertThat(outputConfigs3.last()).isEqualTo(videoOutput)
- assertThat(outputConfigs4.last()).isEqualTo(videoOutput)
- assertThat(outputConfigs5.last()).isEqualTo(videoOutput)
- assertThat(outputConfigs6.last()).isEqualTo(videoOutput)
- }
-
private fun createSurface(
containerClass: Class<*>
): DeferrableSurface {
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
index 88efac3..90e2733 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
@@ -17,11 +17,14 @@
package androidx.camera.extensions
import android.hardware.camera2.CameraCharacteristics
+import androidx.annotation.NonNull
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.core.Camera
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
+import androidx.camera.core.SurfaceRequest
import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.MutableStateObservable
import androidx.camera.extensions.internal.ExtensionVersion
import androidx.camera.extensions.internal.Version
import androidx.camera.extensions.internal.VersionName
@@ -30,6 +33,9 @@
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.fakes.FakeLifecycleOwner
import androidx.camera.testing.fakes.FakeUseCase
+import androidx.camera.video.MediaSpec
+import androidx.camera.video.VideoCapture
+import androidx.camera.video.VideoOutput
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
@@ -404,7 +410,6 @@
}
}
- @Suppress("DEPRECATION")
@Test
fun throwIllegalArgumentException_whenBindingVideoCapture(): Unit = runBlocking {
val extensionCameraSelector = checkExtensionAvailabilityAndInit()
@@ -416,7 +421,7 @@
cameraProvider.bindToLifecycle(
fakeLifecycleOwner,
extensionCameraSelector,
- androidx.camera.core.VideoCapture.Builder().build()
+ createVideoCapture()
)
}
}
@@ -458,4 +463,27 @@
characteristics
) && previewExtenderImpl.isExtensionAvailable(cameraId, characteristics)
}
+
+ private fun createVideoCapture(): VideoCapture<TestVideoOutput> {
+ val mediaSpec = MediaSpec.builder().build()
+ val videoOutput = TestVideoOutput()
+ videoOutput.mediaSpecObservable.setState(mediaSpec)
+ return VideoCapture.withOutput(videoOutput)
+ }
+
+ /** A fake implementation of VideoOutput */
+ private class TestVideoOutput : VideoOutput {
+ val mediaSpecObservable: MutableStateObservable<MediaSpec> =
+ MutableStateObservable.withInitialState(MediaSpec.builder().build())
+ var surfaceRequest: SurfaceRequest? = null
+ var sourceState: VideoOutput.SourceState? = null
+
+ override fun onSurfaceRequested(@NonNull request: SurfaceRequest) {
+ surfaceRequest = request
+ }
+ override fun getMediaSpec() = mediaSpecObservable
+ override fun onSourceStateChanged(@NonNull sourceState: VideoOutput.SourceState) {
+ this.sourceState = sourceState
+ }
+ }
}