Source code
Revision control
Copy as Markdown
Other Tools
(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;
}
}());