(function(){ // Save platform functions that will be modified var _requestMediaKeySystemAccess = navigator.requestMediaKeySystemAccess.bind( navigator ), _setMediaKeys = HTMLMediaElement.prototype.setMediaKeys; // Allow us to modify the target of Events Object.defineProperties( Event.prototype, { target: { get: function() { return this._target || this.currentTarget; }, set: function( newtarget ) { this._target = newtarget; } } } ); var EventTarget = function(){ this.listeners = {}; }; EventTarget.prototype.listeners = null; EventTarget.prototype.addEventListener = function(type, callback){ if(!(type in this.listeners)) { this.listeners[type] = []; } this.listeners[type].push(callback); }; EventTarget.prototype.removeEventListener = function(type, callback){ if(!(type in this.listeners)) { return; } var stack = this.listeners[type]; for(var i = 0, l = stack.length; i < l; i++){ if(stack[i] === callback){ stack.splice(i, 1); return this.removeEventListener(type, callback); } } }; EventTarget.prototype.dispatchEvent = function(event){ if(!(event.type in this.listeners)) { return; } var stack = this.listeners[event.type]; event.target = this; for(var i = 0, l = stack.length; i < l; i++) { stack[i].call(this, event); } }; function MediaKeySystemAccessProxy( keysystem, access, configuration ) { this._keysystem = keysystem; this._access = access; this._configuration = configuration; } Object.defineProperties( MediaKeySystemAccessProxy.prototype, { keysystem: { get: function() { return this._keysystem; } } }); MediaKeySystemAccessProxy.prototype.getConfiguration = function getConfiguration() { return this._configuration; }; MediaKeySystemAccessProxy.prototype.createMediaKeys = function createMediaKeys() { return new Promise( function( resolve, reject ) { this._access.createMediaKeys() .then( function( mediaKeys ) { resolve( new MediaKeysProxy( mediaKeys ) ); }) .catch( function( error ) { reject( error ); } ); }.bind( this ) ); }; function MediaKeysProxy( mediaKeys ) { this._mediaKeys = mediaKeys; this._sessions = [ ]; this._videoelement = undefined; this._onTimeUpdateListener = MediaKeysProxy.prototype._onTimeUpdate.bind( this ); } MediaKeysProxy.prototype._setVideoElement = function _setVideoElement( videoElement ) { if ( videoElement !== this._videoelement ) { if ( this._videoelement ) { this._videoelement.removeEventListener( 'timeupdate', this._onTimeUpdateListener ); } this._videoelement = videoElement; if ( this._videoelement ) { this._videoelement.addEventListener( 'timeupdate', this._onTimeUpdateListener ); } } }; MediaKeysProxy.prototype._onTimeUpdate = function( event ) { this._sessions.forEach( function( session ) { if ( session._sessionType === 'persistent-usage-record' ) { session._onTimeUpdate( event ); } } ); }; MediaKeysProxy.prototype._removeSession = function _removeSession( session ) { var index = this._sessions.indexOf( session ); if ( index !== -1 ) this._sessions.splice( index, 1 ); }; MediaKeysProxy.prototype.createSession = function createSession( sessionType ) { if ( !sessionType || sessionType === 'temporary' ) return this._mediaKeys.createSession(); var session = new MediaKeySessionProxy( this, sessionType ); this._sessions.push( session ); return session; }; MediaKeysProxy.prototype.setServerCertificate = function setServerCertificate( certificate ) { return this._mediaKeys.setServerCertificate( certificate ); }; function MediaKeySessionProxy( mediaKeysProxy, sessionType ) { EventTarget.call( this ); this._mediaKeysProxy = mediaKeysProxy this._sessionType = sessionType; this._sessionId = ""; // MediaKeySessionProxy states // 'created' - After initial creation // 'loading' - Persistent license session waiting for key message to load stored keys // 'active' - Normal active state - proxy all key messages // 'removing' - Release message generated, waiting for ack // 'closed' - Session closed this._state = 'created'; this._closed = new Promise( function( resolve ) { this._resolveClosed = resolve; }.bind( this ) ); } MediaKeySessionProxy.prototype = Object.create( EventTarget.prototype ); Object.defineProperties( MediaKeySessionProxy.prototype, { sessionId: { get: function() { return this._sessionId; } }, expiration: { get: function() { return NaN; } }, closed: { get: function() { return this._closed; } }, keyStatuses:{ get: function() { return this._session.keyStatuses; } }, // TODO this will fail if examined too early _kids: { get: function() { return this._keys.map( function( key ) { return key.kid; } ); } }, }); MediaKeySessionProxy.prototype._createSession = function _createSession() { this._session = this._mediaKeysProxy._mediaKeys.createSession(); this._session.addEventListener( 'message', MediaKeySessionProxy.prototype._onMessage.bind( this ) ); this._session.addEventListener( 'keystatuseschange', MediaKeySessionProxy.prototype._onKeyStatusesChange.bind( this ) ); }; MediaKeySessionProxy.prototype._onMessage = function _onMessage( event ) { switch( this._state ) { case 'loading': this._session.update( toUtf8( { keys: this._keys } ) ) .then( function() { this._state = 'active'; this._loaded( true ); }.bind(this)).catch( this._loadfailed ); break; case 'active': this.dispatchEvent( event ); break; default: // Swallow the event break; } }; MediaKeySessionProxy.prototype._onKeyStatusesChange = function _onKeyStatusesChange( event ) { switch( this._state ) { case 'active' : case 'removing' : this.dispatchEvent( event ); break; default: // Swallow the event break; } }; MediaKeySessionProxy.prototype._onTimeUpdate = function _onTimeUpdate( event ) { if ( !this._firstTime ) this._firstTime = Date.now(); this._latestTime = Date.now(); this._store(); }; MediaKeySessionProxy.prototype._queueMessage = function _queueMessage( messageType, message ) { setTimeout( function() { var messageAsArray = toUtf8( message ).buffer; this.dispatchEvent( new MediaKeyMessageEvent( 'message', { messageType: messageType, message: messageAsArray } ) ); }.bind( this ) ); }; function _storageKey( sessionId ) { return sessionId; } MediaKeySessionProxy.prototype._store = function _store() { var data; if ( this._sessionType === 'persistent-usage-record' ) { data = { kids: this._kids }; if ( this._firstTime ) data.firstTime = this._firstTime; if ( this._latestTime ) data.latestTime = this._latestTime; } else { data = { keys: this._keys }; } window.localStorage.setItem( _storageKey( this._sessionId ), JSON.stringify( data ) ); }; MediaKeySessionProxy.prototype._load = function _load( sessionId ) { var store = window.localStorage.getItem( _storageKey( sessionId ) ); if ( store === null ) return false; var data; try { data = JSON.parse( store ) } catch( error ) { return false; } if ( data.kids ) { this._sessionType = 'persistent-usage-record'; this._keys = data.kids.map( function( kid ) { return { kid: kid }; } ); if ( data.firstTime ) this._firstTime = data.firstTime; if ( data.latestTime ) this._latestTime = data.latestTime; } else { this._sessionType = 'persistent-license'; this._keys = data.keys; } return true; }; MediaKeySessionProxy.prototype._clear = function _clear() { window.localStorage.removeItem( _storageKey( this._sessionId ) ); }; MediaKeySessionProxy.prototype.generateRequest = function generateRequest( initDataType, initData ) { if ( this._state !== 'created' ) return Promise.reject( new InvalidStateError() ); this._createSession(); this._state = 'active'; return this._session.generateRequest( initDataType, initData ) .then( function() { this._sessionId = Math.random().toString(36).slice(2); }.bind( this ) ); }; MediaKeySessionProxy.prototype.load = function load( sessionId ) { if ( this._state !== 'created' ) return Promise.reject( new InvalidStateError() ); return new Promise( function( resolve, reject ) { try { if ( !this._load( sessionId ) ) { resolve( false ); return; } this._sessionId = sessionId; if ( this._sessionType === 'persistent-usage-record' ) { var msg = { kids: this._kids }; if ( this._firstTime ) msg.firstTime = this._firstTime; if ( this._latestTime ) msg.latestTime = this._latestTime; this._queueMessage( 'license-release', msg ); this._state = 'removing'; resolve( true ); } else { this._createSession(); this._state = 'loading'; this._loaded = resolve; this._loadfailed = reject; var initData = { kids: this._kids }; this._session.generateRequest( 'keyids', toUtf8( initData ) ); } } catch( error ) { reject( error ); } }.bind( this ) ); }; MediaKeySessionProxy.prototype.update = function update( response ) { return new Promise( function( resolve, reject ) { switch( this._state ) { case 'active' : var message = fromUtf8( response ); // JSON Web Key Set this._keys = message.keys; this._store(); resolve( this._session.update( response ) ); break; case 'removing' : this._state = 'closed'; this._clear(); this._mediaKeysProxy._removeSession( this ); this._resolveClosed(); delete this._session; resolve(); break; default: reject( new InvalidStateError() ); } }.bind( this ) ); }; MediaKeySessionProxy.prototype.close = function close() { if ( this._state === 'closed' ) return Promise.resolve(); this._state = 'closed'; this._mediaKeysProxy._removeSession( this ); this._resolveClosed(); var session = this._session; if ( !session ) return Promise.resolve(); this._session = undefined; return session.close(); }; MediaKeySessionProxy.prototype.remove = function remove() { if ( this._state !== 'active' || !this._session ) return Promise.reject( new DOMException('InvalidStateError('+this._state+')') ); this._state = 'removing'; this._mediaKeysProxy._removeSession( this ); return this._session.close() .then( function() { var msg = { kids: this._kids }; if ( this._sessionType === 'persistent-usage-record' ) { if ( this._firstTime ) msg.firstTime = this._firstTime; if ( this._latestTime ) msg.latestTime = this._latestTime; } this._queueMessage( 'license-release', msg ); }.bind( this ) ) }; HTMLMediaElement.prototype.setMediaKeys = function setMediaKeys( mediaKeys ) { if ( mediaKeys instanceof MediaKeysProxy ) { mediaKeys._setVideoElement( this ); return _setMediaKeys.call( this, mediaKeys._mediaKeys ); } else { return _setMediaKeys.call( this, mediaKeys ); } }; navigator.requestMediaKeySystemAccess = function( keysystem, configurations ) { // First, see if this is supported by the platform return new Promise( function( resolve, reject ) { _requestMediaKeySystemAccess( keysystem, configurations ) .then( function( access ) { resolve( access ); } ) .catch( function( error ) { if ( error instanceof TypeError ) reject( error ); if ( keysystem !== 'org.w3.clearkey' ) reject( error ); if ( !configurations.some( is_persistent_configuration ) ) reject( error ); // Shallow copy the configurations, swapping out the labels and omitting the sessiontypes var configurations_copy = configurations.map( function( config, index ) { var config_copy = copy_configuration( config ); config_copy.label = index.toString(); return config_copy; } ); // And try again with these configurations _requestMediaKeySystemAccess( keysystem, configurations_copy ) .then( function( access ) { // Create the supported configuration based on the original request var configuration = access.getConfiguration(), original_configuration = configurations[ configuration.label ]; // If the original configuration did not need persistent session types, then we're done if ( !is_persistent_configuration( original_configuration ) ) resolve( access ); // Create the configuration that we will return var returned_configuration = copy_configuration( configuration ); if ( original_configuration.label ) returned_configuration.label = original_configuration; else delete returned_configuration.label; returned_configuration.sessionTypes = original_configuration.sessionTypes; resolve( new MediaKeySystemAccessProxy( keysystem, access, returned_configuration ) ); } ) .catch( function( error ) { reject( error ); } ); } ); } ); }; function is_persistent_configuration( configuration ) { return configuration.sessionTypes && ( configuration.sessionTypes.indexOf( 'persistent-usage-record' ) !== -1 || configuration.sessionTypes.indexOf( 'persistent-license' ) !== -1 ); } function copy_configuration( src ) { var dst = {}; [ 'label', 'initDataTypes', 'audioCapabilities', 'videoCapabilities', 'distinctiveIdenfifier', 'persistentState' ] .forEach( function( item ) { if ( src[item] ) dst[item] = src[item]; } ); return dst; } }());