diff --git a/icedreammusic/docker-compose.yml b/icedreammusic/docker-compose.yml index 1b73d9a..94fce31 100644 --- a/icedreammusic/docker-compose.yml +++ b/icedreammusic/docker-compose.yml @@ -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 diff --git a/icedreammusic/images/icecast-2.4.4/.dockerignore b/icedreammusic/images/icecast-2.4.4/.dockerignore new file mode 100644 index 0000000..1ae8621 --- /dev/null +++ b/icedreammusic/images/icecast-2.4.4/.dockerignore @@ -0,0 +1,4 @@ +Dockerfile +.docker* +.git + diff --git a/icedreammusic/images/icecast-2.4.4/Dockerfile b/icedreammusic/images/icecast-2.4.4/Dockerfile new file mode 100644 index 0000000..bce5c89 --- /dev/null +++ b/icedreammusic/images/icecast-2.4.4/Dockerfile @@ -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 diff --git a/icedreammusic/liquidsoap/Dockerfile b/icedreammusic/liquidsoap/Dockerfile index 7b323f3..701fba9 100644 --- a/icedreammusic/liquidsoap/Dockerfile +++ b/icedreammusic/liquidsoap/Dockerfile @@ -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 diff --git a/icedreammusic/liquidsoap/metadata_api.liq b/icedreammusic/liquidsoap/metadata_api.liq index 231b129..e092cf0 100644 --- a/icedreammusic/liquidsoap/metadata_api.liq +++ b/icedreammusic/liquidsoap/metadata_api.liq @@ -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","*"), @@ -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) s -end \ No newline at end of file +end diff --git a/icedreammusic/liquidsoap/silent_fallback.liq b/icedreammusic/liquidsoap/silent_fallback.liq index 9ca2465..cce675d 100644 --- a/icedreammusic/liquidsoap/silent_fallback.liq +++ b/icedreammusic/liquidsoap/silent_fallback.liq @@ -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 diff --git a/icedreammusic/liquidsoap/stream.liq b/icedreammusic/liquidsoap/stream.liq index 82ca2e6..0816a31 100644 --- a/icedreammusic/liquidsoap/stream.liq +++ b/icedreammusic/liquidsoap/stream.liq @@ -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)) diff --git a/icedreammusic/liquidsoap/stream_api.liq b/icedreammusic/liquidsoap/stream_api.liq new file mode 100644 index 0000000..9a25761 --- /dev/null +++ b/icedreammusic/liquidsoap/stream_api.liq @@ -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 diff --git a/icedreammusic/metacollector/Dockerfile b/icedreammusic/metacollector/Dockerfile index 488e72f..751c6fd 100644 --- a/icedreammusic/metacollector/Dockerfile +++ b/icedreammusic/metacollector/Dockerfile @@ -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"] diff --git a/icedreammusic/ndi-to-srt/Dockerfile b/icedreammusic/ndi-to-srt/Dockerfile index 39d48c4..8bf0800 100644 --- a/icedreammusic/ndi-to-srt/Dockerfile +++ b/icedreammusic/ndi-to-srt/Dockerfile @@ -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/ / diff --git a/icedreammusic/ndi-to-srt/ndi-to-srt.sh b/icedreammusic/ndi-to-srt/ndi-to-srt.sh index d38bc57..6d94f57 100644 --- a/icedreammusic/ndi-to-srt/ndi-to-srt.sh +++ b/icedreammusic/ndi-to-srt/ndi-to-srt.sh @@ -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,23 +53,27 @@ 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 - + sleep 1 done