Skip to content

Commit

Permalink
Refactor/protection (#3793)
Browse files Browse the repository at this point in the history
* WiP: Refactor content protection

* Refactor some functions in StreamController.js

* Refactored the DRM workflow

* Fix a bug when registering events in the ProtectionController.js

* Rename variables in ProtectionController.js

* Do not call requestKeySystemAccess when creating a new session with an existing key system

* Clear all mediaInfos in ProtectionController when MPD is updated

* Import FactoryMaker in ProtectionController.js

* Fix unit tests for Protection

* Adjust index.d.ts

* Add missing JSDoc for ProtectionController.js

* Add support for dashif:laurl
  • Loading branch information
dsilhavy authored Nov 1, 2021
1 parent 8aac307 commit 1381ccb
Show file tree
Hide file tree
Showing 26 changed files with 1,114 additions and 787 deletions.
3 changes: 2 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ declare namespace dashjs {
interface ProtectionController {
initializeForMedia(mediaInfo: ProtectionMediaInfo): void;

clearMediaInfoArrayByStreamId(streamId: string): void;
clearMediaInfoArray(): void;

createKeySession(initData: ArrayBuffer, cdmData: Uint8Array): void;

Expand Down Expand Up @@ -175,6 +175,7 @@ declare namespace dashjs {
},
protection?: {
keepProtectionMediaKeys?: boolean,
ignoreEmeEncryptedEvent?: boolean
},
buffer?: {
enableSeekDecorrelationFix?: boolean,
Expand Down
95 changes: 95 additions & 0 deletions samples/drm/dashif-laurl.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>License server via MPD example</title>

<script src="../../dist/dash.all.debug.js"></script>

<!-- Bootstrap core CSS -->
<link href="../lib/bootstrap/bootstrap.min.css" rel="stylesheet">
<link href="../lib/main.css" rel="stylesheet">

<style>
video {
width: 640px;
height: 360px;
}
</style>

<script class="code">
function init() {
var protData = {
"com.widevine.alpha": {
"httpRequestHeaders": {
"X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU"
},
priority: 0
},
"com.microsoft.playready": {
"httpRequestHeaders": {
"X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU"
},
priority: 0
}
};
var video,
player,
url = "mpds/laurl.mpd";

video = document.querySelector("video");
player = dashjs.MediaPlayer().create();
player.updateSettings({
debug: {
logLevel: 5
}
})
player.initialize(video, url, true);
player.setProtectionData(protData);
}
</script>
</head>
<body>

<main>
<div class="container py-4">
<header class="pb-3 mb-4 border-bottom">
<img class=""
src="../lib/img/dashjs-logo.png"
width="200">
</header>
<div class="row">
<div class="col-md-4">
<div class="h-100 p-5 bg-light border rounded-3">
<h3>Widevine DRM instantiation example</h3>
<p>This example shows how to specify the license server url as part of the MPD using
'dashif:laurl'. </p>
<p>For a detailed explanation on this checkout the
<a href="https://github.com/Dash-Industry-Forum/dash.js/wiki/Digital-Rights-Management-(DRM)-and-license-acquisition#licenser-server-url-via-mpd"
target="_blank">Wiki</a>.</p>
</div>
</div>
<div class="col-md-8">
<video controls="true"></video>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div id="code-output"></div>
</div>
</div>
<footer class="pt-3 mt-4 text-muted border-top">
&copy; DASH-IF
</footer>
</div>
</main>


<script>
document.addEventListener('DOMContentLoaded', function () {
init();
});
</script>
<script src="../highlighter.js"></script>
</body>
</html>
15 changes: 15 additions & 0 deletions samples/samples.json
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,22 @@
"Video",
"Audio"
]
},
{
"title": "License server via MPD",
"description": "This example shows how to specify the license server url as part of the MPD using 'dashif:laurl'",
"href": "drm/dashif-laurl.html",
"image": "lib/img/tos-3.jpg",
"labels": [
"VoD",
"DRM",
"Widevine",
"Playready",
"Video",
"Audio"
]
}

]
},
{
Expand Down
5 changes: 4 additions & 1 deletion src/core/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,8 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest';
* Set the value for the ProtectionController and MediaKeys life cycle.
*
* If true, the ProtectionController and then created MediaKeys and MediaKeySessions will be preserved during the MediaPlayer lifetime.
* @property {boolean} ignoreEmeEncryptedEvent
* If set to true the player will ignore "encrypted" and "needkey" events thrown by the EME.
*/

/**
Expand Down Expand Up @@ -773,7 +775,8 @@ function Settings() {
applyServiceDescription: true
},
protection: {
keepProtectionMediaKeys: false
keepProtectionMediaKeys: false,
ignoreEmeEncryptedEvent: false
},
buffer: {
enableSeekDecorrelationFix: false,
Expand Down
2 changes: 1 addition & 1 deletion src/streaming/Stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ function Stream(config) {
if (protectionController) {
// Need to check if streamProcessors exists because streamProcessors
// could be cleared in case an error is detected while initializing DRM keysystem
protectionController.clearMediaInfoArrayByStreamId(getId());
protectionController.clearMediaInfoArray();
for (let i = 0; i < ln && streamProcessors[i]; i++) {
const type = streamProcessors[i].getType();
const mediaInfo = streamProcessors[i].getMediaInfo();
Expand Down
3 changes: 3 additions & 0 deletions src/streaming/constants/ProtectionConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class ProtectionConstants {
this.CLEARKEY_KEYSTEM_STRING = 'org.w3.clearkey';
this.WIDEVINE_KEYSTEM_STRING = 'com.widevine.alpha';
this.PLAYREADY_KEYSTEM_STRING = 'com.microsoft.playready';
this.INITIALIZATION_DATA_TYPE_CENC = 'cenc';
this.INITIALIZATION_DATA_TYPE_KEYIDS = 'keyids'
this.INITIALIZATION_DATA_TYPE_WEBM = 'webm'
}

constructor () {
Expand Down
24 changes: 12 additions & 12 deletions src/streaming/controllers/StreamController.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ function StreamController() {
}

function initialize(autoPl, protData) {
checkConfig();
_checkConfig();

autoPlay = autoPl;
protectionData = protData;
Expand Down Expand Up @@ -496,7 +496,7 @@ function StreamController() {
_handleOuterPeriodSeek(e, seekToStream);
}

createPlaylistMetrics(PlayList.SEEK_START_REASON);
_createPlaylistMetrics(PlayList.SEEK_START_REASON);
}

/**
Expand Down Expand Up @@ -698,7 +698,7 @@ function StreamController() {

if (isNaN(initialBufferLevel) || initialBufferLevel <= playbackController.getBufferLevel() || (adapter.getIsDynamic() && initialBufferLevel > playbackController.getLiveDelay())) {
initialPlayback = false;
createPlaylistMetrics(PlayList.INITIAL_PLAYOUT_START_REASON);
_createPlaylistMetrics(PlayList.INITIAL_PLAYOUT_START_REASON);
playbackController.play();
}
}
Expand Down Expand Up @@ -752,9 +752,9 @@ function StreamController() {
* @private
*/
function _onPlaybackStarted( /*e*/) {
logger.debug('[onPlaybackStarted]');
if (!initialPlayback && isPaused) {
createPlaylistMetrics(PlayList.RESUME_FROM_PAUSE_START_REASON);
logger.debug('[onPlaybackStarted]');
if (!initialPlayback && isPaused) {
_createPlaylistMetrics(PlayList.RESUME_FROM_PAUSE_START_REASON);
}
if (initialPlayback) {
initialPlayback = false;
Expand Down Expand Up @@ -1233,7 +1233,7 @@ function StreamController() {
dashMetrics.addPlayList();
}

function createPlaylistMetrics(startReason) {
function _createPlaylistMetrics(startReason) {
dashMetrics.createPlaylistMetrics(playbackController.getTime() * 1000, startReason);
}

Expand Down Expand Up @@ -1324,27 +1324,27 @@ function StreamController() {
return null;
}

function checkConfig() {
function _checkConfig() {
if (!manifestLoader || !manifestLoader.hasOwnProperty('load') || !timelineConverter || !timelineConverter.hasOwnProperty('initialize') ||
!timelineConverter.hasOwnProperty('reset') || !timelineConverter.hasOwnProperty('getClientTimeOffset') || !manifestModel || !errHandler ||
!dashMetrics || !playbackController) {
throw new Error(Constants.MISSING_CONFIG_ERROR);
}
}

function checkInitialize() {
function _checkInitialize() {
if (!manifestUpdater || !manifestUpdater.hasOwnProperty('setManifest')) {
throw new Error('initialize function has to be called previously');
}
}

function load(url) {
checkConfig();
_checkConfig();
manifestLoader.load(url);
}

function loadWithManifest(manifest) {
checkInitialize();
_checkInitialize();
manifestUpdater.setManifest(manifest);
}

Expand Down Expand Up @@ -1446,7 +1446,7 @@ function StreamController() {
}

function reset() {
checkConfig();
_checkConfig();

timeSyncController.reset();

Expand Down
53 changes: 53 additions & 0 deletions src/streaming/protection/CommonEncryption.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
* POSSIBILITY OF SUCH DAMAGE.
*/

const LICENSE_SERVER_MANIFEST_CONFIGURATIONS = {
attributes: ['Laurl','laurl'],
prefixes: ['clearkey', 'dashif']
};

/**
* @class
* @ignore
Expand Down Expand Up @@ -211,6 +216,54 @@ class CommonEncryption {

return pssh;
}

static getLicenseServerUrlFromMediaInfo(mediaInfo, schemeIdUri) {
try {

if (!mediaInfo || mediaInfo.length === 0) {
return null;
}

let i = 0;
let licenseServer = null;

while (i < mediaInfo.length && !licenseServer) {
const info = mediaInfo[i];

if (info && info.contentProtection && info.contentProtection.length > 0) {
const targetProtectionData = info.contentProtection.filter((cp) => {
return cp.schemeIdUri && cp.schemeIdUri === schemeIdUri;
});

if (targetProtectionData && targetProtectionData.length > 0) {
let j = 0;
while (j < targetProtectionData.length && !licenseServer) {
const ckData = targetProtectionData[j];
let k = 0;
while (k < LICENSE_SERVER_MANIFEST_CONFIGURATIONS.attributes.length && !licenseServer) {
let l = 0;
const attribute = LICENSE_SERVER_MANIFEST_CONFIGURATIONS.attributes[k];
while (l < LICENSE_SERVER_MANIFEST_CONFIGURATIONS.prefixes.length && !licenseServer) {
const prefix = LICENSE_SERVER_MANIFEST_CONFIGURATIONS.prefixes[l];
if (ckData[attribute] && ckData[attribute].__prefix && ckData[attribute].__prefix === prefix && ckData[attribute].__text) {
licenseServer = ckData[attribute].__text;
}
l += 1;
}
k += 1;
}
j += 1;
}
}
}
i += 1;
}
return licenseServer;
} catch
(e) {
return null;
}
}
}

export default CommonEncryption;
16 changes: 8 additions & 8 deletions src/streaming/protection/Protection.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ function Protection() {
protectionKeyController.setConfig({ debug: config.debug, BASE64: config.BASE64 });
protectionKeyController.initialize();

let protectionModel = getProtectionModel(config);
let protectionModel = _getProtectionModel(config);

if (!controller && protectionModel) {//TODO add ability to set external controller if still needed at all?
controller = ProtectionController(context).create({
Expand All @@ -138,7 +138,7 @@ function Protection() {
return controller;
}

function getProtectionModel(config) {
function _getProtectionModel(config) {
const debug = config.debug;
const logger = debug.getLogger(instance);
const eventBus = config.eventBus;
Expand All @@ -149,19 +149,19 @@ function Protection() {
(!videoElement || videoElement.mediaKeys !== undefined)) {
logger.info('EME detected on this user agent! (ProtectionModel_21Jan2015)');
return ProtectionModel_21Jan2015(context).create({ debug: debug, eventBus: eventBus, events: config.events });
} else if (getAPI(videoElement, APIS_ProtectionModel_3Feb2014)) {
} else if (_getAPI(videoElement, APIS_ProtectionModel_3Feb2014)) {
logger.info('EME detected on this user agent! (ProtectionModel_3Feb2014)');
return ProtectionModel_3Feb2014(context).create({ debug: debug, eventBus: eventBus, events: config.events, api: getAPI(videoElement, APIS_ProtectionModel_3Feb2014) });
} else if (getAPI(videoElement, APIS_ProtectionModel_01b)) {
return ProtectionModel_3Feb2014(context).create({ debug: debug, eventBus: eventBus, events: config.events, api: _getAPI(videoElement, APIS_ProtectionModel_3Feb2014) });
} else if (_getAPI(videoElement, APIS_ProtectionModel_01b)) {
logger.info('EME detected on this user agent! (ProtectionModel_01b)');
return ProtectionModel_01b(context).create({ debug: debug, eventBus: eventBus, errHandler: errHandler, events: config.events, api: getAPI(videoElement, APIS_ProtectionModel_01b) });
return ProtectionModel_01b(context).create({ debug: debug, eventBus: eventBus, errHandler: errHandler, events: config.events, api: _getAPI(videoElement, APIS_ProtectionModel_01b) });
} else {
logger.warn('No supported version of EME detected on this user agent! - Attempts to play encrypted content will fail!');
return null;
}
}

function getAPI(videoElement, apis) {
function _getAPI(videoElement, apis) {
for (let i = 0; i < apis.length; i++) {
const api = apis[i];
// detect if api is supported by browser
Expand All @@ -177,7 +177,7 @@ function Protection() {
}

instance = {
createProtectionSystem: createProtectionSystem
createProtectionSystem
};

return instance;
Expand Down
7 changes: 0 additions & 7 deletions src/streaming/protection/ProtectionEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,6 @@ class ProtectionEvents extends EventsBase {
*/
this.INTERNAL_KEY_MESSAGE = 'internalKeyMessage';

/**
* Event ID for events delivered when a key system selection procedure
* completes
* @ignore
*/
this.INTERNAL_KEY_SYSTEM_SELECTED = 'internalKeySystemSelected';

/**
* Event ID for events delivered when the status of one decryption keys has changed
* @ignore
Expand Down
Loading

0 comments on commit 1381ccb

Please sign in to comment.