@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,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;
|
package/src/utils/hdr.ts
ADDED
|
@@ -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
|
+
}
|
package/src/utils/hex.ts
ADDED
|
@@ -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;
|