eyevinn-channel-engine 3.3.1 → 3.3.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
@@ -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 DIFF_CORRECTION_RATE = 0.5;
18
19
 
19
20
  const timer = ms => new Promise(res => setTimeout(res, ms));
20
21
 
@@ -43,6 +44,7 @@ class Session {
43
44
  this.playheadDiffThreshold = DEFAULT_PLAYHEAD_DIFF_THRESHOLD;
44
45
  this.maxTickInterval = DEFAULT_MAX_TICK_INTERVAL;
45
46
  this.diffCompensation = null;
47
+ this.timePositionOffset = 0;
46
48
  this.prevVodMediaSeq = {
47
49
  video: null,
48
50
  audio: null
@@ -180,28 +182,43 @@ class Session {
180
182
  const reqTickInterval = playheadState.tickInterval;
181
183
  const timeSpentInIncrement = (tsIncrementEnd - tsIncrementBegin) / 1000;
182
184
  let tickInterval = reqTickInterval - timeSpentInIncrement;
185
+ // Apply HLSVod delta time for current msequence.
183
186
  const delta = await this._getCurrentDeltaTime();
184
187
  if (delta != 0) {
185
- debug(`[${this._sessionId}]: Delta time is != 0 need will adjust ${delta}sec to tick interval`);
186
188
  tickInterval += delta;
189
+ debug(`[${this._sessionId}]: Delta time is != 0 need will adjust ${delta}sec to tick interval. tick=${tickInterval}`);
187
190
  }
188
191
  const position = (await this._getCurrentPlayheadPosition()) * 1000;
189
- const timePosition = Date.now() - playheadState.playheadRef;
192
+ let timePosition = Date.now() - playheadState.playheadRef;
193
+ // Apply time position offset if set, only after external diff compensation has concluded.
194
+ if (this.timePositionOffset && this.diffCompensation <= 0 && this.alwaysNewSegments) {
195
+ timePosition -= this.timePositionOffset;
196
+ cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
197
+ { event: 'applyTimePositionOffset', channel: this._sessionId, offsetMs: this.timePositionOffset });
198
+ }
190
199
  const diff = position - timePosition;
191
200
  debug(`[${this._sessionId}]: ${timePosition}:${position}:${diff > 0 ? '+' : ''}${diff}ms`);
192
201
  cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
193
202
  { 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
- }
203
+ if (this.alwaysNewSegments) {
204
+ // Apply Playhead diff compensation, only after external diff compensation has concluded.
205
+ if (this.diffCompensation <= 0) {
206
+ const timeToAdd = this._getPlayheadDiffCompesationValue(diff, this.playheadDiffThreshold);
207
+ tickInterval += timeToAdd;
208
+ }
209
+ } else {
210
+ // Apply Playhead diff compensation, always.
211
+ const timeToAdd = this._getPlayheadDiffCompesationValue(diff, this.playheadDiffThreshold);
212
+ tickInterval += timeToAdd;
213
+ }
214
+ // Apply external diff compensation if available.
199
215
  if (this.diffCompensation && this.diffCompensation > 0) {
200
- const DIFF_COMPENSATION = 2000;
216
+ const DIFF_COMPENSATION = (reqTickInterval * DIFF_CORRECTION_RATE).toFixed(2) * 1000;
201
217
  debug(`[${this._sessionId}]: Adding ${DIFF_COMPENSATION}msec to tickInterval to compensate for schedule diff (current=${this.diffCompensation}msec)`);
202
218
  tickInterval += (DIFF_COMPENSATION / 1000);
203
219
  this.diffCompensation -= DIFF_COMPENSATION;
204
220
  }
221
+ // Keep tickInterval within upper and lower limits.
205
222
  debug(`[${this._sessionId}]: Requested tickInterval=${tickInterval}s (max=${this.maxTickInterval / 1000}s, diffThreshold=${this.playheadDiffThreshold}msec)`);
206
223
  if (tickInterval <= 0.5) {
207
224
  tickInterval = 0.5;
@@ -214,6 +231,13 @@ class Session {
214
231
  await this._playheadState.set("tickMs", (tsTickEnd - tsIncrementBegin));
215
232
  cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
216
233
  { event: 'tickInterval', channel: this._sessionId, tickTimeMs: (tsTickEnd - tsIncrementBegin) });
234
+ if (this.alwaysNewSegments) {
235
+ // Use dynamic base-tickInterval. Set according to duration of latest segment.
236
+ const lastDuration = await this._getLastDuration(manifest);
237
+ const nextTickInterval = lastDuration < 2 ? 2 : lastDuration;
238
+ await this._playheadState.set("tickInterval", nextTickInterval);
239
+ cloudWatchLog(!this.cloudWatchLogging, "engine-session", { event: "tickIntervalUpdated", channel: this._sessionId, tickIntervalSec: nextTickInterval });
240
+ }
217
241
  }
218
242
  } catch (err) {
219
243
  debug(`[${this._sessionId}]: Playhead consumer crashed (1)`);
@@ -517,6 +541,18 @@ class Session {
517
541
  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
542
  await this._sessionState.clearCurrentVodCache();
519
543
  this.isAllowedToClearVodCache = false;
544
+ const diffMs = await this._playheadState.get("diffCompensation");
545
+ if (diffMs) {
546
+ this.diffCompensation = diffMs;
547
+ debug(`[${this._sessionId}]: Setting diffCompensation->${this.diffCompensation}`);
548
+ if (this.diffCompensation) {
549
+ this.timePositionOffset = this.diffCompensation;
550
+ cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
551
+ { event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
552
+ } else{
553
+ this.timePositionOffset = 0;
554
+ }
555
+ }
520
556
  }
521
557
  } else {
522
558
  this.isAllowedToClearVodCache = true;
@@ -650,6 +686,18 @@ class Session {
650
686
  debug(`[${this._sessionId}]: Not a leader and have just set 'playheadState.vodMediaSeqVideo' to 0|1. Invalidate cache to ensure having the correct VOD.`);
651
687
  await this._sessionState.clearCurrentVodCache();
652
688
  currentVod = await this._sessionState.getCurrentVod();
689
+ const diffMs = await this._playheadState.get("diffCompensation");
690
+ if (diffMs) {
691
+ this.diffCompensation = diffMs;
692
+ debug(`[${this._sessionId}]: Setting diffCompensation->${this.diffCompensation}`);
693
+ if (this.diffCompensation) {
694
+ this.timePositionOffset = this.diffCompensation;
695
+ cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
696
+ { event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
697
+ } else{
698
+ this.timePositionOffset = 0;
699
+ }
700
+ }
653
701
  this.isAllowedToClearVodCache = false;
654
702
  }
655
703
  } else {
@@ -1095,6 +1143,13 @@ class Session {
1095
1143
  loadPromise = newVod.loadAfter(currentVod);
1096
1144
  if (vodResponse.diffMs) {
1097
1145
  this.diffCompensation = vodResponse.diffMs;
1146
+ if (this.diffCompensation) {
1147
+ this.timePositionOffset = this.diffCompensation;
1148
+ cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
1149
+ { event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
1150
+ } else{
1151
+ this.timePositionOffset = 0;
1152
+ }
1098
1153
  }
1099
1154
  } else {
1100
1155
  loadPromise = new Promise((resolve, reject) => {
@@ -1123,6 +1178,8 @@ class Session {
1123
1178
  sessionState.currentVod = await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
1124
1179
  this.leaderIsSettingNextVod = false;
1125
1180
  await this._playheadState.set("playheadRef", Date.now());
1181
+ await this._playheadState.set("diffCompensation", this.diffCompensation);
1182
+ debug(`[${this._sessionId}]: sharing durrent vods diffCompensation=${this.diffCompensation}`);
1126
1183
  this.produceEvent({
1127
1184
  type: 'NOW_PLAYING',
1128
1185
  data: {
@@ -1138,6 +1195,18 @@ class Session {
1138
1195
  // Allow Leader|Follower to clear vodCache...
1139
1196
  this.isAllowedToClearVodCache = true;
1140
1197
  sessionState.currentVod = await this._sessionState.getCurrentVod();
1198
+ const diffMs = await this._playheadState.get("diffCompensation");
1199
+ if (diffMs) {
1200
+ this.diffCompensation = diffMs;
1201
+ debug(`[${this._sessionId}]: Setting diffCompensation=${this.diffCompensation}`);
1202
+ if (this.diffCompensation) {
1203
+ this.timePositionOffset = this.diffCompensation;
1204
+ cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
1205
+ { event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
1206
+ } else{
1207
+ this.timePositionOffset = 0;
1208
+ }
1209
+ }
1141
1210
  }
1142
1211
  } catch (err) {
1143
1212
  console.error(`[${this._sessionId}]: Failed to init next VOD`);
@@ -1209,6 +1278,9 @@ class Session {
1209
1278
  await this._playheadState.set("playheadRef", Date.now());
1210
1279
  debug(`[${this._sessionId}]: LEADER: Set new Reloaded VOD and vodMediaSeq counts in store.`);
1211
1280
  // 4) emit cloudwatch event object
1281
+ debug(`[${this._sessionId}]: next VOD Reloaded (${currentVod.getDeltaTimes()})`);
1282
+ debug(`[${this._sessionId}]: ${currentVod.getPlayheadPositions()}`);
1283
+ debug(`[${this._sessionId}]: msequences=${currentVod.getLiveMediaSequencesCount()}`);
1212
1284
  cloudWatchLog(!this.cloudWatchLogging, "engine-session", { event: "switchback", channel: this._sessionId, reqTimeMs: Date.now() - startTS });
1213
1285
 
1214
1286
  return;
@@ -1435,6 +1507,47 @@ class Session {
1435
1507
  debug(`[${this._sessionId}]: Current playhead position (${sessionState.vodMediaSeqVideo}): ${playheadPositions[sessionState.vodMediaSeqVideo]}`);
1436
1508
  return playheadPositions[sessionState.vodMediaSeqVideo];
1437
1509
  }
1510
+
1511
+ _getLastDuration(manifest) {
1512
+ return new Promise((resolve, reject) => {
1513
+ try {
1514
+ const parser = m3u8.createStream();
1515
+ let manifestStream = new Readable();
1516
+ manifestStream.push(manifest);
1517
+ manifestStream.push(null);
1518
+
1519
+ manifestStream.pipe(parser);
1520
+ parser.on('m3u', m3u => {
1521
+ if (m3u.items.PlaylistItem.length > 0) {
1522
+ const endIdx = m3u.items.PlaylistItem.length - 1;
1523
+ const bottomDuration = m3u.items.PlaylistItem[endIdx].get("duration");
1524
+ resolve(bottomDuration);
1525
+ } else {
1526
+ console.error("Empty media playlist");
1527
+ console.error(manifest);
1528
+ reject('Empty media playlist!')
1529
+ }
1530
+ });
1531
+ } catch (err) {
1532
+ reject(err);
1533
+ }
1534
+ });
1535
+ }
1536
+
1537
+ _getPlayheadDiffCompesationValue(diffMs, thresholdMs) {
1538
+ let compensationSec = 0;
1539
+ if (diffMs > thresholdMs) {
1540
+ compensationSec = (diffMs / 1000) - (thresholdMs / 1000);
1541
+ debug(`[${this._sessionId}]: Playhead stepping msequences too early. Need to wait longer. adding ${compensationSec}s`);
1542
+ return compensationSec;
1543
+ } else if (diffMs < -thresholdMs) {
1544
+ compensationSec = (diffMs / 1000) + (thresholdMs / 1000);
1545
+ debug(`[${this._sessionId}]: Playhead stepping msequences too LATE. Need to fast-forward. adding ${compensationSec}s`);
1546
+ return compensationSec;
1547
+ } else {
1548
+ return compensationSec;
1549
+ }
1550
+ }
1438
1551
  }
1439
1552
 
1440
1553
  module.exports = Session;
@@ -267,7 +267,7 @@ class SessionLive {
267
267
  debug(`[${this.sessionId}]: 'vodSegments' not empty = Using 'transitSegs'`);
268
268
  }
269
269
 
270
- debug(`[${this.sessionId}]: Setting CurrentMediaSequenceSegments. First seg is: [${this.vodSegments[allBws[0]][0].uri}]`);
270
+ debug(`[${this.sessionId}]: Setting CurrentMediaSequenceSegments. First seg is: [${this.vodSegments[Object.keys(this.vodSegments)[0]][0].uri}]`);
271
271
 
272
272
  const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId);
273
273
  if (isLeader) {
@@ -501,7 +501,7 @@ class SessionLive {
501
501
  this.liveSegQueue[liveBw] = [];
502
502
  }
503
503
  // Do not push duplicates
504
- const liveSegURIs = this.liveSegQueue[liveBw].filter(seg => seg.uri).map(seg => seg.uri);
504
+ const liveSegURIs = this.liveSegQueue[liveBw].filter((seg) => seg.uri).map((seg) => seg.uri);
505
505
  if (liveSegFromLeader.uri && liveSegURIs.includes(liveSegFromLeader.uri)) {
506
506
  debug(`[${this.sessionId}]: FOLLOWER: Found duplicate live segment. Skip push! (${liveBw})`);
507
507
  } else {
@@ -638,7 +638,7 @@ class SessionLive {
638
638
  if (manifestList.some((result) => result.status === "rejected")) {
639
639
  FETCH_ATTEMPTS--;
640
640
  debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again in 1000ms...`);
641
- await timer(1000)
641
+ await timer(1000);
642
642
  continue;
643
643
  }
644
644
 
@@ -678,7 +678,7 @@ class SessionLive {
678
678
  let leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts");
679
679
  let tries = 20;
680
680
 
681
- while (!isLeader && (!leadersFirstSeqCounts.liveSourceMseqCount && tries > 0) || leadersFirstSeqCounts.liveSourceMseqCount === 0) {
681
+ while ((!isLeader && !leadersFirstSeqCounts.liveSourceMseqCount && tries > 0) || leadersFirstSeqCounts.liveSourceMseqCount === 0) {
682
682
  debug(`[${this.sessionId}]: NEW FOLLOWER: Waiting for LEADER to add 'firstCounts' in store! Will look again after 1000ms (tries left=${tries})`);
683
683
  await timer(1000);
684
684
  leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts");
@@ -719,8 +719,9 @@ class SessionLive {
719
719
  }
720
720
  if (leadersFirstSeqCounts.mediaSeqCount !== this.mediaSeqCount) {
721
721
  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`);
722
+ debug(
723
+ `[${this.sessionId}]: FOLLOWER transistioned with wrong V2L segments, updating counts to [${this.mediaSeqCount}][${this.discSeqCount}], and reading 'transitSegs' from store`
724
+ );
724
725
  const transitSegs = await this.sessionLiveState.get("transitSegs");
725
726
  if (!this._isEmpty(transitSegs)) {
726
727
  this.vodSegments = transitSegs;
@@ -1070,7 +1071,7 @@ class SessionLive {
1070
1071
  if (startIdx < 0) {
1071
1072
  this.restAmount = startIdx * -1;
1072
1073
  startIdx = 0;
1073
- }
1074
+ }
1074
1075
  if (mediaManifestUri) {
1075
1076
  // push segments
1076
1077
  this._addLiveSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetBandwidth, isLeader);
@@ -1092,7 +1093,7 @@ class SessionLive {
1092
1093
  * @param {string} liveTargetBandwidth
1093
1094
  */
1094
1095
  _addLiveSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetBandwidth, isLeader) {
1095
- const leaderOrFollower = isLeader ? "LEADER": "NEW FOLLOWER";
1096
+ const leaderOrFollower = isLeader ? "LEADER" : "NEW FOLLOWER";
1096
1097
 
1097
1098
  for (let i = startIdx; i < playlistItems.length; i++) {
1098
1099
  let seg = {};
@@ -1162,7 +1163,7 @@ class SessionLive {
1162
1163
  seg["daterange"] = daterangeData;
1163
1164
  }
1164
1165
  // Push new Live Segments! But do not push duplicates
1165
- const liveSegURIs = this.liveSegQueue[liveTargetBandwidth].filter(seg => seg.uri).map(seg => seg.uri);
1166
+ const liveSegURIs = this.liveSegQueue[liveTargetBandwidth].filter((seg) => seg.uri).map((seg) => seg.uri);
1166
1167
  if (seg.uri && liveSegURIs.includes(seg.uri)) {
1167
1168
  debug(`[${this.sessionId}]: ${leaderOrFollower}: Found duplicate live segment. Skip push! (${liveTargetBandwidth})`);
1168
1169
  } else {
@@ -1372,12 +1373,12 @@ class SessionLive {
1372
1373
  if (this._isEmpty(this.liveSegQueue)) {
1373
1374
  return null;
1374
1375
  }
1375
-
1376
- const bw0 = Object.keys(this.liveSegQueue)[0];
1376
+
1377
+ const bw0 = Object.keys(this.liveSegQueue)[0];
1377
1378
  if (this.liveSegQueue[bw0].length === 0) {
1378
1379
  return null;
1379
1380
  }
1380
-
1381
+
1381
1382
  for (let i = 0; i < this.liveSegQueue[bw0].length; i++) {
1382
1383
  const segment = this.liveSegQueue[bw0][i];
1383
1384
  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;
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.2",
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
  });