/**
* MAGSDK basic implementation of pvr plugin.
* Before use stb player should be initialised by calling gSTB.InitPlayer().
*
* @author Fedotov Dmitry <bas.jsdev@gmail.com>
*/
'use strict';
var daemon;
/**
* Base Events Emitter implementation.
*/
function Emitter () { this.events = {}; }
Emitter.prototype = {
addListener: function ( name, callback ) {
this.events[name] = this.events[name] || [];
this.events[name].push(callback);
},
once: function ( name, callback ) {
var self = this;
this.events[name] = this.events[name] || [];
this.events[name].push(function onceWrapper () {
callback.apply(self, arguments);
self.removeListener(name, onceWrapper);
});
},
addListeners: function ( callbacks ) {
var name;
if ( typeof callbacks === 'object' ) {
for ( name in callbacks ) {
if ( callbacks.hasOwnProperty(name) ) {
this.addListener(name, callbacks[name]);
}
}
}
},
removeListener: function ( name, callback ) {
if ( this.events[name] ) {
this.events[name] = this.events[name].filter(function callbacksFilter ( fn ) {
return fn !== callback;
});
if ( this.events[name].length === 0 ) {
this.events[name] = undefined;
}
}
},
removeAllListeners: function ( name ) {
if ( arguments.length === 0 ) {
this.events = {};
} else if ( name ) {
this.events[name] = undefined;
}
},
emit: function ( name ) {
var event = this.events[name],
ind;
if ( event ) {
for ( ind = 0; ind < event.length; ind++ ) {
event[ind].apply(this, Array.prototype.slice.call(arguments, 1));
}
}
}
};
// correct constructor name
Emitter.prototype.constructor = Emitter;
/**
* Callback for called method.
*
* @callback callback
*
* @param {Object} [config] result data
* @param {Object} [config.curr] new property value (for example if event 'state' was emitted here will be new state value)
* @param {Object} [config.old] old property value
* @param {Object} [config.time] for cache reset
* @param {Record} config.item record object (with applied changes)
* @param {Object} [error] error data
* @param {Object} error.code error code
* @param {Object} error.message error text message
*/
/**
* Watching for any changes in records on gSTB level and if there are some - triggering corresponding callbacks in clients.
* Can't be reached from application scope.
*
* @namespace
*/
daemon = {
/**
* Id from update setInterval function
*
* @type {number}
*/
checkTimerId: 0,
/**
* Interval between updates (between pvrManager.GetAllTasks() function calls)
*
* @type {number}
*/
checkTime: 2000,
/**
* List of clients listeners. For example to make first client update progress for recordItem and emit event
* 'progress' for his app, you should call daemon.triggers[0].onProgress(recordItem).
*
* @type {Array}
*/
triggers: [],
/**
* Stack for asynchronous callbacks. For example delay between STB record creation call and record appearance in
* STB records list can be more than few seconds. So callback will wait in stack till corresponding new record
* would be fond by sync operation.
*
* @type {Object}
*/
lostEvents: {
remove: {},
add: {}
},
/**
* Raw records data from pvrManager.GetAllTasks() call
*
* @type {Array}
*/
rawDataList: [],
/**
* Hash to connect raw records data and records objects in clients. Otherwise on each update operation we should
* use multiple cycles to build connections between raw data and record objects and only than apply changes.
*
* @type {Object}
*/
idToIndexHash: {},
/**
* Synchronise changes in records on gSTB level and if there are some - trigger corresponding callbacks in clients.
*
* @type {function}
*/
sync: function () {
var rawData, index, ind, progress, diff;
if ( !daemon.triggers.length ) {
// nobody listen so no need for data update
return;
}
try {
rawData = JSON.parse(pvrManager.GetAllTasks());
//console.log('pvrManager.GetAllTasks(): ' + pvrManager.GetAllTasks());
} catch ( error ) {
rawData = [];
}
// Optimise use of 'forEach' inside 'for' cycle
function callOnAdd ( item ) {
if ( typeof item.onAdd === 'function' ) {
item.onAdd(daemon.rawDataList[daemon.rawDataList.length - 1]);
}
}
// Optimise use of 'forEach' inside 'for' cycle
function callOnProgress ( item ) {
if ( typeof item.onProgress === 'function' ) {
item.onProgress(daemon.rawDataList[index]);
}
}
// Optimise use of 'forEach' inside 'for' cycle
function callOnChange ( item ) {
if ( typeof item.onChange === 'function' ) {
console.log('index: ' + index);
item.onChange(daemon.rawDataList[index]);
}
}
for ( ind = 0; ind < rawData.length; ind++ ) {
index = daemon.idToIndexHash[rawData[ind].id];
if ( index === undefined ) {
// add new record data
daemon.rawDataList.push({
id: rawData[ind].id,
state: rawData[ind].state,
url: rawData[ind].url,
path: rawData[ind].fileName,
channel: rawData[ind].fileName.split('records/')[1].split('/')[0],
name: rawData[ind].fileName.split('/').pop(),
startTime: rawData[ind].startTime,
endTime: rawData[ind].endTime,
progress: rawData[ind].state === 4 ? 100 : 0, // all completed should have 100% progress
server: false,
errorCode: rawData[ind].errorCode
});
daemon.idToIndexHash[rawData[ind].id] = daemon.rawDataList.length - 1;
// trigger callback from client.add(data, callback); call
if ( daemon.lostEvents.add[rawData[ind].fileName] ) {
if ( typeof daemon.lostEvents.add[rawData[ind].fileName] === 'function' ) {
daemon.lostEvents.add[rawData[ind].fileName](daemon.rawDataList[daemon.rawDataList.length - 1]);
}
delete daemon.lostEvents.add[rawData[ind].fileName];
}
// trigger onAdd function in clients
daemon.triggers.forEach(callOnAdd);
} else {
// check if progress changed (every running task)
if ( rawData[ind].state === 2 ) {
progress = Math.ceil((((new Date()).getTime() / 1000 - rawData[ind].startTime) /
(rawData[ind].endTime - rawData[ind].startTime)) * 100);
progress = progress < 0 ? 0 : progress;
progress = progress > 100 ? 100 : progress;
if ( progress !== daemon.rawDataList[index].progress ) {
daemon.rawDataList[index].progress = progress;
daemon.triggers.forEach(callOnProgress);
}
}
// check if state changed
if ( rawData[ind].state !== daemon.rawDataList[index].state ) {
daemon.rawDataList[index].state = rawData[ind].state;
daemon.rawDataList[index].errorCode = rawData[ind].errorCode;
// trigger onChange function in clients
daemon.triggers.forEach(callOnChange);
}
}
}
// find deleted records
if ( rawData.length !== daemon.rawDataList.length ) {
diff = (function Difference ( arr1, arr2 ) {
var Alen = arr1.length,
Blen = arr2.length,
diff = [],
ind1, ind2, ind3;
for ( ind3 = 0; ind3 < Alen; ind3++ ) {
ind1 = ind2 = 0;
while ( ind1 < Blen && arr2[ind1].id !== arr1[ind3].id ) { ind1++; }
while ( ind2 < diff.length && diff[ind2].id !== arr1[ind3].id ) { ind2++; }
if ( ind1 === Blen && ind2 === diff.length ) {
diff[diff.length] = arr1[ind3];
}
}
return diff;
})(daemon.rawDataList, rawData);
diff.forEach(function ( record ) {
var index = daemon.rawDataList.indexOf(record);
// trigger callback from client.remove(record, callback) call
if ( daemon.lostEvents.remove[record.path] ) {
if ( typeof daemon.lostEvents.remove[record.path] === 'function' ) {
daemon.lostEvents.remove[record.path](record);
}
delete daemon.lostEvents.remove[record.path];
}
daemon.rawDataList.splice(index, 1);
// tell clients about deleted record and give it index for fast search and deletion
daemon.triggers.forEach(function ( listener ) {
if ( typeof listener.onRemove === 'function' ) {
listener.onRemove(record, index);
}
});
});
}
}
};
// start listening to STB right now
daemon.sync();
daemon.checkTimerId = window.setInterval(daemon.sync, daemon.checkTime);
/**
* Wrapper with stop method for record data obtained by calling pvrManager.GetAllTasks().
* If record information would be changed it will emit corresponding event.
*
* @constructor
* @extends Emitter
*
* @param {Object} data result of pvrManager.GetAllTasks() call
*
* @example
* var record = new Record(JSON.parse(pvrManager.GetAllTasks())[0]);
*/
function Record ( data ) {
Emitter.call(this);
this.data = {
id: data.id,
state: data.state,
url: data.url,
path: data.path,
channel: data.channel,
name: data.name,
startTime: data.startTime,
endTime: data.endTime,
progress: data.progress,
server: data.server,
errorCode: data.errorCode
};
}
Record.prototype = Object.create(Emitter.prototype);
Record.prototype.constructor = Record;
/**
* Stop recording. Record end time will be set to current and as result it will change state to finished.
*
* @param {callback} callback callback function
*/
Record.prototype.stop = function ( callback ) {
pvrManager.ChangeEndTime(this.data.id, Math.ceil((new Date()).getTime() / 1000));
if ( typeof callback === 'function' ) {
callback({item: this}, null);
}
};
/**
* Record states dictionary (this constants should be used instead of numbers for state comparison).
*
* @namespace
*/
Record.prototype.states = {
/** @const {number} */
WAITING: 1,
/** @const {number} */
RECORDING: 2,
/** @const {number} */
ERROR: 3,
/** @const {number} */
FINISHED: 4
};
/**
* Record manager. Listening daemon for records changes and emitting corresponding events to application.
* Can emit events: progress, state, add, remove.
*
* @constructor
* @extends Emitter
*/
function Client () {
var self = this,
trigger = {};
Emitter.call(this);
this.list = [];
daemon.rawDataList.forEach(function ( item ) {
self.list.push(new Record(item));
});
trigger.onChange = function ( item ) {
var record = self.list[daemon.idToIndexHash[item.id]],
oldValue = record.data.state;
record.data.state = item.state;
if ( record.events['state'] ) {
record.emit('state', {
item: record,
curr: record.data.state,
old: oldValue,
time: (new Date()).getTime()
});
}
};
trigger.onAdd = function ( item ) {
self.list.push(new Record(item));
if ( self.events['add'] ) {
self.emit('add', {item: self.list[self.list.length - 1], time: (new Date()).getTime()});
}
};
trigger.onRemove = function ( item, index ) {
if ( self.events['remove'] ) {
self.emit('remove', {item: (self.list.splice(index, 1))[0], time: (new Date()).getTime()});
}
};
trigger.onProgress = function ( item ) {
var record = self.list[daemon.idToIndexHash[item.id]],
oldValue = record.data.progress;
record.data.progress = item.progress;
if ( record.events['progress'] ) {
record.emit('progress', {
item: record,
curr: record.data.progress,
old: oldValue,
time: (new Date()).getTime()
});
}
};
daemon.triggers.push(trigger);
/**
* Stop this client and remove all it listeners. Use it for cleanup before application exit.
*/
this.destroy = function () {
daemon.triggers.splice(daemon.triggers.indexOf(trigger), 1);
this.removeAllListeners();
this.list = [];
};
}
Client.prototype = Object.create(Emitter.prototype);
Client.prototype.constructor = Client;
/**
* Description for pvr error codes.
*
* @type {Object}
*/
Client.prototype.errorCodes = {
/** @const {number} */
'-1': 'Bad argument.',
/** @const {number} */
'-2': 'Not enough memory.',
'-3': 'Wrong recording range (start or end time). e.i. recording duration must be less or equal than 24 hours.',
'-4': 'Task with specified ID was not found.',
'-5': 'Wrong file name. Folder where you want to save recording must exist and begin with /media/USB- or /ram/media/USB-.',
'-6': 'Duplicate tasks. Recording with that file name already exists.',
'-7': 'Error opening stream URL.',
'-8': 'Error opening output file.',
'-9': 'Maximum number of simultaneous recording is exceeded. It does not mean task number but number of simultaneous' +
' recording. See also SetMaxRecordingCnt.',
'-10': 'Manager got end of stream and recording has finished earlier keeping the recorded file.',
'-11': 'Error writing output file. E.i. disk is full or has been disconnected during recording.',
'-12': 'Wrong url.',
'-13': 'Wrong fileName.',
'-14': 'Wrong startTime.',
'-15': 'Wrong endTime.',
'-16': 'Wrong download object.'
};
/**
* Create new record.
*
* @param {Object} data record info
* @param {string} data.name path to file
* @param {string} data.channel channel url
* @param {number} data.startTime start time
* @param {number} data.endTime end time
* @param {callback} callback callback function
*
* @example
* pvr.add({
* name: '/media/USB-94F9AM9X43RO31TW-1/records/EurosportLive/2016-03-16/00-00-01.ts',
* channel: 'rtp://239.1.1.1:1234',
* startTime: Math.ceil((new Date()).getTime() / 1000 + 10),
* endTime: Math.ceil((new Date()).getTime() / 1000 + 500)
* }, function ( error, data ) {
* console.log(error);
* console.log(data);
* });
*/
Client.prototype.add = function ( data, callback ) {
var state;
if ( typeof callback !== 'function' ) {
console.log('Wrong callback function.');
return;
}
if ( !data.channel ) {
callback(null, {code: '-12', message: this.errorCodes['-12']});
return;
}
if ( !data.name ) {
callback(null, {code: '-13', message: this.errorCodes['-13']});
return;
}
if ( !data.startTime ) {
callback(null, {code: '-14', message: this.errorCodes['-14']});
return;
}
if ( !data.endTime ) {
callback(null, {code: '-15', message: this.errorCodes['-15']});
return;
}
daemon.lostEvents.add[data.name] = callback;
state = pvrManager.CreateTask(data.channel, data.name, data.startTime, data.endTime);
if ( this.errorCodes[state] ) {
callback(null, {code: state, message: this.errorCodes[state]});
// Error happened so record will not be created. Clear unreachable onAdd callback.
delete daemon.lostEvents.add[data.name];
}
};
/**
* Remove record.
*
* @param {Record} item record instance
* @param {Object} options delete options
* @param {boolean} options.deleteFile if true then both file and task will be deleted, if false - only task
* @param {callback} callback callback function
*/
Client.prototype.remove = function ( item, options, callback ) {
options = options || {};
if ( typeof callback !== 'function' ) {
console.log('Wrong callback function.');
return;
}
if ( !item || !(item instanceof Record) ) {
callback(null, {code: '-16', message: this.errorCodes['-16']});
return;
}
daemon.lostEvents.remove[item.data.path] = callback;
// 0 | do not remove any files
// 1 | if temporary file exists, rename it into resulting file
// 2 | remove only temporary file, if it exists
// 3 | remove both temporary and resulting files
pvrManager.RemoveTask(item.data.id, options.deleteFile ? 3 : 1);
};
module.exports = function () {
return new Client();
};