clearkey-polyfill.js (13825B)
1 (function(){ 2 3 // Save platform functions that will be modified 4 var _requestMediaKeySystemAccess = navigator.requestMediaKeySystemAccess.bind( navigator ), 5 _setMediaKeys = HTMLMediaElement.prototype.setMediaKeys; 6 7 // Allow us to modify the target of Events 8 Object.defineProperties( Event.prototype, { 9 target: { get: function() { return this._target || this.currentTarget; }, 10 set: function( newtarget ) { this._target = newtarget; } } 11 } ); 12 13 var EventTarget = function(){ 14 this.listeners = {}; 15 }; 16 17 EventTarget.prototype.listeners = null; 18 19 EventTarget.prototype.addEventListener = function(type, callback){ 20 if(!(type in this.listeners)) { 21 this.listeners[type] = []; 22 } 23 this.listeners[type].push(callback); 24 }; 25 26 EventTarget.prototype.removeEventListener = function(type, callback){ 27 if(!(type in this.listeners)) { 28 return; 29 } 30 var stack = this.listeners[type]; 31 for(var i = 0, l = stack.length; i < l; i++){ 32 if(stack[i] === callback){ 33 stack.splice(i, 1); 34 return this.removeEventListener(type, callback); 35 } 36 } 37 }; 38 39 EventTarget.prototype.dispatchEvent = function(event){ 40 if(!(event.type in this.listeners)) { 41 return; 42 } 43 var stack = this.listeners[event.type]; 44 event.target = this; 45 for(var i = 0, l = stack.length; i < l; i++) { 46 stack[i].call(this, event); 47 } 48 }; 49 50 function MediaKeySystemAccessProxy( keysystem, access, configuration ) 51 { 52 this._keysystem = keysystem; 53 this._access = access; 54 this._configuration = configuration; 55 } 56 57 Object.defineProperties( MediaKeySystemAccessProxy.prototype, { 58 keysystem: { get: function() { return this._keysystem; } } 59 }); 60 61 MediaKeySystemAccessProxy.prototype.getConfiguration = function getConfiguration() 62 { 63 return this._configuration; 64 }; 65 66 MediaKeySystemAccessProxy.prototype.createMediaKeys = function createMediaKeys() 67 { 68 return new Promise( function( resolve, reject ) { 69 70 this._access.createMediaKeys() 71 .then( function( mediaKeys ) { resolve( new MediaKeysProxy( mediaKeys ) ); }) 72 .catch( function( error ) { reject( error ); } ); 73 74 }.bind( this ) ); 75 }; 76 77 function MediaKeysProxy( mediaKeys ) 78 { 79 this._mediaKeys = mediaKeys; 80 this._sessions = [ ]; 81 this._videoelement = undefined; 82 } 83 84 MediaKeysProxy.prototype._setVideoElement = function _setVideoElement( videoElement ) 85 { 86 if ( videoElement !== this._videoelement ) 87 { 88 this._videoelement = videoElement; 89 } 90 }; 91 92 93 MediaKeysProxy.prototype._removeSession = function _removeSession( session ) 94 { 95 var index = this._sessions.indexOf( session ); 96 if ( index !== -1 ) this._sessions.splice( index, 1 ); 97 }; 98 99 MediaKeysProxy.prototype.createSession = function createSession( sessionType ) 100 { 101 if ( !sessionType || sessionType === 'temporary' ) return this._mediaKeys.createSession(); 102 103 var session = new MediaKeySessionProxy( this, sessionType ); 104 this._sessions.push( session ); 105 106 return session; 107 }; 108 109 MediaKeysProxy.prototype.setServerCertificate = function setServerCertificate( certificate ) 110 { 111 return this._mediaKeys.setServerCertificate( certificate ); 112 }; 113 114 function MediaKeySessionProxy( mediaKeysProxy, sessionType ) 115 { 116 EventTarget.call( this ); 117 118 this._mediaKeysProxy = mediaKeysProxy 119 this._sessionType = sessionType; 120 this._sessionId = ""; 121 122 // MediaKeySessionProxy states 123 // 'created' - After initial creation 124 // 'loading' - Persistent license session waiting for key message to load stored keys 125 // 'active' - Normal active state - proxy all key messages 126 // 'removing' - Release message generated, waiting for ack 127 // 'closed' - Session closed 128 this._state = 'created'; 129 130 this._closed = new Promise( function( resolve ) { this._resolveClosed = resolve; }.bind( this ) ); 131 } 132 133 MediaKeySessionProxy.prototype = Object.create( EventTarget.prototype ); 134 135 Object.defineProperties( MediaKeySessionProxy.prototype, { 136 137 sessionId: { get: function() { return this._sessionId; } }, 138 expiration: { get: function() { return NaN; } }, 139 closed: { get: function() { return this._closed; } }, 140 keyStatuses:{ get: function() { return this._session.keyStatuses; } }, // TODO this will fail if examined too early 141 _kids: { get: function() { return this._keys.map( function( key ) { return key.kid; } ); } }, 142 }); 143 144 MediaKeySessionProxy.prototype._createSession = function _createSession() 145 { 146 this._session = this._mediaKeysProxy._mediaKeys.createSession(); 147 148 this._session.addEventListener( 'message', MediaKeySessionProxy.prototype._onMessage.bind( this ) ); 149 this._session.addEventListener( 'keystatuseschange', MediaKeySessionProxy.prototype._onKeyStatusesChange.bind( this ) ); 150 }; 151 152 MediaKeySessionProxy.prototype._onMessage = function _onMessage( event ) 153 { 154 switch( this._state ) 155 { 156 case 'loading': 157 this._session.update( toUtf8( { keys: this._keys } ) ) 158 .then( function() { 159 this._state = 'active'; 160 this._loaded( true ); 161 }.bind(this)).catch( this._loadfailed ); 162 163 break; 164 165 case 'active': 166 this.dispatchEvent( event ); 167 break; 168 169 default: 170 // Swallow the event 171 break; 172 } 173 }; 174 175 MediaKeySessionProxy.prototype._onKeyStatusesChange = function _onKeyStatusesChange( event ) 176 { 177 switch( this._state ) 178 { 179 case 'active' : 180 case 'removing' : 181 this.dispatchEvent( event ); 182 break; 183 184 default: 185 // Swallow the event 186 break; 187 } 188 }; 189 190 MediaKeySessionProxy.prototype._queueMessage = function _queueMessage( messageType, message ) 191 { 192 setTimeout( function() { 193 194 var messageAsArray = toUtf8( message ).buffer; 195 196 this.dispatchEvent( new MediaKeyMessageEvent( 'message', { messageType: messageType, message: messageAsArray } ) ); 197 198 }.bind( this ) ); 199 }; 200 201 function _storageKey( sessionId ) 202 { 203 return sessionId; 204 } 205 206 MediaKeySessionProxy.prototype._store = function _store() 207 { 208 var data = { keys: this._keys }; 209 210 window.localStorage.setItem( _storageKey( this._sessionId ), JSON.stringify( data ) ); 211 }; 212 213 MediaKeySessionProxy.prototype._load = function _load( sessionId ) 214 { 215 var store = window.localStorage.getItem( _storageKey( sessionId ) ); 216 if ( store === null ) return false; 217 218 var data; 219 try { data = JSON.parse( store ) } catch( error ) { 220 return false; 221 } 222 223 this._sessionType = 'persistent-license'; 224 this._keys = data.keys; 225 226 return true; 227 }; 228 229 MediaKeySessionProxy.prototype._clear = function _clear() 230 { 231 window.localStorage.removeItem( _storageKey( this._sessionId ) ); 232 }; 233 234 MediaKeySessionProxy.prototype.generateRequest = function generateRequest( initDataType, initData ) 235 { 236 if ( this._state !== 'created' ) return Promise.reject( new InvalidStateError() ); 237 238 this._createSession(); 239 240 this._state = 'active'; 241 242 return this._session.generateRequest( initDataType, initData ) 243 .then( function() { 244 this._sessionId = Math.random().toString(36).slice(2); 245 }.bind( this ) ); 246 }; 247 248 MediaKeySessionProxy.prototype.load = function load( sessionId ) 249 { 250 if ( this._state !== 'created' ) return Promise.reject( new InvalidStateError() ); 251 252 return new Promise( function( resolve, reject ) { 253 254 try 255 { 256 if ( !this._load( sessionId ) ) 257 { 258 resolve( false ); 259 260 return; 261 } 262 263 this._sessionId = sessionId; 264 265 266 this._createSession(); 267 268 this._state = 'loading'; 269 this._loaded = resolve; 270 this._loadfailed = reject; 271 272 var initData = { kids: this._kids }; 273 274 this._session.generateRequest( 'keyids', toUtf8( initData ) ); 275 } 276 catch( error ) 277 { 278 reject( error ); 279 } 280 }.bind( this ) ); 281 }; 282 283 MediaKeySessionProxy.prototype.update = function update( response ) 284 { 285 return new Promise( function( resolve, reject ) { 286 287 switch( this._state ) { 288 289 case 'active' : 290 291 var message = fromUtf8( response ); 292 293 // JSON Web Key Set 294 this._keys = message.keys; 295 296 this._store(); 297 298 resolve( this._session.update( response ) ); 299 300 break; 301 302 case 'removing' : 303 304 this._state = 'closed'; 305 306 this._clear(); 307 308 this._mediaKeysProxy._removeSession( this ); 309 310 this._resolveClosed(); 311 312 delete this._session; 313 314 resolve(); 315 316 break; 317 318 default: 319 reject( new InvalidStateError() ); 320 } 321 322 }.bind( this ) ); 323 }; 324 325 MediaKeySessionProxy.prototype.close = function close() 326 { 327 if ( this._state === 'closed' ) return Promise.resolve(); 328 329 this._state = 'closed'; 330 331 this._mediaKeysProxy._removeSession( this ); 332 333 this._resolveClosed(); 334 335 var session = this._session; 336 if ( !session ) return Promise.resolve(); 337 338 this._session = undefined; 339 340 return session.close(); 341 }; 342 343 MediaKeySessionProxy.prototype.remove = function remove() 344 { 345 if ( this._state !== 'active' || !this._session ) return Promise.reject( new DOMException('InvalidStateError('+this._state+')') ); 346 347 this._state = 'removing'; 348 349 this._mediaKeysProxy._removeSession( this ); 350 351 return this._session.close() 352 .then( function() { 353 354 var msg = { kids: this._kids }; 355 356 this._queueMessage( 'license-release', msg ); 357 358 }.bind( this ) ) 359 }; 360 361 HTMLMediaElement.prototype.setMediaKeys = function setMediaKeys( mediaKeys ) 362 { 363 if ( mediaKeys instanceof MediaKeysProxy ) 364 { 365 mediaKeys._setVideoElement( this ); 366 return _setMediaKeys.call( this, mediaKeys._mediaKeys ); 367 } 368 else 369 { 370 return _setMediaKeys.call( this, mediaKeys ); 371 } 372 }; 373 374 navigator.requestMediaKeySystemAccess = function( keysystem, configurations ) 375 { 376 // First, see if this is supported by the platform 377 return new Promise( function( resolve, reject ) { 378 379 _requestMediaKeySystemAccess( keysystem, configurations ) 380 .then( function( access ) { resolve( access ); } ) 381 .catch( function( error ) { 382 383 if ( error instanceof TypeError ) reject( error ); 384 385 if ( keysystem !== 'org.w3.clearkey' ) reject( error ); 386 387 if ( !configurations.some( is_persistent_configuration ) ) reject( error ); 388 389 // Shallow copy the configurations, swapping out the labels and omitting the sessiontypes 390 var configurations_copy = configurations.map( function( config, index ) { 391 392 var config_copy = copy_configuration( config ); 393 config_copy.label = index.toString(); 394 return config_copy; 395 396 } ); 397 398 // And try again with these configurations 399 _requestMediaKeySystemAccess( keysystem, configurations_copy ) 400 .then( function( access ) { 401 402 // Create the supported configuration based on the original request 403 var configuration = access.getConfiguration(), 404 original_configuration = configurations[ configuration.label ]; 405 406 // If the original configuration did not need persistent session types, then we're done 407 if ( !is_persistent_configuration( original_configuration ) ) resolve( access ); 408 409 // Create the configuration that we will return 410 var returned_configuration = copy_configuration( configuration ); 411 412 if ( original_configuration.label ) 413 returned_configuration.label = original_configuration; 414 else 415 delete returned_configuration.label; 416 417 returned_configuration.sessionTypes = original_configuration.sessionTypes; 418 419 resolve( new MediaKeySystemAccessProxy( keysystem, access, returned_configuration ) ); 420 } ) 421 .catch( function( error ) { reject( error ); } ); 422 } ); 423 } ); 424 }; 425 426 function is_persistent_configuration( configuration ) 427 { 428 return configuration.sessionTypes && 429 ( configuration.sessionTypes.indexOf( 'persistent-license' ) !== -1 ); 430 } 431 432 function copy_configuration( src ) 433 { 434 var dst = {}; 435 [ 'label', 'initDataTypes', 'audioCapabilities', 'videoCapabilities', 'distinctiveIdenfifier', 'persistentState' ] 436 .forEach( function( item ) { if ( src[item] ) dst[item] = src[item]; } ); 437 return dst; 438 } 439 }());