127 lines
3.5 KiB
Bash
127 lines
3.5 KiB
Bash
#!/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 "$@"
|