-re-added recordloop as a thread within the app.
-the app no longer use mutiple services and will instead fully
 operate in a single master service.
-build/install.py will now install the app as a single service.
-added/updated -s, -r and -q options to manage the single master
 service.
This commit is contained in:
zii 2024-04-21 08:03:30 -04:00
parent 33519acf7a
commit daad0dffa2
21 changed files with 1078 additions and 785 deletions

View File

@ -11,10 +11,16 @@ CONFIG -= app_bundle
HEADERS += \
src/common.h \
src/camera.h \
src/services.h
src/detect_loop.h \
src/event_loop.h \
src/proc_control.h \
src/record_loop.h
SOURCES += \
src/common.cpp \
src/camera.cpp \
src/services.cpp \
src/detect_loop.cpp \
src/event_loop.cpp \
src/proc_control.cpp \
src/record_loop.cpp \
src/main.cpp

View File

@ -156,13 +156,14 @@ img_scale = 320:240
# rec_path. it uses width, height numeric strings seperated by a colon,
# eg W:H.
#
service_user = mow
service_user = jmotion
# this sets the service local user of the application dictating the
# amount privilege it will have on the host. if not defined, the
# unprivileged user 'mow' will be used. which ever user is defined here
# just make sure it has read/write access to buffer_path and rec_path.
# amount of privilege it will have on the host. if not defined, the
# unprivileged user 'jmotion' will be used. which ever user is defined
# here just make sure it has read/write access to buffer_path and
# rec_path.
#
service_group = mow
service_group = jmotion
# this sets the service local group of the application that can further
# refine host privileges. if not defined, the name stored in
# service_user will be used.

View File

@ -6,6 +6,7 @@ import subprocess
import shutil
import sys
import platform
import pwd
def get_app_target(text):
return re.search(r'(APP_TARGET) +(\"(.*?)\")', text).group(3)
@ -126,6 +127,7 @@ def linux_build_app_dir(app_ver, app_name, app_target, qt_bin):
verbose_copy("templates/linux_run_script.sh", "app_dir/" + app_target + ".sh")
verbose_copy("templates/linux_uninstall.sh", "app_dir/uninstall.sh")
verbose_copy("templates/linux_service.service", "app_dir/" + app_target + ".service")
complete(app_ver, app_target)
@ -167,6 +169,15 @@ def list_of_words_in_text(list_of_words, text_body):
return False
return True
def user_exists(user_name):
try:
pwd.getpwnam(user_name)
return True
except KeyError:
return False
def platform_setup():
ins_packages = list_installed_packages()
@ -188,6 +199,10 @@ def platform_setup():
subprocess.run(["sudo", "pacman", "-S", "--noconfirm"] + dep_pkgs_a)
subprocess.run(["sudo", "pacman", "-S", "--noconfirm"] + dep_pkgs_b)
if not user_exists("jmotion"):
subprocess.run(["sudo", "useradd", "-r", "jmotion"])
subprocess.run(["sudo", "usermod", "-aG", "video", "jmotion"])
def main():
platform_setup()

View File

@ -8,6 +8,7 @@ import sys
import zipfile
import binascii
import tempfile
import pwd
def cd():
current_dir = os.path.dirname(__file__)
@ -109,6 +110,7 @@ def local_install(app_target, app_name):
if make_app_dirs(app_target):
text_template_deploy("app_dir/" + app_target + ".sh", install_dir + "/" + app_target + ".sh", install_dir, app_name, app_target)
text_template_deploy("app_dir/" + app_target + ".service", "/lib/systemd/system/" + app_target + ".service", install_dir, app_name, app_target)
text_template_deploy("app_dir/uninstall.sh", install_dir + "/uninstall.sh", install_dir, app_name, app_target)
verbose_copy("app_dir/" + app_target, install_dir + "/" + app_target)
@ -119,9 +121,17 @@ def local_install(app_target, app_name):
subprocess.run(["chmod", "755", install_dir + "/" + app_target + ".sh"])
subprocess.run(["chmod", "755", install_dir + "/" + app_target])
subprocess.run(["chmod", "755", install_dir + "/uninstall.sh"])
subprocess.run(["chmod", "644", "/lib/systemd/system/" + app_target + ".service"])
subprocess.run(["chmod", "777", "/var/buffer"])
subprocess.run(["chmod", "777", "/var/footage"])
if not user_exists(app_target):
subprocess.run(["sudo", "useradd", "-r", app_target])
subprocess.run(["sudo", "usermod", "-aG", "video", app_target])
subprocess.run(["systemctl", "start", app_target])
subprocess.run(["systemctl", "enable", app_target])
print("Installation finished. If you ever need to uninstall this application, run this command with root rights:")
print(" sh " + install_dir + "/uninstall.sh\n")
@ -266,6 +276,15 @@ def list_of_words_in_text(list_of_words, text_body):
return False
return True
def user_exists(user_name):
try:
pwd.getpwnam(user_name)
return True
except KeyError:
return False
def platform_setup():
ins_packages = list_installed_packages()
@ -303,6 +322,7 @@ def main(is_sfx):
app_name = info[2]
if is_sfx:
platform_setup()
sfx()
elif "--local" in sys.argv:

View File

@ -12,355 +12,130 @@
#include "camera.h"
Camera::Camera(QObject *parent) : QObject(parent) {}
int Camera::start(const QStringList &args)
Camera::Camera(const QString &confFile, const QString &statDir, ProcControl *proc, QCoreApplication *parent) : QObject(nullptr)
{
if (rdConf(getParam("-c", args), &shared))
{
auto thr1 = new QThread(nullptr);
auto thr2 = new QThread(nullptr);
Q_UNUSED(parent);
new EventLoop(&shared, thr1, nullptr);
new DetectLoop(&shared, thr2, nullptr);
fsW = new QFileSystemWatcher(this);
statTimer = new QTimer(this);
statPath = statDir;
evtLoop = nullptr;
detLoop = nullptr;
recLoop = nullptr;
delOnZero = false;
objCount = 0;
statTimer->setInterval(5000);
statTimer->start();
proc->objPlusOne();
connect(fsW, &QFileSystemWatcher::fileChanged, this, &Camera::confChanged);
connect(proc, &ProcControl::prepForClose, this, &Camera::prepForDel);
connect(this, &Camera::destroyed, proc, &ProcControl::objMinusOne);
connect(statTimer, &QTimer::timeout, this, &Camera::updateStat);
start(confFile);
}
void Camera::objMinusOne()
{
objCount--;
if (objCount == 0)
{
thr1->deleteLater();
thr2->deleteLater();
thr3->deleteLater();
if (delOnZero)
{
QDir(shared.buffPath).removeRecursively();
deleteLater();
}
}
}
void Camera::prepForDel()
{
statTimer->blockSignals(true);
delOnZero = true;
emit stop();
}
void Camera::updateStat()
{
auto statFile = statPath + "/" + shared.camName;
QFile file(statFile);
file.open(QFile::WriteOnly);
file.write(statusLine().toUtf8());
file.close();
}
int Camera::start(const QString &conf)
{
if (rdConf(conf, &shared))
{
setupBuffDir(shared.buffPath, true);
if (!fsW->files().contains(conf))
{
fsW->addPath(conf);
}
thr1 = new QThread(nullptr);
thr2 = new QThread(nullptr);
thr3 = new QThread(nullptr);
evtLoop = new EventLoop(&shared, thr1, nullptr);
detLoop = new DetectLoop(&shared, thr2, nullptr);
recLoop = new RecordLoop(&shared, thr3, nullptr);
connect(this, &Camera::stop, thr1, &QThread::quit);
connect(this, &Camera::stop, thr2, &QThread::quit);
connect(this, &Camera::stop, thr3, &QThread::quit);
connect(thr1, &QThread::finished, this, &Camera::objMinusOne);
connect(thr2, &QThread::finished, this, &Camera::objMinusOne);
connect(thr3, &QThread::finished, this, &Camera::objMinusOne);
thr1->start();
thr2->start();
thr3->start();
objCount = 3;
}
return shared.retCode;
}
EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QObject(parent)
void Camera::confChanged(const QString &path)
{
shared = sharedRes;
heartBeat = 2;
loopTimer = 0;
emit stop();
connect(thr, &QThread::started, this, &EventLoop::init);
moveToThread(thr);
}
void EventLoop::init()
{
loopTimer = new QTimer(this);
connect(loopTimer, &QTimer::timeout, this, &EventLoop::loopSlot);
loopTimer->setSingleShot(false);
loopTimer->start(heartBeat * 1000);
loopSlot();
}
void EventLoop::loopSlot()
{
if (!exec())
if (!QFileInfo::exists(path))
{
loopTimer->stop(); QCoreApplication::exit(shared->retCode);
}
}
bool EventLoop::wrOutVod(const evt_t &event)
{
auto ret = false;
auto cnt = 0;
auto concat = shared->buffPath + "/live/" + event.timeStamp + ".ctmp";
QFile file(concat, this);
file.open(QFile::WriteOnly);
for (auto &&vid : event.vidList)
{
QTextStream(stdout) << "event_src: " << vid << Qt::endl;
if (QFile::exists(vid))
{
file.write(QString("file '" + vid + "'\n").toUtf8()); cnt++;
}
else
{
QTextStream(stdout) << "warning: the event hls clip does not exists." << Qt::endl;
}
}
file.close();
if (cnt == 0)
{
QTextStream(stderr) << "err: none of the event hls clips exists, cancelling write out." << Qt::endl;
QFile::remove(concat);
deleteLater();
}
else
{
QStringList args;
auto ret = start(path);
args << "-f";
args << "concat";
args << "-safe" << "0";
args << "-i" << concat;
args << "-c" << "copy";
args << shared->recPath + "/" + event.timeStamp + shared->recExt;
if (QProcess::execute("ffmpeg", args) == 0)
if (ret != 0)
{
ret = true;
}
QFile::remove(concat);
}
return ret;
}
bool EventLoop::exec()
{
enforceMaxEvents(shared);
enforceMaxImages(shared);
enforceMaxClips(shared);
if (!shared->recList.isEmpty())
{
auto event = shared->recList.takeFirst();
QTextStream(stdout) << "attempting write out of event: " << event.timeStamp << Qt::endl;
if (wrOutVod(event))
{
QStringList args;
args << "convert";
args << event.imgPath;
args << shared->recPath + "/" + event.timeStamp + shared->thumbExt;
QProcess::execute("magick", args);
}
}
return shared->retCode == 0;
}
DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QFileSystemWatcher(parent)
{
pcTimer = 0;
shared = sharedRes;
connect(thr, &QThread::started, this, &DetectLoop::init);
moveToThread(thr);
}
void DetectLoop::init()
{
pcTimer = new QTimer(this);
connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak);
connect(this, &QFileSystemWatcher::directoryChanged, this, &DetectLoop::updated);
addPath(shared->buffPath + "/live");
pcTimer->start(shared->postSecs * 1000);
}
void DetectLoop::reset()
{
eventQue.inQue = false;
eventQue.score = 0;
eventQue.queAge = 0;
eventQue.imgPath.clear();
eventQue.vidList.clear();
eventQue.timeStamp.clear();
}
void DetectLoop::updated(const QString &path)
{
auto clips = lsFilesInDir(path, shared->streamExt);
auto index = clips.indexOf(vidBName);
if (clips.size() - (index + 1) < 3)
{
thread()->sleep(1);
}
else
{
vidAName = clips[clips.size() - 3];
vidBName = clips[clips.size() - 2];
vidAPath = shared->buffPath + "/live/" + vidAName;
vidBPath = shared->buffPath + "/live/" + vidBName;
exec(); thread()->sleep(1);
}
}
void DetectLoop::pcBreak()
{
prevClips.clear();
if (!shared->postCmd.isEmpty())
{
QTextStream(stdout) << "---POST_BREAK---" << Qt::endl;
if (eventQue.inQue)
{
QTextStream(stdout) << "motion detected, skipping the post command." << Qt::endl;
}
else
{
QTextStream(stdout) << "no motion detected, running post command: " << shared->postCmd << Qt::endl;
auto args = parseArgs(shared->postCmd.toUtf8(), -1);
if (args.isEmpty())
{
QTextStream(stderr) << "err: did not parse an executable from the post command line." << Qt::endl;
}
else
{
QProcess::execute(args[0], args.mid(1));
}
deleteLater();
}
}
}
float DetectLoop::getFloatFromExe(const QByteArray &line)
QString Camera::statusLine()
{
QString strLine(line);
QString strNum;
for (auto chr : strLine)
{
if (chr.isDigit() || (chr == '.'))
{
strNum.append(chr);
}
else
{
break;
}
}
auto ok = false;
auto res = strNum.toFloat(&ok);
if (!ok || strNum.isEmpty())
{
QTextStream(stderr) << "err: the image comp command returned unexpected output and couldn't be converted to float." << Qt::endl;
QTextStream(stderr) << " raw output: " << line << Qt::endl;
}
return res;
}
QStringList DetectLoop::buildArgs(const QString &prev, const QString &next)
{
auto args = parseArgs(shared->compCmd.toUtf8(), -1);
for (auto i = 0; i < args.size(); ++i)
{
if (args[i] == PREV_IMG) args[i] = prev;
if (args[i] == NEXT_IMG) args[i] = next;
}
return args;
}
QStringList DetectLoop::buildSnapArgs(const QString &vidSrc, const QString &imgPath)
{
QStringList ret;
ret.append("-hide_banner");
ret.append("-loglevel");
ret.append("panic");
ret.append("-y");
ret.append("-i");
ret.append(vidSrc);
ret.append("-frames:v");
ret.append("1");
ret.append(imgPath);
return ret;
}
void DetectLoop::exec()
{
auto imgAPath = shared->buffPath + "/img/" + QFileInfo(vidAPath).baseName() + ".bmp";
auto imgBPath = shared->buffPath + "/img/" + QFileInfo(vidBPath).baseName() + ".bmp";
auto snapArgsA = buildSnapArgs(vidAPath, imgAPath);
auto snapArgsB = buildSnapArgs(vidBPath, imgBPath);
auto compArgs = buildArgs(imgAPath, imgBPath);
if (compArgs.isEmpty())
{
QTextStream(stderr) << "err: could not parse a executable name from img_comp_cmd: " << shared->compCmd << Qt::endl;
}
else
{
QProcess::execute("ffmpeg", snapArgsA);
QProcess::execute("ffmpeg", snapArgsB);
if (QFile::exists(imgAPath) && QFile::exists(imgBPath))
{
QProcess extComp;
extComp.start(compArgs[0], compArgs.mid(1));
extComp.waitForFinished();
float score = 0;
if (shared->outputType == "stdout")
{
score = getFloatFromExe(extComp.readAllStandardOutput());
}
else
{
score = getFloatFromExe(extComp.readAllStandardError());
}
QTextStream(stdout) << compArgs.join(" ") << " --result: " << QString::number(score) << Qt::endl;
if (eventQue.inQue)
{
eventQue.queAge += 4;
if (eventQue.score < score)
{
eventQue.score = score;
eventQue.imgPath = imgBPath;
}
eventQue.vidList.append(vidAPath);
eventQue.vidList.append(vidBPath);
if (eventQue.queAge >= shared->evMaxSecs)
{
eventQue.inQue = false;
shared->recList.append(eventQue);
reset();
}
}
else if (score >= shared->imgThresh)
{
QTextStream(stdout) << "--threshold_meet: " << QString::number(shared->imgThresh) << Qt::endl;
eventQue.score = score;
eventQue.imgPath = imgBPath;
eventQue.inQue = true;
eventQue.queAge = 0;
eventQue.timeStamp = QDateTime::currentDateTime().toString(DATETIME_FMT);
eventQue.vidList.append(vidAPath);
eventQue.vidList.append(vidBPath);
}
}
}
vidAPath.clear();
vidBPath.clear();
return "Detection: " + detLoop->statusLine() +
" Event-Scraping: " + evtLoop->statusLine() +
" Recording: " + recLoop->statusLine() +
" Name: " + shared.camName + "\n";
}

View File

@ -14,6 +14,10 @@
// GNU General Public License for more details.
#include "common.h"
#include "event_loop.h"
#include "detect_loop.h"
#include "record_loop.h"
#include "proc_control.h"
class Camera : public QObject
{
@ -21,70 +25,36 @@ class Camera : public QObject
private:
shared_t shared;
public:
explicit Camera(QObject *parent = nullptr);
int start(const QStringList &args);
};
class EventLoop : public QObject
{
Q_OBJECT
QFileSystemWatcher *fsW;
EventLoop *evtLoop;
DetectLoop *detLoop;
RecordLoop *recLoop;
QThread *thr1;
QThread *thr2;
QThread *thr3;
QTimer *statTimer;
QString statPath;
shared_t shared;
uint objCount;
bool delOnZero;
private slots:
void init();
void loopSlot();
private:
shared_t *shared;
QTimer *loopTimer;
int heartBeat;
bool wrOutVod(const evt_t &event);
void confChanged(const QString &path);
void objMinusOne();
void updateStat();
void prepForDel();
public:
explicit EventLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
explicit Camera(const QString &confFile, const QString &statDir, ProcControl *proc, QCoreApplication *parent);
bool exec();
};
int start(const QString &conf);
QString statusLine();
class DetectLoop : public QFileSystemWatcher
{
Q_OBJECT
signals:
private:
QString vidAPath;
QString vidBPath;
QString vidAName;
QString vidBName;
QStringList prevClips;
QTimer *pcTimer;
evt_t eventQue;
shared_t *shared;
float getFloatFromExe(const QByteArray &line);
QStringList buildArgs(const QString &prev, const QString &next);
QStringList buildSnapArgs(const QString &vidSrc, const QString &imgPath);
private slots:
void init();
void reset();
void pcBreak();
void updated(const QString &path);
public:
explicit DetectLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
void exec();
void stop();
};
#endif // CAMERA_H

View File

@ -12,43 +12,6 @@
#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))
{
// 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;
@ -212,7 +175,6 @@ bool rdConf(const QString &filePath, shared_t *share)
share->camName.clear();
share->buffPath.clear();
share->recPath.clear();
share->servGroup.clear();
share->retCode = 0;
share->imgThresh = 8000;
@ -232,7 +194,6 @@ bool rdConf(const QString &filePath, shared_t *share)
share->liveSecs = 80;
share->recScale = "1280:720";
share->imgScale = "320:240";
share->servUser = APP_TARGET;
QString line;
@ -261,8 +222,6 @@ bool rdConf(const QString &filePath, shared_t *share)
rdLine("rec_fps = ", line, &share->recFps);
rdLine("rec_scale = ", line, &share->recScale);
rdLine("img_scale = ", line, &share->imgScale);
rdLine("service_user = ", line, &share->servUser);
rdLine("service_group = ", line, &share->servGroup);
rdLine("live_secs = ", line, &share->liveSecs);
}
@ -309,19 +268,34 @@ bool rdConf(const QString &filePath, shared_t *share)
{
share->evMaxSecs = share->liveSecs - 4;
}
if (share->servGroup.isEmpty())
{
share->servGroup = share->servUser;
}
share->servMainLoop = QString(APP_TARGET) + ".main_loop." + share->camName;
share->servVidLoop = QString(APP_TARGET) + ".vid_loop." + share->camName;
}
return share->retCode == 0;
}
void setupBuffDir(const QString &path, bool del)
{
if (del)
{
QDir(path).removeRecursively();
}
if (!QFileInfo::exists(path))
{
mkPath(path);
}
if (!QFileInfo::exists(path + "/live"))
{
mkPath(path + "/live");
}
if (!QFileInfo::exists(path + "/img"))
{
mkPath(path + "/img");
}
}
QString buildThreadCount(int count)
{
QString ret = "0";

View File

@ -26,11 +26,13 @@
#include <QMutex>
#include <QRegularExpression>
#include <QFileSystemWatcher>
#include <QDebug>
#include <QElapsedTimer>
#include <iostream>
using namespace std;
#define APP_VERSION "3.4"
#define APP_VERSION "3.5.t2"
#define APP_NAME "JustMotion"
#define APP_TARGET "jmotion"
#define DATETIME_FMT "yyyyMMddhhmmss"
@ -72,10 +74,6 @@ struct shared_t
QString thumbExt;
QString recScale;
QString imgScale;
QString servMainLoop;
QString servVidLoop;
QString servUser;
QString servGroup;
bool singleTenant;
bool skipCmd;
int liveSecs;
@ -87,7 +85,6 @@ struct shared_t
int retCode;
};
QString getParam(const QString &key, const QStringList &args);
QString buildThreadCount(int count);
QStringList lsFilesInDir(const QString &path, const QString &ext = QString());
QStringList lsDirsInDir(const QString &path);
@ -97,6 +94,7 @@ QStringList forwardFacingFiles(const QString &path, const QString &ext, const QD
QStringList parseArgs(const QByteArray &data, int maxArgs, int *pos = nullptr);
bool rdConf(const QString &filePath, shared_t *share);
bool mkPath(const QString &path);
void setupBuffDir(const QString &path, bool del = false);
void rdLine(const QString &param, const QString &line, QString *value);
void rdLine(const QString &param, const QString &line, int *value);
void rdLine(const QString &param, const QString &line, bool *value);

251
src/detect_loop.cpp Normal file
View File

@ -0,0 +1,251 @@
// This file is part of JustMotion.
// JustMotion 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.
// JustMotion 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 "detect_loop.h"
DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QFileSystemWatcher(parent)
{
pcTimer = 0;
shared = sharedRes;
connect(thr, &QThread::started, this, &DetectLoop::init);
connect(thr, &QThread::finished, this, &DetectLoop::deleteLater);
moveToThread(thr);
}
void DetectLoop::init()
{
pcTimer = new QTimer(this);
connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak);
connect(this, &QFileSystemWatcher::directoryChanged, this, &DetectLoop::updated);
pcTimer->start(shared->postSecs * 1000);
setupBuffDir(shared->buffPath);
addPath(shared->buffPath + "/live");
}
void DetectLoop::reset()
{
eventQue.inQue = false;
eventQue.score = 0;
eventQue.queAge = 0;
eventQue.imgPath.clear();
eventQue.vidList.clear();
eventQue.timeStamp.clear();
}
void DetectLoop::updated(const QString &path)
{
eTimer.start();
auto clips = lsFilesInDir(path, shared->streamExt);
auto index = clips.indexOf(vidBName);
if (clips.size() - (index + 1) < 3)
{
thread()->sleep(1);
}
else
{
vidAName = clips[clips.size() - 3];
vidBName = clips[clips.size() - 2];
vidAPath = shared->buffPath + "/live/" + vidAName;
vidBPath = shared->buffPath + "/live/" + vidBName;
exec(); thread()->sleep(1);
}
}
void DetectLoop::pcBreak()
{
prevClips.clear();
if (!shared->postCmd.isEmpty())
{
qInfo() << "---POST_BREAK---";
if (eventQue.inQue)
{
qInfo() << "motion detected, skipping the post command";
}
else
{
qInfo() << "no motion detected, running post command: " << shared->postCmd;
auto args = parseArgs(shared->postCmd.toUtf8(), -1);
if (args.isEmpty())
{
qCritical() << "err: did not parse an executable from the post command line.";
}
else
{
QProcess::execute(args[0], args.mid(1));
}
}
}
}
float DetectLoop::getFloatFromExe(const QByteArray &line)
{
QString strLine(line);
QString strNum;
for (auto chr : strLine)
{
if (chr.isDigit() || (chr == '.'))
{
strNum.append(chr);
}
else
{
break;
}
}
auto ok = false;
auto res = strNum.toFloat(&ok);
if (!ok || strNum.isEmpty())
{
qCritical() << "err: the image comp command returned unexpected output and couldn't be converted to float";
qCritical() << " raw output: " << line;
}
return res;
}
QStringList DetectLoop::buildArgs(const QString &prev, const QString &next)
{
auto args = parseArgs(shared->compCmd.toUtf8(), -1);
for (auto i = 0; i < args.size(); ++i)
{
if (args[i] == PREV_IMG) args[i] = prev;
if (args[i] == NEXT_IMG) args[i] = next;
}
return args;
}
QStringList DetectLoop::buildSnapArgs(const QString &vidSrc, const QString &imgPath)
{
QStringList ret;
ret.append("-hide_banner");
ret.append("-loglevel");
ret.append("panic");
ret.append("-y");
ret.append("-i");
ret.append(vidSrc);
ret.append("-frames:v");
ret.append("1");
ret.append(imgPath);
return ret;
}
QString DetectLoop::statusLine()
{
if (eTimer.elapsed() >= 5000)
{
return "STARVED";
}
else
{
return "OK ";
}
}
void DetectLoop::exec()
{
auto imgAPath = shared->buffPath + "/img/" + QFileInfo(vidAPath).baseName() + ".bmp";
auto imgBPath = shared->buffPath + "/img/" + QFileInfo(vidBPath).baseName() + ".bmp";
auto snapArgsA = buildSnapArgs(vidAPath, imgAPath);
auto snapArgsB = buildSnapArgs(vidBPath, imgBPath);
auto compArgs = buildArgs(imgAPath, imgBPath);
if (compArgs.isEmpty())
{
qCritical() << "err: could not parse a executable name from img_comp_cmd: " << shared->compCmd;
}
else
{
QProcess::execute("ffmpeg", snapArgsA);
QProcess::execute("ffmpeg", snapArgsB);
if (QFile::exists(imgAPath) && QFile::exists(imgBPath))
{
QProcess extComp;
extComp.start(compArgs[0], compArgs.mid(1));
extComp.waitForFinished();
float score = 0;
if (shared->outputType == "stdout")
{
score = getFloatFromExe(extComp.readAllStandardOutput());
}
else
{
score = getFloatFromExe(extComp.readAllStandardError());
}
qInfo() << compArgs.join(" ") << " --result: " << QString::number(score);
if (eventQue.inQue)
{
eventQue.queAge += 4;
if (eventQue.score < score)
{
eventQue.score = score;
eventQue.imgPath = imgBPath;
}
eventQue.vidList.append(vidAPath);
eventQue.vidList.append(vidBPath);
if (eventQue.queAge >= shared->evMaxSecs)
{
eventQue.inQue = false;
shared->recList.append(eventQue);
reset();
}
}
else if (score >= shared->imgThresh)
{
qInfo() << "--threshold_meet: " << QString::number(shared->imgThresh);
eventQue.score = score;
eventQue.imgPath = imgBPath;
eventQue.inQue = true;
eventQue.queAge = 0;
eventQue.timeStamp = QDateTime::currentDateTime().toString(DATETIME_FMT);
eventQue.vidList.append(vidAPath);
eventQue.vidList.append(vidBPath);
}
}
}
vidAPath.clear();
vidBPath.clear();
}

53
src/detect_loop.h Normal file
View File

@ -0,0 +1,53 @@
#ifndef DETECT_LOOP_H
#define DETECT_LOOP_H
// This file is part of JustMotion.
// JustMotion 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.
// JustMotion 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"
class DetectLoop : public QFileSystemWatcher
{
Q_OBJECT
private:
QString vidAPath;
QString vidBPath;
QString vidAName;
QString vidBName;
QStringList prevClips;
QTimer *pcTimer;
QElapsedTimer eTimer;
evt_t eventQue;
shared_t *shared;
float getFloatFromExe(const QByteArray &line);
QStringList buildArgs(const QString &prev, const QString &next);
QStringList buildSnapArgs(const QString &vidSrc, const QString &imgPath);
private slots:
void init();
void reset();
void pcBreak();
void updated(const QString &path);
public:
explicit DetectLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
void exec();
QString statusLine();
};
#endif // DETECT_LOOP_H

138
src/event_loop.cpp Normal file
View File

@ -0,0 +1,138 @@
// This file is part of JustMotion.
// JustMotion 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.
// JustMotion 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 "event_loop.h"
EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QObject(parent)
{
shared = sharedRes;
heartBeat = 2;
loopTimer = 0;
connect(thr, &QThread::started, this, &EventLoop::init);
connect(thr, &QThread::finished, this, &EventLoop::deleteLater);
moveToThread(thr);
}
void EventLoop::init()
{
loopTimer = new QTimer(this);
connect(loopTimer, &QTimer::timeout, this, &EventLoop::loopSlot);
loopTimer->setSingleShot(false);
loopTimer->start(heartBeat * 1000);
loopSlot();
}
void EventLoop::loopSlot()
{
if (!exec())
{
loopTimer->stop(); QCoreApplication::exit(shared->retCode);
}
}
bool EventLoop::wrOutVod(const evt_t &event)
{
auto ret = false;
auto cnt = 0;
auto concat = shared->buffPath + "/live/" + event.timeStamp + ".ctmp";
QFile file(concat, this);
file.open(QFile::WriteOnly);
for (auto &&vid : event.vidList)
{
qInfo() << "event_src: " << vid;
if (QFile::exists(vid))
{
file.write(QString("file '" + vid + "'\n").toUtf8()); cnt++;
}
else
{
qInfo() << "warning: the event video clip does not exists";
}
}
file.close();
if (cnt == 0)
{
qCritical() << "err: none of the event hls clips exists, cancelling write out";
QFile::remove(concat);
}
else
{
QStringList args;
args << "-f";
args << "concat";
args << "-safe" << "0";
args << "-i" << concat;
args << "-c" << "copy";
args << shared->recPath + "/" + event.timeStamp + shared->recExt;
if (QProcess::execute("ffmpeg", args) == 0)
{
ret = true;
}
QFile::remove(concat);
}
return ret;
}
QString EventLoop::statusLine()
{
if (loopTimer->isActive())
{
return "OK ";
}
else
{
return "FAIL";
}
}
bool EventLoop::exec()
{
enforceMaxEvents(shared);
enforceMaxImages(shared);
enforceMaxClips(shared);
if (!shared->recList.isEmpty())
{
auto event = shared->recList.takeFirst();
qInfo() << "attempting write out of event: " << event.timeStamp;
if (wrOutVod(event))
{
QStringList args;
args << "convert";
args << event.imgPath;
args << shared->recPath + "/" + event.timeStamp + shared->thumbExt;
QProcess::execute("magick", args);
}
}
return shared->retCode == 0;
}

43
src/event_loop.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef EVENT_LOOP_H
#define EVENT_LOOP_H
// This file is part of JustMotion.
// JustMotion 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.
// JustMotion 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"
class EventLoop : public QObject
{
Q_OBJECT
private slots:
void init();
void loopSlot();
private:
shared_t *shared;
QTimer *loopTimer;
int heartBeat;
bool wrOutVod(const evt_t &event);
public:
explicit EventLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
bool exec();
QString statusLine();
};
#endif // EVENT_LOOP_H

View File

@ -12,24 +12,22 @@
#include "common.h"
#include "camera.h"
#include "services.h"
#include "proc_control.h"
void showHelp(const QString etcDir)
{
QTextStream(stdout) << APP_NAME << " " << APP_VERSION << Qt::endl << Qt::endl;
QTextStream(stdout) << "Usage: " << APP_TARGET << " <argument>" << Qt::endl << Qt::endl;
QTextStream(stdout) << "-h : display usage information about this application." << Qt::endl;
QTextStream(stdout) << "-c : path to the config file used to run a single main loop instance." << Qt::endl;
QTextStream(stdout) << "-i : all valid config files found in " << etcDir << " will be used to create" << Qt::endl;
QTextStream(stdout) << " a full instance; full meaning main and vid loop systemd services" << Qt::endl;
QTextStream(stdout) << " will be created for each config file." << Qt::endl;
QTextStream(stdout) << "-d : this is the same as -i except it will not auto start the services." << Qt::endl;
QTextStream(stdout) << "-d : all valid config files found in " << etcDir << " will be used to" << Qt::endl;
QTextStream(stdout) << " create camera instances. (this is blocking, meant to run with systemd)" << Qt::endl;
QTextStream(stdout) << "-v : display the current version." << Qt::endl;
QTextStream(stdout) << "-u : uninstall the entire app from your system, including all" << Qt::endl;
QTextStream(stdout) << " systemd services related to it." << Qt::endl;
QTextStream(stdout) << "-u : uninstall the entire app from your system, including the service." << Qt::endl;
QTextStream(stdout) << "-f : force an action without pausing for user confirmation." << Qt::endl;
QTextStream(stdout) << "-l : list all attached services to this application along with statuses." << Qt::endl;
QTextStream(stdout) << "-r : remove all attached services." << Qt::endl;
QTextStream(stdout) << "-s : view the status of all camera instances." << Qt::endl;
QTextStream(stdout) << "-q : kill all camera instances." << Qt::endl;
QTextStream(stdout) << "-r : same as -d except it is non-blocking by starting all camera instances" << Qt::endl;
QTextStream(stdout) << " via systemd. same as 'systemctl start " << APP_TARGET << "'" << Qt::endl;
}
int main(int argc, char** argv)
@ -39,9 +37,16 @@ int main(int argc, char** argv)
QCoreApplication::setApplicationName(APP_NAME);
QCoreApplication::setApplicationVersion(APP_VERSION);
auto args = QCoreApplication::arguments();
auto etcDir = "/etc/" + QString(APP_TARGET);
auto ret = 0;
auto args = QCoreApplication::arguments();
auto etcDir = "/etc/" + QString(APP_TARGET);
auto staDir = QDir::tempPath() + "/" + APP_TARGET + "-stats";
auto procFile = QDir::tempPath() + "/" + APP_TARGET + "-proc";
auto ret = 0;
if (!QFileInfo::exists(staDir))
{
mkPath(staDir);
}
if (args.contains("-h"))
{
@ -51,44 +56,30 @@ int main(int argc, char** argv)
{
QTextStream(stdout) << APP_VERSION << Qt::endl;
}
else if (args.contains("-l"))
else if (args.contains("-d"))
{
servStatByDir(etcDir);
}
else if (args.contains("-i") || args.contains("-d"))
{
ret = rmServiceByDir(etcDir);
if ((ret == 0) && args.contains("-i"))
{
ret = loadServiceByDir(etcDir, true);
}
else if (ret == 0)
{
ret = loadServiceByDir(etcDir, false);
}
}
else if (args.contains("-c"))
{
auto *cam = new Camera(&app);
auto confs = lsFilesInDir(etcDir);
auto proc = new ProcControl(procFile, staDir, &app);
ret = cam->start(args);
if (ret == 0)
if (!proc->init())
{
ret = QCoreApplication::exec();
ret = EACCES;
}
else
{
for (auto &&conf : confs)
{
new Camera(etcDir + "/" + conf, staDir, proc, &app);
}
ret = app.exec();
}
}
else if (args.contains("-u"))
{
if (args.contains("-f"))
{
ret = rmServiceByDir(etcDir);
if (ret == 0)
{
ret = QProcess::execute("/opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList());
}
ret = QProcess::execute("/opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList());
}
else
{
@ -99,18 +90,33 @@ int main(int argc, char** argv)
if (ans == 'y' || ans == 'Y')
{
ret = rmServiceByDir("/etc/mow");
if (ret == 0)
{
ret = QProcess::execute("/opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList());
}
ret = QProcess::execute("/opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList());
}
}
}
else if (args.contains("-s"))
{
auto statFiles = lsFilesInDir(staDir);
for (auto &&statFile: statFiles)
{
QFile file(staDir + "/" + statFile);
file.open(QFile::ReadOnly);
std::cout << file.readAll().data();
file.close();
}
}
else if (args.contains("-q"))
{
QFile::remove(procFile);
QThread::currentThread()->sleep(5);
}
else if (args.contains("-r"))
{
ret = rmServiceByDir(etcDir);
QProcess::execute("systemctl start " + QString(APP_TARGET));
}
else
{

108
src/proc_control.cpp Normal file
View File

@ -0,0 +1,108 @@
// This file is part of JustMotion.
// JustMotion 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.
// JustMotion 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 "proc_control.h"
ProcControl::ProcControl(const QString &procFile, const QString &statDir, QCoreApplication *parent) : QObject(parent)
{
fsMon = new QFileSystemWatcher(this);
file = procFile;
statPath = statDir;
closeOnZero = false;
objCount = 0;
connect(fsMon, &QFileSystemWatcher::fileChanged, this, &ProcControl::procFileUpdated);
}
bool ProcControl::init()
{
auto ret = false;
if (!QFileInfo::exists(file))
{
QFile fObj(file);
if (!fObj.open(QFile::WriteOnly))
{
QTextStream(stderr) << "err: Failed to open process control file for writing: " << file << " reason: " << fObj.errorString();
}
else
{
fObj.write("##");
fObj.close();
ret = true;
}
}
else
{
QTextStream(stdout) << "wrn: " << file << " already exists so it will be removed, this will cause any other existing instance to close.";
if (!QFile::remove(file))
{
QTextStream(stderr) << "err: Failed to remove process control file: " << file << " check permissions.";
}
else
{
thread()->sleep(3);
ret = init();
}
}
if (ret)
{
fsMon->addPath(file);
}
return ret;
}
void ProcControl::procFileUpdated(const QString &path)
{
fsMon->removePath(path);
QFile::remove(path);
closeApp();
}
void ProcControl::closeApp()
{
if (objCount == 0)
{
QDir(statPath).removeRecursively();
QCoreApplication::instance()->quit();
}
else
{
closeOnZero = true;
emit prepForClose();
}
}
void ProcControl::objPlusOne()
{
objCount++;
}
void ProcControl::objMinusOne()
{
objCount--;
if (closeOnZero && (objCount == 0))
{
closeApp();
}
}

51
src/proc_control.h Normal file
View File

@ -0,0 +1,51 @@
#ifndef PROC_CONTROL_H
#define PROC_CONTROL_H
// This file is part of JustMotion.
// JustMotion 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.
// JustMotion 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"
class ProcControl : public QObject
{
Q_OBJECT
private:
QFileSystemWatcher *fsMon;
QString file;
QString statPath;
bool closeOnZero;
uint objCount;
private slots:
void procFileUpdated(const QString &path);
void closeApp();
public:
explicit ProcControl(const QString &procFile, const QString &statDir, QCoreApplication *parent);
void objPlusOne();
bool init();
public slots:
void objMinusOne();
signals:
void prepForClose();
};
#endif // PROC_CONTROL_H

112
src/record_loop.cpp Normal file
View File

@ -0,0 +1,112 @@
// This file is part of JustMotion.
// JustMotion 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.
// JustMotion 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 "record_loop.h"
RecordLoop::RecordLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QProcess(parent)
{
checkTimer = 0;
shared = sharedRes;
connect(thr, &QThread::started, this, &RecordLoop::init);
connect(thr, &QThread::finished, this, &RecordLoop::deleteLater);
connect(this, &RecordLoop::readyReadStandardOutput, this, &RecordLoop::resetTime);
connect(this, &RecordLoop::readyReadStandardError, this, &RecordLoop::resetTime);
connect(this, &RecordLoop::started, this, &RecordLoop::resetTime);
moveToThread(thr);
}
RecordLoop::~RecordLoop()
{
terminate();
waitForFinished();
}
void RecordLoop::init()
{
checkTimer = new QTimer(this);
connect(checkTimer, &QTimer::timeout, this, &RecordLoop::restart);
checkTimer->setSingleShot(true);
checkTimer->setInterval(3000);
setupBuffDir(shared->buffPath);
restart();
}
QString RecordLoop::camCmdFromConf()
{
auto ret = "ffmpeg -hide_banner -y -i '" + shared->recordUri + "' -strftime 1 -strftime_mkdir 1 ";
if (shared->recordUri.contains("rtsp"))
{
ret += "-rtsp_transport udp ";
}
if (shared->vidCodec != "copy")
{
ret += "-vf fps=" + QString::number(shared->recFps) + ",scale=" + shared->recScale + " ";
}
ret += "-vcodec " + shared->vidCodec + " ";
ret += "-acodec " + shared->audCodec + " ";
ret += "-reset_timestamps 1 -sc_threshold 0 -g 2 -force_key_frames 'expr:gte(t, n_forced * 2)' -segment_time 2 -f segment ";
ret += shared->buffPath + "/live/" + QString(STRFTIME_FMT) + shared->streamExt;
return ret;
}
QString RecordLoop::statusLine()
{
if (state() == QProcess::Running)
{
return "OK ";
}
else
{
return "FAIL";
}
}
void RecordLoop::resetTime()
{
checkTimer->start();
}
void RecordLoop::restart()
{
if (state() == QProcess::Running)
{
terminate();
waitForFinished();
}
auto cmdLine = camCmdFromConf();
auto args = parseArgs(cmdLine.toUtf8(), -1);
qInfo() << "start recording command: " << cmdLine;
if (args.isEmpty())
{
qCritical() << "err: couldn't parse a program name";
}
else
{
setProgram(args[0]);
setArguments(args.mid(1));
start();
}
}

44
src/record_loop.h Normal file
View File

@ -0,0 +1,44 @@
#ifndef RECORD_LOOP_H
#define RECORD_LOOP_H
// This file is part of JustMotion.
// JustMotion 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.
// JustMotion 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"
class RecordLoop : public QProcess
{
Q_OBJECT
private slots:
void init();
void resetTime();
void restart();
private:
shared_t *shared;
QTimer *checkTimer;
QString camCmdFromConf();
public:
explicit RecordLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
~RecordLoop();
QString statusLine();
};
#endif // RECORD_LOOP_H

View File

@ -1,261 +0,0 @@
// This file is part of JustMotion.
// JustMotion 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.
// JustMotion 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 "services.h"
int loadService(const QString &desc, const QString &user, const QString &servName, const QString &workDir, const QString &recPath, bool start)
{
Q_UNUSED(recPath);
QFile file("/lib/systemd/system/" + servName + ".service");
auto ret = 0;
auto exists = file.exists();
if (!file.open(QFile::ReadWrite | QFile::Truncate))
{
QTextStream(stderr) << "err: failed to open service file: " << file.fileName() << " for writing. reason: " << file.errorString() << Qt::endl;
ret = EACCES;
}
else
{
if (exists)
{
if (ret == 0) ret = QProcess::execute("systemctl", {"stop", servName});
if (ret == 0) ret = QProcess::execute("systemctl", {"disable", servName});
}
if (ret == 0)
{
file.write("[Unit]\n");
file.write("Description=" + desc.toUtf8() + "\n");
file.write("After=network.target\n\n");
file.write("[Service]\n");
file.write("WorkingDirectory=" + workDir.toUtf8() + "\n");
file.write("Type=simple\n");
file.write("User=" + user.toUtf8() + "\n");
file.write("Restart=always\n");
if (servName.contains("vid_loop"))
{
file.write("RuntimeMaxSec=61\n");
}
file.write("ExecStart=/usr/bin/env " + servName.toUtf8() + "\n\n");
file.write("[Install]\n");
file.write("WantedBy=multi-user.target\n");
file.close();
if (ret == 0) ret = QProcess::execute("systemctl", {"daemon-reload"});
if (start)
{
if (ret == 0) ret = QProcess::execute("systemctl", {"enable", servName});
if (ret == 0) ret = QProcess::execute("systemctl", {"start", servName});
}
}
}
file.close();
return ret;
}
int loadSh(const QString &name, const QString &exeCmd, const QString &buffDir, const QString &outDir, const QString &servUser, const QString &servGroup)
{
QFile file("/usr/bin/" + name);
auto ret = 0;
if (!file.open(QFile::ReadWrite | QFile::Truncate))
{
QTextStream(stderr) << "err: failed to open shell script file: " << file.fileName() << " for writing. reason: " << file.errorString() << Qt::endl;
ret = EACCES;
}
else
{
file.write("#!/bin/sh\n\n");
file.write("cd " + buffDir.toUtf8() + "\n");
file.write(exeCmd.toUtf8() + "\n");
file.close();
mkPath(buffDir);
mkPath(outDir);
mkPath(buffDir + "/live");
mkPath(buffDir + "/img");
QProcess::execute("chmod", {"-v", "+x", file.fileName()});
QProcess::execute("chown", {servUser + ":" + servGroup, "-R", buffDir});
}
file.close();
return ret;
}
QString camCmdFromConf(shared_t *conf, CmdExeType type)
{
QString ret = "";
if (type == MAIN_LOOP)
{
ret += QString(APP_TARGET) + " -c " + conf->conf;
}
else
{
ret += "ffmpeg -hide_banner -y -i '" + conf->recordUri + "' -strftime 1 -strftime_mkdir 1 ";
if (conf->recordUri.contains("rtsp"))
{
ret += "-rtsp_transport udp ";
}
if (conf->vidCodec != "copy")
{
ret += "-vf fps=" + QString::number(conf->recFps) + ",scale=" + conf->recScale + " ";
}
ret += "-vcodec " + conf->vidCodec + " ";
ret += "-acodec " + conf->audCodec + " ";
ret += "-reset_timestamps 1 -sc_threshold 0 -g 2 -force_key_frames \"expr:gte(t, n_forced * 2)\" -t 60 -segment_time 2 -f segment ";
ret += conf->buffPath + "/live/" + QString(STRFTIME_FMT) + conf->streamExt;
}
return ret;
}
int loadServiceByConf(const QString &confFile, bool start)
{
auto ret = 0;
shared_t conf;
if (!rdConf(confFile, &conf))
{
ret = conf.retCode;
}
else
{
auto desc = QString(APP_NAME) + " - " + conf.camName;
if (ret == 0) ret = loadSh(conf.servMainLoop, camCmdFromConf(&conf, MAIN_LOOP), conf.buffPath, conf.recPath, conf.servUser, conf.servGroup);
if (ret == 0) ret = loadSh(conf.servVidLoop, camCmdFromConf(&conf, VID_LOOP), conf.buffPath, conf.recPath, conf.servUser, conf.servGroup);
if (ret == 0) ret = loadService(desc, conf.servUser, conf.servMainLoop, conf.buffPath, conf.recPath, start);
if (ret == 0) ret = loadService(desc, conf.servUser, conf.servVidLoop, conf.buffPath, conf.recPath, start);
}
return ret;
}
int rmService(const QString &servName)
{
auto path = "/lib/systemd/system/" + servName + ".service";
auto ret = 0;
if (QFile::exists(path))
{
if (ret == 0) ret = QProcess::execute("systemctl", {"stop", servName});
if (ret == 0) ret = QProcess::execute("systemctl", {"disable", servName});
QFile::remove(path);
QFile::remove("/usr/bin/" + servName);
}
return ret;
}
int rmServiceByConf(const QString &confFile)
{
shared_t conf;
if (rdConf(confFile, &conf))
{
conf.retCode = rmService(conf.servMainLoop);
conf.retCode = rmService(conf.servVidLoop);
QDir(conf.buffPath).removeRecursively();
}
return conf.retCode;
}
void servStatByConf(const QString &confFile)
{
shared_t conf;
if (rdConf(confFile, &conf))
{
QTextStream cout(stdout);
cout << "--" << conf.camName << Qt::endl;
cout << " " << conf.servMainLoop << ": ";
if (QFile::exists("/lib/systemd/system/" + conf.servMainLoop + ".service"))
{
QProcess::execute("systemctl", {"is-enabled", conf.servMainLoop}); cout << Qt::endl;
}
else
{
cout << "Not Installed" << Qt::endl;
}
cout << " " << conf.servVidLoop << ": ";
if (QFile::exists("/lib/systemd/system/" + conf.servVidLoop + ".service"))
{
QProcess::execute("systemctl", {"is-enabled", conf.servVidLoop}); cout << Qt::endl;
}
else
{
cout << "Not Installed" << Qt::endl;
}
}
}
void servStatByDir(const QString &path)
{
auto files = lsFilesInDir(path);
for (auto &&conf : files)
{
servStatByConf(QDir::cleanPath(path) + "/" + conf);
}
}
int loadServiceByDir(const QString &path, bool start)
{
auto ret = 0;
auto files = lsFilesInDir(path);
for (auto &&conf : files)
{
if (ret == 0 ) ret = loadServiceByConf(QDir::cleanPath(path) + "/" + conf, start);
}
return ret;
}
int rmServiceByDir(const QString &path)
{
auto ret = 0;
auto files = lsFilesInDir(path);
for (auto &&conf : files)
{
if (ret == 0 ) ret = rmServiceByConf(QDir::cleanPath(path) + "/" + conf);
}
return ret;
}

View File

@ -1,29 +0,0 @@
#ifndef SERVICES_H
#define SERVICES_H
// This file is part of JustMotion.
// JustMotion 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.
// JustMotion 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 camCmdFromConf(shared_t *conf, CmdExeType type);
void servStatByDir(const QString &path);
void servStatByConf(const QString &confFile);
int loadService(const QString &desc, const QString &user, const QString &servName, const QString &workDir, const QString &recPath, bool start);
int loadServiceByConf(const QString &confFile, bool start);
int loadServiceByDir(const QString &path, bool start);
int rmServiceByConf(const QString &confFile);
int rmService(const QString &servName);
int rmServiceByDir(const QString &path);
int loadSh(const QString &name, const QString &exeCmd, const QString &buffDir, const QString &outDir, const QString &servUser, const QString &servGroup);
#endif // SERVICES_H

View File

@ -0,0 +1,15 @@
[Unit]
Description=$app_name Daemon
After=network.target
[Service]
Type=simple
User=$app_target
Restart=on-failure
RestartSec=5
TimeoutStopSec=infinity
ExecStart=/usr/bin/env $app_target -d
ExecStop=/usr/bin/env $app_target -q
[Install]
WantedBy=multi-user.target

View File

@ -1,6 +1,9 @@
#!/bin/sh
$app_target -r
systemctl stop $app_target
systemctl disable $app_target
rm -v /lib/systemd/system/$app_target.service
rm -v /usr/bin/$app_target
rm -rv /tmp/$app_target-stats
rm -rv $install_dir
deluser $app_target
echo "Uninstallation Complete"