diff --git a/README.md b/README.md index 7654278..2ed158d 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,10 @@ Usage: jmotion -u : uninstall the entire server from your system, including the service. all recorded footage and config files will remain. -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. --r : same as -d except it is non-blocking for starting all camera instances - via systemd. same as 'systemctl start jmotion' +-s : same as -d except it is non-blocking for starting all camera instances + via systemd. same as 'sudo systemctl start jmotion' ``` ### 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 # 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 # 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 @@ -73,12 +80,17 @@ buffer_path = /tmp/jmotion # rec_path = /var/jmotion/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 -# this is the maximum amount of seconds worth of live footage to keep in -# buffer_path before deleting the oldest 3 seconds worth of footage. -# note: each video clip in buffer_path is typically 3 seconds long. +live_mins = 60 +# this is the maximum amount minutes worth of live video clips to record +# to the buffer_path before deleting the oldest video clips. values less +# 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 # 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 # 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 # this is the command line template this application will use when calling # the external image comparison application. the external application is # expected to compare snapshots from the video stream to determine how # different the images are from each other. it needs to output a numeric # score with the higher values meaning very different while low values -# mean similar images. the snapshots pulled from the stream will be bitmap -# formatted so the app will be required to support this format. also avoid -# outputting any special chars, only numeric chars with a single '.' if -# outputting a decimal value. magick is the default if not defined in the -# config file. this parameter can also be set to 'null' as a means to turn -# off motion detection. +# mean similar images. magick is the default if not defined in the config +# file. this parameter can also be set to 'null' as a means to turn off +# motion/event detection. +# +sec_per_image = 4 +# 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 # this is the standard output stream the app defined in img_comp_cmd will # use to output the comparison score. this can only be stderr or stdout, -# any other stream name is considered invalid. +# any other stream name is considered invalid. it will default to 'stderr' +# if the parameter is invalid. # vid_codec = copy # 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 # 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 # this the image format that will be used when creating the thumbnails # for the videos clips recorded to rec_path. @@ -140,19 +157,25 @@ rec_ext = .mkv # img_thresh = 8000 # this parameter defines the score threshold from img_comp_cmd that will -# be considered motion. any motion events will queue up max_event_secs -# worth of video clips to be written out to rec_path. +# be considered an event of any kind. any video clips found with events +# 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 # 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. # post_cmd = move_the_ptz_camera.py # 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. -# note: the call to this command will be delayed if motion was detected. -# also, motion detection is paused while this command is running. +# note: the call to this command will be delayed if an event was detected. +# also, event detection is paused while this command is running. # rec_fps = 30 # 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 # 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 ### diff --git a/client/build.py b/client/build.py index fc43636..f4043b1 100755 --- a/client/build.py +++ b/client/build.py @@ -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"): 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/xcbglintegrations", "app_dir/xcbglintegrations") verbose_copy(qt_bin + "/../plugins/multimedia", "app_dir/multimedia") 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("icons/main.svg", "app_dir/icons/scalable.svg") diff --git a/client/install.py b/client/install.py index 0360bf0..818f938 100755 --- a/client/install.py +++ b/client/install.py @@ -121,7 +121,8 @@ def local_install(app_target, app_name): verbose_copy("app_dir/xcbglintegrations", install_dir + "/xcbglintegrations") verbose_copy("app_dir/multimedia", install_dir + "/multimedia") 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) subprocess.run(["chmod", "644", "/usr/share/icons/hicolor/scalable/apps/jmc.svg"]) diff --git a/client/src/common.h b/client/src/common.h index fc6f6f1..6276bfa 100644 --- a/client/src/common.h +++ b/client/src/common.h @@ -66,7 +66,7 @@ #include #include -#define APP_VERSION "1.0.0" +#define APP_VERSION "1.1" #define APP_NAME "JustMotion-Client" #define APP_TARGET "jmotion-client" #define DEFAULT_PLAYER "vlc --no-audio --no-video-title-show --no-osd --repeat pls.m3u8" diff --git a/client/src/main_widget.cpp b/client/src/main_widget.cpp index 227e308..ce60d40 100644 --- a/client/src/main_widget.cpp +++ b/client/src/main_widget.cpp @@ -43,6 +43,8 @@ MainWidget::MainWidget(shared_t *share, MainGui *parent) : QWidget(parent) sshOpts = new ssh_opt(); homeLayout = new QVBoxLayout(this); + homeLayout->addWidget(new QLabel(tr("Version: ") + APP_VERSION, this)); + genConnectWidget(); genSettingsWidget(); genSpacer(); diff --git a/client/src/playlist_widget.cpp b/client/src/playlist_widget.cpp index d0218f0..0b0367d 100644 --- a/client/src/playlist_widget.cpp +++ b/client/src/playlist_widget.cpp @@ -55,7 +55,7 @@ void PlaylistItem::checkConfExists() void PlaylistItem::loadImgFromFile(QString path) { - if (path.isEmpty() || !QFileInfo::exists(path)) + if (path.isEmpty() || !QFileInfo(path).isFile()) { path = ":/img/no_mdeia.png"; } @@ -67,7 +67,12 @@ void PlaylistItem::loadImgFromFile(QString path) 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 imgFile = randPickItem(imgList); diff --git a/server/JustVideo-Server.pro b/server/JustMotion-Server.pro similarity index 100% rename from server/JustVideo-Server.pro rename to server/JustMotion-Server.pro diff --git a/server/src/camera.cpp b/server/src/camera.cpp index da6b7d0..3325f9f 100644 --- a/server/src/camera.cpp +++ b/server/src/camera.cpp @@ -104,6 +104,8 @@ int Camera::start(const QString &conf) connect(thr3, &QThread::finished, this, &Camera::objMinusOne); connect(detLoop, &DetectLoop::starving, recLoop, &RecordLoop::restart); + connect(recLoop, &RecordLoop::started, detLoop, &DetectLoop::run); + connect(recLoop, &RecordLoop::finished, detLoop, &DetectLoop::terminate); thr1->start(); thr2->start(); diff --git a/server/src/common.cpp b/server/src/common.cpp index 760b6b9..ed00ef0 100644 --- a/server/src/common.cpp +++ b/server/src/common.cpp @@ -155,28 +155,31 @@ bool rdConf(const QString &filePath, shared_t *share) else { share->recordUri.clear(); + share->detectUri.clear(); share->postCmd.clear(); share->camName.clear(); share->buffPath.clear(); share->recPath.clear(); - share->retCode = 0; - share->imgThresh = 8000; - share->postSecs = 60; - share->evMaxSecs = 30; - share->evtMaxBytes = 10000000000; - share->conf = filePath; - share->outputType = "stderr"; - share->compCmd = "compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null"; - share->vidCodec = "copy"; - share->audCodec = "copy"; - share->streamExt = ".mkv"; - share->recExt = ".mkv"; - share->thumbExt = ".bmp"; - share->recFps = 30; - share->liveSecs = 160; - share->recScale = "1280:720"; - share->imgScale = "320:240"; + share->retCode = 0; + share->imgThresh = 8000; + share->postSecs = 300; + share->evtMaxBytes = 10000000000; + share->conf = filePath; + share->outputType = "stderr"; + share->compCmd = "compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null"; + share->vidCodec = "copy"; + share->audCodec = "copy"; + share->streamExt = ".ts"; + share->recExt = ".ts"; + share->thumbExt = ".jpg"; + share->imgExt = ".jpg"; + share->recFps = 30; + share->liveMins = 60; + share->liveClipSecs = 5; + share->secPerImg = 4; + share->grayImg = true; + share->recScale = "1280:720"; QString line; @@ -188,15 +191,16 @@ bool rdConf(const QString &filePath, shared_t *share) { rdLine("cam_name = ", line, &share->camName); rdLine("recording_uri = ", line, &share->recordUri); + rdLine("detect_uri = ", line, &share->detectUri); rdLine("buffer_path = ", line, &share->buffPath); rdLine("rec_path = ", line, &share->recPath); - rdLine("max_event_secs = ", line, &share->evMaxSecs); rdLine("post_secs = ", line, &share->postSecs); rdLine("post_cmd = ", line, &share->postCmd); rdLine("img_thresh = ", line, &share->imgThresh); rdLine("max_events_bytes = ", line, &share->evtMaxBytes); rdLine("img_comp_out = ", line, &share->outputType); rdLine("img_comp_cmd = ", line, &share->compCmd); + rdLine("img_ext = ", line, &share->imgExt); rdLine("vid_codec = ", line, &share->vidCodec); rdLine("aud_codec = ", line, &share->audCodec); 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("rec_fps = ", line, &share->recFps); rdLine("rec_scale = ", line, &share->recScale); - rdLine("img_scale = ", line, &share->imgScale); - rdLine("live_secs = ", line, &share->liveSecs); + rdLine("live_mins = ", line, &share->liveMins); + rdLine("live_clip_secs = ", line, &share->liveClipSecs); + rdLine("sec_per_image = ", line, &share->secPerImg); + rdLine("gray_image = ", line, &share->grayImg); } } while(!line.isEmpty()); @@ -218,9 +224,33 @@ bool rdConf(const QString &filePath, shared_t *share) extCorrection(share->streamExt); extCorrection(share->recExt); 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") { + qInfo() << "img_comp_out = " << share->outputType << " is not valid. must be stdout or stderr only. default set: stderr"; + share->outputType = "stderr"; } @@ -241,21 +271,47 @@ bool rdConf(const QString &filePath, shared_t *share) { 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; } +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) { if (del) diff --git a/server/src/common.h b/server/src/common.h index 8401d58..46991c1 100644 --- a/server/src/common.h +++ b/server/src/common.h @@ -32,11 +32,11 @@ using namespace std; -#define APP_VERSION "1.1" +#define APP_VERSION "1.2" #define APP_NAME "JustMotion" #define APP_TARGET "jmotion" -#define DATETIME_FMT "yyyyMMddhhmmss" -#define STRFTIME_FMT "%Y%m%d%H%M%S" +#define DATETIME_FMT "yyyyMMddhhmm" +#define STRFTIME_FMT "%Y%m%d%H%M" #define PREV_IMG "&prev&" #define NEXT_IMG "&next&" @@ -46,39 +46,37 @@ enum CmdExeType VID_LOOP }; -struct evt_t -{ - QList vidList; - QList imgList; -}; - struct shared_t { - evt_t recList; - QString conf; - QString recordUri; - QString buffPath; - QString postCmd; - QString camName; - QString recPath; - QString outputType; - QString compCmd; - QString vidCodec; - QString audCodec; - QString streamExt; - QString recExt; - QString thumbExt; - QString recScale; - QString imgScale; - quint64 evtMaxBytes; - int liveSecs; - int recFps; - int evMaxSecs; - int postSecs; - int imgThresh; - int retCode; + QStringList recList; + QString conf; + QString recordUri; + QString detectUri; + QString buffPath; + QString postCmd; + QString camName; + QString recPath; + QString outputType; + QString compCmd; + QString vidCodec; + QString audCodec; + QString streamExt; + QString recExt; + QString imgExt; + QString thumbExt; + QString recScale; + quint64 evtMaxBytes; + bool grayImg; + int liveMins; + int liveClipSecs; + int recFps; + int secPerImg; + int postSecs; + int imgThresh; + int retCode; }; +QString getStatLineFromProc(QProcess *proc); QString buildThreadCount(int count); QStringList lsFilesInDir(const QString &path, const QString &ext = QString()); QStringList lsDirsInDir(const QString &path); @@ -95,5 +93,6 @@ void rdLine(const QString ¶m, const QString &line, int *value); void rdLine(const QString ¶m, const QString &line, quint64 *value); void rdLine(const QString ¶m, const QString &line, bool *value); void extCorrection(QString &ext); +void addStrsToList(QStringList &strList, QStringList &add); #endif // COMMON_H diff --git a/server/src/detect_loop.cpp b/server/src/detect_loop.cpp index d0dd354..516b183 100644 --- a/server/src/detect_loop.cpp +++ b/server/src/detect_loop.cpp @@ -12,11 +12,12 @@ #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; - shared = sharedRes; + pcTimer = 0; + starvCnt = 0; + shared = sharedRes; + rerun = false; connect(thr, &QThread::started, this, &DetectLoop::init); connect(thr, &QThread::finished, this, &DetectLoop::deleteLater); @@ -24,59 +25,99 @@ DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QFi moveToThread(thr); } +DetectLoop::~DetectLoop() +{ + terminate(); + stopTimers(); + waitForFinished(); +} + void DetectLoop::init() { - pcTimer = new QTimer(this); - evtTimer = new QTimer(this); + pcTimer = new QTimer(this); + evtTimer = new QTimer(this); + detTimer = new QTimer(this); + mkdirTimer = new QTimer(this); + postProc = new QProcess(this); setupBuffDir(shared->buffPath); - connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak); + pcTimer->setInterval(shared->postSecs * 1000); + mkdirTimer->setInterval(60000); + detTimer->setInterval((shared->secPerImg * 2) * 1000); + evtTimer->setInterval(60000); + evtTimer->setSingleShot(true); - connect(this, &QFileSystemWatcher::directoryChanged, this, &DetectLoop::updated); + connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak); + connect(detTimer, &QTimer::timeout, this, &DetectLoop::exec); + connect(mkdirTimer, &QTimer::timeout, this, &DetectLoop::mkdirs); - pcTimer->start(shared->postSecs * 1000); - - if (shared->compCmd.toLower() != "null") - { - connect(evtTimer, &QTimer::timeout, this, &DetectLoop::reset); - - evtTimer->setSingleShot(true); - - eTimer.start(); - } - else - { - eTimer.invalidate(); - } - - addPath(shared->buffPath + "/vid"); + connect(this, &DetectLoop::started, this, &DetectLoop::startTimers); + connect(this, &DetectLoop::finished, this, &DetectLoop::stopTimers); } -void DetectLoop::updated(const QString &path) +void DetectLoop::startTimers() { - if (shared->compCmd.toLower() != "null") + pcTimer->start(); + detTimer->start(); + mkdirTimer->start(); +} + +void DetectLoop::stopTimers() +{ + starvCnt = 0; + + pcTimer->stop(); + detTimer->stop(); + mkdirTimer->stop(); + evtTimer->stop(); + + if (rerun) { - eTimer.start(); + 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 "; } - auto clips = lsFilesInDir(path, shared->streamExt); - auto index = clips.indexOf(vidBName); + ret += "-vf fps=1/" + QString::number(shared->secPerImg); - if (clips.size() - (index + 1) < 3) + if (shared->grayImg) { - thread()->sleep(1); + ret += ",format=gray "; } 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); + ret += " "; } + + ret += "img/" + QString(STRFTIME_FMT) + "/%S" + shared->imgExt; + + qInfo() << ret; + + return ret; } void DetectLoop::pcBreak() @@ -87,7 +128,14 @@ void DetectLoop::pcBreak() 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 { @@ -101,7 +149,7 @@ void DetectLoop::pcBreak() } 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()) { - 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; } 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) { auto args = parseArgs(shared->compCmd.toUtf8(), -1); @@ -149,98 +238,73 @@ QStringList DetectLoop::buildArgs(const QString &prev, const QString &next) return args; } -QStringList DetectLoop::buildSnapArgs(const QString &vidSrc, const QString &imgPath) -{ - QStringList ret; - - 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()) - { - return "OFF "; - } - else if (eTimer.elapsed() >= 5000) - { - emit starving(); - - return "STARVED"; - } - else - { - return "OK "; - } -} - -void DetectLoop::reset() -{ - evtOn = false; -} - 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()) + if (starvCnt >= STARV_LIMIT) { - qCritical() << "err: could not parse a executable name from img_comp_cmd: " << shared->compCmd; + if (detTimer->isActive()) + { + emit starving(); + } } else { - QProcess::execute("ffmpeg", snapArgsA); - QProcess::execute("ffmpeg", snapArgsB); + auto timeStamp = QDateTime::currentDateTime(); + auto path = shared->buffPath + "/img/" + timeStamp.toString(DATETIME_FMT); + auto imgList = lsFilesInDir(path, shared->imgExt); - if (QFile::exists(imgAPath) && QFile::exists(imgBPath) && (shared->compCmd.toLower() != "null")) + if (imgList.size() >= 2) { - QProcess extComp; + starvCnt = 0; - extComp.start(compArgs[0], compArgs.mid(1)); - extComp.waitForFinished(); - - float score = 0; - - if (shared->outputType == "stdout") + if ((shared->compCmd != "null") && (postProc->state() != QProcess::Running)) { - score = getFloatFromExe(extComp.readAllStandardOutput()); - } - else - { - score = getFloatFromExe(extComp.readAllStandardError()); - } + auto imgA = path + "/" + imgList[imgList.size() - 2]; + auto imgB = path + "/" + imgList[imgList.size() - 1]; + auto compArgs = buildArgs(imgA, imgB); - qInfo() << compArgs.join(" ") << " --result: " << QString::number(score); - - if ((score >= shared->imgThresh) || evtOn) - { - shared->recList.vidList.append(vidAPath); - shared->recList.vidList.append(vidBPath); - shared->recList.imgList.append(imgAPath); - shared->recList.imgList.append(imgBPath); - - if (!evtTimer->isActive()) + if (compArgs.isEmpty()) { - evtTimer->start(shared->evMaxSecs * 1000); + qCritical() << "err: could not parse a executable name from img_comp_cmd: " << shared->compCmd; + } + else + { + QProcess extComp; + + extComp.start(compArgs[0], compArgs.mid(1)); + extComp.waitForFinished(); + + float score = 0; + + if (shared->outputType == "stdout") + { + score = getFloatFromExe(extComp.readAllStandardOutput()); + } + else + { + score = getFloatFromExe(extComp.readAllStandardError()); + } + + qInfo() << compArgs.join(" ") << " --result: " << QString::number(score); + + if ((score >= shared->imgThresh) || evtTimer->isActive()) + { + if (!shared->recList.contains(timeStamp.toString(DATETIME_FMT))) + { + shared->recList.append(timeStamp.toString(DATETIME_FMT)); + } + + if (!evtTimer->isActive()) + { + evtTimer->start(); + } + } } } } + else + { + starvCnt++; + } } - - vidAPath.clear(); - vidBPath.clear(); } diff --git a/server/src/detect_loop.h b/server/src/detect_loop.h index 1e0b979..8c5043d 100644 --- a/server/src/detect_loop.h +++ b/server/src/detect_loop.h @@ -15,38 +15,46 @@ #include "common.h" -class DetectLoop : public QFileSystemWatcher +#define STARV_LIMIT 24 + +class DetectLoop : public QProcess { Q_OBJECT private: - QString vidAPath; - QString vidBPath; - QString vidAName; - QString vidBName; - QTimer *pcTimer; - QTimer *evtTimer; - QElapsedTimer eTimer; - shared_t *shared; - bool evtOn; + QTimer *pcTimer; + QTimer *evtTimer; + QTimer *detTimer; + QTimer *mkdirTimer; + shared_t *shared; + QProcess *postProc; + uint starvCnt; + bool rerun; float getFloatFromExe(const QByteArray &line); + QString camCmdFromConf(); QStringList buildArgs(const QString &prev, const QString &next); - QStringList buildSnapArgs(const QString &vidSrc, const QString &imgPath); private slots: + void exec(); void init(); - void reset(); void pcBreak(); - void updated(const QString &path); + void mkdirs(); + void startTimers(); + void stopTimers(); + +public slots: + + void run(); public: explicit DetectLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr); - void exec(); + ~DetectLoop(); + QString statusLine(); signals: diff --git a/server/src/event_loop.cpp b/server/src/event_loop.cpp index e5fbf99..616af11 100644 --- a/server/src/event_loop.cpp +++ b/server/src/event_loop.cpp @@ -15,7 +15,7 @@ EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QObject(parent) { shared = sharedRes; - heartBeat = 1; + heartBeat = 3; loopTimer = 0; 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() { QString text; auto txt = QTextStream(&text); - 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; + txt << "#EXTM3U" << Qt::endl; - auto vids = lsFilesInDir(shared->recPath + "/vid", shared->recExt); - - for (auto vid : vids) - { - txt << "#EXTINF:4.000000," << Qt::endl; - txt << "vid/" << vid << Qt::endl; - } + deepListVids(text); QFile file(shared->recPath + "/pls.m3u8"); @@ -74,73 +82,83 @@ void EventLoop::updatePls() 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(); - auto imgPath = shared->recList.imgList.takeFirst(); - auto vidDst = shared->recPath + "/vid/" + QFileInfo(vidPath).fileName(); - auto imgDst = shared->recPath + "/img/" + QFileInfo(imgPath).baseName() + shared->thumbExt; + QFile::copy(src + QDir::separator() + vidName, dst + QDir::separator() + vidName); + } +} - qInfo() << "---EVENT WRITEOUT START---"; - qInfo() << "source files: "; - qInfo() << vidPath; - qInfo() << imgPath; - qInfo() << "destination files: "; - qInfo() << vidDst; - qInfo() << imgDst; +void EventLoop::copyDirImg(const QString &src, const QString &dst) +{ + auto imgList = lsFilesInDir(src, shared->imgExt); - mkPath(shared->recPath + "/vid"); - mkPath(shared->recPath + "/img"); + for (auto imgName : imgList) + { + auto srcImgPath = src + QDir::separator() + imgName; + auto dstImgPath = dst + QDir::separator() + imgName; - if (!QFile::copy(vidPath, vidDst)) + if (imgName.endsWith(shared->thumbExt)) { - qCritical() << "err: file copy operation failed." << Qt::endl; - - qInfo() << "exists?: " << vidPath << ": " << QFileInfo::exists(vidPath); - } - - if (imgPath.endsWith(shared->thumbExt)) - { - QFile::copy(imgPath, imgDst); + QFile::copy(srcImgPath, dstImgPath); } else { QStringList args; - args << imgPath; - args << imgDst; + args << srcImgPath; + args << dstImgPath; 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; - - qInfo() << "---EVENT WRITEOUT END---"; } 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(); } - // 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(); } @@ -148,17 +166,15 @@ void EventLoop::exec() // 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) { - 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; - auto imgFile = shared->recPath + "/img/" + nameOnly + shared->thumbExt; - - QFile::remove(vidFile); - QFile::remove(imgFile); + QDir(vidDir).removeRecursively(); + QDir(imgDir).removeRecursively(); names.removeFirst(); diff --git a/server/src/event_loop.h b/server/src/event_loop.h index 1125dd8..d0bc31a 100644 --- a/server/src/event_loop.h +++ b/server/src/event_loop.h @@ -31,6 +31,9 @@ private: int heartBeat; void updatePls(); + void deepListVids(QString &plsText); + void copyDirImg(const QString &src, const QString &dst); + void copyDirVid(const QString &src, const QString &dst); public: diff --git a/server/src/main.cpp b/server/src/main.cpp index 2530f41..c6620d2 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -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) << " recorded footage and config files will remain." << 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) << "-r : 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) << "-s : same as -d except it is non-blocking for starting all camera instances" << Qt::endl; + QTextStream(stdout) << " via systemd. same as 'sudo systemctl start " << APP_TARGET << "'" << Qt::endl; } int main(int argc, char** argv) @@ -81,7 +81,7 @@ int main(int argc, char** argv) { 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 { @@ -92,11 +92,11 @@ int main(int argc, char** argv) 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); @@ -116,9 +116,9 @@ int main(int argc, char** argv) QFile::remove(procFile); 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 { diff --git a/server/src/record_loop.cpp b/server/src/record_loop.cpp index c871765..0d7e94d 100644 --- a/server/src/record_loop.cpp +++ b/server/src/record_loop.cpp @@ -23,12 +23,15 @@ RecordLoop::RecordLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QPr connect(this, &RecordLoop::readyReadStandardOutput, this, &RecordLoop::resetTime); connect(this, &RecordLoop::readyReadStandardError, this, &RecordLoop::resetTime); connect(this, &RecordLoop::started, this, &RecordLoop::resetTime); + connect(this, &RecordLoop::finished, this, &RecordLoop::restart); moveToThread(thr); } RecordLoop::~RecordLoop() { + disconnect(this, &RecordLoop::finished, this, &RecordLoop::restart); + terminate(); waitForFinished(); } @@ -36,16 +39,39 @@ RecordLoop::~RecordLoop() void RecordLoop::init() { checkTimer = new QTimer(this); + mkdirTimer = new QTimer(this); + sync = false; connect(checkTimer, &QTimer::timeout, this, &RecordLoop::restart); + connect(mkdirTimer, &QTimer::timeout, this, &RecordLoop::mkdirs); - checkTimer->setSingleShot(true); - checkTimer->setInterval(3000); + connect(this, &RecordLoop::finished, mkdirTimer, &QTimer::stop); + 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); 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() { 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 + " "; } + auto maxClips = (60 / shared->liveClipSecs) * shared->liveMins; + ret += "-vcodec " + shared->vidCodec + " "; 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"; qInfo() << ret; @@ -72,44 +101,66 @@ QString RecordLoop::camCmdFromConf() QString RecordLoop::statusLine() { - if (state() == QProcess::Running) + if (!sync) { - return "OK "; + return "WAIT "; } else { - return "FAIL"; + return getStatLineFromProc(this); } } +void RecordLoop::synced() +{ + sync = true; + + restart(); +} + void RecordLoop::resetTime() { + // reset the stall timer to prevent it from timing out when ffmpeg doesn't + // need to be restarted. + checkTimer->start(); } void RecordLoop::restart() { + auto sec = QTime::currentTime().second(); + if (state() == QProcess::Running) { terminate(); - waitForFinished(); } - - auto cmdLine = camCmdFromConf(); - auto args = parseArgs(cmdLine.toUtf8(), -1); - - //qInfo() << "start recording command: " << cmdLine; - - if (args.isEmpty()) + else if (!sync && (sec != 0)) { - qCritical() << "err: couldn't parse a program name"; + // start recording when the seconds mark hit zero in real life. + + QTimer::singleShot((60 - sec) * 1000, this, &RecordLoop::synced); } else { - setWorkingDirectory(shared->buffPath); - setProgram(args[0]); - setArguments(args.mid(1)); + auto cmdLine = camCmdFromConf(); + auto args = parseArgs(cmdLine.toUtf8(), -1); - start(); + //qInfo() << "start recording command: " << cmdLine; + + if (args.isEmpty()) + { + qCritical() << "err: couldn't parse a program name"; + } + else + { + setWorkingDirectory(shared->buffPath); + setProgram(args[0]); + setArguments(args.mid(1)); + mkdirs(); + + mkdirTimer->start(); + + start(); + } } } diff --git a/server/src/record_loop.h b/server/src/record_loop.h index 494a779..056f14d 100644 --- a/server/src/record_loop.h +++ b/server/src/record_loop.h @@ -23,17 +23,22 @@ private slots: void init(); void resetTime(); + void mkdirs(); private: shared_t *shared; QTimer *checkTimer; + QTimer *mkdirTimer; + bool sync; QString camCmdFromConf(); + QString nearest15mins(QString &sec); public slots: void restart(); + void synced(); public: