mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
Add pssh box parsing and expose them in resolveKey
This commit is contained in:
@@ -352,7 +352,10 @@ using input = new Input({
|
|||||||
formats: ALL_FORMATS,
|
formats: ALL_FORMATS,
|
||||||
formatOptions: {
|
formatOptions: {
|
||||||
isobmff: {
|
isobmff: {
|
||||||
resolveKeyId: ({ keyId }) => {
|
resolveKeyId: ({ keyId, psshBoxes }) => {
|
||||||
|
// psshBoxes contains Protection System Specific Header boxes
|
||||||
|
// relevant to this key ID. They can be used to obtain a
|
||||||
|
// decryption key from a DRM license server.
|
||||||
const key = keyMap.get(keyId);
|
const key = keyMap.get(keyId);
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw new Error('Unknown key ID.');
|
throw new Error('Unknown key ID.');
|
||||||
@@ -367,4 +370,4 @@ using input = new Input({
|
|||||||
|
|
||||||
## Subtitles
|
## Subtitles
|
||||||
|
|
||||||
Reading subtitles from HLS playlists is not currently supported. Sorry!
|
Reading subtitles from HLS playlists is not currently supported. Sorry!
|
||||||
|
|||||||
@@ -9,7 +9,16 @@
|
|||||||
import { AES_128_BLOCK_SIZE, createAes128CbcDecryptStream } from '../aes';
|
import { AES_128_BLOCK_SIZE, createAes128CbcDecryptStream } from '../aes';
|
||||||
import { ENCRYPTION_KEY_CACHE_GROUP, Input } from '../input';
|
import { ENCRYPTION_KEY_CACHE_GROUP, Input } from '../input';
|
||||||
import { Segment, SegmentedInput, SegmentedInputTrackDeclaration, SegmentRetrievalOptions } from '../segmented-input';
|
import { Segment, SegmentedInput, SegmentedInputTrackDeclaration, SegmentRetrievalOptions } from '../segmented-input';
|
||||||
import { toDataView, joinPaths, last, assert, binarySearchLessOrEqual, arrayArgmin, wait } from '../misc';
|
import {
|
||||||
|
toDataView,
|
||||||
|
joinPaths,
|
||||||
|
last,
|
||||||
|
assert,
|
||||||
|
binarySearchLessOrEqual,
|
||||||
|
arrayArgmin,
|
||||||
|
wait,
|
||||||
|
base64ToBytes,
|
||||||
|
} from '../misc';
|
||||||
import { readAllLines, readBytes, Reader } from '../reader';
|
import { readAllLines, readBytes, Reader } from '../reader';
|
||||||
import { CustomPathedSource, ReadableStreamSource, SourceRef, SourceRequest } from '../source';
|
import { CustomPathedSource, ReadableStreamSource, SourceRef, SourceRequest } from '../source';
|
||||||
import { HlsDemuxer } from './hls-demuxer';
|
import { HlsDemuxer } from './hls-demuxer';
|
||||||
@@ -27,9 +36,11 @@ import {
|
|||||||
TAG_PROGRAM_DATE_TIME,
|
TAG_PROGRAM_DATE_TIME,
|
||||||
TAG_TARGETDURATION,
|
TAG_TARGETDURATION,
|
||||||
} from './hls-misc';
|
} from './hls-misc';
|
||||||
import { HlsInputFormat } from '../input-format';
|
import { HlsInputFormat, type InputFormatOptions } from '../input-format';
|
||||||
|
import { parsePsshBoxContents, psshBoxesAreEqual, type PsshBox } from '../isobmff/isobmff-misc';
|
||||||
|
|
||||||
const IV_STRING_REGEX = /^0[xX][0-9a-fA-F]+$/;
|
const IV_STRING_REGEX = /^0[xX][0-9a-fA-F]+$/;
|
||||||
|
const BASE64_DATA_URI_REGEX = /^data:.*;base64,/i;
|
||||||
|
|
||||||
export type HlsSegment = Segment & {
|
export type HlsSegment = Segment & {
|
||||||
sequenceNumber: number | null;
|
sequenceNumber: number | null;
|
||||||
@@ -47,6 +58,7 @@ export type HlsEncryptionInfo = {
|
|||||||
keyFormat: string;
|
keyFormat: string;
|
||||||
} | {
|
} | {
|
||||||
method: 'SAMPLE-AES' | 'SAMPLE-AES-CTR';
|
method: 'SAMPLE-AES' | 'SAMPLE-AES-CTR';
|
||||||
|
psshBox: PsshBox | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HlsSegmentLocation = {
|
export type HlsSegmentLocation = {
|
||||||
@@ -368,6 +380,11 @@ export class HlsSegmentedInput extends SegmentedInput {
|
|||||||
keyFormat,
|
keyFormat,
|
||||||
};
|
};
|
||||||
} else if (method === 'SAMPLE-AES' || method === 'SAMPLE-AES-CTR') {
|
} else if (method === 'SAMPLE-AES' || method === 'SAMPLE-AES-CTR') {
|
||||||
|
const uri = attributes.get('uri');
|
||||||
|
if (!uri) {
|
||||||
|
throw new Error(`Invalid #EXT-X-KEY: ${method} requires a URI attribute.`);
|
||||||
|
}
|
||||||
|
|
||||||
const keyFormat = attributes.get('keyformat') ?? 'identity';
|
const keyFormat = attributes.get('keyformat') ?? 'identity';
|
||||||
if (keyFormat === 'identity') {
|
if (keyFormat === 'identity') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -376,8 +393,26 @@ export class HlsSegmentedInput extends SegmentedInput {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let psshBox: PsshBox | null = null;
|
||||||
|
if (BASE64_DATA_URI_REGEX.test(uri)) {
|
||||||
|
const commaIndex = uri.indexOf(',');
|
||||||
|
const bytes = base64ToBytes(uri.slice(commaIndex + 1));
|
||||||
|
|
||||||
|
if (
|
||||||
|
bytes.length >= 8
|
||||||
|
&& bytes[4] === 0x70
|
||||||
|
&& bytes[5] === 0x73
|
||||||
|
&& bytes[6] === 0x73
|
||||||
|
&& bytes[7] === 0x68
|
||||||
|
) {
|
||||||
|
const size = toDataView(bytes).getUint32(0);
|
||||||
|
psshBox = parsePsshBoxContents(bytes.subarray(8, Math.min(size, bytes.length)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentKey = {
|
currentKey = {
|
||||||
method,
|
method,
|
||||||
|
psshBox,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -562,6 +597,38 @@ export class HlsSegmentedInput extends SegmentedInput {
|
|||||||
initInput = this.getInputForSegment((hlsSegment.initSegment ?? hlsSegment.firstSegment)!);
|
initInput = this.getInputForSegment((hlsSegment.initSegment ?? hlsSegment.firstSegment)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatOptions: InputFormatOptions = {
|
||||||
|
...this.input._formatOptions,
|
||||||
|
isobmff: {
|
||||||
|
...this.input._formatOptions.isobmff,
|
||||||
|
// Intercept calls to resolveKeyId to inject our psshBox knowledge into it
|
||||||
|
resolveKeyId: this.input._formatOptions.isobmff?.resolveKeyId && ((options) => {
|
||||||
|
if (
|
||||||
|
!hlsSegment.encryption
|
||||||
|
|| !(
|
||||||
|
hlsSegment.encryption.method === 'SAMPLE-AES'
|
||||||
|
|| hlsSegment.encryption.method === 'SAMPLE-AES-CTR'
|
||||||
|
)
|
||||||
|
|| !hlsSegment.encryption.psshBox
|
||||||
|
) {
|
||||||
|
return this.input._formatOptions.isobmff!.resolveKeyId!(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
let psshBoxes = options.psshBoxes;
|
||||||
|
const { psshBox } = hlsSegment.encryption;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(psshBox.keyIds === null || psshBox.keyIds.includes(options.keyId))
|
||||||
|
&& !psshBoxes.some(x => psshBoxesAreEqual(x, psshBox))
|
||||||
|
) {
|
||||||
|
psshBoxes = [...psshBoxes, psshBox];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.input._formatOptions.isobmff!.resolveKeyId!({ ...options, psshBoxes });
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const input = new Input({
|
const input = new Input({
|
||||||
source: new CustomPathedSource(
|
source: new CustomPathedSource(
|
||||||
hlsSegment.location.path,
|
hlsSegment.location.path,
|
||||||
@@ -638,7 +705,7 @@ export class HlsSegmentedInput extends SegmentedInput {
|
|||||||
// Do not allow recursive HLS. Cool on paper, but allows for nasty infinite-depth request trees.
|
// Do not allow recursive HLS. Cool on paper, but allows for nasty infinite-depth request trees.
|
||||||
formats: this.input._formats.filter(x => !(x instanceof HlsInputFormat)),
|
formats: this.input._formats.filter(x => !(x instanceof HlsInputFormat)),
|
||||||
initInput: initInput ?? undefined,
|
initInput: initInput ?? undefined,
|
||||||
formatOptions: this.input._formatOptions,
|
formatOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
input._onFormatDetermined = (format) => {
|
input._onFormatDetermined = (format) => {
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ export {
|
|||||||
EventListenerOptions,
|
EventListenerOptions,
|
||||||
FilePath,
|
FilePath,
|
||||||
MaybePromise,
|
MaybePromise,
|
||||||
|
PsshBox,
|
||||||
Rational,
|
Rational,
|
||||||
Rectangle,
|
Rectangle,
|
||||||
Rotation,
|
Rotation,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { Demuxer } from './demuxer';
|
import { Demuxer } from './demuxer';
|
||||||
import { Input } from './input';
|
import { Input } from './input';
|
||||||
import { IsobmffDemuxer } from './isobmff/isobmff-demuxer';
|
import { IsobmffDemuxer } from './isobmff/isobmff-demuxer';
|
||||||
|
import type { PsshBox } from './isobmff/isobmff-misc';
|
||||||
import {
|
import {
|
||||||
EBMLId,
|
EBMLId,
|
||||||
MAX_HEADER_SIZE,
|
MAX_HEADER_SIZE,
|
||||||
@@ -738,7 +739,15 @@ export type IsobmffInputFormatOptions = {
|
|||||||
resolveKeyId?: (options: {
|
resolveKeyId?: (options: {
|
||||||
/** The key ID that is to be resolved to a key. This is a 32-character lowercase hexadecimal string. */
|
/** The key ID that is to be resolved to a key. This is a 32-character lowercase hexadecimal string. */
|
||||||
keyId: string;
|
keyId: string;
|
||||||
|
/**
|
||||||
|
* Protection System Specific Header (pssh) boxes that apply to this key ID. Can be used to obtain a
|
||||||
|
* description key from a DRM license server.
|
||||||
|
*/
|
||||||
|
psshBoxes: PsshBox[];
|
||||||
}) => MaybePromise<Uint8Array | string>;
|
}) => MaybePromise<Uint8Array | string>;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_suppressPsshParsing?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateInputFormatOptions = (options: InputFormatOptions, prefix: string) => {
|
export const validateInputFormatOptions = (options: InputFormatOptions, prefix: string) => {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ import {
|
|||||||
HEX_STRING_REGEX,
|
HEX_STRING_REGEX,
|
||||||
} from '../misc';
|
} from '../misc';
|
||||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||||
import { buildIsobmffMimeType } from './isobmff-misc';
|
import { buildIsobmffMimeType, parsePsshBoxContents, psshBoxesAreEqual, PsshBox } from './isobmff-misc';
|
||||||
import {
|
import {
|
||||||
MAX_BOX_HEADER_SIZE,
|
MAX_BOX_HEADER_SIZE,
|
||||||
MIN_BOX_HEADER_SIZE,
|
MIN_BOX_HEADER_SIZE,
|
||||||
@@ -254,6 +254,7 @@ type Fragment = {
|
|||||||
moofSize: number;
|
moofSize: number;
|
||||||
implicitBaseDataOffset: number;
|
implicitBaseDataOffset: number;
|
||||||
trackData: Map<InternalTrack['id'], FragmentTrackData>;
|
trackData: Map<InternalTrack['id'], FragmentTrackData>;
|
||||||
|
psshBoxes: PsshBox[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type TrackEncryptionInfo = {
|
type TrackEncryptionInfo = {
|
||||||
@@ -301,6 +302,7 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
|
|
||||||
isFragmented = false;
|
isFragmented = false;
|
||||||
fragmentTrackDefaults: FragmentTrackDefaults[] = [];
|
fragmentTrackDefaults: FragmentTrackDefaults[] = [];
|
||||||
|
psshBoxes: PsshBox[] = [];
|
||||||
currentFragment: Fragment | null = null;
|
currentFragment: Fragment | null = null;
|
||||||
/**
|
/**
|
||||||
* Caches the last fragment that was read. Based on the assumption that there will be multiple reads to the
|
* Caches the last fragment that was read. Based on the assumption that there will be multiple reads to the
|
||||||
@@ -405,6 +407,7 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
this.metadataTags = initDemuxer.metadataTags;
|
this.metadataTags = initDemuxer.metadataTags;
|
||||||
this.isFragmented = true;
|
this.isFragmented = true;
|
||||||
this.fragmentTrackDefaults = initDemuxer.fragmentTrackDefaults;
|
this.fragmentTrackDefaults = initDemuxer.fragmentTrackDefaults;
|
||||||
|
this.psshBoxes = initDemuxer.psshBoxes;
|
||||||
|
|
||||||
// Create tracks from the init input's tracks
|
// Create tracks from the init input's tracks
|
||||||
for (const foreignTrack of initDemuxer.tracks) {
|
for (const foreignTrack of initDemuxer.tracks) {
|
||||||
@@ -2041,6 +2044,7 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
moofSize: boxInfo.totalSize,
|
moofSize: boxInfo.totalSize,
|
||||||
implicitBaseDataOffset: startPos,
|
implicitBaseDataOffset: startPos,
|
||||||
trackData: new Map(),
|
trackData: new Map(),
|
||||||
|
psshBoxes: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
|
this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
|
||||||
@@ -2108,6 +2112,20 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
}
|
}
|
||||||
}; break;
|
}; break;
|
||||||
|
|
||||||
|
case 'pssh': {
|
||||||
|
if (this.input._formatOptions.isobmff?._suppressPsshParsing) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const psshBox = parsePsshBoxContents(readBytes(slice, boxInfo.contentSize));
|
||||||
|
|
||||||
|
if (this.currentFragment) {
|
||||||
|
this.currentFragment.psshBoxes.push(psshBox);
|
||||||
|
} else if (!this.currentTrack) {
|
||||||
|
this.psshBoxes.push(psshBox);
|
||||||
|
}
|
||||||
|
}; break;
|
||||||
|
|
||||||
case 'tfhd': {
|
case 'tfhd': {
|
||||||
assert(this.currentFragment);
|
assert(this.currentFragment);
|
||||||
|
|
||||||
@@ -3063,7 +3081,7 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (sampleIndex < entries.length) {
|
if (sampleIndex < entries.length) {
|
||||||
data = await decryptSample(this.internalTrack, entries[sampleIndex]!, data);
|
data = await decryptSample(this.internalTrack, entries[sampleIndex]!, data, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3110,7 +3128,7 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
data = readBytes(slice, fragmentSample.byteSize);
|
data = readBytes(slice, fragmentSample.byteSize);
|
||||||
|
|
||||||
if (fragmentSample.encryption) {
|
if (fragmentSample.encryption) {
|
||||||
data = await decryptSample(this.internalTrack, fragmentSample.encryption, data);
|
data = await decryptSample(this.internalTrack, fragmentSample.encryption, data, fragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3666,6 +3684,7 @@ const decryptSample = async (
|
|||||||
track: InternalTrack,
|
track: InternalTrack,
|
||||||
sampleEncryption: SampleEncryptionInfo,
|
sampleEncryption: SampleEncryptionInfo,
|
||||||
data: Uint8Array,
|
data: Uint8Array,
|
||||||
|
fragment: Fragment | null,
|
||||||
): Promise<Uint8Array> => {
|
): Promise<Uint8Array> => {
|
||||||
assert(track.encryptionInfo);
|
assert(track.encryptionInfo);
|
||||||
const encryptionInfo = track.encryptionInfo;
|
const encryptionInfo = track.encryptionInfo;
|
||||||
@@ -3686,7 +3705,25 @@ const decryptSample = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const promise = (async () => {
|
const promise = (async () => {
|
||||||
const keyResult = await track.demuxer.input._formatOptions.isobmff!.resolveKeyId!({ keyId });
|
let psshBoxes = track.demuxer.psshBoxes;
|
||||||
|
if (fragment) {
|
||||||
|
psshBoxes = [
|
||||||
|
...psshBoxes,
|
||||||
|
...fragment.psshBoxes,
|
||||||
|
].filter(x => x.keyIds === null || x.keyIds.includes(keyId));
|
||||||
|
|
||||||
|
// Filter out duplicates
|
||||||
|
for (let i = 0; i < psshBoxes.length - 1; i++) {
|
||||||
|
for (let j = i + 1; j < psshBoxes.length; j++) {
|
||||||
|
if (psshBoxesAreEqual(psshBoxes[i]!, psshBoxes[j]!)) {
|
||||||
|
psshBoxes.splice(j, 1);
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyResult = await track.demuxer.input._formatOptions.isobmff!.resolveKeyId!({ keyId, psshBoxes });
|
||||||
|
|
||||||
if (!(
|
if (!(
|
||||||
(typeof keyResult === 'string' && keyResult.length === 32 && HEX_STRING_REGEX.test(keyResult))
|
(typeof keyResult === 'string' && keyResult.length === 32 && HEX_STRING_REGEX.test(keyResult))
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { bytesToHexString, toDataView, uint8ArraysAreEqual } from '../misc';
|
||||||
|
|
||||||
export const buildIsobmffMimeType = (info: {
|
export const buildIsobmffMimeType = (info: {
|
||||||
isQuickTime: boolean;
|
isQuickTime: boolean;
|
||||||
hasVideo: boolean;
|
hasVideo: boolean;
|
||||||
@@ -27,3 +29,62 @@ export const buildIsobmffMimeType = (info: {
|
|||||||
|
|
||||||
return string;
|
return string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Protection System Specific Header box as used by ISOBMFF Common Encryption. Contains
|
||||||
|
* DRM system-specific data that can be used to obtain a decryption key.
|
||||||
|
*
|
||||||
|
* @group Miscellaneous
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type PsshBox = {
|
||||||
|
/** The system ID as a 32-bit lowercase hex string. */
|
||||||
|
systemId: string;
|
||||||
|
/**
|
||||||
|
* The list of key IDs (32-bit lowercase hex strings) this box applies to, or `null` if it applies to all key IDs.
|
||||||
|
*/
|
||||||
|
keyIds: string[] | null;
|
||||||
|
/** The content protection system-specific data. */
|
||||||
|
data: Uint8Array;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parsePsshBoxContents = (contents: Uint8Array): PsshBox => {
|
||||||
|
const view = toDataView(contents);
|
||||||
|
let pos = 0;
|
||||||
|
|
||||||
|
const version = view.getUint8(pos);
|
||||||
|
pos += 1;
|
||||||
|
|
||||||
|
pos += 3; // Flags
|
||||||
|
|
||||||
|
const systemId = bytesToHexString(contents.subarray(pos, pos + 16));
|
||||||
|
pos += 16;
|
||||||
|
|
||||||
|
let keyIds: string[] | null = null;
|
||||||
|
if (version > 0) {
|
||||||
|
const kidCount = view.getUint32(pos);
|
||||||
|
pos += 4;
|
||||||
|
|
||||||
|
if (kidCount > 0) {
|
||||||
|
keyIds = [];
|
||||||
|
for (let i = 0; i < kidCount; i++) {
|
||||||
|
keyIds.push(bytesToHexString(contents.subarray(pos, pos + 16)));
|
||||||
|
pos += 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataSize = view.getUint32(pos);
|
||||||
|
pos += 4;
|
||||||
|
|
||||||
|
return {
|
||||||
|
systemId,
|
||||||
|
keyIds,
|
||||||
|
data: contents.slice(pos, pos + dataSize),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const psshBoxesAreEqual = (a: PsshBox, b: PsshBox) => (
|
||||||
|
a.systemId === b.systemId
|
||||||
|
&& uint8ArraysAreEqual(a.data, b.data)
|
||||||
|
);
|
||||||
|
|||||||
@@ -1309,3 +1309,5 @@ export class ConcurrentRunner {
|
|||||||
await Promise.all(this._queue);
|
await Promise.all(this._queue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { PsshBox } from './isobmff/isobmff-misc';
|
||||||
|
|||||||
@@ -897,7 +897,17 @@ test.concurrent('Widevine encryption (SAMPLE-AES-CTR) succeeds with string keys'
|
|||||||
formats: ALL_FORMATS,
|
formats: ALL_FORMATS,
|
||||||
formatOptions: {
|
formatOptions: {
|
||||||
isobmff: {
|
isobmff: {
|
||||||
resolveKeyId: ({ keyId }) => {
|
resolveKeyId: ({ keyId, psshBoxes }) => {
|
||||||
|
expect(psshBoxes).toHaveLength(1);
|
||||||
|
expect(psshBoxes[0]!.systemId).toBe('edef8ba979d64acea3c827dcd51d21ed');
|
||||||
|
expect(psshBoxes[0]!.keyIds).toBeNull();
|
||||||
|
expect(psshBoxes[0]!.data).toEqual(new Uint8Array([
|
||||||
|
34, 22, 115, 104, 97, 107, 97, 95,
|
||||||
|
99, 101, 99, 50, 102, 54, 52, 97,
|
||||||
|
97, 55, 56, 57, 48, 97, 49, 49,
|
||||||
|
72, 227, 220, 149, 155, 6,
|
||||||
|
]));
|
||||||
|
|
||||||
const key = keyMap.get(keyId);
|
const key = keyMap.get(keyId);
|
||||||
assert(key);
|
assert(key);
|
||||||
|
|
||||||
@@ -949,6 +959,50 @@ test.concurrent('Widevine encryption (SAMPLE-AES-CTR) succeeds with buffer keys'
|
|||||||
expect(lastPacket.timestamp + lastPacket.duration).toBe(60);
|
expect(lastPacket.timestamp + lastPacket.duration).toBe(60);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.concurrent('Widevine HLS passes #EXT-X-KEY PSSH boxes to key resolver', async () => {
|
||||||
|
const keyMap = new Map([
|
||||||
|
['4d97930a3d7b55fa81d0028653f5e499', '429ec76475e7a952d224d8ef867f12b6'],
|
||||||
|
['d21373c0b8ab5ba9954742bcdfb5f48b', '150a6c7d7dee6a91b74dccfce5b31928'],
|
||||||
|
['6f1729072b4a5cd288c916e11846b89e', 'a84b4bd66901874556093454c075e2c6'],
|
||||||
|
['800aacaa522958ae888062b5695db6bf', '775dbf7289c4cc5847becd571f536ff2'],
|
||||||
|
['67b30c86756f57c5a0a38a23ac8c9178', 'efa2878c2ccf6dd47ab349fcf90e6259'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
using input = new Input({
|
||||||
|
source: new UrlSource('https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine-hls/hls.m3u8'),
|
||||||
|
formats: ALL_FORMATS,
|
||||||
|
formatOptions: {
|
||||||
|
isobmff: {
|
||||||
|
_suppressPsshParsing: true,
|
||||||
|
resolveKeyId: ({ keyId, psshBoxes }) => {
|
||||||
|
expect(psshBoxes).toHaveLength(1);
|
||||||
|
expect(psshBoxes[0]!.systemId).toBe('edef8ba979d64acea3c827dcd51d21ed');
|
||||||
|
expect(psshBoxes[0]!.keyIds).toBeNull();
|
||||||
|
expect(psshBoxes[0]!.data).toEqual(new Uint8Array([
|
||||||
|
34, 22, 115, 104, 97, 107, 97, 95,
|
||||||
|
99, 101, 99, 50, 102, 54, 52, 97,
|
||||||
|
97, 55, 56, 57, 48, 97, 49, 49,
|
||||||
|
72, 227, 220, 149, 155, 6,
|
||||||
|
]));
|
||||||
|
|
||||||
|
const key = keyMap.get(keyId);
|
||||||
|
assert(key);
|
||||||
|
|
||||||
|
return key;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const videoTrack = await input.getPrimaryVideoTrack();
|
||||||
|
assert(videoTrack);
|
||||||
|
|
||||||
|
const sink = new EncodedPacketSink(videoTrack);
|
||||||
|
const lastPacket = await sink.getPacket(Infinity);
|
||||||
|
assert(lastPacket);
|
||||||
|
expect(lastPacket.timestamp + lastPacket.duration).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
test.concurrent('SourceRequest.isRoot', async () => {
|
test.concurrent('SourceRequest.isRoot', async () => {
|
||||||
using input = new Input({
|
using input = new Input({
|
||||||
source: new CustomPathedSource(
|
source: new CustomPathedSource(
|
||||||
|
|||||||
Reference in New Issue
Block a user