feat(playout): add Liquidsoap 2.0 support (#2786)
### Description add Liquidsoap2.0 files (port syntax 1.4 to 2.0) ### Testing Notes I ran libretime on ubuntu 22.04 and liquidsoap2.0 this pr is just the beginning, just 2 files added it's a clean one... in order to work under 22.04,it requires changes in 1. this pr 1. (https://github.com/libretime/libretime/pull/2789) 1. libretime/propel (https://github.com/libretime/propel1/pull/1) or change legacy /composer.json ``` "type": "vcs", - "url": "https://github.com/libretime/propel1" + "url": "https://github.com/mp3butcher/propel1" }, { "type": "vcs", @@ -30,7 +30,7 @@ "james-heinrich/getid3": "^1.9", "league/uri": "^6.7", "libretime/celery-php": "dev-main", - "libretime/propel1": "dev-main", + "mp3butcher/propel1": "main", "php-amqplib/php-amqplib": "^3.0", ``` 4. and few mods in install ``` case "$ID-$VERSION_ID" in ubuntu-20.04) is_ubuntu=true && distro="focal" ;; + ubuntu-22.04) is_ubuntu=true && distro="jammy" ;; debian-11) is_debian=true && distro="bullseye" ;; *) error "could not determine supported distribution '$ID-$VERSION_ID' @@ -375,8 +376,12 @@ prepare_packages_install() { if $is_ubuntu; then install_packages software-properties-common - add-apt-repository -y ppa:libretime/libretime + +if echo $distro | grep -q 'focal'; then + add-apt-repository -y ppa:libretime/libretime + fi + if echo $distro | grep -q 'jammy'; then + apt-get install php-cli php-dev php php-fpm php-pear php-yaml php-gd php-bcmath php-curl + fi DEBIAN_FRONTEND=noninteractive apt-get -q update fi } ``` It will require testing changes against ubuntu 20.4 and debian,that's why i think a testing branch can be wise --------- Co-authored-by: mp3butcher <mp3butcher@gmail.com>
This commit is contained in:
parent
eb3f160eae
commit
f9c0bd5a05
264
playout/libretime_playout/liquidsoap/2.0/ls_lib.liq
Normal file
264
playout/libretime_playout/liquidsoap/2.0/ls_lib.liq
Normal file
@ -0,0 +1,264 @@
|
||||
def gateway(args)
|
||||
command = "timeout --signal=KILL 45 libretime-playout-notify #{args} &"
|
||||
log(command)
|
||||
process.run(command)
|
||||
end
|
||||
|
||||
def notify(m)
|
||||
gateway("media '#{m['schedule_table_id']}'")
|
||||
end
|
||||
|
||||
def notify_queue(m)
|
||||
f = !dynamic_metadata_callback
|
||||
ignore(f(m))
|
||||
notify(m)
|
||||
m
|
||||
end
|
||||
|
||||
def notify_stream(m)
|
||||
if !web_stream_id != "-1" then
|
||||
json_str = string.replace(pattern="\n",(fun (s) -> ""), json_of(m))
|
||||
#if a string has a single apostrophe in it, let's comment it out by ending the string before right before it
|
||||
#escaping the apostrophe, and then starting a new string right after it. This is why we use 3 apostrophes.
|
||||
json_str = string.replace(pattern="'",(fun (s) -> "'\''"), json_str)
|
||||
|
||||
gateway("webstream '#{!web_stream_id}' '#{json_str}'")
|
||||
end
|
||||
end
|
||||
|
||||
# A function applied to each metadata chunk
|
||||
def append_title(m) =
|
||||
log("Using message format #{message_format()}")
|
||||
|
||||
if list.mem_assoc("mapped", m) then
|
||||
# protection against applying this function twice. It shouldn't be happening and bug
|
||||
# file with Liquidsoap.
|
||||
m
|
||||
else
|
||||
if message_format() == "1" then
|
||||
[("title", "#{show_name()} - #{m['artist']} - #{m['title']}"), ("mapped", "true")]
|
||||
elsif message_format() == "2" then
|
||||
[("title", "#{station_name()} - #{show_name()}"), ("mapped", "true")]
|
||||
else
|
||||
if "#{m['artist']}" == "" then
|
||||
[("title", "#{m['title']}"), ("mapped", "true")]
|
||||
else
|
||||
[("title", "#{m['artist']} - #{m['title']}"), ("mapped", "true")]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def transition(a,b) =
|
||||
log("transition called...")
|
||||
add(
|
||||
normalize=false,
|
||||
[
|
||||
sequence([
|
||||
blank(duration=0.01),
|
||||
fade.in(duration=input_fade_transition(), b)
|
||||
]),
|
||||
fade.out(duration=input_fade_transition(), a)
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
# we need this function for special transition case(from default to queue) we don't want
|
||||
# the transition fade to have effect on the first song that would be played switching out
|
||||
# of the default(silent) source
|
||||
def transition_default(a,b) =
|
||||
log("transition called...")
|
||||
if !just_switched then
|
||||
just_switched := false
|
||||
add(
|
||||
normalize=false,
|
||||
[
|
||||
sequence([
|
||||
blank(duration=0.01),
|
||||
fade.in(duration=input_fade_transition(), b)
|
||||
]),
|
||||
fade.out(duration=input_fade_transition(), a)
|
||||
]
|
||||
)
|
||||
else
|
||||
just_switched := false
|
||||
b
|
||||
end
|
||||
end
|
||||
|
||||
# Define a transition that fades out the old source, adds a single, and then plays the
|
||||
# new source
|
||||
def to_live(old,new) =
|
||||
# Fade out old source
|
||||
old = fade.out(old)
|
||||
# Compose this in sequence with the new source
|
||||
sequence([old,new])
|
||||
end
|
||||
|
||||
|
||||
def make_ouput_on_connect_handler(stream)
|
||||
def on_connect()
|
||||
gateway("stream '#{stream}' '#{boot_timestamp}'")
|
||||
end
|
||||
on_connect
|
||||
end
|
||||
|
||||
def make_ouput_on_error_handler(stream)
|
||||
def on_error(msg)
|
||||
gateway("stream '#{stream}' '#{boot_timestamp}' --error='#{msg}'")
|
||||
5.
|
||||
end
|
||||
on_error
|
||||
end
|
||||
|
||||
def clear_queue(s)
|
||||
source.skip(s)
|
||||
end
|
||||
|
||||
# NOTE
|
||||
# A few values are hardcoded and may be dependent:
|
||||
# - the delay in gracetime is linked with the buffer duration of input.http
|
||||
# (delay should be a bit less than buffer)
|
||||
# - crossing duration should be less than buffer length
|
||||
# (at best, a higher duration will be ineffective)
|
||||
|
||||
# HTTP input with "restart" command that waits for "stop" to be effected
|
||||
# before "start" command is issued. Optionally it takes a new URL to play,
|
||||
# which makes it a convenient replacement for "url".
|
||||
# In the future, this may become a core feature of the HTTP input.
|
||||
# TODO If we stop and restart quickly several times in a row,
|
||||
# the data bursts accumulate and create buffer overflow.
|
||||
# Flushing the buffer on restart could be a good idea, but
|
||||
# it would also create an interruptions while the buffer is
|
||||
# refilling... on the other hand, this would avoid having to
|
||||
# fade using both cross() and switch().buffer=5.,max=15.,,autostart=false
|
||||
def input.http_restart(~id,~initial_url="http://dummy/url")
|
||||
|
||||
source = audio_to_stereo( mksafe(input.http(id=id,initial_url)))
|
||||
|
||||
def stopped()
|
||||
"stopped" == list.hd(server.execute("#{id}.status"), default="")
|
||||
end
|
||||
|
||||
server.register(namespace=id,
|
||||
"restart",
|
||||
usage="restart [url]",
|
||||
fun (url) -> begin
|
||||
if url != "" then
|
||||
log(string_of(server.execute("#{id}.url #{url}")))
|
||||
end
|
||||
log(string_of(server.execute("#{id}.stop")))
|
||||
add_timeout(0.5,
|
||||
{ if stopped() then
|
||||
log(string_of(server.execute("#{id}.start"))) ;
|
||||
(-1.)
|
||||
else 0.5 end})
|
||||
"OK"
|
||||
end)
|
||||
|
||||
source
|
||||
|
||||
end
|
||||
|
||||
# Transitions between URL changes in HTTP streams.
|
||||
def cross_http(~debug=true,~http_input_id,source)
|
||||
id = http_input_id
|
||||
last_url = ref ("")
|
||||
change = ref (false)
|
||||
|
||||
def on_m(m) =
|
||||
notify_stream(m)
|
||||
changed = m["source_url"] != !last_url
|
||||
log("URL now #{m['source_url']} (change: #{changed})")
|
||||
if changed then
|
||||
if !last_url != "" then change := true end
|
||||
last_url := m["source_url"]
|
||||
end
|
||||
m
|
||||
end
|
||||
# We use both metadata and status to know about the current URL.
|
||||
# Using only metadata may be more precise is crazy corner cases,
|
||||
# but it's also asking too much: the metadata may not pass through
|
||||
# before the crosser is instantiated.
|
||||
# Using only status in crosser misses some info, eg. on first URL.
|
||||
source = map_metadata(on_m,source)
|
||||
|
||||
cross_d = 3.
|
||||
|
||||
def crosser(ending, starting)
|
||||
url = list.hd(server.execute("#{id}.url"), default="")
|
||||
status = list.hd(server.execute("#{id}.status"))
|
||||
on_m([("source_url",url)])
|
||||
if debug then
|
||||
log("New track inside HTTP stream")
|
||||
log(" status: #{status}")
|
||||
log(" need to cross: #{!change}")
|
||||
#log(" remaining #{source.remaining(ending.source)} sec before, \
|
||||
# #{source.remaining(starting.source)} sec after")
|
||||
end
|
||||
if !change then
|
||||
change := false
|
||||
# In principle one should avoid crossing on a live stream
|
||||
# it'd be okay to do it here (eg. use add instead of sequence)
|
||||
# because it's only once per URL, but be cautious.
|
||||
sequence([fade.out(duration=cross_d,ending.source),fade.in(starting.source)])
|
||||
else
|
||||
# This is done on tracks inside a single stream.
|
||||
# Do NOT cross here or you'll gradually empty the buffer!
|
||||
sequence([ending.source,starting.source])
|
||||
end
|
||||
end
|
||||
|
||||
# Setting conservative=true would mess with the delayed switch below
|
||||
cross(duration=cross_d,conservative=false,crosser,source)
|
||||
|
||||
end
|
||||
|
||||
# Custom fallback between http and default source with fading of
|
||||
# beginning and end of HTTP stream.
|
||||
# It does not take potential URL changes into account, as long as
|
||||
# they do not interrupt streaming (thanks to the HTTP buffer).
|
||||
def http_fallback(~http_input_id,~http,~default)
|
||||
|
||||
id = http_input_id
|
||||
|
||||
# We use a custom switching predicate to trigger switching (and thus,
|
||||
# transitions) before the end of a track (rather, end of HTTP stream).
|
||||
# It is complexified because we don't want to trigger switching when
|
||||
# HTTP disconnects for just an instant, when changing URL: for that
|
||||
# we use gracetime below.
|
||||
|
||||
def gracetime(~delay=3.,f)
|
||||
last_true = ref(0.)
|
||||
{ if f() then
|
||||
last_true := time()
|
||||
true
|
||||
else
|
||||
time() < !last_true+delay
|
||||
end }
|
||||
end
|
||||
|
||||
def connected()
|
||||
status = list.hd(server.execute("#{id}.status"), default="")
|
||||
not(list.mem(status,["polling","stopped"]))
|
||||
end
|
||||
connected = gracetime(connected)
|
||||
|
||||
def to_live(a,b) =
|
||||
log("TRANSITION to live")
|
||||
add(normalize=false,
|
||||
[fade.in(b),fade.out(a)])
|
||||
end
|
||||
def to_static(a,b) =
|
||||
log("TRANSITION to static")
|
||||
sequence([fade.out(a),fade.in(b)])
|
||||
end
|
||||
|
||||
switch(
|
||||
track_sensitive=false,
|
||||
transitions=[to_live,to_static],
|
||||
[(# make sure it is connected, and not buffering
|
||||
{connected() and source.is_ready(http) and !web_stream_enabled}, http),
|
||||
({true},default)])
|
||||
|
||||
end
|
||||
218
playout/libretime_playout/liquidsoap/2.0/ls_script.liq
Normal file
218
playout/libretime_playout/liquidsoap/2.0/ls_script.liq
Normal file
@ -0,0 +1,218 @@
|
||||
boot_timestamp = string_of(time())
|
||||
|
||||
web_stream_enabled = ref(false)
|
||||
web_stream_id = ref( '-1')
|
||||
|
||||
show_name = interactive.string("show_name", "")
|
||||
|
||||
dynamic_metadata_callback = ref (fun (~new_track=false, s) -> begin () end)
|
||||
|
||||
just_switched = ref (false)
|
||||
|
||||
%include "ls_lib.liq"
|
||||
|
||||
sources = ref([])
|
||||
source_id = ref (0)
|
||||
|
||||
def create_source()
|
||||
this_source_id = !source_id
|
||||
l = request.queue(id="s#{this_source_id}")#, length=0.5
|
||||
|
||||
l = audio_to_stereo(id="queue_src", l)
|
||||
l = cue_cut(l)
|
||||
l = amplify(1., override="replay_gain", l)
|
||||
|
||||
# Fade the tracks to avoid hard cuts in between two tracks and at the end of the shows.
|
||||
# Liquidsoap reads the fade in/out durations from the annotation "liq_fade_in/out" which
|
||||
# value can be set via Libretime settings.
|
||||
l = fade.in(l)
|
||||
l = fade.out(l)
|
||||
|
||||
l = map_metadata(notify_queue,l)
|
||||
l = cross_http(http_input_id="http",l)
|
||||
l = http_fallback(http_input_id="http", http=l, default=l)
|
||||
l = map_metadata(id="map_metadata:schedule", update=false, append_title, l)
|
||||
sources := list.append([l], !sources)
|
||||
server.register(namespace="queues",
|
||||
"s#{this_source_id}_skip",
|
||||
fun (s) -> begin log("queues.s#{this_source_id}_skip")
|
||||
clear_queue(l)
|
||||
"Done"
|
||||
end)
|
||||
source_id := !source_id + 1
|
||||
end
|
||||
|
||||
enable_replaygain_metadata()
|
||||
|
||||
create_source()
|
||||
create_source()
|
||||
create_source()
|
||||
create_source()
|
||||
|
||||
queue = add(!sources)#, normalize=false)
|
||||
queue = insert_metadata(queue)
|
||||
dynamic_metadata_callback := queue.insert_metadata
|
||||
|
||||
output.dummy(fallible=true, queue)
|
||||
|
||||
#http = input.http_restart(id="http")
|
||||
#output.dummy(fallible=true, http)
|
||||
|
||||
ignore(output.dummy(queue, fallible=true))
|
||||
|
||||
def web_stream_set_id(value)
|
||||
web_stream_id := value
|
||||
string_of(!web_stream_id)
|
||||
end
|
||||
|
||||
def web_stream_get_id()
|
||||
string_of(!web_stream_id)
|
||||
end
|
||||
|
||||
server.register(namespace="sources",
|
||||
description="Start webstream source",
|
||||
"start_web_stream",
|
||||
fun (s) -> begin log("sources.start_web_stream")
|
||||
notify([("schedule_table_id", !web_stream_id)])
|
||||
web_stream_enabled := true "enabled" end)
|
||||
server.register(namespace="sources",
|
||||
description="Stop webstream source",
|
||||
"stop_web_stream",
|
||||
fun (s) -> begin log("sources.stop_web_stream") web_stream_enabled := false "disabled" end)
|
||||
|
||||
server.register(namespace="web_stream",
|
||||
description="Set the web stream id",
|
||||
"set_id",
|
||||
fun (s) -> begin log("web_stream.set_id") web_stream_set_id(s) end)
|
||||
|
||||
server.register(namespace="web_stream",
|
||||
description="Get the web stream id",
|
||||
"get_id",
|
||||
fun (s) -> begin log("web_stream.get_id") web_stream_get_id() end)
|
||||
|
||||
default = amplify(id="silence_src", 0.00001, noise())
|
||||
|
||||
def map_message_offline(m) =
|
||||
[("title", message_offline())]
|
||||
end
|
||||
|
||||
default = map_metadata(id="map_metadata:offline", map_message_offline, default)
|
||||
ignore(output.dummy(default, fallible=true))
|
||||
|
||||
input_main_streaming = ref (false)
|
||||
input_show_streaming = ref (false)
|
||||
schedule_streaming = ref (false)
|
||||
|
||||
def start_input_main() input_main_streaming := true end
|
||||
def stop_input_main() input_main_streaming := false end
|
||||
def start_input_show() input_show_streaming := true end
|
||||
def stop_input_show() input_show_streaming := false end
|
||||
def start_schedule() schedule_streaming := true; just_switched := true end
|
||||
def stop_schedule() schedule_streaming := false end
|
||||
|
||||
def update_source_status(sourcename, status) =
|
||||
gateway("live '#{sourcename}' '#{status}'")
|
||||
end
|
||||
|
||||
def input_main_on_connect(header) update_source_status("master_dj", true) end
|
||||
def input_main_on_disconnect() update_source_status("master_dj", false) end
|
||||
def input_show_on_connect(header) update_source_status("live_dj", true) end
|
||||
def input_show_on_disconnect() update_source_status("live_dj", false) end
|
||||
|
||||
def make_input_auth_handler(input_name)
|
||||
def auth_handler(args)
|
||||
log("user '#{args.user}' connected", label="#{input_name}_input")
|
||||
|
||||
# Check auth based on return value from auth script
|
||||
ret = test_process("libretime-playout-notify live-auth '#{input_name}' '#{args.user}' '#{args.password}'")
|
||||
if ret then
|
||||
log("user '#{args.user}' authenticated", label="#{input_name}_input")
|
||||
else
|
||||
log("user '#{args.user}' auth failed", label="#{input_name}_input",level=2)
|
||||
end
|
||||
|
||||
ret
|
||||
end
|
||||
auth_handler
|
||||
end
|
||||
|
||||
s = switch(id="switch:blank+schedule",
|
||||
track_sensitive=false,
|
||||
transitions=[transition_default, transition],
|
||||
[({!schedule_streaming}, queue), ({true}, default)]
|
||||
)
|
||||
|
||||
s = if input_show_port != 0 and input_show_mount != "" then
|
||||
input_show_source =
|
||||
audio_to_stereo(
|
||||
input.harbor(id="harbor:input_show",
|
||||
input_show_mount,
|
||||
port=input_show_port,
|
||||
auth=make_input_auth_handler("show"),
|
||||
max=40.,
|
||||
on_connect=input_show_on_connect,
|
||||
on_disconnect=input_show_on_disconnect))
|
||||
|
||||
ignore(output.dummy(input_show_source, fallible=true))
|
||||
|
||||
switch(id="switch:blank+schedule+show",
|
||||
track_sensitive=false,
|
||||
transitions=[transition, transition],
|
||||
[({!input_show_streaming}, input_show_source), ({true}, s)]
|
||||
)
|
||||
else
|
||||
s
|
||||
end
|
||||
|
||||
s = if input_main_port != 0 and input_main_mount != "" then
|
||||
input_main_source =
|
||||
audio_to_stereo(
|
||||
input.harbor(id="harbor:input_main",
|
||||
input_main_mount,
|
||||
port=input_main_port,
|
||||
auth=make_input_auth_handler("main"),
|
||||
max=40.,
|
||||
on_connect=input_main_on_connect,
|
||||
on_disconnect=input_main_on_disconnect))
|
||||
|
||||
ignore(output.dummy(input_main_source, fallible=true))
|
||||
|
||||
switch(id="switch:blank+schedule+show+main",
|
||||
track_sensitive=false,
|
||||
transitions=[transition, transition],
|
||||
[({!input_main_streaming}, input_main_source), ({true}, s)]
|
||||
)
|
||||
else
|
||||
s
|
||||
end
|
||||
|
||||
server.register(namespace="sources",
|
||||
description="Stop main input source.",
|
||||
usage="stop_input_main",
|
||||
"stop_input_main",
|
||||
fun (s) -> begin log("sources.stop_input_main") stop_input_main() "Done." end)
|
||||
server.register(namespace="sources",
|
||||
description="Start main input source.",
|
||||
usage="start_input_main",
|
||||
"start_input_main",
|
||||
fun (s) -> begin log("sources.start_input_main") start_input_main() "Done." end)
|
||||
server.register(namespace="sources",
|
||||
description="Stop show input source.",
|
||||
usage="stop_input_show",
|
||||
"stop_input_show",
|
||||
fun (s) -> begin log("sources.stop_input_show") stop_input_show() "Done." end)
|
||||
server.register(namespace="sources",
|
||||
description="Start show input source.",
|
||||
usage="start_input_show",
|
||||
"start_input_show",
|
||||
fun (s) -> begin log("sources.start_input_show") start_input_show() "Done." end)
|
||||
server.register(namespace="sources",
|
||||
description="Stop schedule source.",
|
||||
usage="stop_schedule",
|
||||
"stop_schedule",
|
||||
fun (s) -> begin log("sources.stop_schedule") stop_schedule() "Done." end)
|
||||
server.register(namespace="sources",
|
||||
description="Start schedule source.",
|
||||
usage="start_schedule",
|
||||
"start_schedule",
|
||||
fun (s) -> begin log("sources.start_schedule") start_schedule() "Done." end)
|
||||
Loading…
x
Reference in New Issue
Block a user