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 +1 -0
- package/engine/server.js +5 -0
- package/engine/session.js +21 -15
- package/engine/session_live.js +49 -15
- package/engine/stream_switcher.js +4 -1
- package/package.json +1 -1
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
|
|
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 *
|
|
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}]: [${
|
|
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
|
-
|
|
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
|
-
//
|
|
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`);
|
package/engine/session_live.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
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 (!
|
|
654
|
-
debug(`[${this.sessionId}]: Live Mseq counts=[${
|
|
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 =
|
|
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]);
|