New packages: - luci-app-webradio: Web radio management with Lyrion bridge tab - luci-app-turn: TURN/STUN server UI for WebRTC (Jitsi integration) - secubox-app-lyrion-bridge: Lyrion → Squeezelite → FFmpeg → Icecast pipeline - secubox-app-squeezelite: Squeezelite audio player with FIFO output - secubox-app-turn: TURN server with ACME SSL and Jitsi setup - secubox-app-webradio: Icecast/ezstream web radio server Features: - HTTPS streaming via HAProxy (stream.gk2.secubox.in) - Lyrion Music Server bridge for streaming playlists to Icecast - TURN server with time-limited credential generation - CrowdSec integration for WebRadio security - Schedule-based radio programming with jingles Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
125 lines
3.8 KiB
Bash
125 lines
3.8 KiB
Bash
#!/bin/sh
|
|
# Lyrion to Icecast Bridge - FFmpeg Audio Pipeline
|
|
# Reads PCM from Squeezelite FIFO and streams to Icecast
|
|
|
|
LOG_FILE="/var/log/lyrion-bridge.log"
|
|
|
|
log() {
|
|
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE"
|
|
logger -t lyrion-bridge "$1"
|
|
}
|
|
|
|
uci_get() { uci -q get "lyrion-bridge.$1" 2>/dev/null || echo "$2"; }
|
|
|
|
# Load configuration
|
|
INPUT_FIFO=$(uci_get audio.input_fifo "/tmp/squeezelite.pcm")
|
|
SAMPLE_RATE=$(uci_get audio.sample_rate "44100")
|
|
CHANNELS=$(uci_get audio.channels "2")
|
|
FORMAT=$(uci_get audio.format "s16le")
|
|
|
|
ICECAST_HOST=$(uci_get icecast.host "127.0.0.1")
|
|
ICECAST_PORT=$(uci_get icecast.port "8000")
|
|
ICECAST_MOUNT=$(uci_get icecast.mount "/lyrion")
|
|
ICECAST_PASS=$(uci_get icecast.password "hackme")
|
|
BITRATE=$(uci_get icecast.bitrate "192")
|
|
STREAM_NAME=$(uci_get icecast.name "Lyrion Stream")
|
|
STREAM_DESC=$(uci_get icecast.description "Streaming from Lyrion Music Server")
|
|
STREAM_GENRE=$(uci_get icecast.genre "Various")
|
|
|
|
METADATA_SYNC=$(uci_get metadata.sync_enabled "1")
|
|
METADATA_INTERVAL=$(uci_get metadata.sync_interval "5")
|
|
|
|
log "Starting Lyrion to Icecast bridge..."
|
|
log "Input: $INPUT_FIFO (PCM $FORMAT, ${SAMPLE_RATE}Hz, ${CHANNELS}ch)"
|
|
log "Output: icecast://${ICECAST_HOST}:${ICECAST_PORT}${ICECAST_MOUNT} (MP3 ${BITRATE}kbps)"
|
|
|
|
# Ensure FIFO exists
|
|
if [ ! -p "$INPUT_FIFO" ]; then
|
|
log "Creating FIFO: $INPUT_FIFO"
|
|
mkfifo "$INPUT_FIFO"
|
|
fi
|
|
|
|
# Wait for Squeezelite to start writing to FIFO
|
|
log "Waiting for audio input..."
|
|
while [ ! -s "$INPUT_FIFO" ] && [ -p "$INPUT_FIFO" ]; do
|
|
sleep 1
|
|
done
|
|
|
|
# Build Icecast URL
|
|
ICECAST_URL="icecast://source:${ICECAST_PASS}@${ICECAST_HOST}:${ICECAST_PORT}${ICECAST_MOUNT}"
|
|
|
|
# Metadata update function (background process)
|
|
update_metadata() {
|
|
local lyrion_host=$(uci_get main.lyrion_server "127.0.0.1")
|
|
local lyrion_port=$(uci_get main.lyrion_port "9000")
|
|
|
|
while true; do
|
|
# Query Lyrion for current track info
|
|
local status=$(curl -s "http://${lyrion_host}:${lyrion_port}/jsonrpc.js" \
|
|
-d '{"id":1,"method":"slim.request","params":["",["status","-",1,"tags:adl"]]}' 2>/dev/null)
|
|
|
|
if [ -n "$status" ]; then
|
|
local title=$(echo "$status" | jsonfilter -e '@.result.playlist_loop[0].title' 2>/dev/null)
|
|
local artist=$(echo "$status" | jsonfilter -e '@.result.playlist_loop[0].artist' 2>/dev/null)
|
|
local album=$(echo "$status" | jsonfilter -e '@.result.playlist_loop[0].album' 2>/dev/null)
|
|
|
|
if [ -n "$title" ]; then
|
|
local metadata="${artist:+$artist - }${title}"
|
|
# Update Icecast metadata via admin API
|
|
curl -s "http://admin:$(uci -q get webradio.main.admin_password)@${ICECAST_HOST}:${ICECAST_PORT}/admin/metadata?mount=${ICECAST_MOUNT}&mode=updinfo&song=$(echo "$metadata" | sed 's/ /%20/g')" >/dev/null 2>&1
|
|
fi
|
|
fi
|
|
|
|
sleep "$METADATA_INTERVAL"
|
|
done
|
|
}
|
|
|
|
# Start metadata sync in background
|
|
if [ "$METADATA_SYNC" = "1" ]; then
|
|
update_metadata &
|
|
METADATA_PID=$!
|
|
log "Metadata sync started (PID: $METADATA_PID)"
|
|
fi
|
|
|
|
# Cleanup on exit
|
|
cleanup() {
|
|
log "Stopping bridge..."
|
|
[ -n "$METADATA_PID" ] && kill $METADATA_PID 2>/dev/null
|
|
[ -n "$FFMPEG_PID" ] && kill $FFMPEG_PID 2>/dev/null
|
|
exit 0
|
|
}
|
|
trap cleanup INT TERM
|
|
|
|
# Main streaming loop
|
|
while true; do
|
|
log "Starting FFmpeg stream..."
|
|
|
|
ffmpeg -re \
|
|
-f $FORMAT -ar $SAMPLE_RATE -ac $CHANNELS \
|
|
-i "$INPUT_FIFO" \
|
|
-acodec libmp3lame -ab ${BITRATE}k -ar $SAMPLE_RATE -ac $CHANNELS \
|
|
-f mp3 \
|
|
-content_type audio/mpeg \
|
|
"$ICECAST_URL" \
|
|
2>> "$LOG_FILE" &
|
|
|
|
FFMPEG_PID=$!
|
|
log "FFmpeg started (PID: $FFMPEG_PID)"
|
|
|
|
# Wait for FFmpeg to exit
|
|
wait $FFMPEG_PID
|
|
EXIT_CODE=$?
|
|
|
|
log "FFmpeg exited with code $EXIT_CODE"
|
|
|
|
# If exited normally (0 or 255), it means stream ended - wait and retry
|
|
if [ $EXIT_CODE -eq 0 ] || [ $EXIT_CODE -eq 255 ]; then
|
|
log "Stream ended, waiting for new audio..."
|
|
sleep 2
|
|
else
|
|
# Error - wait longer before retry
|
|
log "Stream error, retrying in 5 seconds..."
|
|
sleep 5
|
|
fi
|
|
done
|