From 82862b9e77b6563db3078a9cb9fa6205a019a93e Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Sun, 7 Aug 2016 12:31:21 +0200 Subject: [PATCH] Added support for capturing groups of cameras Changed ffmpeg processor time being in seconds; milliseconds aren't actually supported but are converted for consistency in the config Updated readme --- README.md | 55 +++++++++++++++++++++++-------- config.sample.js | 2 ++ index.js | 79 +++++++++++++++++++++++++++++++++++++++++---- processor-ffmpeg.js | 2 +- 4 files changed, 117 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 344f150..b776d12 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ | --- | --- | | / | Outputs a bit of status information in JSON format. | | /capture | Starts capturing all cameras as defined in config.js. | +| /capture/group/id | Starts capturing all cameras in the specified group as defined in config.js. | | /capture/id | Starts capturing the camera with the specified id as defined in config.js. | +

#### Configuration @@ -14,22 +16,20 @@ Refer to config.sample.js for the syntax. | 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. | +| processor | The processor plugin to use. This corresponds to a 'processor-*.js' file. For example, 'ffmpeg', 'http-mjpegsplit' or 'http-raw'. | +| options | Options depending on the processor plugin used. Refer to the specific processor configuration below. | | before | An array of commands to run before starting the capture. Refer to the Commands section below. | | during | An array of commands to run when capture has started. Waits for the first response from the camera before running. | | after | An array of commands to run after the capture. Will wait for 'during' commands to finish. | +

-##### Output - Variables +##### Processor - 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. @@ -38,38 +38,65 @@ 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. + +##### Processor - 'http-raw' +The simplest of outputs; reads from an HTTP stream and writes all data it receives from the URL to a file without further processing. | Option | Description | | --- | --- | +| url | The URL to the video stream. At the moment it is assumed to be an endless stream, snapshots are not polled and will result in capturing only a single frame. | | filename | The filename where the raw output will be written to. If the file exists it will be overwritten. | +| time | How long this camera will be captured, in milliseconds. | +

- -##### Output - 'mjpeg-split' -Processes an MJPEG stream and outputs individual JPEG files. +##### Processor - 'http-mjpegsplit' +Processes an MJPEG HTTP stream and outputs individual JPEG files. | Option | Description | | --- | --- | +| url | The URL to the video stream. At the moment it is assumed to be an endless stream, snapshots are not polled and will result in capturing only a single frame. | | filename | The filename for each JPEG. Be sure to include the <frame> variable to get unique filenames! | +| time | How long this camera will be captured, in milliseconds. | | 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). +##### Processor - 'http-ffmpeg' +Uses [ffmpeg](https://ffmpeg.org/) to transcode the input HTTP 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 | | --- | --- | +| url | The URL to the video stream. At the moment it is assumed to be an endless stream, snapshots are not polled and will result in capturing only a single frame. | | filename | The filename where the encoded output will be written to. If the file exists it will be overwritten. | +| time | How long this camera will be captured, in milliseconds. Note: since FFmpeg uses seconds for the timeout, this value will be rounded up towards the nearest whole second. | | 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: ...)'. | +

+ + +##### Processor - '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). + +Similar to 'http-ffmpeg', with subtle differences: +- Upside: the input option is passed directly to FFmpeg, which means it supports non-HTTP sources (RTSP for example) +- Downside: 'during' commands are executed immediately instead of waiting for the stream to start up + +| Option | Description | +| --- | --- | +| input | The source of the video stream, passed directly to FFmpeg. | +| filename | The filename where the encoded output will be written to. If the file exists it will be overwritten. | +| time | How long this camera will be captured, in milliseconds. Note: since FFmpeg uses seconds for the timeout, this value will be rounded up towards the nearest whole second. | +| 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: ...)'. | +

##### Commands diff --git a/config.sample.js b/config.sample.js index e08dc07..d18ac42 100644 --- a/config.sample.js +++ b/config.sample.js @@ -8,6 +8,7 @@ config.cams = frontdoor: { processor: 'ffmpeg', + group: 'doors', options: { // You can specify any URL string here, the Foscam helper just makes it easier @@ -28,6 +29,7 @@ config.cams = backdoor: { processor: 'mjpeg-split', + groups: ['doors', 'back'], options: { // You can use username:password@ in the URL to log in with basic diff --git a/index.js b/index.js index 04ca470..628c707 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,9 @@ var serverPort = 0; var config = null; var configError = null; -var loadConfig = function() + + +function loadConfig() { try { @@ -66,6 +68,17 @@ var loadConfig = function() } +function inGroup(groupId, cam) +{ + if (cam.group === groupId) + return true; + + if (cam.groups && cam.groups.indexOf(groupId) > -1) + return true; + + return false; +} + app.get('/', function(req, res) { @@ -121,11 +134,54 @@ app.get('/capture', function(req, res) if (alreadyRunning.length > 0) logger.info('Capture already running for: ' + alreadyRunning.join(', ')); - res.send(JSON.stringify(cams)); + res.send(JSON.stringify( + { + started: cams, + alreadyRunning: alreadyRunning + })); } else { - res.status(500).send({ error: 'See / for more information' }); + res.status(500).send({ error: 'Error loading configuration. See status page for more information' }); + } +}); + + +app.get('/capture/group/:groupId', function(req, res) +{ + if (config !== null) + { + var groupId = req.params.groupId; + var now = moment(); + var cams = []; + var alreadyRunning = []; + + for (var camId in config.cams) + { + if (config.cams.hasOwnProperty(camId) && inGroup(groupId, config.cams[camId])) + { + if (capture.start(camId, config.cams[camId], now)) + cams.push(camId); + else + alreadyRunning.push(camId); + } + } + + if (cams.length > 0) + logger.info('Started capture for: ' + cams.join(', ')); + + if (alreadyRunning.length > 0) + logger.info('Capture already running for: ' + alreadyRunning.join(', ')); + + res.send(JSON.stringify( + { + started: cams, + alreadyRunning: alreadyRunning + })); + } + else + { + res.status(500).send({ error: 'Error loading configuration. See status page for more information' }); } }); @@ -139,12 +195,23 @@ app.get('/capture/:camId', function(req, res) if (config.cams.hasOwnProperty(camId)) { if (capture.start(camId, config.cams[camId], moment())) + { logger.info('Started capture for: ' + camId); + + res.send(JSON.stringify( + { + started: [camId] + })); + } else + { logger.info('Capture already running for: ' + camId); - - res.send(JSON.stringify([camId])); + res.send(JSON.stringify( + { + alreadyRunning: [camId] + })); + } } else { @@ -154,7 +221,7 @@ app.get('/capture/:camId', function(req, res) } else { - res.status(500).send({ error: 'See / for more information' }); + res.status(500).send({ error: 'Error loading configuration. See status page for more information' }); } }); diff --git a/processor-ffmpeg.js b/processor-ffmpeg.js index 203b9cf..2aebdce 100644 --- a/processor-ffmpeg.js +++ b/processor-ffmpeg.js @@ -41,7 +41,7 @@ FFMPEGProcessor.prototype.run = function() var command = new FfmpegCommand(); command .input(this.cam.options.input) - .inputOptions(['-t ' + this.cam.options.time, + .inputOptions(['-t ' + ceil(this.cam.options.time / 1000), '-rtsp_transport tcp']); if (typeof(this.cam.options.inputFormat) !== 'undefined')