diff options
Diffstat (limited to '')
7 files changed, 1019 insertions, 0 deletions
diff --git a/testing/web-platform/tests/encrypted-media/polyfill/cast-polyfill.js b/testing/web-platform/tests/encrypted-media/polyfill/cast-polyfill.js new file mode 100644 index 0000000000..576e0ad040 --- /dev/null +++ b/testing/web-platform/tests/encrypted-media/polyfill/cast-polyfill.js @@ -0,0 +1,80 @@ +(function() { + + if ( /CrKey\/[0-9]+\.[0-9a-z]+\.[0-9a-z]+/i.exec( navigator.userAgent ) ) { + + var castscript = document.createElement('script'); + castscript.type = 'text/javascript'; + castscript.src = 'https://www.gstatic.com/cast/sdk/libs/receiver/2.0.0/cast_receiver.js' + document.head.appendChild( castscript ); + + var _requestMediaKeySystemAccess = navigator.requestMediaKeySystemAccess.bind( navigator ), + _setMediaKeys = HTMLMediaElement.prototype.setMediaKeys, + _load = MediaKeySession.prototype.load; + + MediaKeySession.prototype.load = function load() + { + return _load.call( this ).then( function( success ) + { + return success ? this.remove() : false; + }.bind( this ) ); + }; + + function MediaKeys( mediaKeys ) + { + this._mediaKeys = mediaKeys; + } + + MediaKeys.prototype.setServerCertificate = function setServerCertificate( certificate ) + { + return this._mediaKeys.setServerCertificate( certificate ); + }; + + MediaKeys.prototype.createSession = function createSession( sessionType ) { + + if ( sessionType === 'persistent-usage-record' ) + { + return cast.receiver.eme.KeySession.createSession( this._mediaKeys, 'persistent-release-message' ); + } + + return this._mediaKeys.createSession( sessionType ); + }; + + function MediaKeySystemAccess( access ) + { + this._access = mediaKeySystemAccess; + } + + Object.defineProperty( MediaKeySystemAccess.prototype, 'keySystem', { get: function() { return this._access.keySystem; } } ); + + MediaKeySystemAccess.prototype.getConfiguration = function getConfiguration() { return this._access.getConfiguration(); }; + + MediaKeySystemAccess.prototype.createMediaKeys = function createMediaKeys() { + + return this._access.createMediaKey().then( function( mediaKeys ) { return new MediaKeys( mediaKeys ); } ); + + }; + + HTMLMediaElement.prototype.setMediaKeys = function setMediaKeys( mediaKeys ) + { + if ( mediaKeys instanceof MediaKeys ) + { + return _setMediaKeys.call( this, mediaKeys._mediaKeys ); + } + else + { + return _setMediaKeys.call( this, mediaKeys ); + } + }; + + navigator.requestMediaKeySystemAccess = function requestMediaKeySystemAccess( keysystem, supportedConfigurations ) { + + if ( keysystem !== 'com.chromecast.playready' ) + { + return _requestMediaKeySystemAccess( keysystem, supportedConfigurations ); + } + + return _requestMediaKeySystemAccess( keysystem, supportedConfigurations ) + .then( function( access ) { return new MediaKeySystemAccess( access ); } ); + }; + } +})();
\ No newline at end of file diff --git a/testing/web-platform/tests/encrypted-media/polyfill/chrome-polyfill.js b/testing/web-platform/tests/encrypted-media/polyfill/chrome-polyfill.js new file mode 100644 index 0000000000..2f11497cca --- /dev/null +++ b/testing/web-platform/tests/encrypted-media/polyfill/chrome-polyfill.js @@ -0,0 +1,37 @@ +(function(){ + if( navigator.userAgent.toLowerCase().indexOf('edge') === -1 + && navigator.userAgent.toLowerCase().indexOf('chrome') > -1){ + + if ( ( /chrome\/([0-9]*)\./.exec( navigator.userAgent.toLowerCase() )[1] | 0 ) < 54 ) { + + // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=622956 + // Chrome does not fire the empty keystatuschange event when a session is closed + var _mediaKeySessionClose = MediaKeySession.prototype.close; + var _mediaKeySessionKeyStatusesGetter = Object.getOwnPropertyDescriptor( MediaKeySession.prototype, 'keyStatuses' ).get; + var _emptyMediaKeyStatusMap = { size: 0, + has: function() { return false; }, + get: function() { return undefined; }, + entries:function() { return []; }, // this may not be correct, I think it should be some iterator thing + keys: function() { return []; }, + values: function() { return []; }, + forEach:function() { return; } }; + + MediaKeySession.prototype.close = function close() + { + this.__closed = true; + + setTimeout( function() { + this.dispatchEvent( new Event( 'keystatuseschange' ) ); + }.bind( this ), 0 ); + + return _mediaKeySessionClose.call( this ); + }; + + Object.defineProperty( MediaKeySession.prototype, 'keyStatuses', { get: function() { + + return this.__closed ? _emptyMediaKeyStatusMap : _mediaKeySessionKeyStatusesGetter.call( this ); + + } } ); + } + } +}()); 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 0000000000..057ea3e030 --- /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; + } +}()); diff --git a/testing/web-platform/tests/encrypted-media/polyfill/edge-keystatuses.js b/testing/web-platform/tests/encrypted-media/polyfill/edge-keystatuses.js new file mode 100644 index 0000000000..8861444591 --- /dev/null +++ b/testing/web-platform/tests/encrypted-media/polyfill/edge-keystatuses.js @@ -0,0 +1,144 @@ +(function() { + + // This polyfill fixes the following problems with Edge browser + // (1) Various maplike methods for keystatuses are not supported or suported incorrectly + // (2) Key Ids exposed in keystatuses are incorrect (byte swaps) + if ( navigator.userAgent.toLowerCase().indexOf('edge') > -1 ) { + /////////////////////////////////////////////////////////////////////////////////////////////// + // The following function is the core of this JS patch. The rest of this file is infrastructure + // required to enable this function + /////////////////////////////////////////////////////////////////////////////////////////////// + function _proxyKeyStatusesChange( event ) { + this._keyStatuses.clear(); + var keyStatuses = []; + this._session.keyStatuses.forEach( function( keyId, status ) { + var newKeyId = new Uint8Array( keyId ); + + function swap( arr, a, b ) { var t = arr[a]; arr[a] = arr[b]; arr[b] = t; } + swap( newKeyId, 0, 3 ); + swap( newKeyId, 1, 2 ); + swap( newKeyId, 4, 5 ); + swap( newKeyId, 6, 7 ); + + keyStatuses.push( { key: newKeyId, status: status, ord: arrayBufferAsString( newKeyId ) } ); + }); + + function lexicographical( a, b ) { return a < b ? -1 : a === b ? 0 : +1; } + function lexicographicalkey( a, b ) { return lexicographical( a.ord, b.ord ); } + + keyStatuses.sort( lexicographicalkey ).forEach( function( obj ) { + this._keyStatuses._set( obj.key, obj.status ); + }.bind( this ) ); + + this.dispatchEvent( event ); + }; + /////////////////////////////////////////////////////////////////////////////////////////////// + + // Override MediaKeys.createSession + var _mediaKeysCreateSession = MediaKeys.prototype.createSession; + MediaKeys.prototype.createSession = function ( sessionType ) { + return new MediaKeySession( _mediaKeysCreateSession.call( this, sessionType ) ); + }; + + // MediaKeySession proxy + function MediaKeySession( session ) { + EventTarget.call( this ); + this._session = session; + this._keyStatuses = new MediaKeyStatusMap(); + this._session.addEventListener("keystatuseschange",this._onKeyStatusesChange.bind(this)); + this._session.addEventListener("message",this.dispatchEvent.bind(this)); + } + + MediaKeySession.prototype = Object.create( EventTarget.prototype ); + + Object.defineProperties( MediaKeySession.prototype, { + sessionId: { get: function() { return this._session.sessionId; } }, + expiration: { get: function() { return this._session.expiration; } }, + closed: { get: function() { return this._session.closed; } }, + keyStatuses:{ get: function() { return this._keyStatuses; } } + }); + + [ "generateRequest", "load", "update", "remove", "close" ].forEach( function( fnname ) { + MediaKeySession.prototype[ fnname ] = function() { + return window.MediaKeySession.prototype[ fnname ].apply( this._session, arguments ); + } + } ); + + MediaKeySession.prototype._onKeyStatusesChange = _proxyKeyStatusesChange; + + // MediaKeyStatusMap proxy + // + // We need a proxy class to replace the broken MediaKeyStatusMap one. We cannot use a + // regular Map directly because we need get and has methods to compare by value not + // as references. + function MediaKeyStatusMap() { this._map = new Map(); } + + Object.defineProperties( MediaKeyStatusMap.prototype, { + size: { get: function() { return this._map.size; } }, + forEach: { get: function() { return function( f ) { return this._map.forEach( f ); } } }, + entries: { get: function() { return function() { return this._map.entries(); } } }, + values: { get: function() { return function() { return this._map.values(); } } }, + keys: { get: function() { return function() { return this._map.keys(); } } }, + clear: { get: function() { return function() { return this._map.clear(); } } } } ); + + MediaKeyStatusMap.prototype[ Symbol.iterator ] = function() { return this._map[ Symbol.iterator ]() }; + + MediaKeyStatusMap.prototype.has = function has( keyId ) { + for ( var k of this._map.keys() ) { if ( arrayBufferEqual( k, keyId ) ) return true; } + return false; + }; + + MediaKeyStatusMap.prototype.get = function get( keyId ) { + for ( var k of this._map.entries() ) { if ( arrayBufferEqual( k[ 0 ], keyId ) ) return k[ 1 ]; } + }; + + MediaKeyStatusMap.prototype._set = function _set( keyId, status ) { + this._map.set( new Uint8Array( keyId ), status ); + }; + + function arrayBufferEqual(buf1, buf2) + { + if (buf1.byteLength !== buf2.byteLength) return false; + var a1 = Array.from( new Int8Array(buf1) ), a2 = Array.from( new Int8Array(buf2) ); + return a1.every( function( x, i ) { return x === a2[i]; } ); + } + + // EventTarget + function EventTarget(){ + 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); + } + }; + } +})(); diff --git a/testing/web-platform/tests/encrypted-media/polyfill/edge-persistent-usage-record.js b/testing/web-platform/tests/encrypted-media/polyfill/edge-persistent-usage-record.js new file mode 100644 index 0000000000..7f86f0c058 --- /dev/null +++ b/testing/web-platform/tests/encrypted-media/polyfill/edge-persistent-usage-record.js @@ -0,0 +1,193 @@ +(function() { + + // This polyfill fixes the following problems with Edge browser + // (1) To retrieve a persisted usage record, you must use session type 'persistent-release-message' instead of 'persistent-usage-record' + // (2) To retrieve a persisted usage record, you must call remove() after calling load() + // (3) On providing a license release acknowledgement, the session does not automatically close as is should + // (4) Retrieval of the usage record at the end of an active session is not supported + + if ( navigator.userAgent.toLowerCase().indexOf('edge') > -1 ) { + + var _mediaKeySystemAccessCreateMediaKeys = MediaKeySystemAccess.prototype.createMediaKeys; + _mediaKeysCreateSession = MediaKeys.prototype.createSession; + + // MediaKeySession proxy + function MediaKeySession( mediaKeys, session ) + { + EventTarget.call( this ); + + this._mediaKeys = mediaKeys; + this._session = session; + this._sessionId = undefined; + this._removing = false; + + session.addEventListener( 'message', this.dispatchEvent.bind( this ) ); + session.addEventListener( 'keystatuseschange', this.dispatchEvent.bind( this ) ); + session.closed.then( function() { if ( !this._removing ) this._resolveClosed(); }.bind ( this ) ); + + this._closed = new Promise( function( resolve ) { this._resolveClosed = resolve; }.bind( this ) ); + } + + MediaKeySession.prototype = Object.create( EventTarget.prototype ); + + Object.defineProperties( MediaKeySession.prototype, { + sessionId: { get: function() { return this._sessionId ? this._sessionId : this._session.sessionId; } }, + expiration: { get: function() { return this._session.expiration; } }, + closed: { get: function() { return this._closed; } }, + keyStatuses:{ get: function() { return this._session.keyStatuses; } } + }); + + // load() + // + // Use a surrogate 'persistent-release-message' session to obtain the release message + // + MediaKeySession.prototype.load = function load( sessionId ) + { + if ( this.sessionId ) return Promise.reject( new DOMException('InvalidAccessError') ); + + this._surrogate = this._mediaKeys.createSession( 'persistent-release-message' ); + this._surrogate.addEventListener( 'message', this.dispatchEvent.bind( this ) ); + + return this._surrogate.load( sessionId ).then( function( success ) { + if (!success) return false; + + this._sessionId = sessionId; + this._removing = true; + this._session.close(); + + return this._surrogate.remove().then( function() { return true; } ); + }.bind( this ) ); + }; + + // remove() + // + // On an existing session, use a surrogate 'persistent-release-message' session to obtain the release message + // + MediaKeySession.prototype.remove = function remove() + { + if ( this._sessionId !== undefined ) return Promise.reject( new DOMException('InvalidAccessError') ); + if ( this.sessionId === undefined ) return Promise.reject( new DOMException('InvalidAccessError') ); + + this._surrogate = this._mediaKeys.createSession( 'persistent-release-message' ); + this._surrogate.addEventListener( 'message', this.dispatchEvent.bind( this ) ); + this._removing = true; + this._sessionId = this._session.sessionId; + + var self = this; + + return Promise.all( [ self._session.close(), self._session.closed ] ).then( function() { + return self._surrogate.load( self._sessionId ); + }).then( function( success ) { + if ( !success ) { + throw new DOMException('InvalidAccessError'); + } + + return self._surrogate.remove(); + }).then( function() { return true; } ); + } + + // update() + // + // For a normal session, pass through, otherwise update the surrogate and close the proxy + MediaKeySession.prototype.update = function update( message ) + { + if ( !this._removing ) return this._session.update( message ); + + return this._surrogate.update( message ).then( function() { + this._sessionId = undefined; + this._resolveClosed(); + }.bind( this ) ); + }; + + // close() - pass through + // + MediaKeySession.prototype.close = function close() + { + if ( !this._removing ) return this._session.close(); + this._resolveClosed(); + return Promise.resolve(); + }; + + // generateRequest() - pass through + // + MediaKeySession.prototype.generateRequest = function generateRequest( initDataType, initData ) + { + if ( this.sessionId ) Promise.reject( new DOMException('InvalidAccessError') ); + return this._session.generateRequest( initDataType, initData ); + }; + + // Wrap PlayReady persistent-usage-record sessions in our Proxy + MediaKeys.prototype.createSession = function createSession( sessionType ) { + + var session = _mediaKeysCreateSession.call( this, sessionType ); + if ( this._keySystem !== 'com.microsoft.playready' || sessionType !== 'persistent-usage-record' ) + { + return session; + } + + return new MediaKeySession( this, session ); + + }; + + // + // Annotation polyfills - annotate not otherwise available data + // + + // Annotate MediaKeys with the keysystem + MediaKeySystemAccess.prototype.createMediaKeys = function createMediaKeys() + { + return _mediaKeySystemAccessCreateMediaKeys.call( this ).then( function( mediaKeys ) { + mediaKeys._keySystem = this.keySystem; + return mediaKeys; + }.bind( this ) ); + }; + + // + // Utilities + // + + // 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; } } + } ); + + // Make an EventTarget base class + function EventTarget(){ + 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); + } + }; + } +})(); diff --git a/testing/web-platform/tests/encrypted-media/polyfill/firefox-polyfill.js b/testing/web-platform/tests/encrypted-media/polyfill/firefox-polyfill.js new file mode 100644 index 0000000000..ce241af362 --- /dev/null +++ b/testing/web-platform/tests/encrypted-media/polyfill/firefox-polyfill.js @@ -0,0 +1,23 @@ +(function(){ + if(navigator.userAgent.toLowerCase().indexOf('firefox') > -1){ + + // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=1282142 + // Firefox does not correctly reject the Clear Key session types it does not support + var _requestMediaKeySystemAccess = navigator.requestMediaKeySystemAccess.bind( navigator ); + + navigator.requestMediaKeySystemAccess = function( keysystem, configurations ) + { + if ( keysystem !== 'org.w3.clearkey' ) return _requestMediaKeySystemAccess( keysystem, configurations ); + + var supported_configurations = configurations.filter( function( c ) { + + return !c.sessionTypes || ( c.sessionTypes.length === 1 && c.sessionTypes[ 0 ] === 'temporary' ); + + } ); + + if ( supported_configurations.length === 0 ) return Promise.reject( new DOMException( 'None of the requested configurations were supported.' ) ); + + return _requestMediaKeySystemAccess( keysystem, supported_configurations ); + } + } +}());
\ No newline at end of file diff --git a/testing/web-platform/tests/encrypted-media/polyfill/make-polyfill-tests.py b/testing/web-platform/tests/encrypted-media/polyfill/make-polyfill-tests.py new file mode 100644 index 0000000000..532037e2a3 --- /dev/null +++ b/testing/web-platform/tests/encrypted-media/polyfill/make-polyfill-tests.py @@ -0,0 +1,32 @@ +#!/usr/bin/python + +from __future__ import print_function + +import os, re, os.path, glob + +head = re.compile( r"^(\s*</head>)", re.MULTILINE ) +runtest = re.compile( r"runTest\(\s*(\S.*?)\s*\)", re.DOTALL ) + +scripts = ''' + <!-- Polyfill files (NOTE: These are added by auto-generation script) --> + <script src=/encrypted-media/polyfill/chrome-polyfill.js></script> + <script src=/encrypted-media/polyfill/firefox-polyfill.js></script> + <script src=/encrypted-media/polyfill/edge-persistent-usage-record.js></script> + <script src=/encrypted-media/polyfill/edge-keystatuses.js></script> + <script src=/encrypted-media/polyfill/clearkey-polyfill.js></script>''' + +def process_file( infile, outfile ) : + with open( outfile, "w" ) as output : + with open( infile, "r" ) as input : + output.write( runtest.sub( r"runTest( \1, 'polyfill: ' )", head.sub( scripts + r"\1", input.read() ) ) ) + +if __name__ == '__main__' : + if (not os.getcwd().endswith('polyfill')) : + print("Please run from polyfill directory") + exit( 1 ) + + for infile in glob.glob( "../*.html" ) : + process_file( infile, os.path.basename( infile ) ) + + for infile in glob.glob( "../resources/*.html" ) : + process_file( infile, os.path.join( "resources", os.path.basename( infile ) ) ) |