eyevinn-channel-engine 3.3.1 → 3.3.4
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 +140 -21
- package/engine/session_live.js +62 -27
- package/engine/stream_switcher.js +3 -3
- 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)`);
|
|
@@ -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;
|
|
@@ -526,14 +566,16 @@ class Session {
|
|
|
526
566
|
if (currentVod) {
|
|
527
567
|
try {
|
|
528
568
|
|
|
569
|
+
let manifestMseq = playheadState.mediaSeq + playheadState.vodMediaSeqVideo;
|
|
529
570
|
if (currentVod.sequenceAlwaysContainNewSegments) {
|
|
530
571
|
const mediaSequenceValue = currentVod.mediaSequenceValues[playheadState.vodMediaSeqVideo];
|
|
531
572
|
debug(`[${this._sessionId}]: {${mediaSequenceValue}}_{${currentVod.getLastSequenceMediaSequenceValue()}}`);
|
|
573
|
+
manifestMseq = playheadState.mediaSeq + mediaSequenceValue;
|
|
532
574
|
}
|
|
533
575
|
|
|
534
576
|
debug(`[${this._sessionId}]: [${playheadState.vodMediaSeqVideo}]_[${currentVod.getLiveMediaSequencesCount()}]`);
|
|
535
577
|
const m3u8 = currentVod.getLiveMediaSequences(playheadState.mediaSeq, bw, playheadState.vodMediaSeqVideo, sessionState.discSeq, this.targetDurationPadding, this.forceTargetDuration);
|
|
536
|
-
debug(`[${this._sessionId}]: [${
|
|
578
|
+
debug(`[${this._sessionId}]: [${manifestMseq}][${sessionState.discSeq}][+${this.targetDurationPadding || 0}] Current media manifest for ${bw} requested`);
|
|
537
579
|
this.prevVodMediaSeq.video = playheadState.vodMediaSeqVideo;
|
|
538
580
|
this.prevMediaSeqOffset.video = playheadState.mediaSeq;
|
|
539
581
|
return m3u8;
|
|
@@ -650,6 +692,18 @@ class Session {
|
|
|
650
692
|
debug(`[${this._sessionId}]: Not a leader and have just set 'playheadState.vodMediaSeqVideo' to 0|1. Invalidate cache to ensure having the correct VOD.`);
|
|
651
693
|
await this._sessionState.clearCurrentVodCache();
|
|
652
694
|
currentVod = await this._sessionState.getCurrentVod();
|
|
695
|
+
const diffMs = await this._playheadState.get("diffCompensation");
|
|
696
|
+
if (diffMs) {
|
|
697
|
+
this.diffCompensation = diffMs;
|
|
698
|
+
debug(`[${this._sessionId}]: Setting diffCompensation->${this.diffCompensation}`);
|
|
699
|
+
if (this.diffCompensation) {
|
|
700
|
+
this.timePositionOffset = this.diffCompensation;
|
|
701
|
+
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
702
|
+
{ event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
|
|
703
|
+
} else{
|
|
704
|
+
this.timePositionOffset = 0;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
653
707
|
this.isAllowedToClearVodCache = false;
|
|
654
708
|
}
|
|
655
709
|
} else {
|
|
@@ -1095,6 +1149,13 @@ class Session {
|
|
|
1095
1149
|
loadPromise = newVod.loadAfter(currentVod);
|
|
1096
1150
|
if (vodResponse.diffMs) {
|
|
1097
1151
|
this.diffCompensation = vodResponse.diffMs;
|
|
1152
|
+
if (this.diffCompensation) {
|
|
1153
|
+
this.timePositionOffset = this.diffCompensation;
|
|
1154
|
+
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
1155
|
+
{ event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
|
|
1156
|
+
} else{
|
|
1157
|
+
this.timePositionOffset = 0;
|
|
1158
|
+
}
|
|
1098
1159
|
}
|
|
1099
1160
|
} else {
|
|
1100
1161
|
loadPromise = new Promise((resolve, reject) => {
|
|
@@ -1123,6 +1184,8 @@ class Session {
|
|
|
1123
1184
|
sessionState.currentVod = await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
|
|
1124
1185
|
this.leaderIsSettingNextVod = false;
|
|
1125
1186
|
await this._playheadState.set("playheadRef", Date.now());
|
|
1187
|
+
await this._playheadState.set("diffCompensation", this.diffCompensation);
|
|
1188
|
+
debug(`[${this._sessionId}]: sharing durrent vods diffCompensation=${this.diffCompensation}`);
|
|
1126
1189
|
this.produceEvent({
|
|
1127
1190
|
type: 'NOW_PLAYING',
|
|
1128
1191
|
data: {
|
|
@@ -1138,6 +1201,18 @@ class Session {
|
|
|
1138
1201
|
// Allow Leader|Follower to clear vodCache...
|
|
1139
1202
|
this.isAllowedToClearVodCache = true;
|
|
1140
1203
|
sessionState.currentVod = await this._sessionState.getCurrentVod();
|
|
1204
|
+
const diffMs = await this._playheadState.get("diffCompensation");
|
|
1205
|
+
if (diffMs) {
|
|
1206
|
+
this.diffCompensation = diffMs;
|
|
1207
|
+
debug(`[${this._sessionId}]: Setting diffCompensation=${this.diffCompensation}`);
|
|
1208
|
+
if (this.diffCompensation) {
|
|
1209
|
+
this.timePositionOffset = this.diffCompensation;
|
|
1210
|
+
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
1211
|
+
{ event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
|
|
1212
|
+
} else{
|
|
1213
|
+
this.timePositionOffset = 0;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1141
1216
|
}
|
|
1142
1217
|
} catch (err) {
|
|
1143
1218
|
console.error(`[${this._sessionId}]: Failed to init next VOD`);
|
|
@@ -1161,34 +1236,33 @@ class Session {
|
|
|
1161
1236
|
try {
|
|
1162
1237
|
debug(`[${this._sessionId}]: state=VOD_RELOAD_INIT`);
|
|
1163
1238
|
if (isLeader) {
|
|
1164
|
-
|
|
1165
1239
|
const startTS = Date.now();
|
|
1166
|
-
|
|
1167
1240
|
// 1) To tell Follower that, Leader is working on it!
|
|
1168
1241
|
sessionState.state = await this._sessionState.set("state", SessionState.VOD_RELOAD_INITIATING);
|
|
1169
|
-
|
|
1170
1242
|
// 2) Set new 'offset' sequences, to carry on the continuity from session-live
|
|
1171
1243
|
let mSeq = this.switchDataForSession.mediaSeq;
|
|
1172
1244
|
let currentVod = await this._sessionState.getCurrentVod();
|
|
1173
1245
|
if (currentVod.sequenceAlwaysContainNewSegments) {
|
|
1174
1246
|
// (!) will need to compensate if using this setting on HLSVod Object.
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
+
}
|
|
1177
1254
|
const dSeq = this.switchDataForSession.discSeq;
|
|
1178
1255
|
const mSeqOffset = this.switchDataForSession.mediaSeqOffset;
|
|
1179
1256
|
const reloadBehind = this.switchDataForSession.reloadBehind;
|
|
1180
1257
|
const segments = this.switchDataForSession.transitionSegments;
|
|
1181
|
-
|
|
1182
1258
|
if ([mSeq,dSeq,mSeqOffset,reloadBehind,segments].includes(null)) {
|
|
1183
1259
|
debug(`[${this._sessionId}]: LEADER: Cannot Reload VOD, missing switch-back data`);
|
|
1184
1260
|
return;
|
|
1185
1261
|
}
|
|
1186
|
-
|
|
1187
1262
|
await this._sessionState.set("mediaSeq", mSeq);
|
|
1188
1263
|
await this._playheadState.set("mediaSeq", mSeq);
|
|
1189
1264
|
await this._sessionState.set("discSeq", dSeq);
|
|
1190
1265
|
debug(`[${this._sessionId}]: Setting current media and discontinuity count -> [${mSeq}]:[${dSeq}]`);
|
|
1191
|
-
|
|
1192
1266
|
// 3) Set new media segments/currentVod, to carry on the continuity from session-live
|
|
1193
1267
|
debug(`[${this._sessionId}]: LEADER: making changes to current VOD. I will also update currentVod in store.`);
|
|
1194
1268
|
const playheadState = await this._playheadState.getValues(["vodMediaSeqVideo"]);
|
|
@@ -1197,8 +1271,10 @@ class Session {
|
|
|
1197
1271
|
nextMseq = currentVod.getLiveMediaSequencesCount() - 1;
|
|
1198
1272
|
}
|
|
1199
1273
|
|
|
1200
|
-
//
|
|
1201
|
-
|
|
1274
|
+
// ---------------------------------------------------.
|
|
1275
|
+
// TODO: Support reloading with audioSegments as well |
|
|
1276
|
+
// ---------------------------------------------------'
|
|
1277
|
+
|
|
1202
1278
|
await currentVod.reload(nextMseq, segments, null, reloadBehind);
|
|
1203
1279
|
await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
|
|
1204
1280
|
await this._sessionState.set("vodReloaded", 1);
|
|
@@ -1207,10 +1283,12 @@ class Session {
|
|
|
1207
1283
|
await this._playheadState.set("vodMediaSeqVideo", 0);
|
|
1208
1284
|
await this._playheadState.set("vodMediaSeqAudio", 0);
|
|
1209
1285
|
await this._playheadState.set("playheadRef", Date.now());
|
|
1286
|
+
// 4) Log to debug and cloudwatch
|
|
1210
1287
|
debug(`[${this._sessionId}]: LEADER: Set new Reloaded VOD and vodMediaSeq counts in store.`);
|
|
1211
|
-
|
|
1288
|
+
debug(`[${this._sessionId}]: next VOD Reloaded (${currentVod.getDeltaTimes()})`);
|
|
1289
|
+
debug(`[${this._sessionId}]: ${currentVod.getPlayheadPositions()}`);
|
|
1290
|
+
debug(`[${this._sessionId}]: msequences=${currentVod.getLiveMediaSequencesCount()}`);
|
|
1212
1291
|
cloudWatchLog(!this.cloudWatchLogging, "engine-session", { event: "switchback", channel: this._sessionId, reqTimeMs: Date.now() - startTS });
|
|
1213
|
-
|
|
1214
1292
|
return;
|
|
1215
1293
|
} else {
|
|
1216
1294
|
debug(`[${this._sessionId}]: not a leader so will go directly to state VOD_RELOAD_INITIATING`);
|
|
@@ -1435,6 +1513,47 @@ class Session {
|
|
|
1435
1513
|
debug(`[${this._sessionId}]: Current playhead position (${sessionState.vodMediaSeqVideo}): ${playheadPositions[sessionState.vodMediaSeqVideo]}`);
|
|
1436
1514
|
return playheadPositions[sessionState.vodMediaSeqVideo];
|
|
1437
1515
|
}
|
|
1516
|
+
|
|
1517
|
+
_getLastDuration(manifest) {
|
|
1518
|
+
return new Promise((resolve, reject) => {
|
|
1519
|
+
try {
|
|
1520
|
+
const parser = m3u8.createStream();
|
|
1521
|
+
let manifestStream = new Readable();
|
|
1522
|
+
manifestStream.push(manifest);
|
|
1523
|
+
manifestStream.push(null);
|
|
1524
|
+
|
|
1525
|
+
manifestStream.pipe(parser);
|
|
1526
|
+
parser.on('m3u', m3u => {
|
|
1527
|
+
if (m3u.items.PlaylistItem.length > 0) {
|
|
1528
|
+
const endIdx = m3u.items.PlaylistItem.length - 1;
|
|
1529
|
+
const bottomDuration = m3u.items.PlaylistItem[endIdx].get("duration");
|
|
1530
|
+
resolve(bottomDuration);
|
|
1531
|
+
} else {
|
|
1532
|
+
console.error("Empty media playlist");
|
|
1533
|
+
console.error(manifest);
|
|
1534
|
+
reject('Empty media playlist!')
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
} catch (err) {
|
|
1538
|
+
reject(err);
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
_getPlayheadDiffCompesationValue(diffMs, thresholdMs) {
|
|
1544
|
+
let compensationSec = 0;
|
|
1545
|
+
if (diffMs > thresholdMs) {
|
|
1546
|
+
compensationSec = (diffMs / 1000) - (thresholdMs / 1000);
|
|
1547
|
+
debug(`[${this._sessionId}]: Playhead stepping msequences too early. Need to wait longer. adding ${compensationSec}s`);
|
|
1548
|
+
return compensationSec;
|
|
1549
|
+
} else if (diffMs < -thresholdMs) {
|
|
1550
|
+
compensationSec = (diffMs / 1000) + (thresholdMs / 1000);
|
|
1551
|
+
debug(`[${this._sessionId}]: Playhead stepping msequences too LATE. Need to fast-forward. adding ${compensationSec}s`);
|
|
1552
|
+
return compensationSec;
|
|
1553
|
+
} else {
|
|
1554
|
+
return compensationSec;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1438
1557
|
}
|
|
1439
1558
|
|
|
1440
1559
|
module.exports = Session;
|
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,21 +254,37 @@ 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 {
|
|
267
284
|
debug(`[${this.sessionId}]: 'vodSegments' not empty = Using 'transitSegs'`);
|
|
268
285
|
}
|
|
269
286
|
|
|
270
|
-
debug(`[${this.sessionId}]: Setting CurrentMediaSequenceSegments. First seg is: [${this.vodSegments[
|
|
287
|
+
debug(`[${this.sessionId}]: Setting CurrentMediaSequenceSegments. First seg is: [${this.vodSegments[Object.keys(this.vodSegments)[0]][0].uri}]`);
|
|
271
288
|
|
|
272
289
|
const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId);
|
|
273
290
|
if (isLeader) {
|
|
@@ -501,7 +518,7 @@ class SessionLive {
|
|
|
501
518
|
this.liveSegQueue[liveBw] = [];
|
|
502
519
|
}
|
|
503
520
|
// Do not push duplicates
|
|
504
|
-
const liveSegURIs = this.liveSegQueue[liveBw].filter(seg => seg.uri).map(seg => seg.uri);
|
|
521
|
+
const liveSegURIs = this.liveSegQueue[liveBw].filter((seg) => seg.uri).map((seg) => seg.uri);
|
|
505
522
|
if (liveSegFromLeader.uri && liveSegURIs.includes(liveSegFromLeader.uri)) {
|
|
506
523
|
debug(`[${this.sessionId}]: FOLLOWER: Found duplicate live segment. Skip push! (${liveBw})`);
|
|
507
524
|
} 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
|
}
|
|
@@ -638,20 +661,32 @@ class SessionLive {
|
|
|
638
661
|
if (manifestList.some((result) => result.status === "rejected")) {
|
|
639
662
|
FETCH_ATTEMPTS--;
|
|
640
663
|
debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again in 1000ms...`);
|
|
641
|
-
await timer(1000)
|
|
664
|
+
await timer(1000);
|
|
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,13 +707,13 @@ 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");
|
|
679
714
|
let tries = 20;
|
|
680
715
|
|
|
681
|
-
while (!isLeader &&
|
|
716
|
+
while ((!isLeader && !leadersFirstSeqCounts.liveSourceMseqCount && tries > 0) || leadersFirstSeqCounts.liveSourceMseqCount === 0) {
|
|
682
717
|
debug(`[${this.sessionId}]: NEW FOLLOWER: Waiting for LEADER to add 'firstCounts' in store! Will look again after 1000ms (tries left=${tries})`);
|
|
683
718
|
await timer(1000);
|
|
684
719
|
leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts");
|
|
@@ -719,8 +754,9 @@ class SessionLive {
|
|
|
719
754
|
}
|
|
720
755
|
if (leadersFirstSeqCounts.mediaSeqCount !== this.mediaSeqCount) {
|
|
721
756
|
this.mediaSeqCount = leadersFirstSeqCounts.mediaSeqCount;
|
|
722
|
-
debug(
|
|
723
|
-
this.mediaSeqCount}][${this.discSeqCount}], and reading 'transitSegs' from store`
|
|
757
|
+
debug(
|
|
758
|
+
`[${this.sessionId}]: FOLLOWER transistioned with wrong V2L segments, updating counts to [${this.mediaSeqCount}][${this.discSeqCount}], and reading 'transitSegs' from store`
|
|
759
|
+
);
|
|
724
760
|
const transitSegs = await this.sessionLiveState.get("transitSegs");
|
|
725
761
|
if (!this._isEmpty(transitSegs)) {
|
|
726
762
|
this.vodSegments = transitSegs;
|
|
@@ -732,7 +768,6 @@ class SessionLive {
|
|
|
732
768
|
if (currentMseqRaw === leadersFirstSeqCounts.liveSourceMseqCount) {
|
|
733
769
|
this.pushAmount = 1; // Follower from start
|
|
734
770
|
} else {
|
|
735
|
-
|
|
736
771
|
// TODO: To support and account for past discontinuity tags in the Live Source stream,
|
|
737
772
|
// we will need to get the real 'current' discontinuity-sequence count from Leader somehow.
|
|
738
773
|
|
|
@@ -1029,8 +1064,8 @@ class SessionLive {
|
|
|
1029
1064
|
const resolveObj = {
|
|
1030
1065
|
M3U: m3u,
|
|
1031
1066
|
mediaSeq: m3u.get("mediaSequence"),
|
|
1067
|
+
bandwidth: liveTargetBandwidth,
|
|
1032
1068
|
};
|
|
1033
|
-
this.liveSourceM3Us[liveTargetBandwidth] = resolveObj;
|
|
1034
1069
|
resolve(resolveObj);
|
|
1035
1070
|
} catch (exc) {
|
|
1036
1071
|
debug(`[${this.sessionId}]: Error when parsing latest manifest`);
|
|
@@ -1070,7 +1105,7 @@ class SessionLive {
|
|
|
1070
1105
|
if (startIdx < 0) {
|
|
1071
1106
|
this.restAmount = startIdx * -1;
|
|
1072
1107
|
startIdx = 0;
|
|
1073
|
-
}
|
|
1108
|
+
}
|
|
1074
1109
|
if (mediaManifestUri) {
|
|
1075
1110
|
// push segments
|
|
1076
1111
|
this._addLiveSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetBandwidth, isLeader);
|
|
@@ -1092,7 +1127,7 @@ class SessionLive {
|
|
|
1092
1127
|
* @param {string} liveTargetBandwidth
|
|
1093
1128
|
*/
|
|
1094
1129
|
_addLiveSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetBandwidth, isLeader) {
|
|
1095
|
-
const leaderOrFollower = isLeader ? "LEADER": "NEW FOLLOWER";
|
|
1130
|
+
const leaderOrFollower = isLeader ? "LEADER" : "NEW FOLLOWER";
|
|
1096
1131
|
|
|
1097
1132
|
for (let i = startIdx; i < playlistItems.length; i++) {
|
|
1098
1133
|
let seg = {};
|
|
@@ -1162,7 +1197,7 @@ class SessionLive {
|
|
|
1162
1197
|
seg["daterange"] = daterangeData;
|
|
1163
1198
|
}
|
|
1164
1199
|
// Push new Live Segments! But do not push duplicates
|
|
1165
|
-
const liveSegURIs = this.liveSegQueue[liveTargetBandwidth].filter(seg => seg.uri).map(seg => seg.uri);
|
|
1200
|
+
const liveSegURIs = this.liveSegQueue[liveTargetBandwidth].filter((seg) => seg.uri).map((seg) => seg.uri);
|
|
1166
1201
|
if (seg.uri && liveSegURIs.includes(seg.uri)) {
|
|
1167
1202
|
debug(`[${this.sessionId}]: ${leaderOrFollower}: Found duplicate live segment. Skip push! (${liveTargetBandwidth})`);
|
|
1168
1203
|
} else {
|
|
@@ -1372,12 +1407,12 @@ class SessionLive {
|
|
|
1372
1407
|
if (this._isEmpty(this.liveSegQueue)) {
|
|
1373
1408
|
return null;
|
|
1374
1409
|
}
|
|
1375
|
-
|
|
1376
|
-
const bw0 = Object.keys(this.liveSegQueue)[0];
|
|
1410
|
+
|
|
1411
|
+
const bw0 = Object.keys(this.liveSegQueue)[0];
|
|
1377
1412
|
if (this.liveSegQueue[bw0].length === 0) {
|
|
1378
1413
|
return null;
|
|
1379
1414
|
}
|
|
1380
|
-
|
|
1415
|
+
|
|
1381
1416
|
for (let i = 0; i < this.liveSegQueue[bw0].length; i++) {
|
|
1382
1417
|
const segment = this.liveSegQueue[bw0][i];
|
|
1383
1418
|
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;
|
|
@@ -626,7 +626,7 @@ 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
630
|
OUTPUT_SEGMENTS[bw] = toSegments[bw].concat(fromSegments[targetBw]);
|
|
631
631
|
} else {
|
|
632
632
|
OUTPUT_SEGMENTS[bw] = toSegments[bw].concat(fromSegments[targetBw]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eyevinn-channel-engine",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.4",
|
|
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
|
});
|