@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,54 @@
|
|
|
1
|
+
import { mimeTypeForCodec } from './utils/codecs';
|
|
2
|
+
import { getMediaSource } from './utils/mediasource-helper';
|
|
3
|
+
import type { ExtendedSourceBuffer } from './types/buffer';
|
|
4
|
+
|
|
5
|
+
function getSourceBuffer(): typeof self.SourceBuffer {
|
|
6
|
+
return self.SourceBuffer || (self as any).WebKitSourceBuffer;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isMSESupported(): boolean {
|
|
10
|
+
const mediaSource = getMediaSource();
|
|
11
|
+
if (!mediaSource) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// if SourceBuffer is exposed ensure its API is valid
|
|
16
|
+
// Older browsers do not expose SourceBuffer globally so checking SourceBuffer.prototype is impossible
|
|
17
|
+
const sourceBuffer = getSourceBuffer();
|
|
18
|
+
return (
|
|
19
|
+
!sourceBuffer ||
|
|
20
|
+
(sourceBuffer.prototype &&
|
|
21
|
+
typeof sourceBuffer.prototype.appendBuffer === 'function' &&
|
|
22
|
+
typeof sourceBuffer.prototype.remove === 'function')
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isSupported(): boolean {
|
|
27
|
+
if (!isMSESupported()) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const mediaSource = getMediaSource();
|
|
32
|
+
return (
|
|
33
|
+
typeof mediaSource?.isTypeSupported === 'function' &&
|
|
34
|
+
(['avc1.42E01E,mp4a.40.2', 'av01.0.01M.08', 'vp09.00.50.08'].some(
|
|
35
|
+
(codecsForVideoContainer) =>
|
|
36
|
+
mediaSource.isTypeSupported(
|
|
37
|
+
mimeTypeForCodec(codecsForVideoContainer, 'video'),
|
|
38
|
+
),
|
|
39
|
+
) ||
|
|
40
|
+
['mp4a.40.2', 'fLaC'].some((codecForAudioContainer) =>
|
|
41
|
+
mediaSource.isTypeSupported(
|
|
42
|
+
mimeTypeForCodec(codecForAudioContainer, 'audio'),
|
|
43
|
+
),
|
|
44
|
+
))
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function changeTypeSupported(): boolean {
|
|
49
|
+
const sourceBuffer = getSourceBuffer();
|
|
50
|
+
return (
|
|
51
|
+
typeof (sourceBuffer?.prototype as ExtendedSourceBuffer)?.changeType ===
|
|
52
|
+
'function'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { AttrList } from '../utils/attr-list';
|
|
2
|
+
import { logger } from '../utils/logger';
|
|
3
|
+
import type { MediaFragmentRef } from './fragment';
|
|
4
|
+
|
|
5
|
+
// Avoid exporting const enum so that these values can be inlined
|
|
6
|
+
const enum DateRangeAttribute {
|
|
7
|
+
ID = 'ID',
|
|
8
|
+
CLASS = 'CLASS',
|
|
9
|
+
CUE = 'CUE',
|
|
10
|
+
START_DATE = 'START-DATE',
|
|
11
|
+
DURATION = 'DURATION',
|
|
12
|
+
END_DATE = 'END-DATE',
|
|
13
|
+
END_ON_NEXT = 'END-ON-NEXT',
|
|
14
|
+
PLANNED_DURATION = 'PLANNED-DURATION',
|
|
15
|
+
SCTE35_OUT = 'SCTE35-OUT',
|
|
16
|
+
SCTE35_IN = 'SCTE35-IN',
|
|
17
|
+
SCTE35_CMD = 'SCTE35-CMD',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type DateRangeCue = {
|
|
21
|
+
pre: boolean;
|
|
22
|
+
post: boolean;
|
|
23
|
+
once: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const CLASS_INTERSTITIAL = 'com.apple.hls.interstitial';
|
|
27
|
+
|
|
28
|
+
export function isDateRangeCueAttribute(attrName: string): boolean {
|
|
29
|
+
return (
|
|
30
|
+
attrName !== DateRangeAttribute.ID &&
|
|
31
|
+
attrName !== DateRangeAttribute.CLASS &&
|
|
32
|
+
attrName !== DateRangeAttribute.CUE &&
|
|
33
|
+
attrName !== DateRangeAttribute.START_DATE &&
|
|
34
|
+
attrName !== DateRangeAttribute.DURATION &&
|
|
35
|
+
attrName !== DateRangeAttribute.END_DATE &&
|
|
36
|
+
attrName !== DateRangeAttribute.END_ON_NEXT
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isSCTE35Attribute(attrName: string): boolean {
|
|
41
|
+
return (
|
|
42
|
+
attrName === DateRangeAttribute.SCTE35_OUT ||
|
|
43
|
+
attrName === DateRangeAttribute.SCTE35_IN ||
|
|
44
|
+
attrName === DateRangeAttribute.SCTE35_CMD
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class DateRange {
|
|
49
|
+
public attr: AttrList;
|
|
50
|
+
public tagAnchor: MediaFragmentRef | null;
|
|
51
|
+
public tagOrder: number;
|
|
52
|
+
private _startDate: Date;
|
|
53
|
+
private _endDate?: Date;
|
|
54
|
+
private _dateAtEnd?: Date;
|
|
55
|
+
private _cue?: DateRangeCue;
|
|
56
|
+
private _badValueForSameId?: string;
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
dateRangeAttr: AttrList,
|
|
60
|
+
dateRangeWithSameId?: DateRange | undefined,
|
|
61
|
+
tagCount: number = 0,
|
|
62
|
+
) {
|
|
63
|
+
this.tagAnchor = dateRangeWithSameId?.tagAnchor || null;
|
|
64
|
+
this.tagOrder = dateRangeWithSameId?.tagOrder ?? tagCount;
|
|
65
|
+
if (dateRangeWithSameId) {
|
|
66
|
+
const previousAttr = dateRangeWithSameId.attr;
|
|
67
|
+
for (const key in previousAttr) {
|
|
68
|
+
if (
|
|
69
|
+
Object.prototype.hasOwnProperty.call(dateRangeAttr, key) &&
|
|
70
|
+
dateRangeAttr[key] !== previousAttr[key]
|
|
71
|
+
) {
|
|
72
|
+
logger.warn(
|
|
73
|
+
`DATERANGE tag attribute: "${key}" does not match for tags with ID: "${dateRangeAttr.ID}"`,
|
|
74
|
+
);
|
|
75
|
+
this._badValueForSameId = key;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Merge DateRange tags with the same ID
|
|
80
|
+
dateRangeAttr = Object.assign(
|
|
81
|
+
new AttrList({}),
|
|
82
|
+
previousAttr,
|
|
83
|
+
dateRangeAttr,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
this.attr = dateRangeAttr;
|
|
87
|
+
if (dateRangeWithSameId) {
|
|
88
|
+
this._startDate = dateRangeWithSameId._startDate;
|
|
89
|
+
this._cue = dateRangeWithSameId._cue;
|
|
90
|
+
this._endDate = dateRangeWithSameId._endDate;
|
|
91
|
+
this._dateAtEnd = dateRangeWithSameId._dateAtEnd;
|
|
92
|
+
} else {
|
|
93
|
+
this._startDate = new Date(dateRangeAttr[DateRangeAttribute.START_DATE]);
|
|
94
|
+
}
|
|
95
|
+
if (DateRangeAttribute.END_DATE in this.attr) {
|
|
96
|
+
const endDate =
|
|
97
|
+
dateRangeWithSameId?.endDate ||
|
|
98
|
+
new Date(this.attr[DateRangeAttribute.END_DATE]);
|
|
99
|
+
if (Number.isFinite(endDate.getTime())) {
|
|
100
|
+
this._endDate = endDate;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get id(): string {
|
|
106
|
+
return this.attr.ID;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get class(): string {
|
|
110
|
+
return this.attr.CLASS;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get cue(): DateRangeCue {
|
|
114
|
+
const _cue = this._cue;
|
|
115
|
+
if (_cue === undefined) {
|
|
116
|
+
return (this._cue = this.attr.enumeratedStringList(
|
|
117
|
+
this.attr.CUE ? 'CUE' : 'X-CUE',
|
|
118
|
+
{
|
|
119
|
+
pre: false,
|
|
120
|
+
post: false,
|
|
121
|
+
once: false,
|
|
122
|
+
},
|
|
123
|
+
));
|
|
124
|
+
}
|
|
125
|
+
return _cue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get startTime(): number {
|
|
129
|
+
const { tagAnchor } = this;
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
|
131
|
+
if (tagAnchor === null || tagAnchor.programDateTime === null) {
|
|
132
|
+
logger.warn(
|
|
133
|
+
`Expected tagAnchor Fragment with PDT set for DateRange "${this.id}": ${tagAnchor}`,
|
|
134
|
+
);
|
|
135
|
+
return NaN;
|
|
136
|
+
}
|
|
137
|
+
return (
|
|
138
|
+
tagAnchor.start +
|
|
139
|
+
(this.startDate.getTime() - tagAnchor.programDateTime) / 1000
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get startDate(): Date {
|
|
144
|
+
return this._startDate;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get endDate(): Date | null {
|
|
148
|
+
const dateAtEnd = this._endDate || this._dateAtEnd;
|
|
149
|
+
if (dateAtEnd) {
|
|
150
|
+
return dateAtEnd;
|
|
151
|
+
}
|
|
152
|
+
const duration = this.duration;
|
|
153
|
+
if (duration !== null) {
|
|
154
|
+
return (this._dateAtEnd = new Date(
|
|
155
|
+
this._startDate.getTime() + duration * 1000,
|
|
156
|
+
));
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get duration(): number | null {
|
|
162
|
+
if (DateRangeAttribute.DURATION in this.attr) {
|
|
163
|
+
const duration = this.attr.decimalFloatingPoint(
|
|
164
|
+
DateRangeAttribute.DURATION,
|
|
165
|
+
);
|
|
166
|
+
if (Number.isFinite(duration)) {
|
|
167
|
+
return duration;
|
|
168
|
+
}
|
|
169
|
+
} else if (this._endDate) {
|
|
170
|
+
return (this._endDate.getTime() - this._startDate.getTime()) / 1000;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get plannedDuration(): number | null {
|
|
176
|
+
if (DateRangeAttribute.PLANNED_DURATION in this.attr) {
|
|
177
|
+
return this.attr.decimalFloatingPoint(
|
|
178
|
+
DateRangeAttribute.PLANNED_DURATION,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
get endOnNext(): boolean {
|
|
185
|
+
return this.attr.bool(DateRangeAttribute.END_ON_NEXT);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
get isInterstitial(): boolean {
|
|
189
|
+
return this.class === CLASS_INTERSTITIAL;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
get isValid(): boolean {
|
|
193
|
+
return (
|
|
194
|
+
!!this.id &&
|
|
195
|
+
!this._badValueForSameId &&
|
|
196
|
+
Number.isFinite(this.startDate.getTime()) &&
|
|
197
|
+
(this.duration === null || this.duration >= 0) &&
|
|
198
|
+
(!this.endOnNext || !!this.class) &&
|
|
199
|
+
(!this.attr.CUE ||
|
|
200
|
+
(!this.cue.pre && !this.cue.post) ||
|
|
201
|
+
this.cue.pre !== this.cue.post) &&
|
|
202
|
+
(!this.isInterstitial ||
|
|
203
|
+
'X-ASSET-URI' in this.attr ||
|
|
204
|
+
'X-ASSET-LIST' in this.attr)
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { ErrorDetails, ErrorTypes } from '../errors';
|
|
2
|
+
import { getLoaderConfigWithoutReties } from '../utils/error-helper';
|
|
3
|
+
import type { BaseSegment, Fragment, Part } from './fragment';
|
|
4
|
+
import type { HlsConfig } from '../config';
|
|
5
|
+
import type {
|
|
6
|
+
ErrorData,
|
|
7
|
+
FragLoadedData,
|
|
8
|
+
PartsLoadedData,
|
|
9
|
+
} from '../types/events';
|
|
10
|
+
import type {
|
|
11
|
+
FragmentLoaderContext,
|
|
12
|
+
Loader,
|
|
13
|
+
LoaderCallbacks,
|
|
14
|
+
LoaderConfiguration,
|
|
15
|
+
} from '../types/loader';
|
|
16
|
+
import type { NullableNetworkDetails } from '../types/network-details';
|
|
17
|
+
|
|
18
|
+
const MIN_CHUNK_SIZE = Math.pow(2, 17); // 128kb
|
|
19
|
+
|
|
20
|
+
export default class FragmentLoader {
|
|
21
|
+
private readonly config: HlsConfig;
|
|
22
|
+
private loader: Loader<FragmentLoaderContext> | null = null;
|
|
23
|
+
private partLoadTimeout: number = -1;
|
|
24
|
+
|
|
25
|
+
constructor(config: HlsConfig) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
destroy() {
|
|
30
|
+
if (this.loader) {
|
|
31
|
+
this.loader.destroy();
|
|
32
|
+
this.loader = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
abort() {
|
|
37
|
+
if (this.loader) {
|
|
38
|
+
// Abort the loader for current fragment. Only one may load at any given time
|
|
39
|
+
this.loader.abort();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
load(
|
|
44
|
+
frag: Fragment,
|
|
45
|
+
onProgress?: FragmentLoadProgressCallback,
|
|
46
|
+
): Promise<FragLoadedData> {
|
|
47
|
+
const url = frag.url;
|
|
48
|
+
if (!url) {
|
|
49
|
+
return Promise.reject(
|
|
50
|
+
new LoadError({
|
|
51
|
+
type: ErrorTypes.NETWORK_ERROR,
|
|
52
|
+
details: ErrorDetails.FRAG_LOAD_ERROR,
|
|
53
|
+
fatal: false,
|
|
54
|
+
frag,
|
|
55
|
+
error: new Error(
|
|
56
|
+
`Fragment does not have a ${url ? 'part list' : 'url'}`,
|
|
57
|
+
),
|
|
58
|
+
networkDetails: null,
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
this.abort();
|
|
63
|
+
|
|
64
|
+
const config = this.config;
|
|
65
|
+
const FragmentILoader = config.fLoader;
|
|
66
|
+
const DefaultILoader = config.loader;
|
|
67
|
+
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
if (this.loader) {
|
|
70
|
+
this.loader.destroy();
|
|
71
|
+
}
|
|
72
|
+
if (frag.gap) {
|
|
73
|
+
if (frag.tagList.some((tags) => tags[0] === 'GAP')) {
|
|
74
|
+
reject(createGapLoadError(frag));
|
|
75
|
+
return;
|
|
76
|
+
} else {
|
|
77
|
+
// Reset temporary treatment as GAP tag
|
|
78
|
+
frag.gap = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const loader = (this.loader = FragmentILoader
|
|
82
|
+
? new FragmentILoader(config)
|
|
83
|
+
: (new DefaultILoader(config) as Loader<FragmentLoaderContext>));
|
|
84
|
+
const loaderContext = createLoaderContext(frag);
|
|
85
|
+
frag.loader = loader;
|
|
86
|
+
const loadPolicy = getLoaderConfigWithoutReties(
|
|
87
|
+
config.fragLoadPolicy.default,
|
|
88
|
+
);
|
|
89
|
+
const loaderConfig: LoaderConfiguration = {
|
|
90
|
+
loadPolicy,
|
|
91
|
+
timeout: loadPolicy.maxLoadTimeMs,
|
|
92
|
+
maxRetry: 0,
|
|
93
|
+
retryDelay: 0,
|
|
94
|
+
maxRetryDelay: 0,
|
|
95
|
+
highWaterMark: frag.sn === 'initSegment' ? Infinity : MIN_CHUNK_SIZE,
|
|
96
|
+
};
|
|
97
|
+
// Assign frag stats to the loader's stats reference
|
|
98
|
+
frag.stats = loader.stats;
|
|
99
|
+
const callbacks: LoaderCallbacks<FragmentLoaderContext> = {
|
|
100
|
+
onSuccess: (response, stats, context, networkDetails) => {
|
|
101
|
+
this.resetLoader(frag, loader);
|
|
102
|
+
let payload = response.data as ArrayBuffer;
|
|
103
|
+
if (context.resetIV && frag.decryptdata) {
|
|
104
|
+
frag.decryptdata.iv = new Uint8Array(payload.slice(0, 16));
|
|
105
|
+
payload = payload.slice(16);
|
|
106
|
+
}
|
|
107
|
+
resolve({
|
|
108
|
+
frag,
|
|
109
|
+
part: null,
|
|
110
|
+
payload,
|
|
111
|
+
networkDetails,
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
onError: (response, context, networkDetails, stats) => {
|
|
115
|
+
this.resetLoader(frag, loader);
|
|
116
|
+
reject(
|
|
117
|
+
new LoadError({
|
|
118
|
+
type: ErrorTypes.NETWORK_ERROR,
|
|
119
|
+
details: ErrorDetails.FRAG_LOAD_ERROR,
|
|
120
|
+
fatal: false,
|
|
121
|
+
frag,
|
|
122
|
+
response: { url, data: undefined, ...response },
|
|
123
|
+
error: new Error(`HTTP Error ${response.code} ${response.text}`),
|
|
124
|
+
networkDetails,
|
|
125
|
+
stats,
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
onAbort: (stats, context, networkDetails) => {
|
|
130
|
+
this.resetLoader(frag, loader);
|
|
131
|
+
reject(
|
|
132
|
+
new LoadError({
|
|
133
|
+
type: ErrorTypes.NETWORK_ERROR,
|
|
134
|
+
details: ErrorDetails.INTERNAL_ABORTED,
|
|
135
|
+
fatal: false,
|
|
136
|
+
frag,
|
|
137
|
+
error: new Error('Aborted'),
|
|
138
|
+
networkDetails,
|
|
139
|
+
stats,
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
onTimeout: (stats, context, networkDetails) => {
|
|
144
|
+
this.resetLoader(frag, loader);
|
|
145
|
+
reject(
|
|
146
|
+
new LoadError({
|
|
147
|
+
type: ErrorTypes.NETWORK_ERROR,
|
|
148
|
+
details: ErrorDetails.FRAG_LOAD_TIMEOUT,
|
|
149
|
+
fatal: false,
|
|
150
|
+
frag,
|
|
151
|
+
error: new Error(`Timeout after ${loaderConfig.timeout}ms`),
|
|
152
|
+
networkDetails,
|
|
153
|
+
stats,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
if (onProgress) {
|
|
159
|
+
callbacks.onProgress = (stats, context, data, networkDetails) =>
|
|
160
|
+
onProgress({
|
|
161
|
+
frag,
|
|
162
|
+
part: null,
|
|
163
|
+
payload: data as ArrayBuffer,
|
|
164
|
+
networkDetails,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
loader.load(loaderContext, loaderConfig, callbacks);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public loadPart(
|
|
172
|
+
frag: Fragment,
|
|
173
|
+
part: Part,
|
|
174
|
+
onProgress: FragmentLoadProgressCallback,
|
|
175
|
+
): Promise<FragLoadedData> {
|
|
176
|
+
this.abort();
|
|
177
|
+
|
|
178
|
+
const config = this.config;
|
|
179
|
+
const FragmentILoader = config.fLoader;
|
|
180
|
+
const DefaultILoader = config.loader;
|
|
181
|
+
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
if (this.loader) {
|
|
184
|
+
this.loader.destroy();
|
|
185
|
+
}
|
|
186
|
+
if (frag.gap || part.gap) {
|
|
187
|
+
reject(createGapLoadError(frag, part));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const loader = (this.loader = FragmentILoader
|
|
191
|
+
? new FragmentILoader(config)
|
|
192
|
+
: (new DefaultILoader(config) as Loader<FragmentLoaderContext>));
|
|
193
|
+
const loaderContext = createLoaderContext(frag, part);
|
|
194
|
+
frag.loader = loader;
|
|
195
|
+
// Should we define another load policy for parts?
|
|
196
|
+
const loadPolicy = getLoaderConfigWithoutReties(
|
|
197
|
+
config.fragLoadPolicy.default,
|
|
198
|
+
);
|
|
199
|
+
const loaderConfig: LoaderConfiguration = {
|
|
200
|
+
loadPolicy,
|
|
201
|
+
timeout: loadPolicy.maxLoadTimeMs,
|
|
202
|
+
maxRetry: 0,
|
|
203
|
+
retryDelay: 0,
|
|
204
|
+
maxRetryDelay: 0,
|
|
205
|
+
highWaterMark: MIN_CHUNK_SIZE,
|
|
206
|
+
};
|
|
207
|
+
// Assign part stats to the loader's stats reference
|
|
208
|
+
part.stats = loader.stats;
|
|
209
|
+
loader.load(loaderContext, loaderConfig, {
|
|
210
|
+
onSuccess: (response, stats, context, networkDetails) => {
|
|
211
|
+
this.resetLoader(frag, loader);
|
|
212
|
+
this.updateStatsFromPart(frag, part);
|
|
213
|
+
const partLoadedData: FragLoadedData = {
|
|
214
|
+
frag,
|
|
215
|
+
part,
|
|
216
|
+
payload: response.data as ArrayBuffer,
|
|
217
|
+
networkDetails,
|
|
218
|
+
};
|
|
219
|
+
onProgress(partLoadedData);
|
|
220
|
+
resolve(partLoadedData);
|
|
221
|
+
},
|
|
222
|
+
onError: (response, context, networkDetails, stats) => {
|
|
223
|
+
this.resetLoader(frag, loader);
|
|
224
|
+
reject(
|
|
225
|
+
new LoadError({
|
|
226
|
+
type: ErrorTypes.NETWORK_ERROR,
|
|
227
|
+
details: ErrorDetails.FRAG_LOAD_ERROR,
|
|
228
|
+
fatal: false,
|
|
229
|
+
frag,
|
|
230
|
+
part,
|
|
231
|
+
response: {
|
|
232
|
+
url: loaderContext.url,
|
|
233
|
+
data: undefined,
|
|
234
|
+
...response,
|
|
235
|
+
},
|
|
236
|
+
error: new Error(`HTTP Error ${response.code} ${response.text}`),
|
|
237
|
+
networkDetails,
|
|
238
|
+
stats,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
},
|
|
242
|
+
onAbort: (stats, context, networkDetails) => {
|
|
243
|
+
frag.stats.aborted = part.stats.aborted;
|
|
244
|
+
this.resetLoader(frag, loader);
|
|
245
|
+
reject(
|
|
246
|
+
new LoadError({
|
|
247
|
+
type: ErrorTypes.NETWORK_ERROR,
|
|
248
|
+
details: ErrorDetails.INTERNAL_ABORTED,
|
|
249
|
+
fatal: false,
|
|
250
|
+
frag,
|
|
251
|
+
part,
|
|
252
|
+
error: new Error('Aborted'),
|
|
253
|
+
networkDetails,
|
|
254
|
+
stats,
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
},
|
|
258
|
+
onTimeout: (stats, context, networkDetails) => {
|
|
259
|
+
this.resetLoader(frag, loader);
|
|
260
|
+
reject(
|
|
261
|
+
new LoadError({
|
|
262
|
+
type: ErrorTypes.NETWORK_ERROR,
|
|
263
|
+
details: ErrorDetails.FRAG_LOAD_TIMEOUT,
|
|
264
|
+
fatal: false,
|
|
265
|
+
frag,
|
|
266
|
+
part,
|
|
267
|
+
error: new Error(`Timeout after ${loaderConfig.timeout}ms`),
|
|
268
|
+
networkDetails,
|
|
269
|
+
stats,
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private updateStatsFromPart(frag: Fragment, part: Part) {
|
|
278
|
+
const fragStats = frag.stats;
|
|
279
|
+
const partStats = part.stats;
|
|
280
|
+
const partTotal = partStats.total;
|
|
281
|
+
fragStats.loaded += partStats.loaded;
|
|
282
|
+
if (partTotal) {
|
|
283
|
+
const estTotalParts = Math.round(frag.duration / part.duration);
|
|
284
|
+
const estLoadedParts = Math.min(
|
|
285
|
+
Math.round(fragStats.loaded / partTotal),
|
|
286
|
+
estTotalParts,
|
|
287
|
+
);
|
|
288
|
+
const estRemainingParts = estTotalParts - estLoadedParts;
|
|
289
|
+
const estRemainingBytes =
|
|
290
|
+
estRemainingParts * Math.round(fragStats.loaded / estLoadedParts);
|
|
291
|
+
fragStats.total = fragStats.loaded + estRemainingBytes;
|
|
292
|
+
} else {
|
|
293
|
+
fragStats.total = Math.max(fragStats.loaded, fragStats.total);
|
|
294
|
+
}
|
|
295
|
+
const fragLoading = fragStats.loading;
|
|
296
|
+
const partLoading = partStats.loading;
|
|
297
|
+
if (fragLoading.start) {
|
|
298
|
+
// add to fragment loader latency
|
|
299
|
+
fragLoading.first += partLoading.first - partLoading.start;
|
|
300
|
+
} else {
|
|
301
|
+
fragLoading.start = partLoading.start;
|
|
302
|
+
fragLoading.first = partLoading.first;
|
|
303
|
+
}
|
|
304
|
+
fragLoading.end = partLoading.end;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private resetLoader(frag: Fragment, loader: Loader<FragmentLoaderContext>) {
|
|
308
|
+
frag.loader = null;
|
|
309
|
+
if (this.loader === loader) {
|
|
310
|
+
self.clearTimeout(this.partLoadTimeout);
|
|
311
|
+
this.loader = null;
|
|
312
|
+
}
|
|
313
|
+
loader.destroy();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function createLoaderContext(
|
|
318
|
+
frag: Fragment,
|
|
319
|
+
part: Part | null = null,
|
|
320
|
+
): FragmentLoaderContext {
|
|
321
|
+
const segment: BaseSegment = part || frag;
|
|
322
|
+
const loaderContext: FragmentLoaderContext = {
|
|
323
|
+
frag,
|
|
324
|
+
part,
|
|
325
|
+
responseType: 'arraybuffer',
|
|
326
|
+
url: segment.url,
|
|
327
|
+
headers: {},
|
|
328
|
+
rangeStart: 0,
|
|
329
|
+
rangeEnd: 0,
|
|
330
|
+
};
|
|
331
|
+
const start = segment.byteRangeStartOffset as number;
|
|
332
|
+
const end = segment.byteRangeEndOffset as number;
|
|
333
|
+
if (Number.isFinite(start) && Number.isFinite(end)) {
|
|
334
|
+
let byteRangeStart = start;
|
|
335
|
+
let byteRangeEnd = end;
|
|
336
|
+
if (
|
|
337
|
+
frag.sn === 'initSegment' &&
|
|
338
|
+
isMethodFullSegmentAesCbc(frag.decryptdata?.method)
|
|
339
|
+
) {
|
|
340
|
+
// MAP segment encrypted with method 'AES-128' or 'AES-256' (cbc), when served with HTTP Range,
|
|
341
|
+
// has the unencrypted size specified in the range.
|
|
342
|
+
// Ref: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6
|
|
343
|
+
const fragmentLen = end - start;
|
|
344
|
+
if (fragmentLen % 16) {
|
|
345
|
+
byteRangeEnd = end + (16 - (fragmentLen % 16));
|
|
346
|
+
}
|
|
347
|
+
if (start !== 0) {
|
|
348
|
+
loaderContext.resetIV = true;
|
|
349
|
+
byteRangeStart = start - 16;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
loaderContext.rangeStart = byteRangeStart;
|
|
353
|
+
loaderContext.rangeEnd = byteRangeEnd;
|
|
354
|
+
}
|
|
355
|
+
return loaderContext;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function createGapLoadError(frag: Fragment, part?: Part): LoadError {
|
|
359
|
+
const error = new Error(`GAP ${frag.gap ? 'tag' : 'attribute'} found`);
|
|
360
|
+
const errorData: FragLoadFailResult = {
|
|
361
|
+
type: ErrorTypes.MEDIA_ERROR,
|
|
362
|
+
details: ErrorDetails.FRAG_GAP,
|
|
363
|
+
fatal: false,
|
|
364
|
+
frag,
|
|
365
|
+
error,
|
|
366
|
+
networkDetails: null,
|
|
367
|
+
};
|
|
368
|
+
if (part) {
|
|
369
|
+
errorData.part = part;
|
|
370
|
+
}
|
|
371
|
+
(part ? part : frag).stats.aborted = true;
|
|
372
|
+
return new LoadError(errorData);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function isMethodFullSegmentAesCbc(method) {
|
|
376
|
+
return method === 'AES-128' || method === 'AES-256';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export class LoadError extends Error {
|
|
380
|
+
public readonly data: FragLoadFailResult;
|
|
381
|
+
constructor(data: FragLoadFailResult) {
|
|
382
|
+
super(data.error.message);
|
|
383
|
+
this.data = data;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export interface FragLoadFailResult extends ErrorData {
|
|
388
|
+
frag: Fragment;
|
|
389
|
+
part?: Part;
|
|
390
|
+
response?: {
|
|
391
|
+
data: any;
|
|
392
|
+
// error status code
|
|
393
|
+
code: number;
|
|
394
|
+
// error description
|
|
395
|
+
text: string;
|
|
396
|
+
url: string;
|
|
397
|
+
};
|
|
398
|
+
networkDetails: NullableNetworkDetails;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export type FragmentLoadProgressCallback = (
|
|
402
|
+
result: FragLoadedData | PartsLoadedData,
|
|
403
|
+
) => void;
|