eyevinn-channel-engine 3.2.1 → 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/session.js +13 -4
- package/engine/stream_switcher.js +336 -196
- package/engine/util.js +66 -0
- package/package.json +1 -1
- package/server-livemix.js +6 -1
package/engine/session.js
CHANGED
|
@@ -289,6 +289,14 @@ class Session {
|
|
|
289
289
|
this.switchDataForSession.transitionSegments = segments;
|
|
290
290
|
this.switchDataForSession.mediaSeqOffset = mSeqOffset;
|
|
291
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
|
+
|
|
292
300
|
let isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
293
301
|
if (!isLeader) {
|
|
294
302
|
debug(`[${this._sessionId}]: FOLLOWER: Invalidate cache to ensure having the correct VOD!`);
|
|
@@ -315,14 +323,14 @@ class Session {
|
|
|
315
323
|
}
|
|
316
324
|
debug(`[${this._sessionId}]: NEW LEADER: Setting state=VOD_RELOAD_INIT`);
|
|
317
325
|
this.isSwitchingBackToV2L = true;
|
|
318
|
-
await this._sessionState.set("state", SessionState.VOD_RELOAD_INIT)
|
|
326
|
+
await this._sessionState.set("state", SessionState.VOD_RELOAD_INIT);
|
|
319
327
|
|
|
320
328
|
} else {
|
|
321
329
|
let vodReloaded = await this._sessionState.get("vodReloaded");
|
|
322
|
-
let attempts =
|
|
330
|
+
let attempts = 12;
|
|
323
331
|
while (!vodReloaded && attempts > 0) {
|
|
324
|
-
debug(`[${this._sessionId}]: LEADER: Waiting (
|
|
325
|
-
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);
|
|
326
334
|
vodReloaded = await this._sessionState.get("vodReloaded");
|
|
327
335
|
attempts--;
|
|
328
336
|
}
|
|
@@ -961,6 +969,7 @@ class Session {
|
|
|
961
969
|
if (!isLeader) {
|
|
962
970
|
debug(`[${this._sessionId}]: not the leader so just waiting for the VOD to be initiated`);
|
|
963
971
|
}
|
|
972
|
+
this.alreadyClearedVodCache = false;
|
|
964
973
|
return;
|
|
965
974
|
case SessionState.VOD_NEXT_INIT:
|
|
966
975
|
try {
|
|
@@ -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);
|
|
@@ -150,6 +151,68 @@ class WaitTimeGenerator {
|
|
|
150
151
|
}
|
|
151
152
|
}
|
|
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
|
+
};
|
|
215
|
+
|
|
153
216
|
module.exports = {
|
|
154
217
|
filterQueryParser,
|
|
155
218
|
applyFilter,
|
|
@@ -159,4 +222,7 @@ module.exports = {
|
|
|
159
222
|
logerror,
|
|
160
223
|
timer,
|
|
161
224
|
WaitTimeGenerator,
|
|
225
|
+
findNearestValue,
|
|
226
|
+
isValidUrl,
|
|
227
|
+
fetchWithRetry
|
|
162
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(),
|