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)
|
||||
project( MotionWatch )
|
||||
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} )
|
||||
add_executable( mow src/main.cpp )
|
||||
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
|
||||
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.
|
||||
# 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
|
||||
# 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
|
||||
# the file already exists, new footage is appended to it.
|
||||
# from the camera. the file naming convention uses the current date for
|
||||
# playlist files which points to hidden video clips taken from the camera.
|
||||
#
|
||||
buff_dir = /tmp/ramdisk/cam_name
|
||||
# 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.
|
||||
#
|
||||
color_threshold = 8
|
||||
# the color levels in each pixel of the detection stream can range from
|
||||
# 0-255. in an ideal world the color differences in between frames should
|
||||
# be 0 if there is no motion but must cameras can't do this. the threshold
|
||||
# value here is used to filter if the pixels are truly different or if its
|
||||
# seeing color differences of small objects that are of no interest.
|
||||
# the color levels in each pixel of the camera stream can range from 0-255.
|
||||
# in an ideal world the color differences in between frames should be 0 if
|
||||
# there is no motion but must cameras can't do this. the threshold value
|
||||
# here is used to filter if the pixels are truly different.
|
||||
#
|
||||
block_threshold = 3456
|
||||
# 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
|
||||
# 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 ###
|
||||
|
|
182
src/main.cpp
182
src/main.cpp
|
@ -8,15 +8,17 @@
|
|||
#include <errno.h>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <dirent.h>
|
||||
#include <filesystem>
|
||||
|
||||
#include <opencv4/opencv2/opencv.hpp>
|
||||
#include <opencv4/opencv2/videoio.hpp>
|
||||
|
||||
using namespace cv;
|
||||
using namespace std;
|
||||
using namespace std::filesystem;
|
||||
|
||||
#define BUF_SZ 3
|
||||
#define BUF_SZ 10
|
||||
|
||||
struct shared_t
|
||||
{
|
||||
|
@ -28,7 +30,7 @@ struct shared_t
|
|||
string concatTxtTmp;
|
||||
string concatShTmp;
|
||||
string createShTmp;
|
||||
mutex thrMutex;
|
||||
string vidEtx;
|
||||
bool init;
|
||||
int tmpId;
|
||||
int colorThresh;
|
||||
|
@ -36,6 +38,7 @@ struct shared_t
|
|||
int blockThresh;
|
||||
int blockX;
|
||||
int blockY;
|
||||
int maxDays;
|
||||
int retCode;
|
||||
|
||||
} 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;
|
||||
|
||||
|
@ -108,22 +157,30 @@ string genDstFile(const string &dirOut, const string &ext)
|
|||
|
||||
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));
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
|
@ -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 pixB = imgB.at<uchar>(Point(x, y));
|
||||
|
||||
//cout << "pnts: " << pnts << endl;
|
||||
|
||||
if (pixDiff(pixA, pixB, share))
|
||||
{
|
||||
pnts += 1;
|
||||
|
||||
if (pnts >= share->blockThresh)
|
||||
{
|
||||
lock_guard<mutex> guard(share->thrMutex);
|
||||
|
||||
*mod = true; return;
|
||||
}
|
||||
}
|
||||
|
@ -272,6 +325,8 @@ bool rdConf(shared_t *share)
|
|||
share->blockX = 32;
|
||||
share->blockY = 32;
|
||||
share->blockThresh = 900;
|
||||
share->maxDays = 5;
|
||||
share->vidEtx = "mp4";
|
||||
|
||||
do
|
||||
{
|
||||
|
@ -288,17 +343,21 @@ bool rdConf(shared_t *share)
|
|||
rdLine("block_x = ", line, &share->blockX);
|
||||
rdLine("block_y = ", line, &share->blockY);
|
||||
rdLine("block_threshold = ", line, &share->blockThresh);
|
||||
rdLine("max_days = ", line, &share->maxDays);
|
||||
rdLine("vid_container = ", line, &share->vidEtx);
|
||||
}
|
||||
|
||||
} while(!line.empty());
|
||||
|
||||
if (share->init)
|
||||
{
|
||||
system(string("rm -r " + share->buffDir).c_str());
|
||||
remove_all(share->buffDir.c_str());
|
||||
|
||||
share->init = false;
|
||||
}
|
||||
|
||||
new thread(enforceMaxDays, share);
|
||||
|
||||
share->retCode = 0;
|
||||
}
|
||||
|
||||
|
@ -315,45 +374,30 @@ bool capPair(Mat &prev, Mat &next, VideoCapture &capture, shared_t *share)
|
|||
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;
|
||||
|
||||
auto scriptFile = genTmpFile(share->buffDir, ".sh", share);
|
||||
auto scriptData = string();
|
||||
|
||||
if (fileExists(dstPath))
|
||||
if (fileExists(lisOut))
|
||||
{
|
||||
auto concatFile = genTmpFile(share->buffDir, ".txt", share);
|
||||
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();
|
||||
file.open(lisOut.c_str(), ios_base::app);
|
||||
}
|
||||
else
|
||||
{
|
||||
scriptData = share->createShTmp;
|
||||
file.open(lisOut.c_str());
|
||||
}
|
||||
|
||||
replaceAll(scriptData, "%buffFile%", buffFile);
|
||||
replaceAll(scriptData, "%dstPath%", dstPath);
|
||||
replaceAll(scriptData, "%scriptFile%", scriptFile);
|
||||
file << m3uOut << endl;
|
||||
|
||||
file.open(scriptFile.c_str());
|
||||
file << scriptData;
|
||||
file.close();
|
||||
|
||||
system(string("sh " + scriptFile + " &").c_str());
|
||||
}
|
||||
|
||||
bool moDetect(const string &buffFile, shared_t *share)
|
||||
|
@ -377,19 +421,18 @@ bool moDetect(const string &buffFile, shared_t *share)
|
|||
|
||||
if (mod)
|
||||
{
|
||||
auto dstPath = genDstFile(share->outDir, ".ts");
|
||||
|
||||
wrOut(buffFile, dstPath, share);
|
||||
new thread(wrOut, buffFile, share);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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)
|
||||
{
|
||||
system(string("rm " + buffFile + " &").c_str());
|
||||
remove(buffFile.c_str());
|
||||
}
|
||||
|
||||
return mod;
|
||||
|
@ -401,21 +444,36 @@ void recLoop(shared_t *share)
|
|||
{
|
||||
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 cmd = "ffmpeg -hide_banner -loglevel error -i " + share->recordUrl + " -y -vcodec copy -t " + to_string(BUF_SZ) + " " + dstPath;
|
||||
auto bufPath = genTmpFile(share->buffDir, "." + share->vidEtx, share);
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void showHelp()
|
||||
|
@ -444,22 +502,6 @@ int main(int argc, char** argv)
|
|||
sharedRes.tmpId = 0;
|
||||
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);
|
||||
|
||||
th1.join();
|
||||
|
|
Loading…
Reference in New Issue
Block a user