eyevinn-channel-engine 4.2.1 → 4.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +6 -5
  2. package/dist/engine/server.d.ts +11 -1
  3. package/dist/engine/server.js +56 -51
  4. package/dist/engine/session.d.ts +2 -0
  5. package/dist/engine/session.js +352 -293
  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 +36 -34
  17. package/engine/session.js +60 -7
  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
@@ -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.3",
4
4
  "description": "OTT TV Channel Engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,9 +10,9 @@
10
10
  "test": "jasmine",
11
11
  "version:rc": "npm version prerelease --preid=rc",
12
12
  "postversion": "git push && git push --tags",
13
- "start": "node dist/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,13 +87,19 @@ export interface VodTimedMetadata {
85
87
  'class': string;
86
88
  }
87
89
 
90
+ export interface LiveTimedMetadata {
91
+ 'id': string;
92
+ 'start-date': string;
93
+ 'x-title'?: string;
94
+ }
95
+
88
96
  export interface VodResponse {
89
97
  title: any;
90
98
  id: string;
91
99
  uri: string;
92
100
  offset?: number;
93
101
  diffMs?: number;
94
- desiredDuraiton?: number;
102
+ desiredDuration?: number;
95
103
  type?: string;
96
104
  currentMetadata?: VodResponseMetadata;
97
105
  timedMetadata?: VodTimedMetadata;
@@ -142,6 +150,7 @@ export interface SubtitleTracks {
142
150
 
143
151
  export interface IChannelManager {
144
152
  getChannels: () => Channel[];
153
+ autoCreateChannel?: (channelId: string) => void;
145
154
  }
146
155
 
147
156
  export enum ScheduleStreamType {
@@ -158,6 +167,7 @@ export interface Schedule {
158
167
  end_time: number;
159
168
  uri: string;
160
169
  duration?: number;
170
+ timedMetadata?: LiveTimedMetadata;
161
171
  }
162
172
 
163
173
  export interface IStreamSwitchManager {
@@ -187,6 +197,7 @@ export class ChannelEngine {
187
197
  private logCloudWatchMetrics: boolean;
188
198
  private adCopyMgrUri?: string;
189
199
  private adXchangeUri?: string;
200
+ private autoCreateSession: boolean = false;
190
201
 
191
202
  constructor(assetMgr: IAssetManager, options?: ChannelEngineOpts) {
192
203
  this.options = options;
@@ -217,6 +228,9 @@ export class ChannelEngine {
217
228
  if (options && options.streamSwitchManager) {
218
229
  this.streamSwitchManager = options.streamSwitchManager;
219
230
  }
231
+ if (options && options.autoCreateSession !== undefined) {
232
+ this.autoCreateSession = options.autoCreateSession;
233
+ }
220
234
  this.assetMgr = assetMgr;
221
235
  this.monitorTimer = {};
222
236
  this.server = restify.createServer();
@@ -428,6 +442,7 @@ export class ChannelEngine {
428
442
  async updateChannelsAsync(channelMgr, options) {
429
443
  debug(`Do we have any new channels?`);
430
444
  const newChannels = channelMgr.getChannels().filter(channel => !sessions[channel.id]);
445
+ debug(newChannels);
431
446
  const addAsync = async (channel) => {
432
447
  debug(`Adding channel with ID ${channel.id}`);
433
448
  sessions[channel.id] = new Session(this.assetMgr, {
@@ -472,14 +487,14 @@ export class ChannelEngine {
472
487
  await sessions[channel.id].initAsync();
473
488
  await sessionsLive[channel.id].initAsync();
474
489
  if (!this.monitorTimer[channel.id]) {
475
- this.monitorTimer[channel.id] = 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();
@@ -1325,7 +1331,12 @@ class Session {
1325
1331
  })
1326
1332
  }
1327
1333
  currentVod = newVod;
1328
- loadPromise = currentVod.load();
1334
+ if (vodResponse.desiredDuration) {
1335
+ const { mediaManifestLoader, audioManifestLoader} = await this._truncateVod(vodResponse);
1336
+ loadPromise = currentVod.load(null, mediaManifestLoader, audioManifestLoader);
1337
+ } else {
1338
+ loadPromise = currentVod.load();
1339
+ }
1329
1340
  } else {
1330
1341
  if (vodResponse.type === 'gap') {
1331
1342
  loadPromise = new Promise((resolve, reject) => {
@@ -1434,7 +1445,7 @@ class Session {
1434
1445
  debug(`[${this._sessionId}]: state=VOD_PLAYING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`);
1435
1446
  return;
1436
1447
  case SessionState.VOD_NEXT_INITIATING:
1437
- debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`);
1448
+ debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`);
1438
1449
  if (!isLeader) {
1439
1450
  debug(`[${this._sessionId}]: not the leader so just waiting for the VOD to be initiated`);
1440
1451
  }
@@ -1503,7 +1514,12 @@ class Session {
1503
1514
  title: this.currentMetadata.title || '',
1504
1515
  }
1505
1516
  });
1506
- loadPromise = newVod.loadAfter(currentVod);
1517
+ if (vodResponse.desiredDuration) {
1518
+ const { mediaManifestLoader, audioManifestLoader} = await this._truncateVod(vodResponse);
1519
+ loadPromise = newVod.loadAfter(currentVod, null, mediaManifestLoader, audioManifestLoader);
1520
+ } else {
1521
+ loadPromise = newVod.loadAfter(currentVod);
1522
+ }
1507
1523
  if (vodResponse.diffMs) {
1508
1524
  this.diffCompensation = vodResponse.diffMs;
1509
1525
  if (this.diffCompensation) {
@@ -1542,7 +1558,7 @@ class Session {
1542
1558
  sessionState.discSeq = await this._sessionState.set("discSeq", sessionState.discSeq + lastDiscontinuity);
1543
1559
  sessionState.discSeqAudio = await this._sessionState.set("discSeqAudio", sessionState.discSeqAudio + lastDiscontinuityAudio);
1544
1560
  sessionState.discSeqSubtitle = await this._sessionState.set("discSeqSubtitle", sessionState.discSeqSubtitle + lastDiscontinuitySubtitle);
1545
- debug(`[${this._sessionId}]: new sequence data set in store [${sessionState.mediaSeq}][${sessionState.discSeq}]_[${sessionState.mediaSeqSubtitle}][${sessionState.discSeqSubtitle}]_[${sessionState.mediaSeqSubtitle}][${sessionState.discSeqSubtitle}]`);
1561
+ debug(`[${this._sessionId}]: new sequence data set in store V[${sessionState.mediaSeq}][${sessionState.discSeq}]_A[${sessionState.mediaSeqAudio}][${sessionState.discSeqAudio}]_S[${sessionState.mediaSeqSubtitle}][${sessionState.discSeqSubtitle}]`);
1546
1562
  await this._sessionState.remove("nextVod");
1547
1563
  sessionState.currentVod = await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 });
1548
1564
  this.leaderIsSettingNextVod = false;
@@ -1776,6 +1792,33 @@ class Session {
1776
1792
  });
1777
1793
  }
1778
1794
 
1795
+ _truncateVod(vodResponse) {
1796
+ return new Promise((resolve, reject) => {
1797
+ const truncatedVod = new HLSTruncateVod(vodResponse.uri, vodResponse.desiredDuration / 1000);
1798
+ truncatedVod.load()
1799
+ .then(() => {
1800
+ let audioManifestLoader;
1801
+ const mediaManifestLoader = (bw) => {
1802
+ let mediaManifestStream = new Readable();
1803
+ mediaManifestStream.push(truncatedVod.getMediaManifest(bw));
1804
+ mediaManifestStream.push(null);
1805
+ return mediaManifestStream;
1806
+ };
1807
+ if (this.use_demuxed_audio) {
1808
+ audioManifestLoader = (audioGroupId, audioLanguage) => {
1809
+ let mediaManifestStream = new Readable();
1810
+ mediaManifestStream.push(truncatedVod.getAudioManifest(audioGroupId, audioLanguage));
1811
+ mediaManifestStream.push(null);
1812
+ return mediaManifestStream;
1813
+ };
1814
+ }
1815
+ resolve({ mediaManifestLoader, audioManifestLoader });
1816
+ });
1817
+ });
1818
+
1819
+
1820
+ }
1821
+
1779
1822
  _truncateSlate(afterVod, requestedDuration, vodUri) {
1780
1823
  return new Promise((resolve, reject) => {
1781
1824
  let nexVodUri = null;
@@ -1983,6 +2026,16 @@ class Session {
1983
2026
  return compensationSec;
1984
2027
  }
1985
2028
  }
2029
+
2030
+ _isOldVod(refTs, vodDur) {
2031
+ const TIME_PADDING_MS = 20 * 1000 // 20secs
2032
+ const VOD_DURATION_MS = vodDur * 1000;
2033
+ const TIME_SINCE_VOD_STARTED_MS = (Date.now() - refTs) + TIME_PADDING_MS;
2034
+ if (TIME_SINCE_VOD_STARTED_MS > VOD_DURATION_MS) {
2035
+ return true;
2036
+ }
2037
+ return false;
2038
+ }
1986
2039
  }
1987
2040
 
1988
2041
  module.exports = Session;
@@ -238,8 +238,10 @@ class StreamSwitcher {
238
238
  // Insert preroll if available for current channel
239
239
  if (this.prerollsCache[this.sessionId]) {
240
240
  const prerollSegments = this.prerollsCache[this.sessionId].segments;
241
+ this._insertTimedMetadata(prerollSegments, scheduleObj.timedMetadata || {});
241
242
  currVodSegments = this._mergeSegments(prerollSegments, currVodSegments, false);
242
243
  }
244
+
243
245
  // In risk that the SL-playhead might have updated some data after
244
246
  // we reset last time... we should Reset SessionLive before sending new data.
245
247
  await sessionLive.resetLiveStoreAsync(0);
@@ -397,6 +399,7 @@ class StreamSwitcher {
397
399
  // Insert preroll, if available, for current channel
398
400
  if (this.prerollsCache[this.sessionId]) {
399
401
  const prerollSegments = this.prerollsCache[this.sessionId].segments;
402
+ this._insertTimedMetadata(prerollSegments, scheduleObj.timedMetadata || {});
400
403
  eventSegments.currMseqSegs = this._mergeSegments(prerollSegments, eventSegments.currMseqSegs, false);
401
404
  }
402
405
 
@@ -639,6 +642,21 @@ class StreamSwitcher {
639
642
  });
640
643
  return OUTPUT_SEGMENTS;
641
644
  }
645
+
646
+ _insertTimedMetadata(segments, timedMetadata) {
647
+ const bandwidths = Object.keys(segments);
648
+ debug(`[${this.sessionId}]: Inserting timed metadata ${Object.keys(timedMetadata).join(',')}`);
649
+ bandwidths.forEach((bw) => {
650
+ let daterangeData = segments[bw][0]["daterange"];
651
+ if (!daterangeData) {
652
+ daterangeData = {};
653
+ Object.keys(timedMetadata).forEach((k) => {
654
+ daterangeData[k] = timedMetadata[k];
655
+ });
656
+ }
657
+ segments[bw][0]["daterange"] = daterangeData;
658
+ });
659
+ }
642
660
  }
643
661
 
644
662
  module.exports = StreamSwitcher;
@@ -0,0 +1,112 @@
1
+ /*
2
+ * Reference implementation of Channel Engine library
3
+ */
4
+
5
+ import { ChannelEngine, ChannelEngineOpts,
6
+ IAssetManager, IChannelManager,
7
+ VodRequest, VodResponse, Channel, ChannelProfile
8
+ } from "../index";
9
+
10
+ const DEFAULT_ASSETS = [
11
+ {
12
+ id: 1,
13
+ title: "Tears of Steel",
14
+ uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/master.m3u8",
15
+ },
16
+ {
17
+ id: 2,
18
+ title: "VINN",
19
+ uri: "https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8",
20
+ },
21
+ ];
22
+
23
+ class RefAssetManager implements IAssetManager {
24
+ private assets;
25
+ private pos;
26
+
27
+ constructor(opts?) {
28
+ this.assets = {};
29
+ this.assets['1'] = DEFAULT_ASSETS;
30
+ this.pos = {
31
+ '1': 0,
32
+ };
33
+ }
34
+
35
+ /**
36
+ *
37
+ * @param {Object} vodRequest
38
+ * {
39
+ * sessionId,
40
+ * category,
41
+ * playlistId
42
+ * }
43
+ */
44
+ getNextVod(vodRequest: VodRequest): Promise<VodResponse> {
45
+ return new Promise((resolve, reject) => {
46
+ const channelId = vodRequest.playlistId;
47
+ if (this.pos[channelId] === undefined) {
48
+ this.pos[channelId] = 0;
49
+ this.assets[channelId] = DEFAULT_ASSETS;
50
+ }
51
+ if (this.assets[channelId]) {
52
+ let vod = this.assets[channelId][this.pos[channelId]++];
53
+ if (this.pos[channelId] > this.assets[channelId].length - 1) {
54
+ this.pos[channelId] = 0;
55
+ }
56
+ const vodResponse = {
57
+ id: vod.id,
58
+ title: vod.title,
59
+ uri: vod.uri,
60
+ };
61
+ resolve(vodResponse);
62
+ } else {
63
+ reject("Invalid channelId provided");
64
+ }
65
+ });
66
+ }
67
+ }
68
+
69
+ class RefChannelManager implements IChannelManager {
70
+ private channels: Channel[] = [];
71
+
72
+ constructor() {
73
+ this.channels.push({ id: "1", profile: this._getProfile() });
74
+ }
75
+
76
+ getChannels(): Channel[] {
77
+ //return [ { id: '1', profile: this._getProfile() }, { id: 'faulty', profile: this._getProfile() } ];
78
+ //return [{ id: "1", profile: this._getProfile() }];
79
+ return this.channels;
80
+ }
81
+
82
+ autoCreateChannel(channelId: string) {
83
+ if (!this.channels.find((ch) => ch.id === channelId)) {
84
+ this.channels.push({ id: channelId, profile: this._getProfile() });
85
+ }
86
+ }
87
+
88
+ _getProfile(): ChannelProfile[] {
89
+ return [
90
+ { bw: 6134000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [1024, 458] },
91
+ { bw: 2323000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [640, 286] },
92
+ { bw: 1313000, codecs: "avc1.4d001f,mp4a.40.2", resolution: [480, 214] },
93
+ ];
94
+ }
95
+ }
96
+
97
+ const refAssetManager = new RefAssetManager();
98
+ const refChannelManager = new RefChannelManager();
99
+
100
+ const engineOptions: ChannelEngineOpts = {
101
+ heartbeat: "/",
102
+ averageSegmentDuration: 2000,
103
+ channelManager: refChannelManager,
104
+ autoCreateSession: true,
105
+ defaultSlateUri:
106
+ "https://maitv-vod.lab.eyevinn.technology/slate-consuo.mp4/master.m3u8",
107
+ slateRepetitions: 10,
108
+ };
109
+
110
+ const engine = new ChannelEngine(refAssetManager, engineOptions);
111
+ engine.start();
112
+ engine.listen(process.env.PORT || 8000);