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-mow
/.build-opencv
/src/opencv

View File

@ -1,31 +1,7 @@
cmake_minimum_required(VERSION 3.14)
project(MotionWatch LANGUAGES CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOUIC ON)
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})
cmake_minimum_required(VERSION 2.8.12)
project( MotionWatch )
find_package( OpenCV REQUIRED )
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pthread")
include_directories( ${OpenCV_INCLUDE_DIRS} )
add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp src/web.cpp src/logger.cpp )
target_link_libraries( mow ${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
advantage of this is reduced storage requirements as opposed to continuous
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 ###
@ -11,11 +13,13 @@ recording because only video footage of interest is recorded to storage.
Usage: mow <argument>
-h : display usage information about this application.
-c : path to the config file used to run a single camera instance.
-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.
-c : path to the config file(s).
-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 ###
@ -45,20 +49,30 @@ cam_name = cam-1
# 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.
#
max_event_secs = 30
# this is the maximum amount of secs of video footage that can be
# recorded in a motion event.
pix_thresh = 150
# this value tells the application how far different the pixels need to be
# 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
# this application uses 'magick compare' to score the differences between
# two, one second gapped snapshots of the camera stream. any image pairs
# that score greater than this value is considered motion and queues up
# max_event_secs worth of hls clips to be written out as a motion event.
img_thresh = 80000
# this indicates how many pixels need to be different in between frames
# before it is considered motion. any video clips found with frames
# exceeding this value will be copied from live footage to event footage.
#
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
# 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
# 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
@ -91,7 +105,8 @@ web_font = courier
### Setup/Build/Install ###
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

Binary file not shown.

View File

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

View File

@ -1,7 +1,29 @@
#!/bin/sh
export DEBIAN_FRONTEND=noninteractive
apt update -y
apt install -y pkg-config cmake make g++
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
cp ./bin/magick /usr/bin/magick
chmod +x /usr/bin/magick
apt install -y pkg-config
apt install -y cmake
apt install -y make
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"
QString getParam(const QString &key, const QStringList &args)
string cleanDir(const string &path)
{
// this can be used by command objects to pick out parameters
// 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)
if (path[path.size() - 1] == '/')
{
// key found.
if ((pos + 1) <= (args.size() - 1))
return path.substr(0, path.size() - 1);
}
else
{
// check ahead to make sure pos + 1 will not go out
// 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];
return path;
}
}
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;
}
QStringList lsFilesInDir(const QString &path, const QString &ext)
void cleanupEmptyDirs(const string &path)
{
QStringList filters;
filters << "*" + ext;
QDir dirObj(path);
dirObj.setFilter(QDir::Files);
dirObj.setNameFilters(filters);
dirObj.setSorting(QDir::Name);
return dirObj.entryList();
if (exists(path))
{
for (auto &entry : directory_iterator(path))
{
if (entry.is_directory())
{
try
{
remove(entry.path());
}
QStringList lsDirsInDir(const QString &path)
catch (filesystem_error const &ex)
{
QDir dirObj(path);
dirObj.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
dirObj.setSorting(QDir::Name);
return dirObj.entryList();
// non-empty dir assumed when filesystem_error is raised.
cleanupEmptyDirs(path + "/" + entry.path().filename().string());
}
}
}
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)
{
return listFacingFiles(path, ext, stamp, secs, '-');
sort(names.begin(), names.end());
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)
@ -111,81 +138,83 @@ void enforceMaxEvents(shared_t *share)
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";
auto imgFile = nameOnly + ".jpg";
auto webFile = nameOnly + ".html";
QFile::remove(mp4File);
QFile::remove(imgFile);
QFile::remove(webFile);
names.removeFirst();
names.erase(names.begin());
}
}
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");
while (names.size() > MAX_VIDEOS)
if (line.rfind(param.c_str(), 0) == 0)
{
QFile::remove("live/" + names[0]);
names.removeFirst();
*value = strtol(line.substr(param.size()).c_str(), NULL, 10);
}
}
void rdLine(const QString &param, const QString &line, QString *value)
bool rdConf(const string &filePath, shared_t *share)
{
if (line.startsWith(param))
{
*value = line.mid(param.size());
}
}
ifstream varFile(filePath.c_str());
void rdLine(const QString &param, const QString &line, int *value)
{
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))
if (!varFile.is_open())
{
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
{
QString line;
string line;
do
{
line = QString::fromUtf8(varFile.readLine());
getline(varFile, line);
if (!line.startsWith("#"))
if (line.rfind("#", 0) != 0)
{
rdLine("cam_name = ", line, &share->camName);
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("post_secs = ", line, &share->postSecs);
rdLine("post_cmd = ", line, &share->postCmd);
rdLine("pix_thresh = ", line, &share->pixThresh);
rdLine("img_thresh = ", line, &share->imgThresh);
rdLine("frame_gap = ", line, &share->frameGap);
rdLine("max_events = ", line, &share->maxEvents);
rdLine("max_log_size = ", line, &share->maxLogSize);
}
} while(!line.isEmpty());
} while(!line.empty());
}
return share->retCode == 0;
@ -209,100 +240,122 @@ bool rdConf(const QString &filePath, 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 (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;
share->retCode = ENOENT;
cerr << "err: " << ec.message() << endl;
}
}
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)
{
QTextStream(stdout) << proc->readAllStandardOutput();
}
}
auto argInParams = string(argv[offs]);
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
{
ret = 0;
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);
ret = string("true");
}
}
}
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
// GNU General Public License for more details.
#include <QCoreApplication>
#include <QProcess>
#include <QTextStream>
#include <QObject>
#include <QRegularExpression>
#include <QDir>
#include <QCryptographicHash>
#include <QFile>
#include <QDateTime>
#include <QThread>
#include <QTimer>
#include <QStringList>
#include <QMutex>
#include <iostream>
#include <fstream>
#include <string>
#include <time.h>
#include <chrono>
#include <stdlib.h>
#include <errno.h>
#include <vector>
#include <thread>
#include <filesystem>
#include <sys/stat.h>
#include <map>
#include <opencv4/opencv2/opencv.hpp>
#include <opencv4/opencv2/videoio.hpp>
using namespace cv;
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_BIN "mow"
#define REC_LOG_NAME "rec_log_lines.html"
#define DET_LOG_NAME "det_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
{
QDateTime timeStamp;
QString imgPath;
float score;
string evName;
vector<string> srcPaths;
Mat thumbnail;
};
struct shared_t
{
QList<evt_t> recList;
QMutex recMutex;
QMutex logMutex;
QString conf;
QString recLog;
QString detLog;
QString recordUrl;
QString outDir;
QString postCmd;
QString camName;
QString webBg;
QString webTxt;
QString webFont;
QString webRoot;
vector<evt_t> recList;
string conf;
string recLog;
string detLog;
string upkLog;
string recordUrl;
string outDir;
string postCmd;
string camName;
string webBg;
string webTxt;
string webFont;
string webRoot;
evt_t curEvent;
bool skipCmd;
int frameGap;
int evMaxSecs;
int postSecs;
int maxScore;
int procCnt;
int hlsCnt;
int pixThresh;
int imgThresh;
int maxEvents;
int maxLogSize;
int retCode;
int postInd;
int evInd;
};
QString getParam(const QString &key, const QStringList &args);
QStringList lsFilesInDir(const QString &path, const QString &ext = QString());
QStringList lsDirsInDir(const QString &path);
QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir);
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
bool rdConf(const QString &filePath, shared_t *share);
bool rdConf(shared_t *share);
void rdLine(const QString &param, const QString &line, QString *value);
void rdLine(const QString &param, const QString &line, int *value);
string genVidNameFromLive(const string &tsPath);
string genEventPath(const string &tsPath);
string genEventName(int score);
string genDstFile(const string &dirOut, const char *fmt, const string &ext);
string genTimeStr(const char *fmt);
string cleanDir(const string &path);
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs);
string parseForParam(const string &arg, int argc, char** argv, bool argOnly);
bool createDir(const string &dir);
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 enforceMaxImages();
void enforceMaxVids();
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);
};
bool rdConf(shared_t *share);
vector<string> lsFilesInDir(const string &path, const string &ext = string());
vector<string> lsDirsInDir(const string &path);
#endif // COMMON_H

View File

@ -12,62 +12,58 @@
#include "logger.h"
void recLog(const QString &line, shared_t *share)
void recLog(const string &line, shared_t *share)
{
share->logMutex.lock();
share->recLog += QDateTime::currentDateTime().toString("[yyyy-MM-dd-hh-mm-ss] ") + line + "<br>\n";
share->logMutex.unlock();
share->recLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
}
void detLog(const QString &line, shared_t *share)
void detLog(const string &line, shared_t *share)
{
share->logMutex.lock();
share->detLog += QDateTime::currentDateTime().toString("[yyyy-MM-dd-hh-mm-ss] ") + line + "<br>\n";
share->logMutex.unlock();
share->detLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
}
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
{
outFile.open(QFile::WriteOnly);
outFile.open(fileName.c_str());
}
outFile.write(lines.toUtf8());
outFile << lines;
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 += "<script>\n";
@ -110,16 +106,17 @@ void initLogFrontPage(const QString &filePath, const QString &logLinesFile)
htmlText += "</body>\n";
htmlText += "</html>\n";
QFile outFile(filePath);
ofstream outFile(filePath);
outFile << htmlText;
outFile.open(QFile::WriteOnly);
outFile.write(htmlText.toUtf8());
outFile.close();
}
}
void initLogFrontPages()
void initLogFrontPages(shared_t *share)
{
initLogFrontPage("logs/recording_log.html", REC_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"
void recLog(const QString &line, shared_t *share);
void detLog(const QString &line, shared_t *share);
void dumpLogs(const QString &fileName, const QString &lines);
void enforceMaxLogSize(const QString &filePath, shared_t *share);
void initLogFrontPages();
void recLog(const string &line, shared_t *share);
void detLog(const string &line, shared_t *share);
void upkLog(const string &line, shared_t *share);
void dumpLogs(const string &fileName, const string &lines);
void enforceMaxLogSize(const string &filePath, shared_t *share);
void initLogFrontPages(shared_t *share);
#endif // lOGGER_H

View File

@ -10,60 +10,192 @@
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "common.h"
#include "camera.h"
#include "mo_detect.h"
#include "logger.h"
#include "web.h"
int main(int argc, char** argv)
void timer(shared_t *share)
{
QCoreApplication app(argc, argv);
QCoreApplication::setApplicationName(APP_NAME);
QCoreApplication::setApplicationVersion(APP_VER);
auto args = QCoreApplication::arguments();
auto ret = 0;
if (args.contains("-h"))
while (share->retCode == 0)
{
QTextStream(stdout) << "Motion Watch " << APP_VER << Qt::endl << Qt::endl;
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);
sleep(1);
ret = muli->start(args);
if (ret == 0)
{
ret = QCoreApplication::exec();
share->postInd += 1;
share->evInd += 1;
}
}
else if (args.contains("-c"))
{
auto *cam = new Camera(&app);
ret = cam->start(args);
if (ret == 0)
void detectMo(shared_t *share)
{
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
{
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"
void genHTMLul(const QString &outputDir, const QString &title, shared_t *share)
void genHTMLul(const string &outputDir, const string &title, shared_t *share)
{
QStringList logNames;
QStringList eveNames;
QStringList dirNames;
vector<string> logNames;
vector<string> eveNames;
vector<string> dirNames;
QString htmlText = "<!DOCTYPE html>\n";
string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n";
htmlText += "<head>\n";
@ -31,7 +31,7 @@ void genHTMLul(const QString &outputDir, const QString &title, shared_t *share)
htmlText += "<body>\n";
htmlText += "<h3>" + title + "</h3>\n";
if (QDir().exists(outputDir + "/live"))
if (exists(outputDir + "/live"))
{
eveNames = lsFilesInDir(outputDir + "/events", ".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)
{
auto name = logName;
name.remove("_log.html");
// name.substr(0, name.size() - 9) removes _log.html
auto name = logName.substr(0, logName.size() - 9);
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)
{
auto name = eveName;
name.remove(".html");
// regName.substr(0, regName.size() - 5) removes .html
auto name = eveName.substr(0, eveName.size() - 5);
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 += "</html>";
QFile outFile(QDir().cleanPath(outputDir) + "/index.html");
ofstream file(string(cleanDir(outputDir) + "/index.html").c_str());
outFile.open(QFile::WriteOnly);
outFile.write(htmlText.toUtf8());
outFile.close();
file << htmlText << endl;
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 += "<head>\n";
@ -130,16 +128,16 @@ void genHTMLstream(const QString &name)
htmlText += "</body>\n";
htmlText += "</html>";
QFile outFile(name + ".html");
ofstream file(string(name + ".html").c_str());
outFile.open(QFile::WriteOnly);
outFile.write(htmlText.toUtf8());
outFile.close();
file << htmlText << endl;
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 += "<head>\n";
@ -156,16 +154,16 @@ void genHTMLvod(const QString &name)
htmlText += "</body>\n";
htmlText += "</html>";
QFile outFile("events/" + name + ".html");
ofstream file(string("events/" + name + ".html").c_str());
outFile.open(QFile::WriteOnly);
outFile.write(htmlText.toUtf8());
outFile.close();
file << htmlText << endl;
file.close();
}
void genCSS(shared_t *share)
{
QString cssText = "body {\n";
string cssText = "body {\n";
cssText += " background-color: " + share->webBg + ";\n";
cssText += " color: " + share->webTxt + ";\n";
@ -175,9 +173,9 @@ void genCSS(shared_t *share)
cssText += " color: " + share->webTxt + ";\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);
outFile.write(cssText.toUtf8());
outFile.close();
file << cssText << endl;
file.close();
}

View File

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