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 +7 -4
- package/engine/session.js +34 -35
- package/engine/session_live.js +39 -12
- package/engine/stream_switcher.js +6 -0
- package/engine/util.js +30 -0
- package/package.json +1 -1
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
|
-
|
|
175
|
-
await
|
|
176
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
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();
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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 (
|
|
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
|
|
946
|
+
if (this.prevVodMediaSeq.video === null) {
|
|
944
947
|
this.prevVodMediaSeq.video = sessionState.vodMediaSeqVideo;
|
|
945
948
|
}
|
|
946
|
-
if
|
|
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:
|
package/engine/session_live.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
549
|
-
|
|
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
|
-
|
|
570
|
-
|
|
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
|
-
|
|
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
|
};
|