summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore18
-rw-r--r--README.md84
-rw-r--r--example/express-app.js19
-rw-r--r--example/public/index.html13
-rw-r--r--index.js4
-rw-r--r--mjpeg-proxy.js121
-rw-r--r--node-mjpeg-proxy.js49
-rw-r--r--package.json24
8 files changed, 260 insertions, 72 deletions
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 <legege@legege.com>
-## 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 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>Node MJPEG Proxy Example</title>
+</head>
+<body>
+
+<img src="index1.jpg" />
+<img src="index2.jpg" />
+
+</body>
+</html>
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"
+ }
+}