eyevinn-channel-engine 3.1.9 → 3.2.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/engine/server.js +7 -4
- package/engine/session.js +45 -37
- package/engine/session_live.js +3 -1
- package/engine/stream_switcher.js +336 -196
- package/engine/util.js +95 -2
- package/package.json +1 -1
- package/server-livemix.js +6 -1
package/engine/server.js
CHANGED
|
@@ -12,7 +12,7 @@ const { SessionStateStore } = require('./session_state.js');
|
|
|
12
12
|
const { SessionLiveStateStore } = require('./session_live_state.js');
|
|
13
13
|
const { PlayheadStateStore } = require('./playhead_state.js');
|
|
14
14
|
|
|
15
|
-
const { filterQueryParser, toHHMMSS } = require('./util.js');
|
|
15
|
+
const { filterQueryParser, toHHMMSS, WaitTimeGenerator } = require('./util.js');
|
|
16
16
|
const { version } = require('../package.json');
|
|
17
17
|
|
|
18
18
|
const timer = ms => new Promise(res => setTimeout(res, ms));
|
|
@@ -166,14 +166,17 @@ class ChannelEngine {
|
|
|
166
166
|
}, 3000);
|
|
167
167
|
|
|
168
168
|
const StreamSwitchLoop = async (timeIntervalMs) => {
|
|
169
|
+
const minIntervalMs = 50;
|
|
170
|
+
const WTG = new WaitTimeGenerator(timeIntervalMs, minIntervalMs);
|
|
169
171
|
while(true) {
|
|
170
172
|
try {
|
|
171
173
|
const ts_1 = Date.now();
|
|
172
174
|
await this.updateStreamSwitchAsync()
|
|
173
175
|
const ts_2 = Date.now();
|
|
174
|
-
|
|
175
|
-
await
|
|
176
|
-
|
|
176
|
+
const interval = (timeIntervalMs - (ts_2 - ts_1)) < 0 ? minIntervalMs : (timeIntervalMs - (ts_2 - ts_1));
|
|
177
|
+
const tickInterval = await WTG.getWaitTime(interval);
|
|
178
|
+
await timer(tickInterval)
|
|
179
|
+
debug(`StreamSwitchLoop waited for all channels. Next tick in: ${tickInterval}ms`)
|
|
177
180
|
} catch (err) {
|
|
178
181
|
console.error(err)
|
|
179
182
|
debug(`StreamSwitchLoop iteration failed. Trying Again in 1000ms!`);
|
package/engine/session.js
CHANGED
|
@@ -57,6 +57,7 @@ class Session {
|
|
|
57
57
|
transitionSegments: null,
|
|
58
58
|
reloadBehind: null,
|
|
59
59
|
}
|
|
60
|
+
this.alreadyClearedVodCache = null;
|
|
60
61
|
|
|
61
62
|
if (config) {
|
|
62
63
|
if (config.sessionId) {
|
|
@@ -288,6 +289,14 @@ class Session {
|
|
|
288
289
|
this.switchDataForSession.transitionSegments = segments;
|
|
289
290
|
this.switchDataForSession.mediaSeqOffset = mSeqOffset;
|
|
290
291
|
|
|
292
|
+
let waitTimeMs = 2000;
|
|
293
|
+
for (let i = segments[Object.keys(segments)[0]].length - 1; 0 < i; i--) {
|
|
294
|
+
if (segments[Object.keys(segments)[0]][i].duration) {
|
|
295
|
+
waitTimeMs = parseInt(1000 * (segments[Object.keys(segments)[0]][i].duration / 3), 10);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
291
300
|
let isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
292
301
|
if (!isLeader) {
|
|
293
302
|
debug(`[${this._sessionId}]: FOLLOWER: Invalidate cache to ensure having the correct VOD!`);
|
|
@@ -314,14 +323,14 @@ class Session {
|
|
|
314
323
|
}
|
|
315
324
|
debug(`[${this._sessionId}]: NEW LEADER: Setting state=VOD_RELOAD_INIT`);
|
|
316
325
|
this.isSwitchingBackToV2L = true;
|
|
317
|
-
await this._sessionState.set("state", SessionState.VOD_RELOAD_INIT)
|
|
326
|
+
await this._sessionState.set("state", SessionState.VOD_RELOAD_INIT);
|
|
318
327
|
|
|
319
328
|
} else {
|
|
320
329
|
let vodReloaded = await this._sessionState.get("vodReloaded");
|
|
321
|
-
let attempts =
|
|
330
|
+
let attempts = 12;
|
|
322
331
|
while (!vodReloaded && attempts > 0) {
|
|
323
|
-
debug(`[${this._sessionId}]: LEADER: Waiting (
|
|
324
|
-
await timer(
|
|
332
|
+
debug(`[${this._sessionId}]: LEADER: Waiting (${waitTimeMs}ms) to buy some time reloading vod and adding it to store! (tries left=${attempts})`);
|
|
333
|
+
await timer(waitTimeMs);
|
|
325
334
|
vodReloaded = await this._sessionState.get("vodReloaded");
|
|
326
335
|
attempts--;
|
|
327
336
|
}
|
|
@@ -353,12 +362,9 @@ class Session {
|
|
|
353
362
|
}
|
|
354
363
|
|
|
355
364
|
const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo"]);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
await this._sessionState.clearCurrentVodCache(); // force reading up from shared store
|
|
360
|
-
}
|
|
361
|
-
}
|
|
365
|
+
|
|
366
|
+
// NOTE: Assume that VOD cache was already cleared in 'getCurrentMediaAndDiscSequenceCount()'
|
|
367
|
+
// and that we now have access to the correct vod cache
|
|
362
368
|
const currentVod = await this._sessionState.getCurrentVod();
|
|
363
369
|
if (currentVod) {
|
|
364
370
|
try {
|
|
@@ -404,10 +410,11 @@ class Session {
|
|
|
404
410
|
|
|
405
411
|
const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo"]);
|
|
406
412
|
const discSeqOffset = await this._sessionState.get("discSeq");
|
|
413
|
+
// Clear Vod Cache here when Switching to Live just to be safe...
|
|
407
414
|
if (playheadState.vodMediaSeqVideo === 0) {
|
|
408
415
|
const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
409
416
|
if (!isLeader) {
|
|
410
|
-
debug(`[${this._sessionId}]: Not a leader and first media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
|
|
417
|
+
debug(`[${this._sessionId}]: Not a leader, about to switch, and first media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
|
|
411
418
|
await this._sessionState.clearCurrentVodCache(); // force reading up from shared store
|
|
412
419
|
}
|
|
413
420
|
}
|
|
@@ -461,13 +468,18 @@ class Session {
|
|
|
461
468
|
}
|
|
462
469
|
}
|
|
463
470
|
|
|
471
|
+
// Force reading up from store, but only once if the condition is right
|
|
464
472
|
if (playheadState.vodMediaSeqVideo < 2 || playheadState.vodMediaSeqVideo < this.prevVodMediaSeq.video) {
|
|
465
473
|
debug(`[${this._sessionId}]: current[${playheadState.vodMediaSeqVideo}]_prev[${this.prevVodMediaSeq.video}]`);
|
|
466
474
|
const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
467
|
-
if (!isLeader) {
|
|
475
|
+
if (!isLeader && !this.alreadyClearedVodCache) {
|
|
468
476
|
debug(`[${this._sessionId}]: Not a leader and first|second media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
|
|
469
|
-
await this._sessionState.clearCurrentVodCache();
|
|
477
|
+
await this._sessionState.clearCurrentVodCache();
|
|
478
|
+
this.alreadyClearedVodCache = true;
|
|
470
479
|
}
|
|
480
|
+
} else {
|
|
481
|
+
// Reset the value
|
|
482
|
+
this.alreadyClearedVodCache = false;
|
|
471
483
|
}
|
|
472
484
|
|
|
473
485
|
const currentVod = await this._sessionState.getCurrentVod();
|
|
@@ -575,10 +587,23 @@ class Session {
|
|
|
575
587
|
let m3u8 = currentVod.getLiveMediaSequences(playheadState.mediaSeq, 180000, playheadState.vodMediaSeqVideo, sessionState.discSeq);
|
|
576
588
|
await this._playheadState.setLastM3u8(m3u8);
|
|
577
589
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
590
|
+
// As a FOLLOWER, we might need to read up from shared store...
|
|
591
|
+
if (playheadState.vodMediaSeqVideo < 2 || playheadState.vodMediaSeqVideo < this.prevVodMediaSeq.video) {
|
|
592
|
+
debug(`[${this._sessionId}]: current[${playheadState.vodMediaSeqVideo}]_prev[${this.prevVodMediaSeq.video}]`);
|
|
593
|
+
if (playheadState.vodMediaSeqVideo < this.prevVodMediaSeq.video) {
|
|
594
|
+
this.alreadyCleredVodCache = false;
|
|
595
|
+
}
|
|
596
|
+
if (!isLeader && !this.alreadyClearedVodCache) {
|
|
597
|
+
debug(`[${this._sessionId}]: Not a leader and have just set 'playheadState.vodMediaSeqVideo' to 0|1. Invalidate cache to ensure having the correct VOD.`);
|
|
598
|
+
await this._sessionState.clearCurrentVodCache();
|
|
599
|
+
this.alreadyClearedVodCache = true;
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
// Reset the value
|
|
603
|
+
this.alreadyClearedVodCache = false;
|
|
581
604
|
}
|
|
605
|
+
// Update Value for Previous Sequence
|
|
606
|
+
this.prevVodMediaSeq.video = playheadState.vodMediaSeqVideo;
|
|
582
607
|
|
|
583
608
|
return m3u8;
|
|
584
609
|
}
|
|
@@ -834,13 +859,6 @@ class Session {
|
|
|
834
859
|
sessionState.state = SessionState.VOD_INIT;
|
|
835
860
|
}
|
|
836
861
|
|
|
837
|
-
if (!isLeader && this.waitingForNextVod) {
|
|
838
|
-
// By now Leader should have added the next Vod in store
|
|
839
|
-
debug(`[${this._sessionId}]: Not leader! New VOD loaded during last tick. Invalidate current VOD cache`);
|
|
840
|
-
await this._sessionState.clearCurrentVodCache();
|
|
841
|
-
this.waitingForNextVod = false;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
862
|
switch (sessionState.state) {
|
|
845
863
|
case SessionState.VOD_INIT:
|
|
846
864
|
case SessionState.VOD_INIT_BY_ID:
|
|
@@ -931,19 +949,13 @@ class Session {
|
|
|
931
949
|
}
|
|
932
950
|
}
|
|
933
951
|
case SessionState.VOD_PLAYING:
|
|
934
|
-
if (
|
|
935
|
-
if (sessionState.vodMediaSeqVideo === 0 || this.waitingForNextVod) {
|
|
936
|
-
debug(`[${this._sessionId}]: Not leader! Invalidate current VOD cache and fetch the new one from the leader`);
|
|
937
|
-
await this._sessionState.clearCurrentVodCache();
|
|
938
|
-
currentVod = await this._sessionState.getCurrentVod();
|
|
939
|
-
this.waitingForNextVod = false;
|
|
940
|
-
}
|
|
941
|
-
} else {
|
|
952
|
+
if (isLeader) {
|
|
942
953
|
// Handle edge case where store has been reset, but leader has not cleared cache.
|
|
943
954
|
if (this.prevVodMediaSeq.video === null) {
|
|
944
955
|
this.prevVodMediaSeq.video = sessionState.vodMediaSeqVideo;
|
|
945
956
|
}
|
|
946
|
-
if
|
|
957
|
+
// Clear Cache if prev count is HIGHER than current...
|
|
958
|
+
if (sessionState.vodMediaSeqVideo < this.prevVodMediaSeq.video ) {
|
|
947
959
|
debug(`[${this._sessionId}]: current[${sessionState.vodMediaSeqVideo}], prev[${this.prevVodMediaSeq.video}], total[${currentVod.getLiveMediaSequencesCount()}]`);
|
|
948
960
|
await this._sessionState.clearCurrentVodCache();
|
|
949
961
|
currentVod = await this._sessionState.getCurrentVod();
|
|
@@ -956,12 +968,8 @@ class Session {
|
|
|
956
968
|
debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}, ${currentVod.getLiveMediaSequencesCount()})`);
|
|
957
969
|
if (!isLeader) {
|
|
958
970
|
debug(`[${this._sessionId}]: not the leader so just waiting for the VOD to be initiated`);
|
|
959
|
-
if (sessionState.vodMediaSeqVideo === 0 || this.waitingForNextVod) {
|
|
960
|
-
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`);
|
|
961
|
-
await this._sessionState.clearCurrentVodCache();
|
|
962
|
-
}
|
|
963
|
-
this.waitingForNextVod = true;
|
|
964
971
|
}
|
|
972
|
+
this.alreadyClearedVodCache = false;
|
|
965
973
|
return;
|
|
966
974
|
case SessionState.VOD_NEXT_INIT:
|
|
967
975
|
try {
|
package/engine/session_live.js
CHANGED
|
@@ -636,7 +636,9 @@ class SessionLive {
|
|
|
636
636
|
|
|
637
637
|
// Handle if any promise got rejected
|
|
638
638
|
if (manifestList.some((result) => result.status === "rejected")) {
|
|
639
|
-
|
|
639
|
+
FETCH_ATTEMPTS--;
|
|
640
|
+
debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again in 1000ms...`);
|
|
641
|
+
await timer(1000)
|
|
640
642
|
continue;
|
|
641
643
|
}
|
|
642
644
|
|
|
@@ -2,8 +2,9 @@ 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(
|
|
6
|
-
const { timer } = require("./util")
|
|
5
|
+
const { SessionState } = require("./session_state");
|
|
6
|
+
const { timer, findNearestValue, isValidUrl, fetchWithRetry } = require("./util");
|
|
7
|
+
const m3u8 = require("@eyevinn/m3u8");
|
|
7
8
|
|
|
8
9
|
const SwitcherState = Object.freeze({
|
|
9
10
|
V2L_TO_LIVE: 1,
|
|
@@ -31,8 +32,6 @@ class StreamSwitcher {
|
|
|
31
32
|
this.working = false;
|
|
32
33
|
this.timeDiff = null;
|
|
33
34
|
this.abortTimeStamp = null;
|
|
34
|
-
|
|
35
|
-
|
|
36
35
|
if (config) {
|
|
37
36
|
if (config.sessionId) {
|
|
38
37
|
this.sessionId = config.sessionId;
|
|
@@ -47,6 +46,7 @@ class StreamSwitcher {
|
|
|
47
46
|
this.streamSwitchManager = config.streamSwitchManager;
|
|
48
47
|
}
|
|
49
48
|
}
|
|
49
|
+
this.prerollsCache = {};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
getEventId() {
|
|
@@ -59,12 +59,7 @@ class StreamSwitcher {
|
|
|
59
59
|
debug(`[${this.sessionId}]: Abort Live Stream! Reason: ${message}`);
|
|
60
60
|
try {
|
|
61
61
|
this.abortTimeStamp = Date.now();
|
|
62
|
-
status = await this._initSwitching(
|
|
63
|
-
SwitcherState.LIVE_TO_V2L,
|
|
64
|
-
session,
|
|
65
|
-
sessionLive,
|
|
66
|
-
null
|
|
67
|
-
);
|
|
62
|
+
status = await this._initSwitching(SwitcherState.LIVE_TO_V2L, session, sessionLive, null);
|
|
68
63
|
return status;
|
|
69
64
|
} catch (err) {
|
|
70
65
|
debug(`Failed to force a switch off live feed: ${err}`);
|
|
@@ -89,158 +84,135 @@ class StreamSwitcher {
|
|
|
89
84
|
debug(`[${this.sessionId}]: streamSwitcher is currently busy`);
|
|
90
85
|
return null;
|
|
91
86
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
this.streamTypeLive
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// Handle Complete Storage Reset
|
|
90
|
+
let sessionState = await session.getSessionState();
|
|
91
|
+
if (sessionState === SessionState.VOD_INIT || !sessionState) {
|
|
92
|
+
this.working = true;
|
|
93
|
+
sessionLive.waitForPlayhead = false;
|
|
94
|
+
sessionLive.allowedToSet = false; // only have effect on leader
|
|
95
|
+
await sessionLive.resetSession();
|
|
96
|
+
await sessionLive.resetLiveStoreAsync(0);
|
|
97
|
+
this.working = false;
|
|
98
|
+
this.abortTimeStamp = Date.now() + 30 * 1000; // 30 second V2L->LIVE timeout
|
|
99
|
+
if (this.streamTypeLive) {
|
|
100
|
+
debug(`[${this.sessionId}]: [ Ending LIVE Abruptly, Going to -> V2L ]`);
|
|
101
|
+
this.streamTypeLive = false;
|
|
102
|
+
}
|
|
103
|
+
debug(`[${this.sessionId}]: StreamSwitcher reacting to Full Store Reset`);
|
|
104
|
+
return false; // Go to V2L feed
|
|
105
105
|
}
|
|
106
|
-
debug(`[${this.sessionId}]: StreamSwitcher reacting to Full Store Reset`);
|
|
107
|
-
return false; // Go to V2L feed
|
|
108
|
-
}
|
|
109
106
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
107
|
+
// Filter out schedule objects from the past
|
|
108
|
+
const tsNow = Date.now();
|
|
109
|
+
const strmSchedule = await this.streamSwitchManager.getSchedule(this.sessionId);
|
|
110
|
+
const schedule = strmSchedule.filter((obj) => obj.end_time > tsNow);
|
|
111
|
+
|
|
112
|
+
// Load Preroll, if any, once per channel every (maxAge) seconds
|
|
113
|
+
if (!this.prerollsCache[this.sessionId] || this.prerollsCache[this.sessionId].maxAge < tsNow) {
|
|
114
|
+
if (this.streamSwitchManager.getPrerollUri) {
|
|
115
|
+
const prerollUri = await this.streamSwitchManager.getPrerollUri(this.sessionId);
|
|
116
|
+
if (isValidUrl(prerollUri)) {
|
|
117
|
+
try {
|
|
118
|
+
const segments = await this._loadPreroll(prerollUri);
|
|
119
|
+
const prerollItem = {
|
|
120
|
+
segments: segments,
|
|
121
|
+
maxAge: tsNow + 30 * 60 * 1000,
|
|
122
|
+
};
|
|
123
|
+
this.prerollsCache[this.sessionId] = prerollItem;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
debug(`[${this.sessionId}]: Failed loading preroll vod for channel=${this.sessionId}`);
|
|
126
|
+
console.error(err);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
debug(`[${this.sessionId}]: Preroll uri:'${prerollUri}' is not a valid URL. Using No preroll.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (schedule.length === 0 && this.streamTypeLive) {
|
|
135
|
+
status = await this._initSwitching(SwitcherState.LIVE_TO_V2L, session, sessionLive, null);
|
|
137
136
|
return status;
|
|
138
137
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
if (schedule.length === 0) {
|
|
139
|
+
this.eventId = null;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const scheduleObj = schedule[0];
|
|
143
|
+
this.timeDiff = scheduleObj;
|
|
144
|
+
if (tsNow < scheduleObj.start_time) {
|
|
145
|
+
if (this.streamTypeLive) {
|
|
146
|
+
status = await this._initSwitching(SwitcherState.LIVE_TO_V2L, session, sessionLive, null);
|
|
147
|
+
return status;
|
|
148
|
+
}
|
|
149
|
+
this.eventId = null;
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
142
152
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
let tries = 0;
|
|
154
|
+
let validURI = false;
|
|
155
|
+
while (!validURI && tries < MAX_FAILS) {
|
|
156
|
+
debug(`[${this.sessionId}]: Switcher is validating Master URI... (tries left=${MAX_FAILS - tries})`);
|
|
157
|
+
validURI = await this._validURI(scheduleObj.uri);
|
|
158
|
+
tries++;
|
|
159
|
+
if (!validURI) {
|
|
160
|
+
const delayMs = tries * 500;
|
|
161
|
+
debug(`[${this.sessionId}]: Going to try validating Master URI again in ${delayMs}ms`);
|
|
162
|
+
await timer(delayMs);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
155
165
|
if (!validURI) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
debug(`[${this.sessionId}]: Unreachable URI: [${scheduleObj.uri}]`);
|
|
167
|
+
if (this.streamTypeLive) {
|
|
168
|
+
await this.abortLiveFeed(session, sessionLive, "Switching back to VOD2Live due to unreachable URI");
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
159
171
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"Switching back to VOD2Live due to unreachable URI"
|
|
168
|
-
);
|
|
172
|
+
debug(`[${this.sessionId}]: ....Master URI -> VALID`);
|
|
173
|
+
|
|
174
|
+
if (this.abortTimeStamp && tsNow - this.abortTimeStamp <= 10000) {
|
|
175
|
+
// If we have a valid URI and no more than 10 seconds have passed since switching from Live->V2L.
|
|
176
|
+
// Stay on V2L to give live sessionLive some time to prepare before switching back to live.
|
|
177
|
+
debug(`[${this.sessionId}]: Waiting [${10000 - (tsNow - this.abortTimeStamp)}ms] before switching back to Live due to unreachable URI`);
|
|
178
|
+
return false;
|
|
169
179
|
}
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
debug(`[${this.sessionId}]: ....Master URI -> VALID`);
|
|
173
|
-
|
|
174
|
-
if (this.abortTimeStamp && tsNow - this.abortTimeStamp <= 10000) {
|
|
175
|
-
// If we have a valid URI and no more than 10 seconds have passed since switching from Live->V2L.
|
|
176
|
-
// Stay on V2L to give live sessionLive some time to prepare before switching back to live.
|
|
177
|
-
debug(
|
|
178
|
-
`[${this.sessionId}]: Waiting [${
|
|
179
|
-
10000 - (tsNow - this.abortTimeStamp)
|
|
180
|
-
}ms] before switching back to Live due to unreachable URI`
|
|
181
|
-
);
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
this.abortTimeStamp = null;
|
|
180
|
+
this.abortTimeStamp = null;
|
|
185
181
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
status = await this._initSwitching(
|
|
193
|
-
SwitcherState.LIVE_TO_LIVE,
|
|
194
|
-
session,
|
|
195
|
-
sessionLive,
|
|
196
|
-
scheduleObj
|
|
197
|
-
);
|
|
182
|
+
if (this.streamTypeLive) {
|
|
183
|
+
if (tsNow >= scheduleObj.start_time && this.eventId !== scheduleObj.eventId) {
|
|
184
|
+
if (scheduleObj.type === StreamType.LIVE) {
|
|
185
|
+
status = await this._initSwitching(SwitcherState.LIVE_TO_LIVE, session, sessionLive, scheduleObj);
|
|
186
|
+
return status;
|
|
187
|
+
}
|
|
188
|
+
status = await this._initSwitching(SwitcherState.LIVE_TO_VOD, session, sessionLive, scheduleObj);
|
|
198
189
|
return status;
|
|
199
190
|
}
|
|
200
|
-
status = await this._initSwitching(
|
|
201
|
-
SwitcherState.LIVE_TO_VOD,
|
|
202
|
-
session,
|
|
203
|
-
sessionLive,
|
|
204
|
-
scheduleObj
|
|
205
|
-
);
|
|
206
|
-
return status;
|
|
207
191
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (!this.streamTypeLive) {
|
|
216
|
-
status = await this._initSwitching(
|
|
217
|
-
SwitcherState.V2L_TO_LIVE,
|
|
218
|
-
session,
|
|
219
|
-
sessionLive,
|
|
220
|
-
scheduleObj
|
|
221
|
-
);
|
|
222
|
-
return status;
|
|
192
|
+
if (tsNow >= scheduleObj.start_time && tsNow < scheduleObj.end_time && scheduleObj.end_time - tsNow > 10000) {
|
|
193
|
+
if (scheduleObj.type === StreamType.LIVE) {
|
|
194
|
+
if (!this.streamTypeLive) {
|
|
195
|
+
status = await this._initSwitching(SwitcherState.V2L_TO_LIVE, session, sessionLive, scheduleObj);
|
|
196
|
+
return status;
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
223
199
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
200
|
+
if (!this.streamTypeLive) {
|
|
201
|
+
if (!scheduleObj.duration) {
|
|
202
|
+
debug(`[${this.sessionId}]: Cannot switch VOD no duration specified for schedule item: [${scheduleObj.assetId}]`);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
if (this.eventId !== scheduleObj.eventId) {
|
|
206
|
+
status = await this._initSwitching(SwitcherState.V2L_TO_VOD, session, sessionLive, scheduleObj);
|
|
207
|
+
return status;
|
|
208
|
+
}
|
|
231
209
|
return false;
|
|
232
210
|
}
|
|
233
|
-
if (this.eventId !== scheduleObj.eventId) {
|
|
234
|
-
status = await this._initSwitching(
|
|
235
|
-
SwitcherState.V2L_TO_VOD,
|
|
236
|
-
session,
|
|
237
|
-
sessionLive,
|
|
238
|
-
scheduleObj
|
|
239
|
-
);
|
|
240
|
-
return status;
|
|
241
|
-
}
|
|
242
|
-
return false;
|
|
243
211
|
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
debug(`[${this.sessionId}]: Unexpected failure in Stream Switcher...`);
|
|
214
|
+
console.error(err);
|
|
215
|
+
throw new Error(err);
|
|
244
216
|
}
|
|
245
217
|
}
|
|
246
218
|
|
|
@@ -263,13 +235,15 @@ class StreamSwitcher {
|
|
|
263
235
|
currVodCounts = await session.getCurrentMediaAndDiscSequenceCount();
|
|
264
236
|
currVodSegments = await session.getCurrentMediaSequenceSegments();
|
|
265
237
|
|
|
238
|
+
// Insert preroll if available for current channel
|
|
239
|
+
if (this.prerollsCache[this.sessionId]) {
|
|
240
|
+
const prerollSegments = this.prerollsCache[this.sessionId].segments;
|
|
241
|
+
currVodSegments = this._mergeSegments(prerollSegments, currVodSegments, false);
|
|
242
|
+
}
|
|
266
243
|
// In risk that the SL-playhead might have updated some data after
|
|
267
244
|
// we reset last time... we should Reset SessionLive before sending new data.
|
|
268
245
|
await sessionLive.resetLiveStoreAsync(0);
|
|
269
|
-
await sessionLive.setCurrentMediaAndDiscSequenceCount(
|
|
270
|
-
currVodCounts.mediaSeq,
|
|
271
|
-
currVodCounts.discSeq
|
|
272
|
-
);
|
|
246
|
+
await sessionLive.setCurrentMediaAndDiscSequenceCount(currVodCounts.mediaSeq, currVodCounts.discSeq);
|
|
273
247
|
await sessionLive.setCurrentMediaSequenceSegments(currVodSegments);
|
|
274
248
|
liveUri = await sessionLive.setLiveUri(scheduleObj.uri);
|
|
275
249
|
|
|
@@ -289,6 +263,7 @@ class StreamSwitcher {
|
|
|
289
263
|
this.working = false;
|
|
290
264
|
this.eventId = null;
|
|
291
265
|
debug(`[${this.sessionId}]: [ ERROR Switching from V2L->LIVE ]`);
|
|
266
|
+
console.error(err);
|
|
292
267
|
throw new Error(err);
|
|
293
268
|
}
|
|
294
269
|
|
|
@@ -297,10 +272,7 @@ class StreamSwitcher {
|
|
|
297
272
|
debug(`[${this.sessionId}]: [ INIT Switching from V2L->VOD ]`);
|
|
298
273
|
this.eventId = scheduleObj.eventId;
|
|
299
274
|
currVodCounts = await session.getCurrentMediaAndDiscSequenceCount();
|
|
300
|
-
eventSegments = await session.getTruncatedVodSegments(
|
|
301
|
-
scheduleObj.uri,
|
|
302
|
-
scheduleObj.duration / 1000
|
|
303
|
-
);
|
|
275
|
+
eventSegments = await session.getTruncatedVodSegments(scheduleObj.uri, scheduleObj.duration / 1000);
|
|
304
276
|
|
|
305
277
|
if (!eventSegments) {
|
|
306
278
|
debug(`[${this.sessionId}]: [ ERROR Switching from V2L->VOD ]`);
|
|
@@ -309,10 +281,13 @@ class StreamSwitcher {
|
|
|
309
281
|
return false;
|
|
310
282
|
}
|
|
311
283
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
284
|
+
// Insert preroll if available for current channel
|
|
285
|
+
if (this.prerollsCache[this.sessionId]) {
|
|
286
|
+
const prerollSegments = this.prerollsCache[this.sessionId].segments;
|
|
287
|
+
eventSegments = this._mergeSegments(prerollSegments, eventSegments, true);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
await session.setCurrentMediaAndDiscSequenceCount(currVodCounts.mediaSeq, currVodCounts.discSeq);
|
|
316
291
|
await session.setCurrentMediaSequenceSegments(eventSegments, 0, true);
|
|
317
292
|
|
|
318
293
|
this.working = false;
|
|
@@ -336,9 +311,7 @@ class StreamSwitcher {
|
|
|
336
311
|
sessionLive.resetLiveStoreAsync(RESET_DELAY); // In parallel
|
|
337
312
|
|
|
338
313
|
if (scheduleObj && !scheduleObj.duration) {
|
|
339
|
-
debug(
|
|
340
|
-
`[${this.sessionId}]: Cannot switch VOD. No duration specified for schedule item: [${scheduleObj.assetId}]`
|
|
341
|
-
);
|
|
314
|
+
debug(`[${this.sessionId}]: Cannot switch VOD. No duration specified for schedule item: [${scheduleObj.assetId}]`);
|
|
342
315
|
}
|
|
343
316
|
|
|
344
317
|
if (this._isEmpty(liveSegments.currMseqSegs)) {
|
|
@@ -348,14 +321,15 @@ class StreamSwitcher {
|
|
|
348
321
|
return false;
|
|
349
322
|
}
|
|
350
323
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
);
|
|
324
|
+
// Insert preroll, if available, for current channel
|
|
325
|
+
if (this.prerollsCache[this.sessionId]) {
|
|
326
|
+
const prerollSegments = this.prerollsCache[this.sessionId].segments;
|
|
327
|
+
liveSegments.currMseqSegs = this._mergeSegments(prerollSegments, liveSegments.currMseqSegs, false);
|
|
328
|
+
liveSegments.segCount += prerollSegments.length;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq, liveCounts.discSeq);
|
|
332
|
+
await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount);
|
|
359
333
|
|
|
360
334
|
this.working = false;
|
|
361
335
|
this.streamTypeLive = false;
|
|
@@ -378,10 +352,7 @@ class StreamSwitcher {
|
|
|
378
352
|
await sessionLive.resetSession();
|
|
379
353
|
sessionLive.resetLiveStoreAsync(RESET_DELAY); // In parallel
|
|
380
354
|
|
|
381
|
-
eventSegments = await session.getTruncatedVodSegments(
|
|
382
|
-
scheduleObj.uri,
|
|
383
|
-
scheduleObj.duration / 1000
|
|
384
|
-
);
|
|
355
|
+
eventSegments = await session.getTruncatedVodSegments(scheduleObj.uri, scheduleObj.duration / 1000);
|
|
385
356
|
if (!eventSegments) {
|
|
386
357
|
debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->VOD ]`);
|
|
387
358
|
this.streamTypeLive = false;
|
|
@@ -390,14 +361,15 @@ class StreamSwitcher {
|
|
|
390
361
|
return false;
|
|
391
362
|
}
|
|
392
363
|
|
|
393
|
-
await session.setCurrentMediaAndDiscSequenceCount(
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
364
|
+
await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq - 1, liveCounts.discSeq - 1);
|
|
365
|
+
await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount);
|
|
366
|
+
|
|
367
|
+
// Insert preroll, if available, for current channel
|
|
368
|
+
if (this.prerollsCache[this.sessionId]) {
|
|
369
|
+
const prerollSegments = this.prerollsCache[this.sessionId].segments;
|
|
370
|
+
eventSegments = this._mergeSegments(prerollSegments, eventSegments, true);
|
|
371
|
+
}
|
|
372
|
+
|
|
401
373
|
await session.setCurrentMediaSequenceSegments(eventSegments, 0, true);
|
|
402
374
|
|
|
403
375
|
this.working = false;
|
|
@@ -417,19 +389,19 @@ class StreamSwitcher {
|
|
|
417
389
|
// TODO: Not yet fully tested/supported
|
|
418
390
|
this.eventId = scheduleObj.eventId;
|
|
419
391
|
eventSegments = await sessionLive.getCurrentMediaSequenceSegments();
|
|
420
|
-
currLiveCounts =
|
|
421
|
-
await sessionLive.getCurrentMediaAndDiscSequenceCount();
|
|
392
|
+
currLiveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount();
|
|
422
393
|
|
|
423
394
|
await sessionLive.resetSession();
|
|
424
395
|
await sessionLive.resetLiveStoreAsync(0);
|
|
425
396
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
);
|
|
397
|
+
// Insert preroll, if available, for current channel
|
|
398
|
+
if (this.prerollsCache[this.sessionId]) {
|
|
399
|
+
const prerollSegments = this.prerollsCache[this.sessionId].segments;
|
|
400
|
+
eventSegments = this._mergeSegments(prerollSegments, eventSegments, false);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await sessionLive.setCurrentMediaAndDiscSequenceCount(currLiveCounts.mediaSeq, currLiveCounts.discSeq);
|
|
404
|
+
await sessionLive.setCurrentMediaSequenceSegments(eventSegments.currMseqSegs);
|
|
433
405
|
liveUri = await sessionLive.setLiveUri(scheduleObj.uri);
|
|
434
406
|
|
|
435
407
|
if (!liveUri) {
|
|
@@ -483,19 +455,187 @@ class StreamSwitcher {
|
|
|
483
455
|
if (online.status >= 200 && online.status < 300) {
|
|
484
456
|
return true;
|
|
485
457
|
}
|
|
486
|
-
debug(
|
|
487
|
-
`[${this.sessionId}]: Failed to validate URI: ${uri}\nERROR! Returned Status Code: ${online.status}`
|
|
488
|
-
);
|
|
458
|
+
debug(`[${this.sessionId}]: Failed to validate URI: ${uri}\nERROR! Returned Status Code: ${online.status}`);
|
|
489
459
|
return false;
|
|
490
460
|
} catch (err) {
|
|
491
|
-
debug(
|
|
492
|
-
`[${this.sessionId}]: Failed to validate URI: ${uri}\nERROR! ${err}`
|
|
493
|
-
);
|
|
461
|
+
debug(`[${this.sessionId}]: Failed to validate URI: ${uri}\nERROR: ${err}`);
|
|
494
462
|
return false;
|
|
495
463
|
} finally {
|
|
496
464
|
clearTimeout(timeout);
|
|
497
465
|
}
|
|
498
466
|
}
|
|
467
|
+
|
|
468
|
+
async _loadPreroll(uri) {
|
|
469
|
+
const prerollSegments = {};
|
|
470
|
+
const mediaM3UPlaylists = {};
|
|
471
|
+
const mediaURIs = {};
|
|
472
|
+
try {
|
|
473
|
+
const m3u = await this._fetchParseM3u8(uri);
|
|
474
|
+
debug(`[${this.sessionId}]: ...Fetched a New Preroll Slate Master Manifest from:\n${uri}`);
|
|
475
|
+
// Is the first URI an actual Multivariant manifest
|
|
476
|
+
if (m3u.items.StreamItem.length > 0) {
|
|
477
|
+
// Process Master M3U. Collect Media URIs
|
|
478
|
+
for (let i = 0; i < m3u.items.StreamItem.length; i++) {
|
|
479
|
+
const streamItem = m3u.items.StreamItem[i];
|
|
480
|
+
const bw = streamItem.get("bandwidth");
|
|
481
|
+
const mediaUri = streamItem.get("uri");
|
|
482
|
+
if (mediaUri.match("^http")) {
|
|
483
|
+
mediaURIs[bw] = mediaUri;
|
|
484
|
+
} else {
|
|
485
|
+
mediaURIs[bw] = new URL(mediaUri, uri);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Fetch and parse Media URIs
|
|
490
|
+
const bandwidths = Object.keys(mediaURIs);
|
|
491
|
+
const loadMediaPromises = [];
|
|
492
|
+
// Queue up...
|
|
493
|
+
bandwidths.forEach((bw) => loadMediaPromises.push(this._fetchParseM3u8(mediaURIs[bw])));
|
|
494
|
+
// Execute...
|
|
495
|
+
const results = await Promise.allSettled(loadMediaPromises);
|
|
496
|
+
// Process...
|
|
497
|
+
results.forEach((item, idx) => {
|
|
498
|
+
if (item.status === "fulfilled" && item.value) {
|
|
499
|
+
const resultM3U = item.value;
|
|
500
|
+
const bw = bandwidths[idx];
|
|
501
|
+
mediaM3UPlaylists[bw] = resultM3U.items.PlaylistItem;
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
} else if (m3u.items.PlaylistItem.length > 0) {
|
|
505
|
+
// Process the Media M3U.
|
|
506
|
+
const arbitraryBw = 1;
|
|
507
|
+
mediaURIs[arbitraryBw] = uri;
|
|
508
|
+
mediaM3UPlaylists[arbitraryBw] = m3u.items.PlaylistItem;
|
|
509
|
+
} else {
|
|
510
|
+
debug(`[${this.sessionId}]: WARNING! M3U has no variants nor playlist segments!`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Turn original m3u playlist items into custom-simple segment items list
|
|
514
|
+
const bandwidths = Object.keys(mediaM3UPlaylists);
|
|
515
|
+
for (let i = 0; i < bandwidths.length; i++) {
|
|
516
|
+
const bw = bandwidths[i];
|
|
517
|
+
if (!prerollSegments[bw]) {
|
|
518
|
+
prerollSegments[bw] = [];
|
|
519
|
+
}
|
|
520
|
+
for (let k = 0; k < mediaM3UPlaylists[bw].length; k++) {
|
|
521
|
+
let seg = {};
|
|
522
|
+
let playlistItem = mediaM3UPlaylists[bw][k];
|
|
523
|
+
let segmentUri;
|
|
524
|
+
let cueData = null;
|
|
525
|
+
let daterangeData = null;
|
|
526
|
+
let attributes = playlistItem["attributes"].attributes;
|
|
527
|
+
if (playlistItem.properties.discontinuity) {
|
|
528
|
+
prerollSegments[bw].push({ discontinuity: true });
|
|
529
|
+
}
|
|
530
|
+
if ("cuein" in attributes) {
|
|
531
|
+
if (!cueData) {
|
|
532
|
+
cueData = {};
|
|
533
|
+
}
|
|
534
|
+
cueData["in"] = true;
|
|
535
|
+
}
|
|
536
|
+
if ("cueout" in attributes) {
|
|
537
|
+
if (!cueData) {
|
|
538
|
+
cueData = {};
|
|
539
|
+
}
|
|
540
|
+
cueData["out"] = true;
|
|
541
|
+
cueData["duration"] = attributes["cueout"];
|
|
542
|
+
}
|
|
543
|
+
if ("cuecont" in attributes) {
|
|
544
|
+
if (!cueData) {
|
|
545
|
+
cueData = {};
|
|
546
|
+
}
|
|
547
|
+
cueData["cont"] = true;
|
|
548
|
+
}
|
|
549
|
+
if ("scteData" in attributes) {
|
|
550
|
+
if (!cueData) {
|
|
551
|
+
cueData = {};
|
|
552
|
+
}
|
|
553
|
+
cueData["scteData"] = attributes["scteData"];
|
|
554
|
+
}
|
|
555
|
+
if ("assetData" in attributes) {
|
|
556
|
+
if (!cueData) {
|
|
557
|
+
cueData = {};
|
|
558
|
+
}
|
|
559
|
+
cueData["assetData"] = attributes["assetData"];
|
|
560
|
+
}
|
|
561
|
+
if ("daterange" in attributes) {
|
|
562
|
+
if (!daterangeData) {
|
|
563
|
+
daterangeData = {};
|
|
564
|
+
}
|
|
565
|
+
let allDaterangeAttributes = Object.keys(attributes["daterange"]);
|
|
566
|
+
allDaterangeAttributes.forEach((attr) => {
|
|
567
|
+
if (attr.match(/DURATION$/)) {
|
|
568
|
+
daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]);
|
|
569
|
+
} else {
|
|
570
|
+
daterangeData[attr.toLowerCase()] = attributes["daterange"][attr];
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
if (playlistItem.properties.uri) {
|
|
575
|
+
if (playlistItem.properties.uri.match("^http")) {
|
|
576
|
+
segmentUri = playlistItem.properties.uri;
|
|
577
|
+
} else {
|
|
578
|
+
segmentUri = new URL(playlistItem.properties.uri, mediaURIs[bw]);
|
|
579
|
+
}
|
|
580
|
+
seg["duration"] = playlistItem.properties.duration;
|
|
581
|
+
seg["uri"] = segmentUri;
|
|
582
|
+
seg["cue"] = cueData;
|
|
583
|
+
if (daterangeData) {
|
|
584
|
+
seg["daterange"] = daterangeData;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
prerollSegments[bw].push(seg);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
debug(`[${this.sessionId}]: Loaded all Variants of the Preroll Slate!`);
|
|
591
|
+
return prerollSegments;
|
|
592
|
+
} catch (err) {
|
|
593
|
+
throw new Error(err);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Input: hls vod uri. Output: an M3U object.
|
|
598
|
+
async _fetchParseM3u8(uri) {
|
|
599
|
+
const parser = m3u8.createStream();
|
|
600
|
+
try {
|
|
601
|
+
const res = await fetchWithRetry(uri, {}, 5, 1000, 1500, debug);
|
|
602
|
+
res.body.pipe(parser);
|
|
603
|
+
return new Promise((resolve, reject) => {
|
|
604
|
+
parser.on("m3u", (m3u) => {
|
|
605
|
+
resolve(m3u);
|
|
606
|
+
parser.on("error", (exc) => {
|
|
607
|
+
debug(`Parser Error: ${JSON.stringify(exc)}`);
|
|
608
|
+
reject(exc);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
} catch (err) {
|
|
613
|
+
return Promise.reject(`[${this.sessionId}]: Failed to Fetch URI: ${uri}\nERROR, ${err}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
_mergeSegments(fromSegments, toSegments, prepend) {
|
|
618
|
+
const OUTPUT_SEGMENTS = {};
|
|
619
|
+
const fromBws = Object.keys(fromSegments);
|
|
620
|
+
const toBws = Object.keys(toSegments);
|
|
621
|
+
toBws.forEach((bw) => {
|
|
622
|
+
const targetBw = findNearestValue(bw, fromBws);
|
|
623
|
+
if (prepend) {
|
|
624
|
+
OUTPUT_SEGMENTS[bw] = fromSegments[targetBw].concat(toSegments[bw]);
|
|
625
|
+
OUTPUT_SEGMENTS[bw].unshift({ discontinuity: true });
|
|
626
|
+
} else {
|
|
627
|
+
const lastSeg = toSegments[bw][toSegments[bw].length - 1];
|
|
628
|
+
if (lastSeg.uri && !lastSeg.discontinuity) {
|
|
629
|
+
toSegments[bw].push({ discontinuity: true });
|
|
630
|
+
OUTPUT_SEGMENTS[bw] = toSegments[bw].concat(fromSegments[targetBw]);
|
|
631
|
+
} else {
|
|
632
|
+
OUTPUT_SEGMENTS[bw] = toSegments[bw].concat(fromSegments[targetBw]);
|
|
633
|
+
OUTPUT_SEGMENTS[bw].push({ discontinuity: true });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
return OUTPUT_SEGMENTS;
|
|
638
|
+
}
|
|
499
639
|
}
|
|
500
640
|
|
|
501
641
|
module.exports = StreamSwitcher;
|
package/engine/util.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { version } = require("../package.json");
|
|
2
|
+
const fetch = require("node-fetch");
|
|
2
3
|
|
|
3
4
|
const filterQueryParser = (filterQuery) => {
|
|
4
5
|
const conditions = filterQuery.match(/\(([^\(\)]*?)\)/g);
|
|
@@ -122,7 +123,95 @@ const logerror = (sessionId, err) => {
|
|
|
122
123
|
console.error(err);
|
|
123
124
|
};
|
|
124
125
|
|
|
125
|
-
const timer = ms => new Promise(res => setTimeout(res, ms));
|
|
126
|
+
const timer = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
127
|
+
|
|
128
|
+
class WaitTimeGenerator {
|
|
129
|
+
constructor(defaultIntervalMs, minValue) {
|
|
130
|
+
(this.timestamp = null), (this.prevWaitTime = null), (this.defaultIntervalMs = defaultIntervalMs || 3000), (this.minValue = minValue);
|
|
131
|
+
}
|
|
132
|
+
_getWaitTimeFromTimestamp() {
|
|
133
|
+
if (!this.timestamp) {
|
|
134
|
+
this.timestamp = new Date();
|
|
135
|
+
}
|
|
136
|
+
const sec = this.timestamp.getSeconds();
|
|
137
|
+
const defaultSec = this.defaultIntervalMs / 1000;
|
|
138
|
+
const d = parseInt(sec / defaultSec);
|
|
139
|
+
const waitSec = defaultSec * (d + 1) - sec;
|
|
140
|
+
return waitSec * 1000;
|
|
141
|
+
}
|
|
142
|
+
async getWaitTime(plannedTime) {
|
|
143
|
+
if (!this.timestamp || (this.prevWaitTime === this.minValue && plannedTime !== this.minValue)) {
|
|
144
|
+
this.timestamp = new Date();
|
|
145
|
+
const waitMs = this._getWaitTimeFromTimestamp();
|
|
146
|
+
this.prevWaitTime = waitMs;
|
|
147
|
+
return waitMs;
|
|
148
|
+
}
|
|
149
|
+
this.prevWaitTime = plannedTime;
|
|
150
|
+
return plannedTime;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const findNearestValue = (val, array) => {
|
|
155
|
+
const sorted = array.sort((a, b) => b - a);
|
|
156
|
+
return sorted.reduce((a, b) => {
|
|
157
|
+
return Math.abs(b - val) < Math.abs(a - val) ? b : a;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const isValidUrl = (url) => {
|
|
162
|
+
try {
|
|
163
|
+
const _url = new URL(url);
|
|
164
|
+
return true;
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
const fetchWithRetry = async (uri, opts, maxRetries, retryDelayMs, timeoutMs, debug) => {
|
|
172
|
+
if (!debug) {
|
|
173
|
+
var debug = (msg) => { console.log(msg); };
|
|
174
|
+
}
|
|
175
|
+
let tryFetchCount = 0;
|
|
176
|
+
const RETRY_LIMIT = maxRetries || 5;
|
|
177
|
+
const TIMEOUT_LIMIT = timeoutMs || 10 * 1000;
|
|
178
|
+
const RETRY_DELAY = retryDelayMs || 1000;
|
|
179
|
+
while (tryFetchCount < RETRY_LIMIT) {
|
|
180
|
+
tryFetchCount++;
|
|
181
|
+
debug(`Fetching URI -> ${uri}, attempt ${tryFetchCount} of ${maxRetries}`);
|
|
182
|
+
//
|
|
183
|
+
const controller = new AbortController();
|
|
184
|
+
const timeout = setTimeout(() => {
|
|
185
|
+
`Request Timeout after (${TIMEOUT_LIMIT})ms @ ${uri}`;
|
|
186
|
+
controller.abort();
|
|
187
|
+
}, TIMEOUT_LIMIT);
|
|
188
|
+
try {
|
|
189
|
+
const fetchOpts = Object.assign({ signal: controller.signal }, opts);
|
|
190
|
+
const response = await fetch(uri, fetchOpts);
|
|
191
|
+
|
|
192
|
+
if (response.status >= 400 && response.status < 600) {
|
|
193
|
+
const msg = `Bad response from URI: ${uri}\nReturned Status Code: ${response.status}`;
|
|
194
|
+
debug(msg);
|
|
195
|
+
if (tryFetchCount === maxRetries) {
|
|
196
|
+
return Promise.resolve(response);
|
|
197
|
+
}
|
|
198
|
+
debug(`Going to try fetch again in ${RETRY_DELAY}ms`);
|
|
199
|
+
await timer(RETRY_DELAY);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
// Return Good response
|
|
203
|
+
return Promise.resolve(response);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
debug(`Node-Fetch Error on URI: ${uri}\nFull Error -> ${err}`);
|
|
206
|
+
if (tryFetchCount === maxRetries) {
|
|
207
|
+
return Promise.reject(err);
|
|
208
|
+
}
|
|
209
|
+
continue;
|
|
210
|
+
} finally {
|
|
211
|
+
clearTimeout(timeout);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
126
215
|
|
|
127
216
|
module.exports = {
|
|
128
217
|
filterQueryParser,
|
|
@@ -131,5 +220,9 @@ module.exports = {
|
|
|
131
220
|
m3u8Header,
|
|
132
221
|
toHHMMSS,
|
|
133
222
|
logerror,
|
|
134
|
-
timer
|
|
223
|
+
timer,
|
|
224
|
+
WaitTimeGenerator,
|
|
225
|
+
findNearestValue,
|
|
226
|
+
isValidUrl,
|
|
227
|
+
fetchWithRetry
|
|
135
228
|
};
|
package/package.json
CHANGED
package/server-livemix.js
CHANGED
|
@@ -136,6 +136,11 @@ class StreamSwitchManager {
|
|
|
136
136
|
return uuidv4();
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
getPrerollUri(channelId) {
|
|
140
|
+
const defaultPrerollSlateUri = "https://maitv-vod.lab.eyevinn.technology/slate-consuo.mp4/master.m3u8"
|
|
141
|
+
return new Promise((resolve, reject) => { resolve(defaultPrerollSlateUri); });
|
|
142
|
+
}
|
|
143
|
+
|
|
139
144
|
getSchedule(channelId) {
|
|
140
145
|
return new Promise((resolve, reject) => {
|
|
141
146
|
if (this.schedule[channelId]) {
|
|
@@ -155,7 +160,7 @@ class StreamSwitchManager {
|
|
|
155
160
|
type: StreamType.LIVE,
|
|
156
161
|
start_time: startOffset,
|
|
157
162
|
end_time: endTime,
|
|
158
|
-
uri: "https://d2fz24s2fts31b.cloudfront.net/out/v1/
|
|
163
|
+
uri: "https://d2fz24s2fts31b.cloudfront.net/out/v1/6484d7c664924b77893f9b4f63080e5d/manifest.m3u8",
|
|
159
164
|
},
|
|
160
165
|
{
|
|
161
166
|
eventId: this.generateID(),
|