QualityExploredEncoderProfilesProvider.java
/*
* Copyright 2023 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.internal;
import static androidx.camera.core.internal.utils.SizeUtil.findNearestHigherFor;
import static androidx.camera.video.internal.config.VideoConfigUtil.toVideoEncoderConfig;
import static androidx.camera.video.internal.utils.DynamicRangeUtil.isHdrSettingsMatched;
import static androidx.camera.video.internal.utils.EncoderProfilesUtil.deriveVideoProfile;
import static androidx.core.util.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.arch.core.util.Function;
import androidx.camera.core.DynamicRange;
import androidx.camera.core.impl.EncoderProfilesProvider;
import androidx.camera.core.impl.EncoderProfilesProxy;
import androidx.camera.core.impl.EncoderProfilesProxy.ImmutableEncoderProfilesProxy;
import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
import androidx.camera.core.impl.utils.CompareSizesByArea;
import androidx.camera.video.CapabilitiesByQuality;
import androidx.camera.video.Quality;
import androidx.camera.video.internal.encoder.VideoEncoderConfig;
import androidx.camera.video.internal.encoder.VideoEncoderInfo;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* An implementation that provides the {@link EncoderProfilesProxy} with additional quality added.
*
* <p>The basic EncoderProfilesProvider references to {@link android.media.CamcorderProfile}.
* This class explores more camera and codec supported qualities in addition to CamcorderProfile.
* When a quality is explored, the corresponding profile will be derived from a nearest higher
* supported profile.
*/
@RequiresApi(21)
public class QualityExploredEncoderProfilesProvider implements EncoderProfilesProvider {
private final EncoderProfilesProvider mBaseEncoderProfilesProvider;
private final Set<Quality> mTargetQualities;
private final Set<Size> mCameraSupportedResolutions;
private final Set<DynamicRange> mTargetDynamicRanges;
private final Function<VideoEncoderConfig, VideoEncoderInfo> mVideoEncoderInfoFinder;
private final Map<Integer, EncoderProfilesProxy> mEncoderProfilesCache = new HashMap<>();
private final Map<DynamicRange, CapabilitiesByQuality> mDynamicRangeToCapabilitiesMap =
new HashMap<>();
/**
* Creates a QualityExploredEncoderProfilesProvider.
*
* @param baseProvider the base EncoderProfilesProvider.
* @param targetQualities the target qualities to be explored.
* @param targetDynamicRanges the target dynamic range to explore with. Must be fully
* specified dynamic ranges.
* @param cameraSupportedResolutions the camera supported resolutions.
* @param videoEncoderInfoFinder the VideEncoderInfo finder.
*/
public QualityExploredEncoderProfilesProvider(
@NonNull EncoderProfilesProvider baseProvider,
@NonNull Collection<Quality> targetQualities,
@NonNull Collection<DynamicRange> targetDynamicRanges,
@NonNull Collection<Size> cameraSupportedResolutions,
@NonNull Function<VideoEncoderConfig, VideoEncoderInfo> videoEncoderInfoFinder) {
checkFullySpecifiedOrThrow(targetDynamicRanges);
mBaseEncoderProfilesProvider = baseProvider;
mTargetQualities = new HashSet<>(targetQualities);
mTargetDynamicRanges = new HashSet<>(targetDynamicRanges);
mCameraSupportedResolutions = new HashSet<>(cameraSupportedResolutions);
mVideoEncoderInfoFinder = videoEncoderInfoFinder;
}
@Override
public boolean hasProfile(int quality) {
return getProfilesInternal(quality) != null;
}
@Nullable
@Override
public EncoderProfilesProxy getAll(int quality) {
return getProfilesInternal(quality);
}
@Nullable
private EncoderProfilesProxy getProfilesInternal(int qualityValue) {
if (mEncoderProfilesCache.containsKey(qualityValue)) {
return mEncoderProfilesCache.get(qualityValue);
}
EncoderProfilesProxy profiles = mBaseEncoderProfilesProvider.getAll(qualityValue);
Quality.ConstantQuality quality = findQualityInTargetQualities(qualityValue);
if (quality != null && !hasMatchedVideoProfileForAllTargetDynamicRanges(profiles)) {
profiles = mergeEncoderProfiles(profiles, exploreProfiles(quality));
}
mEncoderProfilesCache.put(qualityValue, profiles);
return profiles;
}
private boolean hasMatchedVideoProfileForAllTargetDynamicRanges(
@Nullable EncoderProfilesProxy encoderProfiles) {
if (encoderProfiles == null) {
return false;
}
// Return true only if the encoderProfiles contains all target DynamicRange.
for (DynamicRange dynamicRange : mTargetDynamicRanges) {
if (!hasMatchedVideoProfileForDynamicRange(encoderProfiles, dynamicRange)) {
return false;
}
}
return true;
}
@Nullable
private EncoderProfilesProxy exploreProfiles(@NonNull Quality.ConstantQuality quality) {
checkArgument(mTargetQualities.contains(quality));
EncoderProfilesProxy qualityMappedProfiles =
mBaseEncoderProfilesProvider.getAll(quality.getValue());
for (Size size : quality.getTypicalSizes()) {
if (!mCameraSupportedResolutions.contains(size)) {
continue;
}
TreeMap<Size, EncoderProfilesProxy> areaSortedSizeToEncoderProfilesMap = new TreeMap<>(
new CompareSizesByArea());
List<VideoProfileProxy> generatedVideoProfiles = new ArrayList<>();
for (DynamicRange dynamicRange : mTargetDynamicRanges) {
if (hasMatchedVideoProfileForDynamicRange(qualityMappedProfiles, dynamicRange)) {
continue;
}
// Find a nearest higher EncoderProfiles by the target dynamic range.
VideoValidatedEncoderProfilesProxy encoderProfiles =
getCapabilitiesByQualityFor(dynamicRange)
.findNearestHigherSupportedEncoderProfilesFor(size);
if (encoderProfiles == null) {
continue;
}
VideoProfileProxy baseVideoProfile = encoderProfiles.getDefaultVideoProfile();
// Find VideoEncoderInfo from VideoProfile.
VideoEncoderConfig encoderConfig = toVideoEncoderConfig(baseVideoProfile);
VideoEncoderInfo encoderInfo = mVideoEncoderInfoFinder.apply(encoderConfig);
// Check if size is valid for the Encoder.
if (encoderInfo == null || !encoderInfo.isSizeSupportedAllowSwapping(
size.getWidth(), size.getHeight())) {
continue;
}
// Add the encoderProfiles to the candidates of base EncoderProfiles.
areaSortedSizeToEncoderProfilesMap.put(
new Size(baseVideoProfile.getWidth(), baseVideoProfile.getHeight()),
encoderProfiles);
// Generate VideoProfile from base VideoProfile and new size.
generatedVideoProfiles.add(
deriveVideoProfile(baseVideoProfile, size,
encoderInfo.getSupportedBitrateRange()));
}
if (!generatedVideoProfiles.isEmpty()) {
// Use the nearest higher EncoderProfiles as base EncoderProfiles.
EncoderProfilesProxy baseProfiles = requireNonNull(
findNearestHigherFor(size, areaSortedSizeToEncoderProfilesMap));
return ImmutableEncoderProfilesProxy.create(
baseProfiles.getDefaultDurationSeconds(),
baseProfiles.getRecommendedFileFormat(),
baseProfiles.getAudioProfiles(),
generatedVideoProfiles);
}
}
return null;
}
@Nullable
private Quality.ConstantQuality findQualityInTargetQualities(int qualityValue) {
for (Quality quality : mTargetQualities) {
Quality.ConstantQuality constantQuality = (Quality.ConstantQuality) quality;
if (constantQuality.getValue() == qualityValue) {
return constantQuality;
}
}
return null;
}
@NonNull
private CapabilitiesByQuality getCapabilitiesByQualityFor(@NonNull DynamicRange dynamicRange) {
if (mDynamicRangeToCapabilitiesMap.containsKey(dynamicRange)) {
return requireNonNull(mDynamicRangeToCapabilitiesMap.get(dynamicRange));
}
EncoderProfilesProvider constrainedProvider =
new DynamicRangeMatchedEncoderProfilesProvider(mBaseEncoderProfilesProvider,
dynamicRange);
CapabilitiesByQuality capabilities = new CapabilitiesByQuality(constrainedProvider);
mDynamicRangeToCapabilitiesMap.put(dynamicRange, capabilities);
return capabilities;
}
@Nullable
private static EncoderProfilesProxy mergeEncoderProfiles(
@Nullable EncoderProfilesProxy baseProfiles,
@Nullable EncoderProfilesProxy profilesToMerge) {
if (baseProfiles == null && profilesToMerge == null) {
return null;
}
int duration = baseProfiles != null ? baseProfiles.getDefaultDurationSeconds() :
profilesToMerge.getDefaultDurationSeconds();
int format = baseProfiles != null ? baseProfiles.getRecommendedFileFormat() :
profilesToMerge.getRecommendedFileFormat();
List<EncoderProfilesProxy.AudioProfileProxy> audioProfiles = baseProfiles != null
? baseProfiles.getAudioProfiles() : profilesToMerge.getAudioProfiles();
List<EncoderProfilesProxy.VideoProfileProxy> videoProfiles = new ArrayList<>();
if (baseProfiles != null) {
videoProfiles.addAll(baseProfiles.getVideoProfiles());
}
if (profilesToMerge != null) {
videoProfiles.addAll(profilesToMerge.getVideoProfiles());
}
return EncoderProfilesProxy.ImmutableEncoderProfilesProxy.create(
duration,
format,
audioProfiles,
videoProfiles
);
}
private static boolean hasMatchedVideoProfileForDynamicRange(
@Nullable EncoderProfilesProxy encoderProfiles,
@NonNull DynamicRange dynamicRange) {
if (encoderProfiles == null) {
return false;
}
for (VideoProfileProxy videoProfile : encoderProfiles.getVideoProfiles()) {
if (isHdrSettingsMatched(videoProfile, dynamicRange)) {
return true;
}
}
return false;
}
private static void checkFullySpecifiedOrThrow(
@NonNull Collection<DynamicRange> dynamicRanges) {
for (DynamicRange dynamicRange : dynamicRanges) {
if (!dynamicRange.isFullySpecified()) {
throw new IllegalArgumentException(
"Contains non-fully specified DynamicRange: " + dynamicRange);
}
}
}
}