Compare commits

..

No commits in common. "33519acf7a4880aa8173b0037301c5caef2c60c2" and "970b9b5fc5647b6762a7f14636c447cce05b2911" have entirely different histories.

19 changed files with 231 additions and 734 deletions

5
.gitignore vendored
View File

@ -30,7 +30,6 @@ Makefile*
*build-* *build-*
/build /build
/app_dir /app_dir
/installers
/release /release
/debug /debug
@ -55,3 +54,7 @@ compile_commands.json
# VSCode # VSCode
/.vscode /.vscode
# Build folders
/.build-mow
/.build-imagemagick

29
CMakeLists.txt Normal file
View File

@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.14)
project(MotionWatch LANGUAGES CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_BUILD_TYPE Debug)
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED)
add_executable(mow
src/main.cpp
src/common.h
src/common.cpp
src/camera.h
src/camera.cpp
src/services.h
src/services.cpp
)
target_link_libraries(mow Qt${QT_VERSION_MAJOR}::Core ${OpenCV_LIBS})

View File

@ -1,20 +0,0 @@
QT -= gui
CONFIG += c++11 console
CONFIG -= app_bundle
TARGET = build/jmotion
OBJECTS_DIR = build
MOC_DIR = build
RCC_DIR = build
HEADERS += \
src/common.h \
src/camera.h \
src/services.h
SOURCES += \
src/common.cpp \
src/camera.cpp \
src/services.cpp \
src/main.cpp

View File

@ -1,38 +1,21 @@
# JustMotion # # Motion Watch #
JustMotion is a video surveillance application that monitors the video feeds Motion Watch is a video surveillance application that monitors the video feeds
of an IP or local camera and records only footage that contains motion. The of an IP camera and records only footage that contains motion. The main
main advantage of this is reduced storage requirements as opposed to advantage of this is reduced storage requirements as opposed to continuous
continuous recording because only video footage of interest is recorded to recording because only video footage of interest is recorded to storage.
storage.
Sticking to the principle of doing the least as needed, this application is
extremely lightweight with the fact it doesn't attempt to re-implement much
of it's functions internally but will instead rely on external applications
that already implement the functions very well.
No user interface is implemented instead exteral applications are more than
welcome to interface with the buffer/footage directories to implement a
user interface of any flavor.
### Usage ### ### Usage ###
``` ```
Usage: jmotion <argument> Usage: mow <argument>
-h : display usage information about this application. -h : display usage information about this application.
-c : path to the config file used to run a single main loop instance. -c : path to the config file used to run a single camera instance.
-i : all valid config files found in /etc/jmotion will be used to create -d : path to a directory that can contain multiple config files.
a full instance; full meaning main and vid loop systemd services each file found in the directory will be used to run a
will be created for each config file. camera instance.
-d : this is the same as -i except it will not auto start the services.
-v : display the current version. -v : display the current version.
-u : uninstall the entire app from your system, including all
systemd services related to it.
-f : force an action without pausing for user confirmation.
-l : list all jmotion services along with statuses.
-r : remove all jmotion services.
``` ```
### Config File ### ### Config File ###
@ -95,20 +78,14 @@ img_comp_out = stderr
# use to output the comparison score. this can only be stderr or stdout, # use to output the comparison score. this can only be stderr or stdout,
# any other stream name is considered invalid. # any other stream name is considered invalid.
# #
vid_codec = copy stream_codec = copy
# this is the encoding codec to use when recording footage from the camera. # this is the encoding codec to use when recording footage from the camera.
# the list of supported codecs entirely depend on the hosts' ffmpeg # the list of supported codecs entirely depend on the hosts' ffmpeg
# installation. run 'ffmpeg -codecs' to determine this list. if not # installation. run 'ffmpeg -codecs' to determine this list. if not
# defined, 'copy' will be used as in it will just copy the codec format # defined, 'copy' will be used as in it will just copy the codec format
# from the camera itself. # from camera itself.
# #
aud_codec = copy stream_ext = .avi
# this is the audio encoding codec to use when recording from the camera.
# the list of supported audio codes can be determined by running 'ffmpeg
# -codecs' on the host machine. if not defined, 'copy' will be used as in
# it will directly copy the audio stream from the camera if present.
#
stream_ext = .mkv
# this is the file extension that will be used to when recording footage # this is the file extension that will be used to when recording footage
# from the camera in buffer_path. ffmpeg will also use this to determine # from the camera in buffer_path. ffmpeg will also use this to determine
# what format container to use for the video clips. # what format container to use for the video clips.
@ -117,7 +94,7 @@ thumbnail_ext = .jpg
# this the image format that will be used when creating the thumbnails # this the image format that will be used when creating the thumbnails
# for the videos clips recorded to rec_path. # for the videos clips recorded to rec_path.
# #
rec_ext = .mkv rec_ext = .avi
# this the the file extension that will be used when storing motion footage # this the the file extension that will be used when storing motion footage
# to rec_path. ffmpeg will also use this to determine what format container # to rec_path. ffmpeg will also use this to determine what format container
# to use for the video clips. # to use for the video clips.
@ -144,12 +121,13 @@ post_cmd = move_the_ptz_camera.py
# #
rec_fps = 30 rec_fps = 30
# this sets the recording frames per second for the footage recorded # this sets the recording frames per second for the footage recorded
# from the camera. this has no affect if using 'copy' as the vid_codec. # from the camera. this has no affect if using 'copy' as the
# stream_codec.
# #
rec_scale = 1280:720 rec_scale = 1280:720
# this sets the pixel scale of the recorded footage from the camera. it # this sets the pixel scale of the recorded footage from the camera. it
# uses width, height numeric strings seperated by a colon, eg W:H. this # uses width, height numeric strings seperated by a colon, eg W:H. this
# has no affect of using 'copy' as the vid_codec. # has no affect of using 'copy' as the stream_codec.
# #
img_scale = 320:240 img_scale = 320:240
# this sets the pixel size of the thumbnails for recorded stored in # this sets the pixel size of the thumbnails for recorded stored in
@ -168,20 +146,18 @@ service_group = mow
# service_user will be used. # service_user will be used.
``` ```
### Build/Install ### ### Setup/Build/Install ###
This application is currently only compatible with a Linux based operating This application is currently only compatible with a Linux based operating
systems that are capable of installing python3 and the QT API (QT6.X.X or systems that are capable of installing the QT API.
better).
``` ```
./build.py <--run this first sh ./setup.sh <--- only need to run this once if compiling for the first
./install.py <--run this next sh ./build.sh time or if upgrading from the ground up.
sh ./install.sh
``` ```
``` ```
note 1: the build script will search for the QT api installed in your note 1: be sure to run setup.sh and install.sh as root (or use sudo).
system. if not found, it will ask you where it is. either way note 2: if building from scratch the following scripts will need to
it is recommended to install the QT API before running this be run in this order - setup.sh -> build.sh -> install.sh.
script.
note 2: both scripts assume python3 is already installed.
``` ```

233
build.py
View File

@ -1,233 +0,0 @@
#!/usr/bin/python3
import os
import re
import subprocess
import shutil
import sys
import platform
def get_app_target(text):
return re.search(r'(APP_TARGET) +(\"(.*?)\")', text).group(3)
def get_app_ver(text):
return re.search(r'(APP_VERSION) +(\"(.*?)\")', text).group(3)
def get_app_name(text):
return re.search(r'(APP_NAME) +(\"(.*?)\")', text).group(3)
def get_qt_path():
try:
return str(subprocess.check_output(["qtpaths", "--binaries-dir"]), 'utf-8').strip()
except:
print("A direct call to 'qtpaths' 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): ")
def get_qt_from_cli():
for arg in sys.argv:
if arg == "-qt_dir":
index = sys.argv.index(arg)
try:
return sys.argv[index + 1]
except:
return ""
return ""
def get_ver_header():
current_dir = os.path.dirname(__file__)
if current_dir == "":
return "src" + os.sep + "common.h"
else:
return current_dir + os.sep + "src" + os.sep + "common.h"
def get_nearest_subdir(path, sub_name):
dir_list = os.listdir(path)
ret = ""
for entry in dir_list:
if sub_name in entry:
ret = entry
break
return ret
def cd():
current_dir = os.path.dirname(__file__)
if current_dir != "":
os.chdir(current_dir)
def verbose_copy(src, dst):
print("cpy: " + src + " --> " + dst)
if os.path.isdir(src):
if os.path.exists(dst) and os.path.isdir(dst):
shutil.rmtree(dst)
try:
# ignore errors thrown by shutil.copytree()
# it's likely not actually failing to copy
# the directory but still throws errors if
# it fails to apply the same file stats as
# the source. this type of error can be
# ignored.
shutil.copytree(src, dst)
except:
pass
elif os.path.exists(src):
shutil.copyfile(src, dst)
else:
print("wrn: " + src + " does not exists. skipping.")
def linux_build_app_dir(app_ver, app_name, app_target, qt_bin):
if not os.path.exists("app_dir/lib"):
os.makedirs("app_dir/lib")
verbose_copy("build/" + app_target, "app_dir/" + app_target)
shutil.copyfile("build/" + app_target, "/tmp/" + app_target)
# copying the executable file from the build folder to
# temp bypasses any -noexe retrictions a linux file
# system may have. there is a chance temp is also
# restricted in this way but that kind of config is
# rare. ldd will not run correctly with -noexe
# enabled.
lines = str(subprocess.check_output(["ldd", "/tmp/" + app_target]), 'utf-8').split("\n")
os.remove("/tmp/" + app_target)
for line in lines:
if " => " in line and "libc" not in line:
#if ("libQt" in line) or ("libicu" in line) or ("libGL.so" in line) or ("libpcre16.so" in line) or ("libpcre.so" in line):
if " (0x0" in line:
start_index = line.index("> ") + 2
end_index = line.index(" (0x0")
src_file = line[start_index:end_index]
file_name = os.path.basename(src_file)
verbose_copy(src_file, "app_dir/lib/" + file_name)
if "/usr/lib/x86_64-linux-gnu/qt6/bin" == qt_bin:
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_uninstall.sh", "app_dir/uninstall.sh")
complete(app_ver, app_target)
def complete(app_ver, app_target):
print("Build complete for version: " + app_ver)
print("You can now run the install.py script to install onto this machine or create an installer.")
def get_like_distro():
info = platform.freedesktop_os_release()
ids = [info["ID"]]
if "ID_LIKE" in info:
# ids are space separated and ordered by precedence
ids.extend(info["ID_LIKE"].split())
return ids
def list_installed_packages():
like_distro = get_like_distro()
if ("ubuntu" in like_distro) or ("debian" in like_distro) or ("linuxmint" in like_distro):
return str(subprocess.check_output(["apt", "list", "--installed"]), 'utf-8')
elif ("fedora" in like_distro) or ("rhel" in like_distro):
return str(subprocess.check_output(["dnf", "list", "installed"]), 'utf-8')
elif ("arch" in like_distro):
return str(subprocess.check_output(["pacman", "-Q"]), 'utf-8')
else:
print("Warning: unable to determine a package manager for this platform.")
return []
def list_of_words_in_text(list_of_words, text_body):
for word in list_of_words:
if not word in text_body:
return False
return True
def platform_setup():
ins_packages = list_installed_packages()
like_distro = get_like_distro()
dep_pkgs_a = ["pkg-config", "make", "g++"]
dep_pkgs_b = ["ffmpeg", "libfuse-dev", "fuse3", "imagemagick"]
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):
subprocess.run(["sudo", "apt", "update", "-y"])
subprocess.run(["sudo", "apt", "install", "-y"] + dep_pkgs_a)
subprocess.run(["sudo", "apt", "install", "-y"] + dep_pkgs_b)
elif ("fedora" in like_distro) or ("rhel" in like_distro):
subprocess.run(["sudo", "dnf", "install", "-y"] + dep_pkgs_a)
subprocess.run(["sudo", "dnf", "install", "-y"] + dep_pkgs_b)
elif ("arch" in like_distro):
subprocess.run(["sudo", "pacman", "-S", "--noconfirm"] + dep_pkgs_a)
subprocess.run(["sudo", "pacman", "-S", "--noconfirm"] + dep_pkgs_b)
def main():
platform_setup()
with open(get_ver_header()) as file:
text = file.read()
app_target = get_app_target(text)
app_ver = get_app_ver(text)
app_name = get_app_name(text)
qt_bin = get_qt_from_cli()
if qt_bin == "":
qt_bin = get_qt_path()
if qt_bin != "":
print("app_target = " + app_target)
print("app_version = " + app_ver)
print("app_name = " + app_name)
print("qt_bin = " + qt_bin)
cd()
result = subprocess.run([qt_bin + os.sep + "qmake", "-config", "release"])
if result.returncode == 0:
result = subprocess.run(["make"])
if result.returncode == 0:
if os.path.exists("app_dir"):
shutil.rmtree("app_dir")
os.makedirs("app_dir")
with open("app_dir" + os.sep + "info.txt", "w") as info_file:
info_file.write(app_target + "\n")
info_file.write(app_ver + "\n")
info_file.write(app_name + "\n")
linux_build_app_dir(app_ver, app_name, app_target, qt_bin)
if __name__ == "__main__":
main()

5
build.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/sh
mkdir -p ./.build-mow
cd ./.build-mow
cmake ..
make -j4

13
imgmagick_build.sh Normal file
View File

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

View File

@ -1,336 +0,0 @@
#!/usr/bin/python3
import os
import subprocess
import shutil
import platform
import sys
import zipfile
import binascii
import tempfile
def cd():
current_dir = os.path.dirname(__file__)
if current_dir != "":
os.chdir(current_dir)
def get_default_installer_path(app_ver, app_name):
if not os.path.exists("installers"):
os.makedirs("installers")
return "installers/" + app_name + "-" + app_ver + "-" + platform.system() + "-" + platform.machine() + ".run"
def make_install_dir(path, false_on_fail):
try:
if not os.path.exists(path):
os.makedirs(path)
return True
except:
if false_on_fail:
print("Failed to create directory: " + path + ", please make sure you are runnning this script with admin rights.")
return False
else:
return True
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)
def replace_bin(binary, old_bin, new_bin, offs):
while(True):
try:
index = binary.index(old_bin, offs)
binary = binary[:index] + new_bin + binary[index + len(old_bin):]
except ValueError:
break
return binary
def bin_sub_copy_file(src, dst, old_bin, new_bin, offs):
binary = bytearray()
with open(src, "rb") as rd_file:
binary = rd_file.read()
binary = replace_bin(binary, old_bin, new_bin, offs)
with open(dst, "wb") as wr_file:
wr_file.write(binary)
def text_sub_copy_file(src, dst, old_text, new_text, offs):
bin_sub_copy_file(src, dst, old_text.encode("utf-8"), new_text.encode("utf-8"), offs)
def text_template_deploy(src, dst, install_dir, app_name, app_target):
print("dep: " + dst)
text_sub_copy_file(src, dst, "$install_dir", install_dir, 0)
text_sub_copy_file(dst, dst, "$app_name", app_name, 0)
text_sub_copy_file(dst, dst, "$app_target", app_target, 0)
def verbose_copy(src, dst):
print("cpy: " + src + " --> " + dst)
if os.path.isdir(src):
files = os.listdir(src)
if not os.path.exists(dst):
os.makedirs(dst)
for file in files:
tree_src = src + os.path.sep + file
tree_dst = dst + os.path.sep + file
if os.path.isdir(tree_src):
if not os.path.exists(tree_dst):
os.makedirs(tree_dst)
verbose_copy(tree_src, tree_dst)
else:
shutil.copyfile(src, dst)
def verbose_create_symmlink(src, dst):
print("lnk: " + src + " --> " + dst)
if os.path.exists(dst):
os.remove(dst)
os.symlink(src, dst)
def local_install(app_target, app_name):
install_dir = "/opt/" + app_target
if os.path.exists(install_dir + "/uninstall.sh"):
subprocess.run([install_dir + "/uninstall.sh"])
if make_app_dirs(app_target):
text_template_deploy("app_dir/" + app_target + ".sh", install_dir + "/" + app_target + ".sh", install_dir, app_name, app_target)
text_template_deploy("app_dir/uninstall.sh", install_dir + "/uninstall.sh", install_dir, app_name, app_target)
verbose_copy("app_dir/" + app_target, install_dir + "/" + app_target)
verbose_copy("app_dir/lib", install_dir + "/lib")
verbose_create_symmlink(install_dir + "/" + app_target + ".sh", "/usr/bin/" + app_target)
subprocess.run(["chmod", "755", install_dir + "/" + app_target + ".sh"])
subprocess.run(["chmod", "755", install_dir + "/" + app_target])
subprocess.run(["chmod", "755", install_dir + "/uninstall.sh"])
subprocess.run(["chmod", "777", "/var/buffer"])
subprocess.run(["chmod", "777", "/var/footage"])
print("Installation finished. If you ever need to uninstall this application, run this command with root rights:")
print(" sh " + install_dir + "/uninstall.sh\n")
def dir_tree(path):
ret = []
if os.path.isdir(path):
for entry in os.listdir(path):
full_path = os.path.join(path, entry)
if os.path.isdir(full_path):
for sub_dir_file in dir_tree(full_path):
ret.append(sub_dir_file)
else:
ret.append(full_path)
return ret
def to_hex(data):
return str(binascii.hexlify(data))[2:-1]
def from_hex(text_line):
return binascii.unhexlify(text_line)
def make_install(app_ver, app_name):
path = get_default_installer_path(app_ver, app_name)
with zipfile.ZipFile("app_dir.zip", "w", compression=zipfile.ZIP_DEFLATED) as zip_file:
print("Compressing app_dir --")
for file in dir_tree("app_dir"):
print("adding file: " + file)
zip_file.write(file)
text_sub_copy_file(__file__, path, "main(is_sfx=False)", "main(is_sfx=True)\n\n\n", 10322)
with open(path, "a") as dst_file, open("app_dir.zip", "rb") as src_file:
print("Packing the compressed app_dir into the sfx script file --")
dst_file.write("# APP_DIR\n")
stat = os.stat("app_dir.zip")
while(True):
buffer = src_file.read(4000000)
if len(buffer) != 0:
dst_file.write("# " + to_hex(buffer) + "\n")
print(str(src_file.tell()) + "/" + str(stat.st_size))
if len(buffer) < 4000000:
break
os.remove("app_dir.zip")
subprocess.run(["chmod", "+x", path])
print("Finished packing the app.")
print("Installer: " + path)
def sfx():
abs_sfx_path = os.path.abspath(__file__)
mark_found = False
os.chdir(tempfile.gettempdir())
with open(abs_sfx_path) as packed_file, open("app_dir.zip", "wb") as zip_file:
stat = os.stat(abs_sfx_path)
print("Unpacking the app_dir compressed file from the sfx script.")
while(True):
line = packed_file.readline()
if not line:
break
elif mark_found:
zip_file.write(from_hex(line[2:-1]))
print(str(packed_file.tell()) + "/" + str(stat.st_size))
else:
if line == "# APP_DIR\n":
mark_found = True
print("Done.")
if not mark_found:
print("The app_dir mark was not found, unable to continue.")
else:
with zipfile.ZipFile("app_dir.zip", "r", compression=zipfile.ZIP_DEFLATED) as zip_file:
print("De-compressing app_dir --")
zip_file.extractall()
print("Preparing for installation.")
os.remove("app_dir.zip")
with open("app_dir" + os.sep + "info.txt") as info_file:
info = info_file.read().split("\n")
local_install(info[0], info[2])
shutil.rmtree("app_dir")
def get_like_distro():
info = platform.freedesktop_os_release()
ids = [info["ID"]]
if "ID_LIKE" in info:
# ids are space separated and ordered by precedence
ids.extend(info["ID_LIKE"].split())
return ids
def list_installed_packages():
like_distro = get_like_distro()
if ("ubuntu" in like_distro) or ("debian" in like_distro) or ("linuxmint" in like_distro):
return str(subprocess.check_output(["apt", "list", "--installed"]), 'utf-8')
elif ("fedora" in like_distro) or ("rhel" in like_distro):
return str(subprocess.check_output(["dnf", "list", "installed"]), 'utf-8')
elif ("arch" in like_distro):
return str(subprocess.check_output(["pacman", "-Q"]), 'utf-8')
else:
print("Warning: unable to determine a package manager for this platform.")
return []
def list_of_words_in_text(list_of_words, text_body):
for word in list_of_words:
if not word in text_body:
return False
return True
def platform_setup():
ins_packages = list_installed_packages()
like_distro = get_like_distro()
dep_pkgs_a = ["pkg-config"]
dep_pkgs_b = ["ffmpeg", "libfuse-dev", "fuse3", "imagemagick"]
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):
subprocess.run(["sudo", "apt", "update", "-y"])
subprocess.run(["sudo", "apt", "install", "-y"] + dep_pkgs_a)
subprocess.run(["sudo", "apt", "install", "-y"] + dep_pkgs_b)
elif ("fedora" in like_distro) or ("rhel" in like_distro):
subprocess.run(["sudo", "dnf", "install", "-y"] + dep_pkgs_a)
subprocess.run(["sudo", "dnf", "install", "-y"] + dep_pkgs_b)
elif ("arch" in like_distro):
subprocess.run(["sudo", "pacman", "-S", "--noconfirm"] + dep_pkgs_a)
subprocess.run(["sudo", "pacman", "-S", "--noconfirm"] + dep_pkgs_b)
def main(is_sfx):
cd()
app_target = ""
app_ver = ""
app_name = ""
if not is_sfx:
with open("app_dir" + os.sep + "info.txt") as info_file:
info = info_file.read().split("\n")
app_target = info[0]
app_ver = info[1]
app_name = info[2]
if is_sfx:
sfx()
elif "--local" in sys.argv:
platform_setup()
local_install(app_target, app_name)
elif "--installer" in sys.argv:
make_install(app_ver, app_name)
else:
print("Do you want to install onto this machine or create an installer?")
print("[1] local machine")
print("[2] create installer")
print("[3] exit")
while(True):
opt = input("select an option: ")
if opt == "1":
subprocess.run(["sudo", "python3", "install.py", "--local"])
break
elif opt == "2":
subprocess.run(["python3", "install.py", "--installer"])
break
elif opt == "3":
break
if __name__ == "__main__":
main(is_sfx=False)

54
install.sh Normal file
View File

@ -0,0 +1,54 @@
#!/bin/sh
if [ -f "/opt/mow/uninst" ]; then
mow -u -f
fi
if [ ! -d "/opt" ]; then
mkdir /opt
fi
if [ ! -d "/opt/mow" ]; then
mkdir /opt/mow
fi
if [ ! -d "/var/footage" ]; then
mkdir /var/footage
fi
if [ ! -d "/etc/mow" ]; then
mkdir /etc/mow
fi
if [ ! -d "/var/buffer" ]; then
mkdir /var/buffer
fi
cp -v ./.build-mow/mow /opt/mow/bin
echo "writing /opt/mow/run"
printf "#!/bin/sh\n" > /opt/mow/run
printf "/opt/mow/bin \$1 \$2 \$3\n" >> /opt/mow/run
echo "writing /opt/mow/uninst"
printf "#!/bin/sh\n" > /opt/mow/uninst
printf "mow -r\n" >> /opt/mow/uninst
printf "rm -v /opt/mow/bin\n" >> /opt/mow/uninst
printf "rm -v /opt/mow/run\n" >> /opt/mow/uninst
printf "rm -v /opt/mow/uninst\n" >> /opt/mow/uninst
printf "rm -v /usr/bin/mow\n" >> /opt/mow/uninst
printf "rm -rv /opt/mow\n" >> /opt/mow/uninst
printf "deluser mow\n" >> /opt/mow/uninst
useradd -r mow
usermod -aG video mow
chown -R mow:mow /var/footage
chown -R mow:mow /var/buffer
chmod -v +x /opt/mow/run
chmod -v +x /opt/mow/bin
chmod -v +x /opt/mow/uninst
ln -sv /opt/mow/run /usr/bin/mow
sh imgmagick_build.sh

26
setup.sh Normal file
View File

@ -0,0 +1,26 @@
#!/bin/sh
export DEBIAN_FRONTEND=noninteractive
apt update -y
apt install -y pkg-config cmake make g++
if [ $? -eq 0 ]
then
apt install -y ffmpeg
apt install -y libavcodec-dev
apt install -y libavformat-dev
apt install -y libavutil-dev
apt install -y libswscale-dev
apt install -y x264
apt install -y libx264-dev
apt install -y libilmbase-dev
apt install -y qt6-base-dev
apt install -y qtchooser
apt install -y qmake6
apt install -y qt6-base-dev-tools
apt install -y libxkbcommon-dev
apt install -y libfuse-dev
apt install -y fuse3
sh imgmagick_build.sh
fi

View File

@ -1,11 +1,11 @@
// This file is part of JustMotion. // This file is part of Motion Watch.
// JustMotion is free software: you can redistribute it and/or modify // Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// JustMotion is distributed in the hope that it will be useful, // Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// 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.

View File

@ -1,14 +1,14 @@
#ifndef CAMERA_H #ifndef CAMERA_H
#define CAMERA_H #define CAMERA_H
// This file is part of JustMotion. // This file is part of Motion Watch.
// JustMotion is free software: you can redistribute it and/or modify // Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// JustMotion is distributed in the hope that it will be useful, // Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// 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.

View File

@ -1,11 +1,11 @@
// This file is part of JustMotion. // This file is part of Motion Watch.
// JustMotion is free software: you can redistribute it and/or modify // Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// JustMotion is distributed in the hope that it will be useful, // Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// 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.
@ -214,6 +214,8 @@ bool rdConf(const QString &filePath, shared_t *share)
share->recPath.clear(); share->recPath.clear();
share->servGroup.clear(); share->servGroup.clear();
auto thrCount = QThread::idealThreadCount() / 2;
share->retCode = 0; share->retCode = 0;
share->imgThresh = 8000; share->imgThresh = 8000;
share->maxEvents = 30; share->maxEvents = 30;
@ -223,16 +225,15 @@ bool rdConf(const QString &filePath, shared_t *share)
share->conf = filePath; share->conf = filePath;
share->outputType = "stderr"; share->outputType = "stderr";
share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null"; share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null";
share->vidCodec = "copy"; share->streamCodec = "copy";
share->audCodec = "copy"; share->streamExt = ".avi";
share->streamExt = ".mkv"; share->recExt = ".avi";
share->recExt = ".mkv";
share->thumbExt = ".jpg"; share->thumbExt = ".jpg";
share->recFps = 30; share->recFps = 30;
share->liveSecs = 80; share->liveSecs = 80;
share->recScale = "1280:720"; share->recScale = "1280:720";
share->imgScale = "320:240"; share->imgScale = "320:240";
share->servUser = APP_TARGET; share->servUser = APP_BIN;
QString line; QString line;
@ -253,8 +254,7 @@ bool rdConf(const QString &filePath, shared_t *share)
rdLine("max_events = ", line, &share->maxEvents); rdLine("max_events = ", line, &share->maxEvents);
rdLine("img_comp_out = ", line, &share->outputType); rdLine("img_comp_out = ", line, &share->outputType);
rdLine("img_comp_cmd = ", line, &share->compCmd); rdLine("img_comp_cmd = ", line, &share->compCmd);
rdLine("vid_codec = ", line, &share->vidCodec); rdLine("stream_codec = ", line, &share->streamCodec);
rdLine("aud_codec = ", line, &share->audCodec);
rdLine("stream_ext = ", line, &share->streamExt); rdLine("stream_ext = ", line, &share->streamExt);
rdLine("rec_ext = ", line, &share->recExt); rdLine("rec_ext = ", line, &share->recExt);
rdLine("thumbnail_ext = ", line, &share->thumbExt); rdLine("thumbnail_ext = ", line, &share->thumbExt);
@ -315,8 +315,8 @@ bool rdConf(const QString &filePath, shared_t *share)
share->servGroup = share->servUser; share->servGroup = share->servUser;
} }
share->servMainLoop = QString(APP_TARGET) + ".main_loop." + share->camName; share->servMainLoop = QString(APP_BIN) + ".main_loop." + share->camName;
share->servVidLoop = QString(APP_TARGET) + ".vid_loop." + share->camName; share->servVidLoop = QString(APP_BIN) + ".vid_loop." + share->camName;
} }
return share->retCode == 0; return share->retCode == 0;

View File

@ -1,14 +1,14 @@
#ifndef COMMON_H #ifndef COMMON_H
#define COMMON_H #define COMMON_H
// This file is part of JustMotion. // This file is part of Motion Watch.
// JustMotion is free software: you can redistribute it and/or modify // Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// JustMotion is distributed in the hope that it will be useful, // Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// 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.
@ -30,9 +30,9 @@
using namespace std; using namespace std;
#define APP_VERSION "3.4" #define APP_VER "3.3"
#define APP_NAME "JustMotion" #define APP_NAME "Motion Watch"
#define APP_TARGET "jmotion" #define APP_BIN "mow"
#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&"
@ -65,8 +65,7 @@ struct shared_t
QString recPath; QString recPath;
QString outputType; QString outputType;
QString compCmd; QString compCmd;
QString vidCodec; QString streamCodec;
QString audCodec;
QString streamExt; QString streamExt;
QString recExt; QString recExt;
QString thumbExt; QString thumbExt;

View File

@ -1,11 +1,11 @@
// This file is part of JustMotion. // This file is part of Motion Watch.
// JustMotion is free software: you can redistribute it and/or modify // Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// JustMotion is distributed in the hope that it will be useful, // Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// 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.
@ -14,13 +14,13 @@
#include "camera.h" #include "camera.h"
#include "services.h" #include "services.h"
void showHelp(const QString etcDir) void showHelp()
{ {
QTextStream(stdout) << APP_NAME << " " << APP_VERSION << Qt::endl << Qt::endl; QTextStream(stdout) << APP_NAME << " " << APP_VER << Qt::endl << Qt::endl;
QTextStream(stdout) << "Usage: " << APP_TARGET << " <argument>" << Qt::endl << Qt::endl; QTextStream(stdout) << "Usage: mow <argument>" << Qt::endl << Qt::endl;
QTextStream(stdout) << "-h : display usage information about this application." << Qt::endl; QTextStream(stdout) << "-h : display usage information about this application." << Qt::endl;
QTextStream(stdout) << "-c : path to the config file used to run a single main loop instance." << Qt::endl; QTextStream(stdout) << "-c : path to the config file used to run a single main loop instance." << Qt::endl;
QTextStream(stdout) << "-i : all valid config files found in " << etcDir << " will be used to create" << Qt::endl; QTextStream(stdout) << "-i : all valid config files found in /etc/mow will be used to create" << Qt::endl;
QTextStream(stdout) << " a full instance; full meaning main and vid loop systemd services" << Qt::endl; QTextStream(stdout) << " a full instance; full meaning main and vid loop systemd services" << Qt::endl;
QTextStream(stdout) << " will be created for each config file." << Qt::endl; QTextStream(stdout) << " will be created for each config file." << Qt::endl;
QTextStream(stdout) << "-d : this is the same as -i except it will not auto start the services." << Qt::endl; QTextStream(stdout) << "-d : this is the same as -i except it will not auto start the services." << Qt::endl;
@ -28,8 +28,8 @@ void showHelp(const QString etcDir)
QTextStream(stdout) << "-u : uninstall the entire app from your system, including all" << Qt::endl; QTextStream(stdout) << "-u : uninstall the entire app from your system, including all" << Qt::endl;
QTextStream(stdout) << " systemd services related to it." << Qt::endl; QTextStream(stdout) << " systemd services related to it." << Qt::endl;
QTextStream(stdout) << "-f : force an action without pausing for user confirmation." << Qt::endl; QTextStream(stdout) << "-f : force an action without pausing for user confirmation." << Qt::endl;
QTextStream(stdout) << "-l : list all attached services to this application along with statuses." << Qt::endl; QTextStream(stdout) << "-l : list all mow services along with statuses." << Qt::endl;
QTextStream(stdout) << "-r : remove all attached services." << Qt::endl; QTextStream(stdout) << "-r : remove all mow services." << Qt::endl;
} }
int main(int argc, char** argv) int main(int argc, char** argv)
@ -37,35 +37,34 @@ int main(int argc, char** argv)
QCoreApplication app(argc, argv); QCoreApplication app(argc, argv);
QCoreApplication::setApplicationName(APP_NAME); QCoreApplication::setApplicationName(APP_NAME);
QCoreApplication::setApplicationVersion(APP_VERSION); QCoreApplication::setApplicationVersion(APP_VER);
auto args = QCoreApplication::arguments(); auto args = QCoreApplication::arguments();
auto etcDir = "/etc/" + QString(APP_TARGET);
auto ret = 0; auto ret = 0;
if (args.contains("-h")) if (args.contains("-h"))
{ {
showHelp(etcDir); showHelp();
} }
else if (args.contains("-v")) else if (args.contains("-v"))
{ {
QTextStream(stdout) << APP_VERSION << Qt::endl; QTextStream(stdout) << APP_VER << Qt::endl;
} }
else if (args.contains("-l")) else if (args.contains("-l"))
{ {
servStatByDir(etcDir); servStatByDir("/etc/mow");
} }
else if (args.contains("-i") || args.contains("-d")) else if (args.contains("-i") || args.contains("-d"))
{ {
ret = rmServiceByDir(etcDir); ret = rmServiceByDir("/etc/mow");
if ((ret == 0) && args.contains("-i")) if ((ret == 0) && args.contains("-i"))
{ {
ret = loadServiceByDir(etcDir, true); ret = loadServiceByDir("/etc/mow", true);
} }
else if (ret == 0) else if (ret == 0)
{ {
ret = loadServiceByDir(etcDir, false); ret = loadServiceByDir("/etc/mow", false);
} }
} }
else if (args.contains("-c")) else if (args.contains("-c"))
@ -83,11 +82,11 @@ int main(int argc, char** argv)
{ {
if (args.contains("-f")) if (args.contains("-f"))
{ {
ret = rmServiceByDir(etcDir); ret = rmServiceByDir("/etc/mow");
if (ret == 0) if (ret == 0)
{ {
ret = QProcess::execute("/opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList()); ret = QProcess::execute("/opt/mow/uninst", QStringList());
} }
} }
else else
@ -103,18 +102,18 @@ int main(int argc, char** argv)
if (ret == 0) if (ret == 0)
{ {
ret = QProcess::execute("/opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList()); ret = QProcess::execute("/opt/mow/uninst", QStringList());
} }
} }
} }
} }
else if (args.contains("-r")) else if (args.contains("-r"))
{ {
ret = rmServiceByDir(etcDir); ret = rmServiceByDir("/etc/mow");
} }
else else
{ {
showHelp(etcDir); showHelp();
} }
return ret; return ret;

View File

@ -1,11 +1,11 @@
// This file is part of JustMotion. // This file is part of Motion Watch.
// JustMotion is free software: you can redistribute it and/or modify // Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// JustMotion is distributed in the hope that it will be useful, // Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// 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.
@ -14,8 +14,6 @@
int loadService(const QString &desc, const QString &user, const QString &servName, const QString &workDir, const QString &recPath, bool start) int loadService(const QString &desc, const QString &user, const QString &servName, const QString &workDir, const QString &recPath, bool start)
{ {
Q_UNUSED(recPath);
QFile file("/lib/systemd/system/" + servName + ".service"); QFile file("/lib/systemd/system/" + servName + ".service");
auto ret = 0; auto ret = 0;
@ -45,12 +43,7 @@ int loadService(const QString &desc, const QString &user, const QString &servNam
file.write("Type=simple\n"); file.write("Type=simple\n");
file.write("User=" + user.toUtf8() + "\n"); file.write("User=" + user.toUtf8() + "\n");
file.write("Restart=always\n"); file.write("Restart=always\n");
file.write("TimeoutStopSec=infinity\n");
if (servName.contains("vid_loop"))
{
file.write("RuntimeMaxSec=61\n");
}
file.write("ExecStart=/usr/bin/env " + servName.toUtf8() + "\n\n"); file.write("ExecStart=/usr/bin/env " + servName.toUtf8() + "\n\n");
file.write("[Install]\n"); file.write("[Install]\n");
file.write("WantedBy=multi-user.target\n"); file.write("WantedBy=multi-user.target\n");
@ -97,6 +90,7 @@ int loadSh(const QString &name, const QString &exeCmd, const QString &buffDir, c
QProcess::execute("chmod", {"-v", "+x", file.fileName()}); QProcess::execute("chmod", {"-v", "+x", file.fileName()});
QProcess::execute("chown", {servUser + ":" + servGroup, "-R", buffDir}); QProcess::execute("chown", {servUser + ":" + servGroup, "-R", buffDir});
QProcess::execute("chown", {servUser + ":" + servGroup, "-R", outDir});
} }
file.close(); file.close();
@ -110,7 +104,7 @@ QString camCmdFromConf(shared_t *conf, CmdExeType type)
if (type == MAIN_LOOP) if (type == MAIN_LOOP)
{ {
ret += QString(APP_TARGET) + " -c " + conf->conf; ret += QString(APP_BIN) + " -c " + conf->conf;
} }
else else
{ {
@ -118,16 +112,15 @@ QString camCmdFromConf(shared_t *conf, CmdExeType type)
if (conf->recordUri.contains("rtsp")) if (conf->recordUri.contains("rtsp"))
{ {
ret += "-rtsp_transport udp "; ret += "-rtsp_transport tcp ";
} }
if (conf->vidCodec != "copy") if (conf->streamCodec != "copy")
{ {
ret += "-vf fps=" + QString::number(conf->recFps) + ",scale=" + conf->recScale + " "; ret += "-vf fps=" + QString::number(conf->recFps) + ",scale=" + conf->recScale + " ";
} }
ret += "-vcodec " + conf->vidCodec + " "; ret += "-vcodec " + conf->streamCodec + " ";
ret += "-acodec " + conf->audCodec + " ";
ret += "-reset_timestamps 1 -sc_threshold 0 -g 2 -force_key_frames \"expr:gte(t, n_forced * 2)\" -t 60 -segment_time 2 -f segment "; ret += "-reset_timestamps 1 -sc_threshold 0 -g 2 -force_key_frames \"expr:gte(t, n_forced * 2)\" -t 60 -segment_time 2 -f segment ";
ret += conf->buffPath + "/live/" + QString(STRFTIME_FMT) + conf->streamExt; ret += conf->buffPath + "/live/" + QString(STRFTIME_FMT) + conf->streamExt;
} }

View File

@ -1,14 +1,14 @@
#ifndef SERVICES_H #ifndef SERVICES_H
#define SERVICES_H #define SERVICES_H
// This file is part of JustMotion. // This file is part of Motion Watch.
// JustMotion is free software: you can redistribute it and/or modify // Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// JustMotion is distributed in the hope that it will be useful, // Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// 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.

View File

@ -1,5 +0,0 @@
#!/bin/sh
export QTDIR=$install_dir
export QT_PLUGIN_PATH=$install_dir
export LD_LIBRARY_PATH=$install_dir/lib
$install_dir/$app_target $1 $2 $3

View File

@ -1,6 +0,0 @@
#!/bin/sh
$app_target -r
rm -v /usr/bin/$app_target
rm -rv $install_dir
deluser $app_target
echo "Uninstallation Complete"