Compare commits
11 Commits
b09ff1a19a
...
970b9b5fc5
Author | SHA1 | Date | |
---|---|---|---|
|
970b9b5fc5 | ||
|
2aa5683c15 | ||
|
7ca4fc3dbe | ||
|
bb8a1fad45 | ||
|
da941d8d18 | ||
|
5b444fd754 | ||
|
4f0c37b92b | ||
|
60a24c9d67 | ||
|
525c342c0f | ||
|
83080cfe41 | ||
|
41ccf1d1e7 |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -57,5 +57,4 @@ compile_commands.json
|
||||||
|
|
||||||
# Build folders
|
# Build folders
|
||||||
/.build-mow
|
/.build-mow
|
||||||
/.build-opencv
|
/.build-imagemagick
|
||||||
/src/opencv
|
|
||||||
|
|
|
@ -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})
|
||||||
|
|
96
README.md
96
README.md
|
@ -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
BIN
bin/magick
BIN
bin/magick
Binary file not shown.
13
imgmagick_build.sh
Normal file
13
imgmagick_build.sh
Normal 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
|
39
install.sh
39
install.sh
|
@ -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
|
||||||
|
|
25
setup.sh
25
setup.sh
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
555
src/camera.cpp
555
src/camera.cpp
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
98
src/camera.h
98
src/camera.h
|
@ -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
|
||||||
|
|
307
src/common.cpp
307
src/common.cpp
|
@ -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 ¶m, 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 ¶m, 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 ¶m, 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();
|
||||||
|
|
||||||
|
auto thrCount = QThread::idealThreadCount() / 2;
|
||||||
|
|
||||||
share->retCode = 0;
|
share->retCode = 0;
|
||||||
share->imgThresh = 8000;
|
share->imgThresh = 8000;
|
||||||
share->maxEvents = 100;
|
share->maxEvents = 30;
|
||||||
share->maxLogSize = 100000;
|
share->skipCmd = false;
|
||||||
share->skipCmd = false;
|
share->postSecs = 60;
|
||||||
share->postSecs = 60;
|
share->evMaxSecs = 30;
|
||||||
share->evMaxSecs = 30;
|
share->conf = filePath;
|
||||||
share->conf = filePath;
|
share->outputType = "stderr";
|
||||||
share->buffPath = "/var/opt/" + QString(APP_BIN) + "/buf";
|
share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null";
|
||||||
share->webRoot = "/var/opt/" + QString(APP_BIN) + "/web";
|
share->streamCodec = "copy";
|
||||||
share->webBg = "#485564";
|
share->streamExt = ".avi";
|
||||||
share->webTxt = "#dee5ee";
|
share->recExt = ".avi";
|
||||||
share->webFont = "courier";
|
share->thumbExt = ".jpg";
|
||||||
share->outputType = "stderr";
|
share->recFps = 30;
|
||||||
share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null";
|
share->liveSecs = 80;
|
||||||
|
share->recScale = "1280:720";
|
||||||
|
share->imgScale = "320:240";
|
||||||
|
share->servUser = APP_BIN;
|
||||||
|
|
||||||
QString line;
|
QString line;
|
||||||
|
|
||||||
|
@ -222,146 +243,94 @@ 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
extCorrection(share->streamExt);
|
||||||
|
extCorrection(share->recExt);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName;
|
if (share->liveSecs < 10)
|
||||||
share->tmpDir = share->buffPath + "/" + share->camName;
|
{
|
||||||
share->servPath = QString("/var/opt/") + APP_BIN + "/" + APP_BIN + "." + share->camName + ".service";
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
67
src/common.h
67
src/common.h
|
@ -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 ¶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);
|
||||||
|
void rdLine(const QString ¶m, 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
|
||||||
|
|
125
src/logger.cpp
125
src/logger.cpp
|
@ -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);
|
|
||||||
}
|
|
24
src/logger.h
24
src/logger.h
|
@ -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
|
|
61
src/main.cpp
61
src/main.cpp
|
@ -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));
|
if ((ret == 0) && args.contains("-i"))
|
||||||
|
{
|
||||||
rmServices(); ret = loadServices(args);
|
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
254
src/services.cpp
Normal 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
29
src/services.h
Normal 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
|
189
src/web.cpp
189
src/web.cpp
|
@ -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();
|
|
||||||
}
|
|
24
src/web.h
24
src/web.h
|
@ -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
|
|
Loading…
Reference in New Issue
Block a user