Separated the output into separate pluggable modules
Added raw and mjpeg-split output modules Added before/after/during commands Updated sample config and added readme
This commit is contained in:
parent
84b3e8f1a7
commit
dd75bef94f
69
README.md
Normal file
69
README.md
Normal file
@ -0,0 +1,69 @@
|
||||
# SecurityCam.js
|
||||
#### URL's
|
||||
| Path | Description |
|
||||
| --- | --- |
|
||||
| / | Outputs a bit of status information in JSON format. |
|
||||
| /capture | Starts capturing all cameras as defined in config.js. |
|
||||
| /capture/id | Starts capturing the camera with the specified id as defined in config.js. |
|
||||
|
||||
#### Configuration
|
||||
|
||||
Refer to config.sample.js for the syntax.
|
||||
|
||||
##### Basic
|
||||
| Name | Description |
|
||||
| --- | --- |
|
||||
| port | The port on which the HTTP server will run. Default is 5705. |
|
||||
| defaultTime | The default time for which a capture runs, in milliseconds. |
|
||||
| cams | An object where each property defines a camera. The property name is the camId, the property value an object with the options for that camera. |
|
||||
|
||||
##### Camera
|
||||
| Name | Description |
|
||||
| --- | --- |
|
||||
| url | The URL to the video stream. At the moment it is assumed to be an endless stream (video or server-pushed mjpeg), snapshots are not polled and will result in capturing only a single frame. |
|
||||
| time | How long this camera will be captured, in milliseconds. If not specified, defaultTime is used. |
|
||||
| output | The output format used. This is pluggable and corresponds to the output-*.js files. For example, 'ffmpeg', 'mjpeg-split' or 'raw'. |
|
||||
| outputOptions | Options depending on the output format used. Refer to the specific output configuration below. |
|
||||
|
||||
|
||||
##### Output - Variables
|
||||
When specifying a filename you can use date / time specifiers and variables. The filename will be processed by the [format() function of the moment library](http://momentjs.com/docs/#/displaying/). This means that text like 'YYYY' will be replaced by the current year. Any literal text you do not want to have replaced should be enclosed in square brackets.
|
||||
|
||||
All outputs support the <camId> variable which will be replaced with the camera's property name as specified in the config. Any extra supported variables are listed under the output's section below.
|
||||
|
||||
An example: '[/srv/www/publiccam/]YYYY-MM-DD HH.mm.ss[/<camId>.avi]'
|
||||
|
||||
|
||||
If multiple cameras are captured at the same time, the date and time used in the filename will be the same for each, regardless of when the camera actually starts streaming. This ensures you can group these files together.
|
||||
|
||||
##### Output - 'raw'
|
||||
The simplest of outputs; writes all data it receives from the URL to a file without further processing.
|
||||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| filename | The filename where the raw output will be written to. If the file exists it will be overwritten. |
|
||||
|
||||
|
||||
|
||||
##### Output - 'mjpeg-split'
|
||||
Processes an MJPEG stream and outputs individual JPEG files.
|
||||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| filename | The filename for each JPEG. Be sure to include the <frame> variable to get unique filenames! |
|
||||
|
||||
| Variable | Description |
|
||||
| --- | --- |
|
||||
| frame | The frame number in the current capture. Starts at 1. |
|
||||
|
||||
|
||||
|
||||
##### Output - 'ffmpeg'
|
||||
Uses [ffmpeg](https://ffmpeg.org/) to transcode the input stream into another output format. Requires ffmpeg to be installed as described in [node-fluent-ffmpeg's readme under Prerequisites](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg#prerequisites).
|
||||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| filename | The filename where the encoded output will be written to. If the file exists it will be overwritten. |
|
||||
| inputFormat | The input format, for example 'mjpeg'. For the full list, run 'ffmpeg -formats' and look for formats with the 'D' column. |
|
||||
| outputFormat | The output format, for example 'avi'. For the full list, run 'ffmpeg -formats' and look for formats with the 'E' column. |
|
||||
| videoCodec | The output video codec, for example 'libx264'. For the full list, run 'ffmpeg -codecs' and look for formats with the 'V' column. For some codecs (like x264) you need to specify the encoder instead of the codec identifier, which are listed after the description as '(encoders: ...)'. |
|
181
capture.js
181
capture.js
@ -1,80 +1,139 @@
|
||||
var http = require('http');
|
||||
var fs = require('fs');
|
||||
var util = require('util');
|
||||
var Readable = require('stream').Readable;
|
||||
var FfmpegCommand = require('fluent-ffmpeg');
|
||||
var stream = require('stream');
|
||||
|
||||
var config = require('./config');
|
||||
|
||||
|
||||
var PushStream = function(options) { Readable.call(this, options); };
|
||||
util.inherits(PushStream, Readable);
|
||||
PushStream.prototype._read = function(n) { };
|
||||
|
||||
|
||||
module.exports =
|
||||
function runCommand(command, displayName, callback)
|
||||
{
|
||||
start: function(camId, cam, now)
|
||||
var wait = function()
|
||||
{
|
||||
var timer = null;
|
||||
var req = null;
|
||||
|
||||
console.log('Starting FFmpeg');
|
||||
|
||||
var output = new PushStream();
|
||||
var command = new FfmpegCommand();
|
||||
command
|
||||
.input(output)
|
||||
.inputFormat('mjpeg')
|
||||
.inputOption('-use_wallclock_as_timestamps 1')
|
||||
.output('D:/Temp/' + camId + '.avi')
|
||||
.videoCodec('libx264')
|
||||
.outputFormat('avi')
|
||||
.run();
|
||||
|
||||
|
||||
var cleanup = function()
|
||||
if (command.wait)
|
||||
{
|
||||
if (timer !== null)
|
||||
{
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
setTimeout(callback, command.wait)
|
||||
}
|
||||
else
|
||||
callback();
|
||||
}
|
||||
|
||||
if (command !== null)
|
||||
{
|
||||
output.push(null);
|
||||
command = null;
|
||||
}
|
||||
};
|
||||
if (command.url)
|
||||
{
|
||||
console.log('Running command: ' + (command.displayName ? command.displayName : displayName));
|
||||
|
||||
|
||||
req = http.request(cam.url, function(res)
|
||||
req = http.request(command.url, function(res)
|
||||
{
|
||||
timer = setTimeout(function()
|
||||
{
|
||||
console.log('Timeout!');
|
||||
req.abort();
|
||||
|
||||
cleanup();
|
||||
}, cam.timeout);
|
||||
|
||||
res.on('data', function(chunk)
|
||||
{
|
||||
output.push(chunk);
|
||||
});
|
||||
|
||||
res.on('end', function()
|
||||
{
|
||||
console.log('End!');
|
||||
cleanup();
|
||||
});
|
||||
res.resume();
|
||||
wait();
|
||||
});
|
||||
|
||||
req.on('error', function(e)
|
||||
{
|
||||
console.log(e);
|
||||
cleanup();
|
||||
wait();
|
||||
});
|
||||
|
||||
req.end();
|
||||
}
|
||||
else
|
||||
wait();
|
||||
}
|
||||
|
||||
|
||||
function runCommands(commandArray, displayName, callback)
|
||||
{
|
||||
if (!commandArray || !commandArray.length)
|
||||
{
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var commandIndex = 0;
|
||||
|
||||
(function runNextCommand()
|
||||
{
|
||||
if (commandIndex < commandArray.length)
|
||||
{
|
||||
runCommand(commandArray[commandIndex], displayName + ' #' + (commandIndex + 1), function()
|
||||
{
|
||||
runNextCommand();
|
||||
});
|
||||
|
||||
commandIndex++;
|
||||
}
|
||||
else
|
||||
callback();
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
module.exports =
|
||||
{
|
||||
init: function(cams)
|
||||
{
|
||||
// Preload all modules to get error messages early
|
||||
for (var camId in cams)
|
||||
{
|
||||
if (cams.hasOwnProperty(camId))
|
||||
{
|
||||
require('./output-' + cams[camId].output);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
start: function(camId, cam, now)
|
||||
{
|
||||
var timer = null;
|
||||
var req = null;
|
||||
var output = new (require('./output-' + cam.output))(camId, cam.outputOptions, now);
|
||||
|
||||
|
||||
runCommands(cam.before, 'before', function()
|
||||
{
|
||||
var cleanup = function()
|
||||
{
|
||||
if (timer !== null)
|
||||
{
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
|
||||
if (output !== null)
|
||||
{
|
||||
output.cleanup();
|
||||
output = null;
|
||||
}
|
||||
|
||||
runCommands(cam.after, 'after', function() { });
|
||||
};
|
||||
|
||||
|
||||
req = http.request(cam.url, function(res)
|
||||
{
|
||||
runCommands(cam.during, 'during', function() { })
|
||||
|
||||
timer = setTimeout(function()
|
||||
{
|
||||
req.abort();
|
||||
cleanup();
|
||||
}, cam.time || config.defaultTime || 10000);
|
||||
|
||||
res.on('end', function()
|
||||
{
|
||||
cleanup();
|
||||
});
|
||||
|
||||
res.pipe(output.getStream());
|
||||
});
|
||||
|
||||
req.on('error', function(e)
|
||||
{
|
||||
console.log(e);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
};
|
@ -1,19 +1,35 @@
|
||||
var config = {};
|
||||
|
||||
config.port = 5705;
|
||||
config.defaultOutputBase = '/srv/cam/';
|
||||
config.defaultOutputFilename = 'YYYY-MM-DD HH.mm.ss <camId>.avi';
|
||||
config.defaultTimeout = 5000;
|
||||
config.defaultTime = 5000;
|
||||
|
||||
config.cams =
|
||||
{
|
||||
frontdoor:
|
||||
{
|
||||
url: 'http://10.138.1.10/videostream.cgi?user=viewer&pwd=verysecure',
|
||||
timeout: 5000,
|
||||
time: 5000,
|
||||
|
||||
outputBase: '/srv/www/publiccam/YYYY-MM-DD HH.mm.ss/',
|
||||
outputFilename: '<camId>.avi'
|
||||
output: 'ffmpeg',
|
||||
outputOptions:
|
||||
{
|
||||
inputFormat: 'mjpeg',
|
||||
outputFormat: 'avi',
|
||||
videoCodec: 'libx264',
|
||||
filename: '[/srv/www/publiccam/]YYYY-MM-DD HH.mm.ss[/<camId>.avi]'
|
||||
}
|
||||
},
|
||||
|
||||
backdoor:
|
||||
{
|
||||
url: 'http://10.138.1.11/videostream.cgi?user=viewer&pwd=verysecure',
|
||||
time: 5000,
|
||||
|
||||
output: 'mjpeg-split',
|
||||
outputOptions:
|
||||
{
|
||||
filename: '[/srv/www/publiccam/]YYYY-MM-DD HH.mm.ss[/<camId> <frame>.avi]'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
34
helpers.js
Normal file
34
helpers.js
Normal file
@ -0,0 +1,34 @@
|
||||
var path = require('path');
|
||||
var mkdirp = require('mkdirp');
|
||||
|
||||
|
||||
function parseVariables(value, now, variables)
|
||||
{
|
||||
for (var variable in variables)
|
||||
{
|
||||
if (variables.hasOwnProperty(variable))
|
||||
{
|
||||
value = value.replace('<' + variable + '>', variables[variable].toString());
|
||||
}
|
||||
}
|
||||
|
||||
value = now.format(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
function createVariableFilename(value, now, variables)
|
||||
{
|
||||
var filename = parseVariables(value, now, variables);
|
||||
var dirname = path.dirname(filename);
|
||||
mkdirp.sync(dirname);
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
|
||||
module.exports =
|
||||
{
|
||||
parseVariables: parseVariables,
|
||||
createVariableFilename: createVariableFilename
|
||||
};
|
20
index.js
20
index.js
@ -1,8 +1,10 @@
|
||||
var moment = require('moment');
|
||||
var express = require('express');
|
||||
|
||||
var config = require('./config');
|
||||
var capture = require('./capture');
|
||||
|
||||
var moment = require('moment');
|
||||
var express = require('express');
|
||||
|
||||
var app = express();
|
||||
|
||||
|
||||
@ -10,6 +12,8 @@ var app = express();
|
||||
if (!config.cams)
|
||||
config.cams = [];
|
||||
|
||||
capture.init(config.cams);
|
||||
|
||||
|
||||
app.get('/', function(req, res)
|
||||
{
|
||||
@ -28,10 +32,15 @@ app.get('/capture', function(req, res)
|
||||
|
||||
for (var camId in config.cams)
|
||||
{
|
||||
cams.push(camId);
|
||||
capture.start(camId, config.cams[camId], now);
|
||||
if (config.cams.hasOwnProperty(camId))
|
||||
{
|
||||
cams.push(camId);
|
||||
capture.start(camId, config.cams[camId], now);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Started capture for: ' + cams.join(', '));
|
||||
|
||||
res.send(JSON.stringify(cams));
|
||||
});
|
||||
|
||||
@ -43,10 +52,13 @@ app.get('/capture/:camId', function(req, res)
|
||||
if (config.cams.hasOwnProperty(camId))
|
||||
{
|
||||
capture.start(config.cams[camId], moment());
|
||||
|
||||
console.log('Started capture for: ' + camId);
|
||||
res.send(JSON.stringify([camId]));
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log('Cam not found: ' + camId);
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
12
nodegyp-windows.txt
Normal file
12
nodegyp-windows.txt
Normal file
@ -0,0 +1,12 @@
|
||||
Getting node-gyp to work on Windows (required for buildtools, used by mjpeg-consumer):
|
||||
|
||||
https://github.com/nodejs/node-gyp/issues/629#issuecomment-153196245
|
||||
|
||||
|
||||
|
||||
I couldn't get the 2015 build tools to work, but with 2013 already installed
|
||||
it worked using:
|
||||
|
||||
set VCTargetsPath=C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\V120
|
||||
set VCTargetsPath12=C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\V120
|
||||
npm config set msvs_version 2013
|
45
output-ffmpeg.js
Normal file
45
output-ffmpeg.js
Normal file
@ -0,0 +1,45 @@
|
||||
var util = require('util');
|
||||
var stream = require('stream');
|
||||
var FfmpegCommand = require('fluent-ffmpeg');
|
||||
|
||||
var helpers = require('./helpers');
|
||||
|
||||
|
||||
function OutputFFMPEG(camId, options, now)
|
||||
{
|
||||
this.output = new stream.PassThrough();
|
||||
var command = new FfmpegCommand();
|
||||
command
|
||||
.input(this.output)
|
||||
.inputFormat(options.inputFormat);
|
||||
|
||||
if (options.inputFormat === 'mjpeg')
|
||||
command.inputOption('-use_wallclock_as_timestamps 1');
|
||||
|
||||
command
|
||||
.output(helpers.createVariableFilename(options.filename, now,
|
||||
{
|
||||
camId: camId
|
||||
}))
|
||||
.videoCodec(options.videoCodec)
|
||||
.outputFormat(options.outputFormat)
|
||||
.run();
|
||||
}
|
||||
|
||||
|
||||
OutputFFMPEG.prototype.getStream = function()
|
||||
{
|
||||
return this.output;
|
||||
};
|
||||
|
||||
|
||||
OutputFFMPEG.prototype.cleanup = function()
|
||||
{
|
||||
if (this.output !== null)
|
||||
{
|
||||
this.output.end();
|
||||
this.output = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OutputFFMPEG;
|
44
output-mjpeg-split.js
Normal file
44
output-mjpeg-split.js
Normal file
@ -0,0 +1,44 @@
|
||||
var MjpegConsumer = require('mjpeg-consumer');
|
||||
var FileOnWrite = require('file-on-write');
|
||||
|
||||
var helpers = require('./helpers');
|
||||
|
||||
|
||||
function OutputMJPEGSplit(camId, options, now)
|
||||
{
|
||||
var frameCounter = 0;
|
||||
|
||||
this.output = new FileOnWrite({
|
||||
filename: function(data)
|
||||
{
|
||||
frameCounter++;
|
||||
|
||||
return helpers.createVariableFilename(options.filename, now,
|
||||
{
|
||||
camId: camId,
|
||||
frame: frameCounter
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.consumer = new MjpegConsumer();
|
||||
this.consumer.pipe(this.output);
|
||||
}
|
||||
|
||||
|
||||
OutputMJPEGSplit.prototype.getStream = function()
|
||||
{
|
||||
return this.consumer;
|
||||
};
|
||||
|
||||
|
||||
OutputMJPEGSplit.prototype.cleanup = function()
|
||||
{
|
||||
if (this.consumer !== null)
|
||||
{
|
||||
this.consumer.end();
|
||||
this.consumer = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OutputMJPEGSplit;
|
30
output-raw.js
Normal file
30
output-raw.js
Normal file
@ -0,0 +1,30 @@
|
||||
var fs = require('fs');
|
||||
|
||||
var helpers = require('./helpers');
|
||||
|
||||
|
||||
function OutputRaw(camId, options, now)
|
||||
{
|
||||
this.output = fs.createWriteStream(helpers.createVariableFilename(options.filename, now,
|
||||
{
|
||||
camId: camId
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
OutputRaw.prototype.getStream = function()
|
||||
{
|
||||
return this.output;
|
||||
};
|
||||
|
||||
|
||||
OutputRaw.prototype.cleanup = function()
|
||||
{
|
||||
if (this.output !== null)
|
||||
{
|
||||
this.output.end();
|
||||
this.output = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OutputRaw;
|
@ -10,7 +10,10 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.14.0",
|
||||
"file-on-write": "^1.1.1",
|
||||
"fluent-ffmpeg": "^2.1.0",
|
||||
"mjpeg-consumer": "^1.1.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"moment": "^2.14.1"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user