import videojs from 'video.js';
import { standard5July2016, getSupportedKeySystem } from './eme';
import {
default as fairplay,
FAIRPLAY_KEY_SYSTEM
} from './fairplay';
import {
default as msPrefixed,
PLAYREADY_KEY_SYSTEM
} from './ms-prefixed';
import { arrayBuffersEqual, arrayBufferFrom } from './utils';
export const hasSession = (sessions, initData) => {
for (let i = 0; i < sessions.length; i++) {
// Other types of sessions may be in the sessions array that don't store the initData
// (for instance, PlayReady sessions on IE11).
if (!sessions[i].initData) {
continue;
}
// initData should be an ArrayBuffer by the spec:
// eslint-disable-next-line max-len
// @see [Media Encrypted Event initData Spec]{@link https://www.w3.org/TR/encrypted-media/#mediaencryptedeventinit}
//
// However, on some browsers it may come back with a typed array view of the buffer.
// This is the case for IE11, however, since IE11 sessions are handled differently
// (following the msneedkey PlayReady path), this coversion may not be important. It
// is safe though, and might be a good idea to retain in the short term (until we have
// catalogued the full range of browsers and their implementations).
const sessionBuffer = arrayBufferFrom(sessions[i].initData);
const initDataBuffer = arrayBufferFrom(initData);
if (arrayBuffersEqual(sessionBuffer, initDataBuffer)) {
return true;
}
}
return false;
};
export const removeSession = (sessions, initData) => {
for (let i = 0; i < sessions.length; i++) {
if (sessions[i].initData === initData) {
sessions.splice(i, 1);
return;
}
}
};
export const handleEncryptedEvent = (event, options, sessions, eventBus) => {
if (!options || !options.keySystems) {
// return silently since it may be handled by a different system
return Promise.resolve();
}
let initData = event.initData;
return getSupportedKeySystem(options.keySystems).then(({keySystem}) => {
// Use existing init data from options if provided
if (options.keySystems[keySystem] &&
options.keySystems[keySystem].pssh) {
initData = options.keySystems[keySystem].pssh;
}
// "Initialization Data must be a fixed value for a given set of stream(s) or media
// data. It must only contain information related to the keys required to play a given
// set of stream(s) or media data."
// eslint-disable-next-line max-len
// @see [Initialization Data Spec]{@link https://www.w3.org/TR/encrypted-media/#initialization-data}
if (hasSession(sessions, initData)) {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log('eme',
// 'Already have a configured session for init data, ignoring event.');
return;
}
sessions.push({ initData });
return standard5July2016({
video: event.target,
initDataType: event.initDataType,
initData,
options,
removeSession: removeSession.bind(null, sessions),
eventBus
});
});
};
export const handleWebKitNeedKeyEvent = (event, options, eventBus) => {
if (!options.keySystems || !options.keySystems[FAIRPLAY_KEY_SYSTEM]) {
// return silently since it may be handled by a different system
return;
}
// From Apple's example Safari FairPlay integration code, webkitneedkey is not repeated
// for the same content. Unless documentation is found to present the opposite, handle
// all webkitneedkey events the same (even if they are repeated).
return fairplay({
video: event.target,
initData: event.initData,
options,
eventBus
});
};
export const handleMsNeedKeyEvent = (event, options, sessions, eventBus) => {
if (!options.keySystems || !options.keySystems[PLAYREADY_KEY_SYSTEM]) {
// return silently since it may be handled by a different system
return;
}
// "With PlayReady content protection, your Web app must handle the first needKey event,
// but it must then ignore any other needKey event that occurs."
// eslint-disable-next-line max-len
// @see [PlayReady License Acquisition]{@link https://msdn.microsoft.com/en-us/library/dn468979.aspx}
//
// Usually (and as per the example in the link above) this is determined by checking for
// the existence of video.msKeys. However, since the video element may be reused, it's
// easier to directly manage the session.
if (sessions.reduce((acc, session) => acc || session.playready, false)) {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log('eme',
// 'An \'msneedkey\' event was receieved earlier, ignoring event.');
return;
}
let initData = event.initData;
// Use existing init data from options if provided
if (options.keySystems[PLAYREADY_KEY_SYSTEM] &&
options.keySystems[PLAYREADY_KEY_SYSTEM].pssh) {
initData = options.keySystems[PLAYREADY_KEY_SYSTEM].pssh;
}
sessions.push({ playready: true, initData });
msPrefixed({
video: event.target,
initData,
options,
eventBus
});
};
export const getOptions = (player) => {
return videojs.mergeOptions(player.currentSource(), player.eme.options);
};
/**
* Configure a persistent sessions array and activeSrc property to ensure we properly
* handle each independent source's events. Should be run on any encrypted or needkey
* style event to ensure that the sessions reflect the active source.
*
* @function setupSessions
* @param {Player} player
*/
export const setupSessions = (player) => {
const src = player.src();
if (src !== player.eme.activeSrc) {
player.eme.activeSrc = src;
player.eme.sessions = [];
}
};
/**
* Function to invoke when the player is ready.
*
* This is a great place for your plugin to initialize itself. When this
* function is called, the player will have its DOM and child components
* in place.
*
* @function onPlayerReady
* @param {Player} player
* @param {Object} [options={}]
*/
const onPlayerReady = (player) => {
if (player.$('.vjs-tech').tagName.toLowerCase() !== 'video') {
return;
}
setupSessions(player);
// Support EME 05 July 2016
// Chrome 42+, Firefox 47+, Edge
player.tech_.el_.addEventListener('encrypted', (event) => {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log('eme', 'Received an \'encrypted\' event');
setupSessions(player);
handleEncryptedEvent(event, getOptions(player), player.eme.sessions, player.tech_);
});
// Support Safari EME with FairPlay
// (also used in early Chrome or Chrome with EME disabled flag)
player.tech_.el_.addEventListener('webkitneedkey', (event) => {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log('eme', 'Received a \'webkitneedkey\' event');
// TODO it's possible that the video state must be cleared if reusing the same video
// element between sources
setupSessions(player);
handleWebKitNeedKeyEvent(event, getOptions(player), player.tech_);
});
// EDGE still fires msneedkey, but should use encrypted instead
if (videojs.browser.IS_EDGE) {
return;
}
// IE11 Windows 8.1+
player.tech_.el_.addEventListener('msneedkey', (event) => {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log('eme', 'Received an \'msneedkey\' event');
setupSessions(player);
handleMsNeedKeyEvent(event, getOptions(player), player.eme.sessions, player.tech_);
});
};
/**
* Sets up MediaKeys on demand
*
* @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
// fake an encrypted event for handleEncryptedEvent
const mockEncryptedEvent = {
initDataType: 'cenc',
initData: null,
target: player.tech_.el_
};
setupSessions(player);
// TODO: this should be refactored and renamed to be less tied
// to encrypted events
if (player.tech_.el_.setMediaKeys) {
return handleEncryptedEvent(mockEncryptedEvent, emeOptions, player.eme.sessions, player.tech_);
} else if (player.tech_.el_.msSetMediaKeys) {
handleMsNeedKeyEvent(mockEncryptedEvent, emeOptions, player.eme.sessions, player.tech_);
}
};
/**
* A video.js plugin.
*
* In the plugin function, the value of `this` is a video.js `Player`
* instance. You cannot rely on the player being in a "ready" state here,
* depending on how the plugin is invoked. This may or may not be important
* 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(player, options = {}) {
player.ready(() => onPlayerReady(player));
player.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', function(options) {
eme(this, options);
});
export default eme;