summaryrefslogtreecommitdiff
path: root/bin/ipcam_motion_worker.sh
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2023-09-27 00:54:57 +0300
committerEvgeny Zinoviev <me@ch1p.io>2023-09-27 00:54:57 +0300
commitd3a295872c49defb55fc8e4e43e55550991e0927 (patch)
treeb9dca15454f9027d5a9dad0d4443a20de04dbc5d /bin/ipcam_motion_worker.sh
parentb7cbc2571c1870b4582ead45277d0aa7f961bec8 (diff)
parentbdbb296697f55f4c3a07af43c9aaf7a9ea86f3d0 (diff)
Merge branch 'master' of ch1p.io:homekit
Diffstat (limited to 'bin/ipcam_motion_worker.sh')
-rwxr-xr-xbin/ipcam_motion_worker.sh327
1 files changed, 327 insertions, 0 deletions
diff --git a/bin/ipcam_motion_worker.sh b/bin/ipcam_motion_worker.sh
new file mode 100755
index 0000000..603a407
--- /dev/null
+++ b/bin/ipcam_motion_worker.sh
@@ -0,0 +1,327 @@
+#!/bin/bash
+
+set -e
+
+DIR="$( cd "$( dirname "$(realpath "${BASH_SOURCE[0]}")" )" &>/dev/null && pwd )"
+PROGNAME="$0"
+
+. "$DIR/../include/bash/include.bash"
+
+curl_opts="-s --connect-timeout 10 --retry 5 --max-time 180 --retry-delay 0 --retry-max-time 180"
+allow_multiple=
+fetch_limit=10
+
+config=
+config_camera=
+is_remote=
+api_url=
+
+dvr_scan_path="$HOME/.local/bin/dvr-scan"
+fs_root="/var/ipcam_motion_fs"
+fs_max_filesize=146800640
+
+declare -A config=()
+
+usage() {
+ cat <<EOF
+usage: $PROGNAME OPTIONS
+
+Options:
+ -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
+ --L, --fetch-limit default: $fetch_limit
+ --remote
+ --local
+ --dvr-scan-path default: $dvr_scan_path
+ --fs-root default: $fs_root
+ --fs-max-filesize default: $fs_max_filesize
+EOF
+ exit 1
+}
+
+get_recordings_dir() {
+ local camera="$1"
+ curl $curl_opts "${api_url}/api/camera/list" \
+ | jq ".response.\"${camera}\".recordings_path" | tr -d '"'
+}
+
+# returns three words per line:
+# filename filesize camera
+get_recordings_list() {
+ curl $curl_opts "${api_url}/api/recordings?limit=${fetch_limit}" \
+ | jq '.response.files[] | [.name, .size, .cam] | join(" ")' | tr -d '"'
+}
+
+read_camera_motion_config() {
+ local camera="$1"
+ local dst=config
+
+ if [ "$config_camera" != "$camera" ]; then
+ 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 < <(curl $curl_opts "${api_url}/api/motion/params/${camera}")
+
+ config_camera="$camera"
+
+ [ -z "$failed" ]
+ else
+ debug "read_camera_motion_config: config for $camera already loaded"
+ fi
+}
+
+dump_config() {
+ for key in min_event_length downscale_factor frame_skip threshold; do
+ debug "config[$key]=${config[$key]}"
+ done
+}
+
+get_camera_roi_config() {
+ local camera="$1"
+ curl $curl_opts "${api_url}/api/motion/params/${camera}/roi"
+}
+
+report_failure() {
+ local camera="$1"
+ local file="$2"
+ local message="$3"
+
+ local response=$(curl $curl_opts -X POST "${api_url}/api/motion/fail/${camera}" \
+ -F "filename=$file" \
+ -F "message=$message")
+
+ print_response_error "$response" "report_failure"
+}
+
+report_timecodes() {
+ local camera="$1"
+ local file="$2"
+ local timecodes="$3"
+
+ local response=$(curl $curl_opts -X POST "${api_url}/api/motion/done/${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
+}
+
+process_queue() {
+ local tc
+ local url
+ local words
+ local file
+ local size
+ local camera
+ local local_recs_dir
+
+ if [ "$is_remote" = "1" ]; then
+ pushd "${fs_root}" >/dev/null || die "failed to change to ${fs_root}"
+ touch tmp || die "directory '${fs_root}' is not writable"
+ rm tmp
+
+ [ -f "video.mp4" ] && {
+ echowarn "video.mp4 already exists in ${fs_root}, removing.."
+ rm "video.mp4"
+ }
+ fi
+
+ while read line; do
+ words=($line)
+ file=${words[0]}
+ size=${words[1]}
+ camera=${words[2]}
+
+ debug "next video: cam=$camera file=$file"
+
+ read_camera_motion_config "$camera"
+# dump_config
+
+ if [ "$is_remote" = "0" ]; then
+ local_recs_dir="$(get_recordings_dir "$camera")"
+
+ debug "[$camera] processing $file..."
+
+ tc=$(do_motion "$camera" "${local_recs_dir}/$file")
+ debug "[$camera] $file: timecodes=$tc"
+
+ report_timecodes "$camera" "$file" "$tc"
+ else
+ if (( size > fs_max_filesize )); then
+ echoerr "[$camera] won't download $file, size exceeds fs_max_filesize ($size > ${fs_max_filesize})"
+ report_failure "$camera" "$file" "too large file"
+ continue
+ fi
+
+ url="${api_url}/api/recordings/${camera}/download/${file}"
+ debug "[$camera] downloading $url..."
+
+ if ! download "$url" "video.mp4"; then
+ echoerr "[$camera] failed to download $file"
+ report_failure "$camera" "$file" "download error"
+ continue
+ fi
+
+ tc=$(do_motion "$camera" "video.mp4")
+ debug "[$camera] $file: timecodes=$tc"
+
+ report_timecodes "$camera" "$file" "$tc"
+
+ rm "video.mp4"
+ fi
+ done < <(get_recordings_list)
+
+ if [ "$is_remote" = "1" ]; then popd >/dev/null; fi
+}
+
+do_motion() {
+ local camera="$1"
+ local input="$2"
+ local tc
+
+ local timecodes=()
+
+ time_start
+ while read line; do
+ if ! [[ "$line" =~ ^#.* ]]; then
+ tc="$(do_dvr_scan "$input" "$line")"
+ if [ -n "$tc" ]; then
+ timecodes+=("$tc")
+ fi
+ fi
+ done < <(get_camera_roi_config "$camera")
+
+ debug "[$camera] do_motion: finished in $(time_elapsed)s"
+
+ timecodes="$(echo "${timecodes[@]}" | sed 's/ */ /g' | xargs)"
+ timecodes="${timecodes// /,}"
+
+ echo "$timecodes"
+}
+
+dvr_scan() {
+ "${dvr_scan_path}" "$@"
+}
+
+do_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
+
+ dvr_scan -q -i "$input" -so \
+ --min-event-length ${config[min_event_length]} \
+ -df ${config[downscale_factor]} \
+ --frame-skip ${config[frame_skip]} \
+ -t ${config[threshold]} $args | tail -1
+}
+
+[[ $# -lt 1 ]] && usage
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -L|--fetch-limit)
+ fetch_limit="$2"
+ shift; shift
+ ;;
+
+ --allow-multiple)
+ allow_multiple=1
+ shift
+ ;;
+
+ --remote)
+ is_remote=1
+ shift
+ ;;
+
+ --local)
+ is_remote=0
+ shift
+ ;;
+
+ --dvr-scan-path)
+ dvr_scan_path="$2"
+ shift; shift
+ ;;
+
+ --fs-root)
+ fs_root="$2"
+ shift; shift
+ ;;
+
+ --fs-max-filesize)
+ fs_max_filesize="$2"
+ shift; shift
+ ;;
+
+ --api-url)
+ api_url="$2"
+ shift; 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
+
+[ -z "$is_remote" ] && die "either --remote or --local is required"
+[ -z "$api_url" ] && die "--api-url is required"
+
+process_queue \ No newline at end of file