eyevinn-channel-engine 3.2.7 → 3.3.2
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 +7 -1
- package/engine/session.js +218 -34
- package/engine/session_live.js +17 -16
- package/engine/stream_switcher.js +2 -2
- package/package.json +2 -2
- package/server.js +1 -1
- package/spec/engine/init_switching_spec.js +19 -19
package/README.md
CHANGED
|
@@ -108,6 +108,7 @@ Available options when constructing the Channel Engine object are:
|
|
|
108
108
|
- `maxTickInterval`: The maximum interval for playhead tick interval. Default is 10000 ms.
|
|
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
|
+
- `alwaysNewSegments`: Force all new HLS media sequences to always contain at least 1 new segment. Default is false.
|
|
111
112
|
|
|
112
113
|
## High Availability
|
|
113
114
|
|
package/engine/server.js
CHANGED
|
@@ -33,9 +33,13 @@ class ChannelEngine {
|
|
|
33
33
|
this.adXchangeUri = options.adXchangeUri;
|
|
34
34
|
}
|
|
35
35
|
this.useDemuxedAudio = false;
|
|
36
|
-
if (options && options.useDemuxedAudio
|
|
36
|
+
if (options && options.useDemuxedAudio) {
|
|
37
37
|
this.useDemuxedAudio = true;
|
|
38
38
|
}
|
|
39
|
+
this.alwaysNewSegments = false;
|
|
40
|
+
if (options && options.alwaysNewSegments) {
|
|
41
|
+
this.alwaysNewSegments = true;
|
|
42
|
+
}
|
|
39
43
|
if (options && options.defaultSlateUri) {
|
|
40
44
|
this.defaultSlateUri = options.defaultSlateUri;
|
|
41
45
|
this.slateRepetitions = options.slateRepetitions || 10;
|
|
@@ -228,6 +232,7 @@ class ChannelEngine {
|
|
|
228
232
|
sessionId: channel.id,
|
|
229
233
|
averageSegmentDuration: channel.options && channel.options.averageSegmentDuration ? channel.options.averageSegmentDuration : this.streamerOpts.defaultAverageSegmentDuration,
|
|
230
234
|
useDemuxedAudio: options.useDemuxedAudio,
|
|
235
|
+
alwaysNewSegments: options.alwaysNewSegments,
|
|
231
236
|
playheadDiffThreshold: channel.options && channel.options.playheadDiffThreshold ? channel.options.playheadDiffThreshold : this.streamerOpts.defaultPlayheadDiffThreshold,
|
|
232
237
|
maxTickInterval: channel.options && channel.options.maxTickInterval ? channel.options.maxTickInterval : this.streamerOpts.defaultMaxTickInterval,
|
|
233
238
|
targetDurationPadding: channel.options && channel.options.targetDurationPadding ? channel.options.targetDurationPadding : this.streamerOpts.targetDurationPadding,
|
|
@@ -444,6 +449,7 @@ class ChannelEngine {
|
|
|
444
449
|
options.adXchangeUri = this.adXchangeUri;
|
|
445
450
|
options.averageSegmentDuration = this.streamerOpts.defaultAverageSegmentDuration;
|
|
446
451
|
options.useDemuxedAudio = this.useDemuxedAudio;
|
|
452
|
+
options.alwaysNewSegments = this.alwaysNewSegments;
|
|
447
453
|
options.playheadDiffThreshold = this.streamerOpts.defaultPlayheadDiffThreshold;
|
|
448
454
|
options.maxTickInterval = this.streamerOpts.defaultMaxTickInterval;
|
|
449
455
|
options.targetDurationPadding = this.streamerOpts.targetDurationPadding;
|
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 DIFF_CORRECTION_RATE = 0.5;
|
|
18
19
|
|
|
19
20
|
const timer = ms => new Promise(res => setTimeout(res, ms));
|
|
20
21
|
|
|
@@ -43,10 +44,15 @@ class Session {
|
|
|
43
44
|
this.playheadDiffThreshold = DEFAULT_PLAYHEAD_DIFF_THRESHOLD;
|
|
44
45
|
this.maxTickInterval = DEFAULT_MAX_TICK_INTERVAL;
|
|
45
46
|
this.diffCompensation = null;
|
|
47
|
+
this.timePositionOffset = 0;
|
|
46
48
|
this.prevVodMediaSeq = {
|
|
47
49
|
video: null,
|
|
48
50
|
audio: null
|
|
49
51
|
}
|
|
52
|
+
this.prevMediaSeqOffset = {
|
|
53
|
+
video: null,
|
|
54
|
+
audio: null
|
|
55
|
+
}
|
|
50
56
|
this.waitingForNextVod = false;
|
|
51
57
|
this.leaderIsSettingNextVod = false;
|
|
52
58
|
this.isSwitchingBackToV2L = false;
|
|
@@ -58,8 +64,12 @@ class Session {
|
|
|
58
64
|
reloadBehind: null,
|
|
59
65
|
}
|
|
60
66
|
this.isAllowedToClearVodCache = null;
|
|
61
|
-
|
|
67
|
+
this.alwaysNewSegments = null;
|
|
62
68
|
if (config) {
|
|
69
|
+
if (config.alwaysNewSegments) {
|
|
70
|
+
this.alwaysNewSegments = config.alwaysNewSegments;
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
if (config.sessionId) {
|
|
64
74
|
this._sessionId = config.sessionId;
|
|
65
75
|
}
|
|
@@ -172,30 +182,45 @@ class Session {
|
|
|
172
182
|
const reqTickInterval = playheadState.tickInterval;
|
|
173
183
|
const timeSpentInIncrement = (tsIncrementEnd - tsIncrementBegin) / 1000;
|
|
174
184
|
let tickInterval = reqTickInterval - timeSpentInIncrement;
|
|
185
|
+
// Apply HLSVod delta time for current msequence.
|
|
175
186
|
const delta = await this._getCurrentDeltaTime();
|
|
176
187
|
if (delta != 0) {
|
|
177
|
-
debug(`[${this._sessionId}]: Delta time is != 0 need will adjust ${delta}sec to tick interval`);
|
|
178
188
|
tickInterval += delta;
|
|
189
|
+
debug(`[${this._sessionId}]: Delta time is != 0 need will adjust ${delta}sec to tick interval. tick=${tickInterval}`);
|
|
179
190
|
}
|
|
180
191
|
const position = (await this._getCurrentPlayheadPosition()) * 1000;
|
|
181
|
-
|
|
192
|
+
let timePosition = Date.now() - playheadState.playheadRef;
|
|
193
|
+
// Apply time position offset if set, only after external diff compensation has concluded.
|
|
194
|
+
if (this.timePositionOffset && this.diffCompensation <= 0 && this.alwaysNewSegments) {
|
|
195
|
+
timePosition -= this.timePositionOffset;
|
|
196
|
+
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
197
|
+
{ event: 'applyTimePositionOffset', channel: this._sessionId, offsetMs: this.timePositionOffset });
|
|
198
|
+
}
|
|
182
199
|
const diff = position - timePosition;
|
|
183
200
|
debug(`[${this._sessionId}]: ${timePosition}:${position}:${diff > 0 ? '+' : ''}${diff}ms`);
|
|
184
201
|
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
185
202
|
{ event: 'playheadDiff', channel: this._sessionId, diffMs: diff });
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
203
|
+
if (this.alwaysNewSegments) {
|
|
204
|
+
// Apply Playhead diff compensation, only after external diff compensation has concluded.
|
|
205
|
+
if (this.diffCompensation <= 0) {
|
|
206
|
+
const timeToAdd = this._getPlayheadDiffCompesationValue(diff, this.playheadDiffThreshold);
|
|
207
|
+
tickInterval += timeToAdd;
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
// Apply Playhead diff compensation, always.
|
|
211
|
+
const timeToAdd = this._getPlayheadDiffCompesationValue(diff, this.playheadDiffThreshold);
|
|
212
|
+
tickInterval += timeToAdd;
|
|
213
|
+
}
|
|
214
|
+
// Apply external diff compensation if available.
|
|
191
215
|
if (this.diffCompensation && this.diffCompensation > 0) {
|
|
192
|
-
const DIFF_COMPENSATION =
|
|
216
|
+
const DIFF_COMPENSATION = (reqTickInterval * DIFF_CORRECTION_RATE).toFixed(2) * 1000;
|
|
193
217
|
debug(`[${this._sessionId}]: Adding ${DIFF_COMPENSATION}msec to tickInterval to compensate for schedule diff (current=${this.diffCompensation}msec)`);
|
|
194
218
|
tickInterval += (DIFF_COMPENSATION / 1000);
|
|
195
219
|
this.diffCompensation -= DIFF_COMPENSATION;
|
|
196
220
|
}
|
|
221
|
+
// Keep tickInterval within upper and lower limits.
|
|
197
222
|
debug(`[${this._sessionId}]: Requested tickInterval=${tickInterval}s (max=${this.maxTickInterval / 1000}s, diffThreshold=${this.playheadDiffThreshold}msec)`);
|
|
198
|
-
if (tickInterval <= 0) {
|
|
223
|
+
if (tickInterval <= 0.5) {
|
|
199
224
|
tickInterval = 0.5;
|
|
200
225
|
} else if (tickInterval > (this.maxTickInterval / 1000)) {
|
|
201
226
|
tickInterval = this.maxTickInterval / 1000;
|
|
@@ -206,6 +231,13 @@ class Session {
|
|
|
206
231
|
await this._playheadState.set("tickMs", (tsTickEnd - tsIncrementBegin));
|
|
207
232
|
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
208
233
|
{ event: 'tickInterval', channel: this._sessionId, tickTimeMs: (tsTickEnd - tsIncrementBegin) });
|
|
234
|
+
if (this.alwaysNewSegments) {
|
|
235
|
+
// Use dynamic base-tickInterval. Set according to duration of latest segment.
|
|
236
|
+
const lastDuration = await this._getLastDuration(manifest);
|
|
237
|
+
const nextTickInterval = lastDuration < 2 ? 2 : lastDuration;
|
|
238
|
+
await this._playheadState.set("tickInterval", nextTickInterval);
|
|
239
|
+
cloudWatchLog(!this.cloudWatchLogging, "engine-session", { event: "tickIntervalUpdated", channel: this._sessionId, tickIntervalSec: nextTickInterval });
|
|
240
|
+
}
|
|
209
241
|
}
|
|
210
242
|
} catch (err) {
|
|
211
243
|
debug(`[${this._sessionId}]: Playhead consumer crashed (1)`);
|
|
@@ -376,7 +408,16 @@ class Session {
|
|
|
376
408
|
if (currentVod) {
|
|
377
409
|
try {
|
|
378
410
|
const mediaSegments = currentVod.getLiveMediaSequenceSegments(playheadState.vodMediaSeqVideo);
|
|
379
|
-
|
|
411
|
+
|
|
412
|
+
let mediaSequenceValue = 0;
|
|
413
|
+
if (currentVod.sequenceAlwaysContainNewSegments) {
|
|
414
|
+
mediaSequenceValue = currentVod.mediaSequenceValues[playheadState.vodMediaSeqVideo];
|
|
415
|
+
debug(`[${this._sessionId}]: {${mediaSequenceValue}}_{${currentVod.getLastSequenceMediaSequenceValue()}}`);
|
|
416
|
+
} else {
|
|
417
|
+
mediaSequenceValue = playheadState.vodMediaSeqVideo;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
debug(`[${this._sessionId}]: Requesting all segments from Media Sequence: ${playheadState.vodMediaSeqVideo}(${mediaSequenceValue})_${currentVod.getLiveMediaSequencesCount()}`);
|
|
380
421
|
return mediaSegments;
|
|
381
422
|
} catch (err) {
|
|
382
423
|
logerror(this._sessionId, err);
|
|
@@ -428,10 +469,18 @@ class Session {
|
|
|
428
469
|
const currentVod = await this._sessionState.getCurrentVod();
|
|
429
470
|
if (currentVod) {
|
|
430
471
|
try {
|
|
472
|
+
let mediaSequenceValue = 0;
|
|
473
|
+
if (currentVod.sequenceAlwaysContainNewSegments) {
|
|
474
|
+
mediaSequenceValue = currentVod.mediaSequenceValues[playheadState.vodMediaSeqVideo];
|
|
475
|
+
debug(`[${this._sessionId}]: seqIndex=${playheadState.vodMediaSeqVideo}_seqValue=${mediaSequenceValue}`)
|
|
476
|
+
} else {
|
|
477
|
+
mediaSequenceValue = playheadState.vodMediaSeqVideo;
|
|
478
|
+
}
|
|
431
479
|
const discSeqCount = discSeqOffset + currentVod.discontinuities[playheadState.vodMediaSeqVideo];
|
|
432
|
-
|
|
480
|
+
|
|
481
|
+
debug(`[${this._sessionId}]: MediaSeq: (${playheadState.mediaSeq}+${mediaSequenceValue}=${(playheadState.mediaSeq + mediaSequenceValue)}) and DiscSeq: (${discSeqCount}) requested `);
|
|
433
482
|
return {
|
|
434
|
-
'mediaSeq': (playheadState.mediaSeq +
|
|
483
|
+
'mediaSeq': (playheadState.mediaSeq + mediaSequenceValue),
|
|
435
484
|
'discSeq': discSeqCount,
|
|
436
485
|
'vodMediaSeqVideo': playheadState.vodMediaSeqVideo,
|
|
437
486
|
};
|
|
@@ -464,6 +513,9 @@ class Session {
|
|
|
464
513
|
if (this.prevVodMediaSeq.video === null) {
|
|
465
514
|
this.prevVodMediaSeq.video = playheadState.vodMediaSeqVideo;
|
|
466
515
|
}
|
|
516
|
+
if (this.prevMediaSeqOffset.video === null) {
|
|
517
|
+
this.prevMediaSeqOffset.video = playheadState.mediaSeq;
|
|
518
|
+
}
|
|
467
519
|
|
|
468
520
|
if (playheadState.vodMediaSeqVideo > sessionState.vodMediaSeqVideo) {
|
|
469
521
|
debug(`[${this._sessionId}]: Recently Loaded Next Vod. PlayheadState not up-to-date. Return the last generated m3u8`);
|
|
@@ -477,13 +529,30 @@ class Session {
|
|
|
477
529
|
}
|
|
478
530
|
|
|
479
531
|
// Force reading up from store, but only once if the condition is right
|
|
480
|
-
if (playheadState.vodMediaSeqVideo < 2 || playheadState.
|
|
532
|
+
if (playheadState.vodMediaSeqVideo < 2 || playheadState.mediaSeq !== this.prevMediaSeqOffset.video) {
|
|
481
533
|
debug(`[${this._sessionId}]: current[${playheadState.vodMediaSeqVideo}]_prev[${this.prevVodMediaSeq.video}]`);
|
|
534
|
+
debug(`[${this._sessionId}]: current-offset[${playheadState.mediaSeq}]_prev-offset[${this.prevMediaSeqOffset.video}]`);
|
|
535
|
+
// If true, then we have not updated the prev-values and not cleared the cache yet.
|
|
536
|
+
if (playheadState.vodMediaSeqVideo < this.prevVodMediaSeq.video || playheadState.mediaSeq !== this.prevMediaSeqOffset.video) {
|
|
537
|
+
this.isAllowedToClearVodCache = true;
|
|
538
|
+
}
|
|
482
539
|
const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
483
540
|
if (!isLeader && this.isAllowedToClearVodCache) {
|
|
484
541
|
debug(`[${this._sessionId}]: Not a leader and first|second media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
|
|
485
542
|
await this._sessionState.clearCurrentVodCache();
|
|
486
543
|
this.isAllowedToClearVodCache = false;
|
|
544
|
+
const diffMs = await this._playheadState.get("diffCompensation");
|
|
545
|
+
if (diffMs) {
|
|
546
|
+
this.diffCompensation = diffMs;
|
|
547
|
+
debug(`[${this._sessionId}]: Setting diffCompensation->${this.diffCompensation}`);
|
|
548
|
+
if (this.diffCompensation) {
|
|
549
|
+
this.timePositionOffset = this.diffCompensation;
|
|
550
|
+
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
551
|
+
{ event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
|
|
552
|
+
} else{
|
|
553
|
+
this.timePositionOffset = 0;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
487
556
|
}
|
|
488
557
|
} else {
|
|
489
558
|
this.isAllowedToClearVodCache = true;
|
|
@@ -492,10 +561,17 @@ class Session {
|
|
|
492
561
|
const currentVod = await this._sessionState.getCurrentVod();
|
|
493
562
|
if (currentVod) {
|
|
494
563
|
try {
|
|
564
|
+
|
|
565
|
+
if (currentVod.sequenceAlwaysContainNewSegments) {
|
|
566
|
+
const mediaSequenceValue = currentVod.mediaSequenceValues[playheadState.vodMediaSeqVideo];
|
|
567
|
+
debug(`[${this._sessionId}]: {${mediaSequenceValue}}_{${currentVod.getLastSequenceMediaSequenceValue()}}`);
|
|
568
|
+
}
|
|
569
|
+
|
|
495
570
|
debug(`[${this._sessionId}]: [${playheadState.vodMediaSeqVideo}]_[${currentVod.getLiveMediaSequencesCount()}]`);
|
|
496
571
|
const m3u8 = currentVod.getLiveMediaSequences(playheadState.mediaSeq, bw, playheadState.vodMediaSeqVideo, sessionState.discSeq, this.targetDurationPadding, this.forceTargetDuration);
|
|
497
572
|
debug(`[${this._sessionId}]: [${playheadState.mediaSeq + playheadState.vodMediaSeqVideo}][${sessionState.discSeq}][+${this.targetDurationPadding || 0}] Current media manifest for ${bw} requested`);
|
|
498
573
|
this.prevVodMediaSeq.video = playheadState.vodMediaSeqVideo;
|
|
574
|
+
this.prevMediaSeqOffset.video = playheadState.mediaSeq;
|
|
499
575
|
return m3u8;
|
|
500
576
|
} catch (err) {
|
|
501
577
|
logerror(this._sessionId, err);
|
|
@@ -591,19 +667,37 @@ class Session {
|
|
|
591
667
|
playheadState.mediaSeq = await this._playheadState.set("mediaSeq", sessionState.mediaSeq);
|
|
592
668
|
playheadState.vodMediaSeqVideo = await this._playheadState.set("vodMediaSeqVideo", sessionState.vodMediaSeqVideo);
|
|
593
669
|
playheadState.vodMediaSeqAudio = await this._playheadState.set("vodMediaSeqAudio", sessionState.vodMediaSeqAudio);
|
|
670
|
+
|
|
671
|
+
if (currentVod.sequenceAlwaysContainNewSegments) {
|
|
672
|
+
const mediaSequenceValue = currentVod.mediaSequenceValues[playheadState.vodMediaSeqVideo];
|
|
673
|
+
debug(`[${this._sessionId}]: Session can now serve mseq={${playheadState.mediaSeq + mediaSequenceValue}}`);
|
|
674
|
+
}
|
|
675
|
+
|
|
594
676
|
debug(`[${this._sessionId}]: INCREMENT (mseq=${playheadState.mediaSeq + playheadState.vodMediaSeqVideo}) vodMediaSeq=(${playheadState.vodMediaSeqVideo}_${playheadState.vodMediaSeqAudio} of ${currentVod.getLiveMediaSequencesCount()})`);
|
|
595
|
-
let m3u8 = currentVod.getLiveMediaSequences(playheadState.mediaSeq, 180000, playheadState.vodMediaSeqVideo, sessionState.discSeq);
|
|
596
|
-
await this._playheadState.setLastM3u8(m3u8);
|
|
597
677
|
|
|
598
678
|
// As a FOLLOWER, we might need to read up from shared store...
|
|
599
|
-
if (playheadState.vodMediaSeqVideo < 2 || playheadState.
|
|
679
|
+
if (playheadState.vodMediaSeqVideo < 2 || playheadState.mediaSeq !== this.prevMediaSeqOffset.video) {
|
|
600
680
|
debug(`[${this._sessionId}]: current[${playheadState.vodMediaSeqVideo}]_prev[${this.prevVodMediaSeq.video}]`);
|
|
601
|
-
|
|
602
|
-
|
|
681
|
+
debug(`[${this._sessionId}]: current-offset[${playheadState.mediaSeq}]_prev-offset[${this.prevMediaSeqOffset.video}]`);
|
|
682
|
+
if (playheadState.vodMediaSeqVideo < this.prevVodMediaSeq.video || playheadState.mediaSeq !== this.prevMediaSeqOffset.video) {
|
|
683
|
+
this.isAllowedToClearVodCache = true;
|
|
603
684
|
}
|
|
604
685
|
if (!isLeader && this.isAllowedToClearVodCache) {
|
|
605
686
|
debug(`[${this._sessionId}]: Not a leader and have just set 'playheadState.vodMediaSeqVideo' to 0|1. Invalidate cache to ensure having the correct VOD.`);
|
|
606
687
|
await this._sessionState.clearCurrentVodCache();
|
|
688
|
+
currentVod = await this._sessionState.getCurrentVod();
|
|
689
|
+
const diffMs = await this._playheadState.get("diffCompensation");
|
|
690
|
+
if (diffMs) {
|
|
691
|
+
this.diffCompensation = diffMs;
|
|
692
|
+
debug(`[${this._sessionId}]: Setting diffCompensation->${this.diffCompensation}`);
|
|
693
|
+
if (this.diffCompensation) {
|
|
694
|
+
this.timePositionOffset = this.diffCompensation;
|
|
695
|
+
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
696
|
+
{ event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
|
|
697
|
+
} else{
|
|
698
|
+
this.timePositionOffset = 0;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
607
701
|
this.isAllowedToClearVodCache = false;
|
|
608
702
|
}
|
|
609
703
|
} else {
|
|
@@ -611,7 +705,10 @@ class Session {
|
|
|
611
705
|
}
|
|
612
706
|
// Update Value for Previous Sequence
|
|
613
707
|
this.prevVodMediaSeq.video = playheadState.vodMediaSeqVideo;
|
|
708
|
+
this.prevMediaSeqOffset.video = playheadState.mediaSeq;
|
|
614
709
|
|
|
710
|
+
let m3u8 = currentVod.getLiveMediaSequences(playheadState.mediaSeq, 180000, playheadState.vodMediaSeqVideo, sessionState.discSeq);
|
|
711
|
+
await this._playheadState.setLastM3u8(m3u8);
|
|
615
712
|
return m3u8;
|
|
616
713
|
}
|
|
617
714
|
|
|
@@ -828,17 +925,21 @@ class Session {
|
|
|
828
925
|
const slateVod = await this._loadSlate(currentVod);
|
|
829
926
|
debug(`[${this._sessionId}]: slate loaded`);
|
|
830
927
|
const sessionState = await this._sessionState.getValues(["slateCount", "mediaSeq", "discSeq"]);
|
|
831
|
-
let
|
|
928
|
+
let endValue = 0;
|
|
832
929
|
let lastDiscontinuity = 0;
|
|
833
930
|
if (currentVod) {
|
|
834
|
-
|
|
931
|
+
if (currentVod.sequenceAlwaysContainNewSegments) {
|
|
932
|
+
endValue = currentVod.getLastSequenceMediaSequenceValue();
|
|
933
|
+
} else {
|
|
934
|
+
endValue = currentVod.getLiveMediaSequencesCount();
|
|
935
|
+
}
|
|
835
936
|
lastDiscontinuity = currentVod.getLastDiscontinuity();
|
|
836
937
|
}
|
|
837
938
|
await this._sessionState.set("vodMediaSeqVideo", 0);
|
|
838
939
|
await this._sessionState.set("vodMediaSeqAudio", 0);
|
|
839
940
|
await this._sessionState.set("state", SessionState.VOD_NEXT_INITIATING);
|
|
840
941
|
await this._sessionState.setCurrentVod(slateVod);
|
|
841
|
-
await this._sessionState.set("mediaSeq", sessionState.mediaSeq +
|
|
942
|
+
await this._sessionState.set("mediaSeq", sessionState.mediaSeq + endValue);
|
|
842
943
|
await this._sessionState.set("discSeq", sessionState.discSeq + lastDiscontinuity);
|
|
843
944
|
await this._sessionState.set("slateCount", sessionState.slateCount + 1);
|
|
844
945
|
await this._playheadState.set("playheadRef", Date.now());
|
|
@@ -891,7 +992,8 @@ class Session {
|
|
|
891
992
|
let loadPromise;
|
|
892
993
|
if (!vodResponse.type) {
|
|
893
994
|
debug(`[${this._sessionId}]: got first VOD uri=${vodResponse.uri}:${vodResponse.offset || 0}`);
|
|
894
|
-
|
|
995
|
+
const hlsOpts = { sequenceAlwaysContainNewSegments: this.alwaysNewSegments };
|
|
996
|
+
newVod = new HLSVod(vodResponse.uri, [], null, vodResponse.offset * 1000, m3u8Header(this._instanceId), hlsOpts);
|
|
895
997
|
if (vodResponse.timedMetadata) {
|
|
896
998
|
Object.keys(vodResponse.timedMetadata).map(k => {
|
|
897
999
|
newVod.addMetadata(k, vodResponse.timedMetadata[k]);
|
|
@@ -961,6 +1063,9 @@ class Session {
|
|
|
961
1063
|
if (this.prevVodMediaSeq.video === null) {
|
|
962
1064
|
this.prevVodMediaSeq.video = sessionState.vodMediaSeqVideo;
|
|
963
1065
|
}
|
|
1066
|
+
if (this.prevMediaSeqOffset.video === null) {
|
|
1067
|
+
this.prevMediaSeqOffset.video = sessionState.mediaSeq;
|
|
1068
|
+
}
|
|
964
1069
|
// Clear Cache if prev count is HIGHER than current...
|
|
965
1070
|
if (sessionState.vodMediaSeqVideo < this.prevVodMediaSeq.video) {
|
|
966
1071
|
debug(`[${this._sessionId}]: state=VOD_PLAYING, current[${sessionState.vodMediaSeqVideo}], prev[${this.prevVodMediaSeq.video}], total[${currentVod.getLiveMediaSequencesCount()}]`);
|
|
@@ -970,8 +1075,9 @@ class Session {
|
|
|
970
1075
|
}
|
|
971
1076
|
} else {
|
|
972
1077
|
// Handle edge case where Leader loaded next vod but Follower remained in state=VOD_PLAYING
|
|
973
|
-
if (this.
|
|
1078
|
+
if (this.prevMediaSeqOffset.video !== null & sessionState.mediaSeq !== this.prevMediaSeqOffset.video) {
|
|
974
1079
|
debug(`[${this._sessionId}]: state=VOD_PLAYING, current[${sessionState.vodMediaSeqVideo}], prev[${this.prevVodMediaSeq.video}], total[${currentVod.getLiveMediaSequencesCount()}]`);
|
|
1080
|
+
debug(`[${this._sessionId}]: mediaSeq offsets -> current[${sessionState.vodMediaSeqVideo}], prev[${this.prevVodMediaSeq.video}]`);
|
|
975
1081
|
// Allow Follower to clear VodCache...
|
|
976
1082
|
this.isAllowedToClearVodCache = true;
|
|
977
1083
|
}
|
|
@@ -996,6 +1102,12 @@ class Session {
|
|
|
996
1102
|
return;
|
|
997
1103
|
}
|
|
998
1104
|
const length = currentVod.getLiveMediaSequencesCount();
|
|
1105
|
+
let endMseqValue;
|
|
1106
|
+
if (currentVod.sequenceAlwaysContainNewSegments) {
|
|
1107
|
+
endMseqValue = currentVod.getLastSequenceMediaSequenceValue();
|
|
1108
|
+
} else {
|
|
1109
|
+
endMseqValue = currentVod.getLiveMediaSequencesCount();
|
|
1110
|
+
}
|
|
999
1111
|
const lastDiscontinuity = currentVod.getLastDiscontinuity();
|
|
1000
1112
|
sessionState.state = await this._sessionState.set("state", SessionState.VOD_NEXT_INITIATING);
|
|
1001
1113
|
let vodPromise = this._getNextVod();
|
|
@@ -1013,7 +1125,8 @@ class Session {
|
|
|
1013
1125
|
let loadPromise;
|
|
1014
1126
|
if (!vodResponse.type) {
|
|
1015
1127
|
debug(`[${this._sessionId}]: got next VOD uri=${vodResponse.uri}:${vodResponse.offset}`);
|
|
1016
|
-
|
|
1128
|
+
const hlsOpts = { sequenceAlwaysContainNewSegments: this.alwaysNewSegments };
|
|
1129
|
+
newVod = new HLSVod(vodResponse.uri, null, null, vodResponse.offset * 1000, m3u8Header(this._instanceId), hlsOpts);
|
|
1017
1130
|
if (vodResponse.timedMetadata) {
|
|
1018
1131
|
Object.keys(vodResponse.timedMetadata).map(k => {
|
|
1019
1132
|
newVod.addMetadata(k, vodResponse.timedMetadata[k]);
|
|
@@ -1030,6 +1143,13 @@ class Session {
|
|
|
1030
1143
|
loadPromise = newVod.loadAfter(currentVod);
|
|
1031
1144
|
if (vodResponse.diffMs) {
|
|
1032
1145
|
this.diffCompensation = vodResponse.diffMs;
|
|
1146
|
+
if (this.diffCompensation) {
|
|
1147
|
+
this.timePositionOffset = this.diffCompensation;
|
|
1148
|
+
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
1149
|
+
{ event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
|
|
1150
|
+
} else{
|
|
1151
|
+
this.timePositionOffset = 0;
|
|
1152
|
+
}
|
|
1033
1153
|
}
|
|
1034
1154
|
} else {
|
|
1035
1155
|
loadPromise = new Promise((resolve, reject) => {
|
|
@@ -1051,13 +1171,15 @@ class Session {
|
|
|
1051
1171
|
debug(`[${this._sessionId}]: msequences=${currentVod.getLiveMediaSequencesCount()}`);
|
|
1052
1172
|
sessionState.vodMediaSeqVideo = await this._sessionState.set("vodMediaSeqVideo", 0);
|
|
1053
1173
|
sessionState.vodMediaSeqAudio = await this._sessionState.set("vodMediaSeqAudio", 0);
|
|
1054
|
-
sessionState.mediaSeq = await this._sessionState.set("mediaSeq", sessionState.mediaSeq +
|
|
1174
|
+
sessionState.mediaSeq = await this._sessionState.set("mediaSeq", sessionState.mediaSeq + endMseqValue);
|
|
1055
1175
|
sessionState.discSeq = await this._sessionState.set("discSeq", sessionState.discSeq + lastDiscontinuity);
|
|
1056
1176
|
debug(`[${this._sessionId}]: new sequence data set in store [${sessionState.mediaSeq}][${sessionState.discSeq}]`);
|
|
1057
1177
|
await this._sessionState.remove("nextVod");
|
|
1058
1178
|
sessionState.currentVod = await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
|
|
1059
1179
|
this.leaderIsSettingNextVod = false;
|
|
1060
1180
|
await this._playheadState.set("playheadRef", Date.now());
|
|
1181
|
+
await this._playheadState.set("diffCompensation", this.diffCompensation);
|
|
1182
|
+
debug(`[${this._sessionId}]: sharing durrent vods diffCompensation=${this.diffCompensation}`);
|
|
1061
1183
|
this.produceEvent({
|
|
1062
1184
|
type: 'NOW_PLAYING',
|
|
1063
1185
|
data: {
|
|
@@ -1073,6 +1195,18 @@ class Session {
|
|
|
1073
1195
|
// Allow Leader|Follower to clear vodCache...
|
|
1074
1196
|
this.isAllowedToClearVodCache = true;
|
|
1075
1197
|
sessionState.currentVod = await this._sessionState.getCurrentVod();
|
|
1198
|
+
const diffMs = await this._playheadState.get("diffCompensation");
|
|
1199
|
+
if (diffMs) {
|
|
1200
|
+
this.diffCompensation = diffMs;
|
|
1201
|
+
debug(`[${this._sessionId}]: Setting diffCompensation=${this.diffCompensation}`);
|
|
1202
|
+
if (this.diffCompensation) {
|
|
1203
|
+
this.timePositionOffset = this.diffCompensation;
|
|
1204
|
+
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
1205
|
+
{ event: 'timePositionOffsetUpdated', channel: this._sessionId, offsetMs: this.timePositionOffset });
|
|
1206
|
+
} else{
|
|
1207
|
+
this.timePositionOffset = 0;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1076
1210
|
}
|
|
1077
1211
|
} catch (err) {
|
|
1078
1212
|
console.error(`[${this._sessionId}]: Failed to init next VOD`);
|
|
@@ -1103,7 +1237,12 @@ class Session {
|
|
|
1103
1237
|
sessionState.state = await this._sessionState.set("state", SessionState.VOD_RELOAD_INITIATING);
|
|
1104
1238
|
|
|
1105
1239
|
// 2) Set new 'offset' sequences, to carry on the continuity from session-live
|
|
1106
|
-
|
|
1240
|
+
let mSeq = this.switchDataForSession.mediaSeq;
|
|
1241
|
+
let currentVod = await this._sessionState.getCurrentVod();
|
|
1242
|
+
if (currentVod.sequenceAlwaysContainNewSegments) {
|
|
1243
|
+
// (!) will need to compensate if using this setting on HLSVod Object.
|
|
1244
|
+
mSeq -= 1;
|
|
1245
|
+
}
|
|
1107
1246
|
const dSeq = this.switchDataForSession.discSeq;
|
|
1108
1247
|
const mSeqOffset = this.switchDataForSession.mediaSeqOffset;
|
|
1109
1248
|
const reloadBehind = this.switchDataForSession.reloadBehind;
|
|
@@ -1122,15 +1261,14 @@ class Session {
|
|
|
1122
1261
|
// 3) Set new media segments/currentVod, to carry on the continuity from session-live
|
|
1123
1262
|
debug(`[${this._sessionId}]: LEADER: making changes to current VOD. I will also update currentVod in store.`);
|
|
1124
1263
|
const playheadState = await this._playheadState.getValues(["vodMediaSeqVideo"]);
|
|
1125
|
-
let
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
currentMseq = currentVod.getLiveMediaSequencesCount() - 1;
|
|
1264
|
+
let nextMseq = playheadState.vodMediaSeqVideo + 1;
|
|
1265
|
+
if (nextMseq > currentVod.getLiveMediaSequencesCount() - 1) {
|
|
1266
|
+
nextMseq = currentVod.getLiveMediaSequencesCount() - 1;
|
|
1129
1267
|
}
|
|
1130
1268
|
|
|
1131
1269
|
// TODO: Support reloading with audioSegments as well
|
|
1132
1270
|
|
|
1133
|
-
await currentVod.reload(
|
|
1271
|
+
await currentVod.reload(nextMseq, segments, null, reloadBehind);
|
|
1134
1272
|
await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
|
|
1135
1273
|
await this._sessionState.set("vodReloaded", 1);
|
|
1136
1274
|
await this._sessionState.set("vodMediaSeqVideo", 0);
|
|
@@ -1140,6 +1278,9 @@ class Session {
|
|
|
1140
1278
|
await this._playheadState.set("playheadRef", Date.now());
|
|
1141
1279
|
debug(`[${this._sessionId}]: LEADER: Set new Reloaded VOD and vodMediaSeq counts in store.`);
|
|
1142
1280
|
// 4) emit cloudwatch event object
|
|
1281
|
+
debug(`[${this._sessionId}]: next VOD Reloaded (${currentVod.getDeltaTimes()})`);
|
|
1282
|
+
debug(`[${this._sessionId}]: ${currentVod.getPlayheadPositions()}`);
|
|
1283
|
+
debug(`[${this._sessionId}]: msequences=${currentVod.getLiveMediaSequencesCount()}`);
|
|
1143
1284
|
cloudWatchLog(!this.cloudWatchLogging, "engine-session", { event: "switchback", channel: this._sessionId, reqTimeMs: Date.now() - startTS });
|
|
1144
1285
|
|
|
1145
1286
|
return;
|
|
@@ -1210,7 +1351,8 @@ class Session {
|
|
|
1210
1351
|
|
|
1211
1352
|
slateVod.load()
|
|
1212
1353
|
.then(() => {
|
|
1213
|
-
|
|
1354
|
+
const hlsOpts = { sequenceAlwaysContainNewSegments: this.alwaysNewSegments };
|
|
1355
|
+
hlsVod = new HLSVod(this.slateUri, null, null, null, m3u8Header(this._instanceId), hlsOpts);
|
|
1214
1356
|
const timestamp = Date.now();
|
|
1215
1357
|
hlsVod.addMetadata('id', `slate-${timestamp}`);
|
|
1216
1358
|
hlsVod.addMetadata('start-date', new Date(timestamp).toISOString());
|
|
@@ -1254,7 +1396,8 @@ class Session {
|
|
|
1254
1396
|
|
|
1255
1397
|
slateVod.load()
|
|
1256
1398
|
.then(() => {
|
|
1257
|
-
|
|
1399
|
+
const hlsOpts = { sequenceAlwaysContainNewSegments: this.alwaysNewSegments };
|
|
1400
|
+
hlsVod = new HLSVod(nexVodUri, null, null, null, m3u8Header(this._instanceId), hlsOpts);
|
|
1258
1401
|
const timestamp = Date.now();
|
|
1259
1402
|
hlsVod.addMetadata('id', `slate-${timestamp}`);
|
|
1260
1403
|
hlsVod.addMetadata('start-date', new Date(timestamp).toISOString());
|
|
@@ -1364,6 +1507,47 @@ class Session {
|
|
|
1364
1507
|
debug(`[${this._sessionId}]: Current playhead position (${sessionState.vodMediaSeqVideo}): ${playheadPositions[sessionState.vodMediaSeqVideo]}`);
|
|
1365
1508
|
return playheadPositions[sessionState.vodMediaSeqVideo];
|
|
1366
1509
|
}
|
|
1510
|
+
|
|
1511
|
+
_getLastDuration(manifest) {
|
|
1512
|
+
return new Promise((resolve, reject) => {
|
|
1513
|
+
try {
|
|
1514
|
+
const parser = m3u8.createStream();
|
|
1515
|
+
let manifestStream = new Readable();
|
|
1516
|
+
manifestStream.push(manifest);
|
|
1517
|
+
manifestStream.push(null);
|
|
1518
|
+
|
|
1519
|
+
manifestStream.pipe(parser);
|
|
1520
|
+
parser.on('m3u', m3u => {
|
|
1521
|
+
if (m3u.items.PlaylistItem.length > 0) {
|
|
1522
|
+
const endIdx = m3u.items.PlaylistItem.length - 1;
|
|
1523
|
+
const bottomDuration = m3u.items.PlaylistItem[endIdx].get("duration");
|
|
1524
|
+
resolve(bottomDuration);
|
|
1525
|
+
} else {
|
|
1526
|
+
console.error("Empty media playlist");
|
|
1527
|
+
console.error(manifest);
|
|
1528
|
+
reject('Empty media playlist!')
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
reject(err);
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
_getPlayheadDiffCompesationValue(diffMs, thresholdMs) {
|
|
1538
|
+
let compensationSec = 0;
|
|
1539
|
+
if (diffMs > thresholdMs) {
|
|
1540
|
+
compensationSec = (diffMs / 1000) - (thresholdMs / 1000);
|
|
1541
|
+
debug(`[${this._sessionId}]: Playhead stepping msequences too early. Need to wait longer. adding ${compensationSec}s`);
|
|
1542
|
+
return compensationSec;
|
|
1543
|
+
} else if (diffMs < -thresholdMs) {
|
|
1544
|
+
compensationSec = (diffMs / 1000) + (thresholdMs / 1000);
|
|
1545
|
+
debug(`[${this._sessionId}]: Playhead stepping msequences too LATE. Need to fast-forward. adding ${compensationSec}s`);
|
|
1546
|
+
return compensationSec;
|
|
1547
|
+
} else {
|
|
1548
|
+
return compensationSec;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1367
1551
|
}
|
|
1368
1552
|
|
|
1369
1553
|
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 {
|
|
@@ -542,7 +542,7 @@ class SessionLive {
|
|
|
542
542
|
this.blockGenerateManifest = false;
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
-
let attempts =
|
|
545
|
+
let attempts = 10;
|
|
546
546
|
// CHECK AGAIN CASE 1: Store Empty
|
|
547
547
|
while (!leadersMediaSeqRaw && attempts > 0) {
|
|
548
548
|
const segDur = this._getAnyFirstSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS;
|
|
@@ -565,7 +565,7 @@ class SessionLive {
|
|
|
565
565
|
}
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
-
attempts =
|
|
568
|
+
attempts = 10;
|
|
569
569
|
// CHECK AGAIN CASE 2: Store Old
|
|
570
570
|
while (leadersMediaSeqRaw <= this.lastRequestedMediaSeqRaw && attempts > 0) {
|
|
571
571
|
const segDur = this._getAnyFirstSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS;
|
|
@@ -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.2
|
|
3
|
+
"version": "3.3.2",
|
|
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": "^
|
|
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",
|
package/server.js
CHANGED
|
@@ -414,11 +414,11 @@ describe("The initialize switching", () => {
|
|
|
414
414
|
});
|
|
415
415
|
expect(sessionCurrentSegs["1313000"][size - 1 - 2]).toEqual({
|
|
416
416
|
discontinuity: true,
|
|
417
|
-
cue: { in: true }
|
|
417
|
+
cue: { in: true }
|
|
418
418
|
});
|
|
419
419
|
expect(sessionCurrentSegs["1313000"][size - 1]).toEqual({
|
|
420
|
-
duration:
|
|
421
|
-
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",
|
|
422
422
|
timelinePosition: null,
|
|
423
423
|
cue: null,
|
|
424
424
|
});
|
|
@@ -469,7 +469,7 @@ describe("The initialize switching", () => {
|
|
|
469
469
|
const size = sessionCurrentSegs["1313000"].length;
|
|
470
470
|
expect(sessionCurrentSegs["1313000"][0]).toEqual({
|
|
471
471
|
duration: 7.5,
|
|
472
|
-
uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/600/600-
|
|
472
|
+
uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/600/600-00012.ts",
|
|
473
473
|
timelinePosition: null,
|
|
474
474
|
cue: null,
|
|
475
475
|
});
|
|
@@ -745,41 +745,41 @@ describe("The initialize switching", () => {
|
|
|
745
745
|
const borderSegment = newVodSegments["1313000"][6];
|
|
746
746
|
|
|
747
747
|
const expectedLastVODSegItem = {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
timelinePosition: null,
|
|
751
|
-
cue: null,
|
|
748
|
+
discontinuity: true,
|
|
749
|
+
daterange: null
|
|
752
750
|
}
|
|
753
751
|
const expectedFirstV2LSegItem = {
|
|
754
|
-
duration:
|
|
755
|
-
uri: "https://maitv-vod.lab.eyevinn.technology/VINN.mp4/600/600-00000.ts",
|
|
752
|
+
duration: 7.2,
|
|
756
753
|
timelinePosition: null,
|
|
757
754
|
cue: null,
|
|
755
|
+
uri: 'https://maitv-vod.lab.eyevinn.technology/VINN.mp4/600/600-00001.ts'
|
|
758
756
|
}
|
|
759
757
|
const expectedSecondVODSegItem = {
|
|
760
758
|
duration: 7.5075,
|
|
761
|
-
uri: "https://maitv-vod.lab.eyevinn.technology/BECKY_Trailer_2020.mp4/
|
|
759
|
+
uri: "https://maitv-vod.lab.eyevinn.technology/BECKY_Trailer_2020.mp4/1000/1000-00001.ts",
|
|
762
760
|
timelinePosition: null,
|
|
763
761
|
cue: null,
|
|
764
762
|
}
|
|
765
763
|
|
|
766
764
|
expect(newVodSegments["1313000"][0]).toEqual(expectedLastVODSegItem);
|
|
767
765
|
expect(newVodSegments["1313000"][1]).toEqual({
|
|
768
|
-
|
|
769
|
-
|
|
766
|
+
duration: 10.846444,
|
|
767
|
+
timelinePosition: null,
|
|
768
|
+
cue: null,
|
|
769
|
+
uri: 'https://maitv-vod.lab.eyevinn.technology/VINN.mp4/600/600-00000.ts'
|
|
770
770
|
});
|
|
771
771
|
expect(newVodSegments["1313000"][2]).toEqual(expectedFirstV2LSegItem);
|
|
772
|
-
expect(newVodSegments["1313000"][5]).toEqual({
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
});
|
|
772
|
+
// expect(newVodSegments["1313000"][5]).toEqual({
|
|
773
|
+
// discontinuity: true,
|
|
774
|
+
// cue: { in: true },
|
|
775
|
+
// });
|
|
776
776
|
expect(borderSegment.daterange["planned-duration"]).toEqual(vodEventDurationMs2 / 1000);
|
|
777
777
|
expect("start-date" in borderSegment.daterange).toBe(true);
|
|
778
778
|
expect("id" in borderSegment.daterange).toBe(true);
|
|
779
|
-
expect(borderSegment.duration).toEqual(11.
|
|
779
|
+
expect(borderSegment.duration).toEqual(11.3447);
|
|
780
780
|
expect(borderSegment.timelinePosition).toEqual(null);
|
|
781
781
|
expect(borderSegment.cue).toEqual(null);
|
|
782
|
-
expect(borderSegment.uri).toEqual("https://maitv-vod.lab.eyevinn.technology/BECKY_Trailer_2020.mp4/
|
|
782
|
+
expect(borderSegment.uri).toEqual("https://maitv-vod.lab.eyevinn.technology/BECKY_Trailer_2020.mp4/1000/1000-00000.ts");
|
|
783
783
|
expect(newVodSegments["1313000"][7]).toEqual(expectedSecondVODSegItem);
|
|
784
784
|
});
|
|
785
785
|
|