v3.0.t6
The Qt approach to grabbing frames from the live stream was also a failure. - decided to switch to a combination ffmpeg and imagemagic was external commands to do motion detection. this approach elimates the need for opencv altogeather so it was removed from the project. system resource usage appears to be decent and perhaps better than opencv.
This commit is contained in:
parent
f850ec6a46
commit
4134d4befb
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -57,3 +57,5 @@ compile_commands.json
|
||||||
|
|
||||||
# Build folders
|
# Build folders
|
||||||
/.build-mow
|
/.build-mow
|
||||||
|
/.build-opencv
|
||||||
|
/src/opencv
|
||||||
|
|
|
@ -11,18 +11,13 @@ set(CMAKE_AUTORCC ON)
|
||||||
set(CMAKE_CXX_STANDARD 11)
|
set(CMAKE_CXX_STANDARD 11)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
include_directories(${OpenCV_INCLUDE_DIRS})
|
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED)
|
||||||
|
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED)
|
||||||
find_package(QT NAMES Qt6 COMPONENTS Core REQUIRED)
|
|
||||||
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Multimedia REQUIRED)
|
|
||||||
find_package(OpenCV REQUIRED)
|
|
||||||
|
|
||||||
add_executable(mow
|
add_executable(mow
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/common.h
|
src/common.h
|
||||||
src/common.cpp
|
src/common.cpp
|
||||||
src/mo_detect.h
|
|
||||||
src/mo_detect.cpp
|
|
||||||
src/web.h
|
src/web.h
|
||||||
src/web.cpp
|
src/web.cpp
|
||||||
src/logger.h
|
src/logger.h
|
||||||
|
@ -31,4 +26,4 @@ add_executable(mow
|
||||||
src/camera.cpp
|
src/camera.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(mow Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Multimedia ${OpenCV_LIBS})
|
target_link_libraries(mow Qt${QT_VERSION_MAJOR}::Core ${OpenCV_LIBS})
|
||||||
|
|
BIN
bin/magick
Normal file
BIN
bin/magick
Normal file
Binary file not shown.
7
setup.sh
7
setup.sh
|
@ -1,4 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt update -y
|
apt update -y
|
||||||
apt install -y pkg-config cmake make g++ wget unzip git
|
apt install -y pkg-config cmake make g++
|
||||||
apt install -y ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libgstreamer1.0-dev x264 libx264-dev libilmbase-dev libopencv-dev qt6-base-dev qtchooser qmake6 qt6-base-dev-tools qt6-multimedia-dev libxkbcommon-dev
|
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
|
||||||
|
cp ./bin/magick /usr/bin/magick
|
||||||
|
chmod +x /usr/bin/magick
|
||||||
|
|
364
src/camera.cpp
364
src/camera.cpp
|
@ -21,8 +21,8 @@ Camera::Camera(QObject *parent) : QObject(parent)
|
||||||
shared.retCode = 0;
|
shared.retCode = 0;
|
||||||
shared.maxScore = 0;
|
shared.maxScore = 0;
|
||||||
shared.pixThresh = 50;
|
shared.pixThresh = 50;
|
||||||
shared.imgThresh = 800;
|
shared.imgThresh = 8000;
|
||||||
shared.maxEvents = 40;
|
shared.maxEvents = 100;
|
||||||
shared.maxLogSize = 100000;
|
shared.maxLogSize = 100000;
|
||||||
shared.skipCmd = false;
|
shared.skipCmd = false;
|
||||||
shared.postSecs = 60;
|
shared.postSecs = 60;
|
||||||
|
@ -47,10 +47,10 @@ int Camera::start(const QStringList &args)
|
||||||
auto thr3 = new QThread(nullptr);
|
auto thr3 = new QThread(nullptr);
|
||||||
auto thr4 = new QThread(nullptr);
|
auto thr4 = new QThread(nullptr);
|
||||||
|
|
||||||
new RecLoop(&shared, thr1, this);
|
new RecLoop(&shared, thr1, nullptr);
|
||||||
new Upkeep(&shared, thr2, this);
|
new Upkeep(&shared, thr2, nullptr);
|
||||||
new EventLoop(&shared, thr3, this);
|
new EventLoop(&shared, thr3, nullptr);
|
||||||
new DetectLoop(&shared, thr4, this);
|
new DetectLoop(&shared, thr4, nullptr);
|
||||||
|
|
||||||
thr1->start();
|
thr1->start();
|
||||||
thr2->start();
|
thr2->start();
|
||||||
|
@ -61,25 +61,25 @@ int Camera::start(const QStringList &args)
|
||||||
return shared.retCode;
|
return shared.retCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
Loop::Loop(shared_t *sharedRes, QThread *thr, QObject *parent) : QObject(0)
|
Loop::Loop(shared_t *sharedRes, QThread *thr, QObject *parent) : QObject(parent)
|
||||||
{
|
{
|
||||||
shared = sharedRes;
|
shared = sharedRes;
|
||||||
heartBeat = 10;
|
heartBeat = 10;
|
||||||
loopTimer = new QTimer(nullptr);
|
loopTimer = 0;
|
||||||
|
|
||||||
loopTimer->setSingleShot(false);
|
|
||||||
|
|
||||||
connect(thr, &QThread::started, this, &Loop::init);
|
connect(thr, &QThread::started, this, &Loop::init);
|
||||||
connect(parent, &QObject::destroyed, this, &Loop::deleteLater);
|
connect(this, &Loop::loopSig, this, &Loop::loopSlot);
|
||||||
connect(parent, &QObject::destroyed, thr, &QThread::terminate);
|
|
||||||
connect(parent, &QObject::destroyed, loopTimer, &QTimer::deleteLater);
|
|
||||||
connect(loopTimer, &QTimer::timeout, this, &Loop::loopSlot);
|
|
||||||
|
|
||||||
moveToThread(thr);
|
moveToThread(thr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Loop::init()
|
void Loop::init()
|
||||||
{
|
{
|
||||||
|
loopTimer = new QTimer(nullptr);
|
||||||
|
|
||||||
|
connect(loopTimer, &QTimer::timeout, this, &Loop::loopSlot);
|
||||||
|
|
||||||
|
loopTimer->setSingleShot(false);
|
||||||
loopTimer->start(heartBeat * 1000);
|
loopTimer->start(heartBeat * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,82 +93,127 @@ void Loop::loopSlot()
|
||||||
|
|
||||||
bool Loop::exec()
|
bool Loop::exec()
|
||||||
{
|
{
|
||||||
|
if (loopTimer->interval() != heartBeat * 1000)
|
||||||
|
{
|
||||||
|
loopTimer->start(heartBeat * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
return shared->retCode == 0;
|
return shared->retCode == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
RecLoop::RecLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
|
RecLoop::RecLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
|
||||||
{
|
{
|
||||||
once = true;
|
once = true;
|
||||||
|
baseListRdy = false;
|
||||||
|
recProc = 0;
|
||||||
|
imgProc = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecLoop::init()
|
||||||
|
{
|
||||||
|
recProc = new QProcess(this);
|
||||||
|
imgProc = new QProcess(this);
|
||||||
|
|
||||||
|
Loop::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RecLoop::updateCmd()
|
void RecLoop::updateCmd()
|
||||||
{
|
{
|
||||||
QStringList args;
|
QStringList recArgs;
|
||||||
|
QStringList imgArgs;
|
||||||
|
|
||||||
args << "-hide_banner";
|
recArgs << "-hide_banner";
|
||||||
args << "-i" << shared->recordUrl;
|
recArgs << "-i" << shared->recordUrl;
|
||||||
args << "-strftime" << "1";
|
recArgs << "-strftime" << "1";
|
||||||
args << "-strftime_mkdir" << "1";
|
recArgs << "-strftime_mkdir" << "1";
|
||||||
args << "-hls_segment_filename" << "live/%Y-%j-%H-%M-%S.ts";
|
recArgs << "-hls_segment_filename" << "live/" + QString(STRFTIME_FMT) + ".ts";
|
||||||
args << "-hls_flags" << "delete_segments";
|
recArgs << "-hls_flags" << "delete_segments";
|
||||||
args << "-y";
|
recArgs << "-y";
|
||||||
args << "-vcodec" << "copy";
|
recArgs << "-vcodec" << "copy";
|
||||||
args << "-f" << "hls";
|
recArgs << "-f" << "hls";
|
||||||
args << "-hls_time" << "2";
|
recArgs << "-hls_time" << "2";
|
||||||
args << "-hls_list_size" << "1000";
|
recArgs << "-hls_list_size" << "1000";
|
||||||
args << "stream.m3u8";
|
recArgs << "stream.m3u8";
|
||||||
|
|
||||||
proc.setProgram("ffmpeg");
|
imgArgs << "-hide_banner";
|
||||||
proc.setArguments(args);
|
imgArgs << "-i" << shared->recordUrl;
|
||||||
|
imgArgs << "-strftime" << "1";
|
||||||
|
imgArgs << "-vf" << "fps=1,scale=320:240";
|
||||||
|
imgArgs << "img/" + QString(STRFTIME_FMT) + ".bmp";
|
||||||
|
|
||||||
|
recProc->setProgram("ffmpeg");
|
||||||
|
recProc->setArguments(recArgs);
|
||||||
|
|
||||||
|
imgProc->setProgram("ffmpeg");
|
||||||
|
imgProc->setArguments(imgArgs);
|
||||||
|
|
||||||
curUrl = shared->recordUrl;
|
curUrl = shared->recordUrl;
|
||||||
|
|
||||||
recLog("rec_args_updated: " + args.join(" "), shared);
|
recLog("rec_args_updated: " + recArgs.join(" "), shared);
|
||||||
|
recLog("img_args_updated: " + imgArgs.join(" "), shared);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RecLoop::reset()
|
void RecLoop::reset()
|
||||||
{
|
{
|
||||||
recLog("--rec_cmd_resetting--", shared);
|
recLog("--rec_and_img_cmds_resetting--", shared);
|
||||||
|
|
||||||
proc.kill();
|
baseListRdy = false;
|
||||||
proc.waitForFinished();
|
|
||||||
|
recProc->kill();
|
||||||
|
recProc->waitForFinished();
|
||||||
|
|
||||||
|
imgProc->kill();
|
||||||
|
imgProc->waitForFinished();
|
||||||
|
|
||||||
updateCmd();
|
updateCmd();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RecLoop::exec()
|
void RecLoop::startProc(const QString &desc, QProcess *proc)
|
||||||
{
|
{
|
||||||
auto md5 = genMD5(QString("stream.m3u8"));
|
if (proc->state() == QProcess::NotRunning)
|
||||||
|
|
||||||
if (once)
|
|
||||||
{
|
{
|
||||||
updateCmd(); once = false; streamMD5 = genMD5(QByteArray("FIRST"));
|
proc->start();
|
||||||
}
|
|
||||||
else if ((curUrl != shared->recordUrl) || (streamMD5 == md5))
|
if (proc->waitForStarted())
|
||||||
{
|
{
|
||||||
reset();
|
recLog(desc + "_cmd_start: ok", shared);
|
||||||
}
|
|
||||||
|
|
||||||
auto hashLogLine = "stream_hash--prev:" + QString(streamMD5.toHex()) + "--new:" + QString(md5.toHex());
|
|
||||||
|
|
||||||
streamMD5 = md5;
|
|
||||||
|
|
||||||
recLog(hashLogLine, shared);
|
|
||||||
|
|
||||||
if (proc.state() == QProcess::NotRunning)
|
|
||||||
{
|
|
||||||
proc.start();
|
|
||||||
|
|
||||||
if (proc.waitForStarted())
|
|
||||||
{
|
|
||||||
recLog("rec_cmd_start: ok", shared);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
recLog("rec_cmd_start: fail", shared);
|
recLog(desc + "_cmd_start: fail", shared);
|
||||||
recLog("rec_cmd_stderr: " + QString(proc.readAllStandardError()), shared);
|
recLog(desc + "_cmd_stderr: " + QString(proc->readAllStandardError()), shared);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RecLoop::exec()
|
||||||
|
{
|
||||||
|
if (once)
|
||||||
|
{
|
||||||
|
updateCmd(); once = false;
|
||||||
|
}
|
||||||
|
else if (!baseListRdy)
|
||||||
|
{
|
||||||
|
baseListRdy = true;
|
||||||
|
}
|
||||||
|
else if (backwardFacingFiles("live", ".ts", QDateTime::currentDateTime(), heartBeat).isEmpty())
|
||||||
|
{
|
||||||
|
recLog("backward facing files in the live stream are empty. cmd stall is suspected.", shared);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
else if (backwardFacingFiles("img", ".bmp", QDateTime::currentDateTime(), heartBeat).isEmpty())
|
||||||
|
{
|
||||||
|
recLog("backward facing files in the image stream are empty. cmd stall is suspected.", shared);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
else if (curUrl != shared->recordUrl)
|
||||||
|
{
|
||||||
|
recLog("a change in the recording URL was detected.", shared);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
startProc("img", imgProc);
|
||||||
|
startProc("rec", recProc);
|
||||||
|
|
||||||
return Loop::exec();
|
return Loop::exec();
|
||||||
}
|
}
|
||||||
|
@ -180,6 +225,7 @@ bool Upkeep::exec()
|
||||||
QDir().mkdir("live");
|
QDir().mkdir("live");
|
||||||
QDir().mkdir("events");
|
QDir().mkdir("events");
|
||||||
QDir().mkdir("logs");
|
QDir().mkdir("logs");
|
||||||
|
QDir().mkdir("img");
|
||||||
|
|
||||||
enforceMaxLogSize(QString("logs/") + REC_LOG_NAME, shared);
|
enforceMaxLogSize(QString("logs/") + REC_LOG_NAME, shared);
|
||||||
enforceMaxLogSize(QString("logs/") + DET_LOG_NAME, shared);
|
enforceMaxLogSize(QString("logs/") + DET_LOG_NAME, shared);
|
||||||
|
@ -195,6 +241,7 @@ bool Upkeep::exec()
|
||||||
|
|
||||||
initLogFrontPages(shared);
|
initLogFrontPages(shared);
|
||||||
enforceMaxEvents(shared);
|
enforceMaxEvents(shared);
|
||||||
|
enforceMaxImages();
|
||||||
|
|
||||||
genHTMLul(".", shared->camName, shared);
|
genHTMLul(".", shared->camName, shared);
|
||||||
upkLog("camera specific webroot page updated: " + shared->outDir + "/index.html", shared);
|
upkLog("camera specific webroot page updated: " + shared->outDir + "/index.html", shared);
|
||||||
|
@ -224,7 +271,7 @@ bool Upkeep::exec()
|
||||||
|
|
||||||
EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
|
EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
|
||||||
{
|
{
|
||||||
heartBeat = 2;
|
heartBeat = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EventLoop::exec()
|
bool EventLoop::exec()
|
||||||
|
@ -232,14 +279,35 @@ bool EventLoop::exec()
|
||||||
if (!shared->recList.isEmpty())
|
if (!shared->recList.isEmpty())
|
||||||
{
|
{
|
||||||
auto event = shared->recList[0];
|
auto event = shared->recList[0];
|
||||||
|
auto name = event.timeStamp.toString(DATETIME_FMT);
|
||||||
|
auto vidList = backwardFacingFiles("live", ".ts", event.timeStamp, shared->evMaxSecs / 2);
|
||||||
|
|
||||||
recLog("attempting write out of event: " + event.evName, shared);
|
if (vidList.isEmpty())
|
||||||
|
|
||||||
if (wrOutVod(event))
|
|
||||||
{
|
{
|
||||||
genHTMLvod(event.evName);
|
recLog("err: no backward faces files were found for event: " + name, shared);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
vidList.removeLast();
|
||||||
|
|
||||||
event.thumbnail.save(QString("events/" + event.evName + ".jpg"));
|
vidList += forwardFacingFiles("live", ".ts", event.timeStamp, shared->evMaxSecs / 2);
|
||||||
|
|
||||||
|
recLog("attempting write out of event: " + name, shared);
|
||||||
|
|
||||||
|
if (wrOutVod(name, vidList))
|
||||||
|
{
|
||||||
|
genHTMLvod(name);
|
||||||
|
|
||||||
|
QProcess proc;
|
||||||
|
QStringList args;
|
||||||
|
|
||||||
|
args << "convert";
|
||||||
|
args << event.imgPath;
|
||||||
|
args << "events/" + name + ".jpg";
|
||||||
|
|
||||||
|
proc.start("magick", args);
|
||||||
|
proc.waitForFinished();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shared->recList.removeFirst();
|
shared->recList.removeFirst();
|
||||||
|
@ -248,22 +316,22 @@ bool EventLoop::exec()
|
||||||
return Loop::exec();
|
return Loop::exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EventLoop::wrOutVod(const evt_t &event)
|
bool EventLoop::wrOutVod(const QString &name, const QStringList &vids)
|
||||||
{
|
{
|
||||||
auto cnt = 0;
|
auto cnt = 0;
|
||||||
auto concat = event.evName + ".tmp";
|
auto concat = name + ".tmp";
|
||||||
|
|
||||||
QFile file(concat);
|
QFile file(concat);
|
||||||
|
|
||||||
file.open(QFile::WriteOnly);
|
file.open(QFile::WriteOnly);
|
||||||
|
|
||||||
for (auto i = 0; i < event.srcPaths.size(); ++i)
|
for (auto &&vid : vids)
|
||||||
{
|
{
|
||||||
recLog("event_src: " + event.srcPaths[i], shared);
|
recLog("event_src: " + vid, shared);
|
||||||
|
|
||||||
if (QFile::exists(event.srcPaths[i]))
|
if (QFile::exists(vid))
|
||||||
{
|
{
|
||||||
file.write(QString("file '" + event.srcPaths[i] + "'\n").toUtf8()); cnt++;
|
file.write(QString("file '" + vid + "'\n").toUtf8()); cnt++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,7 +353,7 @@ bool EventLoop::wrOutVod(const evt_t &event)
|
||||||
args << "-safe" << "0";
|
args << "-safe" << "0";
|
||||||
args << "-i" << concat;
|
args << "-i" << concat;
|
||||||
args << "-c" << "copy";
|
args << "-c" << "copy";
|
||||||
args << "events/" + event.evName + ".mp4";
|
args << "events/" + name + ".mp4";
|
||||||
|
|
||||||
proc.setProgram("ffmpeg");
|
proc.setProgram("ffmpeg");
|
||||||
proc.setArguments(args);
|
proc.setArguments(args);
|
||||||
|
@ -313,20 +381,16 @@ bool EventLoop::wrOutVod(const evt_t &event)
|
||||||
|
|
||||||
DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
|
DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
|
||||||
{
|
{
|
||||||
heartBeat = 1;
|
pcTimer = 0;
|
||||||
evId = 0;
|
heartBeat = 3;
|
||||||
pcId = 0;
|
|
||||||
player = new QMediaPlayer(this);
|
|
||||||
frameAnalyzer = new MoDetect(shared, this);
|
|
||||||
|
|
||||||
player->setVideoSink(frameAnalyzer);
|
|
||||||
|
|
||||||
connect(player, &QMediaPlayer::errorOccurred, this, &DetectLoop::playError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DetectLoop::init()
|
void DetectLoop::init()
|
||||||
{
|
{
|
||||||
thread()->sleep(1);
|
pcTimer = new QTimer(nullptr);
|
||||||
|
mod = false;
|
||||||
|
|
||||||
|
connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak);
|
||||||
|
|
||||||
resetTimers();
|
resetTimers();
|
||||||
|
|
||||||
|
@ -335,134 +399,68 @@ void DetectLoop::init()
|
||||||
|
|
||||||
void DetectLoop::resetTimers()
|
void DetectLoop::resetTimers()
|
||||||
{
|
{
|
||||||
if (evId != 0) killTimer(evId);
|
pcTimer->start(shared->postSecs * 1000);
|
||||||
if (pcId != 0) killTimer(pcId);
|
|
||||||
|
|
||||||
evId = startTimer(shared->evMaxSecs * 1000);
|
|
||||||
pcId = startTimer(shared->postSecs * 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DetectLoop::timerEvent(QTimerEvent *event)
|
void DetectLoop::pcBreak()
|
||||||
{
|
{
|
||||||
if (event->timerId() == evId)
|
if (!shared->postCmd.isEmpty())
|
||||||
{
|
|
||||||
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 = QImage();
|
|
||||||
|
|
||||||
shared->maxScore = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((event->timerId() == pcId) && (!shared->postCmd.isEmpty()))
|
|
||||||
{
|
{
|
||||||
detLog("---POST_BREAK---", shared);
|
detLog("---POST_BREAK---", shared);
|
||||||
|
|
||||||
if (!shared->skipCmd)
|
if (mod)
|
||||||
|
{
|
||||||
|
detLog("motion detected, skipping the post command.", shared);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
detLog("no motion detected, running post command: " + shared->postCmd, shared);
|
detLog("no motion detected, running post command: " + shared->postCmd, shared);
|
||||||
system(shared->postCmd.toUtf8().data());
|
system(shared->postCmd.toUtf8().data());
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
shared->skipCmd = false;
|
|
||||||
|
|
||||||
detLog("motion detected, skipping the post command.", shared);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetectLoop::playError(QMediaPlayer::Error error, const QString &errorString)
|
mod = false;
|
||||||
{
|
|
||||||
Q_UNUSED(error)
|
|
||||||
|
|
||||||
detLog("err: media player error - " + errorString, shared);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetectLoop::playStateChanged(QMediaPlayer::PlaybackState newState)
|
|
||||||
{
|
|
||||||
if (newState == QMediaPlayer::PlayingState)
|
|
||||||
{
|
|
||||||
detLog("detection playback started.", shared);
|
|
||||||
}
|
|
||||||
else if (newState == QMediaPlayer::StoppedState)
|
|
||||||
{
|
|
||||||
detLog("detection playback stopped.", shared);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DetectLoop::exec()
|
bool DetectLoop::exec()
|
||||||
{
|
{
|
||||||
if (player->playbackState() != QMediaPlayer::PlayingState)
|
auto curDT = QDateTime::currentDateTime();
|
||||||
{
|
auto images = backwardFacingFiles("img", ".bmp", curDT, 10);
|
||||||
QFile fileIn("stream.m3u8");
|
|
||||||
QString tsPath;
|
|
||||||
|
|
||||||
if (!fileIn.open(QFile::ReadOnly))
|
if (images.size() < 3)
|
||||||
{
|
{
|
||||||
detLog("err: failed to open the stream hls file for reading. reason: " + fileIn.errorString(), shared);
|
detLog("wrn: didn't pick up enough image files from the image stream. number of files: " + QString::number(images.size()), 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
|
else
|
||||||
{
|
{
|
||||||
QString line;
|
QProcess extComp;
|
||||||
|
QStringList args;
|
||||||
|
|
||||||
do
|
args << "compare";
|
||||||
|
args << "-metric" << "FUZZ";
|
||||||
|
args << images[0];
|
||||||
|
args << images[1];
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (output.toFloat() >= shared->imgThresh)
|
||||||
{
|
{
|
||||||
line = QString::fromUtf8(fileIn.readLine());
|
detLog("--threshold_breached: " + QString::number(shared->imgThresh), shared);
|
||||||
|
|
||||||
if (line.startsWith("live/"))
|
evt_t event;
|
||||||
{
|
|
||||||
tsPath = line;
|
|
||||||
}
|
|
||||||
|
|
||||||
} while(!line.isEmpty());
|
event.timeStamp = curDT;
|
||||||
}
|
event.imgPath = images[2];
|
||||||
|
|
||||||
if (tsPath.isEmpty())
|
shared->recList.append(event); mod = true;
|
||||||
{
|
|
||||||
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
|
|
||||||
{
|
|
||||||
detLog("stream_clip: " + tsPath, shared);
|
|
||||||
|
|
||||||
prevTs = tsPath;
|
|
||||||
|
|
||||||
player->setSource(QUrl::fromLocalFile(tsPath));
|
|
||||||
frameAnalyzer->play(tsPath);
|
|
||||||
player->play();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
32
src/camera.h
32
src/camera.h
|
@ -16,7 +16,6 @@
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "web.h"
|
#include "web.h"
|
||||||
#include "mo_detect.h"
|
|
||||||
|
|
||||||
class Camera : public QObject
|
class Camera : public QObject
|
||||||
{
|
{
|
||||||
|
@ -41,12 +40,17 @@ protected:
|
||||||
|
|
||||||
shared_t *shared;
|
shared_t *shared;
|
||||||
QTimer *loopTimer;
|
QTimer *loopTimer;
|
||||||
|
bool interruptible;
|
||||||
int heartBeat;
|
int heartBeat;
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
|
|
||||||
virtual void init();
|
virtual void init();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
|
||||||
|
void loopSig();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
||||||
void loopSlot();
|
void loopSlot();
|
||||||
|
@ -64,13 +68,21 @@ class RecLoop : public Loop
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
QProcess proc;
|
QProcess *recProc;
|
||||||
|
QProcess *imgProc;
|
||||||
|
QStringList recList;
|
||||||
|
QStringList imgList;
|
||||||
QString curUrl;
|
QString curUrl;
|
||||||
QByteArray streamMD5;
|
bool baseListRdy;
|
||||||
bool once;
|
bool once;
|
||||||
|
|
||||||
void updateCmd();
|
void updateCmd();
|
||||||
void reset();
|
void reset();
|
||||||
|
void startProc(const QString &desc, QProcess *proc);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
|
||||||
|
void init();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
|
@ -96,7 +108,7 @@ class EventLoop : public Loop
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
bool wrOutVod(const evt_t &event);
|
bool wrOutVod(const QString &name, const QStringList &vids);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
|
@ -111,21 +123,15 @@ class DetectLoop : public Loop
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
int pcId;
|
QTimer *pcTimer;
|
||||||
int evId;
|
bool mod;
|
||||||
QString prevTs;
|
|
||||||
QMediaPlayer *player;
|
|
||||||
MoDetect *frameAnalyzer;
|
|
||||||
|
|
||||||
void resetTimers();
|
void resetTimers();
|
||||||
void resetDetect();
|
|
||||||
void timerEvent(QTimerEvent *event);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
||||||
void init();
|
void init();
|
||||||
void playError(QMediaPlayer::Error error, const QString &errorString);
|
void pcBreak();
|
||||||
void playStateChanged(QMediaPlayer::PlaybackState newState);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
|
|
|
@ -49,27 +49,6 @@ QString getParam(const QString &key, const QStringList &args)
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray genMD5(const QByteArray &bytes)
|
|
||||||
{
|
|
||||||
QCryptographicHash hasher(QCryptographicHash::Md5);
|
|
||||||
|
|
||||||
hasher.addData(bytes); return hasher.result();
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray genMD5(const QString &path)
|
|
||||||
{
|
|
||||||
QFile file(path);
|
|
||||||
|
|
||||||
if (file.open(QFile::ReadOnly))
|
|
||||||
{
|
|
||||||
return genMD5(file.readAll());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return genMD5(QByteArray("EMPTY"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList lsFilesInDir(const QString &path, const QString &ext)
|
QStringList lsFilesInDir(const QString &path, const QString &ext)
|
||||||
{
|
{
|
||||||
QStringList filters;
|
QStringList filters;
|
||||||
|
@ -95,6 +74,37 @@ QStringList lsDirsInDir(const QString &path)
|
||||||
return dirObj.entryList();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs)
|
||||||
|
{
|
||||||
|
return listFacingFiles(path, ext, stamp, secs, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs)
|
||||||
|
{
|
||||||
|
return listFacingFiles(path, ext, stamp, secs, '+');
|
||||||
|
}
|
||||||
|
|
||||||
void enforceMaxEvents(shared_t *share)
|
void enforceMaxEvents(shared_t *share)
|
||||||
{
|
{
|
||||||
auto names = lsFilesInDir("events", ".mp4");
|
auto names = lsFilesInDir("events", ".mp4");
|
||||||
|
@ -117,6 +127,18 @@ void enforceMaxEvents(shared_t *share)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void enforceMaxImages()
|
||||||
|
{
|
||||||
|
auto names = lsFilesInDir("img", ".bmp");
|
||||||
|
|
||||||
|
while (names.size() > MAX_IMAGES)
|
||||||
|
{
|
||||||
|
QFile::remove("img/" + names[0]);
|
||||||
|
|
||||||
|
names.removeFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void rdLine(const QString ¶m, const QString &line, QString *value)
|
void rdLine(const QString ¶m, const QString &line, QString *value)
|
||||||
{
|
{
|
||||||
if (line.startsWith(param))
|
if (line.startsWith(param))
|
||||||
|
|
26
src/common.h
26
src/common.h
|
@ -23,31 +23,25 @@
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QMediaPlayer>
|
|
||||||
#include <QVideoSink>
|
|
||||||
#include <QVideoFrame>
|
|
||||||
#include <QImage>
|
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
#include <opencv4/opencv2/opencv.hpp>
|
|
||||||
#include <opencv4/opencv2/videoio.hpp>
|
|
||||||
#include <opencv2/core/utils/logger.hpp>
|
|
||||||
|
|
||||||
using namespace cv;
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
#define APP_VER "3.0.t5"
|
#define APP_VER "3.0.t6"
|
||||||
#define APP_NAME "Motion Watch"
|
#define APP_NAME "Motion Watch"
|
||||||
#define APP_BIN "mow"
|
#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 40
|
||||||
|
|
||||||
struct evt_t
|
struct evt_t
|
||||||
{
|
{
|
||||||
QString evName;
|
QDateTime timeStamp;
|
||||||
QStringList srcPaths;
|
QString imgPath;
|
||||||
QImage thumbnail;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct shared_t
|
struct shared_t
|
||||||
|
@ -82,16 +76,18 @@ struct shared_t
|
||||||
int evInd;
|
int evInd;
|
||||||
};
|
};
|
||||||
|
|
||||||
QByteArray genMD5(const QString &path);
|
|
||||||
QByteArray genMD5(const QByteArray &bytes);
|
|
||||||
QString getParam(const QString &key, const QStringList &args);
|
QString getParam(const QString &key, const QStringList &args);
|
||||||
QStringList lsFilesInDir(const QString &path, const QString &ext = QString());
|
QStringList lsFilesInDir(const QString &path, const QString &ext = QString());
|
||||||
QStringList lsDirsInDir(const QString &path);
|
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(const QString &filePath, shared_t *share);
|
||||||
bool rdConf(shared_t *share);
|
bool rdConf(shared_t *share);
|
||||||
void rdLine(const QString ¶m, const QString &line, QString *value);
|
void rdLine(const QString ¶m, const QString &line, QString *value);
|
||||||
void rdLine(const QString ¶m, const QString &line, int *value);
|
void rdLine(const QString ¶m, const QString &line, int *value);
|
||||||
void enforceMaxEvents(shared_t *share);
|
void enforceMaxEvents(shared_t *share);
|
||||||
|
void enforceMaxImages();
|
||||||
|
|
||||||
class MultiInstance : public QObject
|
class MultiInstance : public QObject
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,83 +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 "mo_detect.h"
|
|
||||||
|
|
||||||
MoDetect::MoDetect(shared_t *share, QObject *parent) : QVideoSink(parent)
|
|
||||||
{
|
|
||||||
shared = share;
|
|
||||||
|
|
||||||
connect(this, &MoDetect::videoFrameChanged, this, &MoDetect::newFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MoDetect::stop()
|
|
||||||
{
|
|
||||||
baseImg = QImage(); gap = 0; shared->maxScore = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MoDetect::play(const QString &path)
|
|
||||||
{
|
|
||||||
filePath = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MoDetect::newFrame()
|
|
||||||
{
|
|
||||||
if (!baseImg.isNull())
|
|
||||||
{
|
|
||||||
baseImg = videoFrame().toImage().convertToFormat(QImage::Format_Grayscale8);
|
|
||||||
}
|
|
||||||
else if (shared->frameGap > gap)
|
|
||||||
{
|
|
||||||
gap++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
gap = 0;
|
|
||||||
|
|
||||||
auto nextImg = videoFrame().toImage();
|
|
||||||
auto nextImgGray = nextImg.convertToFormat(QImage::Format_Grayscale8);
|
|
||||||
auto score = 0;
|
|
||||||
|
|
||||||
if (diff(baseImg, nextImgGray, &score))
|
|
||||||
{
|
|
||||||
shared->skipCmd = true;
|
|
||||||
|
|
||||||
if (!shared->curEvent.srcPaths.contains(filePath))
|
|
||||||
{
|
|
||||||
shared->curEvent.srcPaths.append(filePath);
|
|
||||||
|
|
||||||
if (shared->maxScore <= score)
|
|
||||||
{
|
|
||||||
shared->maxScore = score;
|
|
||||||
|
|
||||||
shared->curEvent.thumbnail = nextImg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MoDetect::diff(QImage &base, QImage &next, int *score)
|
|
||||||
{
|
|
||||||
Mat baseMat = Mat(base.height(), base.width(), CV_8U, base.bits(), base.bytesPerLine());
|
|
||||||
Mat nextMat = Mat(next.height(), next.width(), CV_8U, next.bits(), next.bytesPerLine());
|
|
||||||
Mat diffMat;
|
|
||||||
|
|
||||||
absdiff(baseMat, nextMat, diffMat);
|
|
||||||
threshold(diffMat, diffMat, shared->pixThresh, 255, THRESH_BINARY);
|
|
||||||
|
|
||||||
*score = countNonZero(diffMat);
|
|
||||||
|
|
||||||
detLog("diff_score: " + QString::number(*score) + " thresh: " + QString::number(shared->imgThresh), shared);
|
|
||||||
|
|
||||||
return *score >= shared->imgThresh;
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
#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"
|
|
||||||
|
|
||||||
class MoDetect : public QVideoSink
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
QImage baseImg;
|
|
||||||
QString filePath;
|
|
||||||
shared_t *shared;
|
|
||||||
int gap;
|
|
||||||
|
|
||||||
bool diff(QImage &base, QImage &next, int *score);
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
|
|
||||||
void newFrame();
|
|
||||||
|
|
||||||
public:
|
|
||||||
|
|
||||||
void play(const QString &path);
|
|
||||||
void stop();
|
|
||||||
|
|
||||||
explicit MoDetect(shared_t *share, QObject *parent = nullptr);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // MO_DETECT_H
|
|
Loading…
Reference in New Issue
Block a user