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