// 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"; } bool Camera::start(const QStringList &args) { auto ret = false; shared.conf = getParam("-c", args); if (rdConf(&shared)) { QDir("live").removeRecursively(); } return ret; } Loop::Loop(shared_t *sharedRes, QObject *parent) : QObject(parent) { shared = sharedRes; heartBeat = 10; } void Loop::loop() { while (exec()) { if (heartBeat != 0) { thread()->sleep(heartBeat); } } } bool Loop::exec() { return shared->retCode == 0; } RecLoop::RecLoop(shared_t *sharedRes, QObject *parent) : Loop(sharedRes, parent) { 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); recLog("rec_args_updated: " + args.join(" "), shared); } void RecLoop::reset() { recLog("--rec_cmd_resetting--", shared); proc.kill(); proc.waitForFinished(); updateCmd(); } bool RecLoop::exec() { auto args = proc.arguments(); auto md5 = genMD5(QString("stream.m3u8")); if (once) { updateCmd(); once = false; streamMD5 = genMD5(QByteArray("FIRST")); } else if ((args[3] != shared->recordUrl) || (streamMD5 == md5)) { reset(); } auto hashLogLine = "stream_hash prev:" + QString(streamMD5.toHex()) + " new:" + QString(md5.toHex()); 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(); } Upkeep::Upkeep(shared_t *sharedRes, QObject *parent) : Loop(sharedRes, parent) {} 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(); } EventLoop::EventLoop(shared_t *sharedRes, QObject *parent) : Loop(sharedRes, parent) { 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; } } DetectLoop::DetectLoop(shared_t *sharedRes, QObject *parent) : Loop(sharedRes, parent) { heartBeat = 0; evId = 0; pcId = 0; resetTimers(); } 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; } if (event->timerId() == pcId) { 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(); }