JustMotion/src/common.cpp

541 lines
16 KiB
C++
Raw Normal View History

// This file is part of Motion Watch.
// Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "common.h"
QString getParam(const QString &key, const QStringList &args)
{
// this can be used by command objects to pick out parameters
// from a command line that are pointed by a name identifier
// example: -i /etc/some_file, this function should pick out
// "/etc/some_file" from args if "-i" is passed into key.
QString ret;
int pos = args.indexOf(QRegularExpression(key, QRegularExpression::CaseInsensitiveOption));
if (pos != -1)
{
// key found.
if ((pos + 1) <= (args.size() - 1))
v2.0.t1 Completely reformed the internal workings of the application code. I brought back multi-threaded functions so there is now 5 separate threads for different tasks. recLoop() - this function calls ffmpeg to begin recording footage from the defined camera and stores the footage in hls format. It is designed to keep running for as long as the application is running and if it does stop for whatever reason, it will attempt to auto re-start. upkeep() - this function does regular cleanup and enforcement of maxDays maxLogSize and maxEvents without the need to stop recording or detecting motion. detectMo() - this function reads directly from recLoop's hls output and list all footage that has motion in it. motion detection no longer has to wait for the clip to finish recording thanks to the use of .ts containers for the video clips. this makes the motion detection for less cpu intensive now that it will now operate at the camera's fps (slower). eventLoop() - this function reads the motion list from detectMo and copies the footage pointed out by the list to an events folder, also in hls format. schLoop() - this function runs an optional user defined external command every amount of seconds defined in sch_sec. this command temporary stops motion detection without actually terminating the thread. It will also not run the command at the scheduled time if motion was detected. Benefits to this reform: - far less cpu intensive operation - multi-threaded architecture for better asynchronous operation - it has support for live streaming now that hls is being used - a buff_dir is no longer necessary
2023-03-05 16:07:07 -05:00
{
// check ahead to make sure pos + 1 will not go out
// of range.
if (!args[pos + 1].startsWith("-"))
{
// the "-" used throughout this application
// indicates an argument so the above 'if'
// statement will check to make sure it does
// not return another argument as a parameter
// in case a back-to-back "-arg -arg" is
// present.
ret = args[pos + 1];
}
}
}
return ret;
}
QStringList lsFilesInDir(const QString &path, const QString &ext)
{
QStringList filters;
filters << "*" + ext;
QDir dirObj(path);
dirObj.setFilter(QDir::Files);
dirObj.setNameFilters(filters);
dirObj.setSorting(QDir::Name);
return dirObj.entryList();
}
v2.0.t1 Completely reformed the internal workings of the application code. I brought back multi-threaded functions so there is now 5 separate threads for different tasks. recLoop() - this function calls ffmpeg to begin recording footage from the defined camera and stores the footage in hls format. It is designed to keep running for as long as the application is running and if it does stop for whatever reason, it will attempt to auto re-start. upkeep() - this function does regular cleanup and enforcement of maxDays maxLogSize and maxEvents without the need to stop recording or detecting motion. detectMo() - this function reads directly from recLoop's hls output and list all footage that has motion in it. motion detection no longer has to wait for the clip to finish recording thanks to the use of .ts containers for the video clips. this makes the motion detection for less cpu intensive now that it will now operate at the camera's fps (slower). eventLoop() - this function reads the motion list from detectMo and copies the footage pointed out by the list to an events folder, also in hls format. schLoop() - this function runs an optional user defined external command every amount of seconds defined in sch_sec. this command temporary stops motion detection without actually terminating the thread. It will also not run the command at the scheduled time if motion was detected. Benefits to this reform: - far less cpu intensive operation - multi-threaded architecture for better asynchronous operation - it has support for live streaming now that hls is being used - a buff_dir is no longer necessary
2023-03-05 16:07:07 -05:00
QStringList lsDirsInDir(const QString &path)
{
QDir dirObj(path);
dirObj.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
dirObj.setSorting(QDir::Name);
return dirObj.entryList();
}
QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir)
{
QStringList ret;
for (auto i = 0; i < secs; ++i)
{
QString filePath;
if (dir == '-') filePath = path + "/" + stamp.addSecs(-i).toString(DATETIME_FMT) + ext;
if (dir == '+') filePath = path + "/" + stamp.addSecs(i).toString(DATETIME_FMT) + ext;
if (QFile::exists(filePath))
{
if (dir == '-') ret.insert(0, filePath);
if (dir == '+') ret.append(filePath);
}
}
return ret;
}
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs)
{
return listFacingFiles(path, ext, stamp, secs, '-');
}
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs)
{
return listFacingFiles(path, ext, stamp, secs, '+');
}
void enforceMaxEvents(shared_t *share)
{
auto names = lsFilesInDir(share->outDir, share->recExt);
while (names.size() > share->maxEvents)
{
auto nameOnly = share->outDir + "/" + names[0];
nameOnly.chop(share->recExt.size());
auto vidFile = nameOnly + share->recExt;
auto imgFile = nameOnly + share->thumbExt;
QFile::remove(vidFile);
QFile::remove(imgFile);
names.removeFirst();
}
}
void enforceMaxImages(shared_t *share)
{
auto names = lsFilesInDir(share->tmpDir + "/img", ".bmp");
while (names.size() > MAX_IMAGES)
{
QFile::remove(share->tmpDir + "/img/" + names[0]);
names.removeFirst();
}
}
void enforceMaxVids(shared_t *share)
{
auto names = lsFilesInDir(share->tmpDir + "/live", share->streamExt);
while (names.size() > MAX_VIDEOS)
{
QFile::remove(share->tmpDir + "/live/" + names[0]);
names.removeFirst();
}
}
void rdLine(const QString &param, const QString &line, QString *value)
{
if (line.startsWith(param))
{
*value = line.mid(param.size());
}
}
void rdLine(const QString &param, const QString &line, int *value)
{
if (line.startsWith(param))
{
*value = line.mid(param.size()).toInt();
}
}
void rdLine(const QString &param, const QString &line, bool *value)
{
if (line.startsWith(param))
{
auto val = line.mid(param.size()).trimmed();
*value = (val == "y" || val == "Y");
}
}
void extCorrection(QString &ext)
{
if (!ext.startsWith("."))
{
ext = "." + ext;
}
}
bool mkPath(const QString &path)
{
auto ret = true;
if (!QDir().exists(path))
{
ret = QDir().mkpath(path);
}
return ret;
}
bool rdConf(const QString &filePath, shared_t *share)
{
QFile varFile(filePath);
if (!varFile.open(QFile::ReadOnly))
{
share->retCode = ENOENT;
QTextStream(stderr) << "err: config file - " << filePath << " does not exists or lack read permissions." << Qt::endl;
}
else
{
share->recordUri.clear();
share->postCmd.clear();
share->camName.clear();
auto thrCount = QThread::idealThreadCount() / 2;
share->retCode = 0;
share->imgThresh = 8000;
share->maxEvents = 100;
share->maxLogSize = 100000;
share->skipCmd = false;
share->postSecs = 60;
share->evMaxSecs = 30;
share->conf = filePath;
share->buffPath = "/var/buffer";
share->recPath = "/var/footage";
share->outputType = "stderr";
share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null";
share->streamCodec = "copy";
share->streamExt = ".ts";
share->recExt = ".mp4";
share->thumbExt = ".jpg";
share->singleTenant = false;
share->imgThreads = thrCount;
share->recThreads = thrCount;
share->recFps = 30;
share->recScale = "1280:720";
share->imgScale = "320:240";
QString line;
do
{
line = QString::fromUtf8(varFile.readLine());
if (!line.startsWith("#"))
{
rdLine("cam_name = ", line, &share->camName);
rdLine("recording_uri = ", line, &share->recordUri);
rdLine("buffer_path = ", line, &share->buffPath);
rdLine("rec_path = ", line, &share->recPath);
rdLine("max_event_secs = ", line, &share->evMaxSecs);
rdLine("post_secs = ", line, &share->postSecs);
rdLine("post_cmd = ", line, &share->postCmd);
rdLine("img_thresh = ", line, &share->imgThresh);
rdLine("max_events = ", line, &share->maxEvents);
rdLine("max_log_size = ", line, &share->maxLogSize);
rdLine("img_comp_out = ", line, &share->outputType);
rdLine("img_comp_cmd = ", line, &share->compCmd);
rdLine("stream_codec = ", line, &share->streamCodec);
rdLine("stream_ext = ", line, &share->streamExt);
rdLine("rec_ext = ", line, &share->recExt);
rdLine("thumbnail_ext = ", line, &share->thumbExt);
rdLine("single_tenant = ", line, &share->singleTenant);
rdLine("img_threads = ", line, &share->imgThreads);
rdLine("rec_threads = ", line, &share->recThreads);
rdLine("rec_fps = ", line, &share->recFps);
rdLine("rec_scale = ", line, &share->recScale);
rdLine("img_scale = ", line, &share->imgScale);
}
} while(!line.isEmpty());
if (share->camName.isEmpty())
{
share->camName = QFileInfo(share->conf).fileName();
}
extCorrection(share->streamExt);
extCorrection(share->recExt);
extCorrection(share->thumbExt);
if (share->singleTenant)
{
share->outDir = QDir().cleanPath(share->recPath);
share->tmpDir = QDir().cleanPath(share->buffPath);
}
else
{
share->outDir = QDir().cleanPath(share->recPath) + "/" + share->camName;
share->tmpDir = QDir().cleanPath(share->buffPath) + "/" + share->camName;
}
auto servDir = QString("/var/") + APP_BIN + QString("_serv");
share->retCode = EACCES;
if (!mkPath(servDir))
{
QTextStream(stderr) << "err: failed to create service directory - " << servDir << " check for write permissions." << Qt::endl;
}
else if (!mkPath(share->recPath))
{
QTextStream(stderr) << "err: failed to create root recording directory - " << share->recPath << " check for write permissions." << Qt::endl;
}
else if (!mkPath(share->buffPath))
{
QTextStream(stderr) << "err: failed to create root buffer directory - " << share->buffPath << " check for write permissions." << Qt::endl;
}
else if (!mkPath(share->outDir))
{
QTextStream(stderr) << "err: failed to create recording directory - " << share->outDir << " check for write permissions." << Qt::endl;
}
else if (!mkPath(share->tmpDir))
{
QTextStream(stderr) << "err: failed to create buffer directory - " << share->tmpDir << " check for write permissions." << Qt::endl;
}
else if (!mkPath(share->tmpDir + "/live"))
{
QTextStream(stderr) << "err: failed to create 'live' in the buffer directory - " << share->tmpDir << "/live" << " check for write permissions." << Qt::endl;
}
else if (!mkPath(share->tmpDir + "/logs"))
{
QTextStream(stderr) << "err: failed to create 'logs' in the buffer directory - " << share->tmpDir << "/logs" << " check for write permissions." << Qt::endl;
}
else if (!mkPath(share->tmpDir + "/img"))
{
QTextStream(stderr) << "err: failed to create 'img' in the buffer directory - " << share->tmpDir << "/img" << " check for write permissions." << Qt::endl;
}
else
{
share->retCode = 0;
share->servPath = QString("/var/") + APP_BIN + QString("_serv/") + APP_BIN + "." + share->camName + ".service";
}
}
return share->retCode == 0;
v2.0.t1 Completely reformed the internal workings of the application code. I brought back multi-threaded functions so there is now 5 separate threads for different tasks. recLoop() - this function calls ffmpeg to begin recording footage from the defined camera and stores the footage in hls format. It is designed to keep running for as long as the application is running and if it does stop for whatever reason, it will attempt to auto re-start. upkeep() - this function does regular cleanup and enforcement of maxDays maxLogSize and maxEvents without the need to stop recording or detecting motion. detectMo() - this function reads directly from recLoop's hls output and list all footage that has motion in it. motion detection no longer has to wait for the clip to finish recording thanks to the use of .ts containers for the video clips. this makes the motion detection for less cpu intensive now that it will now operate at the camera's fps (slower). eventLoop() - this function reads the motion list from detectMo and copies the footage pointed out by the list to an events folder, also in hls format. schLoop() - this function runs an optional user defined external command every amount of seconds defined in sch_sec. this command temporary stops motion detection without actually terminating the thread. It will also not run the command at the scheduled time if motion was detected. Benefits to this reform: - far less cpu intensive operation - multi-threaded architecture for better asynchronous operation - it has support for live streaming now that hls is being used - a buff_dir is no longer necessary
2023-03-05 16:07:07 -05:00
}
void rmServices()
{
auto path = QString("/var/") + APP_BIN + QString("_serv");
auto files = lsFilesInDir(path, ".service");
for (auto &&serv : files)
{
QProcess::execute("systemctl", {"stop", serv});
QProcess::execute("systemctl", {"disable", serv});
QFile::remove(QString("/lib/systemd/system/") + serv);
QFile::remove(path + "/" + serv);
}
QProcess::execute("systemctl", {"daemon-reload"});
}
void listServices()
{
auto path = QString("/var/") + APP_BIN + QString("_serv");
auto files = lsFilesInDir(path, ".service");
for (auto &&serv : files)
{
QTextStream(stdout) << serv << ": "; QProcess::execute("systemctl", {"is-active", serv});
}
}
int loadServices(const QStringList &args)
{
auto ret = ENOENT;
auto path = QDir().cleanPath(getParam("-d", args));
auto files = lsFilesInDir(path);
if (!QDir(path).exists())
{
QTextStream(stderr) << "err: the supplied directory in -d '" << path << "' does not exists or is not a directory." << Qt::endl;
}
else if (files.isEmpty())
{
QTextStream(stderr) << "err: no config files found in '" << path << "'" << Qt::endl;
}
else
{
ret = 0;
QTextStream(stdout) << "loading conf files from dir: " << path << Qt::endl;
for (auto &&conf : files)
{
shared_t shared;
if (!rdConf(path + "/" + conf, &shared))
{
ret = shared.retCode; break;
}
else
{
QTextStream(stdout) << conf << " --" << Qt::endl;
QFile file(shared.servPath);
if (!file.open(QFile::ReadWrite | QFile::Truncate))
{
QTextStream(stderr) << "err: failed to open service file: " << shared.servPath << " for writing. reason: " << file.errorString();
ret = EACCES; file.close(); break;
}
else
{
file.write("[Unit]\n");
file.write("Description=" + QByteArray(APP_NAME) + " Camera - " + shared.camName.toUtf8() + "\n");
file.write("After=network.target\n\n");
file.write("[Service]\n");
file.write("Type=simple\n");
file.write("User=" + QByteArray(APP_BIN) + "\n");
file.write("Restart=always\n");
file.write("RestartSec=5\n");
file.write("TimeoutStopSec=infinity\n");
file.write("ExecStart=/usr/bin/env " + QByteArray(APP_BIN) + " -c " + shared.conf.toUtf8() + "\n\n");
file.write("[Install]\n");
file.write("WantedBy=multi-user.target");
file.close();
auto servName = QFileInfo(shared.servPath).fileName();
if (!QFile::link(shared.servPath, "/lib/systemd/system/" + servName))
{
ret = EACCES; break;
}
else
{
if (ret == 0) ret = QProcess::execute("systemctl", {"daemon-reload"});
if (ret == 0) ret = QProcess::execute("systemctl", {"enable", servName});
if (ret == 0) ret = QProcess::execute("systemctl", {"start", servName});
if (ret != 0)
{
break;
}
else
{
QTextStream(stdout) << "Successfully loaded camera service: " << servName << Qt::endl;
if (shared.singleTenant) break;
}
}
}
}
}
}
return ret;
}
QString buildThreadCount(int count)
{
QString ret = "0";
for (auto i = 1; i < count; ++i)
{
ret.append(","); ret.append(QString::number(i));
}
return ret;
}
QStringList parseArgs(const QByteArray &data, int maxArgs, int *pos)
{
QStringList ret;
QString arg;
auto line = QString::fromUtf8(data);
auto inDQuotes = false;
auto inSQuotes = false;
auto escaped = false;
if (pos != nullptr) *pos = 0;
for (int i = 0; i < line.size(); ++i)
{
if (pos != nullptr) *pos += 1;
if ((line[i] == '\'') && !inDQuotes && !escaped)
{
// single quote '
inSQuotes = !inSQuotes;
}
else if ((line[i] == '\"') && !inSQuotes && !escaped)
{
// double quote "
inDQuotes = !inDQuotes;
}
else
{
escaped = false;
if (line[i].isSpace() && !inDQuotes && !inSQuotes)
{
// space
if (!arg.isEmpty())
{
ret.append(arg);
arg.clear();
}
}
else
{
if ((line[i] == '\\') && ((i + 1) < line.size()))
{
if ((line[i + 1] == '\'') || (line[i + 1] == '\"'))
{
escaped = true;
}
else
{
arg.append(line[i]);
}
}
else
{
arg.append(line[i]);
}
}
}
if ((ret.size() >= maxArgs) && (maxArgs != -1))
{
break;
}
}
if (!arg.isEmpty() && !inDQuotes && !inSQuotes)
{
ret.append(arg);
}
return ret;
}