eyevinn-channel-engine 3.0.3 → 3.0.6

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/README.md CHANGED
@@ -141,8 +141,9 @@ class MyStreamSwitchManager {
141
141
  getSchedule(channelId) -> [ { eventId, assetId, title, type, start_time, end_time, uri, duration } ]
142
142
  }
143
143
  ```
144
+ When using **Live Mixing** in **High-Availablilty** mode, it is important to know that the class's `getSchedule(channelId)` function will be called by each channel-engine instance. Meaning that it is crucial that the implementation of this function will return the same response for each and every channel-engine instance, as they are expecting to be recieving identical lists in order to be synced up for live mixing.
144
145
 
145
- The class's `getSchedule(channelId)` function should return a list of event objects in the following format:
146
+ The class's `getSchedule(channelId)` function should return a promise containing the list of events as an object in the following format below:
146
147
 
147
148
  ```
148
149
  {
@@ -208,6 +209,7 @@ Below are examples of a Live stream event and a VOD event respectively:
208
209
  (*Important Note: When it comes to VOD events. Desired `duration` has priority over `end_time`. Meaning that you will only resume VOD2Live content after the asset in the VOD event has finished. This may change in the future.*)
209
210
 
210
211
 
212
+
211
213
  Then create an instance of the class and reference it as the `streamSwitchManager` in your engineOptions, just like you'd do with the channel manager.
212
214
 
213
215
  ```
package/engine/server.js CHANGED
@@ -167,12 +167,18 @@ class ChannelEngine {
167
167
 
168
168
  const StreamSwitchLoop = async (timeIntervalMs) => {
169
169
  while(true) {
170
- const ts_1 = Date.now();
171
- await this.updateStreamSwitchAsync()
172
- const ts_2 = Date.now();
173
- let interval = (timeIntervalMs - (ts_2 - ts_1)) < 0 ? 50 : (timeIntervalMs - (ts_2 - ts_1));
174
- await timer(interval)
175
- debug(`SteamSwitchLoop waited for all channels. Next tick in: ${interval}ms`)
170
+ try {
171
+ const ts_1 = Date.now();
172
+ await this.updateStreamSwitchAsync()
173
+ const ts_2 = Date.now();
174
+ let interval = (timeIntervalMs - (ts_2 - ts_1)) < 0 ? 50 : (timeIntervalMs - (ts_2 - ts_1));
175
+ await timer(interval)
176
+ debug(`StreamSwitchLoop waited for all channels. Next tick in: ${interval}ms`)
177
+ } catch (err) {
178
+ console.error(err)
179
+ debug(`StreamSwitchLoop iteration failed. Trying Again in 1000ms!`);
180
+ await timer(1000);
181
+ }
176
182
  }
177
183
  }
178
184
  StreamSwitchLoop(this.streamSwitchTimeIntervalMs);
@@ -136,7 +136,7 @@ class SessionLive {
136
136
  if (!this.masterManifestUri) {
137
137
  await timer(3000);
138
138
  continue;
139
- }
139
+ }
140
140
  if (this.playheadState === PlayheadState.STOPPED) {
141
141
  debug(`[${this.sessionId}]: Playhead has Stopped, clearing local session and store.`);
142
142
  this.waitForPlayhead = false;
@@ -208,7 +208,7 @@ class SessionLive {
208
208
  while (!this.masterManifestUri && attempts > 0) {
209
209
  attempts--;
210
210
  try {
211
- debug(`[${this.instanceId}][${this.sessionId}]: Going to fetch Live Master Manifest!`)
211
+ debug(`[${this.instanceId}][${this.sessionId}]: Going to fetch Live Master Manifest!`);
212
212
  // Load & Parse all Media Manifest URIs from Master
213
213
  await this._loadMasterManifest(masterManifestUri);
214
214
  this.masterManifestUri = masterManifestUri;
@@ -390,7 +390,7 @@ class SessionLive {
390
390
  // Generate manifest to give to client
391
391
  async getCurrentMediaManifestAsync(bw) {
392
392
  if (!this.sessionLiveState) {
393
- throw new Error('SessionLive not ready');
393
+ throw new Error("SessionLive not ready");
394
394
  }
395
395
  if (bw === null) {
396
396
  debug(`[${this.sessionId}]: No bandwidth provided`);
@@ -488,7 +488,11 @@ class SessionLive {
488
488
  const size = this.liveSegsForFollowers[liveBws[0]].length;
489
489
 
490
490
  let newSegCount = 0;
491
- this.liveSegsForFollowers[liveBws[0]].map((seg) => {if (seg.uri) { newSegCount++ }});
491
+ this.liveSegsForFollowers[liveBws[0]].map((seg) => {
492
+ if (seg.uri) {
493
+ newSegCount++;
494
+ }
495
+ });
492
496
 
493
497
  debug(`[${this.sessionId}]: FOLLOWER: liveSegsForFollowers Bws=${Object.keys(this.liveSegsForFollowers)}_newSegCount=${newSegCount} `);
494
498
  // Remove transitional segs & add live source segs collected from store
@@ -509,7 +513,7 @@ class SessionLive {
509
513
  // Push to bottom, new live source segment on all variants
510
514
  for (let segIdx = 0; segIdx < size; segIdx++) {
511
515
  let incrementDiscSeqCount = false;
512
- liveBws.forEach((bw)=>{
516
+ liveBws.forEach((bw) => {
513
517
  let liveSegFromLeader = this.liveSegsForFollowers[bw][segIdx];
514
518
  if (!this.liveSegQueue[bw]) {
515
519
  this.liveSegQueue[bw] = [];
@@ -517,7 +521,11 @@ class SessionLive {
517
521
  this.liveSegQueue[bw].push(liveSegFromLeader);
518
522
 
519
523
  let segCount = 0;
520
- this.liveSegQueue[bw].map((seg) => {if (seg.uri) { segCount++ }});
524
+ this.liveSegQueue[bw].map((seg) => {
525
+ if (seg.uri) {
526
+ segCount++;
527
+ }
528
+ });
521
529
  if (segCount > this.targetNumSeg) {
522
530
  let seg = this.liveSegQueue[bw].shift();
523
531
  if (seg && seg.discontinuity) {
@@ -651,7 +659,7 @@ class SessionLive {
651
659
  }
652
660
 
653
661
  // Handle if any promise got rejected
654
- if (manifestList.some(result => result.status === "rejected")) {
662
+ if (manifestList.some((result) => result.status === "rejected")) {
655
663
  debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again...`);
656
664
  continue;
657
665
  }
@@ -663,16 +671,21 @@ class SessionLive {
663
671
  return item.value.mediaSeq;
664
672
  });
665
673
 
666
-
667
674
  // Handle if mediaSeqCounts are NOT synced up!
668
675
  if (!allMediaSeqCounts.every((val, i, arr) => val === arr[0])) {
669
-
670
676
  debug(`[${this.sessionId}]: Live Mseq counts=[${allMediaSeqCounts}]`);
671
677
  // Decrement fetch counter
672
678
  FETCH_ATTEMPTS--;
679
+ // Calculate retry delay time. Default=1500
680
+ let retryDelayMs = 1500;
681
+ if (Object.keys(this.liveSegQueue).length > 0) {
682
+ const firstBw = Object.keys(this.liveSegQueue)[0];
683
+ const lastIdx = this.liveSegQueue[firstBw].length - 1;
684
+ retryDelayMs = this.liveSegQueue[firstBw][lastIdx].duration * 1000 * 0.25;
685
+ }
673
686
  // Wait a little before trying again
674
- debug(`[${this.sessionId}]: ALERT! Live Source Data NOT in sync! Will try again after 1500ms`);
675
- await timer(1500);
687
+ debug(`[${this.sessionId}]: ALERT! Live Source Data NOT in sync! Will try again after ${retryDelayMs}ms`);
688
+ await timer(retryDelayMs);
676
689
  this.timerCompensation = false;
677
690
  continue;
678
691
  }
@@ -681,7 +694,7 @@ class SessionLive {
681
694
  let leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts");
682
695
  let tries = 20;
683
696
 
684
- while (!leadersFirstSeqCounts.liveSourceMseqCount && tries > 0 || (leadersFirstSeqCounts.liveSourceMseqCount === 0)) {
697
+ while ((!leadersFirstSeqCounts.liveSourceMseqCount && tries > 0) || leadersFirstSeqCounts.liveSourceMseqCount === 0) {
685
698
  debug(`[${this.sessionId}]: NEW FOLLOWER: Waiting for LEADER to add 'firstCounts' in store! Will look again after 1000ms (tries left=${tries})`);
686
699
  await timer(1000);
687
700
  leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts");
@@ -727,7 +740,7 @@ class SessionLive {
727
740
  this.pushAmount = 1; // Follower from start
728
741
  } else {
729
742
  // RESPAWNED NODES
730
- this.pushAmount = (allMediaSeqCounts[0] - leadersFirstSeqCounts.liveSourceMseqCount) + 1;
743
+ this.pushAmount = allMediaSeqCounts[0] - leadersFirstSeqCounts.liveSourceMseqCount + 1;
731
744
 
732
745
  const transitSegs = await this.sessionLiveState.get("transitSegs");
733
746
  //debug(`[${this.sessionId}]: NEW FOLLOWER: I tried to get 'transitSegs'. This is what I found ${JSON.stringify(transitSegs)}`);
@@ -760,7 +773,7 @@ class SessionLive {
760
773
  }
761
774
 
762
775
  if (FETCH_ATTEMPTS === 0) {
763
- debug(`[${this.sessionId}]: Fetching from Live-Source did not work! Returning to Playhead Loop...`)
776
+ debug(`[${this.sessionId}]: Fetching from Live-Source did not work! Returning to Playhead Loop...`);
764
777
  return;
765
778
  }
766
779
 
@@ -781,8 +794,7 @@ class SessionLive {
781
794
  this.firstTime = false;
782
795
  debug(`[${this.sessionId}]: Got all needed segments from live-source (read from store).\nWe are now able to build Live Manifest: [${this.mediaSeqCount}]`);
783
796
  return;
784
- }
785
- else if (leadersCurrentMseqRaw < this.lastRequestedMediaSeqRaw) {
797
+ } else if (leadersCurrentMseqRaw < this.lastRequestedMediaSeqRaw) {
786
798
  // WE ARE A RESPAWN-NODE, and we are ahead of leader.
787
799
  this.blockGenerateManifest = true;
788
800
  }
@@ -798,7 +810,7 @@ class SessionLive {
798
810
  }
799
811
  // Segment Pushing
800
812
  debug(`[${this.sessionId}]: Executing Promises II: Segment Pushing`);
801
- let results = await allSettled(pushPromises);//await Promise.all(pushPromises);
813
+ let results = await allSettled(pushPromises); //await Promise.all(pushPromises);
802
814
  const removedDiscSeqList = results.map((item) => {
803
815
  if (item.status === "rejected") {
804
816
  return -1;
@@ -951,7 +963,9 @@ class SessionLive {
951
963
  // this.targetNumSeg = m3u.items.PlaylistItem.length;
952
964
  // }
953
965
 
954
- this.lastRequestedMediaSeqRaw = m3u.get("mediaSequence");
966
+ if (this.pushAmount >= 0) {
967
+ this.lastRequestedMediaSeqRaw = m3u.get("mediaSequence");
968
+ }
955
969
  this.targetDuration = m3u.get("targetDuration");
956
970
  let amountRemovedDiscSeqs = 0;
957
971
  let startIdx = m3u.items.PlaylistItem.length - this.pushAmount;
@@ -962,8 +976,8 @@ class SessionLive {
962
976
  }
963
977
  let resolveObj = {
964
978
  removedDiscSeqs: amountRemovedDiscSeqs,
965
- bw: liveTargetBandwidth
966
- }
979
+ bw: liveTargetBandwidth,
980
+ };
967
981
  resolve(resolveObj);
968
982
  } catch (exc) {
969
983
  console.error("ERROR: " + exc);
@@ -998,7 +1012,7 @@ class SessionLive {
998
1012
  }
999
1013
  cueData["out"] = true;
1000
1014
  cueData["duration"] = attributes["cueout"];
1001
- }
1015
+ }
1002
1016
  if ("cuecont" in attributes) {
1003
1017
  if (!cueData) {
1004
1018
  cueData = {};
@@ -1018,16 +1032,17 @@ class SessionLive {
1018
1032
  cueData["assetData"] = attributes["assetData"];
1019
1033
  }
1020
1034
  if ("daterange" in attributes) {
1021
- if(!daterangeData) {
1022
- daterangeData = {}
1035
+ if (!daterangeData) {
1036
+ daterangeData = {};
1023
1037
  }
1024
- daterangeData = {
1025
- id: attributes["daterange"]["ID"],
1026
- "start-date": attributes["daterange"]["START-DATE"],
1027
- "planned-duration": parseFloat(
1028
- attributes["daterange"]["PLANNED-DURATION"]
1029
- ),
1030
- };
1038
+ let allDaterangeAttributes = Object.keys(attributes["daterange"]);
1039
+ allDaterangeAttributes.forEach((attr) => {
1040
+ if (attr.match(/DURATION$/)) {
1041
+ daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]);
1042
+ } else {
1043
+ daterangeData[attr.toLowerCase()] = attributes["daterange"][attr];
1044
+ }
1045
+ });
1031
1046
  }
1032
1047
  if (playlistItem.properties.uri) {
1033
1048
  if (playlistItem.properties.uri.match("^http")) {
@@ -1045,10 +1060,14 @@ class SessionLive {
1045
1060
  this.liveSegQueue[liveTargetBandwidth].push(seg);
1046
1061
  this.liveSegsForFollowers[liveTargetBandwidth].push(seg);
1047
1062
  let segCount = 0;
1048
- this.liveSegQueue[liveTargetBandwidth].map((seg) => {if (seg.uri) { segCount++ }});
1063
+ this.liveSegQueue[liveTargetBandwidth].map((seg) => {
1064
+ if (seg.uri) {
1065
+ segCount++;
1066
+ }
1067
+ });
1049
1068
  debug(`[${this.sessionId}]: size of queue=${segCount}_targetNumseg=${this.targetNumSeg}`);
1050
1069
  if (segCount > this.targetNumSeg) {
1051
- debug(`[${this.sessionId}]: liveTargetBandwidth=${liveTargetBandwidth}_seg=${JSON.stringify(seg,null,2)}`);
1070
+ debug(`[${this.sessionId}]: liveTargetBandwidth=${liveTargetBandwidth}_seg=${JSON.stringify(seg, null, 2)}`);
1052
1071
  seg = this.liveSegQueue[liveTargetBandwidth].shift();
1053
1072
  if (seg) {
1054
1073
  if (seg.discontinuity) {
@@ -1078,8 +1097,7 @@ class SessionLive {
1078
1097
  }
1079
1098
  const liveTargetBandwidth = this._findNearestBw(bw, Object.keys(this.mediaManifestURIs));
1080
1099
  const vodTargetBandwidth = this._findNearestBw(bw, Object.keys(this.vodSegments));
1081
- debug(`[${this.sessionId}]: Client requesting manifest for bw=(${bw}). Nearest LiveBw=(${liveTargetBandwidth})`)
1082
-
1100
+ debug(`[${this.sessionId}]: Client requesting manifest for bw=(${bw}). Nearest LiveBw=(${liveTargetBandwidth})`);
1083
1101
 
1084
1102
  if (this.blockGenerateManifest) {
1085
1103
  debug(`[${this.sessionId}]: FOLLOWER: Cannot Generate Manifest! Waiting to sync-up with Leader...`);
@@ -1153,7 +1171,7 @@ class SessionLive {
1153
1171
  m3u8 += "#EXT-X-DISCONTINUITY\n";
1154
1172
  }
1155
1173
  if (seg.cue) {
1156
- if(seg.cue.out) {
1174
+ if (seg.cue.out) {
1157
1175
  if (seg.cue.scteData) {
1158
1176
  m3u8 += "#EXT-OATCLS-SCTE35:" + seg.cue.scteData + "\n";
1159
1177
  }
@@ -1165,21 +1183,25 @@ class SessionLive {
1165
1183
  if (seg.cue.cont) {
1166
1184
  if (seg.cue.scteData) {
1167
1185
  m3u8 += "#EXT-X-CUE-OUT-CONT:ElapsedTime=" + seg.cue.cont + ",Duration=" + seg.cue.duration + ",SCTE35=" + seg.cue.scteData + "\n";
1168
- }
1169
- else {
1186
+ } else {
1170
1187
  m3u8 += "#EXT-X-CUE-OUT-CONT:" + seg.cue.cont + "/" + seg.cue.duration + "\n";
1171
1188
  }
1172
1189
  }
1173
1190
  }
1191
+ if (seg.datetime) {
1192
+ m3u8 += `#EXT-X-PROGRAM-DATE-TIME:${seg.datetime}\n`;
1193
+ }
1174
1194
  if (seg.daterange) {
1175
- const dateRangeAttributes = Object.keys(seg.daterange).map(key => daterangeAttribute(key, seg.daterange[key])).join(',');
1176
- if (seg.daterange['start-date']) {
1177
- m3u8 += "#EXT-X-PROGRAM-DATE-TIME:" + seg.daterange['start-date'] + "\n";
1195
+ const dateRangeAttributes = Object.keys(seg.daterange)
1196
+ .map((key) => daterangeAttribute(key, seg.daterange[key]))
1197
+ .join(",");
1198
+ if (!seg.datetime && seg.daterange["start-date"]) {
1199
+ m3u8 += "#EXT-X-PROGRAM-DATE-TIME:" + seg.daterange["start-date"] + "\n";
1178
1200
  }
1179
1201
  m3u8 += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n";
1180
1202
  }
1181
1203
  // Mimick logic used in hls-vodtolive
1182
- if (seg.cue && seg.cue.in){
1204
+ if (seg.cue && seg.cue.in) {
1183
1205
  m3u8 += "#EXT-X-CUE-IN" + "\n";
1184
1206
  }
1185
1207
  if (seg.uri) {
@@ -1230,13 +1252,13 @@ class SessionLive {
1230
1252
  const profiles = this.sessionLiveProfile;
1231
1253
  const toKeep = new Set();
1232
1254
  let newItem = {};
1233
- profiles.forEach(profile => {
1255
+ profiles.forEach((profile) => {
1234
1256
  let bwToKeep = this._findNearestBw(profile.bw, Object.keys(this.mediaManifestURIs));
1235
1257
  toKeep.add(bwToKeep);
1236
1258
  });
1237
1259
  toKeep.forEach((bw) => {
1238
1260
  newItem[bw] = this.mediaManifestURIs[bw];
1239
- })
1261
+ });
1240
1262
  this.mediaManifestURIs = newItem;
1241
1263
  }
1242
1264
 
@@ -278,6 +278,9 @@ class StreamSwitcher {
278
278
  debug(`[${this.sessionId}]: [ Switched from V2L->LIVE ]`);
279
279
  return true;
280
280
  } catch (err) {
281
+ this.streamTypeLive = false;
282
+ this.working = false;
283
+ this.eventId = null;
281
284
  debug(`[${this.sessionId}]: [ ERROR Switching from V2L->LIVE ]`);
282
285
  throw new Error(err);
283
286
  }
@@ -309,6 +312,9 @@ class StreamSwitcher {
309
312
  debug(`[${this.sessionId}]: [ Switched from V2L->VOD ]`);
310
313
  return false;
311
314
  } catch (err) {
315
+ this.streamTypeLive = false;
316
+ this.working = false;
317
+ this.eventId = null;
312
318
  debug(`[${this.sessionId}]: [ ERROR Switching from V2L->VOD ]`);
313
319
  throw new Error(err);
314
320
  }
@@ -349,6 +355,9 @@ class StreamSwitcher {
349
355
  debug(`[${this.sessionId}]: [ Switched from LIVE->V2L ]`);
350
356
  return false;
351
357
  } catch (err) {
358
+ this.streamTypeLive = false;
359
+ this.working = false;
360
+ this.eventId = null;
352
361
  debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->V2L ]`);
353
362
  throw new Error(err);
354
363
  }
@@ -389,6 +398,9 @@ class StreamSwitcher {
389
398
  debug(`[${this.sessionId}]: Switched from LIVE->VOD`);
390
399
  return false;
391
400
  } catch (err) {
401
+ this.streamTypeLive = false;
402
+ this.working = false;
403
+ this.eventId = null;
392
404
  debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->VOD ]`);
393
405
  throw new Error(err);
394
406
  }
@@ -425,6 +437,9 @@ class StreamSwitcher {
425
437
  debug(`[${this.sessionId}]: Switched from LIVE->LIVE`);
426
438
  return true;
427
439
  } catch (err) {
440
+ this.streamTypeLive = false;
441
+ this.working = false;
442
+ this.eventId = null;
428
443
  debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->LIVE ]`);
429
444
  throw new Error(err);
430
445
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyevinn-channel-engine",
3
- "version": "3.0.3",
3
+ "version": "3.0.6",
4
4
  "description": "OTT TV Channel Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server-livemix.js CHANGED
@@ -155,7 +155,7 @@ class StreamSwitchManager {
155
155
  type: StreamType.LIVE,
156
156
  start_time: startOffset,
157
157
  end_time: endTime,
158
- uri: "https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8",
158
+ uri: "https://d2fz24s2fts31b.cloudfront.net/out/v1/2e5b17b7dd0548c7b844eb1b311b6434/manifest.m3u8",
159
159
  },
160
160
  {
161
161
  eventId: this.generateID(),