eyevinn-channel-engine 4.2.1 → 4.2.2
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 +10 -0
- package/dist/engine/server.js +56 -51
- package/dist/engine/session.d.ts +1 -0
- package/dist/engine/session.js +21 -6
- 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 +35 -33
- package/engine/session.js +21 -5
- 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
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ Build the app:
|
|
|
110
110
|
npm run build
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
To start the reference implementation in `
|
|
113
|
+
To start the reference implementation in `examples/default.ts` run:
|
|
114
114
|
|
|
115
115
|
```
|
|
116
116
|
npm start
|
|
@@ -129,10 +129,11 @@ DEBUG=engine-* npm start
|
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
In addition there other reference implemetations that can be used:
|
|
132
|
-
- `
|
|
133
|
-
- `
|
|
134
|
-
- `
|
|
135
|
-
- `
|
|
132
|
+
- `examples/demux.ts` : example with demuxed audio
|
|
133
|
+
- `examples/livemix.ts` : example with live mimxing
|
|
134
|
+
- `examples/multicodec.ts` : example with multicodec
|
|
135
|
+
- `examples/drm.ts` : example with DRM
|
|
136
|
+
- `examples/autocreate.ts` : example to auto create channel on demand
|
|
136
137
|
|
|
137
138
|
### Source linking hls-vodtolive library
|
|
138
139
|
|
package/dist/engine/server.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface ChannelEngineOpts {
|
|
|
26
26
|
adXchangeUri?: string;
|
|
27
27
|
noSessionDataTags?: boolean;
|
|
28
28
|
volatileKeyTTL?: number;
|
|
29
|
+
autoCreateSession?: boolean;
|
|
29
30
|
}
|
|
30
31
|
export interface VodRequest {
|
|
31
32
|
sessionId: string;
|
|
@@ -43,6 +44,11 @@ export interface VodTimedMetadata {
|
|
|
43
44
|
'x-channelid'?: string;
|
|
44
45
|
'class': string;
|
|
45
46
|
}
|
|
47
|
+
export interface LiveTimedMetadata {
|
|
48
|
+
'id': string;
|
|
49
|
+
'start-date': string;
|
|
50
|
+
'x-title'?: string;
|
|
51
|
+
}
|
|
46
52
|
export interface VodResponse {
|
|
47
53
|
title: any;
|
|
48
54
|
id: string;
|
|
@@ -92,6 +98,7 @@ export interface SubtitleTracks {
|
|
|
92
98
|
}
|
|
93
99
|
export interface IChannelManager {
|
|
94
100
|
getChannels: () => Channel[];
|
|
101
|
+
autoCreateChannel?: (channelId: string) => void;
|
|
95
102
|
}
|
|
96
103
|
export declare enum ScheduleStreamType {
|
|
97
104
|
LIVE = 1,
|
|
@@ -106,6 +113,7 @@ export interface Schedule {
|
|
|
106
113
|
end_time: number;
|
|
107
114
|
uri: string;
|
|
108
115
|
duration?: number;
|
|
116
|
+
timedMetadata?: LiveTimedMetadata;
|
|
109
117
|
}
|
|
110
118
|
export interface IStreamSwitchManager {
|
|
111
119
|
getSchedule: (channelId: string) => Promise<Schedule[]>;
|
|
@@ -133,6 +141,7 @@ export declare class ChannelEngine {
|
|
|
133
141
|
private logCloudWatchMetrics;
|
|
134
142
|
private adCopyMgrUri?;
|
|
135
143
|
private adXchangeUri?;
|
|
144
|
+
private autoCreateSession;
|
|
136
145
|
constructor(assetMgr: IAssetManager, options?: ChannelEngineOpts);
|
|
137
146
|
updateStreamSwitchAsync(): Promise<void>;
|
|
138
147
|
updateChannelsAsync(channelMgr: any, options: any): Promise<void>;
|
|
@@ -142,6 +151,7 @@ export declare class ChannelEngine {
|
|
|
142
151
|
getSessionCount(): number;
|
|
143
152
|
getPlayheadCount(): number;
|
|
144
153
|
_monitorAsync(session: any, sessionLive: any): Promise<void>;
|
|
154
|
+
createChannel(channelId: any): Promise<void>;
|
|
145
155
|
getMasterManifest(channelId: any): Promise<any>;
|
|
146
156
|
getMediaManifests(channelId: any): Promise<{}>;
|
|
147
157
|
getAudioManifests(channelId: any): Promise<{}>;
|
package/dist/engine/server.js
CHANGED
|
@@ -47,12 +47,13 @@ var SessionLive = require('./session_live.js');
|
|
|
47
47
|
var StreamSwitcher = require('./stream_switcher.js');
|
|
48
48
|
var EventStream = require('./event_stream.js');
|
|
49
49
|
var SubtitleSlicer = require('./subtitle_slicer.js');
|
|
50
|
+
var timer = require('./util.js').timer;
|
|
50
51
|
var SessionStateStore = require('./session_state.js').SessionStateStore;
|
|
51
52
|
var SessionLiveStateStore = require('./session_live_state.js').SessionLiveStateStore;
|
|
52
53
|
var PlayheadStateStore = require('./playhead_state.js').PlayheadStateStore;
|
|
53
54
|
var _a = require('./util.js'), filterQueryParser = _a.filterQueryParser, toHHMMSS = _a.toHHMMSS, WaitTimeGenerator = _a.WaitTimeGenerator;
|
|
54
55
|
var version = require('../package.json').version;
|
|
55
|
-
var
|
|
56
|
+
var AUTO_CREATE_CHANNEL_TIMEOUT = 3000;
|
|
56
57
|
var sessions = {}; // Should be a persistent store...
|
|
57
58
|
var sessionsLive = {}; // Should be a persistent store...
|
|
58
59
|
var sessionSwitchers = {}; // Should be a persistent store...
|
|
@@ -69,6 +70,7 @@ var ScheduleStreamType;
|
|
|
69
70
|
var ChannelEngine = /** @class */ (function () {
|
|
70
71
|
function ChannelEngine(assetMgr, options) {
|
|
71
72
|
var _this = this;
|
|
73
|
+
this.autoCreateSession = false;
|
|
72
74
|
this.options = options;
|
|
73
75
|
if (options && options.adCopyMgrUri) {
|
|
74
76
|
this.adCopyMgrUri = options.adCopyMgrUri;
|
|
@@ -95,6 +97,9 @@ var ChannelEngine = /** @class */ (function () {
|
|
|
95
97
|
if (options && options.streamSwitchManager) {
|
|
96
98
|
this.streamSwitchManager = options.streamSwitchManager;
|
|
97
99
|
}
|
|
100
|
+
if (options && options.autoCreateSession !== undefined) {
|
|
101
|
+
this.autoCreateSession = options.autoCreateSession;
|
|
102
|
+
}
|
|
98
103
|
this.assetMgr = assetMgr;
|
|
99
104
|
this.monitorTimer = {};
|
|
100
105
|
this.server = restify.createServer();
|
|
@@ -417,6 +422,7 @@ var ChannelEngine = /** @class */ (function () {
|
|
|
417
422
|
case 0:
|
|
418
423
|
debug("Do we have any new channels?");
|
|
419
424
|
newChannels = channelMgr.getChannels().filter(function (channel) { return !sessions[channel.id]; });
|
|
425
|
+
debug(newChannels);
|
|
420
426
|
addAsync = function (channel) { return __awaiter(_this, void 0, void 0, function () {
|
|
421
427
|
var _this = this;
|
|
422
428
|
return __generator(this, function (_a) {
|
|
@@ -486,7 +492,7 @@ var ChannelEngine = /** @class */ (function () {
|
|
|
486
492
|
return __generator(this, function (_a) {
|
|
487
493
|
switch (_a.label) {
|
|
488
494
|
case 0:
|
|
489
|
-
debug("Adding channel with ID ".concat(channel.id));
|
|
495
|
+
debug("Adding live channel with ID ".concat(channel.id));
|
|
490
496
|
return [4 /*yield*/, sessionsLive[channel.id].initAsync()];
|
|
491
497
|
case 1:
|
|
492
498
|
_a.sent();
|
|
@@ -652,6 +658,32 @@ var ChannelEngine = /** @class */ (function () {
|
|
|
652
658
|
});
|
|
653
659
|
});
|
|
654
660
|
};
|
|
661
|
+
ChannelEngine.prototype.createChannel = function (channelId) {
|
|
662
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
663
|
+
var _this = this;
|
|
664
|
+
return __generator(this, function (_a) {
|
|
665
|
+
switch (_a.label) {
|
|
666
|
+
case 0:
|
|
667
|
+
if (!!sessions[channelId]) return [3 /*break*/, 2];
|
|
668
|
+
if (!this.options.channelManager.autoCreateChannel) return [3 /*break*/, 2];
|
|
669
|
+
this.options.channelManager.autoCreateChannel(channelId);
|
|
670
|
+
setTimeout(function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
671
|
+
switch (_a.label) {
|
|
672
|
+
case 0: return [4 /*yield*/, this.updateChannelsAsync(this.options.channelManager, this.options)];
|
|
673
|
+
case 1:
|
|
674
|
+
_a.sent();
|
|
675
|
+
return [2 /*return*/];
|
|
676
|
+
}
|
|
677
|
+
}); }); });
|
|
678
|
+
return [4 /*yield*/, timer(AUTO_CREATE_CHANNEL_TIMEOUT)];
|
|
679
|
+
case 1:
|
|
680
|
+
_a.sent();
|
|
681
|
+
_a.label = 2;
|
|
682
|
+
case 2: return [2 /*return*/];
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
};
|
|
655
687
|
ChannelEngine.prototype.getMasterManifest = function (channelId) {
|
|
656
688
|
return __awaiter(this, void 0, void 0, function () {
|
|
657
689
|
var session, masterM3U8, err;
|
|
@@ -835,52 +867,25 @@ var ChannelEngine = /** @class */ (function () {
|
|
|
835
867
|
if (req.query['category']) {
|
|
836
868
|
options.category = req.query['category'];
|
|
837
869
|
}
|
|
838
|
-
if (!(
|
|
839
|
-
|
|
840
|
-
return [
|
|
870
|
+
if (!(this.autoCreateSession && req.query['channel'])) return [3 /*break*/, 2];
|
|
871
|
+
debug("Attempting to create channel with id ".concat(req.query['channel']));
|
|
872
|
+
return [4 /*yield*/, this.createChannel(req.query['channel'])];
|
|
841
873
|
case 1:
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
874
|
+
_a.sent();
|
|
875
|
+
debug("Automatically created channel with id ".concat(req.query['channel']));
|
|
876
|
+
_a.label = 2;
|
|
845
877
|
case 2:
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
options.averageSegmentDuration = this.streamerOpts.defaultAverageSegmentDuration;
|
|
849
|
-
options.useDemuxedAudio = this.useDemuxedAudio;
|
|
850
|
-
options.dummySubtitleEndpoint = this.dummySubtitleEndpoint;
|
|
851
|
-
options.subtitleSliceEndpoint = this.subtitleSliceEndpoint;
|
|
852
|
-
options.useVTTSubtitles = this.useVTTSubtitles;
|
|
853
|
-
options.alwaysNewSegments = this.alwaysNewSegments;
|
|
854
|
-
options.playheadDiffThreshold = this.streamerOpts.defaultPlayheadDiffThreshold;
|
|
855
|
-
options.maxTickInterval = this.streamerOpts.defaultMaxTickInterval;
|
|
856
|
-
options.targetDurationPadding = this.streamerOpts.targetDurationPadding;
|
|
857
|
-
options.forceTargetDuration = this.streamerOpts.forceTargetDuration;
|
|
858
|
-
options.diffCompensationRate = this.streamerOpts.diffCompensationRate;
|
|
859
|
-
// if we are initiating a master manifest
|
|
860
|
-
// outside of specific Channel context,
|
|
861
|
-
// if slate options are set at the ChannelEngine level, then set these here
|
|
862
|
-
if (this.defaultSlateUri) {
|
|
863
|
-
options.slateUri = this.defaultSlateUri;
|
|
878
|
+
if (req.query['channel'] && sessions[req.query['channel']]) {
|
|
879
|
+
session = sessions[req.query['channel']];
|
|
864
880
|
}
|
|
865
|
-
if (
|
|
866
|
-
|
|
881
|
+
else if (req.query['session'] && sessions[req.query['session']]) {
|
|
882
|
+
session = sessions[req.query['session']];
|
|
867
883
|
}
|
|
868
|
-
if (this.slateDuration) {
|
|
869
|
-
options.slateDuration = this.slateDuration;
|
|
870
|
-
}
|
|
871
|
-
options.disabledPlayhead = true; // disable playhead for personalized streams
|
|
872
|
-
session = new Session(this.assetMgr, options, this.sessionStore);
|
|
873
|
-
return [4 /*yield*/, session.initAsync()];
|
|
874
|
-
case 3:
|
|
875
|
-
_a.sent();
|
|
876
|
-
sessions[session.sessionId] = session;
|
|
877
|
-
_a.label = 4;
|
|
878
|
-
case 4:
|
|
879
884
|
if (req.query['startWithId']) {
|
|
880
885
|
options.startWithId = req.query['startWithId'];
|
|
881
886
|
debug("New session to start with assetId=".concat(options.startWithId));
|
|
882
887
|
}
|
|
883
|
-
if (!session) return [3 /*break*/,
|
|
888
|
+
if (!session) return [3 /*break*/, 7];
|
|
884
889
|
eventStream = new EventStream(session);
|
|
885
890
|
eventStreams[session.sessionId] = eventStream;
|
|
886
891
|
filter = void 0;
|
|
@@ -888,11 +893,11 @@ var ChannelEngine = /** @class */ (function () {
|
|
|
888
893
|
debug("Applying filter on master manifest ".concat(req.query['filter']));
|
|
889
894
|
filter = filterQueryParser(req.query['filter']);
|
|
890
895
|
}
|
|
891
|
-
_a.label =
|
|
892
|
-
case
|
|
893
|
-
_a.trys.push([
|
|
896
|
+
_a.label = 3;
|
|
897
|
+
case 3:
|
|
898
|
+
_a.trys.push([3, 5, , 6]);
|
|
894
899
|
return [4 /*yield*/, session.getMasterManifestAsync(filter)];
|
|
895
|
-
case
|
|
900
|
+
case 4:
|
|
896
901
|
body = _a.sent();
|
|
897
902
|
res.sendRaw(200, Buffer.from(body, 'utf8'), {
|
|
898
903
|
"Content-Type": "application/vnd.apple.mpegurl",
|
|
@@ -904,16 +909,16 @@ var ChannelEngine = /** @class */ (function () {
|
|
|
904
909
|
"X-Instance-Id": this.instanceId + "<".concat(version, ">"),
|
|
905
910
|
});
|
|
906
911
|
next();
|
|
907
|
-
return [3 /*break*/,
|
|
908
|
-
case
|
|
912
|
+
return [3 /*break*/, 6];
|
|
913
|
+
case 5:
|
|
909
914
|
err_4 = _a.sent();
|
|
910
915
|
next(this._errorHandler(err_4));
|
|
911
|
-
return [3 /*break*/,
|
|
912
|
-
case
|
|
913
|
-
case
|
|
916
|
+
return [3 /*break*/, 6];
|
|
917
|
+
case 6: return [3 /*break*/, 8];
|
|
918
|
+
case 7:
|
|
914
919
|
next(this._gracefulErrorHandler("Could not find a valid session"));
|
|
915
|
-
_a.label =
|
|
916
|
-
case
|
|
920
|
+
_a.label = 8;
|
|
921
|
+
case 8: return [2 /*return*/];
|
|
917
922
|
}
|
|
918
923
|
});
|
|
919
924
|
});
|
package/dist/engine/session.d.ts
CHANGED
|
@@ -117,4 +117,5 @@ declare class Session {
|
|
|
117
117
|
_getSubtitlePlayheadPosition(seqIdx: any): Promise<any>;
|
|
118
118
|
_getLastDuration(manifest: any): Promise<any>;
|
|
119
119
|
_getPlayheadDiffCompensationValue(diffMs: any, thresholdMs: any): number;
|
|
120
|
+
_isOldVod(refTs: any, vodDur: any): boolean;
|
|
120
121
|
}
|
package/dist/engine/session.js
CHANGED
|
@@ -1062,7 +1062,7 @@ var Session = /** @class */ (function () {
|
|
|
1062
1062
|
};
|
|
1063
1063
|
Session.prototype.incrementAsync = function () {
|
|
1064
1064
|
return __awaiter(this, void 0, void 0, function () {
|
|
1065
|
-
var isLeader, sessionState, playheadState, currentVod, _a, leaderAction, _b, m3u8_4, _c, audioIncrement, positionV, positionA, position, currentVod_1, sessionState_1, posDiff, incrementValue, index, audioSeqLastIdx, thresh, audioPosition, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, mediaSequenceValue, audioInfo, mediaSequenceValueAudio, diffMs, m3u8;
|
|
1065
|
+
var isLeader, sessionState, playheadState, currentVod, _a, isOldVod, nextState, leaderAction, leaderState, _b, m3u8_4, _c, audioIncrement, positionV, positionA, position, currentVod_1, sessionState_1, posDiff, incrementValue, index, audioSeqLastIdx, thresh, audioPosition, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, mediaSequenceValue, audioInfo, mediaSequenceValueAudio, diffMs, m3u8;
|
|
1066
1066
|
return __generator(this, function (_s) {
|
|
1067
1067
|
switch (_s.label) {
|
|
1068
1068
|
case 0: return [4 /*yield*/, this._tickAsync()];
|
|
@@ -1074,7 +1074,7 @@ var Session = /** @class */ (function () {
|
|
|
1074
1074
|
return [4 /*yield*/, this._sessionState.getValues(["state", "mediaSeq", "mediaSeqAudio", "mediaSeqSubtitle", "discSeq", "discSeqAudio", "discSeqSubtitle", "vodMediaSeqVideo", "vodMediaSeqAudio", "vodMediaSeqSubtitle"])];
|
|
1075
1075
|
case 3:
|
|
1076
1076
|
sessionState = _s.sent();
|
|
1077
|
-
return [4 /*yield*/, this._playheadState.getValues(["mediaSeq", "mediaSeqAudio", "mediaSeqSubtitle", "vodMediaSeqVideo", "vodMediaSeqAudio", "vodMediaSubtitle"])];
|
|
1077
|
+
return [4 /*yield*/, this._playheadState.getValues(["mediaSeq", "mediaSeqAudio", "mediaSeqSubtitle", "vodMediaSeqVideo", "vodMediaSeqAudio", "vodMediaSubtitle", "playheadRef"])];
|
|
1078
1078
|
case 4:
|
|
1079
1079
|
playheadState = _s.sent();
|
|
1080
1080
|
return [4 /*yield*/, this._sessionState.getCurrentVod()];
|
|
@@ -1103,10 +1103,16 @@ var Session = /** @class */ (function () {
|
|
|
1103
1103
|
case 9:
|
|
1104
1104
|
if (!(sessionState.state === SessionState.VOD_NEXT_INITIATING || sessionState.state === SessionState.VOD_RELOAD_INITIATING)) return [3 /*break*/, 14];
|
|
1105
1105
|
if (!isLeader) return [3 /*break*/, 11];
|
|
1106
|
+
isOldVod = this._isOldVod(playheadState.playheadRef, currentVod.getDuration());
|
|
1107
|
+
nextState = SessionState.VOD_PLAYING;
|
|
1108
|
+
if (isOldVod) {
|
|
1109
|
+
nextState = SessionState.VOD_NEXT_INIT;
|
|
1110
|
+
}
|
|
1106
1111
|
leaderAction = sessionState.state === SessionState.VOD_NEXT_INITIATING ? "initiated" : "reloaded";
|
|
1107
|
-
|
|
1112
|
+
leaderState = nextState === SessionState.VOD_NEXT_INIT ? "VOD_NEXT_INIT" : "VOD_PLAYING";
|
|
1113
|
+
debug("[".concat(this._sessionId, "]: I am the leader and have just ").concat(leaderAction, " next VOD, let's move to ").concat(leaderState));
|
|
1108
1114
|
_b = sessionState;
|
|
1109
|
-
return [4 /*yield*/, this._sessionState.set("state",
|
|
1115
|
+
return [4 /*yield*/, this._sessionState.set("state", nextState)];
|
|
1110
1116
|
case 10:
|
|
1111
1117
|
_b.state = _s.sent();
|
|
1112
1118
|
return [3 /*break*/, 13];
|
|
@@ -1999,7 +2005,7 @@ var Session = /** @class */ (function () {
|
|
|
1999
2005
|
debug("[".concat(this._sessionId, "]: state=VOD_PLAYING (").concat(sessionState.vodMediaSeqVideo, "_").concat(sessionState.vodMediaSeqAudio, "_").concat(sessionState.vodMediaSeqSubtitle, ", ").concat(currentVod.getLiveMediaSequencesCount(), "_").concat(currentVod.getLiveMediaSequencesCount("audio"), "_").concat(currentVod.getLiveMediaSequencesCount("subtitle"), ")"));
|
|
2000
2006
|
return [2 /*return*/];
|
|
2001
2007
|
case 36:
|
|
2002
|
-
debug("[".concat(this._sessionId, "]: state=VOD_NEXT_INITIATING (").concat(sessionState.vodMediaSeqVideo, "_").concat(sessionState.vodMediaSeqAudio, "_").concat(sessionState.vodMediaSeqSubtitle, ", ").concat(currentVod.getLiveMediaSequencesCount(), "_").concat(currentVod.getLiveMediaSequencesCount("audio"), "_").concat(currentVod.getLiveMediaSequencesCount("
|
|
2008
|
+
debug("[".concat(this._sessionId, "]: state=VOD_NEXT_INITIATING (").concat(sessionState.vodMediaSeqVideo, "_").concat(sessionState.vodMediaSeqAudio, "_").concat(sessionState.vodMediaSeqSubtitle, ", ").concat(currentVod.getLiveMediaSequencesCount(), "_").concat(currentVod.getLiveMediaSequencesCount("audio"), "_").concat(currentVod.getLiveMediaSequencesCount("subtitle"), ")"));
|
|
2003
2009
|
if (!isLeader) {
|
|
2004
2010
|
debug("[".concat(this._sessionId, "]: not the leader so just waiting for the VOD to be initiated"));
|
|
2005
2011
|
}
|
|
@@ -2149,7 +2155,7 @@ var Session = /** @class */ (function () {
|
|
|
2149
2155
|
return [4 /*yield*/, this._sessionState.set("discSeqSubtitle", sessionState.discSeqSubtitle + lastDiscontinuitySubtitle)];
|
|
2150
2156
|
case 54:
|
|
2151
2157
|
_2.discSeqSubtitle = _8.sent();
|
|
2152
|
-
debug("[".concat(this._sessionId, "]: new sequence data set in store [").concat(sessionState.mediaSeq, "][").concat(sessionState.discSeq, "]
|
|
2158
|
+
debug("[".concat(this._sessionId, "]: new sequence data set in store V[").concat(sessionState.mediaSeq, "][").concat(sessionState.discSeq, "]_A[").concat(sessionState.mediaSeqAudio, "][").concat(sessionState.discSeqAudio, "]_S[").concat(sessionState.mediaSeqSubtitle, "][").concat(sessionState.discSeqSubtitle, "]"));
|
|
2153
2159
|
return [4 /*yield*/, this._sessionState.remove("nextVod")];
|
|
2154
2160
|
case 55:
|
|
2155
2161
|
_8.sent();
|
|
@@ -2698,6 +2704,15 @@ var Session = /** @class */ (function () {
|
|
|
2698
2704
|
return compensationSec;
|
|
2699
2705
|
}
|
|
2700
2706
|
};
|
|
2707
|
+
Session.prototype._isOldVod = function (refTs, vodDur) {
|
|
2708
|
+
var TIME_PADDING_MS = 20 * 1000; // 20secs
|
|
2709
|
+
var VOD_DURATION_MS = vodDur * 1000;
|
|
2710
|
+
var TIME_SINCE_VOD_STARTED_MS = (Date.now() - refTs) + TIME_PADDING_MS;
|
|
2711
|
+
if (TIME_SINCE_VOD_STARTED_MS > VOD_DURATION_MS) {
|
|
2712
|
+
return true;
|
|
2713
|
+
}
|
|
2714
|
+
return false;
|
|
2715
|
+
};
|
|
2701
2716
|
return Session;
|
|
2702
2717
|
}());
|
|
2703
2718
|
module.exports = Session;
|
|
@@ -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.2",
|
|
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,6 +87,12 @@ 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;
|
|
@@ -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();
|
|
@@ -1434,7 +1440,7 @@ class Session {
|
|
|
1434
1440
|
debug(`[${this._sessionId}]: state=VOD_PLAYING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`);
|
|
1435
1441
|
return;
|
|
1436
1442
|
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("
|
|
1443
|
+
debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`);
|
|
1438
1444
|
if (!isLeader) {
|
|
1439
1445
|
debug(`[${this._sessionId}]: not the leader so just waiting for the VOD to be initiated`);
|
|
1440
1446
|
}
|
|
@@ -1542,7 +1548,7 @@ class Session {
|
|
|
1542
1548
|
sessionState.discSeq = await this._sessionState.set("discSeq", sessionState.discSeq + lastDiscontinuity);
|
|
1543
1549
|
sessionState.discSeqAudio = await this._sessionState.set("discSeqAudio", sessionState.discSeqAudio + lastDiscontinuityAudio);
|
|
1544
1550
|
sessionState.discSeqSubtitle = await this._sessionState.set("discSeqSubtitle", sessionState.discSeqSubtitle + lastDiscontinuitySubtitle);
|
|
1545
|
-
debug(`[${this._sessionId}]: new sequence data set in store [${sessionState.mediaSeq}][${sessionState.discSeq}]
|
|
1551
|
+
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
1552
|
await this._sessionState.remove("nextVod");
|
|
1547
1553
|
sessionState.currentVod = await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
|
|
1548
1554
|
this.leaderIsSettingNextVod = false;
|
|
@@ -1983,6 +1989,16 @@ class Session {
|
|
|
1983
1989
|
return compensationSec;
|
|
1984
1990
|
}
|
|
1985
1991
|
}
|
|
1992
|
+
|
|
1993
|
+
_isOldVod(refTs, vodDur) {
|
|
1994
|
+
const TIME_PADDING_MS = 20 * 1000 // 20secs
|
|
1995
|
+
const VOD_DURATION_MS = vodDur * 1000;
|
|
1996
|
+
const TIME_SINCE_VOD_STARTED_MS = (Date.now() - refTs) + TIME_PADDING_MS;
|
|
1997
|
+
if (TIME_SINCE_VOD_STARTED_MS > VOD_DURATION_MS) {
|
|
1998
|
+
return true;
|
|
1999
|
+
}
|
|
2000
|
+
return false;
|
|
2001
|
+
}
|
|
1986
2002
|
}
|
|
1987
2003
|
|
|
1988
2004
|
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);
|
|
@@ -6,7 +6,7 @@ import { ChannelEngine, ChannelEngineOpts,
|
|
|
6
6
|
IAssetManager, IChannelManager, IStreamSwitchManager,
|
|
7
7
|
VodRequest, VodResponse, Channel, ChannelProfile,
|
|
8
8
|
Schedule
|
|
9
|
-
} from "
|
|
9
|
+
} from "../index";
|
|
10
10
|
const { v4: uuidv4 } = require('uuid');
|
|
11
11
|
|
|
12
12
|
const STITCH_ENDPOINT = process.env.STITCH_ENDPOINT || "http://lambda.eyevinn.technology/stitch/master.m3u8";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eyevinn-channel-engine",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.2",
|
|
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/tsconfig.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|