shifro 1.1.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +409 -8
- package/dist/index.d.mts +129 -10
- package/dist/index.mjs +408 -7
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,27 +1,428 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
|
-
import { ALL_FORMATS, Conversion, FilePathSource, FilePathTarget, Input, Mp4OutputFormat, Output } from "mediabunny";
|
|
3
|
+
import { ALL_FORMATS, Conversion, EncodedPacket, FilePathSource, FilePathTarget, Input, Mp4OutputFormat, Output } from "mediabunny";
|
|
4
4
|
//#region package.json
|
|
5
5
|
var name = "shifro";
|
|
6
|
-
var version = "1.
|
|
6
|
+
var version = "1.2.0";
|
|
7
7
|
var author = "Vitaly Gashkov <vitalygashkov@vk.com>";
|
|
8
8
|
//#endregion
|
|
9
|
+
//#region src/mediabunny-isobmff.ts
|
|
10
|
+
const binarySearchLessOrEqual = (items, needle, getValue) => {
|
|
11
|
+
let low = 0;
|
|
12
|
+
let high = items.length - 1;
|
|
13
|
+
let result = -1;
|
|
14
|
+
while (low <= high) {
|
|
15
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
16
|
+
if (getValue(items[middle]) <= needle) {
|
|
17
|
+
result = middle;
|
|
18
|
+
low = middle + 1;
|
|
19
|
+
} else high = middle - 1;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
const binarySearchExact = (items, needle, getValue) => {
|
|
24
|
+
let low = 0;
|
|
25
|
+
let high = items.length - 1;
|
|
26
|
+
while (low <= high) {
|
|
27
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
28
|
+
const value = getValue(items[middle]);
|
|
29
|
+
if (value === needle) return middle;
|
|
30
|
+
if (value < needle) low = middle + 1;
|
|
31
|
+
else high = middle - 1;
|
|
32
|
+
}
|
|
33
|
+
return -1;
|
|
34
|
+
};
|
|
35
|
+
const readBytes = (slice, length) => {
|
|
36
|
+
const bytes = slice.bytes.slice(slice.bufferPos, slice.bufferPos + length);
|
|
37
|
+
slice.bufferPos += length;
|
|
38
|
+
return bytes;
|
|
39
|
+
};
|
|
40
|
+
const readU16Be = (slice) => {
|
|
41
|
+
const value = slice.view.getUint16(slice.bufferPos, false);
|
|
42
|
+
slice.bufferPos += 2;
|
|
43
|
+
return value;
|
|
44
|
+
};
|
|
45
|
+
const readU32Be = (slice) => {
|
|
46
|
+
const value = slice.view.getUint32(slice.bufferPos, false);
|
|
47
|
+
slice.bufferPos += 4;
|
|
48
|
+
return value;
|
|
49
|
+
};
|
|
50
|
+
const readSampleBytes = async (internalTrack, byteOffset, byteLength) => {
|
|
51
|
+
let slice = internalTrack.demuxer.reader.requestSlice(byteOffset, byteLength);
|
|
52
|
+
if (slice instanceof Promise) slice = await slice;
|
|
53
|
+
if (!slice) throw new Error("Failed to read encrypted sample bytes.");
|
|
54
|
+
return readBytes(slice, byteLength);
|
|
55
|
+
};
|
|
56
|
+
const resolveEncryptionAuxInfo = async (internalTrack, auxInfo) => {
|
|
57
|
+
if (auxInfo.resolved) return auxInfo.resolved;
|
|
58
|
+
const encryptionInfo = internalTrack.encryptionInfo;
|
|
59
|
+
if (!encryptionInfo || encryptionInfo.defaultPerSampleIvSize === null) throw new Error("Missing track encryption info.");
|
|
60
|
+
if (auxInfo.offset === null || auxInfo.sampleCount === 0) throw new Error("Incomplete saiz/saio info; cannot resolve encryption data.");
|
|
61
|
+
if (auxInfo.defaultSampleInfoSize === 0 && !auxInfo.sampleSizes) throw new Error("Invalid auxiliary encryption info: auxInfo.sampleSizes is required when auxInfo.defaultSampleInfoSize is 0.");
|
|
62
|
+
let totalSize = 0;
|
|
63
|
+
if (auxInfo.defaultSampleInfoSize > 0) totalSize = auxInfo.defaultSampleInfoSize * auxInfo.sampleCount;
|
|
64
|
+
else if (auxInfo.sampleSizes) for (const size of auxInfo.sampleSizes) totalSize += size;
|
|
65
|
+
let slice = internalTrack.demuxer.reader.requestSlice(auxInfo.offset, totalSize);
|
|
66
|
+
if (slice instanceof Promise) slice = await slice;
|
|
67
|
+
if (!slice) throw new Error("Failed to read auxiliary encryption info.");
|
|
68
|
+
const entries = [];
|
|
69
|
+
const ivSize = encryptionInfo.defaultPerSampleIvSize;
|
|
70
|
+
for (let i = 0; i < auxInfo.sampleCount; i++) {
|
|
71
|
+
const entrySize = auxInfo.defaultSampleInfoSize > 0 ? auxInfo.defaultSampleInfoSize : auxInfo.sampleSizes?.[i] ?? 0;
|
|
72
|
+
const iv = new Uint8Array(16);
|
|
73
|
+
if (ivSize > 0) iv.set(readBytes(slice, ivSize), 0);
|
|
74
|
+
else if (encryptionInfo.defaultConstantIv) iv.set(encryptionInfo.defaultConstantIv, 0);
|
|
75
|
+
let subsamples = null;
|
|
76
|
+
if (entrySize > ivSize) {
|
|
77
|
+
const subsampleCount = readU16Be(slice);
|
|
78
|
+
subsamples = [];
|
|
79
|
+
for (let j = 0; j < subsampleCount; j++) subsamples.push({
|
|
80
|
+
clearLen: readU16Be(slice),
|
|
81
|
+
protectedLen: readU32Be(slice)
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
entries.push({
|
|
85
|
+
iv,
|
|
86
|
+
subsamples
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
auxInfo.resolved = entries;
|
|
90
|
+
return entries;
|
|
91
|
+
};
|
|
92
|
+
const getSampleInfo = (sampleTable, sampleIndex) => {
|
|
93
|
+
const timingEntryIndex = binarySearchLessOrEqual(sampleTable.sampleTimingEntries, sampleIndex, (entry) => entry.startIndex);
|
|
94
|
+
const timingEntry = sampleTable.sampleTimingEntries[timingEntryIndex];
|
|
95
|
+
if (!timingEntry || timingEntry.startIndex + timingEntry.count <= sampleIndex) return null;
|
|
96
|
+
let presentationTimestamp = timingEntry.startDecodeTimestamp + (sampleIndex - timingEntry.startIndex) * timingEntry.delta;
|
|
97
|
+
const offsetEntryIndex = binarySearchLessOrEqual(sampleTable.sampleCompositionTimeOffsets, sampleIndex, (entry) => entry.startIndex);
|
|
98
|
+
const offsetEntry = sampleTable.sampleCompositionTimeOffsets[offsetEntryIndex];
|
|
99
|
+
if (offsetEntry && sampleIndex - offsetEntry.startIndex < offsetEntry.count) presentationTimestamp += offsetEntry.offset;
|
|
100
|
+
const sampleSize = sampleTable.sampleSizes[Math.min(sampleIndex, sampleTable.sampleSizes.length - 1)];
|
|
101
|
+
if (sampleSize === void 0) return null;
|
|
102
|
+
const chunkEntryIndex = binarySearchLessOrEqual(sampleTable.sampleToChunk, sampleIndex, (entry) => entry.startSampleIndex);
|
|
103
|
+
const chunkEntry = sampleTable.sampleToChunk[chunkEntryIndex];
|
|
104
|
+
if (!chunkEntry) return null;
|
|
105
|
+
const chunkIndex = chunkEntry.startChunkIndex + Math.floor((sampleIndex - chunkEntry.startSampleIndex) / chunkEntry.samplesPerChunk);
|
|
106
|
+
const chunkOffset = sampleTable.chunkOffsets[chunkIndex];
|
|
107
|
+
if (chunkOffset === void 0) return null;
|
|
108
|
+
const startSampleIndexOfChunk = chunkEntry.startSampleIndex + (chunkIndex - chunkEntry.startChunkIndex) * chunkEntry.samplesPerChunk;
|
|
109
|
+
let chunkSize = 0;
|
|
110
|
+
let sampleOffset = chunkOffset;
|
|
111
|
+
if (sampleTable.sampleSizes.length === 1) {
|
|
112
|
+
sampleOffset += sampleSize * (sampleIndex - startSampleIndexOfChunk);
|
|
113
|
+
chunkSize += sampleSize * chunkEntry.samplesPerChunk;
|
|
114
|
+
} else for (let i = startSampleIndexOfChunk; i < startSampleIndexOfChunk + chunkEntry.samplesPerChunk; i++) {
|
|
115
|
+
const currentSampleSize = sampleTable.sampleSizes[i];
|
|
116
|
+
if (currentSampleSize === void 0) return null;
|
|
117
|
+
if (i < sampleIndex) sampleOffset += currentSampleSize;
|
|
118
|
+
chunkSize += currentSampleSize;
|
|
119
|
+
}
|
|
120
|
+
let duration = timingEntry.delta;
|
|
121
|
+
if (sampleTable.presentationTimestamps && sampleTable.presentationTimestampIndexMap) {
|
|
122
|
+
const presentationIndex = sampleTable.presentationTimestampIndexMap[sampleIndex];
|
|
123
|
+
if (presentationIndex !== void 0 && presentationIndex < sampleTable.presentationTimestamps.length - 1) {
|
|
124
|
+
const nextEntry = sampleTable.presentationTimestamps[presentationIndex + 1];
|
|
125
|
+
if (nextEntry) duration = nextEntry.presentationTimestamp - presentationTimestamp;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const isKeyFrame = sampleTable.keySampleIndices ? binarySearchExact(sampleTable.keySampleIndices, sampleIndex, (value) => value) !== -1 : true;
|
|
129
|
+
return {
|
|
130
|
+
presentationTimestamp,
|
|
131
|
+
duration,
|
|
132
|
+
sampleOffset,
|
|
133
|
+
sampleSize,
|
|
134
|
+
chunkOffset,
|
|
135
|
+
chunkSize,
|
|
136
|
+
isKeyFrame
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
const psshBoxesAreEqual = (left, right) => {
|
|
140
|
+
if (left.systemId !== right.systemId) return false;
|
|
141
|
+
if (left.data.byteLength !== right.data.byteLength) return false;
|
|
142
|
+
for (let i = 0; i < left.data.byteLength; i++) if (left.data[i] !== right.data[i]) return false;
|
|
143
|
+
return true;
|
|
144
|
+
};
|
|
145
|
+
const collectCryptRanges = (subsamples, cryptByteBlock, skipByteBlock) => {
|
|
146
|
+
const ranges = [];
|
|
147
|
+
const hasPattern = cryptByteBlock !== 0 || skipByteBlock !== 0;
|
|
148
|
+
let cursor = 0;
|
|
149
|
+
for (const subsample of subsamples) {
|
|
150
|
+
cursor += subsample.clearLen;
|
|
151
|
+
if (!hasPattern) {
|
|
152
|
+
if (subsample.protectedLen > 0) ranges.push({
|
|
153
|
+
offset: cursor,
|
|
154
|
+
length: subsample.protectedLen
|
|
155
|
+
});
|
|
156
|
+
cursor += subsample.protectedLen;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
let remaining = subsample.protectedLen;
|
|
160
|
+
let position = cursor;
|
|
161
|
+
while (remaining > 0) {
|
|
162
|
+
if (remaining < 16 * cryptByteBlock) break;
|
|
163
|
+
const cryptLength = 16 * cryptByteBlock;
|
|
164
|
+
ranges.push({
|
|
165
|
+
offset: position,
|
|
166
|
+
length: cryptLength
|
|
167
|
+
});
|
|
168
|
+
position += cryptLength;
|
|
169
|
+
remaining -= cryptLength;
|
|
170
|
+
const skipLength = Math.min(16 * skipByteBlock, remaining);
|
|
171
|
+
position += skipLength;
|
|
172
|
+
remaining -= skipLength;
|
|
173
|
+
}
|
|
174
|
+
cursor += subsample.protectedLen;
|
|
175
|
+
}
|
|
176
|
+
return ranges;
|
|
177
|
+
};
|
|
178
|
+
const cloneSubsamples = (subsamples) => {
|
|
179
|
+
if (!subsamples) return null;
|
|
180
|
+
return subsamples.map((subsample) => ({
|
|
181
|
+
clearLen: subsample.clearLen,
|
|
182
|
+
protectedLen: subsample.protectedLen
|
|
183
|
+
}));
|
|
184
|
+
};
|
|
185
|
+
const getPattern = (encryptionInfo) => {
|
|
186
|
+
const cryptByteBlock = encryptionInfo.defaultCryptByteBlock ?? 0;
|
|
187
|
+
const skipByteBlock = encryptionInfo.defaultSkipByteBlock ?? 0;
|
|
188
|
+
if (cryptByteBlock === 0 && skipByteBlock === 0) return null;
|
|
189
|
+
return {
|
|
190
|
+
cryptByteBlock,
|
|
191
|
+
skipByteBlock
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
const createProtectedDataLayout = (data, encryptionInfo, sampleEncryption) => {
|
|
195
|
+
if (!sampleEncryption.subsamples) return {
|
|
196
|
+
data,
|
|
197
|
+
merge: (decryptedData) => new Uint8Array(decryptedData)
|
|
198
|
+
};
|
|
199
|
+
if (encryptionInfo.scheme === "cbcs") return null;
|
|
200
|
+
const cryptRanges = collectCryptRanges(sampleEncryption.subsamples, encryptionInfo.defaultCryptByteBlock ?? 0, encryptionInfo.defaultSkipByteBlock ?? 0);
|
|
201
|
+
let totalCryptLength = 0;
|
|
202
|
+
for (const range of cryptRanges) totalCryptLength += range.length;
|
|
203
|
+
const protectedData = new Uint8Array(totalCryptLength);
|
|
204
|
+
let writeOffset = 0;
|
|
205
|
+
for (const range of cryptRanges) {
|
|
206
|
+
protectedData.set(data.subarray(range.offset, range.offset + range.length), writeOffset);
|
|
207
|
+
writeOffset += range.length;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
data: protectedData,
|
|
211
|
+
merge: (decryptedData) => {
|
|
212
|
+
const merged = new Uint8Array(data);
|
|
213
|
+
let readOffset = 0;
|
|
214
|
+
for (const range of cryptRanges) {
|
|
215
|
+
merged.set(decryptedData.subarray(readOffset, readOffset + range.length), range.offset);
|
|
216
|
+
readOffset += range.length;
|
|
217
|
+
}
|
|
218
|
+
return merged;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
const isUint8Array = (value) => value instanceof Uint8Array;
|
|
223
|
+
const isIsobmffTrackBacking = (backing) => {
|
|
224
|
+
if (!backing || typeof backing !== "object") return false;
|
|
225
|
+
const maybeBacking = backing;
|
|
226
|
+
const internalTrack = maybeBacking.internalTrack;
|
|
227
|
+
return Boolean(internalTrack && internalTrack.demuxer && typeof internalTrack.demuxer.getSampleTableForTrack === "function" && maybeBacking.packetToSampleIndex instanceof WeakMap && maybeBacking.packetToFragmentLocation instanceof WeakMap);
|
|
228
|
+
};
|
|
229
|
+
const getTrackEncryptionInfo = (backing) => backing.internalTrack.encryptionInfo;
|
|
230
|
+
const getKeyId = (backing) => backing.internalTrack.encryptionInfo?.defaultKid ?? null;
|
|
231
|
+
const getEncryptedSample = async (backing, packet) => {
|
|
232
|
+
const internalTrack = backing.internalTrack;
|
|
233
|
+
const fragmentLocation = backing.packetToFragmentLocation.get(packet);
|
|
234
|
+
if (fragmentLocation) {
|
|
235
|
+
const fragment = fragmentLocation.fragment;
|
|
236
|
+
const sample = fragment.trackData.get(internalTrack.id)?.samples[fragmentLocation.sampleIndex];
|
|
237
|
+
if (!sample?.encryption) return null;
|
|
238
|
+
return {
|
|
239
|
+
data: await readSampleBytes(internalTrack, sample.byteOffset, sample.byteSize),
|
|
240
|
+
sampleEncryption: sample.encryption,
|
|
241
|
+
fragment
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const sampleIndex = backing.packetToSampleIndex.get(packet);
|
|
245
|
+
if (sampleIndex === void 0) return null;
|
|
246
|
+
const sampleInfo = getSampleInfo(internalTrack.demuxer.getSampleTableForTrack(internalTrack), sampleIndex);
|
|
247
|
+
if (!sampleInfo) return null;
|
|
248
|
+
const auxInfo = internalTrack.encryptionAuxInfo;
|
|
249
|
+
if (!auxInfo) return null;
|
|
250
|
+
const sampleEncryption = (await resolveEncryptionAuxInfo(internalTrack, auxInfo))[sampleIndex];
|
|
251
|
+
if (!sampleEncryption) return null;
|
|
252
|
+
return {
|
|
253
|
+
data: await readSampleBytes(internalTrack, sampleInfo.sampleOffset, sampleInfo.sampleSize),
|
|
254
|
+
sampleEncryption,
|
|
255
|
+
fragment: null
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
const getPsshBoxes = (backing, keyId, fragment) => {
|
|
259
|
+
const { psshBoxes } = backing.internalTrack.demuxer;
|
|
260
|
+
if (!fragment) return psshBoxes;
|
|
261
|
+
const boxes = [...psshBoxes, ...fragment.psshBoxes].filter((psshBox) => psshBox.keyIds === null || psshBox.keyIds.includes(keyId));
|
|
262
|
+
for (let i = 0; i < boxes.length - 1; i++) for (let j = i + 1; j < boxes.length; j++) if (psshBoxesAreEqual(boxes[i], boxes[j])) {
|
|
263
|
+
boxes.splice(j, 1);
|
|
264
|
+
j--;
|
|
265
|
+
}
|
|
266
|
+
return boxes;
|
|
267
|
+
};
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/isobmff-decrypt-sample-patcher.ts
|
|
270
|
+
const createMetadataOnlyOptions = (options) => ({
|
|
271
|
+
...options,
|
|
272
|
+
metadataOnly: true,
|
|
273
|
+
verifyKeyPackets: false
|
|
274
|
+
});
|
|
275
|
+
var IsobmffDecryptSamplePatcher = class {
|
|
276
|
+
input;
|
|
277
|
+
decryptSample;
|
|
278
|
+
decryptProtectedData;
|
|
279
|
+
reportEncryptionInfo;
|
|
280
|
+
patchableTrackBackings = /* @__PURE__ */ new WeakSet();
|
|
281
|
+
patchTrackBackingsPromise = null;
|
|
282
|
+
constructor(input, decryptSample, decryptProtectedData, reportEncryptionInfo) {
|
|
283
|
+
this.input = input;
|
|
284
|
+
this.decryptSample = decryptSample;
|
|
285
|
+
this.decryptProtectedData = decryptProtectedData;
|
|
286
|
+
this.reportEncryptionInfo = reportEncryptionInfo;
|
|
287
|
+
}
|
|
288
|
+
async ensurePatched() {
|
|
289
|
+
this.patchTrackBackingsPromise ??= (async () => {
|
|
290
|
+
const tracks = await this.input.getTracks();
|
|
291
|
+
for (const track of tracks) {
|
|
292
|
+
const backing = track._backing;
|
|
293
|
+
if (!isIsobmffTrackBacking(backing) || this.patchableTrackBackings.has(backing)) continue;
|
|
294
|
+
this.patchableTrackBackings.add(backing);
|
|
295
|
+
this.patchTrackBacking(backing);
|
|
296
|
+
}
|
|
297
|
+
})();
|
|
298
|
+
await this.patchTrackBackingsPromise;
|
|
299
|
+
}
|
|
300
|
+
patchTrackBacking(backing) {
|
|
301
|
+
const packetToMetadataPacket = /* @__PURE__ */ new WeakMap();
|
|
302
|
+
const getOriginalPacket = (packet) => packetToMetadataPacket.get(packet) ?? packet;
|
|
303
|
+
const getFirstPacket = backing.getFirstPacket.bind(backing);
|
|
304
|
+
backing.getFirstPacket = async (options) => {
|
|
305
|
+
if (!this.shouldUseCustomDecryptSample(backing, options)) return getFirstPacket(options);
|
|
306
|
+
const metadataPacket = await getFirstPacket(createMetadataOnlyOptions(options));
|
|
307
|
+
return this.materializePacket(backing, metadataPacket, () => getFirstPacket(options), packetToMetadataPacket);
|
|
308
|
+
};
|
|
309
|
+
const getPacket = backing.getPacket.bind(backing);
|
|
310
|
+
backing.getPacket = async (timestamp, options) => {
|
|
311
|
+
if (!this.shouldUseCustomDecryptSample(backing, options)) return getPacket(timestamp, options);
|
|
312
|
+
const metadataPacket = await getPacket(timestamp, createMetadataOnlyOptions(options));
|
|
313
|
+
return this.materializePacket(backing, metadataPacket, () => getPacket(timestamp, options), packetToMetadataPacket);
|
|
314
|
+
};
|
|
315
|
+
const getNextPacket = backing.getNextPacket.bind(backing);
|
|
316
|
+
backing.getNextPacket = async (packet, options) => {
|
|
317
|
+
const originalPacket = getOriginalPacket(packet);
|
|
318
|
+
if (!this.shouldUseCustomDecryptSample(backing, options)) return getNextPacket(originalPacket, options);
|
|
319
|
+
const metadataPacket = await getNextPacket(originalPacket, createMetadataOnlyOptions(options));
|
|
320
|
+
return this.materializePacket(backing, metadataPacket, () => getNextPacket(originalPacket, options), packetToMetadataPacket);
|
|
321
|
+
};
|
|
322
|
+
const getKeyPacket = backing.getKeyPacket.bind(backing);
|
|
323
|
+
backing.getKeyPacket = async (timestamp, options) => {
|
|
324
|
+
if (!this.shouldUseCustomDecryptSample(backing, options)) return getKeyPacket(timestamp, options);
|
|
325
|
+
const metadataPacket = await getKeyPacket(timestamp, createMetadataOnlyOptions(options));
|
|
326
|
+
return this.materializePacket(backing, metadataPacket, () => getKeyPacket(timestamp, options), packetToMetadataPacket);
|
|
327
|
+
};
|
|
328
|
+
const getNextKeyPacket = backing.getNextKeyPacket.bind(backing);
|
|
329
|
+
backing.getNextKeyPacket = async (packet, options) => {
|
|
330
|
+
const originalPacket = getOriginalPacket(packet);
|
|
331
|
+
if (!this.shouldUseCustomDecryptSample(backing, options)) return getNextKeyPacket(originalPacket, options);
|
|
332
|
+
const metadataPacket = await getNextKeyPacket(originalPacket, createMetadataOnlyOptions(options));
|
|
333
|
+
return this.materializePacket(backing, metadataPacket, () => getNextKeyPacket(originalPacket, options), packetToMetadataPacket);
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
shouldUseCustomDecryptSample(backing, options) {
|
|
337
|
+
return Boolean(!options.metadataOnly && getTrackEncryptionInfo(backing)?.defaultIsProtected !== false);
|
|
338
|
+
}
|
|
339
|
+
async materializePacket(backing, metadataPacket, fallback, packetToMetadataPacket) {
|
|
340
|
+
if (!metadataPacket) return null;
|
|
341
|
+
const decryptedPacket = await this.decryptPacket(backing, metadataPacket, fallback);
|
|
342
|
+
if (decryptedPacket && decryptedPacket !== metadataPacket) packetToMetadataPacket.set(decryptedPacket, metadataPacket);
|
|
343
|
+
return decryptedPacket;
|
|
344
|
+
}
|
|
345
|
+
async decryptPacket(backing, metadataPacket, fallback) {
|
|
346
|
+
const encryptionInfo = getTrackEncryptionInfo(backing);
|
|
347
|
+
if (!encryptionInfo?.defaultIsProtected) return fallback();
|
|
348
|
+
const keyId = getKeyId(backing);
|
|
349
|
+
if (!keyId) throw new Error("Encrypted sample encountered without a key ID.");
|
|
350
|
+
const encryptedSample = await getEncryptedSample(backing, metadataPacket);
|
|
351
|
+
if (!encryptedSample) return fallback();
|
|
352
|
+
const psshBoxes = getPsshBoxes(backing, keyId, encryptedSample.fragment);
|
|
353
|
+
this.reportEncryptionInfo(keyId, psshBoxes);
|
|
354
|
+
const decryptedData = this.decryptSample ? await this.decryptSample({
|
|
355
|
+
data: encryptedSample.data,
|
|
356
|
+
keyId,
|
|
357
|
+
psshBoxes,
|
|
358
|
+
scheme: encryptionInfo.scheme,
|
|
359
|
+
iv: encryptedSample.sampleEncryption.iv.slice(),
|
|
360
|
+
timestamp: metadataPacket.microsecondTimestamp,
|
|
361
|
+
subsamples: cloneSubsamples(encryptedSample.sampleEncryption.subsamples),
|
|
362
|
+
pattern: getPattern(encryptionInfo)
|
|
363
|
+
}) : await this.decryptWithProtectedData(encryptionInfo, encryptedSample, metadataPacket.microsecondTimestamp, keyId, psshBoxes);
|
|
364
|
+
if (!isUint8Array(decryptedData)) throw new TypeError("decryption callback must return a Uint8Array.");
|
|
365
|
+
if (decryptedData.byteLength !== metadataPacket.byteLength) throw new Error("decryption callback must return the same number of bytes as the encrypted sample.");
|
|
366
|
+
return new EncodedPacket(decryptedData, metadataPacket.type, metadataPacket.timestamp, metadataPacket.duration, metadataPacket.sequenceNumber, metadataPacket.byteLength, metadataPacket.sideData);
|
|
367
|
+
}
|
|
368
|
+
async decryptWithProtectedData(encryptionInfo, encryptedSample, timestamp, keyId, psshBoxes) {
|
|
369
|
+
if (!this.decryptProtectedData) throw new Error("Encrypted sample encountered without a decryption callback.");
|
|
370
|
+
const protectedDataLayout = createProtectedDataLayout(encryptedSample.data, encryptionInfo, encryptedSample.sampleEncryption);
|
|
371
|
+
if (!protectedDataLayout) throw new Error("decryptProtectedData does not support cbcs samples with subsample encryption. Use decryptSample for this content.");
|
|
372
|
+
const decryptedProtectedData = await this.decryptProtectedData({
|
|
373
|
+
data: protectedDataLayout.data,
|
|
374
|
+
keyId,
|
|
375
|
+
psshBoxes,
|
|
376
|
+
scheme: encryptionInfo.scheme,
|
|
377
|
+
iv: encryptedSample.sampleEncryption.iv.slice(),
|
|
378
|
+
timestamp,
|
|
379
|
+
subsamples: [{
|
|
380
|
+
clearLen: 0,
|
|
381
|
+
protectedLen: protectedDataLayout.data.byteLength
|
|
382
|
+
}],
|
|
383
|
+
pattern: null
|
|
384
|
+
});
|
|
385
|
+
if (!isUint8Array(decryptedProtectedData)) throw new TypeError("decryptProtectedData must return a Uint8Array.");
|
|
386
|
+
if (decryptedProtectedData.byteLength !== protectedDataLayout.data.byteLength) throw new Error("decryptProtectedData must return the same number of bytes as the protected data input.");
|
|
387
|
+
return protectedDataLayout.merge(decryptedProtectedData);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
//#endregion
|
|
9
391
|
//#region src/index.ts
|
|
392
|
+
const createEncryptionReporter = (handleEncryptionInfo) => {
|
|
393
|
+
const reportedEncryptionInfo = /* @__PURE__ */ new Set();
|
|
394
|
+
return (keyId, psshBoxes) => {
|
|
395
|
+
if (!handleEncryptionInfo || reportedEncryptionInfo.has(keyId)) return;
|
|
396
|
+
reportedEncryptionInfo.add(keyId);
|
|
397
|
+
handleEncryptionInfo({
|
|
398
|
+
keyId,
|
|
399
|
+
psshBoxes
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
};
|
|
10
403
|
var Input$1 = class extends Input {
|
|
11
|
-
|
|
404
|
+
decryptSamplePatcher;
|
|
405
|
+
constructor({ keys, decryptSample, decryptProtectedData, handleEncryptionInfo, ...options }) {
|
|
406
|
+
if (!keys?.size && !decryptSample && !decryptProtectedData) throw new Error("Either keys, decryptSample, or decryptProtectedData must be provided.");
|
|
407
|
+
const reportEncryptionInfo = createEncryptionReporter(handleEncryptionInfo);
|
|
12
408
|
super({
|
|
13
409
|
...options,
|
|
14
410
|
formats: ALL_FORMATS,
|
|
15
411
|
formatOptions: { isobmff: { resolveKeyId: ({ keyId, psshBoxes }) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
psshBoxes
|
|
19
|
-
});
|
|
20
|
-
const key = keys.get(keyId);
|
|
412
|
+
reportEncryptionInfo(keyId, psshBoxes);
|
|
413
|
+
const key = keys?.get(keyId);
|
|
21
414
|
if (!key) throw new Error("Unknown key ID.");
|
|
22
415
|
return key;
|
|
23
416
|
} } }
|
|
24
417
|
});
|
|
418
|
+
this.decryptSamplePatcher = decryptSample || decryptProtectedData ? new IsobmffDecryptSamplePatcher({ getTracks: (query) => this.getTracksWithoutPatching(query) }, decryptSample, decryptProtectedData, reportEncryptionInfo) : null;
|
|
419
|
+
}
|
|
420
|
+
async getTracks(query) {
|
|
421
|
+
await this.decryptSamplePatcher?.ensurePatched();
|
|
422
|
+
return super.getTracks(query);
|
|
423
|
+
}
|
|
424
|
+
async getTracksWithoutPatching(query) {
|
|
425
|
+
return super.getTracks(query);
|
|
25
426
|
}
|
|
26
427
|
};
|
|
27
428
|
var Output$1 = class extends Output {
|
package/dist/index.d.mts
CHANGED
|
@@ -1,21 +1,140 @@
|
|
|
1
|
-
import { Conversion, FilePathSource, FilePathTarget, Input as Input$1, InputOptions, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output as Output$1, OutputFormat, OutputOptions, PsshBox, ReadableStreamSource, StreamSource, StreamSourceOptions, StreamTarget, StreamTargetOptions, UrlSource } from "mediabunny";
|
|
1
|
+
import { Conversion, FilePathSource, FilePathTarget, Input as Input$1, InputOptions as MediabunnyInputOptions, InputTrack, InputTrackQuery, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output as Output$1, OutputFormat, OutputOptions, PsshBox, ReadableStreamSource, StreamSource, StreamSourceOptions, StreamTarget, StreamTargetOptions, UrlSource } from "mediabunny";
|
|
2
2
|
|
|
3
|
-
//#region src/
|
|
3
|
+
//#region src/decrypt-sample.d.ts
|
|
4
4
|
type KeyId = string;
|
|
5
5
|
type Key = string;
|
|
6
6
|
type KeyMap = Map<KeyId, Key>;
|
|
7
|
+
type IsobmffScheme = 'cenc' | 'cens' | 'cbcs';
|
|
8
|
+
type SubsampleEncryption = {
|
|
9
|
+
/** Number of clear bytes that appear before the protected bytes in this subsample. */clearLen: number; /** Number of encrypted bytes in this subsample. */
|
|
10
|
+
protectedLen: number;
|
|
11
|
+
};
|
|
12
|
+
type EncryptionPattern = {
|
|
13
|
+
/** Number of consecutive 16-byte blocks that are encrypted in each pattern step. */cryptByteBlock: number; /** Number of consecutive 16-byte blocks that are skipped in each pattern step. */
|
|
14
|
+
skipByteBlock: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Options passed to {@link DecryptSample}.
|
|
18
|
+
*
|
|
19
|
+
* This is the advanced callback contract. `data` contains the full sample bytes exactly as they appear in the
|
|
20
|
+
* container, including any clear regions. `subsamples` and `pattern` describe which parts of `data` are protected.
|
|
21
|
+
*
|
|
22
|
+
* Use this callback when the decryption backend needs the original sample layout, or when working with content
|
|
23
|
+
* that cannot be safely represented as a single flattened protected-byte stream, such as patterned `cbcs`
|
|
24
|
+
* subsample encryption.
|
|
25
|
+
*/
|
|
26
|
+
type IsobmffDecryptSampleOptions = {
|
|
27
|
+
/** Full sample bytes as stored in the container. Clear and protected regions are both present in this buffer. */data: Uint8Array; /** Key ID from the track encryption metadata. */
|
|
28
|
+
keyId: string; /** PSSH boxes associated with this key/sample and useful for DRM-specific key resolution. */
|
|
29
|
+
psshBoxes: PsshBox[]; /** Sample encryption scheme. */
|
|
30
|
+
scheme: IsobmffScheme; /** Per-sample IV resolved from container metadata. */
|
|
31
|
+
iv: Uint8Array; /** Presentation timestamp of the sample in microseconds. */
|
|
32
|
+
timestamp: number; /** Subsample layout describing which parts of `data` are clear vs protected, or `null` for whole-sample encryption. */
|
|
33
|
+
subsamples: SubsampleEncryption[] | null; /** Pattern encryption parameters, or `null` when no pattern is used. */
|
|
34
|
+
pattern: EncryptionPattern | null;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Options passed to {@link DecryptProtectedData}.
|
|
38
|
+
*
|
|
39
|
+
* This is the compatibility/simple callback contract. `data` contains only the protected byte stream that needs
|
|
40
|
+
* to be decrypted. `shifro` is responsible for extracting those protected bytes from the sample and inserting the
|
|
41
|
+
* decrypted result back into the sample afterward.
|
|
42
|
+
*
|
|
43
|
+
* This matches the old `transformSample` style API used by `wapter`.
|
|
44
|
+
*/
|
|
45
|
+
type IsobmffDecryptProtectedDataOptions = {
|
|
46
|
+
/** Only the protected bytes that should be decrypted. Clear sample bytes are not included. */data: Uint8Array; /** Key ID from the track encryption metadata. */
|
|
47
|
+
keyId: string; /** PSSH boxes associated with this key/sample and useful for DRM-specific key resolution. */
|
|
48
|
+
psshBoxes: PsshBox[]; /** Sample encryption scheme. */
|
|
49
|
+
scheme: IsobmffScheme; /** Per-sample IV resolved from container metadata. */
|
|
50
|
+
iv: Uint8Array; /** Presentation timestamp of the sample in microseconds. */
|
|
51
|
+
timestamp: number;
|
|
52
|
+
/**
|
|
53
|
+
* Subsample layout for the protected-data buffer passed to this callback.
|
|
54
|
+
*
|
|
55
|
+
* Since `data` already contains only protected bytes, this is typically a single entry covering the whole buffer.
|
|
56
|
+
*/
|
|
57
|
+
subsamples: SubsampleEncryption[];
|
|
58
|
+
/**
|
|
59
|
+
* Pattern metadata for the protected-data buffer passed to this callback.
|
|
60
|
+
*
|
|
61
|
+
* This is `null` when the flattened protected-data representation no longer has any skipped blocks to describe.
|
|
62
|
+
*/
|
|
63
|
+
pattern: EncryptionPattern | null;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Advanced sample decryption callback.
|
|
67
|
+
*
|
|
68
|
+
* The callback receives the full encrypted sample layout and must return the fully decrypted sample bytes with the
|
|
69
|
+
* exact same length. This is the most general integration point and should be used when a backend needs explicit
|
|
70
|
+
* subsample and pattern metadata.
|
|
71
|
+
*/
|
|
72
|
+
type DecryptSample = (options: IsobmffDecryptSampleOptions) => Uint8Array | Promise<Uint8Array>;
|
|
73
|
+
/**
|
|
74
|
+
* Simplified protected-data decryption callback.
|
|
75
|
+
*
|
|
76
|
+
* The callback receives only the protected byte stream extracted from a sample and must return the decrypted bytes
|
|
77
|
+
* for that protected region with the exact same length. `shifro` handles extraction from and reinsertion into the
|
|
78
|
+
* original sample.
|
|
79
|
+
*
|
|
80
|
+
* This is the recommended callback for integrations that only need a raw "decrypt these bytes" operation.
|
|
81
|
+
*
|
|
82
|
+
* Note: this simplified callback is not sufficient for every encryption layout. For example, `cbcs` samples with
|
|
83
|
+
* subsample encryption still require {@link DecryptSample}.
|
|
84
|
+
*/
|
|
85
|
+
type DecryptProtectedData = (options: IsobmffDecryptProtectedDataOptions) => Uint8Array | Promise<Uint8Array>;
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/index.d.ts
|
|
88
|
+
/**
|
|
89
|
+
* Options for creating a `shifro` {@link Input}.
|
|
90
|
+
*
|
|
91
|
+
* In addition to the regular Mediabunny input options, `shifro` accepts either raw content keys or one of the
|
|
92
|
+
* decryption callbacks used to delegate decryption to another library.
|
|
93
|
+
*/
|
|
94
|
+
type ShifroInputOptions = Omit<MediabunnyInputOptions, 'formats' | 'formatOptions'> & {
|
|
95
|
+
/**
|
|
96
|
+
* Content decryption keys indexed by key ID.
|
|
97
|
+
*
|
|
98
|
+
* Use this when the caller already has the actual content keys and wants `shifro`/Mediabunny to perform the
|
|
99
|
+
* sample decryption internally.
|
|
100
|
+
*/
|
|
101
|
+
keys?: KeyMap;
|
|
102
|
+
/**
|
|
103
|
+
* Advanced decryption callback that receives the full encrypted sample layout.
|
|
104
|
+
*
|
|
105
|
+
* Use this when the decryption backend needs explicit access to the sample's clear/protected layout, subsample
|
|
106
|
+
* metadata, or pattern encryption information.
|
|
107
|
+
*/
|
|
108
|
+
decryptSample?: DecryptSample;
|
|
109
|
+
/**
|
|
110
|
+
* Simplified decryption callback that receives only the flattened protected byte stream.
|
|
111
|
+
*
|
|
112
|
+
* `shifro` extracts the protected bytes from the sample before calling this callback and reinserts the decrypted
|
|
113
|
+
* bytes back into the sample afterward.
|
|
114
|
+
*/
|
|
115
|
+
decryptProtectedData?: DecryptProtectedData;
|
|
116
|
+
/**
|
|
117
|
+
* Optional callback invoked when encryption metadata is discovered for a sample.
|
|
118
|
+
*
|
|
119
|
+
* This is useful when the caller wants access to the key ID and PSSH boxes without providing the key directly,
|
|
120
|
+
* for example to perform external DRM license handling.
|
|
121
|
+
*/
|
|
122
|
+
handleEncryptionInfo?: (options: {
|
|
123
|
+
keyId: string;
|
|
124
|
+
psshBoxes: PsshBox[];
|
|
125
|
+
}) => void;
|
|
126
|
+
};
|
|
7
127
|
declare class Input extends Input$1 {
|
|
128
|
+
private readonly decryptSamplePatcher;
|
|
8
129
|
constructor({
|
|
9
130
|
keys,
|
|
131
|
+
decryptSample,
|
|
132
|
+
decryptProtectedData,
|
|
10
133
|
handleEncryptionInfo,
|
|
11
134
|
...options
|
|
12
|
-
}:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
keyId: string;
|
|
16
|
-
psshBoxes: PsshBox[];
|
|
17
|
-
}) => void;
|
|
18
|
-
});
|
|
135
|
+
}: ShifroInputOptions);
|
|
136
|
+
getTracks(query?: InputTrackQuery<InputTrack>): Promise<InputTrack[]>;
|
|
137
|
+
private getTracksWithoutPatching;
|
|
19
138
|
}
|
|
20
139
|
declare class Output extends Output$1 {
|
|
21
140
|
constructor({
|
|
@@ -25,4 +144,4 @@ declare class Output extends Output$1 {
|
|
|
25
144
|
});
|
|
26
145
|
}
|
|
27
146
|
//#endregion
|
|
28
|
-
export { Conversion as Decryption, FilePathSource, FilePathTarget, Input, type InputOptions, type Key, type KeyId, type KeyMap, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output, type OutputFormat, type OutputOptions, ReadableStreamSource, StreamSource, type StreamSourceOptions, StreamTarget, type StreamTargetOptions, UrlSource };
|
|
147
|
+
export { type DecryptProtectedData, type DecryptSample, Conversion as Decryption, FilePathSource, FilePathTarget, Input, type MediabunnyInputOptions as InputOptions, type IsobmffDecryptProtectedDataOptions, type IsobmffDecryptSampleOptions, type Key, type KeyId, type KeyMap, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output, type OutputFormat, type OutputOptions, ReadableStreamSource, ShifroInputOptions, StreamSource, type StreamSourceOptions, StreamTarget, type StreamTargetOptions, UrlSource };
|
package/dist/index.mjs
CHANGED
|
@@ -1,20 +1,421 @@
|
|
|
1
|
-
import { ALL_FORMATS, Conversion, FilePathSource, FilePathTarget, Input as Input$1, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output as Output$1, ReadableStreamSource, StreamSource, StreamTarget, UrlSource } from "mediabunny";
|
|
1
|
+
import { ALL_FORMATS, Conversion, EncodedPacket, FilePathSource, FilePathTarget, Input as Input$1, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output as Output$1, ReadableStreamSource, StreamSource, StreamTarget, UrlSource } from "mediabunny";
|
|
2
|
+
//#region src/mediabunny-isobmff.ts
|
|
3
|
+
const binarySearchLessOrEqual = (items, needle, getValue) => {
|
|
4
|
+
let low = 0;
|
|
5
|
+
let high = items.length - 1;
|
|
6
|
+
let result = -1;
|
|
7
|
+
while (low <= high) {
|
|
8
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
9
|
+
if (getValue(items[middle]) <= needle) {
|
|
10
|
+
result = middle;
|
|
11
|
+
low = middle + 1;
|
|
12
|
+
} else high = middle - 1;
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
};
|
|
16
|
+
const binarySearchExact = (items, needle, getValue) => {
|
|
17
|
+
let low = 0;
|
|
18
|
+
let high = items.length - 1;
|
|
19
|
+
while (low <= high) {
|
|
20
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
21
|
+
const value = getValue(items[middle]);
|
|
22
|
+
if (value === needle) return middle;
|
|
23
|
+
if (value < needle) low = middle + 1;
|
|
24
|
+
else high = middle - 1;
|
|
25
|
+
}
|
|
26
|
+
return -1;
|
|
27
|
+
};
|
|
28
|
+
const readBytes = (slice, length) => {
|
|
29
|
+
const bytes = slice.bytes.slice(slice.bufferPos, slice.bufferPos + length);
|
|
30
|
+
slice.bufferPos += length;
|
|
31
|
+
return bytes;
|
|
32
|
+
};
|
|
33
|
+
const readU16Be = (slice) => {
|
|
34
|
+
const value = slice.view.getUint16(slice.bufferPos, false);
|
|
35
|
+
slice.bufferPos += 2;
|
|
36
|
+
return value;
|
|
37
|
+
};
|
|
38
|
+
const readU32Be = (slice) => {
|
|
39
|
+
const value = slice.view.getUint32(slice.bufferPos, false);
|
|
40
|
+
slice.bufferPos += 4;
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
const readSampleBytes = async (internalTrack, byteOffset, byteLength) => {
|
|
44
|
+
let slice = internalTrack.demuxer.reader.requestSlice(byteOffset, byteLength);
|
|
45
|
+
if (slice instanceof Promise) slice = await slice;
|
|
46
|
+
if (!slice) throw new Error("Failed to read encrypted sample bytes.");
|
|
47
|
+
return readBytes(slice, byteLength);
|
|
48
|
+
};
|
|
49
|
+
const resolveEncryptionAuxInfo = async (internalTrack, auxInfo) => {
|
|
50
|
+
if (auxInfo.resolved) return auxInfo.resolved;
|
|
51
|
+
const encryptionInfo = internalTrack.encryptionInfo;
|
|
52
|
+
if (!encryptionInfo || encryptionInfo.defaultPerSampleIvSize === null) throw new Error("Missing track encryption info.");
|
|
53
|
+
if (auxInfo.offset === null || auxInfo.sampleCount === 0) throw new Error("Incomplete saiz/saio info; cannot resolve encryption data.");
|
|
54
|
+
if (auxInfo.defaultSampleInfoSize === 0 && !auxInfo.sampleSizes) throw new Error("Invalid auxiliary encryption info: auxInfo.sampleSizes is required when auxInfo.defaultSampleInfoSize is 0.");
|
|
55
|
+
let totalSize = 0;
|
|
56
|
+
if (auxInfo.defaultSampleInfoSize > 0) totalSize = auxInfo.defaultSampleInfoSize * auxInfo.sampleCount;
|
|
57
|
+
else if (auxInfo.sampleSizes) for (const size of auxInfo.sampleSizes) totalSize += size;
|
|
58
|
+
let slice = internalTrack.demuxer.reader.requestSlice(auxInfo.offset, totalSize);
|
|
59
|
+
if (slice instanceof Promise) slice = await slice;
|
|
60
|
+
if (!slice) throw new Error("Failed to read auxiliary encryption info.");
|
|
61
|
+
const entries = [];
|
|
62
|
+
const ivSize = encryptionInfo.defaultPerSampleIvSize;
|
|
63
|
+
for (let i = 0; i < auxInfo.sampleCount; i++) {
|
|
64
|
+
const entrySize = auxInfo.defaultSampleInfoSize > 0 ? auxInfo.defaultSampleInfoSize : auxInfo.sampleSizes?.[i] ?? 0;
|
|
65
|
+
const iv = new Uint8Array(16);
|
|
66
|
+
if (ivSize > 0) iv.set(readBytes(slice, ivSize), 0);
|
|
67
|
+
else if (encryptionInfo.defaultConstantIv) iv.set(encryptionInfo.defaultConstantIv, 0);
|
|
68
|
+
let subsamples = null;
|
|
69
|
+
if (entrySize > ivSize) {
|
|
70
|
+
const subsampleCount = readU16Be(slice);
|
|
71
|
+
subsamples = [];
|
|
72
|
+
for (let j = 0; j < subsampleCount; j++) subsamples.push({
|
|
73
|
+
clearLen: readU16Be(slice),
|
|
74
|
+
protectedLen: readU32Be(slice)
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
entries.push({
|
|
78
|
+
iv,
|
|
79
|
+
subsamples
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
auxInfo.resolved = entries;
|
|
83
|
+
return entries;
|
|
84
|
+
};
|
|
85
|
+
const getSampleInfo = (sampleTable, sampleIndex) => {
|
|
86
|
+
const timingEntryIndex = binarySearchLessOrEqual(sampleTable.sampleTimingEntries, sampleIndex, (entry) => entry.startIndex);
|
|
87
|
+
const timingEntry = sampleTable.sampleTimingEntries[timingEntryIndex];
|
|
88
|
+
if (!timingEntry || timingEntry.startIndex + timingEntry.count <= sampleIndex) return null;
|
|
89
|
+
let presentationTimestamp = timingEntry.startDecodeTimestamp + (sampleIndex - timingEntry.startIndex) * timingEntry.delta;
|
|
90
|
+
const offsetEntryIndex = binarySearchLessOrEqual(sampleTable.sampleCompositionTimeOffsets, sampleIndex, (entry) => entry.startIndex);
|
|
91
|
+
const offsetEntry = sampleTable.sampleCompositionTimeOffsets[offsetEntryIndex];
|
|
92
|
+
if (offsetEntry && sampleIndex - offsetEntry.startIndex < offsetEntry.count) presentationTimestamp += offsetEntry.offset;
|
|
93
|
+
const sampleSize = sampleTable.sampleSizes[Math.min(sampleIndex, sampleTable.sampleSizes.length - 1)];
|
|
94
|
+
if (sampleSize === void 0) return null;
|
|
95
|
+
const chunkEntryIndex = binarySearchLessOrEqual(sampleTable.sampleToChunk, sampleIndex, (entry) => entry.startSampleIndex);
|
|
96
|
+
const chunkEntry = sampleTable.sampleToChunk[chunkEntryIndex];
|
|
97
|
+
if (!chunkEntry) return null;
|
|
98
|
+
const chunkIndex = chunkEntry.startChunkIndex + Math.floor((sampleIndex - chunkEntry.startSampleIndex) / chunkEntry.samplesPerChunk);
|
|
99
|
+
const chunkOffset = sampleTable.chunkOffsets[chunkIndex];
|
|
100
|
+
if (chunkOffset === void 0) return null;
|
|
101
|
+
const startSampleIndexOfChunk = chunkEntry.startSampleIndex + (chunkIndex - chunkEntry.startChunkIndex) * chunkEntry.samplesPerChunk;
|
|
102
|
+
let chunkSize = 0;
|
|
103
|
+
let sampleOffset = chunkOffset;
|
|
104
|
+
if (sampleTable.sampleSizes.length === 1) {
|
|
105
|
+
sampleOffset += sampleSize * (sampleIndex - startSampleIndexOfChunk);
|
|
106
|
+
chunkSize += sampleSize * chunkEntry.samplesPerChunk;
|
|
107
|
+
} else for (let i = startSampleIndexOfChunk; i < startSampleIndexOfChunk + chunkEntry.samplesPerChunk; i++) {
|
|
108
|
+
const currentSampleSize = sampleTable.sampleSizes[i];
|
|
109
|
+
if (currentSampleSize === void 0) return null;
|
|
110
|
+
if (i < sampleIndex) sampleOffset += currentSampleSize;
|
|
111
|
+
chunkSize += currentSampleSize;
|
|
112
|
+
}
|
|
113
|
+
let duration = timingEntry.delta;
|
|
114
|
+
if (sampleTable.presentationTimestamps && sampleTable.presentationTimestampIndexMap) {
|
|
115
|
+
const presentationIndex = sampleTable.presentationTimestampIndexMap[sampleIndex];
|
|
116
|
+
if (presentationIndex !== void 0 && presentationIndex < sampleTable.presentationTimestamps.length - 1) {
|
|
117
|
+
const nextEntry = sampleTable.presentationTimestamps[presentationIndex + 1];
|
|
118
|
+
if (nextEntry) duration = nextEntry.presentationTimestamp - presentationTimestamp;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const isKeyFrame = sampleTable.keySampleIndices ? binarySearchExact(sampleTable.keySampleIndices, sampleIndex, (value) => value) !== -1 : true;
|
|
122
|
+
return {
|
|
123
|
+
presentationTimestamp,
|
|
124
|
+
duration,
|
|
125
|
+
sampleOffset,
|
|
126
|
+
sampleSize,
|
|
127
|
+
chunkOffset,
|
|
128
|
+
chunkSize,
|
|
129
|
+
isKeyFrame
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
const psshBoxesAreEqual = (left, right) => {
|
|
133
|
+
if (left.systemId !== right.systemId) return false;
|
|
134
|
+
if (left.data.byteLength !== right.data.byteLength) return false;
|
|
135
|
+
for (let i = 0; i < left.data.byteLength; i++) if (left.data[i] !== right.data[i]) return false;
|
|
136
|
+
return true;
|
|
137
|
+
};
|
|
138
|
+
const collectCryptRanges = (subsamples, cryptByteBlock, skipByteBlock) => {
|
|
139
|
+
const ranges = [];
|
|
140
|
+
const hasPattern = cryptByteBlock !== 0 || skipByteBlock !== 0;
|
|
141
|
+
let cursor = 0;
|
|
142
|
+
for (const subsample of subsamples) {
|
|
143
|
+
cursor += subsample.clearLen;
|
|
144
|
+
if (!hasPattern) {
|
|
145
|
+
if (subsample.protectedLen > 0) ranges.push({
|
|
146
|
+
offset: cursor,
|
|
147
|
+
length: subsample.protectedLen
|
|
148
|
+
});
|
|
149
|
+
cursor += subsample.protectedLen;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
let remaining = subsample.protectedLen;
|
|
153
|
+
let position = cursor;
|
|
154
|
+
while (remaining > 0) {
|
|
155
|
+
if (remaining < 16 * cryptByteBlock) break;
|
|
156
|
+
const cryptLength = 16 * cryptByteBlock;
|
|
157
|
+
ranges.push({
|
|
158
|
+
offset: position,
|
|
159
|
+
length: cryptLength
|
|
160
|
+
});
|
|
161
|
+
position += cryptLength;
|
|
162
|
+
remaining -= cryptLength;
|
|
163
|
+
const skipLength = Math.min(16 * skipByteBlock, remaining);
|
|
164
|
+
position += skipLength;
|
|
165
|
+
remaining -= skipLength;
|
|
166
|
+
}
|
|
167
|
+
cursor += subsample.protectedLen;
|
|
168
|
+
}
|
|
169
|
+
return ranges;
|
|
170
|
+
};
|
|
171
|
+
const cloneSubsamples = (subsamples) => {
|
|
172
|
+
if (!subsamples) return null;
|
|
173
|
+
return subsamples.map((subsample) => ({
|
|
174
|
+
clearLen: subsample.clearLen,
|
|
175
|
+
protectedLen: subsample.protectedLen
|
|
176
|
+
}));
|
|
177
|
+
};
|
|
178
|
+
const getPattern = (encryptionInfo) => {
|
|
179
|
+
const cryptByteBlock = encryptionInfo.defaultCryptByteBlock ?? 0;
|
|
180
|
+
const skipByteBlock = encryptionInfo.defaultSkipByteBlock ?? 0;
|
|
181
|
+
if (cryptByteBlock === 0 && skipByteBlock === 0) return null;
|
|
182
|
+
return {
|
|
183
|
+
cryptByteBlock,
|
|
184
|
+
skipByteBlock
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
const createProtectedDataLayout = (data, encryptionInfo, sampleEncryption) => {
|
|
188
|
+
if (!sampleEncryption.subsamples) return {
|
|
189
|
+
data,
|
|
190
|
+
merge: (decryptedData) => new Uint8Array(decryptedData)
|
|
191
|
+
};
|
|
192
|
+
if (encryptionInfo.scheme === "cbcs") return null;
|
|
193
|
+
const cryptRanges = collectCryptRanges(sampleEncryption.subsamples, encryptionInfo.defaultCryptByteBlock ?? 0, encryptionInfo.defaultSkipByteBlock ?? 0);
|
|
194
|
+
let totalCryptLength = 0;
|
|
195
|
+
for (const range of cryptRanges) totalCryptLength += range.length;
|
|
196
|
+
const protectedData = new Uint8Array(totalCryptLength);
|
|
197
|
+
let writeOffset = 0;
|
|
198
|
+
for (const range of cryptRanges) {
|
|
199
|
+
protectedData.set(data.subarray(range.offset, range.offset + range.length), writeOffset);
|
|
200
|
+
writeOffset += range.length;
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
data: protectedData,
|
|
204
|
+
merge: (decryptedData) => {
|
|
205
|
+
const merged = new Uint8Array(data);
|
|
206
|
+
let readOffset = 0;
|
|
207
|
+
for (const range of cryptRanges) {
|
|
208
|
+
merged.set(decryptedData.subarray(readOffset, readOffset + range.length), range.offset);
|
|
209
|
+
readOffset += range.length;
|
|
210
|
+
}
|
|
211
|
+
return merged;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
const isUint8Array = (value) => value instanceof Uint8Array;
|
|
216
|
+
const isIsobmffTrackBacking = (backing) => {
|
|
217
|
+
if (!backing || typeof backing !== "object") return false;
|
|
218
|
+
const maybeBacking = backing;
|
|
219
|
+
const internalTrack = maybeBacking.internalTrack;
|
|
220
|
+
return Boolean(internalTrack && internalTrack.demuxer && typeof internalTrack.demuxer.getSampleTableForTrack === "function" && maybeBacking.packetToSampleIndex instanceof WeakMap && maybeBacking.packetToFragmentLocation instanceof WeakMap);
|
|
221
|
+
};
|
|
222
|
+
const getTrackEncryptionInfo = (backing) => backing.internalTrack.encryptionInfo;
|
|
223
|
+
const getKeyId = (backing) => backing.internalTrack.encryptionInfo?.defaultKid ?? null;
|
|
224
|
+
const getEncryptedSample = async (backing, packet) => {
|
|
225
|
+
const internalTrack = backing.internalTrack;
|
|
226
|
+
const fragmentLocation = backing.packetToFragmentLocation.get(packet);
|
|
227
|
+
if (fragmentLocation) {
|
|
228
|
+
const fragment = fragmentLocation.fragment;
|
|
229
|
+
const sample = fragment.trackData.get(internalTrack.id)?.samples[fragmentLocation.sampleIndex];
|
|
230
|
+
if (!sample?.encryption) return null;
|
|
231
|
+
return {
|
|
232
|
+
data: await readSampleBytes(internalTrack, sample.byteOffset, sample.byteSize),
|
|
233
|
+
sampleEncryption: sample.encryption,
|
|
234
|
+
fragment
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const sampleIndex = backing.packetToSampleIndex.get(packet);
|
|
238
|
+
if (sampleIndex === void 0) return null;
|
|
239
|
+
const sampleInfo = getSampleInfo(internalTrack.demuxer.getSampleTableForTrack(internalTrack), sampleIndex);
|
|
240
|
+
if (!sampleInfo) return null;
|
|
241
|
+
const auxInfo = internalTrack.encryptionAuxInfo;
|
|
242
|
+
if (!auxInfo) return null;
|
|
243
|
+
const sampleEncryption = (await resolveEncryptionAuxInfo(internalTrack, auxInfo))[sampleIndex];
|
|
244
|
+
if (!sampleEncryption) return null;
|
|
245
|
+
return {
|
|
246
|
+
data: await readSampleBytes(internalTrack, sampleInfo.sampleOffset, sampleInfo.sampleSize),
|
|
247
|
+
sampleEncryption,
|
|
248
|
+
fragment: null
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
const getPsshBoxes = (backing, keyId, fragment) => {
|
|
252
|
+
const { psshBoxes } = backing.internalTrack.demuxer;
|
|
253
|
+
if (!fragment) return psshBoxes;
|
|
254
|
+
const boxes = [...psshBoxes, ...fragment.psshBoxes].filter((psshBox) => psshBox.keyIds === null || psshBox.keyIds.includes(keyId));
|
|
255
|
+
for (let i = 0; i < boxes.length - 1; i++) for (let j = i + 1; j < boxes.length; j++) if (psshBoxesAreEqual(boxes[i], boxes[j])) {
|
|
256
|
+
boxes.splice(j, 1);
|
|
257
|
+
j--;
|
|
258
|
+
}
|
|
259
|
+
return boxes;
|
|
260
|
+
};
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/isobmff-decrypt-sample-patcher.ts
|
|
263
|
+
const createMetadataOnlyOptions = (options) => ({
|
|
264
|
+
...options,
|
|
265
|
+
metadataOnly: true,
|
|
266
|
+
verifyKeyPackets: false
|
|
267
|
+
});
|
|
268
|
+
var IsobmffDecryptSamplePatcher = class {
|
|
269
|
+
input;
|
|
270
|
+
decryptSample;
|
|
271
|
+
decryptProtectedData;
|
|
272
|
+
reportEncryptionInfo;
|
|
273
|
+
patchableTrackBackings = /* @__PURE__ */ new WeakSet();
|
|
274
|
+
patchTrackBackingsPromise = null;
|
|
275
|
+
constructor(input, decryptSample, decryptProtectedData, reportEncryptionInfo) {
|
|
276
|
+
this.input = input;
|
|
277
|
+
this.decryptSample = decryptSample;
|
|
278
|
+
this.decryptProtectedData = decryptProtectedData;
|
|
279
|
+
this.reportEncryptionInfo = reportEncryptionInfo;
|
|
280
|
+
}
|
|
281
|
+
async ensurePatched() {
|
|
282
|
+
this.patchTrackBackingsPromise ??= (async () => {
|
|
283
|
+
const tracks = await this.input.getTracks();
|
|
284
|
+
for (const track of tracks) {
|
|
285
|
+
const backing = track._backing;
|
|
286
|
+
if (!isIsobmffTrackBacking(backing) || this.patchableTrackBackings.has(backing)) continue;
|
|
287
|
+
this.patchableTrackBackings.add(backing);
|
|
288
|
+
this.patchTrackBacking(backing);
|
|
289
|
+
}
|
|
290
|
+
})();
|
|
291
|
+
await this.patchTrackBackingsPromise;
|
|
292
|
+
}
|
|
293
|
+
patchTrackBacking(backing) {
|
|
294
|
+
const packetToMetadataPacket = /* @__PURE__ */ new WeakMap();
|
|
295
|
+
const getOriginalPacket = (packet) => packetToMetadataPacket.get(packet) ?? packet;
|
|
296
|
+
const getFirstPacket = backing.getFirstPacket.bind(backing);
|
|
297
|
+
backing.getFirstPacket = async (options) => {
|
|
298
|
+
if (!this.shouldUseCustomDecryptSample(backing, options)) return getFirstPacket(options);
|
|
299
|
+
const metadataPacket = await getFirstPacket(createMetadataOnlyOptions(options));
|
|
300
|
+
return this.materializePacket(backing, metadataPacket, () => getFirstPacket(options), packetToMetadataPacket);
|
|
301
|
+
};
|
|
302
|
+
const getPacket = backing.getPacket.bind(backing);
|
|
303
|
+
backing.getPacket = async (timestamp, options) => {
|
|
304
|
+
if (!this.shouldUseCustomDecryptSample(backing, options)) return getPacket(timestamp, options);
|
|
305
|
+
const metadataPacket = await getPacket(timestamp, createMetadataOnlyOptions(options));
|
|
306
|
+
return this.materializePacket(backing, metadataPacket, () => getPacket(timestamp, options), packetToMetadataPacket);
|
|
307
|
+
};
|
|
308
|
+
const getNextPacket = backing.getNextPacket.bind(backing);
|
|
309
|
+
backing.getNextPacket = async (packet, options) => {
|
|
310
|
+
const originalPacket = getOriginalPacket(packet);
|
|
311
|
+
if (!this.shouldUseCustomDecryptSample(backing, options)) return getNextPacket(originalPacket, options);
|
|
312
|
+
const metadataPacket = await getNextPacket(originalPacket, createMetadataOnlyOptions(options));
|
|
313
|
+
return this.materializePacket(backing, metadataPacket, () => getNextPacket(originalPacket, options), packetToMetadataPacket);
|
|
314
|
+
};
|
|
315
|
+
const getKeyPacket = backing.getKeyPacket.bind(backing);
|
|
316
|
+
backing.getKeyPacket = async (timestamp, options) => {
|
|
317
|
+
if (!this.shouldUseCustomDecryptSample(backing, options)) return getKeyPacket(timestamp, options);
|
|
318
|
+
const metadataPacket = await getKeyPacket(timestamp, createMetadataOnlyOptions(options));
|
|
319
|
+
return this.materializePacket(backing, metadataPacket, () => getKeyPacket(timestamp, options), packetToMetadataPacket);
|
|
320
|
+
};
|
|
321
|
+
const getNextKeyPacket = backing.getNextKeyPacket.bind(backing);
|
|
322
|
+
backing.getNextKeyPacket = async (packet, options) => {
|
|
323
|
+
const originalPacket = getOriginalPacket(packet);
|
|
324
|
+
if (!this.shouldUseCustomDecryptSample(backing, options)) return getNextKeyPacket(originalPacket, options);
|
|
325
|
+
const metadataPacket = await getNextKeyPacket(originalPacket, createMetadataOnlyOptions(options));
|
|
326
|
+
return this.materializePacket(backing, metadataPacket, () => getNextKeyPacket(originalPacket, options), packetToMetadataPacket);
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
shouldUseCustomDecryptSample(backing, options) {
|
|
330
|
+
return Boolean(!options.metadataOnly && getTrackEncryptionInfo(backing)?.defaultIsProtected !== false);
|
|
331
|
+
}
|
|
332
|
+
async materializePacket(backing, metadataPacket, fallback, packetToMetadataPacket) {
|
|
333
|
+
if (!metadataPacket) return null;
|
|
334
|
+
const decryptedPacket = await this.decryptPacket(backing, metadataPacket, fallback);
|
|
335
|
+
if (decryptedPacket && decryptedPacket !== metadataPacket) packetToMetadataPacket.set(decryptedPacket, metadataPacket);
|
|
336
|
+
return decryptedPacket;
|
|
337
|
+
}
|
|
338
|
+
async decryptPacket(backing, metadataPacket, fallback) {
|
|
339
|
+
const encryptionInfo = getTrackEncryptionInfo(backing);
|
|
340
|
+
if (!encryptionInfo?.defaultIsProtected) return fallback();
|
|
341
|
+
const keyId = getKeyId(backing);
|
|
342
|
+
if (!keyId) throw new Error("Encrypted sample encountered without a key ID.");
|
|
343
|
+
const encryptedSample = await getEncryptedSample(backing, metadataPacket);
|
|
344
|
+
if (!encryptedSample) return fallback();
|
|
345
|
+
const psshBoxes = getPsshBoxes(backing, keyId, encryptedSample.fragment);
|
|
346
|
+
this.reportEncryptionInfo(keyId, psshBoxes);
|
|
347
|
+
const decryptedData = this.decryptSample ? await this.decryptSample({
|
|
348
|
+
data: encryptedSample.data,
|
|
349
|
+
keyId,
|
|
350
|
+
psshBoxes,
|
|
351
|
+
scheme: encryptionInfo.scheme,
|
|
352
|
+
iv: encryptedSample.sampleEncryption.iv.slice(),
|
|
353
|
+
timestamp: metadataPacket.microsecondTimestamp,
|
|
354
|
+
subsamples: cloneSubsamples(encryptedSample.sampleEncryption.subsamples),
|
|
355
|
+
pattern: getPattern(encryptionInfo)
|
|
356
|
+
}) : await this.decryptWithProtectedData(encryptionInfo, encryptedSample, metadataPacket.microsecondTimestamp, keyId, psshBoxes);
|
|
357
|
+
if (!isUint8Array(decryptedData)) throw new TypeError("decryption callback must return a Uint8Array.");
|
|
358
|
+
if (decryptedData.byteLength !== metadataPacket.byteLength) throw new Error("decryption callback must return the same number of bytes as the encrypted sample.");
|
|
359
|
+
return new EncodedPacket(decryptedData, metadataPacket.type, metadataPacket.timestamp, metadataPacket.duration, metadataPacket.sequenceNumber, metadataPacket.byteLength, metadataPacket.sideData);
|
|
360
|
+
}
|
|
361
|
+
async decryptWithProtectedData(encryptionInfo, encryptedSample, timestamp, keyId, psshBoxes) {
|
|
362
|
+
if (!this.decryptProtectedData) throw new Error("Encrypted sample encountered without a decryption callback.");
|
|
363
|
+
const protectedDataLayout = createProtectedDataLayout(encryptedSample.data, encryptionInfo, encryptedSample.sampleEncryption);
|
|
364
|
+
if (!protectedDataLayout) throw new Error("decryptProtectedData does not support cbcs samples with subsample encryption. Use decryptSample for this content.");
|
|
365
|
+
const decryptedProtectedData = await this.decryptProtectedData({
|
|
366
|
+
data: protectedDataLayout.data,
|
|
367
|
+
keyId,
|
|
368
|
+
psshBoxes,
|
|
369
|
+
scheme: encryptionInfo.scheme,
|
|
370
|
+
iv: encryptedSample.sampleEncryption.iv.slice(),
|
|
371
|
+
timestamp,
|
|
372
|
+
subsamples: [{
|
|
373
|
+
clearLen: 0,
|
|
374
|
+
protectedLen: protectedDataLayout.data.byteLength
|
|
375
|
+
}],
|
|
376
|
+
pattern: null
|
|
377
|
+
});
|
|
378
|
+
if (!isUint8Array(decryptedProtectedData)) throw new TypeError("decryptProtectedData must return a Uint8Array.");
|
|
379
|
+
if (decryptedProtectedData.byteLength !== protectedDataLayout.data.byteLength) throw new Error("decryptProtectedData must return the same number of bytes as the protected data input.");
|
|
380
|
+
return protectedDataLayout.merge(decryptedProtectedData);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
//#endregion
|
|
2
384
|
//#region src/index.ts
|
|
385
|
+
const createEncryptionReporter = (handleEncryptionInfo) => {
|
|
386
|
+
const reportedEncryptionInfo = /* @__PURE__ */ new Set();
|
|
387
|
+
return (keyId, psshBoxes) => {
|
|
388
|
+
if (!handleEncryptionInfo || reportedEncryptionInfo.has(keyId)) return;
|
|
389
|
+
reportedEncryptionInfo.add(keyId);
|
|
390
|
+
handleEncryptionInfo({
|
|
391
|
+
keyId,
|
|
392
|
+
psshBoxes
|
|
393
|
+
});
|
|
394
|
+
};
|
|
395
|
+
};
|
|
3
396
|
var Input = class extends Input$1 {
|
|
4
|
-
|
|
397
|
+
decryptSamplePatcher;
|
|
398
|
+
constructor({ keys, decryptSample, decryptProtectedData, handleEncryptionInfo, ...options }) {
|
|
399
|
+
if (!keys?.size && !decryptSample && !decryptProtectedData) throw new Error("Either keys, decryptSample, or decryptProtectedData must be provided.");
|
|
400
|
+
const reportEncryptionInfo = createEncryptionReporter(handleEncryptionInfo);
|
|
5
401
|
super({
|
|
6
402
|
...options,
|
|
7
403
|
formats: ALL_FORMATS,
|
|
8
404
|
formatOptions: { isobmff: { resolveKeyId: ({ keyId, psshBoxes }) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
psshBoxes
|
|
12
|
-
});
|
|
13
|
-
const key = keys.get(keyId);
|
|
405
|
+
reportEncryptionInfo(keyId, psshBoxes);
|
|
406
|
+
const key = keys?.get(keyId);
|
|
14
407
|
if (!key) throw new Error("Unknown key ID.");
|
|
15
408
|
return key;
|
|
16
409
|
} } }
|
|
17
410
|
});
|
|
411
|
+
this.decryptSamplePatcher = decryptSample || decryptProtectedData ? new IsobmffDecryptSamplePatcher({ getTracks: (query) => this.getTracksWithoutPatching(query) }, decryptSample, decryptProtectedData, reportEncryptionInfo) : null;
|
|
412
|
+
}
|
|
413
|
+
async getTracks(query) {
|
|
414
|
+
await this.decryptSamplePatcher?.ensurePatched();
|
|
415
|
+
return super.getTracks(query);
|
|
416
|
+
}
|
|
417
|
+
async getTracksWithoutPatching(query) {
|
|
418
|
+
return super.getTracks(query);
|
|
18
419
|
}
|
|
19
420
|
};
|
|
20
421
|
var Output = class extends Output$1 {
|