Compare commits

...

20 Commits

Author SHA1 Message Date
Maurice ONeal
528b4105f7 v1.6
The app can be considered in stable state, releasing to master branch.
2022-12-24 21:16:49 -05:00
Maurice ONeal
62b2bfd76b v1.5.t19
Added the ability to read multiple config files so it's now possible to
load a singular global config and then load a camera specific config in
another.

Many elements in the web interface are coming out too small. Added meta
viewport device width in hopes that the web interface will self adjust
to device it is being displayed on.

Changed duration to num_of_clips and added clip_len so the amount of
seconds in each clip and the amount of clips to be processed for motion
are now adjustable.

Adjusted a several default values.
2022-12-24 13:48:51 -05:00
Maurice ONeal
4e44111ea8 v1.5.t18
Discovered that the "ffmpeg stall" is still an issue so I put the
timeout call back into the ffmpeg command.

Changed the default pix_thresh to 10000.
2022-12-22 20:15:14 -05:00
Maurice ONeal
736b8c5ec4 v1.5.t17
Fixed an invalid argument in the ffmpeg command from vcodec to -vcodec.

Also added a 10sec delay to simulate a running ffmpeg if it fails for
what ever reason.
2022-12-21 20:44:58 -05:00
Maurice ONeal
b470915276 v1.5.t16
The app is still failing with intermittent moov atom failures on trying
to open the video clips with opencv VideoCapture. I suspect it is trying
to open the files while ffmpeg is not done finalizing them. Reformed the
detection loop to spawn dedicated motion detection threads for each
video clip and only when ffmpeg confirmed finished.
2022-12-21 20:30:37 -05:00
Maurice ONeal
ad4a6357b4 v1.5.t15
Thanks the debug messages, in found a potential issue with the video
clips being pulled from the in house test cameras. Sometimes the video
clips are being pulled with incomplete meta information causing opencv
to fail to open the clips. Added "-movflags faststart" to the ffmpeg
command that should hopefully fix this and should help the app to handle
unreliable camera streams more gracefully.

max_clips now defaults to 90 instead of 30.
2022-12-20 20:25:54 -05:00
Maurice ONeal
4667cf2e96 v1.5.t14
Added another debug clause for opencv videoio so it will provide even
more debug information. Going back to implicitly defining FFMPEG as the
videoio for opencv, turns out FFMPEG is the only real stable option to
use when it comes to reading video files with opencv. Any other option
would just severely limit codec and container support.
2022-12-20 19:29:36 -05:00
Maurice ONeal
2e10d31ab6 v1.5.t13
Added the ability change the video codec via the config file.

Changed the install script to now install the application in the /opt
directory and then symm link to /usr/bin. Doing this allowed me to
create a run script to start the application and enable the
OPENCV_VIDEOIO_DEBUG parameter for opencv. This should make it easier to
diagnose video-io issues with opencv.

Updated the README documentation with all of the changes done to the
application since v1.5.
2022-12-18 10:25:46 -05:00
Maurice ONeal
f8f7564911 v1.5.t12
Turned off caching for all web interface pages. Opencv will no longer be
compiled from source as part the setup.sh script, instead the
libopencv-dev package will be installed.
2022-12-17 20:40:52 -05:00
Maurice ONeal
523ff57215 v1.5.t11
Found the infinite loop issue in moDetect(), turns out the frame
parameters at some point were never returning empty, hence moDetect()
would continue into perpetuity. Changed the loop structure to use a
fixed frame count instead of relying on frameFF() to return empty on
EOF.
2022-12-17 10:34:40 -05:00
Maurice ONeal
c153fdcd21 v1.5.t10
Somewhere in the code is causing an infinite loop, root cause still
undermined. added more logging statements to help me find misbehavior.
imgDiff() will now handle empty frames on the parameters more
gracefully.

enforceMaxClips() will no longer assume all video clips are accompanied
by html and jpg files but will now instead "delete if exists."
2022-12-16 18:30:27 -05:00
Maurice ONeal
a5ee164b4e v1.5.t9
The app is hard crashing now but I was able to determine the cause this
time. Must functions in filesystem tend to abort if the filesystem
object doesn't exists. Added protection where needed to prevent crashing
2022-12-13 21:13:10 -05:00
Maurice ONeal
954fdfba0b v1.5.t8
Logs are still being cutoff, I'm assuming the app is crashing but can't
locate the problem without any logs. Reformed logging to never overwrite
the logs and will instead append only. Size control will be in the form
of the byte size of the log files.
2022-12-13 20:27:32 -05:00
Maurice ONeal
651ec96083 v1.5.t7
Moved logging out of it's own loop, hopefully this fixes the issue with
it not outputting all log lines. recLoop() and detectLoop() will now
update logs synchronously.

The setup.sh script will now include gstreamer and pkg-config. This
should help fix opencv video-io format support.
2022-12-11 21:00:53 -05:00
Maurice ONeal
89129ad3f4 v1.5.t6
Can't get opencv to work with FFMPEG to open the buff file on the test
machine. I've given up on trying to figure out why. Testing out video
capture without explicitly specifying FFMPEG to see how that works out.
2022-12-11 16:10:04 -05:00
Maurice ONeal
2bd600bd51 v1.5.t5
Fixed a bug that caused the buffDir to get deleted before the app did
any work. Apparently the filesystem lib crashes the app if the directory
doesn't exists. Might need to add protection against that in the future.

Directory pages with now explicitly link to index.html since implicit
does not work for apache2.
2022-12-11 15:06:09 -05:00
Maurice ONeal
5ab50433cf v1.5.t4
The error checking with ffmpeg is not working. Learned that it doesn't
always return 0 on success. Decided to remove the error checking
altogether. Instead ffmpeg failures should be checked manually using
stderr.

Dirent includes .. and . so I decided to switch to the filesystem entry
listing that should hopefully exclude those special directories.

The camera webroot was not generating .index files. those files would
only get generated if motion was detected. Copied the code that does
that onto recLoop() to execute regardless of motion.
2022-12-11 12:33:56 -05:00
Maurice ONeal
baeaabbd55 v1.5.t3
Fixed the default webroot directory to apache's correct webroot. Also
renamed separated outDir from webRoot and made webRoot changeable on the
config file.

Added logging the recorder and detection loops to help with debugging
and troubleshooting. Just like the video clips, max log lines were added
to control the size of the data being saved to storage.
2022-12-11 10:25:22 -05:00
Maurice ONeal
ce4a326b24 v1.5.t2
The camera folder will now be auto created and eventually will cause the
app to auto create the webroot regardless if motion was detected or not.
2022-12-04 20:07:20 -05:00
Maurice ONeal
9816ba339f v1.5.t1
Decided to switch using opencv's builtin pixel diff motion detection via
absdiff and thresh. Doing this should increase efficiency instead of
using the home brewed pixel loops and threads.

Added a web interface of sorts by having html files output along with
the video clips. These files are designed to link together with the
assumption that the output directory is a web root like /var/www/html
that apache2 uses. The interface is crude at best but at least allow
playback of recorded footage.

Added max_clips config variable that can limit the amount of motion
events that can recorded to storage on a single day.
2022-12-04 15:13:39 -05:00
15 changed files with 827 additions and 391 deletions

6
.gitignore vendored
View File

@ -46,7 +46,7 @@ target_wrapper.*
# QtCreator CMake
CMakeLists.txt.user*
# QtCreator 4.8< compilation database
# QtCreator 4.8< compilation database
compile_commands.json
# QtCreator local machine specific files for imported projects
@ -57,7 +57,3 @@ compile_commands.json
# Build folders
/.build-mow
/.build-opencv
# Opencv src folder
/src/opencv

View File

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 2.8.12)
project( MotionWatch )
find_package( OpenCV REQUIRED )
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -pthread")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pthread")
include_directories( ${OpenCV_INCLUDE_DIRS} )
add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp )
add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp src/web.cpp src/logger.cpp )
target_link_libraries( mow ${OpenCV_LIBS} )

131
README.md
View File

@ -13,15 +13,20 @@ of this app can be used to operate multiple cameras.
Usage: mow <argument>
-h : display usage information about this application.
-c : path to the config file.
-c : path to the config file(s).
-v : display the current version.
note: multiple -c config files can be passed, reading from left
to right. any conflicting values between the files will
have the latest value from the latest file overwrite the
the earliest.
```
### Config File ###
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
parameters supported and descriptions of each parameter.
parameters supported and descriptions of each.
```
# Motion Watch config file
#
@ -33,77 +38,109 @@ 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
# to record footage.
#
output_dir = /path/to/footage/directory
web_root = /var/www/html
# this is the output directory that will be used to store recorded footage
# from the camera. the file naming convention uses the current date for
# playlist files which points to hidden video clips taken from the camera.
# from the cameras as well as the web interface for the application.
# warning: this will overwrite any existing index.html files so be sure
# to choose a directory that doesn't have an existing website.
#
buff_dir = /tmp/ramdisk/cam_name
buff_dir = /tmp
# this application records small clips of the footage from the camera and
# then stores them into this directory. any clips with motion detected in
# them are moved to output_dir, if no motion they are deleted. highly
# recommend to use a ramdisk tempfs for this since this directory is used
# for lots of writes.
# them are moved to web_root; if no motion is detected, they are deleted.
# it is highly recommend to use a ramdisk tempfs for this since this
# directory is used for large amounts of writes.
#
consec_threshold = 512
# motion is detected by comparing each frame in the camera feed for
# differences in the pixels. this value determine how many consecutive
# pixels need to different or how large the suspect object in motion
# needs to be.
cam_name = cam-1
# this is the optional camera name parameter to identify the camera. this
# name will be used to form the directory structure in the web_root as
# well as buff_dir. if not defined, the name of the config file will be
# used.
#
block_threshold = 1024
# this value tells the application how many "lines" of pixels need to
# exceed consec_threshold before being considered motion.
pix_thresh = 150
# this value tells the application how far different the pixels need to be
# before the pixels are actually considered different. think of this as
# pixel diff sensitivity, the higher the value the lesser the sensitivity.
#
block_x = 64
# this is the x coordinate size or horizontal size of a block of pixels
# that the application will use to detect the presents of motion.
frame_gap = 20
# this value is used to tell the application how far in between frames to
# check the pixel diffs for motion. the lower the value, the more frames
# will be checked, however with that comes higher cpu usage.
#
block_y = 60
# this is the y coordinate size or vertical size of a block of pixels
# that the application will use to detect the presents of motion.
img_thresh = 80000
# this indicates how many pixels need to be different in between frame_gap
# before it is considered motion. any video clips found with frames
# exceeding this value will be moved from buff_dir to web_root.
#
duration = 60
# this sets the internal timer used to reset to the detection loop and
# 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
# to the settings will be applied without restarting the application.
clip_len = 20
# this parameter indicate the amount of seconds to record in each video
# clip from the camera that will be stored and then processed in buff_dir.
#
num_of_clips = 3
# 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
# this an optional command to run after the internal timer duration has
# elapsed. one great use for this is to move a ptz camera to the next
# position of it's patrol pattern. note: the call to this command will be
# delayed if motion was detected.
# this an optional command to run after num_of_clips is met. one great use
# for this is to move a ptz camera to the next position of it's patrol
# pattern. note: the call to this command will be delayed if motion was
# detected.
#
max_days = 15
# this defines the maximum amount of days worth of video clips that is
# allowed to be stored in the output_dir. whenever this limit is met,
# the oldest day is deleted.
# allowed to be stored in the web_root. whenever this limit is met, the
# oldest day and all of it's associated video clips will be 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
# this is the video file format to use from recording footage from the
# this is the video file format to use for recording footage from the
# camera. the format support depends entirely on the under laying ffmpeg
# 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 ###
This application is currently only compatible with a Linux based operating
systems that are capable of building and installing the opencv API from source.
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.
```
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 ./build.sh time or if upgrading from the ground up.
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.
```

View File

@ -1,7 +1,5 @@
#!/bin/sh
mkdir -p ./.build-mow
cd ./.build-mow
cmake ..
make -j4

View File

@ -1,3 +1,13 @@
#!/bin/sh
cp ./.build-mow/mow /usr/bin/mow
if [ ! -d "/opt/mow" ]; then
mkdir /opt/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

View File

@ -1,19 +1,28 @@
#!/bin/sh
apt update
apt install -y cmake g++ wget unzip git ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev
cd ./src
if [ -d "./opencv" ]
then
cd ./opencv
git pull origin
cd ..
else
git clone https://github.com/opencv/opencv.git
fi
cd ..
mkdir -p ./.build-opencv
cd ./.build-opencv
cmake ../src/opencv
make -j4
make install
apt update -y
apt install -y pkg-config
apt install -y cmake
apt install -y make
apt install -y g++
apt install -y wget
apt install -y unzip
apt install -y git
apt install -y ffmpeg
apt install -y gstreamer1.0*
apt install -y libavcodec-dev
apt install -y libavformat-dev
apt install -y libavutil-dev
apt install -y libswscale-dev
apt install -y libgstreamer1.0-dev
apt install -y x264
apt install -y libx264-dev
apt install -y 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

View File

@ -52,48 +52,24 @@ bool createDirTree(const string &full_path)
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)
{
DIR *dir;
struct dirent *ent;
vector<string> names;
if ((dir = opendir(path.c_str())) != NULL)
if (exists(path))
{
while ((ent = readdir(dir)) != NULL)
for (auto &entry : directory_iterator(path))
{
auto name = string(ent->d_name);
if ((name.size() >= 4) && (ent->d_type & DT_REG))
if (entry.is_regular_file())
{
if (name.substr(name.size() - 4) == ext)
auto name = entry.path().filename().string();
if (ext.empty() || name.ends_with(ext))
{
names.push_back(name);
}
}
}
closedir(dir);
}
sort(names.begin(), names.end());
@ -101,18 +77,53 @@ vector<string> lsFilesInDir(const string &path, const string &ext)
return names;
}
void enforceMaxDays(shared_t *share)
vector<string> lsDirsInDir(const string &path)
{
auto names = lsFilesInDir(share->outDir, ".m3u");
vector<string> names;
while (names.size() > share->maxDays)
if (exists(path))
{
auto name = names[0];
auto plsFile = cleanDir(share->outDir) + "/" + name;
auto vidFold = cleanDir(share->outDir) + "/." + name.substr(0, name.size() - 4);
for (auto &entry : directory_iterator(path))
{
if (entry.is_directory())
{
names.push_back(entry.path().filename().string());
}
}
}
remove(plsFile.c_str());
remove_all(vidFold.c_str());
sort(names.begin(), names.end());
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());
}
@ -140,22 +151,11 @@ string genDstFile(const string &dirOut, const char *fmt, const string &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 &param, const string &line, string *value)
{
if (line.rfind(param.c_str(), 0) == 0)
{
*value = line.substr(param.size());
//cout << param << *value << endl;
}
}
@ -164,154 +164,186 @@ void rdLine(const string &param, const string &line, int *value)
if (line.rfind(param.c_str(), 0) == 0)
{
*value = strtol(line.substr(param.size()).c_str(), NULL, 10);
//cout << param << *value << endl;
}
}
bool rdConf(shared_t *share)
bool rdConf(const string &filePath, shared_t *share)
{
ifstream varFile(share->conf.c_str());
ifstream varFile(filePath.c_str());
if (!varFile.is_open())
{
share->retCode = ENOENT;
cerr << "err: Failed to open the config file: " << share->conf << " for reading. please check file permissions or if it exists." << endl;
cout << "wrn: config file: " << filePath << " does not exists or lack read permissions." << endl;
}
else
{
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
{
getline(varFile, line);
if (line.rfind("#", 0) != 0)
{
rdLine("cam_name = ", line, &share->camName);
rdLine("recording_stream = ", line, &share->recordUrl);
rdLine("output_dir = ", line, &share->outDir);
rdLine("web_root = ", line, &share->webRoot);
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("consec_threshold = ", line, &share->consecThresh);
rdLine("duration = ", line, &share->secs);
rdLine("clip_len = ", line, &share->clipLen);
rdLine("num_of_clips = ", line, &share->numOfClips);
rdLine("buff_dir = ", line, &share->buffDir);
rdLine("block_x = ", line, &share->blockX);
rdLine("block_y = ", line, &share->blockY);
rdLine("block_threshold = ", line, &share->blockThresh);
rdLine("frame_gap = ", line, &share->frameGap);
rdLine("pix_thresh = ", line, &share->pixThresh);
rdLine("img_thresh = ", line, &share->imgThresh);
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_codec = ", line, &share->vidCodec);
}
} while(!line.empty());
// 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.
if (share->blockX == 0) share->blockX = 32;
if (share->blockY == 0) share->blockY = 32;
if (share->init)
{
remove_all(share->buffDir.c_str());
share->init = false;
}
new thread(enforceMaxDays, share);
createDirTree(cleanDir(share->buffDir));
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)
bool rdConf(shared_t *share)
{
capture >> prev;
capture >> next;
share->recordUrl.clear();
share->postCmd.clear();
share->buffDir.clear();
share->camName.clear();
share->recLogPath.clear();
share->detLogPath.clear();
share->recLogFile.close();
share->detLogFile.close();
return !prev.empty() && !next.empty();
}
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";
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");
auto ret = false;
copy_file(buffFile.c_str(), vidOut.c_str());
remove(buffFile.c_str());
ofstream file;
if (fileExists(lisOut))
for (auto &&confPath: share->conf)
{
file.open(lisOut.c_str(), ios_base::app);
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 (exists(share->buffDir))
{
remove_all(share->buffDir);
}
share->init = false;
}
createDirTree(cleanDir(share->buffDir));
createDirTree(share->outDir);
}
else
{
file.open(lisOut.c_str());
cerr << "err: none of the expected config files could be read." << endl;
}
file << m3uOut << endl;
file.close();
return ret;
}
string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs)
{
for (int i = 0; i < argc; ++i)
auto ret = string();
for (; offs < argc; ++offs)
{
auto argInParams = string(argv[i]);
auto argInParams = string(argv[offs]);
if (arg.compare(argInParams) == 0)
{
if (!argOnly)
{
// check ahead, make sure i + 1 won't cause out-of-range exception
if ((i + 1) <= (argc - 1))
offs++;
// check ahead, make sure offs + 1 won't cause out-of-range exception
if (offs <= (argc - 1))
{
return string(argv[i + 1]);
ret = string(argv[offs]);
}
}
else
{
return string("true");
ret = string("true");
}
}
}
return string();
return ret;
}
void statOut(shared_t *share)
string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
{
system(string("touch " + cleanDir(share->buffDir) + "/stat").c_str());
auto notUsed = 0;
auto path = string(cleanDir(share->buffDir) + "/stat");
auto fd = open(path.c_str(), fstream::out | fstream::trunc);
write(fd, share->stat.c_str(), share->stat.size() + 1);
close(fd);
return parseForParam(arg, argc, argv, argOnly, notUsed);
}
vector<string> parseForList(const string &arg, int argc, char** argv)
{
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();
}

View File

@ -22,7 +22,6 @@
#include <errno.h>
#include <vector>
#include <thread>
#include <dirent.h>
#include <filesystem>
#include <mutex>
#include <sys/types.h>
@ -36,46 +35,57 @@ using namespace cv;
using namespace std;
using namespace std::filesystem;
#define BUF_SZ 10
#define APP_VER "1.5"
#define APP_VER "1.6"
#define APP_NAME "Motion Watch"
struct shared_t
{
string stat;
string recordUrl;
string outDir;
string postCmd;
string conf;
string buffDir;
string vidExt;
bool init;
bool recLoopWait;
bool skipCmd;
int consecThresh;
int secs;
int blockThresh;
int blockX;
int blockY;
int maxDays;
int retCode;
vector<thread> detThreads;
vector<string> conf;
ofstream recLogFile;
ofstream detLogFile;
string recLogPath;
string detLogPath;
string recordUrl;
string outDir;
string postCmd;
string buffDir;
string vidExt;
string vidCodec;
string camName;
string webBg;
string webTxt;
string webFont;
string webRoot;
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 genTimeStr(const char *fmt);
string cleanDir(const string &path);
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs);
string parseForParam(const string &arg, int argc, char** argv, bool argOnly);
bool createDir(const string &dir);
bool createDirTree(const string &full_path);
bool fileExists(const string& name);
void wrOut(const string &buffFile, shared_t *share);
void replaceAll(string &str, const string &from, const string &to);
void enforceMaxDays(shared_t *share);
void enforceMaxDays(const string &dirPath, shared_t *share);
void enforceMaxClips(const string &dirPath, shared_t *share);
void rdLine(const string &param, const string &line, string *value);
void rdLine(const string &param, const string &line, int *value);
void statOut(shared_t *share);
void waitForDetThreads(shared_t *share);
bool rdConf(shared_t *share);
bool capPair(Mat &prev, Mat &next, VideoCapture &capture, shared_t *share);
Mat toGray(const Mat &src);
vector<string> lsFilesInDir(const string &path, const string &ext);
vector<string> parseForList(const string &arg, int argc, char** argv);
vector<string> lsFilesInDir(const string &path, const string &ext = string());
vector<string> lsDirsInDir(const string &path);
#endif // COMMON_H

108
src/logger.cpp Normal file
View File

@ -0,0 +1,108 @@
// 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 Normal file
View File

@ -0,0 +1,24 @@
#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

View File

@ -11,63 +11,119 @@
// GNU General Public License for more details.
#include "mo_detect.h"
#include "logger.h"
void detectLoop(shared_t *share)
void detectMoInFile(const string &bufPath, shared_t *share)
{
vector<string> bufFiles;
detLog("detect_mo_in_file() -- start", share);
while (!bufFiles.empty() || !share->recLoopWait)
Mat thumbNail;
if (moDetect(bufPath, thumbNail, share))
{
bufFiles = lsFilesInDir(share->buffDir, "." + share->vidExt);
share->skipCmd = true;
if ((bufFiles.size() >= 2) || (share->recLoopWait && !bufFiles.empty()))
{
auto fullPath = cleanDir(share->buffDir) + "/" + bufFiles[0];
share->stat.clear();
if (moDetect(fullPath, share))
{
share->skipCmd = true;
wrOut(fullPath, share);
}
else
{
remove(fullPath.c_str());
}
statOut(share);
}
else
{
sleep(1);
}
wrOut(bufPath, thumbNail, share);
}
else if (exists(bufPath))
{
remove(bufPath);
}
detLog("detect_mo_in_file() -- finished", share);
}
void recLoop(shared_t *share)
{
while (rdConf(share))
{
createDirTree(share->buffDir);
recLog("rec_loop() -- start", share);
auto bufPath = cleanDir(share->buffDir) + "/%03d." + share->vidExt;
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;
enforceMaxLogSize(share->recLogPath, share);
enforceMaxLogSize(share->detLogPath, share);
thread th2(detectLoop, share);
initLogFile(share->recLogPath, share->recLogFile);
initLogFile(share->detLogPath, share->detLogFile);
system(cmd.c_str());
initLogFrontPages(share);
share->recLoopWait = true;
if (!exists("/tmp/mow-lock"))
{
system("touch /tmp/mow-lock");
th2.join();
genCSS(share);
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)
{
system(share->postCmd.c_str());
recLog("no motion detected", share);
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;
}
}
}
@ -76,34 +132,35 @@ int main(int argc, char** argv)
{
struct shared_t sharedRes;
sharedRes.conf = parseForParam("-c", argc, argv, false);
sharedRes.conf = parseForList("-c", argc, argv);
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;
cout << "-c : path to a config file." << endl;
cout << "-v : display the current version." << endl << 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;
}
if (parseForParam("-v", argc, argv, true) == "true")
else if (parseForParam("-v", argc, argv, true) == "true")
{
cout << APP_VER << endl;
}
else if (sharedRes.conf.empty())
{
cerr << "err: A config file was not given in -c" << endl;
cerr << "err: no config file(s) were given in -c" << endl;
}
else
{
sharedRes.retCode = 0;
sharedRes.recLoopWait = false;
sharedRes.skipCmd = false;
sharedRes.init = true;
sharedRes.retCode = 0;
sharedRes.skipCmd = false;
sharedRes.init = true;
thread th1(recLoop, &sharedRes);
th1.join();
recLoop(&sharedRes);
return sharedRes.retCode;
}

View File

@ -12,124 +12,131 @@
#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)
{
vector<thread> threads;
vector<sec_t> results;
mutex secMutex;
auto ret = false;
auto id = 0;
detLog("img_diff() -- start()", share);
for (auto x = 0; x < prev.cols; x += share->blockX)
if (prev.empty()) detLog("prev_frame is empty -- Borken frame from the camera assumed.", share);
if (next.empty()) detLog("next_frame is empty -- EOF assumed.", share);
if (!prev.empty() && !next.empty())
{
// spawn all of the block motion detection threads.
for (auto y = 0; y < prev.rows; y += share->blockY, id += 1)
{
threads.push_back(thread(secDiff, prev, next, id, share->blockY, share->blockX, y, x, &results, &secMutex, share));
}
Mat diff;
absdiff(prev, next, diff);
threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY);
auto diffScore = countNonZero(diff);
detLog("diff_score: " + to_string(diffScore), share);
ret = diffScore >= share->imgThresh;
}
for (auto &&thr : threads)
{
// wait for all of the threads to finish.
thr.join();
}
detLog("img_diff() -- finished()", share);
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;
return ret;
}
bool moDetect(const string &buffFile, shared_t *share)
Mat frameFF(VideoCapture *cap, int gap)
{
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;
VideoCapture capture(buffFile.c_str(), CAP_FFMPEG);
if (capture.isOpened())
{
Mat prev;
Mat next;
Mat prev;
Mat next;
auto frameCount = capture.get(CAP_PROP_FRAME_COUNT);
auto frameGaps = frameCount / share->frameGap;
while (capPair(prev, next, capture, share))
detLog("frame_count: " + to_string(frameCount), share);
detLog("frame_gaps: " + to_string(frameGaps), share);
for (auto i = 0; i < frameGaps; i++)
{
if (imgDiff(toGray(prev), toGray(next), share))
if (prev.empty()) prev = frameFF(&capture, 1);
else prev = next.clone();
next = frameFF(&capture, share->frameGap);
if (imgDiff(prev, next, share))
{
mod = true; break;
resize(next, vidThumb, Size(720, 480), INTER_LINEAR);
mod = true; break;
}
}
}
else
{
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;
detLog("failed to open the buff file for reading. check permissions and/or opencv's video-io support (gstreamer/ffmpeg).", share);
}
capture.release();
detLog("mo_detect() -- finished()", share);
return mod;
}

View File

@ -14,20 +14,12 @@
// GNU General Public License for more details.
#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 moDetect(const string &buffFile, shared_t *share);
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share);
void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share);
Mat frameFF(VideoCapture *cap, int gap);
#endif // MO_DETECT_H

134
src/web.cpp Normal file
View File

@ -0,0 +1,134 @@
// 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 &&regName : 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 Normal file
View File

@ -0,0 +1,22 @@
#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