Added the ability to read multiple config files so it's now possible to
load a singular global config and then load a camera specific config in
another.

Many elements in the web interface are coming out too small. Added meta
viewport device width in hopes that the web interface will self adjust
to device it is being displayed on.

Changed duration to num_of_clips and added clip_len so the amount of
seconds in each clip and the amount of clips to be processed for motion
are now adjustable.

Adjusted a several default values.
This commit is contained in:
Maurice ONeal 2022-12-24 13:48:51 -05:00
parent 4e44111ea8
commit 62b2bfd76b
6 changed files with 144 additions and 72 deletions

View File

@ -13,15 +13,20 @@ of this app can be used to operate multiple cameras.
Usage: mow <argument> Usage: mow <argument>
-h : display usage information about this application. -h : display usage information about this application.
-c : path to the config file. -c : path to the config file(s).
-v : display the current version. -v : display the current version.
note: multiple -c config files can be passed, reading from left
to right. any conflicting values between the files will
have the latest value from the latest file overwrite the
the earliest.
``` ```
### Config File ### ### Config File ###
The config file is a simple text file that contain parameters that dictate the The config file is a simple text file that contain parameters that dictate the
behavior of the application. Below is an example of a config file with all behavior of the application. Below is an example of a config file with all
parameters supported and descriptions of each parameter. parameters supported and descriptions of each.
``` ```
# Motion Watch config file # Motion Watch config file
# #
@ -35,9 +40,9 @@ recording_stream = rtsp://1.2.3.4:554/h264
# #
web_root = /var/www/html web_root = /var/www/html
# 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. it is # from the cameras as well as the web interface for the application.
# recommended to leave it on the default value if using apache2 as the # warning: this will overwrite any existing index.html files so be sure
# http(s) server. # to choose a directory that doesn't have an existing website.
# #
buff_dir = /tmp buff_dir = /tmp
# this application records small clips of the footage from the camera and # this application records small clips of the footage from the camera and
@ -57,27 +62,32 @@ pix_thresh = 150
# before the pixels are actually considered different. think of this as # before the pixels are actually considered different. think of this as
# pixel diff sensitivity, the higher the value the lesser the sensitivity. # pixel diff sensitivity, the higher the value the lesser the sensitivity.
# #
frame_gap = 10 frame_gap = 20
# this value is used to tell the application how far in between frames to # this value is used to tell the application how far in between frames to
# check the pixel diffs for motion. the lower the value, the more frames # check the pixel diffs for motion. the lower the value, the more frames
# will be checked, however with that comes higher cpu usage. # will be checked, however with that comes higher cpu usage.
# #
img_thresh = 10000 img_thresh = 80000
# this indicates how many pixels need to be different in between frame_gap # this indicates how many pixels need to be different in between frame_gap
# before it is considered motion. any video clips found with frames # before it is considered motion. any video clips found with frames
# exceeding this value will be moved from buff_dir to web_root. # exceeding this value will be moved from buff_dir to web_root.
# #
post_cmd = move_the_ptz_camera.py clip_len = 20
# this an optional command to run after the internal timer duration has # this parameter indicate the amount of seconds to record in each video
# elapsed. one great use for this is to move a ptz camera to the next # clip from the camera that will be stored and then processed in buff_dir.
# position of it's patrol pattern. note: the call to this command will be
# delayed if motion was detected.
# #
duration = 60 num_of_clips = 3
# this sets the internal timer used to reset to the detection loop and # this will tell the application how many video clips should be recorded
# then call post_cmd if it is defined. this will also reload the config # to buff_dir from the camera before the recording loop pauses to do some
# file so changes to any settings will be applied without restarting the # house keeping. by house keeping, it will wait until all motion detection
# application. # threads are finished, reload the config file and then call the post_cmd
# if no motion was detected in any of the video clips.
#
post_cmd = move_the_ptz_camera.py
# this an optional command to run after num_of_clips is met. one great use
# for this is to move a ptz camera to the next position of it's patrol
# pattern. note: the call to this command will be delayed if motion was
# detected.
# #
max_days = 15 max_days = 15
# this defines the maximum amount of days worth of video clips that is # this defines the maximum amount of days worth of video clips that is

View File

@ -167,46 +167,20 @@ void rdLine(const string &param, const string &line, int *value)
} }
} }
bool rdConf(shared_t *share) bool rdConf(const string &filePath, shared_t *share)
{ {
ifstream varFile(share->conf.c_str()); ifstream varFile(filePath.c_str());
if (!varFile.is_open()) if (!varFile.is_open())
{ {
share->retCode = ENOENT; share->retCode = ENOENT;
cerr << "err: Failed to open the config file: " << share->conf << " for reading. please check file permissions or if it exists." << endl; cout << "wrn: config file: " << filePath << " does not exists or lack read permissions." << endl;
} }
else else
{ {
string line; string line;
share->recordUrl.clear();
share->postCmd.clear();
share->buffDir.clear();
share->recLogPath.clear();
share->detLogPath.clear();
share->recLogFile.close();
share->detLogFile.close();
share->retCode = 0;
share->frameGap = 10;
share->pixThresh = 150;
share->imgThresh = 10000;
share->secs = 60;
share->maxDays = 15;
share->maxClips = 90;
share->maxLogSize = 50000;
share->camName = path(share->conf.c_str()).filename();
share->webRoot = "/var/www/html";
share->buffDir = "/tmp";
share->vidExt = "mp4";
share->vidCodec = "copy";
share->skipCmd = false;
share->webBg = "#485564";
share->webTxt = "#dee5ee";
share->webFont = "courier";
do do
{ {
getline(varFile, line); getline(varFile, line);
@ -220,7 +194,8 @@ bool rdConf(shared_t *share)
rdLine("web_bg = ", line, &share->webBg); rdLine("web_bg = ", line, &share->webBg);
rdLine("web_font = ", line, &share->webFont); rdLine("web_font = ", line, &share->webFont);
rdLine("post_cmd = ", line, &share->postCmd); rdLine("post_cmd = ", line, &share->postCmd);
rdLine("duration = ", line, &share->secs); rdLine("clip_len = ", line, &share->clipLen);
rdLine("num_of_clips = ", line, &share->numOfClips);
rdLine("buff_dir = ", line, &share->buffDir); rdLine("buff_dir = ", line, &share->buffDir);
rdLine("frame_gap = ", line, &share->frameGap); rdLine("frame_gap = ", line, &share->frameGap);
rdLine("pix_thresh = ", line, &share->pixThresh); rdLine("pix_thresh = ", line, &share->pixThresh);
@ -233,6 +208,53 @@ bool rdConf(shared_t *share)
} }
} while(!line.empty()); } while(!line.empty());
}
return share->retCode == 0;
}
bool rdConf(shared_t *share)
{
share->recordUrl.clear();
share->postCmd.clear();
share->buffDir.clear();
share->camName.clear();
share->recLogPath.clear();
share->detLogPath.clear();
share->recLogFile.close();
share->detLogFile.close();
share->retCode = 0;
share->frameGap = 20;
share->pixThresh = 150;
share->imgThresh = 80000;
share->clipLen = 20;
share->numOfClips = 3;
share->maxDays = 15;
share->maxClips = 90;
share->maxLogSize = 50000;
share->webRoot = "/var/www/html";
share->buffDir = "/tmp";
share->vidExt = "mp4";
share->vidCodec = "copy";
share->skipCmd = false;
share->webBg = "#485564";
share->webTxt = "#dee5ee";
share->webFont = "courier";
auto ret = false;
for (auto &&confPath: share->conf)
{
if (rdConf(confPath, share)) ret = true;
}
if (ret)
{
if (share->camName.empty())
{
share->camName = path(share->conf.back()).filename();
}
share->outDir = cleanDir(share->webRoot) + "/" + share->camName; share->outDir = cleanDir(share->webRoot) + "/" + share->camName;
share->buffDir = cleanDir(share->buffDir) + "/" + share->camName; share->buffDir = cleanDir(share->buffDir) + "/" + share->camName;
@ -252,36 +274,68 @@ bool rdConf(shared_t *share)
createDirTree(cleanDir(share->buffDir)); createDirTree(cleanDir(share->buffDir));
createDirTree(share->outDir); createDirTree(share->outDir);
} }
else
varFile.close(); {
cerr << "err: none of the expected config files could be read." << endl;
return share->retCode == 0;
} }
string parseForParam(const string &arg, int argc, char** argv, bool argOnly) return ret;
}
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs)
{ {
for (int i = 0; i < argc; ++i) auto ret = string();
for (; offs < argc; ++offs)
{ {
auto argInParams = string(argv[i]); auto argInParams = string(argv[offs]);
if (arg.compare(argInParams) == 0) if (arg.compare(argInParams) == 0)
{ {
if (!argOnly) if (!argOnly)
{ {
// check ahead, make sure i + 1 won't cause out-of-range exception offs++;
if ((i + 1) <= (argc - 1)) // check ahead, make sure offs + 1 won't cause out-of-range exception
if (offs <= (argc - 1))
{ {
return string(argv[i + 1]); ret = string(argv[offs]);
} }
} }
else else
{ {
return string("true"); ret = string("true");
} }
} }
} }
return string(); return ret;
}
string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
{
auto notUsed = 0;
return parseForParam(arg, argc, argv, argOnly, notUsed);
}
vector<string> parseForList(const string &arg, int argc, char** argv)
{
auto offs = 0;
auto ret = vector<string>();
string param;
do
{
param = parseForParam(arg, argc, argv, false, offs);
if (!param.empty())
{
ret.push_back(param);
}
}
while (!param.empty());
return ret;
} }
void waitForDetThreads(shared_t *share) void waitForDetThreads(shared_t *share)

View File

@ -35,12 +35,13 @@ using namespace cv;
using namespace std; using namespace std;
using namespace std::filesystem; using namespace std::filesystem;
#define APP_VER "1.5.t18" #define APP_VER "1.5.t19"
#define APP_NAME "Motion Watch" #define APP_NAME "Motion Watch"
struct shared_t struct shared_t
{ {
vector<thread> detThreads; vector<thread> detThreads;
vector<string> conf;
ofstream recLogFile; ofstream recLogFile;
ofstream detLogFile; ofstream detLogFile;
string recLogPath; string recLogPath;
@ -48,7 +49,6 @@ struct shared_t
string recordUrl; string recordUrl;
string outDir; string outDir;
string postCmd; string postCmd;
string conf;
string buffDir; string buffDir;
string vidExt; string vidExt;
string vidCodec; string vidCodec;
@ -59,10 +59,11 @@ struct shared_t
string webRoot; string webRoot;
bool init; bool init;
bool skipCmd; bool skipCmd;
int clipLen;
int frameGap; int frameGap;
int pixThresh; int pixThresh;
int imgThresh; int imgThresh;
int secs; int numOfClips;
int maxDays; int maxDays;
int maxClips; int maxClips;
int maxLogSize; int maxLogSize;
@ -72,6 +73,7 @@ struct shared_t
string genDstFile(const string &dirOut, const char *fmt, const string &ext); string genDstFile(const string &dirOut, const char *fmt, const string &ext);
string genTimeStr(const char *fmt); string genTimeStr(const char *fmt);
string cleanDir(const string &path); string cleanDir(const string &path);
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs);
string parseForParam(const string &arg, int argc, char** argv, bool argOnly); string parseForParam(const string &arg, int argc, char** argv, bool argOnly);
bool createDir(const string &dir); bool createDir(const string &dir);
bool createDirTree(const string &full_path); bool createDirTree(const string &full_path);
@ -82,6 +84,7 @@ void rdLine(const string &param, const string &line, int *value);
void statOut(shared_t *share); void statOut(shared_t *share);
void waitForDetThreads(shared_t *share); void waitForDetThreads(shared_t *share);
bool rdConf(shared_t *share); bool rdConf(shared_t *share);
vector<string> parseForList(const string &arg, int argc, char** argv);
vector<string> lsFilesInDir(const string &path, const string &ext = string()); vector<string> lsFilesInDir(const string &path, const string &ext = string());
vector<string> lsDirsInDir(const string &path); vector<string> lsDirsInDir(const string &path);

View File

@ -66,13 +66,12 @@ void recLoop(shared_t *share)
recLog("camera specific webroot page updated: " + share->outDir + "/index.html", share); recLog("camera specific webroot page updated: " + share->outDir + "/index.html", share);
for (auto i = 0; i < share->secs; i += 10) for (auto i = 0; i < share->numOfClips; ++i)
{ {
auto bufPath = cleanDir(share->buffDir) + "/" + to_string(i) + "." + share->vidExt; auto bufPath = cleanDir(share->buffDir) + "/" + to_string(i) + "." + share->vidExt;
auto limSecs = to_string(share->secs + 2); auto cmd = "timeout -k 1 " + to_string(share->clipLen + 2) + " ";
auto cmd = "timeout -k 1 " + limSecs + " ";
cmd += "ffmpeg -hide_banner -i " + share->recordUrl + " -y -vcodec " + share->vidCodec + " -movflags faststart -t 10 " + bufPath; cmd += "ffmpeg -hide_banner -i " + share->recordUrl + " -y -vcodec " + share->vidCodec + " -movflags faststart -t " + to_string(share->clipLen) + " " + bufPath;
recLog("ffmpeg_run: " + cmd, share); recLog("ffmpeg_run: " + cmd, share);
@ -95,7 +94,7 @@ void recLoop(shared_t *share)
remove(bufPath); remove(bufPath);
} }
sleep(10); sleep(share->clipLen);
} }
} }
@ -133,15 +132,19 @@ int main(int argc, char** argv)
{ {
struct shared_t sharedRes; struct shared_t sharedRes;
sharedRes.conf = parseForParam("-c", argc, argv, false); sharedRes.conf = parseForList("-c", argc, argv);
if (parseForParam("-h", argc, argv, true) == "true") if (parseForParam("-h", argc, argv, true) == "true")
{ {
cout << "Motion Watch " << APP_VER << endl << endl; cout << "Motion Watch " << APP_VER << endl << endl;
cout << "Usage: mow <argument>" << endl << endl; cout << "Usage: mow <argument>" << endl << endl;
cout << "-h : display usage information about this application." << endl; cout << "-h : display usage information about this application." << endl;
cout << "-c : path to the config file." << endl; cout << "-c : path to a config file." << endl;
cout << "-v : display the current version." << endl; cout << "-v : display the current version." << endl << endl;
cout << "note: multiple -c config files can be passed, reading from left" << endl;
cout << " to right. any conflicting values between the files will" << endl;
cout << " have the latest value from the latest file overwrite the" << endl;
cout << " the earliest." << endl;
} }
else if (parseForParam("-v", argc, argv, true) == "true") else if (parseForParam("-v", argc, argv, true) == "true")
{ {
@ -149,7 +152,7 @@ int main(int argc, char** argv)
} }
else if (sharedRes.conf.empty()) else if (sharedRes.conf.empty())
{ {
cerr << "err: A config file was not given in -c" << endl; cerr << "err: no config file(s) were given in -c" << endl;
} }
else else
{ {

View File

@ -18,7 +18,7 @@ bool imgDiff(const Mat &prev, const Mat &next, shared_t *share)
detLog("img_diff() -- start()", share); detLog("img_diff() -- start()", share);
if (prev.empty()) detLog("prev_frame is empty -- this should never happen (opencv to blame).", share); if (prev.empty()) detLog("prev_frame is empty -- Borken frame from the camera assumed.", share);
if (next.empty()) detLog("next_frame is empty -- EOF assumed.", share); if (next.empty()) detLog("next_frame is empty -- EOF assumed.", share);
if (!prev.empty() && !next.empty()) if (!prev.empty() && !next.empty())

View File

@ -25,6 +25,7 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n"; htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n";
htmlText += "<meta http-equiv=\"Pragma\" content=\"no-cache\" />\n"; htmlText += "<meta http-equiv=\"Pragma\" content=\"no-cache\" />\n";
htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n"; htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n";
htmlText += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n";
htmlText += "<link rel='stylesheet' href='/theme.css'>\n"; htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
htmlText += "</head>\n"; htmlText += "</head>\n";
htmlText += "<body>\n"; htmlText += "<body>\n";
@ -56,7 +57,7 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
// regName.substr(0, regName.size() - 5) removes .html // regName.substr(0, regName.size() - 5) removes .html
auto name = regName.substr(0, regName.size() - 5); auto name = regName.substr(0, regName.size() - 5);
htmlText += "<a href='" + regName + "'><img src='" + name + ".jpg" + "' style='width:10%;height:10%;'</a>\n"; htmlText += "<a href='" + regName + "'><img src='" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n";
} }
} }
@ -96,6 +97,7 @@ void genHTMLvid(const string &outputVid, shared_t *share)
htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n"; htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n";
htmlText += "<meta http-equiv=\"Pragma\" content=\"no-cache\" />\n"; htmlText += "<meta http-equiv=\"Pragma\" content=\"no-cache\" />\n";
htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n"; htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n";
htmlText += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n";
htmlText += "<link rel='stylesheet' href='/theme.css'>\n"; htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
htmlText += "</head>\n"; htmlText += "</head>\n";
htmlText += "<body>\n"; htmlText += "<body>\n";