LINUX.ORG.RU

Сообщения Razorwar

 

Изменение скрипта aa-recorder

Приветствую всех! В js я не шибко силён. Но что нужно: записывать RTSP-потоки с камер с разделением по дате и времени. Взял скрипт aa-recorder https://github.com/asquared/aa-recorder Он работает на node js. Берёт поток с камеры и пишет её. В случае прерывания, делает новый файл, при том старый файл записи (до обрыва) остаётся целым. Всё бы хорошо, но настройки можно сделать только для аргументов камеры в конфиге этого скрипта, которые тупо добавляются в ссылку rtsp. Но камеры, с которыми я работаю, подобных длинне записи аргрументов не понимают.

Файл конфига:

{
	"storageDir": "/srv/video-storage",
	"cameras": [
		{ 
			"name": "axis-camera", 
			"url": "rtsp://192.168.1.101/axis-media/media.amp",
			"args": {
				"videocodec": "h264",
				"compression": "20",
				"fps": "10",
				"duration": "120",
				"resolution": "1920x1080"
			}
		}
]

Поэтому дует всё в один файл, пока не прервётся силой. Использовать bash для ожидания ffmpeg в случае отсутствия пинга немного не подходит, ибо при прерывании сигнала с камеры, записываемый файл повреждается. Вопрос: как изменить сам скрипт aa-record, чтобы он писал видео 1 час, а после перезапускал скрипт с дочерним процессом. Создавал новый файл и писал дальше. Куда и как встроить счётчик? Сам скрипт aa-record:

#!/usr/bin/env node
/* aa-recorder v0.0.1 */

const querystring = require('querystring');
const child_process = require('child_process');
const fs = require('fs');
const filesizeParser = require('filesize-parser');
const moment = require('moment');
const path = require('path');
const diskusage = require('diskusage');
const filewalker = require('filewalker');
const async = require('async');

/* load configuration */
fs.readFile(process.argv[2], 'utf8', function(err, data) {
	if (err) {
		console.error("Failed to read config: " + err);
		process.exit(1);
	}

	var config = JSON.parse(data);
	runApp(config);
});

function runApp(config) {
	var cameras = [];
	var cameraConfigs = config['cameras'];
	var cleanupThreshold = config['cleanupThreshold'];
	var storageDir = config['storageDir'];

	if (!cameraConfigs) {
		console.error("No cameras defined, exiting");
		process.exit(1);
	}

	if (!storageDir) {
		console.error("No storage directory defined, exiting");
		process.exit(1);
	}

	if (!cleanupThreshold) {
		console.error("No cleanup threshold defined, using 10GB");
		cleanupThreshold = "10GB";
	}

	try {
		cleanupThreshold = filesizeParser(cleanupThreshold);
	} catch (err) {
		console.error("Didn't understand " + cleanupThreshold + ", using 10GB");
		cleanupThreshold = 10 * 1024 * 1024 * 1024;
	}

	for (var i = 0; i < cameraConfigs.length; i++) {
		var camera = new Camera(cameraConfigs[i]);
		cameras.push(camera);
		camera.startRecording(storageDir);
	}

	setInterval(function() {
		cleanupFiles(storageDir, cleanupThreshold);
	}, 60000);

}

/* check if there is enough free space; if not, remove some files */
function cleanupFiles(storageDir, threshold) {
	diskusage.check(storageDir, function(err, info) {
		if (err) {
			console.error(
				"couldn't get free disk space for %s: %s",
				storageDir, err
			);
			return;
		}

		if (info.available < threshold) {
			removeFiles(storageDir, threshold - info.available);
		}
	});
}

/* delete files from storageDir until we have removed up to bytesToRemove worth */
function removeFiles(storageDir, bytesToRemove) {
	var fileList = [];
	filewalker(storageDir, { maxPending: 32, maxAttempts: 0 })
		.on('file', function(path, stat, abspath) {
			fileList.push({
				"path": abspath,
				"size": stat.size,
				"mtime": stat.mtime.getTime()
			});
		})
		.on('error', function(err) {
			console.log("error walking directory tree: %s", err);
		})
		.on('done', function() {
			// sort the file list in ascending order of modification time
			fileList.sort(function(a, b) {
				return a.mtime - b.mtime;
			});

			// go through until we reach the required number of bytes deleted
			// note: we want to delete the files asynchronously, but we still
			// have to do so sequentially so we can skip over failures. Thus
			// the async.reduce chain.
			var rfn = function(bytesToRemove, fileInfo, callback) {
				if (bytesToRemove < 0) {
					setImmediate(callback, null, bytesToRemove);
					return;
				}
				
				console.log("deleting %s", fileInfo.path);
				fs.unlink(fileInfo.path, function(err) {
					if (err) {
						console.log("failed to delete %s", fileInfo.path);
						callback(null, bytesToRemove);
					} else {
						callback(null, bytesToRemove - fileInfo.size);
					}
				});
			};

			// async has a bug, docs say the callback for reduce is optional but
			// it's actually required.
			async.reduce(fileList, bytesToRemove, rfn, function(err, result) {});
		})
		.walk();
}

/* Camera constructor and methods */
function Camera(config) {
	// Every camera must have a name.
	if (!config.name) {
		throw "no name defined";
	}
	this.name = config.name;

	// Every camera must also have a URL to pass to ffmpeg.
	if (!config.url) {
		throw "no URL defined for camera " + this.name;
	}
	this.urlBase = config.url;

	// Arguments to be added to the URL. May be undefined.
	this.urlArgs = config.args;

	// Some parameters used for supervision of the ffmpeg process.
	// First, if the ffmpeg process exits too quickly this is a
	// sign that something has gone wrong. respawnThreshold is
	// the minimum runtime that we permit. (default 5 sec)
	this.respawnThreshold = config.respawnThreshold;
	if (!this.respawnThreshold) {
		this.respawnThreshold = 5000;
	}

	// Second, the respawnDelay is the amount of time we wait before
	// trying again if the respawnThreshold was not exceeded.
	// (default 60 seconds)
	this.respawnDelay = config.respawnDelay;
	if (!this.respawnDelay) {
		this.respawnDelay = 60000;
	}

	// The type of file to record to (default mp4)
	this.fileType = config.fileType;
	if (!this.fileType) {
		this.fileType = 'mp4';
	}

	// Internal items used to track the ffmpeg process.
	this.childProcess = null;
	this.whenChildStarted = null;
	this.stopRequested = false;
}

Camera.prototype.startRecording = function(storageDir) {
	if (this.childProcess === null) {
		this.storageDir = storageDir;
		this.startChild();
	}
}

Camera.prototype.stopRecording  = function() {
	this.stopChild();
}

Camera.prototype.buildUrl = function() {
	if (this.urlArgs) {
		return this.urlBase + '?' + querystring.stringify(this.urlArgs);
	} else {
		return this.urlBase;
	}
}

Camera.prototype.buildFilename = function() {
	var dateString = moment().format("YYYYMMDD_HHmmss");
	var basename = dateString + "_" + this.name + "." + this.fileType;
	return path.join(this.storageDir, basename);
}

Camera.prototype.buildFfmpegArgs = function() {
	var url = this.buildUrl();
	var args = [];

	/* hack to force rtp over tcp */
	if (url.substr(0, 4) == "rtsp") {
		args.push('-rtsp_transport', 'tcp');
		args.push('-stimeout', '10000000');
	}

	args.push(
		'-i', this.buildUrl(),
		'-vcodec', 'copy', '-an',
		this.buildFilename()
	);

	return args;
}

Camera.prototype.childExited = function(code, signal) {
	var self = this;

	if (signal) {
		console.error(
			"camera %s: ffmpeg exited with signal %s",
			this.name, signal
		);
	} else if (code != 0) {
		console.error(
			"camera %s: ffmpeg terminated abnormally (code %d)",
			this.name, code
		);
	}

	this.childProcess = null;
	// respawn the child, unless the exit was requested
	// also look at how long the process actually ran for so we don't
	// spawn too many ffmpeg processes too quickly
	if (!this.stopRequested) {
		var runTime = null;
		if (this.whenChildStarted) {
			var now = new Date();
			runTime = now.getTime() - this.whenChildStarted.getTime();
		}

		var go = function() { self.startChild(); }

		if (runTime !== null && runTime < this.respawnThreshold) {
			console.error(
				"camera %s: child exited too quickly, waiting",
				this.name
			);
			setTimeout(go, this.respawnDelay);
		} else {
			setImmediate(go);
		}
	} }

Camera.prototype.startChild = function() {
	/* spawn a ffmpeg child process and capture its output */
	var self = this;
	var args = this.buildFfmpegArgs();

	console.log("camera %s: ffmpeg %s", this.name, args.join(' '));

	var child = child_process.spawn(
		'ffmpeg', args,
		{ 'stdio': [ 'ignore', 'pipe', 'pipe' ] }
	);

	child.on('exit', function(code, signal) {
		self.childExited(code, signal);
	});

	child.stdout.on('data', function(data) {
		console.log("camera " + self.name + ": " + data);
	});

	child.stderr.on('data', function(data) {
		if (data.toString('utf8').substr(0, 6) != "frame=") {
			console.log("camera " + self.name + ": " + data);
		}
	});

	this.childProcess = child;
	this.whenChildStarted = new Date();
}

Camera.prototype.stopChild = function() {
	/* shut down the ffmpeg child process if we have one */
	if (this.childProcess) {
		this.stopRequested = true;
		this.childProcess.kill('INT');
	}
}

 , , ,

Razorwar
()

RSS подписка на новые темы