hls.js 1.5.13 → 1.5.14-0.canary.10417
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/README.md +4 -3
- package/dist/hls-demo.js +41 -38
- package/dist/hls-demo.js.map +1 -1
- package/dist/hls.js +4211 -2666
- package/dist/hls.js.d.ts +179 -110
- package/dist/hls.js.map +1 -1
- package/dist/hls.light.js +2841 -1921
- package/dist/hls.light.js.map +1 -1
- package/dist/hls.light.min.js +1 -1
- package/dist/hls.light.min.js.map +1 -1
- package/dist/hls.light.mjs +2569 -1639
- package/dist/hls.light.mjs.map +1 -1
- package/dist/hls.min.js +1 -1
- package/dist/hls.min.js.map +1 -1
- package/dist/hls.mjs +3572 -2017
- package/dist/hls.mjs.map +1 -1
- package/dist/hls.worker.js +1 -1
- package/dist/hls.worker.js.map +1 -1
- package/package.json +38 -38
- package/src/config.ts +5 -2
- package/src/controller/abr-controller.ts +39 -25
- package/src/controller/audio-stream-controller.ts +156 -136
- package/src/controller/audio-track-controller.ts +1 -1
- package/src/controller/base-playlist-controller.ts +27 -10
- package/src/controller/base-stream-controller.ts +234 -89
- package/src/controller/buffer-controller.ts +250 -97
- package/src/controller/buffer-operation-queue.ts +16 -19
- package/src/controller/cap-level-controller.ts +3 -2
- package/src/controller/cmcd-controller.ts +51 -14
- package/src/controller/content-steering-controller.ts +29 -15
- package/src/controller/eme-controller.ts +10 -23
- package/src/controller/error-controller.ts +28 -22
- package/src/controller/fps-controller.ts +8 -3
- package/src/controller/fragment-finders.ts +44 -16
- package/src/controller/fragment-tracker.ts +58 -25
- package/src/controller/gap-controller.ts +43 -16
- package/src/controller/id3-track-controller.ts +45 -35
- package/src/controller/latency-controller.ts +18 -13
- package/src/controller/level-controller.ts +37 -19
- package/src/controller/stream-controller.ts +100 -83
- package/src/controller/subtitle-stream-controller.ts +35 -47
- package/src/controller/subtitle-track-controller.ts +5 -3
- package/src/controller/timeline-controller.ts +20 -22
- package/src/crypt/aes-crypto.ts +21 -2
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +32 -16
- package/src/crypt/fast-aes-key.ts +28 -5
- package/src/demux/audio/aacdemuxer.ts +2 -2
- package/src/demux/audio/ac3-demuxer.ts +4 -3
- package/src/demux/audio/adts.ts +9 -4
- package/src/demux/audio/base-audio-demuxer.ts +16 -14
- package/src/demux/audio/mp3demuxer.ts +4 -3
- package/src/demux/audio/mpegaudio.ts +1 -1
- package/src/demux/mp4demuxer.ts +7 -7
- package/src/demux/sample-aes.ts +2 -0
- package/src/demux/transmuxer-interface.ts +8 -16
- package/src/demux/transmuxer-worker.ts +4 -4
- package/src/demux/transmuxer.ts +16 -3
- package/src/demux/tsdemuxer.ts +75 -38
- package/src/demux/video/avc-video-parser.ts +210 -121
- package/src/demux/video/base-video-parser.ts +135 -2
- package/src/demux/video/exp-golomb.ts +0 -208
- package/src/demux/video/hevc-video-parser.ts +749 -0
- package/src/events.ts +8 -1
- package/src/exports-named.ts +1 -1
- package/src/hls.ts +84 -47
- package/src/loader/date-range.ts +71 -5
- package/src/loader/fragment-loader.ts +23 -21
- package/src/loader/fragment.ts +8 -4
- package/src/loader/key-loader.ts +3 -1
- package/src/loader/level-details.ts +6 -6
- package/src/loader/level-key.ts +10 -9
- package/src/loader/m3u8-parser.ts +138 -144
- package/src/loader/playlist-loader.ts +5 -7
- package/src/remux/mp4-generator.ts +196 -1
- package/src/remux/mp4-remuxer.ts +32 -62
- package/src/remux/passthrough-remuxer.ts +1 -1
- package/src/task-loop.ts +5 -2
- package/src/types/component-api.ts +3 -1
- package/src/types/demuxer.ts +3 -0
- package/src/types/events.ts +19 -6
- package/src/types/fragment-tracker.ts +2 -2
- package/src/types/media-playlist.ts +9 -1
- package/src/types/remuxer.ts +1 -1
- package/src/utils/attr-list.ts +96 -9
- package/src/utils/buffer-helper.ts +12 -31
- package/src/utils/cea-608-parser.ts +1 -3
- package/src/utils/codecs.ts +34 -5
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/fetch-loader.ts +1 -1
- package/src/utils/hash.ts +10 -0
- package/src/utils/hdr.ts +4 -7
- package/src/utils/imsc1-ttml-parser.ts +1 -1
- package/src/utils/keysystem-util.ts +1 -6
- package/src/utils/level-helper.ts +71 -44
- package/src/utils/logger.ts +58 -23
- package/src/utils/mp4-tools.ts +5 -3
- package/src/utils/rendition-helper.ts +100 -74
- package/src/utils/utf8-utils.ts +18 -0
- package/src/utils/variable-substitution.ts +0 -19
- package/src/utils/webvtt-parser.ts +2 -12
- package/src/demux/id3.ts +0 -411
- package/src/types/general.ts +0 -6
@@ -1,5 +1,5 @@
|
|
1
1
|
import { Events } from '../events';
|
2
|
-
import { Fragment, Part } from '../loader/fragment';
|
2
|
+
import { Fragment, MediaFragment, Part } from '../loader/fragment';
|
3
3
|
import { PlaylistLevelType } from '../types/loader';
|
4
4
|
import type { SourceBufferName } from '../types/buffer';
|
5
5
|
import type {
|
@@ -47,6 +47,7 @@ export class FragmentTracker implements ComponentAPI {
|
|
47
47
|
|
48
48
|
private _registerListeners() {
|
49
49
|
const { hls } = this;
|
50
|
+
hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
50
51
|
hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this);
|
51
52
|
hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
|
52
53
|
hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
|
@@ -54,6 +55,7 @@ export class FragmentTracker implements ComponentAPI {
|
|
54
55
|
|
55
56
|
private _unregisterListeners() {
|
56
57
|
const { hls } = this;
|
58
|
+
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
57
59
|
hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this);
|
58
60
|
hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
|
59
61
|
hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
|
@@ -107,12 +109,23 @@ export class FragmentTracker implements ComponentAPI {
|
|
107
109
|
public getBufferedFrag(
|
108
110
|
position: number,
|
109
111
|
levelType: PlaylistLevelType,
|
110
|
-
):
|
112
|
+
): MediaFragment | null {
|
113
|
+
return this.getFragAtPos(position, levelType, true);
|
114
|
+
}
|
115
|
+
|
116
|
+
public getFragAtPos(
|
117
|
+
position: number,
|
118
|
+
levelType: PlaylistLevelType,
|
119
|
+
buffered?: boolean,
|
120
|
+
): MediaFragment | null {
|
111
121
|
const { fragments } = this;
|
112
122
|
const keys = Object.keys(fragments);
|
113
123
|
for (let i = keys.length; i--; ) {
|
114
124
|
const fragmentEntity = fragments[keys[i]];
|
115
|
-
if (
|
125
|
+
if (
|
126
|
+
fragmentEntity?.body.type === levelType &&
|
127
|
+
(!buffered || fragmentEntity.buffered)
|
128
|
+
) {
|
116
129
|
const frag = fragmentEntity.body;
|
117
130
|
if (frag.start <= position && position <= frag.end) {
|
118
131
|
return frag;
|
@@ -132,22 +145,26 @@ export class FragmentTracker implements ComponentAPI {
|
|
132
145
|
timeRange: TimeRanges,
|
133
146
|
playlistType: PlaylistLevelType,
|
134
147
|
appendedPart?: Part | null,
|
148
|
+
removeAppending?: boolean,
|
135
149
|
) {
|
136
150
|
if (this.timeRanges) {
|
137
151
|
this.timeRanges[elementaryStream] = timeRange;
|
138
152
|
}
|
139
153
|
// Check if any flagged fragments have been unloaded
|
140
154
|
// excluding anything newer than appendedPartSn
|
141
|
-
const appendedPartSn =
|
155
|
+
const appendedPartSn = appendedPart?.fragment.sn || -1;
|
142
156
|
Object.keys(this.fragments).forEach((key) => {
|
143
157
|
const fragmentEntity = this.fragments[key];
|
144
158
|
if (!fragmentEntity) {
|
145
159
|
return;
|
146
160
|
}
|
147
|
-
if (appendedPartSn >=
|
161
|
+
if (appendedPartSn >= fragmentEntity.body.sn) {
|
148
162
|
return;
|
149
163
|
}
|
150
|
-
if (
|
164
|
+
if (
|
165
|
+
!fragmentEntity.buffered &&
|
166
|
+
(!fragmentEntity.loaded || removeAppending)
|
167
|
+
) {
|
151
168
|
if (fragmentEntity.body.type === playlistType) {
|
152
169
|
this.removeFragment(fragmentEntity.body);
|
153
170
|
}
|
@@ -157,6 +174,10 @@ export class FragmentTracker implements ComponentAPI {
|
|
157
174
|
if (!esData) {
|
158
175
|
return;
|
159
176
|
}
|
177
|
+
if (esData.time.length === 0) {
|
178
|
+
this.removeFragment(fragmentEntity.body);
|
179
|
+
return;
|
180
|
+
}
|
160
181
|
esData.time.some((time: FragmentTimeRange) => {
|
161
182
|
const isNotBuffered = !this.isTimeBuffered(
|
162
183
|
time.startPTS,
|
@@ -178,11 +199,11 @@ export class FragmentTracker implements ComponentAPI {
|
|
178
199
|
*/
|
179
200
|
public detectPartialFragments(data: FragBufferedData) {
|
180
201
|
const timeRanges = this.timeRanges;
|
181
|
-
|
182
|
-
if (!timeRanges || frag.sn === 'initSegment') {
|
202
|
+
if (!timeRanges || data.frag.sn === 'initSegment') {
|
183
203
|
return;
|
184
204
|
}
|
185
205
|
|
206
|
+
const frag = data.frag as MediaFragment;
|
186
207
|
const fragKey = getFragmentKey(frag);
|
187
208
|
const fragmentEntity = this.fragments[fragKey];
|
188
209
|
if (!fragmentEntity || (fragmentEntity.buffered && frag.gap)) {
|
@@ -198,7 +219,7 @@ export class FragmentTracker implements ComponentAPI {
|
|
198
219
|
const partial = isFragHint || streamInfo.partial === true;
|
199
220
|
fragmentEntity.range[elementaryStream] = this.getBufferedTimes(
|
200
221
|
frag,
|
201
|
-
part,
|
222
|
+
data.part,
|
202
223
|
partial,
|
203
224
|
timeRange,
|
204
225
|
);
|
@@ -213,7 +234,7 @@ export class FragmentTracker implements ComponentAPI {
|
|
213
234
|
}
|
214
235
|
if (!isPartial(fragmentEntity)) {
|
215
236
|
// Remove older fragment parts from lookup after frag is tracked as buffered
|
216
|
-
this.removeParts(
|
237
|
+
this.removeParts(frag.sn - 1, frag.type);
|
217
238
|
}
|
218
239
|
} else {
|
219
240
|
// remove fragment if nothing was appended
|
@@ -227,11 +248,11 @@ export class FragmentTracker implements ComponentAPI {
|
|
227
248
|
return;
|
228
249
|
}
|
229
250
|
this.activePartLists[levelType] = activeParts.filter(
|
230
|
-
(part) =>
|
251
|
+
(part) => part.fragment.sn >= snToKeep,
|
231
252
|
);
|
232
253
|
}
|
233
254
|
|
234
|
-
public fragBuffered(frag:
|
255
|
+
public fragBuffered(frag: MediaFragment, force?: true) {
|
235
256
|
const fragKey = getFragmentKey(frag);
|
236
257
|
let fragmentEntity = this.fragments[fragKey];
|
237
258
|
if (!fragmentEntity && force) {
|
@@ -376,16 +397,20 @@ export class FragmentTracker implements ComponentAPI {
|
|
376
397
|
return false;
|
377
398
|
}
|
378
399
|
|
400
|
+
private onManifestLoading() {
|
401
|
+
this.removeAllFragments();
|
402
|
+
}
|
403
|
+
|
379
404
|
private onFragLoaded(event: Events.FRAG_LOADED, data: FragLoadedData) {
|
380
|
-
const { frag, part } = data;
|
381
405
|
// don't track initsegment (for which sn is not a number)
|
382
406
|
// don't track frags used for bitrateTest, they're irrelevant.
|
383
|
-
if (frag.sn === 'initSegment' || frag.bitrateTest) {
|
407
|
+
if (data.frag.sn === 'initSegment' || data.frag.bitrateTest) {
|
384
408
|
return;
|
385
409
|
}
|
386
410
|
|
411
|
+
const frag = data.frag as MediaFragment;
|
387
412
|
// Fragment entity `loaded` FragLoadedData is null when loading parts
|
388
|
-
const loaded = part ? null : data;
|
413
|
+
const loaded = data.part ? null : data;
|
389
414
|
|
390
415
|
const fragKey = getFragmentKey(frag);
|
391
416
|
this.fragments[fragKey] = {
|
@@ -401,7 +426,7 @@ export class FragmentTracker implements ComponentAPI {
|
|
401
426
|
event: Events.BUFFER_APPENDED,
|
402
427
|
data: BufferAppendedData,
|
403
428
|
) {
|
404
|
-
const { frag, part, timeRanges } = data;
|
429
|
+
const { frag, part, timeRanges, type } = data;
|
405
430
|
if (frag.sn === 'initSegment') {
|
406
431
|
return;
|
407
432
|
}
|
@@ -415,15 +440,8 @@ export class FragmentTracker implements ComponentAPI {
|
|
415
440
|
}
|
416
441
|
// Store the latest timeRanges loaded in the buffer
|
417
442
|
this.timeRanges = timeRanges;
|
418
|
-
|
419
|
-
|
420
|
-
this.detectEvictedFragments(
|
421
|
-
elementaryStream,
|
422
|
-
timeRange,
|
423
|
-
playlistType,
|
424
|
-
part,
|
425
|
-
);
|
426
|
-
});
|
443
|
+
const timeRange = timeRanges[type] as TimeRanges;
|
444
|
+
this.detectEvictedFragments(type, timeRange, playlistType, part);
|
427
445
|
}
|
428
446
|
|
429
447
|
private onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
|
@@ -435,6 +453,21 @@ export class FragmentTracker implements ComponentAPI {
|
|
435
453
|
return !!this.fragments[fragKey];
|
436
454
|
}
|
437
455
|
|
456
|
+
public hasFragments(type?: PlaylistLevelType): boolean {
|
457
|
+
const { fragments } = this;
|
458
|
+
const keys = Object.keys(fragments);
|
459
|
+
if (!type) {
|
460
|
+
return keys.length > 0;
|
461
|
+
}
|
462
|
+
for (let i = keys.length; i--; ) {
|
463
|
+
const fragmentEntity = fragments[keys[i]];
|
464
|
+
if (fragmentEntity?.body.type === type) {
|
465
|
+
return true;
|
466
|
+
}
|
467
|
+
}
|
468
|
+
return false;
|
469
|
+
}
|
470
|
+
|
438
471
|
public hasParts(type: PlaylistLevelType): boolean {
|
439
472
|
return !!this.activePartLists[type]?.length;
|
440
473
|
}
|
@@ -1,20 +1,22 @@
|
|
1
|
-
import
|
1
|
+
import { State } from './base-stream-controller';
|
2
2
|
import { BufferHelper } from '../utils/buffer-helper';
|
3
3
|
import { ErrorTypes, ErrorDetails } from '../errors';
|
4
4
|
import { PlaylistLevelType } from '../types/loader';
|
5
5
|
import { Events } from '../events';
|
6
|
-
import {
|
6
|
+
import { Logger } from '../utils/logger';
|
7
7
|
import type Hls from '../hls';
|
8
|
+
import type { BufferInfo } from '../utils/buffer-helper';
|
8
9
|
import type { HlsConfig } from '../config';
|
9
10
|
import type { Fragment } from '../loader/fragment';
|
10
11
|
import type { FragmentTracker } from './fragment-tracker';
|
12
|
+
import type { LevelDetails } from '../loader/level-details';
|
11
13
|
|
12
14
|
export const STALL_MINIMUM_DURATION_MS = 250;
|
13
15
|
export const MAX_START_GAP_JUMP = 2.0;
|
14
16
|
export const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1;
|
15
17
|
export const SKIP_BUFFER_RANGE_START = 0.05;
|
16
18
|
|
17
|
-
export default class GapController {
|
19
|
+
export default class GapController extends Logger {
|
18
20
|
private config: HlsConfig;
|
19
21
|
private media: HTMLMediaElement | null = null;
|
20
22
|
private fragmentTracker: FragmentTracker;
|
@@ -24,8 +26,15 @@ export default class GapController {
|
|
24
26
|
private stalled: number | null = null;
|
25
27
|
private moved: boolean = false;
|
26
28
|
private seeking: boolean = false;
|
29
|
+
private ended: number = 0;
|
27
30
|
|
28
|
-
constructor(
|
31
|
+
constructor(
|
32
|
+
config: HlsConfig,
|
33
|
+
media: HTMLMediaElement,
|
34
|
+
fragmentTracker: FragmentTracker,
|
35
|
+
hls: Hls,
|
36
|
+
) {
|
37
|
+
super('gap-controller', hls.logger);
|
29
38
|
this.config = config;
|
30
39
|
this.media = media;
|
31
40
|
this.fragmentTracker = fragmentTracker;
|
@@ -44,7 +53,12 @@ export default class GapController {
|
|
44
53
|
*
|
45
54
|
* @param lastCurrentTime - Previously read playhead position
|
46
55
|
*/
|
47
|
-
public poll(
|
56
|
+
public poll(
|
57
|
+
lastCurrentTime: number,
|
58
|
+
activeFrag: Fragment | null,
|
59
|
+
levelDetails: LevelDetails | undefined,
|
60
|
+
state: string,
|
61
|
+
) {
|
48
62
|
const { config, media, stalled } = this;
|
49
63
|
if (media === null) {
|
50
64
|
return;
|
@@ -57,6 +71,7 @@ export default class GapController {
|
|
57
71
|
|
58
72
|
// The playhead is moving, no-op
|
59
73
|
if (currentTime !== lastCurrentTime) {
|
74
|
+
this.ended = 0;
|
60
75
|
this.moved = true;
|
61
76
|
if (!seeking) {
|
62
77
|
this.nudgeRetry = 0;
|
@@ -65,7 +80,7 @@ export default class GapController {
|
|
65
80
|
// The playhead is now moving, but was previously stalled
|
66
81
|
if (this.stallReported) {
|
67
82
|
const stalledDuration = self.performance.now() - stalled;
|
68
|
-
|
83
|
+
this.warn(
|
69
84
|
`playback not stuck anymore @${currentTime}, after ${Math.round(
|
70
85
|
stalledDuration,
|
71
86
|
)}ms`,
|
@@ -128,12 +143,9 @@ export default class GapController {
|
|
128
143
|
// When joining a live stream with audio tracks, account for live playlist window sliding by allowing
|
129
144
|
// a larger jump over start gaps caused by the audio-stream-controller buffering a start fragment
|
130
145
|
// that begins over 1 target duration after the video start position.
|
131
|
-
const
|
132
|
-
? this.hls.levels[this.hls.currentLevel]
|
133
|
-
: null;
|
134
|
-
const isLive = level?.details?.live;
|
146
|
+
const isLive = !!levelDetails?.live;
|
135
147
|
const maxStartGapJump = isLive
|
136
|
-
?
|
148
|
+
? levelDetails!.targetduration * 2
|
137
149
|
: MAX_START_GAP_JUMP;
|
138
150
|
const partialOrGap = this.fragmentTracker.getPartialFragment(currentTime);
|
139
151
|
if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) {
|
@@ -153,6 +165,21 @@ export default class GapController {
|
|
153
165
|
|
154
166
|
const stalledDuration = tnow - stalled;
|
155
167
|
if (!seeking && stalledDuration >= STALL_MINIMUM_DURATION_MS) {
|
168
|
+
// Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream
|
169
|
+
if (
|
170
|
+
state === State.ENDED &&
|
171
|
+
!levelDetails?.live &&
|
172
|
+
Math.abs(currentTime - (levelDetails?.edge || 0)) < 1
|
173
|
+
) {
|
174
|
+
if (stalledDuration < 1000 || this.ended) {
|
175
|
+
return;
|
176
|
+
}
|
177
|
+
this.ended = currentTime;
|
178
|
+
this.hls.trigger(Events.MEDIA_ENDED, {
|
179
|
+
stalled: true,
|
180
|
+
});
|
181
|
+
return;
|
182
|
+
}
|
156
183
|
// Report stalling after trying to fix
|
157
184
|
this._reportStall(bufferInfo);
|
158
185
|
if (!this.media) {
|
@@ -206,7 +233,7 @@ export default class GapController {
|
|
206
233
|
bufferInfo.nextStart - currentTime < config.maxBufferHole)) &&
|
207
234
|
stalledDurationMs > config.highBufferWatchdogPeriod * 1000
|
208
235
|
) {
|
209
|
-
|
236
|
+
this.warn('Trying to nudge playhead over buffer-hole');
|
210
237
|
// Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds
|
211
238
|
// We only try to jump the hole if it's under the configured size
|
212
239
|
// Reset stalled so to rearm watchdog timer
|
@@ -230,7 +257,7 @@ export default class GapController {
|
|
230
257
|
media.currentTime
|
231
258
|
} due to low buffer (${JSON.stringify(bufferInfo)})`,
|
232
259
|
);
|
233
|
-
|
260
|
+
this.warn(error.message);
|
234
261
|
hls.trigger(Events.ERROR, {
|
235
262
|
type: ErrorTypes.MEDIA_ERROR,
|
236
263
|
details: ErrorDetails.BUFFER_STALLED_ERROR,
|
@@ -305,7 +332,7 @@ export default class GapController {
|
|
305
332
|
startTime + SKIP_BUFFER_RANGE_START,
|
306
333
|
currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS,
|
307
334
|
);
|
308
|
-
|
335
|
+
this.warn(
|
309
336
|
`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`,
|
310
337
|
);
|
311
338
|
this.moved = true;
|
@@ -348,7 +375,7 @@ export default class GapController {
|
|
348
375
|
const error = new Error(
|
349
376
|
`Nudging 'currentTime' from ${currentTime} to ${targetTime}`,
|
350
377
|
);
|
351
|
-
|
378
|
+
this.warn(error.message);
|
352
379
|
media.currentTime = targetTime;
|
353
380
|
hls.trigger(Events.ERROR, {
|
354
381
|
type: ErrorTypes.MEDIA_ERROR,
|
@@ -360,7 +387,7 @@ export default class GapController {
|
|
360
387
|
const error = new Error(
|
361
388
|
`Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`,
|
362
389
|
);
|
363
|
-
|
390
|
+
this.error(error.message);
|
364
391
|
hls.trigger(Events.ERROR, {
|
365
392
|
type: ErrorTypes.MEDIA_ERROR,
|
366
393
|
details: ErrorDetails.BUFFER_STALLED_ERROR,
|
@@ -4,21 +4,24 @@ import {
|
|
4
4
|
clearCurrentCues,
|
5
5
|
removeCuesInRange,
|
6
6
|
} from '../utils/texttrack-utils';
|
7
|
-
import * as ID3 from '../demux/id3';
|
8
7
|
import {
|
9
8
|
DateRange,
|
10
9
|
isDateRangeCueAttribute,
|
11
10
|
isSCTE35Attribute,
|
12
11
|
} from '../loader/date-range';
|
12
|
+
import { LevelDetails } from '../loader/level-details';
|
13
13
|
import { MetadataSchema } from '../types/demuxer';
|
14
14
|
import type {
|
15
15
|
BufferFlushingData,
|
16
16
|
FragParsingMetadataData,
|
17
|
+
LevelPTSUpdatedData,
|
17
18
|
LevelUpdatedData,
|
18
19
|
MediaAttachedData,
|
19
20
|
} from '../types/events';
|
20
21
|
import type { ComponentAPI } from '../types/component-api';
|
21
22
|
import type Hls from '../hls';
|
23
|
+
import { getId3Frames } from '@svta/common-media-library/id3/getId3Frames';
|
24
|
+
import { isId3TimestampFrame } from '@svta/common-media-library/id3/isId3TimestampFrame';
|
22
25
|
|
23
26
|
declare global {
|
24
27
|
interface Window {
|
@@ -68,10 +71,6 @@ const MAX_CUE_ENDTIME = (() => {
|
|
68
71
|
return Number.POSITIVE_INFINITY;
|
69
72
|
})();
|
70
73
|
|
71
|
-
function dateRangeDateToTimelineSeconds(date: Date, offset: number): number {
|
72
|
-
return date.getTime() / 1000 - offset;
|
73
|
-
}
|
74
|
-
|
75
74
|
function hexToArrayBuffer(str): ArrayBuffer {
|
76
75
|
return Uint8Array.from(
|
77
76
|
str
|
@@ -99,7 +98,7 @@ class ID3TrackController implements ComponentAPI {
|
|
99
98
|
this._registerListeners();
|
100
99
|
}
|
101
100
|
|
102
|
-
destroy() {
|
101
|
+
public destroy() {
|
103
102
|
this._unregisterListeners();
|
104
103
|
this.id3Track = null;
|
105
104
|
this.media = null;
|
@@ -116,6 +115,7 @@ class ID3TrackController implements ComponentAPI {
|
|
116
115
|
hls.on(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this);
|
117
116
|
hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
|
118
117
|
hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
|
118
|
+
hls.on(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this);
|
119
119
|
}
|
120
120
|
|
121
121
|
private _unregisterListeners() {
|
@@ -126,22 +126,22 @@ class ID3TrackController implements ComponentAPI {
|
|
126
126
|
hls.off(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this);
|
127
127
|
hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
|
128
128
|
hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
|
129
|
+
hls.off(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this);
|
129
130
|
}
|
130
131
|
|
131
132
|
// Add ID3 metatadata text track.
|
132
|
-
|
133
|
+
private onMediaAttached(
|
133
134
|
event: Events.MEDIA_ATTACHED,
|
134
135
|
data: MediaAttachedData,
|
135
136
|
): void {
|
136
137
|
this.media = data.media;
|
137
138
|
}
|
138
139
|
|
139
|
-
|
140
|
-
if (
|
141
|
-
|
140
|
+
private onMediaDetaching(): void {
|
141
|
+
if (this.id3Track) {
|
142
|
+
clearCurrentCues(this.id3Track);
|
143
|
+
this.id3Track = null;
|
142
144
|
}
|
143
|
-
clearCurrentCues(this.id3Track);
|
144
|
-
this.id3Track = null;
|
145
145
|
this.media = null;
|
146
146
|
this.dateRangeCuesAppended = {};
|
147
147
|
}
|
@@ -150,13 +150,13 @@ class ID3TrackController implements ComponentAPI {
|
|
150
150
|
this.dateRangeCuesAppended = {};
|
151
151
|
}
|
152
152
|
|
153
|
-
createTrack(media: HTMLMediaElement): TextTrack {
|
153
|
+
private createTrack(media: HTMLMediaElement): TextTrack {
|
154
154
|
const track = this.getID3Track(media.textTracks) as TextTrack;
|
155
155
|
track.mode = 'hidden';
|
156
156
|
return track;
|
157
157
|
}
|
158
158
|
|
159
|
-
getID3Track(textTracks: TextTrackList): TextTrack | void {
|
159
|
+
private getID3Track(textTracks: TextTrackList): TextTrack | void {
|
160
160
|
if (!this.media) {
|
161
161
|
return;
|
162
162
|
}
|
@@ -173,7 +173,7 @@ class ID3TrackController implements ComponentAPI {
|
|
173
173
|
return this.media.addTextTrack('metadata', 'id3');
|
174
174
|
}
|
175
175
|
|
176
|
-
onFragParsingMetadata(
|
176
|
+
private onFragParsingMetadata(
|
177
177
|
event: Events.FRAG_PARSING_METADATA,
|
178
178
|
data: FragParsingMetadataData,
|
179
179
|
) {
|
@@ -211,7 +211,7 @@ class ID3TrackController implements ComponentAPI {
|
|
211
211
|
continue;
|
212
212
|
}
|
213
213
|
|
214
|
-
const frames =
|
214
|
+
const frames = getId3Frames(samples[i].data);
|
215
215
|
if (frames) {
|
216
216
|
const startTime = samples[i].pts;
|
217
217
|
let endTime: number = startTime + samples[i].duration;
|
@@ -228,7 +228,7 @@ class ID3TrackController implements ComponentAPI {
|
|
228
228
|
for (let j = 0; j < frames.length; j++) {
|
229
229
|
const frame = frames[j];
|
230
230
|
// Safari doesn't put the timestamp frame in the TextTrack
|
231
|
-
if (!
|
231
|
+
if (!isId3TimestampFrame(frame)) {
|
232
232
|
// add a bounds to any unbounded cues
|
233
233
|
this.updateId3CueEnds(startTime, type);
|
234
234
|
const cue = createCueWithDataFields(
|
@@ -247,7 +247,7 @@ class ID3TrackController implements ComponentAPI {
|
|
247
247
|
}
|
248
248
|
}
|
249
249
|
|
250
|
-
updateId3CueEnds(startTime: number, type: MetadataSchema) {
|
250
|
+
private updateId3CueEnds(startTime: number, type: MetadataSchema) {
|
251
251
|
const cues = this.id3Track?.cues;
|
252
252
|
if (cues) {
|
253
253
|
for (let i = cues.length; i--; ) {
|
@@ -263,7 +263,7 @@ class ID3TrackController implements ComponentAPI {
|
|
263
263
|
}
|
264
264
|
}
|
265
265
|
|
266
|
-
onBufferFlushing(
|
266
|
+
private onBufferFlushing(
|
267
267
|
event: Events.BUFFER_FLUSHING,
|
268
268
|
{ startOffset, endOffset, type }: BufferFlushingData,
|
269
269
|
) {
|
@@ -295,7 +295,23 @@ class ID3TrackController implements ComponentAPI {
|
|
295
295
|
}
|
296
296
|
}
|
297
297
|
|
298
|
-
onLevelUpdated(
|
298
|
+
private onLevelUpdated(
|
299
|
+
event: Events.LEVEL_UPDATED,
|
300
|
+
{ details }: LevelUpdatedData,
|
301
|
+
) {
|
302
|
+
this.updateDateRangeCues(details, true);
|
303
|
+
}
|
304
|
+
|
305
|
+
private onLevelPtsUpdated(
|
306
|
+
event: Events.LEVEL_PTS_UPDATED,
|
307
|
+
data: LevelPTSUpdatedData,
|
308
|
+
) {
|
309
|
+
if (Math.abs(data.drift) > 0.01) {
|
310
|
+
this.updateDateRangeCues(data.details);
|
311
|
+
}
|
312
|
+
}
|
313
|
+
|
314
|
+
private updateDateRangeCues(details: LevelDetails, removeOldCues?: true) {
|
299
315
|
if (
|
300
316
|
!this.media ||
|
301
317
|
!details.hasProgramDateTime ||
|
@@ -307,7 +323,7 @@ class ID3TrackController implements ComponentAPI {
|
|
307
323
|
const { dateRanges } = details;
|
308
324
|
const ids = Object.keys(dateRanges);
|
309
325
|
// Remove cues from track not found in details.dateRanges
|
310
|
-
if (id3Track) {
|
326
|
+
if (id3Track && removeOldCues) {
|
311
327
|
const idsToRemove = Object.keys(dateRangeCuesAppended).filter(
|
312
328
|
(id) => !ids.includes(id),
|
313
329
|
);
|
@@ -329,26 +345,20 @@ class ID3TrackController implements ComponentAPI {
|
|
329
345
|
this.id3Track = this.createTrack(this.media);
|
330
346
|
}
|
331
347
|
|
332
|
-
const dateTimeOffset =
|
333
|
-
(lastFragment.programDateTime as number) / 1000 - lastFragment.start;
|
334
348
|
const Cue = getCueClass();
|
335
|
-
|
336
349
|
for (let i = 0; i < ids.length; i++) {
|
337
350
|
const id = ids[i];
|
338
351
|
const dateRange = dateRanges[id];
|
339
|
-
const startTime =
|
340
|
-
dateRange.startDate,
|
341
|
-
dateTimeOffset,
|
342
|
-
);
|
352
|
+
const startTime = dateRange.startTime;
|
343
353
|
|
344
354
|
// Process DateRanges to determine end-time (known DURATION, END-DATE, or END-ON-NEXT)
|
345
355
|
const appendedDateRangeCues = dateRangeCuesAppended[id];
|
346
356
|
const cues = appendedDateRangeCues?.cues || {};
|
347
357
|
let durationKnown = appendedDateRangeCues?.durationKnown || false;
|
348
358
|
let endTime = MAX_CUE_ENDTIME;
|
349
|
-
const endDate = dateRange
|
350
|
-
if (endDate) {
|
351
|
-
endTime =
|
359
|
+
const { duration, endDate } = dateRange;
|
360
|
+
if (endDate && duration !== null) {
|
361
|
+
endTime = startTime + duration;
|
352
362
|
durationKnown = true;
|
353
363
|
} else if (dateRange.endOnNext && !durationKnown) {
|
354
364
|
const nextDateRangeWithSameClass = ids.reduce(
|
@@ -369,10 +379,7 @@ class ID3TrackController implements ComponentAPI {
|
|
369
379
|
null,
|
370
380
|
);
|
371
381
|
if (nextDateRangeWithSameClass) {
|
372
|
-
endTime =
|
373
|
-
nextDateRangeWithSameClass.startDate,
|
374
|
-
dateTimeOffset,
|
375
|
-
);
|
382
|
+
endTime = nextDateRangeWithSameClass.startTime;
|
376
383
|
durationKnown = true;
|
377
384
|
}
|
378
385
|
}
|
@@ -389,6 +396,9 @@ class ID3TrackController implements ComponentAPI {
|
|
389
396
|
if (cue) {
|
390
397
|
if (durationKnown && !appendedDateRangeCues.durationKnown) {
|
391
398
|
cue.endTime = endTime;
|
399
|
+
} else if (Math.abs(cue.startTime - startTime) > 0.01) {
|
400
|
+
cue.startTime = startTime;
|
401
|
+
cue.endTime = endTime;
|
392
402
|
}
|
393
403
|
} else if (Cue) {
|
394
404
|
let data = dateRange.attr[key];
|
@@ -6,7 +6,6 @@ import type {
|
|
6
6
|
LevelUpdatedData,
|
7
7
|
MediaAttachingData,
|
8
8
|
} from '../types/events';
|
9
|
-
import { logger } from '../utils/logger';
|
10
9
|
import type { ComponentAPI } from '../types/component-api';
|
11
10
|
import type Hls from '../hls';
|
12
11
|
import type { HlsConfig } from '../config';
|
@@ -19,7 +18,7 @@ export default class LatencyController implements ComponentAPI {
|
|
19
18
|
private currentTime: number = 0;
|
20
19
|
private stallCount: number = 0;
|
21
20
|
private _latency: number | null = null;
|
22
|
-
private
|
21
|
+
private _targetLatencyUpdated = false;
|
23
22
|
|
24
23
|
constructor(hls: Hls) {
|
25
24
|
this.hls = hls;
|
@@ -52,6 +51,7 @@ export default class LatencyController implements ComponentAPI {
|
|
52
51
|
const userConfig = this.hls.userConfig;
|
53
52
|
let targetLatency = lowLatencyMode ? partHoldBack || holdBack : holdBack;
|
54
53
|
if (
|
54
|
+
this._targetLatencyUpdated ||
|
55
55
|
userConfig.liveSyncDuration ||
|
56
56
|
userConfig.liveSyncDurationCount ||
|
57
57
|
targetLatency === 0
|
@@ -62,16 +62,21 @@ export default class LatencyController implements ComponentAPI {
|
|
62
62
|
: liveSyncDurationCount * targetduration;
|
63
63
|
}
|
64
64
|
const maxLiveSyncOnStallIncrease = targetduration;
|
65
|
-
const liveSyncOnStallIncrease = 1.0;
|
66
65
|
return (
|
67
66
|
targetLatency +
|
68
67
|
Math.min(
|
69
|
-
this.stallCount * liveSyncOnStallIncrease,
|
68
|
+
this.stallCount * this.config.liveSyncOnStallIncrease,
|
70
69
|
maxLiveSyncOnStallIncrease,
|
71
70
|
)
|
72
71
|
);
|
73
72
|
}
|
74
73
|
|
74
|
+
set targetLatency(latency: number) {
|
75
|
+
this.stallCount = 0;
|
76
|
+
this.config.liveSyncDuration = latency;
|
77
|
+
this._targetLatencyUpdated = true;
|
78
|
+
}
|
79
|
+
|
75
80
|
get liveSyncPosition(): number | null {
|
76
81
|
const liveEdge = this.estimateLiveEdge();
|
77
82
|
const targetLatency = this.targetLatency;
|
@@ -126,7 +131,7 @@ export default class LatencyController implements ComponentAPI {
|
|
126
131
|
this.onMediaDetaching();
|
127
132
|
this.levelDetails = null;
|
128
133
|
// @ts-ignore
|
129
|
-
this.hls =
|
134
|
+
this.hls = null;
|
130
135
|
}
|
131
136
|
|
132
137
|
private registerListeners() {
|
@@ -150,12 +155,12 @@ export default class LatencyController implements ComponentAPI {
|
|
150
155
|
data: MediaAttachingData,
|
151
156
|
) {
|
152
157
|
this.media = data.media;
|
153
|
-
this.media.addEventListener('timeupdate', this.
|
158
|
+
this.media.addEventListener('timeupdate', this.onTimeupdate);
|
154
159
|
}
|
155
160
|
|
156
161
|
private onMediaDetaching() {
|
157
162
|
if (this.media) {
|
158
|
-
this.media.removeEventListener('timeupdate', this.
|
163
|
+
this.media.removeEventListener('timeupdate', this.onTimeupdate);
|
159
164
|
this.media = null;
|
160
165
|
}
|
161
166
|
}
|
@@ -172,10 +177,10 @@ export default class LatencyController implements ComponentAPI {
|
|
172
177
|
) {
|
173
178
|
this.levelDetails = details;
|
174
179
|
if (details.advanced) {
|
175
|
-
this.
|
180
|
+
this.onTimeupdate();
|
176
181
|
}
|
177
182
|
if (!details.live && this.media) {
|
178
|
-
this.media.removeEventListener('timeupdate', this.
|
183
|
+
this.media.removeEventListener('timeupdate', this.onTimeupdate);
|
179
184
|
}
|
180
185
|
}
|
181
186
|
|
@@ -185,13 +190,13 @@ export default class LatencyController implements ComponentAPI {
|
|
185
190
|
}
|
186
191
|
this.stallCount++;
|
187
192
|
if (this.levelDetails?.live) {
|
188
|
-
logger.warn(
|
189
|
-
'[
|
193
|
+
this.hls.logger.warn(
|
194
|
+
'[latency-controller]: Stall detected, adjusting target latency',
|
190
195
|
);
|
191
196
|
}
|
192
197
|
}
|
193
198
|
|
194
|
-
private
|
199
|
+
private onTimeupdate = () => {
|
195
200
|
const { media, levelDetails } = this;
|
196
201
|
if (!media || !levelDetails) {
|
197
202
|
return;
|
@@ -242,7 +247,7 @@ export default class LatencyController implements ComponentAPI {
|
|
242
247
|
} else if (media.playbackRate !== 1 && media.playbackRate !== 0) {
|
243
248
|
media.playbackRate = 1;
|
244
249
|
}
|
245
|
-
}
|
250
|
+
};
|
246
251
|
|
247
252
|
private estimateLiveEdge(): number | null {
|
248
253
|
const { levelDetails } = this;
|