#!/usr/bin/env bash set -euo pipefail CAM=/dev/video0 KEY_FILE=/opt/ai/conf/whip-key START_DELAY=20 CHECK_URL=https://whip.hobostreamer.com/ BACKOFF=5 MAX_BACKOFF=300 choose_video_size() { if ! command -v v4l2-ctl >/dev/null 2>&1; then printf '%s\n' '640x480' return fi local size size=$(v4l2-ctl -d "$CAM" --list-formats-ext 2>/dev/null | awk ' /Size: Discrete/ { sz = $3; split(sz, dims, "x"); width = dims[1] + 0; height = dims[2] + 0; area = width * height; if (width <= 640 && height <= 480) { candidate = area "|" sz; } else { candidate = ""; } next } /Interval: Discrete .*30(\.000)? fps/ && candidate { print candidate; candidate = ""; } ' | sort -t'|' -k1,1nr | head -n 1 | cut -d'|' -f2) if [[ -z "$size" ]]; then printf '%s\n' '640x480' else printf '%s\n' "$size" fi } choose_video_bitrate() { case "$1" in 1280x720) printf '%s\n' '1500k' ;; 1024x576|960x720) printf '%s\n' '1200k' ;; 640x480) printf '%s\n' '900k' ;; 640x360) printf '%s\n' '900k' ;; *) printf '%s\n' '900k' ;; esac } choose_buffer_size() { case "$1" in 1500k) printf '%s\n' '3000k' ;; 1200k) printf '%s\n' '2400k' ;; 900k) printf '%s\n' '1800k' ;; *) printf '%s\n' '1800k' ;; esac } choose_frame_rate() { case "$1" in 1280x720) printf '%s\n' '15' ;; 640x480) printf '%s\n' '20' ;; *) printf '%s\n' '30' ;; esac } choose_input_format() { local target="$1" if ! command -v v4l2-ctl >/dev/null 2>&1; then printf '%s\n' 'mjpeg' return fi local format format=$(v4l2-ctl -d "$CAM" --list-formats-ext 2>/dev/null | awk -v target="$target" ' /^[[:space:]]*\[[0-9]+\]:/ { line = $0; gsub(/^[[:space:]]*\[[0-9]+\]:[[:space:]]*\x27/, "", line); gsub(/\x27.*$/, "", line); curfmt = line; next } /^[[:space:]]*Size: Discrete/ { if ($3 == target) { size_ok = 1; } else { size_ok = 0; } next } /^[[:space:]]*Interval: Discrete .*30(\.000)? fps/ && size_ok { if (curfmt == "YUYV") { print "yuyv422"; exit } if (curfmt == "MJPG") { print "mjpeg"; exit } } ') if [[ -z "$format" ]]; then printf '%s\n' 'mjpeg' else printf '%s\n' "$format" fi } check_connectivity() { if command -v curl >/dev/null 2>&1; then curl -fsS --connect-timeout 10 "$CHECK_URL" >/dev/null 2>&1 && return 0 elif command -v ping >/dev/null 2>&1; then ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1 && return 0 fi return 1 } if [[ "$START_DELAY" -gt 0 ]]; then echo "$(date) startup delay: waiting $START_DELAY seconds for network" sleep "$START_DELAY" fi while true; do if [[ ! -c "$CAM" ]]; then echo "$(date) camera missing: $CAM" sleep 10 continue fi if [[ ! -f "$KEY_FILE" ]]; then echo "$(date) key file missing: $KEY_FILE" sleep "$BACKOFF" BACKOFF=$(( BACKOFF * 2 )) if (( BACKOFF > MAX_BACKOFF )); then BACKOFF=$MAX_BACKOFF; fi continue fi STREAM_KEY="$(tr -d '\r\n ' < "$KEY_FILE")" URL="https://whip.hobostreamer.com/whip/50?key=$STREAM_KEY" if ! check_connectivity; then echo "$(date) network/HoboStreamer unreachable, waiting $BACKOFF seconds" sleep "$BACKOFF" BACKOFF=$(( BACKOFF * 2 )) if (( BACKOFF > MAX_BACKOFF )); then BACKOFF=$MAX_BACKOFF; fi continue fi VIDEO_SIZE="$(choose_video_size)" VIDEO_BITRATE="$(choose_video_bitrate "$VIDEO_SIZE")" VIDEO_BUFSIZE="$(choose_buffer_size "$VIDEO_BITRATE")" VIDEO_FPS="$(choose_frame_rate "$VIDEO_SIZE")" VIDEO_FORMAT="$(choose_input_format "$VIDEO_SIZE")" echo "$(date) using camera resolution $VIDEO_SIZE at $VIDEO_BITRATE (bufsize=$VIDEO_BUFSIZE) fps=$VIDEO_FPS format=$VIDEO_FORMAT" ffmpeg -hide_banner -loglevel info \ -f v4l2 -input_format "$VIDEO_FORMAT" -video_size "$VIDEO_SIZE" -framerate "$VIDEO_FPS" -i "$CAM" \ -f alsa -i default \ -vf format=yuv420p \ -c:v libx264 -preset ultrafast -tune zerolatency \ -profile:v baseline -level 3.0 \ -x264-params "keyint=30:min-keyint=30:scenecut=0:force-cfr=1:repeat-headers=1" \ -b:v "$VIDEO_BITRATE" -maxrate "$VIDEO_BITRATE" -bufsize "$VIDEO_BUFSIZE" \ -c:a libopus -b:a 64k -ar 48000 \ -ts_buffer_size 20M \ -f whip "$URL" echo "$(date) FFmpeg exited. retrying in $BACKOFF seconds." sleep "$BACKOFF" BACKOFF=$(( BACKOFF * 2 )) if (( BACKOFF > MAX_BACKOFF )); then BACKOFF=$MAX_BACKOFF; fi done