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.
- package/.github/workflows/nodejs.yml +1 -1
- package/.github/workflows/publish.yml +1 -1
- package/.github/workflows/rc.yml +22 -0
- package/README.md +4 -9
- package/dist/engine/session.js +11 -6
- package/dist/engine/session_state.js +2 -1
- package/dist/engine/util.d.ts +1 -0
- package/dist/engine/util.js +22 -1
- package/dist/package.json +5 -4
- package/dist/server-drm.d.ts +1 -0
- package/dist/server-drm.js +114 -0
- package/docs/demosite.png +0 -0
- package/engine/session.js +10 -5
- package/engine/session_state.js +7 -1
- package/engine/util.js +24 -5
- package/package.json +5 -4
- package/server-drm.ts +126 -0
- package/tsconfig.json +8 -1
|
@@ -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 |
|
|
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
|
-
|
|
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
|
|
package/dist/engine/session.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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")];
|
package/dist/engine/util.d.ts
CHANGED
|
@@ -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;
|
package/dist/engine/util.js
CHANGED
|
@@ -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.
|
|
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
|
+
"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.
|
|
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": "
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1041
|
+
m3u8 += "master" + profile.bw + ".m3u8%3Bsession=" + this._sessionId + "\n";
|
|
1037
1042
|
});
|
|
1038
1043
|
}
|
|
1039
1044
|
|
package/engine/session_state.js
CHANGED
|
@@ -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(
|
|
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.
|
|
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
|
+
"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.
|
|
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": "
|
|
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": [
|
|
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
|
}
|