1 /*
2  * Copyright 2022 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.AspectRatio.RATIO_16_9;
20 import static androidx.camera.core.AspectRatio.RATIO_4_3;
21 import static androidx.camera.core.AspectRatio.RATIO_DEFAULT;
22 
23 import static java.lang.Math.abs;
24 import static java.util.Objects.requireNonNull;
25 
26 import android.util.Range;
27 import android.util.Rational;
28 import android.util.Size;
29 
30 import androidx.camera.core.AspectRatio;
31 import androidx.camera.core.impl.utils.AspectRatioUtil;
32 import androidx.camera.core.internal.utils.SizeUtil;
33 
34 import com.google.auto.value.AutoValue;
35 
36 import org.jspecify.annotations.NonNull;
37 import org.jspecify.annotations.Nullable;
38 
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44 
45 /**
46  * This class saves the mapping from a {@link Quality} + {@code VideoSpec#ASPECT_RATIO_*}
47  * combination to a resolution list.
48  *
49  * <p>The class defines the video height range for each Quality. It classifies the input
50  * resolutions by the Quality ranges and aspect ratios. For example, assume the input resolutions
51  * are [1920x1080, 1440x1080, 1080x1080, 1280x720, 960x720 864x480, 640x480, 640x360],
52  * <pre>{@code
53  * SD-4:3 = [640x480]
54  * SD-16:9 = [640x360, 864x480]
55  * HD-4:3 = [960x720]
56  * HD-16:9 = [1280x720]
57  * FHD-4:3 = [1440x1080]
58  * FHD-16:9 = [1920x1080]
59  * }</pre>
60  * It ignores resolutions not belong to the supported aspect ratios. It sorts each resolution
61  * list based on the smallest area difference to the given video size of CamcorderProfile.
62  * It provides {@link #getResolutions(Quality, int)} API to query the result.
63  */
64 class QualityRatioToResolutionsTable {
65 
66     // Key: Quality
67     // Value: the height range of Quality
68     private static final Map<Quality, Range<Integer>> sQualityRangeMap = new HashMap<>();
69     static {
sQualityRangeMap.put(Quality.UHD, Range.create(2160, 4319))70         sQualityRangeMap.put(Quality.UHD, Range.create(2160, 4319));
sQualityRangeMap.put(Quality.FHD, Range.create(1080, 1439))71         sQualityRangeMap.put(Quality.FHD, Range.create(1080, 1439));
sQualityRangeMap.put(Quality.HD, Range.create(720, 1079))72         sQualityRangeMap.put(Quality.HD, Range.create(720, 1079));
sQualityRangeMap.put(Quality.SD, Range.create(241, 719))73         sQualityRangeMap.put(Quality.SD, Range.create(241, 719));
74     }
75 
76     // Key: aspect ratio constant
77     // Value: aspect ratio rational
78     private static final Map<Integer, Rational> sAspectRatioMap = new HashMap<>();
79     static {
sAspectRatioMap.put(RATIO_4_3, AspectRatioUtil.ASPECT_RATIO_4_3)80         sAspectRatioMap.put(RATIO_4_3, AspectRatioUtil.ASPECT_RATIO_4_3);
sAspectRatioMap.put(RATIO_16_9, AspectRatioUtil.ASPECT_RATIO_16_9)81         sAspectRatioMap.put(RATIO_16_9, AspectRatioUtil.ASPECT_RATIO_16_9);
82     }
83 
84     // Key: QualityRatio (Quality + AspectRatio)
85     // Value: resolutions
86     private final Map<QualityRatio, List<Size>> mTable = new HashMap<>();
87     {
88         for (Quality quality : sQualityRangeMap.keySet()) {
ArrayList()89             mTable.put(QualityRatio.of(quality, RATIO_DEFAULT), new ArrayList<>());
90             for (Integer aspectRatio : sAspectRatioMap.keySet()) {
ArrayList()91                 mTable.put(QualityRatio.of(quality, aspectRatio), new ArrayList<>());
92             }
93         }
94     }
95 
96     /**
97      * Constructs table.
98      *
99      * @param resolutions             the resolutions to be classified.
100      * @param profileQualityToSizeMap the video sizes of CamcorderProfile. It will be used to map
101      *                                [quality + {@link AspectRatio#RATIO_DEFAULT}] to the profile
102      *                                size, and used to sort each Quality-Ratio row by the
103      *                                smallest area difference to the profile size.
104      */
QualityRatioToResolutionsTable(@onNull List<Size> resolutions, @NonNull Map<Quality, Size> profileQualityToSizeMap)105     QualityRatioToResolutionsTable(@NonNull List<Size> resolutions,
106             @NonNull Map<Quality, Size> profileQualityToSizeMap) {
107         addProfileSizesToTable(profileQualityToSizeMap);
108         addResolutionsToTable(resolutions);
109         sortQualityRatioRow(profileQualityToSizeMap);
110     }
111 
112     /**
113      * Gets the resolutions of the mapped Quality + AspectRatio.
114      *
115      * <p>Giving {@link AspectRatio#RATIO_DEFAULT} will return the mapped profile size.
116      */
getResolutions(@onNull Quality quality, @AspectRatio.Ratio int aspectRatio)117     @NonNull List<Size> getResolutions(@NonNull Quality quality,
118             @AspectRatio.Ratio int aspectRatio) {
119         List<Size> qualityRatioRow = getQualityRatioRow(quality, aspectRatio);
120         return qualityRatioRow != null ? new ArrayList<>(qualityRatioRow) : new ArrayList<>(0);
121     }
122 
addProfileSizesToTable(@onNull Map<Quality, Size> profileQualityToSizeMap)123     private void addProfileSizesToTable(@NonNull Map<Quality, Size> profileQualityToSizeMap) {
124         for (Map.Entry<Quality, Size> entry : profileQualityToSizeMap.entrySet()) {
125             requireNonNull(getQualityRatioRow(entry.getKey(), RATIO_DEFAULT)).add(entry.getValue());
126         }
127     }
128 
addResolutionsToTable(@onNull List<Size> resolutions)129     private void addResolutionsToTable(@NonNull List<Size> resolutions) {
130         for (Size resolution : resolutions) {
131             Quality quality = findMappedQuality(resolution);
132             if (quality == null) {
133                 continue;
134             }
135             Integer aspectRatio = findMappedAspectRatio(resolution);
136             if (aspectRatio == null) {
137                 continue;
138             }
139             List<Size> qualityRatioRow = requireNonNull(getQualityRatioRow(quality, aspectRatio));
140             qualityRatioRow.add(resolution);
141         }
142     }
143 
sortQualityRatioRow(@onNull Map<Quality, Size> profileQualityToSizeMap)144     private void sortQualityRatioRow(@NonNull Map<Quality, Size> profileQualityToSizeMap) {
145         for (Map.Entry<QualityRatio, List<Size>> entry : mTable.entrySet()) {
146             Size profileSize = profileQualityToSizeMap.get(entry.getKey().getQuality());
147             if (profileSize == null) {
148                 // Sorting is ignored if the profile doesn't contain the corresponding size.
149                 continue;
150             }
151             // Sort by the smallest area difference from the profile size.
152             int qualitySizeArea = SizeUtil.getArea(profileSize);
153             Collections.sort(entry.getValue(), (s1, s2) -> {
154                 int s1Diff = abs(SizeUtil.getArea(s1) - qualitySizeArea);
155                 int s2Diff = abs(SizeUtil.getArea(s2) - qualitySizeArea);
156                 return s1Diff - s2Diff;
157             });
158         }
159     }
160 
findMappedQuality(@onNull Size resolution)161     private static @Nullable Quality findMappedQuality(@NonNull Size resolution) {
162         for (Map.Entry<Quality, Range<Integer>> entry : sQualityRangeMap.entrySet()) {
163             if (entry.getValue().contains(resolution.getHeight())) {
164                 return entry.getKey();
165             }
166         }
167         return null;
168     }
169 
findMappedAspectRatio(@onNull Size resolution)170     private static @Nullable Integer findMappedAspectRatio(@NonNull Size resolution) {
171         for (Map.Entry<Integer, Rational> entry : sAspectRatioMap.entrySet()) {
172             if (AspectRatioUtil.hasMatchingAspectRatio(resolution, entry.getValue(),
173                     SizeUtil.RESOLUTION_QVGA)) {
174                 return entry.getKey();
175             }
176         }
177         return null;
178     }
179 
getQualityRatioRow(@onNull Quality quality, @AspectRatio.Ratio int aspectRatio)180     private @Nullable List<Size> getQualityRatioRow(@NonNull Quality quality,
181             @AspectRatio.Ratio int aspectRatio) {
182         return mTable.get(QualityRatio.of(quality, aspectRatio));
183     }
184 
185     @AutoValue
186     abstract static class QualityRatio {
187 
of(@onNull Quality quality, @AspectRatio.Ratio int aspectRatio)188         static QualityRatio of(@NonNull Quality quality, @AspectRatio.Ratio int aspectRatio) {
189             return new AutoValue_QualityRatioToResolutionsTable_QualityRatio(quality, aspectRatio);
190         }
191 
getQuality()192         abstract @NonNull Quality getQuality();
193 
194         @SuppressWarnings("unused")
195         @AspectRatio.Ratio
getAspectRatio()196         abstract int getAspectRatio();
197     }
198 }
199