/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.media.AdaptationSetCriteria');
goog.provide('shaka.media.ExampleBasedCriteria');
goog.provide('shaka.media.PreferenceBasedCriteria');
goog.require('shaka.config.CodecSwitchingStrategy');
goog.require('shaka.log');
goog.require('shaka.media.AdaptationSet');
goog.require('shaka.media.Capabilities');
goog.require('shaka.util.LanguageUtils');
goog.require('shaka.util.StreamUtils');
/**
* An adaptation set criteria is a unit of logic that can take a set of
* variants and return a subset of variants that should (and can) be
* adapted between.
*
* @interface
*/
shaka.media.AdaptationSetCriteria = class {
/**
* Take a set of variants, and return a subset of variants that can be
* adapted between.
*
* @param {!Array.<shaka.extern.Variant>} variants
* @return {!shaka.media.AdaptationSet}
*/
create(variants) {}
};
/**
* @implements {shaka.media.AdaptationSetCriteria}
* @final
*/
shaka.media.ExampleBasedCriteria = class {
/**
* @param {shaka.extern.Variant} example
* @param {shaka.config.CodecSwitchingStrategy=} codecSwitchingStrategy
*/
constructor(example,
codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.RELOAD) {
/** @private {shaka.extern.Variant} */
this.example_ = example;
/** @private {shaka.config.CodecSwitchingStrategy} */
this.codecSwitchingStrategy_ = codecSwitchingStrategy;
// We can't know if role and label are really important, so we don't use
// role and label for this.
const role = '';
const label = '';
const hdrLevel = '';
const channelCount = example.audio && example.audio.channelsCount ?
example.audio.channelsCount :
0;
/** @private {!shaka.media.AdaptationSetCriteria} */
this.fallback_ = new shaka.media.PreferenceBasedCriteria(
example.language, role, channelCount, hdrLevel, label,
codecSwitchingStrategy);
}
/** @override */
create(variants) {
const supportsSmoothCodecTransitions = this.codecSwitchingStrategy_ ==
shaka.config.CodecSwitchingStrategy.SMOOTH &&
shaka.media.Capabilities.isChangeTypeSupported();
// We can't assume that the example is in |variants| because it could
// actually be from another period.
const shortList = variants.filter((variant) => {
return shaka.media.AdaptationSet.areAdaptable(this.example_, variant,
!supportsSmoothCodecTransitions);
});
if (shortList.length) {
// Use the first item in the short list as the root. It should not matter
// which element we use as all items in the short list should already be
// compatible.
return new shaka.media.AdaptationSet(shortList[0], shortList);
} else {
return this.fallback_.create(variants);
}
}
};
/**
* @implements {shaka.media.AdaptationSetCriteria}
* @final
*/
shaka.media.PreferenceBasedCriteria = class {
/**
* @param {string} language
* @param {string} role
* @param {number} channelCount
* @param {string} hdrLevel
* @param {string=} label
* @param {shaka.config.CodecSwitchingStrategy=} codecSwitchingStrategy
*/
constructor(language, role, channelCount, hdrLevel, label = '',
codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.RELOAD) {
/** @private {string} */
this.language_ = language;
/** @private {string} */
this.role_ = role;
/** @private {number} */
this.channelCount_ = channelCount;
/** @private {string} */
this.hdrLevel_ = hdrLevel;
/** @private {string} */
this.label_ = label;
/** @private {shaka.config.CodecSwitchingStrategy} */
this.codecSwitchingStrategy_ = codecSwitchingStrategy;
}
/** @override */
create(variants) {
const Class = shaka.media.PreferenceBasedCriteria;
const StreamUtils = shaka.util.StreamUtils;
let current = [];
const byLanguage = Class.filterByLanguage_(variants, this.language_);
const byPrimary = variants.filter((variant) => variant.primary);
if (byLanguage.length) {
current = byLanguage;
} else if (byPrimary.length) {
current = byPrimary;
} else {
current = variants;
}
// Now refine the choice based on role preference. Even the empty string
// works here, and will match variants without any roles.
const byRole = Class.filterVariantsByRole_(current, this.role_);
if (byRole.length) {
current = byRole;
} else {
shaka.log.warning('No exact match for variant role could be found.');
}
if (this.hdrLevel_) {
const byHdrLevel = Class.filterVariantsByHDRLevel_(
current, this.hdrLevel_);
if (byHdrLevel.length) {
current = byHdrLevel;
} else {
shaka.log.warning(
'No exact match for the hdr level could be found.');
}
}
if (this.channelCount_) {
const byChannel = StreamUtils.filterVariantsByAudioChannelCount(
current, this.channelCount_);
if (byChannel.length) {
current = byChannel;
} else {
shaka.log.warning(
'No exact match for the channel count could be found.');
}
}
if (this.label_) {
const byLabel = Class.filterVariantsByLabel_(current, this.label_);
if (byLabel.length) {
current = byLabel;
} else {
shaka.log.warning('No exact match for variant label could be found.');
}
}
const supportsSmoothCodecTransitions = this.codecSwitchingStrategy_ ==
shaka.config.CodecSwitchingStrategy.SMOOTH &&
shaka.media.Capabilities.isChangeTypeSupported();
// Make sure we only return a valid adaptation set.
const set = new shaka.media.AdaptationSet(current[0]);
for (const variant of current) {
// `add` checks combatability by calling `canInclude` internally.
set.add(variant, !supportsSmoothCodecTransitions);
}
return set;
}
/**
* @param {!Array.<shaka.extern.Variant>} variants
* @param {string} preferredLanguage
* @return {!Array.<shaka.extern.Variant>}
* @private
*/
static filterByLanguage_(variants, preferredLanguage) {
const LanguageUtils = shaka.util.LanguageUtils;
/** @type {string} */
const preferredLocale = LanguageUtils.normalize(preferredLanguage);
/** @type {?string} */
const closestLocale = LanguageUtils.findClosestLocale(
preferredLocale,
variants.map((variant) => LanguageUtils.getLocaleForVariant(variant)));
// There were no locales close to what we preferred.
if (!closestLocale) {
return [];
}
// Find the variants that use the closest variant.
return variants.filter((variant) => {
return closestLocale == LanguageUtils.getLocaleForVariant(variant);
});
}
/**
* Filter Variants by role.
*
* @param {!Array.<shaka.extern.Variant>} variants
* @param {string} preferredRole
* @return {!Array.<shaka.extern.Variant>}
* @private
*/
static filterVariantsByRole_(variants, preferredRole) {
return variants.filter((variant) => {
if (!variant.audio) {
return false;
}
if (preferredRole) {
return variant.audio.roles.includes(preferredRole);
} else {
return variant.audio.roles.length == 0;
}
});
}
/**
* Filter Variants by label.
*
* @param {!Array.<shaka.extern.Variant>} variants
* @param {string} preferredLabel
* @return {!Array.<shaka.extern.Variant>}
* @private
*/
static filterVariantsByLabel_(variants, preferredLabel) {
return variants.filter((variant) => {
if (!variant.audio || !variant.audio.label) {
return false;
}
const label1 = variant.audio.label.toLowerCase();
const label2 = preferredLabel.toLowerCase();
return label1 == label2;
});
}
/**
* Filters variants according to the given hdr level config.
*
* @param {!Array.<shaka.extern.Variant>} variants
* @param {string} hdrLevel
* @private
*/
static filterVariantsByHDRLevel_(variants, hdrLevel) {
if (hdrLevel == 'AUTO') {
// Auto detect the ideal HDR level.
if (window.matchMedia('(color-gamut: p3)').matches) {
hdrLevel = 'PQ';
} else {
hdrLevel = 'SDR';
}
}
return variants.filter((variant) => {
if (variant.video && variant.video.hdr && variant.video.hdr != hdrLevel) {
return false;
}
return true;
});
}
};