1 /* 2 * Copyright 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.camera.video; 18 19 import static androidx.camera.core.DynamicRange.SDR; 20 21 import static java.util.Collections.singletonList; 22 import static java.util.Objects.requireNonNull; 23 24 import android.annotation.SuppressLint; 25 import android.util.Size; 26 27 import androidx.annotation.RestrictTo; 28 import androidx.annotation.VisibleForTesting; 29 import androidx.camera.core.CameraInfo; 30 import androidx.camera.core.DynamicRange; 31 import androidx.camera.core.Logger; 32 import androidx.camera.video.internal.VideoValidatedEncoderProfilesProxy; 33 import androidx.core.util.Preconditions; 34 35 import org.jspecify.annotations.NonNull; 36 import org.jspecify.annotations.Nullable; 37 38 import java.util.ArrayList; 39 import java.util.Collections; 40 import java.util.HashMap; 41 import java.util.LinkedHashSet; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Set; 45 46 /** 47 * QualitySelector defines a desired quality setting that can be used to configure components 48 * with quality setting requirements such as creating a 49 * {@link Recorder.Builder#setQualitySelector(QualitySelector) Recorder}. 50 * 51 * <p>There are pre-defined quality constants that are universally used for video, such as 52 * {@link Quality#SD}, {@link Quality#HD}, {@link Quality#FHD} and {@link Quality#UHD}, but 53 * not all of them are supported on every device since each device has its own capabilities. 54 * {@link #isQualitySupported(CameraInfo, Quality)} can be used to check whether a quality is 55 * supported on the device or not and {@link #getResolution(CameraInfo, Quality)} can be used to get 56 * the actual resolution defined in the device. Aside from checking the qualities one by one, 57 * QualitySelector provides a more convenient way to select a quality. The typical usage of 58 * selecting a single desired quality is: 59 * <pre>{@code 60 * QualitySelector qualitySelector = QualitySelector.from(Quality.FHD); 61 * }</pre> 62 * or the usage of selecting a series of qualities by desired order: 63 * <pre>{@code 64 * QualitySelector qualitySelector = QualitySelector.fromOrderedList( 65 * Arrays.asList(Quality.FHD, Quality.HD, Quality.HIGHEST) 66 * ); 67 * }</pre> 68 * The recommended way is giving a guaranteed supported quality such as {@link Quality#LOWEST} or 69 * {@link Quality#HIGHEST} in the end of the desired quality list, which ensures the 70 * QualitySelector can always choose a supported quality. Another way to ensure a quality will be 71 * selected when none of the desired qualities are supported is to use 72 * {@link #fromOrderedList(List, FallbackStrategy)} with an open-ended fallback strategy such as 73 * a fallback strategy from {@link FallbackStrategy#lowerQualityOrHigherThan(Quality)}: 74 * <pre>{@code 75 * QualitySelector qualitySelector = QualitySelector.fromOrderedList( 76 * Arrays.asList(Quality.UHD, Quality.FHD), 77 * FallbackStrategy.lowerQualityOrHigherThan(Quality.FHD) 78 * ); 79 * }</pre> 80 * If UHD and FHD are not supported on the device, QualitySelector will select the quality that 81 * is closest to and lower than FHD. If no lower quality is supported, the quality that is 82 * closest to and higher than FHD will be selected. 83 */ 84 public final class QualitySelector { 85 private static final String TAG = "QualitySelector"; 86 87 /** 88 * Gets all supported qualities on the device. 89 * 90 * <p>The returned list is sorted by quality size from largest to smallest. For the qualities in 91 * the returned list, with the same input cameraInfo, 92 * {@link #isQualitySupported(CameraInfo, Quality)} will return {@code true} and 93 * {@link #getResolution(CameraInfo, Quality)} will return the corresponding resolution. 94 * 95 * <p>Note: Constants {@link Quality#HIGHEST} and {@link Quality#LOWEST} are not included 96 * in the returned list, but their corresponding qualities are included. 97 * 98 * @param cameraInfo the cameraInfo 99 * 100 * @deprecated use {@link VideoCapabilities#getSupportedQualities(DynamicRange)} instead. 101 */ 102 @Deprecated getSupportedQualities(@onNull CameraInfo cameraInfo)103 public static @NonNull List<Quality> getSupportedQualities(@NonNull CameraInfo cameraInfo) { 104 return Recorder.getVideoCapabilities(cameraInfo).getSupportedQualities(SDR); 105 } 106 107 /** 108 * Checks if the quality is supported. 109 * 110 * <p>Calling this method with one of the qualities contained in the returned list of 111 * {@link #getSupportedQualities} will return {@code true}. 112 * 113 * <p>Possible values for {@code quality} include {@link Quality#LOWEST}, 114 * {@link Quality#HIGHEST}, {@link Quality#SD}, {@link Quality#HD}, {@link Quality#FHD} 115 * and {@link Quality#UHD}. 116 * 117 * <p>If this method is called with {@link Quality#LOWEST} or {@link Quality#HIGHEST}, it 118 * will return {@code true} except the case that none of the qualities can be supported. 119 * 120 * @param cameraInfo the cameraInfo for checking the quality. 121 * @param quality one of the quality constants. 122 * @return {@code true} if the quality is supported; {@code false} otherwise. 123 * @see #getSupportedQualities(CameraInfo) 124 * 125 * @deprecated use {@link VideoCapabilities#isQualitySupported(Quality, DynamicRange)} instead. 126 */ 127 @Deprecated isQualitySupported(@onNull CameraInfo cameraInfo, @NonNull Quality quality)128 public static boolean isQualitySupported(@NonNull CameraInfo cameraInfo, 129 @NonNull Quality quality) { 130 return Recorder.getVideoCapabilities(cameraInfo).isQualitySupported(quality, SDR); 131 } 132 133 /** 134 * Gets the corresponding resolution from the input quality. 135 * 136 * <p>Possible values for {@code quality} include {@link Quality#LOWEST}, 137 * {@link Quality#HIGHEST}, {@link Quality#SD}, {@link Quality#HD}, {@link Quality#FHD} 138 * and {@link Quality#UHD}. 139 * 140 * @param cameraInfo the cameraInfo for checking the quality. 141 * @param quality one of the quality constants. 142 * @return the corresponding resolution from the input quality, or {@code null} if the 143 * quality is not supported on the device. {@link #isQualitySupported(CameraInfo, Quality)} can 144 * be used to check if the input quality is supported. 145 * @throws IllegalArgumentException if quality is not one of the possible values. 146 * @see #isQualitySupported 147 */ getResolution(@onNull CameraInfo cameraInfo, @NonNull Quality quality)148 public static @Nullable Size getResolution(@NonNull CameraInfo cameraInfo, 149 @NonNull Quality quality) { 150 checkQualityConstantsOrThrow(quality); 151 VideoCapabilities videoCapabilities = Recorder.getVideoCapabilities(cameraInfo); 152 VideoValidatedEncoderProfilesProxy profiles = videoCapabilities.getProfiles(quality, SDR); 153 return profiles != null ? getProfileVideoSize(profiles) : null; 154 } 155 156 /** 157 * Gets a map from all supported qualities to mapped resolutions. 158 * 159 * @param videoCapabilities the videoCapabilities to query the supported qualities. 160 * @param dynamicRange the dynamicRange to query the supported qualities. 161 */ 162 @RestrictTo(RestrictTo.Scope.LIBRARY) getQualityToResolutionMap( @onNull VideoCapabilities videoCapabilities, @NonNull DynamicRange dynamicRange)163 public static @NonNull Map<Quality, Size> getQualityToResolutionMap( 164 @NonNull VideoCapabilities videoCapabilities, @NonNull DynamicRange dynamicRange) { 165 Map<Quality, Size> map = new HashMap<>(); 166 for (Quality supportedQuality : videoCapabilities.getSupportedQualities(dynamicRange)) { 167 map.put(supportedQuality, getProfileVideoSize( 168 requireNonNull(videoCapabilities.getProfiles(supportedQuality, dynamicRange)))); 169 } 170 return map; 171 } 172 173 private final List<Quality> mPreferredQualityList; 174 private final FallbackStrategy mFallbackStrategy; 175 QualitySelector(@onNull List<Quality> preferredQualityList, @NonNull FallbackStrategy fallbackStrategy)176 QualitySelector(@NonNull List<Quality> preferredQualityList, 177 @NonNull FallbackStrategy fallbackStrategy) { 178 Preconditions.checkArgument( 179 !preferredQualityList.isEmpty() || fallbackStrategy != FallbackStrategy.NONE, 180 "No preferred quality and fallback strategy."); 181 mPreferredQualityList = Collections.unmodifiableList(new ArrayList<>(preferredQualityList)); 182 mFallbackStrategy = fallbackStrategy; 183 } 184 185 /** 186 * Gets an instance of QualitySelector with a desired quality. 187 * 188 * @param quality the quality. Possible values include {@link Quality#LOWEST}, 189 * {@link Quality#HIGHEST}, {@link Quality#SD}, {@link Quality#HD}, {@link Quality#FHD}, 190 * or {@link Quality#UHD}. 191 * @return the QualitySelector instance. 192 * @throws NullPointerException if {@code quality} is {@code null}. 193 * @throws IllegalArgumentException if {@code quality} is not one of the possible values. 194 */ from(@onNull Quality quality)195 public static @NonNull QualitySelector from(@NonNull Quality quality) { 196 return from(quality, FallbackStrategy.NONE); 197 } 198 199 /** 200 * Gets an instance of QualitySelector with a desired quality and a fallback strategy. 201 * 202 * <p>If the quality is not supported, the fallback strategy will be applied. The fallback 203 * strategy can be created by {@link FallbackStrategy} API such as 204 * {@link FallbackStrategy#lowerQualityThan(Quality)}. 205 * 206 * @param quality the quality. Possible values include {@link Quality#LOWEST}, 207 * {@link Quality#HIGHEST}, {@link Quality#SD}, {@link Quality#HD}, {@link Quality#FHD}, 208 * or {@link Quality#UHD}. 209 * @param fallbackStrategy the fallback strategy that will be applied when the device does 210 * not support {@code quality}. 211 * @return the QualitySelector instance. 212 * @throws NullPointerException if {@code quality} is {@code null} or {@code fallbackStrategy} 213 * is {@code null}. 214 * @throws IllegalArgumentException if {@code quality} is not one of the possible values. 215 */ from(@onNull Quality quality, @NonNull FallbackStrategy fallbackStrategy)216 public static @NonNull QualitySelector from(@NonNull Quality quality, 217 @NonNull FallbackStrategy fallbackStrategy) { 218 Preconditions.checkNotNull(quality, "quality cannot be null"); 219 Preconditions.checkNotNull(fallbackStrategy, "fallbackStrategy cannot be null"); 220 checkQualityConstantsOrThrow(quality); 221 return new QualitySelector(singletonList(quality), fallbackStrategy); 222 } 223 224 /** 225 * Gets an instance of QualitySelector with ordered desired qualities. 226 * 227 * <p>The final quality will be selected according to the order in the quality list. 228 * 229 * @param qualities the quality list. Possible values include {@link Quality#LOWEST}, 230 * {@link Quality#HIGHEST}, {@link Quality#SD}, {@link Quality#HD}, {@link Quality#FHD}, 231 * or {@link Quality#UHD}. 232 * @return the QualitySelector instance. 233 * @throws NullPointerException if {@code qualities} is {@code null}. 234 * @throws IllegalArgumentException if {@code qualities} is empty or contains a quality that is 235 * not one of the possible values, including a {@code null} value. 236 */ fromOrderedList(@onNull List<Quality> qualities)237 public static @NonNull QualitySelector fromOrderedList(@NonNull List<Quality> qualities) { 238 return fromOrderedList(qualities, FallbackStrategy.NONE); 239 } 240 241 /** 242 * Gets an instance of QualitySelector with ordered desired qualities and a fallback strategy. 243 * 244 * <p>The final quality will be selected according to the order in the quality list. 245 * If no quality is supported, the fallback strategy will be applied. The fallback 246 * strategy can be created by {@link FallbackStrategy} API such as 247 * {@link FallbackStrategy#lowerQualityThan(Quality)}. 248 * 249 * @param qualities the quality list. Possible values include {@link Quality#LOWEST}, 250 * {@link Quality#HIGHEST}, {@link Quality#SD}, {@link Quality#HD}, {@link Quality#FHD}, 251 * or {@link Quality#UHD}. 252 * @param fallbackStrategy the fallback strategy that will be applied when the device does 253 * not support those {@code qualities}. 254 * @throws NullPointerException if {@code qualities} is {@code null} or 255 * {@code fallbackStrategy} is {@code null}. 256 * @throws IllegalArgumentException if {@code qualities} is empty or contains a quality that is 257 * not one of the possible values, including a {@code null} value. 258 */ fromOrderedList(@onNull List<Quality> qualities, @NonNull FallbackStrategy fallbackStrategy)259 public static @NonNull QualitySelector fromOrderedList(@NonNull List<Quality> qualities, 260 @NonNull FallbackStrategy fallbackStrategy) { 261 Preconditions.checkNotNull(qualities, "qualities cannot be null"); 262 Preconditions.checkNotNull(fallbackStrategy, "fallbackStrategy cannot be null"); 263 Preconditions.checkArgument(!qualities.isEmpty(), "qualities cannot be empty"); 264 checkQualityConstantsOrThrow(qualities); 265 return new QualitySelector(qualities, fallbackStrategy); 266 } 267 268 /** 269 * Generates a sorted quality list that matches the desired quality settings. 270 * 271 * <p>The method bases on the desired qualities and the fallback strategy to find a matched 272 * quality list on this device. The search algorithm first checks which desired quality is 273 * supported according to the set sequence and adds to the returned list by order. Then the 274 * fallback strategy will be applied to add more valid qualities. 275 * 276 * @param supportedQualities the supported qualities. 277 * @return a sorted supported quality list according to the desired quality settings. 278 */ 279 @SuppressLint("UsesNonDefaultVisibleForTesting") 280 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) 281 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) getPrioritizedQualities( @onNull List<Quality> supportedQualities)282 public @NonNull List<Quality> getPrioritizedQualities( 283 @NonNull List<Quality> supportedQualities) { 284 if (supportedQualities.isEmpty()) { 285 Logger.w(TAG, "No supported quality on the device."); 286 return new ArrayList<>(); 287 } 288 Logger.d(TAG, "supportedQualities = " + supportedQualities); 289 290 // Use LinkedHashSet to prevent from duplicate quality and keep the adding order. 291 Set<Quality> sortedQualities = new LinkedHashSet<>(); 292 // Add exact quality. 293 for (Quality quality : mPreferredQualityList) { 294 if (quality == Quality.HIGHEST) { 295 // Highest means user want a quality as higher as possible, so the return list can 296 // contain all supported resolutions from large to small. 297 sortedQualities.addAll(supportedQualities); 298 break; 299 } else if (quality == Quality.LOWEST) { 300 // Opposite to the highest 301 List<Quality> reversedList = new ArrayList<>(supportedQualities); 302 Collections.reverse(reversedList); 303 sortedQualities.addAll(reversedList); 304 break; 305 } else { 306 if (supportedQualities.contains(quality)) { 307 sortedQualities.add(quality); 308 } else { 309 Logger.w(TAG, "quality is not supported and will be ignored: " + quality); 310 } 311 } 312 } 313 314 // Add quality by fallback strategy based on fallback quality. 315 addByFallbackStrategy(supportedQualities, sortedQualities); 316 317 return new ArrayList<>(sortedQualities); 318 } 319 320 @Override toString()321 public @NonNull String toString() { 322 return "QualitySelector{" 323 + "preferredQualities=" + mPreferredQualityList 324 + ", fallbackStrategy=" + mFallbackStrategy 325 + "}"; 326 } 327 addByFallbackStrategy(@onNull List<Quality> supportedQualities, @NonNull Set<Quality> priorityQualities)328 private void addByFallbackStrategy(@NonNull List<Quality> supportedQualities, 329 @NonNull Set<Quality> priorityQualities) { 330 if (supportedQualities.isEmpty()) { 331 return; 332 } 333 if (priorityQualities.containsAll(supportedQualities)) { 334 // priorityQualities already contains all supported qualities, no need to add by 335 // fallback strategy. 336 return; 337 } 338 Logger.d(TAG, "Select quality by fallbackStrategy = " + mFallbackStrategy); 339 // No fallback strategy, return directly. 340 if (mFallbackStrategy == FallbackStrategy.NONE) { 341 return; 342 } 343 Preconditions.checkState(mFallbackStrategy instanceof FallbackStrategy.RuleStrategy, 344 "Currently only support type RuleStrategy"); 345 FallbackStrategy.RuleStrategy fallbackStrategy = 346 (FallbackStrategy.RuleStrategy) mFallbackStrategy; 347 348 // Note that fallback quality could be an unsupported quality, so all quality constants 349 // need to be loaded to find the position of fallback quality. 350 // The list returned from getSortedQualities() is sorted from large to small. 351 List<Quality> sizeSortedQualities = Quality.getSortedQualities(); 352 Quality fallbackQuality; 353 if (fallbackStrategy.getFallbackQuality() == Quality.HIGHEST) { 354 fallbackQuality = sizeSortedQualities.get(0); 355 } else if (fallbackStrategy.getFallbackQuality() == Quality.LOWEST) { 356 fallbackQuality = sizeSortedQualities.get(sizeSortedQualities.size() - 1); 357 } else { 358 fallbackQuality = fallbackStrategy.getFallbackQuality(); 359 } 360 361 int index = sizeSortedQualities.indexOf(fallbackQuality); 362 Preconditions.checkState(index != -1); // Should not happen. 363 364 // search larger supported quality 365 List<Quality> largerQualities = new ArrayList<>(); 366 for (int i = index - 1; i >= 0; i--) { 367 Quality quality = sizeSortedQualities.get(i); 368 if (supportedQualities.contains(quality)) { 369 largerQualities.add(quality); 370 } 371 } 372 373 // search smaller supported quality 374 List<Quality> smallerQualities = new ArrayList<>(); 375 for (int i = index + 1; i < sizeSortedQualities.size(); i++) { 376 Quality quality = sizeSortedQualities.get(i); 377 if (supportedQualities.contains(quality)) { 378 smallerQualities.add(quality); 379 } 380 } 381 382 Logger.d(TAG, "sizeSortedQualities = " + sizeSortedQualities 383 + ", fallback quality = " + fallbackQuality 384 + ", largerQualities = " + largerQualities 385 + ", smallerQualities = " + smallerQualities); 386 387 switch (fallbackStrategy.getFallbackRule()) { 388 case FallbackStrategy.FALLBACK_RULE_HIGHER_OR_LOWER: 389 priorityQualities.addAll(largerQualities); 390 priorityQualities.addAll(smallerQualities); 391 break; 392 case FallbackStrategy.FALLBACK_RULE_HIGHER: 393 priorityQualities.addAll(largerQualities); 394 break; 395 case FallbackStrategy.FALLBACK_RULE_LOWER_OR_HIGHER: 396 priorityQualities.addAll(smallerQualities); 397 priorityQualities.addAll(largerQualities); 398 break; 399 case FallbackStrategy.FALLBACK_RULE_LOWER: 400 priorityQualities.addAll(smallerQualities); 401 break; 402 case FallbackStrategy.FALLBACK_RULE_NONE: 403 // No-Op 404 break; 405 default: 406 throw new AssertionError("Unhandled fallback strategy: " + mFallbackStrategy); 407 } 408 } 409 getProfileVideoSize( @onNull VideoValidatedEncoderProfilesProxy profiles)410 private static @NonNull Size getProfileVideoSize( 411 @NonNull VideoValidatedEncoderProfilesProxy profiles) { 412 return profiles.getDefaultVideoProfile().getResolution(); 413 } 414 checkQualityConstantsOrThrow(@onNull List<Quality> qualities)415 private static void checkQualityConstantsOrThrow(@NonNull List<Quality> qualities) { 416 for (Quality quality : qualities) { 417 Preconditions.checkArgument(Quality.containsQuality(quality), 418 "qualities contain invalid quality: " + quality); 419 } 420 } 421 checkQualityConstantsOrThrow(@onNull Quality quality)422 private static void checkQualityConstantsOrThrow(@NonNull Quality quality) { 423 Preconditions.checkArgument(Quality.containsQuality(quality), 424 "Invalid quality: " + quality); 425 } 426 } 427