eyevinn-channel-engine 3.0.1 → 3.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/nodejs.yml +1 -1
- package/README.md +3 -1
- package/engine/server.js +24 -10
- package/engine/session.js +20 -6
- package/engine/session_live.js +43 -23
- package/engine/stream_switcher.js +244 -116
- package/package.json +1 -1
- package/server.js +1 -1
package/README.md
CHANGED
|
@@ -141,8 +141,9 @@ class MyStreamSwitchManager {
|
|
|
141
141
|
getSchedule(channelId) -> [ { eventId, assetId, title, type, start_time, end_time, uri, duration } ]
|
|
142
142
|
}
|
|
143
143
|
```
|
|
144
|
+
When using **Live Mixing** in **High-Availablilty** mode, it is important to know that the class's `getSchedule(channelId)` function will be called by each channel-engine instance. Meaning that it is crucial that the implementation of this function will return the same response for each and every channel-engine instance, as they are expecting to be recieving identical lists in order to be synced up for live mixing.
|
|
144
145
|
|
|
145
|
-
The class's `getSchedule(channelId)` function should return a list of
|
|
146
|
+
The class's `getSchedule(channelId)` function should return a promise containing the list of events as an object in the following format below:
|
|
146
147
|
|
|
147
148
|
```
|
|
148
149
|
{
|
|
@@ -208,6 +209,7 @@ Below are examples of a Live stream event and a VOD event respectively:
|
|
|
208
209
|
(*Important Note: When it comes to VOD events. Desired `duration` has priority over `end_time`. Meaning that you will only resume VOD2Live content after the asset in the VOD event has finished. This may change in the future.*)
|
|
209
210
|
|
|
210
211
|
|
|
212
|
+
|
|
211
213
|
Then create an instance of the class and reference it as the `streamSwitchManager` in your engineOptions, just like you'd do with the channel manager.
|
|
212
214
|
|
|
213
215
|
```
|
package/engine/server.js
CHANGED
|
@@ -167,12 +167,18 @@ class ChannelEngine {
|
|
|
167
167
|
|
|
168
168
|
const StreamSwitchLoop = async (timeIntervalMs) => {
|
|
169
169
|
while(true) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
170
|
+
try {
|
|
171
|
+
const ts_1 = Date.now();
|
|
172
|
+
await this.updateStreamSwitchAsync()
|
|
173
|
+
const ts_2 = Date.now();
|
|
174
|
+
let interval = (timeIntervalMs - (ts_2 - ts_1)) < 0 ? 50 : (timeIntervalMs - (ts_2 - ts_1));
|
|
175
|
+
await timer(interval)
|
|
176
|
+
debug(`StreamSwitchLoop waited for all channels. Next tick in: ${interval}ms`)
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error(err)
|
|
179
|
+
debug(`StreamSwitchLoop iteration failed. Trying Again in 1000ms!`);
|
|
180
|
+
await timer(1000);
|
|
181
|
+
}
|
|
176
182
|
}
|
|
177
183
|
}
|
|
178
184
|
StreamSwitchLoop(this.streamSwitchTimeIntervalMs);
|
|
@@ -188,6 +194,7 @@ class ChannelEngine {
|
|
|
188
194
|
let status = null;
|
|
189
195
|
try {
|
|
190
196
|
status = await switcher.streamSwitcher(sessions[channel], sessionsLive[channel]);
|
|
197
|
+
debug(`[${channel}]: streamSwitcher returned switchstatus=${status}`);
|
|
191
198
|
if (status === undefined) {
|
|
192
199
|
debug(`[WARNING]: switcherStatus->${status}. Setting value to previous status->${prevStatus}`);
|
|
193
200
|
status = prevStatus;
|
|
@@ -200,7 +207,13 @@ class ChannelEngine {
|
|
|
200
207
|
debug(`Tried to switch stream on a non-existing channel=[${channel}]. Switching Ignored!)`);
|
|
201
208
|
}
|
|
202
209
|
}
|
|
203
|
-
|
|
210
|
+
try {
|
|
211
|
+
await Promise.all(channels.map(channel => getSwitchStatusAndPerformSwitch(channel)));
|
|
212
|
+
} catch (err) {
|
|
213
|
+
debug('Problem occured when updating streamSwitchers');
|
|
214
|
+
throw new Error (err);
|
|
215
|
+
}
|
|
216
|
+
|
|
204
217
|
}
|
|
205
218
|
|
|
206
219
|
async updateChannelsAsync(channelMgr, options) {
|
|
@@ -521,7 +534,7 @@ class ChannelEngine {
|
|
|
521
534
|
await timer(500);
|
|
522
535
|
}
|
|
523
536
|
let body = null;
|
|
524
|
-
debug(`switcherStatus[req.params[1]]=[${switcherStatus[req.params[1]]}]`);
|
|
537
|
+
debug(`switcherStatus[${req.params[1]}]=[${switcherStatus[req.params[1]]}]`);
|
|
525
538
|
if (switcherStatus[req.params[1]]) {
|
|
526
539
|
debug(`[${req.params[1]}]: Responding with Live-stream manifest`);
|
|
527
540
|
body = await sessionLive.getCurrentMediaManifestAsync(req.params[0]);
|
|
@@ -663,8 +676,9 @@ class ChannelEngine {
|
|
|
663
676
|
let sessionResets = [];
|
|
664
677
|
for (const sessionId of Object.keys(sessions)) {
|
|
665
678
|
const session = sessions[sessionId];
|
|
666
|
-
|
|
667
|
-
|
|
679
|
+
const sessionLive = sessionsLive[sessionId];
|
|
680
|
+
if (session && sessionLive) {
|
|
681
|
+
await session.resetAsync();
|
|
668
682
|
sessionResets.push(sessionId);
|
|
669
683
|
} else {
|
|
670
684
|
const err = new errs.NotFoundError('Invalid session');
|
package/engine/session.js
CHANGED
|
@@ -43,6 +43,7 @@ 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.waitingForNextVod = false;
|
|
46
47
|
|
|
47
48
|
if (config) {
|
|
48
49
|
if (config.sessionId) {
|
|
@@ -362,6 +363,7 @@ class Session {
|
|
|
362
363
|
throw new Error('Session not ready');
|
|
363
364
|
}
|
|
364
365
|
const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo"]);
|
|
366
|
+
const discSeqOffset = await this._sessionState.get("discSeq");
|
|
365
367
|
if (playheadState.vodMediaSeqVideo === 0) {
|
|
366
368
|
const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
367
369
|
if (!isLeader) {
|
|
@@ -372,7 +374,7 @@ class Session {
|
|
|
372
374
|
const currentVod = await this._sessionState.getCurrentVod();
|
|
373
375
|
if (currentVod) {
|
|
374
376
|
try {
|
|
375
|
-
const discSeqCount = currentVod.
|
|
377
|
+
const discSeqCount = discSeqOffset + currentVod.discontinuities[playheadState.vodMediaSeqVideo];
|
|
376
378
|
debug(`[${this._sessionId}]: MediaSeq: (${(playheadState.mediaSeq + playheadState.vodMediaSeqVideo)}) and DiscSeq: (${discSeqCount}) requested `);
|
|
377
379
|
return {
|
|
378
380
|
'mediaSeq': (playheadState.mediaSeq + playheadState.vodMediaSeqVideo),
|
|
@@ -756,6 +758,13 @@ class Session {
|
|
|
756
758
|
sessionState.state = SessionState.VOD_INIT;
|
|
757
759
|
}
|
|
758
760
|
|
|
761
|
+
if (!isLeader && this.waitingForNextVod) {
|
|
762
|
+
// By now Leader should have added the next Vod in store
|
|
763
|
+
debug(`[${this._sessionId}]: Not leader! New VOD loaded during last tick. Invalidate current VOD cache`);
|
|
764
|
+
await this._sessionState.clearCurrentVodCache();
|
|
765
|
+
this.waitingForNextVod = false;
|
|
766
|
+
}
|
|
767
|
+
|
|
759
768
|
switch (sessionState.state) {
|
|
760
769
|
case SessionState.VOD_INIT:
|
|
761
770
|
case SessionState.VOD_INIT_BY_ID:
|
|
@@ -842,10 +851,13 @@ class Session {
|
|
|
842
851
|
}
|
|
843
852
|
}
|
|
844
853
|
case SessionState.VOD_PLAYING:
|
|
845
|
-
if (!isLeader
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
854
|
+
if (!isLeader) {
|
|
855
|
+
if (sessionState.vodMediaSeqVideo === 0 || this.waitingForNextVod) {
|
|
856
|
+
debug(`[${this._sessionId}]: Not leader! Invalidate current VOD cache and fetch the new one from the leader`);
|
|
857
|
+
await this._sessionState.clearCurrentVodCache();
|
|
858
|
+
currentVod = await this._sessionState.getCurrentVod();
|
|
859
|
+
this.waitingForNextVod = false;
|
|
860
|
+
}
|
|
849
861
|
}
|
|
850
862
|
debug(`[${this._sessionId}]: state=VOD_PLAYING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}, ${currentVod.getLiveMediaSequencesCount()})`);
|
|
851
863
|
return;
|
|
@@ -853,10 +865,11 @@ class Session {
|
|
|
853
865
|
debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}, ${currentVod.getLiveMediaSequencesCount()})`);
|
|
854
866
|
if (!isLeader) {
|
|
855
867
|
debug(`[${this._sessionId}]: not the leader so just waiting for the VOD to be initiated`);
|
|
856
|
-
if (sessionState.vodMediaSeqVideo === 0) {
|
|
868
|
+
if (sessionState.vodMediaSeqVideo === 0 || this.waitingForNextVod) {
|
|
857
869
|
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`);
|
|
858
870
|
await this._sessionState.clearCurrentVodCache();
|
|
859
871
|
}
|
|
872
|
+
this.waitingForNextVod = true;
|
|
860
873
|
}
|
|
861
874
|
return;
|
|
862
875
|
case SessionState.VOD_NEXT_INIT:
|
|
@@ -938,6 +951,7 @@ class Session {
|
|
|
938
951
|
return;
|
|
939
952
|
} else {
|
|
940
953
|
debug(`[${this._sessionId}]: not a leader so will go directly to state VOD_NEXT_INITIATING`);
|
|
954
|
+
this.waitingForNextVod = true;
|
|
941
955
|
sessionState.state = await this._sessionState.set("state", SessionState.VOD_NEXT_INITIATING);
|
|
942
956
|
sessionState.currentVod = await this._sessionState.getCurrentVod();
|
|
943
957
|
}
|
package/engine/session_live.js
CHANGED
|
@@ -136,7 +136,7 @@ class SessionLive {
|
|
|
136
136
|
if (!this.masterManifestUri) {
|
|
137
137
|
await timer(3000);
|
|
138
138
|
continue;
|
|
139
|
-
}
|
|
139
|
+
}
|
|
140
140
|
if (this.playheadState === PlayheadState.STOPPED) {
|
|
141
141
|
debug(`[${this.sessionId}]: Playhead has Stopped, clearing local session and store.`);
|
|
142
142
|
this.waitForPlayhead = false;
|
|
@@ -289,6 +289,13 @@ class SessionLive {
|
|
|
289
289
|
// Don't use what Session gave you. Use the Leaders number if it's available
|
|
290
290
|
const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId);
|
|
291
291
|
let liveCounts = await this.sessionLiveState.get("firstCounts");
|
|
292
|
+
if (liveCounts === null) {
|
|
293
|
+
liveCounts = {
|
|
294
|
+
liveSourceMseqCount: null,
|
|
295
|
+
mediaSeqCount: null,
|
|
296
|
+
discSeqCount: null,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
292
299
|
if (isLeader) {
|
|
293
300
|
liveCounts.discSeqCount = this.discSeqCount;
|
|
294
301
|
await this.sessionLiveState.set("firstCounts", liveCounts);
|
|
@@ -479,9 +486,13 @@ class SessionLive {
|
|
|
479
486
|
const vodBws = Object.keys(this.vodSegments);
|
|
480
487
|
const liveBws = Object.keys(this.liveSegsForFollowers);
|
|
481
488
|
const size = this.liveSegsForFollowers[liveBws[0]].length;
|
|
482
|
-
|
|
489
|
+
|
|
490
|
+
let newSegCount = 0;
|
|
491
|
+
this.liveSegsForFollowers[liveBws[0]].map((seg) => {if (seg.uri) { newSegCount++ }});
|
|
492
|
+
|
|
493
|
+
debug(`[${this.sessionId}]: FOLLOWER: liveSegsForFollowers Bws=${Object.keys(this.liveSegsForFollowers)}_newSegCount=${newSegCount} `);
|
|
483
494
|
// Remove transitional segs & add live source segs collected from store
|
|
484
|
-
for (let k = 0; k <
|
|
495
|
+
for (let k = 0; k < newSegCount; k++) {
|
|
485
496
|
let incrementDiscSeqCount = false;
|
|
486
497
|
// Shift the top vod segment on all variants
|
|
487
498
|
for (let i = 0; i < vodBws.length; i++) {
|
|
@@ -493,12 +504,13 @@ class SessionLive {
|
|
|
493
504
|
}
|
|
494
505
|
if (incrementDiscSeqCount) {
|
|
495
506
|
this.discSeqCount++;
|
|
496
|
-
incrementDiscSeqCount = false;
|
|
497
507
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
508
|
+
}
|
|
509
|
+
// Push to bottom, new live source segment on all variants
|
|
510
|
+
for (let segIdx = 0; segIdx < size; segIdx++) {
|
|
511
|
+
let incrementDiscSeqCount = false;
|
|
512
|
+
liveBws.forEach((bw)=>{
|
|
513
|
+
let liveSegFromLeader = this.liveSegsForFollowers[bw][segIdx];
|
|
502
514
|
if (!this.liveSegQueue[bw]) {
|
|
503
515
|
this.liveSegQueue[bw] = [];
|
|
504
516
|
}
|
|
@@ -507,19 +519,21 @@ class SessionLive {
|
|
|
507
519
|
let segCount = 0;
|
|
508
520
|
this.liveSegQueue[bw].map((seg) => {if (seg.uri) { segCount++ }});
|
|
509
521
|
if (segCount > this.targetNumSeg) {
|
|
510
|
-
seg = this.liveSegQueue[bw].shift();
|
|
522
|
+
let seg = this.liveSegQueue[bw].shift();
|
|
511
523
|
if (seg && seg.discontinuity) {
|
|
512
524
|
incrementDiscSeqCount = true;
|
|
513
525
|
seg = this.liveSegQueue[bw].shift();
|
|
514
526
|
}
|
|
515
527
|
}
|
|
516
|
-
debug(`[${this.sessionId}]: Pushed
|
|
517
|
-
}
|
|
528
|
+
debug(`[${this.sessionId}]: Pushed segment (${liveSegFromLeader.uri ? liveSegFromLeader.uri : "Disc-tag"}) to 'liveSegQueue'`);
|
|
529
|
+
});
|
|
518
530
|
if (incrementDiscSeqCount) {
|
|
519
531
|
this.discSeqCount++;
|
|
520
532
|
}
|
|
521
|
-
this.mediaSeqCount++;
|
|
522
533
|
}
|
|
534
|
+
let prevVal = this.mediaSeqCount;
|
|
535
|
+
this.mediaSeqCount += newSegCount;
|
|
536
|
+
debug(`[${this.sessionId}]: Incrementing Mseq Count from [${prevVal}] -> [${this.mediaSeqCount}]`);
|
|
523
537
|
debug(`[${this.sessionId}]: Finished updating all Follower's Counts and Segment Queues!`);
|
|
524
538
|
}
|
|
525
539
|
|
|
@@ -810,7 +824,7 @@ class SessionLive {
|
|
|
810
824
|
}
|
|
811
825
|
|
|
812
826
|
// [LASTLY]: LEADER does this for respawned-FOLLOWERS' sake.
|
|
813
|
-
if (this.firstTime) {
|
|
827
|
+
if (this.firstTime && this.allowedToSet) {
|
|
814
828
|
// Buy some time for followers (NOT Respawned) to fetch their own L.S m3u8.
|
|
815
829
|
await timer(1000); // maybe remove
|
|
816
830
|
const firstCounts = {
|
|
@@ -1004,16 +1018,17 @@ class SessionLive {
|
|
|
1004
1018
|
cueData["assetData"] = attributes["assetData"];
|
|
1005
1019
|
}
|
|
1006
1020
|
if ("daterange" in attributes) {
|
|
1007
|
-
if(!daterangeData) {
|
|
1008
|
-
daterangeData = {}
|
|
1021
|
+
if (!daterangeData) {
|
|
1022
|
+
daterangeData = {};
|
|
1009
1023
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1024
|
+
let allDaterangeAttributes = Object.keys(attributes["daterange"]);
|
|
1025
|
+
allDaterangeAttributes.forEach((attr) => {
|
|
1026
|
+
if (attr.match(/DURATION$/)) {
|
|
1027
|
+
daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]);
|
|
1028
|
+
} else {
|
|
1029
|
+
daterangeData[attr.toLowerCase()] = attributes["daterange"][attr];
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1017
1032
|
}
|
|
1018
1033
|
if (playlistItem.properties.uri) {
|
|
1019
1034
|
if (playlistItem.properties.uri.match("^http")) {
|
|
@@ -1034,6 +1049,7 @@ class SessionLive {
|
|
|
1034
1049
|
this.liveSegQueue[liveTargetBandwidth].map((seg) => {if (seg.uri) { segCount++ }});
|
|
1035
1050
|
debug(`[${this.sessionId}]: size of queue=${segCount}_targetNumseg=${this.targetNumSeg}`);
|
|
1036
1051
|
if (segCount > this.targetNumSeg) {
|
|
1052
|
+
debug(`[${this.sessionId}]: liveTargetBandwidth=${liveTargetBandwidth}_seg=${JSON.stringify(seg,null,2)}`);
|
|
1037
1053
|
seg = this.liveSegQueue[liveTargetBandwidth].shift();
|
|
1038
1054
|
if (seg) {
|
|
1039
1055
|
if (seg.discontinuity) {
|
|
@@ -1041,6 +1057,7 @@ class SessionLive {
|
|
|
1041
1057
|
amountRemovedDiscSeqs++;
|
|
1042
1058
|
}
|
|
1043
1059
|
}
|
|
1060
|
+
debug(`[${this.sessionId}]: After Shifting -> size of queue=${segCount}_targetNumseg=${this.targetNumSeg}`);
|
|
1044
1061
|
}
|
|
1045
1062
|
}
|
|
1046
1063
|
}
|
|
@@ -1155,9 +1172,12 @@ class SessionLive {
|
|
|
1155
1172
|
}
|
|
1156
1173
|
}
|
|
1157
1174
|
}
|
|
1175
|
+
if (seg.datetime) {
|
|
1176
|
+
m3u8 += `#EXT-X-PROGRAM-DATE-TIME:${seg.datetime}\n`;
|
|
1177
|
+
}
|
|
1158
1178
|
if (seg.daterange) {
|
|
1159
1179
|
const dateRangeAttributes = Object.keys(seg.daterange).map(key => daterangeAttribute(key, seg.daterange[key])).join(',');
|
|
1160
|
-
if (seg.daterange['start-date']) {
|
|
1180
|
+
if (!seg.datetime && seg.daterange['start-date']) {
|
|
1161
1181
|
m3u8 += "#EXT-X-PROGRAM-DATE-TIME:" + seg.daterange['start-date'] + "\n";
|
|
1162
1182
|
}
|
|
1163
1183
|
m3u8 += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n";
|
|
@@ -30,6 +30,7 @@ class StreamSwitcher {
|
|
|
30
30
|
this.timeDiff = null;
|
|
31
31
|
this.abortTimeStamp = null;
|
|
32
32
|
|
|
33
|
+
|
|
33
34
|
if (config) {
|
|
34
35
|
if (config.sessionId) {
|
|
35
36
|
this.sessionId = config.sessionId;
|
|
@@ -50,6 +51,26 @@ class StreamSwitcher {
|
|
|
50
51
|
return this.eventId;
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
async abortLiveFeed(session, sessionLive, message) {
|
|
55
|
+
if (this.streamTypeLive) {
|
|
56
|
+
let status = null;
|
|
57
|
+
debug(`[${this.sessionId}]: Abort Live Stream! Reason: ${message}`);
|
|
58
|
+
try {
|
|
59
|
+
this.abortTimeStamp = Date.now();
|
|
60
|
+
status = await this._initSwitching(
|
|
61
|
+
SwitcherState.LIVE_TO_V2L,
|
|
62
|
+
session,
|
|
63
|
+
sessionLive,
|
|
64
|
+
null
|
|
65
|
+
);
|
|
66
|
+
return status;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
debug(`Failed to force a switch off live feed: ${err}`);
|
|
69
|
+
throw new Error(err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
/**
|
|
54
75
|
*
|
|
55
76
|
* @param {Session} session The VOD2Live Session object.
|
|
@@ -57,6 +78,7 @@ class StreamSwitcher {
|
|
|
57
78
|
* @returns A bool, true if streamSwitchManager contains current Live event to be played else false.
|
|
58
79
|
*/
|
|
59
80
|
async streamSwitcher(session, sessionLive) {
|
|
81
|
+
let status = null;
|
|
60
82
|
if (!this.streamSwitchManager) {
|
|
61
83
|
debug(`[${this.sessionId}]: No streamSwitchManager available`);
|
|
62
84
|
return false;
|
|
@@ -65,11 +87,28 @@ class StreamSwitcher {
|
|
|
65
87
|
debug(`[${this.sessionId}]: streamSwitcher is currently busy`);
|
|
66
88
|
return null;
|
|
67
89
|
}
|
|
68
|
-
|
|
90
|
+
// Handle Complete Storage Reset
|
|
91
|
+
let sessionState = await session._sessionState.get("state");
|
|
92
|
+
if (sessionState === 1 || !sessionState) {
|
|
93
|
+
this.working = true;
|
|
94
|
+
sessionLive.waitForPlayhead = false;
|
|
95
|
+
sessionLive.allowedToSet = false; // only have effect on leader
|
|
96
|
+
await sessionLive.resetSession();
|
|
97
|
+
await sessionLive.resetLiveStoreAsync(0);
|
|
98
|
+
this.working = false;
|
|
99
|
+
this.abortTimeStamp = Date.now() + 20 * 1000; // 30 second V2L->LIVE timeout
|
|
100
|
+
if (this.streamTypeLive) {
|
|
101
|
+
debug(`[${this.sessionId}]: [ Ending LIVE Abruptly, Going to -> V2L ]`);
|
|
102
|
+
this.streamTypeLive = false;
|
|
103
|
+
}
|
|
104
|
+
debug(`[${this.sessionId}]: StreamSwitcher reacting to Full Store Reset`);
|
|
105
|
+
return false; // Go to V2L feed
|
|
106
|
+
}
|
|
107
|
+
|
|
69
108
|
// Filter out schedule objects from the past
|
|
70
|
-
const tsNow = Date.now();
|
|
109
|
+
const tsNow = Date.now();
|
|
71
110
|
const strmSchedule = await this.streamSwitchManager.getSchedule(this.sessionId);
|
|
72
|
-
const schedule = strmSchedule.filter((obj) => obj.end_time > tsNow);
|
|
111
|
+
const schedule = strmSchedule.filter((obj) => obj.end_time > tsNow);
|
|
73
112
|
if (schedule.length === 0 && this.streamTypeLive) {
|
|
74
113
|
status = await this._initSwitching(
|
|
75
114
|
SwitcherState.LIVE_TO_V2L,
|
|
@@ -102,47 +141,54 @@ class StreamSwitcher {
|
|
|
102
141
|
let tries = 0;
|
|
103
142
|
let validURI = false;
|
|
104
143
|
while (!validURI && tries < MAX_FAILS) {
|
|
105
|
-
debug(
|
|
144
|
+
debug(
|
|
145
|
+
`[${
|
|
146
|
+
this.sessionId
|
|
147
|
+
}]: Switcher is validating Master URI... (tries left=${
|
|
148
|
+
MAX_FAILS - tries
|
|
149
|
+
})`
|
|
150
|
+
);
|
|
106
151
|
validURI = await this._validURI(scheduleObj.uri);
|
|
107
152
|
tries++;
|
|
108
153
|
}
|
|
109
154
|
if (!validURI) {
|
|
110
155
|
debug(`[${this.sessionId}]: Unreachable URI: [${scheduleObj.uri}]`);
|
|
111
156
|
if (this.streamTypeLive) {
|
|
112
|
-
|
|
113
|
-
this.abortTimeStamp = Date.now();
|
|
114
|
-
status = await this._initSwitching(
|
|
115
|
-
SwitcherState.LIVE_TO_V2L,
|
|
157
|
+
await this.abortLiveFeed(
|
|
116
158
|
session,
|
|
117
159
|
sessionLive,
|
|
118
|
-
|
|
160
|
+
"Switching back to VOD2Live due to unreachable URI"
|
|
119
161
|
);
|
|
120
|
-
return status;
|
|
121
162
|
}
|
|
122
163
|
return false;
|
|
123
164
|
}
|
|
124
165
|
debug(`[${this.sessionId}]: ....Master URI -> VALID`);
|
|
125
166
|
|
|
126
|
-
|
|
127
|
-
if (this.abortTimeStamp && (tsNow - this.abortTimeStamp) <= 10000) {
|
|
167
|
+
if (this.abortTimeStamp && tsNow - this.abortTimeStamp <= 10000) {
|
|
128
168
|
// If we have a valid URI and no more than 10 seconds have passed since switching from Live->V2L.
|
|
129
169
|
// Stay on V2L to give live sessionLive some time to prepare before switching back to live.
|
|
130
|
-
debug(
|
|
170
|
+
debug(
|
|
171
|
+
`[${this.sessionId}]: Waiting [${
|
|
172
|
+
10000 - (tsNow - this.abortTimeStamp)
|
|
173
|
+
}ms] before switching back to Live due to unreachable URI`
|
|
174
|
+
);
|
|
131
175
|
return false;
|
|
132
|
-
}
|
|
176
|
+
}
|
|
133
177
|
this.abortTimeStamp = null;
|
|
134
178
|
|
|
135
179
|
if (this.streamTypeLive) {
|
|
136
|
-
if (
|
|
180
|
+
if (
|
|
181
|
+
tsNow >= scheduleObj.start_time &&
|
|
182
|
+
this.eventId !== scheduleObj.eventId
|
|
183
|
+
) {
|
|
137
184
|
if (scheduleObj.type === StreamType.LIVE) {
|
|
138
|
-
status =
|
|
185
|
+
status = await this._initSwitching(
|
|
139
186
|
SwitcherState.LIVE_TO_LIVE,
|
|
140
187
|
session,
|
|
141
188
|
sessionLive,
|
|
142
189
|
scheduleObj
|
|
143
190
|
);
|
|
144
191
|
return status;
|
|
145
|
-
|
|
146
192
|
}
|
|
147
193
|
status = await this._initSwitching(
|
|
148
194
|
SwitcherState.LIVE_TO_VOD,
|
|
@@ -153,7 +199,11 @@ class StreamSwitcher {
|
|
|
153
199
|
return status;
|
|
154
200
|
}
|
|
155
201
|
}
|
|
156
|
-
if (
|
|
202
|
+
if (
|
|
203
|
+
tsNow >= scheduleObj.start_time &&
|
|
204
|
+
tsNow < scheduleObj.end_time &&
|
|
205
|
+
scheduleObj.end_time - tsNow > 10000
|
|
206
|
+
) {
|
|
157
207
|
if (scheduleObj.type === StreamType.LIVE) {
|
|
158
208
|
if (!this.streamTypeLive) {
|
|
159
209
|
status = await this._initSwitching(
|
|
@@ -168,7 +218,9 @@ class StreamSwitcher {
|
|
|
168
218
|
}
|
|
169
219
|
if (!this.streamTypeLive) {
|
|
170
220
|
if (!scheduleObj.duration) {
|
|
171
|
-
debug(
|
|
221
|
+
debug(
|
|
222
|
+
`[${this.sessionId}]: Cannot switch VOD no duration specified for schedule item: [${scheduleObj.assetId}]`
|
|
223
|
+
);
|
|
172
224
|
return false;
|
|
173
225
|
}
|
|
174
226
|
if (this.eventId !== scheduleObj.eventId) {
|
|
@@ -198,133 +250,205 @@ class StreamSwitcher {
|
|
|
198
250
|
|
|
199
251
|
switch (state) {
|
|
200
252
|
case SwitcherState.V2L_TO_LIVE:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
this.eventId = null;
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
253
|
+
try {
|
|
254
|
+
debug(`[${this.sessionId}]: [ INIT Switching from V2L->LIVE ]`);
|
|
255
|
+
this.eventId = scheduleObj.eventId;
|
|
256
|
+
currVodCounts = await session.getCurrentMediaAndDiscSequenceCount();
|
|
257
|
+
currVodSegments = await session.getCurrentMediaSequenceSegments();
|
|
258
|
+
|
|
259
|
+
// In risk that the SL-playhead might have updated some data after
|
|
260
|
+
// we reset last time... we should Reset SessionLive before sending new data.
|
|
261
|
+
await sessionLive.resetLiveStoreAsync(0);
|
|
262
|
+
await sessionLive.setCurrentMediaAndDiscSequenceCount(
|
|
263
|
+
currVodCounts.mediaSeq,
|
|
264
|
+
currVodCounts.discSeq
|
|
265
|
+
);
|
|
266
|
+
await sessionLive.setCurrentMediaSequenceSegments(currVodSegments);
|
|
267
|
+
liveUri = await sessionLive.setLiveUri(scheduleObj.uri);
|
|
219
268
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.eventId = scheduleObj.eventId;
|
|
227
|
-
currVodCounts = await session.getCurrentMediaAndDiscSequenceCount();
|
|
228
|
-
eventSegments = await session.getTruncatedVodSegments(scheduleObj.uri, scheduleObj.duration / 1000);
|
|
269
|
+
if (!liveUri) {
|
|
270
|
+
debug(`[${this.sessionId}]: [ ERROR Switching from V2L->LIVE ]`);
|
|
271
|
+
this.working = false;
|
|
272
|
+
this.eventId = null;
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
229
275
|
|
|
230
|
-
|
|
231
|
-
|
|
276
|
+
this.working = false;
|
|
277
|
+
this.streamTypeLive = true;
|
|
278
|
+
debug(`[${this.sessionId}]: [ Switched from V2L->LIVE ]`);
|
|
279
|
+
return true;
|
|
280
|
+
} catch (err) {
|
|
281
|
+
this.streamTypeLive = false;
|
|
232
282
|
this.working = false;
|
|
233
283
|
this.eventId = null;
|
|
234
|
-
|
|
284
|
+
debug(`[${this.sessionId}]: [ ERROR Switching from V2L->LIVE ]`);
|
|
285
|
+
throw new Error(err);
|
|
235
286
|
}
|
|
236
287
|
|
|
237
|
-
|
|
238
|
-
|
|
288
|
+
case SwitcherState.V2L_TO_VOD:
|
|
289
|
+
try {
|
|
290
|
+
debug(`[${this.sessionId}]: [ INIT Switching from V2L->VOD ]`);
|
|
291
|
+
this.eventId = scheduleObj.eventId;
|
|
292
|
+
currVodCounts = await session.getCurrentMediaAndDiscSequenceCount();
|
|
293
|
+
eventSegments = await session.getTruncatedVodSegments(
|
|
294
|
+
scheduleObj.uri,
|
|
295
|
+
scheduleObj.duration / 1000
|
|
296
|
+
);
|
|
239
297
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
liveSegments = await sessionLive.getCurrentMediaSequenceSegments();
|
|
247
|
-
liveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount();
|
|
298
|
+
if (!eventSegments) {
|
|
299
|
+
debug(`[${this.sessionId}]: [ ERROR Switching from V2L->VOD ]`);
|
|
300
|
+
this.working = false;
|
|
301
|
+
this.eventId = null;
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
248
304
|
|
|
249
|
-
|
|
250
|
-
|
|
305
|
+
await session.setCurrentMediaAndDiscSequenceCount(
|
|
306
|
+
currVodCounts.mediaSeq,
|
|
307
|
+
currVodCounts.discSeq
|
|
308
|
+
);
|
|
309
|
+
await session.setCurrentMediaSequenceSegments(eventSegments, 0, true);
|
|
251
310
|
|
|
252
|
-
|
|
253
|
-
debug(`[${this.sessionId}]:
|
|
311
|
+
this.working = false;
|
|
312
|
+
debug(`[${this.sessionId}]: [ Switched from V2L->VOD ]`);
|
|
313
|
+
return false;
|
|
314
|
+
} catch (err) {
|
|
315
|
+
this.streamTypeLive = false;
|
|
316
|
+
this.working = false;
|
|
317
|
+
this.eventId = null;
|
|
318
|
+
debug(`[${this.sessionId}]: [ ERROR Switching from V2L->VOD ]`);
|
|
319
|
+
throw new Error(err);
|
|
254
320
|
}
|
|
321
|
+
case SwitcherState.LIVE_TO_V2L:
|
|
322
|
+
try {
|
|
323
|
+
debug(`[${this.sessionId}]: [ INIT Switching from LIVE->V2L ]`);
|
|
324
|
+
this.eventId = null;
|
|
325
|
+
liveSegments = await sessionLive.getCurrentMediaSequenceSegments();
|
|
326
|
+
liveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount();
|
|
327
|
+
|
|
328
|
+
await sessionLive.resetSession();
|
|
329
|
+
sessionLive.resetLiveStoreAsync(RESET_DELAY); // In parallel
|
|
330
|
+
|
|
331
|
+
if (scheduleObj && !scheduleObj.duration) {
|
|
332
|
+
debug(
|
|
333
|
+
`[${this.sessionId}]: Cannot switch VOD. No duration specified for schedule item: [${scheduleObj.assetId}]`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (this._isEmpty(liveSegments.currMseqSegs)) {
|
|
338
|
+
this.working = false;
|
|
339
|
+
this.streamTypeLive = false;
|
|
340
|
+
debug(`[${this.sessionId}]: [ Switched from LIVE->V2L ]`);
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
await session.setCurrentMediaSequenceSegments(
|
|
345
|
+
liveSegments.currMseqSegs,
|
|
346
|
+
liveSegments.segCount
|
|
347
|
+
);
|
|
348
|
+
await session.setCurrentMediaAndDiscSequenceCount(
|
|
349
|
+
liveCounts.mediaSeq,
|
|
350
|
+
liveCounts.discSeq
|
|
351
|
+
);
|
|
255
352
|
|
|
256
|
-
if (this._isEmpty(liveSegments.currMseqSegs)) {
|
|
257
353
|
this.working = false;
|
|
258
354
|
this.streamTypeLive = false;
|
|
259
355
|
debug(`[${this.sessionId}]: [ Switched from LIVE->V2L ]`);
|
|
260
|
-
return false
|
|
356
|
+
return false;
|
|
357
|
+
} catch (err) {
|
|
358
|
+
this.streamTypeLive = false;
|
|
359
|
+
this.working = false;
|
|
360
|
+
this.eventId = null;
|
|
361
|
+
debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->V2L ]`);
|
|
362
|
+
throw new Error(err);
|
|
261
363
|
}
|
|
262
|
-
|
|
263
|
-
await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount);
|
|
264
|
-
await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq, liveCounts.discSeq);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
this.working = false;
|
|
268
|
-
this.streamTypeLive = false;
|
|
269
|
-
debug(`[${this.sessionId}]: [ Switched from LIVE->V2L ]`);
|
|
270
|
-
return false;
|
|
271
364
|
case SwitcherState.LIVE_TO_VOD:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
365
|
+
try {
|
|
366
|
+
debug(`[${this.sessionId}]: INIT Switching from LIVE->VOD`);
|
|
367
|
+
// TODO: Not yet fully tested/supported
|
|
368
|
+
this.eventId = scheduleObj.eventId;
|
|
369
|
+
liveSegments = await sessionLive.getCurrentMediaSequenceSegments();
|
|
370
|
+
liveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount();
|
|
371
|
+
await sessionLive.resetSession();
|
|
372
|
+
sessionLive.resetLiveStoreAsync(RESET_DELAY); // In parallel
|
|
373
|
+
|
|
374
|
+
eventSegments = await session.getTruncatedVodSegments(
|
|
375
|
+
scheduleObj.uri,
|
|
376
|
+
scheduleObj.duration / 1000
|
|
377
|
+
);
|
|
378
|
+
if (!eventSegments) {
|
|
379
|
+
debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->VOD ]`);
|
|
380
|
+
this.streamTypeLive = false;
|
|
381
|
+
this.working = false;
|
|
382
|
+
this.eventId = null;
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
await session.setCurrentMediaAndDiscSequenceCount(
|
|
387
|
+
liveCounts.mediaSeq - 1,
|
|
388
|
+
liveCounts.discSeq - 1
|
|
389
|
+
);
|
|
390
|
+
await session.setCurrentMediaSequenceSegments(
|
|
391
|
+
liveSegments.currMseqSegs,
|
|
392
|
+
liveSegments.segCount
|
|
393
|
+
);
|
|
394
|
+
await session.setCurrentMediaSequenceSegments(eventSegments, 0, true);
|
|
395
|
+
|
|
396
|
+
this.working = false;
|
|
397
|
+
this.streamTypeLive = false;
|
|
398
|
+
debug(`[${this.sessionId}]: Switched from LIVE->VOD`);
|
|
399
|
+
return false;
|
|
400
|
+
} catch (err) {
|
|
283
401
|
this.streamTypeLive = false;
|
|
284
402
|
this.working = false;
|
|
285
403
|
this.eventId = null;
|
|
286
|
-
|
|
404
|
+
debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->VOD ]`);
|
|
405
|
+
throw new Error(err);
|
|
287
406
|
}
|
|
288
|
-
|
|
289
|
-
await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq - 1, liveCounts.discSeq - 1);
|
|
290
|
-
await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount);
|
|
291
|
-
await session.setCurrentMediaSequenceSegments(eventSegments, 0, true);
|
|
292
|
-
|
|
293
|
-
this.working = false;
|
|
294
|
-
this.streamTypeLive = false;
|
|
295
|
-
debug(`[${this.sessionId}]: Switched from LIVE->VOD`);
|
|
296
|
-
return false;
|
|
297
407
|
case SwitcherState.LIVE_TO_LIVE:
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
408
|
+
try {
|
|
409
|
+
debug(`[${this.sessionId}]: INIT Switching from LIVE->LIVE`);
|
|
410
|
+
// TODO: Not yet fully tested/supported
|
|
411
|
+
this.eventId = scheduleObj.eventId;
|
|
412
|
+
eventSegments = await sessionLive.getCurrentMediaSequenceSegments();
|
|
413
|
+
currLiveCounts =
|
|
414
|
+
await sessionLive.getCurrentMediaAndDiscSequenceCount();
|
|
415
|
+
|
|
416
|
+
await sessionLive.resetSession();
|
|
417
|
+
await sessionLive.resetLiveStoreAsync(0);
|
|
418
|
+
|
|
419
|
+
await sessionLive.setCurrentMediaAndDiscSequenceCount(
|
|
420
|
+
currLiveCounts.mediaSeq,
|
|
421
|
+
currLiveCounts.discSeq
|
|
422
|
+
);
|
|
423
|
+
await sessionLive.setCurrentMediaSequenceSegments(
|
|
424
|
+
eventSegments.currMseqSegs
|
|
425
|
+
);
|
|
426
|
+
liveUri = await sessionLive.setLiveUri(scheduleObj.uri);
|
|
306
427
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
428
|
+
if (!liveUri) {
|
|
429
|
+
debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->LIVE ]`);
|
|
430
|
+
this.streamTypeLive = false;
|
|
431
|
+
this.working = false;
|
|
432
|
+
this.eventId = null;
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
310
435
|
|
|
311
|
-
|
|
312
|
-
debug(`[${this.sessionId}]:
|
|
436
|
+
this.working = false;
|
|
437
|
+
debug(`[${this.sessionId}]: Switched from LIVE->LIVE`);
|
|
438
|
+
return true;
|
|
439
|
+
} catch (err) {
|
|
313
440
|
this.streamTypeLive = false;
|
|
314
441
|
this.working = false;
|
|
315
442
|
this.eventId = null;
|
|
316
|
-
|
|
443
|
+
debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->LIVE ]`);
|
|
444
|
+
throw new Error(err);
|
|
317
445
|
}
|
|
318
|
-
|
|
319
|
-
this.working = false;
|
|
320
|
-
debug(`[${this.sessionId}]: Switched from LIVE->LIVE`);
|
|
321
|
-
return true;
|
|
322
446
|
default:
|
|
323
447
|
debug(`[${this.sessionId}]: SwitcherState [${state}] not implemented`);
|
|
324
448
|
this.streamTypeLive = false;
|
|
325
449
|
this.working = false;
|
|
326
450
|
this.eventId = null;
|
|
327
|
-
return false
|
|
451
|
+
return false;
|
|
328
452
|
}
|
|
329
453
|
}
|
|
330
454
|
|
|
@@ -352,10 +476,14 @@ class StreamSwitcher {
|
|
|
352
476
|
if (online.status >= 200 && online.status < 300) {
|
|
353
477
|
return true;
|
|
354
478
|
}
|
|
355
|
-
debug(
|
|
479
|
+
debug(
|
|
480
|
+
`[${this.sessionId}]: Failed to validate URI: ${uri}\nERROR! Returned Status Code: ${online.status}`
|
|
481
|
+
);
|
|
356
482
|
return false;
|
|
357
483
|
} catch (err) {
|
|
358
|
-
debug(
|
|
484
|
+
debug(
|
|
485
|
+
`[${this.sessionId}]: Failed to validate URI: ${uri}\nERROR! ${err}`
|
|
486
|
+
);
|
|
359
487
|
return false;
|
|
360
488
|
} finally {
|
|
361
489
|
clearTimeout(timeout);
|
package/package.json
CHANGED
package/server.js
CHANGED