Fix audio setup.
- Use Icecast for transmitting NDI audio to Liquidsoap. - Update liquidsoap to 2.0.4-preview (mem optims) - Add new API endpoints for starting/stopping outputs.liquidsoap-2.2
parent
29ffc5f4c9
commit
b6473d1c1d
|
@ -55,3 +55,15 @@ services:
|
||||||
limits:
|
limits:
|
||||||
cpus: "2"
|
cpus: "2"
|
||||||
memory: 128M
|
memory: 128M
|
||||||
|
icecast:
|
||||||
|
image: icedream/icecast
|
||||||
|
build: images/icecast-2.4.4
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- "/share/VM_Disks/Docker/IcedreamLive/config/icecast/icecast.xml:/icecast.xml:ro"
|
||||||
|
network_mode: host
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "2"
|
||||||
|
memory: 128M
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Dockerfile
|
||||||
|
.docker*
|
||||||
|
.git
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
FROM alpine:3.15 AS share
|
||||||
|
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
WORKDIR /icecast/share/
|
||||||
|
#COPY share/ .
|
||||||
|
RUN git clone --depth=1 --recursive https://github.com/logue/icecast2-bootstrap-theme . && rm -rf .git
|
||||||
|
RUN chown 9999:0 .
|
||||||
|
RUN chmod -R a-rwx,a+rX .
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
FROM alpine:3.15 AS icecast-download
|
||||||
|
|
||||||
|
RUN apk add --no-cache curl ca-certificates
|
||||||
|
WORKDIR /usr/src/
|
||||||
|
ARG ICECAST_VERSION=2.4.4
|
||||||
|
RUN curl -L http://downloads.xiph.org/releases/icecast/icecast-${ICECAST_VERSION}.tar.gz | tar xz -v
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
FROM alpine:3.15 AS icecast
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
build-base file openssl-dev libxslt-dev \
|
||||||
|
libvorbis-dev opus-dev libogg-dev speex-dev \
|
||||||
|
libtheora-dev curl-dev
|
||||||
|
|
||||||
|
ARG ICECAST_VERSION=2.4.4
|
||||||
|
WORKDIR /usr/src/icecast-${ICECAST_VERSION}
|
||||||
|
COPY --from=icecast-download /usr/src/icecast-${ICECAST_VERSION}/ .
|
||||||
|
RUN ./configure
|
||||||
|
RUN make
|
||||||
|
RUN make install
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
FROM alpine:3.15
|
||||||
|
|
||||||
|
# add runtime deps
|
||||||
|
RUN \
|
||||||
|
apk add --no-cache file libssl1.1 libxslt libvorbis \
|
||||||
|
opus libogg speex libtheora \
|
||||||
|
libtheora curl && \
|
||||||
|
rm -rf /tmp/* /var/cache/apk/*
|
||||||
|
|
||||||
|
# add icecast user
|
||||||
|
RUN \
|
||||||
|
addgroup -g 950 icecast &&\
|
||||||
|
adduser -S -D -H -u 9999 -G icecast -s /bin/false icecast
|
||||||
|
|
||||||
|
# add mime.types file
|
||||||
|
RUN apk add --no-cache mailcap && cp /etc/mime.types /etc/mime.types.keep && apk del --no-cache mailcap && mv /etc/mime.types.keep /etc/mime.types
|
||||||
|
|
||||||
|
# add dumb-init
|
||||||
|
ARG DUMB_INIT_VERSION=1.2.5
|
||||||
|
ADD https://github.com/Yelp/dumb-init/releases/download/v${DUMB_INIT_VERSION}/dumb-init_${DUMB_INIT_VERSION}_x86_64 /usr/local/bin/dumb-init
|
||||||
|
RUN chmod +x /usr/local/bin/dumb-init
|
||||||
|
|
||||||
|
# install icecast bins
|
||||||
|
COPY --from=icecast /usr/local/ /usr/local/
|
||||||
|
|
||||||
|
# install share files
|
||||||
|
COPY --from=share /icecast/share/ /icecast/share/
|
||||||
|
|
||||||
|
#USER 9999
|
||||||
|
USER root
|
||||||
|
#VOLUME [ "/data" ]
|
||||||
|
ENTRYPOINT [ "dumb-init" ]
|
||||||
|
CMD [ "icecast", "-c", "/icecast.xml" ]
|
||||||
|
EXPOSE 8000
|
|
@ -1,5 +1,5 @@
|
||||||
ARG IMAGE=savonet/liquidsoap:v1.4.4
|
|
||||||
# ARG IMAGE=savonet/liquidsoap:master
|
# ARG IMAGE=savonet/liquidsoap:master
|
||||||
|
ARG IMAGE=savonet/liquidsoap-ci-build:v2.0.4-preview_alpine_amd64
|
||||||
|
|
||||||
# FROM $IMAGE
|
# FROM $IMAGE
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,9 @@
|
||||||
def setup_harbor_metadata_api(~metadata_api_port=21338, s) =
|
def setup_harbor_metadata_api(~metadata_api_port=21338, s) =
|
||||||
# [insert_metadata_func, source_with_new_metadata]
|
# [insert_metadata_func, source_with_new_metadata]
|
||||||
result = insert_metadata(s)
|
result = insert_metadata(s)
|
||||||
insert_metadata_func = fst(result)
|
|
||||||
s = snd(result)
|
|
||||||
|
|
||||||
# Only Liquidsoap 2.x+
|
# Only Liquidsoap 2.x+
|
||||||
# s = insert_metadata(s)
|
s = insert_metadata(s)
|
||||||
|
|
||||||
# Handler for receiving metadata`
|
# Handler for receiving metadata`
|
||||||
def on_http_metadata(~protocol, ~data, ~headers, uri) =
|
def on_http_metadata(~protocol, ~data, ~headers, uri) =
|
||||||
|
@ -20,7 +18,7 @@ def setup_harbor_metadata_api(~metadata_api_port=21338, s) =
|
||||||
], data)
|
], data)
|
||||||
|
|
||||||
m = list.assoc(default=[], "data", data)
|
m = list.assoc(default=[], "data", data)
|
||||||
insert_metadata_func(m)
|
s.insert_metadata(m)
|
||||||
http_response(protocol=protocol, code=200, headers=[
|
http_response(protocol=protocol, code=200, headers=[
|
||||||
("allow","POST"),
|
("allow","POST"),
|
||||||
("access-control-allow-origin","*"),
|
("access-control-allow-origin","*"),
|
||||||
|
@ -29,8 +27,6 @@ def setup_harbor_metadata_api(~metadata_api_port=21338, s) =
|
||||||
("access-control-allow-headers","Origin,X-Requested-With,Content-Type,Accept,Authorization,access-control-allow-headers,access-control-allow-origin"),
|
("access-control-allow-headers","Origin,X-Requested-With,Content-Type,Accept,Authorization,access-control-allow-headers,access-control-allow-origin"),
|
||||||
("content-type","application/json"),
|
("content-type","application/json"),
|
||||||
], data=json_of(data))
|
], data=json_of(data))
|
||||||
# Only Liquidsoap 2.x+
|
|
||||||
#s.insert_metadata(m)
|
|
||||||
# http.response(protocol=protocol, code=200, headers=[
|
# http.response(protocol=protocol, code=200, headers=[
|
||||||
# ("allow","POST"),
|
# ("allow","POST"),
|
||||||
# ("access-control-allow-origin","*"),
|
# ("access-control-allow-origin","*"),
|
||||||
|
@ -66,4 +62,4 @@ def setup_harbor_metadata_api(~metadata_api_port=21338, s) =
|
||||||
harbor.http.register(port=metadata_api_port, method="OPTIONS", "/", on_http_metadata_cors)
|
harbor.http.register(port=metadata_api_port, method="OPTIONS", "/", on_http_metadata_cors)
|
||||||
|
|
||||||
s
|
s
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
# Silent fallback stream that is still loud enough that it forces Vorbis/OPUS codecs to continue broadcasting data.
|
# Silent fallback stream that is still loud enough that it forces Vorbis/OPUS codecs to continue broadcasting data.
|
||||||
# Some Icecast-compatible software on the server-side tends to freak out over us not sending data for extended amount of times, for example during technical difficulties.
|
# Some Icecast-compatible software on the server-side tends to freak out over us not sending data for extended amount of times, for example during technical difficulties.
|
||||||
def mksafe_soft(s) =
|
def mksafe_soft(s) =
|
||||||
blank_s = blank()
|
|
||||||
blank_v = drop_audio(blank_s)
|
|
||||||
silent_a = amplify(0.000075, noise())
|
silent_a = amplify(0.000075, noise())
|
||||||
silent_s = mux_video(video=blank_v, silent_a)
|
fallback(track_sensitive=false, [s, silent_a])
|
||||||
mksafe(fallback(track_sensitive=false, [s, silent_s]))
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,43 +10,50 @@ set("server.telnet.port", 21337)
|
||||||
set("init.allow_root",true)
|
set("init.allow_root",true)
|
||||||
set("frame.video.width", 1920)
|
set("frame.video.width", 1920)
|
||||||
set("frame.video.height", 1080)
|
set("frame.video.height", 1080)
|
||||||
|
set("audio.converter.samplerate.libsamplerate.quality", "best")
|
||||||
|
set("audio.converter.samplerate.native.quality","linear")
|
||||||
|
set("sandbox", "disabled")
|
||||||
|
|
||||||
%include "settings.liq"
|
%include "settings.liq"
|
||||||
%include "metadata_api.liq"
|
%include "metadata_api.liq"
|
||||||
|
%include "stream_api.liq"
|
||||||
%include "silent_fallback.liq"
|
%include "silent_fallback.liq"
|
||||||
|
|
||||||
s = input.srt(id="input_srt_main", max=3., port=9000)
|
s = input.http(id="input_ice_main", max_buffer=4., "http://127.0.0.1:61120/main")
|
||||||
output.dummy(fallible=true, s)
|
|
||||||
|
|
||||||
# Split audio off to be handled specially
|
# Split audio off to be handled specially
|
||||||
a = drop_video(s)
|
a = drop_video(s)
|
||||||
a = mksafe_soft(a)
|
a = mksafe_soft(a)
|
||||||
a = setup_harbor_metadata_api(a)
|
a = setup_harbor_metadata_api(a)
|
||||||
output.dummy(a)
|
output.dummy(a)
|
||||||
output.harbor(
|
|
||||||
id="out_a_harbor",
|
# encoded lossless stream
|
||||||
%ogg(%flac),
|
a_flac = ffmpeg.encode.audio(
|
||||||
# fallible=true,
|
%ffmpeg(%audio(codec="flac")),
|
||||||
port=8050,
|
a)
|
||||||
|
|
||||||
|
internal_icecast=output.icecast(
|
||||||
|
fallible=true,
|
||||||
|
port=61120,
|
||||||
|
host="127.0.0.1",
|
||||||
|
name=stream_name())
|
||||||
|
setup_harbor_stream_api(internal_icecast(
|
||||||
|
id="out_a_int",
|
||||||
|
%ffmpeg(format="ogg", %audio.copy),
|
||||||
mount="/outa/flac",
|
mount="/outa/flac",
|
||||||
a)
|
a_flac))
|
||||||
output.harbor(
|
|
||||||
id="out_a_harbor",
|
# REKT.fm
|
||||||
%mp3(bitrate=320),
|
setup_harbor_stream_api(output.icecast(
|
||||||
# fallible=true,
|
|
||||||
port=8050,
|
|
||||||
mount="/outa/mp3",
|
|
||||||
a)
|
|
||||||
output.icecast(
|
|
||||||
id="out_a_rekt",
|
id="out_a_rekt",
|
||||||
%mp3(bitrate=320),
|
%ffmpeg(format="ogg", %audio.copy),
|
||||||
# fallible=true,
|
fallible=true,
|
||||||
mount="rekt",
|
mount="rekt",
|
||||||
port=60000,
|
port=60000,
|
||||||
#host="stream.rekt.network",
|
#host="stream.rekt.network",
|
||||||
host="stream.rekt.fm",
|
host="stream.rekt.fm",
|
||||||
user="icedream",
|
user="icedream",
|
||||||
name=stream_name,
|
name=stream_name(),
|
||||||
password="***REMOVED***",
|
password="***REMOVED***",
|
||||||
start=false,
|
start=false,
|
||||||
a)
|
a_flac))
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
stream_api_port=21336
|
||||||
|
|
||||||
|
interactive.harbor(port=stream_api_port, uri="/interactive") # expose through stream API port
|
||||||
|
|
||||||
|
def setup_harbor_stream_api(s) =
|
||||||
|
def on_start(~protocol, ~data, ~headers, uri) =
|
||||||
|
s.start()
|
||||||
|
http.response(protocol=protocol, code=200, headers=[
|
||||||
|
("content-type","application/json"),
|
||||||
|
], data=json.stringify([]))
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_stop(~protocol, ~data, ~headers, uri) =
|
||||||
|
s.stop()
|
||||||
|
http.response(protocol=protocol, code=200, headers=[
|
||||||
|
("content-type","application/json"),
|
||||||
|
], data=json.stringify([]))
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_info(~protocol, ~data, ~headers, uri) =
|
||||||
|
data = [
|
||||||
|
("id", s.id()),
|
||||||
|
("last_metadata", json.stringify(s.last_metadata())),
|
||||||
|
("is_up", json.stringify(s.is_up())),
|
||||||
|
("is_started", json.stringify(s.is_started())),
|
||||||
|
("is_ready", json.stringify(s.is_ready())),
|
||||||
|
("is_active", json.stringify(s.is_active())),
|
||||||
|
]
|
||||||
|
http.response(protocol=protocol, code=200, headers=[
|
||||||
|
("content-type","application/json"),
|
||||||
|
], data=json.stringify(data))
|
||||||
|
end
|
||||||
|
|
||||||
|
harbor.http.register(port=stream_api_port, method="POST", "/streams/#{s.id()}/start", on_start)
|
||||||
|
harbor.http.register(port=stream_api_port, method="POST", "/streams/#{s.id()}/stop", on_stop)
|
||||||
|
harbor.http.register(port=stream_api_port, method="GET", "/streams/#{s.id()}", on_info)
|
||||||
|
|
||||||
|
s
|
||||||
|
end
|
|
@ -20,7 +20,7 @@ COPY --from=0 /target/ /
|
||||||
WORKDIR /library
|
WORKDIR /library
|
||||||
|
|
||||||
VOLUME ["/library"]
|
VOLUME ["/library"]
|
||||||
RUN addgroup -S -g 950 app
|
#RUN addgroup -S -g 950 app
|
||||||
RUN adduser -S -k /dev/empty -g "App user" -h /library -u 950 -G app app
|
#RUN adduser -S -k /dev/empty -g "App user" -h /library -u 950 -G app app
|
||||||
USER 950
|
#USER 950
|
||||||
CMD ["metacollectord"]
|
CMD ["metacollectord"]
|
||||||
|
|
|
@ -20,6 +20,7 @@ USER app
|
||||||
RUN git clone --recursive https://aur.archlinux.org/yay.git yay/
|
RUN git clone --recursive https://aur.archlinux.org/yay.git yay/
|
||||||
RUN cd yay && makepkg --noconfirm -si && cd .. && rm -r yay
|
RUN cd yay && makepkg --noconfirm -si && cd .. && rm -r yay
|
||||||
RUN yay --noconfirm -S pod2man && sudo rm -r ~/.cache /var/cache/pacman/*
|
RUN yay --noconfirm -S pod2man && sudo rm -r ~/.cache /var/cache/pacman/*
|
||||||
|
RUN yay --noconfirm -S ndi-advanced-sdk && sudo rm -r ~/.cache /var/cache/pacman/*
|
||||||
RUN yay --noconfirm -S ffmpeg-ndi && sudo rm -r ~/.cache /var/cache/pacman/*
|
RUN yay --noconfirm -S ffmpeg-ndi && sudo rm -r ~/.cache /var/cache/pacman/*
|
||||||
|
|
||||||
COPY --from=0 /target/ /
|
COPY --from=0 /target/ /
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash -e
|
#!/bin/bash -e
|
||||||
|
|
||||||
target_url="${1:-srt://127.0.0.1:9000}"
|
target_url="${1:-icecast://source:source@127.0.0.1:61120/main}"
|
||||||
ffmpeg_pid=
|
ffmpeg_pids=()
|
||||||
|
|
||||||
call_ffmpeg() {
|
call_ffmpeg() {
|
||||||
command ffmpeg -hide_banner "$@"
|
command ffmpeg -hide_banner "$@"
|
||||||
|
@ -9,23 +9,19 @@ call_ffmpeg() {
|
||||||
|
|
||||||
daemon_ffmpeg() {
|
daemon_ffmpeg() {
|
||||||
call_ffmpeg "$@" &
|
call_ffmpeg "$@" &
|
||||||
ffmpeg_pid=$!
|
ffmpeg_pids+=($!)
|
||||||
}
|
}
|
||||||
|
|
||||||
shutdown_ffmpeg() {
|
shutdown_ffmpeg() {
|
||||||
if is_ffmpeg_running
|
if is_ffmpeg_running; then
|
||||||
then
|
|
||||||
kill "$ffmpeg_pid" || true
|
kill "$ffmpeg_pid" || true
|
||||||
for t in $(seq 0 10)
|
for t in $(seq 0 10); do
|
||||||
do
|
if ! kill -0 "$ffmpeg_pid"; then
|
||||||
if ! kill -0 "$ffmpeg_pid"
|
|
||||||
then
|
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
if kill -0 "$ffmpeg_pid"
|
if kill -0 "$ffmpeg_pid"; then
|
||||||
then
|
|
||||||
kill -9 "$ffmpeg_pid" || true
|
kill -9 "$ffmpeg_pid" || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
@ -43,15 +39,13 @@ trap on_exit EXIT
|
||||||
|
|
||||||
offline=0
|
offline=0
|
||||||
|
|
||||||
while true
|
while true; do
|
||||||
do
|
|
||||||
found_audio_source=""
|
found_audio_source=""
|
||||||
|
|
||||||
while read -r line
|
while read -r line; do
|
||||||
do
|
declare -a "found_source=($(sed -e 's/"/\\"/g' -e "s/'/\"/g" -e 's/[][`~!@#$%^&*():;<>.,?/\|{}=+-]/\\&/g' <<<"$line"))"
|
||||||
declare -a "found_source=($(sed -e 's/"/\\"/g' -e "s/'/\"/g" -e 's/[][`~!@#$%^&*():;<>.,?/\|{}=+-]/\\&/g' <<< "$line"))"
|
found_source[0]=$(sed -e 's/\\\([`~!@#$%^&*():;<>.,?/\|{}=+-]\)/\1/g' <<<"${found_source[0]}")
|
||||||
found_source[0]=$(sed -e 's/\\\([`~!@#$%^&*():;<>.,?/\|{}=+-]\)/\1/g' <<< "${found_source[0]}")
|
found_source[1]=$(sed -e 's/\\\([`~!@#$%^&*():;<>.,?/\|{}=+-]\)/\1/g' <<<"${found_source[1]}")
|
||||||
found_source[1]=$(sed -e 's/\\\([`~!@#$%^&*():;<>.,?/\|{}=+-]\)/\1/g' <<< "${found_source[1]}")
|
|
||||||
case "${found_source[0]}" in
|
case "${found_source[0]}" in
|
||||||
*\(IDHPC\ Main\ Audio\))
|
*\(IDHPC\ Main\ Audio\))
|
||||||
found_audio_source="${found_source[0]}"
|
found_audio_source="${found_source[0]}"
|
||||||
|
@ -59,23 +53,27 @@ do
|
||||||
esac
|
esac
|
||||||
done < <(call_ffmpeg -loglevel info -extra_ips 192.168.188.21 -find_sources true -f libndi_newtek -i "dummy" 2>&1 | grep -Po "'(.+)'\s+'(.+)" | tee)
|
done < <(call_ffmpeg -loglevel info -extra_ips 192.168.188.21 -find_sources true -f libndi_newtek -i "dummy" 2>&1 | grep -Po "'(.+)'\s+'(.+)" | tee)
|
||||||
|
|
||||||
if [ -z "$found_audio_source" ]
|
if [ -z "$found_audio_source" ]; then
|
||||||
then
|
offline=$((offline + 1))
|
||||||
offline=$(( offline + 1 ))
|
|
||||||
else
|
else
|
||||||
offline=0
|
offline=0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! is_ffmpeg_running && [ -n "$found_audio_source" ]
|
if ! is_ffmpeg_running && [ -n "$found_audio_source" ]; then
|
||||||
then
|
|
||||||
echo "starting ffmpeg with audio source: $found_audio_source" >&2
|
echo "starting ffmpeg with audio source: $found_audio_source" >&2
|
||||||
|
|
||||||
|
call_ffmpeg -loglevel warning \
|
||||||
|
-analyzeduration 1 -f libndi_newtek -extra_ips 192.168.188.21 -i "$found_audio_source" \
|
||||||
|
-map a -c:a pcm_s16le -ar 48000 -ac 2 -f s16le - |
|
||||||
|
call_ffmpeg -loglevel warning \
|
||||||
|
-ar 48000 -channels 2 -f s16le -i - \
|
||||||
|
-map a -c:a flac -f ogg -content_type application/ogg "${target_url}" || true
|
||||||
|
|
||||||
# HACK - can't use the standard mpegts here, but liquidsoap will happily accept anything ffmpeg can parse (by default)… so let's just use nut here even though it feels super duper wrong
|
# HACK - can't use the standard mpegts here, but liquidsoap will happily accept anything ffmpeg can parse (by default)… so let's just use nut here even though it feels super duper wrong
|
||||||
daemon_ffmpeg -loglevel warning -extra_ips 192.168.188.21 -f libndi_newtek -i "$found_audio_source" -c copy -f nut -write_index false "${target_url}"
|
elif is_ffmpeg_running && [ -z "$found_audio_source" ] && [ "$offline" -gt 0 ]; then
|
||||||
elif is_ffmpeg_running && [ -z "$found_audio_source" ] && [ "$offline" -gt 0 ]
|
|
||||||
then
|
|
||||||
echo "shutting down ffmpeg since no source has been found" >&2
|
echo "shutting down ffmpeg since no source has been found" >&2
|
||||||
shutdown_ffmpeg # it won't shut down by itself unfortunately
|
shutdown_ffmpeg # it won't shut down by itself unfortunately
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
Loading…
Reference in New Issue