Skip to content

Commit

Permalink
feat: enable playlists with 'usable' keystatus (#1460)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrums86 authored Dec 13, 2023
1 parent 39dbe77 commit 7d7c639
Show file tree
Hide file tree
Showing 9 changed files with 394 additions and 56 deletions.
22 changes: 22 additions & 0 deletions src/dash-playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -924,4 +924,26 @@ export default class DashPlaylistLoader extends EventTarget {
this.addMetadataToTextTrack('EventStream', metadataArray, this.mainPlaylistLoader_.main.duration);
}
}

/**
* Returns the key ID set from a playlist
*
* @param {playlist} playlist to fetch the key ID set from.
* @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist.
*/
getKeyIdSet(playlist) {
if (playlist.contentProtection) {
const keyIds = new Set();

for (const keysystem in playlist.contentProtection) {
const defaultKID = playlist.contentProtection[keysystem].attributes['cenc:default_KID'];

if (defaultKID) {
// DASH keyIds are separated by dashes.
keyIds.add(defaultKID.replace(/-/g, ''));
}
}
return keyIds;
}
}
}
74 changes: 73 additions & 1 deletion src/playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import logger from './util/logger';
import {merge, createTimeRanges} from './util/vjs-compat';
import { addMetadata, createMetadataTrackIfNotExists, addDateRangeMetadata } from './util/text-tracks';
import ContentSteeringController from './content-steering-controller';
import { bufferToHexString } from './util/string.js';

const ABORT_EARLY_EXCLUSION_SECONDS = 10;

Expand Down Expand Up @@ -235,6 +236,7 @@ export class PlaylistController extends videojs.EventTarget {
this.sourceUpdater_ = new SourceUpdater(this.mediaSource);
this.inbandTextTracks_ = {};
this.timelineChangeController_ = new TimelineChangeController();
this.keyStatusMap_ = new Map();

const segmentLoaderSettings = {
vhs: this.vhs_,
Expand Down Expand Up @@ -403,7 +405,7 @@ export class PlaylistController extends videojs.EventTarget {
switchMedia_(playlist, cause, delay) {
const oldMedia = this.media();
const oldId = oldMedia && (oldMedia.id || oldMedia.uri);
const newId = playlist.id || playlist.uri;
const newId = playlist && (playlist.id || playlist.uri);

if (oldId && oldId !== newId) {
this.logger_(`switch media ${oldId} -> ${newId} from ${cause}`);
Expand Down Expand Up @@ -1708,6 +1710,7 @@ export class PlaylistController extends videojs.EventTarget {
this.mainPlaylistLoader_.dispose();
this.mainSegmentLoader_.dispose();
this.contentSteeringController_.dispose();
this.keyStatusMap_.clear();

if (this.loadOnPlay_) {
this.tech_.off('play', this.loadOnPlay_);
Expand Down Expand Up @@ -2380,4 +2383,73 @@ export class PlaylistController extends videojs.EventTarget {

this.switchMedia_(nextPlaylist, 'content-steering');
}

/**
* Iterates through playlists and check their keyId set and compare with the
* keyStatusMap, only enable playlists that have a usable key. If the playlist
* has no keyId leave it enabled by default.
*/
excludeNonUsablePlaylistsByKeyId_() {

if (!this.mainPlaylistLoader_ || !this.mainPlaylistLoader_.main) {
return;
}

this.mainPlaylistLoader_.main.playlists.forEach((playlist) => {
const keyIdSet = this.mainPlaylistLoader_.getKeyIdSet(playlist);

// If the playlist doesn't have keyIDs lets not exclude it.
if (!keyIdSet || !keyIdSet.size) {
return;
}
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}`);
} else if (hasUsableKeyStatus && nonUsableExclusion) {
delete playlist.excludeUntil;
delete playlist.lastExcludeReason_;
this.logger_(`enabling playlist ${playlist.id} because key ID ${key} is ${USABLE}`);
}
});
});
}

/**
* Adds a keystatus to the keystatus map, tries to convert to string if necessary.
*
* @param {any} keyId the keyId to add a status for
* @param {string} status the status of the keyId
*/
addKeyStatus_(keyId, status) {
const isString = typeof keyId === 'string';
const keyIdHexString = isString ? keyId : bufferToHexString(keyId);

// 32 digit keyId hex string.
this.keyStatusMap_.set(keyIdHexString.slice(0, 32), status);
}

/**
* Utility function for adding key status to the keyStatusMap and filtering usable encrypted playlists.
*
* @param {any} keyId the keyId from the keystatuschange event
* @param {string} status the key status string
*/
updatePlaylistByKeyStatus(keyId, status) {
this.addKeyStatus_(keyId, status);
this.excludeNonUsablePlaylistsByKeyId_();
const oldPlaylist = this.mainPlaylistLoader_.media();
const newPlaylist = this.selectPlaylist();
const keystatusChange = 'keystatus-change';

if (newPlaylist !== oldPlaylist) {
this.switchMedia_(newPlaylist, keystatusChange);
}
}
}
21 changes: 21 additions & 0 deletions src/playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1194,4 +1194,25 @@ export default class PlaylistLoader extends EventTarget {

return attributes;
}

/**
* Returns the key ID set from a playlist
*
* @param {playlist} playlist to fetch the key ID set from.
* @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist.
*/
getKeyIdSet(playlist) {
if (playlist.contentProtection) {
const keyIds = new Set();

for (const keysystem in playlist.contentProtection) {
const keyId = playlist.contentProtection[keysystem].attributes.keyId;

if (keyId) {
keyIds.add(keyId);
}
}
return keyIds;
}
}
}
6 changes: 6 additions & 0 deletions src/util/string.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
export const uint8ToUtf8 = (uintArray) =>
decodeURIComponent(escape(String.fromCharCode.apply(null, uintArray)));

export const bufferToHexString = (buffer) => {
const uInt8Buffer = new Uint8Array(buffer);

return Array.from(uInt8Buffer).map((byte) => byte.toString(16).padStart(2, '0')).join('');
};
36 changes: 1 addition & 35 deletions src/videojs-http-streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -1126,41 +1126,7 @@ class VhsHandler extends Component {
});

this.player_.tech_.on('keystatuschange', (e) => {
if (e.status !== 'output-restricted') {
return;
}

const mainPlaylist = this.playlistController_.main();

if (!mainPlaylist || !mainPlaylist.playlists) {
return;
}

const excludedHDPlaylists = [];

// Assume all HD streams are unplayable and exclude them from ABR selection
mainPlaylist.playlists.forEach(playlist => {
if (playlist && playlist.attributes && playlist.attributes.RESOLUTION &&
playlist.attributes.RESOLUTION.height >= 720) {
if (!playlist.excludeUntil || playlist.excludeUntil < Infinity) {

playlist.excludeUntil = Infinity;
excludedHDPlaylists.push(playlist);
}
}
});

if (excludedHDPlaylists.length) {
videojs.log.warn(
'DRM keystatus changed to "output-restricted." Removing the following HD playlists ' +
'that will most likely fail to play and clearing the buffer. ' +
'This may be due to HDCP restrictions on the stream and the capabilities of the current device.',
...excludedHDPlaylists
);

// Clear the buffer before switching playlists, since it may already contain unplayable segments
this.playlistController_.fastQualityChange_();
}
this.playlistController_.updatePlaylistByKeyStatus(e.keyId, e.status);
});

this.handleWaitingForKey_ = this.handleWaitingForKey_.bind(this);
Expand Down
19 changes: 19 additions & 0 deletions test/dash-playlist-loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ QUnit.module('DASH Playlist Loader: unit', {
}
});

QUnit.test('can getKeyIdSet from a playlist', function(assert) {
const loader = new DashPlaylistLoader('variant.mpd', this.fakeVhs);
const keyId = '188743e1-bd62-400e-92d9-748f8c753d1a';
// We currently only pass keyId for widevine content protection.
const playlist = {
contentProtection: {
mp4protection: {
attributes: {
'cenc:default_KID': keyId
}
}
}
};
const keyIdSet = loader.getKeyIdSet(playlist);

assert.ok(keyIdSet.size);
assert.ok(keyIdSet.has(keyId.replace(/-/g, '')), 'keyId is expected hex string');
});

QUnit.test('updateMain: returns falsy when there are no changes', function(assert) {
const main = {
playlists: {
Expand Down
Loading

0 comments on commit 7d7c639

Please sign in to comment.