eyevinn-channel-engine 4.2.1 → 4.2.3
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/README.md +6 -5
- package/dist/engine/server.d.ts +11 -1
- package/dist/engine/server.js +56 -51
- package/dist/engine/session.d.ts +2 -0
- package/dist/engine/session.js +352 -293
- package/dist/engine/stream_switcher.d.ts +1 -0
- package/dist/engine/stream_switcher.js +16 -0
- package/dist/examples/autocreate.js +99 -0
- package/dist/{server.js → examples/default.js} +1 -1
- package/dist/{server-demux.js → examples/demux.js} +1 -1
- package/dist/{server-drm.js → examples/drm.js} +1 -1
- package/dist/{server-livemix.js → examples/livemix.js} +1 -1
- package/dist/examples/multicodec.d.ts +1 -0
- package/dist/{server-multicodec.js → examples/multicodec.js} +1 -1
- package/dist/package.json +4 -4
- package/engine/server.ts +36 -34
- package/engine/session.js +60 -7
- package/engine/stream_switcher.js +18 -0
- package/examples/autocreate.ts +112 -0
- package/{server.ts → examples/default.ts} +1 -1
- package/{server-demux.ts → examples/demux.ts} +1 -1
- package/{server-drm.ts → examples/drm.ts} +1 -1
- package/{server-livemix.ts → examples/livemix.ts} +1 -1
- package/{server-multicodec.ts → examples/multicodec.ts} +1 -1
- package/package.json +4 -4
- package/tsconfig.json +1 -5
- /package/dist/{server-demux.d.ts → examples/autocreate.d.ts} +0 -0
- /package/dist/{server-drm.d.ts → examples/default.d.ts} +0 -0
- /package/dist/{server-livemix.d.ts → examples/demux.d.ts} +0 -0
- /package/dist/{server-multicodec.d.ts → examples/drm.d.ts} +0 -0
- /package/dist/{server.d.ts → examples/livemix.d.ts} +0 -0
|
@@ -327,6 +327,7 @@ var StreamSwitcher = /** @class */ (function () {
|
|
|
327
327
|
// Insert preroll if available for current channel
|
|
328
328
|
if (this.prerollsCache[this.sessionId]) {
|
|
329
329
|
prerollSegments = this.prerollsCache[this.sessionId].segments;
|
|
330
|
+
this._insertTimedMetadata(prerollSegments, scheduleObj.timedMetadata || {});
|
|
330
331
|
currVodSegments = this._mergeSegments(prerollSegments, currVodSegments, false);
|
|
331
332
|
}
|
|
332
333
|
// In risk that the SL-playhead might have updated some data after
|
|
@@ -516,6 +517,7 @@ var StreamSwitcher = /** @class */ (function () {
|
|
|
516
517
|
// Insert preroll, if available, for current channel
|
|
517
518
|
if (this.prerollsCache[this.sessionId]) {
|
|
518
519
|
prerollSegments = this.prerollsCache[this.sessionId].segments;
|
|
520
|
+
this._insertTimedMetadata(prerollSegments, scheduleObj.timedMetadata || {});
|
|
519
521
|
eventSegments.currMseqSegs = this._mergeSegments(prerollSegments, eventSegments.currMseqSegs, false);
|
|
520
522
|
}
|
|
521
523
|
return [4 /*yield*/, sessionLive.setCurrentMediaAndDiscSequenceCount(currLiveCounts.mediaSeq, currLiveCounts.discSeq)];
|
|
@@ -808,6 +810,20 @@ var StreamSwitcher = /** @class */ (function () {
|
|
|
808
810
|
});
|
|
809
811
|
return OUTPUT_SEGMENTS;
|
|
810
812
|
};
|
|
813
|
+
StreamSwitcher.prototype._insertTimedMetadata = function (segments, timedMetadata) {
|
|
814
|
+
var bandwidths = Object.keys(segments);
|
|
815
|
+
debug("[".concat(this.sessionId, "]: Inserting timed metadata ").concat(Object.keys(timedMetadata).join(',')));
|
|
816
|
+
bandwidths.forEach(function (bw) {
|
|
817
|
+
var daterangeData = segments[bw][0]["daterange"];
|
|
818
|
+
if (!daterangeData) {
|
|
819
|
+
daterangeData = {};
|
|
820
|
+
Object.keys(timedMetadata).forEach(function (k) {
|
|
821
|
+
daterangeData[k] = timedMetadata[k];
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
segments[bw][0]["daterange"] = daterangeData;
|
|
825
|
+
});
|
|
826
|
+
};
|
|
811
827
|
return StreamSwitcher;
|
|
812
828
|
}());
|
|
813
829
|
module.exports = StreamSwitcher;
|
|
@@ -0,0 +1,99 @@
|
|
|
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 DEFAULT_ASSETS = [
|
|
8
|
+
{
|
|
9
|
+
id: 1,
|
|
10
|
+
title: "Tears of Steel",
|
|
11
|
+
uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/master.m3u8",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 2,
|
|
15
|
+
title: "VINN",
|
|
16
|
+
uri: "https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8",
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
var RefAssetManager = /** @class */ (function () {
|
|
20
|
+
function RefAssetManager(opts) {
|
|
21
|
+
this.assets = {};
|
|
22
|
+
this.assets['1'] = DEFAULT_ASSETS;
|
|
23
|
+
this.pos = {
|
|
24
|
+
'1': 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} vodRequest
|
|
30
|
+
* {
|
|
31
|
+
* sessionId,
|
|
32
|
+
* category,
|
|
33
|
+
* playlistId
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
RefAssetManager.prototype.getNextVod = function (vodRequest) {
|
|
37
|
+
var _this = this;
|
|
38
|
+
return new Promise(function (resolve, reject) {
|
|
39
|
+
var channelId = vodRequest.playlistId;
|
|
40
|
+
if (_this.pos[channelId] === undefined) {
|
|
41
|
+
_this.pos[channelId] = 0;
|
|
42
|
+
_this.assets[channelId] = DEFAULT_ASSETS;
|
|
43
|
+
}
|
|
44
|
+
if (_this.assets[channelId]) {
|
|
45
|
+
var vod = _this.assets[channelId][_this.pos[channelId]++];
|
|
46
|
+
if (_this.pos[channelId] > _this.assets[channelId].length - 1) {
|
|
47
|
+
_this.pos[channelId] = 0;
|
|
48
|
+
}
|
|
49
|
+
var vodResponse = {
|
|
50
|
+
id: vod.id,
|
|
51
|
+
title: vod.title,
|
|
52
|
+
uri: vod.uri,
|
|
53
|
+
};
|
|
54
|
+
resolve(vodResponse);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
reject("Invalid channelId provided");
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
return RefAssetManager;
|
|
62
|
+
}());
|
|
63
|
+
var RefChannelManager = /** @class */ (function () {
|
|
64
|
+
function RefChannelManager() {
|
|
65
|
+
this.channels = [];
|
|
66
|
+
this.channels.push({ id: "1", profile: this._getProfile() });
|
|
67
|
+
}
|
|
68
|
+
RefChannelManager.prototype.getChannels = function () {
|
|
69
|
+
//return [ { id: '1', profile: this._getProfile() }, { id: 'faulty', profile: this._getProfile() } ];
|
|
70
|
+
//return [{ id: "1", profile: this._getProfile() }];
|
|
71
|
+
return this.channels;
|
|
72
|
+
};
|
|
73
|
+
RefChannelManager.prototype.autoCreateChannel = function (channelId) {
|
|
74
|
+
if (!this.channels.find(function (ch) { return ch.id === channelId; })) {
|
|
75
|
+
this.channels.push({ id: channelId, profile: this._getProfile() });
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
RefChannelManager.prototype._getProfile = function () {
|
|
79
|
+
return [
|
|
80
|
+
{ bw: 6134000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [1024, 458] },
|
|
81
|
+
{ bw: 2323000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [640, 286] },
|
|
82
|
+
{ bw: 1313000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [480, 214] },
|
|
83
|
+
];
|
|
84
|
+
};
|
|
85
|
+
return RefChannelManager;
|
|
86
|
+
}());
|
|
87
|
+
var refAssetManager = new RefAssetManager();
|
|
88
|
+
var refChannelManager = new RefChannelManager();
|
|
89
|
+
var engineOptions = {
|
|
90
|
+
heartbeat: "/",
|
|
91
|
+
averageSegmentDuration: 2000,
|
|
92
|
+
channelManager: refChannelManager,
|
|
93
|
+
autoCreateSession: true,
|
|
94
|
+
defaultSlateUri: "https://maitv-vod.lab.eyevinn.technology/slate-consuo.mp4/master.m3u8",
|
|
95
|
+
slateRepetitions: 10,
|
|
96
|
+
};
|
|
97
|
+
var engine = new index_1.ChannelEngine(refAssetManager, engineOptions);
|
|
98
|
+
engine.start();
|
|
99
|
+
engine.listen(process.env.PORT || 8000);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Reference implementation of Channel Engine library
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
var index_1 = require("
|
|
6
|
+
var index_1 = require("../index");
|
|
7
7
|
var STITCH_ENDPOINT = process.env.STITCH_ENDPOINT ||
|
|
8
8
|
"http://lambda.eyevinn.technology/stitch/master.m3u8";
|
|
9
9
|
var RefAssetManager = /** @class */ (function () {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Reference implementation of Channel Engine library using demuxed VOD assets.
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
var index_1 = require("
|
|
6
|
+
var index_1 = require("../index");
|
|
7
7
|
var RefAssetManager = /** @class */ (function () {
|
|
8
8
|
function RefAssetManager(opts) {
|
|
9
9
|
this.assets = {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
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
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
var index_1 = require("
|
|
8
|
+
var index_1 = require("../index");
|
|
9
9
|
var RefAssetManager = /** @class */ (function () {
|
|
10
10
|
function RefAssetManager(opts) {
|
|
11
11
|
this.assets = {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Reference implementation of Channel Engine library
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
var index_1 = require("
|
|
6
|
+
var index_1 = require("../index");
|
|
7
7
|
var uuidv4 = require('uuid').v4;
|
|
8
8
|
var STITCH_ENDPOINT = process.env.STITCH_ENDPOINT || "http://lambda.eyevinn.technology/stitch/master.m3u8";
|
|
9
9
|
var RefAssetManager = /** @class */ (function () {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Reference implementation of Channel Engine library
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
var index_1 = require("
|
|
6
|
+
var index_1 = require("../index");
|
|
7
7
|
var RefAssetManager = /** @class */ (function () {
|
|
8
8
|
function RefAssetManager(opts) {
|
|
9
9
|
this.assets = {
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eyevinn-channel-engine",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.3",
|
|
4
4
|
"description": "OTT TV Channel Engine",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
"test": "jasmine",
|
|
11
11
|
"version:rc": "npm version prerelease --preid=rc",
|
|
12
12
|
"postversion": "git push && git push --tags",
|
|
13
|
-
"start": "node dist/
|
|
14
|
-
"start-demux": "node dist/
|
|
15
|
-
"start-livemix": "node dist/
|
|
13
|
+
"start": "node dist/examples/default.js",
|
|
14
|
+
"start-demux": "node dist/examples/demux.js",
|
|
15
|
+
"start-livemix": "node dist/examples/livemix.js"
|
|
16
16
|
},
|
|
17
17
|
"engines": {
|
|
18
18
|
"node": ">=14 <20"
|
package/engine/server.ts
CHANGED
|
@@ -8,6 +8,7 @@ const SessionLive = require('./session_live.js');
|
|
|
8
8
|
const StreamSwitcher = require('./stream_switcher.js');
|
|
9
9
|
const EventStream = require('./event_stream.js');
|
|
10
10
|
const SubtitleSlicer = require('./subtitle_slicer.js');
|
|
11
|
+
const { timer }= require('./util.js');
|
|
11
12
|
|
|
12
13
|
const { SessionStateStore } = require('./session_state.js');
|
|
13
14
|
const { SessionLiveStateStore } = require('./session_live_state.js');
|
|
@@ -16,7 +17,7 @@ const { PlayheadStateStore } = require('./playhead_state.js');
|
|
|
16
17
|
const { filterQueryParser, toHHMMSS, WaitTimeGenerator } = require('./util.js');
|
|
17
18
|
const { version } = require('../package.json');
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
+
const AUTO_CREATE_CHANNEL_TIMEOUT = 3000;
|
|
20
21
|
|
|
21
22
|
const sessions = {}; // Should be a persistent store...
|
|
22
23
|
const sessionsLive = {}; // Should be a persistent store...
|
|
@@ -54,6 +55,7 @@ export interface ChannelEngineOpts {
|
|
|
54
55
|
adXchangeUri?: string; // deprecated
|
|
55
56
|
noSessionDataTags?: boolean;
|
|
56
57
|
volatileKeyTTL?: number;
|
|
58
|
+
autoCreateSession?: boolean;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
interface StreamerOpts {
|
|
@@ -85,13 +87,19 @@ export interface VodTimedMetadata {
|
|
|
85
87
|
'class': string;
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
export interface LiveTimedMetadata {
|
|
91
|
+
'id': string;
|
|
92
|
+
'start-date': string;
|
|
93
|
+
'x-title'?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
88
96
|
export interface VodResponse {
|
|
89
97
|
title: any;
|
|
90
98
|
id: string;
|
|
91
99
|
uri: string;
|
|
92
100
|
offset?: number;
|
|
93
101
|
diffMs?: number;
|
|
94
|
-
|
|
102
|
+
desiredDuration?: number;
|
|
95
103
|
type?: string;
|
|
96
104
|
currentMetadata?: VodResponseMetadata;
|
|
97
105
|
timedMetadata?: VodTimedMetadata;
|
|
@@ -142,6 +150,7 @@ export interface SubtitleTracks {
|
|
|
142
150
|
|
|
143
151
|
export interface IChannelManager {
|
|
144
152
|
getChannels: () => Channel[];
|
|
153
|
+
autoCreateChannel?: (channelId: string) => void;
|
|
145
154
|
}
|
|
146
155
|
|
|
147
156
|
export enum ScheduleStreamType {
|
|
@@ -158,6 +167,7 @@ export interface Schedule {
|
|
|
158
167
|
end_time: number;
|
|
159
168
|
uri: string;
|
|
160
169
|
duration?: number;
|
|
170
|
+
timedMetadata?: LiveTimedMetadata;
|
|
161
171
|
}
|
|
162
172
|
|
|
163
173
|
export interface IStreamSwitchManager {
|
|
@@ -187,6 +197,7 @@ export class ChannelEngine {
|
|
|
187
197
|
private logCloudWatchMetrics: boolean;
|
|
188
198
|
private adCopyMgrUri?: string;
|
|
189
199
|
private adXchangeUri?: string;
|
|
200
|
+
private autoCreateSession: boolean = false;
|
|
190
201
|
|
|
191
202
|
constructor(assetMgr: IAssetManager, options?: ChannelEngineOpts) {
|
|
192
203
|
this.options = options;
|
|
@@ -217,6 +228,9 @@ export class ChannelEngine {
|
|
|
217
228
|
if (options && options.streamSwitchManager) {
|
|
218
229
|
this.streamSwitchManager = options.streamSwitchManager;
|
|
219
230
|
}
|
|
231
|
+
if (options && options.autoCreateSession !== undefined) {
|
|
232
|
+
this.autoCreateSession = options.autoCreateSession;
|
|
233
|
+
}
|
|
220
234
|
this.assetMgr = assetMgr;
|
|
221
235
|
this.monitorTimer = {};
|
|
222
236
|
this.server = restify.createServer();
|
|
@@ -428,6 +442,7 @@ export class ChannelEngine {
|
|
|
428
442
|
async updateChannelsAsync(channelMgr, options) {
|
|
429
443
|
debug(`Do we have any new channels?`);
|
|
430
444
|
const newChannels = channelMgr.getChannels().filter(channel => !sessions[channel.id]);
|
|
445
|
+
debug(newChannels);
|
|
431
446
|
const addAsync = async (channel) => {
|
|
432
447
|
debug(`Adding channel with ID ${channel.id}`);
|
|
433
448
|
sessions[channel.id] = new Session(this.assetMgr, {
|
|
@@ -472,14 +487,14 @@ export class ChannelEngine {
|
|
|
472
487
|
await sessions[channel.id].initAsync();
|
|
473
488
|
await sessionsLive[channel.id].initAsync();
|
|
474
489
|
if (!this.monitorTimer[channel.id]) {
|
|
475
|
-
this.monitorTimer[channel.id]
|
|
490
|
+
this.monitorTimer[channel.id] = setInterval(async () => { await this._monitorAsync(sessions[channel.id], sessionsLive[channel.id]) }, 5000);
|
|
476
491
|
}
|
|
477
492
|
|
|
478
493
|
await sessions[channel.id].startPlayheadAsync();
|
|
479
494
|
};
|
|
480
495
|
|
|
481
496
|
const addLiveAsync = async (channel) => {
|
|
482
|
-
debug(`Adding channel with ID ${channel.id}`);
|
|
497
|
+
debug(`Adding live channel with ID ${channel.id}`);
|
|
483
498
|
await sessionsLive[channel.id].initAsync();
|
|
484
499
|
await sessionsLive[channel.id].startPlayheadAsync();
|
|
485
500
|
};
|
|
@@ -564,6 +579,16 @@ export class ChannelEngine {
|
|
|
564
579
|
}
|
|
565
580
|
}
|
|
566
581
|
|
|
582
|
+
async createChannel(channelId) {
|
|
583
|
+
if (!sessions[channelId]) {
|
|
584
|
+
if (this.options.channelManager.autoCreateChannel) {
|
|
585
|
+
this.options.channelManager.autoCreateChannel(channelId);
|
|
586
|
+
setTimeout(async () => { await this.updateChannelsAsync(this.options.channelManager, this.options) });
|
|
587
|
+
await timer(AUTO_CREATE_CHANNEL_TIMEOUT);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
567
592
|
async getMasterManifest(channelId) {
|
|
568
593
|
if (sessions[channelId]) {
|
|
569
594
|
const session = sessions[channelId];
|
|
@@ -679,45 +704,22 @@ export class ChannelEngine {
|
|
|
679
704
|
if (req.query['category']) {
|
|
680
705
|
options.category = req.query['category'];
|
|
681
706
|
}
|
|
707
|
+
if (this.autoCreateSession && req.query['channel']) {
|
|
708
|
+
debug(`Attempting to create channel with id ${req.query['channel']}`);
|
|
709
|
+
await this.createChannel(req.query['channel']);
|
|
710
|
+
debug(`Automatically created channel with id ${req.query['channel']}`);
|
|
711
|
+
}
|
|
712
|
+
|
|
682
713
|
if (req.query['channel'] && sessions[req.query['channel']]) {
|
|
683
714
|
session = sessions[req.query['channel']];
|
|
684
715
|
} else if (req.query['session'] && sessions[req.query['session']]) {
|
|
685
716
|
session = sessions[req.query['session']];
|
|
686
|
-
} else {
|
|
687
|
-
options.adCopyMgrUri = this.adCopyMgrUri;
|
|
688
|
-
options.adXchangeUri = this.adXchangeUri;
|
|
689
|
-
options.averageSegmentDuration = this.streamerOpts.defaultAverageSegmentDuration;
|
|
690
|
-
options.useDemuxedAudio = this.useDemuxedAudio;
|
|
691
|
-
options.dummySubtitleEndpoint = this.dummySubtitleEndpoint;
|
|
692
|
-
options.subtitleSliceEndpoint = this.subtitleSliceEndpoint;
|
|
693
|
-
options.useVTTSubtitles = this.useVTTSubtitles;
|
|
694
|
-
options.alwaysNewSegments = this.alwaysNewSegments;
|
|
695
|
-
options.playheadDiffThreshold = this.streamerOpts.defaultPlayheadDiffThreshold;
|
|
696
|
-
options.maxTickInterval = this.streamerOpts.defaultMaxTickInterval;
|
|
697
|
-
options.targetDurationPadding = this.streamerOpts.targetDurationPadding;
|
|
698
|
-
options.forceTargetDuration = this.streamerOpts.forceTargetDuration;
|
|
699
|
-
options.diffCompensationRate = this.streamerOpts.diffCompensationRate;
|
|
700
|
-
// if we are initiating a master manifest
|
|
701
|
-
// outside of specific Channel context,
|
|
702
|
-
// if slate options are set at the ChannelEngine level, then set these here
|
|
703
|
-
if (this.defaultSlateUri) {
|
|
704
|
-
options.slateUri = this.defaultSlateUri;
|
|
705
|
-
}
|
|
706
|
-
if (this.slateRepetitions) {
|
|
707
|
-
options.slateRepetitions = this.slateRepetitions;
|
|
708
|
-
}
|
|
709
|
-
if (this.slateDuration) {
|
|
710
|
-
options.slateDuration = this.slateDuration;
|
|
711
|
-
}
|
|
712
|
-
options.disabledPlayhead = true; // disable playhead for personalized streams
|
|
713
|
-
session = new Session(this.assetMgr, options, this.sessionStore);
|
|
714
|
-
await session.initAsync();
|
|
715
|
-
sessions[session.sessionId] = session;
|
|
716
717
|
}
|
|
717
718
|
if (req.query['startWithId']) {
|
|
718
719
|
options.startWithId = req.query['startWithId'];
|
|
719
720
|
debug(`New session to start with assetId=${options.startWithId}`);
|
|
720
721
|
}
|
|
722
|
+
|
|
721
723
|
if (session) {
|
|
722
724
|
const eventStream = new EventStream(session);
|
|
723
725
|
eventStreams[session.sessionId] = eventStream;
|
package/engine/session.js
CHANGED
|
@@ -782,7 +782,7 @@ class Session {
|
|
|
782
782
|
|
|
783
783
|
let sessionState = await this._sessionState.getValues(
|
|
784
784
|
["state", "mediaSeq", "mediaSeqAudio", "mediaSeqSubtitle", "discSeq", "discSeqAudio", "discSeqSubtitle", "vodMediaSeqVideo", "vodMediaSeqAudio", "vodMediaSeqSubtitle"]);
|
|
785
|
-
let playheadState = await this._playheadState.getValues(["mediaSeq", "mediaSeqAudio", "mediaSeqSubtitle", "vodMediaSeqVideo", "vodMediaSeqAudio", "vodMediaSubtitle"]);
|
|
785
|
+
let playheadState = await this._playheadState.getValues(["mediaSeq", "mediaSeqAudio", "mediaSeqSubtitle", "vodMediaSeqVideo", "vodMediaSeqAudio", "vodMediaSubtitle", "playheadRef"]);
|
|
786
786
|
let currentVod = await this._sessionState.getCurrentVod();
|
|
787
787
|
if (!currentVod ||
|
|
788
788
|
sessionState.vodMediaSeqVideo === null ||
|
|
@@ -802,9 +802,15 @@ class Session {
|
|
|
802
802
|
}
|
|
803
803
|
if (sessionState.state === SessionState.VOD_NEXT_INITIATING || sessionState.state === SessionState.VOD_RELOAD_INITIATING) {
|
|
804
804
|
if (isLeader) {
|
|
805
|
+
const isOldVod = this._isOldVod(playheadState.playheadRef, currentVod.getDuration());
|
|
806
|
+
let nextState = SessionState.VOD_PLAYING;
|
|
807
|
+
if (isOldVod) {
|
|
808
|
+
nextState = SessionState.VOD_NEXT_INIT;
|
|
809
|
+
}
|
|
805
810
|
const leaderAction = sessionState.state === SessionState.VOD_NEXT_INITIATING ? "initiated" : "reloaded";
|
|
806
|
-
|
|
807
|
-
|
|
811
|
+
const leaderState = nextState === SessionState.VOD_NEXT_INIT ? "VOD_NEXT_INIT" : "VOD_PLAYING";
|
|
812
|
+
debug(`[${this._sessionId}]: I am the leader and have just ${leaderAction} next VOD, let's move to ${leaderState}`);
|
|
813
|
+
sessionState.state = await this._sessionState.set("state", nextState);
|
|
808
814
|
} else {
|
|
809
815
|
debug(`[${this._sessionId}]: Return the last generated m3u8 to give the leader some time`);
|
|
810
816
|
let m3u8 = await this._playheadState.getLastM3u8();
|
|
@@ -1325,7 +1331,12 @@ class Session {
|
|
|
1325
1331
|
})
|
|
1326
1332
|
}
|
|
1327
1333
|
currentVod = newVod;
|
|
1328
|
-
|
|
1334
|
+
if (vodResponse.desiredDuration) {
|
|
1335
|
+
const { mediaManifestLoader, audioManifestLoader} = await this._truncateVod(vodResponse);
|
|
1336
|
+
loadPromise = currentVod.load(null, mediaManifestLoader, audioManifestLoader);
|
|
1337
|
+
} else {
|
|
1338
|
+
loadPromise = currentVod.load();
|
|
1339
|
+
}
|
|
1329
1340
|
} else {
|
|
1330
1341
|
if (vodResponse.type === 'gap') {
|
|
1331
1342
|
loadPromise = new Promise((resolve, reject) => {
|
|
@@ -1434,7 +1445,7 @@ class Session {
|
|
|
1434
1445
|
debug(`[${this._sessionId}]: state=VOD_PLAYING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`);
|
|
1435
1446
|
return;
|
|
1436
1447
|
case SessionState.VOD_NEXT_INITIATING:
|
|
1437
|
-
debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("
|
|
1448
|
+
debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`);
|
|
1438
1449
|
if (!isLeader) {
|
|
1439
1450
|
debug(`[${this._sessionId}]: not the leader so just waiting for the VOD to be initiated`);
|
|
1440
1451
|
}
|
|
@@ -1503,7 +1514,12 @@ class Session {
|
|
|
1503
1514
|
title: this.currentMetadata.title || '',
|
|
1504
1515
|
}
|
|
1505
1516
|
});
|
|
1506
|
-
|
|
1517
|
+
if (vodResponse.desiredDuration) {
|
|
1518
|
+
const { mediaManifestLoader, audioManifestLoader} = await this._truncateVod(vodResponse);
|
|
1519
|
+
loadPromise = newVod.loadAfter(currentVod, null, mediaManifestLoader, audioManifestLoader);
|
|
1520
|
+
} else {
|
|
1521
|
+
loadPromise = newVod.loadAfter(currentVod);
|
|
1522
|
+
}
|
|
1507
1523
|
if (vodResponse.diffMs) {
|
|
1508
1524
|
this.diffCompensation = vodResponse.diffMs;
|
|
1509
1525
|
if (this.diffCompensation) {
|
|
@@ -1542,7 +1558,7 @@ class Session {
|
|
|
1542
1558
|
sessionState.discSeq = await this._sessionState.set("discSeq", sessionState.discSeq + lastDiscontinuity);
|
|
1543
1559
|
sessionState.discSeqAudio = await this._sessionState.set("discSeqAudio", sessionState.discSeqAudio + lastDiscontinuityAudio);
|
|
1544
1560
|
sessionState.discSeqSubtitle = await this._sessionState.set("discSeqSubtitle", sessionState.discSeqSubtitle + lastDiscontinuitySubtitle);
|
|
1545
|
-
debug(`[${this._sessionId}]: new sequence data set in store [${sessionState.mediaSeq}][${sessionState.discSeq}]
|
|
1561
|
+
debug(`[${this._sessionId}]: new sequence data set in store V[${sessionState.mediaSeq}][${sessionState.discSeq}]_A[${sessionState.mediaSeqAudio}][${sessionState.discSeqAudio}]_S[${sessionState.mediaSeqSubtitle}][${sessionState.discSeqSubtitle}]`);
|
|
1546
1562
|
await this._sessionState.remove("nextVod");
|
|
1547
1563
|
sessionState.currentVod = await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
|
|
1548
1564
|
this.leaderIsSettingNextVod = false;
|
|
@@ -1776,6 +1792,33 @@ class Session {
|
|
|
1776
1792
|
});
|
|
1777
1793
|
}
|
|
1778
1794
|
|
|
1795
|
+
_truncateVod(vodResponse) {
|
|
1796
|
+
return new Promise((resolve, reject) => {
|
|
1797
|
+
const truncatedVod = new HLSTruncateVod(vodResponse.uri, vodResponse.desiredDuration / 1000);
|
|
1798
|
+
truncatedVod.load()
|
|
1799
|
+
.then(() => {
|
|
1800
|
+
let audioManifestLoader;
|
|
1801
|
+
const mediaManifestLoader = (bw) => {
|
|
1802
|
+
let mediaManifestStream = new Readable();
|
|
1803
|
+
mediaManifestStream.push(truncatedVod.getMediaManifest(bw));
|
|
1804
|
+
mediaManifestStream.push(null);
|
|
1805
|
+
return mediaManifestStream;
|
|
1806
|
+
};
|
|
1807
|
+
if (this.use_demuxed_audio) {
|
|
1808
|
+
audioManifestLoader = (audioGroupId, audioLanguage) => {
|
|
1809
|
+
let mediaManifestStream = new Readable();
|
|
1810
|
+
mediaManifestStream.push(truncatedVod.getAudioManifest(audioGroupId, audioLanguage));
|
|
1811
|
+
mediaManifestStream.push(null);
|
|
1812
|
+
return mediaManifestStream;
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
resolve({ mediaManifestLoader, audioManifestLoader });
|
|
1816
|
+
});
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1779
1822
|
_truncateSlate(afterVod, requestedDuration, vodUri) {
|
|
1780
1823
|
return new Promise((resolve, reject) => {
|
|
1781
1824
|
let nexVodUri = null;
|
|
@@ -1983,6 +2026,16 @@ class Session {
|
|
|
1983
2026
|
return compensationSec;
|
|
1984
2027
|
}
|
|
1985
2028
|
}
|
|
2029
|
+
|
|
2030
|
+
_isOldVod(refTs, vodDur) {
|
|
2031
|
+
const TIME_PADDING_MS = 20 * 1000 // 20secs
|
|
2032
|
+
const VOD_DURATION_MS = vodDur * 1000;
|
|
2033
|
+
const TIME_SINCE_VOD_STARTED_MS = (Date.now() - refTs) + TIME_PADDING_MS;
|
|
2034
|
+
if (TIME_SINCE_VOD_STARTED_MS > VOD_DURATION_MS) {
|
|
2035
|
+
return true;
|
|
2036
|
+
}
|
|
2037
|
+
return false;
|
|
2038
|
+
}
|
|
1986
2039
|
}
|
|
1987
2040
|
|
|
1988
2041
|
module.exports = Session;
|
|
@@ -238,8 +238,10 @@ class StreamSwitcher {
|
|
|
238
238
|
// Insert preroll if available for current channel
|
|
239
239
|
if (this.prerollsCache[this.sessionId]) {
|
|
240
240
|
const prerollSegments = this.prerollsCache[this.sessionId].segments;
|
|
241
|
+
this._insertTimedMetadata(prerollSegments, scheduleObj.timedMetadata || {});
|
|
241
242
|
currVodSegments = this._mergeSegments(prerollSegments, currVodSegments, false);
|
|
242
243
|
}
|
|
244
|
+
|
|
243
245
|
// In risk that the SL-playhead might have updated some data after
|
|
244
246
|
// we reset last time... we should Reset SessionLive before sending new data.
|
|
245
247
|
await sessionLive.resetLiveStoreAsync(0);
|
|
@@ -397,6 +399,7 @@ class StreamSwitcher {
|
|
|
397
399
|
// Insert preroll, if available, for current channel
|
|
398
400
|
if (this.prerollsCache[this.sessionId]) {
|
|
399
401
|
const prerollSegments = this.prerollsCache[this.sessionId].segments;
|
|
402
|
+
this._insertTimedMetadata(prerollSegments, scheduleObj.timedMetadata || {});
|
|
400
403
|
eventSegments.currMseqSegs = this._mergeSegments(prerollSegments, eventSegments.currMseqSegs, false);
|
|
401
404
|
}
|
|
402
405
|
|
|
@@ -639,6 +642,21 @@ class StreamSwitcher {
|
|
|
639
642
|
});
|
|
640
643
|
return OUTPUT_SEGMENTS;
|
|
641
644
|
}
|
|
645
|
+
|
|
646
|
+
_insertTimedMetadata(segments, timedMetadata) {
|
|
647
|
+
const bandwidths = Object.keys(segments);
|
|
648
|
+
debug(`[${this.sessionId}]: Inserting timed metadata ${Object.keys(timedMetadata).join(',')}`);
|
|
649
|
+
bandwidths.forEach((bw) => {
|
|
650
|
+
let daterangeData = segments[bw][0]["daterange"];
|
|
651
|
+
if (!daterangeData) {
|
|
652
|
+
daterangeData = {};
|
|
653
|
+
Object.keys(timedMetadata).forEach((k) => {
|
|
654
|
+
daterangeData[k] = timedMetadata[k];
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
segments[bw][0]["daterange"] = daterangeData;
|
|
658
|
+
});
|
|
659
|
+
}
|
|
642
660
|
}
|
|
643
661
|
|
|
644
662
|
module.exports = StreamSwitcher;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Reference implementation of Channel Engine library
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ChannelEngine, ChannelEngineOpts,
|
|
6
|
+
IAssetManager, IChannelManager,
|
|
7
|
+
VodRequest, VodResponse, Channel, ChannelProfile
|
|
8
|
+
} from "../index";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ASSETS = [
|
|
11
|
+
{
|
|
12
|
+
id: 1,
|
|
13
|
+
title: "Tears of Steel",
|
|
14
|
+
uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/master.m3u8",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 2,
|
|
18
|
+
title: "VINN",
|
|
19
|
+
uri: "https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8",
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
class RefAssetManager implements IAssetManager {
|
|
24
|
+
private assets;
|
|
25
|
+
private pos;
|
|
26
|
+
|
|
27
|
+
constructor(opts?) {
|
|
28
|
+
this.assets = {};
|
|
29
|
+
this.assets['1'] = DEFAULT_ASSETS;
|
|
30
|
+
this.pos = {
|
|
31
|
+
'1': 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} vodRequest
|
|
38
|
+
* {
|
|
39
|
+
* sessionId,
|
|
40
|
+
* category,
|
|
41
|
+
* playlistId
|
|
42
|
+
* }
|
|
43
|
+
*/
|
|
44
|
+
getNextVod(vodRequest: VodRequest): Promise<VodResponse> {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const channelId = vodRequest.playlistId;
|
|
47
|
+
if (this.pos[channelId] === undefined) {
|
|
48
|
+
this.pos[channelId] = 0;
|
|
49
|
+
this.assets[channelId] = DEFAULT_ASSETS;
|
|
50
|
+
}
|
|
51
|
+
if (this.assets[channelId]) {
|
|
52
|
+
let vod = this.assets[channelId][this.pos[channelId]++];
|
|
53
|
+
if (this.pos[channelId] > this.assets[channelId].length - 1) {
|
|
54
|
+
this.pos[channelId] = 0;
|
|
55
|
+
}
|
|
56
|
+
const vodResponse = {
|
|
57
|
+
id: vod.id,
|
|
58
|
+
title: vod.title,
|
|
59
|
+
uri: vod.uri,
|
|
60
|
+
};
|
|
61
|
+
resolve(vodResponse);
|
|
62
|
+
} else {
|
|
63
|
+
reject("Invalid channelId provided");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class RefChannelManager implements IChannelManager {
|
|
70
|
+
private channels: Channel[] = [];
|
|
71
|
+
|
|
72
|
+
constructor() {
|
|
73
|
+
this.channels.push({ id: "1", profile: this._getProfile() });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getChannels(): Channel[] {
|
|
77
|
+
//return [ { id: '1', profile: this._getProfile() }, { id: 'faulty', profile: this._getProfile() } ];
|
|
78
|
+
//return [{ id: "1", profile: this._getProfile() }];
|
|
79
|
+
return this.channels;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
autoCreateChannel(channelId: string) {
|
|
83
|
+
if (!this.channels.find((ch) => ch.id === channelId)) {
|
|
84
|
+
this.channels.push({ id: channelId, profile: this._getProfile() });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_getProfile(): ChannelProfile[] {
|
|
89
|
+
return [
|
|
90
|
+
{ bw: 6134000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [1024, 458] },
|
|
91
|
+
{ bw: 2323000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [640, 286] },
|
|
92
|
+
{ bw: 1313000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [480, 214] },
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const refAssetManager = new RefAssetManager();
|
|
98
|
+
const refChannelManager = new RefChannelManager();
|
|
99
|
+
|
|
100
|
+
const engineOptions: ChannelEngineOpts = {
|
|
101
|
+
heartbeat: "/",
|
|
102
|
+
averageSegmentDuration: 2000,
|
|
103
|
+
channelManager: refChannelManager,
|
|
104
|
+
autoCreateSession: true,
|
|
105
|
+
defaultSlateUri:
|
|
106
|
+
"https://maitv-vod.lab.eyevinn.technology/slate-consuo.mp4/master.m3u8",
|
|
107
|
+
slateRepetitions: 10,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const engine = new ChannelEngine(refAssetManager, engineOptions);
|
|
111
|
+
engine.start();
|
|
112
|
+
engine.listen(process.env.PORT || 8000);
|