Skip to content

Commit

Permalink
feat: Add preferredVideoHdrLevel config. (shaka-project#5370)
Browse files Browse the repository at this point in the history
This configuration value allows the manifest variants to be filtered based on the HDR level of their video stream.
By default this is set to an auto-detect setting, which chooses PQ or SDR based on the device's detected capabilities.
  • Loading branch information
theodab committed Jun 29, 2023
1 parent 7f8e051 commit 2f511a2
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 22 deletions.
6 changes: 6 additions & 0 deletions demo/common/message_ids.js
Expand Up @@ -199,6 +199,12 @@ shakaDemo.MessageIds = {
FORCE_TRANSMUX: 'DEMO_FORCE_TRANSMUX',
FUZZ_FACTOR: 'DEMO_FUZZ_FACTOR',
GAP_DETECTION_THRESHOLD: 'DEMO_GAP_DETECTION_THRESHOLD',
HDR_LEVEL: 'DEMO_HDR_LEVEL',
HDR_LEVEL_AUTO: 'DEMO_HDR_LEVEL_AUTO',
HDR_LEVEL_HLG: 'DEMO_HDR_LEVEL_HLG',
HDR_LEVEL_NONE: 'DEMO_HDR_LEVEL_NONE',
HDR_LEVEL_PQ: 'DEMO_HDR_LEVEL_PQ',
HDR_LEVEL_SDR: 'DEMO_HDR_LEVEL_SDR',
HLS_SEQUENCE_MODE: 'DEMO_HLS_SEQUENCE_MODE',
IGNORE_DASH_EMPTY_ADAPTATION_SET: 'DEMO_IGNORE_DASH_EMPTY_ADAPTATION_SET',
IGNORE_DASH_DRM: 'DEMO_IGNORE_DASH_DRM',
Expand Down
18 changes: 18 additions & 0 deletions demo/config.js
Expand Up @@ -456,6 +456,24 @@ shakaDemo.Config = class {
this.latestInput_.input().checked = true;
}

const hdrLevels = {
'': '',
'AUTO': 'AUTO',
'SDR': 'SDR',
'PQ': 'PQ',
'HLG': 'HLG',
};
const localize = (name) => shakaDemoMain.getLocalizedString(name);
const hdrLevelNames = {
'AUTO': localize(MessageIds.HDR_LEVEL_AUTO),
'SDR': localize(MessageIds.HDR_LEVEL_SDR),
'PQ': localize(MessageIds.HDR_LEVEL_PQ),
'HLG': localize(MessageIds.HDR_LEVEL_HLG),
'': localize(MessageIds.HDR_LEVEL_NONE),
};
this.addSelectInput_(MessageIds.HDR_LEVEL, 'preferredVideoHdrLevel',
hdrLevels, hdrLevelNames);

this.addBoolInput_(MessageIds.START_AT_SEGMENT_BOUNDARY,
'streaming.startAtSegmentBoundary')
.addBoolInput_(MessageIds.IGNORE_TEXT_FAILURES,
Expand Down
6 changes: 6 additions & 0 deletions demo/locales/en.json
Expand Up @@ -92,6 +92,12 @@
"DEMO_GAP_DETECTION_THRESHOLD": "Gap detection threshold",
"DEMO_GPAC": "GPAC",
"DEMO_HEADERS_TAB": "Headers",
"DEMO_HDR_LEVEL": "Preferred HDR Level",
"DEMO_HDR_LEVEL_AUTO": "Auto Detect",
"DEMO_HDR_LEVEL_HLG": "HLG",
"DEMO_HDR_LEVEL_NONE": "No Preference",
"DEMO_HDR_LEVEL_PQ": "PQ",
"DEMO_HDR_LEVEL_SDR": "SDR",
"DEMO_HIGH_DEFINITION": "High definition",
"DEMO_HIGH_DEFINITION_SEARCH": "Filters for assets with at least one high-definition video stream.",
"DEMO_HLS": "HLS",
Expand Down
24 changes: 24 additions & 0 deletions demo/locales/source.json
Expand Up @@ -371,6 +371,30 @@
"description": "The header for a tab within the custom asset creation dialog.",
"message": "Headers"
},
"DEMO_HDR_LEVEL": {
"description": "The name of a configuration value.",
"message": "Preferred HDR Level"
},
"DEMO_HDR_LEVEL_AUTO": {
"description": "The name of a configuration value.",
"message": "Auto Detect"
},
"DEMO_HDR_LEVEL_HLG": {
"description": "The name of a configuration value.",
"message": "[PROPER_NAME:HLG]"
},
"DEMO_HDR_LEVEL_NONE": {
"description": "The name of a configuration value.",
"message": "No Preference"
},
"DEMO_HDR_LEVEL_PQ": {
"description": "The name of a configuration value.",
"message": "[PROPER_NAME:PQ]"
},
"DEMO_HDR_LEVEL_SDR": {
"description": "The name of a configuration value.",
"message": "[PROPER_NAME:SDR]"
},
"DEMO_HIGH_DEFINITION": {
"description": "Text that describes an asset that has a high definition video stream.",
"message": "High definition"
Expand Down
5 changes: 4 additions & 1 deletion demo/main.js
Expand Up @@ -1394,7 +1394,10 @@ shakaDemo.Main = class {
// NaN != NaN, so there has to be a special check for it to prevent
// false positives.
const bothAreNaN = isNaN(currentValue) && isNaN(defaultValue);
if (currentValue != defaultValue && !bothAreNaN) {
// Strings count as NaN too, so check for them specifically.
const bothAreStrings = (typeof currentValue) == 'string' &&
(typeof defaultValue) == 'string';
if (currentValue != defaultValue && (!bothAreNaN || bothAreStrings)) {
// Don't bother saving in the hash unless it's a non-default value.
params.push(hashName + '=' + currentValue);
}
Expand Down
9 changes: 9 additions & 0 deletions externs/shaka/player.js
Expand Up @@ -1472,6 +1472,7 @@ shaka.extern.OfflineConfiguration;
* preferredVideoCodecs: !Array.<string>,
* preferredAudioCodecs: !Array.<string>,
* preferredAudioChannelCount: number,
* preferredVideoHdrLevel: string,
* preferredDecodingAttributes: !Array.<string>,
* preferForcedSubs: boolean,
* restrictions: shaka.extern.Restrictions,
Expand Down Expand Up @@ -1524,6 +1525,14 @@ shaka.extern.OfflineConfiguration;
* The list of preferred audio codecs, in order of highest to lowest priority.
* @property {number} preferredAudioChannelCount
* The preferred number of audio channels.
* @property {string} preferredVideoHdrLevel
* The preferred HDR level of the video. If possible, this will cause the
* player to filter to assets that either have that HDR level, or no HDR level
* at all.
* Can be 'SDR', 'PQ', 'HLG', 'AUTO' for auto-detect, or '' for no preference.
* Defaults to 'AUTO'.
* Note that one some platforms, such as Chrome, attempting to play PQ content
* may cause problems.
* @property {!Array.<string>} preferredDecodingAttributes
* The list of preferred attributes of decodingInfo, in the order of their
* priorities.
Expand Down
44 changes: 42 additions & 2 deletions lib/media/adaptation_set_criteria.js
Expand Up @@ -49,13 +49,14 @@ shaka.media.ExampleBasedCriteria = class {
// 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, label);
example.language, role, channelCount, hdrLevel, label);
}

/** @override */
Expand Down Expand Up @@ -87,16 +88,19 @@ shaka.media.PreferenceBasedCriteria = class {
* @param {string} language
* @param {string} role
* @param {number} channelCount
* @param {string} hdrLevel
* @param {string=} label
*/
constructor(language, role, channelCount, label = '') {
constructor(language, role, channelCount, hdrLevel, label = '') {
/** @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;
}

Expand Down Expand Up @@ -127,6 +131,17 @@ shaka.media.PreferenceBasedCriteria = class {
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_);
Expand Down Expand Up @@ -227,4 +242,29 @@ shaka.media.PreferenceBasedCriteria = class {
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;
});
}
};
8 changes: 5 additions & 3 deletions lib/player.js
Expand Up @@ -588,7 +588,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
new shaka.media.PreferenceBasedCriteria(
this.config_.preferredAudioLanguage,
this.config_.preferredVariantRole,
this.config_.preferredAudioChannelCount);
this.config_.preferredAudioChannelCount,
this.config_.preferredVideoHdrLevel);

/** @private {string} */
this.currentTextLanguage_ = this.config_.preferredTextLanguage;
Expand Down Expand Up @@ -2108,6 +2109,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
this.config_.preferredAudioLanguage,
this.config_.preferredVariantRole,
this.config_.preferredAudioChannelCount,
this.config_.preferredVideoHdrLevel,
this.config_.preferredAudioLabel);

this.currentTextLanguage_ = this.config_.preferredTextLanguage;
Expand Down Expand Up @@ -4197,7 +4199,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
if (this.manifest_ && this.playhead_) {
this.currentAdaptationSetCriteria_ =
new shaka.media.PreferenceBasedCriteria(language, role || '',
channelsCount, /* label= */ '');
channelsCount, /* hdrLevel= */ '', /* label= */ '');

const diff = (a, b) => {
if (!a.video && !b.video) {
Expand Down Expand Up @@ -4344,7 +4346,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
// label have the same language.
this.currentAdaptationSetCriteria_ =
new shaka.media.PreferenceBasedCriteria(
firstVariantWithLabel.language, '', 0, label);
firstVariantWithLabel.language, '', 0, '', label);

this.chooseVariantAndSwitch_(clearBuffer, safeMargin);
} else if (this.video_ && this.video_.audioTracks) {
Expand Down
1 change: 1 addition & 0 deletions lib/util/player_configuration.js
Expand Up @@ -329,6 +329,7 @@ shaka.util.PlayerConfiguration = class {
preferredVariantRole: '',
preferredTextRole: '',
preferredAudioChannelCount: 2,
preferredVideoHdrLevel: 'AUTO',
preferredVideoCodecs: [],
preferredAudioCodecs: [],
preferForcedSubs: false,
Expand Down
4 changes: 2 additions & 2 deletions lib/util/stream_utils.js
Expand Up @@ -40,6 +40,7 @@ shaka.util.StreamUtils = class {
const StreamUtils = shaka.util.StreamUtils;

let variants = manifest.variants;

// To start, choose the codecs based on configured preferences if available.
if (preferredVideoCodecs.length || preferredAudioCodecs.length) {
variants = StreamUtils.choosePreferredCodecs(variants,
Expand Down Expand Up @@ -404,8 +405,7 @@ shaka.util.StreamUtils = class {
* @param {?shaka.extern.Variant} currentVariant
* @param {shaka.extern.Manifest} manifest
*/
static async filterManifest(
drmEngine, currentVariant, manifest) {
static async filterManifest(drmEngine, currentVariant, manifest) {
await shaka.util.StreamUtils.filterManifestByMediaCapabilities(manifest,
manifest.offlineSessionIds.length > 0);
shaka.util.StreamUtils.filterManifestByCurrentVariant(
Expand Down

0 comments on commit 2f511a2

Please sign in to comment.