@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,46 @@
|
|
|
1
|
+
import type { SourceBufferName } from './buffer';
|
|
2
|
+
import type { HlsChunkPerformanceTiming } from './loader';
|
|
3
|
+
import type { RemuxerResult } from './remuxer';
|
|
4
|
+
|
|
5
|
+
export interface TransmuxerResult {
|
|
6
|
+
remuxResult: RemuxerResult;
|
|
7
|
+
chunkMeta: ChunkMetadata;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ChunkMetadata {
|
|
11
|
+
public readonly level: number;
|
|
12
|
+
public readonly sn: number;
|
|
13
|
+
public readonly part: number;
|
|
14
|
+
public readonly id: number;
|
|
15
|
+
public readonly size: number;
|
|
16
|
+
public readonly partial: boolean;
|
|
17
|
+
public readonly transmuxing: HlsChunkPerformanceTiming =
|
|
18
|
+
getNewPerformanceTiming();
|
|
19
|
+
public readonly buffering: {
|
|
20
|
+
[key in SourceBufferName]: HlsChunkPerformanceTiming;
|
|
21
|
+
} = {
|
|
22
|
+
audio: getNewPerformanceTiming(),
|
|
23
|
+
video: getNewPerformanceTiming(),
|
|
24
|
+
audiovideo: getNewPerformanceTiming(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
level: number,
|
|
29
|
+
sn: number,
|
|
30
|
+
id: number,
|
|
31
|
+
size = 0,
|
|
32
|
+
part = -1,
|
|
33
|
+
partial = false,
|
|
34
|
+
) {
|
|
35
|
+
this.level = level;
|
|
36
|
+
this.sn = sn;
|
|
37
|
+
this.id = id;
|
|
38
|
+
this.size = size;
|
|
39
|
+
this.part = part;
|
|
40
|
+
this.partial = partial;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getNewPerformanceTiming(): HlsChunkPerformanceTiming {
|
|
45
|
+
return { start: 0, executeStart: 0, executeEnd: 0, end: 0 };
|
|
46
|
+
}
|
package/src/types/vtt.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function arrayValuesMatch(
|
|
2
|
+
a: (string | number)[] | Uint8Array,
|
|
3
|
+
b: (string | number)[] | Uint8Array,
|
|
4
|
+
): boolean {
|
|
5
|
+
if (a.length === b.length) {
|
|
6
|
+
return !a.some((value: string | number, i: number) => value !== b[i]);
|
|
7
|
+
}
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function optionalArrayValuesMatch(
|
|
12
|
+
a: (string | number)[] | Uint8Array | null | undefined,
|
|
13
|
+
b: (string | number)[] | Uint8Array | null | undefined,
|
|
14
|
+
): boolean {
|
|
15
|
+
if (!a && !b) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (!a || !b) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return arrayValuesMatch(a, b);
|
|
22
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { logger } from './logger';
|
|
2
|
+
import { substituteVariables } from './variable-substitution';
|
|
3
|
+
import type { LevelDetails } from '../loader/level-details';
|
|
4
|
+
import type { ParsedMultivariantPlaylist } from '../loader/m3u8-parser';
|
|
5
|
+
|
|
6
|
+
const DECIMAL_RESOLUTION_REGEX = /^(\d+)x(\d+)$/;
|
|
7
|
+
const ATTR_LIST_REGEX = /(.+?)=(".*?"|.*?)(?:,|$)/g;
|
|
8
|
+
|
|
9
|
+
// adapted from https://github.com/kanongil/node-m3u8parse/blob/master/attrlist.js
|
|
10
|
+
export class AttrList {
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
attrs: string | Record<string, any>,
|
|
15
|
+
parsed?: Pick<
|
|
16
|
+
ParsedMultivariantPlaylist | LevelDetails,
|
|
17
|
+
'variableList' | 'hasVariableRefs' | 'playlistParsingError'
|
|
18
|
+
>,
|
|
19
|
+
) {
|
|
20
|
+
if (typeof attrs === 'string') {
|
|
21
|
+
attrs = AttrList.parseAttrList(attrs, parsed);
|
|
22
|
+
}
|
|
23
|
+
Object.assign(this, attrs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get clientAttrs(): string[] {
|
|
27
|
+
return Object.keys(this).filter((attr) => attr.substring(0, 2) === 'X-');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
decimalInteger(attrName: string): number {
|
|
31
|
+
const intValue = parseInt(this[attrName], 10);
|
|
32
|
+
if (intValue > Number.MAX_SAFE_INTEGER) {
|
|
33
|
+
return Infinity;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return intValue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
hexadecimalInteger(attrName: string) {
|
|
40
|
+
if (this[attrName]) {
|
|
41
|
+
let stringValue = (this[attrName] || '0x').slice(2);
|
|
42
|
+
stringValue = (stringValue.length & 1 ? '0' : '') + stringValue;
|
|
43
|
+
|
|
44
|
+
const value = new Uint8Array(stringValue.length / 2);
|
|
45
|
+
for (let i = 0; i < stringValue.length / 2; i++) {
|
|
46
|
+
value[i] = parseInt(stringValue.slice(i * 2, i * 2 + 2), 16);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
hexadecimalIntegerAsNumber(attrName: string): number {
|
|
55
|
+
const intValue = parseInt(this[attrName], 16);
|
|
56
|
+
if (intValue > Number.MAX_SAFE_INTEGER) {
|
|
57
|
+
return Infinity;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return intValue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
decimalFloatingPoint(attrName: string): number {
|
|
64
|
+
return parseFloat(this[attrName]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
optionalFloat(attrName: string, defaultValue: number): number {
|
|
68
|
+
const value = this[attrName];
|
|
69
|
+
return value ? parseFloat(value) : defaultValue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
enumeratedString(attrName: string): string | undefined {
|
|
73
|
+
return this[attrName];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
enumeratedStringList<T extends { [key: string]: boolean }>(
|
|
77
|
+
attrName: string,
|
|
78
|
+
dict: T,
|
|
79
|
+
): { [key in keyof T]: boolean } {
|
|
80
|
+
const attrValue = this[attrName];
|
|
81
|
+
return (attrValue ? attrValue.split(/[ ,]+/) : []).reduce(
|
|
82
|
+
(result: { [key in keyof T]: boolean }, identifier: string) => {
|
|
83
|
+
result[identifier.toLowerCase() as keyof T] = true;
|
|
84
|
+
return result;
|
|
85
|
+
},
|
|
86
|
+
dict,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
bool(attrName: string): boolean {
|
|
91
|
+
return this[attrName] === 'YES';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
decimalResolution(attrName: string):
|
|
95
|
+
| {
|
|
96
|
+
width: number;
|
|
97
|
+
height: number;
|
|
98
|
+
}
|
|
99
|
+
| undefined {
|
|
100
|
+
const res = DECIMAL_RESOLUTION_REGEX.exec(this[attrName]);
|
|
101
|
+
if (res === null) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
width: parseInt(res[1], 10),
|
|
107
|
+
height: parseInt(res[2], 10),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
static parseAttrList(
|
|
112
|
+
input: string,
|
|
113
|
+
parsed?: Pick<
|
|
114
|
+
ParsedMultivariantPlaylist | LevelDetails,
|
|
115
|
+
'variableList' | 'hasVariableRefs' | 'playlistParsingError'
|
|
116
|
+
>,
|
|
117
|
+
): Record<string, string> {
|
|
118
|
+
let match: RegExpExecArray | null;
|
|
119
|
+
const attrs = {};
|
|
120
|
+
const quote = '"';
|
|
121
|
+
ATTR_LIST_REGEX.lastIndex = 0;
|
|
122
|
+
while ((match = ATTR_LIST_REGEX.exec(input)) !== null) {
|
|
123
|
+
const name = match[1].trim();
|
|
124
|
+
let value = match[2];
|
|
125
|
+
const quotedString =
|
|
126
|
+
value.indexOf(quote) === 0 &&
|
|
127
|
+
value.lastIndexOf(quote) === value.length - 1;
|
|
128
|
+
let hexadecimalSequence = false;
|
|
129
|
+
if (quotedString) {
|
|
130
|
+
value = value.slice(1, -1);
|
|
131
|
+
} else {
|
|
132
|
+
switch (name) {
|
|
133
|
+
case 'IV':
|
|
134
|
+
case 'SCTE35-CMD':
|
|
135
|
+
case 'SCTE35-IN':
|
|
136
|
+
case 'SCTE35-OUT':
|
|
137
|
+
hexadecimalSequence = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (parsed && (quotedString || hexadecimalSequence)) {
|
|
141
|
+
if (__USE_VARIABLE_SUBSTITUTION__) {
|
|
142
|
+
value = substituteVariables(parsed, value);
|
|
143
|
+
}
|
|
144
|
+
} else if (!hexadecimalSequence && !quotedString) {
|
|
145
|
+
switch (name) {
|
|
146
|
+
case 'CLOSED-CAPTIONS':
|
|
147
|
+
if (value === 'NONE') {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
// falls through
|
|
151
|
+
case 'ALLOWED-CPC':
|
|
152
|
+
case 'CLASS':
|
|
153
|
+
case 'ASSOC-LANGUAGE':
|
|
154
|
+
case 'AUDIO':
|
|
155
|
+
case 'BYTERANGE':
|
|
156
|
+
case 'CHANNELS':
|
|
157
|
+
case 'CHARACTERISTICS':
|
|
158
|
+
case 'CODECS':
|
|
159
|
+
case 'DATA-ID':
|
|
160
|
+
case 'END-DATE':
|
|
161
|
+
case 'GROUP-ID':
|
|
162
|
+
case 'ID':
|
|
163
|
+
case 'IMPORT':
|
|
164
|
+
case 'INSTREAM-ID':
|
|
165
|
+
case 'KEYFORMAT':
|
|
166
|
+
case 'KEYFORMATVERSIONS':
|
|
167
|
+
case 'LANGUAGE':
|
|
168
|
+
case 'NAME':
|
|
169
|
+
case 'PATHWAY-ID':
|
|
170
|
+
case 'QUERYPARAM':
|
|
171
|
+
case 'RECENTLY-REMOVED-DATERANGES':
|
|
172
|
+
case 'SERVER-URI':
|
|
173
|
+
case 'STABLE-RENDITION-ID':
|
|
174
|
+
case 'STABLE-VARIANT-ID':
|
|
175
|
+
case 'START-DATE':
|
|
176
|
+
case 'SUBTITLES':
|
|
177
|
+
case 'SUPPLEMENTAL-CODECS':
|
|
178
|
+
case 'URI':
|
|
179
|
+
case 'VALUE':
|
|
180
|
+
case 'VIDEO':
|
|
181
|
+
case 'X-ASSET-LIST':
|
|
182
|
+
case 'X-ASSET-URI':
|
|
183
|
+
// Since we are not checking tag:attribute combination, just warn rather than ignoring attribute
|
|
184
|
+
logger.warn(`${input}: attribute ${name} is missing quotes`);
|
|
185
|
+
// continue;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
attrs[name] = value;
|
|
189
|
+
}
|
|
190
|
+
return attrs;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
type BinarySearchComparison<T> = (candidate: T) => -1 | 0 | 1;
|
|
2
|
+
|
|
3
|
+
const BinarySearch = {
|
|
4
|
+
/**
|
|
5
|
+
* Searches for an item in an array which matches a certain condition.
|
|
6
|
+
* This requires the condition to only match one item in the array,
|
|
7
|
+
* and for the array to be ordered.
|
|
8
|
+
*
|
|
9
|
+
* @param list The array to search.
|
|
10
|
+
* @param comparisonFn
|
|
11
|
+
* Called and provided a candidate item as the first argument.
|
|
12
|
+
* Should return:
|
|
13
|
+
* > -1 if the item should be located at a lower index than the provided item.
|
|
14
|
+
* > 1 if the item should be located at a higher index than the provided item.
|
|
15
|
+
* > 0 if the item is the item you're looking for.
|
|
16
|
+
*
|
|
17
|
+
* @returns the object if found, otherwise returns null
|
|
18
|
+
*/
|
|
19
|
+
search: function <T>(
|
|
20
|
+
list: T[],
|
|
21
|
+
comparisonFn: BinarySearchComparison<T>,
|
|
22
|
+
): T | null {
|
|
23
|
+
let minIndex: number = 0;
|
|
24
|
+
let maxIndex: number = list.length - 1;
|
|
25
|
+
let currentIndex: number | null = null;
|
|
26
|
+
let currentElement: T | null = null;
|
|
27
|
+
|
|
28
|
+
while (minIndex <= maxIndex) {
|
|
29
|
+
currentIndex = ((minIndex + maxIndex) / 2) | 0;
|
|
30
|
+
currentElement = list[currentIndex];
|
|
31
|
+
|
|
32
|
+
const comparisonResult = comparisonFn(currentElement);
|
|
33
|
+
if (comparisonResult > 0) {
|
|
34
|
+
minIndex = currentIndex + 1;
|
|
35
|
+
} else if (comparisonResult < 0) {
|
|
36
|
+
maxIndex = currentIndex - 1;
|
|
37
|
+
} else {
|
|
38
|
+
return currentElement;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default BinarySearch;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides methods dealing with buffer length retrieval for example.
|
|
3
|
+
*
|
|
4
|
+
* In general, a helper around HTML5 MediaElement TimeRanges gathered from `buffered` property.
|
|
5
|
+
*
|
|
6
|
+
* Also @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/buffered
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from './logger';
|
|
10
|
+
|
|
11
|
+
export type BufferTimeRange = {
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type Bufferable = {
|
|
17
|
+
buffered: TimeRanges;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type BufferInfo = {
|
|
21
|
+
len: number;
|
|
22
|
+
start: number;
|
|
23
|
+
end: number;
|
|
24
|
+
nextStart?: number;
|
|
25
|
+
buffered?: BufferTimeRange[];
|
|
26
|
+
bufferedIndex: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const noopBuffered: TimeRanges = {
|
|
30
|
+
length: 0,
|
|
31
|
+
start: () => 0,
|
|
32
|
+
end: () => 0,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export class BufferHelper {
|
|
36
|
+
/**
|
|
37
|
+
* Return true if `media`'s buffered include `position`
|
|
38
|
+
*/
|
|
39
|
+
static isBuffered(media: Bufferable | null, position: number): boolean {
|
|
40
|
+
if (media) {
|
|
41
|
+
const buffered = BufferHelper.getBuffered(media);
|
|
42
|
+
for (let i = buffered.length; i--; ) {
|
|
43
|
+
if (position >= buffered.start(i) && position <= buffered.end(i)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static bufferedRanges(media: Bufferable | null): BufferTimeRange[] {
|
|
52
|
+
if (media) {
|
|
53
|
+
const timeRanges = BufferHelper.getBuffered(media);
|
|
54
|
+
return BufferHelper.timeRangesToArray(timeRanges);
|
|
55
|
+
}
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static timeRangesToArray(timeRanges: TimeRanges): BufferTimeRange[] {
|
|
60
|
+
const buffered: BufferTimeRange[] = [];
|
|
61
|
+
for (let i = 0; i < timeRanges.length; i++) {
|
|
62
|
+
buffered.push({ start: timeRanges.start(i), end: timeRanges.end(i) });
|
|
63
|
+
}
|
|
64
|
+
return buffered;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static bufferInfo(
|
|
68
|
+
media: Bufferable | null,
|
|
69
|
+
pos: number,
|
|
70
|
+
maxHoleDuration: number,
|
|
71
|
+
): BufferInfo {
|
|
72
|
+
if (media) {
|
|
73
|
+
const buffered = BufferHelper.bufferedRanges(media);
|
|
74
|
+
if (buffered.length) {
|
|
75
|
+
return BufferHelper.bufferedInfo(buffered, pos, maxHoleDuration);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { len: 0, start: pos, end: pos, bufferedIndex: -1 };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static bufferedInfo(
|
|
82
|
+
buffered: BufferTimeRange[],
|
|
83
|
+
pos: number,
|
|
84
|
+
maxHoleDuration: number,
|
|
85
|
+
): BufferInfo {
|
|
86
|
+
pos = Math.max(0, pos);
|
|
87
|
+
// sort on buffer.start/smaller end (IE does not always return sorted buffered range)
|
|
88
|
+
if (buffered.length > 1) {
|
|
89
|
+
buffered.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let bufferedIndex: number = -1;
|
|
93
|
+
let buffered2: BufferTimeRange[] = [];
|
|
94
|
+
if (maxHoleDuration) {
|
|
95
|
+
// there might be some small holes between buffer time range
|
|
96
|
+
// consider that holes smaller than maxHoleDuration are irrelevant and build another
|
|
97
|
+
// buffer time range representations that discards those holes
|
|
98
|
+
for (let i = 0; i < buffered.length; i++) {
|
|
99
|
+
if (pos >= buffered[i].start && pos <= buffered[i].end) {
|
|
100
|
+
bufferedIndex = i;
|
|
101
|
+
}
|
|
102
|
+
const buf2len = buffered2.length;
|
|
103
|
+
if (buf2len) {
|
|
104
|
+
const buf2end = buffered2[buf2len - 1].end;
|
|
105
|
+
// if small hole (value between 0 or maxHoleDuration ) or overlapping (negative)
|
|
106
|
+
if (buffered[i].start - buf2end < maxHoleDuration) {
|
|
107
|
+
// merge overlapping time ranges
|
|
108
|
+
// update lastRange.end only if smaller than item.end
|
|
109
|
+
// e.g. [ 1, 15] with [ 2,8] => [ 1,15] (no need to modify lastRange.end)
|
|
110
|
+
// whereas [ 1, 8] with [ 2,15] => [ 1,15] ( lastRange should switch from [1,8] to [1,15])
|
|
111
|
+
if (buffered[i].end > buf2end) {
|
|
112
|
+
buffered2[buf2len - 1].end = buffered[i].end;
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
// big hole
|
|
116
|
+
buffered2.push(buffered[i]);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
// first value
|
|
120
|
+
buffered2.push(buffered[i]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
buffered2 = buffered;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let bufferLen = 0;
|
|
128
|
+
|
|
129
|
+
let nextStart: number | undefined;
|
|
130
|
+
|
|
131
|
+
// bufferStart and bufferEnd are buffer boundaries around current playback position (pos)
|
|
132
|
+
let bufferStart: number = pos;
|
|
133
|
+
let bufferEnd: number = pos;
|
|
134
|
+
for (let i = 0; i < buffered2.length; i++) {
|
|
135
|
+
const start = buffered2[i].start;
|
|
136
|
+
const end = buffered2[i].end;
|
|
137
|
+
// logger.log('buf start/end:' + buffered.start(i) + '/' + buffered.end(i));
|
|
138
|
+
if (bufferedIndex === -1 && pos >= start && pos <= end) {
|
|
139
|
+
bufferedIndex = i;
|
|
140
|
+
}
|
|
141
|
+
if (pos + maxHoleDuration >= start && pos < end) {
|
|
142
|
+
// play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length
|
|
143
|
+
bufferStart = start;
|
|
144
|
+
bufferEnd = end;
|
|
145
|
+
bufferLen = bufferEnd - pos;
|
|
146
|
+
} else if (pos + maxHoleDuration < start) {
|
|
147
|
+
nextStart = start;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
len: bufferLen,
|
|
153
|
+
start: bufferStart || 0,
|
|
154
|
+
end: bufferEnd || 0,
|
|
155
|
+
nextStart,
|
|
156
|
+
buffered,
|
|
157
|
+
bufferedIndex,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Safe method to get buffered property.
|
|
163
|
+
* SourceBuffer.buffered may throw if SourceBuffer is removed from it's MediaSource
|
|
164
|
+
*/
|
|
165
|
+
static getBuffered(media: Bufferable): TimeRanges {
|
|
166
|
+
try {
|
|
167
|
+
return media.buffered || noopBuffered;
|
|
168
|
+
} catch (e) {
|
|
169
|
+
logger.log('failed to get media.buffered', e);
|
|
170
|
+
return noopBuffered;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|