UNPKG

shaka-player

Version:
855 lines (759 loc) 27.5 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.media.ClearKeyWebCryptoDecryptor'); goog.require('goog.asserts'); goog.require('shaka.device.DeviceFactory'); goog.require('shaka.drm.DrmUtils'); goog.require('shaka.log'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.Mp4BoxParsers'); goog.require('shaka.util.Mp4Parser'); goog.require('shaka.util.Uint8ArrayUtils'); /** * @implements {shaka.util.IDestroyable} */ shaka.media.ClearKeyWebCryptoDecryptor = class { constructor() { /** @private {?shaka.extern.DrmInfo} */ this.drmInfo_ = null; /** @private {!Map<string, {cbc:!CryptoKey, ctr:!CryptoKey}>} */ this.keyMap_ = new Map(); /** @private {?Promise<!Map<string, {cbc:!CryptoKey, ctr:!CryptoKey}>>} */ this.keyMapPromise_ = null; /** @private {?Uint8Array} */ this.lastInit_ = null; } /** * @param {!BufferSource} data * @param {boolean} isInit * @param {!shaka.extern.DrmInfo} drmInfo * @return {!Promise<!Uint8Array>} */ async decrypt(data, isInit, drmInfo) { const uint8ArrayData = shaka.util.BufferUtils.toUint8(data); if (isInit) { this.lastInit_ = uint8ArrayData; return this.stripEncryptionFromInit_(uint8ArrayData); } if (!this.lastInit_) { return uint8ArrayData; } if (this.drmInfo_ !== drmInfo) { this.drmInfo_ = drmInfo; this.keyMapPromise_ = this.buildKeyMap_(this.drmInfo_); this.keyMap_ = await this.keyMapPromise_; this.keyMapPromise_ = null; } return this.decryptSegment_(uint8ArrayData, this.lastInit_); } /** * @override */ async destroy() { if (this.keyMapPromise_) { try { await this.keyMapPromise_; } catch (e) { // Ignore errors. } } this.drmInfo_ = null; this.keyMap_.clear(); this.keyMapPromise_ = null; this.lastInit_ = null; } /** * Extracts a Map<keyIdHex, {cbc, ctr}> from a ClearKey DrmInfo whose * licenseServerUri is a data:application/json;base64,<JWK-set> URI. * * @param {!shaka.extern.DrmInfo} drmInfo * @return {!Promise<!Map<string, {cbc:!CryptoKey, ctr:!CryptoKey}>>} * @private */ async buildKeyMap_(drmInfo) { /** @type {!Map<string, {cbc:!CryptoKey, ctr:!CryptoKey}>} */ const keyMap = new Map(); if (!drmInfo.clearKeys) { return keyMap; } const results = await Promise.all( [...drmInfo.clearKeys.entries()].map(([kid, key]) => { const keyBytes = shaka.util.Uint8ArrayUtils.fromBase64(key); const kidBytes = shaka.util.Uint8ArrayUtils.fromBase64(kid); return Promise.all([ crypto.subtle.importKey( 'raw', keyBytes, {name: 'AES-CBC'}, /* extractable= */ false, ['decrypt', 'encrypt']), crypto.subtle.importKey( 'raw', keyBytes, {name: 'AES-CTR'}, /* extractable= */ false, ['decrypt']), ]).then(([cbc, ctr]) => ({ kidHex: shaka.util.Uint8ArrayUtils.toHex(kidBytes), cbc, ctr, })); }), ); for (const {kidHex, cbc, ctr} of results) { keyMap.set(kidHex, {cbc, ctr}); } return keyMap; } /** * Full segment decryption pipeline. * * @param {!Uint8Array} segmentData * @param {!Uint8Array} initData * @return {!Promise<!Uint8Array>} * @private */ async decryptSegment_(segmentData, initData) { const trackInfos = this.parseInitSegment_(initData); const segInfo = this.parseMediaSegment_(segmentData, trackInfos); const fragmentPromises = segInfo.fragments.map((fragment) => { const initInfo = trackInfos.get(fragment.trackId) || trackInfos.values().next().value; return this.decryptFragment_(fragment, segmentData, initInfo); }); const decryptedFragments = await Promise.all(fragmentPromises); const outputChunks = [ segmentData.slice(0, segInfo.firstFragmentOffset), ...decryptedFragments, ]; return shaka.util.Uint8ArrayUtils.concat(...outputChunks); } /** * Rewrites the init segment to strip encryption signalling: * encv/enca -> original codec fourcc (from frma), sinf -> free. * MSE will then accept the plain decrypted samples without complaint. * * @param {!Uint8Array} initData * @return {!Uint8Array} * @private */ stripEncryptionFromInit_(initData) { const initSegment = shaka.util.BufferUtils.toUint8(initData).slice(); const view = shaka.util.BufferUtils.toDataView(initSegment); const modifications = []; let currentEncBoxStart = -1; const freeBox = (box) => { modifications.push(() => { view.setUint32(box.start + 4, shaka.media.ClearKeyWebCryptoDecryptor.BOX_TYPE_FREE_, /* littleEndian= */ false); initSegment.fill(0, box.start + 8, box.start + box.size); }); }; new shaka.util.Mp4Parser() .boxes(shaka.util.Mp4Parser.SAMPLE_TABLE_PATH, shaka.util.Mp4Parser.children) .fullBox('stsd', shaka.util.Mp4Parser.sampleDescription) .box('encv', (box) => { currentEncBoxStart = box.start; shaka.util.Mp4Parser.visualSampleEntry(box); }) .box('enca', (box) => { currentEncBoxStart = box.start; shaka.util.Mp4Parser.audioSampleEntry(box); }) .box('sinf', (box) => { freeBox(box); shaka.util.Mp4Parser.children(box); }) .box('frma', (box) => { const {codec} = shaka.util.Mp4BoxParsers.parseFRMA(box.reader); const targetEncStart = currentEncBoxStart; if (targetEncStart !== -1 && codec) { modifications.push(() => { for (let i = 0; i < 4; ++i) { view.setUint8(targetEncStart + 4 + i, codec.charCodeAt(i)); } }); } }) .fullBox('sgpd', freeBox) .box('pssh', freeBox) .parse(initSegment, /* partialOkay= */ true); for (const mod of modifications) { mod(); } return initSegment; } /** * @param {!Uint8Array} initData * @return {!Map<number, !shaka.media.ClearKeyWebCryptoDecryptor.InitInfo>} * @private */ parseInitSegment_(initData) { const trackInfos = new Map(); let currentTrackId = 0; const Mp4Parser = shaka.util.Mp4Parser; const Mp4BoxParsers = shaka.util.Mp4BoxParsers; new Mp4Parser() .boxes([ 'moov', 'mdia', 'minf', 'stbl', ], Mp4Parser.children) .box('trak', (box) => { currentTrackId = 0; Mp4Parser.children(box); }) .fullBox('tkhd', (box) => { goog.asserts.assert( box.version != null, 'TKHD is a full box and should have a valid version.'); const parsed = Mp4BoxParsers.parseTKHD(box.reader, box.version); currentTrackId = parsed.trackId; trackInfos.getOrInsert(currentTrackId, { defaultKID: '', encryptionScheme: 'cenc', defaultIVSize: 8, defaultConstantIV: null, defaultCryptByteBlock: 0, defaultSkipByteBlock: 0, }); }) .fullBox('stsd', Mp4Parser.sampleDescription) .box('encv', Mp4Parser.visualSampleEntry) .box('enca', Mp4Parser.audioSampleEntry) .box('sinf', Mp4Parser.children) .fullBox('schm', (box) => { const parsed = Mp4BoxParsers.parseSCHM(box.reader); const info = trackInfos.get(currentTrackId); if (info) { info.encryptionScheme = parsed.encryptionScheme.toLowerCase(); } }) .box('schi', Mp4Parser.children) .fullBox('tenc', (box) => { goog.asserts.assert( box.version != null, 'TENC is a full box and should have a valid version.'); const parsed = Mp4BoxParsers.parseTENC(box.reader, box.version); const info = trackInfos.get(currentTrackId); if (info) { info.defaultKID = parsed.defaultKID; info.defaultIVSize = parsed.defaultPerSampleIVSize; info.defaultConstantIV = parsed.defaultConstantIV; info.defaultCryptByteBlock = parsed.defaultCryptByteBlock; info.defaultSkipByteBlock = parsed.defaultSkipByteBlock; } }) .parse(initData, /* partialOkay= */ true); return trackInfos; } /** * @param {!Uint8Array} segData * @param {!Map<number, * !shaka.media.ClearKeyWebCryptoDecryptor.InitInfo>} trackInfos * @return {!shaka.media.ClearKeyWebCryptoDecryptor.SegmentParseResult} * @private */ parseMediaSegment_(segData, trackInfos) { const Mp4Parser = shaka.util.Mp4Parser; const Mp4BoxParsers = shaka.util.Mp4BoxParsers; const fragments = []; let firstFragmentOffset = 0; /** @type {?shaka.media.ClearKeyWebCryptoDecryptor.FragmentInfo} */ let currentFragment = null; const markFree = (box) => { if (currentFragment) { currentFragment.boxesToFree.push({start: box.start, size: box.size}); } }; new Mp4Parser() .box('moof', (box) => { if (!fragments.length) { firstFragmentOffset = box.start; } currentFragment = { moofStart: box.start, moofSize: box.size, mdatStart: -1, mdatSize: -1, sencInfo: null, tfhdDefaultSize: 0, trackId: 0, trunSamples: [], boxesToFree: [], }; Mp4Parser.children(box); }) .box('traf', Mp4Parser.children) .fullBox('tfhd', (box) => { if (!currentFragment) { return; } goog.asserts.assert( box.flags != null, 'TFHD is a full box and should have valid flags.'); const parsed = Mp4BoxParsers.parseTFHD(box.reader, box.flags); currentFragment.trackId = parsed.trackId; currentFragment.tfhdDefaultSize = parsed.defaultSampleSize || 0; }) .fullBox('trun', (box) => { if (!currentFragment) { return; } goog.asserts.assert( box.version != null && box.flags != null, 'TRUN is a full box and should have a valid version & flags.'); const parsed = Mp4BoxParsers.parseTRUN( box.reader, box.version, box.flags); for (const sample of parsed.sampleData) { currentFragment.trunSamples.push({ size: sample.sampleSize || currentFragment.tfhdDefaultSize, }); } }) .fullBox('senc', (box) => { if (!currentFragment) { return; } goog.asserts.assert( box.flags != null, 'SENC is a full box and should have valid flags.'); const info = trackInfos.get(currentFragment.trackId); if (info) { currentFragment.sencInfo = shaka.util.Mp4BoxParsers.parseSENC( box.reader, box.flags, info.defaultIVSize, info.defaultConstantIV); } markFree(box); }) .fullBoxes([ 'saiz', 'saio', 'sgpd', 'sbgp', ], markFree) .box('pssh', markFree) .box('mdat', (mdatBox) => { if (currentFragment) { currentFragment.mdatStart = mdatBox.start; currentFragment.mdatSize = mdatBox.size; fragments.push(currentFragment); currentFragment = null; } }) .parse(segData); return {fragments, firstFragmentOffset}; } /** * @param {!shaka.media.ClearKeyWebCryptoDecryptor.FragmentInfo} fragment * @param {!Uint8Array} segData * @param {!shaka.media.ClearKeyWebCryptoDecryptor.InitInfo} initInfo * @return {!Promise<!Uint8Array>} * @private */ async decryptFragment_(fragment, segData, initInfo) { const keyId = initInfo.defaultKID; const keyEntry = this.keyMap_.get(keyId); if (!keyEntry) { shaka.log.warning('[ClearKeyDecryptor] No key found for KID:', keyId); return segData.slice( fragment.moofStart, fragment.mdatStart + fragment.mdatSize); } const scheme = initInfo.encryptionScheme; let sencInfo = fragment.sencInfo; if (sencInfo && initInfo.defaultIVSize !== 8) { const moofSlice = segData.subarray( fragment.moofStart, fragment.moofStart + fragment.moofSize); const Mp4Parser = shaka.util.Mp4Parser; new Mp4Parser() .boxes(Mp4Parser.FRAGMENT_PATH, Mp4Parser.children) .fullBox('senc', (box) => { goog.asserts.assert( box.flags != null, 'SENC is a full box and should have valid flags.'); sencInfo = shaka.util.Mp4BoxParsers.parseSENC( box.reader, box.flags, initInfo.defaultIVSize, initInfo.defaultConstantIV); }) .parse(moofSlice, /* partialOkay= */ false); } const mdatPayloadStart = fragment.mdatStart + 8; const mdatPayload = segData.subarray( mdatPayloadStart, mdatPayloadStart + fragment.mdatSize - 8); const decryptedMdat = await this.decryptMdat_( mdatPayload, fragment.trunSamples.map((s) => s.size), sencInfo, initInfo, keyEntry, scheme); const moof = segData.slice( fragment.moofStart, fragment.moofStart + fragment.moofSize); const moofView = shaka.util.BufferUtils.toDataView(moof); if (fragment.boxesToFree) { for (const boxToFree of fragment.boxesToFree) { const relStart = boxToFree.start - fragment.moofStart; if (relStart >= 0 && (relStart + boxToFree.size) <= moof.byteLength) { moofView.setUint32(relStart + 4, shaka.media.ClearKeyWebCryptoDecryptor.BOX_TYPE_FREE_, /* littleEndian= */ false); // Zero payload moof.fill(0, relStart + 8, relStart + boxToFree.size); } } } const newMdatSize = 8 + decryptedMdat.byteLength; const newMdat = new Uint8Array(newMdatSize); shaka.util.BufferUtils.toDataView(newMdat).setUint32( 0, newMdatSize, /* LE= */ false); newMdat.set([0x6d, 0x64, 0x61, 0x74], 4); // 'mdat' newMdat.set(decryptedMdat, 8); return shaka.util.Uint8ArrayUtils.concat(moof, newMdat); } /** * Decrypt the raw mdat payload sample by sample. * * @param {!Uint8Array} mdatPayload * @param {!Array<number>} sampleSizes * @param {?shaka.media.ClearKeyWebCryptoDecryptor.SencInfo} senc * @param {!shaka.media.ClearKeyWebCryptoDecryptor.InitInfo} initInfo * @param {{cbc:!CryptoKey, ctr:!CryptoKey}} keyEntry * @param {string} scheme 'cenc' | 'cbcs' * @return {!Promise<!Uint8Array>} * @private */ async decryptMdat_( mdatPayload, sampleSizes, senc, initInfo, keyEntry, scheme) { const out = new Uint8Array(mdatPayload.byteLength); // Build per-sample decrypt promises; samples are independent of each // other so we can run them all in parallel with Promise.all. let sampleOffset = 0; const samplePromises = sampleSizes.map((sampleSize, i) => { const offset = sampleOffset; sampleOffset += sampleSize; const sampleData = mdatPayload.subarray(offset, offset + sampleSize); if (senc && senc.samples[i]) { const sencSample = senc.samples[i]; // Zero-pad 8-byte IVs into the high bytes of a 16-byte block. const ivLen = initInfo.defaultIVSize === 16 ? 16 : 8; const iv = new Uint8Array(16); iv.set(sencSample.iv.slice(0, ivLen), 0); if (scheme === 'cenc') { return this.decryptSampleCenc_( sampleData, iv, sencSample.subsamples, keyEntry) .then((dec) => ({offset, dec})); } // cbcs: use constant IV if signalled in tenc, else per-sample IV. const cbcsIV = initInfo.defaultConstantIV || iv; return this.decryptSampleCbcs_( sampleData, cbcsIV, sencSample.subsamples, initInfo, keyEntry) .then((dec) => ({offset, dec})); } if (scheme === 'cbcs' && initInfo.defaultConstantIV) { // No per-sample senc entry — whole sample uses constant IV. return this.decryptSampleCbcs_( sampleData, initInfo.defaultConstantIV, null, initInfo, keyEntry) .then((dec) => ({offset, dec})); } // Clear sample — pass through unchanged. return Promise.resolve({offset, dec: sampleData}); }); const results = await Promise.all(samplePromises); for (const {offset, dec} of results) { out.set(dec, offset); } return out; } /** * Decrypt one sample under CENC (AES-128-CTR, full or subsample). * * The IV is the initial 128-bit counter block (big-endian). For * subsample encryption the counter is NOT reset between subsample * regions — it advances by the number of whole 16-byte blocks * consumed in prior regions. * * @param {!Uint8Array} sampleData * @param {!Uint8Array} iv 16 bytes * @param {?Array<{clearBytes: number, encryptedBytes: number}>} subsamples * @param {{cbc:!CryptoKey, ctr:!CryptoKey}} keyEntry * @return {!Promise<!Uint8Array>} * @private */ async decryptSampleCenc_(sampleData, iv, subsamples, keyEntry) { if (!subsamples || !subsamples.length) { return shaka.util.BufferUtils.toUint8(await crypto.subtle.decrypt( {name: 'AES-CTR', counter: iv, length: 64}, keyEntry.ctr, sampleData)); } // Pre-compute per-subsample counters (counter state is cumulative), // then decrypt all encrypted ranges in parallel. const out = new Uint8Array(sampleData.byteLength); let pos = 0; let totalEncryptedBlocks = 0; const decryptJobs = subsamples.map((sub) => { // Copy clear bytes synchronously; record their range. const clearStart = pos; pos += sub.clearBytes; if (sub.encryptedBytes === 0) { return Promise.resolve({ clearStart, clearLen: sub.clearBytes, encStart: pos, encLen: 0, decrypted: null, }); } // Snapshot counter for this subsample before advancing. const counter = iv.slice(); this.addCounterOffset_(counter, totalEncryptedBlocks); const encStart = pos; pos += sub.encryptedBytes; totalEncryptedBlocks += Math.ceil(sub.encryptedBytes / 16); const encData = sampleData.subarray( encStart, encStart + sub.encryptedBytes); return crypto.subtle.decrypt( {name: 'AES-CTR', counter, length: 64}, keyEntry.ctr, encData) .then((buf) => ({ clearStart, clearLen: sub.clearBytes, encStart, encLen: sub.encryptedBytes, decrypted: shaka.util.BufferUtils.toUint8(buf), })); }); const results = await Promise.all(decryptJobs); for (const r of results) { out.set( sampleData.subarray(r.clearStart, r.clearStart + r.clearLen), r.clearStart); if (r.decrypted) { out.set(r.decrypted, r.encStart); } } return out; } /** * Decrypt one sample under CBCS (AES-128-CBC pattern encryption). * * Within each encrypted range, blocks alternate between encrypted * (cryptByteBlock x 16 bytes) and clear (skipByteBlock x 16 bytes). * Partial trailing blocks are always clear. * * @param {!Uint8Array} sampleData * @param {!Uint8Array} iv 16 bytes * @param {?Array<{clearBytes: number, encryptedBytes: number}>} subsamples * @param {!shaka.media.ClearKeyWebCryptoDecryptor.InitInfo} initInfo * @param {{cbc:!CryptoKey, ctr:!CryptoKey}} keyEntry * @return {!Promise<!Uint8Array>} * @private */ async decryptSampleCbcs_( sampleData, iv, subsamples, initInfo, keyEntry) { let cryptBlocks = initInfo.defaultCryptByteBlock; let skipBlocks = initInfo.defaultSkipByteBlock; // In cbcs, a 0:0 pattern means 100% encrypted, equivalent to 1:0. if (cryptBlocks === 0) { cryptBlocks = 1; skipBlocks = 0; } const out = new Uint8Array(sampleData.byteLength); const jobs = []; // State variable to maintain CBC IV chaining across the entire sample. let currentIv = iv; const processRange = (rangeStart, rangeLen) => { let offset = rangeStart; const end = rangeStart + rangeLen; while (offset < end) { const remaining = end - offset; const encLen = Math.min(cryptBlocks * 16, remaining); if (encLen >= 16) { const alignedLen = Math.floor(encLen / 16) * 16; const encStart = offset; // Partial trailing block of the crypt group is always clear. const partialLen = encLen - alignedLen; const clearAfterStart = offset + alignedLen; // Capture the correct IV for this specific asynchronous block. const chunkIv = currentIv; jobs.push(this.rawCBCDecrypt_( sampleData.subarray(encStart, encStart + alignedLen), chunkIv, keyEntry.cbc) .then((dec) => { out.set(dec, encStart); if (partialLen > 0) { out.set( sampleData.subarray( clearAfterStart, clearAfterStart + partialLen), clearAfterStart); } }), ); // Update the IV for the next block: // CBC chaining requires the last 16 bytes of the current ciphertext. currentIv = sampleData.slice( encStart + alignedLen - 16, encStart + alignedLen); offset += alignedLen + partialLen; } else { // Less than one full block remaining in crypt group — clear. out.set(sampleData.subarray(offset, offset + encLen), offset); offset += encLen; } // Skip group — always copied clear. const skipLen = Math.min(skipBlocks * 16, end - offset); if (skipLen > 0) { out.set(sampleData.subarray(offset, offset + skipLen), offset); offset += skipLen; } } }; // Process subsamples synchronously to ensure correct IV chaining order // before resolving promises. if (!subsamples || !subsamples.length) { processRange(0, sampleData.byteLength); } else { let pos = 0; for (const sub of subsamples) { const clearStart = pos; pos += sub.clearBytes; out.set( sampleData.subarray(clearStart, clearStart + sub.clearBytes), clearStart); if (sub.encryptedBytes > 0) { const encStart = pos; pos += sub.encryptedBytes; processRange(encStart, sub.encryptedBytes); } } } // Await all decryption jobs concurrently. await Promise.all(jobs); return out; } /** * AES-128-CBC decryption without PKCS7 unpadding. * * WebCrypto AES-CBC always applies PKCS7. CBCS stream data is NOT * padded — partial final blocks are clear rather than padded. We work * around this by appending a synthetic full PKCS7 padding block * (16 x 0x10) so WebCrypto's automatic unpadding removes only that * dummy block, leaving our real data (always 16-byte aligned here) * intact. * * @param {!Uint8Array} data Must be 16-byte aligned * @param {!Uint8Array} iv 16 bytes * @param {!CryptoKey} cbcKey AES-CBC key * @return {!Promise<!Uint8Array>} Exactly data.byteLength bytes * @private */ async rawCBCDecrypt_(data, iv, cbcKey) { if (!data || !data.byteLength || data.byteLength % 16 !== 0) { return new Uint8Array(0); } const numBlocks = data.byteLength / 16; const lastCiphertextBlock = data.subarray((numBlocks - 1) * 16, numBlocks * 16); // Mathematical requirement to force a PKCS#7 padding block const paddingBlock = lastCiphertextBlock.map((b) => 0x10 ^ b); // Artificial padding encryption using zero IV (AES-ECB simulation) const zeroIv = new Uint8Array(16); const encryptedPadding = await crypto.subtle.encrypt( {name: 'AES-CBC', iv: zeroIv}, cbcKey, paddingBlock, ); // Take only the first 16 bytes const extraCiphertextBlock = new Uint8Array(encryptedPadding, 0, 16); // Concatenate synthetic block at the end const extendedCiphertext = new Uint8Array(data.byteLength + 16); extendedCiphertext.set(data, 0); extendedCiphertext.set(extraCiphertextBlock, data.byteLength); // WebCrypto will strip the extra block cleanly const decrypted = await crypto.subtle.decrypt( {name: 'AES-CBC', iv: iv}, cbcKey, extendedCiphertext, ); return shaka.util.BufferUtils.toUint8(decrypted); } /** * Add a block-count offset to a 16-byte big-endian AES-CTR counter. * Only the lower 8 bytes (bytes 8-15) act as the incrementing counter * per the CENC spec (section 9.1); the upper 8 bytes are the IV nonce. * * @param {!Uint8Array} counter modified in place, 16 bytes * @param {number} offset number of 16-byte blocks to add * @private */ addCounterOffset_(counter, offset) { let carry = offset; for (let i = 15; i >= 8 && carry > 0; i--) { carry += counter[i]; counter[i] = carry & 0xff; carry >>>= 8; } } /** * Returns true if the ClearKey WebCrypto path should be used. * * @param {?shaka.extern.DrmInfo} drmInfo * @return {boolean} */ static shouldUse(drmInfo) { if (!drmInfo) { return false; } if (!window.crypto?.subtle) { return false; } if (!shaka.drm.DrmUtils.isClearKeySystem(drmInfo.keySystem)) { return false; } return !shaka.device.DeviceFactory.getDevice().hasWorkingClearKeySupport(); } }; /** * Box type for "free". * * @const {number} * @private */ shaka.media.ClearKeyWebCryptoDecryptor.BOX_TYPE_FREE_ = 0x66726565; /** * @typedef {{ * defaultKID: string, * encryptionScheme: string, * defaultIVSize: number, * defaultConstantIV: ?Uint8Array, * defaultCryptByteBlock: number, * defaultSkipByteBlock: number, * }} */ shaka.media.ClearKeyWebCryptoDecryptor.InitInfo; /** * @typedef {{ * moofStart: number, * moofSize: number, * mdatStart: number, * mdatSize: number, * sencInfo: ?shaka.media.ClearKeyWebCryptoDecryptor.SencInfo, * tfhdDefaultSize: number, * trackId: number, * trunSamples: !Array<{size: number}>, * boxesToFree: !Array<{start: number, size: number}>, * }} */ shaka.media.ClearKeyWebCryptoDecryptor.FragmentInfo; /** * @typedef {{ * samples: !Array<{ * iv: !Uint8Array, * subsamples: * ?Array<{clearBytes: number, encryptedBytes: number}> * }> * }} */ shaka.media.ClearKeyWebCryptoDecryptor.SencInfo; /** * @typedef {{ * fragments: * !Array<!shaka.media.ClearKeyWebCryptoDecryptor.FragmentInfo>, * firstFragmentOffset: number, * }} */ shaka.media.ClearKeyWebCryptoDecryptor.SegmentParseResult;