eyevinn-channel-engine 3.1.3 → 3.1.6
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 +55 -14
- package/engine/session_live.js +26 -17
- 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
|
@@ -44,6 +44,8 @@ class Session {
|
|
|
44
44
|
this.maxTickInterval = DEFAULT_MAX_TICK_INTERVAL;
|
|
45
45
|
this.diffCompensation = null;
|
|
46
46
|
this.waitingForNextVod = false;
|
|
47
|
+
this.leaderIsSettingNextVod = false;
|
|
48
|
+
this.isSwitchingBackFromLive = false;
|
|
47
49
|
|
|
48
50
|
if (config) {
|
|
49
51
|
if (config.sessionId) {
|
|
@@ -258,6 +260,8 @@ class Session {
|
|
|
258
260
|
throw new Error('Session not ready');
|
|
259
261
|
}
|
|
260
262
|
|
|
263
|
+
this.isSwitchingBackFromLive = true;
|
|
264
|
+
|
|
261
265
|
let isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
262
266
|
if (!isLeader) {
|
|
263
267
|
debug(`[${this._sessionId}]: FOLLOWER: Invalidate cache to ensure having the correct VOD!`);
|
|
@@ -278,6 +282,7 @@ class Session {
|
|
|
278
282
|
if (!isLeader || vodReloaded) {
|
|
279
283
|
debug(`[${this._sessionId}]: FOLLOWER: leader is alive, and has presumably updated currentVod. Clearing the cache now`);
|
|
280
284
|
await this._sessionState.clearCurrentVodCache();
|
|
285
|
+
this.isSwitchingBackFromLive = false;
|
|
281
286
|
return;
|
|
282
287
|
}
|
|
283
288
|
}
|
|
@@ -313,6 +318,7 @@ class Session {
|
|
|
313
318
|
await this._playheadState.set("vodMediaSeqAudio", 0);
|
|
314
319
|
await this._playheadState.set("playheadRef", Date.now());
|
|
315
320
|
}
|
|
321
|
+
this.isSwitchingBackFromLive = false;
|
|
316
322
|
}
|
|
317
323
|
|
|
318
324
|
async getCurrentMediaSequenceSegments() {
|
|
@@ -350,6 +356,9 @@ class Session {
|
|
|
350
356
|
if (!this._sessionState) {
|
|
351
357
|
throw new Error('Session not ready');
|
|
352
358
|
}
|
|
359
|
+
|
|
360
|
+
this.isSwitchingBackFromLive = true;
|
|
361
|
+
|
|
353
362
|
const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
354
363
|
if (isLeader) {
|
|
355
364
|
await this._sessionState.set("mediaSeq", _mediaSeq);
|
|
@@ -357,6 +366,7 @@ class Session {
|
|
|
357
366
|
await this._sessionState.set("discSeq", _discSeq);
|
|
358
367
|
debug(`[${this._sessionId}]: Setting current media and discontinuity count -> [${_mediaSeq}]:[${_discSeq}]`);
|
|
359
368
|
}
|
|
369
|
+
this.isSwitchingBackFromLive = false;
|
|
360
370
|
}
|
|
361
371
|
|
|
362
372
|
async getCurrentMediaAndDiscSequenceCount() {
|
|
@@ -395,16 +405,36 @@ class Session {
|
|
|
395
405
|
if (!this._sessionState) {
|
|
396
406
|
throw new Error('Session not ready');
|
|
397
407
|
}
|
|
398
|
-
|
|
408
|
+
// Be sure that the leader is not in the middle of setting new vod data in store.
|
|
409
|
+
// Followers will never run this part...
|
|
410
|
+
let tries = 4;
|
|
411
|
+
while(tries > 0 && this.leaderIsSettingNextVod) {
|
|
412
|
+
debug(`[${this._sessionId}]: Leader is setting the next vod. Waiting 500ms_${tries}`);
|
|
413
|
+
await timer(500);
|
|
414
|
+
tries--;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const sessionState = await this._sessionState.getValues(["vodMediaSeqVideo", "discSeq"]);
|
|
399
418
|
const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo"]);
|
|
400
419
|
|
|
401
|
-
if (playheadState.vodMediaSeqVideo
|
|
420
|
+
if (playheadState.vodMediaSeqVideo > sessionState.vodMediaSeqVideo) {
|
|
421
|
+
debug(`[${this._sessionId}]: Recently Loaded Next Vod. PlayheadState not up-to-date. Return the last generated m3u8`);
|
|
422
|
+
const m3u8 = await this._playheadState.getLastM3u8();
|
|
423
|
+
if (m3u8) {
|
|
424
|
+
return m3u8;
|
|
425
|
+
} else {
|
|
426
|
+
debug(`[${this._sessionId}]: We don't have any previously generated m3u8`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (playheadState.vodMediaSeqVideo < 2) {
|
|
402
431
|
const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
403
432
|
if (!isLeader) {
|
|
404
|
-
debug(`[${this._sessionId}]: Not a leader and first media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
|
|
433
|
+
debug(`[${this._sessionId}]: Not a leader and first|second media sequence in a VOD is requested. Invalidate cache to ensure having the correct VOD.`);
|
|
405
434
|
await this._sessionState.clearCurrentVodCache(); // force reading up from shared store
|
|
406
435
|
}
|
|
407
436
|
}
|
|
437
|
+
|
|
408
438
|
const currentVod = await this._sessionState.getCurrentVod();
|
|
409
439
|
if (currentVod) {
|
|
410
440
|
try {
|
|
@@ -450,8 +480,16 @@ class Session {
|
|
|
450
480
|
}
|
|
451
481
|
|
|
452
482
|
async incrementAsync() {
|
|
453
|
-
await this._tickAsync();
|
|
483
|
+
await this._tickAsync();
|
|
454
484
|
const isLeader = await this._sessionStateStore.isLeader(this._instanceId);
|
|
485
|
+
// Avoid reading old values if new ones are on their way...
|
|
486
|
+
let tries = 10;
|
|
487
|
+
while (tries > 0 && this.isSwitchingBackFromLive) {
|
|
488
|
+
debug(`[${this._sessionId}]: Leader is currently setting new data in store. Waiting 500ms_${tries}`);
|
|
489
|
+
await timer(500);
|
|
490
|
+
tries--;
|
|
491
|
+
}
|
|
492
|
+
|
|
455
493
|
let sessionState = await this._sessionState.getValues(
|
|
456
494
|
["state", "mediaSeq", "discSeq", "vodMediaSeqVideo", "vodMediaSeqAudio"]);
|
|
457
495
|
let playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo", "vodMediaSeqAudio"]);
|
|
@@ -487,12 +525,13 @@ class Session {
|
|
|
487
525
|
sessionState.vodMediaSeqVideo = await this._sessionState.increment("vodMediaSeqVideo");
|
|
488
526
|
sessionState.vodMediaSeqAudio = await this._sessionState.increment("vodMediaSeqAudio");
|
|
489
527
|
}
|
|
490
|
-
|
|
528
|
+
|
|
491
529
|
if (sessionState.vodMediaSeqVideo >= currentVod.getLiveMediaSequencesCount() - 1) {
|
|
492
530
|
sessionState.vodMediaSeqVideo = await this._sessionState.set("vodMediaSeqVideo", currentVod.getLiveMediaSequencesCount() - 1);
|
|
493
531
|
sessionState.vodMediaSeqAudio = await this._sessionState.set("vodMediaSeqAudio", currentVod.getLiveMediaSequencesCount() - 1);
|
|
494
532
|
sessionState.state = await this._sessionState.set("state", SessionState.VOD_NEXT_INIT);
|
|
495
533
|
}
|
|
534
|
+
|
|
496
535
|
playheadState.mediaSeq = await this._playheadState.set("mediaSeq", sessionState.mediaSeq);
|
|
497
536
|
playheadState.vodMediaSeqVideo = await this._playheadState.set("vodMediaSeqVideo", sessionState.vodMediaSeqVideo);
|
|
498
537
|
playheadState.vodMediaSeqAudio = await this._playheadState.set("vodMediaSeqAudio", sessionState.vodMediaSeqAudio);
|
|
@@ -727,11 +766,10 @@ class Session {
|
|
|
727
766
|
length = currentVod.getLiveMediaSequencesCount();
|
|
728
767
|
lastDiscontinuity = currentVod.getLastDiscontinuity();
|
|
729
768
|
}
|
|
730
|
-
await this._sessionState.set("state", SessionState.VOD_NEXT_INITIATING);
|
|
731
|
-
await this._sessionState.setCurrentVod(slateVod);
|
|
732
|
-
|
|
733
769
|
await this._sessionState.set("vodMediaSeqVideo", 0);
|
|
734
770
|
await this._sessionState.set("vodMediaSeqAudio", 0);
|
|
771
|
+
await this._sessionState.set("state", SessionState.VOD_NEXT_INITIATING);
|
|
772
|
+
await this._sessionState.setCurrentVod(slateVod);
|
|
735
773
|
await this._sessionState.set("mediaSeq", sessionState.mediaSeq + length);
|
|
736
774
|
await this._sessionState.set("discSeq", sessionState.discSeq + lastDiscontinuity);
|
|
737
775
|
await this._sessionState.set("slateCount", sessionState.slateCount + 1);
|
|
@@ -815,6 +853,10 @@ class Session {
|
|
|
815
853
|
debug(`[${this._sessionId}]: ${currentVod.getDeltaTimes()}`);
|
|
816
854
|
debug(`[${this._sessionId}]: ${currentVod.getPlayheadPositions()}`);
|
|
817
855
|
//debug(newVod);
|
|
856
|
+
sessionState.mediaSeq = await this._sessionState.set("mediaSeq", 0);
|
|
857
|
+
sessionState.discSeq = await this._sessionState.set("discSeq", 0);
|
|
858
|
+
sessionState.vodMediaSeqVideo = await this._sessionState.set("vodMediaSeqVideo", 0);
|
|
859
|
+
sessionState.vodMediaSeqAudio = await this._sessionState.set("vodMediaSeqAudio", 0);
|
|
818
860
|
await this._playheadState.set("playheadRef", Date.now());
|
|
819
861
|
this.produceEvent({
|
|
820
862
|
type: 'NOW_PLAYING',
|
|
@@ -825,10 +867,6 @@ class Session {
|
|
|
825
867
|
});
|
|
826
868
|
sessionState.state = await this._sessionState.set("state", SessionState.VOD_PLAYING);
|
|
827
869
|
sessionState.currentVod = await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
|
|
828
|
-
sessionState.mediaSeq = await this._sessionState.set("mediaSeq", 0);
|
|
829
|
-
sessionState.discSeq = await this._sessionState.set("discSeq", 0);
|
|
830
|
-
sessionState.vodMediaSeqVideo = await this._sessionState.set("vodMediaSeqVideo", 0);
|
|
831
|
-
sessionState.vodMediaSeqAudio = await this._sessionState.set("vodMediaSeqAudio", 0);
|
|
832
870
|
await this._sessionState.remove("nextVod");
|
|
833
871
|
return;
|
|
834
872
|
} else {
|
|
@@ -932,16 +970,19 @@ class Session {
|
|
|
932
970
|
await ChaosMonkey.loadVod(loadPromise);
|
|
933
971
|
cloudWatchLog(!this.cloudWatchLogging, 'engine-session',
|
|
934
972
|
{ event: 'loadVod', channel: this._sessionId, loadTimeMs: Date.now() - loadStart });
|
|
973
|
+
this.leaderIsSettingNextVod = true;
|
|
935
974
|
debug(`[${this._sessionId}]: next VOD loaded (${newVod.getDeltaTimes()})`);
|
|
936
975
|
debug(`[${this._sessionId}]: ${newVod.getPlayheadPositions()}`);
|
|
937
976
|
currentVod = newVod;
|
|
938
977
|
debug(`[${this._sessionId}]: msequences=${currentVod.getLiveMediaSequencesCount()}`);
|
|
939
|
-
await this._sessionState.remove("nextVod");
|
|
940
|
-
sessionState.currentVod = await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
|
|
941
978
|
sessionState.vodMediaSeqVideo = await this._sessionState.set("vodMediaSeqVideo", 0);
|
|
942
979
|
sessionState.vodMediaSeqAudio = await this._sessionState.set("vodMediaSeqAudio", 0);
|
|
943
980
|
sessionState.mediaSeq = await this._sessionState.set("mediaSeq", sessionState.mediaSeq + length);
|
|
944
981
|
sessionState.discSeq = await this._sessionState.set("discSeq", sessionState.discSeq + lastDiscontinuity);
|
|
982
|
+
debug(`[${this._sessionId}]: new sequence data set in store [${sessionState.mediaSeq}][${sessionState.discSeq}]`);
|
|
983
|
+
await this._sessionState.remove("nextVod");
|
|
984
|
+
sessionState.currentVod = await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
|
|
985
|
+
this.leaderIsSettingNextVod = false;
|
|
945
986
|
await this._playheadState.set("playheadRef", Date.now());
|
|
946
987
|
this.produceEvent({
|
|
947
988
|
type: 'NOW_PLAYING',
|
package/engine/session_live.js
CHANGED
|
@@ -242,24 +242,28 @@ class SessionLive {
|
|
|
242
242
|
// Make it possible to add & share new segments
|
|
243
243
|
this.allowedToSet = true;
|
|
244
244
|
const allBws = Object.keys(segments);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
this.vodSegments[bw]
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (segments[bw][0].discontinuity) {
|
|
252
|
-
segments[bw].shift();
|
|
253
|
-
}
|
|
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
|
+
}
|
|
254
251
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
252
|
+
if (segments[bw][0].discontinuity) {
|
|
253
|
+
segments[bw].shift();
|
|
254
|
+
}
|
|
255
|
+
|
|
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
|
+
}
|
|
262
264
|
}
|
|
265
|
+
} else {
|
|
266
|
+
debug(`[${this.sessionId}]: 'vodSegments' not empty = Using 'transitSegs'`);
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
debug(`[${this.sessionId}]: Setting CurrentMediaSequenceSegments. First seg is: [${this.vodSegments[allBws[0]][0].uri}]`);
|
|
@@ -301,6 +305,11 @@ class SessionLive {
|
|
|
301
305
|
if (leadersMediaSeqCount !== null) {
|
|
302
306
|
this.mediaSeqCount = leadersMediaSeqCount;
|
|
303
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
|
+
}
|
|
304
313
|
}
|
|
305
314
|
if (leadersDiscSeqCount !== null) {
|
|
306
315
|
this.discSeqCount = leadersDiscSeqCount;
|
|
@@ -1040,7 +1049,6 @@ class SessionLive {
|
|
|
1040
1049
|
*/
|
|
1041
1050
|
_addLiveSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetBandwidth) {
|
|
1042
1051
|
for (let i = startIdx; i < playlistItems.length; i++) {
|
|
1043
|
-
debug(`[${this.sessionId}]: Adding Live Segment(s) to Queue (for bw=${liveTargetBandwidth})`);
|
|
1044
1052
|
let seg = {};
|
|
1045
1053
|
let playlistItem = playlistItems[i];
|
|
1046
1054
|
let segmentUri;
|
|
@@ -1108,6 +1116,7 @@ class SessionLive {
|
|
|
1108
1116
|
seg["daterange"] = daterangeData;
|
|
1109
1117
|
}
|
|
1110
1118
|
// Push new Live Segments!
|
|
1119
|
+
debug(`[${this.sessionId}]: Adding Live Segment to Queue (for bw=${liveTargetBandwidth})\n${seg.uri ? seg.uri : ''}`);
|
|
1111
1120
|
this.liveSegQueue[liveTargetBandwidth].push(seg);
|
|
1112
1121
|
this.liveSegsForFollowers[liveTargetBandwidth].push(seg);
|
|
1113
1122
|
}
|
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
|
});
|