API Docs for: 2.0.0

src/node-livevideodecoder.js

// SAGE2 is available for use under the SAGE2 Software License
//
// University of Illinois at Chicago's Electronic Visualization Laboratory (EVL)
// and University of Hawai'i at Manoa's Laboratory for Advanced Visualization and
// Applications (LAVA)
//
// See full text, terms and conditions in the LICENSE.txt included file
//
// Copyright (c) 2014

/**
 * Live decoding of video using the fluent-ffmpeg package (not used anymore)
 *
 * @module server
 * @submodule livevideodecoder
 * @requires fluent-ffmpeg
 */

// require variables to be declared
"use strict";

var exec   = require('child_process').exec;  // spawn a process and receive output
var ffmpeg = require('fluent-ffmpeg');       // ffmpeg video manipulator

/**
 * LiveVideoDecoder class
 *
 * @class LiveVideoDecoder
 * @constructor
 */
function LiveVideoDecoder(options) {
	this.options       = options;

	this.url           = null;
	this.width         = null;
	this.height        = null;
	this.numframes     = null;
	this.framerate     = null;
	this.frameSize     = null;

	this.yuvFrame      = null;
	this.frameIdx      = 0;

	this.decode        = null;
	this.startTime     = 0.0;
	this.playAfterSeek = false;

	this.onmetadata    = null;
	this.onstartdecode = null;
	this.onstopdecode  = null;
	this.onnewframe    = null;
}

/**
 *
 *
 * @method initializeLiveDecoder
 */
LiveVideoDecoder.prototype.initializeLiveDecoder = function(url) {
	var _this = this;

	this.url = url;
	var fURL = "\"" + url + "\""; // must quote around url in case there are spaces or other special characters
	var cmd = (this.options.ffmpegPath || "") + "ffprobe";
	exec(cmd + " -of json -show_streams -show_format " + fURL, function(error, stdout, stderr) {
		var data = JSON.parse(stdout);
		if (error) {
			throw error;
		}

		for (var i = 0; i < data.streams.length; i++) {
			if (data.streams[i].codec_type === "video") {
				_this.width     = data.streams[i].width  + (data.streams[i].width % 2); // ensure even width
				_this.height    = data.streams[i].height + (data.streams[i].height % 2); // ensure even height
				_this.numframes = data.streams[i].nb_frames;
				_this.frameSize = _this.width * _this.height * 1.5;

				var avg_framerate = data.streams[i].avg_frame_rate;
				var div = avg_framerate.indexOf("/");
				var numerator = avg_framerate.substring(0, div);
				var denominator = avg_framerate.substring(div + 1, avg_framerate.length);

				_this.framerate = numerator / denominator;

				break;
			}
		}

		if (_this.onmetadata instanceof Function) {
			_this.onmetadata(error, {
				width: _this.width, height: _this.height,
				numframes: _this.numframes,
				framerate: _this.framerate
			});
		}
	});
};

/**
 *
 *
 * @method startLiveDecoding
 */
LiveVideoDecoder.prototype.startLiveDecoding = function() {
	var _this = this;

	var readPosition = 0;
	var frameBuffer  = new Buffer(this.frameSize);

	var command = ffmpeg(this.url).native().seekInput(this.frameIdx / this.framerate).size(this.width + 'x' + this.height)
		.outputFormat('rawvideo').outputOptions('-pix_fmt yuv420p');
	if (this.options.ffmpegPath !== undefined) {
		command.setFfmpegPath(this.options.ffmpegPath + "ffmpeg");
	}
	command.on('start', function(commandLine) {
		_this.decode = command;
		if (_this.onstartdecode instanceof Function) {
			_this.onstartdecode();
		}
	});
	command.on('error', function(err) {
		if (_this.onstopdecode instanceof Function) {
			_this.onstopdecode(err.message, false);
		}
		_this.decode = null;
	});
	command.on('end', function() {
		if (_this.onstopdecode instanceof Function) {
			_this.onstopdecode(null, true);
		}
		_this.decode   = null;
		_this.frameIdx = 0;
	});

	var ffstream = command.pipe();
	ffstream.on('data', function(chunk) {
		// next chunk of data does not overflow frame
		if (readPosition + chunk.length <= _this.frameSize) {
			chunk.copy(frameBuffer, readPosition);
			readPosition += chunk.length;
			if (readPosition === _this.frameSize) {
				_this.yuvFrame = frameBuffer;

				if (_this.onnewframe instanceof Function) {
					_this.onnewframe(_this.frameIdx, _this.yuvFrame);
				}

				_this.frameIdx++;
				readPosition = 0;
			}
		} else {
			// next chunk of data overflows frame
			var current  = _this.frameSize - readPosition;
			var overflow = chunk.length - current;

			chunk.copy(frameBuffer, readPosition, 0, current);

			_this.yuvFrame = frameBuffer;

			if (_this.onnewframe instanceof Function) {
				_this.onnewframe(_this.frameIdx, _this.yuvFrame);
			}

			_this.frameIdx++;
			chunk.copy(frameBuffer, 0, current, chunk.length);
			readPosition = overflow;
		}
	});
};

/**
 *
 *
 * @method pauseLiveDecoding
 */
LiveVideoDecoder.prototype.pauseLiveDecoding = function() {
	if (this.decode !== null) {
		this.decode.kill();
	}
};

/**
 *
 *
 * @method stopLiveDecoding
 */
LiveVideoDecoder.prototype.stopLiveDecoding = function() {
	if (this.decode !== null) {
		this.decode.kill();
	}
	this.frameIdx = 0;
};

/**
 *
 *
 * @method startSeekLiveDecoding
 */
LiveVideoDecoder.prototype.startSeekLiveDecoding = function() {
	if (this.decode !== null) {
		this.decode.kill();
		this.playAfterSeek = true;
	} else {
		this.playAfterSeek = false;
	}
};

/**
 *
 *
 * @method updateSeekLiveDecoding
 */
LiveVideoDecoder.prototype.updateSeekLiveDecoding = function(frameIdx) {
	this.frameIdx = frameIdx;
};

/**
 *
 *
 * @method finishSeekLiveDecoding
 */
LiveVideoDecoder.prototype.finishSeekLiveDecoding = function() {
	if (this.playAfterSeek === true) {
		this.startLiveDecoding();
	}
};

module.exports = LiveVideoDecoder;