@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.
Files changed (159) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +472 -0
  3. package/dist/hls-demo.js +26995 -0
  4. package/dist/hls-demo.js.map +1 -0
  5. package/dist/hls.d.mts +4204 -0
  6. package/dist/hls.d.ts +4204 -0
  7. package/dist/hls.js +40050 -0
  8. package/dist/hls.js.d.ts +4204 -0
  9. package/dist/hls.js.map +1 -0
  10. package/dist/hls.light.js +27145 -0
  11. package/dist/hls.light.js.map +1 -0
  12. package/dist/hls.light.min.js +2 -0
  13. package/dist/hls.light.min.js.map +1 -0
  14. package/dist/hls.light.mjs +26392 -0
  15. package/dist/hls.light.mjs.map +1 -0
  16. package/dist/hls.min.js +2 -0
  17. package/dist/hls.min.js.map +1 -0
  18. package/dist/hls.mjs +38956 -0
  19. package/dist/hls.mjs.map +1 -0
  20. package/dist/hls.worker.js +2 -0
  21. package/dist/hls.worker.js.map +1 -0
  22. package/package.json +143 -0
  23. package/src/config.ts +794 -0
  24. package/src/controller/abr-controller.ts +1019 -0
  25. package/src/controller/algo-data-controller.ts +794 -0
  26. package/src/controller/audio-stream-controller.ts +1099 -0
  27. package/src/controller/audio-track-controller.ts +454 -0
  28. package/src/controller/base-playlist-controller.ts +438 -0
  29. package/src/controller/base-stream-controller.ts +2526 -0
  30. package/src/controller/buffer-controller.ts +2015 -0
  31. package/src/controller/buffer-operation-queue.ts +159 -0
  32. package/src/controller/cap-level-controller.ts +367 -0
  33. package/src/controller/cmcd-controller.ts +422 -0
  34. package/src/controller/content-steering-controller.ts +622 -0
  35. package/src/controller/eme-controller.ts +1617 -0
  36. package/src/controller/error-controller.ts +627 -0
  37. package/src/controller/fps-controller.ts +146 -0
  38. package/src/controller/fragment-finders.ts +256 -0
  39. package/src/controller/fragment-tracker.ts +567 -0
  40. package/src/controller/gap-controller.ts +719 -0
  41. package/src/controller/id3-track-controller.ts +488 -0
  42. package/src/controller/interstitial-player.ts +302 -0
  43. package/src/controller/interstitials-controller.ts +2895 -0
  44. package/src/controller/interstitials-schedule.ts +698 -0
  45. package/src/controller/latency-controller.ts +294 -0
  46. package/src/controller/level-controller.ts +776 -0
  47. package/src/controller/stream-controller.ts +1597 -0
  48. package/src/controller/subtitle-stream-controller.ts +508 -0
  49. package/src/controller/subtitle-track-controller.ts +617 -0
  50. package/src/controller/timeline-controller.ts +677 -0
  51. package/src/crypt/aes-crypto.ts +36 -0
  52. package/src/crypt/aes-decryptor.ts +339 -0
  53. package/src/crypt/decrypter-aes-mode.ts +4 -0
  54. package/src/crypt/decrypter.ts +225 -0
  55. package/src/crypt/fast-aes-key.ts +39 -0
  56. package/src/define-plugin.d.ts +17 -0
  57. package/src/demux/audio/aacdemuxer.ts +126 -0
  58. package/src/demux/audio/ac3-demuxer.ts +170 -0
  59. package/src/demux/audio/adts.ts +249 -0
  60. package/src/demux/audio/base-audio-demuxer.ts +205 -0
  61. package/src/demux/audio/dolby.ts +21 -0
  62. package/src/demux/audio/mp3demuxer.ts +85 -0
  63. package/src/demux/audio/mpegaudio.ts +177 -0
  64. package/src/demux/chunk-cache.ts +42 -0
  65. package/src/demux/dummy-demuxed-track.ts +13 -0
  66. package/src/demux/inject-worker.ts +75 -0
  67. package/src/demux/mp4demuxer.ts +234 -0
  68. package/src/demux/sample-aes.ts +198 -0
  69. package/src/demux/transmuxer-interface.ts +449 -0
  70. package/src/demux/transmuxer-worker.ts +221 -0
  71. package/src/demux/transmuxer.ts +560 -0
  72. package/src/demux/tsdemuxer.ts +1256 -0
  73. package/src/demux/video/avc-video-parser.ts +401 -0
  74. package/src/demux/video/base-video-parser.ts +198 -0
  75. package/src/demux/video/exp-golomb.ts +153 -0
  76. package/src/demux/video/hevc-video-parser.ts +736 -0
  77. package/src/empty-es.js +5 -0
  78. package/src/empty.js +3 -0
  79. package/src/errors.ts +107 -0
  80. package/src/events.ts +548 -0
  81. package/src/exports-default.ts +3 -0
  82. package/src/exports-named.ts +81 -0
  83. package/src/hls.ts +1613 -0
  84. package/src/is-supported.ts +54 -0
  85. package/src/loader/date-range.ts +207 -0
  86. package/src/loader/fragment-loader.ts +403 -0
  87. package/src/loader/fragment.ts +487 -0
  88. package/src/loader/interstitial-asset-list.ts +162 -0
  89. package/src/loader/interstitial-event.ts +337 -0
  90. package/src/loader/key-loader.ts +439 -0
  91. package/src/loader/level-details.ts +203 -0
  92. package/src/loader/level-key.ts +259 -0
  93. package/src/loader/load-stats.ts +17 -0
  94. package/src/loader/m3u8-parser.ts +1072 -0
  95. package/src/loader/playlist-loader.ts +839 -0
  96. package/src/polyfills/number.ts +15 -0
  97. package/src/remux/aac-helper.ts +81 -0
  98. package/src/remux/mp4-generator.ts +1380 -0
  99. package/src/remux/mp4-remuxer.ts +1261 -0
  100. package/src/remux/passthrough-remuxer.ts +434 -0
  101. package/src/task-loop.ts +130 -0
  102. package/src/types/algo.ts +44 -0
  103. package/src/types/buffer.ts +105 -0
  104. package/src/types/component-api.ts +20 -0
  105. package/src/types/demuxer.ts +208 -0
  106. package/src/types/events.ts +574 -0
  107. package/src/types/fragment-tracker.ts +23 -0
  108. package/src/types/level.ts +268 -0
  109. package/src/types/loader.ts +198 -0
  110. package/src/types/media-playlist.ts +92 -0
  111. package/src/types/network-details.ts +3 -0
  112. package/src/types/remuxer.ts +104 -0
  113. package/src/types/track.ts +12 -0
  114. package/src/types/transmuxer.ts +46 -0
  115. package/src/types/tuples.ts +6 -0
  116. package/src/types/vtt.ts +11 -0
  117. package/src/utils/arrays.ts +22 -0
  118. package/src/utils/attr-list.ts +192 -0
  119. package/src/utils/binary-search.ts +46 -0
  120. package/src/utils/buffer-helper.ts +173 -0
  121. package/src/utils/cea-608-parser.ts +1413 -0
  122. package/src/utils/chunker.ts +41 -0
  123. package/src/utils/codecs.ts +314 -0
  124. package/src/utils/cues.ts +96 -0
  125. package/src/utils/discontinuities.ts +174 -0
  126. package/src/utils/encryption-methods-util.ts +21 -0
  127. package/src/utils/error-helper.ts +95 -0
  128. package/src/utils/event-listener-helper.ts +16 -0
  129. package/src/utils/ewma-bandwidth-estimator.ts +97 -0
  130. package/src/utils/ewma.ts +43 -0
  131. package/src/utils/fetch-loader.ts +331 -0
  132. package/src/utils/global.ts +2 -0
  133. package/src/utils/hash.ts +10 -0
  134. package/src/utils/hdr.ts +67 -0
  135. package/src/utils/hex.ts +32 -0
  136. package/src/utils/imsc1-ttml-parser.ts +261 -0
  137. package/src/utils/keysystem-util.ts +45 -0
  138. package/src/utils/level-helper.ts +629 -0
  139. package/src/utils/logger.ts +120 -0
  140. package/src/utils/media-option-attributes.ts +49 -0
  141. package/src/utils/mediacapabilities-helper.ts +301 -0
  142. package/src/utils/mediakeys-helper.ts +210 -0
  143. package/src/utils/mediasource-helper.ts +37 -0
  144. package/src/utils/mp4-tools.ts +1473 -0
  145. package/src/utils/number.ts +3 -0
  146. package/src/utils/numeric-encoding-utils.ts +26 -0
  147. package/src/utils/output-filter.ts +46 -0
  148. package/src/utils/rendition-helper.ts +505 -0
  149. package/src/utils/safe-json-stringify.ts +22 -0
  150. package/src/utils/texttrack-utils.ts +164 -0
  151. package/src/utils/time-ranges.ts +17 -0
  152. package/src/utils/timescale-conversion.ts +46 -0
  153. package/src/utils/utf8-utils.ts +18 -0
  154. package/src/utils/variable-substitution.ts +105 -0
  155. package/src/utils/vttcue.ts +384 -0
  156. package/src/utils/vttparser.ts +497 -0
  157. package/src/utils/webvtt-parser.ts +166 -0
  158. package/src/utils/xhr-loader.ts +337 -0
  159. package/src/version.ts +1 -0
@@ -0,0 +1,1256 @@
1
+ /**
2
+ * highly optimized TS demuxer:
3
+ * parse PAT, PMT
4
+ * extract PES packet from audio and video PIDs
5
+ * extract AVC/H264 (or HEVC/H265) NAL units and AAC/ADTS samples from PES packet
6
+ * trigger the remuxer upon parsing completion
7
+ * it also tries to workaround as best as it can audio codec switch (HE-AAC to AAC and vice versa), without having to restart the MediaSource.
8
+ * it also controls the remuxing process :
9
+ * upon discontinuity or level switch detection, it will also notifies the remuxer so that it can reset its state.
10
+ */
11
+
12
+ import * as AC3 from './audio/ac3-demuxer';
13
+ import * as ADTS from './audio/adts';
14
+ import * as MpegAudio from './audio/mpegaudio';
15
+ import SampleAesDecrypter from './sample-aes';
16
+ import AvcVideoParser from './video/avc-video-parser';
17
+ import HevcVideoParser from './video/hevc-video-parser';
18
+ import { ErrorDetails, ErrorTypes } from '../errors';
19
+ import { Events } from '../events';
20
+ import {
21
+ type DemuxedAudioTrack,
22
+ type DemuxedMetadataTrack,
23
+ type DemuxedTrack,
24
+ type DemuxedUserdataTrack,
25
+ type DemuxedVideoTrack,
26
+ type Demuxer,
27
+ type DemuxerResult,
28
+ type ElementaryStreamData,
29
+ type KeyData,
30
+ MetadataSchema,
31
+ type VideoSample,
32
+ } from '../types/demuxer';
33
+ import { appendUint8Array, RemuxerTrackIdConfig } from '../utils/mp4-tools';
34
+ import type { HlsConfig } from '../config';
35
+ import type { HlsEventEmitter } from '../events';
36
+ import type BaseVideoParser from './video/base-video-parser';
37
+ import type { AudioFrame, DemuxedAAC } from '../types/demuxer';
38
+ import type { TypeSupported } from '../utils/codecs';
39
+ import type { ILogger } from '../utils/logger';
40
+
41
+ export type ParsedTimestamp = {
42
+ pts?: number;
43
+ dts?: number;
44
+ };
45
+
46
+ export type PES = ParsedTimestamp & {
47
+ data: Uint8Array;
48
+ len: number;
49
+ };
50
+
51
+ export type ParsedVideoSample = ParsedTimestamp &
52
+ Omit<VideoSample, 'pts' | 'dts'>;
53
+
54
+ const PACKET_LENGTH = 188;
55
+
56
+ class TSDemuxer implements Demuxer {
57
+ private readonly logger: ILogger;
58
+ private readonly observer: HlsEventEmitter;
59
+ private readonly config: HlsConfig;
60
+ private readonly typeSupported: TypeSupported;
61
+
62
+ private sampleAes: SampleAesDecrypter | null = null;
63
+ private pmtParsed: boolean = false;
64
+ private audioCodec?: string;
65
+ private videoCodec?: string;
66
+ private _pmtId: number = -1;
67
+
68
+ private _videoTrack?: DemuxedVideoTrack;
69
+ private _audioTrack?: DemuxedAudioTrack;
70
+ private _id3Track?: DemuxedMetadataTrack;
71
+ private _txtTrack?: DemuxedUserdataTrack;
72
+ private _klvPid: number = -1;
73
+ private aacOverFlow: AudioFrame | null = null;
74
+ private remainderData: Uint8Array | null = null;
75
+ private videoParser: BaseVideoParser | null;
76
+ private videoIntegrityChecker: PacketsIntegrityChecker | null = null;
77
+
78
+ constructor(
79
+ observer: HlsEventEmitter,
80
+ config: HlsConfig,
81
+ typeSupported: TypeSupported,
82
+ logger: ILogger,
83
+ ) {
84
+ this.observer = observer;
85
+ this.config = config;
86
+ this.typeSupported = typeSupported;
87
+ this.logger = logger;
88
+ this.videoParser = null;
89
+ }
90
+
91
+ static probe(data: Uint8Array, logger: ILogger) {
92
+ const syncOffset = TSDemuxer.syncOffset(data);
93
+ if (syncOffset > 0) {
94
+ logger.warn(
95
+ `MPEG2-TS detected but first sync word found @ offset ${syncOffset}`,
96
+ );
97
+ }
98
+ return syncOffset !== -1;
99
+ }
100
+
101
+ static syncOffset(data: Uint8Array): number {
102
+ const length = data.length;
103
+ let scanwindow = Math.min(PACKET_LENGTH * 5, length - PACKET_LENGTH) + 1;
104
+ let i = 0;
105
+ while (i < scanwindow) {
106
+ // a TS init segment should contain at least 2 TS packets: PAT and PMT, each starting with 0x47
107
+ let foundPat = false;
108
+ let packetStart = -1;
109
+ let tsPackets = 0;
110
+ for (let j = i; j < length; j += PACKET_LENGTH) {
111
+ if (
112
+ data[j] === 0x47 &&
113
+ (length - j === PACKET_LENGTH || data[j + PACKET_LENGTH] === 0x47)
114
+ ) {
115
+ tsPackets++;
116
+ if (packetStart === -1) {
117
+ packetStart = j;
118
+ // First sync word found at offset, increase scan length (#5251)
119
+ if (packetStart !== 0) {
120
+ scanwindow =
121
+ Math.min(
122
+ packetStart + PACKET_LENGTH * 99,
123
+ data.length - PACKET_LENGTH,
124
+ ) + 1;
125
+ }
126
+ }
127
+ if (!foundPat) {
128
+ foundPat = parsePID(data, j) === 0;
129
+ }
130
+ // Sync word found at 0 with 3 packets, or found at offset least 2 packets up to scanwindow (#5501)
131
+ if (
132
+ foundPat &&
133
+ tsPackets > 1 &&
134
+ ((packetStart === 0 && tsPackets > 2) ||
135
+ j + PACKET_LENGTH > scanwindow)
136
+ ) {
137
+ return packetStart;
138
+ }
139
+ } else if (tsPackets) {
140
+ // Exit if sync word found, but does not contain contiguous packets
141
+ return -1;
142
+ } else {
143
+ break;
144
+ }
145
+ }
146
+ i++;
147
+ }
148
+ return -1;
149
+ }
150
+
151
+ /**
152
+ * Creates a track model internal to demuxer used to drive remuxing input
153
+ */
154
+ static createTrack(
155
+ type: 'audio' | 'video' | 'id3' | 'text',
156
+ duration?: number,
157
+ ): DemuxedTrack {
158
+ return {
159
+ container:
160
+ type === 'video' || type === 'audio' ? 'video/mp2t' : undefined,
161
+ type,
162
+ id: RemuxerTrackIdConfig[type],
163
+ pid: -1,
164
+ inputTimeScale: 90000,
165
+ sequenceNumber: 0,
166
+ samples: [],
167
+ dropped: 0,
168
+ duration: type === 'audio' ? duration : undefined,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Initializes a new init segment on the demuxer/remuxer interface. Needed for discontinuities/track-switches (or at stream start)
174
+ * Resets all internal track instances of the demuxer.
175
+ */
176
+ public resetInitSegment(
177
+ initSegment: Uint8Array | undefined,
178
+ audioCodec: string,
179
+ videoCodec: string,
180
+ trackDuration: number,
181
+ ) {
182
+ this.pmtParsed = false;
183
+ this._pmtId = -1;
184
+
185
+ this._videoTrack = TSDemuxer.createTrack('video') as DemuxedVideoTrack;
186
+ this._videoTrack.duration = trackDuration;
187
+ this.videoIntegrityChecker =
188
+ this.config.handleMpegTsVideoIntegrityErrors === 'skip'
189
+ ? new PacketsIntegrityChecker(this.logger)
190
+ : null;
191
+ this._audioTrack = TSDemuxer.createTrack(
192
+ 'audio',
193
+ trackDuration,
194
+ ) as DemuxedAudioTrack;
195
+ this._id3Track = TSDemuxer.createTrack('id3') as DemuxedMetadataTrack;
196
+ this._txtTrack = TSDemuxer.createTrack('text') as DemuxedUserdataTrack;
197
+ this._audioTrack.segmentCodec = 'aac';
198
+
199
+ // flush any partial content
200
+ this.videoParser = null;
201
+ this.aacOverFlow = null;
202
+ this.remainderData = null;
203
+ this.audioCodec = audioCodec;
204
+ this.videoCodec = videoCodec;
205
+ }
206
+
207
+ public resetTimeStamp() {}
208
+
209
+ public resetContiguity(): void {
210
+ const { _audioTrack, _videoTrack, _id3Track } = this;
211
+ if (_audioTrack) {
212
+ _audioTrack.pesData = null;
213
+ }
214
+ if (_videoTrack) {
215
+ _videoTrack.pesData = null;
216
+ }
217
+ if (_id3Track) {
218
+ _id3Track.pesData = null;
219
+ }
220
+ this.aacOverFlow = null;
221
+ this.remainderData = null;
222
+ }
223
+
224
+ public demux(
225
+ data: Uint8Array,
226
+ timeOffset: number,
227
+ isSampleAes = false,
228
+ flush = false,
229
+ ): DemuxerResult {
230
+ if (!isSampleAes) {
231
+ this.sampleAes = null;
232
+ }
233
+
234
+ let pes: PES | null;
235
+
236
+ const videoTrack = this._videoTrack as DemuxedVideoTrack;
237
+ const videoIntegrityChecker = this.videoIntegrityChecker;
238
+ const audioTrack = this._audioTrack as DemuxedAudioTrack;
239
+ const id3Track = this._id3Track as DemuxedMetadataTrack;
240
+ const textTrack = this._txtTrack as DemuxedUserdataTrack;
241
+
242
+ let videoPid = videoTrack.pid;
243
+ let videoData = videoTrack.pesData;
244
+ let audioPid = audioTrack.pid;
245
+ let id3Pid = id3Track.pid;
246
+ let klvPid = this._klvPid;
247
+ let audioData = audioTrack.pesData;
248
+ let id3Data = id3Track.pesData;
249
+ let klvData: ElementaryStreamData | null = null;
250
+ let unknownPID: number | null = null;
251
+ let pmtParsed = this.pmtParsed;
252
+ let pmtId = this._pmtId;
253
+
254
+ let len = data.length;
255
+ if (this.remainderData) {
256
+ data = appendUint8Array(this.remainderData, data);
257
+ len = data.length;
258
+ this.remainderData = null;
259
+ }
260
+
261
+ if (len < PACKET_LENGTH && !flush) {
262
+ this.remainderData = data;
263
+ return {
264
+ audioTrack,
265
+ videoTrack,
266
+ id3Track,
267
+ textTrack,
268
+ };
269
+ }
270
+
271
+ const syncOffset = Math.max(0, TSDemuxer.syncOffset(data));
272
+ len -= (len - syncOffset) % PACKET_LENGTH;
273
+ if (len < data.byteLength && !flush) {
274
+ this.remainderData = new Uint8Array(
275
+ data.buffer,
276
+ len,
277
+ data.buffer.byteLength - len,
278
+ );
279
+ }
280
+
281
+ // loop through TS packets
282
+ let tsPacketErrors = 0;
283
+ for (let start = syncOffset; start < len; start += PACKET_LENGTH) {
284
+ if (data[start] === 0x47) {
285
+ const stt = !!(data[start + 1] & 0x40);
286
+ const pid = parsePID(data, start);
287
+ const atf = (data[start + 3] & 0x30) >> 4;
288
+
289
+ // if an adaption field is present, its length is specified by the fifth byte of the TS packet header.
290
+ let offset: number;
291
+ if (atf > 1) {
292
+ offset = start + 5 + data[start + 4];
293
+ // continue if there is only adaptation field
294
+ if (offset === start + PACKET_LENGTH) {
295
+ continue;
296
+ }
297
+ } else {
298
+ offset = start + 4;
299
+ }
300
+ switch (pid) {
301
+ case videoPid:
302
+ if (stt) {
303
+ if (
304
+ videoData &&
305
+ !videoIntegrityChecker?.isCorrupted &&
306
+ (pes = parsePES(videoData, this.logger))
307
+ ) {
308
+ this.readyVideoParser(videoTrack.segmentCodec);
309
+ if (this.videoParser !== null) {
310
+ this.videoParser.parsePES(videoTrack, textTrack, pes, false);
311
+ }
312
+ }
313
+
314
+ videoData = { data: [], size: 0 };
315
+ videoIntegrityChecker?.reset(videoPid);
316
+ }
317
+ videoIntegrityChecker?.handlePacket(data.subarray(start));
318
+ if (videoData) {
319
+ videoData.data.push(data.subarray(offset, start + PACKET_LENGTH));
320
+ videoData.size += start + PACKET_LENGTH - offset;
321
+ }
322
+ break;
323
+ case audioPid:
324
+ if (stt) {
325
+ if (audioData && (pes = parsePES(audioData, this.logger))) {
326
+ switch (audioTrack.segmentCodec) {
327
+ case 'aac':
328
+ this.parseAACPES(audioTrack, pes);
329
+ break;
330
+ case 'mp3':
331
+ this.parseMPEGPES(audioTrack, pes);
332
+ break;
333
+ case 'ac3':
334
+ if (__USE_M2TS_ADVANCED_CODECS__) {
335
+ this.parseAC3PES(audioTrack, pes);
336
+ }
337
+ break;
338
+ }
339
+ }
340
+ audioData = { data: [], size: 0 };
341
+ }
342
+ if (audioData) {
343
+ audioData.data.push(data.subarray(offset, start + PACKET_LENGTH));
344
+ audioData.size += start + PACKET_LENGTH - offset;
345
+ }
346
+ break;
347
+ case id3Pid:
348
+ if (stt) {
349
+ if (id3Data && (pes = parsePES(id3Data, this.logger))) {
350
+ this.parseID3PES(id3Track, pes);
351
+ }
352
+
353
+ id3Data = { data: [], size: 0 };
354
+ }
355
+ if (id3Data) {
356
+ id3Data.data.push(data.subarray(offset, start + PACKET_LENGTH));
357
+ id3Data.size += start + PACKET_LENGTH - offset;
358
+ }
359
+ break;
360
+ case klvPid:
361
+ if (stt) {
362
+ if (klvData && (pes = parsePES(klvData, this.logger))) {
363
+ this.parseKlvPES(id3Track, pes);
364
+ }
365
+
366
+ klvData = { data: [], size: 0 };
367
+ }
368
+ if (klvData) {
369
+ klvData.data.push(data.subarray(offset, start + PACKET_LENGTH));
370
+ klvData.size += start + PACKET_LENGTH - offset;
371
+ }
372
+ break;
373
+ case 0:
374
+ if (stt) {
375
+ offset += data[offset] + 1;
376
+ }
377
+
378
+ pmtId = this._pmtId = parsePAT(data, offset);
379
+ // this.logger.log('PMT PID:' + this._pmtId);
380
+ break;
381
+ case pmtId: {
382
+ if (stt) {
383
+ offset += data[offset] + 1;
384
+ }
385
+
386
+ const parsedPIDs = parsePMT(
387
+ data,
388
+ offset,
389
+ this.typeSupported,
390
+ isSampleAes,
391
+ this.observer,
392
+ this.logger,
393
+ this.config,
394
+ );
395
+
396
+ // only update track id if track PID found while parsing PMT
397
+ // this is to avoid resetting the PID to -1 in case
398
+ // track PID transiently disappears from the stream
399
+ // this could happen in case of transient missing audio samples for example
400
+ // NOTE this is only the PID of the track as found in TS,
401
+ // but we are not using this for MP4 track IDs.
402
+ videoPid = parsedPIDs.videoPid;
403
+ if (videoPid > 0) {
404
+ videoTrack.pid = videoPid;
405
+ videoTrack.segmentCodec = parsedPIDs.segmentVideoCodec;
406
+ }
407
+
408
+ audioPid = parsedPIDs.audioPid;
409
+ if (audioPid > 0) {
410
+ audioTrack.pid = audioPid;
411
+ audioTrack.segmentCodec = parsedPIDs.segmentAudioCodec;
412
+ }
413
+ id3Pid = parsedPIDs.id3Pid;
414
+ if (id3Pid > 0) {
415
+ id3Track.pid = id3Pid;
416
+ }
417
+ klvPid = parsedPIDs.klvPid;
418
+ if (klvPid > 0) {
419
+ this._klvPid = klvPid;
420
+ }
421
+
422
+ if (unknownPID !== null && !pmtParsed) {
423
+ this.logger.warn(
424
+ `MPEG-TS PMT found at ${start} after unknown PID '${unknownPID}'. Backtracking to sync byte @${syncOffset} to parse all TS packets.`,
425
+ );
426
+ unknownPID = null;
427
+ // we set it to -188, the += 188 in the for loop will reset start to 0
428
+ start = syncOffset - 188;
429
+ }
430
+ pmtParsed = this.pmtParsed = true;
431
+ break;
432
+ }
433
+ case 0x11:
434
+ case 0x1fff:
435
+ break;
436
+ default:
437
+ unknownPID = pid;
438
+ break;
439
+ }
440
+ } else {
441
+ tsPacketErrors++;
442
+ }
443
+ }
444
+
445
+ if (tsPacketErrors > 0) {
446
+ emitParsingError(
447
+ this.observer,
448
+ new Error(
449
+ `Found ${tsPacketErrors} TS packet/s that do not start with 0x47`,
450
+ ),
451
+ undefined,
452
+ this.logger,
453
+ );
454
+ }
455
+
456
+ videoTrack.pesData = videoData;
457
+ audioTrack.pesData = audioData;
458
+ id3Track.pesData = id3Data;
459
+
460
+ const demuxResult: DemuxerResult = {
461
+ audioTrack,
462
+ videoTrack,
463
+ id3Track,
464
+ textTrack,
465
+ };
466
+
467
+ if (flush) {
468
+ this.extractRemainingSamples(demuxResult);
469
+ }
470
+
471
+ return demuxResult;
472
+ }
473
+
474
+ public flush(): DemuxerResult | Promise<DemuxerResult> {
475
+ const { remainderData } = this;
476
+ this.remainderData = null;
477
+ let result: DemuxerResult;
478
+ if (remainderData) {
479
+ result = this.demux(remainderData, -1, false, true);
480
+ } else {
481
+ result = {
482
+ videoTrack: this._videoTrack as DemuxedVideoTrack,
483
+ audioTrack: this._audioTrack as DemuxedAudioTrack,
484
+ id3Track: this._id3Track as DemuxedMetadataTrack,
485
+ textTrack: this._txtTrack as DemuxedUserdataTrack,
486
+ };
487
+ }
488
+ this.extractRemainingSamples(result);
489
+ if (this.sampleAes) {
490
+ return this.decrypt(result, this.sampleAes);
491
+ }
492
+ return result;
493
+ }
494
+
495
+ private extractRemainingSamples(demuxResult: DemuxerResult) {
496
+ const { audioTrack, videoTrack, id3Track, textTrack } = demuxResult;
497
+ const videoData = videoTrack.pesData;
498
+ const audioData = audioTrack.pesData;
499
+ const id3Data = id3Track.pesData;
500
+ const videoIntegrityChecker = this.videoIntegrityChecker;
501
+ // try to parse last PES packets
502
+ let pes: PES | null;
503
+ if (
504
+ videoData &&
505
+ !videoIntegrityChecker?.isCorrupted &&
506
+ (pes = parsePES(videoData, this.logger))
507
+ ) {
508
+ this.readyVideoParser(videoTrack.segmentCodec);
509
+ if (this.videoParser !== null) {
510
+ this.videoParser.parsePES(
511
+ videoTrack as DemuxedVideoTrack,
512
+ textTrack as DemuxedUserdataTrack,
513
+ pes,
514
+ true,
515
+ );
516
+ videoTrack.pesData = null;
517
+ }
518
+ } else {
519
+ // either avcData null or PES truncated, keep it for next frag parsing
520
+ videoTrack.pesData = videoData;
521
+ }
522
+
523
+ if (audioData && (pes = parsePES(audioData, this.logger))) {
524
+ switch (audioTrack.segmentCodec) {
525
+ case 'aac':
526
+ this.parseAACPES(audioTrack, pes);
527
+ break;
528
+ case 'mp3':
529
+ this.parseMPEGPES(audioTrack, pes);
530
+ break;
531
+ case 'ac3':
532
+ if (__USE_M2TS_ADVANCED_CODECS__) {
533
+ this.parseAC3PES(audioTrack, pes);
534
+ }
535
+ break;
536
+ }
537
+ audioTrack.pesData = null;
538
+ } else {
539
+ if (audioData?.size) {
540
+ this.logger.log(
541
+ 'last AAC PES packet truncated,might overlap between fragments',
542
+ );
543
+ }
544
+
545
+ // either audioData null or PES truncated, keep it for next frag parsing
546
+ audioTrack.pesData = audioData;
547
+ }
548
+
549
+ if (id3Data && (pes = parsePES(id3Data, this.logger))) {
550
+ this.parseID3PES(id3Track, pes);
551
+ id3Track.pesData = null;
552
+ } else {
553
+ // either id3Data null or PES truncated, keep it for next frag parsing
554
+ id3Track.pesData = id3Data;
555
+ }
556
+ }
557
+
558
+ public demuxSampleAes(
559
+ data: Uint8Array,
560
+ keyData: KeyData,
561
+ timeOffset: number,
562
+ ): Promise<DemuxerResult> {
563
+ const demuxResult = this.demux(
564
+ data,
565
+ timeOffset,
566
+ true,
567
+ !this.config.progressive,
568
+ );
569
+ const sampleAes = (this.sampleAes = new SampleAesDecrypter(
570
+ this.observer,
571
+ this.config,
572
+ keyData,
573
+ ));
574
+ return this.decrypt(demuxResult, sampleAes);
575
+ }
576
+
577
+ private readyVideoParser(codec: string | undefined) {
578
+ if (this.videoParser === null) {
579
+ if (codec === 'avc') {
580
+ this.videoParser = new AvcVideoParser();
581
+ } else if (__USE_M2TS_ADVANCED_CODECS__ && codec === 'hevc') {
582
+ this.videoParser = new HevcVideoParser();
583
+ }
584
+ }
585
+ }
586
+
587
+ private decrypt(
588
+ demuxResult: DemuxerResult,
589
+ sampleAes: SampleAesDecrypter,
590
+ ): Promise<DemuxerResult> {
591
+ return new Promise((resolve) => {
592
+ const { audioTrack, videoTrack } = demuxResult;
593
+ if (audioTrack.samples && audioTrack.segmentCodec === 'aac') {
594
+ sampleAes.decryptAacSamples(
595
+ (audioTrack as DemuxedAAC).samples,
596
+ 0,
597
+ () => {
598
+ if (videoTrack.samples) {
599
+ sampleAes.decryptAvcSamples(videoTrack.samples, 0, 0, () => {
600
+ resolve(demuxResult);
601
+ });
602
+ } else {
603
+ resolve(demuxResult);
604
+ }
605
+ },
606
+ );
607
+ } else if (videoTrack.samples) {
608
+ sampleAes.decryptAvcSamples(videoTrack.samples, 0, 0, () => {
609
+ resolve(demuxResult);
610
+ });
611
+ }
612
+ });
613
+ }
614
+
615
+ public destroy() {
616
+ if (this.observer) {
617
+ this.observer.removeAllListeners();
618
+ }
619
+ // @ts-ignore
620
+ this.config = this.logger = this.observer = null;
621
+ this.aacOverFlow =
622
+ this.videoParser =
623
+ this.remainderData =
624
+ this.sampleAes =
625
+ null;
626
+ this._videoTrack =
627
+ this._audioTrack =
628
+ this._id3Track =
629
+ this._txtTrack =
630
+ undefined;
631
+
632
+ this.videoIntegrityChecker = null;
633
+ }
634
+
635
+ private parseAACPES(track: DemuxedAudioTrack, pes: PES) {
636
+ let startOffset = 0;
637
+ const aacOverFlow = this.aacOverFlow;
638
+ let data = pes.data;
639
+ if (aacOverFlow) {
640
+ this.aacOverFlow = null;
641
+ const frameMissingBytes = aacOverFlow.missing;
642
+ const sampleLength = aacOverFlow.sample.unit.byteLength;
643
+ // logger.log(`AAC: append overflowing ${sampleLength} bytes to beginning of new PES`);
644
+ if (frameMissingBytes === -1) {
645
+ data = appendUint8Array(aacOverFlow.sample.unit, data);
646
+ } else {
647
+ const frameOverflowBytes = sampleLength - frameMissingBytes;
648
+ aacOverFlow.sample.unit.set(
649
+ data.subarray(0, frameMissingBytes),
650
+ frameOverflowBytes,
651
+ );
652
+ track.samples.push(aacOverFlow.sample);
653
+ startOffset = aacOverFlow.missing;
654
+ }
655
+ }
656
+ // look for ADTS header (0xFFFx)
657
+ let offset: number;
658
+ let len: number;
659
+ for (offset = startOffset, len = data.length; offset < len - 1; offset++) {
660
+ if (ADTS.isHeader(data, offset)) {
661
+ break;
662
+ }
663
+ }
664
+ // if ADTS header does not start straight from the beginning of the PES payload, raise an error
665
+ if (offset !== startOffset) {
666
+ let reason: string;
667
+ const recoverable = offset < len - 1;
668
+ if (recoverable) {
669
+ reason = `AAC PES did not start with ADTS header,offset:${offset}`;
670
+ } else {
671
+ reason = 'No ADTS header found in AAC PES';
672
+ }
673
+ emitParsingError(
674
+ this.observer,
675
+ new Error(reason),
676
+ recoverable,
677
+ this.logger,
678
+ );
679
+ if (!recoverable) {
680
+ return;
681
+ }
682
+ }
683
+
684
+ ADTS.initTrackConfig(track, this.observer, data, offset, this.audioCodec);
685
+
686
+ let pts: number;
687
+ if (pes.pts !== undefined) {
688
+ pts = pes.pts;
689
+ } else if (aacOverFlow) {
690
+ // if last AAC frame is overflowing, we should ensure timestamps are contiguous:
691
+ // first sample PTS should be equal to last sample PTS + frameDuration
692
+ const frameDuration = ADTS.getFrameDuration(track.samplerate as number);
693
+ pts = aacOverFlow.sample.pts + frameDuration;
694
+ } else {
695
+ this.logger.warn('[tsdemuxer]: AAC PES unknown PTS');
696
+ return;
697
+ }
698
+
699
+ // scan for aac samples
700
+ let frameIndex = 0;
701
+ let frame;
702
+ while (offset < len) {
703
+ frame = ADTS.appendFrame(track, data, offset, pts, frameIndex);
704
+ offset += frame.length;
705
+ if (!frame.missing) {
706
+ frameIndex++;
707
+ for (; offset < len - 1; offset++) {
708
+ if (ADTS.isHeader(data, offset)) {
709
+ break;
710
+ }
711
+ }
712
+ } else {
713
+ this.aacOverFlow = frame;
714
+ break;
715
+ }
716
+ }
717
+ }
718
+
719
+ private parseMPEGPES(track: DemuxedAudioTrack, pes: PES) {
720
+ const data = pes.data;
721
+ const length = data.length;
722
+ let frameIndex = 0;
723
+ let offset = 0;
724
+ const pts = pes.pts;
725
+ if (pts === undefined) {
726
+ this.logger.warn('[tsdemuxer]: MPEG PES unknown PTS');
727
+ return;
728
+ }
729
+
730
+ while (offset < length) {
731
+ if (MpegAudio.isHeader(data, offset)) {
732
+ const frame = MpegAudio.appendFrame(
733
+ track,
734
+ data,
735
+ offset,
736
+ pts,
737
+ frameIndex,
738
+ );
739
+ if (frame) {
740
+ offset += frame.length;
741
+ frameIndex++;
742
+ } else {
743
+ // logger.log('Unable to parse Mpeg audio frame');
744
+ break;
745
+ }
746
+ } else {
747
+ // nothing found, keep looking
748
+ offset++;
749
+ }
750
+ }
751
+ }
752
+
753
+ private parseAC3PES(track: DemuxedAudioTrack, pes: PES) {
754
+ if (__USE_M2TS_ADVANCED_CODECS__) {
755
+ const data = pes.data;
756
+ const pts = pes.pts;
757
+ if (pts === undefined) {
758
+ this.logger.warn('[tsdemuxer]: AC3 PES unknown PTS');
759
+ return;
760
+ }
761
+ const length = data.length;
762
+ let frameIndex = 0;
763
+ let offset = 0;
764
+ let parsed;
765
+
766
+ while (
767
+ offset < length &&
768
+ (parsed = AC3.appendFrame(track, data, offset, pts, frameIndex++)) > 0
769
+ ) {
770
+ offset += parsed;
771
+ }
772
+ }
773
+ }
774
+
775
+ private parseID3PES(id3Track: DemuxedMetadataTrack, pes: PES) {
776
+ if (pes.pts === undefined) {
777
+ this.logger.warn('[tsdemuxer]: ID3 PES unknown PTS');
778
+ return;
779
+ }
780
+ const id3Sample = Object.assign({}, pes as Required<PES>, {
781
+ type: this._videoTrack ? MetadataSchema.emsg : MetadataSchema.audioId3,
782
+ duration: Number.POSITIVE_INFINITY,
783
+ });
784
+ id3Track.samples.push(id3Sample);
785
+ }
786
+
787
+ private parseKlvPES(id3Track: DemuxedMetadataTrack, pes: PES) {
788
+ const pts = pes.pts ?? pes.dts;
789
+ if (pts === undefined) {
790
+ this.logger.warn('[tsdemuxer]: KLV PES unknown PTS and DTS');
791
+ return;
792
+ }
793
+ // Parse KLV data from PES payload
794
+ // KLV format: Key (16 bytes) + Length (BER encoded) + Value (variable)
795
+ const klvData = pes.data;
796
+ if (klvData.length < 16) {
797
+ this.logger.warn('[tsdemuxer]: KLV PES payload too short');
798
+ return;
799
+ }
800
+
801
+ // Extract KLV packets from the PES payload
802
+ // Multiple KLV packets can be in a single PES packet
803
+ let offset = 0;
804
+ while (offset < klvData.length) {
805
+ if (offset + 16 > klvData.length) {
806
+ break; // Not enough data for a KLV key
807
+ }
808
+
809
+ const klvStart = offset;
810
+
811
+ // Extract Key (16 bytes for MISB ST 0601)
812
+ offset += 16;
813
+
814
+ if (offset >= klvData.length) {
815
+ break; // No length field
816
+ }
817
+
818
+ // Parse BER-encoded Length
819
+ const firstByte = klvData[offset];
820
+ offset += 1;
821
+
822
+ let length = 0;
823
+
824
+ if (firstByte & 0x80) {
825
+ // Long form BER encoding
826
+ const lengthBytes = firstByte & 0x7f;
827
+ if (
828
+ lengthBytes === 0 ||
829
+ lengthBytes > 4 ||
830
+ offset + lengthBytes > klvData.length
831
+ ) {
832
+ this.logger.warn('[tsdemuxer]: Invalid KLV length encoding');
833
+ break;
834
+ }
835
+ for (let i = 0; i < lengthBytes; i++) {
836
+ length = (length << 8) | klvData[offset + i];
837
+ }
838
+ offset += lengthBytes;
839
+ } else {
840
+ // Short form BER encoding
841
+ length = firstByte;
842
+ }
843
+
844
+ if (offset + length > klvData.length) {
845
+ this.logger.warn('[tsdemuxer]: KLV value extends beyond PES payload');
846
+ break;
847
+ }
848
+
849
+ // Extract the complete KLV packet (Key + Length + Value)
850
+ const klvEnd = offset + length;
851
+ const klvPacket = klvData.subarray(klvStart, klvEnd);
852
+
853
+ const klvSample = {
854
+ data: klvPacket,
855
+ len: klvPacket.byteLength,
856
+ pts: pts,
857
+ dts: pes.dts ?? pts,
858
+ type: MetadataSchema.misbklv,
859
+ duration: Number.POSITIVE_INFINITY,
860
+ };
861
+ id3Track.samples.push(klvSample);
862
+
863
+ offset = klvEnd;
864
+ }
865
+ }
866
+ }
867
+
868
+ function parsePID(data: Uint8Array, offset: number): number {
869
+ // pid is a 13-bit field starting at the last bit of TS[1]
870
+ return ((data[offset + 1] & 0x1f) << 8) + data[offset + 2];
871
+ }
872
+
873
+ function parsePAT(data: Uint8Array, offset: number): number {
874
+ // skip the PSI header and parse the first PMT entry
875
+ return ((data[offset + 10] & 0x1f) << 8) | data[offset + 11];
876
+ }
877
+
878
+ function parsePMT(
879
+ data: Uint8Array,
880
+ offset: number,
881
+ typeSupported: TypeSupported,
882
+ isSampleAes: boolean,
883
+ observer: HlsEventEmitter,
884
+ logger: ILogger,
885
+ config: HlsConfig,
886
+ ) {
887
+ const result = {
888
+ audioPid: -1,
889
+ videoPid: -1,
890
+ id3Pid: -1,
891
+ klvPid: -1,
892
+ segmentVideoCodec: 'avc' as 'avc' | 'hevc',
893
+ segmentAudioCodec: 'aac' as 'aac' | 'ac3' | 'mp3',
894
+ };
895
+ const sectionLength = ((data[offset + 1] & 0x0f) << 8) | data[offset + 2];
896
+ const tableEnd = offset + 3 + sectionLength - 4;
897
+ // to determine where the table is, we have to figure out how
898
+ // long the program info descriptors are
899
+ const programInfoLength =
900
+ ((data[offset + 10] & 0x0f) << 8) | data[offset + 11];
901
+ // advance the offset to the first entry in the mapping table
902
+ offset += 12 + programInfoLength;
903
+ while (offset < tableEnd) {
904
+ const pid = parsePID(data, offset);
905
+ const esInfoLength = ((data[offset + 3] & 0x0f) << 8) | data[offset + 4];
906
+ switch (data[offset]) {
907
+ case 0xcf: // SAMPLE-AES AAC
908
+ if (!isSampleAes) {
909
+ logEncryptedSamplesFoundInUnencryptedStream('ADTS AAC', logger);
910
+ break;
911
+ }
912
+ /* falls through */
913
+ case 0x0f: // ISO/IEC 13818-7 ADTS AAC (MPEG-2 lower bit-rate audio)
914
+ // logger.log('AAC PID:' + pid);
915
+ if (result.audioPid === -1) {
916
+ result.audioPid = pid;
917
+ }
918
+
919
+ break;
920
+
921
+ // Packetized metadata (ID3)
922
+ case 0x15:
923
+ // logger.log('ID3 PID:' + pid);
924
+ if (result.id3Pid === -1) {
925
+ result.id3Pid = pid;
926
+ }
927
+
928
+ break;
929
+
930
+ case 0xdb: // SAMPLE-AES AVC
931
+ if (!isSampleAes) {
932
+ logEncryptedSamplesFoundInUnencryptedStream('H.264', logger);
933
+ break;
934
+ }
935
+ /* falls through */
936
+ case 0x1b: // ITU-T Rec. H.264 and ISO/IEC 14496-10 (lower bit-rate video)
937
+ // logger.log('AVC PID:' + pid);
938
+ if (result.videoPid === -1) {
939
+ result.videoPid = pid;
940
+ }
941
+
942
+ break;
943
+
944
+ // ISO/IEC 11172-3 (MPEG-1 audio)
945
+ // or ISO/IEC 13818-3 (MPEG-2 halved sample rate audio)
946
+ case 0x03:
947
+ case 0x04:
948
+ // logger.log('MPEG PID:' + pid);
949
+ if (!typeSupported.mpeg && !typeSupported.mp3) {
950
+ logger.log('MPEG audio found, not supported in this browser');
951
+ } else if (result.audioPid === -1) {
952
+ result.audioPid = pid;
953
+ result.segmentAudioCodec = 'mp3';
954
+ }
955
+ break;
956
+
957
+ case 0xc1: // SAMPLE-AES AC3
958
+ if (!isSampleAes) {
959
+ logEncryptedSamplesFoundInUnencryptedStream('AC-3', logger);
960
+ break;
961
+ }
962
+ /* falls through */
963
+ case 0x81:
964
+ if (__USE_M2TS_ADVANCED_CODECS__) {
965
+ if (!typeSupported.ac3) {
966
+ logger.log('AC-3 audio found, not supported in this browser');
967
+ } else if (result.audioPid === -1) {
968
+ result.audioPid = pid;
969
+ result.segmentAudioCodec = 'ac3';
970
+ }
971
+ } else {
972
+ logger.warn('AC-3 in M2TS support not included in build');
973
+ }
974
+ break;
975
+
976
+ case 0x06:
977
+ // stream_type 6 can mean a lot of different things in case of DVB.
978
+ // We need to look at the descriptors. Right now, we're only interested
979
+ // in AC-3 audio, so we do the descriptor parsing only when we don't have
980
+ // an audio PID yet.
981
+ if (esInfoLength > 0) {
982
+ let parsePos = offset + 5;
983
+ let remaining = esInfoLength;
984
+
985
+ while (remaining > 2) {
986
+ const descriptorId = data[parsePos];
987
+
988
+ switch (descriptorId) {
989
+ case 0x6a: // DVB Descriptor for AC-3
990
+ if (result.audioPid === -1) {
991
+ if (__USE_M2TS_ADVANCED_CODECS__) {
992
+ if (typeSupported.ac3 !== true) {
993
+ logger.log(
994
+ 'AC-3 audio found, not supported in this browser for now',
995
+ );
996
+ } else {
997
+ result.audioPid = pid;
998
+ result.segmentAudioCodec = 'ac3';
999
+ }
1000
+ } else {
1001
+ logger.warn('AC-3 in M2TS support not included in build');
1002
+ }
1003
+ }
1004
+ break;
1005
+ case 0x05: // MISB KLV descriptor
1006
+ if (result.klvPid === -1 && config.enableEmsgKLVMetadata) {
1007
+ result.klvPid = pid;
1008
+ logger.log(`KLV metadata PID found: ${pid}`);
1009
+ }
1010
+ break;
1011
+ }
1012
+
1013
+ const descriptorLen = data[parsePos + 1] + 2;
1014
+ parsePos += descriptorLen;
1015
+ remaining -= descriptorLen;
1016
+ }
1017
+ }
1018
+ break;
1019
+
1020
+ case 0xc2: // SAMPLE-AES EC3
1021
+ /* falls through */
1022
+ case 0x87:
1023
+ emitParsingError(
1024
+ observer,
1025
+ new Error('Unsupported EC-3 in M2TS found'),
1026
+ undefined,
1027
+ logger,
1028
+ );
1029
+ return result;
1030
+
1031
+ case 0x24: // ITU-T Rec. H.265 and ISO/IEC 23008-2 (HEVC)
1032
+ if (__USE_M2TS_ADVANCED_CODECS__) {
1033
+ if (result.videoPid === -1) {
1034
+ result.videoPid = pid;
1035
+ result.segmentVideoCodec = 'hevc';
1036
+ logger.log('HEVC in M2TS found');
1037
+ }
1038
+ } else {
1039
+ emitParsingError(
1040
+ observer,
1041
+ new Error('Unsupported HEVC in M2TS found'),
1042
+ undefined,
1043
+ logger,
1044
+ );
1045
+ return result;
1046
+ }
1047
+ break;
1048
+
1049
+ default:
1050
+ // logger.log('unknown stream type:' + data[offset]);
1051
+ break;
1052
+ }
1053
+ // move to the next table entry
1054
+ // skip past the elementary stream descriptors, if present
1055
+ offset += esInfoLength + 5;
1056
+ }
1057
+ return result;
1058
+ }
1059
+
1060
+ function emitParsingError(
1061
+ observer: HlsEventEmitter,
1062
+ error: Error,
1063
+ levelRetry: boolean | undefined,
1064
+ logger: ILogger,
1065
+ ) {
1066
+ logger.warn(`parsing error: ${error.message}`);
1067
+ observer.emit(Events.ERROR, Events.ERROR, {
1068
+ type: ErrorTypes.MEDIA_ERROR,
1069
+ details: ErrorDetails.FRAG_PARSING_ERROR,
1070
+ fatal: false,
1071
+ levelRetry,
1072
+ error,
1073
+ reason: error.message,
1074
+ });
1075
+ }
1076
+
1077
+ function logEncryptedSamplesFoundInUnencryptedStream(
1078
+ type: string,
1079
+ logger: ILogger,
1080
+ ) {
1081
+ logger.log(`${type} with AES-128-CBC encryption found in unencrypted stream`);
1082
+ }
1083
+
1084
+ function parsePES(stream: ElementaryStreamData, logger: ILogger): PES | null {
1085
+ let i = 0;
1086
+ let frag: Uint8Array;
1087
+ let pesLen: number;
1088
+ let pesHdrLen: number;
1089
+ let pesPts: number | undefined;
1090
+ let pesDts: number | undefined;
1091
+ const data = stream.data;
1092
+ // safety check
1093
+ if (!stream || stream.size === 0) {
1094
+ return null;
1095
+ }
1096
+
1097
+ // we might need up to 19 bytes to read PES header
1098
+ // if first chunk of data is less than 19 bytes, let's merge it with following ones until we get 19 bytes
1099
+ // usually only one merge is needed (and this is rare ...)
1100
+ while (data[0].length < 19 && data.length > 1) {
1101
+ data[0] = appendUint8Array(data[0], data[1]);
1102
+ data.splice(1, 1);
1103
+ }
1104
+ // retrieve PTS/DTS from first fragment
1105
+ frag = data[0];
1106
+ const pesPrefix = (frag[0] << 16) + (frag[1] << 8) + frag[2];
1107
+ if (pesPrefix === 1) {
1108
+ pesLen = (frag[4] << 8) + frag[5];
1109
+ // if PES parsed length is not zero and greater than total received length, stop parsing. PES might be truncated
1110
+ // minus 6 : PES header size
1111
+ if (pesLen && pesLen > stream.size - 6) {
1112
+ return null;
1113
+ }
1114
+
1115
+ const pesFlags = frag[7];
1116
+ if (pesFlags & 0xc0) {
1117
+ /* PES header described here : http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
1118
+ as PTS / DTS is 33 bit we cannot use bitwise operator in JS,
1119
+ as Bitwise operators treat their operands as a sequence of 32 bits */
1120
+ pesPts =
1121
+ (frag[9] & 0x0e) * 536870912 + // 1 << 29
1122
+ (frag[10] & 0xff) * 4194304 + // 1 << 22
1123
+ (frag[11] & 0xfe) * 16384 + // 1 << 14
1124
+ (frag[12] & 0xff) * 128 + // 1 << 7
1125
+ (frag[13] & 0xfe) / 2;
1126
+
1127
+ if (pesFlags & 0x40) {
1128
+ pesDts =
1129
+ (frag[14] & 0x0e) * 536870912 + // 1 << 29
1130
+ (frag[15] & 0xff) * 4194304 + // 1 << 22
1131
+ (frag[16] & 0xfe) * 16384 + // 1 << 14
1132
+ (frag[17] & 0xff) * 128 + // 1 << 7
1133
+ (frag[18] & 0xfe) / 2;
1134
+
1135
+ if (pesPts - pesDts > 60 * 90000) {
1136
+ logger.warn(
1137
+ `${Math.round(
1138
+ (pesPts - pesDts) / 90000,
1139
+ )}s delta between PTS and DTS, align them`,
1140
+ );
1141
+ pesPts = pesDts;
1142
+ }
1143
+ } else {
1144
+ pesDts = pesPts;
1145
+ }
1146
+ }
1147
+ pesHdrLen = frag[8];
1148
+ // 9 bytes : 6 bytes for PES header + 3 bytes for PES extension
1149
+ let payloadStartOffset = pesHdrLen + 9;
1150
+ if (stream.size <= payloadStartOffset) {
1151
+ return null;
1152
+ }
1153
+ stream.size -= payloadStartOffset;
1154
+ // reassemble PES packet
1155
+ const pesData = new Uint8Array(stream.size);
1156
+ for (let j = 0, dataLen = data.length; j < dataLen; j++) {
1157
+ frag = data[j];
1158
+ let len = frag.byteLength;
1159
+ if (payloadStartOffset) {
1160
+ if (payloadStartOffset > len) {
1161
+ // trim full frag if PES header bigger than frag
1162
+ payloadStartOffset -= len;
1163
+ continue;
1164
+ } else {
1165
+ // trim partial frag if PES header smaller than frag
1166
+ frag = frag.subarray(payloadStartOffset);
1167
+ len -= payloadStartOffset;
1168
+ payloadStartOffset = 0;
1169
+ }
1170
+ }
1171
+ pesData.set(frag, i);
1172
+ i += len;
1173
+ }
1174
+ if (pesLen) {
1175
+ // payload size : remove PES header + PES extension
1176
+ pesLen -= pesHdrLen + 3;
1177
+ }
1178
+ return { data: pesData, pts: pesPts, dts: pesDts, len: pesLen };
1179
+ }
1180
+ return null;
1181
+ }
1182
+
1183
+ // See FFMpeg for reference: https://github.com/FFmpeg/FFmpeg/blob/e4c8e80a2efee275f2a10fcf0424c9fc1d86e309/libavformat/mpegts.c#L2811-L2834
1184
+ class PacketsIntegrityChecker {
1185
+ private readonly logger: ILogger;
1186
+
1187
+ private pid: number = 0;
1188
+ private lastContinuityCounter = -1;
1189
+ private integrityState: 'ok' | 'tei-bit' | 'cc-failed' = 'ok';
1190
+
1191
+ constructor(logger: ILogger) {
1192
+ this.logger = logger;
1193
+ }
1194
+
1195
+ public get isCorrupted(): boolean {
1196
+ return this.integrityState !== 'ok';
1197
+ }
1198
+
1199
+ public reset(pid: number) {
1200
+ this.pid = pid;
1201
+ this.lastContinuityCounter = -1;
1202
+ this.integrityState = 'ok';
1203
+ }
1204
+
1205
+ public handlePacket(data: Uint8Array) {
1206
+ if (data.byteLength < 4) {
1207
+ return;
1208
+ }
1209
+
1210
+ const pid = parsePID(data, 0);
1211
+ if (pid !== this.pid) {
1212
+ this.logger.debug(`Packet PID mismatch, expected ${this.pid} got ${pid}`);
1213
+ return;
1214
+ }
1215
+
1216
+ const adaptationFieldControl = (data[3] & 0x30) >> 4;
1217
+ if (adaptationFieldControl === 0) {
1218
+ return;
1219
+ }
1220
+ const continuityCounter = data[3] & 0xf;
1221
+
1222
+ const lastContinuityCounter = this.lastContinuityCounter;
1223
+ this.lastContinuityCounter = continuityCounter;
1224
+
1225
+ const hasPayload = (adaptationFieldControl & 0b01) != 0;
1226
+ const hasAdaptation = (adaptationFieldControl & 0b10) != 0;
1227
+ const isDiscontinuity =
1228
+ hasAdaptation && data[4] != 0 && (data[5] & 0x80) != 0;
1229
+
1230
+ if (isDiscontinuity) {
1231
+ return;
1232
+ }
1233
+ if (lastContinuityCounter < 0) {
1234
+ return;
1235
+ }
1236
+
1237
+ const expectedContinuityCounter = hasPayload
1238
+ ? (lastContinuityCounter + 1) & 0x0f
1239
+ : lastContinuityCounter;
1240
+ if (continuityCounter !== expectedContinuityCounter) {
1241
+ this.logger.warn(
1242
+ `MPEG-TS Continuity Counter check failed for PID='${pid}', CC=${continuityCounter}, Expected-CC=${expectedContinuityCounter} Last-CC=${lastContinuityCounter}`,
1243
+ );
1244
+ this.integrityState = 'cc-failed';
1245
+ return;
1246
+ }
1247
+
1248
+ if ((data[1] & 0x80) !== 0) {
1249
+ this.logger.warn(`MPEG-TS Packet had TEI flag set for PID='${pid}'`);
1250
+ this.integrityState = 'tei-bit';
1251
+ return;
1252
+ }
1253
+ }
1254
+ }
1255
+
1256
+ export default TSDemuxer;