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