eyevinn-channel-engine 3.3.1 → 3.3.4

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,6 +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 DEFAULT_DIFF_COMPENSATION_RATE = 0.5;
18
19
 
19
20
  const timer = ms => new Promise(res => setTimeout(res, ms));
20
21
 
@@ -42,7 +43,9 @@ class Session {
42
43
  this.cloudWatchLogging = false;
43
44
  this.playheadDiffThreshold = DEFAULT_PLAYHEAD_DIFF_THRESHOLD;
44
45
  this.maxTickInterval = DEFAULT_MAX_TICK_INTERVAL;
46
+ this.diffCompensationRate = DEFAULT_DIFF_COMPENSATION_RATE;
45
47
  this.diffCompensation = null;
48
+ this.timePositionOffset = 0;
46
49
  this.prevVodMediaSeq = {
47
50
  video: null,
48
51
  audio: null
@@ -117,6 +120,9 @@ class Session {
117
120
  if (config.forceTargetDuration) {
118
121
  this.forceTargetDuration = config.forceTargetDuration;
119
122
  }
123
+ if (config.diffCompensationRate) {
124
+ this.diffCompensationRate = config.diffCompensationRate;
125
+ }
120
126
  }
121
127
  }
122
128
 
@@ -180,28 +186,43 @@ class Session {
180
186
  const reqTickInterval = playheadState.tickInterval;
181
187
  const timeSpentInIncrement = (tsIncrementEnd - tsIncrementBegin) / 1000;
182
188
  let tickInterval = reqTickInterval - timeSpentInIncrement;
189
+ // Apply HLSVod delta time for current msequence.
183
190
  const delta = await this._getCurrentDeltaTime();
184
191
  if (delta != 0) {
185
- debug(`[${this._sessionId}]: Delta time is != 0 need will adjust ${delta}sec to tick interval`);
186
192
  tickInterval += delta;
193
+ debug(`[${this._sessionId}]: Delta time is != 0 need will adjust ${delta}sec to tick interval. tick=${tickInterval}`);
187
194
  }
188
195
  const position = (await this._getCurrentPlayheadPosition()) * 1000;
189
- const timePosition = Date.now() - playheadState.playheadRef;
196
+ let timePosition = Date.now() - playheadState.playheadRef;
197
+ // Apply time position offset if set, only after external diff compensation has concluded.
198
+ if (this.timePositionOffset && this.diffCompensation <= 0 && this.alwaysNewSegments) {
199
+ timePosition -= this.timePositionOffset;
200
+ cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
201
+ { event: 'applyTimePositionOffset', channel: this._sessionId, offsetMs: this.timePositionOffset });
202
+ }
190
203
  const diff = position - timePosition;
191
204
  debug(`[${this._sessionId}]: ${timePosition}:${position}:${diff > 0 ? '+' : ''}${diff}ms`);
192
205
  cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
193
206
  { event: 'playheadDiff', channel: this._sessionId, diffMs: diff });
194
- if (diff > this.playheadDiffThreshold) {
195
- tickInterval += ((diff / 1000)) - (this.playheadDiffThreshold / 1000);
196
- } else if (diff < -this.playheadDiffThreshold) {
197
- tickInterval += ((diff / 1000)) + (this.playheadDiffThreshold / 1000);
198
- }
207
+ if (this.alwaysNewSegments) {
208
+ // Apply Playhead diff compensation, only after external diff compensation has concluded.
209
+ if (this.diffCompensation <= 0) {
210
+ const timeToAdd = this._getPlayheadDiffCompesationValue(diff, this.playheadDiffThreshold);
211
+ tickInterval += timeToAdd;
212
+ }
213
+ } else {
214
+ // Apply Playhead diff compensation, always.
215
+ const timeToAdd = this._getPlayheadDiffCompesationValue(diff, this.playheadDiffThreshold);
216
+ tickInterval += timeToAdd;
217
+ }
218
+ // Apply external diff compensation if available.
199
219
  if (this.diffCompensation && this.diffCompensation > 0) {
200
- const DIFF_COMPENSATION = 2000;
220
+ const DIFF_COMPENSATION = (reqTickInterval * this.diffCompensationRate).toFixed(2) * 1000;
201
221
  debug(`[${this._sessionId}]: Adding ${DIFF_COMPENSATION}msec to tickInterval to compensate for schedule diff (current=${this.diffCompensation}msec)`);
202
222
  tickInterval += (DIFF_COMPENSATION / 1000);
203
223
  this.diffCompensation -= DIFF_COMPENSATION;
204
224
  }
225
+ // Keep tickInterval within upper and lower limits.
205
226
  debug(`[${this._sessionId}]: Requested tickInterval=${tickInterval}s (max=${this.maxTickInterval / 1000}s, diffThreshold=${this.playheadDiffThreshold}msec)`);
206
227
  if (tickInterval <= 0.5) {
207
228
  tickInterval = 0.5;
@@ -214,6 +235,13 @@ class Session {
214
235
  await this._playheadState.set("tickMs", (tsTickEnd - tsIncrementBegin));
215
236
  cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
216
237
  { event: 'tickInterval', channel: this._sessionId, tickTimeMs: (tsTickEnd - tsIncrementBegin) });
238
+ if (this.alwaysNewSegments) {
239
+ // Use dynamic base-tickInterval. Set according to duration of latest segment.
240
+ const lastDuration = await this._getLastDuration(manifest);
241
+ const nextTickInterval = lastDuration < 2 ? 2 : lastDuration;
242
+ await this._playheadState.set("tickInterval", nextTickInterval);
243
+ cloudWatchLog(!this.cloudWatchLogging, "engine-session", { event: "tickIntervalUpdated", channel: this._sessionId, tickIntervalSec: nextTickInterval });
244
+ }
217
245
  }
218
246
  } catch (err) {
219
247
  debug(`[${this._sessionId}]: Playhead consumer crashed (1)`);
@@ -517,6 +545,18 @@ class Session {
517
545
  debug(`[${this._sessionId}]: Not a leader and first|second media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
518
546
  await this._sessionState.clearCurrentVodCache();
519
547
  this.isAllowedToClearVodCache = false;
548
+ const diffMs = await this._playheadState.get("diffCompensation");
549
+ if (diffMs) {
550
+ this.diffCompensation = diffMs;
551
+ debug(`[${this._sessionId}]: Setting diffCompensation->${this.diffCompensation}`);
552
+ if (this.diffCompensation) {
553
+ this.timePositionOffset = this.diffCompensation;
554
+ cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
555
+ { event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
556
+ } else{
557
+ this.timePositionOffset = 0;
558
+ }
559
+ }
520
560
  }
521
561
  } else {
522
562
  this.isAllowedToClearVodCache = true;
@@ -526,14 +566,16 @@ class Session {
526
566
  if (currentVod) {
527
567
  try {
528
568
 
569
+ let manifestMseq = playheadState.mediaSeq + playheadState.vodMediaSeqVideo;
529
570
  if (currentVod.sequenceAlwaysContainNewSegments) {
530
571
  const mediaSequenceValue = currentVod.mediaSequenceValues[playheadState.vodMediaSeqVideo];
531
572
  debug(`[${this._sessionId}]: {${mediaSequenceValue}}_{${currentVod.getLastSequenceMediaSequenceValue()}}`);
573
+ manifestMseq = playheadState.mediaSeq + mediaSequenceValue;
532
574
  }
533
575
 
534
576
  debug(`[${this._sessionId}]: [${playheadState.vodMediaSeqVideo}]_[${currentVod.getLiveMediaSequencesCount()}]`);
535
577
  const m3u8 = currentVod.getLiveMediaSequences(playheadState.mediaSeq, bw, playheadState.vodMediaSeqVideo, sessionState.discSeq, this.targetDurationPadding, this.forceTargetDuration);
536
- 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`);
537
579
  this.prevVodMediaSeq.video = playheadState.vodMediaSeqVideo;
538
580
  this.prevMediaSeqOffset.video = playheadState.mediaSeq;
539
581
  return m3u8;
@@ -650,6 +692,18 @@ class Session {
650
692
  debug(`[${this._sessionId}]: Not a leader and have just set 'playheadState.vodMediaSeqVideo' to 0|1. Invalidate cache to ensure having the correct VOD.`);
651
693
  await this._sessionState.clearCurrentVodCache();
652
694
  currentVod = await this._sessionState.getCurrentVod();
695
+ const diffMs = await this._playheadState.get("diffCompensation");
696
+ if (diffMs) {
697
+ this.diffCompensation = diffMs;
698
+ debug(`[${this._sessionId}]: Setting diffCompensation->${this.diffCompensation}`);
699
+ if (this.diffCompensation) {
700
+ this.timePositionOffset = this.diffCompensation;
701
+ cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
702
+ { event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
703
+ } else{
704
+ this.timePositionOffset = 0;
705
+ }
706
+ }
653
707
  this.isAllowedToClearVodCache = false;
654
708
  }
655
709
  } else {
@@ -1095,6 +1149,13 @@ class Session {
1095
1149
  loadPromise = newVod.loadAfter(currentVod);
1096
1150
  if (vodResponse.diffMs) {
1097
1151
  this.diffCompensation = vodResponse.diffMs;
1152
+ if (this.diffCompensation) {
1153
+ this.timePositionOffset = this.diffCompensation;
1154
+ cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
1155
+ { event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
1156
+ } else{
1157
+ this.timePositionOffset = 0;
1158
+ }
1098
1159
  }
1099
1160
  } else {
1100
1161
  loadPromise = new Promise((resolve, reject) => {
@@ -1123,6 +1184,8 @@ class Session {
1123
1184
  sessionState.currentVod = await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
1124
1185
  this.leaderIsSettingNextVod = false;
1125
1186
  await this._playheadState.set("playheadRef", Date.now());
1187
+ await this._playheadState.set("diffCompensation", this.diffCompensation);
1188
+ debug(`[${this._sessionId}]: sharing durrent vods diffCompensation=${this.diffCompensation}`);
1126
1189
  this.produceEvent({
1127
1190
  type: 'NOW_PLAYING',
1128
1191
  data: {
@@ -1138,6 +1201,18 @@ class Session {
1138
1201
  // Allow Leader|Follower to clear vodCache...
1139
1202
  this.isAllowedToClearVodCache = true;
1140
1203
  sessionState.currentVod = await this._sessionState.getCurrentVod();
1204
+ const diffMs = await this._playheadState.get("diffCompensation");
1205
+ if (diffMs) {
1206
+ this.diffCompensation = diffMs;
1207
+ debug(`[${this._sessionId}]: Setting diffCompensation=${this.diffCompensation}`);
1208
+ if (this.diffCompensation) {
1209
+ this.timePositionOffset = this.diffCompensation;
1210
+ cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
1211
+ { event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
1212
+ } else{
1213
+ this.timePositionOffset = 0;
1214
+ }
1215
+ }
1141
1216
  }
1142
1217
  } catch (err) {
1143
1218
  console.error(`[${this._sessionId}]: Failed to init next VOD`);
@@ -1161,34 +1236,33 @@ class Session {
1161
1236
  try {
1162
1237
  debug(`[${this._sessionId}]: state=VOD_RELOAD_INIT`);
1163
1238
  if (isLeader) {
1164
-
1165
1239
  const startTS = Date.now();
1166
-
1167
1240
  // 1) To tell Follower that, Leader is working on it!
1168
1241
  sessionState.state = await this._sessionState.set("state", SessionState.VOD_RELOAD_INITIATING);
1169
-
1170
1242
  // 2) Set new 'offset' sequences, to carry on the continuity from session-live
1171
1243
  let mSeq = this.switchDataForSession.mediaSeq;
1172
1244
  let currentVod = await this._sessionState.getCurrentVod();
1173
1245
  if (currentVod.sequenceAlwaysContainNewSegments) {
1174
1246
  // (!) will need to compensate if using this setting on HLSVod Object.
1175
- mSeq -= 1;
1176
- }
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
+ }
1177
1254
  const dSeq = this.switchDataForSession.discSeq;
1178
1255
  const mSeqOffset = this.switchDataForSession.mediaSeqOffset;
1179
1256
  const reloadBehind = this.switchDataForSession.reloadBehind;
1180
1257
  const segments = this.switchDataForSession.transitionSegments;
1181
-
1182
1258
  if ([mSeq,dSeq,mSeqOffset,reloadBehind,segments].includes(null)) {
1183
1259
  debug(`[${this._sessionId}]: LEADER: Cannot Reload VOD, missing switch-back data`);
1184
1260
  return;
1185
1261
  }
1186
-
1187
1262
  await this._sessionState.set("mediaSeq", mSeq);
1188
1263
  await this._playheadState.set("mediaSeq", mSeq);
1189
1264
  await this._sessionState.set("discSeq", dSeq);
1190
1265
  debug(`[${this._sessionId}]: Setting current media and discontinuity count -> [${mSeq}]:[${dSeq}]`);
1191
-
1192
1266
  // 3) Set new media segments/currentVod, to carry on the continuity from session-live
1193
1267
  debug(`[${this._sessionId}]: LEADER: making changes to current VOD. I will also update currentVod in store.`);
1194
1268
  const playheadState = await this._playheadState.getValues(["vodMediaSeqVideo"]);
@@ -1197,8 +1271,10 @@ class Session {
1197
1271
  nextMseq = currentVod.getLiveMediaSequencesCount() - 1;
1198
1272
  }
1199
1273
 
1200
- // TODO: Support reloading with audioSegments as well
1201
-
1274
+ // ---------------------------------------------------.
1275
+ // TODO: Support reloading with audioSegments as well |
1276
+ // ---------------------------------------------------'
1277
+
1202
1278
  await currentVod.reload(nextMseq, segments, null, reloadBehind);
1203
1279
  await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
1204
1280
  await this._sessionState.set("vodReloaded", 1);
@@ -1207,10 +1283,12 @@ class Session {
1207
1283
  await this._playheadState.set("vodMediaSeqVideo", 0);
1208
1284
  await this._playheadState.set("vodMediaSeqAudio", 0);
1209
1285
  await this._playheadState.set("playheadRef", Date.now());
1286
+ // 4) Log to debug and cloudwatch
1210
1287
  debug(`[${this._sessionId}]: LEADER: Set new Reloaded VOD and vodMediaSeq counts in store.`);
1211
- // 4) emit cloudwatch event object
1288
+ debug(`[${this._sessionId}]: next VOD Reloaded (${currentVod.getDeltaTimes()})`);
1289
+ debug(`[${this._sessionId}]: ${currentVod.getPlayheadPositions()}`);
1290
+ debug(`[${this._sessionId}]: msequences=${currentVod.getLiveMediaSequencesCount()}`);
1212
1291
  cloudWatchLog(!this.cloudWatchLogging, "engine-session", { event: "switchback", channel: this._sessionId, reqTimeMs: Date.now() - startTS });
1213
-
1214
1292
  return;
1215
1293
  } else {
1216
1294
  debug(`[${this._sessionId}]: not a leader so will go directly to state VOD_RELOAD_INITIATING`);
@@ -1435,6 +1513,47 @@ class Session {
1435
1513
  debug(`[${this._sessionId}]: Current playhead position (${sessionState.vodMediaSeqVideo}): ${playheadPositions[sessionState.vodMediaSeqVideo]}`);
1436
1514
  return playheadPositions[sessionState.vodMediaSeqVideo];
1437
1515
  }
1516
+
1517
+ _getLastDuration(manifest) {
1518
+ return new Promise((resolve, reject) => {
1519
+ try {
1520
+ const parser = m3u8.createStream();
1521
+ let manifestStream = new Readable();
1522
+ manifestStream.push(manifest);
1523
+ manifestStream.push(null);
1524
+
1525
+ manifestStream.pipe(parser);
1526
+ parser.on('m3u', m3u => {
1527
+ if (m3u.items.PlaylistItem.length > 0) {
1528
+ const endIdx = m3u.items.PlaylistItem.length - 1;
1529
+ const bottomDuration = m3u.items.PlaylistItem[endIdx].get("duration");
1530
+ resolve(bottomDuration);
1531
+ } else {
1532
+ console.error("Empty media playlist");
1533
+ console.error(manifest);
1534
+ reject('Empty media playlist!')
1535
+ }
1536
+ });
1537
+ } catch (err) {
1538
+ reject(err);
1539
+ }
1540
+ });
1541
+ }
1542
+
1543
+ _getPlayheadDiffCompesationValue(diffMs, thresholdMs) {
1544
+ let compensationSec = 0;
1545
+ if (diffMs > thresholdMs) {
1546
+ compensationSec = (diffMs / 1000) - (thresholdMs / 1000);
1547
+ debug(`[${this._sessionId}]: Playhead stepping msequences too early. Need to wait longer. adding ${compensationSec}s`);
1548
+ return compensationSec;
1549
+ } else if (diffMs < -thresholdMs) {
1550
+ compensationSec = (diffMs / 1000) + (thresholdMs / 1000);
1551
+ debug(`[${this._sessionId}]: Playhead stepping msequences too LATE. Need to fast-forward. adding ${compensationSec}s`);
1552
+ return compensationSec;
1553
+ } else {
1554
+ return compensationSec;
1555
+ }
1556
+ }
1438
1557
  }
1439
1558
 
1440
1559
  module.exports = Session;
@@ -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,21 +254,37 @@ 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 {
267
284
  debug(`[${this.sessionId}]: 'vodSegments' not empty = Using 'transitSegs'`);
268
285
  }
269
286
 
270
- debug(`[${this.sessionId}]: Setting CurrentMediaSequenceSegments. First seg is: [${this.vodSegments[allBws[0]][0].uri}]`);
287
+ debug(`[${this.sessionId}]: Setting CurrentMediaSequenceSegments. First seg is: [${this.vodSegments[Object.keys(this.vodSegments)[0]][0].uri}]`);
271
288
 
272
289
  const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId);
273
290
  if (isLeader) {
@@ -501,7 +518,7 @@ class SessionLive {
501
518
  this.liveSegQueue[liveBw] = [];
502
519
  }
503
520
  // Do not push duplicates
504
- const liveSegURIs = this.liveSegQueue[liveBw].filter(seg => seg.uri).map(seg => seg.uri);
521
+ const liveSegURIs = this.liveSegQueue[liveBw].filter((seg) => seg.uri).map((seg) => seg.uri);
505
522
  if (liveSegFromLeader.uri && liveSegURIs.includes(liveSegFromLeader.uri)) {
506
523
  debug(`[${this.sessionId}]: FOLLOWER: Found duplicate live segment. Skip push! (${liveBw})`);
507
524
  } 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
  }
@@ -638,20 +661,32 @@ class SessionLive {
638
661
  if (manifestList.some((result) => result.status === "rejected")) {
639
662
  FETCH_ATTEMPTS--;
640
663
  debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again in 1000ms...`);
641
- await timer(1000)
664
+ await timer(1000);
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,13 +707,13 @@ 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");
679
714
  let tries = 20;
680
715
 
681
- while (!isLeader && (!leadersFirstSeqCounts.liveSourceMseqCount && tries > 0) || leadersFirstSeqCounts.liveSourceMseqCount === 0) {
716
+ while ((!isLeader && !leadersFirstSeqCounts.liveSourceMseqCount && tries > 0) || leadersFirstSeqCounts.liveSourceMseqCount === 0) {
682
717
  debug(`[${this.sessionId}]: NEW FOLLOWER: Waiting for LEADER to add 'firstCounts' in store! Will look again after 1000ms (tries left=${tries})`);
683
718
  await timer(1000);
684
719
  leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts");
@@ -719,8 +754,9 @@ class SessionLive {
719
754
  }
720
755
  if (leadersFirstSeqCounts.mediaSeqCount !== this.mediaSeqCount) {
721
756
  this.mediaSeqCount = leadersFirstSeqCounts.mediaSeqCount;
722
- debug(`[${this.sessionId}]: FOLLOWER transistioned with wrong V2L segments, updating counts to [${
723
- this.mediaSeqCount}][${this.discSeqCount}], and reading 'transitSegs' from store`);
757
+ debug(
758
+ `[${this.sessionId}]: FOLLOWER transistioned with wrong V2L segments, updating counts to [${this.mediaSeqCount}][${this.discSeqCount}], and reading 'transitSegs' from store`
759
+ );
724
760
  const transitSegs = await this.sessionLiveState.get("transitSegs");
725
761
  if (!this._isEmpty(transitSegs)) {
726
762
  this.vodSegments = transitSegs;
@@ -732,7 +768,6 @@ class SessionLive {
732
768
  if (currentMseqRaw === leadersFirstSeqCounts.liveSourceMseqCount) {
733
769
  this.pushAmount = 1; // Follower from start
734
770
  } else {
735
-
736
771
  // TODO: To support and account for past discontinuity tags in the Live Source stream,
737
772
  // we will need to get the real 'current' discontinuity-sequence count from Leader somehow.
738
773
 
@@ -1029,8 +1064,8 @@ class SessionLive {
1029
1064
  const resolveObj = {
1030
1065
  M3U: m3u,
1031
1066
  mediaSeq: m3u.get("mediaSequence"),
1067
+ bandwidth: liveTargetBandwidth,
1032
1068
  };
1033
- this.liveSourceM3Us[liveTargetBandwidth] = resolveObj;
1034
1069
  resolve(resolveObj);
1035
1070
  } catch (exc) {
1036
1071
  debug(`[${this.sessionId}]: Error when parsing latest manifest`);
@@ -1070,7 +1105,7 @@ class SessionLive {
1070
1105
  if (startIdx < 0) {
1071
1106
  this.restAmount = startIdx * -1;
1072
1107
  startIdx = 0;
1073
- }
1108
+ }
1074
1109
  if (mediaManifestUri) {
1075
1110
  // push segments
1076
1111
  this._addLiveSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetBandwidth, isLeader);
@@ -1092,7 +1127,7 @@ class SessionLive {
1092
1127
  * @param {string} liveTargetBandwidth
1093
1128
  */
1094
1129
  _addLiveSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetBandwidth, isLeader) {
1095
- const leaderOrFollower = isLeader ? "LEADER": "NEW FOLLOWER";
1130
+ const leaderOrFollower = isLeader ? "LEADER" : "NEW FOLLOWER";
1096
1131
 
1097
1132
  for (let i = startIdx; i < playlistItems.length; i++) {
1098
1133
  let seg = {};
@@ -1162,7 +1197,7 @@ class SessionLive {
1162
1197
  seg["daterange"] = daterangeData;
1163
1198
  }
1164
1199
  // Push new Live Segments! But do not push duplicates
1165
- const liveSegURIs = this.liveSegQueue[liveTargetBandwidth].filter(seg => seg.uri).map(seg => seg.uri);
1200
+ const liveSegURIs = this.liveSegQueue[liveTargetBandwidth].filter((seg) => seg.uri).map((seg) => seg.uri);
1166
1201
  if (seg.uri && liveSegURIs.includes(seg.uri)) {
1167
1202
  debug(`[${this.sessionId}]: ${leaderOrFollower}: Found duplicate live segment. Skip push! (${liveTargetBandwidth})`);
1168
1203
  } else {
@@ -1372,12 +1407,12 @@ class SessionLive {
1372
1407
  if (this._isEmpty(this.liveSegQueue)) {
1373
1408
  return null;
1374
1409
  }
1375
-
1376
- const bw0 = Object.keys(this.liveSegQueue)[0];
1410
+
1411
+ const bw0 = Object.keys(this.liveSegQueue)[0];
1377
1412
  if (this.liveSegQueue[bw0].length === 0) {
1378
1413
  return null;
1379
1414
  }
1380
-
1415
+
1381
1416
  for (let i = 0; i < this.liveSegQueue[bw0].length; i++) {
1382
1417
  const segment = this.liveSegQueue[bw0][i];
1383
1418
  if (!segment.duration) {
@@ -482,7 +482,7 @@ class StreamSwitcher {
482
482
  if (mediaUri.match("^http")) {
483
483
  mediaURIs[bw] = mediaUri;
484
484
  } else {
485
- mediaURIs[bw] = new URL(mediaUri, uri);
485
+ mediaURIs[bw] = new URL(mediaUri, uri).href;
486
486
  }
487
487
  }
488
488
 
@@ -575,7 +575,7 @@ class StreamSwitcher {
575
575
  if (playlistItem.properties.uri.match("^http")) {
576
576
  segmentUri = playlistItem.properties.uri;
577
577
  } else {
578
- segmentUri = new URL(playlistItem.properties.uri, mediaURIs[bw]);
578
+ segmentUri = new URL(playlistItem.properties.uri, mediaURIs[bw]).href;
579
579
  }
580
580
  seg["duration"] = playlistItem.properties.duration;
581
581
  seg["uri"] = segmentUri;
@@ -626,7 +626,7 @@ 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
630
  OUTPUT_SEGMENTS[bw] = toSegments[bw].concat(fromSegments[targetBw]);
631
631
  } else {
632
632
  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.1",
3
+ "version": "3.3.4",
4
4
  "description": "OTT TV Channel Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -22,7 +22,7 @@
22
22
  "dependencies": {
23
23
  "@eyevinn/hls-repeat": "^0.1.1",
24
24
  "@eyevinn/hls-truncate": "^0.1.0",
25
- "@eyevinn/hls-vodtolive": "^2.0.1",
25
+ "@eyevinn/hls-vodtolive": "^2.0.2",
26
26
  "@eyevinn/m3u8": "^0.4.1",
27
27
  "abort-controller": "^3.0.0",
28
28
  "debug": "^3.2.7",
@@ -413,14 +413,12 @@ describe("The initialize switching", () => {
413
413
  uri: "http://mock.mock.com/180000/seg12.ts",
414
414
  });
415
415
  expect(sessionCurrentSegs["1313000"][size - 1 - 2]).toEqual({
416
- duration: 7.5,
417
- timelinePosition: null,
418
- uri: 'https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/600/600-00008.ts',
419
- cue: null
416
+ discontinuity: true,
417
+ cue: { in: true }
420
418
  });
421
419
  expect(sessionCurrentSegs["1313000"][size - 1]).toEqual({
422
- duration: 7.5,
423
- uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/600/600-00010.ts",
420
+ duration: 11.25,
421
+ uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/600/600-00015.ts",
424
422
  timelinePosition: null,
425
423
  cue: null,
426
424
  });