server 1.2:

- removed -r command line option and renamed it to -s. then
   added -l as what -s used to be.

 - added detect_uri option to camera parameters so stream
   snapshots can now run a secondary stream that differs
   from the recording stream. also added sync logic so the
   recording loop and detection loop will run synchronously
   while still running in seperate threads.

 - live_secs renamed to live_mins. the amount of live
   footage to buffer in buffer_path is now based on minutes
   instead of seconds.

 - added live_clip_secs parameter so the amount of seconds
   each hls video clip should have is now adjustable.

 - remove max_event_secs. events are now transfered to
   rec_path based on minute increments.

 - added sec_per_image to make the amount of seconds
   between stream snapshots pulled from detect_uri
   adjustable.

 - added img_ext so the image snapshot format from
   detect_uri and then ultimately to img_comp_cmd can now
   be configured instead of being hardcoded to bmp.

 - added gray_image as a boolean parameter so the snapshots
   from detect_uri can be pulled in grayscale format or
   remain in color if desired.

 - removed img_scale since this parameter was not doing
   anything.

overview:

   use of QFileSystemWatcher on the detection loop has
   been eliminated and thus eliminated use of functions
   such as listFacingFiles(), backwardFacingFiles() and
   forwardFacingFiles(). Instead, the detection and
   recording loops will now run synchronously and use
   predictable up-to-the-minute file naming scheme.

client v1.1:

 - the build script will now include all imageformat
   plugins from the QT lib directory, adding support for
   jpg, svg, ico and gif image formats.

 - switched to 2 number versioning.

 - this client app will now support the server's new
   up-to-the-minute directory structure for live and
   recorded footage.

 - the version number is now be displayed on the main
   interface.
This commit is contained in:
Zii 2025-10-11 08:05:51 -04:00
parent c0f167e5b1
commit e72feb40ee
17 changed files with 545 additions and 311 deletions

View File

@ -43,10 +43,10 @@ Usage: jmotion <argument>
-u : uninstall the entire server from your system, including the service. all -u : uninstall the entire server from your system, including the service. all
recorded footage and config files will remain. recorded footage and config files will remain.
-f : force an action without pausing for user confirmation. -f : force an action without pausing for user confirmation.
-s : view the status of all camera instances. -l : view the status of all camera instances.
-q : kill all camera instances. -q : kill all camera instances.
-r : same as -d except it is non-blocking for starting all camera instances -s : same as -d except it is non-blocking for starting all camera instances
via systemd. same as 'systemctl start jmotion' via systemd. same as 'sudo systemctl start jmotion'
``` ```
### Config File (server) ### ### Config File (server) ###
@ -65,6 +65,13 @@ recording_uri = rtsp://1.2.3.4:554/h264
# to record footage. it can be a url to an rtsp stream or a direct device # to record footage. it can be a url to an rtsp stream or a direct device
# path such as /dev/video0. # path such as /dev/video0.
# #
detect_uri = rtsp://1.2.3.4:554/h264cif
# this is the uri to the secondary stream of the IP camera that will be
# used to take image snapshots that will be used with img_comp_cmd to
# detect any events of intrest. it can be a url to an rtsp stream or a
# direct device path such as /dev/video0. If the camera doesn't have a
# secondary stream, you can just use the same as recording_uri.
#
buffer_path = /tmp/jmotion buffer_path = /tmp/jmotion
# 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
@ -73,12 +80,17 @@ buffer_path = /tmp/jmotion
# #
rec_path = /var/jmotion/footage rec_path = /var/jmotion/footage
# this is video output directory that will be used to store any footage # this is video output directory that will be used to store any footage
# that contain any motion events. # that contain any detected events.
# #
live_secs = 160 live_mins = 60
# this is the maximum amount of seconds worth of live footage to keep in # this is the maximum amount minutes worth of live video clips to record
# buffer_path before deleting the oldest 3 seconds worth of footage. # to the buffer_path before deleting the oldest video clips. values less
# note: each video clip in buffer_path is typically 3 seconds long. # than 1 is considered invalid and will cause the app to default to 60.
#
live_clip_secs = 5
# this is the amount of seconds each video clip recorded from recording_uri
# can be. values between 1-59 are valid. anything outside of that range will
# cause the app to default to 5.
# #
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
@ -89,27 +101,27 @@ max_events_bytes = 10G
# this is the maximum amount of disk space of video footage that can be # this is the maximum amount of disk space of video footage that can be
# recorded in the rec_path directory. # recorded in the rec_path directory.
# #
max_event_secs = 30
# this is the maximum amount of secs of video footage that can be recorded
# in a motion event.
#
img_comp_cmd = compare -metric FUZZ &prev& &next& /dev/null img_comp_cmd = 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
# the external image comparison application. the external application is # the external image comparison application. the external application is
# expected to compare snapshots from the video stream to determine how # expected to compare snapshots from the video stream to determine how
# different the images are from each other. it needs to output a numeric # different the images are from each other. it needs to output a numeric
# score with the higher values meaning very different while low values # score with the higher values meaning very different while low values
# mean similar images. the snapshots pulled from the stream will be bitmap # mean similar images. magick is the default if not defined in the config
# formatted so the app will be required to support this format. also avoid # file. this parameter can also be set to 'null' as a means to turn off
# outputting any special chars, only numeric chars with a single '.' if # motion/event detection.
# outputting a decimal value. magick is the default if not defined in the #
# config file. this parameter can also be set to 'null' as a means to turn sec_per_image = 4
# off motion detection. # this is the amount of seconds between video stream snap shots to take
# from detect_uri that will then be passed on to img_comp_cmd via the &prev&
# and &next& parameters. values between 1-59 are valid. anything outside of
# that range will cause the app to default to 4.
# #
img_comp_out = stderr img_comp_out = stderr
# this is the standard output stream the app defined in img_comp_cmd will # this is the standard output stream the app defined in img_comp_cmd will
# use to output the comparison score. this can only be stderr or stdout, # 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. it will default to 'stderr'
# if the parameter is invalid.
# #
vid_codec = copy vid_codec = copy
# this is the encoding codec to use when recording footage from the camera. # this is the encoding codec to use when recording footage from the camera.
@ -129,6 +141,11 @@ stream_ext = .mkv
# from the camera in buffer_path. ffmpeg will also use this to determine # from the camera in buffer_path. ffmpeg will also use this to determine
# what format container to use for the video clips. # what format container to use for the video clips.
# #
img_ext = .jpg
# this is the file extension that will be used with detect_uri to take
# snapshots of the video stream. make sure the app defined in img_comp_cmd
# supports the image format defined here.
#
thumbnail_ext = .jpg thumbnail_ext = .jpg
# this the image format that will be used when creating the thumbnails # this the image format that will be used when creating the thumbnails
# for the videos clips recorded to rec_path. # for the videos clips recorded to rec_path.
@ -140,19 +157,25 @@ rec_ext = .mkv
# #
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 an event of any kind. any video clips found with events
# worth of video clips to be written out to rec_path. # are queued up to be copied to rec_path.
#
gray_image = y
# this is a boolean 'y' or 'n' parameter that enables or disable grayscale
# color formatting for the images pulled from the camera stream via the
# detect_uri. recommend turning this off if the img_comp_cmd already does
# this internally.
# #
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
# defined in post_cmd. the command will not run if motion was detected # defined in post_cmd. the command will not run if an event was detected
# in the space before post_secs elapsed. # in the space before post_secs elapsed.
# #
post_cmd = move_the_ptz_camera.py 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 an event was detected.
# also, motion detection is paused while this command is running. # also, event detection is paused while this command is running.
# #
rec_fps = 30 rec_fps = 30
# this sets the recording frames per second for the footage recorded # this sets the recording frames per second for the footage recorded
@ -163,11 +186,6 @@ rec_scale = 1280:720
# uses width, height numeric strings seperated by a colon, eg W:H. this # uses width, height numeric strings seperated by a colon, eg W:H. this
# has no affect of using 'copy' as the vid_codec. # has no affect of using 'copy' as the vid_codec.
# #
img_scale = 320:240
# this sets the pixel size of the thumbnails for recorded stored in
# rec_path. it uses width, height numeric strings seperated by a colon,
# eg W:H.
#
``` ```
### Build/Install ### ### Build/Install ###

View File

@ -108,10 +108,14 @@ def linux_build_app_dir(app_ver, app_name, app_target, qt_bin):
if not os.path.exists("app_dir/icons"): if not os.path.exists("app_dir/icons"):
os.makedirs("app_dir/icons") os.makedirs("app_dir/icons")
if not os.path.exists("app_dir/imageformats"):
os.makedirs("app_dir/imageformats")
verbose_copy(qt_bin + "/../plugins/platforms", "app_dir/platforms") verbose_copy(qt_bin + "/../plugins/platforms", "app_dir/platforms")
verbose_copy(qt_bin + "/../plugins/xcbglintegrations", "app_dir/xcbglintegrations") verbose_copy(qt_bin + "/../plugins/xcbglintegrations", "app_dir/xcbglintegrations")
verbose_copy(qt_bin + "/../plugins/multimedia", "app_dir/multimedia") verbose_copy(qt_bin + "/../plugins/multimedia", "app_dir/multimedia")
verbose_copy(qt_bin + "/../plugins/platformthemes", "app_dir/platformthemes") verbose_copy(qt_bin + "/../plugins/platformthemes", "app_dir/platformthemes")
verbose_copy(qt_bin + "/../plugins/imageformats", "app_dir/imageformats")
verbose_copy("build/" + app_target, "app_dir/" + app_target) verbose_copy("build/" + app_target, "app_dir/" + app_target)
verbose_copy("icons/main.svg", "app_dir/icons/scalable.svg") verbose_copy("icons/main.svg", "app_dir/icons/scalable.svg")

View File

@ -121,6 +121,7 @@ def local_install(app_target, app_name):
verbose_copy("app_dir/xcbglintegrations", install_dir + "/xcbglintegrations") verbose_copy("app_dir/xcbglintegrations", install_dir + "/xcbglintegrations")
verbose_copy("app_dir/multimedia", install_dir + "/multimedia") verbose_copy("app_dir/multimedia", install_dir + "/multimedia")
verbose_copy("app_dir/platformthemes", install_dir + "/platformthemes") verbose_copy("app_dir/platformthemes", install_dir + "/platformthemes")
verbose_copy("app_dir/imageformats", install_dir + "/imageformats")
verbose_create_symmlink(install_dir + "/" + app_target + ".sh", "/usr/bin/" + app_target) verbose_create_symmlink(install_dir + "/" + app_target + ".sh", "/usr/bin/" + app_target)

View File

@ -66,7 +66,7 @@
#include <QRandomGenerator> #include <QRandomGenerator>
#include <QProcess> #include <QProcess>
#define APP_VERSION "1.0.0" #define APP_VERSION "1.1"
#define APP_NAME "JustMotion-Client" #define APP_NAME "JustMotion-Client"
#define APP_TARGET "jmotion-client" #define APP_TARGET "jmotion-client"
#define DEFAULT_PLAYER "vlc --no-audio --no-video-title-show --no-osd --repeat pls.m3u8" #define DEFAULT_PLAYER "vlc --no-audio --no-video-title-show --no-osd --repeat pls.m3u8"

View File

@ -43,6 +43,8 @@ MainWidget::MainWidget(shared_t *share, MainGui *parent) : QWidget(parent)
sshOpts = new ssh_opt(); sshOpts = new ssh_opt();
homeLayout = new QVBoxLayout(this); homeLayout = new QVBoxLayout(this);
homeLayout->addWidget(new QLabel(tr("Version: ") + APP_VERSION, this));
genConnectWidget(); genConnectWidget();
genSettingsWidget(); genSettingsWidget();
genSpacer(); genSpacer();

View File

@ -55,7 +55,7 @@ void PlaylistItem::checkConfExists()
void PlaylistItem::loadImgFromFile(QString path) void PlaylistItem::loadImgFromFile(QString path)
{ {
if (path.isEmpty() || !QFileInfo::exists(path)) if (path.isEmpty() || !QFileInfo(path).isFile())
{ {
path = ":/img/no_mdeia.png"; path = ":/img/no_mdeia.png";
} }
@ -68,6 +68,11 @@ void PlaylistItem::loadImgFromFile(QString path)
void PlaylistItem::genImgAndText() void PlaylistItem::genImgAndText()
{ {
auto imgDir = basePath + QDir::separator() + "img" + QDir::separator(); auto imgDir = basePath + QDir::separator() + "img" + QDir::separator();
auto imgFolderList = lsDirsInDir(imgDir);
auto imgFolder = randPickItem(imgFolderList);
imgDir = imgDir + imgFolder + QDir::separator();
auto imgList = lsImgFiles(imgDir); auto imgList = lsImgFiles(imgDir);
auto imgFile = randPickItem(imgList); auto imgFile = randPickItem(imgList);

View File

@ -104,6 +104,8 @@ int Camera::start(const QString &conf)
connect(thr3, &QThread::finished, this, &Camera::objMinusOne); connect(thr3, &QThread::finished, this, &Camera::objMinusOne);
connect(detLoop, &DetectLoop::starving, recLoop, &RecordLoop::restart); connect(detLoop, &DetectLoop::starving, recLoop, &RecordLoop::restart);
connect(recLoop, &RecordLoop::started, detLoop, &DetectLoop::run);
connect(recLoop, &RecordLoop::finished, detLoop, &DetectLoop::terminate);
thr1->start(); thr1->start();
thr2->start(); thr2->start();

View File

@ -155,6 +155,7 @@ bool rdConf(const QString &filePath, shared_t *share)
else else
{ {
share->recordUri.clear(); share->recordUri.clear();
share->detectUri.clear();
share->postCmd.clear(); share->postCmd.clear();
share->camName.clear(); share->camName.clear();
share->buffPath.clear(); share->buffPath.clear();
@ -162,21 +163,23 @@ bool rdConf(const QString &filePath, shared_t *share)
share->retCode = 0; share->retCode = 0;
share->imgThresh = 8000; share->imgThresh = 8000;
share->postSecs = 60; share->postSecs = 300;
share->evMaxSecs = 30;
share->evtMaxBytes = 10000000000; share->evtMaxBytes = 10000000000;
share->conf = filePath; share->conf = filePath;
share->outputType = "stderr"; share->outputType = "stderr";
share->compCmd = "compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null"; share->compCmd = "compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null";
share->vidCodec = "copy"; share->vidCodec = "copy";
share->audCodec = "copy"; share->audCodec = "copy";
share->streamExt = ".mkv"; share->streamExt = ".ts";
share->recExt = ".mkv"; share->recExt = ".ts";
share->thumbExt = ".bmp"; share->thumbExt = ".jpg";
share->imgExt = ".jpg";
share->recFps = 30; share->recFps = 30;
share->liveSecs = 160; share->liveMins = 60;
share->liveClipSecs = 5;
share->secPerImg = 4;
share->grayImg = true;
share->recScale = "1280:720"; share->recScale = "1280:720";
share->imgScale = "320:240";
QString line; QString line;
@ -188,15 +191,16 @@ bool rdConf(const QString &filePath, shared_t *share)
{ {
rdLine("cam_name = ", line, &share->camName); rdLine("cam_name = ", line, &share->camName);
rdLine("recording_uri = ", line, &share->recordUri); rdLine("recording_uri = ", line, &share->recordUri);
rdLine("detect_uri = ", line, &share->detectUri);
rdLine("buffer_path = ", line, &share->buffPath); rdLine("buffer_path = ", line, &share->buffPath);
rdLine("rec_path = ", line, &share->recPath); rdLine("rec_path = ", line, &share->recPath);
rdLine("max_event_secs = ", line, &share->evMaxSecs);
rdLine("post_secs = ", line, &share->postSecs); rdLine("post_secs = ", line, &share->postSecs);
rdLine("post_cmd = ", line, &share->postCmd); rdLine("post_cmd = ", line, &share->postCmd);
rdLine("img_thresh = ", line, &share->imgThresh); rdLine("img_thresh = ", line, &share->imgThresh);
rdLine("max_events_bytes = ", line, &share->evtMaxBytes); rdLine("max_events_bytes = ", line, &share->evtMaxBytes);
rdLine("img_comp_out = ", line, &share->outputType); rdLine("img_comp_out = ", line, &share->outputType);
rdLine("img_comp_cmd = ", line, &share->compCmd); rdLine("img_comp_cmd = ", line, &share->compCmd);
rdLine("img_ext = ", line, &share->imgExt);
rdLine("vid_codec = ", line, &share->vidCodec); rdLine("vid_codec = ", line, &share->vidCodec);
rdLine("aud_codec = ", line, &share->audCodec); rdLine("aud_codec = ", line, &share->audCodec);
rdLine("stream_ext = ", line, &share->streamExt); rdLine("stream_ext = ", line, &share->streamExt);
@ -204,8 +208,10 @@ bool rdConf(const QString &filePath, shared_t *share)
rdLine("thumbnail_ext = ", line, &share->thumbExt); rdLine("thumbnail_ext = ", line, &share->thumbExt);
rdLine("rec_fps = ", line, &share->recFps); rdLine("rec_fps = ", line, &share->recFps);
rdLine("rec_scale = ", line, &share->recScale); rdLine("rec_scale = ", line, &share->recScale);
rdLine("img_scale = ", line, &share->imgScale); rdLine("live_mins = ", line, &share->liveMins);
rdLine("live_secs = ", line, &share->liveSecs); rdLine("live_clip_secs = ", line, &share->liveClipSecs);
rdLine("sec_per_image = ", line, &share->secPerImg);
rdLine("gray_image = ", line, &share->grayImg);
} }
} while(!line.isEmpty()); } while(!line.isEmpty());
@ -218,9 +224,33 @@ bool rdConf(const QString &filePath, shared_t *share)
extCorrection(share->streamExt); extCorrection(share->streamExt);
extCorrection(share->recExt); extCorrection(share->recExt);
extCorrection(share->thumbExt); extCorrection(share->thumbExt);
extCorrection(share->imgExt);
if ((share->liveClipSecs > 59) || (share->liveClipSecs <= 0))
{
qInfo() << "live_clip_secs = " << share->liveClipSecs << " is not valid. must be between 1-59. default set: " << 5;
share->liveClipSecs = 5;
}
if (share->liveMins <= 0)
{
qInfo() << "live_mins = " << share->liveMins << " is not valid. must be greater than 1. default set: " << 60;
share->liveMins = 60;
}
if ((share->secPerImg > 59) || (share->secPerImg <= 0))
{
qInfo() << "sec_per_image = " << share->secPerImg << " is not valid. must be between 1-59. default set: " << 10;
share->secPerImg = 4;
}
if (share->outputType != "stdout" && share->outputType != "stderr") if (share->outputType != "stdout" && share->outputType != "stderr")
{ {
qInfo() << "img_comp_out = " << share->outputType << " is not valid. must be stdout or stderr only. default set: stderr";
share->outputType = "stderr"; share->outputType = "stderr";
} }
@ -241,21 +271,47 @@ bool rdConf(const QString &filePath, shared_t *share)
{ {
share->recPath = QDir::cleanPath(share->recPath); share->recPath = QDir::cleanPath(share->recPath);
} }
if (share->liveSecs < 10)
{
share->liveSecs = 10;
}
if ((share->liveSecs - 4) < share->evMaxSecs)
{
share->evMaxSecs = share->liveSecs - 4;
}
} }
return share->retCode == 0; return share->retCode == 0;
} }
QString getStatLineFromProc(QProcess *proc)
{
if (proc->state() == QProcess::Running)
{
return "OK ";
}
else if (proc->state() == QProcess::Starting)
{
return "STARTING";
}
else if (proc->error() == QProcess::FailedToStart)
{
return "FAILED ";
}
else if (proc->error() == QProcess::UnknownError)
{
return "STOPPED ";
}
else if (proc->error() == QProcess::Crashed)
{
return "CRASHED ";
}
else if (proc->error() == QProcess::Timedout)
{
return "TIMEOUT ";
}
else if (proc->error() == QProcess::WriteError)
{
return "WR_ERR ";
}
else
{
return "RD_ERR ";
}
}
void setupBuffDir(const QString &path, bool del) void setupBuffDir(const QString &path, bool del)
{ {
if (del) if (del)

View File

@ -32,11 +32,11 @@
using namespace std; using namespace std;
#define APP_VERSION "1.1" #define APP_VERSION "1.2"
#define APP_NAME "JustMotion" #define APP_NAME "JustMotion"
#define APP_TARGET "jmotion" #define APP_TARGET "jmotion"
#define DATETIME_FMT "yyyyMMddhhmmss" #define DATETIME_FMT "yyyyMMddhhmm"
#define STRFTIME_FMT "%Y%m%d%H%M%S" #define STRFTIME_FMT "%Y%m%d%H%M"
#define PREV_IMG "&prev&" #define PREV_IMG "&prev&"
#define NEXT_IMG "&next&" #define NEXT_IMG "&next&"
@ -46,17 +46,12 @@ enum CmdExeType
VID_LOOP VID_LOOP
}; };
struct evt_t
{
QList<QString> vidList;
QList<QString> imgList;
};
struct shared_t struct shared_t
{ {
evt_t recList; QStringList recList;
QString conf; QString conf;
QString recordUri; QString recordUri;
QString detectUri;
QString buffPath; QString buffPath;
QString postCmd; QString postCmd;
QString camName; QString camName;
@ -67,18 +62,21 @@ struct shared_t
QString audCodec; QString audCodec;
QString streamExt; QString streamExt;
QString recExt; QString recExt;
QString imgExt;
QString thumbExt; QString thumbExt;
QString recScale; QString recScale;
QString imgScale;
quint64 evtMaxBytes; quint64 evtMaxBytes;
int liveSecs; bool grayImg;
int liveMins;
int liveClipSecs;
int recFps; int recFps;
int evMaxSecs; int secPerImg;
int postSecs; int postSecs;
int imgThresh; int imgThresh;
int retCode; int retCode;
}; };
QString getStatLineFromProc(QProcess *proc);
QString buildThreadCount(int count); 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);
@ -95,5 +93,6 @@ void rdLine(const QString &param, const QString &line, int *value);
void rdLine(const QString &param, const QString &line, quint64 *value); void rdLine(const QString &param, const QString &line, quint64 *value);
void rdLine(const QString &param, const QString &line, bool *value); void rdLine(const QString &param, const QString &line, bool *value);
void extCorrection(QString &ext); void extCorrection(QString &ext);
void addStrsToList(QStringList &strList, QStringList &add);
#endif // COMMON_H #endif // COMMON_H

View File

@ -12,11 +12,12 @@
#include "detect_loop.h" #include "detect_loop.h"
DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QFileSystemWatcher(parent) DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QProcess(parent)
{ {
evtOn = false;
pcTimer = 0; pcTimer = 0;
starvCnt = 0;
shared = sharedRes; shared = sharedRes;
rerun = false;
connect(thr, &QThread::started, this, &DetectLoop::init); connect(thr, &QThread::started, this, &DetectLoop::init);
connect(thr, &QThread::finished, this, &DetectLoop::deleteLater); connect(thr, &QThread::finished, this, &DetectLoop::deleteLater);
@ -24,59 +25,99 @@ DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QFi
moveToThread(thr); moveToThread(thr);
} }
DetectLoop::~DetectLoop()
{
terminate();
stopTimers();
waitForFinished();
}
void DetectLoop::init() void DetectLoop::init()
{ {
pcTimer = new QTimer(this); pcTimer = new QTimer(this);
evtTimer = new QTimer(this); evtTimer = new QTimer(this);
detTimer = new QTimer(this);
mkdirTimer = new QTimer(this);
postProc = new QProcess(this);
setupBuffDir(shared->buffPath); setupBuffDir(shared->buffPath);
connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak); pcTimer->setInterval(shared->postSecs * 1000);
mkdirTimer->setInterval(60000);
connect(this, &QFileSystemWatcher::directoryChanged, this, &DetectLoop::updated); detTimer->setInterval((shared->secPerImg * 2) * 1000);
evtTimer->setInterval(60000);
pcTimer->start(shared->postSecs * 1000);
if (shared->compCmd.toLower() != "null")
{
connect(evtTimer, &QTimer::timeout, this, &DetectLoop::reset);
evtTimer->setSingleShot(true); evtTimer->setSingleShot(true);
eTimer.start(); connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak);
connect(detTimer, &QTimer::timeout, this, &DetectLoop::exec);
connect(mkdirTimer, &QTimer::timeout, this, &DetectLoop::mkdirs);
connect(this, &DetectLoop::started, this, &DetectLoop::startTimers);
connect(this, &DetectLoop::finished, this, &DetectLoop::stopTimers);
}
void DetectLoop::startTimers()
{
pcTimer->start();
detTimer->start();
mkdirTimer->start();
}
void DetectLoop::stopTimers()
{
starvCnt = 0;
pcTimer->stop();
detTimer->stop();
mkdirTimer->stop();
evtTimer->stop();
if (rerun)
{
rerun = false;
run();
}
}
void DetectLoop::mkdirs()
{
auto timeStamp = QDateTime::currentDateTime();
auto path1 = shared->buffPath + "/img/" + timeStamp.toString(DATETIME_FMT);
timeStamp = timeStamp.addSecs(60);
auto path2 = shared->buffPath + "/img/" + timeStamp.toString(DATETIME_FMT);
if (!QFileInfo(path1).exists()) QDir().mkpath(path1);
if (!QFileInfo(path2).exists()) QDir().mkpath(path2);
}
QString DetectLoop::camCmdFromConf()
{
auto ret = "ffmpeg -hide_banner -y -i '" + shared->detectUri + "' -strftime 1 -strftime_mkdir 1 ";
if (shared->detectUri.contains("rtsp"))
{
ret += "-rtsp_transport udp ";
}
ret += "-vf fps=1/" + QString::number(shared->secPerImg);
if (shared->grayImg)
{
ret += ",format=gray ";
} }
else else
{ {
eTimer.invalidate(); ret += " ";
} }
addPath(shared->buffPath + "/vid"); ret += "img/" + QString(STRFTIME_FMT) + "/%S" + shared->imgExt;
}
void DetectLoop::updated(const QString &path) qInfo() << ret;
{
if (shared->compCmd.toLower() != "null")
{
eTimer.start();
}
auto clips = lsFilesInDir(path, shared->streamExt); return ret;
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 + "/vid/" + vidAName;
vidBPath = shared->buffPath + "/vid/" + vidBName;
exec(); thread()->sleep(1);
}
} }
void DetectLoop::pcBreak() void DetectLoop::pcBreak()
@ -87,7 +128,14 @@ void DetectLoop::pcBreak()
if (evtTimer->isActive()) if (evtTimer->isActive())
{ {
qInfo() << "motion detected, skipping the post command"; qInfo() << "motion detected, skipping the post command.";
}
else if (postProc->state() == QProcess::Running)
{
qInfo() << "previous post command is still running.";
qInfo() << "attempting to force close it.";
postProc->terminate();
} }
else else
{ {
@ -101,7 +149,7 @@ void DetectLoop::pcBreak()
} }
else else
{ {
QProcess::execute(args[0], args.mid(1)); postProc->start(args[0], args.mid(1));
} }
} }
} }
@ -129,13 +177,54 @@ float DetectLoop::getFloatFromExe(const QByteArray &line)
if (!ok || strNum.isEmpty()) if (!ok || strNum.isEmpty())
{ {
qCritical() << "err: the image comp command returned unexpected output and couldn't be converted to float"; qCritical() << "err: the image comp command returned unexpected output that couldn't be converted to float";
qCritical() << " raw output: " << line; qCritical() << " raw output: " << line;
} }
return res; return res;
} }
QString DetectLoop::statusLine()
{
if (starvCnt >= STARV_LIMIT)
{
return "STARVING";
}
else if (shared->compCmd == "null")
{
return "OFF ";
}
else
{
return getStatLineFromProc(this);
}
}
void DetectLoop::run()
{
auto cmdLine = camCmdFromConf();
auto args = parseArgs(cmdLine.toUtf8(), -1);
//qInfo() << "start image extraction command: " << cmdLine;
if (args.isEmpty())
{
qCritical() << "err: couldn't parse a program name from detectloop cmd: '" << cmdLine << "'";
}
else if (state() == QProcess::Running)
{
rerun = true; terminate();
}
else
{
setWorkingDirectory(shared->buffPath);
setProgram(args[0]);
setArguments(args.mid(1));
mkdirs();
start();
}
}
QStringList DetectLoop::buildArgs(const QString &prev, const QString &next) QStringList DetectLoop::buildArgs(const QString &prev, const QString &next)
{ {
auto args = parseArgs(shared->compCmd.toUtf8(), -1); auto args = parseArgs(shared->compCmd.toUtf8(), -1);
@ -149,64 +238,36 @@ QStringList DetectLoop::buildArgs(const QString &prev, const QString &next)
return args; return args;
} }
QStringList DetectLoop::buildSnapArgs(const QString &vidSrc, const QString &imgPath) void DetectLoop::exec()
{ {
QStringList ret; if (starvCnt >= STARV_LIMIT)
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;
}
QString DetectLoop::statusLine()
{ {
if (!eTimer.isValid()) if (detTimer->isActive())
{
return "OFF ";
}
else if (eTimer.elapsed() >= 5000)
{ {
emit starving(); emit starving();
}
return "STARVED";
} }
else else
{ {
return "OK "; auto timeStamp = QDateTime::currentDateTime();
} auto path = shared->buffPath + "/img/" + timeStamp.toString(DATETIME_FMT);
} auto imgList = lsFilesInDir(path, shared->imgExt);
void DetectLoop::reset() if (imgList.size() >= 2)
{ {
evtOn = false; starvCnt = 0;
}
void DetectLoop::exec() if ((shared->compCmd != "null") && (postProc->state() != QProcess::Running))
{ {
auto imgAPath = shared->buffPath + "/img/" + QFileInfo(vidAPath).baseName() + ".bmp"; auto imgA = path + "/" + imgList[imgList.size() - 2];
auto imgBPath = shared->buffPath + "/img/" + QFileInfo(vidBPath).baseName() + ".bmp"; auto imgB = path + "/" + imgList[imgList.size() - 1];
auto snapArgsA = buildSnapArgs(vidAPath, imgAPath); auto compArgs = buildArgs(imgA, imgB);
auto snapArgsB = buildSnapArgs(vidBPath, imgBPath);
auto compArgs = buildArgs(imgAPath, imgBPath);
if (compArgs.isEmpty()) if (compArgs.isEmpty())
{ {
qCritical() << "err: could not parse a executable name from img_comp_cmd: " << shared->compCmd; qCritical() << "err: could not parse a executable name from img_comp_cmd: " << shared->compCmd;
} }
else else
{
QProcess::execute("ffmpeg", snapArgsA);
QProcess::execute("ffmpeg", snapArgsB);
if (QFile::exists(imgAPath) && QFile::exists(imgBPath) && (shared->compCmd.toLower() != "null"))
{ {
QProcess extComp; QProcess extComp;
@ -226,21 +287,24 @@ void DetectLoop::exec()
qInfo() << compArgs.join(" ") << " --result: " << QString::number(score); qInfo() << compArgs.join(" ") << " --result: " << QString::number(score);
if ((score >= shared->imgThresh) || evtOn) if ((score >= shared->imgThresh) || evtTimer->isActive())
{ {
shared->recList.vidList.append(vidAPath); if (!shared->recList.contains(timeStamp.toString(DATETIME_FMT)))
shared->recList.vidList.append(vidBPath); {
shared->recList.imgList.append(imgAPath); shared->recList.append(timeStamp.toString(DATETIME_FMT));
shared->recList.imgList.append(imgBPath); }
if (!evtTimer->isActive()) if (!evtTimer->isActive())
{ {
evtTimer->start(shared->evMaxSecs * 1000); evtTimer->start();
} }
} }
} }
} }
}
vidAPath.clear(); else
vidBPath.clear(); {
starvCnt++;
}
}
} }

View File

@ -15,38 +15,46 @@
#include "common.h" #include "common.h"
class DetectLoop : public QFileSystemWatcher #define STARV_LIMIT 24
class DetectLoop : public QProcess
{ {
Q_OBJECT Q_OBJECT
private: private:
QString vidAPath;
QString vidBPath;
QString vidAName;
QString vidBName;
QTimer *pcTimer; QTimer *pcTimer;
QTimer *evtTimer; QTimer *evtTimer;
QElapsedTimer eTimer; QTimer *detTimer;
QTimer *mkdirTimer;
shared_t *shared; shared_t *shared;
bool evtOn; QProcess *postProc;
uint starvCnt;
bool rerun;
float getFloatFromExe(const QByteArray &line); float getFloatFromExe(const QByteArray &line);
QString camCmdFromConf();
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 exec();
void init(); void init();
void reset();
void pcBreak(); void pcBreak();
void updated(const QString &path); void mkdirs();
void startTimers();
void stopTimers();
public slots:
void run();
public: public:
explicit DetectLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr); explicit DetectLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
void exec(); ~DetectLoop();
QString statusLine(); QString statusLine();
signals: signals:

View File

@ -15,7 +15,7 @@
EventLoop::EventLoop(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 = 1; heartBeat = 3;
loopTimer = 0; loopTimer = 0;
connect(thr, &QThread::started, this, &EventLoop::init); connect(thr, &QThread::started, this, &EventLoop::init);
@ -46,23 +46,31 @@ QString EventLoop::statusLine()
} }
} }
void EventLoop::deepListVids(QString &plsText)
{
auto txt = QTextStream(&plsText);
auto dirs = lsDirsInDir(shared->recPath + "/vid");
for (auto dir : dirs)
{
auto vids = lsFilesInDir(shared->recPath + "/vid/" + dir, shared->streamExt);
for (auto vid : vids)
{
txt << "#EXTINF:0" << Qt::endl;
txt << "vid/" << dir << "/" << vid << Qt::endl;
}
}
}
void EventLoop::updatePls() void EventLoop::updatePls()
{ {
QString text; QString text;
auto txt = QTextStream(&text); auto txt = QTextStream(&text);
txt << "#EXTM3U" << Qt::endl; txt << "#EXTM3U" << Qt::endl;
txt << "#EXT-X-VERSION:3" << Qt::endl;
txt << "#EXT-X-TARGETDURATION:4" << Qt::endl;
txt << "#EXT-X-MEDIA-SEQUENCE:0" << Qt::endl;
auto vids = lsFilesInDir(shared->recPath + "/vid", shared->recExt); deepListVids(text);
for (auto vid : vids)
{
txt << "#EXTINF:4.000000," << Qt::endl;
txt << "vid/" << vid << Qt::endl;
}
QFile file(shared->recPath + "/pls.m3u8"); QFile file(shared->recPath + "/pls.m3u8");
@ -74,73 +82,83 @@ void EventLoop::updatePls()
file.close(); file.close();
} }
void EventLoop::exec() void EventLoop::copyDirVid(const QString &src, const QString &dst)
{ {
bool dirUpdated = false; auto vidList = lsFilesInDir(src, shared->streamExt);
if (!shared->recList.vidList.isEmpty()) for (auto vidName : vidList)
{ {
auto vidPath = shared->recList.vidList.takeFirst(); QFile::copy(src + QDir::separator() + vidName, dst + QDir::separator() + vidName);
auto imgPath = shared->recList.imgList.takeFirst(); }
auto vidDst = shared->recPath + "/vid/" + QFileInfo(vidPath).fileName();
auto imgDst = shared->recPath + "/img/" + QFileInfo(imgPath).baseName() + shared->thumbExt;
qInfo() << "---EVENT WRITEOUT START---";
qInfo() << "source files: ";
qInfo() << vidPath;
qInfo() << imgPath;
qInfo() << "destination files: ";
qInfo() << vidDst;
qInfo() << imgDst;
mkPath(shared->recPath + "/vid");
mkPath(shared->recPath + "/img");
if (!QFile::copy(vidPath, vidDst))
{
qCritical() << "err: file copy operation failed." << Qt::endl;
qInfo() << "exists?: " << vidPath << ": " << QFileInfo::exists(vidPath);
} }
if (imgPath.endsWith(shared->thumbExt)) void EventLoop::copyDirImg(const QString &src, const QString &dst)
{ {
QFile::copy(imgPath, imgDst); auto imgList = lsFilesInDir(src, shared->imgExt);
for (auto imgName : imgList)
{
auto srcImgPath = src + QDir::separator() + imgName;
auto dstImgPath = dst + QDir::separator() + imgName;
if (imgName.endsWith(shared->thumbExt))
{
QFile::copy(srcImgPath, dstImgPath);
} }
else else
{ {
QStringList args; QStringList args;
args << imgPath; args << srcImgPath;
args << imgDst; args << dstImgPath;
QProcess::execute("convert", args); QProcess::execute("convert", args);
} }
}
}
void EventLoop::exec()
{
bool dirUpdated = false;
if (!shared->recList.isEmpty())
{
auto timeStamp = shared->recList.takeFirst();
auto srcVidPath = shared->buffPath + "/vid/" + timeStamp;
auto srcImgPath = shared->buffPath + "/img/" + timeStamp;
auto dstVidPath = shared->recPath + "/vid/" + timeStamp;
auto dstImgPath = shared->recPath + "/img/" + timeStamp;
mkPath(dstVidPath);
mkPath(dstImgPath);
copyDirVid(srcVidPath, dstVidPath);
copyDirImg(srcImgPath, dstImgPath);
dirUpdated = true; dirUpdated = true;
qInfo() << "---EVENT WRITEOUT END---";
} }
else else
{ {
// maintain max live secs worth of image files in the img buffer folder. // maintain max live vid clips worth of img files on the buffer folder.
auto names = lsFilesInDir(shared->buffPath + "/img", ".bmp"); auto names = lsDirsInDir(shared->buffPath + "/img");
if (names.size() > (shared->liveSecs / 3)) if (names.size() > shared->liveMins)
{ {
QFile::remove(shared->buffPath + "/img/" + names[0]); QDir(shared->buffPath + "/img/" + names[0]).removeRecursively();
names.removeFirst(); names.removeFirst();
} }
// maintain max live secs worth of video files in the vid buffer folder. // maintain max live vid clips worth of video files in the vid buffer folder.
names = lsFilesInDir(shared->buffPath + "/vid", shared->streamExt); names = lsDirsInDir(shared->buffPath + "/vid");
if (names.size() > (shared->liveSecs / 3)) if (names.size() > shared->liveMins)
{ {
QFile::remove(shared->buffPath + "/vid/" + names[0]); QDir(shared->buffPath + "/vid/" + names[0]).removeRecursively();
names.removeFirst(); names.removeFirst();
} }
@ -148,17 +166,15 @@ void EventLoop::exec()
// maintain max byte size of the vid/img folder in the recording path. // maintain max byte size of the vid/img folder in the recording path.
auto names = lsFilesInDir(shared->recPath + "/vid", shared->recExt); auto names = lsDirsInDir(shared->recPath + "/vid");
for (auto i = 0; (byteSize(shared->recPath) >= shared->evtMaxBytes); ++i) for (auto i = 0; (byteSize(shared->recPath) >= shared->evtMaxBytes); ++i)
{ {
auto nameOnly = QFileInfo(shared->recPath + "/vid/" + names[0]).baseName(); auto vidDir = shared->recPath + "/vid/" + names[0];
auto imgDir = shared->recPath + "/img/" + names[0];
auto vidFile = shared->recPath + "/vid/" + nameOnly + shared->recExt; QDir(vidDir).removeRecursively();
auto imgFile = shared->recPath + "/img/" + nameOnly + shared->thumbExt; QDir(imgDir).removeRecursively();
QFile::remove(vidFile);
QFile::remove(imgFile);
names.removeFirst(); names.removeFirst();

View File

@ -31,6 +31,9 @@ private:
int heartBeat; int heartBeat;
void updatePls(); void updatePls();
void deepListVids(QString &plsText);
void copyDirImg(const QString &src, const QString &dst);
void copyDirVid(const QString &src, const QString &dst);
public: public:

View File

@ -25,10 +25,10 @@ void showHelp(const QString etcDir)
QTextStream(stdout) << "-u : uninstall the entire server from your system, including the service. all" << Qt::endl; QTextStream(stdout) << "-u : uninstall the entire server from your system, including the service. all" << Qt::endl;
QTextStream(stdout) << " recorded footage and config files will remain." << Qt::endl; QTextStream(stdout) << " recorded footage and config files will remain." << 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) << "-s : view the status of all camera instances." << Qt::endl; QTextStream(stdout) << "-l : view the status of all camera instances." << Qt::endl;
QTextStream(stdout) << "-q : kill all camera instances." << Qt::endl; QTextStream(stdout) << "-q : kill all camera instances." << Qt::endl;
QTextStream(stdout) << "-r : same as -d except it is non-blocking for starting all camera instances" << Qt::endl; QTextStream(stdout) << "-s : same as -d except it is non-blocking for starting all camera instances" << Qt::endl;
QTextStream(stdout) << " via systemd. same as 'systemctl start " << APP_TARGET << "'" << Qt::endl; QTextStream(stdout) << " via systemd. same as 'sudo systemctl start " << APP_TARGET << "'" << Qt::endl;
} }
int main(int argc, char** argv) int main(int argc, char** argv)
@ -81,7 +81,7 @@ int main(int argc, char** argv)
{ {
if (args.contains("-f")) if (args.contains("-f"))
{ {
ret = QProcess::execute("/opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList()); ret = QProcess::execute("sudo /opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList());
} }
else else
{ {
@ -92,11 +92,11 @@ int main(int argc, char** argv)
if (ans == 'y' || ans == 'Y') if (ans == 'y' || ans == 'Y')
{ {
ret = QProcess::execute("/opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList()); ret = QProcess::execute("sudo /opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList());
} }
} }
} }
else if (args.contains("-s")) else if (args.contains("-l"))
{ {
auto statFiles = lsFilesInDir(staDir); auto statFiles = lsFilesInDir(staDir);
@ -116,9 +116,9 @@ int main(int argc, char** argv)
QFile::remove(procFile); QFile::remove(procFile);
QThread::currentThread()->sleep(5); QThread::currentThread()->sleep(5);
} }
else if (args.contains("-r")) else if (args.contains("-s"))
{ {
QProcess::execute("systemctl start " + QString(APP_TARGET)); QProcess::execute("sudo systemctl start " + QString(APP_TARGET));
} }
else else
{ {

View File

@ -23,12 +23,15 @@ RecordLoop::RecordLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QPr
connect(this, &RecordLoop::readyReadStandardOutput, this, &RecordLoop::resetTime); connect(this, &RecordLoop::readyReadStandardOutput, this, &RecordLoop::resetTime);
connect(this, &RecordLoop::readyReadStandardError, this, &RecordLoop::resetTime); connect(this, &RecordLoop::readyReadStandardError, this, &RecordLoop::resetTime);
connect(this, &RecordLoop::started, this, &RecordLoop::resetTime); connect(this, &RecordLoop::started, this, &RecordLoop::resetTime);
connect(this, &RecordLoop::finished, this, &RecordLoop::restart);
moveToThread(thr); moveToThread(thr);
} }
RecordLoop::~RecordLoop() RecordLoop::~RecordLoop()
{ {
disconnect(this, &RecordLoop::finished, this, &RecordLoop::restart);
terminate(); terminate();
waitForFinished(); waitForFinished();
} }
@ -36,16 +39,39 @@ RecordLoop::~RecordLoop()
void RecordLoop::init() void RecordLoop::init()
{ {
checkTimer = new QTimer(this); checkTimer = new QTimer(this);
mkdirTimer = new QTimer(this);
sync = false;
connect(checkTimer, &QTimer::timeout, this, &RecordLoop::restart); connect(checkTimer, &QTimer::timeout, this, &RecordLoop::restart);
connect(mkdirTimer, &QTimer::timeout, this, &RecordLoop::mkdirs);
checkTimer->setSingleShot(true); connect(this, &RecordLoop::finished, mkdirTimer, &QTimer::stop);
checkTimer->setInterval(3000); connect(this, &RecordLoop::finished, checkTimer, &QTimer::stop);
mkdirTimer->setInterval(60000);
checkTimer->setInterval(60000);
// stall timeout. ffmpeg has a tendency to just 'stop.' meaning it would just stop
// reording at random times without stop signals or even error messages. this timer,
// monitors for any text output from ffmpeg. if no updates in 60000 msecs, it will
// automatically restart ffmpeg.
setupBuffDir(shared->buffPath); setupBuffDir(shared->buffPath);
restart(); restart();
} }
void RecordLoop::mkdirs()
{
auto timeStamp = QDateTime::currentDateTime();
auto path1 = shared->buffPath + "/vid/" + timeStamp.toString(DATETIME_FMT);
timeStamp = timeStamp.addSecs(60);
auto path2 = shared->buffPath + "/vid/" + timeStamp.toString(DATETIME_FMT);
if (!QFileInfo(path1).exists()) QDir().mkpath(path1);
if (!QFileInfo(path2).exists()) QDir().mkpath(path2);
}
QString RecordLoop::camCmdFromConf() QString RecordLoop::camCmdFromConf()
{ {
auto ret = "ffmpeg -hide_banner -y -i '" + shared->recordUri + "' -strftime 1 -strftime_mkdir 1 "; auto ret = "ffmpeg -hide_banner -y -i '" + shared->recordUri + "' -strftime 1 -strftime_mkdir 1 ";
@ -60,9 +86,12 @@ QString RecordLoop::camCmdFromConf()
ret += "-vf fps=" + QString::number(shared->recFps) + ",scale=" + shared->recScale + " "; ret += "-vf fps=" + QString::number(shared->recFps) + ",scale=" + shared->recScale + " ";
} }
auto maxClips = (60 / shared->liveClipSecs) * shared->liveMins;
ret += "-vcodec " + shared->vidCodec + " "; ret += "-vcodec " + shared->vidCodec + " ";
ret += "-acodec " + shared->audCodec + " "; ret += "-acodec " + shared->audCodec + " ";
ret += "-hls_time 3 -hls_list_size " + QString::number(shared->liveSecs / 3) + " -hls_flags delete_segments -hls_segment_filename vid/" + QString(STRFTIME_FMT) + shared->streamExt + " "; ret += "-hls_time " + QString::number(shared->liveClipSecs) + " -hls_list_size " + QString::number(maxClips) + " ";
ret += "-hls_flags delete_segments -hls_segment_filename vid/" + QString(STRFTIME_FMT) + "/%S" + shared->streamExt + " ";
ret += "pls.m3u8"; ret += "pls.m3u8";
qInfo() << ret; qInfo() << ret;
@ -72,29 +101,47 @@ QString RecordLoop::camCmdFromConf()
QString RecordLoop::statusLine() QString RecordLoop::statusLine()
{ {
if (state() == QProcess::Running) if (!sync)
{ {
return "OK "; return "WAIT ";
} }
else else
{ {
return "FAIL"; return getStatLineFromProc(this);
} }
} }
void RecordLoop::synced()
{
sync = true;
restart();
}
void RecordLoop::resetTime() void RecordLoop::resetTime()
{ {
// reset the stall timer to prevent it from timing out when ffmpeg doesn't
// need to be restarted.
checkTimer->start(); checkTimer->start();
} }
void RecordLoop::restart() void RecordLoop::restart()
{ {
auto sec = QTime::currentTime().second();
if (state() == QProcess::Running) if (state() == QProcess::Running)
{ {
terminate(); terminate();
waitForFinished();
} }
else if (!sync && (sec != 0))
{
// start recording when the seconds mark hit zero in real life.
QTimer::singleShot((60 - sec) * 1000, this, &RecordLoop::synced);
}
else
{
auto cmdLine = camCmdFromConf(); auto cmdLine = camCmdFromConf();
auto args = parseArgs(cmdLine.toUtf8(), -1); auto args = parseArgs(cmdLine.toUtf8(), -1);
@ -109,7 +156,11 @@ void RecordLoop::restart()
setWorkingDirectory(shared->buffPath); setWorkingDirectory(shared->buffPath);
setProgram(args[0]); setProgram(args[0]);
setArguments(args.mid(1)); setArguments(args.mid(1));
mkdirs();
mkdirTimer->start();
start(); start();
} }
} }
}

View File

@ -23,17 +23,22 @@ private slots:
void init(); void init();
void resetTime(); void resetTime();
void mkdirs();
private: private:
shared_t *shared; shared_t *shared;
QTimer *checkTimer; QTimer *checkTimer;
QTimer *mkdirTimer;
bool sync;
QString camCmdFromConf(); QString camCmdFromConf();
QString nearest15mins(QString &sec);
public slots: public slots:
void restart(); void restart();
void synced();
public: public: