// 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)) { auto thr1 = new QThread(nullptr); auto thr2 = new QThread(nullptr); new EventLoop(&shared, thr1, nullptr); new DetectLoop(&shared, thr2, nullptr); thr1->start(); thr2->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; } EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) { heartBeat = 2; } bool EventLoop::wrOutVod(const evt_t &event) { auto ret = false; auto cnt = 0; auto concat = shared->buffPath + "/live/" + event.timeStamp + ".ctmp"; QFile file(concat, this); file.open(QFile::WriteOnly); for (auto &&vid : event.vidList) { QTextStream(stdout) << "event_src: " << vid << Qt::endl; if (QFile::exists(vid)) { file.write(QString("file '" + vid + "'\n").toUtf8()); cnt++; } else { QTextStream(stdout) << "warning: the event hls clip does not exists." << Qt::endl; } } file.close(); if (cnt == 0) { QTextStream(stderr) << "err: none of the event hls clips exists, cancelling write out." << Qt::endl; QFile::remove(concat); } else { QStringList args; args << "-f"; args << "concat"; args << "-safe" << "0"; args << "-i" << concat; args << "-c" << "copy"; args << shared->recPath + "/" + event.timeStamp + shared->recExt; if (QProcess::execute("ffmpeg", args) == 0) { ret = true; } QFile::remove(concat); } return ret; } bool EventLoop::exec() { enforceMaxEvents(shared); enforceMaxImages(shared); enforceMaxClips(shared); if (!shared->recList.isEmpty()) { auto event = shared->recList.takeFirst(); QTextStream(stdout) << "attempting write out of event: " << event.timeStamp << Qt::endl; if (wrOutVod(event)) { QStringList args; args << "convert"; args << event.imgPath; args << shared->recPath + "/" + event.timeStamp + shared->thumbExt; QProcess::execute("magick", args); } } return Loop::exec(); } DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) { pcTimer = 0; heartBeat = 2; } void DetectLoop::init() { pcTimer = new QTimer(this); eventQue.queAge = 0; eventQue.score = 0; eventQue.inQue = 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()) { QTextStream(stdout) << "---POST_BREAK---" << Qt::endl; if (eventQue.inQue) { QTextStream(stdout) << "motion detected, skipping the post command." << Qt::endl; } else { QTextStream(stdout) << "no motion detected, running post command: " << shared->postCmd << Qt::endl; auto args = parseArgs(shared->postCmd.toUtf8(), -1); if (args.isEmpty()) { QTextStream(stderr) << "err: did not parse an executable from the post command line." << Qt::endl; } else { QProcess::execute(args[0], args.mid(1)); } } } } float DetectLoop::getFloatFromExe(const QByteArray &line) { QString strLine(line); QString strNum; for (auto chr : strLine) { if (chr.isDigit() || (chr == '.')) { strNum.append(chr); } else { break; } } auto ok = false; auto res = strNum.toFloat(&ok); if (!ok || strNum.isEmpty()) { QTextStream(stderr) << "err: the image comp command returned unexpected output and couldn't be converted to float." << Qt::endl; QTextStream(stderr) << " raw output: " << line << Qt::endl; } return res; } 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; } QStringList DetectLoop::buildSnapArgs(const QString &vidSrc, const QString &imgPath) { QStringList ret; ret.append("-hide_banner"); ret.append("-y"); ret.append("-i"); ret.append(vidSrc); ret.append("-frames:v"); ret.append("1"); ret.append(imgPath); return ret; } bool DetectLoop::exec() { if (eventQue.inQue) { eventQue.queAge += heartBeat; } auto clips = lsFilesInDir(shared->buffPath + "/live", shared->streamExt); if (clips.size() < 2) { QTextStream(stdout) << "warning: didn't pick up enough clips files from the video stream. number of files: " << QString::number(clips.size()) << Qt::endl; QTextStream(stdout) << " will try again on the next loop." << Qt::endl; } else { auto vidAPath = shared->buffPath + "/live/" + clips[clips.size() - 2]; auto vidBPath = shared->buffPath + "/live/" + clips[clips.size() - 1]; auto imgAPath = shared->buffPath + "/img/" + QFileInfo(vidAPath).baseName() + ".bmp"; auto imgBPath = shared->buffPath + "/img/" + QFileInfo(vidBPath).baseName() + ".bmp"; auto snapArgsA = buildSnapArgs(vidAPath, imgAPath); auto snapArgsB = buildSnapArgs(vidBPath, imgBPath); auto compArgs = buildArgs(imgAPath, imgBPath); if (compArgs.isEmpty()) { QTextStream(stderr) << "err: could not parse a executable name from img_comp_cmd: " << shared->compCmd << Qt::endl; } else { QProcess::execute("ffmpeg", snapArgsA); QProcess::execute("ffmpeg", snapArgsB); if (QFile::exists(imgAPath) && QFile::exists(imgBPath)) { QProcess extComp; extComp.start(compArgs[0], compArgs.mid(1)); extComp.waitForFinished(); float score = 0; if (shared->outputType == "stdout") { score = getFloatFromExe(extComp.readAllStandardOutput()); } else { score = getFloatFromExe(extComp.readAllStandardError()); } QTextStream(stdout) << compArgs.join(" ") << " --result: " << QString::number(score) << Qt::endl; if (eventQue.inQue) { if (eventQue.score < score) { eventQue.score = score; eventQue.imgPath = imgBPath; } eventQue.vidList.append(vidAPath); eventQue.vidList.append(vidBPath); if (eventQue.queAge >= shared->evMaxSecs) { eventQue.inQue = false; shared->recList.append(eventQue); eventQue.imgPath.clear(); eventQue.vidList.clear(); eventQue.timeStamp.clear(); eventQue.score = 0; eventQue.queAge = 0; } } else if (score >= shared->imgThresh) { QTextStream(stdout) << "--threshold_breached: " << QString::number(shared->imgThresh) << Qt::endl; eventQue.score = score; eventQue.imgPath = imgBPath; eventQue.inQue = true; eventQue.queAge = 0; eventQue.timeStamp = QDateTime::currentDateTime().toString(DATETIME_FMT); eventQue.vidList.clear(); eventQue.vidList.append(vidAPath); eventQue.vidList.append(vidBPath); } } } } return Loop::exec(); }