eyevinn-channel-engine 3.3.0 → 3.3.3
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 +128 -11
- package/engine/session_live.js +15 -14
- package/engine/stream_switcher.js +2 -2
- package/package.json +2 -2
- package/spec/engine/init_switching_spec.js +4 -6
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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 =
|
|
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)`);
|
|
@@ -388,7 +416,7 @@ class Session {
|
|
|
388
416
|
let mediaSequenceValue = 0;
|
|
389
417
|
if (currentVod.sequenceAlwaysContainNewSegments) {
|
|
390
418
|
mediaSequenceValue = currentVod.mediaSequenceValues[playheadState.vodMediaSeqVideo];
|
|
391
|
-
debug(`[${this._sessionId}]: {${mediaSequenceValue}}_{${currentVod.getLastSequenceMediaSequenceValue()}`);
|
|
419
|
+
debug(`[${this._sessionId}]: {${mediaSequenceValue}}_{${currentVod.getLastSequenceMediaSequenceValue()}}`);
|
|
392
420
|
} else {
|
|
393
421
|
mediaSequenceValue = playheadState.vodMediaSeqVideo;
|
|
394
422
|
}
|
|
@@ -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;
|
|
@@ -650,6 +690,18 @@ class Session {
|
|
|
650
690
|
debug(`[${this._sessionId}]: Not a leader and have just set 'playheadState.vodMediaSeqVideo' to 0|1. Invalidate cache to ensure having the correct VOD.`);
|
|
651
691
|
await this._sessionState.clearCurrentVodCache();
|
|
652
692
|
currentVod = await this._sessionState.getCurrentVod();
|
|
693
|
+
const diffMs = await this._playheadState.get("diffCompensation");
|
|
694
|
+
if (diffMs) {
|
|
695
|
+
this.diffCompensation = diffMs;
|
|
696
|
+
debug(`[${this._sessionId}]: Setting diffCompensation->${this.diffCompensation}`);
|
|
697
|
+
if (this.diffCompensation) {
|
|
698
|
+
this.timePositionOffset = this.diffCompensation;
|
|
699
|
+
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
700
|
+
{ event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
|
|
701
|
+
} else{
|
|
702
|
+
this.timePositionOffset = 0;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
653
705
|
this.isAllowedToClearVodCache = false;
|
|
654
706
|
}
|
|
655
707
|
} else {
|
|
@@ -1095,6 +1147,13 @@ class Session {
|
|
|
1095
1147
|
loadPromise = newVod.loadAfter(currentVod);
|
|
1096
1148
|
if (vodResponse.diffMs) {
|
|
1097
1149
|
this.diffCompensation = vodResponse.diffMs;
|
|
1150
|
+
if (this.diffCompensation) {
|
|
1151
|
+
this.timePositionOffset = this.diffCompensation;
|
|
1152
|
+
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
1153
|
+
{ event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
|
|
1154
|
+
} else{
|
|
1155
|
+
this.timePositionOffset = 0;
|
|
1156
|
+
}
|
|
1098
1157
|
}
|
|
1099
1158
|
} else {
|
|
1100
1159
|
loadPromise = new Promise((resolve, reject) => {
|
|
@@ -1114,15 +1173,17 @@ class Session {
|
|
|
1114
1173
|
debug(`[${this._sessionId}]: ${newVod.getPlayheadPositions()}`);
|
|
1115
1174
|
currentVod = newVod;
|
|
1116
1175
|
debug(`[${this._sessionId}]: msequences=${currentVod.getLiveMediaSequencesCount()}`);
|
|
1117
|
-
sessionState.mediaSeq = await this._sessionState.set("mediaSeq", sessionState.mediaSeq + endMseqValue);
|
|
1118
|
-
sessionState.discSeq = await this._sessionState.set("discSeq", sessionState.discSeq + lastDiscontinuity);
|
|
1119
1176
|
sessionState.vodMediaSeqVideo = await this._sessionState.set("vodMediaSeqVideo", 0);
|
|
1120
1177
|
sessionState.vodMediaSeqAudio = await this._sessionState.set("vodMediaSeqAudio", 0);
|
|
1178
|
+
sessionState.mediaSeq = await this._sessionState.set("mediaSeq", sessionState.mediaSeq + endMseqValue);
|
|
1179
|
+
sessionState.discSeq = await this._sessionState.set("discSeq", sessionState.discSeq + lastDiscontinuity);
|
|
1121
1180
|
debug(`[${this._sessionId}]: new sequence data set in store [${sessionState.mediaSeq}][${sessionState.discSeq}]`);
|
|
1122
1181
|
await this._sessionState.remove("nextVod");
|
|
1123
1182
|
sessionState.currentVod = await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
|
|
1124
1183
|
this.leaderIsSettingNextVod = false;
|
|
1125
1184
|
await this._playheadState.set("playheadRef", Date.now());
|
|
1185
|
+
await this._playheadState.set("diffCompensation", this.diffCompensation);
|
|
1186
|
+
debug(`[${this._sessionId}]: sharing durrent vods diffCompensation=${this.diffCompensation}`);
|
|
1126
1187
|
this.produceEvent({
|
|
1127
1188
|
type: 'NOW_PLAYING',
|
|
1128
1189
|
data: {
|
|
@@ -1138,6 +1199,18 @@ class Session {
|
|
|
1138
1199
|
// Allow Leader|Follower to clear vodCache...
|
|
1139
1200
|
this.isAllowedToClearVodCache = true;
|
|
1140
1201
|
sessionState.currentVod = await this._sessionState.getCurrentVod();
|
|
1202
|
+
const diffMs = await this._playheadState.get("diffCompensation");
|
|
1203
|
+
if (diffMs) {
|
|
1204
|
+
this.diffCompensation = diffMs;
|
|
1205
|
+
debug(`[${this._sessionId}]: Setting diffCompensation=${this.diffCompensation}`);
|
|
1206
|
+
if (this.diffCompensation) {
|
|
1207
|
+
this.timePositionOffset = this.diffCompensation;
|
|
1208
|
+
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
1209
|
+
{ event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
|
|
1210
|
+
} else{
|
|
1211
|
+
this.timePositionOffset = 0;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1141
1214
|
}
|
|
1142
1215
|
} catch (err) {
|
|
1143
1216
|
console.error(`[${this._sessionId}]: Failed to init next VOD`);
|
|
@@ -1209,6 +1282,9 @@ class Session {
|
|
|
1209
1282
|
await this._playheadState.set("playheadRef", Date.now());
|
|
1210
1283
|
debug(`[${this._sessionId}]: LEADER: Set new Reloaded VOD and vodMediaSeq counts in store.`);
|
|
1211
1284
|
// 4) emit cloudwatch event object
|
|
1285
|
+
debug(`[${this._sessionId}]: next VOD Reloaded (${currentVod.getDeltaTimes()})`);
|
|
1286
|
+
debug(`[${this._sessionId}]: ${currentVod.getPlayheadPositions()}`);
|
|
1287
|
+
debug(`[${this._sessionId}]: msequences=${currentVod.getLiveMediaSequencesCount()}`);
|
|
1212
1288
|
cloudWatchLog(!this.cloudWatchLogging, "engine-session", { event: "switchback", channel: this._sessionId, reqTimeMs: Date.now() - startTS });
|
|
1213
1289
|
|
|
1214
1290
|
return;
|
|
@@ -1435,6 +1511,47 @@ class Session {
|
|
|
1435
1511
|
debug(`[${this._sessionId}]: Current playhead position (${sessionState.vodMediaSeqVideo}): ${playheadPositions[sessionState.vodMediaSeqVideo]}`);
|
|
1436
1512
|
return playheadPositions[sessionState.vodMediaSeqVideo];
|
|
1437
1513
|
}
|
|
1514
|
+
|
|
1515
|
+
_getLastDuration(manifest) {
|
|
1516
|
+
return new Promise((resolve, reject) => {
|
|
1517
|
+
try {
|
|
1518
|
+
const parser = m3u8.createStream();
|
|
1519
|
+
let manifestStream = new Readable();
|
|
1520
|
+
manifestStream.push(manifest);
|
|
1521
|
+
manifestStream.push(null);
|
|
1522
|
+
|
|
1523
|
+
manifestStream.pipe(parser);
|
|
1524
|
+
parser.on('m3u', m3u => {
|
|
1525
|
+
if (m3u.items.PlaylistItem.length > 0) {
|
|
1526
|
+
const endIdx = m3u.items.PlaylistItem.length - 1;
|
|
1527
|
+
const bottomDuration = m3u.items.PlaylistItem[endIdx].get("duration");
|
|
1528
|
+
resolve(bottomDuration);
|
|
1529
|
+
} else {
|
|
1530
|
+
console.error("Empty media playlist");
|
|
1531
|
+
console.error(manifest);
|
|
1532
|
+
reject('Empty media playlist!')
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
} catch (err) {
|
|
1536
|
+
reject(err);
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
_getPlayheadDiffCompesationValue(diffMs, thresholdMs) {
|
|
1542
|
+
let compensationSec = 0;
|
|
1543
|
+
if (diffMs > thresholdMs) {
|
|
1544
|
+
compensationSec = (diffMs / 1000) - (thresholdMs / 1000);
|
|
1545
|
+
debug(`[${this._sessionId}]: Playhead stepping msequences too early. Need to wait longer. adding ${compensationSec}s`);
|
|
1546
|
+
return compensationSec;
|
|
1547
|
+
} else if (diffMs < -thresholdMs) {
|
|
1548
|
+
compensationSec = (diffMs / 1000) + (thresholdMs / 1000);
|
|
1549
|
+
debug(`[${this._sessionId}]: Playhead stepping msequences too LATE. Need to fast-forward. adding ${compensationSec}s`);
|
|
1550
|
+
return compensationSec;
|
|
1551
|
+
} else {
|
|
1552
|
+
return compensationSec;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1438
1555
|
}
|
|
1439
1556
|
|
|
1440
1557
|
module.exports = Session;
|
package/engine/session_live.js
CHANGED
|
@@ -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[
|
|
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
|
|
|
@@ -654,8 +654,8 @@ class SessionLive {
|
|
|
654
654
|
debug(`[${this.sessionId}]: Live Mseq counts=[${allMediaSeqCounts}]`);
|
|
655
655
|
// Decrement fetch counter
|
|
656
656
|
FETCH_ATTEMPTS--;
|
|
657
|
-
// Calculate retry delay time. Default=
|
|
658
|
-
let retryDelayMs =
|
|
657
|
+
// Calculate retry delay time. Default=1000
|
|
658
|
+
let retryDelayMs = 1000;
|
|
659
659
|
if (Object.keys(this.liveSegQueue).length > 0) {
|
|
660
660
|
const firstBw = Object.keys(this.liveSegQueue)[0];
|
|
661
661
|
const lastIdx = this.liveSegQueue[firstBw].length - 1;
|
|
@@ -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 &&
|
|
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(
|
|
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.
|
|
3
|
+
"version": "3.3.3",
|
|
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.
|
|
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
|
-
|
|
417
|
-
|
|
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:
|
|
423
|
-
uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/600/600-
|
|
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
|
});
|