From 1e9e6f5938ed40167507720460bef9a51f106ff4 Mon Sep 17 00:00:00 2001 From: Fan Cao Date: Sat, 11 May 2024 16:12:29 +0800 Subject: [PATCH 1/5] feat: add cache for getDecodingInfosForVariants when polyfill used --- externs/shaka/player.js | 7 +- lib/media/drm_engine.js | 3 +- lib/media/manifest_filterer.js | 4 +- lib/util/player_configuration.js | 1 + lib/util/stream_utils.js | 106 +++++++++++++++++++++++++++++-- 5 files changed, 112 insertions(+), 9 deletions(-) diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 1bc7c1994d..1af93bf42d 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -802,7 +802,8 @@ shaka.extern.PersistentSessionMetadata; * keySystemsMapping: !Object., * parseInbandPsshEnabled: boolean, * minHdcpVersion: string, - * ignoreDuplicateInitData: boolean + * ignoreDuplicateInitData: boolean, + * enableMediaCapabilitiesCache: boolean * }} * * @property {shaka.extern.RetryParameters} retryParameters @@ -865,6 +866,10 @@ shaka.extern.PersistentSessionMetadata; * Note: Tizen 2015 and 2016 models will send multiple webkitneedkey events * with the same init data. If the duplicates are supressed, playback * will stall without errors. + * @property {boolean} enableMediaCapabilitiesCache + * Custom optimization, enable aggressive cache on mediaCapabilities call. + * Should only be enabled on platform where polyfill for mediaCapabilities + * is used. * @exportDoc */ shaka.extern.DrmConfiguration; diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 959fbbf6c0..03fff7770a 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -401,7 +401,8 @@ shaka.media.DrmEngine = class { // in the drm infos, and before queryMediaKeys_(). await shaka.util.StreamUtils.getDecodingInfosForVariants(variants, this.usePersistentLicenses_, this.srcEquals_, - this.config_.preferredKeySystems); + this.config_.preferredKeySystems, + this.config_.enableMediaCapabilitiesCache); const hasDrmInfo = hadDrmInfo || Object.keys(this.config_.servers).length; // An unencrypted content is initialized. diff --git a/lib/media/manifest_filterer.js b/lib/media/manifest_filterer.js index 8a0bf915ee..6a11d8b991 100644 --- a/lib/media/manifest_filterer.js +++ b/lib/media/manifest_filterer.js @@ -60,11 +60,11 @@ shaka.media.ManifestFilterer = class { async filterManifestWithStreamUtils_(manifest) { goog.asserts.assert(manifest, 'Manifest should exist!'); await shaka.util.StreamUtils.filterManifest(this.drmEngine_, manifest, - this.config_.drm.preferredKeySystems); + this.config_.drm.preferredKeySystems, + this.config_.drm.enableMediaCapabilitiesCache); this.checkPlayableVariants_(manifest); } - /** * @param {?shaka.extern.Manifest} manifest * @return {boolean} tracksChanged diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index 661feba9a1..a1520753e8 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -92,6 +92,7 @@ shaka.util.PlayerConfiguration = class { parseInbandPsshEnabled: shaka.util.Platform.isXboxOne(), minHdcpVersion: '', ignoreDuplicateInitData: !shaka.util.Platform.isTizen2(), + enableMediaCapabilitiesCache: false, }; // The Xbox One and PS4 only support the Playready DRM, so they should diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 98eaf7845d..bbfa93632c 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -348,11 +348,13 @@ shaka.util.StreamUtils = class { * @param {shaka.media.DrmEngine} drmEngine * @param {shaka.extern.Manifest} manifest * @param {!Array=} preferredKeySystems + * @param {boolean} enableMediaCapabilitiesCache */ - static async filterManifest(drmEngine, manifest, preferredKeySystems = []) { + static async filterManifest(drmEngine, manifest, + preferredKeySystems = [], enableMediaCapabilitiesCache = false) { await shaka.util.StreamUtils.filterManifestByMediaCapabilities( drmEngine, manifest, manifest.offlineSessionIds.length > 0, - preferredKeySystems); + preferredKeySystems, enableMediaCapabilitiesCache); shaka.util.StreamUtils.filterTextStreams_(manifest); await shaka.util.StreamUtils.filterImageStreams_(manifest); } @@ -366,15 +368,17 @@ shaka.util.StreamUtils = class { * @param {shaka.extern.Manifest} manifest * @param {boolean} usePersistentLicenses * @param {!Array} preferredKeySystems + * @param {boolean} enableMediaCapabilitiesCache */ static async filterManifestByMediaCapabilities( - drmEngine, manifest, usePersistentLicenses, preferredKeySystems) { + drmEngine, manifest, usePersistentLicenses, + preferredKeySystems, enableMediaCapabilitiesCache = false) { goog.asserts.assert(navigator.mediaCapabilities, 'MediaCapabilities should be valid.'); await shaka.util.StreamUtils.getDecodingInfosForVariants( manifest.variants, usePersistentLicenses, /* srcEquals= */ false, - preferredKeySystems); + preferredKeySystems, enableMediaCapabilitiesCache); let keySystem = null; if (drmEngine) { @@ -666,10 +670,11 @@ shaka.util.StreamUtils = class { * @param {boolean} usePersistentLicenses * @param {boolean} srcEquals * @param {!Array} preferredKeySystems + * @param {boolean} enableMediaCapabilitiesCache * @exportDoc */ static async getDecodingInfosForVariants(variants, usePersistentLicenses, - srcEquals, preferredKeySystems) { + srcEquals, preferredKeySystems, enableMediaCapabilitiesCache = false) { const gotDecodingInfo = variants.some((variant) => variant.decodingInfos.length); if (gotDecodingInfo) { @@ -710,6 +715,97 @@ shaka.util.StreamUtils = class { } } // for (const preferredKeySystem of preferredKeySystems) + /** + * For LR devices, polyfill is used, which calls isTypeSupported and + * requestMediaKeySystemAccess instead. + * Group similar variants, and share results. + * Helpful for video with a lot of variants on LR. + */ + const enableCahe = enableMediaCapabilitiesCache && + !srcEquals && !shaka.util.Platform.isChromecast(); + if (enableCahe) { + const decodeInfoCache = {}; + const buildCacheKey = (variant) => { + if (variant.audio.fullMimeTypes.size < 1 || + variant.video.fullMimeTypes.size < 1) { + return undefined; + } + // if pure function for calculate the fulltype + const videoDrmInfos = variant.video ? variant.video.drmInfos : []; + const audioDrmInfos = variant.audio ? variant.audio.drmInfos : []; + const allDrmInfos = videoDrmInfos.concat(audioDrmInfos); + + const keySystems= new Set(allDrmInfos.map((info) => info.keySystem)); + // # of cachekey should match the result of getDecodingConfigs_ + const cacheKeys = []; + for (const keySystem of keySystems) { + for (const fullVideoMimeType of variant.video.fullMimeTypes) { + for (const fullAudioMimeType of variant.audio.fullMimeTypes) { + cacheKeys.push( + `${fullVideoMimeType}#${fullAudioMimeType}#${keySystem}`); + } + } + } + return cacheKeys; + }; + + // sort variants, so that high bandwidth checked first. + // High bandwidth tends to be less likely to be supported, or smooth + const sortedVariants = variants.sort((a, b) => b.bandwidth-a.bandwidth); + const keyToCheck = 'supported'; + + for (const variant of sortedVariants) { + const cacheKeys = buildCacheKey(variant); + // If not meet criteria, will continue query. + const useCache = cacheKeys && cacheKeys.length && + cacheKeys.reduce((hasCache, key) => + (hasCache && decodeInfoCache[key] && + // needs to meet criteria. + decodeInfoCache[key][keyToCheck]) + , true); + shaka.log.info('MediaCapabilities.decodingInfo() buildCacheKey.', + useCache, cacheKeys, variant); + if (useCache) { + variant.decodingInfos = + cacheKeys.map((key) => decodeInfoCache[key]); + continue; + } + + /** @type {!Array.>} */ + const decodingConfigs = shaka.util.StreamUtils.getDecodingConfigs_( + variant, usePersistentLicenses, srcEquals) + .filter((configs) => { + // All configs in a batch will have the same keySystem. + const config = configs[0]; + const keySystem = config.keySystemConfiguration && + config.keySystemConfiguration.keySystem; + // Avoid checking preferred systems twice. + return !keySystem || !preferredKeySystems.includes(keySystem); + }); + + // The reason we are performing this await in a loop rather than + // batching into a `promise.all` is performance related. + // https://github.com/shaka-project/shaka-player/pull/4708#discussion_r1022581178 + for (let i=0; i>} */ const decodingConfigs = shaka.util.StreamUtils.getDecodingConfigs_( From 8cf41e956521338ec72e467f7ac6466f50d0bc02 Mon Sep 17 00:00:00 2001 From: Fan Cao Date: Tue, 14 May 2024 15:34:45 +0800 Subject: [PATCH 2/5] chore: update comments --- lib/util/stream_utils.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index bbfa93632c..51e9b97cf6 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -730,19 +730,22 @@ shaka.util.StreamUtils = class { variant.video.fullMimeTypes.size < 1) { return undefined; } - // if pure function for calculate the fulltype - const videoDrmInfos = variant.video ? variant.video.drmInfos : []; - const audioDrmInfos = variant.audio ? variant.audio.drmInfos : []; + const audio = variant.audio; + const video = variant.video; + + // if pure function for calculating the fulltype + const videoDrmInfos = video ? video.drmInfos : []; + const audioDrmInfos = audio ? audio.drmInfos : []; const allDrmInfos = videoDrmInfos.concat(audioDrmInfos); const keySystems= new Set(allDrmInfos.map((info) => info.keySystem)); - // # of cachekey should match the result of getDecodingConfigs_ const cacheKeys = []; for (const keySystem of keySystems) { - for (const fullVideoMimeType of variant.video.fullMimeTypes) { - for (const fullAudioMimeType of variant.audio.fullMimeTypes) { + for (const videoFullMimeType of video.fullMimeTypes) { + for (const audioFullMimeType of audio.fullMimeTypes) { cacheKeys.push( - `${fullVideoMimeType}#${fullAudioMimeType}#${keySystem}`); + `${videoFullMimeType}#${audioFullMimeType}#${keySystem}`); + // video.hdr + audio.spatialAudio + audio.channelsCount } } } @@ -751,19 +754,18 @@ shaka.util.StreamUtils = class { // sort variants, so that high bandwidth checked first. // High bandwidth tends to be less likely to be supported, or smooth - const sortedVariants = variants.sort((a, b) => b.bandwidth-a.bandwidth); + const sortedVariants = variants.sort((a, b) => b.bandwidth - a.bandwidth); const keyToCheck = 'supported'; for (const variant of sortedVariants) { const cacheKeys = buildCacheKey(variant); - // If not meet criteria, will continue query. const useCache = cacheKeys && cacheKeys.length && cacheKeys.reduce((hasCache, key) => (hasCache && decodeInfoCache[key] && - // needs to meet criteria. + // If not meet criteria, won't use cache. decodeInfoCache[key][keyToCheck]) , true); - shaka.log.info('MediaCapabilities.decodingInfo() buildCacheKey.', + shaka.log.info('getDecodingInfosForVariants used cache.', useCache, cacheKeys, variant); if (useCache) { variant.decodingInfos = From 40965f9a02b01bb35100f022a1059017db78609b Mon Sep 17 00:00:00 2001 From: Fan Cao Date: Wed, 15 May 2024 15:36:00 +0800 Subject: [PATCH 3/5] test: add config to demo --- demo/config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/demo/config.js b/demo/config.js index 7cddc6bdac..5ef4fbe10d 100644 --- a/demo/config.js +++ b/demo/config.js @@ -136,7 +136,9 @@ shakaDemo.Config = class { 'drm.parseInbandPsshEnabled') .addTextInput_('Min HDCP version', 'drm.minHdcpVersion') .addBoolInput_('Ignore duplicate init data', - 'drm.ignoreDuplicateInitData'); + 'drm.ignoreDuplicateInitData') + .addBoolInput_('Enable cache for mediacapabilities', + 'drm.enableMediaCapabilitiesCache'); const advanced = shakaDemoMain.getConfiguration().drm.advanced || {}; const addDRMAdvancedField = (name, valueName, suggestions) => { // All advanced fields of a given type are set at once. From 70175519ece9214427ad5393747d500255932b04 Mon Sep 17 00:00:00 2001 From: Fan Cao Date: Fri, 17 May 2024 19:36:32 +0800 Subject: [PATCH 4/5] chore: lint issue --- lib/util/stream_utils.js | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 51e9b97cf6..0c4a3fdfe1 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -721,9 +721,10 @@ shaka.util.StreamUtils = class { * Group similar variants, and share results. * Helpful for video with a lot of variants on LR. */ - const enableCahe = enableMediaCapabilitiesCache && + const enableCache = enableMediaCapabilitiesCache && !srcEquals && !shaka.util.Platform.isChromecast(); - if (enableCahe) { + if (enableCache) { + /** @type {!Object.} */ const decodeInfoCache = {}; const buildCacheKey = (variant) => { if (variant.audio.fullMimeTypes.size < 1 || @@ -738,13 +739,16 @@ shaka.util.StreamUtils = class { const audioDrmInfos = audio ? audio.drmInfos : []; const allDrmInfos = videoDrmInfos.concat(audioDrmInfos); - const keySystems= new Set(allDrmInfos.map((info) => info.keySystem)); + const keySystems = new Set(allDrmInfos.map((info) => info.keySystem)); const cacheKeys = []; for (const keySystem of keySystems) { for (const videoFullMimeType of video.fullMimeTypes) { for (const audioFullMimeType of audio.fullMimeTypes) { cacheKeys.push( - `${videoFullMimeType}#${audioFullMimeType}#${keySystem}`); + `${videoFullMimeType}#${video.hdr} + #${audioFullMimeType}#${audio.spatialAudio} + #${audio.channelsCount} + #${keySystem}`); // video.hdr + audio.spatialAudio + audio.channelsCount } } @@ -760,16 +764,16 @@ shaka.util.StreamUtils = class { for (const variant of sortedVariants) { const cacheKeys = buildCacheKey(variant); const useCache = cacheKeys && cacheKeys.length && - cacheKeys.reduce((hasCache, key) => - (hasCache && decodeInfoCache[key] && - // If not meet criteria, won't use cache. - decodeInfoCache[key][keyToCheck]) - , true); + cacheKeys.reduce((hasCache, key) => + (hasCache && decodeInfoCache[key] && + // If not meet criteria, won't use cache. + decodeInfoCache[key][keyToCheck]) + , true); shaka.log.info('getDecodingInfosForVariants used cache.', useCache, cacheKeys, variant); if (useCache) { variant.decodingInfos = - cacheKeys.map((key) => decodeInfoCache[key]); + cacheKeys.map((key) => decodeInfoCache[key]); continue; } @@ -777,10 +781,10 @@ shaka.util.StreamUtils = class { const decodingConfigs = shaka.util.StreamUtils.getDecodingConfigs_( variant, usePersistentLicenses, srcEquals) .filter((configs) => { - // All configs in a batch will have the same keySystem. + // All configs in a batch will have the same keySystem. const config = configs[0]; const keySystem = config.keySystemConfiguration && - config.keySystemConfiguration.keySystem; + config.keySystemConfiguration.keySystem; // Avoid checking preferred systems twice. return !keySystem || !preferredKeySystems.includes(keySystem); }); @@ -788,14 +792,14 @@ shaka.util.StreamUtils = class { // The reason we are performing this await in a loop rather than // batching into a `promise.all` is performance related. // https://github.com/shaka-project/shaka-player/pull/4708#discussion_r1022581178 - for (let i=0; i Date: Tue, 21 May 2024 13:42:26 +0800 Subject: [PATCH 5/5] refactor: extract duplicate code to getAndApplyDecodingInfosForVariant --- lib/util/stream_utils.js | 108 +++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 60 deletions(-) diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 0c4a3fdfe1..119899b417 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -716,10 +716,11 @@ shaka.util.StreamUtils = class { } // for (const preferredKeySystem of preferredKeySystems) /** - * For LR devices, polyfill is used, which calls isTypeSupported and + * For platforms which use the MediaCapabilities polyfill, calls to media + * capabilities are replaced with isTypeSupported and * requestMediaKeySystemAccess instead. - * Group similar variants, and share results. - * Helpful for video with a lot of variants on LR. + * By grouping similar variants, we can improve performance for manifests + * with lots of variants. */ const enableCache = enableMediaCapabilitiesCache && !srcEquals && !shaka.util.Platform.isChromecast(); @@ -745,11 +746,7 @@ shaka.util.StreamUtils = class { for (const videoFullMimeType of video.fullMimeTypes) { for (const audioFullMimeType of audio.fullMimeTypes) { cacheKeys.push( - `${videoFullMimeType}#${video.hdr} - #${audioFullMimeType}#${audio.spatialAudio} - #${audio.channelsCount} - #${keySystem}`); - // video.hdr + audio.spatialAudio + audio.channelsCount + `${videoFullMimeType}#${audioFullMimeType}#${keySystem}`); } } } @@ -759,52 +756,27 @@ shaka.util.StreamUtils = class { // sort variants, so that high bandwidth checked first. // High bandwidth tends to be less likely to be supported, or smooth const sortedVariants = variants.sort((a, b) => b.bandwidth - a.bandwidth); - const keyToCheck = 'supported'; for (const variant of sortedVariants) { const cacheKeys = buildCacheKey(variant); const useCache = cacheKeys && cacheKeys.length && - cacheKeys.reduce((hasCache, key) => - (hasCache && decodeInfoCache[key] && - // If not meet criteria, won't use cache. - decodeInfoCache[key][keyToCheck]) - , true); - shaka.log.info('getDecodingInfosForVariants used cache.', - useCache, cacheKeys, variant); + cacheKeys.every((key) => { + return decodeInfoCache[key] && decodeInfoCache[key].supported; + }); if (useCache) { variant.decodingInfos = cacheKeys.map((key) => decodeInfoCache[key]); continue; } - /** @type {!Array.>} */ - const decodingConfigs = shaka.util.StreamUtils.getDecodingConfigs_( - variant, usePersistentLicenses, srcEquals) - .filter((configs) => { - // All configs in a batch will have the same keySystem. - const config = configs[0]; - const keySystem = config.keySystemConfiguration && - config.keySystemConfiguration.keySystem; - // Avoid checking preferred systems twice. - return !keySystem || !preferredKeySystems.includes(keySystem); - }); - - // The reason we are performing this await in a loop rather than - // batching into a `promise.all` is performance related. - // https://github.com/shaka-project/shaka-player/pull/4708#discussion_r1022581178 - for (let i = 0; i < decodingConfigs.length; i++) { - const configs = decodingConfigs[i]; - // eslint-disable-next-line no-await-in-loop - await shaka.util.StreamUtils.getDecodingInfosForVariant_( - variant, configs); + // eslint-disable-next-line no-await-in-loop + await shaka.util.StreamUtils.getAndApplyDecodingInfosForVariant_( + variant, usePersistentLicenses, srcEquals, preferredKeySystems); - // getDecodingInfosForVariant_ could fail. - const result = variant.decodingInfos.length ? - variant.decodingInfos[i] : null; - if (cacheKeys && result) { + if (cacheKeys) { + for (let i = 0; i < variant.decodingInfos.length; i++) { const cacheKey = cacheKeys[i]; - - decodeInfoCache[cacheKey] = result; + decodeInfoCache[cacheKey] = variant.decodingInfos[i]; delete decodeInfoCache[cacheKey]['configuration']; } } @@ -813,26 +785,42 @@ shaka.util.StreamUtils = class { } for (const variant of variants) { - /** @type {!Array.>} */ - const decodingConfigs = shaka.util.StreamUtils.getDecodingConfigs_( - variant, usePersistentLicenses, srcEquals) - .filter((configs) => { - // All configs in a batch will have the same keySystem. - const config = configs[0]; - const keySystem = config.keySystemConfiguration && + // eslint-disable-next-line no-await-in-loop + await shaka.util.StreamUtils.getAndApplyDecodingInfosForVariant_( + variant, usePersistentLicenses, srcEquals, preferredKeySystems); + } + } + + /** + * Call mediaCapabilities + * Generate input for querying mediaCapabilities and call the API . + * @param {!shaka.extern.Variant} variant + * @param {boolean} usePersistentLicenses + * @param {boolean} srcEquals + * @param {!Array=} preferredKeySystems + * @private + */ + static async getAndApplyDecodingInfosForVariant_( + variant, usePersistentLicenses, srcEquals, preferredKeySystems = []) { + /** @type {!Array.>} */ + const decodingConfigs = shaka.util.StreamUtils.getDecodingConfigs_( + variant, usePersistentLicenses, srcEquals) + .filter((configs) => { + // All configs in a batch will have the same keySystem. + const config = configs[0]; + const keySystem = config.keySystemConfiguration && config.keySystemConfiguration.keySystem; - // Avoid checking preferred systems twice. - return !keySystem || !preferredKeySystems.includes(keySystem); - }); + // Avoid checking preferred systems twice. + return !keySystem || !preferredKeySystems.includes(keySystem); + }); - // The reason we are performing this await in a loop rather than - // batching into a `promise.all` is performance related. - // https://github.com/shaka-project/shaka-player/pull/4708#discussion_r1022581178 - for (const configs of decodingConfigs) { - // eslint-disable-next-line no-await-in-loop - await shaka.util.StreamUtils.getDecodingInfosForVariant_( - variant, configs); - } + // The reason we are performing this await in a loop rather than + // batching into a `promise.all` is performance related. + // https://github.com/shaka-project/shaka-player/pull/4708#discussion_r1022581178 + for (const configs of decodingConfigs) { + // eslint-disable-next-line no-await-in-loop + await shaka.util.StreamUtils.getDecodingInfosForVariant_( + variant, configs); } }