@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.
- package/LICENSE +28 -0
- package/README.md +472 -0
- package/dist/hls-demo.js +26995 -0
- package/dist/hls-demo.js.map +1 -0
- package/dist/hls.d.mts +4204 -0
- package/dist/hls.d.ts +4204 -0
- package/dist/hls.js +40050 -0
- package/dist/hls.js.d.ts +4204 -0
- package/dist/hls.js.map +1 -0
- package/dist/hls.light.js +27145 -0
- package/dist/hls.light.js.map +1 -0
- package/dist/hls.light.min.js +2 -0
- package/dist/hls.light.min.js.map +1 -0
- package/dist/hls.light.mjs +26392 -0
- package/dist/hls.light.mjs.map +1 -0
- package/dist/hls.min.js +2 -0
- package/dist/hls.min.js.map +1 -0
- package/dist/hls.mjs +38956 -0
- package/dist/hls.mjs.map +1 -0
- package/dist/hls.worker.js +2 -0
- package/dist/hls.worker.js.map +1 -0
- package/package.json +143 -0
- package/src/config.ts +794 -0
- package/src/controller/abr-controller.ts +1019 -0
- package/src/controller/algo-data-controller.ts +794 -0
- package/src/controller/audio-stream-controller.ts +1099 -0
- package/src/controller/audio-track-controller.ts +454 -0
- package/src/controller/base-playlist-controller.ts +438 -0
- package/src/controller/base-stream-controller.ts +2526 -0
- package/src/controller/buffer-controller.ts +2015 -0
- package/src/controller/buffer-operation-queue.ts +159 -0
- package/src/controller/cap-level-controller.ts +367 -0
- package/src/controller/cmcd-controller.ts +422 -0
- package/src/controller/content-steering-controller.ts +622 -0
- package/src/controller/eme-controller.ts +1617 -0
- package/src/controller/error-controller.ts +627 -0
- package/src/controller/fps-controller.ts +146 -0
- package/src/controller/fragment-finders.ts +256 -0
- package/src/controller/fragment-tracker.ts +567 -0
- package/src/controller/gap-controller.ts +719 -0
- package/src/controller/id3-track-controller.ts +488 -0
- package/src/controller/interstitial-player.ts +302 -0
- package/src/controller/interstitials-controller.ts +2895 -0
- package/src/controller/interstitials-schedule.ts +698 -0
- package/src/controller/latency-controller.ts +294 -0
- package/src/controller/level-controller.ts +776 -0
- package/src/controller/stream-controller.ts +1597 -0
- package/src/controller/subtitle-stream-controller.ts +508 -0
- package/src/controller/subtitle-track-controller.ts +617 -0
- package/src/controller/timeline-controller.ts +677 -0
- package/src/crypt/aes-crypto.ts +36 -0
- package/src/crypt/aes-decryptor.ts +339 -0
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +225 -0
- package/src/crypt/fast-aes-key.ts +39 -0
- package/src/define-plugin.d.ts +17 -0
- package/src/demux/audio/aacdemuxer.ts +126 -0
- package/src/demux/audio/ac3-demuxer.ts +170 -0
- package/src/demux/audio/adts.ts +249 -0
- package/src/demux/audio/base-audio-demuxer.ts +205 -0
- package/src/demux/audio/dolby.ts +21 -0
- package/src/demux/audio/mp3demuxer.ts +85 -0
- package/src/demux/audio/mpegaudio.ts +177 -0
- package/src/demux/chunk-cache.ts +42 -0
- package/src/demux/dummy-demuxed-track.ts +13 -0
- package/src/demux/inject-worker.ts +75 -0
- package/src/demux/mp4demuxer.ts +234 -0
- package/src/demux/sample-aes.ts +198 -0
- package/src/demux/transmuxer-interface.ts +449 -0
- package/src/demux/transmuxer-worker.ts +221 -0
- package/src/demux/transmuxer.ts +560 -0
- package/src/demux/tsdemuxer.ts +1256 -0
- package/src/demux/video/avc-video-parser.ts +401 -0
- package/src/demux/video/base-video-parser.ts +198 -0
- package/src/demux/video/exp-golomb.ts +153 -0
- package/src/demux/video/hevc-video-parser.ts +736 -0
- package/src/empty-es.js +5 -0
- package/src/empty.js +3 -0
- package/src/errors.ts +107 -0
- package/src/events.ts +548 -0
- package/src/exports-default.ts +3 -0
- package/src/exports-named.ts +81 -0
- package/src/hls.ts +1613 -0
- package/src/is-supported.ts +54 -0
- package/src/loader/date-range.ts +207 -0
- package/src/loader/fragment-loader.ts +403 -0
- package/src/loader/fragment.ts +487 -0
- package/src/loader/interstitial-asset-list.ts +162 -0
- package/src/loader/interstitial-event.ts +337 -0
- package/src/loader/key-loader.ts +439 -0
- package/src/loader/level-details.ts +203 -0
- package/src/loader/level-key.ts +259 -0
- package/src/loader/load-stats.ts +17 -0
- package/src/loader/m3u8-parser.ts +1072 -0
- package/src/loader/playlist-loader.ts +839 -0
- package/src/polyfills/number.ts +15 -0
- package/src/remux/aac-helper.ts +81 -0
- package/src/remux/mp4-generator.ts +1380 -0
- package/src/remux/mp4-remuxer.ts +1261 -0
- package/src/remux/passthrough-remuxer.ts +434 -0
- package/src/task-loop.ts +130 -0
- package/src/types/algo.ts +44 -0
- package/src/types/buffer.ts +105 -0
- package/src/types/component-api.ts +20 -0
- package/src/types/demuxer.ts +208 -0
- package/src/types/events.ts +574 -0
- package/src/types/fragment-tracker.ts +23 -0
- package/src/types/level.ts +268 -0
- package/src/types/loader.ts +198 -0
- package/src/types/media-playlist.ts +92 -0
- package/src/types/network-details.ts +3 -0
- package/src/types/remuxer.ts +104 -0
- package/src/types/track.ts +12 -0
- package/src/types/transmuxer.ts +46 -0
- package/src/types/tuples.ts +6 -0
- package/src/types/vtt.ts +11 -0
- package/src/utils/arrays.ts +22 -0
- package/src/utils/attr-list.ts +192 -0
- package/src/utils/binary-search.ts +46 -0
- package/src/utils/buffer-helper.ts +173 -0
- package/src/utils/cea-608-parser.ts +1413 -0
- package/src/utils/chunker.ts +41 -0
- package/src/utils/codecs.ts +314 -0
- package/src/utils/cues.ts +96 -0
- package/src/utils/discontinuities.ts +174 -0
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/error-helper.ts +95 -0
- package/src/utils/event-listener-helper.ts +16 -0
- package/src/utils/ewma-bandwidth-estimator.ts +97 -0
- package/src/utils/ewma.ts +43 -0
- package/src/utils/fetch-loader.ts +331 -0
- package/src/utils/global.ts +2 -0
- package/src/utils/hash.ts +10 -0
- package/src/utils/hdr.ts +67 -0
- package/src/utils/hex.ts +32 -0
- package/src/utils/imsc1-ttml-parser.ts +261 -0
- package/src/utils/keysystem-util.ts +45 -0
- package/src/utils/level-helper.ts +629 -0
- package/src/utils/logger.ts +120 -0
- package/src/utils/media-option-attributes.ts +49 -0
- package/src/utils/mediacapabilities-helper.ts +301 -0
- package/src/utils/mediakeys-helper.ts +210 -0
- package/src/utils/mediasource-helper.ts +37 -0
- package/src/utils/mp4-tools.ts +1473 -0
- package/src/utils/number.ts +3 -0
- package/src/utils/numeric-encoding-utils.ts +26 -0
- package/src/utils/output-filter.ts +46 -0
- package/src/utils/rendition-helper.ts +505 -0
- package/src/utils/safe-json-stringify.ts +22 -0
- package/src/utils/texttrack-utils.ts +164 -0
- package/src/utils/time-ranges.ts +17 -0
- package/src/utils/timescale-conversion.ts +46 -0
- package/src/utils/utf8-utils.ts +18 -0
- package/src/utils/variable-substitution.ts +105 -0
- package/src/utils/vttcue.ts +384 -0
- package/src/utils/vttparser.ts +497 -0
- package/src/utils/webvtt-parser.ts +166 -0
- package/src/utils/xhr-loader.ts +337 -0
- 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
|
+
}
|