Compare commits

..

24 Commits

Author SHA1 Message Date
Zii
ff5f95f445 v3.0.0
The app in it's current iteration is stable in testing. Releasing
to master.
2023-06-18 09:06:12 -04:00
Zii
4300651a52 v3.0.t19
-the ffmpeg auto refresh logic is working, just need to shorten
 the frequency by setting recloop's heartbeat to default.

-added hls flag "omit_endlist" so the auto refresh doesn't cut off
 the stream at the web browser end.
2023-06-14 20:57:18 -04:00
Zii
71fc5b0bc2 v3.0.t18
-ffmpeg still stalls even with tcp timeout parameters in place.
 added self end -t option to match the heart beat of RecLoop.
 doing this auto re-fresh ffmpeg every 30mins and should prevent
 stall. for some reason ffmpeg just can't run long term without
 stalling.
2023-06-13 20:58:45 -04:00
Zii
a493c7da5d v3.0.t17
-finally fixed the crashing issue. it turns out EventLoop::exec()
 was calling vidList.removeLast() on an empty list every now and
 then, causing qt to fatal assert. I removed the line since it is
 not even needed.
2023-06-11 09:09:49 -04:00
Zii
510bdda64c v3.0.t16
-the debug code from the previous commit help narrow down what
 function is causing the crash but that's it, I need more. decided
 add the debug build type to cmake and will use gdb to help with
 debugging.

-decided to remove the debug code since I'll use gdb as the long
 term solution going forward.
2023-06-11 07:41:57 -04:00
Zii
9e7d8ee1ee v3.0.t16
-the app is still crashing, adding a bunch of console output to
 help with debug.
2023-06-10 22:44:38 -04:00
Zii
09a0f030ac v3.0.t16
-added mutex thread protection. getting sigmentation faults on the
 test machine. added thread protection on various shared
 parameters in hope that it fix the random crashing issue.
2023-06-10 21:51:39 -04:00
Zii
51218198b5 v3.0.t15
-changed up the logic in EventLoop to better honer maxEventSecs
 and pick the highest scoring event as the event name and
 thumbnail.
2023-06-10 09:45:26 -04:00
Zii
baf0b86610 v3.0.t14
-didn't properly remove use of evhist from the last commit. it is
 truely removed now.

-added delay cycles to detectLoop is motion was detected to
 prevent some event overlap.
2023-06-09 19:59:16 -04:00
Zii
4b4c2649b8 v3.0.t13
-removed use of evtHist. will instead allow eventLoop to que up
 duplicate live video clips and then remove later using
 QStringList::removeDuplicates().

-changed up the ffmpeg commands to utilize tcp and re-added a tcp
 timeout argument, removing the need for command stall checking.

-added logic to pick the snapshot with the highest diff score as
 the event thumbnail.
2023-06-09 16:24:32 -04:00
Zii
16312a93f5 v3.0.t12
-added a termination slot to RecLoop that will kill the long term
 ffmpeg commands and connected it to 'aboutToQuit' signal. this is
 expected to kill the ffmpeg commands properly when quiting the
 main process.
2023-06-01 16:59:37 -04:00
Zii
4bf260c0ae v3.0.t11
-max_event_secs is not being honered correctly. EventLoop was not
 calculating the amount the hls clips to grab from live correctly.
 chanaged it to properly calculate file count based on hls segment
 size.

-updated the documentation as the current version nears stable
 release.
2023-06-01 16:11:58 -04:00
Zii
71df7d1eb5 v3.0.t10
-turns out the previous statment on the previous commit is
 incorrect. it is every possible to overlap events. to
 mitigate this, evtHist was added to shared_t to track
 recently copied source vids and remove them from the
 event queued to be written.
2023-05-30 20:03:22 -04:00
Zii
732a604c24 v3.0.t9
-removed the delay after motion was detected. it was not having
 the desired effect and after more thought event overlap would
 be impossible anyway.

-the test cameras are still picking up motion during the post
 command. adjusted the after command delay to see if that
 helps.

-reduced the DetectLoop heartbeat from 3 to 2 to better match
 the record loop's cadence.
2023-05-29 20:06:19 -04:00
Zii
de24a94bd4 v3.0.t8
-the test cameras are picking up motion as post command is running.
 added a delay increment to DetectLoop in hope to fix this.

-removed the upkeep log since it doesn't really provide any useful
 information.

-adjusted the default motion score again.

-reduced the amount of image files DetectLoop needs from 3 to 2.
2023-05-29 17:43:31 -04:00
Maurice ONeal
b445906403 v3.0.t7
-added a dely to DetectLoop after a positive motion detection to
 prevent motion event overlap.

-moved the 2 image diff pair compair to proper "end of array" in
 DetectLoop.

-increased the size of the image stream so queded up events will
 be able generate thumbnails properly.

-cleaned off a bunch of unused parameters in the code.

-adjusted the default motion sensitivity after real world
 testing.

-added libfuse-dev to setup.sh since imagemagic needs that to
 operate.
2023-05-27 09:33:14 -04:00
Maurice ONeal
4134d4befb v3.0.t6
The Qt approach to grabbing frames from the live stream was also
a failure.

- decided to switch to a combination ffmpeg and imagemagic was
  external commands to do motion detection. this approach
  elimates the need for opencv altogeather so it was removed
  from the project. system resource usage appears to be decent
  and perhaps better than opencv.
2023-05-26 16:12:53 -04:00
Maurice ONeal
f850ec6a46 v3.0.t5
-fixed all loop structors used throughout the app. they were
 running too fast.

-added more log lines to aid with debug.
2023-05-21 09:34:57 -04:00
Maurice ONeal
496bac7d7e v3.0.t4
I'm going to test a move away from opencv's videoio module.
Videoio simply refuses to open any video file even with
FFMPEG builtin. I tested old v2.2 code and even that failed
on a fresh install of ubuntu sever so this tells me an
update on opencv's side broke something.

This issue is not new and frankly I'm tired of chasing it.
I'm giving QT's QMediaPlayer a try to see how it works out.
Will still need opencv for the absdiff and threshold
functions, otherwise I would have dropped the API
altogeather.

Now that the app has QT::Multimedia, QT6 is now the minimum
version it will support. CMakeList.txt and the setup script
updated accordingly.
2023-05-20 19:18:55 -04:00
Zii
40ef014e0f v3.0.t3
Got the app up to "not failing immediately" state.

However, for some reason DetectLoop is failing hard via opencv
being unable to open the stream clips.

I'll continue deep diving this. For now everything else works.
2023-05-17 21:10:39 +00:00
Zii
80e6980d9e Fixed a compile error. 2023-05-17 19:40:09 +00:00
Maurice ONeal
bbad30a5b0 v3.0.t2
Added the much need code in Camera object to actual start all of
the threads.

Added multi instance support via the -d option.

Made the Loop object loop structure slot-signal compatible so
all objects using it can interupt the main loop to run other
slots.
2023-05-17 15:06:58 -04:00
Maurice ONeal
b5ebbace12 v3.0.t1
Fixed some compile errors and currently debugging some issue with
setup.sh.
2023-05-15 19:39:29 -04:00
Maurice ONeal
fa834aba6c v3.0.t1
Completely re-written the project to use the QT API. By using Qt,
I've open up use of useful tools like QCryptographicHash, QString,
QByteArray, QFile, etc.. In the future I could even make use of
slots/signals. The code is also in general much more readable and
thread management is by far much easier.

General operation of the app should be the same, this commit just
serves as a base for the migration over to QT.
2023-05-15 15:29:47 -04:00
17 changed files with 1059 additions and 855 deletions

2
.gitignore vendored
View File

@ -57,3 +57,5 @@ compile_commands.json
# Build folders # Build folders
/.build-mow /.build-mow
/.build-opencv
/src/opencv

View File

@ -1,7 +1,31 @@
cmake_minimum_required(VERSION 2.8.12) cmake_minimum_required(VERSION 3.14)
project( MotionWatch )
find_package( OpenCV REQUIRED ) project(MotionWatch LANGUAGES CXX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pthread")
include_directories( ${OpenCV_INCLUDE_DIRS} ) set(CMAKE_INCLUDE_CURRENT_DIR ON)
add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp src/web.cpp src/logger.cpp )
target_link_libraries( mow ${OpenCV_LIBS} ) set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_BUILD_TYPE Debug)
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED)
add_executable(mow
src/main.cpp
src/common.h
src/common.cpp
src/web.h
src/web.cpp
src/logger.h
src/logger.cpp
src/camera.h
src/camera.cpp
)
target_link_libraries(mow Qt${QT_VERSION_MAJOR}::Core ${OpenCV_LIBS})

View File

@ -4,22 +4,18 @@ Motion Watch is a video surveillance application that monitors the video feeds
of an IP camera and records only footage that contains motion. The main of an IP camera and records only footage that contains motion. The main
advantage of this is reduced storage requirements as opposed to continuous advantage of this is reduced storage requirements as opposed to continuous
recording because only video footage of interest is recorded to storage. recording because only video footage of interest is recorded to storage.
The entire app is designed to operate on just one camera but multiple instances
of this app can be used to operate multiple cameras.
### Usage ### ### Usage ###
``` ```
Usage: mow <argument> Usage: mow <argument>
-h : display usage information about this application. -h : display usage information about this application.
-c : path to the config file(s). -c : path to the config file used to run a single camera instance.
-v : display the current version. -d : path to a directory that can contain multiple config files.
each file found in the directory will be used to run a
note: multiple -c config files can be passed, reading from left camera instance.
to right. any conflicting values between the files will -v : display the current version.
have the latest value from the latest file overwrite the
the earliest.
``` ```
### Config File ### ### Config File ###
@ -49,30 +45,20 @@ cam_name = cam-1
# name will also be used to as the base directory in web_root. if not # name will also be used to as the base directory in web_root. if not
# defined, the name of the config file will be used. # defined, the name of the config file will be used.
# #
pix_thresh = 150 max_event_secs = 30
# this value tells the application how far different the pixels need to be
# before the pixels are actually considered different. think of this as
# pixel diff sensitivity, the higher the value the lesser the sensitivity.
# maximum is 255.
#
img_thresh = 80000
# this indicates how many pixels need to be different in between frames
# before it is considered motion. any video clips found with frames
# exceeding this value will be copied from live footage to event footage.
#
frame_gap = 10
# this is the amount of frames in between the comparison frames to check
# for pixel differences. the higher the value, the lower the cpu over
# head, however it does lower motion detection accuracy.
#
max_events = 40
# this indicates the maximum amount of motion event video clips to keep
# before deleting the oldest clip.
#
max_event_secs = 10
# this is the maximum amount of secs of video footage that can be # this is the maximum amount of secs of video footage that can be
# recorded in a motion event. # recorded in a motion event.
# #
img_thresh = 8000
# this application uses 'magick compare' to score the differences between
# two, one second gapped snapshots of the camera stream. any image pairs
# that score greater than this value is considered motion and queues up
# max_event_secs worth of hls clips to be written out as a motion event.
#
max_events = 100
# this indicates the maximum amount of motion event video clips to keep
# before deleting the oldest clip.
#
post_secs = 60 post_secs = 60
# this is the amount of seconds to wait before running the command # this is the amount of seconds to wait before running the command
# defined in post_cmd. the command will not run if motion was detected # defined in post_cmd. the command will not run if motion was detected
@ -105,8 +91,7 @@ web_font = courier
### Setup/Build/Install ### ### Setup/Build/Install ###
This application is currently only compatible with a Linux based operating This application is currently only compatible with a Linux based operating
systems that are capable of installing opencv. The following 3 scripts make systems that are capable of installing the QT API.
building and then installing convenient.
``` ```
sh ./setup.sh <--- only need to run this once if compiling for the first sh ./setup.sh <--- only need to run this once if compiling for the first

BIN
bin/magick Normal file

Binary file not shown.

View File

@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
apt install apache2
if [ ! -d "/opt/mow" ]; then if [ ! -d "/opt/mow" ]; then
mkdir /opt/mow mkdir /opt/mow
fi fi

View File

@ -1,29 +1,7 @@
#!/bin/sh #!/bin/sh
export DEBIAN_FRONTEND=noninteractive
apt update -y apt update -y
apt install -y pkg-config apt install -y pkg-config cmake make g++
apt install -y cmake apt install -y ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev x264 libx264-dev libilmbase-dev qt6-base-dev qtchooser qmake6 qt6-base-dev-tools libxkbcommon-dev libfuse-dev
apt install -y make cp ./bin/magick /usr/bin/magick
apt install -y g++ chmod +x /usr/bin/magick
apt install -y wget
apt install -y unzip
apt install -y git
apt install -y ffmpeg
apt install -y gstreamer1.0*
apt install -y libavcodec-dev
apt install -y libavformat-dev
apt install -y libavutil-dev
apt install -y libswscale-dev
apt install -y libgstreamer1.0-dev
apt install -y x264
apt install -y libx264-dev
apt install -y libilmbase-dev
apt install -y libopencv-dev
apt install -y apache2
add-apt-repository -y ppa:ubuntu-toolchain-r/test
apt update -y
apt install -y gcc-10
apt install -y gcc-10-base
apt install -y gcc-10-doc
apt install -y g++-10
apt install -y libstdc++-10-dev
apt install -y libstdc++-10-doc

485
src/camera.cpp Normal file
View File

@ -0,0 +1,485 @@
// 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.imgThresh = 8000;
shared.maxEvents = 100;
shared.maxLogSize = 100000;
shared.skipCmd = false;
shared.postSecs = 60;
shared.evMaxSecs = 30;
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();
QDir("img").removeRecursively();
QDir().mkdir("live");
QDir().mkdir("events");
QDir().mkdir("logs");
QDir().mkdir("img");
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()
{
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);
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->webRoot, 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 (cycles * 2 >= shared->evMaxSecs)
{
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();
}
else
{
cycles += 1;
shared->recMutex.lock();
for (auto &&event : shared->recList)
{
auto maxFiles = shared->evMaxSecs / 2;
// there's 2 secs in each hls segment
if (highScore < event.score)
{
name = event.timeStamp.toString(DATETIME_FMT);
imgPath = event.imgPath;
highScore = event.score;
}
vidList.append(backwardFacingFiles("live", ".ts", event.timeStamp, maxFiles / 2));
vidList.append(forwardFacingFiles("live", ".ts", event.timeStamp, maxFiles / 2));
}
shared->recList.clear();
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 = 8; // this will be used to delay the
// actual start of DetectLoop by
// 16secs.
}
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);
system(shared->postCmd.toUtf8().data());
}
}
mod = false;
}
bool DetectLoop::exec()
{
if (delayCycles > 0)
{
delayCycles -= 1;
detLog("spec: 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
{
QProcess extComp;
QStringList args;
auto pos = images.size() - 1;
args << "compare";
args << "-metric" << "FUZZ";
args << images[pos - 1];
args << images[pos];
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);
auto score = output.toFloat();
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];
shared->recMutex.lock();
shared->recList.append(event); mod = true;
shared->recMutex.unlock();
}
}
}
return Loop::exec();
}

142
src/camera.h Normal file
View File

@ -0,0 +1,142 @@
#ifndef CAMERA_H
#define CAMERA_H
// 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 "common.h"
#include "logger.h"
#include "web.h"
class Camera : public QObject
{
Q_OBJECT
private:
shared_t shared;
public:
explicit Camera(QObject *parent = nullptr);
int start(const QStringList &args);
};
class Loop : public QObject
{
Q_OBJECT
protected:
shared_t *shared;
QTimer *loopTimer;
int heartBeat;
protected slots:
virtual void init();
private slots:
void loopSlot();
public:
explicit Loop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
virtual bool exec();
};
class RecLoop : public Loop
{
Q_OBJECT
private:
QProcess *recProc;
QProcess *imgProc;
QString curUrl;
void updateCmd();
void procError(const QString &desc, QProcess *proc);
private slots:
void init();
void term();
void rdProcErr();
public:
explicit RecLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
bool exec();
};
class Upkeep : public Loop
{
Q_OBJECT
public:
explicit Upkeep(shared_t *shared, QThread *thr, QObject *parent = nullptr);
bool exec();
};
class EventLoop : public Loop
{
Q_OBJECT
private:
QStringList vidList;
QString imgPath;
QString name;
float highScore;
uint cycles;
bool wrOutVod();
public:
explicit EventLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
bool exec();
};
class DetectLoop : public Loop
{
Q_OBJECT
private:
QTimer *pcTimer;
uint delayCycles;
bool mod;
void resetTimers();
private slots:
void init();
void pcBreak();
public:
explicit DetectLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
bool exec();
};
#endif // CAMERA_H

View File

@ -12,124 +12,97 @@
#include "common.h" #include "common.h"
string cleanDir(const string &path) QString getParam(const QString &key, const QStringList &args)
{ {
if (path[path.size() - 1] == '/') // this can be used by command objects to pick out parameters
{ // from a command line that are pointed by a name identifier
return path.substr(0, path.size() - 1); // example: -i /etc/some_file, this function should pick out
} // "/etc/some_file" from args if "-i" is passed into key.
else
{
return path;
}
}
bool createDir(const string &dir) QString ret;
{
auto ret = mkdir(dir.c_str(), 0777);
if (ret == -1) int pos = args.indexOf(QRegularExpression(key, QRegularExpression::CaseInsensitiveOption));
{
return errno == EEXIST;
}
else
{
return true;
}
}
bool createDirTree(const string &full_path) if (pos != -1)
{
size_t pos = 0;
auto ret = true;
while (ret == true && pos != string::npos)
{ {
pos = full_path.find('/', pos + 1); // key found.
ret = createDir(full_path.substr(0, pos));
if ((pos + 1) <= (args.size() - 1))
{
// check ahead to make sure pos + 1 will not go out
// of range.
if (!args[pos + 1].startsWith("-"))
{
// the "-" used throughout this application
// indicates an argument so the above 'if'
// statement will check to make sure it does
// not return another argument as a parameter
// in case a back-to-back "-arg -arg" is
// present.
ret = args[pos + 1];
}
}
} }
return ret; return ret;
} }
void cleanupEmptyDirs(const string &path) QStringList lsFilesInDir(const QString &path, const QString &ext)
{ {
if (exists(path)) QStringList filters;
{
for (auto &entry : directory_iterator(path)) filters << "*" + ext;
{
if (entry.is_directory()) QDir dirObj(path);
{
try dirObj.setFilter(QDir::Files);
{ dirObj.setNameFilters(filters);
remove(entry.path()); dirObj.setSorting(QDir::Name);
}
catch (filesystem_error const &ex) return dirObj.entryList();
{
// non-empty dir assumed when filesystem_error is raised.
cleanupEmptyDirs(path + "/" + entry.path().filename().string());
}
}
}
}
} }
vector<string> lsFilesInDir(const string &path, const string &ext) QStringList lsDirsInDir(const QString &path)
{ {
vector<string> names; QDir dirObj(path);
if (exists(path)) dirObj.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
{ dirObj.setSorting(QDir::Name);
for (auto &entry : directory_iterator(path))
{
if (entry.is_regular_file())
{
auto name = entry.path().filename().string();
if (ext.empty() || name.ends_with(ext)) return dirObj.entryList();
{
names.push_back(name);
}
}
}
}
sort(names.begin(), names.end());
return names;
} }
vector<string> lsDirsInDir(const string &path) QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir)
{ {
vector<string> names; QStringList ret;
if (exists(path)) for (auto i = 0; i < secs; ++i)
{ {
for (auto &entry : directory_iterator(path)) QString filePath;
if (dir == '-') filePath = path + "/" + stamp.addSecs(-i).toString(DATETIME_FMT) + ext;
if (dir == '+') filePath = path + "/" + stamp.addSecs(i).toString(DATETIME_FMT) + ext;
if (QFile::exists(filePath))
{ {
if (entry.is_directory()) if (dir == '-') ret.insert(0, filePath);
{ if (dir == '+') ret.append(filePath);
names.push_back(entry.path().filename().string());
}
} }
} }
sort(names.begin(), names.end()); return ret;
return names;
} }
void cleanupStream(const string &plsPath) QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs)
{ {
ifstream fileIn(plsPath); return listFacingFiles(path, ext, stamp, secs, '-');
}
for (string line; getline(fileIn, line); ) QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs)
{ {
if (line.starts_with("VIDEO_TS/")) return listFacingFiles(path, ext, stamp, secs, '+');
{
remove(line);
}
}
} }
void enforceMaxEvents(shared_t *share) void enforceMaxEvents(shared_t *share)
@ -138,83 +111,81 @@ void enforceMaxEvents(shared_t *share)
while (names.size() > share->maxEvents) while (names.size() > share->maxEvents)
{ {
// removes the video file extension (.mp4). auto nameOnly = "events/" + names[0];
auto nameOnly = "events/" + names[0].substr(0, names[0].size() - 4);
auto mp4File = nameOnly + string(".mp4");
auto imgFile = nameOnly + string(".jpg");
auto webFile = nameOnly + string(".html");
if (exists(mp4File)) remove(mp4File); nameOnly.remove(".mp4");
if (exists(imgFile)) remove(imgFile);
if (exists(webFile)) remove(webFile);
names.erase(names.begin()); auto mp4File = nameOnly + ".mp4";
auto imgFile = nameOnly + ".jpg";
auto webFile = nameOnly + ".html";
QFile::remove(mp4File);
QFile::remove(imgFile);
QFile::remove(webFile);
names.removeFirst();
} }
} }
void enforceMaxImages()
string genTimeStr(const char *fmt)
{ {
time_t rawtime; auto names = lsFilesInDir("img", ".bmp");
time(&rawtime); while (names.size() > MAX_IMAGES)
auto timeinfo = localtime(&rawtime);
char ret[50];
strftime(ret, 50, fmt, timeinfo);
return string(ret);
}
string genDstFile(const string &dirOut, const char *fmt, const string &ext)
{
createDirTree(cleanDir(dirOut));
return cleanDir(dirOut) + string("/") + genTimeStr(fmt) + ext;
}
string genEventName(int score)
{
return genTimeStr(string("%Y-%j-%H-%M-%S--" + to_string(score)).c_str());
}
void rdLine(const string &param, const string &line, string *value)
{
if (line.rfind(param.c_str(), 0) == 0)
{ {
*value = line.substr(param.size()); QFile::remove("img/" + names[0]);
names.removeFirst();
} }
} }
void rdLine(const string &param, const string &line, int *value) void enforceMaxVids()
{ {
if (line.rfind(param.c_str(), 0) == 0) auto names = lsFilesInDir("live", ".ts");
while (names.size() > MAX_VIDEOS)
{ {
*value = strtol(line.substr(param.size()).c_str(), NULL, 10); QFile::remove("live/" + names[0]);
names.removeFirst();
} }
} }
bool rdConf(const string &filePath, shared_t *share) void rdLine(const QString &param, const QString &line, QString *value)
{ {
ifstream varFile(filePath.c_str()); if (line.startsWith(param))
{
*value = line.mid(param.size());
}
}
if (!varFile.is_open()) void rdLine(const QString &param, const QString &line, int *value)
{
if (line.startsWith(param))
{
*value = line.mid(param.size()).toInt();
}
}
bool rdConf(const QString &filePath, shared_t *share)
{
QFile varFile(filePath);
if (!varFile.open(QFile::ReadOnly))
{ {
share->retCode = ENOENT; share->retCode = ENOENT;
cerr << "err: config file: " << filePath << " does not exists or lack read permissions." << endl; QTextStream(stderr) << "err: config file: " << filePath << " does not exists or lack read permissions." << Qt::endl;
} }
else else
{ {
string line; QString line;
do do
{ {
getline(varFile, line); line = QString::fromUtf8(varFile.readLine());
if (line.rfind("#", 0) != 0) if (!line.startsWith("#"))
{ {
rdLine("cam_name = ", line, &share->camName); rdLine("cam_name = ", line, &share->camName);
rdLine("recording_stream = ", line, &share->recordUrl); rdLine("recording_stream = ", line, &share->recordUrl);
@ -225,14 +196,12 @@ bool rdConf(const string &filePath, shared_t *share)
rdLine("max_event_secs = ", line, &share->evMaxSecs); rdLine("max_event_secs = ", line, &share->evMaxSecs);
rdLine("post_secs = ", line, &share->postSecs); rdLine("post_secs = ", line, &share->postSecs);
rdLine("post_cmd = ", line, &share->postCmd); rdLine("post_cmd = ", line, &share->postCmd);
rdLine("pix_thresh = ", line, &share->pixThresh);
rdLine("img_thresh = ", line, &share->imgThresh); rdLine("img_thresh = ", line, &share->imgThresh);
rdLine("frame_gap = ", line, &share->frameGap);
rdLine("max_events = ", line, &share->maxEvents); rdLine("max_events = ", line, &share->maxEvents);
rdLine("max_log_size = ", line, &share->maxLogSize); rdLine("max_log_size = ", line, &share->maxLogSize);
} }
} while(!line.empty()); } while(!line.isEmpty());
} }
return share->retCode == 0; return share->retCode == 0;
@ -240,122 +209,100 @@ bool rdConf(const string &filePath, shared_t *share)
bool rdConf(shared_t *share) bool rdConf(shared_t *share)
{ {
share->recordUrl.clear();
share->postCmd.clear();
share->camName.clear();
share->retCode = 0;
share->pixThresh = 50;
share->imgThresh = 800;
share->maxEvents = 40;
share->maxLogSize = 100000;
share->skipCmd = false;
share->postSecs = 60;
share->evMaxSecs = 10;
share->frameGap = 10;
share->webRoot = "/var/www/html";
share->webBg = "#485564";
share->webTxt = "#dee5ee";
share->webFont = "courier";
if (rdConf(share->conf, share)) if (rdConf(share->conf, share))
{ {
if (share->camName.empty()) if (share->camName.isEmpty())
{ {
share->camName = path(share->conf).filename(); share->camName = QFileInfo(share->conf).fileName();
} }
share->outDir = cleanDir(share->webRoot) + "/" + share->camName; share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName;
error_code ec; QDir().mkpath(share->outDir);
createDirTree(share->outDir); if (!QDir::setCurrent(share->outDir))
current_path(share->outDir, ec);
share->retCode = ec.value();
if (share->retCode != 0)
{ {
cerr << "err: " << ec.message() << endl; QTextStream(stderr) << "err: failed to change/create the current working directory to camera folder: '" << share->outDir << "' does it exists?" << Qt::endl;
share->retCode = ENOENT;
} }
} }
return share->retCode == 0; return share->retCode == 0;
} }
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs) MultiInstance::MultiInstance(QObject *parent) : QObject(parent) {}
void MultiInstance::instStdout()
{ {
auto ret = string(); for (auto &&proc : procList)
for (; offs < argc; ++offs)
{ {
auto argInParams = string(argv[offs]); QTextStream(stdout) << proc->readAllStandardOutput();
}
}
if (arg.compare(argInParams) == 0) void MultiInstance::instStderr()
{
for (auto &&proc : procList)
{
QTextStream(stderr) << proc->readAllStandardError();
}
}
void MultiInstance::procChanged(QProcess::ProcessState newState)
{
Q_UNUSED(newState)
for (auto &&proc : procList)
{
if (proc->state() == QProcess::Running)
{ {
if (!argOnly) return;
{ }
offs++; }
// check ahead, make sure offs + 1 won't cause out-of-range exception
if (offs <= (argc - 1)) QCoreApplication::quit();
{ }
ret = string(argv[offs]);
} int MultiInstance::start(const QStringList &args)
} {
else auto ret = ENOENT;
{ auto path = QDir().cleanPath(getParam("-d", args));
ret = string("true"); auto files = lsFilesInDir(path);
}
if (!QDir(path).exists())
{
QTextStream(stderr) << "err: the supplied directory in -d '" << path << "' does not exists or is not a directory.";
}
else if (files.isEmpty())
{
QTextStream(stderr) << "err: no config files found in '" << path << "'";
}
else
{
ret = 0;
for (auto &&conf : files)
{
auto proc = new QProcess(this);
QStringList subArgs;
subArgs << "-c" << path + "/" + conf;
connect(proc, &QProcess::readyReadStandardOutput, this, &MultiInstance::instStdout);
connect(proc, &QProcess::readyReadStandardError, this, &MultiInstance::instStderr);
connect(proc, &QProcess::stateChanged, this, &MultiInstance::procChanged);
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, proc, &QProcess::terminate);
proc->setProgram(APP_BIN);
proc->setArguments(subArgs);
proc->start();
procList.append(proc);
} }
} }
return ret; return ret;
} }
string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
{
auto notUsed = 0;
return parseForParam(arg, argc, argv, argOnly, notUsed);
}
string genEventPath(const string &tsPath)
{
if (tsPath.size() > 14)
{
// removes 'VIDEO_TS/live/' from the front of the string.
auto ret = tsPath.substr(14);
return "VIDEO_TS/events/" + ret;
}
else
{
return string();
}
}
string genVidNameFromLive(const string &tsPath)
{
if (tsPath.size() > 17)
{
// removes 'VIDEO_TS/live/' from the front of the string.
auto ret = tsPath.substr(14);
auto ind = tsPath.find('/');
// removes '.ts' from the end of the string.
ret = ret.substr(0, ret.size() - 3);
while (ind != string::npos)
{
// remove all '/'
ret.erase(ind, 1);
ind = ret.find('/');
}
return ret;
}
else
{
return string();
}
}

View File

@ -13,89 +13,98 @@
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
#include <iostream> #include <QCoreApplication>
#include <fstream> #include <QProcess>
#include <string> #include <QTextStream>
#include <time.h> #include <QObject>
#include <chrono> #include <QRegularExpression>
#include <stdlib.h> #include <QDir>
#include <errno.h> #include <QCryptographicHash>
#include <vector> #include <QFile>
#include <thread> #include <QDateTime>
#include <filesystem> #include <QThread>
#include <sys/stat.h> #include <QTimer>
#include <map> #include <QStringList>
#include <QMutex>
#include <opencv4/opencv2/opencv.hpp>
#include <opencv4/opencv2/videoio.hpp>
using namespace cv;
using namespace std; using namespace std;
using namespace std::filesystem;
using namespace std::chrono;
#define APP_VER "2.2" #define APP_VER "3.0.0"
#define APP_NAME "Motion Watch" #define APP_NAME "Motion Watch"
#define APP_BIN "mow"
#define REC_LOG_NAME "rec_log_lines.html" #define REC_LOG_NAME "rec_log_lines.html"
#define DET_LOG_NAME "det_log_lines.html" #define DET_LOG_NAME "det_log_lines.html"
#define UPK_LOG_NAME "upk_log_lines.html" #define UPK_LOG_NAME "upk_log_lines.html"
#define DATETIME_FMT "yyyyMMddhhmmss"
#define STRFTIME_FMT "%Y%m%d%H%M%S"
#define MAX_IMAGES 1000
#define MAX_VIDEOS 1000
struct evt_t struct evt_t
{ {
string evName; QDateTime timeStamp;
vector<string> srcPaths; QString imgPath;
Mat thumbnail; float score;
}; };
struct shared_t struct shared_t
{ {
vector<evt_t> recList; QList<evt_t> recList;
string conf; QMutex recMutex;
string recLog; QMutex logMutex;
string detLog; QString conf;
string upkLog; QString recLog;
string recordUrl; QString detLog;
string outDir; QString recordUrl;
string postCmd; QString outDir;
string camName; QString postCmd;
string webBg; QString camName;
string webTxt; QString webBg;
string webFont; QString webTxt;
string webRoot; QString webFont;
evt_t curEvent; QString webRoot;
bool skipCmd; bool skipCmd;
int frameGap; int evMaxSecs;
int evMaxSecs; int postSecs;
int postSecs; int imgThresh;
int maxScore; int maxEvents;
int procCnt; int maxLogSize;
int hlsCnt; int retCode;
int pixThresh;
int imgThresh;
int maxEvents;
int maxLogSize;
int retCode;
int postInd;
int evInd;
}; };
string genVidNameFromLive(const string &tsPath); QString getParam(const QString &key, const QStringList &args);
string genEventPath(const string &tsPath); QStringList lsFilesInDir(const QString &path, const QString &ext = QString());
string genEventName(int score); QStringList lsDirsInDir(const QString &path);
string genDstFile(const string &dirOut, const char *fmt, const string &ext); QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir);
string genTimeStr(const char *fmt); QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
string cleanDir(const string &path); QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs); bool rdConf(const QString &filePath, shared_t *share);
string parseForParam(const string &arg, int argc, char** argv, bool argOnly); bool rdConf(shared_t *share);
bool createDir(const string &dir); void rdLine(const QString &param, const QString &line, QString *value);
bool createDirTree(const string &full_path); void rdLine(const QString &param, const QString &line, int *value);
void rdLine(const string &param, const string &line, string *value); void enforceMaxEvents(shared_t *share);
void rdLine(const string &param, const string &line, int *value); void enforceMaxImages();
void cleanupEmptyDirs(const string &path); void enforceMaxVids();
void cleanupStream(const string &plsPath);
void enforceMaxEvents(shared_t *share); class MultiInstance : public QObject
bool rdConf(shared_t *share); {
vector<string> lsFilesInDir(const string &path, const string &ext = string()); Q_OBJECT
vector<string> lsDirsInDir(const string &path);
private:
QList<QProcess*> procList;
private slots:
void instStdout();
void instStderr();
void procChanged(QProcess::ProcessState newState);
public:
explicit MultiInstance(QObject *parent = nullptr);
int start(const QStringList &args);
};
#endif // COMMON_H #endif // COMMON_H

View File

@ -12,58 +12,62 @@
#include "logger.h" #include "logger.h"
void recLog(const string &line, shared_t *share) void recLog(const QString &line, shared_t *share)
{ {
share->recLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n"; share->logMutex.lock();
share->recLog += QDateTime::currentDateTime().toString("[yyyy-MM-dd-hh-mm-ss] ") + line + "<br>\n";
share->logMutex.unlock();
} }
void detLog(const string &line, shared_t *share) void detLog(const QString &line, shared_t *share)
{ {
share->detLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n"; share->logMutex.lock();
share->detLog += QDateTime::currentDateTime().toString("[yyyy-MM-dd-hh-mm-ss] ") + line + "<br>\n";
share->logMutex.unlock();
} }
void upkLog(const string &line, shared_t *share) void enforceMaxLogSize(const QString &filePath, shared_t *share)
{ {
share->upkLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n"; QFile file(filePath);
}
void enforceMaxLogSize(const string &filePath, shared_t *share) if (file.exists())
{
if (exists(filePath))
{ {
if (file_size(filePath) >= share->maxLogSize) if (file.size() >= share->maxLogSize)
{ {
remove(filePath); file.remove();
} }
} }
} }
void dumpLogs(const string &fileName, const string &lines) void dumpLogs(const QString &fileName, const QString &lines)
{ {
if (!lines.empty()) if (!lines.isEmpty())
{ {
ofstream outFile; QFile outFile(fileName);
if (exists(fileName)) if (outFile.exists())
{ {
outFile.open(fileName.c_str(), ofstream::app); outFile.open(QFile::Append);
} }
else else
{ {
outFile.open(fileName.c_str()); outFile.open(QFile::WriteOnly);
} }
outFile << lines; outFile.write(lines.toUtf8());
outFile.close(); outFile.close();
} }
} }
void initLogFrontPage(const string &filePath, const string &logLinesFile) void initLogFrontPage(const QString &filePath, const QString &logLinesFile)
{ {
if (!exists(filePath)) if (!QFile::exists(filePath))
{ {
string htmlText = "<!DOCTYPE html>\n"; QString htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n"; htmlText += "<html>\n";
htmlText += "<script>\n"; htmlText += "<script>\n";
@ -106,17 +110,16 @@ void initLogFrontPage(const string &filePath, const string &logLinesFile)
htmlText += "</body>\n"; htmlText += "</body>\n";
htmlText += "</html>\n"; htmlText += "</html>\n";
ofstream outFile(filePath); QFile outFile(filePath);
outFile << htmlText;
outFile.open(QFile::WriteOnly);
outFile.write(htmlText.toUtf8());
outFile.close(); outFile.close();
} }
} }
void initLogFrontPages(shared_t *share) void initLogFrontPages()
{ {
initLogFrontPage("logs/recording_log.html", REC_LOG_NAME); initLogFrontPage("logs/recording_log.html", REC_LOG_NAME);
initLogFrontPage("logs/detection_log.html", DET_LOG_NAME); initLogFrontPage("logs/detection_log.html", DET_LOG_NAME);
initLogFrontPage("logs/upkeep_log.html", UPK_LOG_NAME);
} }

View File

@ -15,11 +15,10 @@
#include "common.h" #include "common.h"
void recLog(const string &line, shared_t *share); void recLog(const QString &line, shared_t *share);
void detLog(const string &line, shared_t *share); void detLog(const QString &line, shared_t *share);
void upkLog(const string &line, shared_t *share); void dumpLogs(const QString &fileName, const QString &lines);
void dumpLogs(const string &fileName, const string &lines); void enforceMaxLogSize(const QString &filePath, shared_t *share);
void enforceMaxLogSize(const string &filePath, shared_t *share); void initLogFrontPages();
void initLogFrontPages(shared_t *share);
#endif // lOGGER_H #endif // lOGGER_H

View File

@ -10,192 +10,60 @@
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
#include "mo_detect.h" #include "common.h"
#include "logger.h" #include "camera.h"
#include "web.h"
void timer(shared_t *share)
{
while (share->retCode == 0)
{
sleep(1);
share->postInd += 1;
share->evInd += 1;
}
}
void detectMo(shared_t *share)
{
while (share->retCode == 0)
{
sleep(2);
detectMoInStream("stream.m3u8", share);
}
}
void eventLoop(shared_t *share)
{
while (share->retCode == 0)
{
if (!share->recList.empty())
{
auto event = share->recList[0];
try
{
recLog("attempting write out of event: " + event.evName, share);
createDirTree("events");
if (wrOutVod(event, share))
{
genHTMLvod(event.evName);
imwrite(string("events/" + event.evName + ".jpg").c_str(), event.thumbnail);
}
}
catch (filesystem_error &ex)
{
recLog(string("err: ") + ex.what(), share);
}
share->recList.erase(share->recList.begin());
}
sleep(10);
}
}
void upkeep(shared_t *share)
{
while (share->retCode == 0)
{
createDirTree("live");
createDirTree("events");
createDirTree("logs");
enforceMaxLogSize(string("logs/") + REC_LOG_NAME, share);
enforceMaxLogSize(string("logs/") + DET_LOG_NAME, share);
enforceMaxLogSize(string("logs/") + UPK_LOG_NAME, share);
dumpLogs(string("logs/") + REC_LOG_NAME, share->recLog);
dumpLogs(string("logs/") + DET_LOG_NAME, share->detLog);
dumpLogs(string("logs/") + UPK_LOG_NAME, share->upkLog);
share->recLog.clear();
share->detLog.clear();
share->upkLog.clear();
initLogFrontPages(share);
enforceMaxEvents(share);
genHTMLul(".", share->camName, share);
upkLog("camera specific webroot page updated: " + share->outDir + "/index.html", share);
if (!exists("/tmp/mow-lock"))
{
system("touch /tmp/mow-lock");
genCSS(share);
genHTMLul(share->webRoot, string(APP_NAME) + " " + string(APP_VER), share);
remove("/tmp/mow-lock");
upkLog("webroot page updated: " + cleanDir(share->webRoot) + "/index.html", share);
}
else
{
upkLog("skipping update of the webroot page, it is busy.", share);
}
sleep(10);
}
}
void rmLive()
{
if (exists("live"))
{
remove_all("live");
}
}
void recLoop(shared_t *share)
{
while (share->retCode == 0)
{
auto cmd = "ffmpeg -hide_banner -rtsp_transport tcp -timeout 3000000 -i " +
share->recordUrl +
" -strftime 1" +
" -strftime_mkdir 1" +
" -hls_segment_filename 'live/%Y-%j-%H-%M-%S.ts'" +
" -hls_flags delete_segments" +
" -y -vcodec copy" +
" -f hls -hls_time 2 -hls_list_size 1000" +
" stream.m3u8";
recLog("ffmpeg_run: " + cmd, share);
rmLive();
auto retCode = system(cmd.c_str());
recLog("ffmpeg_retcode: " + to_string(retCode), share);
if (retCode != 0)
{
recLog("err: ffmpeg returned non zero, indicating failure. please check stderr output.", share);
}
sleep(10);
}
}
int main(int argc, char** argv) int main(int argc, char** argv)
{ {
struct shared_t sharedRes; QCoreApplication app(argc, argv);
sharedRes.conf = parseForParam("-c", argc, argv, false); QCoreApplication::setApplicationName(APP_NAME);
QCoreApplication::setApplicationVersion(APP_VER);
if (parseForParam("-h", argc, argv, true) == "true") auto args = QCoreApplication::arguments();
auto ret = 0;
if (args.contains("-h"))
{ {
cout << "Motion Watch " << APP_VER << endl << endl; QTextStream(stdout) << "Motion Watch " << APP_VER << Qt::endl << Qt::endl;
cout << "Usage: mow <argument>" << endl << endl; QTextStream(stdout) << "Usage: mow <argument>" << Qt::endl << Qt::endl;
cout << "-h : display usage information about this application." << endl; QTextStream(stdout) << "-h : display usage information about this application." << Qt::endl;
cout << "-c : path to the config file." << endl; QTextStream(stdout) << "-c : path to the config file used to run a single camera instance." << Qt::endl;
cout << "-v : display the current version." << endl << endl; QTextStream(stdout) << "-d : path to a directory that can contain multiple config files." << Qt::endl;
QTextStream(stdout) << " each file found in the directory will be used to run a" << Qt::endl;
QTextStream(stdout) << " camera instance." << Qt::endl;
QTextStream(stdout) << "-v : display the current version." << Qt::endl << Qt::endl;
} }
else if (parseForParam("-v", argc, argv, true) == "true") else if (args.contains("-v"))
{ {
cout << APP_VER << endl; QTextStream(stdout) << APP_VER << Qt::endl;
} }
else if (sharedRes.conf.empty()) else if (args.contains("-d"))
{ {
cerr << "err: no config file(s) were given in -c" << endl; auto *muli = new MultiInstance(&app);
ret = muli->start(args);
if (ret == 0)
{
ret = QCoreApplication::exec();
}
}
else if (args.contains("-c"))
{
auto *cam = new Camera(&app);
ret = cam->start(args);
if (ret == 0)
{
ret = QCoreApplication::exec();
}
} }
else else
{ {
sharedRes.retCode = 0; QTextStream(stderr) << "err: no config file(s) were given in -c" << Qt::endl;
sharedRes.maxScore = 0;
sharedRes.postInd = 0;
sharedRes.evInd = 0;
sharedRes.skipCmd = false;
rdConf(&sharedRes);
auto thr1 = thread(recLoop, &sharedRes);
auto thr2 = thread(upkeep, &sharedRes);
auto thr3 = thread(detectMo, &sharedRes);
auto thr4 = thread(eventLoop, &sharedRes);
auto thr5 = thread(timer, &sharedRes);
thr1.join();
thr2.join();
thr3.join();
thr4.join();
thr5.join();
return sharedRes.retCode;
} }
return EINVAL; return ret;
} }

View File

@ -1,217 +0,0 @@
// 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 "mo_detect.h"
void detectMoInStream(const string &streamFile, shared_t *share)
{
if (share->postInd >= share->postSecs)
{
if (!share->postCmd.empty())
{
detLog("---POST_BREAK---", share);
if (!share->skipCmd)
{
detLog("no motion detected, running post command: " + share->postCmd, share);
system(share->postCmd.c_str());
}
else
{
share->skipCmd = false;
detLog("motion detected, skipping the post command.", share);
}
}
share->postInd = 0;
}
if (share->evInd >= share->evMaxSecs)
{
detLog("---EVENT_BREAK---", share);
if (!share->curEvent.srcPaths.empty())
{
share->curEvent.evName = genEventName(share->maxScore);
share->recList.push_back(share->curEvent);
detLog("motion detected in " + to_string(share->curEvent.srcPaths.size()) + " file(s) in " + to_string(share->evMaxSecs) + " secs", share);
detLog("all video clips queued for event generation under event name: " + share->curEvent.evName, share);
}
else
{
detLog("no motion detected in all files. none queued for event generation.", share);
}
share->curEvent.srcPaths.clear();
share->curEvent.evName.clear();
share->curEvent.thumbnail.release();
share->evInd = 0;
share->maxScore = 0;
}
ifstream fileIn(streamFile);
string tsPath;
for (string line; getline(fileIn, line); )
{
if (line.starts_with("live/"))
{
tsPath = line;
}
}
if (!tsPath.empty())
{
if (moDetect(tsPath, share))
{
share->curEvent.srcPaths.push_back(tsPath);
share->skipCmd = true;
}
}
}
bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share)
{
Mat prevGray;
Mat nextGray;
cvtColor(prev, prevGray, COLOR_BGR2GRAY);
cvtColor(next, nextGray, COLOR_BGR2GRAY);
Mat diff;
absdiff(prevGray, nextGray, diff);
threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY);
score = countNonZero(diff);
detLog("diff_score: " + to_string(score) + " thresh: " + to_string(share->imgThresh), share);
return score >= share->imgThresh;
}
bool moDetect(const string &buffFile, shared_t *share)
{
auto score = 0;
auto mod = false;
detLog("stream_clip: " + buffFile, share);
VideoCapture capture;
if (!capture.open(buffFile.c_str(), CAP_FFMPEG))
{
usleep(500);
capture.open(buffFile.c_str(), CAP_FFMPEG);
}
if (capture.isOpened())
{
Mat prev;
Mat next;
int fps = capture.get(cv::CAP_PROP_FPS);
for (auto gap = 0, frm = fps; capture.grab(); ++gap, ++frm)
{
if (frm == fps) sleep(1); frm = 1;
if (prev.empty())
{
capture.retrieve(prev);
}
else if (gap == (share->frameGap - 1))
{
capture.retrieve(next);
if (!next.empty())
{
if (imgDiff(prev, next, score, share))
{
mod = true;
if (share->maxScore <= score)
{
share->maxScore = score;
resize(next, share->curEvent.thumbnail, Size(720, 480), INTER_LINEAR);
}
}
}
prev = next.clone();
gap = 0;
next.release();
}
else
{
capture.grab();
}
}
}
else
{
detLog("err: failed to open: " + buffFile + " after 500 msecs. giving up.", share);
}
capture.release();
return mod;
}
bool wrOutVod(const evt_t &event, shared_t *share)
{
auto cnt = 0;
auto concat = event.evName + ".tmp";
ofstream file(concat.c_str());
for (auto i = 0; i < event.srcPaths.size(); ++i)
{
recLog("event_src: " + event.srcPaths[i], share);
if (exists(event.srcPaths[i]))
{
file << "file '" << event.srcPaths[i] << "''" << endl; cnt++;
}
}
file.close();
if (cnt == 0)
{
recLog("err: none of the event hls clips exists, canceling write out.", share);
if (exists(concat)) remove(concat);
return false;
}
else
{
auto ret = system(string("ffmpeg -f concat -safe 0 -i " + concat + " -c copy events/" + event.evName + ".mp4").c_str());
if (ret != 0)
{
recLog("err: ffmpeg concat failure, canceling write out.", share);
}
if (exists(concat)) remove(concat);
return ret == 0;
}
}

View File

@ -1,24 +0,0 @@
#ifndef MO_DETECT_H
#define MO_DETECT_H
// 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 "common.h"
#include "logger.h"
bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share);
bool moDetect(const string &buffFile, shared_t *share);
void detectMoInStream(const string &streamFile, shared_t *share);
bool wrOutVod(const evt_t &pls, shared_t *share);
#endif // MO_DETECT_H

View File

@ -12,13 +12,13 @@
#include "web.h" #include "web.h"
void genHTMLul(const string &outputDir, const string &title, shared_t *share) void genHTMLul(const QString &outputDir, const QString &title, shared_t *share)
{ {
vector<string> logNames; QStringList logNames;
vector<string> eveNames; QStringList eveNames;
vector<string> dirNames; QStringList dirNames;
string htmlText = "<!DOCTYPE html>\n"; QString htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n"; htmlText += "<html>\n";
htmlText += "<head>\n"; htmlText += "<head>\n";
@ -31,7 +31,7 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
htmlText += "<body>\n"; htmlText += "<body>\n";
htmlText += "<h3>" + title + "</h3>\n"; htmlText += "<h3>" + title + "</h3>\n";
if (exists(outputDir + "/live")) if (QDir().exists(outputDir + "/live"))
{ {
eveNames = lsFilesInDir(outputDir + "/events", ".html"); eveNames = lsFilesInDir(outputDir + "/events", ".html");
logNames = lsFilesInDir(outputDir + "/logs", "_log.html"); logNames = lsFilesInDir(outputDir + "/logs", "_log.html");
@ -41,8 +41,9 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
for (auto &&logName : logNames) for (auto &&logName : logNames)
{ {
// name.substr(0, name.size() - 9) removes _log.html auto name = logName;
auto name = logName.substr(0, logName.size() - 9);
name.remove("_log.html");
htmlText += " <li><a href='logs/" + logName + "'>" + name + "</a></li>\n"; htmlText += " <li><a href='logs/" + logName + "'>" + name + "</a></li>\n";
} }
@ -58,8 +59,9 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
for (auto &&eveName : eveNames) for (auto &&eveName : eveNames)
{ {
// regName.substr(0, regName.size() - 5) removes .html auto name = eveName;
auto name = eveName.substr(0, eveName.size() - 5);
name.remove(".html");
htmlText += "<a href='events/" + eveName + "'><img src='events/" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n"; htmlText += "<a href='events/" + eveName + "'><img src='events/" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n";
} }
@ -81,16 +83,16 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
htmlText += "</body>\n"; htmlText += "</body>\n";
htmlText += "</html>"; htmlText += "</html>";
ofstream file(string(cleanDir(outputDir) + "/index.html").c_str()); QFile outFile(QDir().cleanPath(outputDir) + "/index.html");
file << htmlText << endl; outFile.open(QFile::WriteOnly);
outFile.write(htmlText.toUtf8());
file.close(); outFile.close();
} }
void genHTMLstream(const string &name) void genHTMLstream(const QString &name)
{ {
string htmlText = "<!DOCTYPE html>\n"; QString htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n"; htmlText += "<html>\n";
htmlText += "<head>\n"; htmlText += "<head>\n";
@ -128,16 +130,16 @@ void genHTMLstream(const string &name)
htmlText += "</body>\n"; htmlText += "</body>\n";
htmlText += "</html>"; htmlText += "</html>";
ofstream file(string(name + ".html").c_str()); QFile outFile(name + ".html");
file << htmlText << endl; outFile.open(QFile::WriteOnly);
outFile.write(htmlText.toUtf8());
file.close(); outFile.close();
} }
void genHTMLvod(const string &name) void genHTMLvod(const QString &name)
{ {
string htmlText = "<!DOCTYPE html>\n"; QString htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n"; htmlText += "<html>\n";
htmlText += "<head>\n"; htmlText += "<head>\n";
@ -154,16 +156,16 @@ void genHTMLvod(const string &name)
htmlText += "</body>\n"; htmlText += "</body>\n";
htmlText += "</html>"; htmlText += "</html>";
ofstream file(string("events/" + name + ".html").c_str()); QFile outFile("events/" + name + ".html");
file << htmlText << endl; outFile.open(QFile::WriteOnly);
outFile.write(htmlText.toUtf8());
file.close(); outFile.close();
} }
void genCSS(shared_t *share) void genCSS(shared_t *share)
{ {
string cssText = "body {\n"; QString cssText = "body {\n";
cssText += " background-color: " + share->webBg + ";\n"; cssText += " background-color: " + share->webBg + ";\n";
cssText += " color: " + share->webTxt + ";\n"; cssText += " color: " + share->webTxt + ";\n";
@ -173,9 +175,9 @@ void genCSS(shared_t *share)
cssText += " color: " + share->webTxt + ";\n"; cssText += " color: " + share->webTxt + ";\n";
cssText += "}\n"; cssText += "}\n";
ofstream file(string(cleanDir(share->webRoot) + "/theme.css").c_str()); QFile outFile(QDir().cleanPath(share->webRoot) + "/theme.css");
file << cssText << endl; outFile.open(QFile::WriteOnly);
outFile.write(cssText.toUtf8());
file.close(); outFile.close();
} }

View File

@ -15,9 +15,9 @@
#include "common.h" #include "common.h"
void genHTMLul(const string &outputDir, const string &title, shared_t *share); void genHTMLul(const QString &outputDir, const QString &title, shared_t *share);
void genHTMLstream(const string &name); void genHTMLstream(const QString &name);
void genHTMLvod(const string &name); void genHTMLvod(const QString &name);
void genCSS(shared_t *share); void genCSS(shared_t *share);
#endif // WEB_H #endif // WEB_H