7 Commits

Autor SHA1 Mensagem Data
Bernhard Weißhuhn 641f1b12d2 Update public/js/nodecopter-stream.js
reword comment again
(I'm really just playing around with github online editing)
2012-12-12 12:26:17 +01:00
Bernhard Weißhuhn 117cbe745e Update public/js/nodecopter-stream.js
fix typo in comment
2012-12-12 12:22:10 +01:00
Bernhard K. Weisshuhn 58725f5341 add nodecopter-track.
This should be refactored to use @karlwestin refactored version and
made a separate module. Just checking in for @felixge.
2012-12-11 10:49:40 +01:00
Bernhard K. Weisshuhn dc720a20c2 add sniper image 2012-12-11 10:45:21 +01:00
Bernhard K. Weisshuhn eb2994355d add jsfeat 2012-12-11 10:44:39 +01:00
Bernhard K. Weisshuhn 7bfe53f958 preserveDrawingBuffer of webgl canvas to get contents 2012-12-11 10:43:31 +01:00
Bernhard K. Weisshuhn 247b8582fc add methods to get canvas and canvas contents 2012-12-11 10:42:42 +01:00
37 arquivos alterados com 656 adições e 821 exclusões
-2
Ver Arquivo
@@ -1,4 +1,2 @@
node_modules
dist/broadway.js
.*.swp
.DS_Store
-30
Ver Arquivo
@@ -1,30 +0,0 @@
{
"browser" : false,
"boss" : true,
"curly": true,
"debug": false,
"devel": true,
"eqeqeq": true,
"evil": false,
"forin": true,
"immed": true,
"indent": 4,
"jquery": true,
"laxbreak": false,
"laxcomma": true,
"newcap": true,
"noarg": false,
"noempty": true,
"nonew": true,
"nomen": false,
"onevar": true,
"plusplus": false,
"regexp": false,
"trailing": true,
"undef": true,
"sub": false,
"strict": true,
"globalstrict": true,
"white": true,
"node" : true
}
-49
Ver Arquivo
@@ -1,49 +0,0 @@
module.exports = function (grunt) {
'use strict';
// load all grunt tasks
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
jshint: {
options: {
jshintrc: '.jshintrc'
},
all: [
'Gruntfile.js',
'lib/{,*/}*.js',
'dist/nodecopter-stream.js'
]
},
uglify: {
dist: {
files: {
'dist/broadway.js' : [
'dist/vendor/broadway/glUtils.js',
'dist/vendor/broadway/util.js',
'dist/vendor/broadway/avc.js',
'dist/vendor/broadway/canvas.js',
'dist/nodecopter-stream.js'
]
}
}
},
concat: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
},
dist: {
src: [
'dist/vendor/broadway/sylvester.js',
'dist/vendor/broadway/avc-codec.js',
'dist/broadway.js'
],
dest: 'dist/nodecopter-client.js'
}
}
});
grunt.registerTask('default', ['jshint', 'uglify', 'concat']);
};
-57
Ver Arquivo
@@ -1,57 +0,0 @@
0.1.0 / 2012-10-20
==================
* Initial version, unreleased
0.2.0 / 2012-10-20
==================
* released as npm package dronestream
1.0.0 / 2012-12-15
==================
* add support for multiple browser clients
* add support for browser reloads
* reconnect to drone on failure
* add support for bare http sockers
* remove express dependency
* export as a module
* added examples for using in other applications
1.0.1 / 2012-12-16
==================
* update documentation
* add changelog
1.0.2 / 2012-12-27
==================
* use requestAnimationFrame() for rendering video frames
* cleaned up express example
* update ar-drone and buffy dependencies to latest versions
1.0.3 / 2013-03-19
==================
* add custom drone ip to server options
* fix static files on webserver
1.1.0 / 2013-08-07
==================
* add support for custom host and port to client
* support passing an existing video stream
* switch to grunt for building
* express fixes
* remove constants for strict mode
* bump ws and ar-drone dependencies
* add hook for postprocessors
1.1.1 / 2013-08-31
==================
* use cleaned up version of broadways avc.js, fixes errors about 'clip'
* switch to ardrone 0.2.0
+4 -32
Ver Arquivo
@@ -10,29 +10,6 @@ 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 to use
Please see the http.createServer and Express 3.0 examples in the 'examples' dir.
You attach the stream to your server like this:
```javascript
// in node:
//
// note that the 'server' object points to a server instance and NOT an express app.
require("dronestream").listen(server);
// if your drone is on a different IP
require("dronestream").listen(server, { ip: "192.168.2.155" });
```
We serve the client in the same manner as Socket.IO. Add a reference to
**/dronestream/nodecopter-client.js** in your template. Then attach the stream to a DOM node:
```html
<!-- on the client -->
<script src="/dronestream/nodecopter-client.js"></script>
<script>
// video canvas will auto-size to the DOM-node, or default to 640*360 if no size is set.
new NodecopterStream(document.getElementById("droneStream"));
</script>
```
## How it works
@@ -45,9 +22,10 @@ In the browser broadway takes care of the rendering of the WebGL canvas.
## Status
Node-dronestream has gained some stability in the last release. It attempts
to recover lost connections to the drone, and it handles multiple clients,
disconnections, etc. See "How to use" for API.
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
@@ -67,9 +45,3 @@ disconnections, etc. See "How to use" for API.
- Brian Leroux for being not content with the original solution and for
cleaning up the predecessor, nodecopter-stream.
- @karlwestin for picking up where I was to lazy to actually make this usable.
## Demo
Watch @felixge demoing node-dronestream live at german user group cgnjs:
http://www.youtube.com/watch?v=nwGNNMJt4mE&t=19m52
+82
Ver Arquivo
@@ -0,0 +1,82 @@
'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})
, sockets = []
, Parser = require('./lib/PaVEParser')
, arDrone = require('ar-drone')
;
function init() {
var tcpVideoStream = new arDrone.Client.PngStream.TcpVideoStream({timeout: 4000})
, p = new Parser();
console.log("Connecting to stream");
tcpVideoStream.connect(function () {
tcpVideoStream.pipe(p);
});
tcpVideoStream.on("error", function(err) {
console.log("There was an error: %s", err.message);
tcpVideoStream.end();
tcpVideoStream.emit("end");
init();
});
p.on('data', function (data) {
sockets.forEach(function(socket) {
socket.send(data, {binary: true});
});
});
}
init();
wss.on('connection', function (socket) {
sockets.push(socket);
socket.on("close", function() {
console.log("Closing socket");
sockets = sockets.filter(function(el) {
return el !== socket;
});
});
});
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'));
});
}
-5
Ver Arquivo
Diff do arquivo suprimido porque uma ou mais linhas são muito longas
-129
Ver Arquivo
@@ -1,129 +0,0 @@
/*jshint browser:true */
/*global Avc:true, YUVWebGLCanvas: true, Size: true, requestAnimationFrame:true */
/* requestAnimationFrame polyfill: */
(function (window) {
'use strict';
var lastTime = 0,
vendors = ['ms', 'moz', 'webkit', 'o'],
x,
length,
currTime,
timeToCall;
for (x = 0, length = vendors.length; x < length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[
vendors[x] + 'RequestAnimationFrame'
];
window.cancelAnimationFrame = window[
vendors[x] + 'CancelAnimationFrame'
] || window[vendors[x] + 'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function (callback, element) {
currTime = new Date().getTime();
timeToCall = Math.max(0, 16 - (currTime - lastTime));
lastTime = currTime + timeToCall;
return window.setTimeout(function () {
callback(currTime + timeToCall);
}, timeToCall);
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function (id) {
clearTimeout(id);
};
}
}(window));
/* NodeCopterStream: */
(function (window, document, undefined) {
'use strict';
var NS,
socket,
avc,
webGLCanvas,
width,
height,
callbackOnce = null;
function setupAvc() {
avc = new Avc();
avc.configure({
filter: 'original',
filterHorLuma: 'optimized',
filterVerLumaEdge: 'optimized',
getBoundaryStrengthsA: 'optimized'
});
avc.onPictureDecoded = handleDecodedFrame;
}
function handleNalUnits(message) {
avc.decode(new Uint8Array(message.data));
}
function handleDecodedFrame(buffer, bufWidth, bufHeight) {
var callback;
requestAnimationFrame(function () {
var lumaSize = bufWidth * bufHeight,
chromaSize = lumaSize >> 2;
webGLCanvas.YTexture.fill(buffer.subarray(0, lumaSize));
webGLCanvas.UTexture.fill(buffer.subarray(lumaSize, lumaSize + chromaSize));
webGLCanvas.VTexture.fill(buffer.subarray(lumaSize + chromaSize, lumaSize + 2 * chromaSize));
webGLCanvas.drawScene();
});
// call callback with Y portion (grayscale image)
if (null !== callbackOnce && width) {
callback = callbackOnce;
callbackOnce = null;
// decoded buffer size may be larger,
// so use subarray with actual dimensions
callback(buffer.subarray(0, width * height));
}
}
function setupCanvas(div) {
var canvas = document.createElement('canvas');
width = div.attributes.width ? div.attributes.width.value : 640;
height = div.attributes.height ? div.attributes.height.value : 360;
canvas.width = width;
canvas.height = height;
canvas.style.backgroundColor = "#333333";
div.appendChild(canvas);
webGLCanvas = new YUVWebGLCanvas(canvas, new Size(width, height));
}
NS = function (div, options) {
var hostname, port;
options = options || {};
hostname = options.hostname || window.document.location.hostname;
port = options.port || window.document.location.port;
setupCanvas(div);
setupAvc();
socket = new WebSocket(
'ws://' + hostname + ':' + port + '/dronestream'
);
socket.binaryType = 'arraybuffer';
socket.onmessage = handleNalUnits;
};
// enqueue callback oto be called with next (black&white) frame
NS.prototype.onNextFrame = function (callback) {
callbackOnce = callback;
};
window.NodecopterStream = NS;
}(window, document, undefined));
-285
Ver Arquivo
@@ -1,285 +0,0 @@
/**
* Requires: avc-codec.js
**/
assert (Module);
var Avc = (function avc() {
'use strict';
var MAX_STREAM_BUFFER_LENGTH = 1024 * 1024,
HEAP8 = Module.HEAP8,
HEAPU8 = Module.HEAPU8,
HEAP16 = Module.HEAP16,
HEAP32 = Module.HEAP32,
_h264bsdClip = Module._get_h264bsdClip(),
patches = {
"filter": {
name: "_h264bsdFilterPicture",
display: "Filter Picture",
original: "Original_h264bsdFilterPicture",
options: {
none: {display: "None", fn: function () {}},
original: {display: "Original", fn: null},
}
},
"filterHorLuma": {
name: "_FilterHorLuma",
display: "Filter Hor Luma",
original: "OriginalFilterHorLuma",
options: {
none: {display: "None", fn: function () {}},
original: {display: "Original", fn: null},
optimized: {display: "Optimized", fn: OptimizedFilterHorLuma}
}
},
"filterVerLumaEdge": {
name: "_FilterVerLumaEdge",
display: "Filter Ver Luma Edge",
original: "OriginalFilterVerLumaEdge",
options: {
none: {display: "None", fn: function () {}},
original: {display: "Original", fn: null},
optimized: {display: "Optimized", fn: OptimizedFilterVerLumaEdge}
}
},
"getBoundaryStrengthsA": {
name: "_GetBoundaryStrengthsA",
display: "Get Boundary Strengths",
original: "OriginalGetBoundaryStrengthsA",
options: {
none: {display: "None", fn: function () {}},
original: {display: "Original", fn: null},
optimized: {display: "Optimized", fn: OptimizedGetBoundaryStrengthsA}
}
}
},
origFn = {};
function clip(x, y, z) {
return z < x ? x : (z > y ? y : z);
}
function OptimizedGetBoundaryStrengthsA($mb, $bS) {
var $totalCoeff = $mb + 28;
var tc0 = HEAP16[$totalCoeff + 0 >> 1];
var tc1 = HEAP16[$totalCoeff + 2 >> 1];
var tc2 = HEAP16[$totalCoeff + 4 >> 1];
var tc3 = HEAP16[$totalCoeff + 6 >> 1];
var tc4 = HEAP16[$totalCoeff + 8 >> 1];
var tc5 = HEAP16[$totalCoeff + 10 >> 1];
var tc6 = HEAP16[$totalCoeff + 12 >> 1];
var tc7 = HEAP16[$totalCoeff + 14 >> 1];
var tc8 = HEAP16[$totalCoeff + 16 >> 1];
var tc9 = HEAP16[$totalCoeff + 18 >> 1];
var tc10 = HEAP16[$totalCoeff + 20 >> 1];
var tc11 = HEAP16[$totalCoeff + 22 >> 1];
var tc12 = HEAP16[$totalCoeff + 24 >> 1];
var tc13 = HEAP16[$totalCoeff + 26 >> 1];
var tc14 = HEAP16[$totalCoeff + 28 >> 1];
var tc15 = HEAP16[$totalCoeff + 30 >> 1];
HEAP32[$bS + 32 >> 2] = tc2 || tc0 ? 2 : 0;
HEAP32[$bS + 40 >> 2] = tc3 || tc1 ? 2 : 0;
HEAP32[$bS + 48 >> 2] = tc6 || tc4 ? 2 : 0;
HEAP32[$bS + 56 >> 2] = tc7 || tc5 ? 2 : 0;
HEAP32[$bS + 64 >> 2] = tc8 || tc2 ? 2 : 0;
HEAP32[$bS + 72 >> 2] = tc9 || tc3 ? 2 : 0;
HEAP32[$bS + 80 >> 2] = tc12 || tc6 ? 2 : 0;
HEAP32[$bS + 88 >> 2] = tc13 || tc7 ? 2 : 0;
HEAP32[$bS + 96 >> 2] = tc10 || tc8 ? 2 : 0;
HEAP32[$bS + 104 >> 2] = tc11 || tc9 ? 2 : 0;
HEAP32[$bS + 112 >> 2] = tc14 || tc12 ? 2 : 0;
HEAP32[$bS + 120 >> 2] = tc15 || tc13 ? 2 : 0;
HEAP32[$bS + 12 >> 2] = tc1 || tc0 ? 2 : 0;
HEAP32[$bS + 20 >> 2] = tc4 || tc1 ? 2 : 0;
HEAP32[$bS + 28 >> 2] = tc5 || tc4 ? 2 : 0;
HEAP32[$bS + 44 >> 2] = tc3 || tc2 ? 2 : 0;
HEAP32[$bS + 52 >> 2] = tc6 || tc3 ? 2 : 0;
HEAP32[$bS + 60 >> 2] = tc7 || tc6 ? 2 : 0;
HEAP32[$bS + 76 >> 2] = tc9 || tc8 ? 2 : 0;
HEAP32[$bS + 84 >> 2] = tc12 || tc9 ? 2 : 0;
HEAP32[$bS + 92 >> 2] = tc13 || tc12 ? 2 : 0;
HEAP32[$bS + 108 >> 2] = tc11 || tc10 ? 2 : 0;
HEAP32[$bS + 116 >> 2] = tc14 || tc11 ? 2 : 0;
HEAP32[$bS + 124 >> 2] = tc15 || tc14 ? 2 : 0;
}
function OptimizedFilterVerLumaEdge ($data, bS, $thresholds, imageWidth) {
var delta, tc, tmp;
var p0, q0, p1, q1, p2, q2;
var tmpFlag;
var $clp = _h264bsdClip + 512;
var alpha = HEAP32[$thresholds + 4 >> 2];
var beta = HEAP32[$thresholds + 8 >> 2];
var val;
if (bS < 4) {
tmp = tc = HEAPU8[HEAP32[$thresholds >> 2] + (bS - 1)] & 255;
for (var i = 4; i > 0; i--) {
p1 = HEAPU8[$data + -2] & 255;
p0 = HEAPU8[$data + -1] & 255;
q0 = HEAPU8[$data] & 255;
q1 = HEAPU8[$data + 1] & 255;
if (
(Math.abs(p0 - q0) < alpha) &&
(Math.abs(p1 - p0) < beta) &&
(Math.abs(q1 - q0) < beta)
) {
p2 = HEAPU8[$data - 3] & 255;
if (Math.abs(p2 - p0) < beta) {
val = (p2 + ((p0 + q0 + 1) >> 1) - (p1 << 1)) >> 1;
HEAP8[$data - 2] = p1 + clip(-tc, tc, val);
tmp++;
}
q2 = HEAPU8[$data + 2] & 255;
if (Math.abs(q2 - q0) < beta) {
val = (q2 + ((p0 + q0 + 1) >> 1) - (q1 << 1)) >> 1;
HEAP8[$data + 1] = (q1 + clip(-tc, tc, val));
tmp++;
}
val = ((((q0 - p0) << 2) + (p1 - q1) + 4) >> 3);
delta = clip(-tmp, tmp, val);
p0 = HEAPU8[$clp + (p0 + delta)] & 255;
q0 = HEAPU8[$clp + (q0 - delta)] & 255;
tmp = tc;
HEAP8[$data - 1] = p0;
HEAP8[$data] = q0;
$data += imageWidth;
}
}
} else {
origFn.OriginalFilterVerLumaEdge($data, bS, $thresholds, imageWidth);
}
}
/**
* Filter all four successive horizontal 4-pixel luma edges. This can be
* done when bS is equal to all four edges.
*/
function OptimizedFilterHorLuma ($data, bS, $thresholds, imageWidth) {
var delta, tc, tmp;
var p0, q0, p1, q1, p2, q2;
var tmpFlag;
var $clp = _h264bsdClip + 512;
var alpha = HEAP32[$thresholds + 4 >> 2];
var beta = HEAP32[$thresholds + 8 >> 2];
var val;
if (bS < 4) {
tmp = tc = HEAPU8[HEAP32[$thresholds >> 2] + (bS - 1)] & 255;
for (var i = 16; i > 0; i--) {
p1 = HEAPU8[$data + (-imageWidth << 1)] & 255;
p0 = HEAPU8[$data + -imageWidth] & 255;
q0 = HEAPU8[$data] & 255;
q1 = HEAPU8[$data + imageWidth] & 255;
if (
(Math.abs(p0 - q0) < alpha) &&
(Math.abs(p1 - p0) < beta) &&
(Math.abs(q1 - q0) < beta)
) {
p2 = HEAPU8[$data + (-imageWidth * 3)] & 255;
if (Math.abs(p2 - p0) < beta) {
val = (p2 + ((p0 + q0 + 1) >> 1) - (p1 << 1)) >> 1;
HEAP8[$data + (-imageWidth << 1)] = p1 + clip(-tc, tc, val);
tmp++;
}
q2 = HEAPU8[$data + (imageWidth << 2)] & 255;
if (Math.abs(q2 - q0) < beta) {
val = (q2 + ((p0 + q0 + 1) >> 1) - (q1 << 1)) >> 1;
HEAP8[$data + imageWidth] = (q1 + clip(-tc, tc, val));
tmp++;
}
val = ((((q0 - p0) << 2) + (p1 - q1) + 4) >> 3);
delta = clip(-tmp, tmp, val);
p0 = HEAPU8[$clp + (p0 + delta)] & 255;
q0 = HEAPU8[$clp + (q0 - delta)] & 255;
tmp = tc;
HEAP8[$data - imageWidth] = p0;
HEAP8[$data] = q0;
$data ++;
}
}
} else {
origFn.OriginalFilterHorLuma($data, bS, $thresholds, imageWidth);
}
}
function patchOptimizations(config, patches) {
for (var name in patches) {
var patch = patches[name];
if (patch) {
var option = config[name];
if (!option) option = "original";
console.info(name + ": " + option);
assert (option in patch.options);
var fn = patch.options[option].fn;
if (fn) {
origFn[patch.original] = Module.patch(null, patch.name, fn);
console.info("Patching: " + patch.name + ", with: " + option);
}
}
}
}
function constructor() {
Module._broadwayInit();
this.streamBuffer = toU8Array(Module._broadwayCreateStream(MAX_STREAM_BUFFER_LENGTH), MAX_STREAM_BUFFER_LENGTH);
this.pictureBuffers = {};
this.onPictureDecoded = function (buffer, width, height) {
// console.info(buffer.length);
};
Module.patch(null, "_broadwayOnHeadersDecoded", function (x,y,z) {});
Module.patch(null, "_broadwayOnPictureDecoded", function ($buffer, width, height) {
var buffer = this.pictureBuffers[$buffer];
if (!buffer) {
buffer = this.pictureBuffers[$buffer] = toU8Array($buffer, (width * height * 3) / 2);
}
this.onPictureDecoded(buffer, width, height);
}.bind(this));
}
/**
* Creates a typed array from a HEAP8 pointer.
*/
function toU8Array(ptr, length) {
return HEAPU8.subarray(ptr, ptr + length);
}
constructor.prototype = {
/**
* Decodes a stream buffer. This may be one single (unframed) NAL unit
* without the start code, or a sequence of NAL units with framing start
* code prefixes. This function overwrites stream buffer allocated by
* the codec with the supplied buffer.
*/
decode: function decode(buffer) {
// console.info("Decoding: " + buffer.length);
this.streamBuffer.set(buffer);
Module._broadwaySetStreamLength(buffer.length);
Module._broadwayPlayStream();
},
configure: function (config) {
patchOptimizations(config, patches);
console.info("Broadway Configured: " + JSON.stringify(config));
}
};
return constructor;
})();
-10
Ver Arquivo
@@ -1,10 +0,0 @@
var http = require("http"),
drone = require("../../index");
var server = http.createServer(function(req, res) {
require("fs").createReadStream(__dirname + "/index.html").pipe(res);
});
drone.listen(server);
server.listen(5555);
-16
Ver Arquivo
@@ -1,16 +0,0 @@
<!doctype html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Stream as module</title>
<script src="/dronestream/nodecopter-client.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<h1 id="heading">Stream through a normal require("http").createServer</h1>
<div id="droneStream" style="width: 640px; height: 360px"> </div>
<script type="text/javascript" charset="utf-8">
new NodecopterStream(document.getElementById("droneStream"));
</script>
</body>
</html>
-33
Ver Arquivo
@@ -1,33 +0,0 @@
var express = require('express')
, routes = require('./routes')
, app = express()
, path = require('path')
, server = require("http").createServer(app)
;
app.configure(function () {
app.set('views', __dirname + '/views');
app.set('view engine', 'jade', { pretty: true });
app.use(express.favicon());
app.use(express.logger('dev'));
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);
/*
* Important:
*
* pass in the server object to listen, not the express app
* call 'listen' on the server, not the express app
*/
// should be require("dronestream").listen(server);
require("../../index").listen(server);
server.listen(3000);
-13
Ver Arquivo
@@ -1,13 +0,0 @@
{
"name": "dronestream-example",
"version": "0.1.1",
"dependencies": {
"express": "3.0.x",
"jade": "*"
},
"scripts": {
"start": "node app"
},
"author": "Bernhard K. Weisshuhn <bkw@codingforce.com>",
"license": "BSD"
}
-22
Ver Arquivo
@@ -1,22 +0,0 @@
extends layout
block append head
script(type='text/javascript', src='/dronestream/nodecopter-client.js')
-# for developing the client, use those url:s
-# script(type='text/javascript', src='/dronestream/vendor/broadway/sylvester.js')
-# script(type='text/javascript', src='/dronestream/vendor/broadway/glUtils.js')
-# script(type='text/javascript', src='/dronestream/vendor/broadway/util.js')
-# script(type='text/javascript', src='/dronestream/vendor/broadway/avc-codec.js')
-# script(type='text/javascript', src='/dronestream/vendor/broadway/avc.js')
-# script(type='text/javascript', src='/dronestream/vendor/broadway/canvas.js')
-# script(type='text/javascript', src='/dronestream/nodecopter-stream.js')
-# concatenated version of client
block append bodyscripts
script.
var copterStream = new NodecopterStream(document.querySelector('#dronestream'));
block content
div#dronestream(width=640, height=360)
-1
Ver Arquivo
@@ -1 +0,0 @@
module.exports = require("./lib/server");
-56
Ver Arquivo
@@ -1,56 +0,0 @@
/*
* Drone Stream listen:
* Takes a) a port number or b) a server object (node http or express, etc);
*/
var staticDir = 'dronestream',
check = new RegExp('^/' + staticDir, 'i'),
dist = __dirname + '/../dist';
module.exports.listen = function listen(server, options) {
'use strict';
var port, oldHandlers;
if (typeof server === 'number') {
port = server;
server = require('http').createServer();
server.listen(port);
}
/*
* Serving up the static files needed
*/
oldHandlers = server.listeners('request').splice(0);
server.removeAllListeners('request');
server.on('request', function (req, res) {
var i = 0;
if (handler(req, res)) {
return;
}
for (; i < oldHandlers.length; ++i) {
oldHandlers[i].call(server, req, res);
}
});
function handler(req, res, next) {
var path, read;
if (!check.test(req.url)) {
return false;
}
path = dist + req.url.replace(check, '');
console.log('checking static path: %s', path);
read = require('fs').createReadStream(path);
read.pipe(res);
read.on('error', function (e) {
console.log('Stream error: %s', e.message);
});
return true;
}
/*
* Connecting stream + websocket server
*/
return require('./stream').attach(server, options);
};
-62
Ver Arquivo
@@ -1,62 +0,0 @@
/*jshint node:true*/
/*
* Sets up a real stream + attaches it to a server
*/
module.exports.attach = function droneStream(server, options) {
'use strict';
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer({server: server, path: '/dronestream'}),
sockets = [],
Parser = require('./PaVEParser'),
arDrone = require('ar-drone');
options = options || {};
options.timeout = options.timeout || 4000;
function init() {
var tcpVideoStream, parser;
if (!options.tcpVideoStream) {
tcpVideoStream = new arDrone.Client.PngStream.TcpVideoStream(
options
);
console.log(
"Connecting to drone on %s", options.ip || "192.168.1.1"
);
tcpVideoStream.connect();
tcpVideoStream.on('error', function (err) {
console.log('There was an error: %s', err.message);
tcpVideoStream.end();
tcpVideoStream.emit("end");
init();
});
} else {
tcpVideoStream = options.tcpVideoStream;
}
parser = new Parser();
tcpVideoStream.on('data', function (data) {
parser.write(data);
});
parser.on('data', function (data) {
sockets.forEach(function (socket) {
socket.send(data, {binary: true});
});
});
}
init();
wss.on('connection', function (socket) {
sockets.push(socket);
socket.on("close", function () {
console.log("Closing socket");
sockets = sockets.filter(function (el) {
return el !== socket;
});
});
});
};
+9 -18
Ver Arquivo
@@ -1,12 +1,11 @@
{
"name": "dronestream",
"description": "video live stream from your parrot ar.drone 2.0 to your browser in pure javascript",
"version": "1.1.1",
"version": "0.2.0",
"repository": {
"type": "git",
"url": "git@github.com:bkw/node-dronestream.git"
},
"main": "index",
"keywords": [
"drone",
"nodecopter",
@@ -16,24 +15,16 @@
"browser",
"x264"
],
"dependencies": {
"ws": "~0.4.27",
"ar-drone": "0.2.0",
"buffy": "0.0.5"
"scripts": {
"start": "node app"
},
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-uglify": "~0.2.2",
"grunt-contrib-jshint": "~0.6.2",
"matchdep": "~0.1.2",
"grunt-contrib-concat": "~0.3.0"
"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>",
"contributors": [
{
"name": "Karl Westin",
"email": "karl.westin@gmail.com"
}
],
"license": "BSD"
}
Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 5.5 KiB

+84
Ver Arquivo
@@ -0,0 +1,84 @@
/*jshint browser:true */
/*global Avc:true, YUVWebGLCanvas: true, Size: true */
(function (window, document, undefined) {
'use strict';
var NS,
socket,
avc,
webGLCanvas,
width,
height;
function setupAvc() {
avc = new Avc();
avc.configure({
filter: 'original',
filterHorLuma: 'optimized',
filterVerLumaEdge: 'optimized',
getBoundaryStrengthsA: 'optimized'
});
avc.onPictureDecoded = handleDecodedFrame;
}
function handleNalUnits(message) {
avc.decode(new Uint8Array(message.data));
}
function handleDecodedFrame(buffer, width, height) {
var lumaSize = width * height,
chromaSize = lumaSize >> 2;
webGLCanvas.YTexture.fill(buffer.subarray(0, lumaSize));
webGLCanvas.UTexture.fill(buffer.subarray(lumaSize, lumaSize + chromaSize));
webGLCanvas.VTexture.fill(buffer.subarray(lumaSize + chromaSize, lumaSize + 2 * chromaSize));
webGLCanvas.drawScene();
}
function setupCanvas(div) {
var canvas = document.createElement('canvas');
width = div.attributes.width ? div.attributes.width.value : 640;
height = div.attributes.height ? div.attributes.height.value : 360;
canvas.width = width;
canvas.height = height;
canvas.style.backgroundColor = "#333333";
div.appendChild(canvas);
webGLCanvas = new YUVWebGLCanvas(canvas, new Size(width, height));
}
NS = function (div) {
setupCanvas(div);
setupAvc();
socket = new WebSocket(
'ws://' +
window.document.location.hostname + ':' +
window.document.location.port
);
socket.binaryType = 'arraybuffer';
socket.onmessage = handleNalUnits;
};
NS.prototype.getImageData = function (rgbaData) {
var gl = webGLCanvas.gl;
gl.readPixels(
0, 0, width, height,
gl.RGBA, gl.UNSIGNED_BYTE,
rgbaData
);
// WebGL returns pixels upside down.
// Instead of wasting time by vertically flipping it now,
// we just leave it like it is and invert the coordinates later:
return;
};
NS.prototype.getCanvas = function () {
return webGLCanvas.canvas;
};
window.NodecopterStream = NS;
}(window, document, undefined));
+156
Ver Arquivo
@@ -0,0 +1,156 @@
/*jshint browser:true */
/*global jsfeat:true console:true */
(function (window, document, undefined) {
'use strict';
var NodecopterTrack,
lastTime;
function schedule (callback, element) {
var requestAnimationFrame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback, element) {
var currTime = new Date().getTime(),
timeToCall = Math.max(0, 16 - (currTime - lastTime)),
id = window.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
return requestAnimationFrame.call(window, callback, element);
}
var relMouseCoords = function (event) {
var totalOffsetX = 0,
totalOffsetY = 0,
canvasX = 0,
canvasY = 0,
currentElement = this;
do {
totalOffsetX += currentElement.offsetLeft - currentElement.scrollLeft;
totalOffsetY += currentElement.offsetTop - currentElement.scrollTop;
} while (currentElement = currentElement.offsetParent);
canvasX = event.pageX - totalOffsetX;
canvasY = event.pageY - totalOffsetY;
return {x:canvasX, y:canvasY};
};
NodecopterTrack = function (copterStream, imgId) {
var tracker = this;
this.curr_img_pyr = new jsfeat.pyramid_t(3);
this.prev_img_pyr = new jsfeat.pyramid_t(3);
this.point_count = 0;
this.point_status = new Uint8Array(1);
this.prev_xy = new Float32Array(2);
this.curr_xy = new Float32Array(2);
this.copterStream = copterStream;
this.canvas = copterStream.getCanvas();
this.rgbaData = new Uint8Array(
this.canvas.width * this.canvas.height * 4
); // RGBA
this.crosshairs = document.querySelector(imgId);
this.curr_img_pyr.allocate(
this.canvas.width, this.canvas.height, jsfeat.U8_t | jsfeat.C1_t
);
this.prev_img_pyr.allocate(
this.canvas.width, this.canvas.height, jsfeat.U8_t | jsfeat.C1_t
);
this.canvas.addEventListener('click', function(event) {
tracker.canvasClickHandler(event);
}, false);
HTMLCanvasElement.prototype.relMouseCoords = relMouseCoords;
// this.canvas.prototype.relMouseCoords = relMouseCoords;
this.update();
};
NodecopterTrack.prototype.update = function () {
var _pt_xy, _pyr,
tracker = this;
schedule(function () {
tracker.update();
});
if (! this.point_count) {
this.crosshairs.style.display = 'none';
return;
}
_pt_xy = this.prev_xy;
_pyr = this.prev_img_pyr;
this.prev_xy = this.curr_xy;
this.curr_xy = _pt_xy;
this.prev_img_pyr = this.curr_img_pyr;
this.curr_img_pyr = _pyr; // reuse old pyramid data structure
this.copterStream.getImageData(this.rgbaData);
jsfeat.imgproc.grayscale(
this.rgbaData,
this.curr_img_pyr.data[0].data
);
// optional: enhance contrast:
jsfeat.imgproc.equalize_histogram(
this.curr_img_pyr.data[0].data,
this.curr_img_pyr.data[0].data
);
this.curr_img_pyr.build(this.curr_img_pyr.data[0], true);
jsfeat.optical_flow_lk.track(
this.prev_img_pyr,
this.curr_img_pyr,
this.prev_xy,
this.curr_xy,
1,
50, // win_size
30, // max_iterations
this.point_status,
0.01, // epsilon,
0.001 // min_eigen
);
if (this.point_status[0] == 1) {
this.crosshairs.style.left = (this.curr_xy[0] - 83) + 'px';
this.crosshairs.style.top = (
this.canvas.height - 83 - this.curr_xy[1]
) + 'px';
this.crosshairs.style.display = 'block';
} else {
this.point_count = 0;
console.log('lost target');
}
};
NodecopterTrack.prototype.canvasClickHandler = function (e) {
var coords = this.canvas.relMouseCoords(e);
if (
(coords.x > 0) &&
(coords.y > 0) &&
(coords.x < this.canvas.width) &&
(coords.y < this.canvas.height)
) {
this.curr_xy[0] = coords.x;
this.curr_xy[1] = this.canvas.height - coords.y;
this.point_count = 1;
}
console.log('Click:', coords);
};
window.NodecopterTrack = NodecopterTrack;
}(window, document, undefined));
+283
Ver Arquivo
@@ -0,0 +1,283 @@
/**
* Requires: avc-codec.js
**/
assert (Module);
HEAP8 = Module.HEAP8;
HEAPU8 = Module.HEAPU8;
HEAP16 = Module.HEAP16;
HEAP32 = Module.HEAP32;
_h264bsdClip = Module._get_h264bsdClip();
var Avc = (function avc() {
const MAX_STREAM_BUFFER_LENGTH = 1024 * 1024;
function constructor() {
Module._broadwayInit();
this.streamBuffer = toU8Array(Module._broadwayCreateStream(MAX_STREAM_BUFFER_LENGTH), MAX_STREAM_BUFFER_LENGTH);
this.pictureBuffers = {};
this.onPictureDecoded = function (buffer, width, height) {
// console.info(buffer.length);
}
Module.patch(null, "_broadwayOnHeadersDecoded", function () {
});
Module.patch(null, "_broadwayOnPictureDecoded", function ($buffer, width, height) {
var buffer = this.pictureBuffers[$buffer];
if (!buffer) {
buffer = this.pictureBuffers[$buffer] = toU8Array($buffer, (width * height * 3) / 2);
}
this.onPictureDecoded(buffer, width, height);
}.bind(this));
}
/**
* Creates a typed array from a HEAP8 pointer.
*/
function toU8Array(ptr, length) {
return HEAPU8.subarray(ptr, ptr + length);
}
constructor.prototype = {
/**
* Decodes a stream buffer. This may be one single (unframed) NAL unit without the
* start code, or a sequence of NAL units with framing start code prefixes. This
* function overwrites stream buffer allocated by the codec with the supplied buffer.
*/
decode: function decode(buffer) {
// console.info("Decoding: " + buffer.length);
this.streamBuffer.set(buffer);
Module._broadwaySetStreamLength(buffer.length);
Module._broadwayPlayStream();
},
configure: function (config) {
patchOptimizations(config, patches);
console.info("Broadway Configured: " + JSON.stringify(config));
}
};
return constructor;
})();
function patchOptimizations(config, patches) {
var scope = getGlobalScope();
for (var name in patches) {
var patch = patches[name];
if (patch) {
var option = config[name];
if (!option) option = "original";
console.info(name + ": " + option);
assert (option in patch.options);
var fn = patch.options[option].fn;
if (fn) {
scope[patch.original] = Module.patch(null, patch.name, fn);
console.info("Patching: " + patch.name + ", with: " + option);
}
}
}
}
var patches = {
"filter": {
name: "_h264bsdFilterPicture",
display: "Filter Picture",
original: "Original_h264bsdFilterPicture",
options: {
none: {display: "None", fn: function () {}},
original: {display: "Original", fn: null},
}
},
"filterHorLuma": {
name: "_FilterHorLuma",
display: "Filter Hor Luma",
original: "OriginalFilterHorLuma",
options: {
none: {display: "None", fn: function () {}},
original: {display: "Original", fn: null},
optimized: {display: "Optimized", fn: OptimizedFilterHorLuma}
}
},
"filterVerLumaEdge": {
name: "_FilterVerLumaEdge",
display: "Filter Ver Luma Edge",
original: "OriginalFilterVerLumaEdge",
options: {
none: {display: "None", fn: function () {}},
original: {display: "Original", fn: null},
optimized: {display: "Optimized", fn: OptimizedFilterVerLumaEdge}
}
},
"getBoundaryStrengthsA": {
name: "_GetBoundaryStrengthsA",
display: "Get Boundary Strengths",
original: "OriginalGetBoundaryStrengthsA",
options: {
none: {display: "None", fn: function () {}},
original: {display: "Original", fn: null},
optimized: {display: "Optimized", fn: OptimizedGetBoundaryStrengthsA}
}
}
};
function getGlobalScope() {
return function () { return this; }.call(null);
}
/* Optimizations */
function clip(x, y, z) {
return z < x ? x : (z > y ? y : z);
}
function OptimizedGetBoundaryStrengthsA($mb, $bS) {
var $totalCoeff = $mb + 28;
var tc0 = HEAP16[$totalCoeff + 0 >> 1];
var tc1 = HEAP16[$totalCoeff + 2 >> 1];
var tc2 = HEAP16[$totalCoeff + 4 >> 1];
var tc3 = HEAP16[$totalCoeff + 6 >> 1];
var tc4 = HEAP16[$totalCoeff + 8 >> 1];
var tc5 = HEAP16[$totalCoeff + 10 >> 1];
var tc6 = HEAP16[$totalCoeff + 12 >> 1];
var tc7 = HEAP16[$totalCoeff + 14 >> 1];
var tc8 = HEAP16[$totalCoeff + 16 >> 1];
var tc9 = HEAP16[$totalCoeff + 18 >> 1];
var tc10 = HEAP16[$totalCoeff + 20 >> 1];
var tc11 = HEAP16[$totalCoeff + 22 >> 1];
var tc12 = HEAP16[$totalCoeff + 24 >> 1];
var tc13 = HEAP16[$totalCoeff + 26 >> 1];
var tc14 = HEAP16[$totalCoeff + 28 >> 1];
var tc15 = HEAP16[$totalCoeff + 30 >> 1];
HEAP32[$bS + 32 >> 2] = tc2 || tc0 ? 2 : 0;
HEAP32[$bS + 40 >> 2] = tc3 || tc1 ? 2 : 0;
HEAP32[$bS + 48 >> 2] = tc6 || tc4 ? 2 : 0;
HEAP32[$bS + 56 >> 2] = tc7 || tc5 ? 2 : 0;
HEAP32[$bS + 64 >> 2] = tc8 || tc2 ? 2 : 0;
HEAP32[$bS + 72 >> 2] = tc9 || tc3 ? 2 : 0;
HEAP32[$bS + 80 >> 2] = tc12 || tc6 ? 2 : 0;
HEAP32[$bS + 88 >> 2] = tc13 || tc7 ? 2 : 0;
HEAP32[$bS + 96 >> 2] = tc10 || tc8 ? 2 : 0;
HEAP32[$bS + 104 >> 2] = tc11 || tc9 ? 2 : 0;
HEAP32[$bS + 112 >> 2] = tc14 || tc12 ? 2 : 0;
HEAP32[$bS + 120 >> 2] = tc15 || tc13 ? 2 : 0;
HEAP32[$bS + 12 >> 2] = tc1 || tc0 ? 2 : 0;
HEAP32[$bS + 20 >> 2] = tc4 || tc1 ? 2 : 0;
HEAP32[$bS + 28 >> 2] = tc5 || tc4 ? 2 : 0;
HEAP32[$bS + 44 >> 2] = tc3 || tc2 ? 2 : 0;
HEAP32[$bS + 52 >> 2] = tc6 || tc3 ? 2 : 0;
HEAP32[$bS + 60 >> 2] = tc7 || tc6 ? 2 : 0;
HEAP32[$bS + 76 >> 2] = tc9 || tc8 ? 2 : 0;
HEAP32[$bS + 84 >> 2] = tc12 || tc9 ? 2 : 0;
HEAP32[$bS + 92 >> 2] = tc13 || tc12 ? 2 : 0;
HEAP32[$bS + 108 >> 2] = tc11 || tc10 ? 2 : 0;
HEAP32[$bS + 116 >> 2] = tc14 || tc11 ? 2 : 0;
HEAP32[$bS + 124 >> 2] = tc15 || tc14 ? 2 : 0;
}
function OptimizedFilterVerLumaEdge ($data, bS, $thresholds, imageWidth) {
var delta, tc, tmp;
var p0, q0, p1, q1, p2, q2;
var tmpFlag;
var $clp = _h264bsdClip + 512;
var alpha = HEAP32[$thresholds + 4 >> 2];
var beta = HEAP32[$thresholds + 8 >> 2];
var val;
if (bS < 4) {
tmp = tc = HEAPU8[HEAP32[$thresholds >> 2] + (bS - 1)] & 255;
for (var i = 4; i > 0; i--) {
p1 = HEAPU8[$data + -2] & 255;
p0 = HEAPU8[$data + -1] & 255;
q0 = HEAPU8[$data] & 255;
q1 = HEAPU8[$data + 1] & 255;
if ((Math.abs(p0 - q0) < alpha) && (Math.abs(p1 - p0) < beta) && (Math.abs(q1 - q0) < beta)) {
p2 = HEAPU8[$data - 3] & 255;
if (Math.abs(p2 - p0) < beta) {
val = (p2 + ((p0 + q0 + 1) >> 1) - (p1 << 1)) >> 1;
HEAP8[$data - 2] = p1 + clip(-tc, tc, val);
tmp++;
}
q2 = HEAPU8[$data + 2] & 255;
if (Math.abs(q2 - q0) < beta) {
val = (q2 + ((p0 + q0 + 1) >> 1) - (q1 << 1)) >> 1;
HEAP8[$data + 1] = (q1 + clip(-tc, tc, val));
tmp++;
}
val = ((((q0 - p0) << 2) + (p1 - q1) + 4) >> 3);
delta = clip(-tmp, tmp, val);
p0 = HEAPU8[$clp + (p0 + delta)] & 255;
q0 = HEAPU8[$clp + (q0 - delta)] & 255;
tmp = tc;
HEAP8[$data - 1] = p0;
HEAP8[$data] = q0;
$data += imageWidth;
}
}
} else {
OriginalFilterVerLumaEdge($data, bS, $thresholds, imageWidth);
}
}
/**
* Filter all four successive horizontal 4-pixel luma edges. This can be done when bS is equal to all four edges.
*/
function OptimizedFilterHorLuma ($data, bS, $thresholds, imageWidth) {
var delta, tc, tmp;
var p0, q0, p1, q1, p2, q2;
var tmpFlag;
var $clp = _h264bsdClip + 512;
var alpha = HEAP32[$thresholds + 4 >> 2];
var beta = HEAP32[$thresholds + 8 >> 2];
var val;
if (bS < 4) {
tmp = tc = HEAPU8[HEAP32[$thresholds >> 2] + (bS - 1)] & 255;
for (var i = 16; i > 0; i--) {
p1 = HEAPU8[$data + (-imageWidth << 1)] & 255;
p0 = HEAPU8[$data + -imageWidth] & 255;
q0 = HEAPU8[$data] & 255;
q1 = HEAPU8[$data + imageWidth] & 255;
if ((Math.abs(p0 - q0) < alpha) && (Math.abs(p1 - p0) < beta) && (Math.abs(q1 - q0) < beta)) {
p2 = HEAPU8[$data + (-imageWidth * 3)] & 255;
if (Math.abs(p2 - p0) < beta) {
val = (p2 + ((p0 + q0 + 1) >> 1) - (p1 << 1)) >> 1;
HEAP8[$data + (-imageWidth << 1)] = p1 + clip(-tc, tc, val);
tmp++;
}
q2 = HEAPU8[$data + (imageWidth << 2)] & 255;
if (Math.abs(q2 - q0) < beta) {
val = (q2 + ((p0 + q0 + 1) >> 1) - (q1 << 1)) >> 1;
HEAP8[$data + imageWidth] = (q1 + clip(-tc, tc, val));
tmp++;
}
val = ((((q0 - p0) << 2) + (p1 - q1) + 4) >> 3);
delta = clip(-tmp, tmp, val);
p0 = HEAPU8[$clp + (p0 + delta)] & 255;
q0 = HEAPU8[$clp + (q0 - delta)] & 255;
tmp = tc;
HEAP8[$data - imageWidth] = p0;
HEAP8[$data] = q0;
$data ++;
}
}
} else {
OriginalFilterHorLuma($data, bS, $thresholds, imageWidth);
}
}
@@ -337,7 +337,7 @@ var WebGLCanvas = (function () {
},
onInitWebGL: function () {
try {
this.gl = this.canvas.getContext("experimental-webgl");
this.gl = this.canvas.getContext("experimental-webgl", {preserveDrawingBuffer: true});
} catch(e) {}
if (!this.gl) {
Diff do arquivo suprimido porque uma ou mais linhas são muito longas
Diff do arquivo suprimido porque uma ou mais linhas são muito longas
Diff do arquivo suprimido porque uma ou mais linhas são muito longas
+22
Ver Arquivo
@@ -0,0 +1,22 @@
extends layout
block append head
script(type='text/javascript', src='/js/vendor/broadway/sylvester.js')
script(type='text/javascript', src='/js/vendor/broadway/glUtils.js')
script(type='text/javascript', src='/js/vendor/broadway/util.js')
script(type='text/javascript', src='/js/vendor/broadway/avc-codec.js')
script(type='text/javascript', src='/js/vendor/broadway/avc.js')
script(type='text/javascript', src='/js/vendor/broadway/canvas.js')
script(type='text/javascript', src='/js/vendor/jsfeat/jsfeat-min.js')
script(type='text/javascript', src='/js/nodecopter-stream.js')
script(type='text/javascript', src='/js/nodecopter-track.js')
block append bodyscripts
script
'use strict';
var copterStream = new NodecopterStream(document.querySelector('#dronestream'));
var tracker = new NodecopterTrack(copterStream, '#sniper');
block content
div#dronestream(width=640, height=360, style="position:relative")
img#sniper(src="images/sniper.png", style="position:absolute; opacity:0.6")
@@ -7,6 +7,7 @@ html
title= title
link(rel='stylesheet', href='/css/normalize.min.css')
link(rel='stylesheet', href='/css/style.css')
script(src='/js/vendor/h5bp/modernizr-2.6.1-respond-1.1.0.min.js')
body
div.header-container
header.wrapper.clearfix