hls.js 1.6.0-rc.1.0.canary.11079 → 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.
@@ -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
- frag = this.filterReplacedPrimary(frag, levelDetails);
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
- filterReplacedPrimary(
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
- __USE_INTERSTITIALS__ &&
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
- return null;
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
- this.log(`Queueing EOS`);
1055
- this.blockUntilOpen(() => {
1056
- this.sourceBuffers.forEach(([type]) => {
1057
- if (type !== null) {
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 = media.currentTime + diff;
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.setBufferingItem(scheduledItem);
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
- liveStartPosition,
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
- // detach media and attach to interstitial player if it does not have another element attached
2308
- this.bufferAssetPlayer(player, media);
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
- if (playlists.length > 1) {
628
- this.log(
629
- `"${interstitial.identifier}" resumption ${resumeTime} past ${playlistType} playlist end ${playlistEnd}`,
630
- );
631
- return true;
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 endAllowance = playlistType === 'audio' ? 0.175 : 0;
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 + endAllowance;
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})`,
@@ -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 = startPosition;
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 && (this.startFragRequested || !hls.config.startFragPrefetch))
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 details = this.getLevelDetails();
1107
- const configuredTimelineOffset = this.config.timelineOffset;
1108
- if (configuredTimelineOffset && startPosition) {
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.startPosition =
105
- this.lastCurrentTime =
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
- liveStartPosition: number,
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
- liveStartPosition &&
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.02;
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
- (this.startTime === 0 || this.snapOptions.out) &&
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;