eyevinn-channel-engine 3.3.2 → 3.3.5

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
@@ -109,6 +109,7 @@ Available options when constructing the Channel Engine object are:
109
109
  - `cloudWatchMetrics`: Output CloudWatch JSON metrics on console log. Default is false.
110
110
  - `useDemuxedAudio`: Enable playing VODs with multiple audio tracks. Default is false.
111
111
  - `alwaysNewSegments`: Force all new HLS media sequences to always contain at least 1 new segment. Default is false.
112
+ - `diffCompensationRate`: The rate for how much time is added on each sequence to slow down the playhead until it is back on schedule. Default is 0.5 (delay with half a segment duration).
112
113
 
113
114
  ## High Availability
114
115
 
package/engine/server.js CHANGED
@@ -109,6 +109,9 @@ class ChannelEngine {
109
109
  if (options && options.forceTargetDuration) {
110
110
  this.streamerOpts.forceTargetDuration = options.forceTargetDuration;
111
111
  }
112
+ if (options && options.diffCompensationRate) {
113
+ this.streamerOpts.diffCompensationRate = options.diffCompensationRate;
114
+ }
112
115
  const handleMasterRoute = async (req, res, next) => {
113
116
  debug(req.params);
114
117
  let m;
@@ -237,6 +240,7 @@ class ChannelEngine {
237
240
  maxTickInterval: channel.options && channel.options.maxTickInterval ? channel.options.maxTickInterval : this.streamerOpts.defaultMaxTickInterval,
238
241
  targetDurationPadding: channel.options && channel.options.targetDurationPadding ? channel.options.targetDurationPadding : this.streamerOpts.targetDurationPadding,
239
242
  forceTargetDuration: channel.options && channel.options.forceTargetDuration ? channel.options.forceTargetDuration : this.streamerOpts.forceTargetDuration,
243
+ diffCompensationRate: channel.options && channel.options.diffCompensationRate ? channel.options.diffCompensationRate : this.streamerOpts.diffCompensationRate,
240
244
  profile: channel.profile,
241
245
  audioTracks: channel.audioTracks,
242
246
  closedCaptions: channel.closedCaptions,
@@ -454,6 +458,7 @@ class ChannelEngine {
454
458
  options.maxTickInterval = this.streamerOpts.defaultMaxTickInterval;
455
459
  options.targetDurationPadding = this.streamerOpts.targetDurationPadding;
456
460
  options.forceTargetDuration = this.streamerOpts.forceTargetDuration;
461
+ options.diffCompensationRate = this.streamerOpts.diffCompensationRate;
457
462
  // if we are initiating a master manifest
458
463
  // outside of specific Channel context,
459
464
  // if slate options are set at the ChannelEngine level, then set these here
package/engine/session.js CHANGED
@@ -15,7 +15,7 @@ const ChaosMonkey = require('./chaos_monkey.js');
15
15
  const AVERAGE_SEGMENT_DURATION = 3000;
16
16
  const DEFAULT_PLAYHEAD_DIFF_THRESHOLD = 1000;
17
17
  const DEFAULT_MAX_TICK_INTERVAL = 10000;
18
- const DIFF_CORRECTION_RATE = 0.5;
18
+ const DEFAULT_DIFF_COMPENSATION_RATE = 0.5;
19
19
 
20
20
  const timer = ms => new Promise(res => setTimeout(res, ms));
21
21
 
@@ -43,6 +43,7 @@ class Session {
43
43
  this.cloudWatchLogging = false;
44
44
  this.playheadDiffThreshold = DEFAULT_PLAYHEAD_DIFF_THRESHOLD;
45
45
  this.maxTickInterval = DEFAULT_MAX_TICK_INTERVAL;
46
+ this.diffCompensationRate = DEFAULT_DIFF_COMPENSATION_RATE;
46
47
  this.diffCompensation = null;
47
48
  this.timePositionOffset = 0;
48
49
  this.prevVodMediaSeq = {
@@ -119,6 +120,9 @@ class Session {
119
120
  if (config.forceTargetDuration) {
120
121
  this.forceTargetDuration = config.forceTargetDuration;
121
122
  }
123
+ if (config.diffCompensationRate) {
124
+ this.diffCompensationRate = config.diffCompensationRate;
125
+ }
122
126
  }
123
127
  }
124
128
 
@@ -213,7 +217,7 @@ class Session {
213
217
  }
214
218
  // Apply external diff compensation if available.
215
219
  if (this.diffCompensation && this.diffCompensation > 0) {
216
- const DIFF_COMPENSATION = (reqTickInterval * DIFF_CORRECTION_RATE).toFixed(2) * 1000;
220
+ const DIFF_COMPENSATION = (reqTickInterval * this.diffCompensationRate).toFixed(2) * 1000;
217
221
  debug(`[${this._sessionId}]: Adding ${DIFF_COMPENSATION}msec to tickInterval to compensate for schedule diff (current=${this.diffCompensation}msec)`);
218
222
  tickInterval += (DIFF_COMPENSATION / 1000);
219
223
  this.diffCompensation -= DIFF_COMPENSATION;
@@ -562,14 +566,16 @@ class Session {
562
566
  if (currentVod) {
563
567
  try {
564
568
 
569
+ let manifestMseq = playheadState.mediaSeq + playheadState.vodMediaSeqVideo;
565
570
  if (currentVod.sequenceAlwaysContainNewSegments) {
566
571
  const mediaSequenceValue = currentVod.mediaSequenceValues[playheadState.vodMediaSeqVideo];
567
572
  debug(`[${this._sessionId}]: {${mediaSequenceValue}}_{${currentVod.getLastSequenceMediaSequenceValue()}}`);
573
+ manifestMseq = playheadState.mediaSeq + mediaSequenceValue;
568
574
  }
569
575
 
570
576
  debug(`[${this._sessionId}]: [${playheadState.vodMediaSeqVideo}]_[${currentVod.getLiveMediaSequencesCount()}]`);
571
577
  const m3u8 = currentVod.getLiveMediaSequences(playheadState.mediaSeq, bw, playheadState.vodMediaSeqVideo, sessionState.discSeq, this.targetDurationPadding, this.forceTargetDuration);
572
- debug(`[${this._sessionId}]: [${playheadState.mediaSeq + playheadState.vodMediaSeqVideo}][${sessionState.discSeq}][+${this.targetDurationPadding || 0}] Current media manifest for ${bw} requested`);
578
+ debug(`[${this._sessionId}]: [${manifestMseq}][${sessionState.discSeq}][+${this.targetDurationPadding || 0}] Current media manifest for ${bw} requested`);
573
579
  this.prevVodMediaSeq.video = playheadState.vodMediaSeqVideo;
574
580
  this.prevMediaSeqOffset.video = playheadState.mediaSeq;
575
581
  return m3u8;
@@ -1230,34 +1236,33 @@ class Session {
1230
1236
  try {
1231
1237
  debug(`[${this._sessionId}]: state=VOD_RELOAD_INIT`);
1232
1238
  if (isLeader) {
1233
-
1234
1239
  const startTS = Date.now();
1235
-
1236
1240
  // 1) To tell Follower that, Leader is working on it!
1237
1241
  sessionState.state = await this._sessionState.set("state", SessionState.VOD_RELOAD_INITIATING);
1238
-
1239
1242
  // 2) Set new 'offset' sequences, to carry on the continuity from session-live
1240
1243
  let mSeq = this.switchDataForSession.mediaSeq;
1241
1244
  let currentVod = await this._sessionState.getCurrentVod();
1242
1245
  if (currentVod.sequenceAlwaysContainNewSegments) {
1243
1246
  // (!) will need to compensate if using this setting on HLSVod Object.
1244
- mSeq -= 1;
1245
- }
1247
+ Object.keys(this.switchDataForSession.transitionSegments).forEach(bw => {
1248
+ let shiftedSeg = this.switchDataForSession.transitionSegments[bw].shift();
1249
+ if (shiftedSeg && shiftedSeg.discontinuity) {
1250
+ shiftedSeg = this.switchDataForSession.transitionSegments[bw].shift();
1251
+ }
1252
+ });
1253
+ }
1246
1254
  const dSeq = this.switchDataForSession.discSeq;
1247
1255
  const mSeqOffset = this.switchDataForSession.mediaSeqOffset;
1248
1256
  const reloadBehind = this.switchDataForSession.reloadBehind;
1249
1257
  const segments = this.switchDataForSession.transitionSegments;
1250
-
1251
1258
  if ([mSeq,dSeq,mSeqOffset,reloadBehind,segments].includes(null)) {
1252
1259
  debug(`[${this._sessionId}]: LEADER: Cannot Reload VOD, missing switch-back data`);
1253
1260
  return;
1254
1261
  }
1255
-
1256
1262
  await this._sessionState.set("mediaSeq", mSeq);
1257
1263
  await this._playheadState.set("mediaSeq", mSeq);
1258
1264
  await this._sessionState.set("discSeq", dSeq);
1259
1265
  debug(`[${this._sessionId}]: Setting current media and discontinuity count -> [${mSeq}]:[${dSeq}]`);
1260
-
1261
1266
  // 3) Set new media segments/currentVod, to carry on the continuity from session-live
1262
1267
  debug(`[${this._sessionId}]: LEADER: making changes to current VOD. I will also update currentVod in store.`);
1263
1268
  const playheadState = await this._playheadState.getValues(["vodMediaSeqVideo"]);
@@ -1266,8 +1271,10 @@ class Session {
1266
1271
  nextMseq = currentVod.getLiveMediaSequencesCount() - 1;
1267
1272
  }
1268
1273
 
1269
- // TODO: Support reloading with audioSegments as well
1270
-
1274
+ // ---------------------------------------------------.
1275
+ // TODO: Support reloading with audioSegments as well |
1276
+ // ---------------------------------------------------'
1277
+
1271
1278
  await currentVod.reload(nextMseq, segments, null, reloadBehind);
1272
1279
  await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
1273
1280
  await this._sessionState.set("vodReloaded", 1);
@@ -1276,13 +1283,12 @@ class Session {
1276
1283
  await this._playheadState.set("vodMediaSeqVideo", 0);
1277
1284
  await this._playheadState.set("vodMediaSeqAudio", 0);
1278
1285
  await this._playheadState.set("playheadRef", Date.now());
1286
+ // 4) Log to debug and cloudwatch
1279
1287
  debug(`[${this._sessionId}]: LEADER: Set new Reloaded VOD and vodMediaSeq counts in store.`);
1280
- // 4) emit cloudwatch event object
1281
1288
  debug(`[${this._sessionId}]: next VOD Reloaded (${currentVod.getDeltaTimes()})`);
1282
1289
  debug(`[${this._sessionId}]: ${currentVod.getPlayheadPositions()}`);
1283
1290
  debug(`[${this._sessionId}]: msequences=${currentVod.getLiveMediaSequencesCount()}`);
1284
1291
  cloudWatchLog(!this.cloudWatchLogging, "engine-session", { event: "switchback", channel: this._sessionId, reqTimeMs: Date.now() - startTS });
1285
-
1286
1292
  return;
1287
1293
  } else {
1288
1294
  debug(`[${this._sessionId}]: not a leader so will go directly to state VOD_RELOAD_INITIATING`);
@@ -218,6 +218,7 @@ class SessionLive {
218
218
  this.masterManifestUri = masterManifestUri;
219
219
  if (this.sessionLiveProfile) {
220
220
  this._filterLiveProfiles();
221
+ debug(`[${this.sessionId}]: Filtered Live profiles! (${Object.keys(this.mediaManifestURIs).length}) profiles left!`);
221
222
  }
222
223
  } catch (err) {
223
224
  this.masterManifestUri = null;
@@ -253,14 +254,30 @@ class SessionLive {
253
254
  if (segments[bw][0].discontinuity) {
254
255
  segments[bw].shift();
255
256
  }
256
-
257
+ let cueInExists = null;
257
258
  for (let segIdx = 0; segIdx < segments[bw].length; segIdx++) {
258
- this.vodSegments[bw].push(segments[bw][segIdx]);
259
+ const v2lSegment = segments[bw][segIdx];
260
+ if (v2lSegment.cue) {
261
+ if (v2lSegment.cue["in"]) {
262
+ cueInExists = true;
263
+ } else {
264
+ cueInExists = false;
265
+ }
266
+ }
267
+ this.vodSegments[bw].push(v2lSegment);
259
268
  }
260
- if (!segments[bw][segments[bw].length - 1].discontinuity) {
261
- this.vodSegments[bw].push({ discontinuity: true, cue: { in: true } });
269
+
270
+ const endIdx = segments[bw].length - 1;
271
+ if (!segments[bw][endIdx].discontinuity) {
272
+ const finalSegItem = { discontinuity: true };
273
+ if (!cueInExists) {
274
+ finalSegItem["cue"] = { in: true };
275
+ }
276
+ this.vodSegments[bw].push(finalSegItem);
262
277
  } else {
263
- segments[bw][segments[bw].length - 1]["cue"] = { in: true };
278
+ if (!cueInExists) {
279
+ segments[bw][endIdx]["cue"] = { in: true };
280
+ }
264
281
  }
265
282
  }
266
283
  } else {
@@ -601,7 +618,7 @@ class SessionLive {
601
618
  // ---------------------------------
602
619
  let FETCH_ATTEMPTS = 10;
603
620
  this.liveSegsForFollowers = {};
604
-
621
+ let bandwidthsToSkipOnRetry = [];
605
622
  while (FETCH_ATTEMPTS > 0) {
606
623
  if (isLeader) {
607
624
  debug(`[${this.sessionId}]: LEADER: Trying to fetch manifests for all bandwidths\n Attempts left=[${FETCH_ATTEMPTS}]`);
@@ -619,9 +636,15 @@ class SessionLive {
619
636
  let manifestList = [];
620
637
  this.pushAmount = 0;
621
638
  try {
639
+ if (bandwidthsToSkipOnRetry.length > 0) {
640
+ debug(`[${this.sessionId}]: (X) Skipping loadMedia promises for bws ${JSON.stringify(bandwidthsToSkipOnRetry)}`);
641
+ }
622
642
  // Collect Live Source Requesting Promises
623
643
  for (let i = 0; i < Object.keys(this.mediaManifestURIs).length; i++) {
624
644
  let bw = Object.keys(this.mediaManifestURIs)[i];
645
+ if (bandwidthsToSkipOnRetry.includes(bw)) {
646
+ continue;
647
+ }
625
648
  livePromises.push(this._loadMediaManifest(bw));
626
649
  debug(`[${this.sessionId}]: Pushed loadMedia promise for bw=[${bw}]`);
627
650
  }
@@ -642,16 +665,28 @@ class SessionLive {
642
665
  continue;
643
666
  }
644
667
 
645
- const allMediaSeqCounts = manifestList.map((item) => {
646
- if (item.status === "rejected") {
647
- return item.reason.mediaSeq;
668
+ // Store the results locally
669
+ manifestList.forEach((variantItem) => {
670
+ const bw = variantItem.value.bandwidth;
671
+ if (!this.liveSourceM3Us[bw]) {
672
+ this.liveSourceM3Us[bw] = {};
648
673
  }
649
- return item.value.mediaSeq;
674
+ this.liveSourceM3Us[bw] = variantItem.value;
650
675
  });
651
676
 
677
+ const allStoredMediaSeqCounts = Object.keys(this.liveSourceM3Us).map((variant) => this.liveSourceM3Us[variant].mediaSeq);
678
+
652
679
  // Handle if mediaSeqCounts are NOT synced up!
653
- if (!allMediaSeqCounts.every((val, i, arr) => val === arr[0])) {
654
- debug(`[${this.sessionId}]: Live Mseq counts=[${allMediaSeqCounts}]`);
680
+ if (!allStoredMediaSeqCounts.every((val, i, arr) => val === arr[0])) {
681
+ debug(`[${this.sessionId}]: Live Mseq counts=[${allStoredMediaSeqCounts}]`);
682
+ // Figure out what bw's are behind.
683
+ const higestMediaSeqCount = Math.max(...allStoredMediaSeqCounts);
684
+ bandwidthsToSkipOnRetry = Object.keys(this.liveSourceM3Us).filter((bw) => {
685
+ if (this.liveSourceM3Us[bw].mediaSeq === higestMediaSeqCount) {
686
+ return true;
687
+ }
688
+ return false;
689
+ });
655
690
  // Decrement fetch counter
656
691
  FETCH_ATTEMPTS--;
657
692
  // Calculate retry delay time. Default=1000
@@ -672,7 +707,7 @@ class SessionLive {
672
707
  continue;
673
708
  }
674
709
 
675
- currentMseqRaw = allMediaSeqCounts[0];
710
+ currentMseqRaw = allStoredMediaSeqCounts[0];
676
711
 
677
712
  if (!isLeader) {
678
713
  let leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts");
@@ -733,7 +768,6 @@ class SessionLive {
733
768
  if (currentMseqRaw === leadersFirstSeqCounts.liveSourceMseqCount) {
734
769
  this.pushAmount = 1; // Follower from start
735
770
  } else {
736
-
737
771
  // TODO: To support and account for past discontinuity tags in the Live Source stream,
738
772
  // we will need to get the real 'current' discontinuity-sequence count from Leader somehow.
739
773
 
@@ -1030,8 +1064,8 @@ class SessionLive {
1030
1064
  const resolveObj = {
1031
1065
  M3U: m3u,
1032
1066
  mediaSeq: m3u.get("mediaSequence"),
1067
+ bandwidth: liveTargetBandwidth,
1033
1068
  };
1034
- this.liveSourceM3Us[liveTargetBandwidth] = resolveObj;
1035
1069
  resolve(resolveObj);
1036
1070
  } catch (exc) {
1037
1071
  debug(`[${this.sessionId}]: Error when parsing latest manifest`);
@@ -626,7 +626,10 @@ class StreamSwitcher {
626
626
  } else {
627
627
  const lastSeg = toSegments[bw][toSegments[bw].length - 1];
628
628
  if (lastSeg.uri && !lastSeg.discontinuity) {
629
- toSegments[bw].push({ discontinuity: true });
629
+ toSegments[bw].push({ discontinuity: true, cue: { in: true } });
630
+ OUTPUT_SEGMENTS[bw] = toSegments[bw].concat(fromSegments[targetBw]);
631
+ } else if (lastSeg.discontinuity && !lastSeg.cue) {
632
+ toSegments[bw][toSegments[bw].length - 1].cue = { in: true }
630
633
  OUTPUT_SEGMENTS[bw] = toSegments[bw].concat(fromSegments[targetBw]);
631
634
  } else {
632
635
  OUTPUT_SEGMENTS[bw] = toSegments[bw].concat(fromSegments[targetBw]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyevinn-channel-engine",
3
- "version": "3.3.2",
3
+ "version": "3.3.5",
4
4
  "description": "OTT TV Channel Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {