Изменение скрипта 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');
}
}