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:
Maurice ONeal 2022-09-11 12:56:32 -04:00
parent 7a4555f3c3
commit 2687b938a0
3 changed files with 130 additions and 80 deletions

View File

@ -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} )

View File

@ -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 ###

View File

@ -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,20 +444,35 @@ 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());
}
system(share->postCmd.c_str());
}
}
@ -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();