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

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

View File

@ -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,20 +444,35 @@ 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());
}
} }
} }
@ -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();