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>
-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.
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 ###
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
parameters supported and descriptions of each parameter.
parameters supported and descriptions of each.
```
# Motion Watch config file
#
@ -35,9 +40,9 @@ recording_stream = rtsp://1.2.3.4:554/h264
#
web_root = /var/www/html
# 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
# recommended to leave it on the default value if using apache2 as the
# http(s) server.
# 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.
#
buff_dir = /tmp
# 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
# 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
# check the pixel diffs for motion. the lower the value, the more frames
# 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
# before it is considered motion. any video clips found with frames
# exceeding this value will be moved from buff_dir to web_root.
#
post_cmd = move_the_ptz_camera.py
# this an optional command to run after the internal timer duration has
# elapsed. 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.
clip_len = 20
# this parameter indicate the amount of seconds to record in each video
# clip from the camera that will be stored and then processed in buff_dir.
#
duration = 60
# this sets the internal timer used to reset to the detection loop and
# then call post_cmd if it is defined. this will also reload the config
# file so changes to any settings will be applied without restarting the
# application.
num_of_clips = 3
# this will tell the application how many video clips should be recorded
# to buff_dir from the camera before the recording loop pauses to do some
# house keeping. by house keeping, it will wait until all motion detection
# 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
# 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())
{
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
{
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
{
getline(varFile, line);
@ -220,7 +194,8 @@ bool rdConf(shared_t *share)
rdLine("web_bg = ", line, &share->webBg);
rdLine("web_font = ", line, &share->webFont);
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("frame_gap = ", line, &share->frameGap);
rdLine("pix_thresh = ", line, &share->pixThresh);
@ -233,6 +208,53 @@ bool rdConf(shared_t *share)
}
} 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->buffDir = cleanDir(share->buffDir) + "/" + share->camName;
@ -252,36 +274,68 @@ bool rdConf(shared_t *share)
createDirTree(cleanDir(share->buffDir));
createDirTree(share->outDir);
}
else
{
cerr << "err: none of the expected config files could be read." << endl;
}
varFile.close();
return share->retCode == 0;
return ret;
}
string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
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 (!argOnly)
{
// check ahead, make sure i + 1 won't cause out-of-range exception
if ((i + 1) <= (argc - 1))
offs++;
// 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
{
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)

View File

@ -35,12 +35,13 @@ using namespace cv;
using namespace std;
using namespace std::filesystem;
#define APP_VER "1.5.t18"
#define APP_VER "1.5.t19"
#define APP_NAME "Motion Watch"
struct shared_t
{
vector<thread> detThreads;
vector<string> conf;
ofstream recLogFile;
ofstream detLogFile;
string recLogPath;
@ -48,7 +49,6 @@ struct shared_t
string recordUrl;
string outDir;
string postCmd;
string conf;
string buffDir;
string vidExt;
string vidCodec;
@ -59,10 +59,11 @@ struct shared_t
string webRoot;
bool init;
bool skipCmd;
int clipLen;
int frameGap;
int pixThresh;
int imgThresh;
int secs;
int numOfClips;
int maxDays;
int maxClips;
int maxLogSize;
@ -72,6 +73,7 @@ struct shared_t
string genDstFile(const string &dirOut, const char *fmt, const string &ext);
string genTimeStr(const char *fmt);
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);
bool createDir(const string &dir);
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 waitForDetThreads(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> 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);
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 limSecs = to_string(share->secs + 2);
auto cmd = "timeout -k 1 " + limSecs + " ";
auto cmd = "timeout -k 1 " + to_string(share->clipLen + 2) + " ";
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);
@ -95,7 +94,7 @@ void recLoop(shared_t *share)
remove(bufPath);
}
sleep(10);
sleep(share->clipLen);
}
}
@ -133,15 +132,19 @@ int main(int argc, char** argv)
{
struct shared_t sharedRes;
sharedRes.conf = parseForParam("-c", argc, argv, false);
sharedRes.conf = parseForList("-c", argc, argv);
if (parseForParam("-h", argc, argv, true) == "true")
{
cout << "Motion Watch " << APP_VER << endl << endl;
cout << "Usage: mow <argument>" << endl << endl;
cout << "-h : display usage information about this application." << endl;
cout << "-c : path to the config file." << endl;
cout << "-v : display the current version." << endl;
cout << "-c : path to a config file." << 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")
{
@ -149,7 +152,7 @@ int main(int argc, char** argv)
}
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
{

View File

@ -18,7 +18,7 @@ bool imgDiff(const Mat &prev, const Mat &next, shared_t *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 (!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=\"Pragma\" content=\"no-cache\" />\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 += "</head>\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
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=\"Pragma\" content=\"no-cache\" />\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 += "</head>\n";
htmlText += "<body>\n";