eyevinn-channel-engine 4.0.20 → 4.1.1

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.
@@ -9,7 +9,7 @@ jobs:
9
9
 
10
10
  strategy:
11
11
  matrix:
12
- node-version: [14.x, 16.x]
12
+ node-version: [14.x, 16.x, 18.x]
13
13
 
14
14
  steps:
15
15
  - uses: actions/checkout@v3
@@ -2,7 +2,7 @@ name: Publish
2
2
 
3
3
  on:
4
4
  release:
5
- types: [created]
5
+ types: [released]
6
6
 
7
7
  jobs:
8
8
  build:
@@ -0,0 +1,22 @@
1
+ name: Publish release candidate
2
+
3
+ on:
4
+ release:
5
+ types: [prereleased]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+
11
+ steps:
12
+ - uses: actions/checkout@v1
13
+ - uses: actions/setup-node@v1
14
+ with:
15
+ node-version: 16
16
+ registry-url: https://registry.npmjs.org/
17
+ - run: |
18
+ npm install
19
+ npm run build
20
+ npm publish --tag rc
21
+ env:
22
+ NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
package/README.md CHANGED
@@ -35,6 +35,7 @@ A live demo and example is available here: [https://vod2live.eyevinn.technology]
35
35
  - Mix VOD2Live channel with a "real" live HLS stream
36
36
  - Develop adapters to plugin with custom scheduling endpoints
37
37
  - High Availability with Redis Cache as shared state store
38
+ - Support for DRM encrypted HLS VODs (in beta)
38
39
  - And much more!
39
40
 
40
41
  ## System Requirements
@@ -45,7 +46,7 @@ Supported Node.js Versions
45
46
  | ------- | --------- |
46
47
  | 14.x | Yes |
47
48
  | 16.x | Yes |
48
- | 18.x | No |
49
+ | 18.x | Yes |
49
50
 
50
51
  ## Supported Source Formats
51
52
 
@@ -97,14 +98,7 @@ Read [CONTRIBUTING](CONTRIBUTING.md) first and then follow the instructions belo
97
98
 
98
99
  ### Development
99
100
 
100
- Ensure that you are running node version 16 as version 18 is not supported yet.
101
-
102
- ```
103
- node --version
104
- v16.17.0
105
- ```
106
-
107
- Then install the dependencies:
101
+ Install the dependencies:
108
102
 
109
103
  ```
110
104
  npm install
@@ -138,6 +132,7 @@ In addition there other reference implemetations that can be used:
138
132
  - `server-demux.ts` : example with demuxed audio
139
133
  - `server-livemix.ts` : example with live mimxing
140
134
  - `server-multicodec.ts` : example with multicodec
135
+ - `server-drm.ts` : example with DRM
141
136
 
142
137
  ### Source linking hls-vodtolive library
143
138
 
@@ -952,7 +952,7 @@ var Session = /** @class */ (function () {
952
952
  };
953
953
  Session.prototype.incrementAsync = function () {
954
954
  return __awaiter(this, void 0, void 0, function () {
955
- var isLeader, sessionState, playheadState, currentVod, _a, leaderAction, _b, m3u8_3, _c, audioIncrement, positionV, positionA, position, currentVod_1, sessionState_1, posDiff, incrementValue, index, audioSeqLastIdx, thresh, audioPosition, _d, _e, _f, _g, _h, _j, _k, _l, _m, mediaSequenceValue, diffMs, m3u8;
955
+ var isLeader, sessionState, playheadState, currentVod, _a, leaderAction, _b, m3u8_3, _c, audioIncrement, positionV, positionA, position, currentVod_1, sessionState_1, posDiff, incrementValue, index, audioSeqLastIdx, thresh, audioPosition, _d, _e, _f, _g, _h, _j, _k, _l, _m, mediaSequenceValue, audioInfo, mediaSequenceValueAudio, diffMs, m3u8;
956
956
  return __generator(this, function (_o) {
957
957
  switch (_o.label) {
958
958
  case 0: return [4 /*yield*/, this._tickAsync()];
@@ -1121,7 +1121,12 @@ var Session = /** @class */ (function () {
1121
1121
  _m.vodMediaSeqAudio = _o.sent();
1122
1122
  if (currentVod.sequenceAlwaysContainNewSegments) {
1123
1123
  mediaSequenceValue = currentVod.mediaSequenceValues[playheadState.vodMediaSeqVideo];
1124
- debug("[".concat(this._sessionId, "]: Session can now serve mseq={").concat(playheadState.mediaSeq + mediaSequenceValue, "}"));
1124
+ audioInfo = "";
1125
+ if (this.use_demuxed_audio) {
1126
+ mediaSequenceValueAudio = currentVod.mediaSequenceValuesAudio[playheadState.vodMediaSeqAudio];
1127
+ audioInfo = " mseq[A]={".concat(playheadState.mediaSeqAudio + mediaSequenceValueAudio, "}");
1128
+ }
1129
+ debug("[".concat(this._sessionId, "]: Session can now serve mseq[V]={").concat(playheadState.mediaSeq + mediaSequenceValue, "}") + audioInfo);
1125
1130
  }
1126
1131
  debug("[".concat(this._sessionId, "]: INCREMENT (mseq=").concat(playheadState.mediaSeq + playheadState.vodMediaSeqVideo, ") vodMediaSeq=(").concat(playheadState.vodMediaSeqVideo, "_").concat(playheadState.vodMediaSeqAudio, " of ").concat(currentVod.getLiveMediaSequencesCount(), "_").concat(currentVod.getLiveMediaSequencesCount("audio"), ")"));
1127
1132
  if (!(playheadState.vodMediaSeqVideo < 2 || playheadState.mediaSeq !== this.prevMediaSeqOffset.video)) return [3 /*break*/, 41];
@@ -1403,7 +1408,7 @@ var Session = /** @class */ (function () {
1403
1408
  ",NAME=\"".concat(audioTrack.name, "\"") +
1404
1409
  ",AUTOSELECT=YES,DEFAULT=".concat(audioTrack.default ? 'YES' : 'NO') +
1405
1410
  ",CHANNELS=\"".concat(channels ? channels : 2, "\"") +
1406
- ",URI=\"master-".concat(audioGroupIdFileName, "_").concat(audioTrack.language, ".m3u8;session=").concat(this._sessionId, "\"") +
1411
+ ",URI=\"master-".concat(audioGroupIdFileName, "_").concat(audioTrack.language, ".m3u8%3Bsession=").concat(this._sessionId, "\"") +
1407
1412
  "\n";
1408
1413
  }
1409
1414
  }
@@ -1433,7 +1438,7 @@ var Session = /** @class */ (function () {
1433
1438
  ',CODECS="' + profile.codecs + '"' +
1434
1439
  ",AUDIO=\"".concat(audioGroupIdToUse, "\"") +
1435
1440
  (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1436
- m3u8 += "master" + profile.bw + ".m3u8;session=" + _this._sessionId + "\n";
1441
+ m3u8 += "master" + profile.bw + ".m3u8%3Bsession=" + _this._sessionId + "\n";
1437
1442
  }
1438
1443
  }
1439
1444
  else {
@@ -1442,7 +1447,7 @@ var Session = /** @class */ (function () {
1442
1447
  ',CODECS="' + profile.codecs + '"' +
1443
1448
  (defaultAudioGroupId ? ",AUDIO=\"".concat(defaultAudioGroupId, "\"") : '') +
1444
1449
  (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1445
- m3u8 += "master" + profile.bw + ".m3u8;session=" + _this._sessionId + "\n";
1450
+ m3u8 += "master" + profile.bw + ".m3u8%3Bsession=" + _this._sessionId + "\n";
1446
1451
  }
1447
1452
  });
1448
1453
  }
@@ -1453,7 +1458,7 @@ var Session = /** @class */ (function () {
1453
1458
  ',CODECS="' + profile.codecs + '"' +
1454
1459
  (defaultAudioGroupId ? ",AUDIO=\"".concat(defaultAudioGroupId, "\"") : '') +
1455
1460
  (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1456
- m3u8 += "master" + profile.bw + ".m3u8;session=" + _this._sessionId + "\n";
1461
+ m3u8 += "master" + profile.bw + ".m3u8%3Bsession=" + _this._sessionId + "\n";
1457
1462
  });
1458
1463
  }
1459
1464
  this.produceEvent({
@@ -52,6 +52,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
52
52
  var HLSVod = require('@eyevinn/hls-vodtolive');
53
53
  var SharedStateStore = require('./shared_state_store.js');
54
54
  var debug = require("debug")("session-state-store");
55
+ var timeLeft = require('./util.js').timeLeft;
55
56
  var SessionState = Object.freeze({
56
57
  VOD_INIT: 1,
57
58
  VOD_PLAYING: 2,
@@ -99,7 +100,7 @@ var SharedSessionState = /** @class */ (function () {
99
100
  throw new Error("shared session state store has not been initialized");
100
101
  }
101
102
  if (this.cache.currentVod.value && Date.now() < this.cache.currentVod.ts + this.cache.currentVod.ttl) {
102
- debug("[".concat(this.sessionId, "]: reading 'currentVod' from cache"));
103
+ debug("[".concat(this.sessionId, "]: reading 'currentVod' from cache. Expires in ").concat(timeLeft(this.cache.currentVod.ts + this.cache.currentVod.ttl, Date.now())));
103
104
  return [2 /*return*/, this.cache.currentVod.value];
104
105
  }
105
106
  return [4 /*yield*/, this.get("currentVod")];
@@ -18,3 +18,4 @@ 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
20
  export function codecsFromString(codecs: any): any[];
21
+ export function timeLeft(endTimestamp: any, currentTimestamp: any): string;
@@ -275,6 +275,26 @@ var codecsFromString = function (codecs) {
275
275
  });
276
276
  return [videoCodecs, audioCodecs];
277
277
  };
278
+ var timeLeft = function (endTimestamp, currentTimestamp) {
279
+ var secondsLeft = Math.floor((endTimestamp - currentTimestamp) / 1000);
280
+ if (secondsLeft < 0) {
281
+ return "Time has already passed.";
282
+ }
283
+ var hours = Math.floor(secondsLeft / 3600);
284
+ var minutes = Math.floor((secondsLeft % 3600) / 60);
285
+ var seconds = secondsLeft % 60;
286
+ var msg = "";
287
+ if (hours) {
288
+ msg = "".concat(hours, "h").concat(minutes, "m").concat(seconds, "s");
289
+ }
290
+ else if (minutes) {
291
+ msg = "".concat(minutes, "m").concat(seconds, "s");
292
+ }
293
+ else {
294
+ msg = "".concat(seconds, "s");
295
+ }
296
+ return msg;
297
+ };
278
298
  module.exports = {
279
299
  filterQueryParser: filterQueryParser,
280
300
  applyFilter: applyFilter,
@@ -287,5 +307,6 @@ module.exports = {
287
307
  findNearestValue: findNearestValue,
288
308
  isValidUrl: isValidUrl,
289
309
  fetchWithRetry: fetchWithRetry,
290
- codecsFromString: codecsFromString
310
+ codecsFromString: codecsFromString,
311
+ timeLeft: timeLeft,
291
312
  };
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyevinn-channel-engine",
3
- "version": "4.0.20",
3
+ "version": "4.1.1",
4
4
  "description": "OTT TV Channel Engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,13 +8,14 @@
8
8
  "build": "tsc --project ./ && cp ./package.json dist/",
9
9
  "debug-server": "DEBUG=engine-*,vod-to-live node dist/server.js",
10
10
  "test": "jasmine",
11
+ "version:rc": "npm version prerelease --preid=rc",
11
12
  "postversion": "git push && git push --tags",
12
13
  "start": "node dist/server.js",
13
14
  "start-demux": "node dist/server-demux.js",
14
15
  "start-livemix": "node dist/server-livemix.js"
15
16
  },
16
17
  "engines": {
17
- "node": "<18"
18
+ "node": ">=14 <20"
18
19
  },
19
20
  "author": "Eyevinn Technology AB <work@eyevinn.se>",
20
21
  "contributors": [
@@ -29,7 +30,7 @@
29
30
  "dependencies": {
30
31
  "@eyevinn/hls-repeat": "^0.2.0",
31
32
  "@eyevinn/hls-truncate": "^0.2.0",
32
- "@eyevinn/hls-vodtolive": "^2.3.10",
33
+ "@eyevinn/hls-vodtolive": "^2.3.12",
33
34
  "@eyevinn/m3u8": "^0.5.3",
34
35
  "abort-controller": "^3.0.0",
35
36
  "debug": "^3.2.7",
@@ -39,7 +40,7 @@
39
40
  "promise.allsettled": "^1.0.4",
40
41
  "redis": "3.1.0",
41
42
  "request": ">=2.88.0",
42
- "restify": "9.0.0",
43
+ "restify": "10.0.0",
43
44
  "restify-errors": "^8.0.2",
44
45
  "uuid": "^8.3.2"
45
46
  },
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ /*
3
+ * Reference implementation of Channel Engine library using DRM (HLS+Widevine) VOD assets.
4
+ *
5
+ * Playback: https://shaka-player-demo.appspot.com/demo/#audiolang=sv-SE;textlang=sv-SE;uilang=sv-SE;asset=http://localhost:8000/channels/1/master.m3u8;license=https://cwip-shaka-proxy.appspot.com/no_auth;panel=CUSTOM%20CONTENT;build=uncompiled
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ var index_1 = require("./index");
9
+ var RefAssetManager = /** @class */ (function () {
10
+ function RefAssetManager(opts) {
11
+ this.assets = {
12
+ 1: [
13
+ {
14
+ id: 1,
15
+ title: "VINN DRM",
16
+ uri: "https://testcontent.eyevinn.technology/vinn/multidrm/index.m3u8"
17
+ // License server urL: https://widevine-dash.ezdrm.com/proxy?pX=1D331C
18
+ },
19
+ {
20
+ id: 2,
21
+ title: "VINN No DRM",
22
+ uri: "https://testcontent.eyevinn.technology/vinn/cmaf/index.m3u8"
23
+ },
24
+ {
25
+ id: 3,
26
+ title: "CE Promo DRM",
27
+ uri: "https://testcontent.eyevinn.technology/drm/CE-promo/index.m3u8"
28
+ // License server urL: https://widevine-dash.ezdrm.com/proxy?pX=1D331C
29
+ },
30
+ {
31
+ id: 4,
32
+ title: "Eyevinn Reel DRM",
33
+ uri: "https://testcontent.eyevinn.technology/drm/Eyevinn-Reel/index.m3u8"
34
+ // License server urL: https://widevine-dash.ezdrm.com/proxy?pX=1D331C
35
+ }
36
+ ],
37
+ };
38
+ this.pos = {
39
+ 1: 0,
40
+ };
41
+ }
42
+ /**
43
+ *
44
+ * @param {Object} vodRequest
45
+ * {
46
+ * sessionId,
47
+ * category,
48
+ * playlistId
49
+ * }
50
+ */
51
+ RefAssetManager.prototype.getNextVod = function (vodRequest) {
52
+ var _this = this;
53
+ return new Promise(function (resolve, reject) {
54
+ var channelId = vodRequest.playlistId;
55
+ if (_this.assets[channelId]) {
56
+ var vod = _this.assets[channelId][_this.pos[channelId]++];
57
+ if (_this.pos[channelId] > _this.assets[channelId].length - 1) {
58
+ _this.pos[channelId] = 0;
59
+ }
60
+ resolve(vod);
61
+ }
62
+ else {
63
+ reject("Invalid channelId provided");
64
+ }
65
+ });
66
+ };
67
+ return RefAssetManager;
68
+ }());
69
+ var RefChannelManager = /** @class */ (function () {
70
+ function RefChannelManager() {
71
+ }
72
+ RefChannelManager.prototype.getChannels = function () {
73
+ return [{ id: "1", profile: this._getProfile(), audioTracks: this._getAudioTracks() }];
74
+ };
75
+ RefChannelManager.prototype._getProfile = function () {
76
+ return [
77
+ {
78
+ bw: 7934000,
79
+ codecs: "avc1.4d001f,mp4a.40.2",
80
+ resolution: [2880, 1440],
81
+ },
82
+ {
83
+ bw: 7514000,
84
+ codecs: "avc1.4d001f,mp4a.40.2",
85
+ resolution: [1920, 1080],
86
+ },
87
+ { bw: 7134000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [1280, 720] },
88
+ { bw: 6134000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [1024, 458] },
89
+ { bw: 2323000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [640, 286] },
90
+ { bw: 495894, codecs: "avc1.4d001f,mp4a.40.2", resolution: [480, 214] },
91
+ ];
92
+ };
93
+ RefChannelManager.prototype._getAudioTracks = function () {
94
+ return [
95
+ { language: "en", name: "English" },
96
+ ];
97
+ };
98
+ return RefChannelManager;
99
+ }());
100
+ var refAssetManager = new RefAssetManager();
101
+ var refChannelManager = new RefChannelManager();
102
+ var engineOptions = {
103
+ heartbeat: "/",
104
+ averageSegmentDuration: 4000,
105
+ channelManager: refChannelManager,
106
+ defaultSlateUri: "https://maitv-vod.lab.eyevinn.technology/slate-consuo.mp4/master.m3u8",
107
+ slateRepetitions: 10,
108
+ redisUrl: process.env.REDIS_URL,
109
+ useDemuxedAudio: true,
110
+ alwaysNewSegments: true,
111
+ };
112
+ var engine = new index_1.ChannelEngine(refAssetManager, engineOptions);
113
+ engine.start();
114
+ engine.listen(process.env.PORT || 8000);
package/docs/demosite.png CHANGED
Binary file
package/engine/session.js CHANGED
@@ -787,7 +787,12 @@ class Session {
787
787
 
788
788
  if (currentVod.sequenceAlwaysContainNewSegments) {
789
789
  const mediaSequenceValue = currentVod.mediaSequenceValues[playheadState.vodMediaSeqVideo];
790
- debug(`[${this._sessionId}]: Session can now serve mseq={${playheadState.mediaSeq + mediaSequenceValue}}`);
790
+ let audioInfo = "";
791
+ if (this.use_demuxed_audio) {
792
+ const mediaSequenceValueAudio = currentVod.mediaSequenceValuesAudio[playheadState.vodMediaSeqAudio];
793
+ audioInfo = ` mseq[A]={${playheadState.mediaSeqAudio + mediaSequenceValueAudio}}`
794
+ }
795
+ debug(`[${this._sessionId}]: Session can now serve mseq[V]={${playheadState.mediaSeq + mediaSequenceValue}}` + audioInfo);
791
796
  }
792
797
 
793
798
  debug(`[${this._sessionId}]: INCREMENT (mseq=${playheadState.mediaSeq + playheadState.vodMediaSeqVideo}) vodMediaSeq=(${playheadState.vodMediaSeqVideo}_${playheadState.vodMediaSeqAudio} of ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")})`);
@@ -983,7 +988,7 @@ class Session {
983
988
  `,NAME="${audioTrack.name}"` +
984
989
  `,AUTOSELECT=YES,DEFAULT=${audioTrack.default ? 'YES' : 'NO'}` +
985
990
  `,CHANNELS="${channels ? channels : 2}"` +
986
- `,URI="master-${audioGroupIdFileName}_${audioTrack.language}.m3u8;session=${this._sessionId}"` +
991
+ `,URI="master-${audioGroupIdFileName}_${audioTrack.language}.m3u8%3Bsession=${this._sessionId}"` +
987
992
  "\n";
988
993
  }
989
994
  }
@@ -1015,7 +1020,7 @@ class Session {
1015
1020
  ',CODECS="' + profile.codecs + '"' +
1016
1021
  `,AUDIO="${audioGroupIdToUse}"` +
1017
1022
  (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1018
- m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
1023
+ m3u8 += "master" + profile.bw + ".m3u8%3Bsession=" + this._sessionId + "\n";
1019
1024
  }
1020
1025
  } else {
1021
1026
  m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw +
@@ -1023,7 +1028,7 @@ class Session {
1023
1028
  ',CODECS="' + profile.codecs + '"' +
1024
1029
  (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') +
1025
1030
  (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1026
- m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
1031
+ m3u8 += "master" + profile.bw + ".m3u8%3Bsession=" + this._sessionId + "\n";
1027
1032
  }
1028
1033
  });
1029
1034
  } else {
@@ -1033,7 +1038,7 @@ class Session {
1033
1038
  ',CODECS="' + profile.codecs + '"' +
1034
1039
  (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') +
1035
1040
  (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n';
1036
- m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
1041
+ m3u8 += "master" + profile.bw + ".m3u8%3Bsession=" + this._sessionId + "\n";
1037
1042
  });
1038
1043
  }
1039
1044
 
@@ -1,6 +1,7 @@
1
1
  const HLSVod = require('@eyevinn/hls-vodtolive');
2
2
  const SharedStateStore = require('./shared_state_store.js');
3
3
  const debug = require("debug")("session-state-store");
4
+ const { timeLeft } = require('./util.js');
4
5
 
5
6
  const SessionState = Object.freeze({
6
7
  VOD_INIT: 1,
@@ -43,7 +44,12 @@ class SharedSessionState {
43
44
  }
44
45
 
45
46
  if (this.cache.currentVod.value && Date.now() < this.cache.currentVod.ts + this.cache.currentVod.ttl) {
46
- debug(`[${this.sessionId}]: reading 'currentVod' from cache`);
47
+ debug(
48
+ `[${this.sessionId}]: reading 'currentVod' from cache. Expires in ${timeLeft(
49
+ this.cache.currentVod.ts + this.cache.currentVod.ttl,
50
+ Date.now()
51
+ )}`
52
+ );
47
53
  return this.cache.currentVod.value;
48
54
  }
49
55
 
package/engine/util.js CHANGED
@@ -6,7 +6,7 @@ const filterQueryParser = (filterQuery) => {
6
6
  const conditions = filterQuery.match(/\(([^\(\)]*?)\)/g);
7
7
 
8
8
  let filter = {};
9
-
9
+
10
10
  if (!conditions) {
11
11
  return filter;
12
12
  }
@@ -157,7 +157,7 @@ const findNearestValue = (val, array) => {
157
157
  return sorted.reduce((a, b) => {
158
158
  return Math.abs(b - val) < Math.abs(a - val) ? b : a;
159
159
  });
160
- }
160
+ };
161
161
 
162
162
  const isValidUrl = (url) => {
163
163
  try {
@@ -166,8 +166,7 @@ const isValidUrl = (url) => {
166
166
  } catch (err) {
167
167
  return false;
168
168
  }
169
- }
170
-
169
+ };
171
170
 
172
171
  const fetchWithRetry = async (uri, opts, maxRetries, retryDelayMs, timeoutMs, debug) => {
173
172
  if (!debug) {
@@ -224,6 +223,25 @@ const codecsFromString = (codecs) => {
224
223
  return [videoCodecs, audioCodecs];
225
224
  };
226
225
 
226
+ const timeLeft = (endTimestamp, currentTimestamp) => {
227
+ const secondsLeft = Math.floor((endTimestamp - currentTimestamp) / 1000);
228
+ if (secondsLeft < 0) {
229
+ return "Time has already passed.";
230
+ }
231
+ const hours = Math.floor(secondsLeft / 3600);
232
+ const minutes = Math.floor((secondsLeft % 3600) / 60);
233
+ const seconds = secondsLeft % 60;
234
+ let msg = "";
235
+ if (hours) {
236
+ msg = `${hours}h${minutes}m${seconds}s`;
237
+ } else if (minutes) {
238
+ msg = `${minutes}m${seconds}s`;
239
+ } else {
240
+ msg = `${seconds}s`;
241
+ }
242
+ return msg;
243
+ };
244
+
227
245
  module.exports = {
228
246
  filterQueryParser,
229
247
  applyFilter,
@@ -236,5 +254,6 @@ module.exports = {
236
254
  findNearestValue,
237
255
  isValidUrl,
238
256
  fetchWithRetry,
239
- codecsFromString
257
+ codecsFromString,
258
+ timeLeft,
240
259
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyevinn-channel-engine",
3
- "version": "4.0.20",
3
+ "version": "4.1.1",
4
4
  "description": "OTT TV Channel Engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,13 +8,14 @@
8
8
  "build": "tsc --project ./ && cp ./package.json dist/",
9
9
  "debug-server": "DEBUG=engine-*,vod-to-live node dist/server.js",
10
10
  "test": "jasmine",
11
+ "version:rc": "npm version prerelease --preid=rc",
11
12
  "postversion": "git push && git push --tags",
12
13
  "start": "node dist/server.js",
13
14
  "start-demux": "node dist/server-demux.js",
14
15
  "start-livemix": "node dist/server-livemix.js"
15
16
  },
16
17
  "engines": {
17
- "node": "<18"
18
+ "node": ">=14 <20"
18
19
  },
19
20
  "author": "Eyevinn Technology AB <work@eyevinn.se>",
20
21
  "contributors": [
@@ -29,7 +30,7 @@
29
30
  "dependencies": {
30
31
  "@eyevinn/hls-repeat": "^0.2.0",
31
32
  "@eyevinn/hls-truncate": "^0.2.0",
32
- "@eyevinn/hls-vodtolive": "^2.3.10",
33
+ "@eyevinn/hls-vodtolive": "^2.3.12",
33
34
  "@eyevinn/m3u8": "^0.5.3",
34
35
  "abort-controller": "^3.0.0",
35
36
  "debug": "^3.2.7",
@@ -39,7 +40,7 @@
39
40
  "promise.allsettled": "^1.0.4",
40
41
  "redis": "3.1.0",
41
42
  "request": ">=2.88.0",
42
- "restify": "9.0.0",
43
+ "restify": "10.0.0",
43
44
  "restify-errors": "^8.0.2",
44
45
  "uuid": "^8.3.2"
45
46
  },
package/server-drm.ts ADDED
@@ -0,0 +1,126 @@
1
+ /*
2
+ * Reference implementation of Channel Engine library using DRM (HLS+Widevine) VOD assets.
3
+ *
4
+ * Playback: https://shaka-player-demo.appspot.com/demo/#audiolang=sv-SE;textlang=sv-SE;uilang=sv-SE;asset=http://localhost:8000/channels/1/master.m3u8;license=https://cwip-shaka-proxy.appspot.com/no_auth;panel=CUSTOM%20CONTENT;build=uncompiled
5
+ */
6
+
7
+ import {
8
+ ChannelEngine,
9
+ ChannelEngineOpts,
10
+ IAssetManager,
11
+ IChannelManager,
12
+ VodRequest,
13
+ VodResponse,
14
+ Channel,
15
+ ChannelProfile,
16
+ AudioTracks,
17
+ } from "./index";
18
+
19
+ class RefAssetManager implements IAssetManager {
20
+ private assets;
21
+ private pos;
22
+ constructor(opts?) {
23
+ this.assets = {
24
+ 1: [
25
+ {
26
+ id: 1,
27
+ title: "VINN DRM",
28
+ uri: "https://testcontent.eyevinn.technology/vinn/multidrm/index.m3u8"
29
+ // License server urL: https://widevine-dash.ezdrm.com/proxy?pX=1D331C
30
+ },
31
+ {
32
+ id: 2,
33
+ title: "VINN No DRM",
34
+ uri: "https://testcontent.eyevinn.technology/vinn/cmaf/index.m3u8"
35
+ },
36
+ {
37
+ id: 3,
38
+ title: "CE Promo DRM",
39
+ uri: "https://testcontent.eyevinn.technology/drm/CE-promo/index.m3u8"
40
+ // License server urL: https://widevine-dash.ezdrm.com/proxy?pX=1D331C
41
+ },
42
+ {
43
+ id: 4,
44
+ title: "Eyevinn Reel DRM",
45
+ uri: "https://testcontent.eyevinn.technology/drm/Eyevinn-Reel/index.m3u8"
46
+ // License server urL: https://widevine-dash.ezdrm.com/proxy?pX=1D331C
47
+ }
48
+ ],
49
+ };
50
+ this.pos = {
51
+ 1: 0,
52
+ };
53
+ }
54
+
55
+ /**
56
+ *
57
+ * @param {Object} vodRequest
58
+ * {
59
+ * sessionId,
60
+ * category,
61
+ * playlistId
62
+ * }
63
+ */
64
+ getNextVod(vodRequest: VodRequest): Promise<VodResponse> {
65
+ return new Promise((resolve, reject) => {
66
+ const channelId = vodRequest.playlistId;
67
+ if (this.assets[channelId]) {
68
+ let vod: VodResponse = this.assets[channelId][this.pos[channelId]++];
69
+ if (this.pos[channelId] > this.assets[channelId].length - 1) {
70
+ this.pos[channelId] = 0;
71
+ }
72
+ resolve(vod);
73
+ } else {
74
+ reject("Invalid channelId provided");
75
+ }
76
+ });
77
+ }
78
+ }
79
+
80
+ class RefChannelManager implements IChannelManager {
81
+ getChannels(): Channel[] {
82
+ return [{ id: "1", profile: this._getProfile(), audioTracks: this._getAudioTracks() }];
83
+ }
84
+
85
+ _getProfile(): ChannelProfile[] {
86
+ return [
87
+ {
88
+ bw: 7934000,
89
+ codecs: "avc1.4d001f,mp4a.40.2",
90
+ resolution: [2880, 1440],
91
+ },
92
+ {
93
+ bw: 7514000,
94
+ codecs: "avc1.4d001f,mp4a.40.2",
95
+ resolution: [1920, 1080],
96
+ },
97
+ { bw: 7134000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [1280, 720] },
98
+ { bw: 6134000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [1024, 458] },
99
+ { bw: 2323000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [640, 286] },
100
+ { bw: 495894, codecs: "avc1.4d001f,mp4a.40.2", resolution: [480, 214] },
101
+ ];
102
+ }
103
+ _getAudioTracks(): AudioTracks[] {
104
+ return [
105
+ { language: "en", name: "English" },
106
+ ];
107
+ }
108
+ }
109
+
110
+ const refAssetManager = new RefAssetManager();
111
+ const refChannelManager = new RefChannelManager();
112
+
113
+ const engineOptions: ChannelEngineOpts = {
114
+ heartbeat: "/",
115
+ averageSegmentDuration: 4000,
116
+ channelManager: refChannelManager,
117
+ defaultSlateUri: "https://maitv-vod.lab.eyevinn.technology/slate-consuo.mp4/master.m3u8",
118
+ slateRepetitions: 10,
119
+ redisUrl: process.env.REDIS_URL,
120
+ useDemuxedAudio: true,
121
+ alwaysNewSegments: true,
122
+ };
123
+
124
+ const engine = new ChannelEngine(refAssetManager, engineOptions);
125
+ engine.start();
126
+ engine.listen(process.env.PORT || 8000);
package/tsconfig.json CHANGED
@@ -6,5 +6,12 @@
6
6
  "esModuleInterop": true,
7
7
  "declaration": true
8
8
  },
9
- "include": ["./server.ts", "./server-demux.ts", "./server-livemix.ts", "./server-multicodec.ts", "./engine/**/*"]
9
+ "include": [
10
+ "./server.ts",
11
+ "./server-demux.ts",
12
+ "./server-livemix.ts",
13
+ "./server-multicodec.ts",
14
+ "./server-drm.ts",
15
+ "./engine/**/*"
16
+ ]
10
17
  }