@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,434 @@
|
|
|
1
|
+
import {
|
|
2
|
+
flushTextTrackMetadataCueSamples,
|
|
3
|
+
flushTextTrackUserdataCueSamples,
|
|
4
|
+
} from './mp4-remuxer';
|
|
5
|
+
import { ElementaryStreamTypes } from '../loader/fragment';
|
|
6
|
+
import { getCodecCompatibleName } from '../utils/codecs';
|
|
7
|
+
import { type ILogger, Logger } from '../utils/logger';
|
|
8
|
+
import { patchEncyptionData } from '../utils/mp4-tools';
|
|
9
|
+
import { getSampleData, parseInitSegment } from '../utils/mp4-tools';
|
|
10
|
+
import type { HlsConfig } from '../config';
|
|
11
|
+
import type { HlsEventEmitter } from '../events';
|
|
12
|
+
import type { DecryptData } from '../loader/level-key';
|
|
13
|
+
import type {
|
|
14
|
+
DemuxedAudioTrack,
|
|
15
|
+
DemuxedMetadataTrack,
|
|
16
|
+
DemuxedUserdataTrack,
|
|
17
|
+
PassthroughTrack,
|
|
18
|
+
} from '../types/demuxer';
|
|
19
|
+
import type {
|
|
20
|
+
InitSegmentData,
|
|
21
|
+
RemuxedTrack,
|
|
22
|
+
Remuxer,
|
|
23
|
+
RemuxerResult,
|
|
24
|
+
} from '../types/remuxer';
|
|
25
|
+
import type { TrackSet } from '../types/track';
|
|
26
|
+
import type { TypeSupported } from '../utils/codecs';
|
|
27
|
+
import type { InitData, InitDataTrack, TrackTimes } from '../utils/mp4-tools';
|
|
28
|
+
import type { TimestampOffset } from '../utils/timescale-conversion';
|
|
29
|
+
|
|
30
|
+
class PassThroughRemuxer extends Logger implements Remuxer {
|
|
31
|
+
private emitInitSegment: boolean = false;
|
|
32
|
+
private audioCodec?: string;
|
|
33
|
+
private videoCodec?: string;
|
|
34
|
+
private initData?: InitData;
|
|
35
|
+
private initPTS: TimestampOffset | null = null;
|
|
36
|
+
private initTracks?: TrackSet;
|
|
37
|
+
private lastEndTime: number | null = null;
|
|
38
|
+
private isVideoContiguous: boolean = false;
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
observer: HlsEventEmitter,
|
|
42
|
+
config: HlsConfig,
|
|
43
|
+
typeSupported: TypeSupported,
|
|
44
|
+
logger: ILogger,
|
|
45
|
+
) {
|
|
46
|
+
super('passthrough-remuxer', logger);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public destroy() {}
|
|
50
|
+
|
|
51
|
+
public resetTimeStamp(defaultInitPTS: TimestampOffset | null) {
|
|
52
|
+
this.lastEndTime = null;
|
|
53
|
+
const initPTS = this.initPTS;
|
|
54
|
+
if (initPTS && defaultInitPTS) {
|
|
55
|
+
if (
|
|
56
|
+
initPTS.baseTime === defaultInitPTS.baseTime &&
|
|
57
|
+
initPTS.timescale === defaultInitPTS.timescale
|
|
58
|
+
) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
this.initPTS = defaultInitPTS;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public resetNextTimestamp() {
|
|
66
|
+
this.isVideoContiguous = false;
|
|
67
|
+
this.lastEndTime = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public resetInitSegment(
|
|
71
|
+
initSegment: Uint8Array<ArrayBuffer> | undefined,
|
|
72
|
+
audioCodec: string | undefined,
|
|
73
|
+
videoCodec: string | undefined,
|
|
74
|
+
decryptdata: DecryptData | null,
|
|
75
|
+
) {
|
|
76
|
+
this.audioCodec = audioCodec;
|
|
77
|
+
this.videoCodec = videoCodec;
|
|
78
|
+
this.generateInitSegment(initSegment, decryptdata);
|
|
79
|
+
this.emitInitSegment = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private generateInitSegment(
|
|
83
|
+
initSegment: Uint8Array<ArrayBuffer> | undefined,
|
|
84
|
+
decryptdata?: DecryptData | null,
|
|
85
|
+
) {
|
|
86
|
+
let { audioCodec, videoCodec } = this;
|
|
87
|
+
if (!initSegment?.byteLength) {
|
|
88
|
+
this.initTracks = undefined;
|
|
89
|
+
this.initData = undefined;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const { audio, video } = (this.initData = parseInitSegment(initSegment));
|
|
93
|
+
|
|
94
|
+
if (decryptdata) {
|
|
95
|
+
patchEncyptionData(initSegment, decryptdata);
|
|
96
|
+
} else {
|
|
97
|
+
const eitherTrack = audio || video;
|
|
98
|
+
if (eitherTrack?.encrypted) {
|
|
99
|
+
this.warn(
|
|
100
|
+
`Init segment with encrypted track with has no key ("${eitherTrack.codec}")!`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Get codec from initSegment
|
|
106
|
+
if (audio) {
|
|
107
|
+
audioCodec = getParsedTrackCodec(
|
|
108
|
+
audio,
|
|
109
|
+
ElementaryStreamTypes.AUDIO,
|
|
110
|
+
this,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (video) {
|
|
115
|
+
videoCodec = getParsedTrackCodec(
|
|
116
|
+
video,
|
|
117
|
+
ElementaryStreamTypes.VIDEO,
|
|
118
|
+
this,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const tracks: TrackSet = {};
|
|
123
|
+
if (audio && video) {
|
|
124
|
+
tracks.audiovideo = {
|
|
125
|
+
container: 'video/mp4',
|
|
126
|
+
codec: audioCodec + ',' + videoCodec,
|
|
127
|
+
supplemental: video.supplemental,
|
|
128
|
+
encrypted: video.encrypted,
|
|
129
|
+
initSegment,
|
|
130
|
+
id: 'main',
|
|
131
|
+
};
|
|
132
|
+
} else if (audio) {
|
|
133
|
+
tracks.audio = {
|
|
134
|
+
container: 'audio/mp4',
|
|
135
|
+
codec: audioCodec,
|
|
136
|
+
encrypted: audio.encrypted,
|
|
137
|
+
initSegment,
|
|
138
|
+
id: 'audio',
|
|
139
|
+
};
|
|
140
|
+
} else if (video) {
|
|
141
|
+
tracks.video = {
|
|
142
|
+
container: 'video/mp4',
|
|
143
|
+
codec: videoCodec,
|
|
144
|
+
supplemental: video.supplemental,
|
|
145
|
+
encrypted: video.encrypted,
|
|
146
|
+
initSegment,
|
|
147
|
+
id: 'main',
|
|
148
|
+
};
|
|
149
|
+
} else {
|
|
150
|
+
this.warn('initSegment does not contain moov or trak boxes.');
|
|
151
|
+
}
|
|
152
|
+
this.initTracks = tracks;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public remux(
|
|
156
|
+
audioTrack: DemuxedAudioTrack,
|
|
157
|
+
videoTrack: PassthroughTrack,
|
|
158
|
+
id3Track: DemuxedMetadataTrack,
|
|
159
|
+
textTrack: DemuxedUserdataTrack,
|
|
160
|
+
timeOffset: number,
|
|
161
|
+
accurateTimeOffset: boolean,
|
|
162
|
+
): RemuxerResult {
|
|
163
|
+
let { initPTS, lastEndTime } = this;
|
|
164
|
+
const result: RemuxerResult = {
|
|
165
|
+
audio: undefined,
|
|
166
|
+
video: undefined,
|
|
167
|
+
text: undefined,
|
|
168
|
+
id3: id3Track,
|
|
169
|
+
initSegment: undefined,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// If we haven't yet set a lastEndDTS, or it was reset, set it to the provided timeOffset. We want to use the
|
|
173
|
+
// lastEndDTS over timeOffset whenever possible; during progressive playback, the media source will not update
|
|
174
|
+
// the media duration (which is what timeOffset is provided as) before we need to process the next chunk.
|
|
175
|
+
if (!Number.isFinite(lastEndTime!)) {
|
|
176
|
+
lastEndTime = this.lastEndTime = timeOffset || 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// The binary segment data is added to the videoTrack in the mp4demuxer. We don't check to see if the data is only
|
|
180
|
+
// audio or video (or both); adding it to video was an arbitrary choice.
|
|
181
|
+
const data = videoTrack.samples;
|
|
182
|
+
if (!data.length) {
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const initSegment: InitSegmentData = {
|
|
187
|
+
initPTS: undefined,
|
|
188
|
+
timescale: undefined,
|
|
189
|
+
trackId: undefined,
|
|
190
|
+
};
|
|
191
|
+
let initData = this.initData;
|
|
192
|
+
if (!initData?.length) {
|
|
193
|
+
this.generateInitSegment(data);
|
|
194
|
+
initData = this.initData;
|
|
195
|
+
}
|
|
196
|
+
if (!initData?.length) {
|
|
197
|
+
// We can't remux if the initSegment could not be generated
|
|
198
|
+
this.warn('Failed to generate initSegment.');
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
if (this.emitInitSegment) {
|
|
202
|
+
initSegment.tracks = this.initTracks;
|
|
203
|
+
this.emitInitSegment = false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const trackSampleData = getSampleData(data, initData, this);
|
|
207
|
+
const audioSampleTimestamps = initData.audio
|
|
208
|
+
? trackSampleData[initData.audio.id]
|
|
209
|
+
: null;
|
|
210
|
+
const videoSampleTimestamps = initData.video
|
|
211
|
+
? trackSampleData[initData.video.id]
|
|
212
|
+
: null;
|
|
213
|
+
|
|
214
|
+
const videoStartTime = toStartEndOrDefault(videoSampleTimestamps, Infinity);
|
|
215
|
+
const audioStartTime = toStartEndOrDefault(audioSampleTimestamps, Infinity);
|
|
216
|
+
const videoEndTime = toStartEndOrDefault(videoSampleTimestamps, 0, true);
|
|
217
|
+
const audioEndTime = toStartEndOrDefault(audioSampleTimestamps, 0, true);
|
|
218
|
+
|
|
219
|
+
let decodeTime = timeOffset;
|
|
220
|
+
let duration = 0;
|
|
221
|
+
|
|
222
|
+
const syncOnAudio =
|
|
223
|
+
audioSampleTimestamps &&
|
|
224
|
+
(!videoSampleTimestamps ||
|
|
225
|
+
(!initPTS && audioStartTime < videoStartTime) ||
|
|
226
|
+
(initPTS && initPTS.trackId === initData.audio!.id));
|
|
227
|
+
const baseOffsetSamples = syncOnAudio
|
|
228
|
+
? audioSampleTimestamps
|
|
229
|
+
: videoSampleTimestamps;
|
|
230
|
+
|
|
231
|
+
if (baseOffsetSamples) {
|
|
232
|
+
const timescale = baseOffsetSamples.timescale;
|
|
233
|
+
const baseTime = baseOffsetSamples.start - timeOffset * timescale;
|
|
234
|
+
const trackId = syncOnAudio ? initData.audio!.id : initData.video!.id;
|
|
235
|
+
|
|
236
|
+
decodeTime = baseOffsetSamples.start / timescale;
|
|
237
|
+
duration = syncOnAudio
|
|
238
|
+
? audioEndTime - audioStartTime
|
|
239
|
+
: videoEndTime - videoStartTime;
|
|
240
|
+
|
|
241
|
+
if (
|
|
242
|
+
(accurateTimeOffset || !initPTS) &&
|
|
243
|
+
(isInvalidInitPts(initPTS, decodeTime, timeOffset, duration) ||
|
|
244
|
+
timescale !== initPTS.timescale)
|
|
245
|
+
) {
|
|
246
|
+
if (initPTS) {
|
|
247
|
+
this.warn(
|
|
248
|
+
`Timestamps at playlist time: ${accurateTimeOffset ? '' : '~'}${timeOffset} ${baseTime / timescale} != initPTS: ${initPTS.baseTime / initPTS.timescale} (${initPTS.baseTime}/${initPTS.timescale}) trackId: ${initPTS.trackId}`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
this.log(
|
|
252
|
+
`Found initPTS at playlist time: ${timeOffset} offset: ${decodeTime - timeOffset} (${baseTime}/${timescale}) trackId: ${trackId}`,
|
|
253
|
+
);
|
|
254
|
+
initPTS = null;
|
|
255
|
+
initSegment.initPTS = baseTime;
|
|
256
|
+
initSegment.timescale = timescale;
|
|
257
|
+
initSegment.trackId = trackId;
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
this.warn(
|
|
261
|
+
`No audio or video samples found for initPTS at playlist time: ${timeOffset}`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (!initPTS) {
|
|
265
|
+
if (
|
|
266
|
+
!initSegment.timescale ||
|
|
267
|
+
initSegment.trackId === undefined ||
|
|
268
|
+
initSegment.initPTS === undefined
|
|
269
|
+
) {
|
|
270
|
+
this.warn('Could not set initPTS');
|
|
271
|
+
initSegment.initPTS = decodeTime;
|
|
272
|
+
initSegment.timescale = 1;
|
|
273
|
+
initSegment.trackId = -1;
|
|
274
|
+
}
|
|
275
|
+
this.initPTS = initPTS = {
|
|
276
|
+
baseTime: initSegment.initPTS,
|
|
277
|
+
timescale: initSegment.timescale,
|
|
278
|
+
trackId: initSegment.trackId,
|
|
279
|
+
};
|
|
280
|
+
} else {
|
|
281
|
+
initSegment.initPTS = initPTS.baseTime;
|
|
282
|
+
initSegment.timescale = initPTS.timescale;
|
|
283
|
+
initSegment.trackId = initPTS.trackId;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const startTime = decodeTime - initPTS.baseTime / initPTS.timescale;
|
|
287
|
+
const endTime = startTime + duration;
|
|
288
|
+
|
|
289
|
+
if (duration > 0) {
|
|
290
|
+
this.lastEndTime = endTime;
|
|
291
|
+
} else {
|
|
292
|
+
this.warn('Duration parsed from mp4 should be greater than zero');
|
|
293
|
+
this.resetNextTimestamp();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const hasAudio = !!initData.audio;
|
|
297
|
+
const hasVideo = !!initData.video;
|
|
298
|
+
|
|
299
|
+
let type: any = '';
|
|
300
|
+
if (hasAudio) {
|
|
301
|
+
type += 'audio';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (hasVideo) {
|
|
305
|
+
type += 'video';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const encrypted =
|
|
309
|
+
(initData.audio ? initData.audio.encrypted : false) ||
|
|
310
|
+
(initData.video ? initData.video.encrypted : false);
|
|
311
|
+
|
|
312
|
+
const track: RemuxedTrack = {
|
|
313
|
+
data1: data,
|
|
314
|
+
startPTS: startTime,
|
|
315
|
+
startDTS: startTime,
|
|
316
|
+
endPTS: endTime,
|
|
317
|
+
endDTS: endTime,
|
|
318
|
+
type,
|
|
319
|
+
hasAudio,
|
|
320
|
+
hasVideo,
|
|
321
|
+
nb: 1,
|
|
322
|
+
dropped: 0,
|
|
323
|
+
encrypted,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
result.audio = hasAudio && !hasVideo ? track : undefined;
|
|
327
|
+
result.video = hasVideo ? track : undefined;
|
|
328
|
+
const videoSampleCount = videoSampleTimestamps?.sampleCount;
|
|
329
|
+
if (videoSampleCount) {
|
|
330
|
+
const firstKeyFrame = videoSampleTimestamps.keyFrameIndex;
|
|
331
|
+
const independent = firstKeyFrame !== -1;
|
|
332
|
+
track.nb = videoSampleCount;
|
|
333
|
+
track.dropped =
|
|
334
|
+
firstKeyFrame === 0 || this.isVideoContiguous
|
|
335
|
+
? 0
|
|
336
|
+
: independent
|
|
337
|
+
? firstKeyFrame
|
|
338
|
+
: videoSampleCount;
|
|
339
|
+
track.independent = independent;
|
|
340
|
+
track.firstKeyFrame = firstKeyFrame;
|
|
341
|
+
if (independent && videoSampleTimestamps.keyFrameStart) {
|
|
342
|
+
track.firstKeyFramePTS =
|
|
343
|
+
(videoSampleTimestamps.keyFrameStart - initPTS.baseTime) /
|
|
344
|
+
initPTS.timescale;
|
|
345
|
+
}
|
|
346
|
+
if (!this.isVideoContiguous) {
|
|
347
|
+
result.independent = independent;
|
|
348
|
+
}
|
|
349
|
+
this.isVideoContiguous ||= independent;
|
|
350
|
+
if (track.dropped) {
|
|
351
|
+
this.warn(
|
|
352
|
+
`fmp4 does not start with IDR: firstIDR ${firstKeyFrame}/${videoSampleCount} dropped: ${track.dropped} start: ${track.firstKeyFramePTS || 'NA'}`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
result.initSegment = initSegment;
|
|
358
|
+
result.id3 = flushTextTrackMetadataCueSamples(
|
|
359
|
+
id3Track,
|
|
360
|
+
timeOffset,
|
|
361
|
+
initPTS,
|
|
362
|
+
initPTS,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
if (textTrack.samples.length) {
|
|
366
|
+
result.text = flushTextTrackUserdataCueSamples(
|
|
367
|
+
textTrack,
|
|
368
|
+
timeOffset,
|
|
369
|
+
initPTS,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function toStartEndOrDefault(
|
|
378
|
+
trackTimes: TrackTimes | null,
|
|
379
|
+
defaultValue: number,
|
|
380
|
+
end: boolean = false,
|
|
381
|
+
): number {
|
|
382
|
+
return trackTimes?.start !== undefined
|
|
383
|
+
? (trackTimes.start + (end ? trackTimes.duration : 0)) /
|
|
384
|
+
trackTimes.timescale
|
|
385
|
+
: defaultValue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function isInvalidInitPts(
|
|
389
|
+
initPTS: TimestampOffset | null,
|
|
390
|
+
startDTS: number,
|
|
391
|
+
timeOffset: number,
|
|
392
|
+
duration: number,
|
|
393
|
+
): initPTS is null {
|
|
394
|
+
if (initPTS === null) {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
// InitPTS is invalid when distance from program would be more than or equal to segment duration or a minimum of one second
|
|
398
|
+
const minDuration = Math.max(duration, 1);
|
|
399
|
+
const startTime = startDTS - initPTS.baseTime / initPTS.timescale;
|
|
400
|
+
return Math.abs(startTime - timeOffset) >= minDuration;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function getParsedTrackCodec(
|
|
404
|
+
track: InitDataTrack,
|
|
405
|
+
type: ElementaryStreamTypes.AUDIO | ElementaryStreamTypes.VIDEO,
|
|
406
|
+
logger: ILogger,
|
|
407
|
+
): string {
|
|
408
|
+
const parsedCodec = track.codec;
|
|
409
|
+
if (parsedCodec && parsedCodec.length > 4) {
|
|
410
|
+
return parsedCodec;
|
|
411
|
+
}
|
|
412
|
+
if (type === ElementaryStreamTypes.AUDIO) {
|
|
413
|
+
if (
|
|
414
|
+
parsedCodec === 'ec-3' ||
|
|
415
|
+
parsedCodec === 'ac-3' ||
|
|
416
|
+
parsedCodec === 'alac'
|
|
417
|
+
) {
|
|
418
|
+
return parsedCodec;
|
|
419
|
+
}
|
|
420
|
+
if (parsedCodec === 'fLaC' || parsedCodec === 'Opus') {
|
|
421
|
+
// Opting not to get `preferManagedMediaSource` from player config for isSupported() check for simplicity
|
|
422
|
+
const preferManagedMediaSource = false;
|
|
423
|
+
return getCodecCompatibleName(parsedCodec, preferManagedMediaSource);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
logger.warn(`Unhandled audio codec "${parsedCodec}" in mp4 MAP`);
|
|
427
|
+
return parsedCodec || 'mp4a';
|
|
428
|
+
}
|
|
429
|
+
// Provide defaults based on codec type
|
|
430
|
+
// This allows for some playback of some fmp4 playlists without CODECS defined in manifest
|
|
431
|
+
logger.warn(`Unhandled video codec "${parsedCodec}" in mp4 MAP`);
|
|
432
|
+
return parsedCodec || 'avc1';
|
|
433
|
+
}
|
|
434
|
+
export default PassThroughRemuxer;
|
package/src/task-loop.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { type ILogger, Logger } from './utils/logger';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @ignore
|
|
5
|
+
* Sub-class specialization of EventHandler base class.
|
|
6
|
+
*
|
|
7
|
+
* TaskLoop allows to schedule a task function being called (optionnaly repeatedly) on the main loop,
|
|
8
|
+
* scheduled asynchroneously, avoiding recursive calls in the same tick.
|
|
9
|
+
*
|
|
10
|
+
* The task itself is implemented in `doTick`. It can be requested and called for single execution
|
|
11
|
+
* using the `tick` method.
|
|
12
|
+
*
|
|
13
|
+
* It will be assured that the task execution method (`tick`) only gets called once per main loop "tick",
|
|
14
|
+
* no matter how often it gets requested for execution. Execution in further ticks will be scheduled accordingly.
|
|
15
|
+
*
|
|
16
|
+
* If further execution requests have already been scheduled on the next tick, it can be checked with `hasNextTick`,
|
|
17
|
+
* and cancelled with `clearNextTick`.
|
|
18
|
+
*
|
|
19
|
+
* The task can be scheduled as an interval repeatedly with a period as parameter (see `setInterval`, `clearInterval`).
|
|
20
|
+
*
|
|
21
|
+
* Sub-classes need to implement the `doTick` method which will effectively have the task execution routine.
|
|
22
|
+
*
|
|
23
|
+
* Further explanations:
|
|
24
|
+
*
|
|
25
|
+
* The baseclass has a `tick` method that will schedule the doTick call. It may be called synchroneously
|
|
26
|
+
* only for a stack-depth of one. On re-entrant calls, sub-sequent calls are scheduled for next main loop ticks.
|
|
27
|
+
*
|
|
28
|
+
* When the task execution (`tick` method) is called in re-entrant way this is detected and
|
|
29
|
+
* we are limiting the task execution per call stack to exactly one, but scheduling/post-poning further
|
|
30
|
+
* task processing on the next main loop iteration (also known as "next tick" in the Node/JS runtime lingo).
|
|
31
|
+
*/
|
|
32
|
+
export default class TaskLoop extends Logger {
|
|
33
|
+
private readonly _boundTick: () => void;
|
|
34
|
+
private _tickTimer: number | null = null;
|
|
35
|
+
private _tickInterval: number | null = null;
|
|
36
|
+
private _tickCallCount = 0;
|
|
37
|
+
|
|
38
|
+
constructor(label: string, logger: ILogger) {
|
|
39
|
+
super(label, logger);
|
|
40
|
+
this._boundTick = this.tick.bind(this);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public destroy() {
|
|
44
|
+
this.onHandlerDestroying();
|
|
45
|
+
this.onHandlerDestroyed();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected onHandlerDestroying() {
|
|
49
|
+
// clear all timers before unregistering from event bus
|
|
50
|
+
this.clearNextTick();
|
|
51
|
+
this.clearInterval();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected onHandlerDestroyed() {}
|
|
55
|
+
|
|
56
|
+
public hasInterval(): boolean {
|
|
57
|
+
return !!this._tickInterval;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public hasNextTick(): boolean {
|
|
61
|
+
return !!this._tickTimer;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param millis - Interval time (ms)
|
|
66
|
+
* @eturns True when interval has been scheduled, false when already scheduled (no effect)
|
|
67
|
+
*/
|
|
68
|
+
public setInterval(millis: number): boolean {
|
|
69
|
+
if (!this._tickInterval) {
|
|
70
|
+
this._tickCallCount = 0;
|
|
71
|
+
this._tickInterval = self.setInterval(this._boundTick, millis);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @returns True when interval was cleared, false when none was set (no effect)
|
|
79
|
+
*/
|
|
80
|
+
public clearInterval(): boolean {
|
|
81
|
+
if (this._tickInterval) {
|
|
82
|
+
self.clearInterval(this._tickInterval);
|
|
83
|
+
this._tickInterval = null;
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @returns True when timeout was cleared, false when none was set (no effect)
|
|
91
|
+
*/
|
|
92
|
+
public clearNextTick(): boolean {
|
|
93
|
+
if (this._tickTimer) {
|
|
94
|
+
self.clearTimeout(this._tickTimer);
|
|
95
|
+
this._tickTimer = null;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Will call the subclass doTick implementation in this main loop tick
|
|
103
|
+
* or in the next one (via setTimeout(,0)) in case it has already been called
|
|
104
|
+
* in this tick (in case this is a re-entrant call).
|
|
105
|
+
*/
|
|
106
|
+
public tick(): void {
|
|
107
|
+
this._tickCallCount++;
|
|
108
|
+
if (this._tickCallCount === 1) {
|
|
109
|
+
this.doTick();
|
|
110
|
+
// re-entrant call to tick from previous doTick call stack
|
|
111
|
+
// -> schedule a call on the next main loop iteration to process this task processing request
|
|
112
|
+
if (this._tickCallCount > 1) {
|
|
113
|
+
// make sure only one timer exists at any time at max
|
|
114
|
+
this.tickImmediate();
|
|
115
|
+
}
|
|
116
|
+
this._tickCallCount = 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public tickImmediate(): void {
|
|
121
|
+
this.clearNextTick();
|
|
122
|
+
this._tickTimer = self.setTimeout(this._boundTick, 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* For subclass to implement task logic
|
|
127
|
+
* @abstract
|
|
128
|
+
*/
|
|
129
|
+
protected doTick(): void {}
|
|
130
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type AutoCameraItem = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
focus: number;
|
|
5
|
+
reserved: [number, number, number, number];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type TrackItem = {
|
|
9
|
+
trackId: number;
|
|
10
|
+
score: number;
|
|
11
|
+
box: [number, number, number, number];
|
|
12
|
+
reserved: [number, number, number, number];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type DetItem = {
|
|
16
|
+
classId: number;
|
|
17
|
+
score: number;
|
|
18
|
+
box: [number, number, number, number];
|
|
19
|
+
reserved: [number, number, number, number];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type FrameItem = {
|
|
23
|
+
frameIdx: number;
|
|
24
|
+
autoCameras: AutoCameraItem;
|
|
25
|
+
tracks: TrackItem[];
|
|
26
|
+
detections: DetItem[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type AipdMessage = {
|
|
30
|
+
version: number;
|
|
31
|
+
chunkIndex: number;
|
|
32
|
+
frameSize: number;
|
|
33
|
+
frames: FrameItem[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type AlgoChunk = {
|
|
37
|
+
fragSn: number;
|
|
38
|
+
algoUrl: string;
|
|
39
|
+
chunkIndex: number;
|
|
40
|
+
frameSize: number;
|
|
41
|
+
frameRate: number;
|
|
42
|
+
startFrameIndex: number;
|
|
43
|
+
frames: FrameItem[];
|
|
44
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface ArrayBuffer {
|
|
3
|
+
' buffer_kind'?: 'array';
|
|
4
|
+
}
|
|
5
|
+
interface Uint8Array {
|
|
6
|
+
' buffer_kind'?: 'uint8';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type SourceBufferName = 'video' | 'audio' | 'audiovideo';
|
|
11
|
+
|
|
12
|
+
/* eslint-disable no-restricted-globals */
|
|
13
|
+
// `SourceBuffer` global is restricted for is-supported check with prefixed MSE
|
|
14
|
+
export type ExtendedSourceBuffer = SourceBuffer & {
|
|
15
|
+
onbufferedchange?: ((this: SourceBuffer, ev: Event) => any) | null;
|
|
16
|
+
};
|
|
17
|
+
/* eslint-enable no-restricted-globals */
|
|
18
|
+
|
|
19
|
+
export interface BaseTrack {
|
|
20
|
+
id: 'audio' | 'main';
|
|
21
|
+
container: string;
|
|
22
|
+
codec?: string;
|
|
23
|
+
supplemental?: string;
|
|
24
|
+
encrypted?: boolean;
|
|
25
|
+
levelCodec?: string;
|
|
26
|
+
pendingCodec?: string;
|
|
27
|
+
metadata?: {
|
|
28
|
+
channelCount?: number;
|
|
29
|
+
width?: number;
|
|
30
|
+
height?: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ParsedTrack extends BaseTrack {
|
|
35
|
+
initSegment?: Uint8Array;
|
|
36
|
+
}
|
|
37
|
+
export interface SourceBufferTrack extends BaseTrack {
|
|
38
|
+
buffer?: ExtendedSourceBuffer;
|
|
39
|
+
bufferAppendTimeoutId?: number;
|
|
40
|
+
listeners: SourceBufferListener[];
|
|
41
|
+
ending?: boolean;
|
|
42
|
+
ended?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PendingSourceBufferTrack extends SourceBufferTrack {
|
|
46
|
+
buffer: undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface BufferCreatedTrack extends BaseTrack {
|
|
50
|
+
buffer: ExtendedSourceBuffer;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type ParsedTrackSet = Partial<Record<SourceBufferName, ParsedTrack>>;
|
|
54
|
+
|
|
55
|
+
export type BaseTrackSet = Partial<Record<SourceBufferName, BaseTrack>>;
|
|
56
|
+
|
|
57
|
+
export type SourceBufferTrackSet = Partial<
|
|
58
|
+
Record<SourceBufferName, SourceBufferTrack>
|
|
59
|
+
>;
|
|
60
|
+
|
|
61
|
+
export type BufferCreatedTrackSet = Partial<
|
|
62
|
+
Record<SourceBufferName, BufferCreatedTrack>
|
|
63
|
+
>;
|
|
64
|
+
|
|
65
|
+
export type EmptyTuple = [null, null];
|
|
66
|
+
|
|
67
|
+
export type SourceBuffersTuple = [
|
|
68
|
+
(
|
|
69
|
+
| [Extract<SourceBufferName, 'video' | 'audiovideo'>, ExtendedSourceBuffer]
|
|
70
|
+
| EmptyTuple
|
|
71
|
+
),
|
|
72
|
+
[Extract<SourceBufferName, 'audio'>, ExtendedSourceBuffer] | EmptyTuple,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export type MediaOverrides = {
|
|
76
|
+
duration?: number;
|
|
77
|
+
endOfStream?: boolean;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export interface BufferOperationQueues {
|
|
81
|
+
video: Array<BufferOperation>;
|
|
82
|
+
audio: Array<BufferOperation>;
|
|
83
|
+
audiovideo: Array<BufferOperation>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface BufferOperation {
|
|
87
|
+
label: string;
|
|
88
|
+
execute: Function;
|
|
89
|
+
onStart: Function;
|
|
90
|
+
onComplete: Function;
|
|
91
|
+
onError: Function;
|
|
92
|
+
start?: number;
|
|
93
|
+
end?: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface SourceBufferListener {
|
|
97
|
+
event: string;
|
|
98
|
+
listener: EventListener;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type AttachMediaSourceData = {
|
|
102
|
+
media: HTMLMediaElement;
|
|
103
|
+
mediaSource: MediaSource | null;
|
|
104
|
+
tracks: SourceBufferTrackSet;
|
|
105
|
+
};
|