Skip to content

Commit

Permalink
Camera: implement "streaming" video (frame captures via piped/date ev…
Browse files Browse the repository at this point in the history
…ents)

Signed-off-by: Rick Waldron <waldron.rick@gmail.com>
  • Loading branch information
rwaldron committed Mar 17, 2016
1 parent 5b17ca9 commit 31ffce0
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 104 deletions.
204 changes: 142 additions & 62 deletions lib/camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ var fs = require('fs');
var Emitter = require('events').EventEmitter;
var Readable = require('stream').Readable;

var platform = process.platform;
var isDarwin = platform === 'darwin';
var stub = {
capture: function() {}
};

var priv = new Map();

function scale(value, fromLow, fromHigh, toLow, toHigh) {
return (value - fromLow) * (toHigh - toLow) /
(fromHigh - fromLow) + toLow;
}

function constrain(value, lower, upper) {
return Math.min(upper, Math.max(lower, value));
}

function CaptureStream() {
Readable.call(this);
this._read = function() {};
Expand All @@ -23,80 +26,166 @@ CaptureStream.prototype = Object.create(Readable.prototype, {
}
});

function FSWebcam(options) {
this.path = options.path;
this.quality = options.quality;
this.width = options.width;
this.height = options.height;
}

FSWebcam.prototype.capture = function(callback) {
// TODO: Stream this directly to capture stream/callback
var fswebcam = cp.spawn('fswebcam', [
'-i', '0', '-d', 'v4l2:' + this.path, '--no-banner', '--jpeg', this.quality, '--save', '/tmp/capture.jpg'
]);

fswebcam.on('close', function(code) {
var error = code === 0 ? null : error;

if (error) {
callback(error);
} else {
fs.readFile('/tmp/capture.jpg', callback);
}
});
};

function Camera(options) {
Emitter.call(this);

var device = isDarwin ?
(global.IS_TEST_ENV ? stub : require('bindings')('capture.node')) :
new FSWebcam(options);
options = options || {};

if (typeof options.stream === 'undefined') {
options.stream = false;
}

priv.set(this, {
device: device,
if (typeof options.pipe === 'undefined') {
options.pipe = true;
}

var state = {
isStreaming: false,
stream: options.stream,
pipe: options.pipe,
path: options.path,
quality: constrain(options.quality, 0, 1),
width: options.width,
height: options.height,
});
process: null,
};

priv.set(this, state);
}

Camera.prototype = Object.create(Emitter.prototype, {
constructor: {
value: Camera
},
isStreaming: {
get: function() {
return priv.get(this).isStreaming;
}
}
});

Camera.prototype.capture = function() {
Camera.prototype.stop = function() {
var state = priv.get(this);
state.process.kill('SIGTERM');
state.process = null;
};

Camera.prototype.capture = function(options) {
var state = priv.get(this);
var cs = new CaptureStream();
var buffer;

cs.on('data', function(result) {
this.emit('data', result);
}.bind(this));
options = options || {};

cs.on('end', function(result) {
this.emit('end', result);
}.bind(this));
if (typeof options.pipe !== 'undefined') {
state.pipe = options.pipe;
}

if (isDarwin) {
setImmediate(function() {
cs.push(state.device.capture());
cs.push(null);
});
if (typeof options.stream !== 'undefined') {
state.stream = options.stream;
}

var args = [
// Overwrite: yes
'-y',
// Logging: fatal
'-v', 'fatal',
'-r', '30',
'-i', state.path,
'-s', `${state.width}x${state.height}`,
'-q:v', scale(state.quality, 0, 1, 30, 1),
];

if (state.stream) {
args.push(
// '-r', '30',
// '-i', '/dev/video0',
// '-y',
// '-s', '320x240',
'-f', 'MJPEG',
// '-f', 'h264',
'-b:v', '128k',
'-r', '30',
'pipe:1'
);
} else {
state.device.capture(function(error, data) {
cs.push(data);
cs.push(null);
});
args.push(
// '-v', 'fatal',
// '-i', state.path,
// '-s', `${state.width}x${state.height}`,
// '-f', 'MJPEG',
// '-f', 'h264', '-vcodec',
// '-q:v', scale(state.quality, 0, 1, 30, 1),
'-f', 'MJPEG',
'-vframes', 1
);

if (state.pipe) {
args.push('pipe:1');
} else {
args.push('/tmp/capture.jpg');
}
}

var ffmpeg = cp.spawn('ffmpeg', args);

if (state.stream) {
state.isStreaming = true;
}

ffmpeg.stdout.on('data', (data) => {
if (state.isStreaming) {
cs.push(data);
this.emit('data', data);
} else {
buffer = data;
}
});

ffmpeg.on('close', (code) => {
var error = code === 0 ? null : error;

state.isStreaming = false;

if (error) {
this.emit('error', error);
} else {
if (state.pipe) {
this.emit('data', buffer);
cs.push(buffer);
cs.push(null);
} else {
// This path is only taken for still captures that
// would like to store the image on the disc.
//
// TODO: allow program specified target locations
//
fs.readFile('/tmp/capture.jpg', (error, data) => {
this.emit('data', data);
cs.push(data);
cs.push(null);
});
}
}
});

return cs;
};

Camera.prototype.stream = function() {
// TODO: Accept options/array of custom ffmpeg settings
return this.capture({
stream: true,
pipe: true
});
};

var cameraDefaults = {
width: 320,
height: 240,
quality: 100,
// 0-1 fractional percent. 1 => best, 0 => worst.
// maps to: ffmpeg -q:v 1-30 (Lower number is better)
quality: 1,
path: '/dev/video0',
};

Expand All @@ -106,13 +195,4 @@ module.exports = function(options) {

if (global.IS_TEST_ENV) {
module.exports.CaptureStream = CaptureStream;
module.exports.FSWebcam = FSWebcam;
module.exports.binding = global.IS_TEST_ENV ? stub : require('bindings')('capture.node');
module.exports.isDarwin = function(value) {
if (typeof value === 'undefined') {
return isDarwin;
} else {
isDarwin = value;
}
};
}
6 changes: 1 addition & 5 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ var install = require('./install');


module.exports = {
Camera: function(args) {
install('fswebcam');
return new Camera(args);
},
Camera: Camera,
Player: function(args) {
install('madplay');
return new Player(args);
Expand All @@ -19,7 +16,6 @@ module.exports = {
install('madplay');
return new Speaker(args);
},

// TODO...
Microphone: null,
};
2 changes: 2 additions & 0 deletions test/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
"stream": true,
"Emitter": true,
"Readable": true,
"Writable": true,
"bindings": true,
"sinon": true,
"MemoryStream": true,
"av": true,
"Camera": true,
"Player": true,
Expand Down
1 change: 1 addition & 0 deletions test/common/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ global.util = require('util');

global.Emitter = events.EventEmitter;
global.Readable = stream.Readable;
global.Writable = stream.Writable;


// Third Party
Expand Down

0 comments on commit 31ffce0

Please sign in to comment.