diff --git a/README.md b/README.md index f8bfb56..f0a9af1 100644 --- a/README.md +++ b/README.md @@ -34,16 +34,17 @@ recording_stream = rtsp://1.2.3.4:554/h264 # this is the url to the main stream of the IP camera that will be used # to record footage. # -web_root = /var/www/html +web_root = /var/opt/mow/web # this is the output directory that will be used to store recorded footage # from the cameras as well as the web interface for the application. # warning: this will overwrite any existing index.html files so be sure # to choose a directory that doesn't have an existing website. # -buffer_path = /tmp +buffer_path = /var/opt/mow/buf # this is the work directory the app will use to store live footage and # image frames. it's recommended to use a ram disk for this since there -# will be large amounts of io occuring here. +# will be large amounts of io occuring here. 1g of space per camera is +# a good rule of thumb. # cam_name = cam-1 # this is the optional camera name parameter to identify the camera. this @@ -54,11 +55,29 @@ max_event_secs = 30 # this is the maximum amount of secs of video footage that can be # recorded in a motion event. # +img_comp_cmd = magick compare -metric FUZZ &prev& &next& /dev/null +# this is the command line template this application will use when calling +# the external image comparison application. the external application is +# expected to compare snapshots from the video stream to determine how +# different the images are from each other. it needs to output a numeric +# score with the higher values meaning very different while low values +# mean similar images. the snapshots pulled from the stream will be bitmap +# formatted so the app will be required to support this format. also avoid +# outputting any special chars, only numeric chars with a single '.' if +# outputting a decimal value. magick is the default if not defined in the +# config file. the special string &prev& will be substituted with the path +# to the "previous" bitmap image, behind in time stamp to the image path +# subtituted in &next&. +# +img_comp_out = stderr +# this is the standard output stream the app defined in img_comp_cmd will +# use to output the comparison score. this can only be stderr or stdout, +# any other stream name is considered invalid. +# img_thresh = 8000 -# this application uses 'magick compare' to score the differences between -# two, one second gapped snapshots of the camera stream. any image pairs -# that score greater than this value is considered motion and queues up -# max_event_secs worth of hls clips to be written out as a motion event. +# this parameter defines the score threshold from img_comp_cmd that will +# be considered motion. any motion events will queue up max_event_secs +# worth of hls clips to be written out to web_root. # max_events = 100 # this indicates the maximum amount of motion event video clips to keep diff --git a/install.sh b/install.sh index d5eb61f..ad9798a 100644 --- a/install.sh +++ b/install.sh @@ -23,6 +23,18 @@ if [ ! -d "/var/opt/mow/web" ]; then mkdir /var/opt/mow/web fi +if [ -f "/var/opt/mow/web/index.html" ]; then + rm -v /var/opt/mow/web/index.html + touch /var/opt/mow/buf/index.html + ln -sv /var/opt/mow/buf/index.html /var/opt/mow/web/index.html +fi + +if [ -f "/var/opt/mow/web/theme.css" ]; then + rm -v /var/opt/mow/web/theme.css + touch /var/opt/mow/buf/theme.css + ln -sv /var/opt/mow/buf/theme.css /var/opt/mow/web/theme.css +fi + cp -v ./.build-mow/mow /opt/mow/bin echo "writing /opt/mow/run" diff --git a/src/camera.cpp b/src/camera.cpp index 494ff41..96dc71a 100644 --- a/src/camera.cpp +++ b/src/camera.cpp @@ -236,7 +236,7 @@ bool Upkeep::exec() genHTMLul(".", shared->camName, shared); genCSS(shared); - genHTMLul(shared->webRoot, QString(APP_NAME) + " " + QString(APP_VER), shared); + genHTMLul(shared->buffPath, QString(APP_NAME) + " " + QString(APP_VER), shared); return Loop::exec(); } @@ -426,20 +426,63 @@ void DetectLoop::pcBreak() else delayCycles += 5; detLog("no motion detected, running post command: " + shared->postCmd, shared); - system(shared->postCmd.toUtf8().data()); + + auto args = parseArgs(shared->postCmd.toUtf8(), -1); + + if (args.isEmpty()) + { + detLog("err: did not parse an executable from the post command line.", shared); + } + else + { + QProcess::execute(args[0], args.mid(1)); + } } } mod = false; } +float DetectLoop::getFloatFromExe(const QByteArray &line) +{ + QString strLine(line); + QString strNum; + + for (auto chr : strLine) + { + if (chr.isDigit() || (chr == '.')) + { + strNum.append(chr); + } + else + { + break; + } + } + + return strNum.toFloat(); +} + +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; +} + bool DetectLoop::exec() { if (delayCycles > 0) { delayCycles -= 1; - detLog("spec: detection cycle skipped. cycles left to be skipped: " + QString::number(delayCycles), shared); + detLog("delay: detection cycle skipped. cycles left to be skipped: " + QString::number(delayCycles), shared); } else { @@ -453,42 +496,60 @@ bool DetectLoop::exec() } else { - QProcess extComp; - QStringList args; + auto pos = images.size() - 1; + auto args = buildArgs(images[pos - 1], images[pos]); - auto pos = images.size() - 1; - - args << "compare"; - args << "-metric" << "FUZZ"; - args << images[pos - 1]; - args << images[pos]; - args << "/dev/null"; - - extComp.start("magick", args); - extComp.waitForFinished(); - - QString output = extComp.readAllStandardError(); - - output = output.left(output.indexOf(' ')); - - detLog(extComp.program() + " " + args.join(" ") + " --result: " + output, shared); - - auto score = output.toFloat(); - - if (score >= shared->imgThresh) + if (args.isEmpty()) { - detLog("--threshold_breached: " + QString::number(shared->imgThresh), shared); + detLog("err: could not parse a executable name from img_comp_cmd: " + shared->compCmd, shared); + } + else + { + QProcess extComp; - evt_t event; + extComp.start(args[0], args.mid(1)); + extComp.waitForFinished(); - event.timeStamp = curDT; - event.score = score; - event.imgPath = images[pos]; - event.queAge = 0; + float score = 0; + auto ok = true; - shared->recMutex.lock(); - shared->recList.append(event); mod = true; - shared->recMutex.unlock(); + if (shared->outputType == "stdout") + { + score = getFloatFromExe(extComp.readAllStandardOutput()); + } + else if (shared->outputType == "stderr") + { + score = getFloatFromExe(extComp.readAllStandardError()); + } + else + { + ok = false; + } + + if (!ok) + { + detLog("err: img_comp_out: " + shared->outputType + " is not valid. it must be 'stdout' or 'stderr'" , shared); + } + else + { + detLog(extComp.program() + " " + args.join(" ") + " --result: " + QString::number(score), shared); + + if (score >= shared->imgThresh) + { + detLog("--threshold_breached: " + QString::number(shared->imgThresh), shared); + + evt_t event; + + event.timeStamp = curDT; + event.score = score; + event.imgPath = images[pos]; + event.queAge = 0; + + shared->recMutex.lock(); + shared->recList.append(event); mod = true; + shared->recMutex.unlock(); + } + } } } } diff --git a/src/camera.h b/src/camera.h index 18f66bf..e5d9d79 100644 --- a/src/camera.h +++ b/src/camera.h @@ -125,7 +125,9 @@ private: uint delayCycles; bool mod; - void resetTimers(); + void resetTimers(); + float getFloatFromExe(const QByteArray &line); + QStringList buildArgs(const QString &prev, const QString &next); private slots: diff --git a/src/common.cpp b/src/common.cpp index 1afb09e..c5d4e72 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -211,6 +211,8 @@ bool rdConf(const QString &filePath, shared_t *share) share->webBg = "#485564"; share->webTxt = "#dee5ee"; share->webFont = "courier"; + share->outputType = "stderr"; + share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null"; QString line; @@ -233,6 +235,8 @@ bool rdConf(const QString &filePath, shared_t *share) 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); } } while(!line.isEmpty()); @@ -243,7 +247,7 @@ bool rdConf(const QString &filePath, shared_t *share) } share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName; - share->tmpDir = share->buffPath + "/" + APP_BIN + "/" + share->camName; + share->tmpDir = share->buffPath + "/" + share->camName; share->servPath = QString("/var/opt/") + APP_BIN + "/" + APP_BIN + "." + share->camName + ".service"; } @@ -360,3 +364,79 @@ int loadServices(const QStringList &args) return ret; } + +QStringList parseArgs(const QByteArray &data, int maxArgs, int *pos) +{ + QStringList ret; + QString arg; + + auto line = QString::fromUtf8(data); + auto inDQuotes = false; + auto inSQuotes = false; + auto escaped = false; + + if (pos != nullptr) *pos = 0; + + for (int i = 0; i < line.size(); ++i) + { + if (pos != nullptr) *pos += 1; + + if ((line[i] == '\'') && !inDQuotes && !escaped) + { + // single quote ' + + inSQuotes = !inSQuotes; + } + else if ((line[i] == '\"') && !inSQuotes && !escaped) + { + // double quote " + + inDQuotes = !inDQuotes; + } + else + { + escaped = false; + + if (line[i].isSpace() && !inDQuotes && !inSQuotes) + { + // space + + if (!arg.isEmpty()) + { + ret.append(arg); + arg.clear(); + } + } + else + { + if ((line[i] == '\\') && ((i + 1) < line.size())) + { + if ((line[i + 1] == '\'') || (line[i + 1] == '\"')) + { + escaped = true; + } + else + { + arg.append(line[i]); + } + } + else + { + arg.append(line[i]); + } + } + } + + if ((ret.size() >= maxArgs) && (maxArgs != -1)) + { + break; + } + } + + if (!arg.isEmpty() && !inDQuotes && !inSQuotes) + { + ret.append(arg); + } + + return ret; +} diff --git a/src/common.h b/src/common.h index 400e067..b009bbd 100644 --- a/src/common.h +++ b/src/common.h @@ -30,7 +30,7 @@ using namespace std; -#define APP_VER "3.2.t1" +#define APP_VER "3.2.t2" #define APP_NAME "Motion Watch" #define APP_BIN "mow" #define REC_LOG_NAME "rec_log_lines.html" @@ -40,6 +40,8 @@ using namespace std; #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&" struct evt_t { @@ -68,6 +70,8 @@ struct shared_t QString webFont; QString webRoot; QString servPath; + QString outputType; + QString compCmd; bool skipCmd; int evMaxSecs; int postSecs; @@ -83,6 +87,7 @@ QStringList lsDirsInDir(const QString &path); QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir); QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs); 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); int loadServices(const QStringList &args); void listServices(); diff --git a/src/main.cpp b/src/main.cpp index 56ff239..4fe5461 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -64,8 +64,7 @@ int main(int argc, char** argv) { args.clear(); args.append("-d"); - args.append("/etc/"); - args.append(APP_BIN); + args.append("/etc/" + QString(APP_BIN)); rmServices(); ret = loadServices(args); } @@ -84,7 +83,7 @@ int main(int argc, char** argv) { if (args.contains("-f")) { - rmServices(); QProcess::execute("/opt/mow/uninst"); + rmServices(); QProcess::execute("/opt/mow/uninst", QStringList()); } else { @@ -95,7 +94,7 @@ int main(int argc, char** argv) if (ans == 'y' || ans == 'Y') { - rmServices(); QProcess::execute("/opt/mow/uninst"); + rmServices(); QProcess::execute("/opt/mow/uninst", QStringList()); } } } diff --git a/src/web.cpp b/src/web.cpp index 2344e44..6c8ac71 100644 --- a/src/web.cpp +++ b/src/web.cpp @@ -175,7 +175,7 @@ void genCSS(shared_t *share) cssText += " color: " + share->webTxt + ";\n"; cssText += "}\n"; - QFile outFile(QDir().cleanPath(share->webRoot) + "/theme.css"); + QFile outFile(QDir().cleanPath(share->buffPath) + "/theme.css"); outFile.open(QFile::WriteOnly); outFile.write(cssText.toUtf8());