Compare commits

..

5 Commits

Author SHA1 Message Date
zii
33519acf7a v3.4
-added the conf open option to the file menu.
-added /installers to the git ignore file so the large installer
 files don't get included in the git repo.
-the build script will no longer include libc because it doesn't
 need it and having it was causing seg faults on the deployed app.
-releasing to main.
2024-04-14 08:01:54 -04:00
zii
553585e275 v3.4.t4
-changed the app name to JustMotion to match the git repository.
-changed the build system away from cmake to qmake now that the
 app doesn't depend on openCV.
-changed the build/install scripts to python based scripts which
 now includes the ability create an installer.
2024-04-10 20:51:56 -04:00
zii
c5393484c2 v3.4.t3
-added aud_codec to the conf file so audio codec is now a
 configurable option.

-renamed stream_codec to vid_codec on the conf file to contrast
 aud_codec.

-rtsp streams now use udp instead of tcp.

-updated README with conf file changes.
2024-04-03 17:28:53 -04:00
zii
330393667e v3.4.t2
- RuntimeMaxSec only needs to apply for vid_loop services.
  condition added.

- The app will no longer attempt to take ownership of the
  footage folder.
2024-02-24 21:26:36 -05:00
zii
4ce8a75d9d v3.4.t1
- the recorder service is having problems with ffmpeg stalling out
  once again. added "RuntimeMaxSec" to the service files to see
  if that helps.
2024-02-24 21:10:06 -05:00
19 changed files with 734 additions and 231 deletions

5
.gitignore vendored
View File

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

View File

@ -1,29 +0,0 @@
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})

20
JustMotion.pro Normal file
View File

@ -0,0 +1,20 @@
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,21 +1,38 @@
# Motion Watch # # JustMotion #
Motion Watch is a video surveillance application that monitors the video feeds JustMotion is a video surveillance application that monitors the video feeds
of an IP camera and records only footage that contains motion. The main of an IP or local camera and records only footage that contains motion. The
advantage of this is reduced storage requirements as opposed to continuous main advantage of this is reduced storage requirements as opposed to
recording because only video footage of interest is recorded to storage. continuous recording because only video footage of interest is recorded to
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: mow <argument> Usage: jmotion <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 camera instance. -c : path to the config file used to run a single main loop instance.
-d : path to a directory that can contain multiple config files. -i : all valid config files found in /etc/jmotion will be used to create
each file found in the directory will be used to run a a full instance; full meaning main and vid loop systemd services
camera instance. will be created for each config file.
-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 ###
@ -78,14 +95,20 @@ img_comp_out = stderr
# use to output the comparison score. this can only be stderr or stdout, # use to output the comparison score. this can only be stderr or stdout,
# any other stream name is considered invalid. # any other stream name is considered invalid.
# #
stream_codec = copy vid_codec = copy
# this is the encoding codec to use when recording footage from the camera. # this is the encoding codec to use when recording footage from the camera.
# 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 camera itself. # from the camera itself.
# #
stream_ext = .avi aud_codec = copy
# 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.
@ -94,7 +117,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 = .avi rec_ext = .mkv
# 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.
@ -121,13 +144,12 @@ 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 # from the camera. this has no affect if using 'copy' as the vid_codec.
# 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 stream_codec. # has no affect of using 'copy' as the vid_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
@ -146,18 +168,20 @@ service_group = mow
# service_user will be used. # service_user will be used.
``` ```
### Setup/Build/Install ### ### 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 the QT API. systems that are capable of installing python3 and the QT API (QT6.X.X or
better).
``` ```
sh ./setup.sh <--- only need to run this once if compiling for the first ./build.py <--run this first
sh ./build.sh time or if upgrading from the ground up. ./install.py <--run this next
sh ./install.sh
``` ```
``` ```
note 1: be sure to run setup.sh and install.sh as root (or use sudo). note 1: the build script will search for the QT api installed in your
note 2: if building from scratch the following scripts will need to system. if not found, it will ask you where it is. either way
be run in this order - setup.sh -> build.sh -> install.sh. it is recommended to install the QT API before running this
script.
note 2: both scripts assume python3 is already installed.
``` ```

233
build.py Executable file
View File

@ -0,0 +1,233 @@
#!/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()

View File

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

View File

@ -1,13 +0,0 @@
#!/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

336
install.py Executable file
View File

@ -0,0 +1,336 @@
#!/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)

View File

@ -1,54 +0,0 @@
#!/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

View File

@ -1,26 +0,0 @@
#!/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 Motion Watch. // This file is part of JustMotion.
// Motion Watch is free software: you can redistribute it and/or modify // JustMotion 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.
// Motion Watch is distributed in the hope that it will be useful, // JustMotion 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 Motion Watch. // This file is part of JustMotion.
// Motion Watch is free software: you can redistribute it and/or modify // JustMotion 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.
// Motion Watch is distributed in the hope that it will be useful, // JustMotion 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 Motion Watch. // This file is part of JustMotion.
// Motion Watch is free software: you can redistribute it and/or modify // JustMotion 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.
// Motion Watch is distributed in the hope that it will be useful, // JustMotion 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.
@ -213,27 +213,26 @@ bool rdConf(const QString &filePath, shared_t *share)
share->buffPath.clear(); share->buffPath.clear();
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;
share->skipCmd = false; share->skipCmd = false;
share->postSecs = 60; share->postSecs = 60;
share->evMaxSecs = 30; share->evMaxSecs = 30;
share->conf = filePath; share->conf = filePath;
share->outputType = "stderr"; share->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->streamCodec = "copy"; share->vidCodec = "copy";
share->streamExt = ".avi"; share->audCodec = "copy";
share->recExt = ".avi"; 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->servUser = APP_BIN; share->imgScale = "320:240";
share->servUser = APP_TARGET;
QString line; QString line;
@ -254,7 +253,8 @@ 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("stream_codec = ", line, &share->streamCodec); rdLine("vid_codec = ", line, &share->vidCodec);
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_BIN) + ".main_loop." + share->camName; share->servMainLoop = QString(APP_TARGET) + ".main_loop." + share->camName;
share->servVidLoop = QString(APP_BIN) + ".vid_loop." + share->camName; share->servVidLoop = QString(APP_TARGET) + ".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 Motion Watch. // This file is part of JustMotion.
// Motion Watch is free software: you can redistribute it and/or modify // JustMotion 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.
// Motion Watch is distributed in the hope that it will be useful, // JustMotion 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_VER "3.3" #define APP_VERSION "3.4"
#define APP_NAME "Motion Watch" #define APP_NAME "JustMotion"
#define APP_BIN "mow" #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&"
@ -65,7 +65,8 @@ struct shared_t
QString recPath; QString recPath;
QString outputType; QString outputType;
QString compCmd; QString compCmd;
QString streamCodec; QString vidCodec;
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 Motion Watch. // This file is part of JustMotion.
// Motion Watch is free software: you can redistribute it and/or modify // JustMotion 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.
// Motion Watch is distributed in the hope that it will be useful, // JustMotion 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() void showHelp(const QString etcDir)
{ {
QTextStream(stdout) << APP_NAME << " " << APP_VER << Qt::endl << Qt::endl; QTextStream(stdout) << APP_NAME << " " << APP_VERSION << Qt::endl << Qt::endl;
QTextStream(stdout) << "Usage: mow <argument>" << Qt::endl << Qt::endl; QTextStream(stdout) << "Usage: " << APP_TARGET << " <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 /etc/mow will be used to create" << Qt::endl; QTextStream(stdout) << "-i : all valid config files found in " << etcDir << " 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()
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 mow services along with statuses." << Qt::endl; QTextStream(stdout) << "-l : list all attached services to this application along with statuses." << Qt::endl;
QTextStream(stdout) << "-r : remove all mow services." << Qt::endl; QTextStream(stdout) << "-r : remove all attached services." << Qt::endl;
} }
int main(int argc, char** argv) int main(int argc, char** argv)
@ -37,34 +37,35 @@ 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_VER); QCoreApplication::setApplicationVersion(APP_VERSION);
auto args = QCoreApplication::arguments(); auto args = QCoreApplication::arguments();
auto ret = 0; auto etcDir = "/etc/" + QString(APP_TARGET);
auto ret = 0;
if (args.contains("-h")) if (args.contains("-h"))
{ {
showHelp(); showHelp(etcDir);
} }
else if (args.contains("-v")) else if (args.contains("-v"))
{ {
QTextStream(stdout) << APP_VER << Qt::endl; QTextStream(stdout) << APP_VERSION << Qt::endl;
} }
else if (args.contains("-l")) else if (args.contains("-l"))
{ {
servStatByDir("/etc/mow"); servStatByDir(etcDir);
} }
else if (args.contains("-i") || args.contains("-d")) else if (args.contains("-i") || args.contains("-d"))
{ {
ret = rmServiceByDir("/etc/mow"); ret = rmServiceByDir(etcDir);
if ((ret == 0) && args.contains("-i")) if ((ret == 0) && args.contains("-i"))
{ {
ret = loadServiceByDir("/etc/mow", true); ret = loadServiceByDir(etcDir, true);
} }
else if (ret == 0) else if (ret == 0)
{ {
ret = loadServiceByDir("/etc/mow", false); ret = loadServiceByDir(etcDir, false);
} }
} }
else if (args.contains("-c")) else if (args.contains("-c"))
@ -82,11 +83,11 @@ int main(int argc, char** argv)
{ {
if (args.contains("-f")) if (args.contains("-f"))
{ {
ret = rmServiceByDir("/etc/mow"); ret = rmServiceByDir(etcDir);
if (ret == 0) if (ret == 0)
{ {
ret = QProcess::execute("/opt/mow/uninst", QStringList()); ret = QProcess::execute("/opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList());
} }
} }
else else
@ -102,18 +103,18 @@ int main(int argc, char** argv)
if (ret == 0) if (ret == 0)
{ {
ret = QProcess::execute("/opt/mow/uninst", QStringList()); ret = QProcess::execute("/opt/" + QString(APP_TARGET) + "/uninstall.sh", QStringList());
} }
} }
} }
} }
else if (args.contains("-r")) else if (args.contains("-r"))
{ {
ret = rmServiceByDir("/etc/mow"); ret = rmServiceByDir(etcDir);
} }
else else
{ {
showHelp(); showHelp(etcDir);
} }
return ret; return ret;

View File

@ -1,11 +1,11 @@
// This file is part of Motion Watch. // This file is part of JustMotion.
// Motion Watch is free software: you can redistribute it and/or modify // JustMotion 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.
// Motion Watch is distributed in the hope that it will be useful, // JustMotion 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.
@ -13,7 +13,9 @@
#include "services.h" #include "services.h"
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;
@ -43,7 +45,12 @@ 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");
@ -90,7 +97,6 @@ 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();
@ -104,7 +110,7 @@ QString camCmdFromConf(shared_t *conf, CmdExeType type)
if (type == MAIN_LOOP) if (type == MAIN_LOOP)
{ {
ret += QString(APP_BIN) + " -c " + conf->conf; ret += QString(APP_TARGET) + " -c " + conf->conf;
} }
else else
{ {
@ -112,15 +118,16 @@ QString camCmdFromConf(shared_t *conf, CmdExeType type)
if (conf->recordUri.contains("rtsp")) if (conf->recordUri.contains("rtsp"))
{ {
ret += "-rtsp_transport tcp "; ret += "-rtsp_transport udp ";
} }
if (conf->streamCodec != "copy") if (conf->vidCodec != "copy")
{ {
ret += "-vf fps=" + QString::number(conf->recFps) + ",scale=" + conf->recScale + " "; ret += "-vf fps=" + QString::number(conf->recFps) + ",scale=" + conf->recScale + " ";
} }
ret += "-vcodec " + conf->streamCodec + " "; ret += "-vcodec " + conf->vidCodec + " ";
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 Motion Watch. // This file is part of JustMotion.
// Motion Watch is free software: you can redistribute it and/or modify // JustMotion 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.
// Motion Watch is distributed in the hope that it will be useful, // JustMotion 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

@ -0,0 +1,5 @@
#!/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

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