I have a camera which sends JPEG images to a webserver through a continuous multipart http stream. When I visit the IP address of the stream, the browser reads this stream as a series of images which mimics a video. I am wanting to download the files from this stream to a remote server.
I do not know how to parse the stream and the save the files to either my ubuntu server directly, or through my ruby on rails application filesystem.
Here is how the browser sees the stream:
Response Headers:
HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=frame
Request Headers:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cache-Control: max-age=0
Connection: keep-alive
DNT: 1
Host: my-ip-address
Please help me find the correct approach to this problem.
You can use ffmpeg to download a video stream from a continues video stream. Since you are using ubuntu, you can do it by simply running a command in your terminal and save the stream to your remote server. Following command is a sample ffmpeg command to save a live stream to your local disk.
ffmpeg.exe -y -i http://stream2.cnmns.net/hope-mp3 hopestream-latest.mp3
In above command -i indicates URL to be recorded. "hopestream-latest.mp3" is the output mp3 file. You can replace this with your remote server file path.
I don't have a sample server which does so. I made one myself and tried to test the solution.
const request = require('request');
const fs = require('fs')
var boundary = "";
var first = null;
var last_image = "";
let next_type = 3;
let content_length = -1;
let content_type = '';
request.get({
url: "http://localhost:9192/online.png",
forever: true,
headers: {
'referer': 'http://localhost:9192/'
},
// encoding: 'utf-8'
}
)
.on('error', (err) =>
console.log(err)
).on('response', (resp) => {
// console.log(resp)
boundary = resp.headers['content-type'].split('boundary=')[1]
// 0 - data
// 1 - content-type
// 2 - content-length
// 3 - boundary
// 4 - blank line
resp.on('data', (data)=> {
switch (next_type) {
case 0:
if (data.length + last_image.length == content_length)
{
last_image = data;
next_type = 3
} else {
last_image += data;
}
break;
case 1:
if (data.toString() == "\r\n")
{
next_type = 3
} else {
content_type = data.toString().toLowerCase().split("content-type:")[1].trim()
next_type = 2
}
break;
case 2:
content_length = parseInt(data.toString().toLowerCase().split("content-length:")[1].trim())
next_type =4
break;
case 3:
// we have got a boundary
next_type = 1;
if (last_image) {
fs.writeFileSync("image.png", last_image)
}
console.log(last_image)
last_image = ""
break;
case 4:
next_type = 0;
break;
}
})
})
This is node
, since you were open to non ROR solutions also. Below is the test server I had used
streamServer.js
/* Real-Time PNG-Streaming HTTP User Counter
Copyright Drew Gottlieb, 2012
Free for any use, but don't claim
that this is your work.
Doesn't work on Windows because
node-canvas only works on Linux and OSX. */
var moment = require('moment');
var http = require('http');
var _ = require('underscore');
var Backbone = require('backbone');
var Canvas = require('canvas');
var config = {
port: 9192,
host: "0.0.0.0",
updateInterval: 3000, // 5 seconds
multipartBoundary: "whyhellothere"
};
var Client = Backbone.Model.extend({
initialize: function() {
var req = this.get('req');
var res = this.get('res');
console.log("Page opened:", req.headers.referer);
res.on('close', _.bind(this.handleClose, this));
req.on('close', _.bind(this.handleClose, this));
this.sendInitialHeaders();
this.set('updateinterval', setInterval(_.bind(this.sendUpdate, this), config.updateInterval));
},
// Re-send the image in case it needs to be re-rendered
sendUpdate: function() {
if (this.get('sending')) return;
if (!this.get('imagecache')) return;
this.sendFrame(this.get('imagecache'));
},
// Sends the actual HTTP headers
sendInitialHeaders: function() {
this.set('sending', true);
var res = this.get('res');
res.writeHead(200, {
'Connection': 'Close',
'Expires': '-1',
'Last-Modified': moment().utc().format("ddd, DD MMM YYYY HH:mm:ss") + ' GMT',
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0, false',
'Pragma': 'no-cache',
'Content-Type': 'multipart/x-mixed-replace; boundary=--' + config.multipartBoundary
});
res.write("--" + config.multipartBoundary + "\r\n");
this.set('sending', false);
},
// Sends an image frame, followed by an empty part to flush the image through
sendFrame: function(image) {
this.set('sending', true);
this.set('imagecache', image);
var res = this.get('res');
res.write("Content-Type: image/png\r\n");
res.write("Content-Length: " + image.length + "\r\n");
res.write("\r\n");
res.write(image);
res.write("--" + config.multipartBoundary + "\r\n");
res.write("\r\n");
res.write("--" + config.multipartBoundary + "\r\n");
this.set('sending', false);
},
// Handle a disconnect
handleClose: function() {
if (this.get('closed')) return;
this.set('closed', true);
console.log("Page closed:", this.get('req').headers.referer);
this.collection.remove(this);
clearInterval(this.get('updateinterval'));
}
});
var Clients = Backbone.Collection.extend({
model: Client,
initialize: function() {
this.on("add", this.countUpdated, this);
this.on("remove", this.countUpdated, this);
},
// Handle the client count changing
countUpdated: function() {
var image = this.generateUserCountImage(this.size());
this.each(function(client) {
client.sendFrame(image);
});
console.log("Connections:", this.size());
},
// Generate a new image
generateUserCountImage: function(count) {
var canvas = new Canvas(200, 30);
var ctx = canvas.getContext('2d');
// Background
ctx.fillStyle = "rgba(100, 149, 237, 0)";
ctx.fillRect(0, 0, 200, 30);
// Text
ctx.fillStyle = "rgb(0, 100, 0)";
ctx.font = "20px Impact";
ctx.fillText("Users online: " + count, 10, 20);
return canvas.toBuffer();
}
});
function handleRequest(req, res) {
switch (req.url) {
case '/':
case '/index.html':
showDemoPage(req, res);
break;
case '/online.png':
showImage(req, res);
break;
default:
show404(req, res);
break;
}
}
function showDemoPage(req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write("<h1>Users viewing this page:</h1>");
res.write("<img src=\"/online.png\" />");
res.write("<h5>(probably won't work on IE or Opera)</h5>");
res.end();
}
function showImage(req, res) {
// If this image is not embedded in a <img> tag, don't show it.
if (!req.headers.referer) {
res.writeHead(403, {'Content-Type': 'text/html'});
res.end("You can't view this image directly.");
return;
}
// Create a new client to handle this connection
clients.add({
req: req,
res: res
});
}
function show404(req, res) {
res.writeHead(404, {'Content-Type': 'text/html'});
res.end("<h1>not found</h1><br /><a href=\"/\">go home</a>");
}
// Ready, Set, Go!
var clients = new Clients();
http.createServer(handleRequest).listen(config.port, config.host);
console.log("Started.");
PS: Taken from https://gist.github.com/dag10/48e6d25415ca92318815
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With