@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,567 @@
|
|
|
1
|
+
import { Events } from '../events';
|
|
2
|
+
import type Hls from '../hls';
|
|
3
|
+
import type { Fragment, MediaFragment, Part } from '../loader/fragment';
|
|
4
|
+
import type { SourceBufferName } from '../types/buffer';
|
|
5
|
+
import type { ComponentAPI } from '../types/component-api';
|
|
6
|
+
import type {
|
|
7
|
+
BufferAppendedData,
|
|
8
|
+
FragBufferedData,
|
|
9
|
+
FragLoadedData,
|
|
10
|
+
} from '../types/events';
|
|
11
|
+
import type {
|
|
12
|
+
FragmentBufferedRange,
|
|
13
|
+
FragmentEntity,
|
|
14
|
+
FragmentTimeRange,
|
|
15
|
+
} from '../types/fragment-tracker';
|
|
16
|
+
import type { PlaylistLevelType } from '../types/loader';
|
|
17
|
+
|
|
18
|
+
export const enum FragmentState {
|
|
19
|
+
NOT_LOADED = 'NOT_LOADED',
|
|
20
|
+
APPENDING = 'APPENDING',
|
|
21
|
+
PARTIAL = 'PARTIAL',
|
|
22
|
+
OK = 'OK',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class FragmentTracker implements ComponentAPI {
|
|
26
|
+
private activePartLists: { [key in PlaylistLevelType]?: Part[] } =
|
|
27
|
+
Object.create(null);
|
|
28
|
+
private endListFragments: { [key in PlaylistLevelType]?: FragmentEntity } =
|
|
29
|
+
Object.create(null);
|
|
30
|
+
private fragments: Partial<Record<string, FragmentEntity>> =
|
|
31
|
+
Object.create(null);
|
|
32
|
+
private timeRanges:
|
|
33
|
+
| {
|
|
34
|
+
[key in SourceBufferName]?: TimeRanges;
|
|
35
|
+
}
|
|
36
|
+
| null = Object.create(null);
|
|
37
|
+
|
|
38
|
+
private bufferPadding: number = 0.2;
|
|
39
|
+
private hls: Hls | null;
|
|
40
|
+
private hasGaps: boolean = false;
|
|
41
|
+
|
|
42
|
+
constructor(hls: Hls) {
|
|
43
|
+
this.hls = hls;
|
|
44
|
+
|
|
45
|
+
this._registerListeners();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private _registerListeners() {
|
|
49
|
+
const { hls } = this;
|
|
50
|
+
if (hls) {
|
|
51
|
+
hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
|
52
|
+
hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this);
|
|
53
|
+
hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
|
|
54
|
+
hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private _unregisterListeners() {
|
|
59
|
+
const { hls } = this;
|
|
60
|
+
if (hls) {
|
|
61
|
+
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
|
62
|
+
hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this);
|
|
63
|
+
hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
|
|
64
|
+
hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public destroy() {
|
|
69
|
+
this._unregisterListeners();
|
|
70
|
+
// @ts-ignore
|
|
71
|
+
this.hls =
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
this.fragments =
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
this.activePartLists =
|
|
76
|
+
// @ts-ignore
|
|
77
|
+
this.endListFragments =
|
|
78
|
+
this.timeRanges =
|
|
79
|
+
null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Return a Fragment or Part with an appended range that matches the position and levelType
|
|
84
|
+
* Otherwise, return null
|
|
85
|
+
*/
|
|
86
|
+
public getAppendedFrag(
|
|
87
|
+
position: number,
|
|
88
|
+
levelType: PlaylistLevelType,
|
|
89
|
+
): MediaFragment | Part | null {
|
|
90
|
+
const activeParts = this.activePartLists[levelType];
|
|
91
|
+
if (activeParts) {
|
|
92
|
+
for (let i = activeParts.length; i--; ) {
|
|
93
|
+
const activePart = activeParts[i];
|
|
94
|
+
if (!activePart as any) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
activePart.start <= position &&
|
|
99
|
+
position <= activePart.end &&
|
|
100
|
+
activePart.loaded
|
|
101
|
+
) {
|
|
102
|
+
return activePart;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return this.getBufferedFrag(position, levelType);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Return a buffered Fragment that matches the position and levelType.
|
|
111
|
+
* A buffered Fragment is one whose loading, parsing and appending is done (completed or "partial" meaning aborted).
|
|
112
|
+
* If not found any Fragment, return null
|
|
113
|
+
*/
|
|
114
|
+
public getBufferedFrag(
|
|
115
|
+
position: number,
|
|
116
|
+
levelType: PlaylistLevelType,
|
|
117
|
+
): MediaFragment | null {
|
|
118
|
+
return this.getFragAtPos(position, levelType, true);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public getFragAtPos(
|
|
122
|
+
position: number,
|
|
123
|
+
levelType: PlaylistLevelType,
|
|
124
|
+
buffered?: boolean,
|
|
125
|
+
): MediaFragment | null {
|
|
126
|
+
const { fragments } = this;
|
|
127
|
+
const keys = Object.keys(fragments);
|
|
128
|
+
for (let i = keys.length; i--; ) {
|
|
129
|
+
const fragmentEntity = fragments[keys[i]];
|
|
130
|
+
if (
|
|
131
|
+
fragmentEntity?.body.type === levelType &&
|
|
132
|
+
(!buffered || fragmentEntity.buffered)
|
|
133
|
+
) {
|
|
134
|
+
const frag = fragmentEntity.body;
|
|
135
|
+
if (frag.start <= position && position <= frag.end) {
|
|
136
|
+
return frag;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Partial fragments effected by coded frame eviction will be removed
|
|
145
|
+
* The browser will unload parts of the buffer to free up memory for new buffer data
|
|
146
|
+
* Fragments will need to be reloaded when the buffer is freed up, removing partial fragments will allow them to reload(since there might be parts that are still playable)
|
|
147
|
+
*/
|
|
148
|
+
public detectEvictedFragments(
|
|
149
|
+
elementaryStream: SourceBufferName,
|
|
150
|
+
timeRange: TimeRanges,
|
|
151
|
+
playlistType: PlaylistLevelType,
|
|
152
|
+
appendedPart?: Part | null,
|
|
153
|
+
removeAppending?: boolean,
|
|
154
|
+
) {
|
|
155
|
+
if (this.timeRanges) {
|
|
156
|
+
this.timeRanges[elementaryStream] = timeRange;
|
|
157
|
+
}
|
|
158
|
+
// Check if any flagged fragments have been unloaded
|
|
159
|
+
// excluding anything newer than appendedPartSn
|
|
160
|
+
const appendedPartSn = appendedPart?.fragment.sn || -1;
|
|
161
|
+
Object.keys(this.fragments).forEach((key) => {
|
|
162
|
+
const fragmentEntity = this.fragments[key];
|
|
163
|
+
if (!fragmentEntity) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (appendedPartSn >= fragmentEntity.body.sn) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
!fragmentEntity.buffered &&
|
|
171
|
+
(!fragmentEntity.loaded || removeAppending)
|
|
172
|
+
) {
|
|
173
|
+
if (fragmentEntity.body.type === playlistType) {
|
|
174
|
+
this.removeFragment(fragmentEntity.body);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const esData = fragmentEntity.range[elementaryStream];
|
|
179
|
+
if (!esData) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (esData.time.length === 0) {
|
|
183
|
+
this.removeFragment(fragmentEntity.body);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
esData.time.some((time: FragmentTimeRange) => {
|
|
187
|
+
const isNotBuffered = !this.isTimeBuffered(
|
|
188
|
+
time.startPTS,
|
|
189
|
+
time.endPTS,
|
|
190
|
+
timeRange,
|
|
191
|
+
);
|
|
192
|
+
if (isNotBuffered) {
|
|
193
|
+
// Unregister partial fragment as it needs to load again to be reused
|
|
194
|
+
this.removeFragment(fragmentEntity.body);
|
|
195
|
+
}
|
|
196
|
+
return isNotBuffered;
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Checks if the fragment passed in is loaded in the buffer properly
|
|
203
|
+
* Partially loaded fragments will be registered as a partial fragment
|
|
204
|
+
*/
|
|
205
|
+
public detectPartialFragments(data: FragBufferedData) {
|
|
206
|
+
const timeRanges = this.timeRanges;
|
|
207
|
+
if (!timeRanges || data.frag.sn === 'initSegment') {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const frag = data.frag as MediaFragment;
|
|
212
|
+
const fragKey = getFragmentKey(frag);
|
|
213
|
+
const fragmentEntity = this.fragments[fragKey];
|
|
214
|
+
if (!fragmentEntity || (fragmentEntity.buffered && frag.gap)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const isFragHint = !frag.relurl;
|
|
218
|
+
Object.keys(timeRanges).forEach((elementaryStream: SourceBufferName) => {
|
|
219
|
+
const streamInfo = frag.elementaryStreams[elementaryStream];
|
|
220
|
+
if (!streamInfo) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const timeRange = timeRanges[elementaryStream] as TimeRanges;
|
|
224
|
+
const partial = isFragHint || streamInfo.partial === true;
|
|
225
|
+
fragmentEntity.range[elementaryStream] = this.getBufferedTimes(
|
|
226
|
+
frag,
|
|
227
|
+
data.part,
|
|
228
|
+
partial,
|
|
229
|
+
timeRange,
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
fragmentEntity.loaded = null;
|
|
233
|
+
if (Object.keys(fragmentEntity.range).length) {
|
|
234
|
+
this.bufferedEnd(fragmentEntity, frag);
|
|
235
|
+
if (!isPartial(fragmentEntity)) {
|
|
236
|
+
// Remove older fragment parts from lookup after frag is tracked as buffered
|
|
237
|
+
this.removeParts(frag.sn - 1, frag.type);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
// remove fragment if nothing was appended
|
|
241
|
+
this.removeFragment(fragmentEntity.body);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private bufferedEnd(fragmentEntity: FragmentEntity, frag: MediaFragment) {
|
|
246
|
+
fragmentEntity.buffered = true;
|
|
247
|
+
const endList = (fragmentEntity.body.endList =
|
|
248
|
+
frag.endList || fragmentEntity.body.endList);
|
|
249
|
+
if (endList) {
|
|
250
|
+
this.endListFragments[fragmentEntity.body.type] = fragmentEntity;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private removeParts(snToKeep: number, levelType: PlaylistLevelType) {
|
|
255
|
+
const activeParts = this.activePartLists[levelType];
|
|
256
|
+
if (!activeParts) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
this.activePartLists[levelType] = filterParts(
|
|
260
|
+
activeParts,
|
|
261
|
+
(part) => part.fragment.sn >= snToKeep,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public fragBuffered(frag: MediaFragment, force?: true) {
|
|
266
|
+
const fragKey = getFragmentKey(frag);
|
|
267
|
+
let fragmentEntity = this.fragments[fragKey];
|
|
268
|
+
if (!fragmentEntity && force) {
|
|
269
|
+
fragmentEntity = this.fragments[fragKey] = {
|
|
270
|
+
body: frag,
|
|
271
|
+
appendedPTS: null,
|
|
272
|
+
loaded: null,
|
|
273
|
+
buffered: false,
|
|
274
|
+
range: Object.create(null),
|
|
275
|
+
};
|
|
276
|
+
if (frag.gap) {
|
|
277
|
+
this.hasGaps = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (fragmentEntity) {
|
|
281
|
+
fragmentEntity.loaded = null;
|
|
282
|
+
this.bufferedEnd(fragmentEntity, frag);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private getBufferedTimes(
|
|
287
|
+
fragment: Fragment,
|
|
288
|
+
part: Part | null,
|
|
289
|
+
partial: boolean,
|
|
290
|
+
timeRange: TimeRanges,
|
|
291
|
+
): FragmentBufferedRange {
|
|
292
|
+
const buffered: FragmentBufferedRange = {
|
|
293
|
+
time: [],
|
|
294
|
+
partial,
|
|
295
|
+
};
|
|
296
|
+
const startPTS = fragment.start;
|
|
297
|
+
const endPTS = fragment.end;
|
|
298
|
+
const minEndPTS = fragment.minEndPTS || endPTS;
|
|
299
|
+
const maxStartPTS = fragment.maxStartPTS || startPTS;
|
|
300
|
+
for (let i = 0; i < timeRange.length; i++) {
|
|
301
|
+
const startTime = timeRange.start(i) - this.bufferPadding;
|
|
302
|
+
const endTime = timeRange.end(i) + this.bufferPadding;
|
|
303
|
+
if (maxStartPTS >= startTime && minEndPTS <= endTime) {
|
|
304
|
+
// Fragment is entirely contained in buffer
|
|
305
|
+
// No need to check the other timeRange times since it's completely playable
|
|
306
|
+
buffered.time.push({
|
|
307
|
+
startPTS: Math.max(startPTS, timeRange.start(i)),
|
|
308
|
+
endPTS: Math.min(endPTS, timeRange.end(i)),
|
|
309
|
+
});
|
|
310
|
+
break;
|
|
311
|
+
} else if (startPTS < endTime && endPTS > startTime) {
|
|
312
|
+
const start = Math.max(startPTS, timeRange.start(i));
|
|
313
|
+
const end = Math.min(endPTS, timeRange.end(i));
|
|
314
|
+
if (end > start) {
|
|
315
|
+
buffered.partial = true;
|
|
316
|
+
// Check for intersection with buffer
|
|
317
|
+
// Get playable sections of the fragment
|
|
318
|
+
buffered.time.push({
|
|
319
|
+
startPTS: start,
|
|
320
|
+
endPTS: end,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
} else if (endPTS <= startTime) {
|
|
324
|
+
// No need to check the rest of the timeRange as it is in order
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return buffered;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Gets the partial fragment for a certain time
|
|
333
|
+
*/
|
|
334
|
+
public getPartialFragment(time: number): MediaFragment | null {
|
|
335
|
+
let bestFragment: Fragment | null = null;
|
|
336
|
+
let timePadding: number;
|
|
337
|
+
let startTime: number;
|
|
338
|
+
let endTime: number;
|
|
339
|
+
let bestOverlap: number = 0;
|
|
340
|
+
const { bufferPadding, fragments } = this;
|
|
341
|
+
Object.keys(fragments).forEach((key) => {
|
|
342
|
+
const fragmentEntity = fragments[key];
|
|
343
|
+
if (!fragmentEntity) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (isPartial(fragmentEntity)) {
|
|
347
|
+
startTime = fragmentEntity.body.start - bufferPadding;
|
|
348
|
+
endTime = fragmentEntity.body.end + bufferPadding;
|
|
349
|
+
if (time >= startTime && time <= endTime) {
|
|
350
|
+
// Use the fragment that has the most padding from start and end time
|
|
351
|
+
timePadding = Math.min(time - startTime, endTime - time);
|
|
352
|
+
if (bestOverlap <= timePadding) {
|
|
353
|
+
bestFragment = fragmentEntity.body;
|
|
354
|
+
bestOverlap = timePadding;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
return bestFragment;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
public isEndListAppended(type: PlaylistLevelType): boolean {
|
|
363
|
+
const lastFragmentEntity = this.endListFragments[type];
|
|
364
|
+
return (
|
|
365
|
+
lastFragmentEntity !== undefined &&
|
|
366
|
+
(lastFragmentEntity.buffered || isPartial(lastFragmentEntity))
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
public getState(fragment: Fragment): FragmentState {
|
|
371
|
+
const fragKey = getFragmentKey(fragment);
|
|
372
|
+
const fragmentEntity = this.fragments[fragKey];
|
|
373
|
+
|
|
374
|
+
if (fragmentEntity) {
|
|
375
|
+
if (!fragmentEntity.buffered) {
|
|
376
|
+
return FragmentState.APPENDING;
|
|
377
|
+
} else if (isPartial(fragmentEntity)) {
|
|
378
|
+
return FragmentState.PARTIAL;
|
|
379
|
+
} else {
|
|
380
|
+
return FragmentState.OK;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return FragmentState.NOT_LOADED;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private isTimeBuffered(
|
|
388
|
+
startPTS: number,
|
|
389
|
+
endPTS: number,
|
|
390
|
+
timeRange: TimeRanges,
|
|
391
|
+
): boolean {
|
|
392
|
+
let startTime;
|
|
393
|
+
let endTime;
|
|
394
|
+
for (let i = 0; i < timeRange.length; i++) {
|
|
395
|
+
startTime = timeRange.start(i) - this.bufferPadding;
|
|
396
|
+
endTime = timeRange.end(i) + this.bufferPadding;
|
|
397
|
+
if (startPTS >= startTime && endPTS <= endTime) {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (endPTS <= startTime) {
|
|
402
|
+
// No need to check the rest of the timeRange as it is in order
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private onManifestLoading() {
|
|
411
|
+
this.removeAllFragments();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private onFragLoaded(event: Events.FRAG_LOADED, data: FragLoadedData) {
|
|
415
|
+
// don't track initsegment (for which sn is not a number)
|
|
416
|
+
// don't track frags used for bitrateTest, they're irrelevant.
|
|
417
|
+
if (data.frag.sn === 'initSegment' || data.frag.bitrateTest) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const frag = data.frag as MediaFragment;
|
|
422
|
+
// Fragment entity `loaded` FragLoadedData is null when loading parts
|
|
423
|
+
const loaded = data.part ? null : data;
|
|
424
|
+
|
|
425
|
+
const fragKey = getFragmentKey(frag);
|
|
426
|
+
this.fragments[fragKey] = {
|
|
427
|
+
body: frag,
|
|
428
|
+
appendedPTS: null,
|
|
429
|
+
loaded,
|
|
430
|
+
buffered: false,
|
|
431
|
+
range: Object.create(null),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private onBufferAppended(
|
|
436
|
+
event: Events.BUFFER_APPENDED,
|
|
437
|
+
data: BufferAppendedData,
|
|
438
|
+
) {
|
|
439
|
+
const { frag, part, timeRanges, type } = data;
|
|
440
|
+
if (frag.sn === 'initSegment') {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const playlistType = frag.type;
|
|
444
|
+
if (part) {
|
|
445
|
+
let activeParts = this.activePartLists[playlistType];
|
|
446
|
+
if (!activeParts) {
|
|
447
|
+
this.activePartLists[playlistType] = activeParts = [];
|
|
448
|
+
}
|
|
449
|
+
activeParts.push(part);
|
|
450
|
+
}
|
|
451
|
+
// Store the latest timeRanges loaded in the buffer
|
|
452
|
+
this.timeRanges = timeRanges;
|
|
453
|
+
const timeRange = timeRanges[type] as TimeRanges;
|
|
454
|
+
this.detectEvictedFragments(type, timeRange, playlistType, part);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
|
|
458
|
+
this.detectPartialFragments(data);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private hasFragment(fragment: Fragment): boolean {
|
|
462
|
+
const fragKey = getFragmentKey(fragment);
|
|
463
|
+
return !!this.fragments[fragKey];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
public hasFragments(type?: PlaylistLevelType): boolean {
|
|
467
|
+
const { fragments } = this;
|
|
468
|
+
const keys = Object.keys(fragments);
|
|
469
|
+
if (!type) {
|
|
470
|
+
return keys.length > 0;
|
|
471
|
+
}
|
|
472
|
+
for (let i = keys.length; i--; ) {
|
|
473
|
+
const fragmentEntity = fragments[keys[i]];
|
|
474
|
+
if (fragmentEntity?.body.type === type) {
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
public hasParts(type: PlaylistLevelType): boolean {
|
|
482
|
+
return !!this.activePartLists[type]?.length;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
public removeFragmentsInRange(
|
|
486
|
+
start: number,
|
|
487
|
+
end: number,
|
|
488
|
+
playlistType: PlaylistLevelType,
|
|
489
|
+
withGapOnly?: boolean,
|
|
490
|
+
unbufferedOnly?: boolean,
|
|
491
|
+
) {
|
|
492
|
+
if (withGapOnly && !this.hasGaps) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
Object.keys(this.fragments).forEach((key) => {
|
|
496
|
+
const fragmentEntity = this.fragments[key];
|
|
497
|
+
if (!fragmentEntity) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const frag = fragmentEntity.body;
|
|
501
|
+
if (frag.type !== playlistType || (withGapOnly && !frag.gap)) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (
|
|
505
|
+
frag.start < end &&
|
|
506
|
+
frag.end > start &&
|
|
507
|
+
(fragmentEntity.buffered || unbufferedOnly)
|
|
508
|
+
) {
|
|
509
|
+
this.removeFragment(frag);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
public removeFragment(fragment: Fragment) {
|
|
515
|
+
const fragKey = getFragmentKey(fragment);
|
|
516
|
+
fragment.clearElementaryStreamInfo();
|
|
517
|
+
const activeParts = this.activePartLists[fragment.type];
|
|
518
|
+
if (activeParts) {
|
|
519
|
+
const snToRemove = fragment.sn;
|
|
520
|
+
this.activePartLists[fragment.type] = filterParts(
|
|
521
|
+
activeParts,
|
|
522
|
+
(part) => part.fragment.sn !== snToRemove,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
delete this.fragments[fragKey];
|
|
526
|
+
if (fragment.endList) {
|
|
527
|
+
delete this.endListFragments[fragment.type];
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
public removeAllFragments() {
|
|
532
|
+
this.fragments = Object.create(null);
|
|
533
|
+
this.endListFragments = Object.create(null);
|
|
534
|
+
this.activePartLists = Object.create(null);
|
|
535
|
+
this.hasGaps = false;
|
|
536
|
+
const partlist = this.hls?.latestLevelDetails?.partList;
|
|
537
|
+
if (partlist) {
|
|
538
|
+
partlist.forEach((part) => part.clearElementaryStreamInfo());
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function isPartial(fragmentEntity: FragmentEntity): boolean {
|
|
544
|
+
return (
|
|
545
|
+
fragmentEntity.buffered &&
|
|
546
|
+
!!(
|
|
547
|
+
fragmentEntity.body.gap ||
|
|
548
|
+
fragmentEntity.range.video?.partial ||
|
|
549
|
+
fragmentEntity.range.audio?.partial ||
|
|
550
|
+
fragmentEntity.range.audiovideo?.partial
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function getFragmentKey(fragment: Fragment): string {
|
|
556
|
+
return `${fragment.type}_${fragment.level}_${fragment.sn}`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function filterParts(partList: Part[], predicate: (part: Part) => boolean) {
|
|
560
|
+
return partList.filter((part) => {
|
|
561
|
+
const keep = predicate(part);
|
|
562
|
+
if (!keep) {
|
|
563
|
+
part.clearElementaryStreamInfo();
|
|
564
|
+
}
|
|
565
|
+
return keep;
|
|
566
|
+
});
|
|
567
|
+
}
|