Skip to content

Commit

Permalink
Merge 5bca170 into 1f18d49
Browse files Browse the repository at this point in the history
  • Loading branch information
oleh-poberezhets committed Dec 18, 2019
2 parents 1f18d49 + 5bca170 commit d9a5bef
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 86 deletions.
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -182,7 +182,8 @@ const framesMonitorOptions = {
timeoutInMs: 2000,
bufferMaxLengthInBytes: 100000,
errorLevel: 'error',
exitProcessGuardTimeoutInMs: 1000
exitProcessGuardTimeoutInMs: 1000,
analyzeDurationInMs: 9000
};

const framesMonitor = new FramesMonitor(framesMonitorOptions, 'rtmp://host:port/appInstance/name');
Expand Down Expand Up @@ -210,6 +211,7 @@ process will be hard killed if the attempt of soft stop fails. When you try to s
method the `FramesMonitor` sends `SIGTERM` signal to ffprobe process. ffprobe may ignore this signal (some versions
do it pretty often). If ffprobe doesn't exit after `exitProcessGuardTimeoutInMs` milliseconds, `FramesMonitor` sends
`SIGKILL` signal and forces underlying ffprobe process to exit.
* analyzeDurationInMs - integer, greater than 0, specifies the maximum analyzing time of the input.

## Listening of Frames

Expand Down
59 changes: 34 additions & 25 deletions src/FramesMonitor.js
Expand Up @@ -33,27 +33,28 @@ class FramesMonitor extends EventEmitter {
super();

if (!_.isPlainObject(config)) {
throw new TypeError('Config param should be a plain object, bastard.');
throw new TypeError('Config param should be a plain object.');
}

if (!_.isString(url)) {
throw new TypeError('You should provide a correct url, bastard.');
throw new TypeError('You should provide a correct url.');
}

const {
ffprobePath,
timeoutInMs,
bufferMaxLengthInBytes,
errorLevel,
exitProcessGuardTimeoutInMs
exitProcessGuardTimeoutInMs,
analyzeDurationInMs
} = config;

if (!_.isString(ffprobePath) || _.isEmpty(ffprobePath)) {
throw new Errors.ConfigError('You should provide a correct path to ffprobe, bastard.');
throw new Errors.ConfigError('You should provide a correct path to ffprobe.');
}

if (!_.isSafeInteger(timeoutInMs) || timeoutInMs <= 0) {
throw new Errors.ConfigError('You should provide a correct timeout, bastard.');
throw new Errors.ConfigError('You should provide a correct timeout.');
}

if (!_.isSafeInteger(bufferMaxLengthInBytes) || bufferMaxLengthInBytes <= 0) {
Expand All @@ -62,22 +63,27 @@ class FramesMonitor extends EventEmitter {

if (!_.isString(errorLevel) || !FramesMonitor._isValidErrorLevel(errorLevel)) {
throw new Errors.ConfigError(
'You should provide correct error level, bastard. Check ffprobe documentation.'
'You should provide correct error level. Check ffprobe documentation.'
);
}

if (!_.isSafeInteger(exitProcessGuardTimeoutInMs) || exitProcessGuardTimeoutInMs <= 0) {
throw new Errors.ConfigError('exitProcessGuardTimeoutInMs param should be a positive integer.');
}

if (analyzeDurationInMs !== undefined && (!_.isSafeInteger(analyzeDurationInMs) || analyzeDurationInMs <= 0)) {
throw new Errors.ConfigError('You should provide a correct analyze duration.');
}

FramesMonitor._assertExecutable(ffprobePath);

this._config = {
ffprobePath,
bufferMaxLengthInBytes,
errorLevel,
exitProcessGuardTimeoutInMs,
timeout: timeoutInMs * 1000
timeout: timeoutInMs * 1000,
analyzeDuration: analyzeDurationInMs && analyzeDurationInMs * 1000 || undefined
};

this._url = url;
Expand Down Expand Up @@ -267,26 +273,29 @@ class FramesMonitor extends EventEmitter {
}

_runShowFramesProcess() {
const {ffprobePath, timeout, errorLevel} = this._config;
const {ffprobePath, timeout, analyzeDuration, errorLevel} = this._config;

const args = [
'-hide_banner',
'-v',
errorLevel,
'-fflags',
'nobuffer',
'-rw_timeout',
timeout,
'-show_frames',
'-show_entries',
'frame=pkt_size,pkt_pts_time,media_type,pict_type,key_frame,width,height',
];

if (analyzeDuration) {
args.push('-analyzeduration', analyzeDuration);
}

args.push('-i', this._url);

try {
return spawn(
ffprobePath,
[
'-hide_banner',
'-v',
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
]
);
return spawn(ffprobePath, args);
} catch (err) {
if (err instanceof TypeError) {
// spawn method throws TypeError if some argument is invalid
Expand Down
43 changes: 23 additions & 20 deletions src/StreamsInfo.js
Expand Up @@ -14,28 +14,33 @@ const DAR_OR_SAR_01 = '0:1';
class StreamsInfo {
constructor(config, url) {
if (!_.isObject(config) || _.isFunction(config)) {
throw new TypeError('Config param should be an object, bastard.');
throw new TypeError('Config param should be an object.');
}

if (!_.isString(url)) {
throw new TypeError('You should provide a correct url, bastard.');
throw new TypeError('You should provide a correct url.');
}

const {ffprobePath, timeoutInMs} = config;
const {ffprobePath, timeoutInMs, analyzeDurationInMs} = config;

if (!_.isString(ffprobePath) || _.isEmpty(ffprobePath)) {
throw new Errors.ConfigError('You should provide a correct path to ffprobe, bastard.');
throw new Errors.ConfigError('You should provide a correct path to ffprobe.');
}

if (!_.isInteger(timeoutInMs) || timeoutInMs <= 0) {
throw new Errors.ConfigError('You should provide a correct timeout, bastard.');
throw new Errors.ConfigError('You should provide a correct timeout.');
}

if (analyzeDurationInMs !== undefined && (!_.isInteger(analyzeDurationInMs) || analyzeDurationInMs <= 0)) {
throw new Errors.ConfigError('You should provide a correct analyze duration.');
}

this._assertExecutable(ffprobePath);

this._config = {
ffprobePath,
timeout: timeoutInMs * 1000
ffprobePath: config.ffprobePath,
timeout: config.timeoutInMs * 1000,
analyzeDuration: config.analyzeDurationInMs && config.analyzeDurationInMs * 1000 || 0
};

this._url = url;
Expand Down Expand Up @@ -83,19 +88,17 @@ class StreamsInfo {
}

_runShowStreamsProcess() {
const {ffprobePath, timeout} = this._config;

const command = `\
${ffprobePath}\
-hide_banner\
-v error\
-show_streams\
-print_format json\
-rw_timeout ${timeout}\
${this._url}\
`;

return promisify(exec)(command);
const {ffprobePath, timeout, analyzeDuration} = this._config;

const commandArgs = [ffprobePath, '-hide_banner', '-v error'];

if (analyzeDuration) {
commandArgs.push('-analyzeduration', analyzeDuration);
}

commandArgs.push('-rw_timeout', timeout, '-show_streams', '-print_format json', '-i', this._url);

return promisify(exec)(commandArgs.join(' '));
}

_parseStreamsInfo(rawResult) {
Expand Down
4 changes: 3 additions & 1 deletion tests/Unit/FramesMonitor/Helpers/index.js
Expand Up @@ -10,6 +10,7 @@ const timeoutInMs = 1000;
const url = 'rtmp://localhost:1935/myapp/mystream';
const errorLevel = 'fatal'; // https://ffmpeg.org/ffprobe.html
const exitProcessGuardTimeoutInMs = 2000;
const analyzeDurationInMs = 1000;


const FramesMonitor = proxyquire('src/FramesMonitor', {
Expand Down Expand Up @@ -41,7 +42,8 @@ module.exports = {
timeoutInMs,
bufferMaxLengthInBytes,
errorLevel,
exitProcessGuardTimeoutInMs
exitProcessGuardTimeoutInMs,
analyzeDurationInMs
},
url,
FramesMonitor,
Expand Down
60 changes: 55 additions & 5 deletions tests/Unit/FramesMonitor/_runShowFramesProcess.test.js
Expand Up @@ -6,8 +6,8 @@ const {assert} = require('chai');

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

function getSpawnArguments(url, timeoutInMs, errorLevel) {
return [
function getSpawnArguments(url, timeoutInMs, analyzeDurationInMs, errorLevel) {
const args = [
'-hide_banner',
'-v',
errorLevel,
Expand All @@ -18,14 +18,22 @@ function getSpawnArguments(url, timeoutInMs, errorLevel) {
'-show_frames',
'-show_entries',
'frame=pkt_size,pkt_pts_time,media_type,pict_type,key_frame,width,height',
'-i',
url
];

if (analyzeDurationInMs) {
args.push('-analyzeduration', analyzeDurationInMs * 1000);
}

args.push('-i', url);

return args;
}

describe('FramesMonitor::_handleProcessingError', () => {
const expectedFfprobePath = config.ffprobePath;
const expectedFfprobeArguments = getSpawnArguments(url, config.timeoutInMs, config.errorLevel);
const expectedFfprobeArguments = getSpawnArguments(
url, config.timeoutInMs, config.analyzeDurationInMs, config.errorLevel
);

it('must returns child process object just fine', () => {
const expectedOutput = {cp: true};
Expand Down Expand Up @@ -62,6 +70,48 @@ describe('FramesMonitor::_handleProcessingError', () => {
assert.isTrue(spyOnProcessStartError.notCalled);
});

it('must returns child process object just fine with default analyze duration', () => {
const analyzeDurationInMs = undefined;

const expectedOutput = {cp: true};
const expectedFfprobeArguments = getSpawnArguments(
url, config.timeoutInMs, analyzeDurationInMs, config.errorLevel
);

const spawn = () => expectedOutput;
const spySpawn = sinon.spy(spawn);

const FramesMonitor = proxyquire('src/FramesMonitor', {
fs : {
accessSync(filePath) {
if (filePath !== config.ffprobePath) {
throw new Error('no such file or directory');
}
}
},
child_process: {
spawn: spySpawn
}
});

const options = Object.assign({}, config, {analyzeDurationInMs});

const framesMonitor = new FramesMonitor(options, url);

const spyOnProcessStartError = sinon.spy(framesMonitor, '_onProcessStartError');

const result = framesMonitor._runShowFramesProcess();

assert.strictEqual(result, expectedOutput);

assert.isTrue(spySpawn.calledOnce);
assert.isTrue(
spySpawn.calledWithExactly(expectedFfprobePath, expectedFfprobeArguments)
);

assert.isTrue(spyOnProcessStartError.notCalled);
});

it('must re-thrown TypeError error from the spawn call', () => {
const expectedError = new TypeError('some error');

Expand Down

0 comments on commit d9a5bef

Please sign in to comment.