Compare commits

..

11 Commits

Author SHA1 Message Date
zii
970b9b5fc5 v3.3
-releasing to master branch
2023-12-24 10:30:36 -05:00
zii
2aa5683c15 -fixed up the documentation 2023-12-24 10:26:52 -05:00
zii
7ca4fc3dbe v3.3.t9
-moved buff_path setup code away from rdconf() to loadSh(). this
 insures root will never take ownership of the camera buffer.
-added service group option.
-updated the documentation in preperation of release.
2023-12-23 17:38:11 -05:00
zii
bb8a1fad45 v3.3.t8
-removed the chown commands from install services function. It
 will now be up to the user to manage folder permissions.
2023-12-18 21:07:00 -05:00
Zii
da941d8d18 v3.3.t7
-detect loop will now use the previous video clip as a base for the
 next 3 video clips. doing this prevents the same clips from being
 processed multiple times.
2023-11-23 09:22:06 -05:00
Zii
5b444fd754 v3.3.t6
-detectloop now operates on a minimum of 3 clips and then grabs
 only the second and third to last clips. this insures only
 finished clips from the stream are grabbed.
2023-11-19 20:45:22 -05:00
Zii
4f0c37b92b v3.3.t5
-again reformed the detectloop class to use filesystem monitoring
 instead of timers to pull clips from the live stream.

-removed the loop class after reforming detectloop.
2023-11-18 18:40:26 -05:00
Zii
60a24c9d67 v3.3.t4
-completely reformed the eventloop code to be more efficent and
 removed the use backward/forward facing functions.
-added the live_secs config option that limits the amount of live
 footage the app will record to the buffer directory.
-moved away from ffmpeg hls formatting. live footage will instead
 just be recorded in clips inside of the 'live' directory, the
 stream.m3u8 file will not longer be created. doing this removes
 the codec/container limitations that hls imposed.
-changed up the default conf values that better suits a low spec
 machine like a resberrypi.
2023-11-05 18:44:50 -05:00
Zii
525c342c0f v3.3.t3
-moved all service related code to seperate files.
-split the camera service into 2 systemd services.
-the -i, -c and -d options now function differently to support the
 split systemd services.
-internal logging was completely removed. all verbose output will
 instead just go to stderr/stdout. will use journalctl for real
 time logging instead.
-fixed the magick compile/install script so it will actually run
 now.
-fixed a bug in the config file reading functions so it will now
 trim off white spaces and line breaks at the ends. doing this
 now prevents undefined behaviour if any parameter has a line
 break in it.
-service user was added as a config file option. this can be used
 to set user name the installed services will run under. the
 default user is "mow."
-the install.sh script will now add the default mow user to the
 video group making it possible to record footage from webcams
 without permission issues.
2023-10-27 15:43:17 -04:00
Zii
83080cfe41 v3.3.t2
-pixel sizes for video and thumbnails are now configurable
-the amount of recording and snapshot threads are now
 configurable
-changed the default buffer and recording directories to
 drop the application name and also drop a level
-service files are now stored in a dedicated dir
-fixed logging so it will capture errors better
-moved folder structure building to rdConfig() and added more
 error capturing
-recording fps is now configurable
2023-10-19 15:04:39 -04:00
Zii
41ccf1d1e7 v3.3.t1
-removed the web interface. this project will instead continue to
 focus on backend operations. external applications can interface
 with the buf/rec directories to provide frontend operations.

-removed the magick binary file from the project. magick will
 instead be built from source on the target machine for maximum
 support for the target architecture.

-stream codec and format are now user configurable.

-recording thumbnail and video formats are now user configurable.
2023-10-08 10:09:15 -04:00
19 changed files with 808 additions and 1109 deletions

3
.gitignore vendored
View File

@ -57,5 +57,4 @@ compile_commands.json
# Build folders # Build folders
/.build-mow /.build-mow
/.build-opencv /.build-imagemagick
/src/opencv

View File

@ -20,12 +20,10 @@ add_executable(mow
src/main.cpp src/main.cpp
src/common.h src/common.h
src/common.cpp src/common.cpp
src/web.h
src/web.cpp
src/logger.h
src/logger.cpp
src/camera.h src/camera.h
src/camera.cpp src/camera.cpp
src/services.h
src/services.cpp
) )
target_link_libraries(mow Qt${QT_VERSION_MAJOR}::Core ${OpenCV_LIBS}) target_link_libraries(mow Qt${QT_VERSION_MAJOR}::Core ${OpenCV_LIBS})

View File

@ -30,30 +30,34 @@ parameters supported and descriptions of each.
# also note to avoid using empty lines. if you're going to need an empty # also note to avoid using empty lines. if you're going to need an empty
# line, start it with a '#' # line, start it with a '#'
# #
recording_stream = rtsp://1.2.3.4:554/h264 recording_uri = 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 uri to the main stream of the IP camera that will be used
# to record footage. # to record footage. it can be a url to an rtsp stream or a direct device
# path such as /dev/video0.
# #
web_root = /var/opt/mow/web buffer_path = /var/buffer
# 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.
# warning: this will overwrite any existing index.html files so be sure
# to choose a directory that doesn't have an existing website.
#
buffer_path = /var/opt/mow/buf
# 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. 1GB of space per camera is
# a good rule of thumb. # a good rule of thumb.
# #
rec_path = /var/footage
# this is video output directory that will be used to store any footage
# that contain any motion events.
#
live_secs = 30
# this is the maximum amount of seconds worth of live footage to keep in
# buffer_path before deleting the oldest 2 seconds worth of footage.
# note: each video clip in buffer_path is typically 2 seconds long.
#
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 also be used to as the base directory in web_root. if not # name will also be used as the base directory in buffer_path and rec_path.
# defined, the name of the config file will be used. # if not defined, the name of the config file will be used.
# #
max_event_secs = 30 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
# recorded in a motion event. # in a motion event.
# #
img_comp_cmd = magick compare -metric FUZZ &prev& &next& /dev/null img_comp_cmd = magick compare -metric FUZZ &prev& &next& /dev/null
# this is the command line template this application will use when calling # this is the command line template this application will use when calling
@ -74,14 +78,35 @@ img_comp_out = stderr
# use to output the comparison score. this can only be stderr or stdout, # use to output the comparison score. this can only be stderr or stdout,
# any other stream name is considered invalid. # any other stream name is considered invalid.
# #
stream_codec = copy
# this is the encoding codec to use when recording footage from the camera.
# the list of supported codecs entirely depend on the hosts' ffmpeg
# installation. run 'ffmpeg -codecs' to determine this list. if not
# defined, 'copy' will be used as in it will just copy the codec format
# from camera itself.
#
stream_ext = .avi
# this is the file extension that will be used to when recording footage
# from the camera in buffer_path. ffmpeg will also use this to determine
# what format container to use for the video clips.
#
thumbnail_ext = .jpg
# this the image format that will be used when creating the thumbnails
# for the videos clips recorded to rec_path.
#
rec_ext = .avi
# this the the file extension that will be used when storing motion footage
# to rec_path. ffmpeg will also use this to determine what format container
# to use for the video clips.
#
img_thresh = 8000 img_thresh = 8000
# this parameter defines the score threshold from img_comp_cmd that will # this parameter defines the score threshold from img_comp_cmd that will
# be considered motion. any motion events will queue up max_event_secs # be considered motion. any motion events will queue up max_event_secs
# worth of hls clips to be written out to web_root. # worth of video clips to be written out to rec_path.
# #
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
# before deleting the oldest clip. # in rec_path before deleting the oldest clip.
# #
post_secs = 60 post_secs = 60
# this is the amount of seconds to wait before running the command # this is the amount of seconds to wait before running the command
@ -92,24 +117,33 @@ post_cmd = move_the_ptz_camera.py
# this an optional command to run with post_secs. one great use for this # this an optional command to run with post_secs. one great use for this
# is to move a ptz camera to the next position of it's patrol pattern. # is to move a ptz camera to the next position of it's patrol pattern.
# note: the call to this command will be delayed if motion was detected. # note: the call to this command will be delayed if motion was detected.
# also, motion detection is paused while this command is running.
# #
max_log_size = 50000 rec_fps = 30
# this is the maximum byte size of all log files that can be stored in # this sets the recording frames per second for the footage recorded
# web_root. whenever this limit is met, the log file will be deleted and # from the camera. this has no affect if using 'copy' as the
# then eventually recreated blank. # stream_codec.
# #
web_text = #dee5ee rec_scale = 1280:720
# this can be used to customize the color of the text in the web # this sets the pixel scale of the recorded footage from the camera. it
# interface. it can be defined as any color understood by html5 standard. # uses width, height numeric strings seperated by a colon, eg W:H. this
# has no affect of using 'copy' as the stream_codec.
# #
web_bg = #485564 img_scale = 320:240
# this can be used to customize the background color of the web # this sets the pixel size of the thumbnails for recorded stored in
# interface. just like web_text, it also follows the html5 standard. # rec_path. it uses width, height numeric strings seperated by a colon,
# eg W:H.
# #
web_font = courier service_user = mow
# this will customize the text font family to use in the web interface. # this sets the service local user of the application dictating the
# it is recommended to use mono-spaced font because this is also used to # amount privilege it will have on the host. if not defined, the
# display logs and logs are best displayed in mono-spaced font. # unprivileged user 'mow' will be used. which ever user is defined here
# just make sure it has read/write access to buffer_path and rec_path.
#
service_group = mow
# this sets the service local group of the application that can further
# refine host privileges. if not defined, the name stored in
# service_user will be used.
``` ```
### Setup/Build/Install ### ### Setup/Build/Install ###

File diff suppressed because one or more lines are too long

Binary file not shown.

13
imgmagick_build.sh Normal file
View File

@ -0,0 +1,13 @@
#!/bin/sh
export DEBIAN_FRONTEND=noninteractive
if [ ! -f "/usr/local/bin/magick" ]
then
apt install -y git
git clone https://github.com/ImageMagick/ImageMagick.git .build-imagemagick
cd .build-imagemagick
./configure
make
make install
ldconfig /usr/local/lib
fi

View File

@ -3,42 +3,27 @@ if [ -f "/opt/mow/uninst" ]; then
mow -u -f mow -u -f
fi fi
if [ ! -d "/opt" ]; then
mkdir /opt
fi
if [ ! -d "/opt/mow" ]; then if [ ! -d "/opt/mow" ]; then
mkdir /opt/mow mkdir /opt/mow
fi fi
if [ ! -d "/var/opt/mow" ]; then if [ ! -d "/var/footage" ]; then
mkdir /var/opt/mow mkdir /var/footage
fi fi
if [ ! -d "/etc/mow" ]; then if [ ! -d "/etc/mow" ]; then
mkdir /etc/mow mkdir /etc/mow
fi fi
if [ ! -d "/var/opt/mow/buf" ]; then if [ ! -d "/var/buffer" ]; then
mkdir /var/opt/mow/buf mkdir /var/buffer
fi 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 ./.build-mow/mow /opt/mow/bin
cp -v ./bin/hls.js /var/opt/mow/web/hls.js
echo "writing /opt/mow/run" echo "writing /opt/mow/run"
printf "#!/bin/sh\n" > /opt/mow/run printf "#!/bin/sh\n" > /opt/mow/run
@ -46,20 +31,24 @@ printf "/opt/mow/bin \$1 \$2 \$3\n" >> /opt/mow/run
echo "writing /opt/mow/uninst" echo "writing /opt/mow/uninst"
printf "#!/bin/sh\n" > /opt/mow/uninst printf "#!/bin/sh\n" > /opt/mow/uninst
printf "mow -r\n" >> /opt/mow/uninst
printf "rm -v /opt/mow/bin\n" >> /opt/mow/uninst printf "rm -v /opt/mow/bin\n" >> /opt/mow/uninst
printf "rm -v /opt/mow/run\n" >> /opt/mow/uninst printf "rm -v /opt/mow/run\n" >> /opt/mow/uninst
printf "rm -v /opt/mow/uninst\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 -v /usr/bin/mow\n" >> /opt/mow/uninst
printf "rm -rv /opt/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 printf "deluser mow\n" >> /opt/mow/uninst
useradd -r mow useradd -r mow
usermod -aG video mow
chown -R mow:mow /var/opt/mow chown -R mow:mow /var/footage
chown -R mow:mow /var/buffer
chmod -v +x /opt/mow/run chmod -v +x /opt/mow/run
chmod -v +x /opt/mow/bin chmod -v +x /opt/mow/bin
chmod -v +x /opt/mow/uninst chmod -v +x /opt/mow/uninst
ln -sv /opt/mow/run /usr/bin/mow ln -sv /opt/mow/run /usr/bin/mow
sh imgmagick_build.sh

View File

@ -2,6 +2,25 @@
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
cp ./bin/magick /usr/bin/magick if [ $? -eq 0 ]
chmod +x /usr/bin/magick then
apt install -y ffmpeg
apt install -y libavcodec-dev
apt install -y libavformat-dev
apt install -y libavutil-dev
apt install -y libswscale-dev
apt install -y x264
apt install -y libx264-dev
apt install -y libilmbase-dev
apt install -y qt6-base-dev
apt install -y qtchooser
apt install -y qmake6
apt install -y qt6-base-dev-tools
apt install -y libxkbcommon-dev
apt install -y libfuse-dev
apt install -y fuse3
sh imgmagick_build.sh
fi

View File

@ -14,84 +14,39 @@
Camera::Camera(QObject *parent) : QObject(parent) {} Camera::Camera(QObject *parent) : QObject(parent) {}
void Camera::cleanup()
{
QProcess::execute("rm", {shared.outDir + "/live"});
QProcess::execute("rm", {shared.outDir + "/logs"});
QProcess::execute("rm", {shared.outDir + "/img"});
QProcess::execute("rm", {shared.outDir + "/index.html"});
QProcess::execute("rm", {shared.outDir + "/stream.m3u8"});
QProcess::execute("rm", {shared.tmpDir + "/events"});
}
int Camera::start(const QStringList &args) int Camera::start(const QStringList &args)
{ {
if (rdConf(getParam("-c", args), &shared)) if (rdConf(getParam("-c", args), &shared))
{ {
QDir().mkpath(shared.outDir); auto thr1 = new QThread(nullptr);
QDir().mkpath(shared.tmpDir); auto thr2 = new QThread(nullptr);
QDir().mkpath(shared.outDir + "/events"); new EventLoop(&shared, thr1, nullptr);
QDir().mkpath(shared.tmpDir + "/live"); new DetectLoop(&shared, thr2, nullptr);
QDir().mkpath(shared.tmpDir + "/logs");
QDir().mkpath(shared.tmpDir + "/img");
cleanup(); thr1->start();
thr2->start();
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 thr2 = new QThread(nullptr);
auto thr3 = new QThread(nullptr);
auto thr4 = new QThread(nullptr);
new RecLoop(&shared, thr1, nullptr);
new Upkeep(&shared, thr2, nullptr);
new EventLoop(&shared, thr3, nullptr);
new DetectLoop(&shared, thr4, nullptr);
thr1->start();
thr2->start();
thr3->start();
thr4->start();
}
} }
return shared.retCode; return shared.retCode;
} }
Loop::Loop(shared_t *sharedRes, QThread *thr, QObject *parent) : QObject(parent) EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QObject(parent)
{ {
shared = sharedRes; shared = sharedRes;
heartBeat = 10; heartBeat = 2;
loopTimer = 0; loopTimer = 0;
connect(thr, &QThread::started, this, &Loop::init); connect(thr, &QThread::started, this, &EventLoop::init);
moveToThread(thr); moveToThread(thr);
} }
void Loop::init() void EventLoop::init()
{ {
loopTimer = new QTimer(this); loopTimer = new QTimer(this);
connect(loopTimer, &QTimer::timeout, this, &Loop::loopSlot); connect(loopTimer, &QTimer::timeout, this, &EventLoop::loopSlot);
loopTimer->setSingleShot(false); loopTimer->setSingleShot(false);
loopTimer->start(heartBeat * 1000); loopTimer->start(heartBeat * 1000);
@ -99,7 +54,7 @@ void Loop::init()
loopSlot(); loopSlot();
} }
void Loop::loopSlot() void EventLoop::loopSlot()
{ {
if (!exec()) if (!exec())
{ {
@ -107,259 +62,40 @@ void Loop::loopSlot()
} }
} }
bool Loop::exec() bool EventLoop::wrOutVod(const evt_t &event)
{ {
if (loopTimer->interval() != heartBeat * 1000)
{
loopTimer->start(heartBeat * 1000);
}
return shared->retCode == 0;
}
RecLoop::RecLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
{
recProc = 0;
imgProc = 0;
}
void RecLoop::init()
{
recProc = new QProcess(this);
imgProc = new QProcess(this);
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &RecLoop::term);
connect(recProc, &QProcess::readyReadStandardError, this, &RecLoop::rdProcErr);
connect(imgProc, &QProcess::readyReadStandardError, this, &RecLoop::rdProcErr);
Loop::init();
}
void RecLoop::updateCmd()
{
QStringList recArgs;
QStringList imgArgs;
recArgs << "-hide_banner";
recArgs << "-i" << shared->recordUrl;
recArgs << "-strftime" << "1";
recArgs << "-strftime_mkdir" << "1";
recArgs << "-hls_segment_filename" << "live/" + QString(STRFTIME_FMT) + ".ts";
recArgs << "-y";
recArgs << "-vcodec" << "copy";
recArgs << "-f" << "hls";
recArgs << "-hls_time" << "2";
recArgs << "-hls_list_size" << "1000";
recArgs << "-hls_flags" << "append_list+omit_endlist";
recArgs << "-rtsp_transport" << "tcp";
recArgs << "-t" << QString::number(heartBeat);
recArgs << "stream.m3u8";
imgArgs << "-hide_banner";
imgArgs << "-i" << shared->recordUrl;
imgArgs << "-strftime" << "1";
imgArgs << "-strftime_mkdir" << "1";
imgArgs << "-vf" << "fps=1,scale=320:240";
imgArgs << "-rtsp_transport" << "tcp";
imgArgs << "-t" << QString::number(heartBeat);
imgArgs << "img/" + QString(STRFTIME_FMT) + ".bmp";
recProc->setProgram("ffmpeg");
recProc->setArguments(recArgs);
imgProc->setProgram("ffmpeg");
imgProc->setArguments(imgArgs);
recLog("rec_args: " + recArgs.join(" "), shared);
recLog("img_args: " + imgArgs.join(" "), shared);
}
void RecLoop::rdProcErr()
{
procError("img", imgProc);
procError("rec", recProc);
}
void RecLoop::term()
{
recProc->kill();
recProc->waitForFinished();
imgProc->kill();
imgProc->waitForFinished();
}
void RecLoop::procError(const QString &desc, QProcess *proc)
{
if (proc->isOpen() && (proc->state() != QProcess::Running))
{
auto errBlob = QString(proc->readAllStandardError());
auto errLines = errBlob.split('\n');
if (!errLines.isEmpty())
{
for (auto &&line : errLines)
{
recLog(desc + "_cmd_stderr: " + line, shared);
}
}
}
}
bool RecLoop::exec()
{
if ((imgProc->state() == QProcess::Running) || (recProc->state() == QProcess::Running))
{
term();
}
updateCmd();
imgProc->start();
recProc->start();
return Loop::exec();
}
Upkeep::Upkeep(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) {}
bool Upkeep::exec()
{
enforceMaxLogSize(QString("logs/") + REC_LOG_NAME, shared);
enforceMaxLogSize(QString("logs/") + DET_LOG_NAME, shared);
dumpLogs(QString("logs/") + REC_LOG_NAME, shared->recLog);
dumpLogs(QString("logs/") + DET_LOG_NAME, shared->detLog);
shared->logMutex.lock();
shared->recLog.clear();
shared->detLog.clear();
shared->logMutex.unlock();
initLogFrontPages();
enforceMaxEvents(shared);
enforceMaxImages();
enforceMaxVids();
genFrontPage(shared);
genCSS(shared);
genCamPage(shared);
return Loop::exec();
}
EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
{
heartBeat = 2;
highScore = 0;
cycles = 0;
}
bool EventLoop::exec()
{
if (!vidList.isEmpty())
{
vidList.removeDuplicates();
if (vidList.size() > 1)
{
recLog("attempting write out of event: " + name, shared);
if (wrOutVod())
{
genHTMLvod(name);
QProcess proc;
QStringList args;
args << "convert";
args << imgPath;
args << "events/" + name + ".jpg";
proc.start("magick", args);
proc.waitForFinished();
}
}
cycles = 0;
highScore = 0;
vidList.clear();
}
shared->recMutex.lock();
QList<int> rmIndx;
for (auto i = 0; i < shared->recList.size(); ++i)
{
auto event = &shared->recList[i];
if (highScore < event->score)
{
name = event->timeStamp.toString(DATETIME_FMT);
imgPath = event->imgPath;
highScore = event->score;
}
if (event->queAge >= (shared->evMaxSecs / heartBeat))
{
auto maxSecs = shared->evMaxSecs / 2;
// half the maxsecs value to get front-back half secs
auto backFiles = backwardFacingFiles("live", ".ts", event->timeStamp, maxSecs);
auto frontFiles = forwardFacingFiles("live", ".ts", event->timeStamp, maxSecs);
vidList.append(backFiles + frontFiles);
rmIndx.append(i);
}
else
{
event->queAge += heartBeat;
}
}
for (auto i : rmIndx)
{
shared->recList.removeAt(i);
}
shared->recMutex.unlock();
return Loop::exec();
}
bool EventLoop::wrOutVod()
{
auto cnt = 0;
auto concat = name + ".tmp";
auto ret = false; auto ret = false;
auto cnt = 0;
auto concat = shared->buffPath + "/live/" + event.timeStamp + ".ctmp";
QFile file(concat); QFile file(concat, this);
file.open(QFile::WriteOnly); file.open(QFile::WriteOnly);
for (auto &&vid : vidList) for (auto &&vid : event.vidList)
{ {
recLog("event_src: " + vid, shared); QTextStream(stdout) << "event_src: " << vid << Qt::endl;
if (QFile::exists(vid)) if (QFile::exists(vid))
{ {
file.write(QString("file '" + vid + "'\n").toUtf8()); cnt++; file.write(QString("file '" + vid + "'\n").toUtf8()); cnt++;
} }
else
{
QTextStream(stdout) << "warning: the event hls clip does not exists." << Qt::endl;
}
} }
file.close(); file.close();
if (cnt == 0) if (cnt == 0)
{ {
recLog("err: none of the event hls clips exists, canceling write out.", shared); QTextStream(stderr) << "err: none of the event hls clips exists, cancelling write out." << Qt::endl;
QFile::remove(concat); QFile::remove(concat);
} }
else else
{ {
QProcess proc;
QStringList args; QStringList args;
args << "-f"; args << "-f";
@ -367,22 +103,11 @@ bool EventLoop::wrOutVod()
args << "-safe" << "0"; args << "-safe" << "0";
args << "-i" << concat; args << "-i" << concat;
args << "-c" << "copy"; args << "-c" << "copy";
args << "events/" + name + ".mp4"; args << shared->recPath + "/" + event.timeStamp + shared->recExt;
proc.setProgram("ffmpeg"); if (QProcess::execute("ffmpeg", args) == 0)
proc.setArguments(args);
proc.start();
if (proc.waitForStarted())
{ {
recLog("concat_cmd_start: ok", shared); ret = true;
proc.waitForFinished(); ret = true;
}
else
{
recLog("concat_cmd_start: fail", shared);
recLog("concat_cmd_stderr: " + QString(proc.readAllStandardError()), shared);
} }
QFile::remove(concat); QFile::remove(concat);
@ -391,54 +116,108 @@ bool EventLoop::wrOutVod()
return ret; return ret;
} }
DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) bool EventLoop::exec()
{ {
pcTimer = 0; enforceMaxEvents(shared);
heartBeat = 2; enforceMaxImages(shared);
delayCycles = 12; // this will be used to delay the enforceMaxClips(shared);
// actual start of DetectLoop by
// 24secs. if (!shared->recList.isEmpty())
{
auto event = shared->recList.takeFirst();
QTextStream(stdout) << "attempting write out of event: " << event.timeStamp << Qt::endl;
if (wrOutVod(event))
{
QStringList args;
args << "convert";
args << event.imgPath;
args << shared->recPath + "/" + event.timeStamp + shared->thumbExt;
QProcess::execute("magick", args);
}
}
return shared->retCode == 0;
}
DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QFileSystemWatcher(parent)
{
pcTimer = 0;
shared = sharedRes;
connect(thr, &QThread::started, this, &DetectLoop::init);
moveToThread(thr);
} }
void DetectLoop::init() void DetectLoop::init()
{ {
pcTimer = new QTimer(this); pcTimer = new QTimer(this);
mod = false;
connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak); connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak);
connect(this, &QFileSystemWatcher::directoryChanged, this, &DetectLoop::updated);
resetTimers(); addPath(shared->buffPath + "/live");
Loop::init(); pcTimer->start(shared->postSecs * 1000);
} }
void DetectLoop::resetTimers() void DetectLoop::reset()
{ {
pcTimer->start(shared->postSecs * 1000); eventQue.inQue = false;
eventQue.score = 0;
eventQue.queAge = 0;
eventQue.imgPath.clear();
eventQue.vidList.clear();
eventQue.timeStamp.clear();
}
void DetectLoop::updated(const QString &path)
{
auto clips = lsFilesInDir(path, shared->streamExt);
auto index = clips.indexOf(vidBName);
if (clips.size() - (index + 1) < 3)
{
thread()->sleep(1);
}
else
{
vidAName = clips[clips.size() - 3];
vidBName = clips[clips.size() - 2];
vidAPath = shared->buffPath + "/live/" + vidAName;
vidBPath = shared->buffPath + "/live/" + vidBName;
exec(); thread()->sleep(1);
}
} }
void DetectLoop::pcBreak() void DetectLoop::pcBreak()
{ {
prevClips.clear();
if (!shared->postCmd.isEmpty()) if (!shared->postCmd.isEmpty())
{ {
detLog("---POST_BREAK---", shared); QTextStream(stdout) << "---POST_BREAK---" << Qt::endl;
if (mod) if (eventQue.inQue)
{ {
detLog("motion detected, skipping the post command.", shared); QTextStream(stdout) << "motion detected, skipping the post command." << Qt::endl;
} }
else else
{ {
if (delayCycles == 0) delayCycles = 5; QTextStream(stdout) << "no motion detected, running post command: " << shared->postCmd << Qt::endl;
else delayCycles += 5;
detLog("no motion detected, running post command: " + shared->postCmd, shared);
auto args = parseArgs(shared->postCmd.toUtf8(), -1); auto args = parseArgs(shared->postCmd.toUtf8(), -1);
if (args.isEmpty()) if (args.isEmpty())
{ {
detLog("err: did not parse an executable from the post command line.", shared); QTextStream(stderr) << "err: did not parse an executable from the post command line." << Qt::endl;
} }
else else
{ {
@ -446,8 +225,6 @@ void DetectLoop::pcBreak()
} }
} }
} }
mod = false;
} }
float DetectLoop::getFloatFromExe(const QByteArray &line) float DetectLoop::getFloatFromExe(const QByteArray &line)
@ -467,7 +244,16 @@ float DetectLoop::getFloatFromExe(const QByteArray &line)
} }
} }
return strNum.toFloat(); auto ok = false;
auto res = strNum.toFloat(&ok);
if (!ok || strNum.isEmpty())
{
QTextStream(stderr) << "err: the image comp command returned unexpected output and couldn't be converted to float." << Qt::endl;
QTextStream(stderr) << " raw output: " << line << Qt::endl;
}
return res;
} }
QStringList DetectLoop::buildArgs(const QString &prev, const QString &next) QStringList DetectLoop::buildArgs(const QString &prev, const QString &next)
@ -483,83 +269,98 @@ QStringList DetectLoop::buildArgs(const QString &prev, const QString &next)
return args; return args;
} }
bool DetectLoop::exec() QStringList DetectLoop::buildSnapArgs(const QString &vidSrc, const QString &imgPath)
{ {
if (delayCycles > 0) QStringList ret;
{
delayCycles -= 1;
detLog("delay: detection cycle skipped. cycles left to be skipped: " + QString::number(delayCycles), shared); ret.append("-hide_banner");
ret.append("-loglevel");
ret.append("panic");
ret.append("-y");
ret.append("-i");
ret.append(vidSrc);
ret.append("-frames:v");
ret.append("1");
ret.append(imgPath);
return ret;
}
void DetectLoop::exec()
{
auto imgAPath = shared->buffPath + "/img/" + QFileInfo(vidAPath).baseName() + ".bmp";
auto imgBPath = shared->buffPath + "/img/" + QFileInfo(vidBPath).baseName() + ".bmp";
auto snapArgsA = buildSnapArgs(vidAPath, imgAPath);
auto snapArgsB = buildSnapArgs(vidBPath, imgBPath);
auto compArgs = buildArgs(imgAPath, imgBPath);
if (compArgs.isEmpty())
{
QTextStream(stderr) << "err: could not parse a executable name from img_comp_cmd: " << shared->compCmd << Qt::endl;
} }
else else
{ {
auto curDT = QDateTime::currentDateTime(); QProcess::execute("ffmpeg", snapArgsA);
auto images = backwardFacingFiles("img", ".bmp", curDT, 6); QProcess::execute("ffmpeg", snapArgsB);
if (images.size() < 2) if (QFile::exists(imgAPath) && QFile::exists(imgBPath))
{ {
detLog("wrn: didn't pick up enough image files from the image stream. number of files: " + QString::number(images.size()), shared); QProcess extComp;
detLog(" will try again on the next loop.", shared);
}
else
{
auto pos = images.size() - 1;
auto args = buildArgs(images[pos - 1], images[pos]);
if (args.isEmpty()) extComp.start(compArgs[0], compArgs.mid(1));
extComp.waitForFinished();
float score = 0;
if (shared->outputType == "stdout")
{ {
detLog("err: could not parse a executable name from img_comp_cmd: " + shared->compCmd, shared); score = getFloatFromExe(extComp.readAllStandardOutput());
} }
else else
{ {
QProcess extComp; score = getFloatFromExe(extComp.readAllStandardError());
}
extComp.start(args[0], args.mid(1)); QTextStream(stdout) << compArgs.join(" ") << " --result: " << QString::number(score) << Qt::endl;
extComp.waitForFinished();
float score = 0; if (eventQue.inQue)
auto ok = true; {
eventQue.queAge += 4;
if (shared->outputType == "stdout") if (eventQue.score < score)
{ {
score = getFloatFromExe(extComp.readAllStandardOutput()); eventQue.score = score;
} eventQue.imgPath = imgBPath;
else if (shared->outputType == "stderr")
{
score = getFloatFromExe(extComp.readAllStandardError());
}
else
{
ok = false;
} }
if (!ok) eventQue.vidList.append(vidAPath);
eventQue.vidList.append(vidBPath);
if (eventQue.queAge >= shared->evMaxSecs)
{ {
detLog("err: img_comp_out: " + shared->outputType + " is not valid. it must be 'stdout' or 'stderr'" , shared); eventQue.inQue = false;
shared->recList.append(eventQue);
reset();
} }
else }
{ else if (score >= shared->imgThresh)
detLog(args.join(" ") + " --result: " + QString::number(score), shared); {
QTextStream(stdout) << "--threshold_meet: " << QString::number(shared->imgThresh) << Qt::endl;
if (score >= shared->imgThresh) eventQue.score = score;
{ eventQue.imgPath = imgBPath;
detLog("--threshold_breached: " + QString::number(shared->imgThresh), shared); eventQue.inQue = true;
eventQue.queAge = 0;
eventQue.timeStamp = QDateTime::currentDateTime().toString(DATETIME_FMT);
evt_t event; eventQue.vidList.append(vidAPath);
eventQue.vidList.append(vidBPath);
event.timeStamp = curDT;
event.score = score;
event.imgPath = images[pos];
event.queAge = 0;
shared->recMutex.lock();
shared->recList.append(event); mod = true;
shared->recMutex.unlock();
}
}
} }
} }
} }
return Loop::exec(); vidAPath.clear();
vidBPath.clear();
} }

View File

@ -14,8 +14,6 @@
// GNU General Public License for more details. // GNU General Public License for more details.
#include "common.h" #include "common.h"
#include "logger.h"
#include "web.h"
class Camera : public QObject class Camera : public QObject
{ {
@ -25,8 +23,6 @@ private:
shared_t shared; shared_t shared;
void cleanup();
public: public:
explicit Camera(QObject *parent = nullptr); explicit Camera(QObject *parent = nullptr);
@ -34,81 +30,22 @@ public:
int start(const QStringList &args); int start(const QStringList &args);
}; };
class Loop : public QObject class EventLoop : public QObject
{ {
Q_OBJECT Q_OBJECT
protected: private slots:
void init();
void loopSlot();
private:
shared_t *shared; shared_t *shared;
QTimer *loopTimer; QTimer *loopTimer;
int heartBeat; int heartBeat;
protected slots: bool wrOutVod(const evt_t &event);
virtual void init();
private slots:
void loopSlot();
public:
explicit Loop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
virtual bool exec();
};
class RecLoop : public Loop
{
Q_OBJECT
private:
QProcess *recProc;
QProcess *imgProc;
QString curUrl;
void updateCmd();
void procError(const QString &desc, QProcess *proc);
private slots:
void init();
void term();
void rdProcErr();
public:
explicit RecLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
bool exec();
};
class Upkeep : public Loop
{
Q_OBJECT
public:
explicit Upkeep(shared_t *shared, QThread *thr, QObject *parent = nullptr);
bool exec();
};
class EventLoop : public Loop
{
Q_OBJECT
private:
QStringList vidList;
QString imgPath;
QString name;
float highScore;
uint cycles;
bool wrOutVod();
public: public:
@ -117,30 +54,37 @@ public:
bool exec(); bool exec();
}; };
class DetectLoop : public Loop class DetectLoop : public QFileSystemWatcher
{ {
Q_OBJECT Q_OBJECT
private: private:
QTimer *pcTimer; QString vidAPath;
uint delayCycles; QString vidBPath;
bool mod; QString vidAName;
QString vidBName;
QStringList prevClips;
QTimer *pcTimer;
evt_t eventQue;
shared_t *shared;
void resetTimers();
float getFloatFromExe(const QByteArray &line); float getFloatFromExe(const QByteArray &line);
QStringList buildArgs(const QString &prev, const QString &next); QStringList buildArgs(const QString &prev, const QString &next);
QStringList buildSnapArgs(const QString &vidSrc, const QString &imgPath);
private slots: private slots:
void init(); void init();
void reset();
void pcBreak(); void pcBreak();
void updated(const QString &path);
public: public:
explicit DetectLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr); explicit DetectLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
bool exec(); void exec();
}; };
#endif // CAMERA_H #endif // CAMERA_H

View File

@ -107,45 +107,43 @@ QStringList forwardFacingFiles(const QString &path, const QString &ext, const QD
void enforceMaxEvents(shared_t *share) void enforceMaxEvents(shared_t *share)
{ {
auto names = lsFilesInDir("events", ".mp4"); auto names = lsFilesInDir(share->recPath, share->recExt);
while (names.size() > share->maxEvents) while (names.size() > share->maxEvents)
{ {
auto nameOnly = "events/" + names[0]; auto nameOnly = share->recPath + "/" + names[0];
nameOnly.remove(".mp4"); nameOnly.chop(share->recExt.size());
auto mp4File = nameOnly + ".mp4"; auto vidFile = nameOnly + share->recExt;
auto imgFile = nameOnly + ".jpg"; auto imgFile = nameOnly + share->thumbExt;
auto webFile = nameOnly + ".html";
QFile::remove(mp4File); QFile::remove(vidFile);
QFile::remove(imgFile); QFile::remove(imgFile);
QFile::remove(webFile);
names.removeFirst(); names.removeFirst();
} }
} }
void enforceMaxImages() void enforceMaxImages(shared_t *share)
{ {
auto names = lsFilesInDir("img", ".bmp"); auto names = lsFilesInDir(share->buffPath + "/img", ".bmp");
while (names.size() > MAX_IMAGES) while (names.size() > (share->liveSecs / 2))
{ {
QFile::remove("img/" + names[0]); QFile::remove(share->buffPath + "/img/" + names[0]);
names.removeFirst(); names.removeFirst();
} }
} }
void enforceMaxVids() void enforceMaxClips(shared_t *share)
{ {
auto names = lsFilesInDir("live", ".ts"); auto names = lsFilesInDir(share->buffPath + "/live", share->streamExt);
while (names.size() > MAX_VIDEOS) while (names.size() > (share->liveSecs / 2))
{ {
QFile::remove("live/" + names[0]); QFile::remove(share->buffPath + "/live/" + names[0]);
names.removeFirst(); names.removeFirst();
} }
@ -155,7 +153,7 @@ void rdLine(const QString &param, const QString &line, QString *value)
{ {
if (line.startsWith(param)) if (line.startsWith(param))
{ {
*value = line.mid(param.size()); *value = line.mid(param.size()).trimmed();
} }
} }
@ -163,25 +161,40 @@ void rdLine(const QString &param, const QString &line, int *value)
{ {
if (line.startsWith(param)) if (line.startsWith(param))
{ {
*value = line.mid(param.size()).toInt(); *value = line.mid(param.size()).trimmed().toInt();
} }
} }
void touch(const QString &path) void rdLine(const QString &param, const QString &line, bool *value)
{ {
if (!QFile::exists(path)) if (line.startsWith(param))
{ {
QFile file(path); auto val = line.mid(param.size()).trimmed();
if (file.open(QFile::WriteOnly)) *value = (val == "y" || val == "Y");
{
file.write("");
}
file.close();
} }
} }
void extCorrection(QString &ext)
{
if (!ext.startsWith("."))
{
ext = "." + ext;
}
}
bool mkPath(const QString &path)
{
auto ret = true;
if (!QDir().exists(path))
{
ret = QDir().mkpath(path);
}
return ret;
}
bool rdConf(const QString &filePath, shared_t *share) bool rdConf(const QString &filePath, shared_t *share)
{ {
QFile varFile(filePath); QFile varFile(filePath);
@ -190,29 +203,37 @@ bool rdConf(const QString &filePath, shared_t *share)
{ {
share->retCode = ENOENT; share->retCode = ENOENT;
QTextStream(stderr) << "err: config file: " << filePath << " does not exists or lack read permissions." << Qt::endl; QTextStream(stderr) << "err: config file - " << filePath << " does not exists or lack read permissions." << Qt::endl;
} }
else else
{ {
share->recordUrl.clear(); share->recordUri.clear();
share->postCmd.clear(); share->postCmd.clear();
share->camName.clear(); share->camName.clear();
share->buffPath.clear();
share->recPath.clear();
share->servGroup.clear();
share->retCode = 0; auto thrCount = QThread::idealThreadCount() / 2;
share->imgThresh = 8000;
share->maxEvents = 100; share->retCode = 0;
share->maxLogSize = 100000; share->imgThresh = 8000;
share->skipCmd = false; share->maxEvents = 30;
share->postSecs = 60; share->skipCmd = false;
share->evMaxSecs = 30; share->postSecs = 60;
share->conf = filePath; share->evMaxSecs = 30;
share->buffPath = "/var/opt/" + QString(APP_BIN) + "/buf"; share->conf = filePath;
share->webRoot = "/var/opt/" + QString(APP_BIN) + "/web"; share->outputType = "stderr";
share->webBg = "#485564"; share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null";
share->webTxt = "#dee5ee"; share->streamCodec = "copy";
share->webFont = "courier"; share->streamExt = ".avi";
share->outputType = "stderr"; share->recExt = ".avi";
share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null"; share->thumbExt = ".jpg";
share->recFps = 30;
share->liveSecs = 80;
share->recScale = "1280:720";
share->imgScale = "320:240";
share->servUser = APP_BIN;
QString line; QString line;
@ -222,144 +243,92 @@ bool rdConf(const QString &filePath, shared_t *share)
if (!line.startsWith("#")) if (!line.startsWith("#"))
{ {
rdLine("cam_name = ", line, &share->camName); rdLine("cam_name = ", line, &share->camName);
rdLine("recording_stream = ", line, &share->recordUrl); rdLine("recording_uri = ", line, &share->recordUri);
rdLine("buffer_path = ", line, &share->buffPath); rdLine("buffer_path = ", line, &share->buffPath);
rdLine("web_root = ", line, &share->webRoot); rdLine("rec_path = ", line, &share->recPath);
rdLine("web_text = ", line, &share->webTxt); rdLine("max_event_secs = ", line, &share->evMaxSecs);
rdLine("web_bg = ", line, &share->webBg); rdLine("post_secs = ", line, &share->postSecs);
rdLine("web_font = ", line, &share->webFont); rdLine("post_cmd = ", line, &share->postCmd);
rdLine("max_event_secs = ", line, &share->evMaxSecs); rdLine("img_thresh = ", line, &share->imgThresh);
rdLine("post_secs = ", line, &share->postSecs); rdLine("max_events = ", line, &share->maxEvents);
rdLine("post_cmd = ", line, &share->postCmd); rdLine("img_comp_out = ", line, &share->outputType);
rdLine("img_thresh = ", line, &share->imgThresh); rdLine("img_comp_cmd = ", line, &share->compCmd);
rdLine("max_events = ", line, &share->maxEvents); rdLine("stream_codec = ", line, &share->streamCodec);
rdLine("max_log_size = ", line, &share->maxLogSize); rdLine("stream_ext = ", line, &share->streamExt);
rdLine("img_comp_out = ", line, &share->outputType); rdLine("rec_ext = ", line, &share->recExt);
rdLine("img_comp_cmd = ", line, &share->compCmd); rdLine("thumbnail_ext = ", line, &share->thumbExt);
rdLine("rec_fps = ", line, &share->recFps);
rdLine("rec_scale = ", line, &share->recScale);
rdLine("img_scale = ", line, &share->imgScale);
rdLine("service_user = ", line, &share->servUser);
rdLine("service_group = ", line, &share->servGroup);
rdLine("live_secs = ", line, &share->liveSecs);
} }
} while(!line.isEmpty()); } while(!line.isEmpty());
if (share->camName.isEmpty()) if (share->camName.isEmpty())
{ {
share->camName = QFileInfo(share->conf).fileName(); share->camName = QFileInfo(share->conf).baseName();
} }
share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName; extCorrection(share->streamExt);
share->tmpDir = share->buffPath + "/" + share->camName; extCorrection(share->recExt);
share->servPath = QString("/var/opt/") + APP_BIN + "/" + APP_BIN + "." + share->camName + ".service"; extCorrection(share->thumbExt);
if (share->outputType != "stdout" && share->outputType != "stderr")
{
share->outputType = "stderr";
}
if (share->buffPath.isEmpty())
{
share->buffPath = "/var/buffer/" + share->camName;
}
else
{
share->buffPath = QDir::cleanPath(share->buffPath);
}
if (share->recPath.isEmpty())
{
share->recPath = "/var/footage/" + share->camName;
}
else
{
share->recPath = QDir::cleanPath(share->recPath);
}
if (share->liveSecs < 10)
{
share->liveSecs = 10;
}
if ((share->liveSecs - 4) < share->evMaxSecs)
{
share->evMaxSecs = share->liveSecs - 4;
}
if (share->servGroup.isEmpty())
{
share->servGroup = share->servUser;
}
share->servMainLoop = QString(APP_BIN) + ".main_loop." + share->camName;
share->servVidLoop = QString(APP_BIN) + ".vid_loop." + share->camName;
} }
return share->retCode == 0; return share->retCode == 0;
} }
void rmServices() QString buildThreadCount(int count)
{ {
auto files = lsFilesInDir(QString("/var/opt/") + APP_BIN, ".service"); QString ret = "0";
for (auto &&serv : files) for (auto i = 1; i < count; ++i)
{ {
QProcess::execute("systemctl", {"stop", serv}); ret.append(","); ret.append(QString::number(i));
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)
{
auto ret = ENOENT;
auto path = QDir().cleanPath(getParam("-d", args));
auto files = lsFilesInDir(path);
if (!QDir(path).exists())
{
QTextStream(stderr) << "err: the supplied directory in -d '" << path << "' does not exists or is not a directory." << Qt::endl;
}
else if (files.isEmpty())
{
QTextStream(stderr) << "err: no config files found in '" << path << "'" << Qt::endl;
}
else
{
ret = 0;
QTextStream(stdout) << "loading conf files from dir: " << path << Qt::endl;
for (auto &&conf : files)
{
shared_t shared;
if (!rdConf(path + "/" + conf, &shared))
{
ret = shared.retCode; break;
}
else
{
QTextStream(stdout) << conf << " --" << Qt::endl;
QFile file(shared.servPath);
if (!file.open(QFile::ReadWrite | QFile::Truncate))
{
QTextStream(stderr) << "err: failed to open service file: " << shared.servPath << " for writing. reason: " << file.errorString();
ret = EACCES; file.close(); break;
}
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();
if (!QFile::link(shared.servPath, "/lib/systemd/system/" + servName))
{
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;

View File

@ -17,71 +17,77 @@
#include <QProcess> #include <QProcess>
#include <QTextStream> #include <QTextStream>
#include <QObject> #include <QObject>
#include <QRegularExpression>
#include <QDir> #include <QDir>
#include <QCryptographicHash>
#include <QFile> #include <QFile>
#include <QDateTime> #include <QDateTime>
#include <QThread> #include <QThread>
#include <QTimer> #include <QTimer>
#include <QStringList> #include <QStringList>
#include <QMutex> #include <QMutex>
#include <QRegularExpression>
#include <QFileSystemWatcher>
#include <iostream> #include <iostream>
using namespace std; using namespace std;
#define APP_VER "3.2" #define APP_VER "3.3"
#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 DET_LOG_NAME "det_log_lines.html"
#define UPK_LOG_NAME "upk_log_lines.html"
#define DATETIME_FMT "yyyyMMddhhmmss" #define DATETIME_FMT "yyyyMMddhhmmss"
#define STRFTIME_FMT "%Y%m%d%H%M%S" #define STRFTIME_FMT "%Y%m%d%H%M%S"
#define MAX_IMAGES 1000
#define MAX_VIDEOS 1000
#define PREV_IMG "&prev&" #define PREV_IMG "&prev&"
#define NEXT_IMG "&next&" #define NEXT_IMG "&next&"
enum CmdExeType
{
MAIN_LOOP,
VID_LOOP
};
struct evt_t struct evt_t
{ {
QDateTime timeStamp; QList<QString> vidList;
QString imgPath; QString timeStamp;
float score; QString imgPath;
uint queAge; float score;
bool inQue;
int queAge;
}; };
struct shared_t struct shared_t
{ {
QList<evt_t> recList; QList<evt_t> recList;
QMutex recMutex;
QMutex logMutex;
QString conf; QString conf;
QString recLog; QString recordUri;
QString detLog;
QString recordUrl;
QString outDir;
QString tmpDir;
QString buffPath; QString buffPath;
QString postCmd; QString postCmd;
QString camName; QString camName;
QString webBg; QString recPath;
QString webTxt;
QString webFont;
QString webRoot;
QString servPath;
QString outputType; QString outputType;
QString compCmd; QString compCmd;
QString streamCodec;
QString streamExt;
QString recExt;
QString thumbExt;
QString recScale;
QString imgScale;
QString servMainLoop;
QString servVidLoop;
QString servUser;
QString servGroup;
bool singleTenant;
bool skipCmd; bool skipCmd;
int liveSecs;
int recFps;
int evMaxSecs; int evMaxSecs;
int postSecs; int postSecs;
int imgThresh; int imgThresh;
int maxEvents; int maxEvents;
int maxLogSize;
int retCode; int retCode;
}; };
QString getParam(const QString &key, const QStringList &args); QString getParam(const QString &key, const QStringList &args);
QString buildThreadCount(int count);
QStringList lsFilesInDir(const QString &path, const QString &ext = QString()); QStringList lsFilesInDir(const QString &path, const QString &ext = QString());
QStringList lsDirsInDir(const QString &path); 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);
@ -89,14 +95,13 @@ QStringList backwardFacingFiles(const QString &path, const QString &ext, const Q
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); 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 mkPath(const QString &path);
void listServices();
void rmServices();
void touch(const QString &path);
void rdLine(const QString &param, const QString &line, QString *value); void rdLine(const QString &param, const QString &line, QString *value);
void rdLine(const QString &param, const QString &line, int *value); void rdLine(const QString &param, const QString &line, int *value);
void rdLine(const QString &param, const QString &line, bool *value);
void enforceMaxEvents(shared_t *share); void enforceMaxEvents(shared_t *share);
void enforceMaxImages(); void enforceMaxImages(shared_t *share);
void enforceMaxVids(); void enforceMaxClips(shared_t *share);
void extCorrection(QString &ext);
#endif // COMMON_H #endif // COMMON_H

View File

@ -1,125 +0,0 @@
// This file is part of Motion Watch.
// Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "logger.h"
void recLog(const QString &line, shared_t *share)
{
share->logMutex.lock();
share->recLog += QDateTime::currentDateTime().toString("[yyyy-MM-dd-hh-mm-ss] ") + line + "<br>\n";
share->logMutex.unlock();
}
void detLog(const QString &line, shared_t *share)
{
share->logMutex.lock();
share->detLog += QDateTime::currentDateTime().toString("[yyyy-MM-dd-hh-mm-ss] ") + line + "<br>\n";
share->logMutex.unlock();
}
void enforceMaxLogSize(const QString &filePath, shared_t *share)
{
QFile file(filePath);
if (file.exists())
{
if (file.size() >= share->maxLogSize)
{
file.remove();
}
}
}
void dumpLogs(const QString &fileName, const QString &lines)
{
if (!lines.isEmpty())
{
QFile outFile(fileName);
if (outFile.exists())
{
outFile.open(QFile::Append);
}
else
{
outFile.open(QFile::WriteOnly);
}
outFile.write(lines.toUtf8());
outFile.close();
}
}
void initLogFrontPage(const QString &filePath, const QString &logLinesFile)
{
if (!QFile::exists(filePath))
{
QString htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n";
htmlText += "<script>\n";
htmlText += "function includeHTML() {\n";
htmlText += " var z, i, elmnt, file, xhttp;\n";
htmlText += " z = document.getElementsByTagName(\"*\");\n";
htmlText += " for (i = 0; i < z.length; i++) {\n";
htmlText += " elmnt = z[i];\n";
htmlText += " file = elmnt.getAttribute(\"include-html\");\n";
htmlText += " if (file) {\n";
htmlText += " xhttp = new XMLHttpRequest();\n";
htmlText += " xhttp.onreadystatechange = function() {\n";
htmlText += " if (this.readyState == 4) {\n";
htmlText += " if (this.status == 200) {elmnt.innerHTML = this.responseText;}\n";
htmlText += " if (this.status == 404) {elmnt.innerHTML = \"Page not found.\";}\n";
htmlText += " elmnt.removeAttribute(\"include-html\");\n";
htmlText += " includeHTML();\n";
htmlText += " }\n";
htmlText += " }\n";
htmlText += " xhttp.open(\"GET\", file, true);\n";
htmlText += " xhttp.send();\n";
htmlText += " return;\n";
htmlText += " }\n";
htmlText += " }\n";
htmlText += "};\n";
htmlText += "</script>\n";
htmlText += "<head>\n";
htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n";
htmlText += "<meta http-equiv=\"Pragma\" content=\"no-cache\" />\n";
htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n";
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
htmlText += "</head>\n";
htmlText += "<body>\n";
htmlText += "<p>\n";
htmlText += "<div include-html='" + logLinesFile + "'></div>\n";
htmlText += "<script>\n";
htmlText += "includeHTML();\n";
htmlText += "</script>\n";
htmlText += "</p>\n";
htmlText += "</body>\n";
htmlText += "</html>\n";
QFile outFile(filePath);
outFile.open(QFile::WriteOnly);
outFile.write(htmlText.toUtf8());
outFile.close();
}
}
void initLogFrontPages()
{
initLogFrontPage("logs/recording_log.html", REC_LOG_NAME);
initLogFrontPage("logs/detection_log.html", DET_LOG_NAME);
}

View File

@ -1,24 +0,0 @@
#ifndef lOGGER_H
#define lOGGER_H
// This file is part of Motion Watch.
// Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "common.h"
void recLog(const QString &line, shared_t *share);
void detLog(const QString &line, shared_t *share);
void dumpLogs(const QString &fileName, const QString &lines);
void enforceMaxLogSize(const QString &filePath, shared_t *share);
void initLogFrontPages();
#endif // lOGGER_H

View File

@ -12,26 +12,24 @@
#include "common.h" #include "common.h"
#include "camera.h" #include "camera.h"
#include "services.h"
void showHelp() void showHelp()
{ {
QTextStream(stdout) << APP_NAME << " " << APP_VER << Qt::endl << Qt::endl; QTextStream(stdout) << APP_NAME << " " << APP_VER << Qt::endl << Qt::endl;
QTextStream(stdout) << "Usage: mow <argument>" << 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) << "-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) << "-c : path to the config file used to run a single main loop instance." << Qt::endl;
QTextStream(stdout) << "-d : path to a directory that can contain multiple config files." << Qt::endl; QTextStream(stdout) << "-i : all valid config files found in /etc/mow will be used to create" << Qt::endl;
QTextStream(stdout) << " each file found in the directory will be used to create a " << Qt::endl; QTextStream(stdout) << " a full instance; full meaning main and vid loop systemd services" << Qt::endl;
QTextStream(stdout) << " systemd service for the camera. already existing camera" << Qt::endl; QTextStream(stdout) << " will be created for each config file." << Qt::endl;
QTextStream(stdout) << " services will be reloaded and restarted. any services without" << Qt::endl; QTextStream(stdout) << "-d : this is the same as -i except it will not auto start the services." << 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) << "-v : display the current version." << Qt::endl;
QTextStream(stdout) << "-u : uninstall the entire app from your system, including all" << Qt::endl; QTextStream(stdout) << "-u : uninstall the entire app from your system, including all" << Qt::endl;
QTextStream(stdout) << " camera services." << Qt::endl; QTextStream(stdout) << " systemd services related to it." << Qt::endl;
QTextStream(stdout) << "-f : force an action without pausing for user confirmation." << 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) << "-l : list all mow services along with statuses." << Qt::endl;
QTextStream(stdout) << "-r : remove all camera services." << Qt::endl; QTextStream(stdout) << "-r : remove all mow services." << Qt::endl;
} }
int main(int argc, char** argv) int main(int argc, char** argv)
@ -52,21 +50,22 @@ int main(int argc, char** argv)
{ {
QTextStream(stdout) << APP_VER << Qt::endl; QTextStream(stdout) << APP_VER << Qt::endl;
} }
else if (args.contains("-d"))
{
rmServices(); ret = loadServices(args);
}
else if (args.contains("-l")) else if (args.contains("-l"))
{ {
listServices(); servStatByDir("/etc/mow");
} }
else if (args.contains("-i")) else if (args.contains("-i") || args.contains("-d"))
{ {
args.clear(); ret = rmServiceByDir("/etc/mow");
args.append("-d");
args.append("/etc/" + QString(APP_BIN));
rmServices(); ret = loadServices(args); if ((ret == 0) && args.contains("-i"))
{
ret = loadServiceByDir("/etc/mow", true);
}
else if (ret == 0)
{
ret = loadServiceByDir("/etc/mow", false);
}
} }
else if (args.contains("-c")) else if (args.contains("-c"))
{ {
@ -83,7 +82,12 @@ int main(int argc, char** argv)
{ {
if (args.contains("-f")) if (args.contains("-f"))
{ {
rmServices(); QProcess::execute("/opt/mow/uninst", QStringList()); ret = rmServiceByDir("/etc/mow");
if (ret == 0)
{
ret = QProcess::execute("/opt/mow/uninst", QStringList());
}
} }
else else
{ {
@ -94,13 +98,18 @@ int main(int argc, char** argv)
if (ans == 'y' || ans == 'Y') if (ans == 'y' || ans == 'Y')
{ {
rmServices(); QProcess::execute("/opt/mow/uninst", QStringList()); ret = rmServiceByDir("/etc/mow");
if (ret == 0)
{
ret = QProcess::execute("/opt/mow/uninst", QStringList());
}
} }
} }
} }
else if (args.contains("-r")) else if (args.contains("-r"))
{ {
rmServices(); ret = rmServiceByDir("/etc/mow");
} }
else else
{ {

254
src/services.cpp Normal file
View File

@ -0,0 +1,254 @@
// 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 "services.h"
int loadService(const QString &desc, const QString &user, const QString &servName, const QString &workDir, const QString &recPath, bool start)
{
QFile file("/lib/systemd/system/" + servName + ".service");
auto ret = 0;
auto exists = file.exists();
if (!file.open(QFile::ReadWrite | QFile::Truncate))
{
QTextStream(stderr) << "err: failed to open service file: " << file.fileName() << " for writing. reason: " << file.errorString() << Qt::endl;
ret = EACCES;
}
else
{
if (exists)
{
if (ret == 0) ret = QProcess::execute("systemctl", {"stop", servName});
if (ret == 0) ret = QProcess::execute("systemctl", {"disable", servName});
}
if (ret == 0)
{
file.write("[Unit]\n");
file.write("Description=" + desc.toUtf8() + "\n");
file.write("After=network.target\n\n");
file.write("[Service]\n");
file.write("WorkingDirectory=" + workDir.toUtf8() + "\n");
file.write("Type=simple\n");
file.write("User=" + user.toUtf8() + "\n");
file.write("Restart=always\n");
file.write("TimeoutStopSec=infinity\n");
file.write("ExecStart=/usr/bin/env " + servName.toUtf8() + "\n\n");
file.write("[Install]\n");
file.write("WantedBy=multi-user.target\n");
file.close();
if (ret == 0) ret = QProcess::execute("systemctl", {"daemon-reload"});
if (start)
{
if (ret == 0) ret = QProcess::execute("systemctl", {"enable", servName});
if (ret == 0) ret = QProcess::execute("systemctl", {"start", servName});
}
}
}
file.close();
return ret;
}
int loadSh(const QString &name, const QString &exeCmd, const QString &buffDir, const QString &outDir, const QString &servUser, const QString &servGroup)
{
QFile file("/usr/bin/" + name);
auto ret = 0;
if (!file.open(QFile::ReadWrite | QFile::Truncate))
{
QTextStream(stderr) << "err: failed to open shell script file: " << file.fileName() << " for writing. reason: " << file.errorString() << Qt::endl;
ret = EACCES;
}
else
{
file.write("#!/bin/sh\n\n");
file.write("cd " + buffDir.toUtf8() + "\n");
file.write(exeCmd.toUtf8() + "\n");
file.close();
mkPath(buffDir);
mkPath(outDir);
mkPath(buffDir + "/live");
mkPath(buffDir + "/img");
QProcess::execute("chmod", {"-v", "+x", file.fileName()});
QProcess::execute("chown", {servUser + ":" + servGroup, "-R", buffDir});
QProcess::execute("chown", {servUser + ":" + servGroup, "-R", outDir});
}
file.close();
return ret;
}
QString camCmdFromConf(shared_t *conf, CmdExeType type)
{
QString ret = "";
if (type == MAIN_LOOP)
{
ret += QString(APP_BIN) + " -c " + conf->conf;
}
else
{
ret += "ffmpeg -hide_banner -y -i '" + conf->recordUri + "' -strftime 1 -strftime_mkdir 1 ";
if (conf->recordUri.contains("rtsp"))
{
ret += "-rtsp_transport tcp ";
}
if (conf->streamCodec != "copy")
{
ret += "-vf fps=" + QString::number(conf->recFps) + ",scale=" + conf->recScale + " ";
}
ret += "-vcodec " + conf->streamCodec + " ";
ret += "-reset_timestamps 1 -sc_threshold 0 -g 2 -force_key_frames \"expr:gte(t, n_forced * 2)\" -t 60 -segment_time 2 -f segment ";
ret += conf->buffPath + "/live/" + QString(STRFTIME_FMT) + conf->streamExt;
}
return ret;
}
int loadServiceByConf(const QString &confFile, bool start)
{
auto ret = 0;
shared_t conf;
if (!rdConf(confFile, &conf))
{
ret = conf.retCode;
}
else
{
auto desc = QString(APP_NAME) + " - " + conf.camName;
if (ret == 0) ret = loadSh(conf.servMainLoop, camCmdFromConf(&conf, MAIN_LOOP), conf.buffPath, conf.recPath, conf.servUser, conf.servGroup);
if (ret == 0) ret = loadSh(conf.servVidLoop, camCmdFromConf(&conf, VID_LOOP), conf.buffPath, conf.recPath, conf.servUser, conf.servGroup);
if (ret == 0) ret = loadService(desc, conf.servUser, conf.servMainLoop, conf.buffPath, conf.recPath, start);
if (ret == 0) ret = loadService(desc, conf.servUser, conf.servVidLoop, conf.buffPath, conf.recPath, start);
}
return ret;
}
int rmService(const QString &servName)
{
auto path = "/lib/systemd/system/" + servName + ".service";
auto ret = 0;
if (QFile::exists(path))
{
if (ret == 0) ret = QProcess::execute("systemctl", {"stop", servName});
if (ret == 0) ret = QProcess::execute("systemctl", {"disable", servName});
QFile::remove(path);
QFile::remove("/usr/bin/" + servName);
}
return ret;
}
int rmServiceByConf(const QString &confFile)
{
shared_t conf;
if (rdConf(confFile, &conf))
{
conf.retCode = rmService(conf.servMainLoop);
conf.retCode = rmService(conf.servVidLoop);
QDir(conf.buffPath).removeRecursively();
}
return conf.retCode;
}
void servStatByConf(const QString &confFile)
{
shared_t conf;
if (rdConf(confFile, &conf))
{
QTextStream cout(stdout);
cout << "--" << conf.camName << Qt::endl;
cout << " " << conf.servMainLoop << ": ";
if (QFile::exists("/lib/systemd/system/" + conf.servMainLoop + ".service"))
{
QProcess::execute("systemctl", {"is-enabled", conf.servMainLoop}); cout << Qt::endl;
}
else
{
cout << "Not Installed" << Qt::endl;
}
cout << " " << conf.servVidLoop << ": ";
if (QFile::exists("/lib/systemd/system/" + conf.servVidLoop + ".service"))
{
QProcess::execute("systemctl", {"is-enabled", conf.servVidLoop}); cout << Qt::endl;
}
else
{
cout << "Not Installed" << Qt::endl;
}
}
}
void servStatByDir(const QString &path)
{
auto files = lsFilesInDir(path);
for (auto &&conf : files)
{
servStatByConf(QDir::cleanPath(path) + "/" + conf);
}
}
int loadServiceByDir(const QString &path, bool start)
{
auto ret = 0;
auto files = lsFilesInDir(path);
for (auto &&conf : files)
{
if (ret == 0 ) ret = loadServiceByConf(QDir::cleanPath(path) + "/" + conf, start);
}
return ret;
}
int rmServiceByDir(const QString &path)
{
auto ret = 0;
auto files = lsFilesInDir(path);
for (auto &&conf : files)
{
if (ret == 0 ) ret = rmServiceByConf(QDir::cleanPath(path) + "/" + conf);
}
return ret;
}

29
src/services.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef SERVICES_H
#define SERVICES_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"
QString camCmdFromConf(shared_t *conf, CmdExeType type);
void servStatByDir(const QString &path);
void servStatByConf(const QString &confFile);
int loadService(const QString &desc, const QString &user, const QString &servName, const QString &workDir, const QString &recPath, bool start);
int loadServiceByConf(const QString &confFile, bool start);
int loadServiceByDir(const QString &path, bool start);
int rmServiceByConf(const QString &confFile);
int rmService(const QString &servName);
int rmServiceByDir(const QString &path);
int loadSh(const QString &name, const QString &exeCmd, const QString &buffDir, const QString &outDir, const QString &servUser, const QString &servGroup);
#endif // SERVICES_H

View File

@ -1,189 +0,0 @@
// This file is part of Motion Watch.
// Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "web.h"
void genFrontPage(shared_t *share)
{
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>" + QString(APP_NAME) + " " + QString(APP_VER) + "</h3>\n";
auto dirNames = lsDirsInDir(share->buffPath);
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 += "</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 += "<ul>\n";
for (auto &&logName : logNames)
{
auto name = logName;
name.remove("_log.html");
htmlText += " <li><a href='logs/" + logName + "'>" + name + "</a></li>\n";
}
htmlText += "</ul>\n";
}
if (QDir().exists(outputDir + "/live"))
{
htmlText += "<h4>Live</h4>\n";
genHTMLstream(htmlText);
}
if (QDir().exists(outputDir + "/events"))
{
auto eveNames = lsFilesInDir(outputDir + "/events", ".html");
htmlText += "<h4>Motion Events</h4>\n";
for (auto &&eveName : eveNames)
{
auto name = eveName;
name.remove(".html");
htmlText += "<a href='events/" + eveName + "'><img src='events/" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n";
}
}
htmlText += "</body>\n";
htmlText += "</html>";
QFile outFile(QDir().cleanPath(outputDir) + "/index.html");
outFile.open(QFile::WriteOnly);
outFile.write(htmlText.toUtf8());
outFile.close();
}
void genHTMLstream(QString &text)
{
text += "<script src=\"/hls.js\">\n";
text += "</script>\n";
text += "<video width=50% height=50% id=\"video\" controls>\n";
text += "</video>\n";
text += "<script>\n";
text += " var video = document.getElementById('video');\n";
text += " if (Hls.isSupported()) {\n";
text += " var hls = new Hls({\n";
text += " debug: true,\n";
text += " });\n";
text += " hls.loadSource('stream.m3u8');\n";
text += " hls.attachMedia(video);\n";
text += " hls.on(Hls.Events.MEDIA_ATTACHED, function () {\n";
text += " video.muted = true;\n";
text += " video.play();\n";
text += " });\n";
text += " }\n";
text += " else if (video.canPlayType('application/vnd.apple.mpegurl')) {\n";
text += " video.src = 'stream.m3u8';\n";
text += " video.addEventListener('canplay', function () {\n";
text += " video.play();\n";
text += " });\n";
text += " }\n";
text += "</script>\n";
}
void genHTMLvod(const QString &name)
{
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 += "<video width=100% height=100% controls autoplay>\n";
htmlText += " <source src='" + name + ".mp4' type='video/mp4'>\n";
htmlText += "</video>\n";
htmlText += "</body>\n";
htmlText += "</html>";
QFile outFile("events/" + name + ".html");
outFile.open(QFile::WriteOnly);
outFile.write(htmlText.toUtf8());
outFile.close();
}
void genCSS(shared_t *share)
{
QString cssText = "body {\n";
cssText += " background-color: " + share->webBg + ";\n";
cssText += " color: " + share->webTxt + ";\n";
cssText += " font-family: " + share->webFont + ";\n";
cssText += "}\n";
cssText += "a {\n";
cssText += " color: " + share->webTxt + ";\n";
cssText += "}\n";
QFile outFile(QDir().cleanPath(share->buffPath) + "/theme.css");
outFile.open(QFile::WriteOnly);
outFile.write(cssText.toUtf8());
outFile.close();
}

View File

@ -1,24 +0,0 @@
#ifndef WEB_H
#define WEB_H
// This file is part of Motion Watch.
// Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "common.h"
void genFrontPage(shared_t *share);
void genCamPage(shared_t *share);
void genHTMLstream(QString &text);
void genHTMLvod(const QString &name);
void genCSS(shared_t *share);
#endif // WEB_H