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.
Files changed (31) hide show
  1. package/README.md +6 -5
  2. package/dist/engine/server.d.ts +10 -0
  3. package/dist/engine/server.js +56 -51
  4. package/dist/engine/session.d.ts +1 -0
  5. package/dist/engine/session.js +21 -6
  6. package/dist/engine/stream_switcher.d.ts +1 -0
  7. package/dist/engine/stream_switcher.js +16 -0
  8. package/dist/examples/autocreate.js +99 -0
  9. package/dist/{server.js → examples/default.js} +1 -1
  10. package/dist/{server-demux.js → examples/demux.js} +1 -1
  11. package/dist/{server-drm.js → examples/drm.js} +1 -1
  12. package/dist/{server-livemix.js → examples/livemix.js} +1 -1
  13. package/dist/examples/multicodec.d.ts +1 -0
  14. package/dist/{server-multicodec.js → examples/multicodec.js} +1 -1
  15. package/dist/package.json +4 -4
  16. package/engine/server.ts +35 -33
  17. package/engine/session.js +21 -5
  18. package/engine/stream_switcher.js +18 -0
  19. package/examples/autocreate.ts +112 -0
  20. package/{server.ts → examples/default.ts} +1 -1
  21. package/{server-demux.ts → examples/demux.ts} +1 -1
  22. package/{server-drm.ts → examples/drm.ts} +1 -1
  23. package/{server-livemix.ts → examples/livemix.ts} +1 -1
  24. package/{server-multicodec.ts → examples/multicodec.ts} +1 -1
  25. package/package.json +4 -4
  26. package/tsconfig.json +1 -5
  27. /package/dist/{server-demux.d.ts → examples/autocreate.d.ts} +0 -0
  28. /package/dist/{server-drm.d.ts → examples/default.d.ts} +0 -0
  29. /package/dist/{server-livemix.d.ts → examples/demux.d.ts} +0 -0
  30. /package/dist/{server-multicodec.d.ts → examples/drm.d.ts} +0 -0
  31. /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 `server.ts` run:
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
- - `server-demux.ts` : example with demuxed audio
133
- - `server-livemix.ts` : example with live mimxing
134
- - `server-multicodec.ts` : example with multicodec
135
- - `server-drm.ts` : example with DRM
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
 
@@ -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<{}>;
@@ -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 timer = function (ms) { return new Promise(function (res) { return setTimeout(res, ms); }); };
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 (!(req.query['channel'] && sessions[req.query['channel']])) return [3 /*break*/, 1];
839
- session = sessions[req.query['channel']];
840
- return [3 /*break*/, 4];
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
- if (!(req.query['session'] && sessions[req.query['session']])) return [3 /*break*/, 2];
843
- session = sessions[req.query['session']];
844
- return [3 /*break*/, 4];
874
+ _a.sent();
875
+ debug("Automatically created channel with id ".concat(req.query['channel']));
876
+ _a.label = 2;
845
877
  case 2:
846
- options.adCopyMgrUri = this.adCopyMgrUri;
847
- options.adXchangeUri = this.adXchangeUri;
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 (this.slateRepetitions) {
866
- options.slateRepetitions = this.slateRepetitions;
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*/, 9];
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 = 5;
892
- case 5:
893
- _a.trys.push([5, 7, , 8]);
896
+ _a.label = 3;
897
+ case 3:
898
+ _a.trys.push([3, 5, , 6]);
894
899
  return [4 /*yield*/, session.getMasterManifestAsync(filter)];
895
- case 6:
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*/, 8];
908
- case 7:
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*/, 8];
912
- case 8: return [3 /*break*/, 10];
913
- case 9:
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 = 10;
916
- case 10: return [2 /*return*/];
920
+ _a.label = 8;
921
+ case 8: return [2 /*return*/];
917
922
  }
918
923
  });
919
924
  });
@@ -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
  }
@@ -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
- debug("[".concat(this._sessionId, "]: I am the leader and have just ").concat(leaderAction, " next VOD, let's move to VOD_PLAYING"));
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", SessionState.VOD_PLAYING)];
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("audio"), "_").concat(currentVod.getLiveMediaSequencesCount("subtitle"), ")"));
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, "]_[").concat(sessionState.mediaSeqSubtitle, "][").concat(sessionState.discSeqSubtitle, "]_[").concat(sessionState.mediaSeqSubtitle, "][").concat(sessionState.discSeqSubtitle, "]"));
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;
@@ -26,4 +26,5 @@ declare class StreamSwitcher {
26
26
  _loadPreroll(uri: any): Promise<{}>;
27
27
  _fetchParseM3u8(uri: any): Promise<any>;
28
28
  _mergeSegments(fromSegments: any, toSegments: any, prepend: any): {};
29
+ _insertTimedMetadata(segments: any, timedMetadata: any): void;
29
30
  }
@@ -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("./index");
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("./index");
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("./index");
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("./index");
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("./index");
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.1",
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/server.js",
14
- "start-demux": "node dist/server-demux.js",
15
- "start-livemix": "node dist/server-livemix.js"
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 timer = ms => new Promise(res => setTimeout(res, ms));
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] = setInterval(async () => { await this._monitorAsync(sessions[channel.id], sessionsLive[channel.id]) }, 5000);
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
- debug(`[${this._sessionId}]: I am the leader and have just ${leaderAction} next VOD, let's move to VOD_PLAYING`);
807
- sessionState.state = await this._sessionState.set("state", SessionState.VOD_PLAYING);
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("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`);
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}]_[${sessionState.mediaSeqSubtitle}][${sessionState.discSeqSubtitle}]_[${sessionState.mediaSeqSubtitle}][${sessionState.discSeqSubtitle}]`);
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);
@@ -5,7 +5,7 @@
5
5
  import { ChannelEngine, ChannelEngineOpts,
6
6
  IAssetManager, IChannelManager,
7
7
  VodRequest, VodResponse, Channel, ChannelProfile
8
- } from "./index";
8
+ } from "../index";
9
9
 
10
10
  const STITCH_ENDPOINT =
11
11
  process.env.STITCH_ENDPOINT ||
@@ -13,7 +13,7 @@ import {
13
13
  ChannelProfile,
14
14
  AudioTracks,
15
15
  SubtitleTracks,
16
- } from "./index";
16
+ } from "../index";
17
17
 
18
18
  class RefAssetManager implements IAssetManager {
19
19
  private assets;
@@ -14,7 +14,7 @@ import {
14
14
  Channel,
15
15
  ChannelProfile,
16
16
  AudioTracks,
17
- } from "./index";
17
+ } from "../index";
18
18
 
19
19
  class RefAssetManager implements IAssetManager {
20
20
  private assets;
@@ -6,7 +6,7 @@ import { ChannelEngine, ChannelEngineOpts,
6
6
  IAssetManager, IChannelManager, IStreamSwitchManager,
7
7
  VodRequest, VodResponse, Channel, ChannelProfile,
8
8
  Schedule
9
- } from "./index";
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";
@@ -6,7 +6,7 @@ import { ChannelEngine, ChannelEngineOpts,
6
6
  IAssetManager, IChannelManager,
7
7
  VodRequest, VodResponse, Channel, ChannelProfile,
8
8
  AudioTracks
9
- } from "./index";
9
+ } from "../index";
10
10
 
11
11
  class RefAssetManager implements IAssetManager {
12
12
  private assets;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyevinn-channel-engine",
3
- "version": "4.2.1",
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/server.js",
14
- "start-demux": "node dist/server-demux.js",
15
- "start-livemix": "node dist/server-livemix.js"
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
@@ -7,11 +7,7 @@
7
7
  "declaration": true
8
8
  },
9
9
  "include": [
10
- "./server.ts",
11
- "./server-demux.ts",
12
- "./server-livemix.ts",
13
- "./server-multicodec.ts",
14
- "./server-drm.ts",
10
+ "./examples/*",
15
11
  "./engine/**/*"
16
12
  ]
17
13
  }
File without changes
File without changes