From daad0dffa251c4c77cbcb7d27ed6d925e091e631 Mon Sep 17 00:00:00 2001 From: zii Date: Sun, 21 Apr 2024 08:03:30 -0400 Subject: [PATCH] v3.5.t2 -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. --- JustMotion.pro | 10 +- README.md | 11 +- build.py | 15 ++ install.py | 20 ++ src/camera.cpp | 431 ++++++++------------------------ src/camera.h | 80 ++---- src/common.cpp | 72 ++---- src/common.h | 10 +- src/detect_loop.cpp | 251 +++++++++++++++++++ src/detect_loop.h | 53 ++++ src/event_loop.cpp | 138 ++++++++++ src/event_loop.h | 43 ++++ src/main.cpp | 104 ++++---- src/proc_control.cpp | 108 ++++++++ src/proc_control.h | 51 ++++ src/record_loop.cpp | 112 +++++++++ src/record_loop.h | 44 ++++ src/services.cpp | 261 ------------------- src/services.h | 29 --- templates/linux_service.service | 15 ++ templates/linux_uninstall.sh | 5 +- 21 files changed, 1078 insertions(+), 785 deletions(-) create mode 100644 src/detect_loop.cpp create mode 100644 src/detect_loop.h create mode 100644 src/event_loop.cpp create mode 100644 src/event_loop.h create mode 100644 src/proc_control.cpp create mode 100644 src/proc_control.h create mode 100644 src/record_loop.cpp create mode 100644 src/record_loop.h delete mode 100644 src/services.cpp delete mode 100644 src/services.h create mode 100644 templates/linux_service.service diff --git a/JustMotion.pro b/JustMotion.pro index 97443be..69af38d 100644 --- a/JustMotion.pro +++ b/JustMotion.pro @@ -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 diff --git a/README.md b/README.md index 740dd09..d587aa9 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/build.py b/build.py index 9f62c79..e970e18 100755 --- a/build.py +++ b/build.py @@ -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() diff --git a/install.py b/install.py index 42b63b5..9478f85 100755 --- a/install.py +++ b/install.py @@ -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: diff --git a/src/camera.cpp b/src/camera.cpp index e053341..18e157e 100644 --- a/src/camera.cpp +++ b/src/camera.cpp @@ -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"; } diff --git a/src/camera.h b/src/camera.h index 949efd4..0d10f68 100644 --- a/src/camera.h +++ b/src/camera.h @@ -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 diff --git a/src/common.cpp b/src/common.cpp index 7882c8a..6e49eb6 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -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"; diff --git a/src/common.h b/src/common.h index 176107e..b3323db 100644 --- a/src/common.h +++ b/src/common.h @@ -26,11 +26,13 @@ #include #include #include +#include +#include #include 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 ¶m, const QString &line, QString *value); void rdLine(const QString ¶m, const QString &line, int *value); void rdLine(const QString ¶m, const QString &line, bool *value); diff --git a/src/detect_loop.cpp b/src/detect_loop.cpp new file mode 100644 index 0000000..89f349c --- /dev/null +++ b/src/detect_loop.cpp @@ -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(); +} diff --git a/src/detect_loop.h b/src/detect_loop.h new file mode 100644 index 0000000..c44d966 --- /dev/null +++ b/src/detect_loop.h @@ -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 diff --git a/src/event_loop.cpp b/src/event_loop.cpp new file mode 100644 index 0000000..23b5674 --- /dev/null +++ b/src/event_loop.cpp @@ -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; +} diff --git a/src/event_loop.h b/src/event_loop.h new file mode 100644 index 0000000..c61f669 --- /dev/null +++ b/src/event_loop.h @@ -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 diff --git a/src/main.cpp b/src/main.cpp index 7e7f970..78b16fb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 << " " << 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 { diff --git a/src/proc_control.cpp b/src/proc_control.cpp new file mode 100644 index 0000000..610c790 --- /dev/null +++ b/src/proc_control.cpp @@ -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(); + } +} diff --git a/src/proc_control.h b/src/proc_control.h new file mode 100644 index 0000000..a5bc970 --- /dev/null +++ b/src/proc_control.h @@ -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 diff --git a/src/record_loop.cpp b/src/record_loop.cpp new file mode 100644 index 0000000..eb949b5 --- /dev/null +++ b/src/record_loop.cpp @@ -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(); + } +} diff --git a/src/record_loop.h b/src/record_loop.h new file mode 100644 index 0000000..29e6633 --- /dev/null +++ b/src/record_loop.h @@ -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 diff --git a/src/services.cpp b/src/services.cpp deleted file mode 100644 index 4a7cb2d..0000000 --- a/src/services.cpp +++ /dev/null @@ -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; -} diff --git a/src/services.h b/src/services.h deleted file mode 100644 index cad8e7b..0000000 --- a/src/services.h +++ /dev/null @@ -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 diff --git a/templates/linux_service.service b/templates/linux_service.service new file mode 100644 index 0000000..41b19db --- /dev/null +++ b/templates/linux_service.service @@ -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 diff --git a/templates/linux_uninstall.sh b/templates/linux_uninstall.sh index 9b3ba1d..09980e9 100644 --- a/templates/linux_uninstall.sh +++ b/templates/linux_uninstall.sh @@ -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"