Compare commits
No commits in common. "528b4105f75e5ab314f7528ab4ea76251c6bfc96" and "01a11741c780113becbea2ce5cec8a27bbe5a1bc" have entirely different histories.
528b4105f7
...
01a11741c7
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -57,3 +57,7 @@ compile_commands.json
|
||||||
|
|
||||||
# Build folders
|
# Build folders
|
||||||
/.build-mow
|
/.build-mow
|
||||||
|
/.build-opencv
|
||||||
|
|
||||||
|
# Opencv src folder
|
||||||
|
/src/opencv
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
cmake_minimum_required(VERSION 2.8.12)
|
cmake_minimum_required(VERSION 2.8.12)
|
||||||
project( MotionWatch )
|
project( MotionWatch )
|
||||||
find_package( OpenCV REQUIRED )
|
find_package( OpenCV REQUIRED )
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pthread")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -pthread")
|
||||||
include_directories( ${OpenCV_INCLUDE_DIRS} )
|
include_directories( ${OpenCV_INCLUDE_DIRS} )
|
||||||
add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp src/web.cpp src/logger.cpp )
|
add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp )
|
||||||
target_link_libraries( mow ${OpenCV_LIBS} )
|
target_link_libraries( mow ${OpenCV_LIBS} )
|
||||||
|
|
131
README.md
131
README.md
|
@ -13,20 +13,15 @@ of this app can be used to operate multiple cameras.
|
||||||
Usage: mow <argument>
|
Usage: mow <argument>
|
||||||
|
|
||||||
-h : display usage information about this application.
|
-h : display usage information about this application.
|
||||||
-c : path to the config file(s).
|
-c : path to the config file.
|
||||||
-v : display the current version.
|
-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.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Config File ###
|
### Config File ###
|
||||||
|
|
||||||
The config file is a simple text file that contain parameters that dictate the
|
The config file is a simple text file that contain parameters that dictate the
|
||||||
behavior of the application. Below is an example of a config file with all
|
behavior of the application. Below is an example of a config file with all
|
||||||
parameters supported and descriptions of each.
|
parameters supported and descriptions of each parameter.
|
||||||
```
|
```
|
||||||
# Motion Watch config file
|
# Motion Watch config file
|
||||||
#
|
#
|
||||||
|
@ -38,109 +33,77 @@ recording_stream = rtsp://1.2.3.4:554/h264
|
||||||
# this is the url to the main stream of the IP camera that will be used
|
# this is the url to the main stream of the IP camera that will be used
|
||||||
# to record footage.
|
# to record footage.
|
||||||
#
|
#
|
||||||
web_root = /var/www/html
|
output_dir = /path/to/footage/directory
|
||||||
# this is the output directory that will be used to store recorded footage
|
# this is the output directory that will be used to store recorded footage
|
||||||
# from the cameras as well as the web interface for the application.
|
# from the camera. the file naming convention uses the current date for
|
||||||
# warning: this will overwrite any existing index.html files so be sure
|
# playlist files which points to hidden video clips taken from the camera.
|
||||||
# to choose a directory that doesn't have an existing website.
|
|
||||||
#
|
#
|
||||||
buff_dir = /tmp
|
buff_dir = /tmp/ramdisk/cam_name
|
||||||
# this application records small clips of the footage from the camera and
|
# this application records small clips of the footage from the camera and
|
||||||
# then stores them into this directory. any clips with motion detected in
|
# then stores them into this directory. any clips with motion detected in
|
||||||
# them are moved to web_root; if no motion is detected, they are deleted.
|
# them are moved to output_dir, if no motion they are deleted. highly
|
||||||
# it is highly recommend to use a ramdisk tempfs for this since this
|
# recommend to use a ramdisk tempfs for this since this directory is used
|
||||||
# directory is used for large amounts of writes.
|
# for lots of writes.
|
||||||
#
|
#
|
||||||
cam_name = cam-1
|
consec_threshold = 512
|
||||||
# this is the optional camera name parameter to identify the camera. this
|
# motion is detected by comparing each frame in the camera feed for
|
||||||
# name will be used to form the directory structure in the web_root as
|
# differences in the pixels. this value determine how many consecutive
|
||||||
# well as buff_dir. if not defined, the name of the config file will be
|
# pixels need to different or how large the suspect object in motion
|
||||||
# used.
|
# needs to be.
|
||||||
#
|
#
|
||||||
pix_thresh = 150
|
block_threshold = 1024
|
||||||
# this value tells the application how far different the pixels need to be
|
# this value tells the application how many "lines" of pixels need to
|
||||||
# before the pixels are actually considered different. think of this as
|
# exceed consec_threshold before being considered motion.
|
||||||
# pixel diff sensitivity, the higher the value the lesser the sensitivity.
|
|
||||||
#
|
#
|
||||||
frame_gap = 20
|
block_x = 64
|
||||||
# this value is used to tell the application how far in between frames to
|
# this is the x coordinate size or horizontal size of a block of pixels
|
||||||
# check the pixel diffs for motion. the lower the value, the more frames
|
# that the application will use to detect the presents of motion.
|
||||||
# will be checked, however with that comes higher cpu usage.
|
|
||||||
#
|
#
|
||||||
img_thresh = 80000
|
block_y = 60
|
||||||
# this indicates how many pixels need to be different in between frame_gap
|
# this is the y coordinate size or vertical size of a block of pixels
|
||||||
# before it is considered motion. any video clips found with frames
|
# that the application will use to detect the presents of motion.
|
||||||
# exceeding this value will be moved from buff_dir to web_root.
|
|
||||||
#
|
#
|
||||||
clip_len = 20
|
duration = 60
|
||||||
# this parameter indicate the amount of seconds to record in each video
|
# this sets the internal timer used to reset to the detection loop and
|
||||||
# clip from the camera that will be stored and then processed in buff_dir.
|
# then call post_cmd if it is defined. note: this time is extended if
|
||||||
#
|
# motion was detected. this will also reload the config file so changes
|
||||||
num_of_clips = 3
|
# to the settings will be applied without restarting the application.
|
||||||
# this will tell the application how many video clips should be recorded
|
|
||||||
# to buff_dir from the camera before the recording loop pauses to do some
|
|
||||||
# house keeping. by house keeping, it will wait until all motion detection
|
|
||||||
# threads are finished, reload the config file and then call the post_cmd
|
|
||||||
# if no motion was detected in any of the video clips.
|
|
||||||
#
|
#
|
||||||
post_cmd = move_the_ptz_camera.py
|
post_cmd = move_the_ptz_camera.py
|
||||||
# this an optional command to run after num_of_clips is met. one great use
|
# this an optional command to run after the internal timer duration has
|
||||||
# for this is to move a ptz camera to the next position of it's patrol
|
# elapsed. one great use for this is to move a ptz camera to the next
|
||||||
# pattern. note: the call to this command will be delayed if motion was
|
# position of it's patrol pattern. note: the call to this command will be
|
||||||
# detected.
|
# delayed if motion was detected.
|
||||||
#
|
#
|
||||||
max_days = 15
|
max_days = 15
|
||||||
# this defines the maximum amount of days worth of video clips that is
|
# this defines the maximum amount of days worth of video clips that is
|
||||||
# allowed to be stored in the web_root. whenever this limit is met, the
|
# allowed to be stored in the output_dir. whenever this limit is met,
|
||||||
# oldest day and all of it's associated video clips will be deleted.
|
# the oldest day is deleted.
|
||||||
#
|
|
||||||
max_clips = 30
|
|
||||||
# this is the maximum amount of video clips that is allowed to be stored
|
|
||||||
# in web_root per day. whenever this limit is met, the oldest clip is
|
|
||||||
# deleted.
|
|
||||||
#
|
|
||||||
max_log_size = 50000
|
|
||||||
# this is the maximum byte size of all log files that can be stored in
|
|
||||||
# web_root. whenever this limit is met, the log file will be deleted and
|
|
||||||
# then eventually recreated blank.
|
|
||||||
#
|
#
|
||||||
vid_container = mp4
|
vid_container = mp4
|
||||||
# this is the video file format to use for recording footage from the
|
# this is the video file format to use from recording footage from the
|
||||||
# camera. the format support depends entirely on the under laying ffmpeg
|
# camera. the format support depends entirely on the under laying ffmpeg
|
||||||
# installation.
|
# installation.
|
||||||
#
|
|
||||||
vid_codec = copy
|
|
||||||
# this is the video codec to use when pulling footage from the camera
|
|
||||||
# via ffmpeg. the default is "copy" meaning it will just match the codec
|
|
||||||
# from the camera itself without trans-coding.
|
|
||||||
#
|
|
||||||
web_text = #dee5ee
|
|
||||||
# this can be used to customize the color of the text in the web
|
|
||||||
# interface. it can be defined as any color understood by html5 standard.
|
|
||||||
#
|
|
||||||
web_bg = #485564
|
|
||||||
# this can be used to customize the background color of the web
|
|
||||||
# interface. just like web_text, it also follows the html5 standard.
|
|
||||||
#
|
|
||||||
web_font = courier
|
|
||||||
# this will customize the text font family to use in the web interface.
|
|
||||||
# it is recommended to use mono-spaced font because this is also used to
|
|
||||||
# display logs and logs are best displayed in mono-spaced font.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Setup/Build/Install ###
|
### Setup/Build/Install ###
|
||||||
|
|
||||||
This application is currently only compatible with a Linux based operating
|
This application is currently only compatible with a Linux based operating
|
||||||
systems that are capable of installing opencv. The following 3 scripts make
|
systems that are capable of building and installing the opencv API from source.
|
||||||
building and then installing convenient.
|
|
||||||
|
The following 3 scripts make this convenient by downloading, compiling and then
|
||||||
|
installing the opencv API for you directly from opencv's git repository. This
|
||||||
|
also makes sure FFMPEG and all of it's dependencies are installed because this
|
||||||
|
application needs it to work properly.
|
||||||
|
|
||||||
|
```
|
||||||
|
note 1: be sure to run setup.sh and install.sh as root (or use sudo).
|
||||||
|
note 2: if building from scratch the following scripts will need to
|
||||||
|
be run in this order - setup.sh -> build.sh -> install.sh.
|
||||||
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
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
|
||||||
sh ./build.sh time or if upgrading from the ground up.
|
sh ./build.sh time or if upgrading from the ground up.
|
||||||
sh ./install.sh
|
sh ./install.sh
|
||||||
```
|
```
|
||||||
```
|
|
||||||
note 1: be sure to run setup.sh and install.sh as root (or use sudo).
|
|
||||||
note 2: if building from scratch the following scripts will need to
|
|
||||||
be run in this order - setup.sh -> build.sh -> install.sh.
|
|
||||||
```
|
|
||||||
|
|
2
build.sh
2
build.sh
|
@ -1,5 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
mkdir -p ./.build-mow
|
mkdir -p ./.build-mow
|
||||||
cd ./.build-mow
|
cd ./.build-mow
|
||||||
cmake ..
|
cmake ..
|
||||||
make -j4
|
make -j4
|
||||||
|
|
||||||
|
|
14
install.sh
14
install.sh
|
@ -1,13 +1,3 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
if [ ! -d "/opt/mow" ]; then
|
|
||||||
mkdir /opt/mow
|
cp ./.build-mow/mow /usr/bin/mow
|
||||||
fi
|
|
||||||
cp ./.build-mow/mow /opt/mow/bin
|
|
||||||
printf "#!/bin/sh\n" > /opt/mow/run
|
|
||||||
printf "export OPENCV_LOG_LEVEL=DEBUG\n" >> /opt/mow/run
|
|
||||||
printf "export OPENCV_VIDEOIO_DEBUG=1\n" >> /opt/mow/run
|
|
||||||
printf "/opt/mow/bin \$1 \$2 \$3\n" >> /opt/mow/run
|
|
||||||
chmod +x /opt/mow/run
|
|
||||||
chmod +x /opt/mow/bin
|
|
||||||
rm /usr/bin/mow
|
|
||||||
ln -s /opt/mow/run /usr/bin/mow
|
|
||||||
|
|
45
setup.sh
45
setup.sh
|
@ -1,28 +1,19 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
apt update -y
|
|
||||||
apt install -y pkg-config
|
apt update
|
||||||
apt install -y cmake
|
apt install -y cmake g++ wget unzip git ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev
|
||||||
apt install -y make
|
cd ./src
|
||||||
apt install -y g++
|
if [ -d "./opencv" ]
|
||||||
apt install -y wget
|
then
|
||||||
apt install -y unzip
|
cd ./opencv
|
||||||
apt install -y git
|
git pull origin
|
||||||
apt install -y ffmpeg
|
cd ..
|
||||||
apt install -y gstreamer1.0*
|
else
|
||||||
apt install -y libavcodec-dev
|
git clone https://github.com/opencv/opencv.git
|
||||||
apt install -y libavformat-dev
|
fi
|
||||||
apt install -y libavutil-dev
|
cd ..
|
||||||
apt install -y libswscale-dev
|
mkdir -p ./.build-opencv
|
||||||
apt install -y libgstreamer1.0-dev
|
cd ./.build-opencv
|
||||||
apt install -y x264
|
cmake ../src/opencv
|
||||||
apt install -y libx264-dev
|
make -j4
|
||||||
apt install -y libopencv-dev
|
make install
|
||||||
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
|
|
||||||
|
|
302
src/common.cpp
302
src/common.cpp
|
@ -52,24 +52,48 @@ bool createDirTree(const string &full_path)
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool fileExists(const string& name)
|
||||||
|
{
|
||||||
|
return access(name.c_str(), F_OK) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void replaceAll(string &str, const string &from, const string &to)
|
||||||
|
{
|
||||||
|
if(from.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
size_t startPos = 0;
|
||||||
|
|
||||||
|
while((startPos = str.find(from, startPos)) != string::npos)
|
||||||
|
{
|
||||||
|
str.replace(startPos, from.length(), to);
|
||||||
|
|
||||||
|
startPos += to.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
vector<string> lsFilesInDir(const string &path, const string &ext)
|
vector<string> lsFilesInDir(const string &path, const string &ext)
|
||||||
{
|
{
|
||||||
|
DIR *dir;
|
||||||
|
struct dirent *ent;
|
||||||
vector<string> names;
|
vector<string> names;
|
||||||
|
|
||||||
if (exists(path))
|
if ((dir = opendir(path.c_str())) != NULL)
|
||||||
{
|
{
|
||||||
for (auto &entry : directory_iterator(path))
|
while ((ent = readdir(dir)) != NULL)
|
||||||
{
|
{
|
||||||
if (entry.is_regular_file())
|
auto name = string(ent->d_name);
|
||||||
{
|
|
||||||
auto name = entry.path().filename().string();
|
|
||||||
|
|
||||||
if (ext.empty() || name.ends_with(ext))
|
if ((name.size() >= 4) && (ent->d_type & DT_REG))
|
||||||
|
{
|
||||||
|
if (name.substr(name.size() - 4) == ext)
|
||||||
{
|
{
|
||||||
names.push_back(name);
|
names.push_back(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closedir(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
sort(names.begin(), names.end());
|
sort(names.begin(), names.end());
|
||||||
|
@ -77,53 +101,18 @@ vector<string> lsFilesInDir(const string &path, const string &ext)
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
vector<string> lsDirsInDir(const string &path)
|
void enforceMaxDays(shared_t *share)
|
||||||
{
|
{
|
||||||
vector<string> names;
|
auto names = lsFilesInDir(share->outDir, ".m3u");
|
||||||
|
|
||||||
if (exists(path))
|
while (names.size() > share->maxDays)
|
||||||
{
|
{
|
||||||
for (auto &entry : directory_iterator(path))
|
auto name = names[0];
|
||||||
{
|
auto plsFile = cleanDir(share->outDir) + "/" + name;
|
||||||
if (entry.is_directory())
|
auto vidFold = cleanDir(share->outDir) + "/." + name.substr(0, name.size() - 4);
|
||||||
{
|
|
||||||
names.push_back(entry.path().filename().string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort(names.begin(), names.end());
|
remove(plsFile.c_str());
|
||||||
|
remove_all(vidFold.c_str());
|
||||||
return names;
|
|
||||||
}
|
|
||||||
|
|
||||||
void enforceMaxDays(const string &dirPath, shared_t *share)
|
|
||||||
{
|
|
||||||
auto names = lsDirsInDir(dirPath);
|
|
||||||
|
|
||||||
while (names.size() > (share->maxDays - 1))
|
|
||||||
{
|
|
||||||
remove_all(string(cleanDir(dirPath) + "/" + names[0]).c_str());
|
|
||||||
|
|
||||||
names.erase(names.begin());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void enforceMaxClips(const string &dirPath, shared_t *share)
|
|
||||||
{
|
|
||||||
auto names = lsFilesInDir(dirPath, "." + share->vidExt);
|
|
||||||
|
|
||||||
while (names.size() > share->maxClips)
|
|
||||||
{
|
|
||||||
// removes the video file extension.
|
|
||||||
auto nameOnly = names[0].substr(0, names[0].size() - (share->vidExt.size() + 1));
|
|
||||||
auto imgFile = cleanDir(dirPath) + "/" + nameOnly + ".jpg";
|
|
||||||
auto webFile = cleanDir(dirPath) + "/" + nameOnly + ".html";
|
|
||||||
|
|
||||||
remove(cleanDir(dirPath) + "/" + names[0]);
|
|
||||||
|
|
||||||
if (exists(imgFile)) remove(imgFile);
|
|
||||||
if (exists(webFile)) remove(webFile);
|
|
||||||
|
|
||||||
names.erase(names.begin());
|
names.erase(names.begin());
|
||||||
}
|
}
|
||||||
|
@ -151,11 +140,22 @@ string genDstFile(const string &dirOut, const char *fmt, const string &ext)
|
||||||
return cleanDir(dirOut) + string("/") + genTimeStr(fmt) + ext;
|
return cleanDir(dirOut) + string("/") + genTimeStr(fmt) + ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Mat toGray(const Mat &src)
|
||||||
|
{
|
||||||
|
Mat ret;
|
||||||
|
|
||||||
|
cvtColor(src, ret, COLOR_BGR2GRAY);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
void rdLine(const string ¶m, const string &line, string *value)
|
void rdLine(const string ¶m, const string &line, string *value)
|
||||||
{
|
{
|
||||||
if (line.rfind(param.c_str(), 0) == 0)
|
if (line.rfind(param.c_str(), 0) == 0)
|
||||||
{
|
{
|
||||||
*value = line.substr(param.size());
|
*value = line.substr(param.size());
|
||||||
|
|
||||||
|
//cout << param << *value << endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,186 +164,154 @@ void rdLine(const string ¶m, const string &line, int *value)
|
||||||
if (line.rfind(param.c_str(), 0) == 0)
|
if (line.rfind(param.c_str(), 0) == 0)
|
||||||
{
|
{
|
||||||
*value = strtol(line.substr(param.size()).c_str(), NULL, 10);
|
*value = strtol(line.substr(param.size()).c_str(), NULL, 10);
|
||||||
|
|
||||||
|
//cout << param << *value << endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool rdConf(const string &filePath, shared_t *share)
|
bool rdConf(shared_t *share)
|
||||||
{
|
{
|
||||||
ifstream varFile(filePath.c_str());
|
ifstream varFile(share->conf.c_str());
|
||||||
|
|
||||||
if (!varFile.is_open())
|
if (!varFile.is_open())
|
||||||
{
|
{
|
||||||
share->retCode = ENOENT;
|
share->retCode = ENOENT;
|
||||||
|
|
||||||
cout << "wrn: config file: " << filePath << " does not exists or lack read permissions." << endl;
|
cerr << "err: Failed to open the config file: " << share->conf << " for reading. please check file permissions or if it exists." << endl;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string line;
|
string line;
|
||||||
|
|
||||||
|
share->recordUrl.clear();
|
||||||
|
share->outDir.clear();
|
||||||
|
share->postCmd.clear();
|
||||||
|
share->buffDir.clear();
|
||||||
|
|
||||||
|
share->consecThresh = 512;
|
||||||
|
share->secs = 60;
|
||||||
|
share->blockX = 32;
|
||||||
|
share->blockY = 32;
|
||||||
|
share->blockThresh = 1024;
|
||||||
|
share->maxDays = 5;
|
||||||
|
share->vidExt = "mp4";
|
||||||
|
share->recLoopWait = false;
|
||||||
|
share->skipCmd = false;
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
getline(varFile, line);
|
getline(varFile, line);
|
||||||
|
|
||||||
if (line.rfind("#", 0) != 0)
|
if (line.rfind("#", 0) != 0)
|
||||||
{
|
{
|
||||||
rdLine("cam_name = ", line, &share->camName);
|
|
||||||
rdLine("recording_stream = ", line, &share->recordUrl);
|
rdLine("recording_stream = ", line, &share->recordUrl);
|
||||||
rdLine("web_root = ", line, &share->webRoot);
|
rdLine("output_dir = ", line, &share->outDir);
|
||||||
rdLine("web_text = ", line, &share->webTxt);
|
|
||||||
rdLine("web_bg = ", line, &share->webBg);
|
|
||||||
rdLine("web_font = ", line, &share->webFont);
|
|
||||||
rdLine("post_cmd = ", line, &share->postCmd);
|
rdLine("post_cmd = ", line, &share->postCmd);
|
||||||
rdLine("clip_len = ", line, &share->clipLen);
|
rdLine("consec_threshold = ", line, &share->consecThresh);
|
||||||
rdLine("num_of_clips = ", line, &share->numOfClips);
|
rdLine("duration = ", line, &share->secs);
|
||||||
rdLine("buff_dir = ", line, &share->buffDir);
|
rdLine("buff_dir = ", line, &share->buffDir);
|
||||||
rdLine("frame_gap = ", line, &share->frameGap);
|
rdLine("block_x = ", line, &share->blockX);
|
||||||
rdLine("pix_thresh = ", line, &share->pixThresh);
|
rdLine("block_y = ", line, &share->blockY);
|
||||||
rdLine("img_thresh = ", line, &share->imgThresh);
|
rdLine("block_threshold = ", line, &share->blockThresh);
|
||||||
rdLine("max_days = ", line, &share->maxDays);
|
rdLine("max_days = ", line, &share->maxDays);
|
||||||
rdLine("max_clips = ", line, &share->maxClips);
|
|
||||||
rdLine("max_log_size = ", line, &share->maxLogSize);
|
|
||||||
rdLine("vid_container = ", line, &share->vidExt);
|
rdLine("vid_container = ", line, &share->vidExt);
|
||||||
rdLine("vid_codec = ", line, &share->vidCodec);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} while(!line.empty());
|
} while(!line.empty());
|
||||||
}
|
|
||||||
|
|
||||||
return share->retCode == 0;
|
// it's imperative that blockX/Y are not zero or it will cause
|
||||||
}
|
// an infinte loop. if bad data is read from the conf, default
|
||||||
|
// values will be used.
|
||||||
bool rdConf(shared_t *share)
|
if (share->blockX == 0) share->blockX = 32;
|
||||||
{
|
if (share->blockY == 0) share->blockY = 32;
|
||||||
share->recordUrl.clear();
|
|
||||||
share->postCmd.clear();
|
|
||||||
share->buffDir.clear();
|
|
||||||
share->camName.clear();
|
|
||||||
share->recLogPath.clear();
|
|
||||||
share->detLogPath.clear();
|
|
||||||
share->recLogFile.close();
|
|
||||||
share->detLogFile.close();
|
|
||||||
|
|
||||||
share->retCode = 0;
|
|
||||||
share->frameGap = 20;
|
|
||||||
share->pixThresh = 150;
|
|
||||||
share->imgThresh = 80000;
|
|
||||||
share->clipLen = 20;
|
|
||||||
share->numOfClips = 3;
|
|
||||||
share->maxDays = 15;
|
|
||||||
share->maxClips = 90;
|
|
||||||
share->maxLogSize = 50000;
|
|
||||||
share->webRoot = "/var/www/html";
|
|
||||||
share->buffDir = "/tmp";
|
|
||||||
share->vidExt = "mp4";
|
|
||||||
share->vidCodec = "copy";
|
|
||||||
share->skipCmd = false;
|
|
||||||
share->webBg = "#485564";
|
|
||||||
share->webTxt = "#dee5ee";
|
|
||||||
share->webFont = "courier";
|
|
||||||
|
|
||||||
auto ret = false;
|
|
||||||
|
|
||||||
for (auto &&confPath: share->conf)
|
|
||||||
{
|
|
||||||
if (rdConf(confPath, share)) ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ret)
|
|
||||||
{
|
|
||||||
if (share->camName.empty())
|
|
||||||
{
|
|
||||||
share->camName = path(share->conf.back()).filename();
|
|
||||||
}
|
|
||||||
|
|
||||||
share->outDir = cleanDir(share->webRoot) + "/" + share->camName;
|
|
||||||
share->buffDir = cleanDir(share->buffDir) + "/" + share->camName;
|
|
||||||
share->recLogPath = share->outDir + "/rec_log_lines.html";
|
|
||||||
share->detLogPath = share->outDir + "/det_log_lines.html";
|
|
||||||
|
|
||||||
if (share->init)
|
if (share->init)
|
||||||
{
|
{
|
||||||
if (exists(share->buffDir))
|
remove_all(share->buffDir.c_str());
|
||||||
{
|
|
||||||
remove_all(share->buffDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
share->init = false;
|
share->init = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
new thread(enforceMaxDays, share);
|
||||||
|
|
||||||
createDirTree(cleanDir(share->buffDir));
|
createDirTree(cleanDir(share->buffDir));
|
||||||
createDirTree(share->outDir);
|
system(string("touch " + cleanDir(share->buffDir) + "/stat").c_str());
|
||||||
|
|
||||||
|
share->retCode = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
varFile.close();
|
||||||
|
|
||||||
|
return share->retCode == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool capPair(Mat &prev, Mat &next, VideoCapture &capture, shared_t *share)
|
||||||
|
{
|
||||||
|
capture >> prev;
|
||||||
|
capture >> next;
|
||||||
|
|
||||||
|
return !prev.empty() && !next.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void wrOut(const string &buffFile, shared_t *share)
|
||||||
|
{
|
||||||
|
auto clnDir = cleanDir(share->outDir);
|
||||||
|
auto vidOut = genDstFile(clnDir + "/." + genTimeStr("%Y-%m-%d"), "%H%M%S", "." + share->vidExt);
|
||||||
|
auto m3uOut = vidOut.substr(clnDir.size() + 1);
|
||||||
|
auto lisOut = genDstFile(share->outDir, "%Y-%m-%d", ".m3u");
|
||||||
|
|
||||||
|
copy_file(buffFile.c_str(), vidOut.c_str());
|
||||||
|
remove(buffFile.c_str());
|
||||||
|
|
||||||
|
ofstream file;
|
||||||
|
|
||||||
|
if (fileExists(lisOut))
|
||||||
|
{
|
||||||
|
file.open(lisOut.c_str(), ios_base::app);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
cerr << "err: none of the expected config files could be read." << endl;
|
file.open(lisOut.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
file << m3uOut << endl;
|
||||||
|
|
||||||
|
file.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs)
|
string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
|
||||||
{
|
{
|
||||||
auto ret = string();
|
for (int i = 0; i < argc; ++i)
|
||||||
|
|
||||||
for (; offs < argc; ++offs)
|
|
||||||
{
|
{
|
||||||
auto argInParams = string(argv[offs]);
|
auto argInParams = string(argv[i]);
|
||||||
|
|
||||||
if (arg.compare(argInParams) == 0)
|
if (arg.compare(argInParams) == 0)
|
||||||
{
|
{
|
||||||
if (!argOnly)
|
if (!argOnly)
|
||||||
{
|
{
|
||||||
offs++;
|
// check ahead, make sure i + 1 won't cause out-of-range exception
|
||||||
// check ahead, make sure offs + 1 won't cause out-of-range exception
|
if ((i + 1) <= (argc - 1))
|
||||||
if (offs <= (argc - 1))
|
|
||||||
{
|
{
|
||||||
ret = string(argv[offs]);
|
return string(argv[i + 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ret = string("true");
|
return string("true");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return string();
|
||||||
}
|
}
|
||||||
|
|
||||||
string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
|
void statOut(shared_t *share)
|
||||||
{
|
{
|
||||||
auto notUsed = 0;
|
system(string("touch " + cleanDir(share->buffDir) + "/stat").c_str());
|
||||||
|
|
||||||
return parseForParam(arg, argc, argv, argOnly, notUsed);
|
auto path = string(cleanDir(share->buffDir) + "/stat");
|
||||||
}
|
auto fd = open(path.c_str(), fstream::out | fstream::trunc);
|
||||||
|
|
||||||
vector<string> parseForList(const string &arg, int argc, char** argv)
|
write(fd, share->stat.c_str(), share->stat.size() + 1);
|
||||||
{
|
close(fd);
|
||||||
auto offs = 0;
|
|
||||||
auto ret = vector<string>();
|
|
||||||
string param;
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
param = parseForParam(arg, argc, argv, false, offs);
|
|
||||||
|
|
||||||
if (!param.empty())
|
|
||||||
{
|
|
||||||
ret.push_back(param);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (!param.empty());
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
void waitForDetThreads(shared_t *share)
|
|
||||||
{
|
|
||||||
for (auto &&thr : share->detThreads)
|
|
||||||
{
|
|
||||||
thr.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
share->detThreads.clear();
|
|
||||||
}
|
}
|
||||||
|
|
64
src/common.h
64
src/common.h
|
@ -22,6 +22,7 @@
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <dirent.h>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
@ -35,57 +36,46 @@ using namespace cv;
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace std::filesystem;
|
using namespace std::filesystem;
|
||||||
|
|
||||||
#define APP_VER "1.6"
|
#define BUF_SZ 10
|
||||||
#define APP_NAME "Motion Watch"
|
#define APP_VER "1.5"
|
||||||
|
|
||||||
struct shared_t
|
struct shared_t
|
||||||
{
|
{
|
||||||
vector<thread> detThreads;
|
string stat;
|
||||||
vector<string> conf;
|
string recordUrl;
|
||||||
ofstream recLogFile;
|
string outDir;
|
||||||
ofstream detLogFile;
|
string postCmd;
|
||||||
string recLogPath;
|
string conf;
|
||||||
string detLogPath;
|
string buffDir;
|
||||||
string recordUrl;
|
string vidExt;
|
||||||
string outDir;
|
bool init;
|
||||||
string postCmd;
|
bool recLoopWait;
|
||||||
string buffDir;
|
bool skipCmd;
|
||||||
string vidExt;
|
int consecThresh;
|
||||||
string vidCodec;
|
int secs;
|
||||||
string camName;
|
int blockThresh;
|
||||||
string webBg;
|
int blockX;
|
||||||
string webTxt;
|
int blockY;
|
||||||
string webFont;
|
int maxDays;
|
||||||
string webRoot;
|
int retCode;
|
||||||
bool init;
|
|
||||||
bool skipCmd;
|
|
||||||
int clipLen;
|
|
||||||
int frameGap;
|
|
||||||
int pixThresh;
|
|
||||||
int imgThresh;
|
|
||||||
int numOfClips;
|
|
||||||
int maxDays;
|
|
||||||
int maxClips;
|
|
||||||
int maxLogSize;
|
|
||||||
int retCode;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
string genDstFile(const string &dirOut, const char *fmt, const string &ext);
|
string genDstFile(const string &dirOut, const char *fmt, const string &ext);
|
||||||
string genTimeStr(const char *fmt);
|
string genTimeStr(const char *fmt);
|
||||||
string cleanDir(const string &path);
|
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);
|
string parseForParam(const string &arg, int argc, char** argv, bool argOnly);
|
||||||
bool createDir(const string &dir);
|
bool createDir(const string &dir);
|
||||||
bool createDirTree(const string &full_path);
|
bool createDirTree(const string &full_path);
|
||||||
void enforceMaxDays(const string &dirPath, shared_t *share);
|
bool fileExists(const string& name);
|
||||||
void enforceMaxClips(const string &dirPath, shared_t *share);
|
void wrOut(const string &buffFile, shared_t *share);
|
||||||
|
void replaceAll(string &str, const string &from, const string &to);
|
||||||
|
void enforceMaxDays(shared_t *share);
|
||||||
void rdLine(const string ¶m, const string &line, string *value);
|
void rdLine(const string ¶m, const string &line, string *value);
|
||||||
void rdLine(const string ¶m, const string &line, int *value);
|
void rdLine(const string ¶m, const string &line, int *value);
|
||||||
void statOut(shared_t *share);
|
void statOut(shared_t *share);
|
||||||
void waitForDetThreads(shared_t *share);
|
|
||||||
bool rdConf(shared_t *share);
|
bool rdConf(shared_t *share);
|
||||||
vector<string> parseForList(const string &arg, int argc, char** argv);
|
bool capPair(Mat &prev, Mat &next, VideoCapture &capture, shared_t *share);
|
||||||
vector<string> lsFilesInDir(const string &path, const string &ext = string());
|
Mat toGray(const Mat &src);
|
||||||
vector<string> lsDirsInDir(const string &path);
|
vector<string> lsFilesInDir(const string &path, const string &ext);
|
||||||
|
|
||||||
#endif // COMMON_H
|
#endif // COMMON_H
|
||||||
|
|
108
src/logger.cpp
108
src/logger.cpp
|
@ -1,108 +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 "logger.h"
|
|
||||||
|
|
||||||
void recLog(const string &line, shared_t *share)
|
|
||||||
{
|
|
||||||
share->recLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "<br>" << endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
void detLog(const string &line, shared_t *share)
|
|
||||||
{
|
|
||||||
share->detLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "<br>" << endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
void enforceMaxLogSize(const string &filePath, shared_t *share)
|
|
||||||
{
|
|
||||||
if (exists(filePath))
|
|
||||||
{
|
|
||||||
if (file_size(filePath) >= share->maxLogSize)
|
|
||||||
{
|
|
||||||
remove(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void initLogFile(const string &filePath, ofstream &fileObj)
|
|
||||||
{
|
|
||||||
if (!fileObj.is_open())
|
|
||||||
{
|
|
||||||
if (!exists(filePath))
|
|
||||||
{
|
|
||||||
system(string("touch " + filePath).c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
fileObj.open(filePath.c_str(), ofstream::app | ofstream::out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void initLogFrontPage(const string &filePath, const string &logLinesFile)
|
|
||||||
{
|
|
||||||
string htmlText = "<!DOCTYPE html>\n";
|
|
||||||
|
|
||||||
htmlText += "<html>\n";
|
|
||||||
htmlText += "<script>\n";
|
|
||||||
htmlText += "function includeHTML() {\n";
|
|
||||||
htmlText += " var z, i, elmnt, file, xhttp;\n";
|
|
||||||
htmlText += " z = document.getElementsByTagName(\"*\");\n";
|
|
||||||
htmlText += " for (i = 0; i < z.length; i++) {\n";
|
|
||||||
htmlText += " elmnt = z[i];\n";
|
|
||||||
htmlText += " file = elmnt.getAttribute(\"include-html\");\n";
|
|
||||||
htmlText += " if (file) {\n";
|
|
||||||
htmlText += " xhttp = new XMLHttpRequest();\n";
|
|
||||||
htmlText += " xhttp.onreadystatechange = function() {\n";
|
|
||||||
htmlText += " if (this.readyState == 4) {\n";
|
|
||||||
htmlText += " if (this.status == 200) {elmnt.innerHTML = this.responseText;}\n";
|
|
||||||
htmlText += " if (this.status == 404) {elmnt.innerHTML = \"Page not found.\";}\n";
|
|
||||||
htmlText += " elmnt.removeAttribute(\"include-html\");\n";
|
|
||||||
htmlText += " includeHTML();\n";
|
|
||||||
htmlText += " }\n";
|
|
||||||
htmlText += " }\n";
|
|
||||||
htmlText += " xhttp.open(\"GET\", file, true);\n";
|
|
||||||
htmlText += " xhttp.send();\n";
|
|
||||||
htmlText += " return;\n";
|
|
||||||
htmlText += " }\n";
|
|
||||||
htmlText += " }\n";
|
|
||||||
htmlText += "};\n";
|
|
||||||
htmlText += "</script>\n";
|
|
||||||
htmlText += "<head>\n";
|
|
||||||
htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n";
|
|
||||||
htmlText += "<meta http-equiv=\"Pragma\" content=\"no-cache\" />\n";
|
|
||||||
htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n";
|
|
||||||
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
|
|
||||||
htmlText += "</head>\n";
|
|
||||||
htmlText += "<body>\n";
|
|
||||||
htmlText += "<p>\n";
|
|
||||||
htmlText += "<div include-html='" + logLinesFile + "'></div>\n";
|
|
||||||
htmlText += "<script>\n";
|
|
||||||
htmlText += "includeHTML();\n";
|
|
||||||
htmlText += "</script>\n";
|
|
||||||
htmlText += "</p>\n";
|
|
||||||
htmlText += "</body>\n";
|
|
||||||
htmlText += "</html>\n";
|
|
||||||
|
|
||||||
ofstream outFile(filePath);
|
|
||||||
|
|
||||||
outFile << htmlText;
|
|
||||||
|
|
||||||
outFile.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void initLogFrontPages(shared_t *share)
|
|
||||||
{
|
|
||||||
auto recLogFilePath = share->outDir + "/recording_log.html";
|
|
||||||
auto detLogFilePath = share->outDir + "/detection_log.html";
|
|
||||||
|
|
||||||
initLogFrontPage(recLogFilePath, path(share->recLogPath).filename().string());
|
|
||||||
initLogFrontPage(detLogFilePath, path(share->detLogPath).filename().string());
|
|
||||||
}
|
|
24
src/logger.h
24
src/logger.h
|
@ -1,24 +0,0 @@
|
||||||
#ifndef lOGGER_H
|
|
||||||
#define lOGGER_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"
|
|
||||||
|
|
||||||
void recLog(const string &line, shared_t *share);
|
|
||||||
void detLog(const string &line, shared_t *share);
|
|
||||||
void enforceMaxLogSize(const string &filePath, shared_t *share);
|
|
||||||
void initLogFile(const string &filePath, ofstream &fileObj);
|
|
||||||
void initLogFrontPages(shared_t *share);
|
|
||||||
|
|
||||||
#endif // lOGGER_H
|
|
155
src/main.cpp
155
src/main.cpp
|
@ -11,119 +11,63 @@
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
#include "mo_detect.h"
|
#include "mo_detect.h"
|
||||||
#include "logger.h"
|
|
||||||
|
|
||||||
void detectMoInFile(const string &bufPath, shared_t *share)
|
void detectLoop(shared_t *share)
|
||||||
{
|
{
|
||||||
detLog("detect_mo_in_file() -- start", share);
|
vector<string> bufFiles;
|
||||||
|
|
||||||
Mat thumbNail;
|
while (!bufFiles.empty() || !share->recLoopWait)
|
||||||
|
|
||||||
if (moDetect(bufPath, thumbNail, share))
|
|
||||||
{
|
{
|
||||||
share->skipCmd = true;
|
bufFiles = lsFilesInDir(share->buffDir, "." + share->vidExt);
|
||||||
|
|
||||||
wrOut(bufPath, thumbNail, share);
|
if ((bufFiles.size() >= 2) || (share->recLoopWait && !bufFiles.empty()))
|
||||||
}
|
{
|
||||||
else if (exists(bufPath))
|
auto fullPath = cleanDir(share->buffDir) + "/" + bufFiles[0];
|
||||||
{
|
|
||||||
remove(bufPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
detLog("detect_mo_in_file() -- finished", share);
|
share->stat.clear();
|
||||||
|
|
||||||
|
if (moDetect(fullPath, share))
|
||||||
|
{
|
||||||
|
share->skipCmd = true;
|
||||||
|
|
||||||
|
wrOut(fullPath, share);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
remove(fullPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
statOut(share);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void recLoop(shared_t *share)
|
void recLoop(shared_t *share)
|
||||||
{
|
{
|
||||||
while (rdConf(share))
|
while (rdConf(share))
|
||||||
{
|
{
|
||||||
recLog("rec_loop() -- start", share);
|
createDirTree(share->buffDir);
|
||||||
|
|
||||||
enforceMaxLogSize(share->recLogPath, share);
|
auto bufPath = cleanDir(share->buffDir) + "/%03d." + share->vidExt;
|
||||||
enforceMaxLogSize(share->detLogPath, share);
|
auto secs = to_string(share->secs);
|
||||||
|
auto limSecs = to_string(share->secs + 3);
|
||||||
|
auto cmd = "timeout -k 1 " + limSecs + " ffmpeg -hide_banner -i " + share->recordUrl + " -y -vcodec copy -map 0 -segment_time 00:00:10 -f segment -t " + secs + " " + bufPath;
|
||||||
|
|
||||||
initLogFile(share->recLogPath, share->recLogFile);
|
thread th2(detectLoop, share);
|
||||||
initLogFile(share->detLogPath, share->detLogFile);
|
|
||||||
|
|
||||||
initLogFrontPages(share);
|
system(cmd.c_str());
|
||||||
|
|
||||||
if (!exists("/tmp/mow-lock"))
|
share->recLoopWait = true;
|
||||||
{
|
|
||||||
system("touch /tmp/mow-lock");
|
|
||||||
|
|
||||||
genCSS(share);
|
th2.join();
|
||||||
genHTMLul(share->webRoot, string(APP_NAME) + " " + string(APP_VER), share);
|
|
||||||
|
|
||||||
remove("/tmp/mow-lock");
|
|
||||||
recLog("webroot page updated: " + cleanDir(share->webRoot) + "/index.html", share);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
recLog("skipping update of the webroot page, it is busy.", share);
|
|
||||||
}
|
|
||||||
|
|
||||||
genHTMLul(share->outDir, share->camName, share);
|
|
||||||
|
|
||||||
recLog("camera specific webroot page updated: " + share->outDir + "/index.html", share);
|
|
||||||
|
|
||||||
for (auto i = 0; i < share->numOfClips; ++i)
|
|
||||||
{
|
|
||||||
auto bufPath = cleanDir(share->buffDir) + "/" + to_string(i) + "." + share->vidExt;
|
|
||||||
auto cmd = "timeout -k 1 " + to_string(share->clipLen + 2) + " ";
|
|
||||||
|
|
||||||
cmd += "ffmpeg -hide_banner -i " + share->recordUrl + " -y -vcodec " + share->vidCodec + " -movflags faststart -t " + to_string(share->clipLen) + " " + bufPath;
|
|
||||||
|
|
||||||
recLog("ffmpeg_run: " + cmd, share);
|
|
||||||
|
|
||||||
auto retCode = system(cmd.c_str());
|
|
||||||
|
|
||||||
recLog("ffmpeg_retcode: " + to_string(retCode), share);
|
|
||||||
|
|
||||||
if (retCode == 0)
|
|
||||||
{
|
|
||||||
recLog("detect_mo_in_file() -- started in a seperate thread.", share);
|
|
||||||
|
|
||||||
share->detThreads.push_back(thread(detectMoInFile, bufPath, share));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
recLog("ffmpeg returned non zero, indicating failure. please check stderr output.", share);
|
|
||||||
|
|
||||||
if (exists(bufPath))
|
|
||||||
{
|
|
||||||
remove(bufPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
sleep(share->clipLen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForDetThreads(share);
|
|
||||||
|
|
||||||
if (!share->skipCmd)
|
if (!share->skipCmd)
|
||||||
{
|
{
|
||||||
recLog("no motion detected", share);
|
system(share->postCmd.c_str());
|
||||||
|
|
||||||
if (share->postCmd.empty())
|
|
||||||
{
|
|
||||||
recLog("post command not defined, skipping.", share);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
recLog("running post command: " + share->postCmd, share);
|
|
||||||
system(share->postCmd.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
recLog("motion detected, skipping the post command.", share);
|
|
||||||
}
|
|
||||||
|
|
||||||
recLog("rec_loop() -- finished", share);
|
|
||||||
|
|
||||||
if (share->retCode != 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,35 +76,34 @@ int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
struct shared_t sharedRes;
|
struct shared_t sharedRes;
|
||||||
|
|
||||||
sharedRes.conf = parseForList("-c", argc, argv);
|
sharedRes.conf = parseForParam("-c", argc, argv, false);
|
||||||
|
|
||||||
if (parseForParam("-h", argc, argv, true) == "true")
|
if (parseForParam("-h", argc, argv, true) == "true")
|
||||||
{
|
{
|
||||||
cout << "Motion Watch " << APP_VER << endl << endl;
|
cout << "Motion Watch " << APP_VER << endl << endl;
|
||||||
cout << "Usage: mow <argument>" << endl << endl;
|
cout << "Usage: mow <argument>" << endl << endl;
|
||||||
cout << "-h : display usage information about this application." << endl;
|
cout << "-h : display usage information about this application." << endl;
|
||||||
cout << "-c : path to a config file." << endl;
|
cout << "-c : path to the config file." << endl;
|
||||||
cout << "-v : display the current version." << endl << endl;
|
cout << "-v : display the current version." << endl;
|
||||||
cout << "note: multiple -c config files can be passed, reading from left" << endl;
|
|
||||||
cout << " to right. any conflicting values between the files will" << endl;
|
|
||||||
cout << " have the latest value from the latest file overwrite the" << endl;
|
|
||||||
cout << " the earliest." << endl;
|
|
||||||
}
|
}
|
||||||
else if (parseForParam("-v", argc, argv, true) == "true")
|
if (parseForParam("-v", argc, argv, true) == "true")
|
||||||
{
|
{
|
||||||
cout << APP_VER << endl;
|
cout << APP_VER << endl;
|
||||||
}
|
}
|
||||||
else if (sharedRes.conf.empty())
|
else if (sharedRes.conf.empty())
|
||||||
{
|
{
|
||||||
cerr << "err: no config file(s) were given in -c" << endl;
|
cerr << "err: A config file was not given in -c" << endl;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
sharedRes.retCode = 0;
|
sharedRes.retCode = 0;
|
||||||
sharedRes.skipCmd = false;
|
sharedRes.recLoopWait = false;
|
||||||
sharedRes.init = true;
|
sharedRes.skipCmd = false;
|
||||||
|
sharedRes.init = true;
|
||||||
|
|
||||||
recLoop(&sharedRes);
|
thread th1(recLoop, &sharedRes);
|
||||||
|
|
||||||
|
th1.join();
|
||||||
|
|
||||||
return sharedRes.retCode;
|
return sharedRes.retCode;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,131 +12,124 @@
|
||||||
|
|
||||||
#include "mo_detect.h"
|
#include "mo_detect.h"
|
||||||
|
|
||||||
|
bool pixDiff(const uchar &pixA, const uchar &pixB, shared_t *share)
|
||||||
|
{
|
||||||
|
if (pixA > pixB) return true;
|
||||||
|
if (pixB > pixA) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void secDiff(const Mat &imgA, const Mat &imgB, int id, int rows, int cols, int rowOffs, int colOffs, vector<sec_t> *results, mutex *secMutex, shared_t *share)
|
||||||
|
{
|
||||||
|
auto diff = 0;
|
||||||
|
auto pnts = 0;
|
||||||
|
|
||||||
|
for (auto y = rowOffs; y < (rowOffs + rows); y++)
|
||||||
|
{
|
||||||
|
for (auto x = colOffs; x < (colOffs + cols); x++)
|
||||||
|
{
|
||||||
|
auto pixA = imgA.at<uchar>(Point(x, y));
|
||||||
|
auto pixB = imgB.at<uchar>(Point(x, y));
|
||||||
|
|
||||||
|
if (pixDiff(pixA, pixB, share)) pnts += 1;
|
||||||
|
else pnts = 0;
|
||||||
|
|
||||||
|
if (pnts >= share->consecThresh)
|
||||||
|
{
|
||||||
|
diff += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sec_t res;
|
||||||
|
|
||||||
|
res.x = colOffs;
|
||||||
|
res.y = rowOffs;
|
||||||
|
res.xSize = cols;
|
||||||
|
res.ySize = rows;
|
||||||
|
res.pixDiff = diff;
|
||||||
|
res.id = id;
|
||||||
|
|
||||||
|
lock_guard<mutex> guard(*secMutex);
|
||||||
|
|
||||||
|
results->push_back(res);
|
||||||
|
}
|
||||||
|
|
||||||
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share)
|
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share)
|
||||||
{
|
{
|
||||||
auto ret = false;
|
vector<thread> threads;
|
||||||
|
vector<sec_t> results;
|
||||||
|
mutex secMutex;
|
||||||
|
|
||||||
detLog("img_diff() -- start()", share);
|
auto id = 0;
|
||||||
|
|
||||||
if (prev.empty()) detLog("prev_frame is empty -- Borken frame from the camera assumed.", share);
|
for (auto x = 0; x < prev.cols; x += share->blockX)
|
||||||
if (next.empty()) detLog("next_frame is empty -- EOF assumed.", share);
|
|
||||||
|
|
||||||
if (!prev.empty() && !next.empty())
|
|
||||||
{
|
{
|
||||||
Mat diff;
|
// spawn all of the block motion detection threads.
|
||||||
|
for (auto y = 0; y < prev.rows; y += share->blockY, id += 1)
|
||||||
absdiff(prev, next, diff);
|
{
|
||||||
threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY);
|
threads.push_back(thread(secDiff, prev, next, id, share->blockY, share->blockX, y, x, &results, &secMutex, share));
|
||||||
|
}
|
||||||
auto diffScore = countNonZero(diff);
|
|
||||||
|
|
||||||
detLog("diff_score: " + to_string(diffScore), share);
|
|
||||||
|
|
||||||
ret = diffScore >= share->imgThresh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
detLog("img_diff() -- finished()", share);
|
for (auto &&thr : threads)
|
||||||
|
{
|
||||||
|
// wait for all of the threads to finish.
|
||||||
|
thr.join();
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
auto maxPixDiff = 0;
|
||||||
|
auto blockPick = 0;
|
||||||
|
|
||||||
|
for (auto i = 0; i < results.size(); ++i)
|
||||||
|
{
|
||||||
|
// out of all of the results returned form the threads, pick
|
||||||
|
// the block with the highest amount of pixDiff.
|
||||||
|
auto x = results[i].x;
|
||||||
|
auto y = results[i].y;
|
||||||
|
auto diff = results[i].pixDiff;
|
||||||
|
auto id = results[i].id;
|
||||||
|
|
||||||
|
if (diff > 0)
|
||||||
|
{
|
||||||
|
share->stat += string("block_thread:") + " id=" + to_string(id) + " diff=" + to_string(diff) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((results[i].pixDiff >= share->blockThresh) && (results[i].pixDiff > maxPixDiff))
|
||||||
|
{
|
||||||
|
maxPixDiff = results[i].pixDiff;
|
||||||
|
blockPick = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxPixDiff >= share->blockThresh;
|
||||||
}
|
}
|
||||||
|
|
||||||
Mat frameFF(VideoCapture *cap, int gap)
|
bool moDetect(const string &buffFile, shared_t *share)
|
||||||
{
|
{
|
||||||
Mat ret;
|
|
||||||
|
|
||||||
if (gap == 0) gap = 1;
|
|
||||||
|
|
||||||
for (int i = 0; i < gap; ++i)
|
|
||||||
{
|
|
||||||
cap->grab();
|
|
||||||
}
|
|
||||||
|
|
||||||
cap->retrieve(ret);
|
|
||||||
|
|
||||||
if (!ret.empty())
|
|
||||||
{
|
|
||||||
cvtColor(ret, ret, COLOR_BGR2GRAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share)
|
|
||||||
{
|
|
||||||
detLog("wr_out() -- start()", share);
|
|
||||||
detLog("buff_file: " + buffFile, share);
|
|
||||||
|
|
||||||
auto dayStr = genTimeStr("%Y-%m-%d");
|
|
||||||
auto timStr = genTimeStr("%H%M%S");
|
|
||||||
auto outDir = cleanDir(share->outDir) + "/" + dayStr;
|
|
||||||
|
|
||||||
if (!exists(outDir))
|
|
||||||
{
|
|
||||||
enforceMaxDays(share->outDir, share);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto vidOut = genDstFile(outDir, timStr.c_str(), "." + share->vidExt);
|
|
||||||
auto imgOut = genDstFile(outDir, timStr.c_str(), ".jpg");
|
|
||||||
|
|
||||||
detLog("write_out_vid: " + vidOut, share);
|
|
||||||
detLog("write_out_img: " + imgOut, share);
|
|
||||||
|
|
||||||
enforceMaxClips(outDir, share);
|
|
||||||
|
|
||||||
copy_file(buffFile.c_str(), vidOut.c_str());
|
|
||||||
remove(buffFile.c_str());
|
|
||||||
|
|
||||||
imwrite(imgOut.c_str(), vidThumb);
|
|
||||||
|
|
||||||
genHTMLvid(vidOut, share);
|
|
||||||
genHTMLul(outDir, share->camName + ": " + dayStr, share);
|
|
||||||
genHTMLul(share->outDir, share->camName, share);
|
|
||||||
|
|
||||||
detLog("wr_out() -- finished()", share);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
|
|
||||||
{
|
|
||||||
detLog("mo_detect() -- start()", share);
|
|
||||||
detLog("buff_file: " + buffFile, share);
|
|
||||||
|
|
||||||
auto mod = false;
|
auto mod = false;
|
||||||
|
|
||||||
VideoCapture capture(buffFile.c_str(), CAP_FFMPEG);
|
VideoCapture capture(buffFile.c_str(), CAP_FFMPEG);
|
||||||
|
|
||||||
if (capture.isOpened())
|
if (capture.isOpened())
|
||||||
{
|
{
|
||||||
Mat prev;
|
Mat prev;
|
||||||
Mat next;
|
Mat next;
|
||||||
auto frameCount = capture.get(CAP_PROP_FRAME_COUNT);
|
|
||||||
auto frameGaps = frameCount / share->frameGap;
|
|
||||||
|
|
||||||
detLog("frame_count: " + to_string(frameCount), share);
|
while (capPair(prev, next, capture, share))
|
||||||
detLog("frame_gaps: " + to_string(frameGaps), share);
|
|
||||||
|
|
||||||
for (auto i = 0; i < frameGaps; i++)
|
|
||||||
{
|
{
|
||||||
if (prev.empty()) prev = frameFF(&capture, 1);
|
if (imgDiff(toGray(prev), toGray(next), share))
|
||||||
else prev = next.clone();
|
|
||||||
|
|
||||||
next = frameFF(&capture, share->frameGap);
|
|
||||||
|
|
||||||
if (imgDiff(prev, next, share))
|
|
||||||
{
|
{
|
||||||
resize(next, vidThumb, Size(720, 480), INTER_LINEAR);
|
mod = true; break;
|
||||||
|
|
||||||
mod = true; break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
detLog("failed to open the buff file for reading. check permissions and/or opencv's video-io support (gstreamer/ffmpeg).", share);
|
cerr << "err: Could not open buff file: " << buffFile << " for reading. check formatting/permissions." << endl;
|
||||||
|
cerr << " Also check if opencv was compiled with FFMPEG encoding enabled." << endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
capture.release();
|
|
||||||
|
|
||||||
detLog("mo_detect() -- finished()", share);
|
|
||||||
|
|
||||||
return mod;
|
return mod;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,20 @@
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "web.h"
|
|
||||||
#include "logger.h"
|
|
||||||
|
|
||||||
|
struct sec_t
|
||||||
|
{
|
||||||
|
int id;
|
||||||
|
int x;
|
||||||
|
int y;
|
||||||
|
int xSize;
|
||||||
|
int ySize;
|
||||||
|
int pixDiff;
|
||||||
|
};
|
||||||
|
|
||||||
|
void secDiff(const Mat &imgA, const Mat &imgB, int id, int rows, int cols, int rowOffs, int colOffs, vector<sec_t> *results, mutex *secMutex, shared_t *share);
|
||||||
|
bool pixDiff(const uchar &pixA, const uchar &pixB, shared_t *share);
|
||||||
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share);
|
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share);
|
||||||
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share);
|
bool moDetect(const string &buffFile, shared_t *share);
|
||||||
void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share);
|
|
||||||
Mat frameFF(VideoCapture *cap, int gap);
|
|
||||||
|
|
||||||
#endif // MO_DETECT_H
|
#endif // MO_DETECT_H
|
||||||
|
|
134
src/web.cpp
134
src/web.cpp
|
@ -1,134 +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 "web.h"
|
|
||||||
|
|
||||||
void genHTMLul(const string &outputDir, const string &title, shared_t *share)
|
|
||||||
{
|
|
||||||
vector<string> logNames;
|
|
||||||
vector<string> regNames = lsFilesInDir(outputDir);
|
|
||||||
vector<string> dirNames = lsDirsInDir(outputDir);
|
|
||||||
|
|
||||||
string htmlText = "<!DOCTYPE html>\n";
|
|
||||||
|
|
||||||
htmlText += "<html>\n";
|
|
||||||
htmlText += "<head>\n";
|
|
||||||
htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n";
|
|
||||||
htmlText += "<meta http-equiv=\"Pragma\" content=\"no-cache\" />\n";
|
|
||||||
htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n";
|
|
||||||
htmlText += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n";
|
|
||||||
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
|
|
||||||
htmlText += "</head>\n";
|
|
||||||
htmlText += "<body>\n";
|
|
||||||
htmlText += "<h3>" + title + "</h3>\n";
|
|
||||||
|
|
||||||
if (!dirNames.empty())
|
|
||||||
{
|
|
||||||
htmlText += "<ul>\n";
|
|
||||||
|
|
||||||
for (auto &&dirName : dirNames)
|
|
||||||
{
|
|
||||||
htmlText += " <li><a href='" + dirName + "/index.html'>" + dirName + "</a></li>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlText += "</ul>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto &®Name : regNames)
|
|
||||||
{
|
|
||||||
if (regName.ends_with("_log.html"))
|
|
||||||
{
|
|
||||||
logNames.push_back(regName);
|
|
||||||
}
|
|
||||||
else if (regName.ends_with(".html") &&
|
|
||||||
!regName.ends_with("index.html") &&
|
|
||||||
!regName.ends_with("rec_log_lines.html") &&
|
|
||||||
!regName.ends_with("det_log_lines.html"))
|
|
||||||
{
|
|
||||||
// regName.substr(0, regName.size() - 5) removes .html
|
|
||||||
auto name = regName.substr(0, regName.size() - 5);
|
|
||||||
|
|
||||||
htmlText += "<a href='" + regName + "'><img src='" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!logNames.empty())
|
|
||||||
{
|
|
||||||
htmlText += "<h4>Logs</h4>\n";
|
|
||||||
htmlText += "<ul>\n";
|
|
||||||
|
|
||||||
for (auto &&name : logNames)
|
|
||||||
{
|
|
||||||
// name.substr(0, name.size() - 9) removes _log.html
|
|
||||||
htmlText += " <li><a href='" + name + "'>" + name.substr(0, name.size() - 9) + "</a></li>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlText += "</ul>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlText += "</body>\n";
|
|
||||||
htmlText += "</html>";
|
|
||||||
|
|
||||||
ofstream file(string(cleanDir(outputDir) + "/index.html").c_str());
|
|
||||||
|
|
||||||
file << htmlText << endl;
|
|
||||||
|
|
||||||
file.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void genHTMLvid(const string &outputVid, shared_t *share)
|
|
||||||
{
|
|
||||||
auto vidName = path(outputVid).filename().string();
|
|
||||||
auto filePath = path(outputVid).parent_path().string();
|
|
||||||
auto fileName = vidName.substr(0, vidName.size() - (share->vidExt.size() + 1));
|
|
||||||
string htmlText = "<!DOCTYPE html>\n";
|
|
||||||
|
|
||||||
htmlText += "<html>\n";
|
|
||||||
htmlText += "<head>\n";
|
|
||||||
htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n";
|
|
||||||
htmlText += "<meta http-equiv=\"Pragma\" content=\"no-cache\" />\n";
|
|
||||||
htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n";
|
|
||||||
htmlText += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n";
|
|
||||||
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
|
|
||||||
htmlText += "</head>\n";
|
|
||||||
htmlText += "<body>\n";
|
|
||||||
htmlText += "<video width=100% height=100% controls autoplay>\n";
|
|
||||||
htmlText += " <source src='" + vidName + "' type='video/" + share->vidExt + "'>\n";
|
|
||||||
htmlText += "</video>\n";
|
|
||||||
htmlText += "</body>\n";
|
|
||||||
htmlText += "</html>";
|
|
||||||
|
|
||||||
ofstream file(string(filePath + "/" + fileName + ".html").c_str());
|
|
||||||
|
|
||||||
file << htmlText << endl;
|
|
||||||
|
|
||||||
file.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void genCSS(shared_t *share)
|
|
||||||
{
|
|
||||||
string cssText = "body {\n";
|
|
||||||
|
|
||||||
cssText += " background-color: " + share->webBg + ";\n";
|
|
||||||
cssText += " color: " + share->webTxt + ";\n";
|
|
||||||
cssText += " font-family: " + share->webFont + ";\n";
|
|
||||||
cssText += "}\n";
|
|
||||||
cssText += "a {\n";
|
|
||||||
cssText += " color: " + share->webTxt + ";\n";
|
|
||||||
cssText += "}\n";
|
|
||||||
|
|
||||||
ofstream file(string(cleanDir(share->webRoot) + "/theme.css").c_str());
|
|
||||||
|
|
||||||
file << cssText << endl;
|
|
||||||
|
|
||||||
file.close();
|
|
||||||
}
|
|
22
src/web.h
22
src/web.h
|
@ -1,22 +0,0 @@
|
||||||
#ifndef WEB_H
|
|
||||||
#define WEB_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"
|
|
||||||
|
|
||||||
void genHTMLul(const string &outputDir, const string &title, shared_t *share);
|
|
||||||
void genHTMLvid(const string &outputVid, shared_t *share);
|
|
||||||
void genCSS(shared_t *share);
|
|
||||||
|
|
||||||
#endif // WEB_H
|
|
Loading…
Reference in New Issue
Block a user