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
This commit is contained in:
parent
81da33ba81
commit
a065b7a1d3
1
setup.sh
1
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
|
||||
|
|
199
src/common.cpp
199
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<string> lsFilesInDir(const string &path, const string &ext)
|
||||
{
|
||||
vector<string> names;
|
||||
|
@ -97,30 +119,32 @@ vector<string> 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
|
||||
current_path(share->outDir, ec);
|
||||
|
||||
share->retCode = ec.value();
|
||||
|
||||
if (share->retCode != 0)
|
||||
{
|
||||
cerr << "err: none of the expected config files could be read." << endl;
|
||||
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<string> parseForList(const string &arg, int argc, char** argv)
|
||||
string genEventPath(const string &tsPath)
|
||||
{
|
||||
auto offs = 0;
|
||||
auto ret = vector<string>();
|
||||
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
|
||||
{
|
||||
ret.push_back(param);
|
||||
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)
|
||||
{
|
||||
// remove all '/'
|
||||
ret.erase(ind, 1);
|
||||
|
||||
ind = ret.find('/');
|
||||
}
|
||||
while (!param.empty());
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void waitForDetThreads(shared_t *share)
|
||||
{
|
||||
for (auto &&thr : share->detThreads)
|
||||
{
|
||||
thr.join();
|
||||
}
|
||||
|
||||
share->detThreads.clear();
|
||||
else
|
||||
{
|
||||
return string();
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t genEpoch()
|
||||
{
|
||||
return duration_cast<seconds>(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();
|
||||
}
|
||||
}
|
||||
|
|
41
src/common.h
41
src/common.h
|
@ -18,6 +18,7 @@
|
|||
#include <string>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <chrono>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <vector>
|
||||
|
@ -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<thread> detThreads;
|
||||
vector<string> conf;
|
||||
vector<pls_t> recList;
|
||||
ofstream recLogFile;
|
||||
ofstream detLogFile;
|
||||
ofstream upkLogFile;
|
||||
string conf;
|
||||
string recLogPath;
|
||||
string detLogPath;
|
||||
string upkLogPath;
|
||||
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;
|
||||
bool postCmdRunning;
|
||||
int schSec;
|
||||
int frameGap;
|
||||
int pixThresh;
|
||||
int imgThresh;
|
||||
int numOfClips;
|
||||
int maxDays;
|
||||
int maxClips;
|
||||
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<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);
|
||||
uint64_t genEpoch();
|
||||
|
||||
#endif // COMMON_H
|
||||
|
|
|
@ -22,6 +22,11 @@ void detLog(const string &line, shared_t *share)
|
|||
share->detLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "<br>" << endl;
|
||||
}
|
||||
|
||||
void upkLog(const string &line, shared_t *share)
|
||||
{
|
||||
share->upkLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "<br>" << endl;
|
||||
}
|
||||
|
||||
void enforceMaxLogSize(const string &filePath, shared_t *share)
|
||||
{
|
||||
if (exists(filePath))
|
||||
|
@ -48,6 +53,8 @@ void initLogFile(const string &filePath, ofstream &fileObj)
|
|||
|
||||
void initLogFrontPage(const string &filePath, const string &logLinesFile)
|
||||
{
|
||||
if (!exists(filePath))
|
||||
{
|
||||
string htmlText = "<!DOCTYPE html>\n";
|
||||
|
||||
htmlText += "<html>\n";
|
||||
|
@ -96,13 +103,16 @@ void initLogFrontPage(const string &filePath, const string &logLinesFile)
|
|||
outFile << htmlText;
|
||||
|
||||
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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
201
src/main.cpp
201
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,23 +117,31 @@ 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);
|
||||
|
||||
for (auto i = 0; i < share->numOfClips; ++i)
|
||||
void recLoop(shared_t *share)
|
||||
{
|
||||
while (share->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;
|
||||
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";
|
||||
|
||||
recLog("ffmpeg_run: " + cmd, share);
|
||||
|
||||
|
@ -79,52 +149,12 @@ void recLoop(shared_t *share)
|
|||
|
||||
recLog("ffmpeg_retcode: " + to_string(retCode), share);
|
||||
|
||||
if (retCode == 0)
|
||||
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);
|
||||
recLog("err: ffmpeg returned non zero, indicating failure. please check stderr output.", share);
|
||||
}
|
||||
|
||||
sleep(share->clipLen);
|
||||
}
|
||||
}
|
||||
|
||||
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 <argument>" << 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")
|
||||
{
|
||||
|
@ -158,9 +184,26 @@ int main(int argc, char** argv)
|
|||
{
|
||||
sharedRes.retCode = 0;
|
||||
sharedRes.skipCmd = false;
|
||||
sharedRes.init = true;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -12,17 +12,47 @@
|
|||
|
||||
#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);
|
||||
|
||||
if (!prev.empty() && !next.empty())
|
||||
{
|
||||
Mat diff;
|
||||
|
||||
absdiff(prev, next, diff);
|
||||
|
@ -32,95 +62,36 @@ bool imgDiff(const Mat &prev, const Mat &next, shared_t *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 (prev.empty())
|
||||
{
|
||||
capture.retrieve(prev);
|
||||
}
|
||||
else
|
||||
{
|
||||
capture.retrieve(next);
|
||||
|
||||
if (!share->postCmdRunning)
|
||||
{
|
||||
if (imgDiff(prev, next, share))
|
||||
{
|
||||
resize(next, vidThumb, Size(720, 480), INTER_LINEAR);
|
||||
|
@ -128,15 +99,20 @@ bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
19
src/web.cpp
19
src/web.cpp
|
@ -31,7 +31,7 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
|
|||
htmlText += "<body>\n";
|
||||
htmlText += "<h3>" + title + "</h3>\n";
|
||||
|
||||
if (!dirNames.empty())
|
||||
if (!dirNames.empty() && !exists(outputDir + "/VIDEO_TS"))
|
||||
{
|
||||
htmlText += "<ul>\n";
|
||||
|
||||
|
@ -43,6 +43,11 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
|
|||
htmlText += "</ul>\n";
|
||||
}
|
||||
|
||||
if (exists(outputDir + "/VIDEO_TS"))
|
||||
{
|
||||
htmlText += "<h4>Motion Events</h4>\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 = "<!DOCTYPE html>\n";
|
||||
|
||||
htmlText += "<html>\n";
|
||||
|
@ -102,12 +103,12 @@ void genHTMLvid(const string &outputVid, shared_t *share)
|
|||
htmlText += "</head>\n";
|
||||
htmlText += "<body>\n";
|
||||
htmlText += "<video width=100% height=100% controls autoplay>\n";
|
||||
htmlText += " <source src='" + vidName + "' type='video/" + share->vidExt + "'>\n";
|
||||
htmlText += " <source src='" + name + ".m3u8' type='video/MP2T'>\n";
|
||||
htmlText += "</video>\n";
|
||||
htmlText += "</body>\n";
|
||||
htmlText += "</html>";
|
||||
|
||||
ofstream file(string(filePath + "/" + fileName + ".html").c_str());
|
||||
ofstream file(string(name + ".html").c_str());
|
||||
|
||||
file << htmlText << endl;
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user