Implement compat version of EncoderProfiles
* Add EncoderProfilesProxy to wrap EncoderProfiles information.
* Add EncoderProfilesProxyCompat and corresponded implementations to
handle methods for different API levels and back compatible with
CamcorderProfile.
* Add EncoderProfilesProvider implementations for camera-camera2
and camera-pipe.
* Add codes to handle resolution quirk.
* Add tests and test utils.
For details, please see design doc: go/camerax-encoder-profiles-compat.
Bug: 264514768
Test: manual test and ./gradlew bOS
Change-Id: I4bcb04e644922bf7fcc8e56b4e2a422f7dcc681b
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt
new file mode 100644
index 0000000..c61aa16
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2023 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.pipe.integration
+
+import android.media.CamcorderProfile
+import android.media.EncoderProfiles.VideoProfile.HDR_NONE
+import android.media.EncoderProfiles.VideoProfile.YUV_420
+import android.os.Build
+import androidx.camera.camera2.pipe.integration.adapter.EncoderProfilesProviderAdapter
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_8
+import androidx.camera.testing.CameraUtil
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 21)
+class EncoderProfilesProviderAdapterDeviceTest(private val quality: Int) {
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters
+ fun data(): Array<Array<Int>> = arrayOf(
+ arrayOf(CamcorderProfile.QUALITY_LOW),
+ arrayOf(CamcorderProfile.QUALITY_HIGH),
+ arrayOf(CamcorderProfile.QUALITY_QCIF),
+ arrayOf(CamcorderProfile.QUALITY_CIF),
+ arrayOf(CamcorderProfile.QUALITY_480P),
+ arrayOf(CamcorderProfile.QUALITY_720P),
+ arrayOf(CamcorderProfile.QUALITY_1080P),
+ arrayOf(CamcorderProfile.QUALITY_QVGA),
+ arrayOf(CamcorderProfile.QUALITY_2160P),
+ arrayOf(CamcorderProfile.QUALITY_VGA),
+ arrayOf(CamcorderProfile.QUALITY_4KDCI),
+ arrayOf(CamcorderProfile.QUALITY_QHD),
+ arrayOf(CamcorderProfile.QUALITY_2K)
+ )
+ }
+
+ private lateinit var encoderProfilesProvider: EncoderProfilesProviderAdapter
+ private var cameraId = ""
+ private var intCameraId = -1
+
+ @get:Rule
+ val useCamera = CameraUtil.grantCameraPermissionAndPreTest()
+
+ @Before
+ fun setup() {
+ skipTestOnProblematicBuildsOfCuttlefishApi33()
+ Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+
+ cameraId = CameraUtil.getCameraIdWithLensFacing(CameraSelector.LENS_FACING_BACK)!!
+ intCameraId = cameraId.toInt()
+
+ encoderProfilesProvider = EncoderProfilesProviderAdapter(cameraId)
+ }
+
+ @Test
+ fun hasProfile_returnSameResult() {
+ assertThat(encoderProfilesProvider.hasProfile(quality))
+ .isEqualTo(CamcorderProfile.hasProfile(intCameraId, quality))
+ }
+
+ @Test
+ fun hasProfile_getReturnNonNull() {
+ Assume.assumeTrue(CamcorderProfile.hasProfile(intCameraId, quality))
+
+ assertThat(encoderProfilesProvider.getAll(quality)).isNotNull()
+ }
+
+ @Test
+ fun notHasProfile_getReturnNull() {
+ Assume.assumeTrue(!CamcorderProfile.hasProfile(intCameraId, quality))
+
+ assertThat(encoderProfilesProvider.getAll(quality)).isNull()
+ }
+
+ @Suppress("DEPRECATION")
+ @Test
+ fun hasSameContentAsCamcorderProfile() {
+ Assume.assumeTrue(CamcorderProfile.hasProfile(quality))
+
+ val profile = CamcorderProfile.get(quality)
+ val encoderProfiles = encoderProfilesProvider.getAll(quality)
+ val videoProfile = encoderProfiles!!.videoProfiles[0]
+ val audioProfile = encoderProfiles.audioProfiles[0]
+
+ assertThat(encoderProfiles.defaultDurationSeconds).isEqualTo(profile.duration)
+ assertThat(encoderProfiles.recommendedFileFormat).isEqualTo(profile.fileFormat)
+ assertThat(videoProfile.codec).isEqualTo(profile.videoCodec)
+ assertThat(videoProfile.bitrate).isEqualTo(profile.videoBitRate)
+ assertThat(videoProfile.frameRate).isEqualTo(profile.videoFrameRate)
+ assertThat(videoProfile.width).isEqualTo(profile.videoFrameWidth)
+ assertThat(videoProfile.height).isEqualTo(profile.videoFrameHeight)
+ assertThat(audioProfile.codec).isEqualTo(profile.audioCodec)
+ assertThat(audioProfile.bitrate).isEqualTo(profile.audioBitRate)
+ assertThat(audioProfile.sampleRate).isEqualTo(profile.audioSampleRate)
+ assertThat(audioProfile.channels).isEqualTo(profile.audioChannels)
+ }
+
+ @SdkSuppress(minSdkVersion = 31, maxSdkVersion = 32)
+ @Test
+ fun api31Api32_hasSameContentAsEncoderProfiles() {
+ Assume.assumeTrue(CamcorderProfile.hasProfile(quality))
+
+ val profiles = CamcorderProfile.getAll(cameraId, quality)
+ val video = profiles!!.videoProfiles[0]
+ val audio = profiles.audioProfiles[0]
+ val profilesProxy = encoderProfilesProvider.getAll(quality)
+ val videoProxy = profilesProxy!!.videoProfiles[0]
+ val audioProxy = profilesProxy.audioProfiles[0]
+
+ assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
+ assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
+ assertThat(videoProxy.codec).isEqualTo(video.codec)
+ assertThat(videoProxy.mediaType).isEqualTo(video.mediaType)
+ assertThat(videoProxy.bitrate).isEqualTo(video.bitrate)
+ assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
+ assertThat(videoProxy.width).isEqualTo(video.width)
+ assertThat(videoProxy.height).isEqualTo(video.height)
+ assertThat(videoProxy.profile).isEqualTo(video.profile)
+ assertThat(videoProxy.bitDepth).isEqualTo(BIT_DEPTH_8)
+ assertThat(videoProxy.chromaSubsampling).isEqualTo(YUV_420)
+ assertThat(videoProxy.hdrFormat).isEqualTo(HDR_NONE)
+ assertThat(audioProxy.codec).isEqualTo(audio.codec)
+ assertThat(audioProxy.mediaType).isEqualTo(audio.mediaType)
+ assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
+ assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
+ assertThat(audioProxy.channels).isEqualTo(audio.channels)
+ assertThat(audioProxy.profile).isEqualTo(audio.profile)
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ fun afterApi33_hasSameContentAsEncoderProfiles() {
+ Assume.assumeTrue(CamcorderProfile.hasProfile(quality))
+
+ val profiles = CamcorderProfile.getAll(cameraId, quality)
+ val video = profiles!!.videoProfiles[0]
+ val audio = profiles.audioProfiles[0]
+ val profilesProxy = encoderProfilesProvider.getAll(quality)
+ val videoProxy = profilesProxy!!.videoProfiles[0]
+ val audioProxy = profilesProxy.audioProfiles[0]
+
+ assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
+ assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
+ assertThat(videoProxy.codec).isEqualTo(video.codec)
+ assertThat(videoProxy.mediaType).isEqualTo(video.mediaType)
+ assertThat(videoProxy.bitrate).isEqualTo(video.bitrate)
+ assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
+ assertThat(videoProxy.width).isEqualTo(video.width)
+ assertThat(videoProxy.height).isEqualTo(video.height)
+ assertThat(videoProxy.profile).isEqualTo(video.profile)
+ assertThat(videoProxy.bitDepth).isEqualTo(video.bitDepth)
+ assertThat(videoProxy.chromaSubsampling).isEqualTo(video.chromaSubsampling)
+ assertThat(videoProxy.hdrFormat).isEqualTo(video.hdrFormat)
+ assertThat(audioProxy.codec).isEqualTo(audio.codec)
+ assertThat(audioProxy.mediaType).isEqualTo(audio.mediaType)
+ assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
+ assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
+ assertThat(audioProxy.channels).isEqualTo(audio.channels)
+ assertThat(audioProxy.profile).isEqualTo(audio.profile)
+ }
+
+ // TODO: removes after b/265613005 is fixed
+ private fun skipTestOnProblematicBuildsOfCuttlefishApi33() {
+ // Skip test for b/265613005
+ Assume.assumeFalse(
+ "Cuttlefish has null VideoProfile issue. Unable to test.",
+ Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 33 &&
+ Build.ID.startsWith("TP1A")
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EncoderProfilesProviderAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EncoderProfilesProviderAdapter.kt
new file mode 100644
index 0000000..224eeab
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EncoderProfilesProviderAdapter.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023 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.pipe.integration.adapter
+
+import android.media.CamcorderProfile
+import android.media.EncoderProfiles
+import android.os.Build
+import androidx.annotation.DoNotInline
+import androidx.annotation.Nullable
+import androidx.annotation.RequiresApi
+import androidx.camera.core.Logger
+import androidx.camera.core.impl.EncoderProfilesProvider
+import androidx.camera.core.impl.EncoderProfilesProxy
+import androidx.camera.core.impl.compat.EncoderProfilesProxyCompat
+
+/**
+ * Adapt the [EncoderProfilesProvider] interface to [CameraPipe].
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+class EncoderProfilesProviderAdapter(private val cameraIdString: String) : EncoderProfilesProvider {
+ private val hasValidCameraId: Boolean
+ private val cameraId: Int
+
+ init {
+ var hasValidCameraId = false
+ var intCameraId = -1
+ try {
+ intCameraId = cameraIdString.toInt()
+ hasValidCameraId = true
+ } catch (e: NumberFormatException) {
+ Logger.w(
+ TAG, "Camera id is not an integer: $cameraIdString, unable to create" +
+ " EncoderProfilesProviderAdapter."
+ )
+ }
+ this.hasValidCameraId = hasValidCameraId
+ cameraId = intCameraId
+
+ // TODO(b/241296464): CamcorderProfileResolutionQuirk
+ }
+
+ override fun hasProfile(quality: Int): Boolean {
+ if (!hasValidCameraId) {
+ return false
+ }
+ return CamcorderProfile.hasProfile(cameraId, quality)
+ }
+
+ override fun getAll(quality: Int): EncoderProfilesProxy? {
+ if (!hasValidCameraId) {
+ return null
+ }
+ if (!CamcorderProfile.hasProfile(cameraId, quality)) {
+ return null
+ }
+ return getProfilesInternal(quality)
+ }
+
+ @Nullable
+ @Suppress("DEPRECATION")
+ private fun getProfilesInternal(quality: Int): EncoderProfilesProxy? {
+ return if (Build.VERSION.SDK_INT >= 31) {
+ val profiles: EncoderProfiles? = Api31Impl.getAll(cameraIdString, quality)
+ if (profiles != null) EncoderProfilesProxyCompat.from(profiles) else null
+ } else {
+ var profile: CamcorderProfile? = null
+ try {
+ profile = CamcorderProfile.get(cameraId, quality)
+ } catch (e: RuntimeException) {
+ // CamcorderProfile.get() will throw
+ // - RuntimeException if not able to retrieve camcorder profile params.
+ // - IllegalArgumentException if quality is not valid.
+ Logger.w(TAG, "Unable to get CamcorderProfile by quality: $quality", e)
+ }
+ if (profile != null) EncoderProfilesProxyCompat.from(profile) else null
+ }
+ }
+
+ @RequiresApi(31)
+ internal object Api31Impl {
+ @DoNotInline
+ fun getAll(cameraId: String, quality: Int): EncoderProfiles? {
+ return CamcorderProfile.getAll(cameraId, quality)
+ }
+ }
+
+ companion object {
+ private const val TAG = "EncoderProfilesProviderAdapter"
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProviderTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProviderTest.kt
new file mode 100644
index 0000000..8a87d6a
--- /dev/null
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProviderTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2023 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.internal
+
+import android.media.CamcorderProfile
+import android.media.EncoderProfiles.VideoProfile.YUV_420
+import android.media.EncoderProfiles.VideoProfile.HDR_NONE
+import android.os.Build
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_8
+import androidx.camera.testing.CameraUtil
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+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.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 21)
+class Camera2EncoderProfilesProviderTest(private val quality: Int) {
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters
+ fun data(): Array<Array<Int>> = arrayOf(
+ arrayOf(CamcorderProfile.QUALITY_LOW),
+ arrayOf(CamcorderProfile.QUALITY_HIGH),
+ arrayOf(CamcorderProfile.QUALITY_QCIF),
+ arrayOf(CamcorderProfile.QUALITY_CIF),
+ arrayOf(CamcorderProfile.QUALITY_480P),
+ arrayOf(CamcorderProfile.QUALITY_720P),
+ arrayOf(CamcorderProfile.QUALITY_1080P),
+ arrayOf(CamcorderProfile.QUALITY_QVGA),
+ arrayOf(CamcorderProfile.QUALITY_2160P),
+ arrayOf(CamcorderProfile.QUALITY_VGA),
+ arrayOf(CamcorderProfile.QUALITY_4KDCI),
+ arrayOf(CamcorderProfile.QUALITY_QHD),
+ arrayOf(CamcorderProfile.QUALITY_2K)
+ )
+ }
+
+ private lateinit var encoderProfilesProvider: Camera2EncoderProfilesProvider
+ private var cameraId = ""
+ private var intCameraId = -1
+
+ @get:Rule
+ val useCamera = CameraUtil.grantCameraPermissionAndPreTest()
+
+ @Before
+ fun setup() {
+ skipTestOnProblematicBuildsOfCuttlefishApi33()
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+
+ cameraId = CameraUtil.getCameraIdWithLensFacing(CameraSelector.LENS_FACING_BACK)!!
+ intCameraId = cameraId.toInt()
+
+ encoderProfilesProvider = Camera2EncoderProfilesProvider(cameraId)
+ }
+
+ @Test
+ fun hasProfile_returnSameResult() {
+ assertThat(encoderProfilesProvider.hasProfile(quality))
+ .isEqualTo(CamcorderProfile.hasProfile(intCameraId, quality))
+ }
+
+ @Test
+ fun hasProfile_getReturnNonNull() {
+ assumeTrue(CamcorderProfile.hasProfile(intCameraId, quality))
+
+ assertThat(encoderProfilesProvider.getAll(quality)).isNotNull()
+ }
+
+ @Test
+ fun notHasProfile_getReturnNull() {
+ assumeTrue(!CamcorderProfile.hasProfile(intCameraId, quality))
+
+ assertThat(encoderProfilesProvider.getAll(quality)).isNull()
+ }
+
+ @Suppress("DEPRECATION")
+ @Test
+ fun hasSameContentAsCamcorderProfile() {
+ assumeTrue(CamcorderProfile.hasProfile(quality))
+
+ val profile = CamcorderProfile.get(quality)
+ val encoderProfiles = encoderProfilesProvider.getAll(quality)
+ val videoProfile = encoderProfiles!!.videoProfiles[0]
+ val audioProfile = encoderProfiles.audioProfiles[0]
+
+ assertThat(encoderProfiles.defaultDurationSeconds).isEqualTo(profile.duration)
+ assertThat(encoderProfiles.recommendedFileFormat).isEqualTo(profile.fileFormat)
+ assertThat(videoProfile.codec).isEqualTo(profile.videoCodec)
+ assertThat(videoProfile.bitrate).isEqualTo(profile.videoBitRate)
+ assertThat(videoProfile.frameRate).isEqualTo(profile.videoFrameRate)
+ assertThat(videoProfile.width).isEqualTo(profile.videoFrameWidth)
+ assertThat(videoProfile.height).isEqualTo(profile.videoFrameHeight)
+ assertThat(audioProfile.codec).isEqualTo(profile.audioCodec)
+ assertThat(audioProfile.bitrate).isEqualTo(profile.audioBitRate)
+ assertThat(audioProfile.sampleRate).isEqualTo(profile.audioSampleRate)
+ assertThat(audioProfile.channels).isEqualTo(profile.audioChannels)
+ }
+
+ @SdkSuppress(minSdkVersion = 31, maxSdkVersion = 32)
+ @Test
+ fun api31Api32_hasSameContentAsEncoderProfiles() {
+ assumeTrue(CamcorderProfile.hasProfile(quality))
+
+ val profiles = CamcorderProfile.getAll(cameraId, quality)
+ val video = profiles!!.videoProfiles[0]
+ val audio = profiles.audioProfiles[0]
+ val profilesProxy = encoderProfilesProvider.getAll(quality)
+ val videoProxy = profilesProxy!!.videoProfiles[0]
+ val audioProxy = profilesProxy.audioProfiles[0]
+
+ assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
+ assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
+ assertThat(videoProxy.codec).isEqualTo(video.codec)
+ assertThat(videoProxy.mediaType).isEqualTo(video.mediaType)
+ assertThat(videoProxy.bitrate).isEqualTo(video.bitrate)
+ assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
+ assertThat(videoProxy.width).isEqualTo(video.width)
+ assertThat(videoProxy.height).isEqualTo(video.height)
+ assertThat(videoProxy.profile).isEqualTo(video.profile)
+ assertThat(videoProxy.bitDepth).isEqualTo(BIT_DEPTH_8)
+ assertThat(videoProxy.chromaSubsampling).isEqualTo(YUV_420)
+ assertThat(videoProxy.hdrFormat).isEqualTo(HDR_NONE)
+ assertThat(audioProxy.codec).isEqualTo(audio.codec)
+ assertThat(audioProxy.mediaType).isEqualTo(audio.mediaType)
+ assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
+ assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
+ assertThat(audioProxy.channels).isEqualTo(audio.channels)
+ assertThat(audioProxy.profile).isEqualTo(audio.profile)
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ fun afterApi33_hasSameContentAsEncoderProfiles() {
+ assumeTrue(CamcorderProfile.hasProfile(quality))
+
+ val profiles = CamcorderProfile.getAll(cameraId, quality)
+ val video = profiles!!.videoProfiles[0]
+ val audio = profiles.audioProfiles[0]
+ val profilesProxy = encoderProfilesProvider.getAll(quality)
+ val videoProxy = profilesProxy!!.videoProfiles[0]
+ val audioProxy = profilesProxy.audioProfiles[0]
+
+ assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
+ assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
+ assertThat(videoProxy.codec).isEqualTo(video.codec)
+ assertThat(videoProxy.mediaType).isEqualTo(video.mediaType)
+ assertThat(videoProxy.bitrate).isEqualTo(video.bitrate)
+ assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
+ assertThat(videoProxy.width).isEqualTo(video.width)
+ assertThat(videoProxy.height).isEqualTo(video.height)
+ assertThat(videoProxy.profile).isEqualTo(video.profile)
+ assertThat(videoProxy.bitDepth).isEqualTo(video.bitDepth)
+ assertThat(videoProxy.chromaSubsampling).isEqualTo(video.chromaSubsampling)
+ assertThat(videoProxy.hdrFormat).isEqualTo(video.hdrFormat)
+ assertThat(audioProxy.codec).isEqualTo(audio.codec)
+ assertThat(audioProxy.mediaType).isEqualTo(audio.mediaType)
+ assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
+ assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
+ assertThat(audioProxy.channels).isEqualTo(audio.channels)
+ assertThat(audioProxy.profile).isEqualTo(audio.profile)
+ }
+
+ // TODO: removes after b/265613005 is fixed
+ private fun skipTestOnProblematicBuildsOfCuttlefishApi33() {
+ // Skip test for b/265613005
+ Assume.assumeFalse(
+ "Cuttlefish has null VideoProfile issue. Unable to test.",
+ Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 33 &&
+ Build.ID.startsWith("TP1A")
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProvider.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProvider.java
new file mode 100644
index 0000000..a35596c
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProvider.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2023 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.internal;
+
+import android.media.CamcorderProfile;
+import android.media.EncoderProfiles;
+import android.os.Build;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.EncoderProfilesProvider;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.compat.EncoderProfilesProxyCompat;
+
+/** An implementation that provides the {@link EncoderProfilesProxy}. */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class Camera2EncoderProfilesProvider implements EncoderProfilesProvider {
+
+ private static final String TAG = "Camera2EncoderProfilesProvider";
+
+ private final boolean mHasValidCameraId;
+ private final String mCameraId;
+ private final int mIntCameraId;
+
+ public Camera2EncoderProfilesProvider(@NonNull String cameraId) {
+ mCameraId = cameraId;
+ boolean hasValidCameraId = false;
+ int intCameraId = -1;
+ try {
+ intCameraId = Integer.parseInt(cameraId);
+ hasValidCameraId = true;
+ } catch (NumberFormatException e) {
+ Logger.w(TAG, "Camera id is not an integer: " + cameraId
+ + ", unable to create Camera2EncoderProfilesProvider");
+ }
+ mHasValidCameraId = hasValidCameraId;
+ mIntCameraId = intCameraId;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasProfile(int quality) {
+ if (!mHasValidCameraId) {
+ return false;
+ }
+
+ return CamcorderProfile.hasProfile(mIntCameraId, quality);
+ }
+
+ /** {@inheritDoc} */
+ @Nullable
+ @Override
+ public EncoderProfilesProxy getAll(int quality) {
+ if (!mHasValidCameraId) {
+ return null;
+ }
+
+ if (!CamcorderProfile.hasProfile(mIntCameraId, quality)) {
+ return null;
+ }
+
+ return getProfilesInternal(quality);
+ }
+
+ @Nullable
+ @SuppressWarnings("deprecation")
+ private EncoderProfilesProxy getProfilesInternal(int quality) {
+ if (Build.VERSION.SDK_INT >= 31) {
+ EncoderProfiles profiles = Api31Impl.getAll(mCameraId, quality);
+ return profiles != null ? EncoderProfilesProxyCompat.from(profiles) : null;
+ } else {
+ CamcorderProfile profile = null;
+ try {
+ profile = CamcorderProfile.get(mIntCameraId, quality);
+ } catch (RuntimeException e) {
+ // CamcorderProfile.get() will throw
+ // - RuntimeException if not able to retrieve camcorder profile params.
+ // - IllegalArgumentException if quality is not valid.
+ Logger.w(TAG, "Unable to get CamcorderProfile by quality: " + quality, e);
+ }
+ return profile != null ? EncoderProfilesProxyCompat.from(profile) : null;
+ }
+ }
+
+ @RequiresApi(31)
+ static class Api31Impl {
+ @DoNotInline
+ static EncoderProfiles getAll(String cameraId, int quality) {
+ return CamcorderProfile.getAll(cameraId, quality);
+ }
+
+ // This class is not instantiable.
+ private Api31Impl() {
+ }
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java
index fb2d013..86a0c16 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java
@@ -28,7 +28,7 @@
import androidx.camera.camera2.internal.compat.workaround.CamcorderProfileResolutionValidator;
import androidx.camera.core.Logger;
import androidx.camera.core.impl.ImageFormatConstants;
-import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk;
import java.util.ArrayList;
import java.util.Arrays;
@@ -55,7 +55,7 @@
* @see CamcorderProfileResolutionValidator
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public class CamcorderProfileResolutionQuirk implements Quirk {
+public class CamcorderProfileResolutionQuirk implements ProfileResolutionQuirk {
private static final String TAG = "CamcorderProfileResolutionQuirk";
static boolean load(@NonNull CameraCharacteristicsCompat characteristicsCompat) {
@@ -86,6 +86,7 @@
}
/** Returns the supported video resolutions. */
+ @Override
@NonNull
public List<Size> getSupportedResolutions() {
return new ArrayList<>(mSupportedResolutions);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProvider.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProvider.java
new file mode 100644
index 0000000..1e041d6
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProvider.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 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 android.media.CamcorderProfile;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+/**
+ * EncoderProfilesProvider is used to obtain the {@link EncoderProfilesProxy}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface EncoderProfilesProvider {
+
+ /**
+ * Checks if the quality is supported on this device.
+ *
+ * <p>The quality should be one of quality constants defined in {@link CamcorderProfile}.
+ */
+ boolean hasProfile(int quality);
+
+ /**
+ * Gets the {@link EncoderProfilesProxy} if the quality is supported on the device.
+ *
+ * <p>The quality should be one of quality constants defined in {@link CamcorderProfile}.
+ *
+ * @see #hasProfile(int)
+ */
+ @Nullable
+ EncoderProfilesProxy getAll(int quality);
+
+ /** An implementation that contains no data. */
+ EncoderProfilesProvider EMPTY = new EncoderProfilesProvider() {
+ @Override
+ public boolean hasProfile(int quality) {
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public EncoderProfilesProxy getAll(int quality) {
+ return null;
+ }
+ };
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java
new file mode 100644
index 0000000..963e3d6
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2023 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 static android.media.MediaRecorder.AudioEncoder.AAC;
+import static android.media.MediaRecorder.AudioEncoder.AAC_ELD;
+import static android.media.MediaRecorder.AudioEncoder.AMR_NB;
+import static android.media.MediaRecorder.AudioEncoder.AMR_WB;
+import static android.media.MediaRecorder.AudioEncoder.HE_AAC;
+import static android.media.MediaRecorder.AudioEncoder.OPUS;
+import static android.media.MediaRecorder.AudioEncoder.VORBIS;
+import static android.media.MediaRecorder.VideoEncoder.AV1;
+import static android.media.MediaRecorder.VideoEncoder.DOLBY_VISION;
+import static android.media.MediaRecorder.VideoEncoder.H263;
+import static android.media.MediaRecorder.VideoEncoder.H264;
+import static android.media.MediaRecorder.VideoEncoder.HEVC;
+import static android.media.MediaRecorder.VideoEncoder.MPEG_4_SP;
+import static android.media.MediaRecorder.VideoEncoder.VP8;
+import static android.media.MediaRecorder.VideoEncoder.VP9;
+
+import static java.util.Collections.unmodifiableList;
+
+import android.media.EncoderProfiles;
+import android.media.MediaRecorder;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.google.auto.value.AutoValue;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * EncoderProfilesProxy defines the get methods that is mapping to the fields of
+ * {@link EncoderProfiles}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@AutoValue
+public abstract class EncoderProfilesProxy {
+
+ /** Constant representing no codec profile. */
+ public static final int CODEC_PROFILE_NONE = -1;
+
+ /** Creates an EncoderProfilesProxy instance. */
+ @NonNull
+ public static EncoderProfilesProxy create(
+ int defaultDurationSeconds,
+ int recommendedFileFormat,
+ @NonNull List<AudioProfileProxy> audioProfiles,
+ @NonNull List<VideoProfileProxy> videoProfiles) {
+ return new AutoValue_EncoderProfilesProxy(
+ defaultDurationSeconds,
+ recommendedFileFormat,
+ unmodifiableList(new ArrayList<>(audioProfiles)),
+ unmodifiableList(new ArrayList<>(videoProfiles))
+ );
+ }
+
+ /** @see EncoderProfiles#getDefaultDurationSeconds() */
+ public abstract int getDefaultDurationSeconds();
+
+ /** @see EncoderProfiles#getRecommendedFileFormat() */
+ public abstract int getRecommendedFileFormat();
+
+ /** @see EncoderProfiles#getAudioProfiles() */
+ @SuppressWarnings("AutoValueImmutableFields")
+ @NonNull
+ public abstract List<AudioProfileProxy> getAudioProfiles();
+
+ /** @see EncoderProfiles#getVideoProfiles() */
+ @SuppressWarnings("AutoValueImmutableFields")
+ @NonNull
+ public abstract List<VideoProfileProxy> getVideoProfiles();
+
+ /**
+ * VideoProfileProxy defines the get methods that is mapping to the fields of
+ * {@link EncoderProfiles.VideoProfile}.
+ */
+ @AutoValue
+ public abstract static class VideoProfileProxy {
+
+ /** Constant representing no media type. */
+ public static final String MEDIA_TYPE_NONE = "video/none";
+
+ /** Constant representing bit depth 8. */
+ public static final int BIT_DEPTH_8 = 8;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({H263, H264, HEVC, VP8, MPEG_4_SP, VP9, DOLBY_VISION, AV1,
+ MediaRecorder.VideoEncoder.DEFAULT})
+ public @interface VideoEncoder {
+ }
+
+ /** Creates a VideoProfileProxy instance. */
+ @NonNull
+ public static VideoProfileProxy create(
+ @VideoEncoder int codec,
+ @NonNull String mediaType,
+ int bitrate,
+ int frameRate,
+ int width,
+ int height,
+ int profile,
+ int bitDepth,
+ int chromaSubsampling,
+ int hdrFormat) {
+ return new AutoValue_EncoderProfilesProxy_VideoProfileProxy(
+ codec,
+ mediaType,
+ bitrate,
+ frameRate,
+ width,
+ height,
+ profile,
+ bitDepth,
+ chromaSubsampling,
+ hdrFormat
+ );
+ }
+
+ /** @see EncoderProfiles.VideoProfile#getCodec() */
+ @VideoEncoder
+ public abstract int getCodec();
+
+ /** @see EncoderProfiles.VideoProfile#getMediaType() */
+ @NonNull
+ public abstract String getMediaType();
+
+ /** @see EncoderProfiles.VideoProfile#getBitrate() */
+ public abstract int getBitrate();
+
+ /** @see EncoderProfiles.VideoProfile#getFrameRate() */
+ public abstract int getFrameRate();
+
+ /** @see EncoderProfiles.VideoProfile#getWidth() */
+ public abstract int getWidth();
+
+ /** @see EncoderProfiles.VideoProfile#getHeight() */
+ public abstract int getHeight();
+
+ /** @see EncoderProfiles.VideoProfile#getProfile() */
+ public abstract int getProfile();
+
+ /** @see EncoderProfiles.VideoProfile#getBitDepth() */
+ public abstract int getBitDepth();
+
+ /** @see EncoderProfiles.VideoProfile#getChromaSubsampling() */
+ public abstract int getChromaSubsampling();
+
+ /** @see EncoderProfiles.VideoProfile#getHdrFormat() */
+ public abstract int getHdrFormat();
+ }
+
+ /**
+ * AudioProfileProxy defines the get methods that is mapping to the fields of
+ * {@link EncoderProfiles.AudioProfile}.
+ */
+ @AutoValue
+ public abstract static class AudioProfileProxy {
+
+ /** Constant representing no media type. */
+ public static final String MEDIA_TYPE_NONE = "audio/none";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({AAC, AAC_ELD, AMR_NB, AMR_WB, HE_AAC, OPUS, VORBIS,
+ MediaRecorder.AudioEncoder.DEFAULT})
+ public @interface AudioEncoder {
+ }
+
+ /** Creates an AudioProfileProxy instance. */
+ @NonNull
+ public static AudioProfileProxy create(
+ @AudioEncoder int codec,
+ @NonNull String mediaType,
+ int bitRate,
+ int sampleRate,
+ int channels,
+ int profile) {
+ return new AutoValue_EncoderProfilesProxy_AudioProfileProxy(
+ codec,
+ mediaType,
+ bitRate,
+ sampleRate,
+ channels,
+ profile
+ );
+ }
+
+ /** @see EncoderProfiles.AudioProfile#getCodec() */
+ @AudioEncoder
+ public abstract int getCodec();
+
+ /** @see EncoderProfiles.AudioProfile#getMediaType() */
+ @NonNull
+ public abstract String getMediaType();
+
+ /** @see EncoderProfiles.AudioProfile#getBitrate() */
+ public abstract int getBitrate();
+
+ /** @see EncoderProfiles.AudioProfile#getSampleRate() */
+ public abstract int getSampleRate();
+
+ /** @see EncoderProfiles.AudioProfile#getChannels() */
+ public abstract int getChannels();
+
+ /** @see EncoderProfiles.AudioProfile#getProfile() */
+ public abstract int getProfile();
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesResolutionValidator.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesResolutionValidator.java
new file mode 100644
index 0000000..0d84cd4
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesResolutionValidator.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2023 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 android.media.EncoderProfiles;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Validates the video resolution of {@link EncoderProfiles}.
+ *
+ * @see ProfileResolutionQuirk
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class EncoderProfilesResolutionValidator {
+
+ @NonNull
+ private final List<ProfileResolutionQuirk> mQuirks;
+ @NonNull
+ private final Set<Size> mSupportedResolutions;
+
+ public EncoderProfilesResolutionValidator(@Nullable List<ProfileResolutionQuirk> quirks) {
+ mQuirks = new ArrayList<>();
+ if (quirks != null) {
+ mQuirks.addAll(quirks);
+ }
+
+ mSupportedResolutions = generateSupportedResolutions(quirks);
+ }
+
+ @NonNull
+ private Set<Size> generateSupportedResolutions(@Nullable List<ProfileResolutionQuirk> quirks) {
+ if (quirks == null || quirks.isEmpty()) {
+ return Collections.emptySet();
+ }
+
+ Set<Size> supportedResolutions = new HashSet<>(quirks.get(0).getSupportedResolutions());
+ for (int i = 1; i < quirks.size(); i++) {
+ supportedResolutions.retainAll(quirks.get(i).getSupportedResolutions());
+ }
+
+ return supportedResolutions;
+ }
+
+ /** Checks if this validator contains quirk. */
+ public boolean hasQuirk() {
+ return !mQuirks.isEmpty();
+ }
+
+ /** Checks if any video resolution of EncoderProfiles is valid. */
+ public boolean hasValidVideoResolution(@Nullable EncoderProfilesProxy profiles) {
+ if (profiles == null) {
+ return false;
+ }
+
+ if (!hasQuirk()) {
+ return !profiles.getVideoProfiles().isEmpty();
+ }
+
+ boolean hasValidResolution = false;
+ for (VideoProfileProxy videoProfile : profiles.getVideoProfiles()) {
+ Size videoSize = new Size(videoProfile.getWidth(), videoProfile.getHeight());
+ if (mSupportedResolutions.contains(videoSize)) {
+ hasValidResolution = true;
+ break;
+ }
+ }
+
+ return hasValidResolution;
+ }
+
+ /** Returns an {@link EncoderProfilesProxy} that filters out invalid resolutions. */
+ @Nullable
+ public EncoderProfilesProxy filterInvalidVideoResolution(
+ @Nullable EncoderProfilesProxy profiles) {
+ if (profiles == null) {
+ return null;
+ }
+
+ if (!hasQuirk()) {
+ return profiles;
+ }
+
+ List<VideoProfileProxy> validVideoProfiles = new ArrayList<>();
+ for (VideoProfileProxy videoProfile : profiles.getVideoProfiles()) {
+ Size videoSize = new Size(videoProfile.getWidth(), videoProfile.getHeight());
+ if (mSupportedResolutions.contains(videoSize)) {
+ validVideoProfiles.add(videoProfile);
+ }
+ }
+
+ return validVideoProfiles.isEmpty() ? null : EncoderProfilesProxy.create(
+ profiles.getDefaultDurationSeconds(),
+ profiles.getRecommendedFileFormat(),
+ profiles.getAudioProfiles(),
+ validVideoProfiles
+ );
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProvider.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProvider.java
new file mode 100644
index 0000000..df32c6d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProvider.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 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.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk;
+
+import java.util.List;
+
+/**
+ * An implementation that provides the {@link EncoderProfilesProxy} whose video resolutions are
+ * validated.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ResolutionValidatedEncoderProfilesProvider implements EncoderProfilesProvider {
+
+ private final EncoderProfilesProvider mProvider;
+ private final EncoderProfilesResolutionValidator mEncoderProfilesResolutionValidator;
+
+ public ResolutionValidatedEncoderProfilesProvider(@NonNull EncoderProfilesProvider provider,
+ @NonNull Quirks quirks) {
+ mProvider = provider;
+ List<ProfileResolutionQuirk> resolutionQuirks = quirks.getAll(ProfileResolutionQuirk.class);
+ mEncoderProfilesResolutionValidator = new EncoderProfilesResolutionValidator(
+ resolutionQuirks);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasProfile(int quality) {
+ if (!mProvider.hasProfile(quality)) {
+ return false;
+ }
+
+ if (mEncoderProfilesResolutionValidator.hasQuirk()) {
+ EncoderProfilesProxy profiles = mProvider.getAll(quality);
+ return mEncoderProfilesResolutionValidator.hasValidVideoResolution(profiles);
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Nullable
+ @Override
+ public EncoderProfilesProxy getAll(int quality) {
+ if (!mProvider.hasProfile(quality)) {
+ return null;
+ }
+
+ EncoderProfilesProxy profiles = mProvider.getAll(quality);
+ if (mEncoderProfilesResolutionValidator.hasQuirk()) {
+ profiles = mEncoderProfilesResolutionValidator.filterInvalidVideoResolution(profiles);
+ }
+
+ return profiles;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompat.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompat.java
new file mode 100644
index 0000000..cd50106
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompat.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023 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.compat;
+
+import android.media.CamcorderProfile;
+import android.media.EncoderProfiles;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+
+/**
+ * Helper for accessing features of {@link EncoderProfiles} and {@link CamcorderProfile} in a
+ * backwards compatible fashion.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public final class EncoderProfilesProxyCompat {
+
+ /** Creates an EncoderProfilesProxy instance from {@link EncoderProfiles}. */
+ @RequiresApi(31)
+ @NonNull
+ public static EncoderProfilesProxy from(@NonNull EncoderProfiles encoderProfiles) {
+ if (Build.VERSION.SDK_INT >= 33) {
+ return EncoderProfilesProxyCompatApi33Impl.from(encoderProfiles);
+ } else if (Build.VERSION.SDK_INT >= 31) {
+ return EncoderProfilesProxyCompatApi31Impl.from(encoderProfiles);
+ } else {
+ throw new RuntimeException(
+ "Unable to call from(EncoderProfiles) on API " + Build.VERSION.SDK_INT
+ + ". Version 31 or higher required.");
+ }
+ }
+
+ /** Creates an EncoderProfilesProxy instance from {@link CamcorderProfile}. */
+ @NonNull
+ public static EncoderProfilesProxy from(@NonNull CamcorderProfile camcorderProfile) {
+ if (Build.VERSION.SDK_INT >= 31) {
+ throw new RuntimeException(
+ "Should not use from(CamcorderProfile) on API " + Build.VERSION.SDK_INT
+ + ". CamcorderProfile is deprecated on API 31, use "
+ + "from(EncoderProfiles) instead.");
+ } else {
+ return EncoderProfilesProxyCompatBaseImpl.from(camcorderProfile);
+ }
+ }
+
+ // Class should not be instantiated.
+ private EncoderProfilesProxyCompat() {
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi31Impl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi31Impl.java
new file mode 100644
index 0000000..6e63f4b
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi31Impl.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023 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.compat;
+
+import android.media.EncoderProfiles;
+import android.media.EncoderProfiles.AudioProfile;
+import android.media.EncoderProfiles.VideoProfile;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.AudioProfileProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(31)
+class EncoderProfilesProxyCompatApi31Impl {
+
+ /** Creates an EncoderProfilesProxy instance from {@link EncoderProfiles}. */
+ @NonNull
+ public static EncoderProfilesProxy from(
+ @NonNull EncoderProfiles encoderProfiles) {
+ return EncoderProfilesProxy.create(
+ encoderProfiles.getDefaultDurationSeconds(),
+ encoderProfiles.getRecommendedFileFormat(),
+ fromAudioProfiles(encoderProfiles.getAudioProfiles()),
+ fromVideoProfiles(encoderProfiles.getVideoProfiles())
+ );
+ }
+
+ /** Creates VideoProfileProxy instances from a list of {@link VideoProfile}. */
+ @NonNull
+ private static List<VideoProfileProxy> fromVideoProfiles(
+ @NonNull List<VideoProfile> profiles) {
+ List<VideoProfileProxy> proxies = new ArrayList<>();
+ for (VideoProfile profile : profiles) {
+ proxies.add(VideoProfileProxy.create(
+ profile.getCodec(),
+ profile.getMediaType(),
+ profile.getBitrate(),
+ profile.getFrameRate(),
+ profile.getWidth(),
+ profile.getHeight(),
+ profile.getProfile(),
+ VideoProfileProxy.BIT_DEPTH_8,
+ VideoProfile.YUV_420,
+ VideoProfile.HDR_NONE
+ ));
+ }
+ return proxies;
+ }
+
+ /** Creates AudioProfileProxy instances from a list of {@link AudioProfile}. */
+ @NonNull
+ private static List<AudioProfileProxy> fromAudioProfiles(
+ @NonNull List<AudioProfile> profiles) {
+ List<AudioProfileProxy> proxies = new ArrayList<>();
+ for (AudioProfile profile : profiles) {
+ proxies.add(AudioProfileProxy.create(
+ profile.getCodec(),
+ profile.getMediaType(),
+ profile.getBitrate(),
+ profile.getSampleRate(),
+ profile.getChannels(),
+ profile.getProfile()
+ ));
+ }
+ return proxies;
+ }
+
+ // Class should not be instantiated.
+ private EncoderProfilesProxyCompatApi31Impl() {
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi33Impl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi33Impl.java
new file mode 100644
index 0000000..9dd5e2d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi33Impl.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023 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.compat;
+
+import android.media.EncoderProfiles;
+import android.media.EncoderProfiles.AudioProfile;
+import android.media.EncoderProfiles.VideoProfile;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.AudioProfileProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(33)
+class EncoderProfilesProxyCompatApi33Impl {
+
+ /** Creates an EncoderProfilesProxy instance from {@link EncoderProfiles}. */
+ @NonNull
+ public static EncoderProfilesProxy from(
+ @NonNull EncoderProfiles encoderProfiles) {
+ return EncoderProfilesProxy.create(
+ encoderProfiles.getDefaultDurationSeconds(),
+ encoderProfiles.getRecommendedFileFormat(),
+ fromAudioProfiles(encoderProfiles.getAudioProfiles()),
+ fromVideoProfiles(encoderProfiles.getVideoProfiles())
+ );
+ }
+
+ /** Creates VideoProfileProxy instances from a list of {@link VideoProfile}. */
+ @NonNull
+ private static List<VideoProfileProxy> fromVideoProfiles(
+ @NonNull List<VideoProfile> profiles) {
+ List<VideoProfileProxy> proxies = new ArrayList<>();
+ for (VideoProfile profile : profiles) {
+ proxies.add(VideoProfileProxy.create(
+ profile.getCodec(),
+ profile.getMediaType(),
+ profile.getBitrate(),
+ profile.getFrameRate(),
+ profile.getWidth(),
+ profile.getHeight(),
+ profile.getProfile(),
+ profile.getBitDepth(),
+ profile.getChromaSubsampling(),
+ profile.getHdrFormat()
+ ));
+ }
+ return proxies;
+ }
+
+ /** Creates AudioProfileProxy instances from a list of {@link AudioProfile}. */
+ @NonNull
+ private static List<AudioProfileProxy> fromAudioProfiles(
+ @NonNull List<AudioProfile> profiles) {
+ List<AudioProfileProxy> proxies = new ArrayList<>();
+ for (AudioProfile profile : profiles) {
+ proxies.add(AudioProfileProxy.create(
+ profile.getCodec(),
+ profile.getMediaType(),
+ profile.getBitrate(),
+ profile.getSampleRate(),
+ profile.getChannels(),
+ profile.getProfile()
+ ));
+ }
+ return proxies;
+ }
+
+ // Class should not be instantiated.
+ private EncoderProfilesProxyCompatApi33Impl() {
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatBaseImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatBaseImpl.java
new file mode 100644
index 0000000..cacc8d6
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatBaseImpl.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2023 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.compat;
+
+import static android.media.MediaRecorder.AudioEncoder.AAC;
+import static android.media.MediaRecorder.AudioEncoder.AAC_ELD;
+import static android.media.MediaRecorder.AudioEncoder.AMR_NB;
+import static android.media.MediaRecorder.AudioEncoder.AMR_WB;
+import static android.media.MediaRecorder.AudioEncoder.HE_AAC;
+import static android.media.MediaRecorder.AudioEncoder.OPUS;
+import static android.media.MediaRecorder.AudioEncoder.VORBIS;
+import static android.media.MediaRecorder.VideoEncoder.AV1;
+import static android.media.MediaRecorder.VideoEncoder.DOLBY_VISION;
+import static android.media.MediaRecorder.VideoEncoder.H263;
+import static android.media.MediaRecorder.VideoEncoder.H264;
+import static android.media.MediaRecorder.VideoEncoder.HEVC;
+import static android.media.MediaRecorder.VideoEncoder.MPEG_4_SP;
+import static android.media.MediaRecorder.VideoEncoder.VP8;
+import static android.media.MediaRecorder.VideoEncoder.VP9;
+
+import android.media.CamcorderProfile;
+import android.media.EncoderProfiles;
+import android.media.MediaCodecInfo;
+import android.media.MediaFormat;
+import android.media.MediaRecorder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.AudioProfileProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class EncoderProfilesProxyCompatBaseImpl {
+
+ /** Creates an EncoderProfilesProxy instance from {@link CamcorderProfile}. */
+ @NonNull
+ public static EncoderProfilesProxy from(
+ @NonNull CamcorderProfile camcorderProfile) {
+ return EncoderProfilesProxy.create(
+ camcorderProfile.duration,
+ camcorderProfile.fileFormat,
+ toAudioProfiles(camcorderProfile),
+ toVideoProfiles(camcorderProfile)
+ );
+ }
+
+ /** Creates VideoProfileProxy instances from {@link CamcorderProfile}. */
+ @NonNull
+ private static List<VideoProfileProxy> toVideoProfiles(
+ @NonNull CamcorderProfile camcorderProfile) {
+ List<VideoProfileProxy> proxies = new ArrayList<>();
+ proxies.add(VideoProfileProxy.create(
+ camcorderProfile.videoCodec,
+ getVideoCodecMimeType(camcorderProfile.videoCodec),
+ camcorderProfile.videoBitRate,
+ camcorderProfile.videoFrameRate,
+ camcorderProfile.videoFrameWidth,
+ camcorderProfile.videoFrameHeight,
+ EncoderProfilesProxy.CODEC_PROFILE_NONE,
+ VideoProfileProxy.BIT_DEPTH_8,
+ EncoderProfiles.VideoProfile.YUV_420,
+ EncoderProfiles.VideoProfile.HDR_NONE
+ ));
+ return proxies;
+ }
+
+ /** Creates AudioProfileProxy instances from {@link CamcorderProfile}. */
+ @NonNull
+ private static List<AudioProfileProxy> toAudioProfiles(
+ @NonNull CamcorderProfile camcorderProfile) {
+ List<AudioProfileProxy> proxies = new ArrayList<>();
+ proxies.add(AudioProfileProxy.create(
+ camcorderProfile.audioCodec,
+ getAudioCodecMimeType(camcorderProfile.audioCodec),
+ camcorderProfile.audioBitRate,
+ camcorderProfile.audioSampleRate,
+ camcorderProfile.audioChannels,
+ getRequiredAudioProfile(camcorderProfile.audioCodec)
+ ));
+ return proxies;
+ }
+
+ /**
+ * Returns a mime-type string for the given video codec type.
+ *
+ * @return A mime-type string or {@link VideoProfileProxy#MEDIA_TYPE_NONE} if the codec type is
+ * {@link MediaRecorder.VideoEncoder#DEFAULT}, as this type is under-defined and cannot be
+ * resolved to a specific mime type without more information.
+ */
+ @NonNull
+ private static String getVideoCodecMimeType(
+ @VideoProfileProxy.VideoEncoder int codec) {
+ switch (codec) {
+ // Mime-type definitions taken from
+ // frameworks/av/media/libstagefright/foundation/MediaDefs.cpp
+ case H263:
+ return MediaFormat.MIMETYPE_VIDEO_H263;
+ case H264:
+ return MediaFormat.MIMETYPE_VIDEO_AVC;
+ case HEVC:
+ return MediaFormat.MIMETYPE_VIDEO_HEVC;
+ case VP8:
+ return MediaFormat.MIMETYPE_VIDEO_VP8;
+ case MPEG_4_SP:
+ return MediaFormat.MIMETYPE_VIDEO_MPEG4;
+ case VP9:
+ return MediaFormat.MIMETYPE_VIDEO_VP9;
+ case DOLBY_VISION:
+ return MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION;
+ case AV1:
+ return MediaFormat.MIMETYPE_VIDEO_AV1;
+ case MediaRecorder.VideoEncoder.DEFAULT:
+ break;
+ }
+
+ return VideoProfileProxy.MEDIA_TYPE_NONE;
+ }
+
+ /**
+ * Returns a mime-type string for the given audio codec type.
+ *
+ * @return A mime-type string or {@link AudioProfileProxy#MEDIA_TYPE_NONE} if the codec type is
+ * {@link android.media.MediaRecorder.AudioEncoder#DEFAULT}, as this type is under-defined
+ * and cannot be resolved to a specific mime type without more information.
+ */
+ @NonNull
+ private static String getAudioCodecMimeType(@AudioProfileProxy.AudioEncoder int codec) {
+ // Mime-type definitions taken from
+ // frameworks/av/media/libstagefright/foundation/MediaDefs.cpp
+ switch (codec) {
+ case AAC: // Should use aac-profile LC
+ case HE_AAC: // Should use aac-profile HE
+ case AAC_ELD: // Should use aac-profile ELD
+ return MediaFormat.MIMETYPE_AUDIO_AAC;
+ case AMR_NB:
+ return MediaFormat.MIMETYPE_AUDIO_AMR_NB;
+ case AMR_WB:
+ return MediaFormat.MIMETYPE_AUDIO_AMR_WB;
+ case OPUS:
+ return MediaFormat.MIMETYPE_AUDIO_OPUS;
+ case VORBIS:
+ return MediaFormat.MIMETYPE_AUDIO_VORBIS;
+ case MediaRecorder.AudioEncoder.DEFAULT:
+ break;
+ }
+
+ return AudioProfileProxy.MEDIA_TYPE_NONE;
+ }
+
+ /**
+ * Returns the required audio profile for the given audio encoder.
+ *
+ * <p>For example, this can be used to differentiate between AAC encoders
+ * {@link android.media.MediaRecorder.AudioEncoder#AAC},
+ * {@link android.media.MediaRecorder.AudioEncoder#AAC_ELD},
+ * and {@link android.media.MediaRecorder.AudioEncoder#HE_AAC}.
+ * Should be used with the {@link MediaCodecInfo.CodecProfileLevel#profile} field.
+ *
+ * @return The profile required by the audio codec. If no profile is required, returns
+ * {@link EncoderProfilesProxy#CODEC_PROFILE_NONE}.
+ */
+ private static int getRequiredAudioProfile(@AudioProfileProxy.AudioEncoder int codec) {
+ switch (codec) {
+ case AAC:
+ return MediaCodecInfo.CodecProfileLevel.AACObjectLC;
+ case AAC_ELD:
+ return MediaCodecInfo.CodecProfileLevel.AACObjectELD;
+ case HE_AAC:
+ return MediaCodecInfo.CodecProfileLevel.AACObjectHE;
+ default:
+ return EncoderProfilesProxy.CODEC_PROFILE_NONE;
+ }
+ }
+
+ // Class should not be instantiated.
+ private EncoderProfilesProxyCompatBaseImpl() {
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/ProfileResolutionQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/ProfileResolutionQuirk.java
new file mode 100644
index 0000000..cdb5a87
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/ProfileResolutionQuirk.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 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.quirk;
+
+import android.media.EncoderProfiles;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.List;
+
+/**
+ * A Quirk interface which denotes that CameraX should validate video resolutions returned from
+ * {@link EncoderProfiles} instead of using them directly.
+ *
+ * <p>Subclasses of this quirk should provide a list of supported resolutions for CameraX to
+ * verify.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface ProfileResolutionQuirk extends Quirk {
+
+ /** Returns a list of supported resolutions. */
+ @NonNull
+ List<Size> getSupportedResolutions();
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/EncoderProfilesResolutionValidatorTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/EncoderProfilesResolutionValidatorTest.kt
new file mode 100644
index 0000000..57b9338
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/EncoderProfilesResolutionValidatorTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2023 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 android.os.Build
+import android.util.Size
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk
+import androidx.camera.testing.EncoderProfilesUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class EncoderProfilesResolutionValidatorTest {
+
+ @Test
+ fun noQuirk_alwaysValid() {
+ val validator = EncoderProfilesResolutionValidator(null)
+
+ assertThat(validator.hasValidVideoResolution(EncoderProfilesUtil.PROFILES_2160P)).isTrue()
+ assertThat(validator.hasValidVideoResolution(EncoderProfilesUtil.PROFILES_720P)).isTrue()
+ }
+
+ @Test
+ fun hasQuirk_shouldCheckSupportedResolutions() {
+ val quirk = createFakeProfileResolutionQuirk(
+ supportedResolution = arrayOf(EncoderProfilesUtil.RESOLUTION_2160P)
+ )
+ val validator = EncoderProfilesResolutionValidator(listOf(quirk))
+
+ assertThat(validator.hasValidVideoResolution(EncoderProfilesUtil.PROFILES_2160P)).isTrue()
+ assertThat(validator.hasValidVideoResolution(EncoderProfilesUtil.PROFILES_720P)).isFalse()
+ }
+
+ @Test
+ fun nullProfile_notValid() {
+ val quirk = createFakeProfileResolutionQuirk(
+ supportedResolution = arrayOf(EncoderProfilesUtil.RESOLUTION_2160P)
+ )
+ val validator = EncoderProfilesResolutionValidator(listOf(quirk))
+
+ assertThat(validator.hasValidVideoResolution(null)).isFalse()
+ }
+
+ private fun createFakeProfileResolutionQuirk(
+ supportedResolution: Array<Size> = emptyArray()
+ ): ProfileResolutionQuirk {
+ return FakeQuirk(supportedResolution)
+ }
+
+ class FakeQuirk(private val supportedResolutions: Array<Size>) : ProfileResolutionQuirk {
+
+ override fun getSupportedResolutions(): MutableList<Size> {
+ return supportedResolutions.toMutableList()
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProviderTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProviderTest.kt
new file mode 100644
index 0000000..198bc4c
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProviderTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2023 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 android.media.CamcorderProfile.QUALITY_1080P
+import android.media.CamcorderProfile.QUALITY_2160P
+import android.media.CamcorderProfile.QUALITY_480P
+import android.media.CamcorderProfile.QUALITY_720P
+import android.os.Build
+import android.util.Size
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_1080P
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_2160P
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_480P
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_720P
+import androidx.camera.testing.EncoderProfilesUtil.RESOLUTION_1080P
+import androidx.camera.testing.fakes.FakeEncoderProfilesProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+private val PAIR_2160 = Pair(QUALITY_2160P, PROFILES_2160P)
+private val PAIR_1080 = Pair(QUALITY_1080P, PROFILES_1080P)
+private val PAIR_720 = Pair(QUALITY_720P, PROFILES_720P)
+private val PAIR_480 = Pair(QUALITY_480P, PROFILES_480P)
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class ResolutionValidatedEncoderProfilesProviderTest {
+
+ private val defaultProvider = createFakeEncoderProfilesProvider(
+ arrayOf(PAIR_2160, PAIR_1080, PAIR_720, PAIR_480)
+ )
+
+ @Test
+ fun hasNoProfile_canNotGetProfiles() {
+ val quirks = createQuirksWithProfileResolutionQuirk(
+ supportedResolution = arrayOf(RESOLUTION_1080P)
+ )
+ val emptyProvider = createFakeEncoderProfilesProvider()
+ val provider = ResolutionValidatedEncoderProfilesProvider(emptyProvider, quirks)
+
+ assertThat(provider.hasProfile(QUALITY_2160P)).isFalse()
+ assertThat(provider.hasProfile(QUALITY_1080P)).isFalse()
+ assertThat(provider.hasProfile(QUALITY_720P)).isFalse()
+ assertThat(provider.hasProfile(QUALITY_480P)).isFalse()
+ assertThat(provider.getAll(QUALITY_2160P)).isNull()
+ assertThat(provider.getAll(QUALITY_1080P)).isNull()
+ assertThat(provider.getAll(QUALITY_720P)).isNull()
+ assertThat(provider.getAll(QUALITY_480P)).isNull()
+ }
+
+ @Test
+ fun hasQuirk_canOnlyGetSupportedProfiles() {
+ val quirks = createQuirksWithProfileResolutionQuirk(
+ supportedResolution = arrayOf(RESOLUTION_1080P)
+ )
+ val provider = ResolutionValidatedEncoderProfilesProvider(defaultProvider, quirks)
+
+ assertThat(provider.hasProfile(QUALITY_2160P)).isFalse()
+ assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+ assertThat(provider.hasProfile(QUALITY_720P)).isFalse()
+ assertThat(provider.hasProfile(QUALITY_480P)).isFalse()
+ assertThat(provider.getAll(QUALITY_2160P)).isNull()
+ assertThat(provider.getAll(QUALITY_1080P)).isNotNull()
+ assertThat(provider.getAll(QUALITY_720P)).isNull()
+ assertThat(provider.getAll(QUALITY_480P)).isNull()
+ }
+
+ @Test
+ fun hasNoQuirk_canGetProfiles() {
+ val quirks = Quirks(emptyList())
+ val provider = ResolutionValidatedEncoderProfilesProvider(defaultProvider, quirks)
+
+ assertThat(provider.hasProfile(QUALITY_2160P)).isTrue()
+ assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+ assertThat(provider.hasProfile(QUALITY_720P)).isTrue()
+ assertThat(provider.hasProfile(QUALITY_480P)).isTrue()
+ assertThat(provider.getAll(QUALITY_2160P)).isNotNull()
+ assertThat(provider.getAll(QUALITY_1080P)).isNotNull()
+ assertThat(provider.getAll(QUALITY_720P)).isNotNull()
+ assertThat(provider.getAll(QUALITY_480P)).isNotNull()
+ }
+
+ private fun createFakeEncoderProfilesProvider(
+ qualityToProfilesPairs: Array<Pair<Int, EncoderProfilesProxy>> = emptyArray()
+ ): EncoderProfilesProvider {
+ return FakeEncoderProfilesProvider.Builder().also { builder ->
+ for (pair in qualityToProfilesPairs) {
+ builder.add(pair.first, pair.second)
+ }
+ }.build()
+ }
+
+ private fun createQuirksWithProfileResolutionQuirk(
+ supportedResolution: Array<Size> = emptyArray()
+ ): Quirks {
+ return Quirks(listOf(FakeQuirk(supportedResolution)))
+ }
+
+ class FakeQuirk(private val supportedResolutions: Array<Size>) : ProfileResolutionQuirk {
+
+ override fun getSupportedResolutions(): MutableList<Size> {
+ return supportedResolutions.toMutableList()
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java
new file mode 100644
index 0000000..f3d56b2
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2023 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.testing;
+
+import android.media.EncoderProfiles;
+import android.media.MediaFormat;
+import android.media.MediaRecorder;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.AudioProfileProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.Collections;
+
+/**
+ * Utility methods for testing {@link EncoderProfiles} related classes, including predefined
+ * resolutions, attributes and {@link EncoderProfilesProxy}, which can be used directly on the
+ * unit tests.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public final class EncoderProfilesUtil {
+
+ /** Resolution for QCIF. */
+ public static final Size RESOLUTION_QCIF = new Size(176, 144);
+ /** Resolution for QVGA. */
+ public static final Size RESOLUTION_QVGA = new Size(320, 240);
+ /** Resolution for CIF. */
+ public static final Size RESOLUTION_CIF = new Size(352, 288);
+ /** Resolution for VGA. */
+ public static final Size RESOLUTION_VGA = new Size(640, 480);
+ /** Resolution for 480P. */
+ public static final Size RESOLUTION_480P = new Size(720, 480); /* 640, 704 or 720 x 480 */
+ /** Resolution for 720P. */
+ public static final Size RESOLUTION_720P = new Size(1280, 720);
+ /** Resolution for 1080P. */
+ public static final Size RESOLUTION_1080P = new Size(1920, 1080); /* 1920 x 1080 or 1088 */
+ /** Resolution for 2K. */
+ public static final Size RESOLUTION_2K = new Size(2048, 1080);
+ /** Resolution for QHD. */
+ public static final Size RESOLUTION_QHD = new Size(2560, 1440);
+ /** Resolution for 2160P. */
+ public static final Size RESOLUTION_2160P = new Size(3840, 2160);
+ /** Resolution for 4KDCI. */
+ public static final Size RESOLUTION_4KDCI = new Size(4096, 2160);
+
+ /** Default duration. */
+ public static final int DEFAULT_DURATION = 30;
+ /** Default output format. */
+ public static final int DEFAULT_OUTPUT_FORMAT = MediaRecorder.OutputFormat.MPEG_4;
+ /** Default video codec. */
+ public static final int DEFAULT_VIDEO_CODEC = MediaRecorder.VideoEncoder.H264;
+ /** Default media type. */
+ public static final String DEFAULT_VIDEO_MEDIA_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
+ /** Default video bitrate. */
+ public static final int DEFAULT_VIDEO_BITRATE = 8 * 1024 * 1024;
+ /** Default video frame rate. */
+ public static final int DEFAULT_VIDEO_FRAME_RATE = 30;
+ /** Default video code profile. */
+ public static final int DEFAULT_VIDEO_PROFILE = EncoderProfilesProxy.CODEC_PROFILE_NONE;
+ /** Default bit depth. */
+ public static final int DEFAULT_VIDEO_BIT_DEPTH = VideoProfileProxy.BIT_DEPTH_8;
+ /** Default chroma subsampling. */
+ public static final int DEFAULT_VIDEO_CHROMA_SUBSAMPLING = EncoderProfiles.VideoProfile.YUV_420;
+ /** Default hdr format. */
+ public static final int DEFAULT_VIDEO_HDR_FORMAT = EncoderProfiles.VideoProfile.HDR_NONE;
+ /** Default audio codec. */
+ public static final int DEFAULT_AUDIO_CODEC = MediaRecorder.AudioEncoder.AAC;
+ /** Default media type. */
+ public static final String DEFAULT_AUDIO_MEDIA_TYPE = MediaFormat.MIMETYPE_AUDIO_AAC;
+ /** Default audio bitrate. */
+ public static final int DEFAULT_AUDIO_BITRATE = 192_000;
+ /** Default audio sample rate. */
+ public static final int DEFAULT_AUDIO_SAMPLE_RATE = 48_000;
+ /** Default channel count. */
+ public static final int DEFAULT_AUDIO_CHANNELS = 1;
+ /** Default audio code profile. */
+ public static final int DEFAULT_AUDIO_PROFILE = EncoderProfilesProxy.CODEC_PROFILE_NONE;
+
+ public static final EncoderProfilesProxy PROFILES_QCIF = createFakeEncoderProfilesProxy(
+ RESOLUTION_QCIF.getWidth(),
+ RESOLUTION_QCIF.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_QVGA = createFakeEncoderProfilesProxy(
+ RESOLUTION_QVGA.getWidth(),
+ RESOLUTION_QVGA.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_CIF = createFakeEncoderProfilesProxy(
+ RESOLUTION_CIF.getWidth(),
+ RESOLUTION_CIF.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_VGA = createFakeEncoderProfilesProxy(
+ RESOLUTION_VGA.getWidth(),
+ RESOLUTION_VGA.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_480P = createFakeEncoderProfilesProxy(
+ RESOLUTION_480P.getWidth(),
+ RESOLUTION_480P.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_720P = createFakeEncoderProfilesProxy(
+ RESOLUTION_720P.getWidth(),
+ RESOLUTION_720P.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_1080P = createFakeEncoderProfilesProxy(
+ RESOLUTION_1080P.getWidth(),
+ RESOLUTION_1080P.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_2K = createFakeEncoderProfilesProxy(
+ RESOLUTION_2K.getWidth(),
+ RESOLUTION_2K.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_QHD = createFakeEncoderProfilesProxy(
+ RESOLUTION_QHD.getWidth(),
+ RESOLUTION_QHD.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_2160P = createFakeEncoderProfilesProxy(
+ RESOLUTION_2160P.getWidth(),
+ RESOLUTION_2160P.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_4KDCI = createFakeEncoderProfilesProxy(
+ RESOLUTION_4KDCI.getWidth(),
+ RESOLUTION_4KDCI.getHeight()
+ );
+
+ /** A utility method to create an EncoderProfilesProxy with some default values. */
+ @NonNull
+ public static EncoderProfilesProxy createFakeEncoderProfilesProxy(
+ int videoFrameWidth,
+ int videoFrameHeight
+ ) {
+ VideoProfileProxy videoProfile = VideoProfileProxy.create(
+ DEFAULT_VIDEO_CODEC,
+ DEFAULT_VIDEO_MEDIA_TYPE,
+ DEFAULT_VIDEO_BITRATE,
+ DEFAULT_VIDEO_FRAME_RATE,
+ videoFrameWidth,
+ videoFrameHeight,
+ DEFAULT_VIDEO_PROFILE,
+ DEFAULT_VIDEO_BIT_DEPTH,
+ DEFAULT_VIDEO_CHROMA_SUBSAMPLING,
+ DEFAULT_VIDEO_HDR_FORMAT
+ );
+ AudioProfileProxy audioProfile = AudioProfileProxy.create(
+ DEFAULT_AUDIO_CODEC,
+ DEFAULT_AUDIO_MEDIA_TYPE,
+ DEFAULT_AUDIO_BITRATE,
+ DEFAULT_AUDIO_SAMPLE_RATE,
+ DEFAULT_AUDIO_CHANNELS,
+ DEFAULT_AUDIO_PROFILE
+ );
+
+ return EncoderProfilesProxy.create(
+ DEFAULT_DURATION,
+ DEFAULT_OUTPUT_FORMAT,
+ Collections.singletonList(audioProfile),
+ Collections.singletonList(videoProfile)
+ );
+ }
+
+ // This class is not instantiable.
+ private EncoderProfilesUtil() {
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeEncoderProfilesProvider.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeEncoderProfilesProvider.java
new file mode 100644
index 0000000..a4d1e46
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeEncoderProfilesProvider.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 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.testing.fakes;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProvider;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A fake implementation of the {@link EncoderProfilesProvider} and used for test.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class FakeEncoderProfilesProvider implements EncoderProfilesProvider {
+
+ private final Map<Integer, EncoderProfilesProxy> mQualityToProfileMap;
+
+ FakeEncoderProfilesProvider(@NonNull Map<Integer, EncoderProfilesProxy> qualityToProfileMap) {
+ mQualityToProfileMap = qualityToProfileMap;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasProfile(int quality) {
+ return mQualityToProfileMap.get(quality) != null;
+ }
+
+ /** {@inheritDoc} */
+ @Nullable
+ @Override
+ public EncoderProfilesProxy getAll(int quality) {
+ return mQualityToProfileMap.get(quality);
+ }
+
+ /**
+ * The builder to create a FakeEncoderProfilesProvider instance.
+ */
+ @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+ public static class Builder {
+
+ private final Map<Integer, EncoderProfilesProxy> mQualityToProfileMap = new HashMap<>();
+
+ /**
+ * Adds a quality and its corresponding profiles.
+ */
+ @NonNull
+ public Builder add(int quality, @NonNull EncoderProfilesProxy profiles) {
+ mQualityToProfileMap.put(quality, profiles);
+ return this;
+ }
+
+ /**
+ * Adds qualities and their corresponding profiles.
+ */
+ @NonNull
+ public Builder addAll(@NonNull Map<Integer, EncoderProfilesProxy> qualityToProfileMap) {
+ mQualityToProfileMap.putAll(qualityToProfileMap);
+ return this;
+ }
+
+ /** Builds the FakeEncoderProfilesProvider instance. */
+ @NonNull
+ public FakeEncoderProfilesProvider build() {
+ return new FakeEncoderProfilesProvider(mQualityToProfileMap);
+ }
+ }
+}