Compare commits
23 Commits
528b4105f7
...
19872b3ff5
Author | SHA1 | Date | |
---|---|---|---|
|
19872b3ff5 | ||
|
83b206c06c | ||
|
93723bb7b1 | ||
|
58d957d0a4 | ||
|
533c27d9cb | ||
|
061c2571b4 | ||
|
baa69da2cd | ||
|
3c5dbec24c | ||
|
78919effcf | ||
|
0f6e7603df | ||
|
bddde644c1 | ||
|
ae46834777 | ||
|
b0dbfa0852 | ||
|
a065b7a1d3 | ||
|
81da33ba81 | ||
|
13eaf75c8a | ||
|
4dcd6c05a3 | ||
|
4758b62275 | ||
|
6ffe80b672 | ||
|
f4f1f62d25 | ||
|
23e0ae935e | ||
|
80f8ec07e3 | ||
|
62a6139f3a |
63
README.md
63
README.md
|
@ -44,76 +44,39 @@ web_root = /var/www/html
|
||||||
# warning: this will overwrite any existing index.html files so be sure
|
# warning: this will overwrite any existing index.html files so be sure
|
||||||
# to choose a directory that doesn't have an existing website.
|
# 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
|
cam_name = cam-1
|
||||||
# this is the optional camera name parameter to identify the camera. this
|
# this is the optional camera name parameter to identify the camera. this
|
||||||
# name will be used to form the directory structure in the web_root as
|
# name will also be used to as the base directory in web_root. if not
|
||||||
# well as buff_dir. if not defined, the name of the config file will be
|
# defined, the name of the config file will be used.
|
||||||
# used.
|
|
||||||
#
|
#
|
||||||
pix_thresh = 150
|
pix_thresh = 150
|
||||||
# this value tells the application how far different the pixels need to be
|
# this value tells the application how far different the pixels need to be
|
||||||
# before the pixels are actually considered different. think of this as
|
# before the pixels are actually considered different. think of this as
|
||||||
# pixel diff sensitivity, the higher the value the lesser the sensitivity.
|
# 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
|
img_thresh = 80000
|
||||||
# this indicates how many pixels need to be different in between frame_gap
|
# this indicates how many pixels need to be different in between frames
|
||||||
# before it is considered motion. any video clips found with frames
|
# before it is considered motion. any video clips found with frames
|
||||||
# exceeding this value will be moved from buff_dir to web_root.
|
# exceeding this value will be copied from live footage to event footage.
|
||||||
#
|
#
|
||||||
clip_len = 20
|
max_events = 40
|
||||||
# this parameter indicate the amount of seconds to record in each video
|
# this indicates the maximum amount of motion event video clips to keep
|
||||||
# clip from the camera that will be stored and then processed in buff_dir.
|
# before deleting the oldest clip.
|
||||||
#
|
#
|
||||||
num_of_clips = 3
|
sch_sec = 60
|
||||||
# this will tell the application how many video clips should be recorded
|
# this is the amount of seconds to wait in between running post_cmd.
|
||||||
# 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
|
post_cmd = move_the_ptz_camera.py
|
||||||
# this an optional command to run after num_of_clips is met. one great use
|
# this an optional command to run with sch_sec. one great use for this
|
||||||
# for this is to move a ptz camera to the next position of it's patrol
|
# is to move a ptz camera to the next position of it's patrol pattern.
|
||||||
# pattern. note: the call to this command will be delayed if motion was
|
# note: the call to this command will be delayed if motion was detected.
|
||||||
# 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
|
max_log_size = 50000
|
||||||
# this is the maximum byte size of all log files that can be stored in
|
# 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
|
# web_root. whenever this limit is met, the log file will be deleted and
|
||||||
# then eventually recreated blank.
|
# 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
|
web_text = #dee5ee
|
||||||
# this can be used to customize the color of the text in the web
|
# 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.
|
# interface. it can be defined as any color understood by html5 standard.
|
||||||
|
|
1
setup.sh
1
setup.sh
|
@ -16,6 +16,7 @@ apt install -y libswscale-dev
|
||||||
apt install -y libgstreamer1.0-dev
|
apt install -y libgstreamer1.0-dev
|
||||||
apt install -y x264
|
apt install -y x264
|
||||||
apt install -y libx264-dev
|
apt install -y libx264-dev
|
||||||
|
apt install -y libilmbase-dev
|
||||||
apt install -y libopencv-dev
|
apt install -y libopencv-dev
|
||||||
apt install -y apache2
|
apt install -y apache2
|
||||||
add-apt-repository -y ppa:ubuntu-toolchain-r/test
|
add-apt-repository -y ppa:ubuntu-toolchain-r/test
|
||||||
|
|
184
src/common.cpp
184
src/common.cpp
|
@ -52,6 +52,28 @@ bool createDirTree(const string &full_path)
|
||||||
return ret;
|
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> lsFilesInDir(const string &path, const string &ext)
|
||||||
{
|
{
|
||||||
vector<string> names;
|
vector<string> names;
|
||||||
|
@ -97,31 +119,32 @@ vector<string> lsDirsInDir(const string &path)
|
||||||
return names;
|
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());
|
if (line.starts_with("VIDEO_TS/"))
|
||||||
|
{
|
||||||
names.erase(names.begin());
|
remove(line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void enforceMaxClips(const string &dirPath, shared_t *share)
|
void enforceMaxEvents(shared_t *share)
|
||||||
{
|
{
|
||||||
auto names = lsFilesInDir(dirPath, "." + share->vidExt);
|
auto names = lsFilesInDir("events", ".mp4");
|
||||||
|
|
||||||
while (names.size() > share->maxClips)
|
while (names.size() > share->maxEvents)
|
||||||
{
|
{
|
||||||
// removes the video file extension.
|
// removes the video file extension (.mp4).
|
||||||
auto nameOnly = names[0].substr(0, names[0].size() - (share->vidExt.size() + 1));
|
auto nameOnly = "events/" + names[0].substr(0, names[0].size() - 4);
|
||||||
auto imgFile = cleanDir(dirPath) + "/" + nameOnly + ".jpg";
|
auto mp4File = nameOnly + string(".mp4");
|
||||||
auto webFile = cleanDir(dirPath) + "/" + nameOnly + ".html";
|
auto imgFile = nameOnly + string(".jpg");
|
||||||
|
auto webFile = nameOnly + string(".html");
|
||||||
remove(cleanDir(dirPath) + "/" + names[0]);
|
|
||||||
|
|
||||||
|
if (exists(mp4File)) remove(mp4File);
|
||||||
if (exists(imgFile)) remove(imgFile);
|
if (exists(imgFile)) remove(imgFile);
|
||||||
if (exists(webFile)) remove(webFile);
|
if (exists(webFile)) remove(webFile);
|
||||||
|
|
||||||
|
@ -129,6 +152,7 @@ void enforceMaxClips(const string &dirPath, shared_t *share)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
string genTimeStr(const char *fmt)
|
string genTimeStr(const char *fmt)
|
||||||
{
|
{
|
||||||
time_t rawtime;
|
time_t rawtime;
|
||||||
|
@ -175,7 +199,7 @@ bool rdConf(const string &filePath, shared_t *share)
|
||||||
{
|
{
|
||||||
share->retCode = ENOENT;
|
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
|
else
|
||||||
{
|
{
|
||||||
|
@ -193,18 +217,12 @@ bool rdConf(const string &filePath, shared_t *share)
|
||||||
rdLine("web_text = ", line, &share->webTxt);
|
rdLine("web_text = ", line, &share->webTxt);
|
||||||
rdLine("web_bg = ", line, &share->webBg);
|
rdLine("web_bg = ", line, &share->webBg);
|
||||||
rdLine("web_font = ", line, &share->webFont);
|
rdLine("web_font = ", line, &share->webFont);
|
||||||
|
rdLine("sch_sec = ", line, &share->schSec);
|
||||||
rdLine("post_cmd = ", line, &share->postCmd);
|
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("pix_thresh = ", line, &share->pixThresh);
|
||||||
rdLine("img_thresh = ", line, &share->imgThresh);
|
rdLine("img_thresh = ", line, &share->imgThresh);
|
||||||
rdLine("max_days = ", line, &share->maxDays);
|
rdLine("max_events = ", line, &share->maxEvents);
|
||||||
rdLine("max_clips = ", line, &share->maxClips);
|
|
||||||
rdLine("max_log_size = ", line, &share->maxLogSize);
|
rdLine("max_log_size = ", line, &share->maxLogSize);
|
||||||
rdLine("vid_container = ", line, &share->vidExt);
|
|
||||||
rdLine("vid_codec = ", line, &share->vidCodec);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} while(!line.empty());
|
} while(!line.empty());
|
||||||
|
@ -217,69 +235,43 @@ bool rdConf(shared_t *share)
|
||||||
{
|
{
|
||||||
share->recordUrl.clear();
|
share->recordUrl.clear();
|
||||||
share->postCmd.clear();
|
share->postCmd.clear();
|
||||||
share->buffDir.clear();
|
|
||||||
share->camName.clear();
|
share->camName.clear();
|
||||||
share->recLogPath.clear();
|
|
||||||
share->detLogPath.clear();
|
|
||||||
share->recLogFile.close();
|
|
||||||
share->detLogFile.close();
|
|
||||||
|
|
||||||
share->retCode = 0;
|
share->retCode = 0;
|
||||||
share->frameGap = 20;
|
share->pixThresh = 50;
|
||||||
share->pixThresh = 150;
|
share->imgThresh = 800;
|
||||||
share->imgThresh = 80000;
|
share->maxEvents = 40;
|
||||||
share->clipLen = 20;
|
|
||||||
share->numOfClips = 3;
|
|
||||||
share->maxDays = 15;
|
|
||||||
share->maxClips = 90;
|
|
||||||
share->maxLogSize = 50000;
|
share->maxLogSize = 50000;
|
||||||
share->webRoot = "/var/www/html";
|
|
||||||
share->buffDir = "/tmp";
|
|
||||||
share->vidExt = "mp4";
|
|
||||||
share->vidCodec = "copy";
|
|
||||||
share->skipCmd = false;
|
share->skipCmd = false;
|
||||||
|
share->schSec = 60;
|
||||||
|
share->webRoot = "/var/www/html";
|
||||||
share->webBg = "#485564";
|
share->webBg = "#485564";
|
||||||
share->webTxt = "#dee5ee";
|
share->webTxt = "#dee5ee";
|
||||||
share->webFont = "courier";
|
share->webFont = "courier";
|
||||||
|
|
||||||
auto ret = false;
|
if (rdConf(share->conf, share))
|
||||||
|
|
||||||
for (auto &&confPath: share->conf)
|
|
||||||
{
|
|
||||||
if (rdConf(confPath, share)) ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ret)
|
|
||||||
{
|
{
|
||||||
if (share->camName.empty())
|
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->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";
|
|
||||||
|
|
||||||
if (share->init)
|
error_code ec;
|
||||||
{
|
|
||||||
if (exists(share->buffDir))
|
|
||||||
{
|
|
||||||
remove_all(share->buffDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
share->init = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
createDirTree(cleanDir(share->buffDir));
|
|
||||||
createDirTree(share->outDir);
|
createDirTree(share->outDir);
|
||||||
}
|
current_path(share->outDir, ec);
|
||||||
else
|
|
||||||
{
|
share->retCode = ec.value();
|
||||||
cerr << "err: none of the expected config files could be read." << endl;
|
|
||||||
|
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)
|
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs)
|
||||||
|
@ -318,32 +310,48 @@ string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
|
||||||
return parseForParam(arg, argc, argv, argOnly, notUsed);
|
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;
|
if (tsPath.size() > 14)
|
||||||
auto ret = vector<string>();
|
|
||||||
string param;
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
{
|
||||||
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());
|
else
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
void waitForDetThreads(shared_t *share)
|
|
||||||
{
|
|
||||||
for (auto &&thr : share->detThreads)
|
|
||||||
{
|
{
|
||||||
thr.join();
|
return string();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
share->detThreads.clear();
|
|
||||||
|
uint64_t genEpoch()
|
||||||
|
{
|
||||||
|
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
|
||||||
}
|
}
|
||||||
|
|
82
src/common.h
82
src/common.h
|
@ -16,17 +16,15 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unistd.h>
|
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#include <chrono>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <mutex>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <fcntl.h>
|
#include <map>
|
||||||
|
|
||||||
#include <opencv4/opencv2/opencv.hpp>
|
#include <opencv4/opencv2/opencv.hpp>
|
||||||
#include <opencv4/opencv2/videoio.hpp>
|
#include <opencv4/opencv2/videoio.hpp>
|
||||||
|
@ -34,42 +32,49 @@
|
||||||
using namespace cv;
|
using namespace cv;
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace std::filesystem;
|
using namespace std::filesystem;
|
||||||
|
using namespace std::chrono;
|
||||||
|
|
||||||
#define APP_VER "1.6"
|
#define APP_VER "2.0"
|
||||||
#define APP_NAME "Motion Watch"
|
#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
|
struct shared_t
|
||||||
{
|
{
|
||||||
vector<thread> detThreads;
|
map<string, pls_t> recList;
|
||||||
vector<string> conf;
|
string conf;
|
||||||
ofstream recLogFile;
|
string recLog;
|
||||||
ofstream detLogFile;
|
string detLog;
|
||||||
string recLogPath;
|
string upkLog;
|
||||||
string detLogPath;
|
string recordUrl;
|
||||||
string recordUrl;
|
string outDir;
|
||||||
string outDir;
|
string postCmd;
|
||||||
string postCmd;
|
string camName;
|
||||||
string buffDir;
|
string webBg;
|
||||||
string vidExt;
|
string webTxt;
|
||||||
string vidCodec;
|
string webFont;
|
||||||
string camName;
|
string webRoot;
|
||||||
string webBg;
|
bool skipCmd;
|
||||||
string webTxt;
|
int procTime;
|
||||||
string webFont;
|
int schSec;
|
||||||
string webRoot;
|
int pixThresh;
|
||||||
bool init;
|
int imgThresh;
|
||||||
bool skipCmd;
|
int maxEvents;
|
||||||
int clipLen;
|
int maxLogSize;
|
||||||
int frameGap;
|
int retCode;
|
||||||
int pixThresh;
|
|
||||||
int imgThresh;
|
|
||||||
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 genDstFile(const string &dirOut, const char *fmt, const string &ext);
|
||||||
string genTimeStr(const char *fmt);
|
string genTimeStr(const char *fmt);
|
||||||
string cleanDir(const string &path);
|
string cleanDir(const string &path);
|
||||||
|
@ -77,15 +82,14 @@ string parseForParam(const string &arg, int argc, char** argv, bool argO
|
||||||
string parseForParam(const string &arg, int argc, char** argv, bool argOnly);
|
string parseForParam(const string &arg, int argc, char** argv, bool argOnly);
|
||||||
bool createDir(const string &dir);
|
bool createDir(const string &dir);
|
||||||
bool createDirTree(const string &full_path);
|
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, string *value);
|
||||||
void rdLine(const string ¶m, const string &line, int *value);
|
void rdLine(const string ¶m, const string &line, int *value);
|
||||||
void statOut(shared_t *share);
|
void cleanupEmptyDirs(const string &path);
|
||||||
void waitForDetThreads(shared_t *share);
|
void cleanupStream(const string &plsPath);
|
||||||
|
void enforceMaxEvents(shared_t *share);
|
||||||
bool rdConf(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> lsFilesInDir(const string &path, const string &ext = string());
|
||||||
vector<string> lsDirsInDir(const string &path);
|
vector<string> lsDirsInDir(const string &path);
|
||||||
|
uint64_t genEpoch();
|
||||||
|
|
||||||
#endif // COMMON_H
|
#endif // COMMON_H
|
||||||
|
|
126
src/logger.cpp
126
src/logger.cpp
|
@ -14,12 +14,17 @@
|
||||||
|
|
||||||
void recLog(const string &line, shared_t *share)
|
void recLog(const string &line, shared_t *share)
|
||||||
{
|
{
|
||||||
share->recLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "<br>" << endl;
|
share->recLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
void detLog(const string &line, shared_t *share)
|
void detLog(const string &line, shared_t *share)
|
||||||
{
|
{
|
||||||
share->detLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "<br>" << endl;
|
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";
|
||||||
}
|
}
|
||||||
|
|
||||||
void enforceMaxLogSize(const string &filePath, shared_t *share)
|
void enforceMaxLogSize(const string &filePath, shared_t *share)
|
||||||
|
@ -33,76 +38,85 @@ void enforceMaxLogSize(const string &filePath, shared_t *share)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void initLogFile(const string &filePath, ofstream &fileObj)
|
void dumpLogs(const string &fileName, const string &lines)
|
||||||
{
|
{
|
||||||
if (!fileObj.is_open())
|
if (!lines.empty())
|
||||||
{
|
{
|
||||||
if (!exists(filePath))
|
ofstream outFile;
|
||||||
|
|
||||||
|
if (exists(fileName))
|
||||||
{
|
{
|
||||||
system(string("touch " + filePath).c_str());
|
outFile.open(fileName.c_str(), ofstream::app);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outFile.open(fileName.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
fileObj.open(filePath.c_str(), ofstream::app | ofstream::out);
|
outFile << lines;
|
||||||
|
|
||||||
|
outFile.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void initLogFrontPage(const string &filePath, const string &logLinesFile)
|
void initLogFrontPage(const string &filePath, const string &logLinesFile)
|
||||||
{
|
{
|
||||||
string htmlText = "<!DOCTYPE html>\n";
|
if (!exists(filePath))
|
||||||
|
{
|
||||||
|
string htmlText = "<!DOCTYPE html>\n";
|
||||||
|
|
||||||
htmlText += "<html>\n";
|
htmlText += "<html>\n";
|
||||||
htmlText += "<script>\n";
|
htmlText += "<script>\n";
|
||||||
htmlText += "function includeHTML() {\n";
|
htmlText += "function includeHTML() {\n";
|
||||||
htmlText += " var z, i, elmnt, file, xhttp;\n";
|
htmlText += " var z, i, elmnt, file, xhttp;\n";
|
||||||
htmlText += " z = document.getElementsByTagName(\"*\");\n";
|
htmlText += " z = document.getElementsByTagName(\"*\");\n";
|
||||||
htmlText += " for (i = 0; i < z.length; i++) {\n";
|
htmlText += " for (i = 0; i < z.length; i++) {\n";
|
||||||
htmlText += " elmnt = z[i];\n";
|
htmlText += " elmnt = z[i];\n";
|
||||||
htmlText += " file = elmnt.getAttribute(\"include-html\");\n";
|
htmlText += " file = elmnt.getAttribute(\"include-html\");\n";
|
||||||
htmlText += " if (file) {\n";
|
htmlText += " if (file) {\n";
|
||||||
htmlText += " xhttp = new XMLHttpRequest();\n";
|
htmlText += " xhttp = new XMLHttpRequest();\n";
|
||||||
htmlText += " xhttp.onreadystatechange = function() {\n";
|
htmlText += " xhttp.onreadystatechange = function() {\n";
|
||||||
htmlText += " if (this.readyState == 4) {\n";
|
htmlText += " if (this.readyState == 4) {\n";
|
||||||
htmlText += " if (this.status == 200) {elmnt.innerHTML = this.responseText;}\n";
|
htmlText += " if (this.status == 200) {elmnt.innerHTML = this.responseText;}\n";
|
||||||
htmlText += " if (this.status == 404) {elmnt.innerHTML = \"Page not found.\";}\n";
|
htmlText += " if (this.status == 404) {elmnt.innerHTML = \"Page not found.\";}\n";
|
||||||
htmlText += " elmnt.removeAttribute(\"include-html\");\n";
|
htmlText += " elmnt.removeAttribute(\"include-html\");\n";
|
||||||
htmlText += " includeHTML();\n";
|
htmlText += " includeHTML();\n";
|
||||||
htmlText += " }\n";
|
htmlText += " }\n";
|
||||||
htmlText += " }\n";
|
htmlText += " }\n";
|
||||||
htmlText += " xhttp.open(\"GET\", file, true);\n";
|
htmlText += " xhttp.open(\"GET\", file, true);\n";
|
||||||
htmlText += " xhttp.send();\n";
|
htmlText += " xhttp.send();\n";
|
||||||
htmlText += " return;\n";
|
htmlText += " return;\n";
|
||||||
htmlText += " }\n";
|
htmlText += " }\n";
|
||||||
htmlText += " }\n";
|
htmlText += " }\n";
|
||||||
htmlText += "};\n";
|
htmlText += "};\n";
|
||||||
htmlText += "</script>\n";
|
htmlText += "</script>\n";
|
||||||
htmlText += "<head>\n";
|
htmlText += "<head>\n";
|
||||||
htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\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=\"Pragma\" content=\"no-cache\" />\n";
|
||||||
htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n";
|
htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n";
|
||||||
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
|
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
|
||||||
htmlText += "</head>\n";
|
htmlText += "</head>\n";
|
||||||
htmlText += "<body>\n";
|
htmlText += "<body>\n";
|
||||||
htmlText += "<p>\n";
|
htmlText += "<p>\n";
|
||||||
htmlText += "<div include-html='" + logLinesFile + "'></div>\n";
|
htmlText += "<div include-html='" + logLinesFile + "'></div>\n";
|
||||||
htmlText += "<script>\n";
|
htmlText += "<script>\n";
|
||||||
htmlText += "includeHTML();\n";
|
htmlText += "includeHTML();\n";
|
||||||
htmlText += "</script>\n";
|
htmlText += "</script>\n";
|
||||||
htmlText += "</p>\n";
|
htmlText += "</p>\n";
|
||||||
htmlText += "</body>\n";
|
htmlText += "</body>\n";
|
||||||
htmlText += "</html>\n";
|
htmlText += "</html>\n";
|
||||||
|
|
||||||
ofstream outFile(filePath);
|
ofstream outFile(filePath);
|
||||||
|
|
||||||
outFile << htmlText;
|
outFile << htmlText;
|
||||||
|
|
||||||
outFile.close();
|
outFile.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void initLogFrontPages(shared_t *share)
|
void initLogFrontPages(shared_t *share)
|
||||||
{
|
{
|
||||||
auto recLogFilePath = share->outDir + "/recording_log.html";
|
initLogFrontPage("logs/recording_log.html", REC_LOG_NAME);
|
||||||
auto detLogFilePath = share->outDir + "/detection_log.html";
|
initLogFrontPage("logs/detection_log.html", DET_LOG_NAME);
|
||||||
|
initLogFrontPage("logs/upkeep_log.html", UPK_LOG_NAME);
|
||||||
initLogFrontPage(recLogFilePath, path(share->recLogPath).filename().string());
|
|
||||||
initLogFrontPage(detLogFilePath, path(share->detLogPath).filename().string());
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,9 @@
|
||||||
|
|
||||||
void recLog(const string &line, shared_t *share);
|
void recLog(const string &line, shared_t *share);
|
||||||
void detLog(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 enforceMaxLogSize(const string &filePath, shared_t *share);
|
||||||
void initLogFile(const string &filePath, ofstream &fileObj);
|
|
||||||
void initLogFrontPages(shared_t *share);
|
void initLogFrontPages(shared_t *share);
|
||||||
|
|
||||||
#endif // lOGGER_H
|
#endif // lOGGER_H
|
||||||
|
|
200
src/main.cpp
Executable file → Normal file
200
src/main.cpp
Executable file → Normal file
|
@ -12,40 +12,84 @@
|
||||||
|
|
||||||
#include "mo_detect.h"
|
#include "mo_detect.h"
|
||||||
#include "logger.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);
|
while (share->retCode == 0)
|
||||||
|
|
||||||
Mat thumbNail;
|
|
||||||
|
|
||||||
if (moDetect(bufPath, thumbNail, share))
|
|
||||||
{
|
{
|
||||||
share->skipCmd = true;
|
sleep(2);
|
||||||
|
detectMoInStream("stream.m3u8", share);
|
||||||
wrOut(bufPath, thumbNail, 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 it = share->recList.begin();
|
||||||
|
auto evName = it->first;
|
||||||
|
auto event = it->second;
|
||||||
|
auto timeDiff = genEpoch() - event.createTime;
|
||||||
|
|
||||||
enforceMaxLogSize(share->recLogPath, share);
|
// wait at least 62 seconds before processing the event in
|
||||||
enforceMaxLogSize(share->detLogPath, share);
|
// queue.
|
||||||
|
if ((timeDiff > 0) && (timeDiff > 62))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
createDirTree("events");
|
||||||
|
wrOutVod(event, share);
|
||||||
|
genHTMLvod(evName);
|
||||||
|
|
||||||
initLogFile(share->recLogPath, share->recLogFile);
|
if (!exists("events/" + evName + ".jpg"))
|
||||||
initLogFile(share->detLogPath, share->detLogFile);
|
{
|
||||||
|
imwrite(string("events/" + evName + ".jpg").c_str(), event.thumbnail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (filesystem_error &ex)
|
||||||
|
{
|
||||||
|
recLog(string("err: ") + ex.what(), share);
|
||||||
|
}
|
||||||
|
|
||||||
|
share->recList.erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void upkeep(shared_t *share)
|
||||||
|
{
|
||||||
|
while (share->retCode == 0)
|
||||||
|
{
|
||||||
|
createDirTree("live");
|
||||||
|
createDirTree("events");
|
||||||
|
createDirTree("logs");
|
||||||
|
|
||||||
|
enforceMaxLogSize(string("logs/") + REC_LOG_NAME, share);
|
||||||
|
enforceMaxLogSize(string("logs/") + DET_LOG_NAME, share);
|
||||||
|
enforceMaxLogSize(string("logs/") + UPK_LOG_NAME, 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();
|
||||||
|
|
||||||
initLogFrontPages(share);
|
initLogFrontPages(share);
|
||||||
|
enforceMaxEvents(share);
|
||||||
|
|
||||||
|
genHTMLul(".", share->camName, share);
|
||||||
|
|
||||||
|
upkLog("camera specific webroot page updated: " + share->outDir + "/index.html", share);
|
||||||
|
|
||||||
if (!exists("/tmp/mow-lock"))
|
if (!exists("/tmp/mow-lock"))
|
||||||
{
|
{
|
||||||
|
@ -55,76 +99,48 @@ void recLoop(shared_t *share)
|
||||||
genHTMLul(share->webRoot, string(APP_NAME) + " " + string(APP_VER), share);
|
genHTMLul(share->webRoot, string(APP_NAME) + " " + string(APP_VER), share);
|
||||||
|
|
||||||
remove("/tmp/mow-lock");
|
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
|
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)
|
||||||
|
{
|
||||||
for (auto i = 0; i < share->numOfClips; ++i)
|
while (share->retCode == 0)
|
||||||
|
{
|
||||||
|
if (exists("live"))
|
||||||
{
|
{
|
||||||
auto bufPath = cleanDir(share->buffDir) + "/" + to_string(i) + "." + share->vidExt;
|
remove_all("live");
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForDetThreads(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";
|
||||||
|
|
||||||
if (!share->skipCmd)
|
recLog("ffmpeg_run: " + cmd, share);
|
||||||
|
|
||||||
|
auto retCode = system(cmd.c_str());
|
||||||
|
|
||||||
|
recLog("ffmpeg_retcode: " + to_string(retCode), share);
|
||||||
|
|
||||||
|
if (retCode != 0)
|
||||||
{
|
{
|
||||||
recLog("no motion detected", share);
|
recLog("err: ffmpeg returned non zero, indicating failure. please check stderr output.", 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);
|
sleep(10);
|
||||||
|
|
||||||
if (share->retCode != 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,19 +148,15 @@ int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
struct shared_t sharedRes;
|
struct shared_t sharedRes;
|
||||||
|
|
||||||
sharedRes.conf = parseForList("-c", argc, argv);
|
sharedRes.conf = parseForParam("-c", argc, argv, false);
|
||||||
|
|
||||||
if (parseForParam("-h", argc, argv, true) == "true")
|
if (parseForParam("-h", argc, argv, true) == "true")
|
||||||
{
|
{
|
||||||
cout << "Motion Watch " << APP_VER << endl << endl;
|
cout << "Motion Watch " << APP_VER << endl << endl;
|
||||||
cout << "Usage: mow <argument>" << endl << endl;
|
cout << "Usage: mow <argument>" << endl << endl;
|
||||||
cout << "-h : display usage information about this application." << 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 << "-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")
|
else if (parseForParam("-v", argc, argv, true) == "true")
|
||||||
{
|
{
|
||||||
|
@ -156,11 +168,21 @@ int main(int argc, char** argv)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
sharedRes.retCode = 0;
|
sharedRes.retCode = 0;
|
||||||
sharedRes.skipCmd = false;
|
sharedRes.procTime = 0;
|
||||||
sharedRes.init = true;
|
sharedRes.skipCmd = false;
|
||||||
|
|
||||||
recLoop(&sharedRes);
|
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();
|
||||||
|
|
||||||
return sharedRes.retCode;
|
return sharedRes.retCode;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,131 +12,155 @@
|
||||||
|
|
||||||
#include "mo_detect.h"
|
#include "mo_detect.h"
|
||||||
|
|
||||||
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share)
|
void detectMoInStream(const string &streamFile, shared_t *share)
|
||||||
{
|
{
|
||||||
auto ret = false;
|
if (share->procTime >= share->schSec)
|
||||||
|
|
||||||
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())
|
|
||||||
{
|
{
|
||||||
Mat diff;
|
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;
|
||||||
|
|
||||||
absdiff(prev, next, diff);
|
detLog("motion detected, skipping the post command.", share);
|
||||||
threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY);
|
}
|
||||||
|
|
||||||
auto diffScore = countNonZero(diff);
|
|
||||||
|
|
||||||
detLog("diff_score: " + to_string(diffScore), share);
|
|
||||||
|
|
||||||
ret = diffScore >= share->imgThresh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
detLog("img_diff() -- finished()", share);
|
ifstream fileIn(streamFile);
|
||||||
|
string tsPath;
|
||||||
|
Mat thumbnail;
|
||||||
|
|
||||||
return ret;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Mat frameFF(VideoCapture *cap, int gap)
|
bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share)
|
||||||
{
|
{
|
||||||
Mat ret;
|
Mat prevGray;
|
||||||
|
Mat nextGray;
|
||||||
|
|
||||||
if (gap == 0) gap = 1;
|
cvtColor(prev, prevGray, COLOR_BGR2GRAY);
|
||||||
|
cvtColor(next, nextGray, COLOR_BGR2GRAY);
|
||||||
|
|
||||||
for (int i = 0; i < gap; ++i)
|
Mat diff;
|
||||||
{
|
|
||||||
cap->grab();
|
|
||||||
}
|
|
||||||
|
|
||||||
cap->retrieve(ret);
|
absdiff(prevGray, nextGray, diff);
|
||||||
|
threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY);
|
||||||
|
|
||||||
if (!ret.empty())
|
score = countNonZero(diff);
|
||||||
{
|
|
||||||
cvtColor(ret, ret, COLOR_BGR2GRAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
detLog("diff_score: " + to_string(score) + " tresh: " + to_string(share->imgThresh), share);
|
||||||
}
|
|
||||||
|
|
||||||
void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share)
|
return score >= share->imgThresh;
|
||||||
{
|
|
||||||
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)
|
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
|
||||||
{
|
{
|
||||||
detLog("mo_detect() -- start()", share);
|
auto maxScore = 0;
|
||||||
detLog("buff_file: " + buffFile, share);
|
auto score = 0;
|
||||||
|
|
||||||
auto mod = false;
|
detLog("stream_clip: " + buffFile, share);
|
||||||
|
|
||||||
VideoCapture capture(buffFile.c_str(), CAP_FFMPEG);
|
VideoCapture capture(buffFile.c_str(), CAP_FFMPEG);
|
||||||
|
|
||||||
if (capture.isOpened())
|
if (capture.isOpened())
|
||||||
{
|
{
|
||||||
Mat prev;
|
Mat prev;
|
||||||
Mat next;
|
Mat next;
|
||||||
auto frameCount = capture.get(CAP_PROP_FRAME_COUNT);
|
|
||||||
auto frameGaps = frameCount / share->frameGap;
|
|
||||||
|
|
||||||
detLog("frame_count: " + to_string(frameCount), share);
|
detLog("capture open successful.", share);
|
||||||
detLog("frame_gaps: " + to_string(frameGaps), share);
|
|
||||||
|
|
||||||
for (auto i = 0; i < frameGaps; i++)
|
while (capture.grab())
|
||||||
{
|
{
|
||||||
if (prev.empty()) prev = frameFF(&capture, 1);
|
if (prev.empty())
|
||||||
else prev = next.clone();
|
|
||||||
|
|
||||||
next = frameFF(&capture, share->frameGap);
|
|
||||||
|
|
||||||
if (imgDiff(prev, next, share))
|
|
||||||
{
|
{
|
||||||
resize(next, vidThumb, Size(720, 480), INTER_LINEAR);
|
capture.retrieve(prev);
|
||||||
|
|
||||||
mod = true; break;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
capture.retrieve(next);
|
||||||
|
|
||||||
|
if (!next.empty())
|
||||||
|
{
|
||||||
|
if (imgDiff(prev, next, score, share))
|
||||||
|
{
|
||||||
|
if (score > maxScore)
|
||||||
|
{
|
||||||
|
maxScore = score;
|
||||||
|
|
||||||
|
resize(next, vidThumb, Size(720, 480), INTER_LINEAR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev.release();
|
||||||
|
next.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
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();
|
capture.release();
|
||||||
|
|
||||||
detLog("mo_detect() -- finished()", share);
|
return maxScore > 0;
|
||||||
|
}
|
||||||
return mod;
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,11 @@
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "web.h"
|
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
|
|
||||||
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share);
|
bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share);
|
||||||
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share);
|
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share);
|
||||||
void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share);
|
void detectMoInStream(const string &streamFile, shared_t *share);
|
||||||
Mat frameFF(VideoCapture *cap, int gap);
|
void wrOutVod(const pls_t &pls, shared_t *share);
|
||||||
|
|
||||||
#endif // MO_DETECT_H
|
#endif // MO_DETECT_H
|
||||||
|
|
129
src/web.cpp
129
src/web.cpp
|
@ -15,8 +15,8 @@
|
||||||
void genHTMLul(const string &outputDir, const string &title, shared_t *share)
|
void genHTMLul(const string &outputDir, const string &title, shared_t *share)
|
||||||
{
|
{
|
||||||
vector<string> logNames;
|
vector<string> logNames;
|
||||||
vector<string> regNames = lsFilesInDir(outputDir);
|
vector<string> eveNames;
|
||||||
vector<string> dirNames = lsDirsInDir(outputDir);
|
vector<string> dirNames;
|
||||||
|
|
||||||
string htmlText = "<!DOCTYPE html>\n";
|
string htmlText = "<!DOCTYPE html>\n";
|
||||||
|
|
||||||
|
@ -31,8 +31,43 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
|
||||||
htmlText += "<body>\n";
|
htmlText += "<body>\n";
|
||||||
htmlText += "<h3>" + title + "</h3>\n";
|
htmlText += "<h3>" + title + "</h3>\n";
|
||||||
|
|
||||||
if (!dirNames.empty())
|
if (exists(outputDir + "/live"))
|
||||||
{
|
{
|
||||||
|
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";
|
htmlText += "<ul>\n";
|
||||||
|
|
||||||
for (auto &&dirName : dirNames)
|
for (auto &&dirName : dirNames)
|
||||||
|
@ -43,38 +78,6 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
|
||||||
htmlText += "</ul>\n";
|
htmlText += "</ul>\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto &®Name : 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 += "</body>\n";
|
||||||
htmlText += "</html>";
|
htmlText += "</html>";
|
||||||
|
|
||||||
|
@ -85,11 +88,55 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
|
||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void genHTMLvid(const string &outputVid, shared_t *share)
|
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)
|
||||||
{
|
{
|
||||||
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";
|
string htmlText = "<!DOCTYPE html>\n";
|
||||||
|
|
||||||
htmlText += "<html>\n";
|
htmlText += "<html>\n";
|
||||||
|
@ -102,12 +149,12 @@ void genHTMLvid(const string &outputVid, shared_t *share)
|
||||||
htmlText += "</head>\n";
|
htmlText += "</head>\n";
|
||||||
htmlText += "<body>\n";
|
htmlText += "<body>\n";
|
||||||
htmlText += "<video width=100% height=100% controls autoplay>\n";
|
htmlText += "<video width=100% height=100% controls autoplay>\n";
|
||||||
htmlText += " <source src='" + vidName + "' type='video/" + share->vidExt + "'>\n";
|
htmlText += " <source src='" + name + ".mp4' type='video/mp4'>\n";
|
||||||
htmlText += "</video>\n";
|
htmlText += "</video>\n";
|
||||||
htmlText += "</body>\n";
|
htmlText += "</body>\n";
|
||||||
htmlText += "</html>";
|
htmlText += "</html>";
|
||||||
|
|
||||||
ofstream file(string(filePath + "/" + fileName + ".html").c_str());
|
ofstream file(string("events/" + name + ".html").c_str());
|
||||||
|
|
||||||
file << htmlText << endl;
|
file << htmlText << endl;
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
void genHTMLul(const string &outputDir, const string &title, shared_t *share);
|
void genHTMLul(const string &outputDir, const string &title, shared_t *share);
|
||||||
void genHTMLvid(const string &outputVid, shared_t *share);
|
void genHTMLstream(const string &name);
|
||||||
|
void genHTMLvod(const string &name);
|
||||||
void genCSS(shared_t *share);
|
void genCSS(shared_t *share);
|
||||||
|
|
||||||
#endif // WEB_H
|
#endif // WEB_H
|
||||||
|
|
Loading…
Reference in New Issue
Block a user