@zenvor/hls.js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +472 -0
  3. package/dist/hls-demo.js +26995 -0
  4. package/dist/hls-demo.js.map +1 -0
  5. package/dist/hls.d.mts +4204 -0
  6. package/dist/hls.d.ts +4204 -0
  7. package/dist/hls.js +40050 -0
  8. package/dist/hls.js.d.ts +4204 -0
  9. package/dist/hls.js.map +1 -0
  10. package/dist/hls.light.js +27145 -0
  11. package/dist/hls.light.js.map +1 -0
  12. package/dist/hls.light.min.js +2 -0
  13. package/dist/hls.light.min.js.map +1 -0
  14. package/dist/hls.light.mjs +26392 -0
  15. package/dist/hls.light.mjs.map +1 -0
  16. package/dist/hls.min.js +2 -0
  17. package/dist/hls.min.js.map +1 -0
  18. package/dist/hls.mjs +38956 -0
  19. package/dist/hls.mjs.map +1 -0
  20. package/dist/hls.worker.js +2 -0
  21. package/dist/hls.worker.js.map +1 -0
  22. package/package.json +143 -0
  23. package/src/config.ts +794 -0
  24. package/src/controller/abr-controller.ts +1019 -0
  25. package/src/controller/algo-data-controller.ts +794 -0
  26. package/src/controller/audio-stream-controller.ts +1099 -0
  27. package/src/controller/audio-track-controller.ts +454 -0
  28. package/src/controller/base-playlist-controller.ts +438 -0
  29. package/src/controller/base-stream-controller.ts +2526 -0
  30. package/src/controller/buffer-controller.ts +2015 -0
  31. package/src/controller/buffer-operation-queue.ts +159 -0
  32. package/src/controller/cap-level-controller.ts +367 -0
  33. package/src/controller/cmcd-controller.ts +422 -0
  34. package/src/controller/content-steering-controller.ts +622 -0
  35. package/src/controller/eme-controller.ts +1617 -0
  36. package/src/controller/error-controller.ts +627 -0
  37. package/src/controller/fps-controller.ts +146 -0
  38. package/src/controller/fragment-finders.ts +256 -0
  39. package/src/controller/fragment-tracker.ts +567 -0
  40. package/src/controller/gap-controller.ts +719 -0
  41. package/src/controller/id3-track-controller.ts +488 -0
  42. package/src/controller/interstitial-player.ts +302 -0
  43. package/src/controller/interstitials-controller.ts +2895 -0
  44. package/src/controller/interstitials-schedule.ts +698 -0
  45. package/src/controller/latency-controller.ts +294 -0
  46. package/src/controller/level-controller.ts +776 -0
  47. package/src/controller/stream-controller.ts +1597 -0
  48. package/src/controller/subtitle-stream-controller.ts +508 -0
  49. package/src/controller/subtitle-track-controller.ts +617 -0
  50. package/src/controller/timeline-controller.ts +677 -0
  51. package/src/crypt/aes-crypto.ts +36 -0
  52. package/src/crypt/aes-decryptor.ts +339 -0
  53. package/src/crypt/decrypter-aes-mode.ts +4 -0
  54. package/src/crypt/decrypter.ts +225 -0
  55. package/src/crypt/fast-aes-key.ts +39 -0
  56. package/src/define-plugin.d.ts +17 -0
  57. package/src/demux/audio/aacdemuxer.ts +126 -0
  58. package/src/demux/audio/ac3-demuxer.ts +170 -0
  59. package/src/demux/audio/adts.ts +249 -0
  60. package/src/demux/audio/base-audio-demuxer.ts +205 -0
  61. package/src/demux/audio/dolby.ts +21 -0
  62. package/src/demux/audio/mp3demuxer.ts +85 -0
  63. package/src/demux/audio/mpegaudio.ts +177 -0
  64. package/src/demux/chunk-cache.ts +42 -0
  65. package/src/demux/dummy-demuxed-track.ts +13 -0
  66. package/src/demux/inject-worker.ts +75 -0
  67. package/src/demux/mp4demuxer.ts +234 -0
  68. package/src/demux/sample-aes.ts +198 -0
  69. package/src/demux/transmuxer-interface.ts +449 -0
  70. package/src/demux/transmuxer-worker.ts +221 -0
  71. package/src/demux/transmuxer.ts +560 -0
  72. package/src/demux/tsdemuxer.ts +1256 -0
  73. package/src/demux/video/avc-video-parser.ts +401 -0
  74. package/src/demux/video/base-video-parser.ts +198 -0
  75. package/src/demux/video/exp-golomb.ts +153 -0
  76. package/src/demux/video/hevc-video-parser.ts +736 -0
  77. package/src/empty-es.js +5 -0
  78. package/src/empty.js +3 -0
  79. package/src/errors.ts +107 -0
  80. package/src/events.ts +548 -0
  81. package/src/exports-default.ts +3 -0
  82. package/src/exports-named.ts +81 -0
  83. package/src/hls.ts +1613 -0
  84. package/src/is-supported.ts +54 -0
  85. package/src/loader/date-range.ts +207 -0
  86. package/src/loader/fragment-loader.ts +403 -0
  87. package/src/loader/fragment.ts +487 -0
  88. package/src/loader/interstitial-asset-list.ts +162 -0
  89. package/src/loader/interstitial-event.ts +337 -0
  90. package/src/loader/key-loader.ts +439 -0
  91. package/src/loader/level-details.ts +203 -0
  92. package/src/loader/level-key.ts +259 -0
  93. package/src/loader/load-stats.ts +17 -0
  94. package/src/loader/m3u8-parser.ts +1072 -0
  95. package/src/loader/playlist-loader.ts +839 -0
  96. package/src/polyfills/number.ts +15 -0
  97. package/src/remux/aac-helper.ts +81 -0
  98. package/src/remux/mp4-generator.ts +1380 -0
  99. package/src/remux/mp4-remuxer.ts +1261 -0
  100. package/src/remux/passthrough-remuxer.ts +434 -0
  101. package/src/task-loop.ts +130 -0
  102. package/src/types/algo.ts +44 -0
  103. package/src/types/buffer.ts +105 -0
  104. package/src/types/component-api.ts +20 -0
  105. package/src/types/demuxer.ts +208 -0
  106. package/src/types/events.ts +574 -0
  107. package/src/types/fragment-tracker.ts +23 -0
  108. package/src/types/level.ts +268 -0
  109. package/src/types/loader.ts +198 -0
  110. package/src/types/media-playlist.ts +92 -0
  111. package/src/types/network-details.ts +3 -0
  112. package/src/types/remuxer.ts +104 -0
  113. package/src/types/track.ts +12 -0
  114. package/src/types/transmuxer.ts +46 -0
  115. package/src/types/tuples.ts +6 -0
  116. package/src/types/vtt.ts +11 -0
  117. package/src/utils/arrays.ts +22 -0
  118. package/src/utils/attr-list.ts +192 -0
  119. package/src/utils/binary-search.ts +46 -0
  120. package/src/utils/buffer-helper.ts +173 -0
  121. package/src/utils/cea-608-parser.ts +1413 -0
  122. package/src/utils/chunker.ts +41 -0
  123. package/src/utils/codecs.ts +314 -0
  124. package/src/utils/cues.ts +96 -0
  125. package/src/utils/discontinuities.ts +174 -0
  126. package/src/utils/encryption-methods-util.ts +21 -0
  127. package/src/utils/error-helper.ts +95 -0
  128. package/src/utils/event-listener-helper.ts +16 -0
  129. package/src/utils/ewma-bandwidth-estimator.ts +97 -0
  130. package/src/utils/ewma.ts +43 -0
  131. package/src/utils/fetch-loader.ts +331 -0
  132. package/src/utils/global.ts +2 -0
  133. package/src/utils/hash.ts +10 -0
  134. package/src/utils/hdr.ts +67 -0
  135. package/src/utils/hex.ts +32 -0
  136. package/src/utils/imsc1-ttml-parser.ts +261 -0
  137. package/src/utils/keysystem-util.ts +45 -0
  138. package/src/utils/level-helper.ts +629 -0
  139. package/src/utils/logger.ts +120 -0
  140. package/src/utils/media-option-attributes.ts +49 -0
  141. package/src/utils/mediacapabilities-helper.ts +301 -0
  142. package/src/utils/mediakeys-helper.ts +210 -0
  143. package/src/utils/mediasource-helper.ts +37 -0
  144. package/src/utils/mp4-tools.ts +1473 -0
  145. package/src/utils/number.ts +3 -0
  146. package/src/utils/numeric-encoding-utils.ts +26 -0
  147. package/src/utils/output-filter.ts +46 -0
  148. package/src/utils/rendition-helper.ts +505 -0
  149. package/src/utils/safe-json-stringify.ts +22 -0
  150. package/src/utils/texttrack-utils.ts +164 -0
  151. package/src/utils/time-ranges.ts +17 -0
  152. package/src/utils/timescale-conversion.ts +46 -0
  153. package/src/utils/utf8-utils.ts +18 -0
  154. package/src/utils/variable-substitution.ts +105 -0
  155. package/src/utils/vttcue.ts +384 -0
  156. package/src/utils/vttparser.ts +497 -0
  157. package/src/utils/webvtt-parser.ts +166 -0
  158. package/src/utils/xhr-loader.ts +337 -0
  159. package/src/version.ts +1 -0
@@ -0,0 +1,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;