Merge "Add API to query the supported DynamicRanges from Recorder" into androidx-main
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderVideoCapabilitiesTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderVideoCapabilitiesTest.kt
new file mode 100644
index 0000000..969868a
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderVideoCapabilitiesTest.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.video
+
+import android.content.Context
+import android.os.Build
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.DynamicRange.SDR
+import androidx.camera.testing.AndroidUtil
+import androidx.camera.testing.CameraPipeConfigTestRule
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CameraXUtil
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+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.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 21)
+class RecorderVideoCapabilitiesTest(
+ private val implName: String,
+ private val cameraConfig: CameraXConfig,
+) {
+
+ @get:Rule
+ val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+ active = implName == CameraPipeConfig::class.simpleName,
+ )
+
+ @get:Rule
+ val cameraRule = CameraUtil.grantCameraPermissionAndPreTest(
+ CameraUtil.PreTestCameraIdList(cameraConfig)
+ )
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() = listOf(
+ arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+ arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+ )
+ }
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+
+ private lateinit var videoCapabilities: RecorderVideoCapabilities
+
+ @Before
+ fun setUp() {
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+
+ CameraXUtil.initialize(context, cameraConfig).get()
+
+ val cameraInfo = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector).cameraInfo
+ videoCapabilities = RecorderVideoCapabilities.from(cameraInfo)
+ }
+
+ @After
+ fun tearDown() {
+ CameraXUtil.shutdown().get(10, TimeUnit.SECONDS)
+ }
+
+ @Test
+ fun supportStandardDynamicRange() {
+ assumeFalse(isSpecificSkippedDevice())
+ assumeFalse(AndroidUtil.isEmulatorAndAPI21())
+ assertThat(videoCapabilities.supportedDynamicRanges).contains(SDR)
+ }
+
+ @Test
+ fun supportedQualitiesOfSdrIsNotEmpty() {
+ assumeFalse(isSpecificSkippedDevice())
+ assumeFalse(AndroidUtil.isEmulatorAndAPI21())
+ assertThat(videoCapabilities.getSupportedQualities(SDR)).isNotEmpty()
+ }
+
+ private fun isSpecificSkippedDevice(): Boolean {
+ // skip for b/231903433
+ val isNokia2Point1 = "nokia".equals(Build.BRAND, true) &&
+ "nokia 2.1".equals(Build.MODEL, true)
+ val isMotoE5Play = "motorola".equals(Build.BRAND, true) &&
+ "moto e5 play".equals(Build.MODEL, true)
+
+ return isNokia2Point1 || isMotoE5Play
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/QualitySelector.java b/camera/camera-video/src/main/java/androidx/camera/video/QualitySelector.java
index 0dd1fe0..526a66a 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/QualitySelector.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/QualitySelector.java
@@ -313,6 +313,55 @@
return new ArrayList<>(sortedQualities);
}
+ /**
+ * Generates a sorted quality list that matches the desired quality settings.
+ *
+ * <p>The method bases on the desired qualities and the fallback strategy to find a matched
+ * quality list on this device. The search algorithm first checks which desired quality is
+ * supported according to the set sequence and adds to the returned list by order. Then the
+ * fallback strategy will be applied to add more valid qualities.
+ *
+ * @param supportedQualities the supported qualities.
+ * @return a sorted supported quality list according to the desired quality settings.
+ */
+ @NonNull
+ List<Quality> getPrioritizedQualities(@NonNull List<Quality> supportedQualities) {
+ if (supportedQualities.isEmpty()) {
+ Logger.w(TAG, "No supported quality on the device.");
+ return new ArrayList<>();
+ }
+ Logger.d(TAG, "supportedQualities = " + supportedQualities);
+
+ // Use LinkedHashSet to prevent from duplicate quality and keep the adding order.
+ Set<Quality> sortedQualities = new LinkedHashSet<>();
+ // Add exact quality.
+ for (Quality quality : mPreferredQualityList) {
+ if (quality == Quality.HIGHEST) {
+ // Highest means user want a quality as higher as possible, so the return list can
+ // contain all supported resolutions from large to small.
+ sortedQualities.addAll(supportedQualities);
+ break;
+ } else if (quality == Quality.LOWEST) {
+ // Opposite to the highest
+ List<Quality> reversedList = new ArrayList<>(supportedQualities);
+ Collections.reverse(reversedList);
+ sortedQualities.addAll(reversedList);
+ break;
+ } else {
+ if (supportedQualities.contains(quality)) {
+ sortedQualities.add(quality);
+ } else {
+ Logger.w(TAG, "quality is not supported and will be ignored: " + quality);
+ }
+ }
+ }
+
+ // Add quality by fallback strategy based on fallback quality.
+ addByFallbackStrategy(supportedQualities, sortedQualities);
+
+ return new ArrayList<>(sortedQualities);
+ }
+
@NonNull
@Override
public String toString() {
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
index 56b0c79..30fd36c 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
@@ -57,6 +57,7 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.AspectRatio;
+import androidx.camera.core.CameraInfo;
import androidx.camera.core.Logger;
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.impl.MutableStateObservable;
@@ -2614,6 +2615,15 @@
return defaultMuxerFormat;
}
+ /**
+ * Gets the {@link VideoCapabilities} of Recorder.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @NonNull
+ public static VideoCapabilities getVideoCapabilities(@NonNull CameraInfo cameraInfo) {
+ return RecorderVideoCapabilities.from(cameraInfo);
+ }
+
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@AutoValue
abstract static class RecordingRecord implements AutoCloseable {
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
new file mode 100644
index 0000000..d78d614
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
@@ -0,0 +1,396 @@
+/*
+ * 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.video;
+
+import static androidx.camera.core.DynamicRange.FORMAT_HLG;
+import static androidx.camera.core.DynamicRange.HDR_UNSPECIFIED_10_BIT;
+import static androidx.camera.core.DynamicRange.SDR;
+import static androidx.camera.video.internal.BackupHdrProfileEncoderProfilesProvider.DEFAULT_VALIDATOR;
+import static androidx.camera.video.internal.utils.DynamicRangeUtil.VP_TO_DR_FORMAT_MAP;
+
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.arch.core.util.Function;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.DynamicRange;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.EncoderProfilesProvider;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+import androidx.camera.core.impl.Quirks;
+import androidx.camera.core.impl.ResolutionValidatedEncoderProfilesProvider;
+import androidx.camera.core.impl.utils.CompareSizesByArea;
+import androidx.camera.video.internal.BackupHdrProfileEncoderProfilesProvider;
+import androidx.camera.video.internal.DynamicRangeMatchedEncoderProfilesProvider;
+import androidx.camera.video.internal.VideoValidatedEncoderProfilesProxy;
+import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.video.internal.workaround.QualityValidatedEncoderProfilesProvider;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * RecorderVideoCapabilities is used to query video recording capabilities related to Recorder.
+ *
+ * <p>The {@link EncoderProfilesProxy} queried from RecorderVideoCapabilities will contain
+ * {@link VideoProfileProxy}s matches with the target {@link DynamicRange}. When HDR is
+ * supported, RecorderVideoCapabilities will try best to provide additional backup HDR
+ * {@link VideoProfileProxy}s in case the information is lacked in the device.
+ *
+ * @see Recorder#getVideoCapabilities(CameraInfo)
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public final class RecorderVideoCapabilities implements VideoCapabilities {
+
+ private static final String TAG = "RecorderVideoCapabilities";
+
+ private final Map<DynamicRange, CapabilitiesByQuality> mCapabilitiesMap = new HashMap<>();
+
+ /**
+ * Creates a RecorderVideoCapabilities.
+ *
+ * @param cameraInfoInternal the cameraInfo.
+ * @param backupVideoProfileValidator the validator used to check if the derived backup HDR
+ * {@link VideoProfileProxy} is valid.
+ * @throws IllegalArgumentException if unable to get the capability information from the
+ * CameraInfo.
+ */
+ @VisibleForTesting
+ RecorderVideoCapabilities(@NonNull CameraInfoInternal cameraInfoInternal,
+ @NonNull Function<VideoProfileProxy, VideoProfileProxy> backupVideoProfileValidator) {
+ EncoderProfilesProvider encoderProfilesProvider =
+ cameraInfoInternal.getEncoderProfilesProvider();
+
+ // Add backup HDR video information. In the initial version, only HLG10 profile is added.
+ if (isHlg10SupportedByCamera(cameraInfoInternal)) {
+ encoderProfilesProvider = new BackupHdrProfileEncoderProfilesProvider(
+ encoderProfilesProvider, backupVideoProfileValidator);
+ }
+
+ // Workaround resolution quirk.
+ Quirks cameraQuirks = cameraInfoInternal.getCameraQuirks();
+ encoderProfilesProvider = new ResolutionValidatedEncoderProfilesProvider(
+ encoderProfilesProvider, cameraQuirks);
+
+ // Workaround quality quirk.
+ Quirks deviceQuirks = DeviceQuirks.getAll();
+ encoderProfilesProvider = new QualityValidatedEncoderProfilesProvider(
+ encoderProfilesProvider, cameraInfoInternal, deviceQuirks);
+
+ // Group by dynamic range.
+ for (DynamicRange dynamicRange : getCandidateDynamicRanges(encoderProfilesProvider)) {
+ if (!isDynamicRangeSupported(dynamicRange, cameraInfoInternal)) {
+ continue;
+ }
+
+ // Filter video profiles to include only the profiles match with the target dynamic
+ // range.
+ EncoderProfilesProvider constrainedProvider =
+ new DynamicRangeMatchedEncoderProfilesProvider(encoderProfilesProvider,
+ dynamicRange);
+ CapabilitiesByQuality capabilitiesByQuality =
+ new CapabilitiesByQuality(constrainedProvider);
+
+ if (!capabilitiesByQuality.getSupportedQualities().isEmpty()) {
+ mCapabilitiesMap.put(dynamicRange, capabilitiesByQuality);
+ }
+ }
+ }
+
+ /**
+ * Gets RecorderVideoCapabilities by the {@link CameraInfo}.
+ *
+ * <p>Should not be called directly, use {@link Recorder#getVideoCapabilities(CameraInfo)}
+ * instead.
+ *
+ * <p>The {@link BackupHdrProfileEncoderProfilesProvider#DEFAULT_VALIDATOR} is used for
+ * validating the derived backup HDR {@link VideoProfileProxy}.
+ */
+ @NonNull
+ static RecorderVideoCapabilities from(@NonNull CameraInfo cameraInfo) {
+ return new RecorderVideoCapabilities((CameraInfoInternal) cameraInfo, DEFAULT_VALIDATOR);
+ }
+
+ @NonNull
+ @Override
+ public Set<DynamicRange> getSupportedDynamicRanges() {
+ Set<DynamicRange> dynamicRanges = mCapabilitiesMap.keySet();
+
+ // Remove HDR_UNSPECIFIED_10_BIT from output, since it does not have explicit content.
+ dynamicRanges.remove(HDR_UNSPECIFIED_10_BIT);
+
+ return dynamicRanges;
+ }
+
+ @NonNull
+ @Override
+ public List<Quality> getSupportedQualities(@NonNull DynamicRange dynamicRange) {
+ CapabilitiesByQuality capabilities = mCapabilitiesMap.get(dynamicRange);
+ return capabilities == null ? new ArrayList<>() : capabilities.getSupportedQualities();
+ }
+
+ @Override
+ public boolean isQualitySupported(@NonNull Quality quality,
+ @NonNull DynamicRange dynamicRange) {
+ CapabilitiesByQuality capabilities = mCapabilitiesMap.get(dynamicRange);
+ return capabilities != null && capabilities.isQualitySupported(quality);
+ }
+
+ @Nullable
+ @Override
+ public VideoValidatedEncoderProfilesProxy getProfiles(@NonNull Quality quality,
+ @NonNull DynamicRange dynamicRange) {
+ CapabilitiesByQuality capabilities = mCapabilitiesMap.get(dynamicRange);
+ return capabilities == null ? null : capabilities.getProfiles(quality);
+ }
+
+ @Nullable
+ @Override
+ public VideoValidatedEncoderProfilesProxy findHighestSupportedEncoderProfilesFor(
+ @NonNull Size size, @NonNull DynamicRange dynamicRange) {
+ CapabilitiesByQuality capabilities = mCapabilitiesMap.get(dynamicRange);
+ return capabilities == null ? null : capabilities.findHighestSupportedEncoderProfilesFor(
+ size);
+ }
+
+ @NonNull
+ @Override
+ public Quality findHighestSupportedQualityFor(@NonNull Size size,
+ @NonNull DynamicRange dynamicRange) {
+ CapabilitiesByQuality capabilities = mCapabilitiesMap.get(dynamicRange);
+ return capabilities == null ? Quality.NONE : capabilities.findHighestSupportedQualityFor(
+ size);
+ }
+
+ @NonNull
+ private static Set<DynamicRange> getCandidateDynamicRanges(
+ @NonNull EncoderProfilesProvider provider) {
+ Set<DynamicRange> dynamicRanges = new HashSet<>();
+ for (Quality quality : Quality.getSortedQualities()) {
+ int qualityValue = ((Quality.ConstantQuality) quality).getValue();
+ EncoderProfilesProxy encoderProfiles = provider.getAll(qualityValue);
+
+ if (encoderProfiles != null) {
+ for (VideoProfileProxy videoProfile : encoderProfiles.getVideoProfiles()) {
+ Integer format = VP_TO_DR_FORMAT_MAP.get(videoProfile.getHdrFormat());
+ if (format != null) {
+ dynamicRanges.add(new DynamicRange(format, videoProfile.getBitDepth()));
+ }
+ }
+ }
+ }
+
+ dynamicRanges.add(SDR);
+ dynamicRanges.add(HDR_UNSPECIFIED_10_BIT);
+
+ return dynamicRanges;
+ }
+
+ private static boolean isDynamicRangeSupported(@NonNull DynamicRange dynamicRange,
+ @NonNull CameraInfoInternal cameraInfoInternal) {
+ Set<DynamicRange> supportedDynamicRanges = cameraInfoInternal.getSupportedDynamicRanges();
+ if (supportedDynamicRanges.contains(dynamicRange)) {
+ return true;
+ } else {
+ return dynamicRange.equals(HDR_UNSPECIFIED_10_BIT) && contains10BitHdrDynamicRange(
+ supportedDynamicRanges);
+ }
+ }
+
+ private static boolean contains10BitHdrDynamicRange(@NonNull Set<DynamicRange> dynamicRanges) {
+ for (DynamicRange dynamicRange : dynamicRanges) {
+ if (dynamicRange.getFormat() != DynamicRange.FORMAT_SDR
+ && dynamicRange.getBitDepth() == DynamicRange.BIT_DEPTH_10_BIT) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean isHlg10SupportedByCamera(
+ @NonNull CameraInfoInternal cameraInfoInternal) {
+ Set<DynamicRange> dynamicRanges = cameraInfoInternal.getSupportedDynamicRanges();
+ for (DynamicRange dynamicRange : dynamicRanges) {
+ Integer format = dynamicRange.getFormat();
+ int bitDepth = dynamicRange.getBitDepth();
+ if (format.equals(FORMAT_HLG) && bitDepth == DynamicRange.BIT_DEPTH_10_BIT) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * This class implements the video capabilities query logic related to quality and resolution.
+ */
+ private static class CapabilitiesByQuality {
+
+ /**
+ * Maps quality to supported {@link VideoValidatedEncoderProfilesProxy}. The order is from
+ * size large to small.
+ */
+ private final Map<Quality, VideoValidatedEncoderProfilesProxy> mSupportedProfilesMap =
+ new LinkedHashMap<>();
+ private final TreeMap<Size, Quality> mAreaSortedSizeToQualityMap =
+ new TreeMap<>(new CompareSizesByArea());
+ private final VideoValidatedEncoderProfilesProxy mHighestProfiles;
+ private final VideoValidatedEncoderProfilesProxy mLowestProfiles;
+
+ CapabilitiesByQuality(@NonNull EncoderProfilesProvider provider) {
+ // Construct supported profile map.
+ for (Quality quality : Quality.getSortedQualities()) {
+ EncoderProfilesProxy profiles = getEncoderProfiles(quality, provider);
+ if (profiles == null) {
+ continue;
+ }
+
+ // Validate that EncoderProfiles contain video information.
+ Logger.d(TAG, "profiles = " + profiles);
+ VideoValidatedEncoderProfilesProxy validatedProfiles = toValidatedProfiles(
+ profiles);
+ if (validatedProfiles == null) {
+ Logger.w(TAG, "EncoderProfiles of quality " + quality + " has no video "
+ + "validated profiles.");
+ continue;
+ }
+
+ EncoderProfilesProxy.VideoProfileProxy videoProfile =
+ validatedProfiles.getDefaultVideoProfile();
+ Size size = new Size(videoProfile.getWidth(), videoProfile.getHeight());
+ mAreaSortedSizeToQualityMap.put(size, quality);
+
+ // SortedQualities is from size large to small.
+ mSupportedProfilesMap.put(quality, validatedProfiles);
+ }
+ if (mSupportedProfilesMap.isEmpty()) {
+ Logger.e(TAG, "No supported EncoderProfiles");
+ mLowestProfiles = null;
+ mHighestProfiles = null;
+ } else {
+ Deque<VideoValidatedEncoderProfilesProxy> profileQueue = new ArrayDeque<>(
+ mSupportedProfilesMap.values());
+ mHighestProfiles = profileQueue.peekFirst();
+ mLowestProfiles = profileQueue.peekLast();
+ }
+ }
+
+ @NonNull
+ public List<Quality> getSupportedQualities() {
+ return new ArrayList<>(mSupportedProfilesMap.keySet());
+ }
+
+ public boolean isQualitySupported(@NonNull Quality quality) {
+ checkQualityConstantsOrThrow(quality);
+ return getProfiles(quality) != null;
+ }
+
+ @Nullable
+ public VideoValidatedEncoderProfilesProxy getProfiles(@NonNull Quality quality) {
+ checkQualityConstantsOrThrow(quality);
+ if (quality == Quality.HIGHEST) {
+ return mHighestProfiles;
+ } else if (quality == Quality.LOWEST) {
+ return mLowestProfiles;
+ }
+ return mSupportedProfilesMap.get(quality);
+ }
+
+ @Nullable
+ public VideoValidatedEncoderProfilesProxy findHighestSupportedEncoderProfilesFor(
+ @NonNull Size size) {
+ VideoValidatedEncoderProfilesProxy encoderProfiles = null;
+ Quality highestSupportedQuality = findHighestSupportedQualityFor(size);
+ Logger.d(TAG,
+ "Using supported quality of " + highestSupportedQuality + " for size " + size);
+ if (highestSupportedQuality != Quality.NONE) {
+ encoderProfiles = getProfiles(highestSupportedQuality);
+ if (encoderProfiles == null) {
+ throw new AssertionError("Camera advertised available quality but did not "
+ + "produce EncoderProfiles for advertised quality.");
+ }
+ }
+ return encoderProfiles;
+ }
+
+ @NonNull
+ public Quality findHighestSupportedQualityFor(@NonNull Size size) {
+ Map.Entry<Size, Quality> ceilEntry = mAreaSortedSizeToQualityMap.ceilingEntry(size);
+
+ if (ceilEntry != null) {
+ // The ceiling entry will either be equivalent or higher in size, so always
+ // return it.
+ return ceilEntry.getValue();
+ } else {
+ // If a ceiling entry doesn't exist and a floor entry exists, it is the closest
+ // we have, so return it.
+ Map.Entry<Size, Quality> floorEntry = mAreaSortedSizeToQualityMap.floorEntry(size);
+ if (floorEntry != null) {
+ return floorEntry.getValue();
+ }
+ }
+
+ // No supported qualities.
+ return Quality.NONE;
+ }
+
+ @Nullable
+ private EncoderProfilesProxy getEncoderProfiles(@NonNull Quality quality,
+ @NonNull EncoderProfilesProvider provider) {
+ Preconditions.checkState(quality instanceof Quality.ConstantQuality,
+ "Currently only support ConstantQuality");
+ int qualityValue = ((Quality.ConstantQuality) quality).getValue();
+
+ return provider.getAll(qualityValue);
+ }
+
+ @Nullable
+ private VideoValidatedEncoderProfilesProxy toValidatedProfiles(
+ @NonNull EncoderProfilesProxy profiles) {
+ // According to the document, the first profile is the default video profile.
+ List<EncoderProfilesProxy.VideoProfileProxy> videoProfiles =
+ profiles.getVideoProfiles();
+ if (videoProfiles.isEmpty()) {
+ return null;
+ }
+
+ return VideoValidatedEncoderProfilesProxy.from(profiles);
+ }
+
+ private static void checkQualityConstantsOrThrow(@NonNull Quality quality) {
+ Preconditions.checkArgument(Quality.containsQuality(quality),
+ "Unknown quality: " + quality);
+ }
+ }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
new file mode 100644
index 0000000..5814ce7
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
@@ -0,0 +1,152 @@
+/*
+ * 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.video;
+
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.DynamicRange;
+import androidx.camera.video.internal.VideoValidatedEncoderProfilesProxy;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * VideoCapabilities is used to query video recording capabilities on the device.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface VideoCapabilities {
+
+ /**
+ * Gets all dynamic ranges supported by both the camera and video output.
+ *
+ * <p>Only {@link DynamicRange}s with specified values both in {@link DynamicRange.BitDepth}
+ * and {@link DynamicRange.DynamicRangeFormat} will be present in the returned set.
+ * {@link DynamicRange}s such as {@link DynamicRange#HDR_UNSPECIFIED_10_BIT} will not be
+ * included, but they can be used in other methods, such as checking for quality support with
+ * {@link #isQualitySupported(Quality, DynamicRange)}.
+ */
+ @NonNull
+ Set<DynamicRange> getSupportedDynamicRanges();
+
+ /**
+ * Gets all supported qualities for the input dynamic range.
+ *
+ * <p>The returned list is sorted by quality size from large to small.
+ *
+ * <p>Note: Constants {@link Quality#HIGHEST} and {@link Quality#LOWEST} are not included.
+ */
+ @NonNull
+ List<Quality> getSupportedQualities(@NonNull DynamicRange dynamicRange);
+
+ /**
+ * Checks if the quality is supported for the input dynamic range.
+ *
+ * @param quality one of the quality constants. Possible values include
+ * {@link Quality#LOWEST}, {@link Quality#HIGHEST}, {@link Quality#SD},
+ * {@link Quality#HD}, {@link Quality#FHD}, or {@link Quality#UHD}.
+ * @param dynamicRange the target dynamicRange.
+ * @return {@code true} if the quality is supported; {@code false} otherwise.
+ */
+ boolean isQualitySupported(@NonNull Quality quality, @NonNull DynamicRange dynamicRange);
+
+ /**
+ * Gets the corresponding {@link VideoValidatedEncoderProfilesProxy} of the input quality and
+ * dynamic range.
+ *
+ * @param quality one of the quality constants. Possible values include
+ * {@link Quality#LOWEST}, {@link Quality#HIGHEST}, {@link Quality#SD},
+ * {@link Quality#HD}, {@link Quality#FHD}, or {@link Quality#UHD}.
+ * @param dynamicRange target dynamicRange.
+ * @return the corresponding VideoValidatedEncoderProfilesProxy, or {@code null} if the
+ * quality is not supported on the device.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Nullable
+ default VideoValidatedEncoderProfilesProxy getProfiles(@NonNull Quality quality,
+ @NonNull DynamicRange dynamicRange) {
+ return null;
+ }
+
+ /**
+ * Finds the supported EncoderProfilesProxy with the resolution nearest to the given
+ * {@link Size}.
+ *
+ * <p>The supported EncoderProfilesProxy means the corresponding {@link Quality} is also
+ * supported. If the size aligns exactly with the pixel count of an EncoderProfilesProxy,
+ * that EncoderProfilesProxy will be selected. If the size falls between two
+ * EncoderProfilesProxy, the higher resolution will always be selected. Otherwise, the
+ * nearest EncoderProfilesProxy will be selected, whether that EncoderProfilesProxy's
+ * resolution is above or below the given size.
+ *
+ * @see #findHighestSupportedQualityFor(Size, DynamicRange)
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Nullable
+ default VideoValidatedEncoderProfilesProxy findHighestSupportedEncoderProfilesFor(
+ @NonNull Size size, @NonNull DynamicRange dynamicRange) {
+ return null;
+ }
+
+ /**
+ * Finds the nearest quality by number of pixels to the given {@link Size}.
+ *
+ * <p>If the size aligns exactly with the pixel count of a supported quality, that quality
+ * will be selected. If the size falls between two qualities, the higher quality will always
+ * be selected. Otherwise, the nearest single quality will be selected, whether that
+ * quality's size is above or below the given size.
+ *
+ * @param size the size representing the number of pixels for comparison. Pixels are assumed
+ * to be square.
+ * @param dynamicRange target dynamicRange.
+ * @return the quality constant defined in {@link Quality}. If no qualities are supported,
+ * then {@link Quality#NONE} is returned.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @NonNull
+ default Quality findHighestSupportedQualityFor(@NonNull Size size,
+ @NonNull DynamicRange dynamicRange) {
+ return Quality.NONE;
+ }
+
+ /** An empty implementation. */
+ VideoCapabilities EMPTY = new VideoCapabilities() {
+ @NonNull
+ @Override
+ public Set<DynamicRange> getSupportedDynamicRanges() {
+ return new HashSet<>();
+ }
+
+ @NonNull
+ @Override
+ public List<Quality> getSupportedQualities(@NonNull DynamicRange dynamicRange) {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public boolean isQualitySupported(@NonNull Quality quality,
+ @NonNull DynamicRange dynamicRange) {
+ return false;
+ }
+ };
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/DynamicRangeUtil.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/DynamicRangeUtil.java
index 2b081c7..8057fdf 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/DynamicRangeUtil.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/DynamicRangeUtil.java
@@ -52,6 +52,7 @@
public static final Map<Integer, Set<Integer>> DR_TO_VP_BIT_DEPTH_MAP = new HashMap<>();
public static final Map<Integer, Set<Integer>> DR_TO_VP_FORMAT_MAP = new HashMap<>();
+ public static final Map<Integer, Integer> VP_TO_DR_FORMAT_MAP = new HashMap<>();
private DynamicRangeUtil() {
}
@@ -72,5 +73,12 @@
DR_TO_VP_FORMAT_MAP.put(FORMAT_HDR10_PLUS, new HashSet<>(singletonList(HDR_HDR10PLUS)));
DR_TO_VP_FORMAT_MAP.put(FORMAT_DOLBY_VISION,
new HashSet<>(singletonList(HDR_DOLBY_VISION)));
+
+ // VideoProfile HDR format to DynamicRange format.
+ VP_TO_DR_FORMAT_MAP.put(HDR_NONE, FORMAT_SDR);
+ VP_TO_DR_FORMAT_MAP.put(HDR_HLG, FORMAT_HLG);
+ VP_TO_DR_FORMAT_MAP.put(HDR_HDR10, FORMAT_HDR10);
+ VP_TO_DR_FORMAT_MAP.put(HDR_HDR10PLUS, FORMAT_HDR10_PLUS);
+ VP_TO_DR_FORMAT_MAP.put(HDR_DOLBY_VISION, FORMAT_DOLBY_VISION);
}
}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/RecorderVideoCapabilitiesTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/RecorderVideoCapabilitiesTest.kt
new file mode 100644
index 0000000..fb598f9
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/RecorderVideoCapabilitiesTest.kt
@@ -0,0 +1,217 @@
+/*
+ * 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.video
+
+import android.media.CamcorderProfile
+import android.os.Build
+import android.util.Size
+import androidx.camera.core.DynamicRange
+import androidx.camera.core.DynamicRange.BIT_DEPTH_10_BIT
+import androidx.camera.core.DynamicRange.FORMAT_HLG
+import androidx.camera.core.DynamicRange.HDR_UNSPECIFIED_10_BIT
+import androidx.camera.core.DynamicRange.SDR
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_2160P
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_720P
+import androidx.camera.testing.EncoderProfilesUtil.RESOLUTION_2160P
+import androidx.camera.testing.EncoderProfilesUtil.RESOLUTION_720P
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
+import androidx.camera.testing.fakes.FakeEncoderProfilesProvider
+import androidx.camera.video.Quality.FHD
+import androidx.camera.video.Quality.HD
+import androidx.camera.video.Quality.HIGHEST
+import androidx.camera.video.Quality.LOWEST
+import androidx.camera.video.Quality.SD
+import androidx.camera.video.Quality.UHD
+import androidx.camera.video.internal.VideoValidatedEncoderProfilesProxy
+import androidx.core.util.component1
+import androidx.core.util.component2
+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 HLG10 = DynamicRange(FORMAT_HLG, BIT_DEPTH_10_BIT)
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class RecorderVideoCapabilitiesTest {
+
+ private val defaultProfilesProvider = FakeEncoderProfilesProvider.Builder()
+ .add(CamcorderProfile.QUALITY_HIGH, PROFILES_2160P) // UHD (2160p) per above definition
+ .add(CamcorderProfile.QUALITY_2160P, PROFILES_2160P) // UHD (2160p)
+ .add(CamcorderProfile.QUALITY_720P, PROFILES_720P) // HD (720p)
+ .add(CamcorderProfile.QUALITY_LOW, PROFILES_720P) // HD (720p) per above definition
+ .build()
+ private val defaultDynamicRanges = setOf(SDR, HLG10)
+ private val cameraInfo = FakeCameraInfoInternal().apply {
+ encoderProfilesProvider = defaultProfilesProvider
+ supportedDynamicRanges = defaultDynamicRanges
+ }
+
+ private val fakeValidator: (VideoProfileProxy) -> VideoProfileProxy = {
+ // Just returns the input video profile.
+ it
+ }
+ private val validatedProfiles2160p = VideoValidatedEncoderProfilesProxy.from(PROFILES_2160P)
+ private val validatedProfiles720p = VideoValidatedEncoderProfilesProxy.from(PROFILES_720P)
+ private val videoCapabilities = RecorderVideoCapabilities(cameraInfo, fakeValidator)
+
+ @Test
+ fun canGetSupportedDynamicRanges() {
+ assertThat(videoCapabilities.supportedDynamicRanges).containsExactly(SDR, HLG10)
+ }
+
+ @Test
+ fun isQualitySupported_sdr() {
+ assertThat(videoCapabilities.isQualitySupported(HIGHEST, SDR)).isTrue()
+ assertThat(videoCapabilities.isQualitySupported(LOWEST, SDR)).isTrue()
+ assertThat(videoCapabilities.isQualitySupported(UHD, SDR)).isTrue()
+ assertThat(videoCapabilities.isQualitySupported(FHD, SDR)).isFalse()
+ assertThat(videoCapabilities.isQualitySupported(HD, SDR)).isTrue()
+ assertThat(videoCapabilities.isQualitySupported(SD, SDR)).isFalse()
+ }
+
+ @Test
+ fun isQualitySupported_hlg10WithBackupProfile() {
+ assertThat(videoCapabilities.isQualitySupported(HIGHEST, HLG10)).isTrue()
+ assertThat(videoCapabilities.isQualitySupported(LOWEST, HLG10)).isTrue()
+ assertThat(videoCapabilities.isQualitySupported(UHD, HLG10)).isTrue()
+ assertThat(videoCapabilities.isQualitySupported(FHD, HLG10)).isFalse()
+ assertThat(videoCapabilities.isQualitySupported(HD, HLG10)).isTrue()
+ assertThat(videoCapabilities.isQualitySupported(SD, HLG10)).isFalse()
+ }
+
+ @Test
+ fun isQualitySupported_hdrUnspecifiedWithBackupProfile() {
+ assertThat(videoCapabilities.isQualitySupported(HIGHEST, HDR_UNSPECIFIED_10_BIT)).isTrue()
+ assertThat(videoCapabilities.isQualitySupported(LOWEST, HDR_UNSPECIFIED_10_BIT)).isTrue()
+ assertThat(videoCapabilities.isQualitySupported(UHD, HDR_UNSPECIFIED_10_BIT)).isTrue()
+ assertThat(videoCapabilities.isQualitySupported(FHD, HDR_UNSPECIFIED_10_BIT)).isFalse()
+ assertThat(videoCapabilities.isQualitySupported(HD, HDR_UNSPECIFIED_10_BIT)).isTrue()
+ assertThat(videoCapabilities.isQualitySupported(SD, HDR_UNSPECIFIED_10_BIT)).isFalse()
+ }
+
+ @Test
+ fun canGetSameSdrProfile() {
+ assertThat(videoCapabilities.getProfiles(HIGHEST, SDR)).isEqualTo(validatedProfiles2160p)
+ assertThat(videoCapabilities.getProfiles(LOWEST, SDR)).isEqualTo(validatedProfiles720p)
+ assertThat(videoCapabilities.getProfiles(UHD, SDR)).isEqualTo(validatedProfiles2160p)
+ assertThat(videoCapabilities.getProfiles(FHD, SDR)).isNull()
+ assertThat(videoCapabilities.getProfiles(HD, SDR)).isEqualTo(validatedProfiles720p)
+ assertThat(videoCapabilities.getProfiles(SD, SDR)).isNull()
+ }
+
+ @Test
+ fun canGetNonNullHdrUnspecifiedBackupProfile_whenSdrProfileExisted() {
+ assertThat(videoCapabilities.getProfiles(HIGHEST, HDR_UNSPECIFIED_10_BIT)).isNotNull()
+ assertThat(videoCapabilities.getProfiles(LOWEST, HDR_UNSPECIFIED_10_BIT)).isNotNull()
+ assertThat(videoCapabilities.getProfiles(UHD, HDR_UNSPECIFIED_10_BIT)).isNotNull()
+ assertThat(videoCapabilities.getProfiles(FHD, HDR_UNSPECIFIED_10_BIT)).isNull()
+ assertThat(videoCapabilities.getProfiles(HD, HDR_UNSPECIFIED_10_BIT)).isNotNull()
+ assertThat(videoCapabilities.getProfiles(SD, HDR_UNSPECIFIED_10_BIT)).isNull()
+ }
+
+ @Test
+ fun canGetNonNullHlg10BackupProfile_whenSdrProfileExisted() {
+ assertThat(videoCapabilities.getProfiles(HIGHEST, HLG10)).isNotNull()
+ assertThat(videoCapabilities.getProfiles(LOWEST, HLG10)).isNotNull()
+ assertThat(videoCapabilities.getProfiles(UHD, HLG10)).isNotNull()
+ assertThat(videoCapabilities.getProfiles(FHD, HLG10)).isNull()
+ assertThat(videoCapabilities.getProfiles(HD, HLG10)).isNotNull()
+ assertThat(videoCapabilities.getProfiles(SD, HLG10)).isNull()
+ }
+
+ @Test
+ fun findHighestSupportedQuality_returnsHigherQuality() {
+ // Create a size between 720p and 2160p
+ val (width720p, height720p) = RESOLUTION_720P
+ val inBetweenSize = Size(width720p + 10, height720p)
+
+ assertThat(videoCapabilities.findHighestSupportedQualityFor(inBetweenSize, SDR))
+ .isEqualTo(UHD)
+ }
+
+ @Test
+ fun findHighestSupportedQuality_returnsHighestQuality_whenAboveHighest() {
+ // Create a size between greater than the max quality (UHD)
+ val (width2160p, height2160p) = RESOLUTION_2160P
+ val aboveHighestSize = Size(width2160p + 10, height2160p)
+
+ assertThat(videoCapabilities.findHighestSupportedQualityFor(aboveHighestSize, SDR))
+ .isEqualTo(UHD)
+ }
+
+ @Test
+ fun findHighestSupportedQuality_returnsLowestQuality_whenBelowLowest() {
+ // Create a size below the lowest quality (HD)
+ val (width720p, height720p) = RESOLUTION_720P
+ val belowLowestSize = Size(width720p - 10, height720p)
+
+ assertThat(videoCapabilities.findHighestSupportedQualityFor(belowLowestSize, SDR))
+ .isEqualTo(HD)
+ }
+
+ @Test
+ fun findHighestSupportedQuality_returnsExactQuality_whenExactSizeGiven() {
+ val exactSize720p = RESOLUTION_720P
+
+ assertThat(videoCapabilities.findHighestSupportedQualityFor(exactSize720p, SDR))
+ .isEqualTo(HD)
+ }
+
+ @Test
+ fun findHighestSupportedEncoderProfilesFor_returnsHigherProfile() {
+ // Create a size between 720p and 2160p
+ val (width720p, height720p) = RESOLUTION_720P
+ val inBetweenSize = Size(width720p + 10, height720p)
+
+ assertThat(videoCapabilities.findHighestSupportedEncoderProfilesFor(inBetweenSize, SDR))
+ .isEqualTo(validatedProfiles2160p)
+ }
+
+ @Test
+ fun findHighestSupportedEncoderProfilesFor_returnsHighestProfile_whenAboveHighest() {
+ // Create a size between greater than the max quality (UHD)
+ val (width2160p, height2160p) = RESOLUTION_2160P
+ val aboveHighestSize = Size(width2160p + 10, height2160p)
+
+ assertThat(videoCapabilities.findHighestSupportedEncoderProfilesFor(aboveHighestSize, SDR))
+ .isEqualTo(validatedProfiles2160p)
+ }
+
+ @Test
+ fun findHighestSupportedEncoderProfilesFor_returnsLowestProfile_whenBelowLowest() {
+ // Create a size below the lowest quality (HD)
+ val (width720p, height720p) = RESOLUTION_720P
+ val belowLowestSize = Size(width720p - 10, height720p)
+
+ assertThat(videoCapabilities.findHighestSupportedEncoderProfilesFor(belowLowestSize, SDR))
+ .isEqualTo(validatedProfiles720p)
+ }
+
+ @Test
+ fun findHighestSupportedEncoderProfilesFor_returnsExactProfile_whenExactSizeGiven() {
+ val exactSize720p = RESOLUTION_720P
+
+ assertThat(videoCapabilities.findHighestSupportedEncoderProfilesFor(exactSize720p, SDR))
+ .isEqualTo(validatedProfiles720p)
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java b/camera/camera-video/src/test/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
index 30a53cc..6db867b 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
@@ -19,6 +19,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.impl.Quirks;
import java.util.ArrayList;
import java.util.List;
@@ -39,6 +40,12 @@
private DeviceQuirks() {
}
+ /** Returns all video specific quirks loaded on the current device. */
+ @NonNull
+ public static Quirks getAll() {
+ return new Quirks(DeviceQuirksLoader.loadQuirks());
+ }
+
/**
* Retrieves a specific device {@link Quirk} instance given its type.
*