From 2687b938a0067631f688262f00dc41a23edf0804 Mon Sep 17 00:00:00 2001 From: Maurice ONeal Date: Sun, 11 Sep 2022 12:56:32 -0400 Subject: [PATCH] v1.2 Update Video clips recorded from the camera are no longer append, instead the clips are kept as is and then linked together in a playlist file in the output_dir. this makes it much more efficient and easier to maintain code. Also discovered that ffmpeg have a tendency to stall mid execution of recording from the rtsp stream every now and then. added a work around in the form of calling ffmpeg via the timeout command instead of directly so it will force kill ffmpeg if it goes longer than the expected BUF_SZ. Increased BUF_SZ to 10 secs. Added a clause in the recording loop that will make it write out a second clip if motion was detected. --- CMakeLists.txt | 2 +- README.md | 24 ++++--- src/main.cpp | 184 ++++++++++++++++++++++++++++++------------------- 3 files changed, 130 insertions(+), 80 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 27f6928..a6018ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 2.8) project( MotionWatch ) find_package( OpenCV REQUIRED ) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -pthread") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -pthread") include_directories( ${OpenCV_INCLUDE_DIRS} ) add_executable( mow src/main.cpp ) target_link_libraries( mow ${OpenCV_LIBS} ) diff --git a/README.md b/README.md index b3135e4..9167403 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ 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. ``` -# Motion Watch config file v1.1 +# Motion Watch config file v1.2 # # note all lines in this config file that starts with a '#' are ignored. # also note to avoid using empty lines. if you're going to need an empty @@ -34,8 +34,8 @@ recording_stream = rtsp://1.2.3.4:554/h264 # output_dir = /path/to/footage/directory # this is the output directory that will be used to store recorded footage -# from the camera. the file naming convention uses the current date. if -# the file already exists, new footage is appended to it. +# from the camera. the file naming convention uses the current date for +# playlist files which points to hidden video clips taken from the camera. # buff_dir = /tmp/ramdisk/cam_name # this application records small clips of the footage from the camera and @@ -45,11 +45,10 @@ buff_dir = /tmp/ramdisk/cam_name # for lots of writes. # color_threshold = 8 -# the color levels in each pixel of the detection stream can range from -# 0-255. in an ideal world the color differences in between frames should -# be 0 if there is no motion but must cameras can't do this. the threshold -# value here is used to filter if the pixels are truly different or if its -# seeing color differences of small objects that are of no interest. +# the color levels in each pixel of the camera stream can range from 0-255. +# in an ideal world the color differences in between frames should be 0 if +# there is no motion but must cameras can't do this. the threshold value +# here is used to filter if the pixels are truly different. # block_threshold = 3456 # this application detects motion by loading frames from the camera and @@ -80,6 +79,15 @@ post_cmd = move_the_ptz_camera.py # position of it's patrol pattern. note: the call to this command can be # delayed if motion was detected. # +max_days = 15 +# this defines the maximum amount of days worth of video clips that is +# allowed to be stored in the output_dir. whenever this limit is met, +# the oldest day is deleted. +# +vid_container = mp4 +# this is the video file format to use from recording footage from the +# camera. the format support depends entirely on the under laying ffmpeg +# installation. ``` ### Build Setup ### diff --git a/src/main.cpp b/src/main.cpp index 87b9820..59769f3 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,15 +8,17 @@ #include #include #include -#include +#include +#include #include #include using namespace cv; using namespace std; +using namespace std::filesystem; -#define BUF_SZ 3 +#define BUF_SZ 10 struct shared_t { @@ -28,7 +30,7 @@ struct shared_t string concatTxtTmp; string concatShTmp; string createShTmp; - mutex thrMutex; + string vidEtx; bool init; int tmpId; int colorThresh; @@ -36,6 +38,7 @@ struct shared_t int blockThresh; int blockX; int blockY; + int maxDays; int retCode; } sharedRes; @@ -100,7 +103,53 @@ void replaceAll(string &str, const string &from, const string &to) } } -string genDstFile(const string &dirOut, const string &ext) +vector lsFilesInDir(const string &path, const string &ext) +{ + DIR *dir; + struct dirent *ent; + vector 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) { time_t rawtime; @@ -108,22 +157,30 @@ string genDstFile(const string &dirOut, const string &ext) auto timeinfo = localtime(&rawtime); - char dateC[20]; + char ret[50]; - strftime(dateC, 20, "%Y-%m-%d", timeinfo); + 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("/") + string(dateC) + ext; + return cleanDir(dirOut) + string("/") + genTimeStr(fmt) + ext; } string genTmpFile(const string &dirOut, const string &ext, shared_t *share) { createDirTree(cleanDir(dirOut)); - share->tmpId += 1; + if (share->tmpId == 9999999) + { + share->tmpId = 0; + } - return cleanDir(dirOut) + string("/") + to_string(share->tmpId) + ext; + return cleanDir(dirOut) + string("/") + to_string(share->tmpId++) + ext; } Mat toGray(const Mat &src) @@ -161,16 +218,12 @@ void secDiff(Mat imgA, Mat imgB, int rows, int cols, int rowOffs, int colOffs, b auto pixA = imgA.at(Point(x, y)); auto pixB = imgB.at(Point(x, y)); - //cout << "pnts: " << pnts << endl; - if (pixDiff(pixA, pixB, share)) { pnts += 1; if (pnts >= share->blockThresh) { - lock_guard guard(share->thrMutex); - *mod = true; return; } } @@ -272,6 +325,8 @@ bool rdConf(shared_t *share) share->blockX = 32; share->blockY = 32; share->blockThresh = 900; + share->maxDays = 5; + share->vidEtx = "mp4"; do { @@ -288,17 +343,21 @@ bool rdConf(shared_t *share) 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) { - system(string("rm -r " + share->buffDir).c_str()); + remove_all(share->buffDir.c_str()); share->init = false; } + new thread(enforceMaxDays, share); + share->retCode = 0; } @@ -315,45 +374,30 @@ bool capPair(Mat &prev, Mat &next, VideoCapture &capture, shared_t *share) return !prev.empty() && !next.empty(); } -void wrOut(const string &buffFile, const string &dstPath, shared_t *share) +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; - auto scriptFile = genTmpFile(share->buffDir, ".sh", share); - auto scriptData = string(); - - if (fileExists(dstPath)) + if (fileExists(lisOut)) { - auto concatFile = genTmpFile(share->buffDir, ".txt", share); - auto existsFile = genTmpFile(share->outDir, ".ts", share); - auto concatData = share->concatTxtTmp; - - scriptData = share->concatShTmp; - - replaceAll(concatData, "%existsFile%", existsFile); - replaceAll(concatData, "%buffFile%", buffFile); - - replaceAll(scriptData, "%existsFile%", existsFile); - replaceAll(scriptData, "%concatFile%", concatFile); - - file.open(concatFile.c_str()); - file << concatData; - file.close(); + file.open(lisOut.c_str(), ios_base::app); } else { - scriptData = share->createShTmp; + file.open(lisOut.c_str()); } - replaceAll(scriptData, "%buffFile%", buffFile); - replaceAll(scriptData, "%dstPath%", dstPath); - replaceAll(scriptData, "%scriptFile%", scriptFile); + file << m3uOut << endl; - file.open(scriptFile.c_str()); - file << scriptData; file.close(); - - system(string("sh " + scriptFile + " &").c_str()); } bool moDetect(const string &buffFile, shared_t *share) @@ -377,19 +421,18 @@ bool moDetect(const string &buffFile, shared_t *share) if (mod) { - auto dstPath = genDstFile(share->outDir, ".ts"); - - wrOut(buffFile, dstPath, share); + 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) { - system(string("rm " + buffFile + " &").c_str()); + remove(buffFile.c_str()); } return mod; @@ -401,20 +444,35 @@ void recLoop(shared_t *share) { auto mod = false; - for (auto i = 0; i < share->secs; i += BUF_SZ) + for (auto ind = 0; ind < share->secs; ind += BUF_SZ) { - auto dstPath = genTmpFile(share->buffDir, ".ts", share); - auto cmd = "ffmpeg -hide_banner -loglevel error -i " + share->recordUrl + " -y -vcodec copy -t " + to_string(BUF_SZ) + " " + dstPath; + 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; - system(cmd.c_str()); + if (system(cmd.c_str()) == 0) + { + if (mod) + { + new thread(wrOut, bufPath, share); - mod = moDetect(dstPath, share); + mod = false; + } + else if (moDetect(bufPath, share)) + { + mod = true; + ind = 0; + } + } + else if (fileExists(bufPath)) + { + remove(bufPath.c_str()); + sleep(BUF_SZ); + } } - if (!mod) - { - system(share->postCmd.c_str()); - } + system(share->postCmd.c_str()); } } @@ -444,22 +502,6 @@ int main(int argc, char** argv) sharedRes.tmpId = 0; sharedRes.init = true; - sharedRes.concatTxtTmp += "file '%existsFile%'\n"; - sharedRes.concatTxtTmp += "file '%buffFile%'\n"; - - sharedRes.concatShTmp += "#!/bin/sh\n"; - sharedRes.concatShTmp += "cp '%dstPath%' '%existsFile%'\n"; - sharedRes.concatShTmp += "ffmpeg -hide_banner -loglevel error -y -f concat -safe 0 -i '%concatFile%' -c copy '%dstPath%'\n"; - sharedRes.concatShTmp += "rm '%concatFile%'\n"; - sharedRes.concatShTmp += "rm '%existsFile%'\n"; - sharedRes.concatShTmp += "rm '%buffFile%'\n"; - sharedRes.concatShTmp += "rm '%scriptFile%'\n"; - - sharedRes.createShTmp += "#!/bin/sh\n"; - sharedRes.createShTmp += "cp '%buffFile%' '%dstPath%'\n"; - sharedRes.createShTmp += "rm '%buffFile%'\n"; - sharedRes.createShTmp += "rm '%scriptFile%'\n"; - thread th1(recLoop, &sharedRes); th1.join();