@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,95 @@
1
+ import { ErrorDetails } from '../errors';
2
+ import type { LoaderConfig, LoadPolicy, RetryConfig } from '../config';
3
+ import type { ErrorData } from '../types/events';
4
+ import type { LoaderResponse } from '../types/loader';
5
+
6
+ export function isTimeoutError(error: ErrorData): boolean {
7
+ switch (error.details) {
8
+ case ErrorDetails.FRAG_LOAD_TIMEOUT:
9
+ case ErrorDetails.KEY_LOAD_TIMEOUT:
10
+ case ErrorDetails.LEVEL_LOAD_TIMEOUT:
11
+ case ErrorDetails.MANIFEST_LOAD_TIMEOUT:
12
+ case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT:
13
+ case ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT:
14
+ case ErrorDetails.ASSET_LIST_LOAD_TIMEOUT:
15
+ return true;
16
+ }
17
+ return false;
18
+ }
19
+
20
+ export function isKeyError(error: ErrorData): boolean {
21
+ return error.details.startsWith('key');
22
+ }
23
+
24
+ export function isUnusableKeyError(error: ErrorData): boolean {
25
+ return isKeyError(error) && !!error.frag && !error.frag.decryptdata;
26
+ }
27
+
28
+ export function getRetryConfig(
29
+ loadPolicy: LoadPolicy,
30
+ error: ErrorData,
31
+ ): RetryConfig | null {
32
+ const isTimeout = isTimeoutError(error);
33
+ return loadPolicy.default[`${isTimeout ? 'timeout' : 'error'}Retry`];
34
+ }
35
+
36
+ export function getRetryDelay(
37
+ retryConfig: RetryConfig,
38
+ retryCount: number,
39
+ ): number {
40
+ // exponential backoff capped to max retry delay
41
+ const backoffFactor =
42
+ retryConfig.backoff === 'linear' ? 1 : Math.pow(2, retryCount);
43
+ return Math.min(
44
+ backoffFactor * retryConfig.retryDelayMs,
45
+ retryConfig.maxRetryDelayMs,
46
+ );
47
+ }
48
+
49
+ export function getLoaderConfigWithoutReties(
50
+ loderConfig: LoaderConfig,
51
+ ): LoaderConfig {
52
+ return {
53
+ ...loderConfig,
54
+ ...{
55
+ errorRetry: null,
56
+ timeoutRetry: null,
57
+ },
58
+ };
59
+ }
60
+
61
+ export function shouldRetry(
62
+ retryConfig: RetryConfig | null | undefined,
63
+ retryCount: number,
64
+ isTimeout: boolean,
65
+ loaderResponse?: LoaderResponse | undefined,
66
+ ): retryConfig is RetryConfig & boolean {
67
+ if (!retryConfig) {
68
+ return false;
69
+ }
70
+ const httpStatus = loaderResponse?.code;
71
+ const retry =
72
+ retryCount < retryConfig.maxNumRetry &&
73
+ (retryForHttpStatus(httpStatus) || !!isTimeout);
74
+ return retryConfig.shouldRetry
75
+ ? retryConfig.shouldRetry(
76
+ retryConfig,
77
+ retryCount,
78
+ isTimeout,
79
+ loaderResponse,
80
+ retry,
81
+ )
82
+ : retry;
83
+ }
84
+
85
+ export function retryForHttpStatus(httpStatus: number | undefined): boolean {
86
+ // Do not retry on status 4xx, status 0 (CORS error), or undefined (decrypt/gap/parse error)
87
+ return (
88
+ offlineHttpStatus(httpStatus) ||
89
+ (!!httpStatus && (httpStatus < 400 || httpStatus > 499))
90
+ );
91
+ }
92
+
93
+ export function offlineHttpStatus(httpStatus: number | undefined): boolean {
94
+ return httpStatus === 0 && navigator.onLine === false;
95
+ }
@@ -0,0 +1,16 @@
1
+ export function addEventListener(
2
+ el: EventTarget,
3
+ type: string,
4
+ listener: EventListenerOrEventListenerObject,
5
+ ) {
6
+ removeEventListener(el, type, listener);
7
+ el.addEventListener(type, listener);
8
+ }
9
+
10
+ export function removeEventListener(
11
+ el: EventTarget,
12
+ type: string,
13
+ listener: EventListenerOrEventListenerObject,
14
+ ) {
15
+ el.removeEventListener(type, listener);
16
+ }
@@ -0,0 +1,97 @@
1
+ /*
2
+ * EWMA Bandwidth Estimator
3
+ * - heavily inspired from shaka-player
4
+ * Tracks bandwidth samples and estimates available bandwidth.
5
+ * Based on the minimum of two exponentially-weighted moving averages with
6
+ * different half-lives.
7
+ */
8
+
9
+ import EWMA from '../utils/ewma';
10
+
11
+ class EwmaBandWidthEstimator {
12
+ private defaultEstimate_: number;
13
+ private minWeight_: number;
14
+ private minDelayMs_: number;
15
+ private slow_: EWMA;
16
+ private fast_: EWMA;
17
+ private defaultTTFB_: number;
18
+ private ttfb_: EWMA;
19
+
20
+ constructor(
21
+ slow: number,
22
+ fast: number,
23
+ defaultEstimate: number,
24
+ defaultTTFB: number = 100,
25
+ ) {
26
+ this.defaultEstimate_ = defaultEstimate;
27
+ this.minWeight_ = 0.001;
28
+ this.minDelayMs_ = 50;
29
+ this.slow_ = new EWMA(slow);
30
+ this.fast_ = new EWMA(fast);
31
+ this.defaultTTFB_ = defaultTTFB;
32
+ this.ttfb_ = new EWMA(slow);
33
+ }
34
+
35
+ update(slow: number, fast: number) {
36
+ const { slow_, fast_, ttfb_ } = this;
37
+ if (slow_.halfLife !== slow) {
38
+ this.slow_ = new EWMA(slow, slow_.getEstimate(), slow_.getTotalWeight());
39
+ }
40
+ if (fast_.halfLife !== fast) {
41
+ this.fast_ = new EWMA(fast, fast_.getEstimate(), fast_.getTotalWeight());
42
+ }
43
+ if (ttfb_.halfLife !== slow) {
44
+ this.ttfb_ = new EWMA(slow, ttfb_.getEstimate(), ttfb_.getTotalWeight());
45
+ }
46
+ }
47
+
48
+ sample(durationMs: number, numBytes: number) {
49
+ durationMs = Math.max(durationMs, this.minDelayMs_);
50
+ const numBits = 8 * numBytes;
51
+ // weight is duration in seconds
52
+ const durationS = durationMs / 1000;
53
+ // value is bandwidth in bits/s
54
+ const bandwidthInBps = numBits / durationS;
55
+ this.fast_.sample(durationS, bandwidthInBps);
56
+ this.slow_.sample(durationS, bandwidthInBps);
57
+ }
58
+
59
+ sampleTTFB(ttfb: number) {
60
+ // weight is frequency curve applied to TTFB in seconds
61
+ // (longer times have less weight with expected input under 1 second)
62
+ const seconds = ttfb / 1000;
63
+ const weight = Math.sqrt(2) * Math.exp(-Math.pow(seconds, 2) / 2);
64
+ this.ttfb_.sample(weight, Math.max(ttfb, 5));
65
+ }
66
+
67
+ canEstimate(): boolean {
68
+ return this.fast_.getTotalWeight() >= this.minWeight_;
69
+ }
70
+
71
+ getEstimate(): number {
72
+ if (this.canEstimate()) {
73
+ // console.log('slow estimate:'+ Math.round(this.slow_.getEstimate()));
74
+ // console.log('fast estimate:'+ Math.round(this.fast_.getEstimate()));
75
+ // Take the minimum of these two estimates. This should have the effect of
76
+ // adapting down quickly, but up more slowly.
77
+ return Math.min(this.fast_.getEstimate(), this.slow_.getEstimate());
78
+ } else {
79
+ return this.defaultEstimate_;
80
+ }
81
+ }
82
+
83
+ getEstimateTTFB(): number {
84
+ if (this.ttfb_.getTotalWeight() >= this.minWeight_) {
85
+ return this.ttfb_.getEstimate();
86
+ } else {
87
+ return this.defaultTTFB_;
88
+ }
89
+ }
90
+
91
+ get defaultEstimate(): number {
92
+ return this.defaultEstimate_;
93
+ }
94
+
95
+ destroy() {}
96
+ }
97
+ export default EwmaBandWidthEstimator;
@@ -0,0 +1,43 @@
1
+ /*
2
+ * compute an Exponential Weighted moving average
3
+ * - https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
4
+ * - heavily inspired from shaka-player
5
+ */
6
+
7
+ class EWMA {
8
+ public readonly halfLife: number;
9
+ private alpha_: number;
10
+ private estimate_: number;
11
+ private totalWeight_: number;
12
+
13
+ // About half of the estimated value will be from the last |halfLife| samples by weight.
14
+ constructor(halfLife: number, estimate: number = 0, weight: number = 0) {
15
+ this.halfLife = halfLife;
16
+ // Larger values of alpha expire historical data more slowly.
17
+ this.alpha_ = halfLife ? Math.exp(Math.log(0.5) / halfLife) : 0;
18
+ this.estimate_ = estimate;
19
+ this.totalWeight_ = weight;
20
+ }
21
+
22
+ sample(weight: number, value: number) {
23
+ const adjAlpha = Math.pow(this.alpha_, weight);
24
+ this.estimate_ = value * (1 - adjAlpha) + adjAlpha * this.estimate_;
25
+ this.totalWeight_ += weight;
26
+ }
27
+
28
+ getTotalWeight(): number {
29
+ return this.totalWeight_;
30
+ }
31
+
32
+ getEstimate(): number {
33
+ if (this.alpha_) {
34
+ const zeroFactor = 1 - Math.pow(this.alpha_, this.totalWeight_);
35
+ if (zeroFactor) {
36
+ return this.estimate_ / zeroFactor;
37
+ }
38
+ }
39
+ return this.estimate_;
40
+ }
41
+ }
42
+
43
+ export default EWMA;
@@ -0,0 +1,331 @@
1
+ import ChunkCache from '../demux/chunk-cache';
2
+ import { isPromise } from '../demux/transmuxer';
3
+ import { LoadStats } from '../loader/load-stats';
4
+ import type { HlsConfig } from '../config';
5
+ import type {
6
+ Loader,
7
+ LoaderCallbacks,
8
+ LoaderConfiguration,
9
+ LoaderContext,
10
+ LoaderOnProgress,
11
+ LoaderResponse,
12
+ LoaderStats,
13
+ } from '../types/loader';
14
+
15
+ export function fetchSupported() {
16
+ if (
17
+ // @ts-ignore
18
+ self.fetch &&
19
+ self.AbortController &&
20
+ self.ReadableStream &&
21
+ self.Request
22
+ ) {
23
+ try {
24
+ new self.ReadableStream({}); // eslint-disable-line no-new
25
+ return true;
26
+ } catch (e) {
27
+ /* noop */
28
+ }
29
+ }
30
+ return false;
31
+ }
32
+
33
+ const BYTERANGE = /(\d+)-(\d+)\/(\d+)/;
34
+
35
+ class FetchLoader implements Loader<LoaderContext> {
36
+ private fetchSetup: NonNullable<HlsConfig['fetchSetup']>;
37
+ private requestTimeout?: number;
38
+ private request: Promise<Request> | Request | null = null;
39
+ private response: Response | null = null;
40
+ private controller: AbortController;
41
+ public context: LoaderContext | null = null;
42
+ private config: LoaderConfiguration | null = null;
43
+ private callbacks: LoaderCallbacks<LoaderContext> | null = null;
44
+ public stats: LoaderStats;
45
+ private loader: Response | null = null;
46
+
47
+ constructor(config: HlsConfig) {
48
+ this.fetchSetup = config.fetchSetup || getRequest;
49
+ this.controller = new self.AbortController();
50
+ this.stats = new LoadStats();
51
+ }
52
+
53
+ destroy(): void {
54
+ this.loader =
55
+ this.callbacks =
56
+ this.context =
57
+ this.config =
58
+ this.request =
59
+ null;
60
+ this.abortInternal();
61
+ this.response = null;
62
+ // @ts-ignore
63
+ this.fetchSetup = this.controller = this.stats = null;
64
+ }
65
+
66
+ abortInternal(): void {
67
+ if (this.controller && !this.stats.loading.end) {
68
+ this.stats.aborted = true;
69
+ this.controller.abort();
70
+ }
71
+ }
72
+
73
+ abort(): void {
74
+ this.abortInternal();
75
+ if (this.callbacks?.onAbort) {
76
+ this.callbacks.onAbort(
77
+ this.stats,
78
+ this.context as LoaderContext,
79
+ this.response,
80
+ );
81
+ }
82
+ }
83
+
84
+ load(
85
+ context: LoaderContext,
86
+ config: LoaderConfiguration,
87
+ callbacks: LoaderCallbacks<LoaderContext>,
88
+ ): void {
89
+ const stats = this.stats;
90
+ if (stats.loading.start) {
91
+ throw new Error('Loader can only be used once.');
92
+ }
93
+ stats.loading.start = self.performance.now();
94
+
95
+ const initParams = getRequestParameters(context, this.controller.signal);
96
+ const isArrayBuffer = context.responseType === 'arraybuffer';
97
+ const LENGTH = isArrayBuffer ? 'byteLength' : 'length';
98
+ const { maxTimeToFirstByteMs, maxLoadTimeMs } = config.loadPolicy;
99
+
100
+ this.context = context;
101
+ this.config = config;
102
+ this.callbacks = callbacks;
103
+ this.request = this.fetchSetup(context, initParams);
104
+ self.clearTimeout(this.requestTimeout);
105
+ config.timeout =
106
+ maxTimeToFirstByteMs && Number.isFinite(maxTimeToFirstByteMs)
107
+ ? maxTimeToFirstByteMs
108
+ : maxLoadTimeMs;
109
+ this.requestTimeout = self.setTimeout(() => {
110
+ if (this.callbacks) {
111
+ this.abortInternal();
112
+ this.callbacks.onTimeout(stats, context, this.response);
113
+ }
114
+ }, config.timeout);
115
+
116
+ const fetchPromise = isPromise(this.request)
117
+ ? this.request.then(self.fetch)
118
+ : self.fetch(this.request);
119
+
120
+ fetchPromise
121
+ .then((response: Response): Promise<string | ArrayBuffer> => {
122
+ this.response = this.loader = response;
123
+
124
+ const first = Math.max(self.performance.now(), stats.loading.start);
125
+
126
+ self.clearTimeout(this.requestTimeout);
127
+ config.timeout = maxLoadTimeMs;
128
+ this.requestTimeout = self.setTimeout(
129
+ () => {
130
+ if (this.callbacks) {
131
+ this.abortInternal();
132
+ this.callbacks.onTimeout(stats, context, this.response);
133
+ }
134
+ },
135
+ maxLoadTimeMs - (first - stats.loading.start),
136
+ );
137
+
138
+ if (!response.ok) {
139
+ const { status, statusText } = response;
140
+ throw new FetchError(
141
+ statusText || 'fetch, bad network response',
142
+ status,
143
+ response,
144
+ );
145
+ }
146
+ stats.loading.first = first;
147
+
148
+ stats.total = getContentLength(response.headers) || stats.total;
149
+
150
+ const onProgress = this.callbacks?.onProgress;
151
+ if (onProgress && Number.isFinite(config.highWaterMark)) {
152
+ return this.loadProgressively(
153
+ response,
154
+ stats,
155
+ context,
156
+ config.highWaterMark,
157
+ onProgress,
158
+ );
159
+ }
160
+
161
+ if (isArrayBuffer) {
162
+ return response.arrayBuffer();
163
+ }
164
+ if (context.responseType === 'json') {
165
+ return response.json();
166
+ }
167
+ return response.text();
168
+ })
169
+ .then((responseData: string | ArrayBuffer) => {
170
+ const response = this.response;
171
+ if (!response) {
172
+ throw new Error('loader destroyed');
173
+ }
174
+ self.clearTimeout(this.requestTimeout);
175
+ stats.loading.end = Math.max(
176
+ self.performance.now(),
177
+ stats.loading.first,
178
+ );
179
+ const total = responseData[LENGTH];
180
+ if (total) {
181
+ stats.loaded = stats.total = total;
182
+ }
183
+
184
+ const loaderResponse: LoaderResponse = {
185
+ url: response.url,
186
+ data: responseData,
187
+ code: response.status,
188
+ };
189
+
190
+ const onProgress = this.callbacks?.onProgress;
191
+ if (onProgress && !Number.isFinite(config.highWaterMark)) {
192
+ onProgress(stats, context, responseData, response);
193
+ }
194
+
195
+ this.callbacks?.onSuccess(loaderResponse, stats, context, response);
196
+ })
197
+ .catch((error) => {
198
+ self.clearTimeout(this.requestTimeout);
199
+ if (stats.aborted) {
200
+ return;
201
+ }
202
+ // CORS errors result in an undefined code. Set it to 0 here to align with XHR's behavior
203
+ // when destroying, 'error' itself can be undefined
204
+ const code: number = !error ? 0 : error.code || 0;
205
+ const text: string = !error ? null : error.message;
206
+ this.callbacks?.onError(
207
+ { code, text },
208
+ context,
209
+ error ? error.details : null,
210
+ stats,
211
+ );
212
+ });
213
+ }
214
+
215
+ getCacheAge(): number | null {
216
+ let result: number | null = null;
217
+ if (this.response) {
218
+ const ageHeader = this.response.headers.get('age');
219
+ result = ageHeader ? parseFloat(ageHeader) : null;
220
+ }
221
+ return result;
222
+ }
223
+
224
+ getResponseHeader(name: string): string | null {
225
+ return this.response ? this.response.headers.get(name) : null;
226
+ }
227
+
228
+ private loadProgressively(
229
+ response: Response,
230
+ stats: LoaderStats,
231
+ context: LoaderContext,
232
+ highWaterMark: number = 0,
233
+ onProgress: LoaderOnProgress<LoaderContext>,
234
+ ): Promise<ArrayBuffer> {
235
+ const chunkCache = new ChunkCache();
236
+ const reader = (response.body as ReadableStream).getReader();
237
+
238
+ const pump = (): Promise<ArrayBuffer> => {
239
+ return reader
240
+ .read()
241
+ .then((data) => {
242
+ if (data.done) {
243
+ if (chunkCache.dataLength) {
244
+ onProgress(stats, context, chunkCache.flush().buffer, response);
245
+ }
246
+
247
+ return Promise.resolve(new ArrayBuffer(0));
248
+ }
249
+ const chunk: Uint8Array<ArrayBuffer> = data.value;
250
+ const len = chunk.length;
251
+ stats.loaded += len;
252
+ if (len < highWaterMark || chunkCache.dataLength) {
253
+ // The current chunk is too small to to be emitted or the cache already has data
254
+ // Push it to the cache
255
+ chunkCache.push(chunk);
256
+ if (chunkCache.dataLength >= highWaterMark) {
257
+ // flush in order to join the typed arrays
258
+ onProgress(stats, context, chunkCache.flush().buffer, response);
259
+ }
260
+ } else {
261
+ // If there's nothing cached already, and the chache is large enough
262
+ // just emit the progress event
263
+ onProgress(stats, context, chunk.buffer, response);
264
+ }
265
+ return pump();
266
+ })
267
+ .catch(() => {
268
+ /* aborted */
269
+ return Promise.reject();
270
+ });
271
+ };
272
+
273
+ return pump();
274
+ }
275
+ }
276
+
277
+ function getRequestParameters(context: LoaderContext, signal): any {
278
+ const initParams: any = {
279
+ method: 'GET',
280
+ mode: 'cors',
281
+ credentials: 'same-origin',
282
+ signal,
283
+ headers: new self.Headers(Object.assign({}, context.headers)),
284
+ };
285
+
286
+ if (context.rangeEnd) {
287
+ initParams.headers.set(
288
+ 'Range',
289
+ 'bytes=' + context.rangeStart + '-' + String(context.rangeEnd - 1),
290
+ );
291
+ }
292
+
293
+ return initParams;
294
+ }
295
+
296
+ function getByteRangeLength(byteRangeHeader: string): number | undefined {
297
+ const result = BYTERANGE.exec(byteRangeHeader);
298
+ if (result) {
299
+ return parseInt(result[2]) - parseInt(result[1]) + 1;
300
+ }
301
+ }
302
+
303
+ function getContentLength(headers: Headers): number | undefined {
304
+ const contentRange = headers.get('Content-Range');
305
+ if (contentRange) {
306
+ const byteRangeLength = getByteRangeLength(contentRange);
307
+ if (Number.isFinite(byteRangeLength)) {
308
+ return byteRangeLength;
309
+ }
310
+ }
311
+ const contentLength = headers.get('Content-Length');
312
+ if (contentLength) {
313
+ return parseInt(contentLength);
314
+ }
315
+ }
316
+
317
+ function getRequest(context: LoaderContext, initParams: any): Request {
318
+ return new self.Request(context.url, initParams);
319
+ }
320
+
321
+ class FetchError extends Error {
322
+ public code: number;
323
+ public details: any;
324
+ constructor(message: string, code: number, details: any) {
325
+ super(message);
326
+ this.code = code;
327
+ this.details = details;
328
+ }
329
+ }
330
+
331
+ export default FetchLoader;
@@ -0,0 +1,2 @@
1
+ /** returns `undefined` is `self` is missing, e.g. in node */
2
+ export const optionalSelf = typeof self !== 'undefined' ? self : undefined;
@@ -0,0 +1,10 @@
1
+ // From https://github.com/darkskyapp/string-hash
2
+ export function hash(text: string) {
3
+ let hash = 5381;
4
+ let i = text.length;
5
+ while (i) {
6
+ hash = (hash * 33) ^ text.charCodeAt(--i);
7
+ }
8
+
9
+ return (hash >>> 0).toString();
10
+ }
@@ -0,0 +1,67 @@
1
+ import { type VideoRange, VideoRangeValues } from '../types/level';
2
+ import type { VideoSelectionOption } from '../types/media-playlist';
3
+
4
+ /**
5
+ * @returns Whether we can detect and validate HDR capability within the window context
6
+ */
7
+ export function isHdrSupported() {
8
+ if (typeof matchMedia === 'function') {
9
+ const mediaQueryList = matchMedia('(dynamic-range: high)');
10
+ const badQuery = matchMedia('bad query');
11
+ if (mediaQueryList.media !== badQuery.media) {
12
+ return mediaQueryList.matches === true;
13
+ }
14
+ }
15
+ return false;
16
+ }
17
+
18
+ /**
19
+ * Sanitizes inputs to return the active video selection options for HDR/SDR.
20
+ * When both inputs are null:
21
+ *
22
+ * `{ preferHDR: false, allowedVideoRanges: [] }`
23
+ *
24
+ * When `currentVideoRange` non-null, maintain the active range:
25
+ *
26
+ * `{ preferHDR: currentVideoRange !== 'SDR', allowedVideoRanges: [currentVideoRange] }`
27
+ *
28
+ * When VideoSelectionOption non-null:
29
+ *
30
+ * - Allow all video ranges if `allowedVideoRanges` unspecified.
31
+ * - If `preferHDR` is non-null use the value to filter `allowedVideoRanges`.
32
+ * - Else check window for HDR support and set `preferHDR` to the result.
33
+ *
34
+ * @param currentVideoRange
35
+ * @param videoPreference
36
+ */
37
+ export function getVideoSelectionOptions(
38
+ currentVideoRange: VideoRange | undefined,
39
+ videoPreference: VideoSelectionOption | undefined,
40
+ ) {
41
+ let preferHDR = false;
42
+ let allowedVideoRanges: Array<VideoRange> = [];
43
+
44
+ if (currentVideoRange) {
45
+ preferHDR = currentVideoRange !== 'SDR';
46
+ allowedVideoRanges = [currentVideoRange];
47
+ }
48
+
49
+ if (videoPreference) {
50
+ allowedVideoRanges =
51
+ videoPreference.allowedVideoRanges || VideoRangeValues.slice(0);
52
+ const allowAutoPreferHDR =
53
+ allowedVideoRanges.join('') !== 'SDR' && !videoPreference.videoCodec;
54
+ preferHDR =
55
+ videoPreference.preferHDR !== undefined
56
+ ? videoPreference.preferHDR
57
+ : allowAutoPreferHDR && isHdrSupported();
58
+ if (!preferHDR) {
59
+ allowedVideoRanges = ['SDR'];
60
+ }
61
+ }
62
+
63
+ return {
64
+ preferHDR,
65
+ allowedVideoRanges,
66
+ };
67
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * hex dump helper class
3
+ */
4
+
5
+ export function arrayToHex(array: Uint8Array<ArrayBuffer> | number[]) {
6
+ let str = '';
7
+ for (let i = 0; i < array.length; i++) {
8
+ let h = array[i].toString(16);
9
+ if (h.length < 2) {
10
+ h = '0' + h;
11
+ }
12
+
13
+ str += h;
14
+ }
15
+ return str;
16
+ }
17
+
18
+ export function hexToArrayBuffer(str: string): ArrayBuffer {
19
+ return Uint8Array.from(
20
+ str
21
+ .replace(/^0x/, '')
22
+ .replace(/([\da-fA-F]{2}) ?/g, '0x$1 ')
23
+ .replace(/ +$/, '')
24
+ .split(' '),
25
+ ).buffer;
26
+ }
27
+
28
+ const Hex = {
29
+ hexDump: arrayToHex,
30
+ };
31
+
32
+ export default Hex;