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.
This commit is contained in:
parent
ce4a326b24
commit
baeaabbd55
|
@ -3,5 +3,5 @@ project( MotionWatch )
|
|||
find_package( OpenCV REQUIRED )
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pthread")
|
||||
include_directories( ${OpenCV_INCLUDE_DIRS} )
|
||||
add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp src/web.cpp )
|
||||
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} )
|
||||
|
|
|
@ -114,7 +114,12 @@ vector<string> lsDirsInDir(const string &path)
|
|||
{
|
||||
if (ent->d_type & DT_DIR)
|
||||
{
|
||||
names.push_back(string(ent->d_name));
|
||||
auto name = string(ent->d_name);
|
||||
|
||||
if ((name != "..") || (name != "."))
|
||||
{
|
||||
names.push_back(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -210,17 +215,22 @@ bool rdConf(shared_t *share)
|
|||
share->postCmd.clear();
|
||||
share->buffDir.clear();
|
||||
|
||||
share->retCode = 0;
|
||||
share->frameGap = 10;
|
||||
share->pixThresh = 30;
|
||||
share->imgThresh = 512;
|
||||
share->secs = 60;
|
||||
share->maxDays = 15;
|
||||
share->maxClips = 30;
|
||||
share->maxLogLines = 1000;
|
||||
share->camName = path(share->conf.c_str()).filename();
|
||||
share->outDir = "/var/www/hmtl";
|
||||
share->webRoot = "/var/www/html";
|
||||
share->vidExt = "mp4";
|
||||
share->recLoopWait = false;
|
||||
share->skipCmd = false;
|
||||
share->webBg = "#485564";
|
||||
share->webTxt = "#dee5ee";
|
||||
share->webFont = "courier";
|
||||
|
||||
do
|
||||
{
|
||||
|
@ -230,7 +240,10 @@ bool rdConf(shared_t *share)
|
|||
{
|
||||
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("duration = ", line, &share->secs);
|
||||
rdLine("buff_dir = ", line, &share->buffDir);
|
||||
|
@ -239,12 +252,16 @@ bool rdConf(shared_t *share)
|
|||
rdLine("img_thresh = ", line, &share->imgThresh);
|
||||
rdLine("max_days = ", line, &share->maxDays);
|
||||
rdLine("max_clips = ", line, &share->maxClips);
|
||||
rdLine("max_log_lines = ", line, &share->maxLogLines);
|
||||
rdLine("vid_container = ", line, &share->vidExt);
|
||||
}
|
||||
|
||||
} while(!line.empty());
|
||||
|
||||
share->outDir = cleanDir(share->outDir) + "/" + share->camName;
|
||||
share->outDir = cleanDir(share->webRoot) + "/" + share->camName;
|
||||
|
||||
createDirTree(cleanDir(share->buffDir));
|
||||
createDirTree(share->outDir);
|
||||
|
||||
if (share->init)
|
||||
{
|
||||
|
@ -252,11 +269,6 @@ bool rdConf(shared_t *share)
|
|||
|
||||
share->init = false;
|
||||
}
|
||||
|
||||
createDirTree(cleanDir(share->buffDir));
|
||||
createDirTree(share->outDir);
|
||||
|
||||
share->retCode = 0;
|
||||
}
|
||||
|
||||
varFile.close();
|
||||
|
|
10
src/common.h
10
src/common.h
|
@ -36,11 +36,13 @@ using namespace cv;
|
|||
using namespace std;
|
||||
using namespace std::filesystem;
|
||||
|
||||
#define APP_VER "1.5.t2"
|
||||
#define APP_VER "1.5.t3"
|
||||
#define APP_NAME "Motion Watch"
|
||||
|
||||
struct shared_t
|
||||
{
|
||||
vector<string> recLogLines;
|
||||
vector<string> detLogLines;
|
||||
string recordUrl;
|
||||
string outDir;
|
||||
string postCmd;
|
||||
|
@ -48,8 +50,13 @@ struct shared_t
|
|||
string buffDir;
|
||||
string vidExt;
|
||||
string camName;
|
||||
string webBg;
|
||||
string webTxt;
|
||||
string webFont;
|
||||
string webRoot;
|
||||
bool init;
|
||||
bool recLoopWait;
|
||||
bool logRun;
|
||||
bool skipCmd;
|
||||
int frameGap;
|
||||
int pixThresh;
|
||||
|
@ -57,6 +64,7 @@ struct shared_t
|
|||
int secs;
|
||||
int maxDays;
|
||||
int maxClips;
|
||||
int maxLogLines;
|
||||
int retCode;
|
||||
};
|
||||
|
||||
|
|
73
src/logger.cpp
Normal file
73
src/logger.cpp
Normal file
|
@ -0,0 +1,73 @@
|
|||
// 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->recLogLines.push_back(genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n");
|
||||
}
|
||||
|
||||
void detLog(const string &line, shared_t *share)
|
||||
{
|
||||
share->detLogLines.push_back(genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n");
|
||||
}
|
||||
|
||||
void enforceMaxLogLines(vector<string> &log, shared_t *share)
|
||||
{
|
||||
while (log.size() > share->maxLogLines)
|
||||
{
|
||||
log.erase(log.begin());
|
||||
}
|
||||
}
|
||||
|
||||
string combine(const vector<string> &log)
|
||||
{
|
||||
string ret;
|
||||
|
||||
for (auto &&line : log) ret += line;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void logLoop(shared_t *share)
|
||||
{
|
||||
string htmlText = "<!DOCTYPE html>\n";
|
||||
|
||||
htmlText += "<html>\n";
|
||||
htmlText += "<head>\n";
|
||||
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
|
||||
htmlText += "<meta http-equiv='refresh' content='10'>";
|
||||
htmlText += "</head>\n";
|
||||
htmlText += "<body>\n";
|
||||
htmlText += "<p>\n";
|
||||
|
||||
while (share->logRun && (share->retCode == 0))
|
||||
{
|
||||
sleep(10);
|
||||
|
||||
enforceMaxLogLines(share->recLogLines, share);
|
||||
enforceMaxLogLines(share->detLogLines, share);
|
||||
|
||||
auto recLogFilePath = cleanDir(share->outDir) + "/recording_log.html";
|
||||
auto detLogFilePath = cleanDir(share->outDir) + "/detection_log.html";
|
||||
|
||||
ofstream recFile(recLogFilePath.c_str());
|
||||
ofstream detFile(detLogFilePath.c_str());
|
||||
|
||||
recFile << htmlText << combine(share->recLogLines) << "</p>\n</body>\n</html>";
|
||||
detFile << htmlText << combine(share->detLogLines) << "</p>\n</body>\n</html>";
|
||||
|
||||
recFile.close();
|
||||
detFile.close();
|
||||
}
|
||||
}
|
24
src/logger.h
Normal file
24
src/logger.h
Normal 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 logLoop(shared_t *share);
|
||||
void enforceMaxLogLines(vector<string> &log, shared_t *share);
|
||||
string combine(const vector<string> &log);
|
||||
|
||||
#endif // lOGGER_H
|
50
src/main.cpp
50
src/main.cpp
|
@ -11,9 +11,12 @@
|
|||
// GNU General Public License for more details.
|
||||
|
||||
#include "mo_detect.h"
|
||||
#include "logger.h"
|
||||
|
||||
void detectLoop(shared_t *share)
|
||||
{
|
||||
detLog("detectLoop() -- start", share);
|
||||
|
||||
vector<string> bufFiles;
|
||||
|
||||
do
|
||||
|
@ -35,10 +38,12 @@ void detectLoop(shared_t *share)
|
|||
{
|
||||
share->skipCmd = true;
|
||||
|
||||
detLog("motion detected in file: " + fullPath, share);
|
||||
wrOut(fullPath, thumbNail, share);
|
||||
}
|
||||
else
|
||||
{
|
||||
detLog("no motion detected in file: " + fullPath + " removing it.", share);
|
||||
remove(fullPath.c_str());
|
||||
}
|
||||
}
|
||||
|
@ -48,23 +53,29 @@ void detectLoop(shared_t *share)
|
|||
}
|
||||
}
|
||||
while (!bufFiles.empty());
|
||||
|
||||
detLog("detectLoop() -- finished", share);
|
||||
}
|
||||
|
||||
void recLoop(shared_t *share)
|
||||
{
|
||||
while (rdConf(share))
|
||||
{
|
||||
recLog("recLoop() -- start", share);
|
||||
|
||||
if (!fileExists("/tmp/mow-lock"))
|
||||
{
|
||||
recLog("/tmp/mow-lock not found, assuming it is safe to update the webroot page.", share);
|
||||
recLog("webroot page = " + cleanDir(share->webRoot) + "/index.html", share);
|
||||
system("touch /tmp/mow-lock");
|
||||
|
||||
auto webRoot = path(share->outDir).parent_path().string();
|
||||
|
||||
genHTMLul(webRoot, string(APP_NAME) + " " + string(APP_VER));
|
||||
genCSS(share);
|
||||
genHTMLul(share->webRoot, string(APP_NAME) + " " + string(APP_VER), share);
|
||||
system("rm /tmp/mow-lock");
|
||||
}
|
||||
|
||||
createDirTree(share->buffDir);
|
||||
else
|
||||
{
|
||||
recLog("/tmp/mow-lock pesent, skipping update of the webroot page.", share);
|
||||
}
|
||||
|
||||
auto bufPath = cleanDir(share->buffDir) + "/%03d." + share->vidExt;
|
||||
auto secs = to_string(share->secs);
|
||||
|
@ -73,14 +84,24 @@ void recLoop(shared_t *share)
|
|||
|
||||
thread th2(detectLoop, share);
|
||||
|
||||
if (system(cmd.c_str()) != 0)
|
||||
recLog("detect_loop started in a seperate thread.", share);
|
||||
recLog("ffmpeg = " + cmd, share);
|
||||
|
||||
auto ret = system(cmd.c_str());
|
||||
|
||||
if (ret == 0)
|
||||
{
|
||||
recLog("ffmpeg_return_code = " + to_string(ret), share);
|
||||
|
||||
share->recLoopWait = true;
|
||||
|
||||
th2.join();
|
||||
}
|
||||
else
|
||||
{
|
||||
recLog("ffmpeg failed, cooling down for " + to_string(share->secs) + "secs.", share);
|
||||
recLog("ffmpeg_return_code = " + to_string(ret), share);
|
||||
|
||||
// simulate that ffmpeg is running even after it has failed.
|
||||
sleep(share->secs);
|
||||
|
||||
|
@ -89,9 +110,19 @@ void recLoop(shared_t *share)
|
|||
|
||||
if (!share->skipCmd)
|
||||
{
|
||||
recLog("motion not detected by detect loop.", share);
|
||||
recLog("running post command = " + share->postCmd, share);
|
||||
system(share->postCmd.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
recLog("motion detected by detect loop, skpping the post command.", share);
|
||||
}
|
||||
|
||||
recLog("recLoop() -- finished", share);
|
||||
}
|
||||
|
||||
share->logRun = false;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
|
@ -122,9 +153,14 @@ int main(int argc, char** argv)
|
|||
sharedRes.recLoopWait = false;
|
||||
sharedRes.skipCmd = false;
|
||||
sharedRes.init = true;
|
||||
sharedRes.logRun = true;
|
||||
|
||||
thread th3(logLoop, &sharedRes);
|
||||
|
||||
recLoop(&sharedRes);
|
||||
|
||||
th3.join();
|
||||
|
||||
return sharedRes.retCode;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,14 +12,16 @@
|
|||
|
||||
#include "mo_detect.h"
|
||||
|
||||
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share)
|
||||
bool imgDiff(const Mat &prev, const Mat &next, int &diffScore, shared_t *share)
|
||||
{
|
||||
Mat diff;
|
||||
|
||||
absdiff(prev, next, diff);
|
||||
threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY);
|
||||
|
||||
return countNonZero(diff) >= share->imgThresh;
|
||||
diffScore = countNonZero(diff);
|
||||
|
||||
return diffScore >= share->imgThresh;
|
||||
}
|
||||
|
||||
Mat frameFF(VideoCapture *cap, int gap)
|
||||
|
@ -57,6 +59,10 @@ void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *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);
|
||||
detLog("remove_file: " + buffFile, share);
|
||||
|
||||
enforceMaxClips(outDir, share);
|
||||
|
||||
copy_file(buffFile.c_str(), vidOut.c_str());
|
||||
|
@ -65,8 +71,8 @@ void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share)
|
|||
imwrite(imgOut.c_str(), vidThumb);
|
||||
|
||||
genHTMLvid(vidOut, share);
|
||||
genHTMLul(outDir, share->camName + ": " + dayStr);
|
||||
genHTMLul(share->outDir, share->camName);
|
||||
genHTMLul(outDir, share->camName + ": " + dayStr, share);
|
||||
genHTMLul(share->outDir, share->camName, share);
|
||||
}
|
||||
|
||||
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
|
||||
|
@ -79,6 +85,9 @@ bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
|
|||
{
|
||||
Mat prev;
|
||||
Mat next;
|
||||
int diff = 0;
|
||||
int maxDiff = 0;
|
||||
int frameGaps = 0;
|
||||
|
||||
do
|
||||
{
|
||||
|
@ -87,19 +96,29 @@ bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
|
|||
|
||||
next = frameFF(&capture, share->frameGap);
|
||||
|
||||
if (imgDiff(prev, next, share))
|
||||
if (imgDiff(prev, next, diff, share))
|
||||
{
|
||||
resize(next, vidThumb, Size(360, 202), INTER_LINEAR);
|
||||
resize(next, vidThumb, Size(1280, 720), INTER_LINEAR);
|
||||
|
||||
if (diff > maxDiff)
|
||||
{
|
||||
maxDiff = diff;
|
||||
}
|
||||
|
||||
mod = true; break;
|
||||
}
|
||||
|
||||
frameGaps++;
|
||||
}
|
||||
while (!prev.empty() && !next.empty());
|
||||
|
||||
detLog("scanned_buff_file = " + buffFile + " max_score = " + to_string(maxDiff) + " frame_gaps = " + to_string(frameGaps), share);
|
||||
}
|
||||
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 buff file: " + buffFile + " for reading.", share);
|
||||
detLog("check formatting/permissions.", share);
|
||||
detLog("also check if opencv was compiled with FFMPEG encoding enabled.", share);
|
||||
}
|
||||
|
||||
return mod;
|
||||
|
|
|
@ -15,8 +15,9 @@
|
|||
|
||||
#include "common.h"
|
||||
#include "web.h"
|
||||
#include "logger.h"
|
||||
|
||||
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share);
|
||||
bool imgDiff(const Mat &prev, const Mat &next, int &diffScore, 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);
|
||||
|
|
87
src/web.cpp
87
src/web.cpp
|
@ -12,54 +12,64 @@
|
|||
|
||||
#include "web.h"
|
||||
|
||||
void genHTMLul(const string &outputDir, const string &title)
|
||||
void genHTMLul(const string &outputDir, const string &title, shared_t *share)
|
||||
{
|
||||
DIR *dir;
|
||||
struct dirent *ent;
|
||||
vector<string> regNames;
|
||||
vector<string> dirNames;
|
||||
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 += "<link rel='stylesheet' href='/theme.css'>\n";
|
||||
htmlText += "</head>\n";
|
||||
htmlText += "<body>\n";
|
||||
htmlText += "<h2>" + title + "</h2>\n";
|
||||
htmlText += "<h3>" + title + "</h3>\n";
|
||||
|
||||
if (!dirNames.empty())
|
||||
{
|
||||
htmlText += "<ul>\n";
|
||||
|
||||
if ((dir = opendir(outputDir.c_str())) != NULL)
|
||||
{
|
||||
while ((ent = readdir(dir)) != NULL)
|
||||
{
|
||||
if (ent->d_type & DT_REG)
|
||||
{
|
||||
regNames.push_back(string(ent->d_name));
|
||||
}
|
||||
else if (ent->d_type & DT_DIR)
|
||||
{
|
||||
dirNames.push_back(string(ent->d_name));
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
sort(regNames.begin(), regNames.end());
|
||||
sort(dirNames.begin(), dirNames.end());
|
||||
|
||||
for (auto dirName : dirNames)
|
||||
for (auto &&dirName : dirNames)
|
||||
{
|
||||
htmlText += " <li><a href='" + dirName + "'>" + dirName + "</a></li>\n";
|
||||
}
|
||||
|
||||
for (auto regName : regNames)
|
||||
htmlText += "</ul>\n";
|
||||
}
|
||||
|
||||
for (auto &®Name : regNames)
|
||||
{
|
||||
if (regName.ends_with(".html") && !regName.ends_with("index.html"))
|
||||
if (regName.ends_with("_log.html"))
|
||||
{
|
||||
htmlText += " <li><a href='" + regName + "'> <img src='" + replaceAll(regName, ".html", ".jpg") + "' </a></li>\n";
|
||||
logNames.push_back(regName);
|
||||
}
|
||||
else if (regName.ends_with(".html") && !regName.ends_with("index.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>";
|
||||
|
||||
|
@ -79,8 +89,11 @@ void genHTMLvid(const string &outputVid, shared_t *share)
|
|||
filename = replaceAll(filename, share->vidExt, "html");
|
||||
|
||||
htmlText += "<html>\n";
|
||||
htmlText += "<head>\n";
|
||||
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
|
||||
htmlText += "</head>\n";
|
||||
htmlText += "<body>\n";
|
||||
htmlText += "<video width='420' height='320' controls autoplay>\n";
|
||||
htmlText += "<video width=100% height=100% controls autoplay>\n";
|
||||
htmlText += " <source src='" + filename + "' type='video/" + share->vidExt + "'>\n";
|
||||
htmlText += "</video>\n";
|
||||
htmlText += "</body>\n";
|
||||
|
@ -92,3 +105,19 @@ void genHTMLvid(const string &outputVid, shared_t *share)
|
|||
|
||||
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 += "}";
|
||||
|
||||
ofstream file(string(cleanDir(share->webRoot) + "/theme.css").c_str());
|
||||
|
||||
file << cssText << endl;
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
|
||||
#include "common.h"
|
||||
|
||||
void genHTMLul(const string &outputDir, const string &title);
|
||||
void genHTMLul(const string &outputDir, const string &title, shared_t *share);
|
||||
void genHTMLvid(const string &outputVid, shared_t *share);
|
||||
void genCSS(shared_t *share);
|
||||
|
||||
#endif // WEB_H
|
||||
|
|
Loading…
Reference in New Issue
Block a user