eyevinn-channel-engine 3.1.2 → 3.1.5

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 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"&&height>200)&&(type=="video"&&height<400)`. This needs to be URL encoded resulting in the following URL: `http://localhost:8000/channels/1/master.m3u8?filter=%28type%3D%3D%22video%22%26%26height%3E200%29%26%26%28type%3D%3D%22video%22%26%26height%3C400%29`
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(["mediaSeq", "discSeq", "vodMediaSeqVideo"]);
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
- playheadState.vodMediaSeqVideo = sessionState.vodMediaSeqVideo;
408
- playheadState.mediaSeq = sessionState.mediaSeq;
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 === 0 || this.waitingForNextVod) {
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
 
@@ -740,11 +739,10 @@ class Session {
740
739
  length = currentVod.getLiveMediaSequencesCount();
741
740
  lastDiscontinuity = currentVod.getLastDiscontinuity();
742
741
  }
743
- await this._sessionState.set("state", SessionState.VOD_NEXT_INITIATING);
744
- await this._sessionState.setCurrentVod(slateVod);
745
-
746
742
  await this._sessionState.set("vodMediaSeqVideo", 0);
747
743
  await this._sessionState.set("vodMediaSeqAudio", 0);
744
+ await this._sessionState.set("state", SessionState.VOD_NEXT_INITIATING);
745
+ await this._sessionState.setCurrentVod(slateVod);
748
746
  await this._sessionState.set("mediaSeq", sessionState.mediaSeq + length);
749
747
  await this._sessionState.set("discSeq", sessionState.discSeq + lastDiscontinuity);
750
748
  await this._sessionState.set("slateCount", sessionState.slateCount + 1);
@@ -828,6 +826,10 @@ class Session {
828
826
  debug(`[${this._sessionId}]: ${currentVod.getDeltaTimes()}`);
829
827
  debug(`[${this._sessionId}]: ${currentVod.getPlayheadPositions()}`);
830
828
  //debug(newVod);
829
+ sessionState.mediaSeq = await this._sessionState.set("mediaSeq", 0);
830
+ sessionState.discSeq = await this._sessionState.set("discSeq", 0);
831
+ sessionState.vodMediaSeqVideo = await this._sessionState.set("vodMediaSeqVideo", 0);
832
+ sessionState.vodMediaSeqAudio = await this._sessionState.set("vodMediaSeqAudio", 0);
831
833
  await this._playheadState.set("playheadRef", Date.now());
832
834
  this.produceEvent({
833
835
  type: 'NOW_PLAYING',
@@ -838,10 +840,6 @@ class Session {
838
840
  });
839
841
  sessionState.state = await this._sessionState.set("state", SessionState.VOD_PLAYING);
840
842
  sessionState.currentVod = await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
841
- sessionState.mediaSeq = await this._sessionState.set("mediaSeq", 0);
842
- sessionState.discSeq = await this._sessionState.set("discSeq", 0);
843
- sessionState.vodMediaSeqVideo = await this._sessionState.set("vodMediaSeqVideo", 0);
844
- sessionState.vodMediaSeqAudio = await this._sessionState.set("vodMediaSeqAudio", 0);
845
843
  await this._sessionState.remove("nextVod");
846
844
  return;
847
845
  } else {
@@ -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
- for (let i = 0; i < allBws.length; i++) {
246
- const bw = allBws[i];
247
- if (!this.vodSegments[bw]) {
248
- this.vodSegments[bw] = [];
249
- }
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
+ }
250
251
 
251
- if (segments[bw][0].discontinuity) {
252
- segments[bw].shift();
253
- }
252
+ if (segments[bw][0].discontinuity) {
253
+ segments[bw].shift();
254
+ }
254
255
 
255
- for (let segIdx = 0; segIdx < segments[bw].length; segIdx++) {
256
- this.vodSegments[bw].push(segments[bw][segIdx]);
257
- }
258
- if (!segments[bw][segments[bw].length - 1].discontinuity) {
259
- this.vodSegments[bw].push({ discontinuity: true, cue: { in: true } });
260
- } else {
261
- segments[bw][segments[bw].length - 1]["cue"] = { in: true };
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;
@@ -644,6 +653,9 @@ class SessionLive {
644
653
  // Wait a little before trying again
645
654
  debug(`[${this.sessionId}]: ALERT! Live Source Data NOT in sync! Will try again after ${retryDelayMs}ms`);
646
655
  await timer(retryDelayMs);
656
+ if (isLeader) {
657
+ this.timerCompensation = false;
658
+ }
647
659
  continue;
648
660
  }
649
661
 
@@ -1037,7 +1049,6 @@ class SessionLive {
1037
1049
  */
1038
1050
  _addLiveSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetBandwidth) {
1039
1051
  for (let i = startIdx; i < playlistItems.length; i++) {
1040
- debug(`[${this.sessionId}]: Adding Live Segment(s) to Queue (for bw=${liveTargetBandwidth})`);
1041
1052
  let seg = {};
1042
1053
  let playlistItem = playlistItems[i];
1043
1054
  let segmentUri;
@@ -1105,6 +1116,7 @@ class SessionLive {
1105
1116
  seg["daterange"] = daterangeData;
1106
1117
  }
1107
1118
  // Push new Live Segments!
1119
+ debug(`[${this.sessionId}]: Adding Live Segment to Queue (for bw=${liveTargetBandwidth})\n${seg.uri ? seg.uri : ''}`);
1108
1120
  this.liveSegQueue[liveTargetBandwidth].push(seg);
1109
1121
  this.liveSegsForFollowers[liveTargetBandwidth].push(seg);
1110
1122
  }
package/engine/util.js CHANGED
@@ -1,29 +1,33 @@
1
- const { version } = require('../package.json');
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
- return profiles.filter(profile => {
36
- if (filter.video && filter.video.systemBitrate) {
37
- return (profile.bw >= filter.video.systemBitrate.low &&
38
- profile.bw <= filter.video.systemBitrate.high);
39
- } else if (filter.video && filter.video.height) {
40
- return (profile.resolution[1] >= filter.video.height.low &&
41
- profile.resolution[1] <= filter.video.height.high);
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 = (new Date()).toISOString();
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 + ">" : "" })\n`;
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 = Math.floor(sec_num / 3600)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyevinn-channel-engine",
3
- "version": "3.1.2",
3
+ "version": "3.1.5",
4
4
  "description": "OTT TV Channel Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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"&&systemBitrate>100000)&&(type=="video"&&systemBitrate<800000)`;
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"&&height>600)&&(type=="video"&&height<1080)`;
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"&&height>200)&&(type=="video"&&height<400)`;
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
  });