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: Add support for defining custom headers for default license and certificate requests. #76

Merged
merged 7 commits into from
Mar 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
463 changes: 280 additions & 183 deletions README.md

Large diffs are not rendered by default.

23 changes: 14 additions & 9 deletions src/eme.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import videojs from 'video.js';
import { requestPlayreadyLicense } from './playready';
import window from 'global/window';
import {mergeAndRemoveNull} from './utils';

export const getSupportedKeySystem = (keySystems) => {
// As this happens after the src is set on the video, we rely only on the set src (we
Expand Down Expand Up @@ -171,8 +172,8 @@ const setMediaKeys = ({
return Promise.all(promises);
};

const defaultPlayreadyGetLicense = (url) => (emeOptions, keyMessage, callback) => {
requestPlayreadyLicense(url, keyMessage, (err, response, responseBody) => {
const defaultPlayreadyGetLicense = (keySystemOptions) => (emeOptions, keyMessage, callback) => {
requestPlayreadyLicense(keySystemOptions, keyMessage, emeOptions, (err, response, responseBody) => {
if (err) {
callback(err);
return;
Expand All @@ -182,15 +183,19 @@ const defaultPlayreadyGetLicense = (url) => (emeOptions, keyMessage, callback) =
});
};

const defaultGetLicense = (url) => (emeOptions, keyMessage, callback) => {
const defaultGetLicense = (keySystemOptions) => (emeOptions, keyMessage, callback) => {
const headers = mergeAndRemoveNull(
{'Content-type': 'application/octet-stream'},
Copy link
Contributor

@squarebracket squarebracket Mar 15, 2019

Choose a reason for hiding this comment

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

Since you're now providing options to add headers, any chance you could not include this by default? For whatever reason, our DRM provider doesn't have Content-Type in the Access-Control-Allow-Headers header, so I provide a custom function just so this header isn't there.

Copy link
Member

Choose a reason for hiding this comment

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

do you think there would be issues in removing it? I've got no major objections unless removing it may break someone.

Copy link
Member

Choose a reason for hiding this comment

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

What if we allowed removing headers by setting them to null or something, so you could do:

licenseHeaders: {
  'Content-Type': null
}

I am hesitant to remove defaults because it could easily break someone.

emeOptions.emeHeaders,
keySystemOptions.licenseHeaders
);

videojs.xhr({
uri: url,
uri: keySystemOptions.url,
method: 'POST',
responseType: 'arraybuffer',
body: keyMessage,
headers: {
'Content-type': 'application/octet-stream'
}
headers
}, (err, response, responseBody) => {
if (err) {
callback(err);
Expand Down Expand Up @@ -230,8 +235,8 @@ const standardizeKeySystemOptions = (keySystem, keySystemOptions) => {

if (keySystemOptions.url && !keySystemOptions.getLicense) {
keySystemOptions.getLicense = keySystem === 'com.microsoft.playready' ?
defaultPlayreadyGetLicense(keySystemOptions.url) :
defaultGetLicense(keySystemOptions.url);
defaultPlayreadyGetLicense(keySystemOptions) :
defaultGetLicense(keySystemOptions);
}

return keySystemOptions;
Expand Down
32 changes: 21 additions & 11 deletions src/fairplay.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import videojs from 'video.js';
import window from 'global/window';
import {stringToUint16Array, uint8ArrayToString, getHostnameFromUri} from './utils';
import {stringToUint16Array, uint8ArrayToString, getHostnameFromUri, mergeAndRemoveNull} from './utils';

export const FAIRPLAY_KEY_SYSTEM = 'com.apple.fps.1_0';

Expand Down Expand Up @@ -99,11 +99,17 @@ const addKey = ({video, contentId, initData, cert, options, getLicense, eventBus
});
};

const defaultGetCertificate = (certificateUri) => {
export const defaultGetCertificate = (fairplayOptions) => {
return (emeOptions, callback) => {
const headers = mergeAndRemoveNull(
emeOptions.emeHeaders,
fairplayOptions.certificateHeaders
);

videojs.xhr({
uri: certificateUri,
responseType: 'arraybuffer'
uri: fairplayOptions.certificateUri,
responseType: 'arraybuffer',
headers
}, (err, response, responseBody) => {
if (err) {
callback(err);
Expand All @@ -119,16 +125,20 @@ const defaultGetContentId = (emeOptions, initData) => {
return getHostnameFromUri(uint8ArrayToString(initData));
};

const defaultGetLicense = (licenseUri) => {
export const defaultGetLicense = (fairplayOptions) => {
return (emeOptions, contentId, keyMessage, callback) => {
const headers = mergeAndRemoveNull(
{'Content-type': 'application/octet-stream'},
emeOptions.emeHeaders,
fairplayOptions.licenseHeaders
);

videojs.xhr({
uri: licenseUri,
uri: fairplayOptions.licenseUri,
method: 'POST',
responseType: 'arraybuffer',
body: keyMessage,
headers: {
'Content-type': 'application/octet-stream'
}
headers
}, (err, response, responseBody) => {
if (err) {
callback(err);
Expand All @@ -143,10 +153,10 @@ const defaultGetLicense = (licenseUri) => {
const fairplay = ({video, initData, options, eventBus}) => {
const fairplayOptions = options.keySystems[FAIRPLAY_KEY_SYSTEM];
const getCertificate = fairplayOptions.getCertificate ||
defaultGetCertificate(fairplayOptions.certificateUri);
defaultGetCertificate(fairplayOptions);
const getContentId = fairplayOptions.getContentId || defaultGetContentId;
const getLicense = fairplayOptions.getLicense ||
defaultGetLicense(fairplayOptions.licenseUri);
defaultGetLicense(fairplayOptions);

return new Promise((resolve, reject) => {
getCertificate(options, (err, cert) => {
Expand Down
12 changes: 8 additions & 4 deletions src/ms-prefixed.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,22 @@ export const addKeyToSession = (options, session, event, eventBus) => {
}

if (typeof playreadyOptions === 'string') {
playreadyOptions = { url: playreadyOptions };
playreadyOptions = {url: playreadyOptions};
} else if (typeof playreadyOptions === 'boolean') {
playreadyOptions = {};
}

const url = playreadyOptions.url || event.destinationURL;
if (!playreadyOptions.url) {
playreadyOptions.url = event.destinationURL;
}

requestPlayreadyLicense(url, event.message.buffer, (err, response) => {
requestPlayreadyLicense(playreadyOptions, event.message.buffer, options, (err, response) => {
if (eventBus) {
eventBus.trigger('licenserequestattempted');
}
if (err) {
eventBus.trigger({
message: 'Unable to request key from url: ' + url,
message: 'Unable to request key from url: ' + playreadyOptions.url,
target: session,
type: 'mskeyerror'
});
Expand Down
14 changes: 11 additions & 3 deletions src/playready.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import videojs from 'video.js';
import window from 'global/window';
import {mergeAndRemoveNull} from './utils';

/**
* Parses the EME key message XML to extract HTTP headers and the Challenge element to use
Expand Down Expand Up @@ -40,11 +41,18 @@ export const getMessageContents = (message) => {
};
};

export const requestPlayreadyLicense = (url, messageBuffer, callback) => {
const { headers, message } = getMessageContents(messageBuffer);
export const requestPlayreadyLicense = (keySystemOptions, messageBuffer, emeOptions, callback) => {
const messageContents = getMessageContents(messageBuffer);
const message = messageContents.message;

const headers = mergeAndRemoveNull(
messageContents.headers,
emeOptions.emeHeaders,
keySystemOptions.licenseHeaders
);

videojs.xhr({
uri: url,
uri: keySystemOptions.url,
method: 'post',
headers,
body: message,
Expand Down
14 changes: 14 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import document from 'global/document';
import videojs from 'video.js';

export const stringToUint16Array = (string) => {
// 2 bytes for each char
Expand Down Expand Up @@ -52,3 +53,16 @@ export const arrayBufferFrom = (bufferOrTypedArray) => {

return bufferOrTypedArray;
};

export const mergeAndRemoveNull = (...args) => {
const result = videojs.mergeOptions(...args);

// Any header whose value is `null` will be removed.
Object.keys(result).forEach(k => {
if (result[k] === null) {
delete result[k];
}
});

return result;
};
125 changes: 125 additions & 0 deletions test/eme.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -763,3 +763,128 @@ QUnit.test('keySession.update promise rejection', function(assert) {
});

});

QUnit.test('emeHeaders option sets headers on default license xhr request', function(assert) {
const done = assert.async();
const origXhr = videojs.xhr;
const xhrCalls = [];
const session = new videojs.EventTarget();

videojs.xhr = (options) => {
xhrCalls.push(options);
};

const keySystemAccess = {
keySystem: 'com.widevine.alpha',
createMediaKeys: () => {
return {
createSession: () => session
};
}
};

standard5July2016({
keySystemAccess,
video: {
setMediaKeys: (createdMediaKeys) => Promise.resolve(createdMediaKeys)
},
initDataType: '',
initData: '',
options: {
keySystems: {
'com.widevine.alpha': 'some-url'
},
emeHeaders: {
'Some-Header': 'some-header-value'
}
}
}).catch((e) => {});

setTimeout(() => {
session.trigger({
type: 'message',
message: 'the-message'
});

assert.equal(xhrCalls.length, 1, 'made one XHR');
assert.deepEqual(xhrCalls[0], {
uri: 'some-url',
method: 'POST',
responseType: 'arraybuffer',
body: 'the-message',
headers: {
'Content-type': 'application/octet-stream',
'Some-Header': 'some-header-value'
}
}, 'made request with proper emeHeaders option value');

videojs.xhr = origXhr;

done();
});
});

QUnit.test('licenseHeaders keySystems property overrides emeHeaders value', function(assert) {
const done = assert.async();
const origXhr = videojs.xhr;
const xhrCalls = [];
const session = new videojs.EventTarget();

videojs.xhr = (options) => {
xhrCalls.push(options);
};

const keySystemAccess = {
keySystem: 'com.widevine.alpha',
createMediaKeys: () => {
return {
createSession: () => session
};
}
};

standard5July2016({
keySystemAccess,
video: {
setMediaKeys: (createdMediaKeys) => Promise.resolve(createdMediaKeys)
},
initDataType: '',
initData: '',
options: {
keySystems: {
'com.widevine.alpha': {
url: 'some-url',
licenseHeaders: {
'Some-Header': 'priority-header-value'
}
}
},
emeHeaders: {
'Some-Header': 'lower-priority-header-value'
}
}
}).catch((e) => {});

setTimeout(() => {
session.trigger({
type: 'message',
message: 'the-message'
});

assert.equal(xhrCalls.length, 1, 'made one XHR');
assert.deepEqual(xhrCalls[0], {
uri: 'some-url',
method: 'POST',
responseType: 'arraybuffer',
body: 'the-message',
headers: {
'Content-type': 'application/octet-stream',
'Some-Header': 'priority-header-value'
}
}, 'made request with proper licenseHeaders value');

videojs.xhr = origXhr;

done();
});
});
Loading