Compare commits

...

13 Commits

Author SHA1 Message Date
zii
045aa25f6d v3.6 - release to master 2025-02-23 09:32:35 -05:00
zii
492fa88585 v3.6.t10
-fixed default magick command to work with new versions that doesn't
 actually have 'magick' in it.
2025-02-23 09:24:35 -05:00
zii
5093ee6c8b v3.6.t9
-updated ramdisk class so the project is compilable on older qt
 builds < v6.5.
-more fixes to build.py to work with newer versions of qt and
 less manual steps to take.
2025-02-02 16:01:48 -05:00
zii
f4b2f0ce72 -updated build.py for proper qt6 support 2025-02-02 09:36:15 -05:00
zii
e0a6294e87 v3.6.t8
-created an internal ram disk to buffer streaming from the cameras
-the internal ram disk now directly send finished files to detect
 loop instead of relying on directory listing
2025-02-01 09:59:19 -05:00
zii
1a76159b91 v3.6.t7
-previous 3.6.tx commits are deemed failures, rolled back to v3.5
 code from master.
2024-11-21 19:06:59 -05:00
zii
bb998da739 v3.6.t6
-ffmpeg stall has re-surfaced, not entirely sure what causes it
 added proper logic that will actually start the restart timer
 when ffmpeg is running.
2024-10-09 20:55:23 -04:00
zii
baa6c0f217 v3.6.t5
-fixed the ffmpmeg cmd line formatting so it stops rewirting into
 the same video file.
2024-10-09 15:48:29 -04:00
zii
69bb706b6b v3.6.t4
-ffmpeg arbitrarily limits %020d. backed it down %014d, not sure if
 that is true limit. 14 was tessted to work.

-added file size checking in detect loop to prevent it from
 processesing unfinished video clips.
2024-10-09 14:39:11 -04:00
zii
7ba06ab92b v3.6.t3
-fixed some more issues with the increment naming.
2024-10-09 13:44:25 -04:00
zii
d9cdd30877 v3.6.t2
-fixed an issue that prevented the name seed value from
 incrementing
2024-10-09 13:00:35 -04:00
zii
ba54b5b44a v3.6.t1
-updated the way the video clips from the live stream are named to
 simple unsigned int number increments. doing this makes the file
 names much more predictable and removes the need for filesystem
 monitoring.
2024-10-09 12:38:49 -04:00
zii
88ea1086f6 v3.6.t1
-all recordings, clips and snapshots are now base on UTC naming
 scheme for easy name prediction for client apps.

-detect loop no longer list dirs for files or monitors for dir
 changes. it will now predict video clip names base on current
 UTC time.
2024-08-04 14:42:13 -04:00
18 changed files with 1311 additions and 297 deletions

View File

@ -3,6 +3,8 @@ QT -= gui
CONFIG += c++11 console CONFIG += c++11 console
CONFIG -= app_bundle CONFIG -= app_bundle
LIBS += -lfuse3
TARGET = build/jmotion TARGET = build/jmotion
OBJECTS_DIR = build OBJECTS_DIR = build
MOC_DIR = build MOC_DIR = build
@ -14,6 +16,7 @@ HEADERS += \
src/detect_loop.h \ src/detect_loop.h \
src/event_loop.h \ src/event_loop.h \
src/proc_control.h \ src/proc_control.h \
src/ram_disk.h \
src/record_loop.h src/record_loop.h
SOURCES += \ SOURCES += \
@ -22,5 +25,6 @@ SOURCES += \
src/detect_loop.cpp \ src/detect_loop.cpp \
src/event_loop.cpp \ src/event_loop.cpp \
src/proc_control.cpp \ src/proc_control.cpp \
src/ram_disk.cpp \
src/record_loop.cpp \ src/record_loop.cpp \
src/main.cpp src/main.cpp

View File

@ -19,13 +19,33 @@ def get_app_name(text):
def get_qt_path(): def get_qt_path():
try: try:
return str(subprocess.check_output(["qtpaths", "--binaries-dir"]), 'utf-8').strip() if os.path.exists("/usr/lib/qt6/bin"):
return "/usr/lib/qt6/bin"
return str(subprocess.check_output(["qtpaths6", "--binaries-dir"]), 'utf-8').strip()
except: except:
print("A direct call to 'qtpaths' has failed so automatic retrieval of the QT bin folder is not possible.") print("A direct call to 'qtpaths6' has failed so automatic retrieval of the QT bin folder is not possible.")
return input("Please enter the QT bin path (leave blank to cancel the build): ") return input("Please enter the QT bin path (leave blank to cancel the build): ")
def get_qt_lib_path(qt_bin):
output = str(subprocess.check_output([qt_bin + "/qtpaths6", "--query"]), 'utf-8').strip()
lines = output.split("\n")
ret = ""
for line in lines:
if line.startswith("QT_HOST_LIBS:"):
ret = line[13:]
if ret == "":
print("failed automatic retrieval of the QT lib folder.")
return input("Please enter the QT lib path (leave blank to cancel the build): ")
else:
return ret
def get_qt_from_cli(): def get_qt_from_cli():
for arg in sys.argv: for arg in sys.argv:
if arg == "-qt_dir": if arg == "-qt_dir":
@ -90,7 +110,7 @@ def verbose_copy(src, dst):
else: else:
print("wrn: " + src + " does not exists. skipping.") print("wrn: " + src + " does not exists. skipping.")
def linux_build_app_dir(app_ver, app_name, app_target, qt_bin): def linux_build_app_dir(app_ver, app_name, app_target, qt_bin, qt_lib):
if not os.path.exists("app_dir/lib"): if not os.path.exists("app_dir/lib"):
os.makedirs("app_dir/lib") os.makedirs("app_dir/lib")
@ -118,12 +138,8 @@ def linux_build_app_dir(app_ver, app_name, app_target, qt_bin):
file_name = os.path.basename(src_file) file_name = os.path.basename(src_file)
verbose_copy(src_file, "app_dir/lib/" + file_name) verbose_copy(src_file, "app_dir/lib/" + file_name)
if "/usr/lib/x86_64-linux-gnu/qt6/bin" == qt_bin: verbose_copy(qt_lib + "/libQt6DBus.so.6", "app_dir/lib/libQt6DBus.so.6")
verbose_copy(qt_bin + "/../../libQt6DBus.so.6", "app_dir/lib/libQt6DBus.so.6")
else:
verbose_copy(qt_bin + "/../lib/libQt6DBus.so.6", "app_dir/lib/libQt6DBus.so.6")
verbose_copy("templates/linux_run_script.sh", "app_dir/" + app_target + ".sh") verbose_copy("templates/linux_run_script.sh", "app_dir/" + app_target + ".sh")
verbose_copy("templates/linux_uninstall.sh", "app_dir/uninstall.sh") verbose_copy("templates/linux_uninstall.sh", "app_dir/uninstall.sh")
@ -183,7 +199,7 @@ def platform_setup():
ins_packages = list_installed_packages() ins_packages = list_installed_packages()
like_distro = get_like_distro() like_distro = get_like_distro()
dep_pkgs_a = ["pkg-config", "make", "g++"] dep_pkgs_a = ["pkg-config", "make", "g++"]
dep_pkgs_b = ["ffmpeg", "libfuse-dev", "fuse3", "imagemagick"] dep_pkgs_b = ["ffmpeg", "libfuse3-dev", "libfuse-dev", "fuse3", "imagemagick", "qt6-base-dev"]
if not list_of_words_in_text(dep_pkgs_a, ins_packages) or not list_of_words_in_text(dep_pkgs_b, ins_packages): if not list_of_words_in_text(dep_pkgs_a, ins_packages) or not list_of_words_in_text(dep_pkgs_b, ins_packages):
if ("ubuntu" in like_distro) or ("debian" in like_distro) or ("linuxmint" in like_distro): if ("ubuntu" in like_distro) or ("debian" in like_distro) or ("linuxmint" in like_distro):
@ -218,10 +234,16 @@ def main():
qt_bin = get_qt_path() qt_bin = get_qt_path()
if qt_bin != "": if qt_bin != "":
qt_lib = get_qt_lib_path(qt_bin)
if qt_lib == "":
exit(1)
print("app_target = " + app_target) print("app_target = " + app_target)
print("app_version = " + app_ver) print("app_version = " + app_ver)
print("app_name = " + app_name) print("app_name = " + app_name)
print("qt_bin = " + qt_bin) print("qt_bin = " + qt_bin)
print("qt_lib = " + qt_lib)
cd() cd()
@ -241,7 +263,7 @@ def main():
info_file.write(app_ver + "\n") info_file.write(app_ver + "\n")
info_file.write(app_name + "\n") info_file.write(app_name + "\n")
linux_build_app_dir(app_ver, app_name, app_target, qt_bin) linux_build_app_dir(app_ver, app_name, app_target, qt_bin, qt_lib)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -39,7 +39,7 @@ def make_install_dir(path, false_on_fail):
return True return True
def make_app_dirs(app_target): def make_app_dirs(app_target):
return make_install_dir("/etc/" + app_target, True) and make_install_dir("/opt/" + app_target, True) and make_install_dir("/var/buffer", False) and make_install_dir("/var/footage", False) return make_install_dir("/etc/" + app_target, True) and make_install_dir("/opt/" + app_target, True) and make_install_dir("/var/local/" + app_target, False)
def replace_bin(binary, old_bin, new_bin, offs): def replace_bin(binary, old_bin, new_bin, offs):
while(True): while(True):
@ -123,13 +123,12 @@ def local_install(app_target, app_name):
subprocess.run(["chmod", "755", install_dir + "/uninstall.sh"]) subprocess.run(["chmod", "755", install_dir + "/uninstall.sh"])
subprocess.run(["chmod", "644", "/lib/systemd/system/" + app_target + ".service"]) subprocess.run(["chmod", "644", "/lib/systemd/system/" + app_target + ".service"])
subprocess.run(["chmod", "777", "/var/buffer"])
subprocess.run(["chmod", "777", "/var/footage"])
if not user_exists(app_target): if not user_exists(app_target):
subprocess.run(["sudo", "useradd", "-r", app_target]) subprocess.run(["sudo", "useradd", "-r", app_target])
subprocess.run(["sudo", "usermod", "-aG", "video", app_target]) subprocess.run(["sudo", "usermod", "-aG", "video", app_target])
subprocess.run(["chown", app_target + ":" + app_target, "/var/local/" + app_target])
subprocess.run(["systemctl", "start", app_target]) subprocess.run(["systemctl", "start", app_target])
subprocess.run(["systemctl", "enable", app_target]) subprocess.run(["systemctl", "enable", app_target])

View File

@ -12,13 +12,16 @@
#include "camera.h" #include "camera.h"
Camera::Camera(const QString &confFile, const QString &statDir, ProcControl *proc, QCoreApplication *parent) : QObject(nullptr) Camera::Camera(const shared_t &glbShare, const QString &confFile, const QString &statDir, RamDisk *ram, ProcControl *proc, QCoreApplication *parent) : QObject(nullptr)
{ {
Q_UNUSED(parent); Q_UNUSED(parent);
fsW = new QFileSystemWatcher(this); fsW = new QFileSystemWatcher(this);
statTimer = new QTimer(this); statTimer = new QTimer(this);
shared = glbShare;
procCon = proc;
statPath = statDir; statPath = statDir;
ramDisk = ram;
evtLoop = nullptr; evtLoop = nullptr;
detLoop = nullptr; detLoop = nullptr;
recLoop = nullptr; recLoop = nullptr;
@ -27,11 +30,9 @@ Camera::Camera(const QString &confFile, const QString &statDir, ProcControl *pro
statTimer->setInterval(5000); statTimer->setInterval(5000);
statTimer->start(); statTimer->start();
proc->objPlusOne();
connect(fsW, &QFileSystemWatcher::fileChanged, this, &Camera::confChanged); connect(fsW, &QFileSystemWatcher::fileChanged, this, &Camera::confChanged);
connect(proc, &ProcControl::prepForClose, this, &Camera::prepForDel); connect(proc, &ProcControl::prepForClose, this, &Camera::prepForDel);
connect(this, &Camera::destroyed, proc, &ProcControl::objMinusOne);
connect(statTimer, &QTimer::timeout, this, &Camera::updateStat); connect(statTimer, &QTimer::timeout, this, &Camera::updateStat);
start(confFile); start(confFile);
@ -49,6 +50,8 @@ void Camera::objMinusOne()
if (delOnZero) if (delOnZero)
{ {
procCon->objMinusOne(shared.conf);
QDir(shared.buffPath).removeRecursively(); QDir(shared.buffPath).removeRecursively();
deleteLater(); deleteLater();
@ -58,6 +61,7 @@ void Camera::objMinusOne()
void Camera::prepForDel() void Camera::prepForDel()
{ {
ramDisk->rmCam(shared.camName);
statTimer->blockSignals(true); statTimer->blockSignals(true);
delOnZero = true; delOnZero = true;
@ -76,16 +80,22 @@ void Camera::updateStat()
file.close(); file.close();
} }
int Camera::start(const QString &conf) void Camera::start(const QString &conf)
{ {
if (rdConf(conf, &shared)) if (!rdConf(conf, &shared))
{ {
setupBuffDir(shared.buffPath, true); deleteLater();
}
else
{
ramDisk->mkdir(QString("/" + shared.camName).toUtf8().data(), 0);
ramDisk->mkdir(QString("/" + shared.camName + "/vid").toUtf8().data(), 0);
ramDisk->mkdir(QString("/" + shared.camName + "/img").toUtf8().data(), 0);
if (!fsW->files().contains(conf)) ramDisk->addCam(shared.camName);
{ procCon->objPlusOne(shared.conf);
fsW->addPath(conf);
} if (!fsW->files().contains(conf)) fsW->addPath(conf);
thr1 = new QThread(nullptr); thr1 = new QThread(nullptr);
thr2 = new QThread(nullptr); thr2 = new QThread(nullptr);
@ -103,35 +113,29 @@ int Camera::start(const QString &conf)
connect(thr2, &QThread::finished, this, &Camera::objMinusOne); connect(thr2, &QThread::finished, this, &Camera::objMinusOne);
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(ramDisk, &RamDisk::watchedFileFlushed, detLoop, &DetectLoop::fileFlushed);
thr1->start(); thr1->start();
thr2->start(); thr2->start();
thr3->start(); thr3->start();
objCount = 3; objCount = 3;
}
return shared.retCode; auto global = QDir::cleanPath(shared.confPath) + "/global";
if (QFileInfo::exists(global))
{
rdConf(global, &shared, false);
}
}
} }
void Camera::confChanged(const QString &path) void Camera::confChanged(const QString &path)
{ {
emit stop(); Q_UNUSED(path);
if (!QFileInfo::exists(path)) prepForDel();
{
deleteLater();
}
else
{
auto ret = start(path);
if (ret != 0)
{
deleteLater();
}
}
} }
QString Camera::statusLine() QString Camera::statusLine()

View File

@ -18,6 +18,7 @@
#include "detect_loop.h" #include "detect_loop.h"
#include "record_loop.h" #include "record_loop.h"
#include "proc_control.h" #include "proc_control.h"
#include "ram_disk.h"
class Camera : public QObject class Camera : public QObject
{ {
@ -29,10 +30,12 @@ private:
EventLoop *evtLoop; EventLoop *evtLoop;
DetectLoop *detLoop; DetectLoop *detLoop;
RecordLoop *recLoop; RecordLoop *recLoop;
RamDisk *ramDisk;
QThread *thr1; QThread *thr1;
QThread *thr2; QThread *thr2;
QThread *thr3; QThread *thr3;
QTimer *statTimer; QTimer *statTimer;
ProcControl *procCon;
QString statPath; QString statPath;
shared_t shared; shared_t shared;
uint objCount; uint objCount;
@ -47,9 +50,9 @@ private slots:
public: public:
explicit Camera(const QString &confFile, const QString &statDir, ProcControl *proc, QCoreApplication *parent); explicit Camera(const shared_t &glbShare, const QString &confFile, const QString &statDir, RamDisk *ram, ProcControl *proc, QCoreApplication *parent);
int start(const QString &conf); void start(const QString &conf);
QString statusLine(); QString statusLine();
signals: signals:

View File

@ -102,11 +102,11 @@ void enforceMaxImages(shared_t *share)
void enforceMaxClips(shared_t *share) void enforceMaxClips(shared_t *share)
{ {
auto names = lsFilesInDir(share->buffPath + "/live", share->streamExt); auto names = lsFilesInDir(share->buffPath + "/vid", share->streamExt);
while (names.size() > (share->liveSecs / 2)) while (names.size() > (share->liveSecs / 2))
{ {
QFile::remove(share->buffPath + "/live/" + names[0]); QFile::remove(share->buffPath + "/vid/" + names[0]);
names.removeFirst(); names.removeFirst();
} }
@ -128,6 +128,14 @@ void rdLine(const QString &param, const QString &line, int *value)
} }
} }
void rdLine(const QString &param, const QString &line, quint64 *value)
{
if (line.startsWith(param))
{
*value = line.mid(param.size()).trimmed().toULongLong();
}
}
void rdLine(const QString &param, const QString &line, bool *value) void rdLine(const QString &param, const QString &line, bool *value)
{ {
if (line.startsWith(param)) if (line.startsWith(param))
@ -158,42 +166,44 @@ bool mkPath(const QString &path)
return ret; return ret;
} }
bool rdConf(const QString &filePath, shared_t *share) bool rdConf(const QString &filePath, shared_t *share, bool reset)
{ {
auto ret = true;
QFile varFile(filePath); QFile varFile(filePath);
if (!varFile.open(QFile::ReadOnly)) if (!varFile.open(QFile::ReadOnly))
{ {
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;
ret = false;
} }
else else
{ {
share->recordUri.clear(); if (reset)
share->postCmd.clear(); {
share->camName.clear(); share->recordUri.clear();
share->buffPath.clear(); share->postCmd.clear();
share->recPath.clear(); share->camName.clear();
share->recPath.clear();
share->retCode = 0; share->imgThresh = 8000;
share->imgThresh = 8000; share->maxEvents = 30;
share->maxEvents = 30; share->postSecs = 60;
share->skipCmd = false; share->evMaxSecs = 30;
share->postSecs = 60; share->conf = filePath;
share->evMaxSecs = 30; share->outputType = "stderr";
share->conf = filePath; share->compCmd = "compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null";
share->outputType = "stderr"; share->vidCodec = "copy";
share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null"; share->audCodec = "copy";
share->vidCodec = "copy"; share->streamExt = ".mkv";
share->audCodec = "copy"; share->recExt = ".mkv";
share->streamExt = ".mkv"; share->thumbExt = ".jpg";
share->recExt = ".mkv"; share->recFps = 30;
share->thumbExt = ".jpg"; share->liveSecs = 80;
share->recFps = 30; share->recScale = "1280:720";
share->liveSecs = 80; share->imgScale = "320:240";
share->recScale = "1280:720"; }
share->imgScale = "320:240";
QString line; QString line;
@ -205,7 +215,6 @@ 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("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("max_event_secs = ", line, &share->evMaxSecs);
rdLine("post_secs = ", line, &share->postSecs); rdLine("post_secs = ", line, &share->postSecs);
@ -236,29 +245,27 @@ bool rdConf(const QString &filePath, shared_t *share)
extCorrection(share->recExt); extCorrection(share->recExt);
extCorrection(share->thumbExt); extCorrection(share->thumbExt);
share->buffPath = QDir::cleanPath(share->varPath) + "/stream/" + share->camName;
if (share->outputType != "stdout" && share->outputType != "stderr") if (share->outputType != "stdout" && share->outputType != "stderr")
{ {
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()) if (share->recPath.isEmpty())
{ {
share->recPath = "/var/footage/" + share->camName; share->recPath = QDir::cleanPath(share->varPath) + "/footage/" + share->camName;
} }
else else
{ {
share->recPath = QDir::cleanPath(share->recPath); share->recPath = QDir::cleanPath(share->recPath);
} }
if (!QFileInfo::exists(share->recPath))
{
mkPath(share->recPath);
}
if (share->liveSecs < 10) if (share->liveSecs < 10)
{ {
share->liveSecs = 10; share->liveSecs = 10;
@ -270,30 +277,7 @@ bool rdConf(const QString &filePath, shared_t *share)
} }
} }
return share->retCode == 0; return ret;
}
void setupBuffDir(const QString &path, bool del)
{
if (del)
{
QDir(path).removeRecursively();
}
if (!QFileInfo::exists(path))
{
mkPath(path);
}
if (!QFileInfo::exists(path + "/live"))
{
mkPath(path + "/live");
}
if (!QFileInfo::exists(path + "/img"))
{
mkPath(path + "/img");
}
} }
QString buildThreadCount(int count) QString buildThreadCount(int count)

View File

@ -13,6 +13,8 @@
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
#define FUSE_USE_VERSION 31
#include <QCoreApplication> #include <QCoreApplication>
#include <QProcess> #include <QProcess>
#include <QTextStream> #include <QTextStream>
@ -28,17 +30,25 @@
#include <QFileSystemWatcher> #include <QFileSystemWatcher>
#include <QDebug> #include <QDebug>
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QMutex>
#include <iostream> #include <iostream>
#include <fuse3/fuse.h>
#include <fuse3/fuse_lowlevel.h>
#include <fuse3/fuse_common.h>
#include <errno.h>
#include <cstring>
#include <unistd.h>
using namespace std; using namespace std;
#define APP_VERSION "3.5" #define APP_VERSION "3.6"
#define APP_NAME "JustMotion" #define APP_NAME "JustMotion"
#define APP_TARGET "jmotion" #define APP_TARGET "jmotion"
#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 PREV_IMG "&prev&" #define PREV_IMG "&prev&"
#define NEXT_IMG "&next&" #define NEXT_IMG "&next&"
#define BLK_SIZE 262144
enum CmdExeType enum CmdExeType
{ {
@ -46,22 +56,41 @@ enum CmdExeType
VID_LOOP VID_LOOP
}; };
class ProcControl;
class Camera;
struct evt_t struct evt_t
{ {
QList<QString> vidList; QStringList vidList;
QString timeStamp; QString timeStamp;
QString imgPath; QString imgPath;
float score; float score;
bool inQue; bool inQue;
int queAge; int queAge;
};
class file_t
{
public:
quint64 ino;
quint64 ctime;
quint64 mtime;
quint32 mode;
qint32 blks;
QByteArray bytes;
}; };
struct shared_t struct shared_t
{ {
QList<evt_t> recList; QList<evt_t> recList;
QString rdMnt;
QString conf; QString conf;
QString confPath;
QString recordUri; QString recordUri;
QString buffPath; QString buffPath;
QString varPath;
QString postCmd; QString postCmd;
QString camName; QString camName;
QString recPath; QString recPath;
@ -74,8 +103,7 @@ struct shared_t
QString thumbExt; QString thumbExt;
QString recScale; QString recScale;
QString imgScale; QString imgScale;
bool singleTenant; quint64 maxMemSize;
bool skipCmd;
int liveSecs; int liveSecs;
int recFps; int recFps;
int evMaxSecs; int evMaxSecs;
@ -85,6 +113,8 @@ struct shared_t
int retCode; int retCode;
}; };
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);
@ -92,11 +122,11 @@ QStringList listFacingFiles(const QString &path, const QString &ext, const QDate
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs); QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs); QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
QStringList parseArgs(const QByteArray &data, int maxArgs, int *pos = nullptr); 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, bool reset = true);
bool mkPath(const QString &path); bool mkPath(const QString &path);
void setupBuffDir(const QString &path, bool del = false);
void rdLine(const QString &param, const QString &line, QString *value); void rdLine(const QString &param, const QString &line, QString *value);
void rdLine(const QString &param, const QString &line, int *value); void rdLine(const QString &param, const QString &line, int *value);
void rdLine(const QString &param, const QString &line, quint64 *value);
void rdLine(const QString &param, const QString &line, bool *value); void rdLine(const QString &param, const QString &line, bool *value);
void enforceMaxEvents(shared_t *share); void enforceMaxEvents(shared_t *share);
void enforceMaxImages(shared_t *share); void enforceMaxImages(shared_t *share);

View File

@ -12,10 +12,11 @@
#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) : QObject(parent)
{ {
pcTimer = 0; pcTimer = 0;
shared = sharedRes; shared = sharedRes;
starved = true;
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);
@ -25,15 +26,15 @@ DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : QFi
void DetectLoop::init() void DetectLoop::init()
{ {
pcTimer = new QTimer(this); pcTimer = new QTimer(this);
checkTimer = new QTimer(this);
connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak); connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak);
connect(this, &QFileSystemWatcher::directoryChanged, this, &DetectLoop::updated); connect(checkTimer, &QTimer::timeout, this, &DetectLoop::eTimeOut);
pcTimer->start(shared->postSecs * 1000); pcTimer->start(shared->postSecs * 1000);
checkTimer->start(5000);
setupBuffDir(shared->buffPath); checkTimer->setSingleShot(true);
addPath(shared->buffPath + "/live");
} }
void DetectLoop::reset() void DetectLoop::reset()
@ -47,35 +48,19 @@ void DetectLoop::reset()
eventQue.timeStamp.clear(); eventQue.timeStamp.clear();
} }
void DetectLoop::updated(const QString &path) void DetectLoop::eTimeOut()
{ {
eTimer.start(); starved = true;
auto clips = lsFilesInDir(path, shared->streamExt); emit starving();
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())
{ {
checkTimer->stop();
qInfo() << "---POST_BREAK---"; qInfo() << "---POST_BREAK---";
if (eventQue.inQue) if (eventQue.inQue)
@ -97,6 +82,8 @@ void DetectLoop::pcBreak()
QProcess::execute(args[0], args.mid(1)); QProcess::execute(args[0], args.mid(1));
} }
} }
checkTimer->start();
} }
} }
@ -161,10 +148,8 @@ QStringList DetectLoop::buildSnapArgs(const QString &vidSrc, const QString &imgP
QString DetectLoop::statusLine() QString DetectLoop::statusLine()
{ {
if (eTimer.elapsed() >= 5000) if (starved)
{ {
emit starving();
return "STARVED"; return "STARVED";
} }
else else
@ -173,81 +158,95 @@ QString DetectLoop::statusLine()
} }
} }
void DetectLoop::exec() bool DetectLoop::pathFilter(const QString &path, const QString &subName)
{ {
auto imgAPath = shared->buffPath + "/img/" + QFileInfo(vidAPath).baseName() + ".bmp"; return path.startsWith("/" + shared->camName + "/" + subName) && path.endsWith(shared->streamExt);
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()) void DetectLoop::fileFlushed(const QString &path)
{
if (pathFilter(path, "vid"))
{ {
qCritical() << "err: could not parse a executable name from img_comp_cmd: " << shared->compCmd; if (vidAPath.isEmpty())
}
else
{
QProcess::execute("ffmpeg", snapArgsA);
QProcess::execute("ffmpeg", snapArgsB);
if (QFile::exists(imgAPath) && QFile::exists(imgBPath))
{ {
QProcess extComp; vidAPath = shared->varPath + "/stream" + path;
imgAPath = QDir::cleanPath(shared->buffPath) + QDir::separator() + QString("img") + QDir::separator() + QFileInfo(vidAPath).baseName() + ".bmp";
extComp.start(compArgs[0], compArgs.mid(1)); QProcess::execute("ffmpeg", buildSnapArgs(vidAPath, imgAPath));
extComp.waitForFinished(); }
else
{
auto vidBPath = shared->varPath + "/stream" + path;
auto imgBPath = QDir::cleanPath(shared->buffPath) + QDir::separator() + QString("img") + QDir::separator() + QFileInfo(vidBPath).baseName() + ".bmp";
float score = 0; QProcess::execute("ffmpeg", buildSnapArgs(vidBPath, imgBPath));
if (shared->outputType == "stdout") starved = false;
checkTimer->start();
auto compArgs = buildArgs(imgAPath, imgBPath);
if (compArgs.isEmpty())
{ {
score = getFloatFromExe(extComp.readAllStandardOutput()); qCritical() << "err: could not parse a executable name from img_comp_cmd: " << shared->compCmd;
} }
else else
{ {
score = getFloatFromExe(extComp.readAllStandardError()); QProcess extComp;
}
qInfo() << compArgs.join(" ") << " --result: " << QString::number(score); extComp.start(compArgs[0], compArgs.mid(1));
extComp.waitForFinished();
if (eventQue.inQue) float score = 0;
{
eventQue.queAge += 4;
if (eventQue.score < score) if (shared->outputType == "stdout")
{ {
eventQue.score = score; score = getFloatFromExe(extComp.readAllStandardOutput());
eventQue.imgPath = imgBPath; }
else
{
score = getFloatFromExe(extComp.readAllStandardError());
} }
eventQue.vidList.append(vidAPath); qInfo() << compArgs.join(" ") << " --result: " << QString::number(score);
eventQue.vidList.append(vidBPath);
if (eventQue.queAge >= shared->evMaxSecs) if (eventQue.inQue)
{ {
eventQue.inQue = false; eventQue.queAge += 2;
shared->recList.append(eventQue); if (eventQue.score < score)
{
eventQue.score = score;
eventQue.imgPath = imgBPath;
}
reset(); eventQue.vidList.append(vidBPath);
if (eventQue.queAge >= shared->evMaxSecs)
{
shared->recList.append(eventQue);
reset();
}
}
else if (score >= shared->imgThresh)
{
qInfo() << "--threshold_meet: " << QString::number(shared->imgThresh);
eventQue.score = score;
eventQue.imgPath = imgBPath;
eventQue.inQue = true;
eventQue.queAge = 0;
eventQue.timeStamp = QDateTime::currentDateTime().toString(DATETIME_FMT);
eventQue.vidList.append(vidAPath);
eventQue.vidList.append(vidBPath);
} }
} }
else if (score >= shared->imgThresh)
{
qInfo() << "--threshold_meet: " << QString::number(shared->imgThresh);
eventQue.score = score; vidAPath = vidBPath;
eventQue.imgPath = imgBPath; imgAPath = imgBPath;
eventQue.inQue = true;
eventQue.queAge = 0;
eventQue.timeStamp = QDateTime::currentDateTime().toString(DATETIME_FMT);
eventQue.vidList.append(vidAPath);
eventQue.vidList.append(vidBPath);
}
} }
} }
vidAPath.clear();
vidBPath.clear();
} }

View File

@ -15,7 +15,7 @@
#include "common.h" #include "common.h"
class DetectLoop : public QFileSystemWatcher class DetectLoop : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -23,32 +23,37 @@ private:
QString vidAPath; QString vidAPath;
QString vidBPath; QString vidBPath;
QString vidAName; QString imgAPath;
QString vidBName; QString imgBPath;
QStringList prevClips;
QTimer *pcTimer; QTimer *pcTimer;
QTimer *checkTimer;
QElapsedTimer eTimer; QElapsedTimer eTimer;
evt_t eventQue; evt_t eventQue;
shared_t *shared; shared_t *shared;
bool starved;
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); QStringList buildSnapArgs(const QString &vidSrc, const QString &imgPath);
bool pathFilter(const QString &path, const QString &subName);
private slots: private slots:
void init(); void init();
void reset(); void reset();
void pcBreak(); void pcBreak();
void updated(const QString &path); void eTimeOut();
public: public:
explicit DetectLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr); explicit DetectLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
void exec();
QString statusLine(); QString statusLine();
public slots:
void fileFlushed(const QString &path);
signals: signals:
void starving(); void starving();

View File

@ -28,27 +28,17 @@ void EventLoop::init()
{ {
loopTimer = new QTimer(this); loopTimer = new QTimer(this);
connect(loopTimer, &QTimer::timeout, this, &EventLoop::loopSlot); connect(loopTimer, &QTimer::timeout, this, &EventLoop::exec);
loopTimer->setSingleShot(false); loopTimer->setSingleShot(false);
loopTimer->start(heartBeat * 1000); loopTimer->start(heartBeat * 1000);
loopSlot();
}
void EventLoop::loopSlot()
{
if (!exec())
{
loopTimer->stop(); QCoreApplication::exit(shared->retCode);
}
} }
bool EventLoop::wrOutVod(const evt_t &event) bool EventLoop::wrOutVod(const evt_t &event)
{ {
auto ret = false; auto ret = false;
auto cnt = 0; auto cnt = 0;
auto concat = shared->buffPath + "/live/" + event.timeStamp + ".ctmp"; auto concat = shared->buffPath + "/vid/" + event.timeStamp + ".ctmp";
QFile file(concat, this); QFile file(concat, this);
@ -110,7 +100,7 @@ QString EventLoop::statusLine()
} }
} }
bool EventLoop::exec() void EventLoop::exec()
{ {
enforceMaxEvents(shared); enforceMaxEvents(shared);
enforceMaxImages(shared); enforceMaxImages(shared);
@ -122,6 +112,8 @@ bool EventLoop::exec()
qInfo() << "attempting write out of event: " << event.timeStamp; qInfo() << "attempting write out of event: " << event.timeStamp;
event.vidList.removeDuplicates();
if (wrOutVod(event)) if (wrOutVod(event))
{ {
QStringList args; QStringList args;
@ -133,6 +125,4 @@ bool EventLoop::exec()
QProcess::execute("magick", args); QProcess::execute("magick", args);
} }
} }
return shared->retCode == 0;
} }

View File

@ -22,7 +22,7 @@ class EventLoop : public QObject
private slots: private slots:
void init(); void init();
void loopSlot(); void exec();
private: private:
@ -36,7 +36,6 @@ public:
explicit EventLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr); explicit EventLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr);
bool exec();
QString statusLine(); QString statusLine();
}; };

View File

@ -38,8 +38,14 @@ int main(int argc, char** argv)
QCoreApplication::setApplicationName(APP_NAME); QCoreApplication::setApplicationName(APP_NAME);
QCoreApplication::setApplicationVersion(APP_VERSION); QCoreApplication::setApplicationVersion(APP_VERSION);
shared_t globalConf;
globalConf.confPath = "/etc/" + QString(APP_TARGET);
globalConf.varPath = "/var/local/" + QString(APP_TARGET);
globalConf.rdMnt = globalConf.varPath + "/stream";
globalConf.maxMemSize = 4000000000;
auto args = QCoreApplication::arguments(); auto args = QCoreApplication::arguments();
auto etcDir = "/etc/" + QString(APP_TARGET);
auto staDir = QDir::tempPath() + "/" + APP_TARGET + "-stats"; auto staDir = QDir::tempPath() + "/" + APP_TARGET + "-stats";
auto procFile = QDir::tempPath() + "/" + APP_TARGET + "-proc"; auto procFile = QDir::tempPath() + "/" + APP_TARGET + "-proc";
auto ret = 0; auto ret = 0;
@ -49,9 +55,19 @@ int main(int argc, char** argv)
mkPath(staDir); mkPath(staDir);
} }
if (!QFileInfo::exists(globalConf.rdMnt))
{
mkPath(globalConf.rdMnt);
}
if (!QFileInfo::exists(globalConf.varPath + "/footage"))
{
mkPath(globalConf.varPath + "/footage");
}
if (args.contains("-h")) if (args.contains("-h"))
{ {
showHelp(etcDir); showHelp(globalConf.confPath);
} }
else if (args.contains("-v")) else if (args.contains("-v"))
{ {
@ -59,22 +75,11 @@ int main(int argc, char** argv)
} }
else if (args.contains("-d")) else if (args.contains("-d"))
{ {
auto confs = lsFilesInDir(etcDir); auto proc = new ProcControl(procFile, staDir, &globalConf, &app);
auto proc = new ProcControl(procFile, staDir, &app);
if (!proc->init()) proc->startApp();
{
ret = EACCES;
}
else
{
for (auto &&conf : confs)
{
new Camera(etcDir + "/" + conf, staDir, proc, &app);
}
ret = app.exec(); ret = app.exec();
}
} }
else if (args.contains("-u")) else if (args.contains("-u"))
{ {
@ -121,7 +126,7 @@ int main(int argc, char** argv)
} }
else else
{ {
showHelp(etcDir); showHelp(globalConf.confPath);
} }
return ret; return ret;

View File

@ -12,59 +12,100 @@
#include "proc_control.h" #include "proc_control.h"
ProcControl::ProcControl(const QString &procFile, const QString &statDir, QCoreApplication *parent) : QObject(parent) ProcControl::ProcControl(const QString &procFile, const QString &statDir, shared_t *glbShare, QCoreApplication *parent) : QObject(parent)
{ {
fsMon = new QFileSystemWatcher(this); share = glbShare;
file = procFile; fsMon = new QFileSystemWatcher(this);
statPath = statDir; rdThread = new QThread(nullptr);
closeOnZero = false; ramDisk = new RamDisk(rdThread, share->maxMemSize, share->rdMnt, nullptr);
objCount = 0; checkTimer = new QTimer(this);
file = procFile;
statPath = statDir;
closeOnZero = false;
procFileAdded = false;
objCount = 0;
connect(fsMon, &QFileSystemWatcher::fileChanged, this, &ProcControl::procFileUpdated); checkTimer->setInterval(3000);
checkTimer->setSingleShot(true);
connect(fsMon, &QFileSystemWatcher::fileChanged, this, &ProcControl::procFileUpdated);
connect(checkTimer, &QTimer::timeout, this, &ProcControl::startApp);
} }
bool ProcControl::init() void ProcControl::createProcFile()
{ {
auto ret = false; if (!procFileAdded)
if (!QFileInfo::exists(file))
{ {
QFile fObj(file); if (!QFileInfo::exists(file))
if (!fObj.open(QFile::WriteOnly))
{ {
QTextStream(stderr) << "err: Failed to open process control file for writing: " << file << " reason: " << fObj.errorString(); QFile fObj(file);
if (!fObj.open(QFile::WriteOnly))
{
QTextStream(stderr) << "err: Failed to open process control file for writing: " << file << " reason: " << fObj.errorString() << Qt::endl;
}
else
{
fObj.write("##");
fObj.close();
procFileAdded = fsMon->addPath(file);
}
} }
else else
{ {
fObj.write("##"); QTextStream(stdout) << "wrn: " << file << " already exists so it will be removed, this will cause any other existing instance to close." << Qt::endl;
fObj.close();
ret = true; if (!QFile::remove(file))
{
QTextStream(stderr) << "err: Failed to remove process control file: " << file << " check permissions." << Qt::endl;
}
} }
} }
else }
void ProcControl::startRamdisk()
{
if (!rdThread->isRunning())
{ {
QTextStream(stdout) << "wrn: " << file << " already exists so it will be removed, this will cause any other existing instance to close."; rdThread->start();
if (!QFile::remove(file))
{
QTextStream(stderr) << "err: Failed to remove process control file: " << file << " check permissions.";
}
else
{
thread()->sleep(3);
ret = init();
}
} }
}
if (ret) void ProcControl::umntThenClose()
{
QDir(statPath).removeRecursively();
if (rdThread->isRunning())
{ {
fsMon->addPath(file); for (int i = 0; i < 3; ++i)
{
thread()->sleep(1);
if (QProcess::execute("umount", {share->rdMnt}) == 0)
{
break;
}
}
} }
return ret; QCoreApplication::instance()->quit();
}
void ProcControl::createCamObjs()
{
if (procFileAdded)
{
for (auto &&conf : lsFilesInDir(share->confPath))
{
auto confFilePath = QDir::cleanPath(share->confPath) + "/" + conf;
if (!confList.contains(confFilePath))
{
new Camera(*share, confFilePath, statPath, ramDisk, this, QCoreApplication::instance());
}
}
}
} }
void ProcControl::procFileUpdated(const QString &path) void ProcControl::procFileUpdated(const QString &path)
@ -78,11 +119,9 @@ void ProcControl::procFileUpdated(const QString &path)
void ProcControl::closeApp() void ProcControl::closeApp()
{ {
if (objCount == 0) if (confList.isEmpty())
{ {
QDir(statPath).removeRecursively(); umntThenClose();
QCoreApplication::instance()->quit();
} }
else else
{ {
@ -92,17 +131,29 @@ void ProcControl::closeApp()
} }
} }
void ProcControl::objPlusOne() void ProcControl::startApp()
{ {
objCount++; if (!closeOnZero)
}
void ProcControl::objMinusOne()
{
objCount--;
if (closeOnZero && (objCount == 0))
{ {
closeApp(); startRamdisk();
createProcFile();
createCamObjs();
checkTimer->start();
}
}
void ProcControl::objPlusOne(const QString &conf)
{
confList.append(conf);
}
void ProcControl::objMinusOne(const QString &conf)
{
confList.removeAll(conf);
if (closeOnZero && confList.isEmpty())
{
umntThenClose();
} }
} }

View File

@ -14,6 +14,8 @@
// GNU General Public License for more details. // GNU General Public License for more details.
#include "common.h" #include "common.h"
#include "ram_disk.h"
#include "camera.h"
class ProcControl : public QObject class ProcControl : public QObject
{ {
@ -22,26 +24,37 @@ class ProcControl : public QObject
private: private:
QFileSystemWatcher *fsMon; QFileSystemWatcher *fsMon;
QTimer *checkTimer;
QThread *rdThread;
RamDisk *ramDisk;
shared_t *share;
QStringList confList;
QString file; QString file;
QString statPath; QString statPath;
bool closeOnZero; bool closeOnZero;
bool procFileAdded;
uint objCount; uint objCount;
void createCamObjs();
void createProcFile();
void startRamdisk();
void umntThenClose();
private slots: private slots:
void procFileUpdated(const QString &path); void procFileUpdated(const QString &path);
void closeApp();
public: public:
explicit ProcControl(const QString &procFile, const QString &statDir, QCoreApplication *parent); explicit ProcControl(const QString &procFile, const QString &statDir, shared_t *glbShare, QCoreApplication *parent);
void objPlusOne(); void objPlusOne(const QString &conf);
bool init();
public slots: public slots:
void objMinusOne(); void objMinusOne(const QString &conf);
void closeApp();
void startApp();
signals: signals:

811
src/ram_disk.cpp Normal file
View File

@ -0,0 +1,811 @@
// This file is part of JustMotion.
// JustMotion 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.
// JustMotion 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 "ram_disk.h"
mode_t fileDefaultMode()
{
return S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH | S_IFREG;
}
mode_t symmDefaultMode()
{
return S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH | S_IFLNK;
}
mode_t dirDefaultMode()
{
return S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH | S_IFDIR;
}
RamDisk *RamDisk::instance = nullptr;
QString RamDisk::mnt = QString();
QStringList RamDisk::flushHist = QStringList();
QStringList RamDisk::bondDirs = QStringList();
QHash<QString, file_t*> RamDisk::data = QHash<QString, file_t*>();
fuse_operations RamDisk::fs_oper = fuse_operations();
quint64 RamDisk::inoSeed = 2;
quint64 RamDisk::rootCtime = 0;
quint64 RamDisk::rootMtime = 0;
quint64 RamDisk::duMax = 0;
quint64 RamDisk::du = 0;
QRecursiveMutex RamDisk::mutex = QRecursiveMutex();
RamDisk::RamDisk(QThread *thr, quint64 maxSize, const QString &mntPnt, QObject *parent) : QObject(parent)
{
duMax = maxSize;
mnt = mntPnt;
instance = this;
rootCtime = QDateTime::currentMSecsSinceEpoch();
rootMtime = rootCtime;
connect(thr, &QThread::started, this, &RamDisk::exec);
moveToThread(thr);
}
void RamDisk::lock()
{
mutex.lock();
}
void RamDisk::unlock()
{
mutex.unlock();
}
void RamDisk::exec()
{
fs_oper.getattr = RamDisk::getattr;
fs_oper.readlink = RamDisk::readlink;
fs_oper.readdir = RamDisk::readdir;
fs_oper.mkdir = RamDisk::mkdir;
fs_oper.symlink = RamDisk::symlink;
fs_oper.unlink = RamDisk::unlink;
fs_oper.rmdir = RamDisk::rmdir;
fs_oper.rename = RamDisk::rename;
fs_oper.truncate = RamDisk::truncate;
fs_oper.open = RamDisk::open;
fs_oper.create = RamDisk::create;
fs_oper.read = RamDisk::read;
fs_oper.write = RamDisk::write;
fs_oper.statfs = RamDisk::statfs;
fs_oper.init = RamDisk::init;
fs_oper.destroy = RamDisk::destroy;
fs_oper.utimens = RamDisk::utimens;
fs_oper.fallocate = RamDisk::fallocate;
fs_oper.flush = RamDisk::flush;
fs_oper.release = RamDisk::release;
fs_oper.chmod = RamDisk::chmod;
fs_oper.chown = RamDisk::chown;
fs_oper.copy_file_range = NULL;
fs_oper.link = NULL;
fs_oper.lock = NULL;
fs_oper.releasedir = NULL;
fs_oper.fsync = NULL;
fs_oper.opendir = NULL;
fs_oper.fsyncdir = NULL;
fs_oper.bmap = NULL;
fs_oper.ioctl = NULL;
fs_oper.flock = NULL;
fs_oper.lseek = NULL;
fs_oper.access = NULL;
fs_oper.write_buf = NULL;
fs_oper.read_buf = NULL;
fs_oper.getxattr = NULL;
fs_oper.setxattr = NULL;
fs_oper.listxattr = NULL;
fs_oper.removexattr = NULL;
QByteArray appName(APP_TARGET);
//QByteArray debug("-d");
//QByteArray singleTh("-s");
QByteArray foreground("-f");
QByteArray option("-o");
//QByteArray dp("default_permissions");
QByteArray other("allow_other");
int argc = 5;
char *argv[] = {appName.data(),
//debug.data(),
//singleTh.data(),
foreground.data(),
//option.data(),
//dp.data(),
option.data(),
other.data(),
mnt.toUtf8().data()};
auto ret = fuse_main(argc, argv, &fs_oper, NULL);
if (ret != 0)
{
qCritical() << "err: the ram disk failed to mount, return code: " << ret;
}
}
bool RamDisk::isFull()
{
return du >= duMax;
}
int RamDisk::calcNumOfBlocks(quint64 len, bool countZero)
{
if ((len == 0) && !countZero)
{
return 0;
}
if (len == 0)
{
return 1;
}
else
{
qreal blks = static_cast<qreal>(len) / static_cast<qreal>(BLK_SIZE);
return static_cast<int>(ceil(blks));
}
}
void RamDisk::fillStatTimes(struct stat *info, quint64 cTimeSec, quint64 mTimeSec)
{
// this converts the UTC epoch stored in the ram disk to
// local epoch using QDateTime.
auto cTime = QDateTime::fromMSecsSinceEpoch(cTimeSec, Qt::LocalTime);
auto mTime = QDateTime::fromMSecsSinceEpoch(mTimeSec, Qt::LocalTime);
auto curr = QDateTime::currentDateTime();
info->st_ctim.tv_sec = cTime.toSecsSinceEpoch();
info->st_mtim.tv_sec = mTime.toSecsSinceEpoch();
info->st_ctim.tv_nsec = cTime.toMSecsSinceEpoch();
info->st_mtim.tv_nsec = mTime.toMSecsSinceEpoch();
info->st_atim.tv_nsec = curr.toMSecsSinceEpoch();
info->st_atim.tv_sec = curr.toSecsSinceEpoch();
}
int RamDisk::getattr(const char *path, struct stat *info, struct fuse_file_info *fi)
{
Q_UNUSED(fi);
lock();
auto cleanedPath = QDir::cleanPath(path);
auto ret = 0;
memset(info, 0, sizeof(struct stat));
info->st_uid = getuid();
info->st_gid = getgid();
info->st_nlink = 1;
info->st_blksize = BLK_SIZE;
if (cleanedPath == "/")
{
info->st_mode = dirDefaultMode();
info->st_ino = 1;
info->st_size = 0;
info->st_blocks = 0;
fillStatTimes(info, rootCtime, rootMtime);
}
else if (data.contains(cleanedPath))
{
auto file = data[cleanedPath];
info->st_ino = file->ino;
info->st_mode = file->mode;
info->st_size = file->bytes.size();
info->st_blocks = file->blks;
fillStatTimes(info, file->ctime, file->mtime);
}
else
{
ret = -ENOENT;
}
unlock();
return ret;
}
int RamDisk::readlink(const char *path, char *buff, size_t len)
{
auto cleanedPath = QDir::cleanPath(path);
auto ret = 0;
lock();
if (data.contains(cleanedPath))
{
memcpy(buff, data[cleanedPath]->bytes.data(), len);
}
else
{
ret = -ENOENT;
}
unlock();
return ret;
}
QString RamDisk::getFileName(const QString &basePath, const QString &fullPath)
{
QString ret;
if (fullPath.startsWith(basePath))
{
auto name = fullPath.mid(basePath.size());
if (name.startsWith("/"))
{
name = name.mid(1);
}
if (name.indexOf("/") == -1)
{
ret = name;
}
}
return ret;
}
bool RamDisk::dirEmpty(const QString &path, int &ret)
{
ret = 0;
if (!data.contains(path) && path != "/")
{
ret = -ENOENT;
}
else
{
auto treeList = data.keys();
for (auto fullPath : treeList)
{
auto name = getFileName(path, fullPath);
if (!name.isEmpty())
{
ret = -ENOTEMPTY; break;
}
}
}
return ret == 0;
}
quint32 RamDisk::getMode(const QString &path)
{
quint32 ret = 0;
if (path == "/")
{
ret = dirDefaultMode();
}
else if (data.contains(path))
{
ret = data[path]->mode;
}
return ret;
}
int RamDisk::readdir(const char *path, void *buff, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags)
{
Q_UNUSED(offset);
Q_UNUSED(fi);
Q_UNUSED(flags);
lock();
auto fileList = data.keys();
auto cleanedPath = QDir::cleanPath(path);
auto ret = 0;
struct stat st;
memset(&st, 0, sizeof(st));
for (auto filePath : fileList)
{
auto name = getFileName(cleanedPath, filePath);
if (!name.isEmpty())
{
auto file = data[QDir::cleanPath(cleanedPath + "/" + name)];
st.st_mode = file->mode;
st.st_ino = file->ino;
st.st_size = file->bytes.size();
st.st_blocks = file->blks;
fillStatTimes(&st, file->ctime, file->mtime);
if (filler(buff, name.toUtf8().data(), &st, 0, FUSE_FILL_DIR_PLUS))
{
ret = -ENOMEM;
}
}
}
unlock();
return ret;
}
int RamDisk::mkdir(const char *path, mode_t mode)
{
Q_UNUSED(mode);
lock();
auto cleanedPath = QDir::cleanPath(path);
auto ret = 0;
if (!data.contains(cleanedPath) && (cleanedPath != "/"))
{
auto info = new file_t();
auto curr = QDateTime::currentDateTimeUtc();
info->mode = dirDefaultMode();
info->ino = inoSeed++;
info->blks = 0;
info->mtime = curr.toMSecsSinceEpoch();
info->ctime = curr.toMSecsSinceEpoch();
data.insert(cleanedPath, info);
}
unlock();
return ret;
}
int RamDisk::symlink(const char *src, const char *dst)
{
lock();
auto cleanedSrcPath = QDir::cleanPath(src);
auto cleanedDstPath = QDir::cleanPath(dst);
auto ret = 0;
if (isFull())
{
ret = -ENOSPC;
}
else
{
auto info = new file_t();
auto curr = QDateTime::currentDateTimeUtc();
info->mode = symmDefaultMode();
info->ino = inoSeed++;
info->blks = 1;
info->bytes = cleanedSrcPath.toUtf8();
info->mtime = curr.toMSecsSinceEpoch();
info->ctime = curr.toMSecsSinceEpoch();
du += info->bytes.size();
data.insert(cleanedDstPath, info);
}
unlock();
return ret;
}
int RamDisk::unlink(const char *path)
{
lock();
auto cleanedPath = QDir::cleanPath(path);
auto ret = 0;
if (!data.contains(cleanedPath))
{
ret = -ENOENT;
}
else
{
du -= data[cleanedPath]->bytes.size();
data.remove(cleanedPath);
}
unlock();
return ret;
}
int RamDisk::rmdir(const char *path)
{
lock();
auto cleanedPath = QDir::cleanPath(path);
auto ret = 0;
if (dirEmpty(cleanedPath, ret))
{
data.remove(cleanedPath);
}
unlock();
return ret;
}
void RamDisk::rePath(const QString &src, const QString &dst)
{
auto pathList = data.keys();
for (auto path : pathList)
{
if (path.startsWith(src))
{
auto file = data.take(path);
auto newPath = path.replace(src, dst);
data.insert(newPath, file);
}
}
}
int RamDisk::rename(const char *src, const char *dst, unsigned int flags)
{
lock();
auto cleanedSrcPath = QDir::cleanPath(src);
auto cleanedDstPath = QDir::cleanPath(dst);
auto srcMode = getMode(cleanedSrcPath);
auto dstMode = getMode(cleanedDstPath);
auto ret = 0;
if (srcMode && dstMode)
{
if (srcMode != dstMode)
{
ret = -EINVAL;
}
else if (flags & RENAME_EXCHANGE)
{
auto tmp = QString(TMP_DIR) + QDir::separator() + QString(cleanedSrcPath);
rePath(cleanedSrcPath, tmp);
rePath(cleanedDstPath, cleanedSrcPath);
rePath(tmp, cleanedDstPath);
}
else if ((flags & RENAME_NOREPLACE) == 0)
{
rePath(cleanedSrcPath, cleanedDstPath);
}
else
{
ret = -EEXIST;
}
}
else if (srcMode && !dstMode)
{
rePath(cleanedSrcPath, cleanedDstPath);
}
else
{
ret = -ENOENT;
}
unlock();
return ret;
}
int RamDisk::open(const char *path, struct fuse_file_info *fi)
{
Q_UNUSED(path);
Q_UNUSED(fi);
return 0;
}
int RamDisk::truncate(const char *path, off_t size, struct fuse_file_info *fi)
{
Q_UNUSED(fi);
lock();
auto cleanedPath = QDir::cleanPath(path);
auto ret = 0;
if (!data.contains(cleanedPath))
{
ret = -ENOENT;
}
else
{
auto file = data[cleanedPath];
auto oldSize = file->bytes.size();
file->bytes = file->bytes.leftJustified(size, 0x00, true);
file->mtime = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch();
if (oldSize > size)
{
du -= oldSize - size;
}
else if (size > oldSize)
{
du += size - oldSize;
}
file->blks = calcNumOfBlocks(size);
}
unlock();
return ret;
}
int RamDisk::create(const char *path, mode_t mode, struct fuse_file_info *fi)
{
Q_UNUSED(mode);
Q_UNUSED(fi);
lock();
auto cleanedPath = QDir::cleanPath(path);
auto ret = 0;
if (cleanedPath == "/")
{
ret = -EINVAL;
}
else if (data.contains(cleanedPath))
{
ret = -EEXIST;
}
else
{
auto file = new file_t();
auto curr = QDateTime::currentDateTimeUtc();
file->ino = inoSeed++;
file->blks = 1;
file->mode = fileDefaultMode();
file->ctime = curr.toMSecsSinceEpoch();
file->mtime = curr.toMSecsSinceEpoch();
data.insert(cleanedPath, file);
}
unlock();
return ret;
}
int RamDisk::read(const char *path, char *buff, size_t len, off_t offs, struct fuse_file_info *fi)
{
Q_UNUSED(fi);
lock();
auto cleanedPath = QDir::cleanPath(path);
auto ret = 0;
if (!data.contains(cleanedPath))
{
ret = -ENOENT;
}
else
{
auto file = data[cleanedPath];
auto iOffs = static_cast<int>(offs);
auto iLen = static_cast<int>(len);
if (iOffs > (file->bytes.size() - 1))
{
ret = -EFAULT;
}
else
{
if ((iOffs + iLen) > file->bytes.size())
{
iLen = file->bytes.size() - iOffs;
}
memcpy(buff, file->bytes.data() + iOffs, iLen);
ret = iLen;
}
}
unlock();
return ret;
}
int RamDisk::write(const char *path, const char *buff, size_t len, off_t offs, struct fuse_file_info *fi)
{
Q_UNUSED(fi);
lock();
auto cleanedPath = QDir::cleanPath(path);
auto ret = 0;
if (isFull())
{
ret = -ENOSPC;
}
else if (!data.contains(cleanedPath))
{
ret = -ENOENT;
}
else
{
auto file = data[cleanedPath];
auto oldSize = file->bytes.size();
file->bytes.replace(offs, len, QByteArray::fromRawData(buff, len));
if (oldSize > file->bytes.size())
{
du -= oldSize - file->bytes.size();
}
else if (file->bytes.size() > oldSize)
{
du += file->bytes.size() - oldSize;
}
file->blks = calcNumOfBlocks(file->bytes.size());
ret = len;
}
unlock();
return ret;
}
int RamDisk::statfs(const char *path, struct statvfs *stbuff)
{
Q_UNUSED(path);
stbuff->f_frsize = BLK_SIZE;
stbuff->f_bsize = BLK_SIZE;
stbuff->f_blocks = calcNumOfBlocks(duMax);
stbuff->f_bfree = stbuff->f_blocks - calcNumOfBlocks(du, false);
stbuff->f_bavail = stbuff->f_bfree;
return 0;
}
void RamDisk::destroy(void *privateData)
{
Q_UNUSED(privateData);
lock();
data.clear();
unlock();
inoSeed = 1;
du = 0;
}
int RamDisk::utimens(const char *path, const struct timespec newTimes[2], struct fuse_file_info *fi)
{
Q_UNUSED(fi);
Q_UNUSED(newTimes);
lock();
auto cleanedPath = QDir::cleanPath(path);
auto ret = 0;
if (!data.contains(cleanedPath))
{
ret = -ENOENT;
}
else
{
data[cleanedPath]->mtime = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch();
}
unlock();
return ret;
}
int RamDisk::fallocate(const char *path, int mode, off_t offs, off_t len, struct fuse_file_info *fi)
{
Q_UNUSED(path);
Q_UNUSED(mode);
Q_UNUSED(fi);
Q_UNUSED(offs);
Q_UNUSED(len);
return 0;
}
int RamDisk::flush(const char *path, struct fuse_file_info *fi)
{
Q_UNUSED(fi);
auto cleanedPath = QDir::cleanPath(path);
auto basePath = QFileInfo(cleanedPath).path();
if (flushHist.size() >= (bondDirs.size() * 4))
{
flushHist = flushHist.mid(bondDirs.size() * 2);
}
if (bondDirs.contains(basePath) && !flushHist.contains(cleanedPath))
{
flushHist.append(cleanedPath);
emit instance->watchedFileFlushed(cleanedPath);
}
return 0;
}
int RamDisk::release(const char *path, struct fuse_file_info *fi)
{
Q_UNUSED(path);
Q_UNUSED(fi);
return 0;
}
int RamDisk::chmod(const char *path, mode_t mode, struct fuse_file_info *fi)
{
Q_UNUSED(path);
Q_UNUSED(mode);
Q_UNUSED(fi);
return 0;
}
int RamDisk::chown(const char *path, uid_t uid, gid_t gid, struct fuse_file_info *fi)
{
Q_UNUSED(path);
Q_UNUSED(uid);
Q_UNUSED(gid);
Q_UNUSED(fi);
return 0;
}
void *RamDisk::init(struct fuse_conn_info *conn, struct fuse_config *cfg)
{
Q_UNUSED(conn);
Q_UNUSED(cfg);
return NULL;
}
void RamDisk::addCam(const QString &name)
{
bondDirs.append("/" + name + "/vid");
}
void RamDisk::rmCam(const QString &name)
{
bondDirs.removeAll("/" + name + "/vid");
}

91
src/ram_disk.h Normal file
View File

@ -0,0 +1,91 @@
#ifndef RAM_DISK_H
#define RAM_DISK_H
// This file is part of JustMotion.
// JustMotion 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.
// JustMotion 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"
#define TMP_DIR "#TMP"
mode_t dirDefaultMode();
mode_t fileDefaultMode();
mode_t symmDefaultMode();
class RamDisk : public QObject
{
Q_OBJECT
private:
static RamDisk *instance;
static QString mnt;
static QStringList flushHist;
static QStringList bondDirs;
static QHash<QString, file_t*> data;
static fuse_operations fs_oper;
static quint64 rootMtime;
static quint64 rootCtime;
static quint64 inoSeed;
static quint64 duMax;
static quint64 du;
static QRecursiveMutex mutex;
static QString getFileName(const QString &basePath, const QString &fullPath);
static quint32 getMode(const QString &path);
static bool dirEmpty(const QString &path, int &ret);
static void fillStatTimes(struct stat *info, quint64 cTimeSec, quint64 mTimeSec);
static void rePath(const QString &src, const QString &dst);
static void lock();
static void unlock();
static int calcNumOfBlocks(quint64 len, bool countZero = true);
private slots:
void exec();
public:
static bool isFull();
static int getattr(const char *path, struct stat *info, struct fuse_file_info *fi);
static int readlink(const char *path, char *buff, size_t len);
static int readdir(const char *path, void *buff, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags);
static int mkdir(const char *path, mode_t mode);
static int symlink(const char *src, const char *dst);
static int unlink(const char *path);
static int rmdir(const char *path);
static int chmod(const char *path, mode_t mode, struct fuse_file_info *fi);
static int chown(const char *path, uid_t uid, gid_t gid, struct fuse_file_info *fi);
static int flush(const char *path, struct fuse_file_info *fi);
static int release(const char *path, struct fuse_file_info *fi);
static int rename(const char *src, const char *dst, unsigned int flags);
static int truncate(const char *path, off_t size, struct fuse_file_info *fi);
static int open(const char *path, struct fuse_file_info *fi);
static int create(const char *path, mode_t mode, struct fuse_file_info *fi);
static int read(const char *path, char *buff, size_t len, off_t offs, struct fuse_file_info *fi);
static int write(const char *path, const char *buff, size_t len, off_t offs, struct fuse_file_info *fi);
static int statfs(const char *path, struct statvfs *stbuff);
static int utimens(const char *path, const struct timespec newTimes[2], struct fuse_file_info *fi);
static int fallocate(const char *path, int mode, off_t offs, off_t len, struct fuse_file_info *fi);
static void *init(struct fuse_conn_info *conn, struct fuse_config *cfg);
static void destroy(void *privateData);
static void addCam(const QString &name);
static void rmCam(const QString &name);
explicit RamDisk(QThread *thr, quint64 maxSize, const QString &mntPnt, QObject *parent = nullptr);
signals:
void watchedFileFlushed(const QString &path);
};
#endif // RAM_DISK_H

View File

@ -22,6 +22,7 @@ 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::readyReadStandardError, this, &RecordLoop::readErr);
connect(this, &RecordLoop::started, this, &RecordLoop::resetTime); connect(this, &RecordLoop::started, this, &RecordLoop::resetTime);
moveToThread(thr); moveToThread(thr);
@ -42,7 +43,6 @@ void RecordLoop::init()
checkTimer->setSingleShot(true); checkTimer->setSingleShot(true);
checkTimer->setInterval(3000); checkTimer->setInterval(3000);
setupBuffDir(shared->buffPath);
restart(); restart();
} }
@ -63,7 +63,7 @@ QString RecordLoop::camCmdFromConf()
ret += "-vcodec " + shared->vidCodec + " "; ret += "-vcodec " + shared->vidCodec + " ";
ret += "-acodec " + shared->audCodec + " "; ret += "-acodec " + shared->audCodec + " ";
ret += "-reset_timestamps 1 -sc_threshold 0 -g 2 -force_key_frames 'expr:gte(t, n_forced * 2)' -segment_time 2 -f segment "; ret += "-reset_timestamps 1 -sc_threshold 0 -g 2 -force_key_frames 'expr:gte(t, n_forced * 2)' -segment_time 2 -f segment ";
ret += shared->buffPath + "/live/" + QString(STRFTIME_FMT) + shared->streamExt; ret += shared->buffPath + "/vid/" + QString(STRFTIME_FMT) + shared->streamExt;
return ret; return ret;
} }
@ -80,6 +80,11 @@ QString RecordLoop::statusLine()
} }
} }
void RecordLoop::readErr()
{
//qCritical() << "err: " << readAllStandardError();
}
void RecordLoop::resetTime() void RecordLoop::resetTime()
{ {
checkTimer->start(); checkTimer->start();
@ -96,8 +101,6 @@ void RecordLoop::restart()
auto cmdLine = camCmdFromConf(); auto cmdLine = camCmdFromConf();
auto args = parseArgs(cmdLine.toUtf8(), -1); auto args = parseArgs(cmdLine.toUtf8(), -1);
qInfo() << "start recording command: " << cmdLine;
if (args.isEmpty()) if (args.isEmpty())
{ {
qCritical() << "err: couldn't parse a program name"; qCritical() << "err: couldn't parse a program name";

View File

@ -22,6 +22,7 @@ class RecordLoop : public QProcess
private slots: private slots:
void init(); void init();
void readErr();
void resetTime(); void resetTime();
private: private: