summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2022-06-08 22:01:22 +0300
committerEvgeny Zinoviev <me@ch1p.io>2022-06-11 19:05:51 +0300
commiteb502ab9c94cc8a89a29f9310e2f56404b432053 (patch)
tree70ec81d40a9fc406960d85773436d3b33a014c7a /tools
parent1ed87f69878b85daf94cde4c7b187939d9e15778 (diff)
ipcam: rewrite motion detection system
Diffstat (limited to 'tools')
-rwxr-xr-xtools/ipcam_motion_worker.sh236
-rwxr-xr-xtools/ipcam_motion_worker_multiple.sh49
-rw-r--r--tools/lib.bash122
-rwxr-xr-xtools/process-motion-timecodes.py26
-rwxr-xr-xtools/video-util.sh94
5 files changed, 411 insertions, 116 deletions
diff --git a/tools/ipcam_motion_worker.sh b/tools/ipcam_motion_worker.sh
new file mode 100755
index 0000000..52ca487
--- /dev/null
+++ b/tools/ipcam_motion_worker.sh
@@ -0,0 +1,236 @@
+#!/bin/bash
+
+set -e
+
+DIR="$( cd "$( dirname "$(realpath "${BASH_SOURCE[0]}")" )" &>/dev/null && pwd )"
+PROGNAME="$0"
+
+. "$DIR/lib.bash"
+
+allow_multiple=
+config_file="$HOME/.config/ipcam_motion_worker/config.txt"
+declare -A config=()
+
+usage() {
+ cat <<EOF
+usage: $PROGNAME OPTIONS
+
+Options:
+ -c|--config FILE configuration file, default is $config_file
+ -v, -vx be verbose.
+ -v enables debug logs.
+ -vx does \`set -x\`, may be used to debug the script.
+ --allow-multiple don't check for another instance
+EOF
+ exit 1
+}
+
+get_recordings_dir() {
+ curl -s "${config[api_url]}/api/camera/list" \
+ | jq ".response.\"${config[camera]}\".recordings_path" | tr -d '"'
+}
+
+# returns two words per line:
+# filename filesize
+get_recordings_list() {
+ curl -s "${config[api_url]}/api/recordings/${config[camera]}?filter=motion" \
+ | jq '.response.files[] | [.name, .size] | join(" ")' | tr -d '"'
+}
+
+report_failure() {
+ local file="$1"
+ local message="$2"
+ local response=$(curl -s -X POST "${config[api_url]}/api/motion/fail/${config[camera]}" \
+ -F "filename=$file" \
+ -F "message=$message")
+ print_response_error "$response" "report_failure"
+}
+
+report_timecodes() {
+ local file="$1"
+ local timecodes="$2"
+ local response=$(curl -s -X POST "${config[api_url]}/api/motion/done/${config[camera]}" \
+ -F "filename=$file" \
+ -F "timecodes=$timecodes")
+ print_response_error "$response" "report_timecodes"
+}
+
+print_response_error() {
+ local resp="$1"
+ local sufx="$2"
+
+ local error="$(echo "$resp" | jq '.error')"
+ local message
+
+ if [ "$error" != "null" ]; then
+ message="$(echo "$resp" | jq '.message' | tr -d '"')"
+ error="$(echo "$error" | tr -d '"')"
+
+ echoerr "$sufx: $error ($message)"
+ fi
+}
+
+get_roi_file() {
+ if [ -n "${config[roi_file]}" ]; then
+ file="${config[roi_file]}"
+ if ! [[ "$file" =~ ^/.* ]]; then
+ file="$(dirname "$config_file")/$file"
+ fi
+
+ debug "get_roi_file: detected file $file"
+ [ -f "$file" ] || die "invalid roi_file: $file: no such file"
+
+ echo "$file"
+ fi
+}
+
+process_local() {
+ local recdir="$(get_recordings_dir)"
+ local tc
+ local words
+ local file
+
+ while read line; do
+ words=($line)
+ file=${words[0]}
+
+ debug "processing $file..."
+
+ tc=$(do_motion "${recdir}/$file")
+ debug "$file: timecodes=$tc"
+
+ report_timecodes "$file" "$tc"
+ done < <(get_recordings_list)
+}
+
+process_remote() {
+ local tc
+ local url
+ local words
+ local file
+ local size
+
+ pushd "${config[fs_root]}" >/dev/null || die "failed to change to ${config[fs_root]}"
+ touch tmp || die "directory '${config[fs_root]}' is not writable"
+ rm tmp
+
+ [ -f "video.mp4" ] && {
+ echowarn "video.mp4 already exists in ${config[fs_root]}, removing.."
+ rm "video.mp4"
+ }
+
+ while read line; do
+ words=($line)
+ file=${words[0]}
+ size=${words[1]}
+
+ if (( size > config[fs_max_filesize] )); then
+ echoerr "won't download $file, size exceedes fs_max_filesize ($size > ${config[fs_max_filesize]})"
+ report_failure "$file" "too large file"
+ continue
+ fi
+
+ url="${config[api_url]}/api/recordings/${config[camera]}/download/${file}"
+ debug "downloading $url..."
+
+ if ! download "$url" "video.mp4"; then
+ echoerr "failed to download $file"
+ report_failure "$file" "download error"
+ continue
+ fi
+
+ tc=$(do_motion "video.mp4")
+ debug "$file: timecodes=$tc"
+
+ report_timecodes "$file" "$tc"
+
+ rm "video.mp4"
+ done < <(get_recordings_list)
+
+ popd >/dev/null
+}
+
+do_motion() {
+ local input="$1"
+ local roi_file="$(get_roi_file)"
+
+ local timecodes=()
+ if [ -z "$roi_file" ]; then
+ timecodes+=($(dvr_scan "$input"))
+ else
+ echoinfo "using roi sets from file: ${BOLD}$roi_file"
+ while read line; do
+ if ! [[ "$line" =~ ^#.* ]]; then
+ timecodes+=("$(dvr_scan "$input" "$line")")
+ fi
+ done < <(cat "$roi_file")
+ fi
+
+ timecodes="${timecodes[@]}"
+ timecodes=${timecodes// /,}
+
+ echo "$timecodes"
+}
+
+dvr_scan() {
+ local input="$1"
+ local args=
+ if [ ! -z "$2" ]; then
+ args="-roi $2"
+ echoinfo "dvr_scan(${BOLD}${input}${RST}${CYAN}): roi=($2), mt=${config[threshold]}"
+ else
+ echoinfo "dvr_scan(${BOLD}${input}${RST}${CYAN}): no roi, mt=${config[threshold]}"
+ fi
+ time_start
+ dvr-scan -q -i "$input" -so --min-event-length 3s -df 3 --frame-skip 2 -t ${config[threshold]} $args | tail -1
+ debug "dvr_scan: finished in $(time_elapsed)s"
+}
+
+[[ $# -lt 1 ]] && usage
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -c|--config)
+ config_file="$2"
+ shift; shift
+ ;;
+
+ --allow-multiple)
+ allow_multiple=1
+ shift
+ ;;
+
+ -v)
+ VERBOSE=1
+ shift
+ ;;
+
+ -vx)
+ VERBOSE=1
+ set -x
+ shift
+ ;;
+
+ *)
+ die "unrecognized argument '$1'"
+ exit 1
+ ;;
+ esac
+done
+
+if [ -z "$allow_multiple" ] && pidof -o %PPID -x "$(basename "${BASH_SOURCE[0]}")" >/dev/null; then
+ die "process already running"
+fi
+
+read_config "$config_file" config
+check_config config "api_url camera threshold"
+
+if [ -n "${config[remote]}" ]; then
+ check_config config "fs_root fs_max_filesize"
+fi
+
+if [ -z "${config[remote]}" ]; then
+ process_local
+else
+ process_remote
+fi
diff --git a/tools/ipcam_motion_worker_multiple.sh b/tools/ipcam_motion_worker_multiple.sh
new file mode 100755
index 0000000..5da6974
--- /dev/null
+++ b/tools/ipcam_motion_worker_multiple.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+
+set -e
+
+DIR="$( cd "$( dirname "$(realpath "${BASH_SOURCE[0]}")" )" &>/dev/null && pwd )"
+PROGNAME="$0"
+
+. "$DIR/lib.bash"
+
+configs=()
+
+usage() {
+ cat <<EOF
+usage: $PROGNAME [OPTIONS] CONFIG_NAME ...
+
+Options:
+ -v be verbose
+EOF
+ exit 1
+}
+
+[[ $# -lt 1 ]] && usage
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -v)
+ VERBOSE=1
+ shift
+ ;;
+
+ *)
+ configs+=("$1")
+ shift
+ ;;
+ esac
+done
+
+[ -z "$configs" ] && die "no config files supplied"
+
+if pidof -o %PPID -x "$(basename "${BASH_SOURCE[0]}")" >/dev/null; then
+ die "process already running"
+fi
+
+worker_args=
+[ "$VERBOSE" = "1" ] && worker_args="-v"
+for name in "${configs[@]}"; do
+ echoinfo "starting worker $name..."
+ $DIR/ipcam_motion_worker.sh $worker_args -c "$HOME/.config/ipcam_motion_worker/$name.txt" --allow-multiple
+done
diff --git a/tools/lib.bash b/tools/lib.bash
new file mode 100644
index 0000000..1cf459b
--- /dev/null
+++ b/tools/lib.bash
@@ -0,0 +1,122 @@
+# colored output
+# --------------
+
+BOLD=$(tput bold)
+RST=$(tput sgr0)
+RED=$(tput setaf 1)
+GREEN=$(tput setaf 2)
+YELLOW=$(tput setaf 3)
+CYAN=$(tput setaf 6)
+VERBOSE=
+
+echoinfo() {
+ >&2 echo "${CYAN}$@${RST}"
+}
+
+echoerr() {
+ >&2 echo "${RED}${BOLD}error:${RST}${RED} $@${RST}"
+}
+
+echowarn() {
+ >&2 echo "${YELLOW}${BOLD}warning:${RST}${YELLOW} $@${RST}"
+}
+
+die() {
+ echoerr "$@"
+ exit 1
+}
+
+debug() {
+ if [ -n "$VERBOSE" ]; then
+ >&2 echo "$@"
+ fi
+}
+
+
+# measuring executing time
+# ------------------------
+
+__time_started=
+
+time_start() {
+ __time_started=$(date +%s)
+}
+
+time_elapsed() {
+ local fin=$(date +%s)
+ echo $(( fin - __time_started ))
+}
+
+
+# config parsing
+# --------------
+
+read_config() {
+ local config_file="$1"
+ local dst="$2"
+
+ [ -f "$config_file" ] || die "read_config: $config_file: no such file"
+
+ local n=0
+ local failed=
+ local key
+ local value
+
+ while read line; do
+ n=$(( n+1 ))
+
+ # skip empty lines or comments
+ if [ -z "$line" ] || [[ "$line" =~ ^#.* ]]; then
+ continue
+ fi
+
+ if [[ $line = *"="* ]]; then
+ key="${line%%=*}"
+ value="${line#*=}"
+ eval "$dst[$key]=\"$value\""
+ else
+ echoerr "config: invalid line $n"
+ failed=1
+ fi
+ done < <(cat "$config_file")
+
+ [ -z "$failed" ]
+}
+
+check_config() {
+ local var="$1"
+ local keys="$2"
+
+ local failed=
+
+ for key in $keys; do
+ if [ -z "$(eval "echo -n \${$var[$key]}")" ]; then
+ echoerr "config: ${BOLD}${key}${RST}${RED} is missing"
+ failed=1
+ fi
+ done
+
+ [ -z "$failed" ]
+}
+
+
+# other functions
+# ---------------
+
+installed() {
+ command -v "$1" > /dev/null
+ return $?
+}
+
+download() {
+ local source="$1"
+ local target="$2"
+
+ if installed curl; then
+ curl -f -s -o "$target" "$source"
+ elif installed wget; then
+ wget -q -O "$target" "$source"
+ else
+ die "neither curl nor wget found, can't proceed"
+ fi
+}
diff --git a/tools/process-motion-timecodes.py b/tools/process-motion-timecodes.py
index ba4ee26..7be7977 100755
--- a/tools/process-motion-timecodes.py
+++ b/tools/process-motion-timecodes.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import os.path
+from src.home.camera.util import dvr_scan_timecodes
from argparse import ArgumentParser
from datetime import datetime, timedelta
@@ -39,31 +40,10 @@ if __name__ == '__main__':
if arg.padding < 0:
raise ValueError('invalid padding')
- timecodes = arg.timecodes.split(',')
- if len(timecodes) % 2 != 0:
- raise ValueError('invalid number of timecodes')
-
- timecodes = list(map(time2seconds, timecodes))
- timecodes = list(chunks(timecodes, 2))
-
- # sort out invalid fragments (dvr-scan returns them sometimes, idk why...)
- timecodes = list(filter(lambda f: f[0] < f[1], timecodes))
- if not timecodes:
- raise ValueError('no valid timecodes')
-
+ fragments = dvr_scan_timecodes(arg.timecodes)
file_dt = filename_to_datetime(arg.source_filename)
- # https://stackoverflow.com/a/43600953
- timecodes.sort(key=lambda interval: interval[0])
- merged = [timecodes[0]]
- for current in timecodes:
- previous = merged[-1]
- if current[0] <= previous[1]:
- previous[1] = max(previous[1], current[1])
- else:
- merged.append(current)
-
- for fragment in merged:
+ for fragment in fragments:
start, end = fragment
start -= arg.padding
diff --git a/tools/video-util.sh b/tools/video-util.sh
index 08d8938..0a148d8 100755
--- a/tools/video-util.sh
+++ b/tools/video-util.sh
@@ -5,12 +5,7 @@ set -e
DIR="$( cd "$( dirname "$(realpath "${BASH_SOURCE[0]}")" )" &> /dev/null && pwd )"
PROGNAME="$0"
-BOLD=$(tput bold)
-RST=$(tput sgr0)
-RED=$(tput setaf 1)
-GREEN=$(tput setaf 2)
-YELLOW=$(tput setaf 3)
-CYAN=$(tput setaf 6)
+. "$DIR/lib.bash"
input=
output=
@@ -18,46 +13,11 @@ command=
motion_threshold=1
ffmpeg_args="-nostats -loglevel error"
dvr_scan_args="-q"
-verbose=
config_dir=$HOME/.config/video-util
config_dir_set=
write_data_prefix=
write_data_time=
-_time_started=
-
-time_start() {
- _time_started=$(date +%s)
-}
-
-time_elapsed() {
- local _time_finished=$(date +%s)
- echo $(( _time_finished - _time_started ))
-}
-
-debug() {
- if [ -n "$verbose" ]; then
- >&2 echo "$@"
- fi
-}
-
-echoinfo() {
- >&2 echo "${CYAN}$@${RST}"
-}
-
-echoerr() {
- >&2 echo "${RED}${BOLD}error:${RST}${RED} $@${RST}"
-}
-
-echowarn() {
- >&2 echo "${YELLOW}${BOLD}warning:${RST}${YELLOW} $@${RST}"
-}
-
-die() {
- echoerr "$@"
- exit 1
-}
-
file_in_use() {
[ -n "$(lsof "$1")" ]
}
@@ -223,44 +183,6 @@ do_mass_fix_mtime() {
done
}
-do_motion() {
- local input="$1"
- local timecodes=()
- local roi_file="$config_dir/roi.txt"
- if ! [ -f "$roi_file" ]; then
- timecodes+=($(dvr_scan "$input"))
- else
- echoinfo "using roi sets from file: ${BOLD}$roi_file"
- while read line; do
- if ! [[ "$line" =~ ^#.* ]]; then
- timecodes+=("$(dvr_scan "$input" "$line")")
- fi
- done < <(cat "$roi_file")
- fi
-
- timecodes="${timecodes[@]}"
- timecodes=${timecodes// /,}
-
- if [ -z "$timecodes" ]; then
- debug "do_motion: no motion detected"
- else
- debug "do_motion: detected timecodes: $timecodes"
-
- local output_dir="$(dirname "$input")/motion"
- if ! [ -d "$output_dir" ]; then
- mkdir "$output_dir" || die "do_motion: mkdir($output_dir) failed"
- debug "do_motion: created $output_dir directory"
- fi
-
- local fragment
- while read line; do
- fragment=($line)
- debug "do_motion: writing fragment start=${fragment[0]} duration=${fragment[1]} filename=$output_dir/${fragment[2]}"
- ffmpeg $ffmpeg_args -i "$input" -ss ${fragment[0]} -t ${fragment[1]} -c copy -y "$output_dir/${fragment[2]}" </dev/null
- done < <($DIR/process-motion-timecodes.py --source-filename "$input" --timecodes "$timecodes")
- fi
-}
-
do_mass_motion() {
local input="$1"
local saved_time=$(config_get_prev_mtime motion)
@@ -285,20 +207,6 @@ do_mass_motion() {
# echo "00:05:06.930,00:05:24.063"
#}
-dvr_scan() {
- local input="$1"
- local args=
- if [ ! -z "$2" ]; then
- args="-roi $2"
- echoinfo "dvr_scan(${BOLD}${input}${RST}${CYAN}): roi=($2), mt=$motion_threshold"
- else
- echoinfo "dvr_scan(${BOLD}${input}${RST}${CYAN}): no roi, mt=$motion_threshold"
- fi
- time_start
- dvr-scan $dvr_scan_args -i "$input" -so --min-event-length 3s -df 3 --frame-skip 2 -t $motion_threshold $args | tail -1
- debug "dvr_scan: finished in $(time_elapsed)s"
-}
-
[[ $# -lt 1 ]] && usage
while [[ $# -gt 0 ]]; do