New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More →

eyevinn-channel-engine

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

eyevinn-channel-engine - npm Package Compare versions

Comparing version

to
2.6.0

@@ -8,2 +8,5 @@ const restify = require('restify');

const { SessionStateStore } = require('./session_state.js');
const { PlayheadStateStore } = require('./playhead_state.js');
const sessions = {}; // Should be a persistent store...

@@ -14,2 +17,3 @@ const eventStreams = {};

constructor(assetMgr, options) {
this.options = options;
if (options && options.adCopyMgrUri) {

@@ -34,3 +38,8 @@ this.adCopyMgrUri = options.adCopyMgrUri;

this.server.use(restify.plugins.queryParser());
this.sessionStore = {
sessionStateStore: new SessionStateStore({ redisUrl: options.redisUrl }),
playheadStateStore: new PlayheadStateStore({ redisUrl: options.redisUrl })
};
if (options && options.staticDirectory) {

@@ -46,15 +55,15 @@ this.server.get('/', restify.plugins.serveStatic({

}
this.server.get('/live/:file', (req, res, next) => {
this.server.get('/live/:file', async (req, res, next) => {
debug(req.params);
let m;
if (req.params.file.match(/master.m3u8/)) {
this._handleMasterManifest(req, res, next);
await this._handleMasterManifest(req, res, next);
} else if (m = req.params.file.match(/master(\d+).m3u8;session=(.*)$/)) {
req.params[0] = m[1];
req.params[1] = m[2];
this._handleMediaManifest(req, res, next);
await this._handleMediaManifest(req, res, next);
} else if (m = req.params.file.match(/master-(\S+).m3u8;session=(.*)$/)) {
req.params[0] = m[1];
req.params[1] = m[2];
this._handleAudioManifest(req, res, next);
await this._handleAudioManifest(req, res, next);
}

@@ -71,11 +80,10 @@ });

if (options && options.channelManager) {
this.updateChannels(options.channelManager, options);
setInterval(() => this.updateChannels(options.channelManager, options), 60 * 1000);
const t = setInterval(async () => { await this.updateChannelsAsync(options.channelManager, options) }, 60 * 1000);
}
}
updateChannels(channelMgr, options) {
async updateChannelsAsync(channelMgr, options) {
debug(`Do we have any new channels?`);
const newChannels = channelMgr.getChannels().filter(channel => !sessions[channel.id]);
newChannels.map(channel => {
const addAsync = async (channel) => {
debug(`Adding channel with ID ${channel.id}`);

@@ -89,26 +97,35 @@ sessions[channel.id] = new Session(this.assetMgr, {

slateRepetitions: this.slateRepetitions,
});
}, this.sessionStore);
await sessions[channel.id].initAsync();
if (!this.monitorTimer[channel.id]) {
this.monitorTimer[channel.id] = setInterval(() => this._monitor(sessions[channel.id]), 5000);
this.monitorTimer[channel.id] = setInterval(async () => { await this._monitorAsync(sessions[channel.id]) }, 5000);
}
});
await sessions[channel.id].startPlayheadAsync();
};
await Promise.all(newChannels.map(channel => addAsync(channel)));
debug(`Have any channels been removed?`);
const removedChannels = Object.keys(sessions).filter(channelId => !channelMgr.getChannels().find(ch => ch.id == channelId));
removedChannels.map(channelId => {
const removeAsync = async (channelId) => {
debug(`Removing channel with ID ${channelId}`);
clearInterval(this.monitorTimer[channelId]);
sessions[channelId].stopPlayhead();
await sessions[channelId].stopPlayheadAsync();
delete sessions[channelId];
});
};
await Promise.all(removedChannels.map(channelId => removeAsync(channelId)));
}
start() {
Object.keys(sessions).map(channelId => {
const startAsync = async (channelId) => {
const session = sessions[channelId];
if (!this.monitorTimer[channelId]) {
this.monitorTimer[channel.id] = setInterval(() => this._monitor(session), 5000);
this.monitorTimer[channelId] = setInterval(async () => { await this._monitorAsync(session) }, 5000);
}
});
await session.startPlayheadAsync();
};
(async () => {
debug("Starting engine");
await this.updateChannelsAsync(this.options.channelManager, this.options);
await Promise.all(Object.keys(sessions).map(channelId => startAsync(channelId)));
})();
}

@@ -122,4 +139,4 @@

getStatusForSession(sessionId) {
return sessions[sessionId].getStatus();
async getStatusForSessionAsync(sessionId) {
return await sessions[sessionId].getStatusAsync();
}

@@ -131,13 +148,12 @@

_monitor(session) {
session.getStatus().then(status => {
debug(`MONITOR (${new Date().toISOString()}) [${status.sessionId}]: playhead: ${status.playhead.state}`);
if (status.playhead.state === 'crashed') {
debug(`[${status.sessionId}]: Playhead crashed, restarting`);
session.restartPlayhead();
} else if (status.playhead.state === 'idle') {
debug(`[${status.sessionId}]: Starting playhead`);
session.startPlayhead();
}
});
async _monitorAsync(session) {
const status = await session.getStatusAsync();
debug(`MONITOR (${new Date().toISOString()}) [${status.sessionId}]: playhead: ${status.playhead.state}`);
if (status.playhead.state === 'crashed') {
debug(`[${status.sessionId}]: Playhead crashed, restarting`);
await session.restartPlayheadAsync();
} else if (status.playhead.state === 'idle') {
debug(`[${status.sessionId}]: Starting playhead`);
await session.startPlayheadAsync();
}
}

@@ -151,3 +167,3 @@

_handleMasterManifest(req, res, next) {
async _handleMasterManifest(req, res, next) {
debug('req.url=' + req.url);

@@ -173,3 +189,3 @@ debug(req.query);

options.useDemuxedAudio = this.useDemuxedAudio;
session = new Session(this.assetMgr, options);
session = new Session(this.assetMgr, options, this.sessionStore);
sessions[session.sessionId] = session;

@@ -185,3 +201,4 @@ }

session.getMasterManifest().then(body => {
try {
const body = await session.getMasterManifestAsync();
res.sendRaw(200, body, {

@@ -196,5 +213,5 @@ "Content-Type": "application/x-mpegURL",

next();
}).catch(err => {
} catch (err) {
next(this._errorHandler(err));
});
}
} else {

@@ -205,7 +222,8 @@ next(this._gracefulErrorHandler("Could not find a valid session"));

_handleAudioManifest(req, res, next) {
async _handleAudioManifest(req, res, next) {
debug(`req.url=${req.url}`);
const session = sessions[req.params[1]];
if (session) {
session.getCurrentAudioManifest(req.params[0], req.headers["x-playback-session-id"]).then(body => {
try {
const body = await session.getCurrentAudioManifestAsync(req.params[0], req.headers["x-playback-session-id"]);
//verbose(`[${session.sessionId}] body=`);

@@ -219,5 +237,5 @@ //verbose(body);

next();
}).catch(err => {
} catch (err) {
next(this._gracefulErrorHandler(err));
});
}
} else {

@@ -229,3 +247,3 @@ const err = new errs.NotFoundError('Invalid session');

_handleMediaManifest(req, res, next) {
async _handleMediaManifest(req, res, next) {
debug(`${req.headers["x-playback-session-id"]} req.url=${req.url}`);

@@ -235,3 +253,4 @@ debug(req.params);

if (session) {
session.getCurrentMediaManifest(req.params[0], req.headers["x-playback-session-id"]).then(body => {
try {
const body = await session.getCurrentMediaManifestAsync(req.params[0], req.headers["x-playback-session-id"]);
//verbose(`[${session.sessionId}] body=`);

@@ -245,5 +264,5 @@ //verbose(body);

next();
}).catch(err => {
} catch (err) {
next(this._gracefulErrorHandler(err));
})
}
} else {

@@ -281,10 +300,9 @@ const err = new errs.NotFoundError('Invalid session');

_handleStatus(req, res, next) {
async _handleStatus(req, res, next) {
debug(`req.url=${req.url}`);
const session = sessions[req.params.sessionId];
if (session) {
session.getStatus().then(body => {
res.send(200, body);
next();
});
const body = await session.getStatusAsync();
res.send(200, body);
next();
} else {

@@ -296,13 +314,12 @@ const err = new errs.NotFoundError('Invalid session');

_handleSessionHealth(req, res, next) {
async _handleSessionHealth(req, res, next) {
debug(`req.url=${req.url}`);
const session = sessions[req.params.sessionId];
if (session) {
session.getStatus().then(status => {
if (status.playhead && status.playhead.state === "running") {
res.send(200, { "health": "ok" });
} else {
res.send(503, { "health": "unhealthy" });
}
});
const status = await session.getStatusAsync();
if (status.playhead && status.playhead.state === "running") {
res.send(200, { "health": "ok" });
} else {
res.send(503, { "health": "unhealthy" });
}
} else {

@@ -309,0 +326,0 @@ const err = new errs.NotFoundError('Invalid session');

const crypto = require('crypto');
const debug = require('debug')('engine-session');
const HLSVod = require('@eyevinn/hls-vodtolive');
const AdRequest = require('./ad_request.js');
const m3u8 = require('@eyevinn/m3u8');

@@ -9,16 +8,5 @@ const HLSRepeatVod = require('@eyevinn/hls-repeat');

const SessionState = Object.freeze({
VOD_INIT: 1,
VOD_PLAYING: 2,
VOD_NEXT_INIT: 3,
VOD_NEXT_INITIATING: 4,
});
const { SessionState } = require('./session_state.js');
const { PlayheadState } = require('./playhead_state.js');
const PlayheadState = Object.freeze({
RUNNING: 1,
STOPPED: 2,
CRASHED: 3,
IDLE: 4
});
const AVERAGE_SEGMENT_DURATION = 3000;

@@ -36,24 +24,10 @@

*/
constructor(assetManager, config) {
constructor(assetManager, config, sessionStore) {
this._assetManager = assetManager;
this._sessionId = crypto.randomBytes(20).toString('hex');
this._state = {
mediaSeq: 0,
discSeq: 0,
vodMediaSeq: {
video: 0,
audio: 0, // assume only one audio group now
},
state: SessionState.VOD_INIT,
lastM3u8: {},
tsLastRequest: {
video: null,
master: null,
audio: null
},
playhead: {
state: PlayheadState.IDLE,
}
};
this.currentVod;
this._sessionStateStore = sessionStore.sessionStateStore;
this._playheadStateStore = sessionStore.playheadStateStore;
//this.currentVod;
this.currentMetadata = {};

@@ -67,6 +41,6 @@ this._events = [];

}
if (config.startWithId) {
this._state.state = SessionState.VOD_INIT_BY_ID;
this._state.assetId = config.startWithId;
}
this._sessionStateStore.create(this._sessionId);
this._playheadStateStore.create(this._sessionId);
if (config.category) {

@@ -81,2 +55,5 @@ this._category = config.category;

}
if (config.startWithId) {
this.startWithId = config.startWithId;
}
if (config.profile) {

@@ -90,5 +67,15 @@ this._sessionProfile = config.profile;

}
} else {
this._sessionStateStore.create(this._sessionId);
this._playheadStateStore.create(this._sessionId);
}
}
async initAsync() {
if (this.startWithId) {
await this._sessionStateStore.set(this._sessionId, "state", SessionState.VOD_INIT_BY_ID);
await this._sessionStateStore.set(this._sessionId, "assetId", this.startWithId);
}
}
get sessionId() {

@@ -98,252 +85,267 @@ return this._sessionId;

startPlayhead() {
const loop = () => {
return this.increment()
.then(manifest => {
if ([SessionState.VOD_NEXT_INIT, SessionState.VOD_NEXT_INITIATING].indexOf(this._state.state) !== -1) {
return loop();
} else if (this._state.playhead.state == PlayheadState.STOPPED) {
getCurrentVod(sessionState) {
if (sessionState.currentVod) {
let hlsVod = new HLSVod();
hlsVod.fromJSON(sessionState.currentVod);
return hlsVod;
}
}
async setCurrentVod(hlsVod) {
return await this._sessionStateStore.set(this._sessionId, "currentVod", hlsVod.toJSON());
}
async startPlayheadAsync() {
debug(`[${this._sessionId}]: Playhead consumer started`);
let playheadState = await this._playheadStateStore.get(this._sessionId);
playheadState = await this._playheadStateStore.set(this._sessionId, "state", PlayheadState.RUNNING);
while (playheadState.state !== PlayheadState.CRASHED) {
try {
const manifest = await this.incrementAsync();
const sessionState = await this._sessionStateStore.get(this._sessionId);
playheadState = await this._playheadStateStore.get(this._sessionId);
if ([SessionState.VOD_NEXT_INIT, SessionState.VOD_NEXT_INITIATING].indexOf(sessionState.state) !== -1) {
} else if (playheadState.state == PlayheadState.STOPPED) {
debug(`[${this._sessionId}]: Stopping playhead`);
return;
} else {
this._getFirstDuration(manifest)
.then(firstDuration => {
debug(`[${this._sessionId}]: Next tick in ${firstDuration} seconds`)
return timer((firstDuration * 1000) - 50).then(() => {
return loop();
});
}).catch(err => {
console.error(err);
debug(`[${this._sessionId}]: Playhead consumer crashed (1)`);
this._state.playhead.state = PlayheadState.CRASHED;
});
}
}).catch(err => {
console.error(err);
debug(`[${this._sessionId}]: Playhead consumer crashed (2)`);
this._state.playhead.state = PlayheadState.CRASHED;
});
const firstDuration = await this._getFirstDuration(manifest);
debug(`[${this._sessionId}]: Next tick in ${firstDuration} seconds`)
await timer((firstDuration * 1000) - 50);
}
} catch (err) {
debug(`[${this._sessionId}]: Playhead consumer crashed (1)`);
console.error(`[${this._sessionId}]: ${err.message}`);
debug(err);
playheadState = await this._playheadStateStore.set(this._sessionId, "state", PlayheadState.CRASHED);
}
}
loop().then(final => {
if (this._state.playhead.state !== PlayheadState.CRASHED) {
debug(`[${this._sessionId}]: Playhead consumer started`);
this._state.playhead.state = PlayheadState.RUNNING;
}
}).catch(err => {
console.error(err);
debug(`[${this._sessionId}]: Playhead consumer crashed (2)`);
this._state.playhead.state = PlayheadState.CRASHED;
});
}
restartPlayhead() {
this._state.state = SessionState.VOD_NEXT_INIT;
async restartPlayheadAsync() {
await this._sessionStateStore.set(this._sessionId, "state", SessionState.VOD_NEXT_INIT);
debug(`[${this._sessionId}]: Restarting playhead consumer`);
this.startPlayhead();
await this.startPlayheadAsync();
}
stopPlayhead() {
this._state.playhead.state = PlayheadState.STOPPED;
async stopPlayheadAsync() {
debug(`[${this._sessionId}]: Stopping playhead consumer`);
await this._playheadStateStore.set(this._sessionId, "state", PlayheadState.STOPPED);
}
getStatus() {
return new Promise((resolve, reject) => {
const playheadStateMap = {};
playheadStateMap[PlayheadState.IDLE] = 'idle';
playheadStateMap[PlayheadState.RUNNING] = 'running';
playheadStateMap[PlayheadState.CRASHED] = 'crashed';
const status = {
sessionId: this._sessionId,
playhead: {
state: playheadStateMap[this._state.playhead.state]
}
};
resolve(status);
});
}
async getStatusAsync() {
const playheadState = await this._playheadStateStore.get(this._sessionId);
const playheadStateMap = {};
playheadStateMap[PlayheadState.IDLE] = 'idle';
playheadStateMap[PlayheadState.RUNNING] = 'running';
playheadStateMap[PlayheadState.CRASHED] = 'crashed';
playheadStateMap[PlayheadState.STOPPED] = 'stopped';
getCurrentMediaManifest(bw, playbackSessionId) {
return new Promise((resolve, reject) => {
if (this.currentVod) {
const m3u8 = this.currentVod.getLiveMediaSequences(this._state.playhead.mediaSeq, bw, this._state.playhead.vodMediaSeq.video, this._state.discSeq);
debug(`[${playbackSessionId}]: [${this._state.playhead.mediaSeq + this._state.playhead.vodMediaSeq.video}] Current media manifest for ${bw} requested`);
resolve(m3u8);
} else {
resolve("Engine not ready");
const status = {
sessionId: this._sessionId,
playhead: {
state: playheadStateMap[playheadState.state]
}
});
};
return status;
}
getCurrentAudioManifest(audioGroupId, playbackSessionId) {
return new Promise((resolve, reject) => {
if (this.currentVod) {
const m3u8 = this.currentVod.getLiveMediaAudioSequences(this._state.playhead.mediaSeq, audioGroupId, this._state.playhead.vodMediaSeq.audio, this._state.discSeq);
debug(`[${playbackSessionId}]: [${this._state.playhead.mediaSeq + this._state.playhead.vodMediaSeq.audio}] Current audio manifest for ${bw} requested`);
resolve(m3u8);
} else {
resolve("Engine not ready");
}
});
async getCurrentMediaManifestAsync(bw, playbackSessionId) {
const sessionState = await this._sessionStateStore.get(this._sessionId);
const playheadState = await this._playheadStateStore.get(this._sessionId);
const currentVod = this.getCurrentVod(sessionState);
if (currentVod) {
const m3u8 = currentVod.getLiveMediaSequences(playheadState.mediaSeq, bw, playheadState.vodMediaSeqVideo, sessionState.discSeq);
debug(`[${playbackSessionId}]: [${playheadState.mediaSeq + playheadState.vodMediaSeqVideo}] Current media manifest for ${bw} requested`);
return m3u8;
} else {
return "Engine not ready";
}
}
increment() {
return new Promise((resolve, reject) => {
this._tick().then(() => {
if (this._state.state === SessionState.VOD_NEXT_INITIATING) {
this._state.state = SessionState.VOD_PLAYING;
} else {
this._state.vodMediaSeq.video += 1;
this._state.vodMediaSeq.audio += 1;
}
if (this._state.vodMediaSeq.video >= this.currentVod.getLiveMediaSequencesCount() - 1) {
this._state.vodMediaSeq.video = this._state.vodMediaSeq.audio = this.currentVod.getLiveMediaSequencesCount() - 1;
this._state.state = SessionState.VOD_NEXT_INIT;
}
this._state.playhead.mediaSeq = this._state.mediaSeq;
this._state.playhead.vodMediaSeq = this._state.vodMediaSeq;
debug(`[${this._sessionId}]: INCREMENT (mseq=${this._state.playhead.mediaSeq + this._state.playhead.vodMediaSeq.video}) vodMediaSeq=(${this._state.playhead.vodMediaSeq.video}_${this._state.playhead.vodMediaSeq.audio})`);
let m3u8 = this.currentVod.getLiveMediaSequences(this._state.playhead.mediaSeq, 180000, this._state.playhead.vodMediaSeq.video, this._state.discSeq);
resolve(m3u8);
});
})
async getCurrentAudioManifestAsync(audioGroupId, playbackSessionId) {
const sessionState = await this._sessionStateStore.get(this._sessionId);
const playheadState = await this._playheadStateStore.get(this._sessionId);
const currentVod = this.getCurrentVod(sessionState);
if (currentVod) {
const m3u8 = currentVod.getLiveMediaAudioSequences(playheadState.mediaSeq, audioGroupId, playheadState.vodMediaSeqAudio, sessionState.discSeq);
debug(`[${playbackSessionId}]: [${playheadState.mediaSeq + playheadState.vodMediaSeqAudio}] Current audio manifest for ${bw} requested`);
return m3u8;
} else {
return "Engine not ready";
}
}
getMediaManifest(bw, opts) {
return new Promise((resolve, reject) => {
this._tick().then(() => {
let timeSinceLastRequest = (this._state.tsLastRequest.video === null) ? 0 : Date.now() - this._state.tsLastRequest.video;
async incrementAsync() {
await this._tickAsync();
let sessionState = await this._sessionStateStore.get(this._sessionId);
let playheadState = await this._playheadStateStore.get(this._sessionId);
const currentVod = this.getCurrentVod(sessionState);
if (sessionState.state === SessionState.VOD_NEXT_INITIATING) {
sessionState = await this._sessionStateStore.set(this._sessionId, "state", SessionState.VOD_PLAYING);
} else {
sessionState = await this._sessionStateStore.set(this._sessionId, "vodMediaSeqVideo", sessionState.vodMediaSeqVideo + 1);
sessionState = await this._sessionStateStore.set(this._sessionId, "vodMediaSeqAudio", sessionState.vodMediaSeqAudio + 1);
}
if (sessionState.vodMediaSeqVideo >= currentVod.getLiveMediaSequencesCount() - 1) {
sessionState = await this._sessionStateStore.set(this._sessionId, "vodMediaSeqVideo", currentVod.getLiveMediaSequencesCount() - 1);
sessionState = await this._sessionStateStore.set(this._sessionId, "vodMediaSeqAudio", currentVod.getLiveMediaSequencesCount() - 1);
sessionState = await this._sessionStateStore.set(this._sessionId, "state", SessionState.VOD_NEXT_INIT);
}
playheadState = await this._playheadStateStore.set(this._sessionId, "mediaSeq", sessionState.mediaSeq);
playheadState = await this._playheadStateStore.set(this._sessionId, "vodMediaSeqVideo", sessionState.vodMediaSeqVideo);
playheadState = await this._playheadStateStore.set(this._sessionId, "vodMediaSeqAudio", sessionState.vodMediaSeqAudio);
debug(`[${this._sessionId}]: INCREMENT (mseq=${playheadState.mediaSeq + playheadState.vodMediaSeqVideo}) vodMediaSeq=(${playheadState.vodMediaSeqVideo}_${playheadState.vodMediaSeqAudio})`);
let m3u8 = currentVod.getLiveMediaSequences(playheadState.mediaSeq, 180000, playheadState.vodMediaSeqVideo, sessionState.discSeq);
return m3u8;
}
if (this._state.state === SessionState.VOD_NEXT_INITIATING) {
this._state.state = SessionState.VOD_PLAYING;
} else {
let sequencesToIncrement = Math.ceil(timeSinceLastRequest / this.averageSegmentDuration);
this._state.vodMediaSeq.video += sequencesToIncrement;
}
if (this._state.vodMediaSeq.video >= this.currentVod.getLiveMediaSequencesCount() - 1) {
this._state.vodMediaSeq.video = this.currentVod.getLiveMediaSequencesCount() - 1;
this._state.state = SessionState.VOD_NEXT_INIT;
}
async getMediaManifestAsync(bw, opts) {
await this._tickAsync();
const tsLastRequestVideo = await this._sessionStateStore.get(this._sessionId).tsLastRequestVideo;
let timeSinceLastRequest = (tsLastRequestVideo === null) ? 0 : Date.now() - tsLastRequestVideo;
debug(`[${this._sessionId}]: VIDEO ${timeSinceLastRequest} (${this.averageSegmentDuration}) bandwidth=${bw} vodMediaSeq=(${this._state.vodMediaSeq.video}_${this._state.vodMediaSeq.audio})`);
let m3u8;
try {
m3u8 = this.currentVod.getLiveMediaSequences(this._state.mediaSeq, bw, this._state.vodMediaSeq.video, this._state.discSeq);
} catch (exc) {
if (this._state.lastM3u8[bw]) {
m3u8 = this._state.lastM3u8[bw]
} else {
reject('Failed to generate media manifest');
}
}
this._state.lastM3u8[bw] = m3u8;
this._state.lastServedM3u8 = m3u8;
this._state.tsLastRequest.video = Date.now();
if (this._state.state === SessionState.VOD_NEXT_INIT) {
this._tick().then(() => {
timeSinceLastRequest = (this._state.tsLastRequest.video === null) ? 0 : Date.now() - this._state.tsLastRequest.video;
if (this._state.state === SessionState.VOD_NEXT_INITIATING) {
this._state.state = SessionState.VOD_PLAYING;
}
debug(`[${this._sessionId}]: VIDEO ${timeSinceLastRequest} (${this.averageSegmentDuration}) bandwidth=${bw} vodMediaSeq=(${this._state.vodMediaSeq.video}_${this._state.vodMediaSeq.audio})`);
try {
m3u8 = this.currentVod.getLiveMediaSequences(this._state.mediaSeq, bw, this._state.vodMediaSeq.video, this._state.discSeq);
} catch (exc) {
if (this._state.lastM3u8[bw]) {
m3u8 = this._state.lastM3u8[bw]
} else {
reject('Failed to generate media manifest');
}
}
this._state.lastM3u8[bw] = m3u8;
this._state.lastServedM3u8 = m3u8;
this._state.tsLastRequest.video = Date.now();
resolve(m3u8);
});
} else {
resolve(m3u8);
}
}).catch(reject);
});
}
let sessionState = await this._sessionStateStore.get(this._sessionId);
const currentVod = this.getCurrentVod(sessionState);
if (sessionState.state === SessionState.VOD_NEXT_INITIATING) {
sessionState = await this._sessionStateStore.set(this._sessionId, "state", SessionState.VOD_PLAYING);
} else {
let sequencesToIncrement = Math.ceil(timeSinceLastRequest / this.averageSegmentDuration);
sessionState = await this._sessionStateStore.set(this._sessionId, "vodMediaSeqVideo", sessionState.vodMediaSeqVideo + sequencesToIncrement);
}
if (sessionState.vodMediaSeqVideo >= currentVod.getLiveMediaSequencesCount() - 1) {
sessionState = await this._sessionStateStore.set(this._sessionId, "vodMediaSeqVideo", currentVod.getLiveMediaSequencesCount() - 1);
sessionState = await this._sessionStateStore.set(this._sessionId, "state", SessionState.VOD_NEXT_INIT);
}
getAudioManifest(audioGroupId, opts) {
return new Promise((resolve, reject) => {
let timeSinceLastRequest = (this._state.tsLastRequest.audio === null) ? 0 : Date.now() - this._state.tsLastRequest.audio;
if (this._state.state !== SessionState.VOD_NEXT_INITIATING) {
let sequencesToIncrement = Math.ceil(timeSinceLastRequest / this.averageSegmentDuration);
if (this._state.vodMediaSeq.audio < this._state.vodMediaSeq.video) {
this._state.vodMediaSeq.audio += sequencesToIncrement;
if (this._state.vodMediaSeq.audio >= this.currentVod.getLiveMediaSequencesCount() - 1) {
this._state.vodMediaSeq.audio = this.currentVod.getLiveMediaSequencesCount() - 1;
}
}
debug(`[${this._sessionId}]: VIDEO ${timeSinceLastRequest} (${this.averageSegmentDuration}) bandwidth=${bw} vodMediaSeq=(${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio})`);
let m3u8;
try {
m3u8 = currentVod.getLiveMediaSequences(sessionState.mediaSeq, bw, sessionState.vodMediaSeqVideo, sessionState.discSeq);
} catch (exc) {
if (sessionState.lastM3u8[bw]) {
m3u8 = sessionState.lastM3u8[bw]
} else {
throw new Error('Failed to generate media manifest');
}
}
let lastM3u8 = sessionState.lastM3u8;
lastM3u8[bw] = m3u8;
sessionState = await this._sessionStateStore.set(this._sessionId, "lastM3u8", lastM3u8);
sessionState = await this._sessionStateStore.set(this._sessionId, "lastServedM3u8", m3u8);
sessionState = await this._sessionStateStore.set(this._sessionId, "tsLastRequestVideo", Date.now());
debug(`[${this._sessionId}]: AUDIO ${timeSinceLastRequest} (${this.averageSegmentDuration}) audioGroupId=${audioGroupId} vodMediaSeq=(${this._state.vodMediaSeq.video}_${this._state.vodMediaSeq.audio})`);
let m3u8;
if (sessionState.state === SessionState.VOD_NEXT_INIT) {
await this._tickAsync();
const tsLastRequestVideo = await this._sessionStateStore.get(this._sessionId).tsLastRequestVideo;
let timeSinceLastRequest = (tsLastRequestVideo === null) ? 0 : Date.now() - tsLastRequestVideo;
let sessionState = await this._sessionStateStore.get(this._sessionId);
if (sessionState.state === SessionState.VOD_NEXT_INITIATING) {
sessionState = await this._sessionStateStore.set(this._sessionId, "state", SessionState.VOD_PLAYING);
}
debug(`[${this._sessionId}]: VIDEO ${timeSinceLastRequest} (${this.averageSegmentDuration}) bandwidth=${bw} vodMediaSeq=(${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio})`);
try {
m3u8 = this.currentVod.getLiveMediaAudioSequences(this._state.mediaSeq, audioGroupId, this._state.vodMediaSeq.audio, this._state.discSeq);
m3u8 = currentVod.getLiveMediaSequences(sessionState.mediaSeq, bw, sessionState.vodMediaSeqVideo, sessionState.discSeq);
} catch (exc) {
if (this._state.lastM3u8[audioGroupId]) {
m3u8 = this._state.lastM3u8[audioGroupId];
if (sessionState.lastM3u8[bw]) {
m3u8 = sessionState.lastM3u8[bw]
} else {
reject('Failed to generate audio manifest');
throw new Error('Failed to generate media manifest');
}
}
this._state.lastM3u8[audioGroupId] = m3u8;
this._state.tsLastRequest.audio = Date.now();
resolve(m3u8);
});
let lastM3u8 = sessionState.lastM3u8;
lastM3u8[bw] = m3u8;
sessionState = await this._sessionStateStore.set(this._sessionId, "lastM3u8", lastM3u8);
sessionState = await this._sessionStateStore.set(this._sessionId, "lastServedM3u8", m3u8);
sessionState = await this._sessionStateStore.set(this._sessionId, "tsLastRequestVideo", Date.now());
return m3u8;
} else {
return m3u8;
}
}
getMasterManifest() {
return new Promise((resolve, reject) => {
this._tick().then(() => {
let m3u8 = "#EXTM3U\n";
m3u8 += "#EXT-X-VERSION:4\n";
m3u8 += `#EXT-X-SESSION-DATA:DATA-ID="eyevinn.tv.session.id",VALUE="${this._sessionId}"\n`;
m3u8 += `#EXT-X-SESSION-DATA:DATA-ID="eyevinn.tv.eventstream",VALUE="/eventstream/${this._sessionId}"\n`;
let audioGroupIds = this.currentVod.getAudioGroups();
let defaultAudioGroupId;
if (this.use_demuxed_audio === true) {
if (audioGroupIds.length > 0) {
m3u8 += "# AUDIO groups\n";
for (let i = 0; i < audioGroupIds.length; i++) {
let audioGroupId = audioGroupIds[i];
m3u8 += `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="${audioGroupId}",NAME="audio",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2",URI="master-${audioGroupId}.m3u8;session=${this._sessionId}"\n`;
}
defaultAudioGroupId = audioGroupIds[0];
}
async getAudioManifestAsync(audioGroupId, opts) {
const tsLastRequestAudio = await this._sessionStateStore.get(this._sessionId).tsLastRequestAudio;
let timeSinceLastRequest = (tsLastRequestAudio === null) ? 0 : Date.now() - tsLastRequestAudio;
let sessionState = await this._sessionStateStore.get(this._sessionId);
const currentVod = this.getCurrentVod(sessionState);
if (sessionState.state !== SessionState.VOD_NEXT_INITIATING) {
let sequencesToIncrement = Math.ceil(timeSinceLastRequest / this.averageSegmentDuration);
if (sessionState.vodMediaSeqAudio < sessionState.vodMediaSeqVideo) {
sessionState = await this._sessionStateStore.set(this._sessionId, "vodMediaSeqAudio", sessionState,vodMediaSeqAudio + sequencesToIncrement);
if (sessionState.vodMediaSeqAudio >= currentVod.getLiveMediaSequencesCount() - 1) {
sessionState = await this._sessionStateStore.set(this._sessionId, "vodMediaSeqAudio", currentVod.getLiveMediaSequencesCount() - 1);
}
if (this._sessionProfile) {
this._sessionProfile.forEach(profile => {
m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] + ',CODECS="' + profile.codecs + '"' + (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') + '\n';
m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
});
} else {
this.currentVod.getUsageProfiles().forEach(profile => {
m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + ',RESOLUTION=' + profile.resolution + ',CODECS="' + profile.codecs + '"' + (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') + '\n';
m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
});
}
}
debug(`[${this._sessionId}]: AUDIO ${timeSinceLastRequest} (${this.averageSegmentDuration}) audioGroupId=${audioGroupId} vodMediaSeq=(${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio})`);
let m3u8;
try {
m3u8 = currentVod.getLiveMediaAudioSequences(sessionState.mediaSeq, audioGroupId, sessionState.vodMediaSeqAudio, sessionState.discSeq);
} catch (exc) {
if (sessionState.lastM3u8[audioGroupId]) {
m3u8 = sessionState.lastM3u8[audioGroupId];
} else {
throw new Error('Failed to generate audio manifest');
}
}
let lastM3u8 = sessionState.lastM3u8;
lastM3u8[audioGroupId] = m3u8;
sessionState = await this._sessionStateStore.set(this._sessionId, "lastM3u8", lastM3u8);
sessionState = await this._sessionStateStore.set(this._sessionId, "tsLastRequestAudio", Date.now());
return m3u8;
}
async getMasterManifestAsync() {
await this._tickAsync();
let m3u8 = "#EXTM3U\n";
m3u8 += "#EXT-X-VERSION:4\n";
m3u8 += `#EXT-X-SESSION-DATA:DATA-ID="eyevinn.tv.session.id",VALUE="${this._sessionId}"\n`;
m3u8 += `#EXT-X-SESSION-DATA:DATA-ID="eyevinn.tv.eventstream",VALUE="/eventstream/${this._sessionId}"\n`;
const sessionState = await this._sessionStateStore.get(this._sessionId);
const currentVod = this.getCurrentVod(sessionState);
let audioGroupIds = currentVod.getAudioGroups();
let defaultAudioGroupId;
if (this.use_demuxed_audio === true) {
if (audioGroupIds.length > 0) {
m3u8 += "# AUDIO groups\n";
for (let i = 0; i < audioGroupIds.length; i++) {
let audioGroupId = audioGroupIds[i];
m3u8 += `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="${audioGroupId}",NAME="audio",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2",URI="master-${audioGroupId}.m3u8;session=${this._sessionId}"\n`;
}
if (this.use_demuxed_audio === true) {
for (let i = 0; i < audioGroupIds.length; i++) {
let audioGroupId = audioGroupIds[i];
m3u8 += `#EXT-X-STREAM-INF:BANDWIDTH=97000,CODECS="mp4a.40.2",AUDIO="${audioGroupId}"\n`;
m3u8 += `master-${audioGroupId}.m3u8;session=${this._sessionId}\n`;
}
}
this.produceEvent({
type: 'NOW_PLAYING',
data: {
id: this.currentMetadata.id,
title: this.currentMetadata.title,
}
});
this._state.tsLastRequest.master = Date.now();
resolve(m3u8);
}).catch(reject);
defaultAudioGroupId = audioGroupIds[0];
}
}
if (this._sessionProfile) {
this._sessionProfile.forEach(profile => {
m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] + ',CODECS="' + profile.codecs + '"' + (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') + '\n';
m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
});
} else {
currentVod.getUsageProfiles().forEach(profile => {
m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + ',RESOLUTION=' + profile.resolution + ',CODECS="' + profile.codecs + '"' + (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') + '\n';
m3u8 += "master" + profile.bw + ".m3u8;session=" + this._sessionId + "\n";
});
}
if (this.use_demuxed_audio === true) {
for (let i = 0; i < audioGroupIds.length; i++) {
let audioGroupId = audioGroupIds[i];
m3u8 += `#EXT-X-STREAM-INF:BANDWIDTH=97000,CODECS="mp4a.40.2",AUDIO="${audioGroupId}"\n`;
m3u8 += `master-${audioGroupId}.m3u8;session=${this._sessionId}\n`;
}
}
this.produceEvent({
type: 'NOW_PLAYING',
data: {
id: this.currentMetadata.id,
title: this.currentMetadata.title,
}
});
this._sessionStateStore.set(this._sessionId, "tsLastRequestMaster", Date.now());
return m3u8;
}

@@ -359,175 +361,146 @@

_tick() {
return new Promise((resolve, reject) => {
// State machine
let newVod;
let splices = null;
async _insertSlate(currentVod) {
if(this.slateUri) {
console.error(`[${this._sessionId}]: Will insert slate`);
const slateVod = await this._loadSlate(currentVod);
debug(`[${this._sessionId}]: slate loaded`);
await this._sessionStateStore.set(this._sessionId, "vodMediaSeqVideo", 0);
await this._sessionStateStore.set(this._sessionId, "vodMediaSeqAudio", 0);
await this._sessionStateStore.set(this._sessionId, "state", SessionState.VOD_PLAYING);
await this.setCurrentVod(slateVod);
return slateVod;
} else {
return null;
}
}
switch(this._state.state) {
case SessionState.VOD_INIT:
case SessionState.VOD_INIT_BY_ID:
async _tickAsync() {
let newVod;
let sessionState = await this._sessionStateStore.get(this._sessionId);
let currentVod = this.getCurrentVod(sessionState);
switch(sessionState.state) {
case SessionState.VOD_INIT:
case SessionState.VOD_INIT_BY_ID:
try {
let nextVodPromise;
if (this._state.state === SessionState.VOD_INIT) {
if (sessionState.state === SessionState.VOD_INIT) {
debug(`[${this._sessionId}]: state=VOD_INIT`);
nextVodPromise = this._getNextVod();
} else if (this._state.state === SessionState.VOD_INIT_BY_ID) {
debug(`[${this._sessionId}]: state=VOD_INIT_BY_ID ${this._state.assetId}`);
nextVodPromise = this._getNextVodById(this._state.assetId);
} else if (sessionState.state === SessionState.VOD_INIT_BY_ID) {
debug(`[${this._sessionId}]: state=VOD_INIT_BY_ID ${sessionState.assetId}`);
nextVodPromise = this._getNextVodById(sessionState.assetId);
}
nextVodPromise.then(vodResponse => {
if (!vodResponse.type) {
debug(`[${this._sessionId}]: got first VOD uri=${vodResponse.uri}:${vodResponse.offset || 0}`);
//newVod = new HLSVod(uri, [], Date.now());
newVod = new HLSVod(vodResponse.uri, [], null, vodResponse.offset * 1000);
this.currentVod = newVod;
return this.currentVod.load();
} else {
if (vodResponse.type === 'gap') {
return new Promise((resolve, reject) => {
this._fillGap(null, vodResponse.desiredDuration)
.then(gapVod => {
this.currentVod = gapVod;
resolve(gapVod);
}).catch(reject);
});
}
const vodResponse = await nextVodPromise;
let loadPromise;
if (!vodResponse.type) {
debug(`[${this._sessionId}]: got first VOD uri=${vodResponse.uri}:${vodResponse.offset || 0}`);
newVod = new HLSVod(vodResponse.uri, [], null, vodResponse.offset * 1000);
currentVod = newVod;
loadPromise = currentVod.load();
} else {
if (vodResponse.type === 'gap') {
loadPromise = new Promise((resolve, reject) => {
this._fillGap(null, vodResponse.desiredDuration)
.then(gapVod => {
currentVod = gapVod;
resolve(gapVod);
}).catch(reject);
});
}
}).then(() => {
debug(`[${this._sessionId}]: first VOD loaded`);
//debug(newVod);
this._state.vodMediaSeq.video = 0;
this._state.vodMediaSeq.audio = 0;
this.produceEvent({
type: 'NOW_PLAYING',
data: {
id: this.currentMetadata.id,
title: this.currentMetadata.title,
}
});
this._state.state = SessionState.VOD_PLAYING;
resolve();
}).catch(e => {
console.error("Failed to init first VOD");
if(this.slateUri) {
console.error("Will insert slate");
this._loadSlate()
.then(slateVod => {
this.currentVod = slateVod;
debug(`[${this._sessionId}]: slate loaded`);
this._state.vodMediaSeq.video = 0;
this._state.vodMediaSeq.audio = 0;
this._state.state = SessionState.VOD_PLAYING;
resolve();
})
.catch(reject);
} else {
debug('No slate to load');
}
await loadPromise;
debug(`[${this._sessionId}]: first VOD loaded`);
//debug(newVod);
sessionState = await this._sessionStateStore.set(this._sessionId, "vodMediaSeqVideo", 0);
sessionState = await this._sessionStateStore.set(this._sessionId, "vodMediaSeqAudio", 0);
this.produceEvent({
type: 'NOW_PLAYING',
data: {
id: this.currentMetadata.id,
title: this.currentMetadata.title,
}
});
break;
case SessionState.VOD_PLAYING:
debug(`[${this._sessionId}]: state=VOD_PLAYING (${this._state.vodMediaSeq.video}_${this._state.vodMediaSeq.audio}, ${this.currentVod.getLiveMediaSequencesCount()})`);
/*
if (this._state.vodMediaSeq.video >= this.currentVod.getLiveMediaSequencesCount() - 1) {
this._state.state = SessionState.VOD_NEXT_INIT;
sessionState = await this._sessionStateStore.set(this._sessionId, "state", SessionState.VOD_PLAYING);
sessionState = await this.setCurrentVod(currentVod);
return;
} catch (err) {
console.error(`[${this._sessionId}]: Failed to init first VOD`);
debug(err);
currentVod = await this._insertSlate(currentVod);
if (!currentVod) {
debug("No slate to load");
throw err;
}
*/
resolve();
break;
case SessionState.VOD_NEXT_INITIATING:
debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING`);
resolve();
break;
case SessionState.VOD_NEXT_INIT:
}
case SessionState.VOD_PLAYING:
debug(`[${this._sessionId}]: state=VOD_PLAYING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}, ${currentVod.getLiveMediaSequencesCount()})`);
return;
case SessionState.VOD_NEXT_INITIATING:
debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING`);
return;
case SessionState.VOD_NEXT_INIT:
try {
debug(`[${this._sessionId}]: state=VOD_NEXT_INIT`);
const length = this.currentVod.getLiveMediaSequencesCount();
const lastDiscontinuity = this.currentVod.getLastDiscontinuity();
this._state.state = SessionState.VOD_NEXT_INITIATING;
let vodPromise;
if (this._adCopyMgrUri) {
const adRequest = new AdRequest(this._adCopyMgrUri, this._adXchangeUri);
vodPromise = new Promise((resolve, reject) => {
adRequest.resolve().then(_splices => {
debug(`[${this._sessionId}]: got splices=${_splices.length}`);
if (_splices.length > 0) {
splices = _splices;
debug(splices);
}
return this._getNextVod();
}).then(resolve);
});
} else {
vodPromise = this._getNextVod();
if (!currentVod) {
throw new Error("No VOD to init");
}
vodPromise.then(vodResponse => {
if (!vodResponse.type) {
debug(`[${this._sessionId}]: got next VOD uri=${vodResponse.uri}:${vodResponse.offset}`);
newVod = new HLSVod(vodResponse.uri, splices, null, vodResponse.offset * 1000);
this.produceEvent({
type: 'NEXT_VOD_SELECTED',
data: {
id: this.currentMetadata.id,
uri: vodResponse.uri,
title: this.currentMetadata.title || '',
}
});
return newVod.loadAfter(this.currentVod);
} else {
if (vodResponse.type === 'gap') {
return new Promise((resolve, reject) => {
this._fillGap(this.currentVod, vodResponse.desiredDuration)
.then(gapVod => {
newVod = gapVod;
resolve(newVod);
}).catch(reject);
})
}
}
})
.then(() => {
debug(`[${this._sessionId}]: next VOD loaded`);
//debug(newVod);
this.currentVod = newVod;
debug(`[${this._sessionId}]: msequences=${this.currentVod.getLiveMediaSequencesCount()}`);
this._state.vodMediaSeq.video = 0;
this._state.vodMediaSeq.audio = 0;
this._state.mediaSeq += length;
this._state.discSeq += lastDiscontinuity;
const length = currentVod.getLiveMediaSequencesCount();
const lastDiscontinuity = currentVod.getLastDiscontinuity();
sessionState = await this._sessionStateStore.set(this._sessionId, "state", SessionState.VOD_NEXT_INITIATING);
let vodPromise = this._getNextVod();
const vodResponse = await vodPromise;
let loadPromise;
if (!vodResponse.type) {
debug(`[${this._sessionId}]: got next VOD uri=${vodResponse.uri}:${vodResponse.offset}`);
newVod = new HLSVod(vodResponse.uri, null, null, vodResponse.offset * 1000);
this.produceEvent({
type: 'NOW_PLAYING',
type: 'NEXT_VOD_SELECTED',
data: {
id: this.currentMetadata.id,
title: this.currentMetadata.title,
uri: vodResponse.uri,
title: this.currentMetadata.title || '',
}
});
resolve();
})
.catch(err => {
console.error("Failed to init next VOD");
debug(err);
if(this.slateUri) {
console.error("Will insert slate");
this._loadSlate(this.currentVod)
.then(slateVod => {
this.currentVod = slateVod;
debug(`[${this._sessionId}]: slate loaded`);
this._state.vodMediaSeq.video = 0;
this._state.vodMediaSeq.audio = 0;
this._state.mediaSeq += length;
this._state.discSeq += lastDiscontinuity;
this._state.state = SessionState.VOD_NEXT_INITIATING;
resolve();
})
.catch(reject);
} else {
debug('No slate to load');
reject(err);
});
loadPromise = newVod.loadAfter(currentVod);
} else {
loadPromise = new Promise((resolve, reject) => {
this._fillGap(currentVod, vodResponse.desiredDuration)
.then(gapVod => {
newVod = gapVod;
resolve(newVod);
}).catch(reject);
});
}
await loadPromise;
debug(`[${this._sessionId}]: next VOD loaded`);
currentVod = newVod;
debug(`[${this._sessionId}]: msequences=${currentVod.getLiveMediaSequencesCount()}`);
sessionState = await this._sessionStateStore.set(this._sessionId, "vodMediaSeqVideo", 0);
sessionState = await this._sessionStateStore.set(this._sessionId, "vodMediaSeqAudio", 0);
sessionState = await this._sessionStateStore.set(this._sessionId, "mediaSeq", sessionState.mediaSeq + length);
sessionState = await this._sessionStateStore.set(this._sessionId, "discSeq", sessionState.discSeq + lastDiscontinuity);
sessionState = await this.setCurrentVod(currentVod);
this.produceEvent({
type: 'NOW_PLAYING',
data: {
id: this.currentMetadata.id,
title: this.currentMetadata.title,
}
})
break;
default:
reject("Invalid state: " + this.state.state);
}
});
});
return;
} catch(err) {
console.error(`[${this._sessionId}]: Failed to init next VOD`);
debug(err);
currentVod = await this._insertSlate(currentVod);
if (!currentVod) {
debug("No slate to load");
throw err;
}
}
break;
default:
throw new Error("Invalid state: " + sessionState.state);
}
}

@@ -623,12 +596,2 @@

_getNearestBandwidth(bandwidth) {
const availableBandwidths = this.currentVod.getBandwidths().sort((a,b) => b - a);
for (let i = 0; i < availableBandwidths.length; i++) {
if (bandwidth >= availableBandwidths[i]) {
return availableBandwidths[i];
}
}
return availableBandwidths[availableBandwidths.length - 1];
}
_getFirstDuration(manifest) {

@@ -635,0 +598,0 @@ return new Promise((resolve, reject) => {

{
"name": "eyevinn-channel-engine",
"version": "2.5.0",
"version": "2.6.0",
"description": "OTT TV Channel Engine",

@@ -16,8 +16,9 @@ "main": "index.js",

"dependencies": {
"@eyevinn/hls-repeat": "^0.1.1",
"@eyevinn/hls-vodtolive": ">=1.2.0",
"@eyevinn/m3u8": "^0.1.1",
"debug": "^3.1.0",
"redis": "^3.0.2",
"request": ">=2.88.0",
"restify": ">=8.0.0",
"@eyevinn/hls-vodtolive": ">=1.1.1",
"@eyevinn/hls-repeat": "^0.1.1"
"restify": ">=8.0.0"
},

@@ -24,0 +25,0 @@ "devDependencies": {

@@ -24,3 +24,3 @@ The Eyevinn Channel Engine is an NPM library that provides the functionality to generate "fake" live HLS stream by stitching HLS VOD's together. The library is provided as open source and this repository includes a basic reference implementation as a guide on how the library can be used.

```
const ChannelEngine = require('eyevinn-channel-engine);
const ChannelEngine = require('eyevinn-channel-engine');

@@ -46,2 +46,12 @@ const engine = new ChannelEngine(myAssetManager, { channelManager: myChannelManager });

### Options
Available options when constructing the Channel Engine object are:
- `defaultSlateUri`: URI to an HLS VOD that can be inserted when a VOD for some reason cannot be loaded.
- `slateRepetitions`: Number of times the slate should be repeated.
- `redisUrl`: A Redis DB URL for storing states that can be shared between nodes.
- `heartbeat`: Path for heartbeat requests
- `channelManager`: A reference to a channel manager object.
## Commercial Alternative

@@ -48,0 +58,0 @@

@@ -48,3 +48,4 @@ /*

return [ { id: '1', profile: this._getProfile() }, { id: 'faulty', profile: this._getProfile() } ];
}
// return [ { id: '1', profile: this._getProfile() } ];
}

@@ -69,2 +70,3 @@ _getProfile() {

slateRepetitions: 10,
redisUrl: process.env.REDIS_URL,
};

@@ -71,0 +73,0 @@

@@ -59,3 +59,3 @@ const ChannelEngine = require('../../index.js');

it("is updated when new channels are added", done => {
it("is updated when new channels are added", async () => {
const testAssetManager = new TestAssetManager();

@@ -72,13 +72,10 @@ const testChannelManager = new TestChannelManager();

engine.getStatusForSession("1")
.then(status => {
expect(status.playhead.state).toEqual("idle");
testChannelManager._increment();
jasmine.clock().tick((60 * 1000) + 1);
expect(engine.getSessionCount()).toEqual(2);
done();
});
const status = await engine.getStatusForSessionAsync("1")
expect(status.playhead.state).toEqual("idle");
testChannelManager._increment();
jasmine.clock().tick((60 * 1000) + 1);
expect(engine.getSessionCount()).toEqual(2);
});
it("is updated when channels are removed", done => {
xit("is updated when channels are removed", async () => {
const testAssetManager = new TestAssetManager();

@@ -89,20 +86,22 @@ const testChannelManager = new TestChannelManager();

engine.start();
testChannelManager._increment();
jasmine.clock().tick((60 * 1000) + 1);
jasmine.clock().tick(5001);
jasmine.clock().tick(5001);
console.log(engine.getSessionCount());
expect(engine.getSessionCount()).toEqual(1);
engine.getStatusForSession("1")
.then(status => {
expect(status.playhead.state).toEqual("idle");
testChannelManager._increment();
jasmine.clock().tick((60 * 1000) + 1);
expect(engine.getSessionCount()).toEqual(2);
testChannelManager._increment();
jasmine.clock().tick((60 * 1000) + 1);
expect(engine.getSessionCount()).toEqual(1);
done();
});
const status = await engine.getStatusForSessionAsync("1");
expect(status.playhead.state).toEqual("idle");
testChannelManager._increment();
jasmine.clock().tick((60 * 1000) + 1);
console.log(engine.getSessionCount());
expect(engine.getSessionCount()).toEqual(2);
testChannelManager._increment();
jasmine.clock().tick((2 * 60 * 1000) + 1);
console.log(engine.getSessionCount());
expect(engine.getSessionCount()).toEqual(1);
});
});

@@ -5,2 +5,5 @@ const Session = require('../../engine/session.js');

const { SessionStateStore } = require('../../engine/session_state.js');
const { PlayheadStateStore } = require('../../engine/playhead_state.js');
class TestAssetManager {

@@ -44,3 +47,3 @@ constructor(opts, assets) {

while (remain > 0) {
promiseFns.push(() => session.increment());
promiseFns.push(() => session.incrementAsync());
remain--;

@@ -107,5 +110,13 @@ }

describe("Playhead consumer", () => {
let sessionStore = undefined;
beforeEach(() => {
sessionStore = {
sessionStateStore: new SessionStateStore(),
playheadStateStore: new PlayheadStateStore()
};
});
it("continously increases media sequence over two VOD switches", async (done) => {
const assetMgr = new TestAssetManager();
const session = new Session(assetMgr, { sessionId: '1' });
const session = new Session(assetMgr, { sessionId: '1' }, sessionStore);
const loop = async (increments) => {

@@ -115,3 +126,3 @@ let remain = increments;

while (remain > 0) {
promiseFns.push(() => session.increment());
promiseFns.push(() => session.incrementAsync());
remain--;

@@ -125,3 +136,3 @@ }

}
currentMediaManifest = await session.getCurrentMediaManifest(180000);
currentMediaManifest = await session.getCurrentMediaManifestAsync(180000);
expect(currentMediaManifest).toEqual(manifest);

@@ -136,3 +147,3 @@ }

const assetMgr = new TestAssetManager();
const session = new Session(assetMgr, { sessionId: '1' });
const session = new Session(assetMgr, { sessionId: '1' }, sessionStore);
await verificationLoop(session, 10);

@@ -144,3 +155,3 @@ done();

const assetMgr = new TestAssetManager(null, [{ id: 1, title: "Short", uri: "https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8" }]);
const session = new Session(assetMgr, { sessionId: '1' });
const session = new Session(assetMgr, { sessionId: '1' }, sessionStore);
await verificationLoop(session, 10);

@@ -157,4 +168,4 @@ done();

];
const session = new Session(assetMgr, { sessionId: '1', profile: channelProfile });
const masterManifest = await session.getMasterManifest();
const session = new Session(assetMgr, { sessionId: '1', profile: channelProfile }, sessionStore);
const masterManifest = await session.getMasterManifestAsync();
const profile = await parseMasterManifest(masterManifest);

@@ -168,20 +179,13 @@ expect(profile[0].bw).toEqual(6134000);

while (remain > 0) {
const verificationFn = () => {
return new Promise((resolve, reject) => {
const bwMap = {
'6134000': '2000',
'2323000': '1000',
'1313000': '600'
};
session.increment()
.then(manifest => {
profile.map(p => {
session.getCurrentMediaManifest(p.bw)
.then(mediaManifest => {
expect(mediaManifest.match(`${bwMap[p.bw]}/${bwMap[p.bw]}-.*\.ts$`));
});
});
resolve();
});
});
const verificationFn = async () => {
const bwMap = {
'6134000': '2000',
'2323000': '1000',
'1313000': '600'
};
const manifest = await session.incrementAsync();
await Promise.all(profile.map(async (p) => {
const mediaManifest = await session.getCurrentMediaManifestAsync(p.bw);
expect(mediaManifest.match(`${bwMap[p.bw]}/${bwMap[p.bw]}-.*\.ts$`));
}));
};

@@ -202,4 +206,4 @@ verificationFns.push(verificationFn);

const assetMgr = new TestAssetManager();
const session = new Session(assetMgr, { sessionId: '1' });
const masterManifest = await session.getMasterManifest();
const session = new Session(assetMgr, { sessionId: '1' }, sessionStore);
const masterManifest = await session.getMasterManifestAsync();
const profile = await parseMasterManifest(masterManifest);

@@ -213,20 +217,13 @@ expect(profile[0].bw).toEqual(6134000);

while (remain > 0) {
const verificationFn = () => {
return new Promise((resolve, reject) => {
const bwMap = {
'6134000': '2000',
'2323000': '1000',
'1313000': '600'
};
session.increment()
.then(manifest => {
profile.map(p => {
session.getCurrentMediaManifest(p.bw)
.then(mediaManifest => {
expect(mediaManifest.match(`${bwMap[p.bw]}/${bwMap[p.bw]}-.*\.ts$`));
});
});
resolve();
});
});
const verificationFn = async () => {
const bwMap = {
'6134000': '2000',
'2323000': '1000',
'1313000': '600'
};
const manifest = await session.incrementAsync();
await Promise.all(profile.map(async (p) => {
const mediaManifest = await session.getCurrentMediaManifestAsync(p.bw);
expect(mediaManifest.match(`${bwMap[p.bw]}/${bwMap[p.bw]}-.*\.ts$`));
}));
};

@@ -247,3 +244,3 @@ verificationFns.push(verificationFn);

const assetMgr = new TestAssetManager();
const session = new Session(assetMgr, { sessionId: '1' });
const session = new Session(assetMgr, { sessionId: '1' }, sessionStore);
const expectedLastSegment = "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/2000/2000-00091.ts";

@@ -256,17 +253,10 @@ let found = false;

while(remain > 0) {
const verificationFn = () => {
return new Promise((resolve, reject) => {
session.increment()
.then(async (manifest) => {
return session.getCurrentMediaManifest(6134000);
})
.then(async (manifest) => {
const m3u = await parseMediaManifest(manifest);
const playlistItems = m3u.items.PlaylistItem;
if (playlistItems[playlistItems.length - 1].get('uri') === expectedLastSegment) {
found = true;
}
resolve();
});
});
const verificationFn = async () => {
const manifest = await session.incrementAsync();
const mediaManifest = await session.getCurrentMediaManifestAsync(6134000);
const m3u = await parseMediaManifest(mediaManifest);
const playlistItems = m3u.items.PlaylistItem;
if (playlistItems[playlistItems.length - 1].get('uri') === expectedLastSegment) {
found = true;
}
};

@@ -288,3 +278,3 @@ verificationFns.push(verificationFn);

const assetMgr = new TestAssetManager({ fail: true });
const session = new Session(assetMgr, { sessionId: '1', slateUri: 'http://testcontent.eyevinn.technology/slates/ottera/playlist.m3u8' });
const session = new Session(assetMgr, { sessionId: '1', slateUri: 'http://testcontent.eyevinn.technology/slates/ottera/playlist.m3u8' }, sessionStore);
let slateManifest;

@@ -295,13 +285,6 @@ const loop = async (increments) => {

while (remain > 0) {
const verificationFn = () => {
return new Promise((resolve, reject) => {
session.increment()
.then(async (manifest) => {
return session.getCurrentMediaManifest(6134000);
})
.then(manifest => {
slateManifest = manifest;
resolve();
});
});
const verificationFn = async () => {
await session.incrementAsync();
const manifest = await session.getCurrentMediaManifestAsync(6134000);
slateManifest = manifest;
};

@@ -324,3 +307,3 @@ verificationFns.push(verificationFn);

const assetMgr = new TestAssetManager({failOnIndex: 1});
const session = new Session(assetMgr, { sessionId: '1', slateUri: 'http://testcontent.eyevinn.technology/slates/ottera/playlist.m3u8' });
const session = new Session(assetMgr, { sessionId: '1', slateUri: 'http://testcontent.eyevinn.technology/slates/ottera/playlist.m3u8' }, sessionStore);
let slateManifest;

@@ -331,14 +314,6 @@ const loop = async (increments) => {

while (remain > 0) {
const verificationFn = () => {
return new Promise((resolve, reject) => {
session.increment()
.then(async (manifest) => {
return session.getCurrentMediaManifest(6134000);
})
.then(manifest => {
slateManifest = manifest;
resolve();
})
.catch(reject);
});
const verificationFn = async () => {
await session.incrementAsync();
const manifest = await session.getCurrentMediaManifestAsync(6134000);
slateManifest = manifest;
};

@@ -345,0 +320,0 @@ verificationFns.push(verificationFn);

const Session = require('../../engine/session.js');
const { SessionStateStore } = require('../../engine/session_state.js');
const { PlayheadStateStore } = require('../../engine/playhead_state.js');
describe("Session", () => {
let sessionStore = undefined;
beforeEach(() => {
sessionStore = {
sessionStateStore: new SessionStateStore(),
playheadStateStore: new PlayheadStateStore()
};
});
it("creates a unique session ID", () => {
const id1 = new Session('dummy').sessionId;
const id2 = new Session('dummy').sessionId;
const id1 = new Session('dummy', null, sessionStore).sessionId;
const id2 = new Session('dummy', null, sessionStore).sessionId;
expect(id1).not.toEqual(id2);
});
});