// 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) {} int Camera::start(const QStringList &args) { if (rdConf(getParam("-c", args), &shared)) { QDir().mkpath(shared.outDir); QDir().mkpath(shared.tmpDir); QDir().mkpath(shared.outDir + "/events"); QDir().mkpath(shared.tmpDir + "/live"); QDir().mkpath(shared.tmpDir + "/logs"); QDir().mkpath(shared.tmpDir + "/img"); touch(shared.tmpDir + "/index.html"); touch(shared.tmpDir + "/stream.html"); touch(shared.tmpDir + "/stream.m3u8"); QFile::link(shared.tmpDir + "/live", shared.outDir + "/live"); QFile::link(shared.tmpDir + "/logs", shared.outDir + "/logs"); QFile::link(shared.tmpDir + "/img", shared.outDir + "/img"); QFile::link(shared.tmpDir + "/index.html", shared.outDir + "/index.html"); QFile::link(shared.tmpDir + "/stream.html", shared.outDir + "/stream.html"); QFile::link(shared.tmpDir + "/stream.m3u8", shared.outDir + "/stream.m3u8"); QFile::link(shared.outDir + "/events", shared.tmpDir + "/events"); if (!QDir::setCurrent(shared.tmpDir)) { QTextStream(stderr) << "err: failed to change/create the current working directory to camera folder: '" << shared.outDir << "' does it exists?" << Qt::endl; shared.retCode = ENOENT; } else { 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->buffPath, 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 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); auto args = parseArgs(shared->postCmd.toUtf8(), -1); if (args.isEmpty()) { detLog("err: did not parse an executable from the post command line.", shared); } else { QProcess::execute(args[0], args.mid(1)); } } } mod = false; } float DetectLoop::getFloatFromExe(const QByteArray &line) { QString strLine(line); QString strNum; for (auto chr : strLine) { if (chr.isDigit() || (chr == '.')) { strNum.append(chr); } else { break; } } return strNum.toFloat(); } QStringList DetectLoop::buildArgs(const QString &prev, const QString &next) { auto args = parseArgs(shared->compCmd.toUtf8(), -1); for (auto i = 0; i < args.size(); ++i) { if (args[i] == PREV_IMG) args[i] = prev; if (args[i] == NEXT_IMG) args[i] = next; } return args; } bool DetectLoop::exec() { if (delayCycles > 0) { delayCycles -= 1; detLog("delay: 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 { auto pos = images.size() - 1; auto args = buildArgs(images[pos - 1], images[pos]); if (args.isEmpty()) { detLog("err: could not parse a executable name from img_comp_cmd: " + shared->compCmd, shared); } else { QProcess extComp; extComp.start(args[0], args.mid(1)); extComp.waitForFinished(); float score = 0; auto ok = true; if (shared->outputType == "stdout") { score = getFloatFromExe(extComp.readAllStandardOutput()); } else if (shared->outputType == "stderr") { score = getFloatFromExe(extComp.readAllStandardError()); } else { ok = false; } if (!ok) { detLog("err: img_comp_out: " + shared->outputType + " is not valid. it must be 'stdout' or 'stderr'" , shared); } else { detLog(extComp.program() + " " + args.join(" ") + " --result: " + QString::number(score), shared); 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(); }