#!/usr/bin/env bash set -euo pipefail # --- Configuration (from environment variables with defaults) --- URLS="${URLS:-}" URLS_FILE="${URLS_FILE:-/app/urls.txt}" LOOP="${LOOP:-1}" PROTOCOL="${PROTOCOL:-udp}" # udp | rtp | rtmp | icecast TARGET="${TARGET:-udp://239.0.0.1:1234?ttl=16}" CODEC="${CODEC:-aac}" COPY_CODEC_WHEN_POSSIBLE="${COPY_CODEC_WHEN_POSSIBLE:-1}" BITRATE="${BITRATE:-160k}" SAMPLE_RATE="${SAMPLE_RATE:-48000}" FFMPEG_EXTRA_ARGS="${FFMPEG_EXTRA_ARGS:-}" # --- Globals --- PID_FILE="/tmp/ffmpeg.pid" PLAYLIST_FILE="/tmp/playlist.txt" declare -a URL_LIST=() declare -a CODEC_ARGS=() declare -a OUTPUT_FORMAT_ARGS=() # --- Functions --- # Graceful cleanup on exit cleanup() { rm -f "$PID_FILE" "$PLAYLIST_FILE" } trap cleanup EXIT # Populates URL_LIST from environment or file get_urls() { local raw_urls=() if [[ -n "$URLS" ]]; then IFS=$'\n,' read -r -d '' -a raw_urls < <(printf '%s\0' "$URLS") elif [[ -f "$URLS_FILE" ]]; then mapfile -t raw_urls < "$URLS_FILE" fi # Trim whitespace and remove empty entries for u in "${raw_urls[@]:-}"; do u="$(echo "$u" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" [[ -n "$u" ]] && URL_LIST+=("$u") done } # Creates the playlist file for ffmpeg's concat demuxer build_playlist() { : > "$PLAYLIST_FILE" for u in "${URL_LIST[@]}"; do printf "file '%s'\n" "$u" >> "$PLAYLIST_FILE" done } # Sets OUTPUT_FORMAT_ARGS based on the streaming protocol get_output_format_args() { case "$PROTOCOL" in udp) OUTPUT_FORMAT_ARGS=(-f mpegts) ;; rtp) OUTPUT_FORMAT_ARGS=(-f rtp) ;; rtmp) OUTPUT_FORMAT_ARGS=(-f flv) ;; icecast) OUTPUT_FORMAT_ARGS=(-content_type audio/mpeg -f mp3) ;; *) echo "Unsupported PROTOCOL: $PROTOCOL" >&2; exit 2 ;; esac } # Sets CODEC_ARGS for copying or re-encoding get_codec_args() { if [[ "$COPY_CODEC_WHEN_POSSIBLE" == "1" && "$PROTOCOL" != "icecast" ]]; then CODEC_ARGS=(-c:a copy) elif [[ "$PROTOCOL" == "icecast" ]]; then # Icecast typically requires MP3 CODEC_ARGS=(-c:a libmp3lame -b:a "$BITRATE" -ar "$SAMPLE_RATE" -ac 2) else # Default re-encode CODEC_ARGS=(-c:a "$CODEC" -b:a "$BITRATE" -ar "$SAMPLE_RATE" -ac 2) fi } # Runs a single ffmpeg instance run_ffmpeg() { local proto_whitelist="file,crypto,data,subfile,http,https,tcp,tls,pipe" # Run in background to allow this script to wait and manage it ffmpeg -hide_banner -nostats -v info \ -protocol_whitelist "$proto_whitelist" \ -re -stream_loop -1 -f concat -safe 0 -i "$PLAYLIST_FILE" \ -vn "${CODEC_ARGS[@]}" \ "${OUTPUT_FORMAT_ARGS[@]}" \ $FFMPEG_EXTRA_ARGS \ "$TARGET" & echo $! > "$PID_FILE" wait $! } # --- Main Execution --- main() { get_urls if [[ ${#URL_LIST[@]} -eq 0 ]]; then echo "No URLs provided. Set URLS env or mount a file at $URLS_FILE." >&2 exit 1 fi get_output_format_args get_codec_args build_playlist echo "Starting stream from ${#URL_LIST[@]} URL(s) → $PROTOCOL → $TARGET" if [[ "$LOOP" == "1" ]]; then while true; do set +e # Prevent exit on non-zero ffmpeg exit code run_ffmpeg local exit_code=$? set -e echo "FFmpeg exited with code $exit_code; restarting in 2s..." sleep 2 # Rebuild playlist in case URLs have changed/expired build_playlist done else run_ffmpeg fi } main "$@"