eyevinn-channel-engine 3.2.1 → 3.2.2

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/engine/session.js CHANGED
@@ -289,6 +289,14 @@ class Session {
289
289
  this.switchDataForSession.transitionSegments = segments;
290
290
  this.switchDataForSession.mediaSeqOffset = mSeqOffset;
291
291
 
292
+ let waitTimeMs = 2000;
293
+ for (let i = segments[Object.keys(segments)[0]].length - 1; 0 < i; i--) {
294
+ if (segments[Object.keys(segments)[0]][i].duration) {
295
+ waitTimeMs = parseInt(1000 * (segments[Object.keys(segments)[0]][i].duration / 3), 10);
296
+ break;
297
+ }
298
+ }
299
+
292
300
  let isLeader = await this._sessionStateStore.isLeader(this._instanceId);
293
301
  if (!isLeader) {
294
302
  debug(`[${this._sessionId}]: FOLLOWER: Invalidate cache to ensure having the correct VOD!`);
@@ -315,14 +323,14 @@ class Session {
315
323
  }
316
324
  debug(`[${this._sessionId}]: NEW LEADER: Setting state=VOD_RELOAD_INIT`);
317
325
  this.isSwitchingBackToV2L = true;
318
- await this._sessionState.set("state", SessionState.VOD_RELOAD_INIT)
326
+ await this._sessionState.set("state", SessionState.VOD_RELOAD_INIT);
319
327
 
320
328
  } else {
321
329
  let vodReloaded = await this._sessionState.get("vodReloaded");
322
- let attempts = 9;
330
+ let attempts = 12;
323
331
  while (!vodReloaded && attempts > 0) {
324
- debug(`[${this._sessionId}]: LEADER: Waiting (1000ms) to buy some time reloading vod and adding it to store! (tries left=${attempts})`);
325
- await timer(1000);
332
+ debug(`[${this._sessionId}]: LEADER: Waiting (${waitTimeMs}ms) to buy some time reloading vod and adding it to store! (tries left=${attempts})`);
333
+ await timer(waitTimeMs);
326
334
  vodReloaded = await this._sessionState.get("vodReloaded");
327
335
  attempts--;
328
336
  }
@@ -961,6 +969,7 @@ class Session {
961
969
  if (!isLeader) {
962
970
  debug(`[${this._sessionId}]: not the leader so just waiting for the VOD to be initiated`);
963
971
  }
972
+ this.alreadyClearedVodCache = false;
964
973
  return;
965
974
  case SessionState.VOD_NEXT_INIT:
966
975
  try {
@@ -2,8 +2,9 @@ const debug = require("debug")("engine-stream-switcher");
2
2
  const crypto = require("crypto");
3
3
  const fetch = require("node-fetch");
4
4
  const { AbortController } = require("abort-controller");
5
- const { SessionState } = require('./session_state');
6
- const { timer } = require("./util")
5
+ const { SessionState } = require("./session_state");
6
+ const { timer, findNearestValue, isValidUrl, fetchWithRetry } = require("./util");
7
+ const m3u8 = require("@eyevinn/m3u8");
7
8
 
8
9
  const SwitcherState = Object.freeze({
9
10
  V2L_TO_LIVE: 1,
@@ -31,8 +32,6 @@ class StreamSwitcher {
31
32
  this.working = false;
32
33
  this.timeDiff = null;
33
34
  this.abortTimeStamp = null;
34
-
35
-
36
35
  if (config) {
37
36
  if (config.sessionId) {
38
37
  this.sessionId = config.sessionId;
@@ -47,6 +46,7 @@ class StreamSwitcher {
47
46
  this.streamSwitchManager = config.streamSwitchManager;
48
47
  }
49
48
  }
49
+ this.prerollsCache = {};
50
50
  }
51
51
 
52
52
  getEventId() {
@@ -59,12 +59,7 @@ class StreamSwitcher {
59
59
  debug(`[${this.sessionId}]: Abort Live Stream! Reason: ${message}`);
60
60
  try {
61
61
  this.abortTimeStamp = Date.now();
62
- status = await this._initSwitching(
63
- SwitcherState.LIVE_TO_V2L,
64
- session,
65
- sessionLive,
66
- null
67
- );
62
+ status = await this._initSwitching(SwitcherState.LIVE_TO_V2L, session, sessionLive, null);
68
63
  return status;
69
64
  } catch (err) {
70
65
  debug(`Failed to force a switch off live feed: ${err}`);
@@ -89,158 +84,135 @@ class StreamSwitcher {
89
84
  debug(`[${this.sessionId}]: streamSwitcher is currently busy`);
90
85
  return null;
91
86
  }
92
- // Handle Complete Storage Reset
93
- let sessionState = await session.getSessionState();
94
- if (sessionState === SessionState.VOD_INIT || !sessionState) {
95
- this.working = true;
96
- sessionLive.waitForPlayhead = false;
97
- sessionLive.allowedToSet = false; // only have effect on leader
98
- await sessionLive.resetSession();
99
- await sessionLive.resetLiveStoreAsync(0);
100
- this.working = false;
101
- this.abortTimeStamp = Date.now() + 20 * 1000; // 30 second V2L->LIVE timeout
102
- if (this.streamTypeLive) {
103
- debug(`[${this.sessionId}]: [ Ending LIVE Abruptly, Going to -> V2L ]`);
104
- this.streamTypeLive = false;
87
+
88
+ try {
89
+ // Handle Complete Storage Reset
90
+ let sessionState = await session.getSessionState();
91
+ if (sessionState === SessionState.VOD_INIT || !sessionState) {
92
+ this.working = true;
93
+ sessionLive.waitForPlayhead = false;
94
+ sessionLive.allowedToSet = false; // only have effect on leader
95
+ await sessionLive.resetSession();
96
+ await sessionLive.resetLiveStoreAsync(0);
97
+ this.working = false;
98
+ this.abortTimeStamp = Date.now() + 30 * 1000; // 30 second V2L->LIVE timeout
99
+ if (this.streamTypeLive) {
100
+ debug(`[${this.sessionId}]: [ Ending LIVE Abruptly, Going to -> V2L ]`);
101
+ this.streamTypeLive = false;
102
+ }
103
+ debug(`[${this.sessionId}]: StreamSwitcher reacting to Full Store Reset`);
104
+ return false; // Go to V2L feed
105
105
  }
106
- debug(`[${this.sessionId}]: StreamSwitcher reacting to Full Store Reset`);
107
- return false; // Go to V2L feed
108
- }
109
106
 
110
- // Filter out schedule objects from the past
111
- const tsNow = Date.now();
112
- const strmSchedule = await this.streamSwitchManager.getSchedule(this.sessionId);
113
- const schedule = strmSchedule.filter((obj) => obj.end_time > tsNow);
114
- if (schedule.length === 0 && this.streamTypeLive) {
115
- status = await this._initSwitching(
116
- SwitcherState.LIVE_TO_V2L,
117
- session,
118
- sessionLive,
119
- null
120
- );
121
- return status;
122
- }
123
- if (schedule.length === 0) {
124
- this.eventId = null;
125
- return false;
126
- }
127
- const scheduleObj = schedule[0];
128
- this.timeDiff = scheduleObj;
129
- if (tsNow < scheduleObj.start_time) {
130
- if (this.streamTypeLive) {
131
- status = await this._initSwitching(
132
- SwitcherState.LIVE_TO_V2L,
133
- session,
134
- sessionLive,
135
- null
136
- );
107
+ // Filter out schedule objects from the past
108
+ const tsNow = Date.now();
109
+ const strmSchedule = await this.streamSwitchManager.getSchedule(this.sessionId);
110
+ const schedule = strmSchedule.filter((obj) => obj.end_time > tsNow);
111
+
112
+ // Load Preroll, if any, once per channel every (maxAge) seconds
113
+ if (!this.prerollsCache[this.sessionId] || this.prerollsCache[this.sessionId].maxAge < tsNow) {
114
+ if (this.streamSwitchManager.getPrerollUri) {
115
+ const prerollUri = await this.streamSwitchManager.getPrerollUri(this.sessionId);
116
+ if (isValidUrl(prerollUri)) {
117
+ try {
118
+ const segments = await this._loadPreroll(prerollUri);
119
+ const prerollItem = {
120
+ segments: segments,
121
+ maxAge: tsNow + 30 * 60 * 1000,
122
+ };
123
+ this.prerollsCache[this.sessionId] = prerollItem;
124
+ } catch (err) {
125
+ debug(`[${this.sessionId}]: Failed loading preroll vod for channel=${this.sessionId}`);
126
+ console.error(err);
127
+ }
128
+ } else {
129
+ debug(`[${this.sessionId}]: Preroll uri:'${prerollUri}' is not a valid URL. Using No preroll.`);
130
+ }
131
+ }
132
+ }
133
+
134
+ if (schedule.length === 0 && this.streamTypeLive) {
135
+ status = await this._initSwitching(SwitcherState.LIVE_TO_V2L, session, sessionLive, null);
137
136
  return status;
138
137
  }
139
- this.eventId = null;
140
- return false;
141
- }
138
+ if (schedule.length === 0) {
139
+ this.eventId = null;
140
+ return false;
141
+ }
142
+ const scheduleObj = schedule[0];
143
+ this.timeDiff = scheduleObj;
144
+ if (tsNow < scheduleObj.start_time) {
145
+ if (this.streamTypeLive) {
146
+ status = await this._initSwitching(SwitcherState.LIVE_TO_V2L, session, sessionLive, null);
147
+ return status;
148
+ }
149
+ this.eventId = null;
150
+ return false;
151
+ }
142
152
 
143
- let tries = 0;
144
- let validURI = false;
145
- while (!validURI && tries < MAX_FAILS) {
146
- debug(
147
- `[${
148
- this.sessionId
149
- }]: Switcher is validating Master URI... (tries left=${
150
- MAX_FAILS - tries
151
- })`
152
- );
153
- validURI = await this._validURI(scheduleObj.uri);
154
- tries++;
153
+ let tries = 0;
154
+ let validURI = false;
155
+ while (!validURI && tries < MAX_FAILS) {
156
+ debug(`[${this.sessionId}]: Switcher is validating Master URI... (tries left=${MAX_FAILS - tries})`);
157
+ validURI = await this._validURI(scheduleObj.uri);
158
+ tries++;
159
+ if (!validURI) {
160
+ const delayMs = tries * 500;
161
+ debug(`[${this.sessionId}]: Going to try validating Master URI again in ${delayMs}ms`);
162
+ await timer(delayMs);
163
+ }
164
+ }
155
165
  if (!validURI) {
156
- const delayMs = tries * 500;
157
- debug(`[${this.sessionId}]: Going to try validating Master URI again in ${delayMs}ms`);
158
- await timer(delayMs);
166
+ debug(`[${this.sessionId}]: Unreachable URI: [${scheduleObj.uri}]`);
167
+ if (this.streamTypeLive) {
168
+ await this.abortLiveFeed(session, sessionLive, "Switching back to VOD2Live due to unreachable URI");
169
+ }
170
+ return false;
159
171
  }
160
- }
161
- if (!validURI) {
162
- debug(`[${this.sessionId}]: Unreachable URI: [${scheduleObj.uri}]`);
163
- if (this.streamTypeLive) {
164
- await this.abortLiveFeed(
165
- session,
166
- sessionLive,
167
- "Switching back to VOD2Live due to unreachable URI"
168
- );
172
+ debug(`[${this.sessionId}]: ....Master URI -> VALID`);
173
+
174
+ if (this.abortTimeStamp && tsNow - this.abortTimeStamp <= 10000) {
175
+ // If we have a valid URI and no more than 10 seconds have passed since switching from Live->V2L.
176
+ // Stay on V2L to give live sessionLive some time to prepare before switching back to live.
177
+ debug(`[${this.sessionId}]: Waiting [${10000 - (tsNow - this.abortTimeStamp)}ms] before switching back to Live due to unreachable URI`);
178
+ return false;
169
179
  }
170
- return false;
171
- }
172
- debug(`[${this.sessionId}]: ....Master URI -> VALID`);
173
-
174
- if (this.abortTimeStamp && tsNow - this.abortTimeStamp <= 10000) {
175
- // If we have a valid URI and no more than 10 seconds have passed since switching from Live->V2L.
176
- // Stay on V2L to give live sessionLive some time to prepare before switching back to live.
177
- debug(
178
- `[${this.sessionId}]: Waiting [${
179
- 10000 - (tsNow - this.abortTimeStamp)
180
- }ms] before switching back to Live due to unreachable URI`
181
- );
182
- return false;
183
- }
184
- this.abortTimeStamp = null;
180
+ this.abortTimeStamp = null;
185
181
 
186
- if (this.streamTypeLive) {
187
- if (
188
- tsNow >= scheduleObj.start_time &&
189
- this.eventId !== scheduleObj.eventId
190
- ) {
191
- if (scheduleObj.type === StreamType.LIVE) {
192
- status = await this._initSwitching(
193
- SwitcherState.LIVE_TO_LIVE,
194
- session,
195
- sessionLive,
196
- scheduleObj
197
- );
182
+ if (this.streamTypeLive) {
183
+ if (tsNow >= scheduleObj.start_time && this.eventId !== scheduleObj.eventId) {
184
+ if (scheduleObj.type === StreamType.LIVE) {
185
+ status = await this._initSwitching(SwitcherState.LIVE_TO_LIVE, session, sessionLive, scheduleObj);
186
+ return status;
187
+ }
188
+ status = await this._initSwitching(SwitcherState.LIVE_TO_VOD, session, sessionLive, scheduleObj);
198
189
  return status;
199
190
  }
200
- status = await this._initSwitching(
201
- SwitcherState.LIVE_TO_VOD,
202
- session,
203
- sessionLive,
204
- scheduleObj
205
- );
206
- return status;
207
191
  }
208
- }
209
- if (
210
- tsNow >= scheduleObj.start_time &&
211
- tsNow < scheduleObj.end_time &&
212
- scheduleObj.end_time - tsNow > 10000
213
- ) {
214
- if (scheduleObj.type === StreamType.LIVE) {
215
- if (!this.streamTypeLive) {
216
- status = await this._initSwitching(
217
- SwitcherState.V2L_TO_LIVE,
218
- session,
219
- sessionLive,
220
- scheduleObj
221
- );
222
- return status;
192
+ if (tsNow >= scheduleObj.start_time && tsNow < scheduleObj.end_time && scheduleObj.end_time - tsNow > 10000) {
193
+ if (scheduleObj.type === StreamType.LIVE) {
194
+ if (!this.streamTypeLive) {
195
+ status = await this._initSwitching(SwitcherState.V2L_TO_LIVE, session, sessionLive, scheduleObj);
196
+ return status;
197
+ }
198
+ return true;
223
199
  }
224
- return true;
225
- }
226
- if (!this.streamTypeLive) {
227
- if (!scheduleObj.duration) {
228
- debug(
229
- `[${this.sessionId}]: Cannot switch VOD no duration specified for schedule item: [${scheduleObj.assetId}]`
230
- );
200
+ if (!this.streamTypeLive) {
201
+ if (!scheduleObj.duration) {
202
+ debug(`[${this.sessionId}]: Cannot switch VOD no duration specified for schedule item: [${scheduleObj.assetId}]`);
203
+ return false;
204
+ }
205
+ if (this.eventId !== scheduleObj.eventId) {
206
+ status = await this._initSwitching(SwitcherState.V2L_TO_VOD, session, sessionLive, scheduleObj);
207
+ return status;
208
+ }
231
209
  return false;
232
210
  }
233
- if (this.eventId !== scheduleObj.eventId) {
234
- status = await this._initSwitching(
235
- SwitcherState.V2L_TO_VOD,
236
- session,
237
- sessionLive,
238
- scheduleObj
239
- );
240
- return status;
241
- }
242
- return false;
243
211
  }
212
+ } catch (err) {
213
+ debug(`[${this.sessionId}]: Unexpected failure in Stream Switcher...`);
214
+ console.error(err);
215
+ throw new Error(err);
244
216
  }
245
217
  }
246
218
 
@@ -263,13 +235,15 @@ class StreamSwitcher {
263
235
  currVodCounts = await session.getCurrentMediaAndDiscSequenceCount();
264
236
  currVodSegments = await session.getCurrentMediaSequenceSegments();
265
237
 
238
+ // Insert preroll if available for current channel
239
+ if (this.prerollsCache[this.sessionId]) {
240
+ const prerollSegments = this.prerollsCache[this.sessionId].segments;
241
+ currVodSegments = this._mergeSegments(prerollSegments, currVodSegments, false);
242
+ }
266
243
  // In risk that the SL-playhead might have updated some data after
267
244
  // we reset last time... we should Reset SessionLive before sending new data.
268
245
  await sessionLive.resetLiveStoreAsync(0);
269
- await sessionLive.setCurrentMediaAndDiscSequenceCount(
270
- currVodCounts.mediaSeq,
271
- currVodCounts.discSeq
272
- );
246
+ await sessionLive.setCurrentMediaAndDiscSequenceCount(currVodCounts.mediaSeq, currVodCounts.discSeq);
273
247
  await sessionLive.setCurrentMediaSequenceSegments(currVodSegments);
274
248
  liveUri = await sessionLive.setLiveUri(scheduleObj.uri);
275
249
 
@@ -289,6 +263,7 @@ class StreamSwitcher {
289
263
  this.working = false;
290
264
  this.eventId = null;
291
265
  debug(`[${this.sessionId}]: [ ERROR Switching from V2L->LIVE ]`);
266
+ console.error(err);
292
267
  throw new Error(err);
293
268
  }
294
269
 
@@ -297,10 +272,7 @@ class StreamSwitcher {
297
272
  debug(`[${this.sessionId}]: [ INIT Switching from V2L->VOD ]`);
298
273
  this.eventId = scheduleObj.eventId;
299
274
  currVodCounts = await session.getCurrentMediaAndDiscSequenceCount();
300
- eventSegments = await session.getTruncatedVodSegments(
301
- scheduleObj.uri,
302
- scheduleObj.duration / 1000
303
- );
275
+ eventSegments = await session.getTruncatedVodSegments(scheduleObj.uri, scheduleObj.duration / 1000);
304
276
 
305
277
  if (!eventSegments) {
306
278
  debug(`[${this.sessionId}]: [ ERROR Switching from V2L->VOD ]`);
@@ -309,10 +281,13 @@ class StreamSwitcher {
309
281
  return false;
310
282
  }
311
283
 
312
- await session.setCurrentMediaAndDiscSequenceCount(
313
- currVodCounts.mediaSeq,
314
- currVodCounts.discSeq
315
- );
284
+ // Insert preroll if available for current channel
285
+ if (this.prerollsCache[this.sessionId]) {
286
+ const prerollSegments = this.prerollsCache[this.sessionId].segments;
287
+ eventSegments = this._mergeSegments(prerollSegments, eventSegments, true);
288
+ }
289
+
290
+ await session.setCurrentMediaAndDiscSequenceCount(currVodCounts.mediaSeq, currVodCounts.discSeq);
316
291
  await session.setCurrentMediaSequenceSegments(eventSegments, 0, true);
317
292
 
318
293
  this.working = false;
@@ -336,9 +311,7 @@ class StreamSwitcher {
336
311
  sessionLive.resetLiveStoreAsync(RESET_DELAY); // In parallel
337
312
 
338
313
  if (scheduleObj && !scheduleObj.duration) {
339
- debug(
340
- `[${this.sessionId}]: Cannot switch VOD. No duration specified for schedule item: [${scheduleObj.assetId}]`
341
- );
314
+ debug(`[${this.sessionId}]: Cannot switch VOD. No duration specified for schedule item: [${scheduleObj.assetId}]`);
342
315
  }
343
316
 
344
317
  if (this._isEmpty(liveSegments.currMseqSegs)) {
@@ -348,14 +321,15 @@ class StreamSwitcher {
348
321
  return false;
349
322
  }
350
323
 
351
- await session.setCurrentMediaAndDiscSequenceCount(
352
- liveCounts.mediaSeq,
353
- liveCounts.discSeq
354
- );
355
- await session.setCurrentMediaSequenceSegments(
356
- liveSegments.currMseqSegs,
357
- liveSegments.segCount
358
- );
324
+ // Insert preroll, if available, for current channel
325
+ if (this.prerollsCache[this.sessionId]) {
326
+ const prerollSegments = this.prerollsCache[this.sessionId].segments;
327
+ liveSegments.currMseqSegs = this._mergeSegments(prerollSegments, liveSegments.currMseqSegs, false);
328
+ liveSegments.segCount += prerollSegments.length;
329
+ }
330
+
331
+ await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq, liveCounts.discSeq);
332
+ await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount);
359
333
 
360
334
  this.working = false;
361
335
  this.streamTypeLive = false;
@@ -378,10 +352,7 @@ class StreamSwitcher {
378
352
  await sessionLive.resetSession();
379
353
  sessionLive.resetLiveStoreAsync(RESET_DELAY); // In parallel
380
354
 
381
- eventSegments = await session.getTruncatedVodSegments(
382
- scheduleObj.uri,
383
- scheduleObj.duration / 1000
384
- );
355
+ eventSegments = await session.getTruncatedVodSegments(scheduleObj.uri, scheduleObj.duration / 1000);
385
356
  if (!eventSegments) {
386
357
  debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->VOD ]`);
387
358
  this.streamTypeLive = false;
@@ -390,14 +361,15 @@ class StreamSwitcher {
390
361
  return false;
391
362
  }
392
363
 
393
- await session.setCurrentMediaAndDiscSequenceCount(
394
- liveCounts.mediaSeq - 1,
395
- liveCounts.discSeq - 1
396
- );
397
- await session.setCurrentMediaSequenceSegments(
398
- liveSegments.currMseqSegs,
399
- liveSegments.segCount
400
- );
364
+ await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq - 1, liveCounts.discSeq - 1);
365
+ await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount);
366
+
367
+ // Insert preroll, if available, for current channel
368
+ if (this.prerollsCache[this.sessionId]) {
369
+ const prerollSegments = this.prerollsCache[this.sessionId].segments;
370
+ eventSegments = this._mergeSegments(prerollSegments, eventSegments, true);
371
+ }
372
+
401
373
  await session.setCurrentMediaSequenceSegments(eventSegments, 0, true);
402
374
 
403
375
  this.working = false;
@@ -417,19 +389,19 @@ class StreamSwitcher {
417
389
  // TODO: Not yet fully tested/supported
418
390
  this.eventId = scheduleObj.eventId;
419
391
  eventSegments = await sessionLive.getCurrentMediaSequenceSegments();
420
- currLiveCounts =
421
- await sessionLive.getCurrentMediaAndDiscSequenceCount();
392
+ currLiveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount();
422
393
 
423
394
  await sessionLive.resetSession();
424
395
  await sessionLive.resetLiveStoreAsync(0);
425
396
 
426
- await sessionLive.setCurrentMediaAndDiscSequenceCount(
427
- currLiveCounts.mediaSeq,
428
- currLiveCounts.discSeq
429
- );
430
- await sessionLive.setCurrentMediaSequenceSegments(
431
- eventSegments.currMseqSegs
432
- );
397
+ // Insert preroll, if available, for current channel
398
+ if (this.prerollsCache[this.sessionId]) {
399
+ const prerollSegments = this.prerollsCache[this.sessionId].segments;
400
+ eventSegments = this._mergeSegments(prerollSegments, eventSegments, false);
401
+ }
402
+
403
+ await sessionLive.setCurrentMediaAndDiscSequenceCount(currLiveCounts.mediaSeq, currLiveCounts.discSeq);
404
+ await sessionLive.setCurrentMediaSequenceSegments(eventSegments.currMseqSegs);
433
405
  liveUri = await sessionLive.setLiveUri(scheduleObj.uri);
434
406
 
435
407
  if (!liveUri) {
@@ -483,19 +455,187 @@ class StreamSwitcher {
483
455
  if (online.status >= 200 && online.status < 300) {
484
456
  return true;
485
457
  }
486
- debug(
487
- `[${this.sessionId}]: Failed to validate URI: ${uri}\nERROR! Returned Status Code: ${online.status}`
488
- );
458
+ debug(`[${this.sessionId}]: Failed to validate URI: ${uri}\nERROR! Returned Status Code: ${online.status}`);
489
459
  return false;
490
460
  } catch (err) {
491
- debug(
492
- `[${this.sessionId}]: Failed to validate URI: ${uri}\nERROR! ${err}`
493
- );
461
+ debug(`[${this.sessionId}]: Failed to validate URI: ${uri}\nERROR: ${err}`);
494
462
  return false;
495
463
  } finally {
496
464
  clearTimeout(timeout);
497
465
  }
498
466
  }
467
+
468
+ async _loadPreroll(uri) {
469
+ const prerollSegments = {};
470
+ const mediaM3UPlaylists = {};
471
+ const mediaURIs = {};
472
+ try {
473
+ const m3u = await this._fetchParseM3u8(uri);
474
+ debug(`[${this.sessionId}]: ...Fetched a New Preroll Slate Master Manifest from:\n${uri}`);
475
+ // Is the first URI an actual Multivariant manifest
476
+ if (m3u.items.StreamItem.length > 0) {
477
+ // Process Master M3U. Collect Media URIs
478
+ for (let i = 0; i < m3u.items.StreamItem.length; i++) {
479
+ const streamItem = m3u.items.StreamItem[i];
480
+ const bw = streamItem.get("bandwidth");
481
+ const mediaUri = streamItem.get("uri");
482
+ if (mediaUri.match("^http")) {
483
+ mediaURIs[bw] = mediaUri;
484
+ } else {
485
+ mediaURIs[bw] = new URL(mediaUri, uri);
486
+ }
487
+ }
488
+
489
+ // Fetch and parse Media URIs
490
+ const bandwidths = Object.keys(mediaURIs);
491
+ const loadMediaPromises = [];
492
+ // Queue up...
493
+ bandwidths.forEach((bw) => loadMediaPromises.push(this._fetchParseM3u8(mediaURIs[bw])));
494
+ // Execute...
495
+ const results = await Promise.allSettled(loadMediaPromises);
496
+ // Process...
497
+ results.forEach((item, idx) => {
498
+ if (item.status === "fulfilled" && item.value) {
499
+ const resultM3U = item.value;
500
+ const bw = bandwidths[idx];
501
+ mediaM3UPlaylists[bw] = resultM3U.items.PlaylistItem;
502
+ }
503
+ });
504
+ } else if (m3u.items.PlaylistItem.length > 0) {
505
+ // Process the Media M3U.
506
+ const arbitraryBw = 1;
507
+ mediaURIs[arbitraryBw] = uri;
508
+ mediaM3UPlaylists[arbitraryBw] = m3u.items.PlaylistItem;
509
+ } else {
510
+ debug(`[${this.sessionId}]: WARNING! M3U has no variants nor playlist segments!`);
511
+ }
512
+
513
+ // Turn original m3u playlist items into custom-simple segment items list
514
+ const bandwidths = Object.keys(mediaM3UPlaylists);
515
+ for (let i = 0; i < bandwidths.length; i++) {
516
+ const bw = bandwidths[i];
517
+ if (!prerollSegments[bw]) {
518
+ prerollSegments[bw] = [];
519
+ }
520
+ for (let k = 0; k < mediaM3UPlaylists[bw].length; k++) {
521
+ let seg = {};
522
+ let playlistItem = mediaM3UPlaylists[bw][k];
523
+ let segmentUri;
524
+ let cueData = null;
525
+ let daterangeData = null;
526
+ let attributes = playlistItem["attributes"].attributes;
527
+ if (playlistItem.properties.discontinuity) {
528
+ prerollSegments[bw].push({ discontinuity: true });
529
+ }
530
+ if ("cuein" in attributes) {
531
+ if (!cueData) {
532
+ cueData = {};
533
+ }
534
+ cueData["in"] = true;
535
+ }
536
+ if ("cueout" in attributes) {
537
+ if (!cueData) {
538
+ cueData = {};
539
+ }
540
+ cueData["out"] = true;
541
+ cueData["duration"] = attributes["cueout"];
542
+ }
543
+ if ("cuecont" in attributes) {
544
+ if (!cueData) {
545
+ cueData = {};
546
+ }
547
+ cueData["cont"] = true;
548
+ }
549
+ if ("scteData" in attributes) {
550
+ if (!cueData) {
551
+ cueData = {};
552
+ }
553
+ cueData["scteData"] = attributes["scteData"];
554
+ }
555
+ if ("assetData" in attributes) {
556
+ if (!cueData) {
557
+ cueData = {};
558
+ }
559
+ cueData["assetData"] = attributes["assetData"];
560
+ }
561
+ if ("daterange" in attributes) {
562
+ if (!daterangeData) {
563
+ daterangeData = {};
564
+ }
565
+ let allDaterangeAttributes = Object.keys(attributes["daterange"]);
566
+ allDaterangeAttributes.forEach((attr) => {
567
+ if (attr.match(/DURATION$/)) {
568
+ daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]);
569
+ } else {
570
+ daterangeData[attr.toLowerCase()] = attributes["daterange"][attr];
571
+ }
572
+ });
573
+ }
574
+ if (playlistItem.properties.uri) {
575
+ if (playlistItem.properties.uri.match("^http")) {
576
+ segmentUri = playlistItem.properties.uri;
577
+ } else {
578
+ segmentUri = new URL(playlistItem.properties.uri, mediaURIs[bw]);
579
+ }
580
+ seg["duration"] = playlistItem.properties.duration;
581
+ seg["uri"] = segmentUri;
582
+ seg["cue"] = cueData;
583
+ if (daterangeData) {
584
+ seg["daterange"] = daterangeData;
585
+ }
586
+ }
587
+ prerollSegments[bw].push(seg);
588
+ }
589
+ }
590
+ debug(`[${this.sessionId}]: Loaded all Variants of the Preroll Slate!`);
591
+ return prerollSegments;
592
+ } catch (err) {
593
+ throw new Error(err);
594
+ }
595
+ }
596
+
597
+ // Input: hls vod uri. Output: an M3U object.
598
+ async _fetchParseM3u8(uri) {
599
+ const parser = m3u8.createStream();
600
+ try {
601
+ const res = await fetchWithRetry(uri, {}, 5, 1000, 1500, debug);
602
+ res.body.pipe(parser);
603
+ return new Promise((resolve, reject) => {
604
+ parser.on("m3u", (m3u) => {
605
+ resolve(m3u);
606
+ parser.on("error", (exc) => {
607
+ debug(`Parser Error: ${JSON.stringify(exc)}`);
608
+ reject(exc);
609
+ });
610
+ });
611
+ });
612
+ } catch (err) {
613
+ return Promise.reject(`[${this.sessionId}]: Failed to Fetch URI: ${uri}\nERROR, ${err}`);
614
+ }
615
+ }
616
+
617
+ _mergeSegments(fromSegments, toSegments, prepend) {
618
+ const OUTPUT_SEGMENTS = {};
619
+ const fromBws = Object.keys(fromSegments);
620
+ const toBws = Object.keys(toSegments);
621
+ toBws.forEach((bw) => {
622
+ const targetBw = findNearestValue(bw, fromBws);
623
+ if (prepend) {
624
+ OUTPUT_SEGMENTS[bw] = fromSegments[targetBw].concat(toSegments[bw]);
625
+ OUTPUT_SEGMENTS[bw].unshift({ discontinuity: true });
626
+ } else {
627
+ const lastSeg = toSegments[bw][toSegments[bw].length - 1];
628
+ if (lastSeg.uri && !lastSeg.discontinuity) {
629
+ toSegments[bw].push({ discontinuity: true });
630
+ OUTPUT_SEGMENTS[bw] = toSegments[bw].concat(fromSegments[targetBw]);
631
+ } else {
632
+ OUTPUT_SEGMENTS[bw] = toSegments[bw].concat(fromSegments[targetBw]);
633
+ OUTPUT_SEGMENTS[bw].push({ discontinuity: true });
634
+ }
635
+ }
636
+ });
637
+ return OUTPUT_SEGMENTS;
638
+ }
499
639
  }
500
640
 
501
641
  module.exports = StreamSwitcher;
package/engine/util.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const { version } = require("../package.json");
2
+ const fetch = require("node-fetch");
2
3
 
3
4
  const filterQueryParser = (filterQuery) => {
4
5
  const conditions = filterQuery.match(/\(([^\(\)]*?)\)/g);
@@ -150,6 +151,68 @@ class WaitTimeGenerator {
150
151
  }
151
152
  }
152
153
 
154
+ const findNearestValue = (val, array) => {
155
+ const sorted = array.sort((a, b) => b - a);
156
+ return sorted.reduce((a, b) => {
157
+ return Math.abs(b - val) < Math.abs(a - val) ? b : a;
158
+ });
159
+ }
160
+
161
+ const isValidUrl = (url) => {
162
+ try {
163
+ const _url = new URL(url);
164
+ return true;
165
+ } catch (err) {
166
+ return false;
167
+ }
168
+ }
169
+
170
+
171
+ const fetchWithRetry = async (uri, opts, maxRetries, retryDelayMs, timeoutMs, debug) => {
172
+ if (!debug) {
173
+ var debug = (msg) => { console.log(msg); };
174
+ }
175
+ let tryFetchCount = 0;
176
+ const RETRY_LIMIT = maxRetries || 5;
177
+ const TIMEOUT_LIMIT = timeoutMs || 10 * 1000;
178
+ const RETRY_DELAY = retryDelayMs || 1000;
179
+ while (tryFetchCount < RETRY_LIMIT) {
180
+ tryFetchCount++;
181
+ debug(`Fetching URI -> ${uri}, attempt ${tryFetchCount} of ${maxRetries}`);
182
+ //
183
+ const controller = new AbortController();
184
+ const timeout = setTimeout(() => {
185
+ `Request Timeout after (${TIMEOUT_LIMIT})ms @ ${uri}`;
186
+ controller.abort();
187
+ }, TIMEOUT_LIMIT);
188
+ try {
189
+ const fetchOpts = Object.assign({ signal: controller.signal }, opts);
190
+ const response = await fetch(uri, fetchOpts);
191
+
192
+ if (response.status >= 400 && response.status < 600) {
193
+ const msg = `Bad response from URI: ${uri}\nReturned Status Code: ${response.status}`;
194
+ debug(msg);
195
+ if (tryFetchCount === maxRetries) {
196
+ return Promise.resolve(response);
197
+ }
198
+ debug(`Going to try fetch again in ${RETRY_DELAY}ms`);
199
+ await timer(RETRY_DELAY);
200
+ continue;
201
+ }
202
+ // Return Good response
203
+ return Promise.resolve(response);
204
+ } catch (err) {
205
+ debug(`Node-Fetch Error on URI: ${uri}\nFull Error -> ${err}`);
206
+ if (tryFetchCount === maxRetries) {
207
+ return Promise.reject(err);
208
+ }
209
+ continue;
210
+ } finally {
211
+ clearTimeout(timeout);
212
+ }
213
+ }
214
+ };
215
+
153
216
  module.exports = {
154
217
  filterQueryParser,
155
218
  applyFilter,
@@ -159,4 +222,7 @@ module.exports = {
159
222
  logerror,
160
223
  timer,
161
224
  WaitTimeGenerator,
225
+ findNearestValue,
226
+ isValidUrl,
227
+ fetchWithRetry
162
228
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyevinn-channel-engine",
3
- "version": "3.2.1",
3
+ "version": "3.2.2",
4
4
  "description": "OTT TV Channel Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server-livemix.js CHANGED
@@ -136,6 +136,11 @@ class StreamSwitchManager {
136
136
  return uuidv4();
137
137
  }
138
138
 
139
+ getPrerollUri(channelId) {
140
+ const defaultPrerollSlateUri = "https://maitv-vod.lab.eyevinn.technology/slate-consuo.mp4/master.m3u8"
141
+ return new Promise((resolve, reject) => { resolve(defaultPrerollSlateUri); });
142
+ }
143
+
139
144
  getSchedule(channelId) {
140
145
  return new Promise((resolve, reject) => {
141
146
  if (this.schedule[channelId]) {
@@ -155,7 +160,7 @@ class StreamSwitchManager {
155
160
  type: StreamType.LIVE,
156
161
  start_time: startOffset,
157
162
  end_time: endTime,
158
- uri: "https://d2fz24s2fts31b.cloudfront.net/out/v1/2e5b17b7dd0548c7b844eb1b311b6434/manifest.m3u8",
163
+ uri: "https://d2fz24s2fts31b.cloudfront.net/out/v1/6484d7c664924b77893f9b4f63080e5d/manifest.m3u8",
159
164
  },
160
165
  {
161
166
  eventId: this.generateID(),