eyevinn-channel-engine 3.1.1 → 3.1.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/README.md +5 -1
- package/engine/session.js +19 -20
- package/engine/session_live.js +37 -50
- package/engine/util.js +73 -26
- package/package.json +1 -1
- package/spec/engine/util_spec.js +68 -3
package/README.md
CHANGED
|
@@ -35,7 +35,11 @@ Then point your HLS video player to `http://localhost:8000/channels/1/master.m3u
|
|
|
35
35
|
|
|
36
36
|
## Master manifest filtering
|
|
37
37
|
|
|
38
|
-
The engine supports a very simplistic and basic filtering of media playlists included in the master manifest. Currently supports to filter on video bandwidth and video height. To specify a filter provide the query parameter `filter` when loading the master manifest, e.g. `(type=="video"
|
|
38
|
+
The engine supports a very simplistic and basic filtering of media playlists included in the master manifest. Currently supports to filter on video bandwidth and video height. To specify a filter provide the query parameter `filter` when loading the master manifest, e.g. `(type=="video"ANDheight>200)AND(type=="video"ANDheight<400)`. This needs to be URL encoded resulting in the following URL: `http://localhost:8000/channels/1/master.m3u8?filter=%28type%3D%3D%22video%22ANDheight%3E200%29AND%28type%3D%3D%22video%22ANDheight%3C400%29`.
|
|
39
|
+
|
|
40
|
+
To filter by video bandwidth use the `systemBitrate` keyword in the query, e.g. `(type=="video"ANDsystemBitrate>2121000)AND(type=="video"ANDsystemBitrate<6161000)`.
|
|
41
|
+
|
|
42
|
+
You can also combine the filter conditions, e.g. `(type=="video"ANDheight>240)AND(type=="video"ANDsystemBitrate<4141000)`
|
|
39
43
|
|
|
40
44
|
## API
|
|
41
45
|
|
package/engine/session.js
CHANGED
|
@@ -395,27 +395,27 @@ class Session {
|
|
|
395
395
|
if (!this._sessionState) {
|
|
396
396
|
throw new Error('Session not ready');
|
|
397
397
|
}
|
|
398
|
-
const sessionState = await this._sessionState.getValues(["
|
|
398
|
+
const sessionState = await this._sessionState.getValues(["vodMediaSeqVideo", "discSeq"]);
|
|
399
399
|
const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo"]);
|
|
400
400
|
|
|
401
|
-
/*
|
|
402
|
-
Since sessionState is always newer. If it is a smaller value, then this signals that
|
|
403
|
-
a new vod has been loaded but playheadState has yet to get the new mseq value in 'incrementAsync()'.
|
|
404
|
-
Without this, we risk returning an m3u8 with an old mseq on a new vod to the client.
|
|
405
|
-
*/
|
|
406
401
|
if (playheadState.vodMediaSeqVideo > sessionState.vodMediaSeqVideo) {
|
|
407
|
-
|
|
408
|
-
|
|
402
|
+
debug(`[${this._sessionId}]: Recently Loaded Next Vod. PlayheadState not up-to-date. Return the last generated m3u8`);
|
|
403
|
+
const m3u8 = await this._playheadState.getLastM3u8();
|
|
404
|
+
if (m3u8) {
|
|
405
|
+
return m3u8;
|
|
406
|
+
} else {
|
|
407
|
+
debug(`[${this._sessionId}]: We don't have any previously generated m3u8`);
|
|
408
|
+
}
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
if (playheadState.vodMediaSeqVideo
|
|
412
|
-
this.waitingForNextVod = false;
|
|
411
|
+
if (playheadState.vodMediaSeqVideo < 2) {
|
|
413
412
|
const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
414
413
|
if (!isLeader) {
|
|
415
|
-
debug(`[${this._sessionId}]: Not a leader and first media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
|
|
414
|
+
debug(`[${this._sessionId}]: Not a leader and first|second media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
|
|
416
415
|
await this._sessionState.clearCurrentVodCache(); // force reading up from shared store
|
|
417
416
|
}
|
|
418
417
|
}
|
|
418
|
+
|
|
419
419
|
const currentVod = await this._sessionState.getCurrentVod();
|
|
420
420
|
if (currentVod) {
|
|
421
421
|
try {
|
|
@@ -461,19 +461,11 @@ class Session {
|
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
async incrementAsync() {
|
|
464
|
-
await this._tickAsync();
|
|
464
|
+
await this._tickAsync();
|
|
465
465
|
const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
466
466
|
let sessionState = await this._sessionState.getValues(
|
|
467
467
|
["state", "mediaSeq", "discSeq", "vodMediaSeqVideo", "vodMediaSeqAudio"]);
|
|
468
468
|
let playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo", "vodMediaSeqAudio"]);
|
|
469
|
-
if (playheadState.vodMediaSeqVideo < 2) {
|
|
470
|
-
// This is due to the possibility that we miss the tick where vodMediaSeqVideo is 0
|
|
471
|
-
// The worst case scenario is that we clear cache two times in a row on a switch
|
|
472
|
-
if (!isLeader) {
|
|
473
|
-
debug(`[${this._sessionId}]: Not a leader and first media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
|
|
474
|
-
await this._sessionState.clearCurrentVodCache(); // force reading up from shared store
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
469
|
let currentVod = await this._sessionState.getCurrentVod();
|
|
478
470
|
if (!currentVod ||
|
|
479
471
|
sessionState.vodMediaSeqVideo === null ||
|
|
@@ -512,12 +504,19 @@ class Session {
|
|
|
512
504
|
sessionState.vodMediaSeqAudio = await this._sessionState.set("vodMediaSeqAudio", currentVod.getLiveMediaSequencesCount() - 1);
|
|
513
505
|
sessionState.state = await this._sessionState.set("state", SessionState.VOD_NEXT_INIT);
|
|
514
506
|
}
|
|
507
|
+
|
|
515
508
|
playheadState.mediaSeq = await this._playheadState.set("mediaSeq", sessionState.mediaSeq);
|
|
516
509
|
playheadState.vodMediaSeqVideo = await this._playheadState.set("vodMediaSeqVideo", sessionState.vodMediaSeqVideo);
|
|
517
510
|
playheadState.vodMediaSeqAudio = await this._playheadState.set("vodMediaSeqAudio", sessionState.vodMediaSeqAudio);
|
|
518
511
|
debug(`[${this._sessionId}]: INCREMENT (mseq=${playheadState.mediaSeq + playheadState.vodMediaSeqVideo}) vodMediaSeq=(${playheadState.vodMediaSeqVideo}_${playheadState.vodMediaSeqAudio} of ${currentVod.getLiveMediaSequencesCount()})`);
|
|
519
512
|
let m3u8 = currentVod.getLiveMediaSequences(playheadState.mediaSeq, 180000, playheadState.vodMediaSeqVideo, sessionState.discSeq);
|
|
520
513
|
await this._playheadState.setLastM3u8(m3u8);
|
|
514
|
+
|
|
515
|
+
if (!isLeader && playheadState.vodMediaSeqVideo < 2) {
|
|
516
|
+
debug(`[${this._sessionId}]: Not a leader and have just set playheadState vodMediaSeqVideo to 0|1. Invalidate cache to ensure having the correct VOD.`);
|
|
517
|
+
await this._sessionState.clearCurrentVodCache();
|
|
518
|
+
}
|
|
519
|
+
|
|
521
520
|
return m3u8;
|
|
522
521
|
}
|
|
523
522
|
|
package/engine/session_live.js
CHANGED
|
@@ -18,6 +18,7 @@ const daterangeAttribute = (key, attr) => {
|
|
|
18
18
|
const TARGET_PLAYLIST_DURATION_SEC = 60;
|
|
19
19
|
const RESET_DELAY = 5000;
|
|
20
20
|
const FAIL_TIMEOUT = 4000;
|
|
21
|
+
const DEFAULT_PLAYHEAD_INTERVAL_MS = 6 * 1000;
|
|
21
22
|
const PlayheadState = Object.freeze({
|
|
22
23
|
RUNNING: 1,
|
|
23
24
|
STOPPED: 2,
|
|
@@ -40,7 +41,6 @@ class SessionLive {
|
|
|
40
41
|
this.mediaManifestURIs = {};
|
|
41
42
|
this.liveSegQueue = {};
|
|
42
43
|
this.lastRequestedMediaSeqRaw = null;
|
|
43
|
-
this.targetNumSeg = 0;
|
|
44
44
|
this.liveSourceM3Us = {};
|
|
45
45
|
this.playheadState = PlayheadState.IDLE;
|
|
46
46
|
this.liveSegsForFollowers = {};
|
|
@@ -117,7 +117,6 @@ class SessionLive {
|
|
|
117
117
|
this.mediaManifestURIs = {};
|
|
118
118
|
this.liveSegQueue = {};
|
|
119
119
|
this.lastRequestedMediaSeqRaw = null;
|
|
120
|
-
this.targetNumSeg = 0;
|
|
121
120
|
this.liveSourceM3Us = {};
|
|
122
121
|
this.liveSegsForFollowers = {};
|
|
123
122
|
this.timerCompensation = null;
|
|
@@ -149,13 +148,6 @@ class SessionLive {
|
|
|
149
148
|
return;
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
// Let the playhead move at an interval set according to live segment duration
|
|
153
|
-
let liveSegmentDurationMs = 6000;
|
|
154
|
-
let liveBws = Object.keys(this.liveSegQueue);
|
|
155
|
-
if (liveBws.length !== 0 && this.liveSegQueue[liveBws[0]].length !== 0 && this.liveSegQueue[liveBws[0]][0].duration) {
|
|
156
|
-
liveSegmentDurationMs = this.liveSegQueue[liveBws[0]][0].duration * 1000;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
151
|
// Fetch Live-Source Segments, and get ready for on-the-fly manifest generation
|
|
160
152
|
// And also compensate for processing time
|
|
161
153
|
|
|
@@ -165,6 +157,13 @@ class SessionLive {
|
|
|
165
157
|
const tsIncrementEnd = Date.now();
|
|
166
158
|
this.waitForPlayhead = false;
|
|
167
159
|
|
|
160
|
+
// Let the playhead move at an interval set according to live segment duration
|
|
161
|
+
let liveSegmentDurationMs = DEFAULT_PLAYHEAD_INTERVAL_MS;
|
|
162
|
+
let liveBws = Object.keys(this.liveSegQueue);
|
|
163
|
+
if (liveBws.length !== 0 && this.liveSegQueue[liveBws[0]].length !== 0 && this.liveSegQueue[liveBws[0]][0].duration) {
|
|
164
|
+
liveSegmentDurationMs = this.liveSegQueue[liveBws[0]][0].duration * 1000;
|
|
165
|
+
}
|
|
166
|
+
|
|
168
167
|
// Set the timer
|
|
169
168
|
let timerValueMs = 0;
|
|
170
169
|
if (this.timerCompensation) {
|
|
@@ -242,34 +241,31 @@ class SessionLive {
|
|
|
242
241
|
}
|
|
243
242
|
// Make it possible to add & share new segments
|
|
244
243
|
this.allowedToSet = true;
|
|
245
|
-
let segCount = 0;
|
|
246
244
|
const allBws = Object.keys(segments);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
this.vodSegments[bw]
|
|
251
|
-
|
|
245
|
+
if (this._isEmpty(this.vodSegments)) {
|
|
246
|
+
for (let i = 0; i < allBws.length; i++) {
|
|
247
|
+
const bw = allBws[i];
|
|
248
|
+
if (!this.vodSegments[bw]) {
|
|
249
|
+
this.vodSegments[bw] = [];
|
|
250
|
+
}
|
|
252
251
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
if (segments[bw][0].discontinuity) {
|
|
253
|
+
segments[bw].shift();
|
|
254
|
+
}
|
|
256
255
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
256
|
+
for (let segIdx = 0; segIdx < segments[bw].length; segIdx++) {
|
|
257
|
+
this.vodSegments[bw].push(segments[bw][segIdx]);
|
|
258
|
+
}
|
|
259
|
+
if (!segments[bw][segments[bw].length - 1].discontinuity) {
|
|
260
|
+
this.vodSegments[bw].push({ discontinuity: true, cue: { in: true } });
|
|
261
|
+
} else {
|
|
262
|
+
segments[bw][segments[bw].length - 1]["cue"] = { in: true };
|
|
263
|
+
}
|
|
264
264
|
}
|
|
265
|
+
} else {
|
|
266
|
+
debug(`[${this.sessionId}]: 'vodSegments' not empty = Using 'transitSegs'`);
|
|
265
267
|
}
|
|
266
268
|
|
|
267
|
-
for (let segIdx = 0; segIdx < segments[allBws[0]].length; segIdx++) {
|
|
268
|
-
if (segments[allBws[0]][segIdx].uri) {
|
|
269
|
-
segCount++;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
this.targetNumSeg = segCount;
|
|
273
269
|
debug(`[${this.sessionId}]: Setting CurrentMediaSequenceSegments. First seg is: [${this.vodSegments[allBws[0]][0].uri}]`);
|
|
274
270
|
|
|
275
271
|
const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId);
|
|
@@ -309,6 +305,11 @@ class SessionLive {
|
|
|
309
305
|
if (leadersMediaSeqCount !== null) {
|
|
310
306
|
this.mediaSeqCount = leadersMediaSeqCount;
|
|
311
307
|
debug(`[${this.sessionId}]: Setting mediaSeqCount to: [${this.mediaSeqCount}]`);
|
|
308
|
+
const transitSegs = await this.sessionLiveState.get("transitSegs");
|
|
309
|
+
if (!this._isEmpty(transitSegs)) {
|
|
310
|
+
debug(`[${this.sessionId}]: Getting and loading 'transitSegs'`);
|
|
311
|
+
this.vodSegments = transitSegs;
|
|
312
|
+
}
|
|
312
313
|
}
|
|
313
314
|
if (leadersDiscSeqCount !== null) {
|
|
314
315
|
this.discSeqCount = leadersDiscSeqCount;
|
|
@@ -652,7 +653,9 @@ class SessionLive {
|
|
|
652
653
|
// Wait a little before trying again
|
|
653
654
|
debug(`[${this.sessionId}]: ALERT! Live Source Data NOT in sync! Will try again after ${retryDelayMs}ms`);
|
|
654
655
|
await timer(retryDelayMs);
|
|
655
|
-
|
|
656
|
+
if (isLeader) {
|
|
657
|
+
this.timerCompensation = false;
|
|
658
|
+
}
|
|
656
659
|
continue;
|
|
657
660
|
}
|
|
658
661
|
|
|
@@ -690,14 +693,6 @@ class SessionLive {
|
|
|
690
693
|
const transitSegs = await this.sessionLiveState.get("transitSegs");
|
|
691
694
|
if (!this._isEmpty(transitSegs)) {
|
|
692
695
|
this.vodSegments = transitSegs;
|
|
693
|
-
let discTagCount = 0;
|
|
694
|
-
const allBws = Object.keys(this.vodSegments);
|
|
695
|
-
for (let segIdx = 0; segIdx < this.vodSegments[allBws[0]].length; segIdx++) {
|
|
696
|
-
if (this.vodSegments[allBws[0]][segIdx].discontinuity) {
|
|
697
|
-
discTagCount++;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
this.targetNumSeg = this.vodSegments[allBws[0]].length - (discTagCount - 1);
|
|
701
696
|
}
|
|
702
697
|
}
|
|
703
698
|
|
|
@@ -713,14 +708,6 @@ class SessionLive {
|
|
|
713
708
|
//debug(`[${this.sessionId}]: NEW FOLLOWER: I tried to get 'transitSegs'. This is what I found ${JSON.stringify(transitSegs)}`);
|
|
714
709
|
if (!this._isEmpty(transitSegs)) {
|
|
715
710
|
this.vodSegments = transitSegs;
|
|
716
|
-
let discTagCount = 0;
|
|
717
|
-
const allBws = Object.keys(this.vodSegments);
|
|
718
|
-
for (let segIdx = 0; segIdx < this.vodSegments[allBws[0]].length; segIdx++) {
|
|
719
|
-
if (this.vodSegments[allBws[0]][segIdx].discontinuity) {
|
|
720
|
-
discTagCount++;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
this.targetNumSeg = this.vodSegments[allBws[0]].length - (discTagCount - 1);
|
|
724
711
|
}
|
|
725
712
|
}
|
|
726
713
|
debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmount}`);
|
|
@@ -1062,7 +1049,6 @@ class SessionLive {
|
|
|
1062
1049
|
*/
|
|
1063
1050
|
_addLiveSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetBandwidth) {
|
|
1064
1051
|
for (let i = startIdx; i < playlistItems.length; i++) {
|
|
1065
|
-
debug(`[${this.sessionId}]: Adding Live Segment(s) to Queue (for bw=${liveTargetBandwidth})`);
|
|
1066
1052
|
let seg = {};
|
|
1067
1053
|
let playlistItem = playlistItems[i];
|
|
1068
1054
|
let segmentUri;
|
|
@@ -1130,8 +1116,9 @@ class SessionLive {
|
|
|
1130
1116
|
seg["daterange"] = daterangeData;
|
|
1131
1117
|
}
|
|
1132
1118
|
// Push new Live Segments!
|
|
1119
|
+
debug(`[${this.sessionId}]: Adding Live Segment to Queue (for bw=${liveTargetBandwidth})\n${seg.uri ? seg.uri : ''}`);
|
|
1133
1120
|
this.liveSegQueue[liveTargetBandwidth].push(seg);
|
|
1134
|
-
this.liveSegsForFollowers[liveTargetBandwidth].push(seg);
|
|
1121
|
+
this.liveSegsForFollowers[liveTargetBandwidth].push(seg);
|
|
1135
1122
|
}
|
|
1136
1123
|
}
|
|
1137
1124
|
}
|
package/engine/util.js
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { version } = require("../package.json");
|
|
2
2
|
|
|
3
3
|
const filterQueryParser = (filterQuery) => {
|
|
4
4
|
const conditions = filterQuery.match(/\(([^\(\)]*?)\)/g);
|
|
5
5
|
|
|
6
6
|
let filter = {};
|
|
7
|
+
|
|
8
|
+
if (!conditions) {
|
|
9
|
+
return filter;
|
|
10
|
+
}
|
|
7
11
|
conditions.map((c) => {
|
|
8
|
-
const m = c.match(/\(type=="(.*?)"(
|
|
12
|
+
const m = c.match(/\(type=="(.*?)"(AND|\|\|)(.*?)(<|>)(.*)\)/);
|
|
9
13
|
if (m) {
|
|
10
14
|
const type = m[1];
|
|
11
15
|
const operator = m[2];
|
|
12
16
|
const key = m[3];
|
|
13
17
|
const comp = m[4];
|
|
14
18
|
const value = m[5];
|
|
15
|
-
|
|
19
|
+
|
|
16
20
|
if (!filter[type]) {
|
|
17
21
|
filter[type] = {};
|
|
18
22
|
}
|
|
19
|
-
if (operator === "
|
|
23
|
+
if (operator === "AND") {
|
|
20
24
|
if (!filter[type][key]) {
|
|
21
25
|
filter[type][key] = {};
|
|
22
26
|
}
|
|
23
27
|
if (comp === "<") {
|
|
24
|
-
filter[type][key].high = parseInt(value, 10);
|
|
28
|
+
filter[type][key].high = parseInt(value, 10);
|
|
25
29
|
} else if (comp === ">") {
|
|
26
|
-
filter[type][key].low = parseInt(value, 10);
|
|
30
|
+
filter[type][key].low = parseInt(value, 10);
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
}
|
|
@@ -32,42 +36,85 @@ const filterQueryParser = (filterQuery) => {
|
|
|
32
36
|
};
|
|
33
37
|
|
|
34
38
|
const applyFilter = (profiles, filter) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
let filteredProfiles = {};
|
|
40
|
+
const supportedFilterKeys = ["systemBitrate", "height"];
|
|
41
|
+
|
|
42
|
+
if (!filter.video) {
|
|
43
|
+
return profiles;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const keys = Object.keys(filter.video);
|
|
47
|
+
if (supportedFilterKeys.every((supportedKey) => !keys.includes(supportedKey))) {
|
|
48
|
+
return profiles;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (filter.video.systemBitrate) {
|
|
52
|
+
filteredProfiles = profiles.filter((profile) => {
|
|
53
|
+
if (filter.video.systemBitrate.low && filter.video.systemBitrate.high) {
|
|
54
|
+
return profile.bw >= filter.video.systemBitrate.low && profile.bw <= filter.video.systemBitrate.high;
|
|
55
|
+
} else if (filter.video.systemBitrate.low && !filter.video.systemBitrate.high) {
|
|
56
|
+
return profile.bw >= filter.video.systemBitrate.low;
|
|
57
|
+
} else if (!filter.video.systemBitrate.low && filter.video.systemBitrate.high) {
|
|
58
|
+
return profile.bw <= filter.video.systemBitrate.high;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (filter.video.height) {
|
|
65
|
+
let toFilter = profiles;
|
|
66
|
+
if (!ItemIsEmpty(filteredProfiles)) {
|
|
67
|
+
toFilter = filteredProfiles;
|
|
42
68
|
}
|
|
69
|
+
filteredProfiles = toFilter.filter((profile) => {
|
|
70
|
+
if (filter.video.height.low && filter.video.height.high) {
|
|
71
|
+
return profile.resolution[1] >= filter.video.height.low && profile.resolution[1] <= filter.video.height.high;
|
|
72
|
+
} else if (filter.video.height.low) {
|
|
73
|
+
return profile.resolution[1] >= filter.video.height.low;
|
|
74
|
+
} else if (filter.video.height.high) {
|
|
75
|
+
return profile.resolution[1] <= filter.video.height.high;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return filteredProfiles;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const ItemIsEmpty = (obj) => {
|
|
85
|
+
if (!obj) {
|
|
43
86
|
return true;
|
|
44
|
-
}
|
|
87
|
+
}
|
|
88
|
+
for (var key in obj) {
|
|
89
|
+
if (obj.hasOwnProperty(key)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
45
94
|
};
|
|
46
95
|
|
|
47
96
|
const cloudWatchLog = (silent, type, logEntry) => {
|
|
48
97
|
if (!silent) {
|
|
49
98
|
logEntry.type = type;
|
|
50
|
-
logEntry.time =
|
|
99
|
+
logEntry.time = new Date().toISOString();
|
|
51
100
|
console.log(JSON.stringify(logEntry));
|
|
52
101
|
}
|
|
53
102
|
};
|
|
54
103
|
|
|
55
104
|
const m3u8Header = (instanceId) => {
|
|
56
105
|
let m3u8 = "";
|
|
57
|
-
m3u8 += `## Created with Eyevinn Channel Engine library (version=${version}${instanceId ? "<" + instanceId + ">" : ""
|
|
106
|
+
m3u8 += `## Created with Eyevinn Channel Engine library (version=${version}${instanceId ? "<" + instanceId + ">" : ""})\n`;
|
|
58
107
|
m3u8 += "## https://www.npmjs.com/package/eyevinn-channel-engine\n";
|
|
59
108
|
return m3u8;
|
|
60
109
|
};
|
|
61
110
|
|
|
62
111
|
const toHHMMSS = (secs) => {
|
|
63
|
-
var sec_num = parseInt(secs, 10)
|
|
64
|
-
var hours
|
|
65
|
-
var minutes = Math.floor(sec_num / 60) % 60
|
|
66
|
-
var seconds = sec_num % 60
|
|
67
|
-
|
|
68
|
-
return [hours,minutes,seconds]
|
|
69
|
-
.map(v => v < 10 ? "0" + v : v)
|
|
70
|
-
.join(":")
|
|
112
|
+
var sec_num = parseInt(secs, 10);
|
|
113
|
+
var hours = Math.floor(sec_num / 3600);
|
|
114
|
+
var minutes = Math.floor(sec_num / 60) % 60;
|
|
115
|
+
var seconds = sec_num % 60;
|
|
116
|
+
|
|
117
|
+
return [hours, minutes, seconds].map((v) => (v < 10 ? "0" + v : v)).join(":");
|
|
71
118
|
};
|
|
72
119
|
|
|
73
120
|
const logerror = (sessionId, err) => {
|
|
@@ -81,5 +128,5 @@ module.exports = {
|
|
|
81
128
|
cloudWatchLog,
|
|
82
129
|
m3u8Header,
|
|
83
130
|
toHHMMSS,
|
|
84
|
-
logerror
|
|
85
|
-
}
|
|
131
|
+
logerror,
|
|
132
|
+
};
|
package/package.json
CHANGED
package/spec/engine/util_spec.js
CHANGED
|
@@ -2,7 +2,7 @@ const { filterQueryParser, applyFilter } = require('../../engine/util.js');
|
|
|
2
2
|
|
|
3
3
|
describe("Filter Query parser", () => {
|
|
4
4
|
it("can handle systemBitrate low/high range", () => {
|
|
5
|
-
const filterQuery = `(type=="video"
|
|
5
|
+
const filterQuery = `(type=="video"ANDsystemBitrate>100000)AND(type=="video"ANDsystemBitrate<800000)`;
|
|
6
6
|
const filter = filterQueryParser(filterQuery);
|
|
7
7
|
|
|
8
8
|
expect(filter.video.systemBitrate.low).toEqual(100000);
|
|
@@ -10,12 +10,19 @@ describe("Filter Query parser", () => {
|
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
it("can handle resolution low/high range", () => {
|
|
13
|
-
const filterQuery = `(type=="video"
|
|
13
|
+
const filterQuery = `(type=="video"ANDheight>600)AND(type=="video"ANDheight<1080)`;
|
|
14
14
|
const filter = filterQueryParser(filterQuery);
|
|
15
15
|
|
|
16
16
|
expect(filter.video.height.low).toEqual(600);
|
|
17
17
|
expect(filter.video.height.high).toEqual(1080);
|
|
18
18
|
});
|
|
19
|
+
|
|
20
|
+
it("can handle a faulty filter query", () => {
|
|
21
|
+
const filterQuery = `type=video&height=600&type=video&height=1080`;
|
|
22
|
+
const filter = filterQueryParser(filterQuery);
|
|
23
|
+
|
|
24
|
+
expect(filter).toEqual({});
|
|
25
|
+
});
|
|
19
26
|
});
|
|
20
27
|
|
|
21
28
|
describe("Profile filter", () => {
|
|
@@ -60,10 +67,68 @@ describe("Profile filter", () => {
|
|
|
60
67
|
|
|
61
68
|
|
|
62
69
|
it ("can filter out video tracks with height min 200 and max 400", () => {
|
|
63
|
-
const filterQuery = `(type=="video"
|
|
70
|
+
const filterQuery = `(type=="video"ANDheight>200)AND(type=="video"ANDheight<400)`;
|
|
71
|
+
const filter = filterQueryParser(filterQuery);
|
|
72
|
+
|
|
73
|
+
const filteredProfiles = applyFilter(PROFILE, filter);
|
|
74
|
+
expect(filteredProfiles.length).toEqual(2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it ("can filter out video tracks with bitrate min 1414000 and max 3232000", () => {
|
|
78
|
+
const filterQuery = `(type=="video"ANDsystemBitrate>1414000)AND(type=="video"ANDsystemBitrate<3232000)`;
|
|
79
|
+
const filter = filterQueryParser(filterQuery);
|
|
80
|
+
|
|
81
|
+
const filteredProfiles = applyFilter(PROFILE, filter);
|
|
82
|
+
expect(filteredProfiles.length).toEqual(1);
|
|
83
|
+
expect(filteredProfiles[0].bw).toEqual(2323000);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it ("can filter out video tracks with bitrate min 1414000 and height min 300", () => {
|
|
87
|
+
const filterQuery = `(type=="video"ANDsystemBitrate>1414000)AND(type=="video"ANDheight>300)`;
|
|
88
|
+
const filter = filterQueryParser(filterQuery);
|
|
89
|
+
|
|
90
|
+
const filteredProfiles = applyFilter(PROFILE, filter);
|
|
91
|
+
expect(filteredProfiles.length).toEqual(1);
|
|
92
|
+
expect(filteredProfiles[0].bw).toEqual(6134000);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it ("can filter out video tracks with bitrate min 1414000", () => {
|
|
96
|
+
const filterQuery = `(type=="video"ANDsystemBitrate>1414000)`;
|
|
64
97
|
const filter = filterQueryParser(filterQuery);
|
|
65
98
|
|
|
66
99
|
const filteredProfiles = applyFilter(PROFILE, filter);
|
|
67
100
|
expect(filteredProfiles.length).toEqual(2);
|
|
101
|
+
expect(filteredProfiles[1].bw).toEqual(2323000);
|
|
102
|
+
expect(filteredProfiles[0].bw).toEqual(6134000);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it ("can filter out video tracks with bitrate max 1414000", () => {
|
|
106
|
+
const filterQuery = `(type=="video"ANDsystemBitrate<1414000)`;
|
|
107
|
+
const filter = filterQueryParser(filterQuery);
|
|
108
|
+
|
|
109
|
+
const filteredProfiles = applyFilter(PROFILE, filter);
|
|
110
|
+
expect(filteredProfiles.length).toEqual(1);
|
|
111
|
+
expect(filteredProfiles[0].bw).toEqual(1313000);
|
|
68
112
|
});
|
|
113
|
+
|
|
114
|
+
it ("can filter out video tracks with height min 250", () => {
|
|
115
|
+
const filterQuery = `(type=="video"ANDheight>250)`;
|
|
116
|
+
const filter = filterQueryParser(filterQuery);
|
|
117
|
+
|
|
118
|
+
const filteredProfiles = applyFilter(PROFILE, filter);
|
|
119
|
+
expect(filteredProfiles.length).toEqual(2);
|
|
120
|
+
expect(filteredProfiles[1].bw).toEqual(2323000);
|
|
121
|
+
expect(filteredProfiles[0].bw).toEqual(6134000);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it ("can filter out video tracks with height max 300", () => {
|
|
125
|
+
const filterQuery = `(type=="video"ANDheight<300)`;
|
|
126
|
+
const filter = filterQueryParser(filterQuery);
|
|
127
|
+
|
|
128
|
+
const filteredProfiles = applyFilter(PROFILE, filter);
|
|
129
|
+
expect(filteredProfiles.length).toEqual(2);
|
|
130
|
+
expect(filteredProfiles[1].bw).toEqual(1313000);
|
|
131
|
+
expect(filteredProfiles[0].bw).toEqual(2323000);
|
|
132
|
+
});
|
|
133
|
+
|
|
69
134
|
});
|