Skip to content

Commit

Permalink
import
Browse files Browse the repository at this point in the history
  • Loading branch information
bkw committed Oct 20, 2012
0 parents commit ccb09e7
Show file tree
Hide file tree
Showing 22 changed files with 1,606 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -0,0 +1,2 @@
node_modules
.*.swp
27 changes: 27 additions & 0 deletions LICENSE
@@ -0,0 +1,27 @@
Copyright (c) 2012 Bernhard K. Weisshuhn (bkw@codingforce.com) and contributors

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:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

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.

This code includes files from the Boradway.js project:
https://github.com/mbebenita/Broadway .

License and author information in included in the directory
public/js/vendor/broadway.


47 changes: 47 additions & 0 deletions README.md
@@ -0,0 +1,47 @@
# node-dronestream

Get a realtime live video stream from your
[Parrot AR Drone 2.0](http://ardrone2.parrot.com/) straight to your browser.

## Requirements

You'll needs a decent a decent and current browser and some cpu horsepower.
This code uses web-sockets and the incredibly awesome
[Broadway.js](https://github.com/mbebenita/Broadway) to render the video frames
in your browser using a WebGL canvas.


## How it works

The drone sends a proprietary video feed on 192.168.1.1 port 5555. This is
mostly a h264 baseline video, but adds custom framing. These frames are parsed
and mostly disposed of. The remaining h264 payload is split into NAL units and
sent to the browser via web sockets.

In the browser broadway takes care of the rendering of the WebGL canvas.

## Status

For this release I was exclusively interested in the lowest possible latency.
There is no error handling for the websockets, the connection to the drone or
the video player what-so-ever. This may come eventually, or may not. I think it
is enough to be used as a starting point for your own integration.

## Thanks

- Triple high fives to Felix 'felixge' Geisendörfer for getting the whole
NodeCopter movement started and being extremely helpful in the process of
getting this together.

- André 'zoddy' Kussmann for supplying the drone and allowing me to keep
hacking on it, even when he had to cancel the NodeCopter event for himself.

- Michael Bebenita, Alon Zakai, Andreas Gal and Mathieu 'p01' Henri for the
magic of Broadway.js

- Johann Phillip Strathausen for being a great team mate at NodeCopter 2012
Berlin.

- Brian Leroux for being not content with the original solution and for
cleaning up the predecessor, nodecopter-stream.

59 changes: 59 additions & 0 deletions app.js
@@ -0,0 +1,59 @@
'use strict';
/**
* Module dependencies.
*/

var express = require('express')
, routes = require('./routes')
, http = require('http')
, path = require('path')
, app = express()
, server = http.createServer(app)
, WebSocketServer = require('ws').Server
, wss = new WebSocketServer({server: server})
;

wss.on('connection', function (socket) {
var arDrone = require('ar-drone'),
tcpVideoStream = new arDrone.Client.PngStream.TcpVideoStream({timeout: 4000}),
Parser = require('./lib/PaVEParser'),
p = new Parser();

// TODO: handle client disconnect
p.on('data', function (data) {
socket.send(data, {binary: true});
});

tcpVideoStream.connect(function () {
tcpVideoStream.pipe(p);
});
});


app.configure(function () {
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade', { pretty: true });
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
});

app.configure('development', function () {
app.use(express.errorHandler());
app.locals.pretty = true;
});


app.get('/', routes.index);

if (module.parent) {
module.exports = server;
} else {
server.listen(app.get('port'), function () {
console.log("Express server listening on port " + app.get('port'));
});
}
123 changes: 123 additions & 0 deletions lib/PaVEParser.js
@@ -0,0 +1,123 @@
// Based on PaVEParser by Felix Geisendörfer
// https://github.com/felixge/node-ar-drone/blob/master/lib/video/PaVEParser.js

// The AR Drone 2.0 allows a tcp client to receive H264 (MPEG4.10 AVC) video
// from the drone. However, the frames are wrapped by Parrot Video
// Encapsulation (PaVE), which this class parses.
'use strict';

var util = require('util')
, Stream = require('stream').Stream
, buffy = require('buffy')
;

module.exports = PaVEParser;
util.inherits(PaVEParser, Stream);
function PaVEParser() {
Stream.call(this);

this.writable = true;
this.readable = true;

this._parser = buffy.createReader();
this._state = 'header';
this._toRead = undefined;
// TODO: search forward in buffer to last I-Frame
this._frame_type = undefined;
}

PaVEParser.HEADER_SIZE = 68; // 64 in older firmwares, but doesn't matter.

PaVEParser.prototype.write = function (buffer) {
var parser = this._parser
, signature
, header_size
, readable
;

parser.write(buffer);

while (true) {
switch (this._state) {
case 'header':
if (parser.bytesAhead() < PaVEParser.HEADER_SIZE) {
return true;
}
signature = parser.ascii(4);

if (signature !== 'PaVE') {
// TODO: wait/look for next PaVE frame
this.emit('error', new Error(
'Invalid signature: ' + JSON.stringify(signature)
));
return;
}

parser.skip(2);
header_size = parser.uint16LE();
// payload_size
this._toRead = parser.uint32LE();
// skip 18 bytes::
// encoded_stream_width 2
// encoded_stream_height 2
// display_width 2
// display_height 2
// frame_number 4
// timestamp 4
// total_chunks 1
// chunk_index 1
parser.skip(18);
this._frame_type = parser.uint8();

// bytes consumed so far: 4 + 2 + 2 + 4 + 18 + 1 = 31. Skip ahead.
parser.skip(header_size - 31);

this._state = 'payload';
break;

case 'payload':
readable = parser.bytesAhead();
if (readable < this._toRead) {
return true;
}

// also skip first NAL-Unit boundary: (4)
parser.skip(4);
this._toRead -= 4;
this.sendData(parser.buffer(this._toRead), this._frame_type);
this._toRead = undefined;
this._state = 'header';
break;
}
}
};


PaVEParser.prototype.sendData = function (data, frametype) {
var lastBegin = 0, i, l;
if (frametype === 1) {
// I-Frame, split more
// Todo: optimize.
for (i = 0, l = data.length - 4; i < l; i++) {
if (
data[i] === 0 &&
data[i + 1] === 0 &&
data[i + 2] === 0 &&
data[i + 3] === 1
) {
if (lastBegin < i) {
this.emit('data', data.slice(lastBegin, i));
lastBegin = i + 4;
i += 4;
}
}
}
this.emit('data', data.slice(lastBegin));
} else {
this.emit('data', data);
}
};

PaVEParser.prototype.end = function () {
// nothing to do, just here so pipe() does not complain
};
30 changes: 30 additions & 0 deletions package.json
@@ -0,0 +1,30 @@
{
"name": "node-dronestream",
"description": "video live stream from your parrot ar.drone 2.0 to your browser in pure javascript",
"version": "0.1.0",
"repository": {
"type": "git",
"url": "git@github.com:bkw/node-dronestream.git"
},
"keywords": [
"drone",
"nodecopter",
"parrot",
"video",
"stream",
"browser",
"x264"
],
"scripts": {
"start": "node app"
},
"dependencies": {
"express": "3.0.0rc5",
"jade": "*",
"ws": "~0.4.22",
"ar-drone": "0.0.3",
"buffy": "0.0.4"
},
"author": "Bernhard K. Weisshuhn <bkw@codingforce.com>",
"license": "BSD"
}
50 changes: 50 additions & 0 deletions public/css/normalize.min.css
@@ -0,0 +1,50 @@
/*! normalize.css v1.0.1 | MIT License | git.io/normalize */
article,aside,details,figcaption,figure,footer,header,hgroup,nav,section,summary{display:block}
audio,canvas,video{display:inline-block;*display:inline;*zoom:1}
audio:not([controls]){display:none;height:0}
[hidden]{display:none}
html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}
html,button,input,select,textarea{font-family:sans-serif}
body{margin:0}
a:focus{outline:thin dotted}
a:active,a:hover{outline:0}
h1{font-size:2em;margin:.67em 0}
h2{font-size:1.5em;margin:.83em 0}
h3{font-size:1.17em;margin:1em 0}
h4{font-size:1em;margin:1.33em 0}
h5{font-size:.83em;margin:1.67em 0}
h6{font-size:.75em;margin:2.33em 0}
abbr[title]{border-bottom:1px dotted}
b,strong{font-weight:bold}
blockquote{margin:1em 40px}
dfn{font-style:italic}
mark{background:#ff0;color:#000}
p,pre{margin:1em 0}
code,kbd,pre,samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:1em}
pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}
q{quotes:none}
q:before,q:after{content:'';content:none}
small{font-size:80%}
sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
sup{top:-0.5em}
sub{bottom:-0.25em}
dl,menu,ol,ul{margin:1em 0}
dd{margin:0 0 0 40px}
menu,ol,ul{padding:0 0 0 40px}
nav ul,nav ol{list-style:none;list-style-image:none}
img{border:0;-ms-interpolation-mode:bicubic}
svg:not(:root){overflow:hidden}
figure{margin:0}
form{margin:0}
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
legend{border:0;padding:0;white-space:normal;*margin-left:-7px}
button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}
button,input{line-height:normal}
button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;*overflow:visible}
button[disabled],input[disabled]{cursor:default}
input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*height:13px;*width:13px}
input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}
input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
textarea{overflow:auto;vertical-align:top}
table{border-collapse:collapse;border-spacing:0}
8 changes: 8 additions & 0 deletions public/css/style.css
@@ -0,0 +1,8 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}

a {
color: #00B7FF;
}

0 comments on commit ccb09e7

Please sign in to comment.