2023-05-15 15:29:47 -04:00
|
|
|
// 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.pixThresh = 50;
|
|
|
|
shared.imgThresh = 800;
|
|
|
|
shared.maxEvents = 40;
|
|
|
|
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";
|
|
|
|
}
|
|
|
|
|
2023-05-17 15:06:58 -04:00
|
|
|
int Camera::start(const QStringList &args)
|
2023-05-15 15:29:47 -04:00
|
|
|
{
|
|
|
|
auto ret = false;
|
|
|
|
|
|
|
|
shared.conf = getParam("-c", args);
|
|
|
|
|
|
|
|
if (rdConf(&shared))
|
|
|
|
{
|
|
|
|
QDir("live").removeRecursively();
|
|
|
|
|
2023-05-17 15:06:58 -04:00
|
|
|
auto thr1 = new QThread(QCoreApplication::instance());
|
|
|
|
auto thr2 = new QThread(QCoreApplication::instance());
|
|
|
|
auto thr3 = new QThread(QCoreApplication::instance());
|
|
|
|
auto thr4 = new QThread(QCoreApplication::instance());
|
|
|
|
|
|
|
|
new RecLoop(&shared, thr1, this);
|
|
|
|
new Upkeep(&shared, thr2, this);
|
|
|
|
new EventLoop(&shared, thr3, this);
|
|
|
|
new DetectLoop(&shared, thr4, this);
|
|
|
|
|
|
|
|
thr1->start();
|
|
|
|
thr2->start();
|
|
|
|
thr3->start();
|
|
|
|
thr4->start();
|
2023-05-15 15:29:47 -04:00
|
|
|
}
|
|
|
|
|
2023-05-17 15:06:58 -04:00
|
|
|
return shared.retCode;
|
2023-05-15 15:29:47 -04:00
|
|
|
}
|
|
|
|
|
2023-05-17 15:06:58 -04:00
|
|
|
Loop::Loop(shared_t *sharedRes, QThread *thr, QObject *parent) : QObject(0)
|
2023-05-15 15:29:47 -04:00
|
|
|
{
|
|
|
|
shared = sharedRes;
|
|
|
|
heartBeat = 10;
|
2023-05-17 15:06:58 -04:00
|
|
|
|
|
|
|
connect(this, &Loop::loopSig, this, &Loop::loopSlot);
|
|
|
|
connect(thr, &QThread::started, this, &Loop::init);
|
|
|
|
connect(parent, &QObject::destroyed, this, &Loop::deleteLater);
|
|
|
|
connect(parent, &QObject::destroyed, thr, &QThread::terminate);
|
|
|
|
|
|
|
|
moveToThread(thr);
|
2023-05-15 15:29:47 -04:00
|
|
|
}
|
|
|
|
|
2023-05-17 15:06:58 -04:00
|
|
|
void Loop::init()
|
2023-05-15 15:29:47 -04:00
|
|
|
{
|
2023-05-17 15:06:58 -04:00
|
|
|
emit loopSig();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Loop::loopSlot()
|
|
|
|
{
|
|
|
|
auto ret = exec();
|
|
|
|
|
|
|
|
if (heartBeat != 0)
|
2023-05-15 15:29:47 -04:00
|
|
|
{
|
2023-05-17 15:06:58 -04:00
|
|
|
thread()->sleep(heartBeat);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ret)
|
|
|
|
{
|
|
|
|
emit loopSig();
|
2023-05-15 15:29:47 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Loop::exec()
|
|
|
|
{
|
|
|
|
return shared->retCode == 0;
|
|
|
|
}
|
|
|
|
|
2023-05-17 15:06:58 -04:00
|
|
|
RecLoop::RecLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
|
2023-05-15 15:29:47 -04:00
|
|
|
{
|
|
|
|
once = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void RecLoop::updateCmd()
|
|
|
|
{
|
|
|
|
QStringList args;
|
|
|
|
|
|
|
|
args << "-hide_banner";
|
|
|
|
args << "-i" << shared->recordUrl;
|
|
|
|
args << "-strftime" << "1";
|
|
|
|
args << "-strftime_mkdir" << "1";
|
|
|
|
args << "-hls_segment_filename" << "live/%Y-%j-%H-%M-%S.ts";
|
|
|
|
args << "-hls_flags" << "delete_segments";
|
|
|
|
args << "-y";
|
|
|
|
args << "-vcodec" << "copy";
|
|
|
|
args << "-f" << "hls";
|
|
|
|
args << "-hls_time" << "2";
|
|
|
|
args << "-hls_list_size" << "1000";
|
|
|
|
args << "stream.m3u8";
|
|
|
|
|
|
|
|
proc.setProgram("ffmpeg");
|
|
|
|
proc.setArguments(args);
|
|
|
|
|
2023-05-17 15:06:58 -04:00
|
|
|
curUrl = shared->recordUrl;
|
|
|
|
|
2023-05-15 15:29:47 -04:00
|
|
|
recLog("rec_args_updated: " + args.join(" "), shared);
|
|
|
|
}
|
|
|
|
|
|
|
|
void RecLoop::reset()
|
|
|
|
{
|
|
|
|
recLog("--rec_cmd_resetting--", shared);
|
|
|
|
|
|
|
|
proc.kill();
|
|
|
|
proc.waitForFinished();
|
|
|
|
|
|
|
|
updateCmd();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool RecLoop::exec()
|
|
|
|
{
|
2023-05-17 15:06:58 -04:00
|
|
|
auto md5 = genMD5(QString("stream.m3u8"));
|
2023-05-15 15:29:47 -04:00
|
|
|
|
|
|
|
if (once)
|
|
|
|
{
|
|
|
|
updateCmd(); once = false; streamMD5 = genMD5(QByteArray("FIRST"));
|
|
|
|
}
|
2023-05-17 15:06:58 -04:00
|
|
|
else if ((curUrl != shared->recordUrl) || (streamMD5 == md5))
|
2023-05-15 15:29:47 -04:00
|
|
|
{
|
|
|
|
reset();
|
|
|
|
}
|
|
|
|
|
2023-05-17 15:06:58 -04:00
|
|
|
auto hashLogLine = "stream_hash--prev:" + QString(streamMD5.toHex()) + "--new:" + QString(md5.toHex());
|
2023-05-15 15:29:47 -04:00
|
|
|
|
|
|
|
recLog(hashLogLine, shared);
|
|
|
|
|
|
|
|
if (proc.state() == QProcess::NotRunning)
|
|
|
|
{
|
|
|
|
proc.start();
|
|
|
|
|
|
|
|
if (proc.waitForStarted())
|
|
|
|
{
|
|
|
|
recLog("rec_cmd_start: ok", shared);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
recLog("rec_cmd_start: fail", shared);
|
|
|
|
recLog("rec_cmd_stderr: " + QString(proc.readAllStandardError()), shared);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Loop::exec();
|
|
|
|
}
|
|
|
|
|
2023-05-17 15:06:58 -04:00
|
|
|
Upkeep::Upkeep(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) {}
|
2023-05-15 15:29:47 -04:00
|
|
|
|
|
|
|
bool Upkeep::exec()
|
|
|
|
{
|
|
|
|
QDir().mkdir("live");
|
|
|
|
QDir().mkdir("events");
|
|
|
|
QDir().mkdir("logs");
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2023-05-17 15:06:58 -04:00
|
|
|
EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
|
2023-05-15 15:29:47 -04:00
|
|
|
{
|
|
|
|
heartBeat = 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool EventLoop::exec()
|
|
|
|
{
|
|
|
|
if (!shared->recList.isEmpty())
|
|
|
|
{
|
|
|
|
auto event = shared->recList[0];
|
|
|
|
|
|
|
|
recLog("attempting write out of event: " + event.evName, shared);
|
|
|
|
|
|
|
|
if (wrOutVod(event))
|
|
|
|
{
|
|
|
|
genHTMLvod(event.evName);
|
|
|
|
imwrite(QString("events/" + event.evName + ".jpg").toUtf8().data(), event.thumbnail);
|
|
|
|
}
|
|
|
|
|
|
|
|
shared->recList.removeFirst();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Loop::exec();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool EventLoop::wrOutVod(const evt_t &event)
|
|
|
|
{
|
|
|
|
auto cnt = 0;
|
|
|
|
auto concat = event.evName + ".tmp";
|
|
|
|
|
|
|
|
QFile file(concat);
|
|
|
|
|
|
|
|
file.open(QFile::WriteOnly);
|
|
|
|
|
|
|
|
for (auto i = 0; i < event.srcPaths.size(); ++i)
|
|
|
|
{
|
|
|
|
recLog("event_src: " + event.srcPaths[i], shared);
|
|
|
|
|
|
|
|
if (QFile::exists(event.srcPaths[i]))
|
|
|
|
{
|
|
|
|
file.write(QString("file '" + event.srcPaths[i] + "'\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/" + event.evName + ".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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-17 15:06:58 -04:00
|
|
|
DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
|
2023-05-15 15:29:47 -04:00
|
|
|
{
|
|
|
|
heartBeat = 0;
|
|
|
|
evId = 0;
|
|
|
|
pcId = 0;
|
2023-05-17 15:06:58 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
void DetectLoop::init()
|
|
|
|
{
|
|
|
|
thread()->usleep(200);
|
2023-05-15 15:29:47 -04:00
|
|
|
|
2023-05-17 15:06:58 -04:00
|
|
|
resetTimers(); Loop::init();
|
2023-05-15 15:29:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
void DetectLoop::resetTimers()
|
|
|
|
{
|
|
|
|
if (evId != 0) killTimer(evId);
|
|
|
|
if (pcId != 0) killTimer(pcId);
|
|
|
|
|
|
|
|
evId = startTimer(shared->evMaxSecs);
|
|
|
|
pcId = startTimer(shared->postSecs);
|
|
|
|
}
|
|
|
|
|
|
|
|
void DetectLoop::timerEvent(QTimerEvent *event)
|
|
|
|
{
|
|
|
|
if (event->timerId() == evId)
|
|
|
|
{
|
|
|
|
detLog("---EVENT_BREAK---", shared);
|
|
|
|
|
|
|
|
if (!shared->curEvent.srcPaths.isEmpty())
|
|
|
|
{
|
|
|
|
shared->curEvent.evName = QDateTime::currentDateTime().toString("yyyyMMddmmss--") + QString::number(shared->maxScore);
|
|
|
|
|
|
|
|
shared->recList.append(shared->curEvent);
|
|
|
|
|
|
|
|
detLog("motion detected in " + QString::number(shared->curEvent.srcPaths.size()) + " file(s) in " + QString::number(shared->evMaxSecs) + " secs", shared);
|
|
|
|
detLog("all video clips queued for event generation under event name: " + shared->curEvent.evName, shared);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
detLog("no motion detected in all files. none queued for event generation.", shared);
|
|
|
|
}
|
|
|
|
|
|
|
|
shared->curEvent.srcPaths.clear();
|
|
|
|
shared->curEvent.evName.clear();
|
|
|
|
shared->curEvent.thumbnail.release();
|
|
|
|
|
|
|
|
shared->maxScore = 0;
|
|
|
|
}
|
|
|
|
|
2023-05-17 15:06:58 -04:00
|
|
|
if ((event->timerId() == pcId) && (!shared->postCmd.isEmpty()))
|
2023-05-15 15:29:47 -04:00
|
|
|
{
|
|
|
|
detLog("---POST_BREAK---", shared);
|
|
|
|
|
|
|
|
if (!shared->skipCmd)
|
|
|
|
{
|
|
|
|
detLog("no motion detected, running post command: " + shared->postCmd, shared);
|
|
|
|
system(shared->postCmd.toUtf8().data());
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
shared->skipCmd = false;
|
|
|
|
|
|
|
|
detLog("motion detected, skipping the post command.", shared);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool DetectLoop::exec()
|
|
|
|
{
|
|
|
|
QFile fileIn("stream.m3u8");
|
|
|
|
QString tsPath;
|
|
|
|
|
|
|
|
if (!fileIn.open(QFile::ReadOnly))
|
|
|
|
{
|
|
|
|
detLog("err: failed to open the stream hls file for reading. reason: " + fileIn.errorString(), shared);
|
|
|
|
}
|
|
|
|
else if (fileIn.size() < 50)
|
|
|
|
{
|
|
|
|
detLog("the stream hls list is not big enough yet. waiting for more clips.", shared);
|
|
|
|
}
|
|
|
|
else if (!fileIn.seek(fileIn.size() - 50))
|
|
|
|
{
|
|
|
|
detLog("err: failed to seek to 'near end' of stream file. reason: " + fileIn.errorString(), shared);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
QString line;
|
|
|
|
|
|
|
|
do
|
|
|
|
{
|
|
|
|
line = QString::fromUtf8(fileIn.readLine());
|
|
|
|
|
|
|
|
if (line.startsWith("live/"))
|
|
|
|
{
|
|
|
|
tsPath = line;
|
|
|
|
}
|
|
|
|
|
|
|
|
} while(!line.isEmpty());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tsPath.isEmpty())
|
|
|
|
{
|
|
|
|
detLog("wrn: didn't find the latest hls clip. previous failure? waiting 5secs.", shared);
|
|
|
|
|
|
|
|
thread()->sleep(5);
|
|
|
|
}
|
|
|
|
else if (prevTs == tsPath)
|
|
|
|
{
|
|
|
|
detLog("wrn: the lastest hls clip is the same as the previous clip. is the recording loop running? waiting 5secs.", shared);
|
|
|
|
|
|
|
|
thread()->sleep(5);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
prevTs = tsPath;
|
|
|
|
|
|
|
|
if (moDetect(tsPath, thread(), shared))
|
|
|
|
{
|
|
|
|
shared->curEvent.srcPaths.append(tsPath);
|
|
|
|
|
|
|
|
shared->skipCmd = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Loop::exec();
|
|
|
|
}
|