JustMotion/src/camera.cpp

565 lines
13 KiB
C++
Raw Normal View History

// 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 = 10000;
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()
{
funcMsg("enter: Loop::exec()");
if (loopTimer->interval() != heartBeat * 1000)
{
loopTimer->start(heartBeat * 1000);
}
funcMsg("enter: Loop::exit()");
return shared->retCode == 0;
}
RecLoop::RecLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
{
recProc = 0;
imgProc = 0;
}
void RecLoop::init()
{
funcMsg("enter: RecLoop::init()");
recProc = new QProcess(this);
imgProc = new QProcess(this);
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &RecLoop::term);
updateCmd();
Loop::init();
funcMsg("exit: RecLoop::init()");
}
void RecLoop::updateCmd()
{
funcMsg("enter: 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";
recArgs << "-rtsp_transport" << "tcp";
recArgs << "-stimeout" << "3000";
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 << "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);
curUrl = shared->recordUrl;
funcMsg("exit: RecLoop::updateCmd()");
}
void RecLoop::term()
{
funcMsg("enter: RecLoop::term()");
recProc->kill();
recProc->waitForFinished();
imgProc->kill();
imgProc->waitForFinished();
funcMsg("exit: RecLoop::term()");
}
void RecLoop::reset()
{
funcMsg("enter: RecLoop::reset()");
recLog("--rec_and_img_cmds_resetting--", shared);
term();
updateCmd();
funcMsg("exit: RecLoop::reset()");
}
void RecLoop::procError(const QString &desc, QProcess *proc)
{
funcMsg("enter: RecLoop::procError()");
if (proc->isOpen() && proc->state() == QProcess::NotRunning)
{
auto errBlob = QString(proc->readAllStandardError());
auto errLines = errBlob.split('\n');
if (!errLines.isEmpty())
{
for (auto &&line : errLines)
{
recLog(desc + "_cmd_stderr: " + line, shared);
}
}
}
funcMsg("exit: RecLoop::procError()");
}
void RecLoop::startProc(const QString &desc, QProcess *proc)
{
funcMsg("enter: RecLoop::startProc()");
if (proc->state() == QProcess::NotRunning)
{
proc->start();
if (proc->waitForStarted())
{
recLog(desc + "_cmd_start: ok", shared);
}
else
{
recLog(desc + "_cmd_start: fail", shared);
procError(desc, proc);
}
}
funcMsg("exit: RecLoop::startProc()");
}
bool RecLoop::exec()
{
funcMsg("enter: RecLoop::exec()");
procError("img", imgProc);
procError("rec", recProc);
if (curUrl != shared->recordUrl)
{
recLog("a change in the recording URL was detected.", shared);
reset();
}
startProc("img", imgProc);
startProc("rec", recProc);
funcMsg("exit: RecLoop::exec()");
return Loop::exec();
}
Upkeep::Upkeep(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) {}
bool Upkeep::exec()
{
funcMsg("enter: 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);
funcMsg("exit: Upkeep::exec()");
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()
{
funcMsg("enter: EventLoop::exec()");
if (cycles * 2 >= shared->evMaxSecs)
{
vidList.removeDuplicates();
if (!vidList.size() > 1)
{
recLog("attempting write out of event: " + name, shared);
if (wrOutVod(name, vidList))
{
genHTMLvod(name);
QProcess proc;
QStringList args;
args << "convert";
args << imgPath;
args << "events/" + name + ".jpg";
proc.start("magick", args);
proc.waitForFinished();
}
}
cycles = 0;
highScore = 0;
vidList.clear();
}
else
{
cycles += 1;
while (!shared->recList.isEmpty())
{
shared->recMutex.lock();
auto event = shared->recList[0];
auto maxFiles = shared->evMaxSecs / 2;
// there's 2 secs in each hls segment
if (highScore < event.score)
{
name = event.timeStamp.toString(DATETIME_FMT);
imgPath = event.imgPath;
highScore = event.score;
}
vidList.append(backwardFacingFiles("live", ".ts", event.timeStamp, maxFiles / 2));
vidList.removeLast();
vidList.append(forwardFacingFiles("live", ".ts", event.timeStamp, maxFiles / 2));
shared->recList.removeFirst();
shared->recMutex.unlock();
}
}
funcMsg("exit: EventLoop::exec()");
return Loop::exec();
}
bool EventLoop::wrOutVod(const QString &name, const QStringList &vids)
{
funcMsg("enter: EventLoop::wrOutVod()");
auto cnt = 0;
auto concat = name + ".tmp";
auto ret = false;
QFile file(concat);
file.open(QFile::WriteOnly);
for (auto &&vid : vids)
{
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);
}
funcMsg("exit: EventLoop::wrOutVod()");
return ret;
}
DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
{
pcTimer = 0;
heartBeat = 2;
delayCycles = 8; // this will be used to delay the
// actual start of DetectLoop by
// 16secs.
}
void DetectLoop::init()
{
funcMsg("enter: DetectLoop::init()");
pcTimer = new QTimer(this);
mod = false;
connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak);
resetTimers();
funcMsg("exit: DetectLoop::init()");
Loop::init();
}
void DetectLoop::resetTimers()
{
pcTimer->start(shared->postSecs * 1000);
}
void DetectLoop::pcBreak()
{
funcMsg("enter: 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());
}
}
funcMsg("exit: DetectLoop::pcBreak()");
mod = false;
}
bool DetectLoop::exec()
{
funcMsg("enter: DetectLoop::exec()");
if (delayCycles > 0)
{
delayCycles -= 1;
detLog("spec: detection cycle skipped. cycles left to be skipped: " + QString::number(delayCycles), shared);
}
else
{
auto curDT = QDateTime::currentDateTime();
auto images = backwardFacingFiles("img", ".bmp", curDT, 6);
if (images.size() < 2)
{
detLog("wrn: didn't pick up enough image files from the image stream. number of files: " + QString::number(images.size()), shared);
detLog(" will try again on the next loop.", shared);
}
else
{
QProcess extComp;
QStringList args;
auto pos = images.size() - 1;
args << "compare";
args << "-metric" << "FUZZ";
args << images[pos - 1];
args << images[pos];
args << "/dev/null";
extComp.start("magick", args);
extComp.waitForFinished();
QString output = extComp.readAllStandardError();
output = output.left(output.indexOf(' '));
detLog(extComp.program() + " " + args.join(" ") + " --result: " + output, shared);
auto score = output.toFloat();
if (score >= shared->imgThresh)
{
detLog("--threshold_breached: " + QString::number(shared->imgThresh), shared);
evt_t event;
event.timeStamp = curDT;
event.score = score;
event.imgPath = images[pos];
shared->recMutex.lock();
shared->recList.append(event); mod = true;
shared->recMutex.unlock();
}
}
}
funcMsg("exit: DetectLoop::exec()");
return Loop::exec();
}