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.
- package/dist/engine/server.d.ts +1 -0
- package/dist/engine/session.js +46 -15
- 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.d.ts +1 -0
- package/dist/server-multicodec.js +96 -0
- package/engine/server.ts +1 -0
- package/engine/session.js +42 -10
- package/engine/util.js +12 -1
- package/package.json +2 -2
- package/server-demux.ts +2 -2
- package/server-multicodec.ts +104 -0
- package/tsconfig.json +1 -1
package/dist/engine/server.d.ts
CHANGED
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) {
|
|
@@ -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 (
|
|
1352
|
-
switch (
|
|
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 =
|
|
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
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
-
|
|
1398
|
-
|
|
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 +
|
|
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
|
}
|
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.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.
|
|
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;
|
|
@@ -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
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
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
978
|
-
|
|
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 +
|
|
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.
|
|
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.
|
|
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