eyevinn-channel-engine 4.0.7 → 4.0.9

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.
@@ -2,6 +2,7 @@ export = RedisStateStore;
2
2
  declare class RedisStateStore {
3
3
  constructor(keyPrefix: any, opts: any);
4
4
  keyPrefix: any;
5
+ volatileKeyTTL: any;
5
6
  client: any;
6
7
  initAsync(id: any, initData: any): Promise<{}>;
7
8
  resetAsync(id: any, initData: any): Promise<void>;
@@ -36,7 +36,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
36
36
  };
37
37
  var redis = require("redis");
38
38
  var debug = require("debug")("redis-state-store");
39
- var VOLATILE_KEY_TTL = 5; // Timeout so it should not expire within one normal increment iteration (in seconds)
39
+ var DEFAULT_VOLATILE_KEY_TTL = 5; // Timeout so it should not expire within one normal increment iteration (in seconds)
40
40
  var RedisStateStore = /** @class */ (function () {
41
41
  function RedisStateStore(keyPrefix, opts) {
42
42
  this.keyPrefix = keyPrefix;
@@ -45,6 +45,11 @@ var RedisStateStore = /** @class */ (function () {
45
45
  this.keyPrefix = prependPrefix + this.keyPrefix;
46
46
  debug("Prepending keyprefix with ".concat(prependPrefix, " => ").concat(this.keyPrefix));
47
47
  }
48
+ this.volatileKeyTTL = DEFAULT_VOLATILE_KEY_TTL;
49
+ if (opts.volatileKeyTTL) {
50
+ debug("Overriding default, volatileKeyTTL=".concat(opts.volatileKeyTTL, "s"));
51
+ this.volatileKeyTTL = opts.volatileKeyTTL;
52
+ }
48
53
  this.client = redis.createClient(opts.redisUrl);
49
54
  }
50
55
  RedisStateStore.prototype.initAsync = function (id, initData) {
@@ -187,9 +192,9 @@ var RedisStateStore = /** @class */ (function () {
187
192
  data = _a.sent();
188
193
  storeKey = "" + this.keyPrefix + id + key;
189
194
  expireAsync = new Promise(function (resolve, reject) {
190
- _this.client.expire(storeKey, VOLATILE_KEY_TTL, function (err, res) {
195
+ _this.client.expire(storeKey, _this.volatileKeyTTL, function (err, res) {
191
196
  if (!err) {
192
- debug("REDIS expire ".concat(storeKey, " ").concat(VOLATILE_KEY_TTL, "s: ").concat(res === 1 ? "OK" : "KEY DOES NOT EXIST"));
197
+ debug("REDIS expire ".concat(storeKey, " ").concat(_this.volatileKeyTTL, "s: ").concat(res === 1 ? "OK" : "KEY DOES NOT EXIST"));
193
198
  resolve();
194
199
  }
195
200
  else {
@@ -22,6 +22,7 @@ export interface ChannelEngineOpts {
22
22
  adCopyMgrUri?: string;
23
23
  adXchangeUri?: string;
24
24
  noSessionDataTags?: boolean;
25
+ volatileKeyTTL?: number;
25
26
  }
26
27
  export interface VodRequest {
27
28
  sessionId: string;
@@ -58,6 +59,7 @@ export interface ChannelProfile {
58
59
  bw: number;
59
60
  codecs: string;
60
61
  resolution: number[];
62
+ channels?: string;
61
63
  }
62
64
  export interface Channel {
63
65
  id: string;
@@ -75,8 +77,6 @@ export interface ClosedCaptions {
75
77
  export interface AudioTracks {
76
78
  language: string;
77
79
  name: string;
78
- channels?: number;
79
- codecs?: string;
80
80
  default?: boolean;
81
81
  }
82
82
  export interface IChannelManager {
@@ -101,11 +101,13 @@ var ChannelEngine = /** @class */ (function () {
101
101
  redisUrl: options.redisUrl,
102
102
  memcachedUrl: options.memcachedUrl,
103
103
  cacheTTL: options.sharedStoreCacheTTL,
104
+ volatileKeyTTL: options.volatileKeyTTL,
104
105
  }),
105
106
  playheadStateStore: new PlayheadStateStore({
106
107
  redisUrl: options.redisUrl,
107
108
  memcachedUrl: options.memcachedUrl,
108
109
  cacheTTL: options.sharedStoreCacheTTL,
110
+ volatileKeyTTL: options.volatileKeyTTL,
109
111
  }),
110
112
  instanceId: this.instanceId,
111
113
  };
@@ -114,6 +116,7 @@ var ChannelEngine = /** @class */ (function () {
114
116
  redisUrl: options.redisUrl,
115
117
  memcachedUrl: options.memcachedUrl,
116
118
  cacheTTL: options.sharedStoreCacheTTL,
119
+ volatileKeyTTL: options.volatileKeyTTL,
117
120
  }),
118
121
  instanceId: this.instanceId,
119
122
  };
@@ -43,7 +43,7 @@ var HLSTruncateVod = require('@eyevinn/hls-truncate');
43
43
  var Readable = require('stream').Readable;
44
44
  var SessionState = require('./session_state.js').SessionState;
45
45
  var PlayheadState = require('./playhead_state.js').PlayheadState;
46
- var _a = require('./util.js'), applyFilter = _a.applyFilter, cloudWatchLog = _a.cloudWatchLog, m3u8Header = _a.m3u8Header, logerror = _a.logerror;
46
+ var _a = require('./util.js'), applyFilter = _a.applyFilter, cloudWatchLog = _a.cloudWatchLog, m3u8Header = _a.m3u8Header, logerror = _a.logerror, codecsFromString = _a.codecsFromString;
47
47
  var ChaosMonkey = require('./chaos_monkey.js');
48
48
  var AVERAGE_SEGMENT_DURATION = 3000;
49
49
  var DEFAULT_PLAYHEAD_DIFF_THRESHOLD = 1000;
@@ -923,7 +923,7 @@ var Session = /** @class */ (function () {
923
923
  debug("[".concat(this._sessionId, "]: {").concat(mediaSequenceValue, "}_{").concat(currentVod.getLastSequenceMediaSequenceValueAudio(), "} AUDIO"));
924
924
  manifestMseq = playheadState.mediaSeqAudio + mediaSequenceValue;
925
925
  }
926
- debug("[".concat(this._sessionId, "]: [").concat(playheadState.vodMediaSeqAudio, "]_[").concat(currentVod.getLiveMediaSequencesCount("audio"), "] AUDIO"));
926
+ debug("[".concat(this._sessionId, "]: [").concat(playheadState.vodMediaSeqAudio, "]_[").concat(currentVod.getLiveMediaSequencesCount("audio"), "] AUDIO (").concat(audioGroupId, ")"));
927
927
  m3u8_2 = currentVod.getLiveMediaAudioSequences(playheadState.mediaSeqAudio, audioGroupId, audioLanguage, playheadState.vodMediaSeqAudio, sessionState.discSeqAudio, this.targetDurationPadding, this.forceTargetDuration);
928
928
  // # Case: current VOD does not have the selected track.
929
929
  if (!m3u8_2) {
@@ -1031,6 +1031,7 @@ var Session = /** @class */ (function () {
1031
1031
  index = 0;
1032
1032
  audioSeqLastIdx = currentVod_1.getLiveMediaSequencesCount("audio") - 1;
1033
1033
  thresh = 0.5;
1034
+ debug("[".concat(this._sessionId, "]: About to determine audio increment"));
1034
1035
  _o.label = 19;
1035
1036
  case 19: return [4 /*yield*/, this._getAudioPlayheadPosition(sessionState_1.vodMediaSeqAudio + index)];
1036
1037
  case 20:
@@ -1058,6 +1059,7 @@ var Session = /** @class */ (function () {
1058
1059
  audioIncrement = index;
1059
1060
  _o.label = 23;
1060
1061
  case 23:
1062
+ debug("[".concat(this._sessionId, "]: Will increment audio with ").concat(audioIncrement));
1061
1063
  _d = sessionState;
1062
1064
  return [4 /*yield*/, this._sessionState.increment("vodMediaSeqAudio", audioIncrement)];
1063
1065
  case 24:
@@ -1346,10 +1348,10 @@ var Session = /** @class */ (function () {
1346
1348
  };
1347
1349
  Session.prototype.getMasterManifestAsync = function (filter) {
1348
1350
  return __awaiter(this, void 0, void 0, function () {
1349
- var m3u8, currentVod, audioGroupIds, defaultAudioGroupId, hasClosedCaptions, i, audioGroupId, j, audioTrack, channels, sessionProfile;
1351
+ var m3u8, currentVod, audioGroupIds, defaultAudioGroupId, hasClosedCaptions, i, audioGroupId, j, audioTrack, _a, _, channels, sessionProfile;
1350
1352
  var _this = this;
1351
- return __generator(this, function (_a) {
1352
- switch (_a.label) {
1353
+ return __generator(this, function (_b) {
1354
+ switch (_b.label) {
1353
1355
  case 0:
1354
1356
  m3u8 = "#EXTM3U\n";
1355
1357
  m3u8 += "#EXT-X-VERSION:4\n";
@@ -1360,7 +1362,7 @@ var Session = /** @class */ (function () {
1360
1362
  }
1361
1363
  return [4 /*yield*/, this._sessionState.getCurrentVod()];
1362
1364
  case 1:
1363
- currentVod = _a.sent();
1365
+ currentVod = _b.sent();
1364
1366
  if (!currentVod) {
1365
1367
  throw new Error('Session not ready');
1366
1368
  }
@@ -1378,14 +1380,16 @@ var Session = /** @class */ (function () {
1378
1380
  audioGroupId = audioGroupIds[i];
1379
1381
  for (j = 0; j < this._audioTracks.length; j++) {
1380
1382
  audioTrack = this._audioTracks[j];
1381
- channels = this._audioTracks[j].channels ? this._audioTracks[j].channels : 2;
1383
+ _a = currentVod.getAudioCodecsAndChannelsForGroupId(audioGroupId), _ = _a[0], channels = _a[1];
1382
1384
  // Make default track if set property is true.
1383
- if (audioTrack.default) {
1384
- m3u8 += "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"".concat(audioGroupId, "\",LANGUAGE=\"").concat(audioTrack.language, "\", NAME=\"").concat(audioTrack.name, "\",AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"").concat(channels, "\",URI=\"master-").concat(audioGroupId, "_").concat(audioTrack.language, ".m3u8;session=").concat(this._sessionId, "\"\n");
1385
- }
1386
- else {
1387
- m3u8 += "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"".concat(audioGroupId, "\",LANGUAGE=\"").concat(audioTrack.language, "\", NAME=\"").concat(audioTrack.name, "\",AUTOSELECT=YES,DEFAULT=NO,CHANNELS=\"").concat(channels, "\",URI=\"master-").concat(audioGroupId, "_").concat(audioTrack.language, ".m3u8;session=").concat(this._sessionId, "\"\n");
1388
- }
1385
+ m3u8 += "#EXT-X-MEDIA:TYPE=AUDIO" +
1386
+ ",GROUP-ID=\"".concat(audioGroupId, "\"") +
1387
+ ",LANGUAGE=\"".concat(audioTrack.language, "\"") +
1388
+ ",NAME=\"".concat(audioTrack.name, "\"") +
1389
+ ",AUTOSELECT=YES,DEFAULT=".concat(audioTrack.default ? 'YES' : 'NO') +
1390
+ ",CHANNELS=\"".concat(channels ? channels : 2, "\"") +
1391
+ ",URI=\"master-".concat(audioGroupId, "_").concat(audioTrack.language, ".m3u8;session=").concat(this._sessionId, "\"") +
1392
+ "\n";
1389
1393
  }
1390
1394
  }
1391
1395
  // As of now, by default set StreamItem's AUDIO attribute to <first audio group-id>
@@ -1395,13 +1399,41 @@ var Session = /** @class */ (function () {
1395
1399
  if (this._sessionProfile) {
1396
1400
  sessionProfile = filter ? applyFilter(this._sessionProfile, filter) : this._sessionProfile;
1397
1401
  sessionProfile.forEach(function (profile) {
1398
- m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] + ',CODECS="' + profile.codecs + '"' + (defaultAudioGroupId ? ",AUDIO=\"".concat(defaultAudioGroupId, "\"") : '') + (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1399
- m3u8 += "master" + profile.bw + ".m3u8;session=" + _this._sessionId + "\n";
1402
+ if (_this.use_demuxed_audio) {
1403
+ // find matching audio group based on codec in stream
1404
+ var audioGroupIdToUse = void 0;
1405
+ var _a = codecsFromString(profile.codecs), _ = _a[0], audioCodec = _a[1];
1406
+ if (audioCodec) {
1407
+ var profileChannels = profile.channels ? profile.channels : "2";
1408
+ audioGroupIdToUse = currentVod.getAudioGroupIdForCodecs(audioCodec, profileChannels);
1409
+ }
1410
+ // skip stream if no corresponding audio group can be found
1411
+ if (audioGroupIdToUse) {
1412
+ m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw +
1413
+ ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] +
1414
+ ',CODECS="' + profile.codecs + '"' +
1415
+ ",AUDIO=\"".concat(audioGroupIdToUse, "\"") +
1416
+ (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1417
+ m3u8 += "master" + profile.bw + ".m3u8;session=" + _this._sessionId + "\n";
1418
+ }
1419
+ }
1420
+ else {
1421
+ m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw +
1422
+ ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] +
1423
+ ',CODECS="' + profile.codecs + '"' +
1424
+ (defaultAudioGroupId ? ",AUDIO=\"".concat(defaultAudioGroupId, "\"") : '') +
1425
+ (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1426
+ m3u8 += "master" + profile.bw + ".m3u8;session=" + _this._sessionId + "\n";
1427
+ }
1400
1428
  });
1401
1429
  }
1402
1430
  else {
1403
1431
  currentVod.getUsageProfiles().forEach(function (profile) {
1404
- m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + ',RESOLUTION=' + profile.resolution + ',CODECS="' + profile.codecs + '"' + (defaultAudioGroupId ? ",AUDIO=\"".concat(defaultAudioGroupId, "\"") : '') + (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1432
+ m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw +
1433
+ ',RESOLUTION=' + profile.resolution +
1434
+ ',CODECS="' + profile.codecs + '"' +
1435
+ (defaultAudioGroupId ? ",AUDIO=\"".concat(defaultAudioGroupId, "\"") : '') +
1436
+ (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1405
1437
  m3u8 += "master" + profile.bw + ".m3u8;session=" + _this._sessionId + "\n";
1406
1438
  });
1407
1439
  }
@@ -1606,7 +1638,8 @@ var Session = /** @class */ (function () {
1606
1638
  cloudWatchLog(!this.cloudWatchLogging, 'engine-session', { event: 'loadVod', channel: this._sessionId, loadTimeMs: Date.now() - loadStart });
1607
1639
  debug("[".concat(this._sessionId, "]: first VOD loaded"));
1608
1640
  debug("[".concat(this._sessionId, "]: ").concat(currentVod.getDeltaTimes()));
1609
- debug("[".concat(this._sessionId, "]: ").concat(currentVod.getPlayheadPositions()));
1641
+ debug("[".concat(this._sessionId, "]: playhead positions [V]=").concat(currentVod.getPlayheadPositions("video")));
1642
+ debug("[".concat(this._sessionId, "]: playhead positions [A]=").concat(currentVod.getPlayheadPositions("audio")));
1610
1643
  //debug(newVod);
1611
1644
  _c = sessionState;
1612
1645
  return [4 /*yield*/, this._sessionState.set("mediaSeq", 0)];
@@ -1828,7 +1861,8 @@ var Session = /** @class */ (function () {
1828
1861
  cloudWatchLog(!this.cloudWatchLogging, 'engine-session', { event: 'loadVod', channel: this._sessionId, loadTimeMs: Date.now() - loadStart });
1829
1862
  this.leaderIsSettingNextVod = true;
1830
1863
  debug("[".concat(this._sessionId, "]: next VOD loaded (").concat(newVod.getDeltaTimes(), ")"));
1831
- debug("[".concat(this._sessionId, "]: ").concat(newVod.getPlayheadPositions()));
1864
+ debug("[".concat(this._sessionId, "]: playhead positions [V]=").concat(newVod.getPlayheadPositions("video")));
1865
+ debug("[".concat(this._sessionId, "]: playhead positions [A]=").concat(newVod.getPlayheadPositions("audio")));
1832
1866
  currentVod = newVod;
1833
1867
  debug("[".concat(this._sessionId, "]: msequences=").concat(currentVod.getLiveMediaSequencesCount(), "; audio msequences=").concat(currentVod.getLiveMediaSequencesCount("audio")));
1834
1868
  _r = sessionState;
@@ -17,3 +17,4 @@ export class WaitTimeGenerator {
17
17
  export function findNearestValue(val: any, array: any): any;
18
18
  export function isValidUrl(url: any): boolean;
19
19
  export function fetchWithRetry(uri: any, opts: any, maxRetries: any, retryDelayMs: any, timeoutMs: any, debug: any): Promise<any>;
20
+ export function codecsFromString(codecs: any): any[];
@@ -266,6 +266,15 @@ var fetchWithRetry = function (uri, opts, maxRetries, retryDelayMs, timeoutMs, d
266
266
  }
267
267
  });
268
268
  }); };
269
+ var codecsFromString = function (codecs) {
270
+ var audioCodecs = codecs.split(",").find(function (c) {
271
+ return c.match(/^mp4a/) || c.match(/^ac-3/) || c.match(/^ec-3/);
272
+ });
273
+ var videoCodecs = codecs.split(",").find(function (c) {
274
+ return c.match(/^avc/) || c.match(/^hevc/) || c.match(/^dvh/);
275
+ });
276
+ return [videoCodecs, audioCodecs];
277
+ };
269
278
  module.exports = {
270
279
  filterQueryParser: filterQueryParser,
271
280
  applyFilter: applyFilter,
@@ -277,5 +286,6 @@ module.exports = {
277
286
  WaitTimeGenerator: WaitTimeGenerator,
278
287
  findNearestValue: findNearestValue,
279
288
  isValidUrl: isValidUrl,
280
- fetchWithRetry: fetchWithRetry
289
+ fetchWithRetry: fetchWithRetry,
290
+ codecsFromString: codecsFromString
281
291
  };
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyevinn-channel-engine",
3
- "version": "4.0.7",
3
+ "version": "4.0.9",
4
4
  "description": "OTT TV Channel Engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "@eyevinn/hls-repeat": "^0.2.0",
28
28
  "@eyevinn/hls-truncate": "^0.2.0",
29
- "@eyevinn/hls-vodtolive": "^2.2.1",
29
+ "@eyevinn/hls-vodtolive": "^2.3.3",
30
30
  "@eyevinn/m3u8": "^0.5.3",
31
31
  "abort-controller": "^3.0.0",
32
32
  "debug": "^3.2.7",
@@ -81,8 +81,8 @@ var RefChannelManager = /** @class */ (function () {
81
81
  };
82
82
  RefChannelManager.prototype._getAudioTracks = function () {
83
83
  return [
84
- { language: "en", name: "English" },
85
- { language: "sp", name: "Spanish" }
84
+ { language: "en", name: "English", default: true },
85
+ { language: "sp", name: "Spanish", default: false }
86
86
  ];
87
87
  };
88
88
  return RefChannelManager;
@@ -65,18 +65,18 @@ var RefChannelManager = /** @class */ (function () {
65
65
  };
66
66
  RefChannelManager.prototype._getProfile = function () {
67
67
  return [
68
- { resolution: [640, 360], bw: 3663471, codecs: "avc1.64001F,mp4a.40.2" },
69
- { resolution: [1280, 720], bw: 5841380, codecs: "avc1.64001F,mp4a.40.2" },
70
- { resolution: [1920, 1080], bw: 8973571, codecs: "avc1.64001F,mp4a.40.2" },
71
- { resolution: [640, 360], bw: 4301519, codecs: "avc1.64001F,ec-3" },
72
- { resolution: [1280, 720], bw: 6479428, codecs: "avc1.64001F,ec-3" },
73
- { resolution: [1920, 1080], bw: 9611619, codecs: "avc1.640032,ec-3" },
68
+ { resolution: [640, 360], bw: 3663471, codecs: "avc1.64001F,mp4a.40.2", channels: "2" },
69
+ { resolution: [1280, 720], bw: 5841380, codecs: "avc1.64001F,mp4a.40.2", channels: "2" },
70
+ { resolution: [1920, 1080], bw: 8973571, codecs: "avc1.64001F,mp4a.40.2", channels: "2" },
71
+ { resolution: [640, 360], bw: 4301519, codecs: "avc1.64001F,ec-3", channels: "16/JOC" },
72
+ { resolution: [1280, 720], bw: 6479428, codecs: "avc1.64001F,ec-3", channels: "16/JOC" },
73
+ { resolution: [1920, 1080], bw: 9611619, codecs: "avc1.640032,ec-3", channels: "16/JOC" },
74
74
  ];
75
75
  };
76
76
  RefChannelManager.prototype._getAudioTracks = function () {
77
77
  return [
78
- { language: "ja", "name": "日本語 stereo", channels: 2, codecs: "mp4a.40.2", default: true },
79
- { language: "ja", "name": "日本語 surround", channels: 16, codecs: "ec-3", default: false }
78
+ { language: "ja", "name": "日本語", default: true },
79
+ { language: "en", "name": "English", default: false }
80
80
  ];
81
81
  };
82
82
  return RefChannelManager;
@@ -1,7 +1,7 @@
1
1
  const redis = require("redis");
2
2
  const debug = require("debug")("redis-state-store");
3
3
 
4
- const VOLATILE_KEY_TTL = 5; // Timeout so it should not expire within one normal increment iteration (in seconds)
4
+ const DEFAULT_VOLATILE_KEY_TTL = 5; // Timeout so it should not expire within one normal increment iteration (in seconds)
5
5
 
6
6
  class RedisStateStore {
7
7
  constructor(keyPrefix, opts) {
@@ -11,6 +11,11 @@ class RedisStateStore {
11
11
  this.keyPrefix = prependPrefix + this.keyPrefix;
12
12
  debug(`Prepending keyprefix with ${prependPrefix} => ${this.keyPrefix}`);
13
13
  }
14
+ this.volatileKeyTTL = DEFAULT_VOLATILE_KEY_TTL;
15
+ if (opts.volatileKeyTTL) {
16
+ debug(`Overriding default, volatileKeyTTL=${opts.volatileKeyTTL}s`);
17
+ this.volatileKeyTTL = opts.volatileKeyTTL;
18
+ }
14
19
  this.client = redis.createClient(opts.redisUrl);
15
20
  }
16
21
 
@@ -82,9 +87,9 @@ class RedisStateStore {
82
87
  const data = await this.setAsync(id, key, value);
83
88
  const storeKey = "" + this.keyPrefix + id + key;
84
89
  const expireAsync = new Promise((resolve, reject) => {
85
- this.client.expire(storeKey, VOLATILE_KEY_TTL, (err, res) => {
90
+ this.client.expire(storeKey, this.volatileKeyTTL, (err, res) => {
86
91
  if (!err) {
87
- debug(`REDIS expire ${storeKey} ${VOLATILE_KEY_TTL}s: ${res === 1 ? "OK" : "KEY DOES NOT EXIST"}`);
92
+ debug(`REDIS expire ${storeKey} ${this.volatileKeyTTL}s: ${res === 1 ? "OK" : "KEY DOES NOT EXIST"}`);
88
93
  resolve();
89
94
  } else {
90
95
  reject(err);
package/engine/server.ts CHANGED
@@ -47,6 +47,7 @@ export interface ChannelEngineOpts {
47
47
  adCopyMgrUri?: string; // deprecated
48
48
  adXchangeUri?: string; // deprecated
49
49
  noSessionDataTags?: boolean;
50
+ volatileKeyTTL?: number;
50
51
  }
51
52
 
52
53
  interface StreamerOpts {
@@ -99,6 +100,7 @@ export interface ChannelProfile {
99
100
  bw: number;
100
101
  codecs: string;
101
102
  resolution: number[];
103
+ channels?: string;
102
104
  }
103
105
 
104
106
  export interface Channel {
@@ -119,8 +121,6 @@ export interface ClosedCaptions {
119
121
  export interface AudioTracks {
120
122
  language: string;
121
123
  name: string;
122
- channels?: number;
123
- codecs?: string;
124
124
  default?: boolean;
125
125
  }
126
126
 
@@ -207,11 +207,13 @@ export class ChannelEngine {
207
207
  redisUrl: options.redisUrl,
208
208
  memcachedUrl: options.memcachedUrl,
209
209
  cacheTTL: options.sharedStoreCacheTTL,
210
+ volatileKeyTTL: options.volatileKeyTTL,
210
211
  }),
211
212
  playheadStateStore: new PlayheadStateStore({
212
213
  redisUrl: options.redisUrl,
213
214
  memcachedUrl: options.memcachedUrl,
214
215
  cacheTTL: options.sharedStoreCacheTTL,
216
+ volatileKeyTTL: options.volatileKeyTTL,
215
217
  }),
216
218
  instanceId: this.instanceId,
217
219
  };
@@ -221,6 +223,7 @@ export class ChannelEngine {
221
223
  redisUrl: options.redisUrl,
222
224
  memcachedUrl: options.memcachedUrl,
223
225
  cacheTTL: options.sharedStoreCacheTTL,
226
+ volatileKeyTTL: options.volatileKeyTTL,
224
227
  }),
225
228
  instanceId: this.instanceId,
226
229
  };
package/engine/session.js CHANGED
@@ -9,7 +9,7 @@ const Readable = require('stream').Readable;
9
9
  const { SessionState } = require('./session_state.js');
10
10
  const { PlayheadState } = require('./playhead_state.js');
11
11
 
12
- const { applyFilter, cloudWatchLog, m3u8Header, logerror } = require('./util.js');
12
+ const { applyFilter, cloudWatchLog, m3u8Header, logerror, codecsFromString } = require('./util.js');
13
13
  const ChaosMonkey = require('./chaos_monkey.js');
14
14
 
15
15
  const AVERAGE_SEGMENT_DURATION = 3000;
@@ -655,7 +655,7 @@ class Session {
655
655
  debug(`[${this._sessionId}]: {${mediaSequenceValue}}_{${currentVod.getLastSequenceMediaSequenceValueAudio()}} AUDIO`);
656
656
  manifestMseq = playheadState.mediaSeqAudio + mediaSequenceValue;
657
657
  }
658
- debug(`[${this._sessionId}]: [${playheadState.vodMediaSeqAudio}]_[${currentVod.getLiveMediaSequencesCount("audio")}] AUDIO`);
658
+ debug(`[${this._sessionId}]: [${playheadState.vodMediaSeqAudio}]_[${currentVod.getLiveMediaSequencesCount("audio")}] AUDIO (${audioGroupId})`);
659
659
  const m3u8 = currentVod.getLiveMediaAudioSequences(
660
660
  playheadState.mediaSeqAudio,
661
661
  audioGroupId,
@@ -733,6 +733,7 @@ class Session {
733
733
  let index = 0;
734
734
  const audioSeqLastIdx = currentVod.getLiveMediaSequencesCount("audio") - 1;
735
735
  const thresh = 0.5;
736
+ debug(`[${this._sessionId}]: About to determine audio increment`);
736
737
  do {
737
738
  const audioPosition = (await this._getAudioPlayheadPosition(sessionState.vodMediaSeqAudio + index)) * 1000;
738
739
  posDiff = position - audioPosition;
@@ -752,6 +753,7 @@ class Session {
752
753
  } while (!(-thresh < posDiff && posDiff < thresh));
753
754
  audioIncrement = index;
754
755
  }
756
+ debug(`[${this._sessionId}]: Will increment audio with ${audioIncrement}`);
755
757
  sessionState.vodMediaSeqAudio = await this._sessionState.increment("vodMediaSeqAudio", audioIncrement);
756
758
  }
757
759
 
@@ -959,13 +961,16 @@ class Session {
959
961
  let audioGroupId = audioGroupIds[i];
960
962
  for (let j = 0; j < this._audioTracks.length; j++) {
961
963
  let audioTrack = this._audioTracks[j];
962
- let channels = this._audioTracks[j].channels ? this._audioTracks[j].channels : 2;
964
+ const [_, channels] = currentVod.getAudioCodecsAndChannelsForGroupId(audioGroupId);
963
965
  // Make default track if set property is true.
964
- if (audioTrack.default) {
965
- m3u8 += `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="${audioGroupId}",LANGUAGE="${audioTrack.language}", NAME="${audioTrack.name}",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="${channels}",URI="master-${audioGroupId}_${audioTrack.language}.m3u8;session=${this._sessionId}"\n`;
966
- } else {
967
- m3u8 += `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="${audioGroupId}",LANGUAGE="${audioTrack.language}", NAME="${audioTrack.name}",AUTOSELECT=YES,DEFAULT=NO,CHANNELS="${channels}",URI="master-${audioGroupId}_${audioTrack.language}.m3u8;session=${this._sessionId}"\n`;
968
- }
966
+ m3u8 += `#EXT-X-MEDIA:TYPE=AUDIO` +
967
+ `,GROUP-ID="${audioGroupId}"` +
968
+ `,LANGUAGE="${audioTrack.language}"` +
969
+ `,NAME="${audioTrack.name}"` +
970
+ `,AUTOSELECT=YES,DEFAULT=${audioTrack.default ? 'YES' : 'NO'}` +
971
+ `,CHANNELS="${channels ? channels : 2}"` +
972
+ `,URI="master-${audioGroupId}_${audioTrack.language}.m3u8;session=${this._sessionId}"` +
973
+ "\n";
969
974
  }
970
975
  }
971
976
  // As of now, by default set StreamItem's AUDIO attribute to <first audio group-id>
@@ -975,12 +980,40 @@ class Session {
975
980
  if (this._sessionProfile) {
976
981
  const sessionProfile = filter ? applyFilter(this._sessionProfile, filter) : this._sessionProfile;
977
982
  sessionProfile.forEach(profile => {
978
- m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] + ',CODECS="' + profile.codecs + '"' + (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') + (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
979
- m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
983
+ if (this.use_demuxed_audio) {
984
+ // find matching audio group based on codec in stream
985
+ let audioGroupIdToUse;
986
+ const [_, audioCodec] = codecsFromString(profile.codecs);
987
+ if (audioCodec) {
988
+ const profileChannels = profile.channels ? profile.channels : "2";
989
+ audioGroupIdToUse = currentVod.getAudioGroupIdForCodecs(audioCodec, profileChannels);
990
+ }
991
+
992
+ // skip stream if no corresponding audio group can be found
993
+ if (audioGroupIdToUse) {
994
+ m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw +
995
+ ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] +
996
+ ',CODECS="' + profile.codecs + '"' +
997
+ `,AUDIO="${audioGroupIdToUse}"` +
998
+ (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
999
+ m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
1000
+ }
1001
+ } else {
1002
+ m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw +
1003
+ ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] +
1004
+ ',CODECS="' + profile.codecs + '"' +
1005
+ (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') +
1006
+ (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1007
+ m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
1008
+ }
980
1009
  });
981
1010
  } else {
982
1011
  currentVod.getUsageProfiles().forEach(profile => {
983
- m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + ',RESOLUTION=' + profile.resolution + ',CODECS="' + profile.codecs + '"' + (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') + (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1012
+ m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw +
1013
+ ',RESOLUTION=' + profile.resolution +
1014
+ ',CODECS="' + profile.codecs + '"' +
1015
+ (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') +
1016
+ (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
984
1017
  m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
985
1018
  });
986
1019
  }
@@ -1129,7 +1162,8 @@ class Session {
1129
1162
  { event: 'loadVod', channel: this._sessionId, loadTimeMs: Date.now() - loadStart });
1130
1163
  debug(`[${this._sessionId}]: first VOD loaded`);
1131
1164
  debug(`[${this._sessionId}]: ${currentVod.getDeltaTimes()}`);
1132
- debug(`[${this._sessionId}]: ${currentVod.getPlayheadPositions()}`);
1165
+ debug(`[${this._sessionId}]: playhead positions [V]=${currentVod.getPlayheadPositions("video")}`);
1166
+ debug(`[${this._sessionId}]: playhead positions [A]=${currentVod.getPlayheadPositions("audio")}`);
1133
1167
  //debug(newVod);
1134
1168
  sessionState.mediaSeq = await this._sessionState.set("mediaSeq", 0);
1135
1169
  sessionState.mediaSeqAudio = await this._sessionState.set("mediaSeqAudio", 0);
@@ -1292,7 +1326,8 @@ class Session {
1292
1326
  { event: 'loadVod', channel: this._sessionId, loadTimeMs: Date.now() - loadStart });
1293
1327
  this.leaderIsSettingNextVod = true;
1294
1328
  debug(`[${this._sessionId}]: next VOD loaded (${newVod.getDeltaTimes()})`);
1295
- debug(`[${this._sessionId}]: ${newVod.getPlayheadPositions()}`);
1329
+ debug(`[${this._sessionId}]: playhead positions [V]=${newVod.getPlayheadPositions("video")}`);
1330
+ debug(`[${this._sessionId}]: playhead positions [A]=${newVod.getPlayheadPositions("audio")}`);
1296
1331
  currentVod = newVod;
1297
1332
  debug(`[${this._sessionId}]: msequences=${currentVod.getLiveMediaSequencesCount()}; audio msequences=${currentVod.getLiveMediaSequencesCount("audio")}`);
1298
1333
  sessionState.vodMediaSeqVideo = await this._sessionState.set("vodMediaSeqVideo", 0);
package/engine/util.js CHANGED
@@ -214,6 +214,16 @@ const fetchWithRetry = async (uri, opts, maxRetries, retryDelayMs, timeoutMs, de
214
214
  }
215
215
  };
216
216
 
217
+ const codecsFromString = (codecs) => {
218
+ const audioCodecs = codecs.split(",").find(c => {
219
+ return c.match(/^mp4a/) || c.match(/^ac-3/) || c.match(/^ec-3/);
220
+ });
221
+ const videoCodecs = codecs.split(",").find(c => {
222
+ return c.match(/^avc/) || c.match(/^hevc/) || c.match(/^dvh/);
223
+ });
224
+ return [videoCodecs, audioCodecs];
225
+ };
226
+
217
227
  module.exports = {
218
228
  filterQueryParser,
219
229
  applyFilter,
@@ -225,5 +235,6 @@ module.exports = {
225
235
  WaitTimeGenerator,
226
236
  findNearestValue,
227
237
  isValidUrl,
228
- fetchWithRetry
238
+ fetchWithRetry,
239
+ codecsFromString
229
240
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyevinn-channel-engine",
3
- "version": "4.0.7",
3
+ "version": "4.0.9",
4
4
  "description": "OTT TV Channel Engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "@eyevinn/hls-repeat": "^0.2.0",
28
28
  "@eyevinn/hls-truncate": "^0.2.0",
29
- "@eyevinn/hls-vodtolive": "^2.2.1",
29
+ "@eyevinn/hls-vodtolive": "^2.3.3",
30
30
  "@eyevinn/m3u8": "^0.5.3",
31
31
  "abort-controller": "^3.0.0",
32
32
  "debug": "^3.2.7",
package/server-demux.ts CHANGED
@@ -91,8 +91,8 @@ class RefChannelManager implements IChannelManager {
91
91
  }
92
92
  _getAudioTracks(): AudioTracks[] {
93
93
  return [
94
- { language: "en", name: "English" },
95
- { language: "sp", name: "Spanish" }
94
+ { language: "en", name: "English", default: true },
95
+ { language: "sp", name: "Spanish", default: false }
96
96
  ];
97
97
  }
98
98
  }
@@ -69,20 +69,20 @@ class RefChannelManager implements IChannelManager {
69
69
 
70
70
  _getProfile(): ChannelProfile[] {
71
71
  return [
72
- { resolution: [640, 360], bw: 3663471, codecs: "avc1.64001F,mp4a.40.2" },
73
- { resolution: [1280, 720], bw: 5841380, codecs: "avc1.64001F,mp4a.40.2" },
74
- { resolution: [1920, 1080], bw: 8973571, codecs: "avc1.64001F,mp4a.40.2" },
72
+ { resolution: [640, 360], bw: 3663471, codecs: "avc1.64001F,mp4a.40.2", channels: "2" },
73
+ { resolution: [1280, 720], bw: 5841380, codecs: "avc1.64001F,mp4a.40.2", channels: "2" },
74
+ { resolution: [1920, 1080], bw: 8973571, codecs: "avc1.64001F,mp4a.40.2", channels: "2" },
75
75
 
76
- { resolution: [640, 360], bw: 4301519, codecs: "avc1.64001F,ec-3" },
77
- { resolution: [1280, 720], bw: 6479428, codecs: "avc1.64001F,ec-3" },
78
- { resolution: [1920, 1080], bw: 9611619, codecs: "avc1.640032,ec-3" },
76
+ { resolution: [640, 360], bw: 4301519, codecs: "avc1.64001F,ec-3", channels: "16/JOC" },
77
+ { resolution: [1280, 720], bw: 6479428, codecs: "avc1.64001F,ec-3", channels: "16/JOC" },
78
+ { resolution: [1920, 1080], bw: 9611619, codecs: "avc1.640032,ec-3", channels: "16/JOC" },
79
79
  ];
80
80
  }
81
81
 
82
82
  _getAudioTracks(): AudioTracks[] {
83
83
  return [
84
- { language: "ja", "name": "日本語 stereo", channels: 2, codecs: "mp4a.40.2", default: true },
85
- { language: "ja", "name": "日本語 surround", channels: 16, codecs: "ec-3", default: false }
84
+ { language: "ja", "name": "日本語", default: true },
85
+ { language: "en", "name": "English", default: false }
86
86
  ];
87
87
  }
88
88
  }
@@ -101,4 +101,4 @@ const engineOptions: ChannelEngineOpts = {
101
101
 
102
102
  const engine = new ChannelEngine(refAssetManager, engineOptions);
103
103
  engine.start();
104
- engine.listen(process.env.PORT || 8000);
104
+ engine.listen(process.env.PORT || 8000);