v1.2 Update
Video clips recorded from the camera are no longer append, instead the clips are kept as is and then linked together in a playlist file in the output_dir. this makes it much more efficient and easier to maintain code. Also discovered that ffmpeg have a tendency to stall mid execution of recording from the rtsp stream every now and then. added a work around in the form of calling ffmpeg via the timeout command instead of directly so it will force kill ffmpeg if it goes longer than the expected BUF_SZ. Increased BUF_SZ to 10 secs. Added a clause in the recording loop that will make it write out a second clip if motion was detected.
This commit is contained in:
parent
7a4555f3c3
commit
2687b938a0
|
@ -1,7 +1,7 @@
|
||||||
cmake_minimum_required(VERSION 2.8)
|
cmake_minimum_required(VERSION 2.8)
|
||||||
project( MotionWatch )
|
project( MotionWatch )
|
||||||
find_package( OpenCV REQUIRED )
|
find_package( OpenCV REQUIRED )
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -pthread")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -pthread")
|
||||||
include_directories( ${OpenCV_INCLUDE_DIRS} )
|
include_directories( ${OpenCV_INCLUDE_DIRS} )
|
||||||
add_executable( mow src/main.cpp )
|
add_executable( mow src/main.cpp )
|
||||||
target_link_libraries( mow ${OpenCV_LIBS} )
|
target_link_libraries( mow ${OpenCV_LIBS} )
|
||||||
|
|
24
README.md
24
README.md
|
@ -22,7 +22,7 @@ The config file is a simple text file that contain parameters that dictate the
|
||||||
behavior of the application. Below is an example of a config file with all
|
behavior of the application. Below is an example of a config file with all
|
||||||
parameters supported and descriptions of each parameter.
|
parameters supported and descriptions of each parameter.
|
||||||
```
|
```
|
||||||
# Motion Watch config file v1.1
|
# Motion Watch config file v1.2
|
||||||
#
|
#
|
||||||
# note all lines in this config file that starts with a '#' are ignored.
|
# note all lines in this config file that starts with a '#' are ignored.
|
||||||
# also note to avoid using empty lines. if you're going to need an empty
|
# also note to avoid using empty lines. if you're going to need an empty
|
||||||
|
@ -34,8 +34,8 @@ recording_stream = rtsp://1.2.3.4:554/h264
|
||||||
#
|
#
|
||||||
output_dir = /path/to/footage/directory
|
output_dir = /path/to/footage/directory
|
||||||
# this is the output directory that will be used to store recorded footage
|
# this is the output directory that will be used to store recorded footage
|
||||||
# from the camera. the file naming convention uses the current date. if
|
# from the camera. the file naming convention uses the current date for
|
||||||
# the file already exists, new footage is appended to it.
|
# playlist files which points to hidden video clips taken from the camera.
|
||||||
#
|
#
|
||||||
buff_dir = /tmp/ramdisk/cam_name
|
buff_dir = /tmp/ramdisk/cam_name
|
||||||
# this application records small clips of the footage from the camera and
|
# this application records small clips of the footage from the camera and
|
||||||
|
@ -45,11 +45,10 @@ buff_dir = /tmp/ramdisk/cam_name
|
||||||
# for lots of writes.
|
# for lots of writes.
|
||||||
#
|
#
|
||||||
color_threshold = 8
|
color_threshold = 8
|
||||||
# the color levels in each pixel of the detection stream can range from
|
# the color levels in each pixel of the camera stream can range from 0-255.
|
||||||
# 0-255. in an ideal world the color differences in between frames should
|
# in an ideal world the color differences in between frames should be 0 if
|
||||||
# be 0 if there is no motion but must cameras can't do this. the threshold
|
# there is no motion but must cameras can't do this. the threshold value
|
||||||
# value here is used to filter if the pixels are truly different or if its
|
# here is used to filter if the pixels are truly different.
|
||||||
# seeing color differences of small objects that are of no interest.
|
|
||||||
#
|
#
|
||||||
block_threshold = 3456
|
block_threshold = 3456
|
||||||
# this application detects motion by loading frames from the camera and
|
# this application detects motion by loading frames from the camera and
|
||||||
|
@ -80,6 +79,15 @@ post_cmd = move_the_ptz_camera.py
|
||||||
# position of it's patrol pattern. note: the call to this command can be
|
# position of it's patrol pattern. note: the call to this command can be
|
||||||
# delayed if motion was detected.
|
# delayed if motion was detected.
|
||||||
#
|
#
|
||||||
|
max_days = 15
|
||||||
|
# this defines the maximum amount of days worth of video clips that is
|
||||||
|
# allowed to be stored in the output_dir. whenever this limit is met,
|
||||||
|
# the oldest day is deleted.
|
||||||
|
#
|
||||||
|
vid_container = mp4
|
||||||
|
# this is the video file format to use from recording footage from the
|
||||||
|
# camera. the format support depends entirely on the under laying ffmpeg
|
||||||
|
# installation.
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build Setup ###
|
### Build Setup ###
|
||||||
|
|
182
src/main.cpp
182
src/main.cpp
|
@ -8,15 +8,17 @@
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <mutex>
|
#include <dirent.h>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
#include <opencv4/opencv2/opencv.hpp>
|
#include <opencv4/opencv2/opencv.hpp>
|
||||||
#include <opencv4/opencv2/videoio.hpp>
|
#include <opencv4/opencv2/videoio.hpp>
|
||||||
|
|
||||||
using namespace cv;
|
using namespace cv;
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
using namespace std::filesystem;
|
||||||
|
|
||||||
#define BUF_SZ 3
|
#define BUF_SZ 10
|
||||||
|
|
||||||
struct shared_t
|
struct shared_t
|
||||||
{
|
{
|
||||||
|
@ -28,7 +30,7 @@ struct shared_t
|
||||||
string concatTxtTmp;
|
string concatTxtTmp;
|
||||||
string concatShTmp;
|
string concatShTmp;
|
||||||
string createShTmp;
|
string createShTmp;
|
||||||
mutex thrMutex;
|
string vidEtx;
|
||||||
bool init;
|
bool init;
|
||||||
int tmpId;
|
int tmpId;
|
||||||
int colorThresh;
|
int colorThresh;
|
||||||
|
@ -36,6 +38,7 @@ struct shared_t
|
||||||
int blockThresh;
|
int blockThresh;
|
||||||
int blockX;
|
int blockX;
|
||||||
int blockY;
|
int blockY;
|
||||||
|
int maxDays;
|
||||||
int retCode;
|
int retCode;
|
||||||
|
|
||||||
} sharedRes;
|
} sharedRes;
|
||||||
|
@ -100,7 +103,53 @@ void replaceAll(string &str, const string &from, const string &to)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string genDstFile(const string &dirOut, const string &ext)
|
vector<string> lsFilesInDir(const string &path, const string &ext)
|
||||||
|
{
|
||||||
|
DIR *dir;
|
||||||
|
struct dirent *ent;
|
||||||
|
vector<string> names;
|
||||||
|
|
||||||
|
if ((dir = opendir(path.c_str())) != NULL)
|
||||||
|
{
|
||||||
|
while ((ent = readdir(dir)) != NULL)
|
||||||
|
{
|
||||||
|
auto name = string(ent->d_name);
|
||||||
|
|
||||||
|
if ((name.size() >= 4) && (ent->d_type & DT_REG))
|
||||||
|
{
|
||||||
|
if (name.substr(name.size() - 4) == ext)
|
||||||
|
{
|
||||||
|
names.push_back(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
sort(names.begin(), names.end());
|
||||||
|
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
void enforceMaxDays(shared_t *share)
|
||||||
|
{
|
||||||
|
auto names = lsFilesInDir(share->outDir, ".m3u");
|
||||||
|
|
||||||
|
while (names.size() > share->maxDays)
|
||||||
|
{
|
||||||
|
auto name = names[0];
|
||||||
|
auto plsFile = cleanDir(share->outDir) + "/" + name;
|
||||||
|
auto vidFold = cleanDir(share->outDir) + "/." + name.substr(0, name.size() - 4);
|
||||||
|
|
||||||
|
remove(plsFile.c_str());
|
||||||
|
remove_all(vidFold.c_str());
|
||||||
|
|
||||||
|
names.erase(names.begin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string genTimeStr(const char *fmt)
|
||||||
{
|
{
|
||||||
time_t rawtime;
|
time_t rawtime;
|
||||||
|
|
||||||
|
@ -108,22 +157,30 @@ string genDstFile(const string &dirOut, const string &ext)
|
||||||
|
|
||||||
auto timeinfo = localtime(&rawtime);
|
auto timeinfo = localtime(&rawtime);
|
||||||
|
|
||||||
char dateC[20];
|
char ret[50];
|
||||||
|
|
||||||
strftime(dateC, 20, "%Y-%m-%d", timeinfo);
|
strftime(ret, 50, fmt, timeinfo);
|
||||||
|
|
||||||
|
return string(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
string genDstFile(const string &dirOut, const char *fmt, const string &ext)
|
||||||
|
{
|
||||||
createDirTree(cleanDir(dirOut));
|
createDirTree(cleanDir(dirOut));
|
||||||
|
|
||||||
return cleanDir(dirOut) + string("/") + string(dateC) + ext;
|
return cleanDir(dirOut) + string("/") + genTimeStr(fmt) + ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
string genTmpFile(const string &dirOut, const string &ext, shared_t *share)
|
string genTmpFile(const string &dirOut, const string &ext, shared_t *share)
|
||||||
{
|
{
|
||||||
createDirTree(cleanDir(dirOut));
|
createDirTree(cleanDir(dirOut));
|
||||||
|
|
||||||
share->tmpId += 1;
|
if (share->tmpId == 9999999)
|
||||||
|
{
|
||||||
|
share->tmpId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
return cleanDir(dirOut) + string("/") + to_string(share->tmpId) + ext;
|
return cleanDir(dirOut) + string("/") + to_string(share->tmpId++) + ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
Mat toGray(const Mat &src)
|
Mat toGray(const Mat &src)
|
||||||
|
@ -161,16 +218,12 @@ void secDiff(Mat imgA, Mat imgB, int rows, int cols, int rowOffs, int colOffs, b
|
||||||
auto pixA = imgA.at<uchar>(Point(x, y));
|
auto pixA = imgA.at<uchar>(Point(x, y));
|
||||||
auto pixB = imgB.at<uchar>(Point(x, y));
|
auto pixB = imgB.at<uchar>(Point(x, y));
|
||||||
|
|
||||||
//cout << "pnts: " << pnts << endl;
|
|
||||||
|
|
||||||
if (pixDiff(pixA, pixB, share))
|
if (pixDiff(pixA, pixB, share))
|
||||||
{
|
{
|
||||||
pnts += 1;
|
pnts += 1;
|
||||||
|
|
||||||
if (pnts >= share->blockThresh)
|
if (pnts >= share->blockThresh)
|
||||||
{
|
{
|
||||||
lock_guard<mutex> guard(share->thrMutex);
|
|
||||||
|
|
||||||
*mod = true; return;
|
*mod = true; return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -272,6 +325,8 @@ bool rdConf(shared_t *share)
|
||||||
share->blockX = 32;
|
share->blockX = 32;
|
||||||
share->blockY = 32;
|
share->blockY = 32;
|
||||||
share->blockThresh = 900;
|
share->blockThresh = 900;
|
||||||
|
share->maxDays = 5;
|
||||||
|
share->vidEtx = "mp4";
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
|
@ -288,17 +343,21 @@ bool rdConf(shared_t *share)
|
||||||
rdLine("block_x = ", line, &share->blockX);
|
rdLine("block_x = ", line, &share->blockX);
|
||||||
rdLine("block_y = ", line, &share->blockY);
|
rdLine("block_y = ", line, &share->blockY);
|
||||||
rdLine("block_threshold = ", line, &share->blockThresh);
|
rdLine("block_threshold = ", line, &share->blockThresh);
|
||||||
|
rdLine("max_days = ", line, &share->maxDays);
|
||||||
|
rdLine("vid_container = ", line, &share->vidEtx);
|
||||||
}
|
}
|
||||||
|
|
||||||
} while(!line.empty());
|
} while(!line.empty());
|
||||||
|
|
||||||
if (share->init)
|
if (share->init)
|
||||||
{
|
{
|
||||||
system(string("rm -r " + share->buffDir).c_str());
|
remove_all(share->buffDir.c_str());
|
||||||
|
|
||||||
share->init = false;
|
share->init = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
new thread(enforceMaxDays, share);
|
||||||
|
|
||||||
share->retCode = 0;
|
share->retCode = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,45 +374,30 @@ bool capPair(Mat &prev, Mat &next, VideoCapture &capture, shared_t *share)
|
||||||
return !prev.empty() && !next.empty();
|
return !prev.empty() && !next.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void wrOut(const string &buffFile, const string &dstPath, shared_t *share)
|
void wrOut(const string &buffFile, shared_t *share)
|
||||||
{
|
{
|
||||||
|
auto clnDir = cleanDir(share->outDir);
|
||||||
|
auto vidOut = genDstFile(clnDir + "/." + genTimeStr("%Y-%m-%d"), "%H%M%S", "." + share->vidEtx);
|
||||||
|
auto m3uOut = vidOut.substr(clnDir.size() + 1);
|
||||||
|
auto lisOut = genDstFile(share->outDir, "%Y-%m-%d", ".m3u");
|
||||||
|
|
||||||
|
copy_file(buffFile.c_str(), vidOut.c_str());
|
||||||
|
remove(buffFile.c_str());
|
||||||
|
|
||||||
ofstream file;
|
ofstream file;
|
||||||
|
|
||||||
auto scriptFile = genTmpFile(share->buffDir, ".sh", share);
|
if (fileExists(lisOut))
|
||||||
auto scriptData = string();
|
|
||||||
|
|
||||||
if (fileExists(dstPath))
|
|
||||||
{
|
{
|
||||||
auto concatFile = genTmpFile(share->buffDir, ".txt", share);
|
file.open(lisOut.c_str(), ios_base::app);
|
||||||
auto existsFile = genTmpFile(share->outDir, ".ts", share);
|
|
||||||
auto concatData = share->concatTxtTmp;
|
|
||||||
|
|
||||||
scriptData = share->concatShTmp;
|
|
||||||
|
|
||||||
replaceAll(concatData, "%existsFile%", existsFile);
|
|
||||||
replaceAll(concatData, "%buffFile%", buffFile);
|
|
||||||
|
|
||||||
replaceAll(scriptData, "%existsFile%", existsFile);
|
|
||||||
replaceAll(scriptData, "%concatFile%", concatFile);
|
|
||||||
|
|
||||||
file.open(concatFile.c_str());
|
|
||||||
file << concatData;
|
|
||||||
file.close();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
scriptData = share->createShTmp;
|
file.open(lisOut.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceAll(scriptData, "%buffFile%", buffFile);
|
file << m3uOut << endl;
|
||||||
replaceAll(scriptData, "%dstPath%", dstPath);
|
|
||||||
replaceAll(scriptData, "%scriptFile%", scriptFile);
|
|
||||||
|
|
||||||
file.open(scriptFile.c_str());
|
|
||||||
file << scriptData;
|
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
system(string("sh " + scriptFile + " &").c_str());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool moDetect(const string &buffFile, shared_t *share)
|
bool moDetect(const string &buffFile, shared_t *share)
|
||||||
|
@ -377,19 +421,18 @@ bool moDetect(const string &buffFile, shared_t *share)
|
||||||
|
|
||||||
if (mod)
|
if (mod)
|
||||||
{
|
{
|
||||||
auto dstPath = genDstFile(share->outDir, ".ts");
|
new thread(wrOut, buffFile, share);
|
||||||
|
|
||||||
wrOut(buffFile, dstPath, share);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
cerr << "err: Could not open buff file: " << buffFile << " for reading. check formatting/permissions." << endl;
|
cerr << "err: Could not open buff file: " << buffFile << " for reading. check formatting/permissions." << endl;
|
||||||
|
cerr << " Also check if opencv was compiled with FFMPEG encoding enabled." << endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mod)
|
if (!mod)
|
||||||
{
|
{
|
||||||
system(string("rm " + buffFile + " &").c_str());
|
remove(buffFile.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
return mod;
|
return mod;
|
||||||
|
@ -401,21 +444,36 @@ void recLoop(shared_t *share)
|
||||||
{
|
{
|
||||||
auto mod = false;
|
auto mod = false;
|
||||||
|
|
||||||
for (auto i = 0; i < share->secs; i += BUF_SZ)
|
for (auto ind = 0; ind < share->secs; ind += BUF_SZ)
|
||||||
{
|
{
|
||||||
auto dstPath = genTmpFile(share->buffDir, ".ts", share);
|
auto bufPath = genTmpFile(share->buffDir, "." + share->vidEtx, share);
|
||||||
auto cmd = "ffmpeg -hide_banner -loglevel error -i " + share->recordUrl + " -y -vcodec copy -t " + to_string(BUF_SZ) + " " + dstPath;
|
auto secs = to_string(BUF_SZ);
|
||||||
|
auto limSecs = to_string(BUF_SZ + 3);
|
||||||
|
auto cmd = "timeout -k 1 " + limSecs + " ffmpeg -hide_banner -i " + share->recordUrl + " -y -vcodec copy -t " + secs + " " + bufPath;
|
||||||
|
|
||||||
system(cmd.c_str());
|
if (system(cmd.c_str()) == 0)
|
||||||
|
{
|
||||||
|
if (mod)
|
||||||
|
{
|
||||||
|
new thread(wrOut, bufPath, share);
|
||||||
|
|
||||||
mod = moDetect(dstPath, share);
|
mod = false;
|
||||||
|
}
|
||||||
|
else if (moDetect(bufPath, share))
|
||||||
|
{
|
||||||
|
mod = true;
|
||||||
|
ind = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (fileExists(bufPath))
|
||||||
|
{
|
||||||
|
remove(bufPath.c_str());
|
||||||
|
sleep(BUF_SZ);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mod)
|
|
||||||
{
|
|
||||||
system(share->postCmd.c_str());
|
system(share->postCmd.c_str());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void showHelp()
|
void showHelp()
|
||||||
|
@ -444,22 +502,6 @@ int main(int argc, char** argv)
|
||||||
sharedRes.tmpId = 0;
|
sharedRes.tmpId = 0;
|
||||||
sharedRes.init = true;
|
sharedRes.init = true;
|
||||||
|
|
||||||
sharedRes.concatTxtTmp += "file '%existsFile%'\n";
|
|
||||||
sharedRes.concatTxtTmp += "file '%buffFile%'\n";
|
|
||||||
|
|
||||||
sharedRes.concatShTmp += "#!/bin/sh\n";
|
|
||||||
sharedRes.concatShTmp += "cp '%dstPath%' '%existsFile%'\n";
|
|
||||||
sharedRes.concatShTmp += "ffmpeg -hide_banner -loglevel error -y -f concat -safe 0 -i '%concatFile%' -c copy '%dstPath%'\n";
|
|
||||||
sharedRes.concatShTmp += "rm '%concatFile%'\n";
|
|
||||||
sharedRes.concatShTmp += "rm '%existsFile%'\n";
|
|
||||||
sharedRes.concatShTmp += "rm '%buffFile%'\n";
|
|
||||||
sharedRes.concatShTmp += "rm '%scriptFile%'\n";
|
|
||||||
|
|
||||||
sharedRes.createShTmp += "#!/bin/sh\n";
|
|
||||||
sharedRes.createShTmp += "cp '%buffFile%' '%dstPath%'\n";
|
|
||||||
sharedRes.createShTmp += "rm '%buffFile%'\n";
|
|
||||||
sharedRes.createShTmp += "rm '%scriptFile%'\n";
|
|
||||||
|
|
||||||
thread th1(recLoop, &sharedRes);
|
thread th1(recLoop, &sharedRes);
|
||||||
|
|
||||||
th1.join();
|
th1.join();
|
||||||
|
|
Loading…
Reference in New Issue
Block a user