eyevinn-channel-engine 4.0.6 → 4.0.8

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.
@@ -58,6 +58,7 @@ export interface ChannelProfile {
58
58
  bw: number;
59
59
  codecs: string;
60
60
  resolution: number[];
61
+ channels?: string;
61
62
  }
62
63
  export interface Channel {
63
64
  id: string;
@@ -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) {
@@ -1346,10 +1346,10 @@ var Session = /** @class */ (function () {
1346
1346
  };
1347
1347
  Session.prototype.getMasterManifestAsync = function (filter) {
1348
1348
  return __awaiter(this, void 0, void 0, function () {
1349
- var m3u8, currentVod, audioGroupIds, defaultAudioGroupId, hasClosedCaptions, i, audioGroupId, j, audioTrack, sessionProfile;
1349
+ var m3u8, currentVod, audioGroupIds, defaultAudioGroupId, hasClosedCaptions, i, audioGroupId, j, audioTrack, _a, _, channels, sessionProfile;
1350
1350
  var _this = this;
1351
- return __generator(this, function (_a) {
1352
- switch (_a.label) {
1351
+ return __generator(this, function (_b) {
1352
+ switch (_b.label) {
1353
1353
  case 0:
1354
1354
  m3u8 = "#EXTM3U\n";
1355
1355
  m3u8 += "#EXT-X-VERSION:4\n";
@@ -1360,7 +1360,7 @@ var Session = /** @class */ (function () {
1360
1360
  }
1361
1361
  return [4 /*yield*/, this._sessionState.getCurrentVod()];
1362
1362
  case 1:
1363
- currentVod = _a.sent();
1363
+ currentVod = _b.sent();
1364
1364
  if (!currentVod) {
1365
1365
  throw new Error('Session not ready');
1366
1366
  }
@@ -1378,13 +1378,16 @@ var Session = /** @class */ (function () {
1378
1378
  audioGroupId = audioGroupIds[i];
1379
1379
  for (j = 0; j < this._audioTracks.length; j++) {
1380
1380
  audioTrack = this._audioTracks[j];
1381
+ _a = currentVod.getAudioCodecsAndChannelsForGroupId(audioGroupId), _ = _a[0], channels = _a[1];
1381
1382
  // Make default track if set property is true.
1382
- if (audioTrack.default) {
1383
- m3u8 += "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"".concat(audioGroupId, "\",LANGUAGE=\"").concat(audioTrack.language, "\", NAME=\"").concat(audioTrack.name, "\",AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"2\",URI=\"master-").concat(audioGroupId, "_").concat(audioTrack.language, ".m3u8;session=").concat(this._sessionId, "\"\n");
1384
- }
1385
- else {
1386
- m3u8 += "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"".concat(audioGroupId, "\",LANGUAGE=\"").concat(audioTrack.language, "\", NAME=\"").concat(audioTrack.name, "\",AUTOSELECT=YES,DEFAULT=NO,CHANNELS=\"2\",URI=\"master-").concat(audioGroupId, "_").concat(audioTrack.language, ".m3u8;session=").concat(this._sessionId, "\"\n");
1387
- }
1383
+ m3u8 += "#EXT-X-MEDIA:TYPE=AUDIO" +
1384
+ ",GROUP-ID=\"".concat(audioGroupId, "\"") +
1385
+ ",LANGUAGE=\"".concat(audioTrack.language, "\"") +
1386
+ ",NAME=\"".concat(audioTrack.name, "\"") +
1387
+ ",AUTOSELECT=YES,DEFAULT=".concat(audioTrack.default ? 'YES' : 'NO') +
1388
+ ",CHANNELS=\"".concat(channels ? channels : 2, "\"") +
1389
+ ",URI=\"master-".concat(audioGroupId, "_").concat(audioTrack.language, ".m3u8;session=").concat(this._sessionId, "\"") +
1390
+ "\n";
1388
1391
  }
1389
1392
  }
1390
1393
  // As of now, by default set StreamItem's AUDIO attribute to <first audio group-id>
@@ -1394,13 +1397,41 @@ var Session = /** @class */ (function () {
1394
1397
  if (this._sessionProfile) {
1395
1398
  sessionProfile = filter ? applyFilter(this._sessionProfile, filter) : this._sessionProfile;
1396
1399
  sessionProfile.forEach(function (profile) {
1397
- 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';
1398
- m3u8 += "master" + profile.bw + ".m3u8;session=" + _this._sessionId + "\n";
1400
+ if (_this.use_demuxed_audio) {
1401
+ // find matching audio group based on codec in stream
1402
+ var audioGroupIdToUse = void 0;
1403
+ var _a = codecsFromString(profile.codecs), _ = _a[0], audioCodec = _a[1];
1404
+ if (audioCodec) {
1405
+ var profileChannels = profile.channels ? profile.channels : "2";
1406
+ audioGroupIdToUse = currentVod.getAudioGroupIdForCodecs(audioCodec, profileChannels);
1407
+ }
1408
+ // skip stream if no corresponding audio group can be found
1409
+ if (audioGroupIdToUse) {
1410
+ m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw +
1411
+ ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] +
1412
+ ',CODECS="' + profile.codecs + '"' +
1413
+ ",AUDIO=\"".concat(audioGroupIdToUse, "\"") +
1414
+ (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1415
+ m3u8 += "master" + profile.bw + ".m3u8;session=" + _this._sessionId + "\n";
1416
+ }
1417
+ }
1418
+ else {
1419
+ m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw +
1420
+ ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] +
1421
+ ',CODECS="' + profile.codecs + '"' +
1422
+ (defaultAudioGroupId ? ",AUDIO=\"".concat(defaultAudioGroupId, "\"") : '') +
1423
+ (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1424
+ m3u8 += "master" + profile.bw + ".m3u8;session=" + _this._sessionId + "\n";
1425
+ }
1399
1426
  });
1400
1427
  }
1401
1428
  else {
1402
1429
  currentVod.getUsageProfiles().forEach(function (profile) {
1403
- m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + ',RESOLUTION=' + profile.resolution + ',CODECS="' + profile.codecs + '"' + (defaultAudioGroupId ? ",AUDIO=\"".concat(defaultAudioGroupId, "\"") : '') + (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1430
+ m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw +
1431
+ ',RESOLUTION=' + profile.resolution +
1432
+ ',CODECS="' + profile.codecs + '"' +
1433
+ (defaultAudioGroupId ? ",AUDIO=\"".concat(defaultAudioGroupId, "\"") : '') +
1434
+ (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1404
1435
  m3u8 += "master" + profile.bw + ".m3u8;session=" + _this._sessionId + "\n";
1405
1436
  });
1406
1437
  }
@@ -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.6",
3
+ "version": "4.0.8",
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;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ /*
3
+ * Reference implementation of Channel Engine library
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ var index_1 = require("./index");
7
+ var RefAssetManager = /** @class */ (function () {
8
+ function RefAssetManager(opts) {
9
+ this.assets = {
10
+ 1: [
11
+ {
12
+ id: 1,
13
+ title: "Sollevante",
14
+ uri: "https://testcontent.eyevinn.technology/dolby/index.m3u8",
15
+ }
16
+ ],
17
+ };
18
+ this.pos = {
19
+ 1: 0,
20
+ };
21
+ }
22
+ /**
23
+ *
24
+ * @param {Object} vodRequest
25
+ * {
26
+ * sessionId,
27
+ * category,
28
+ * playlistId
29
+ * }
30
+ */
31
+ RefAssetManager.prototype.getNextVod = function (vodRequest) {
32
+ var _this = this;
33
+ console.log(this.assets);
34
+ return new Promise(function (resolve, reject) {
35
+ var channelId = vodRequest.playlistId;
36
+ if (_this.assets[channelId]) {
37
+ var vod = _this.assets[channelId][_this.pos[channelId]++];
38
+ if (_this.pos[channelId] > _this.assets[channelId].length - 1) {
39
+ _this.pos[channelId] = 0;
40
+ }
41
+ var vodResponse = {
42
+ id: vod.id,
43
+ title: vod.title,
44
+ uri: vod.uri,
45
+ };
46
+ resolve(vodResponse);
47
+ }
48
+ else {
49
+ reject("Invalid channelId provided");
50
+ }
51
+ });
52
+ };
53
+ return RefAssetManager;
54
+ }());
55
+ var RefChannelManager = /** @class */ (function () {
56
+ function RefChannelManager() {
57
+ }
58
+ RefChannelManager.prototype.getChannels = function () {
59
+ //return [ { id: '1', profile: this._getProfile() }, { id: 'faulty', profile: this._getProfile() } ];
60
+ return [{
61
+ id: "1",
62
+ profile: this._getProfile(),
63
+ audioTracks: this._getAudioTracks(),
64
+ }];
65
+ };
66
+ RefChannelManager.prototype._getProfile = function () {
67
+ return [
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
+ ];
75
+ };
76
+ RefChannelManager.prototype._getAudioTracks = function () {
77
+ return [
78
+ { language: "ja", "name": "日本語", default: true },
79
+ { language: "en", "name": "English", default: false }
80
+ ];
81
+ };
82
+ return RefChannelManager;
83
+ }());
84
+ var refAssetManager = new RefAssetManager();
85
+ var refChannelManager = new RefChannelManager();
86
+ var engineOptions = {
87
+ heartbeat: "/",
88
+ useDemuxedAudio: true,
89
+ averageSegmentDuration: 2000,
90
+ channelManager: refChannelManager,
91
+ slateRepetitions: 10,
92
+ redisUrl: process.env.REDIS_URL,
93
+ };
94
+ var engine = new index_1.ChannelEngine(refAssetManager, engineOptions);
95
+ engine.start();
96
+ engine.listen(process.env.PORT || 8000);
package/engine/server.ts CHANGED
@@ -99,6 +99,7 @@ export interface ChannelProfile {
99
99
  bw: number;
100
100
  codecs: string;
101
101
  resolution: number[];
102
+ channels?: string;
102
103
  }
103
104
 
104
105
  export interface Channel {
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,
@@ -959,12 +959,16 @@ class Session {
959
959
  let audioGroupId = audioGroupIds[i];
960
960
  for (let j = 0; j < this._audioTracks.length; j++) {
961
961
  let audioTrack = this._audioTracks[j];
962
+ const [_, channels] = currentVod.getAudioCodecsAndChannelsForGroupId(audioGroupId);
962
963
  // Make default track if set property is true.
963
- if (audioTrack.default) {
964
- m3u8 += `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="${audioGroupId}",LANGUAGE="${audioTrack.language}", NAME="${audioTrack.name}",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2",URI="master-${audioGroupId}_${audioTrack.language}.m3u8;session=${this._sessionId}"\n`;
965
- } else {
966
- m3u8 += `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="${audioGroupId}",LANGUAGE="${audioTrack.language}", NAME="${audioTrack.name}",AUTOSELECT=YES,DEFAULT=NO,CHANNELS="2",URI="master-${audioGroupId}_${audioTrack.language}.m3u8;session=${this._sessionId}"\n`;
967
- }
964
+ m3u8 += `#EXT-X-MEDIA:TYPE=AUDIO` +
965
+ `,GROUP-ID="${audioGroupId}"` +
966
+ `,LANGUAGE="${audioTrack.language}"` +
967
+ `,NAME="${audioTrack.name}"` +
968
+ `,AUTOSELECT=YES,DEFAULT=${audioTrack.default ? 'YES' : 'NO'}` +
969
+ `,CHANNELS="${channels ? channels : 2}"` +
970
+ `,URI="master-${audioGroupId}_${audioTrack.language}.m3u8;session=${this._sessionId}"` +
971
+ "\n";
968
972
  }
969
973
  }
970
974
  // As of now, by default set StreamItem's AUDIO attribute to <first audio group-id>
@@ -974,12 +978,40 @@ class Session {
974
978
  if (this._sessionProfile) {
975
979
  const sessionProfile = filter ? applyFilter(this._sessionProfile, filter) : this._sessionProfile;
976
980
  sessionProfile.forEach(profile => {
977
- 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';
978
- m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
981
+ if (this.use_demuxed_audio) {
982
+ // find matching audio group based on codec in stream
983
+ let audioGroupIdToUse;
984
+ const [_, audioCodec] = codecsFromString(profile.codecs);
985
+ if (audioCodec) {
986
+ const profileChannels = profile.channels ? profile.channels : "2";
987
+ audioGroupIdToUse = currentVod.getAudioGroupIdForCodecs(audioCodec, profileChannels);
988
+ }
989
+
990
+ // skip stream if no corresponding audio group can be found
991
+ if (audioGroupIdToUse) {
992
+ m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw +
993
+ ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] +
994
+ ',CODECS="' + profile.codecs + '"' +
995
+ `,AUDIO="${audioGroupIdToUse}"` +
996
+ (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
997
+ m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
998
+ }
999
+ } else {
1000
+ m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw +
1001
+ ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] +
1002
+ ',CODECS="' + profile.codecs + '"' +
1003
+ (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') +
1004
+ (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1005
+ m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
1006
+ }
979
1007
  });
980
1008
  } else {
981
1009
  currentVod.getUsageProfiles().forEach(profile => {
982
- m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + ',RESOLUTION=' + profile.resolution + ',CODECS="' + profile.codecs + '"' + (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') + (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1010
+ m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw +
1011
+ ',RESOLUTION=' + profile.resolution +
1012
+ ',CODECS="' + profile.codecs + '"' +
1013
+ (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') +
1014
+ (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
983
1015
  m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
984
1016
  });
985
1017
  }
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.6",
3
+ "version": "4.0.8",
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
  }
@@ -0,0 +1,104 @@
1
+ /*
2
+ * Reference implementation of Channel Engine library
3
+ */
4
+
5
+ import { ChannelEngine, ChannelEngineOpts,
6
+ IAssetManager, IChannelManager,
7
+ VodRequest, VodResponse, Channel, ChannelProfile,
8
+ AudioTracks
9
+ } from "./index";
10
+
11
+ class RefAssetManager implements IAssetManager {
12
+ private assets;
13
+ private pos;
14
+ constructor(opts?) {
15
+ this.assets = {
16
+ 1: [
17
+ {
18
+ id: 1,
19
+ title: "Sollevante",
20
+ uri: "https://testcontent.eyevinn.technology/dolby/index.m3u8",
21
+ }
22
+ ],
23
+ };
24
+ this.pos = {
25
+ 1: 0,
26
+ };
27
+ }
28
+
29
+ /**
30
+ *
31
+ * @param {Object} vodRequest
32
+ * {
33
+ * sessionId,
34
+ * category,
35
+ * playlistId
36
+ * }
37
+ */
38
+ getNextVod(vodRequest: VodRequest): Promise<VodResponse> {
39
+ console.log(this.assets);
40
+ return new Promise((resolve, reject) => {
41
+ const channelId = vodRequest.playlistId;
42
+ if (this.assets[channelId]) {
43
+ let vod = this.assets[channelId][this.pos[channelId]++];
44
+ if (this.pos[channelId] > this.assets[channelId].length - 1) {
45
+ this.pos[channelId] = 0;
46
+ }
47
+ const vodResponse = {
48
+ id: vod.id,
49
+ title: vod.title,
50
+ uri: vod.uri,
51
+ };
52
+ resolve(vodResponse);
53
+ } else {
54
+ reject("Invalid channelId provided");
55
+ }
56
+ });
57
+ }
58
+ }
59
+
60
+ class RefChannelManager implements IChannelManager {
61
+ getChannels(): Channel[] {
62
+ //return [ { id: '1', profile: this._getProfile() }, { id: 'faulty', profile: this._getProfile() } ];
63
+ return [{
64
+ id: "1",
65
+ profile: this._getProfile(),
66
+ audioTracks: this._getAudioTracks(),
67
+ }];
68
+ }
69
+
70
+ _getProfile(): ChannelProfile[] {
71
+ return [
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
+
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
+ ];
80
+ }
81
+
82
+ _getAudioTracks(): AudioTracks[] {
83
+ return [
84
+ { language: "ja", "name": "日本語", default: true },
85
+ { language: "en", "name": "English", default: false }
86
+ ];
87
+ }
88
+ }
89
+
90
+ const refAssetManager = new RefAssetManager();
91
+ const refChannelManager = new RefChannelManager();
92
+
93
+ const engineOptions: ChannelEngineOpts = {
94
+ heartbeat: "/",
95
+ useDemuxedAudio: true,
96
+ averageSegmentDuration: 2000,
97
+ channelManager: refChannelManager,
98
+ slateRepetitions: 10,
99
+ redisUrl: process.env.REDIS_URL,
100
+ };
101
+
102
+ const engine = new ChannelEngine(refAssetManager, engineOptions);
103
+ engine.start();
104
+ engine.listen(process.env.PORT || 8000);
package/tsconfig.json CHANGED
@@ -6,5 +6,5 @@
6
6
  "esModuleInterop": true,
7
7
  "declaration": true
8
8
  },
9
- "include": ["./server.ts", "./server-demux.ts", "./server-livemix.ts", "./engine/**/*"]
9
+ "include": ["./server.ts", "./server-demux.ts", "./server-livemix.ts", "./server-multicodec.ts", "./engine/**/*"]
10
10
  }