hls.js 1.6.0-rc.1.0.canary.11078 → 1.6.0-rc.1.0.canary.11080
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/dist/hls.d.mts +12 -3
- package/dist/hls.d.ts +12 -3
- package/dist/hls.js +270 -146
- package/dist/hls.js.d.ts +12 -3
- package/dist/hls.js.map +1 -1
- package/dist/hls.light.js +102 -66
- 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 +59 -28
- 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 +191 -75
- package/dist/hls.mjs.map +1 -1
- package/dist/hls.worker.js +1 -1
- package/package.json +1 -1
- package/src/controller/audio-stream-controller.ts +7 -7
- package/src/controller/base-stream-controller.ts +44 -11
- package/src/controller/buffer-controller.ts +21 -13
- package/src/controller/interstitial-player.ts +30 -0
- package/src/controller/interstitials-controller.ts +69 -21
- package/src/controller/interstitials-schedule.ts +14 -10
- package/src/controller/latency-controller.ts +16 -2
- package/src/controller/stream-controller.ts +9 -8
- package/src/controller/subtitle-stream-controller.ts +4 -5
- package/src/loader/interstitial-asset-list.ts +3 -13
- package/src/loader/interstitial-event.ts +20 -2
@@ -255,6 +255,17 @@ export default class BaseStreamController
|
|
255
255
|
}
|
256
256
|
}
|
257
257
|
|
258
|
+
protected get timelineOffset(): number {
|
259
|
+
const configuredTimelineOffset = this.config.timelineOffset;
|
260
|
+
if (configuredTimelineOffset) {
|
261
|
+
return (
|
262
|
+
this.getLevelDetails()?.appliedTimelineOffset ||
|
263
|
+
configuredTimelineOffset
|
264
|
+
);
|
265
|
+
}
|
266
|
+
return 0;
|
267
|
+
}
|
268
|
+
|
258
269
|
protected onMediaAttached(
|
259
270
|
event: Events.MEDIA_ATTACHED,
|
260
271
|
data: MediaAttachedData,
|
@@ -1299,9 +1310,8 @@ export default class BaseStreamController
|
|
1299
1310
|
: levelDetails.fragmentEnd;
|
1300
1311
|
frag = this.getFragmentAtPosition(pos, end, levelDetails);
|
1301
1312
|
}
|
1302
|
-
|
1303
|
-
|
1304
|
-
return this.mapToInitFragWhenRequired(frag);
|
1313
|
+
const programFrag = this.filterReplacedPrimary(frag, levelDetails);
|
1314
|
+
return this.mapToInitFragWhenRequired(programFrag);
|
1305
1315
|
}
|
1306
1316
|
|
1307
1317
|
protected isLoopLoading(frag: Fragment, targetBufferTime: number): boolean {
|
@@ -1351,18 +1361,26 @@ export default class BaseStreamController
|
|
1351
1361
|
return nextFragment;
|
1352
1362
|
}
|
1353
1363
|
|
1354
|
-
|
1364
|
+
protected get primaryPrefetch(): boolean {
|
1365
|
+
if (interstitialsEnabled(this.hls.config)) {
|
1366
|
+
const playingInterstitial =
|
1367
|
+
this.hls.interstitialsManager?.playingItem?.event;
|
1368
|
+
if (playingInterstitial) {
|
1369
|
+
return true;
|
1370
|
+
}
|
1371
|
+
}
|
1372
|
+
return false;
|
1373
|
+
}
|
1374
|
+
|
1375
|
+
protected filterReplacedPrimary(
|
1355
1376
|
frag: MediaFragment | null,
|
1356
1377
|
details: LevelDetails | undefined,
|
1357
1378
|
): MediaFragment | null {
|
1358
1379
|
if (!frag) {
|
1359
1380
|
return frag;
|
1360
1381
|
}
|
1361
|
-
const config = this.hls.config;
|
1362
1382
|
if (
|
1363
|
-
|
1364
|
-
config.interstitialsController &&
|
1365
|
-
config.enableInterstitialPlayback !== false &&
|
1383
|
+
interstitialsEnabled(this.hls.config) &&
|
1366
1384
|
frag.type !== PlaylistLevelType.SUBTITLE
|
1367
1385
|
) {
|
1368
1386
|
// Do not load fragments outside the buffering schedule segment
|
@@ -1388,7 +1406,13 @@ export default class BaseStreamController
|
|
1388
1406
|
}
|
1389
1407
|
if (frag.start > bufferingItem.end && bufferingItem.nextEvent) {
|
1390
1408
|
// fragment is past schedule item end
|
1391
|
-
|
1409
|
+
// allow some overflow when not appending in place to prevent stalls
|
1410
|
+
if (
|
1411
|
+
bufferingItem.nextEvent.appendInPlace ||
|
1412
|
+
frag.start - bufferingItem.end > 1
|
1413
|
+
) {
|
1414
|
+
return null;
|
1415
|
+
}
|
1392
1416
|
}
|
1393
1417
|
}
|
1394
1418
|
}
|
@@ -1659,6 +1683,7 @@ export default class BaseStreamController
|
|
1659
1683
|
if (startPosition < sliding) {
|
1660
1684
|
startPosition = -1;
|
1661
1685
|
}
|
1686
|
+
const timelineOffset = this.timelineOffset;
|
1662
1687
|
if (startPosition === -1) {
|
1663
1688
|
// Use Playlist EXT-X-START:TIME-OFFSET when set
|
1664
1689
|
// Prioritize Multivariant Playlist offset so that main, audio, and subtitle stream-controller start times match
|
@@ -1693,9 +1718,9 @@ export default class BaseStreamController
|
|
1693
1718
|
this.log(`setting startPosition to 0 by default`);
|
1694
1719
|
this.startPosition = startPosition = 0;
|
1695
1720
|
}
|
1696
|
-
this.lastCurrentTime = startPosition;
|
1721
|
+
this.lastCurrentTime = startPosition + timelineOffset;
|
1697
1722
|
}
|
1698
|
-
this.nextLoadPosition = startPosition;
|
1723
|
+
this.nextLoadPosition = startPosition + timelineOffset;
|
1699
1724
|
}
|
1700
1725
|
|
1701
1726
|
protected getLoadPosition(): number {
|
@@ -2066,3 +2091,11 @@ export default class BaseStreamController
|
|
2066
2091
|
return this._state;
|
2067
2092
|
}
|
2068
2093
|
}
|
2094
|
+
|
2095
|
+
function interstitialsEnabled(config: HlsConfig): boolean {
|
2096
|
+
return (
|
2097
|
+
__USE_INTERSTITIALS__ &&
|
2098
|
+
!!config.interstitialsController &&
|
2099
|
+
config.enableInterstitialPlayback !== false
|
2100
|
+
);
|
2101
|
+
}
|
@@ -1051,17 +1051,10 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
|
|
1051
1051
|
!this.sourceBuffers.some(([type]) => type && !this.tracks[type]?.ended);
|
1052
1052
|
|
1053
1053
|
if (allTracksEnding) {
|
1054
|
-
|
1055
|
-
|
1056
|
-
this.
|
1057
|
-
|
1058
|
-
const track = this.tracks[type];
|
1059
|
-
if (track) {
|
1060
|
-
track.ending = false;
|
1061
|
-
}
|
1062
|
-
}
|
1063
|
-
});
|
1064
|
-
if (allowEndOfStream) {
|
1054
|
+
if (allowEndOfStream) {
|
1055
|
+
this.log(`Queueing EOS`);
|
1056
|
+
this.blockUntilOpen(() => {
|
1057
|
+
this.tracksEnded();
|
1065
1058
|
const { mediaSource } = this;
|
1066
1059
|
if (!mediaSource || mediaSource.readyState !== 'open') {
|
1067
1060
|
if (mediaSource) {
|
@@ -1074,12 +1067,27 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
|
|
1074
1067
|
this.log(`Calling mediaSource.endOfStream()`);
|
1075
1068
|
// Allow this to throw and be caught by the enqueueing function
|
1076
1069
|
mediaSource.endOfStream();
|
1077
|
-
|
1070
|
+
|
1071
|
+
this.hls.trigger(Events.BUFFERED_TO_END, undefined);
|
1072
|
+
});
|
1073
|
+
} else {
|
1074
|
+
this.tracksEnded();
|
1078
1075
|
this.hls.trigger(Events.BUFFERED_TO_END, undefined);
|
1079
|
-
}
|
1076
|
+
}
|
1080
1077
|
}
|
1081
1078
|
}
|
1082
1079
|
|
1080
|
+
private tracksEnded() {
|
1081
|
+
this.sourceBuffers.forEach(([type]) => {
|
1082
|
+
if (type !== null) {
|
1083
|
+
const track = this.tracks[type];
|
1084
|
+
if (track) {
|
1085
|
+
track.ending = false;
|
1086
|
+
}
|
1087
|
+
}
|
1088
|
+
});
|
1089
|
+
}
|
1090
|
+
|
1083
1091
|
private onLevelUpdated(
|
1084
1092
|
event: Events.LEVEL_UPDATED,
|
1085
1093
|
{ details }: LevelUpdatedData,
|
@@ -28,6 +28,7 @@ export class HlsAssetPlayer {
|
|
28
28
|
private hasDetails: boolean = false;
|
29
29
|
private mediaAttached: HTMLMediaElement | null = null;
|
30
30
|
private _currentTime?: number;
|
31
|
+
private _bufferedEosTime?: number;
|
31
32
|
|
32
33
|
constructor(
|
33
34
|
HlsPlayerClass: typeof Hls,
|
@@ -62,6 +63,22 @@ export class HlsAssetPlayer {
|
|
62
63
|
});
|
63
64
|
}
|
64
65
|
|
66
|
+
bufferedInPlaceToEnd(media?: HTMLMediaElement | null) {
|
67
|
+
if (!this.interstitial.appendInPlace) {
|
68
|
+
return false;
|
69
|
+
}
|
70
|
+
if (this.hls?.bufferedToEnd) {
|
71
|
+
return true;
|
72
|
+
}
|
73
|
+
if (!media || !this._bufferedEosTime) {
|
74
|
+
return false;
|
75
|
+
}
|
76
|
+
const start = this.timelineOffset;
|
77
|
+
const bufferInfo = BufferHelper.bufferInfo(media, start, 0);
|
78
|
+
const bufferedEnd = this.getAssetTime(bufferInfo.end);
|
79
|
+
return bufferedEnd >= this._bufferedEosTime - 0.02;
|
80
|
+
}
|
81
|
+
|
65
82
|
private checkPlayout = () => {
|
66
83
|
const interstitial = this.interstitial;
|
67
84
|
const playoutLimit = interstitial.playoutLimit;
|
@@ -90,6 +107,9 @@ export class HlsAssetPlayer {
|
|
90
107
|
get bufferedEnd(): number {
|
91
108
|
const media = this.media || this.mediaAttached;
|
92
109
|
if (!media) {
|
110
|
+
if (this._bufferedEosTime) {
|
111
|
+
return this._bufferedEosTime;
|
112
|
+
}
|
93
113
|
return this.currentTime;
|
94
114
|
}
|
95
115
|
const bufferInfo = BufferHelper.bufferInfo(media, media.currentTime, 0.001);
|
@@ -153,10 +173,19 @@ export class HlsAssetPlayer {
|
|
153
173
|
const media = this.mediaAttached;
|
154
174
|
if (media) {
|
155
175
|
this._currentTime = media.currentTime;
|
176
|
+
this.bufferSnapShot();
|
156
177
|
media.removeEventListener('timeupdate', this.checkPlayout);
|
157
178
|
}
|
158
179
|
}
|
159
180
|
|
181
|
+
private bufferSnapShot() {
|
182
|
+
if (this.mediaAttached) {
|
183
|
+
if (this.hls?.bufferedToEnd) {
|
184
|
+
this._bufferedEosTime = this.bufferedEnd;
|
185
|
+
}
|
186
|
+
}
|
187
|
+
}
|
188
|
+
|
160
189
|
destroy() {
|
161
190
|
this.removeMediaListeners();
|
162
191
|
this.hls.destroy();
|
@@ -185,6 +214,7 @@ export class HlsAssetPlayer {
|
|
185
214
|
}
|
186
215
|
|
187
216
|
transferMedia() {
|
217
|
+
this.bufferSnapShot();
|
188
218
|
return this.hls.transferMedia();
|
189
219
|
}
|
190
220
|
|
@@ -417,7 +417,8 @@ export default class InterstitialsController
|
|
417
417
|
);
|
418
418
|
|
419
419
|
const diff = time - currentTime;
|
420
|
-
const seekToTime =
|
420
|
+
const seekToTime =
|
421
|
+
(appendInPlace ? currentTime : media.currentTime) + diff;
|
421
422
|
if (
|
422
423
|
seekToTime >= 0 &&
|
423
424
|
(!assetPlayer ||
|
@@ -592,10 +593,6 @@ export default class InterstitialsController
|
|
592
593
|
},
|
593
594
|
get currentTime() {
|
594
595
|
const timelinePos = c.timelinePos;
|
595
|
-
const playingItem = c.effectivePlayingItem;
|
596
|
-
if (playingItem?.event?.appendInPlace) {
|
597
|
-
return playingItem.start;
|
598
|
-
}
|
599
596
|
return timelinePos > 0 ? timelinePos : 0;
|
600
597
|
},
|
601
598
|
set currentTime(time: number) {
|
@@ -806,6 +803,11 @@ export default class InterstitialsController
|
|
806
803
|
const interstitial = queuedPlayer.interstitial;
|
807
804
|
this.clearInterstitial(queuedPlayer.interstitial, null);
|
808
805
|
interstitial.appendInPlace = false;
|
806
|
+
if (interstitial.appendInPlace) {
|
807
|
+
this.warn(
|
808
|
+
`Could not change append strategy for queued assets ${interstitial}`,
|
809
|
+
);
|
810
|
+
}
|
809
811
|
}
|
810
812
|
});
|
811
813
|
}
|
@@ -1165,7 +1167,9 @@ export default class InterstitialsController
|
|
1165
1167
|
}
|
1166
1168
|
// Ensure Interstitial is enqueued
|
1167
1169
|
const waitingItem = this.waitingItem;
|
1168
|
-
this.
|
1170
|
+
if (!this.assetsBuffered(scheduledItem, media)) {
|
1171
|
+
this.setBufferingItem(scheduledItem);
|
1172
|
+
}
|
1169
1173
|
let player = this.preloadAssets(interstitial, assetListIndex);
|
1170
1174
|
if (!this.eventItemsMatch(scheduledItem, waitingItem || currentItem)) {
|
1171
1175
|
this.waitingItem = scheduledItem;
|
@@ -1791,6 +1795,20 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
|
|
1791
1795
|
}
|
1792
1796
|
}
|
1793
1797
|
|
1798
|
+
private assetsBuffered(
|
1799
|
+
item: InterstitialScheduleEventItem,
|
1800
|
+
media: HTMLMediaElement | null,
|
1801
|
+
): boolean {
|
1802
|
+
const assetList = item.event.assetList;
|
1803
|
+
if (assetList.length === 0) {
|
1804
|
+
return false;
|
1805
|
+
}
|
1806
|
+
return !item.event.assetList.some((asset) => {
|
1807
|
+
const player = this.getAssetPlayer(asset.identifier);
|
1808
|
+
return !player?.bufferedInPlaceToEnd(media);
|
1809
|
+
});
|
1810
|
+
}
|
1811
|
+
|
1794
1812
|
private setBufferingItem(
|
1795
1813
|
item: InterstitialScheduleItem,
|
1796
1814
|
): InterstitialScheduleItem | null {
|
@@ -1910,13 +1928,34 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
|
|
1910
1928
|
const neverLoaded = assetListLength === 0 && !interstitial.assetListLoader;
|
1911
1929
|
const playOnce = interstitial.cue.once;
|
1912
1930
|
if (neverLoaded) {
|
1913
|
-
this.log(
|
1914
|
-
`Load interstitial asset ${assetListIndex + 1}/${uri ? 1 : assetListLength} ${interstitial}`,
|
1915
|
-
);
|
1916
1931
|
const timelineStart = interstitial.timelineStart;
|
1917
1932
|
if (interstitial.appendInPlace) {
|
1918
1933
|
this.flushFrontBuffer(timelineStart + 0.25);
|
1919
1934
|
}
|
1935
|
+
let hlsStartOffset;
|
1936
|
+
let liveStartPosition = 0;
|
1937
|
+
if (!this.playingItem && this.primaryLive) {
|
1938
|
+
liveStartPosition = this.hls.startPosition;
|
1939
|
+
if (liveStartPosition === -1) {
|
1940
|
+
liveStartPosition = this.hls.liveSyncPosition || 0;
|
1941
|
+
}
|
1942
|
+
}
|
1943
|
+
if (
|
1944
|
+
liveStartPosition &&
|
1945
|
+
!(interstitial.cue.pre || interstitial.cue.post)
|
1946
|
+
) {
|
1947
|
+
const startOffset = liveStartPosition - timelineStart;
|
1948
|
+
if (startOffset > 0) {
|
1949
|
+
hlsStartOffset = Math.round(startOffset * 1000) / 1000;
|
1950
|
+
}
|
1951
|
+
}
|
1952
|
+
this.log(
|
1953
|
+
`Load interstitial asset ${assetListIndex + 1}/${uri ? 1 : assetListLength} ${interstitial}${
|
1954
|
+
hlsStartOffset
|
1955
|
+
? ` live-start: ${liveStartPosition} start-offset: ${hlsStartOffset}`
|
1956
|
+
: ''
|
1957
|
+
}`,
|
1958
|
+
);
|
1920
1959
|
if (uri) {
|
1921
1960
|
return this.createAsset(
|
1922
1961
|
interstitial,
|
@@ -1927,16 +1966,9 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
|
|
1927
1966
|
uri,
|
1928
1967
|
);
|
1929
1968
|
}
|
1930
|
-
let liveStartPosition = 0;
|
1931
|
-
if (!this.playingItem && this.primaryLive) {
|
1932
|
-
liveStartPosition = this.hls.startPosition;
|
1933
|
-
if (liveStartPosition === -1) {
|
1934
|
-
liveStartPosition = this.hls.liveSyncPosition || 0;
|
1935
|
-
}
|
1936
|
-
}
|
1937
1969
|
const assetListLoader = this.assetListLoader.loadAssetList(
|
1938
1970
|
interstitial as InterstitialEventWithAssetList,
|
1939
|
-
|
1971
|
+
hlsStartOffset,
|
1940
1972
|
);
|
1941
1973
|
if (assetListLoader) {
|
1942
1974
|
interstitial.assetListLoader = assetListLoader;
|
@@ -2046,7 +2078,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
|
|
2046
2078
|
const selectedAudio = primary.audioTracks[primary.audioTrack];
|
2047
2079
|
const selectedSubtitle = primary.subtitleTracks[primary.subtitleTrack];
|
2048
2080
|
let startPosition = 0;
|
2049
|
-
if (this.primaryLive) {
|
2081
|
+
if (this.primaryLive || interstitial.appendInPlace) {
|
2050
2082
|
const timePastStart = this.timelinePos - assetItem.timelineStart;
|
2051
2083
|
if (timePastStart > 1) {
|
2052
2084
|
const duration = assetItem.duration;
|
@@ -2304,8 +2336,10 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
|
|
2304
2336
|
});
|
2305
2337
|
}
|
2306
2338
|
|
2307
|
-
|
2308
|
-
|
2339
|
+
if (!player.bufferedInPlaceToEnd(media)) {
|
2340
|
+
// detach media and attach to interstitial player if it does not have another element attached
|
2341
|
+
this.bufferAssetPlayer(player, media);
|
2342
|
+
}
|
2309
2343
|
}
|
2310
2344
|
|
2311
2345
|
private bufferAssetPlayer(player: HlsAssetPlayer, media: HTMLMediaElement) {
|
@@ -2466,6 +2500,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
|
|
2466
2500
|
return;
|
2467
2501
|
}
|
2468
2502
|
const eventStart = interstitial.timelineStart;
|
2503
|
+
const previousDuration = interstitial.duration;
|
2469
2504
|
let sumDuration = 0;
|
2470
2505
|
assets.forEach((asset, assetListIndex) => {
|
2471
2506
|
const duration = parseFloat(asset.DURATION);
|
@@ -2480,7 +2515,9 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
|
|
2480
2515
|
sumDuration += duration;
|
2481
2516
|
});
|
2482
2517
|
interstitial.duration = sumDuration;
|
2483
|
-
|
2518
|
+
this.log(
|
2519
|
+
`Loaded asset-list with duration: ${sumDuration} (was: ${previousDuration}) ${interstitial}`,
|
2520
|
+
);
|
2484
2521
|
const waitingItem = this.waitingItem;
|
2485
2522
|
const waitingForItem = waitingItem?.event.identifier === interstitialId;
|
2486
2523
|
|
@@ -2495,6 +2532,17 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
|
|
2495
2532
|
const scheduleIndex = this.schedule.findEventIndex(interstitialId);
|
2496
2533
|
const item = this.schedule.items?.[scheduleIndex];
|
2497
2534
|
if (item) {
|
2535
|
+
if (!this.playingItem && this.timelinePos > item.end) {
|
2536
|
+
// Abandon if new duration is reduced enough to land playback in primary start
|
2537
|
+
const index = this.schedule.findItemIndexAtTime(this.timelinePos);
|
2538
|
+
if (index !== scheduleIndex) {
|
2539
|
+
interstitial.error = new Error(
|
2540
|
+
`Interstitial no longer within playback range ${this.timelinePos} ${interstitial}`,
|
2541
|
+
);
|
2542
|
+
this.primaryFallback(interstitial);
|
2543
|
+
return;
|
2544
|
+
}
|
2545
|
+
}
|
2498
2546
|
this.setBufferingItem(item);
|
2499
2547
|
}
|
2500
2548
|
this.setSchedulePosition(scheduleIndex);
|
@@ -588,6 +588,11 @@ export class InterstitialsSchedule extends Logger {
|
|
588
588
|
interstitialEvents[i].resumeTime;
|
589
589
|
if (timeBetween < ABUTTING_THRESHOLD_SECONDS) {
|
590
590
|
interstitialEvents[i + 1].appendInPlace = false;
|
591
|
+
if (interstitialEvents[i + 1].appendInPlace) {
|
592
|
+
this.warn(
|
593
|
+
`Could not change append strategy for abutting event ${interstitial}`,
|
594
|
+
);
|
595
|
+
}
|
591
596
|
}
|
592
597
|
}
|
593
598
|
// Update cumulativeDuration for next abutting interstitial with the same start date
|
@@ -624,12 +629,11 @@ export class InterstitialsSchedule extends Logger {
|
|
624
629
|
const details = mediaSelection[playlistType].details;
|
625
630
|
const playlistEnd = details.edge;
|
626
631
|
if (resumeTime > playlistEnd) {
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
}
|
632
|
+
// Live playback - resumption segments are not yet available
|
633
|
+
this.log(
|
634
|
+
`"${interstitial.identifier}" resumption ${resumeTime} past ${playlistType} playlist end ${playlistEnd}`,
|
635
|
+
);
|
636
|
+
// Assume alignment is possible (or reset can take place)
|
633
637
|
return false;
|
634
638
|
}
|
635
639
|
const startFragment = findFragmentByPTS(
|
@@ -639,16 +643,16 @@ export class InterstitialsSchedule extends Logger {
|
|
639
643
|
);
|
640
644
|
if (!startFragment) {
|
641
645
|
this.log(
|
642
|
-
`"${interstitial.identifier}" resumption ${resumeTime} does not align with any fragments in ${playlistType} playlist`,
|
646
|
+
`"${interstitial.identifier}" resumption ${resumeTime} does not align with any fragments in ${playlistType} playlist (${details.fragStart}-${details.fragmentEnd})`,
|
643
647
|
);
|
644
648
|
return true;
|
645
649
|
}
|
646
|
-
const
|
650
|
+
const allowance = playlistType === 'audio' ? 0.175 : 0;
|
647
651
|
const alignedWithSegment =
|
648
652
|
Math.abs(startFragment.start - resumeTime) <
|
649
|
-
ALIGNED_END_THRESHOLD_SECONDS ||
|
653
|
+
ALIGNED_END_THRESHOLD_SECONDS + allowance ||
|
650
654
|
Math.abs(startFragment.end - resumeTime) <
|
651
|
-
ALIGNED_END_THRESHOLD_SECONDS +
|
655
|
+
ALIGNED_END_THRESHOLD_SECONDS + allowance;
|
652
656
|
if (!alignedWithSegment) {
|
653
657
|
this.log(
|
654
658
|
`"${interstitial.identifier}" resumption ${resumeTime} not aligned with ${playlistType} fragment bounds (${startFragment.start}-${startFragment.end} sn: ${startFragment.sn} cc: ${startFragment.cc})`,
|
@@ -256,12 +256,26 @@ export default class LatencyController implements ComponentAPI {
|
|
256
256
|
(2 / (1 + Math.exp(-0.75 * distanceFromTarget - this.edgeStalled))) *
|
257
257
|
20,
|
258
258
|
) / 20;
|
259
|
-
|
259
|
+
const playbackRate = Math.min(max, Math.max(1, rate));
|
260
|
+
this.changeMediaPlaybackRate(media, playbackRate);
|
260
261
|
} else if (media.playbackRate !== 1 && media.playbackRate !== 0) {
|
261
|
-
media
|
262
|
+
this.changeMediaPlaybackRate(media, 1);
|
262
263
|
}
|
263
264
|
};
|
264
265
|
|
266
|
+
private changeMediaPlaybackRate(
|
267
|
+
media: HTMLMediaElement,
|
268
|
+
playbackRate: number,
|
269
|
+
) {
|
270
|
+
if (media.playbackRate === playbackRate) {
|
271
|
+
return;
|
272
|
+
}
|
273
|
+
this.hls?.logger.debug(
|
274
|
+
`[latency-controller]: latency=${this.latency.toFixed(3)}, targetLatency=${this.targetLatency?.toFixed(3)}, forwardBufferLength=${this.forwardBufferLength.toFixed(3)}: adjusting playback rate from ${media.playbackRate} to ${playbackRate}`,
|
275
|
+
);
|
276
|
+
media.playbackRate = playbackRate;
|
277
|
+
}
|
278
|
+
|
265
279
|
private estimateLiveEdge(): number | null {
|
266
280
|
const levelDetails = this.levelDetails;
|
267
281
|
if (levelDetails === null) {
|
@@ -176,7 +176,8 @@ export default class StreamController
|
|
176
176
|
startPosition = lastCurrentTime;
|
177
177
|
}
|
178
178
|
this.state = State.IDLE;
|
179
|
-
this.nextLoadPosition = this.lastCurrentTime =
|
179
|
+
this.nextLoadPosition = this.lastCurrentTime =
|
180
|
+
startPosition + this.timelineOffset;
|
180
181
|
this.startPosition = skipSeekToStartPosition ? -1 : startPosition;
|
181
182
|
this.tick();
|
182
183
|
} else {
|
@@ -251,7 +252,9 @@ export default class StreamController
|
|
251
252
|
// exit loop, as we either need more info (level not parsed) or we need media to be attached to load new fragment
|
252
253
|
if (
|
253
254
|
levelLastLoaded === null ||
|
254
|
-
(!media &&
|
255
|
+
(!media &&
|
256
|
+
!this.primaryPrefetch &&
|
257
|
+
(this.startFragRequested || !hls.config.startFragPrefetch))
|
255
258
|
) {
|
256
259
|
return;
|
257
260
|
}
|
@@ -1103,13 +1106,11 @@ export default class StreamController
|
|
1103
1106
|
}
|
1104
1107
|
|
1105
1108
|
// Offset start position by timeline offset
|
1106
|
-
const
|
1107
|
-
|
1108
|
-
|
1109
|
-
startPosition +=
|
1110
|
-
details?.appliedTimelineOffset || configuredTimelineOffset;
|
1109
|
+
const timelineOffset = this.timelineOffset;
|
1110
|
+
if (timelineOffset && startPosition) {
|
1111
|
+
startPosition += timelineOffset;
|
1111
1112
|
}
|
1112
|
-
|
1113
|
+
const details = this.getLevelDetails();
|
1113
1114
|
const buffered = BufferHelper.getBuffered(media);
|
1114
1115
|
const bufferStart = buffered.length ? buffered.start(0) : 0;
|
1115
1116
|
const delta = bufferStart - startPosition;
|
@@ -94,16 +94,15 @@ export class SubtitleStreamController
|
|
94
94
|
hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
|
95
95
|
}
|
96
96
|
|
97
|
-
startLoad(startPosition: number) {
|
97
|
+
startLoad(startPosition: number, skipSeekToStartPosition?: boolean) {
|
98
98
|
this.stopLoad();
|
99
99
|
this.state = State.IDLE;
|
100
100
|
|
101
101
|
this.setInterval(TICK_INTERVAL);
|
102
102
|
|
103
|
-
this.nextLoadPosition =
|
104
|
-
this.
|
105
|
-
|
106
|
-
startPosition;
|
103
|
+
this.nextLoadPosition = this.lastCurrentTime =
|
104
|
+
startPosition + this.timelineOffset;
|
105
|
+
this.startPosition = skipSeekToStartPosition ? -1 : startPosition;
|
107
106
|
|
108
107
|
this.tick();
|
109
108
|
}
|
@@ -31,7 +31,7 @@ export class AssetListLoader {
|
|
31
31
|
|
32
32
|
loadAssetList(
|
33
33
|
interstitial: InterstitialEventWithAssetList,
|
34
|
-
|
34
|
+
hlsStartOffset: number | undefined,
|
35
35
|
): Loader<LoaderContext> | undefined {
|
36
36
|
const assetListUrl = interstitial.assetListUrl;
|
37
37
|
let url: URL;
|
@@ -51,18 +51,8 @@ export class AssetListLoader {
|
|
51
51
|
this.hls.trigger(Events.ERROR, errorData);
|
52
52
|
return;
|
53
53
|
}
|
54
|
-
if (
|
55
|
-
|
56
|
-
!(interstitial.cue.pre || interstitial.cue.post) &&
|
57
|
-
url.protocol !== 'data:'
|
58
|
-
) {
|
59
|
-
const startOffset = liveStartPosition - interstitial.startTime;
|
60
|
-
if (startOffset > 0) {
|
61
|
-
url.searchParams.set(
|
62
|
-
'_HLS_start_offset',
|
63
|
-
'' + Math.round(startOffset * 1000) / 1000,
|
64
|
-
);
|
65
|
-
}
|
54
|
+
if (hlsStartOffset && url.protocol !== 'data:') {
|
55
|
+
url.searchParams.set('_HLS_start_offset', '' + hlsStartOffset);
|
66
56
|
}
|
67
57
|
const config = this.hls.config;
|
68
58
|
const Loader = config.loader;
|
@@ -3,7 +3,7 @@ import type { DateRange, DateRangeCue } from './date-range';
|
|
3
3
|
import type { MediaFragmentRef } from './fragment';
|
4
4
|
import type { Loader, LoaderContext } from '../types/loader';
|
5
5
|
|
6
|
-
export const ALIGNED_END_THRESHOLD_SECONDS = 0.
|
6
|
+
export const ALIGNED_END_THRESHOLD_SECONDS = 0.025;
|
7
7
|
|
8
8
|
export type PlaybackRestrictions = {
|
9
9
|
skip: boolean;
|
@@ -76,6 +76,7 @@ export class InterstitialEvent {
|
|
76
76
|
public assetListResponse: AssetListJSON | null = null;
|
77
77
|
public resumeAnchor?: MediaFragmentRef;
|
78
78
|
public error?: Error;
|
79
|
+
public resetOnResume?: boolean;
|
79
80
|
|
80
81
|
constructor(dateRange: DateRange, base: BaseData) {
|
81
82
|
this.base = base;
|
@@ -157,6 +158,19 @@ export class InterstitialEvent {
|
|
157
158
|
return this.cue.pre ? 0 : this.startTime;
|
158
159
|
}
|
159
160
|
|
161
|
+
get startIsAligned(): boolean {
|
162
|
+
if (this.startTime === 0 || this.snapOptions.out) {
|
163
|
+
return true;
|
164
|
+
}
|
165
|
+
const frag = this.dateRange.tagAnchor;
|
166
|
+
if (frag) {
|
167
|
+
const startTime = this.dateRange.startTime;
|
168
|
+
const snappedStart = getSnapToFragmentTime(startTime, frag);
|
169
|
+
return startTime - snappedStart < 0.1;
|
170
|
+
}
|
171
|
+
return false;
|
172
|
+
}
|
173
|
+
|
160
174
|
get resumptionOffset(): number {
|
161
175
|
const resumeOffset = this.resumeOffset;
|
162
176
|
const offset = Number.isFinite(resumeOffset) ? resumeOffset : this.duration;
|
@@ -176,13 +190,16 @@ export class InterstitialEvent {
|
|
176
190
|
}
|
177
191
|
|
178
192
|
get appendInPlace(): boolean {
|
193
|
+
if (this.appendInPlaceStarted) {
|
194
|
+
return true;
|
195
|
+
}
|
179
196
|
if (this.appendInPlaceDisabled) {
|
180
197
|
return false;
|
181
198
|
}
|
182
199
|
if (
|
183
200
|
!this.cue.once &&
|
184
201
|
!this.cue.pre && // preroll starts at startPosition before startPosition is known (live)
|
185
|
-
|
202
|
+
this.startIsAligned &&
|
186
203
|
((isNaN(this.playoutLimit) && isNaN(this.resumeOffset)) ||
|
187
204
|
(this.resumeOffset &&
|
188
205
|
this.duration &&
|
@@ -196,6 +213,7 @@ export class InterstitialEvent {
|
|
196
213
|
|
197
214
|
set appendInPlace(value: boolean) {
|
198
215
|
if (this.appendInPlaceStarted) {
|
216
|
+
this.resetOnResume = !value;
|
199
217
|
return;
|
200
218
|
}
|
201
219
|
this.appendInPlaceDisabled = !value;
|