From 3545314913312da4a127461575f21912983d6c82 Mon Sep 17 00:00:00 2001 From: Tony Miller Date: Fri, 29 May 2026 10:31:43 -0700 Subject: [PATCH 1/5] rtmp relay --- liquidsoap/radio.liq | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/liquidsoap/radio.liq b/liquidsoap/radio.liq index 778034e..3f4c48a 100755 --- a/liquidsoap/radio.liq +++ b/liquidsoap/radio.liq @@ -12,6 +12,7 @@ set("request.deprecated_on_air_metadata", true) #set("server.telnet",true) set("server.socket",true) +# set("server.socket.path","/tmp/datafruits/liquidsoap.sock") set("server.socket.path","/home/liquidsoap/tracks/liquidsoap.sock") set("harbor.bind_addrs",["0.0.0.0"]) @@ -43,6 +44,7 @@ stop_dump_f = ref (fun () -> ()) # live_dj = ref empty() live_dj = ref(source.fail()) +rtmp_relay = ref(source.fail()) # fallback_ref = ref(fallback([source.fail()])) def icy_update(v) = @@ -85,7 +87,7 @@ def dj_dummy_stopped() = f() end -def get_user(user,password) = +def get_user(user, password) = if user == "source" then x = string.split(separator=';',password) list.nth(x,0,default="") @@ -94,7 +96,7 @@ def get_user(user,password) = end end -def get_password(user,password) = +def get_password(user, password) = if user == "source" then x = string.split(separator=';',password) list.nth(x,1,default="") @@ -104,10 +106,10 @@ def get_password(user,password) = end #auth function -def dj_auth(params) = +def dj_auth(req) = log("dj auth") - u = get_user(params.user,params.password) - p = get_password(params.user,params.password) + u = get_user(req.user, req.password) + p = get_password(req.user, req.password) ret = process.read.lines("./dj_auth.sh '#{u}' '#{p}' '#{radio_name}'") #ret has now the value of the live client (dj1,dj2, or djx), or "ERROR"/"unknown" ret = list.hd(default="",ret) @@ -226,6 +228,12 @@ end # buffer breaks fallback? live_dj := stereo(input.harbor("#{radio_name}",id="live_dj",port=9000,auth=dj_auth,on_connect=on_connect,on_disconnect=on_disconnect,logfile="/tmp/liquidsoap_harbor.log",buffer=15.0,max=40.0)) +# this source doesn't need on_connect/disconnect handlers, all recording/metadata is handled elsewhere +# TODO maybe we need disconnect handler to clear metadata?? +rtmp_relay := stereo(input.harbor("#{radio_name}_rtmp",id="rtmp_relay",port=9001,logfile="/tmp/liquidsoap_rtmp_relay_harbor.log",user="rtmp_source",password="pricemaster9876",buffer=15.0,max=40.0)) + +# TODO different harbor for rtmp relay??? + # live_dj := input.harbor( # "liq_live_dj", # port=9000, @@ -294,7 +302,7 @@ end output.file(%mp3, current_dump_filename_getter, live_dj(), fallible=true, reopen_when=reopen_when, reopen_delay=0.0, append=true) source = fallback(id="fallback",track_sensitive=false, - [live_dj(),scheduled_shows,mksafe(id="backup_playlist_mksafe", backup_playlist)]) + [rtmp_relay(),live_dj(),scheduled_shows,mksafe(id="backup_playlist_mksafe", backup_playlist)]) source.on_metadata(synchronous=false, pub_metadata) From 002b46c08d26c29cacac5c212f4960e7e7adb029 Mon Sep 17 00:00:00 2001 From: Tony Miller Date: Sun, 31 May 2026 16:06:31 -0700 Subject: [PATCH 2/5] add hls output --- liquidsoap/Dockerfile | 65 ++++++++++++++++++++++++++++--------------- liquidsoap/PACKAGES | 4 +-- liquidsoap/radio.liq | 50 +++++++++++++++++++++++++++++---- 3 files changed, 88 insertions(+), 31 deletions(-) diff --git a/liquidsoap/Dockerfile b/liquidsoap/Dockerfile index 4ae3be8..6efd302 100644 --- a/liquidsoap/Dockerfile +++ b/liquidsoap/Dockerfile @@ -1,21 +1,27 @@ -FROM archlinux +# FROM archlinux +# +# RUN pacman -Sy --noconfirm archlinux-keyring +# +# RUN pacman-key --init +# RUN pacman-key --populate archlinux +# RUN yes | pacman -Syu --noconfirm opam make m4 gcc patch diffutils gavl ffmpeg git automake autoconf pkg-config which taglib sudo glibc jq libffi pcre libmad +# -RUN pacman -Sy --noconfirm archlinux-keyring +FROM savonet/liquidsoap:v2.4.x-latest +#RUN useradd liquidsoap && mkdir /home/liquidsoap && chown -R liquidsoap /home/liquidsoap +#RUN usermod -aG wheel liquidsoap +# RUN echo "Defaults !requiretty" >> /etc/sudoers +# RUN echo "%wheel ALL=NOPASSWD: ALL" >> /etc/sudoers -RUN pacman-key --init -RUN pacman-key --populate archlinux -RUN yes | pacman -Syu --noconfirm opam make m4 gcc patch diffutils ffmpeg git automake autoconf pkg-config which taglib sudo glibc jq libffi pcre libmad +ENV HOME=/home/liquidsoap -RUN useradd liquidsoap && mkdir /home/liquidsoap && chown -R liquidsoap /home/liquidsoap -RUN usermod -aG wheel liquidsoap -RUN echo "Defaults !requiretty" >> /etc/sudoers -RUN echo "%wheel ALL=NOPASSWD: ALL" >> /etc/sudoers +USER root -ENV HOME=/home/liquidsoap +RUN apt-get update +RUN apt-get install -y --no-install-recommends curl jq -USER liquidsoap -RUN opam init --disable-sandboxing --yes +# RUN opam init --disable-sandboxing --yes WORKDIR /home/liquidsoap @@ -26,17 +32,17 @@ WORKDIR /home/liquidsoap # RUN echo "%sudo ALL=NOPASSWD: ALL" >> /etc/sudoers #RUN echo 'eval "$(opam config env)"' >> /home/liquidsoap/.bashrc -RUN sudo touch /var/log/liquidsoap.log -RUN sudo chown liquidsoap:users /var/log/liquidsoap.log - -RUN opam switch create 4.14.2 -RUN opam update +RUN touch /var/log/liquidsoap.log +RUN chown liquidsoap:users /var/log/liquidsoap.log +# RUN opam switch create 4.14.2 +# RUN opam update +# # depext no longer required to install, integrated with opam # RUN opam install depext -y # RUN opam depext taglib mad lame ogg vorbis cry samplerate liquidsoap -y # RUN opam install taglib mad lame ogg vorbis cry samplerate ocurl liquidsoap -y -RUN opam install ctypes-foreign metadata mad lame ogg vorbis cry samplerate ocurl liquidsoap -y +#RUN opam install ctypes-foreign metadata mad lame ogg vorbis cry samplerate ocurl gavl ffmpeg liquidsoap -y # RUN for i in ocaml-gavl ocaml-ffmpeg ocaml-dtools ocaml-duppy ocaml-mm ocaml-cry ocaml-taglib ocaml-lame ocaml-mad ocaml-ogg ocaml-vorbis ocaml-samplerate; do \ # cd /home/liquidsoap && git clone https://github.com/savonet/$i && cd $i && opam pin add --yes --no-action .; \ @@ -45,12 +51,12 @@ RUN opam install ctypes-foreign metadata mad lame ogg vorbis cry samplerate ocur #RUN cd /home/liquidsoap && git clone https://github.com/savonet/liquidsoap && cd liquidsoap && opam pin add --yes --no-action . # RUN opam install --yes liquidsoap ffmpeg cry taglib lame mad ogg vorbis samplerate # -RUN eval $(opam env) && liquidsoap --version - +# RUN eval $(opam env) && liquidsoap --version +# RUN mkdir /home/liquidsoap/radio ADD ./ /home/liquidsoap/radio/ -RUN sudo chown -R liquidsoap:liquidsoap /home/liquidsoap/radio/ +#RUN chown -R liquidsoap:liquidsoap /home/liquidsoap/radio/ RUN chmod +x /home/liquidsoap/radio/libStereoTool_64.so RUN mkdir /home/liquidsoap/tracks @@ -58,10 +64,23 @@ RUN chown liquidsoap:liquidsoap /home/liquidsoap/tracks RUN mkdir /home/liquidsoap/recordings RUN chown liquidsoap:liquidsoap /home/liquidsoap/recordings +RUN mkdir /home/liquidsoap/hls +RUN chown liquidsoap:liquidsoap /home/liquidsoap/hls + +RUN chown -R liquidsoap:liquidsoap /home/liquidsoap/ + WORKDIR /home/liquidsoap/radio -RUN eval $(opam env) && liquidsoap --check radio.liq +# RUN eval $(opam env) && liquidsoap --check radio.liq +RUN liquidsoap --check radio.liq # RUN chown 1000:1000 /home/liquidsoap/tracks # RUN chown 1000:1000 /home/liquidsoap/recordings +#RUN chown liquidsoap:liquidsoap /home/liquidsoap/tracks +#RUN chown liquidsoap:liquidsoap /home/liquidsoap/recordings + +USER liquidsoap EXPOSE 9000 -CMD ["/bin/bash", "-c", "eval `opam config env`; liquidsoap radio.liq"] +EXPOSE 9001 +EXPOSE 1936 +# CMD ["/bin/bash", "-c", "eval `opam config env`; liquidsoap radio.liq"] +CMD ["liquidsoap", "radio.liq"] diff --git a/liquidsoap/PACKAGES b/liquidsoap/PACKAGES index be4b45c..af1d642 100644 --- a/liquidsoap/PACKAGES +++ b/liquidsoap/PACKAGES @@ -117,11 +117,11 @@ ocaml-flac # is very efficient and has a quality setting for # tweaking load vs. quality of the conversion. # It is the recommended module to use with video -# ocaml-gavl +ocaml-gavl # FFMPEG is currently only used to convert from # and to many formats. -# ocaml-ffmpeg +ocaml-ffmpeg # Frei0r is a minimalistic plugin API for video sources and filters. # ocaml-frei0r diff --git a/liquidsoap/radio.liq b/liquidsoap/radio.liq index 3f4c48a..e70e7d0 100755 --- a/liquidsoap/radio.liq +++ b/liquidsoap/radio.liq @@ -44,7 +44,7 @@ stop_dump_f = ref (fun () -> ()) # live_dj = ref empty() live_dj = ref(source.fail()) -rtmp_relay = ref(source.fail()) +# rtmp_relay = ref(source.fail()) # fallback_ref = ref(fallback([source.fail()])) def icy_update(v) = @@ -228,11 +228,17 @@ end # buffer breaks fallback? live_dj := stereo(input.harbor("#{radio_name}",id="live_dj",port=9000,auth=dj_auth,on_connect=on_connect,on_disconnect=on_disconnect,logfile="/tmp/liquidsoap_harbor.log",buffer=15.0,max=40.0)) -# this source doesn't need on_connect/disconnect handlers, all recording/metadata is handled elsewhere -# TODO maybe we need disconnect handler to clear metadata?? -rtmp_relay := stereo(input.harbor("#{radio_name}_rtmp",id="rtmp_relay",port=9001,logfile="/tmp/liquidsoap_rtmp_relay_harbor.log",user="rtmp_source",password="pricemaster9876",buffer=15.0,max=40.0)) +# this source doesn't need on_connect handler, all recording/metadata is handled elsewhere +# disconnect handler to clear metadata is necessary though +# rtmp_relay := stereo(input.harbor("#{radio_name}_rtmp",id="rtmp_relay",port=9001,logfile="/tmp/liquidsoap_rtmp_relay_harbor.log",user="rtmp_source",password="pricemaster9876",on_disconnect=on_disconnect,buffer=15.0,max=40.0)) -# TODO different harbor for rtmp relay??? +# ingest rtmp from viz server + +# how do we get the key?! +# live = input.rtmp(listen=false, "rtmp://localhost:1935/live/key") +# live = input.rtmp(listen=false, "rtmp://vjstream.streampusher.com:1935/live/key") + +# rtmp = input.rtmp("rtmp://0.0.0.0:1935/live", listen=true) # live_dj := input.harbor( # "liq_live_dj", @@ -301,8 +307,14 @@ end output.file(%mp3, current_dump_filename_getter, live_dj(), fallible=true, reopen_when=reopen_when, reopen_delay=0.0, append=true) +# nginx-rtmp pushes the live RTMP stream here when a DJ is streaming with obs +rtmp_relay = input.rtmp(listen=true, "rtmp://datafruits_viz:1936") +# TODO uncommenting this causes type error +# rtmp_audio = mksafe(stereo(source.tracks(rtmp_relay).audio)) + source = fallback(id="fallback",track_sensitive=false, - [rtmp_relay(),live_dj(),scheduled_shows,mksafe(id="backup_playlist_mksafe", backup_playlist)]) + [live_dj(),scheduled_shows,mksafe(id="backup_playlist_mksafe", backup_playlist)]) +# [rtmp_audio, live_dj(),scheduled_shows,mksafe(id="backup_playlist_mksafe", backup_playlist)]) source.on_metadata(synchronous=false, pub_metadata) @@ -344,3 +356,29 @@ output.icecast(%mp3,id="icecast", send_icy_metadata=true,description="", url="", encoding="UTF-8", mksafe(buffer(source))) + +# HLS video output + +single_video = noise() + +logo_fallback = mux_audio(single_video, audio=source) + +# RTMP video takes priority; fall back to static logo + audio +hls_source = fallback(id="hls_fallback", track_sensitive=false, [rtmp_relay, logo_fallback]) + +hls_enc = %ffmpeg( + format="mpegts", + %video(codec="libx264", x264opts="keyint=100:min-keyint=100"), + %audio(codec="aac", channels=2, ar=44100) +) + +output.file.hls( + id="hls_output", + playlist="live.m3u8", + segment_duration=4.0, + segments=5, + segments_overhead=5, + "/home/liquidsoap/hls/", + [("live", hls_enc)], + hls_source +) From a23f91823fbfc19327565263a18c62b3aee4a162 Mon Sep 17 00:00:00 2001 From: Tony Miller Date: Sun, 31 May 2026 20:40:39 -0700 Subject: [PATCH 3/5] separate hls audio fallback --- liquidsoap/radio.liq | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/liquidsoap/radio.liq b/liquidsoap/radio.liq index e70e7d0..475f4c8 100755 --- a/liquidsoap/radio.liq +++ b/liquidsoap/radio.liq @@ -310,11 +310,10 @@ output.file(%mp3, current_dump_filename_getter, live_dj(), fallible=true, reopen # nginx-rtmp pushes the live RTMP stream here when a DJ is streaming with obs rtmp_relay = input.rtmp(listen=true, "rtmp://datafruits_viz:1936") # TODO uncommenting this causes type error -# rtmp_audio = mksafe(stereo(source.tracks(rtmp_relay).audio)) +rtmp_audio = stereo(source.tracks(rtmp_relay).audio) source = fallback(id="fallback",track_sensitive=false, - [live_dj(),scheduled_shows,mksafe(id="backup_playlist_mksafe", backup_playlist)]) -# [rtmp_audio, live_dj(),scheduled_shows,mksafe(id="backup_playlist_mksafe", backup_playlist)]) + [rtmp_audio, live_dj(),scheduled_shows,mksafe(id="backup_playlist_mksafe", backup_playlist)]) source.on_metadata(synchronous=false, pub_metadata) @@ -359,9 +358,12 @@ output.icecast(%mp3,id="icecast", # HLS video output +hls_audio_fallback = fallback(id="fallback",track_sensitive=false, + [live_dj(),scheduled_shows,mksafe(id="backup_playlist_mksafe", backup_playlist)]) + single_video = noise() -logo_fallback = mux_audio(single_video, audio=source) +logo_fallback = mux_audio(single_video, audio=hls_audio_fallback) # RTMP video takes priority; fall back to static logo + audio hls_source = fallback(id="hls_fallback", track_sensitive=false, [rtmp_relay, logo_fallback]) From b93f22dc12bfba2b879ff37b27005fc477351bb5 Mon Sep 17 00:00:00 2001 From: Tony Miller Date: Mon, 1 Jun 2026 12:31:35 -0700 Subject: [PATCH 4/5] dont use source.tracks just call stereo() --- liquidsoap/radio.liq | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/liquidsoap/radio.liq b/liquidsoap/radio.liq index 475f4c8..b9d6aa4 100755 --- a/liquidsoap/radio.liq +++ b/liquidsoap/radio.liq @@ -6,7 +6,7 @@ set("sandbox.tool","disabled") set("log.file",true) set("log.file.path","/var/log/liquidsoap.log") set("log.stdout",true) -set("log.level", 5) +set("log.level", 3) set("request.deprecated_on_air_metadata", true) @@ -308,17 +308,16 @@ end output.file(%mp3, current_dump_filename_getter, live_dj(), fallible=true, reopen_when=reopen_when, reopen_delay=0.0, append=true) # nginx-rtmp pushes the live RTMP stream here when a DJ is streaming with obs -rtmp_relay = input.rtmp(listen=true, "rtmp://datafruits_viz:1936") -# TODO uncommenting this causes type error -rtmp_audio = stereo(source.tracks(rtmp_relay).audio) +rtmp_relay = input.rtmp(listen=true, "rtmp://0.0.0.0:1936/live") +rtmp_audio = stereo(rtmp_relay) -source = fallback(id="fallback",track_sensitive=false, +radio_fallback = fallback(id="fallback",track_sensitive=false, [rtmp_audio, live_dj(),scheduled_shows,mksafe(id="backup_playlist_mksafe", backup_playlist)]) -source.on_metadata(synchronous=false, pub_metadata) +radio_fallback.on_metadata(synchronous=false, pub_metadata) def current_fallback_source(v) = - selected_source = "#{source.selected()}" + selected_source = "#{radio_fallback.selected()}" selected_source end @@ -328,9 +327,9 @@ server.register("current_source", namespace="fallback", current_fallback_source) def current_fallback_duration(v) = - duration = "#{source.duration()}" - elapsed = "#{source.elapsed()}" - remaining = "#{source.remaining()}" + duration = "#{radio_fallback.duration()}" + elapsed = "#{radio_fallback.elapsed()}" + remaining = "#{radio_fallback.remaining()}" "elapsed: #{elapsed} / duration: #{duration} / remaining: #{remaining}" end @@ -339,34 +338,30 @@ server.register("duration", namespace="fallback", usage="duration", current_fallback_duration) -# TODO unneeded ??? -# source.on_track(pub_metadata) -# source = server.insert_metadata(id="fallback", source) - output.icecast(%vorbis,id="icecast", mount="#{radio_name}.ogg", host=icecast_host, port=int_of_string(icecast_port), password="hackme", send_icy_metadata=true,description="", url="", encoding="UTF-8", - mksafe(buffer(source))) + mksafe(buffer(radio_fallback))) output.icecast(%mp3,id="icecast", mount="#{radio_name}.mp3", host=icecast_host, port=int_of_string(icecast_port), password="hackme", send_icy_metadata=true,description="", url="", encoding="UTF-8", - mksafe(buffer(source))) + mksafe(buffer(radio_fallback))) # HLS video output hls_audio_fallback = fallback(id="fallback",track_sensitive=false, [live_dj(),scheduled_shows,mksafe(id="backup_playlist_mksafe", backup_playlist)]) -single_video = noise() +single_video = video.testsrc.ffmpeg() -logo_fallback = mux_audio(single_video, audio=hls_audio_fallback) +logo_fallback = source.mux.audio(single_video, audio=hls_audio_fallback) # RTMP video takes priority; fall back to static logo + audio -hls_source = fallback(id="hls_fallback", track_sensitive=false, [rtmp_relay, logo_fallback]) +hls_source = fallback(id="hls_fallback", track_sensitive=false, [rtmp_relay, mksafe(logo_fallback)]) hls_enc = %ffmpeg( format="mpegts", From 725a138e07030b7713d490ba9fa5d3708cd9e939 Mon Sep 17 00:00:00 2001 From: Tony Miller Date: Mon, 1 Jun 2026 15:38:49 -0700 Subject: [PATCH 5/5] add separate harbor for rtmp again --- liquidsoap/Dockerfile | 2 +- liquidsoap/radio.liq | 26 +++++++++----------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/liquidsoap/Dockerfile b/liquidsoap/Dockerfile index 6efd302..3690d47 100644 --- a/liquidsoap/Dockerfile +++ b/liquidsoap/Dockerfile @@ -70,7 +70,7 @@ RUN chown liquidsoap:liquidsoap /home/liquidsoap/hls RUN chown -R liquidsoap:liquidsoap /home/liquidsoap/ WORKDIR /home/liquidsoap/radio -# RUN eval $(opam env) && liquidsoap --check radio.liq +RUN liquidsoap --version RUN liquidsoap --check radio.liq # RUN chown 1000:1000 /home/liquidsoap/tracks # RUN chown 1000:1000 /home/liquidsoap/recordings diff --git a/liquidsoap/radio.liq b/liquidsoap/radio.liq index b9d6aa4..38585da 100755 --- a/liquidsoap/radio.liq +++ b/liquidsoap/radio.liq @@ -225,27 +225,12 @@ def on_disconnect() = log(ret) end -# buffer breaks fallback? live_dj := stereo(input.harbor("#{radio_name}",id="live_dj",port=9000,auth=dj_auth,on_connect=on_connect,on_disconnect=on_disconnect,logfile="/tmp/liquidsoap_harbor.log",buffer=15.0,max=40.0)) # this source doesn't need on_connect handler, all recording/metadata is handled elsewhere # disconnect handler to clear metadata is necessary though -# rtmp_relay := stereo(input.harbor("#{radio_name}_rtmp",id="rtmp_relay",port=9001,logfile="/tmp/liquidsoap_rtmp_relay_harbor.log",user="rtmp_source",password="pricemaster9876",on_disconnect=on_disconnect,buffer=15.0,max=40.0)) +rtmp_audio = stereo(input.harbor("#{radio_name}_rtmp",id="rtmp_relay",port=9001,logfile="/tmp/liquidsoap_rtmp_relay_harbor.log",user="rtmp_source",password="pricemaster9876",on_disconnect=on_disconnect,buffer=15.0,max=40.0)) -# ingest rtmp from viz server - -# how do we get the key?! -# live = input.rtmp(listen=false, "rtmp://localhost:1935/live/key") -# live = input.rtmp(listen=false, "rtmp://vjstream.streampusher.com:1935/live/key") - -# rtmp = input.rtmp("rtmp://0.0.0.0:1935/live", listen=true) - -# live_dj := input.harbor( -# "liq_live_dj", -# port=9000, -# password="abc123" -# ) -# def new_meta(m) = log("in new meta") log(string(m)) @@ -309,7 +294,6 @@ output.file(%mp3, current_dump_filename_getter, live_dj(), fallible=true, reopen # nginx-rtmp pushes the live RTMP stream here when a DJ is streaming with obs rtmp_relay = input.rtmp(listen=true, "rtmp://0.0.0.0:1936/live") -rtmp_audio = stereo(rtmp_relay) radio_fallback = fallback(id="fallback",track_sensitive=false, [rtmp_audio, live_dj(),scheduled_shows,mksafe(id="backup_playlist_mksafe", backup_playlist)]) @@ -358,6 +342,14 @@ hls_audio_fallback = fallback(id="fallback",track_sensitive=false, single_video = video.testsrc.ffmpeg() +single_video = video.add_image(width=200, height=200, x=10, y=10, file="df_logo_2019.png", single_video) + +single_video = video.add_text.sdl( + font="./hinted-Debussy.woff", + "DATAFRUITS.FM", + single_video + ) + logo_fallback = source.mux.audio(single_video, audio=hls_audio_fallback) # RTMP video takes priority; fall back to static logo + audio