From 2f511a293014f2b5e7c8b14db5dedcbb4f24e3fe Mon Sep 17 00:00:00 2001 From: theodab Date: Thu, 29 Jun 2023 03:16:31 -0700 Subject: [PATCH] feat: Add preferredVideoHdrLevel config. (#5370) 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. --- demo/common/message_ids.js | 6 +++ demo/config.js | 18 +++++++ demo/locales/en.json | 6 +++ demo/locales/source.json | 24 +++++++++ demo/main.js | 5 +- externs/shaka/player.js | 9 ++++ lib/media/adaptation_set_criteria.js | 44 +++++++++++++++- lib/player.js | 8 +-- lib/util/player_configuration.js | 1 + lib/util/stream_utils.js | 4 +- test/media/adaptation_set_criteria_unit.js | 60 +++++++++++++++++----- 11 files changed, 163 insertions(+), 22 deletions(-) diff --git a/demo/common/message_ids.js b/demo/common/message_ids.js index 8b852c9450..d27a87e846 100644 --- a/demo/common/message_ids.js +++ b/demo/common/message_ids.js @@ -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', diff --git a/demo/config.js b/demo/config.js index 21c3328855..58874c17f2 100644 --- a/demo/config.js +++ b/demo/config.js @@ -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, diff --git a/demo/locales/en.json b/demo/locales/en.json index 4057c042b1..533db9599f 100644 --- a/demo/locales/en.json +++ b/demo/locales/en.json @@ -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", diff --git a/demo/locales/source.json b/demo/locales/source.json index 6f9b793133..4653c1ac85 100644 --- a/demo/locales/source.json +++ b/demo/locales/source.json @@ -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" diff --git a/demo/main.js b/demo/main.js index 6db7f232f7..0464052d38 100644 --- a/demo/main.js +++ b/demo/main.js @@ -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); } diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 87ac11d544..ba1a42c555 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -1472,6 +1472,7 @@ shaka.extern.OfflineConfiguration; * preferredVideoCodecs: !Array., * preferredAudioCodecs: !Array., * preferredAudioChannelCount: number, + * preferredVideoHdrLevel: string, * preferredDecodingAttributes: !Array., * preferForcedSubs: boolean, * restrictions: shaka.extern.Restrictions, @@ -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.} preferredDecodingAttributes * The list of preferred attributes of decodingInfo, in the order of their * priorities. diff --git a/lib/media/adaptation_set_criteria.js b/lib/media/adaptation_set_criteria.js index b1fe6a3cfa..9abed02870 100644 --- a/lib/media/adaptation_set_criteria.js +++ b/lib/media/adaptation_set_criteria.js @@ -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 */ @@ -87,9 +88,10 @@ 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} */ @@ -97,6 +99,8 @@ shaka.media.PreferenceBasedCriteria = class { /** @private {number} */ this.channelCount_ = channelCount; /** @private {string} */ + this.hdrLevel_ = hdrLevel; + /** @private {string} */ this.label_ = label; } @@ -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_); @@ -227,4 +242,29 @@ shaka.media.PreferenceBasedCriteria = class { return label1 == label2; }); } + + + /** + * Filters variants according to the given hdr level config. + * + * @param {!Array.} 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; + }); + } }; diff --git a/lib/player.js b/lib/player.js index 5ea855516d..82a09f3b2f 100644 --- a/lib/player.js +++ b/lib/player.js @@ -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; @@ -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; @@ -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) { @@ -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) { diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index a466145487..59a732aaee 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -329,6 +329,7 @@ shaka.util.PlayerConfiguration = class { preferredVariantRole: '', preferredTextRole: '', preferredAudioChannelCount: 2, + preferredVideoHdrLevel: 'AUTO', preferredVideoCodecs: [], preferredAudioCodecs: [], preferForcedSubs: false, diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 3b128f22b5..3c5974e858 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -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, @@ -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( diff --git a/test/media/adaptation_set_criteria_unit.js b/test/media/adaptation_set_criteria_unit.js index 47e5e7a7e5..429e1d0b0a 100644 --- a/test/media/adaptation_set_criteria_unit.js +++ b/test/media/adaptation_set_criteria_unit.js @@ -19,7 +19,7 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0); + const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0, ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -40,7 +40,7 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0); + const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0, ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -71,7 +71,8 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('en', 'main', 0); + const builder = new shaka.media.PreferenceBasedCriteria( + 'en', 'main', 0, ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -120,7 +121,7 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0); + const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0, ''); const set = builder.create(manifest.variants); // Which role is chosen is an implementation detail. @@ -178,7 +179,7 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('zh', '', 0); + const builder = new shaka.media.PreferenceBasedCriteria('zh', '', 0, ''); const set = builder.create(manifest.variants); // Which role is chosen is an implementation detail. @@ -214,7 +215,7 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('zh', '', 0); + const builder = new shaka.media.PreferenceBasedCriteria('zh', '', 0, ''); const set = builder.create(manifest.variants); // Which language is chosen is an implementation detail. @@ -270,7 +271,8 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('zh', '', 0); + const builder = new shaka.media.PreferenceBasedCriteria( + 'zh', '', 0, ''); const set = builder.create(manifest.variants); // Which role is chosen is an implementation detail. Each role is @@ -327,7 +329,8 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('zh', '', 0); + const builder = new shaka.media.PreferenceBasedCriteria( + 'zh', '', 0, ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -336,6 +339,34 @@ describe('AdaptationSetCriteria', () => { ]); }); + it('chooses variants with preferred hdr level', () => { + const manifest = shaka.test.ManifestGenerator.generate((manifest) => { + manifest.addVariant(1, (variant) => { + variant.addVideo(10, (stream) => { + stream.hdr = 'PQ'; + }); + }); + manifest.addVariant(2, (variant) => { + variant.addVideo(20, (stream) => { + stream.hdr = 'SDR'; + }); + }); + manifest.addVariant(3, (variant) => { + variant.addVideo(30, (stream) => { + stream.hdr = 'PQ'; + }); + }); + }); + + const builder = new shaka.media.PreferenceBasedCriteria('', '', 0, 'PQ'); + const set = builder.create(manifest.variants); + + checkSet(set, [ + manifest.variants[0], + manifest.variants[2], + ]); + }); + it('chooses variants with preferred audio channels count', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(1, (variant) => { @@ -355,7 +386,7 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('', '', 2); + const builder = new shaka.media.PreferenceBasedCriteria('', '', 2, ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -384,7 +415,8 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('', '', 6); + const builder = + new shaka.media.PreferenceBasedCriteria('', '', 6, ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -413,7 +445,7 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('', '', 2); + const builder = new shaka.media.PreferenceBasedCriteria('', '', 2, ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -441,8 +473,8 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = - new shaka.media.PreferenceBasedCriteria('', '', 0, 'preferredLabel'); + const builder = new shaka.media.PreferenceBasedCriteria( + '', '', 0, '', 'preferredLabel'); const set = builder.create(manifest.variants); checkSet(set, [ @@ -484,7 +516,7 @@ describe('AdaptationSetCriteria', () => { }); const builder = new shaka.media.PreferenceBasedCriteria( - 'zh', '', 0, 'preferredLabel'); + 'zh', '', 0, '', 'preferredLabel'); const set = builder.create(manifest.variants); checkSet(set, [