Compare commits
No commits in common. "ff5f95f44562b2530c32c9049851f6dac9a6bda5" and "f4ea944f97ccbb6df75256785c66745c637972ec" have entirely different histories.
ff5f95f445
...
f4ea944f97
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -57,5 +57,3 @@ compile_commands.json
|
||||||
|
|
||||||
# Build folders
|
# Build folders
|
||||||
/.build-mow
|
/.build-mow
|
||||||
/.build-opencv
|
|
||||||
/src/opencv
|
|
||||||
|
|
|
@ -1,31 +1,7 @@
|
||||||
cmake_minimum_required(VERSION 3.14)
|
cmake_minimum_required(VERSION 2.8.12)
|
||||||
|
project( MotionWatch )
|
||||||
project(MotionWatch LANGUAGES CXX)
|
find_package( OpenCV REQUIRED )
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pthread")
|
||||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
include_directories( ${OpenCV_INCLUDE_DIRS} )
|
||||||
|
add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp src/web.cpp src/logger.cpp )
|
||||||
set(CMAKE_AUTOUIC ON)
|
target_link_libraries( mow ${OpenCV_LIBS} )
|
||||||
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})
|
|
||||||
|
|
47
README.md
47
README.md
|
@ -4,18 +4,22 @@ 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 used to run a single camera instance.
|
-c : path to the config file(s).
|
||||||
-d : path to a directory that can contain multiple config files.
|
-v : display the current version.
|
||||||
each file found in the directory will be used to run a
|
|
||||||
camera instance.
|
note: multiple -c config files can be passed, reading from left
|
||||||
-v : display the current version.
|
to right. any conflicting values between the files will
|
||||||
|
have the latest value from the latest file overwrite the
|
||||||
|
the earliest.
|
||||||
```
|
```
|
||||||
|
|
||||||
### Config File ###
|
### Config File ###
|
||||||
|
@ -45,20 +49,30 @@ 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.
|
||||||
#
|
#
|
||||||
max_event_secs = 30
|
pix_thresh = 150
|
||||||
# this is the maximum amount of secs of video footage that can be
|
# this value tells the application how far different the pixels need to be
|
||||||
# recorded in a motion event.
|
# 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 = 8000
|
img_thresh = 80000
|
||||||
# this application uses 'magick compare' to score the differences between
|
# this indicates how many pixels need to be different in between frames
|
||||||
# two, one second gapped snapshots of the camera stream. any image pairs
|
# before it is considered motion. any video clips found with frames
|
||||||
# that score greater than this value is considered motion and queues up
|
# exceeding this value will be copied from live footage to event footage.
|
||||||
# max_event_secs worth of hls clips to be written out as a motion event.
|
|
||||||
#
|
#
|
||||||
max_events = 100
|
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
|
# this indicates the maximum amount of motion event video clips to keep
|
||||||
# before deleting the oldest clip.
|
# before deleting the oldest clip.
|
||||||
#
|
#
|
||||||
|
max_event_secs = 10
|
||||||
|
# this is the maximum amount of secs of video footage that can be
|
||||||
|
# recorded in a motion event.
|
||||||
|
#
|
||||||
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
|
||||||
|
@ -91,7 +105,8 @@ 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 the QT API.
|
systems that are capable of installing opencv. The following 3 scripts make
|
||||||
|
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
BIN
bin/magick
Binary file not shown.
|
@ -1,5 +1,4 @@
|
||||||
#!/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
|
||||||
|
|
32
setup.sh
32
setup.sh
|
@ -1,7 +1,29 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
|
||||||
apt update -y
|
apt update -y
|
||||||
apt install -y pkg-config cmake make g++
|
apt install -y pkg-config
|
||||||
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 cmake
|
||||||
cp ./bin/magick /usr/bin/magick
|
apt install -y make
|
||||||
chmod +x /usr/bin/magick
|
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
|
||||||
|
|
485
src/camera.cpp
485
src/camera.cpp
|
@ -1,485 +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 "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
142
src/camera.h
|
@ -1,142 +0,0 @@
|
||||||
#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
|
|
427
src/common.cpp
427
src/common.cpp
|
@ -12,97 +12,124 @@
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
QString getParam(const QString &key, const QStringList &args)
|
string cleanDir(const string &path)
|
||||||
{
|
{
|
||||||
// this can be used by command objects to pick out parameters
|
if (path[path.size() - 1] == '/')
|
||||||
// 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.
|
|
||||||
|
|
||||||
QString ret;
|
|
||||||
|
|
||||||
int pos = args.indexOf(QRegularExpression(key, QRegularExpression::CaseInsensitiveOption));
|
|
||||||
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
{
|
||||||
// key found.
|
return path.substr(0, path.size() - 1);
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool createDir(const string &dir)
|
||||||
|
{
|
||||||
|
auto ret = mkdir(dir.c_str(), 0777);
|
||||||
|
|
||||||
|
if (ret == -1)
|
||||||
|
{
|
||||||
|
return errno == EEXIST;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool createDirTree(const string &full_path)
|
||||||
|
{
|
||||||
|
size_t pos = 0;
|
||||||
|
auto ret = true;
|
||||||
|
|
||||||
|
while (ret == true && pos != string::npos)
|
||||||
|
{
|
||||||
|
pos = full_path.find('/', pos + 1);
|
||||||
|
ret = createDir(full_path.substr(0, pos));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList lsFilesInDir(const QString &path, const QString &ext)
|
void cleanupEmptyDirs(const string &path)
|
||||||
{
|
{
|
||||||
QStringList filters;
|
if (exists(path))
|
||||||
|
{
|
||||||
filters << "*" + ext;
|
for (auto &entry : directory_iterator(path))
|
||||||
|
{
|
||||||
QDir dirObj(path);
|
if (entry.is_directory())
|
||||||
|
{
|
||||||
dirObj.setFilter(QDir::Files);
|
try
|
||||||
dirObj.setNameFilters(filters);
|
{
|
||||||
dirObj.setSorting(QDir::Name);
|
remove(entry.path());
|
||||||
|
}
|
||||||
return dirObj.entryList();
|
catch (filesystem_error const &ex)
|
||||||
|
{
|
||||||
|
// non-empty dir assumed when filesystem_error is raised.
|
||||||
|
cleanupEmptyDirs(path + "/" + entry.path().filename().string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList lsDirsInDir(const QString &path)
|
vector<string> lsFilesInDir(const string &path, const string &ext)
|
||||||
{
|
{
|
||||||
QDir dirObj(path);
|
vector<string> names;
|
||||||
|
|
||||||
dirObj.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
|
if (exists(path))
|
||||||
dirObj.setSorting(QDir::Name);
|
|
||||||
|
|
||||||
return dirObj.entryList();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir)
|
|
||||||
{
|
|
||||||
QStringList ret;
|
|
||||||
|
|
||||||
for (auto i = 0; i < secs; ++i)
|
|
||||||
{
|
{
|
||||||
QString filePath;
|
for (auto &entry : directory_iterator(path))
|
||||||
|
|
||||||
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 (dir == '-') ret.insert(0, filePath);
|
if (entry.is_regular_file())
|
||||||
if (dir == '+') ret.append(filePath);
|
{
|
||||||
|
auto name = entry.path().filename().string();
|
||||||
|
|
||||||
|
if (ext.empty() || name.ends_with(ext))
|
||||||
|
{
|
||||||
|
names.push_back(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
sort(names.begin(), names.end());
|
||||||
|
|
||||||
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs)
|
vector<string> lsDirsInDir(const string &path)
|
||||||
{
|
{
|
||||||
return listFacingFiles(path, ext, stamp, secs, '-');
|
vector<string> names;
|
||||||
|
|
||||||
|
if (exists(path))
|
||||||
|
{
|
||||||
|
for (auto &entry : directory_iterator(path))
|
||||||
|
{
|
||||||
|
if (entry.is_directory())
|
||||||
|
{
|
||||||
|
names.push_back(entry.path().filename().string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort(names.begin(), names.end());
|
||||||
|
|
||||||
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs)
|
void cleanupStream(const string &plsPath)
|
||||||
{
|
{
|
||||||
return listFacingFiles(path, ext, stamp, secs, '+');
|
ifstream fileIn(plsPath);
|
||||||
|
|
||||||
|
for (string line; getline(fileIn, line); )
|
||||||
|
{
|
||||||
|
if (line.starts_with("VIDEO_TS/"))
|
||||||
|
{
|
||||||
|
remove(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void enforceMaxEvents(shared_t *share)
|
void enforceMaxEvents(shared_t *share)
|
||||||
|
@ -111,81 +138,83 @@ void enforceMaxEvents(shared_t *share)
|
||||||
|
|
||||||
while (names.size() > share->maxEvents)
|
while (names.size() > share->maxEvents)
|
||||||
{
|
{
|
||||||
auto nameOnly = "events/" + names[0];
|
// 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");
|
||||||
|
|
||||||
nameOnly.remove(".mp4");
|
if (exists(mp4File)) remove(mp4File);
|
||||||
|
if (exists(imgFile)) remove(imgFile);
|
||||||
|
if (exists(webFile)) remove(webFile);
|
||||||
|
|
||||||
auto mp4File = nameOnly + ".mp4";
|
names.erase(names.begin());
|
||||||
auto imgFile = nameOnly + ".jpg";
|
|
||||||
auto webFile = nameOnly + ".html";
|
|
||||||
|
|
||||||
QFile::remove(mp4File);
|
|
||||||
QFile::remove(imgFile);
|
|
||||||
QFile::remove(webFile);
|
|
||||||
|
|
||||||
names.removeFirst();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void enforceMaxImages()
|
|
||||||
{
|
|
||||||
auto names = lsFilesInDir("img", ".bmp");
|
|
||||||
|
|
||||||
while (names.size() > MAX_IMAGES)
|
string genTimeStr(const char *fmt)
|
||||||
|
{
|
||||||
|
time_t rawtime;
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
QFile::remove("img/" + names[0]);
|
*value = line.substr(param.size());
|
||||||
|
|
||||||
names.removeFirst();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void enforceMaxVids()
|
void rdLine(const string ¶m, const string &line, int *value)
|
||||||
{
|
{
|
||||||
auto names = lsFilesInDir("live", ".ts");
|
if (line.rfind(param.c_str(), 0) == 0)
|
||||||
|
|
||||||
while (names.size() > MAX_VIDEOS)
|
|
||||||
{
|
{
|
||||||
QFile::remove("live/" + names[0]);
|
*value = strtol(line.substr(param.size()).c_str(), NULL, 10);
|
||||||
|
|
||||||
names.removeFirst();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void rdLine(const QString ¶m, const QString &line, QString *value)
|
bool rdConf(const string &filePath, shared_t *share)
|
||||||
{
|
{
|
||||||
if (line.startsWith(param))
|
ifstream varFile(filePath.c_str());
|
||||||
{
|
|
||||||
*value = line.mid(param.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void rdLine(const QString ¶m, const QString &line, int *value)
|
if (!varFile.is_open())
|
||||||
{
|
|
||||||
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;
|
||||||
|
|
||||||
QTextStream(stderr) << "err: config file: " << filePath << " does not exists or lack read permissions." << Qt::endl;
|
cerr << "err: config file: " << filePath << " does not exists or lack read permissions." << endl;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
QString line;
|
string line;
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
line = QString::fromUtf8(varFile.readLine());
|
getline(varFile, line);
|
||||||
|
|
||||||
if (!line.startsWith("#"))
|
if (line.rfind("#", 0) != 0)
|
||||||
{
|
{
|
||||||
rdLine("cam_name = ", line, &share->camName);
|
rdLine("cam_name = ", line, &share->camName);
|
||||||
rdLine("recording_stream = ", line, &share->recordUrl);
|
rdLine("recording_stream = ", line, &share->recordUrl);
|
||||||
|
@ -196,12 +225,14 @@ bool rdConf(const QString &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.isEmpty());
|
} while(!line.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
return share->retCode == 0;
|
return share->retCode == 0;
|
||||||
|
@ -209,100 +240,122 @@ bool rdConf(const QString &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.isEmpty())
|
if (share->camName.empty())
|
||||||
{
|
{
|
||||||
share->camName = QFileInfo(share->conf).fileName();
|
share->camName = path(share->conf).filename();
|
||||||
}
|
}
|
||||||
|
|
||||||
share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName;
|
share->outDir = cleanDir(share->webRoot) + "/" + share->camName;
|
||||||
|
|
||||||
QDir().mkpath(share->outDir);
|
error_code ec;
|
||||||
|
|
||||||
if (!QDir::setCurrent(share->outDir))
|
createDirTree(share->outDir);
|
||||||
|
current_path(share->outDir, ec);
|
||||||
|
|
||||||
|
share->retCode = ec.value();
|
||||||
|
|
||||||
|
if (share->retCode != 0)
|
||||||
{
|
{
|
||||||
QTextStream(stderr) << "err: failed to change/create the current working directory to camera folder: '" << share->outDir << "' does it exists?" << Qt::endl;
|
cerr << "err: " << ec.message() << endl;
|
||||||
|
|
||||||
share->retCode = ENOENT;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return share->retCode == 0;
|
return share->retCode == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
MultiInstance::MultiInstance(QObject *parent) : QObject(parent) {}
|
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs)
|
||||||
|
|
||||||
void MultiInstance::instStdout()
|
|
||||||
{
|
{
|
||||||
for (auto &&proc : procList)
|
auto ret = string();
|
||||||
|
|
||||||
|
for (; offs < argc; ++offs)
|
||||||
{
|
{
|
||||||
QTextStream(stdout) << proc->readAllStandardOutput();
|
auto argInParams = string(argv[offs]);
|
||||||
|
|
||||||
|
if (arg.compare(argInParams) == 0)
|
||||||
|
{
|
||||||
|
if (!argOnly)
|
||||||
|
{
|
||||||
|
offs++;
|
||||||
|
// check ahead, make sure offs + 1 won't cause out-of-range exception
|
||||||
|
if (offs <= (argc - 1))
|
||||||
|
{
|
||||||
|
ret = string(argv[offs]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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
|
else
|
||||||
{
|
{
|
||||||
ret = 0;
|
ret = string("true");
|
||||||
|
}
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
129
src/common.h
129
src/common.h
|
@ -13,98 +13,89 @@
|
||||||
// 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 <QCoreApplication>
|
#include <iostream>
|
||||||
#include <QProcess>
|
#include <fstream>
|
||||||
#include <QTextStream>
|
#include <string>
|
||||||
#include <QObject>
|
#include <time.h>
|
||||||
#include <QRegularExpression>
|
#include <chrono>
|
||||||
#include <QDir>
|
#include <stdlib.h>
|
||||||
#include <QCryptographicHash>
|
#include <errno.h>
|
||||||
#include <QFile>
|
#include <vector>
|
||||||
#include <QDateTime>
|
#include <thread>
|
||||||
#include <QThread>
|
#include <filesystem>
|
||||||
#include <QTimer>
|
#include <sys/stat.h>
|
||||||
#include <QStringList>
|
#include <map>
|
||||||
#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 "3.0.0"
|
#define APP_VER "2.2"
|
||||||
#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
|
||||||
{
|
{
|
||||||
QDateTime timeStamp;
|
string evName;
|
||||||
QString imgPath;
|
vector<string> srcPaths;
|
||||||
float score;
|
Mat thumbnail;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct shared_t
|
struct shared_t
|
||||||
{
|
{
|
||||||
QList<evt_t> recList;
|
vector<evt_t> recList;
|
||||||
QMutex recMutex;
|
string conf;
|
||||||
QMutex logMutex;
|
string recLog;
|
||||||
QString conf;
|
string detLog;
|
||||||
QString recLog;
|
string upkLog;
|
||||||
QString detLog;
|
string recordUrl;
|
||||||
QString recordUrl;
|
string outDir;
|
||||||
QString outDir;
|
string postCmd;
|
||||||
QString postCmd;
|
string camName;
|
||||||
QString camName;
|
string webBg;
|
||||||
QString webBg;
|
string webTxt;
|
||||||
QString webTxt;
|
string webFont;
|
||||||
QString webFont;
|
string webRoot;
|
||||||
QString webRoot;
|
evt_t curEvent;
|
||||||
bool skipCmd;
|
bool skipCmd;
|
||||||
|
int frameGap;
|
||||||
int evMaxSecs;
|
int evMaxSecs;
|
||||||
int postSecs;
|
int postSecs;
|
||||||
|
int maxScore;
|
||||||
|
int procCnt;
|
||||||
|
int hlsCnt;
|
||||||
|
int pixThresh;
|
||||||
int imgThresh;
|
int imgThresh;
|
||||||
int maxEvents;
|
int maxEvents;
|
||||||
int maxLogSize;
|
int maxLogSize;
|
||||||
int retCode;
|
int retCode;
|
||||||
|
int postInd;
|
||||||
|
int evInd;
|
||||||
};
|
};
|
||||||
|
|
||||||
QString getParam(const QString &key, const QStringList &args);
|
string genVidNameFromLive(const string &tsPath);
|
||||||
QStringList lsFilesInDir(const QString &path, const QString &ext = QString());
|
string genEventPath(const string &tsPath);
|
||||||
QStringList lsDirsInDir(const QString &path);
|
string genEventName(int score);
|
||||||
QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir);
|
string genDstFile(const string &dirOut, const char *fmt, const string &ext);
|
||||||
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
|
string genTimeStr(const char *fmt);
|
||||||
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
|
string cleanDir(const string &path);
|
||||||
bool rdConf(const QString &filePath, shared_t *share);
|
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs);
|
||||||
bool rdConf(shared_t *share);
|
string parseForParam(const string &arg, int argc, char** argv, bool argOnly);
|
||||||
void rdLine(const QString ¶m, const QString &line, QString *value);
|
bool createDir(const string &dir);
|
||||||
void rdLine(const QString ¶m, const QString &line, int *value);
|
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);
|
void enforceMaxEvents(shared_t *share);
|
||||||
void enforceMaxImages();
|
bool rdConf(shared_t *share);
|
||||||
void enforceMaxVids();
|
vector<string> lsFilesInDir(const string &path, const string &ext = string());
|
||||||
|
vector<string> lsDirsInDir(const string &path);
|
||||||
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
|
#endif // COMMON_H
|
||||||
|
|
|
@ -12,62 +12,58 @@
|
||||||
|
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
|
|
||||||
void recLog(const QString &line, shared_t *share)
|
void recLog(const string &line, shared_t *share)
|
||||||
{
|
{
|
||||||
share->logMutex.lock();
|
share->recLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
|
||||||
|
|
||||||
share->recLog += QDateTime::currentDateTime().toString("[yyyy-MM-dd-hh-mm-ss] ") + line + "<br>\n";
|
|
||||||
|
|
||||||
share->logMutex.unlock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void detLog(const QString &line, shared_t *share)
|
void detLog(const string &line, shared_t *share)
|
||||||
{
|
{
|
||||||
share->logMutex.lock();
|
share->detLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
|
||||||
|
|
||||||
share->detLog += QDateTime::currentDateTime().toString("[yyyy-MM-dd-hh-mm-ss] ") + line + "<br>\n";
|
|
||||||
|
|
||||||
share->logMutex.unlock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void enforceMaxLogSize(const QString &filePath, shared_t *share)
|
void upkLog(const string &line, shared_t *share)
|
||||||
{
|
{
|
||||||
QFile file(filePath);
|
share->upkLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
|
||||||
|
}
|
||||||
|
|
||||||
if (file.exists())
|
void enforceMaxLogSize(const string &filePath, shared_t *share)
|
||||||
|
{
|
||||||
|
if (exists(filePath))
|
||||||
{
|
{
|
||||||
if (file.size() >= share->maxLogSize)
|
if (file_size(filePath) >= share->maxLogSize)
|
||||||
{
|
{
|
||||||
file.remove();
|
remove(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void dumpLogs(const QString &fileName, const QString &lines)
|
void dumpLogs(const string &fileName, const string &lines)
|
||||||
{
|
{
|
||||||
if (!lines.isEmpty())
|
if (!lines.empty())
|
||||||
{
|
{
|
||||||
QFile outFile(fileName);
|
ofstream outFile;
|
||||||
|
|
||||||
if (outFile.exists())
|
if (exists(fileName))
|
||||||
{
|
{
|
||||||
outFile.open(QFile::Append);
|
outFile.open(fileName.c_str(), ofstream::app);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
outFile.open(QFile::WriteOnly);
|
outFile.open(fileName.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
outFile.write(lines.toUtf8());
|
outFile << lines;
|
||||||
|
|
||||||
outFile.close();
|
outFile.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void initLogFrontPage(const QString &filePath, const QString &logLinesFile)
|
void initLogFrontPage(const string &filePath, const string &logLinesFile)
|
||||||
{
|
{
|
||||||
if (!QFile::exists(filePath))
|
if (!exists(filePath))
|
||||||
{
|
{
|
||||||
QString htmlText = "<!DOCTYPE html>\n";
|
string htmlText = "<!DOCTYPE html>\n";
|
||||||
|
|
||||||
htmlText += "<html>\n";
|
htmlText += "<html>\n";
|
||||||
htmlText += "<script>\n";
|
htmlText += "<script>\n";
|
||||||
|
@ -110,16 +106,17 @@ void initLogFrontPage(const QString &filePath, const QString &logLinesFile)
|
||||||
htmlText += "</body>\n";
|
htmlText += "</body>\n";
|
||||||
htmlText += "</html>\n";
|
htmlText += "</html>\n";
|
||||||
|
|
||||||
QFile outFile(filePath);
|
ofstream outFile(filePath);
|
||||||
|
|
||||||
|
outFile << htmlText;
|
||||||
|
|
||||||
outFile.open(QFile::WriteOnly);
|
|
||||||
outFile.write(htmlText.toUtf8());
|
|
||||||
outFile.close();
|
outFile.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void initLogFrontPages()
|
void initLogFrontPages(shared_t *share)
|
||||||
{
|
{
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
11
src/logger.h
11
src/logger.h
|
@ -15,10 +15,11 @@
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
void recLog(const QString &line, shared_t *share);
|
void recLog(const string &line, shared_t *share);
|
||||||
void detLog(const QString &line, shared_t *share);
|
void detLog(const string &line, shared_t *share);
|
||||||
void dumpLogs(const QString &fileName, const QString &lines);
|
void upkLog(const string &line, shared_t *share);
|
||||||
void enforceMaxLogSize(const QString &filePath, shared_t *share);
|
void dumpLogs(const string &fileName, const string &lines);
|
||||||
void initLogFrontPages();
|
void enforceMaxLogSize(const string &filePath, shared_t *share);
|
||||||
|
void initLogFrontPages(shared_t *share);
|
||||||
|
|
||||||
#endif // lOGGER_H
|
#endif // lOGGER_H
|
||||||
|
|
210
src/main.cpp
210
src/main.cpp
|
@ -10,60 +10,192 @@
|
||||||
// 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 "common.h"
|
#include "mo_detect.h"
|
||||||
#include "camera.h"
|
#include "logger.h"
|
||||||
|
#include "web.h"
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
void timer(shared_t *share)
|
||||||
{
|
{
|
||||||
QCoreApplication app(argc, argv);
|
while (share->retCode == 0)
|
||||||
|
|
||||||
QCoreApplication::setApplicationName(APP_NAME);
|
|
||||||
QCoreApplication::setApplicationVersion(APP_VER);
|
|
||||||
|
|
||||||
auto args = QCoreApplication::arguments();
|
|
||||||
auto ret = 0;
|
|
||||||
|
|
||||||
if (args.contains("-h"))
|
|
||||||
{
|
{
|
||||||
QTextStream(stdout) << "Motion Watch " << APP_VER << Qt::endl << Qt::endl;
|
sleep(1);
|
||||||
QTextStream(stdout) << "Usage: mow <argument>" << Qt::endl << Qt::endl;
|
|
||||||
QTextStream(stdout) << "-h : display usage information about this application." << Qt::endl;
|
share->postInd += 1;
|
||||||
QTextStream(stdout) << "-c : path to the config file used to run a single camera instance." << Qt::endl;
|
share->evInd += 1;
|
||||||
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 (args.contains("-v"))
|
}
|
||||||
|
|
||||||
|
void detectMo(shared_t *share)
|
||||||
|
{
|
||||||
|
while (share->retCode == 0)
|
||||||
{
|
{
|
||||||
QTextStream(stdout) << APP_VER << Qt::endl;
|
sleep(2);
|
||||||
|
detectMoInStream("stream.m3u8", share);
|
||||||
}
|
}
|
||||||
else if (args.contains("-d"))
|
}
|
||||||
{
|
|
||||||
auto *muli = new MultiInstance(&app);
|
|
||||||
|
|
||||||
ret = muli->start(args);
|
void eventLoop(shared_t *share)
|
||||||
|
{
|
||||||
if (ret == 0)
|
while (share->retCode == 0)
|
||||||
{
|
{
|
||||||
ret = QCoreApplication::exec();
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (args.contains("-c"))
|
catch (filesystem_error &ex)
|
||||||
{
|
{
|
||||||
auto *cam = new Camera(&app);
|
recLog(string("err: ") + ex.what(), share);
|
||||||
|
|
||||||
ret = cam->start(args);
|
|
||||||
|
|
||||||
if (ret == 0)
|
|
||||||
{
|
|
||||||
ret = QCoreApplication::exec();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
else
|
||||||
{
|
{
|
||||||
QTextStream(stderr) << "err: no config file(s) were given in -c" << Qt::endl;
|
upkLog("skipping update of the webroot page, it is busy.", share);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
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)
|
||||||
|
{
|
||||||
|
struct shared_t sharedRes;
|
||||||
|
|
||||||
|
sharedRes.conf = parseForParam("-c", argc, argv, false);
|
||||||
|
|
||||||
|
if (parseForParam("-h", argc, argv, true) == "true")
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
else if (parseForParam("-v", argc, argv, true) == "true")
|
||||||
|
{
|
||||||
|
cout << APP_VER << endl;
|
||||||
|
}
|
||||||
|
else if (sharedRes.conf.empty())
|
||||||
|
{
|
||||||
|
cerr << "err: no config file(s) were given in -c" << endl;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EINVAL;
|
||||||
}
|
}
|
||||||
|
|
217
src/mo_detect.cpp
Normal file
217
src/mo_detect.cpp
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
24
src/mo_detect.h
Normal file
24
src/mo_detect.h
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
#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"
|
#include "web.h"
|
||||||
|
|
||||||
void genHTMLul(const QString &outputDir, const QString &title, shared_t *share)
|
void genHTMLul(const string &outputDir, const string &title, shared_t *share)
|
||||||
{
|
{
|
||||||
QStringList logNames;
|
vector<string> logNames;
|
||||||
QStringList eveNames;
|
vector<string> eveNames;
|
||||||
QStringList dirNames;
|
vector<string> dirNames;
|
||||||
|
|
||||||
QString htmlText = "<!DOCTYPE html>\n";
|
string htmlText = "<!DOCTYPE html>\n";
|
||||||
|
|
||||||
htmlText += "<html>\n";
|
htmlText += "<html>\n";
|
||||||
htmlText += "<head>\n";
|
htmlText += "<head>\n";
|
||||||
|
@ -31,7 +31,7 @@ void genHTMLul(const QString &outputDir, const QString &title, shared_t *share)
|
||||||
htmlText += "<body>\n";
|
htmlText += "<body>\n";
|
||||||
htmlText += "<h3>" + title + "</h3>\n";
|
htmlText += "<h3>" + title + "</h3>\n";
|
||||||
|
|
||||||
if (QDir().exists(outputDir + "/live"))
|
if (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,9 +41,8 @@ void genHTMLul(const QString &outputDir, const QString &title, shared_t *share)
|
||||||
|
|
||||||
for (auto &&logName : logNames)
|
for (auto &&logName : logNames)
|
||||||
{
|
{
|
||||||
auto name = logName;
|
// name.substr(0, name.size() - 9) removes _log.html
|
||||||
|
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";
|
||||||
}
|
}
|
||||||
|
@ -59,9 +58,8 @@ void genHTMLul(const QString &outputDir, const QString &title, shared_t *share)
|
||||||
|
|
||||||
for (auto &&eveName : eveNames)
|
for (auto &&eveName : eveNames)
|
||||||
{
|
{
|
||||||
auto name = eveName;
|
// regName.substr(0, regName.size() - 5) removes .html
|
||||||
|
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";
|
||||||
}
|
}
|
||||||
|
@ -83,16 +81,16 @@ void genHTMLul(const QString &outputDir, const QString &title, shared_t *share)
|
||||||
htmlText += "</body>\n";
|
htmlText += "</body>\n";
|
||||||
htmlText += "</html>";
|
htmlText += "</html>";
|
||||||
|
|
||||||
QFile outFile(QDir().cleanPath(outputDir) + "/index.html");
|
ofstream file(string(cleanDir(outputDir) + "/index.html").c_str());
|
||||||
|
|
||||||
outFile.open(QFile::WriteOnly);
|
file << htmlText << endl;
|
||||||
outFile.write(htmlText.toUtf8());
|
|
||||||
outFile.close();
|
file.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void genHTMLstream(const QString &name)
|
void genHTMLstream(const string &name)
|
||||||
{
|
{
|
||||||
QString htmlText = "<!DOCTYPE html>\n";
|
string htmlText = "<!DOCTYPE html>\n";
|
||||||
|
|
||||||
htmlText += "<html>\n";
|
htmlText += "<html>\n";
|
||||||
htmlText += "<head>\n";
|
htmlText += "<head>\n";
|
||||||
|
@ -130,16 +128,16 @@ void genHTMLstream(const QString &name)
|
||||||
htmlText += "</body>\n";
|
htmlText += "</body>\n";
|
||||||
htmlText += "</html>";
|
htmlText += "</html>";
|
||||||
|
|
||||||
QFile outFile(name + ".html");
|
ofstream file(string(name + ".html").c_str());
|
||||||
|
|
||||||
outFile.open(QFile::WriteOnly);
|
file << htmlText << endl;
|
||||||
outFile.write(htmlText.toUtf8());
|
|
||||||
outFile.close();
|
file.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void genHTMLvod(const QString &name)
|
void genHTMLvod(const string &name)
|
||||||
{
|
{
|
||||||
QString htmlText = "<!DOCTYPE html>\n";
|
string htmlText = "<!DOCTYPE html>\n";
|
||||||
|
|
||||||
htmlText += "<html>\n";
|
htmlText += "<html>\n";
|
||||||
htmlText += "<head>\n";
|
htmlText += "<head>\n";
|
||||||
|
@ -156,16 +154,16 @@ void genHTMLvod(const QString &name)
|
||||||
htmlText += "</body>\n";
|
htmlText += "</body>\n";
|
||||||
htmlText += "</html>";
|
htmlText += "</html>";
|
||||||
|
|
||||||
QFile outFile("events/" + name + ".html");
|
ofstream file(string("events/" + name + ".html").c_str());
|
||||||
|
|
||||||
outFile.open(QFile::WriteOnly);
|
file << htmlText << endl;
|
||||||
outFile.write(htmlText.toUtf8());
|
|
||||||
outFile.close();
|
file.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void genCSS(shared_t *share)
|
void genCSS(shared_t *share)
|
||||||
{
|
{
|
||||||
QString cssText = "body {\n";
|
string 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";
|
||||||
|
@ -175,9 +173,9 @@ void genCSS(shared_t *share)
|
||||||
cssText += " color: " + share->webTxt + ";\n";
|
cssText += " color: " + share->webTxt + ";\n";
|
||||||
cssText += "}\n";
|
cssText += "}\n";
|
||||||
|
|
||||||
QFile outFile(QDir().cleanPath(share->webRoot) + "/theme.css");
|
ofstream file(string(cleanDir(share->webRoot) + "/theme.css").c_str());
|
||||||
|
|
||||||
outFile.open(QFile::WriteOnly);
|
file << cssText << endl;
|
||||||
outFile.write(cssText.toUtf8());
|
|
||||||
outFile.close();
|
file.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,9 @@
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
void genHTMLul(const QString &outputDir, const QString &title, shared_t *share);
|
void genHTMLul(const string &outputDir, const string &title, shared_t *share);
|
||||||
void genHTMLstream(const QString &name);
|
void genHTMLstream(const string &name);
|
||||||
void genHTMLvod(const QString &name);
|
void genHTMLvod(const string &name);
|
||||||
void genCSS(shared_t *share);
|
void genCSS(shared_t *share);
|
||||||
|
|
||||||
#endif // WEB_H
|
#endif // WEB_H
|
||||||
|
|
Loading…
Reference in New Issue
Block a user