diff --git a/README.md b/README.md index 5eb3f44..c1a9136 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ const {StreamsInfo} = require('video-quality-tools'); const streamsInfoOptions = { ffprobePath: '/usr/local/bin/ffprobe', - timeoutInSec: 5 + timeoutInMs: 2000 }; const streamsInfo = new StreamsInfo(streamsInfoOptions, 'rtmp://host:port/appInstance/name'); ``` @@ -179,7 +179,7 @@ const {FramesMonitor} = require('video-quality-tools'); const framesMonitorOptions = { ffprobePath: '/usr/local/bin/ffprobe', - timeoutInSec: 5, + timeoutInMs: 2000, bufferMaxLengthInBytes: 100000, errorLevel: 'error', exitProcessGuardTimeoutInMs: 1000 @@ -197,7 +197,8 @@ Constructor throws: The first argument of `FramesMonitor` must be an `options` object. All `options` object's fields are mandatory: * `ffprobePath` - string, path to ffprobe executable; -* `timeoutInSec` - integer, greater than 0, specifies the waiting time of a live stream’s first frame; +* `timeoutInMs` - integer, greater than 0, specifies the maximum time to wait for (network) read/write operations +to complete; * `bufferMaxLengthInBytes` - integer, greater than 0, specifies the buffer length for ffprobe frames. This setting prevents from hanging and receiving incorrect data from the stream, usually 1-2 KB is enough; * `errorLevel` - specifies log level for debugging purposes, must be equal to ffprobe's diff --git a/examples/realtimeStats.js b/examples/realtimeStats.js index c511d84..43c5ae6 100644 --- a/examples/realtimeStats.js +++ b/examples/realtimeStats.js @@ -8,7 +8,7 @@ const STREAM_URI = 'rtmp://host:port/path'; const framesMonitorOptions = { ffprobePath: '/usr/local/bin/ffprobe', - timeoutInSec: 5, + timeoutInMs: 2000, bufferMaxLengthInBytes: 100000, errorLevel: 'error', exitProcessGuardTimeoutInMs: 1000 diff --git a/src/FramesMonitor.js b/src/FramesMonitor.js index 6792ad4..addfb9e 100644 --- a/src/FramesMonitor.js +++ b/src/FramesMonitor.js @@ -42,7 +42,7 @@ class FramesMonitor extends EventEmitter { const { ffprobePath, - timeoutInSec, + timeoutInMs, bufferMaxLengthInBytes, errorLevel, exitProcessGuardTimeoutInMs @@ -52,7 +52,7 @@ class FramesMonitor extends EventEmitter { throw new Errors.ConfigError('You should provide a correct path to ffprobe, bastard.'); } - if (!_.isSafeInteger(timeoutInSec) || timeoutInSec <= 0) { + if (!_.isSafeInteger(timeoutInMs) || timeoutInMs <= 0) { throw new Errors.ConfigError('You should provide a correct timeout, bastard.'); } @@ -72,8 +72,15 @@ class FramesMonitor extends EventEmitter { FramesMonitor._assertExecutable(ffprobePath); - this._config = _.cloneDeep(config); - this._url = url; + this._config = { + ffprobePath, + bufferMaxLengthInBytes, + errorLevel, + exitProcessGuardTimeoutInMs, + timeout: timeoutInMs * 1000 + }; + + this._url = url; this._cp = null; this._chunkRemainder = ''; @@ -260,10 +267,10 @@ class FramesMonitor extends EventEmitter { } _runShowFramesProcess() { - const {ffprobePath, timeoutInSec, errorLevel} = this._config; + const {ffprobePath, timeout, errorLevel} = this._config; try { - const exec = spawn( + return spawn( ffprobePath, [ '-hide_banner', @@ -271,15 +278,15 @@ class FramesMonitor extends EventEmitter { errorLevel, '-fflags', 'nobuffer', + '-rw_timeout', + timeout, '-show_frames', '-show_entries', 'frame=pkt_size,pkt_pts_time,media_type,pict_type,key_frame,width,height', '-i', - `${this._url} timeout=${timeoutInSec}` + this._url ] ); - - return exec; } catch (err) { if (err instanceof TypeError) { // spawn method throws TypeError if some argument is invalid diff --git a/src/StreamsInfo.js b/src/StreamsInfo.js index 1107bf7..4d7c550 100644 --- a/src/StreamsInfo.js +++ b/src/StreamsInfo.js @@ -21,20 +21,24 @@ class StreamsInfo { throw new TypeError('You should provide a correct url, bastard.'); } - const {ffprobePath, timeoutInSec} = config; + const {ffprobePath, timeoutInMs} = config; if (!_.isString(ffprobePath) || _.isEmpty(ffprobePath)) { throw new Errors.ConfigError('You should provide a correct path to ffprobe, bastard.'); } - if (!_.isInteger(timeoutInSec) || timeoutInSec <= 0) { + if (!_.isInteger(timeoutInMs) || timeoutInMs <= 0) { throw new Errors.ConfigError('You should provide a correct timeout, bastard.'); } this._assertExecutable(ffprobePath); - this._config = config; - this._url = url; + this._config = { + ffprobePath, + timeout: timeoutInMs * 1000 + }; + + this._url = url; } async fetch() { @@ -79,15 +83,16 @@ class StreamsInfo { } _runShowStreamsProcess() { - const {ffprobePath, timeoutInSec} = this._config; + const {ffprobePath, timeout} = this._config; - const command = ` + const command = `\ ${ffprobePath}\ -hide_banner\ -v error\ -show_streams\ -print_format json\ - '${this._url} timeout=${timeoutInSec}' + -rw_timeout ${timeout}\ + ${this._url}\ `; return promisify(exec)(command); diff --git a/tests/Functional/FramesMonitor/listen.test.js b/tests/Functional/FramesMonitor/listen.test.js index 34f2bd6..8eb94f9 100644 --- a/tests/Functional/FramesMonitor/listen.test.js +++ b/tests/Functional/FramesMonitor/listen.test.js @@ -31,7 +31,7 @@ describe('FramesMonitor::listen, fetch frames from inactive stream', () => { framesMonitor = new FramesMonitor({ ffprobePath : process.env.FFPROBE, - timeoutInSec : 1, + timeoutInMs : 1000, bufferMaxLengthInBytes : bufferMaxLengthInBytes, errorLevel : errorLevel, exitProcessGuardTimeoutInMs: exitProcessGuardTimeoutInMs @@ -82,7 +82,7 @@ describe('FramesMonitor::listen, fetch frames from active stream', () => { framesMonitor = new FramesMonitor({ ffprobePath : process.env.FFPROBE, - timeoutInSec : 1, + timeoutInMs : 1000, bufferMaxLengthInBytes : bufferMaxLengthInBytes, errorLevel : errorLevel, exitProcessGuardTimeoutInMs: exitProcessGuardTimeoutInMs @@ -102,7 +102,7 @@ describe('FramesMonitor::listen, fetch frames from active stream', () => { }); it('must receive all stream frames', done => { - const expectedReturnCode = 0; + const expectedReturnCode = 0; const onFrame = {I: spyOnIFrame, P: spyOnPFrame}; @@ -146,7 +146,7 @@ describe('FramesMonitor::listen, stop ffprobe process', () => { framesMonitor = new FramesMonitor({ ffprobePath : process.env.FFPROBE, - timeoutInSec : 1, + timeoutInMs : 1000, bufferMaxLengthInBytes : bufferMaxLengthInBytes, errorLevel : errorLevel, exitProcessGuardTimeoutInMs: exitProcessGuardTimeoutInMs @@ -188,7 +188,7 @@ describe('FramesMonitor::listen, exit with correct code after stream has been fi framesMonitor = new FramesMonitor({ ffprobePath : process.env.FFPROBE, - timeoutInSec : 1, + timeoutInMs : 1000, bufferMaxLengthInBytes : bufferMaxLengthInBytes, errorLevel : errorLevel, exitProcessGuardTimeoutInMs: exitProcessGuardTimeoutInMs diff --git a/tests/Functional/StreamsInfo/fetch.test.js b/tests/Functional/StreamsInfo/fetch.test.js index cedb8e1..200f484 100644 --- a/tests/Functional/StreamsInfo/fetch.test.js +++ b/tests/Functional/StreamsInfo/fetch.test.js @@ -23,8 +23,8 @@ describe('StreamsInfo::fetch, fetch streams info from inactive stream', () => { streamUrl = `http://localhost:${port}`; streamsInfo = new StreamsInfo({ - ffprobePath : process.env.FFPROBE, - timeoutInSec: 1, + ffprobePath: process.env.FFPROBE, + timeoutInMs: 1000, }, streamUrl); }); @@ -54,8 +54,8 @@ describe('StreamsInfo::fetch, fetch streams info from active stream', () => { streamUrl = `http://localhost:${port}`; streamsInfo = new StreamsInfo({ - ffprobePath : process.env.FFPROBE, - timeoutInSec: 1, + ffprobePath: process.env.FFPROBE, + timeoutInMs: 1000, }, streamUrl); stream = await startStream(testFile, streamUrl); diff --git a/tests/Unit/FramesMonitor/Helpers/index.js b/tests/Unit/FramesMonitor/Helpers/index.js index 84d2afc..bcada2f 100644 --- a/tests/Unit/FramesMonitor/Helpers/index.js +++ b/tests/Unit/FramesMonitor/Helpers/index.js @@ -6,7 +6,7 @@ const proxyquire = require('proxyquire'); const ffprobePath = '/correct/path'; const bufferMaxLengthInBytes = 2 ** 20; -const timeoutInSec = 1; +const timeoutInMs = 1000; const url = 'rtmp://localhost:1935/myapp/mystream'; const errorLevel = 'fatal'; // https://ffmpeg.org/ffprobe.html const exitProcessGuardTimeoutInMs = 2000; @@ -38,7 +38,7 @@ function makeChildProcess() { module.exports = { config: { ffprobePath, - timeoutInSec, + timeoutInMs, bufferMaxLengthInBytes, errorLevel, exitProcessGuardTimeoutInMs diff --git a/tests/Unit/FramesMonitor/_runShowFramesProcess.test.js b/tests/Unit/FramesMonitor/_runShowFramesProcess.test.js index 80d338a..98db3ef 100644 --- a/tests/Unit/FramesMonitor/_runShowFramesProcess.test.js +++ b/tests/Unit/FramesMonitor/_runShowFramesProcess.test.js @@ -6,24 +6,26 @@ const {assert} = require('chai'); const {config, url} = require('./Helpers'); -function getSpawnArguments(url, timeoutInSec, errorLevel) { +function getSpawnArguments(url, timeoutInMs, errorLevel) { return [ '-hide_banner', '-v', errorLevel, '-fflags', 'nobuffer', + '-rw_timeout', + timeoutInMs * 1000, '-show_frames', '-show_entries', 'frame=pkt_size,pkt_pts_time,media_type,pict_type,key_frame,width,height', '-i', - `${url} timeout=${timeoutInSec}` + url ]; } describe('FramesMonitor::_handleProcessingError', () => { const expectedFfprobePath = config.ffprobePath; - const expectedFfprobeArguments = getSpawnArguments(url, config.timeoutInSec, config.errorLevel); + const expectedFfprobeArguments = getSpawnArguments(url, config.timeoutInMs, config.errorLevel); it('must returns child process object just fine', () => { const expectedOutput = {cp: true}; diff --git a/tests/Unit/FramesMonitor/constructor.data.js b/tests/Unit/FramesMonitor/constructor.data.js index 323dd62..447c6d1 100644 --- a/tests/Unit/FramesMonitor/constructor.data.js +++ b/tests/Unit/FramesMonitor/constructor.data.js @@ -39,7 +39,7 @@ const incorrectFfprobePath = [ new Error('bastard') ]; -const incorrectTimeoutInSec = [ +const incorrectTimeoutInMs = [ undefined, null, false, @@ -93,13 +93,13 @@ const incorrectExitProcessGuardTimeoutInMs = [ const incorrectConfigObject = [ { - description: 'config.timeoutInSec param must be a positive integer, float is passed', - config : {timeoutInSec: 1.1}, + description: 'config.timeoutInMs param must be a positive integer, float is passed', + config : {timeoutInMs: 1.1}, errorMsg : 'You should provide a correct timeout, bastard.' }, { - description: 'config.timeoutInSec param must be a positive integer, negative is passed', - config : {timeoutInSec: -1}, + description: 'config.timeoutInMs param must be a positive integer, negative is passed', + config : {timeoutInMs: -1}, errorMsg : 'You should provide a correct timeout, bastard.' }, { @@ -133,7 +133,7 @@ module.exports = { incorrectConfig, incorrectUrl, incorrectFfprobePath, - incorrectTimeoutInSec, + incorrectTimeoutInMs, incorrectBufferMaxLengthInBytes, incorrectErrorLevel, incorrectExitProcessGuardTimeoutInMs, diff --git a/tests/Unit/FramesMonitor/constructor.test.js b/tests/Unit/FramesMonitor/constructor.test.js index b04828c..f3d93da 100644 --- a/tests/Unit/FramesMonitor/constructor.test.js +++ b/tests/Unit/FramesMonitor/constructor.test.js @@ -71,11 +71,11 @@ describe('FramesMonitor::constructor', () => { ); dataDriven( - testData.incorrectTimeoutInSec.map(item => ({type: typeOf(item), timeoutInSec: item})), + testData.incorrectTimeoutInMs.map(item => ({type: typeOf(item), timeoutInMs: item})), () => { - it('config.timeoutInSec param has invalid ({type}) type', ctx => { + it('config.timeoutInMs param has invalid ({type}) type', ctx => { const incorrectConfig = Object.assign({}, config, { - timeoutInSec: ctx.timeoutInSec + timeoutInMs: ctx.timeoutInMs }); assert.throws(() => { @@ -172,13 +172,21 @@ describe('FramesMonitor::constructor', () => { const expectedChildProcessDefaultValue = null; const expectedChunkRemainderDefaultValue = ''; const expectedStderrOutputs = []; + const expectedConfig = { + ffprobePath : config.ffprobePath, + bufferMaxLengthInBytes : config.bufferMaxLengthInBytes, + errorLevel : config.errorLevel, + exitProcessGuardTimeoutInMs: config.exitProcessGuardTimeoutInMs, + timeout : config.timeoutInMs * 1000 + }; const framesMonitor = new FramesMonitor(config, url); assert.isTrue(spyAssertExecutable.calledOnce); assert.isTrue(spyAssertExecutable.calledWithExactly(config.ffprobePath)); - assert.deepEqual(framesMonitor._config, config); + + assert.deepEqual(framesMonitor._config, expectedConfig); assert.strictEqual(framesMonitor._url, url); assert.strictEqual(framesMonitor._cp, expectedChildProcessDefaultValue); diff --git a/tests/Unit/StreamsInfo/_adjustAspectRatio.test.js b/tests/Unit/StreamsInfo/_adjustAspectRatio.test.js index dd2fa26..e0f49d4 100644 --- a/tests/Unit/StreamsInfo/_adjustAspectRatio.test.js +++ b/tests/Unit/StreamsInfo/_adjustAspectRatio.test.js @@ -13,8 +13,8 @@ const {invalidParams, validParams} = require('./_adjustAspectRatio.data'); describe('StreamsInfo::_adjustAspectRatio', () => { const streamsInfo = new StreamsInfo({ - ffprobePath : correctPath, - timeoutInSec: 1 + ffprobePath: correctPath, + timeoutInMs: 1 }, correctUrl); dataDriven(invalidParams, function () { diff --git a/tests/Unit/StreamsInfo/_parseStreamsInfo.test.js b/tests/Unit/StreamsInfo/_parseStreamsInfo.test.js index 930392a..c515ce2 100644 --- a/tests/Unit/StreamsInfo/_parseStreamsInfo.test.js +++ b/tests/Unit/StreamsInfo/_parseStreamsInfo.test.js @@ -14,8 +14,8 @@ function typeOf(obj) { describe('StreamsInfo::_parseStreamsInfo', () => { const streamsInfo = new StreamsInfo({ - ffprobePath : correctPath, - timeoutInSec: 1 + ffprobePath: correctPath, + timeoutInMs: 1 }, correctUrl); it('method awaits for stringified json', () => { diff --git a/tests/Unit/StreamsInfo/_runShowStreamsProcess.test.js b/tests/Unit/StreamsInfo/_runShowStreamsProcess.test.js new file mode 100644 index 0000000..fa0b2df --- /dev/null +++ b/tests/Unit/StreamsInfo/_runShowStreamsProcess.test.js @@ -0,0 +1,64 @@ +'use strict'; + +const {assert} = require('chai'); +const sinon = require('sinon'); +const proxyquire = require('proxyquire'); + +const {correctPath, correctUrl} = require('./Helpers/'); + +function getExecCommand(ffprobePath, timeout, url) { + return `\ + ${ffprobePath}\ + -hide_banner\ + -v error\ + -show_streams\ + -print_format json\ + -rw_timeout ${timeout}\ + ${url}\ + `; +} + +describe('StreamsInfo::_runShowStreamsProcess', () => { + const timeoutInMs = 1000; + + it('must returns child process object just fine', () => { + const expectedFfprobeCommand = getExecCommand(correctPath, timeoutInMs * 1000, correctUrl); + + const execOutput = {cp: true}; + const exec = () => execOutput; + const spyExec = sinon.spy(exec); + + const stubPromisify = sinon.stub(); + stubPromisify.returns(spyExec); + + const StreamsInfo = proxyquire('src/StreamsInfo', { + fs: { + accessSync(filePath) { + if (filePath !== correctPath) { + throw new Error('no such file or directory'); + } + } + }, + util: { + promisify: stubPromisify + }, + child_process: { + exec: spyExec + } + }); + + const streamsInfo = new StreamsInfo({ + ffprobePath: correctPath, + timeoutInMs + }, correctUrl); + + const result = streamsInfo._runShowStreamsProcess(); + + assert.strictEqual(result, execOutput); + + assert.isTrue(spyExec.calledOnce); + assert.isTrue( + spyExec.calledWithExactly(expectedFfprobeCommand) + ); + }); +}); diff --git a/tests/Unit/StreamsInfo/constructor.data.js b/tests/Unit/StreamsInfo/constructor.data.js index 50956ee..c2d3795 100644 --- a/tests/Unit/StreamsInfo/constructor.data.js +++ b/tests/Unit/StreamsInfo/constructor.data.js @@ -71,23 +71,23 @@ const incorrectConfig = [ 'errorMsg' : 'You should provide a correct path to ffprobe, bastard.' }, { - 'description': 'config.timeout must be passed', + 'description': 'config.timeoutInMs must be passed', 'config' : {ffprobePath: correctPath}, 'errorMsg' : 'You should provide a correct timeout, bastard.' }, { - 'description': 'config.timeout param must be a positive integer, float is passed', - 'config' : {ffprobePath: correctPath, timeoutInSec: 1.1}, + 'description': 'config.timeoutInMs param must be a positive integer, float is passed', + 'config' : {ffprobePath: correctPath, timeoutInMs: 1.1}, 'errorMsg' : 'You should provide a correct timeout, bastard.' }, { - 'description': 'config.timeout param must be a positive integer, negative is passed', - 'config' : {ffprobePath: correctPath, timeoutInSec: -1}, + 'description': 'config.timeoutInMs param must be a positive integer, negative is passed', + 'config' : {ffprobePath: correctPath, timeoutInMs: -1}, 'errorMsg' : 'You should provide a correct timeout, bastard.' }, { - 'description': 'config.timeout param must be a positive integer, string is passed', - 'config' : {ffprobePath: correctPath, timeoutInSec: '10'}, + 'description': 'config.timeoutInMs param must be a positive integer, string is passed', + 'config' : {ffprobePath: correctPath, timeoutInMs: '10'}, 'errorMsg' : 'You should provide a correct timeout, bastard.' }, ]; diff --git a/tests/Unit/StreamsInfo/constructor.test.js b/tests/Unit/StreamsInfo/constructor.test.js index e31495c..496c49c 100644 --- a/tests/Unit/StreamsInfo/constructor.test.js +++ b/tests/Unit/StreamsInfo/constructor.test.js @@ -38,8 +38,8 @@ describe('StreamsInfo::constructor', () => { it('config.ffprobePath points to incorrect path', () => { assert.throws(() => { new StreamsInfo({ - ffprobePath : `/incorrect/path/${correctUrl}`, - timeoutInSec: 1 + ffprobePath: `/incorrect/path/${correctUrl}`, + timeoutInMs: 1 }, correctUrl); }, Errors.ExecutablePathError); }); @@ -47,8 +47,8 @@ describe('StreamsInfo::constructor', () => { it('all params are good', () => { assert.doesNotThrow(() => { new StreamsInfo({ - ffprobePath : correctPath, - timeoutInSec: 1 + ffprobePath: correctPath, + timeoutInMs: 1 }, correctUrl); }); }); diff --git a/tests/Unit/StreamsInfo/fetch.test.js b/tests/Unit/StreamsInfo/fetch.test.js index 555d523..3356e3a 100644 --- a/tests/Unit/StreamsInfo/fetch.test.js +++ b/tests/Unit/StreamsInfo/fetch.test.js @@ -15,8 +15,8 @@ function typeOf(obj) { describe('StreamsInfo::fetch', () => { let streamsInfo = new StreamsInfo({ - ffprobePath : correctPath, - timeoutInSec: 1 + ffprobePath: correctPath, + timeoutInMs: 1 }, correctUrl); let stubRunShowStreamsProcess;