summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/encrypted-media/polyfill/clearkey-polyfill.js
diff options
context:
space:
mode:
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.js510
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;
+ }
+}());