Compare commits
24 Commits
f4ea944f97
...
ff5f95f445
Author | SHA1 | Date | |
---|---|---|---|
|
ff5f95f445 | ||
|
4300651a52 | ||
|
71fc5b0bc2 | ||
|
a493c7da5d | ||
|
510bdda64c | ||
|
9e7d8ee1ee | ||
|
09a0f030ac | ||
|
51218198b5 | ||
|
baf0b86610 | ||
|
4b4c2649b8 | ||
|
16312a93f5 | ||
|
4bf260c0ae | ||
|
71df7d1eb5 | ||
|
732a604c24 | ||
|
de24a94bd4 | ||
|
b445906403 | ||
|
4134d4befb | ||
|
f850ec6a46 | ||
|
496bac7d7e | ||
|
40ef014e0f | ||
|
80e6980d9e | ||
|
bbad30a5b0 | ||
|
b5ebbace12 | ||
|
fa834aba6c |
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -57,3 +57,5 @@ compile_commands.json
|
|||
|
||||
# Build folders
|
||||
/.build-mow
|
||||
/.build-opencv
|
||||
/src/opencv
|
||||
|
|
|
@ -1,7 +1,31 @@
|
|||
cmake_minimum_required(VERSION 2.8.12)
|
||||
project( MotionWatch )
|
||||
find_package( OpenCV REQUIRED )
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pthread")
|
||||
include_directories( ${OpenCV_INCLUDE_DIRS} )
|
||||
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} )
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
project(MotionWatch LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
|
||||
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})
|
||||
|
|
51
README.md
51
README.md
|
@ -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
|
||||
advantage of this is reduced storage requirements as opposed to continuous
|
||||
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: mow <argument>
|
||||
|
||||
-h : display usage information about this application.
|
||||
-c : path to the config file(s).
|
||||
-v : display the current version.
|
||||
|
||||
note: multiple -c config files can be passed, reading from left
|
||||
to right. any conflicting values between the files will
|
||||
have the latest value from the latest file overwrite the
|
||||
the earliest.
|
||||
-h : display usage information about this application.
|
||||
-c : path to the config file used to run a single camera instance.
|
||||
-d : path to a directory that can contain multiple config files.
|
||||
each file found in the directory will be used to run a
|
||||
camera instance.
|
||||
-v : display the current version.
|
||||
```
|
||||
|
||||
### 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
|
||||
# defined, the name of the config file will be used.
|
||||
#
|
||||
pix_thresh = 150
|
||||
# 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
|
||||
max_event_secs = 30
|
||||
# this is the maximum amount of secs of video footage that can be
|
||||
# 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
|
||||
# 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
|
||||
|
@ -105,8 +91,7 @@ web_font = courier
|
|||
### Setup/Build/Install ###
|
||||
|
||||
This application is currently only compatible with a Linux based operating
|
||||
systems that are capable of installing opencv. The following 3 scripts make
|
||||
building and then installing convenient.
|
||||
systems that are capable of installing the QT API.
|
||||
|
||||
```
|
||||
sh ./setup.sh <--- only need to run this once if compiling for the first
|
||||
|
|
BIN
bin/magick
Normal file
BIN
bin/magick
Normal file
Binary file not shown.
|
@ -1,4 +1,5 @@
|
|||
#!/bin/sh
|
||||
apt install apache2
|
||||
if [ ! -d "/opt/mow" ]; then
|
||||
mkdir /opt/mow
|
||||
fi
|
||||
|
|
32
setup.sh
32
setup.sh
|
@ -1,29 +1,7 @@
|
|||
#!/bin/sh
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt update -y
|
||||
apt install -y pkg-config
|
||||
apt install -y cmake
|
||||
apt install -y make
|
||||
apt install -y g++
|
||||
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
|
||||
apt install -y pkg-config cmake make g++
|
||||
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
|
||||
cp ./bin/magick /usr/bin/magick
|
||||
chmod +x /usr/bin/magick
|
||||
|
|
485
src/camera.cpp
Normal file
485
src/camera.cpp
Normal 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
142
src/camera.h
Normal 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
|
421
src/common.cpp
421
src/common.cpp
|
@ -12,124 +12,97 @@
|
|||
|
||||
#include "common.h"
|
||||
|
||||
string cleanDir(const string &path)
|
||||
QString getParam(const QString &key, const QStringList &args)
|
||||
{
|
||||
if (path[path.size() - 1] == '/')
|
||||
{
|
||||
return path.substr(0, path.size() - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
// this can be used by command objects to pick out parameters
|
||||
// from a command line that are pointed by a name identifier
|
||||
// example: -i /etc/some_file, this function should pick out
|
||||
// "/etc/some_file" from args if "-i" is passed into key.
|
||||
|
||||
bool createDir(const string &dir)
|
||||
{
|
||||
auto ret = mkdir(dir.c_str(), 0777);
|
||||
QString ret;
|
||||
|
||||
if (ret == -1)
|
||||
{
|
||||
return errno == EEXIST;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
int pos = args.indexOf(QRegularExpression(key, QRegularExpression::CaseInsensitiveOption));
|
||||
|
||||
bool createDirTree(const string &full_path)
|
||||
{
|
||||
size_t pos = 0;
|
||||
auto ret = true;
|
||||
|
||||
while (ret == true && pos != string::npos)
|
||||
if (pos != -1)
|
||||
{
|
||||
pos = full_path.find('/', pos + 1);
|
||||
ret = createDir(full_path.substr(0, pos));
|
||||
// key found.
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void cleanupEmptyDirs(const string &path)
|
||||
QStringList lsFilesInDir(const QString &path, const QString &ext)
|
||||
{
|
||||
if (exists(path))
|
||||
{
|
||||
for (auto &entry : directory_iterator(path))
|
||||
{
|
||||
if (entry.is_directory())
|
||||
{
|
||||
try
|
||||
{
|
||||
remove(entry.path());
|
||||
}
|
||||
catch (filesystem_error const &ex)
|
||||
{
|
||||
// non-empty dir assumed when filesystem_error is raised.
|
||||
cleanupEmptyDirs(path + "/" + entry.path().filename().string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
QStringList filters;
|
||||
|
||||
filters << "*" + ext;
|
||||
|
||||
QDir dirObj(path);
|
||||
|
||||
dirObj.setFilter(QDir::Files);
|
||||
dirObj.setNameFilters(filters);
|
||||
dirObj.setSorting(QDir::Name);
|
||||
|
||||
return dirObj.entryList();
|
||||
}
|
||||
|
||||
vector<string> lsFilesInDir(const string &path, const string &ext)
|
||||
QStringList lsDirsInDir(const QString &path)
|
||||
{
|
||||
vector<string> names;
|
||||
QDir dirObj(path);
|
||||
|
||||
if (exists(path))
|
||||
{
|
||||
for (auto &entry : directory_iterator(path))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
auto name = entry.path().filename().string();
|
||||
dirObj.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
dirObj.setSorting(QDir::Name);
|
||||
|
||||
if (ext.empty() || name.ends_with(ext))
|
||||
{
|
||||
names.push_back(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort(names.begin(), names.end());
|
||||
|
||||
return names;
|
||||
return dirObj.entryList();
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
names.push_back(entry.path().filename().string());
|
||||
}
|
||||
if (dir == '-') ret.insert(0, filePath);
|
||||
if (dir == '+') ret.append(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
sort(names.begin(), names.end());
|
||||
|
||||
return names;
|
||||
return ret;
|
||||
}
|
||||
|
||||
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); )
|
||||
{
|
||||
if (line.starts_with("VIDEO_TS/"))
|
||||
{
|
||||
remove(line);
|
||||
}
|
||||
}
|
||||
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs)
|
||||
{
|
||||
return listFacingFiles(path, ext, stamp, secs, '+');
|
||||
}
|
||||
|
||||
void enforceMaxEvents(shared_t *share)
|
||||
|
@ -138,83 +111,81 @@ void enforceMaxEvents(shared_t *share)
|
|||
|
||||
while (names.size() > share->maxEvents)
|
||||
{
|
||||
// removes the video file extension (.mp4).
|
||||
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");
|
||||
auto nameOnly = "events/" + names[0];
|
||||
|
||||
if (exists(mp4File)) remove(mp4File);
|
||||
if (exists(imgFile)) remove(imgFile);
|
||||
if (exists(webFile)) remove(webFile);
|
||||
nameOnly.remove(".mp4");
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
string genTimeStr(const char *fmt)
|
||||
void enforceMaxImages()
|
||||
{
|
||||
time_t rawtime;
|
||||
auto names = lsFilesInDir("img", ".bmp");
|
||||
|
||||
time(&rawtime);
|
||||
|
||||
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 ¶m, const string &line, string *value)
|
||||
{
|
||||
if (line.rfind(param.c_str(), 0) == 0)
|
||||
while (names.size() > MAX_IMAGES)
|
||||
{
|
||||
*value = line.substr(param.size());
|
||||
QFile::remove("img/" + names[0]);
|
||||
|
||||
names.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
void rdLine(const string ¶m, 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 ¶m, 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 ¶m, 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;
|
||||
|
||||
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
|
||||
{
|
||||
string line;
|
||||
QString line;
|
||||
|
||||
do
|
||||
{
|
||||
getline(varFile, line);
|
||||
line = QString::fromUtf8(varFile.readLine());
|
||||
|
||||
if (line.rfind("#", 0) != 0)
|
||||
if (!line.startsWith("#"))
|
||||
{
|
||||
rdLine("cam_name = ", line, &share->camName);
|
||||
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("post_secs = ", line, &share->postSecs);
|
||||
rdLine("post_cmd = ", line, &share->postCmd);
|
||||
rdLine("pix_thresh = ", line, &share->pixThresh);
|
||||
rdLine("img_thresh = ", line, &share->imgThresh);
|
||||
rdLine("frame_gap = ", line, &share->frameGap);
|
||||
rdLine("max_events = ", line, &share->maxEvents);
|
||||
rdLine("max_log_size = ", line, &share->maxLogSize);
|
||||
}
|
||||
|
||||
} while(!line.empty());
|
||||
} while(!line.isEmpty());
|
||||
}
|
||||
|
||||
return share->retCode == 0;
|
||||
|
@ -240,122 +209,100 @@ bool rdConf(const string &filePath, 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 (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);
|
||||
current_path(share->outDir, ec);
|
||||
|
||||
share->retCode = ec.value();
|
||||
|
||||
if (share->retCode != 0)
|
||||
if (!QDir::setCurrent(share->outDir))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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 (; offs < argc; ++offs)
|
||||
for (auto &&proc : procList)
|
||||
{
|
||||
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)
|
||||
{
|
||||
offs++;
|
||||
// check ahead, make sure offs + 1 won't cause out-of-range exception
|
||||
if (offs <= (argc - 1))
|
||||
{
|
||||
ret = string(argv[offs]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = string("true");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QCoreApplication::quit();
|
||||
}
|
||||
|
||||
int MultiInstance::start(const QStringList &args)
|
||||
{
|
||||
auto ret = ENOENT;
|
||||
auto path = QDir().cleanPath(getParam("-d", args));
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
145
src/common.h
145
src/common.h
|
@ -13,89 +13,98 @@
|
|||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <time.h>
|
||||
#include <chrono>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <filesystem>
|
||||
#include <sys/stat.h>
|
||||
#include <map>
|
||||
#include <QCoreApplication>
|
||||
#include <QProcess>
|
||||
#include <QTextStream>
|
||||
#include <QObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QDir>
|
||||
#include <QCryptographicHash>
|
||||
#include <QFile>
|
||||
#include <QDateTime>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QStringList>
|
||||
#include <QMutex>
|
||||
|
||||
#include <opencv4/opencv2/opencv.hpp>
|
||||
#include <opencv4/opencv2/videoio.hpp>
|
||||
|
||||
using namespace cv;
|
||||
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_BIN "mow"
|
||||
#define REC_LOG_NAME "rec_log_lines.html"
|
||||
#define DET_LOG_NAME "det_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
|
||||
{
|
||||
string evName;
|
||||
vector<string> srcPaths;
|
||||
Mat thumbnail;
|
||||
QDateTime timeStamp;
|
||||
QString imgPath;
|
||||
float score;
|
||||
};
|
||||
|
||||
struct shared_t
|
||||
{
|
||||
vector<evt_t> recList;
|
||||
string conf;
|
||||
string recLog;
|
||||
string detLog;
|
||||
string upkLog;
|
||||
string recordUrl;
|
||||
string outDir;
|
||||
string postCmd;
|
||||
string camName;
|
||||
string webBg;
|
||||
string webTxt;
|
||||
string webFont;
|
||||
string webRoot;
|
||||
evt_t curEvent;
|
||||
bool skipCmd;
|
||||
int frameGap;
|
||||
int evMaxSecs;
|
||||
int postSecs;
|
||||
int maxScore;
|
||||
int procCnt;
|
||||
int hlsCnt;
|
||||
int pixThresh;
|
||||
int imgThresh;
|
||||
int maxEvents;
|
||||
int maxLogSize;
|
||||
int retCode;
|
||||
int postInd;
|
||||
int evInd;
|
||||
QList<evt_t> recList;
|
||||
QMutex recMutex;
|
||||
QMutex logMutex;
|
||||
QString conf;
|
||||
QString recLog;
|
||||
QString detLog;
|
||||
QString recordUrl;
|
||||
QString outDir;
|
||||
QString postCmd;
|
||||
QString camName;
|
||||
QString webBg;
|
||||
QString webTxt;
|
||||
QString webFont;
|
||||
QString webRoot;
|
||||
bool skipCmd;
|
||||
int evMaxSecs;
|
||||
int postSecs;
|
||||
int imgThresh;
|
||||
int maxEvents;
|
||||
int maxLogSize;
|
||||
int retCode;
|
||||
};
|
||||
|
||||
string genVidNameFromLive(const string &tsPath);
|
||||
string genEventPath(const string &tsPath);
|
||||
string genEventName(int score);
|
||||
string genDstFile(const string &dirOut, const char *fmt, const string &ext);
|
||||
string genTimeStr(const char *fmt);
|
||||
string cleanDir(const string &path);
|
||||
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs);
|
||||
string parseForParam(const string &arg, int argc, char** argv, bool argOnly);
|
||||
bool createDir(const string &dir);
|
||||
bool createDirTree(const string &full_path);
|
||||
void rdLine(const string ¶m, const string &line, string *value);
|
||||
void rdLine(const string ¶m, const string &line, int *value);
|
||||
void cleanupEmptyDirs(const string &path);
|
||||
void cleanupStream(const string &plsPath);
|
||||
void enforceMaxEvents(shared_t *share);
|
||||
bool rdConf(shared_t *share);
|
||||
vector<string> lsFilesInDir(const string &path, const string &ext = string());
|
||||
vector<string> lsDirsInDir(const string &path);
|
||||
QString getParam(const QString &key, const QStringList &args);
|
||||
QStringList lsFilesInDir(const QString &path, const QString &ext = QString());
|
||||
QStringList lsDirsInDir(const QString &path);
|
||||
QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir);
|
||||
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
|
||||
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
|
||||
bool rdConf(const QString &filePath, shared_t *share);
|
||||
bool rdConf(shared_t *share);
|
||||
void rdLine(const QString ¶m, const QString &line, QString *value);
|
||||
void rdLine(const QString ¶m, const QString &line, int *value);
|
||||
void enforceMaxEvents(shared_t *share);
|
||||
void enforceMaxImages();
|
||||
void enforceMaxVids();
|
||||
|
||||
class MultiInstance : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
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
|
||||
|
|
|
@ -12,58 +12,62 @@
|
|||
|
||||
#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 (exists(filePath))
|
||||
if (file.exists())
|
||||
{
|
||||
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
|
||||
{
|
||||
outFile.open(fileName.c_str());
|
||||
outFile.open(QFile::WriteOnly);
|
||||
}
|
||||
|
||||
outFile << lines;
|
||||
|
||||
outFile.write(lines.toUtf8());
|
||||
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 += "<script>\n";
|
||||
|
@ -106,17 +110,16 @@ void initLogFrontPage(const string &filePath, const string &logLinesFile)
|
|||
htmlText += "</body>\n";
|
||||
htmlText += "</html>\n";
|
||||
|
||||
ofstream outFile(filePath);
|
||||
|
||||
outFile << htmlText;
|
||||
QFile outFile(filePath);
|
||||
|
||||
outFile.open(QFile::WriteOnly);
|
||||
outFile.write(htmlText.toUtf8());
|
||||
outFile.close();
|
||||
}
|
||||
}
|
||||
|
||||
void initLogFrontPages(shared_t *share)
|
||||
void initLogFrontPages()
|
||||
{
|
||||
initLogFrontPage("logs/recording_log.html", REC_LOG_NAME);
|
||||
initLogFrontPage("logs/detection_log.html", DET_LOG_NAME);
|
||||
initLogFrontPage("logs/upkeep_log.html", UPK_LOG_NAME);
|
||||
}
|
||||
|
|
11
src/logger.h
11
src/logger.h
|
@ -15,11 +15,10 @@
|
|||
|
||||
#include "common.h"
|
||||
|
||||
void recLog(const string &line, shared_t *share);
|
||||
void detLog(const string &line, shared_t *share);
|
||||
void upkLog(const string &line, shared_t *share);
|
||||
void dumpLogs(const string &fileName, const string &lines);
|
||||
void enforceMaxLogSize(const string &filePath, shared_t *share);
|
||||
void initLogFrontPages(shared_t *share);
|
||||
void recLog(const QString &line, shared_t *share);
|
||||
void detLog(const QString &line, shared_t *share);
|
||||
void dumpLogs(const QString &fileName, const QString &lines);
|
||||
void enforceMaxLogSize(const QString &filePath, shared_t *share);
|
||||
void initLogFrontPages();
|
||||
|
||||
#endif // lOGGER_H
|
||||
|
|
214
src/main.cpp
214
src/main.cpp
|
@ -10,192 +10,60 @@
|
|||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
#include "mo_detect.h"
|
||||
#include "logger.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);
|
||||
}
|
||||
}
|
||||
#include "common.h"
|
||||
#include "camera.h"
|
||||
|
||||
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;
|
||||
cout << "Usage: mow <argument>" << endl << endl;
|
||||
cout << "-h : display usage information about this application." << endl;
|
||||
cout << "-c : path to the config file." << endl;
|
||||
cout << "-v : display the current version." << endl << endl;
|
||||
QTextStream(stdout) << "Motion Watch " << APP_VER << Qt::endl << Qt::endl;
|
||||
QTextStream(stdout) << "Usage: mow <argument>" << Qt::endl << Qt::endl;
|
||||
QTextStream(stdout) << "-h : display usage information about this application." << Qt::endl;
|
||||
QTextStream(stdout) << "-c : path to the config file used to run a single camera instance." << Qt::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
|
||||
{
|
||||
sharedRes.retCode = 0;
|
||||
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;
|
||||
QTextStream(stderr) << "err: no config file(s) were given in -c" << Qt::endl;
|
||||
}
|
||||
|
||||
return EINVAL;
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
64
src/web.cpp
64
src/web.cpp
|
@ -12,13 +12,13 @@
|
|||
|
||||
#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;
|
||||
vector<string> eveNames;
|
||||
vector<string> dirNames;
|
||||
QStringList logNames;
|
||||
QStringList eveNames;
|
||||
QStringList dirNames;
|
||||
|
||||
string htmlText = "<!DOCTYPE html>\n";
|
||||
QString htmlText = "<!DOCTYPE html>\n";
|
||||
|
||||
htmlText += "<html>\n";
|
||||
htmlText += "<head>\n";
|
||||
|
@ -31,7 +31,7 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
|
|||
htmlText += "<body>\n";
|
||||
htmlText += "<h3>" + title + "</h3>\n";
|
||||
|
||||
if (exists(outputDir + "/live"))
|
||||
if (QDir().exists(outputDir + "/live"))
|
||||
{
|
||||
eveNames = lsFilesInDir(outputDir + "/events", ".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)
|
||||
{
|
||||
// name.substr(0, name.size() - 9) removes _log.html
|
||||
auto name = logName.substr(0, logName.size() - 9);
|
||||
auto name = logName;
|
||||
|
||||
name.remove("_log.html");
|
||||
|
||||
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)
|
||||
{
|
||||
// regName.substr(0, regName.size() - 5) removes .html
|
||||
auto name = eveName.substr(0, eveName.size() - 5);
|
||||
auto name = eveName;
|
||||
|
||||
name.remove(".html");
|
||||
|
||||
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 += "</html>";
|
||||
|
||||
ofstream file(string(cleanDir(outputDir) + "/index.html").c_str());
|
||||
QFile outFile(QDir().cleanPath(outputDir) + "/index.html");
|
||||
|
||||
file << htmlText << endl;
|
||||
|
||||
file.close();
|
||||
outFile.open(QFile::WriteOnly);
|
||||
outFile.write(htmlText.toUtf8());
|
||||
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 += "<head>\n";
|
||||
|
@ -128,16 +130,16 @@ void genHTMLstream(const string &name)
|
|||
htmlText += "</body>\n";
|
||||
htmlText += "</html>";
|
||||
|
||||
ofstream file(string(name + ".html").c_str());
|
||||
QFile outFile(name + ".html");
|
||||
|
||||
file << htmlText << endl;
|
||||
|
||||
file.close();
|
||||
outFile.open(QFile::WriteOnly);
|
||||
outFile.write(htmlText.toUtf8());
|
||||
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 += "<head>\n";
|
||||
|
@ -154,16 +156,16 @@ void genHTMLvod(const string &name)
|
|||
htmlText += "</body>\n";
|
||||
htmlText += "</html>";
|
||||
|
||||
ofstream file(string("events/" + name + ".html").c_str());
|
||||
QFile outFile("events/" + name + ".html");
|
||||
|
||||
file << htmlText << endl;
|
||||
|
||||
file.close();
|
||||
outFile.open(QFile::WriteOnly);
|
||||
outFile.write(htmlText.toUtf8());
|
||||
outFile.close();
|
||||
}
|
||||
|
||||
void genCSS(shared_t *share)
|
||||
{
|
||||
string cssText = "body {\n";
|
||||
QString cssText = "body {\n";
|
||||
|
||||
cssText += " background-color: " + share->webBg + ";\n";
|
||||
cssText += " color: " + share->webTxt + ";\n";
|
||||
|
@ -173,9 +175,9 @@ void genCSS(shared_t *share)
|
|||
cssText += " color: " + share->webTxt + ";\n";
|
||||
cssText += "}\n";
|
||||
|
||||
ofstream file(string(cleanDir(share->webRoot) + "/theme.css").c_str());
|
||||
QFile outFile(QDir().cleanPath(share->webRoot) + "/theme.css");
|
||||
|
||||
file << cssText << endl;
|
||||
|
||||
file.close();
|
||||
outFile.open(QFile::WriteOnly);
|
||||
outFile.write(cssText.toUtf8());
|
||||
outFile.close();
|
||||
}
|
||||
|
|
|
@ -15,9 +15,9 @@
|
|||
|
||||
#include "common.h"
|
||||
|
||||
void genHTMLul(const string &outputDir, const string &title, shared_t *share);
|
||||
void genHTMLstream(const string &name);
|
||||
void genHTMLvod(const string &name);
|
||||
void genHTMLul(const QString &outputDir, const QString &title, shared_t *share);
|
||||
void genHTMLstream(const QString &name);
|
||||
void genHTMLvod(const QString &name);
|
||||
void genCSS(shared_t *share);
|
||||
|
||||
#endif // WEB_H
|
||||
|
|
Loading…
Reference in New Issue
Block a user