diff options
Diffstat (limited to 'testing/web-platform/tests/encrypted-media/polyfill/clearkey-polyfill.js')
-rw-r--r-- | testing/web-platform/tests/encrypted-media/polyfill/clearkey-polyfill.js | 510 |
1 files changed, 510 insertions, 0 deletions
diff --git a/testing/web-platform/tests/encrypted-media/polyfill/clearkey-polyfill.js b/testing/web-platform/tests/encrypted-media/polyfill/clearkey-polyfill.js new file mode 100644 index 000000000..057ea3e03 --- /dev/null +++ b/testing/web-platform/tests/encrypted-media/polyfill/clearkey-polyfill.js @@ -0,0 +1,510 @@ +(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; + } +}()); |