Skip to content

Commit

Permalink
fix: keyId filtering loadedplaylist listener and improvements (#1468)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrums86 authored Dec 21, 2023
1 parent 7debc17 commit f12c197
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 7 deletions.
44 changes: 37 additions & 7 deletions src/playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -2404,21 +2406,39 @@ 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_;
this.logger_(`enabling playlist ${playlist.id} because key ID ${key} is ${USABLE}`);
}
});
});

// 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`);
}
});
}
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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_();
}
Expand Down
70 changes: 70 additions & 0 deletions test/playlist-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit f12c197

Please sign in to comment.