diff options
Diffstat (limited to 'tools')
-rwxr-xr-x | tools/ipcam_motion_worker.sh | 236 | ||||
-rwxr-xr-x | tools/ipcam_motion_worker_multiple.sh | 49 | ||||
-rw-r--r-- | tools/lib.bash | 122 | ||||
-rwxr-xr-x | tools/process-motion-timecodes.py | 26 | ||||
-rwxr-xr-x | tools/video-util.sh | 94 |
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 |