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:
|
||||
cpus: "2"
|
||||
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-ci-build:v2.0.4-preview_alpine_amd64
|
||||
|
||||
# FROM $IMAGE
|
||||
|
||||
|
|
|
@ -7,11 +7,9 @@
|
|||
def setup_harbor_metadata_api(~metadata_api_port=21338, s) =
|
||||
# [insert_metadata_func, source_with_new_metadata]
|
||||
result = insert_metadata(s)
|
||||
insert_metadata_func = fst(result)
|
||||
s = snd(result)
|
||||
|
||||
# Only Liquidsoap 2.x+
|
||||
# s = insert_metadata(s)
|
||||
s = insert_metadata(s)
|
||||
|
||||
# Handler for receiving metadata`
|
||||
def on_http_metadata(~protocol, ~data, ~headers, uri) =
|
||||
|
@ -20,7 +18,7 @@ def setup_harbor_metadata_api(~metadata_api_port=21338, s) =
|
|||
], data)
|
||||
|
||||
m = list.assoc(default=[], "data", data)
|
||||
insert_metadata_func(m)
|
||||
s.insert_metadata(m)
|
||||
http_response(protocol=protocol, code=200, headers=[
|
||||
("allow","POST"),
|
||||
("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"),
|
||||
("content-type","application/json"),
|
||||
], data=json_of(data))
|
||||
# Only Liquidsoap 2.x+
|
||||
#s.insert_metadata(m)
|
||||
# http.response(protocol=protocol, code=200, headers=[
|
||||
# ("allow","POST"),
|
||||
# ("access-control-allow-origin","*"),
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
# 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.
|
||||
def mksafe_soft(s) =
|
||||
blank_s = blank()
|
||||
blank_v = drop_audio(blank_s)
|
||||
silent_a = amplify(0.000075, noise())
|
||||
silent_s = mux_video(video=blank_v, silent_a)
|
||||
mksafe(fallback(track_sensitive=false, [s, silent_s]))
|
||||
fallback(track_sensitive=false, [s, silent_a])
|
||||
end
|
||||
|
|
|
@ -10,43 +10,50 @@ set("server.telnet.port", 21337)
|
|||
set("init.allow_root",true)
|
||||
set("frame.video.width", 1920)
|
||||
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 "metadata_api.liq"
|
||||
%include "stream_api.liq"
|
||||
%include "silent_fallback.liq"
|
||||
|
||||
s = input.srt(id="input_srt_main", max=3., port=9000)
|
||||
output.dummy(fallible=true, s)
|
||||
s = input.http(id="input_ice_main", max_buffer=4., "http://127.0.0.1:61120/main")
|
||||
|
||||
# Split audio off to be handled specially
|
||||
a = drop_video(s)
|
||||
a = mksafe_soft(a)
|
||||
a = setup_harbor_metadata_api(a)
|
||||
output.dummy(a)
|
||||
output.harbor(
|
||||
id="out_a_harbor",
|
||||
%ogg(%flac),
|
||||
# fallible=true,
|
||||
port=8050,
|
||||
|
||||
# encoded lossless stream
|
||||
a_flac = ffmpeg.encode.audio(
|
||||
%ffmpeg(%audio(codec="flac")),
|
||||
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",
|
||||
a)
|
||||
output.harbor(
|
||||
id="out_a_harbor",
|
||||
%mp3(bitrate=320),
|
||||
# fallible=true,
|
||||
port=8050,
|
||||
mount="/outa/mp3",
|
||||
a)
|
||||
output.icecast(
|
||||
a_flac))
|
||||
|
||||
# REKT.fm
|
||||
setup_harbor_stream_api(output.icecast(
|
||||
id="out_a_rekt",
|
||||
%mp3(bitrate=320),
|
||||
# fallible=true,
|
||||
%ffmpeg(format="ogg", %audio.copy),
|
||||
fallible=true,
|
||||
mount="rekt",
|
||||
port=60000,
|
||||
#host="stream.rekt.network",
|
||||
host="stream.rekt.fm",
|
||||
user="icedream",
|
||||
name=stream_name,
|
||||
name=stream_name(),
|
||||
password="***REMOVED***",
|
||||
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
|
||||
|
||||
VOLUME ["/library"]
|
||||
RUN addgroup -S -g 950 app
|
||||
RUN adduser -S -k /dev/empty -g "App user" -h /library -u 950 -G app app
|
||||
USER 950
|
||||
#RUN addgroup -S -g 950 app
|
||||
#RUN adduser -S -k /dev/empty -g "App user" -h /library -u 950 -G app app
|
||||
#USER 950
|
||||
CMD ["metacollectord"]
|
||||
|
|
|
@ -20,6 +20,7 @@ USER app
|
|||
RUN git clone --recursive https://aur.archlinux.org/yay.git 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 ndi-advanced-sdk && sudo rm -r ~/.cache /var/cache/pacman/*
|
||||
RUN yay --noconfirm -S ffmpeg-ndi && sudo rm -r ~/.cache /var/cache/pacman/*
|
||||
|
||||
COPY --from=0 /target/ /
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash -e
|
||||
|
||||
target_url="${1:-srt://127.0.0.1:9000}"
|
||||
ffmpeg_pid=
|
||||
target_url="${1:-icecast://source:source@127.0.0.1:61120/main}"
|
||||
ffmpeg_pids=()
|
||||
|
||||
call_ffmpeg() {
|
||||
command ffmpeg -hide_banner "$@"
|
||||
|
@ -9,23 +9,19 @@ call_ffmpeg() {
|
|||
|
||||
daemon_ffmpeg() {
|
||||
call_ffmpeg "$@" &
|
||||
ffmpeg_pid=$!
|
||||
ffmpeg_pids+=($!)
|
||||
}
|
||||
|
||||
shutdown_ffmpeg() {
|
||||
if is_ffmpeg_running
|
||||
then
|
||||
if is_ffmpeg_running; then
|
||||
kill "$ffmpeg_pid" || true
|
||||
for t in $(seq 0 10)
|
||||
do
|
||||
if ! kill -0 "$ffmpeg_pid"
|
||||
then
|
||||
for t in $(seq 0 10); do
|
||||
if ! kill -0 "$ffmpeg_pid"; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if kill -0 "$ffmpeg_pid"
|
||||
then
|
||||
if kill -0 "$ffmpeg_pid"; then
|
||||
kill -9 "$ffmpeg_pid" || true
|
||||
fi
|
||||
fi
|
||||
|
@ -43,15 +39,13 @@ trap on_exit EXIT
|
|||
|
||||
offline=0
|
||||
|
||||
while true
|
||||
do
|
||||
while true; do
|
||||
found_audio_source=""
|
||||
|
||||
while read -r line
|
||||
do
|
||||
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[1]=$(sed -e 's/\\\([`~!@#$%^&*():;<>.,?/\|{}=+-]\)/\1/g' <<< "${found_source[1]}")
|
||||
while read -r line; do
|
||||
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[1]=$(sed -e 's/\\\([`~!@#$%^&*():;<>.,?/\|{}=+-]\)/\1/g' <<<"${found_source[1]}")
|
||||
case "${found_source[0]}" in
|
||||
*\(IDHPC\ Main\ Audio\))
|
||||
found_audio_source="${found_source[0]}"
|
||||
|
@ -59,20 +53,24 @@ do
|
|||
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)
|
||||
|
||||
if [ -z "$found_audio_source" ]
|
||||
then
|
||||
offline=$(( offline + 1 ))
|
||||
if [ -z "$found_audio_source" ]; then
|
||||
offline=$((offline + 1))
|
||||
else
|
||||
offline=0
|
||||
fi
|
||||
|
||||
if ! is_ffmpeg_running && [ -n "$found_audio_source" ]
|
||||
then
|
||||
if ! is_ffmpeg_running && [ -n "$found_audio_source" ]; then
|
||||
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
|
||||
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
|
||||
shutdown_ffmpeg # it won't shut down by itself unfortunately
|
||||
fi
|
||||
|
|
Loading…
Reference in New Issue