Compare commits

..

No commits in common. "19872b3ff59fc69d647320a397f1b6024731c992" and "528b4105f75e5ab314f7528ab4ea76251c6bfc96" have entirely different histories.

11 changed files with 457 additions and 541 deletions

View File

@ -44,39 +44,76 @@ web_root = /var/www/html
# warning: this will overwrite any existing index.html files so be sure
# to choose a directory that doesn't have an existing website.
#
buff_dir = /tmp
# this application records small clips of the footage from the camera and
# then stores them into this directory. any clips with motion detected in
# them are moved to web_root; if no motion is detected, they are deleted.
# it is highly recommend to use a ramdisk tempfs for this since this
# directory is used for large amounts of writes.
#
cam_name = cam-1
# this is the optional camera name parameter to identify the camera. this
# name will also be used to as the base directory in web_root. if not
# defined, the name of the config file will be used.
# name will be used to form the directory structure in the web_root as
# well as buff_dir. if not defined, the name of the config file will be
# used.
#
pix_thresh = 150
# this value tells the application how far different the pixels need to be
# before the pixels are actually considered different. think of this as
# pixel diff sensitivity, the higher the value the lesser the sensitivity.
# maximum is 255.
#
frame_gap = 20
# this value is used to tell the application how far in between frames to
# check the pixel diffs for motion. the lower the value, the more frames
# will be checked, however with that comes higher cpu usage.
#
img_thresh = 80000
# this indicates how many pixels need to be different in between frames
# this indicates how many pixels need to be different in between frame_gap
# before it is considered motion. any video clips found with frames
# exceeding this value will be copied from live footage to event footage.
# exceeding this value will be moved from buff_dir to web_root.
#
max_events = 40
# this indicates the maximum amount of motion event video clips to keep
# before deleting the oldest clip.
clip_len = 20
# this parameter indicate the amount of seconds to record in each video
# clip from the camera that will be stored and then processed in buff_dir.
#
sch_sec = 60
# this is the amount of seconds to wait in between running post_cmd.
num_of_clips = 3
# this will tell the application how many video clips should be recorded
# to buff_dir from the camera before the recording loop pauses to do some
# house keeping. by house keeping, it will wait until all motion detection
# threads are finished, reload the config file and then call the post_cmd
# if no motion was detected in any of the video clips.
#
post_cmd = move_the_ptz_camera.py
# this an optional command to run with sch_sec. one great use for this
# is to move a ptz camera to the next position of it's patrol pattern.
# note: the call to this command will be delayed if motion was detected.
# this an optional command to run after num_of_clips is met. one great use
# for this is to move a ptz camera to the next position of it's patrol
# pattern. note: the call to this command will 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 web_root. whenever this limit is met, the
# oldest day and all of it's associated video clips will be deleted.
#
max_clips = 30
# this is the maximum amount of video clips that is allowed to be stored
# in web_root per day. whenever this limit is met, the oldest clip is
# deleted.
#
max_log_size = 50000
# this is the maximum byte size of all log files that can be stored in
# web_root. whenever this limit is met, the log file will be deleted and
# then eventually recreated blank.
#
vid_container = mp4
# this is the video file format to use for recording footage from the
# camera. the format support depends entirely on the under laying ffmpeg
# installation.
#
vid_codec = copy
# this is the video codec to use when pulling footage from the camera
# via ffmpeg. the default is "copy" meaning it will just match the codec
# from the camera itself without trans-coding.
#
web_text = #dee5ee
# this can be used to customize the color of the text in the web
# interface. it can be defined as any color understood by html5 standard.

View File

@ -16,7 +16,6 @@ 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

View File

@ -52,28 +52,6 @@ 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;
@ -119,32 +97,31 @@ vector<string> lsDirsInDir(const string &path)
return names;
}
void cleanupStream(const string &plsPath)
void enforceMaxDays(const string &dirPath, shared_t *share)
{
ifstream fileIn(plsPath);
auto names = lsDirsInDir(dirPath);
for (string line; getline(fileIn, line); )
while (names.size() > (share->maxDays - 1))
{
if (line.starts_with("VIDEO_TS/"))
{
remove(line);
}
remove_all(string(cleanDir(dirPath) + "/" + names[0]).c_str());
names.erase(names.begin());
}
}
void enforceMaxEvents(shared_t *share)
void enforceMaxClips(const string &dirPath, shared_t *share)
{
auto names = lsFilesInDir("events", ".mp4");
auto names = lsFilesInDir(dirPath, "." + share->vidExt);
while (names.size() > share->maxEvents)
while (names.size() > share->maxClips)
{
// removes the video file extension (.mp4).
auto nameOnly = "events/" + names[0].substr(0, names[0].size() - 4);
auto mp4File = nameOnly + string(".mp4");
auto imgFile = nameOnly + string(".jpg");
auto webFile = nameOnly + string(".html");
// 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(mp4File)) remove(mp4File);
if (exists(imgFile)) remove(imgFile);
if (exists(webFile)) remove(webFile);
@ -152,7 +129,6 @@ void enforceMaxEvents(shared_t *share)
}
}
string genTimeStr(const char *fmt)
{
time_t rawtime;
@ -199,7 +175,7 @@ bool rdConf(const string &filePath, shared_t *share)
{
share->retCode = ENOENT;
cerr << "err: config file: " << filePath << " does not exists or lack read permissions." << endl;
cout << "wrn: config file: " << filePath << " does not exists or lack read permissions." << endl;
}
else
{
@ -217,12 +193,18 @@ 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_events = ", line, &share->maxEvents);
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());
@ -235,43 +217,69 @@ bool rdConf(shared_t *share)
{
share->recordUrl.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->pixThresh = 50;
share->imgThresh = 800;
share->maxEvents = 40;
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->skipCmd = false;
share->schSec = 60;
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";
if (rdConf(share->conf, share))
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).filename();
share->camName = path(share->conf.back()).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";
error_code ec;
createDirTree(share->outDir);
current_path(share->outDir, ec);
share->retCode = ec.value();
if (share->retCode != 0)
if (share->init)
{
cerr << "err: " << ec.message() << endl;
}
if (exists(share->buffDir))
{
remove_all(share->buffDir);
}
return share->retCode == 0;
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;
}
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs)
@ -310,48 +318,32 @@ string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
return parseForParam(arg, argc, argv, argOnly, notUsed);
}
string genEventPath(const string &tsPath)
vector<string> parseForList(const string &arg, int argc, char** argv)
{
if (tsPath.size() > 14)
{
// removes 'VIDEO_TS/live/' from the front of the string.
auto ret = tsPath.substr(14);
auto offs = 0;
auto ret = vector<string>();
string param;
return "VIDEO_TS/events/" + ret;
}
else
do
{
return string();
param = parseForParam(arg, argc, argv, false, offs);
if (!param.empty())
{
ret.push_back(param);
}
}
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;
}
else
void waitForDetThreads(shared_t *share)
{
return string();
}
for (auto &&thr : share->detThreads)
{
thr.join();
}
uint64_t genEpoch()
{
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
share->detThreads.clear();
}

View File

@ -16,15 +16,17 @@
#include <iostream>
#include <fstream>
#include <string>
#include <unistd.h>
#include <time.h>
#include <chrono>
#include <stdlib.h>
#include <errno.h>
#include <vector>
#include <thread>
#include <filesystem>
#include <mutex>
#include <sys/types.h>
#include <sys/stat.h>
#include <map>
#include <fcntl.h>
#include <opencv4/opencv2/opencv.hpp>
#include <opencv4/opencv2/videoio.hpp>
@ -32,49 +34,42 @@
using namespace cv;
using namespace std;
using namespace std::filesystem;
using namespace std::chrono;
#define APP_VER "2.0"
#define APP_VER "1.6"
#define APP_NAME "Motion Watch"
#define REC_LOG_NAME "rec_log_lines.html"
#define DET_LOG_NAME "det_log_lines.html"
#define UPK_LOG_NAME "upk_log_lines.html"
struct pls_t
{
string evName;
vector<string> srcPaths;
uint64_t createTime;
Mat thumbnail;
};
struct shared_t
{
map<string, pls_t> recList;
string conf;
string recLog;
string detLog;
string upkLog;
vector<thread> detThreads;
vector<string> 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 procTime;
int schSec;
int clipLen;
int frameGap;
int pixThresh;
int imgThresh;
int maxEvents;
int numOfClips;
int maxDays;
int maxClips;
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);
@ -82,14 +77,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 &param, const string &line, string *value);
void rdLine(const string &param, const string &line, int *value);
void cleanupEmptyDirs(const string &path);
void cleanupStream(const string &plsPath);
void enforceMaxEvents(shared_t *share);
void statOut(shared_t *share);
void waitForDetThreads(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

View File

@ -14,17 +14,12 @@
void recLog(const string &line, shared_t *share)
{
share->recLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
share->recLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "<br>" << endl;
}
void detLog(const string &line, shared_t *share)
{
share->detLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
}
void upkLog(const string &line, shared_t *share)
{
share->upkLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
share->detLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "<br>" << endl;
}
void enforceMaxLogSize(const string &filePath, shared_t *share)
@ -38,30 +33,20 @@ void enforceMaxLogSize(const string &filePath, shared_t *share)
}
}
void dumpLogs(const string &fileName, const string &lines)
void initLogFile(const string &filePath, ofstream &fileObj)
{
if (!lines.empty())
if (!fileObj.is_open())
{
ofstream outFile;
if (exists(fileName))
if (!exists(filePath))
{
outFile.open(fileName.c_str(), ofstream::app);
}
else
{
outFile.open(fileName.c_str());
system(string("touch " + filePath).c_str());
}
outFile << lines;
outFile.close();
fileObj.open(filePath.c_str(), ofstream::app | ofstream::out);
}
}
void initLogFrontPage(const string &filePath, const string &logLinesFile)
{
if (!exists(filePath))
{
string htmlText = "<!DOCTYPE html>\n";
@ -112,11 +97,12 @@ void initLogFrontPage(const string &filePath, const string &logLinesFile)
outFile.close();
}
}
void initLogFrontPages(shared_t *share)
{
initLogFrontPage("logs/recording_log.html", REC_LOG_NAME);
initLogFrontPage("logs/detection_log.html", DET_LOG_NAME);
initLogFrontPage("logs/upkeep_log.html", UPK_LOG_NAME);
auto recLogFilePath = share->outDir + "/recording_log.html";
auto detLogFilePath = share->outDir + "/detection_log.html";
initLogFrontPage(recLogFilePath, path(share->recLogPath).filename().string());
initLogFrontPage(detLogFilePath, path(share->detLogPath).filename().string());
}

View File

@ -17,9 +17,8 @@
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 dumpLogs(const string &fileName, const string &lines);
void enforceMaxLogSize(const string &filePath, shared_t *share);
void initLogFile(const string &filePath, ofstream &fileObj);
void initLogFrontPages(shared_t *share);
#endif // lOGGER_H

184
src/main.cpp Normal file → Executable file
View File

@ -12,84 +12,40 @@
#include "mo_detect.h"
#include "logger.h"
#include "web.h"
void detectMo(shared_t *share)
void detectMoInFile(const string &bufPath, shared_t *share)
{
while (share->retCode == 0)
detLog("detect_mo_in_file() -- start", share);
Mat thumbNail;
if (moDetect(bufPath, thumbNail, share))
{
sleep(2);
detectMoInStream("stream.m3u8", share);
share->skipCmd = true;
wrOut(bufPath, thumbNail, share);
}
else if (exists(bufPath))
{
remove(bufPath);
}
void eventLoop(shared_t *share)
{
while (share->retCode == 0)
{
while (!share->recList.empty())
{
auto it = share->recList.begin();
auto evName = it->first;
auto event = it->second;
auto timeDiff = genEpoch() - event.createTime;
// wait at least 62 seconds before processing the event in
// queue.
if ((timeDiff > 0) && (timeDiff > 62))
{
try
{
createDirTree("events");
wrOutVod(event, share);
genHTMLvod(evName);
if (!exists("events/" + evName + ".jpg"))
{
imwrite(string("events/" + evName + ".jpg").c_str(), event.thumbnail);
}
}
catch (filesystem_error &ex)
{
recLog(string("err: ") + ex.what(), share);
detLog("detect_mo_in_file() -- finished", share);
}
share->recList.erase(it);
}
sleep(5);
}
sleep(5);
}
}
void upkeep(shared_t *share)
void recLoop(shared_t *share)
{
while (share->retCode == 0)
while (rdConf(share))
{
createDirTree("live");
createDirTree("events");
createDirTree("logs");
recLog("rec_loop() -- start", share);
enforceMaxLogSize(string("logs/") + REC_LOG_NAME, share);
enforceMaxLogSize(string("logs/") + DET_LOG_NAME, share);
enforceMaxLogSize(string("logs/") + UPK_LOG_NAME, share);
enforceMaxLogSize(share->recLogPath, share);
enforceMaxLogSize(share->detLogPath, share);
dumpLogs(string("logs/") + REC_LOG_NAME, share->recLog);
dumpLogs(string("logs/") + DET_LOG_NAME, share->detLog);
dumpLogs(string("logs/") + UPK_LOG_NAME, share->upkLog);
share->recLog.clear();
share->detLog.clear();
share->upkLog.clear();
initLogFile(share->recLogPath, share->recLogFile);
initLogFile(share->detLogPath, share->detLogFile);
initLogFrontPages(share);
enforceMaxEvents(share);
genHTMLul(".", share->camName, share);
upkLog("camera specific webroot page updated: " + share->outDir + "/index.html", share);
if (!exists("/tmp/mow-lock"))
{
@ -99,35 +55,23 @@ void upkeep(shared_t *share)
genHTMLul(share->webRoot, string(APP_NAME) + " " + string(APP_VER), share);
remove("/tmp/mow-lock");
upkLog("webroot page updated: " + cleanDir(share->webRoot) + "/index.html", share);
recLog("webroot page updated: " + cleanDir(share->webRoot) + "/index.html", share);
}
else
{
upkLog("skipping update of the webroot page, it is busy.", share);
recLog("skipping update of the webroot page, it is busy.", share);
}
sleep(60);
}
}
genHTMLul(share->outDir, share->camName, share);
void recLoop(shared_t *share)
{
while (share->retCode == 0)
{
if (exists("live"))
{
remove_all("live");
}
recLog("camera specific webroot page updated: " + share->outDir + "/index.html", share);
auto cmd = "ffmpeg -hide_banner -rtsp_transport tcp -timeout 3000000 -i " +
share->recordUrl +
" -strftime 1" +
" -strftime_mkdir 1" +
" -hls_segment_filename 'live/%Y-%j-%H-%M-%S.ts'" +
" -hls_flags delete_segments" +
" -y -vcodec copy" +
" -f hls -hls_time 10 -hls_list_size 400" +
" stream.m3u8";
for (auto i = 0; i < share->numOfClips; ++i)
{
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);
@ -135,12 +79,52 @@ void recLoop(shared_t *share)
recLog("ffmpeg_retcode: " + to_string(retCode), share);
if (retCode != 0)
if (retCode == 0)
{
recLog("err: ffmpeg returned non zero, indicating failure. please check stderr output.", share);
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(10);
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;
}
}
}
@ -148,15 +132,19 @@ int main(int argc, char** argv)
{
struct shared_t sharedRes;
sharedRes.conf = parseForParam("-c", argc, argv, false);
sharedRes.conf = parseForList("-c", argc, argv);
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 the config file." << endl;
cout << "-c : path to a 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")
{
@ -169,20 +157,10 @@ int main(int argc, char** argv)
else
{
sharedRes.retCode = 0;
sharedRes.procTime = 0;
sharedRes.skipCmd = false;
sharedRes.init = true;
rdConf(&sharedRes);
auto thr1 = thread(recLoop, &sharedRes);
auto thr2 = thread(upkeep, &sharedRes);
auto thr3 = thread(detectMo, &sharedRes);
auto thr4 = thread(eventLoop, &sharedRes);
thr1.join();
thr2.join();
thr3.join();
thr4.join();
recLoop(&sharedRes);
return sharedRes.retCode;
}

View File

@ -12,92 +12,95 @@
#include "mo_detect.h"
void detectMoInStream(const string &streamFile, shared_t *share)
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share)
{
if (share->procTime >= share->schSec)
auto ret = false;
detLog("img_diff() -- start()", share);
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())
{
if (!share->skipCmd)
{
detLog("no motion detected, running post command: " + share->postCmd, share);
system(share->postCmd.c_str());
}
else
{
share->skipCmd = false;
share->procTime = 0;
detLog("motion detected, skipping the post command.", share);
}
}
ifstream fileIn(streamFile);
string tsPath;
Mat thumbnail;
for (string line; getline(fileIn, line); )
{
if (line.starts_with("live/"))
{
tsPath = line;
}
}
if (!tsPath.empty())
{
if (moDetect(tsPath, thumbnail, share))
{
auto eventName = genTimeStr("%Y-%j-%H-%M");
if (share->recList.find(eventName) != share->recList.end())
{
share->recList[eventName].srcPaths.push_back(tsPath);
}
else
{
pls_t event;
event.srcPaths.push_back(tsPath);
event.createTime = genEpoch();
event.thumbnail = thumbnail.clone();
event.evName = eventName;
share->recList.insert(pair{eventName, event});
}
share->skipCmd = true;
}
share->procTime += 10;
}
}
bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share)
{
Mat prevGray;
Mat nextGray;
cvtColor(prev, prevGray, COLOR_BGR2GRAY);
cvtColor(next, nextGray, COLOR_BGR2GRAY);
Mat diff;
absdiff(prevGray, nextGray, diff);
absdiff(prev, next, diff);
threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY);
score = countNonZero(diff);
auto diffScore = countNonZero(diff);
detLog("diff_score: " + to_string(score) + " tresh: " + to_string(share->imgThresh), share);
detLog("diff_score: " + to_string(diffScore), share);
return score >= share->imgThresh;
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);
}
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
{
auto maxScore = 0;
auto score = 0;
detLog("mo_detect() -- start()", share);
detLog("buff_file: " + buffFile, share);
detLog("stream_clip: " + buffFile, share);
auto mod = false;
VideoCapture capture(buffFile.c_str(), CAP_FFMPEG);
@ -105,62 +108,35 @@ bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
{
Mat prev;
Mat next;
auto frameCount = capture.get(CAP_PROP_FRAME_COUNT);
auto frameGaps = frameCount / share->frameGap;
detLog("capture open successful.", share);
detLog("frame_count: " + to_string(frameCount), share);
detLog("frame_gaps: " + to_string(frameGaps), share);
while (capture.grab())
for (auto i = 0; i < frameGaps; i++)
{
if (prev.empty())
{
capture.retrieve(prev);
}
else
{
capture.retrieve(next);
if (prev.empty()) prev = frameFF(&capture, 1);
else prev = next.clone();
if (!next.empty())
{
if (imgDiff(prev, next, score, share))
{
if (score > maxScore)
{
maxScore = score;
next = frameFF(&capture, share->frameGap);
if (imgDiff(prev, next, share))
{
resize(next, vidThumb, Size(720, 480), INTER_LINEAR);
}
}
}
prev.release();
next.release();
mod = true; break;
}
sleep(1);
}
}
else
{
detLog("capture open failure, check debug output.", share);
detLog("failed to open the buff file for reading. check permissions and/or opencv's video-io support (gstreamer/ffmpeg).", share);
}
capture.release();
return maxScore > 0;
}
void wrOutVod(const pls_t &event, shared_t *share)
{
auto concat = event.evName + ".tmp";
ofstream file(concat.c_str());
for (auto i = 0; i < event.srcPaths.size(); ++i)
{
file << "file '" << event.srcPaths[i] << "''" << endl;
}
file.close();
system(string("ffmpeg -f concat -safe 0 -i " + concat + " -c copy events/" + event.evName + ".mp4").c_str());
remove(concat);
detLog("mo_detect() -- finished()", share);
return mod;
}

View File

@ -14,11 +14,12 @@
// GNU General Public License for more details.
#include "common.h"
#include "web.h"
#include "logger.h"
bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share);
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share);
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share);
void detectMoInStream(const string &streamFile, shared_t *share);
void wrOutVod(const pls_t &pls, shared_t *share);
void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share);
Mat frameFF(VideoCapture *cap, int gap);
#endif // MO_DETECT_H

View File

@ -15,8 +15,8 @@
void genHTMLul(const string &outputDir, const string &title, shared_t *share)
{
vector<string> logNames;
vector<string> eveNames;
vector<string> dirNames;
vector<string> regNames = lsFilesInDir(outputDir);
vector<string> dirNames = lsDirsInDir(outputDir);
string htmlText = "<!DOCTYPE html>\n";
@ -31,43 +31,8 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
htmlText += "<body>\n";
htmlText += "<h3>" + title + "</h3>\n";
if (exists(outputDir + "/live"))
if (!dirNames.empty())
{
eveNames = lsFilesInDir(outputDir + "/events", ".html");
logNames = lsFilesInDir(outputDir + "/logs", "_log.html");
htmlText += "<h4>Logs</h4>\n";
htmlText += "<ul>\n";
for (auto &&logName : logNames)
{
// name.substr(0, name.size() - 9) removes _log.html
auto name = logName.substr(0, logName.size() - 9);
htmlText += " <li><a href='logs/" + logName + "'>" + name + "</a></li>\n";
}
htmlText += "</ul>\n";
htmlText += "<h4>Live</h4>\n";
htmlText += "<ul>\n";
htmlText += " <li><a href='stream.html'>" + share->camName + ":live" + "</a></li>\n";
htmlText += "</ul>\n";
htmlText += "<h4>Motion Events</h4>\n";
genHTMLstream("stream");
for (auto &&eveName : eveNames)
{
// regName.substr(0, regName.size() - 5) removes .html
auto name = eveName.substr(0, eveName.size() - 5);
htmlText += "<a href='events/" + eveName + "'><img src='events/" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n";
}
}
else
{
dirNames = lsDirsInDir(outputDir);
htmlText += "<ul>\n";
for (auto &&dirName : dirNames)
@ -78,6 +43,38 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
htmlText += "</ul>\n";
}
for (auto &&regName : regNames)
{
if (regName.ends_with("_log.html"))
{
logNames.push_back(regName);
}
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.substr(0, regName.size() - 5) removes .html
auto name = regName.substr(0, regName.size() - 5);
htmlText += "<a href='" + regName + "'><img src='" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n";
}
}
if (!logNames.empty())
{
htmlText += "<h4>Logs</h4>\n";
htmlText += "<ul>\n";
for (auto &&name : logNames)
{
// name.substr(0, name.size() - 9) removes _log.html
htmlText += " <li><a href='" + name + "'>" + name.substr(0, name.size() - 9) + "</a></li>\n";
}
htmlText += "</ul>\n";
}
htmlText += "</body>\n";
htmlText += "</html>";
@ -88,55 +85,11 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
file.close();
}
void genHTMLstream(const string &name)
{
string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n";
htmlText += "<head>\n";
htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n";
htmlText += "<meta http-equiv=\"Pragma\" content=\"no-cache\" />\n";
htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n";
htmlText += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n";
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
htmlText += "</head>\n";
htmlText += "<body>\n";
htmlText += " <script src=\"https://cdn.jsdelivr.net/npm/hls.js@1\">\n";
htmlText += " </script>\n";
htmlText += " <video width=100% height=100% id=\"video\" controls>\n";
htmlText += " </video>\n";
htmlText += " <script>\n";
htmlText += " var video = document.getElementById('video');\n";
htmlText += " if (Hls.isSupported()) {\n";
htmlText += " var hls = new Hls({\n";
htmlText += " debug: true,\n";
htmlText += " });\n";
htmlText += " hls.loadSource('" + name + ".m3u8');\n";
htmlText += " hls.attachMedia(video);\n";
htmlText += " hls.on(Hls.Events.MEDIA_ATTACHED, function () {\n";
htmlText += " video.muted = true;\n";
htmlText += " video.play();\n";
htmlText += " });\n";
htmlText += " }\n";
htmlText += " else if (video.canPlayType('application/vnd.apple.mpegurl')) {\n";
htmlText += " video.src = '" + name + ".m3u8';\n";
htmlText += " video.addEventListener('canplay', function () {\n";
htmlText += " video.play();\n";
htmlText += " });\n";
htmlText += " }\n";
htmlText += " </script>\n";
htmlText += "</body>\n";
htmlText += "</html>";
ofstream file(string(name + ".html").c_str());
file << htmlText << endl;
file.close();
}
void genHTMLvod(const string &name)
void genHTMLvid(const string &outputVid, shared_t *share)
{
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";
@ -149,12 +102,12 @@ void genHTMLvod(const string &name)
htmlText += "</head>\n";
htmlText += "<body>\n";
htmlText += "<video width=100% height=100% controls autoplay>\n";
htmlText += " <source src='" + name + ".mp4' type='video/mp4'>\n";
htmlText += " <source src='" + vidName + "' type='video/" + share->vidExt + "'>\n";
htmlText += "</video>\n";
htmlText += "</body>\n";
htmlText += "</html>";
ofstream file(string("events/" + name + ".html").c_str());
ofstream file(string(filePath + "/" + fileName + ".html").c_str());
file << htmlText << endl;

View File

@ -16,8 +16,7 @@
#include "common.h"
void genHTMLul(const string &outputDir, const string &title, shared_t *share);
void genHTMLstream(const string &name);
void genHTMLvod(const string &name);
void genHTMLvid(const string &outputVid, shared_t *share);
void genCSS(shared_t *share);
#endif // WEB_H