JustMotion/src/main.cpp

514 lines
12 KiB
C++
Raw Normal View History

2022-04-14 09:45:54 -04:00
#include <iostream>
#include <fstream>
2022-04-14 09:45:54 -04:00
#include <string>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
#include <vector>
#include <thread>
#include <dirent.h>
#include <filesystem>
2022-04-14 09:45:54 -04:00
#include <opencv4/opencv2/opencv.hpp>
#include <opencv4/opencv2/videoio.hpp>
using namespace cv;
using namespace std;
using namespace std::filesystem;
2022-04-14 09:45:54 -04:00
#define BUF_SZ 10
2022-04-14 09:45:54 -04:00
struct shared_t
{
string recordUrl;
string outDir;
string postCmd;
string conf;
string buffDir;
string concatTxtTmp;
string concatShTmp;
string createShTmp;
string vidEtx;
bool init;
int tmpId;
int colorThresh;
int secs;
int blockThresh;
int blockX;
int blockY;
int maxDays;
int retCode;
2022-04-14 09:45:54 -04:00
} sharedRes;
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;
}
bool fileExists(const string& name)
{
return access(name.c_str(), F_OK) != -1;
}
void replaceAll(string &str, const string &from, const string &to)
{
if(from.empty())
return;
size_t startPos = 0;
while((startPos = str.find(from, startPos)) != string::npos)
{
str.replace(startPos, from.length(), to);
startPos += to.length();
}
}
vector<string> lsFilesInDir(const string &path, const string &ext)
{
DIR *dir;
struct dirent *ent;
vector<string> names;
if ((dir = opendir(path.c_str())) != NULL)
{
while ((ent = readdir(dir)) != NULL)
{
auto name = string(ent->d_name);
if ((name.size() >= 4) && (ent->d_type & DT_REG))
{
if (name.substr(name.size() - 4) == ext)
{
names.push_back(name);
}
}
}
closedir(dir);
}
sort(names.begin(), names.end());
return names;
}
void enforceMaxDays(shared_t *share)
{
auto names = lsFilesInDir(share->outDir, ".m3u");
while (names.size() > share->maxDays)
{
auto name = names[0];
auto plsFile = cleanDir(share->outDir) + "/" + name;
auto vidFold = cleanDir(share->outDir) + "/." + name.substr(0, name.size() - 4);
remove(plsFile.c_str());
remove_all(vidFold.c_str());
names.erase(names.begin());
}
}
string genTimeStr(const char *fmt)
2022-04-14 09:45:54 -04:00
{
time_t rawtime;
2022-04-14 09:45:54 -04:00
time(&rawtime);
2022-04-14 09:45:54 -04:00
auto timeinfo = localtime(&rawtime);
2022-04-14 09:45:54 -04:00
char ret[50];
strftime(ret, 50, fmt, timeinfo);
2022-04-14 09:45:54 -04:00
return string(ret);
}
2022-04-14 09:45:54 -04:00
string genDstFile(const string &dirOut, const char *fmt, const string &ext)
{
createDirTree(cleanDir(dirOut));
2022-04-14 09:45:54 -04:00
return cleanDir(dirOut) + string("/") + genTimeStr(fmt) + ext;
}
2022-04-14 09:45:54 -04:00
string genTmpFile(const string &dirOut, const string &ext, shared_t *share)
{
createDirTree(cleanDir(dirOut));
if (share->tmpId == 9999999)
{
share->tmpId = 0;
}
return cleanDir(dirOut) + string("/") + to_string(share->tmpId++) + ext;
}
2022-04-14 09:45:54 -04:00
Mat toGray(const Mat &src)
{
Mat ret;
cvtColor(src, ret, COLOR_BGR2GRAY);
return ret;
2022-04-14 09:45:54 -04:00
}
bool pixDiff(const uchar &pixA, const uchar &pixB, shared_t *share)
2022-04-14 09:45:54 -04:00
{
auto diff = 0;
2022-04-14 09:45:54 -04:00
if (pixA > pixB) diff = pixA - pixB;
if (pixB > pixA) diff = pixB - pixA;
if (diff < share->colorThresh)
{
diff = 0;
}
2022-04-14 09:45:54 -04:00
return diff != 0;
}
void secDiff(Mat imgA, Mat imgB, int rows, int cols, int rowOffs, int colOffs, bool *mod, shared_t *share)
{
auto pnts = 0;
for (auto y = rowOffs; y < rows; y++)
2022-04-14 09:45:54 -04:00
{
for (auto x = colOffs; x < cols; x++)
2022-04-14 09:45:54 -04:00
{
auto pixA = imgA.at<uchar>(Point(x, y));
auto pixB = imgB.at<uchar>(Point(x, y));
if (pixDiff(pixA, pixB, share))
{
pnts += 1;
if (pnts >= share->blockThresh)
{
*mod = true; return;
}
}
}
2022-04-14 09:45:54 -04:00
}
}
bool imgDiff(Mat prev, Mat next, shared_t *share)
{
auto numOfXBlocks = prev.cols / share->blockX;
auto numOfYBlocks = prev.rows / share->blockY;
auto moInBlock = false;
vector<thread> threads;
for (auto x = 0; (x < numOfXBlocks) && !moInBlock; x += share->blockX)
{
for (auto y = 0; (y < numOfYBlocks) && !moInBlock; y += share->blockY)
{
threads.push_back(thread(secDiff, prev, next, share->blockY, share->blockX, y, x, &moInBlock, share));
}
}
for (auto &&thr : threads)
{
thr.join();
2022-04-14 09:45:54 -04:00
}
return moInBlock;
2022-04-14 09:45:54 -04:00
}
string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
2022-04-14 09:45:54 -04:00
{
for (int i = 0; i < argc; ++i)
{
auto argInParams = string(argv[i]);
if (arg.compare(argInParams) == 0)
{
if (!argOnly)
2022-04-14 09:45:54 -04:00
{
// check ahead, make sure i + 1 won't cause out-of-range exception
if ((i + 1) <= (argc - 1))
{
return string(argv[i + 1]);
}
}
else
{
return string("true");
2022-04-14 09:45:54 -04:00
}
}
}
return string();
}
void rdLine(const string &param, const string &line, string *value)
2022-04-14 09:45:54 -04:00
{
if (line.rfind(param.c_str(), 0) == 0)
{
*value = line.substr(param.size());
//cout << param << *value << endl;
}
}
void rdLine(const string &param, const string &line, int *value)
{
if (line.rfind(param.c_str(), 0) == 0)
{
*value = strtol(line.substr(param.size()).c_str(), NULL, 10);
//cout << param << *value << endl;
}
}
bool rdConf(shared_t *share)
{
ifstream varFile(share->conf.c_str());
if (!varFile.is_open())
2022-04-14 09:45:54 -04:00
{
share->retCode = ENOENT;
cerr << "err: Failed to open the config file: " << share->conf << " for reading. please check file permissions or if it exists." << endl;
2022-04-14 09:45:54 -04:00
}
else
2022-04-14 09:45:54 -04:00
{
string line;
share->recordUrl.clear();
share->outDir.clear();
share->postCmd.clear();
share->buffDir.clear();
share->colorThresh = 5;
share->secs = 60;
share->blockX = 32;
share->blockY = 32;
share->blockThresh = 900;
share->maxDays = 5;
share->vidEtx = "mp4";
do
{
getline(varFile, line);
if (line.rfind("#", 0) != 0)
{
rdLine("recording_stream = ", line, &share->recordUrl);
rdLine("output_dir = ", line, &share->outDir);
rdLine("post_cmd = ", line, &share->postCmd);
rdLine("color_threshold = ", line, &share->colorThresh);
rdLine("duration = ", line, &share->secs);
rdLine("buff_dir = ", line, &share->buffDir);
rdLine("block_x = ", line, &share->blockX);
rdLine("block_y = ", line, &share->blockY);
rdLine("block_threshold = ", line, &share->blockThresh);
rdLine("max_days = ", line, &share->maxDays);
rdLine("vid_container = ", line, &share->vidEtx);
}
} while(!line.empty());
if (share->init)
{
remove_all(share->buffDir.c_str());
share->init = false;
}
new thread(enforceMaxDays, share);
share->retCode = 0;
2022-04-14 09:45:54 -04:00
}
varFile.close();
return share->retCode == 0;
}
bool capPair(Mat &prev, Mat &next, VideoCapture &capture, shared_t *share)
{
capture >> prev;
capture >> next;
return !prev.empty() && !next.empty();
}
void wrOut(const string &buffFile, shared_t *share)
{
auto clnDir = cleanDir(share->outDir);
auto vidOut = genDstFile(clnDir + "/." + genTimeStr("%Y-%m-%d"), "%H%M%S", "." + share->vidEtx);
auto m3uOut = vidOut.substr(clnDir.size() + 1);
auto lisOut = genDstFile(share->outDir, "%Y-%m-%d", ".m3u");
copy_file(buffFile.c_str(), vidOut.c_str());
remove(buffFile.c_str());
ofstream file;
if (fileExists(lisOut))
{
file.open(lisOut.c_str(), ios_base::app);
}
else
{
file.open(lisOut.c_str());
}
file << m3uOut << endl;
file.close();
}
bool moDetect(const string &buffFile, shared_t *share)
{
auto mod = false;
VideoCapture capture(buffFile.c_str(), CAP_FFMPEG);
if (capture.isOpened())
{
Mat prev;
Mat next;
while (capPair(prev, next, capture, share))
{
if (imgDiff(toGray(prev), toGray(next), share))
{
mod = true; break;
}
}
if (mod)
{
new thread(wrOut, buffFile, share);
}
}
else
{
cerr << "err: Could not open buff file: " << buffFile << " for reading. check formatting/permissions." << endl;
cerr << " Also check if opencv was compiled with FFMPEG encoding enabled." << endl;
}
if (!mod)
{
remove(buffFile.c_str());
}
return mod;
}
void recLoop(shared_t *share)
{
while (rdConf(share))
{
auto mod = false;
for (auto ind = 0; ind < share->secs; ind += BUF_SZ)
{
auto bufPath = genTmpFile(share->buffDir, "." + share->vidEtx, share);
auto secs = to_string(BUF_SZ);
auto limSecs = to_string(BUF_SZ + 3);
auto cmd = "timeout -k 1 " + limSecs + " ffmpeg -hide_banner -i " + share->recordUrl + " -y -vcodec copy -t " + secs + " " + bufPath;
if (system(cmd.c_str()) == 0)
{
if (mod)
{
new thread(wrOut, bufPath, share);
mod = false;
}
else if (moDetect(bufPath, share))
{
mod = true;
ind = 0;
}
}
else if (fileExists(bufPath))
{
remove(bufPath.c_str());
sleep(BUF_SZ);
}
}
system(share->postCmd.c_str());
}
}
void showHelp()
{
cout << "Motion Watch v1.1" << 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;
}
int main(int argc, char** argv)
{
sharedRes.conf = parseForParam("-c", argc, argv, false);
if (parseForParam("-h", argc, argv, true) == "true")
2022-04-14 09:45:54 -04:00
{
showHelp();
2022-04-14 09:45:54 -04:00
}
else if (sharedRes.conf.empty())
2022-04-14 09:45:54 -04:00
{
cerr << "err: A config file was not given in -c" << endl;
2022-04-14 09:45:54 -04:00
}
else
{
sharedRes.retCode = 0;
sharedRes.tmpId = 0;
sharedRes.init = true;
thread th1(recLoop, &sharedRes);
th1.join();
2022-04-14 09:45:54 -04:00
return sharedRes.retCode;
2022-04-14 09:45:54 -04:00
}
return EINVAL;
2022-04-14 09:45:54 -04:00
}