@zenvor/hls.js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +472 -0
  3. package/dist/hls-demo.js +26995 -0
  4. package/dist/hls-demo.js.map +1 -0
  5. package/dist/hls.d.mts +4204 -0
  6. package/dist/hls.d.ts +4204 -0
  7. package/dist/hls.js +40050 -0
  8. package/dist/hls.js.d.ts +4204 -0
  9. package/dist/hls.js.map +1 -0
  10. package/dist/hls.light.js +27145 -0
  11. package/dist/hls.light.js.map +1 -0
  12. package/dist/hls.light.min.js +2 -0
  13. package/dist/hls.light.min.js.map +1 -0
  14. package/dist/hls.light.mjs +26392 -0
  15. package/dist/hls.light.mjs.map +1 -0
  16. package/dist/hls.min.js +2 -0
  17. package/dist/hls.min.js.map +1 -0
  18. package/dist/hls.mjs +38956 -0
  19. package/dist/hls.mjs.map +1 -0
  20. package/dist/hls.worker.js +2 -0
  21. package/dist/hls.worker.js.map +1 -0
  22. package/package.json +143 -0
  23. package/src/config.ts +794 -0
  24. package/src/controller/abr-controller.ts +1019 -0
  25. package/src/controller/algo-data-controller.ts +794 -0
  26. package/src/controller/audio-stream-controller.ts +1099 -0
  27. package/src/controller/audio-track-controller.ts +454 -0
  28. package/src/controller/base-playlist-controller.ts +438 -0
  29. package/src/controller/base-stream-controller.ts +2526 -0
  30. package/src/controller/buffer-controller.ts +2015 -0
  31. package/src/controller/buffer-operation-queue.ts +159 -0
  32. package/src/controller/cap-level-controller.ts +367 -0
  33. package/src/controller/cmcd-controller.ts +422 -0
  34. package/src/controller/content-steering-controller.ts +622 -0
  35. package/src/controller/eme-controller.ts +1617 -0
  36. package/src/controller/error-controller.ts +627 -0
  37. package/src/controller/fps-controller.ts +146 -0
  38. package/src/controller/fragment-finders.ts +256 -0
  39. package/src/controller/fragment-tracker.ts +567 -0
  40. package/src/controller/gap-controller.ts +719 -0
  41. package/src/controller/id3-track-controller.ts +488 -0
  42. package/src/controller/interstitial-player.ts +302 -0
  43. package/src/controller/interstitials-controller.ts +2895 -0
  44. package/src/controller/interstitials-schedule.ts +698 -0
  45. package/src/controller/latency-controller.ts +294 -0
  46. package/src/controller/level-controller.ts +776 -0
  47. package/src/controller/stream-controller.ts +1597 -0
  48. package/src/controller/subtitle-stream-controller.ts +508 -0
  49. package/src/controller/subtitle-track-controller.ts +617 -0
  50. package/src/controller/timeline-controller.ts +677 -0
  51. package/src/crypt/aes-crypto.ts +36 -0
  52. package/src/crypt/aes-decryptor.ts +339 -0
  53. package/src/crypt/decrypter-aes-mode.ts +4 -0
  54. package/src/crypt/decrypter.ts +225 -0
  55. package/src/crypt/fast-aes-key.ts +39 -0
  56. package/src/define-plugin.d.ts +17 -0
  57. package/src/demux/audio/aacdemuxer.ts +126 -0
  58. package/src/demux/audio/ac3-demuxer.ts +170 -0
  59. package/src/demux/audio/adts.ts +249 -0
  60. package/src/demux/audio/base-audio-demuxer.ts +205 -0
  61. package/src/demux/audio/dolby.ts +21 -0
  62. package/src/demux/audio/mp3demuxer.ts +85 -0
  63. package/src/demux/audio/mpegaudio.ts +177 -0
  64. package/src/demux/chunk-cache.ts +42 -0
  65. package/src/demux/dummy-demuxed-track.ts +13 -0
  66. package/src/demux/inject-worker.ts +75 -0
  67. package/src/demux/mp4demuxer.ts +234 -0
  68. package/src/demux/sample-aes.ts +198 -0
  69. package/src/demux/transmuxer-interface.ts +449 -0
  70. package/src/demux/transmuxer-worker.ts +221 -0
  71. package/src/demux/transmuxer.ts +560 -0
  72. package/src/demux/tsdemuxer.ts +1256 -0
  73. package/src/demux/video/avc-video-parser.ts +401 -0
  74. package/src/demux/video/base-video-parser.ts +198 -0
  75. package/src/demux/video/exp-golomb.ts +153 -0
  76. package/src/demux/video/hevc-video-parser.ts +736 -0
  77. package/src/empty-es.js +5 -0
  78. package/src/empty.js +3 -0
  79. package/src/errors.ts +107 -0
  80. package/src/events.ts +548 -0
  81. package/src/exports-default.ts +3 -0
  82. package/src/exports-named.ts +81 -0
  83. package/src/hls.ts +1613 -0
  84. package/src/is-supported.ts +54 -0
  85. package/src/loader/date-range.ts +207 -0
  86. package/src/loader/fragment-loader.ts +403 -0
  87. package/src/loader/fragment.ts +487 -0
  88. package/src/loader/interstitial-asset-list.ts +162 -0
  89. package/src/loader/interstitial-event.ts +337 -0
  90. package/src/loader/key-loader.ts +439 -0
  91. package/src/loader/level-details.ts +203 -0
  92. package/src/loader/level-key.ts +259 -0
  93. package/src/loader/load-stats.ts +17 -0
  94. package/src/loader/m3u8-parser.ts +1072 -0
  95. package/src/loader/playlist-loader.ts +839 -0
  96. package/src/polyfills/number.ts +15 -0
  97. package/src/remux/aac-helper.ts +81 -0
  98. package/src/remux/mp4-generator.ts +1380 -0
  99. package/src/remux/mp4-remuxer.ts +1261 -0
  100. package/src/remux/passthrough-remuxer.ts +434 -0
  101. package/src/task-loop.ts +130 -0
  102. package/src/types/algo.ts +44 -0
  103. package/src/types/buffer.ts +105 -0
  104. package/src/types/component-api.ts +20 -0
  105. package/src/types/demuxer.ts +208 -0
  106. package/src/types/events.ts +574 -0
  107. package/src/types/fragment-tracker.ts +23 -0
  108. package/src/types/level.ts +268 -0
  109. package/src/types/loader.ts +198 -0
  110. package/src/types/media-playlist.ts +92 -0
  111. package/src/types/network-details.ts +3 -0
  112. package/src/types/remuxer.ts +104 -0
  113. package/src/types/track.ts +12 -0
  114. package/src/types/transmuxer.ts +46 -0
  115. package/src/types/tuples.ts +6 -0
  116. package/src/types/vtt.ts +11 -0
  117. package/src/utils/arrays.ts +22 -0
  118. package/src/utils/attr-list.ts +192 -0
  119. package/src/utils/binary-search.ts +46 -0
  120. package/src/utils/buffer-helper.ts +173 -0
  121. package/src/utils/cea-608-parser.ts +1413 -0
  122. package/src/utils/chunker.ts +41 -0
  123. package/src/utils/codecs.ts +314 -0
  124. package/src/utils/cues.ts +96 -0
  125. package/src/utils/discontinuities.ts +174 -0
  126. package/src/utils/encryption-methods-util.ts +21 -0
  127. package/src/utils/error-helper.ts +95 -0
  128. package/src/utils/event-listener-helper.ts +16 -0
  129. package/src/utils/ewma-bandwidth-estimator.ts +97 -0
  130. package/src/utils/ewma.ts +43 -0
  131. package/src/utils/fetch-loader.ts +331 -0
  132. package/src/utils/global.ts +2 -0
  133. package/src/utils/hash.ts +10 -0
  134. package/src/utils/hdr.ts +67 -0
  135. package/src/utils/hex.ts +32 -0
  136. package/src/utils/imsc1-ttml-parser.ts +261 -0
  137. package/src/utils/keysystem-util.ts +45 -0
  138. package/src/utils/level-helper.ts +629 -0
  139. package/src/utils/logger.ts +120 -0
  140. package/src/utils/media-option-attributes.ts +49 -0
  141. package/src/utils/mediacapabilities-helper.ts +301 -0
  142. package/src/utils/mediakeys-helper.ts +210 -0
  143. package/src/utils/mediasource-helper.ts +37 -0
  144. package/src/utils/mp4-tools.ts +1473 -0
  145. package/src/utils/number.ts +3 -0
  146. package/src/utils/numeric-encoding-utils.ts +26 -0
  147. package/src/utils/output-filter.ts +46 -0
  148. package/src/utils/rendition-helper.ts +505 -0
  149. package/src/utils/safe-json-stringify.ts +22 -0
  150. package/src/utils/texttrack-utils.ts +164 -0
  151. package/src/utils/time-ranges.ts +17 -0
  152. package/src/utils/timescale-conversion.ts +46 -0
  153. package/src/utils/utf8-utils.ts +18 -0
  154. package/src/utils/variable-substitution.ts +105 -0
  155. package/src/utils/vttcue.ts +384 -0
  156. package/src/utils/vttparser.ts +497 -0
  157. package/src/utils/webvtt-parser.ts +166 -0
  158. package/src/utils/xhr-loader.ts +337 -0
  159. package/src/version.ts +1 -0
@@ -0,0 +1,2895 @@
1
+ import { createDoNothingErrorAction } from './error-controller';
2
+ import { HlsAssetPlayer } from './interstitial-player';
3
+ import {
4
+ type InterstitialScheduleEventItem,
5
+ type InterstitialScheduleItem,
6
+ type InterstitialSchedulePrimaryItem,
7
+ InterstitialsSchedule,
8
+ segmentToString,
9
+ type TimelineType,
10
+ } from './interstitials-schedule';
11
+ import { ErrorDetails, ErrorTypes } from '../errors';
12
+ import { Events } from '../events';
13
+ import { AssetListLoader } from '../loader/interstitial-asset-list';
14
+ import {
15
+ ALIGNED_END_THRESHOLD_SECONDS,
16
+ eventAssetToString,
17
+ generateAssetIdentifier,
18
+ getNextAssetIndex,
19
+ type InterstitialAssetId,
20
+ type InterstitialAssetItem,
21
+ type InterstitialEvent,
22
+ type InterstitialEventWithAssetList,
23
+ TimelineOccupancy,
24
+ } from '../loader/interstitial-event';
25
+ import { BufferHelper } from '../utils/buffer-helper';
26
+ import {
27
+ addEventListener,
28
+ removeEventListener,
29
+ } from '../utils/event-listener-helper';
30
+ import { hash } from '../utils/hash';
31
+ import { Logger } from '../utils/logger';
32
+ import { isCompatibleTrackChange } from '../utils/mediasource-helper';
33
+ import { getBasicSelectionOption } from '../utils/rendition-helper';
34
+ import { stringify } from '../utils/safe-json-stringify';
35
+ import type {
36
+ HlsAssetPlayerConfig,
37
+ InterstitialPlayer,
38
+ } from './interstitial-player';
39
+ import type Hls from '../hls';
40
+ import type { LevelDetails } from '../loader/level-details';
41
+ import type { SourceBufferName } from '../types/buffer';
42
+ import type { NetworkComponentAPI } from '../types/component-api';
43
+ import type {
44
+ AssetListLoadedData,
45
+ AudioTrackSwitchingData,
46
+ AudioTrackUpdatedData,
47
+ BufferAppendedData,
48
+ BufferCodecsData,
49
+ BufferFlushedData,
50
+ ErrorData,
51
+ LevelUpdatedData,
52
+ MediaAttachedData,
53
+ MediaAttachingData,
54
+ MediaDetachingData,
55
+ SubtitleTrackSwitchData,
56
+ SubtitleTrackUpdatedData,
57
+ } from '../types/events';
58
+ import type { MediaPlaylist, MediaSelection } from '../types/media-playlist';
59
+
60
+ export interface InterstitialsManager {
61
+ events: InterstitialEvent[];
62
+ schedule: InterstitialScheduleItem[];
63
+ interstitialPlayer: InterstitialPlayer | null;
64
+ playerQueue: HlsAssetPlayer[];
65
+ bufferingAsset: InterstitialAssetItem | null;
66
+ bufferingItem: InterstitialScheduleItem | null;
67
+ bufferingIndex: number;
68
+ playingAsset: InterstitialAssetItem | null;
69
+ playingItem: InterstitialScheduleItem | null;
70
+ playingIndex: number;
71
+ primary: PlayheadTimes;
72
+ integrated: PlayheadTimes;
73
+ skip: () => void;
74
+ }
75
+
76
+ export type PlayheadTimes = {
77
+ bufferedEnd: number;
78
+ currentTime: number;
79
+ duration: number;
80
+ seekableStart: number;
81
+ };
82
+
83
+ function playWithCatch(media: HTMLMediaElement | null) {
84
+ (media?.play() as Promise<void> | undefined)?.catch(() => {
85
+ /* no-op */
86
+ });
87
+ }
88
+
89
+ function timelineMessage(label: string, time: number) {
90
+ return `[${label}] Advancing timeline position to ${time}`;
91
+ }
92
+
93
+ export default class InterstitialsController
94
+ extends Logger
95
+ implements NetworkComponentAPI
96
+ {
97
+ private readonly HlsPlayerClass: typeof Hls;
98
+ private readonly hls: Hls;
99
+ private readonly assetListLoader: AssetListLoader;
100
+
101
+ // Last updated LevelDetails
102
+ private mediaSelection: MediaSelection | null = null;
103
+ private altSelection: {
104
+ audio?: MediaPlaylist;
105
+ subtitles?: MediaPlaylist;
106
+ } | null = null;
107
+
108
+ // Media and MediaSource/SourceBuffers
109
+ private media: HTMLMediaElement | null = null;
110
+ private detachedData: MediaAttachingData | null = null;
111
+ private requiredTracks: Partial<BufferCodecsData> | null = null;
112
+
113
+ // Public Interface for Interstitial playback state and control
114
+ private manager: InterstitialsManager | null = null;
115
+
116
+ // Interstitial Asset Players
117
+ private playerQueue: HlsAssetPlayer[] = [];
118
+
119
+ // Timeline position tracking
120
+ private bufferedPos: number = -1;
121
+ private timelinePos: number = -1;
122
+
123
+ // Schedule
124
+ private schedule: InterstitialsSchedule | null;
125
+
126
+ // Schedule playback and buffering state
127
+ private playingItem: InterstitialScheduleItem | null = null;
128
+ private bufferingItem: InterstitialScheduleItem | null = null;
129
+ private waitingItem: InterstitialScheduleEventItem | null = null;
130
+ private endedItem: InterstitialScheduleItem | null = null;
131
+ private playingAsset: InterstitialAssetItem | null = null;
132
+ private endedAsset: InterstitialAssetItem | null = null;
133
+ private bufferingAsset: InterstitialAssetItem | null = null;
134
+ private shouldPlay: boolean = false;
135
+
136
+ constructor(hls: Hls, HlsPlayerClass: typeof Hls) {
137
+ super('interstitials', hls.logger);
138
+ this.hls = hls;
139
+ this.HlsPlayerClass = HlsPlayerClass;
140
+ this.assetListLoader = new AssetListLoader(hls);
141
+ this.schedule = new InterstitialsSchedule(
142
+ this.onScheduleUpdate,
143
+ hls.logger,
144
+ );
145
+ this.registerListeners();
146
+ }
147
+
148
+ private registerListeners() {
149
+ const hls = this.hls;
150
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
151
+ if (hls) {
152
+ hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
153
+ hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
154
+ hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
155
+ hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
156
+ hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
157
+ hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
158
+ hls.on(Events.AUDIO_TRACK_UPDATED, this.onAudioTrackUpdated, this);
159
+ hls.on(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this);
160
+ hls.on(Events.SUBTITLE_TRACK_UPDATED, this.onSubtitleTrackUpdated, this);
161
+ hls.on(Events.EVENT_CUE_ENTER, this.onInterstitialCueEnter, this);
162
+ hls.on(Events.ASSET_LIST_LOADED, this.onAssetListLoaded, this);
163
+ hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this);
164
+ hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this);
165
+ hls.on(Events.BUFFERED_TO_END, this.onBufferedToEnd, this);
166
+ hls.on(Events.MEDIA_ENDED, this.onMediaEnded, this);
167
+ hls.on(Events.ERROR, this.onError, this);
168
+ hls.on(Events.DESTROYING, this.onDestroying, this);
169
+ }
170
+ }
171
+
172
+ private unregisterListeners() {
173
+ const hls = this.hls;
174
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
175
+ if (hls) {
176
+ hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
177
+ hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
178
+ hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
179
+ hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
180
+ hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
181
+ hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
182
+ hls.off(Events.AUDIO_TRACK_UPDATED, this.onAudioTrackUpdated, this);
183
+ hls.off(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this);
184
+ hls.off(Events.SUBTITLE_TRACK_UPDATED, this.onSubtitleTrackUpdated, this);
185
+ hls.off(Events.EVENT_CUE_ENTER, this.onInterstitialCueEnter, this);
186
+ hls.off(Events.ASSET_LIST_LOADED, this.onAssetListLoaded, this);
187
+ hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this);
188
+ hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this);
189
+ hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this);
190
+ hls.off(Events.BUFFERED_TO_END, this.onBufferedToEnd, this);
191
+ hls.off(Events.MEDIA_ENDED, this.onMediaEnded, this);
192
+ hls.off(Events.ERROR, this.onError, this);
193
+ hls.off(Events.DESTROYING, this.onDestroying, this);
194
+ }
195
+ }
196
+
197
+ startLoad() {
198
+ // TODO: startLoad - check for waitingItem and retry by resetting schedule
199
+ this.resumeBuffering();
200
+ }
201
+
202
+ stopLoad() {
203
+ // TODO: stopLoad - stop all scheule.events[].assetListLoader?.abort() then delete the loaders
204
+ this.pauseBuffering();
205
+ }
206
+
207
+ resumeBuffering() {
208
+ this.getBufferingPlayer()?.resumeBuffering();
209
+ }
210
+
211
+ pauseBuffering() {
212
+ this.getBufferingPlayer()?.pauseBuffering();
213
+ }
214
+
215
+ destroy() {
216
+ this.unregisterListeners();
217
+ this.stopLoad();
218
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
219
+ if (this.assetListLoader) {
220
+ this.assetListLoader.destroy();
221
+ }
222
+ this.emptyPlayerQueue();
223
+ this.clearScheduleState();
224
+ if (this.schedule) {
225
+ this.schedule.destroy();
226
+ }
227
+ this.media =
228
+ this.detachedData =
229
+ this.mediaSelection =
230
+ this.requiredTracks =
231
+ this.altSelection =
232
+ this.schedule =
233
+ this.manager =
234
+ null;
235
+ // @ts-ignore
236
+ this.hls = this.HlsPlayerClass = this.log = null;
237
+ // @ts-ignore
238
+ this.assetListLoader = null;
239
+ // @ts-ignore
240
+ this.onPlay = this.onPause = this.onSeeking = this.onTimeupdate = null;
241
+ // @ts-ignore
242
+ this.onScheduleUpdate = null;
243
+ }
244
+
245
+ private onDestroying() {
246
+ const media = this.primaryMedia || this.media;
247
+ if (media) {
248
+ this.removeMediaListeners(media);
249
+ }
250
+ }
251
+
252
+ private removeMediaListeners(media: HTMLMediaElement) {
253
+ removeEventListener(media, 'play', this.onPlay);
254
+ removeEventListener(media, 'pause', this.onPause);
255
+ removeEventListener(media, 'seeking', this.onSeeking);
256
+ removeEventListener(media, 'timeupdate', this.onTimeupdate);
257
+ }
258
+
259
+ private onMediaAttaching(
260
+ event: Events.MEDIA_ATTACHING,
261
+ data: MediaAttachingData,
262
+ ) {
263
+ const media = (this.media = data.media);
264
+ addEventListener(media, 'seeking', this.onSeeking);
265
+ addEventListener(media, 'timeupdate', this.onTimeupdate);
266
+ addEventListener(media, 'play', this.onPlay);
267
+ addEventListener(media, 'pause', this.onPause);
268
+ }
269
+
270
+ private onMediaAttached(
271
+ event: Events.MEDIA_ATTACHED,
272
+ data: MediaAttachedData,
273
+ ) {
274
+ const playingItem = this.effectivePlayingItem;
275
+ const detachedMedia = this.detachedData;
276
+ this.detachedData = null;
277
+ if (playingItem === null) {
278
+ this.checkStart();
279
+ } else if (!detachedMedia) {
280
+ // Resume schedule after detached externally
281
+ this.clearScheduleState();
282
+ const playingIndex = this.findItemIndex(playingItem);
283
+ this.setSchedulePosition(playingIndex);
284
+ }
285
+ }
286
+
287
+ private clearScheduleState() {
288
+ this.log(`clear schedule state`);
289
+ this.playingItem =
290
+ this.bufferingItem =
291
+ this.waitingItem =
292
+ this.endedItem =
293
+ this.playingAsset =
294
+ this.endedAsset =
295
+ this.bufferingAsset =
296
+ null;
297
+ }
298
+
299
+ private onMediaDetaching(
300
+ event: Events.MEDIA_DETACHING,
301
+ data: MediaDetachingData,
302
+ ) {
303
+ const transferringMedia = !!data.transferMedia;
304
+ const media = this.media;
305
+ this.media = null;
306
+ if (transferringMedia) {
307
+ return;
308
+ }
309
+ if (media) {
310
+ this.removeMediaListeners(media);
311
+ }
312
+ // If detachMedia is called while in an Interstitial, detach the asset player as well and reset the schedule position
313
+ if (this.detachedData) {
314
+ const player = this.getBufferingPlayer();
315
+ if (player) {
316
+ this.log(`Removing schedule state for detachedData and ${player}`);
317
+ this.playingAsset =
318
+ this.endedAsset =
319
+ this.bufferingAsset =
320
+ this.bufferingItem =
321
+ this.waitingItem =
322
+ this.detachedData =
323
+ null;
324
+ player.detachMedia();
325
+ }
326
+ this.shouldPlay = false;
327
+ }
328
+ }
329
+
330
+ public get interstitialsManager(): InterstitialsManager | null {
331
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
332
+ if (!this.hls) {
333
+ return null;
334
+ }
335
+ if (this.manager) {
336
+ return this.manager;
337
+ }
338
+ const c = this;
339
+ const effectiveBufferingItem = () => c.bufferingItem || c.waitingItem;
340
+ const getAssetPlayer = (asset: InterstitialAssetItem | null) =>
341
+ asset ? c.getAssetPlayer(asset.identifier) : asset;
342
+ const getMappedTime = (
343
+ item: InterstitialScheduleItem | null,
344
+ timelineType: TimelineType,
345
+ asset: InterstitialAssetItem | null,
346
+ controllerField: 'bufferedPos' | 'timelinePos',
347
+ assetPlayerField: 'bufferedEnd' | 'currentTime',
348
+ ): number => {
349
+ if (item) {
350
+ let time = (
351
+ item[timelineType] as {
352
+ start: number;
353
+ end: number;
354
+ }
355
+ ).start;
356
+ const interstitial = item.event;
357
+ if (interstitial) {
358
+ if (
359
+ timelineType === 'playout' ||
360
+ interstitial.timelineOccupancy !== TimelineOccupancy.Point
361
+ ) {
362
+ const assetPlayer = getAssetPlayer(asset);
363
+ if (assetPlayer?.interstitial === interstitial) {
364
+ time +=
365
+ assetPlayer.assetItem.startOffset +
366
+ assetPlayer[assetPlayerField];
367
+ }
368
+ }
369
+ } else {
370
+ const value =
371
+ controllerField === 'bufferedPos'
372
+ ? getBufferedEnd()
373
+ : c[controllerField];
374
+ time += value - item.start;
375
+ }
376
+ return time;
377
+ }
378
+ return 0;
379
+ };
380
+ const findMappedTime = (
381
+ primaryTime: number,
382
+ timelineType: TimelineType,
383
+ ): number => {
384
+ if (
385
+ primaryTime !== 0 &&
386
+ timelineType !== 'primary' &&
387
+ c.schedule?.length
388
+ ) {
389
+ const index = c.schedule.findItemIndexAtTime(primaryTime);
390
+ const item = c.schedule.items?.[index];
391
+ if (item) {
392
+ const diff = item[timelineType].start - item.start;
393
+ return primaryTime + diff;
394
+ }
395
+ }
396
+ return primaryTime;
397
+ };
398
+ const getBufferedEnd = (): number => {
399
+ const value = c.bufferedPos;
400
+ if (value === Number.MAX_VALUE) {
401
+ return getMappedDuration('primary');
402
+ }
403
+ return Math.max(value, 0);
404
+ };
405
+ const getMappedDuration = (timelineType: TimelineType): number => {
406
+ if (c.primaryDetails?.live) {
407
+ // return end of last event item or playlist
408
+ return c.primaryDetails.edge;
409
+ }
410
+ return c.schedule?.durations[timelineType] || 0;
411
+ };
412
+ const seekTo = (time: number, timelineType: TimelineType) => {
413
+ const item = c.effectivePlayingItem;
414
+ if (item?.event?.restrictions.skip || !c.schedule) {
415
+ return;
416
+ }
417
+ c.log(`seek to ${time} "${timelineType}"`);
418
+ const playingItem = c.effectivePlayingItem;
419
+ const targetIndex = c.schedule.findItemIndexAtTime(time, timelineType);
420
+ const targetItem = c.schedule.items?.[targetIndex];
421
+ const bufferingPlayer = c.getBufferingPlayer();
422
+ const bufferingInterstitial = bufferingPlayer?.interstitial;
423
+ const appendInPlace = bufferingInterstitial?.appendInPlace;
424
+ const seekInItem = playingItem && c.itemsMatch(playingItem, targetItem);
425
+ if (playingItem && (appendInPlace || seekInItem)) {
426
+ // seek in asset player or primary media (appendInPlace)
427
+ const assetPlayer = getAssetPlayer(c.playingAsset);
428
+ const media = assetPlayer?.media || c.primaryMedia;
429
+ if (media) {
430
+ const currentTime =
431
+ timelineType === 'primary'
432
+ ? media.currentTime
433
+ : getMappedTime(
434
+ playingItem,
435
+ timelineType,
436
+ c.playingAsset,
437
+ 'timelinePos',
438
+ 'currentTime',
439
+ );
440
+
441
+ const diff = time - currentTime;
442
+ const seekToTime =
443
+ (appendInPlace ? currentTime : media.currentTime) + diff;
444
+ if (
445
+ seekToTime >= 0 &&
446
+ (!assetPlayer ||
447
+ appendInPlace ||
448
+ seekToTime <= assetPlayer.duration)
449
+ ) {
450
+ media.currentTime = seekToTime;
451
+ return;
452
+ }
453
+ }
454
+ }
455
+ // seek out of item or asset
456
+ if (targetItem) {
457
+ let seekToTime = time;
458
+ if (timelineType !== 'primary') {
459
+ const primarySegmentStart = targetItem[timelineType].start;
460
+ const diff = time - primarySegmentStart;
461
+ seekToTime = targetItem.start + diff;
462
+ }
463
+ const targetIsPrimary = !c.isInterstitial(targetItem);
464
+ if (
465
+ (!c.isInterstitial(playingItem) || playingItem.event.appendInPlace) &&
466
+ (targetIsPrimary || targetItem.event.appendInPlace)
467
+ ) {
468
+ const media =
469
+ c.media || (appendInPlace ? bufferingPlayer?.media : null);
470
+ if (media) {
471
+ media.currentTime = seekToTime;
472
+ }
473
+ } else if (playingItem) {
474
+ // check if an Interstitial between the current item and target item has an X-RESTRICT JUMP restriction
475
+ const playingIndex = c.findItemIndex(playingItem);
476
+ if (targetIndex > playingIndex) {
477
+ const jumpIndex = c.schedule.findJumpRestrictedIndex(
478
+ playingIndex + 1,
479
+ targetIndex,
480
+ );
481
+ if (jumpIndex > playingIndex) {
482
+ c.setSchedulePosition(jumpIndex);
483
+ return;
484
+ }
485
+ }
486
+
487
+ let assetIndex = 0;
488
+ if (targetIsPrimary) {
489
+ c.timelinePos = seekToTime;
490
+ c.checkBuffer();
491
+ } else {
492
+ const assetList = targetItem.event.assetList;
493
+ const eventTime =
494
+ time - (targetItem[timelineType] || targetItem).start;
495
+ for (let i = assetList.length; i--; ) {
496
+ const asset = assetList[i];
497
+ if (
498
+ asset.duration &&
499
+ eventTime >= asset.startOffset &&
500
+ eventTime < asset.startOffset + asset.duration
501
+ ) {
502
+ assetIndex = i;
503
+ break;
504
+ }
505
+ }
506
+ }
507
+ c.setSchedulePosition(targetIndex, assetIndex);
508
+ }
509
+ }
510
+ };
511
+ const getActiveInterstitial = () => {
512
+ const playingItem = c.effectivePlayingItem;
513
+ if (c.isInterstitial(playingItem)) {
514
+ return playingItem;
515
+ }
516
+ const bufferingItem = effectiveBufferingItem();
517
+ if (c.isInterstitial(bufferingItem)) {
518
+ return bufferingItem;
519
+ }
520
+ return null;
521
+ };
522
+ const interstitialPlayer: InterstitialPlayer = {
523
+ get bufferedEnd() {
524
+ const interstitialItem = effectiveBufferingItem();
525
+ const bufferingItem = c.bufferingItem;
526
+ if (bufferingItem && bufferingItem === interstitialItem) {
527
+ return (
528
+ getMappedTime(
529
+ bufferingItem,
530
+ 'playout',
531
+ c.bufferingAsset,
532
+ 'bufferedPos',
533
+ 'bufferedEnd',
534
+ ) - bufferingItem.playout.start ||
535
+ c.bufferingAsset?.startOffset ||
536
+ 0
537
+ );
538
+ }
539
+ return 0;
540
+ },
541
+ get currentTime() {
542
+ const interstitialItem = getActiveInterstitial();
543
+ const playingItem = c.effectivePlayingItem;
544
+ if (playingItem && playingItem === interstitialItem) {
545
+ return (
546
+ getMappedTime(
547
+ playingItem,
548
+ 'playout',
549
+ c.effectivePlayingAsset,
550
+ 'timelinePos',
551
+ 'currentTime',
552
+ ) - playingItem.playout.start
553
+ );
554
+ }
555
+ return 0;
556
+ },
557
+ set currentTime(time: number) {
558
+ const interstitialItem = getActiveInterstitial();
559
+ const playingItem = c.effectivePlayingItem;
560
+ if (playingItem && playingItem === interstitialItem) {
561
+ seekTo(time + playingItem.playout.start, 'playout');
562
+ }
563
+ },
564
+ get duration() {
565
+ const interstitialItem = getActiveInterstitial();
566
+ if (interstitialItem) {
567
+ return interstitialItem.playout.end - interstitialItem.playout.start;
568
+ }
569
+ return 0;
570
+ },
571
+ get assetPlayers() {
572
+ const assetList = getActiveInterstitial()?.event.assetList;
573
+ if (assetList) {
574
+ return assetList.map((asset) => c.getAssetPlayer(asset.identifier));
575
+ }
576
+ return [];
577
+ },
578
+ get playingIndex() {
579
+ const interstitial = getActiveInterstitial()?.event;
580
+ if (interstitial && c.effectivePlayingAsset) {
581
+ return interstitial.findAssetIndex(c.effectivePlayingAsset);
582
+ }
583
+ return -1;
584
+ },
585
+ get scheduleItem() {
586
+ return getActiveInterstitial();
587
+ },
588
+ };
589
+ return (this.manager = {
590
+ get events() {
591
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
592
+ return c.schedule?.events?.slice(0) || [];
593
+ },
594
+ get schedule() {
595
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
596
+ return c.schedule?.items?.slice(0) || [];
597
+ },
598
+ get interstitialPlayer() {
599
+ if (getActiveInterstitial()) {
600
+ return interstitialPlayer;
601
+ }
602
+ return null;
603
+ },
604
+ get playerQueue() {
605
+ return c.playerQueue.slice(0);
606
+ },
607
+ get bufferingAsset() {
608
+ return c.bufferingAsset;
609
+ },
610
+ get bufferingItem() {
611
+ return effectiveBufferingItem();
612
+ },
613
+ get bufferingIndex() {
614
+ const item = effectiveBufferingItem();
615
+ return c.findItemIndex(item);
616
+ },
617
+ get playingAsset() {
618
+ return c.effectivePlayingAsset;
619
+ },
620
+ get playingItem() {
621
+ return c.effectivePlayingItem;
622
+ },
623
+ get playingIndex() {
624
+ const item = c.effectivePlayingItem;
625
+ return c.findItemIndex(item);
626
+ },
627
+ primary: {
628
+ get bufferedEnd() {
629
+ return getBufferedEnd();
630
+ },
631
+ get currentTime() {
632
+ const timelinePos = c.timelinePos;
633
+ return timelinePos > 0 ? timelinePos : 0;
634
+ },
635
+ set currentTime(time: number) {
636
+ seekTo(time, 'primary');
637
+ },
638
+ get duration() {
639
+ return getMappedDuration('primary');
640
+ },
641
+ get seekableStart() {
642
+ return c.primaryDetails?.fragmentStart || 0;
643
+ },
644
+ },
645
+ integrated: {
646
+ get bufferedEnd() {
647
+ return getMappedTime(
648
+ effectiveBufferingItem(),
649
+ 'integrated',
650
+ c.bufferingAsset,
651
+ 'bufferedPos',
652
+ 'bufferedEnd',
653
+ );
654
+ },
655
+ get currentTime() {
656
+ return getMappedTime(
657
+ c.effectivePlayingItem,
658
+ 'integrated',
659
+ c.effectivePlayingAsset,
660
+ 'timelinePos',
661
+ 'currentTime',
662
+ );
663
+ },
664
+ set currentTime(time: number) {
665
+ seekTo(time, 'integrated');
666
+ },
667
+ get duration() {
668
+ return getMappedDuration('integrated');
669
+ },
670
+ get seekableStart() {
671
+ return findMappedTime(
672
+ c.primaryDetails?.fragmentStart || 0,
673
+ 'integrated',
674
+ );
675
+ },
676
+ },
677
+ skip: () => {
678
+ const item = c.effectivePlayingItem;
679
+ const event = item?.event;
680
+ if (event && !event.restrictions.skip) {
681
+ const index = c.findItemIndex(item);
682
+ if (event.appendInPlace) {
683
+ const time = item.playout.start + item.event.duration;
684
+ seekTo(time + 0.001, 'playout');
685
+ } else {
686
+ c.advanceAfterAssetEnded(event, index, Infinity);
687
+ }
688
+ }
689
+ },
690
+ });
691
+ }
692
+
693
+ // Schedule getters
694
+ private get effectivePlayingItem(): InterstitialScheduleItem | null {
695
+ return this.waitingItem || this.playingItem || this.endedItem;
696
+ }
697
+
698
+ private get effectivePlayingAsset(): InterstitialAssetItem | null {
699
+ return this.playingAsset || this.endedAsset;
700
+ }
701
+
702
+ private get playingLastItem(): boolean {
703
+ const playingItem = this.playingItem;
704
+ const items = this.schedule?.items;
705
+ if (!this.playbackStarted || !playingItem || !items) {
706
+ return false;
707
+ }
708
+
709
+ return this.findItemIndex(playingItem) === items.length - 1;
710
+ }
711
+
712
+ private get playbackStarted(): boolean {
713
+ return this.effectivePlayingItem !== null;
714
+ }
715
+
716
+ // Media getters and event callbacks
717
+ private get currentTime(): number | undefined {
718
+ if (this.mediaSelection === null) {
719
+ // Do not advance before schedule is known
720
+ return undefined;
721
+ }
722
+ // Ignore currentTime when detached for Interstitial playback with source reset
723
+ const queuedForPlayback = this.waitingItem || this.playingItem;
724
+ if (
725
+ this.isInterstitial(queuedForPlayback) &&
726
+ !queuedForPlayback.event.appendInPlace
727
+ ) {
728
+ return undefined;
729
+ }
730
+ let media = this.media;
731
+ if (!media && this.bufferingItem?.event?.appendInPlace) {
732
+ // Observe detached media currentTime when appending in place
733
+ media = this.primaryMedia;
734
+ }
735
+ const currentTime = media?.currentTime;
736
+ if (currentTime === undefined || !Number.isFinite(currentTime)) {
737
+ return undefined;
738
+ }
739
+ return currentTime;
740
+ }
741
+
742
+ private get primaryMedia(): HTMLMediaElement | null {
743
+ return this.media || this.detachedData?.media || null;
744
+ }
745
+
746
+ private isInterstitial(
747
+ item: InterstitialScheduleItem | null | undefined,
748
+ ): item is InterstitialScheduleEventItem {
749
+ return !!item?.event;
750
+ }
751
+
752
+ private retreiveMediaSource(
753
+ assetId: InterstitialAssetId,
754
+ toSegment: InterstitialScheduleItem | null,
755
+ ) {
756
+ const player = this.getAssetPlayer(assetId);
757
+ if (player) {
758
+ this.transferMediaFromPlayer(player, toSegment);
759
+ }
760
+ }
761
+
762
+ private transferMediaFromPlayer(
763
+ player: HlsAssetPlayer,
764
+ toSegment: InterstitialScheduleItem | null | undefined,
765
+ ) {
766
+ const appendInPlace = player.interstitial.appendInPlace;
767
+ const playerMedia = player.media;
768
+ if (appendInPlace && playerMedia === this.primaryMedia) {
769
+ this.bufferingAsset = null;
770
+ if (
771
+ !toSegment ||
772
+ (this.isInterstitial(toSegment) && !toSegment.event.appendInPlace)
773
+ ) {
774
+ // MediaSource cannot be transfered back to an Interstitial that requires a source reset
775
+ // no-op when toSegment is undefined
776
+ if (toSegment && playerMedia) {
777
+ this.detachedData = { media: playerMedia };
778
+ return;
779
+ }
780
+ }
781
+ const attachMediaSourceData = player.transferMedia();
782
+ this.log(
783
+ `transfer MediaSource from ${player} ${stringify(attachMediaSourceData)}`,
784
+ );
785
+ this.detachedData = attachMediaSourceData;
786
+ } else if (toSegment && playerMedia) {
787
+ this.shouldPlay ||= !playerMedia.paused;
788
+ }
789
+ }
790
+
791
+ private transferMediaTo(
792
+ player: Hls | HlsAssetPlayer,
793
+ media: HTMLMediaElement,
794
+ ) {
795
+ if (player.media === media) {
796
+ return;
797
+ }
798
+ let attachMediaSourceData: MediaAttachingData | null = null;
799
+ const primaryPlayer = this.hls;
800
+ const isAssetPlayer = player !== primaryPlayer;
801
+ const appendInPlace =
802
+ isAssetPlayer && (player as HlsAssetPlayer).interstitial.appendInPlace;
803
+ const detachedMediaSource = this.detachedData?.mediaSource;
804
+
805
+ let logFromSource: string;
806
+ if (primaryPlayer.media) {
807
+ if (appendInPlace) {
808
+ attachMediaSourceData = primaryPlayer.transferMedia();
809
+ this.detachedData = attachMediaSourceData;
810
+ }
811
+ logFromSource = `Primary`;
812
+ } else if (detachedMediaSource) {
813
+ const bufferingPlayer = this.getBufferingPlayer();
814
+ if (bufferingPlayer) {
815
+ attachMediaSourceData = bufferingPlayer.transferMedia();
816
+ logFromSource = `${bufferingPlayer}`;
817
+ } else {
818
+ logFromSource = `detached MediaSource`;
819
+ }
820
+ } else {
821
+ logFromSource = `detached media`;
822
+ }
823
+ if (!attachMediaSourceData) {
824
+ if (detachedMediaSource) {
825
+ attachMediaSourceData = this.detachedData;
826
+ this.log(
827
+ `using detachedData: MediaSource ${stringify(attachMediaSourceData)}`,
828
+ );
829
+ } else if (!this.detachedData || primaryPlayer.media === media) {
830
+ // Keep interstitial media transition consistent
831
+ const playerQueue = this.playerQueue;
832
+ if (playerQueue.length > 1) {
833
+ playerQueue.forEach((queuedPlayer) => {
834
+ if (
835
+ isAssetPlayer &&
836
+ queuedPlayer.interstitial.appendInPlace !== appendInPlace
837
+ ) {
838
+ const interstitial = queuedPlayer.interstitial;
839
+ this.clearInterstitial(queuedPlayer.interstitial, null);
840
+ interstitial.appendInPlace = false; // setter may be a no-op;
841
+ // `appendInPlace` getter may still return `true` after insterstitial streaming has begun in that mode.
842
+ if (interstitial.appendInPlace as boolean) {
843
+ this.warn(
844
+ `Could not change append strategy for queued assets ${interstitial}`,
845
+ );
846
+ }
847
+ }
848
+ });
849
+ }
850
+ this.hls.detachMedia();
851
+ this.detachedData = { media };
852
+ }
853
+ }
854
+ const transferring =
855
+ attachMediaSourceData &&
856
+ 'mediaSource' in attachMediaSourceData &&
857
+ attachMediaSourceData.mediaSource?.readyState !== 'closed';
858
+ const dataToAttach =
859
+ transferring && attachMediaSourceData ? attachMediaSourceData : media;
860
+ this.log(
861
+ `${transferring ? 'transfering MediaSource' : 'attaching media'} to ${
862
+ isAssetPlayer ? player : 'Primary'
863
+ } from ${logFromSource} (media.currentTime: ${media.currentTime})`,
864
+ );
865
+ const schedule = this.schedule;
866
+ if (dataToAttach === attachMediaSourceData && schedule) {
867
+ const isAssetAtEndOfSchedule =
868
+ isAssetPlayer &&
869
+ (player as HlsAssetPlayer).assetId === schedule.assetIdAtEnd;
870
+ // Prevent asset players from marking EoS on transferred MediaSource
871
+ dataToAttach.overrides = {
872
+ duration: schedule.duration,
873
+ endOfStream: !isAssetPlayer || isAssetAtEndOfSchedule,
874
+ };
875
+ }
876
+ player.attachMedia(dataToAttach);
877
+ }
878
+
879
+ private onPlay = () => {
880
+ this.shouldPlay = true;
881
+ };
882
+
883
+ private onPause = () => {
884
+ this.shouldPlay = false;
885
+ };
886
+
887
+ private onSeeking = () => {
888
+ const currentTime = this.currentTime;
889
+ if (currentTime === undefined || this.playbackDisabled || !this.schedule) {
890
+ return;
891
+ }
892
+ const diff = currentTime - this.timelinePos;
893
+ const roundingError = Math.abs(diff) < 1 / 705600000; // one flick
894
+ if (roundingError) {
895
+ return;
896
+ }
897
+ const backwardSeek = diff <= -0.01;
898
+ this.timelinePos = currentTime;
899
+ this.bufferedPos = currentTime;
900
+
901
+ // Check if seeking out of an item
902
+ const playingItem = this.playingItem;
903
+ if (!playingItem) {
904
+ this.checkBuffer();
905
+ return;
906
+ }
907
+ if (backwardSeek) {
908
+ const resetCount = this.schedule.resetErrorsInRange(
909
+ currentTime,
910
+ currentTime - diff,
911
+ );
912
+ if (resetCount) {
913
+ this.updateSchedule(true);
914
+ }
915
+ }
916
+ this.checkBuffer();
917
+ if (
918
+ (backwardSeek && currentTime < playingItem.start) ||
919
+ currentTime >= playingItem.end
920
+ ) {
921
+ const playingIndex = this.findItemIndex(playingItem);
922
+ let scheduleIndex = this.schedule.findItemIndexAtTime(currentTime);
923
+ if (scheduleIndex === -1) {
924
+ scheduleIndex = playingIndex + (backwardSeek ? -1 : 1);
925
+ this.log(
926
+ `seeked ${backwardSeek ? 'back ' : ''}to position not covered by schedule ${currentTime} (resolving from ${playingIndex} to ${scheduleIndex})`,
927
+ );
928
+ }
929
+ if (!this.isInterstitial(playingItem) && this.media?.paused) {
930
+ this.shouldPlay = false;
931
+ }
932
+ if (!backwardSeek) {
933
+ // check if an Interstitial between the current item and target item has an X-RESTRICT JUMP restriction
934
+ if (scheduleIndex > playingIndex) {
935
+ const jumpIndex = this.schedule.findJumpRestrictedIndex(
936
+ playingIndex + 1,
937
+ scheduleIndex,
938
+ );
939
+ if (jumpIndex > playingIndex) {
940
+ this.setSchedulePosition(jumpIndex);
941
+ return;
942
+ }
943
+ }
944
+ }
945
+ this.setSchedulePosition(scheduleIndex);
946
+ return;
947
+ }
948
+ // Check if seeking out of an asset (assumes same item following above check)
949
+ const playingAsset = this.playingAsset;
950
+ if (!playingAsset) {
951
+ // restart Interstitial at end
952
+ if (this.playingLastItem && this.isInterstitial(playingItem)) {
953
+ const restartAsset = playingItem.event.assetList[0];
954
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
955
+ if (restartAsset) {
956
+ this.endedItem = this.playingItem;
957
+ this.playingItem = null;
958
+ this.setScheduleToAssetAtTime(currentTime, restartAsset);
959
+ }
960
+ }
961
+ return;
962
+ }
963
+ const start = playingAsset.timelineStart;
964
+ const duration = playingAsset.duration || 0;
965
+ if (
966
+ (backwardSeek && currentTime < start) ||
967
+ currentTime >= start + duration
968
+ ) {
969
+ if (playingItem.event?.appendInPlace) {
970
+ // Return SourceBuffer(s) to primary player and flush
971
+ this.clearAssetPlayers(playingItem.event, playingItem);
972
+ this.flushFrontBuffer(currentTime);
973
+ }
974
+ this.setScheduleToAssetAtTime(currentTime, playingAsset);
975
+ }
976
+ };
977
+
978
+ private onInterstitialCueEnter() {
979
+ this.onTimeupdate();
980
+ }
981
+
982
+ private onTimeupdate = () => {
983
+ const currentTime = this.currentTime;
984
+ if (currentTime === undefined || this.playbackDisabled) {
985
+ return;
986
+ }
987
+
988
+ // Only allow timeupdate to advance primary position, seeking is used for jumping back
989
+ // this prevents primaryPos from being reset to 0 after re-attach
990
+ if (currentTime > this.timelinePos) {
991
+ this.timelinePos = currentTime;
992
+ if (currentTime > this.bufferedPos) {
993
+ this.checkBuffer();
994
+ }
995
+ } else {
996
+ return;
997
+ }
998
+
999
+ // Check if playback has entered the next item
1000
+ const playingItem = this.playingItem;
1001
+ if (!playingItem || this.playingLastItem) {
1002
+ return;
1003
+ }
1004
+ if (currentTime >= playingItem.end) {
1005
+ this.timelinePos = playingItem.end;
1006
+ const playingIndex = this.findItemIndex(playingItem);
1007
+ this.setSchedulePosition(playingIndex + 1);
1008
+ }
1009
+ // Check if playback has entered the next asset
1010
+ const playingAsset = this.playingAsset;
1011
+ if (!playingAsset) {
1012
+ return;
1013
+ }
1014
+ const end = playingAsset.timelineStart + (playingAsset.duration || 0);
1015
+ if (currentTime >= end) {
1016
+ this.setScheduleToAssetAtTime(currentTime, playingAsset);
1017
+ }
1018
+ };
1019
+
1020
+ // Scheduling methods
1021
+ private checkStart() {
1022
+ const schedule = this.schedule;
1023
+ const interstitialEvents = schedule?.events;
1024
+ if (!interstitialEvents || this.playbackDisabled || !this.media) {
1025
+ return;
1026
+ }
1027
+ // Check buffered to pre-roll
1028
+ if (this.bufferedPos === -1) {
1029
+ this.bufferedPos = 0;
1030
+ }
1031
+ // Start stepping through schedule when playback begins for the first time and we have a pre-roll
1032
+ const timelinePos = this.timelinePos;
1033
+ const effectivePlayingItem = this.effectivePlayingItem;
1034
+ if (timelinePos === -1) {
1035
+ const startPosition = this.hls.startPosition;
1036
+ this.log(timelineMessage('checkStart', startPosition));
1037
+ this.timelinePos = startPosition;
1038
+ if (interstitialEvents.length && interstitialEvents[0].cue.pre) {
1039
+ const index = schedule.findEventIndex(interstitialEvents[0].identifier);
1040
+ this.setSchedulePosition(index);
1041
+ } else if (startPosition >= 0 || !this.primaryLive) {
1042
+ const start = (this.timelinePos =
1043
+ startPosition > 0 ? startPosition : 0);
1044
+ const index = schedule.findItemIndexAtTime(start);
1045
+ this.setSchedulePosition(index);
1046
+ }
1047
+ } else if (effectivePlayingItem && !this.playingItem) {
1048
+ const index = schedule.findItemIndex(effectivePlayingItem);
1049
+ this.setSchedulePosition(index);
1050
+ }
1051
+ }
1052
+
1053
+ private advanceAssetBuffering(
1054
+ item: InterstitialScheduleEventItem,
1055
+ assetItem: InterstitialAssetItem,
1056
+ ) {
1057
+ const interstitial = item.event;
1058
+ const assetListIndex = interstitial.findAssetIndex(assetItem);
1059
+ const nextAssetIndex = getNextAssetIndex(interstitial, assetListIndex);
1060
+ if (!interstitial.isAssetPastPlayoutLimit(nextAssetIndex)) {
1061
+ this.bufferedToEvent(item, nextAssetIndex);
1062
+ } else if (this.schedule) {
1063
+ const nextItem = this.schedule.items?.[this.findItemIndex(item) + 1];
1064
+ if (nextItem) {
1065
+ this.bufferedToItem(nextItem);
1066
+ }
1067
+ }
1068
+ }
1069
+
1070
+ private advanceAfterAssetEnded(
1071
+ interstitial: InterstitialEvent,
1072
+ index: number,
1073
+ assetListIndex: number,
1074
+ ) {
1075
+ const nextAssetIndex = getNextAssetIndex(interstitial, assetListIndex);
1076
+ if (!interstitial.isAssetPastPlayoutLimit(nextAssetIndex)) {
1077
+ // Advance to next asset list item
1078
+ if (interstitial.appendInPlace) {
1079
+ const assetItem = interstitial.assetList[nextAssetIndex] as
1080
+ | InterstitialAssetItem
1081
+ | undefined;
1082
+ if (assetItem) {
1083
+ this.advanceInPlace(assetItem.timelineStart);
1084
+ }
1085
+ }
1086
+ this.setSchedulePosition(index, nextAssetIndex);
1087
+ } else if (this.schedule) {
1088
+ // Advance to next schedule segment
1089
+ // check if we've reached the end of the program
1090
+ const scheduleItems = this.schedule.items;
1091
+ if (scheduleItems) {
1092
+ const nextIndex = index + 1;
1093
+ const scheduleLength = scheduleItems.length;
1094
+ if (nextIndex >= scheduleLength) {
1095
+ this.setSchedulePosition(-1);
1096
+ return;
1097
+ }
1098
+ const resumptionTime = interstitial.resumeTime;
1099
+ if (this.timelinePos < resumptionTime) {
1100
+ this.log(timelineMessage('advanceAfterAssetEnded', resumptionTime));
1101
+ this.timelinePos = resumptionTime;
1102
+ if (interstitial.appendInPlace) {
1103
+ this.advanceInPlace(resumptionTime);
1104
+ }
1105
+ this.checkBuffer(this.bufferedPos < resumptionTime);
1106
+ }
1107
+ this.setSchedulePosition(nextIndex);
1108
+ }
1109
+ }
1110
+ }
1111
+
1112
+ private setScheduleToAssetAtTime(
1113
+ time: number,
1114
+ playingAsset: InterstitialAssetItem,
1115
+ ) {
1116
+ const schedule = this.schedule;
1117
+ if (!schedule) {
1118
+ return;
1119
+ }
1120
+ const parentIdentifier = playingAsset.parentIdentifier;
1121
+ const interstitial = schedule.getEvent(parentIdentifier);
1122
+ if (interstitial) {
1123
+ const itemIndex = schedule.findEventIndex(parentIdentifier);
1124
+ const assetListIndex = schedule.findAssetIndex(interstitial, time);
1125
+ this.advanceAfterAssetEnded(interstitial, itemIndex, assetListIndex - 1);
1126
+ }
1127
+ }
1128
+
1129
+ private setSchedulePosition(index: number, assetListIndex?: number) {
1130
+ const scheduleItems = this.schedule?.items;
1131
+ if (!scheduleItems || this.playbackDisabled) {
1132
+ return;
1133
+ }
1134
+ const scheduledItem = index >= 0 ? scheduleItems[index] : null;
1135
+ this.log(
1136
+ `setSchedulePosition ${index}, ${assetListIndex} (${scheduledItem ? segmentToString(scheduledItem) : scheduledItem}) pos: ${this.timelinePos}`,
1137
+ );
1138
+ // Cleanup current item / asset
1139
+ const currentItem = this.waitingItem || this.playingItem;
1140
+ const playingLastItem = this.playingLastItem;
1141
+ if (this.isInterstitial(currentItem)) {
1142
+ const interstitial = currentItem.event;
1143
+ const playingAsset = this.playingAsset;
1144
+ const assetId = playingAsset?.identifier;
1145
+ const player = assetId ? this.getAssetPlayer(assetId) : null;
1146
+ if (
1147
+ player &&
1148
+ assetId &&
1149
+ (!this.eventItemsMatch(currentItem, scheduledItem) ||
1150
+ (assetListIndex !== undefined &&
1151
+ assetId !== interstitial.assetList[assetListIndex].identifier))
1152
+ ) {
1153
+ const playingAssetListIndex = interstitial.findAssetIndex(playingAsset);
1154
+ this.log(
1155
+ `INTERSTITIAL_ASSET_ENDED ${playingAssetListIndex + 1}/${interstitial.assetList.length} ${eventAssetToString(playingAsset)}`,
1156
+ );
1157
+ this.endedAsset = playingAsset;
1158
+ this.playingAsset = null;
1159
+ this.hls.trigger(Events.INTERSTITIAL_ASSET_ENDED, {
1160
+ asset: playingAsset,
1161
+ assetListIndex: playingAssetListIndex,
1162
+ event: interstitial,
1163
+ schedule: scheduleItems.slice(0),
1164
+ scheduleIndex: index,
1165
+ player,
1166
+ });
1167
+ if (currentItem !== this.playingItem) {
1168
+ // Schedule change occured on INTERSTITIAL_ASSET_ENDED
1169
+ if (
1170
+ this.itemsMatch(currentItem, this.playingItem) &&
1171
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1172
+ !this.playingAsset // INTERSTITIAL_ASSET_ENDED side-effect
1173
+ ) {
1174
+ this.advanceAfterAssetEnded(
1175
+ interstitial,
1176
+ this.findItemIndex(this.playingItem),
1177
+ playingAssetListIndex,
1178
+ );
1179
+ }
1180
+ // Navigation occured on INTERSTITIAL_ASSET_ENDED
1181
+ return;
1182
+ }
1183
+ this.retreiveMediaSource(assetId, scheduledItem);
1184
+ if (player.media && !this.detachedData?.mediaSource) {
1185
+ player.detachMedia();
1186
+ }
1187
+ }
1188
+ if (!this.eventItemsMatch(currentItem, scheduledItem)) {
1189
+ this.endedItem = currentItem;
1190
+ this.playingItem = null;
1191
+ this.log(
1192
+ `INTERSTITIAL_ENDED ${interstitial} ${segmentToString(currentItem)}`,
1193
+ );
1194
+ interstitial.hasPlayed = true;
1195
+ this.hls.trigger(Events.INTERSTITIAL_ENDED, {
1196
+ event: interstitial,
1197
+ schedule: scheduleItems.slice(0),
1198
+ scheduleIndex: index,
1199
+ });
1200
+ // Exiting an Interstitial
1201
+ if (interstitial.cue.once) {
1202
+ // Remove interstitial with CUE attribute value of ONCE after it has played
1203
+ this.updateSchedule();
1204
+ const updatedScheduleItems = this.schedule?.items;
1205
+ if (scheduledItem && updatedScheduleItems) {
1206
+ const updatedIndex = this.findItemIndex(scheduledItem);
1207
+ this.advanceSchedule(
1208
+ updatedIndex,
1209
+ updatedScheduleItems,
1210
+ assetListIndex,
1211
+ currentItem,
1212
+ playingLastItem,
1213
+ );
1214
+ }
1215
+ return;
1216
+ }
1217
+ }
1218
+ }
1219
+ this.advanceSchedule(
1220
+ index,
1221
+ scheduleItems,
1222
+ assetListIndex,
1223
+ currentItem,
1224
+ playingLastItem,
1225
+ );
1226
+ }
1227
+ private advanceSchedule(
1228
+ index: number,
1229
+ scheduleItems: InterstitialScheduleItem[],
1230
+ assetListIndex: number | undefined,
1231
+ currentItem: InterstitialScheduleItem | null,
1232
+ playedLastItem: boolean,
1233
+ ) {
1234
+ const schedule = this.schedule;
1235
+ if (!schedule) {
1236
+ return;
1237
+ }
1238
+ const scheduledItem = scheduleItems[index] || null;
1239
+ const media = this.primaryMedia;
1240
+ // Cleanup out of range Interstitials
1241
+ const playerQueue = this.playerQueue;
1242
+ if (playerQueue.length) {
1243
+ playerQueue.forEach((player) => {
1244
+ const interstitial = player.interstitial;
1245
+ const queuedIndex = schedule.findEventIndex(interstitial.identifier);
1246
+ if (queuedIndex < index || queuedIndex > index + 1) {
1247
+ this.clearInterstitial(interstitial, scheduledItem);
1248
+ }
1249
+ });
1250
+ }
1251
+ // Setup scheduled item
1252
+ if (this.isInterstitial(scheduledItem)) {
1253
+ this.timelinePos = Math.min(
1254
+ Math.max(this.timelinePos, scheduledItem.start),
1255
+ scheduledItem.end,
1256
+ );
1257
+ // Handle Interstitial
1258
+ const interstitial = scheduledItem.event;
1259
+ // find asset index
1260
+ if (assetListIndex === undefined) {
1261
+ assetListIndex = schedule.findAssetIndex(
1262
+ interstitial,
1263
+ this.timelinePos,
1264
+ );
1265
+ const assetIndexCandidate = getNextAssetIndex(
1266
+ interstitial,
1267
+ assetListIndex - 1,
1268
+ );
1269
+ if (
1270
+ interstitial.isAssetPastPlayoutLimit(assetIndexCandidate) ||
1271
+ (interstitial.appendInPlace && this.timelinePos === scheduledItem.end)
1272
+ ) {
1273
+ this.advanceAfterAssetEnded(interstitial, index, assetListIndex);
1274
+ return;
1275
+ }
1276
+ assetListIndex = assetIndexCandidate;
1277
+ }
1278
+ // Ensure Interstitial is enqueued
1279
+ const waitingItem = this.waitingItem;
1280
+ if (!this.assetsBuffered(scheduledItem, media)) {
1281
+ this.setBufferingItem(scheduledItem);
1282
+ }
1283
+ let player = this.preloadAssets(interstitial, assetListIndex);
1284
+ if (!this.eventItemsMatch(scheduledItem, waitingItem || currentItem)) {
1285
+ this.waitingItem = scheduledItem;
1286
+ this.log(
1287
+ `INTERSTITIAL_STARTED ${segmentToString(scheduledItem)} ${interstitial.appendInPlace ? 'append in place' : ''}`,
1288
+ );
1289
+ this.hls.trigger(Events.INTERSTITIAL_STARTED, {
1290
+ event: interstitial,
1291
+ schedule: scheduleItems.slice(0),
1292
+ scheduleIndex: index,
1293
+ });
1294
+ }
1295
+ if (!interstitial.assetListLoaded) {
1296
+ // Waiting at end of primary content segment
1297
+ // Expect setSchedulePosition to be called again once ASSET-LIST is loaded
1298
+ this.log(`Waiting for ASSET-LIST to complete loading ${interstitial}`);
1299
+ return;
1300
+ }
1301
+ if (interstitial.assetListLoader) {
1302
+ interstitial.assetListLoader.destroy();
1303
+ interstitial.assetListLoader = undefined;
1304
+ }
1305
+ if (!media) {
1306
+ this.log(
1307
+ `Waiting for attachMedia to start Interstitial ${interstitial}`,
1308
+ );
1309
+ return;
1310
+ }
1311
+ // Update schedule and asset list position now that it can start
1312
+ this.waitingItem = this.endedItem = null;
1313
+ this.playingItem = scheduledItem;
1314
+
1315
+ // If asset-list is empty or missing asset index, advance to next item
1316
+ const assetItem = interstitial.assetList[assetListIndex] as
1317
+ | InterstitialAssetItem
1318
+ | undefined;
1319
+ if (!assetItem) {
1320
+ this.advanceAfterAssetEnded(interstitial, index, assetListIndex || 0);
1321
+ return;
1322
+ }
1323
+
1324
+ // Start Interstitial Playback
1325
+ if (!player) {
1326
+ player = this.getAssetPlayer(assetItem.identifier);
1327
+ }
1328
+ if (player === null || player.destroyed) {
1329
+ const assetListLength = interstitial.assetList.length;
1330
+ this.warn(
1331
+ `asset ${
1332
+ assetListIndex + 1
1333
+ }/${assetListLength} player destroyed ${interstitial}`,
1334
+ );
1335
+ player = this.createAssetPlayer(
1336
+ interstitial,
1337
+ assetItem,
1338
+ assetListIndex,
1339
+ );
1340
+ player.loadSource();
1341
+ }
1342
+ if (!this.eventItemsMatch(scheduledItem, this.bufferingItem)) {
1343
+ if (interstitial.appendInPlace && this.isAssetBuffered(assetItem)) {
1344
+ return;
1345
+ }
1346
+ }
1347
+ this.startAssetPlayer(
1348
+ player,
1349
+ assetListIndex,
1350
+ scheduleItems,
1351
+ index,
1352
+ media,
1353
+ );
1354
+ if (this.shouldPlay) {
1355
+ playWithCatch(player.media);
1356
+ }
1357
+ } else if (scheduledItem) {
1358
+ this.resumePrimary(scheduledItem, index, currentItem);
1359
+ if (this.shouldPlay) {
1360
+ playWithCatch(this.hls.media);
1361
+ }
1362
+ } else if (playedLastItem && this.isInterstitial(currentItem)) {
1363
+ // Maintain playingItem state at end of schedule (setSchedulePosition(-1) called to end program)
1364
+ // this allows onSeeking handler to update schedule position
1365
+ this.endedItem = null;
1366
+ this.playingItem = currentItem;
1367
+ if (!currentItem.event.appendInPlace) {
1368
+ // Media must be re-attached to resume primary schedule if not sharing source
1369
+ this.attachPrimary(schedule.durations.primary, null);
1370
+ }
1371
+ }
1372
+ }
1373
+
1374
+ private get playbackDisabled(): boolean {
1375
+ return this.hls.config.enableInterstitialPlayback === false;
1376
+ }
1377
+
1378
+ private get primaryDetails(): LevelDetails | undefined {
1379
+ return this.mediaSelection?.main.details;
1380
+ }
1381
+
1382
+ private get primaryLive(): boolean {
1383
+ return !!this.primaryDetails?.live;
1384
+ }
1385
+
1386
+ private resumePrimary(
1387
+ scheduledItem: InterstitialSchedulePrimaryItem,
1388
+ index: number,
1389
+ fromItem: InterstitialScheduleItem | null,
1390
+ ) {
1391
+ this.playingItem = scheduledItem;
1392
+ this.playingAsset = this.endedAsset = null;
1393
+ this.waitingItem = this.endedItem = null;
1394
+
1395
+ this.bufferedToItem(scheduledItem);
1396
+
1397
+ this.log(`resuming ${segmentToString(scheduledItem)}`);
1398
+
1399
+ if (!this.detachedData?.mediaSource) {
1400
+ let timelinePos = this.timelinePos;
1401
+ if (
1402
+ timelinePos < scheduledItem.start ||
1403
+ timelinePos >= scheduledItem.end
1404
+ ) {
1405
+ timelinePos = this.getPrimaryResumption(scheduledItem, index);
1406
+ this.log(timelineMessage('resumePrimary', timelinePos));
1407
+ this.timelinePos = timelinePos;
1408
+ }
1409
+ this.attachPrimary(timelinePos, scheduledItem);
1410
+ }
1411
+
1412
+ if (!fromItem) {
1413
+ return;
1414
+ }
1415
+
1416
+ const scheduleItems = this.schedule?.items;
1417
+ if (!scheduleItems) {
1418
+ return;
1419
+ }
1420
+ this.log(`INTERSTITIALS_PRIMARY_RESUMED ${segmentToString(scheduledItem)}`);
1421
+ this.hls.trigger(Events.INTERSTITIALS_PRIMARY_RESUMED, {
1422
+ schedule: scheduleItems.slice(0),
1423
+ scheduleIndex: index,
1424
+ });
1425
+ this.checkBuffer();
1426
+ }
1427
+
1428
+ private getPrimaryResumption(
1429
+ scheduledItem: InterstitialSchedulePrimaryItem,
1430
+ index: number,
1431
+ ): number {
1432
+ const itemStart = scheduledItem.start;
1433
+ if (this.primaryLive) {
1434
+ const details = this.primaryDetails;
1435
+ if (index === 0) {
1436
+ return this.hls.startPosition;
1437
+ } else if (
1438
+ details &&
1439
+ (itemStart < details.fragmentStart || itemStart > details.edge)
1440
+ ) {
1441
+ return this.hls.liveSyncPosition || -1;
1442
+ }
1443
+ }
1444
+ return itemStart;
1445
+ }
1446
+
1447
+ private isAssetBuffered(asset: InterstitialAssetItem): boolean {
1448
+ const player = this.getAssetPlayer(asset.identifier);
1449
+ if (player?.hls) {
1450
+ return player.hls.bufferedToEnd;
1451
+ }
1452
+ const bufferInfo = BufferHelper.bufferInfo(
1453
+ this.primaryMedia,
1454
+ this.timelinePos,
1455
+ 0,
1456
+ );
1457
+ return bufferInfo.end + 1 >= asset.timelineStart + (asset.duration || 0);
1458
+ }
1459
+
1460
+ private attachPrimary(
1461
+ timelinePos: number,
1462
+ item: InterstitialSchedulePrimaryItem | null,
1463
+ skipSeekToStartPosition?: boolean,
1464
+ ) {
1465
+ if (item) {
1466
+ this.setBufferingItem(item);
1467
+ } else {
1468
+ this.bufferingItem = this.playingItem;
1469
+ }
1470
+ this.bufferingAsset = null;
1471
+
1472
+ const media = this.primaryMedia;
1473
+ if (!media) {
1474
+ return;
1475
+ }
1476
+ const hls = this.hls;
1477
+ if (hls.media) {
1478
+ this.checkBuffer();
1479
+ } else {
1480
+ this.transferMediaTo(hls, media);
1481
+ if (skipSeekToStartPosition) {
1482
+ this.startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition);
1483
+ }
1484
+ }
1485
+ if (!skipSeekToStartPosition) {
1486
+ // Set primary position to resume time
1487
+ this.log(timelineMessage('attachPrimary', timelinePos));
1488
+ this.timelinePos = timelinePos;
1489
+ this.startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition);
1490
+ }
1491
+ }
1492
+
1493
+ private startLoadingPrimaryAt(
1494
+ timelinePos: number,
1495
+ skipSeekToStartPosition?: boolean,
1496
+ ) {
1497
+ const hls = this.hls;
1498
+ if (
1499
+ !hls.loadingEnabled ||
1500
+ !hls.media ||
1501
+ Math.abs(
1502
+ (hls.mainForwardBufferInfo?.start || hls.media.currentTime) -
1503
+ timelinePos,
1504
+ ) > 0.5
1505
+ ) {
1506
+ hls.startLoad(timelinePos, skipSeekToStartPosition);
1507
+ } else if (!hls.bufferingEnabled) {
1508
+ hls.resumeBuffering();
1509
+ }
1510
+ }
1511
+
1512
+ // HLS.js event callbacks
1513
+ private onManifestLoading() {
1514
+ this.stopLoad();
1515
+ this.schedule?.reset();
1516
+ this.emptyPlayerQueue();
1517
+ this.clearScheduleState();
1518
+ this.shouldPlay = false;
1519
+ this.bufferedPos = this.timelinePos = -1;
1520
+ this.mediaSelection =
1521
+ this.altSelection =
1522
+ this.manager =
1523
+ this.requiredTracks =
1524
+ null;
1525
+ // BUFFER_CODECS listener added here for buffer-controller to handle it first where it adds tracks
1526
+ this.hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this);
1527
+ this.hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this);
1528
+ }
1529
+
1530
+ private onLevelUpdated(event: Events.LEVEL_UPDATED, data: LevelUpdatedData) {
1531
+ if (data.level === -1 || !this.schedule) {
1532
+ // level was removed
1533
+ return;
1534
+ }
1535
+ const main = this.hls.levels[data.level];
1536
+ if (!main.details) {
1537
+ return;
1538
+ }
1539
+ const currentSelection = {
1540
+ ...(this.mediaSelection || this.altSelection),
1541
+ main,
1542
+ };
1543
+ this.mediaSelection = currentSelection;
1544
+ this.schedule.parseInterstitialDateRanges(
1545
+ currentSelection,
1546
+ this.hls.config.interstitialAppendInPlace,
1547
+ );
1548
+
1549
+ if (!this.effectivePlayingItem && this.schedule.items) {
1550
+ this.checkStart();
1551
+ }
1552
+ }
1553
+
1554
+ private onAudioTrackUpdated(
1555
+ event: Events.AUDIO_TRACK_UPDATED,
1556
+ data: AudioTrackUpdatedData,
1557
+ ) {
1558
+ const audio = this.hls.audioTracks[data.id];
1559
+ const previousSelection = this.mediaSelection;
1560
+ if (!previousSelection) {
1561
+ this.altSelection = { ...this.altSelection, audio };
1562
+ return;
1563
+ }
1564
+ const currentSelection = { ...previousSelection, audio };
1565
+ this.mediaSelection = currentSelection;
1566
+ }
1567
+
1568
+ private onSubtitleTrackUpdated(
1569
+ event: Events.SUBTITLE_TRACK_UPDATED,
1570
+ data: SubtitleTrackUpdatedData,
1571
+ ) {
1572
+ const subtitles = this.hls.subtitleTracks[data.id];
1573
+ const previousSelection = this.mediaSelection;
1574
+ if (!previousSelection) {
1575
+ this.altSelection = { ...this.altSelection, subtitles };
1576
+ return;
1577
+ }
1578
+ const currentSelection = { ...previousSelection, subtitles };
1579
+ this.mediaSelection = currentSelection;
1580
+ }
1581
+
1582
+ private onAudioTrackSwitching(
1583
+ event: Events.AUDIO_TRACK_SWITCHING,
1584
+ data: AudioTrackSwitchingData,
1585
+ ) {
1586
+ const audioOption = getBasicSelectionOption(data);
1587
+ this.playerQueue.forEach(
1588
+ ({ hls }) =>
1589
+ hls && (hls.setAudioOption(data) || hls.setAudioOption(audioOption)),
1590
+ );
1591
+ }
1592
+
1593
+ private onSubtitleTrackSwitch(
1594
+ event: Events.SUBTITLE_TRACK_SWITCH,
1595
+ data: SubtitleTrackSwitchData,
1596
+ ) {
1597
+ const subtitleOption = getBasicSelectionOption(data);
1598
+ this.playerQueue.forEach(
1599
+ ({ hls }) =>
1600
+ hls &&
1601
+ (hls.setSubtitleOption(data) ||
1602
+ (data.id !== -1 && hls.setSubtitleOption(subtitleOption))),
1603
+ );
1604
+ }
1605
+
1606
+ private onBufferCodecs(event: Events.BUFFER_CODECS, data: BufferCodecsData) {
1607
+ const requiredTracks = data.tracks;
1608
+ if (requiredTracks) {
1609
+ this.requiredTracks = requiredTracks;
1610
+ }
1611
+ }
1612
+
1613
+ private onBufferAppended(
1614
+ event: Events.BUFFER_APPENDED,
1615
+ data: BufferAppendedData,
1616
+ ) {
1617
+ this.checkBuffer();
1618
+ }
1619
+
1620
+ private onBufferFlushed(
1621
+ event: Events.BUFFER_FLUSHED,
1622
+ data: BufferFlushedData,
1623
+ ) {
1624
+ const playingItem = this.playingItem;
1625
+ if (
1626
+ playingItem &&
1627
+ !this.itemsMatch(playingItem, this.bufferingItem) &&
1628
+ !this.isInterstitial(playingItem)
1629
+ ) {
1630
+ const timelinePos = this.timelinePos;
1631
+ this.bufferedPos = timelinePos;
1632
+ this.checkBuffer();
1633
+ }
1634
+ }
1635
+
1636
+ private onBufferedToEnd(event: Events.BUFFERED_TO_END) {
1637
+ if (!this.schedule) {
1638
+ return;
1639
+ }
1640
+ // Buffered to post-roll
1641
+ const interstitialEvents = this.schedule.events;
1642
+ if (this.bufferedPos < Number.MAX_VALUE && interstitialEvents) {
1643
+ for (let i = 0; i < interstitialEvents.length; i++) {
1644
+ const interstitial = interstitialEvents[i];
1645
+ if (interstitial.cue.post) {
1646
+ const scheduleIndex = this.schedule.findEventIndex(
1647
+ interstitial.identifier,
1648
+ );
1649
+ const item = this.schedule.items?.[scheduleIndex];
1650
+ if (
1651
+ this.isInterstitial(item) &&
1652
+ this.eventItemsMatch(item, this.bufferingItem)
1653
+ ) {
1654
+ this.bufferedToItem(item, 0);
1655
+ }
1656
+ break;
1657
+ }
1658
+ }
1659
+ this.bufferedPos = Number.MAX_VALUE;
1660
+ }
1661
+ }
1662
+
1663
+ private onMediaEnded(event: Events.MEDIA_ENDED) {
1664
+ const playingItem = this.playingItem;
1665
+ if (!this.playingLastItem && playingItem) {
1666
+ const playingIndex = this.findItemIndex(playingItem);
1667
+ this.setSchedulePosition(playingIndex + 1);
1668
+ } else {
1669
+ this.shouldPlay = false;
1670
+ }
1671
+ }
1672
+
1673
+ // Schedule update callback
1674
+ private onScheduleUpdate = (
1675
+ removedInterstitials: InterstitialEvent[],
1676
+ previousItems: InterstitialScheduleItem[] | null,
1677
+ ) => {
1678
+ const schedule = this.schedule;
1679
+ if (!schedule) {
1680
+ return;
1681
+ }
1682
+ const playingItem = this.playingItem;
1683
+ const interstitialEvents = schedule.events || [];
1684
+ const scheduleItems = schedule.items || [];
1685
+ const durations = schedule.durations;
1686
+ const removedIds = removedInterstitials.map(
1687
+ (interstitial) => interstitial.identifier,
1688
+ );
1689
+ const interstitialsUpdated = !!(
1690
+ interstitialEvents.length || removedIds.length
1691
+ );
1692
+ if (interstitialsUpdated || previousItems) {
1693
+ this.log(
1694
+ `INTERSTITIALS_UPDATED (${
1695
+ interstitialEvents.length
1696
+ }): ${interstitialEvents}
1697
+ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timelinePos}`,
1698
+ );
1699
+ }
1700
+ if (removedIds.length) {
1701
+ this.log(`Removed events ${removedIds}`);
1702
+ }
1703
+
1704
+ // Update schedule item references
1705
+ // Do not replace Interstitial playingItem without a match - used for INTERSTITIAL_ASSET_ENDED and INTERSTITIAL_ENDED
1706
+ let updatedPlayingItem: InterstitialScheduleItem | null = null;
1707
+ let updatedBufferingItem: InterstitialScheduleItem | null = null;
1708
+ if (playingItem) {
1709
+ updatedPlayingItem = this.updateItem(playingItem, this.timelinePos);
1710
+ if (this.itemsMatch(playingItem, updatedPlayingItem)) {
1711
+ this.playingItem = updatedPlayingItem;
1712
+ } else {
1713
+ this.waitingItem = this.endedItem = null;
1714
+ }
1715
+ }
1716
+ // Clear waitingItem if it has been removed from the schedule
1717
+ this.waitingItem = this.updateItem(this.waitingItem);
1718
+ this.endedItem = this.updateItem(this.endedItem);
1719
+ // Do not replace Interstitial bufferingItem without a match - used for transfering media element or source
1720
+ const bufferingItem = this.bufferingItem;
1721
+ if (bufferingItem) {
1722
+ updatedBufferingItem = this.updateItem(bufferingItem, this.bufferedPos);
1723
+ if (this.itemsMatch(bufferingItem, updatedBufferingItem)) {
1724
+ this.bufferingItem = updatedBufferingItem;
1725
+ } else if (bufferingItem.event) {
1726
+ // Interstitial removed from schedule (Live -> VOD or other scenario where Start Date is outside the range of VOD Playlist)
1727
+ this.bufferingItem = this.playingItem;
1728
+ this.clearInterstitial(bufferingItem.event, null);
1729
+ }
1730
+ }
1731
+
1732
+ removedInterstitials.forEach((interstitial) => {
1733
+ interstitial.assetList.forEach((asset) => {
1734
+ this.clearAssetPlayer(asset.identifier, null);
1735
+ });
1736
+ });
1737
+
1738
+ this.playerQueue.forEach((player) => {
1739
+ if (player.interstitial.appendInPlace) {
1740
+ const timelineStart = player.assetItem.timelineStart;
1741
+ const diff = player.timelineOffset - timelineStart;
1742
+ if (diff) {
1743
+ try {
1744
+ player.timelineOffset = timelineStart;
1745
+ } catch (e) {
1746
+ if (Math.abs(diff) > ALIGNED_END_THRESHOLD_SECONDS) {
1747
+ this.warn(
1748
+ `${e} ("${player.assetId}" ${player.timelineOffset}->${timelineStart})`,
1749
+ );
1750
+ }
1751
+ }
1752
+ }
1753
+ }
1754
+ });
1755
+
1756
+ if (interstitialsUpdated || previousItems) {
1757
+ this.hls.trigger(Events.INTERSTITIALS_UPDATED, {
1758
+ events: interstitialEvents.slice(0),
1759
+ schedule: scheduleItems.slice(0),
1760
+ durations,
1761
+ removedIds,
1762
+ });
1763
+
1764
+ if (
1765
+ this.isInterstitial(playingItem) &&
1766
+ removedIds.includes(playingItem.event.identifier)
1767
+ ) {
1768
+ this.warn(
1769
+ `Interstitial "${playingItem.event.identifier}" removed while playing`,
1770
+ );
1771
+ this.primaryFallback(playingItem.event);
1772
+ return;
1773
+ }
1774
+
1775
+ if (playingItem) {
1776
+ this.trimInPlace(updatedPlayingItem, playingItem);
1777
+ }
1778
+ if (bufferingItem && updatedBufferingItem !== updatedPlayingItem) {
1779
+ this.trimInPlace(updatedBufferingItem, bufferingItem);
1780
+ }
1781
+
1782
+ // Check if buffered to new Interstitial event boundary
1783
+ // (Live update publishes Interstitial with new segment)
1784
+ this.checkBuffer();
1785
+ }
1786
+ };
1787
+
1788
+ private updateItem<T extends InterstitialScheduleItem>(
1789
+ previousItem: T | null,
1790
+ time?: number,
1791
+ ): T | null {
1792
+ // find item in this.schedule.items;
1793
+ const items = this.schedule?.items;
1794
+ if (previousItem && items) {
1795
+ const index = this.findItemIndex(previousItem, time);
1796
+ return (items[index] as T | undefined) || null;
1797
+ }
1798
+ return null;
1799
+ }
1800
+
1801
+ private trimInPlace(
1802
+ updatedItem: InterstitialScheduleItem | null,
1803
+ itemBeforeUpdate: InterstitialScheduleItem,
1804
+ ) {
1805
+ if (
1806
+ this.isInterstitial(updatedItem) &&
1807
+ updatedItem.event.appendInPlace &&
1808
+ itemBeforeUpdate.end - updatedItem.end > 0.25
1809
+ ) {
1810
+ updatedItem.event.assetList.forEach((asset, index) => {
1811
+ if (updatedItem.event.isAssetPastPlayoutLimit(index)) {
1812
+ this.clearAssetPlayer(asset.identifier, null);
1813
+ }
1814
+ });
1815
+ const flushStart = updatedItem.end + 0.25;
1816
+ const bufferInfo = BufferHelper.bufferInfo(
1817
+ this.primaryMedia,
1818
+ flushStart,
1819
+ 0,
1820
+ );
1821
+ if (
1822
+ bufferInfo.end > flushStart ||
1823
+ (bufferInfo.nextStart || 0) > flushStart
1824
+ ) {
1825
+ this.log(
1826
+ `trim buffered interstitial ${segmentToString(updatedItem)} (was ${segmentToString(itemBeforeUpdate)})`,
1827
+ );
1828
+ const skipSeekToStartPosition = true;
1829
+ this.attachPrimary(flushStart, null, skipSeekToStartPosition);
1830
+ this.flushFrontBuffer(flushStart);
1831
+ }
1832
+ }
1833
+ }
1834
+
1835
+ private itemsMatch(
1836
+ a: InterstitialScheduleItem,
1837
+ b: InterstitialScheduleItem | null | undefined,
1838
+ ): boolean {
1839
+ return (
1840
+ !!b &&
1841
+ (a === b ||
1842
+ (a.event && b.event && this.eventItemsMatch(a, b)) ||
1843
+ (!a.event &&
1844
+ !b.event &&
1845
+ this.findItemIndex(a) === this.findItemIndex(b)))
1846
+ );
1847
+ }
1848
+
1849
+ private eventItemsMatch(
1850
+ a: InterstitialScheduleEventItem,
1851
+ b: InterstitialScheduleItem | null | undefined,
1852
+ ): boolean {
1853
+ return !!b && (a === b || a.event.identifier === b.event?.identifier);
1854
+ }
1855
+
1856
+ private findItemIndex(
1857
+ item: InterstitialScheduleItem | null,
1858
+ time?: number,
1859
+ ): number {
1860
+ return item && this.schedule ? this.schedule.findItemIndex(item, time) : -1;
1861
+ }
1862
+
1863
+ private updateSchedule(forceUpdate: boolean = false) {
1864
+ const mediaSelection = this.mediaSelection;
1865
+ if (!mediaSelection) {
1866
+ return;
1867
+ }
1868
+ this.schedule?.updateSchedule(mediaSelection, [], forceUpdate);
1869
+ }
1870
+
1871
+ // Schedule buffer control
1872
+ private checkBuffer(starved?: boolean) {
1873
+ const items = this.schedule?.items;
1874
+ if (!items) {
1875
+ return;
1876
+ }
1877
+ // Find when combined forward buffer change reaches next schedule segment
1878
+ const bufferInfo = BufferHelper.bufferInfo(
1879
+ this.primaryMedia,
1880
+ this.timelinePos,
1881
+ 0,
1882
+ );
1883
+ if (starved) {
1884
+ this.bufferedPos = this.timelinePos;
1885
+ }
1886
+ starved ||= bufferInfo.len < 1;
1887
+ this.updateBufferedPos(bufferInfo.end, items, starved);
1888
+ }
1889
+
1890
+ private updateBufferedPos(
1891
+ bufferEnd: number,
1892
+ items: InterstitialScheduleItem[],
1893
+ bufferIsEmpty?: boolean,
1894
+ ) {
1895
+ const schedule = this.schedule;
1896
+ const bufferingItem = this.bufferingItem;
1897
+ if (this.bufferedPos > bufferEnd || !schedule) {
1898
+ return;
1899
+ }
1900
+ if (items.length === 1 && this.itemsMatch(items[0], bufferingItem)) {
1901
+ this.bufferedPos = bufferEnd;
1902
+ return;
1903
+ }
1904
+ const playingItem = this.playingItem;
1905
+ const playingIndex = this.findItemIndex(playingItem);
1906
+ let bufferEndIndex = schedule.findItemIndexAtTime(bufferEnd);
1907
+
1908
+ if (this.bufferedPos < bufferEnd) {
1909
+ const bufferingIndex = this.findItemIndex(bufferingItem);
1910
+ const nextToBufferIndex = Math.min(bufferingIndex + 1, items.length - 1);
1911
+ const nextItemToBuffer = items[nextToBufferIndex];
1912
+ if (
1913
+ (bufferEndIndex === -1 &&
1914
+ bufferingItem &&
1915
+ bufferEnd >= bufferingItem.end) ||
1916
+ (nextItemToBuffer.event?.appendInPlace &&
1917
+ bufferEnd + 0.01 >= nextItemToBuffer.start)
1918
+ ) {
1919
+ bufferEndIndex = nextToBufferIndex;
1920
+ }
1921
+ if (this.isInterstitial(bufferingItem)) {
1922
+ const interstitial = bufferingItem.event;
1923
+ if (
1924
+ nextToBufferIndex - playingIndex > 1 &&
1925
+ interstitial.appendInPlace === false
1926
+ ) {
1927
+ // do not advance buffering item past Interstitial that requires source reset
1928
+ return;
1929
+ }
1930
+ if (
1931
+ interstitial.assetList.length === 0 &&
1932
+ interstitial.assetListLoader
1933
+ ) {
1934
+ // do not advance buffering item past Interstitial loading asset-list
1935
+ return;
1936
+ }
1937
+ }
1938
+ this.bufferedPos = bufferEnd;
1939
+ if (bufferEndIndex > bufferingIndex && bufferEndIndex > playingIndex) {
1940
+ this.bufferedToItem(nextItemToBuffer);
1941
+ } else {
1942
+ // allow more time than distance from edge for assets to load
1943
+ const details = this.primaryDetails;
1944
+ if (
1945
+ this.primaryLive &&
1946
+ details &&
1947
+ bufferEnd > details.edge - details.targetduration &&
1948
+ nextItemToBuffer.start <
1949
+ details.edge + this.hls.config.interstitialLiveLookAhead &&
1950
+ this.isInterstitial(nextItemToBuffer)
1951
+ ) {
1952
+ this.preloadAssets(nextItemToBuffer.event, 0);
1953
+ }
1954
+ }
1955
+ } else if (
1956
+ bufferIsEmpty &&
1957
+ playingItem &&
1958
+ !this.itemsMatch(playingItem, bufferingItem)
1959
+ ) {
1960
+ if (bufferEndIndex === playingIndex) {
1961
+ this.bufferedToItem(playingItem);
1962
+ } else if (bufferEndIndex === playingIndex + 1) {
1963
+ this.bufferedToItem(items[bufferEndIndex]);
1964
+ }
1965
+ }
1966
+ }
1967
+
1968
+ private assetsBuffered(
1969
+ item: InterstitialScheduleEventItem,
1970
+ media: HTMLMediaElement | null,
1971
+ ): boolean {
1972
+ const assetList = item.event.assetList;
1973
+ if (assetList.length === 0) {
1974
+ return false;
1975
+ }
1976
+ return !item.event.assetList.some((asset) => {
1977
+ const player = this.getAssetPlayer(asset.identifier);
1978
+ return !player?.bufferedInPlaceToEnd(media);
1979
+ });
1980
+ }
1981
+
1982
+ private setBufferingItem(
1983
+ item: InterstitialScheduleItem,
1984
+ ): InterstitialScheduleItem | null {
1985
+ const bufferingLast = this.bufferingItem;
1986
+ const schedule = this.schedule;
1987
+
1988
+ if (!this.itemsMatch(item, bufferingLast) && schedule) {
1989
+ const { items, events } = schedule;
1990
+ if (!items || !events) {
1991
+ return bufferingLast;
1992
+ }
1993
+ const isInterstitial = this.isInterstitial(item);
1994
+ const bufferingPlayer = this.getBufferingPlayer();
1995
+ this.bufferingItem = item;
1996
+ this.bufferedPos = Math.max(
1997
+ item.start,
1998
+ Math.min(item.end, this.timelinePos),
1999
+ );
2000
+ const timeRemaining = bufferingPlayer
2001
+ ? bufferingPlayer.remaining
2002
+ : bufferingLast
2003
+ ? bufferingLast.end - this.timelinePos
2004
+ : 0;
2005
+ this.log(
2006
+ `INTERSTITIALS_BUFFERED_TO_BOUNDARY ${segmentToString(item)}` +
2007
+ (bufferingLast ? ` (${timeRemaining.toFixed(2)} remaining)` : ''),
2008
+ );
2009
+ if (!this.playbackDisabled) {
2010
+ if (isInterstitial) {
2011
+ const bufferIndex = schedule.findAssetIndex(
2012
+ item.event,
2013
+ this.bufferedPos,
2014
+ );
2015
+ // primary fragment loading will exit early in base-stream-controller while `bufferingItem` is set to an Interstitial block
2016
+ item.event.assetList.forEach((asset, i) => {
2017
+ const player = this.getAssetPlayer(asset.identifier);
2018
+ if (player) {
2019
+ if (i === bufferIndex) {
2020
+ player.loadSource();
2021
+ }
2022
+ player.resumeBuffering();
2023
+ }
2024
+ });
2025
+ } else {
2026
+ this.hls.resumeBuffering();
2027
+ this.playerQueue.forEach((player) => player.pauseBuffering());
2028
+ }
2029
+ }
2030
+ this.hls.trigger(Events.INTERSTITIALS_BUFFERED_TO_BOUNDARY, {
2031
+ events: events.slice(0),
2032
+ schedule: items.slice(0),
2033
+ bufferingIndex: this.findItemIndex(item),
2034
+ playingIndex: this.findItemIndex(this.playingItem),
2035
+ });
2036
+ } else if (this.bufferingItem !== item) {
2037
+ this.bufferingItem = item;
2038
+ }
2039
+ return bufferingLast;
2040
+ }
2041
+
2042
+ private bufferedToItem(
2043
+ item: InterstitialScheduleItem,
2044
+ assetListIndex: number = 0,
2045
+ ) {
2046
+ const bufferingLast = this.setBufferingItem(item);
2047
+ if (this.playbackDisabled) {
2048
+ return;
2049
+ }
2050
+ if (this.isInterstitial(item)) {
2051
+ // Ensure asset list is loaded
2052
+ this.bufferedToEvent(item, assetListIndex);
2053
+ } else if (bufferingLast !== null) {
2054
+ // If primary player is detached, it is also stopped, restart loading at primary position
2055
+ this.bufferingAsset = null;
2056
+ const detachedData = this.detachedData;
2057
+ if (detachedData) {
2058
+ if (detachedData.mediaSource) {
2059
+ const skipSeekToStartPosition = true;
2060
+ this.attachPrimary(item.start, item, skipSeekToStartPosition);
2061
+ } else {
2062
+ this.preloadPrimary(item);
2063
+ }
2064
+ } else {
2065
+ // If not detached seek to resumption point
2066
+ this.preloadPrimary(item);
2067
+ }
2068
+ }
2069
+ }
2070
+
2071
+ private preloadPrimary(item: InterstitialSchedulePrimaryItem) {
2072
+ const index = this.findItemIndex(item);
2073
+ const timelinePos = this.getPrimaryResumption(item, index);
2074
+ this.startLoadingPrimaryAt(timelinePos);
2075
+ }
2076
+
2077
+ private bufferedToEvent(
2078
+ item: InterstitialScheduleEventItem,
2079
+ assetListIndex: number,
2080
+ ) {
2081
+ const interstitial = item.event;
2082
+ const neverLoaded =
2083
+ interstitial.assetList.length === 0 && !interstitial.assetListLoader;
2084
+ const playOnce = interstitial.cue.once;
2085
+ if (neverLoaded || !playOnce) {
2086
+ // Buffered to Interstitial boundary
2087
+ const player = this.preloadAssets(interstitial, assetListIndex);
2088
+ if (player?.interstitial.appendInPlace) {
2089
+ const media = this.primaryMedia;
2090
+ if (media) {
2091
+ this.bufferAssetPlayer(player, media);
2092
+ }
2093
+ }
2094
+ }
2095
+ }
2096
+
2097
+ private preloadAssets(
2098
+ interstitial: InterstitialEvent,
2099
+ assetListIndex: number,
2100
+ ): HlsAssetPlayer | null {
2101
+ const uri = interstitial.assetUrl;
2102
+ const assetListLength = interstitial.assetList.length;
2103
+ const neverLoaded = assetListLength === 0 && !interstitial.assetListLoader;
2104
+ const playOnce = interstitial.cue.once;
2105
+ if (neverLoaded) {
2106
+ const timelineStart = interstitial.timelineStart;
2107
+ if (interstitial.appendInPlace) {
2108
+ const playingItem = this.playingItem;
2109
+ if (
2110
+ !this.isInterstitial(playingItem) &&
2111
+ playingItem?.nextEvent?.identifier === interstitial.identifier
2112
+ ) {
2113
+ this.flushFrontBuffer(timelineStart + 0.25);
2114
+ }
2115
+ }
2116
+ let hlsStartOffset;
2117
+ let liveStartPosition = 0;
2118
+ if (!this.playingItem && this.primaryLive) {
2119
+ liveStartPosition = this.hls.startPosition;
2120
+ if (liveStartPosition === -1) {
2121
+ liveStartPosition = this.hls.liveSyncPosition || 0;
2122
+ }
2123
+ }
2124
+ if (
2125
+ liveStartPosition &&
2126
+ !(interstitial.cue.pre || interstitial.cue.post)
2127
+ ) {
2128
+ const startOffset = liveStartPosition - timelineStart;
2129
+ if (startOffset > 0) {
2130
+ hlsStartOffset = Math.round(startOffset * 1000) / 1000;
2131
+ }
2132
+ }
2133
+ this.log(
2134
+ `Load interstitial asset ${assetListIndex + 1}/${uri ? 1 : assetListLength} ${interstitial}${
2135
+ hlsStartOffset
2136
+ ? ` live-start: ${liveStartPosition} start-offset: ${hlsStartOffset}`
2137
+ : ''
2138
+ }`,
2139
+ );
2140
+ if (uri) {
2141
+ return this.createAsset(
2142
+ interstitial,
2143
+ 0,
2144
+ 0,
2145
+ timelineStart,
2146
+ interstitial.duration,
2147
+ uri,
2148
+ );
2149
+ }
2150
+ const assetListLoader = this.assetListLoader.loadAssetList(
2151
+ interstitial as InterstitialEventWithAssetList,
2152
+ hlsStartOffset,
2153
+ );
2154
+ if (assetListLoader) {
2155
+ interstitial.assetListLoader = assetListLoader;
2156
+ }
2157
+ } else if (!playOnce && assetListLength) {
2158
+ // Re-buffered to Interstitial boundary, re-create asset player(s)
2159
+ for (let i = assetListIndex; i < assetListLength; i++) {
2160
+ const asset = interstitial.assetList[i];
2161
+ const playerIndex = this.getAssetPlayerQueueIndex(asset.identifier);
2162
+ if (
2163
+ (playerIndex === -1 || this.playerQueue[playerIndex].destroyed) &&
2164
+ !asset.error
2165
+ ) {
2166
+ this.createAssetPlayer(interstitial, asset, i);
2167
+ }
2168
+ }
2169
+ const asset = interstitial.assetList[assetListIndex];
2170
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2171
+ if (asset) {
2172
+ const player = this.getAssetPlayer(asset.identifier);
2173
+ if (player) {
2174
+ player.loadSource();
2175
+ }
2176
+ return player;
2177
+ }
2178
+ }
2179
+ return null;
2180
+ }
2181
+
2182
+ private flushFrontBuffer(startOffset: number) {
2183
+ // Force queued flushing of all buffers
2184
+ const requiredTracks = this.requiredTracks;
2185
+ if (!requiredTracks) {
2186
+ return;
2187
+ }
2188
+ this.log(`Removing front buffer starting at ${startOffset}`);
2189
+ const sourceBufferNames = Object.keys(requiredTracks);
2190
+ sourceBufferNames.forEach((type: SourceBufferName) => {
2191
+ this.hls.trigger(Events.BUFFER_FLUSHING, {
2192
+ startOffset,
2193
+ endOffset: Infinity,
2194
+ type,
2195
+ });
2196
+ });
2197
+ }
2198
+
2199
+ // Interstitial Asset Player control
2200
+ private getAssetPlayerQueueIndex(assetId: InterstitialAssetId): number {
2201
+ const playerQueue = this.playerQueue;
2202
+ for (let i = 0; i < playerQueue.length; i++) {
2203
+ if (assetId === playerQueue[i].assetId) {
2204
+ return i;
2205
+ }
2206
+ }
2207
+ return -1;
2208
+ }
2209
+
2210
+ private getAssetPlayer(assetId: InterstitialAssetId): HlsAssetPlayer | null {
2211
+ const index = this.getAssetPlayerQueueIndex(assetId);
2212
+ return this.playerQueue[index] || null;
2213
+ }
2214
+
2215
+ private getBufferingPlayer(): HlsAssetPlayer | null {
2216
+ const { playerQueue, primaryMedia } = this;
2217
+ if (primaryMedia) {
2218
+ for (let i = 0; i < playerQueue.length; i++) {
2219
+ if (playerQueue[i].media === primaryMedia) {
2220
+ return playerQueue[i];
2221
+ }
2222
+ }
2223
+ }
2224
+ return null;
2225
+ }
2226
+
2227
+ private createAsset(
2228
+ interstitial: InterstitialEvent,
2229
+ assetListIndex: number,
2230
+ startOffset: number,
2231
+ timelineStart: number,
2232
+ duration: number,
2233
+ uri: string,
2234
+ ): HlsAssetPlayer {
2235
+ const assetItem: InterstitialAssetItem = {
2236
+ parentIdentifier: interstitial.identifier,
2237
+ identifier: generateAssetIdentifier(interstitial, uri, assetListIndex),
2238
+ duration,
2239
+ startOffset,
2240
+ timelineStart,
2241
+ uri,
2242
+ };
2243
+ return this.createAssetPlayer(interstitial, assetItem, assetListIndex);
2244
+ }
2245
+
2246
+ private createAssetPlayer(
2247
+ interstitial: InterstitialEvent,
2248
+ assetItem: InterstitialAssetItem,
2249
+ assetListIndex: number,
2250
+ ): HlsAssetPlayer {
2251
+ const primary = this.hls;
2252
+ const userConfig = primary.userConfig;
2253
+ let videoPreference = userConfig.videoPreference;
2254
+ const currentLevel =
2255
+ primary.loadLevelObj || primary.levels[primary.currentLevel];
2256
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2257
+ if (videoPreference || currentLevel) {
2258
+ videoPreference = Object.assign({}, videoPreference);
2259
+ if (currentLevel.videoCodec) {
2260
+ videoPreference.videoCodec = currentLevel.videoCodec;
2261
+ }
2262
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2263
+ if (currentLevel.videoRange) {
2264
+ videoPreference.allowedVideoRanges = [currentLevel.videoRange];
2265
+ }
2266
+ }
2267
+ const selectedAudio = primary.audioTracks[primary.audioTrack];
2268
+ const selectedSubtitle = primary.subtitleTracks[primary.subtitleTrack];
2269
+ let startPosition = 0;
2270
+ if (this.primaryLive || interstitial.appendInPlace) {
2271
+ const timePastStart = this.timelinePos - assetItem.timelineStart;
2272
+ if (timePastStart > 1) {
2273
+ const duration = assetItem.duration;
2274
+ if (duration && timePastStart < duration) {
2275
+ startPosition = timePastStart;
2276
+ }
2277
+ }
2278
+ }
2279
+ const assetId = assetItem.identifier;
2280
+ const playerConfig: HlsAssetPlayerConfig = {
2281
+ ...userConfig,
2282
+ maxMaxBufferLength: Math.min(180, primary.config.maxMaxBufferLength),
2283
+ autoStartLoad: true,
2284
+ startFragPrefetch: true,
2285
+ primarySessionId: primary.sessionId,
2286
+ assetPlayerId: assetId,
2287
+ abrEwmaDefaultEstimate: primary.bandwidthEstimate,
2288
+ interstitialsController: undefined,
2289
+ startPosition,
2290
+ liveDurationInfinity: false,
2291
+ testBandwidth: false,
2292
+ videoPreference,
2293
+ audioPreference:
2294
+ (selectedAudio as MediaPlaylist | undefined) ||
2295
+ userConfig.audioPreference,
2296
+ subtitlePreference:
2297
+ (selectedSubtitle as MediaPlaylist | undefined) ||
2298
+ userConfig.subtitlePreference,
2299
+ };
2300
+ // TODO: limit maxMaxBufferLength in asset players to prevent QEE
2301
+ if (interstitial.appendInPlace) {
2302
+ interstitial.appendInPlaceStarted = true;
2303
+ if (assetItem.timelineStart) {
2304
+ playerConfig.timelineOffset = assetItem.timelineStart;
2305
+ }
2306
+ }
2307
+ const cmcd = playerConfig.cmcd;
2308
+ if (cmcd?.sessionId && cmcd.contentId) {
2309
+ playerConfig.cmcd = Object.assign({}, cmcd, {
2310
+ contentId: hash(assetItem.uri),
2311
+ });
2312
+ }
2313
+ if (this.getAssetPlayer(assetId)) {
2314
+ this.warn(
2315
+ `Duplicate date range identifier ${interstitial} and asset ${assetId}`,
2316
+ );
2317
+ }
2318
+ const player = new HlsAssetPlayer(
2319
+ this.HlsPlayerClass,
2320
+ playerConfig,
2321
+ interstitial,
2322
+ assetItem,
2323
+ );
2324
+ this.playerQueue.push(player);
2325
+ interstitial.assetList[assetListIndex] = assetItem;
2326
+ // Listen for LevelDetails and PTS change to update duration
2327
+ let initialDuration = true;
2328
+ const updateAssetPlayerDetails = (details: LevelDetails) => {
2329
+ if (details.live) {
2330
+ const error = new Error(
2331
+ `Interstitials MUST be VOD assets ${interstitial}`,
2332
+ );
2333
+ const errorData: ErrorData = {
2334
+ fatal: true,
2335
+ type: ErrorTypes.OTHER_ERROR,
2336
+ details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR,
2337
+ error,
2338
+ };
2339
+ const scheduleIndex =
2340
+ this.schedule?.findEventIndex(interstitial.identifier) || -1;
2341
+ this.handleAssetItemError(
2342
+ errorData,
2343
+ interstitial,
2344
+ scheduleIndex,
2345
+ assetListIndex,
2346
+ error.message,
2347
+ );
2348
+ return;
2349
+ }
2350
+ // Get time at end of last fragment
2351
+ const duration = details.edge - details.fragmentStart;
2352
+ const currentAssetDuration = assetItem.duration;
2353
+ if (
2354
+ initialDuration ||
2355
+ currentAssetDuration === null ||
2356
+ duration > currentAssetDuration
2357
+ ) {
2358
+ initialDuration = false;
2359
+ this.log(
2360
+ `Interstitial asset "${assetId}" duration change ${currentAssetDuration} > ${duration}`,
2361
+ );
2362
+ assetItem.duration = duration;
2363
+ // Update schedule with new event and asset duration
2364
+ this.updateSchedule();
2365
+ }
2366
+ };
2367
+ player.on(Events.LEVEL_UPDATED, (event, { details }) =>
2368
+ updateAssetPlayerDetails(details),
2369
+ );
2370
+ player.on(Events.LEVEL_PTS_UPDATED, (event, { details }) =>
2371
+ updateAssetPlayerDetails(details),
2372
+ );
2373
+ player.on(Events.EVENT_CUE_ENTER, () => this.onInterstitialCueEnter());
2374
+ const onBufferCodecs = (
2375
+ event: Events.BUFFER_CODECS,
2376
+ data: BufferCodecsData,
2377
+ ) => {
2378
+ const inQueuPlayer = this.getAssetPlayer(assetId);
2379
+ if (inQueuPlayer && data.tracks) {
2380
+ inQueuPlayer.off(Events.BUFFER_CODECS, onBufferCodecs);
2381
+ inQueuPlayer.tracks = data.tracks;
2382
+ const media = this.primaryMedia;
2383
+ if (
2384
+ this.bufferingAsset === inQueuPlayer.assetItem &&
2385
+ media &&
2386
+ !inQueuPlayer.media
2387
+ ) {
2388
+ this.bufferAssetPlayer(inQueuPlayer, media);
2389
+ }
2390
+ }
2391
+ };
2392
+ player.on(Events.BUFFER_CODECS, onBufferCodecs);
2393
+ const bufferedToEnd = () => {
2394
+ const inQueuPlayer = this.getAssetPlayer(assetId);
2395
+ this.log(`buffered to end of asset ${inQueuPlayer}`);
2396
+ if (!inQueuPlayer || !this.schedule) {
2397
+ return;
2398
+ }
2399
+ // Preload at end of asset
2400
+ const scheduleIndex = this.schedule.findEventIndex(
2401
+ interstitial.identifier,
2402
+ );
2403
+ const item = this.schedule.items?.[scheduleIndex];
2404
+ if (this.isInterstitial(item)) {
2405
+ this.advanceAssetBuffering(item, assetItem);
2406
+ }
2407
+ };
2408
+ player.on(Events.BUFFERED_TO_END, bufferedToEnd);
2409
+ const endedWithAssetIndex = (assetIndex) => {
2410
+ return () => {
2411
+ const inQueuPlayer = this.getAssetPlayer(assetId);
2412
+ if (!inQueuPlayer || !this.schedule) {
2413
+ return;
2414
+ }
2415
+ this.shouldPlay = true;
2416
+ const scheduleIndex = this.schedule.findEventIndex(
2417
+ interstitial.identifier,
2418
+ );
2419
+ this.advanceAfterAssetEnded(interstitial, scheduleIndex, assetIndex);
2420
+ };
2421
+ };
2422
+ player.once(Events.MEDIA_ENDED, endedWithAssetIndex(assetListIndex));
2423
+ player.once(Events.PLAYOUT_LIMIT_REACHED, endedWithAssetIndex(Infinity));
2424
+ player.on(Events.ERROR, (event: Events.ERROR, data: ErrorData) => {
2425
+ if (!this.schedule) {
2426
+ return;
2427
+ }
2428
+ const inQueuPlayer = this.getAssetPlayer(assetId);
2429
+ if (data.details === ErrorDetails.BUFFER_STALLED_ERROR) {
2430
+ if (inQueuPlayer?.appendInPlace) {
2431
+ this.handleInPlaceStall(interstitial);
2432
+ return;
2433
+ }
2434
+ this.onTimeupdate();
2435
+ this.checkBuffer(true);
2436
+ return;
2437
+ }
2438
+ this.handleAssetItemError(
2439
+ data,
2440
+ interstitial,
2441
+ this.schedule.findEventIndex(interstitial.identifier),
2442
+ assetListIndex,
2443
+ `Asset player error ${data.error} ${interstitial}`,
2444
+ );
2445
+ });
2446
+ player.on(Events.DESTROYING, () => {
2447
+ const inQueuPlayer = this.getAssetPlayer(assetId);
2448
+ if (!inQueuPlayer || !this.schedule) {
2449
+ return;
2450
+ }
2451
+ const error = new Error(`Asset player destroyed unexpectedly ${assetId}`);
2452
+ const errorData: ErrorData = {
2453
+ fatal: true,
2454
+ type: ErrorTypes.OTHER_ERROR,
2455
+ details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR,
2456
+ error,
2457
+ };
2458
+ this.handleAssetItemError(
2459
+ errorData,
2460
+ interstitial,
2461
+ this.schedule.findEventIndex(interstitial.identifier),
2462
+ assetListIndex,
2463
+ error.message,
2464
+ );
2465
+ });
2466
+ this.log(
2467
+ `INTERSTITIAL_ASSET_PLAYER_CREATED ${eventAssetToString(assetItem)}`,
2468
+ );
2469
+ this.hls.trigger(Events.INTERSTITIAL_ASSET_PLAYER_CREATED, {
2470
+ asset: assetItem,
2471
+ assetListIndex,
2472
+ event: interstitial,
2473
+ player,
2474
+ });
2475
+ return player;
2476
+ }
2477
+
2478
+ private clearInterstitial(
2479
+ interstitial: InterstitialEvent,
2480
+ toSegment: InterstitialScheduleItem | null,
2481
+ ) {
2482
+ this.clearAssetPlayers(interstitial, toSegment);
2483
+ // Remove asset list and resolved duration
2484
+ interstitial.reset();
2485
+ }
2486
+
2487
+ private clearAssetPlayers(
2488
+ interstitial: InterstitialEvent,
2489
+ toSegment: InterstitialScheduleItem | null,
2490
+ ) {
2491
+ interstitial.assetList.forEach((asset) => {
2492
+ this.clearAssetPlayer(asset.identifier, toSegment);
2493
+ });
2494
+ }
2495
+
2496
+ private resetAssetPlayer(assetId: InterstitialAssetId) {
2497
+ // Reset asset player so that it's timeline can be adjusted without reloading the MVP
2498
+ const playerIndex = this.getAssetPlayerQueueIndex(assetId);
2499
+ if (playerIndex !== -1) {
2500
+ this.log(`reset asset player "${assetId}" after error`);
2501
+ const player = this.playerQueue[playerIndex];
2502
+ this.transferMediaFromPlayer(player, null);
2503
+ player.resetDetails();
2504
+ }
2505
+ }
2506
+
2507
+ private clearAssetPlayer(
2508
+ assetId: InterstitialAssetId,
2509
+ toSegment: InterstitialScheduleItem | null,
2510
+ ) {
2511
+ const playerIndex = this.getAssetPlayerQueueIndex(assetId);
2512
+ if (playerIndex !== -1) {
2513
+ const player = this.playerQueue[playerIndex];
2514
+ this.log(
2515
+ `clear ${player} toSegment: ${toSegment ? segmentToString(toSegment) : toSegment}`,
2516
+ );
2517
+ this.transferMediaFromPlayer(player, toSegment);
2518
+ this.playerQueue.splice(playerIndex, 1);
2519
+ player.destroy();
2520
+ }
2521
+ }
2522
+
2523
+ private emptyPlayerQueue() {
2524
+ let player: HlsAssetPlayer | undefined;
2525
+ while ((player = this.playerQueue.pop())) {
2526
+ player.destroy();
2527
+ }
2528
+ this.playerQueue = [];
2529
+ }
2530
+
2531
+ private startAssetPlayer(
2532
+ player: HlsAssetPlayer,
2533
+ assetListIndex: number,
2534
+ scheduleItems: InterstitialScheduleItem[],
2535
+ scheduleIndex: number,
2536
+ media: HTMLMediaElement,
2537
+ ) {
2538
+ const { interstitial, assetItem, assetId } = player;
2539
+ const assetListLength = interstitial.assetList.length;
2540
+
2541
+ const playingAsset = this.playingAsset;
2542
+ this.endedAsset = null;
2543
+ this.playingAsset = assetItem;
2544
+ if (playingAsset?.identifier !== assetId) {
2545
+ if (playingAsset) {
2546
+ // Exiting another Interstitial asset
2547
+ this.clearAssetPlayer(
2548
+ playingAsset.identifier,
2549
+ scheduleItems[scheduleIndex],
2550
+ );
2551
+ delete playingAsset.error;
2552
+ }
2553
+ this.log(
2554
+ `INTERSTITIAL_ASSET_STARTED ${assetListIndex + 1}/${assetListLength} ${eventAssetToString(assetItem)}`,
2555
+ );
2556
+ this.hls.trigger(Events.INTERSTITIAL_ASSET_STARTED, {
2557
+ asset: assetItem,
2558
+ assetListIndex,
2559
+ event: interstitial,
2560
+ schedule: scheduleItems.slice(0),
2561
+ scheduleIndex,
2562
+ player,
2563
+ });
2564
+ }
2565
+
2566
+ // detach media and attach to interstitial player if it does not have another element attached
2567
+ this.bufferAssetPlayer(player, media);
2568
+ }
2569
+
2570
+ private bufferAssetPlayer(player: HlsAssetPlayer, media: HTMLMediaElement) {
2571
+ if (!this.schedule) {
2572
+ return;
2573
+ }
2574
+ const { interstitial, assetItem } = player;
2575
+ const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier);
2576
+ const item = this.schedule.items?.[scheduleIndex];
2577
+ if (!item) {
2578
+ return;
2579
+ }
2580
+ player.loadSource();
2581
+ this.setBufferingItem(item);
2582
+ this.bufferingAsset = assetItem;
2583
+ const bufferingPlayer = this.getBufferingPlayer();
2584
+ if (bufferingPlayer === player) {
2585
+ return;
2586
+ }
2587
+ const appendInPlaceNext = interstitial.appendInPlace;
2588
+ if (
2589
+ appendInPlaceNext &&
2590
+ bufferingPlayer?.interstitial.appendInPlace === false
2591
+ ) {
2592
+ // Media is detached and not available to append in place
2593
+ return;
2594
+ }
2595
+ const activeTracks =
2596
+ bufferingPlayer?.tracks ||
2597
+ this.detachedData?.tracks ||
2598
+ this.requiredTracks;
2599
+ if (appendInPlaceNext && assetItem !== this.playingAsset) {
2600
+ // Do not buffer another item if tracks are unknown or incompatible
2601
+ if (!player.tracks) {
2602
+ this.log(`Waiting for track info before buffering ${player}`);
2603
+ return;
2604
+ }
2605
+ if (
2606
+ activeTracks &&
2607
+ !isCompatibleTrackChange(activeTracks, player.tracks)
2608
+ ) {
2609
+ const error = new Error(
2610
+ `Asset ${eventAssetToString(assetItem)} SourceBuffer tracks ('${Object.keys(player.tracks)}') are not compatible with primary content tracks ('${Object.keys(activeTracks)}')`,
2611
+ );
2612
+ const errorData: ErrorData = {
2613
+ fatal: true,
2614
+ type: ErrorTypes.OTHER_ERROR,
2615
+ details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR,
2616
+ error,
2617
+ };
2618
+ const assetListIndex = interstitial.findAssetIndex(assetItem);
2619
+ this.handleAssetItemError(
2620
+ errorData,
2621
+ interstitial,
2622
+ scheduleIndex,
2623
+ assetListIndex,
2624
+ error.message,
2625
+ );
2626
+ return;
2627
+ }
2628
+ }
2629
+
2630
+ this.transferMediaTo(player, media);
2631
+ }
2632
+
2633
+ private handleInPlaceStall(interstitial: InterstitialEvent) {
2634
+ const schedule = this.schedule;
2635
+ const media = this.primaryMedia;
2636
+ if (!schedule || !media) {
2637
+ return;
2638
+ }
2639
+ const currentTime = media.currentTime;
2640
+ const foundAssetIndex = schedule.findAssetIndex(interstitial, currentTime);
2641
+ const stallingAsset = interstitial.assetList[foundAssetIndex] as
2642
+ | InterstitialAssetItem
2643
+ | undefined;
2644
+ if (stallingAsset) {
2645
+ const player = this.getAssetPlayer(stallingAsset.identifier);
2646
+ if (player) {
2647
+ const assetCurrentTime =
2648
+ player.currentTime || currentTime - stallingAsset.timelineStart;
2649
+ const distanceFromEnd = player.duration - assetCurrentTime;
2650
+ this.warn(
2651
+ `Stalled at ${assetCurrentTime} of ${assetCurrentTime + distanceFromEnd} in ${player} ${interstitial} (media.currentTime: ${currentTime})`,
2652
+ );
2653
+ if (
2654
+ assetCurrentTime &&
2655
+ (distanceFromEnd / media.playbackRate < 0.5 ||
2656
+ player.bufferedInPlaceToEnd(media)) &&
2657
+ player.hls
2658
+ ) {
2659
+ const scheduleIndex = schedule.findEventIndex(
2660
+ interstitial.identifier,
2661
+ );
2662
+ this.advanceAfterAssetEnded(
2663
+ interstitial,
2664
+ scheduleIndex,
2665
+ foundAssetIndex,
2666
+ );
2667
+ }
2668
+ }
2669
+ }
2670
+ }
2671
+
2672
+ private advanceInPlace(time: number) {
2673
+ const media = this.primaryMedia;
2674
+ if (media && media.currentTime < time) {
2675
+ media.currentTime = time;
2676
+ }
2677
+ }
2678
+
2679
+ private handleAssetItemError(
2680
+ data: ErrorData,
2681
+ interstitial: InterstitialEvent,
2682
+ scheduleIndex: number,
2683
+ assetListIndex: number,
2684
+ errorMessage: string,
2685
+ ) {
2686
+ if (data.details === ErrorDetails.BUFFER_STALLED_ERROR) {
2687
+ return;
2688
+ }
2689
+ const assetItem = (interstitial.assetList[assetListIndex] ||
2690
+ null) as InterstitialAssetItem | null;
2691
+ this.warn(
2692
+ `INTERSTITIAL_ASSET_ERROR ${assetItem ? eventAssetToString(assetItem) : assetItem} ${data.error}`,
2693
+ );
2694
+ if (!this.schedule) {
2695
+ return;
2696
+ }
2697
+ const assetId = assetItem?.identifier || '';
2698
+ const playerIndex = this.getAssetPlayerQueueIndex(assetId);
2699
+ const player = this.playerQueue[playerIndex] || null;
2700
+ const items = this.schedule.items;
2701
+ const interstitialAssetError = Object.assign({}, data, {
2702
+ fatal: false,
2703
+ errorAction: createDoNothingErrorAction(true),
2704
+ asset: assetItem,
2705
+ assetListIndex,
2706
+ event: interstitial,
2707
+ schedule: items,
2708
+ scheduleIndex,
2709
+ player,
2710
+ });
2711
+ this.hls.trigger(Events.INTERSTITIAL_ASSET_ERROR, interstitialAssetError);
2712
+ if (!data.fatal) {
2713
+ return;
2714
+ }
2715
+
2716
+ const playingAsset = this.playingAsset;
2717
+ const bufferingAsset = this.bufferingAsset;
2718
+ const error = new Error(errorMessage);
2719
+ if (assetItem) {
2720
+ this.clearAssetPlayer(assetId, null);
2721
+ assetItem.error = error;
2722
+ }
2723
+
2724
+ // If all assets in interstitial fail, mark the interstitial with an error
2725
+ if (!interstitial.assetList.some((asset) => !asset.error)) {
2726
+ interstitial.error = error;
2727
+ } else {
2728
+ // Reset level details and reload/parse media playlists to align with updated schedule
2729
+ for (let i = assetListIndex; i < interstitial.assetList.length; i++) {
2730
+ this.resetAssetPlayer(interstitial.assetList[i].identifier);
2731
+ }
2732
+ }
2733
+ this.updateSchedule(true);
2734
+ if (interstitial.error) {
2735
+ this.primaryFallback(interstitial);
2736
+ } else if (playingAsset?.identifier === assetId) {
2737
+ this.advanceAfterAssetEnded(interstitial, scheduleIndex, assetListIndex);
2738
+ } else if (
2739
+ bufferingAsset?.identifier === assetId &&
2740
+ this.isInterstitial(this.bufferingItem)
2741
+ ) {
2742
+ this.advanceAssetBuffering(this.bufferingItem, bufferingAsset);
2743
+ }
2744
+ }
2745
+
2746
+ private primaryFallback(interstitial: InterstitialEvent) {
2747
+ // Fallback to Primary by on current or future events by updating schedule to skip errored interstitials/assets
2748
+ const flushStart = interstitial.timelineStart;
2749
+ const playingItem = this.effectivePlayingItem;
2750
+ let timelinePos = this.timelinePos;
2751
+ // Update schedule now that interstitial/assets are flagged with `error` for fallback
2752
+ if (playingItem) {
2753
+ this.log(
2754
+ `Fallback to primary from event "${interstitial.identifier}" start: ${
2755
+ flushStart
2756
+ } pos: ${timelinePos} playing: ${segmentToString(
2757
+ playingItem,
2758
+ )} error: ${interstitial.error}`,
2759
+ );
2760
+ if (timelinePos === -1) {
2761
+ timelinePos = this.hls.startPosition;
2762
+ }
2763
+ const newPlayingItem = this.updateItem(playingItem, timelinePos);
2764
+ if (this.itemsMatch(playingItem, newPlayingItem)) {
2765
+ this.clearInterstitial(interstitial, null);
2766
+ }
2767
+ if (interstitial.appendInPlace) {
2768
+ this.attachPrimary(flushStart, null);
2769
+ this.flushFrontBuffer(flushStart);
2770
+ }
2771
+ } else if (timelinePos === -1) {
2772
+ this.checkStart();
2773
+ return;
2774
+ }
2775
+ if (!this.schedule) {
2776
+ return;
2777
+ }
2778
+ const scheduleIndex = this.schedule.findItemIndexAtTime(timelinePos);
2779
+ this.setSchedulePosition(scheduleIndex);
2780
+ }
2781
+
2782
+ // Asset List loading
2783
+ private onAssetListLoaded(
2784
+ event: Events.ASSET_LIST_LOADED,
2785
+ data: AssetListLoadedData,
2786
+ ) {
2787
+ const interstitial = data.event;
2788
+ const interstitialId = interstitial.identifier;
2789
+ const assets = data.assetListResponse.ASSETS;
2790
+ if (!this.schedule?.hasEvent(interstitialId)) {
2791
+ // Interstitial with id was removed
2792
+ return;
2793
+ }
2794
+ const eventStart = interstitial.timelineStart;
2795
+ const previousDuration = interstitial.duration;
2796
+ let sumDuration = 0;
2797
+ assets.forEach((asset, assetListIndex) => {
2798
+ const duration = parseFloat(asset.DURATION);
2799
+ this.createAsset(
2800
+ interstitial,
2801
+ assetListIndex,
2802
+ sumDuration,
2803
+ eventStart + sumDuration,
2804
+ duration,
2805
+ asset.URI,
2806
+ );
2807
+ sumDuration += duration;
2808
+ });
2809
+ interstitial.duration = sumDuration;
2810
+ this.log(
2811
+ `Loaded asset-list with duration: ${sumDuration} (was: ${previousDuration}) ${interstitial}`,
2812
+ );
2813
+ const waitingItem = this.waitingItem;
2814
+ const waitingForItem = waitingItem?.event.identifier === interstitialId;
2815
+
2816
+ // Update schedule now that asset.DURATION(s) are parsed
2817
+ this.updateSchedule();
2818
+
2819
+ const bufferingEvent = this.bufferingItem?.event;
2820
+
2821
+ // If buffer reached Interstitial, start buffering first asset
2822
+ if (waitingForItem) {
2823
+ // Advance schedule when waiting for asset list data to play
2824
+ const scheduleIndex = this.schedule.findEventIndex(interstitialId);
2825
+ const item = this.schedule.items?.[scheduleIndex];
2826
+ if (item) {
2827
+ if (!this.playingItem && this.timelinePos > item.end) {
2828
+ // Abandon if new duration is reduced enough to land playback in primary start
2829
+ const index = this.schedule.findItemIndexAtTime(this.timelinePos);
2830
+ if (index !== scheduleIndex) {
2831
+ interstitial.error = new Error(
2832
+ `Interstitial ${assets.length ? 'no longer within playback range' : 'asset-list is empty'} ${this.timelinePos} ${interstitial}`,
2833
+ );
2834
+ this.log(interstitial.error.message);
2835
+ this.updateSchedule(true);
2836
+ this.primaryFallback(interstitial);
2837
+ return;
2838
+ }
2839
+ }
2840
+ this.setBufferingItem(item);
2841
+ }
2842
+ this.setSchedulePosition(scheduleIndex);
2843
+ } else if (bufferingEvent?.identifier === interstitialId) {
2844
+ const assetItem = interstitial.assetList[0];
2845
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2846
+ if (assetItem) {
2847
+ const player = this.getAssetPlayer(assetItem.identifier);
2848
+ if (bufferingEvent.appendInPlace) {
2849
+ // If buffering (but not playback) has reached this item transfer media-source
2850
+ const media = this.primaryMedia;
2851
+ if (player && media) {
2852
+ this.bufferAssetPlayer(player, media);
2853
+ }
2854
+ } else if (player) {
2855
+ player.loadSource();
2856
+ }
2857
+ }
2858
+ }
2859
+ }
2860
+
2861
+ private onError(event: Events.ERROR, data: ErrorData) {
2862
+ if (!this.schedule) {
2863
+ return;
2864
+ }
2865
+ switch (data.details) {
2866
+ case ErrorDetails.ASSET_LIST_PARSING_ERROR:
2867
+ case ErrorDetails.ASSET_LIST_LOAD_ERROR:
2868
+ case ErrorDetails.ASSET_LIST_LOAD_TIMEOUT: {
2869
+ const interstitial = data.interstitial;
2870
+ if (interstitial) {
2871
+ this.updateSchedule(true);
2872
+ this.primaryFallback(interstitial);
2873
+ }
2874
+ break;
2875
+ }
2876
+ case ErrorDetails.BUFFER_STALLED_ERROR: {
2877
+ const stallingItem =
2878
+ this.endedItem || this.waitingItem || this.playingItem;
2879
+ if (
2880
+ this.isInterstitial(stallingItem) &&
2881
+ stallingItem.event.appendInPlace
2882
+ ) {
2883
+ this.handleInPlaceStall(stallingItem.event);
2884
+ return;
2885
+ }
2886
+ this.log(
2887
+ `Primary player stall @${this.timelinePos} bufferedPos: ${this.bufferedPos}`,
2888
+ );
2889
+ this.onTimeupdate();
2890
+ this.checkBuffer(true);
2891
+ break;
2892
+ }
2893
+ }
2894
+ }
2895
+ }