Compare commits

..

23 Commits

Author SHA1 Message Date
Maurice ONeal
19872b3ff5 v2.0
updated documentation and cleaned up the code. preparing to release to
master.
2023-03-28 20:27:12 -04:00
Zii
83b206c06c v2.0.t13
Fixed the crashing issue by adding tcp timeout args to ffmpeg and
having the app handle empty frames from a disconnected camera
better.

Reformed the directory structure by having live, logs and events in
seperate directories.

schLoop() no longer exists, postCmd is now handled by
detectMoInStream() to ensure motion detection is not done while the
command is running.
2023-03-26 10:45:23 -04:00
Maurice ONeal
93723bb7b1 v2.0.t11
also removed moDetect() loop for debugging.
2023-03-18 21:08:40 -04:00
Maurice ONeal
58d957d0a4 v2.0.t11
removed detectloop() for debug. the app is crashing without explanation.
2023-03-18 20:52:13 -04:00
Maurice ONeal
533c27d9cb v.20.t10
Event files are still not concatenating. I suspect the issue was in
eventLoop() caching old event objects. Changed up the loop to so it will
grab latest event object on each iteration.
2023-03-12 17:19:11 -04:00
Maurice ONeal
061c2571b4 v2.0.t9
Event VODs are still not playing back correctly. Trying mp4 format
instead of hls.
2023-03-12 15:27:53 -04:00
Maurice ONeal
baa69da2cd v2.0.t8
Adjusted the event loop and motion detection to make better stand alone
m3u8 files. Hopefully doing this will make browsers treat the recorded
events as VODs instead of streams.
2023-03-12 13:22:10 -04:00
Maurice ONeal
3c5dbec24c v2.0.t7
Apparently native html5 or modern browsers do not support running .m3u8
playlist directly or I was missing something in the original code. Even
adding the correct mime types in apache2 didn't work so I decided to
embed hls.js into the video html files to support hls playlist.
2023-03-11 16:32:00 -05:00
Maurice ONeal
78919effcf v2.0.t6
Added live camera streaming support to the web interface for testing.
2023-03-11 07:56:16 -05:00
Maurice ONeal
0f6e7603df v.2.0.t5
Found the write out bug. genEventPath() was getting unexpected input
causing it to output empty strings.
2023-03-10 20:51:18 -05:00
Maurice ONeal
bddde644c1 v.2.0.t4
Fixed the compile error.
2023-03-10 19:47:36 -05:00
Maurice ONeal
ae46834777 v2.0.t3
Event recordings are not writing out correctly. added more log lines to
help debug.
2023-03-10 19:35:44 -05:00
Maurice ONeal
b0dbfa0852 v2.0.t2
Logs are not rotating correctly. Changed up the code to append the logs
in memory and then dump them to permanent storage on every loop of
upkeep(). Hopefully this fixes the issue.
2023-03-10 19:05:54 -05:00
Maurice ONeal
a065b7a1d3 v2.0.t1
Completely reformed the internal workings of the application code. I
brought back multi-threaded functions so there is now 5 separate threads
for different tasks.

recLoop() - this function calls ffmpeg to begin recording footage from
the defined camera and stores the footage in hls format. It is designed
to keep running for as long as the application is running and if it does
stop for whatever reason, it will attempt to auto re-start.

upkeep() - this function does regular cleanup and enforcement of maxDays
maxLogSize and maxEvents without the need to stop recording or detecting
motion.

detectMo() - this function reads directly from recLoop's hls output and
list all footage that has motion in it. motion detection no longer has
to wait for the clip to finish recording thanks to the use of .ts
containers for the video clips. this makes the motion detection for less
cpu intensive now that it will now operate at the camera's fps (slower).

eventLoop() - this function reads the motion list from detectMo and
copies the footage pointed out by the list to an events folder, also in
hls format.

schLoop() - this function runs an optional user defined external command
every amount of seconds defined in sch_sec. this command temporary stops
motion detection without actually terminating the thread. It will also
not run the command at the scheduled time if motion was detected.

Benefits to this reform:

- far less cpu intensive operation
- multi-threaded architecture for better asynchronous operation
- it has support for live streaming now that hls is being used
- a buff_dir is no longer necessary
2023-03-05 16:07:07 -05:00
Maurice ONeal
81da33ba81 v1.6.t9
The fork() architecture from the previous commit is also deemed a
failure. Reverted back to v1.5.t19 code. I'll start from scratch, using
this commit as the new base.
2023-02-18 21:21:34 -05:00
Maurice ONeal
13eaf75c8a v1.6.t8
going back to basics. removed all threading code and opted for a multi
process architecture using fork(). previous code had a bad memory leak
and doesn't handle unexpected camera disconnects and for some reason it
also didn't recover gracefully in systemctl when it crashes. Hopefully
this new re-write fixes all of those numerous issues.

moDetect() will now try multiple times to grab buffer footage before
giving up and moving on.
2023-02-18 17:43:10 -05:00
Maurice ONeal
4dcd6c05a3 v1.6.t7
the crashing issue might be the detection threads going out-of-scope
before properly finishing. re-implemented share->detThreads from
previous stable code to see if this fixes the issue.
2023-02-14 19:29:02 -05:00
Maurice ONeal
4758b62275 v1.6.t6
The crashing problems may have started after switching my test machine
to to multiple config file setup. I'll test this theory by completely
removing the multiple config file feasure and see if it crashes again.

I'll figure out a better solution for multi config files in the next
round of deveoplment.
2023-02-12 15:04:32 -05:00
Maurice ONeal
6ffe80b672 v1.6.t5
Added a signal handler that will print out signal details upon receiving
them. This should give up some hint to the cause of crashes for
debugging reasons.

The root index web page will now only be updated once. Hopefully this
reduces chance of multiple instances clashing with each other.
2023-02-11 20:59:19 -05:00
Maurice ONeal
f4f1f62d25 v1.6.t4
Updated the documentation.

The test machine had a mystery crash that needs to be investigated. In
mean time, the timeout run code has been refactored and will not run
thread cancel unless is it absolutely needed at the individual thread
level (hopefully that fixes the crash issue).

post_cmd shall also now run via timeout. With that, no external commands
should cause this application to stall. Timeout protection should
prevent that.
2023-02-07 23:19:41 -05:00
Maurice ONeal
23e0ae935e v1.6.t3
Added string trimming to the vid_container parameter to filter out bad
user input.

Added detection_stream url to the config file and made it so the
application can now use a smaller/lower bit rate stream for motion
detection separate from the recording stream. This can significantly
lower CPU usage.

Moved away from using system() and the explicit timeout command. Instead
opted to using popen() and cancelable pthreads. Doing this pulls back
more control over ffmpeg than before and the app will now properly
respond term signals and even the CTRL-C keyboard interrupt.
2023-02-05 14:05:56 -05:00
Maurice ONeal
80f8ec07e3 v1.6.t2
The app was still cutting out last command line arg of my test setup.
Later found out it was the run script limiting the command line arg
count to 3. I extended it out to 8 but I'll need to find a better option
to make it limitless.
2023-01-18 21:55:17 -05:00
Maurice ONeal
62a6139f3a v1.6.t1
The is not currently parsing multiple config files properly. Changed up
the parser function without complicated check ahead logic. Will test if
this works.
2023-01-18 20:51:16 -05:00
11 changed files with 542 additions and 458 deletions

View File

@ -44,76 +44,39 @@ web_root = /var/www/html
# warning: this will overwrite any existing index.html files so be sure # warning: this will overwrite any existing index.html files so be sure
# to choose a directory that doesn't have an existing website. # to choose a directory that doesn't have an existing website.
# #
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 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.
#
cam_name = cam-1 cam_name = cam-1
# this is the optional camera name parameter to identify the camera. this # 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 # name will also be used to as the base directory in web_root. if not
# well as buff_dir. if not defined, the name of the config file will be # defined, the name of the config file will be used.
# used.
# #
pix_thresh = 150 pix_thresh = 150
# this value tells the application how far different the pixels need to be # this value tells the application how far different the pixels need to be
# before the pixels are actually considered different. think of this as # before the pixels are actually considered different. think of this as
# pixel diff sensitivity, the higher the value the lesser the sensitivity. # pixel diff sensitivity, the higher the value the lesser the sensitivity.
# # maximum is 255.
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.
# #
img_thresh = 80000 img_thresh = 80000
# this indicates how many pixels need to be different in between frame_gap # this indicates how many pixels need to be different in between frames
# before it is considered motion. any video clips found with frames # before it is considered motion. any video clips found with frames
# exceeding this value will be moved from buff_dir to web_root. # exceeding this value will be copied from live footage to event footage.
# #
clip_len = 20 max_events = 40
# this parameter indicate the amount of seconds to record in each video # this indicates the maximum amount of motion event video clips to keep
# clip from the camera that will be stored and then processed in buff_dir. # before deleting the oldest clip.
# #
num_of_clips = 3 sch_sec = 60
# this will tell the application how many video clips should be recorded # this is the amount of seconds to wait in between running post_cmd.
# 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 with sch_sec. one great use for this
# for this is to move a ptz camera to the next position of it's patrol # is to move a ptz camera to the next position of it's patrol pattern.
# pattern. note: the call to this command will be delayed if motion was # note: the call to this command will be delayed if motion was detected.
# detected.
#
max_days = 15
# 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
# 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 max_log_size = 50000
# this is the maximum byte size of all log files that can be stored in # 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 # web_root. whenever this limit is met, the log file will be deleted and
# then eventually recreated blank. # then eventually recreated blank.
# #
vid_container = mp4
# 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 web_text = #dee5ee
# this can be used to customize the color of the text in the web # 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. # interface. it can be defined as any color understood by html5 standard.

View File

@ -16,6 +16,7 @@ apt install -y libswscale-dev
apt install -y libgstreamer1.0-dev apt install -y libgstreamer1.0-dev
apt install -y x264 apt install -y x264
apt install -y libx264-dev apt install -y libx264-dev
apt install -y libilmbase-dev
apt install -y libopencv-dev apt install -y libopencv-dev
apt install -y apache2 apt install -y apache2
add-apt-repository -y ppa:ubuntu-toolchain-r/test add-apt-repository -y ppa:ubuntu-toolchain-r/test

View File

@ -52,6 +52,28 @@ bool createDirTree(const string &full_path)
return ret; return ret;
} }
void cleanupEmptyDirs(const string &path)
{
if (exists(path))
{
for (auto &entry : directory_iterator(path))
{
if (entry.is_directory())
{
try
{
remove(entry.path());
}
catch (filesystem_error const &ex)
{
// non-empty dir assumed when filesystem_error is raised.
cleanupEmptyDirs(path + "/" + entry.path().filename().string());
}
}
}
}
}
vector<string> lsFilesInDir(const string &path, const string &ext) vector<string> lsFilesInDir(const string &path, const string &ext)
{ {
vector<string> names; vector<string> names;
@ -97,31 +119,32 @@ vector<string> lsDirsInDir(const string &path)
return names; return names;
} }
void enforceMaxDays(const string &dirPath, shared_t *share) void cleanupStream(const string &plsPath)
{ {
auto names = lsDirsInDir(dirPath); ifstream fileIn(plsPath);
while (names.size() > (share->maxDays - 1)) for (string line; getline(fileIn, line); )
{ {
remove_all(string(cleanDir(dirPath) + "/" + names[0]).c_str()); if (line.starts_with("VIDEO_TS/"))
{
names.erase(names.begin()); remove(line);
}
} }
} }
void enforceMaxClips(const string &dirPath, shared_t *share) void enforceMaxEvents(shared_t *share)
{ {
auto names = lsFilesInDir(dirPath, "." + share->vidExt); auto names = lsFilesInDir("events", ".mp4");
while (names.size() > share->maxClips) while (names.size() > share->maxEvents)
{ {
// removes the video file extension. // removes the video file extension (.mp4).
auto nameOnly = names[0].substr(0, names[0].size() - (share->vidExt.size() + 1)); auto nameOnly = "events/" + names[0].substr(0, names[0].size() - 4);
auto imgFile = cleanDir(dirPath) + "/" + nameOnly + ".jpg"; auto mp4File = nameOnly + string(".mp4");
auto webFile = cleanDir(dirPath) + "/" + nameOnly + ".html"; auto imgFile = nameOnly + string(".jpg");
auto webFile = nameOnly + string(".html");
remove(cleanDir(dirPath) + "/" + names[0]);
if (exists(mp4File)) remove(mp4File);
if (exists(imgFile)) remove(imgFile); if (exists(imgFile)) remove(imgFile);
if (exists(webFile)) remove(webFile); if (exists(webFile)) remove(webFile);
@ -129,6 +152,7 @@ void enforceMaxClips(const string &dirPath, shared_t *share)
} }
} }
string genTimeStr(const char *fmt) string genTimeStr(const char *fmt)
{ {
time_t rawtime; time_t rawtime;
@ -175,7 +199,7 @@ bool rdConf(const string &filePath, shared_t *share)
{ {
share->retCode = ENOENT; share->retCode = ENOENT;
cout << "wrn: config file: " << filePath << " does not exists or lack read permissions." << endl; cerr << "err: config file: " << filePath << " does not exists or lack read permissions." << endl;
} }
else else
{ {
@ -193,18 +217,12 @@ bool rdConf(const string &filePath, shared_t *share)
rdLine("web_text = ", line, &share->webTxt); rdLine("web_text = ", line, &share->webTxt);
rdLine("web_bg = ", line, &share->webBg); rdLine("web_bg = ", line, &share->webBg);
rdLine("web_font = ", line, &share->webFont); rdLine("web_font = ", line, &share->webFont);
rdLine("sch_sec = ", line, &share->schSec);
rdLine("post_cmd = ", line, &share->postCmd); rdLine("post_cmd = ", line, &share->postCmd);
rdLine("clip_len = ", line, &share->clipLen);
rdLine("num_of_clips = ", line, &share->numOfClips);
rdLine("buff_dir = ", line, &share->buffDir);
rdLine("frame_gap = ", line, &share->frameGap);
rdLine("pix_thresh = ", line, &share->pixThresh); rdLine("pix_thresh = ", line, &share->pixThresh);
rdLine("img_thresh = ", line, &share->imgThresh); rdLine("img_thresh = ", line, &share->imgThresh);
rdLine("max_days = ", line, &share->maxDays); rdLine("max_events = ", line, &share->maxEvents);
rdLine("max_clips = ", line, &share->maxClips);
rdLine("max_log_size = ", line, &share->maxLogSize); rdLine("max_log_size = ", line, &share->maxLogSize);
rdLine("vid_container = ", line, &share->vidExt);
rdLine("vid_codec = ", line, &share->vidCodec);
} }
} while(!line.empty()); } while(!line.empty());
@ -217,69 +235,43 @@ bool rdConf(shared_t *share)
{ {
share->recordUrl.clear(); share->recordUrl.clear();
share->postCmd.clear(); share->postCmd.clear();
share->buffDir.clear();
share->camName.clear(); share->camName.clear();
share->recLogPath.clear();
share->detLogPath.clear();
share->recLogFile.close();
share->detLogFile.close();
share->retCode = 0; share->retCode = 0;
share->frameGap = 20; share->pixThresh = 50;
share->pixThresh = 150; share->imgThresh = 800;
share->imgThresh = 80000; share->maxEvents = 40;
share->clipLen = 20;
share->numOfClips = 3;
share->maxDays = 15;
share->maxClips = 90;
share->maxLogSize = 50000; share->maxLogSize = 50000;
share->webRoot = "/var/www/html";
share->buffDir = "/tmp";
share->vidExt = "mp4";
share->vidCodec = "copy";
share->skipCmd = false; share->skipCmd = false;
share->schSec = 60;
share->webRoot = "/var/www/html";
share->webBg = "#485564"; share->webBg = "#485564";
share->webTxt = "#dee5ee"; share->webTxt = "#dee5ee";
share->webFont = "courier"; share->webFont = "courier";
auto ret = false; if (rdConf(share->conf, share))
for (auto &&confPath: share->conf)
{
if (rdConf(confPath, share)) ret = true;
}
if (ret)
{ {
if (share->camName.empty()) if (share->camName.empty())
{ {
share->camName = path(share->conf.back()).filename(); share->camName = path(share->conf).filename();
} }
share->outDir = cleanDir(share->webRoot) + "/" + share->camName; 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) error_code ec;
{
if (exists(share->buffDir))
{
remove_all(share->buffDir);
}
share->init = false;
}
createDirTree(cleanDir(share->buffDir));
createDirTree(share->outDir); createDirTree(share->outDir);
} current_path(share->outDir, ec);
else
share->retCode = ec.value();
if (share->retCode != 0)
{ {
cerr << "err: none of the expected config files could be read." << endl; cerr << "err: " << ec.message() << endl;
}
} }
return ret; return share->retCode == 0;
} }
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs) string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs)
@ -318,32 +310,48 @@ string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
return parseForParam(arg, argc, argv, argOnly, notUsed); return parseForParam(arg, argc, argv, argOnly, notUsed);
} }
vector<string> parseForList(const string &arg, int argc, char** argv) string genEventPath(const string &tsPath)
{ {
auto offs = 0; if (tsPath.size() > 14)
auto ret = vector<string>();
string param;
do
{ {
param = parseForParam(arg, argc, argv, false, offs); // removes 'VIDEO_TS/live/' from the front of the string.
auto ret = tsPath.substr(14);
if (!param.empty()) return "VIDEO_TS/events/" + ret;
}
else
{ {
ret.push_back(param); return string();
} }
}
string genVidNameFromLive(const string &tsPath)
{
if (tsPath.size() > 17)
{
// removes 'VIDEO_TS/live/' from the front of the string.
auto ret = tsPath.substr(14);
auto ind = tsPath.find('/');
// removes '.ts' from the end of the string.
ret = ret.substr(0, ret.size() - 3);
while (ind != string::npos)
{
// remove all '/'
ret.erase(ind, 1);
ind = ret.find('/');
} }
while (!param.empty());
return ret; return ret;
}
void waitForDetThreads(shared_t *share)
{
for (auto &&thr : share->detThreads)
{
thr.join();
} }
else
share->detThreads.clear(); {
return string();
}
}
uint64_t genEpoch()
{
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
} }

View File

@ -16,17 +16,15 @@
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <string> #include <string>
#include <unistd.h>
#include <time.h> #include <time.h>
#include <chrono>
#include <stdlib.h> #include <stdlib.h>
#include <errno.h> #include <errno.h>
#include <vector> #include <vector>
#include <thread> #include <thread>
#include <filesystem> #include <filesystem>
#include <mutex>
#include <sys/types.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <fcntl.h> #include <map>
#include <opencv4/opencv2/opencv.hpp> #include <opencv4/opencv2/opencv.hpp>
#include <opencv4/opencv2/videoio.hpp> #include <opencv4/opencv2/videoio.hpp>
@ -34,42 +32,49 @@
using namespace cv; using namespace cv;
using namespace std; using namespace std;
using namespace std::filesystem; using namespace std::filesystem;
using namespace std::chrono;
#define APP_VER "1.6" #define APP_VER "2.0"
#define APP_NAME "Motion Watch" #define APP_NAME "Motion Watch"
#define REC_LOG_NAME "rec_log_lines.html"
#define DET_LOG_NAME "det_log_lines.html"
#define UPK_LOG_NAME "upk_log_lines.html"
struct pls_t
{
string evName;
vector<string> srcPaths;
uint64_t createTime;
Mat thumbnail;
};
struct shared_t struct shared_t
{ {
vector<thread> detThreads; map<string, pls_t> recList;
vector<string> conf; string conf;
ofstream recLogFile; string recLog;
ofstream detLogFile; string detLog;
string recLogPath; string upkLog;
string detLogPath;
string recordUrl; string recordUrl;
string outDir; string outDir;
string postCmd; string postCmd;
string buffDir;
string vidExt;
string vidCodec;
string camName; string camName;
string webBg; string webBg;
string webTxt; string webTxt;
string webFont; string webFont;
string webRoot; string webRoot;
bool init;
bool skipCmd; bool skipCmd;
int clipLen; int procTime;
int frameGap; int schSec;
int pixThresh; int pixThresh;
int imgThresh; int imgThresh;
int numOfClips; int maxEvents;
int maxDays;
int maxClips;
int maxLogSize; int maxLogSize;
int retCode; int retCode;
}; };
string genVidNameFromLive(const string &tsPath);
string genEventPath(const string &tsPath);
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);
@ -77,15 +82,14 @@ string parseForParam(const string &arg, int argc, char** argv, bool argO
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);
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, string *value);
void rdLine(const string &param, const string &line, int *value); void rdLine(const string &param, const string &line, int *value);
void statOut(shared_t *share); void cleanupEmptyDirs(const string &path);
void waitForDetThreads(shared_t *share); void cleanupStream(const string &plsPath);
void enforceMaxEvents(shared_t *share);
bool rdConf(shared_t *share); bool rdConf(shared_t *share);
vector<string> parseForList(const string &arg, int argc, char** argv);
vector<string> lsFilesInDir(const string &path, const string &ext = string()); vector<string> lsFilesInDir(const string &path, const string &ext = string());
vector<string> lsDirsInDir(const string &path); vector<string> lsDirsInDir(const string &path);
uint64_t genEpoch();
#endif // COMMON_H #endif // COMMON_H

View File

@ -14,12 +14,17 @@
void recLog(const string &line, shared_t *share) void recLog(const string &line, shared_t *share)
{ {
share->recLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "<br>" << endl; share->recLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
} }
void detLog(const string &line, shared_t *share) void detLog(const string &line, shared_t *share)
{ {
share->detLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "<br>" << endl; share->detLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
}
void upkLog(const string &line, shared_t *share)
{
share->upkLog += genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n";
} }
void enforceMaxLogSize(const string &filePath, shared_t *share) void enforceMaxLogSize(const string &filePath, shared_t *share)
@ -33,21 +38,31 @@ void enforceMaxLogSize(const string &filePath, shared_t *share)
} }
} }
void initLogFile(const string &filePath, ofstream &fileObj) void dumpLogs(const string &fileName, const string &lines)
{ {
if (!fileObj.is_open()) if (!lines.empty())
{ {
if (!exists(filePath)) ofstream outFile;
if (exists(fileName))
{ {
system(string("touch " + filePath).c_str()); outFile.open(fileName.c_str(), ofstream::app);
}
else
{
outFile.open(fileName.c_str());
} }
fileObj.open(filePath.c_str(), ofstream::app | ofstream::out); outFile << lines;
outFile.close();
} }
} }
void initLogFrontPage(const string &filePath, const string &logLinesFile) void initLogFrontPage(const string &filePath, const string &logLinesFile)
{ {
if (!exists(filePath))
{
string htmlText = "<!DOCTYPE html>\n"; string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n"; htmlText += "<html>\n";
@ -96,13 +111,12 @@ void initLogFrontPage(const string &filePath, const string &logLinesFile)
outFile << htmlText; outFile << htmlText;
outFile.close(); outFile.close();
}
} }
void initLogFrontPages(shared_t *share) void initLogFrontPages(shared_t *share)
{ {
auto recLogFilePath = share->outDir + "/recording_log.html"; initLogFrontPage("logs/recording_log.html", REC_LOG_NAME);
auto detLogFilePath = share->outDir + "/detection_log.html"; initLogFrontPage("logs/detection_log.html", DET_LOG_NAME);
initLogFrontPage("logs/upkeep_log.html", UPK_LOG_NAME);
initLogFrontPage(recLogFilePath, path(share->recLogPath).filename().string());
initLogFrontPage(detLogFilePath, path(share->detLogPath).filename().string());
} }

View File

@ -17,8 +17,9 @@
void recLog(const string &line, shared_t *share); void recLog(const string &line, shared_t *share);
void detLog(const string &line, shared_t *share); void detLog(const string &line, shared_t *share);
void upkLog(const string &line, shared_t *share);
void dumpLogs(const string &fileName, const string &lines);
void enforceMaxLogSize(const string &filePath, shared_t *share); void enforceMaxLogSize(const string &filePath, shared_t *share);
void initLogFile(const string &filePath, ofstream &fileObj);
void initLogFrontPages(shared_t *share); void initLogFrontPages(shared_t *share);
#endif // lOGGER_H #endif // lOGGER_H

186
src/main.cpp Executable file → Normal file
View File

@ -12,40 +12,84 @@
#include "mo_detect.h" #include "mo_detect.h"
#include "logger.h" #include "logger.h"
#include "web.h"
void detectMoInFile(const string &bufPath, shared_t *share) void detectMo(shared_t *share)
{ {
detLog("detect_mo_in_file() -- start", share); while (share->retCode == 0)
Mat thumbNail;
if (moDetect(bufPath, thumbNail, share))
{ {
share->skipCmd = true; sleep(2);
detectMoInStream("stream.m3u8", share);
wrOut(bufPath, thumbNail, share);
} }
else if (exists(bufPath))
{
remove(bufPath);
}
detLog("detect_mo_in_file() -- finished", share);
} }
void recLoop(shared_t *share) void eventLoop(shared_t *share)
{ {
while (rdConf(share)) while (share->retCode == 0)
{ {
recLog("rec_loop() -- start", share); while (!share->recList.empty())
{
auto it = share->recList.begin();
auto evName = it->first;
auto event = it->second;
auto timeDiff = genEpoch() - event.createTime;
enforceMaxLogSize(share->recLogPath, share); // wait at least 62 seconds before processing the event in
enforceMaxLogSize(share->detLogPath, share); // queue.
if ((timeDiff > 0) && (timeDiff > 62))
{
try
{
createDirTree("events");
wrOutVod(event, share);
genHTMLvod(evName);
initLogFile(share->recLogPath, share->recLogFile); if (!exists("events/" + evName + ".jpg"))
initLogFile(share->detLogPath, share->detLogFile); {
imwrite(string("events/" + evName + ".jpg").c_str(), event.thumbnail);
}
}
catch (filesystem_error &ex)
{
recLog(string("err: ") + ex.what(), share);
}
share->recList.erase(it);
}
sleep(5);
}
sleep(5);
}
}
void upkeep(shared_t *share)
{
while (share->retCode == 0)
{
createDirTree("live");
createDirTree("events");
createDirTree("logs");
enforceMaxLogSize(string("logs/") + REC_LOG_NAME, share);
enforceMaxLogSize(string("logs/") + DET_LOG_NAME, share);
enforceMaxLogSize(string("logs/") + UPK_LOG_NAME, share);
dumpLogs(string("logs/") + REC_LOG_NAME, share->recLog);
dumpLogs(string("logs/") + DET_LOG_NAME, share->detLog);
dumpLogs(string("logs/") + UPK_LOG_NAME, share->upkLog);
share->recLog.clear();
share->detLog.clear();
share->upkLog.clear();
initLogFrontPages(share); initLogFrontPages(share);
enforceMaxEvents(share);
genHTMLul(".", share->camName, share);
upkLog("camera specific webroot page updated: " + share->outDir + "/index.html", share);
if (!exists("/tmp/mow-lock")) if (!exists("/tmp/mow-lock"))
{ {
@ -55,23 +99,35 @@ void recLoop(shared_t *share)
genHTMLul(share->webRoot, string(APP_NAME) + " " + string(APP_VER), share); genHTMLul(share->webRoot, string(APP_NAME) + " " + string(APP_VER), share);
remove("/tmp/mow-lock"); remove("/tmp/mow-lock");
recLog("webroot page updated: " + cleanDir(share->webRoot) + "/index.html", share); upkLog("webroot page updated: " + cleanDir(share->webRoot) + "/index.html", share);
} }
else else
{ {
recLog("skipping update of the webroot page, it is busy.", share); upkLog("skipping update of the webroot page, it is busy.", share);
} }
genHTMLul(share->outDir, share->camName, share); sleep(60);
}
}
recLog("camera specific webroot page updated: " + share->outDir + "/index.html", share); void recLoop(shared_t *share)
{
for (auto i = 0; i < share->numOfClips; ++i) while (share->retCode == 0)
{ {
auto bufPath = cleanDir(share->buffDir) + "/" + to_string(i) + "." + share->vidExt; if (exists("live"))
auto cmd = "timeout -k 1 " + to_string(share->clipLen + 2) + " "; {
remove_all("live");
}
cmd += "ffmpeg -hide_banner -i " + share->recordUrl + " -y -vcodec " + share->vidCodec + " -movflags faststart -t " + to_string(share->clipLen) + " " + bufPath; auto cmd = "ffmpeg -hide_banner -rtsp_transport tcp -timeout 3000000 -i " +
share->recordUrl +
" -strftime 1" +
" -strftime_mkdir 1" +
" -hls_segment_filename 'live/%Y-%j-%H-%M-%S.ts'" +
" -hls_flags delete_segments" +
" -y -vcodec copy" +
" -f hls -hls_time 10 -hls_list_size 400" +
" stream.m3u8";
recLog("ffmpeg_run: " + cmd, share); recLog("ffmpeg_run: " + cmd, share);
@ -79,52 +135,12 @@ void recLoop(shared_t *share)
recLog("ffmpeg_retcode: " + to_string(retCode), share); recLog("ffmpeg_retcode: " + to_string(retCode), share);
if (retCode == 0) if (retCode != 0)
{ {
recLog("detect_mo_in_file() -- started in a seperate thread.", share); recLog("err: ffmpeg returned non zero, indicating failure. please check stderr output.", 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); sleep(10);
}
}
waitForDetThreads(share);
if (!share->skipCmd)
{
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;
}
} }
} }
@ -132,19 +148,15 @@ 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 << 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") else if (parseForParam("-v", argc, argv, true) == "true")
{ {
@ -157,10 +169,20 @@ int main(int argc, char** argv)
else else
{ {
sharedRes.retCode = 0; sharedRes.retCode = 0;
sharedRes.procTime = 0;
sharedRes.skipCmd = false; sharedRes.skipCmd = false;
sharedRes.init = true;
recLoop(&sharedRes); rdConf(&sharedRes);
auto thr1 = thread(recLoop, &sharedRes);
auto thr2 = thread(upkeep, &sharedRes);
auto thr3 = thread(detectMo, &sharedRes);
auto thr4 = thread(eventLoop, &sharedRes);
thr1.join();
thr2.join();
thr3.join();
thr4.join();
return sharedRes.retCode; return sharedRes.retCode;
} }

View File

@ -12,95 +12,92 @@
#include "mo_detect.h" #include "mo_detect.h"
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share) void detectMoInStream(const string &streamFile, shared_t *share)
{ {
auto ret = false; if (share->procTime >= share->schSec)
detLog("img_diff() -- start()", share);
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())
{ {
if (!share->skipCmd)
{
detLog("no motion detected, running post command: " + share->postCmd, share);
system(share->postCmd.c_str());
}
else
{
share->skipCmd = false;
share->procTime = 0;
detLog("motion detected, skipping the post command.", share);
}
}
ifstream fileIn(streamFile);
string tsPath;
Mat thumbnail;
for (string line; getline(fileIn, line); )
{
if (line.starts_with("live/"))
{
tsPath = line;
}
}
if (!tsPath.empty())
{
if (moDetect(tsPath, thumbnail, share))
{
auto eventName = genTimeStr("%Y-%j-%H-%M");
if (share->recList.find(eventName) != share->recList.end())
{
share->recList[eventName].srcPaths.push_back(tsPath);
}
else
{
pls_t event;
event.srcPaths.push_back(tsPath);
event.createTime = genEpoch();
event.thumbnail = thumbnail.clone();
event.evName = eventName;
share->recList.insert(pair{eventName, event});
}
share->skipCmd = true;
}
share->procTime += 10;
}
}
bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share)
{
Mat prevGray;
Mat nextGray;
cvtColor(prev, prevGray, COLOR_BGR2GRAY);
cvtColor(next, nextGray, COLOR_BGR2GRAY);
Mat diff; Mat diff;
absdiff(prev, next, diff); absdiff(prevGray, nextGray, diff);
threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY); threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY);
auto diffScore = countNonZero(diff); score = countNonZero(diff);
detLog("diff_score: " + to_string(diffScore), share); detLog("diff_score: " + to_string(score) + " tresh: " + to_string(share->imgThresh), share);
ret = diffScore >= share->imgThresh; return score >= share->imgThresh;
}
detLog("img_diff() -- finished()", share);
return ret;
}
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) bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
{ {
detLog("mo_detect() -- start()", share); auto maxScore = 0;
detLog("buff_file: " + buffFile, share); auto score = 0;
auto mod = false; detLog("stream_clip: " + buffFile, share);
VideoCapture capture(buffFile.c_str(), CAP_FFMPEG); VideoCapture capture(buffFile.c_str(), CAP_FFMPEG);
@ -108,35 +105,62 @@ bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
{ {
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); detLog("capture open successful.", share);
detLog("frame_gaps: " + to_string(frameGaps), share);
for (auto i = 0; i < frameGaps; i++) while (capture.grab())
{ {
if (prev.empty()) prev = frameFF(&capture, 1); if (prev.empty())
else prev = next.clone();
next = frameFF(&capture, share->frameGap);
if (imgDiff(prev, next, share))
{ {
resize(next, vidThumb, Size(720, 480), INTER_LINEAR); capture.retrieve(prev);
mod = true; break;
} }
else
{
capture.retrieve(next);
if (!next.empty())
{
if (imgDiff(prev, next, score, share))
{
if (score > maxScore)
{
maxScore = score;
resize(next, vidThumb, Size(720, 480), INTER_LINEAR);
}
}
}
prev.release();
next.release();
}
sleep(1);
} }
} }
else else
{ {
detLog("failed to open the buff file for reading. check permissions and/or opencv's video-io support (gstreamer/ffmpeg).", share); detLog("capture open failure, check debug output.", share);
} }
capture.release(); capture.release();
detLog("mo_detect() -- finished()", share); return maxScore > 0;
}
return mod;
void wrOutVod(const pls_t &event, shared_t *share)
{
auto concat = event.evName + ".tmp";
ofstream file(concat.c_str());
for (auto i = 0; i < event.srcPaths.size(); ++i)
{
file << "file '" << event.srcPaths[i] << "''" << endl;
}
file.close();
system(string("ffmpeg -f concat -safe 0 -i " + concat + " -c copy events/" + event.evName + ".mp4").c_str());
remove(concat);
} }

View File

@ -14,12 +14,11 @@
// 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" #include "logger.h"
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share); bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share);
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share); bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share);
void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share); void detectMoInStream(const string &streamFile, shared_t *share);
Mat frameFF(VideoCapture *cap, int gap); void wrOutVod(const pls_t &pls, shared_t *share);
#endif // MO_DETECT_H #endif // MO_DETECT_H

View File

@ -15,8 +15,8 @@
void genHTMLul(const string &outputDir, const string &title, shared_t *share) void genHTMLul(const string &outputDir, const string &title, shared_t *share)
{ {
vector<string> logNames; vector<string> logNames;
vector<string> regNames = lsFilesInDir(outputDir); vector<string> eveNames;
vector<string> dirNames = lsDirsInDir(outputDir); vector<string> dirNames;
string htmlText = "<!DOCTYPE html>\n"; string htmlText = "<!DOCTYPE html>\n";
@ -31,8 +31,43 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
htmlText += "<body>\n"; htmlText += "<body>\n";
htmlText += "<h3>" + title + "</h3>\n"; htmlText += "<h3>" + title + "</h3>\n";
if (!dirNames.empty()) if (exists(outputDir + "/live"))
{ {
eveNames = lsFilesInDir(outputDir + "/events", ".html");
logNames = lsFilesInDir(outputDir + "/logs", "_log.html");
htmlText += "<h4>Logs</h4>\n";
htmlText += "<ul>\n";
for (auto &&logName : logNames)
{
// name.substr(0, name.size() - 9) removes _log.html
auto name = logName.substr(0, logName.size() - 9);
htmlText += " <li><a href='logs/" + logName + "'>" + name + "</a></li>\n";
}
htmlText += "</ul>\n";
htmlText += "<h4>Live</h4>\n";
htmlText += "<ul>\n";
htmlText += " <li><a href='stream.html'>" + share->camName + ":live" + "</a></li>\n";
htmlText += "</ul>\n";
htmlText += "<h4>Motion Events</h4>\n";
genHTMLstream("stream");
for (auto &&eveName : eveNames)
{
// regName.substr(0, regName.size() - 5) removes .html
auto name = eveName.substr(0, eveName.size() - 5);
htmlText += "<a href='events/" + eveName + "'><img src='events/" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n";
}
}
else
{
dirNames = lsDirsInDir(outputDir);
htmlText += "<ul>\n"; htmlText += "<ul>\n";
for (auto &&dirName : dirNames) for (auto &&dirName : dirNames)
@ -43,38 +78,6 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
htmlText += "</ul>\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 += "</body>\n";
htmlText += "</html>"; htmlText += "</html>";
@ -85,11 +88,55 @@ void genHTMLul(const string &outputDir, const string &title, shared_t *share)
file.close(); file.close();
} }
void genHTMLvid(const string &outputVid, shared_t *share) void genHTMLstream(const string &name)
{
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 += " <script src=\"https://cdn.jsdelivr.net/npm/hls.js@1\">\n";
htmlText += " </script>\n";
htmlText += " <video width=100% height=100% id=\"video\" controls>\n";
htmlText += " </video>\n";
htmlText += " <script>\n";
htmlText += " var video = document.getElementById('video');\n";
htmlText += " if (Hls.isSupported()) {\n";
htmlText += " var hls = new Hls({\n";
htmlText += " debug: true,\n";
htmlText += " });\n";
htmlText += " hls.loadSource('" + name + ".m3u8');\n";
htmlText += " hls.attachMedia(video);\n";
htmlText += " hls.on(Hls.Events.MEDIA_ATTACHED, function () {\n";
htmlText += " video.muted = true;\n";
htmlText += " video.play();\n";
htmlText += " });\n";
htmlText += " }\n";
htmlText += " else if (video.canPlayType('application/vnd.apple.mpegurl')) {\n";
htmlText += " video.src = '" + name + ".m3u8';\n";
htmlText += " video.addEventListener('canplay', function () {\n";
htmlText += " video.play();\n";
htmlText += " });\n";
htmlText += " }\n";
htmlText += " </script>\n";
htmlText += "</body>\n";
htmlText += "</html>";
ofstream file(string(name + ".html").c_str());
file << htmlText << endl;
file.close();
}
void genHTMLvod(const string &name)
{ {
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"; string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n"; htmlText += "<html>\n";
@ -102,12 +149,12 @@ void genHTMLvid(const string &outputVid, shared_t *share)
htmlText += "</head>\n"; htmlText += "</head>\n";
htmlText += "<body>\n"; htmlText += "<body>\n";
htmlText += "<video width=100% height=100% controls autoplay>\n"; htmlText += "<video width=100% height=100% controls autoplay>\n";
htmlText += " <source src='" + vidName + "' type='video/" + share->vidExt + "'>\n"; htmlText += " <source src='" + name + ".mp4' type='video/mp4'>\n";
htmlText += "</video>\n"; htmlText += "</video>\n";
htmlText += "</body>\n"; htmlText += "</body>\n";
htmlText += "</html>"; htmlText += "</html>";
ofstream file(string(filePath + "/" + fileName + ".html").c_str()); ofstream file(string("events/" + name + ".html").c_str());
file << htmlText << endl; file << htmlText << endl;

View File

@ -16,7 +16,8 @@
#include "common.h" #include "common.h"
void genHTMLul(const string &outputDir, const string &title, shared_t *share); void genHTMLul(const string &outputDir, const string &title, shared_t *share);
void genHTMLvid(const string &outputVid, shared_t *share); void genHTMLstream(const string &name);
void genHTMLvod(const string &name);
void genCSS(shared_t *share); void genCSS(shared_t *share);
#endif // WEB_H #endif // WEB_H