diff options
Diffstat (limited to 'bin')
-rwxr-xr-x | bin/ipcam_capture.sh | 119 | ||||
-rwxr-xr-x | bin/ipcam_motion_worker.sh | 327 | ||||
-rwxr-xr-x | bin/ipcam_rtsp2hls.sh | 127 |
3 files changed, 573 insertions, 0 deletions
diff --git a/bin/ipcam_capture.sh b/bin/ipcam_capture.sh new file mode 100755 index 0000000..b97c856 --- /dev/null +++ b/bin/ipcam_capture.sh @@ -0,0 +1,119 @@ +#!/bin/bash + +PROGNAME="$0" +PORT=554 +IP= +CREDS= +DEBUG=0 +CHANNEL=1 +FORCE_UDP=0 +FORCE_TCP=0 +EXTENSION="mp4" + +die() { + echo >&2 "error: $@" + exit 1 +} + +usage() { + cat <<EOF +usage: $PROGNAME [OPTIONS] COMMAND + +Options: + --outdir output directory + --ip camera IP + --port RTSP port (default: 554) + --creds + --debug + --force-tcp + --force-udp + --channel 1|2 + +EOF + exit +} + +validate_channel() { + local c="$1" + case "$c" in + 1|2) + : + ;; + *) + die "Invalid channel" + ;; + esac +} + +[ -z "$1" ] && usage + +while [[ $# -gt 0 ]]; do + case "$1" in + --ip | --port | --creds | --outdir) + _var=${1:2} + _var=${_var^^} + printf -v "$_var" '%s' "$2" + shift + ;; + + --debug) + DEBUG=1 + ;; + + --force-tcp) + FORCE_TCP=1 + ;; + + --force-udp) + FORCE_UDP=1 + ;; + + --channel) + CHANNEL="$2" + shift + ;; + + --mov) + EXTENSION="mov" + ;; + + --mpv) + EXTENSION="mpv" + ;; + + *) + die "Unrecognized argument: $1" + ;; + esac + shift +done + +[ -z "$OUTDIR" ] && die "You must specify output directory (--outdir)." +[ -z "$IP" ] && die "You must specify camera IP address (--ip)." +[ -z "$PORT" ] && die "Port can't be empty." +[ -z "$CREDS" ] && die "You must specify credentials (--creds)." +validate_channel "$CHANNEL" + +if [ ! -d "${OUTDIR}" ]; then + mkdir "${OUTDIR}" || die "Failed to create ${OUTDIR}/${NAME}!" + echo "Created $OUTDIR." +fi + +args= +if [ "$DEBUG" = "1" ]; then + args="$args -v info" +else + args="$args -nostats -loglevel warning" +fi + +if [ "$FORCE_TCP" = "1" ]; then + args="$args -rtsp_transport tcp" +elif [ "$FORCE_UDP" = "1" ]; then + args="$args -rtsp_transport udp" +fi + +[ ! -z "$CREDS" ] && CREDS="${CREDS}@" + +ffmpeg $args -i rtsp://${CREDS}${IP}:${PORT}/Streaming/Channels/${CHANNEL} \ + -c copy -f segment -strftime 1 -segment_time 00:10:00 -segment_atclocktime 1 \ + "$OUTDIR/record_%Y-%m-%d-%H.%M.%S.${EXTENSION}" 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 diff --git a/bin/ipcam_rtsp2hls.sh b/bin/ipcam_rtsp2hls.sh new file mode 100755 index 0000000..c321820 --- /dev/null +++ b/bin/ipcam_rtsp2hls.sh @@ -0,0 +1,127 @@ +#!/bin/bash + +PROGNAME="$0" +OUTDIR=/var/ipcamfs # should be tmpfs +PORT=554 +NAME= +IP= +USER= +PASSWORD= +DEBUG=0 +CHANNEL=1 +FORCE_UDP=0 +FORCE_TCP=0 +CUSTOM_PATH= + +die() { + echo >&2 "error: $@" + exit 1 +} + +usage() { + cat <<EOF +usage: $PROGNAME [OPTIONS] COMMAND + +Options: + --ip camera IP + --port RTSP port (default: 554) + --name camera name (chunks will be stored under $OUTDIR/{name}/) + --user + --password + --debug + --force-tcp + --force-udp + --channel 1|2 + --custom-path PATH + +EOF + exit +} + +validate_channel() { + local c="$1" + case "$c" in + 1|2) + : + ;; + *) + die "Invalid channel" + ;; + esac +} + +[ -z "$1" ] && usage + +while [[ $# -gt 0 ]]; do + case "$1" in + --ip|--port|--name|--user|--password) + _var=${1:2} + _var=${_var^^} + printf -v "$_var" '%s' "$2" + shift + ;; + + --debug) + DEBUG=1 + ;; + + --force-tcp) + FORCE_TCP=1 + ;; + + --force-udp) + FORCE_UDP=1 + ;; + + --channel) + CHANNEL="$2" + shift + ;; + + --custom-path) + CUSTOM_PATH="$2" + shift + ;; + + *) + die "Unrecognized argument: $1" + ;; + esac + shift +done + +[ -z "$IP" ] && die "You must specify camera IP address (--ip)." +[ -z "$PORT" ] && die "Port can't be empty." +[ -z "$NAME" ] && die "You must specify camera name (--name)." +[ -z "$USER" ] && die "You must specify username (--user)." +[ -z "$PASSWORD" ] && die "You must specify username (--password)." +validate_channel "$CHANNEL" + +if [ ! -d "${OUTDIR}/${NAME}" ]; then + mkdir "${OUTDIR}/${NAME}" || die "Failed to create ${OUTDIR}/${NAME}!" +fi + +args= +if [ "$DEBUG" = "1" ]; then + args="-v info" +else + args="-nostats -loglevel error" +fi + +if [ "$FORCE_TCP" = "1" ]; then + args="$args -rtsp_transport tcp" +elif [ "$FORCE_UDP" = "1" ]; then + args="$args -rtsp_transport udp" +fi + +if [ -z "$CUSTOM_PATH" ]; then + path="/Streaming/Channels/${CHANNEL}" +else + path="$CUSTOM_PATH" +fi + +ffmpeg $args -i "rtsp://${USER}:${PASSWORD}@${IP}:${PORT}${path}" \ + -c:v copy -c:a copy -bufsize 1835k \ + -pix_fmt yuv420p \ + -flags -global_header -hls_time 2 -hls_list_size 3 -hls_flags delete_segments \ + ${OUTDIR}/${NAME}/live.m3u8 |