From a065b7a1d3364d708a0896783a022ff1a2a427aa Mon Sep 17 00:00:00 2001 From: Maurice ONeal Date: Sun, 5 Mar 2023 16:07:07 -0500 Subject: [PATCH] v2.0.t1 Completely reformed the internal workings of the application code. I brought back multi-threaded functions so there is now 5 separate threads for different tasks. recLoop() - this function calls ffmpeg to begin recording footage from the defined camera and stores the footage in hls format. It is designed to keep running for as long as the application is running and if it does stop for whatever reason, it will attempt to auto re-start. upkeep() - this function does regular cleanup and enforcement of maxDays maxLogSize and maxEvents without the need to stop recording or detecting motion. detectMo() - this function reads directly from recLoop's hls output and list all footage that has motion in it. motion detection no longer has to wait for the clip to finish recording thanks to the use of .ts containers for the video clips. this makes the motion detection for less cpu intensive now that it will now operate at the camera's fps (slower). eventLoop() - this function reads the motion list from detectMo and copies the footage pointed out by the list to an events folder, also in hls format. schLoop() - this function runs an optional user defined external command every amount of seconds defined in sch_sec. this command temporary stops motion detection without actually terminating the thread. It will also not run the command at the scheduled time if motion was detected. Benefits to this reform: - far less cpu intensive operation - multi-threaded architecture for better asynchronous operation - it has support for live streaming now that hls is being used - a buff_dir is no longer necessary --- setup.sh | 1 + src/common.cpp | 203 ++++++++++++++++++++++++++---------------- src/common.h | 81 ++++++++++------- src/logger.cpp | 98 +++++++++++---------- src/logger.h | 1 + src/main.cpp | 219 +++++++++++++++++++++++++++------------------- src/mo_detect.cpp | 162 +++++++++++++++------------------- src/mo_detect.h | 6 +- src/web.cpp | 19 ++-- src/web.h | 2 +- 10 files changed, 446 insertions(+), 346 deletions(-) diff --git a/setup.sh b/setup.sh index 7b1f528..34579af 100644 --- a/setup.sh +++ b/setup.sh @@ -16,6 +16,7 @@ apt install -y libswscale-dev apt install -y libgstreamer1.0-dev apt install -y x264 apt install -y libx264-dev +apt install -y libilmbase-dev apt install -y libopencv-dev apt install -y apache2 add-apt-repository -y ppa:ubuntu-toolchain-r/test diff --git a/src/common.cpp b/src/common.cpp index 9f51039..5057722 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -52,6 +52,28 @@ bool createDirTree(const string &full_path) return ret; } +void cleanupEmptyDirs(const string &path) +{ + if (exists(path)) + { + for (auto &entry : directory_iterator(path)) + { + if (entry.is_directory()) + { + try + { + remove(entry.path()); + } + catch (filesystem_error const &ex) + { + // non-empty dir assumed when filesystem_error is raised. + cleanupEmptyDirs(path + "/" + entry.path().filename().string()); + } + } + } + } +} + vector lsFilesInDir(const string &path, const string &ext) { vector names; @@ -97,30 +119,32 @@ vector lsDirsInDir(const string &path) return names; } -void enforceMaxDays(const string &dirPath, shared_t *share) +void cleanupStream(const string &plsPath) { - auto names = lsDirsInDir(dirPath); + ifstream fileIn(plsPath); - while (names.size() > (share->maxDays - 1)) + for (string line; getline(fileIn, line); ) { - remove_all(string(cleanDir(dirPath) + "/" + names[0]).c_str()); - - names.erase(names.begin()); + if (line.starts_with("VIDEO_TS/")) + { + remove(line); + } } } -void enforceMaxClips(const string &dirPath, shared_t *share) +void enforceMaxEvents(shared_t *share) { - auto names = lsFilesInDir(dirPath, "." + share->vidExt); + auto names = lsFilesInDir(".", ".m3u8"); - while (names.size() > share->maxClips) + while (names.size() > share->maxEvents) { - // 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"; + // removes the playlist file extension. + auto nameOnly = names[0].substr(0, names[0].size() - 5); + auto imgFile = nameOnly + ".jpg"; + auto webFile = nameOnly + ".html"; - remove(cleanDir(dirPath) + "/" + names[0]); + cleanupStream(names[0]); + remove(names[0]); if (exists(imgFile)) remove(imgFile); if (exists(webFile)) remove(webFile); @@ -129,6 +153,7 @@ void enforceMaxClips(const string &dirPath, shared_t *share) } } + string genTimeStr(const char *fmt) { time_t rawtime; @@ -175,7 +200,7 @@ bool rdConf(const string &filePath, shared_t *share) { share->retCode = ENOENT; - cout << "wrn: config file: " << filePath << " does not exists or lack read permissions." << endl; + cerr << "err: config file: " << filePath << " does not exists or lack read permissions." << endl; } else { @@ -193,17 +218,14 @@ bool rdConf(const string &filePath, shared_t *share) rdLine("web_text = ", line, &share->webTxt); rdLine("web_bg = ", line, &share->webBg); rdLine("web_font = ", line, &share->webFont); + rdLine("sch_sec = ", line, &share->schSec); 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_events = ", line, &share->maxEvents); rdLine("max_log_size = ", line, &share->maxLogSize); - rdLine("vid_container = ", line, &share->vidExt); rdLine("vid_codec = ", line, &share->vidCodec); } @@ -217,69 +239,55 @@ bool rdConf(shared_t *share) { share->recordUrl.clear(); share->postCmd.clear(); - share->buffDir.clear(); share->camName.clear(); share->recLogPath.clear(); share->detLogPath.clear(); + share->upkLogPath.clear(); share->recLogFile.close(); share->detLogFile.close(); + share->upkLogFile.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->maxEvents = 20; share->maxLogSize = 50000; - share->webRoot = "/var/www/html"; - share->buffDir = "/tmp"; - share->vidExt = "mp4"; - share->vidCodec = "copy"; share->skipCmd = false; + share->schSec = 60; + share->webRoot = "/var/www/html"; + share->vidCodec = "copy"; 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 (rdConf(share->conf, share)) { if (share->camName.empty()) { - share->camName = path(share->conf.back()).filename(); + share->camName = path(share->conf).filename(); } 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->upkLogPath = share->outDir + "/upk_log_lines.html"; - if (share->init) - { - if (exists(share->buffDir)) - { - remove_all(share->buffDir); - } + error_code ec; - share->init = false; - } - - createDirTree(cleanDir(share->buffDir)); createDirTree(share->outDir); - } - else - { - cerr << "err: none of the expected config files could be read." << endl; + current_path(share->outDir, ec); + + share->retCode = ec.value(); + + if (share->retCode != 0) + { + cerr << "err: " << ec.message() << endl; + } } - return ret; + return share->retCode == 0; } string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs) @@ -318,32 +326,81 @@ string parseForParam(const string &arg, int argc, char** argv, bool argOnly) return parseForParam(arg, argc, argv, argOnly, notUsed); } -vector parseForList(const string &arg, int argc, char** argv) +string genEventPath(const string &tsPath) { - auto offs = 0; - auto ret = vector(); - string param; - - do + if (tsPath.size() > 14) { - param = parseForParam(arg, argc, argv, false, offs); + // removes 'VIDEO_TS/live/' from the front of the string. + auto ret = tsPath.substr(14); - if (!param.empty()) + return "VIDEO_TS/events/" + ret; + } + else + { + return string(); + } +} + +string genVidNameFromLive(const string &tsPath) +{ + if (tsPath.size() > 17) + { + // removes 'VIDEO_TS/live/' from the front of the string. + auto ret = tsPath.substr(14); + auto ind = tsPath.find('/'); + // removes '.ts' from the end of the string. + ret = ret.substr(0, ret.size() - 3); + + while (ind != string::npos) { - ret.push_back(param); + // remove all '/' + ret.erase(ind, 1); + + ind = ret.find('/'); } + + return ret; } - while (!param.empty()); - - return ret; -} - -void waitForDetThreads(shared_t *share) -{ - for (auto &&thr : share->detThreads) + else { - thr.join(); + return string(); + } +} + +uint64_t genEpoch() +{ + return duration_cast(system_clock::now().time_since_epoch()).count(); +} + +void wrOutm3u8(const pls_t &pls) +{ + createDirTree(path(pls.dstPath).parent_path().string()); + copy_file(pls.clipPath, pls.dstPath); + + auto plsFile = pls.fileName + ".m3u8"; + + if (exists(plsFile)) + { + ofstream file(plsFile.c_str(), ofstream::app); + + file << endl; + file << pls.extINF << endl; + file << pls.dstPath; + + file.close(); + } + else + { + ofstream file(plsFile.c_str()); + + file << "#EXTM3U" << endl; + file << "#EXT-X-VERSION:3" << endl; + file << "#EXT-X-TARGETDURATION:10" << endl; + file << "#EXT-X-MEDIA-SEQUENCE:0" << endl; + file << "#EXT-X-DISCONTINUITY" << endl; + file << pls.extINF << endl; + file << pls.dstPath; + + file.close(); } - - share->detThreads.clear(); } diff --git a/src/common.h b/src/common.h index f882d79..bea0239 100644 --- a/src/common.h +++ b/src/common.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -34,42 +35,54 @@ using namespace cv; using namespace std; using namespace std::filesystem; +using namespace std::chrono; -#define APP_VER "1.6.t9" +#define APP_VER "2.0.t1" #define APP_NAME "Motion Watch" +struct pls_t +{ + string fileName; + string clipPath; + string dstPath; + string extINF; + uint64_t createTime; + Mat thumbnail; +}; + struct shared_t { - vector detThreads; - vector conf; - ofstream recLogFile; - ofstream detLogFile; - string recLogPath; - string detLogPath; - string recordUrl; - string outDir; - string postCmd; - string buffDir; - string vidExt; - string vidCodec; - string camName; - string webBg; - string webTxt; - string webFont; - string webRoot; - bool init; - bool skipCmd; - int clipLen; - int frameGap; - int pixThresh; - int imgThresh; - int numOfClips; - int maxDays; - int maxClips; - int maxLogSize; - int retCode; + vector recList; + ofstream recLogFile; + ofstream detLogFile; + ofstream upkLogFile; + string conf; + string recLogPath; + string detLogPath; + string upkLogPath; + string recordUrl; + string outDir; + string postCmd; + string vidCodec; + string camName; + string webBg; + string webTxt; + string webFont; + string webRoot; + bool skipCmd; + bool postCmdRunning; + int schSec; + int frameGap; + int pixThresh; + int imgThresh; + int maxDays; + int maxEvents; + int maxLogSize; + int retCode; }; +string genVidNameFromLive(const string &tsPath); +string genEventPath(const string &tsPath); string genDstFile(const string &dirOut, const char *fmt, const string &ext); string genTimeStr(const char *fmt); string cleanDir(const string &path); @@ -77,15 +90,15 @@ string parseForParam(const string &arg, int argc, char** argv, bool argO string parseForParam(const string &arg, int argc, char** argv, bool argOnly); bool createDir(const string &dir); bool createDirTree(const string &full_path); -void enforceMaxDays(const string &dirPath, shared_t *share); -void enforceMaxClips(const string &dirPath, shared_t *share); void rdLine(const string ¶m, const string &line, string *value); void rdLine(const string ¶m, const string &line, int *value); -void statOut(shared_t *share); -void waitForDetThreads(shared_t *share); +void cleanupEmptyDirs(const string &path); +void cleanupStream(const string &plsPath); +void wrOutm3u8(const pls_t &pls); +void enforceMaxEvents(shared_t *share); bool rdConf(shared_t *share); -vector parseForList(const string &arg, int argc, char** argv); vector lsFilesInDir(const string &path, const string &ext = string()); vector lsDirsInDir(const string &path); +uint64_t genEpoch(); #endif // COMMON_H diff --git a/src/logger.cpp b/src/logger.cpp index 7ba30e3..1bbbbd8 100644 --- a/src/logger.cpp +++ b/src/logger.cpp @@ -22,6 +22,11 @@ void detLog(const string &line, shared_t *share) share->detLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "
" << endl; } +void upkLog(const string &line, shared_t *share) +{ + share->upkLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "
" << endl; +} + void enforceMaxLogSize(const string &filePath, shared_t *share) { if (exists(filePath)) @@ -48,61 +53,66 @@ void initLogFile(const string &filePath, ofstream &fileObj) void initLogFrontPage(const string &filePath, const string &logLinesFile) { - string htmlText = "\n"; + if (!exists(filePath)) + { + string htmlText = "\n"; - htmlText += "\n"; - htmlText += "\n"; - htmlText += "\n"; - htmlText += "\n"; - htmlText += "\n"; - htmlText += "\n"; - htmlText += "\n"; - htmlText += "\n"; - htmlText += "\n"; - htmlText += "

\n"; - htmlText += "

\n"; - htmlText += "\n"; - htmlText += "

\n"; - htmlText += "\n"; - htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; + htmlText += "

\n"; + htmlText += "

\n"; + htmlText += "\n"; + htmlText += "

\n"; + htmlText += "\n"; + htmlText += "\n"; - ofstream outFile(filePath); + ofstream outFile(filePath); - outFile << htmlText; + outFile << htmlText; - outFile.close(); + outFile.close(); + } } void initLogFrontPages(shared_t *share) { auto recLogFilePath = share->outDir + "/recording_log.html"; auto detLogFilePath = share->outDir + "/detection_log.html"; + auto upkLogFilePath = share->outDir + "/upkeep_log.html"; initLogFrontPage(recLogFilePath, path(share->recLogPath).filename().string()); initLogFrontPage(detLogFilePath, path(share->detLogPath).filename().string()); + initLogFrontPage(upkLogFilePath, path(share->upkLogPath).filename().string()); } diff --git a/src/logger.h b/src/logger.h index 29f9074..3c81557 100644 --- a/src/logger.h +++ b/src/logger.h @@ -17,6 +17,7 @@ void recLog(const string &line, shared_t *share); void detLog(const string &line, shared_t *share); +void upkLog(const string &line, shared_t *share); void enforceMaxLogSize(const string &filePath, shared_t *share); void initLogFile(const string &filePath, ofstream &fileObj); void initLogFrontPages(shared_t *share); diff --git a/src/main.cpp b/src/main.cpp index 8aec3d4..4885df7 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,40 +12,102 @@ #include "mo_detect.h" #include "logger.h" +#include "web.h" -void detectMoInFile(const string &bufPath, shared_t *share) +void detectMo(shared_t *share) { - detLog("detect_mo_in_file() -- start", share); - - Mat thumbNail; - - if (moDetect(bufPath, thumbNail, share)) + while (share->retCode == 0) { - share->skipCmd = true; - - wrOut(bufPath, thumbNail, share); + sleep(2); + detectMoInStream("stream.m3u8", share); } - else if (exists(bufPath)) - { - remove(bufPath); - } - - detLog("detect_mo_in_file() -- finished", share); } -void recLoop(shared_t *share) +void eventLoop(shared_t *share) { - while (rdConf(share)) + while (share->retCode == 0) { - recLog("rec_loop() -- start", share); + while (!share->recList.empty()) + { + auto pls = share->recList[0]; + auto timeDiff = pls.createTime - genEpoch(); + // future time protection. this should never occur but + // this is in place in case the system time is incorrect. + if (timeDiff < 0) timeDiff = 0; + + // wait at least 6 seconds before processing the event in + // queue. + if (timeDiff < 6) sleep(6 - timeDiff); + + try + { + wrOutm3u8(pls); + genHTMLvid(pls.fileName); + + if (!exists(pls.fileName + ".jpg")) + { + imwrite(string(pls.fileName + "jpg").c_str(), pls.thumbnail); + } + } + catch (filesystem_error &ex) + { + recLog("err: could not copy live file: " + pls.clipPath + " reason - " + ex.what(), share); + } + + share->recList.erase(share->recList.begin()); + } + + sleep(5); + } +} + +void schLoop(shared_t *share) +{ + if (!share->postCmd.empty()) + { + while (share->retCode == 0) + { + sleep(share->schSec); + + if (!share->skipCmd) + { + share->postCmdRunning = true; + + detLog("no motion detected, running post command: " + share->postCmd, share); + system(share->postCmd.c_str()); + + share->postCmdRunning = false; + } + else + { + share->skipCmd = false; + + detLog("motion detected, skipping the post command.", share); + } + } + } +} + +void upkeep(shared_t *share) +{ + while (share->retCode == 0) + { enforceMaxLogSize(share->recLogPath, share); enforceMaxLogSize(share->detLogPath, share); + enforceMaxLogSize(share->upkLogPath, share); initLogFile(share->recLogPath, share->recLogFile); initLogFile(share->detLogPath, share->detLogFile); + initLogFile(share->upkLogPath, share->upkLogFile); initLogFrontPages(share); + enforceMaxEvents(share); + cleanupEmptyDirs("VIDEO_TS"); + + genHTMLul(".", share->camName, share); + + upkLog("camera specific webroot page updated: " + share->outDir + "/index.html", share); if (!exists("/tmp/mow-lock")) { @@ -55,76 +117,44 @@ void recLoop(shared_t *share) genHTMLul(share->webRoot, string(APP_NAME) + " " + string(APP_VER), share); remove("/tmp/mow-lock"); - recLog("webroot page updated: " + cleanDir(share->webRoot) + "/index.html", share); + upkLog("webroot page updated: " + cleanDir(share->webRoot) + "/index.html", share); } else { - recLog("skipping update of the webroot page, it is busy.", share); + upkLog("skipping update of the webroot page, it is busy.", share); } - genHTMLul(share->outDir, share->camName, share); + sleep(60); + } +} - recLog("camera specific webroot page updated: " + share->outDir + "/index.html", share); +void recLoop(shared_t *share) +{ + while (share->retCode == 0) + { + auto cmd = "ffmpeg -hide_banner -i " + + share->recordUrl + + " -strftime 1" + + " -strftime_mkdir 1" + + " -hls_segment_filename 'VIDEO_TS/live/%Y/%j/%H/%M%S.ts'" + + " -y -vcodec " + + share->vidCodec + + " -f hls -hls_time 6 -hls_list_size " + + to_string((share->maxDays * 86400) / 6) + // 86400 seconds in a day. + " stream.m3u8"; - for (auto i = 0; i < share->numOfClips; ++i) + recLog("ffmpeg_run: " + cmd, share); + + auto retCode = system(cmd.c_str()); + + recLog("ffmpeg_retcode: " + to_string(retCode), share); + + if (retCode != 0) { - auto bufPath = cleanDir(share->buffDir) + "/" + to_string(i) + "." + share->vidExt; - auto cmd = "timeout -k 1 " + to_string(share->clipLen + 2) + " "; - - cmd += "ffmpeg -hide_banner -i " + share->recordUrl + " -y -vcodec " + share->vidCodec + " -movflags faststart -t " + to_string(share->clipLen) + " " + bufPath; - - recLog("ffmpeg_run: " + cmd, share); - - auto retCode = system(cmd.c_str()); - - recLog("ffmpeg_retcode: " + to_string(retCode), share); - - if (retCode == 0) - { - recLog("detect_mo_in_file() -- started in a seperate thread.", share); - - share->detThreads.push_back(thread(detectMoInFile, bufPath, share)); - } - else - { - recLog("ffmpeg returned non zero, indicating failure. please check stderr output.", share); - - if (exists(bufPath)) - { - remove(bufPath); - } - - sleep(share->clipLen); - } + recLog("err: ffmpeg returned non zero, indicating failure. please check stderr output.", share); } - waitForDetThreads(share); - - if (!share->skipCmd) - { - recLog("no motion detected", share); - - if (share->postCmd.empty()) - { - recLog("post command not defined, skipping.", share); - } - else - { - recLog("running post command: " + share->postCmd, share); - system(share->postCmd.c_str()); - } - } - else - { - recLog("motion detected, skipping the post command.", share); - } - - recLog("rec_loop() -- finished", share); - - if (share->retCode != 0) - { - break; - } + sleep(60); } } @@ -132,19 +162,15 @@ int main(int argc, char** argv) { struct shared_t sharedRes; - sharedRes.conf = parseForList("-c", argc, argv); + sharedRes.conf = parseForParam("-c", argc, argv, false); if (parseForParam("-h", argc, argv, true) == "true") { cout << "Motion Watch " << APP_VER << endl << endl; cout << "Usage: mow " << endl << endl; cout << "-h : display usage information about this application." << endl; - cout << "-c : path to a config file." << endl; + cout << "-c : path to the 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") { @@ -156,11 +182,28 @@ int main(int argc, char** argv) } else { - sharedRes.retCode = 0; - sharedRes.skipCmd = false; - sharedRes.init = true; + sharedRes.retCode = 0; + sharedRes.skipCmd = false; + sharedRes.postCmdRunning = false; - recLoop(&sharedRes); + rdConf(&sharedRes); + + if (exists("VIDEO_TS/live")) + { + remove_all("VIDEO_TS/live"); + } + + auto thr1 = thread(recLoop, &sharedRes); + auto thr2 = thread(upkeep, &sharedRes); + auto thr3 = thread(detectMo, &sharedRes); + auto thr4 = thread(eventLoop, &sharedRes); + auto thr5 = thread(schLoop, &sharedRes); + + thr1.join(); + thr2.join(); + thr3.join(); + thr4.join(); + thr5.join(); return sharedRes.retCode; } diff --git a/src/mo_detect.cpp b/src/mo_detect.cpp index 48a8ab1..4a6dbe1 100644 --- a/src/mo_detect.cpp +++ b/src/mo_detect.cpp @@ -12,131 +12,107 @@ #include "mo_detect.h" -bool imgDiff(const Mat &prev, const Mat &next, shared_t *share) +void detectMoInStream(const string &bufPath, shared_t *share) +{ + ifstream fileIn(bufPath); + pls_t clip; + + auto clipPathFilter = genTimeStr("VIDEO_TS/live/%Y/%j/%H/"); + + for (string line; getline(fileIn, line); ) + { + if (line.starts_with(clipPathFilter)) + { + clip.clipPath = line; + } + else if (line.starts_with("#EXTINF")) + { + clip.extINF = line; + } + } + + if (!clip.clipPath.empty() && !clip.extINF.empty()) + { + if (moDetect(clip.clipPath, clip.thumbnail, share)) + { + clip.fileName = genTimeStr("%Y-%j-%H-%M"); + clip.dstPath = genEventPath(bufPath); + clip.createTime = genEpoch(); + + share->skipCmd = true; + + share->recList.push_back(clip); + } + } +} + +bool imgDiff(Mat &prev, Mat &next, shared_t *share) { auto ret = false; - detLog("img_diff() -- start()", share); + cvtColor(prev, prev, COLOR_BGR2GRAY); + cvtColor(next, next, COLOR_BGR2GRAY); - 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); + Mat diff; - if (!prev.empty() && !next.empty()) - { - Mat diff; + absdiff(prev, next, diff); + threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY); - absdiff(prev, next, diff); - threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY); + auto diffScore = countNonZero(diff); - auto diffScore = countNonZero(diff); + detLog("diff_score: " + to_string(diffScore), share); - detLog("diff_score: " + to_string(diffScore), share); - - ret = diffScore >= share->imgThresh; - } - - detLog("img_diff() -- finished()", share); - - return ret; -} - -Mat frameFF(VideoCapture *cap, int gap) -{ - Mat ret; - - if (gap == 0) gap = 1; - - for (int i = 0; i < gap; ++i) - { - cap->grab(); - } - - cap->retrieve(ret); - - if (!ret.empty()) - { - cvtColor(ret, ret, COLOR_BGR2GRAY); - } - - return ret; -} - -void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share) -{ - detLog("wr_out() -- start()", share); - detLog("buff_file: " + buffFile, share); - - auto dayStr = genTimeStr("%Y-%m-%d"); - auto timStr = genTimeStr("%H%M%S"); - auto outDir = cleanDir(share->outDir) + "/" + dayStr; - - if (!exists(outDir)) - { - enforceMaxDays(share->outDir, share); - } - - auto vidOut = genDstFile(outDir, timStr.c_str(), "." + share->vidExt); - auto imgOut = genDstFile(outDir, timStr.c_str(), ".jpg"); - - detLog("write_out_vid: " + vidOut, share); - detLog("write_out_img: " + imgOut, share); - - enforceMaxClips(outDir, share); - - copy_file(buffFile.c_str(), vidOut.c_str()); - remove(buffFile.c_str()); - - imwrite(imgOut.c_str(), vidThumb); - - genHTMLvid(vidOut, share); - genHTMLul(outDir, share->camName + ": " + dayStr, share); - genHTMLul(share->outDir, share->camName, share); - - detLog("wr_out() -- finished()", share); + return diffScore >= share->imgThresh; } bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share) { - detLog("mo_detect() -- start()", share); - detLog("buff_file: " + buffFile, share); - auto mod = false; + detLog("stream_clip: " + buffFile, share); + VideoCapture capture(buffFile.c_str(), CAP_FFMPEG); if (capture.isOpened()) { Mat prev; Mat next; - auto frameCount = capture.get(CAP_PROP_FRAME_COUNT); - auto frameGaps = frameCount / share->frameGap; - detLog("frame_count: " + to_string(frameCount), share); - detLog("frame_gaps: " + to_string(frameGaps), share); + detLog("capture open successful.", share); - for (auto i = 0; i < frameGaps; i++) + while (capture.grab()) { - if (prev.empty()) prev = frameFF(&capture, 1); - else prev = next.clone(); - - next = frameFF(&capture, share->frameGap); - - if (imgDiff(prev, next, share)) + if (prev.empty()) { - resize(next, vidThumb, Size(720, 480), INTER_LINEAR); - - mod = true; break; + capture.retrieve(prev); } + else + { + capture.retrieve(next); + + if (!share->postCmdRunning) + { + if (imgDiff(prev, next, share)) + { + resize(next, vidThumb, Size(720, 480), INTER_LINEAR); + + mod = true; break; + } + } + + prev.release(); + next.release(); + } + + sleep(1); } } else { - detLog("failed to open the buff file for reading. check permissions and/or opencv's video-io support (gstreamer/ffmpeg).", share); + detLog("capture open failure, check debug output.", share); } capture.release(); - detLog("mo_detect() -- finished()", share); - return mod; } diff --git a/src/mo_detect.h b/src/mo_detect.h index 894573c..fc0c823 100644 --- a/src/mo_detect.h +++ b/src/mo_detect.h @@ -14,12 +14,10 @@ // GNU General Public License for more details. #include "common.h" -#include "web.h" #include "logger.h" -bool imgDiff(const Mat &prev, const Mat &next, shared_t *share); +bool imgDiff(Mat &prev, Mat &next, shared_t *share); bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share); -void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share); -Mat frameFF(VideoCapture *cap, int gap); +void detectMoInStream(const string &bufPath, shared_t *share); #endif // MO_DETECT_H diff --git a/src/web.cpp b/src/web.cpp index ad64476..6154f12 100644 --- a/src/web.cpp +++ b/src/web.cpp @@ -31,7 +31,7 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share) htmlText += "\n"; htmlText += "

" + title + "

\n"; - if (!dirNames.empty()) + if (!dirNames.empty() && !exists(outputDir + "/VIDEO_TS")) { htmlText += "
    \n"; @@ -43,6 +43,11 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share) htmlText += "
\n"; } + if (exists(outputDir + "/VIDEO_TS")) + { + htmlText += "

Motion Events

\n"; + } + for (auto &®Name : regNames) { if (regName.ends_with("_log.html")) @@ -51,8 +56,7 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share) } else if (regName.ends_with(".html") && !regName.ends_with("index.html") && - !regName.ends_with("rec_log_lines.html") && - !regName.ends_with("det_log_lines.html")) + !regName.ends_with("_log_lines.html")) { // regName.substr(0, regName.size() - 5) removes .html auto name = regName.substr(0, regName.size() - 5); @@ -85,11 +89,8 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share) file.close(); } -void genHTMLvid(const string &outputVid, shared_t *share) +void genHTMLvid(const string &name) { - auto vidName = path(outputVid).filename().string(); - auto filePath = path(outputVid).parent_path().string(); - auto fileName = vidName.substr(0, vidName.size() - (share->vidExt.size() + 1)); string htmlText = "\n"; htmlText += "\n"; @@ -102,12 +103,12 @@ void genHTMLvid(const string &outputVid, shared_t *share) htmlText += "\n"; htmlText += "\n"; htmlText += "\n"; htmlText += "\n"; htmlText += ""; - ofstream file(string(filePath + "/" + fileName + ".html").c_str()); + ofstream file(string(name + ".html").c_str()); file << htmlText << endl; diff --git a/src/web.h b/src/web.h index 7e9575b..b716fc0 100644 --- a/src/web.h +++ b/src/web.h @@ -16,7 +16,7 @@ #include "common.h" void genHTMLul(const string &outputDir, const string &title, shared_t *share); -void genHTMLvid(const string &outputVid, shared_t *share); +void genHTMLvid(const string &name); void genCSS(shared_t *share); #endif // WEB_H