@zenvor/hls.js 1.0.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/LICENSE +28 -0
- package/README.md +472 -0
- package/dist/hls-demo.js +26995 -0
- package/dist/hls-demo.js.map +1 -0
- package/dist/hls.d.mts +4204 -0
- package/dist/hls.d.ts +4204 -0
- package/dist/hls.js +40050 -0
- package/dist/hls.js.d.ts +4204 -0
- package/dist/hls.js.map +1 -0
- package/dist/hls.light.js +27145 -0
- package/dist/hls.light.js.map +1 -0
- package/dist/hls.light.min.js +2 -0
- package/dist/hls.light.min.js.map +1 -0
- package/dist/hls.light.mjs +26392 -0
- package/dist/hls.light.mjs.map +1 -0
- package/dist/hls.min.js +2 -0
- package/dist/hls.min.js.map +1 -0
- package/dist/hls.mjs +38956 -0
- package/dist/hls.mjs.map +1 -0
- package/dist/hls.worker.js +2 -0
- package/dist/hls.worker.js.map +1 -0
- package/package.json +143 -0
- package/src/config.ts +794 -0
- package/src/controller/abr-controller.ts +1019 -0
- package/src/controller/algo-data-controller.ts +794 -0
- package/src/controller/audio-stream-controller.ts +1099 -0
- package/src/controller/audio-track-controller.ts +454 -0
- package/src/controller/base-playlist-controller.ts +438 -0
- package/src/controller/base-stream-controller.ts +2526 -0
- package/src/controller/buffer-controller.ts +2015 -0
- package/src/controller/buffer-operation-queue.ts +159 -0
- package/src/controller/cap-level-controller.ts +367 -0
- package/src/controller/cmcd-controller.ts +422 -0
- package/src/controller/content-steering-controller.ts +622 -0
- package/src/controller/eme-controller.ts +1617 -0
- package/src/controller/error-controller.ts +627 -0
- package/src/controller/fps-controller.ts +146 -0
- package/src/controller/fragment-finders.ts +256 -0
- package/src/controller/fragment-tracker.ts +567 -0
- package/src/controller/gap-controller.ts +719 -0
- package/src/controller/id3-track-controller.ts +488 -0
- package/src/controller/interstitial-player.ts +302 -0
- package/src/controller/interstitials-controller.ts +2895 -0
- package/src/controller/interstitials-schedule.ts +698 -0
- package/src/controller/latency-controller.ts +294 -0
- package/src/controller/level-controller.ts +776 -0
- package/src/controller/stream-controller.ts +1597 -0
- package/src/controller/subtitle-stream-controller.ts +508 -0
- package/src/controller/subtitle-track-controller.ts +617 -0
- package/src/controller/timeline-controller.ts +677 -0
- package/src/crypt/aes-crypto.ts +36 -0
- package/src/crypt/aes-decryptor.ts +339 -0
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +225 -0
- package/src/crypt/fast-aes-key.ts +39 -0
- package/src/define-plugin.d.ts +17 -0
- package/src/demux/audio/aacdemuxer.ts +126 -0
- package/src/demux/audio/ac3-demuxer.ts +170 -0
- package/src/demux/audio/adts.ts +249 -0
- package/src/demux/audio/base-audio-demuxer.ts +205 -0
- package/src/demux/audio/dolby.ts +21 -0
- package/src/demux/audio/mp3demuxer.ts +85 -0
- package/src/demux/audio/mpegaudio.ts +177 -0
- package/src/demux/chunk-cache.ts +42 -0
- package/src/demux/dummy-demuxed-track.ts +13 -0
- package/src/demux/inject-worker.ts +75 -0
- package/src/demux/mp4demuxer.ts +234 -0
- package/src/demux/sample-aes.ts +198 -0
- package/src/demux/transmuxer-interface.ts +449 -0
- package/src/demux/transmuxer-worker.ts +221 -0
- package/src/demux/transmuxer.ts +560 -0
- package/src/demux/tsdemuxer.ts +1256 -0
- package/src/demux/video/avc-video-parser.ts +401 -0
- package/src/demux/video/base-video-parser.ts +198 -0
- package/src/demux/video/exp-golomb.ts +153 -0
- package/src/demux/video/hevc-video-parser.ts +736 -0
- package/src/empty-es.js +5 -0
- package/src/empty.js +3 -0
- package/src/errors.ts +107 -0
- package/src/events.ts +548 -0
- package/src/exports-default.ts +3 -0
- package/src/exports-named.ts +81 -0
- package/src/hls.ts +1613 -0
- package/src/is-supported.ts +54 -0
- package/src/loader/date-range.ts +207 -0
- package/src/loader/fragment-loader.ts +403 -0
- package/src/loader/fragment.ts +487 -0
- package/src/loader/interstitial-asset-list.ts +162 -0
- package/src/loader/interstitial-event.ts +337 -0
- package/src/loader/key-loader.ts +439 -0
- package/src/loader/level-details.ts +203 -0
- package/src/loader/level-key.ts +259 -0
- package/src/loader/load-stats.ts +17 -0
- package/src/loader/m3u8-parser.ts +1072 -0
- package/src/loader/playlist-loader.ts +839 -0
- package/src/polyfills/number.ts +15 -0
- package/src/remux/aac-helper.ts +81 -0
- package/src/remux/mp4-generator.ts +1380 -0
- package/src/remux/mp4-remuxer.ts +1261 -0
- package/src/remux/passthrough-remuxer.ts +434 -0
- package/src/task-loop.ts +130 -0
- package/src/types/algo.ts +44 -0
- package/src/types/buffer.ts +105 -0
- package/src/types/component-api.ts +20 -0
- package/src/types/demuxer.ts +208 -0
- package/src/types/events.ts +574 -0
- package/src/types/fragment-tracker.ts +23 -0
- package/src/types/level.ts +268 -0
- package/src/types/loader.ts +198 -0
- package/src/types/media-playlist.ts +92 -0
- package/src/types/network-details.ts +3 -0
- package/src/types/remuxer.ts +104 -0
- package/src/types/track.ts +12 -0
- package/src/types/transmuxer.ts +46 -0
- package/src/types/tuples.ts +6 -0
- package/src/types/vtt.ts +11 -0
- package/src/utils/arrays.ts +22 -0
- package/src/utils/attr-list.ts +192 -0
- package/src/utils/binary-search.ts +46 -0
- package/src/utils/buffer-helper.ts +173 -0
- package/src/utils/cea-608-parser.ts +1413 -0
- package/src/utils/chunker.ts +41 -0
- package/src/utils/codecs.ts +314 -0
- package/src/utils/cues.ts +96 -0
- package/src/utils/discontinuities.ts +174 -0
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/error-helper.ts +95 -0
- package/src/utils/event-listener-helper.ts +16 -0
- package/src/utils/ewma-bandwidth-estimator.ts +97 -0
- package/src/utils/ewma.ts +43 -0
- package/src/utils/fetch-loader.ts +331 -0
- package/src/utils/global.ts +2 -0
- package/src/utils/hash.ts +10 -0
- package/src/utils/hdr.ts +67 -0
- package/src/utils/hex.ts +32 -0
- package/src/utils/imsc1-ttml-parser.ts +261 -0
- package/src/utils/keysystem-util.ts +45 -0
- package/src/utils/level-helper.ts +629 -0
- package/src/utils/logger.ts +120 -0
- package/src/utils/media-option-attributes.ts +49 -0
- package/src/utils/mediacapabilities-helper.ts +301 -0
- package/src/utils/mediakeys-helper.ts +210 -0
- package/src/utils/mediasource-helper.ts +37 -0
- package/src/utils/mp4-tools.ts +1473 -0
- package/src/utils/number.ts +3 -0
- package/src/utils/numeric-encoding-utils.ts +26 -0
- package/src/utils/output-filter.ts +46 -0
- package/src/utils/rendition-helper.ts +505 -0
- package/src/utils/safe-json-stringify.ts +22 -0
- package/src/utils/texttrack-utils.ts +164 -0
- package/src/utils/time-ranges.ts +17 -0
- package/src/utils/timescale-conversion.ts +46 -0
- package/src/utils/utf8-utils.ts +18 -0
- package/src/utils/variable-substitution.ts +105 -0
- package/src/utils/vttcue.ts +384 -0
- package/src/utils/vttparser.ts +497 -0
- package/src/utils/webvtt-parser.ts +166 -0
- package/src/utils/xhr-loader.ts +337 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,1473 @@
|
|
|
1
|
+
import { utf8ArrayToStr } from '@svta/common-media-library/utils/utf8ArrayToStr';
|
|
2
|
+
import { arrayToHex } from './hex';
|
|
3
|
+
import { ElementaryStreamTypes } from '../loader/fragment';
|
|
4
|
+
import { logger } from '../utils/logger';
|
|
5
|
+
import type { KeySystemIds } from './mediakeys-helper';
|
|
6
|
+
import type { DecryptData } from '../loader/level-key';
|
|
7
|
+
import type { PassthroughTrack, UserdataSample } from '../types/demuxer';
|
|
8
|
+
import type { ILogger } from '../utils/logger';
|
|
9
|
+
|
|
10
|
+
type BoxDataOrUndefined = Uint8Array<ArrayBuffer> | undefined;
|
|
11
|
+
|
|
12
|
+
const UINT32_MAX = Math.pow(2, 32) - 1;
|
|
13
|
+
const push = [].push;
|
|
14
|
+
|
|
15
|
+
// We are using fixed track IDs for driving the MP4 remuxer
|
|
16
|
+
// instead of following the TS PIDs.
|
|
17
|
+
// There is no reason not to do this and some browsers/SourceBuffer-demuxers
|
|
18
|
+
// may not like if there are TrackID "switches"
|
|
19
|
+
// See https://github.com/video-dev/hls.js/issues/1331
|
|
20
|
+
// Here we are mapping our internal track types to constant MP4 track IDs
|
|
21
|
+
// With MSE currently one can only have one track of each, and we are muxing
|
|
22
|
+
// whatever video/audio rendition in them.
|
|
23
|
+
export const RemuxerTrackIdConfig = {
|
|
24
|
+
video: 1,
|
|
25
|
+
audio: 2,
|
|
26
|
+
id3: 3,
|
|
27
|
+
text: 4,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function bin2str(data: Uint8Array): string {
|
|
31
|
+
return String.fromCharCode.apply(null, data);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readUint16(buffer: Uint8Array, offset: number): number {
|
|
35
|
+
const val = (buffer[offset] << 8) | buffer[offset + 1];
|
|
36
|
+
return val < 0 ? 65536 + val : val;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readUint32(buffer: Uint8Array, offset: number): number {
|
|
40
|
+
const val = readSint32(buffer, offset);
|
|
41
|
+
return val < 0 ? 4294967296 + val : val;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function readUint64(buffer: Uint8Array, offset: number) {
|
|
45
|
+
let result = readUint32(buffer, offset);
|
|
46
|
+
result *= Math.pow(2, 32);
|
|
47
|
+
result += readUint32(buffer, offset + 4);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function readSint32(buffer: Uint8Array, offset: number): number {
|
|
52
|
+
return (
|
|
53
|
+
(buffer[offset] << 24) |
|
|
54
|
+
(buffer[offset + 1] << 16) |
|
|
55
|
+
(buffer[offset + 2] << 8) |
|
|
56
|
+
buffer[offset + 3]
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function writeUint32(buffer: Uint8Array, offset: number, value: number) {
|
|
61
|
+
buffer[offset] = value >> 24;
|
|
62
|
+
buffer[offset + 1] = (value >> 16) & 0xff;
|
|
63
|
+
buffer[offset + 2] = (value >> 8) & 0xff;
|
|
64
|
+
buffer[offset + 3] = value & 0xff;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Find "moof" box
|
|
68
|
+
export function hasMoofData(data: Uint8Array): boolean {
|
|
69
|
+
const end = data.byteLength;
|
|
70
|
+
for (let i = 0; i < end; ) {
|
|
71
|
+
const size = readUint32(data, i);
|
|
72
|
+
if (
|
|
73
|
+
size > 8 &&
|
|
74
|
+
data[i + 4] === 0x6d &&
|
|
75
|
+
data[i + 5] === 0x6f &&
|
|
76
|
+
data[i + 6] === 0x6f &&
|
|
77
|
+
data[i + 7] === 0x66
|
|
78
|
+
) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
i = size > 1 ? i + size : end;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Find the data for a box specified by its path
|
|
87
|
+
export function findBox(data: Uint8Array, path: string[]): Uint8Array[] {
|
|
88
|
+
const results = [] as Uint8Array[];
|
|
89
|
+
if (!path.length) {
|
|
90
|
+
// short-circuit the search for empty paths
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
const end = data.byteLength;
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < end; ) {
|
|
96
|
+
const size = readUint32(data, i);
|
|
97
|
+
const type = bin2str(data.subarray(i + 4, i + 8));
|
|
98
|
+
const endbox = size > 1 ? i + size : end;
|
|
99
|
+
if (type === path[0]) {
|
|
100
|
+
if (path.length === 1) {
|
|
101
|
+
// this is the end of the path and we've found the box we were
|
|
102
|
+
// looking for
|
|
103
|
+
results.push(data.subarray(i + 8, endbox));
|
|
104
|
+
} else {
|
|
105
|
+
// recursively search for the next box along the path
|
|
106
|
+
const subresults = findBox(data.subarray(i + 8, endbox), path.slice(1));
|
|
107
|
+
if (subresults.length) {
|
|
108
|
+
push.apply(results, subresults);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
i = endbox;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// we've finished searching all of data
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type SidxInfo = {
|
|
120
|
+
earliestPresentationTime: number;
|
|
121
|
+
timescale: number;
|
|
122
|
+
version: number;
|
|
123
|
+
referencesCount: number;
|
|
124
|
+
references: any[];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function parseSegmentIndex(sidx: Uint8Array): SidxInfo | null {
|
|
128
|
+
const references: any[] = [];
|
|
129
|
+
|
|
130
|
+
const version = sidx[0];
|
|
131
|
+
|
|
132
|
+
// set initial offset, we skip the reference ID (not needed)
|
|
133
|
+
let index = 8;
|
|
134
|
+
|
|
135
|
+
const timescale = readUint32(sidx, index);
|
|
136
|
+
index += 4;
|
|
137
|
+
|
|
138
|
+
let earliestPresentationTime = 0;
|
|
139
|
+
let firstOffset = 0;
|
|
140
|
+
|
|
141
|
+
if (version === 0) {
|
|
142
|
+
earliestPresentationTime = readUint32(sidx, index);
|
|
143
|
+
firstOffset = readUint32(sidx, index + 4);
|
|
144
|
+
index += 8;
|
|
145
|
+
} else {
|
|
146
|
+
earliestPresentationTime = readUint64(sidx, index);
|
|
147
|
+
firstOffset = readUint64(sidx, index + 8);
|
|
148
|
+
index += 16;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// skip reserved
|
|
152
|
+
index += 2;
|
|
153
|
+
|
|
154
|
+
let startByte = sidx.length + firstOffset;
|
|
155
|
+
|
|
156
|
+
const referencesCount = readUint16(sidx, index);
|
|
157
|
+
index += 2;
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < referencesCount; i++) {
|
|
160
|
+
let referenceIndex = index;
|
|
161
|
+
|
|
162
|
+
const referenceInfo = readUint32(sidx, referenceIndex);
|
|
163
|
+
referenceIndex += 4;
|
|
164
|
+
|
|
165
|
+
const referenceSize = referenceInfo & 0x7fffffff;
|
|
166
|
+
const referenceType = (referenceInfo & 0x80000000) >>> 31;
|
|
167
|
+
|
|
168
|
+
if (referenceType === 1) {
|
|
169
|
+
logger.warn('SIDX has hierarchical references (not supported)');
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const subsegmentDuration = readUint32(sidx, referenceIndex);
|
|
174
|
+
referenceIndex += 4;
|
|
175
|
+
|
|
176
|
+
references.push({
|
|
177
|
+
referenceSize,
|
|
178
|
+
subsegmentDuration, // unscaled
|
|
179
|
+
info: {
|
|
180
|
+
duration: subsegmentDuration / timescale,
|
|
181
|
+
start: startByte,
|
|
182
|
+
end: startByte + referenceSize - 1,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
startByte += referenceSize;
|
|
187
|
+
|
|
188
|
+
// Skipping 1 bit for |startsWithSap|, 3 bits for |sapType|, and 28 bits
|
|
189
|
+
// for |sapDelta|.
|
|
190
|
+
referenceIndex += 4;
|
|
191
|
+
|
|
192
|
+
// skip to next ref
|
|
193
|
+
index = referenceIndex;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
earliestPresentationTime,
|
|
198
|
+
timescale,
|
|
199
|
+
version,
|
|
200
|
+
referencesCount,
|
|
201
|
+
references,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Parses an MP4 initialization segment and extracts stream type and
|
|
207
|
+
* timescale values for any declared tracks. Timescale values indicate the
|
|
208
|
+
* number of clock ticks per second to assume for time-based values
|
|
209
|
+
* elsewhere in the MP4.
|
|
210
|
+
*
|
|
211
|
+
* To determine the start time of an MP4, you need two pieces of
|
|
212
|
+
* information: the timescale unit and the earliest base media decode
|
|
213
|
+
* time. Multiple timescales can be specified within an MP4 but the
|
|
214
|
+
* base media decode time is always expressed in the timescale from
|
|
215
|
+
* the media header box for the track:
|
|
216
|
+
* ```
|
|
217
|
+
* moov > trak > mdia > mdhd.timescale
|
|
218
|
+
* moov > trak > mdia > hdlr
|
|
219
|
+
* ```
|
|
220
|
+
* @param initSegment the bytes of the init segment
|
|
221
|
+
* @returns a hash of track type to timescale values or null if
|
|
222
|
+
* the init segment is malformed.
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
export interface InitDataTrack {
|
|
226
|
+
timescale: number;
|
|
227
|
+
id: number;
|
|
228
|
+
codec: string;
|
|
229
|
+
encrypted: boolean;
|
|
230
|
+
supplemental: string | undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
type HdlrMetadata = 'meta';
|
|
234
|
+
type HdlrType =
|
|
235
|
+
| ElementaryStreamTypes.AUDIO
|
|
236
|
+
| ElementaryStreamTypes.VIDEO
|
|
237
|
+
| HdlrMetadata;
|
|
238
|
+
|
|
239
|
+
type StsdData = {
|
|
240
|
+
codec: string;
|
|
241
|
+
encrypted: boolean;
|
|
242
|
+
supplemental: string | undefined;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export interface InitData extends Array<any> {
|
|
246
|
+
[index: number]:
|
|
247
|
+
| {
|
|
248
|
+
timescale: number;
|
|
249
|
+
type: HdlrType;
|
|
250
|
+
stsd: StsdData;
|
|
251
|
+
default?: {
|
|
252
|
+
duration: number;
|
|
253
|
+
flags: number;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
| undefined;
|
|
257
|
+
audio?: InitDataTrack;
|
|
258
|
+
video?: InitDataTrack;
|
|
259
|
+
caption?: InitDataTrack;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function parseInitSegment(initSegment: Uint8Array): InitData {
|
|
263
|
+
const result: InitData = [];
|
|
264
|
+
const traks = findBox(initSegment, ['moov', 'trak']);
|
|
265
|
+
for (let i = 0; i < traks.length; i++) {
|
|
266
|
+
const trak = traks[i];
|
|
267
|
+
const tkhd = findBox(trak, ['tkhd'])[0];
|
|
268
|
+
if (tkhd as any) {
|
|
269
|
+
let version = tkhd[0];
|
|
270
|
+
const trackId = readUint32(tkhd, version === 0 ? 12 : 20);
|
|
271
|
+
const mdhd = findBox(trak, ['mdia', 'mdhd'])[0];
|
|
272
|
+
if (mdhd as any) {
|
|
273
|
+
version = mdhd[0];
|
|
274
|
+
const timescale = readUint32(mdhd, version === 0 ? 12 : 20);
|
|
275
|
+
const hdlr = findBox(trak, ['mdia', 'hdlr'])[0];
|
|
276
|
+
if (hdlr as any) {
|
|
277
|
+
const hdlrType = bin2str(hdlr.subarray(8, 12));
|
|
278
|
+
const type: HdlrType | undefined = {
|
|
279
|
+
soun: ElementaryStreamTypes.AUDIO as const,
|
|
280
|
+
vide: ElementaryStreamTypes.VIDEO as const,
|
|
281
|
+
}[hdlrType];
|
|
282
|
+
// Parse codec details
|
|
283
|
+
const stsdBox = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0];
|
|
284
|
+
const stsd = parseStsd(stsdBox);
|
|
285
|
+
if (type) {
|
|
286
|
+
// Add 'audio', 'video', and 'audiovideo' track records that will map to SourceBuffers
|
|
287
|
+
result[trackId] = { timescale, type, stsd };
|
|
288
|
+
result[type] = { timescale, id: trackId, ...stsd };
|
|
289
|
+
} else {
|
|
290
|
+
// Add 'meta' and other track records
|
|
291
|
+
result[trackId] = {
|
|
292
|
+
timescale,
|
|
293
|
+
type: hdlrType as HdlrType,
|
|
294
|
+
stsd,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const trex = findBox(initSegment, ['moov', 'mvex', 'trex']);
|
|
303
|
+
trex.forEach((trex) => {
|
|
304
|
+
const trackId = readUint32(trex, 4);
|
|
305
|
+
const track = result[trackId];
|
|
306
|
+
if (track) {
|
|
307
|
+
track.default = {
|
|
308
|
+
duration: readUint32(trex, 12),
|
|
309
|
+
flags: readUint32(trex, 20),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function parseStsd(stsd: Uint8Array): StsdData {
|
|
318
|
+
const sampleEntries = stsd.subarray(8);
|
|
319
|
+
const sampleEntriesEnd = sampleEntries.subarray(8 + 78);
|
|
320
|
+
const fourCC = bin2str(sampleEntries.subarray(4, 8));
|
|
321
|
+
let codec = fourCC;
|
|
322
|
+
let supplemental;
|
|
323
|
+
const encrypted = fourCC === 'enca' || fourCC === 'encv';
|
|
324
|
+
if (encrypted) {
|
|
325
|
+
const encBox = findBox(sampleEntries, [fourCC])[0];
|
|
326
|
+
const encBoxChildren = encBox.subarray(fourCC === 'enca' ? 28 : 78);
|
|
327
|
+
const sinfs = findBox(encBoxChildren, ['sinf']);
|
|
328
|
+
sinfs.forEach((sinf) => {
|
|
329
|
+
const schm = findBox(sinf, ['schm'])[0];
|
|
330
|
+
if (schm as any) {
|
|
331
|
+
const scheme = bin2str(schm.subarray(4, 8));
|
|
332
|
+
if (scheme === 'cbcs' || scheme === 'cenc') {
|
|
333
|
+
const frma = findBox(sinf, ['frma'])[0];
|
|
334
|
+
if (frma as any) {
|
|
335
|
+
// for encrypted content codec fourCC will be in frma
|
|
336
|
+
codec = bin2str(frma);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const codecFourCC = codec;
|
|
343
|
+
switch (codec) {
|
|
344
|
+
case 'avc1':
|
|
345
|
+
case 'avc2':
|
|
346
|
+
case 'avc3':
|
|
347
|
+
case 'avc4': {
|
|
348
|
+
// extract profile + compatibility + level out of avcC box
|
|
349
|
+
const avcCBox = findBox(sampleEntriesEnd, ['avcC'])[0];
|
|
350
|
+
if ((avcCBox as any) && avcCBox.length > 3) {
|
|
351
|
+
codec +=
|
|
352
|
+
'.' + toHex(avcCBox[1]) + toHex(avcCBox[2]) + toHex(avcCBox[3]);
|
|
353
|
+
supplemental = parseSupplementalDoViCodec(
|
|
354
|
+
codecFourCC === 'avc1' ? 'dva1' : 'dvav',
|
|
355
|
+
sampleEntriesEnd,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case 'mp4a': {
|
|
361
|
+
const codecBox = findBox(sampleEntries, [fourCC])[0];
|
|
362
|
+
const esdsBox = findBox(codecBox.subarray(28), ['esds'])[0];
|
|
363
|
+
if ((esdsBox as any) && esdsBox.length > 7) {
|
|
364
|
+
let i = 4;
|
|
365
|
+
// ES Descriptor tag
|
|
366
|
+
if (esdsBox[i++] !== 0x03) {
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
i = skipBERInteger(esdsBox, i);
|
|
370
|
+
i += 2; // skip es_id;
|
|
371
|
+
const flags = esdsBox[i++];
|
|
372
|
+
if (flags & 0x80) {
|
|
373
|
+
i += 2; // skip dependency es_id
|
|
374
|
+
}
|
|
375
|
+
if (flags & 0x40) {
|
|
376
|
+
i += esdsBox[i++]; // skip URL
|
|
377
|
+
}
|
|
378
|
+
// Decoder config descriptor
|
|
379
|
+
if (esdsBox[i++] !== 0x04) {
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
i = skipBERInteger(esdsBox, i);
|
|
383
|
+
const objectType = esdsBox[i++];
|
|
384
|
+
if (objectType === 0x40) {
|
|
385
|
+
codec += '.' + toHex(objectType);
|
|
386
|
+
} else {
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
i += 12;
|
|
390
|
+
// Decoder specific info
|
|
391
|
+
if (esdsBox[i++] !== 0x05) {
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
i = skipBERInteger(esdsBox, i);
|
|
395
|
+
const firstByte = esdsBox[i++];
|
|
396
|
+
let audioObjectType = (firstByte & 0xf8) >> 3;
|
|
397
|
+
if (audioObjectType === 31) {
|
|
398
|
+
audioObjectType +=
|
|
399
|
+
1 + ((firstByte & 0x7) << 3) + ((esdsBox[i] & 0xe0) >> 5);
|
|
400
|
+
}
|
|
401
|
+
codec += '.' + audioObjectType;
|
|
402
|
+
}
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case 'hvc1':
|
|
406
|
+
case 'hev1': {
|
|
407
|
+
const hvcCBox = findBox(sampleEntriesEnd, ['hvcC'])[0];
|
|
408
|
+
if ((hvcCBox as any) && hvcCBox.length > 12) {
|
|
409
|
+
const profileByte = hvcCBox[1];
|
|
410
|
+
const profileSpace = ['', 'A', 'B', 'C'][profileByte >> 6];
|
|
411
|
+
const generalProfileIdc = profileByte & 0x1f;
|
|
412
|
+
const profileCompat = readUint32(hvcCBox, 2);
|
|
413
|
+
const tierFlag = (profileByte & 0x20) >> 5 ? 'H' : 'L';
|
|
414
|
+
const levelIDC = hvcCBox[12];
|
|
415
|
+
const constraintIndicator = hvcCBox.subarray(6, 12);
|
|
416
|
+
codec += '.' + profileSpace + generalProfileIdc;
|
|
417
|
+
codec +=
|
|
418
|
+
'.' + reverse32BitInt(profileCompat).toString(16).toUpperCase();
|
|
419
|
+
codec += '.' + tierFlag + levelIDC;
|
|
420
|
+
let constraintString = '';
|
|
421
|
+
for (let i = constraintIndicator.length; i--; ) {
|
|
422
|
+
const byte = constraintIndicator[i];
|
|
423
|
+
if (byte || constraintString) {
|
|
424
|
+
const encodedByte = byte.toString(16).toUpperCase();
|
|
425
|
+
constraintString = '.' + encodedByte + constraintString;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
codec += constraintString;
|
|
429
|
+
}
|
|
430
|
+
supplemental = parseSupplementalDoViCodec(
|
|
431
|
+
codecFourCC == 'hev1' ? 'dvhe' : 'dvh1',
|
|
432
|
+
sampleEntriesEnd,
|
|
433
|
+
);
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
case 'dvh1':
|
|
437
|
+
case 'dvhe':
|
|
438
|
+
case 'dvav':
|
|
439
|
+
case 'dva1':
|
|
440
|
+
case 'dav1': {
|
|
441
|
+
codec = parseSupplementalDoViCodec(codec, sampleEntriesEnd) || codec;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
case 'vp09': {
|
|
445
|
+
const vpcCBox = findBox(sampleEntriesEnd, ['vpcC'])[0];
|
|
446
|
+
if ((vpcCBox as any) && vpcCBox.length > 6) {
|
|
447
|
+
const profile = vpcCBox[4];
|
|
448
|
+
const level = vpcCBox[5];
|
|
449
|
+
const bitDepth = (vpcCBox[6] >> 4) & 0x0f;
|
|
450
|
+
codec +=
|
|
451
|
+
'.' +
|
|
452
|
+
addLeadingZero(profile) +
|
|
453
|
+
'.' +
|
|
454
|
+
addLeadingZero(level) +
|
|
455
|
+
'.' +
|
|
456
|
+
addLeadingZero(bitDepth);
|
|
457
|
+
}
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
case 'av01': {
|
|
461
|
+
const av1CBox = findBox(sampleEntriesEnd, ['av1C'])[0];
|
|
462
|
+
if ((av1CBox as any) && av1CBox.length > 2) {
|
|
463
|
+
const profile = av1CBox[1] >>> 5;
|
|
464
|
+
const level = av1CBox[1] & 0x1f;
|
|
465
|
+
const tierFlag = av1CBox[2] >>> 7 ? 'H' : 'M';
|
|
466
|
+
const highBitDepth = (av1CBox[2] & 0x40) >> 6;
|
|
467
|
+
const twelveBit = (av1CBox[2] & 0x20) >> 5;
|
|
468
|
+
const bitDepth =
|
|
469
|
+
profile === 2 && highBitDepth
|
|
470
|
+
? twelveBit
|
|
471
|
+
? 12
|
|
472
|
+
: 10
|
|
473
|
+
: highBitDepth
|
|
474
|
+
? 10
|
|
475
|
+
: 8;
|
|
476
|
+
const monochrome = (av1CBox[2] & 0x10) >> 4;
|
|
477
|
+
const chromaSubsamplingX = (av1CBox[2] & 0x08) >> 3;
|
|
478
|
+
const chromaSubsamplingY = (av1CBox[2] & 0x04) >> 2;
|
|
479
|
+
const chromaSamplePosition = av1CBox[2] & 0x03;
|
|
480
|
+
// TODO: parse color_description_present_flag
|
|
481
|
+
// default it to BT.709/limited range for now
|
|
482
|
+
// more info https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox-syntax
|
|
483
|
+
const colorPrimaries = 1;
|
|
484
|
+
const transferCharacteristics = 1;
|
|
485
|
+
const matrixCoefficients = 1;
|
|
486
|
+
const videoFullRangeFlag = 0;
|
|
487
|
+
codec +=
|
|
488
|
+
'.' +
|
|
489
|
+
profile +
|
|
490
|
+
'.' +
|
|
491
|
+
addLeadingZero(level) +
|
|
492
|
+
tierFlag +
|
|
493
|
+
'.' +
|
|
494
|
+
addLeadingZero(bitDepth) +
|
|
495
|
+
'.' +
|
|
496
|
+
monochrome +
|
|
497
|
+
'.' +
|
|
498
|
+
chromaSubsamplingX +
|
|
499
|
+
chromaSubsamplingY +
|
|
500
|
+
chromaSamplePosition +
|
|
501
|
+
'.' +
|
|
502
|
+
addLeadingZero(colorPrimaries) +
|
|
503
|
+
'.' +
|
|
504
|
+
addLeadingZero(transferCharacteristics) +
|
|
505
|
+
'.' +
|
|
506
|
+
addLeadingZero(matrixCoefficients) +
|
|
507
|
+
'.' +
|
|
508
|
+
videoFullRangeFlag;
|
|
509
|
+
supplemental = parseSupplementalDoViCodec('dav1', sampleEntriesEnd);
|
|
510
|
+
}
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
case 'ac-3':
|
|
514
|
+
case 'ec-3':
|
|
515
|
+
case 'alac':
|
|
516
|
+
case 'fLaC':
|
|
517
|
+
case 'Opus':
|
|
518
|
+
default:
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
return { codec, encrypted, supplemental };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function parseSupplementalDoViCodec(
|
|
525
|
+
fourCC: string,
|
|
526
|
+
sampleEntriesEnd: Uint8Array,
|
|
527
|
+
): string | undefined {
|
|
528
|
+
const dvvCResult = findBox(sampleEntriesEnd, ['dvvC']); // used by DoVi Profile 8 to 10
|
|
529
|
+
const dvXCBox = dvvCResult.length
|
|
530
|
+
? dvvCResult[0]
|
|
531
|
+
: findBox(sampleEntriesEnd, ['dvcC'])[0]; // used by DoVi Profiles up to 7 and 20
|
|
532
|
+
if (dvXCBox as any) {
|
|
533
|
+
const doViProfile = (dvXCBox[2] >> 1) & 0x7f;
|
|
534
|
+
const doViLevel = ((dvXCBox[2] << 5) & 0x20) | ((dvXCBox[3] >> 3) & 0x1f);
|
|
535
|
+
return (
|
|
536
|
+
fourCC +
|
|
537
|
+
'.' +
|
|
538
|
+
addLeadingZero(doViProfile) +
|
|
539
|
+
'.' +
|
|
540
|
+
addLeadingZero(doViLevel)
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function reverse32BitInt(val: number) {
|
|
546
|
+
let result = 0;
|
|
547
|
+
for (let i = 0; i < 32; i++) {
|
|
548
|
+
result |= ((val >> i) & 1) << (32 - 1 - i);
|
|
549
|
+
}
|
|
550
|
+
return result >>> 0;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function skipBERInteger(bytes: Uint8Array, i: number): number {
|
|
554
|
+
const limit = i + 5;
|
|
555
|
+
while (bytes[i++] & 0x80 && i < limit) {
|
|
556
|
+
/* do nothing */
|
|
557
|
+
}
|
|
558
|
+
return i;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function toHex(x: number): string {
|
|
562
|
+
return ('0' + x.toString(16).toUpperCase()).slice(-2);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function addLeadingZero(num: number): string {
|
|
566
|
+
return (num < 10 ? '0' : '') + num;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function patchEncyptionData(
|
|
570
|
+
initSegment: Uint8Array<ArrayBuffer> | undefined,
|
|
571
|
+
decryptdata: DecryptData | null,
|
|
572
|
+
) {
|
|
573
|
+
if (!initSegment || !decryptdata) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const keyId = decryptdata.keyId;
|
|
577
|
+
if (keyId && decryptdata.isCommonEncryption) {
|
|
578
|
+
applyToTencBoxes(initSegment, (tenc, isAudio) => {
|
|
579
|
+
// Look for default key id (keyID offset is always 8 within the tenc box):
|
|
580
|
+
const tencKeyId = tenc.subarray(8, 24);
|
|
581
|
+
if (!tencKeyId.some((b) => b !== 0)) {
|
|
582
|
+
logger.log(
|
|
583
|
+
`[eme] Patching keyId in 'enc${
|
|
584
|
+
isAudio ? 'a' : 'v'
|
|
585
|
+
}>sinf>>tenc' box: ${arrayToHex(tencKeyId)} -> ${arrayToHex(keyId)}`,
|
|
586
|
+
);
|
|
587
|
+
tenc.set(keyId, 8);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function parseKeyIdsFromTenc(
|
|
594
|
+
initSegment: Uint8Array<ArrayBuffer>,
|
|
595
|
+
): Uint8Array<ArrayBuffer>[] {
|
|
596
|
+
const keyIds: Uint8Array<ArrayBuffer>[] = [];
|
|
597
|
+
applyToTencBoxes(initSegment, (tenc) => keyIds.push(tenc.subarray(8, 24)));
|
|
598
|
+
return keyIds;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function applyToTencBoxes(
|
|
602
|
+
initSegment: Uint8Array<ArrayBuffer>,
|
|
603
|
+
predicate: (tenc: Uint8Array<ArrayBuffer>, isAudio: boolean) => void,
|
|
604
|
+
) {
|
|
605
|
+
const traks = findBox(initSegment, ['moov', 'trak']);
|
|
606
|
+
traks.forEach((trak) => {
|
|
607
|
+
const stsd = findBox(trak, [
|
|
608
|
+
'mdia',
|
|
609
|
+
'minf',
|
|
610
|
+
'stbl',
|
|
611
|
+
'stsd',
|
|
612
|
+
])[0] as BoxDataOrUndefined;
|
|
613
|
+
if (!stsd) return;
|
|
614
|
+
const sampleEntries = stsd.subarray(8);
|
|
615
|
+
let encBoxes = findBox(sampleEntries, ['enca']);
|
|
616
|
+
const isAudio = encBoxes.length > 0;
|
|
617
|
+
if (!isAudio) {
|
|
618
|
+
encBoxes = findBox(sampleEntries, ['encv']);
|
|
619
|
+
}
|
|
620
|
+
encBoxes.forEach((enc) => {
|
|
621
|
+
const encBoxChildren = isAudio ? enc.subarray(28) : enc.subarray(78);
|
|
622
|
+
const sinfBoxes = findBox(encBoxChildren, ['sinf']);
|
|
623
|
+
sinfBoxes.forEach((sinf) => {
|
|
624
|
+
const tenc = parseSinf(sinf);
|
|
625
|
+
if (tenc) {
|
|
626
|
+
predicate(tenc, isAudio);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export function parseSinf(sinf: Uint8Array): BoxDataOrUndefined {
|
|
634
|
+
const schm = findBox(sinf, ['schm'])[0] as BoxDataOrUndefined;
|
|
635
|
+
if (schm) {
|
|
636
|
+
const scheme = bin2str(schm.subarray(4, 8));
|
|
637
|
+
if (scheme === 'cbcs' || scheme === 'cenc') {
|
|
638
|
+
const tenc = findBox(sinf, ['schi', 'tenc'])[0] as BoxDataOrUndefined;
|
|
639
|
+
if (tenc) {
|
|
640
|
+
return tenc;
|
|
641
|
+
}
|
|
642
|
+
} else if (scheme === 'cbc2') {
|
|
643
|
+
/* no-op */
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/*
|
|
649
|
+
For Reference:
|
|
650
|
+
aligned(8) class TrackFragmentHeaderBox
|
|
651
|
+
extends FullBox(‘tfhd’, 0, tf_flags){
|
|
652
|
+
unsigned int(32) track_ID;
|
|
653
|
+
// all the following are optional fields
|
|
654
|
+
unsigned int(64) base_data_offset;
|
|
655
|
+
unsigned int(32) sample_description_index;
|
|
656
|
+
unsigned int(32) default_sample_duration;
|
|
657
|
+
unsigned int(32) default_sample_size;
|
|
658
|
+
unsigned int(32) default_sample_flags
|
|
659
|
+
}
|
|
660
|
+
*/
|
|
661
|
+
export type TrackTimes = {
|
|
662
|
+
duration: number;
|
|
663
|
+
keyFrameIndex?: number;
|
|
664
|
+
keyFrameStart?: number;
|
|
665
|
+
start: number;
|
|
666
|
+
sampleCount: number;
|
|
667
|
+
timescale: number;
|
|
668
|
+
type: HdlrType;
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
export function getSampleData(
|
|
672
|
+
data: Uint8Array,
|
|
673
|
+
initData: InitData,
|
|
674
|
+
logger: ILogger,
|
|
675
|
+
): Record<number, TrackTimes> {
|
|
676
|
+
const tracks: Record<number, TrackTimes> = {};
|
|
677
|
+
const trafs = findBox(data, ['moof', 'traf']);
|
|
678
|
+
for (let i = 0; i < trafs.length; i++) {
|
|
679
|
+
const traf = trafs[i];
|
|
680
|
+
// There is only one tfhd & trun per traf
|
|
681
|
+
// This is true for CMAF style content, and we should perhaps check the ftyp
|
|
682
|
+
// and only look for a single trun then, but for ISOBMFF we should check
|
|
683
|
+
// for multiple track runs.
|
|
684
|
+
const tfhd = findBox(traf, ['tfhd'])[0];
|
|
685
|
+
// get the track id from the tfhd
|
|
686
|
+
const id = readUint32(tfhd, 4);
|
|
687
|
+
const track = initData[id];
|
|
688
|
+
if (!track) {
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
(tracks[id] as any) ||= {
|
|
692
|
+
start: NaN,
|
|
693
|
+
duration: 0,
|
|
694
|
+
sampleCount: 0,
|
|
695
|
+
timescale: track.timescale,
|
|
696
|
+
type: track.type,
|
|
697
|
+
};
|
|
698
|
+
const trackTimes: TrackTimes = tracks[id];
|
|
699
|
+
// get start DTS
|
|
700
|
+
const tfdt = findBox(traf, ['tfdt'])[0];
|
|
701
|
+
|
|
702
|
+
if (tfdt as any) {
|
|
703
|
+
const version = tfdt[0];
|
|
704
|
+
let baseTime = readUint32(tfdt, 4);
|
|
705
|
+
if (version === 1) {
|
|
706
|
+
// If value is too large, assume signed 64-bit. Negative track fragment decode times are invalid, but they exist in the wild.
|
|
707
|
+
// This prevents large values from being used for initPTS, which can cause playlist sync issues.
|
|
708
|
+
// https://github.com/video-dev/hls.js/issues/5303
|
|
709
|
+
if (baseTime === UINT32_MAX) {
|
|
710
|
+
logger.warn(
|
|
711
|
+
`[mp4-demuxer]: Ignoring assumed invalid signed 64-bit track fragment decode time`,
|
|
712
|
+
);
|
|
713
|
+
} else {
|
|
714
|
+
baseTime *= UINT32_MAX + 1;
|
|
715
|
+
baseTime += readUint32(tfdt, 8);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (
|
|
719
|
+
Number.isFinite(baseTime) &&
|
|
720
|
+
(!Number.isFinite(trackTimes.start) || baseTime < trackTimes.start)
|
|
721
|
+
) {
|
|
722
|
+
trackTimes.start = baseTime;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const trackDefault = track.default;
|
|
727
|
+
const tfhdFlags = readUint32(tfhd, 0) | trackDefault?.flags!;
|
|
728
|
+
let defaultSampleDuration: number = trackDefault?.duration || 0;
|
|
729
|
+
if (tfhdFlags & 0x000008) {
|
|
730
|
+
// 0x000008 indicates the presence of the default_sample_duration field
|
|
731
|
+
if (tfhdFlags & 0x000002) {
|
|
732
|
+
// 0x000002 indicates the presence of the sample_description_index field, which precedes default_sample_duration
|
|
733
|
+
// If present, the default_sample_duration exists at byte offset 12
|
|
734
|
+
defaultSampleDuration = readUint32(tfhd, 12);
|
|
735
|
+
} else {
|
|
736
|
+
// Otherwise, the duration is at byte offset 8
|
|
737
|
+
defaultSampleDuration = readUint32(tfhd, 8);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const truns = findBox(traf, ['trun']);
|
|
741
|
+
let sampleDTS = trackTimes.start || 0;
|
|
742
|
+
let rawDuration = 0;
|
|
743
|
+
let sampleDuration = defaultSampleDuration;
|
|
744
|
+
for (let j = 0; j < truns.length; j++) {
|
|
745
|
+
const trun = truns[j];
|
|
746
|
+
const sampleCount = readUint32(trun, 4);
|
|
747
|
+
const sampleIndex = trackTimes.sampleCount;
|
|
748
|
+
trackTimes.sampleCount += sampleCount;
|
|
749
|
+
// Get duration from samples
|
|
750
|
+
const dataOffsetPresent = trun[3] & 0x01;
|
|
751
|
+
const firstSampleFlagsPresent = trun[3] & 0x04;
|
|
752
|
+
const sampleDurationPresent = trun[2] & 0x01;
|
|
753
|
+
const sampleSizePresent = trun[2] & 0x02;
|
|
754
|
+
const sampleFlagsPresent = trun[2] & 0x04;
|
|
755
|
+
const sampleCompositionTimeOffsetPresent = trun[2] & 0x08;
|
|
756
|
+
let offset = 8;
|
|
757
|
+
let remaining = sampleCount;
|
|
758
|
+
if (dataOffsetPresent) {
|
|
759
|
+
offset += 4;
|
|
760
|
+
}
|
|
761
|
+
if (firstSampleFlagsPresent && sampleCount) {
|
|
762
|
+
const isNonSyncSample = trun[offset + 1] & 0x01;
|
|
763
|
+
if (!isNonSyncSample && trackTimes.keyFrameIndex === undefined) {
|
|
764
|
+
trackTimes.keyFrameIndex = sampleIndex;
|
|
765
|
+
}
|
|
766
|
+
offset += 4;
|
|
767
|
+
if (sampleDurationPresent) {
|
|
768
|
+
sampleDuration = readUint32(trun, offset);
|
|
769
|
+
offset += 4;
|
|
770
|
+
} else {
|
|
771
|
+
sampleDuration = defaultSampleDuration;
|
|
772
|
+
}
|
|
773
|
+
if (sampleSizePresent) {
|
|
774
|
+
offset += 4;
|
|
775
|
+
}
|
|
776
|
+
if (sampleCompositionTimeOffsetPresent) {
|
|
777
|
+
offset += 4;
|
|
778
|
+
}
|
|
779
|
+
sampleDTS += sampleDuration;
|
|
780
|
+
rawDuration += sampleDuration;
|
|
781
|
+
remaining--;
|
|
782
|
+
}
|
|
783
|
+
while (remaining--) {
|
|
784
|
+
if (sampleDurationPresent) {
|
|
785
|
+
sampleDuration = readUint32(trun, offset);
|
|
786
|
+
offset += 4;
|
|
787
|
+
} else {
|
|
788
|
+
sampleDuration = defaultSampleDuration;
|
|
789
|
+
}
|
|
790
|
+
if (sampleSizePresent) {
|
|
791
|
+
offset += 4;
|
|
792
|
+
}
|
|
793
|
+
if (sampleFlagsPresent) {
|
|
794
|
+
const isNonSyncSample = trun[offset + 1] & 0x01;
|
|
795
|
+
if (!isNonSyncSample) {
|
|
796
|
+
if (trackTimes.keyFrameIndex === undefined) {
|
|
797
|
+
trackTimes.keyFrameIndex =
|
|
798
|
+
trackTimes.sampleCount - (remaining + 1);
|
|
799
|
+
trackTimes.keyFrameStart = sampleDTS;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
offset += 4;
|
|
803
|
+
}
|
|
804
|
+
if (sampleCompositionTimeOffsetPresent) {
|
|
805
|
+
offset += 4;
|
|
806
|
+
}
|
|
807
|
+
sampleDTS += sampleDuration;
|
|
808
|
+
rawDuration += sampleDuration;
|
|
809
|
+
}
|
|
810
|
+
if (!rawDuration && defaultSampleDuration) {
|
|
811
|
+
rawDuration += defaultSampleDuration * sampleCount;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
trackTimes.duration += rawDuration;
|
|
815
|
+
}
|
|
816
|
+
if (!Object.keys(tracks).some((trackId) => tracks[trackId].duration)) {
|
|
817
|
+
// If duration samples are not available in the traf use sidx subsegment_duration
|
|
818
|
+
let sidxMinStart = Infinity;
|
|
819
|
+
let sidxMaxEnd = 0;
|
|
820
|
+
const sidxs = findBox(data, ['sidx']);
|
|
821
|
+
for (let i = 0; i < sidxs.length; i++) {
|
|
822
|
+
const sidx = parseSegmentIndex(sidxs[i]);
|
|
823
|
+
if (sidx?.references) {
|
|
824
|
+
sidxMinStart = Math.min(
|
|
825
|
+
sidxMinStart,
|
|
826
|
+
sidx.earliestPresentationTime / sidx.timescale,
|
|
827
|
+
);
|
|
828
|
+
const subSegmentDuration = sidx.references.reduce(
|
|
829
|
+
(dur, ref) => dur + ref.info.duration || 0,
|
|
830
|
+
0,
|
|
831
|
+
);
|
|
832
|
+
sidxMaxEnd = Math.max(
|
|
833
|
+
sidxMaxEnd,
|
|
834
|
+
subSegmentDuration + sidx.earliestPresentationTime / sidx.timescale,
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (sidxMaxEnd && Number.isFinite(sidxMaxEnd)) {
|
|
839
|
+
Object.keys(tracks).forEach((trackId) => {
|
|
840
|
+
if (!tracks[trackId].duration) {
|
|
841
|
+
tracks[trackId].duration =
|
|
842
|
+
sidxMaxEnd * tracks[trackId].timescale - tracks[trackId].start;
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return tracks;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// TODO: Check if the last moof+mdat pair is part of the valid range
|
|
851
|
+
export function segmentValidRange(
|
|
852
|
+
data: Uint8Array<ArrayBuffer>,
|
|
853
|
+
): SegmentedRange {
|
|
854
|
+
const segmentedRange: SegmentedRange = {
|
|
855
|
+
valid: null,
|
|
856
|
+
remainder: null,
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
const moofs = findBox(data, ['moof']);
|
|
860
|
+
if (moofs.length < 2) {
|
|
861
|
+
segmentedRange.remainder = data;
|
|
862
|
+
return segmentedRange;
|
|
863
|
+
}
|
|
864
|
+
const last = moofs[moofs.length - 1];
|
|
865
|
+
// Offset by 8 bytes; findBox offsets the start by as much
|
|
866
|
+
segmentedRange.valid = data.slice(0, last.byteOffset - 8);
|
|
867
|
+
segmentedRange.remainder = data.slice(last.byteOffset - 8);
|
|
868
|
+
return segmentedRange;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
export interface SegmentedRange {
|
|
872
|
+
valid: Uint8Array<ArrayBuffer> | null;
|
|
873
|
+
remainder: Uint8Array<ArrayBuffer> | null;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export function appendUint8Array(data1: Uint8Array, data2: Uint8Array) {
|
|
877
|
+
const temp = new Uint8Array(data1.length + data2.length);
|
|
878
|
+
temp.set(data1);
|
|
879
|
+
temp.set(data2, data1.length);
|
|
880
|
+
return temp;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
export interface IEmsgParsingData {
|
|
884
|
+
schemeIdUri: string;
|
|
885
|
+
value: string;
|
|
886
|
+
timeScale: number;
|
|
887
|
+
presentationTimeDelta?: number;
|
|
888
|
+
presentationTime?: number;
|
|
889
|
+
eventDuration: number;
|
|
890
|
+
id: number;
|
|
891
|
+
payload: Uint8Array;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
export function parseSamples(
|
|
895
|
+
timeOffset: number,
|
|
896
|
+
track: PassthroughTrack,
|
|
897
|
+
): UserdataSample[] {
|
|
898
|
+
const seiSamples = [] as UserdataSample[];
|
|
899
|
+
const videoData = track.samples;
|
|
900
|
+
const timescale = track.timescale;
|
|
901
|
+
const trackId = track.id;
|
|
902
|
+
let isHEVCFlavor = false;
|
|
903
|
+
|
|
904
|
+
const moofs = findBox(videoData, ['moof']);
|
|
905
|
+
moofs.map((moof) => {
|
|
906
|
+
const moofOffset = moof.byteOffset - 8;
|
|
907
|
+
const trafs = findBox(moof, ['traf']);
|
|
908
|
+
trafs.map((traf) => {
|
|
909
|
+
// get the base media decode time from the tfdt
|
|
910
|
+
const baseTime = findBox(traf, ['tfdt']).map((tfdt) => {
|
|
911
|
+
const version = tfdt[0];
|
|
912
|
+
let result = readUint32(tfdt, 4);
|
|
913
|
+
if (version === 1) {
|
|
914
|
+
result *= Math.pow(2, 32);
|
|
915
|
+
result += readUint32(tfdt, 8);
|
|
916
|
+
}
|
|
917
|
+
return result / timescale;
|
|
918
|
+
})[0];
|
|
919
|
+
|
|
920
|
+
if ((baseTime as any) !== undefined) {
|
|
921
|
+
timeOffset = baseTime;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return findBox(traf, ['tfhd']).map((tfhd) => {
|
|
925
|
+
const id = readUint32(tfhd, 4);
|
|
926
|
+
const tfhdFlags = readUint32(tfhd, 0) & 0xffffff;
|
|
927
|
+
const baseDataOffsetPresent = (tfhdFlags & 0x000001) !== 0;
|
|
928
|
+
const sampleDescriptionIndexPresent = (tfhdFlags & 0x000002) !== 0;
|
|
929
|
+
const defaultSampleDurationPresent = (tfhdFlags & 0x000008) !== 0;
|
|
930
|
+
let defaultSampleDuration = 0;
|
|
931
|
+
const defaultSampleSizePresent = (tfhdFlags & 0x000010) !== 0;
|
|
932
|
+
let defaultSampleSize = 0;
|
|
933
|
+
const defaultSampleFlagsPresent = (tfhdFlags & 0x000020) !== 0;
|
|
934
|
+
let tfhdOffset = 8;
|
|
935
|
+
|
|
936
|
+
if (id === trackId) {
|
|
937
|
+
if (baseDataOffsetPresent) {
|
|
938
|
+
tfhdOffset += 8;
|
|
939
|
+
}
|
|
940
|
+
if (sampleDescriptionIndexPresent) {
|
|
941
|
+
tfhdOffset += 4;
|
|
942
|
+
}
|
|
943
|
+
if (defaultSampleDurationPresent) {
|
|
944
|
+
defaultSampleDuration = readUint32(tfhd, tfhdOffset);
|
|
945
|
+
tfhdOffset += 4;
|
|
946
|
+
}
|
|
947
|
+
if (defaultSampleSizePresent) {
|
|
948
|
+
defaultSampleSize = readUint32(tfhd, tfhdOffset);
|
|
949
|
+
tfhdOffset += 4;
|
|
950
|
+
}
|
|
951
|
+
if (defaultSampleFlagsPresent) {
|
|
952
|
+
tfhdOffset += 4;
|
|
953
|
+
}
|
|
954
|
+
if (track.type === 'video') {
|
|
955
|
+
isHEVCFlavor = isHEVC(track.codec);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
findBox(traf, ['trun']).map((trun) => {
|
|
959
|
+
const version = trun[0];
|
|
960
|
+
const flags = readUint32(trun, 0) & 0xffffff;
|
|
961
|
+
const dataOffsetPresent = (flags & 0x000001) !== 0;
|
|
962
|
+
let dataOffset = 0;
|
|
963
|
+
const firstSampleFlagsPresent = (flags & 0x000004) !== 0;
|
|
964
|
+
const sampleDurationPresent = (flags & 0x000100) !== 0;
|
|
965
|
+
let sampleDuration = 0;
|
|
966
|
+
const sampleSizePresent = (flags & 0x000200) !== 0;
|
|
967
|
+
let sampleSize = 0;
|
|
968
|
+
const sampleFlagsPresent = (flags & 0x000400) !== 0;
|
|
969
|
+
const sampleCompositionOffsetsPresent = (flags & 0x000800) !== 0;
|
|
970
|
+
let compositionOffset = 0;
|
|
971
|
+
const sampleCount = readUint32(trun, 4);
|
|
972
|
+
let trunOffset = 8; // past version, flags, and sample count
|
|
973
|
+
|
|
974
|
+
if (dataOffsetPresent) {
|
|
975
|
+
dataOffset = readUint32(trun, trunOffset);
|
|
976
|
+
trunOffset += 4;
|
|
977
|
+
}
|
|
978
|
+
if (firstSampleFlagsPresent) {
|
|
979
|
+
trunOffset += 4;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
let sampleOffset = dataOffset + moofOffset;
|
|
983
|
+
|
|
984
|
+
for (let ix = 0; ix < sampleCount; ix++) {
|
|
985
|
+
if (sampleDurationPresent) {
|
|
986
|
+
sampleDuration = readUint32(trun, trunOffset);
|
|
987
|
+
trunOffset += 4;
|
|
988
|
+
} else {
|
|
989
|
+
sampleDuration = defaultSampleDuration;
|
|
990
|
+
}
|
|
991
|
+
if (sampleSizePresent) {
|
|
992
|
+
sampleSize = readUint32(trun, trunOffset);
|
|
993
|
+
trunOffset += 4;
|
|
994
|
+
} else {
|
|
995
|
+
sampleSize = defaultSampleSize;
|
|
996
|
+
}
|
|
997
|
+
if (sampleFlagsPresent) {
|
|
998
|
+
trunOffset += 4;
|
|
999
|
+
}
|
|
1000
|
+
if (sampleCompositionOffsetsPresent) {
|
|
1001
|
+
if (version === 0) {
|
|
1002
|
+
compositionOffset = readUint32(trun, trunOffset);
|
|
1003
|
+
} else {
|
|
1004
|
+
compositionOffset = readSint32(trun, trunOffset);
|
|
1005
|
+
}
|
|
1006
|
+
trunOffset += 4;
|
|
1007
|
+
}
|
|
1008
|
+
if (track.type === ElementaryStreamTypes.VIDEO) {
|
|
1009
|
+
let naluTotalSize = 0;
|
|
1010
|
+
while (naluTotalSize < sampleSize) {
|
|
1011
|
+
const naluSize = readUint32(videoData, sampleOffset);
|
|
1012
|
+
sampleOffset += 4;
|
|
1013
|
+
if (isSEIMessage(isHEVCFlavor, videoData[sampleOffset])) {
|
|
1014
|
+
const data = videoData.subarray(
|
|
1015
|
+
sampleOffset,
|
|
1016
|
+
sampleOffset + naluSize,
|
|
1017
|
+
);
|
|
1018
|
+
parseSEIMessageFromNALu(
|
|
1019
|
+
data,
|
|
1020
|
+
isHEVCFlavor ? 2 : 1,
|
|
1021
|
+
timeOffset + compositionOffset / timescale,
|
|
1022
|
+
seiSamples,
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
sampleOffset += naluSize;
|
|
1026
|
+
naluTotalSize += naluSize + 4;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
timeOffset += sampleDuration / timescale;
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
});
|
|
1037
|
+
return seiSamples;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
export function isHEVC(codec: string | undefined) {
|
|
1041
|
+
if (!codec) {
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
const baseCodec = codec.substring(0, 4);
|
|
1045
|
+
return (
|
|
1046
|
+
baseCodec === 'hvc1' ||
|
|
1047
|
+
baseCodec === 'hev1' ||
|
|
1048
|
+
// Dolby Vision
|
|
1049
|
+
baseCodec === 'dvh1' ||
|
|
1050
|
+
baseCodec === 'dvhe'
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function isSEIMessage(isHEVCFlavor: boolean, naluHeader: number) {
|
|
1055
|
+
if (isHEVCFlavor) {
|
|
1056
|
+
const naluType = (naluHeader >> 1) & 0x3f;
|
|
1057
|
+
return naluType === 39 || naluType === 40;
|
|
1058
|
+
} else {
|
|
1059
|
+
const naluType = naluHeader & 0x1f;
|
|
1060
|
+
return naluType === 6;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
export function parseSEIMessageFromNALu(
|
|
1065
|
+
unescapedData: Uint8Array,
|
|
1066
|
+
headerSize: number,
|
|
1067
|
+
pts: number,
|
|
1068
|
+
samples: UserdataSample[],
|
|
1069
|
+
) {
|
|
1070
|
+
const data = discardEPB(unescapedData);
|
|
1071
|
+
let seiPtr = 0;
|
|
1072
|
+
// skip nal header
|
|
1073
|
+
seiPtr += headerSize;
|
|
1074
|
+
let payloadType = 0;
|
|
1075
|
+
let payloadSize = 0;
|
|
1076
|
+
let b = 0;
|
|
1077
|
+
|
|
1078
|
+
while (seiPtr < data.length) {
|
|
1079
|
+
payloadType = 0;
|
|
1080
|
+
do {
|
|
1081
|
+
if (seiPtr >= data.length) {
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
b = data[seiPtr++];
|
|
1085
|
+
payloadType += b;
|
|
1086
|
+
} while (b === 0xff);
|
|
1087
|
+
|
|
1088
|
+
// Parse payload size.
|
|
1089
|
+
payloadSize = 0;
|
|
1090
|
+
do {
|
|
1091
|
+
if (seiPtr >= data.length) {
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
b = data[seiPtr++];
|
|
1095
|
+
payloadSize += b;
|
|
1096
|
+
} while (b === 0xff);
|
|
1097
|
+
|
|
1098
|
+
const leftOver = data.length - seiPtr;
|
|
1099
|
+
// Create a variable to process the payload
|
|
1100
|
+
let payPtr = seiPtr;
|
|
1101
|
+
|
|
1102
|
+
// Increment the seiPtr to the end of the payload
|
|
1103
|
+
if (payloadSize < leftOver) {
|
|
1104
|
+
seiPtr += payloadSize;
|
|
1105
|
+
} else if (payloadSize > leftOver) {
|
|
1106
|
+
// Some type of corruption has happened?
|
|
1107
|
+
logger.error(
|
|
1108
|
+
`Malformed SEI payload. ${payloadSize} is too small, only ${leftOver} bytes left to parse.`,
|
|
1109
|
+
);
|
|
1110
|
+
// We might be able to parse some data, but let's be safe and ignore it.
|
|
1111
|
+
break;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (payloadType === 4) {
|
|
1115
|
+
const countryCode = data[payPtr++];
|
|
1116
|
+
if (countryCode === 181) {
|
|
1117
|
+
const providerCode = readUint16(data, payPtr);
|
|
1118
|
+
payPtr += 2;
|
|
1119
|
+
|
|
1120
|
+
if (providerCode === 49) {
|
|
1121
|
+
const userStructure = readUint32(data, payPtr);
|
|
1122
|
+
payPtr += 4;
|
|
1123
|
+
|
|
1124
|
+
if (userStructure === 0x47413934) {
|
|
1125
|
+
const userDataType = data[payPtr++];
|
|
1126
|
+
|
|
1127
|
+
// Raw CEA-608 bytes wrapped in CEA-708 packet
|
|
1128
|
+
if (userDataType === 3) {
|
|
1129
|
+
const firstByte = data[payPtr++];
|
|
1130
|
+
const totalCCs = 0x1f & firstByte;
|
|
1131
|
+
const enabled = 0x40 & firstByte;
|
|
1132
|
+
const totalBytes = enabled ? 2 + totalCCs * 3 : 0;
|
|
1133
|
+
const byteArray = new Uint8Array(totalBytes);
|
|
1134
|
+
if (enabled) {
|
|
1135
|
+
byteArray[0] = firstByte;
|
|
1136
|
+
for (let i = 1; i < totalBytes; i++) {
|
|
1137
|
+
byteArray[i] = data[payPtr++];
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
samples.push({
|
|
1142
|
+
type: userDataType,
|
|
1143
|
+
payloadType,
|
|
1144
|
+
pts,
|
|
1145
|
+
bytes: byteArray,
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
} else if (payloadType === 5) {
|
|
1152
|
+
if (payloadSize > 16) {
|
|
1153
|
+
const uuidStrArray: Array<string> = [];
|
|
1154
|
+
for (let i = 0; i < 16; i++) {
|
|
1155
|
+
const b = data[payPtr++].toString(16);
|
|
1156
|
+
uuidStrArray.push(b.length == 1 ? '0' + b : b);
|
|
1157
|
+
|
|
1158
|
+
if (i === 3 || i === 5 || i === 7 || i === 9) {
|
|
1159
|
+
uuidStrArray.push('-');
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
const length = payloadSize - 16;
|
|
1163
|
+
const userDataBytes = new Uint8Array(length);
|
|
1164
|
+
for (let i = 0; i < length; i++) {
|
|
1165
|
+
userDataBytes[i] = data[payPtr++];
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
samples.push({
|
|
1169
|
+
payloadType,
|
|
1170
|
+
pts,
|
|
1171
|
+
uuid: uuidStrArray.join(''),
|
|
1172
|
+
userData: utf8ArrayToStr(userDataBytes),
|
|
1173
|
+
userDataBytes,
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* remove Emulation Prevention bytes from a RBSP
|
|
1182
|
+
*/
|
|
1183
|
+
export function discardEPB(data: Uint8Array): Uint8Array {
|
|
1184
|
+
const length = data.byteLength;
|
|
1185
|
+
const EPBPositions = [] as Array<number>;
|
|
1186
|
+
let i = 1;
|
|
1187
|
+
|
|
1188
|
+
// Find all `Emulation Prevention Bytes`
|
|
1189
|
+
while (i < length - 2) {
|
|
1190
|
+
if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) {
|
|
1191
|
+
EPBPositions.push(i + 2);
|
|
1192
|
+
i += 2;
|
|
1193
|
+
} else {
|
|
1194
|
+
i++;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// If no Emulation Prevention Bytes were found just return the original
|
|
1199
|
+
// array
|
|
1200
|
+
if (EPBPositions.length === 0) {
|
|
1201
|
+
return data;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Create a new array to hold the NAL unit data
|
|
1205
|
+
const newLength = length - EPBPositions.length;
|
|
1206
|
+
const newData = new Uint8Array(newLength);
|
|
1207
|
+
let sourceIndex = 0;
|
|
1208
|
+
|
|
1209
|
+
for (i = 0; i < newLength; sourceIndex++, i++) {
|
|
1210
|
+
if (sourceIndex === EPBPositions[0]) {
|
|
1211
|
+
// Skip this byte
|
|
1212
|
+
sourceIndex++;
|
|
1213
|
+
// Remove this position index
|
|
1214
|
+
EPBPositions.shift();
|
|
1215
|
+
}
|
|
1216
|
+
newData[i] = data[sourceIndex];
|
|
1217
|
+
}
|
|
1218
|
+
return newData;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
export function parseEmsg(data: Uint8Array): IEmsgParsingData {
|
|
1222
|
+
const version = data[0];
|
|
1223
|
+
let schemeIdUri: string = '';
|
|
1224
|
+
let value: string = '';
|
|
1225
|
+
let timeScale: number = 0;
|
|
1226
|
+
let presentationTimeDelta: number = 0;
|
|
1227
|
+
let presentationTime: number = 0;
|
|
1228
|
+
let eventDuration: number = 0;
|
|
1229
|
+
let id: number = 0;
|
|
1230
|
+
let offset: number = 0;
|
|
1231
|
+
|
|
1232
|
+
if (version === 0) {
|
|
1233
|
+
while (bin2str(data.subarray(offset, offset + 1)) !== '\0') {
|
|
1234
|
+
schemeIdUri += bin2str(data.subarray(offset, offset + 1));
|
|
1235
|
+
offset += 1;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
schemeIdUri += bin2str(data.subarray(offset, offset + 1));
|
|
1239
|
+
offset += 1;
|
|
1240
|
+
|
|
1241
|
+
while (bin2str(data.subarray(offset, offset + 1)) !== '\0') {
|
|
1242
|
+
value += bin2str(data.subarray(offset, offset + 1));
|
|
1243
|
+
offset += 1;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
value += bin2str(data.subarray(offset, offset + 1));
|
|
1247
|
+
offset += 1;
|
|
1248
|
+
|
|
1249
|
+
timeScale = readUint32(data, 12);
|
|
1250
|
+
presentationTimeDelta = readUint32(data, 16);
|
|
1251
|
+
eventDuration = readUint32(data, 20);
|
|
1252
|
+
id = readUint32(data, 24);
|
|
1253
|
+
offset = 28;
|
|
1254
|
+
} else if (version === 1) {
|
|
1255
|
+
offset += 4;
|
|
1256
|
+
timeScale = readUint32(data, offset);
|
|
1257
|
+
offset += 4;
|
|
1258
|
+
const leftPresentationTime = readUint32(data, offset);
|
|
1259
|
+
offset += 4;
|
|
1260
|
+
const rightPresentationTime = readUint32(data, offset);
|
|
1261
|
+
offset += 4;
|
|
1262
|
+
presentationTime = 2 ** 32 * leftPresentationTime + rightPresentationTime;
|
|
1263
|
+
if (!Number.isSafeInteger(presentationTime)) {
|
|
1264
|
+
presentationTime = Number.MAX_SAFE_INTEGER;
|
|
1265
|
+
logger.warn(
|
|
1266
|
+
'Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box',
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
eventDuration = readUint32(data, offset);
|
|
1271
|
+
offset += 4;
|
|
1272
|
+
id = readUint32(data, offset);
|
|
1273
|
+
offset += 4;
|
|
1274
|
+
|
|
1275
|
+
while (bin2str(data.subarray(offset, offset + 1)) !== '\0') {
|
|
1276
|
+
schemeIdUri += bin2str(data.subarray(offset, offset + 1));
|
|
1277
|
+
offset += 1;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
schemeIdUri += bin2str(data.subarray(offset, offset + 1));
|
|
1281
|
+
offset += 1;
|
|
1282
|
+
|
|
1283
|
+
while (bin2str(data.subarray(offset, offset + 1)) !== '\0') {
|
|
1284
|
+
value += bin2str(data.subarray(offset, offset + 1));
|
|
1285
|
+
offset += 1;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
value += bin2str(data.subarray(offset, offset + 1));
|
|
1289
|
+
offset += 1;
|
|
1290
|
+
}
|
|
1291
|
+
const payload = data.subarray(offset, data.byteLength);
|
|
1292
|
+
|
|
1293
|
+
return {
|
|
1294
|
+
schemeIdUri,
|
|
1295
|
+
value,
|
|
1296
|
+
timeScale,
|
|
1297
|
+
presentationTime,
|
|
1298
|
+
presentationTimeDelta,
|
|
1299
|
+
eventDuration,
|
|
1300
|
+
id,
|
|
1301
|
+
payload,
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
export function mp4Box(type: ArrayLike<number>, ...payload: Uint8Array[]) {
|
|
1306
|
+
const len = payload.length;
|
|
1307
|
+
let size = 8;
|
|
1308
|
+
let i = len;
|
|
1309
|
+
while (i--) {
|
|
1310
|
+
size += payload[i].byteLength;
|
|
1311
|
+
}
|
|
1312
|
+
const result = new Uint8Array(size);
|
|
1313
|
+
result[0] = (size >> 24) & 0xff;
|
|
1314
|
+
result[1] = (size >> 16) & 0xff;
|
|
1315
|
+
result[2] = (size >> 8) & 0xff;
|
|
1316
|
+
result[3] = size & 0xff;
|
|
1317
|
+
result.set(type, 4);
|
|
1318
|
+
for (i = 0, size = 8; i < len; i++) {
|
|
1319
|
+
result.set(payload[i], size);
|
|
1320
|
+
size += payload[i].byteLength;
|
|
1321
|
+
}
|
|
1322
|
+
return result;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
export function mp4pssh(
|
|
1326
|
+
systemId: Uint8Array,
|
|
1327
|
+
keyids: Array<Uint8Array> | null,
|
|
1328
|
+
data: Uint8Array,
|
|
1329
|
+
) {
|
|
1330
|
+
if (systemId.byteLength !== 16) {
|
|
1331
|
+
throw new RangeError('Invalid system id');
|
|
1332
|
+
}
|
|
1333
|
+
let version;
|
|
1334
|
+
let kids;
|
|
1335
|
+
if (keyids) {
|
|
1336
|
+
version = 1;
|
|
1337
|
+
kids = new Uint8Array(keyids.length * 16);
|
|
1338
|
+
for (let ix = 0; ix < keyids.length; ix++) {
|
|
1339
|
+
const k = keyids[ix]; // uint8array
|
|
1340
|
+
if (k.byteLength !== 16) {
|
|
1341
|
+
throw new RangeError('Invalid key');
|
|
1342
|
+
}
|
|
1343
|
+
kids.set(k, ix * 16);
|
|
1344
|
+
}
|
|
1345
|
+
} else {
|
|
1346
|
+
version = 0;
|
|
1347
|
+
kids = new Uint8Array();
|
|
1348
|
+
}
|
|
1349
|
+
let kidCount;
|
|
1350
|
+
if (version > 0) {
|
|
1351
|
+
kidCount = new Uint8Array(4);
|
|
1352
|
+
if (keyids!.length > 0) {
|
|
1353
|
+
new DataView(kidCount.buffer).setUint32(0, keyids!.length, false);
|
|
1354
|
+
}
|
|
1355
|
+
} else {
|
|
1356
|
+
kidCount = new Uint8Array();
|
|
1357
|
+
}
|
|
1358
|
+
const dataSize = new Uint8Array(4);
|
|
1359
|
+
if (data.byteLength > 0) {
|
|
1360
|
+
new DataView(dataSize.buffer).setUint32(0, data.byteLength, false);
|
|
1361
|
+
}
|
|
1362
|
+
return mp4Box(
|
|
1363
|
+
[112, 115, 115, 104],
|
|
1364
|
+
new Uint8Array([
|
|
1365
|
+
version,
|
|
1366
|
+
0x00,
|
|
1367
|
+
0x00,
|
|
1368
|
+
0x00, // Flags
|
|
1369
|
+
]),
|
|
1370
|
+
systemId, // 16 bytes
|
|
1371
|
+
kidCount,
|
|
1372
|
+
kids,
|
|
1373
|
+
dataSize,
|
|
1374
|
+
data,
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
export type PsshData = {
|
|
1379
|
+
version: 0 | 1;
|
|
1380
|
+
systemId: KeySystemIds;
|
|
1381
|
+
kids: null | Uint8Array<ArrayBuffer>[];
|
|
1382
|
+
data: null | Uint8Array<ArrayBuffer>;
|
|
1383
|
+
offset: number;
|
|
1384
|
+
size: number;
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
export type PsshInvalidResult = {
|
|
1388
|
+
systemId?: undefined;
|
|
1389
|
+
kids?: undefined;
|
|
1390
|
+
offset: number;
|
|
1391
|
+
size: number;
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
export function parseMultiPssh(
|
|
1395
|
+
initData: ArrayBuffer,
|
|
1396
|
+
): (PsshData | PsshInvalidResult)[] {
|
|
1397
|
+
const results: (PsshData | PsshInvalidResult)[] = [];
|
|
1398
|
+
if (initData instanceof ArrayBuffer) {
|
|
1399
|
+
const length = initData.byteLength;
|
|
1400
|
+
let offset = 0;
|
|
1401
|
+
while (offset + 32 < length) {
|
|
1402
|
+
const view = new DataView(initData, offset);
|
|
1403
|
+
const pssh = parsePssh(view);
|
|
1404
|
+
results.push(pssh);
|
|
1405
|
+
offset += pssh.size;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return results;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function parsePssh(view: DataView<ArrayBuffer>): PsshData | PsshInvalidResult {
|
|
1412
|
+
const size = view.getUint32(0);
|
|
1413
|
+
const offset = view.byteOffset;
|
|
1414
|
+
const length = view.byteLength;
|
|
1415
|
+
if (length < size) {
|
|
1416
|
+
return {
|
|
1417
|
+
offset,
|
|
1418
|
+
size: length,
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
const type = view.getUint32(4);
|
|
1422
|
+
if (type !== 0x70737368) {
|
|
1423
|
+
return { offset, size };
|
|
1424
|
+
}
|
|
1425
|
+
const version = view.getUint32(8) >>> 24;
|
|
1426
|
+
if (version !== 0 && version !== 1) {
|
|
1427
|
+
return { offset, size };
|
|
1428
|
+
}
|
|
1429
|
+
const buffer = view.buffer;
|
|
1430
|
+
const systemId = arrayToHex(
|
|
1431
|
+
new Uint8Array(buffer, offset + 12, 16),
|
|
1432
|
+
) as KeySystemIds;
|
|
1433
|
+
|
|
1434
|
+
let kids: null | Uint8Array<ArrayBuffer>[] = null;
|
|
1435
|
+
let data: null | Uint8Array<ArrayBuffer> = null;
|
|
1436
|
+
let dataSizeOffset = 0;
|
|
1437
|
+
|
|
1438
|
+
if (version === 0) {
|
|
1439
|
+
dataSizeOffset = 28;
|
|
1440
|
+
} else {
|
|
1441
|
+
const kidCounts = view.getUint32(28);
|
|
1442
|
+
if (!kidCounts || length < 32 + kidCounts * 16) {
|
|
1443
|
+
return { offset, size };
|
|
1444
|
+
}
|
|
1445
|
+
kids = [];
|
|
1446
|
+
for (let i = 0; i < kidCounts; i++) {
|
|
1447
|
+
kids.push(new Uint8Array(buffer, offset + 32 + i * 16, 16));
|
|
1448
|
+
}
|
|
1449
|
+
dataSizeOffset = 32 + kidCounts * 16;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if (!dataSizeOffset) {
|
|
1453
|
+
return { offset, size };
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
const dataSizeOrKidCount = view.getUint32(dataSizeOffset);
|
|
1457
|
+
if (size - 32 < dataSizeOrKidCount) {
|
|
1458
|
+
return { offset, size };
|
|
1459
|
+
}
|
|
1460
|
+
data = new Uint8Array(
|
|
1461
|
+
buffer,
|
|
1462
|
+
offset + dataSizeOffset + 4,
|
|
1463
|
+
dataSizeOrKidCount,
|
|
1464
|
+
);
|
|
1465
|
+
return {
|
|
1466
|
+
version,
|
|
1467
|
+
systemId,
|
|
1468
|
+
kids,
|
|
1469
|
+
data,
|
|
1470
|
+
offset,
|
|
1471
|
+
size,
|
|
1472
|
+
};
|
|
1473
|
+
}
|