-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.
This commit is contained in:
Zii 2023-11-05 18:44:50 -05:00
parent 525c342c0f
commit 60a24c9d67
6 changed files with 174 additions and 218 deletions

View File

@ -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<int> 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;
if (QProcess::execute("ffmpeg", args) == 0)
{
ret = true;
}
QProcess::execute("ffmpeg", args);
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);
}
}
}

View File

@ -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);

View File

@ -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,19 +265,25 @@ 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())
{
share->buffPath = "/var/buffer/" + share->camName;
@ -297,6 +302,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;
if (!mkPath(share->recPath))

View File

@ -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<QString> vidList;
QString timeStamp;
QString imgPath;
float score;
bool inQue;
int queAge;
};
struct shared_t
{
QList<evt_t> 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 &param, const QString &line, int *value);
void rdLine(const QString &param, 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

View File

@ -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;

View File

@ -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;