Compare commits

..

No commits in common. "ff5f95f44562b2530c32c9049851f6dac9a6bda5" and "f4ea944f97ccbb6df75256785c66745c637972ec" have entirely different histories.

17 changed files with 859 additions and 1063 deletions

2
.gitignore vendored
View File

@ -57,5 +57,3 @@ compile_commands.json
# Build folders # Build folders
/.build-mow /.build-mow
/.build-opencv
/src/opencv

View File

@ -1,31 +1,7 @@
cmake_minimum_required(VERSION 3.14) cmake_minimum_required(VERSION 2.8.12)
project( MotionWatch )
project(MotionWatch LANGUAGES CXX) find_package( OpenCV REQUIRED )
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pthread")
set(CMAKE_INCLUDE_CURRENT_DIR ON) include_directories( ${OpenCV_INCLUDE_DIRS} )
add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp src/web.cpp src/logger.cpp )
set(CMAKE_AUTOUIC ON) target_link_libraries( mow ${OpenCV_LIBS} )
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_BUILD_TYPE Debug)
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED)
add_executable(mow
src/main.cpp
src/common.h
src/common.cpp
src/web.h
src/web.cpp
src/logger.h
src/logger.cpp
src/camera.h
src/camera.cpp
)
target_link_libraries(mow Qt${QT_VERSION_MAJOR}::Core ${OpenCV_LIBS})

View File

@ -4,6 +4,8 @@ Motion Watch is a video surveillance application that monitors the video feeds
of an IP camera and records only footage that contains motion. The main of an IP camera and records only footage that contains motion. The main
advantage of this is reduced storage requirements as opposed to continuous advantage of this is reduced storage requirements as opposed to continuous
recording because only video footage of interest is recorded to storage. recording because only video footage of interest is recorded to storage.
The entire app is designed to operate on just one camera but multiple instances
of this app can be used to operate multiple cameras.
### Usage ### ### Usage ###
@ -11,11 +13,13 @@ recording because only video footage of interest is recorded to storage.
Usage: mow <argument> Usage: mow <argument>
-h : display usage information about this application. -h : display usage information about this application.
-c : path to the config file used to run a single camera instance. -c : path to the config file(s).
-d : path to a directory that can contain multiple config files.
each file found in the directory will be used to run a
camera instance.
-v : display the current version. -v : display the current version.
note: multiple -c config files can be passed, reading from left
to right. any conflicting values between the files will
have the latest value from the latest file overwrite the
the earliest.
``` ```
### Config File ### ### Config File ###
@ -45,20 +49,30 @@ cam_name = cam-1
# name will also be used to as the base directory in web_root. if not # name will also be used to as the base directory in web_root. if not
# defined, the name of the config file will be used. # defined, the name of the config file will be used.
# #
max_event_secs = 30 pix_thresh = 150
# this is the maximum amount of secs of video footage that can be # this value tells the application how far different the pixels need to be
# recorded in a motion event. # before the pixels are actually considered different. think of this as
# pixel diff sensitivity, the higher the value the lesser the sensitivity.
# maximum is 255.
# #
img_thresh = 8000 img_thresh = 80000
# this application uses 'magick compare' to score the differences between # this indicates how many pixels need to be different in between frames
# two, one second gapped snapshots of the camera stream. any image pairs # before it is considered motion. any video clips found with frames
# that score greater than this value is considered motion and queues up # exceeding this value will be copied from live footage to event footage.
# max_event_secs worth of hls clips to be written out as a motion event.
# #
max_events = 100 frame_gap = 10
# this is the amount of frames in between the comparison frames to check
# for pixel differences. the higher the value, the lower the cpu over
# head, however it does lower motion detection accuracy.
#
max_events = 40
# this indicates the maximum amount of motion event video clips to keep # this indicates the maximum amount of motion event video clips to keep
# before deleting the oldest clip. # before deleting the oldest clip.
# #
max_event_secs = 10
# this is the maximum amount of secs of video footage that can be
# recorded in a motion event.
#
post_secs = 60 post_secs = 60
# this is the amount of seconds to wait before running the command # this is the amount of seconds to wait before running the command
# defined in post_cmd. the command will not run if motion was detected # defined in post_cmd. the command will not run if motion was detected
@ -91,7 +105,8 @@ web_font = courier
### Setup/Build/Install ### ### Setup/Build/Install ###
This application is currently only compatible with a Linux based operating This application is currently only compatible with a Linux based operating
systems that are capable of installing the QT API. systems that are capable of installing opencv. The following 3 scripts make
building and then installing convenient.
``` ```
sh ./setup.sh <--- only need to run this once if compiling for the first sh ./setup.sh <--- only need to run this once if compiling for the first

Binary file not shown.

View File

@ -1,5 +1,4 @@
#!/bin/sh #!/bin/sh
apt install apache2
if [ ! -d "/opt/mow" ]; then if [ ! -d "/opt/mow" ]; then
mkdir /opt/mow mkdir /opt/mow
fi fi

View File

@ -1,7 +1,29 @@
#!/bin/sh #!/bin/sh
export DEBIAN_FRONTEND=noninteractive
apt update -y apt update -y
apt install -y pkg-config cmake make g++ apt install -y pkg-config
apt install -y ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev x264 libx264-dev libilmbase-dev qt6-base-dev qtchooser qmake6 qt6-base-dev-tools libxkbcommon-dev libfuse-dev apt install -y cmake
cp ./bin/magick /usr/bin/magick apt install -y make
chmod +x /usr/bin/magick apt install -y g++
apt install -y wget
apt install -y unzip
apt install -y git
apt install -y ffmpeg
apt install -y gstreamer1.0*
apt install -y libavcodec-dev
apt install -y libavformat-dev
apt install -y libavutil-dev
apt install -y libswscale-dev
apt install -y libgstreamer1.0-dev
apt install -y x264
apt install -y libx264-dev
apt install -y libilmbase-dev
apt install -y libopencv-dev
apt install -y apache2
add-apt-repository -y ppa:ubuntu-toolchain-r/test
apt update -y
apt install -y gcc-10
apt install -y gcc-10-base
apt install -y gcc-10-doc
apt install -y g++-10
apt install -y libstdc++-10-dev
apt install -y libstdc++-10-doc

View File

@ -1,485 +0,0 @@
// This file is part of Motion Watch.
// Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "camera.h"
Camera::Camera(QObject *parent) : QObject(parent)
{
shared.recordUrl.clear();
shared.postCmd.clear();
shared.camName.clear();
shared.retCode = 0;
shared.imgThresh = 8000;
shared.maxEvents = 100;
shared.maxLogSize = 100000;
shared.skipCmd = false;
shared.postSecs = 60;
shared.evMaxSecs = 30;
shared.webRoot = "/var/www/html";
shared.webBg = "#485564";
shared.webTxt = "#dee5ee";
shared.webFont = "courier";
}
int Camera::start(const QStringList &args)
{
shared.conf = getParam("-c", args);
if (rdConf(&shared))
{
QDir("live").removeRecursively();
QDir("img").removeRecursively();
QDir().mkdir("live");
QDir().mkdir("events");
QDir().mkdir("logs");
QDir().mkdir("img");
auto thr1 = new QThread(nullptr);
auto thr2 = new QThread(nullptr);
auto thr3 = new QThread(nullptr);
auto thr4 = new QThread(nullptr);
new RecLoop(&shared, thr1, nullptr);
new Upkeep(&shared, thr2, nullptr);
new EventLoop(&shared, thr3, nullptr);
new DetectLoop(&shared, thr4, nullptr);
thr1->start();
thr2->start();
thr3->start();
thr4->start();
}
return shared.retCode;
}
Loop::Loop(shared_t *sharedRes, QThread *thr, QObject *parent) : QObject(parent)
{
shared = sharedRes;
heartBeat = 10;
loopTimer = 0;
connect(thr, &QThread::started, this, &Loop::init);
moveToThread(thr);
}
void Loop::init()
{
loopTimer = new QTimer(this);
connect(loopTimer, &QTimer::timeout, this, &Loop::loopSlot);
loopTimer->setSingleShot(false);
loopTimer->start(heartBeat * 1000);
loopSlot();
}
void Loop::loopSlot()
{
if (!exec())
{
loopTimer->stop(); QCoreApplication::exit(shared->retCode);
}
}
bool Loop::exec()
{
if (loopTimer->interval() != heartBeat * 1000)
{
loopTimer->start(heartBeat * 1000);
}
return shared->retCode == 0;
}
RecLoop::RecLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
{
recProc = 0;
imgProc = 0;
}
void RecLoop::init()
{
recProc = new QProcess(this);
imgProc = new QProcess(this);
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &RecLoop::term);
connect(recProc, &QProcess::readyReadStandardError, this, &RecLoop::rdProcErr);
connect(imgProc, &QProcess::readyReadStandardError, this, &RecLoop::rdProcErr);
Loop::init();
}
void RecLoop::updateCmd()
{
QStringList recArgs;
QStringList imgArgs;
recArgs << "-hide_banner";
recArgs << "-i" << shared->recordUrl;
recArgs << "-strftime" << "1";
recArgs << "-strftime_mkdir" << "1";
recArgs << "-hls_segment_filename" << "live/" + QString(STRFTIME_FMT) + ".ts";
recArgs << "-y";
recArgs << "-vcodec" << "copy";
recArgs << "-f" << "hls";
recArgs << "-hls_time" << "2";
recArgs << "-hls_list_size" << "1000";
recArgs << "-hls_flags" << "append_list+omit_endlist";
recArgs << "-rtsp_transport" << "tcp";
recArgs << "-stimeout" << "3000";
recArgs << "-t" << QString::number(heartBeat);
recArgs << "stream.m3u8";
imgArgs << "-hide_banner";
imgArgs << "-i" << shared->recordUrl;
imgArgs << "-strftime" << "1";
imgArgs << "-strftime_mkdir" << "1";
imgArgs << "-vf" << "fps=1,scale=320:240";
imgArgs << "-rtsp_transport" << "tcp";
imgArgs << "-stimeout" << "3000";
imgArgs << "-t" << QString::number(heartBeat);
imgArgs << "img/" + QString(STRFTIME_FMT) + ".bmp";
recProc->setProgram("ffmpeg");
recProc->setArguments(recArgs);
imgProc->setProgram("ffmpeg");
imgProc->setArguments(imgArgs);
recLog("rec_args: " + recArgs.join(" "), shared);
recLog("img_args: " + imgArgs.join(" "), shared);
}
void RecLoop::rdProcErr()
{
procError("img", imgProc);
procError("rec", recProc);
}
void RecLoop::term()
{
recProc->kill();
recProc->waitForFinished();
imgProc->kill();
imgProc->waitForFinished();
}
void RecLoop::procError(const QString &desc, QProcess *proc)
{
if (proc->isOpen() && (proc->state() != QProcess::Running))
{
auto errBlob = QString(proc->readAllStandardError());
auto errLines = errBlob.split('\n');
if (!errLines.isEmpty())
{
for (auto &&line : errLines)
{
recLog(desc + "_cmd_stderr: " + line, shared);
}
}
}
}
bool RecLoop::exec()
{
if ((imgProc->state() == QProcess::Running) || (recProc->state() == QProcess::Running))
{
term();
}
updateCmd();
imgProc->start();
recProc->start();
return Loop::exec();
}
Upkeep::Upkeep(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) {}
bool Upkeep::exec()
{
QDir().mkdir("live");
QDir().mkdir("events");
QDir().mkdir("logs");
QDir().mkdir("img");
enforceMaxLogSize(QString("logs/") + REC_LOG_NAME, shared);
enforceMaxLogSize(QString("logs/") + DET_LOG_NAME, shared);
dumpLogs(QString("logs/") + REC_LOG_NAME, shared->recLog);
dumpLogs(QString("logs/") + DET_LOG_NAME, shared->detLog);
shared->logMutex.lock();
shared->recLog.clear();
shared->detLog.clear();
shared->logMutex.unlock();
initLogFrontPages();
enforceMaxEvents(shared);
enforceMaxImages();
enforceMaxVids();
genHTMLul(".", shared->camName, shared);
genCSS(shared);
genHTMLul(shared->webRoot, QString(APP_NAME) + " " + QString(APP_VER), shared);
return Loop::exec();
}
EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
{
heartBeat = 2;
highScore = 0;
cycles = 0;
}
bool EventLoop::exec()
{
if (cycles * 2 >= shared->evMaxSecs)
{
vidList.removeDuplicates();
if (vidList.size() > 1)
{
recLog("attempting write out of event: " + name, shared);
if (wrOutVod())
{
genHTMLvod(name);
QProcess proc;
QStringList args;
args << "convert";
args << imgPath;
args << "events/" + name + ".jpg";
proc.start("magick", args);
proc.waitForFinished();
}
}
cycles = 0;
highScore = 0;
vidList.clear();
}
else
{
cycles += 1;
shared->recMutex.lock();
for (auto &&event : shared->recList)
{
auto maxFiles = shared->evMaxSecs / 2;
// there's 2 secs in each hls segment
if (highScore < event.score)
{
name = event.timeStamp.toString(DATETIME_FMT);
imgPath = event.imgPath;
highScore = event.score;
}
vidList.append(backwardFacingFiles("live", ".ts", event.timeStamp, maxFiles / 2));
vidList.append(forwardFacingFiles("live", ".ts", event.timeStamp, maxFiles / 2));
}
shared->recList.clear();
shared->recMutex.unlock();
}
return Loop::exec();
}
bool EventLoop::wrOutVod()
{
auto cnt = 0;
auto concat = name + ".tmp";
auto ret = false;
QFile file(concat);
file.open(QFile::WriteOnly);
for (auto &&vid : vidList)
{
recLog("event_src: " + vid, shared);
if (QFile::exists(vid))
{
file.write(QString("file '" + vid + "'\n").toUtf8()); cnt++;
}
}
file.close();
if (cnt == 0)
{
recLog("err: none of the event hls clips exists, canceling write out.", shared);
QFile::remove(concat);
}
else
{
QProcess proc;
QStringList args;
args << "-f";
args << "concat";
args << "-safe" << "0";
args << "-i" << concat;
args << "-c" << "copy";
args << "events/" + name + ".mp4";
proc.setProgram("ffmpeg");
proc.setArguments(args);
proc.start();
if (proc.waitForStarted())
{
recLog("concat_cmd_start: ok", shared);
proc.waitForFinished(); ret = true;
}
else
{
recLog("concat_cmd_start: fail", shared);
recLog("concat_cmd_stderr: " + QString(proc.readAllStandardError()), shared);
}
QFile::remove(concat);
}
return ret;
}
DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
{
pcTimer = 0;
heartBeat = 2;
delayCycles = 8; // this will be used to delay the
// actual start of DetectLoop by
// 16secs.
}
void DetectLoop::init()
{
pcTimer = new QTimer(this);
mod = false;
connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak);
resetTimers();
Loop::init();
}
void DetectLoop::resetTimers()
{
pcTimer->start(shared->postSecs * 1000);
}
void DetectLoop::pcBreak()
{
if (!shared->postCmd.isEmpty())
{
detLog("---POST_BREAK---", shared);
if (mod)
{
detLog("motion detected, skipping the post command.", shared);
}
else
{
if (delayCycles == 0) delayCycles = 5;
else delayCycles += 5;
detLog("no motion detected, running post command: " + shared->postCmd, shared);
system(shared->postCmd.toUtf8().data());
}
}
mod = false;
}
bool DetectLoop::exec()
{
if (delayCycles > 0)
{
delayCycles -= 1;
detLog("spec: detection cycle skipped. cycles left to be skipped: " + QString::number(delayCycles), shared);
}
else
{
auto curDT = QDateTime::currentDateTime();
auto images = backwardFacingFiles("img", ".bmp", curDT, 6);
if (images.size() < 2)
{
detLog("wrn: didn't pick up enough image files from the image stream. number of files: " + QString::number(images.size()), shared);
detLog(" will try again on the next loop.", shared);
}
else
{
QProcess extComp;
QStringList args;
auto pos = images.size() - 1;
args << "compare";
args << "-metric" << "FUZZ";
args << images[pos - 1];
args << images[pos];
args << "/dev/null";
extComp.start("magick", args);
extComp.waitForFinished();
QString output = extComp.readAllStandardError();
output = output.left(output.indexOf(' '));
detLog(extComp.program() + " " + args.join(" ") + " --result: " + output, shared);
auto score = output.toFloat();
if (score >= shared->imgThresh)
{
detLog("--threshold_breached: " + QString::number(shared->imgThresh), shared);
evt_t event;
event.timeStamp = curDT;
event.score = score;
event.imgPath = images[pos];
shared->recMutex.lock();
shared->recList.append(event); mod = true;
shared->recMutex.unlock();
}
}
}
return Loop::exec();
}

View File

@ -1,142 +0,0 @@
#ifndef CAMERA_H
#define CAMERA_H
// This file is part of Motion Watch.
// Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "common.h"
#include "logger.h"
#include "web.h"
class Camera : public QObject
{
Q_OBJECT
private:
shared_t shared;
public:
explicit Camera(QObject *parent = nullptr);
int start(const QStringList &args);
};
class Loop : public QObject
{
Q_OBJECT
protected:
shared_t *shared;
QTimer *loopTimer;
int heartBeat;
protected slots:
virtual void init();
private slots:
void loopSlot();
public:
explicit Loop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
virtual bool exec();
};
class RecLoop : public Loop
{
Q_OBJECT
private:
QProcess *recProc;
QProcess *imgProc;
QString curUrl;
void updateCmd();
void procError(const QString &desc, QProcess *proc);
private slots:
void init();
void term();
void rdProcErr();
public:
explicit RecLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
bool exec();
};
class Upkeep : public Loop
{
Q_OBJECT
public:
explicit Upkeep(shared_t *shared, QThread *thr, QObject *parent = nullptr);
bool exec();
};
class EventLoop : public Loop
{
Q_OBJECT
private:
QStringList vidList;
QString imgPath;
QString name;
float highScore;
uint cycles;
bool wrOutVod();
public:
explicit EventLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
bool exec();
};
class DetectLoop : public Loop
{
Q_OBJECT
private:
QTimer *pcTimer;
uint delayCycles;
bool mod;
void resetTimers();
private slots:
void init();
void pcBreak();
public:
explicit DetectLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
bool exec();
};
#endif // CAMERA_H

View File

@ -12,97 +12,124 @@
#include "common.h" #include "common.h"
QString getParam(const QString &key, const QStringList &args) string cleanDir(const string &path)
{ {
// this can be used by command objects to pick out parameters if (path[path.size() - 1] == '/')
// from a command line that are pointed by a name identifier
// example: -i /etc/some_file, this function should pick out
// "/etc/some_file" from args if "-i" is passed into key.
QString ret;
int pos = args.indexOf(QRegularExpression(key, QRegularExpression::CaseInsensitiveOption));
if (pos != -1)
{ {
// key found. return path.substr(0, path.size() - 1);
}
if ((pos + 1) <= (args.size() - 1)) else
{ {
// check ahead to make sure pos + 1 will not go out return path;
// of range.
if (!args[pos + 1].startsWith("-"))
{
// the "-" used throughout this application
// indicates an argument so the above 'if'
// statement will check to make sure it does
// not return another argument as a parameter
// in case a back-to-back "-arg -arg" is
// present.
ret = args[pos + 1];
} }
} }
bool createDir(const string &dir)
{
auto ret = mkdir(dir.c_str(), 0777);
if (ret == -1)
{
return errno == EEXIST;
}
else
{
return true;
}
}
bool createDirTree(const string &full_path)
{
size_t pos = 0;
auto ret = true;
while (ret == true && pos != string::npos)
{
pos = full_path.find('/', pos + 1);
ret = createDir(full_path.substr(0, pos));
} }
return ret; return ret;
} }
QStringList lsFilesInDir(const QString &path, const QString &ext) void cleanupEmptyDirs(const string &path)
{ {
QStringList filters; if (exists(path))
{
filters << "*" + ext; for (auto &entry : directory_iterator(path))
{
QDir dirObj(path); if (entry.is_directory())
{
dirObj.setFilter(QDir::Files); try
dirObj.setNameFilters(filters); {
dirObj.setSorting(QDir::Name); remove(entry.path());
return dirObj.entryList();
} }
catch (filesystem_error const &ex)
QStringList lsDirsInDir(const QString &path)
{ {
QDir dirObj(path); // non-empty dir assumed when filesystem_error is raised.
cleanupEmptyDirs(path + "/" + entry.path().filename().string());
dirObj.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); }
dirObj.setSorting(QDir::Name); }
return dirObj.entryList();
} }
QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir)
{
QStringList ret;
for (auto i = 0; i < secs; ++i)
{
QString filePath;
if (dir == '-') filePath = path + "/" + stamp.addSecs(-i).toString(DATETIME_FMT) + ext;
if (dir == '+') filePath = path + "/" + stamp.addSecs(i).toString(DATETIME_FMT) + ext;
if (QFile::exists(filePath))
{
if (dir == '-') ret.insert(0, filePath);
if (dir == '+') ret.append(filePath);
} }
} }
return ret; vector<string> lsFilesInDir(const string &path, const string &ext)
{
vector<string> names;
if (exists(path))
{
for (auto &entry : directory_iterator(path))
{
if (entry.is_regular_file())
{
auto name = entry.path().filename().string();
if (ext.empty() || name.ends_with(ext))
{
names.push_back(name);
}
}
}
} }
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs) sort(names.begin(), names.end());
{
return listFacingFiles(path, ext, stamp, secs, '-'); return names;
} }
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs) vector<string> lsDirsInDir(const string &path)
{ {
return listFacingFiles(path, ext, stamp, secs, '+'); vector<string> names;
if (exists(path))
{
for (auto &entry : directory_iterator(path))
{
if (entry.is_directory())
{
names.push_back(entry.path().filename().string());
}
}
}
sort(names.begin(), names.end());
return names;
}
void cleanupStream(const string &plsPath)
{
ifstream fileIn(plsPath);
for (string line; getline(fileIn, line); )
{
if (line.starts_with("VIDEO_TS/"))
{
remove(line);
}
}
} }
void enforceMaxEvents(shared_t *share) void enforceMaxEvents(shared_t *share)
@ -111,81 +138,83 @@ void enforceMaxEvents(shared_t *share)
while (names.size() > share->maxEvents) while (names.size() > share->maxEvents)
{ {
auto nameOnly = "events/" + names[0]; // removes the video file extension (.mp4).
auto nameOnly = "events/" + names[0].substr(0, names[0].size() - 4);
auto mp4File = nameOnly + string(".mp4");
auto imgFile = nameOnly + string(".jpg");
auto webFile = nameOnly + string(".html");
nameOnly.remove(".mp4"); if (exists(mp4File)) remove(mp4File);
if (exists(imgFile)) remove(imgFile);
if (exists(webFile)) remove(webFile);
auto mp4File = nameOnly + ".mp4"; names.erase(names.begin());
auto imgFile = nameOnly + ".jpg";
auto webFile = nameOnly + ".html";
QFile::remove(mp4File);
QFile::remove(imgFile);
QFile::remove(webFile);
names.removeFirst();
} }
} }
void enforceMaxImages()
string genTimeStr(const char *fmt)
{ {
auto names = lsFilesInDir("img", ".bmp"); time_t rawtime;
while (names.size() > MAX_IMAGES) time(&rawtime);
auto timeinfo = localtime(&rawtime);
char ret[50];
strftime(ret, 50, fmt, timeinfo);
return string(ret);
}
string genDstFile(const string &dirOut, const char *fmt, const string &ext)
{ {
QFile::remove("img/" + names[0]); createDirTree(cleanDir(dirOut));
names.removeFirst(); return cleanDir(dirOut) + string("/") + genTimeStr(fmt) + ext;
}
string genEventName(int score)
{
return genTimeStr(string("%Y-%j-%H-%M-%S--" + to_string(score)).c_str());
}
void rdLine(const string &param, const string &line, string *value)
{
if (line.rfind(param.c_str(), 0) == 0)
{
*value = line.substr(param.size());
} }
} }
void enforceMaxVids() void rdLine(const string &param, const string &line, int *value)
{ {
auto names = lsFilesInDir("live", ".ts"); if (line.rfind(param.c_str(), 0) == 0)
while (names.size() > MAX_VIDEOS)
{ {
QFile::remove("live/" + names[0]); *value = strtol(line.substr(param.size()).c_str(), NULL, 10);
names.removeFirst();
} }
} }
void rdLine(const QString &param, const QString &line, QString *value) bool rdConf(const string &filePath, shared_t *share)
{ {
if (line.startsWith(param)) ifstream varFile(filePath.c_str());
{
*value = line.mid(param.size());
}
}
void rdLine(const QString &param, const QString &line, int *value) if (!varFile.is_open())
{
if (line.startsWith(param))
{
*value = line.mid(param.size()).toInt();
}
}
bool rdConf(const QString &filePath, shared_t *share)
{
QFile varFile(filePath);
if (!varFile.open(QFile::ReadOnly))
{ {
share->retCode = ENOENT; share->retCode = ENOENT;
QTextStream(stderr) << "err: config file: " << filePath << " does not exists or lack read permissions." << Qt::endl; cerr << "err: config file: " << filePath << " does not exists or lack read permissions." << endl;
} }
else else
{ {
QString line; string line;
do do
{ {
line = QString::fromUtf8(varFile.readLine()); getline(varFile, line);
if (!line.startsWith("#")) if (line.rfind("#", 0) != 0)
{ {
rdLine("cam_name = ", line, &share->camName); rdLine("cam_name = ", line, &share->camName);
rdLine("recording_stream = ", line, &share->recordUrl); rdLine("recording_stream = ", line, &share->recordUrl);
@ -196,12 +225,14 @@ bool rdConf(const QString &filePath, shared_t *share)
rdLine("max_event_secs = ", line, &share->evMaxSecs); rdLine("max_event_secs = ", line, &share->evMaxSecs);
rdLine("post_secs = ", line, &share->postSecs); rdLine("post_secs = ", line, &share->postSecs);
rdLine("post_cmd = ", line, &share->postCmd); rdLine("post_cmd = ", line, &share->postCmd);
rdLine("pix_thresh = ", line, &share->pixThresh);
rdLine("img_thresh = ", line, &share->imgThresh); rdLine("img_thresh = ", line, &share->imgThresh);
rdLine("frame_gap = ", line, &share->frameGap);
rdLine("max_events = ", line, &share->maxEvents); rdLine("max_events = ", line, &share->maxEvents);
rdLine("max_log_size = ", line, &share->maxLogSize); rdLine("max_log_size = ", line, &share->maxLogSize);
} }
} while(!line.isEmpty()); } while(!line.empty());
} }
return share->retCode == 0; return share->retCode == 0;
@ -209,100 +240,122 @@ bool rdConf(const QString &filePath, shared_t *share)
bool rdConf(shared_t *share) bool rdConf(shared_t *share)
{ {
share->recordUrl.clear();
share->postCmd.clear();
share->camName.clear();
share->retCode = 0;
share->pixThresh = 50;
share->imgThresh = 800;
share->maxEvents = 40;
share->maxLogSize = 100000;
share->skipCmd = false;
share->postSecs = 60;
share->evMaxSecs = 10;
share->frameGap = 10;
share->webRoot = "/var/www/html";
share->webBg = "#485564";
share->webTxt = "#dee5ee";
share->webFont = "courier";
if (rdConf(share->conf, share)) if (rdConf(share->conf, share))
{ {
if (share->camName.isEmpty()) if (share->camName.empty())
{ {
share->camName = QFileInfo(share->conf).fileName(); share->camName = path(share->conf).filename();
} }
share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName; share->outDir = cleanDir(share->webRoot) + "/" + share->camName;
QDir().mkpath(share->outDir); error_code ec;
if (!QDir::setCurrent(share->outDir)) createDirTree(share->outDir);
current_path(share->outDir, ec);
share->retCode = ec.value();
if (share->retCode != 0)
{ {
QTextStream(stderr) << "err: failed to change/create the current working directory to camera folder: '" << share->outDir << "' does it exists?" << Qt::endl; cerr << "err: " << ec.message() << endl;
share->retCode = ENOENT;
} }
} }
return share->retCode == 0; return share->retCode == 0;
} }
MultiInstance::MultiInstance(QObject *parent) : QObject(parent) {} string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs)
{
auto ret = string();
void MultiInstance::instStdout() for (; offs < argc; ++offs)
{ {
for (auto &&proc : procList) auto argInParams = string(argv[offs]);
{
QTextStream(stdout) << proc->readAllStandardOutput();
}
}
void MultiInstance::instStderr() if (arg.compare(argInParams) == 0)
{ {
for (auto &&proc : procList) if (!argOnly)
{ {
QTextStream(stderr) << proc->readAllStandardError(); offs++;
// check ahead, make sure offs + 1 won't cause out-of-range exception
if (offs <= (argc - 1))
{
ret = string(argv[offs]);
} }
} }
void MultiInstance::procChanged(QProcess::ProcessState newState)
{
Q_UNUSED(newState)
for (auto &&proc : procList)
{
if (proc->state() == QProcess::Running)
{
return;
}
}
QCoreApplication::quit();
}
int MultiInstance::start(const QStringList &args)
{
auto ret = ENOENT;
auto path = QDir().cleanPath(getParam("-d", args));
auto files = lsFilesInDir(path);
if (!QDir(path).exists())
{
QTextStream(stderr) << "err: the supplied directory in -d '" << path << "' does not exists or is not a directory.";
}
else if (files.isEmpty())
{
QTextStream(stderr) << "err: no config files found in '" << path << "'";
}
else else
{ {
ret = 0; ret = string("true");
}
for (auto &&conf : files)
{
auto proc = new QProcess(this);
QStringList subArgs;
subArgs << "-c" << path + "/" + conf;
connect(proc, &QProcess::readyReadStandardOutput, this, &MultiInstance::instStdout);
connect(proc, &QProcess::readyReadStandardError, this, &MultiInstance::instStderr);
connect(proc, &QProcess::stateChanged, this, &MultiInstance::procChanged);
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, proc, &QProcess::terminate);
proc->setProgram(APP_BIN);
proc->setArguments(subArgs);
proc->start();
procList.append(proc);
} }
} }
return ret; return ret;
} }
string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
{
auto notUsed = 0;
return parseForParam(arg, argc, argv, argOnly, notUsed);
}
string genEventPath(const string &tsPath)
{
if (tsPath.size() > 14)
{
// removes 'VIDEO_TS/live/' from the front of the string.
auto ret = tsPath.substr(14);
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)
{
// remove all '/'
ret.erase(ind, 1);
ind = ret.find('/');
}
return ret;
}
else
{
return string();
}
}

View File

@ -13,98 +13,89 @@
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
#include <QCoreApplication> #include <iostream>
#include <QProcess> #include <fstream>
#include <QTextStream> #include <string>
#include <QObject> #include <time.h>
#include <QRegularExpression> #include <chrono>
#include <QDir> #include <stdlib.h>
#include <QCryptographicHash> #include <errno.h>
#include <QFile> #include <vector>
#include <QDateTime> #include <thread>
#include <QThread> #include <filesystem>
#include <QTimer> #include <sys/stat.h>
#include <QStringList> #include <map>
#include <QMutex>
#include <opencv4/opencv2/opencv.hpp>
#include <opencv4/opencv2/videoio.hpp>
using namespace cv;
using namespace std; using namespace std;
using namespace std::filesystem;
using namespace std::chrono;
#define APP_VER "3.0.0" #define APP_VER "2.2"
#define APP_NAME "Motion Watch" #define APP_NAME "Motion Watch"
#define APP_BIN "mow"
#define REC_LOG_NAME "rec_log_lines.html" #define REC_LOG_NAME "rec_log_lines.html"
#define DET_LOG_NAME "det_log_lines.html" #define DET_LOG_NAME "det_log_lines.html"
#define UPK_LOG_NAME "upk_log_lines.html" #define UPK_LOG_NAME "upk_log_lines.html"
#define DATETIME_FMT "yyyyMMddhhmmss"
#define STRFTIME_FMT "%Y%m%d%H%M%S"
#define MAX_IMAGES 1000
#define MAX_VIDEOS 1000
struct evt_t struct evt_t
{ {
QDateTime timeStamp; string evName;
QString imgPath; vector<string> srcPaths;
float score; Mat thumbnail;
}; };
struct shared_t struct shared_t
{ {
QList<evt_t> recList; vector<evt_t> recList;
QMutex recMutex; string conf;
QMutex logMutex; string recLog;
QString conf; string detLog;
QString recLog; string upkLog;
QString detLog; string recordUrl;
QString recordUrl; string outDir;
QString outDir; string postCmd;
QString postCmd; string camName;
QString camName; string webBg;
QString webBg; string webTxt;
QString webTxt; string webFont;
QString webFont; string webRoot;
QString webRoot; evt_t curEvent;
bool skipCmd; bool skipCmd;
int frameGap;
int evMaxSecs; int evMaxSecs;
int postSecs; int postSecs;
int maxScore;
int procCnt;
int hlsCnt;
int pixThresh;
int imgThresh; int imgThresh;
int maxEvents; int maxEvents;
int maxLogSize; int maxLogSize;
int retCode; int retCode;
int postInd;
int evInd;
}; };
QString getParam(const QString &key, const QStringList &args); string genVidNameFromLive(const string &tsPath);
QStringList lsFilesInDir(const QString &path, const QString &ext = QString()); string genEventPath(const string &tsPath);
QStringList lsDirsInDir(const QString &path); string genEventName(int score);
QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir); string genDstFile(const string &dirOut, const char *fmt, const string &ext);
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs); string genTimeStr(const char *fmt);
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs); string cleanDir(const string &path);
bool rdConf(const QString &filePath, shared_t *share); string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs);
bool rdConf(shared_t *share); string parseForParam(const string &arg, int argc, char** argv, bool argOnly);
void rdLine(const QString &param, const QString &line, QString *value); bool createDir(const string &dir);
void rdLine(const QString &param, const QString &line, int *value); bool createDirTree(const string &full_path);
void rdLine(const string &param, const string &line, string *value);
void rdLine(const string &param, const string &line, int *value);
void cleanupEmptyDirs(const string &path);
void cleanupStream(const string &plsPath);
void enforceMaxEvents(shared_t *share); void enforceMaxEvents(shared_t *share);
void enforceMaxImages(); bool rdConf(shared_t *share);
void enforceMaxVids(); vector<string> lsFilesInDir(const string &path, const string &ext = string());
vector<string> lsDirsInDir(const string &path);
class MultiInstance : public QObject
{
Q_OBJECT
private:
QList<QProcess*> procList;
private slots:
void instStdout();
void instStderr();
void procChanged(QProcess::ProcessState newState);
public:
explicit MultiInstance(QObject *parent = nullptr);
int start(const QStringList &args);
};
#endif // COMMON_H #endif // COMMON_H

View File

@ -12,62 +12,58 @@
#include "logger.h" #include "logger.h"
void recLog(const QString &line, shared_t *share) void recLog(const string &line, shared_t *share)
{ {
share->logMutex.lock(); share->recLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
share->recLog += QDateTime::currentDateTime().toString("[yyyy-MM-dd-hh-mm-ss] ") + line + "<br>\n";
share->logMutex.unlock();
} }
void detLog(const QString &line, shared_t *share) void detLog(const string &line, shared_t *share)
{ {
share->logMutex.lock(); share->detLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
share->detLog += QDateTime::currentDateTime().toString("[yyyy-MM-dd-hh-mm-ss] ") + line + "<br>\n";
share->logMutex.unlock();
} }
void enforceMaxLogSize(const QString &filePath, shared_t *share) void upkLog(const string &line, shared_t *share)
{ {
QFile file(filePath); share->upkLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
}
if (file.exists()) void enforceMaxLogSize(const string &filePath, shared_t *share)
{ {
if (file.size() >= share->maxLogSize) if (exists(filePath))
{ {
file.remove(); if (file_size(filePath) >= share->maxLogSize)
{
remove(filePath);
} }
} }
} }
void dumpLogs(const QString &fileName, const QString &lines) void dumpLogs(const string &fileName, const string &lines)
{ {
if (!lines.isEmpty()) if (!lines.empty())
{ {
QFile outFile(fileName); ofstream outFile;
if (outFile.exists()) if (exists(fileName))
{ {
outFile.open(QFile::Append); outFile.open(fileName.c_str(), ofstream::app);
} }
else else
{ {
outFile.open(QFile::WriteOnly); outFile.open(fileName.c_str());
} }
outFile.write(lines.toUtf8()); outFile << lines;
outFile.close(); outFile.close();
} }
} }
void initLogFrontPage(const QString &filePath, const QString &logLinesFile) void initLogFrontPage(const string &filePath, const string &logLinesFile)
{ {
if (!QFile::exists(filePath)) if (!exists(filePath))
{ {
QString htmlText = "<!DOCTYPE html>\n"; string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n"; htmlText += "<html>\n";
htmlText += "<script>\n"; htmlText += "<script>\n";
@ -110,16 +106,17 @@ void initLogFrontPage(const QString &filePath, const QString &logLinesFile)
htmlText += "</body>\n"; htmlText += "</body>\n";
htmlText += "</html>\n"; htmlText += "</html>\n";
QFile outFile(filePath); ofstream outFile(filePath);
outFile << htmlText;
outFile.open(QFile::WriteOnly);
outFile.write(htmlText.toUtf8());
outFile.close(); outFile.close();
} }
} }
void initLogFrontPages() void initLogFrontPages(shared_t *share)
{ {
initLogFrontPage("logs/recording_log.html", REC_LOG_NAME); initLogFrontPage("logs/recording_log.html", REC_LOG_NAME);
initLogFrontPage("logs/detection_log.html", DET_LOG_NAME); initLogFrontPage("logs/detection_log.html", DET_LOG_NAME);
initLogFrontPage("logs/upkeep_log.html", UPK_LOG_NAME);
} }

View File

@ -15,10 +15,11 @@
#include "common.h" #include "common.h"
void recLog(const QString &line, shared_t *share); void recLog(const string &line, shared_t *share);
void detLog(const QString &line, shared_t *share); void detLog(const string &line, shared_t *share);
void dumpLogs(const QString &fileName, const QString &lines); void upkLog(const string &line, shared_t *share);
void enforceMaxLogSize(const QString &filePath, shared_t *share); void dumpLogs(const string &fileName, const string &lines);
void initLogFrontPages(); void enforceMaxLogSize(const string &filePath, shared_t *share);
void initLogFrontPages(shared_t *share);
#endif // lOGGER_H #endif // lOGGER_H

View File

@ -10,60 +10,192 @@
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
#include "common.h" #include "mo_detect.h"
#include "camera.h" #include "logger.h"
#include "web.h"
int main(int argc, char** argv) void timer(shared_t *share)
{ {
QCoreApplication app(argc, argv); while (share->retCode == 0)
QCoreApplication::setApplicationName(APP_NAME);
QCoreApplication::setApplicationVersion(APP_VER);
auto args = QCoreApplication::arguments();
auto ret = 0;
if (args.contains("-h"))
{ {
QTextStream(stdout) << "Motion Watch " << APP_VER << Qt::endl << Qt::endl; sleep(1);
QTextStream(stdout) << "Usage: mow <argument>" << Qt::endl << Qt::endl;
QTextStream(stdout) << "-h : display usage information about this application." << Qt::endl;
QTextStream(stdout) << "-c : path to the config file used to run a single camera instance." << Qt::endl;
QTextStream(stdout) << "-d : path to a directory that can contain multiple config files." << Qt::endl;
QTextStream(stdout) << " each file found in the directory will be used to run a" << Qt::endl;
QTextStream(stdout) << " camera instance." << Qt::endl;
QTextStream(stdout) << "-v : display the current version." << Qt::endl << Qt::endl;
}
else if (args.contains("-v"))
{
QTextStream(stdout) << APP_VER << Qt::endl;
}
else if (args.contains("-d"))
{
auto *muli = new MultiInstance(&app);
ret = muli->start(args); share->postInd += 1;
share->evInd += 1;
if (ret == 0)
{
ret = QCoreApplication::exec();
} }
} }
else if (args.contains("-c"))
{
auto *cam = new Camera(&app);
ret = cam->start(args); void detectMo(shared_t *share)
if (ret == 0)
{ {
ret = QCoreApplication::exec(); while (share->retCode == 0)
{
sleep(2);
detectMoInStream("stream.m3u8", share);
} }
} }
void eventLoop(shared_t *share)
{
while (share->retCode == 0)
{
if (!share->recList.empty())
{
auto event = share->recList[0];
try
{
recLog("attempting write out of event: " + event.evName, share);
createDirTree("events");
if (wrOutVod(event, share))
{
genHTMLvod(event.evName);
imwrite(string("events/" + event.evName + ".jpg").c_str(), event.thumbnail);
}
}
catch (filesystem_error &ex)
{
recLog(string("err: ") + ex.what(), share);
}
share->recList.erase(share->recList.begin());
}
sleep(10);
}
}
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);
enforceMaxEvents(share);
genHTMLul(".", share->camName, share);
upkLog("camera specific webroot page updated: " + share->outDir + "/index.html", share);
if (!exists("/tmp/mow-lock"))
{
system("touch /tmp/mow-lock");
genCSS(share);
genHTMLul(share->webRoot, string(APP_NAME) + " " + string(APP_VER), share);
remove("/tmp/mow-lock");
upkLog("webroot page updated: " + cleanDir(share->webRoot) + "/index.html", share);
}
else else
{ {
QTextStream(stderr) << "err: no config file(s) were given in -c" << Qt::endl; upkLog("skipping update of the webroot page, it is busy.", share);
} }
return ret; sleep(10);
}
}
void rmLive()
{
if (exists("live"))
{
remove_all("live");
}
}
void recLoop(shared_t *share)
{
while (share->retCode == 0)
{
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 2 -hls_list_size 1000" +
" stream.m3u8";
recLog("ffmpeg_run: " + cmd, share);
rmLive();
auto retCode = system(cmd.c_str());
recLog("ffmpeg_retcode: " + to_string(retCode), share);
if (retCode != 0)
{
recLog("err: ffmpeg returned non zero, indicating failure. please check stderr output.", share);
}
sleep(10);
}
}
int main(int argc, char** argv)
{
struct shared_t sharedRes;
sharedRes.conf = parseForParam("-c", argc, argv, false);
if (parseForParam("-h", argc, argv, true) == "true")
{
cout << "Motion Watch " << APP_VER << endl << endl;
cout << "Usage: mow <argument>" << endl << endl;
cout << "-h : display usage information about this application." << endl;
cout << "-c : path to the config file." << endl;
cout << "-v : display the current version." << endl << endl;
}
else if (parseForParam("-v", argc, argv, true) == "true")
{
cout << APP_VER << endl;
}
else if (sharedRes.conf.empty())
{
cerr << "err: no config file(s) were given in -c" << endl;
}
else
{
sharedRes.retCode = 0;
sharedRes.maxScore = 0;
sharedRes.postInd = 0;
sharedRes.evInd = 0;
sharedRes.skipCmd = false;
rdConf(&sharedRes);
auto thr1 = thread(recLoop, &sharedRes);
auto thr2 = thread(upkeep, &sharedRes);
auto thr3 = thread(detectMo, &sharedRes);
auto thr4 = thread(eventLoop, &sharedRes);
auto thr5 = thread(timer, &sharedRes);
thr1.join();
thr2.join();
thr3.join();
thr4.join();
thr5.join();
return sharedRes.retCode;
}
return EINVAL;
} }

217
src/mo_detect.cpp Normal file
View File

@ -0,0 +1,217 @@
// This file is part of Motion Watch.
// Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "mo_detect.h"
void detectMoInStream(const string &streamFile, shared_t *share)
{
if (share->postInd >= share->postSecs)
{
if (!share->postCmd.empty())
{
detLog("---POST_BREAK---", share);
if (!share->skipCmd)
{
detLog("no motion detected, running post command: " + share->postCmd, share);
system(share->postCmd.c_str());
}
else
{
share->skipCmd = false;
detLog("motion detected, skipping the post command.", share);
}
}
share->postInd = 0;
}
if (share->evInd >= share->evMaxSecs)
{
detLog("---EVENT_BREAK---", share);
if (!share->curEvent.srcPaths.empty())
{
share->curEvent.evName = genEventName(share->maxScore);
share->recList.push_back(share->curEvent);
detLog("motion detected in " + to_string(share->curEvent.srcPaths.size()) + " file(s) in " + to_string(share->evMaxSecs) + " secs", share);
detLog("all video clips queued for event generation under event name: " + share->curEvent.evName, share);
}
else
{
detLog("no motion detected in all files. none queued for event generation.", share);
}
share->curEvent.srcPaths.clear();
share->curEvent.evName.clear();
share->curEvent.thumbnail.release();
share->evInd = 0;
share->maxScore = 0;
}
ifstream fileIn(streamFile);
string tsPath;
for (string line; getline(fileIn, line); )
{
if (line.starts_with("live/"))
{
tsPath = line;
}
}
if (!tsPath.empty())
{
if (moDetect(tsPath, share))
{
share->curEvent.srcPaths.push_back(tsPath);
share->skipCmd = true;
}
}
}
bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share)
{
Mat prevGray;
Mat nextGray;
cvtColor(prev, prevGray, COLOR_BGR2GRAY);
cvtColor(next, nextGray, COLOR_BGR2GRAY);
Mat diff;
absdiff(prevGray, nextGray, diff);
threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY);
score = countNonZero(diff);
detLog("diff_score: " + to_string(score) + " thresh: " + to_string(share->imgThresh), share);
return score >= share->imgThresh;
}
bool moDetect(const string &buffFile, shared_t *share)
{
auto score = 0;
auto mod = false;
detLog("stream_clip: " + buffFile, share);
VideoCapture capture;
if (!capture.open(buffFile.c_str(), CAP_FFMPEG))
{
usleep(500);
capture.open(buffFile.c_str(), CAP_FFMPEG);
}
if (capture.isOpened())
{
Mat prev;
Mat next;
int fps = capture.get(cv::CAP_PROP_FPS);
for (auto gap = 0, frm = fps; capture.grab(); ++gap, ++frm)
{
if (frm == fps) sleep(1); frm = 1;
if (prev.empty())
{
capture.retrieve(prev);
}
else if (gap == (share->frameGap - 1))
{
capture.retrieve(next);
if (!next.empty())
{
if (imgDiff(prev, next, score, share))
{
mod = true;
if (share->maxScore <= score)
{
share->maxScore = score;
resize(next, share->curEvent.thumbnail, Size(720, 480), INTER_LINEAR);
}
}
}
prev = next.clone();
gap = 0;
next.release();
}
else
{
capture.grab();
}
}
}
else
{
detLog("err: failed to open: " + buffFile + " after 500 msecs. giving up.", share);
}
capture.release();
return mod;
}
bool wrOutVod(const evt_t &event, shared_t *share)
{
auto cnt = 0;
auto concat = event.evName + ".tmp";
ofstream file(concat.c_str());
for (auto i = 0; i < event.srcPaths.size(); ++i)
{
recLog("event_src: " + event.srcPaths[i], share);
if (exists(event.srcPaths[i]))
{
file << "file '" << event.srcPaths[i] << "''" << endl; cnt++;
}
}
file.close();
if (cnt == 0)
{
recLog("err: none of the event hls clips exists, canceling write out.", share);
if (exists(concat)) remove(concat);
return false;
}
else
{
auto ret = system(string("ffmpeg -f concat -safe 0 -i " + concat + " -c copy events/" + event.evName + ".mp4").c_str());
if (ret != 0)
{
recLog("err: ffmpeg concat failure, canceling write out.", share);
}
if (exists(concat)) remove(concat);
return ret == 0;
}
}

24
src/mo_detect.h Normal file
View File

@ -0,0 +1,24 @@
#ifndef MO_DETECT_H
#define MO_DETECT_H
// This file is part of Motion Watch.
// Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "common.h"
#include "logger.h"
bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share);
bool moDetect(const string &buffFile, shared_t *share);
void detectMoInStream(const string &streamFile, shared_t *share);
bool wrOutVod(const evt_t &pls, shared_t *share);
#endif // MO_DETECT_H

View File

@ -12,13 +12,13 @@
#include "web.h" #include "web.h"
void genHTMLul(const QString &outputDir, const QString &title, shared_t *share) void genHTMLul(const string &outputDir, const string &title, shared_t *share)
{ {
QStringList logNames; vector<string> logNames;
QStringList eveNames; vector<string> eveNames;
QStringList dirNames; vector<string> dirNames;
QString htmlText = "<!DOCTYPE html>\n"; string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n"; htmlText += "<html>\n";
htmlText += "<head>\n"; htmlText += "<head>\n";
@ -31,7 +31,7 @@ void genHTMLul(const QString &outputDir, const QString &title, shared_t *share)
htmlText += "<body>\n"; htmlText += "<body>\n";
htmlText += "<h3>" + title + "</h3>\n"; htmlText += "<h3>" + title + "</h3>\n";
if (QDir().exists(outputDir + "/live")) if (exists(outputDir + "/live"))
{ {
eveNames = lsFilesInDir(outputDir + "/events", ".html"); eveNames = lsFilesInDir(outputDir + "/events", ".html");
logNames = lsFilesInDir(outputDir + "/logs", "_log.html"); logNames = lsFilesInDir(outputDir + "/logs", "_log.html");
@ -41,9 +41,8 @@ void genHTMLul(const QString &outputDir, const QString &title, shared_t *share)
for (auto &&logName : logNames) for (auto &&logName : logNames)
{ {
auto name = logName; // name.substr(0, name.size() - 9) removes _log.html
auto name = logName.substr(0, logName.size() - 9);
name.remove("_log.html");
htmlText += " <li><a href='logs/" + logName + "'>" + name + "</a></li>\n"; htmlText += " <li><a href='logs/" + logName + "'>" + name + "</a></li>\n";
} }
@ -59,9 +58,8 @@ void genHTMLul(const QString &outputDir, const QString &title, shared_t *share)
for (auto &&eveName : eveNames) for (auto &&eveName : eveNames)
{ {
auto name = eveName; // regName.substr(0, regName.size() - 5) removes .html
auto name = eveName.substr(0, eveName.size() - 5);
name.remove(".html");
htmlText += "<a href='events/" + eveName + "'><img src='events/" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n"; htmlText += "<a href='events/" + eveName + "'><img src='events/" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n";
} }
@ -83,16 +81,16 @@ void genHTMLul(const QString &outputDir, const QString &title, shared_t *share)
htmlText += "</body>\n"; htmlText += "</body>\n";
htmlText += "</html>"; htmlText += "</html>";
QFile outFile(QDir().cleanPath(outputDir) + "/index.html"); ofstream file(string(cleanDir(outputDir) + "/index.html").c_str());
outFile.open(QFile::WriteOnly); file << htmlText << endl;
outFile.write(htmlText.toUtf8());
outFile.close(); file.close();
} }
void genHTMLstream(const QString &name) void genHTMLstream(const string &name)
{ {
QString htmlText = "<!DOCTYPE html>\n"; string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n"; htmlText += "<html>\n";
htmlText += "<head>\n"; htmlText += "<head>\n";
@ -130,16 +128,16 @@ void genHTMLstream(const QString &name)
htmlText += "</body>\n"; htmlText += "</body>\n";
htmlText += "</html>"; htmlText += "</html>";
QFile outFile(name + ".html"); ofstream file(string(name + ".html").c_str());
outFile.open(QFile::WriteOnly); file << htmlText << endl;
outFile.write(htmlText.toUtf8());
outFile.close(); file.close();
} }
void genHTMLvod(const QString &name) void genHTMLvod(const string &name)
{ {
QString htmlText = "<!DOCTYPE html>\n"; string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n"; htmlText += "<html>\n";
htmlText += "<head>\n"; htmlText += "<head>\n";
@ -156,16 +154,16 @@ void genHTMLvod(const QString &name)
htmlText += "</body>\n"; htmlText += "</body>\n";
htmlText += "</html>"; htmlText += "</html>";
QFile outFile("events/" + name + ".html"); ofstream file(string("events/" + name + ".html").c_str());
outFile.open(QFile::WriteOnly); file << htmlText << endl;
outFile.write(htmlText.toUtf8());
outFile.close(); file.close();
} }
void genCSS(shared_t *share) void genCSS(shared_t *share)
{ {
QString cssText = "body {\n"; string cssText = "body {\n";
cssText += " background-color: " + share->webBg + ";\n"; cssText += " background-color: " + share->webBg + ";\n";
cssText += " color: " + share->webTxt + ";\n"; cssText += " color: " + share->webTxt + ";\n";
@ -175,9 +173,9 @@ void genCSS(shared_t *share)
cssText += " color: " + share->webTxt + ";\n"; cssText += " color: " + share->webTxt + ";\n";
cssText += "}\n"; cssText += "}\n";
QFile outFile(QDir().cleanPath(share->webRoot) + "/theme.css"); ofstream file(string(cleanDir(share->webRoot) + "/theme.css").c_str());
outFile.open(QFile::WriteOnly); file << cssText << endl;
outFile.write(cssText.toUtf8());
outFile.close(); file.close();
} }

View File

@ -15,9 +15,9 @@
#include "common.h" #include "common.h"
void genHTMLul(const QString &outputDir, const QString &title, shared_t *share); void genHTMLul(const string &outputDir, const string &title, shared_t *share);
void genHTMLstream(const QString &name); void genHTMLstream(const string &name);
void genHTMLvod(const QString &name); void genHTMLvod(const string &name);
void genCSS(shared_t *share); void genCSS(shared_t *share);
#endif // WEB_H #endif // WEB_H