Asl3 - ezstream auto reconnect / nptee alternative

I’ve been migrating from hamvoip to ASL3. One of the things I have setup is ezstream. I’ve setup using these directions: Streaming Node Audio to Broadcastify - AllStarLink Manual

The problem I run into is if there’s a network hiccup - ezstream will toss a socket error, show it reconnected, but broadcastify will show it offline. I have to restart asterisk to resume.

Aug 31 09:58:21 r145 ezstream[317456]: connected: http://audio9.broadcastify.com:80/redacted
Aug 31 09:58:21 r145 ezstream[317456]: streaming: standard input
Sep 01 01:18:27 r145 ezstream[317456]: stream: default: send: audio9.broadcastify.com: error -4: Socket error
Sep 01 01:18:27 r145 ezstream[317456]: reconnect: audio9.broadcastify.com: attempt #1 ...
Sep 01 01:18:27 r145 ezstream[317456]: reconnect: audio9.broadcastify.com: success
Sep 01 01:18:27 r145 ezstream[317456]: streaming: standard input

Under hamvoip, they have a tool called nptee that can use pipes to give a bit of separation from asterisk and ezstream. This was done mainly so that a socket error wouldn’t block restart of asterisk, but it also makes it useful to restart. I would restart ezstream process itself (and I could even do a script to check the status with broadcastify).

There is no nptee in ASL3. I could try compiling it for ASL3, but wondered if there other solutions that people have come up with.

The source for NPTEE is available somewhere on HamVoIP’s website. I have not used it on ASL3, but I did use it for a while on ASL 1.01.

I must confess that I’m still using HamVoIP to stream to two different servers – broadcastify and my own custom Icecast server at a higher bitrate with much less delay, which is attached to an Amazon Alexa skill. I’m not using NPTEE, though, because it adds a bunch of latency. I have each stream connected to a private node. I never have to touch it. If the connection to Broadcastify or my own local server breaks, it silently retries in the background and doesn’t seem to do anything bad, as is apparently the case with ASL3, which I haven’t tried using for this purpose since the beginning of ASL3’s release.

Can you please clarify what you mean by this? ASL doesn't distribute or modify sox or ezstreamer so any problems with ezstreamer behavior isn't ASL's fault to fix but it would be good to know what behavior you're seeing. I've had no problems with my standard setup.

Maybe I should have put this in 3rd party software. ezstream is installed by default on the ASL3 image, but it’s probably not supported at the same level as say, asterisk. I don’t think its specifically an ASL issue, but I

Anyways - what I see is that if there’s a disconnect, then broadcastify will report is as offline; even if the log entry reports success reconnecting. I have to restart asterisk so it re-runs the ezstream command.

Next time it happens, I may try sending it a SIGUSR1

  Triggers rereading of metadata for the stream by running the configured program or script.  This  is
            the only meaningful signal when streaming from standard input.

My current setup follows the doc page pretty closely:

outstreamcmd = /bin/sh,-c,/usr/bin/lame --preset cbr 16 -r -m m -s 8 --bitwidth 16 - - | /usr/bin/ezstream -qvc /etc/ezstream.xml

ezstream.xml

<ezstream>
    <servers>
        <server>
            <protocol>HTTP</protocol>
	    <hostname>audio9.broadcastify.com</hostname>
            <port>80</port>
	    <password>redacted</password>
            <tls>none</tls>
        </server>
    </servers>

    <streams>
        <stream>
		<mountpoint>/zredacted</mountpoint>
            <format>MP3</format>
	    <stream_name>BCARS.org 145.490 MHz Linked Repeater</stream_name>
	    <stream_url>https://wwww.bcars.org/</stream_url>
                <stream_genre>Amateur Radio</stream_genre>
		<stream_description>145.490 Bedford/Kinton</stream_description>
                <stream_bitrate>16</stream_bitrate>
                <stream_channels>1</stream_channels>
                <stream_samplerate>22050</stream_samplerate>
                <stream_public>Yes</stream_public>
        </stream>
    </streams>

    <intakes>
        <intake>
            <type>stdin</type>
        </intake>
    </intakes>
</ezstream>

Unfortunately, ASL doesn't support ezstream at all. In fact, I was marginally surprised it's still in Debian 13 Trixie because the code's been untouched/unmaintained for five years. The Broadcastify/RadioReference "Pi Appliance" uses the newer Darkice streamer. From what I've seen, Darkice can read raw PCM audio from from a named pipe and deliver it directly to Broadcastify. You could create the pipe in advance with mkfifo or have a small C or Python program that creates it (as needed) and just copies the PCM audio from outstremcmd's stdin to the named pipe.

"Do something better for Broadcastify" is theoretically on a list somewhere, but if anyone has a strong interest in the topic and wants to come up with a modern, workable solution that's robust I'm sure we'd update the documentation.

One comment I will make is the solution cannot include nptee since that is not, to my knowledge, open source software with a compatible license.

FWIW, sox, ffmpeg, and pipectl are all possible things for the outstreamcmd= command to write to a FIFO.

1 Like

I’m back. I’ve spent some time working on this and think I have a straighforward ffmpeg solution. I’m not a broadcastify or ffmpeg expert. My solution has about a 20-40 second delay to broadcastify, so maybe someone can improve upon that.

Get Setup

First, you’ll need to install ffmpeg.

sudo apt update
sudo apt install ffmpeg

You can test an audio-less broadcast - run this, substituting your own feed credentials.

export BROADCASTIFY_SERVER="audio9.broadcastify.com"
export BROADCASTIFY_PORT="80"
export BROADCASTIFY_MOUNT="/yourmount"
export BROADCASTIFY_PASSWORD="hunter2"
export BROADCASTIFY_URL="icecast://source:${BROADCASTIFY_PASSWORD}@${BROADCASTIFY_SERVER}:${BROADCASTIFY_PORT}${BROADCASTIFY_MOUNT}"
ffmpeg -hide_banner -nostdin -loglevel info \
  -f lavfi -i anullsrc=r=22050:cl=mono \
  -c:a libmp3lame -b:a 32k -content_type audio/mpeg -f mp3 \
  -legacy_icecast 1 -ice_user source \
  "${BROADCASTIFY_URL}"
```

You should see output like this, and after 20-40 seconds your feed will turn green/online.

ffmpeg -hide_banner -nostdin -loglevel info   -f lavfi -i anullsrc=r=22050:cl=mono   -c:a libmp3lame -b:a 32k -content_type audio/mpeg -f mp3   -legacy_icecast 1  "${BROADCASTIFY_URL}"
Input #0, lavfi, from 'anullsrc=r=22050:cl=mono':
  Duration: N/A, start: 0.000000, bitrate: 176 kb/s
  Stream #0:0: Audio: pcm_u8, 22050 Hz, mono, u8, 176 kb/s
Stream mapping:
  Stream #0:0 -> #0:0 (pcm_u8 (native) -> mp3 (libmp3lame))
Output #0, mp3, to 'icecast://source:hunter2@audio9.broadcastify.com:80/amountpoint':
  Metadata:
    TSSE            : Lavf59.27.100
  Stream #0:0: Audio: mp3, 22050 Hz, mono, s16p, 32 kb/s
    Metadata:
      encoder         : Lavc59.37.100 libmp3lame
size=    1971kB time=00:08:24.45 bitrate=  32.0kbits/s speed=78.4x

Create The Script

This script is made to be re-usable - you won't have to edit it to put your credentials in, unless you are
doing dev testing.

# instead of editing and chmod, install creates the empty file and sets ownership permissions
sudo install -o asterisk -g asterisk -m 755 /dev/null /usr/local/bin/allstar-to-broadcastify.sh
sudo nano /usr/local/bin/allstar-to-broadcastify.sh

Content:

#!/usr/bin/env bash
#
# allstar-to-broadcastify.sh
# Reads RAW PCM (s16le/8k/mono) on stdin (from Asterisk outstreamcmd)
# and pushes MP3 to Broadcastify (Icecast).
#
# Uses an optional .env file (default: /etc/allstar-broadcastify.env)
# See sample .env at the end of this message.

set -euo pipefail

### ---------- Config / .env ---------- ###
ENV_FILE="${ENV_FILE:-/etc/allstar-broadcastify.env}"
# Auto-import variables from ENV_FILE if present
if [[ -f "$ENV_FILE" ]]; then
  set -a
  # shellcheck disable=SC1090
  . "$ENV_FILE"
  set +a
fi

# Required (or provided via .env)
BROADCASTIFY_SERVER="${BROADCASTIFY_SERVER:-}"
BROADCASTIFY_PORT="${BROADCASTIFY_PORT:-80}"
BROADCASTIFY_MOUNT="${BROADCASTIFY_MOUNT:-}"
BROADCASTIFY_PASSWORD="${BROADCASTIFY_PASSWORD:-}"
# If URL not provided, derive it. (FFmpeg expects a leading slash in the mount.)
if [[ -z "${BROADCASTIFY_URL:-}" ]]; then
  case "$BROADCASTIFY_MOUNT" in
    /*) mount_norm="$BROADCASTIFY_MOUNT" ;;
    *)  mount_norm="/$BROADCASTIFY_MOUNT" ;;
  esac
  BROADCASTIFY_URL="icecast://source:${BROADCASTIFY_PASSWORD}@${BROADCASTIFY_SERVER}:${BROADCASTIFY_PORT}${mount_norm}"
fi

# Audio settings
IN_AR="${IN_AR:-8000}"             # Asterisk outstreamcmd default is s16le, 8 kHz, mono
OUT_AR="${OUT_AR:-22050}"          # Broadcastify-friendly
BITRATE="${BITRATE:-32k}"          # MP3 CBR bitrate

# Logging / runtime
LOG_FILE="${LOG_FILE:-/var/log/asterisk/allstar-bcast.log}"
STATS_FILE="${STATS_FILE:-/var/log/asterisk/allstar-bcast.stats}"
DEBUG="${DEBUG:-0}"                # 1 = ffmpeg -stats/-progress to STATS_FILE
ROTATE_MAX_BYTES="${ROTATE_MAX_BYTES:-1048576}" # 1 MiB
ROTATE_KEEP="${ROTATE_KEEP:-3}"
BROADCASTIFY_LOCK_FILE="${BROADCASTIFY_LOCK_FILE:-/run/asterisk/allstar-bcast.lock}"

# Retry
RETRY_DELAY_SECS="${RETRY_DELAY_SECS:-3}"
RETRY_JITTER_MAX="${RETRY_JITTER_MAX:-2}"

# Self-test (generate silence instead of using stdin)
SELF_TEST="${SELF_TEST:-0}"        # 1 = use anullsrc at OUT_AR

### ---------- Helpers ---------- ###
touch "$LOG_FILE"

rotate_logs() {
  local f="$1" max_bytes="$2" keep="$3"
  [[ -f "$f" ]] || return 0
  local size; size=$(wc -c <"$f" 2>/dev/null || echo 0)
  [[ "$size" -lt "$max_bytes" ]] && return 0
  for (( i=keep-1; i>=1; i-- )); do
    [[ -f "${f}.${i}" ]] && mv -f "${f}.${i}" "${f}.$((i+1))"
  done
  mv -f "$f" "${f}.1"
  : > "$f"
}
ts() { date +"%Y-%m-%d %H:%M:%S%z"; }
log() { rotate_logs "$LOG_FILE" "$ROTATE_MAX_BYTES" "$ROTATE_KEEP"; printf "%s %s\n" "$(ts)" "$*" >>"$LOG_FILE"; }
mask_pw() { sed -E 's#(source:)[^@]+@#\1***@#'; }

# Validate required inputs
if [[ -z "$BROADCASTIFY_SERVER" || -z "$BROADCASTIFY_MOUNT" || -z "$BROADCASTIFY_PASSWORD" ]]; then
  echo "Missing required BROADCASTIFY_* vars. Set them in $ENV_FILE or env. Aborting." >&2
  exit 2
fi

# Acquire non-blocking lock so only one instance runs
exec 9>"$BROADCASTIFY_LOCK_FILE" || true
if ! flock -n 9; then
  log "[WARN] Another instance appears to be running (lock: $BROADCASTIFY_LOCK_FILE). Exiting."
  exit 1
fi

### ---------- Build ffmpeg args ---------- ###
FF_MISC=(-hide_banner -nostdin)

if [[ "$DEBUG" == "1" ]]; then
  : > "$STATS_FILE" || true
  FF_LOG=(-loglevel info -stats -stats_period 5 -progress "$STATS_FILE")
  log "[INFO] Debug mode ON. Writing ffmpeg progress/stats to $STATS_FILE"
else
  FF_LOG=(-loglevel warning -nostats)
fi

# Input: either stdin (from Asterisk) or test tone
if [[ "$SELF_TEST" == "1" ]]; then
  FF_IN=(-f lavfi -re -i "anullsrc=r=${OUT_AR}:cl=mono")
else
  FF_IN=(-f s16le -ar "$IN_AR" -ac 1 -i -)
fi

# Filters & encoding
FF_FILTER=(-af aresample=async=1:min_hard_comp=0.100:first_pts=0 -ar "$OUT_AR" -ac 1)
FF_ENC=(-c:a libmp3lame -b:a "$BITRATE" -content_type audio/mpeg -f mp3)

# Icecast / Broadcastify flags
FF_ICECAST=(-legacy_icecast 1)

URL="$BROADCASTIFY_URL"

log "[INFO] Running as user=$(id -un) uid=$(id -u) group=$(id -gn) gid=$(id -g)"
log "[INFO] Starting stream to $(echo "$URL" | mask_pw) (bitrate $BITRATE, out_ar $OUT_AR, debug=$DEBUG, self_test=$SELF_TEST)"

trap 'log "[INFO] Caught SIGINT"; exit 130' INT
trap 'log "[INFO] Caught SIGTERM"; exit 143' TERM

### ---------- Main loop ---------- ###
while :; do
  rotate_logs "$LOG_FILE" "$ROTATE_MAX_BYTES" "$ROTATE_KEEP"
  [[ "$DEBUG" == "1" ]] && rotate_logs "$STATS_FILE" "$ROTATE_MAX_BYTES" "$ROTATE_KEEP"

  log "[INFO] Launching ffmpeg…"
  ffmpeg "${FF_MISC[@]}" "${FF_LOG[@]}" \
         "${FF_IN[@]}" "${FF_FILTER[@]}" "${FF_ENC[@]}" \
         "${FF_ICECAST[@]}" \
         "$URL" >>"$LOG_FILE" 2>&1 || true

  ec=$?
  log "[WARN] ffmpeg exited with code $ec"

  # small randomized backoff before reconnecting
  jitter=0
  if command -v shuf >/dev/null 2>&1; then
    jitter=$(shuf -i 0-"$RETRY_JITTER_MAX" -n 1)
  fi
  sleep_time=$(( RETRY_DELAY_SECS + jitter ))
  log "[INFO] Retrying in ${sleep_time}s…"
  sleep "$sleep_time"
done

Create the Config

sudo install -o asterisk -g asterisk -m 660 /dev/null /etc/allstar-broadcastify.env
sudo nano /etc/allstar-broadcastify.env

Content:

# Broadcastify creds (required)
BROADCASTIFY_SERVER="audio9.broadcastify.com"   # your audio server
BROADCASTIFY_PORT="80"                 	        
BROADCASTIFY_MOUNT="/yourmountpoint"            # leading slash required by ffmpeg
BROADCASTIFY_PASSWORD="yourpassword""

# Derived URL (optional; if omitted, script builds it from vars above)
# BROADCASTIFY_URL="icecast://source:${BROADCASTIFY_PASSWORD}@${BROADCASTIFY_SERVER}:${BROADCASTIFY_PORT}${BROADCASTIFY_MOUNT}"

# Audio / behavior (optional)
IN_AR="8000"
OUT_AR="22050"
BITRATE="32k"
# WARNING: enabling debug will log  your credentials to the log file
DEBUG="0"                              # 1 = write ffmpeg -progress to stats file
SELF_TEST="0"                          # 1 = generate silence (no Asterisk needed)


# Logging / retry (optional)
LOG_FILE="/var/log/asterisk/allstar-bcast.log"
STATS_FILE="/var/log/asterisk/allstar-bcast.stats"
RETRY_DELAY_SECS="3"
RETRY_JITTER_MAX="2"

Add to Asterisk

Once you've setup your env file and the script, add the outstreamcmd to rpt.conf

outstreamcmd = /usr/local/bin/allstar-to-broadcastify.sh

Restart Asterisk and Monitor

sudo astres.sh
tail -f /var/log/asterisk/allstar-bcast.log

Without debug your log output looks like this. If the log file doesn’t get created or is empty, double check your script and


2025-09-13 17:30:10-0400 [INFO] Running as user=asterisk uid=106 group=asterisk gid=112
2025-09-13 17:30:10-0400 [INFO] Starting stream to icecast://source:***@audio9.broadcastify.com:80/yourmount (bitrate 32k, out_ar 22050, debug=0, tls=0, self_test=0)
2025-09-13 17:30:10-0400 [INFO] Launching ffmpeg…
Guessed Channel Layout for Input Stream #0.0 : mono

Then when you restart, if you enable debug, you should see something like this. Keep in mind that debug does outuput your creds as part of the URL. Ony use this when debugging issues, and plan to clean up your log files afterwards.

2025-09-13 17:28:41-0400 [INFO] Debug mode ON. Writing ffmpeg progress/stats to /var/log/asterisk/allstar-bcast.stats
2025-09-13 17:28:41-0400 [INFO] Running as user=asterisk uid=106 group=asterisk gid=112
2025-09-13 17:28:41-0400 [INFO] Starting stream to icecast://source:***@audio9.broadcastify.com:80/yourmount (bitrate 32k, out_ar 22050, debug=1, tls=0, self_test=0)
2025-09-13 17:28:41-0400 [INFO] Launching ffmpeg…
ffmpeg stats and -progress period set to 5.
Guessed Channel Layout for Input Stream #0.0 : mono
Input #0, s16le, from 'pipe:':
  Duration: N/A, bitrate: 128 kb/s
  Stream #0:0: Audio: pcm_s16le, 8000 Hz, mono, s16, 128 kb/s
Stream mapping:
  Stream #0:0 -> #0:0 (pcm_s16le (native) -> mp3 (libmp3lame))
Output #0, mp3, to 'icecast://source:hunter2@audio9.broadcastify.com:80/yourmount':
  Metadata:
    TSSE            : Lavf59.27.100
  Stream #0:0: Audio: mp3, 22050 Hz, mono, s16p, 32 kb/s
    Metadata:
      encoder         : Lavc59.37.100 libmp3lame
size=      27kB time=00:00:06.95 bitrate=  32.3kbits/s speed=1.39x

Troubleshooting

Check. your source file for syntax errors (extra quotes, unquoted lines, etc)

asl@r146:/ $ sudo source /etc/allstar-broadcastify.env
-bash: /etc/allstar-broadcastify.env: line 22: unexpected EOF while looking for matching `"'

Run it in test (no audio mode). You may have to shut down asterisk to have it clear the run/lock file.

# sudo astdn.sh
sudo DEBUG=1 SELF_TEST=1 /usr/local/bin/allstar-to-broadcastify.sh

Running this command won’t ouptut anything, but it should write to /var/log/asterisk/allstar-bcast.log

I wonder… Has anyone tried using ffmpeg instead of ezstream? Seems like perhaps it might work better. I haven’t yet.

Right… That’s what I get for not reading further down the thread.

Broadcastify has a pretty substantial delay server-side no matter what you do on the client end due to a combination of low bitrate and buffering.

One of my mountpoints using ezstream to an Icecast server with no prebuffer yields a little more than half a second of delay between app_rpt and a streaming client. That exact setup on Broadcastify, only changing the bitrate and mountpoint, is more like 45 seconds.

About 7 years ago, I used ffmpeg for an unrelated project streaming to Icecast, and I don’t remember there being a substantial delay client-side, but I was also dealing with much higher bitrates.

This topic was automatically closed 3 days after the last reply. New replies are no longer allowed.