#!/bin/bash #### webcamd - A webcam Service for multiple Cams and Stream Services. #### #### written by Stephan Wendel aka KwadFan #### Copyright 2021 #### https://github.com/mainsail-crew/crowsnest #### #### This File is distributed under GPLv3 #### #### Version 2 ### Disable shellcheck Errors # shellcheck disable=SC2012,SC2206 # Exit upon Errors set -e ## Functions ## Version of webcamd function self_version { pushd $(dirname $(readlink -f "${0}")) &> /dev/null git describe --always --tags popd &> /dev/null } ## Message Helpers function missing_args_msg { echo -e "webcamd: Missing Arguments!" echo -e "\n\tTry: webcamd -h\n" } function wrong_args_msg { echo -e "webcamd: Wrong Arguments!" echo -e "\n\tTry: webcamd -h\n" } function help_msg { echo -e "webcamd - webcam deamon\nUsage:" echo -e "\t webcamd [Options]" echo -e "\n\t\t-h Prints this help." echo -e "\n\t\t-v Prints Version of webcamd." echo -e "\n\t\t-c \n\t\t\tPath to your webcam.conf\n" } ## Logging function init_log_entry { log_msg "webcamd - A webcam Service for multiple Cams and Stream Services." log_msg "Version: $(self_version)" log_msg "Prepare Startup ..." } function log_level { local loglevel loglevel="$(get_param webcamd log_level 2> /dev/null)" # Set default log_level to quiet if [ -z "${loglevel}" ] || [[ "${loglevel}" != @(quiet|verbose|debug) ]]; then echo "quiet" else echo "${loglevel}" fi } function develop { local devel logfile logfile="$(get_param "webcamd" log_path | sed "s#^~#$HOME#gi")" devel="$(get_param "webcamd" develop_log 2> /dev/null)" if [ "${devel}" = "true" ]; then rm -rf "${logfile}" fi } function log_msg { local msg logfile prefix msg="${1}" prefix="$(date +'[%D %T]') webcamd:" #Workaround sed ~ to BASH VAR $HOME logfile="$(get_param webcamd log_path | sed "s#^~#$HOME#gi")" #Workaround: Make Dir if not exist if [ ! -d "${logfile}" ]; then mkdir -p "$(dirname "${logfile}")" fi echo -e "${prefix} ${msg}" | tr -s ' ' >> "${logfile}" 2>&1 echo -e "${msg}" | logger -t webcamd } #call '| log_output ""' function log_output { local prefix prefix="DEBUG: ${1}" while read -r line; do if [ "$(log_level)" == "debug" ]; then log_msg "${prefix}: ${line}" fi if [ -n "${line}" ]; then # sed is needed to prettify ustreamers output logger -t webcamd "$(echo ${line} | sed 's/^--/ustreamer/')" fi done } function print_cfg { local prefix prefix="\t\t" log_msg "INFO: Print Configfile: '${WEBCAMD_CFG}'" while read -r line; do log_msg "${prefix}${line}" done < "${WEBCAMD_CFG}" } function print_cams { local count raspicam total debug debug="$(get_param "webcamd" debug_log 2> /dev/null)" count="$(find /dev/v4l/by-id/ 2> /dev/null | sed '1d;1~2d' | wc -l)" total="$((count+$(detect_raspicam)))" if [ "${total}" -eq 0 ]; then log_msg "ERROR: No usable Cameras Found. Stopping $(basename "$0")." exit 1 else log_msg "INFO: Found ${total} available Camera(s)" fi if [ -d "/dev/v4l/by-id/" ]; then detect_avail_cams fi if [ "$(detect_raspicam)" -ne 0 ]; then raspicam="$(v4l2-ctl --list-devices | grep -A1 -e 'mmal' | \ awk 'NR==2 {print $1}')" log_msg "Detected 'Raspicam' Device -> ${raspicam}" if [ ! "$(log_level)" = "quiet" ]; then list_cam_formats "${raspicam}" fi fi } 2> /dev/null ## Sanity Checks function initial_check { log_msg "INFO: Checking Dependencys" check_dep "logger" check_dep "crudini" check_dep "ustreamer" check_dep "v4l2rtspserver" # check_dep "rtsp-simple-server" # Stay for later use. if [ -z "$(check_cfg "${WEBCAMD_CFG}")" ]; then if [ "$(log_level)" != "quiet" ]; then print_cfg fi fi # in systemd show always config file logger -t webcamd -f "${WEBCAMD_CFG}" log_msg "INFO: Detect available Cameras" print_cams } function check_cfg { if [ ! -r "${1}" ]; then log_msg "ERROR: No Configuration File found. Exiting!" exit 1 fi } function check_section { local section param must_exist missing section="cam ${1}" # Ignore missing custom flags param="$(crudini --existing=param --get "${WEBCAMD_CFG}" "${section}" \ 2> /dev/null | sed '/custom_flags/d')" must_exist="streamer port device resolution max_fps" missing="$(echo "${param}" "${must_exist}" | \ tr ' ' '\n' | sort | uniq -u)" if [ -n "${missing}" ]; then log_msg "ERROR: Parameter ${missing} not found in \ Section [${section}]. Start skipped!" exit 1 else log_msg "INFO: Configuration of Section [${section}] looks good. \ Continue..." fi } function check_dep { local dep dep="$(whereis "${1}" | awk '{print $2}')" if [ -z "${dep}" ]; then log_msg "Dependency: '${1}' not found. Exiting!" exit 1 else log_msg "Dependency: '${1}' found in ${dep}." fi } ### Detect Hardware function detect_avail_cams { local avail realpath avail="$(find /dev/v4l/by-id/ 2> /dev/null | sort -n | sed '1d;1~2d')" if [ -d "/dev/v4l/by-id/" ]; then echo "${avail}" | while read -r i; do realpath=$(readlink -e ${i}) log_msg "${i} -> ${realpath}" if [ "$(log_level)" != "quiet" ]; then list_cam_formats "${i}" fi done else log_msg "ERROR: No usable Cameras found. Exiting." exit 1 fi } function list_cam_formats { local device device="${1}" formats="$(v4l2-ctl -d "${device}" --list-formats-ext | sed '1,3d')" log_msg "Supported Formats:" echo "${formats}" | while read -r i; do log_msg "\t\t${i}" done } function detect_raspicam { local avail if [ "$(cat /proc/device-tree/model | cut -d ' ' -f1)" = "Raspberry" ]; then avail="$(vcgencmd get_camera | awk -F '=' '{ print $3 }')" else avail="0" fi echo "${avail}" } ## Spits out all [cam ] configured sections function configured_cams { local cam_count cfg cfg="${WEBCAMD_CFG}" cams="$(crudini --existing=file --get "${cfg}" | \ sed '/webcamd/d;s/cam//')" echo "${cams}" } ## Start Stream Service # sleep to prevent cpu cycle spikes function construct_streamer { local stream_server cams cams=($(configured_cams)) log_msg "Try to start configured Cams / Services..." for (( i=0; i<"${#cams[@]}"; i++ )); do stream_server="$(get_param "cam ${cams[$i]}" streamer 2> /dev/null)" if [ "${stream_server}" == "ustreamer" ]; then run_ustreamer "${cams[$i]}" & sleep 8 & sleep_pid="$!" wait "${sleep_pid}" elif [ "${stream_server}" == "rtsp" ]; then run_rtsp "${cams[$i]}" & sleep 8 & sleep_pid="$!" wait "${sleep_pid}" else log_msg "ERROR: Missing 'streamer' parameter in [cam ${cams[$i]}]. Skipping." fi done log_msg "... Done!" } function run_ustreamer { local cam_section ustreamer_bin device port resolution fps custom local raspicam start_param wwwroot cam_section="${1}" ustreamer_bin="$(whereis ustreamer | awk '{print $2}')" device="$(get_param "cam ${cam_section}" device)" port=$(get_param "cam ${cam_section}" port) resolution=$(get_param "cam ${cam_section}" resolution) fps=$(get_param "cam ${cam_section}" max_fps) custom="$(get_param "cam ${cam_section}" custom_flags 2> /dev/null)" raspicam="$(v4l2-ctl --list-devices | grep -A1 -e 'mmal' | \ awk 'NR==2 {print $1}')" check_section "${cam_section}" wwwroot="$(dirname $(readlink -qe $(whereis webcamd)))/ustreamer-www" #Raspicam Workaround if [ "${device}" == "${raspicam}" ]; then start_param=( --host 127.0.0.1 -p "${port}" -m MJPEG --device-timeout=5 --buffers=3 -r "${resolution}" -f "${fps}" --allow-origin=\* --static "${wwwroot}" ) else start_param=( -d "${device}" -r "${resolution}" -f "${fps}" --host 127.0.0.1 -p "${port}" --allow-origin=\* --device-timeout=2 --static "${wwwroot}" ) fi # Custom Flag Handling if [ -n "${custom}" ]; then start_param=(${start_param[@]} "${custom}" ) fi log_msg "Starting ustreamer with Device ${device} ..." echo "Parameters: ${start_param[*]}" | \ log_output "ustreamer [cam ${cam_section}]" # Ustreamer is designed to run even if the device is not ready or readable. # I dont like that! ustreamer has to exit if Cam isnt there. if [ -e "${device}" ]; then "${ustreamer_bin}" ${start_param[*]} 2>&1 | \ log_output "ustreamer [cam ${cam_section}]" else log_msg "ERROR: Start of ustreamer [cam ${cam_section}] failed!" fi } function run_rtsp { local cam_section rtsp_bin device port resolution fps custom local raspicam start_param cam_section="${1}" rtsp_bin="$(whereis v4l2rtspserver | awk '{print $2}')" device="$(get_param "cam ${cam_section}" device)" port=$(get_param "cam ${cam_section}" port) resolution=$(get_param "cam ${cam_section}" resolution) fps=$(get_param "cam ${cam_section}" max_fps) custom="$(get_param "cam ${cam_section}" custom_flags 2> /dev/null)" check_section "${cam_section}" split_res="$(echo "${resolution}" | \ awk -F 'x' '{print "-W "$1 " -H "$2}')" start_param=( -I 0.0.0.0 -P "${port}" "${split_res}" -F "${fps}" \ "${device}" ) # Custom Flag Handling if [ -n "${custom}" ]; then start_param=(${start_param[@]} "${custom}" ) fi log_msg "Starting v4l2rtspserver with Device ${device} ..." echo "Parameters: ${start_param[*]}" | \ log_output "v4l2rtspserver [cam ${cam_section}]" "${rtsp_bin}" ${start_param[*]} 2>&1 | \ log_output "v4l2rtspserver [cam ${cam_section}]" log_msg "ERROR: Start of v4l2rtspserver [cam ${cam_section}] failed!" } ## MISC # Read Configuration File # call get_param section param # spits out raw value function get_param { local cfg local section local param cfg="${WEBCAMD_CFG}" section="${1}" param="${2}" crudini --get "${cfg}" "${section}" "${param}" | \ sed 's/\#.*//;s/[[:space:]]*$//' } 2> /dev/null function err_exit { if [ "${1}" != "0" ]; then log_msg "ERROR: Error ${1} occured on line ${2}" log_msg "ERROR: Stopping $(basename "$0")." log_msg "Goodbye..." fi if [ -n "$(jobs -pr)" ]; then kill $(jobs -pr) fi exit 1 } function shutdown { log_msg "Shutdown or Killed by User!" log_msg "Please come again :)" if [ -n "$(jobs -pr)" ]; then kill $(jobs -pr) fi log_msg "Goodbye..." exit 0 } #### Watchdog Functions and Variables ## Do not reuse previous functions! function clean_watchdog { rm -f $PWD/lost-* } function webcamd_watchdog { # Helper Functions function available { find ${1} &> /dev/null echo $? } function lost_dev { local lostfile lostfile="$(echo ${1} | awk -F '/' '{print $NF}')" touch /tmp/lost-${lostfile} } function is_lost { local lostdev lostdev="$(echo ${1} | awk -F '/' '{print $NF}')" find /tmp/lost-${lostdev} &> /dev/null echo $? } function returned_dev { local lostdev lostdev="$(echo ${1} | awk -F '/' '{print $NF}')" rm -f /tmp/lost-${lostdev} &> /dev/null } # local Vars local get_conf_devices conf_cams avail_cams # Init empty Arrays get_conf_devices=() conf_cams=() # Grab devices from config file get_conf_devices=("$(crudini --existing=file --get "${WEBCAMD_CFG}" | \ sed '/webcamd/d' | cut -d ' ' -f2)") # Construct Array with configured Devices for gcd in ${get_conf_devices[*]}; do conf_cams+=("$(crudini --get "${WEBCAMD_CFG}" "cam ${gcd}" "device" \ | awk '{print $1}')") done # Send Message if Device available or returned. for cc in ${conf_cams[*]}; do if [ "$(available ${cc})" -ne 0 ] && [ "$(is_lost ${cc})" -ne 0 ]; then log_msg "WATCHDOG: Lost Device: "${cc}"" lost_dev "${cc}" elif [ "$(is_lost ${cc})" -eq 0 ] && [ "$(available ${cc})" -eq 0 ]; then log_msg "WATCHDOG: Device ${cc} returned." returned_dev "${cc}" fi done } #### MAIN ## Args given? if [ "$#" -eq 0 ]; then missing_args_msg exit 1 fi ## Parse Args while getopts ":Vhc:" arg; do case "${arg}" in v ) echo -e "\nwebcamd Version: $(self_version)\n" exit 0 ;; h ) help_msg exit 0 ;; c ) check_cfg "${OPTARG}" WEBCAMD_CFG="${OPTARG}" ;; \?) wrong_args_msg exit 1 ;; esac done # Init Traps trap 'shutdown' 1 2 3 15 trap 'err_exit $? $LINENO' ERR develop init_log_entry initial_check construct_streamer ## Loop and Watchdog ## In this case watchdog acts more like a "cable defect detector" ## The User gets a message if Device is lost. clean_watchdog while true ; do webcamd_watchdog sleep 120 & sleep_pid="$!" wait "${sleep_pid}" done