/** * Hooks EME calls and forwards them for analysis and decryption. * * Most of the code here was borrowed from https://github.com/google/eme_logger/blob/master/eme_listeners.js */ var lastReceivedLicenseRequest = null; var lastReceivedLicenseResponse = null; /** Set up the EME listeners. */ function startEMEInterception() { var listener = new EmeInterception(); listener.setUpListeners(); } /** * Gets called whenever an EME method is getting called or an EME event fires */ EmeInterception.onOperation = function(operationType, args) { if (operationType == "GenerateRequestCall") { // got initData // console.log(args); } else if (operationType == "MessageEvent") { var licenseRequest = args.message; lastReceivedLicenseRequest = licenseRequest; } else if (operationType == "UpdateCall") { var licenseResponse = args[0]; lastReceivedLicenseResponse = licenseResponse; // OK, let's try to decrypt it, assuming the response correlates to the request WidevineCrypto.decryptContentKey(lastReceivedLicenseRequest, lastReceivedLicenseResponse); } }; /** * Manager for EME event and method listeners. * @constructor */ function EmeInterception() { this.unprefixedEmeEnabled = Navigator.prototype.requestMediaKeySystemAccess ? true : false; this.prefixedEmeEnabled = HTMLMediaElement.prototype.webkitGenerateKeyRequest ? true : false; } /** * The number of types of HTML Media Elements to track. * @const {number} */ EmeInterception.NUM_MEDIA_ELEMENT_TYPES = 3; /** * Sets up EME listeners for whichever type of EME is enabled. */ EmeInterception.prototype.setUpListeners = function() { if (!this.unprefixedEmeEnabled && !this.prefixedEmeEnabled) { // EME is not enabled, just ignore return; } if (this.unprefixedEmeEnabled) { this.addListenersToNavigator_(); } if (this.prefixedEmeEnabled) { // Prefixed EME is enabled } this.addListenersToAllEmeElements_(); }; /** * Adds listeners to the EME methods on the Navigator object. * @private */ EmeInterception.prototype.addListenersToNavigator_ = function() { if (navigator.listenersAdded_) return; var originalRequestMediaKeySystemAccessFn = EmeInterception.extendEmeMethod( navigator, navigator.requestMediaKeySystemAccess, "RequestMediaKeySystemAccessCall"); navigator.requestMediaKeySystemAccess = function() { var options = arguments[1]; // slice "It is recommended that a robustness level be specified" warning var modifiedArguments = arguments; var modifiedOptions = EmeInterception.addRobustnessLevelIfNeeded(options); modifiedArguments[1] = modifiedOptions; var result = originalRequestMediaKeySystemAccessFn.apply(null, modifiedArguments); // Attach listeners to returned MediaKeySystemAccess object return result.then(function(mediaKeySystemAccess) { this.addListenersToMediaKeySystemAccess_(mediaKeySystemAccess); return Promise.resolve(mediaKeySystemAccess); }.bind(this)); }.bind(this); navigator.listenersAdded_ = true; }; /** * Adds listeners to the EME methods on a MediaKeySystemAccess object. * @param {MediaKeySystemAccess} mediaKeySystemAccess A MediaKeySystemAccess * object to add listeners to. * @private */ EmeInterception.prototype.addListenersToMediaKeySystemAccess_ = function(mediaKeySystemAccess) { if (mediaKeySystemAccess.listenersAdded_) { return; } mediaKeySystemAccess.originalGetConfiguration = mediaKeySystemAccess.getConfiguration; mediaKeySystemAccess.getConfiguration = EmeInterception.extendEmeMethod( mediaKeySystemAccess, mediaKeySystemAccess.getConfiguration, "GetConfigurationCall"); var originalCreateMediaKeysFn = EmeInterception.extendEmeMethod( mediaKeySystemAccess, mediaKeySystemAccess.createMediaKeys, "CreateMediaKeysCall"); mediaKeySystemAccess.createMediaKeys = function() { var result = originalCreateMediaKeysFn.apply(null, arguments); // Attach listeners to returned MediaKeys object return result.then(function(mediaKeys) { mediaKeys.keySystem_ = mediaKeySystemAccess.keySystem; this.addListenersToMediaKeys_(mediaKeys); return Promise.resolve(mediaKeys); }.bind(this)); }.bind(this); mediaKeySystemAccess.listenersAdded_ = true; }; /** * Adds listeners to the EME methods on a MediaKeys object. * @param {MediaKeys} mediaKeys A MediaKeys object to add listeners to. * @private */ EmeInterception.prototype.addListenersToMediaKeys_ = function(mediaKeys) { if (mediaKeys.listenersAdded_) { return; } var originalCreateSessionFn = EmeInterception.extendEmeMethod(mediaKeys, mediaKeys.createSession, "CreateSessionCall"); mediaKeys.createSession = function() { var result = originalCreateSessionFn.apply(null, arguments); result.keySystem_ = mediaKeys.keySystem_; // Attach listeners to returned MediaKeySession object this.addListenersToMediaKeySession_(result); return result; }.bind(this); mediaKeys.setServerCertificate = EmeInterception.extendEmeMethod(mediaKeys, mediaKeys.setServerCertificate, "SetServerCertificateCall"); mediaKeys.listenersAdded_ = true; }; /** Adds listeners to the EME methods and events on a MediaKeySession object. * @param {MediaKeySession} session A MediaKeySession object to add * listeners to. * @private */ EmeInterception.prototype.addListenersToMediaKeySession_ = function(session) { if (session.listenersAdded_) { return; } session.generateRequest = EmeInterception.extendEmeMethod(session,session.generateRequest, "GenerateRequestCall"); session.load = EmeInterception.extendEmeMethod(session, session.load, "LoadCall"); session.update = EmeInterception.extendEmeMethod(session,session.update, "UpdateCall"); session.close = EmeInterception.extendEmeMethod(session, session.close, "CloseCall"); session.remove = EmeInterception.extendEmeMethod(session, session.remove, "RemoveCall"); session.addEventListener('message', function(e) { e.keySystem = session.keySystem_; EmeInterception.interceptEvent("MessageEvent", e); }); session.addEventListener('keystatuseschange', EmeInterception.interceptEvent.bind(null, "KeyStatusesChangeEvent")); session.listenersAdded_ = true; }; /** * Adds listeners to all currently created media elements (audio, video) and sets up a * mutation-summary observer to add listeners to any newly created media * elements. * @private */ EmeInterception.prototype.addListenersToAllEmeElements_ = function() { this.addEmeInterceptionToInitialMediaElements_(); // TODO: Use MutationObserver directry // var observer = new MutationSummary({ // callback: function(summaries) { // applyListeners(summaries); // }, // queries: [{element: 'video'}, {element: 'audio'}, {element: 'media'}] // }); // var applyListeners = function(summaries) { // for (var i = 0; i < EmeInterception.NUM_MEDIA_ELEMENT_TYPES; i++) { // var elements = summaries[i]; // elements.added.forEach(function(element) { // this.addListenersToEmeElement_(element, true); // }.bind(this)); // } // }.bind(this); }; /** * Adds listeners to the EME elements currently in the document. * @private */ EmeInterception.prototype.addEmeInterceptionToInitialMediaElements_ = function() { var audioElements = document.getElementsByTagName('audio'); for (var i = 0; i < audioElements.length; ++i) { this.addListenersToEmeElement_(audioElements[i], false); } var videoElements = document.getElementsByTagName('video'); for (var i = 0; i < videoElements.length; ++i) { this.addListenersToEmeElement_(videoElements[i], false); } var mediaElements = document.getElementsByTagName('media'); for (var i = 0; i < mediaElements.length; ++i) { this.addListenersToEmeElement_(mediaElements[i], false); } }; /** * Adds method and event listeners to media element. * @param {HTMLMediaElement} element A HTMLMedia element to add listeners to. * @private */ EmeInterception.prototype.addListenersToEmeElement_ = function(element) { this.addEmeEventListeners_(element); this.addEmeMethodListeners_(element); console.info('EME listeners successfully added to:', element); }; /** * Adds event listeners to a media element. * @param {HTMLMediaElement} element A HTMLMedia element to add listeners to. * @private */ EmeInterception.prototype.addEmeEventListeners_ = function(element) { if (element.eventListenersAdded_) { return; } if (this.prefixedEmeEnabled) { element.addEventListener('webkitneedkey', EmeInterception.interceptEvent.bind(null, "NeedKeyEvent")); element.addEventListener('webkitkeymessage', EmeInterception.interceptEvent.bind(null, "KeyMessageEvent")); element.addEventListener('webkitkeyadded', EmeInterception.interceptEvent.bind(null, "KeyAddedEvent")); element.addEventListener('webkitkeyerror', EmeInterception.interceptEvent.bind(null, "KeyErrorEvent")); } element.addEventListener('encrypted', EmeInterception.interceptEvent.bind(null, "EncryptedEvent")); element.addEventListener('play', EmeInterception.interceptEvent.bind(null, "PlayEvent")); element.addEventListener('error', function(e) { console.error('Error Event'); EmeInterception.interceptEvent("ErrorEvent", e); }); element.eventListenersAdded_ = true; }; /** * Adds method listeners to a media element. * @param {HTMLMediaElement} element A HTMLMedia element to add listeners to. * @private */ EmeInterception.prototype.addEmeMethodListeners_ = function(element) { if (element.methodListenersAdded_) { return; } element.play = EmeInterception.extendEmeMethod(element, element.play, "PlayCall"); if (this.prefixedEmeEnabled) { element.canPlayType = EmeInterception.extendEmeMethod(element, element.canPlayType, "CanPlayTypeCall"); element.webkitGenerateKeyRequest = EmeInterception.extendEmeMethod(element, element.webkitGenerateKeyRequest, "GenerateKeyRequestCall"); element.webkitAddKey = EmeInterception.extendEmeMethod(element, element.webkitAddKey, "AddKeyCall"); element.webkitCancelKeyRequest = EmeInterception.extendEmeMethod(element, element.webkitCancelKeyRequest, "CancelKeyRequestCall"); } if (this.unprefixedEmeEnabled) { element.setMediaKeys = EmeInterception.extendEmeMethod(element, element.setMediaKeys, "SetMediaKeysCall"); } element.methodListenersAdded_ = true; }; /** * Creates a wrapper function that logs calls to the given method. * @param {!Object} element An element or object whose function * call will be logged. * @param {!Function} originalFn The function to log. * @param {!Function} type The constructor for a logger class that will * be instantiated to log the originalFn call. * @return {!Function} The new version, with logging, of orginalFn. */ EmeInterception.extendEmeMethod = function(element, originalFn, type) { return function() { try { var result = originalFn.apply(element, arguments); var args = [].slice.call(arguments); EmeInterception.interceptCall(type, args, result, element); } catch (e) { console.error(e); } return result; }; }; /** * Intercepts a method call to the console and a separate frame. * @param {!Function} constructor The constructor for a logger class that will * be instantiated to log this call. * @param {Array} args The arguments this call was made with. * @param {Object} result The result of this method call. * @param {!Object} target The element this method was called on. * @return {!eme.EmeMethodCall} The data that has been logged. */ EmeInterception.interceptCall = function(type, args, result, target) { EmeInterception.onOperation(type, args); return args; }; /** * Intercepts an event to the console and a separate frame. * @param {!Function} constructor The constructor for a logger class that will * be instantiated to log this event. * @param {!Event} event An EME event. * @return {!eme.EmeEvent} The data that has been logged. */ EmeInterception.interceptEvent = function(type, event) { EmeInterception.onOperation(type, event); return event; }; EmeInterception.addRobustnessLevelIfNeeded = function(options) { for (var i = 0; i < options.length; i++) { var option = options[i]; var videoCapabilities = option["videoCapabilities"]; var audioCapabilties = option["audioCapabilities"]; if (videoCapabilities != null) { for (var j = 0; j < videoCapabilities.length; j++) if (videoCapabilities[j]["robustness"] == undefined) videoCapabilities[j]["robustness"] = "SW_SECURE_CRYPTO"; } if (audioCapabilties != null) { for (var j = 0; j < audioCapabilties.length; j++) if (audioCapabilties[j]["robustness"] == undefined) audioCapabilties[j]["robustness"] = "SW_SECURE_CRYPTO"; } option["videoCapabilities"] = videoCapabilities; option["audioCapabilities"] = audioCapabilties; options[i] = option; } return options; } startEMEInterception();