<|start_filename|>tests/Unit/processFrames/identifyGops.data.js<|end_filename|>
'use strict';

const invalidKeyFramesTypes = [
    undefined,
    null,
    true,
    '1',
    {},
    [],
    () => {}
];

const invalidKeyFramesValues = [-1, -0.5, 0.5, 2, 2.5];

const testData = [
    {
        description: 'works okay for the set of frames with no key frame',
        input      : [
            {key_frame: 0},
            {key_frame: 0},
        ],
        res        : {gops: [], remainedFrames: []}
    },
    {
        description: 'works okay for the set of frames with one key frame in the middle, but not completed',
        input      : [
            {key_frame: 0, pkt_pts_time: 1},
            {key_frame: 1, pkt_pts_time: 2},
            {key_frame: 0, pkt_pts_time: 3},
        ],
        res        : {
            gops           : [],
            remainedFrames: [
                {key_frame: 1, pkt_pts_time: 2},
                {key_frame: 0, pkt_pts_time: 3},
            ]
        }
    },
    {
        description: 'works okay for the set of frames with one key frame at the beginning, but not completed',
        input      : [
            {key_frame: 1, pkt_pts_time: 1},
            {key_frame: 0, pkt_pts_time: 2},
            {key_frame: 0, pkt_pts_time: 3},
            {key_frame: 0, pkt_pts_time: 4},
        ],
        res        : {
            gops           : [],
            remainedFrames: [
                {key_frame: 1, pkt_pts_time: 1},
                {key_frame: 0, pkt_pts_time: 2},
                {key_frame: 0, pkt_pts_time: 3},
                {key_frame: 0, pkt_pts_time: 4},
            ]
        }
    },
    {
        description: 'works okay for the set of frames with two completed gops, starts from gop with one frame',
        input      : [
            {key_frame: 1, pkt_pts_time: 1},
            {key_frame: 1, pkt_pts_time: 2},
            {key_frame: 0, pkt_pts_time: 3},
            {key_frame: 0, pkt_pts_time: 4},
            {key_frame: 1, pkt_pts_time: 5},
        ],
        res        : {
            gops           : [
                {
                    frames   : [
                        {key_frame: 1, pkt_pts_time: 1}
                    ],
                    startTime: 1,
                    endTime  : 2
                },
                {
                    frames   : [
                        {key_frame: 1, pkt_pts_time: 2},
                        {key_frame: 0, pkt_pts_time: 3},
                        {key_frame: 0, pkt_pts_time: 4}
                    ],
                    startTime: 2,
                    endTime  : 5
                }
            ],
            remainedFrames: [
                {key_frame: 1, pkt_pts_time: 5},
            ]
        }
    },
    {
        description: 'edge case, works okay even for undefined pkt_pts_time values, error exception would be throw on the next level', // eslint-disable-line
        input      : [
            {key_frame: 1, pkt_pts_time: undefined},
            {key_frame: 0, pkt_pts_time: 3},
            {key_frame: 0, pkt_pts_time: 4},
            {key_frame: 1, pkt_pts_time: undefined},
        ],
        res        : {
            gops           : [
                {
                    frames   : [
                        {key_frame: 1, pkt_pts_time: undefined},
                        {key_frame: 0, pkt_pts_time: 3},
                        {key_frame: 0, pkt_pts_time: 4}
                    ],
                    startTime: undefined,
                    endTime  : undefined
                }
            ],
            remainedFrames: [
                {key_frame: 1, pkt_pts_time: undefined},
            ]
        }
    },
    {
        description: 'works okay for the set of frames with two completed gops, starts from key_frame=1',
        input      : [
            {key_frame: 1, pkt_pts_time: 1},
            {key_frame: 0, pkt_pts_time: 2},
            {key_frame: 0, pkt_pts_time: 3},
            {key_frame: 1, pkt_pts_time: 4},
            {key_frame: 0, pkt_pts_time: 5},
            {key_frame: 0, pkt_pts_time: 6},
            {key_frame: 1, pkt_pts_time: 7},
            {key_frame: 0, pkt_pts_time: 8}
        ],
        res        : {
            gops           : [
                {
                    frames   : [
                        {key_frame: 1, pkt_pts_time: 1},
                        {key_frame: 0, pkt_pts_time: 2},
                        {key_frame: 0, pkt_pts_time: 3}
                    ],
                    startTime: 1,
                    endTime  : 4
                },
                {
                    frames   : [
                        {key_frame: 1, pkt_pts_time: 4},
                        {key_frame: 0, pkt_pts_time: 5},
                        {key_frame: 0, pkt_pts_time: 6}
                    ],
                    startTime: 4,
                    endTime  : 7
                }
            ],
            remainedFrames: [
                {key_frame: 1, pkt_pts_time: 7},
                {key_frame: 0, pkt_pts_time: 8}
            ]
        }
    },
    {
        description: 'works okay for the set of frames with two completed gops, starts from key_frame=0',
        input      : [
            {key_frame: 0, pkt_pts_time: 1},
            {key_frame: 0, pkt_pts_time: 2},
            {key_frame: 1, pkt_pts_time: 3},
            {key_frame: 0, pkt_pts_time: 4},
            {key_frame: 0, pkt_pts_time: 5},
            {key_frame: 1, pkt_pts_time: 6},
            {key_frame: 0, pkt_pts_time: 7},
            {key_frame: 0, pkt_pts_time: 8},
            {key_frame: 1, pkt_pts_time: 9},
            {key_frame: 0, pkt_pts_time: 10},
        ],
        res        : {
            gops           : [
                {
                    frames   : [
                        {key_frame: 1, pkt_pts_time: 3},
                        {key_frame: 0, pkt_pts_time: 4},
                        {key_frame: 0, pkt_pts_time: 5}
                    ],
                    startTime: 3,
                    endTime  : 6
                },
                {
                    frames   : [
                        {key_frame: 1, pkt_pts_time: 6},
                        {key_frame: 0, pkt_pts_time: 7},
                        {key_frame: 0, pkt_pts_time: 8}
                    ],
                    startTime: 6,
                    endTime  : 9
                }
            ],
            remainedFrames: [
                {key_frame: 1, pkt_pts_time: 9},
                {key_frame: 0, pkt_pts_time: 10},
            ]
        }
    }
];

module.exports = {
    invalidKeyFramesTypes,
    invalidKeyFramesValues,
    testData
};



<|start_filename|>src/ExitReasons.js<|end_filename|>
'use strict';

class Reason {
    constructor(payload) {
        this.payload = payload;
    }
}

class StartError extends Reason {}
class ExternalSignal extends Reason {}
class NormalExit extends Reason {}
class AbnormalExit extends Reason {}
class ProcessingError extends Reason {}

module.exports = {
    StartError,
    ExternalSignal,
    NormalExit,
    AbnormalExit,
    ProcessingError
};


<|start_filename|>tests/Functional/Helpers/index.js<|end_filename|>
'use strict';

const {assert} = require('chai');
const {spawn}  = require('child_process');

assert(process.env.FFPROBE, 'Specify path for ffprobe via FFPROBE env var');
assert(process.env.FFMPEG, 'Specify path for ffmpeg via FFMPEG env var');

function startStream(testFile, streamUrl) {
    const command = `${process.env.FFMPEG} -re -i ${testFile} -vcodec copy -acodec copy -listen 1 -f flv ${streamUrl}`
        .split(' ');

    const ffmpeg = spawn(command[0], command.slice(1));

    return new Promise((resolve, reject) => {
        // we listen stderr cuz we rely on stderr banner output
        ffmpeg.stderr.once('data', () => {
            resolve(ffmpeg);
        });

        setTimeout(() => {
            reject('Can not run ffmpeg process');
        }, 5 * 1000);
    });
}

function stopStream(stream) {
    return new Promise((resolve, reject) => {
        stream.kill();

        stream.on('exit', (code, signal) => {
            if (signal === null || signal === 'SIGTERM') {
                return resolve({code, signal});
            }
            return reject(`cannot stop stream in correct way - code: ${code}, signal: ${signal}`);
        });

        setTimeout(() => {
            stream.kill('SIGKILL');
        }, 5 * 1000);
    });
}

module.exports = {
    startStream,
    stopStream
};


<|start_filename|>tests/Unit/FramesMonitor/stopListen.test.js<|end_filename|>
'use strict';

const {assert} = require('chai');
const sinon    = require('sinon');
const Errors   = require('src/Errors');

const {config, url, FramesMonitor, makeChildProcess} = require('./Helpers/');

describe('FramesMonitor::stopListen', () => {
    let framesMonitor;
    let childProcess;

    let stubRunShowFramesProcess;
    let spyOnCpRemoveAllListeners;
    let spyOnCpStdoutRemoveAllListeners;
    let spyOnCpStderrRemoveAllListeners;

    let spySetTimeout;
    let spyCleanTimeout;

    beforeEach(() => {
        framesMonitor = new FramesMonitor(config, url);

        childProcess             = makeChildProcess();
        stubRunShowFramesProcess = sinon.stub(framesMonitor, '_runShowFramesProcess').returns(childProcess);

        spyOnCpRemoveAllListeners       = sinon.spy(childProcess, 'removeAllListeners');
        spyOnCpStdoutRemoveAllListeners = sinon.spy(childProcess.stdout, 'removeAllListeners');
        spyOnCpStderrRemoveAllListeners = sinon.spy(childProcess.stderr, 'removeAllListeners');

        spySetTimeout   = sinon.spy(global, 'setTimeout');
        spyCleanTimeout = sinon.spy(global, 'clearTimeout');
    });

    afterEach(() => {
        stubRunShowFramesProcess.restore();

        spyOnCpRemoveAllListeners.restore();
        spyOnCpStdoutRemoveAllListeners.restore();
        spyOnCpStderrRemoveAllListeners.restore();

        spySetTimeout.restore();
        spyCleanTimeout.restore();
    });

    it('must just resolve when try to stop listen before start listening', async () => {
        const expectedResult = undefined;

        const spyOnKill = sinon.spy(childProcess, 'kill');

        const result = await framesMonitor.stopListen();

        assert.strictEqual(result, expectedResult);

        assert.isTrue(spyOnKill.notCalled);

        assert.isTrue(spyOnCpRemoveAllListeners.notCalled);
        assert.isTrue(spyOnCpStdoutRemoveAllListeners.notCalled);
        assert.isTrue(spyOnCpStderrRemoveAllListeners.notCalled);

        assert.isTrue(spySetTimeout.notCalled);
        assert.isTrue(spyCleanTimeout.notCalled);

        spyOnKill.restore();
    });

    it('must stop listen just fine', async () => {
        const expectedResult = {code: null, signal: 'SIGTERM'};

        const expectedSignal = 'SIGTERM';

        const spyOnKill = sinon.spy(childProcess, 'kill');

        framesMonitor.listen();

        const result = await framesMonitor.stopListen();

        assert.deepEqual(result, expectedResult);

        assert.isTrue(spyOnKill.calledOnce);
        assert.isTrue(spyOnKill.alwaysCalledWithExactly(expectedSignal));

        assert.isTrue(spyOnCpRemoveAllListeners.calledTwice);
        assert.isTrue(spyOnCpStdoutRemoveAllListeners.calledOnce);
        assert.isTrue(spyOnCpStderrRemoveAllListeners.calledOnce);

        assert.isTrue(spySetTimeout.calledOnce);
        assert.isTrue(spyCleanTimeout.calledOnce);

        assert.isNull(framesMonitor._cp);

        spyOnKill.restore();
    });

    it('must reject listen cuz ChildProcess::kill method emitted error event', async () => {
        const expectedError        = new EvalError('SIGTERM is not supported');
        const expectedErrorMessage = 'process exit error';
        const expectedSignal       = 'SIGTERM';

        const stubOnKill = sinon.stub(childProcess, 'kill').callsFake(() => {
            childProcess.emit('error', expectedError);
        });

        framesMonitor.listen();

        try {
            await framesMonitor.stopListen();
            assert.isTrue(false, 'stopListen must reject promise');
        } catch (err) {
            assert.instanceOf(err, Errors.ProcessExitError);
            assert.strictEqual(err.message, expectedErrorMessage);
            assert.strictEqual(err.extra.url, url);
            assert.strictEqual(err.extra.error, expectedError);

            assert.isTrue(stubOnKill.calledOnce);
            assert.isTrue(stubOnKill.alwaysCalledWithExactly(expectedSignal));

            assert.isTrue(spyOnCpRemoveAllListeners.calledTwice);
            assert.isTrue(spyOnCpStdoutRemoveAllListeners.calledOnce);
            assert.isTrue(spyOnCpStderrRemoveAllListeners.calledOnce);

            assert.isTrue(spySetTimeout.notCalled);
            assert.isTrue(spyCleanTimeout.notCalled);

            assert.isNull(framesMonitor._cp);

            stubOnKill.restore();
        }
    });

    it('must reject listen cuz ChildProcess::kill method thrown an error', async () => {
        const expectedError        = new EvalError('SIGTERM is not supported');
        const expectedErrorMessage = 'process exit error';
        const expectedSignal       = 'SIGTERM';

        const stubOnKill = sinon.stub(childProcess, 'kill').throws(expectedError);

        framesMonitor.listen();
        try {
            await framesMonitor.stopListen();
            assert.isTrue(false, 'stopListen must reject promise');
        } catch (err) {
            assert.instanceOf(err, Errors.ProcessExitError);
            assert.strictEqual(err.message, expectedErrorMessage);
            assert.strictEqual(err.extra.url, url);
            assert.strictEqual(err.extra.error, expectedError);

            assert.isTrue(stubOnKill.calledOnce);
            assert.isTrue(stubOnKill.alwaysCalledWithExactly(expectedSignal));

            assert.isTrue(spyOnCpRemoveAllListeners.calledTwice);
            assert.isTrue(spyOnCpStdoutRemoveAllListeners.calledOnce);
            assert.isTrue(spyOnCpStderrRemoveAllListeners.calledOnce);

            assert.isTrue(spySetTimeout.notCalled);
            assert.isTrue(spyCleanTimeout.notCalled);

            assert.isNull(framesMonitor._cp);

            stubOnKill.restore();
        }
    });

    it('must try to kill with SIGKILL if process has ignored SIGTERM', async function () {
        this.timeout(config.exitProcessGuardTimeoutInMs + 2000);

        const expectedResult = {code: null, signal: 'SIGKILL'};

        const stubOnKill = sinon.stub(childProcess, 'kill').callsFake(signal => {
            if (signal === 'SIGKILL') {
                return childProcess.emit('exit', null, 'SIGKILL');
            }
        });

        framesMonitor.listen();

        const result = await framesMonitor.stopListen();

        assert.deepEqual(result, expectedResult);

        assert.isTrue(stubOnKill.calledTwice);
        assert(stubOnKill.getCall(0).calledWithExactly('SIGTERM'));
        assert(stubOnKill.getCall(1).calledWithExactly('SIGKILL'));

        assert.isTrue(spyOnCpRemoveAllListeners.calledTwice);
        assert.isTrue(spyOnCpStdoutRemoveAllListeners.calledOnce);
        assert.isTrue(spyOnCpStderrRemoveAllListeners.calledOnce);

        assert.isTrue(spySetTimeout.calledOnce);
        assert.isTrue(spyCleanTimeout.calledOnce);

        assert.isNull(framesMonitor._cp);

        stubOnKill.restore();
    });

    it('must resolve just okay during the second stop listen in a row', async () => {
        const expectedSignal  = 'SIGTERM';
        const expectedResult1 = {code: null, signal: expectedSignal};
        const expectedResult2 = undefined;

        const spyOnKill = sinon.spy(childProcess, 'kill');

        framesMonitor.listen();

        const result1 = await framesMonitor.stopListen();
        const result2 = await framesMonitor.stopListen();

        assert.deepEqual(result1, expectedResult1);
        assert.strictEqual(result2, expectedResult2);

        assert.isTrue(spyOnKill.calledOnce);
        assert.isTrue(spyOnKill.alwaysCalledWithExactly(expectedSignal));

        assert.isTrue(spyOnCpRemoveAllListeners.calledTwice);
        assert.isTrue(spyOnCpStdoutRemoveAllListeners.calledOnce);
        assert.isTrue(spyOnCpStderrRemoveAllListeners.calledOnce);

        assert.isTrue(spySetTimeout.calledOnce);
        assert.isTrue(spyCleanTimeout.calledOnce);

        assert.isNull(framesMonitor._cp);

        spyOnKill.restore();
    });
});


<|start_filename|>index.js<|end_filename|>
'use strict';

const FramesMonitor = require('./src/FramesMonitor');
const StreamsInfo   = require('./src/StreamsInfo');
const processFrames = require('./src/processFrames');
const ExitReasons   = require('./src/ExitReasons');

const Errors = require('./src/Errors');

module.exports = {
    FramesMonitor,
    StreamsInfo,
    processFrames,
    ExitReasons,
    Errors
};


<|start_filename|>tests/Unit/FramesMonitor/_onProcessStreamsError.test.js<|end_filename|>
'use strict';

const sinon      = require('sinon');
const {assert}   = require('chai');
const dataDriven = require('data-driven');

const Errors = require('src/Errors');

const {config, url, FramesMonitor, makeChildProcess} = require('./Helpers');

describe('FramesMonitor::_onProcessStreamsError', () => {

    let framesMonitor;
    let childProcess;

    let stubRunShowFramesProcess;

    beforeEach(() => {
        framesMonitor = new FramesMonitor(config, url);

        childProcess = makeChildProcess();

        stubRunShowFramesProcess = sinon.stub(framesMonitor, '_runShowFramesProcess').returns(childProcess);
    });

    afterEach(() => {
        stubRunShowFramesProcess.restore();
    });

    const data = [
        {type: 'stdout'},
        {type: 'stderr'}
    ];

    dataDriven(data, () => {
        it('must wrap and handle each error emitted by the child process {type} stream object', async ctx => {
            const expectedErrorType = Errors.ProcessStreamError;

            const stubHandleProcessingError = sinon.stub(framesMonitor, '_handleProcessingError').resolves();

            const originalError = new Error('test error');
            const expectedError = new Errors.ProcessStreamError(
                `got an error from a ${config.ffprobePath} ${ctx.type} process stream.`, {
                    url  : url,
                    error: originalError
                }
            );

            await framesMonitor._onProcessStreamsError(ctx.type, originalError);

            assert.isTrue(stubHandleProcessingError.calledOnce);

            assert.lengthOf(stubHandleProcessingError.getCall(0).args, 1);

            const error = stubHandleProcessingError.getCall(0).args[0];

            assert.instanceOf(error, expectedErrorType);
            assert.strictEqual(error.message, expectedError.message);
            assert.strictEqual(error.extra.url, expectedError.extra.url);
            assert.strictEqual(error.extra.error, originalError);

            stubHandleProcessingError.restore();
        });
    });

    dataDriven(data, () => {
        it('handle error emitted by the child process {type} stream object and reject promise if the error has occurred during the processing', async ctx => { // eslint-disable-line
            const innerError        = new Error('some badass error');
            const expectedErrorType = Errors.ProcessStreamError;

            const stubHandleProcessingError = sinon.stub(framesMonitor, '_handleProcessingError').rejects(innerError);

            const originalError = new Error('test error');
            const expectedError = new Errors.ProcessStreamError(
                `got an error from a ${config.ffprobePath} ${ctx.type} process stream.`, {
                    url  : url,
                    error: originalError
                }
            );

            try {
                await framesMonitor._onProcessStreamsError(ctx.type, originalError);
                assert.isTrue(false, 'should not be here, cuz _onProcessStreamsError rejected promise');
            } catch (err) {
                assert.instanceOf(err, innerError.constructor);
                assert.strictEqual(err.message, innerError.message);

                assert.isTrue(stubHandleProcessingError.calledOnce);

                assert.lengthOf(stubHandleProcessingError.getCall(0).args, 1);

                const error = stubHandleProcessingError.getCall(0).args[0];

                assert.instanceOf(error, expectedErrorType);
                assert.strictEqual(error.message, expectedError.message);
                assert.strictEqual(error.extra.url, expectedError.extra.url);
                assert.strictEqual(error.extra.error, originalError);
            } finally {
                stubHandleProcessingError.restore();
            }
        });
    });

});


<|start_filename|>tests/Unit/processFrames/gopPktSize.test.js<|end_filename|>
'use strict';

const {assert}   = require('chai');
const dataDriven = require('data-driven');

const processFrames = require('src/processFrames');

const Errors = require('src/Errors');

function typeOf(item) {
    return Object.prototype.toString.call(item);
}

describe('processFrames.accumulatePktSize', () => {

    const invalidData = [
        undefined,
        null,
        true,
        '1',
        {},
        () => {},
        Symbol(),
        Buffer.alloc(0)
    ];

    dataDriven(
        invalidData.map(item => ({type: typeOf(item), item: item})),
        () => {
            it('must throw an error if frame pkt_size field has invalid {type} type', ctx => {
                const invalidFrame = {pkt_size: ctx.item};
                const invalidInput = {
                    frames   : [invalidFrame],
                    startTime: 1,
                    endTime  : 1
                };

                try {
                    processFrames.calculatePktSize(invalidInput.frames);
                    assert.isFalse(true, 'should not be here');
                } catch (error) {
                    assert.instanceOf(error, Errors.FrameInvalidData);

                    assert.strictEqual(error.message, `frame's pkt_size field has invalid type ${ctx.type}`);

                    assert.deepEqual(error.extra, {frame: invalidFrame});
                }
            });
        }
    );

    it('must correct accumulate frames pkt_size', () => {
        const frames = [
            {pkt_size: 1},
            {pkt_size: 2},
            {pkt_size: 3}
        ];

        const expectedRes = frames.reduce((sum, frame) => sum + frame.pkt_size, 0);

        const res = processFrames.calculatePktSize(frames);

        assert.strictEqual(res, expectedRes);
    });


});


<|start_filename|>tests/Unit/processFrames/filterVideoFrames.test.js<|end_filename|>
'use strict';

const {assert} = require('chai');

const processFrames = require('src/processFrames');

describe('processFrames.filterVideoFrames', () => {

    it('must corret filter video frames', () => {
        const expectedResult = [
            {media_type: 'video', width: 1},
            {media_type: 'video', width: 2}
        ];

        let frames = [
            {media_type: 'video', width: 1},
            {media_type: 'audio'},
            {media_type: 'data'},
            {media_type: 'video', width: 2}
        ];

        const videoFrames = processFrames.filterVideoFrames(frames);

        assert.deepEqual(videoFrames, expectedResult);
    });

    it('must corret filter empty array of frames', () => {
        const expectedResult = [];

        const videoFrames = processFrames.filterVideoFrames([]);

        assert.deepEqual(videoFrames, expectedResult);
    });

});


<|start_filename|>tests/Unit/processFrames/areAllGopsIdentical.test.js<|end_filename|>
'use strict';

const {assert} = require('chai');

const processFrames = require('src/processFrames');

describe('processFrames.areAllGopsIdentical', () => {

    it('must determine that all gops identical', () => {
        const gops = [
            {
                frames: [
                    {key_frame: 1, pict_type: 'I'},
                    {key_frame: 0, pict_type: 'I'}
                ]
            },
            {
                frames: [
                    {key_frame: 1, pict_type: 'I'},
                    {key_frame: 0, pict_type: 'P'}
                ]
            }
        ];

        const expectedAnswer = true;

        const res = processFrames.areAllGopsIdentical(gops);

        assert.strictEqual(res, expectedAnswer);
    });

    it('must determine that not all gops identical', () => {
        const gops = [
            {
                frames: [
                    {key_frame: 1, pict_type: 'I'},
                    {key_frame: 0, pict_type: 'I'},
                    {key_frame: 0, pict_type: 'P'}
                ]
            },
            {
                frames: [
                    {key_frame: 1, pict_type: 'I'},
                    {key_frame: 0, pict_type: 'P'}
                ]
            }
        ];

        const expectedAnswer = false;

        const res = processFrames.areAllGopsIdentical(gops);

        assert.strictEqual(res, expectedAnswer);
    });

});


<|start_filename|>tests/Unit/processFrames/calculateDisplayAspectRatio.test.js<|end_filename|>
'use strict';

const {assert} = require('chai');
const dataDriven = require('data-driven');

const processFrames = require('src/processFrames');
const {invalidParams, validParams} = require('./calculateDisplayAspectRatio.data');

describe('processFrames.calculateDisplayAspectRatio', () => {

    dataDriven(invalidParams, function () {
        it('width and height must be a positive integers, but {description} was passed', function (ctx) {
            const expectedErrorMsg   = /must be a positive integer/;
            const expectedErrorClass = TypeError;

            assert.throws(() => {
                processFrames.calculateDisplayAspectRatio(ctx.width, ctx.height);
            }, expectedErrorClass, expectedErrorMsg);
        });
    });

    dataDriven(validParams, function () {
        it('calculate display aspect ratio for correct input {aspectRatio}', (ctx) => {
            const expected = ctx.aspectRatio;

            const result = processFrames.calculateDisplayAspectRatio(ctx.width, ctx.height);

            assert.strictEqual(result, expected);
        });
    });
});


<|start_filename|>tests/Unit/FramesMonitor/_assertExecutable.test.js<|end_filename|>
'use strict';

const fs         = require('fs');
const proxyquire = require('proxyquire');
const sinon      = require('sinon');
const {assert}   = require('chai');

const Errors = require('src/Errors');

const {config} = require('./Helpers');

describe('FramesMonitor::_assertExecutable', () => {
    it('do nothing if path points to executable file', () => {
        const stubAccessSync = sinon.stub();

        const FramesMonitor = proxyquire('src/FramesMonitor', {
            fs: {
                accessSync: stubAccessSync
            }
        });

        FramesMonitor._assertExecutable(config.ffprobePath);

        assert.isTrue(stubAccessSync.calledOnce);
        assert.isTrue(stubAccessSync.calledWithExactly(config.ffprobePath, fs.constants.X_OK));
    });

    it('throw an exception if accessSync throws an error', () => {
        const expectedError  = new Error('1');
        const stubAccessSync = sinon.stub().throws(expectedError);

        const FramesMonitor = proxyquire('src/FramesMonitor', {
            fs: {
                accessSync: stubAccessSync
            }
        });

        try {
            FramesMonitor._assertExecutable(config.ffprobePath);
            assert.isTrue(false, '_assertExecutable should throw an error, and you should not be here');
        } catch (err) {
            assert.isTrue(stubAccessSync.calledOnce);
            assert.isTrue(stubAccessSync.calledWithExactly(config.ffprobePath, fs.constants.X_OK));

            assert.instanceOf(err, Errors.ExecutablePathError);
            assert.strictEqual(err.message, expectedError.message);
            assert.deepEqual(err.extra, {
                path: config.ffprobePath
            });
        }
    });
});


<|start_filename|>tests/Unit/processFrames/calculateBitrate.test.js<|end_filename|>
'use strict';

const {assert} = require('chai');

const processFrames = require('src/processFrames');

describe('processFrames.calculateBitrate', () => {

    it('must correct calculate min, max and average bitrate for gops', () => {
        const expectedBitrate = {
            min : ((1 + 2) / (5 - 2)) * 8 / 1024,
            max : (1 / (2 - 1)) * 8 / 1024,
            mean: (((1 + 2) / (5 - 2)) + (1 / (2 - 1))) / 2 * 8 / 1024
        };

        const gops = [
            {
                frames   : [
                    {key_frame: 1, pkt_size: 1, pkt_pts_time: 1},
                ],
                startTime: 1,
                endTime  : 2,
            },
            {
                frames   : [
                    {key_frame: 1, pkt_size: 1, pkt_pts_time: 2},
                    {key_frame: 0, pkt_size: 2, pkt_pts_time: 4},
                ],
                startTime: 2,
                endTime  : 5
            }
        ];

        const bitrate = processFrames.calculateBitrate(gops);

        assert.deepEqual(bitrate, expectedBitrate);
    });

});


<|start_filename|>tests/Unit/FramesMonitor/_onProcessStartError.test.js<|end_filename|>
'use strict';

const sinon    = require('sinon');
const {assert} = require('chai');

const ExitReasons = require('src/ExitReasons');

const {config, url, FramesMonitor, makeChildProcess} = require('./Helpers');

describe('FramesMonitor::_onProcessStartError', () => {
    let framesMonitor;
    let childProcess;

    let stubRunShowFramesProcess;

    let spyOnCpRemoveAllListeners;
    let spyOnCpStdoutRemoveAllListeners;
    let spyOnCpStderrRemoveAllListeners;

    beforeEach(() => {
        framesMonitor = new FramesMonitor(config, url);

        childProcess = makeChildProcess();

        stubRunShowFramesProcess = sinon.stub(framesMonitor, '_runShowFramesProcess').returns(childProcess);

        spyOnCpRemoveAllListeners       = sinon.spy(childProcess, 'removeAllListeners');
        spyOnCpStdoutRemoveAllListeners = sinon.spy(childProcess.stdout, 'removeAllListeners');
        spyOnCpStderrRemoveAllListeners = sinon.spy(childProcess.stderr, 'removeAllListeners');

        framesMonitor.listen();
    });

    afterEach(() => {
        stubRunShowFramesProcess.restore();

        spyOnCpRemoveAllListeners.restore();
        spyOnCpStdoutRemoveAllListeners.restore();
        spyOnCpStderrRemoveAllListeners.restore();
    });

    it('must emit processStartError on call and doesn\'t remove listeners from child process cuz it\'s null', done => {
        const originalError = new Error('original, but not enough');

        const expectedErrorMessage      = `${config.ffprobePath} process could not be started.`;
        const expectedChildProcessValue = null;

        // explicitly set to null to test that no listeners will be removed
        framesMonitor._cp = expectedChildProcessValue;

        framesMonitor.on('exit', reason => {
            assert.isTrue(spyOnCpRemoveAllListeners.notCalled);
            assert.isTrue(spyOnCpStdoutRemoveAllListeners.notCalled);
            assert.isTrue(spyOnCpStderrRemoveAllListeners.notCalled);

            assert.strictEqual(framesMonitor._cp, expectedChildProcessValue);

            assert.instanceOf(reason, ExitReasons.StartError);

            assert.strictEqual(reason.payload.error.message, expectedErrorMessage);
            assert.strictEqual(reason.payload.error.extra.url, url);
            assert.strictEqual(reason.payload.error.extra.error, originalError);

            // done is used in order to check that exactly exit event has been emitted
            done();
        });

        framesMonitor._onProcessStartError(originalError);
    });

    it('must emit processStartError on call and remove all listeners from the child process', done => {
        const originalError = new Error('original, but not enough');

        const expectedErrorMessage      = `${config.ffprobePath} process could not be started.`;
        const expectedChildProcessValue = null;

        framesMonitor.on('exit', reason => {
            assert.isTrue(spyOnCpRemoveAllListeners.calledOnce);
            assert.isTrue(spyOnCpRemoveAllListeners.calledWithExactly());

            assert.isTrue(spyOnCpStdoutRemoveAllListeners.calledOnce);
            assert.isTrue(spyOnCpStdoutRemoveAllListeners.calledWithExactly());

            assert.isTrue(spyOnCpStderrRemoveAllListeners.calledOnce);
            assert.isTrue(spyOnCpStderrRemoveAllListeners.calledWithExactly());

            assert.strictEqual(framesMonitor._cp, expectedChildProcessValue);

            assert.instanceOf(reason, ExitReasons.StartError);

            assert.strictEqual(reason.payload.error.message, expectedErrorMessage);
            assert.strictEqual(reason.payload.error.extra.url, url);
            assert.strictEqual(reason.payload.error.extra.error, originalError);

            done();
        });

        framesMonitor._onProcessStartError(originalError);
    });
});


<|start_filename|>tests/Unit/FramesMonitor/isListening.test.js<|end_filename|>
'use strict';

const {assert} = require('chai');

const {config, url, FramesMonitor, makeChildProcess} = require('./Helpers/');

describe('FramesMonitor::isListening', () => {

    let framesMonitor;

    beforeEach(() => {
        framesMonitor = new FramesMonitor(config, url);
    });

    it("must return false, cuz we didn't run listen method", () => {
        const expectedIsListening = false;

        framesMonitor._cp = null;

        assert.strictEqual(framesMonitor.isListening(), expectedIsListening);
    });

    it('must return true, cuz we started listen', () => {
        const expectedIsListening = true;

        framesMonitor._cp = makeChildProcess();

        assert.strictEqual(framesMonitor.isListening(), expectedIsListening);
    });
});


<|start_filename|>tests/Unit/processFrames/gopDurationInSec.test.js<|end_filename|>
'use strict';

const {assert}   = require('chai');
const dataDriven = require('data-driven');

const processFrames = require('src/processFrames');

const Errors = require('src/Errors');

function typeOf(item) {
    return Object.prototype.toString.call(item);
}

describe('processFrames.gopDurationInSec', () => {

    const invalidData = [
        undefined,
        null,
        true,
        '1',
        {},
        [],
        () => {}
    ];

    dataDriven(
        invalidData.map(item => ({type: typeOf(item), startTime: item})),
        () => {
            it('must throw an error if gop start time field has invalid {type} type', ctx => {
                const invalidStartTime = ctx.startTime;
                const validEndTime     = 2;

                const invalidInput = {
                    frames   : [],
                    startTime: invalidStartTime,
                    endTime  : validEndTime
                };

                try {
                    processFrames.gopDurationInSec(invalidInput);
                    assert.isFalse(true, 'should not be here');
                } catch (error) {
                    assert.instanceOf(error, Errors.GopInvalidData);

                    assert.strictEqual(
                        error.message,
                        `gops's start time has invalid type ${Object.prototype.toString.call(invalidInput.startTime)}`
                    );

                    assert.deepEqual(error.extra, {gop: invalidInput});
                }
            });
        }
    );

    dataDriven(
        invalidData.map(item => ({type: typeOf(item), endTime: item})),
        () => {
            it('must throw an error if gop end time field has invalid {type} type', ctx => {
                const validStartTime = 1;
                const invalidEndTime = ctx.endTime;

                const invalidInput = {
                    frames   : [],
                    startTime: validStartTime,
                    endTime  : invalidEndTime
                };

                try {
                    processFrames.gopDurationInSec(invalidInput);
                    assert.isFalse(true, 'should not be here');
                } catch (error) {
                    assert.instanceOf(error, Errors.GopInvalidData);

                    assert.strictEqual(
                        error.message,
                        `gops's end time has invalid type ${Object.prototype.toString.call(invalidInput.endTime)}`
                    );

                    assert.deepEqual(error.extra, {gop: invalidInput});
                }
            });
        }
    );

    it('must throw an error if gop start time field has invalid, negative value', () => {
        const invalidStartTime = -1;
        const validEndTime     = 2;

        const invalidInput = {
            frames   : [],
            startTime: invalidStartTime,
            endTime  : validEndTime
        };

        try {
            processFrames.gopDurationInSec(invalidInput);
            assert.isFalse(true, 'should not be here');
        } catch (error) {
            assert.instanceOf(error, Errors.GopInvalidData);

            assert.strictEqual(
                error.message,
                `gop's start time has invalid value ${invalidInput.startTime}`
            );

            assert.deepEqual(error.extra, {gop: invalidInput});
        }
    });

    it('must throw an error if gop end time field has invalid, negative value', () => {
        const validStartTime      = 1;
        const invalidValidEndTime = -1;

        const invalidInput = {
            frames   : [],
            startTime: validStartTime,
            endTime  : invalidValidEndTime
        };

        try {
            processFrames.gopDurationInSec(invalidInput);
            assert.isFalse(true, 'should not be here');
        } catch (error) {
            assert.instanceOf(error, Errors.GopInvalidData);

            assert.strictEqual(
                error.message,
                `gop's end time has invalid value ${invalidInput.endTime}`
            );

            assert.deepEqual(error.extra, {gop: invalidInput});
        }
    });

    it('must throw an error if gop end time has invalid, zero value', () => {
        const validStartTime = 0;
        const invalidEndTime = 0;

        const invalidInput = {
            frames   : [],
            startTime: validStartTime,
            endTime  : invalidEndTime
        };

        try {
            processFrames.gopDurationInSec(invalidInput);
            assert.isFalse(true, 'should not be here');
        } catch (error) {
            assert.instanceOf(error, Errors.GopInvalidData);

            assert.strictEqual(
                error.message,
                `gop's end time has invalid value ${invalidInput.endTime}`
            );

            assert.deepEqual(error.extra, {gop: invalidInput});
        }
    });

    it('must throw an error cuz the diff between gop start and end times equals to zero', () => {
        const validStartTime = 2;
        const invalidEndTime = 1;

        const invalidInput = {
            frames   : [],
            startTime: validStartTime,
            endTime  : invalidEndTime
        };

        try {
            processFrames.gopDurationInSec(invalidInput);
            assert.isFalse(true, 'should not be here');
        } catch (error) {
            assert.instanceOf(error, Errors.GopInvalidData);

            assert.strictEqual(
                error.message,
                `invalid difference between gop start and end time: ${invalidEndTime - validStartTime}`,
            );

            assert.deepEqual(error.extra, {gop: invalidInput});
        }
    });

});


<|start_filename|>tests/Unit/StreamsInfo/Helpers/index.js<|end_filename|>
const proxyquire = require('proxyquire');

const correctPath = '/correct/path';

const StreamsInfo = proxyquire('src/StreamsInfo', {
    fs: {
        accessSync(filePath) {
            if (filePath !== correctPath) {
                throw new Error('no such file or directory');
            }
        }
    }
});

const correctUrl = 'rtmp://localhost:1935/myapp/mystream';

module.exports = {
    correctPath,
    correctUrl,
    StreamsInfo
};


<|start_filename|>tests/Unit/FramesMonitor/_reduceFramesFromChunks.test.js<|end_filename|>
'use strict';

const {assert}   = require('chai');
const dataDriven = require('data-driven');

const {FramesMonitor} = require('./Helpers');

const Errors = require('src/Errors');

describe('FramesMonitor::_reduceFramesFromChunks', () => {

    const data = [
        {
            description         : 'must return empty array of frames for empty input',
            inputChunkRemainder : '[FRAME]\na=b\n',
            input               : 'c=d',
            outputChunkRemainder: '[FRAME]\na=b\nc=d',
            expectedFrames      : []
        },
        {
            description         : 'must return empty array of frames for incomplete frame input',
            inputChunkRemainder : '',
            input               : '[FRAME]\na=b\nc=d',
            outputChunkRemainder: '[FRAME]\na=b\nc=d',
            expectedFrames      : []
        },
        {
            description         : 'must return an array with one raw frame',
            inputChunkRemainder : '',
            input               : '[FRAME]\na=b\nc=d\n[/FRAME]',
            outputChunkRemainder: '',
            expectedFrames      : ['a=b\nc=d']
        },
        {
            description         : 'must return an array with two raw frames',
            inputChunkRemainder : '[FRAME]\na=b\n',
            input               : 'c=d\n[/FRAME]\n[FRAME]\na2=b2\nc2=d2\n[/FRAME]\n[FRAME]\na3=b3\nc2=d2',
            outputChunkRemainder: '\n[FRAME]\na3=b3\nc2=d2',
            expectedFrames      : ['a=b\nc=d', 'a2=b2\nc2=d2']
        }
    ];

    dataDriven(data, () => {
        it('{description}', ctx => {
            let chunkRemainder;
            let frames;

            assert.doesNotThrow(() => {
                ({chunkRemainder, frames} = FramesMonitor._reduceFramesFromChunks(ctx.inputChunkRemainder + ctx.input));
            });

            assert.deepStrictEqual(frames, ctx.expectedFrames);
            assert.deepStrictEqual(chunkRemainder, ctx.outputChunkRemainder);
        });
    });

    it('must throw an exception, invalid data input (unclosed frame)', () => {
        const expectedErrorType    = Errors.InvalidFrameError;
        const expectedErrorMessage = 'Can not process frame with invalid structure.';

        const chunkRemainder = '[FRAME]\na=b\n';
        const newChunk       = '[FRAME]\na=b\nc=d\n[/FRAME]';

        try {
            FramesMonitor._reduceFramesFromChunks(chunkRemainder + newChunk);
            assert.isFalse(true, 'Should not be here');
        } catch (err) {
            assert.instanceOf(err, expectedErrorType);

            assert.strictEqual(err.message, expectedErrorMessage);

            assert.deepEqual(err.extra, {
                data : chunkRemainder + newChunk,
                frame: '[FRAME]\na=b\n[FRAME]\na=b\nc=d\n'
            });
        }
    });

    it('must throw an exception, invalid data input (end block without starting one)', () => {
        const expectedErrorType    = Errors.InvalidFrameError;
        const expectedErrorMessage = 'Can not process frame with invalid structure.';

        const chunkRemainder = '[FRAME]\na=b\n[/FRAME]\n';
        const newChunk       = 'a=b\nc=d\n[/FRAME]';

        try {
            FramesMonitor._reduceFramesFromChunks(chunkRemainder + newChunk);
            assert.isFalse(true, 'Should not be here');
        } catch (err) {
            assert.instanceOf(err, expectedErrorType);

            assert.strictEqual(err.message, expectedErrorMessage);

            assert.deepEqual(err.extra, {
                data : chunkRemainder + newChunk,
                frame: '\na=b\nc=d\n'
            });
        }
    });
});


<|start_filename|>tests/Unit/processFrames/networkStats.data.js<|end_filename|>
'use strict';

const invalidFramesTypes = [
    undefined,
    null,
    false,
    1,
    '1',
    {},
    Symbol(),
    () => {},
    Buffer.alloc(0)
];

const invalidDurationInSecTypes = [
    undefined,
    null,
    false,
    [],
    [1, 2],
    '1',
    1.2,
    -1,
    {},
    Symbol(),
    () => {},
    Buffer.alloc(0)
];

const testData = [
    {
        description: 'empty frames array',
        frames        : [],
        durationInMsec: 1000,
        expected      : {
            videoFrameRate: 0,
            audioFrameRate: 0,
            videoBitrate: 0,
            audioBitrate: 0,
        },
    },
    {
        description   : 'audio only frames with 1000 msec duration',
        frames        : [
            {pkt_size: 2, pkt_pts_time: 10, media_type: 'audio', key_frame: 1},
            {pkt_size: 3, pkt_pts_time: 11, media_type: 'audio', key_frame: 1},
        ],
        durationInMsec: 1000,
        expected      : {
            videoFrameRate: 0,
            audioFrameRate: 2,
            videoBitrate: 0,
            audioBitrate: 0.0390625,
        },
    },
    {
        description   : 'video only frames with 1000 msec duration',
        frames        : [
            {width: 854, height: 480, pkt_size: 2, pkt_pts_time: 1, media_type: 'video', key_frame: 1, pict_type: 'P'},
            {width: 854, height: 480, pkt_size: 3, pkt_pts_time: 10, media_type: 'video', key_frame: 0, pict_type: 'P'},
        ],
        durationInMsec: 1000,
        expected      : {
            videoFrameRate: 2,
            audioFrameRate: 0,
            videoBitrate: 0.0390625,
            audioBitrate: 0,
        },
    },
    {
        description   : 'frames with 200 msec duration',
        frames        : [
            {width: 854, height: 480, pkt_size: 2, pkt_pts_time: 1, media_type: 'video', key_frame: 1, pict_type: 'P'},
            {pkt_size: 2, pkt_pts_time: 10, media_type: 'audio', key_frame: 1},
            {pkt_size: 3, pkt_pts_time: 11, media_type: 'audio', key_frame: 1},
            {width: 854, height: 480, pkt_size: 3, pkt_pts_time: 10, media_type: 'video', key_frame: 0, pict_type: 'P'},
            {pkt_size: 4, pkt_pts_time: 12, media_type: 'audio', key_frame: 1},
        ],
        durationInMsec: 200,
        expected      : {
            videoFrameRate: 10,
            audioFrameRate: 15,
            videoBitrate: 0.1953125,
            audioBitrate: 0.3515625,
        },
    },
    {
        description   : 'audio only frames with 2000 msec duration',
        frames        : [
            {width: 854, height: 480, pkt_size: 5, pkt_pts_time: 1, media_type: 'video', key_frame: 1, pict_type: 'P'},
            {pkt_size: 2, pkt_pts_time: 10, media_type: 'audio', key_frame: 1},
            {pkt_size: 3, pkt_pts_time: 11, media_type: 'audio', key_frame: 1},
            {width: 854, height: 480, pkt_size: 6, pkt_pts_time: 10, media_type: 'video', key_frame: 0, pict_type: 'P'},
            {pkt_size: 4, pkt_pts_time: 12, media_type: 'audio', key_frame: 1},
            {width: 854, height: 480, pkt_size: 7, pkt_pts_time: 10, media_type: 'video', key_frame: 0, pict_type: 'I'},
        ],
        durationInMsec: 2000,
        expected      : {
            videoFrameRate: 1.5,
            audioFrameRate: 1.5,
            videoBitrate: 0.0703125,
            audioBitrate: 0.03515625,
        },
    },
];

module.exports = {
    invalidFramesTypes,
    invalidDurationInSecTypes,
    testData
};



<|start_filename|>tests/Unit/FramesMonitor/_handleProcessingError.test.js<|end_filename|>
'use strict';

const sinon    = require('sinon');
const {assert} = require('chai');

const ExitReasons = require('src/ExitReasons');

const {config, url, FramesMonitor, makeChildProcess} = require('./Helpers');

describe('FramesMonitor::_handleProcessingError', () => {
    let framesMonitor;
    let childProcess;

    let stubRunShowFramesProcess;

    beforeEach(() => {
        framesMonitor = new FramesMonitor(config, url);

        childProcess = makeChildProcess();

        stubRunShowFramesProcess = sinon.stub(framesMonitor, '_runShowFramesProcess').returns(childProcess);

        framesMonitor.listen();
    });

    afterEach(() => {
        stubRunShowFramesProcess.restore();
    });

    it('must call `exit` event after correct stopListen call', async () => {
        const expectedError = new Error('super puper bad error');

        const stubStopListen = sinon.stub(framesMonitor, 'stopListen').resolves();
        const spyOnExit      = sinon.spy();
        const spyOnError     = sinon.spy();

        framesMonitor.on('error', spyOnError);
        framesMonitor.on('exit', spyOnExit);

        await framesMonitor._handleProcessingError(expectedError);

        assert.isTrue(stubStopListen.calledOnce);
        assert.isTrue(stubStopListen.calledWithExactly());

        assert.isTrue(spyOnError.notCalled);
        assert.isTrue(spyOnExit.calledOnce);

        const errorObjectOnExit = spyOnExit.getCall(0).args[0];
        assert.instanceOf(errorObjectOnExit, ExitReasons.ProcessingError);
        assert.strictEqual(errorObjectOnExit.payload.error, expectedError);

        stubStopListen.restore();
    });

    it('must call `error` event right before `exit` one if stopListen rejected promise', async () => {
        const expectedError = new Error('super puper bad error');
        const rejectError   = new Error('reject error');

        const stubStopListen = sinon.stub(framesMonitor, 'stopListen').rejects(rejectError);
        const spyOnExit      = sinon.spy();
        const spyOnError     = sinon.spy();

        framesMonitor.on('error', spyOnError);
        framesMonitor.on('exit', spyOnExit);

        await framesMonitor._handleProcessingError(expectedError);

        assert.isTrue(stubStopListen.calledOnce);
        assert.isTrue(stubStopListen.calledWithExactly());

        assert.isTrue(spyOnError.calledOnce);
        assert.isTrue(spyOnExit.calledOnce);

        const errorObjectOnError = spyOnError.getCall(0).args[0];
        assert.strictEqual(errorObjectOnError, rejectError);

        const errorObjectOnExit = spyOnExit.getCall(0).args[0];
        assert.instanceOf(errorObjectOnExit, ExitReasons.ProcessingError);
        assert.strictEqual(errorObjectOnExit.payload.error, expectedError);

        sinon.assert.callOrder(spyOnError, spyOnExit);

        stubStopListen.restore();
    });
});


<|start_filename|>tests/Unit/processFrames/calculateFps.test.js<|end_filename|>
'use strict';

const {assert} = require('chai');

const processFrames = require('src/processFrames');

describe('processFrames.calculateFps', () => {

    it('must correct calculate min, max and average fps for gops', () => {
        const expectedFps = {
            min : 0.5,
            max : 1,
            mean: 0.75
        };

        const gops = [
            {
                frames   : [
                    {key_frame: 1, pkt_pts_time: 1},
                ],
                startTime: 1,
                endTime  : 2
            },
            {
                frames   : [
                    {key_frame: 1, pkt_pts_time: 3},
                    {key_frame: 0, pkt_pts_time: 5},
                    {key_frame: 0, pkt_pts_time: 7},
                ],
                startTime: 3,
                endTime  : 9
            }
        ];

        const bitrate = processFrames.calculateFps(gops);

        assert.deepEqual(bitrate, expectedFps);
    });

});


<|start_filename|>tests/Unit/FramesMonitor/_isValidErrorLevel.test.js<|end_filename|>
'use strict';

const {assert} = require('chai');

const FramesMonitor = require('src/FramesMonitor');
const {config}      = require('./Helpers');

describe('FramesMonitor::_isValidErrorLevel', () => {
    it('must return true for correct error level', () => {
        const expectedResult = true;

        const result = FramesMonitor._isValidErrorLevel(config.errorLevel);

        assert.strictEqual(result, expectedResult);
    });

    it('must return false for in-correct error level', () => {
        const incorrectPath  = 'incorrect-part';
        const expectedResult = false;

        const result = FramesMonitor._isValidErrorLevel(config.errorLevel + incorrectPath);

        assert.strictEqual(result, expectedResult);
    });
});


<|start_filename|>tests/Unit/FramesMonitor/listen.test.js<|end_filename|>
'use strict';

const sinon    = require('sinon');
const {assert} = require('chai');

const Errors = require('src/Errors');

const {config, url, FramesMonitor, makeChildProcess} = require('./Helpers/');

describe('FramesMonitor::listen', () => {

    let framesMonitor;
    let childProcess;

    let stubRunShowFramesProcess;
    let spyIsListening;

    beforeEach(() => {
        framesMonitor = new FramesMonitor(config, url);

        childProcess = makeChildProcess();

        stubRunShowFramesProcess = sinon.stub(framesMonitor, '_runShowFramesProcess').returns(childProcess);
        spyIsListening           = sinon.spy(framesMonitor, 'isListening');
    });

    afterEach(() => {
        stubRunShowFramesProcess.restore();
        spyIsListening.restore();
    });

    it('must start listen just fine', () => {
        framesMonitor.listen();

        assert.isTrue(spyIsListening.calledOnce);

        assert.isTrue(stubRunShowFramesProcess.calledOnce);
        assert.isTrue(stubRunShowFramesProcess.firstCall.calledWithExactly());

        assert.isDefined(framesMonitor._cp);
    });

    it('must throw an exception when try listen several times in a row', () => {
        const expectedErrorType    = Errors.AlreadyListeningError;
        const expectedErrorMessage = 'You are already listening.';

        framesMonitor.listen();

        try {
            framesMonitor.listen();
            assert.isTrue(false, 'listen must throw exception');
        } catch (err) {
            assert.isTrue(spyIsListening.calledTwice);

            assert.instanceOf(err, expectedErrorType);

            assert.strictEqual(err.message, expectedErrorMessage);
            assert.isUndefined(err.extra);

            assert.isTrue(stubRunShowFramesProcess.calledOnce);
            assert.isTrue(stubRunShowFramesProcess.firstCall.calledWithExactly());

            assert.isDefined(framesMonitor._cp);
        }
    });

    it('must correct set callback for exit event', () => {
        const expectedCode   = 1;
        const expectedSignal = 'SIGTERM';

        const stubOnExit = sinon.stub(framesMonitor, '_onExit');

        framesMonitor.listen();

        childProcess.emit('exit', expectedCode, expectedSignal);

        assert.isTrue(spyIsListening.calledOnce);

        assert.isDefined(framesMonitor._cp);

        assert.isTrue(stubOnExit.calledOnce);
        assert.isTrue(stubOnExit.calledWithExactly(expectedCode, expectedSignal));

        stubOnExit.restore();
    });

    it('must correct set callback for error event', () => {
        const expectedError = new Error('process start error');

        const stubOnProcessStartError = sinon.stub(framesMonitor, '_onProcessStartError');

        framesMonitor.listen();

        childProcess.emit('error', expectedError);

        assert.isTrue(spyIsListening.calledOnce);

        assert.isDefined(framesMonitor._cp);

        assert.isTrue(stubOnProcessStartError.calledOnce);
        assert.isTrue(stubOnProcessStartError.calledWithExactly(expectedError));

        stubOnProcessStartError.restore();
    });

    it('must correct set callback for stdout stream error event', () => {
        const expectedError = new Error('stdout stream error');

        const stubOnProcessStdoutStreamError = sinon.stub(framesMonitor, '_onProcessStdoutStreamError');

        framesMonitor.listen();

        childProcess.stdout.emit('error', expectedError);

        assert.isTrue(spyIsListening.calledOnce);

        assert.isDefined(framesMonitor._cp);

        assert.isTrue(stubOnProcessStdoutStreamError.calledOnce);
        assert.isTrue(stubOnProcessStdoutStreamError.calledWithExactly(expectedError));

        stubOnProcessStdoutStreamError.restore();
    });

    it('must correct set callback for stderr stream error event', () => {
        const expectedError = new Error('stderr stream error');

        const stubOnProcessStderrStreamError = sinon.stub(framesMonitor, '_onProcessStderrStreamError');

        framesMonitor.listen();

        childProcess.stderr.emit('error', expectedError);

        assert.isTrue(spyIsListening.calledOnce);

        assert.isDefined(framesMonitor._cp);

        assert.isTrue(stubOnProcessStderrStreamError.calledOnce);
        assert.isTrue(stubOnProcessStderrStreamError.calledWithExactly(expectedError));

        stubOnProcessStderrStreamError.restore();
    });

    it('must correct set callback for stderr data event', () => {
        const expectedData = Buffer.from('some error in stderr');

        const stubOnStderrData = sinon.stub(framesMonitor, '_onStderrData');

        framesMonitor.listen();

        childProcess.stderr.emit('data', expectedData);

        assert.isTrue(spyIsListening.calledOnce);

        assert.isDefined(framesMonitor._cp);

        assert.isTrue(stubOnStderrData.calledOnce);
        assert.isTrue(stubOnStderrData.calledWithExactly(expectedData));

        stubOnStderrData.restore();
    });

    it('must correct set callback for stdout data event', () => {
        const expectedData = Buffer.from('some data in stdout');

        const stubOnStdoutChunk = sinon.stub(framesMonitor, '_onStdoutChunk');

        framesMonitor.listen();

        childProcess.stdout.emit('data', expectedData);

        assert.isTrue(spyIsListening.calledOnce);

        assert.isDefined(framesMonitor._cp);

        assert.isTrue(stubOnStdoutChunk.calledOnce);
        assert.isTrue(stubOnStdoutChunk.calledWithExactly(expectedData));

        stubOnStdoutChunk.restore();
    });
});


<|start_filename|>tests/Unit/processFrames/calculateDisplayAspectRatio.data.js<|end_filename|>
const invalidParams = [
    {
        description: 'undefined width',
        width      : undefined,
        height     : 123,
    },
    {
        description: 'undefined height',
        width      : 123,
        height     : undefined,
    },
    {
        description: 'not-int width',
        width      : 10,
        height     : '123',
    },
    {
        description: 'not-int height',
        width      : '123',
        height     : 10,
    },
    {
        description: 'float width',
        width      : 11.5,
        height     : 10,
    },
    {
        description: 'float height',
        width      : 10,
        height     : 11.5,
    },
    {
        description: 'zero width',
        width      : 0,
        height     : 10,
    },
    {
        description: 'zero height',
        width      : 10,
        height     : 0,
    },
];

const validParams = [
    {
        width     : 1,
        height    : 1,
        aspectRatio: '1:1'
    },
    {
        width     : 10,
        height    : 1,
        aspectRatio: '10:1'
    },
    {
        width     : 1,
        height    : 10,
        aspectRatio: '1:10'
    },
    {
        width     : 13,
        height    : 7,
        aspectRatio: '13:7'
    },
    {
        width     : 7,
        height    : 13,
        aspectRatio: '7:13'
    },
    {
        width     : 10,
        height    : 5,
        aspectRatio: '18:9'
    },
    {
        width     : 5,
        height    : 10,
        aspectRatio: '1:2'
    },
    {
        width     : 640,
        height    : 480,
        aspectRatio: '4:3'
    },
    {
        width     : 854,
        height    : 480,
        aspectRatio: '16:9'
    },
    {
        width     : 1280,
        height    : 720,
        aspectRatio: '16:9'
    },
    {
        width     : 1284,
        height    : 720,
        aspectRatio: '16:9'
    },
    {
        width     : 1275,
        height    : 720,
        aspectRatio: '16:9'
    },
    {
        width     : 1440,
        height    : 720,
        aspectRatio: '18:9'
    },
    {
        width     : 1680,
        height    : 720,
        aspectRatio: '21:9'
    },
    {
        width     : 1000,
        height    : 720,
        aspectRatio: '25:18'
    }
];

module.exports = {invalidParams, validParams};


<|start_filename|>tests/Unit/FramesMonitor/_onStderrData.test.js<|end_filename|>
'use strict';

const {assert} = require('chai');

const {config, url, FramesMonitor} = require('./Helpers');

describe('FramesMonitor::_onStderrData', () => {
    let framesMonitor;

    beforeEach(() => {
        framesMonitor = new FramesMonitor(config, url);
    });

    it('must store last N data from process stderr output', () => {
        const expectedMessage = `got stderr output from a ${config.ffprobePath} process`;

        const STDERR_OBJECTS_LIMIT = 5;
        const overflowOffset       = 2;

        const numberOfItems = STDERR_OBJECTS_LIMIT + overflowOffset;

        const stderr = 'some error with id: ';

        for (let i = 0; i < numberOfItems; i++) {
            framesMonitor._onStderrData(Buffer.from(stderr + i));
        }

        assert.lengthOf(framesMonitor._stderrOutputs, STDERR_OBJECTS_LIMIT);

        framesMonitor._stderrOutputs.forEach(errObject => {
            assert.strictEqual(errObject.message, expectedMessage);
        });

        assert.deepEqual(framesMonitor._stderrOutputs[0].extra, {data: 'some error with id: 2', url: url});
        assert.deepEqual(framesMonitor._stderrOutputs[1].extra, {data: 'some error with id: 3', url: url});
        assert.deepEqual(framesMonitor._stderrOutputs[2].extra, {data: 'some error with id: 4', url: url});
        assert.deepEqual(framesMonitor._stderrOutputs[3].extra, {data: 'some error with id: 5', url: url});
        assert.deepEqual(framesMonitor._stderrOutputs[4].extra, {data: 'some error with id: 6', url: url});
    });
});


<|start_filename|>tests/Unit/processFrames/identifyGops.test.js<|end_filename|>
'use strict';

const {assert}   = require('chai');
const dataDriven = require('data-driven');

const processFrames = require('src/processFrames');

const Errors = require('src/Errors');

const {invalidKeyFramesTypes, invalidKeyFramesValues, testData} = require('./identifyGops.data');

function typeOf(item) {
    return Object.prototype.toString.call(item);
}

describe('processFrames.identifyGops', () => {

    dataDriven(
        invalidKeyFramesTypes.map(item => ({type: typeOf(item), key_frame: item})),
        () => {
            it('must throw an error if frame key_frame field has invalid {type} type', ctx => {
                const invalidFrame = {key_frame: ctx.key_frame};
                const invalidInput = [invalidFrame];

                try {
                    processFrames.identifyGops(invalidInput);
                    assert.isFalse(true, 'should not be here');
                } catch (error) {
                    assert.instanceOf(error, Errors.FrameInvalidData);

                    assert.strictEqual(error.message, `frame's key_frame field has invalid type: ${ctx.type}`);

                    assert.deepEqual(error.extra, {frame: invalidFrame});
                }
            });
        }
    );

    dataDriven(
        invalidKeyFramesValues.map(item => ({key_frame: item})),
        () => {
            it('must throw an error if frame key_frame field has invalid value: {key_frame}. Must be 0 or 1.', ctx => {
                const invalidFrame = {key_frame: ctx.key_frame};
                const invalidInput = [invalidFrame];

                try {
                    processFrames.identifyGops(invalidInput);
                    assert.isFalse(true, 'should not be here');
                } catch (error) {
                    assert.instanceOf(error, Errors.FrameInvalidData);

                    assert.strictEqual(
                        error.message,
                        `frame's key_frame field has invalid value: ${ctx.key_frame}. Must be 1 or 0.`
                    );

                    assert.deepEqual(error.extra, {frame: invalidFrame});
                }
            });
        }
    );

    dataDriven(testData, () => {
        it('{description}', ctx => {
            const expectedResult = ctx.res;

            const gops = processFrames.identifyGops(ctx.input);

            assert.deepEqual(gops, expectedResult);
        });
    });

});


<|start_filename|>tests/Unit/FramesMonitor/_onExit.test.js<|end_filename|>
'use strict';

const sinon    = require('sinon');
const {assert} = require('chai');

const ExitReasons = require('src/ExitReasons');

const {config, url, FramesMonitor, makeChildProcess} = require('./Helpers');

describe('FramesMonitor::_onExit', () => {
    let framesMonitor;
    let childProcess;

    let stubRunShowFramesProcess;
    let spyOnRemoveAllListeners;

    beforeEach(() => {
        framesMonitor = new FramesMonitor(config, url);

        childProcess = makeChildProcess();

        stubRunShowFramesProcess = sinon.stub(framesMonitor, '_runShowFramesProcess').returns(childProcess);

        framesMonitor.listen();

        spyOnRemoveAllListeners = sinon.spy(framesMonitor._cp, 'removeAllListeners');
    });

    afterEach(() => {
        stubRunShowFramesProcess.restore();
        spyOnRemoveAllListeners.restore();
    });

    const data = [
        {
            exitCode      : undefined,
            exitSignal    : 'SIGTERM',
            stderrOutput  : [],
            expectedReason: new ExitReasons.ExternalSignal({signal: 'SIGTERM'})
        },
        {
            exitCode      : 0,
            exitSignal    : undefined,
            stderrOutput  : [],
            expectedReason: new ExitReasons.NormalExit({code: 0})
        },
        {
            exitCode      : 1,
            exitSignal    : null,
            stderrOutput  : [],
            expectedReason: new ExitReasons.AbnormalExit({code: 1, stderrOutput: ''})
        },
        {
            exitCode      : 1,
            exitSignal    : null,
            stderrOutput  : [
                'error1',
                'error2',
                'error3'
            ],
            expectedReason: new ExitReasons.AbnormalExit({code: 1, stderrOutput: 'error1\nerror2\nerror3'})
        }
    ];

    data.forEach(testCase => {
        it('must emit exit event with correct reason type ExternalSignal', done => {
            const {exitCode, exitSignal} = testCase;

            framesMonitor.on('exit', reason => {
                assert.instanceOf(reason, testCase.expectedReason.constructor);
                assert.deepEqual(reason, testCase.expectedReason);

                assert.isTrue(spyOnRemoveAllListeners.calledOnce);
                assert.isTrue(spyOnRemoveAllListeners.calledWithExactly());

                assert.isNull(framesMonitor._cp);

                // done is used in order to check that exactly exit event has been emitted
                done();
            });

            framesMonitor._stderrOutputs = testCase.stderrOutput;

            framesMonitor._onExit(exitCode, exitSignal);
        });
    });
});


<|start_filename|>tests/Unit/FramesMonitor/_frameToJson.test.js<|end_filename|>
'use strict';

const {assert} = require('chai');

const {FramesMonitor} = require('./Helpers');

describe('FramesMonitor::_frameToJson', () => {

    it('must return empty object for empty string input', () => {
        const expectedResult = {};

        const result = FramesMonitor._frameToJson('');

        assert.deepEqual(result, expectedResult);
    });

    it('must return empty object for arbitrary string, with no key value pairs', () => {
        const expectedResult = {};

        const result = FramesMonitor._frameToJson('lorem lorem lorem lorem !!!');

        assert.deepEqual(result, expectedResult);
    });

    it('must return empty object for arbitrary string, with one key value pair', () => {
        const expectedResult = {'lorem key': 'value lorem !!!'};

        const result = FramesMonitor._frameToJson('lorem key=value lorem !!!');

        assert.deepEqual(result, expectedResult);
    });

    it('must return correct object for real input data, with several key value pairs', () => {
        const expectedResult = {
            media_type       : 'video',
            pkt_pts_time     : 9.967900,
            pkt_duration_time: 0.03300,
            pkt_size         : 4253,
            pict_type        : 'P',
            key_frame        : 0
        };

        const result = FramesMonitor._frameToJson(
            '[FRAME]\nmedia_type=video\npkt_pts_time=9.9679000\n' +
            'pkt_duration_time=0.033000\npkt_size=4253\npict_type=P\nkey_frame=0'
        );

        assert.deepEqual(result, expectedResult);
    });

});


<|start_filename|>tests/Unit/FramesMonitor/_onStdoutChunk.test.js<|end_filename|>
'use strict';

const {assert} = require('chai');
const sinon    = require('sinon');

const {config, url, FramesMonitor, makeChildProcess} = require('./Helpers');

const Errors = require('src/Errors');

describe('FramesMonitor::_onStdoutChunk', () => {

    let framesMonitor;

    let spyOnCompleteFrame;
    let stubRunShowFramesProcess;
    let stubHandleProcessingError;
    let spyFrameToJson;
    let spyReduceFramesFromChunks;

    beforeEach(() => {
        framesMonitor = new FramesMonitor(config, url);

        const childProcess = makeChildProcess();

        stubRunShowFramesProcess  = sinon.stub(framesMonitor, '_runShowFramesProcess').returns(childProcess);
        stubHandleProcessingError = sinon.stub(framesMonitor, '_handleProcessingError').resolves();
        spyFrameToJson            = sinon.spy(FramesMonitor, '_frameToJson');
        spyReduceFramesFromChunks = sinon.spy(FramesMonitor, '_reduceFramesFromChunks');
        spyOnCompleteFrame        = sinon.spy();

        framesMonitor.listen();
    });

    afterEach(() => {
        spyOnCompleteFrame.resetHistory();
        stubRunShowFramesProcess.restore();
        stubHandleProcessingError.restore();
        spyFrameToJson.restore();
        spyReduceFramesFromChunks.restore();
    });

    const data = [
        {
            description          : 'emit entire frame with empty chunkRemainder, chunkRemainder should have new, empty value', // eslint-disable-line
            currentChunkRemainder: '',
            input                : '[FRAME]\na=b\n[/FRAME]',
            newChundRemainder    : '',
            output               : {a: 'b'}
        },
        {
            description          : 'emit entire frame with empty chunkRemainder, chunkRemainder should have new, not-empty value', // eslint-disable-line
            currentChunkRemainder: '',
            input                : '[FRAME]\na=b\n[/FRAME]\n[FRAME]\nc=d',
            newChundRemainder    : '\n[FRAME]\nc=d',
            output               : {a: 'b'}
        },
        {
            description          : 'emit accumulated set of frames, chunkRemainder should have new, not-empty value',
            currentChunkRemainder: '[FRAME]\na=b\n',
            input                : 'c=d\n[/FRAME]\n[FRAME]\ne=',
            newChundRemainder    : '\n[FRAME]\ne=',
            output               : {a: 'b', c: 'd'}
        }
    ];

    data.forEach((test) => {
        it(`${test.description}`, done => {
            framesMonitor._chunkRemainder = test.currentChunkRemainder;

            const input = Buffer.from(test.input);

            framesMonitor.on('frame', spyOnCompleteFrame);

            framesMonitor._onStdoutChunk(input)
                .then(() => {
                    setImmediate(() => {
                        assert.isTrue(spyOnCompleteFrame.calledOnce);

                        assert.isTrue(stubHandleProcessingError.notCalled);
                        assert.isTrue(spyFrameToJson.calledOnce);
                        assert.isTrue(spyReduceFramesFromChunks.calledOnce);

                        done();
                    });
                });
        });
    });

    it('must not emit empty frame', done => {
        const input = Buffer.from('');

        framesMonitor.on('frame', spyOnCompleteFrame);

        framesMonitor._onStdoutChunk(input)
            .then(() => {
                setImmediate(() => {
                    assert(spyOnCompleteFrame.notCalled);

                    assert.isTrue(stubHandleProcessingError.notCalled);
                    assert.isTrue(spyFrameToJson.notCalled);
                    assert.isTrue(spyReduceFramesFromChunks.calledOnce);

                    done();
                });
            });
    });

    it('must not emit uncomplete frame', done => {
        const input = Buffer.from('[FRAME]\na=b');

        framesMonitor.on('frame', spyOnCompleteFrame);

        framesMonitor._onStdoutChunk(input)
            .then(() => {
                setImmediate(() => {
                    assert(spyOnCompleteFrame.notCalled);

                    assert.isTrue(stubHandleProcessingError.notCalled);
                    assert.isTrue(spyFrameToJson.notCalled);
                    assert.isTrue(spyReduceFramesFromChunks.calledOnce);

                    done();
                });
            });
    });

    it('must emit complete raw frames in json format, which has been accumulated by several chunks', done => {
        const tests = [
            Buffer.from('[FRAME]\na=b\nc=d\n'),
            Buffer.from('a2=b2\nc2=d2\n'),
            Buffer.from('e2=f2\n[/FRAME]\n'),
            Buffer.from('[FRAME]\na=b\n[/FRAME]\n'),
            Buffer.from('[FRAME]\na=b\nc=d\n')
        ];

        const expectedResult1 = {
            a : 'b',
            c : 'd',
            a2: 'b2',
            c2: 'd2',
            e2: 'f2'
        };

        const expectedResult2 = {a: 'b'};

        framesMonitor.on('frame', spyOnCompleteFrame);

        // we cannot use done with async func identifier, so will use then method
        Promise.all(
            tests.map(async test => {
                await framesMonitor._onStdoutChunk(test);
            })
        )
            .then(() => {
                // _onStdoutChunk uses setImmediate under the hood, so we use it here too
                setImmediate(() => {
                    assert(spyOnCompleteFrame.calledTwice);

                    assert.isTrue(spyOnCompleteFrame.firstCall.calledWithExactly(expectedResult1));
                    assert.isTrue(spyOnCompleteFrame.secondCall.calledWithExactly(expectedResult2));

                    assert.isTrue(stubHandleProcessingError.notCalled);
                    assert.isTrue(spyFrameToJson.calledTwice);
                    assert.strictEqual(spyReduceFramesFromChunks.callCount, tests.length);

                    done();
                });
            });
    });

    it('must emit error, invalid data input (too big frame, probably infinite)', async () => {
        const smallInput = '\na=b\n'.repeat(10);
        const largeInput = '\na=b\n'.repeat(config.bufferMaxLengthInBytes - 10);

        await framesMonitor._onStdoutChunk(smallInput);
        await framesMonitor._onStdoutChunk(largeInput);

        assert.isTrue(stubHandleProcessingError.calledOnce);

        const error = stubHandleProcessingError.getCall(0).args[0];

        assert.instanceOf(error, Errors.InvalidFrameError);

        assert.strictEqual(
            error.message,
            'Too long (probably infinite) frame.' +
            `The frame length is ${smallInput.length + largeInput.length}.` +
            `The max frame length must be ${config.bufferMaxLengthInBytes}`
        );

        assert.isTrue(spyFrameToJson.notCalled);
        assert.isTrue(spyReduceFramesFromChunks.calledOnce); // during the first call on smallInput
    });

    it('must throw an exception, invalid data input (unclosed frame)', async () => {
        const expectedErrorType    = Errors.InvalidFrameError;
        const expectedErrorMessage = 'Can not process frame with invalid structure.';

        const chunkRemainder = '[FRAME]\na=b\n';
        const newChunk       = '[FRAME]\na=b\nc=d\n[/FRAME]';

        framesMonitor._chunkRemainder = chunkRemainder;

        await framesMonitor._onStdoutChunk(newChunk);

        assert.isTrue(stubHandleProcessingError.calledOnce);

        const error = stubHandleProcessingError.getCall(0).args[0];

        assert.instanceOf(error, expectedErrorType);

        assert.strictEqual(error.message, expectedErrorMessage);

        assert.deepEqual(error.extra, {
            data : chunkRemainder + newChunk,
            frame: '[FRAME]\na=b\n[FRAME]\na=b\nc=d\n'
        });

        assert.isTrue(spyFrameToJson.notCalled);
        assert.isTrue(spyReduceFramesFromChunks.calledOnce);
    });

    it('must throw an exception, invalid data input (end block without starting one)', async () => {
        const expectedErrorType    = Errors.InvalidFrameError;
        const expectedErrorMessage = 'Can not process frame with invalid structure.';

        const newChunk = 'a=b\nc=d\n[/FRAME]';

        await framesMonitor._onStdoutChunk(newChunk);

        assert.isTrue(stubHandleProcessingError.calledOnce);

        const error = stubHandleProcessingError.getCall(0).args[0];

        assert.instanceOf(error, expectedErrorType);

        assert.strictEqual(error.message, expectedErrorMessage);

        assert.deepEqual(error.extra, {
            data : newChunk,
            frame: 'a=b\nc=d\n'
        });

        assert.isTrue(spyFrameToJson.notCalled);
        assert.isTrue(spyReduceFramesFromChunks.calledOnce);
    });
});


<|start_filename|>tests/Unit/processFrames/hasAudioFrames.test.js<|end_filename|>
'use strict';

const {assert} = require('chai');

const processFrames = require('src/processFrames');

describe('processFrames.hasAudioFrames', () => {

    it('must detect the audio frames existence', () => {
        const expectedResult = true;

        const frames = [
            {media_type: 'video', width: 1},
            {media_type: 'audio'},
            {media_type: 'data'},
            {media_type: 'video', width: 2}
        ];

        const hasAudioFrames = processFrames.hasAudioFrames(frames);

        assert.deepEqual(hasAudioFrames, expectedResult);
    });

    it('must detect the audio frames absence', () => {
        const expectedResult = false;

        const frames = [
            {media_type: 'video', width: 1},
            {media_type: 'data'},
            {media_type: 'video', width: 2}
        ];

        const hasAudioFrames = processFrames.hasAudioFrames(frames);

        assert.deepEqual(hasAudioFrames, expectedResult);
    });

});


<|start_filename|>tests/Unit/processFrames/networkStats.test.js<|end_filename|>
'use strict';

const _          = require('lodash');
const {assert}   = require('chai');
const dataDriven = require('data-driven');

const processFrames = require('src/processFrames');

const {invalidFramesTypes, invalidDurationInSecTypes, testData} = require('./networkStats.data');

const PRECISION = 0.00001;

function typeOf(item) {
    return Object.prototype.toString.call(item);
}

describe('processFrames.networkStats', () => {

    dataDriven(
        invalidFramesTypes.map(item => ({type: typeOf(item), item: item})),
        () => {
            it('must throw an exception for invalid input {type} type for frames', ctx => {
                assert.throws(() => {
                    processFrames.networkStats(ctx.item);
                }, TypeError, 'Method accepts only an array of frames');
            });
        }
    );

    dataDriven(
        invalidDurationInSecTypes.map(item => ({type: typeOf(item), item: item})),
        () => {
            it('must throw an exception for invalid input {type} type for durationInMsec', ctx => {
                assert.throws(() => {
                    processFrames.networkStats([], ctx.item);
                }, TypeError, 'Method accepts only a positive integer as duration');
            });
        }
    );

    dataDriven(testData, () => {
        it('{description}', ctx => {
            const expectedResult = ctx.expected;

            const result = processFrames.networkStats(ctx.frames, ctx.durationInMsec);

            assert.isTrue(_.inRange(
                result.videoFrameRate,
                expectedResult.videoFrameRate - PRECISION,
                expectedResult.videoFrameRate + PRECISION,
            ));

            assert.isTrue(_.inRange(
                result.audioFrameRate,
                expectedResult.audioFrameRate - PRECISION,
                expectedResult.audioFrameRate + PRECISION,
            ));

            assert.isTrue(_.inRange(
                result.videoBitrate,
                expectedResult.videoBitrate - PRECISION,
                expectedResult.videoBitrate + PRECISION
            ));

            assert.isTrue(_.inRange(
                result.audioBitrate,
                expectedResult.audioBitrate - PRECISION,
                expectedResult.audioBitrate + PRECISION
            ));
        });
    });
});


<|start_filename|>src/processFrames.js<|end_filename|>
'use strict';

const _ = require('lodash');

const Errors = require('./Errors');

const MSECS_IN_SEC = 1000;

const AR_CALCULATION_PRECISION = 0.01;

const SQUARE_AR_COEFFICIENT = 1;
const SQUARE_AR = '1:1';

const TRADITIONAL_TV_AR_COEFFICIENT = 1.333;
const TRADITIONAL_TV_AR = '4:3';

const HD_VIDEO_AR_COEFFICIENT = 1.777;
const HD_VIDEO_AR = '16:9';

const UNIVISIUM_AR_COEFFICIENT = 2;
const UNIVISIUM_AR = '18:9';

const WIDESCREEN_AR_COEFFICIENT = 2.33;
const WIDESCREEN_AR = '21:9';

function encoderStats(frames) {
    if (!Array.isArray(frames)) {
        throw new TypeError('Method accepts only an array of frames');
    }

    const videoFrames            = filterVideoFrames(frames);
    const {gops, remainedFrames} = identifyGops(videoFrames);

    if (_.isEmpty(gops)) {
        throw new Errors.GopNotFoundError('Can not find any gop for these frames', {frames});
    }

    let areAllGopsIdentical = true;
    const hasAudioStream = hasAudioFrames(frames);
    const baseGopSize = gops[0].frames.length;
    const bitrates = [];
    const fpsList = [];
    const gopDurations = [];

    gops.forEach(gop => {
        areAllGopsIdentical     = areAllGopsIdentical && baseGopSize === gop.frames.length;
        const calculatedPktSize = calculatePktSize(gop.frames);
        const gopDuration       = gopDurationInSec(gop);

        const gopBitrate = toKbs(calculatedPktSize / gopDuration);
        bitrates.push(gopBitrate);

        const gopFps = gop.frames.length / gopDuration;
        fpsList.push(gopFps);

        gopDurations.push(gopDuration);
    });

    const bitrate = {
        mean: _.mean(bitrates),
        min : Math.min(...bitrates),
        max : Math.max(...bitrates)
    };

    const fps = {
        mean: _.mean(fpsList),
        min : Math.min(...fpsList),
        max : Math.max(...fpsList)
    };

    const gopDuration = {
        mean: _.mean(gopDurations),
        min: Math.min(...gopDurations),
        max: Math.max(...gopDurations)
    };

    const width = gops[0].frames[0].width;
    const height = gops[0].frames[0].height;
    const displayAspectRatio = calculateDisplayAspectRatio(width, height);

    return {
        payload       : {
            areAllGopsIdentical,
            bitrate,
            fps,
            gopDuration,
            displayAspectRatio,
            width,
            height,
            hasAudioStream
        },
        remainedFrames: remainedFrames
    };
}

function networkStats(frames, durationInMsec) {
    if (!Array.isArray(frames)) {
        throw new TypeError('Method accepts only an array of frames');
    }

    if (!_.isInteger(durationInMsec) || durationInMsec <= 0) {
        throw new TypeError('Method accepts only a positive integer as duration');
    }

    const videoFrames = filterVideoFrames(frames);
    const audioFrames = filterAudioFrames(frames);

    const durationInSec = durationInMsec / MSECS_IN_SEC;

    return {
        videoFrameRate:  videoFrames.length / durationInSec,
        audioFrameRate:  audioFrames.length / durationInSec,
        videoBitrate: toKbs(calculatePktSize(videoFrames) / durationInSec),
        audioBitrate: toKbs(calculatePktSize(audioFrames) / durationInSec),
    };
}

function identifyGops(frames) {
    const GOP_TEMPLATE = {
        frames: []
    };

    const setOfGops = [];
    let newGop      = _.cloneDeep(GOP_TEMPLATE);

    for (let i = 0; i < frames.length; i++) {
        const currentFrame = frames[i];

        if (!_.isNumber(currentFrame.key_frame)) {
            throw new Errors.FrameInvalidData(
                `frame's key_frame field has invalid type: ${Object.prototype.toString.call(currentFrame.key_frame)}`,
                {frame: currentFrame}
            );
        }

        if (currentFrame.key_frame === 1) {
            if ('startTime' in newGop) {
                newGop.endTime = currentFrame.pkt_pts_time;
                setOfGops.push(newGop);
                newGop = _.cloneDeep(GOP_TEMPLATE);
                i -= 1;
            } else {
                newGop.frames.push(_.cloneDeep(currentFrame));

                newGop.startTime = currentFrame.pkt_pts_time;
            }
        } else if (currentFrame.key_frame === 0) {
            if (newGop.frames.length > 0) {
                newGop.frames.push(_.cloneDeep(frames[i]));
            }
        } else {
            throw new Errors.FrameInvalidData(
                `frame's key_frame field has invalid value: ${currentFrame.key_frame}. Must be 1 or 0.`,
                {frame: currentFrame}
            );
        }
    }

    // remainedFrames is a set of frames for which we didn't find gop
    // for example for this array of frames [1 0 0 0 1 0 0] the remainedFrames should be last three frames [1 0 0]
    // this is done in order not to lost part of the next gop and as a direct consequence - entire gop
    return {
        gops          : setOfGops,
        remainedFrames: newGop.frames
    };
}

function calculateBitrate(gops) {
    let bitrates = [];

    gops.forEach(gop => {
        const calculatedPktSize = calculatePktSize(gop.frames);
        const durationInSec     = gopDurationInSec(gop);

        const gopBitrate = toKbs(calculatedPktSize / durationInSec);

        bitrates.push(gopBitrate);
    });

    return {
        mean: _.mean(bitrates),
        min : Math.min.apply(null, bitrates),
        max : Math.max.apply(null, bitrates)
    };
}

function calculatePktSize(frames) {
    const accumulatedPktSize = frames.reduce((accumulator, frame) => {
        if (!_.isNumber(frame.pkt_size)) {
            throw new Errors.FrameInvalidData(
                `frame's pkt_size field has invalid type ${Object.prototype.toString.call(frame.pkt_size)}`,
                {frame}
            );
        }

        return accumulator + frame.pkt_size;
    }, 0);

    return accumulatedPktSize;
}

function gopDurationInSec(gop) {
    if (!_.isNumber(gop.startTime)) {
        throw new Errors.GopInvalidData(
            `gops's start time has invalid type ${Object.prototype.toString.call(gop.startTime)}`,
            {gop}
        );
    }

    if (!_.isNumber(gop.endTime)) {
        throw new Errors.GopInvalidData(
            `gops's end time has invalid type ${Object.prototype.toString.call(gop.endTime)}`,
            {gop}
        );
    }

    // start time may be 0
    if (gop.startTime < 0) {
        throw new Errors.GopInvalidData(
            `gop's start time has invalid value ${gop.startTime}`,
            {gop}
        );
    }

    // end time must be positive
    if (gop.endTime <= 0) {
        throw new Errors.GopInvalidData(
            `gop's end time has invalid value ${gop.endTime}`,
            {gop}
        );
    }

    const diff = gop.endTime - gop.startTime;

    if (diff <= 0) {
        throw new Errors.GopInvalidData(
            `invalid difference between gop start and end time: ${diff}`,
            {gop}
        );
    }

    return diff;
}

function calculateFps(gops) {
    let fps = [];

    gops.forEach(gop => {
        const durationInSec    = gopDurationInSec(gop);
        const gopFps           = gop.frames.length / durationInSec;

        fps.push(gopFps);
    });

    return {
        mean: _.mean(fps),
        min : Math.min.apply(null, fps),
        max : Math.max.apply(null, fps)
    };
}

function calculateGopDuration(gops) {
    const gopsDurations = [];

    gops.forEach(gop => {
        const durationInSec = gopDurationInSec(gop);

        gopsDurations.push(durationInSec);
    });

    return {
        mean: _.mean(gopsDurations),
        min: Math.min(...gopsDurations),
        max: Math.max(...gopsDurations)
    };
}

function calculateDisplayAspectRatio(width, height) {
    if (!_.isInteger(width) || width <= 0) {
        throw new TypeError('"width" must be a positive integer');
    }

    if (!_.isInteger(height) || height <= 0) {
        throw new TypeError('"height" must be a positive integer');
    }

    const arCoefficient = width / height;

    if (Math.abs(arCoefficient - SQUARE_AR_COEFFICIENT) <= AR_CALCULATION_PRECISION) {
        return SQUARE_AR;
    }

    if (Math.abs(arCoefficient - TRADITIONAL_TV_AR_COEFFICIENT) <= AR_CALCULATION_PRECISION) {
        return TRADITIONAL_TV_AR;
    }

    if (Math.abs(arCoefficient - HD_VIDEO_AR_COEFFICIENT) <= AR_CALCULATION_PRECISION) {
        return HD_VIDEO_AR;
    }

    if (Math.abs(arCoefficient - UNIVISIUM_AR_COEFFICIENT) <= AR_CALCULATION_PRECISION) {
        return UNIVISIUM_AR;
    }

    if (Math.abs(arCoefficient - WIDESCREEN_AR_COEFFICIENT) <= AR_CALCULATION_PRECISION) {
        return WIDESCREEN_AR;
    }

    const gcd = findGcd(width, height);

    return `${width / gcd}:${height / gcd}`;
}

function areAllGopsIdentical(gops) {
    return gops.every(gop => _.isEqual(gops[0].frames.length, gop.frames.length));
}

function filterVideoFrames(frames) {
    return frames.filter(frame => frame.media_type === 'video');
}

function filterAudioFrames(frames) {
    return frames.filter(frame => frame.media_type === 'audio');
}

function hasAudioFrames(frames) {
    return frames.some(frame => frame.media_type === 'audio');
}

function toKbs(val) {
    return val * 8 / 1024;
}

function findGcd(a, b) {
    if (a === 0 && b === 0) {
        return 0;
    }

    if (b === 0) {
        return a;
    }

    return findGcd(b, a % b);
}

module.exports = {
    encoderStats,
    networkStats,
    identifyGops,
    calculateBitrate,
    calculateFps,
    calculateGopDuration,
    filterVideoFrames,
    hasAudioFrames,
    gopDurationInSec,
    toKbs,
    calculatePktSize,
    areAllGopsIdentical,
    findGcd,
    calculateDisplayAspectRatio
};


<|start_filename|>tests/Unit/StreamsInfo/_adjustAspectRatio.data.js<|end_filename|>
const invalidParams = [
    {
        description: 'width param is invalid',
        data       : [{sample_aspect_ratio: '0:1', display_aspect_ratio: '10:1', width: 'N/A', height: 10}],
        errorMsg   : 'width field has invalid value.'
    },
    {
        description: 'height param is invalid',
        data       : [{sample_aspect_ratio: '0:1', display_aspect_ratio: '10:1', width: 10, height: 'N/A'}],
        errorMsg   : 'height field has invalid value.'
    },
    {
        description: 'height param is invalid',
        data       : [{sample_aspect_ratio: '10:1', display_aspect_ratio: '0:1', width: 'N/A', height: 10}],
        errorMsg   : 'height field has invalid value.'
    },
    {
        description: 'height param is invalid',
        data       : [{sample_aspect_ratio: '10:1', display_aspect_ratio: '0:1', width: 10, height: 'N/A'}],
        errorMsg   : 'height field has invalid value.'
    },
];

const validParams = [
    {
        description: 'sample_aspect_ratio param is invalid',
        data       : [],
        res        : []
    },
    {
        description: 'sample_aspect_ratio param is invalid',
        data       : [{sample_aspect_ratio: '0:1', display_aspect_ratio: '200:100', width: 10, height: 4}],
        res        : [{sample_aspect_ratio: '1:1', display_aspect_ratio: '5:2', width: 10, height: 4}]
    },
    {
        description: 'display_aspect_ratio param is invalid',
        data       : [{sample_aspect_ratio: '200:100', display_aspect_ratio: '0:1', width: 20, height: 10}],
        res        : [{sample_aspect_ratio: '1:1', display_aspect_ratio: '18:9', width: 20, height: 10}]
    },
    {
        description: 'sample_aspect_ratio param is invalid',
        data       : [{sample_aspect_ratio: 'N/A', display_aspect_ratio: '200:100', width: 10, height: 4}],
        res        : [{sample_aspect_ratio: '1:1', display_aspect_ratio: '5:2', width: 10, height: 4}]
    },
    {
        description: 'display_aspect_ratio param is invalid',
        data       : [{sample_aspect_ratio: '200:100', display_aspect_ratio: 'N/A', width: 20, height: 10}],
        res        : [{sample_aspect_ratio: '1:1', display_aspect_ratio: '18:9', width: 20, height: 10}]
    }
];

module.exports = {
    invalidParams,
    validParams
};


<|start_filename|>tests/Unit/processFrames/calculateGopDuration.test.js<|end_filename|>
'use strict';

const {assert} = require('chai');

const processFrames = require('src/processFrames');

describe('processFrames.calculateGopDuration', () => {

    it('must correct calculate min, max and average gopDuration for gops', () => {
        const expectedGopDuration = {
            min : 1,
            max : 6,
            mean: 3.5
        };

        const gops = [
            {
                startTime: 1,
                endTime  : 2
            },
            {
                startTime: 3,
                endTime  : 9
            }
        ];

        const gopDuration = processFrames.calculateGopDuration(gops);

        assert.deepEqual(gopDuration, expectedGopDuration);
    });

});


<|start_filename|>src/Errors/index.js<|end_filename|>
const ExtendableError = require('./ExtendableError');

class AlreadyListeningError extends ExtendableError {}
class FramesMonitorError extends ExtendableError {}
class StreamsInfoError extends ExtendableError {}
class ConfigError extends ExtendableError {}
class ProcessStreamError extends ExtendableError {}
class InvalidFrameError extends ExtendableError {}
class ExecutablePathError extends ExtendableError {}
class FrameInvalidData extends ExtendableError {}
class GopInvalidData extends ExtendableError {}
class GopNotFoundError extends ExtendableError {}
class ProcessStartError extends ExtendableError {}
class ProcessExitError extends ExtendableError {}

module.exports = {
    AlreadyListeningError,
    ConfigError,
    ProcessStreamError,
    FramesMonitorError,
    StreamsInfoError,
    InvalidFrameError,
    ExecutablePathError,
    FrameInvalidData,
    GopInvalidData,
    GopNotFoundError,
    ProcessStartError,
    ProcessExitError
};


<|start_filename|>tests/Unit/processFrames/findGCD.test.js<|end_filename|>
'use strict';

const {assert}   = require('chai');
const dataDriven = require('data-driven');

const processFrames = require('src/processFrames');

describe('findGcd', () => {
    const data = [
        {a: 0, b: 0, answer: 0},
        {a: 1, b: 0, answer: 1},
        {a: 0, b: 1, answer: 1},
        {a: 1, b: 1, answer: 1},
        {a: 13, b: 7, answer: 1},
        {a: 7, b: 13, answer: 1},
        {a: 56, b: 98, answer: 14},
        {a: 98, b: 56, answer: 14},
        {a: 1280, b: 720, answer: 80},
    ];

    dataDriven(data, function () {
        it('for ({a}, {b})', function (ctx) {
            const expectation = ctx.answer;

            const result = processFrames.findGcd(ctx.a, ctx.b);

            assert.strictEqual(result, expectation);
        });
    });

});


<|start_filename|>tests/Unit/processFrames/encoderStats.test.js<|end_filename|>
'use strict';

const {assert}   = require('chai');
const dataDriven = require('data-driven');

const processFrames = require('src/processFrames');

const Errors = require('src/Errors');

function typeOf(item) {
    return Object.prototype.toString.call(item);
}

describe('processFrames.encoderStats', () => {

    const invalidFramesTypes = [
        undefined,
        null,
        false,
        1,
        '1',
        {},
        Symbol(),
        () => {},
        Buffer.alloc(0)
    ];

    dataDriven(
        invalidFramesTypes.map(item => ({type: typeOf(item), item: item})),
        () => {
            it('must throw an exception for invalid input {type} type', ctx => {
                assert.throws(() => {
                    processFrames.encoderStats(ctx.item);
                }, TypeError, 'Method accepts only an array of frames');
            });
        }
    );

    it('must throw an exception cuz method cannot find gop', () => {
        const frames = [
            {pkt_size: 3, pkt_pts_time: 15, media_type: 'video', key_frame: 1},
            {pkt_size: 2, pkt_pts_time: 2, media_type: 'audio', key_frame: 1},
            {pkt_size: 5, pkt_pts_time: 17, media_type: 'video', key_frame: 0},
            {pkt_size: 7, pkt_pts_time: 19, media_type: 'video', key_frame: 0},
            {pkt_size: 4, pkt_pts_time: 4, media_type: 'audio', key_frame: 1},
        ];

        try {
            processFrames.encoderStats(frames);
            assert.isFalse(true, 'should not be here');
        } catch (error) {
            assert.instanceOf(error, Errors.GopNotFoundError);

            assert.strictEqual(error.message, 'Can not find any gop for these frames');

            assert.deepEqual(error.extra, {frames});
        }
    });

    it('must return correct info just fine', () => {
        const frames1 = [
            {width: 640, height: 480, pkt_size: 1, pkt_pts_time: 11, media_type: 'video', key_frame: 1},
            {width: 640, height: 480, pkt_size: 3, pkt_pts_time: 13, media_type: 'video', key_frame: 0},
            {width: 640, height: 480, pkt_size: 5, pkt_pts_time: 15, media_type: 'video', key_frame: 1},
            {width: 640, height: 480, pkt_size: 7, pkt_pts_time: 17, media_type: 'video', key_frame: 0},
            {width: 640, height: 480, pkt_size: 9, pkt_pts_time: 19, media_type: 'video', key_frame: 1}
        ];

        const frames2 = [
            {width: 854, height: 480, pkt_size: 1, pkt_pts_time: 1, media_type: 'audio', key_frame: 1},
            {width: 854, height: 480, pkt_size: 1, pkt_pts_time: 11, media_type: 'video', key_frame: 0},
            {width: 854, height: 480, pkt_size: 2, pkt_pts_time: 13, media_type: 'video', key_frame: 0},
            {width: 854, height: 480, pkt_size: 3, pkt_pts_time: 15, media_type: 'video', key_frame: 1},
            {width: 854, height: 480, pkt_size: 3, pkt_pts_time: 15, media_type: 'audio', key_frame: 1},
            {width: 854, height: 480, pkt_size: 4, pkt_pts_time: 17, media_type: 'video', key_frame: 0},
            {width: 854, height: 480, pkt_size: 5, pkt_pts_time: 19, media_type: 'video', key_frame: 0},
            {width: 854, height: 480, pkt_size: 6, pkt_pts_time: 21, media_type: 'video', key_frame: 1},
            {width: 854, height: 480, pkt_size: 7, pkt_pts_time: 23, media_type: 'video', key_frame: 0},
            {width: 854, height: 480, pkt_size: 3, pkt_pts_time: 15, media_type: 'audio', key_frame: 1},
            {width: 854, height: 480, pkt_size: 8, pkt_pts_time: 25, media_type: 'video', key_frame: 0},
            {width: 854, height: 480, pkt_size: 9, pkt_pts_time: 27, media_type: 'video', key_frame: 1},
            {width: 854, height: 480, pkt_size: 10, pkt_pts_time: 29, media_type: 'video', key_frame: 0}
        ];

        const expectedBitrate1 = {
            min : processFrames.toKbs((1 + 3) / (15 - 11)),
            max : processFrames.toKbs((5 + 7) / (19 - 15)),
            mean: processFrames.toKbs(((1 + 3) / (15 - 11) + (5 + 7) / (19 - 15)) / 2)
        };

        const expectedRemainedFrames1 = [
            {pkt_size: 9, pkt_pts_time: 19, media_type: 'video', key_frame: 1, width: 640, height: 480}
        ];

        const expectedBitrate2 = {
            min : processFrames.toKbs((3 + 4 + 5) / (21 - 15)),
            max : processFrames.toKbs((6 + 7 + 8) / (27 - 21)),
            mean: processFrames.toKbs(((3 + 4 + 5) / (21 - 15) + (6 + 7 + 8) / (27 - 21)) / 2)
        };

        const expectedRemainedFrames2 = [
            {pkt_size: 9, pkt_pts_time: 27, media_type: 'video', key_frame: 1, width: 854, height: 480},
            {pkt_size: 10, pkt_pts_time: 29, media_type: 'video', key_frame: 0, width: 854, height: 480}
        ];

        const expectedFps1 = {min: 0.5, max: 0.5, mean: 0.5};
        const expectedFps2 = {min: 0.5, max: 0.5, mean: 0.5};

        const expectedGopDuration1 = {
            min: 15 - 11,
            max: 19 - 15,
            mean: (15 - 11 + 19 - 15) / 2
        };

        const expectedGopDuration2 = {
            min: 21 - 15,
            max: 27 - 21,
            mean: (21 - 15 + 27 - 21) / 2
        };

        const expectedAspectRatio1 = '4:3';
        const expectedAspectRatio2 = '16:9';
        const expectedWidth1 = 640;
        const expectedHeight1 = 480;
        const expectedWidth2 = 854;
        const expectedHeight2 = 480;
        const expectAudio1 = false;
        const expectAudio2 = true;

        let res1 = processFrames.encoderStats(frames1);

        assert.deepEqual(res1.payload, {
            areAllGopsIdentical: true,
            bitrate            : expectedBitrate1,
            fps                : expectedFps1,
            gopDuration        : expectedGopDuration1,
            displayAspectRatio : expectedAspectRatio1,
            width              : expectedWidth1,
            height             : expectedHeight1,
            hasAudioStream     : expectAudio1
        });

        assert.deepEqual(res1.remainedFrames, expectedRemainedFrames1);

        let res2 = processFrames.encoderStats(frames2);

        assert.deepEqual(res2.payload, {
            areAllGopsIdentical: true,
            bitrate            : expectedBitrate2,
            fps                : expectedFps2,
            gopDuration        : expectedGopDuration2,
            displayAspectRatio : expectedAspectRatio2,
            width              : expectedWidth2,
            height             : expectedHeight2,
            hasAudioStream     : expectAudio2
        });

        assert.deepEqual(res2.remainedFrames, expectedRemainedFrames2);
    });

    it('must detect that GOPs is not identical ', () => {
        const frames = [
            {width: 640, height: 480, pkt_size: 1, pkt_pts_time: 11, media_type: 'video', key_frame: 1},
            {width: 640, height: 480, pkt_size: 3, pkt_pts_time: 13, media_type: 'video', key_frame: 0},
            {width: 640, height: 480, pkt_size: 5, pkt_pts_time: 15, media_type: 'video', key_frame: 1},
            {width: 640, height: 480, pkt_size: 6, pkt_pts_time: 17, media_type: 'video', key_frame: 0},
            {width: 640, height: 480, pkt_size: 9, pkt_pts_time: 19, media_type: 'video', key_frame: 0},
            {width: 640, height: 480, pkt_size: 11, pkt_pts_time: 21, media_type: 'video', key_frame: 1}
        ];

        const expectedBitrate = {
            min: processFrames.toKbs((1 + 3) / (15 - 11)),
            max: processFrames.toKbs((5 + 6 + 9) / (21 - 15)),
            mean: processFrames.toKbs(((1 + 3) / (15 - 11) + (5 + 6 + 9) / (21 - 15)) / 2)
        };

        const expectedRemainedFrames = [
            {pkt_size: 11, pkt_pts_time: 21, media_type: 'video', key_frame: 1, width: 640, height: 480}
        ];

        const expectedFps = {min: 0.5, max: 0.5, mean: 0.5};

        const expectedGopDuration = {
            min: 15 - 11,
            max: 21 - 15,
            mean: (15 - 11 + 21 - 15) / 2
        };

        const expectedAspectRatio = '4:3';
        const expectedWidth = 640;
        const expectedHeight = 480;
        const expectAudio = false;
        const expectAreAllGopsIdentical = false;

        let res = processFrames.encoderStats(frames);

        assert.deepEqual(res.payload, {
            areAllGopsIdentical: expectAreAllGopsIdentical,
            bitrate: expectedBitrate,
            fps: expectedFps,
            gopDuration: expectedGopDuration,
            displayAspectRatio: expectedAspectRatio,
            width: expectedWidth,
            height: expectedHeight,
            hasAudioStream: expectAudio
        });

        assert.deepEqual(res.remainedFrames, expectedRemainedFrames);
    });

});
