Skip to content

Commit

Permalink
Added motion emitting and MJPEG saving
Browse files Browse the repository at this point in the history
  • Loading branch information
mjohnsullivan committed May 8, 2013
1 parent 49c5b73 commit 768f714
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 24 deletions.
18 changes: 17 additions & 1 deletion README.md
Expand Up @@ -3,6 +3,22 @@ axiscam

Axis (VAPIX) camera control in Node

Run
---

To run, make a settings.json file in the root folder:

```json
{
"url": "https://<user>:<passwd>@<addr>",
"name": "Name for the camera",
"motion": false
}
```

This provides the address and credentials for the camera, a name and whether to emit
any detected motion events.

API
---

Expand All @@ -26,7 +42,7 @@ Streams MJPEG
Creates a stream of javascript objects that represent a snapshot of the Axis camera's motion detection:

```javascript
{group: "0", level: 2, threshold: 10}
{group: 0, level: 2, threshold: 10}
```

```javascript
Expand Down
91 changes: 81 additions & 10 deletions lib/axis.js
Expand Up @@ -4,14 +4,75 @@

var url = require('url'),
util = require('util'),
events = require('events'),
_ = require('underscore'),
request = require('request'),
motion = require('./motion')
motion = require('./motion3')

var sizes = {small: {resolution: 'QCIF'}, medium: {resolution: '320x240'}, large: {resolution: '640x480'}}
var defaultSize = 'medium'

/**
* Returns an object for VAPIX image/video size that can be used in url.format
*/
var getSizeQuery = function(size) {
return sizes[_.contains(_.keys(sizes), size) ? size : defaultSize]
}

/**
* Wrapper for Axis camera VAPIX API
*
* url: the base URL for the camera
* name: a name that's associated with the camera
*/
var AxisCam = function(options) {
this.url = url.parse(options.url)
this.name = options.name
if (options.motion) { // Motion events off by default
this.detectMotion()
}
}
util.inherits(AxisCam, events.EventEmitter)

/**
* Starts motion detection and emits events when motion
* exceeds the threshold on the camera
*/
AxisCam.prototype.detectMotion = function() {
var that = this
var reconnectDelay = 60 * 1000
this.motionTimeoutId = null // Reset timeouts

// Set up the motion stream if one doesn't exist already
if (!this.motionStream) {
this.motionStream = motion.createStream()

this.motionStream.on('data', function(motion) {
if (motion.level < motion.threshold)
that.emit('motion', motion)
})
}

var httpStream = request({
url: this.buildURL('axis-cgi/motion/motiondata.cgi'),
strictSSL: false,
timeout: 5000
})

var startMotionTimeout = function(err) {
if (err) console.error(err)
if (!that.motionTimeoutId) { // Only start timeout if there isn't already one waiting
console.log('Restarting motion detection in ' + reconnectDelay/1000 + ' seconds')
that.motionTimeoutId = setTimeout(function() {
that.detectMotion()
}, reconnectDelay)
}
}

httpStream.on('error', startMotionTimeout)
httpStream.on('end', startMotionTimeout)

httpStream.pipe(this.motionStream)
}

AxisCam.prototype.systemTime = function(cb) {
Expand All @@ -26,16 +87,16 @@ AxisCam.prototype.buildURL = function(path, query) {
return url.format(_.extend({}, this.url, {pathname: path, query: query}))
}

AxisCam.prototype.createImageStream = function() {
AxisCam.prototype.createImageStream = function(size) {
return request({
url: this.buildURL('axis-cgi/jpg/image.cgi', sizes.medium),
url: this.buildURL('axis-cgi/jpg/image.cgi', getSizeQuery(size)),
strictSSL: false
})
}

AxisCam.prototype.createVideoStream = function() {
AxisCam.prototype.createVideoStream = function(size) {
return request({
url: this.buildURL('axis-cgi/mjpg/video.cgi', sizes.medium),
url: this.buildURL('axis-cgi/mjpg/video.cgi', getSizeQuery(size)),
strictSSL: false
})
}
Expand All @@ -47,11 +108,15 @@ AxisCam.prototype.createAudioStream = function() {
})
}

AxisCam.prototype.createMotionStream = function(cb) {
return request({
url: this.buildURL('axis-cgi/motion/motiondata.cgi'),
strictSSL: false
}).pipe(motion.createStream())
AxisCam.prototype.writeImages = function() {
var FileOnWrite = require("file-on-write")

var writer = new FileOnWrite({
path: './video',
ext: '.jpg'
})

this.createVideoStream().pipe(require('./mjpg-stream').createStream()).pipe(writer)
}

var createClient = exports.createClient = function(options) {
Expand All @@ -60,5 +125,11 @@ var createClient = exports.createClient = function(options) {

if (require.main === module) {
var axis = createClient(JSON.parse(require('fs').readFileSync(__dirname + '/../settings.json')))

axis.on('motion', function(data) {
console.log(data)
})

//axis.createImageStream().pipe(require('fs').createWriteStream('./image.jpg'))
axis.writeImages()
}
38 changes: 38 additions & 0 deletions lib/mjpg-stream.js
@@ -0,0 +1,38 @@
/**
* MJPEG decoder for Axis cameras
* Breaks the MJPEG stream into invidividual JPEG images
*/

var Transform = require('stream').Transform

Buffer.prototype.toByteArray = function () {
return Array.prototype.slice.call(this, 0)
}

var MJPGStream = function() {
Transform.call(this, {objectMode: true})
}

MJPGStream.prototype = Object.create(
Transform.prototype, { constructor: { value: MJPGStream }})

MJPGStream.prototype._transform = function(data, encoding, cb) {
// Step through the data, looking for start/end markers (SOI/EOI)
for (var i = 0; i < data.length - 1; i++) {
var num = data.readUInt16BE(i)
if (num === 0xffd8) {
this.imgBuff = []
}
else if (num === 0xffd9) {
this.push(new Buffer(this.imgBuff.concat([0xff, 0xd9])))
this.imgBuff = null
}
if (this.imgBuff)
this.imgBuff.push(data[i])
}
cb()
}

exports.createStream = function() {
return new MJPGStream()
}
3 changes: 2 additions & 1 deletion lib/motion.js
Expand Up @@ -20,7 +20,8 @@ MotionLevelStream.prototype.write = function(data) {
var motionMatch = motion.match(/group=(\d+);level=(\d+);threshold=(\d+)/)
that.emit('data', { group: Number(motionMatch[1]),
level: Number(motionMatch[2]),
threshold: Number(motionMatch[3]) })
threshold: Number(motionMatch[3]),
ts: new Date })
})
}

Expand Down
31 changes: 31 additions & 0 deletions lib/motion2.js
@@ -0,0 +1,31 @@
/**
* Parses out motion data from the Axis camera motion stream
* Implemented with the new Node streams using prototypes
*/
var Transform = require('stream').Transform

var MotionLevelStream = function() {
Transform.call(this, {objectMode: true})
}

MotionLevelStream.prototype = Object.create(
Transform.prototype, { constructor: { value: MotionLevelStream }})

MotionLevelStream.prototype._transform = function(data, encoding, cb) {
var that = this
// Important bit of the stream is: group=0;level=0;threshold=11;
var match = data.toString().match(/group=\d+;level=\d+;threshold=\d+/g)
if (match)
match.forEach(function(motion) {
var motionMatch = motion.match(/group=(\d+);level=(\d+);threshold=(\d+)/)
that.push({ group: Number(motionMatch[1]),
level: Number(motionMatch[2]),
threshold: Number(motionMatch[3]),
ts: new Date })
})
cb()
}

exports.createStream = function() {
return new MotionLevelStream()
}
32 changes: 32 additions & 0 deletions lib/motion3.js
@@ -0,0 +1,32 @@
/**
* Parses out motion data from the Axis camera motion stream
* Implemented with the new Node streams
*/
var stream = require('stream'),
util = require('util')

var MotionLevelStream = function() {
stream.Transform.call(this, {objectMode: true})

this._transform = function(data, encoding, cb) {
var that = this
// Important bit of the stream is: group=0;level=0;threshold=11;
var match = data.toString().match(/group=\d+;level=\d+;threshold=\d+/g)
if (match)
match.forEach(function(motion) {
var motionMatch = motion.match(/group=(\d+);level=(\d+);threshold=(\d+)/)
that.push({ group: Number(motionMatch[1]),
level: Number(motionMatch[2]),
threshold: Number(motionMatch[3]),
ts: new Date })
})
cb()
}

}

util.inherits(MotionLevelStream, stream.Transform)

exports.createStream = function() {
return new MotionLevelStream()
}
21 changes: 11 additions & 10 deletions lib/server.js
Expand Up @@ -10,22 +10,22 @@ var axis = require('./axis').createClient(JSON.parse(require('fs').readFileSync(

var handlers = {
'/': function(req, res) {
res.writeHead(200, {'Content-Type': 'application/json'});
res.writeHead(200, {'Content-Type': 'application/json'})
res.end(JSON.stringify({
version: require('../package.json').version,
time: new Date,
message: 'Welcome to AxisCam'
}));
}))
},
'/image': function(req, res) {
'/image': function(req, res, query) {
res.statusCode = 200
res.setHeader('Content-Type', 'img/jpeg')
axis.createImageStream().pipe(res)
axis.createImageStream(query.size).pipe(res)
},
'/video': function(req, res) {
'/video': function(req, res, query) {
res.statusCode = 200
res.setHeader('Content-Type', 'img/jpeg')
axis.createVideoStream().pipe(res)
axis.createVideoStream(query.size).pipe(res)
},
'/audio': function(req, res) {
res.statusCode = 200
Expand All @@ -35,23 +35,24 @@ var handlers = {
},
'/motion': function(req, res) {
res.statusCode = 200
res.setHeader('Content-Tyoe', 'text/plain')
res.setHeader('Content-Type', 'text/plain')
axis.createMotionStream().pipe(es.stringify()).pipe(res)
}
};
}

var route = function(req, res) {
var uri = url.parse(req.url, true)
var query = uri.query
var path = uri.pathname

if (handlers[path])
handlers[path](req, res)
handlers[path](req, res, query)
else {
// console.log('Invalid URL requested: ' + path);
res.writeHead(404, {'Content-Type': 'text/html'})
res.end('<h1>404</h1><p>Page not found</p>')
}
};
}

http.createServer(route).listen(1337, '127.0.0.1')

Expand Down
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -14,8 +14,9 @@
}
, "dependencies" :
{ "underscore" : "1.4.4"
, "request" : "2.16.6"
, "event-stream": "3.0.13"
, "request" : "2.20.0"
, "event-stream": "3.0.14"
, "file-on-write": "0.1.0"
}
, "devDependencies" :
{ "mocha" : "1.x"
Expand Down
26 changes: 26 additions & 0 deletions test/axis_test.js
Expand Up @@ -7,6 +7,8 @@ var assert = require('assert'),

describe('axis', function() {

this.timeout(5000)

beforeEach(function() {
this.axisCam = createClient(JSON.parse(require('fs').readFileSync('./settings.json')))
})
Expand All @@ -22,4 +24,28 @@ describe('axis', function() {
})
})

describe('createImageStream', function() {
it('should stream an image from an Axis camera', function(done) {
var imageStream = this.axisCam.createImageStream()
var soi = false
var buff

imageStream.on('data', function(data) {
// Test for JPEG SOI
if (!soi) {
assert.equal(data.readUInt16BE(0), 0xffd8)
soi = true
}
buff = data
})

imageStream.on('end', function(data) {
// Test for JPEG EOI
assert.equal(buff.readUInt16BE(buff.length - 2), 0xffd9)
done()
})

})
})

})

0 comments on commit 768f714

Please sign in to comment.