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.
- package/dist/engine/redis_state_store.d.ts +1 -0
- package/dist/engine/redis_state_store.js +8 -3
- package/dist/engine/server.d.ts +2 -2
- package/dist/engine/server.js +3 -0
- package/dist/engine/session.js +52 -18
- package/dist/engine/util.d.ts +1 -0
- package/dist/engine/util.js +11 -1
- package/dist/package.json +2 -2
- package/dist/server-demux.js +2 -2
- package/dist/server-multicodec.js +8 -8
- package/engine/redis_state_store.js +8 -3
- package/engine/server.ts +5 -2
- package/engine/session.js +48 -13
- package/engine/util.js +12 -1
- package/package.json +2 -2
- package/server-demux.ts +2 -2
- package/server-multicodec.ts +9 -9
|
@@ -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
|
|
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,
|
|
195
|
+
_this.client.expire(storeKey, _this.volatileKeyTTL, function (err, res) {
|
|
191
196
|
if (!err) {
|
|
192
|
-
debug("REDIS expire ".concat(storeKey, " ").concat(
|
|
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 {
|
package/dist/engine/server.d.ts
CHANGED
|
@@ -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 {
|
package/dist/engine/server.js
CHANGED
|
@@ -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
|
};
|
package/dist/engine/session.js
CHANGED
|
@@ -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 (
|
|
1352
|
-
switch (
|
|
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 =
|
|
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
|
-
|
|
1383
|
+
_a = currentVod.getAudioCodecsAndChannelsForGroupId(audioGroupId), _ = _a[0], channels = _a[1];
|
|
1382
1384
|
// Make default track if set property is true.
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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
|
-
|
|
1399
|
-
|
|
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 +
|
|
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;
|
package/dist/engine/util.d.ts
CHANGED
|
@@ -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[];
|
package/dist/engine/util.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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/dist/server-demux.js
CHANGED
|
@@ -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": "日本語
|
|
79
|
-
{ language: "
|
|
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
|
|
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,
|
|
90
|
+
this.client.expire(storeKey, this.volatileKeyTTL, (err, res) => {
|
|
86
91
|
if (!err) {
|
|
87
|
-
debug(`REDIS expire ${storeKey} ${
|
|
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
|
-
|
|
964
|
+
const [_, channels] = currentVod.getAudioCodecsAndChannelsForGroupId(audioGroupId);
|
|
963
965
|
// Make default track if set property is true.
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
-
|
|
979
|
-
|
|
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 +
|
|
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}]:
|
|
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}]:
|
|
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.
|
|
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.
|
|
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
|
}
|
package/server-multicodec.ts
CHANGED
|
@@ -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": "日本語
|
|
85
|
-
{ language: "
|
|
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);
|