blob: c5c1f2d6b72367bb3b9805cf125e1cd7e0c8a19e [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/formats/hls/rendition_selector.h"
#include <string>
#include "base/ranges/algorithm.h"
#include "media/formats/hls/audio_rendition.h"
#include "media/formats/hls/multivariant_playlist.h"
#include "media/formats/hls/types.h"
#include "media/formats/hls/variant_stream.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace media::hls {
namespace {
RenditionSelector::CodecSupportType VariantTypeSupported(
RenditionSelector::IsTypeSupportedCallback is_type_supported_cb,
const VariantStream& variant) {
// Check if the codecs reported by this variant can be played at all. If
// this variant does not report its codecs, we'll assume its supported until
// proven otherwise.
auto codecs = variant.GetCodecs();
if (!codecs) {
return RenditionSelector::CodecSupportType::kSupportedAudioVideo;
}
RenditionSelector::CodecSupportType mp4 =
is_type_supported_cb.Run("video/mp4", *codecs);
if (mp4 != RenditionSelector::CodecSupportType::kUnsupported) {
return mp4;
}
RenditionSelector::CodecSupportType mp2t =
is_type_supported_cb.Run("video/mp2t", *codecs);
return mp2t;
}
// |OptimalFilter| will apply an |IsAcceptable (T)->Bool| filter function over
// a set of |options| and filter them based on on the result. However, in the
// case where nothing in |options| is acceptable, it will use a
// |ComputeFallbackCandidate (T, T) -> T| function to ensure that the resulting
// list always has at least one item in it.
template <typename IsAcceptable, typename ComputeFallbackCandidate>
std::vector<const VariantStream*> OptimalFilter(
const std::vector<const VariantStream*>& options,
IsAcceptable filter_functor,
ComputeFallbackCandidate compare_functor) {
std::vector<const VariantStream*> acceptable;
const VariantStream* best_fallback_candidate = nullptr;
for (const VariantStream* test : options) {
if (filter_functor(test)) {
acceptable.push_back(test);
}
if (acceptable.empty()) {
if (best_fallback_candidate == nullptr) {
best_fallback_candidate = test;
} else {
best_fallback_candidate =
compare_functor(best_fallback_candidate, test);
}
}
}
if (acceptable.empty() && best_fallback_candidate != nullptr) {
acceptable.push_back(best_fallback_candidate);
}
return acceptable;
}
// Use the |OptimalFilter| function above to find all Variants under a max bit
// rate, or, if everything is over the max bitrate, find the lowest one.
std::vector<const VariantStream*> GetPreferredVariantsByBitrate(
absl::optional<types::DecimalInteger> max_bitrate,
std::vector<const VariantStream*> inputs) {
if (!max_bitrate.has_value()) {
return inputs;
}
// TODO: Prefer to use average bandwidth, if the streams have that calculated.
types::DecimalInteger bitrate = *max_bitrate;
return OptimalFilter(
inputs,
/*filter_functor=*/
[bitrate](const VariantStream* test) {
return test->GetBandwidth() <= bitrate;
},
/*compare_functor=*/
[](const VariantStream* A, const VariantStream* B) {
return A->GetBandwidth() > B->GetBandwidth() ? B : A;
});
}
} // namespace
RenditionSelector::RenditionSelector(
scoped_refptr<MultivariantPlaylist> playlist,
IsTypeSupportedCallback is_type_supported_cb)
: playlist_(std::move(playlist)) {
DCHECK(playlist_);
DCHECK(is_type_supported_cb);
for (const VariantStream& variant : playlist_->GetVariants()) {
bool has_video = false;
bool has_audio = false;
switch (VariantTypeSupported(is_type_supported_cb, variant)) {
case CodecSupportType::kUnsupported:
break;
case CodecSupportType::kSupportedAudioVideo:
has_audio = true;
has_video = true;
break;
case CodecSupportType::kSupportedAudioOnly:
has_audio = true;
break;
case CodecSupportType::kSupportedVideoOnly:
has_video = true;
break;
}
// Don't consider unsupported types!
if (has_audio || has_video) {
if (variant.GetAudioRenditionGroup()) {
has_audio = true;
}
if (variant.GetVideoRenditionGroupName().has_value()) {
has_video = true;
}
if (has_video) {
video_variants_.push_back(&variant);
}
if (has_audio) {
audio_variants_.push_back(&variant);
}
}
}
// Sort variants by score and bandwidth (descending)
constexpr auto compare = [](const VariantStream* lhs,
const VariantStream* rhs) {
// First compare by |BANDWIDTH|
if (lhs->GetBandwidth() != rhs->GetBandwidth()) {
return lhs->GetBandwidth() > rhs->GetBandwidth();
}
// Then compare by SCORE
if (lhs->GetScore().has_value() && rhs->GetScore().has_value()) {
return *lhs->GetScore() > *rhs->GetScore();
}
// If |lhs| has a score, but |rhs| doesn't, then lhs should be preferred.
return lhs->GetScore().has_value();
};
base::ranges::sort(video_variants_, compare);
base::ranges::sort(audio_variants_, compare);
}
RenditionSelector::RenditionSelector(const RenditionSelector&) = default;
RenditionSelector::RenditionSelector(RenditionSelector&&) = default;
RenditionSelector& RenditionSelector::operator=(const RenditionSelector&) =
default;
RenditionSelector& RenditionSelector::operator=(RenditionSelector&&) = default;
RenditionSelector::~RenditionSelector() = default;
RenditionSelector::PreferredVariants RenditionSelector::GetPreferredVariants(
VideoPlaybackPreferences video_preferences,
AudioPlaybackPreferences audio_preferences) const {
// Start by selecting the preferred video variant, which might also be an
// audio variant. If it's not an audio variant, we have to select the audio
// variant after. Get the acceptable bitrate variants first, then filter by
// resolution. From there, we select the highest bitrate one, which should be
// first, as they are sorted descending by bitrate.
std::vector<const VariantStream*> acceptable_bitrates =
GetPreferredVariantsByBitrate(video_preferences.max_smooth_bitrate,
video_variants_);
// TODO: prefer variants which have the same aspect ratio, even if they are
// a bit larger, as that will probably look better.
std::vector<const VariantStream*> acceptable_resolutions;
if (video_preferences.video_player_resolution.has_value()) {
gfx::Size resolution = *video_preferences.video_player_resolution;
acceptable_resolutions = OptimalFilter(
acceptable_bitrates,
/*filter_functor=*/
[resolution](const VariantStream* test) {
auto stream_res = test->GetResolution();
if (!stream_res.has_value()) {
return true;
}
if (stream_res->Area() > resolution.Area64()) {
return false;
}
return true;
},
/*compare_functor=*/
[](const VariantStream* A, const VariantStream* B) {
// If either A or B didn't have a resolution, then there is at least
// A or B selected already, and this compare_function shouldn't be
// getting called.
DCHECK(A->GetResolution().has_value());
DCHECK(B->GetResolution().has_value());
// If we have to select something and all the resolutions are too
// large, select the smallest resolution.
return A->GetResolution()->Area() > B->GetResolution()->Area() ? B
: A;
});
} else {
acceptable_resolutions = acceptable_bitrates;
}
PreferredVariants result = {nullptr, nullptr};
// This could be audio only, so it's not quite an error yet. Lets grab the
// optimal audio variant, and try that. If that's also null, then we have no
// good variant to play, and we should return an error.
if (acceptable_resolutions.empty()) {
DCHECK(video_variants_.empty());
auto acceptable_audio = GetPreferredVariantsByBitrate(
video_preferences.max_smooth_bitrate, audio_variants_);
if (acceptable_audio.empty()) {
return result;
}
result.selected_variant = acceptable_audio[0];
result.audio_override_rendition =
TryFindAudioOverride(audio_preferences, result.selected_variant);
// If our selected variant is audio, but we got a rendition that overrides
// this, then this selected variant won't actually be selected, since it
// would be overridden by the audio.
if (result.audio_override_rendition != nullptr) {
result.audio_override_variant = result.selected_variant;
result.selected_variant = nullptr;
}
return result;
}
// For now, since we only select a potential override from the selected
// variant, the audio override variant is always the same.
result.selected_variant = acceptable_resolutions[0];
result.audio_override_variant = acceptable_resolutions[0];
// If our selected variant is an audio/video stream, we should use its audio
// group to determine what rendition to pick as an override, if it has an
// audio group. If we don't suspect it of being an audio stream, then we have
// to decide what to do - most implementations just end up with silent video.
// Should we consider finding the best audio variant as well?
result.audio_override_rendition =
TryFindAudioOverride(audio_preferences, result.selected_variant);
return result;
}
const AudioRendition* RenditionSelector::TryFindAudioOverride(
AudioPlaybackPreferences audio_preferences,
const VariantStream* variant) const {
// Note that we're not considering channel count, which should probably be
// changed at some point.
CHECK(variant);
// There are no audio renditions related to this variant anyway.
if (!variant->GetAudioRenditionGroup()) {
return nullptr;
}
// If there is no preference for language, then we can just use the default.
if (!audio_preferences.language.has_value()) {
audio_preferences.language = default_audio_language_;
}
// If we have a preference, check all the renditions that are connected to our
// variant.
if (audio_preferences.language.has_value()) {
const auto& renditions = variant->GetAudioRenditionGroup()->GetRenditions();
for (const auto& rendition : renditions) {
if (rendition.GetAssociatedLanguage() == *audio_preferences.language) {
return &rendition;
}
}
}
// Otherwise, we should just select the one that is marked as DEFAULT (which
// implies that AUTOSELECT is also true)
if (variant->GetAudioRenditionGroup()->GetDefaultRendition()) {
return variant->GetAudioRenditionGroup()->GetDefaultRendition();
}
// If there is no default, and we have no preference, then just grab the
// first thing that was present in the manifest.
const auto& renditions = variant->GetAudioRenditionGroup()->GetRenditions();
if (renditions.size()) {
return &renditions.front();
}
// nothing to select!
return nullptr;
}
} // namespace media::hls