@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,698 @@
|
|
|
1
|
+
import { findFragmentByPTS } from './fragment-finders';
|
|
2
|
+
import {
|
|
3
|
+
ALIGNED_END_THRESHOLD_SECONDS,
|
|
4
|
+
type BaseData,
|
|
5
|
+
InterstitialEvent,
|
|
6
|
+
type InterstitialId,
|
|
7
|
+
TimelineOccupancy,
|
|
8
|
+
} from '../loader/interstitial-event';
|
|
9
|
+
import { Logger } from '../utils/logger';
|
|
10
|
+
import type { DateRange } from '../loader/date-range';
|
|
11
|
+
import type { MediaSelection } from '../types/media-playlist';
|
|
12
|
+
import type { ILogger } from '../utils/logger';
|
|
13
|
+
|
|
14
|
+
const ABUTTING_THRESHOLD_SECONDS = 0.033;
|
|
15
|
+
|
|
16
|
+
export type InterstitialScheduleEventItem = {
|
|
17
|
+
event: InterstitialEvent;
|
|
18
|
+
start: number;
|
|
19
|
+
end: number;
|
|
20
|
+
playout: {
|
|
21
|
+
start: number;
|
|
22
|
+
end: number;
|
|
23
|
+
};
|
|
24
|
+
integrated: {
|
|
25
|
+
start: number;
|
|
26
|
+
end: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type InterstitialSchedulePrimaryItem = {
|
|
31
|
+
nextEvent: InterstitialEvent | null;
|
|
32
|
+
previousEvent: InterstitialEvent | null;
|
|
33
|
+
event?: undefined;
|
|
34
|
+
start: number;
|
|
35
|
+
end: number;
|
|
36
|
+
playout: {
|
|
37
|
+
start: number;
|
|
38
|
+
end: number;
|
|
39
|
+
};
|
|
40
|
+
integrated: {
|
|
41
|
+
start: number;
|
|
42
|
+
end: number;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type InterstitialScheduleItem =
|
|
47
|
+
| InterstitialScheduleEventItem
|
|
48
|
+
| InterstitialSchedulePrimaryItem;
|
|
49
|
+
|
|
50
|
+
export type InterstitialScheduleDurations = {
|
|
51
|
+
primary: number;
|
|
52
|
+
playout: number;
|
|
53
|
+
integrated: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type TimelineType = 'primary' | 'playout' | 'integrated';
|
|
57
|
+
|
|
58
|
+
type ScheduleUpdateCallback = (
|
|
59
|
+
removed: InterstitialEvent[],
|
|
60
|
+
previousItems: InterstitialScheduleItem[] | null,
|
|
61
|
+
) => void;
|
|
62
|
+
|
|
63
|
+
export class InterstitialsSchedule extends Logger {
|
|
64
|
+
private onScheduleUpdate: ScheduleUpdateCallback;
|
|
65
|
+
private eventMap: Record<string, InterstitialEvent | undefined> = {};
|
|
66
|
+
public events: InterstitialEvent[] | null = null;
|
|
67
|
+
public items: InterstitialScheduleItem[] | null = null;
|
|
68
|
+
public durations: InterstitialScheduleDurations = {
|
|
69
|
+
primary: 0,
|
|
70
|
+
playout: 0,
|
|
71
|
+
integrated: 0,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
constructor(onScheduleUpdate: ScheduleUpdateCallback, logger: ILogger) {
|
|
75
|
+
super('interstitials-sched', logger);
|
|
76
|
+
this.onScheduleUpdate = onScheduleUpdate;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public destroy() {
|
|
80
|
+
this.reset();
|
|
81
|
+
// @ts-ignore
|
|
82
|
+
this.onScheduleUpdate = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public reset() {
|
|
86
|
+
this.eventMap = {};
|
|
87
|
+
this.setDurations(0, 0, 0);
|
|
88
|
+
if (this.events) {
|
|
89
|
+
this.events.forEach((interstitial) => interstitial.reset());
|
|
90
|
+
}
|
|
91
|
+
this.events = this.items = null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public resetErrorsInRange(start: number, end: number): number {
|
|
95
|
+
if (this.events) {
|
|
96
|
+
return this.events.reduce((count, interstitial) => {
|
|
97
|
+
if (
|
|
98
|
+
start <= interstitial.startOffset &&
|
|
99
|
+
end > interstitial.startOffset
|
|
100
|
+
) {
|
|
101
|
+
delete interstitial.error;
|
|
102
|
+
return count + 1;
|
|
103
|
+
}
|
|
104
|
+
return count;
|
|
105
|
+
}, 0);
|
|
106
|
+
}
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get duration(): number {
|
|
111
|
+
const items = this.items;
|
|
112
|
+
return items ? items[items.length - 1].end : 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get length(): number {
|
|
116
|
+
return this.items ? this.items.length : 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public getEvent(
|
|
120
|
+
identifier: InterstitialId | undefined,
|
|
121
|
+
): InterstitialEvent | null {
|
|
122
|
+
return identifier ? this.eventMap[identifier] || null : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public hasEvent(identifier: InterstitialId): boolean {
|
|
126
|
+
return identifier in this.eventMap;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public findItemIndex(item: InterstitialScheduleItem, time?: number): number {
|
|
130
|
+
if (item.event) {
|
|
131
|
+
// Find Event Item
|
|
132
|
+
return this.findEventIndex(item.event.identifier);
|
|
133
|
+
}
|
|
134
|
+
// Find Primary Item
|
|
135
|
+
let index = -1;
|
|
136
|
+
if (item.nextEvent) {
|
|
137
|
+
index = this.findEventIndex(item.nextEvent.identifier) - 1;
|
|
138
|
+
} else if (item.previousEvent) {
|
|
139
|
+
index = this.findEventIndex(item.previousEvent.identifier) + 1;
|
|
140
|
+
}
|
|
141
|
+
const items = this.items;
|
|
142
|
+
if (items) {
|
|
143
|
+
if (!items[index]) {
|
|
144
|
+
if (time === undefined) {
|
|
145
|
+
time = item.start;
|
|
146
|
+
}
|
|
147
|
+
index = this.findItemIndexAtTime(time);
|
|
148
|
+
}
|
|
149
|
+
// Only return index of a Primary Item
|
|
150
|
+
while (index >= 0 && items[index]?.event) {
|
|
151
|
+
// If index found is an interstitial it is not a valid result as it should have been matched up top
|
|
152
|
+
// decrement until result is negative (not found) or a primary segment
|
|
153
|
+
index--;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return index;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public findItemIndexAtTime(
|
|
160
|
+
timelinePos: number,
|
|
161
|
+
timelineType?: TimelineType,
|
|
162
|
+
): number {
|
|
163
|
+
const items = this.items;
|
|
164
|
+
if (items) {
|
|
165
|
+
for (let i = 0; i < items.length; i++) {
|
|
166
|
+
let timeRange: { start: number; end: number } = items[i];
|
|
167
|
+
if (timelineType && timelineType !== 'primary') {
|
|
168
|
+
timeRange = timeRange[timelineType];
|
|
169
|
+
}
|
|
170
|
+
if (
|
|
171
|
+
timelinePos === timeRange.start ||
|
|
172
|
+
(timelinePos > timeRange.start && timelinePos < timeRange.end)
|
|
173
|
+
) {
|
|
174
|
+
return i;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return -1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public findJumpRestrictedIndex(startIndex: number, endIndex: number): number {
|
|
182
|
+
const items = this.items;
|
|
183
|
+
if (items) {
|
|
184
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
185
|
+
if (!items[i]) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
const event = items[i].event;
|
|
189
|
+
if (event?.restrictions.jump && !event.appendInPlace) {
|
|
190
|
+
return i;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return -1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public findEventIndex(identifier: InterstitialId): number {
|
|
198
|
+
const items = this.items;
|
|
199
|
+
if (items) {
|
|
200
|
+
for (let i = items.length; i--; ) {
|
|
201
|
+
if (items[i].event?.identifier === identifier) {
|
|
202
|
+
return i;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return -1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public findAssetIndex(event: InterstitialEvent, timelinePos: number): number {
|
|
210
|
+
const assetList = event.assetList;
|
|
211
|
+
const length = assetList.length;
|
|
212
|
+
if (length > 1) {
|
|
213
|
+
for (let i = 0; i < length; i++) {
|
|
214
|
+
const asset = assetList[i];
|
|
215
|
+
if (!asset.error) {
|
|
216
|
+
const timelineStart = asset.timelineStart;
|
|
217
|
+
if (
|
|
218
|
+
timelinePos === timelineStart ||
|
|
219
|
+
(timelinePos > timelineStart &&
|
|
220
|
+
(timelinePos < timelineStart + (asset.duration || 0) ||
|
|
221
|
+
i === length - 1))
|
|
222
|
+
) {
|
|
223
|
+
return i;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public get assetIdAtEnd(): string | null {
|
|
232
|
+
const interstitialAtEnd = this.items?.[this.length - 1]?.event;
|
|
233
|
+
if (interstitialAtEnd) {
|
|
234
|
+
const assetList = interstitialAtEnd.assetList;
|
|
235
|
+
const assetAtEnd = assetList[assetList.length - 1];
|
|
236
|
+
if (assetAtEnd) {
|
|
237
|
+
return assetAtEnd.identifier;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public parseInterstitialDateRanges(
|
|
244
|
+
mediaSelection: MediaSelection,
|
|
245
|
+
enableAppendInPlace: boolean,
|
|
246
|
+
) {
|
|
247
|
+
const details = mediaSelection.main.details!;
|
|
248
|
+
const { dateRanges } = details;
|
|
249
|
+
const previousInterstitialEvents = this.events;
|
|
250
|
+
const interstitialEvents = this.parseDateRanges(
|
|
251
|
+
dateRanges,
|
|
252
|
+
{
|
|
253
|
+
url: details.url,
|
|
254
|
+
},
|
|
255
|
+
enableAppendInPlace,
|
|
256
|
+
);
|
|
257
|
+
const ids = Object.keys(dateRanges);
|
|
258
|
+
const removedInterstitials = previousInterstitialEvents
|
|
259
|
+
? previousInterstitialEvents.filter(
|
|
260
|
+
(event) => !ids.includes(event.identifier),
|
|
261
|
+
)
|
|
262
|
+
: [];
|
|
263
|
+
if (interstitialEvents.length) {
|
|
264
|
+
// pre-rolls, post-rolls, and events with the same start time are played in playlist tag order
|
|
265
|
+
// all other events are ordered by start time
|
|
266
|
+
interstitialEvents.sort((a, b) => {
|
|
267
|
+
const aPre = a.cue.pre;
|
|
268
|
+
const aPost = a.cue.post;
|
|
269
|
+
const bPre = b.cue.pre;
|
|
270
|
+
const bPost = b.cue.post;
|
|
271
|
+
if (aPre && !bPre) {
|
|
272
|
+
return -1;
|
|
273
|
+
}
|
|
274
|
+
if (bPre && !aPre) {
|
|
275
|
+
return 1;
|
|
276
|
+
}
|
|
277
|
+
if (aPost && !bPost) {
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
280
|
+
if (bPost && !aPost) {
|
|
281
|
+
return -1;
|
|
282
|
+
}
|
|
283
|
+
if (!aPre && !bPre && !aPost && !bPost) {
|
|
284
|
+
const startA = a.startTime;
|
|
285
|
+
const startB = b.startTime;
|
|
286
|
+
if (startA !== startB) {
|
|
287
|
+
return startA - startB;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return a.dateRange.tagOrder - b.dateRange.tagOrder;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
this.events = interstitialEvents;
|
|
294
|
+
|
|
295
|
+
// Clear removed DateRanges from buffered list (kills playback of active Interstitials)
|
|
296
|
+
removedInterstitials.forEach((interstitial) => {
|
|
297
|
+
this.removeEvent(interstitial);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
this.updateSchedule(mediaSelection, removedInterstitials);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
public updateSchedule(
|
|
304
|
+
mediaSelection: MediaSelection,
|
|
305
|
+
removedInterstitials: InterstitialEvent[] = [],
|
|
306
|
+
forceUpdate: boolean = false,
|
|
307
|
+
) {
|
|
308
|
+
const events = this.events || [];
|
|
309
|
+
if (events.length || removedInterstitials.length || this.length < 2) {
|
|
310
|
+
const currentItems = this.items;
|
|
311
|
+
const updatedItems = this.parseSchedule(events, mediaSelection);
|
|
312
|
+
const updated =
|
|
313
|
+
forceUpdate ||
|
|
314
|
+
removedInterstitials.length ||
|
|
315
|
+
currentItems?.length !== updatedItems.length ||
|
|
316
|
+
updatedItems.some((item, i) => {
|
|
317
|
+
return (
|
|
318
|
+
Math.abs(item.playout.start - currentItems[i].playout.start) >
|
|
319
|
+
0.005 ||
|
|
320
|
+
Math.abs(item.playout.end - currentItems[i].playout.end) > 0.005
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
if (updated) {
|
|
324
|
+
this.items = updatedItems;
|
|
325
|
+
// call interstitials-controller onScheduleUpdated()
|
|
326
|
+
this.onScheduleUpdate(removedInterstitials, currentItems);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private parseDateRanges(
|
|
332
|
+
dateRanges: Record<string, DateRange | undefined>,
|
|
333
|
+
baseData: BaseData,
|
|
334
|
+
enableAppendInPlace: boolean,
|
|
335
|
+
): InterstitialEvent[] {
|
|
336
|
+
const interstitialEvents: InterstitialEvent[] = [];
|
|
337
|
+
const ids = Object.keys(dateRanges);
|
|
338
|
+
for (let i = 0; i < ids.length; i++) {
|
|
339
|
+
const id = ids[i];
|
|
340
|
+
const dateRange = dateRanges[id]!;
|
|
341
|
+
if (dateRange.isInterstitial) {
|
|
342
|
+
let interstitial = this.eventMap[id];
|
|
343
|
+
if (interstitial) {
|
|
344
|
+
// Update InterstitialEvent already parsed and mapped
|
|
345
|
+
// This retains already loaded duration and loaded asset list info
|
|
346
|
+
interstitial.setDateRange(dateRange);
|
|
347
|
+
} else {
|
|
348
|
+
interstitial = new InterstitialEvent(dateRange, baseData);
|
|
349
|
+
this.eventMap[id] = interstitial;
|
|
350
|
+
if (enableAppendInPlace === false) {
|
|
351
|
+
interstitial.appendInPlace = enableAppendInPlace;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
interstitialEvents.push(interstitial);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return interstitialEvents;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private parseSchedule(
|
|
361
|
+
interstitialEvents: InterstitialEvent[],
|
|
362
|
+
mediaSelection: MediaSelection,
|
|
363
|
+
): InterstitialScheduleItem[] {
|
|
364
|
+
const schedule: InterstitialScheduleItem[] = [];
|
|
365
|
+
const details = mediaSelection.main.details!;
|
|
366
|
+
const primaryDuration = details.live ? Infinity : details.edge;
|
|
367
|
+
let playoutDuration = 0;
|
|
368
|
+
|
|
369
|
+
// Filter events that have errored from the schedule (Primary fallback)
|
|
370
|
+
interstitialEvents = interstitialEvents.filter(
|
|
371
|
+
(event) => !event.error && !(event.cue.once && event.hasPlayed),
|
|
372
|
+
);
|
|
373
|
+
if (interstitialEvents.length) {
|
|
374
|
+
// Update Schedule
|
|
375
|
+
this.resolveOffsets(interstitialEvents, mediaSelection);
|
|
376
|
+
|
|
377
|
+
// Populate Schedule with Interstitial Event and Primary Segment Items
|
|
378
|
+
let primaryPosition = 0;
|
|
379
|
+
let integratedTime = 0;
|
|
380
|
+
interstitialEvents.forEach((interstitial, i) => {
|
|
381
|
+
const preroll = interstitial.cue.pre;
|
|
382
|
+
const postroll = interstitial.cue.post;
|
|
383
|
+
const previousEvent =
|
|
384
|
+
(interstitialEvents[i - 1] as InterstitialEvent | undefined) || null;
|
|
385
|
+
const appendInPlace = interstitial.appendInPlace;
|
|
386
|
+
const eventStart = postroll
|
|
387
|
+
? primaryDuration
|
|
388
|
+
: interstitial.startOffset;
|
|
389
|
+
const interstitialDuration = interstitial.duration;
|
|
390
|
+
const timelineDuration =
|
|
391
|
+
interstitial.timelineOccupancy === TimelineOccupancy.Range
|
|
392
|
+
? interstitialDuration
|
|
393
|
+
: 0;
|
|
394
|
+
const resumptionOffset = interstitial.resumptionOffset;
|
|
395
|
+
const inSameStartTimeSequence = previousEvent?.startTime === eventStart;
|
|
396
|
+
const start = eventStart + interstitial.cumulativeDuration;
|
|
397
|
+
let end = appendInPlace
|
|
398
|
+
? start + interstitialDuration
|
|
399
|
+
: eventStart + resumptionOffset;
|
|
400
|
+
if (preroll || (!postroll && eventStart <= 0)) {
|
|
401
|
+
// preroll or in-progress midroll
|
|
402
|
+
const integratedStart = integratedTime;
|
|
403
|
+
integratedTime += timelineDuration;
|
|
404
|
+
interstitial.timelineStart = start;
|
|
405
|
+
const playoutStart = playoutDuration;
|
|
406
|
+
playoutDuration += interstitialDuration;
|
|
407
|
+
schedule.push({
|
|
408
|
+
event: interstitial,
|
|
409
|
+
start,
|
|
410
|
+
end,
|
|
411
|
+
playout: {
|
|
412
|
+
start: playoutStart,
|
|
413
|
+
end: playoutDuration,
|
|
414
|
+
},
|
|
415
|
+
integrated: {
|
|
416
|
+
start: integratedStart,
|
|
417
|
+
end: integratedTime,
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
} else if (eventStart <= primaryDuration) {
|
|
421
|
+
if (!inSameStartTimeSequence) {
|
|
422
|
+
const segmentDuration = eventStart - primaryPosition;
|
|
423
|
+
// Do not schedule a primary segment if interstitials are abutting by less than ABUTTING_THRESHOLD_SECONDS
|
|
424
|
+
if (segmentDuration > ABUTTING_THRESHOLD_SECONDS) {
|
|
425
|
+
// primary segment
|
|
426
|
+
const timelineStart = primaryPosition;
|
|
427
|
+
const integratedStart = integratedTime;
|
|
428
|
+
integratedTime += segmentDuration;
|
|
429
|
+
const playoutStart = playoutDuration;
|
|
430
|
+
playoutDuration += segmentDuration;
|
|
431
|
+
const primarySegment = {
|
|
432
|
+
previousEvent: interstitialEvents[i - 1] || null,
|
|
433
|
+
nextEvent: interstitial,
|
|
434
|
+
start: timelineStart,
|
|
435
|
+
end: timelineStart + segmentDuration,
|
|
436
|
+
playout: {
|
|
437
|
+
start: playoutStart,
|
|
438
|
+
end: playoutDuration,
|
|
439
|
+
},
|
|
440
|
+
integrated: {
|
|
441
|
+
start: integratedStart,
|
|
442
|
+
end: integratedTime,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
schedule.push(primarySegment);
|
|
446
|
+
} else if (segmentDuration > 0 && previousEvent) {
|
|
447
|
+
// Add previous event `resumeTime` (based on duration or resumeOffset) so that it ends aligned with this one
|
|
448
|
+
previousEvent.cumulativeDuration += segmentDuration;
|
|
449
|
+
schedule[schedule.length - 1].end = eventStart;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// midroll / postroll
|
|
453
|
+
if (postroll) {
|
|
454
|
+
end = start;
|
|
455
|
+
}
|
|
456
|
+
interstitial.timelineStart = start;
|
|
457
|
+
const integratedStart = integratedTime;
|
|
458
|
+
integratedTime += timelineDuration;
|
|
459
|
+
const playoutStart = playoutDuration;
|
|
460
|
+
playoutDuration += interstitialDuration;
|
|
461
|
+
schedule.push({
|
|
462
|
+
event: interstitial,
|
|
463
|
+
start,
|
|
464
|
+
end,
|
|
465
|
+
playout: {
|
|
466
|
+
start: playoutStart,
|
|
467
|
+
end: playoutDuration,
|
|
468
|
+
},
|
|
469
|
+
integrated: {
|
|
470
|
+
start: integratedStart,
|
|
471
|
+
end: integratedTime,
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
} else {
|
|
475
|
+
// Interstitial starts after end of primary VOD - not included in schedule
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const resumeTime = interstitial.resumeTime;
|
|
479
|
+
if (postroll || resumeTime > primaryDuration) {
|
|
480
|
+
primaryPosition = primaryDuration;
|
|
481
|
+
} else {
|
|
482
|
+
primaryPosition = resumeTime;
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
if (primaryPosition < primaryDuration) {
|
|
486
|
+
// last primary segment
|
|
487
|
+
const timelineStart = primaryPosition;
|
|
488
|
+
const integratedStart = integratedTime;
|
|
489
|
+
const segmentDuration = primaryDuration - primaryPosition;
|
|
490
|
+
integratedTime += segmentDuration;
|
|
491
|
+
const playoutStart = playoutDuration;
|
|
492
|
+
playoutDuration += segmentDuration;
|
|
493
|
+
schedule.push({
|
|
494
|
+
previousEvent: schedule[schedule.length - 1]?.event || null,
|
|
495
|
+
nextEvent: null,
|
|
496
|
+
start: primaryPosition,
|
|
497
|
+
end: timelineStart + segmentDuration,
|
|
498
|
+
playout: {
|
|
499
|
+
start: playoutStart,
|
|
500
|
+
end: playoutDuration,
|
|
501
|
+
},
|
|
502
|
+
integrated: {
|
|
503
|
+
start: integratedStart,
|
|
504
|
+
end: integratedTime,
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
this.setDurations(primaryDuration, playoutDuration, integratedTime);
|
|
509
|
+
} else {
|
|
510
|
+
// no interstials - schedule is one primary segment
|
|
511
|
+
const start = 0;
|
|
512
|
+
schedule.push({
|
|
513
|
+
previousEvent: null,
|
|
514
|
+
nextEvent: null,
|
|
515
|
+
start,
|
|
516
|
+
end: primaryDuration,
|
|
517
|
+
playout: {
|
|
518
|
+
start,
|
|
519
|
+
end: primaryDuration,
|
|
520
|
+
},
|
|
521
|
+
integrated: {
|
|
522
|
+
start,
|
|
523
|
+
end: primaryDuration,
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
this.setDurations(primaryDuration, primaryDuration, primaryDuration);
|
|
527
|
+
}
|
|
528
|
+
return schedule;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private setDurations(primary: number, playout: number, integrated: number) {
|
|
532
|
+
this.durations = {
|
|
533
|
+
primary,
|
|
534
|
+
playout,
|
|
535
|
+
integrated,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private resolveOffsets(
|
|
540
|
+
interstitialEvents: InterstitialEvent[],
|
|
541
|
+
mediaSelection: MediaSelection,
|
|
542
|
+
) {
|
|
543
|
+
const details = mediaSelection.main.details!;
|
|
544
|
+
const primaryDuration = details.live ? Infinity : details.edge;
|
|
545
|
+
|
|
546
|
+
// First resolve cumulative resumption offsets for Interstitials that start at the same DateTime
|
|
547
|
+
let cumulativeDuration = 0;
|
|
548
|
+
let lastScheduledStart = -1;
|
|
549
|
+
interstitialEvents.forEach((interstitial, i) => {
|
|
550
|
+
const preroll = interstitial.cue.pre;
|
|
551
|
+
const postroll = interstitial.cue.post;
|
|
552
|
+
const eventStart = preroll
|
|
553
|
+
? 0
|
|
554
|
+
: postroll
|
|
555
|
+
? primaryDuration
|
|
556
|
+
: interstitial.startTime;
|
|
557
|
+
this.updateAssetDurations(interstitial);
|
|
558
|
+
|
|
559
|
+
// X-RESUME-OFFSET values of interstitials scheduled at the same time are cumulative
|
|
560
|
+
const inSameStartTimeSequence = lastScheduledStart === eventStart;
|
|
561
|
+
if (inSameStartTimeSequence) {
|
|
562
|
+
interstitial.cumulativeDuration = cumulativeDuration;
|
|
563
|
+
} else {
|
|
564
|
+
cumulativeDuration = 0;
|
|
565
|
+
lastScheduledStart = eventStart;
|
|
566
|
+
}
|
|
567
|
+
if (!postroll && interstitial.snapOptions.in) {
|
|
568
|
+
// FIXME: Include audio playlist in snapping
|
|
569
|
+
interstitial.resumeAnchor =
|
|
570
|
+
findFragmentByPTS(
|
|
571
|
+
null,
|
|
572
|
+
details.fragments,
|
|
573
|
+
interstitial.startOffset + interstitial.resumptionOffset,
|
|
574
|
+
0,
|
|
575
|
+
0,
|
|
576
|
+
) || undefined;
|
|
577
|
+
}
|
|
578
|
+
// Check if primary fragments align with resumption offset and disable appendInPlace if they do not
|
|
579
|
+
if (interstitial.appendInPlace && !interstitial.appendInPlaceStarted) {
|
|
580
|
+
const alignedSegmentStart = this.primaryCanResumeInPlaceAt(
|
|
581
|
+
interstitial,
|
|
582
|
+
mediaSelection,
|
|
583
|
+
);
|
|
584
|
+
if (!alignedSegmentStart) {
|
|
585
|
+
interstitial.appendInPlace = false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (!interstitial.appendInPlace && i + 1 < interstitialEvents.length) {
|
|
589
|
+
// abutting Interstitials must use the same MediaSource strategy, this applies to all whether or not they are back to back:
|
|
590
|
+
const timeBetween =
|
|
591
|
+
interstitialEvents[i + 1].startTime -
|
|
592
|
+
interstitialEvents[i].resumeTime;
|
|
593
|
+
if (timeBetween < ABUTTING_THRESHOLD_SECONDS) {
|
|
594
|
+
interstitialEvents[i + 1].appendInPlace = false;
|
|
595
|
+
if (interstitialEvents[i + 1].appendInPlace) {
|
|
596
|
+
this.warn(
|
|
597
|
+
`Could not change append strategy for abutting event ${interstitial}`,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Update cumulativeDuration for next abutting interstitial with the same start date
|
|
603
|
+
const resumeOffset = Number.isFinite(interstitial.resumeOffset)
|
|
604
|
+
? interstitial.resumeOffset
|
|
605
|
+
: interstitial.duration;
|
|
606
|
+
cumulativeDuration += resumeOffset;
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private primaryCanResumeInPlaceAt(
|
|
611
|
+
interstitial: InterstitialEvent,
|
|
612
|
+
mediaSelection: MediaSelection,
|
|
613
|
+
): boolean {
|
|
614
|
+
const resumeTime = interstitial.resumeTime;
|
|
615
|
+
const resumesInPlaceAt =
|
|
616
|
+
interstitial.startTime + interstitial.resumptionOffset;
|
|
617
|
+
if (
|
|
618
|
+
Math.abs(resumeTime - resumesInPlaceAt) > ALIGNED_END_THRESHOLD_SECONDS
|
|
619
|
+
) {
|
|
620
|
+
this.log(
|
|
621
|
+
`"${interstitial.identifier}" resumption ${resumeTime} not aligned with estimated timeline end ${resumesInPlaceAt}`,
|
|
622
|
+
);
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
const playlists = Object.keys(mediaSelection);
|
|
626
|
+
return !playlists.some((playlistType) => {
|
|
627
|
+
const details = mediaSelection[playlistType].details;
|
|
628
|
+
const playlistEnd = details.edge;
|
|
629
|
+
if (resumeTime >= playlistEnd) {
|
|
630
|
+
// Live playback - resumption segments are not yet available
|
|
631
|
+
this.log(
|
|
632
|
+
`"${interstitial.identifier}" resumption ${resumeTime} past ${playlistType} playlist end ${playlistEnd}`,
|
|
633
|
+
);
|
|
634
|
+
// Assume alignment is possible (or reset can take place)
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
const startFragment = findFragmentByPTS(
|
|
638
|
+
null,
|
|
639
|
+
details.fragments,
|
|
640
|
+
resumeTime,
|
|
641
|
+
);
|
|
642
|
+
if (!startFragment) {
|
|
643
|
+
this.log(
|
|
644
|
+
`"${interstitial.identifier}" resumption ${resumeTime} does not align with any fragments in ${playlistType} playlist (${details.fragStart}-${details.fragmentEnd})`,
|
|
645
|
+
);
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
const allowance = playlistType === 'audio' ? 0.175 : 0;
|
|
649
|
+
const alignedWithSegment =
|
|
650
|
+
Math.abs(startFragment.start - resumeTime) <
|
|
651
|
+
ALIGNED_END_THRESHOLD_SECONDS + allowance ||
|
|
652
|
+
Math.abs(startFragment.end - resumeTime) <
|
|
653
|
+
ALIGNED_END_THRESHOLD_SECONDS + allowance;
|
|
654
|
+
if (!alignedWithSegment) {
|
|
655
|
+
this.log(
|
|
656
|
+
`"${interstitial.identifier}" resumption ${resumeTime} not aligned with ${playlistType} fragment bounds (${startFragment.start}-${startFragment.end} sn: ${startFragment.sn} cc: ${startFragment.cc})`,
|
|
657
|
+
);
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
return false;
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private updateAssetDurations(interstitial: InterstitialEvent) {
|
|
665
|
+
if (!interstitial.assetListLoaded) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const eventStart = interstitial.timelineStart;
|
|
669
|
+
let sumDuration = 0;
|
|
670
|
+
let hasUnknownDuration = false;
|
|
671
|
+
let hasErrors = false;
|
|
672
|
+
for (let i = 0; i < interstitial.assetList.length; i++) {
|
|
673
|
+
const asset = interstitial.assetList[i];
|
|
674
|
+
const timelineStart = eventStart + sumDuration;
|
|
675
|
+
asset.startOffset = sumDuration;
|
|
676
|
+
asset.timelineStart = timelineStart;
|
|
677
|
+
hasUnknownDuration ||= asset.duration === null;
|
|
678
|
+
hasErrors ||= !!asset.error;
|
|
679
|
+
const duration = asset.error ? 0 : (asset.duration as number) || 0;
|
|
680
|
+
sumDuration += duration;
|
|
681
|
+
}
|
|
682
|
+
// Use the sum of known durations when it is greater than the stated duration
|
|
683
|
+
if (hasUnknownDuration && !hasErrors) {
|
|
684
|
+
interstitial.duration = Math.max(sumDuration, interstitial.duration);
|
|
685
|
+
} else {
|
|
686
|
+
interstitial.duration = sumDuration;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private removeEvent(interstitial: InterstitialEvent) {
|
|
691
|
+
interstitial.reset();
|
|
692
|
+
delete this.eventMap[interstitial.identifier];
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export function segmentToString(segment: InterstitialScheduleItem): string {
|
|
697
|
+
return `[${segment.event ? '"' + segment.event.identifier + '"' : 'primary'}: ${segment.start.toFixed(2)}-${segment.end.toFixed(2)}]`;
|
|
698
|
+
}
|