23e0ae935e
Added string trimming to the vid_container parameter to filter out bad user input. Added detection_stream url to the config file and made it so the application can now use a smaller/lower bit rate stream for motion detection separate from the recording stream. This can significantly lower CPU usage. Moved away from using system() and the explicit timeout command. Instead opted to using popen() and cancelable pthreads. Doing this pulls back more control over ffmpeg than before and the app will now properly respond term signals and even the CTRL-C keyboard interrupt.
393 lines
9.3 KiB
C++
393 lines
9.3 KiB
C++
// This file is part of Motion Watch.
|
|
|
|
// Motion Watch is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
|
|
// Motion Watch is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
|
|
#include "common.h"
|
|
|
|
string leftTrim(const string &str, const string &toRemove)
|
|
{
|
|
auto start = str.find_first_not_of(toRemove);
|
|
|
|
if (start != string::npos)
|
|
{
|
|
return str.substr(start);
|
|
}
|
|
else
|
|
{
|
|
return str;
|
|
}
|
|
}
|
|
|
|
string rightTrim(const string &str, const string &toRemove)
|
|
{
|
|
auto end = str.find_last_not_of(toRemove);
|
|
|
|
if (end != string::npos)
|
|
{
|
|
return str.substr(0, end + 1);
|
|
}
|
|
else
|
|
{
|
|
return str;
|
|
}
|
|
}
|
|
|
|
string trim(const string &str, const string &toRemove)
|
|
{
|
|
return rightTrim(leftTrim(str, toRemove), toRemove);
|
|
}
|
|
|
|
string trim(const string &str)
|
|
{
|
|
return trim(str, TRIM_REMOVE);
|
|
}
|
|
|
|
string cleanDir(const string &path)
|
|
{
|
|
if (path[path.size() - 1] == '/')
|
|
{
|
|
return path.substr(0, path.size() - 1);
|
|
}
|
|
else
|
|
{
|
|
return path;
|
|
}
|
|
}
|
|
|
|
bool createDir(const string &dir)
|
|
{
|
|
auto ret = mkdir(dir.c_str(), 0777);
|
|
|
|
if (ret == -1)
|
|
{
|
|
return errno == EEXIST;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool createDirTree(const string &full_path)
|
|
{
|
|
size_t pos = 0;
|
|
auto ret = true;
|
|
|
|
while (ret == true && pos != string::npos)
|
|
{
|
|
pos = full_path.find('/', pos + 1);
|
|
ret = createDir(full_path.substr(0, pos));
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
vector<string> lsFilesInDir(const string &path, const string &ext)
|
|
{
|
|
vector<string> names;
|
|
|
|
if (exists(path))
|
|
{
|
|
for (auto &entry : directory_iterator(path))
|
|
{
|
|
if (entry.is_regular_file())
|
|
{
|
|
auto name = entry.path().filename().string();
|
|
|
|
if (ext.empty() || name.ends_with(ext))
|
|
{
|
|
names.push_back(name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sort(names.begin(), names.end());
|
|
|
|
return names;
|
|
}
|
|
|
|
vector<string> lsDirsInDir(const string &path)
|
|
{
|
|
vector<string> names;
|
|
|
|
if (exists(path))
|
|
{
|
|
for (auto &entry : directory_iterator(path))
|
|
{
|
|
if (entry.is_directory())
|
|
{
|
|
names.push_back(entry.path().filename().string());
|
|
}
|
|
}
|
|
}
|
|
|
|
sort(names.begin(), names.end());
|
|
|
|
return names;
|
|
}
|
|
|
|
void enforceMaxDays(const string &dirPath, shared_t *share)
|
|
{
|
|
auto names = lsDirsInDir(dirPath);
|
|
|
|
while (names.size() > (share->maxDays - 1))
|
|
{
|
|
remove_all(string(cleanDir(dirPath) + "/" + names[0]).c_str());
|
|
|
|
names.erase(names.begin());
|
|
}
|
|
}
|
|
|
|
void enforceMaxClips(const string &dirPath, shared_t *share)
|
|
{
|
|
auto names = lsFilesInDir(dirPath, "." + share->vidExt);
|
|
|
|
while (names.size() > share->maxClips)
|
|
{
|
|
// removes the video file extension.
|
|
auto nameOnly = names[0].substr(0, names[0].size() - (share->vidExt.size() + 1));
|
|
auto imgFile = cleanDir(dirPath) + "/" + nameOnly + ".jpg";
|
|
auto webFile = cleanDir(dirPath) + "/" + nameOnly + ".html";
|
|
|
|
remove(cleanDir(dirPath) + "/" + names[0]);
|
|
|
|
if (exists(imgFile)) remove(imgFile);
|
|
if (exists(webFile)) remove(webFile);
|
|
|
|
names.erase(names.begin());
|
|
}
|
|
}
|
|
|
|
string genTimeStr(const char *fmt)
|
|
{
|
|
time_t rawtime;
|
|
|
|
time(&rawtime);
|
|
|
|
auto timeinfo = localtime(&rawtime);
|
|
|
|
char ret[50];
|
|
|
|
strftime(ret, 50, fmt, timeinfo);
|
|
|
|
return string(ret);
|
|
}
|
|
|
|
string genDstFile(const string &dirOut, const char *fmt, const string &ext)
|
|
{
|
|
createDirTree(cleanDir(dirOut));
|
|
|
|
return cleanDir(dirOut) + string("/") + genTimeStr(fmt) + ext;
|
|
}
|
|
|
|
void rdLine(const string ¶m, const string &line, string *value)
|
|
{
|
|
if (line.rfind(param.c_str(), 0) == 0)
|
|
{
|
|
*value = line.substr(param.size());
|
|
}
|
|
}
|
|
|
|
void rdLine(const string ¶m, const string &line, int *value)
|
|
{
|
|
if (line.rfind(param.c_str(), 0) == 0)
|
|
{
|
|
*value = strtol(line.substr(param.size()).c_str(), NULL, 10);
|
|
}
|
|
}
|
|
|
|
bool rdConf(const string &filePath, shared_t *share)
|
|
{
|
|
ifstream varFile(filePath.c_str());
|
|
|
|
if (!varFile.is_open())
|
|
{
|
|
share->retCode = ENOENT;
|
|
|
|
cout << "wrn: config file: " << filePath << " does not exists or lack read permissions." << endl;
|
|
}
|
|
else
|
|
{
|
|
string line;
|
|
|
|
do
|
|
{
|
|
getline(varFile, line);
|
|
|
|
if (line.rfind("#", 0) != 0)
|
|
{
|
|
rdLine("cam_name = ", line, &share->camName);
|
|
rdLine("recording_stream = ", line, &share->recordUrl);
|
|
rdLine("detect_stream = ", line, &share->detectUrl);
|
|
rdLine("web_root = ", line, &share->webRoot);
|
|
rdLine("web_text = ", line, &share->webTxt);
|
|
rdLine("web_bg = ", line, &share->webBg);
|
|
rdLine("web_font = ", line, &share->webFont);
|
|
rdLine("post_cmd = ", line, &share->postCmd);
|
|
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);
|
|
rdLine("img_thresh = ", line, &share->imgThresh);
|
|
rdLine("max_days = ", line, &share->maxDays);
|
|
rdLine("max_clips = ", line, &share->maxClips);
|
|
rdLine("max_log_size = ", line, &share->maxLogSize);
|
|
rdLine("vid_container = ", line, &share->vidExt);
|
|
rdLine("vid_codec = ", line, &share->vidCodec);
|
|
}
|
|
|
|
} while(!line.empty());
|
|
}
|
|
|
|
return share->retCode == 0;
|
|
}
|
|
|
|
bool rdConf(shared_t *share)
|
|
{
|
|
share->recordUrl.clear();
|
|
share->detectUrl.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";
|
|
share->detSuffix = ".det.";
|
|
share->recSuffix = ".rec.";
|
|
|
|
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();
|
|
}
|
|
|
|
if (share->detectUrl.empty())
|
|
{
|
|
share->detectUrl = share->recordUrl;
|
|
}
|
|
|
|
share->outDir = cleanDir(share->webRoot) + "/" + share->camName;
|
|
share->buffDir = cleanDir(share->buffDir) + "/" + share->camName;
|
|
share->recLogPath = share->outDir + "/rec_log_lines.html";
|
|
share->detLogPath = share->outDir + "/det_log_lines.html";
|
|
share->vidExt = trim(share->vidExt);
|
|
share->detSuffix += share->vidExt;
|
|
share->recSuffix += share->vidExt;
|
|
|
|
if (share->init)
|
|
{
|
|
if (exists(share->buffDir))
|
|
{
|
|
remove_all(share->buffDir);
|
|
}
|
|
|
|
share->init = false;
|
|
}
|
|
|
|
createDirTree(cleanDir(share->buffDir));
|
|
createDirTree(share->outDir);
|
|
}
|
|
else
|
|
{
|
|
cerr << "err: none of the expected config files could be read." << endl;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
vector<string> parseForList(const string &arg, int argc, char** argv, bool argOnly, int count)
|
|
{
|
|
auto argPresent = false;
|
|
auto argCount = 0;
|
|
auto ret = vector<string>();
|
|
|
|
for (auto i = 1; i < argc; ++i)
|
|
{
|
|
auto argInParams = string(argv[i]);
|
|
|
|
if (argPresent)
|
|
{
|
|
ret.push_back(argInParams);
|
|
|
|
argPresent = false;
|
|
}
|
|
else if (arg.compare(argInParams) == 0)
|
|
{
|
|
argPresent = true; argCount++;
|
|
}
|
|
|
|
if (argPresent && argOnly)
|
|
{
|
|
ret.push_back(string("true"));
|
|
}
|
|
|
|
if (count != 0)
|
|
{
|
|
if (argCount >= count) break;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
|
|
{
|
|
auto params = parseForList(arg, argc, argv, argOnly, 1);
|
|
|
|
if (params.empty())
|
|
{
|
|
return string();
|
|
}
|
|
else
|
|
{
|
|
return params[0];
|
|
}
|
|
}
|
|
|
|
void waitForDetThreads(shared_t *share)
|
|
{
|
|
for (auto &&thr : share->detThreads)
|
|
{
|
|
thr.join();
|
|
}
|
|
|
|
share->detThreads.clear();
|
|
}
|