blob: ea3806669983205e0c497f8860840e8ba4081c6e [file] [log] [blame]
/*
* Copyright 2022 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 static androidx.camera.core.AspectRatio.RATIO_16_9;
import static androidx.camera.core.AspectRatio.RATIO_4_3;
import static androidx.camera.core.AspectRatio.RATIO_DEFAULT;
import static java.lang.Math.abs;
import static java.util.Objects.requireNonNull;
import android.util.Range;
import android.util.Rational;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.impl.utils.AspectRatioUtil;
import androidx.camera.core.internal.utils.SizeUtil;
import com.google.auto.value.AutoValue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class saves the mapping from a {@link Quality} + {@code VideoSpec#ASPECT_RATIO_*}
* combination to a resolution list.
*
* <p>The class defines the video height range for each Quality. It classifies the input
* resolutions by the Quality ranges and aspect ratios. For example, assume the input resolutions
* are [1920x1080, 1440x1080, 1080x1080, 1280x720, 960x720 864x480, 640x480, 640x360],
* <pre>{@code
* SD-4:3 = [640x480]
* SD-16:9 = [640x360, 864x480]
* HD-4:3 = [960x720]
* HD-16:9 = [1280x720]
* FHD-4:3 = [1440x1080]
* FHD-16:9 = [1920x1080]
* }</pre>
* It ignores resolutions not belong to the supported aspect ratios. It sorts each resolution
* list based on the smallest area difference to the given video size of CamcorderProfile.
* It provides {@link #getResolutions(Quality, int)} API to query the result.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
class QualityRatioToResolutionsTable {
// Key: Quality
// Value: the height range of Quality
private static final Map<Quality, Range<Integer>> sQualityRangeMap = new HashMap<>();
static {
sQualityRangeMap.put(Quality.UHD, Range.create(2160, 4319));
sQualityRangeMap.put(Quality.FHD, Range.create(1080, 1439));
sQualityRangeMap.put(Quality.HD, Range.create(720, 1079));
sQualityRangeMap.put(Quality.SD, Range.create(241, 719));
}
// Key: aspect ratio constant
// Value: aspect ratio rational
private static final Map<Integer, Rational> sAspectRatioMap = new HashMap<>();
static {
sAspectRatioMap.put(RATIO_4_3, AspectRatioUtil.ASPECT_RATIO_4_3);
sAspectRatioMap.put(RATIO_16_9, AspectRatioUtil.ASPECT_RATIO_16_9);
}
// Key: QualityRatio (Quality + AspectRatio)
// Value: resolutions
private final Map<QualityRatio, List<Size>> mTable = new HashMap<>();
{
for (Quality quality : sQualityRangeMap.keySet()) {
mTable.put(QualityRatio.of(quality, RATIO_DEFAULT), new ArrayList<>());
for (Integer aspectRatio : sAspectRatioMap.keySet()) {
mTable.put(QualityRatio.of(quality, aspectRatio), new ArrayList<>());
}
}
}
/**
* Constructs table.
*
* @param resolutions the resolutions to be classified.
* @param profileQualityToSizeMap the video sizes of CamcorderProfile. It will be used to map
* [quality + {@link AspectRatio#RATIO_DEFAULT}] to the profile
* size, and used to sort each Quality-Ratio row by the
* smallest area difference to the profile size.
*/
QualityRatioToResolutionsTable(@NonNull List<Size> resolutions,
@NonNull Map<Quality, Size> profileQualityToSizeMap) {
addProfileSizesToTable(profileQualityToSizeMap);
addResolutionsToTable(resolutions);
sortQualityRatioRow(profileQualityToSizeMap);
}
/**
* Gets the resolutions of the mapped Quality + AspectRatio.
*
* <p>Giving {@link AspectRatio#RATIO_DEFAULT} will return the mapped profile size.
*/
@NonNull
List<Size> getResolutions(@NonNull Quality quality, @AspectRatio.Ratio int aspectRatio) {
List<Size> qualityRatioRow = getQualityRatioRow(quality, aspectRatio);
return qualityRatioRow != null ? new ArrayList<>(qualityRatioRow) : new ArrayList<>(0);
}
private void addProfileSizesToTable(@NonNull Map<Quality, Size> profileQualityToSizeMap) {
for (Map.Entry<Quality, Size> entry : profileQualityToSizeMap.entrySet()) {
requireNonNull(getQualityRatioRow(entry.getKey(), RATIO_DEFAULT)).add(entry.getValue());
}
}
private void addResolutionsToTable(@NonNull List<Size> resolutions) {
for (Size resolution : resolutions) {
Quality quality = findMappedQuality(resolution);
if (quality == null) {
continue;
}
Integer aspectRatio = findMappedAspectRatio(resolution);
if (aspectRatio == null) {
continue;
}
List<Size> qualityRatioRow = requireNonNull(getQualityRatioRow(quality, aspectRatio));
qualityRatioRow.add(resolution);
}
}
private void sortQualityRatioRow(@NonNull Map<Quality, Size> profileQualityToSizeMap) {
for (Map.Entry<QualityRatio, List<Size>> entry : mTable.entrySet()) {
Size profileSize = profileQualityToSizeMap.get(entry.getKey().getQuality());
if (profileSize == null) {
// Sorting is ignored if the profile doesn't contain the corresponding size.
continue;
}
// Sort by the smallest area difference from the profile size.
int qualitySizeArea = SizeUtil.getArea(profileSize);
Collections.sort(entry.getValue(), (s1, s2) -> {
int s1Diff = abs(SizeUtil.getArea(s1) - qualitySizeArea);
int s2Diff = abs(SizeUtil.getArea(s2) - qualitySizeArea);
return s1Diff - s2Diff;
});
}
}
@Nullable
private static Quality findMappedQuality(@NonNull Size resolution) {
for (Map.Entry<Quality, Range<Integer>> entry : sQualityRangeMap.entrySet()) {
if (entry.getValue().contains(resolution.getHeight())) {
return entry.getKey();
}
}
return null;
}
@Nullable
private static Integer findMappedAspectRatio(@NonNull Size resolution) {
for (Map.Entry<Integer, Rational> entry : sAspectRatioMap.entrySet()) {
if (AspectRatioUtil.hasMatchingAspectRatio(resolution, entry.getValue(),
SizeUtil.RESOLUTION_QVGA)) {
return entry.getKey();
}
}
return null;
}
@Nullable
private List<Size> getQualityRatioRow(@NonNull Quality quality,
@AspectRatio.Ratio int aspectRatio) {
return mTable.get(QualityRatio.of(quality, aspectRatio));
}
@AutoValue
abstract static class QualityRatio {
static QualityRatio of(@NonNull Quality quality, @AspectRatio.Ratio int aspectRatio) {
return new AutoValue_QualityRatioToResolutionsTable_QualityRatio(quality, aspectRatio);
}
@NonNull
abstract Quality getQuality();
@SuppressWarnings("unused")
@AspectRatio.Ratio
abstract int getAspectRatio();
}
}