Compare commits

...

3 Commits

Author SHA1 Message Date
zii
c2b6575800 v3.5
-The app no longer works on multi-service arch and will instead
 operate under a single master service.
2024-08-02 08:22:37 -04:00
zii
7046dd1162 v3.5.t3
-recording loop detected in "starving" state now triggers and auto
 restart of the recording loop.

-preparing to release to main.
2024-08-02 07:55:04 -04:00
zii
daad0dffa2 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.
2024-04-21 08:03:30 -04:00
21 changed files with 1091 additions and 786 deletions

View File

@ -11,10 +11,16 @@ CONFIG -= app_bundle
HEADERS += \ HEADERS += \
src/common.h \ src/common.h \
src/camera.h \ src/camera.h \
src/services.h src/detect_loop.h \
src/event_loop.h \
src/proc_control.h \
src/record_loop.h
SOURCES += \ SOURCES += \
src/common.cpp \ src/common.cpp \
src/camera.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 src/main.cpp

View File

@ -11,7 +11,7 @@ extremely lightweight with the fact it doesn't attempt to re-implement much
of it's functions internally but will instead rely on external applications of it's functions internally but will instead rely on external applications
that already implement the functions very well. that already implement the functions very well.
No user interface is implemented instead exteral applications are more than No user interface is implemented instead external applications are more than
welcome to interface with the buffer/footage directories to implement a welcome to interface with the buffer/footage directories to implement a
user interface of any flavor. user interface of any flavor.
@ -156,13 +156,14 @@ img_scale = 320:240
# rec_path. it uses width, height numeric strings seperated by a colon, # rec_path. it uses width, height numeric strings seperated by a colon,
# eg W:H. # eg W:H.
# #
service_user = mow service_user = jmotion
# this sets the service local user of the application dictating the # this sets the service local user of the application dictating the
# amount privilege it will have on the host. if not defined, the # amount of privilege it will have on the host. if not defined, the
# unprivileged user 'mow' will be used. which ever user is defined here # unprivileged user 'jmotion' will be used. which ever user is defined
# just make sure it has read/write access to buffer_path and rec_path. # 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 # this sets the service local group of the application that can further
# refine host privileges. if not defined, the name stored in # refine host privileges. if not defined, the name stored in
# service_user will be used. # service_user will be used.

View File

@ -6,6 +6,7 @@ import subprocess
import shutil import shutil
import sys import sys
import platform import platform
import pwd
def get_app_target(text): def get_app_target(text):
return re.search(r'(APP_TARGET) +(\"(.*?)\")', text).group(3) 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_run_script.sh", "app_dir/" + app_target + ".sh")
verbose_copy("templates/linux_uninstall.sh", "app_dir/uninstall.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) complete(app_ver, app_target)
@ -167,6 +169,15 @@ def list_of_words_in_text(list_of_words, text_body):
return False return False
return True return True
def user_exists(user_name):
try:
pwd.getpwnam(user_name)
return True
except KeyError:
return False
def platform_setup(): def platform_setup():
ins_packages = list_installed_packages() 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_a)
subprocess.run(["sudo", "pacman", "-S", "--noconfirm"] + dep_pkgs_b) 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(): def main():
platform_setup() platform_setup()

View File

@ -8,6 +8,7 @@ import sys
import zipfile import zipfile
import binascii import binascii
import tempfile import tempfile
import pwd
def cd(): def cd():
current_dir = os.path.dirname(__file__) current_dir = os.path.dirname(__file__)
@ -109,6 +110,7 @@ def local_install(app_target, app_name):
if make_app_dirs(app_target): 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 + ".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) 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) 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 + ".sh"])
subprocess.run(["chmod", "755", install_dir + "/" + app_target]) subprocess.run(["chmod", "755", install_dir + "/" + app_target])
subprocess.run(["chmod", "755", install_dir + "/uninstall.sh"]) 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/buffer"])
subprocess.run(["chmod", "777", "/var/footage"]) 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("Installation finished. If you ever need to uninstall this application, run this command with root rights:")
print(" sh " + install_dir + "/uninstall.sh\n") 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 False
return True return True
def user_exists(user_name):
try:
pwd.getpwnam(user_name)
return True
except KeyError:
return False
def platform_setup(): def platform_setup():
ins_packages = list_installed_packages() ins_packages = list_installed_packages()
@ -303,6 +322,7 @@ def main(is_sfx):
app_name = info[2] app_name = info[2]
if is_sfx: if is_sfx:
platform_setup()
sfx() sfx()
elif "--local" in sys.argv: elif "--local" in sys.argv:

View File

@ -12,355 +12,132 @@
#include "camera.h" #include "camera.h"
Camera::Camera(QObject *parent) : QObject(parent) {} Camera::Camera(const QString &confFile, const QString &statDir, ProcControl *proc, QCoreApplication *parent) : QObject(nullptr)
int Camera::start(const QStringList &args)
{ {
if (rdConf(getParam("-c", args), &shared)) Q_UNUSED(parent);
{
auto thr1 = new QThread(nullptr);
auto thr2 = new QThread(nullptr);
new EventLoop(&shared, thr1, nullptr); fsW = new QFileSystemWatcher(this);
new DetectLoop(&shared, thr2, nullptr); 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);
connect(detLoop, &DetectLoop::starving, recLoop, &RecordLoop::restart);
thr1->start(); thr1->start();
thr2->start(); thr2->start();
thr3->start();
objCount = 3;
} }
return shared.retCode; return shared.retCode;
} }
EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QObject(parent) void Camera::confChanged(const QString &path)
{ {
shared = sharedRes; emit stop();
heartBeat = 2;
loopTimer = 0;
connect(thr, &QThread::started, this, &EventLoop::init); if (!QFileInfo::exists(path))
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); deleteLater();
}
}
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);
} }
else else
{ {
QStringList args; auto ret = start(path);
args << "-f"; if (ret != 0)
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; deleteLater();
}
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));
}
} }
} }
} }
float DetectLoop::getFloatFromExe(const QByteArray &line) QString Camera::statusLine()
{ {
QString strLine(line); return "Detection: " + detLoop->statusLine() +
QString strNum; " Event-Scraping: " + evtLoop->statusLine() +
" Recording: " + recLoop->statusLine() +
for (auto chr : strLine) " Name: " + shared.camName + "\n";
{
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();
} }

View File

@ -14,6 +14,10 @@
// GNU General Public License for more details. // GNU General Public License for more details.
#include "common.h" #include "common.h"
#include "event_loop.h"
#include "detect_loop.h"
#include "record_loop.h"
#include "proc_control.h"
class Camera : public QObject class Camera : public QObject
{ {
@ -21,70 +25,36 @@ class Camera : public QObject
private: private:
shared_t shared; QFileSystemWatcher *fsW;
EventLoop *evtLoop;
public: DetectLoop *detLoop;
RecordLoop *recLoop;
explicit Camera(QObject *parent = nullptr); QThread *thr1;
QThread *thr2;
int start(const QStringList &args); QThread *thr3;
}; QTimer *statTimer;
QString statPath;
class EventLoop : public QObject shared_t shared;
{ uint objCount;
Q_OBJECT bool delOnZero;
private slots: private slots:
void init(); void confChanged(const QString &path);
void loopSlot(); void objMinusOne();
void updateStat();
private: void prepForDel();
shared_t *shared;
QTimer *loopTimer;
int heartBeat;
bool wrOutVod(const evt_t &event);
public: 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 signals:
{
Q_OBJECT
private: void stop();
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();
}; };
#endif // CAMERA_H #endif // CAMERA_H

View File

@ -12,43 +12,6 @@
#include "common.h" #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 lsFilesInDir(const QString &path, const QString &ext)
{ {
QStringList filters; QStringList filters;
@ -212,7 +175,6 @@ bool rdConf(const QString &filePath, shared_t *share)
share->camName.clear(); share->camName.clear();
share->buffPath.clear(); share->buffPath.clear();
share->recPath.clear(); share->recPath.clear();
share->servGroup.clear();
share->retCode = 0; share->retCode = 0;
share->imgThresh = 8000; share->imgThresh = 8000;
@ -232,7 +194,6 @@ bool rdConf(const QString &filePath, shared_t *share)
share->liveSecs = 80; share->liveSecs = 80;
share->recScale = "1280:720"; share->recScale = "1280:720";
share->imgScale = "320:240"; share->imgScale = "320:240";
share->servUser = APP_TARGET;
QString line; QString line;
@ -261,8 +222,6 @@ bool rdConf(const QString &filePath, shared_t *share)
rdLine("rec_fps = ", line, &share->recFps); rdLine("rec_fps = ", line, &share->recFps);
rdLine("rec_scale = ", line, &share->recScale); rdLine("rec_scale = ", line, &share->recScale);
rdLine("img_scale = ", line, &share->imgScale); rdLine("img_scale = ", line, &share->imgScale);
rdLine("service_user = ", line, &share->servUser);
rdLine("service_group = ", line, &share->servGroup);
rdLine("live_secs = ", line, &share->liveSecs); rdLine("live_secs = ", line, &share->liveSecs);
} }
@ -309,19 +268,34 @@ bool rdConf(const QString &filePath, shared_t *share)
{ {
share->evMaxSecs = share->liveSecs - 4; 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; 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 buildThreadCount(int count)
{ {
QString ret = "0"; QString ret = "0";

View File

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

253
src/detect_loop.cpp Normal file
View File

@ -0,0 +1,253 @@
// 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)
{
emit starving();
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();
}

57
src/detect_loop.h Normal file
View File

@ -0,0 +1,57 @@
#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();
signals:
void starving();
};
#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,23 @@
#include "common.h" #include "common.h"
#include "camera.h" #include "camera.h"
#include "services.h" #include "proc_control.h"
void showHelp(const QString etcDir) void showHelp(const QString etcDir)
{ {
QTextStream(stdout) << APP_NAME << " " << APP_VERSION << Qt::endl << Qt::endl; QTextStream(stdout) << APP_NAME << " " << APP_VERSION << Qt::endl << Qt::endl;
QTextStream(stdout) << "Usage: " << APP_TARGET << " <argument>" << 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) << "-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) << "-d : all valid config files found in " << etcDir << " will be used to" << Qt::endl;
QTextStream(stdout) << "-i : all valid config files found in " << etcDir << " will be used to create" << Qt::endl; QTextStream(stdout) << " create camera instances. (this is blocking, meant to run with systemd)" << 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) << "-v : display the current version." << 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) << "-u : uninstall the entire app from your system, including the service. all" << Qt::endl;
QTextStream(stdout) << " systemd services related to it." << Qt::endl; QTextStream(stdout) << " recorded footage will remain." << Qt::endl;
QTextStream(stdout) << "-f : force an action without pausing for user confirmation." << 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) << "-s : view the status of all camera instances." << Qt::endl;
QTextStream(stdout) << "-r : remove all attached services." << Qt::endl; QTextStream(stdout) << "-q : kill all camera instances." << Qt::endl;
QTextStream(stdout) << "-r : same as -d except it is non-blocking for starting all camera instances" << Qt::endl;
QTextStream(stdout) << " via systemd. same as 'systemctl start " << APP_TARGET << "'" << Qt::endl;
} }
int main(int argc, char** argv) int main(int argc, char** argv)
@ -39,9 +38,16 @@ int main(int argc, char** argv)
QCoreApplication::setApplicationName(APP_NAME); QCoreApplication::setApplicationName(APP_NAME);
QCoreApplication::setApplicationVersion(APP_VERSION); QCoreApplication::setApplicationVersion(APP_VERSION);
auto args = QCoreApplication::arguments(); auto args = QCoreApplication::arguments();
auto etcDir = "/etc/" + QString(APP_TARGET); auto etcDir = "/etc/" + QString(APP_TARGET);
auto ret = 0; 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")) if (args.contains("-h"))
{ {
@ -51,44 +57,30 @@ int main(int argc, char** argv)
{ {
QTextStream(stdout) << APP_VERSION << Qt::endl; QTextStream(stdout) << APP_VERSION << Qt::endl;
} }
else if (args.contains("-l")) else if (args.contains("-d"))
{ {
servStatByDir(etcDir); auto confs = lsFilesInDir(etcDir);
} auto proc = new ProcControl(procFile, staDir, &app);
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);
ret = cam->start(args); if (!proc->init())
if (ret == 0)
{ {
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")) else if (args.contains("-u"))
{ {
if (args.contains("-f")) if (args.contains("-f"))
{ {
ret = rmServiceByDir(etcDir); ret = QProcess::execute("/opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList());
if (ret == 0)
{
ret = QProcess::execute("/opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList());
}
} }
else else
{ {
@ -99,18 +91,33 @@ int main(int argc, char** argv)
if (ans == 'y' || ans == 'Y') if (ans == 'y' || ans == 'Y')
{ {
ret = rmServiceByDir("/etc/mow"); ret = QProcess::execute("/opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList());
if (ret == 0)
{
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")) else if (args.contains("-r"))
{ {
ret = rmServiceByDir(etcDir); QProcess::execute("systemctl start " + QString(APP_TARGET));
} }
else 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();
}
}

47
src/record_loop.h Normal file
View File

@ -0,0 +1,47 @@
#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();
private:
shared_t *shared;
QTimer *checkTimer;
QString camCmdFromConf();
public slots:
void restart();
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 #!/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 -v /usr/bin/$app_target
rm -rv /tmp/$app_target-stats
rm -rv $install_dir rm -rv $install_dir
deluser $app_target deluser $app_target
echo "Uninstallation Complete" echo "Uninstallation Complete"