From 7ef0f924c36d0b839937e9f437a5ba860827a0a0 Mon Sep 17 00:00:00 2001 From: vlazh Date: Thu, 9 Jan 2025 20:18:23 +0500 Subject: [PATCH] feat: Add support for WisePlay DRM (#7854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some info: https://drmnow.pro/wiseplay/ https://developer.huawei.com/consumer/en/doc/Media-Guides/client-dev-0000001050040000 --------- Co-authored-by: Álvaro Velad Galván --- README.md | 43 ++++---- lib/drm/drm_engine.js | 1 + lib/hls/hls_parser.js | 43 ++++++++ lib/offline/storage.js | 3 +- lib/util/player_configuration.js | 2 + package-lock.json | 16 +-- package.json | 2 +- project-words.txt | 1 + .../dash_parser_content_protection_unit.js | 1 + test/hls/hls_parser_unit.js | 99 +++++++++++++++++++ 10 files changed, 180 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 3bb39f5f45..fdbd8016b1 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ HLS features supported: - MPEG-2 TS support - WebVTT and TTML - CEA-608/708 captions - - Encrypted content with PlayReady and Widevine + - Encrypted content with PlayReady, Widevine and WisePlay - Encrypted content with FairPlay (Safari on macOS and iOS 9+ only) - AES-128, AES-256 and AES-256-CTR support on browsers with Web Crypto API support - SAMPLE-AES and SAMPLE-AES-CTR (identity) support on browsers with ClearKey support @@ -227,21 +227,22 @@ MSS features **not** supported: ## DRM support matrix -|Browser |Widevine |PlayReady|FairPlay |ClearKey⁶ | -|:------------:|:--------:|:-------:|:-------:|:--------:| -|Chrome¹ |**Y** | - | - |**Y** | -|Firefox² |**Y** | - | - |**Y** | -|Edge³ | - |**Y** | - | - | -|Edge Chromium |**Y** |**Y** | - |**Y** | -|Safari | - | - |**Y** | - | -|Opera |**Y** | - | - |**Y** | -|Chromecast |**Y** |**Y** | - |**Y** | -|Tizen TV |**Y** |**Y** | - |**Y** | -|WebOS⁷ |untested⁷ |untested⁷| - |untested⁷ | -|Hisense⁷ |untested⁷ |untested⁷| - |untested⁷ | -|Xbox One | - |**Y** | - | - | -|Playstation 4⁷| - |untested⁷| - |untested⁷ | -|Playstation 5⁷| - |untested⁷| - |untested⁷ | +|Browser |Widevine |PlayReady|FairPlay |WisePlay |ClearKey⁶ | +|:------------:|:--------:|:-------:|:-------:|:-------:|:--------:| +|Chrome¹ |**Y** | - | - | - |**Y** | +|Firefox² |**Y** | - | - | - |**Y** | +|Edge³ | - |**Y** | - | - | - | +|Edge Chromium |**Y** |**Y** | - | - |**Y** | +|Safari | - | - |**Y** | - | - | +|Opera |**Y** | - | - | - |**Y** | +|Chromecast |**Y** |**Y** | - | - |**Y** | +|Tizen TV |**Y** |**Y** | - | - |**Y** | +|WebOS⁷ |untested⁷ |untested⁷| - | - |untested⁷ | +|Hisense⁷ |untested⁷ |untested⁷| - | - |untested⁷ | +|Xbox One | - |**Y** | - | - | - | +|Playstation 4⁷| - |untested⁷| - | - |untested⁷ | +|Playstation 5⁷| - |untested⁷| - | - |untested⁷ | +|Huawei⁷ | - | - | - |untested⁷|untested⁷ | Other DRM systems should work out of the box if they are interoperable and compliant to the EME spec. @@ -257,11 +258,11 @@ NOTES: - ⁷: These are expected to work, but are community-supported and untested by us. -|Manifest |Widevine |PlayReady|FairPlay |ClearKey | -|:--------:|:--------:|:-------:|:-------:|:--------:| -|DASH |**Y** |**Y** | - |**Y** | -|HLS |**Y** |**Y** |**Y** ¹ | - | -|MSS | - |**Y** | - | - | +|Manifest |Widevine |PlayReady|FairPlay |WisePlay |ClearKey | +|:--------:|:--------:|:-------:|:-------:|:-------:|:--------:| +|DASH |**Y** |**Y** |**Y** |**Y** |**Y** | +|HLS |**Y** |**Y** |**Y** ¹ |**Y** |**Y** | +|MSS | - |**Y** | - | - | - | NOTES: - ¹: By default, FairPlay is handled using Apple's native HLS player, when on diff --git a/lib/drm/drm_engine.js b/lib/drm/drm_engine.js index 4f4782be5e..880dfbfe78 100644 --- a/lib/drm/drm_engine.js +++ b/lib/drm/drm_engine.js @@ -1819,6 +1819,7 @@ shaka.drm.DrmEngine = class { 'com.chromecast.playready', 'com.apple.fps.1_0', 'com.apple.fps', + 'com.huawei.wiseplay', ]; if (!shaka.drm.DrmUtils.isBrowserSupported()) { diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 8fc8a63a5f..1aa536f1e2 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -4900,6 +4900,47 @@ shaka.hls.HlsParser = class { return drmInfo; } + /** + * @param {!shaka.hls.Tag} drmTag + * @return {?shaka.extern.DrmInfo} + * @private + */ + static wiseplayDrmParser_(drmTag) { + const method = drmTag.getRequiredAttrValue('METHOD'); + const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR']; + if (!VALID_METHODS.includes(method)) { + shaka.log.error('WisePlay in HLS is only supported with [', + VALID_METHODS.join(', '), '], not', method); + return null; + } + + let encryptionScheme = 'cenc'; + if (method == 'SAMPLE-AES') { + encryptionScheme = 'cbcs'; + } + + const uri = drmTag.getRequiredAttrValue('URI'); + const parsedData = shaka.net.DataUriPlugin.parseRaw(uri.split('?')[0]); + + // The data encoded in the URI is a PSSH box to be used as init data. + const pssh = shaka.util.BufferUtils.toUint8(parsedData.data); + const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo( + 'com.huawei.wiseplay', encryptionScheme, [ + {initDataType: 'cenc', initData: pssh}, + ]); + + const keyId = drmTag.getAttributeValue('KEYID'); + if (keyId) { + const keyIdLowerCase = keyId.toLowerCase(); + // This value should begin with '0x': + goog.asserts.assert( + keyIdLowerCase.startsWith('0x'), 'Incorrect KEYID format!'); + // But the output should not contain the '0x': + drmInfo.keyIds = new Set([keyIdLowerCase.substr(2)]); + } + return drmInfo; + } + /** * See: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-5.1 * @@ -5197,6 +5238,8 @@ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = { shaka.hls.HlsParser.widevineDrmParser_, 'com.microsoft.playready': shaka.hls.HlsParser.playreadyDrmParser_, + 'urn:uuid:3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c': + shaka.hls.HlsParser.wiseplayDrmParser_, }; diff --git a/lib/offline/storage.js b/lib/offline/storage.js index 49fee309f3..2e1ceebaed 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -1993,6 +1993,7 @@ shaka.offline.Storage.defaultSystemIds_ = new Map() .set('com.microsoft.playready.software', '9a04f07998404286ab92e65be0885f95') .set('com.microsoft.playready.hardware', - '9a04f07998404286ab92e65be0885f95'); + '9a04f07998404286ab92e65be0885f95') + .set('com.huawei.wiseplay', '3d5e6d359b9a41e8b843dd3c6e72c42c'); shaka.Player.registerSupportPlugin('offline', shaka.offline.Storage.support); diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index 0e7d803b39..95b5e07706 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -149,6 +149,8 @@ shaka.util.PlayerConfiguration = class { 'com.microsoft.playready', 'urn:uuid:94ce86fb-07ff-4f43-adb8-93d2fa968ca2': 'com.apple.fps', + 'urn:uuid:3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c': + 'com.huawei.wiseplay', }, manifestPreprocessor: shaka.util.PlayerConfiguration.defaultManifestPreprocessor, diff --git a/package-lock.json b/package-lock.json index 01459c19d1..035bd8adec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "4.12.0", "license": "Apache-2.0", "dependencies": { - "eme-encryption-scheme-polyfill": "^2.1.6" + "eme-encryption-scheme-polyfill": "^2.2.0" }, "devDependencies": { "@babel/core": "^7.17.5", @@ -66,7 +66,7 @@ "which": "^2.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "build/eslint-plugin-shaka-rules": { @@ -4672,9 +4672,9 @@ "dev": true }, "node_modules/eme-encryption-scheme-polyfill": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.1.6.tgz", - "integrity": "sha512-SmQ8UxDkH/8hrjLo6ASo452hIe4dSJzqKmJyrNsvUciEJNxf4z9hewIwF1k/c7A5uRk4GApavPZ6dgqXqfvegw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.2.0.tgz", + "integrity": "sha512-wfgRcR2cGAX0WKbPahhI13dr2mzFQ/rMnoDibRIdScQlv9S0rtnXd25XF17IUdNJw/voJoFpYQp33C3xkFnyEw==", "license": "Apache-2.0" }, "node_modules/emoji-regex": { @@ -13751,9 +13751,9 @@ "dev": true }, "eme-encryption-scheme-polyfill": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.1.6.tgz", - "integrity": "sha512-SmQ8UxDkH/8hrjLo6ASo452hIe4dSJzqKmJyrNsvUciEJNxf4z9hewIwF1k/c7A5uRk4GApavPZ6dgqXqfvegw==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.2.0.tgz", + "integrity": "sha512-wfgRcR2cGAX0WKbPahhI13dr2mzFQ/rMnoDibRIdScQlv9S0rtnXd25XF17IUdNJw/voJoFpYQp33C3xkFnyEw==" }, "emoji-regex": { "version": "8.0.0", diff --git a/package.json b/package.json index 930810e3cd..6df1349c84 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "prepublishOnly": "python build/checkversion.py && python build/all.py --force" }, "dependencies": { - "eme-encryption-scheme-polyfill": "^2.1.6" + "eme-encryption-scheme-polyfill": "^2.2.0" }, "engines": { "node": ">=18" diff --git a/project-words.txt b/project-words.txt index 995f941e7f..84fff1d614 100644 --- a/project-words.txt +++ b/project-words.txt @@ -194,6 +194,7 @@ Verimatrix Vidaa Vnova Widevine +wiseplay Zenterio # streaming diff --git a/test/dash/dash_parser_content_protection_unit.js b/test/dash/dash_parser_content_protection_unit.js index 2a3fa92759..96934ecc8a 100644 --- a/test/dash/dash_parser_content_protection_unit.js +++ b/test/dash/dash_parser_content_protection_unit.js @@ -496,6 +496,7 @@ describe('DashParser ContentProtection', () => { buildDrmInfo('com.microsoft.playready', keyIds), buildDrmInfo('com.microsoft.playready', keyIds), buildDrmInfo('com.apple.fps', keyIds), + buildDrmInfo('com.huawei.wiseplay', keyIds), ], variantKeyIds); await testDashParser(source, expected, /* ignoreDrmInfo= */ true); }); diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index 8b5791c3ba..531fbe7f53 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -3761,6 +3761,54 @@ describe('HlsParser', () => { expect(newDrmInfoSpy).toHaveBeenCalled(); }); + it('constructs DrmInfo for WisePlay', async () => { + const master = [ + '#EXTM3U\n', + '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1.4d401f",', + 'RESOLUTION=960x540,FRAME-RATE=60\n', + 'video\n', + ].join(''); + + const initDataBase64 = + 'dGhpcyBpbml0IGRhdGEgY29udGFpbnMgaGlkZGVuIHNlY3JldHMhISE='; + + const keyId = 'abc123'; + + const media = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:6\n', + '#EXT-X-PLAYLIST-TYPE:VOD\n', + '#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,', + 'KEYID=0X' + keyId + ',', + 'KEYFORMAT="urn:uuid:3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c",', + 'URI="data:text/plain;base64,', + initDataBase64, '",\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.huawei.wiseplay', (drmInfo) => { + drmInfo.addCencInitData(initDataBase64); + drmInfo.keyIds.add(keyId); + drmInfo.encryptionScheme = 'cenc'; + }); + }); + }); + manifest.sequenceMode = sequenceMode; + manifest.type = shaka.media.ManifestParser.HLS; + }); + + await testHlsParser(master, media, manifest); + expect(newDrmInfoSpy).toHaveBeenCalled(); + }); + it('constructs DrmInfo for PlayReady', async () => { const master = [ '#EXTM3U\n', @@ -4039,6 +4087,57 @@ describe('HlsParser', () => { expect(actual).toEqual(manifest); }); + it('for WisePlay', async () => { + const initDataBase64 = + 'dGhpcyBpbml0IGRhdGEgY29udGFpbnMgaGlkZGVuIHNlY3JldHMhISE='; + + const keyId = 'abc123'; + + const master = [ + '#EXTM3U\n', + '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1.4d401f",', + 'RESOLUTION=960x540,FRAME-RATE=30\n', + 'video\n', + '#EXT-X-STREAM-INF:BANDWIDTH=300,CODECS="avc1.4d401f",', + 'RESOLUTION=960x540,FRAME-RATE=60\n', + 'video2\n', + '#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES-CTR,', + 'KEYID=0X' + keyId + ',', + 'KEYFORMAT="urn:uuid:3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c",', + 'URI="data:text/plain;base64,', + initDataBase64, '",\n', + ].join(''); + + const manifest = shaka.test.ManifestGenerator.generate((manifest) => { + manifest.anyTimeline(); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.addDrmInfo('com.huawei.wiseplay', (drmInfo) => { + drmInfo.addCencInitData(initDataBase64); + drmInfo.keyIds.add(keyId); + drmInfo.encryptionScheme = 'cenc'; + }); + }); + }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.addDrmInfo('com.huawei.wiseplay', (drmInfo) => { + drmInfo.addCencInitData(initDataBase64); + drmInfo.keyIds.add(keyId); + drmInfo.encryptionScheme = 'cenc'; + }); + }); + }); + manifest.sequenceMode = sequenceMode; + manifest.type = shaka.media.ManifestParser.HLS; + }); + + fakeNetEngine.setResponseText('test:/master', master); + + const actual = await parser.start('test:/master', playerInterface); + expect(actual).toEqual(manifest); + }); + it('for PlayReady', async () => { const initDataBase64 = 'AAAAKXBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAAlQbGF5cmVhZHk=';