// 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.maxScore = 0; shared.pixThresh = 50; shared.imgThresh = 8000; shared.maxEvents = 100; 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"; } int Camera::start(const QStringList &args) { shared.conf = getParam("-c", args); if (rdConf(&shared)) { QDir("live").removeRecursively(); 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); connect(this, &Loop::loopSig, this, &Loop::loopSlot); moveToThread(thr); } void Loop::init() { loopTimer = new QTimer(nullptr); connect(loopTimer, &QTimer::timeout, this, &Loop::loopSlot); loopTimer->setSingleShot(false); loopTimer->start(heartBeat * 1000); } 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) { once = true; baseListRdy = false; recProc = 0; imgProc = 0; } void RecLoop::init() { recProc = new QProcess(this); imgProc = new QProcess(this); 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 << "-hls_flags" << "delete_segments"; recArgs << "-y"; recArgs << "-vcodec" << "copy"; recArgs << "-f" << "hls"; recArgs << "-hls_time" << "2"; recArgs << "-hls_list_size" << "1000"; recArgs << "stream.m3u8"; imgArgs << "-hide_banner"; 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; recLog("rec_args_updated: " + recArgs.join(" "), shared); recLog("img_args_updated: " + imgArgs.join(" "), shared); } void RecLoop::reset() { recLog("--rec_and_img_cmds_resetting--", shared); baseListRdy = false; recProc->kill(); recProc->waitForFinished(); imgProc->kill(); imgProc->waitForFinished(); updateCmd(); } void RecLoop::startProc(const QString &desc, QProcess *proc) { if (proc->state() == QProcess::NotRunning) { proc->start(); if (proc->waitForStarted()) { recLog(desc + "_cmd_start: ok", shared); } else { recLog(desc + "_cmd_start: fail", 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(); } 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); 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); enforceMaxImages(); 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, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) { heartBeat = 5; } bool EventLoop::exec() { if (!shared->recList.isEmpty()) { auto event = shared->recList[0]; auto name = event.timeStamp.toString(DATETIME_FMT); auto vidList = backwardFacingFiles("live", ".ts", event.timeStamp, shared->evMaxSecs / 2); if (vidList.isEmpty()) { recLog("err: no backward faces files were found for event: " + name, shared); } else { vidList.removeLast(); 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(); } return Loop::exec(); } bool EventLoop::wrOutVod(const QString &name, const QStringList &vids) { auto cnt = 0; auto concat = name + ".tmp"; 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); return false; } 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(); 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, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) { pcTimer = 0; heartBeat = 3; } void DetectLoop::init() { pcTimer = new QTimer(nullptr); 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 { detLog("no motion detected, running post command: " + shared->postCmd, shared); system(shared->postCmd.toUtf8().data()); } } mod = false; } bool DetectLoop::exec() { auto curDT = QDateTime::currentDateTime(); auto images = backwardFacingFiles("img", ".bmp", curDT, 10); if (images.size() < 3) { detLog("wrn: didn't pick up enough image files from the image stream. number of files: " + QString::number(images.size()), shared); } else { QProcess extComp; QStringList args; 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) { detLog("--threshold_breached: " + QString::number(shared->imgThresh), shared); evt_t event; event.timeStamp = curDT; event.imgPath = images[2]; shared->recList.append(event); mod = true; } } return Loop::exec(); }