From 60a24c9d6798f9c3eee71a37bb56dc1dd969d693 Mon Sep 17 00:00:00 2001 From: Zii Date: Sun, 5 Nov 2023 18:44:50 -0500 Subject: [PATCH] v3.3.t4 -completely reformed the eventloop code to be more efficent and removed the use backward/forward facing functions. -added the live_secs config option that limits the amount of live footage the app will record to the buffer directory. -moved away from ffmpeg hls formatting. live footage will instead just be recorded in clips inside of the 'live' directory, the stream.m3u8 file will not longer be created. doing this removes the codec/container limitations that hls imposed. -changed up the default conf values that better suits a low spec machine like a resberrypi. --- src/camera.cpp | 280 ++++++++++++++++++++--------------------------- src/camera.h | 23 +--- src/common.cpp | 33 ++++-- src/common.h | 29 ++--- src/main.cpp | 1 + src/services.cpp | 26 +++-- 6 files changed, 174 insertions(+), 218 deletions(-) diff --git a/src/camera.cpp b/src/camera.cpp index a305e25..c5a1ed8 100644 --- a/src/camera.cpp +++ b/src/camera.cpp @@ -20,15 +20,12 @@ int Camera::start(const QStringList &args) { auto thr1 = new QThread(nullptr); auto thr2 = new QThread(nullptr); - auto thr3 = new QThread(nullptr); - new Upkeep(&shared, thr1, nullptr); - new EventLoop(&shared, thr2, nullptr); - new DetectLoop(&shared, thr3, nullptr); - + new EventLoop(&shared, thr1, nullptr); + new DetectLoop(&shared, thr2, nullptr); + thr1->start(); thr2->start(); - thr3->start(); } return shared.retCode; @@ -75,103 +72,22 @@ bool Loop::exec() return shared->retCode == 0; } -Upkeep::Upkeep(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) {} - -bool Upkeep::exec() -{ - enforceMaxEvents(shared); - enforceMaxImages(shared); - enforceMaxVids(shared); - - return Loop::exec(); -} - EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) { heartBeat = 2; - highScore = 0; - cycles = 0; } -bool EventLoop::exec() +bool EventLoop::wrOutVod(const evt_t &event) { - if (!vidList.isEmpty()) - { - vidList.removeDuplicates(); - - if (vidList.size() > 1) - { - QTextStream(stdout) << "attempting write out of event: " << name << Qt::endl; - - if (wrOutVod()) - { - QProcess proc; - QStringList args; - - args << "convert"; - args << imgPath; - args << shared->recPath + "/" + name + shared->thumbExt; - - proc.start("magick", args); - proc.waitForFinished(); - } - } - - cycles = 0; - highScore = 0; - - vidList.clear(); - } - - QList rmIndx; - - for (auto i = 0; i < shared->recList.size(); ++i) - { - auto event = &shared->recList[i]; - - if (highScore < event->score) - { - name = event->timeStamp.toString(DATETIME_FMT); - imgPath = event->imgPath; - highScore = event->score; - } - - if (event->queAge >= (shared->evMaxSecs / heartBeat)) - { - auto maxSecs = shared->evMaxSecs / 2; - // half the maxsecs value to get front-back half secs - - auto backFiles = backwardFacingFiles(shared->buffPath + "/live", shared->streamExt, event->timeStamp, maxSecs); - auto frontFiles = forwardFacingFiles(shared->buffPath + "/live", shared->streamExt, event->timeStamp, maxSecs); - - vidList.append(backFiles + frontFiles); - rmIndx.append(i); - } - else - { - event->queAge += heartBeat; - } - } - - for (auto i : rmIndx) - { - shared->recList.removeAt(i); - } - - return Loop::exec(); -} - -bool EventLoop::wrOutVod() -{ - auto cnt = 0; - auto concat = name + ".tmp"; auto ret = false; + auto cnt = 0; + auto concat = shared->buffPath + "/live/" + event.timeStamp + ".ctmp"; - QFile file(concat); + QFile file(concat, this); file.open(QFile::WriteOnly); - for (auto &&vid : vidList) + for (auto &&vid : event.vidList) { QTextStream(stdout) << "event_src: " << vid << Qt::endl; @@ -189,7 +105,7 @@ bool EventLoop::wrOutVod() if (cnt == 0) { - QTextStream(stderr) << "err: none of the event hls clips exists, canceling write out." << Qt::endl; + QTextStream(stderr) << "err: none of the event hls clips exists, cancelling write out." << Qt::endl; QFile::remove(concat); } @@ -202,29 +118,59 @@ bool EventLoop::wrOutVod() args << "-safe" << "0"; args << "-i" << concat; args << "-c" << "copy"; - args << shared->recPath + "/" + name + shared->recExt; + args << shared->recPath + "/" + event.timeStamp + shared->recExt; - QProcess::execute("ffmpeg", args); + if (QProcess::execute("ffmpeg", args) == 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 Loop::exec(); +} + DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) { - seed = 0; - pcTimer = 0; - heartBeat = 2; - delayCycles = 12; // this will be used to delay the - // actual start of DetectLoop by - // 24secs. + pcTimer = 0; + heartBeat = 2; } void DetectLoop::init() { pcTimer = new QTimer(this); - mod = false; + + eventQue.queAge = 0; + eventQue.score = 0; + eventQue.inQue = false; connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak); @@ -244,15 +190,12 @@ void DetectLoop::pcBreak() { QTextStream(stdout) << "---POST_BREAK---" << Qt::endl; - if (mod) + if (eventQue.inQue) { QTextStream(stdout) << "motion detected, skipping the post command." << Qt::endl; } else { - if (delayCycles == 0) delayCycles = 5; - else delayCycles += 5; - QTextStream(stdout) << "no motion detected, running post command: " << shared->postCmd << Qt::endl; auto args = parseArgs(shared->postCmd.toUtf8(), -1); @@ -267,8 +210,6 @@ void DetectLoop::pcBreak() } } } - - mod = false; } float DetectLoop::getFloatFromExe(const QByteArray &line) @@ -288,7 +229,16 @@ float DetectLoop::getFloatFromExe(const QByteArray &line) } } - return strNum.toFloat(); + 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) @@ -308,6 +258,7 @@ QStringList DetectLoop::buildSnapArgs(const QString &vidSrc, const QString &imgP { QStringList ret; + ret.append("-hide_banner"); ret.append("-y"); ret.append("-i"); ret.append(vidSrc); @@ -320,88 +271,95 @@ QStringList DetectLoop::buildSnapArgs(const QString &vidSrc, const QString &imgP bool DetectLoop::exec() { - if (delayCycles > 0) + if (eventQue.inQue) { - delayCycles -= 1; + eventQue.queAge += heartBeat; + } - QTextStream(stdout) << "delay: detection cycle skipped. cycles left to be skipped: " << QString::number(delayCycles) << Qt::endl; + auto clips = lsFilesInDir(shared->buffPath + "/live", shared->streamExt); + + if (clips.size() < 2) + { + QTextStream(stdout) << "warning: didn't pick up enough clips files from the video stream. number of files: " << QString::number(clips.size()) << Qt::endl; + QTextStream(stdout) << " will try again on the next loop." << Qt::endl; } else { - auto imgAPath = "img/" + QString::number(seed++) + ".bmp"; - auto imgBPath = "img/" + QString::number(seed++) + ".bmp"; - auto curDT = QDateTime::currentDateTime(); - auto clips = backwardFacingFiles("live", shared->streamExt, curDT, 16); + auto vidAPath = shared->buffPath + "/live/" + clips[clips.size() - 2]; + auto vidBPath = shared->buffPath + "/live/" + clips[clips.size() - 1]; + 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 (clips.size() < 2) + if (compArgs.isEmpty()) { - QTextStream(stdout) << "warning: didn't pick up enough clips files from the video stream. number of files: " << QString::number(clips.size()) << Qt::endl; - QTextStream(stdout) << " will try again on the next loop." << Qt::endl; + QTextStream(stderr) << "err: could not parse a executable name from img_comp_cmd: " << shared->compCmd << Qt::endl; } else { - auto snapArgsA = buildSnapArgs(clips[clips.size() - 2], imgAPath); - auto snapArgsB = buildSnapArgs(clips[clips.size() - 1], imgAPath); - auto compArgs = buildArgs(imgAPath, imgBPath); + QProcess::execute("ffmpeg", snapArgsA); + QProcess::execute("ffmpeg", snapArgsB); - if (compArgs.isEmpty()) + if (QFile::exists(imgAPath) && QFile::exists(imgBPath)) { - QTextStream(stderr) << "err: could not parse a executable name from img_comp_cmd: " << shared->compCmd << Qt::endl; - } - else - { - QProcess snapFromVidA; - QProcess snapFromVidB; QProcess extComp; - snapFromVidA.start("ffmpeg", snapArgsA); - snapFromVidA.waitForFinished(); - - snapFromVidB.start("ffmpeg", snapArgsB); - snapFromVidB.waitForFinished(); - extComp.start(compArgs[0], compArgs.mid(1)); extComp.waitForFinished(); float score = 0; - auto ok = true; if (shared->outputType == "stdout") { score = getFloatFromExe(extComp.readAllStandardOutput()); } - else if (shared->outputType == "stderr") + else { score = getFloatFromExe(extComp.readAllStandardError()); } - else - { - ok = false; - } - if (!ok) - { - QTextStream(stderr) << "err: img_comp_out: " << shared->outputType << " is not valid. it must be 'stdout' or 'stderr'" << Qt::endl; - } - else - { - QTextStream(stdout) << compArgs.join(" ") << " --result: " << QString::number(score) << Qt::endl; + QTextStream(stdout) << compArgs.join(" ") << " --result: " << QString::number(score) << Qt::endl; - if (score >= shared->imgThresh) + if (eventQue.inQue) + { + if (eventQue.score < score) { - QTextStream(stdout) << "--threshold_breached: " << QString::number(shared->imgThresh) << Qt::endl; - - evt_t event; - - event.timeStamp = curDT; - event.score = score; - event.imgPath = imgBPath; - event.queAge = 0; - - mod = true; - - shared->recList.append(event); + 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); + + eventQue.imgPath.clear(); + eventQue.vidList.clear(); + eventQue.timeStamp.clear(); + + eventQue.score = 0; + eventQue.queAge = 0; + } + } + else if (score >= shared->imgThresh) + { + QTextStream(stdout) << "--threshold_breached: " << 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.clear(); + eventQue.vidList.append(vidAPath); + eventQue.vidList.append(vidBPath); } } } diff --git a/src/camera.h b/src/camera.h index 3897cbf..b9f5e75 100644 --- a/src/camera.h +++ b/src/camera.h @@ -55,30 +55,13 @@ public: virtual bool exec(); }; -class Upkeep : public Loop -{ - Q_OBJECT - -public: - - explicit Upkeep(shared_t *shared, QThread *thr, QObject *parent = nullptr); - - bool exec(); -}; - class EventLoop : public Loop { Q_OBJECT private: - QStringList vidList; - QString imgPath; - QString name; - float highScore; - uint cycles; - - bool wrOutVod(); + bool wrOutVod(const evt_t &event); public: @@ -94,9 +77,7 @@ class DetectLoop : public Loop private: QTimer *pcTimer; - uint delayCycles; - bool mod; - quint64 seed; + evt_t eventQue; void resetTimers(); float getFloatFromExe(const QByteArray &line); diff --git a/src/common.cpp b/src/common.cpp index 73bc1ce..a950d12 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -129,7 +129,7 @@ void enforceMaxImages(shared_t *share) { auto names = lsFilesInDir(share->buffPath + "/img", ".bmp"); - while (names.size() > MAX_IMAGES) + while (names.size() > (share->liveSecs / 2)) { QFile::remove(share->buffPath + "/img/" + names[0]); @@ -137,11 +137,11 @@ void enforceMaxImages(shared_t *share) } } -void enforceMaxVids(shared_t *share) +void enforceMaxClips(shared_t *share) { auto names = lsFilesInDir(share->buffPath + "/live", share->streamExt); - while (names.size() > MAX_VIDEOS) + while (names.size() > (share->liveSecs / 2)) { QFile::remove(share->buffPath + "/live/" + names[0]); @@ -217,8 +217,7 @@ bool rdConf(const QString &filePath, shared_t *share) share->retCode = 0; share->imgThresh = 8000; - share->maxEvents = 100; - share->maxLogSize = 100000; + share->maxEvents = 30; share->skipCmd = false; share->postSecs = 60; share->evMaxSecs = 30; @@ -226,12 +225,13 @@ bool rdConf(const QString &filePath, shared_t *share) share->outputType = "stderr"; share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null"; share->streamCodec = "copy"; - share->streamExt = ".ts"; - share->recExt = ".mp4"; + share->streamExt = ".avi"; + share->recExt = ".avi"; share->thumbExt = ".jpg"; share->imgThreads = thrCount; share->recThreads = thrCount; share->recFps = 30; + share->liveSecs = 80; share->recScale = "1280:720"; share->imgScale = "320:240"; share->servUser = APP_BIN; @@ -253,7 +253,6 @@ bool rdConf(const QString &filePath, shared_t *share) rdLine("post_cmd = ", line, &share->postCmd); rdLine("img_thresh = ", line, &share->imgThresh); rdLine("max_events = ", line, &share->maxEvents); - rdLine("max_log_size = ", line, &share->maxLogSize); rdLine("img_comp_out = ", line, &share->outputType); rdLine("img_comp_cmd = ", line, &share->compCmd); rdLine("stream_codec = ", line, &share->streamCodec); @@ -266,18 +265,24 @@ bool rdConf(const QString &filePath, shared_t *share) rdLine("rec_scale = ", line, &share->recScale); rdLine("img_scale = ", line, &share->imgScale); rdLine("service_user = ", line, &share->servUser); + rdLine("live_secs = ", line, &share->liveSecs); } } while(!line.isEmpty()); if (share->camName.isEmpty()) { - share->camName = QFileInfo(share->conf).fileName(); + share->camName = QFileInfo(share->conf).baseName(); } extCorrection(share->streamExt); extCorrection(share->recExt); extCorrection(share->thumbExt); + + if (share->outputType != "stdout" && share->outputType != "stderr") + { + share->outputType = "stderr"; + } if (share->buffPath.isEmpty()) { @@ -296,6 +301,16 @@ bool rdConf(const QString &filePath, shared_t *share) { share->recPath = QDir::cleanPath(share->recPath); } + + if (share->liveSecs < 10) + { + share->liveSecs = 10; + } + + if ((share->liveSecs - 4) < share->evMaxSecs) + { + share->evMaxSecs = share->liveSecs - 4; + } share->retCode = EACCES; diff --git a/src/common.h b/src/common.h index 7529119..6a2e76c 100644 --- a/src/common.h +++ b/src/common.h @@ -29,42 +29,34 @@ using namespace std; -#define APP_VER "3.3.t3" +#define APP_VER "3.3.t4" #define APP_NAME "Motion Watch" #define APP_BIN "mow" -#define REC_LOG_NAME "rec.log" -#define DET_LOG_NAME "det.log" -#define UPK_LOG_NAME "upk.log" #define DATETIME_FMT "yyyyMMddhhmmss" #define STRFTIME_FMT "%Y%m%d%H%M%S" -#define MAX_IMAGES 1000 -#define MAX_VIDEOS 1000 #define PREV_IMG "&prev&" #define NEXT_IMG "&next&" enum CmdExeType { MAIN_LOOP, - VID_LOOP, - IMG_LOOP + VID_LOOP }; struct evt_t { - QDateTime timeStamp; - QString imgPath; - float score; - uint queAge; + QList vidList; + QString timeStamp; + QString imgPath; + float score; + bool inQue; + int queAge; }; struct shared_t { QList recList; - QMutex recMutex; - QMutex logMutex; QString conf; - QString recLog; - QString detLog; QString recordUri; QString buffPath; QString postCmd; @@ -80,10 +72,10 @@ struct shared_t QString imgScale; QString servMainLoop; QString servVidLoop; - QString servImgLoop; QString servUser; bool singleTenant; bool skipCmd; + int liveSecs; int recFps; int imgThreads; int recThreads; @@ -91,7 +83,6 @@ struct shared_t int postSecs; int imgThresh; int maxEvents; - int maxLogSize; int retCode; }; @@ -110,7 +101,7 @@ void rdLine(const QString ¶m, const QString &line, int *value); void rdLine(const QString ¶m, const QString &line, bool *value); void enforceMaxEvents(shared_t *share); void enforceMaxImages(shared_t *share); -void enforceMaxVids(shared_t *share); +void enforceMaxClips(shared_t *share); void extCorrection(QString &ext); #endif // COMMON_H diff --git a/src/main.cpp b/src/main.cpp index 66b3e5a..c74698b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,7 @@ void showHelp() QTextStream(stdout) << "-i : all valid config files found in /etc/mow 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) << "-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; diff --git a/src/services.cpp b/src/services.cpp index 55adaf0..fbe57ef 100644 --- a/src/services.cpp +++ b/src/services.cpp @@ -103,17 +103,21 @@ QString camCmdFromConf(shared_t *conf, CmdExeType type) } else { - ret += "taskset -c " + buildThreadCount(conf->recThreads) + " "; - ret += "ffmpeg -hide_banner -i '" + conf->recordUri + "' -strftime 1 -strftime_mkdir 1 "; + ret += "ffmpeg -hide_banner -y -i '" + conf->recordUri + "' -strftime 1 -strftime_mkdir 1 "; if (conf->recordUri.contains("rtsp")) { ret += "-rtsp_transport tcp "; } - - ret += "-vf fps=" + QString::number(conf->recFps) + ",scale=" + conf->recScale + " "; - ret += "-hls_segment_filename live/" + QString(STRFTIME_FMT) + conf->streamExt + " "; - ret += "-y -vcodec " + conf->streamCodec + " -f hls -hls_time 2 -hls_list_size 1000 -hls_flags append_list+omit_endlist -t 60 stream.m3u8"; + + if (conf->streamCodec != "copy") + { + ret += "-vf fps=" + QString::number(conf->recFps) + ",scale=" + conf->recScale + " "; + } + + ret += "-vcodec " + conf->streamCodec + " "; + 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; @@ -150,8 +154,8 @@ int rmService(const QString &servName) if (QFile::exists(path)) { - ret = QProcess::execute("systemctl", {"stop", servName}); - ret = QProcess::execute("systemctl", {"disable", servName}); + 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); @@ -168,6 +172,12 @@ int rmServiceByConf(const QString &confFile) { conf.retCode = rmService(conf.servMainLoop); conf.retCode = rmService(conf.servVidLoop); + + QDir live(conf.buffPath + "/live"); + QDir img(conf.buffPath + "/img"); + + live.removeRecursively(); + img.removeRecursively(); } return conf.retCode;