diff --git a/basehttpstreamprocessor.js b/basehttpstreamprocessor.js new file mode 100644 index 0000000..eb188d1 --- /dev/null +++ b/basehttpstreamprocessor.js @@ -0,0 +1,71 @@ +var util = require('util'); +var http = require('http'); + +var BaseProcessor = require('./baseprocessor'); + + +function BaseHTTPStreamProcessor(camId, cam, now) +{ + BaseProcessor.apply(this, arguments); +} + +util.inherits(BaseHTTPStreamProcessor, BaseProcessor); + + +BaseHTTPStreamProcessor.prototype.run = function() +{ + var self = this; + var timer = null; + var req = null; + + var doCleanup = function() + { + if (timer !== null) + { + clearTimeout(timer); + timer = null; + } + + self.cleanup(); + self.doEnd(); + }; + + req = http.request(self.cam.options.url, function(res) + { + self.doStart(); + + timer = setTimeout(function() + { + req.abort(); + doCleanup(); + }, self.cam.options.time || 10000); + + + res.on('end', function() + { + doCleanup(); + }); + + res.pipe(self.getStream()); + }); + + req.on('error', function(e) + { + console.log(e); + doCleanup(); + }); + + req.end(); +}; + + +BaseHTTPStreamProcessor.prototype.getStream = function() +{ +} + + +BaseHTTPStreamProcessor.prototype.cleanup = function() +{ +} + +module.exports = BaseHTTPStreamProcessor; \ No newline at end of file diff --git a/baseprocessor.js b/baseprocessor.js new file mode 100644 index 0000000..51ca75f --- /dev/null +++ b/baseprocessor.js @@ -0,0 +1,42 @@ +var events = require('events'); +var util = require('util'); + +function BaseProcessor(camId, cam, now) +{ + this.camId = camId; + this.cam = cam; + this.now = now; +} + +/* + Descendants are required to call the following functions: + + doStart + Call when the processing starts. This may be delayed slightly, for example, + right after the stream is connected or when the first chunk of data arrives. + + doEnd + Call when the processor is done. + +*/ +util.inherits(BaseProcessor, events.EventEmitter); + + +BaseProcessor.prototype.run = function() +{ +} + + +BaseProcessor.prototype.doStart = function() +{ + this.emit('start'); +} + + +BaseProcessor.prototype.doEnd = function() +{ + this.emit('end'); +} + + +module.exports = BaseProcessor; \ No newline at end of file diff --git a/capture.js b/capture.js index 96b63bf..1945542 100644 --- a/capture.js +++ b/capture.js @@ -76,7 +76,7 @@ module.exports = { if (cams.hasOwnProperty(camId)) { - require('./output-' + cams[camId].output); + require('./processor-' + cams[camId].processor); } } }, @@ -84,71 +84,38 @@ module.exports = start: function(camId, cam, now) { - var timer = null; - var req = null; - var output = new (require('./output-' + cam.output))(camId, cam.outputOptions, now); - var duringDone = false; - var queueAfter = false; + var processor = new (require('./processor-' + cam.processor))(camId, cam, now); + var duringCommandsDone = false; + var queueAfterCommand = false; runCommands(cam.before, 'before', function() { - var cleanup = function() - { - if (timer !== null) - { - clearTimeout(timer); - timer = null; - } - - if (output !== null) - { - output.cleanup(); - output = null; - } - - // If the stream is cut short, or the time is configured - // to be less than the duration of the 'during' commands, - // queue it up. - if (duringDone) - runCommands(cam.after, 'after', function() { }); - else - queueAfter = true; - }; - - req = http.request(cam.url, function(res) + processor.on('start', function() { runCommands(cam.during, 'during', function() { - if (queueAfter) + // Check if the processor has already finished and the + // 'after' commands should be run immediately. + if (queueAfterCommand) runCommands(cam.after, 'after', function() { }); else - duringDone = true; + duringCommandsDone = true; }); - - - 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) + processor.on('end', function() { - console.log(e); - cleanup(); + // If the stream is cut short, or the time is configured + // to be less than the duration of the 'during' commands, + // queue it up. + if (duringCommandsDone) + runCommands(cam.after, 'after', function() { }); + else + queueAfterCommand = true; }); - req.end(); + processor.run(); }); } }; \ No newline at end of file diff --git a/config.sample.js b/config.sample.js index 771591e..e08dc07 100644 --- a/config.sample.js +++ b/config.sample.js @@ -2,23 +2,23 @@ var foscam = require('./config-foscam'); var config = {}; config.port = 5705; -config.defaultTime = 5000; config.cams = { frontdoor: { - // You can specify any URL string here, the Foscam helper just makes it easier - // for compatible models. If you add your own config helper, please publish! - url: foscam.mjpegStream('10.138.1.10', 'viewer', 'verysecure'), - time: 5000, - - output: 'ffmpeg', - outputOptions: + processor: 'ffmpeg', + options: { + // You can specify any URL string here, the Foscam helper just makes it easier + // for compatible models. If you add your own config helper, please publish! + url: foscam.mjpegStream('10.138.1.10', 'viewer', 'verysecure'), + time: 5000, + inputFormat: 'mjpeg', outputFormat: 'avi', videoCodec: 'libx264', + filename: '[/srv/www/publiccam/]YYYY-MM-DD HH.mm.ss[/.avi]' } @@ -27,16 +27,16 @@ config.cams = backdoor: { - // You can use username:password@ in the URL to log in with basic - // authentication. Note that some cams, like Foscam, use Digest - // authentication which is not supported. For Foscam you can provide - // the login in the parameters instead. - url: 'http://viewer:verysecure@10.138.1.11/video.cgi', - time: 5000, - - output: 'mjpeg-split', - outputOptions: + processor: 'mjpeg-split', + options: { + // You can use username:password@ in the URL to log in with basic + // authentication. Note that some cams, like Foscam, use Digest + // authentication which is not supported. For Foscam you can provide + // the login in the parameters instead. + url: 'http://viewer:verysecure@10.138.1.11/video.cgi', + time: 5000, + filename: '[/srv/www/publiccam/]YYYY-MM-DD HH.mm.ss[/ .avi]' } } diff --git a/output-ffmpeg.js b/output-ffmpeg.js deleted file mode 100644 index 901f0c4..0000000 --- a/output-ffmpeg.js +++ /dev/null @@ -1,45 +0,0 @@ -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; \ No newline at end of file diff --git a/output-mjpeg-split.js b/output-mjpeg-split.js deleted file mode 100644 index 3a14d08..0000000 --- a/output-mjpeg-split.js +++ /dev/null @@ -1,44 +0,0 @@ -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; \ No newline at end of file diff --git a/output-raw.js b/output-raw.js deleted file mode 100644 index 33f5a5c..0000000 --- a/output-raw.js +++ /dev/null @@ -1,30 +0,0 @@ -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; \ No newline at end of file diff --git a/processor-ffmpeg.js b/processor-ffmpeg.js new file mode 100644 index 0000000..44cec76 --- /dev/null +++ b/processor-ffmpeg.js @@ -0,0 +1,55 @@ +var fs = require('fs'); +var util = require('util'); +var stream = require('stream'); + +var FfmpegCommand = require('fluent-ffmpeg'); + +var helpers = require('./helpers'); +var BaseProcessor = require('./baseprocessor'); + + +function FFMPEGProcessor() +{ + BaseProcessor.apply(this, arguments); +} + +util.inherits(FFMPEGProcessor, BaseProcessor); + + + +FFMPEGProcessor.prototype.run = function() +{ + var self = this; + var command = new FfmpegCommand(); + command + .input(this.cam.options.input) + .inputOptions(['-t ' + this.cam.options.time]); + + if (typeof(this.cam.options.inputFormat) !== 'undefined') + { + command.inputFormat(this.cam.options.inputFormat); + if (this.cam.options.inputFormat === 'mjpeg') + command.inputOption('-use_wallclock_as_timestamps 1'); + } + + command + .output(helpers.createVariableFilename(this.cam.options.filename, this.now, + { + camId: this.camId + })) + .videoCodec(this.cam.options.videoCodec) + .outputFormat(this.cam.options.outputFormat); + + command.on('end', function() + { + self.doEnd(); + }); + + self.doStart(); + command.run(); + + FFMPEGProcessor.super_.prototype.run.call(this); +} + + +module.exports = FFMPEGProcessor; \ No newline at end of file diff --git a/processor-http-ffmpeg.js b/processor-http-ffmpeg.js new file mode 100644 index 0000000..82dfc95 --- /dev/null +++ b/processor-http-ffmpeg.js @@ -0,0 +1,59 @@ +var fs = require('fs'); +var util = require('util'); +var stream = require('stream'); + +var FfmpegCommand = require('fluent-ffmpeg'); + +var helpers = require('./helpers'); +var BaseHTTPStreamProcessor = require('./basehttpstreamprocessor'); + + +function HTTPFFMPEGProcessor() +{ + BaseHTTPStreamProcessor.apply(this, arguments); +} + +util.inherits(HTTPFFMPEGProcessor, BaseHTTPStreamProcessor); + + + +HTTPFFMPEGProcessor.prototype.run = function() +{ + this.output = new stream.PassThrough(); + var command = new FfmpegCommand(); + command + .input(this.output) + .inputFormat(this.cam.options.inputFormat); + + if (this.cam.options.inputFormat === 'mjpeg') + command.inputOption('-use_wallclock_as_timestamps 1'); + + command + .output(helpers.createVariableFilename(this.cam.options.filename, this.now, + { + camId: this.camId + })) + .videoCodec(this.cam.options.videoCodec) + .outputFormat(this.cam.options.outputFormat) + .run(); + + HTTPFFMPEGProcessor.super_.prototype.run.call(this); +} + + +HTTPFFMPEGProcessor.prototype.getStream = function() +{ + return this.output; +} + + +HTTPFFMPEGProcessor.prototype.cleanup = function() +{ + if (this.output !== null) + { + this.output.end(); + this.output = null; + } +} + +module.exports = HTTPFFMPEGProcessor; \ No newline at end of file diff --git a/processor-http-mjpegsplit.js b/processor-http-mjpegsplit.js new file mode 100644 index 0000000..8ff0bf1 --- /dev/null +++ b/processor-http-mjpegsplit.js @@ -0,0 +1,64 @@ +var fs = require('fs'); +var util = require('util'); + +var MjpegConsumer = require('mjpeg-consumer'); +var FileOnWrite = require('file-on-write'); + +var helpers = require('./helpers'); +var BaseHTTPStreamProcessor = require('./basehttpstreamprocessor'); + +function HTTPMJPEGSplitProcessor() +{ + BaseHTTPStreamProcessor.apply(this, arguments); +} + +util.inherits(HTTPMJPEGSplitProcessor, BaseHTTPStreamProcessor); + + + +HTTPMJPEGSplitProcessor.prototype.run = function() +{ + var self = this; + var frameCounter = 0; + + this.output = new FileOnWrite({ + filename: function(data) + { + frameCounter++; + + return helpers.createVariableFilename(self.cam.options.filename, self.now, + { + camId: self.camId, + frame: frameCounter + }); + } + }); + + this.consumer = new MjpegConsumer(); + this.consumer.pipe(this.output); + + + HTTPMJPEGSplitProcessor.super_.prototype.run.call(this); +} + + +HTTPMJPEGSplitProcessor.prototype.getStream = function() +{ + return this.consumer; +} + +module.exports = HTTPMJPEGSplitProcessor; + + + + +HTTPMJPEGSplitProcessor.prototype.cleanup = function() +{ + if (this.consumer !== null) + { + this.consumer.end(); + this.consumer = null; + } +} + +module.exports = HTTPMJPEGSplitProcessor; \ No newline at end of file diff --git a/processor-http-raw.js b/processor-http-raw.js new file mode 100644 index 0000000..a8e1713 --- /dev/null +++ b/processor-http-raw.js @@ -0,0 +1,46 @@ +var fs = require('fs'); +var util = require('util'); + +var helpers = require('./helpers'); +var BaseHTTPStreamProcessor = require('./basehttpstreamprocessor'); + +function HTTPRawProcessor() +{ + BaseHTTPStreamProcessor.apply(this, arguments); +} + +util.inherits(HTTPRawProcessor, BaseHTTPStreamProcessor); + + + +HTTPRawProcessor.prototype.run = function() +{ + this.output = fs.createWriteStream(helpers.createVariableFilename(this.cam.options.filename, this.now, + { + camId: this.camId + })); + + HTTPRawProcessor.super_.prototype.run.call(this); +} + + +HTTPRawProcessor.prototype.getStream = function() +{ + return this.output; +} + +module.exports = HTTPRawProcessor; + + + + +HTTPRawProcessor.prototype.cleanup = function() +{ + if (this.output !== null) + { + this.output.end(); + this.output = null; + } +} + +module.exports = HTTPRawProcessor; \ No newline at end of file