Liquidsoap for Streaming #4197
|
Hi everyone, Thanks to @toots and @gAlleb for the script. I'm working with the script from this repository for my YouTube live streaming. I want to set up functionality to play requested tracks by viewers. If the requested track isn't already uploaded, I would like to upload it and play it immediately, similar to how AzuraCast works. Additionally, I want to integrate a chatbot to trigger the functions (skip, play, and add to queue). Here’s what I’m looking to do:
It would be much easier if there were a web UI similar to AzuraCast for managing these functions. I like AzuraCast, but I’m not sure if it can work with the ai-radio script while providing dynamic metadata display like Liquidsoap's ai-radio video canvas in real-time. I don't need any AI functionality from the script, just the canvas with dynamic real-time updates. Any help or advice would be greatly appreciated! Thank you! |
Replies: 3 comments 2 replies
|
Hi @WittyWidget
Here is a very quick example which uses Azuracast default
Example
# if your source is radio:
# Skip - will be available via http://localhost:8007/skip
def skipper(_)
radio.skip()
http.response(data="The current song was skipped!")
end
harbor.http.register.simple(port=8007, "/skip", skipper)
queue = request.queue()
radio = fallback(track_sensitive=true, [queue, radio])
def play(request)
fname = request.query["file"]
log.important("Requested file #{fname}.")
if file.exists(fname) then
log.important("File Request Accepted.")
queue.push.uri("#{fname}")
http.response(content_type="text/html", status_code=200, data="<p>Request pushed.</p>")
else
log.important("File Request Rejected File Not Found.")
http.response(content_type="text/html", status_code=404, data="<p>Invalid file: #{fname}</p>")
end
end
harbor.http.register.simple("/play", play, port=8007, method="GET")
# Play: Play a specific track immediately. Create an `interupting_queue`
# and push files via http://localhost:8007/play_now
interrupting_queue = request.queue()
radio = fallback(track_sensitive=false, [interrupting_queue, radio])
def play(request)
fname = request.query["file"]
log.important("Requested file #{fname}.")
if file.exists(fname) then
log.important("File Request Accepted.")
interrupting_queue.push.uri("#{fname}")
http.response(content_type="text/html", status_code=200, data="<p>Request pushed.</p>")
else
log.important("File Request Rejected File Not Found.")
http.response(content_type="text/html", status_code=404, data="<p>Invalid file: #{fname}</p>")
end
end
harbor.http.register.simple("/play_now", play, port=8007, method="GET")
And so on.
So everything is relatively easy if you you are eager to invest some time and learn something new. |
|
Thank you very much, @gAlleb, for the answer. I just tested the harbor functions (skip, play, and play_now). They are working, but not quite like in Azuracast. There's a delay of 10-15 seconds for skip and 7 seconds for play_now, whereas in Azuracast, the skip/play happens immediately. Does Azuracast have any separate configuration for that? Thank you again for helping the community. |
|
Dear @gAlleb,
Liquidsoap code for AzuraCast - RTMP Video Stream# Store and retrieve file covers using metadata. This returns a set of
# getter/setter methods that can be used to store and retrieve cover art.
# Typical usage is to set cover art in a `on_metadata` handler and retrieve
# it in a `video.add_image` operator. See `video.add_cover` for an implementation
# example.
# @category Metadata
# @flag extra
# @param ~default Default cover file when no cover is available
# @param ~mime_types Recognized mime types and their corresponding file extensions.
def file.cover.manager(
~id=null(),
~mime_types=[
("image/gif", "gif"),
("image/jpeg", "jpeg"),
("image/jpg", "jpeg"),
("image/png", "png"),
("image/webp", "webp")
],
~default
) =
id = string.id.default(id, default="cover")
temp_dir = file.temp_dir("cover-manager")
on_cleanup({file.rmdir(temp_dir)})
last_cover_file = ref(default)
current_cover_file = ref(default)
def set_current_cover(filename) =
last_cover = last_cover_file()
last_cover_file := current_cover_file()
current_cover_file := filename
if
last_cover != default
then
log.info(
label=id,
"Removing #{string.quote(last_cover)}"
)
file.remove(last_cover)
end
end
def save_cover_to_file(cover) =
mime = cover.mime
extension = mime_types[mime]
cover_file =
if
extension != ""
then
cover_file = file.temp("#{id}_", ".#{extension}")
file.write(cover_file, data=cover)
cover_file
else
null()
end
(cover_file, mime)
end
def extract_cover_from_metadata(_metadata) =
filename = _metadata["filename"]
log.info(
label=id,
"Extracting cover from #{string.quote(filename)}."
)
cover = metadata.cover(_metadata)
let (cover_file, mime) =
null.case(cover, {(null(), "")}, save_cover_to_file)
next_cover =
if
cover == null()
then
log.important(
label=id,
"File #{string.quote(filename)} has no cover."
)
default
elsif
cover_file == null()
then
log.important(
label=id,
"File #{string.quote(filename)} has unknown mime type #{
string.quote(mime)
}."
)
default
else
cover_file = null.get(cover_file)
log.important(
label=id,
"Cover for #{string.quote(filename)} saved to #{
string.quote(cover_file)
}."
)
cover_file
end
set_current_cover(next_cover)
end
current_cover_file.{set=extract_cover_from_metadata}
end
# Extract cover from the source's metadata and add it as a static image.
# @category Track / Video processing
# @flag extra
# @param ~id Force the value of the source ID.
# @param ~fallible Whether we are allowed to fail (in case the file is non-existent or invalid).
# @param ~width Scale to width
# @param ~height Scale to height
# @param ~x x position.
# @param ~y y position.
# @param ~default Default cover file when no cover is available
# @param ~mime_types Recognized mime types and their corresponding file extensions.
def video.add_cover(
~fallible=false,
~width=null(),
~height=null(),
~x=getter(0),
~y=getter(0),
~mime_types=[
("image/gif", "gif"),
("image/jpeg", "jpeg"),
("image/jpg", "jpeg"),
("image/png", "png"),
("image/webp", "webp")
],
~default,
s
) =
cover_file = file.cover.manager(mime_types=mime_types, default=default)
s = source.on_metadata(s, cover_file.set)
video.add_image(
x=x, y=y, width=width, height=height, file=cover_file, s
)
end
def string.of_int(~digits=0, n) =
s = string(n)
if
string.length(s) >= digits
then
s
else
string.make(char_code=48, digits - string.length(s)) ^ s
end
end
def remaining() =
time = source.remaining(radio_before_crossfade)
seconds = string.of_int(digits=2, int(time mod 60.))
minutes = string.of_int(digits=2, int(time / 60.))
"#{minutes}:#{seconds}"
end
# Custom Configuration (Specified in Station Profile)
base_dir = "/var/azuracast/stations/YOURSTATIONNAME"
stream_key = ""
background_dir = base_dir ^ "/videos"
font_file = base_dir ^ "/font.ttf"
font_size = 50
font_x = 340
font_y = 990
# progress bar
def source_percent(s)
source.elapsed(s) / source.duration(s) * 100.0
end
# pre-calculate percent
px_percent = 1280.0/100.0
# We get NP info right from a radio source
nowplaying = ref("")
def update_nowplaying(m)
if string.length(m["artist"]) <= 0 then
if string.contains(substring=" - ", m["title"]) then
let (a, t) = string.split.first(separator=" - ", m["title"])
nowplaying := a ^ " - " ^ t
end
else
nowplaying := m["artist"] ^ " - " ^ m["title"]
end
end
radio.on_metadata(fun(m) -> thread.run(fast=false, {update_nowplaying(m)}))
videostream = playlist(background_dir)
videostream =
video.add_rectangle(
color=0xFF00FF,
alpha=0.4,
x=52,
y=150,
width=100,
height=100,
videostream
)
videostream = video.add_rectangle(color=0x333333, alpha=0.7, x=925, y=50, width=355, height=55, videostream)
videostream =
video.add_text(
x=938, y=53,
size=43,
color=0xFCB900,
font=font_file,
"Radio",
videostream
)
videostream = video.add_text(
font=font_file,
size=font_size,
color=0xFFFFFF,
x=font_x,
y=font_y,
speed=0,
{nowplaying()},
videostream
)
videostream = video.add_image(x=52, y=52, width=100, height=100, file=base_dir ^ "/logo_500x500.png", videostream)
videostream = video.add_text(size=20, x=50, y=647,color=0xFFFFFF, {"Next in #{remaining()}"}, videostream)
videostream = video.add_rectangle(color=0xfcb900, x=0, y=673, height=10, width={int(px_percent*source_percent(radio_before_crossfade))}, videostream)
videostream = video.add_rectangle(color=0xfcb900, x=50, y=234, height=256, width=256, videostream)
videostream = source.mux.video(video=videostream, radio)
videostream = video.add_cover(x=53, y=237 , width=250, height=250, default=base_dir ^ "/logo_500x500.png",fallible=false, videostream)
# Output to YouTube
enc = %ffmpeg(
format="flv",
%video(codec="libx264", pixel_format="yuv420p", b="300k", preset="superfast", r=25, g=50),
%audio(
codec="aac",
samplerate=44100,
channels=2,
b="128k",
profile="aac_low"
)
)
output.url(url=stream_key, enc, fallible=true, videostream)Now, I have tested the built-in 'Skip' button available on the AzuraCast web interface, which instantly triggers the current track to skip. However, when using standalone Liquidsoap (ai-radio), there is a delay of 10-15 seconds with the harbor/telnet functions. My apologies, I'm trying to understand the code, but I'm getting stuck. Do you have a full template (ai-radio) canvas for AzuraCast configuration? The provided template only displays the album cover and progress bar. The AzuraCast method seems easier. I would be grateful if you could upload the full template for AzuraCast Users in your repository. Many thanks! |
Hi @WittyWidget
Edit Liquidsoap Configurationand adding necessary code there) and use its UI and queuing mechanisms to queue tracks.So it's definitely a good option if you want to save time. Usually "videocode" is pasted into the last box of
Edit Liquidsoap Configuration.Here is a very quick example which uses Azuracast default
radiosource, which can be pasted into the last box of theEdit Liquidsoap Configurationof your Azuracast station.