eyevinn-channel-engine 3.1.7 → 3.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/engine/session.js +184 -62
- package/engine/session_live.js +47 -14
- package/engine/session_state.js +9 -2
- package/engine/stream_switcher.js +7 -6
- package/package.json +1 -1
- package/spec/engine/init_switching_spec.js +118 -153
- package/spec/engine/stream_switcher_spec.js +91 -68
package/engine/session.js
CHANGED
|
@@ -43,9 +43,20 @@ class Session {
|
|
|
43
43
|
this.playheadDiffThreshold = DEFAULT_PLAYHEAD_DIFF_THRESHOLD;
|
|
44
44
|
this.maxTickInterval = DEFAULT_MAX_TICK_INTERVAL;
|
|
45
45
|
this.diffCompensation = null;
|
|
46
|
+
this.prevVodMediaSeq = {
|
|
47
|
+
video: null,
|
|
48
|
+
audio: null
|
|
49
|
+
}
|
|
46
50
|
this.waitingForNextVod = false;
|
|
47
51
|
this.leaderIsSettingNextVod = false;
|
|
48
|
-
this.
|
|
52
|
+
this.isSwitchingBackToV2L = false;
|
|
53
|
+
this.switchDataForSession = {
|
|
54
|
+
mediaSeq: null,
|
|
55
|
+
discSeq: null,
|
|
56
|
+
mediaSeqOffset: null,
|
|
57
|
+
transitionSegments: null,
|
|
58
|
+
reloadBehind: null,
|
|
59
|
+
}
|
|
49
60
|
|
|
50
61
|
if (config) {
|
|
51
62
|
if (config.sessionId) {
|
|
@@ -139,7 +150,14 @@ class Session {
|
|
|
139
150
|
state = await this._playheadState.getState();
|
|
140
151
|
|
|
141
152
|
const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
142
|
-
if (isLeader &&
|
|
153
|
+
if (isLeader &&
|
|
154
|
+
[
|
|
155
|
+
SessionState.VOD_NEXT_INIT,
|
|
156
|
+
SessionState.VOD_NEXT_INITIATING,
|
|
157
|
+
SessionState.VOD_RELOAD_INIT,
|
|
158
|
+
SessionState.VOD_RELOAD_INITIATING
|
|
159
|
+
].indexOf(sessionState.state) !== -1) {
|
|
160
|
+
|
|
143
161
|
const firstDuration = await this._getFirstDuration(manifest);
|
|
144
162
|
const tickInterval = firstDuration < 2 ? 2 : firstDuration;
|
|
145
163
|
debug(`[${this._sessionId}]: I am the leader and updated tick interval to ${tickInterval} sec`);
|
|
@@ -152,13 +170,13 @@ class Session {
|
|
|
152
170
|
} else {
|
|
153
171
|
const reqTickInterval = playheadState.tickInterval;
|
|
154
172
|
const timeSpentInIncrement = (tsIncrementEnd - tsIncrementBegin) / 1000;
|
|
155
|
-
let tickInterval =
|
|
173
|
+
let tickInterval = reqTickInterval - timeSpentInIncrement;
|
|
156
174
|
const delta = await this._getCurrentDeltaTime();
|
|
157
175
|
if (delta != 0) {
|
|
158
176
|
debug(`[${this._sessionId}]: Delta time is != 0 need will adjust ${delta}sec to tick interval`);
|
|
159
177
|
tickInterval += delta;
|
|
160
178
|
}
|
|
161
|
-
const position = await this._getCurrentPlayheadPosition() * 1000;
|
|
179
|
+
const position = (await this._getCurrentPlayheadPosition()) * 1000;
|
|
162
180
|
const timePosition = Date.now() - playheadState.playheadRef;
|
|
163
181
|
const diff = position - timePosition;
|
|
164
182
|
debug(`[${this._sessionId}]: ${timePosition}:${position}:${diff > 0 ? '+' : ''}${diff}ms`);
|
|
@@ -214,7 +232,7 @@ class Session {
|
|
|
214
232
|
if (this.disabledPlayhead) {
|
|
215
233
|
return {
|
|
216
234
|
sessionId: this._sessionId,
|
|
217
|
-
}
|
|
235
|
+
};
|
|
218
236
|
} else {
|
|
219
237
|
const playheadState = await this._playheadState.getValues(["tickMs", "mediaSeq", "vodMediaSeqVideo"]);
|
|
220
238
|
const state = await this._playheadState.getState();
|
|
@@ -243,11 +261,16 @@ class Session {
|
|
|
243
261
|
await this._playheadStateStore.reset(this._sessionId);
|
|
244
262
|
}
|
|
245
263
|
|
|
264
|
+
async getSessionState() {
|
|
265
|
+
const state = await this._sessionState.get("state");
|
|
266
|
+
return state;
|
|
267
|
+
}
|
|
268
|
+
|
|
246
269
|
async getTruncatedVodSegments(vodUri, duration) {
|
|
247
270
|
try {
|
|
248
271
|
const hlsVod = await this._truncateSlate(null, duration, vodUri);
|
|
249
272
|
let vodSegments = hlsVod.getMediaSegments();
|
|
250
|
-
Object.keys(vodSegments).forEach(bw => vodSegments[bw].unshift({ discontinuity: true, cue: { in: true } }));
|
|
273
|
+
Object.keys(vodSegments).forEach((bw) => vodSegments[bw].unshift({ discontinuity: true, cue: { in: true } }));
|
|
251
274
|
return vodSegments;
|
|
252
275
|
} catch (exc) {
|
|
253
276
|
debug(`[${this._sessionId}]: Failed to generate truncated VOD!`);
|
|
@@ -257,20 +280,25 @@ class Session {
|
|
|
257
280
|
|
|
258
281
|
async setCurrentMediaSequenceSegments(segments, mSeqOffset, reloadBehind) {
|
|
259
282
|
if (!this._sessionState) {
|
|
260
|
-
throw new Error(
|
|
283
|
+
throw new Error("Session not ready");
|
|
261
284
|
}
|
|
285
|
+
this.isSwitchingBackToV2L = true;
|
|
262
286
|
|
|
263
|
-
this.
|
|
287
|
+
this.switchDataForSession.reloadBehind = reloadBehind;
|
|
288
|
+
this.switchDataForSession.transitionSegments = segments;
|
|
289
|
+
this.switchDataForSession.mediaSeqOffset = mSeqOffset;
|
|
264
290
|
|
|
265
291
|
let isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
266
292
|
if (!isLeader) {
|
|
267
293
|
debug(`[${this._sessionId}]: FOLLOWER: Invalidate cache to ensure having the correct VOD!`);
|
|
268
294
|
await this._sessionState.clearCurrentVodCache();
|
|
295
|
+
|
|
269
296
|
let vodReloaded = await this._sessionState.get("vodReloaded");
|
|
270
297
|
let attempts = 9;
|
|
271
298
|
while (!isLeader && !vodReloaded && attempts > 0) {
|
|
272
299
|
debug(`[${this._sessionId}]: FOLLOWER: I arrived before LEADER. Waiting (1000ms) for LEADER to reload currentVod in store! (tries left=${attempts})`);
|
|
273
300
|
await timer(1000);
|
|
301
|
+
await this._sessionStateStore.clearLeaderCache();
|
|
274
302
|
isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
275
303
|
vodReloaded = await this._sessionState.get("vodReloaded");
|
|
276
304
|
attempts--;
|
|
@@ -282,43 +310,26 @@ class Session {
|
|
|
282
310
|
if (!isLeader || vodReloaded) {
|
|
283
311
|
debug(`[${this._sessionId}]: FOLLOWER: leader is alive, and has presumably updated currentVod. Clearing the cache now`);
|
|
284
312
|
await this._sessionState.clearCurrentVodCache();
|
|
285
|
-
this.isSwitchingBackFromLive = false;
|
|
286
313
|
return;
|
|
287
314
|
}
|
|
288
|
-
|
|
315
|
+
debug(`[${this._sessionId}]: NEW LEADER: Setting state=VOD_RELOAD_INIT`);
|
|
316
|
+
this.isSwitchingBackToV2L = true;
|
|
317
|
+
await this._sessionState.set("state", SessionState.VOD_RELOAD_INIT)
|
|
289
318
|
|
|
290
|
-
|
|
291
|
-
let
|
|
292
|
-
let
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
debug(`[${this._sessionId}]: LEADER: Wait for session to initialize next VOD. (tries left=${ATTEMPTS})`);
|
|
319
|
+
} else {
|
|
320
|
+
let vodReloaded = await this._sessionState.get("vodReloaded");
|
|
321
|
+
let attempts = 9;
|
|
322
|
+
while (!vodReloaded && attempts > 0) {
|
|
323
|
+
debug(`[${this._sessionId}]: LEADER: Waiting (1000ms) to buy some time reloading vod and adding it to store! (tries left=${attempts})`);
|
|
296
324
|
await timer(1000);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
if (ATTEMPTS === 0) {
|
|
301
|
-
throw new Error("Failed to reload currentVod! Need a new LEADER!");
|
|
325
|
+
vodReloaded = await this._sessionState.get("vodReloaded");
|
|
326
|
+
attempts--;
|
|
302
327
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
let currentVod = await this._sessionState.getCurrentVod();
|
|
307
|
-
let currentMseq = playheadState.vodMediaSeqVideo + mSeqOffset;
|
|
308
|
-
if (currentMseq > currentVod.getLiveMediaSequencesCount() - 1) {
|
|
309
|
-
currentMseq = currentVod.getLiveMediaSequencesCount() - 1;
|
|
328
|
+
if (attempts === 0) {
|
|
329
|
+
debug(`[${this._sessionId}]: LEADER: WARNING! Vod was never Reloaded!`);
|
|
330
|
+
return;
|
|
310
331
|
}
|
|
311
|
-
// TODO: Support reloading with audioSegments as well
|
|
312
|
-
await currentVod.reload(currentMseq, segments, null, reloadBehind);
|
|
313
|
-
await this._sessionState.setCurrentVod(currentVod, { ttl: (currentVod.getDuration() * 1000) });
|
|
314
|
-
await this._sessionState.set("vodReloaded", 1);
|
|
315
|
-
await this._sessionState.set("vodMediaSeqVideo", 0);
|
|
316
|
-
await this._sessionState.set("vodMediaSeqAudio", 0);
|
|
317
|
-
await this._playheadState.set("vodMediaSeqVideo", 0);
|
|
318
|
-
await this._playheadState.set("vodMediaSeqAudio", 0);
|
|
319
|
-
await this._playheadState.set("playheadRef", Date.now());
|
|
320
332
|
}
|
|
321
|
-
this.isSwitchingBackFromLive = false;
|
|
322
333
|
}
|
|
323
334
|
|
|
324
335
|
async getCurrentMediaSequenceSegments() {
|
|
@@ -329,6 +340,18 @@ class Session {
|
|
|
329
340
|
if (isLeader) {
|
|
330
341
|
await this._sessionState.set("vodReloaded", 0);
|
|
331
342
|
}
|
|
343
|
+
|
|
344
|
+
// Only read data from store if state is VOD_PLAYING
|
|
345
|
+
let state = await this.getSessionState();
|
|
346
|
+
let tries = 12;
|
|
347
|
+
while (state !== SessionState.VOD_PLAYING && tries > 0) {
|
|
348
|
+
const waitTimeMs = 500;
|
|
349
|
+
debug(`[${this._sessionId}]: state=${state} - Waiting ${waitTimeMs}ms_${tries} until Leader has finished loading next vod.`);
|
|
350
|
+
await timer(waitTimeMs);
|
|
351
|
+
tries--;
|
|
352
|
+
state = await this.getSessionState();
|
|
353
|
+
}
|
|
354
|
+
|
|
332
355
|
const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo"]);
|
|
333
356
|
if (playheadState.vodMediaSeqVideo === 0) {
|
|
334
357
|
if (!isLeader) {
|
|
@@ -340,7 +363,7 @@ class Session {
|
|
|
340
363
|
if (currentVod) {
|
|
341
364
|
try {
|
|
342
365
|
const mediaSegments = currentVod.getLiveMediaSequenceSegments(playheadState.vodMediaSeqVideo);
|
|
343
|
-
debug(`[${this._sessionId}]: Requesting all segments from Media Sequence: ${playheadState.vodMediaSeqVideo}`);
|
|
366
|
+
debug(`[${this._sessionId}]: Requesting all segments from Media Sequence: ${playheadState.vodMediaSeqVideo}_${currentVod.getLiveMediaSequencesCount()}`);
|
|
344
367
|
return mediaSegments;
|
|
345
368
|
} catch (err) {
|
|
346
369
|
logerror(this._sessionId, err);
|
|
@@ -354,25 +377,31 @@ class Session {
|
|
|
354
377
|
|
|
355
378
|
async setCurrentMediaAndDiscSequenceCount(_mediaSeq, _discSeq) {
|
|
356
379
|
if (!this._sessionState) {
|
|
357
|
-
throw new Error(
|
|
380
|
+
throw new Error("Session not ready");
|
|
358
381
|
}
|
|
359
|
-
|
|
360
|
-
this.isSwitchingBackFromLive = true;
|
|
361
382
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
await this._sessionState.set("discSeq", _discSeq);
|
|
367
|
-
debug(`[${this._sessionId}]: Setting current media and discontinuity count -> [${_mediaSeq}]:[${_discSeq}]`);
|
|
368
|
-
}
|
|
369
|
-
this.isSwitchingBackFromLive = false;
|
|
383
|
+
this.isSwitchingBackToV2L = true;
|
|
384
|
+
|
|
385
|
+
this.switchDataForSession.mediaSeq = _mediaSeq;
|
|
386
|
+
this.switchDataForSession.discSeq = _discSeq;
|
|
370
387
|
}
|
|
371
388
|
|
|
372
389
|
async getCurrentMediaAndDiscSequenceCount() {
|
|
373
390
|
if (!this._sessionState) {
|
|
374
391
|
throw new Error('Session not ready');
|
|
375
392
|
}
|
|
393
|
+
|
|
394
|
+
// Only read data from store if state is VOD_PLAYING
|
|
395
|
+
let state = await this.getSessionState();
|
|
396
|
+
let tries = 12;
|
|
397
|
+
while (state !== SessionState.VOD_PLAYING && tries > 0) {
|
|
398
|
+
const waitTimeMs = 500;
|
|
399
|
+
debug(`[${this._sessionId}]: state=${state} - Waiting ${waitTimeMs}ms_${tries} until Leader has finished loading next vod.`);
|
|
400
|
+
await timer(waitTimeMs);
|
|
401
|
+
tries--;
|
|
402
|
+
state = await this.getSessionState();
|
|
403
|
+
}
|
|
404
|
+
|
|
376
405
|
const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo"]);
|
|
377
406
|
const discSeqOffset = await this._sessionState.get("discSeq");
|
|
378
407
|
if (playheadState.vodMediaSeqVideo === 0) {
|
|
@@ -407,8 +436,8 @@ class Session {
|
|
|
407
436
|
}
|
|
408
437
|
// Be sure that the leader is not in the middle of setting new vod data in store.
|
|
409
438
|
// Followers will never run this part...
|
|
410
|
-
let tries =
|
|
411
|
-
while(tries > 0 && this.leaderIsSettingNextVod) {
|
|
439
|
+
let tries = 12;
|
|
440
|
+
while (tries > 0 && this.leaderIsSettingNextVod) {
|
|
412
441
|
debug(`[${this._sessionId}]: Leader is setting the next vod. Waiting 500ms_${tries}`);
|
|
413
442
|
await timer(500);
|
|
414
443
|
tries--;
|
|
@@ -417,17 +446,23 @@ class Session {
|
|
|
417
446
|
const sessionState = await this._sessionState.getValues(["vodMediaSeqVideo", "discSeq"]);
|
|
418
447
|
const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo"]);
|
|
419
448
|
|
|
449
|
+
if (this.prevVodMediaSeq.video !== null) {
|
|
450
|
+
this.prevVodMediaSeq.video = playheadState.vodMediaSeqVideo;
|
|
451
|
+
}
|
|
452
|
+
|
|
420
453
|
if (playheadState.vodMediaSeqVideo > sessionState.vodMediaSeqVideo) {
|
|
421
454
|
debug(`[${this._sessionId}]: Recently Loaded Next Vod. PlayheadState not up-to-date. Return the last generated m3u8`);
|
|
422
455
|
const m3u8 = await this._playheadState.getLastM3u8();
|
|
423
456
|
if (m3u8) {
|
|
457
|
+
this.prevVodMediaSeq.video = playheadState.vodMediaSeqVideo;
|
|
424
458
|
return m3u8;
|
|
425
459
|
} else {
|
|
426
460
|
debug(`[${this._sessionId}]: We don't have any previously generated m3u8`);
|
|
427
461
|
}
|
|
428
462
|
}
|
|
429
463
|
|
|
430
|
-
if (playheadState.vodMediaSeqVideo < 2) {
|
|
464
|
+
if (playheadState.vodMediaSeqVideo < 2 || playheadState.vodMediaSeqVideo < this.prevVodMediaSeq.video) {
|
|
465
|
+
debug(`[${this._sessionId}]: current[${playheadState.vodMediaSeqVideo}]_prev[${this.prevVodMediaSeq.video}]`);
|
|
431
466
|
const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
432
467
|
if (!isLeader) {
|
|
433
468
|
debug(`[${this._sessionId}]: Not a leader and first|second media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
|
|
@@ -441,6 +476,7 @@ class Session {
|
|
|
441
476
|
debug(`[${this._sessionId}]: [${playheadState.vodMediaSeqVideo}]_[${currentVod.getLiveMediaSequencesCount()}]`);
|
|
442
477
|
const m3u8 = currentVod.getLiveMediaSequences(playheadState.mediaSeq, bw, playheadState.vodMediaSeqVideo, sessionState.discSeq, this.targetDurationPadding, this.forceTargetDuration);
|
|
443
478
|
debug(`[${this._sessionId}]: [${playheadState.mediaSeq + playheadState.vodMediaSeqVideo}][${sessionState.discSeq}][+${this.targetDurationPadding || 0}] Current media manifest for ${bw} requested`);
|
|
479
|
+
this.prevVodMediaSeq.video = playheadState.vodMediaSeqVideo;
|
|
444
480
|
return m3u8;
|
|
445
481
|
} catch (err) {
|
|
446
482
|
logerror(this._sessionId, err);
|
|
@@ -482,13 +518,6 @@ class Session {
|
|
|
482
518
|
async incrementAsync() {
|
|
483
519
|
await this._tickAsync();
|
|
484
520
|
const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
485
|
-
// Avoid reading old values if new ones are on their way...
|
|
486
|
-
let tries = 10;
|
|
487
|
-
while (tries > 0 && this.isSwitchingBackFromLive) {
|
|
488
|
-
debug(`[${this._sessionId}]: Leader is currently setting new data in store. Waiting 500ms_${tries}`);
|
|
489
|
-
await timer(500);
|
|
490
|
-
tries--;
|
|
491
|
-
}
|
|
492
521
|
|
|
493
522
|
let sessionState = await this._sessionState.getValues(
|
|
494
523
|
["state", "mediaSeq", "discSeq", "vodMediaSeqVideo", "vodMediaSeqAudio"]);
|
|
@@ -502,15 +531,17 @@ class Session {
|
|
|
502
531
|
sessionState.discSeq === null) {
|
|
503
532
|
debug(`[${this._sessionId}]: Session is not ready yet`);
|
|
504
533
|
debug(sessionState);
|
|
534
|
+
await this._sessionState.clearCurrentVodCache();
|
|
505
535
|
if (isLeader) {
|
|
506
536
|
debug(`[${this._sessionId}]: I am the leader, trying to initiate the session`);
|
|
507
537
|
sessionState.state = await this._sessionState.set("state", SessionState.VOD_INIT);
|
|
508
538
|
}
|
|
509
539
|
return null;
|
|
510
540
|
}
|
|
511
|
-
if (sessionState.state === SessionState.VOD_NEXT_INITIATING) {
|
|
541
|
+
if (sessionState.state === SessionState.VOD_NEXT_INITIATING || sessionState.state === SessionState.VOD_RELOAD_INITIATING) {
|
|
512
542
|
if (isLeader) {
|
|
513
|
-
|
|
543
|
+
const leaderAction = sessionState.state === SessionState.VOD_NEXT_INITIATING ? "initiated" : "reloaded";
|
|
544
|
+
debug(`[${this._sessionId}]: I am the leader and have just ${leaderAction} next VOD, let's move to VOD_PLAYING`);
|
|
514
545
|
sessionState.state = await this._sessionState.set("state", SessionState.VOD_PLAYING);
|
|
515
546
|
} else {
|
|
516
547
|
debug(`[${this._sessionId}]: Return the last generated m3u8 to give the leader some time`);
|
|
@@ -532,6 +563,11 @@ class Session {
|
|
|
532
563
|
sessionState.state = await this._sessionState.set("state", SessionState.VOD_NEXT_INIT);
|
|
533
564
|
}
|
|
534
565
|
|
|
566
|
+
if (this.isSwitchingBackToV2L) {
|
|
567
|
+
sessionState.state = await this._sessionState.set("state", SessionState.VOD_RELOAD_INIT);
|
|
568
|
+
this.isSwitchingBackToV2L = false;
|
|
569
|
+
}
|
|
570
|
+
|
|
535
571
|
playheadState.mediaSeq = await this._playheadState.set("mediaSeq", sessionState.mediaSeq);
|
|
536
572
|
playheadState.vodMediaSeqVideo = await this._playheadState.set("vodMediaSeqVideo", sessionState.vodMediaSeqVideo);
|
|
537
573
|
playheadState.vodMediaSeqAudio = await this._playheadState.set("vodMediaSeqAudio", sessionState.vodMediaSeqAudio);
|
|
@@ -809,6 +845,10 @@ class Session {
|
|
|
809
845
|
case SessionState.VOD_INIT:
|
|
810
846
|
case SessionState.VOD_INIT_BY_ID:
|
|
811
847
|
try {
|
|
848
|
+
// Needed if store was reset
|
|
849
|
+
await this._sessionStateStore.clearLeaderCache();
|
|
850
|
+
isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
851
|
+
|
|
812
852
|
let nextVodPromise;
|
|
813
853
|
if (sessionState.state === SessionState.VOD_INIT) {
|
|
814
854
|
debug(`[${this._sessionId}]: state=VOD_INIT`);
|
|
@@ -898,6 +938,17 @@ class Session {
|
|
|
898
938
|
currentVod = await this._sessionState.getCurrentVod();
|
|
899
939
|
this.waitingForNextVod = false;
|
|
900
940
|
}
|
|
941
|
+
} else {
|
|
942
|
+
// Handle edge case where store has been reset, but leader has not cleared cache.
|
|
943
|
+
if (this.prevVodMediaSeq.video !== null) {
|
|
944
|
+
this.prevVodMediaSeq.video = sessionState.vodMediaSeqVideo;
|
|
945
|
+
}
|
|
946
|
+
if (this.prevVodMediaSeq.video < sessionState.vodMediaSeqVideo) {
|
|
947
|
+
debug(`[${this._sessionId}]: current[${sessionState.vodMediaSeqVideo}], prev[${this.prevVodMediaSeq.video}], total[${currentVod.getLiveMediaSequencesCount()}]`);
|
|
948
|
+
await this._sessionState.clearCurrentVodCache();
|
|
949
|
+
currentVod = await this._sessionState.getCurrentVod();
|
|
950
|
+
this.prevVodMediaSeq.video = sessionState.vodMediaSeqVideo;
|
|
951
|
+
}
|
|
901
952
|
}
|
|
902
953
|
debug(`[${this._sessionId}]: state=VOD_PLAYING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}, ${currentVod.getLiveMediaSequencesCount()})`);
|
|
903
954
|
return;
|
|
@@ -1014,6 +1065,77 @@ class Session {
|
|
|
1014
1065
|
}
|
|
1015
1066
|
}
|
|
1016
1067
|
break;
|
|
1068
|
+
case SessionState.VOD_RELOAD_INIT:
|
|
1069
|
+
try {
|
|
1070
|
+
debug(`[${this._sessionId}]: state=VOD_RELOAD_INIT`);
|
|
1071
|
+
if (isLeader) {
|
|
1072
|
+
|
|
1073
|
+
const startTS = Date.now();
|
|
1074
|
+
|
|
1075
|
+
// 1) To tell Follower that, Leader is working on it!
|
|
1076
|
+
sessionState.state = await this._sessionState.set("state", SessionState.VOD_RELOAD_INITIATING);
|
|
1077
|
+
|
|
1078
|
+
// 2) Set new 'offset' sequences, to carry on the continuity from session-live
|
|
1079
|
+
const mSeq = this.switchDataForSession.mediaSeq;
|
|
1080
|
+
const dSeq = this.switchDataForSession.discSeq;
|
|
1081
|
+
const mSeqOffset = this.switchDataForSession.mediaSeqOffset;
|
|
1082
|
+
const reloadBehind = this.switchDataForSession.reloadBehind;
|
|
1083
|
+
const segments = this.switchDataForSession.transitionSegments;
|
|
1084
|
+
|
|
1085
|
+
if ([mSeq,dSeq,mSeqOffset,reloadBehind,segments].includes(null)) {
|
|
1086
|
+
debug(`[${this._sessionId}]: LEADER: Cannot Reload VOD, missing switch-back data`);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
await this._sessionState.set("mediaSeq", mSeq);
|
|
1091
|
+
await this._playheadState.set("mediaSeq", mSeq);
|
|
1092
|
+
await this._sessionState.set("discSeq", dSeq);
|
|
1093
|
+
debug(`[${this._sessionId}]: Setting current media and discontinuity count -> [${mSeq}]:[${dSeq}]`);
|
|
1094
|
+
|
|
1095
|
+
// 3) Set new media segments/currentVod, to carry on the continuity from session-live
|
|
1096
|
+
debug(`[${this._sessionId}]: LEADER: making changes to current VOD. I will also update currentVod in store.`);
|
|
1097
|
+
const playheadState = await this._playheadState.getValues(["vodMediaSeqVideo"]);
|
|
1098
|
+
let currentVod = await this._sessionState.getCurrentVod();
|
|
1099
|
+
let currentMseq = playheadState.vodMediaSeqVideo + mSeqOffset;
|
|
1100
|
+
if (currentMseq > currentVod.getLiveMediaSequencesCount() - 1) {
|
|
1101
|
+
currentMseq = currentVod.getLiveMediaSequencesCount() - 1;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// TODO: Support reloading with audioSegments as well
|
|
1105
|
+
|
|
1106
|
+
await currentVod.reload(currentMseq, segments, null, reloadBehind);
|
|
1107
|
+
await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
|
|
1108
|
+
await this._sessionState.set("vodReloaded", 1);
|
|
1109
|
+
await this._sessionState.set("vodMediaSeqVideo", 0);
|
|
1110
|
+
await this._sessionState.set("vodMediaSeqAudio", 0);
|
|
1111
|
+
await this._playheadState.set("vodMediaSeqVideo", 0);
|
|
1112
|
+
await this._playheadState.set("vodMediaSeqAudio", 0);
|
|
1113
|
+
await this._playheadState.set("playheadRef", Date.now());
|
|
1114
|
+
debug(`[${this._sessionId}]: LEADER: Set new Reloaded VOD and vodMediaSeq counts in store.`);
|
|
1115
|
+
// 4) emit cloudwatch event object
|
|
1116
|
+
cloudWatchLog(!this.cloudWatchLogging, "engine-session", { event: "switchback", channel: this._sessionId, reqTimeMs: Date.now() - startTS });
|
|
1117
|
+
|
|
1118
|
+
return;
|
|
1119
|
+
} else {
|
|
1120
|
+
debug(`[${this._sessionId}]: not a leader so will go directly to state VOD_RELOAD_INITIATING`);
|
|
1121
|
+
sessionState.state = await this._sessionState.set("state", SessionState.VOD_RELOAD_INITIATING);
|
|
1122
|
+
}
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
debug("Failed to init reload vod");
|
|
1125
|
+
throw err;
|
|
1126
|
+
}
|
|
1127
|
+
break;
|
|
1128
|
+
case SessionState.VOD_RELOAD_INITIATING:
|
|
1129
|
+
debug(`[${this._sessionId}]: state=VOD_RELOAD_INITIATING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}, ${currentVod.getLiveMediaSequencesCount()})`);
|
|
1130
|
+
if (!isLeader) {
|
|
1131
|
+
debug(`[${this._sessionId}]: not the leader so just waiting for the VOD to be reloaded`);
|
|
1132
|
+
if (sessionState.vodMediaSeqVideo === 0 || this.waitingForNextVod) {
|
|
1133
|
+
debug(`[${this._sessionId}]: First mediasequence in VOD and I am not the leader so invalidate current VOD cache and fetch the new one from the leader`);
|
|
1134
|
+
await this._sessionState.clearCurrentVodCache();
|
|
1135
|
+
}
|
|
1136
|
+
this.waitingForNextVod = true;
|
|
1137
|
+
}
|
|
1138
|
+
return;
|
|
1017
1139
|
default:
|
|
1018
1140
|
throw new Error("Invalid state: " + sessionState.state);
|
|
1019
1141
|
}
|
package/engine/session_live.js
CHANGED
|
@@ -48,6 +48,7 @@ class SessionLive {
|
|
|
48
48
|
this.firstTime = true;
|
|
49
49
|
this.allowedToSet = false;
|
|
50
50
|
this.pushAmount = 0;
|
|
51
|
+
this.restAmount = 0;
|
|
51
52
|
this.waitForPlayhead = true;
|
|
52
53
|
this.blockGenerateManifest = false;
|
|
53
54
|
|
|
@@ -502,7 +503,7 @@ class SessionLive {
|
|
|
502
503
|
// Do not push duplicates
|
|
503
504
|
const liveSegURIs = this.liveSegQueue[liveBw].filter(seg => seg.uri).map(seg => seg.uri);
|
|
504
505
|
if (liveSegFromLeader.uri && liveSegURIs.includes(liveSegFromLeader.uri)) {
|
|
505
|
-
debug(`[${this.sessionId}]: FOLLOWER: Found duplicate live segment. Skip push
|
|
506
|
+
debug(`[${this.sessionId}]: FOLLOWER: Found duplicate live segment. Skip push! (${liveBw})`);
|
|
506
507
|
} else {
|
|
507
508
|
this.liveSegQueue[liveBw].push(liveSegFromLeader);
|
|
508
509
|
debug(`[${this.sessionId}]: FOLLOWER: Pushed segment (${liveSegFromLeader.uri ? liveSegFromLeader.uri : "Disc-tag"}) to 'liveSegQueue' (${liveBw})`);
|
|
@@ -671,28 +672,40 @@ class SessionLive {
|
|
|
671
672
|
let leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts");
|
|
672
673
|
let tries = 20;
|
|
673
674
|
|
|
674
|
-
while ((!leadersFirstSeqCounts.liveSourceMseqCount && tries > 0) || leadersFirstSeqCounts.liveSourceMseqCount === 0) {
|
|
675
|
+
while (!isLeader && (!leadersFirstSeqCounts.liveSourceMseqCount && tries > 0) || leadersFirstSeqCounts.liveSourceMseqCount === 0) {
|
|
675
676
|
debug(`[${this.sessionId}]: NEW FOLLOWER: Waiting for LEADER to add 'firstCounts' in store! Will look again after 1000ms (tries left=${tries})`);
|
|
676
677
|
await timer(1000);
|
|
677
678
|
leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts");
|
|
678
679
|
tries--;
|
|
680
|
+
// Might take over as Leader if Leader is not setting data due to being down.
|
|
681
|
+
isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId);
|
|
682
|
+
if (isLeader) {
|
|
683
|
+
debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`);
|
|
684
|
+
}
|
|
679
685
|
}
|
|
680
686
|
|
|
681
687
|
if (tries === 0) {
|
|
682
688
|
isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId);
|
|
683
689
|
if (isLeader) {
|
|
684
|
-
debug(`[${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`);
|
|
690
|
+
debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`);
|
|
685
691
|
break;
|
|
686
692
|
} else {
|
|
687
|
-
debug(`[${this.instanceId}]: The leader is still alive`);
|
|
693
|
+
debug(`[${this.sessionId}][${this.instanceId}]: The leader is still alive`);
|
|
688
694
|
leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts");
|
|
689
695
|
if (!leadersFirstSeqCounts.liveSourceMseqCount) {
|
|
690
|
-
debug(`[${this.instanceId}]: Could not find 'firstCounts' in store. Abort Executing Promises II & Returning to Playhead.`);
|
|
696
|
+
debug(`[${this.sessionId}][${this.instanceId}]: Could not find 'firstCounts' in store. Abort Executing Promises II & Returning to Playhead.`);
|
|
691
697
|
return;
|
|
692
698
|
}
|
|
693
699
|
}
|
|
694
700
|
}
|
|
695
701
|
|
|
702
|
+
if (isLeader) {
|
|
703
|
+
debug(`[${this.sessionId}]: NEW LEADER: Original Leader went missing, I am retrying live source fetch...`);
|
|
704
|
+
await this.sessionLiveState.set("transitSegs", this.vodSegments);
|
|
705
|
+
debug(`[${this.sessionId}]: NEW LEADER: I am adding 'transitSegs' to Store for future followers`);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
|
|
696
709
|
// Respawners never do this, only starter followers.
|
|
697
710
|
// Edge Case: FOLLOWER transitioned from session with different segments from LEADER
|
|
698
711
|
if (leadersFirstSeqCounts.discSeqCount !== this.discSeqCount) {
|
|
@@ -713,6 +726,10 @@ class SessionLive {
|
|
|
713
726
|
if (currentMseqRaw === leadersFirstSeqCounts.liveSourceMseqCount) {
|
|
714
727
|
this.pushAmount = 1; // Follower from start
|
|
715
728
|
} else {
|
|
729
|
+
|
|
730
|
+
// TODO: To support and account for past discontinuity tags in the Live Source stream,
|
|
731
|
+
// we will need to get the real 'current' discontinuity-sequence count from Leader somehow.
|
|
732
|
+
|
|
716
733
|
// RESPAWNED NODES
|
|
717
734
|
this.pushAmount = currentMseqRaw - leadersFirstSeqCounts.liveSourceMseqCount + 1;
|
|
718
735
|
|
|
@@ -772,7 +789,7 @@ class SessionLive {
|
|
|
772
789
|
for (let i = 0; i < Object.keys(this.mediaManifestURIs).length; i++) {
|
|
773
790
|
let bw = Object.keys(this.mediaManifestURIs)[i];
|
|
774
791
|
// will add new segments to live seg queue
|
|
775
|
-
pushPromises.push(this._parseMediaManifest(this.liveSourceM3Us[bw].M3U, this.mediaManifestURIs[bw], bw));
|
|
792
|
+
pushPromises.push(this._parseMediaManifest(this.liveSourceM3Us[bw].M3U, this.mediaManifestURIs[bw], bw, isLeader));
|
|
776
793
|
debug(`[${this.sessionId}]: Pushed pushPromise for bw=${bw}`);
|
|
777
794
|
}
|
|
778
795
|
// Segment Pushing
|
|
@@ -961,6 +978,12 @@ class SessionLive {
|
|
|
961
978
|
// Update Session Live Media Sequence Count...
|
|
962
979
|
this.prevMediaSeqCount = this.mediaSeqCount;
|
|
963
980
|
this.mediaSeqCount += removedSegments;
|
|
981
|
+
if (this.restAmount) {
|
|
982
|
+
this.mediaSeqCount += this.restAmount;
|
|
983
|
+
debug(`[${this.sessionId}]: ${instanceName}: Added restAmount=[${this.restAmount}] to 'mediaSeqCount'`);
|
|
984
|
+
this.restAmount = 0;
|
|
985
|
+
}
|
|
986
|
+
|
|
964
987
|
if (this.discSeqCount !== this.prevDiscSeqCount) {
|
|
965
988
|
debug(`[${this.sessionId}]: ${instanceName}: Incrementing Dseq Count from {${this.prevDiscSeqCount}} -> {${this.discSeqCount}}`);
|
|
966
989
|
}
|
|
@@ -1015,7 +1038,7 @@ class SessionLive {
|
|
|
1015
1038
|
});
|
|
1016
1039
|
}
|
|
1017
1040
|
|
|
1018
|
-
_parseMediaManifest(m3u, mediaManifestUri, liveTargetBandwidth) {
|
|
1041
|
+
_parseMediaManifest(m3u, mediaManifestUri, liveTargetBandwidth, isLeader) {
|
|
1019
1042
|
return new Promise(async (resolve, reject) => {
|
|
1020
1043
|
try {
|
|
1021
1044
|
if (!this.liveSegQueue[liveTargetBandwidth]) {
|
|
@@ -1038,10 +1061,13 @@ class SessionLive {
|
|
|
1038
1061
|
}
|
|
1039
1062
|
this.targetDuration = m3u.get("targetDuration");
|
|
1040
1063
|
let startIdx = m3u.items.PlaylistItem.length - this.pushAmount;
|
|
1041
|
-
|
|
1064
|
+
if (startIdx < 0) {
|
|
1065
|
+
this.restAmount = startIdx * -1;
|
|
1066
|
+
startIdx = 0;
|
|
1067
|
+
}
|
|
1042
1068
|
if (mediaManifestUri) {
|
|
1043
1069
|
// push segments
|
|
1044
|
-
this._addLiveSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetBandwidth);
|
|
1070
|
+
this._addLiveSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetBandwidth, isLeader);
|
|
1045
1071
|
}
|
|
1046
1072
|
resolve();
|
|
1047
1073
|
} catch (exc) {
|
|
@@ -1059,7 +1085,9 @@ class SessionLive {
|
|
|
1059
1085
|
* @param {string} baseUrl
|
|
1060
1086
|
* @param {string} liveTargetBandwidth
|
|
1061
1087
|
*/
|
|
1062
|
-
_addLiveSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetBandwidth) {
|
|
1088
|
+
_addLiveSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetBandwidth, isLeader) {
|
|
1089
|
+
const leaderOrFollower = isLeader ? "LEADER": "NEW FOLLOWER";
|
|
1090
|
+
|
|
1063
1091
|
for (let i = startIdx; i < playlistItems.length; i++) {
|
|
1064
1092
|
let seg = {};
|
|
1065
1093
|
let playlistItem = playlistItems[i];
|
|
@@ -1127,10 +1155,15 @@ class SessionLive {
|
|
|
1127
1155
|
if (daterangeData) {
|
|
1128
1156
|
seg["daterange"] = daterangeData;
|
|
1129
1157
|
}
|
|
1130
|
-
// Push new Live Segments!
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1158
|
+
// Push new Live Segments! But do not push duplicates
|
|
1159
|
+
const liveSegURIs = this.liveSegQueue[liveTargetBandwidth].filter(seg => seg.uri).map(seg => seg.uri);
|
|
1160
|
+
if (seg.uri && liveSegURIs.includes(seg.uri)) {
|
|
1161
|
+
debug(`[${this.sessionId}]: ${leaderOrFollower}: Found duplicate live segment. Skip push! (${liveTargetBandwidth})`);
|
|
1162
|
+
} else {
|
|
1163
|
+
this.liveSegQueue[liveTargetBandwidth].push(seg);
|
|
1164
|
+
this.liveSegsForFollowers[liveTargetBandwidth].push(seg);
|
|
1165
|
+
debug(`[${this.sessionId}]: ${leaderOrFollower}: Pushed segment (${seg.uri ? seg.uri : "Disc-tag"}) to 'liveSegQueue' (${liveTargetBandwidth})`);
|
|
1166
|
+
}
|
|
1134
1167
|
}
|
|
1135
1168
|
}
|
|
1136
1169
|
}
|
package/engine/session_state.js
CHANGED
|
@@ -7,6 +7,8 @@ const SessionState = Object.freeze({
|
|
|
7
7
|
VOD_PLAYING: 2,
|
|
8
8
|
VOD_NEXT_INIT: 3,
|
|
9
9
|
VOD_NEXT_INITIATING: 4,
|
|
10
|
+
VOD_RELOAD_INIT: 5,
|
|
11
|
+
VOD_RELOAD_INITIATING: 6
|
|
10
12
|
});
|
|
11
13
|
|
|
12
14
|
const CURRENTVOD_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
@@ -157,6 +159,11 @@ class SessionStateStore extends SharedStateStore {
|
|
|
157
159
|
await this.setVolatile("", instanceId, t);
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
async clearLeaderCache() {
|
|
163
|
+
debug(`[${this.instanceId}]: clearing 'leader' cache`);
|
|
164
|
+
this.cache.leader.value = null;
|
|
165
|
+
}
|
|
166
|
+
|
|
160
167
|
async isLeader(instanceId) {
|
|
161
168
|
if (!instanceId) {
|
|
162
169
|
throw new Error("Cannot determine leader without instance id");
|
|
@@ -164,7 +171,7 @@ class SessionStateStore extends SharedStateStore {
|
|
|
164
171
|
let leader;
|
|
165
172
|
if (this.cache.leader.value && Date.now() < this.cache.leader.ts + this.cacheTTL) {
|
|
166
173
|
leader = this.cache.leader.value;
|
|
167
|
-
debug(`[${instanceId}]: reading 'leader' from cache: I am
|
|
174
|
+
debug(`[${instanceId}]: reading 'leader' from cache: I am${leader === instanceId ? "" : " NOT"} the leader!`);
|
|
168
175
|
return leader === instanceId;
|
|
169
176
|
}
|
|
170
177
|
leader = await this.get("", "leader");
|
|
@@ -189,7 +196,7 @@ class SessionStateStore extends SharedStateStore {
|
|
|
189
196
|
}
|
|
190
197
|
}
|
|
191
198
|
}
|
|
192
|
-
debug(`[${instanceId}]: I am
|
|
199
|
+
debug(`[${instanceId}]: I am${leader === instanceId ? "" : " NOT"} the leader!`);
|
|
193
200
|
this.cache.leader.ts = Date.now();
|
|
194
201
|
this.cache.leader.value = leader;
|
|
195
202
|
return leader === instanceId;
|
|
@@ -2,6 +2,7 @@ const debug = require("debug")("engine-stream-switcher");
|
|
|
2
2
|
const crypto = require("crypto");
|
|
3
3
|
const fetch = require("node-fetch");
|
|
4
4
|
const { AbortController } = require("abort-controller");
|
|
5
|
+
const { SessionState } = require('./session_state');
|
|
5
6
|
|
|
6
7
|
const SwitcherState = Object.freeze({
|
|
7
8
|
V2L_TO_LIVE: 1,
|
|
@@ -88,8 +89,8 @@ class StreamSwitcher {
|
|
|
88
89
|
return null;
|
|
89
90
|
}
|
|
90
91
|
// Handle Complete Storage Reset
|
|
91
|
-
let sessionState = await session.
|
|
92
|
-
if (sessionState ===
|
|
92
|
+
let sessionState = await session.getSessionState();
|
|
93
|
+
if (sessionState === SessionState.VOD_INIT || !sessionState) {
|
|
93
94
|
this.working = true;
|
|
94
95
|
sessionLive.waitForPlayhead = false;
|
|
95
96
|
sessionLive.allowedToSet = false; // only have effect on leader
|
|
@@ -341,14 +342,14 @@ class StreamSwitcher {
|
|
|
341
342
|
return false;
|
|
342
343
|
}
|
|
343
344
|
|
|
344
|
-
await session.setCurrentMediaSequenceSegments(
|
|
345
|
-
liveSegments.currMseqSegs,
|
|
346
|
-
liveSegments.segCount
|
|
347
|
-
);
|
|
348
345
|
await session.setCurrentMediaAndDiscSequenceCount(
|
|
349
346
|
liveCounts.mediaSeq,
|
|
350
347
|
liveCounts.discSeq
|
|
351
348
|
);
|
|
349
|
+
await session.setCurrentMediaSequenceSegments(
|
|
350
|
+
liveSegments.currMseqSegs,
|
|
351
|
+
liveSegments.segCount
|
|
352
|
+
);
|
|
352
353
|
|
|
353
354
|
this.working = false;
|
|
354
355
|
this.streamTypeLive = false;
|