diff --git a/imgmagick_build.sh b/imgmagick_build.sh index 74f4e07..fd70d82 100644 --- a/imgmagick_build.sh +++ b/imgmagick_build.sh @@ -1,7 +1,9 @@ #!/bin/sh export DEBIAN_FRONTEND=noninteractive -if ! command -v magick &> /dev/null +magick -version &> /dev/null + +if [ ! $? -eq 0 ] then apt install -y git git clone https://github.com/ImageMagick/ImageMagick.git .build-imagemagick diff --git a/install.sh b/install.sh index 6aec6fa..502e790 100644 --- a/install.sh +++ b/install.sh @@ -7,20 +7,20 @@ if [ ! -d "/opt/mow" ]; then mkdir /opt/mow fi -if [ ! -d "/var/opt/mow" ]; then - mkdir /var/opt/mow +if [ ! -d "/var/footage" ]; then + mkdir /var/footage fi if [ ! -d "/etc/mow" ]; then mkdir /etc/mow fi -if [ ! -d "/var/opt/mow/buf" ]; then - mkdir /var/opt/mow/buf +if [ ! -d "/var/buffer" ]; then + mkdir /var/buffer fi -if [ ! -d "/var/opt/mow/rec" ]; then - mkdir /var/opt/mow/rec +if [ ! -d "/var/mow_serv" ]; then + mkdir /var/mow_serv fi cp -v ./.build-mow/mow /opt/mow/bin @@ -36,12 +36,13 @@ printf "rm -v /opt/mow/run\n" >> /opt/mow/uninst printf "rm -v /opt/mow/uninst\n" >> /opt/mow/uninst printf "rm -v /usr/bin/mow\n" >> /opt/mow/uninst printf "rm -rv /opt/mow\n" >> /opt/mow/uninst -printf "rm -r /var/opt/mow/buf\n" >> /opt/mow/uninst printf "deluser mow\n" >> /opt/mow/uninst useradd -r mow -chown -R mow:mow /var/opt/mow +chown -R -v mow:mow /var/footage +chown -R -v mow:mow /var/buffer +chown -R -v mow:mow /var/mow_serv chmod -v +x /opt/mow/run chmod -v +x /opt/mow/bin diff --git a/setup.sh b/setup.sh index 1c53c2a..60928a7 100644 --- a/setup.sh +++ b/setup.sh @@ -2,5 +2,25 @@ export DEBIAN_FRONTEND=noninteractive apt update -y apt install -y pkg-config cmake make g++ -apt install -y ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev x264 libx264-dev libilmbase-dev qt6-base-dev qtchooser qmake6 qt6-base-dev-tools libxkbcommon-dev libfuse-dev fuse3 -sh imgmagick_build.sh + +if [ $? -eq 0 ] +then + apt install -y ffmpeg + apt install -y libavcodec-dev + apt install -y libavformat-dev + apt install -y libavutil-dev + apt install -y libswscale-dev + apt install -y x264 + apt install -y libx264-dev + apt install -y libilmbase-dev + apt install -y qt6-base-dev + apt install -y qtchooser + apt install -y qmake6 + apt install -y qt6-base-dev-tools + apt install -y libxkbcommon-dev + apt install -y libfuse-dev + apt install -y fuse3 + sh imgmagick_build.sh +fi + + diff --git a/src/camera.cpp b/src/camera.cpp index 370a436..17370d7 100644 --- a/src/camera.cpp +++ b/src/camera.cpp @@ -14,54 +14,24 @@ Camera::Camera(QObject *parent) : QObject(parent) {} -void Camera::cleanup() -{ - if (QFileInfo::exists(shared.tmpDir + "/rec")) - { - QProcess::execute("rm", {shared.tmpDir + "/rec"}); - } -} - int Camera::start(const QStringList &args) { if (rdConf(getParam("-c", args), &shared)) - { - QProcess::execute("mkdir", {"-p", shared.outDir}); - QProcess::execute("mkdir", {"-p", shared.tmpDir}); + { + auto thr1 = new QThread(nullptr); + auto thr2 = new QThread(nullptr); + auto thr3 = new QThread(nullptr); + auto thr4 = new QThread(nullptr); - QProcess::execute("mkdir", {"-p", shared.tmpDir + "/live"}); - QProcess::execute("mkdir", {"-p", shared.tmpDir + "/logs"}); - QProcess::execute("mkdir", {"-p", shared.tmpDir + "/img"}); + new RecLoop(&shared, thr1, nullptr); + new Upkeep(&shared, thr2, nullptr); + new EventLoop(&shared, thr3, nullptr); + new DetectLoop(&shared, thr4, nullptr); - cleanup(); - - touch(shared.tmpDir + "/stream.m3u8"); - - QProcess::execute("ln", {"-s", shared.outDir, shared.tmpDir + "/rec"}); - - if (!QDir::setCurrent(shared.tmpDir)) - { - QTextStream(stderr) << "err: failed to change/create the current working directory to camera folder: '" << shared.outDir << "' does it exists?" << Qt::endl; - - shared.retCode = ENOENT; - } - else - { - auto thr1 = new QThread(nullptr); - auto thr2 = new QThread(nullptr); - auto thr3 = new QThread(nullptr); - auto thr4 = new QThread(nullptr); - - new RecLoop(&shared, thr1, nullptr); - new Upkeep(&shared, thr2, nullptr); - new EventLoop(&shared, thr3, nullptr); - new DetectLoop(&shared, thr4, nullptr); - - thr1->start(); - thr2->start(); - thr3->start(); - thr4->start(); - } + thr1->start(); + thr2->start(); + thr3->start(); + thr4->start(); } return shared.retCode; @@ -130,11 +100,15 @@ void RecLoop::updateCmd() { QStringList recArgs; QStringList imgArgs; - + + recArgs << "-c"; + recArgs << buildThreadCount(shared->recThreads); + recArgs << "ffmpeg"; recArgs << "-hide_banner"; recArgs << "-i" << shared->recordUri; recArgs << "-strftime" << "1"; recArgs << "-strftime_mkdir" << "1"; + recArgs << "-vf" << "fps=" + QString::number(shared->recFps) + ",scale=" + shared->recScale; recArgs << "-hls_segment_filename" << "live/" + QString(STRFTIME_FMT) + shared->streamExt; recArgs << "-y"; recArgs << "-vcodec" << shared->streamCodec; @@ -142,23 +116,38 @@ void RecLoop::updateCmd() recArgs << "-hls_time" << "2"; recArgs << "-hls_list_size" << "1000"; recArgs << "-hls_flags" << "append_list+omit_endlist"; - recArgs << "-rtsp_transport" << "tcp"; + + if (shared->recordUri.contains("rtsp")) + { + recArgs << "-rtsp_transport" << "tcp"; + } + recArgs << "-t" << QString::number(heartBeat); recArgs << "stream.m3u8"; - + + imgArgs << "-c"; + imgArgs << buildThreadCount(shared->imgThreads); + imgArgs << "ffmpeg"; imgArgs << "-hide_banner"; imgArgs << "-i" << shared->recordUri; imgArgs << "-strftime" << "1"; imgArgs << "-strftime_mkdir" << "1"; - imgArgs << "-vf" << "fps=1,scale=320:240"; - imgArgs << "-rtsp_transport" << "tcp"; + imgArgs << "-vf" << "fps=1,scale=" + shared->imgScale; + + if (shared->recordUri.contains("rtsp")) + { + imgArgs << "-rtsp_transport" << "tcp"; + } + imgArgs << "-t" << QString::number(heartBeat); imgArgs << "img/" + QString(STRFTIME_FMT) + ".bmp"; - recProc->setProgram("ffmpeg"); + recProc->setWorkingDirectory(shared->tmpDir); + recProc->setProgram("taskset"); recProc->setArguments(recArgs); - imgProc->setProgram("ffmpeg"); + imgProc->setWorkingDirectory(shared->tmpDir); + imgProc->setProgram("taskset"); imgProc->setArguments(imgArgs); recLog("rec_args: " + recArgs.join(" "), shared); @@ -182,17 +171,14 @@ void RecLoop::term() void RecLoop::procError(const QString &desc, QProcess *proc) { - if (proc->isOpen() && (proc->state() != QProcess::Running)) - { - auto errBlob = QString(proc->readAllStandardError()); - auto errLines = errBlob.split('\n'); + auto errBlob = QString(proc->readAllStandardError()); + auto errLines = errBlob.split('\n'); - if (!errLines.isEmpty()) + if (!errLines.isEmpty()) + { + for (auto &&line : errLines) { - for (auto &&line : errLines) - { - recLog(desc + "_cmd_stderr: " + line, shared); - } + recLog(desc + "_cmd_stderr: " + line, shared); } } } @@ -216,11 +202,11 @@ Upkeep::Upkeep(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(shared bool Upkeep::exec() { - enforceMaxLogSize(QString("logs/") + REC_LOG_NAME, shared); - enforceMaxLogSize(QString("logs/") + DET_LOG_NAME, shared); + enforceMaxLogSize(shared->tmpDir + "/logs/" + REC_LOG_NAME, shared); + enforceMaxLogSize(shared->tmpDir + "/logs/" + DET_LOG_NAME, shared); - dumpLogs(QString("logs/") + REC_LOG_NAME, shared->recLog); - dumpLogs(QString("logs/") + DET_LOG_NAME, shared->detLog); + dumpLogs(shared->tmpDir + "/logs/" + REC_LOG_NAME, shared->recLog); + dumpLogs(shared->tmpDir + "/logs/" + DET_LOG_NAME, shared->detLog); shared->logMutex.lock(); shared->recLog.clear(); @@ -228,7 +214,7 @@ bool Upkeep::exec() shared->logMutex.unlock(); enforceMaxEvents(shared); - enforceMaxImages(); + enforceMaxImages(shared); enforceMaxVids(shared); return Loop::exec(); @@ -258,7 +244,7 @@ bool EventLoop::exec() args << "convert"; args << imgPath; - args << "rec/" + name + shared->thumbExt; + args << shared->outDir + "/" + name + shared->thumbExt; proc.start("magick", args); proc.waitForFinished(); @@ -291,8 +277,8 @@ bool EventLoop::exec() auto maxSecs = shared->evMaxSecs / 2; // half the maxsecs value to get front-back half secs - auto backFiles = backwardFacingFiles("live", shared->streamExt, event->timeStamp, maxSecs); - auto frontFiles = forwardFacingFiles("live", shared->streamExt, event->timeStamp, maxSecs); + auto backFiles = backwardFacingFiles(shared->tmpDir + "/live", shared->streamExt, event->timeStamp, maxSecs); + auto frontFiles = forwardFacingFiles(shared->tmpDir + "/live", shared->streamExt, event->timeStamp, maxSecs); vidList.append(backFiles + frontFiles); rmIndx.append(i); @@ -351,7 +337,7 @@ bool EventLoop::wrOutVod() args << "-safe" << "0"; args << "-i" << concat; args << "-c" << "copy"; - args << "rec/" + name + shared->recExt; + args << shared->outDir + "/" + name + shared->recExt; proc.setProgram("ffmpeg"); proc.setArguments(args); diff --git a/src/camera.h b/src/camera.h index dee79db..883d42e 100644 --- a/src/camera.h +++ b/src/camera.h @@ -24,8 +24,6 @@ private: shared_t shared; - void cleanup(); - public: explicit Camera(QObject *parent = nullptr); diff --git a/src/common.cpp b/src/common.cpp index 0d0d39d..8e27bfd 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -107,31 +107,31 @@ QStringList forwardFacingFiles(const QString &path, const QString &ext, const QD void enforceMaxEvents(shared_t *share) { - auto names = lsFilesInDir("events", ".mp4"); + auto names = lsFilesInDir(share->outDir, share->recExt); while (names.size() > share->maxEvents) { - auto nameOnly = "events/" + names[0]; + auto nameOnly = share->outDir + "/" + names[0]; - nameOnly.remove(".mp4"); + nameOnly.chop(share->recExt.size()); - auto mp4File = nameOnly + ".mp4"; - auto imgFile = nameOnly + ".jpg"; + auto vidFile = nameOnly + share->recExt; + auto imgFile = nameOnly + share->thumbExt; - QFile::remove(mp4File); + QFile::remove(vidFile); QFile::remove(imgFile); names.removeFirst(); } } -void enforceMaxImages() +void enforceMaxImages(shared_t *share) { - auto names = lsFilesInDir("img", ".bmp"); + auto names = lsFilesInDir(share->tmpDir + "/img", ".bmp"); while (names.size() > MAX_IMAGES) { - QFile::remove("img/" + names[0]); + QFile::remove(share->tmpDir + "/img/" + names[0]); names.removeFirst(); } @@ -139,11 +139,11 @@ void enforceMaxImages() void enforceMaxVids(shared_t *share) { - auto names = lsFilesInDir("live", share->streamExt); + auto names = lsFilesInDir(share->tmpDir + "/live", share->streamExt); while (names.size() > MAX_VIDEOS) { - QFile::remove("live/" + names[0]); + QFile::remove(share->tmpDir + "/live/" + names[0]); names.removeFirst(); } @@ -169,22 +169,9 @@ void rdLine(const QString ¶m, const QString &line, bool *value) { if (line.startsWith(param)) { - *value = (line == "y" || line == "Y"); - } -} - -void touch(const QString &path) -{ - if (!QFile::exists(path)) - { - QFile file(path); - - if (file.open(QFile::WriteOnly)) - { - file.write(""); - } - - file.close(); + auto val = line.mid(param.size()).trimmed(); + + *value = (val == "y" || val == "Y"); } } @@ -196,6 +183,18 @@ void extCorrection(QString &ext) } } +bool mkPath(const QString &path) +{ + auto ret = true; + + if (!QDir().exists(path)) + { + ret = QDir().mkpath(path); + } + + return ret; +} + bool rdConf(const QString &filePath, shared_t *share) { QFile varFile(filePath); @@ -204,13 +203,15 @@ bool rdConf(const QString &filePath, shared_t *share) { share->retCode = ENOENT; - QTextStream(stderr) << "err: config file: " << filePath << " does not exists or lack read permissions." << Qt::endl; + QTextStream(stderr) << "err: config file - " << filePath << " does not exists or lack read permissions." << Qt::endl; } else { share->recordUri.clear(); share->postCmd.clear(); share->camName.clear(); + + auto thrCount = QThread::idealThreadCount() / 2; share->retCode = 0; share->imgThresh = 8000; @@ -220,8 +221,8 @@ bool rdConf(const QString &filePath, shared_t *share) share->postSecs = 60; share->evMaxSecs = 30; share->conf = filePath; - share->buffPath = "/var/opt/" + QString(APP_BIN) + "/buf"; - share->recRoot = "/var/opt/" + QString(APP_BIN) + "/rec"; + share->buffPath = "/var/buffer"; + share->recPath = "/var/footage"; share->outputType = "stderr"; share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null"; share->streamCodec = "copy"; @@ -229,6 +230,11 @@ bool rdConf(const QString &filePath, shared_t *share) share->recExt = ".mp4"; share->thumbExt = ".jpg"; share->singleTenant = false; + share->imgThreads = thrCount; + share->recThreads = thrCount; + share->recFps = 30; + share->recScale = "1280:720"; + share->imgScale = "320:240"; QString line; @@ -241,7 +247,7 @@ bool rdConf(const QString &filePath, shared_t *share) rdLine("cam_name = ", line, &share->camName); rdLine("recording_uri = ", line, &share->recordUri); rdLine("buffer_path = ", line, &share->buffPath); - rdLine("rec_root = ", line, &share->recRoot); + rdLine("rec_path = ", line, &share->recPath); rdLine("max_event_secs = ", line, &share->evMaxSecs); rdLine("post_secs = ", line, &share->postSecs); rdLine("post_cmd = ", line, &share->postCmd); @@ -255,6 +261,11 @@ bool rdConf(const QString &filePath, shared_t *share) rdLine("rec_ext = ", line, &share->recExt); rdLine("thumbnail_ext = ", line, &share->thumbExt); rdLine("single_tenant = ", line, &share->singleTenant); + rdLine("img_threads = ", line, &share->imgThreads); + rdLine("rec_threads = ", line, &share->recThreads); + rdLine("rec_fps = ", line, &share->recFps); + rdLine("rec_scale = ", line, &share->recScale); + rdLine("img_scale = ", line, &share->imgScale); } } while(!line.isEmpty()); @@ -270,16 +281,56 @@ bool rdConf(const QString &filePath, shared_t *share) if (share->singleTenant) { - share->outDir = QDir().cleanPath(share->recRoot); + share->outDir = QDir().cleanPath(share->recPath); share->tmpDir = QDir().cleanPath(share->buffPath); } else { - share->outDir = QDir().cleanPath(share->recRoot) + "/" + share->camName; + share->outDir = QDir().cleanPath(share->recPath) + "/" + share->camName; share->tmpDir = QDir().cleanPath(share->buffPath) + "/" + share->camName; } - share->servPath = QString("/var/opt/") + APP_BIN + "/" + APP_BIN + "." + share->camName + ".service"; + auto servDir = QString("/var/") + APP_BIN + QString("_serv"); + + share->retCode = EACCES; + + if (!mkPath(servDir)) + { + QTextStream(stderr) << "err: failed to create service directory - " << servDir << " check for write permissions." << Qt::endl; + } + else if (!mkPath(share->recPath)) + { + QTextStream(stderr) << "err: failed to create root recording directory - " << share->recPath << " check for write permissions." << Qt::endl; + } + else if (!mkPath(share->buffPath)) + { + QTextStream(stderr) << "err: failed to create root buffer directory - " << share->buffPath << " check for write permissions." << Qt::endl; + } + else if (!mkPath(share->outDir)) + { + QTextStream(stderr) << "err: failed to create recording directory - " << share->outDir << " check for write permissions." << Qt::endl; + } + else if (!mkPath(share->tmpDir)) + { + QTextStream(stderr) << "err: failed to create buffer directory - " << share->tmpDir << " check for write permissions." << Qt::endl; + } + else if (!mkPath(share->tmpDir + "/live")) + { + QTextStream(stderr) << "err: failed to create 'live' in the buffer directory - " << share->tmpDir << "/live" << " check for write permissions." << Qt::endl; + } + else if (!mkPath(share->tmpDir + "/logs")) + { + QTextStream(stderr) << "err: failed to create 'logs' in the buffer directory - " << share->tmpDir << "/logs" << " check for write permissions." << Qt::endl; + } + else if (!mkPath(share->tmpDir + "/img")) + { + QTextStream(stderr) << "err: failed to create 'img' in the buffer directory - " << share->tmpDir << "/img" << " check for write permissions." << Qt::endl; + } + else + { + share->retCode = 0; + share->servPath = QString("/var/") + APP_BIN + QString("_serv/") + APP_BIN + "." + share->camName + ".service"; + } } return share->retCode == 0; @@ -287,7 +338,8 @@ bool rdConf(const QString &filePath, shared_t *share) void rmServices() { - auto files = lsFilesInDir(QString("/var/opt/") + APP_BIN, ".service"); + auto path = QString("/var/") + APP_BIN + QString("_serv"); + auto files = lsFilesInDir(path, ".service"); for (auto &&serv : files) { @@ -295,7 +347,7 @@ void rmServices() QProcess::execute("systemctl", {"disable", serv}); QFile::remove(QString("/lib/systemd/system/") + serv); - QFile::remove(QString("/var/opt/") + APP_BIN + "/" + serv); + QFile::remove(path + "/" + serv); } QProcess::execute("systemctl", {"daemon-reload"}); @@ -303,7 +355,8 @@ void rmServices() void listServices() { - auto files = lsFilesInDir(QString("/var/opt/") + APP_BIN, ".service"); + auto path = QString("/var/") + APP_BIN + QString("_serv"); + auto files = lsFilesInDir(path, ".service"); for (auto &&serv : files) { @@ -398,6 +451,18 @@ int loadServices(const QStringList &args) return ret; } +QString buildThreadCount(int count) +{ + QString ret = "0"; + + for (auto i = 1; i < count; ++i) + { + ret.append(","); ret.append(QString::number(i)); + } + + return ret; +} + QStringList parseArgs(const QByteArray &data, int maxArgs, int *pos) { QStringList ret; diff --git a/src/common.h b/src/common.h index 59b2c80..259dd0b 100644 --- a/src/common.h +++ b/src/common.h @@ -29,7 +29,7 @@ using namespace std; -#define APP_VER "3.3.t1" +#define APP_VER "3.3.t2" #define APP_NAME "Motion Watch" #define APP_BIN "mow" #define REC_LOG_NAME "rec.log" @@ -64,7 +64,7 @@ struct shared_t QString buffPath; QString postCmd; QString camName; - QString recRoot; + QString recPath; QString servPath; QString outputType; QString compCmd; @@ -72,8 +72,13 @@ struct shared_t QString streamExt; QString recExt; QString thumbExt; + QString recScale; + QString imgScale; bool singleTenant; bool skipCmd; + int recFps; + int imgThreads; + int recThreads; int evMaxSecs; int postSecs; int imgThresh; @@ -83,6 +88,7 @@ struct shared_t }; QString getParam(const QString &key, const QStringList &args); +QString buildThreadCount(int count); QStringList lsFilesInDir(const QString &path, const QString &ext = QString()); QStringList lsDirsInDir(const QString &path); QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir); @@ -90,15 +96,15 @@ QStringList backwardFacingFiles(const QString &path, const QString &ext, const Q QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs); QStringList parseArgs(const QByteArray &data, int maxArgs, int *pos = nullptr); bool rdConf(const QString &filePath, shared_t *share); +bool mkPath(const QString &path); int loadServices(const QStringList &args); void listServices(); void rmServices(); -void touch(const QString &path); void rdLine(const QString ¶m, const QString &line, QString *value); void rdLine(const QString ¶m, const QString &line, int *value); void rdLine(const QString ¶m, const QString &line, bool *value); void enforceMaxEvents(shared_t *share); -void enforceMaxImages(); +void enforceMaxImages(shared_t *share); void enforceMaxVids(shared_t *share); void extCorrection(QString &ext);