@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,205 @@
|
|
|
1
|
+
import { canParseId3 } from '@svta/common-media-library/id3/canParseId3';
|
|
2
|
+
import { getId3Data } from '@svta/common-media-library/id3/getId3Data';
|
|
3
|
+
import { getId3Timestamp } from '@svta/common-media-library/id3/getId3Timestamp';
|
|
4
|
+
import {
|
|
5
|
+
type AudioFrame,
|
|
6
|
+
type DemuxedAudioTrack,
|
|
7
|
+
type DemuxedMetadataTrack,
|
|
8
|
+
type DemuxedUserdataTrack,
|
|
9
|
+
type DemuxedVideoTrackBase,
|
|
10
|
+
type Demuxer,
|
|
11
|
+
type DemuxerResult,
|
|
12
|
+
type KeyData,
|
|
13
|
+
MetadataSchema,
|
|
14
|
+
} from '../../types/demuxer';
|
|
15
|
+
import { appendUint8Array } from '../../utils/mp4-tools';
|
|
16
|
+
import { dummyTrack } from '../dummy-demuxed-track';
|
|
17
|
+
import type {
|
|
18
|
+
RationalTimestamp,
|
|
19
|
+
TimestampOffset,
|
|
20
|
+
} from '../../utils/timescale-conversion';
|
|
21
|
+
|
|
22
|
+
class BaseAudioDemuxer implements Demuxer {
|
|
23
|
+
protected _audioTrack?: DemuxedAudioTrack;
|
|
24
|
+
protected _id3Track?: DemuxedMetadataTrack;
|
|
25
|
+
protected frameIndex: number = 0;
|
|
26
|
+
protected cachedData: Uint8Array | null = null;
|
|
27
|
+
protected basePTS: number | null = null;
|
|
28
|
+
protected initPTS: TimestampOffset | null = null;
|
|
29
|
+
protected lastPTS: number | null = null;
|
|
30
|
+
|
|
31
|
+
resetInitSegment(
|
|
32
|
+
initSegment: Uint8Array | undefined,
|
|
33
|
+
audioCodec: string | undefined,
|
|
34
|
+
videoCodec: string | undefined,
|
|
35
|
+
trackDuration: number,
|
|
36
|
+
) {
|
|
37
|
+
this._id3Track = {
|
|
38
|
+
type: 'id3',
|
|
39
|
+
id: 3,
|
|
40
|
+
pid: -1,
|
|
41
|
+
inputTimeScale: 90000,
|
|
42
|
+
sequenceNumber: 0,
|
|
43
|
+
samples: [],
|
|
44
|
+
dropped: 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
resetTimeStamp(deaultTimestamp: TimestampOffset | null) {
|
|
49
|
+
this.initPTS = deaultTimestamp;
|
|
50
|
+
this.resetContiguity();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
resetContiguity(): void {
|
|
54
|
+
this.basePTS = null;
|
|
55
|
+
this.lastPTS = null;
|
|
56
|
+
this.frameIndex = 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
canParse(data: Uint8Array, offset: number): boolean {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
appendFrame(
|
|
64
|
+
track: DemuxedAudioTrack,
|
|
65
|
+
data: Uint8Array,
|
|
66
|
+
offset: number,
|
|
67
|
+
): AudioFrame | void {}
|
|
68
|
+
|
|
69
|
+
// feed incoming data to the front of the parsing pipeline
|
|
70
|
+
demux(data: Uint8Array, timeOffset: number): DemuxerResult {
|
|
71
|
+
if (this.cachedData) {
|
|
72
|
+
data = appendUint8Array(this.cachedData, data);
|
|
73
|
+
this.cachedData = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let id3Data: Uint8Array | undefined = getId3Data(data, 0);
|
|
77
|
+
let offset = id3Data ? id3Data.length : 0;
|
|
78
|
+
let lastDataIndex;
|
|
79
|
+
const track = this._audioTrack as DemuxedAudioTrack;
|
|
80
|
+
const id3Track = this._id3Track as DemuxedMetadataTrack;
|
|
81
|
+
const timestamp = id3Data ? getId3Timestamp(id3Data) : undefined;
|
|
82
|
+
const length = data.length;
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
this.basePTS === null ||
|
|
86
|
+
(this.frameIndex === 0 && Number.isFinite(timestamp))
|
|
87
|
+
) {
|
|
88
|
+
this.basePTS = initPTSFn(timestamp, timeOffset, this.initPTS);
|
|
89
|
+
this.lastPTS = this.basePTS;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (this.lastPTS === null) {
|
|
93
|
+
this.lastPTS = this.basePTS;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// more expressive than alternative: id3Data?.length
|
|
97
|
+
if (id3Data && id3Data.length > 0) {
|
|
98
|
+
id3Track.samples.push({
|
|
99
|
+
pts: this.lastPTS,
|
|
100
|
+
dts: this.lastPTS,
|
|
101
|
+
data: id3Data,
|
|
102
|
+
type: MetadataSchema.audioId3,
|
|
103
|
+
duration: Number.POSITIVE_INFINITY,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
while (offset < length) {
|
|
108
|
+
if (this.canParse(data, offset)) {
|
|
109
|
+
const frame = this.appendFrame(track, data, offset);
|
|
110
|
+
if (frame) {
|
|
111
|
+
this.frameIndex++;
|
|
112
|
+
this.lastPTS = frame.sample.pts;
|
|
113
|
+
offset += frame.length;
|
|
114
|
+
lastDataIndex = offset;
|
|
115
|
+
} else {
|
|
116
|
+
offset = length;
|
|
117
|
+
}
|
|
118
|
+
} else if (canParseId3(data, offset)) {
|
|
119
|
+
// after a canParse, a call to getId3Data *should* always returns some data
|
|
120
|
+
id3Data = getId3Data(data, offset)!;
|
|
121
|
+
id3Track.samples.push({
|
|
122
|
+
pts: this.lastPTS,
|
|
123
|
+
dts: this.lastPTS,
|
|
124
|
+
data: id3Data,
|
|
125
|
+
type: MetadataSchema.audioId3,
|
|
126
|
+
duration: Number.POSITIVE_INFINITY,
|
|
127
|
+
});
|
|
128
|
+
offset += id3Data.length;
|
|
129
|
+
lastDataIndex = offset;
|
|
130
|
+
} else {
|
|
131
|
+
offset++;
|
|
132
|
+
}
|
|
133
|
+
if (offset === length && lastDataIndex !== length) {
|
|
134
|
+
const partialData = data.slice(lastDataIndex);
|
|
135
|
+
if (this.cachedData) {
|
|
136
|
+
this.cachedData = appendUint8Array(this.cachedData, partialData);
|
|
137
|
+
} else {
|
|
138
|
+
this.cachedData = partialData;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
audioTrack: track,
|
|
145
|
+
videoTrack: dummyTrack() as DemuxedVideoTrackBase,
|
|
146
|
+
id3Track,
|
|
147
|
+
textTrack: dummyTrack() as DemuxedUserdataTrack,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
demuxSampleAes(
|
|
152
|
+
data: Uint8Array,
|
|
153
|
+
keyData: KeyData,
|
|
154
|
+
timeOffset: number,
|
|
155
|
+
): Promise<DemuxerResult> {
|
|
156
|
+
return Promise.reject(
|
|
157
|
+
new Error(
|
|
158
|
+
`[${this}] This demuxer does not support Sample-AES decryption`,
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
flush(timeOffset: number): DemuxerResult {
|
|
164
|
+
// Parse cache in case of remaining frames.
|
|
165
|
+
const cachedData = this.cachedData;
|
|
166
|
+
if (cachedData) {
|
|
167
|
+
this.cachedData = null;
|
|
168
|
+
this.demux(cachedData, 0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
audioTrack: this._audioTrack as DemuxedAudioTrack,
|
|
173
|
+
videoTrack: dummyTrack() as DemuxedVideoTrackBase,
|
|
174
|
+
id3Track: this._id3Track as DemuxedMetadataTrack,
|
|
175
|
+
textTrack: dummyTrack() as DemuxedUserdataTrack,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
destroy() {
|
|
180
|
+
this.cachedData = null;
|
|
181
|
+
// @ts-ignore
|
|
182
|
+
this._audioTrack = this._id3Track = undefined;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Initialize PTS
|
|
188
|
+
* <p>
|
|
189
|
+
* use timestamp unless it is undefined, NaN or Infinity
|
|
190
|
+
* </p>
|
|
191
|
+
*/
|
|
192
|
+
export const initPTSFn = (
|
|
193
|
+
timestamp: number | undefined,
|
|
194
|
+
timeOffset: number,
|
|
195
|
+
initPTS: RationalTimestamp | null,
|
|
196
|
+
): number => {
|
|
197
|
+
if (Number.isFinite(timestamp as number)) {
|
|
198
|
+
return timestamp! * 90;
|
|
199
|
+
}
|
|
200
|
+
const init90kHz = initPTS
|
|
201
|
+
? (initPTS.baseTime * 90000) / initPTS.timescale
|
|
202
|
+
: 0;
|
|
203
|
+
return timeOffset * 90000 + init90kHz;
|
|
204
|
+
};
|
|
205
|
+
export default BaseAudioDemuxer;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const getAudioBSID = (data: Uint8Array, offset: number): number => {
|
|
2
|
+
// check the bsid to confirm ac-3 | ec-3
|
|
3
|
+
let bsid = 0;
|
|
4
|
+
let numBits = 5;
|
|
5
|
+
offset += numBits;
|
|
6
|
+
const temp = new Uint32Array(1); // unsigned 32 bit for temporary storage
|
|
7
|
+
const mask = new Uint32Array(1); // unsigned 32 bit mask value
|
|
8
|
+
const byte = new Uint8Array(1); // unsigned 8 bit for temporary storage
|
|
9
|
+
while (numBits > 0) {
|
|
10
|
+
byte[0] = data[offset];
|
|
11
|
+
// read remaining bits, upto 8 bits at a time
|
|
12
|
+
const bits = Math.min(numBits, 8);
|
|
13
|
+
const shift = 8 - bits;
|
|
14
|
+
mask[0] = (0xff000000 >>> (24 + shift)) << shift;
|
|
15
|
+
temp[0] = (byte[0] & mask[0]) >> shift;
|
|
16
|
+
bsid = !bsid ? temp[0] : (bsid << bits) | temp[0];
|
|
17
|
+
offset += 1;
|
|
18
|
+
numBits -= bits;
|
|
19
|
+
}
|
|
20
|
+
return bsid;
|
|
21
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MP3 demuxer
|
|
3
|
+
*/
|
|
4
|
+
import { getId3Data } from '@svta/common-media-library/id3/getId3Data';
|
|
5
|
+
import { getId3Timestamp } from '@svta/common-media-library/id3/getId3Timestamp';
|
|
6
|
+
import BaseAudioDemuxer from './base-audio-demuxer';
|
|
7
|
+
import { getAudioBSID } from './dolby';
|
|
8
|
+
import * as MpegAudio from './mpegaudio';
|
|
9
|
+
import { logger } from '../../utils/logger';
|
|
10
|
+
|
|
11
|
+
class MP3Demuxer extends BaseAudioDemuxer {
|
|
12
|
+
resetInitSegment(
|
|
13
|
+
initSegment: Uint8Array | undefined,
|
|
14
|
+
audioCodec: string | undefined,
|
|
15
|
+
videoCodec: string | undefined,
|
|
16
|
+
trackDuration: number,
|
|
17
|
+
) {
|
|
18
|
+
super.resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration);
|
|
19
|
+
this._audioTrack = {
|
|
20
|
+
container: 'audio/mpeg',
|
|
21
|
+
type: 'audio',
|
|
22
|
+
id: 2,
|
|
23
|
+
pid: -1,
|
|
24
|
+
sequenceNumber: 0,
|
|
25
|
+
segmentCodec: 'mp3',
|
|
26
|
+
samples: [],
|
|
27
|
+
manifestCodec: audioCodec,
|
|
28
|
+
duration: trackDuration,
|
|
29
|
+
inputTimeScale: 90000,
|
|
30
|
+
dropped: 0,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static probe(data: Uint8Array | undefined): boolean {
|
|
35
|
+
if (!data) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// check if data contains ID3 timestamp and MPEG sync word
|
|
40
|
+
// Look for MPEG header | 1111 1111 | 111X XYZX | where X can be either 0 or 1 and Y or Z should be 1
|
|
41
|
+
// Layer bits (position 14 and 15) in header should be always different from 0 (Layer I or Layer II or Layer III)
|
|
42
|
+
// More info http://www.mp3-tech.org/programmer/frame_header.html
|
|
43
|
+
const id3Data = getId3Data(data, 0);
|
|
44
|
+
let offset = id3Data?.length || 0;
|
|
45
|
+
|
|
46
|
+
// Check for ac-3|ec-3 sync bytes and return false if present
|
|
47
|
+
if (
|
|
48
|
+
id3Data &&
|
|
49
|
+
data[offset] === 0x0b &&
|
|
50
|
+
data[offset + 1] === 0x77 &&
|
|
51
|
+
getId3Timestamp(id3Data) !== undefined &&
|
|
52
|
+
// check the bsid to confirm ac-3 or ec-3 (not mp3)
|
|
53
|
+
getAudioBSID(data, offset) <= 16
|
|
54
|
+
) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (let length = data.length; offset < length; offset++) {
|
|
59
|
+
if (MpegAudio.probe(data, offset)) {
|
|
60
|
+
logger.log('MPEG Audio sync word found !');
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
canParse(data, offset) {
|
|
68
|
+
return MpegAudio.canParse(data, offset);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
appendFrame(track, data, offset) {
|
|
72
|
+
if (this.basePTS === null) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
return MpegAudio.appendFrame(
|
|
76
|
+
track,
|
|
77
|
+
data,
|
|
78
|
+
offset,
|
|
79
|
+
this.basePTS,
|
|
80
|
+
this.frameIndex,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default MP3Demuxer;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPEG parser helper
|
|
3
|
+
*/
|
|
4
|
+
import type { DemuxedAudioTrack } from '../../types/demuxer';
|
|
5
|
+
|
|
6
|
+
let chromeVersion: number | null = null;
|
|
7
|
+
|
|
8
|
+
const BitratesMap = [
|
|
9
|
+
32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 32, 48, 56,
|
|
10
|
+
64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 32, 40, 48, 56, 64, 80,
|
|
11
|
+
96, 112, 128, 160, 192, 224, 256, 320, 32, 48, 56, 64, 80, 96, 112, 128, 144,
|
|
12
|
+
160, 176, 192, 224, 256, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144,
|
|
13
|
+
160,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const SamplingRateMap = [
|
|
17
|
+
44100, 48000, 32000, 22050, 24000, 16000, 11025, 12000, 8000,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const SamplesCoefficients = [
|
|
21
|
+
// MPEG 2.5
|
|
22
|
+
[
|
|
23
|
+
0, // Reserved
|
|
24
|
+
72, // Layer3
|
|
25
|
+
144, // Layer2
|
|
26
|
+
12, // Layer1
|
|
27
|
+
],
|
|
28
|
+
// Reserved
|
|
29
|
+
[
|
|
30
|
+
0, // Reserved
|
|
31
|
+
0, // Layer3
|
|
32
|
+
0, // Layer2
|
|
33
|
+
0, // Layer1
|
|
34
|
+
],
|
|
35
|
+
// MPEG 2
|
|
36
|
+
[
|
|
37
|
+
0, // Reserved
|
|
38
|
+
72, // Layer3
|
|
39
|
+
144, // Layer2
|
|
40
|
+
12, // Layer1
|
|
41
|
+
],
|
|
42
|
+
// MPEG 1
|
|
43
|
+
[
|
|
44
|
+
0, // Reserved
|
|
45
|
+
144, // Layer3
|
|
46
|
+
144, // Layer2
|
|
47
|
+
12, // Layer1
|
|
48
|
+
],
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const BytesInSlot = [
|
|
52
|
+
0, // Reserved
|
|
53
|
+
1, // Layer3
|
|
54
|
+
1, // Layer2
|
|
55
|
+
4, // Layer1
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export function appendFrame(
|
|
59
|
+
track: DemuxedAudioTrack,
|
|
60
|
+
data: Uint8Array,
|
|
61
|
+
offset: number,
|
|
62
|
+
pts: number,
|
|
63
|
+
frameIndex: number,
|
|
64
|
+
) {
|
|
65
|
+
// Using http://www.datavoyage.com/mpgscript/mpeghdr.htm as a reference
|
|
66
|
+
if (offset + 24 > data.length) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const header = parseHeader(data, offset);
|
|
71
|
+
if (header && offset + header.frameLength <= data.length) {
|
|
72
|
+
const frameDuration = (header.samplesPerFrame * 90000) / header.sampleRate;
|
|
73
|
+
const stamp = pts + frameIndex * frameDuration;
|
|
74
|
+
const sample = {
|
|
75
|
+
unit: data.subarray(offset, offset + header.frameLength),
|
|
76
|
+
pts: stamp,
|
|
77
|
+
dts: stamp,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
track.config = [];
|
|
81
|
+
track.channelCount = header.channelCount;
|
|
82
|
+
track.samplerate = header.sampleRate;
|
|
83
|
+
track.samples.push(sample);
|
|
84
|
+
|
|
85
|
+
return { sample, length: header.frameLength, missing: 0 };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function parseHeader(data: Uint8Array, offset: number) {
|
|
90
|
+
const mpegVersion = (data[offset + 1] >> 3) & 3;
|
|
91
|
+
const mpegLayer = (data[offset + 1] >> 1) & 3;
|
|
92
|
+
const bitRateIndex = (data[offset + 2] >> 4) & 15;
|
|
93
|
+
const sampleRateIndex = (data[offset + 2] >> 2) & 3;
|
|
94
|
+
if (
|
|
95
|
+
mpegVersion !== 1 &&
|
|
96
|
+
bitRateIndex !== 0 &&
|
|
97
|
+
bitRateIndex !== 15 &&
|
|
98
|
+
sampleRateIndex !== 3
|
|
99
|
+
) {
|
|
100
|
+
const paddingBit = (data[offset + 2] >> 1) & 1;
|
|
101
|
+
const channelMode = data[offset + 3] >> 6;
|
|
102
|
+
const columnInBitrates =
|
|
103
|
+
mpegVersion === 3 ? 3 - mpegLayer : mpegLayer === 3 ? 3 : 4;
|
|
104
|
+
const bitRate =
|
|
105
|
+
BitratesMap[columnInBitrates * 14 + bitRateIndex - 1] * 1000;
|
|
106
|
+
const columnInSampleRates =
|
|
107
|
+
mpegVersion === 3 ? 0 : mpegVersion === 2 ? 1 : 2;
|
|
108
|
+
const sampleRate =
|
|
109
|
+
SamplingRateMap[columnInSampleRates * 3 + sampleRateIndex];
|
|
110
|
+
const channelCount = channelMode === 3 ? 1 : 2; // If bits of channel mode are `11` then it is a single channel (Mono)
|
|
111
|
+
const sampleCoefficient = SamplesCoefficients[mpegVersion][mpegLayer];
|
|
112
|
+
const bytesInSlot = BytesInSlot[mpegLayer];
|
|
113
|
+
const samplesPerFrame = sampleCoefficient * 8 * bytesInSlot;
|
|
114
|
+
const frameLength =
|
|
115
|
+
Math.floor((sampleCoefficient * bitRate) / sampleRate + paddingBit) *
|
|
116
|
+
bytesInSlot;
|
|
117
|
+
|
|
118
|
+
if (chromeVersion === null) {
|
|
119
|
+
const userAgent = navigator.userAgent || '';
|
|
120
|
+
const result = userAgent.match(/Chrome\/(\d+)/i);
|
|
121
|
+
chromeVersion = result ? parseInt(result[1]) : 0;
|
|
122
|
+
}
|
|
123
|
+
const needChromeFix = !!chromeVersion && chromeVersion <= 87;
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
needChromeFix &&
|
|
127
|
+
mpegLayer === 2 &&
|
|
128
|
+
bitRate >= 224000 &&
|
|
129
|
+
channelMode === 0
|
|
130
|
+
) {
|
|
131
|
+
// Work around bug in Chromium by setting channelMode to dual-channel (01) instead of stereo (00)
|
|
132
|
+
data[offset + 3] = data[offset + 3] | 0x80;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { sampleRate, channelCount, frameLength, samplesPerFrame };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function isHeaderPattern(data: Uint8Array, offset: number): boolean {
|
|
140
|
+
return (
|
|
141
|
+
data[offset] === 0xff &&
|
|
142
|
+
(data[offset + 1] & 0xe0) === 0xe0 &&
|
|
143
|
+
(data[offset + 1] & 0x06) !== 0x00
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isHeader(data: Uint8Array, offset: number): boolean {
|
|
148
|
+
// Look for MPEG header | 1111 1111 | 111X XYZX | where X can be either 0 or 1 and Y or Z should be 1
|
|
149
|
+
// Layer bits (position 14 and 15) in header should be always different from 0 (Layer I or Layer II or Layer III)
|
|
150
|
+
// More info http://www.mp3-tech.org/programmer/frame_header.html
|
|
151
|
+
return offset + 1 < data.length && isHeaderPattern(data, offset);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function canParse(data: Uint8Array, offset: number): boolean {
|
|
155
|
+
const headerSize = 4;
|
|
156
|
+
|
|
157
|
+
return isHeaderPattern(data, offset) && headerSize <= data.length - offset;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function probe(data: Uint8Array, offset: number): boolean {
|
|
161
|
+
// same as isHeader but we also check that MPEG frame follows last MPEG frame
|
|
162
|
+
// or end of data is reached
|
|
163
|
+
if (offset + 1 < data.length && isHeaderPattern(data, offset)) {
|
|
164
|
+
// MPEG header Length
|
|
165
|
+
const headerLength = 4;
|
|
166
|
+
// MPEG frame Length
|
|
167
|
+
const header = parseHeader(data, offset);
|
|
168
|
+
let frameLength = headerLength;
|
|
169
|
+
if (header?.frameLength) {
|
|
170
|
+
frameLength = header.frameLength;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const newOffset = offset + frameLength;
|
|
174
|
+
return newOffset === data.length || isHeader(data, newOffset);
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export default class ChunkCache {
|
|
2
|
+
private chunks: Array<Uint8Array<ArrayBuffer>> = [];
|
|
3
|
+
public dataLength: number = 0;
|
|
4
|
+
|
|
5
|
+
push(chunk: Uint8Array<ArrayBuffer>) {
|
|
6
|
+
this.chunks.push(chunk);
|
|
7
|
+
this.dataLength += chunk.length;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
flush(): Uint8Array<ArrayBuffer> {
|
|
11
|
+
const { chunks, dataLength } = this;
|
|
12
|
+
let result: Uint8Array<ArrayBuffer>;
|
|
13
|
+
if (!chunks.length) {
|
|
14
|
+
return new Uint8Array(0);
|
|
15
|
+
} else if (chunks.length === 1) {
|
|
16
|
+
result = chunks[0];
|
|
17
|
+
} else {
|
|
18
|
+
result = concatUint8Arrays(chunks, dataLength);
|
|
19
|
+
}
|
|
20
|
+
this.reset();
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
reset() {
|
|
25
|
+
this.chunks.length = 0;
|
|
26
|
+
this.dataLength = 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function concatUint8Arrays(
|
|
31
|
+
chunks: Array<Uint8Array<ArrayBuffer>>,
|
|
32
|
+
dataLength: number,
|
|
33
|
+
): Uint8Array<ArrayBuffer> {
|
|
34
|
+
const result = new Uint8Array(dataLength);
|
|
35
|
+
let offset = 0;
|
|
36
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
37
|
+
const chunk = chunks[i];
|
|
38
|
+
result.set(chunk, offset);
|
|
39
|
+
offset += chunk.length;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DemuxedTrack } from '../types/demuxer';
|
|
2
|
+
|
|
3
|
+
export function dummyTrack(type = '', inputTimeScale = 90000): DemuxedTrack {
|
|
4
|
+
return {
|
|
5
|
+
type,
|
|
6
|
+
id: -1,
|
|
7
|
+
pid: -1,
|
|
8
|
+
inputTimeScale,
|
|
9
|
+
sequenceNumber: -1,
|
|
10
|
+
samples: [],
|
|
11
|
+
dropped: 0,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// ensure the worker ends up in the bundle
|
|
2
|
+
// If the worker should not be included this gets aliased to empty.js
|
|
3
|
+
import './transmuxer-worker';
|
|
4
|
+
import { version } from '../version';
|
|
5
|
+
|
|
6
|
+
const workerStore: Record<string, WorkerContext> = {};
|
|
7
|
+
|
|
8
|
+
export function hasUMDWorker(): boolean {
|
|
9
|
+
return typeof __HLS_WORKER_BUNDLE__ === 'function';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type WorkerContext = {
|
|
13
|
+
worker: Worker;
|
|
14
|
+
objectURL?: string;
|
|
15
|
+
scriptURL?: string;
|
|
16
|
+
clientCount: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function injectWorker(): WorkerContext {
|
|
20
|
+
const workerContext = workerStore[version];
|
|
21
|
+
if (workerContext) {
|
|
22
|
+
workerContext.clientCount++;
|
|
23
|
+
return workerContext;
|
|
24
|
+
}
|
|
25
|
+
const blob = new self.Blob(
|
|
26
|
+
[
|
|
27
|
+
`var exports={};var module={exports:exports};function define(f){f()};define.amd=true;(${__HLS_WORKER_BUNDLE__.toString()})(true);`,
|
|
28
|
+
],
|
|
29
|
+
{
|
|
30
|
+
type: 'text/javascript',
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
const objectURL = self.URL.createObjectURL(blob);
|
|
34
|
+
const worker = new self.Worker(objectURL);
|
|
35
|
+
const result = {
|
|
36
|
+
worker,
|
|
37
|
+
objectURL,
|
|
38
|
+
clientCount: 1,
|
|
39
|
+
};
|
|
40
|
+
workerStore[version] = result;
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadWorker(path: string): WorkerContext {
|
|
45
|
+
const workerContext = workerStore[path];
|
|
46
|
+
if (workerContext) {
|
|
47
|
+
workerContext.clientCount++;
|
|
48
|
+
return workerContext;
|
|
49
|
+
}
|
|
50
|
+
const scriptURL = new self.URL(path, self.location.href).href;
|
|
51
|
+
const worker = new self.Worker(scriptURL);
|
|
52
|
+
const result = {
|
|
53
|
+
worker,
|
|
54
|
+
scriptURL,
|
|
55
|
+
clientCount: 1,
|
|
56
|
+
};
|
|
57
|
+
workerStore[path] = result;
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function removeWorkerFromStore(path?: string | null) {
|
|
62
|
+
const workerContext = workerStore[path || version];
|
|
63
|
+
if (workerContext) {
|
|
64
|
+
const clientCount = workerContext.clientCount--;
|
|
65
|
+
if (clientCount === 1) {
|
|
66
|
+
const { worker, objectURL } = workerContext;
|
|
67
|
+
delete workerStore[path || version];
|
|
68
|
+
if (objectURL) {
|
|
69
|
+
// revoke the Object URL that was used to create transmuxer worker, so as not to leak it
|
|
70
|
+
self.URL.revokeObjectURL(objectURL);
|
|
71
|
+
}
|
|
72
|
+
worker.terminate();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|