Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added API to set media keys directly #61

Merged
merged 15 commits into from
Oct 24, 2018
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Maintenance Status: Stable
- [Source Options](#source-options)
- [Plugin Options](#plugin-options)
- [emeOptions](#emeoptions)
- [initializeMediaKeys](#initializemediakeys)
- [Passing methods seems complicated](#passing-methods-seems-complicated)
- [Special Events](#special-events)
- [Getting Started](#getting-started)
Expand Down Expand Up @@ -328,6 +329,22 @@ player.src({
});
```

### initializeMediaKeys
Type: `function`

`player.eme.initializeMediaKeys()` sets up MediaKeys immediately on demand. This is useful for setting up the video element for DRM before loading any content. Otherwise the video element is set up for DRM on `encrypted` events. This is not supported in Safari.

```javascript
// additional plugin options
var emeOptions = {
keySystems: {
'org.w3.clearkey': {...}
}
};

player.eme.initializeMediaKeys(emeOptions);
ldayananda marked this conversation as resolved.
Show resolved Hide resolved
```

### Passing methods seems complicated

While simple URLs are supported for many EME implementations, we wanted to provide as much
Expand Down
55 changes: 50 additions & 5 deletions src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,36 @@ const onPlayerReady = (player) => {
});
};

/**
* Sets up MediaKeys on demand
* Works around https://bugs.chromium.org/p/chromium/issues/detail?id=895449
*
* @function initializeMediaKeys
* @param {Object} [player]
* A player object.
* @param {Object} [emeOptions={}]
* An object of eme plugin options.
*/
const initializeMediaKeys = (player, emeOptions = {}) => {
// TODO: this should be refactored and renamed to be less tied
// to encrypted events

// fake an encrypted event for handleEncryptedEvent
const mockEncryptedEvent = {
initDataType: 'cenc',
ldayananda marked this conversation as resolved.
Show resolved Hide resolved
initData: null,
target: player.tech_.el_
};

setupSessions(player);

if (player.tech_.el_.setMediaKeys) {
return handleEncryptedEvent(mockEncryptedEvent, emeOptions, player.eme.sessions, player.tech_);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth catching the promise rejection here since the mock is always expected behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is expected to fail if you don't provide enough capability information for EME so I believe the error should bubble up

} else if (player.tech_.el_.msSetMediaKeys) {
handleMsNeedKeyEvent(mockEncryptedEvent, emeOptions, player.eme.sessions, player.tech_);
ldayananda marked this conversation as resolved.
Show resolved Hide resolved
}
};

/**
* A video.js plugin.
*
Expand All @@ -231,18 +261,33 @@ const onPlayerReady = (player) => {
* to you; if not, remove the wait for "ready"!
*
* @function eme
* @param {Object} [player]
* A player object.
* @param {Object} [options={}]
* An object of options left to the plugin author to define.
*/
const eme = function(options = {}) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This worked for me:

const eme = function(options = {}) {
  const player = this;

  this.ready(() => onPlayerReady(this));

  this.eme = {
    initializeMediaKeys(emeOptions = {}) {
      const mergedEmeOptions = videojs.mergeOptions(
        player.currentSource(),
        options,
        emeOptions
      );

      return initializeMediaKeys(player, mergedEmeOptions);
    },
    options
  };
};

// Register the plugin with video.js.
const registerPlugin = videojs.registerPlugin || videojs.plugin;

registerPlugin('eme', eme);

to be clear, I'm trying to avoid an anonymous function being the plugin factory here

this.eme.options = options;

this.ready(() => onPlayerReady(this));
const eme = function(player, options = {}) {
ldayananda marked this conversation as resolved.
Show resolved Hide resolved
player.ready(() => onPlayerReady(player));

player.eme = {
initializeMediaKeys(emeOptions = {}) {
const mergedEmeOptions = videojs.mergeOptions(
player.currentSource(),
ldayananda marked this conversation as resolved.
Show resolved Hide resolved
options,
emeOptions
);

return initializeMediaKeys(player, mergedEmeOptions);
ldayananda marked this conversation as resolved.
Show resolved Hide resolved
},
options
};
};

// Register the plugin with video.js.
const registerPlugin = videojs.registerPlugin || videojs.plugin;

registerPlugin('eme', eme);
registerPlugin('eme', function(options) {
eme(this, options);
});

export default eme;
64 changes: 64 additions & 0 deletions test/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,23 @@ QUnit.module('videojs-contrib-eme', {
this.video = document.createElement('video');
this.fixture.appendChild(this.video);
this.player = videojs(this.video);

this.origRequestMediaKeySystemAccess = window.navigator.requestMediaKeySystemAccess;

window.navigator.requestMediaKeySystemAccess = (keySystem, options) => {
return Promise.resolve({
keySystem: 'org.w3.clearkey',
createMediaKeys: () => {
return {
createSession: () => new videojs.EventTarget()
};
}
});
};
},

afterEach() {
window.navigator.requestMediaKeySystemAccess = this.origRequestMediaKeySystemAccess;
this.player.dispose();
this.clock.restore();
}
Expand Down Expand Up @@ -82,6 +96,56 @@ QUnit.test('exposes options', function(assert) {
'exposes publisherId');
});

QUnit.test('initializeMediaKeys standard', function(assert) {
const done = assert.async();
const initData = new Uint8Array([1, 2, 3]).buffer;

this.player.eme();

// testing the rejection path because this isn't a real session
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this testing a rejection?

this.player.eme.initializeMediaKeys({
keySystems: {
'org.w3.clearkey': {
pssh: initData
}
}
}).catch(() => {
const sessions = this.player.eme.sessions;

assert.equal(sessions.length, 1, 'created a session when keySystems in options');
assert.deepEqual(sessions[0].initData, initData, 'captured initData in the session');
done();
});
});

QUnit.test('initializeMediaKeys ms-prefix', function(assert) {
// stub setMediaKeys
const setMediaKeys = this.player.tech_.el_.setMediaKeys;

this.player.tech_.el_.setMediaKeys = null;
this.player.tech_.el_.msSetMediaKeys = () => {};

const initData = new Uint8Array([1, 2, 3]).buffer;

this.player.eme();

this.player.eme.initializeMediaKeys({
keySystems: {
'com.microsoft.playready': {
pssh: initData
}
}
});

const sessions = this.player.eme.sessions;

assert.equal(sessions.length, 1, 'created a session when keySystems in options');
assert.deepEqual(sessions[0].initData, initData, 'captured initData in the session');

this.player.tech_.el_.msSetMediaKeys = null;
this.player.tech_.el_.setMediaKeys = setMediaKeys;
});

QUnit.module('plugin guard functions', {
beforeEach() {
this.options = {
Expand Down