Source: index.js

/**
 * 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();
};