501 lines
12 KiB
C++
501 lines
12 KiB
C++
// 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 (!vidList.isEmpty())
|
|
{
|
|
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();
|
|
}
|
|
|
|
shared->recMutex.lock();
|
|
|
|
QList<int> rmIndx;
|
|
|
|
for (auto i = 0; i < shared->recList.size(); ++i)
|
|
{
|
|
auto event = &shared->recList[i];
|
|
|
|
if (highScore < event->score)
|
|
{
|
|
name = event->timeStamp.toString(DATETIME_FMT);
|
|
imgPath = event->imgPath;
|
|
highScore = event->score;
|
|
}
|
|
|
|
if (event->queAge >= (shared->evMaxSecs / heartBeat))
|
|
{
|
|
auto maxSecs = shared->evMaxSecs / 2;
|
|
// half the maxsecs value to get front-back half secs
|
|
|
|
auto backFiles = backwardFacingFiles("live", ".ts", event->timeStamp, maxSecs);
|
|
auto frontFiles = forwardFacingFiles("live", ".ts", event->timeStamp, maxSecs);
|
|
|
|
vidList.append(backFiles + frontFiles);
|
|
rmIndx.append(i);
|
|
}
|
|
else
|
|
{
|
|
event->queAge += heartBeat;
|
|
}
|
|
}
|
|
|
|
for (auto i : rmIndx)
|
|
{
|
|
shared->recList.removeAt(i);
|
|
}
|
|
|
|
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 = 12; // this will be used to delay the
|
|
// actual start of DetectLoop by
|
|
// 24secs.
|
|
}
|
|
|
|
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];
|
|
event.queAge = 0;
|
|
|
|
shared->recMutex.lock();
|
|
shared->recList.append(event); mod = true;
|
|
shared->recMutex.unlock();
|
|
}
|
|
}
|
|
}
|
|
|
|
return Loop::exec();
|
|
}
|