/*
* 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.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.Logger;
import androidx.camera.core.impl.CamcorderProfileProxy;
import androidx.core.util.Preconditions;
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, Quality)} can be used to check whether a quality is
* supported on the device or not and {@link #getResolution(CameraInfo, Quality)} 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.from(Quality.FHD);
* }</pre>
* or the usage of selecting a series of qualities by desired order:
* <pre>{@code
* QualitySelector qualitySelector = QualitySelector.fromOrderedList(
* Arrays.asList(Quality.FHD, Quality.HD, Quality.HIGHEST)
* );
* }</pre>
* The recommended way is giving a guaranteed supported quality such as {@link Quality#LOWEST} or
* {@link Quality#HIGHEST} in the end of the desired quality list, 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 #fromOrderedList(List, FallbackStrategy)} with an open-ended fallback strategy such as
* a fallback strategy from {@link FallbackStrategy#lowerQualityOrHigherThan(Quality)}:
* <pre>{@code
* QualitySelector qualitySelector = QualitySelector.fromOrderedList(
* Arrays.asList(Quality.UHD, Quality.FHD),
* FallbackStrategy.lowerQualityOrHigherThan(Quality.FHD)
* );
* }</pre>
* If UHD and FHD are not supported on the device, QualitySelector will select the quality that
* is closest to and lower than FHD. If no lower quality is supported, the quality that is
* closest to and higher than FHD will be selected.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class QualitySelector {
private static final String TAG = "QualitySelector";
/**
* 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, Quality)} will return {@code true} and
* {@link #getResolution(CameraInfo, Quality)} 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<Quality> 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,
@NonNull Quality 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, Quality)} can
* be used to check if the input quality is supported.
* @throws IllegalArgumentException if quality is not one of the possible values.
* @see #isQualitySupported
*/
@Nullable
public static Size getResolution(@NonNull CameraInfo cameraInfo, @NonNull Quality quality) {
checkQualityConstantsOrThrow(quality);
CamcorderProfileProxy profile = VideoCapabilities.from(cameraInfo).getProfile(quality);
return profile != null ? new Size(profile.getVideoFrameWidth(),
profile.getVideoFrameHeight()) : null;
}
private final List<Quality> mPreferredQualityList;
private final FallbackStrategy mFallbackStrategy;
QualitySelector(@NonNull List<Quality> preferredQualityList,
@NonNull FallbackStrategy fallbackStrategy) {
Preconditions.checkArgument(
!preferredQualityList.isEmpty() || fallbackStrategy != FallbackStrategy.NONE,
"No preferred quality and fallback strategy.");
mPreferredQualityList = Collections.unmodifiableList(new ArrayList<>(preferredQualityList));
mFallbackStrategy = fallbackStrategy;
}
/**
* Gets an instance of QualitySelector with a desired quality.
*
* @param quality the quality. 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 NullPointerException if {@code quality} is {@code null}.
* @throws IllegalArgumentException if {@code quality} is not one of the possible values.
*/
@NonNull
public static QualitySelector from(@NonNull Quality quality) {
return from(quality, FallbackStrategy.NONE);
}
/**
* Gets an instance of QualitySelector with a desired quality and a fallback strategy.
*
* <p>If the quality is not supported, the fallback strategy will be applied. The fallback
* strategy can be created by {@link FallbackStrategy} API such as
* {@link FallbackStrategy#lowerQualityThan(Quality)}.
*
* @param quality the quality. 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 that will be applied when the device does
* not support {@code quality}.
* @return the QualitySelector instance.
* @throws NullPointerException if {@code quality} is {@code null} or {@code fallbackStrategy}
* is {@code null}.
* @throws IllegalArgumentException if {@code quality} is not one of the possible values.
*/
@NonNull
public static QualitySelector from(@NonNull Quality quality,
@NonNull FallbackStrategy fallbackStrategy) {
Preconditions.checkNotNull(quality, "quality cannot be null");
Preconditions.checkNotNull(fallbackStrategy, "fallbackStrategy cannot be null");
checkQualityConstantsOrThrow(quality);
return new QualitySelector(Arrays.asList(quality), fallbackStrategy);
}
/**
* Gets an instance of QualitySelector with ordered desired qualities.
*
* <p>The final quality will be selected according to the order in the quality list.
*
* @param qualities the quality list. 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 NullPointerException if {@code qualities} is {@code null}.
* @throws IllegalArgumentException if {@code qualities} is empty or contains a quality that is
* not one of the possible values, including a {@code null} value.
*/
@NonNull
public static QualitySelector fromOrderedList(@NonNull List<Quality> qualities) {
return fromOrderedList(qualities, FallbackStrategy.NONE);
}
/**
* Gets an instance of QualitySelector with ordered desired qualities and a fallback strategy.
*
* <p>The final quality will be selected according to the order in the quality list.
* If no quality is supported, the fallback strategy will be applied. The fallback
* strategy can be created by {@link FallbackStrategy} API such as
* {@link FallbackStrategy#lowerQualityThan(Quality)}.
*
* @param qualities the quality list. 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 that will be applied when the device does
* not support those {@code qualities}.
* @throws NullPointerException if {@code qualities} is {@code null} or
* {@code fallbackStrategy} is {@code null}.
* @throws IllegalArgumentException if {@code qualities} is empty or contains a quality that is
* not one of the possible values, including a {@code null} value.
*/
@NonNull
public static QualitySelector fromOrderedList(@NonNull List<Quality> qualities,
@NonNull FallbackStrategy fallbackStrategy) {
Preconditions.checkNotNull(qualities, "qualities cannot be null");
Preconditions.checkNotNull(fallbackStrategy, "fallbackStrategy cannot be null");
Preconditions.checkArgument(!qualities.isEmpty(), "qualities cannot be empty");
checkQualityConstantsOrThrow(qualities);
return new QualitySelector(qualities, 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 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.
*/
@NonNull
List<Quality> getPrioritizedQualities(@NonNull CameraInfo cameraInfo) {
VideoCapabilities videoCapabilities = VideoCapabilities.from(cameraInfo);
List<Quality> 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<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() {
return "QualitySelector{"
+ "preferredQualities=" + mPreferredQualityList
+ ", fallbackStrategy=" + mFallbackStrategy
+ "}";
}
private void addByFallbackStrategy(@NonNull List<Quality> supportedQualities,
@NonNull Set<Quality> 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);
// No fallback strategy, return directly.
if (mFallbackStrategy == FallbackStrategy.NONE) {
return;
}
Preconditions.checkState(mFallbackStrategy instanceof FallbackStrategy.RuleStrategy,
"Currently only support type RuleStrategy");
FallbackStrategy.RuleStrategy fallbackStrategy =
(FallbackStrategy.RuleStrategy) mFallbackStrategy;
// 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<Quality> sizeSortedQualities = Quality.getSortedQualities();
Quality fallbackQuality;
if (fallbackStrategy.getFallbackQuality() == Quality.HIGHEST) {
fallbackQuality = sizeSortedQualities.get(0);
} else if (fallbackStrategy.getFallbackQuality() == Quality.LOWEST) {
fallbackQuality = sizeSortedQualities.get(sizeSortedQualities.size() - 1);
} else {
fallbackQuality = fallbackStrategy.getFallbackQuality();
}
int index = sizeSortedQualities.indexOf(fallbackQuality);
Preconditions.checkState(index != -1); // Should not happen.
// search larger supported quality
List<Quality> largerQualities = new ArrayList<>();
for (int i = index - 1; i >= 0; i--) {
Quality quality = sizeSortedQualities.get(i);
if (supportedQualities.contains(quality)) {
largerQualities.add(quality);
}
}
// search smaller supported quality
List<Quality> smallerQualities = new ArrayList<>();
for (int i = index + 1; i < sizeSortedQualities.size(); i++) {
Quality quality = sizeSortedQualities.get(i);
if (supportedQualities.contains(quality)) {
smallerQualities.add(quality);
}
}
Logger.d(TAG, "sizeSortedQualities = " + sizeSortedQualities
+ ", fallback quality = " + fallbackQuality
+ ", largerQualities = " + largerQualities
+ ", smallerQualities = " + smallerQualities);
switch (fallbackStrategy.getFallbackRule()) {
case FallbackStrategy.FALLBACK_RULE_HIGHER_OR_LOWER:
priorityQualities.addAll(largerQualities);
priorityQualities.addAll(smallerQualities);
break;
case FallbackStrategy.FALLBACK_RULE_HIGHER:
priorityQualities.addAll(largerQualities);
break;
case FallbackStrategy.FALLBACK_RULE_LOWER_OR_HIGHER:
priorityQualities.addAll(smallerQualities);
priorityQualities.addAll(largerQualities);
break;
case FallbackStrategy.FALLBACK_RULE_LOWER:
priorityQualities.addAll(smallerQualities);
break;
case FallbackStrategy.FALLBACK_RULE_NONE:
// No-Op
break;
default:
throw new AssertionError("Unhandled fallback strategy: " + mFallbackStrategy);
}
}
private static void checkQualityConstantsOrThrow(@NonNull List<Quality> qualities) {
for (Quality quality : qualities) {
Preconditions.checkArgument(Quality.containsQuality(quality),
"qualities contain invalid quality: " + quality);
}
}
private static void checkQualityConstantsOrThrow(@NonNull Quality quality) {
Preconditions.checkArgument(Quality.containsQuality(quality),
"Invalid quality: " + quality);
}
}