From 91736294567db46f2f309f53c42f287622e21591 Mon Sep 17 00:00:00 2001 From: Georges-Etienne Legendre Date: Mon, 4 Feb 2013 13:39:06 -0500 Subject: A module version of this application, with express.js --- .gitignore | 18 +++++++ README.md | 84 ++++++++++++++++++++++++-------- example/express-app.js | 19 ++++++++ example/public/index.html | 13 +++++ index.js | 4 -- mjpeg-proxy.js | 121 ++++++++++++++++++++++++++++++++++++++++++++++ node-mjpeg-proxy.js | 49 ------------------- package.json | 24 +++++++++ 8 files changed, 260 insertions(+), 72 deletions(-) create mode 100644 .gitignore create mode 100644 example/express-app.js create mode 100644 example/public/index.html delete mode 100644 index.js create mode 100644 mjpeg-proxy.js delete mode 100644 node-mjpeg-proxy.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9043e51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +lib-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz +*.swp +.DS_Store + +pids +logs +results +build + +node_modules +npm-debug.log diff --git a/README.md b/README.md index 8bfd2cf..ff44e68 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,87 @@ node-mjpeg-proxy ================ -This is a simple implementation of a MJPEG proxy written with node.js. +A node.js module to proxy MJPEG requests. Supports multiple client consuming a single stream. Fixes an iOS 6 issue with some MJPEG steams. -## Documentation +Installation +------------ + +From npm: + +``` bash +$ npm install mjpeg-proxy +``` + +From source: + +``` bash +$ git clone https://github.com/legege/node-mjpeg-proxy.git +$ cd node-mjpeg-proxy +$ npm install +``` + +Example +------- ### Example Usage - var mpjegproxy = require("./lib/node-mjpeg-proxy"); - mpjegproxy.createProxy("http://192.1.2.3:8080/videofeed"); +``` js +var MjpegProxy = require('mjpeg-proxy').MjpegProxy; +var express = require('express'); +var app = express(); + +app.get('/index1.jpg', new MjpegProxy('http://admin:admin@192.168.1.109/cgi/mjpg/mjpg.cgi').proxyRequest); +app.listen(8080); +``` + +Here, it will create a proxy to the source video feed (`http://admin:admin@192.168.1.109/cgi/mjpg/mjpg.cgi`). You can now access the feed at `http://localhost:8080/index1.jpg`. -Here, it will create a proxy to the source video feed (http://192.1.2.3:8080/videofeed) with the default options (below). You can now access the feed at http://localhost:5080/ . +API +--- -### Proxy +### MjpegProxy - Proxy.createProxy(sourceURL, [options]); +``` js +var mjpegProxy = new MjpegProxy(mjpegUrl); +``` -Returns: a `Proxy` instance. +Returns: a `MjpegProxy` instance for the MJPEG stream at `mjpegUrl` URL. -Arguments: +Credits +------- -- *sourceURL* +Original prototype version from: - The source URL of the MJPEG feed to be proxied. + * Phil Rene ([philrene](http://github.com/philrene)) + * Chris Chua ([chrisirhc](http://github.com/chrisirhc)) -Options: +License +------- -- *port* +(The MIT License) - The destination port. Defaults to `5080`. +Copyright (C) 2012, Georges-Etienne Legendre -## TODO +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the +following conditions: -- Add a resource URL so that it can serve on certain resource URLs rather than require its onw http.Server instance. +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. -## Credits +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. -- Phil Rene ([philrene](http://github.com/philrene)) +A different license may apply to other software included in this package, +including libftdi and libusb. Please consult their respective license files +for the terms of their individual licenses. -- Chris Chua ([chrisirhc](http://github.com/chrisirhc)) diff --git a/example/express-app.js b/example/express-app.js new file mode 100644 index 0000000..9342f66 --- /dev/null +++ b/example/express-app.js @@ -0,0 +1,19 @@ +var express = require('express'); +var MjpegProxy = require('../mjpeg-proxy').MjpegProxy; + +var cam1 = "http://admin:admin@192.168.124.54/cgi/mjpg/mjpg.cgi"; +var cam2 = "http://admin:@192.168.124.32/videostream.cgi"; + +var app = express(); + +app.set("view options", {layout: false}); +app.use(express.static(__dirname + '/public')); + +app.get('/', function(req, res) { + res.render('index.html'); +}); + +app.get('/index1.jpg', new MjpegProxy(cam1).proxyRequest); +app.get('/index2.jpg', new MjpegProxy(cam2).proxyRequest); + +app.listen(8080) \ No newline at end of file diff --git a/example/public/index.html b/example/public/index.html new file mode 100644 index 0000000..c7d3e9d --- /dev/null +++ b/example/public/index.html @@ -0,0 +1,13 @@ + + + + + Node MJPEG Proxy Example + + + + + + + + diff --git a/index.js b/index.js deleted file mode 100644 index 5284ba9..0000000 --- a/index.js +++ /dev/null @@ -1,4 +0,0 @@ -exports.Proxy = require('./node-mjpeg-proxy').Proxy; -exports.createProxy = function (srcURL, options) { - return new exports.Proxy(srcURL, options || {}); -}; diff --git a/mjpeg-proxy.js b/mjpeg-proxy.js new file mode 100644 index 0000000..6009e7d --- /dev/null +++ b/mjpeg-proxy.js @@ -0,0 +1,121 @@ +var url = require('url'); +var http = require('http'); + +require('buffertools'); + +function extractBoundary(contentType) { + var startIndex = contentType.indexOf('boundary='); + var endIndex = contentType.indexOf(';', startIndex); + if (endIndex == -1) { //boundary is the last option + // some servers, like mjpeg-streamer puts a '\r' character at the end of each line. + if ((endIndex = contentType.indexOf('\r', startIndex)) == -1) { + endIndex = contentType.length; + } + } + return contentType.substring(startIndex + 9, endIndex); +} + +var MjpegProxy = exports.MjpegProxy = function(mjpegUrl) { + var self = this; + + if (!mjpegUrl) throw new Error('Please provide a source MJPEG URL'); + + self.mjpegOptions = url.parse(mjpegUrl); + + self.audienceResponses = []; + self.newAudienceResponses = []; + + self.boundary = null; + self.globalMjpegResponse = null; + + self.proxyRequest = function(req, res) { + + // There is already another client consuming the MJPEG response + if (self.audienceResponses.length > 0) { + self._newClient(req, res); + } else { + // Send source MJPEG request + var mjpegRequest = http.request(self.mjpegOptions, function(mjpegResponse) { + self.globalMjpegResponse = mjpegResponse; + self.boundary = extractBoundary(mjpegResponse.headers['content-type']); + + self._newClient(req, res); + + var lastByte1 = null; + var lastByte2 = null; + + mjpegResponse.on('data', function(chunk) { + // Fix CRLF issue on iOS 6+: boundary should be preceded by CRLF. + if (lastByte1 != null && lastByte2 != null) { + var oldheader = '--' + self.boundary; + var p = chunk.indexOf(oldheader); // indexOf provided by buffertools + + if (p == 0 && !(lastByte2 == 0x0d && lastByte1 == 0x0a) || p > 1 && !(chunk[p - 2] == 0x0d && chunk[p - 1] == 0x0a)) { + var b1 = chunk.slice(0, p); + var b2 = new Buffer('\r\n--' + self.boundary); + var b3 = chunk.slice(p + oldheader.length); + chunk = Buffer.concat([b1, b2, b3]); + } + } + + lastByte1 = chunk[chunk.length - 1]; + lastByte2 = chunk[chunk.length - 2]; + + for (var i = self.audienceResponses.length; i--;) { + var res = self.audienceResponses[i]; + + // First time we push data... lets start at a boundary + if (self.newAudienceResponses.indexOf(res) >= 0) { + var p = chunk.indexOf('--' + self.boundary); // indexOf provided by buffertools + res.write(chunk.slice(p)); + + self.newAudienceResponses.splice(self.newAudienceResponses.indexOf(res), 1); // remove from new + } else { + res.write(chunk); + } + } + }); + mjpegResponse.on('end', function () { + // console.log("...end"); + for (var i = self.audienceResponses.length; i--;) { + var res = self.audienceResponses[i]; + res.end(); + } + }); + mjpegResponse.on('close', function () { + // console.log("...close"); + }); + }); + + mjpegRequest.on('error', function(e) { + console.log('problem with request: ', e); + }); + mjpegRequest.end(); + } + } + + self._newClient = function(req, res) { + res.writeHead(200, { + 'Expires': 'Mon, 01 Jul 1980 00:00:00 GMT', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Content-Type': 'multipart/x-mixed-replace;boundary=' + self.boundary + }); + + self.audienceResponses.push(res); + self.newAudienceResponses.push(res); + + res.socket.on('close', function () { + // console.log('exiting client!'); + + self.audienceResponses.splice(self.audienceResponses.indexOf(res), 1); + if (self.newAudienceResponses.indexOf(res) >= 0) { + self.newAudienceResponses.splice(self.newAudienceResponses.indexOf(res), 1); // remove from new + } + + if (self.audienceResponses.length == 0) { + self.globalMjpegResponse.destroy(); + } + }); + } +} \ No newline at end of file diff --git a/node-mjpeg-proxy.js b/node-mjpeg-proxy.js deleted file mode 100644 index 831ef55..0000000 --- a/node-mjpeg-proxy.js +++ /dev/null @@ -1,49 +0,0 @@ -/************************** -TODO: -1. Make it work with existing HTTP servers and listen to resource URLs -**************************/ - -var http = require('http'), -sys = require('sys'), -url = require('url'); - -Proxy = exports.Proxy = function (srcURL, options) { - if (!srcURL) throw new Error("Please provide a source feed URL"); - srcURL = url.parse(srcURL); - - var srcClient = http.createClient(srcURL.port || 80, srcURL.hostname); - - var audienceServer = options.audienceServer || http.createServer(); - var audienceServerPort = options.port || 5080; - var audienceClients = []; - - // Starting the stream on from the source - var request = srcClient.request('GET', srcURL.pathname + - (srcURL.search ? srcURL.search : ""), - {'host': srcURL.hostname}); - request.end(); - request.on('response', function (srcResponse) { - /** Setup Audience server listener **/ - audienceServer.on('request', function (req, res) { - /** Replicate the header from the source **/ - res.writeHead(200, srcResponse.headers); - /** Push the client into the client list **/ - audienceClients.push(res); - /** Clean up connections when they're dead **/ - res.socket.on('close', function () { - audienceClients.splice(audienceClients.indexOf(res), 1); - }); - }); - audienceServer.listen(audienceServerPort); - sys.puts('node-mjpeg-proxy server started on port 5080'); - - /** Send data to relevant clients **/ - srcResponse.setEncoding('binary'); - srcResponse.on('data', function (chunk) { - var i; - for (i = audienceClients.length; i--;) { - audienceClients[i].write(chunk, 'binary'); - } - }); - }); -}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..6694a5d --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "mjpeg-proxy", + "version": "0.0.1", + "description": "MJPEG Proxy", + "author": { + "name" : "Georges-Etienne Legendre", + "email": "legege@legege.com", + "url": "http://legege.com" + }, + "repository": { + "type": "git", + "url": "git://github.com/legege/node-mjpeg-proxy.git" + }, + "main": "./mjpeg-proxy", + "dependencies": { + "buffertools": "1.1.x" + }, + "devDependencies": { + "express": "3.1.x" + }, + "engines": { + "node": ">=0.8.0" + } +} -- cgit v1.2.1