/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.camera.video;
import android.media.CamcorderProfile;
import android.util.Size;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.Logger;
import androidx.camera.core.impl.CamcorderProfileProxy;
import androidx.core.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* QualitySelector defines a desired quality setting that can be used to configure components
* with quality setting requirements such as creating a
* {@link Recorder.Builder#setQualitySelector(QualitySelector) Recorder}.
*
* <p>There are pre-defined quality constants that are universally used for video, such as
* {@link #QUALITY_SD}, {@link #QUALITY_HD}, {@link #QUALITY_FHD} and {@link #QUALITY_UHD}, but
* not all of them are supported on every device since each device has its own capabilities.
* {@link #isQualitySupported(CameraInfo, int)} can be used to check whether a quality is
* supported on the device or not and {@link #getResolution(CameraInfo, int)} can be used to get
* the actual resolution defined in the device. Aside from checking the qualities one by one,
* QualitySelector provides a more convenient way to select a quality. The typical usage of
* selecting a single desired quality is:
* <pre>{@code
* QualitySelector qualitySelector = QualitySelector.of(QualitySelector.QUALITY_FHD)
* }</pre>
* Or the usage of selecting a series of qualities by desired order:
* <pre>{@code
* QualitySelector qualitySelector = QualitySelector
* .firstTry(QualitySelector.QUALITY_FHD)
* .thenTry(QualitySelector.QUALITY_HD)
* .finallyTry(QualitySelector.QUALITY_HIGHEST)
* }</pre>
* The recommended way to set the {@link Procedure#finallyTry(int)} is giving guaranteed supported
* qualities such as {@link #QUALITY_LOWEST} or {@link #QUALITY_HIGHEST}, which ensures the
* QualitySelector can always choose a supported quality. Another way to ensure a quality will be
* selected when none of the desired qualities are supported is to use
* {@link Procedure#finallyTry(int, int)} with an open-ended fallback strategy such as
* {@link #FALLBACK_STRATEGY_LOWER}:
* <pre>{@code
* QualitySelector qualitySelector = QualitySelector
* .firstTry(QualitySelector.QUALITY_UHD)
* .finallyTry(QualitySelector.QUALITY_FHD, QualitySelector.FALLBACK_STRATEGY_LOWER)
* }</pre>
* If QUALITY_UHD and QUALITY_FHD are not supported on the device, QualitySelector will select
* the quality that is closest to and lower than QUALITY_FHD. If no lower quality is supported,
* the quality that is closest to and higher than QUALITY_FHD will be selected.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class QualitySelector {
private static final String TAG = "QualitySelector";
/**
* A non-applicable quality.
*
* <p>Checking QUALITY_NONE via {@link #isQualitySupported(CameraInfo, int)} will return
* {@code false}.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY)
public static final int QUALITY_NONE = -1;
/**
* The lowest video quality supported by the video frame producer.
*/
public static final int QUALITY_LOWEST = CamcorderProfile.QUALITY_LOW;
/**
* The highest video quality supported by the video frame producer.
*/
public static final int QUALITY_HIGHEST = CamcorderProfile.QUALITY_HIGH;
/**
* Standard Definition (SD) 480p video quality.
*
* <p>This video quality usually corresponds to a resolution of 720 x 480 (480p) pixels.
*/
public static final int QUALITY_SD = CamcorderProfile.QUALITY_480P;
/**
* High Definition (HD) video quality.
*
* <p>This video quality usually corresponds to a resolution of 1280 x 720 (720p) pixels.
*/
public static final int QUALITY_HD = CamcorderProfile.QUALITY_720P;
/**
* Full High Definition (FHD) 1080p video quality.
*
* <p>This video quality usually corresponds to a resolution of 1920 x 1080 (1080p) pixels.
*/
public static final int QUALITY_FHD = CamcorderProfile.QUALITY_1080P;
/**
* Ultra High Definition (UHD) 2160p video quality.
*
* <p>This video quality usually corresponds to a resolution of 3840 x 2160 (2160p) pixels.
*/
public static final int QUALITY_UHD = CamcorderProfile.QUALITY_2160P;
/** @hide */
@IntDef({QUALITY_NONE, QUALITY_LOWEST, QUALITY_HIGHEST, QUALITY_SD, QUALITY_HD, QUALITY_FHD,
QUALITY_UHD})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(Scope.LIBRARY)
public @interface VideoQuality {
}
/** All quality constants. */
@SuppressWarnings("WeakerAccess") /* synthetic access */
static final List<Integer> QUALITIES = Arrays.asList(QUALITY_NONE, QUALITY_LOWEST,
QUALITY_HIGHEST, QUALITY_SD, QUALITY_HD, QUALITY_FHD, QUALITY_UHD);
/** Quality constants with size from large to small. */
private static final List<Integer> QUALITIES_ORDER_BY_SIZE = Arrays.asList(QUALITY_UHD,
QUALITY_FHD, QUALITY_HD, QUALITY_SD);
/**
* The strategy that no fallback strategy will be applied.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY)
public static final int FALLBACK_STRATEGY_NONE = 0;
/**
* Choose the quality that is closest to and higher than the desired quality. If that can not
* result in a supported quality, choose the quality that is closest to and lower than the
* desired quality.
*/
public static final int FALLBACK_STRATEGY_HIGHER = 1;
/**
* Choose the quality that is closest to and higher than the desired quality.
*/
public static final int FALLBACK_STRATEGY_STRICTLY_HIGHER = 2;
/**
* Choose the quality that is closest to and lower than the desired quality. If that can not
* result in a supported quality, choose the quality that is closest to and higher than the
* desired quality.
*/
public static final int FALLBACK_STRATEGY_LOWER = 3;
/**
* Choose the quality that is closest to and lower than the desired quality.
*/
public static final int FALLBACK_STRATEGY_STRICTLY_LOWER = 4;
private static final int FALLBACK_STRATEGY_START = FALLBACK_STRATEGY_NONE;
private static final int FALLBACK_STRATEGY_END = FALLBACK_STRATEGY_STRICTLY_LOWER;
/**
* The fallback strategies when desired quality is not supported.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({FALLBACK_STRATEGY_NONE,
FALLBACK_STRATEGY_HIGHER,
FALLBACK_STRATEGY_STRICTLY_HIGHER,
FALLBACK_STRATEGY_LOWER,
FALLBACK_STRATEGY_STRICTLY_LOWER
})
public @interface FallbackStrategy {
}
/**
* Checks if the input quality is one of video quality constants.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY)
public static boolean containsQuality(int quality) {
return QUALITIES.contains(quality);
}
/**
* Gets all video quality constants with clearly defined size sorted from largest to smallest.
*
* <p>{@link #QUALITY_NONE}, {@link #QUALITY_HIGHEST} and {@link #QUALITY_LOWEST} are not
* included.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY)
@NonNull
public static List<Integer> getSortedQualities() {
return new ArrayList<>(QUALITIES_ORDER_BY_SIZE);
}
/**
* Gets all supported qualities on the device.
*
* <p>The returned list is sorted by quality size from largest to smallest. For the qualities in
* the returned list, with the same input cameraInfo,
* {@link #isQualitySupported(CameraInfo, int)} will return {@code true} and
* {@link #getResolution(CameraInfo, int)} will return the corresponding resolution.
*
* <p>Note: Constants {@link #QUALITY_HIGHEST} and {@link #QUALITY_LOWEST} are not included
* in the returned list, but their corresponding qualities are included.
*
* @param cameraInfo the cameraInfo
*/
@NonNull
public static List<Integer> getSupportedQualities(@NonNull CameraInfo cameraInfo) {
return VideoCapabilities.from(cameraInfo).getSupportedQualities();
}
/**
* Checks if the quality is supported.
*
* <p>Calling this method with one of the qualities contained in the returned list of
* {@link #getSupportedQualities} will return {@code true}.
*
* <p>Possible values for {@code quality} include {@link #QUALITY_LOWEST},
* {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD}, {@link #QUALITY_FHD}
* and {@link #QUALITY_UHD}.
*
* <p>If this method is called with {@link #QUALITY_LOWEST} or {@link #QUALITY_HIGHEST}, it
* will return {@code true} except the case that none of the qualities can be supported.
*
* @param cameraInfo the cameraInfo for checking the quality.
* @param quality one of the quality constants.
* @return {@code true} if the quality is supported; {@code false} otherwise.
* @see #getSupportedQualities(CameraInfo)
*/
public static boolean isQualitySupported(@NonNull CameraInfo cameraInfo,
@VideoQuality int quality) {
return VideoCapabilities.from(cameraInfo).isQualitySupported(quality);
}
/**
* Gets the corresponding resolution from the input quality.
*
* <p>Possible values for {@code quality} include {@link #QUALITY_LOWEST},
* {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD}, {@link #QUALITY_FHD}
* and {@link #QUALITY_UHD}.
*
* @param cameraInfo the cameraInfo for checking the quality.
* @param quality one of the quality constants.
* @return the corresponding resolution from the input quality, or {@code null} if the
* quality is not supported on the device. {@link #isQualitySupported(CameraInfo, int)} can
* be used to check if the input quality is supported.
* @throws IllegalArgumentException if not a quality constant
* @see #isQualitySupported
*/
@Nullable
public static Size getResolution(@NonNull CameraInfo cameraInfo, @VideoQuality int quality) {
checkQualityConstantsOrThrow(quality);
CamcorderProfileProxy profile = VideoCapabilities.from(cameraInfo).getProfile(quality);
return profile != null ? new Size(profile.getVideoFrameWidth(),
profile.getVideoFrameHeight()) : null;
}
private final List<Integer> mPreferredQualityList;
@VideoQuality
private final int mFallbackQuality;
@FallbackStrategy
private final int mFallbackStrategy;
QualitySelector(@NonNull List<Integer> preferredQualityList,
@VideoQuality int fallbackQuality,
@FallbackStrategy int fallbackStrategy) {
Preconditions.checkArgument(preferredQualityList.size() > 0, "No preferred quality.");
mPreferredQualityList = Collections.unmodifiableList(preferredQualityList);
mFallbackQuality = fallbackQuality;
mFallbackStrategy = fallbackStrategy;
}
/**
* Sets the desired quality with the highest priority.
*
* <p>This method initiates a procedure for specifying the requirements of selecting
* qualities. Other requirements can be further added with {@link Procedure} methods.
*
* @param quality the quality constant. Possible values include {@link #QUALITY_LOWEST},
* {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD}, {@link #QUALITY_FHD},
* or {@link #QUALITY_UHD}.
* @return the {@link Procedure} for specifying quality selection requirements.
* @throws IllegalArgumentException if the given quality is not a quality constant.
* @see Procedure
*/
@NonNull
public static Procedure firstTry(@VideoQuality int quality) {
return new Procedure(quality);
}
/**
* Gets an instance of QualitySelector with only one desired quality.
*
* <p>If there are more than one desired qualities, use {@link #firstTry} for further settings.
*
* @param quality the quality constant. Possible values include {@link #QUALITY_LOWEST},
* {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD}, {@link #QUALITY_FHD},
* or {@link #QUALITY_UHD}.
* @return the QualitySelector instance.
* @throws IllegalArgumentException if the given quality is not a quality constant.
*/
@NonNull
public static QualitySelector of(@VideoQuality int quality) {
return of(quality, FALLBACK_STRATEGY_NONE);
}
/**
* Gets an instance of QualitySelector with only one desired quality and a fallback strategy.
*
* <p>If there are more than one desired qualities, use {@link #firstTry} for further settings.
*
* @param quality the quality constant. Possible values include {@link #QUALITY_LOWEST},
* {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD}, {@link #QUALITY_FHD},
* or {@link #QUALITY_UHD}.
* @param fallbackStrategy the fallback strategy. Possible values include
* {@link #FALLBACK_STRATEGY_HIGHER}, {@link #FALLBACK_STRATEGY_STRICTLY_HIGHER},
* {@link #FALLBACK_STRATEGY_LOWER} and {@link #FALLBACK_STRATEGY_STRICTLY_LOWER}.
* @return the QualitySelector instance.
* @throws IllegalArgumentException if {@code quality} is not a quality constant or
* {@code fallbackStrategy} is not a fallback strategy constant.
*/
@NonNull
public static QualitySelector of(@VideoQuality int quality,
@FallbackStrategy int fallbackStrategy) {
return firstTry(quality).finallyTry(quality, fallbackStrategy);
}
/**
* 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 supported
* quality list on this device. The desired qualities can be set by a series of try methods such
* as {@link #firstTry(int)}, {@link #of(int)}, {@link Procedure#thenTry(int)} and
* {@link Procedure#finallyTry(int)}. The fallback strategy can be set via
* {@link #of(int, int)} and {@link Procedure#finallyTry(int, int)}. If no fallback strategy
* is specified, {@link #FALLBACK_STRATEGY_NONE} will be applied by default.
*
* <p>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 cameraInfo the cameraInfo for checking the quality.
* @return a sorted supported quality list according to the desired quality settings.
* @see Procedure
*
* @hide
*/
@RestrictTo(Scope.LIBRARY)
@NonNull
public List<Integer> getPrioritizedQualities(@NonNull CameraInfo cameraInfo) {
VideoCapabilities videoCapabilities = VideoCapabilities.from(cameraInfo);
List<Integer> supportedQualities = videoCapabilities.getSupportedQualities();
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<Integer> sortedQualities = new LinkedHashSet<>();
// Add exact quality.
for (Integer 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<Integer> reversedList = new ArrayList<>(supportedQualities);
Collections.reverse(reversedList);
sortedQualities.addAll(reversedList);
break;
} else {
if (supportedQualities.contains(quality)) {
sortedQualities.add(quality);
}
}
}
// Add quality by fallback strategy based on fallback quality.
addByFallbackStrategy(supportedQualities, sortedQualities);
return new ArrayList<>(sortedQualities);
}
@NonNull
@Override
public String toString() {
return "QualitySelector{"
+ "preferredQualities=" + mPreferredQualityList
+ ", fallbackQuality=" + mFallbackQuality
+ ", fallbackStrategy=" + mFallbackStrategy
+ "}";
}
private void addByFallbackStrategy(@NonNull List<Integer> supportedQualities,
@NonNull Set<Integer> priorityQualities) {
if (supportedQualities.isEmpty()) {
return;
}
if (priorityQualities.containsAll(supportedQualities)) {
// priorityQualities already contains all supported qualities, no need to add by
// fallback strategy.
return;
}
Logger.d(TAG, "Select quality by fallbackStrategy = " + mFallbackStrategy
+ " on fallback quality = " + mFallbackQuality);
// No fallback strategy, return directly.
if (mFallbackStrategy == QualitySelector.FALLBACK_STRATEGY_NONE) {
return;
}
// Note that fallback quality could be an unsupported quality, so all quality constants
// need to be loaded to find the position of fallback quality.
// The list returned from getSortedQualities() is sorted from large to small.
List<Integer> sizeSortedQualities = getSortedQualities();
int fallbackQuality;
if (mFallbackQuality == QUALITY_HIGHEST) {
fallbackQuality = sizeSortedQualities.get(0);
} else if (mFallbackQuality == QUALITY_LOWEST) {
fallbackQuality = sizeSortedQualities.get(sizeSortedQualities.size() - 1);
} else {
fallbackQuality = mFallbackQuality;
}
int index = sizeSortedQualities.indexOf(fallbackQuality);
Preconditions.checkState(index != -1); // Should not happen.
// search larger supported quality
List<Integer> largerQualities = new ArrayList<>();
for (int i = index - 1; i >= 0; i--) {
int quality = sizeSortedQualities.get(i);
if (supportedQualities.contains(quality)) {
largerQualities.add(quality);
}
}
// search smaller supported quality
List<Integer> smallerQualities = new ArrayList<>();
for (int i = index + 1; i < sizeSortedQualities.size(); i++) {
int quality = sizeSortedQualities.get(i);
if (supportedQualities.contains(quality)) {
smallerQualities.add(quality);
}
}
Logger.d(TAG, "sizeSortedQualities = " + sizeSortedQualities
+ ", fallback quality = " + fallbackQuality
+ ", largerQualities = " + largerQualities
+ ", smallerQualities = " + smallerQualities);
switch (mFallbackStrategy) {
case QualitySelector.FALLBACK_STRATEGY_HIGHER:
priorityQualities.addAll(largerQualities);
priorityQualities.addAll(smallerQualities);
break;
case QualitySelector.FALLBACK_STRATEGY_STRICTLY_HIGHER:
priorityQualities.addAll(largerQualities);
break;
case QualitySelector.FALLBACK_STRATEGY_LOWER:
priorityQualities.addAll(smallerQualities);
priorityQualities.addAll(largerQualities);
break;
case QualitySelector.FALLBACK_STRATEGY_STRICTLY_LOWER:
priorityQualities.addAll(smallerQualities);
break;
case QualitySelector.FALLBACK_STRATEGY_NONE:
// No-Op
break;
default:
throw new AssertionError("Unhandled fallback strategy: " + mFallbackStrategy);
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
static void checkQualityConstantsOrThrow(@QualitySelector.VideoQuality int quality) {
Preconditions.checkArgument(QualitySelector.containsQuality(quality),
"Unknown quality: " + quality);
}
/**
* Procedure can be used to build {@link QualitySelector} with further settings.
*
* <p>A QualitySelector can be simply created by {@link QualitySelector#of QualitySelector.of}
* with a single desired quality. When more quality settings are needed, use
* {@link QualitySelector#firstTry} to get a Procedure, chain the desired qualities by
* {@link #thenTry} and generate the QualitySelector by {@link #finallyTry} with or without
* fallback strategy. For example:
*
* <pre>{@code
* QualitySelector qualitySelector = QualitySelector
* .firstTry(QualitySelector.QUALITY_UHD)
* .thenTry(QualitySelector.QUALITY_HD)
* .finallyTry(QualitySelector.QUALITY_SD)
* }</pre>
* or
<pre>{@code
* QualitySelector qualitySelector = QualitySelector
* .firstTry(QualitySelector.QUALITY_UHD)
* .finallyTry(QualitySelector.QUALITY_HD,
* QualitySelector.FALLBACK_STRATEGY_STRICTLY_LOWER)
* }</pre>
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public static class Procedure {
private final List<Integer> mPreferredQualityList = new ArrayList<>();
Procedure(int quality) {
addQuality(quality);
}
/**
* Adds a quality candidate.
*
* @param quality the quality constant. Possible values include {@link #QUALITY_LOWEST},
* {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD},
* {@link #QUALITY_FHD} or {@link #QUALITY_UHD}.
* @return the procedure that can continue to be set
* @throws IllegalArgumentException if the given quality is not a quality constant
*/
@NonNull
public Procedure thenTry(@VideoQuality int quality) {
addQuality(quality);
return this;
}
/**
* Sets the final desired quality.
*
* <p>This method finishes the setting procedure and generates a {@link QualitySelector}
* with the requirements set to the procedure.
*
* @param quality the quality constant. Possible values include {@link #QUALITY_LOWEST},
* {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD},
* {@link #QUALITY_FHD} or {@link #QUALITY_UHD}.
* @return the {@link QualitySelector}.
* @throws IllegalArgumentException if the given quality is not a quality constant
*/
@NonNull
public QualitySelector finallyTry(@VideoQuality int quality) {
return finallyTry(quality, FALLBACK_STRATEGY_NONE);
}
/**
* Sets the final desired quality and fallback strategy.
*
* <p>The fallback strategy will be applied on this quality when all desired qualities are
* not supported.
*
* <p>This method finishes the setting procedure and generates a {@link QualitySelector}
* with the requirements set to the procedure.
*
* @param quality the quality constant. Possible values include {@link #QUALITY_LOWEST},
* {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD},
* {@link #QUALITY_FHD} or {@link #QUALITY_UHD}.
* @param fallbackStrategy the fallback strategy. Possible values include
* {@link #FALLBACK_STRATEGY_HIGHER}, {@link #FALLBACK_STRATEGY_STRICTLY_HIGHER},
* {@link #FALLBACK_STRATEGY_LOWER} and {@link #FALLBACK_STRATEGY_STRICTLY_LOWER}.
* @return the {@link QualitySelector}.
* @throws IllegalArgumentException if {@code quality} is not a quality constant or
* {@code fallbackStrategy} is not a fallback strategy constant.
*/
@NonNull
public QualitySelector finallyTry(@VideoQuality int quality,
@FallbackStrategy int fallbackStrategy) {
Preconditions.checkArgument(fallbackStrategy >= FALLBACK_STRATEGY_START
&& fallbackStrategy <= FALLBACK_STRATEGY_END,
"The value must be a fallback strategy constant.");
addQuality(quality);
return new QualitySelector(new ArrayList<>(mPreferredQualityList), quality,
fallbackStrategy);
}
private void addQuality(@VideoQuality int quality) {
checkQualityConstantsOrThrow(quality);
Preconditions.checkArgument(quality != QUALITY_NONE, "Unsupported quality: " + quality);
mPreferredQualityList.add(quality);
}
}
}