Completely re-written the project to use the QT API. By using Qt,
I've open up use of useful tools like QCryptographicHash, QString,
QByteArray, QFile, etc.. In the future I could even make use of
slots/signals. The code is also in general much more readable and
thread management is by far much easier.

General operation of the app should be the same, this commit just
serves as a base for the migration over to QT.
This commit is contained in:
Maurice ONeal 2023-05-15 15:29:47 -04:00
parent f4ea944f97
commit fa834aba6c
13 changed files with 800 additions and 685 deletions

View File

@ -1,7 +1,34 @@
cmake_minimum_required(VERSION 2.8.12)
project( MotionWatch )
find_package( OpenCV REQUIRED )
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pthread")
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)
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} )
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED)
find_package(OpenCV REQUIRED)
add_executable(mow
src/main.cpp
src/common.h
src/common.cpp
src/mo_detect.h
src/mo_detect.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

@ -18,12 +18,5 @@ apt install -y x264
apt install -y libx264-dev
apt install -y libilmbase-dev
apt install -y libopencv-dev
apt install -y qtbase5-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

405
src/camera.cpp Normal file
View File

@ -0,0 +1,405 @@
// 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.pixThresh = 50;
shared.imgThresh = 800;
shared.maxEvents = 40;
shared.maxLogSize = 100000;
shared.skipCmd = false;
shared.postSecs = 60;
shared.evMaxSecs = 10;
shared.frameGap = 10;
shared.webRoot = "/var/www/html";
shared.webBg = "#485564";
shared.webTxt = "#dee5ee";
shared.webFont = "courier";
}
bool Camera::start(const QStringList &args)
{
auto ret = false;
shared.conf = getParam("-c", args);
if (rdConf(&shared))
{
QDir("live").removeRecursively();
}
return ret;
}
Loop::Loop(shared_t *sharedRes, QObject *parent) : QObject(parent)
{
shared = sharedRes;
heartBeat = 10;
}
void Loop::loop()
{
while (exec())
{
if (heartBeat != 0)
{
thread()->sleep(heartBeat);
}
}
}
bool Loop::exec()
{
return shared->retCode == 0;
}
RecLoop::RecLoop(shared_t *sharedRes, QObject *parent) : Loop(sharedRes, parent)
{
once = true;
}
void RecLoop::updateCmd()
{
QStringList args;
args << "-hide_banner";
args << "-i" << shared->recordUrl;
args << "-strftime" << "1";
args << "-strftime_mkdir" << "1";
args << "-hls_segment_filename" << "live/%Y-%j-%H-%M-%S.ts";
args << "-hls_flags" << "delete_segments";
args << "-y";
args << "-vcodec" << "copy";
args << "-f" << "hls";
args << "-hls_time" << "2";
args << "-hls_list_size" << "1000";
args << "stream.m3u8";
proc.setProgram("ffmpeg");
proc.setArguments(args);
recLog("rec_args_updated: " + args.join(" "), shared);
}
void RecLoop::reset()
{
recLog("--rec_cmd_resetting--", shared);
proc.kill();
proc.waitForFinished();
updateCmd();
}
bool RecLoop::exec()
{
auto args = proc.arguments();
auto md5 = genMD5(QString("stream.m3u8"));
if (once)
{
updateCmd(); once = false; streamMD5 = genMD5(QByteArray("FIRST"));
}
else if ((args[3] != shared->recordUrl) || (streamMD5 == md5))
{
reset();
}
auto hashLogLine = "stream_hash prev:" + QString(streamMD5.toHex()) + " new:" + QString(md5.toHex());
recLog(hashLogLine, shared);
if (proc.state() == QProcess::NotRunning)
{
proc.start();
if (proc.waitForStarted())
{
recLog("rec_cmd_start: ok", shared);
}
else
{
recLog("rec_cmd_start: fail", shared);
recLog("rec_cmd_stderr: " + QString(proc.readAllStandardError()), shared);
}
}
return Loop::exec();
}
Upkeep::Upkeep(shared_t *sharedRes, QObject *parent) : Loop(sharedRes, parent) {}
bool Upkeep::exec()
{
QDir().mkdir("live");
QDir().mkdir("events");
QDir().mkdir("logs");
enforceMaxLogSize(QString("logs/") + REC_LOG_NAME, shared);
enforceMaxLogSize(QString("logs/") + DET_LOG_NAME, shared);
enforceMaxLogSize(QString("logs/") + UPK_LOG_NAME, shared);
dumpLogs(QString("logs/") + REC_LOG_NAME, shared->recLog);
dumpLogs(QString("logs/") + DET_LOG_NAME, shared->detLog);
dumpLogs(QString("logs/") + UPK_LOG_NAME, shared->upkLog);
shared->recLog.clear();
shared->detLog.clear();
shared->upkLog.clear();
initLogFrontPages(shared);
enforceMaxEvents(shared);
genHTMLul(".", shared->camName, shared);
upkLog("camera specific webroot page updated: " + shared->outDir + "/index.html", shared);
QFile tmp("/tmp/mow-lock");
if (!tmp.exists())
{
tmp.open(QFile::WriteOnly);
tmp.write(QByteArray());
genCSS(shared);
genHTMLul(shared->webRoot, QString(APP_NAME) + " " + QString(APP_VER), shared);
tmp.close();
tmp.remove();
upkLog("webroot page updated: " + QDir::cleanPath(shared->webRoot) + "/index.html", shared);
}
else
{
upkLog("skipping update of the webroot page, it is busy.", shared);
}
return Loop::exec();
}
EventLoop::EventLoop(shared_t *sharedRes, QObject *parent) : Loop(sharedRes, parent)
{
heartBeat = 2;
}
bool EventLoop::exec()
{
if (!shared->recList.isEmpty())
{
auto event = shared->recList[0];
recLog("attempting write out of event: " + event.evName, shared);
if (wrOutVod(event))
{
genHTMLvod(event.evName);
imwrite(QString("events/" + event.evName + ".jpg").toUtf8().data(), event.thumbnail);
}
shared->recList.removeFirst();
}
return Loop::exec();
}
bool EventLoop::wrOutVod(const evt_t &event)
{
auto cnt = 0;
auto concat = event.evName + ".tmp";
QFile file(concat);
file.open(QFile::WriteOnly);
for (auto i = 0; i < event.srcPaths.size(); ++i)
{
recLog("event_src: " + event.srcPaths[i], shared);
if (QFile::exists(event.srcPaths[i]))
{
file.write(QString("file '" + event.srcPaths[i] + "'\n").toUtf8()); cnt++;
}
}
file.close();
if (cnt == 0)
{
recLog("err: none of the event hls clips exists, canceling write out.", shared);
QFile::remove(concat); return false;
}
else
{
QProcess proc;
QStringList args;
args << "-f";
args << "concat";
args << "-safe" << "0";
args << "-i" << concat;
args << "-c" << "copy";
args << "events/" + event.evName + ".mp4";
proc.setProgram("ffmpeg");
proc.setArguments(args);
proc.start();
auto ret = false;
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, QObject *parent) : Loop(sharedRes, parent)
{
heartBeat = 0;
evId = 0;
pcId = 0;
resetTimers();
}
void DetectLoop::resetTimers()
{
if (evId != 0) killTimer(evId);
if (pcId != 0) killTimer(pcId);
evId = startTimer(shared->evMaxSecs);
pcId = startTimer(shared->postSecs);
}
void DetectLoop::timerEvent(QTimerEvent *event)
{
if (event->timerId() == evId)
{
detLog("---EVENT_BREAK---", shared);
if (!shared->curEvent.srcPaths.isEmpty())
{
shared->curEvent.evName = QDateTime::currentDateTime().toString("yyyyMMddmmss--") + QString::number(shared->maxScore);
shared->recList.append(shared->curEvent);
detLog("motion detected in " + QString::number(shared->curEvent.srcPaths.size()) + " file(s) in " + QString::number(shared->evMaxSecs) + " secs", shared);
detLog("all video clips queued for event generation under event name: " + shared->curEvent.evName, shared);
}
else
{
detLog("no motion detected in all files. none queued for event generation.", shared);
}
shared->curEvent.srcPaths.clear();
shared->curEvent.evName.clear();
shared->curEvent.thumbnail.release();
shared->maxScore = 0;
}
if (event->timerId() == pcId)
{
detLog("---POST_BREAK---", shared);
if (!shared->skipCmd)
{
detLog("no motion detected, running post command: " + shared->postCmd, shared);
system(shared->postCmd.toUtf8().data());
}
else
{
shared->skipCmd = false;
detLog("motion detected, skipping the post command.", shared);
}
}
}
bool DetectLoop::exec()
{
QFile fileIn("stream.m3u8");
QString tsPath;
if (!fileIn.open(QFile::ReadOnly))
{
detLog("err: failed to open the stream hls file for reading. reason: " + fileIn.errorString(), shared);
}
else if (fileIn.size() < 50)
{
detLog("the stream hls list is not big enough yet. waiting for more clips.", shared);
}
else if (!fileIn.seek(fileIn.size() - 50))
{
detLog("err: failed to seek to 'near end' of stream file. reason: " + fileIn.errorString(), shared);
}
else
{
QString line;
do
{
line = QString::fromUtf8(fileIn.readLine());
if (line.startsWith("live/"))
{
tsPath = line;
}
} while(!line.isEmpty());
}
if (tsPath.isEmpty())
{
detLog("wrn: didn't find the latest hls clip. previous failure? waiting 5secs.", shared);
thread()->sleep(5);
}
else if (prevTs == tsPath)
{
detLog("wrn: the lastest hls clip is the same as the previous clip. is the recording loop running? waiting 5secs.", shared);
thread()->sleep(5);
}
else
{
prevTs = tsPath;
if (moDetect(tsPath, thread(), shared))
{
shared->curEvent.srcPaths.append(tsPath);
shared->skipCmd = true;
}
}
return Loop::exec();
}

122
src/camera.h Normal file
View File

@ -0,0 +1,122 @@
#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"
#include "mo_detect.h"
class Loop : public QObject
{
Q_OBJECT
protected:
shared_t *shared;
int heartBeat;
private:
void loop();
public:
explicit Loop(shared_t *shared, QObject *parent = nullptr);
virtual bool exec();
};
class Camera : public QObject
{
Q_OBJECT
private:
shared_t shared;
public:
explicit Camera(QObject *parent = nullptr);
bool start(const QStringList &args);
};
class RecLoop : public Loop
{
Q_OBJECT
private:
QProcess proc;
QByteArray streamMD5;
bool once;
void updateCmd();
void reset();
public:
explicit RecLoop(shared_t *shared, QObject *parent = nullptr);
bool exec();
};
class Upkeep : public Loop
{
Q_OBJECT
public:
explicit Upkeep(shared_t *shared, QObject *parent = nullptr);
bool exec();
};
class EventLoop : public Loop
{
Q_OBJECT
private:
bool wrOutVod(const evt_t &event);
public:
explicit EventLoop(shared_t *shared, QObject *parent = nullptr);
bool exec();
};
class DetectLoop : public Loop
{
Q_OBJECT
private:
int pcId;
int evId;
QString prevTs;
void resetTimers();
void timerEvent(QTimerEvent *event);
public:
explicit DetectLoop(shared_t *shared, QObject *parent = nullptr);
bool exec();
};
#endif // CAMERA_H

View File

@ -12,124 +12,87 @@
#include "common.h"
string cleanDir(const string &path)
QString getParam(const QString &key, const QStringList &args)
{
if (path[path.size() - 1] == '/')
{
return path.substr(0, path.size() - 1);
}
else
{
return 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.
bool createDir(const string &dir)
{
auto ret = mkdir(dir.c_str(), 0777);
QString ret;
if (ret == -1)
int pos = args.indexOf(QRegularExpression(key, QRegularExpression::CaseInsensitiveOption));
if (pos != -1)
{
return errno == EEXIST;
}
else
// key found.
if ((pos + 1) <= (args.size() - 1))
{
return true;
// 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];
}
}
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;
}
void cleanupEmptyDirs(const string &path)
QByteArray genMD5(const QByteArray &bytes)
{
if (exists(path))
{
for (auto &entry : directory_iterator(path))
{
if (entry.is_directory())
{
try
{
remove(entry.path());
QCryptographicHash hasher(QCryptographicHash::Md5);
hasher.addData(bytes); return hasher.result();
}
catch (filesystem_error const &ex)
QByteArray genMD5(const QString &path)
{
// non-empty dir assumed when filesystem_error is raised.
cleanupEmptyDirs(path + "/" + entry.path().filename().string());
}
}
auto file = QFile(path);
if (file.open(QFile::ReadOnly))
{
return genMD5(file.readAll());
}
else
{
return genMD5(QByteArray("EMPTY"));
}
}
vector<string> lsFilesInDir(const string &path, const string &ext)
QStringList lsFilesInDir(const QString &path, const QString &ext)
{
vector<string> names;
QStringList filters;
if (exists(path))
{
for (auto &entry : directory_iterator(path))
{
if (entry.is_regular_file())
{
auto name = entry.path().filename().string();
filters << "*" + ext;
if (ext.empty() || name.ends_with(ext))
{
names.push_back(name);
}
}
}
QDir dirObj(path);
dirObj.setFilter(QDir::Files);
dirObj.setNameFilters(filters);
dirObj.setSorting(QDir::Name);
return dirObj.entryList();
}
sort(names.begin(), names.end());
return names;
}
vector<string> lsDirsInDir(const string &path)
QStringList lsDirsInDir(const QString &path)
{
vector<string> names;
QDir dirObj(path);
if (exists(path))
{
for (auto &entry : directory_iterator(path))
{
if (entry.is_directory())
{
names.push_back(entry.path().filename().string());
}
}
}
dirObj.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
dirObj.setSorting(QDir::Name);
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);
}
}
return dirObj.entryList();
}
void enforceMaxEvents(shared_t *share)
@ -138,83 +101,57 @@ void enforceMaxEvents(shared_t *share)
while (names.size() > share->maxEvents)
{
// 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");
auto nameOnly = "events/" + names[0];
if (exists(mp4File)) remove(mp4File);
if (exists(imgFile)) remove(imgFile);
if (exists(webFile)) remove(webFile);
nameOnly.remove(".mp4");
names.erase(names.begin());
auto mp4File = nameOnly + ".mp4";
auto imgFile = nameOnly + ".jpg";
auto webFile = nameOnly + ".html";
QFile::remove(mp4File);
QFile::remove(imgFile);
QFile::remove(webFile);
names.removeFirst();
}
}
string genTimeStr(const char *fmt)
void rdLine(const QString &param, const QString &line, QString *value)
{
time_t rawtime;
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)
if (line.startsWith(param))
{
createDirTree(cleanDir(dirOut));
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());
*value = line.mid(param.size());
}
}
void rdLine(const string &param, const string &line, int *value)
void rdLine(const QString &param, const QString &line, int *value)
{
if (line.rfind(param.c_str(), 0) == 0)
if (line.startsWith(param))
{
*value = strtol(line.substr(param.size()).c_str(), NULL, 10);
*value = line.mid(param.size()).toInt();
}
}
bool rdConf(const string &filePath, shared_t *share)
bool rdConf(const QString &filePath, shared_t *share)
{
ifstream varFile(filePath.c_str());
QFile varFile(filePath);
if (!varFile.is_open())
if (!varFile.open(QFile::ReadOnly))
{
share->retCode = ENOENT;
cerr << "err: config file: " << filePath << " does not exists or lack read permissions." << endl;
QTextStream(stderr) << "err: config file: " << filePath << " does not exists or lack read permissions." << Qt::endl;
}
else
{
string line;
QString line;
do
{
getline(varFile, line);
line = QString::fromUtf8(varFile.readLine());
if (line.rfind("#", 0) != 0)
if (!line.startsWith("#"))
{
rdLine("cam_name = ", line, &share->camName);
rdLine("recording_stream = ", line, &share->recordUrl);
@ -232,7 +169,7 @@ bool rdConf(const string &filePath, shared_t *share)
rdLine("max_log_size = ", line, &share->maxLogSize);
}
} while(!line.empty());
} while(!line.isEmpty());
}
return share->retCode == 0;
@ -240,122 +177,24 @@ bool rdConf(const string &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.empty())
if (share->camName.isEmpty())
{
share->camName = path(share->conf).filename();
share->camName = QFileInfo(share->conf).fileName();
}
share->outDir = cleanDir(share->webRoot) + "/" + share->camName;
share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName;
error_code ec;
QDir().mkpath(share->outDir);
createDirTree(share->outDir);
current_path(share->outDir, ec);
share->retCode = ec.value();
if (share->retCode != 0)
if (!QDir::setCurrent(share->outDir))
{
cerr << "err: " << ec.message() << endl;
QTextStream(stderr) << "err: failed to change/create the current working directory to camera folder: '" << share->outDir << ".' does it exists?" << Qt::endl;
share->retCode = ENOENT;
}
}
return share->retCode == 0;
}
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs)
{
auto ret = string();
for (; offs < argc; ++offs)
{
auto argInParams = string(argv[offs]);
if (arg.compare(argInParams) == 0)
{
if (!argOnly)
{
offs++;
// check ahead, make sure offs + 1 won't cause out-of-range exception
if (offs <= (argc - 1))
{
ret = string(argv[offs]);
}
}
else
{
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,28 +13,23 @@
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#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 <QCoreApplication>
#include <QProcess>
#include <QTextStream>
#include <QObject>
#include <QRegularExpression>
#include <QDir>
#include <QCryptographicHash>
#include <QFile>
#include <QDateTime>
#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 "2.2"
#define APP_VER "3.0.t1"
#define APP_NAME "Motion Watch"
#define REC_LOG_NAME "rec_log_lines.html"
#define DET_LOG_NAME "det_log_lines.html"
@ -42,27 +37,27 @@ using namespace std::chrono;
struct evt_t
{
string evName;
vector<string> srcPaths;
QString evName;
QStringList srcPaths;
Mat thumbnail;
};
struct shared_t
{
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;
QList<evt_t> recList;
evt_t curEvent;
QString conf;
QString recLog;
QString detLog;
QString upkLog;
QString recordUrl;
QString outDir;
QString postCmd;
QString camName;
QString webBg;
QString webTxt;
QString webFont;
QString webRoot;
bool skipCmd;
int frameGap;
int evMaxSecs;
@ -79,23 +74,15 @@ struct shared_t
int evInd;
};
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);
QByteArray genMD5(const QString &path);
QByteArray genMD5(const QByteArray &bytes);
QString getParam(const QString &key, const QStringList &args);
QStringList lsFilesInDir(const QString &path, const QString &ext = QString());
QStringList lsDirsInDir(const QString &path);
bool rdConf(const QString &filePath, shared_t *share);
bool rdConf(shared_t *share);
vector<string> lsFilesInDir(const string &path, const string &ext = string());
vector<string> lsDirsInDir(const string &path);
void rdLine(const QString &param, const QString &line, QString *value);
void rdLine(const QString &param, const QString &line, int *value);
void enforceMaxEvents(shared_t *share);
#endif // COMMON_H

View File

@ -12,58 +12,59 @@
#include "logger.h"
void recLog(const string &line, shared_t *share)
void recLog(const QString &line, shared_t *share)
{
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";
}
void detLog(const string &line, shared_t *share)
void detLog(const QString &line, shared_t *share)
{
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";
}
void upkLog(const string &line, shared_t *share)
void upkLog(const QString &line, shared_t *share)
{
share->upkLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
share->upkLog += QDateTime::currentDateTime().toString("[yyyy-MM-dd-hh-mm-ss] ") + line + "<br>\n";
}
void enforceMaxLogSize(const string &filePath, shared_t *share)
void enforceMaxLogSize(const QString &filePath, shared_t *share)
{
if (exists(filePath))
QFile file(filePath);
if (file.exists())
{
if (file_size(filePath) >= share->maxLogSize)
if (file.size() >= share->maxLogSize)
{
remove(filePath);
file.remove();
}
}
}
void dumpLogs(const string &fileName, const string &lines)
void dumpLogs(const QString &fileName, const QString &lines)
{
if (!lines.empty())
if (!lines.isEmpty())
{
ofstream outFile;
QFile outFile(fileName);
if (exists(fileName))
if (outFile.exists())
{
outFile.open(fileName.c_str(), ofstream::app);
outFile.open(QFile::Append);
}
else
{
outFile.open(fileName.c_str());
outFile.open(QFile::WriteOnly);
}
outFile << lines;
outFile.write(lines.toUtf8());
outFile.close();
}
}
void initLogFrontPage(const string &filePath, const string &logLinesFile)
void initLogFrontPage(const QString &filePath, const QString &logLinesFile)
{
if (!exists(filePath))
if (!QFile::exists(filePath))
{
string htmlText = "<!DOCTYPE html>\n";
QString htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n";
htmlText += "<script>\n";
@ -106,10 +107,10 @@ void initLogFrontPage(const string &filePath, const string &logLinesFile)
htmlText += "</body>\n";
htmlText += "</html>\n";
ofstream outFile(filePath);
outFile << htmlText;
QFile outFile(filePath);
outFile.open(QFile::WriteOnly);
outFile.write(htmlText.toUtf8());
outFile.close();
}
}

View File

@ -15,11 +15,11 @@
#include "common.h"
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 recLog(const QString &line, shared_t *share);
void detLog(const QString &line, shared_t *share);
void upkLog(const QString &line, shared_t *share);
void dumpLogs(const QString &fileName, const QString &lines);
void enforceMaxLogSize(const QString &filePath, shared_t *share);
void initLogFrontPages(shared_t *share);
#endif // lOGGER_H

View File

@ -13,189 +13,43 @@
#include "mo_detect.h"
#include "logger.h"
#include "web.h"
void timer(shared_t *share)
{
while (share->retCode == 0)
{
sleep(1);
share->postInd += 1;
share->evInd += 1;
}
}
void detectMo(shared_t *share)
{
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
{
upkLog("skipping update of the webroot page, it is busy.", share);
}
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);
}
}
#include "camera.h"
int main(int argc, char** argv)
{
struct shared_t sharedRes;
QCoreApplication app(argc, argv);
sharedRes.conf = parseForParam("-c", argc, argv, false);
QCoreApplication::setApplicationName(APP_NAME);
QCoreApplication::setApplicationVersion(APP_VER);
if (parseForParam("-h", argc, argv, true) == "true")
auto args = QCoreApplication::arguments();
auto ret = 0;
if (args.contains("-h"))
{
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;
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." << Qt::endl;
QTextStream(stdout) << "-v : display the current version." << Qt::endl << Qt::endl;
}
else if (parseForParam("-v", argc, argv, true) == "true")
else if (args.contains("-v"))
{
cout << APP_VER << endl;
QTextStream(stdout) << APP_VER << Qt::endl;
}
else if (sharedRes.conf.empty())
else if (args.contains("-c"))
{
cerr << "err: no config file(s) were given in -c" << endl;
auto *cam = new Camera(&app);
if (cam->start(args))
{
ret = QCoreApplication::exec();
}
}
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;
QTextStream(stderr) << "err: no config file(s) were given in -c" << Qt::endl;
}
return EINVAL;
return ret;
}

View File

@ -12,77 +12,6 @@
#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;
@ -98,12 +27,12 @@ bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share)
score = countNonZero(diff);
detLog("diff_score: " + to_string(score) + " thresh: " + to_string(share->imgThresh), share);
detLog("diff_score: " + QString::number(score) + " thresh: " + QString::number(share->imgThresh), share);
return score >= share->imgThresh;
}
bool moDetect(const string &buffFile, shared_t *share)
bool moDetect(const QString &buffFile, QThread *thr, shared_t *share)
{
auto score = 0;
auto mod = false;
@ -112,11 +41,11 @@ bool moDetect(const string &buffFile, shared_t *share)
VideoCapture capture;
if (!capture.open(buffFile.c_str(), CAP_FFMPEG))
if (!capture.open(buffFile.toUtf8().data(), CAP_FFMPEG))
{
usleep(500);
thr->usleep(500);
capture.open(buffFile.c_str(), CAP_FFMPEG);
capture.open(buffFile.toUtf8().data(), CAP_FFMPEG);
}
if (capture.isOpened())
@ -128,7 +57,7 @@ bool moDetect(const string &buffFile, shared_t *share)
for (auto gap = 0, frm = fps; capture.grab(); ++gap, ++frm)
{
if (frm == fps) sleep(1); frm = 1;
if (frm == fps) thr->sleep(1); frm = 1;
if (prev.empty())
{
@ -173,45 +102,3 @@ bool moDetect(const string &buffFile, shared_t *share)
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;
}
}

View File

@ -17,8 +17,6 @@
#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);
bool moDetect(const QString &buffFile, QThread *thr, shared_t *share);
#endif // MO_DETECT_H

View File

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

View File

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