@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,41 @@
|
|
|
1
|
+
import { appendUint8Array } from './mp4-tools';
|
|
2
|
+
|
|
3
|
+
export default class Chunker {
|
|
4
|
+
private chunkSize: number;
|
|
5
|
+
public cache: Uint8Array | null = null;
|
|
6
|
+
constructor(chunkSize = Math.pow(2, 19)) {
|
|
7
|
+
this.chunkSize = chunkSize;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public push(data: Uint8Array): Array<Uint8Array> {
|
|
11
|
+
const { cache, chunkSize } = this;
|
|
12
|
+
const result: Array<Uint8Array> = [];
|
|
13
|
+
|
|
14
|
+
let temp: Uint8Array | null = null;
|
|
15
|
+
if (cache?.length) {
|
|
16
|
+
temp = appendUint8Array(cache, data);
|
|
17
|
+
this.cache = null;
|
|
18
|
+
} else {
|
|
19
|
+
temp = data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (temp.length < chunkSize) {
|
|
23
|
+
this.cache = temp;
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (temp.length > chunkSize) {
|
|
28
|
+
let offset = 0;
|
|
29
|
+
const len = temp.length;
|
|
30
|
+
while (offset < len - chunkSize) {
|
|
31
|
+
result.push(temp.slice(offset, offset + chunkSize));
|
|
32
|
+
offset += chunkSize;
|
|
33
|
+
}
|
|
34
|
+
this.cache = temp.slice(offset);
|
|
35
|
+
} else {
|
|
36
|
+
result.push(temp);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { getMediaSource } from './mediasource-helper';
|
|
2
|
+
import { isHEVC } from './mp4-tools';
|
|
3
|
+
|
|
4
|
+
export const userAgentHevcSupportIsInaccurate = () => {
|
|
5
|
+
return /\(Windows.+Firefox\//i.test(navigator.userAgent);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// from http://mp4ra.org/codecs.html
|
|
9
|
+
// values indicate codec selection preference (lower is higher priority)
|
|
10
|
+
export const sampleEntryCodesISO = {
|
|
11
|
+
audio: {
|
|
12
|
+
a3ds: 1,
|
|
13
|
+
'ac-3': 0.95,
|
|
14
|
+
'ac-4': 1,
|
|
15
|
+
alac: 0.9,
|
|
16
|
+
alaw: 1,
|
|
17
|
+
dra1: 1,
|
|
18
|
+
'dts+': 1,
|
|
19
|
+
'dts-': 1,
|
|
20
|
+
dtsc: 1,
|
|
21
|
+
dtse: 1,
|
|
22
|
+
dtsh: 1,
|
|
23
|
+
'ec-3': 0.9,
|
|
24
|
+
enca: 1,
|
|
25
|
+
fLaC: 0.9, // MP4-RA listed codec entry for FLAC
|
|
26
|
+
flac: 0.9, // legacy browser codec name for FLAC
|
|
27
|
+
FLAC: 0.9, // some manifests may list "FLAC" with Apple's tools
|
|
28
|
+
g719: 1,
|
|
29
|
+
g726: 1,
|
|
30
|
+
m4ae: 1,
|
|
31
|
+
mha1: 1,
|
|
32
|
+
mha2: 1,
|
|
33
|
+
mhm1: 1,
|
|
34
|
+
mhm2: 1,
|
|
35
|
+
mlpa: 1,
|
|
36
|
+
mp4a: 1,
|
|
37
|
+
'raw ': 1,
|
|
38
|
+
Opus: 1,
|
|
39
|
+
opus: 1, // browsers expect this to be lowercase despite MP4RA says 'Opus'
|
|
40
|
+
samr: 1,
|
|
41
|
+
sawb: 1,
|
|
42
|
+
sawp: 1,
|
|
43
|
+
sevc: 1,
|
|
44
|
+
sqcp: 1,
|
|
45
|
+
ssmv: 1,
|
|
46
|
+
twos: 1,
|
|
47
|
+
ulaw: 1,
|
|
48
|
+
},
|
|
49
|
+
video: {
|
|
50
|
+
avc1: 1,
|
|
51
|
+
avc2: 1,
|
|
52
|
+
avc3: 1,
|
|
53
|
+
avc4: 1,
|
|
54
|
+
avcp: 1,
|
|
55
|
+
av01: 0.8,
|
|
56
|
+
dav1: 0.8,
|
|
57
|
+
drac: 1,
|
|
58
|
+
dva1: 1,
|
|
59
|
+
dvav: 1,
|
|
60
|
+
dvh1: 0.7,
|
|
61
|
+
dvhe: 0.7,
|
|
62
|
+
encv: 1,
|
|
63
|
+
hev1: 0.75,
|
|
64
|
+
hvc1: 0.75,
|
|
65
|
+
mjp2: 1,
|
|
66
|
+
mp4v: 1,
|
|
67
|
+
mvc1: 1,
|
|
68
|
+
mvc2: 1,
|
|
69
|
+
mvc3: 1,
|
|
70
|
+
mvc4: 1,
|
|
71
|
+
resv: 1,
|
|
72
|
+
rv60: 1,
|
|
73
|
+
s263: 1,
|
|
74
|
+
svc1: 1,
|
|
75
|
+
svc2: 1,
|
|
76
|
+
'vc-1': 1,
|
|
77
|
+
vp08: 1,
|
|
78
|
+
vp09: 0.9,
|
|
79
|
+
},
|
|
80
|
+
text: {
|
|
81
|
+
stpp: 1,
|
|
82
|
+
wvtt: 1,
|
|
83
|
+
},
|
|
84
|
+
} as const;
|
|
85
|
+
|
|
86
|
+
export type CodecType = 'audio' | 'video';
|
|
87
|
+
|
|
88
|
+
export function isCodecType(codec: string, type: CodecType): boolean {
|
|
89
|
+
const typeCodes = sampleEntryCodesISO[type];
|
|
90
|
+
return !!typeCodes && !!typeCodes[codec.slice(0, 4)];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function areCodecsMediaSourceSupported(
|
|
94
|
+
codecs: string,
|
|
95
|
+
type: CodecType,
|
|
96
|
+
preferManagedMediaSource = true,
|
|
97
|
+
): boolean {
|
|
98
|
+
return !codecs
|
|
99
|
+
.split(',')
|
|
100
|
+
.some(
|
|
101
|
+
(codec) =>
|
|
102
|
+
!isCodecMediaSourceSupported(codec, type, preferManagedMediaSource),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isCodecMediaSourceSupported(
|
|
107
|
+
codec: string,
|
|
108
|
+
type: CodecType,
|
|
109
|
+
preferManagedMediaSource = true,
|
|
110
|
+
): boolean {
|
|
111
|
+
const MediaSource = getMediaSource(preferManagedMediaSource);
|
|
112
|
+
return MediaSource?.isTypeSupported(mimeTypeForCodec(codec, type)) ?? false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function mimeTypeForCodec(codec: string, type: CodecType): string {
|
|
116
|
+
return `${type}/mp4;codecs=${codec}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function videoCodecPreferenceValue(
|
|
120
|
+
videoCodec: string | undefined,
|
|
121
|
+
): number {
|
|
122
|
+
if (videoCodec) {
|
|
123
|
+
const fourCC = videoCodec.substring(0, 4);
|
|
124
|
+
return sampleEntryCodesISO.video[fourCC];
|
|
125
|
+
}
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function codecsSetSelectionPreferenceValue(codecSet: string): number {
|
|
130
|
+
const limitedHevcSupport = userAgentHevcSupportIsInaccurate();
|
|
131
|
+
return codecSet.split(',').reduce((num, fourCC) => {
|
|
132
|
+
const lowerPriority = limitedHevcSupport && isHEVC(fourCC);
|
|
133
|
+
const preferenceValue = lowerPriority
|
|
134
|
+
? 9
|
|
135
|
+
: sampleEntryCodesISO.video[fourCC];
|
|
136
|
+
if (preferenceValue) {
|
|
137
|
+
return (preferenceValue * 2 + num) / (num ? 3 : 2);
|
|
138
|
+
}
|
|
139
|
+
return (sampleEntryCodesISO.audio[fourCC] + num) / (num ? 2 : 1);
|
|
140
|
+
}, 0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface CodecNameCache {
|
|
144
|
+
flac?: string;
|
|
145
|
+
opus?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const CODEC_COMPATIBLE_NAMES: CodecNameCache = {};
|
|
149
|
+
|
|
150
|
+
type LowerCaseCodecType = 'flac' | 'opus';
|
|
151
|
+
|
|
152
|
+
function getCodecCompatibleNameLower(
|
|
153
|
+
lowerCaseCodec: LowerCaseCodecType,
|
|
154
|
+
preferManagedMediaSource = true,
|
|
155
|
+
): string {
|
|
156
|
+
if (CODEC_COMPATIBLE_NAMES[lowerCaseCodec]) {
|
|
157
|
+
return CODEC_COMPATIBLE_NAMES[lowerCaseCodec]!;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const codecsToCheck = {
|
|
161
|
+
// Idealy fLaC and Opus would be first (spec-compliant) but
|
|
162
|
+
// some browsers will report that fLaC is supported then fail.
|
|
163
|
+
// see: https://bugs.chromium.org/p/chromium/issues/detail?id=1422728
|
|
164
|
+
flac: ['flac', 'fLaC', 'FLAC'],
|
|
165
|
+
opus: ['opus', 'Opus'],
|
|
166
|
+
// Replace audio codec info if browser does not support mp4a.40.34,
|
|
167
|
+
// and demuxer can fallback to 'audio/mpeg' or 'audio/mp4;codecs="mp3"'
|
|
168
|
+
'mp4a.40.34': ['mp3'],
|
|
169
|
+
}[lowerCaseCodec];
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < codecsToCheck.length; i++) {
|
|
172
|
+
if (
|
|
173
|
+
isCodecMediaSourceSupported(
|
|
174
|
+
codecsToCheck[i],
|
|
175
|
+
'audio',
|
|
176
|
+
preferManagedMediaSource,
|
|
177
|
+
)
|
|
178
|
+
) {
|
|
179
|
+
CODEC_COMPATIBLE_NAMES[lowerCaseCodec] = codecsToCheck[i];
|
|
180
|
+
return codecsToCheck[i];
|
|
181
|
+
} else if (
|
|
182
|
+
codecsToCheck[i] === 'mp3' &&
|
|
183
|
+
getMediaSource(preferManagedMediaSource)?.isTypeSupported('audio/mpeg')
|
|
184
|
+
) {
|
|
185
|
+
return '';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return lowerCaseCodec;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const AUDIO_CODEC_REGEXP = /flac|opus|mp4a\.40\.34/i;
|
|
193
|
+
export function getCodecCompatibleName(
|
|
194
|
+
codec: string,
|
|
195
|
+
preferManagedMediaSource = true,
|
|
196
|
+
): string {
|
|
197
|
+
return codec.replace(AUDIO_CODEC_REGEXP, (m) =>
|
|
198
|
+
getCodecCompatibleNameLower(
|
|
199
|
+
m.toLowerCase() as LowerCaseCodecType,
|
|
200
|
+
preferManagedMediaSource,
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function replaceVideoCodec(
|
|
206
|
+
originalCodecs: string | undefined,
|
|
207
|
+
newVideoCodec: string | undefined,
|
|
208
|
+
): string | undefined {
|
|
209
|
+
const codecs: string[] = [];
|
|
210
|
+
if (originalCodecs) {
|
|
211
|
+
const allCodecs = originalCodecs.split(',');
|
|
212
|
+
for (let i = 0; i < allCodecs.length; i++) {
|
|
213
|
+
if (!isCodecType(allCodecs[i], 'video')) {
|
|
214
|
+
codecs.push(allCodecs[i]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (newVideoCodec) {
|
|
219
|
+
codecs.push(newVideoCodec);
|
|
220
|
+
}
|
|
221
|
+
return codecs.join(',');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function pickMostCompleteCodecName(
|
|
225
|
+
parsedCodec: string | undefined,
|
|
226
|
+
levelCodec: string | undefined,
|
|
227
|
+
): string | undefined {
|
|
228
|
+
// Parsing of mp4a codecs strings in mp4-tools from media is incomplete as of d8c6c7a
|
|
229
|
+
// so use level codec is parsed codec is unavailable or incomplete
|
|
230
|
+
if (
|
|
231
|
+
parsedCodec &&
|
|
232
|
+
(parsedCodec.length > 4 ||
|
|
233
|
+
['ac-3', 'ec-3', 'alac', 'fLaC', 'Opus'].indexOf(parsedCodec) !== -1)
|
|
234
|
+
) {
|
|
235
|
+
if (
|
|
236
|
+
isCodecSupportedAsType(parsedCodec, 'audio') ||
|
|
237
|
+
isCodecSupportedAsType(parsedCodec, 'video')
|
|
238
|
+
) {
|
|
239
|
+
return parsedCodec;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (levelCodec) {
|
|
243
|
+
const levelCodecs = levelCodec.split(',');
|
|
244
|
+
if (levelCodecs.length > 1) {
|
|
245
|
+
if (parsedCodec) {
|
|
246
|
+
for (let i = levelCodecs.length; i--; ) {
|
|
247
|
+
if (levelCodecs[i].substring(0, 4) === parsedCodec.substring(0, 4)) {
|
|
248
|
+
return levelCodecs[i];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return levelCodecs[0];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return levelCodec || parsedCodec;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function isCodecSupportedAsType(codec: string, type: CodecType): boolean {
|
|
259
|
+
return isCodecType(codec, type) && isCodecMediaSourceSupported(codec, type);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function convertAVC1ToAVCOTI(videoCodecs: string): string {
|
|
263
|
+
// Convert avc1 codec string from RFC-4281 to RFC-6381 for MediaSource.isTypeSupported
|
|
264
|
+
// Examples: avc1.66.30 to avc1.42001e and avc1.77.30,avc1.66.30 to avc1.4d001e,avc1.42001e.
|
|
265
|
+
const codecs = videoCodecs.split(',');
|
|
266
|
+
for (let i = 0; i < codecs.length; i++) {
|
|
267
|
+
const avcdata = codecs[i].split('.');
|
|
268
|
+
// only convert codec strings starting with avc1 (Examples: avc1.64001f,dvh1.05.07)
|
|
269
|
+
if (avcdata.length > 2 && avcdata[0] === 'avc1') {
|
|
270
|
+
codecs[i] = `avc1.${parseInt(avcdata[1]).toString(16)}${(
|
|
271
|
+
'000' + parseInt(avcdata[2]).toString(16)
|
|
272
|
+
).slice(-4)}`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return codecs.join(',');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function fillInMissingAV01Params(videoCodec: string): string {
|
|
279
|
+
// Used to fill in incomplete AV1 playlist CODECS strings for mediaCapabilities.decodingInfo queries
|
|
280
|
+
if (videoCodec.startsWith('av01.')) {
|
|
281
|
+
const av1params = videoCodec.split('.');
|
|
282
|
+
const placeholders = ['0', '111', '01', '01', '01', '0'];
|
|
283
|
+
for (let i = av1params.length; i > 4 && i < 10; i++) {
|
|
284
|
+
av1params[i] = placeholders[i - 4];
|
|
285
|
+
}
|
|
286
|
+
return av1params.join('.');
|
|
287
|
+
}
|
|
288
|
+
return videoCodec;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface TypeSupported {
|
|
292
|
+
mpeg: boolean;
|
|
293
|
+
mp3: boolean;
|
|
294
|
+
ac3: boolean;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function getM2TSSupportedAudioTypes(
|
|
298
|
+
preferManagedMediaSource: boolean,
|
|
299
|
+
): TypeSupported {
|
|
300
|
+
const MediaSource = getMediaSource(preferManagedMediaSource) || {
|
|
301
|
+
isTypeSupported: () => false,
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
mpeg: MediaSource.isTypeSupported('audio/mpeg'),
|
|
305
|
+
mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"'),
|
|
306
|
+
ac3: __USE_M2TS_ADVANCED_CODECS__
|
|
307
|
+
? MediaSource.isTypeSupported('audio/mp4; codecs="ac-3"')
|
|
308
|
+
: false,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function getCodecsForMimeType(mimeType: string): string {
|
|
313
|
+
return mimeType.replace(/^.+codecs=["']?([^"']+).*$/, '$1');
|
|
314
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { addCueToTrack } from './texttrack-utils';
|
|
2
|
+
import { fixLineBreaks } from './vttparser';
|
|
3
|
+
import { generateCueId } from './webvtt-parser';
|
|
4
|
+
import type { CaptionScreen, Row } from './cea-608-parser';
|
|
5
|
+
|
|
6
|
+
const WHITESPACE_CHAR = /\s/;
|
|
7
|
+
|
|
8
|
+
export interface CuesInterface {
|
|
9
|
+
newCue(
|
|
10
|
+
track: TextTrack | null,
|
|
11
|
+
startTime: number,
|
|
12
|
+
endTime: number,
|
|
13
|
+
captionScreen: CaptionScreen,
|
|
14
|
+
): VTTCue[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const Cues: CuesInterface = {
|
|
18
|
+
newCue(
|
|
19
|
+
track: TextTrack | null,
|
|
20
|
+
startTime: number,
|
|
21
|
+
endTime: number,
|
|
22
|
+
captionScreen: CaptionScreen,
|
|
23
|
+
): VTTCue[] {
|
|
24
|
+
const result: VTTCue[] = [];
|
|
25
|
+
let row: Row;
|
|
26
|
+
// the type data states this is VTTCue, but it can potentially be a TextTrackCue on old browsers
|
|
27
|
+
let cue: VTTCue;
|
|
28
|
+
let indenting: boolean;
|
|
29
|
+
let indent: number;
|
|
30
|
+
let text: string;
|
|
31
|
+
const Cue = (self.VTTCue || self.TextTrackCue) as any;
|
|
32
|
+
|
|
33
|
+
for (let r = 0; r < captionScreen.rows.length; r++) {
|
|
34
|
+
row = captionScreen.rows[r];
|
|
35
|
+
indenting = true;
|
|
36
|
+
indent = 0;
|
|
37
|
+
text = '';
|
|
38
|
+
|
|
39
|
+
if (!row.isEmpty()) {
|
|
40
|
+
for (let c = 0; c < row.chars.length; c++) {
|
|
41
|
+
if (WHITESPACE_CHAR.test(row.chars[c].uchar) && indenting) {
|
|
42
|
+
indent++;
|
|
43
|
+
} else {
|
|
44
|
+
text += row.chars[c].uchar;
|
|
45
|
+
indenting = false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// To be used for cleaning-up orphaned roll-up captions
|
|
49
|
+
row.cueStartTime = startTime;
|
|
50
|
+
|
|
51
|
+
// Give a slight bump to the endTime if it's equal to startTime to avoid a SyntaxError in IE
|
|
52
|
+
if (startTime === endTime) {
|
|
53
|
+
endTime += 0.0001;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (indent >= 16) {
|
|
57
|
+
indent--;
|
|
58
|
+
} else {
|
|
59
|
+
indent++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const cueText = fixLineBreaks(text.trim());
|
|
63
|
+
const id = generateCueId(startTime, endTime, cueText);
|
|
64
|
+
|
|
65
|
+
// If this cue already exists in the track do not push it
|
|
66
|
+
if (!track?.cues?.getCueById(id)) {
|
|
67
|
+
cue = new Cue(startTime, endTime, cueText);
|
|
68
|
+
cue.id = id;
|
|
69
|
+
cue.line = r + 1;
|
|
70
|
+
cue.align = 'left';
|
|
71
|
+
// Clamp the position between 10 and 80 percent (CEA-608 PAC indent code)
|
|
72
|
+
// https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-608
|
|
73
|
+
// Firefox throws an exception and captions break with out of bounds 0-100 values
|
|
74
|
+
cue.position = 10 + Math.min(80, Math.floor((indent * 8) / 32) * 10);
|
|
75
|
+
result.push(cue);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (track && result.length) {
|
|
80
|
+
// Sort bottom cues in reverse order so that they render in line order when overlapping in Chrome
|
|
81
|
+
result.sort((cueA, cueB) => {
|
|
82
|
+
if (cueA.line === 'auto' || cueB.line === 'auto') {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
if (cueA.line > 8 && cueB.line > 8) {
|
|
86
|
+
return cueB.line - cueA.line;
|
|
87
|
+
}
|
|
88
|
+
return cueA.line - cueB.line;
|
|
89
|
+
});
|
|
90
|
+
result.forEach((cue) => addCueToTrack(track, cue));
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default Cues;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { adjustSliding } from './level-helper';
|
|
2
|
+
import type { ILogger } from './logger';
|
|
3
|
+
import type { Fragment } from '../loader/fragment';
|
|
4
|
+
import type { LevelDetails } from '../loader/level-details';
|
|
5
|
+
|
|
6
|
+
export function findFirstFragWithCC(
|
|
7
|
+
fragments: Fragment[],
|
|
8
|
+
cc: number,
|
|
9
|
+
): Fragment | null {
|
|
10
|
+
for (let i = 0, len = fragments.length; i < len; i++) {
|
|
11
|
+
if (fragments[i]?.cc === cc) {
|
|
12
|
+
return fragments[i];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function shouldAlignOnDiscontinuities(
|
|
19
|
+
refDetails: LevelDetails | undefined,
|
|
20
|
+
details: LevelDetails,
|
|
21
|
+
): refDetails is LevelDetails & boolean {
|
|
22
|
+
if (refDetails) {
|
|
23
|
+
if (
|
|
24
|
+
details.startCC < refDetails.endCC &&
|
|
25
|
+
details.endCC > refDetails.startCC
|
|
26
|
+
) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function adjustFragmentStart(frag: Fragment, sliding: number) {
|
|
34
|
+
const start = frag.start + sliding;
|
|
35
|
+
frag.startPTS = start;
|
|
36
|
+
frag.setStart(start);
|
|
37
|
+
frag.endPTS = start + frag.duration;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function adjustSlidingStart(sliding: number, details: LevelDetails) {
|
|
41
|
+
// Update segments
|
|
42
|
+
const fragments = details.fragments;
|
|
43
|
+
for (let i = 0, len = fragments.length; i < len; i++) {
|
|
44
|
+
adjustFragmentStart(fragments[i], sliding);
|
|
45
|
+
}
|
|
46
|
+
// Update LL-HLS parts at the end of the playlist
|
|
47
|
+
if (details.fragmentHint) {
|
|
48
|
+
adjustFragmentStart(details.fragmentHint, sliding);
|
|
49
|
+
}
|
|
50
|
+
details.alignedSliding = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Using the parameters of the last level, this function computes PTS' of the new fragments so that they form a
|
|
55
|
+
* contiguous stream with the last fragments.
|
|
56
|
+
* The PTS of a fragment lets Hls.js know where it fits into a stream - by knowing every PTS, we know which fragment to
|
|
57
|
+
* download at any given time. PTS is normally computed when the fragment is demuxed, so taking this step saves us time
|
|
58
|
+
* and an extra download.
|
|
59
|
+
* @param lastLevel
|
|
60
|
+
* @param details
|
|
61
|
+
*/
|
|
62
|
+
export function alignStream(
|
|
63
|
+
switchDetails: LevelDetails | undefined,
|
|
64
|
+
details: LevelDetails,
|
|
65
|
+
logger: ILogger,
|
|
66
|
+
) {
|
|
67
|
+
if (!switchDetails) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
alignDiscontinuities(details, switchDetails, logger);
|
|
71
|
+
if (!details.alignedSliding) {
|
|
72
|
+
// If the PTS wasn't figured out via discontinuity sequence that means there was no CC increase within the level.
|
|
73
|
+
// Aligning via Program Date Time should therefore be reliable, since PDT should be the same within the same
|
|
74
|
+
// discontinuity sequence.
|
|
75
|
+
alignMediaPlaylistByPDT(details, switchDetails, logger);
|
|
76
|
+
}
|
|
77
|
+
if (!details.alignedSliding && !details.skippedSegments) {
|
|
78
|
+
// Try to align on sn so that we pick a better start fragment.
|
|
79
|
+
// Do not perform this on playlists with delta updates as this is only to align levels on switch
|
|
80
|
+
// and adjustSliding only adjusts fragments after skippedSegments.
|
|
81
|
+
adjustSliding(switchDetails, details, false, logger);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Ajust the start of fragments in `details` by the difference in time between fragments of the latest
|
|
87
|
+
* shared discontinuity sequence change.
|
|
88
|
+
* @param lastLevel - The details of the last loaded level
|
|
89
|
+
* @param details - The details of the new level
|
|
90
|
+
*/
|
|
91
|
+
export function alignDiscontinuities(
|
|
92
|
+
details: LevelDetails,
|
|
93
|
+
refDetails: LevelDetails | undefined,
|
|
94
|
+
logger: ILogger,
|
|
95
|
+
) {
|
|
96
|
+
if (!shouldAlignOnDiscontinuities(refDetails, details)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const targetCC = Math.min(refDetails.endCC, details.endCC);
|
|
100
|
+
const refFrag = findFirstFragWithCC(refDetails.fragments, targetCC);
|
|
101
|
+
const frag = findFirstFragWithCC(details.fragments, targetCC);
|
|
102
|
+
if (!refFrag || !frag) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const delta = refFrag.start - frag.start;
|
|
106
|
+
logger.log(
|
|
107
|
+
`Aligning playlists using dicontinuity sequence ${targetCC} (diff: ${delta})`,
|
|
108
|
+
);
|
|
109
|
+
adjustSlidingStart(delta, details);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Ensures appropriate time-alignment between renditions based on PDT.
|
|
114
|
+
* This function assumes the timelines represented in `refDetails` are accurate, including the PDTs
|
|
115
|
+
* for the last discontinuity sequence number shared by both playlists when present,
|
|
116
|
+
* and uses the "wallclock"/PDT timeline as a cross-reference to `details`, adjusting the presentation
|
|
117
|
+
* times/timelines of `details` accordingly.
|
|
118
|
+
* Given the asynchronous nature of fetches and initial loads of live `main` and audio/subtitle tracks,
|
|
119
|
+
* the primary purpose of this function is to ensure the "local timelines" of audio/subtitle tracks
|
|
120
|
+
* are aligned to the main/video timeline, using PDT as the cross-reference/"anchor" that should
|
|
121
|
+
* be consistent across playlists, per the HLS spec.
|
|
122
|
+
* @param details - The details of the rendition you'd like to time-align (e.g. an audio rendition).
|
|
123
|
+
* @param refDetails - The details of the reference rendition with start and PDT times for alignment.
|
|
124
|
+
*/
|
|
125
|
+
export function alignMediaPlaylistByPDT(
|
|
126
|
+
details: LevelDetails,
|
|
127
|
+
refDetails: LevelDetails,
|
|
128
|
+
logger: ILogger,
|
|
129
|
+
) {
|
|
130
|
+
if (!details.hasProgramDateTime || !refDetails.hasProgramDateTime) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const fragments = details.fragments;
|
|
135
|
+
const refFragments = refDetails.fragments;
|
|
136
|
+
if (!fragments.length || !refFragments.length) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Calculate a delta to apply to all fragments according to the delta in PDT times and start times
|
|
141
|
+
// of a fragment in the reference details, and a fragment in the target details of the same discontinuity.
|
|
142
|
+
// If a fragment of the same discontinuity was not found use the middle fragment of both.
|
|
143
|
+
let refFrag: Fragment | null | undefined;
|
|
144
|
+
let frag: Fragment | null | undefined;
|
|
145
|
+
const targetCC = Math.min(refDetails.endCC, details.endCC);
|
|
146
|
+
if (refDetails.startCC < targetCC && details.startCC < targetCC) {
|
|
147
|
+
refFrag = findFirstFragWithCC(refFragments, targetCC);
|
|
148
|
+
frag = findFirstFragWithCC(fragments, targetCC);
|
|
149
|
+
}
|
|
150
|
+
if (!refFrag || !frag) {
|
|
151
|
+
refFrag = refFragments[Math.floor(refFragments.length / 2)];
|
|
152
|
+
frag =
|
|
153
|
+
findFirstFragWithCC(fragments, refFrag.cc) ||
|
|
154
|
+
fragments[Math.floor(fragments.length / 2)];
|
|
155
|
+
}
|
|
156
|
+
const refPDT = refFrag.programDateTime;
|
|
157
|
+
const targetPDT = frag.programDateTime;
|
|
158
|
+
if (!refPDT || !targetPDT) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const dateDifference = (targetPDT - refPDT) / 1000;
|
|
163
|
+
if (Math.abs(dateDifference) > Math.max(60, details.totalduration)) {
|
|
164
|
+
// Do not align on PDT if ranges differ significantly
|
|
165
|
+
logger.log(
|
|
166
|
+
`Cannot align playlists using PDT without overlap (${Math.abs(dateDifference)} > ${details.totalduration})`,
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const delta = dateDifference - (frag.start - refFrag.start);
|
|
172
|
+
logger.log(`Aligning playlists using PDT (diff: ${delta})`);
|
|
173
|
+
adjustSlidingStart(delta, details);
|
|
174
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DecrypterAesMode } from '../crypt/decrypter-aes-mode';
|
|
2
|
+
|
|
3
|
+
export function isFullSegmentEncryption(method: string): boolean {
|
|
4
|
+
return (
|
|
5
|
+
method === 'AES-128' || method === 'AES-256' || method === 'AES-256-CTR'
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getAesModeFromFullSegmentMethod(
|
|
10
|
+
method: string,
|
|
11
|
+
): DecrypterAesMode {
|
|
12
|
+
switch (method) {
|
|
13
|
+
case 'AES-128':
|
|
14
|
+
case 'AES-256':
|
|
15
|
+
return DecrypterAesMode.cbc;
|
|
16
|
+
case 'AES-256-CTR':
|
|
17
|
+
return DecrypterAesMode.ctr;
|
|
18
|
+
default:
|
|
19
|
+
throw new Error(`invalid full segment method ${method}`);
|
|
20
|
+
}
|
|
21
|
+
}
|