diff --git a/src/playlist-controller.js b/src/playlist-controller.js index 62cf366c1..4af9996e0 100644 --- a/src/playlist-controller.js +++ b/src/playlist-controller.js @@ -2390,11 +2390,13 @@ export class PlaylistController extends videojs.EventTarget { * has no keyId leave it enabled by default. */ excludeNonUsablePlaylistsByKeyId_() { - if (!this.mainPlaylistLoader_ || !this.mainPlaylistLoader_.main) { return; } + let nonUsableKeyStatusCount = 0; + const NON_USABLE = 'non-usable'; + this.mainPlaylistLoader_.main.playlists.forEach((playlist) => { const keyIdSet = this.mainPlaylistLoader_.getKeyIdSet(playlist); @@ -2404,14 +2406,18 @@ export class PlaylistController extends videojs.EventTarget { } keyIdSet.forEach((key) => { const USABLE = 'usable'; - const NON_USABLE = 'non-usable'; const hasUsableKeyStatus = this.keyStatusMap_.has(key) && this.keyStatusMap_.get(key) === USABLE; const nonUsableExclusion = playlist.lastExcludeReason_ === NON_USABLE && playlist.excludeUntil === Infinity; if (!hasUsableKeyStatus) { - playlist.excludeUntil = Infinity; - playlist.lastExcludeReason_ = NON_USABLE; - this.logger_(`excluding playlist ${playlist.id} because the key ID ${key} doesn't exist in the keyStatusMap or is not ${USABLE}`); + // Only exclude playlists that haven't already been excluded as non-usable. + if (playlist.excludeUntil !== Infinity && playlist.lastExcludeReason_ !== NON_USABLE) { + playlist.excludeUntil = Infinity; + playlist.lastExcludeReason_ = NON_USABLE; + this.logger_(`excluding playlist ${playlist.id} because the key ID ${key} doesn't exist in the keyStatusMap or is not ${USABLE}`); + } + // count all nonUsableKeyStatus + nonUsableKeyStatusCount++; } else if (hasUsableKeyStatus && nonUsableExclusion) { delete playlist.excludeUntil; delete playlist.lastExcludeReason_; @@ -2419,6 +2425,20 @@ export class PlaylistController extends videojs.EventTarget { } }); }); + + // If for whatever reason every playlist has a non usable key status. Lets try re-including the SD renditions as a failsafe. + if (nonUsableKeyStatusCount >= this.mainPlaylistLoader_.main.playlists.length) { + this.mainPlaylistLoader_.main.playlists.forEach((playlist) => { + const isNonHD = playlist && playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height < 720; + const excludedForNonUsableKey = playlist.excludeUntil === Infinity && playlist.lastExcludeReason_ === NON_USABLE; + + if (isNonHD && excludedForNonUsableKey) { + // Only delete the excludeUntil so we don't try and re-exclude these playlists. + delete playlist.excludeUntil; + videojs.log.warn(`enabling non-HD playlist ${playlist.id} because all playlists were excluded due to ${NON_USABLE} key IDs`); + } + }); + } } /** @@ -2430,9 +2450,10 @@ export class PlaylistController extends videojs.EventTarget { addKeyStatus_(keyId, status) { const isString = typeof keyId === 'string'; const keyIdHexString = isString ? keyId : bufferToHexString(keyId); + const formattedKeyIdString = keyIdHexString.slice(0, 32).toLowerCase(); - // 32 digit keyId hex string. - this.keyStatusMap_.set(keyIdHexString.slice(0, 32), status); + this.logger_(`KeyStatus '${status}' with key ID ${formattedKeyIdString} added to the keyStatusMap`); + this.keyStatusMap_.set(formattedKeyIdString, status); } /** @@ -2443,6 +2464,15 @@ export class PlaylistController extends videojs.EventTarget { */ updatePlaylistByKeyStatus(keyId, status) { this.addKeyStatus_(keyId, status); + if (!this.waitingForFastQualityPlaylistReceived_) { + this.excludeNonUsableThenChangePlaylist_(); + } + // Listen to loadedplaylist with a single listener and check for new contentProtection elements when a playlist is updated. + this.mainPlaylistLoader_.off('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this)); + this.mainPlaylistLoader_.on('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this)); + } + + excludeNonUsableThenChangePlaylist_() { this.excludeNonUsablePlaylistsByKeyId_(); this.fastQualityChange_(); } diff --git a/test/playlist-controller.test.js b/test/playlist-controller.test.js index 05c6276aa..d0d1b1a9b 100644 --- a/test/playlist-controller.test.js +++ b/test/playlist-controller.test.js @@ -5061,6 +5061,76 @@ QUnit.test('excludeNonUsablePlaylistsByKeyId_ re includes non usable DASH playli }); }); +QUnit.test('excludeNonUsablePlaylistsByKeyId_ re-includes SD playlists when all playlists are excluded', function(assert) { + const options = { + src: 'test', + tech: this.player.tech_, + sourceType: 'dash' + }; + const pc = new PlaylistController(options); + const origWarn = videojs.log.warn; + const warnings = []; + + videojs.log.warn = (text) => warnings.push(text); + + const reIncludedPlaylist1 = { + contentProtection: { + mp4protection: { + attributes: { + 'cenc:default_KID': 'd0bebaf8a3cb4c52bae03d20a71e3df3' + } + } + }, + attributes: { + RESOLUTION: { + height: 480 + } + } + }; + + const reIncludedPlaylist2 = { + contentProtection: { + mp4protection: { + attributes: { + 'cenc:default_KID': '89256e53dbe544e9afba38d2ca17d176' + } + } + }, + attributes: { + RESOLUTION: { + height: 360 + } + } + }; + + const excludedPlaylist = { + contentProtection: { + mp4protection: { + attributes: { + 'cenc:default_KID': '89256e53dbe544e9afba38d2ca17d176' + } + } + }, + attributes: { + RESOLUTION: { + height: 1080 + } + } + }; + + pc.mainPlaylistLoader_.main = { playlists: [reIncludedPlaylist1, reIncludedPlaylist2, excludedPlaylist] }; + pc.excludeNonUsablePlaylistsByKeyId_(); + + assert.notOk(pc.mainPlaylistLoader_.main.playlists[0].excludeUntil, 'excludeUntil is not Infinity'); + assert.equal(pc.mainPlaylistLoader_.main.playlists[0].lastExcludeReason_, 'non-usable', 'lastExcludeReason is non-usable'); + assert.notOk(pc.mainPlaylistLoader_.main.playlists[1].excludeUntil, 'excludeUntil is not Infinity'); + assert.equal(pc.mainPlaylistLoader_.main.playlists[1].lastExcludeReason_, 'non-usable', 'lastExcludeReason is non-usable'); + assert.equal(warnings.length, 2, 're-include warning for both playlists'); + assert.equal(pc.mainPlaylistLoader_.main.playlists[2].excludeUntil, Infinity, 'excludeUntil is Infinity'); + assert.equal(pc.mainPlaylistLoader_.main.playlists[2].lastExcludeReason_, 'non-usable', 'lastExcludeReason is non-usable'); + videojs.log.warn = origWarn; +}); + QUnit.module('PlaylistController codecs', { beforeEach(assert) { sharedHooks.beforeEach.call(this, assert);