| (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; |
| } |
| }()); |