eyevinn-channel-engine 3.1.8 → 3.2.1

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/server.js CHANGED
@@ -12,7 +12,7 @@ const { SessionStateStore } = require('./session_state.js');
12
12
  const { SessionLiveStateStore } = require('./session_live_state.js');
13
13
  const { PlayheadStateStore } = require('./playhead_state.js');
14
14
 
15
- const { filterQueryParser, toHHMMSS } = require('./util.js');
15
+ const { filterQueryParser, toHHMMSS, WaitTimeGenerator } = require('./util.js');
16
16
  const { version } = require('../package.json');
17
17
 
18
18
  const timer = ms => new Promise(res => setTimeout(res, ms));
@@ -166,14 +166,17 @@ class ChannelEngine {
166
166
  }, 3000);
167
167
 
168
168
  const StreamSwitchLoop = async (timeIntervalMs) => {
169
+ const minIntervalMs = 50;
170
+ const WTG = new WaitTimeGenerator(timeIntervalMs, minIntervalMs);
169
171
  while(true) {
170
172
  try {
171
173
  const ts_1 = Date.now();
172
174
  await this.updateStreamSwitchAsync()
173
175
  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`)
176
+ const interval = (timeIntervalMs - (ts_2 - ts_1)) < 0 ? minIntervalMs : (timeIntervalMs - (ts_2 - ts_1));
177
+ const tickInterval = await WTG.getWaitTime(interval);
178
+ await timer(tickInterval)
179
+ debug(`StreamSwitchLoop waited for all channels. Next tick in: ${tickInterval}ms`)
177
180
  } catch (err) {
178
181
  console.error(err)
179
182
  debug(`StreamSwitchLoop iteration failed. Trying Again in 1000ms!`);
package/engine/session.js CHANGED
@@ -57,6 +57,7 @@ class Session {
57
57
  transitionSegments: null,
58
58
  reloadBehind: null,
59
59
  }
60
+ this.alreadyClearedVodCache = null;
60
61
 
61
62
  if (config) {
62
63
  if (config.sessionId) {
@@ -353,12 +354,9 @@ class Session {
353
354
  }
354
355
 
355
356
  const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo"]);
356
- if (playheadState.vodMediaSeqVideo === 0) {
357
- if (!isLeader) {
358
- debug(`[${this._sessionId}]: Not a leader and first media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
359
- await this._sessionState.clearCurrentVodCache(); // force reading up from shared store
360
- }
361
- }
357
+
358
+ // NOTE: Assume that VOD cache was already cleared in 'getCurrentMediaAndDiscSequenceCount()'
359
+ // and that we now have access to the correct vod cache
362
360
  const currentVod = await this._sessionState.getCurrentVod();
363
361
  if (currentVod) {
364
362
  try {
@@ -404,10 +402,11 @@ class Session {
404
402
 
405
403
  const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo"]);
406
404
  const discSeqOffset = await this._sessionState.get("discSeq");
405
+ // Clear Vod Cache here when Switching to Live just to be safe...
407
406
  if (playheadState.vodMediaSeqVideo === 0) {
408
407
  const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
409
408
  if (!isLeader) {
410
- debug(`[${this._sessionId}]: Not a leader and first media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
409
+ debug(`[${this._sessionId}]: Not a leader, about to switch, and first media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
411
410
  await this._sessionState.clearCurrentVodCache(); // force reading up from shared store
412
411
  }
413
412
  }
@@ -446,7 +445,7 @@ class Session {
446
445
  const sessionState = await this._sessionState.getValues(["vodMediaSeqVideo", "discSeq"]);
447
446
  const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo"]);
448
447
 
449
- if (this.prevVodMediaSeq.video !== null) {
448
+ if (this.prevVodMediaSeq.video === null) {
450
449
  this.prevVodMediaSeq.video = playheadState.vodMediaSeqVideo;
451
450
  }
452
451
 
@@ -461,13 +460,18 @@ class Session {
461
460
  }
462
461
  }
463
462
 
463
+ // Force reading up from store, but only once if the condition is right
464
464
  if (playheadState.vodMediaSeqVideo < 2 || playheadState.vodMediaSeqVideo < this.prevVodMediaSeq.video) {
465
465
  debug(`[${this._sessionId}]: current[${playheadState.vodMediaSeqVideo}]_prev[${this.prevVodMediaSeq.video}]`);
466
466
  const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
467
- if (!isLeader) {
467
+ if (!isLeader && !this.alreadyClearedVodCache) {
468
468
  debug(`[${this._sessionId}]: Not a leader and first|second media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
469
- await this._sessionState.clearCurrentVodCache(); // force reading up from shared store
469
+ await this._sessionState.clearCurrentVodCache();
470
+ this.alreadyClearedVodCache = true;
470
471
  }
472
+ } else {
473
+ // Reset the value
474
+ this.alreadyClearedVodCache = false;
471
475
  }
472
476
 
473
477
  const currentVod = await this._sessionState.getCurrentVod();
@@ -575,10 +579,23 @@ class Session {
575
579
  let m3u8 = currentVod.getLiveMediaSequences(playheadState.mediaSeq, 180000, playheadState.vodMediaSeqVideo, sessionState.discSeq);
576
580
  await this._playheadState.setLastM3u8(m3u8);
577
581
 
578
- if (!isLeader && playheadState.vodMediaSeqVideo < 2) {
579
- debug(`[${this._sessionId}]: Not a leader and have just set playheadState vodMediaSeqVideo to 0|1. Invalidate cache to ensure having the correct VOD.`);
580
- await this._sessionState.clearCurrentVodCache();
582
+ // As a FOLLOWER, we might need to read up from shared store...
583
+ if (playheadState.vodMediaSeqVideo < 2 || playheadState.vodMediaSeqVideo < this.prevVodMediaSeq.video) {
584
+ debug(`[${this._sessionId}]: current[${playheadState.vodMediaSeqVideo}]_prev[${this.prevVodMediaSeq.video}]`);
585
+ if (playheadState.vodMediaSeqVideo < this.prevVodMediaSeq.video) {
586
+ this.alreadyCleredVodCache = false;
587
+ }
588
+ if (!isLeader && !this.alreadyClearedVodCache) {
589
+ debug(`[${this._sessionId}]: Not a leader and have just set 'playheadState.vodMediaSeqVideo' to 0|1. Invalidate cache to ensure having the correct VOD.`);
590
+ await this._sessionState.clearCurrentVodCache();
591
+ this.alreadyClearedVodCache = true;
592
+ }
593
+ } else {
594
+ // Reset the value
595
+ this.alreadyClearedVodCache = false;
581
596
  }
597
+ // Update Value for Previous Sequence
598
+ this.prevVodMediaSeq.video = playheadState.vodMediaSeqVideo;
582
599
 
583
600
  return m3u8;
584
601
  }
@@ -834,13 +851,6 @@ class Session {
834
851
  sessionState.state = SessionState.VOD_INIT;
835
852
  }
836
853
 
837
- if (!isLeader && this.waitingForNextVod) {
838
- // By now Leader should have added the next Vod in store
839
- debug(`[${this._sessionId}]: Not leader! New VOD loaded during last tick. Invalidate current VOD cache`);
840
- await this._sessionState.clearCurrentVodCache();
841
- this.waitingForNextVod = false;
842
- }
843
-
844
854
  switch (sessionState.state) {
845
855
  case SessionState.VOD_INIT:
846
856
  case SessionState.VOD_INIT_BY_ID:
@@ -931,19 +941,13 @@ class Session {
931
941
  }
932
942
  }
933
943
  case SessionState.VOD_PLAYING:
934
- if (!isLeader) {
935
- if (sessionState.vodMediaSeqVideo === 0 || this.waitingForNextVod) {
936
- debug(`[${this._sessionId}]: Not leader! Invalidate current VOD cache and fetch the new one from the leader`);
937
- await this._sessionState.clearCurrentVodCache();
938
- currentVod = await this._sessionState.getCurrentVod();
939
- this.waitingForNextVod = false;
940
- }
941
- } else {
944
+ if (isLeader) {
942
945
  // Handle edge case where store has been reset, but leader has not cleared cache.
943
- if (this.prevVodMediaSeq.video !== null) {
946
+ if (this.prevVodMediaSeq.video === null) {
944
947
  this.prevVodMediaSeq.video = sessionState.vodMediaSeqVideo;
945
948
  }
946
- if (this.prevVodMediaSeq.video < sessionState.vodMediaSeqVideo) {
949
+ // Clear Cache if prev count is HIGHER than current...
950
+ if (sessionState.vodMediaSeqVideo < this.prevVodMediaSeq.video ) {
947
951
  debug(`[${this._sessionId}]: current[${sessionState.vodMediaSeqVideo}], prev[${this.prevVodMediaSeq.video}], total[${currentVod.getLiveMediaSequencesCount()}]`);
948
952
  await this._sessionState.clearCurrentVodCache();
949
953
  currentVod = await this._sessionState.getCurrentVod();
@@ -956,11 +960,6 @@ class Session {
956
960
  debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}, ${currentVod.getLiveMediaSequencesCount()})`);
957
961
  if (!isLeader) {
958
962
  debug(`[${this._sessionId}]: not the leader so just waiting for the VOD to be initiated`);
959
- if (sessionState.vodMediaSeqVideo === 0 || this.waitingForNextVod) {
960
- debug(`[${this._sessionId}]: First mediasequence in VOD and I am not the leader so invalidate current VOD cache and fetch the new one from the leader`);
961
- await this._sessionState.clearCurrentVodCache();
962
- }
963
- this.waitingForNextVod = true;
964
963
  }
965
964
  return;
966
965
  case SessionState.VOD_NEXT_INIT:
@@ -159,16 +159,16 @@ class SessionLive {
159
159
  this.waitForPlayhead = false;
160
160
 
161
161
  // Let the playhead move at an interval set according to live segment duration
162
- let liveSegmentDurationMs = DEFAULT_PLAYHEAD_INTERVAL_MS;
163
- let liveBws = Object.keys(this.liveSegQueue);
164
- if (liveBws.length !== 0 && this.liveSegQueue[liveBws[0]].length !== 0 && this.liveSegQueue[liveBws[0]][0].duration) {
165
- liveSegmentDurationMs = this.liveSegQueue[liveBws[0]][0].duration * 1000;
166
- }
162
+ const liveSegmentDurationMs = this._getAnyFirstSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS;
167
163
 
168
164
  // Set the timer
169
165
  let timerValueMs = 0;
170
166
  if (this.timerCompensation) {
171
- if (liveSegmentDurationMs - (tsIncrementEnd - tsIncrementBegin) > 0) {
167
+ const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId);
168
+ const incrementDuration = tsIncrementEnd - tsIncrementBegin;
169
+ if (incrementDuration >= liveSegmentDurationMs * 0.5 && isLeader) {
170
+ timerValueMs = liveSegmentDurationMs;
171
+ } else {
172
172
  timerValueMs = liveSegmentDurationMs - (tsIncrementEnd - tsIncrementBegin);
173
173
  }
174
174
  } else {
@@ -541,12 +541,14 @@ class SessionLive {
541
541
  if (!leadersMediaSeqRaw < this.lastRequestedMediaSeqRaw && this.blockGenerateManifest) {
542
542
  this.blockGenerateManifest = false;
543
543
  }
544
- let attempts = 5;
545
544
 
545
+ let attempts = 5;
546
546
  // CHECK AGAIN CASE 1: Store Empty
547
547
  while (!leadersMediaSeqRaw && attempts > 0) {
548
- debug(`[${this.sessionId}]: FOLLOWER: Leader has not put anything in store... Will check again in 2000ms (Tries left=[${attempts}])`);
549
- await timer(2000);
548
+ const segDur = this._getAnyFirstSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS;
549
+ const waitTimeMs = parseInt(segDur / 3, 10);
550
+ debug(`[${this.sessionId}]: FOLLOWER: Leader has not put anything in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`);
551
+ await timer(waitTimeMs);
550
552
  this.timerCompensation = false;
551
553
  leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw");
552
554
  attempts--;
@@ -566,8 +568,10 @@ class SessionLive {
566
568
  attempts = 5;
567
569
  // CHECK AGAIN CASE 2: Store Old
568
570
  while (leadersMediaSeqRaw <= this.lastRequestedMediaSeqRaw && attempts > 0) {
569
- debug(`[${this.sessionId}]: FOLLOWER: Cannot find anything NEW in store... Will check again in 2000ms (Tries left=[${attempts}])`);
570
- await timer(2000);
571
+ const segDur = this._getAnyFirstSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS;
572
+ const waitTimeMs = parseInt(segDur / 3, 10);
573
+ debug(`[${this.sessionId}]: FOLLOWER: Cannot find anything NEW in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`);
574
+ await timer(waitTimeMs);
571
575
  this.timerCompensation = false;
572
576
  leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw");
573
577
  attempts--;
@@ -632,7 +636,9 @@ class SessionLive {
632
636
 
633
637
  // Handle if any promise got rejected
634
638
  if (manifestList.some((result) => result.status === "rejected")) {
635
- debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again...`);
639
+ FETCH_ATTEMPTS--;
640
+ debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again in 1000ms...`);
641
+ await timer(1000)
636
642
  continue;
637
643
  }
638
644
 
@@ -1362,6 +1368,27 @@ class SessionLive {
1362
1368
  this.mediaManifestURIs = newItem;
1363
1369
  }
1364
1370
 
1371
+ _getAnyFirstSegmentDurationMs() {
1372
+ if (this._isEmpty(this.liveSegQueue)) {
1373
+ return null;
1374
+ }
1375
+
1376
+ const bw0 = Object.keys(this.liveSegQueue)[0];
1377
+ if (this.liveSegQueue[bw0].length === 0) {
1378
+ return null;
1379
+ }
1380
+
1381
+ for (let i = 0; i < this.liveSegQueue[bw0].length; i++) {
1382
+ const segment = this.liveSegQueue[bw0][i];
1383
+ if (!segment.duration) {
1384
+ continue;
1385
+ }
1386
+ return segment.duration * 1000;
1387
+ }
1388
+
1389
+ return null;
1390
+ }
1391
+
1365
1392
  _isEmpty(obj) {
1366
1393
  if (!obj) {
1367
1394
  return true;
@@ -3,6 +3,7 @@ const crypto = require("crypto");
3
3
  const fetch = require("node-fetch");
4
4
  const { AbortController } = require("abort-controller");
5
5
  const { SessionState } = require('./session_state');
6
+ const { timer } = require("./util")
6
7
 
7
8
  const SwitcherState = Object.freeze({
8
9
  V2L_TO_LIVE: 1,
@@ -151,6 +152,11 @@ class StreamSwitcher {
151
152
  );
152
153
  validURI = await this._validURI(scheduleObj.uri);
153
154
  tries++;
155
+ 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);
159
+ }
154
160
  }
155
161
  if (!validURI) {
156
162
  debug(`[${this.sessionId}]: Unreachable URI: [${scheduleObj.uri}]`);
package/engine/util.js CHANGED
@@ -122,6 +122,34 @@ const logerror = (sessionId, err) => {
122
122
  console.error(err);
123
123
  };
124
124
 
125
+ const timer = (ms) => new Promise((res) => setTimeout(res, ms));
126
+
127
+ class WaitTimeGenerator {
128
+ constructor(defaultIntervalMs, minValue) {
129
+ (this.timestamp = null), (this.prevWaitTime = null), (this.defaultIntervalMs = defaultIntervalMs || 3000), (this.minValue = minValue);
130
+ }
131
+ _getWaitTimeFromTimestamp() {
132
+ if (!this.timestamp) {
133
+ this.timestamp = new Date();
134
+ }
135
+ const sec = this.timestamp.getSeconds();
136
+ const defaultSec = this.defaultIntervalMs / 1000;
137
+ const d = parseInt(sec / defaultSec);
138
+ const waitSec = defaultSec * (d + 1) - sec;
139
+ return waitSec * 1000;
140
+ }
141
+ async getWaitTime(plannedTime) {
142
+ if (!this.timestamp || (this.prevWaitTime === this.minValue && plannedTime !== this.minValue)) {
143
+ this.timestamp = new Date();
144
+ const waitMs = this._getWaitTimeFromTimestamp();
145
+ this.prevWaitTime = waitMs;
146
+ return waitMs;
147
+ }
148
+ this.prevWaitTime = plannedTime;
149
+ return plannedTime;
150
+ }
151
+ }
152
+
125
153
  module.exports = {
126
154
  filterQueryParser,
127
155
  applyFilter,
@@ -129,4 +157,6 @@ module.exports = {
129
157
  m3u8Header,
130
158
  toHHMMSS,
131
159
  logerror,
160
+ timer,
161
+ WaitTimeGenerator,
132
162
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyevinn-channel-engine",
3
- "version": "3.1.8",
3
+ "version": "3.2.1",
4
4
  "description": "OTT TV Channel Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {