added a buffer directory so the actual work of image frame capture and live video recording can be moved off of main storage. also removed installation of the apache2 http server from install.sh to make this app more agnostic and leave it up to the end user to install what ever web server they want.
489 lines
12 KiB
C++
489 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.buffPath = QDir::tempPath();
|
|
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))
|
|
{
|
|
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()
|
|
{
|
|
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();
|
|
}
|