v3.2.t2
-fixed the -i option that was using /etc instaead of /etc/mow -the externalized image comparison executable is now configurable in the config file. -moved the main index.html and theme.css files to the buffer dir, moving even more io to the buffer dir.
This commit is contained in:
parent
a8bd0ab7bf
commit
3791b29cf7
33
README.md
33
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
|
# this is the url to the main stream of the IP camera that will be used
|
||||||
# to record footage.
|
# 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
|
# 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.
|
# from the cameras as well as the web interface for the application.
|
||||||
# warning: this will overwrite any existing index.html files so be sure
|
# warning: this will overwrite any existing index.html files so be sure
|
||||||
# to choose a directory that doesn't have an existing website.
|
# 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
|
# 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
|
# 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
|
cam_name = cam-1
|
||||||
# this is the optional camera name parameter to identify the camera. this
|
# 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
|
# this is the maximum amount of secs of video footage that can be
|
||||||
# recorded in a motion event.
|
# 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
|
img_thresh = 8000
|
||||||
# this application uses 'magick compare' to score the differences between
|
# this parameter defines the score threshold from img_comp_cmd that will
|
||||||
# two, one second gapped snapshots of the camera stream. any image pairs
|
# be considered motion. any motion events will queue up max_event_secs
|
||||||
# that score greater than this value is considered motion and queues up
|
# worth of hls clips to be written out to web_root.
|
||||||
# max_event_secs worth of hls clips to be written out as a motion event.
|
|
||||||
#
|
#
|
||||||
max_events = 100
|
max_events = 100
|
||||||
# this indicates the maximum amount of motion event video clips to keep
|
# this indicates the maximum amount of motion event video clips to keep
|
||||||
|
|
12
install.sh
12
install.sh
|
@ -23,6 +23,18 @@ if [ ! -d "/var/opt/mow/web" ]; then
|
||||||
mkdir /var/opt/mow/web
|
mkdir /var/opt/mow/web
|
||||||
fi
|
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
|
cp -v ./.build-mow/mow /opt/mow/bin
|
||||||
|
|
||||||
echo "writing /opt/mow/run"
|
echo "writing /opt/mow/run"
|
||||||
|
|
129
src/camera.cpp
129
src/camera.cpp
|
@ -236,7 +236,7 @@ bool Upkeep::exec()
|
||||||
genHTMLul(".", shared->camName, shared);
|
genHTMLul(".", shared->camName, shared);
|
||||||
|
|
||||||
genCSS(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();
|
return Loop::exec();
|
||||||
}
|
}
|
||||||
|
@ -426,20 +426,63 @@ void DetectLoop::pcBreak()
|
||||||
else delayCycles += 5;
|
else delayCycles += 5;
|
||||||
|
|
||||||
detLog("no motion detected, running post command: " + shared->postCmd, shared);
|
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;
|
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()
|
bool DetectLoop::exec()
|
||||||
{
|
{
|
||||||
if (delayCycles > 0)
|
if (delayCycles > 0)
|
||||||
{
|
{
|
||||||
delayCycles -= 1;
|
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
|
else
|
||||||
{
|
{
|
||||||
|
@ -453,42 +496,60 @@ bool DetectLoop::exec()
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
QProcess extComp;
|
auto pos = images.size() - 1;
|
||||||
QStringList args;
|
auto args = buildArgs(images[pos - 1], images[pos]);
|
||||||
|
|
||||||
auto pos = images.size() - 1;
|
if (args.isEmpty())
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
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;
|
float score = 0;
|
||||||
event.score = score;
|
auto ok = true;
|
||||||
event.imgPath = images[pos];
|
|
||||||
event.queAge = 0;
|
|
||||||
|
|
||||||
shared->recMutex.lock();
|
if (shared->outputType == "stdout")
|
||||||
shared->recList.append(event); mod = true;
|
{
|
||||||
shared->recMutex.unlock();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,7 +125,9 @@ private:
|
||||||
uint delayCycles;
|
uint delayCycles;
|
||||||
bool mod;
|
bool mod;
|
||||||
|
|
||||||
void resetTimers();
|
void resetTimers();
|
||||||
|
float getFloatFromExe(const QByteArray &line);
|
||||||
|
QStringList buildArgs(const QString &prev, const QString &next);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
||||||
|
|
|
@ -211,6 +211,8 @@ bool rdConf(const QString &filePath, shared_t *share)
|
||||||
share->webBg = "#485564";
|
share->webBg = "#485564";
|
||||||
share->webTxt = "#dee5ee";
|
share->webTxt = "#dee5ee";
|
||||||
share->webFont = "courier";
|
share->webFont = "courier";
|
||||||
|
share->outputType = "stderr";
|
||||||
|
share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null";
|
||||||
|
|
||||||
QString line;
|
QString line;
|
||||||
|
|
||||||
|
@ -233,6 +235,8 @@ bool rdConf(const QString &filePath, shared_t *share)
|
||||||
rdLine("img_thresh = ", line, &share->imgThresh);
|
rdLine("img_thresh = ", line, &share->imgThresh);
|
||||||
rdLine("max_events = ", line, &share->maxEvents);
|
rdLine("max_events = ", line, &share->maxEvents);
|
||||||
rdLine("max_log_size = ", line, &share->maxLogSize);
|
rdLine("max_log_size = ", line, &share->maxLogSize);
|
||||||
|
rdLine("img_comp_out = ", line, &share->outputType);
|
||||||
|
rdLine("img_comp_cmd = ", line, &share->compCmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
} while(!line.isEmpty());
|
} while(!line.isEmpty());
|
||||||
|
@ -243,7 +247,7 @@ bool rdConf(const QString &filePath, shared_t *share)
|
||||||
}
|
}
|
||||||
|
|
||||||
share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName;
|
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";
|
share->servPath = QString("/var/opt/") + APP_BIN + "/" + APP_BIN + "." + share->camName + ".service";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,3 +364,79 @@ int loadServices(const QStringList &args)
|
||||||
|
|
||||||
return ret;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
#define APP_VER "3.2.t1"
|
#define APP_VER "3.2.t2"
|
||||||
#define APP_NAME "Motion Watch"
|
#define APP_NAME "Motion Watch"
|
||||||
#define APP_BIN "mow"
|
#define APP_BIN "mow"
|
||||||
#define REC_LOG_NAME "rec_log_lines.html"
|
#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 STRFTIME_FMT "%Y%m%d%H%M%S"
|
||||||
#define MAX_IMAGES 1000
|
#define MAX_IMAGES 1000
|
||||||
#define MAX_VIDEOS 1000
|
#define MAX_VIDEOS 1000
|
||||||
|
#define PREV_IMG "&prev&"
|
||||||
|
#define NEXT_IMG "&next&"
|
||||||
|
|
||||||
struct evt_t
|
struct evt_t
|
||||||
{
|
{
|
||||||
|
@ -68,6 +70,8 @@ struct shared_t
|
||||||
QString webFont;
|
QString webFont;
|
||||||
QString webRoot;
|
QString webRoot;
|
||||||
QString servPath;
|
QString servPath;
|
||||||
|
QString outputType;
|
||||||
|
QString compCmd;
|
||||||
bool skipCmd;
|
bool skipCmd;
|
||||||
int evMaxSecs;
|
int evMaxSecs;
|
||||||
int postSecs;
|
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 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 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 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 rdConf(const QString &filePath, shared_t *share);
|
||||||
int loadServices(const QStringList &args);
|
int loadServices(const QStringList &args);
|
||||||
void listServices();
|
void listServices();
|
||||||
|
|
|
@ -64,8 +64,7 @@ int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
args.clear();
|
args.clear();
|
||||||
args.append("-d");
|
args.append("-d");
|
||||||
args.append("/etc/");
|
args.append("/etc/" + QString(APP_BIN));
|
||||||
args.append(APP_BIN);
|
|
||||||
|
|
||||||
rmServices(); ret = loadServices(args);
|
rmServices(); ret = loadServices(args);
|
||||||
}
|
}
|
||||||
|
@ -84,7 +83,7 @@ int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
if (args.contains("-f"))
|
if (args.contains("-f"))
|
||||||
{
|
{
|
||||||
rmServices(); QProcess::execute("/opt/mow/uninst");
|
rmServices(); QProcess::execute("/opt/mow/uninst", QStringList());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -95,7 +94,7 @@ int main(int argc, char** argv)
|
||||||
|
|
||||||
if (ans == 'y' || ans == 'Y')
|
if (ans == 'y' || ans == 'Y')
|
||||||
{
|
{
|
||||||
rmServices(); QProcess::execute("/opt/mow/uninst");
|
rmServices(); QProcess::execute("/opt/mow/uninst", QStringList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,7 +175,7 @@ void genCSS(shared_t *share)
|
||||||
cssText += " color: " + share->webTxt + ";\n";
|
cssText += " color: " + share->webTxt + ";\n";
|
||||||
cssText += "}\n";
|
cssText += "}\n";
|
||||||
|
|
||||||
QFile outFile(QDir().cleanPath(share->webRoot) + "/theme.css");
|
QFile outFile(QDir().cleanPath(share->buffPath) + "/theme.css");
|
||||||
|
|
||||||
outFile.open(QFile::WriteOnly);
|
outFile.open(QFile::WriteOnly);
|
||||||
outFile.write(cssText.toUtf8());
|
outFile.write(cssText.toUtf8());
|
||||||
|
|
Loading…
Reference in New Issue
Block a user