@@ -48,6 +48,8 @@ const NEEDKEY_BEFORE_INITIALIZE_TIMEOUT = 500;
4848const LICENSE_SERVER_REQUEST_RETRIES = 3 ;
4949const LICENSE_SERVER_REQUEST_RETRY_INTERVAL = 1000 ;
5050const LICENSE_SERVER_REQUEST_DEFAULT_TIMEOUT = 8000 ;
51+ const CERTIFICATE_REQUEST_RETRY_INTERVAL = 500 ;
52+ const CERTIFICATE_REQUEST_DEFAULT_TIMEOUT = 8000 ;
5153
5254/**
5355 * @module ProtectionController
@@ -255,13 +257,17 @@ function ProtectionController(config) {
255257 if ( ! selectedKeySystem ) { return ; }
256258 const ksString = selectedKeySystem . systemString ;
257259 const cacheEntry = certificateCache . get ( ksString ) ;
258- if ( cacheEntry && cacheEntry . applied ) { return ; }
260+ if ( cacheEntry && ( cacheEntry . applied || cacheEntry . inProgress ) ) { return ; }
259261 // Gather certUrls from collected mediaInfoArr contentProtection entries matching this key system
260262 const preferredType = settings . get ( ) . streaming . protection . preferredCertType ;
261263 const certCandidates = _collectCertificateUrlsForSelectedKeySystem ( preferredType ) ;
262264 if ( ! certCandidates . length ) { return ; }
263- // TODO: implement actual certificate acquisition logic (fetch, retry, set via protectionModel.setServerCertificate())
264- logger . debug ( 'DRM: Found ' + certCandidates . length + ' certificate candidate(s) for ' + ksString + ' (download logic to be implemented).' ) ;
265+ logger . debug ( 'DRM: Found ' + certCandidates . length + ' certificate candidate(s) for ' + ksString + '. Starting acquisition.' ) ;
266+ const protData = _getProtDataForKeySystem ( selectedKeySystem ) || { } ;
267+ const entry = cacheEntry || { applied : false , inProgress : true , attempts : 0 } ;
268+ entry . inProgress = true ;
269+ certificateCache . set ( ksString , entry ) ;
270+ _fetchAndApplyCertificateSequentially ( certCandidates , 0 , protData , ksString , entry ) ;
265271 }
266272
267273 /**
@@ -304,6 +310,130 @@ function ProtectionController(config) {
304310 } ) ;
305311 }
306312
313+ /**
314+ * Sequentially try to download and apply a certificate from the candidates list.
315+ * @param {Array<{url:string, certType:string|null}> } candidates
316+ * @param {number } index
317+ * @param {object } protData
318+ * @param {string } ksString
319+ * @param {object } cacheEntry
320+ * @private
321+ */
322+ function _fetchAndApplyCertificateSequentially ( candidates , index , protData , ksString , cacheEntry ) {
323+ if ( index >= candidates . length ) {
324+ cacheEntry . inProgress = false ;
325+ cacheEntry . error = cacheEntry . error || 'All certificate candidates failed' ;
326+ logger . warn ( 'DRM: All certificate candidates failed for ' + ksString + '.' ) ;
327+ return ;
328+ }
329+ const candidate = candidates [ index ] ;
330+ const retryAttempts = settings . get ( ) . streaming . protection . certificateRetryAttempts ;
331+ logger . debug ( 'DRM: Attempting certificate download (' + ( index + 1 ) + '/' + candidates . length + ') url=' + candidate . url ) ;
332+ _downloadCertificate ( candidate . url , protData , retryAttempts )
333+ . then ( ( arrayBuffer ) => {
334+ if ( ! arrayBuffer || ! arrayBuffer . byteLength ) {
335+ throw new Error ( 'Empty certificate response' ) ;
336+ }
337+ cacheEntry . urlUsed = candidate . url ;
338+ cacheEntry . buffer = arrayBuffer ;
339+ return _applyServerCertificate ( arrayBuffer , ksString , cacheEntry ) ;
340+ } )
341+ . then ( ( applied ) => {
342+ if ( applied ) {
343+ cacheEntry . applied = true ;
344+ cacheEntry . inProgress = false ;
345+ logger . info ( 'DRM: Server certificate applied successfully from ' + cacheEntry . urlUsed + ' for ' + ksString + '.' ) ;
346+ } else {
347+ throw new Error ( 'CDM rejected certificate' ) ;
348+ }
349+ } )
350+ . catch ( ( err ) => {
351+ cacheEntry . attempts ++ ;
352+ cacheEntry . lastError = err && err . message ? err . message : err ;
353+ logger . warn ( 'DRM: Certificate attempt failed (' + candidate . url + '): ' + cacheEntry . lastError ) ;
354+ // Try next candidate
355+ _fetchAndApplyCertificateSequentially ( candidates , index + 1 , protData , ksString , cacheEntry ) ;
356+ } ) ;
357+ }
358+
359+ /**
360+ * Download a certificate binary with retries.
361+ * @param {string } url
362+ * @param {object } protData
363+ * @param {number } retries
364+ * @return {Promise<ArrayBuffer> }
365+ * @private
366+ */
367+ function _downloadCertificate ( url , protData , retries ) {
368+ return new Promise ( ( resolve , reject ) => {
369+ const xhr = new XMLHttpRequest ( ) ;
370+ xhr . open ( 'GET' , url , true ) ;
371+ xhr . responseType = 'arraybuffer' ;
372+ const timeout = protData && ! isNaN ( protData . httpTimeout ) ? protData . httpTimeout : CERTIFICATE_REQUEST_DEFAULT_TIMEOUT ;
373+ if ( timeout > 0 ) { xhr . timeout = timeout ; }
374+ let withCredentials = false ;
375+ if ( protData && typeof protData . withCredentials === 'boolean' ) { withCredentials = protData . withCredentials ; }
376+ xhr . withCredentials = withCredentials ;
377+ if ( protData && protData . httpRequestHeaders ) {
378+ Object . keys ( protData . httpRequestHeaders ) . forEach ( h => {
379+ xhr . setRequestHeader ( h , protData . httpRequestHeaders [ h ] ) ;
380+ } ) ;
381+ }
382+ const attemptFail = ( reason ) => {
383+ if ( retries > 0 ) {
384+ const remaining = retries - 1 ;
385+ logger . debug ( 'DRM: Certificate request failed (' + reason + '). Retrying... remaining=' + remaining ) ;
386+ setTimeout ( ( ) => {
387+ _downloadCertificate ( url , protData , remaining ) . then ( resolve ) . catch ( reject ) ;
388+ } , CERTIFICATE_REQUEST_RETRY_INTERVAL ) ;
389+ } else {
390+ reject ( new Error ( reason ) ) ;
391+ }
392+ } ;
393+ xhr . onload = function ( ) {
394+ if ( this . status >= 200 && this . status <= 299 ) {
395+ resolve ( this . response ) ;
396+ } else {
397+ attemptFail ( 'HTTP ' + this . status ) ;
398+ }
399+ } ;
400+ xhr . onerror = function ( ) { attemptFail ( 'network error' ) ; } ;
401+ xhr . ontimeout = function ( ) { attemptFail ( 'timeout' ) ; } ;
402+ xhr . onabort = function ( ) { attemptFail ( 'aborted' ) ; } ;
403+ try { xhr . send ( ) ; } catch ( e ) { reject ( e ) ; }
404+ } ) ;
405+ }
406+
407+ /**
408+ * Apply the certificate to the CDM; supports both sync and promise-returning implementations.
409+ * @param {ArrayBuffer } buffer
410+ * @param {string } ksString
411+ * @param {object } cacheEntry
412+ * @return {Promise<boolean> }
413+ * @private
414+ */
415+ function _applyServerCertificate ( buffer , ksString , cacheEntry ) {
416+ return new Promise ( ( resolve , reject ) => {
417+ try {
418+ const result = protectionModel . setServerCertificate ( buffer ) ;
419+ if ( result && typeof result . then === 'function' ) {
420+ result . then ( ( val ) => {
421+ resolve ( typeof val === 'boolean' ? val : true ) ;
422+ } ) . catch ( ( e ) => {
423+ cacheEntry . error = e && e . message ? e . message : e ;
424+ reject ( e ) ;
425+ } ) ;
426+ } else {
427+ resolve ( true ) ;
428+ }
429+ } catch ( e ) {
430+ cacheEntry . error = e && e . message ? e . message : e ;
431+ logger . warn ( 'DRM: setServerCertificate threw for ' + ksString + ': ' + cacheEntry . error ) ;
432+ reject ( e ) ;
433+ }
434+ } ) ;
435+ }
436+
307437 /**
308438 * If we have already selected a key system we only need to create a new key session and issue a new license request if the init data has changed.
309439 * @private
0 commit comments