Compare commits
No commits in common. "b09ff1a19afff08fffcb8949c08d6c14d17ecdf7" and "25528617d5c1e346d04e7c4c297472ddb2602bae" have entirely different histories.
b09ff1a19a
...
25528617d5
33
README.md
33
README.md
|
@ -34,17 +34,16 @@ recording_stream = rtsp://1.2.3.4:554/h264
|
||||||
# this is the url to the main stream of the IP camera that will be used
|
# this is the url to the main stream of the IP camera that will be used
|
||||||
# to record footage.
|
# to record footage.
|
||||||
#
|
#
|
||||||
web_root = /var/opt/mow/web
|
web_root = /var/www/html
|
||||||
# this is the output directory that will be used to store recorded footage
|
# this is the output directory that will be used to store recorded footage
|
||||||
# from the cameras as well as the web interface for the application.
|
# from the cameras as well as the web interface for the application.
|
||||||
# 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.
|
||||||
#
|
#
|
||||||
buffer_path = /var/opt/mow/buf
|
buffer_path = /tmp
|
||||||
# this is the work directory the app will use to store live footage and
|
# this is the work directory the app will use to store live footage and
|
||||||
# image frames. it's recommended to use a ram disk for this since there
|
# image frames. it's recommended to use a ram disk for this since there
|
||||||
# will be large amounts of io occuring here. 1g of space per camera is
|
# will be large amounts of io occuring here.
|
||||||
# a good rule of thumb.
|
|
||||||
#
|
#
|
||||||
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
|
||||||
|
@ -55,29 +54,11 @@ max_event_secs = 30
|
||||||
# this is the maximum amount of secs of video footage that can be
|
# this is the maximum amount of secs of video footage that can be
|
||||||
# recorded in a motion event.
|
# recorded in a motion event.
|
||||||
#
|
#
|
||||||
img_comp_cmd = magick compare -metric FUZZ &prev& &next& /dev/null
|
|
||||||
# this is the command line template this application will use when calling
|
|
||||||
# the external image comparison application. the external application is
|
|
||||||
# expected to compare snapshots from the video stream to determine how
|
|
||||||
# different the images are from each other. it needs to output a numeric
|
|
||||||
# score with the higher values meaning very different while low values
|
|
||||||
# mean similar images. the snapshots pulled from the stream will be bitmap
|
|
||||||
# formatted so the app will be required to support this format. also avoid
|
|
||||||
# outputting any special chars, only numeric chars with a single '.' if
|
|
||||||
# outputting a decimal value. magick is the default if not defined in the
|
|
||||||
# config file. the special string &prev& will be substituted with the path
|
|
||||||
# to the "previous" bitmap image, behind in time stamp to the image path
|
|
||||||
# subtituted in &next&.
|
|
||||||
#
|
|
||||||
img_comp_out = stderr
|
|
||||||
# this is the standard output stream the app defined in img_comp_cmd will
|
|
||||||
# use to output the comparison score. this can only be stderr or stdout,
|
|
||||||
# any other stream name is considered invalid.
|
|
||||||
#
|
|
||||||
img_thresh = 8000
|
img_thresh = 8000
|
||||||
# this parameter defines the score threshold from img_comp_cmd that will
|
# this application uses 'magick compare' to score the differences between
|
||||||
# be considered motion. any motion events will queue up max_event_secs
|
# two, one second gapped snapshots of the camera stream. any image pairs
|
||||||
# worth of hls clips to be written out to web_root.
|
# that score greater than this value is considered motion and queues up
|
||||||
|
# max_event_secs worth of hls clips to be written out as a motion event.
|
||||||
#
|
#
|
||||||
max_events = 100
|
max_events = 100
|
||||||
# this indicates the maximum amount of motion event video clips to keep
|
# this indicates the maximum amount of motion event video clips to keep
|
||||||
|
|
File diff suppressed because one or more lines are too long
64
install.sh
64
install.sh
|
@ -1,65 +1,11 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
if [ -f "/opt/mow/uninst" ]; then
|
|
||||||
mow -u -f
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -d "/opt/mow" ]; then
|
if [ ! -d "/opt/mow" ]; then
|
||||||
mkdir /opt/mow
|
mkdir /opt/mow
|
||||||
fi
|
fi
|
||||||
|
cp ./.build-mow/mow /opt/mow/bin
|
||||||
if [ ! -d "/var/opt/mow" ]; then
|
|
||||||
mkdir /var/opt/mow
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -d "/etc/mow" ]; then
|
|
||||||
mkdir /etc/mow
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -d "/var/opt/mow/buf" ]; then
|
|
||||||
mkdir /var/opt/mow/buf
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -d "/var/opt/mow/web" ]; then
|
|
||||||
mkdir /var/opt/mow/web
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -e "/var/opt/mow/web/index.html" ]; then
|
|
||||||
rm -v /var/opt/mow/web/index.html
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -e "/var/opt/mow/web/theme.css" ]; then
|
|
||||||
rm -v /var/opt/mow/web/theme.css
|
|
||||||
fi
|
|
||||||
|
|
||||||
touch /var/opt/mow/buf/index.html
|
|
||||||
touch /var/opt/mow/buf/theme.css
|
|
||||||
|
|
||||||
ln -sv /var/opt/mow/buf/index.html /var/opt/mow/web/index.html
|
|
||||||
ln -sv /var/opt/mow/buf/theme.css /var/opt/mow/web/theme.css
|
|
||||||
|
|
||||||
cp -v ./.build-mow/mow /opt/mow/bin
|
|
||||||
cp -v ./bin/hls.js /var/opt/mow/web/hls.js
|
|
||||||
|
|
||||||
echo "writing /opt/mow/run"
|
|
||||||
printf "#!/bin/sh\n" > /opt/mow/run
|
printf "#!/bin/sh\n" > /opt/mow/run
|
||||||
printf "/opt/mow/bin \$1 \$2 \$3\n" >> /opt/mow/run
|
printf "/opt/mow/bin \$1 \$2 \$3\n" >> /opt/mow/run
|
||||||
|
chmod +x /opt/mow/run
|
||||||
echo "writing /opt/mow/uninst"
|
chmod +x /opt/mow/bin
|
||||||
printf "#!/bin/sh\n" > /opt/mow/uninst
|
rm /usr/bin/mow
|
||||||
printf "rm -v /opt/mow/bin\n" >> /opt/mow/uninst
|
ln -s /opt/mow/run /usr/bin/mow
|
||||||
printf "rm -v /opt/mow/run\n" >> /opt/mow/uninst
|
|
||||||
printf "rm -v /opt/mow/uninst\n" >> /opt/mow/uninst
|
|
||||||
printf "rm -v /usr/bin/mow\n" >> /opt/mow/uninst
|
|
||||||
printf "rm -rv /opt/mow\n" >> /opt/mow/uninst
|
|
||||||
printf "rm -r /var/opt/mow/buf\n" >> /opt/mow/uninst
|
|
||||||
printf "deluser mow\n" >> /opt/mow/uninst
|
|
||||||
|
|
||||||
useradd -r mow
|
|
||||||
|
|
||||||
chown -R mow:mow /var/opt/mow
|
|
||||||
|
|
||||||
chmod -v +x /opt/mow/run
|
|
||||||
chmod -v +x /opt/mow/bin
|
|
||||||
chmod -v +x /opt/mow/uninst
|
|
||||||
|
|
||||||
ln -sv /opt/mow/run /usr/bin/mow
|
|
||||||
|
|
2
setup.sh
2
setup.sh
|
@ -2,6 +2,6 @@
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt update -y
|
apt update -y
|
||||||
apt install -y pkg-config cmake make g++
|
apt install -y pkg-config cmake make g++
|
||||||
apt install -y ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev x264 libx264-dev libilmbase-dev qt6-base-dev qtchooser qmake6 qt6-base-dev-tools libxkbcommon-dev libfuse-dev fuse3
|
apt install -y ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev x264 libx264-dev libilmbase-dev qt6-base-dev qtchooser qmake6 qt6-base-dev-tools libxkbcommon-dev libfuse-dev
|
||||||
cp ./bin/magick /usr/bin/magick
|
cp ./bin/magick /usr/bin/magick
|
||||||
chmod +x /usr/bin/magick
|
chmod +x /usr/bin/magick
|
||||||
|
|
159
src/camera.cpp
159
src/camera.cpp
|
@ -12,49 +12,31 @@
|
||||||
|
|
||||||
#include "camera.h"
|
#include "camera.h"
|
||||||
|
|
||||||
Camera::Camera(QObject *parent) : QObject(parent) {}
|
Camera::Camera(QObject *parent) : QObject(parent)
|
||||||
|
|
||||||
void Camera::cleanup()
|
|
||||||
{
|
{
|
||||||
QProcess::execute("rm", {shared.outDir + "/live"});
|
shared.recordUrl.clear();
|
||||||
QProcess::execute("rm", {shared.outDir + "/logs"});
|
shared.postCmd.clear();
|
||||||
QProcess::execute("rm", {shared.outDir + "/img"});
|
shared.camName.clear();
|
||||||
QProcess::execute("rm", {shared.outDir + "/index.html"});
|
|
||||||
QProcess::execute("rm", {shared.outDir + "/stream.m3u8"});
|
shared.retCode = 0;
|
||||||
QProcess::execute("rm", {shared.tmpDir + "/events"});
|
shared.imgThresh = 8000;
|
||||||
|
shared.maxEvents = 100;
|
||||||
|
shared.maxLogSize = 100000;
|
||||||
|
shared.skipCmd = false;
|
||||||
|
shared.postSecs = 60;
|
||||||
|
shared.evMaxSecs = 30;
|
||||||
|
shared.buffPath = QDir::tempPath();
|
||||||
|
shared.webRoot = "/var/www/html";
|
||||||
|
shared.webBg = "#485564";
|
||||||
|
shared.webTxt = "#dee5ee";
|
||||||
|
shared.webFont = "courier";
|
||||||
}
|
}
|
||||||
|
|
||||||
int Camera::start(const QStringList &args)
|
int Camera::start(const QStringList &args)
|
||||||
{
|
{
|
||||||
if (rdConf(getParam("-c", args), &shared))
|
shared.conf = getParam("-c", args);
|
||||||
{
|
|
||||||
QDir().mkpath(shared.outDir);
|
|
||||||
QDir().mkpath(shared.tmpDir);
|
|
||||||
|
|
||||||
QDir().mkpath(shared.outDir + "/events");
|
if (rdConf(&shared))
|
||||||
QDir().mkpath(shared.tmpDir + "/live");
|
|
||||||
QDir().mkpath(shared.tmpDir + "/logs");
|
|
||||||
QDir().mkpath(shared.tmpDir + "/img");
|
|
||||||
|
|
||||||
cleanup();
|
|
||||||
|
|
||||||
touch(shared.tmpDir + "/index.html");
|
|
||||||
touch(shared.tmpDir + "/stream.m3u8");
|
|
||||||
|
|
||||||
QProcess::execute("ln", {"-s", shared.tmpDir + "/live", shared.outDir + "/live"});
|
|
||||||
QProcess::execute("ln", {"-s", shared.tmpDir + "/logs", shared.outDir + "/logs"});
|
|
||||||
QProcess::execute("ln", {"-s", shared.tmpDir + "/img", shared.outDir + "/img"});
|
|
||||||
QProcess::execute("ln", {"-s", shared.tmpDir + "/index.html", shared.outDir + "/index.html"});
|
|
||||||
QProcess::execute("ln", {"-s", shared.tmpDir + "/stream.m3u8", shared.outDir + "/stream.m3u8"});
|
|
||||||
QProcess::execute("ln", {"-s", shared.outDir + "/events", shared.tmpDir + "/events"});
|
|
||||||
|
|
||||||
if (!QDir::setCurrent(shared.tmpDir))
|
|
||||||
{
|
|
||||||
QTextStream(stderr) << "err: failed to change/create the current working directory to camera folder: '" << shared.outDir << "' does it exists?" << Qt::endl;
|
|
||||||
|
|
||||||
shared.retCode = ENOENT;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
auto thr1 = new QThread(nullptr);
|
auto thr1 = new QThread(nullptr);
|
||||||
auto thr2 = new QThread(nullptr);
|
auto thr2 = new QThread(nullptr);
|
||||||
|
@ -71,7 +53,6 @@ int Camera::start(const QStringList &args)
|
||||||
thr3->start();
|
thr3->start();
|
||||||
thr4->start();
|
thr4->start();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return shared.retCode;
|
return shared.retCode;
|
||||||
}
|
}
|
||||||
|
@ -152,6 +133,7 @@ void RecLoop::updateCmd()
|
||||||
recArgs << "-hls_list_size" << "1000";
|
recArgs << "-hls_list_size" << "1000";
|
||||||
recArgs << "-hls_flags" << "append_list+omit_endlist";
|
recArgs << "-hls_flags" << "append_list+omit_endlist";
|
||||||
recArgs << "-rtsp_transport" << "tcp";
|
recArgs << "-rtsp_transport" << "tcp";
|
||||||
|
recArgs << "-stimeout" << "3000";
|
||||||
recArgs << "-t" << QString::number(heartBeat);
|
recArgs << "-t" << QString::number(heartBeat);
|
||||||
recArgs << "stream.m3u8";
|
recArgs << "stream.m3u8";
|
||||||
|
|
||||||
|
@ -161,6 +143,7 @@ void RecLoop::updateCmd()
|
||||||
imgArgs << "-strftime_mkdir" << "1";
|
imgArgs << "-strftime_mkdir" << "1";
|
||||||
imgArgs << "-vf" << "fps=1,scale=320:240";
|
imgArgs << "-vf" << "fps=1,scale=320:240";
|
||||||
imgArgs << "-rtsp_transport" << "tcp";
|
imgArgs << "-rtsp_transport" << "tcp";
|
||||||
|
imgArgs << "-stimeout" << "3000";
|
||||||
imgArgs << "-t" << QString::number(heartBeat);
|
imgArgs << "-t" << QString::number(heartBeat);
|
||||||
imgArgs << "img/" + QString(STRFTIME_FMT) + ".bmp";
|
imgArgs << "img/" + QString(STRFTIME_FMT) + ".bmp";
|
||||||
|
|
||||||
|
@ -241,9 +224,10 @@ bool Upkeep::exec()
|
||||||
enforceMaxImages();
|
enforceMaxImages();
|
||||||
enforceMaxVids();
|
enforceMaxVids();
|
||||||
|
|
||||||
genFrontPage(shared);
|
genHTMLul(".", shared->camName, shared);
|
||||||
|
|
||||||
genCSS(shared);
|
genCSS(shared);
|
||||||
genCamPage(shared);
|
genHTMLul(shared->webRoot, QString(APP_NAME) + " " + QString(APP_VER), shared);
|
||||||
|
|
||||||
return Loop::exec();
|
return Loop::exec();
|
||||||
}
|
}
|
||||||
|
@ -433,63 +417,20 @@ void DetectLoop::pcBreak()
|
||||||
else delayCycles += 5;
|
else delayCycles += 5;
|
||||||
|
|
||||||
detLog("no motion detected, running post command: " + shared->postCmd, shared);
|
detLog("no motion detected, running post command: " + shared->postCmd, shared);
|
||||||
|
system(shared->postCmd.toUtf8().data());
|
||||||
auto args = parseArgs(shared->postCmd.toUtf8(), -1);
|
|
||||||
|
|
||||||
if (args.isEmpty())
|
|
||||||
{
|
|
||||||
detLog("err: did not parse an executable from the post command line.", shared);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
QProcess::execute(args[0], args.mid(1));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod = false;
|
mod = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
float DetectLoop::getFloatFromExe(const QByteArray &line)
|
|
||||||
{
|
|
||||||
QString strLine(line);
|
|
||||||
QString strNum;
|
|
||||||
|
|
||||||
for (auto chr : strLine)
|
|
||||||
{
|
|
||||||
if (chr.isDigit() || (chr == '.'))
|
|
||||||
{
|
|
||||||
strNum.append(chr);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strNum.toFloat();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList DetectLoop::buildArgs(const QString &prev, const QString &next)
|
|
||||||
{
|
|
||||||
auto args = parseArgs(shared->compCmd.toUtf8(), -1);
|
|
||||||
|
|
||||||
for (auto i = 0; i < args.size(); ++i)
|
|
||||||
{
|
|
||||||
if (args[i] == PREV_IMG) args[i] = prev;
|
|
||||||
if (args[i] == NEXT_IMG) args[i] = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DetectLoop::exec()
|
bool DetectLoop::exec()
|
||||||
{
|
{
|
||||||
if (delayCycles > 0)
|
if (delayCycles > 0)
|
||||||
{
|
{
|
||||||
delayCycles -= 1;
|
delayCycles -= 1;
|
||||||
|
|
||||||
detLog("delay: detection cycle skipped. cycles left to be skipped: " + QString::number(delayCycles), shared);
|
detLog("spec: detection cycle skipped. cycles left to be skipped: " + QString::number(delayCycles), shared);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -502,44 +443,28 @@ bool DetectLoop::exec()
|
||||||
detLog(" will try again on the next loop.", shared);
|
detLog(" will try again on the next loop.", shared);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
auto pos = images.size() - 1;
|
|
||||||
auto args = buildArgs(images[pos - 1], images[pos]);
|
|
||||||
|
|
||||||
if (args.isEmpty())
|
|
||||||
{
|
|
||||||
detLog("err: could not parse a executable name from img_comp_cmd: " + shared->compCmd, shared);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
QProcess extComp;
|
QProcess extComp;
|
||||||
|
QStringList args;
|
||||||
|
|
||||||
extComp.start(args[0], args.mid(1));
|
auto pos = images.size() - 1;
|
||||||
|
|
||||||
|
args << "compare";
|
||||||
|
args << "-metric" << "FUZZ";
|
||||||
|
args << images[pos - 1];
|
||||||
|
args << images[pos];
|
||||||
|
args << "/dev/null";
|
||||||
|
|
||||||
|
extComp.start("magick", args);
|
||||||
extComp.waitForFinished();
|
extComp.waitForFinished();
|
||||||
|
|
||||||
float score = 0;
|
QString output = extComp.readAllStandardError();
|
||||||
auto ok = true;
|
|
||||||
|
|
||||||
if (shared->outputType == "stdout")
|
output = output.left(output.indexOf(' '));
|
||||||
{
|
|
||||||
score = getFloatFromExe(extComp.readAllStandardOutput());
|
|
||||||
}
|
|
||||||
else if (shared->outputType == "stderr")
|
|
||||||
{
|
|
||||||
score = getFloatFromExe(extComp.readAllStandardError());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ok)
|
detLog(extComp.program() + " " + args.join(" ") + " --result: " + output, shared);
|
||||||
{
|
|
||||||
detLog("err: img_comp_out: " + shared->outputType + " is not valid. it must be 'stdout' or 'stderr'" , shared);
|
auto score = output.toFloat();
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
detLog(args.join(" ") + " --result: " + QString::number(score), shared);
|
|
||||||
|
|
||||||
if (score >= shared->imgThresh)
|
if (score >= shared->imgThresh)
|
||||||
{
|
{
|
||||||
|
@ -558,8 +483,6 @@ bool DetectLoop::exec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Loop::exec();
|
return Loop::exec();
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,6 @@ private:
|
||||||
|
|
||||||
shared_t shared;
|
shared_t shared;
|
||||||
|
|
||||||
void cleanup();
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
explicit Camera(QObject *parent = nullptr);
|
explicit Camera(QObject *parent = nullptr);
|
||||||
|
@ -128,8 +126,6 @@ private:
|
||||||
bool mod;
|
bool mod;
|
||||||
|
|
||||||
void resetTimers();
|
void resetTimers();
|
||||||
float getFloatFromExe(const QByteArray &line);
|
|
||||||
QStringList buildArgs(const QString &prev, const QString &next);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
||||||
|
|
255
src/common.cpp
255
src/common.cpp
|
@ -194,26 +194,6 @@ bool rdConf(const QString &filePath, shared_t *share)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
share->recordUrl.clear();
|
|
||||||
share->postCmd.clear();
|
|
||||||
share->camName.clear();
|
|
||||||
|
|
||||||
share->retCode = 0;
|
|
||||||
share->imgThresh = 8000;
|
|
||||||
share->maxEvents = 100;
|
|
||||||
share->maxLogSize = 100000;
|
|
||||||
share->skipCmd = false;
|
|
||||||
share->postSecs = 60;
|
|
||||||
share->evMaxSecs = 30;
|
|
||||||
share->conf = filePath;
|
|
||||||
share->buffPath = "/var/opt/" + QString(APP_BIN) + "/buf";
|
|
||||||
share->webRoot = "/var/opt/" + QString(APP_BIN) + "/web";
|
|
||||||
share->webBg = "#485564";
|
|
||||||
share->webTxt = "#dee5ee";
|
|
||||||
share->webFont = "courier";
|
|
||||||
share->outputType = "stderr";
|
|
||||||
share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null";
|
|
||||||
|
|
||||||
QString line;
|
QString line;
|
||||||
|
|
||||||
do
|
do
|
||||||
|
@ -235,52 +215,91 @@ bool rdConf(const QString &filePath, shared_t *share)
|
||||||
rdLine("img_thresh = ", line, &share->imgThresh);
|
rdLine("img_thresh = ", line, &share->imgThresh);
|
||||||
rdLine("max_events = ", line, &share->maxEvents);
|
rdLine("max_events = ", line, &share->maxEvents);
|
||||||
rdLine("max_log_size = ", line, &share->maxLogSize);
|
rdLine("max_log_size = ", line, &share->maxLogSize);
|
||||||
rdLine("img_comp_out = ", line, &share->outputType);
|
|
||||||
rdLine("img_comp_cmd = ", line, &share->compCmd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} while(!line.isEmpty());
|
} while(!line.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
return share->retCode == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool rdConf(shared_t *share)
|
||||||
|
{
|
||||||
|
if (rdConf(share->conf, share))
|
||||||
|
{
|
||||||
if (share->camName.isEmpty())
|
if (share->camName.isEmpty())
|
||||||
{
|
{
|
||||||
share->camName = QFileInfo(share->conf).fileName();
|
share->camName = QFileInfo(share->conf).fileName();
|
||||||
}
|
}
|
||||||
|
|
||||||
share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName;
|
share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName;
|
||||||
share->tmpDir = share->buffPath + "/" + share->camName;
|
share->tmpDir = share->buffPath + "/" + APP_BIN + "/" + share->camName;
|
||||||
share->servPath = QString("/var/opt/") + APP_BIN + "/" + APP_BIN + "." + share->camName + ".service";
|
|
||||||
|
QDir().mkpath(share->outDir);
|
||||||
|
QDir().mkpath(share->tmpDir);
|
||||||
|
|
||||||
|
QDir().mkpath(share->outDir + "/events");
|
||||||
|
QDir().mkpath(share->tmpDir + "/live");
|
||||||
|
QDir().mkpath(share->tmpDir + "/logs");
|
||||||
|
QDir().mkpath(share->tmpDir + "/img");
|
||||||
|
|
||||||
|
touch(share->tmpDir + "/index.html");
|
||||||
|
touch(share->tmpDir + "/stream.html");
|
||||||
|
touch(share->tmpDir + "/stream.m3u8");
|
||||||
|
|
||||||
|
QFile::link(share->tmpDir + "/live", share->outDir + "/live");
|
||||||
|
QFile::link(share->tmpDir + "/logs", share->outDir + "/logs");
|
||||||
|
QFile::link(share->tmpDir + "/img", share->outDir + "/img");
|
||||||
|
QFile::link(share->tmpDir + "/index.html", share->outDir + "/index.html");
|
||||||
|
QFile::link(share->tmpDir + "/stream.html", share->outDir + "/stream.html");
|
||||||
|
QFile::link(share->tmpDir + "/stream.m3u8", share->outDir + "/stream.m3u8");
|
||||||
|
QFile::link(share->outDir + "/events", share->tmpDir + "/events");
|
||||||
|
|
||||||
|
if (!QDir::setCurrent(share->tmpDir))
|
||||||
|
{
|
||||||
|
QTextStream(stderr) << "err: failed to change/create the current working directory to camera folder: '" << share->outDir << "' does it exists?" << Qt::endl;
|
||||||
|
|
||||||
|
share->retCode = ENOENT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return share->retCode == 0;
|
return share->retCode == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void rmServices()
|
MultiInstance::MultiInstance(QObject *parent) : QObject(parent) {}
|
||||||
|
|
||||||
|
void MultiInstance::instStdout()
|
||||||
{
|
{
|
||||||
auto files = lsFilesInDir(QString("/var/opt/") + APP_BIN, ".service");
|
for (auto &&proc : procList)
|
||||||
|
|
||||||
for (auto &&serv : files)
|
|
||||||
{
|
{
|
||||||
QProcess::execute("systemctl", {"stop", serv});
|
QTextStream(stdout) << proc->readAllStandardOutput();
|
||||||
QProcess::execute("systemctl", {"disable", serv});
|
|
||||||
|
|
||||||
QFile::remove(QString("/lib/systemd/system/") + serv);
|
|
||||||
QFile::remove(QString("/var/opt/") + APP_BIN + "/" + serv);
|
|
||||||
}
|
|
||||||
|
|
||||||
QProcess::execute("systemctl", {"daemon-reload"});
|
|
||||||
}
|
|
||||||
|
|
||||||
void listServices()
|
|
||||||
{
|
|
||||||
auto files = lsFilesInDir(QString("/var/opt/") + APP_BIN, ".service");
|
|
||||||
|
|
||||||
for (auto &&serv : files)
|
|
||||||
{
|
|
||||||
QTextStream(stdout) << serv << ": "; QProcess::execute("systemctl", {"is-active", serv});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int loadServices(const QStringList &args)
|
void MultiInstance::instStderr()
|
||||||
|
{
|
||||||
|
for (auto &&proc : procList)
|
||||||
|
{
|
||||||
|
QTextStream(stderr) << proc->readAllStandardError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiInstance::procChanged(QProcess::ProcessState newState)
|
||||||
|
{
|
||||||
|
Q_UNUSED(newState)
|
||||||
|
|
||||||
|
for (auto &&proc : procList)
|
||||||
|
{
|
||||||
|
if (proc->state() == QProcess::Running)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoreApplication::quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
int MultiInstance::start(const QStringList &args)
|
||||||
{
|
{
|
||||||
auto ret = ENOENT;
|
auto ret = ENOENT;
|
||||||
auto path = QDir().cleanPath(getParam("-d", args));
|
auto path = QDir().cleanPath(getParam("-d", args));
|
||||||
|
@ -288,155 +307,37 @@ int loadServices(const QStringList &args)
|
||||||
|
|
||||||
if (!QDir(path).exists())
|
if (!QDir(path).exists())
|
||||||
{
|
{
|
||||||
QTextStream(stderr) << "err: the supplied directory in -d '" << path << "' does not exists or is not a directory." << Qt::endl;
|
QTextStream(stderr) << "err: the supplied directory in -d '" << path << "' does not exists or is not a directory.";
|
||||||
}
|
}
|
||||||
else if (files.isEmpty())
|
else if (files.isEmpty())
|
||||||
{
|
{
|
||||||
QTextStream(stderr) << "err: no config files found in '" << path << "'" << Qt::endl;
|
QTextStream(stderr) << "err: no config files found in '" << path << "'";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ret = 0;
|
ret = 0;
|
||||||
|
|
||||||
QTextStream(stdout) << "loading conf files from dir: " << path << Qt::endl;
|
|
||||||
|
|
||||||
for (auto &&conf : files)
|
for (auto &&conf : files)
|
||||||
{
|
{
|
||||||
shared_t shared;
|
auto proc = new QProcess(this);
|
||||||
|
|
||||||
if (!rdConf(path + "/" + conf, &shared))
|
QStringList subArgs;
|
||||||
{
|
|
||||||
ret = shared.retCode; break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
QTextStream(stdout) << conf << " --" << Qt::endl;
|
|
||||||
|
|
||||||
QFile file(shared.servPath);
|
subArgs << "-c" << path + "/" + conf;
|
||||||
|
|
||||||
if (!file.open(QFile::ReadWrite | QFile::Truncate))
|
connect(proc, &QProcess::readyReadStandardOutput, this, &MultiInstance::instStdout);
|
||||||
{
|
connect(proc, &QProcess::readyReadStandardError, this, &MultiInstance::instStderr);
|
||||||
QTextStream(stderr) << "err: failed to open service file: " << shared.servPath << " for writing. reason: " << file.errorString();
|
connect(proc, &QProcess::stateChanged, this, &MultiInstance::procChanged);
|
||||||
|
|
||||||
ret = EACCES; file.close(); break;
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, proc, &QProcess::terminate);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
file.write("[Unit]\n");
|
|
||||||
file.write("Description=" + QByteArray(APP_NAME) + " Camera - " + shared.camName.toUtf8() + "\n");
|
|
||||||
file.write("After=network.target\n\n");
|
|
||||||
file.write("[Service]\n");
|
|
||||||
file.write("Type=simple\n");
|
|
||||||
file.write("User=" + QByteArray(APP_BIN) + "\n");
|
|
||||||
file.write("Restart=always\n");
|
|
||||||
file.write("RestartSec=5\n");
|
|
||||||
file.write("TimeoutStopSec=infinity\n");
|
|
||||||
file.write("ExecStart=/usr/bin/env " + QByteArray(APP_BIN) + " -c " + shared.conf.toUtf8() + "\n\n");
|
|
||||||
file.write("[Install]\n");
|
|
||||||
file.write("WantedBy=multi-user.target");
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
auto servName = QFileInfo(shared.servPath).fileName();
|
proc->setProgram(APP_BIN);
|
||||||
|
proc->setArguments(subArgs);
|
||||||
|
proc->start();
|
||||||
|
|
||||||
if (!QFile::link(shared.servPath, "/lib/systemd/system/" + servName))
|
procList.append(proc);
|
||||||
{
|
|
||||||
ret = EACCES; break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (ret == 0) ret = QProcess::execute("systemctl", {"daemon-reload"});
|
|
||||||
if (ret == 0) ret = QProcess::execute("systemctl", {"enable", servName});
|
|
||||||
if (ret == 0) ret = QProcess::execute("systemctl", {"start", servName});
|
|
||||||
|
|
||||||
if (ret != 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
QTextStream(stdout) << "Successfully loaded camera service: " << servName << Qt::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList parseArgs(const QByteArray &data, int maxArgs, int *pos)
|
|
||||||
{
|
|
||||||
QStringList ret;
|
|
||||||
QString arg;
|
|
||||||
|
|
||||||
auto line = QString::fromUtf8(data);
|
|
||||||
auto inDQuotes = false;
|
|
||||||
auto inSQuotes = false;
|
|
||||||
auto escaped = false;
|
|
||||||
|
|
||||||
if (pos != nullptr) *pos = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < line.size(); ++i)
|
|
||||||
{
|
|
||||||
if (pos != nullptr) *pos += 1;
|
|
||||||
|
|
||||||
if ((line[i] == '\'') && !inDQuotes && !escaped)
|
|
||||||
{
|
|
||||||
// single quote '
|
|
||||||
|
|
||||||
inSQuotes = !inSQuotes;
|
|
||||||
}
|
|
||||||
else if ((line[i] == '\"') && !inSQuotes && !escaped)
|
|
||||||
{
|
|
||||||
// double quote "
|
|
||||||
|
|
||||||
inDQuotes = !inDQuotes;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
escaped = false;
|
|
||||||
|
|
||||||
if (line[i].isSpace() && !inDQuotes && !inSQuotes)
|
|
||||||
{
|
|
||||||
// space
|
|
||||||
|
|
||||||
if (!arg.isEmpty())
|
|
||||||
{
|
|
||||||
ret.append(arg);
|
|
||||||
arg.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if ((line[i] == '\\') && ((i + 1) < line.size()))
|
|
||||||
{
|
|
||||||
if ((line[i + 1] == '\'') || (line[i + 1] == '\"'))
|
|
||||||
{
|
|
||||||
escaped = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
arg.append(line[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
arg.append(line[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((ret.size() >= maxArgs) && (maxArgs != -1))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!arg.isEmpty() && !inDQuotes && !inSQuotes)
|
|
||||||
{
|
|
||||||
ret.append(arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
34
src/common.h
34
src/common.h
|
@ -26,11 +26,10 @@
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
#define APP_VER "3.2"
|
#define APP_VER "3.1"
|
||||||
#define APP_NAME "Motion Watch"
|
#define APP_NAME "Motion Watch"
|
||||||
#define APP_BIN "mow"
|
#define APP_BIN "mow"
|
||||||
#define REC_LOG_NAME "rec_log_lines.html"
|
#define REC_LOG_NAME "rec_log_lines.html"
|
||||||
|
@ -40,8 +39,6 @@ using namespace std;
|
||||||
#define STRFTIME_FMT "%Y%m%d%H%M%S"
|
#define STRFTIME_FMT "%Y%m%d%H%M%S"
|
||||||
#define MAX_IMAGES 1000
|
#define MAX_IMAGES 1000
|
||||||
#define MAX_VIDEOS 1000
|
#define MAX_VIDEOS 1000
|
||||||
#define PREV_IMG "&prev&"
|
|
||||||
#define NEXT_IMG "&next&"
|
|
||||||
|
|
||||||
struct evt_t
|
struct evt_t
|
||||||
{
|
{
|
||||||
|
@ -69,9 +66,6 @@ struct shared_t
|
||||||
QString webTxt;
|
QString webTxt;
|
||||||
QString webFont;
|
QString webFont;
|
||||||
QString webRoot;
|
QString webRoot;
|
||||||
QString servPath;
|
|
||||||
QString outputType;
|
|
||||||
QString compCmd;
|
|
||||||
bool skipCmd;
|
bool skipCmd;
|
||||||
int evMaxSecs;
|
int evMaxSecs;
|
||||||
int postSecs;
|
int postSecs;
|
||||||
|
@ -87,11 +81,8 @@ QStringList lsDirsInDir(const QString &path);
|
||||||
QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir);
|
QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir);
|
||||||
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
|
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
|
||||||
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
|
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
|
||||||
QStringList parseArgs(const QByteArray &data, int maxArgs, int *pos = nullptr);
|
|
||||||
bool rdConf(const QString &filePath, shared_t *share);
|
bool rdConf(const QString &filePath, shared_t *share);
|
||||||
int loadServices(const QStringList &args);
|
bool rdConf(shared_t *share);
|
||||||
void listServices();
|
|
||||||
void rmServices();
|
|
||||||
void touch(const QString &path);
|
void touch(const QString &path);
|
||||||
void rdLine(const QString ¶m, const QString &line, QString *value);
|
void rdLine(const QString ¶m, const QString &line, QString *value);
|
||||||
void rdLine(const QString ¶m, const QString &line, int *value);
|
void rdLine(const QString ¶m, const QString &line, int *value);
|
||||||
|
@ -99,4 +90,25 @@ void enforceMaxEvents(shared_t *share);
|
||||||
void enforceMaxImages();
|
void enforceMaxImages();
|
||||||
void enforceMaxVids();
|
void enforceMaxVids();
|
||||||
|
|
||||||
|
class MultiInstance : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
QList<QProcess*> procList;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
|
||||||
|
void instStdout();
|
||||||
|
void instStderr();
|
||||||
|
void procChanged(QProcess::ProcessState newState);
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
explicit MultiInstance(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
int start(const QStringList &args);
|
||||||
|
};
|
||||||
|
|
||||||
#endif // COMMON_H
|
#endif // COMMON_H
|
||||||
|
|
74
src/main.cpp
74
src/main.cpp
|
@ -13,27 +13,6 @@
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "camera.h"
|
#include "camera.h"
|
||||||
|
|
||||||
void showHelp()
|
|
||||||
{
|
|
||||||
QTextStream(stdout) << APP_NAME << " " << APP_VER << Qt::endl << Qt::endl;
|
|
||||||
QTextStream(stdout) << "Usage: mow <argument>" << Qt::endl << Qt::endl;
|
|
||||||
QTextStream(stdout) << "-h : display usage information about this application." << Qt::endl;
|
|
||||||
QTextStream(stdout) << "-c : path to the config file used to run a single camera instance." << Qt::endl;
|
|
||||||
QTextStream(stdout) << "-d : path to a directory that can contain multiple config files." << Qt::endl;
|
|
||||||
QTextStream(stdout) << " each file found in the directory will be used to create a " << Qt::endl;
|
|
||||||
QTextStream(stdout) << " systemd service for the camera. already existing camera" << Qt::endl;
|
|
||||||
QTextStream(stdout) << " services will be reloaded and restarted. any services without" << Qt::endl;
|
|
||||||
QTextStream(stdout) << " a config file will be removed." << Qt::endl;
|
|
||||||
QTextStream(stdout) << "-i : this is the same as -d except a directory does not need to be" << Qt::endl;
|
|
||||||
QTextStream(stdout) << " provided. the default directory /etc/" << APP_BIN << " will be used." << Qt::endl;
|
|
||||||
QTextStream(stdout) << "-v : display the current version." << Qt::endl;
|
|
||||||
QTextStream(stdout) << "-u : uninstall the entire app from your system, including all" << Qt::endl;
|
|
||||||
QTextStream(stdout) << " camera services." << Qt::endl;
|
|
||||||
QTextStream(stdout) << "-f : force an action without pausing for user confirmation." << Qt::endl;
|
|
||||||
QTextStream(stdout) << "-l : list all camera services along with statuses." << Qt::endl;
|
|
||||||
QTextStream(stdout) << "-r : remove all camera services." << Qt::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
QCoreApplication app(argc, argv);
|
QCoreApplication app(argc, argv);
|
||||||
|
@ -46,7 +25,14 @@ int main(int argc, char** argv)
|
||||||
|
|
||||||
if (args.contains("-h"))
|
if (args.contains("-h"))
|
||||||
{
|
{
|
||||||
showHelp();
|
QTextStream(stdout) << "Motion Watch " << APP_VER << Qt::endl << Qt::endl;
|
||||||
|
QTextStream(stdout) << "Usage: mow <argument>" << Qt::endl << Qt::endl;
|
||||||
|
QTextStream(stdout) << "-h : display usage information about this application." << Qt::endl;
|
||||||
|
QTextStream(stdout) << "-c : path to the config file used to run a single camera instance." << Qt::endl;
|
||||||
|
QTextStream(stdout) << "-d : path to a directory that can contain multiple config files." << Qt::endl;
|
||||||
|
QTextStream(stdout) << " each file found in the directory will be used to run a" << Qt::endl;
|
||||||
|
QTextStream(stdout) << " camera instance." << Qt::endl;
|
||||||
|
QTextStream(stdout) << "-v : display the current version." << Qt::endl << Qt::endl;
|
||||||
}
|
}
|
||||||
else if (args.contains("-v"))
|
else if (args.contains("-v"))
|
||||||
{
|
{
|
||||||
|
@ -54,19 +40,14 @@ int main(int argc, char** argv)
|
||||||
}
|
}
|
||||||
else if (args.contains("-d"))
|
else if (args.contains("-d"))
|
||||||
{
|
{
|
||||||
rmServices(); ret = loadServices(args);
|
auto *muli = new MultiInstance(&app);
|
||||||
}
|
|
||||||
else if (args.contains("-l"))
|
|
||||||
{
|
|
||||||
listServices();
|
|
||||||
}
|
|
||||||
else if (args.contains("-i"))
|
|
||||||
{
|
|
||||||
args.clear();
|
|
||||||
args.append("-d");
|
|
||||||
args.append("/etc/" + QString(APP_BIN));
|
|
||||||
|
|
||||||
rmServices(); ret = loadServices(args);
|
ret = muli->start(args);
|
||||||
|
|
||||||
|
if (ret == 0)
|
||||||
|
{
|
||||||
|
ret = QCoreApplication::exec();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (args.contains("-c"))
|
else if (args.contains("-c"))
|
||||||
{
|
{
|
||||||
|
@ -79,32 +60,9 @@ int main(int argc, char** argv)
|
||||||
ret = QCoreApplication::exec();
|
ret = QCoreApplication::exec();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (args.contains("-u"))
|
|
||||||
{
|
|
||||||
if (args.contains("-f"))
|
|
||||||
{
|
|
||||||
rmServices(); QProcess::execute("/opt/mow/uninst", QStringList());
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
char ans;
|
QTextStream(stderr) << "err: no config file(s) were given in -c" << Qt::endl;
|
||||||
|
|
||||||
std::cout << "This will completely uninstall " << APP_NAME << " from your system. continue y/n? ";
|
|
||||||
std::cin >> ans;
|
|
||||||
|
|
||||||
if (ans == 'y' || ans == 'Y')
|
|
||||||
{
|
|
||||||
rmServices(); QProcess::execute("/opt/mow/uninst", QStringList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (args.contains("-r"))
|
|
||||||
{
|
|
||||||
rmServices();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
showHelp();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|
150
src/web.cpp
150
src/web.cpp
|
@ -12,8 +12,12 @@
|
||||||
|
|
||||||
#include "web.h"
|
#include "web.h"
|
||||||
|
|
||||||
void genFrontPage(shared_t *share)
|
void genHTMLul(const QString &outputDir, const QString &title, shared_t *share)
|
||||||
{
|
{
|
||||||
|
QStringList logNames;
|
||||||
|
QStringList eveNames;
|
||||||
|
QStringList dirNames;
|
||||||
|
|
||||||
QString htmlText = "<!DOCTYPE html>\n";
|
QString htmlText = "<!DOCTYPE html>\n";
|
||||||
|
|
||||||
htmlText += "<html>\n";
|
htmlText += "<html>\n";
|
||||||
|
@ -25,47 +29,12 @@ void genFrontPage(shared_t *share)
|
||||||
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
|
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
|
||||||
htmlText += "</head>\n";
|
htmlText += "</head>\n";
|
||||||
htmlText += "<body>\n";
|
htmlText += "<body>\n";
|
||||||
htmlText += "<h3>" + QString(APP_NAME) + " " + QString(APP_VER) + "</h3>\n";
|
htmlText += "<h3>" + title + "</h3>\n";
|
||||||
|
|
||||||
auto dirNames = lsDirsInDir(share->buffPath);
|
if (QDir().exists(outputDir + "/live"))
|
||||||
|
|
||||||
htmlText += "<ul>\n";
|
|
||||||
|
|
||||||
for (auto &&dirName : dirNames)
|
|
||||||
{
|
{
|
||||||
htmlText += " <li><a href='" + dirName + "/index.html'>" + dirName + "</a></li>\n";
|
eveNames = lsFilesInDir(outputDir + "/events", ".html");
|
||||||
}
|
logNames = lsFilesInDir(outputDir + "/logs", "_log.html");
|
||||||
|
|
||||||
htmlText += "</ul>\n";
|
|
||||||
htmlText += "</body>\n";
|
|
||||||
htmlText += "</html>";
|
|
||||||
|
|
||||||
QFile outFile(QDir().cleanPath(share->buffPath) + "/index.html");
|
|
||||||
|
|
||||||
outFile.open(QFile::WriteOnly);
|
|
||||||
outFile.write(htmlText.toUtf8());
|
|
||||||
outFile.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void genCamPage(shared_t *share)
|
|
||||||
{
|
|
||||||
auto outputDir = QDir().cleanPath(share->tmpDir);
|
|
||||||
QString 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>" + share->camName + "</h3>\n";
|
|
||||||
|
|
||||||
if (QDir().exists(outputDir + "/logs"))
|
|
||||||
{
|
|
||||||
auto logNames = lsFilesInDir(outputDir + "/logs", "_log.html");
|
|
||||||
|
|
||||||
htmlText += "<h4>Logs</h4>\n";
|
htmlText += "<h4>Logs</h4>\n";
|
||||||
htmlText += "<ul>\n";
|
htmlText += "<ul>\n";
|
||||||
|
@ -80,21 +49,14 @@ void genCamPage(shared_t *share)
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlText += "</ul>\n";
|
htmlText += "</ul>\n";
|
||||||
}
|
|
||||||
|
|
||||||
if (QDir().exists(outputDir + "/live"))
|
|
||||||
{
|
|
||||||
htmlText += "<h4>Live</h4>\n";
|
htmlText += "<h4>Live</h4>\n";
|
||||||
|
htmlText += "<ul>\n";
|
||||||
genHTMLstream(htmlText);
|
htmlText += " <li><a href='stream.html'>" + share->camName + ":live" + "</a></li>\n";
|
||||||
}
|
htmlText += "</ul>\n";
|
||||||
|
|
||||||
if (QDir().exists(outputDir + "/events"))
|
|
||||||
{
|
|
||||||
auto eveNames = lsFilesInDir(outputDir + "/events", ".html");
|
|
||||||
|
|
||||||
htmlText += "<h4>Motion Events</h4>\n";
|
htmlText += "<h4>Motion Events</h4>\n";
|
||||||
|
|
||||||
|
genHTMLstream("stream");
|
||||||
|
|
||||||
for (auto &&eveName : eveNames)
|
for (auto &&eveName : eveNames)
|
||||||
{
|
{
|
||||||
auto name = eveName;
|
auto name = eveName;
|
||||||
|
@ -104,6 +66,19 @@ void genCamPage(shared_t *share)
|
||||||
htmlText += "<a href='events/" + eveName + "'><img src='events/" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n";
|
htmlText += "<a href='events/" + eveName + "'><img src='events/" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dirNames = lsDirsInDir(outputDir);
|
||||||
|
|
||||||
|
htmlText += "<ul>\n";
|
||||||
|
|
||||||
|
for (auto &&dirName : dirNames)
|
||||||
|
{
|
||||||
|
htmlText += " <li><a href='" + dirName + "/index.html'>" + dirName + "</a></li>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlText += "</ul>\n";
|
||||||
|
}
|
||||||
|
|
||||||
htmlText += "</body>\n";
|
htmlText += "</body>\n";
|
||||||
htmlText += "</html>";
|
htmlText += "</html>";
|
||||||
|
@ -115,32 +90,51 @@ void genCamPage(shared_t *share)
|
||||||
outFile.close();
|
outFile.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void genHTMLstream(QString &text)
|
void genHTMLstream(const QString &name)
|
||||||
{
|
{
|
||||||
text += "<script src=\"/hls.js\">\n";
|
QString htmlText = "<!DOCTYPE html>\n";
|
||||||
text += "</script>\n";
|
|
||||||
text += "<video width=50% height=50% id=\"video\" controls>\n";
|
htmlText += "<html>\n";
|
||||||
text += "</video>\n";
|
htmlText += "<head>\n";
|
||||||
text += "<script>\n";
|
htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n";
|
||||||
text += " var video = document.getElementById('video');\n";
|
htmlText += "<meta http-equiv=\"Pragma\" content=\"no-cache\" />\n";
|
||||||
text += " if (Hls.isSupported()) {\n";
|
htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n";
|
||||||
text += " var hls = new Hls({\n";
|
htmlText += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n";
|
||||||
text += " debug: true,\n";
|
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
|
||||||
text += " });\n";
|
htmlText += "</head>\n";
|
||||||
text += " hls.loadSource('stream.m3u8');\n";
|
htmlText += "<body>\n";
|
||||||
text += " hls.attachMedia(video);\n";
|
htmlText += " <script src=\"https://cdn.jsdelivr.net/npm/hls.js@1\">\n";
|
||||||
text += " hls.on(Hls.Events.MEDIA_ATTACHED, function () {\n";
|
htmlText += " </script>\n";
|
||||||
text += " video.muted = true;\n";
|
htmlText += " <video width=100% height=100% id=\"video\" controls>\n";
|
||||||
text += " video.play();\n";
|
htmlText += " </video>\n";
|
||||||
text += " });\n";
|
htmlText += " <script>\n";
|
||||||
text += " }\n";
|
htmlText += " var video = document.getElementById('video');\n";
|
||||||
text += " else if (video.canPlayType('application/vnd.apple.mpegurl')) {\n";
|
htmlText += " if (Hls.isSupported()) {\n";
|
||||||
text += " video.src = 'stream.m3u8';\n";
|
htmlText += " var hls = new Hls({\n";
|
||||||
text += " video.addEventListener('canplay', function () {\n";
|
htmlText += " debug: true,\n";
|
||||||
text += " video.play();\n";
|
htmlText += " });\n";
|
||||||
text += " });\n";
|
htmlText += " hls.loadSource('" + name + ".m3u8');\n";
|
||||||
text += " }\n";
|
htmlText += " hls.attachMedia(video);\n";
|
||||||
text += "</script>\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>";
|
||||||
|
|
||||||
|
QFile outFile(name + ".html");
|
||||||
|
|
||||||
|
outFile.open(QFile::WriteOnly);
|
||||||
|
outFile.write(htmlText.toUtf8());
|
||||||
|
outFile.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void genHTMLvod(const QString &name)
|
void genHTMLvod(const QString &name)
|
||||||
|
@ -181,7 +175,7 @@ void genCSS(shared_t *share)
|
||||||
cssText += " color: " + share->webTxt + ";\n";
|
cssText += " color: " + share->webTxt + ";\n";
|
||||||
cssText += "}\n";
|
cssText += "}\n";
|
||||||
|
|
||||||
QFile outFile(QDir().cleanPath(share->buffPath) + "/theme.css");
|
QFile outFile(QDir().cleanPath(share->webRoot) + "/theme.css");
|
||||||
|
|
||||||
outFile.open(QFile::WriteOnly);
|
outFile.open(QFile::WriteOnly);
|
||||||
outFile.write(cssText.toUtf8());
|
outFile.write(cssText.toUtf8());
|
||||||
|
|
|
@ -15,9 +15,8 @@
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
void genFrontPage(shared_t *share);
|
void genHTMLul(const QString &outputDir, const QString &title, shared_t *share);
|
||||||
void genCamPage(shared_t *share);
|
void genHTMLstream(const QString &name);
|
||||||
void genHTMLstream(QString &text);
|
|
||||||
void genHTMLvod(const QString &name);
|
void genHTMLvod(const QString &name);
|
||||||
void genCSS(shared_t *share);
|
void genCSS(shared_t *share);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user