eyevinn-channel-engine 3.0.0 → 3.0.4
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 +11 -5
- package/engine/server.js +24 -10
- package/engine/session.js +21 -7
- package/engine/session_live.js +117 -131
- package/engine/session_live_state.js +1 -1
- package/engine/stream_switcher.js +234 -121
- package/package.json +5 -4
- package/server-livemix.js +195 -0
- package/server.js +36 -128
- package/spec/engine/init_switching_spec.js +4 -2
package/README.md
CHANGED
|
@@ -18,12 +18,18 @@ To run the basic reference implementation included in this repository you run:
|
|
|
18
18
|
$ npm start
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
To run the basic reference implementation for using demuxed VODs you run:
|
|
22
22
|
|
|
23
23
|
```
|
|
24
24
|
$ npm run start-demux
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
And to run the basic reference implementation for using live-mixing you run:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
$ npm run start-livemix
|
|
31
|
+
```
|
|
32
|
+
|
|
27
33
|
|
|
28
34
|
Then point your HLS video player to `http://localhost:8000/channels/1/master.m3u8` to start playing the linear live stream.
|
|
29
35
|
|
|
@@ -122,8 +128,6 @@ engine.start();
|
|
|
122
128
|
engine.listen(process.env.port || 8000);
|
|
123
129
|
```
|
|
124
130
|
|
|
125
|
-
Note that this feature is currently in beta which means that it is close to production-ready but has not been run in production yet. We appreciate all efforts to try this out and provide feedback.
|
|
126
|
-
|
|
127
131
|
|
|
128
132
|
## Live Mixing (BETA)
|
|
129
133
|
|
|
@@ -137,8 +141,9 @@ class MyStreamSwitchManager {
|
|
|
137
141
|
getSchedule(channelId) -> [ { eventId, assetId, title, type, start_time, end_time, uri, duration } ]
|
|
138
142
|
}
|
|
139
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.
|
|
140
145
|
|
|
141
|
-
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:
|
|
142
147
|
|
|
143
148
|
```
|
|
144
149
|
{
|
|
@@ -204,6 +209,7 @@ Below are examples of a Live stream event and a VOD event respectively:
|
|
|
204
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.*)
|
|
205
210
|
|
|
206
211
|
|
|
212
|
+
|
|
207
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.
|
|
208
214
|
|
|
209
215
|
```
|
|
@@ -244,7 +250,7 @@ If the connection to the Live stream ends or if the current time has passed the
|
|
|
244
250
|
|
|
245
251
|
Note that this feature is also currently in beta which means that it is close to production-ready but has not been run in production yet. We appreciate all efforts to try this out and provide feedback.
|
|
246
252
|
|
|
247
|
-
##
|
|
253
|
+
## Support
|
|
248
254
|
|
|
249
255
|
If you want help to get started to build a service of your own based on this library you can hire an [Eyevinn Video-Dev Team](https://video-dev.team) to help you out.
|
|
250
256
|
|
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) {
|
|
@@ -244,7 +245,7 @@ class Session {
|
|
|
244
245
|
try {
|
|
245
246
|
const hlsVod = await this._truncateSlate(null, duration, vodUri);
|
|
246
247
|
let vodSegments = hlsVod.getMediaSegments();
|
|
247
|
-
Object.keys(vodSegments).forEach(bw => vodSegments[bw].unshift({ discontinuity: true }));
|
|
248
|
+
Object.keys(vodSegments).forEach(bw => vodSegments[bw].unshift({ discontinuity: true, cue: { in: true } }));
|
|
248
249
|
return vodSegments;
|
|
249
250
|
} catch (exc) {
|
|
250
251
|
debug(`[${this._sessionId}]: Failed to generate truncated VOD!`);
|
|
@@ -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const debug = require("debug")("engine-
|
|
1
|
+
const debug = require("debug")("engine-session-live");
|
|
2
2
|
const allSettled = require("promise.allsettled");
|
|
3
3
|
const crypto = require("crypto");
|
|
4
4
|
const m3u8 = require("@eyevinn/m3u8");
|
|
@@ -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;
|
|
@@ -254,7 +254,9 @@ class SessionLive {
|
|
|
254
254
|
this.vodSegments[bw].push(segments[bw][segIdx]);
|
|
255
255
|
}
|
|
256
256
|
if (!segments[bw][segments[bw].length - 1].discontinuity) {
|
|
257
|
-
this.vodSegments[bw].push({ discontinuity: true });
|
|
257
|
+
this.vodSegments[bw].push({ discontinuity: true, cue: { in: true } });
|
|
258
|
+
} else {
|
|
259
|
+
segments[bw][segments[bw].length - 1]["cue"] = { in: true };
|
|
258
260
|
}
|
|
259
261
|
}
|
|
260
262
|
|
|
@@ -287,6 +289,13 @@ class SessionLive {
|
|
|
287
289
|
// Don't use what Session gave you. Use the Leaders number if it's available
|
|
288
290
|
const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId);
|
|
289
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
|
+
}
|
|
290
299
|
if (isLeader) {
|
|
291
300
|
liveCounts.discSeqCount = this.discSeqCount;
|
|
292
301
|
await this.sessionLiveState.set("firstCounts", liveCounts);
|
|
@@ -345,7 +354,7 @@ class SessionLive {
|
|
|
345
354
|
currentMediaSequenceSegments[liveTargetBandwidth] = [];
|
|
346
355
|
// In case we switch back before we've depleted all transitional segments
|
|
347
356
|
currentMediaSequenceSegments[liveTargetBandwidth] = this.vodSegments[vodTargetBandwidth].concat(this.liveSegQueue[liveTargetBandwidth]);
|
|
348
|
-
currentMediaSequenceSegments[liveTargetBandwidth].push({ discontinuity: true });
|
|
357
|
+
currentMediaSequenceSegments[liveTargetBandwidth].push({ discontinuity: true, cue: { in: true } });
|
|
349
358
|
debug(`[${this.sessionId}]: Getting current media segments for bw=${bw}`);
|
|
350
359
|
}
|
|
351
360
|
|
|
@@ -477,33 +486,31 @@ class SessionLive {
|
|
|
477
486
|
const vodBws = Object.keys(this.vodSegments);
|
|
478
487
|
const liveBws = Object.keys(this.liveSegsForFollowers);
|
|
479
488
|
const size = this.liveSegsForFollowers[liveBws[0]].length;
|
|
480
|
-
|
|
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} `);
|
|
481
494
|
// Remove transitional segs & add live source segs collected from store
|
|
482
|
-
for (let k = 0; k <
|
|
495
|
+
for (let k = 0; k < newSegCount; k++) {
|
|
483
496
|
let incrementDiscSeqCount = false;
|
|
484
497
|
// Shift the top vod segment on all variants
|
|
485
498
|
for (let i = 0; i < vodBws.length; i++) {
|
|
486
499
|
let seg = this.vodSegments[vodBws[i]].shift();
|
|
487
|
-
if (seg && seg.discontinuity
|
|
488
|
-
|
|
489
|
-
incrementDiscSeqCount = true;
|
|
490
|
-
}
|
|
491
|
-
seg = this.vodSegments[vodBws[i]].shift();
|
|
492
|
-
}
|
|
493
|
-
if (seg && seg.discontinuity || seg && seg.cue) {
|
|
494
|
-
if (seg.discontinuity) {
|
|
495
|
-
incrementDiscSeqCount = true;
|
|
496
|
-
}
|
|
500
|
+
if (seg && seg.discontinuity) {
|
|
501
|
+
incrementDiscSeqCount = true;
|
|
497
502
|
this.vodSegments[vodBws[i]].shift();
|
|
498
503
|
}
|
|
499
504
|
}
|
|
500
505
|
if (incrementDiscSeqCount) {
|
|
501
|
-
this.
|
|
506
|
+
this.discSeqCount++;
|
|
502
507
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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];
|
|
507
514
|
if (!this.liveSegQueue[bw]) {
|
|
508
515
|
this.liveSegQueue[bw] = [];
|
|
509
516
|
}
|
|
@@ -512,27 +519,21 @@ class SessionLive {
|
|
|
512
519
|
let segCount = 0;
|
|
513
520
|
this.liveSegQueue[bw].map((seg) => {if (seg.uri) { segCount++ }});
|
|
514
521
|
if (segCount > this.targetNumSeg) {
|
|
515
|
-
seg = this.liveSegQueue[bw].shift();
|
|
516
|
-
if (seg && seg.discontinuity
|
|
517
|
-
|
|
518
|
-
incrementDiscSeqCount = true;
|
|
519
|
-
}
|
|
522
|
+
let seg = this.liveSegQueue[bw].shift();
|
|
523
|
+
if (seg && seg.discontinuity) {
|
|
524
|
+
incrementDiscSeqCount = true;
|
|
520
525
|
seg = this.liveSegQueue[bw].shift();
|
|
521
526
|
}
|
|
522
|
-
if (seg && seg.discontinuity || seg && seg.cue) {
|
|
523
|
-
if (seg.discontinuity) {
|
|
524
|
-
incrementDiscSeqCount = true;
|
|
525
|
-
}
|
|
526
|
-
this.liveSegQueue[bw].shift();
|
|
527
|
-
}
|
|
528
527
|
}
|
|
529
|
-
debug(`[${this.sessionId}]: Pushed
|
|
530
|
-
}
|
|
528
|
+
debug(`[${this.sessionId}]: Pushed segment (${liveSegFromLeader.uri ? liveSegFromLeader.uri : "Disc-tag"}) to 'liveSegQueue'`);
|
|
529
|
+
});
|
|
531
530
|
if (incrementDiscSeqCount) {
|
|
532
531
|
this.discSeqCount++;
|
|
533
532
|
}
|
|
534
|
-
this.mediaSeqCount++;
|
|
535
533
|
}
|
|
534
|
+
let prevVal = this.mediaSeqCount;
|
|
535
|
+
this.mediaSeqCount += newSegCount;
|
|
536
|
+
debug(`[${this.sessionId}]: Incrementing Mseq Count from [${prevVal}] -> [${this.mediaSeqCount}]`);
|
|
536
537
|
debug(`[${this.sessionId}]: Finished updating all Follower's Counts and Segment Queues!`);
|
|
537
538
|
}
|
|
538
539
|
|
|
@@ -792,13 +793,22 @@ class SessionLive {
|
|
|
792
793
|
let pushPromises = [];
|
|
793
794
|
for (let i = 0; i < Object.keys(this.mediaManifestURIs).length; i++) {
|
|
794
795
|
let bw = Object.keys(this.mediaManifestURIs)[i];
|
|
795
|
-
pushPromises.push(this._parseMediaManifest(this.liveSourceM3Us[bw].M3U,
|
|
796
|
-
|
|
796
|
+
pushPromises.push(this._parseMediaManifest(this.liveSourceM3Us[bw].M3U, this.mediaManifestURIs[bw], bw));
|
|
797
|
+
debug(`[${this.sessionId}]: Pushed pushPromise for bw=${bw}`);
|
|
797
798
|
}
|
|
798
|
-
|
|
799
799
|
// Segment Pushing
|
|
800
800
|
debug(`[${this.sessionId}]: Executing Promises II: Segment Pushing`);
|
|
801
|
-
await Promise.all(pushPromises);
|
|
801
|
+
let results = await allSettled(pushPromises);//await Promise.all(pushPromises);
|
|
802
|
+
const removedDiscSeqList = results.map((item) => {
|
|
803
|
+
if (item.status === "rejected") {
|
|
804
|
+
return -1;
|
|
805
|
+
}
|
|
806
|
+
return item.value.removedDiscSeqs;
|
|
807
|
+
});
|
|
808
|
+
if (removedDiscSeqList.every((val, i, arr) => val === arr[0])) {
|
|
809
|
+
// if all variants removed equal amounts of disc-tags
|
|
810
|
+
this.discSeqCount += removedDiscSeqList[0];
|
|
811
|
+
}
|
|
802
812
|
// UPDATE COUNTS, & Shift Segments in vodSegments
|
|
803
813
|
await this._incrementAndShift();
|
|
804
814
|
}
|
|
@@ -814,7 +824,7 @@ class SessionLive {
|
|
|
814
824
|
}
|
|
815
825
|
|
|
816
826
|
// [LASTLY]: LEADER does this for respawned-FOLLOWERS' sake.
|
|
817
|
-
if (this.firstTime) {
|
|
827
|
+
if (this.firstTime && this.allowedToSet) {
|
|
818
828
|
// Buy some time for followers (NOT Respawned) to fetch their own L.S m3u8.
|
|
819
829
|
await timer(1000); // maybe remove
|
|
820
830
|
const firstCounts = {
|
|
@@ -843,17 +853,11 @@ class SessionLive {
|
|
|
843
853
|
// Shift the top vod segment
|
|
844
854
|
for (let i = 0; i < vodBandwidths.length; i++) {
|
|
845
855
|
let seg = this.vodSegments[vodBandwidths[i]].shift();
|
|
846
|
-
if (seg
|
|
847
|
-
if (seg.discontinuity) {
|
|
848
|
-
incrementDiscSeqCount = true;
|
|
849
|
-
}
|
|
850
|
-
seg = this.vodSegments[vodBandwidths[i]].shift();
|
|
851
|
-
}
|
|
852
|
-
if (seg && seg.discontinuity || seg && seg.cue) {
|
|
856
|
+
if (seg) {
|
|
853
857
|
if (seg.discontinuity) {
|
|
858
|
+
this.vodSegments[vodBandwidths[i]].shift();
|
|
854
859
|
incrementDiscSeqCount = true;
|
|
855
860
|
}
|
|
856
|
-
this.vodSegments[vodBandwidths[i]].shift();
|
|
857
861
|
}
|
|
858
862
|
}
|
|
859
863
|
if (incrementDiscSeqCount) {
|
|
@@ -905,8 +909,12 @@ class SessionLive {
|
|
|
905
909
|
return new Promise((resolve, reject) => {
|
|
906
910
|
parser.on("m3u", (m3u) => {
|
|
907
911
|
try {
|
|
908
|
-
|
|
909
|
-
|
|
912
|
+
const resolveObj = {
|
|
913
|
+
M3U: m3u,
|
|
914
|
+
mediaSeq: m3u.get("mediaSequence"),
|
|
915
|
+
};
|
|
916
|
+
this.liveSourceM3Us[liveTargetBandwidth] = resolveObj;
|
|
917
|
+
resolve(resolveObj);
|
|
910
918
|
} catch (exc) {
|
|
911
919
|
debug(`[${this.sessionId}]: Error when parsing latest manifest`);
|
|
912
920
|
reject(exc);
|
|
@@ -919,33 +927,7 @@ class SessionLive {
|
|
|
919
927
|
});
|
|
920
928
|
}
|
|
921
929
|
|
|
922
|
-
|
|
923
|
-
if (m3u === null) {
|
|
924
|
-
throw new Error("No m3u object provided");
|
|
925
|
-
}
|
|
926
|
-
return new Promise(async (resolve, reject) => {
|
|
927
|
-
try {
|
|
928
|
-
const resolveObj = {
|
|
929
|
-
M3U: m3u,
|
|
930
|
-
mediaSeq: m3u.get("mediaSequence"),
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
this.liveSourceM3Us[liveTargetBandwidth] = resolveObj;
|
|
934
|
-
|
|
935
|
-
resolve(resolveObj);
|
|
936
|
-
} catch (exc) {
|
|
937
|
-
console.error("ERROR: " + exc);
|
|
938
|
-
reject({
|
|
939
|
-
message: exc,
|
|
940
|
-
bandwidth: liveTargetBandwidth,
|
|
941
|
-
m3u8: null,
|
|
942
|
-
mediaSeq: -1,
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
_parseMediaManifest(m3u, bw, mediaManifestUri, liveTargetBandwidth) {
|
|
930
|
+
_parseMediaManifest(m3u, mediaManifestUri, liveTargetBandwidth) {
|
|
949
931
|
return new Promise(async (resolve, reject) => {
|
|
950
932
|
try {
|
|
951
933
|
if (!this.liveSegQueue[liveTargetBandwidth]) {
|
|
@@ -954,7 +936,6 @@ class SessionLive {
|
|
|
954
936
|
if (!this.liveSegsForFollowers[liveTargetBandwidth]) {
|
|
955
937
|
this.liveSegsForFollowers[liveTargetBandwidth] = [];
|
|
956
938
|
}
|
|
957
|
-
|
|
958
939
|
let baseUrl = "";
|
|
959
940
|
const m = mediaManifestUri.match(/^(.*)\/.*?$/);
|
|
960
941
|
if (m) {
|
|
@@ -972,20 +953,18 @@ class SessionLive {
|
|
|
972
953
|
|
|
973
954
|
this.lastRequestedMediaSeqRaw = m3u.get("mediaSequence");
|
|
974
955
|
this.targetDuration = m3u.get("targetDuration");
|
|
975
|
-
|
|
976
|
-
|
|
956
|
+
let amountRemovedDiscSeqs = 0;
|
|
957
|
+
let startIdx = m3u.items.PlaylistItem.length - this.pushAmount;
|
|
958
|
+
startIdx = startIdx < 0 ? 0 : startIdx;
|
|
977
959
|
if (mediaManifestUri) {
|
|
978
|
-
// CREATE NEW MANIFEST
|
|
979
|
-
let startIdx;
|
|
980
|
-
startIdx = m3u.items.PlaylistItem.length - this.pushAmount;
|
|
981
|
-
if (startIdx < 0) {
|
|
982
|
-
startIdx = 0;
|
|
983
|
-
}
|
|
984
960
|
// push segments
|
|
985
|
-
this._addLiveSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetBandwidth);
|
|
961
|
+
amountRemovedDiscSeqs = this._addLiveSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetBandwidth);
|
|
986
962
|
}
|
|
987
|
-
|
|
988
|
-
|
|
963
|
+
let resolveObj = {
|
|
964
|
+
removedDiscSeqs: amountRemovedDiscSeqs,
|
|
965
|
+
bw: liveTargetBandwidth
|
|
966
|
+
}
|
|
967
|
+
resolve(resolveObj);
|
|
989
968
|
} catch (exc) {
|
|
990
969
|
console.error("ERROR: " + exc);
|
|
991
970
|
reject(exc);
|
|
@@ -994,53 +973,61 @@ class SessionLive {
|
|
|
994
973
|
}
|
|
995
974
|
|
|
996
975
|
_addLiveSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetBandwidth) {
|
|
997
|
-
let
|
|
976
|
+
let amountRemovedDiscSeqs = 0;
|
|
998
977
|
for (let i = startIdx; i < playlistItems.length; i++) {
|
|
999
978
|
debug(`[${this.sessionId}]: Adding Live Segment(s) to Queue (for bw=${liveTargetBandwidth})`);
|
|
1000
979
|
let seg = {};
|
|
1001
980
|
let playlistItem = playlistItems[i];
|
|
1002
981
|
let segmentUri;
|
|
982
|
+
let cueData = null;
|
|
983
|
+
let daterangeData = null;
|
|
1003
984
|
let attributes = playlistItem["attributes"].attributes;
|
|
1004
|
-
|
|
1005
985
|
if (playlistItem.properties.discontinuity) {
|
|
1006
986
|
this.liveSegQueue[liveTargetBandwidth].push({ discontinuity: true });
|
|
1007
987
|
this.liveSegsForFollowers[liveTargetBandwidth].push({ discontinuity: true });
|
|
1008
988
|
}
|
|
1009
989
|
if ("cuein" in attributes) {
|
|
1010
|
-
|
|
1011
|
-
|
|
990
|
+
if (!cueData) {
|
|
991
|
+
cueData = {};
|
|
992
|
+
}
|
|
993
|
+
cueData["in"] = true;
|
|
1012
994
|
}
|
|
1013
995
|
if ("cueout" in attributes) {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
996
|
+
if (!cueData) {
|
|
997
|
+
cueData = {};
|
|
998
|
+
}
|
|
999
|
+
cueData["out"] = true;
|
|
1000
|
+
cueData["duration"] = attributes["cueout"];
|
|
1001
|
+
}
|
|
1017
1002
|
if ("cuecont" in attributes) {
|
|
1018
|
-
|
|
1019
|
-
|
|
1003
|
+
if (!cueData) {
|
|
1004
|
+
cueData = {};
|
|
1005
|
+
}
|
|
1006
|
+
cueData["cont"] = true;
|
|
1020
1007
|
}
|
|
1021
1008
|
if ("scteData" in attributes) {
|
|
1022
|
-
|
|
1023
|
-
|
|
1009
|
+
if (!cueData) {
|
|
1010
|
+
cueData = {};
|
|
1011
|
+
}
|
|
1012
|
+
cueData["scteData"] = attributes["scteData"];
|
|
1024
1013
|
}
|
|
1025
1014
|
if ("assetData" in attributes) {
|
|
1026
|
-
|
|
1027
|
-
|
|
1015
|
+
if (!cueData) {
|
|
1016
|
+
cueData = {};
|
|
1017
|
+
}
|
|
1018
|
+
cueData["assetData"] = attributes["assetData"];
|
|
1028
1019
|
}
|
|
1029
1020
|
if ("daterange" in attributes) {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
"start-date": attributes["daterange"]["START-DATE"],
|
|
1041
|
-
"planned-duration": parseFloat(attributes["daterange"]["PLANNED-DURATION"]),
|
|
1042
|
-
}
|
|
1043
|
-
});
|
|
1021
|
+
if(!daterangeData) {
|
|
1022
|
+
daterangeData = {}
|
|
1023
|
+
}
|
|
1024
|
+
daterangeData = {
|
|
1025
|
+
id: attributes["daterange"]["ID"],
|
|
1026
|
+
"start-date": attributes["daterange"]["START-DATE"],
|
|
1027
|
+
"planned-duration": parseFloat(
|
|
1028
|
+
attributes["daterange"]["PLANNED-DURATION"]
|
|
1029
|
+
),
|
|
1030
|
+
};
|
|
1044
1031
|
}
|
|
1045
1032
|
if (playlistItem.properties.uri) {
|
|
1046
1033
|
if (playlistItem.properties.uri.match("^http")) {
|
|
@@ -1050,6 +1037,10 @@ class SessionLive {
|
|
|
1050
1037
|
}
|
|
1051
1038
|
seg["duration"] = playlistItem.properties.duration;
|
|
1052
1039
|
seg["uri"] = segmentUri;
|
|
1040
|
+
seg["cue"] = cueData;
|
|
1041
|
+
if (daterangeData) {
|
|
1042
|
+
seg["daterange"] = daterangeData;
|
|
1043
|
+
}
|
|
1053
1044
|
|
|
1054
1045
|
this.liveSegQueue[liveTargetBandwidth].push(seg);
|
|
1055
1046
|
this.liveSegsForFollowers[liveTargetBandwidth].push(seg);
|
|
@@ -1057,25 +1048,19 @@ class SessionLive {
|
|
|
1057
1048
|
this.liveSegQueue[liveTargetBandwidth].map((seg) => {if (seg.uri) { segCount++ }});
|
|
1058
1049
|
debug(`[${this.sessionId}]: size of queue=${segCount}_targetNumseg=${this.targetNumSeg}`);
|
|
1059
1050
|
if (segCount > this.targetNumSeg) {
|
|
1051
|
+
debug(`[${this.sessionId}]: liveTargetBandwidth=${liveTargetBandwidth}_seg=${JSON.stringify(seg,null,2)}`);
|
|
1060
1052
|
seg = this.liveSegQueue[liveTargetBandwidth].shift();
|
|
1061
|
-
if (seg
|
|
1053
|
+
if (seg) {
|
|
1062
1054
|
if (seg.discontinuity) {
|
|
1063
|
-
|
|
1055
|
+
this.liveSegQueue[liveTargetBandwidth].shift();
|
|
1056
|
+
amountRemovedDiscSeqs++;
|
|
1064
1057
|
}
|
|
1065
|
-
seg = this.liveSegQueue[liveTargetBandwidth].shift();
|
|
1066
|
-
}
|
|
1067
|
-
if (seg && seg.discontinuity || seg && seg.cue) {
|
|
1068
|
-
if (seg.discontinuity) {
|
|
1069
|
-
incrementDiscSeqCount = true;
|
|
1070
|
-
}
|
|
1071
|
-
this.liveSegQueue[liveTargetBandwidth].shift();
|
|
1072
1058
|
}
|
|
1059
|
+
debug(`[${this.sessionId}]: After Shifting -> size of queue=${segCount}_targetNumseg=${this.targetNumSeg}`);
|
|
1073
1060
|
}
|
|
1074
1061
|
}
|
|
1075
1062
|
}
|
|
1076
|
-
|
|
1077
|
-
this.discSeqCount++;
|
|
1078
|
-
}
|
|
1063
|
+
return amountRemovedDiscSeqs;
|
|
1079
1064
|
}
|
|
1080
1065
|
|
|
1081
1066
|
/*
|
|
@@ -1168,9 +1153,6 @@ class SessionLive {
|
|
|
1168
1153
|
m3u8 += "#EXT-X-DISCONTINUITY\n";
|
|
1169
1154
|
}
|
|
1170
1155
|
if (seg.cue) {
|
|
1171
|
-
if (seg.cue.in){
|
|
1172
|
-
m3u8 += "#EXT-X-CUE-IN" + "\n";
|
|
1173
|
-
}
|
|
1174
1156
|
if(seg.cue.out) {
|
|
1175
1157
|
if (seg.cue.scteData) {
|
|
1176
1158
|
m3u8 += "#EXT-OATCLS-SCTE35:" + seg.cue.scteData + "\n";
|
|
@@ -1196,6 +1178,10 @@ class SessionLive {
|
|
|
1196
1178
|
}
|
|
1197
1179
|
m3u8 += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n";
|
|
1198
1180
|
}
|
|
1181
|
+
// Mimick logic used in hls-vodtolive
|
|
1182
|
+
if (seg.cue && seg.cue.in){
|
|
1183
|
+
m3u8 += "#EXT-X-CUE-IN" + "\n";
|
|
1184
|
+
}
|
|
1199
1185
|
if (seg.uri) {
|
|
1200
1186
|
m3u8 += "#EXTINF:" + seg.duration.toFixed(3) + ",\n";
|
|
1201
1187
|
m3u8 += seg.uri + "\n";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const SharedStateStore = require('./shared_state_store.js');
|
|
2
|
-
const debug = require("debug")("
|
|
2
|
+
const debug = require("debug")("session-live-state-store");
|
|
3
3
|
|
|
4
4
|
class SharedSessionLiveState {
|
|
5
5
|
constructor(store, sessionId, instanceId, opts) {
|