summaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
Diffstat (limited to 'bin')
-rwxr-xr-xbin/ipcam_capture.sh119
-rwxr-xr-xbin/ipcam_motion_worker.sh327
-rwxr-xr-xbin/ipcam_rtsp2hls.sh127
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