From 27102516e7b52fa225c6175f47af94342e3ccc8d Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 29 Nov 2021 17:25:32 +0100 Subject: [PATCH 01/30] feat: add support for HLS EME com.apple.fps keySystem --- lib/hls/hls_parser.js | 20 +++++++++++++++-- lib/media/drm_engine.js | 14 +++++++----- lib/net/http_fetch_plugin.js | 30 ++++++++++++++------------ lib/net/http_xhr_plugin.js | 23 +++++++++++++++----- lib/polyfill/patchedmediakeys_apple.js | 4 ++-- 5 files changed, 63 insertions(+), 28 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 42de5a8d21..d3eb521923 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -2807,6 +2807,24 @@ shaka.hls.HlsParser = class { return op.promise; } + /** + * @return {?shaka.extern.DrmInfo} + * @private + */ + static fairplayDrmParser_() { + /* + * Even if we're not able to construct initData through the HLS tag, adding + * a DRMInfo will allow DRM Engine to request a media key system access + * with the correct keySystem and initDataType + */ + const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo( + 'com.apple.fps', [ + {initDataType: 'sinf'}, + ]); + + return drmInfo; + } + /** * @param {!shaka.hls.Tag} drmTag * @return {?shaka.extern.DrmInfo} @@ -3040,10 +3058,8 @@ shaka.hls.HlsParser.DrmParser_; * @private */ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = { - /* TODO: https://github.com/google/shaka-player/issues/382 'com.apple.streamingkeydelivery': shaka.hls.HlsParser.fairplayDrmParser_, - */ 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': shaka.hls.HlsParser.widevineDrmParser_, 'com.microsoft.playready': diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index f1de943ba1..96ddf47b20 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -434,10 +434,10 @@ shaka.media.DrmEngine = class { this.destroyer_.ensureNotDestroyed(); this.createOrLoad(); - if (!this.currentDrmInfo_.initData.length && - !this.offlineSessionIds_.length) { - // Explicit init data for any one stream or an offline session is - // sufficient to suppress 'encrypted' events for all streams. + + // Explicit init data an offline session is + // sufficient to suppress 'encrypted' events for all streams. + if (!this.offlineSessionIds_.length) { const cb = (e) => this.newInitData( e.initDataType, shaka.util.BufferUtils.toUint8(e.initData)); this.eventManager_.listen(this.video_, 'encrypted', cb); @@ -586,9 +586,13 @@ shaka.media.DrmEngine = class { * been seen yet, this will create a new session for it. * * @param {string} initDataType - * @param {!Uint8Array} initData + * @param {Uint8Array} initData */ newInitData(initDataType, initData) { + if (!initData) { + return; + } + // Suppress duplicate init data. // Note that some init data are extremely large and can't portably be used // as keys in a dictionary. diff --git a/lib/net/http_fetch_plugin.js b/lib/net/http_fetch_plugin.js index b5320258c6..d1f047898b 100644 --- a/lib/net/http_fetch_plugin.js +++ b/lib/net/http_fetch_plugin.js @@ -204,6 +204,8 @@ shaka.net.HttpFetchPlugin = class { const headers = shaka.net.HttpFetchPlugin.headersToGenericObject_( response.headers); + shaka.log.info('Fetch-Response', arrayBuffer); + return shaka.net.HttpPluginUtils.makeResponse( headers, arrayBuffer, response.status, uri, response.url, requestType); } @@ -297,17 +299,17 @@ shaka.net.HttpFetchPlugin.ReadableStream_ = window.ReadableStream; shaka.net.HttpFetchPlugin.Headers_ = window.Headers; -if (shaka.net.HttpFetchPlugin.isSupported()) { - shaka.net.NetworkingEngine.registerScheme( - 'http', shaka.net.HttpFetchPlugin.parse, - shaka.net.NetworkingEngine.PluginPriority.PREFERRED, - /* progressSupport= */ true); - shaka.net.NetworkingEngine.registerScheme( - 'https', shaka.net.HttpFetchPlugin.parse, - shaka.net.NetworkingEngine.PluginPriority.PREFERRED, - /* progressSupport= */ true); - shaka.net.NetworkingEngine.registerScheme( - 'blob', shaka.net.HttpFetchPlugin.parse, - shaka.net.NetworkingEngine.PluginPriority.PREFERRED, - /* progressSupport= */ true); -} +// if (shaka.net.HttpFetchPlugin.isSupported()) { +// shaka.net.NetworkingEngine.registerScheme( +// 'http', shaka.net.HttpFetchPlugin.parse, +// shaka.net.NetworkingEngine.PluginPriority.PREFERRED, +// /* progressSupport= */ true); +// shaka.net.NetworkingEngine.registerScheme( +// 'https', shaka.net.HttpFetchPlugin.parse, +// shaka.net.NetworkingEngine.PluginPriority.PREFERRED, +// /* progressSupport= */ true); +// shaka.net.NetworkingEngine.registerScheme( +// 'blob', shaka.net.HttpFetchPlugin.parse, +// shaka.net.NetworkingEngine.PluginPriority.PREFERRED, +// /* progressSupport= */ true); +// } diff --git a/lib/net/http_xhr_plugin.js b/lib/net/http_xhr_plugin.js index aec4ec6de2..696d0ca6e1 100644 --- a/lib/net/http_xhr_plugin.js +++ b/lib/net/http_xhr_plugin.js @@ -11,7 +11,7 @@ goog.require('shaka.net.HttpPluginUtils'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.util.AbortableOperation'); goog.require('shaka.util.Error'); - +goog.require('shaka.util.Uint8ArrayUtils'); /** * @summary A networking plugin to handle http and https URIs via XHR. @@ -39,7 +39,11 @@ shaka.net.HttpXHRPlugin = class { const promise = new Promise(((resolve, reject) => { xhr.open(request.method, uri, true); - xhr.responseType = 'arraybuffer'; + + if (requestType != shaka.net.NetworkingEngine.RequestType.LICENSE) { + xhr.responseType = 'arraybuffer'; + } + xhr.timeout = request.retryParameters.timeout; xhr.withCredentials = request.allowCrossSiteCredentials; @@ -62,9 +66,18 @@ shaka.net.HttpXHRPlugin = class { }; xhr.onload = (event) => { const headers = shaka.net.HttpXHRPlugin.headersToGenericObject_(xhr); - goog.asserts.assert(xhr.response instanceof ArrayBuffer, - 'XHR should have a response by now!'); - const xhrResponse = xhr.response; + + let xhrResponse = new ArrayBuffer(0); + + if (xhr.responseType != 'arraybuffer') { + xhrResponse = shaka.util.Uint8ArrayUtils.fromBase64(xhr.responseText); + } else { + + xhrResponse = xhr.response; + } + + goog.asserts.assert(xhrResponse instanceof ArrayBuffer, + 'XHR should have a response by now!'); try { const response = shaka.net.HttpPluginUtils.makeResponse(headers, diff --git a/lib/polyfill/patchedmediakeys_apple.js b/lib/polyfill/patchedmediakeys_apple.js index cc44b0bb2a..4e7ca5bf44 100644 --- a/lib/polyfill/patchedmediakeys_apple.js +++ b/lib/polyfill/patchedmediakeys_apple.js @@ -740,5 +740,5 @@ shaka.polyfill.PatchedMediaKeysApple.MediaKeyStatusMap = class { } }; - -shaka.polyfill.register(shaka.polyfill.PatchedMediaKeysApple.install); +// Make it configurable ? +// shaka.polyfill.register(shaka.polyfill.PatchedMediaKeysApple.install); From 6be7725b3b6bd09d053c3c258f833b9db88bebb0 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Tue, 30 Nov 2021 09:02:04 +0100 Subject: [PATCH 02/30] revert --- lib/net/http_fetch_plugin.js | 30 ++++++++++++++---------------- lib/net/http_xhr_plugin.js | 23 +++++------------------ 2 files changed, 19 insertions(+), 34 deletions(-) diff --git a/lib/net/http_fetch_plugin.js b/lib/net/http_fetch_plugin.js index d1f047898b..b5320258c6 100644 --- a/lib/net/http_fetch_plugin.js +++ b/lib/net/http_fetch_plugin.js @@ -204,8 +204,6 @@ shaka.net.HttpFetchPlugin = class { const headers = shaka.net.HttpFetchPlugin.headersToGenericObject_( response.headers); - shaka.log.info('Fetch-Response', arrayBuffer); - return shaka.net.HttpPluginUtils.makeResponse( headers, arrayBuffer, response.status, uri, response.url, requestType); } @@ -299,17 +297,17 @@ shaka.net.HttpFetchPlugin.ReadableStream_ = window.ReadableStream; shaka.net.HttpFetchPlugin.Headers_ = window.Headers; -// if (shaka.net.HttpFetchPlugin.isSupported()) { -// shaka.net.NetworkingEngine.registerScheme( -// 'http', shaka.net.HttpFetchPlugin.parse, -// shaka.net.NetworkingEngine.PluginPriority.PREFERRED, -// /* progressSupport= */ true); -// shaka.net.NetworkingEngine.registerScheme( -// 'https', shaka.net.HttpFetchPlugin.parse, -// shaka.net.NetworkingEngine.PluginPriority.PREFERRED, -// /* progressSupport= */ true); -// shaka.net.NetworkingEngine.registerScheme( -// 'blob', shaka.net.HttpFetchPlugin.parse, -// shaka.net.NetworkingEngine.PluginPriority.PREFERRED, -// /* progressSupport= */ true); -// } +if (shaka.net.HttpFetchPlugin.isSupported()) { + shaka.net.NetworkingEngine.registerScheme( + 'http', shaka.net.HttpFetchPlugin.parse, + shaka.net.NetworkingEngine.PluginPriority.PREFERRED, + /* progressSupport= */ true); + shaka.net.NetworkingEngine.registerScheme( + 'https', shaka.net.HttpFetchPlugin.parse, + shaka.net.NetworkingEngine.PluginPriority.PREFERRED, + /* progressSupport= */ true); + shaka.net.NetworkingEngine.registerScheme( + 'blob', shaka.net.HttpFetchPlugin.parse, + shaka.net.NetworkingEngine.PluginPriority.PREFERRED, + /* progressSupport= */ true); +} diff --git a/lib/net/http_xhr_plugin.js b/lib/net/http_xhr_plugin.js index 696d0ca6e1..aec4ec6de2 100644 --- a/lib/net/http_xhr_plugin.js +++ b/lib/net/http_xhr_plugin.js @@ -11,7 +11,7 @@ goog.require('shaka.net.HttpPluginUtils'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.util.AbortableOperation'); goog.require('shaka.util.Error'); -goog.require('shaka.util.Uint8ArrayUtils'); + /** * @summary A networking plugin to handle http and https URIs via XHR. @@ -39,11 +39,7 @@ shaka.net.HttpXHRPlugin = class { const promise = new Promise(((resolve, reject) => { xhr.open(request.method, uri, true); - - if (requestType != shaka.net.NetworkingEngine.RequestType.LICENSE) { - xhr.responseType = 'arraybuffer'; - } - + xhr.responseType = 'arraybuffer'; xhr.timeout = request.retryParameters.timeout; xhr.withCredentials = request.allowCrossSiteCredentials; @@ -66,18 +62,9 @@ shaka.net.HttpXHRPlugin = class { }; xhr.onload = (event) => { const headers = shaka.net.HttpXHRPlugin.headersToGenericObject_(xhr); - - let xhrResponse = new ArrayBuffer(0); - - if (xhr.responseType != 'arraybuffer') { - xhrResponse = shaka.util.Uint8ArrayUtils.fromBase64(xhr.responseText); - } else { - - xhrResponse = xhr.response; - } - - goog.asserts.assert(xhrResponse instanceof ArrayBuffer, - 'XHR should have a response by now!'); + goog.asserts.assert(xhr.response instanceof ArrayBuffer, + 'XHR should have a response by now!'); + const xhrResponse = xhr.response; try { const response = shaka.net.HttpPluginUtils.makeResponse(headers, From f20bd58af14b0d7ce31b14794aa939dad4b2f755 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 1 Dec 2021 09:10:09 +0100 Subject: [PATCH 03/30] enhance hls parser and add a unit test --- lib/hls/hls_parser.js | 2 +- lib/media/drm_engine.js | 4 ++-- test/hls/hls_parser_unit.js | 36 ++++++++++++++++++++++++++++ test/test/util/manifest_generator.js | 2 +- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index d3eb521923..43f12a16ed 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -2819,7 +2819,7 @@ shaka.hls.HlsParser = class { */ const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo( 'com.apple.fps', [ - {initDataType: 'sinf'}, + {initDataType: 'sinf', initData: new Uint8Array(0)}, ]); return drmInfo; diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 96ddf47b20..02d00cf46e 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -586,10 +586,10 @@ shaka.media.DrmEngine = class { * been seen yet, this will create a new session for it. * * @param {string} initDataType - * @param {Uint8Array} initData + * @param {!Uint8Array} initData */ newInitData(initDataType, initData) { - if (!initData) { + if (!initData.length) { return; } diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index 25c38b1a1a..b73af93877 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -2231,6 +2231,42 @@ describe('HlsParser', () => { await testHlsParser(master, media, manifest); }); + it('constructs DrmInfo for FairPlay', async () => { + const master = [ + '#EXTM3U\n', + '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",', + 'RESOLUTION=960x540,FRAME-RATE=60\n', + 'video\n', + ].join(''); + + const media = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:6\n', + '#EXT-X-PLAYLIST-TYPE:VOD\n', + '#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,', + 'KEYFORMAT="com.apple.streamingkeydelivery",', + 'URI="skd://f93d4e700d7ddde90529a27735d9e7cb",\n', + '#EXT-X-MAP:URI="init.mp4"\n', + '#EXTINF:5,\n', + '#EXT-X-BYTERANGE:121090@616\n', + 'main.mp4', + ].join(''); + + const manifest = shaka.test.ManifestGenerator.generate((manifest) => { + manifest.anyTimeline(); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.encrypted = true; + stream.addDrmInfo('com.apple.fps', (drmInfo) => { + drmInfo.addInitData('sinf', new Uint8Array(0)); + }); + }); + }); + }); + + await testHlsParser(master, media, manifest); + }); + it('falls back to mp4 if HEAD request fails', async () => { const master = [ '#EXTM3U\n', diff --git a/test/test/util/manifest_generator.js b/test/test/util/manifest_generator.js index 89392d4712..be862d3889 100644 --- a/test/test/util/manifest_generator.js +++ b/test/test/util/manifest_generator.js @@ -433,7 +433,7 @@ shaka.test.ManifestGenerator.DrmInfo = class { if (!this.initData) { this.initData = []; } - this.initData.push({initData: buffer, initDataType: type, keyId: null}); + this.initData.push({initData: buffer, initDataType: type}); } /** From 70133e68e10d59617cfb2f0ba0b84a47e2cb7d54 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 1 Dec 2021 18:12:06 +0100 Subject: [PATCH 04/30] add rule on setting media keys if polyfilled --- lib/media/drm_engine.js | 99 ++++++++++++++++++------- lib/polyfill/patchedmediakeys_apple.js | 21 +----- lib/polyfill/patchedmediakeys_ms.js | 7 +- lib/polyfill/patchedmediakeys_webkit.js | 14 ++-- lib/util/platform.js | 68 +++++++++++++++++ 5 files changed, 151 insertions(+), 58 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 02d00cf46e..8e9af305ae 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -387,7 +387,48 @@ shaka.media.DrmEngine = class { } /** - * Attach MediaKeys to the video element and start processing events. + * Attach MediaKeys to the video element + * @return {!Promise} + */ + async attachMediaKeys() { + if (this.video_.mediaKeys) { + return; + } + + try { + await this.video_.setMediaKeys(this.mediaKeys_); + } catch (exception) { + goog.asserts.assert(exception instanceof Error, 'Wrong error type!'); + + this.onError_(new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.DRM, + shaka.util.Error.Code.FAILED_TO_ATTACH_TO_VIDEO, + exception.message)); + } + } + + /** + * Processes encrypted event and start licence challenging + * @return {!Promise} + */ + async onEncryptedEvent(event) { + /** + * MediaKeys should be added when receiving an encrypted event. Setting + * mediaKeys before could result into encrypted event not being fired on + * some browsers + */ + await this.attachMediaKeys(); + + this.destroyer_.ensureNotDestroyed(); + + this.newInitData( + event.initDataType, + shaka.util.BufferUtils.toUint8(event.initData)); + } + + /** + * Start processing events. * @param {HTMLMediaElement} video * @return {!Promise} */ @@ -420,17 +461,15 @@ shaka.media.DrmEngine = class { () => this.closeOpenSessions_()); } - let setMediaKeys = this.video_.setMediaKeys(this.mediaKeys_); - setMediaKeys = setMediaKeys.catch((exception) => { - goog.asserts.assert(exception instanceof Error, 'Wrong error type!'); - return Promise.reject(new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.DRM, - shaka.util.Error.Code.FAILED_TO_ATTACH_TO_VIDEO, - exception.message)); - }); + /** + * Legacy implementations requires MediaKeys to be set before having + * webkitneedkey / msneedkey event, which will be translated as an + * encrypted event by the polyfills + */ + if (shaka.util.Platform.isMediaKeysPolyfilled()) { + await this.attachMediaKeys(); + } - await setMediaKeys; this.destroyer_.ensureNotDestroyed(); this.createOrLoad(); @@ -438,9 +477,9 @@ shaka.media.DrmEngine = class { // Explicit init data an offline session is // sufficient to suppress 'encrypted' events for all streams. if (!this.offlineSessionIds_.length) { - const cb = (e) => this.newInitData( - e.initDataType, shaka.util.BufferUtils.toUint8(e.initData)); - this.eventManager_.listen(this.video_, 'encrypted', cb); + this.eventManager_.listen( + // eslint-disable-next-line no-restricted-syntax + this.video_, 'encrypted', this.onEncryptedEvent.bind(this)); } } @@ -1192,20 +1231,26 @@ shaka.media.DrmEngine = class { }; this.activeSessions_.set(session, metadata); - try { - initData = this.config_.initDataTransform( - initData, initDataType, this.currentDrmInfo_); - } catch (error) { - let shakaError = error; - if (!(error instanceof shaka.util.Error)) { - shakaError = new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.DRM, - shaka.util.Error.Code.INIT_DATA_TRANSFORM_ERROR, - error); + /** + * initDataTransform is only necessary when using legacy protection + * APIs, so prevent doing any transform when using the EME HTML5 spec + */ + if (shaka.util.Platform.isMediaKeysPolyfilled()) { + try { + initData = this.config_.initDataTransform( + initData, initDataType, this.currentDrmInfo_); + } catch (error) { + let shakaError = error; + if (!(error instanceof shaka.util.Error)) { + shakaError = new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.DRM, + shaka.util.Error.Code.INIT_DATA_TRANSFORM_ERROR, + error); + } + this.onError_(shakaError); + return; } - this.onError_(shakaError); - return; } if (this.config_.logLicenseExchange) { diff --git a/lib/polyfill/patchedmediakeys_apple.js b/lib/polyfill/patchedmediakeys_apple.js index 4e7ca5bf44..6a82cc5feb 100644 --- a/lib/polyfill/patchedmediakeys_apple.js +++ b/lib/polyfill/patchedmediakeys_apple.js @@ -17,6 +17,7 @@ goog.require('shaka.util.FakeEventTarget'); goog.require('shaka.util.MediaReadyState'); goog.require('shaka.util.PublicPromise'); goog.require('shaka.util.StringUtils'); +goog.require('shaka.util.Platform'); /** @@ -30,25 +31,10 @@ shaka.polyfill.PatchedMediaKeysApple = class { * @export */ static install() { - if (!window.HTMLVideoElement || !window.WebKitMediaKeys) { - // No HTML5 video or no prefixed EME. + if (!shaka.util.Platform.shouldPolyfillMediaKeysApple()) { return; } - /* Unprefixed EME disabled. See: - https://github.com/google/shaka-player/pull/3021#issuecomment-766999811 - - // Only tested in Safari 14. - const safariVersion = shaka.util.Platform.safariVersion(); - if (navigator.requestMediaKeySystemAccess && - // eslint-disable-next-line no-restricted-syntax - MediaKeySystemAccess.prototype.getConfiguration && - safariVersion && safariVersion >= 14) { - // Unprefixed EME is preferable. - return; - } - */ - shaka.log.info('Using Apple-prefixed EME'); // Alias @@ -740,5 +726,4 @@ shaka.polyfill.PatchedMediaKeysApple.MediaKeyStatusMap = class { } }; -// Make it configurable ? -// shaka.polyfill.register(shaka.polyfill.PatchedMediaKeysApple.install); +shaka.polyfill.register(shaka.polyfill.PatchedMediaKeysApple.install); diff --git a/lib/polyfill/patchedmediakeys_ms.js b/lib/polyfill/patchedmediakeys_ms.js index 23197792fb..466713e2ea 100644 --- a/lib/polyfill/patchedmediakeys_ms.js +++ b/lib/polyfill/patchedmediakeys_ms.js @@ -17,6 +17,7 @@ goog.require('shaka.util.FakeEventTarget'); goog.require('shaka.util.MediaReadyState'); goog.require('shaka.util.Pssh'); goog.require('shaka.util.PublicPromise'); +goog.require('shaka.util.Platform'); /** @@ -32,12 +33,10 @@ shaka.polyfill.PatchedMediaKeysMs = class { * @export */ static install() { - if (!window.HTMLVideoElement || !window.MSMediaKeys || - (navigator.requestMediaKeySystemAccess && - // eslint-disable-next-line no-restricted-syntax - MediaKeySystemAccess.prototype.getConfiguration)) { + if (!shaka.util.Platform.shouldPolyfillMediaKeysMS()) { return; } + shaka.log.info('Using ms-prefixed EME v20140218'); // Alias diff --git a/lib/polyfill/patchedmediakeys_webkit.js b/lib/polyfill/patchedmediakeys_webkit.js index 343868948e..a5b3afe0d5 100644 --- a/lib/polyfill/patchedmediakeys_webkit.js +++ b/lib/polyfill/patchedmediakeys_webkit.js @@ -18,7 +18,7 @@ goog.require('shaka.util.PublicPromise'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.Timer'); goog.require('shaka.util.Uint8ArrayUtils'); - +goog.require('shaka.util.Platform'); /** * @summary A polyfill to implement @@ -32,15 +32,13 @@ shaka.polyfill.PatchedMediaKeysWebkit = class { * @export */ static install() { + if (!shaka.util.Platform.shouldPolyfillMediaKeysWebkit()) { + return; + } + // Alias. const PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit; - if (!window.HTMLVideoElement || - (navigator.requestMediaKeySystemAccess && - // eslint-disable-next-line no-restricted-syntax - MediaKeySystemAccess.prototype.getConfiguration)) { - return; - } // eslint-disable-next-line no-restricted-syntax if (HTMLMediaElement.prototype.webkitGenerateKeyRequest) { shaka.log.info('Using webkit-prefixed EME v0.1b'); @@ -48,8 +46,6 @@ shaka.polyfill.PatchedMediaKeysWebkit = class { // eslint-disable-next-line no-restricted-syntax } else if (HTMLMediaElement.prototype.generateKeyRequest) { shaka.log.info('Using nonprefixed EME v0.1b'); - } else { - return; } goog.asserts.assert( diff --git a/lib/util/platform.js b/lib/util/platform.js index 040c547868..768faff388 100644 --- a/lib/util/platform.js +++ b/lib/util/platform.js @@ -318,6 +318,74 @@ shaka.util.Platform = class { const Platform = shaka.util.Platform; return Platform.isTizen() || Platform.isXboxOne(); } + + /** + * Returns true if we need to polyfill Apple legacy MediaKeys + * implementation + * + * @return {boolean} + */ + static shouldPolyfillMediaKeysApple() { + if (!window.HTMLVideoElement || !window.WebKitMediaKeys) { + return false; + } + + const safariVersion = shaka.util.Platform.safariVersion(); + + if (navigator.requestMediaKeySystemAccess && + // eslint-disable-next-line no-restricted-syntax + MediaKeySystemAccess.prototype.getConfiguration && + safariVersion && safariVersion >= 14) { + // Unprefixed EME is preferable. + return false; + } + + return true; + } + + /** + * Returns true if we need to polyfill Webkit legacy MediaKeys + * implementation + * + * @return {boolean} + */ + static shouldPolyfillMediaKeysWebkit() { + if (!window.HTMLVideoElement || + (navigator.requestMediaKeySystemAccess && + // eslint-disable-next-line no-restricted-syntax + MediaKeySystemAccess.prototype.getConfiguration)) { + return false; + } + + // eslint-disable-next-line no-restricted-syntax + return !!(HTMLMediaElement.prototype.webkitGenerateKeyRequest || + // eslint-disable-next-line no-restricted-syntax + HTMLMediaElement.prototype.generateKeyRequest); + } + + /** + * Returns true if we need to polyfill MS legacy MediaKeys implementation + * + * @return {boolean} + */ + static shouldPolyfillMediaKeysMS() { + return window.HTMLVideoElement && window.MSMediaKeys && + (!navigator.requestMediaKeySystemAccess || + // eslint-disable-next-line no-restricted-syntax + !MediaKeySystemAccess.prototype.getConfiguration); + } + + /** + * Returns true if any MediaKeys polyfill should have been + * installed + * + * @return {boolean} + */ + static isMediaKeysPolyfilled() { + return shaka.util.Platform.shouldPolyfillMediaKeysApple() || + shaka.util.Platform.shouldPolyfillMediaKeysWebkit() || + shaka.util.Platform.shouldPolyfillMediaKeysWebkit(); + } }; /** @private {shaka.util.Timer} */ From 4e9a3e0e7d80856dce2d1da202d09578d8a60bd4 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Thu, 2 Dec 2021 10:23:42 +0100 Subject: [PATCH 05/30] update docs --- README.md | 3 ++- docs/tutorials/drm-config.md | 5 +++++ docs/tutorials/fairplay.md | 20 +++++++++++++++----- externs/shaka/player.js | 2 -- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c25e61fbe4..e73054a164 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,8 @@ NOTES: |HLS |**Y** |**Y** |**Y** ¹ | - | NOTES: - - ¹: We support FairPlay through Apple's native HLS player. + - ¹: We support FairPlay through MSE/EME when available, and fallback to + Apple's native HLS player with `com.apple.fps.1_0` keySystem otherwise. ## Media container and subtitle support diff --git a/docs/tutorials/drm-config.md b/docs/tutorials/drm-config.md index c68d253293..8e9f79767e 100644 --- a/docs/tutorials/drm-config.md +++ b/docs/tutorials/drm-config.md @@ -184,6 +184,11 @@ Microsoft Documentation: https://docs.microsoft.com/en-us/playready/overview/sec NB: Audio Hardware DRM is not supported (PlayReady limitation) +##### FairPlay + +Based on [Apple's Documentation](https://developer.apple.com/streaming/fps/), +you should provide an empty string as robustness + ##### Other key-systems Values for other key systems are not known to us at this time. diff --git a/docs/tutorials/fairplay.md b/docs/tutorials/fairplay.md index 7ca3f7439c..2ff4f77d66 100644 --- a/docs/tutorials/fairplay.md +++ b/docs/tutorials/fairplay.md @@ -1,25 +1,35 @@ # FairPlay Support -When using native `src=` playback, we support using FairPlay on Safari. -Adding FairPlay support involves a bit more work than other key systems. +We support FairPlay with EME on compatible environments or native `src=`. + +You should provide configuration for `com.apple.fps` and `com.apple.fps.1_0` +if you want both EME and native support. +Adding FairPlay support involves a bit more work than other key systems. ## Server certificate -All FairPlay content requires setting a server certificate. This is set in the -Player configuration: +All FairPlay content requires setting a server certificate. You can either +provide it directly or set a serverCertificateUri for Shaka to fetch it for +you. ```js const req = await fetch('https://example.com/cert.der'); const cert = await req.arrayBuffer(); -player.configure('drm.advanced.com\\.apple\\.fps\\.1_0.serverCertificate', +player.configure('drm.advanced.com\\.apple\\.fps\\.serverCertificate', new Uint8Array(cert)); ``` +```js +player.configure('drm.advanced.com\\.apple\\.fps\\.serverCertificateUri', + 'https://example.com/cert.der'); +``` ## Content ID +Note: This is specific for legacy `com.apple.fps.1_0` keySystem fallback + Some FairPlay content use custom signaling for the content ID. The content ID is used by the browser to generate the license request. If you don't use the default content ID derivation, you need to specify a custom init data transform: diff --git a/externs/shaka/player.js b/externs/shaka/player.js index a2f9eff1fb..d03cf195f5 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -934,8 +934,6 @@ shaka.extern.ManifestConfiguration; * @property {boolean} useNativeHlsOnSafari * Desktop Safari has both MediaSource and their native HLS implementation. * Depending on the application's needs, it may prefer one over the other. - * Examples: FairPlay is only supported via Safari's native HLS, but it - * doesn't have an API for selecting specific tracks. * @property {number} inaccurateManifestTolerance * The maximum difference, in seconds, between the times in the manifest and * the times in the segments. Larger values allow us to compensate for more From 3b43e34ba43fcd222c380bf3236c4428d2f8ddfb Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 15 Dec 2021 10:51:46 +0100 Subject: [PATCH 06/30] detect sinf data from legacy media keys implementation --- lib/polyfill/patchedmediakeys_apple.js | 51 +++++++++++++++++++++----- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/lib/polyfill/patchedmediakeys_apple.js b/lib/polyfill/patchedmediakeys_apple.js index 6a82cc5feb..92101e6a1e 100644 --- a/lib/polyfill/patchedmediakeys_apple.js +++ b/lib/polyfill/patchedmediakeys_apple.js @@ -126,6 +126,26 @@ shaka.polyfill.PatchedMediaKeysApple = class { return Promise.resolve(); } + /** + * + * @param {!MediaKeyEvent} sourceEvent + * @param {string} initDataType + * @param {ArrayBuffer} initData + * @private + */ + static dispatchEncryptedEvent_(sourceEvent, initDataType, initData) { + // NOTE: Because "this" is a real EventTarget, the event we dispatch here + // must also be a real Event. + const event = new Event('encrypted'); + + const encryptedEvent = + /** @type {!MediaEncryptedEvent} */(/** @type {?} */(event)); + encryptedEvent.initDataType = initDataType; + encryptedEvent.initData = initData; + + sourceEvent.target.dispatchEvent(event); + } + /** * Handler for the native media elements webkitneedkey event. * @@ -149,6 +169,23 @@ shaka.polyfill.PatchedMediaKeysApple = class { // Convert the prefixed init data to match the native 'encrypted' event. const uint8 = shaka.util.BufferUtils.toUint8(event.initData); + + try { + const base64 = shaka.util.StringUtils.fromCharCode(uint8); + const data = JSON.parse(base64); + + if (data) { + PatchedMediaKeysApple.dispatchEncryptedEvent_( + event, + 'sinf', + shaka.util.BufferUtils.toArrayBuffer(event.initData)); + + return; + } + } catch (error) { + // Continue with SKD initDataType detection + } + const dataview = shaka.util.BufferUtils.toDataView(uint8); // The first part is a 4 byte little-endian int, which is the length of // the second part. @@ -162,16 +199,10 @@ shaka.polyfill.PatchedMediaKeysApple = class { uint8.subarray(4), /* littleEndian= */ true); const initData = shaka.util.StringUtils.toUTF8(str); - // NOTE: Because "this" is a real EventTarget, the event we dispatch here - // must also be a real Event. - const event2 = new Event('encrypted'); - - const encryptedEvent = - /** @type {!MediaEncryptedEvent} */(/** @type {?} */(event2)); - encryptedEvent.initDataType = 'skd'; - encryptedEvent.initData = shaka.util.BufferUtils.toArrayBuffer(initData); - - this.dispatchEvent(event2); + PatchedMediaKeysApple.dispatchEncryptedEvent_( + event, + 'skd', + shaka.util.BufferUtils.toArrayBuffer(initData)); } }; From 421b0b8bfd55067e2a2e30e15dc568b5d420b01f Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 20 Dec 2021 11:59:22 +0100 Subject: [PATCH 07/30] prevent playback when using TS w/ MSE or MSE w/ legacy Apple Media Keys --- lib/hls/hls_parser.js | 54 ++++++++++++++++++++++++++++--------------- lib/util/error.js | 11 +++++++++ 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 43f12a16ed..12caf281a1 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -40,6 +40,7 @@ goog.require('shaka.util.Networking'); goog.require('shaka.util.OperationManager'); goog.require('shaka.util.Pssh'); goog.require('shaka.util.Timer'); +goog.require('shaka.util.Platform'); goog.requireType('shaka.hls.Segment'); @@ -1391,6 +1392,21 @@ shaka.hls.HlsParser = class { shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY); } + /** @type {!Array.} */ + const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags, + 'EXT-X-DEFINE'); + + const mediaVariables = this.parseMediaVariables_(variablesTags); + + goog.asserts.assert(playlist.segments != null, + 'Media playlist should have segments!'); + + this.determinePresentationType_(playlist); + + /** @type {string} */ + const mimeType = await this.guessMimeType_(type, codecs, playlist, + mediaVariables); + /** @type {!Array.} */ const drmTags = []; if (playlist.segments) { @@ -1425,7 +1441,7 @@ shaka.hls.HlsParser = class { const drmParser = shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat]; - const drmInfo = drmParser ? drmParser(drmTag) : null; + const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null; if (drmInfo) { if (drmInfo.keyIds) { for (const keyId of drmInfo.keyIds) { @@ -1446,21 +1462,6 @@ shaka.hls.HlsParser = class { shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED); } - /** @type {!Array.} */ - const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags, - 'EXT-X-DEFINE'); - - const mediaVariables = this.parseMediaVariables_(variablesTags); - - goog.asserts.assert(playlist.segments != null, - 'Media playlist should have segments!'); - - this.determinePresentationType_(playlist); - - /** @type {string} */ - const mimeType = await this.guessMimeType_(type, codecs, playlist, - mediaVariables); - // MediaSource expects no codec strings combined with raw formats. // TODO(#2337): Instead, create a Stream flag indicating a raw format. if (shaka.hls.HlsParser.RAW_FORMATS_.includes(mimeType)) { @@ -2808,10 +2809,27 @@ shaka.hls.HlsParser = class { } /** + * @param {!shaka.hls.Tag} drmTag + * @param {string} mimeType * @return {?shaka.extern.DrmInfo} * @private */ - static fairplayDrmParser_() { + static fairplayDrmParser_(drmTag, mimeType) { + if (mimeType == 'video/mp2t') { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MANIFEST, + shaka.util.Error.Code.HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED); + } + + if (shaka.util.Platform.shouldPolyfillMediaKeysApple()) { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MANIFEST, + shaka.util.Error.Code + .HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED); + } + /* * Even if we're not able to construct initData through the HLS tag, adding * a DRMInfo will allow DRM Engine to request a media key system access @@ -3047,7 +3065,7 @@ shaka.hls.HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_ = { /** - * @typedef {function(!shaka.hls.Tag):?shaka.extern.DrmInfo} + * @typedef {function(!shaka.hls.Tag, string):?shaka.extern.DrmInfo} * @private */ shaka.hls.HlsParser.DrmParser_; diff --git a/lib/util/error.js b/lib/util/error.js index aaea12326e..6cbd56f85a 100644 --- a/lib/util/error.js +++ b/lib/util/error.js @@ -686,6 +686,17 @@ shaka.util.Error.Code = { */ 'HLS_VARIABLE_NOT_FOUND': 4039, + /** + * We do not support playing encrypted mp2t with MSE + */ + 'HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED': 4040, + + /** + * We do not support playing encrypted content (different than mp2t) with MSE + * and legacy Apple MediaKeys API. + */ + 'HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED': 4041, + // RETIRED: 'INCONSISTENT_BUFFER_STATE': 5000, // RETIRED: 'INVALID_SEGMENT_INDEX': 5001, From 7ed4c43c7c1f1ee86fb5b3061b7ca30565faed87 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 20 Dec 2021 14:42:56 +0100 Subject: [PATCH 08/30] fixup doc --- README.md | 5 +++-- docs/tutorials/fairplay.md | 6 +----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e73054a164..987da933ac 100644 --- a/README.md +++ b/README.md @@ -171,8 +171,9 @@ NOTES: |HLS |**Y** |**Y** |**Y** ¹ | - | NOTES: - - ¹: We support FairPlay through MSE/EME when available, and fallback to - Apple's native HLS player with `com.apple.fps.1_0` keySystem otherwise. + - ¹: We support FairPlay through MSE/EME when available and when + `useNativeHlsOnSafari` is turned off. Otherwise, we are using + Apple's native HLS player. ## Media container and subtitle support diff --git a/docs/tutorials/fairplay.md b/docs/tutorials/fairplay.md index 2ff4f77d66..33a27c6296 100644 --- a/docs/tutorials/fairplay.md +++ b/docs/tutorials/fairplay.md @@ -1,10 +1,6 @@ # FairPlay Support We support FairPlay with EME on compatible environments or native `src=`. - -You should provide configuration for `com.apple.fps` and `com.apple.fps.1_0` -if you want both EME and native support. - Adding FairPlay support involves a bit more work than other key systems. ## Server certificate @@ -28,7 +24,7 @@ player.configure('drm.advanced.com\\.apple\\.fps\\.serverCertificateUri', ## Content ID -Note: This is specific for legacy `com.apple.fps.1_0` keySystem fallback +Note: This is specific when legacy Apple Media Keys is used. Some FairPlay content use custom signaling for the content ID. The content ID is used by the browser to generate the license request. If you don't use the From c37fb4670358e65db1b41693c54eb4e9dd3fb1e8 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 20 Dec 2021 15:33:22 +0100 Subject: [PATCH 09/30] add error test in hls parser --- test/hls/hls_parser_unit.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index b73af93877..6c5afe95aa 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -2463,6 +2463,34 @@ describe('HlsParser', () => { }); }); + it('if FairPlay encryption with MSE and mp2t content', async () => { + const master = [ + '#EXTM3U\n', + '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",', + 'RESOLUTION=960x540,FRAME-RATE=60\n', + 'video\n', + ].join(''); + + const media = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:6\n', + '#EXT-X-PLAYLIST-TYPE:VOD\n', + '#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,', + 'KEYFORMAT="com.apple.streamingkeydelivery",', + 'URI="skd://f93d4e700d7ddde90529a27735d9e7cb",\n', + '#EXTINF:5,\n', + '#EXT-X-BYTERANGE:121090@616\n', + 'main.ts', + ].join(''); + + const error = new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MANIFEST, + Code.HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED); + + await verifyError(master, media, error); + }); + describe('if required tags are missing', () => { /** * @param {string} master From bac1ca8e1891d69b92e338eec217fa8f4ada7bff Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Tue, 21 Dec 2021 16:27:33 +0100 Subject: [PATCH 10/30] fixup drm engine tests --- lib/media/drm_engine.js | 26 +++-- test/media/drm_engine_unit.js | 209 +++++++++++++++------------------- 2 files changed, 106 insertions(+), 129 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 8e9af305ae..54ab09a124 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -389,8 +389,9 @@ shaka.media.DrmEngine = class { /** * Attach MediaKeys to the video element * @return {!Promise} + * @private */ - async attachMediaKeys() { + async attachMediaKeys_() { if (this.video_.mediaKeys) { return; } @@ -406,21 +407,22 @@ shaka.media.DrmEngine = class { shaka.util.Error.Code.FAILED_TO_ATTACH_TO_VIDEO, exception.message)); } + + this.destroyer_.ensureNotDestroyed(); } /** * Processes encrypted event and start licence challenging * @return {!Promise} + * @private */ - async onEncryptedEvent(event) { + async onEncryptedEvent_(event) { /** * MediaKeys should be added when receiving an encrypted event. Setting * mediaKeys before could result into encrypted event not being fired on * some browsers */ - await this.attachMediaKeys(); - - this.destroyer_.ensureNotDestroyed(); + await this.attachMediaKeys_(); this.newInitData( event.initDataType, @@ -461,17 +463,21 @@ shaka.media.DrmEngine = class { () => this.closeOpenSessions_()); } + const nonEmptyInitData = this.currentDrmInfo_.initData.find( + (initDataOverride) => initDataOverride.initData.length > 0); + /** * Legacy implementations requires MediaKeys to be set before having * webkitneedkey / msneedkey event, which will be translated as an * encrypted event by the polyfills + * + * If some initData already has been generated, we need to attach media + * keys so we can start session before playback actually begins */ - if (shaka.util.Platform.isMediaKeysPolyfilled()) { - await this.attachMediaKeys(); + if (nonEmptyInitData || shaka.util.Platform.isMediaKeysPolyfilled()) { + await this.attachMediaKeys_(); } - this.destroyer_.ensureNotDestroyed(); - this.createOrLoad(); // Explicit init data an offline session is @@ -479,7 +485,7 @@ shaka.media.DrmEngine = class { if (!this.offlineSessionIds_.length) { this.eventManager_.listen( // eslint-disable-next-line no-restricted-syntax - this.video_, 'encrypted', this.onEncryptedEvent.bind(this)); + this.video_, 'encrypted', this.onEncryptedEvent_.bind(this)); } } diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 9b98f8314c..d9afa06d2c 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -776,10 +776,10 @@ describe('DrmEngine', () => { expect(mockVideo.setMediaKeys).not.toHaveBeenCalled(); }); - it('sets MediaKeys for encrypted content', async () => { - await initAndAttach(); - expect(mockVideo.setMediaKeys).toHaveBeenCalledWith(mockMediaKeys); - }); + // it('sets MediaKeys for encrypted content', async () => { + // await initAndAttach(); + // expect(mockVideo.setMediaKeys).toHaveBeenCalledWith(mockMediaKeys); + // }); it('sets server certificate if present in config', async () => { const cert = new Uint8Array(1); @@ -877,7 +877,7 @@ describe('DrmEngine', () => { /** @type {!Uint8Array} */ const initData1 = new Uint8Array(5); /** @type {!Uint8Array} */ - const initData2 = new Uint8Array(0); + const initData2 = new Uint8Array(1); /** @type {!Uint8Array} */ const initData3 = new Uint8Array(10); @@ -1019,12 +1019,23 @@ describe('DrmEngine', () => { mockVideo.setMediaKeys.and.returnValue(Promise.reject( new Error('whoops!'))); - const expected = Util.jasmineError(new shaka.util.Error( + tweakDrmInfos((drmInfos) => { + drmInfos[0].initData = [ + {initData: new Uint8Array(1), initDataType: 'cenc', keyId: null}, + ]; + }); + + onErrorSpy.and.stub(); + + await initAndAttach(); + + expect(onErrorSpy).toHaveBeenCalled(); + const error = onErrorSpy.calls.argsFor(0)[0]; + shaka.test.Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.FAILED_TO_ATTACH_TO_VIDEO, 'whoops!')); - await expectAsync(initAndAttach()).toBeRejectedWith(expected); }); it('fails with an error if setServerCertificate fails', async () => { @@ -1087,10 +1098,8 @@ describe('DrmEngine', () => { const initData1 = new Uint8Array(1); const initData2 = new Uint8Array(2); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData1, keyId: null}); - mockVideo.on['encrypted']( - {initDataType: 'cenc', initData: initData2, keyId: null}); + await sendEncryptedEvent('webm', initData1); + await sendEncryptedEvent('cenc', initData2); expect(mockMediaKeys.createSession).toHaveBeenCalledTimes(2); expect(session1.generateRequest) @@ -1101,32 +1110,23 @@ describe('DrmEngine', () => { it('suppresses duplicate initDatas', async () => { await initAndAttach(); + const initData1 = new Uint8Array(1); - const initData2 = new Uint8Array(1); // identical to initData1 - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData1, keyId: null}); - mockVideo.on['encrypted']( - {initDataType: 'cenc', initData: initData2, keyId: null}); + await sendEncryptedEvent('webm', initData1); + await sendEncryptedEvent('cenc'); // identical to webm initData expect(mockMediaKeys.createSession).toHaveBeenCalledTimes(1); expect(session1.generateRequest) .toHaveBeenCalledWith('webm', initData1); }); - it('is ignored when init data is in DrmInfo', async () => { - // Set up an init data override in the manifest: - tweakDrmInfos((drmInfos) => { - drmInfos[0].initData = [ - {initData: new Uint8Array(0), initDataType: 'cenc', keyId: null}, - ]; - }); - + it('set media keys when not already done at startup', async () => { await initAndAttach(); - // We already created a session for the init data override. + await sendEncryptedEvent(); + + expect(mockVideo.setMediaKeys).toHaveBeenCalledTimes(1); expect(mockMediaKeys.createSession).toHaveBeenCalledTimes(1); - // We aren't even listening for 'encrypted' events. - expect(mockVideo.on['encrypted']).toBe(undefined); }); it('dispatches an error if createSession fails', async () => { @@ -1134,9 +1134,7 @@ describe('DrmEngine', () => { onErrorSpy.and.stub(); await initAndAttach(); - const initData1 = new Uint8Array(1); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData1, keyId: null}); + await sendEncryptedEvent(); expect(onErrorSpy).toHaveBeenCalled(); const error = onErrorSpy.calls.argsFor(0)[0]; @@ -1156,9 +1154,7 @@ describe('DrmEngine', () => { onErrorSpy.and.stub(); await initAndAttach(); - const initData1 = new Uint8Array(1); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData1, keyId: null}); + await sendEncryptedEvent(); expect(onErrorSpy).toHaveBeenCalled(); const error = onErrorSpy.calls.argsFor(0)[0]; @@ -1172,9 +1168,7 @@ describe('DrmEngine', () => { describe('message', () => { it('is listened for', async () => { await initAndAttach(); - const initData = new Uint8Array(0); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); + await sendEncryptedEvent(); expect(session1.addEventListener).toHaveBeenCalledWith( 'message', jasmine.any(Function), jasmine.anything()); @@ -1214,9 +1208,7 @@ describe('DrmEngine', () => { onErrorSpy.and.stub(); await initAndAttach(); - const initData = new Uint8Array(0); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); + await sendEncryptedEvent(); // Simulate a permission error from the web server. const netError = new shaka.util.Error( @@ -1252,9 +1244,7 @@ describe('DrmEngine', () => { async function sendMessageTest( expectedUrl, messageType = 'license-request') { await initAndAttach(); - const initData = new Uint8Array(0); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); + await sendEncryptedEvent(); const operation = shaka.util.AbortableOperation.completed({}); fakeNetEngine.request.and.returnValue(operation); @@ -1276,9 +1266,7 @@ describe('DrmEngine', () => { describe('keystatuseschange', () => { it('is listened for', async () => { await initAndAttach(); - const initData = new Uint8Array(0); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); + await sendEncryptedEvent(); expect(session1.addEventListener).toHaveBeenCalledWith( 'keystatuseschange', jasmine.any(Function), jasmine.anything()); @@ -1286,9 +1274,7 @@ describe('DrmEngine', () => { it('triggers callback', async () => { await initAndAttach(); - const initData = new Uint8Array(0); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); + await sendEncryptedEvent(); const keyId1 = makeKeyId(1); const keyId2 = makeKeyId(2); @@ -1314,10 +1300,7 @@ describe('DrmEngine', () => { // See https://github.com/google/shaka-player/issues/1541 it('does not update public key statuses before callback', async () => { await initAndAttach(); - - const initData = new Uint8Array(0); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); + await sendEncryptedEvent(); const keyId1 = makeKeyId(1); const keyId2 = makeKeyId(2); @@ -1394,9 +1377,7 @@ describe('DrmEngine', () => { await initAndAttach(); expect(onErrorSpy).not.toHaveBeenCalled(); - const initData = new Uint8Array(0); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); + await sendEncryptedEvent(); const keyId1 = makeKeyId(1); const keyId2 = makeKeyId(2); @@ -1443,9 +1424,7 @@ describe('DrmEngine', () => { await initAndAttach(); expect(onErrorSpy).not.toHaveBeenCalled(); - const initData = new Uint8Array(0); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); + await sendEncryptedEvent(); const keyId1 = makeKeyId(1); const keyId2 = makeKeyId(2); @@ -1489,9 +1468,7 @@ describe('DrmEngine', () => { const license = new Uint8Array(0); await initAndAttach(); - const initData = new Uint8Array(0); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); + await sendEncryptedEvent(); fakeNetEngine.setResponseValue('http://abc.drm/license', license); const message = new Uint8Array(0); @@ -1550,9 +1527,8 @@ describe('DrmEngine', () => { it('publishes an event if update succeeds', async () => { await initAndAttach(); - const initData = new Uint8Array(1); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); + await sendEncryptedEvent(); + const message = new Uint8Array(0); session1.on['message']({target: session1, message: message}); session1.update.and.returnValue(Promise.resolve()); @@ -1568,9 +1544,7 @@ describe('DrmEngine', () => { const license = new Uint8Array(0); await initAndAttach(); - const initData = new Uint8Array(0); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); + await sendEncryptedEvent(); fakeNetEngine.setResponseValue('http://abc.drm/license', license); const message = new Uint8Array(0); @@ -1591,12 +1565,9 @@ describe('DrmEngine', () => { describe('destroy', () => { it('tears down MediaKeys and active sessions', async () => { await initAndAttach(); - const initData1 = new Uint8Array(1); - const initData2 = new Uint8Array(2); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData1, keyId: null}); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData2, keyId: null}); + + await sendEncryptedEvent('webm'); + await sendEncryptedEvent('cenc', new Uint8Array(2)); const message = new Uint8Array(0); session1.on['message']({target: session1, message: message}); @@ -1621,12 +1592,8 @@ describe('DrmEngine', () => { drmEngine.configure(config); await initAndAttach(); - const initData1 = new Uint8Array(1); - const initData2 = new Uint8Array(2); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData1, keyId: null}); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData2, keyId: null}); + await sendEncryptedEvent('webm'); + await sendEncryptedEvent('cenc', new Uint8Array(2)); const message = new Uint8Array(0); session1.on['message']({target: session1, message: message}); @@ -1647,12 +1614,8 @@ describe('DrmEngine', () => { it('swallows errors when closing sessions', async () => { await initAndAttach(); - const initData1 = new Uint8Array(1); - const initData2 = new Uint8Array(2); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData1, keyId: null}); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData2, keyId: null}); + await sendEncryptedEvent('webm'); + await sendEncryptedEvent('cenc', new Uint8Array(2)); const message = new Uint8Array(0); session1.on['message']({target: session1, message: message}); @@ -1668,12 +1631,8 @@ describe('DrmEngine', () => { it('swallows errors when clearing MediaKeys', async () => { await initAndAttach(); - const initData1 = new Uint8Array(1); - const initData2 = new Uint8Array(2); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData1, keyId: null}); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData2, keyId: null}); + await sendEncryptedEvent('webm'); + await sendEncryptedEvent('cenc', new Uint8Array(2)); const message = new Uint8Array(0); session1.on['message']({target: session1, message: message}); @@ -1752,6 +1711,14 @@ describe('DrmEngine', () => { const p1 = new shaka.util.PublicPromise(); mockVideo.setMediaKeys.and.returnValue(p1); + onErrorSpy.and.stub(); + + tweakDrmInfos((drmInfos) => { + drmInfos[0].initData = [ + {initData: new Uint8Array(1), initDataType: 'cenc', keyId: null}, + ]; + }); + const init = expectAsync(initAndAttach()).toBeRejected(); await shaka.test.Util.shortDelay(); @@ -1765,10 +1732,12 @@ describe('DrmEngine', () => { const destroy = drmEngine.destroy(); const fail = async () => { await shaka.test.Util.shortDelay(); - p1.reject(new Error('')); + shaka.log.warning('fail'); + p1.reject(new Error('titi')); }; const success = async () => { await shaka.test.Util.shortDelay(); + shaka.log.warning('success'); p2.resolve(); }; await Promise.all([init, destroy, fail(), success()]); @@ -1780,6 +1749,12 @@ describe('DrmEngine', () => { const p1 = new shaka.util.PublicPromise(); mockVideo.setMediaKeys.and.returnValue(p1); + tweakDrmInfos((drmInfos) => { + drmInfos[0].initData = [ + {initData: new Uint8Array(1), initDataType: 'cenc', keyId: null}, + ]; + }); + const init = expectAsync(initAndAttach()).toBeRejected(); await shaka.test.Util.shortDelay(); @@ -1857,9 +1832,7 @@ describe('DrmEngine', () => { session1.generateRequest.and.returnValue(p); await initAndAttach(); - const initData1 = new Uint8Array(1); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData1, keyId: null}); + await sendEncryptedEvent(); // We are now blocked on generateRequest: expect(session1.generateRequest).toHaveBeenCalledTimes(1); @@ -1878,9 +1851,7 @@ describe('DrmEngine', () => { fakeNetEngine.request.and.returnValue(operation); await initAndAttach(); - const initData1 = new Uint8Array(1); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData1, keyId: null}); + await sendEncryptedEvent(); const message = new Uint8Array(0); session1.on['message']({target: session1, message: message}); @@ -1909,9 +1880,7 @@ describe('DrmEngine', () => { fakeNetEngine.request.and.returnValue(operation); await initAndAttach(); - const initData1 = new Uint8Array(1); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData1, keyId: null}); + await sendEncryptedEvent(); const message = new Uint8Array(0); session1.on['message']({target: session1, message: message}); @@ -1939,9 +1908,7 @@ describe('DrmEngine', () => { session1.update.and.returnValue(p); await initAndAttach(); - const initData1 = new Uint8Array(1); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData1, keyId: null}); + await sendEncryptedEvent(); const message = new Uint8Array(0); session1.on['message']({target: session1, message: message}); @@ -1975,12 +1942,8 @@ describe('DrmEngine', () => { session1.close.and.returnValue(rejected); session2.close.and.returnValue(rejected); - const initData1 = new Uint8Array(1); - const initData2 = new Uint8Array(2); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData1, keyId: null}); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData2, keyId: null}); + await sendEncryptedEvent('webm'); + await sendEncryptedEvent('cenc', new Uint8Array(2)); // Still resolve these since we are mocking close and closed. This // ensures DrmEngine is in the correct state. @@ -2205,9 +2168,7 @@ describe('DrmEngine', () => { mockVideo.paused = true; await initAndAttach(); - const initData = new Uint8Array(0); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); + await sendEncryptedEvent(); const operation = shaka.util.AbortableOperation.completed({}); fakeNetEngine.request.and.returnValue(operation); @@ -2233,9 +2194,7 @@ describe('DrmEngine', () => { mockVideo.paused = true; await initAndAttach(); - const initData = new Uint8Array(0); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); + await sendEncryptedEvent(); const operation = shaka.util.AbortableOperation.completed({}); fakeNetEngine.request.and.returnValue(operation); @@ -2350,10 +2309,9 @@ describe('DrmEngine', () => { session1.expiration = NaN; await initAndAttach(); - const initData = new Uint8Array(0); + await sendEncryptedEvent(); + const message = new Uint8Array(0); - mockVideo.on['encrypted']( - {initDataType: 'webm', initData: initData, keyId: null}); session1.on['message']({target: session1, message: message}); session1.update.and.returnValue(Promise.resolve()); }); @@ -2516,4 +2474,17 @@ describe('DrmEngine', () => { callback(manifest.variants[0].audio.drmInfos); } } + + /** + * + * @param {string} initDataType + * @param {Uint8Array} initData + * @param {string|null} keyId + */ + async function sendEncryptedEvent( + initDataType = 'cenc', initData = new Uint8Array(1), keyId = null) { + mockVideo.on['encrypted']({initDataType, initData, keyId}); + + await Util.shortDelay(); + } }); From 06d8482fa9bc6566ba35968a2b106ea0fff0b6ca Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Thu, 23 Dec 2021 09:44:01 +0100 Subject: [PATCH 11/30] remove useless test --- test/media/drm_engine_unit.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index d9afa06d2c..4851324c26 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -776,11 +776,6 @@ describe('DrmEngine', () => { expect(mockVideo.setMediaKeys).not.toHaveBeenCalled(); }); - // it('sets MediaKeys for encrypted content', async () => { - // await initAndAttach(); - // expect(mockVideo.setMediaKeys).toHaveBeenCalledWith(mockMediaKeys); - // }); - it('sets server certificate if present in config', async () => { const cert = new Uint8Array(1); config.advanced['drm.abc'] = createAdvancedConfig(cert); From c7ea708f469421dbfd42b3b4abfb10ccc64a54de Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 21 Jan 2022 10:48:27 +0100 Subject: [PATCH 12/30] rename platform polyfill detect will => should --- lib/hls/hls_parser.js | 2 +- lib/media/drm_engine.js | 4 ++-- lib/polyfill/patchedmediakeys_apple.js | 2 +- lib/polyfill/patchedmediakeys_ms.js | 2 +- lib/polyfill/patchedmediakeys_webkit.js | 2 +- lib/util/platform.js | 14 +++++++------- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 12caf281a1..cdbf359084 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -2822,7 +2822,7 @@ shaka.hls.HlsParser = class { shaka.util.Error.Code.HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED); } - if (shaka.util.Platform.shouldPolyfillMediaKeysApple()) { + if (shaka.util.Platform.willMediaKeysAppleBePolyfilled()) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 54ab09a124..8d82c99c29 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -474,7 +474,7 @@ shaka.media.DrmEngine = class { * If some initData already has been generated, we need to attach media * keys so we can start session before playback actually begins */ - if (nonEmptyInitData || shaka.util.Platform.isMediaKeysPolyfilled()) { + if (nonEmptyInitData || shaka.util.Platform.willMediaKeysBePolyfilled()) { await this.attachMediaKeys_(); } @@ -1241,7 +1241,7 @@ shaka.media.DrmEngine = class { * initDataTransform is only necessary when using legacy protection * APIs, so prevent doing any transform when using the EME HTML5 spec */ - if (shaka.util.Platform.isMediaKeysPolyfilled()) { + if (shaka.util.Platform.willMediaKeysBePolyfilled()) { try { initData = this.config_.initDataTransform( initData, initDataType, this.currentDrmInfo_); diff --git a/lib/polyfill/patchedmediakeys_apple.js b/lib/polyfill/patchedmediakeys_apple.js index 92101e6a1e..2c37feae9e 100644 --- a/lib/polyfill/patchedmediakeys_apple.js +++ b/lib/polyfill/patchedmediakeys_apple.js @@ -31,7 +31,7 @@ shaka.polyfill.PatchedMediaKeysApple = class { * @export */ static install() { - if (!shaka.util.Platform.shouldPolyfillMediaKeysApple()) { + if (!shaka.util.Platform.willMediaKeysAppleBePolyfilled()) { return; } diff --git a/lib/polyfill/patchedmediakeys_ms.js b/lib/polyfill/patchedmediakeys_ms.js index 466713e2ea..095011d524 100644 --- a/lib/polyfill/patchedmediakeys_ms.js +++ b/lib/polyfill/patchedmediakeys_ms.js @@ -33,7 +33,7 @@ shaka.polyfill.PatchedMediaKeysMs = class { * @export */ static install() { - if (!shaka.util.Platform.shouldPolyfillMediaKeysMS()) { + if (!shaka.util.Platform.willMediaKeysMSBePolyfilled()) { return; } diff --git a/lib/polyfill/patchedmediakeys_webkit.js b/lib/polyfill/patchedmediakeys_webkit.js index a5b3afe0d5..4a6fe9e9a6 100644 --- a/lib/polyfill/patchedmediakeys_webkit.js +++ b/lib/polyfill/patchedmediakeys_webkit.js @@ -32,7 +32,7 @@ shaka.polyfill.PatchedMediaKeysWebkit = class { * @export */ static install() { - if (!shaka.util.Platform.shouldPolyfillMediaKeysWebkit()) { + if (!shaka.util.Platform.willMediaKeysWebkitBePolyfilled()) { return; } diff --git a/lib/util/platform.js b/lib/util/platform.js index 768faff388..bcc6be0211 100644 --- a/lib/util/platform.js +++ b/lib/util/platform.js @@ -325,7 +325,7 @@ shaka.util.Platform = class { * * @return {boolean} */ - static shouldPolyfillMediaKeysApple() { + static willMediaKeysAppleBePolyfilled() { if (!window.HTMLVideoElement || !window.WebKitMediaKeys) { return false; } @@ -349,7 +349,7 @@ shaka.util.Platform = class { * * @return {boolean} */ - static shouldPolyfillMediaKeysWebkit() { + static willMediaKeysWebkitBePolyfilled() { if (!window.HTMLVideoElement || (navigator.requestMediaKeySystemAccess && // eslint-disable-next-line no-restricted-syntax @@ -368,7 +368,7 @@ shaka.util.Platform = class { * * @return {boolean} */ - static shouldPolyfillMediaKeysMS() { + static willMediaKeysMSBePolyfilled() { return window.HTMLVideoElement && window.MSMediaKeys && (!navigator.requestMediaKeySystemAccess || // eslint-disable-next-line no-restricted-syntax @@ -381,10 +381,10 @@ shaka.util.Platform = class { * * @return {boolean} */ - static isMediaKeysPolyfilled() { - return shaka.util.Platform.shouldPolyfillMediaKeysApple() || - shaka.util.Platform.shouldPolyfillMediaKeysWebkit() || - shaka.util.Platform.shouldPolyfillMediaKeysWebkit(); + static willMediaKeysBePolyfilled() { + return shaka.util.Platform.willMediaKeysAppleBePolyfilled() || + shaka.util.Platform.willMediaKeysWebkitBePolyfilled() || + shaka.util.Platform.willMediaKeysWebkitBePolyfilled(); } }; From 1f60cb0cbf4d3e8ae6b8a346ac246168245ad8fd Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 21 Jan 2022 10:49:23 +0100 Subject: [PATCH 13/30] remove eslint disable on drm engine --- lib/media/drm_engine.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 8d82c99c29..5500510d3a 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -484,8 +484,7 @@ shaka.media.DrmEngine = class { // sufficient to suppress 'encrypted' events for all streams. if (!this.offlineSessionIds_.length) { this.eventManager_.listen( - // eslint-disable-next-line no-restricted-syntax - this.video_, 'encrypted', this.onEncryptedEvent_.bind(this)); + this.video_, 'encrypted', (e) => this.onEncryptedEvent_(e)); } } From 854d6eb992f1918087ba43cb60d203e1c1625fbb Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 21 Jan 2022 10:54:19 +0100 Subject: [PATCH 14/30] documentation adjustements --- README.md | 6 +++--- docs/tutorials/fairplay.md | 2 +- externs/shaka/player.js | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 987da933ac..c9e2c61490 100644 --- a/README.md +++ b/README.md @@ -171,9 +171,9 @@ NOTES: |HLS |**Y** |**Y** |**Y** ¹ | - | NOTES: - - ¹: We support FairPlay through MSE/EME when available and when - `useNativeHlsOnSafari` is turned off. Otherwise, we are using - Apple's native HLS player. + - ¹: By default, FairPlay is handled using Apple's native HLS player, when on + Safari. We do support FairPlay through MSE/EME, however. See the + `streaming.useNativeHlsOnSafari` configuration value. ## Media container and subtitle support diff --git a/docs/tutorials/fairplay.md b/docs/tutorials/fairplay.md index 33a27c6296..d223f05fc8 100644 --- a/docs/tutorials/fairplay.md +++ b/docs/tutorials/fairplay.md @@ -24,7 +24,7 @@ player.configure('drm.advanced.com\\.apple\\.fps\\.serverCertificateUri', ## Content ID -Note: This is specific when legacy Apple Media Keys is used. +Note: This only applies when legacy Apple Media Keys is used. Some FairPlay content use custom signaling for the content ID. The content ID is used by the browser to generate the license request. If you don't use the diff --git a/externs/shaka/player.js b/externs/shaka/player.js index d03cf195f5..ddeb2b0fae 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -934,6 +934,9 @@ shaka.extern.ManifestConfiguration; * @property {boolean} useNativeHlsOnSafari * Desktop Safari has both MediaSource and their native HLS implementation. * Depending on the application's needs, it may prefer one over the other. + * Warning: Where single-key HLS Fairplay streams work fine, multi-keys + * streams is showing unexpected behaviours (stall, audio playing with video + * freezes, ...). Use with care. * @property {number} inaccurateManifestTolerance * The maximum difference, in seconds, between the times in the manifest and * the times in the segments. Larger values allow us to compensate for more From e03cae755708c9e3d8d9effc3d6439ba056bb18e Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 21 Jan 2022 16:41:13 +0100 Subject: [PATCH 15/30] fixes conccurence on attachMediaKeys "gets a license and can play encrypted segments" e2e --- lib/media/drm_engine.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 5500510d3a..0f7f685722 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -128,6 +128,9 @@ shaka.media.DrmEngine = class { /** @private {boolean} */ this.srcEquals_ = false; + + /** @private {Promise} */ + this.mediaKeysAttached_ = null; } /** @override */ @@ -184,6 +187,7 @@ shaka.media.DrmEngine = class { this.onError_ = () => {}; this.playerInterface_ = null; this.srcEquals_ = false; + this.mediaKeysAttached_ = null; } /** @@ -396,8 +400,15 @@ shaka.media.DrmEngine = class { return; } + // An attach process has already started, let's wait it out + if (this.mediaKeysAttached_) { + return this.mediaKeysAttached_; + } + try { - await this.video_.setMediaKeys(this.mediaKeys_); + this.mediaKeysAttached_ = this.video_.setMediaKeys(this.mediaKeys_); + + await this.mediaKeysAttached_; } catch (exception) { goog.asserts.assert(exception instanceof Error, 'Wrong error type!'); From d6e8aee5aba546ed77062ca50e0a7ca951ac9134 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 21 Jan 2022 17:01:19 +0100 Subject: [PATCH 16/30] fix offline session loading --- lib/media/drm_engine.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 0f7f685722..483a6bdb36 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -392,7 +392,7 @@ shaka.media.DrmEngine = class { /** * Attach MediaKeys to the video element - * @return {!Promise} + * @return {Promise} * @private */ async attachMediaKeys_() { @@ -402,7 +402,9 @@ shaka.media.DrmEngine = class { // An attach process has already started, let's wait it out if (this.mediaKeysAttached_) { - return this.mediaKeysAttached_; + await this.mediaKeysAttached_; + + return; } try { @@ -446,6 +448,8 @@ shaka.media.DrmEngine = class { * @return {!Promise} */ async attach(video) { + shaka.log.warning('ATTACH'); + if (!this.mediaKeys_) { // Unencrypted, or so we think. We listen for encrypted events in order // to warn when the stream is encrypted, even though the manifest does @@ -478,14 +482,16 @@ shaka.media.DrmEngine = class { (initDataOverride) => initDataOverride.initData.length > 0); /** - * Legacy implementations requires MediaKeys to be set before having - * webkitneedkey / msneedkey event, which will be translated as an - * encrypted event by the polyfills - * - * If some initData already has been generated, we need to attach media - * keys so we can start session before playback actually begins + * We can attach media keys before the playback actually begins when: + * - Using legacy implementations requires MediaKeys to be set before + * having webkitneedkey / msneedkey event, which will be translated as + * an encrypted event by the polyfills + * - Some initData already has been generated (through the manifest) + * - In case of an offline session */ - if (nonEmptyInitData || shaka.util.Platform.willMediaKeysBePolyfilled()) { + if (nonEmptyInitData || + shaka.util.Platform.willMediaKeysBePolyfilled() || + this.offlineSessionIds_.length) { await this.attachMediaKeys_(); } From e9be15dec578f1613b907bc59bbb66d4d16b78c7 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 21 Jan 2022 17:13:29 +0100 Subject: [PATCH 17/30] fixup clearkey playback --- lib/media/drm_engine.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 483a6bdb36..665dc0c175 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -448,8 +448,6 @@ shaka.media.DrmEngine = class { * @return {!Promise} */ async attach(video) { - shaka.log.warning('ATTACH'); - if (!this.mediaKeys_) { // Unencrypted, or so we think. We listen for encrypted events in order // to warn when the stream is encrypted, even though the manifest does @@ -499,7 +497,7 @@ shaka.media.DrmEngine = class { // Explicit init data an offline session is // sufficient to suppress 'encrypted' events for all streams. - if (!this.offlineSessionIds_.length) { + if (!nonEmptyInitData && !this.offlineSessionIds_.length) { this.eventManager_.listen( this.video_, 'encrypted', (e) => this.onEncryptedEvent_(e)); } From 1f659a7463f2b6dbcddd88b34c50ded21c28913b Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 24 Jan 2022 10:09:24 +0100 Subject: [PATCH 18/30] prevent clearkey test to run on Safari (like before) --- lib/media/drm_engine.js | 9 +++++++++ lib/util/platform.js | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 665dc0c175..a39dd3ed8c 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1710,6 +1710,15 @@ shaka.media.DrmEngine = class { const testSystem = async (keySystem) => { try { + // Our Polyfill will reject anything apart com.apple.fps key systems. + // It seems the Safari modern EME API will allow to request a + // MediaKeySystemAccess for the ClearKey CDM, create and update a key + // session but playback will never start + if (keySystem === 'org.w3.clearkey' && + shaka.util.Platform.isSafari()) { + throw new Error('Unsupported keySystem'); + } + const access = await navigator.requestMediaKeySystemAccess( keySystem, configs); diff --git a/lib/util/platform.js b/lib/util/platform.js index bcc6be0211..34d1bdd76a 100644 --- a/lib/util/platform.js +++ b/lib/util/platform.js @@ -228,6 +228,16 @@ shaka.util.Platform = class { return null; } + /** + * Check if the current platform is Apple Safari + * or Safari-based iOS browsers. + * + * @return {boolean} + */ + static isSafari() { + return !!shaka.util.Platform.safariVersion(); + } + /** * Guesses if the platform is a mobile one (iOS or Android). * From df1cee7dd471456f2a12dd3be579f58ab129d0a5 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Tue, 25 Jan 2022 09:25:24 +0100 Subject: [PATCH 19/30] revert comment --- lib/media/drm_engine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index a39dd3ed8c..7192021107 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -495,7 +495,7 @@ shaka.media.DrmEngine = class { this.createOrLoad(); - // Explicit init data an offline session is + // Explicit init data for any one stream or an offline session is // sufficient to suppress 'encrypted' events for all streams. if (!nonEmptyInitData && !this.offlineSessionIds_.length) { this.eventManager_.listen( From 1ca5399b4f0b95c6d480d4ab4ff301d0045317cd Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Tue, 25 Jan 2022 09:26:30 +0100 Subject: [PATCH 20/30] add hint to webkit bug tracker for clearkey --- lib/media/drm_engine.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 7192021107..b6997c2715 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1714,6 +1714,7 @@ shaka.media.DrmEngine = class { // It seems the Safari modern EME API will allow to request a // MediaKeySystemAccess for the ClearKey CDM, create and update a key // session but playback will never start + // Safari bug: https://bugs.webkit.org/show_bug.cgi?id=231006 if (keySystem === 'org.w3.clearkey' && shaka.util.Platform.isSafari()) { throw new Error('Unsupported keySystem'); From acf47d9557fc3457d8ed89d013283eebbe7f11c6 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Tue, 25 Jan 2022 10:09:15 +0100 Subject: [PATCH 21/30] change how we detect polyfill MediaKeys --- externs/polyfill.js | 15 ++++++ lib/hls/hls_parser.js | 2 +- lib/media/drm_engine.js | 4 +- lib/polyfill/patchedmediakeys_apple.js | 66 +++++++++---------------- lib/polyfill/patchedmediakeys_ms.js | 9 ++-- lib/polyfill/patchedmediakeys_nop.js | 2 + lib/polyfill/patchedmediakeys_webkit.js | 16 ++++-- lib/util/platform.js | 41 ++------------- 8 files changed, 66 insertions(+), 89 deletions(-) create mode 100644 externs/polyfill.js diff --git a/externs/polyfill.js b/externs/polyfill.js new file mode 100644 index 0000000000..89d5f58147 --- /dev/null +++ b/externs/polyfill.js @@ -0,0 +1,15 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Externs for Shaka polyfills + * + * @externs + */ + + +/** @type {boolean} */ +window.shakaMediaKeysPolyfill; diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index cdbf359084..0d60e27e7a 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -2822,7 +2822,7 @@ shaka.hls.HlsParser = class { shaka.util.Error.Code.HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED); } - if (shaka.util.Platform.willMediaKeysAppleBePolyfilled()) { + if (shaka.util.Platform.isMediaKeysPolyfilled()) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index b6997c2715..51c08d4ea3 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -488,7 +488,7 @@ shaka.media.DrmEngine = class { * - In case of an offline session */ if (nonEmptyInitData || - shaka.util.Platform.willMediaKeysBePolyfilled() || + shaka.util.Platform.isMediaKeysPolyfilled() || this.offlineSessionIds_.length) { await this.attachMediaKeys_(); } @@ -1255,7 +1255,7 @@ shaka.media.DrmEngine = class { * initDataTransform is only necessary when using legacy protection * APIs, so prevent doing any transform when using the EME HTML5 spec */ - if (shaka.util.Platform.willMediaKeysBePolyfilled()) { + if (shaka.util.Platform.isMediaKeysPolyfilled()) { try { initData = this.config_.initDataTransform( initData, initDataType, this.currentDrmInfo_); diff --git a/lib/polyfill/patchedmediakeys_apple.js b/lib/polyfill/patchedmediakeys_apple.js index 2c37feae9e..09a309293f 100644 --- a/lib/polyfill/patchedmediakeys_apple.js +++ b/lib/polyfill/patchedmediakeys_apple.js @@ -31,7 +31,17 @@ shaka.polyfill.PatchedMediaKeysApple = class { * @export */ static install() { - if (!shaka.util.Platform.willMediaKeysAppleBePolyfilled()) { + if (!window.HTMLVideoElement || !window.WebKitMediaKeys) { + // No HTML5 video or no prefixed EME. + return; + } + + const safariVersion = shaka.util.Platform.safariVersion(); + if (navigator.requestMediaKeySystemAccess && + // eslint-disable-next-line no-restricted-syntax + MediaKeySystemAccess.prototype.getConfiguration && + safariVersion && safariVersion >= 14) { + // Unprefixed EME is preferable. return; } @@ -55,6 +65,8 @@ shaka.polyfill.PatchedMediaKeysApple = class { window.MediaKeySystemAccess = PatchedMediaKeysApple.MediaKeySystemAccess; navigator.requestMediaKeySystemAccess = PatchedMediaKeysApple.requestMediaKeySystemAccess; + + window.shakaMediaKeysPolyfill = true; } /** @@ -126,26 +138,6 @@ shaka.polyfill.PatchedMediaKeysApple = class { return Promise.resolve(); } - /** - * - * @param {!MediaKeyEvent} sourceEvent - * @param {string} initDataType - * @param {ArrayBuffer} initData - * @private - */ - static dispatchEncryptedEvent_(sourceEvent, initDataType, initData) { - // NOTE: Because "this" is a real EventTarget, the event we dispatch here - // must also be a real Event. - const event = new Event('encrypted'); - - const encryptedEvent = - /** @type {!MediaEncryptedEvent} */(/** @type {?} */(event)); - encryptedEvent.initDataType = initDataType; - encryptedEvent.initData = initData; - - sourceEvent.target.dispatchEvent(event); - } - /** * Handler for the native media elements webkitneedkey event. * @@ -169,23 +161,6 @@ shaka.polyfill.PatchedMediaKeysApple = class { // Convert the prefixed init data to match the native 'encrypted' event. const uint8 = shaka.util.BufferUtils.toUint8(event.initData); - - try { - const base64 = shaka.util.StringUtils.fromCharCode(uint8); - const data = JSON.parse(base64); - - if (data) { - PatchedMediaKeysApple.dispatchEncryptedEvent_( - event, - 'sinf', - shaka.util.BufferUtils.toArrayBuffer(event.initData)); - - return; - } - } catch (error) { - // Continue with SKD initDataType detection - } - const dataview = shaka.util.BufferUtils.toDataView(uint8); // The first part is a 4 byte little-endian int, which is the length of // the second part. @@ -199,10 +174,16 @@ shaka.polyfill.PatchedMediaKeysApple = class { uint8.subarray(4), /* littleEndian= */ true); const initData = shaka.util.StringUtils.toUTF8(str); - PatchedMediaKeysApple.dispatchEncryptedEvent_( - event, - 'skd', - shaka.util.BufferUtils.toArrayBuffer(initData)); + // NOTE: Because "this" is a real EventTarget, the event we dispatch here + // must also be a real Event. + const event2 = new Event('encrypted'); + + const encryptedEvent = + /** @type {!MediaEncryptedEvent} */(/** @type {?} */(event2)); + encryptedEvent.initDataType = 'skd'; + encryptedEvent.initData = shaka.util.BufferUtils.toArrayBuffer(initData); + + this.dispatchEvent(event2); } }; @@ -757,4 +738,5 @@ shaka.polyfill.PatchedMediaKeysApple.MediaKeyStatusMap = class { } }; + shaka.polyfill.register(shaka.polyfill.PatchedMediaKeysApple.install); diff --git a/lib/polyfill/patchedmediakeys_ms.js b/lib/polyfill/patchedmediakeys_ms.js index 095011d524..9e46313115 100644 --- a/lib/polyfill/patchedmediakeys_ms.js +++ b/lib/polyfill/patchedmediakeys_ms.js @@ -17,7 +17,6 @@ goog.require('shaka.util.FakeEventTarget'); goog.require('shaka.util.MediaReadyState'); goog.require('shaka.util.Pssh'); goog.require('shaka.util.PublicPromise'); -goog.require('shaka.util.Platform'); /** @@ -33,10 +32,12 @@ shaka.polyfill.PatchedMediaKeysMs = class { * @export */ static install() { - if (!shaka.util.Platform.willMediaKeysMSBePolyfilled()) { + if (!window.HTMLVideoElement || !window.MSMediaKeys || + (navigator.requestMediaKeySystemAccess && + // eslint-disable-next-line no-restricted-syntax + MediaKeySystemAccess.prototype.getConfiguration)) { return; } - shaka.log.info('Using ms-prefixed EME v20140218'); // Alias @@ -57,6 +58,8 @@ shaka.polyfill.PatchedMediaKeysMs = class { // eslint-disable-next-line no-restricted-syntax HTMLMediaElement.prototype.setMediaKeys = PatchedMediaKeysMs.MediaKeySystemAccess.setMediaKeys; + + window.shakaMediaKeysPolyfill = true; } /** diff --git a/lib/polyfill/patchedmediakeys_nop.js b/lib/polyfill/patchedmediakeys_nop.js index be6bfd3d09..5ba9266c50 100644 --- a/lib/polyfill/patchedmediakeys_nop.js +++ b/lib/polyfill/patchedmediakeys_nop.js @@ -49,6 +49,8 @@ shaka.polyfill.PatchedMediaKeysNop = class { // These are not usable, but allow Player.isBrowserSupported to pass. window.MediaKeys = PatchedMediaKeysNop.MediaKeys; window.MediaKeySystemAccess = PatchedMediaKeysNop.MediaKeySystemAccess; + + window.shakaMediaKeysPolyfill = true; } /** diff --git a/lib/polyfill/patchedmediakeys_webkit.js b/lib/polyfill/patchedmediakeys_webkit.js index 4a6fe9e9a6..4ec47e8ce2 100644 --- a/lib/polyfill/patchedmediakeys_webkit.js +++ b/lib/polyfill/patchedmediakeys_webkit.js @@ -18,7 +18,7 @@ goog.require('shaka.util.PublicPromise'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.Timer'); goog.require('shaka.util.Uint8ArrayUtils'); -goog.require('shaka.util.Platform'); + /** * @summary A polyfill to implement @@ -32,13 +32,15 @@ shaka.polyfill.PatchedMediaKeysWebkit = class { * @export */ static install() { - if (!shaka.util.Platform.willMediaKeysWebkitBePolyfilled()) { - return; - } - // Alias. const PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit; + if (!window.HTMLVideoElement || + (navigator.requestMediaKeySystemAccess && + // eslint-disable-next-line no-restricted-syntax + MediaKeySystemAccess.prototype.getConfiguration)) { + return; + } // eslint-disable-next-line no-restricted-syntax if (HTMLMediaElement.prototype.webkitGenerateKeyRequest) { shaka.log.info('Using webkit-prefixed EME v0.1b'); @@ -46,6 +48,8 @@ shaka.polyfill.PatchedMediaKeysWebkit = class { // eslint-disable-next-line no-restricted-syntax } else if (HTMLMediaElement.prototype.generateKeyRequest) { shaka.log.info('Using nonprefixed EME v0.1b'); + } else { + return; } goog.asserts.assert( @@ -68,6 +72,8 @@ shaka.polyfill.PatchedMediaKeysWebkit = class { PatchedMediaKeysWebkit.setMediaKeys; window.MediaKeys = PatchedMediaKeysWebkit.MediaKeys; window.MediaKeySystemAccess = PatchedMediaKeysWebkit.MediaKeySystemAccess; + + window.shakaMediaKeysPolyfill = true; } /** diff --git a/lib/util/platform.js b/lib/util/platform.js index 34d1bdd76a..4f24f2a8b5 100644 --- a/lib/util/platform.js +++ b/lib/util/platform.js @@ -354,47 +354,16 @@ shaka.util.Platform = class { } /** - * Returns true if we need to polyfill Webkit legacy MediaKeys - * implementation + * Returns true if MediaKeys is polyfilled * * @return {boolean} */ - static willMediaKeysWebkitBePolyfilled() { - if (!window.HTMLVideoElement || - (navigator.requestMediaKeySystemAccess && - // eslint-disable-next-line no-restricted-syntax - MediaKeySystemAccess.prototype.getConfiguration)) { - return false; + static isMediaKeysPolyfilled() { + if (window.shakaMediaKeysPolyfill) { + return true; } - // eslint-disable-next-line no-restricted-syntax - return !!(HTMLMediaElement.prototype.webkitGenerateKeyRequest || - // eslint-disable-next-line no-restricted-syntax - HTMLMediaElement.prototype.generateKeyRequest); - } - - /** - * Returns true if we need to polyfill MS legacy MediaKeys implementation - * - * @return {boolean} - */ - static willMediaKeysMSBePolyfilled() { - return window.HTMLVideoElement && window.MSMediaKeys && - (!navigator.requestMediaKeySystemAccess || - // eslint-disable-next-line no-restricted-syntax - !MediaKeySystemAccess.prototype.getConfiguration); - } - - /** - * Returns true if any MediaKeys polyfill should have been - * installed - * - * @return {boolean} - */ - static willMediaKeysBePolyfilled() { - return shaka.util.Platform.willMediaKeysAppleBePolyfilled() || - shaka.util.Platform.willMediaKeysWebkitBePolyfilled() || - shaka.util.Platform.willMediaKeysWebkitBePolyfilled(); + return false; } }; From a3b9ec7a981b02b0ae3b11d103657249e62dca87 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Tue, 25 Jan 2022 10:12:03 +0100 Subject: [PATCH 22/30] enhance comment on useNativeHlsOnSafari --- externs/shaka/player.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/externs/shaka/player.js b/externs/shaka/player.js index ddeb2b0fae..93945d9feb 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -934,9 +934,9 @@ shaka.extern.ManifestConfiguration; * @property {boolean} useNativeHlsOnSafari * Desktop Safari has both MediaSource and their native HLS implementation. * Depending on the application's needs, it may prefer one over the other. - * Warning: Where single-key HLS Fairplay streams work fine, multi-keys - * streams is showing unexpected behaviours (stall, audio playing with video - * freezes, ...). Use with care. + * Warning when disabled: Where single-key HLS Fairplay streams work fine, + * multi-keys streams is showing unexpected behaviours (stall, audio playing + * with video freezes, ...). Use with care. * @property {number} inaccurateManifestTolerance * The maximum difference, in seconds, between the times in the manifest and * the times in the segments. Larger values allow us to compensate for more From f6fbcf1f290e68aee1cab4df47bf357645da06c9 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Tue, 25 Jan 2022 10:49:36 +0100 Subject: [PATCH 23/30] remove useless method in platform util --- lib/util/platform.js | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/lib/util/platform.js b/lib/util/platform.js index 4f24f2a8b5..49dce396cf 100644 --- a/lib/util/platform.js +++ b/lib/util/platform.js @@ -329,30 +329,6 @@ shaka.util.Platform = class { return Platform.isTizen() || Platform.isXboxOne(); } - /** - * Returns true if we need to polyfill Apple legacy MediaKeys - * implementation - * - * @return {boolean} - */ - static willMediaKeysAppleBePolyfilled() { - if (!window.HTMLVideoElement || !window.WebKitMediaKeys) { - return false; - } - - const safariVersion = shaka.util.Platform.safariVersion(); - - if (navigator.requestMediaKeySystemAccess && - // eslint-disable-next-line no-restricted-syntax - MediaKeySystemAccess.prototype.getConfiguration && - safariVersion && safariVersion >= 14) { - // Unprefixed EME is preferable. - return false; - } - - return true; - } - /** * Returns true if MediaKeys is polyfilled * From ed532c5aca53ad683e9e3e33e0fe2604596728c2 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 26 Jan 2022 17:26:18 +0100 Subject: [PATCH 24/30] make fps work with dash --- lib/util/player_configuration.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index b10432481e..26b0a89831 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -108,6 +108,8 @@ shaka.util.PlayerConfiguration = class { 'com.microsoft.playready', 'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime', + 'urn:uuid:94ce86fb-07ff-4f43-adb8-93d2fa968ca2': + 'com.apple.fps', }, // Need some operation in the callback or else closure may remove calls // to the function as it would be a no-op. The operation can't just be From 4bcd860b9e6fc8c8b4512747996f88d50633bd20 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 26 Jan 2022 17:28:36 +0100 Subject: [PATCH 25/30] remove reference to HLS --- externs/shaka/player.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 93945d9feb..139a3e252f 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -934,7 +934,7 @@ shaka.extern.ManifestConfiguration; * @property {boolean} useNativeHlsOnSafari * Desktop Safari has both MediaSource and their native HLS implementation. * Depending on the application's needs, it may prefer one over the other. - * Warning when disabled: Where single-key HLS Fairplay streams work fine, + * Warning when disabled: Where single-key DRM streams work fine, * multi-keys streams is showing unexpected behaviours (stall, audio playing * with video freezes, ...). Use with care. * @property {number} inaccurateManifestTolerance From 971987e87f0b3a25b9236658dd28914b980967be Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 26 Jan 2022 17:34:31 +0100 Subject: [PATCH 26/30] fixup doc --- externs/shaka/player.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 139a3e252f..5b17095be3 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -934,9 +934,9 @@ shaka.extern.ManifestConfiguration; * @property {boolean} useNativeHlsOnSafari * Desktop Safari has both MediaSource and their native HLS implementation. * Depending on the application's needs, it may prefer one over the other. - * Warning when disabled: Where single-key DRM streams work fine, - * multi-keys streams is showing unexpected behaviours (stall, audio playing - * with video freezes, ...). Use with care. + * Warning when disabled: Where single-key DRM streams work fine, multi-keys + * streams is showing unexpected behaviours (stall, audio playing with video + * freezes, ...). Use with care. * @property {number} inaccurateManifestTolerance * The maximum difference, in seconds, between the times in the manifest and * the times in the segments. Larger values allow us to compensate for more From 7f018a8d3396f199de2c093ee0d671f222fdfb7d Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Thu, 27 Jan 2022 08:28:49 +0100 Subject: [PATCH 27/30] fixup test with a new key system uid --- test/dash/dash_parser_content_protection_unit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/dash/dash_parser_content_protection_unit.js b/test/dash/dash_parser_content_protection_unit.js index f9a046bb9a..a9c973ba10 100644 --- a/test/dash/dash_parser_content_protection_unit.js +++ b/test/dash/dash_parser_content_protection_unit.js @@ -461,6 +461,7 @@ describe('DashParser ContentProtection', () => { buildDrmInfo('com.microsoft.playready', keyIds), buildDrmInfo('com.microsoft.playready', keyIds), buildDrmInfo('com.adobe.primetime', keyIds), + buildDrmInfo('com.apple.fps', keyIds), ], variantKeyIds); await testDashParser(source, expected, /* ignoreDrmInfo= */ true); }); From 159ea32f89897aa1c411664d374112a6c1c3d4b9 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Thu, 27 Jan 2022 09:09:06 +0100 Subject: [PATCH 28/30] fixup documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c9e2c61490..030b2ead31 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ NOTES: |Manifest |Widevine |PlayReady|FairPlay |ClearKey | |:--------:|:--------:|:-------:|:-------:|:--------:| -|DASH |**Y** |**Y** | - |**Y** | +|DASH |**Y** |**Y** |**Y** |**Y** | |HLS |**Y** |**Y** |**Y** ¹ | - | NOTES: From b1d78d814188c933a70f8fb4588fdd81908108c7 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 7 Feb 2022 16:23:55 +0100 Subject: [PATCH 29/30] rename nonEmptyInitData to manifestInitData --- lib/media/drm_engine.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 51c08d4ea3..889b205103 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -476,7 +476,7 @@ shaka.media.DrmEngine = class { () => this.closeOpenSessions_()); } - const nonEmptyInitData = this.currentDrmInfo_.initData.find( + const manifestInitData = this.currentDrmInfo_.initData.find( (initDataOverride) => initDataOverride.initData.length > 0); /** @@ -487,7 +487,7 @@ shaka.media.DrmEngine = class { * - Some initData already has been generated (through the manifest) * - In case of an offline session */ - if (nonEmptyInitData || + if (manifestInitData || shaka.util.Platform.isMediaKeysPolyfilled() || this.offlineSessionIds_.length) { await this.attachMediaKeys_(); @@ -497,7 +497,7 @@ shaka.media.DrmEngine = class { // Explicit init data for any one stream or an offline session is // sufficient to suppress 'encrypted' events for all streams. - if (!nonEmptyInitData && !this.offlineSessionIds_.length) { + if (!manifestInitData && !this.offlineSessionIds_.length) { this.eventManager_.listen( this.video_, 'encrypted', (e) => this.onEncryptedEvent_(e)); } From 9d313ed7e1e92f8e19373150d8292e49b64306d9 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 7 Feb 2022 16:28:22 +0100 Subject: [PATCH 30/30] remove DASH-related code --- README.md | 2 +- lib/util/player_configuration.js | 2 -- test/dash/dash_parser_content_protection_unit.js | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 030b2ead31..c9e2c61490 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ NOTES: |Manifest |Widevine |PlayReady|FairPlay |ClearKey | |:--------:|:--------:|:-------:|:-------:|:--------:| -|DASH |**Y** |**Y** |**Y** |**Y** | +|DASH |**Y** |**Y** | - |**Y** | |HLS |**Y** |**Y** |**Y** ¹ | - | NOTES: diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index 26b0a89831..b10432481e 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -108,8 +108,6 @@ shaka.util.PlayerConfiguration = class { 'com.microsoft.playready', 'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime', - 'urn:uuid:94ce86fb-07ff-4f43-adb8-93d2fa968ca2': - 'com.apple.fps', }, // Need some operation in the callback or else closure may remove calls // to the function as it would be a no-op. The operation can't just be diff --git a/test/dash/dash_parser_content_protection_unit.js b/test/dash/dash_parser_content_protection_unit.js index a9c973ba10..f9a046bb9a 100644 --- a/test/dash/dash_parser_content_protection_unit.js +++ b/test/dash/dash_parser_content_protection_unit.js @@ -461,7 +461,6 @@ describe('DashParser ContentProtection', () => { buildDrmInfo('com.microsoft.playready', keyIds), buildDrmInfo('com.microsoft.playready', keyIds), buildDrmInfo('com.adobe.primetime', keyIds), - buildDrmInfo('com.apple.fps', keyIds), ], variantKeyIds); await testDashParser(source, expected, /* ignoreDrmInfo= */ true); });