I've been facing the same issue for a long time, and unfortunately the only working resolution was to disable HLS generation by nginx-rtmp-module itself and switch to ffmpeg instead (previously suggested by @sasuke-uchiha).
Relevant excerpts from nginx configuration:
rtmp {
server {
listen 127.0.0.1:1935;
application cctv {
allow publish 127.0.0.0/24;
deny publish all;
allow play all;
live on;
# HLS fragments generation
hls off;
# maintain HLS via ffmpeg instead of nginx-rtmp-module
exec_push /home/cctv/bin/rtmp2hls $name;
exec_kill_signal term;
# rest of RTMP configuration (recorders etc)
}
}
}
The rtmp2hls
script is a simple shell script which is intended to run ffmpeg for every connected publisher and ensure to kill it once SIGTERM is received from nginx (pasting here with minor edits):
#!/bin/bash -e
SPOOL="/var/spool/cctv"
LOGS="/var/log/cctv"
SELF=$(basename "$0")
_ffmpeg_pid=
log () {
logger --id $$ "$SELF: $*"
}
_teardown() {
if [ -n "${_ffmpeg_pid}" ]; then
if ! kill "${_ffmpeg_pid}"; then
log "ffmpeg (${_ffmpeg_pid}) was not running"
else
log "ffmpeg ${_ffmpeg_pid} killed"
fi
_ffmpeg_pid=
fi
log "stopped rtmp-to-hls ffmpeg for ${CAMERA_ID}"
exit 0
}
if [ $# -lt 1 ]; then
echo "usage: $(basename "$0") camera_id" >&2
exit 1
fi
CAMERA_ID="$1"
PULL_URL="rtmp://127.0.0.1/cctv/${CAMERA_ID}"
while read -r fname; do
log "removing stale stream: ${fname}"
rm -f "${fname}"
done < <(find "${SPOOL}/hls" -type f -name "${CAMERA_ID}*.ts" | sort)
# see also:
# https://ffmpeg.org/ffmpeg-formats.html#hls-2
# https://www.martin-riedl.de/2020/04/17/using-ffmpeg-as-a-hls-streaming-server-overview/
ffmpeg \
-i "${PULL_URL}" \
-nostats \
-c copy \
-sc_threshold 0 \
-f hls \
-hls_time 6 \
-hls_list_size 6 \
-hls_delete_threshold 1 \
-hls_start_number_source epoch \
-hls_flags independent_segments+delete_segments+program_date_time \
"${SPOOL}/hls/${CAMERA_ID}.m3u8" 2>>"${LOGS}/${CAMERA_ID}.ffmpeg-hls.log" &
_ffmpeg_pid=$!
log "started rtmp-to-hls ffmpeg (pid=${_ffmpeg_pid}) for ${CAMERA_ID} from ${PULL_URL} to ${SPOOL}/hls/${CAMERA_ID}.m3u8"
trap "_teardown" INT TERM
wait "${_ffmpeg_pid}"
I've been testing this with nginx/1.27.1, nginx-rtmp-module/1.2.2, and ffmpeg/5.1.6 for a few weeks, and everything works just fine. With this setup I was able to get almost clean report from Apple's mediastreamvalidator
tool:
For comparison, here's what I was getting with plain nginx-rtmp-module:
Interestingly enough, even with nginx-rtmp-module's HLS browsers (Safari on macOS or iPad) were regularly fetching playlists and stream data, but never show anything except a spinning wheel.