Skip to content

Commit 5aacb03

Browse files
committed
Added certificate aquisition logic
1 parent 9684c6b commit 5aacb03

File tree

1 file changed

+133
-3
lines changed

1 file changed

+133
-3
lines changed

src/streaming/protection/controllers/ProtectionController.js

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ const NEEDKEY_BEFORE_INITIALIZE_TIMEOUT = 500;
4848
const LICENSE_SERVER_REQUEST_RETRIES = 3;
4949
const LICENSE_SERVER_REQUEST_RETRY_INTERVAL = 1000;
5050
const 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

Comments
 (0)