Closed netbladenl closed 11 months ago
Logs :
2023/11/11 18:02:28 [lang:3] API feedback - Response (200): true
2023/11/11 18:03:12 [lang:3] API auth - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/auth' with body: { "user": "user", "password": "****" }
2023/11/11 18:03:12 [lang:3] API auth - Response (200): true
2023/11/11 18:03:12 [lang:3] DJ Source connected! Last authenticated DJ: iwan - [("Authorization", "Basic **Password**="), ("Host", "streaming.youradio.nl:8005"), ("User-Agent", "butt 0.1.39"), ("Content-Type", "audio/mpeg"), ("ice-name", "no name"), ("ice-public", "0"), ("ice-audio-info", "ice-bitrate=320;ice-channels=2;ice-samplerate=48000"), ("Expect", "100-continue")]
2023/11/11 18:03:12 [lang:3] API djon - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/djon' with body: { "user": "iwan" }
2023/11/11 18:03:12 [lang:3] API djon - Response (200): true
2023/11/11 18:03:12 [input_streamer:3] Decoding...
2023/11/11 18:03:42 [input_streamer:2] Error while reading from client: Timed out after waiting for 30.02 sec.
2023/11/11 18:03:42 [lang:3] API djoff - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/djoff' with body: { "user": "iwan" }
2023/11/11 18:03:42 [lang:3] API djoff - Response (200): true
2023/11/11 18:03:42 [input_streamer:2] Feeding stopped: Avutil.Error(Invalid data found when processing input).
2023/11/11 18:03:42 [lang:3] API djoff - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/djoff' with body: { "user": "" }
2023/11/11 18:03:42 [lang:3] API djoff - Response (200): true
2023/11/11 18:03:57 [lang:3] API auth - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/auth' with body: { "user": "iwan", "password": "***" }
2023/11/11 18:03:57 [lang:3] API auth - Response (200): true
2023/11/11 18:03:57 [lang:3] DJ Source connected! Last authenticated DJ: iwan - [("Authorization", "Basic *****="), ("Host", "streaming.youradio.nl:8005"), ("User-Agent", "butt 0.1.39"), ("Content-Type", "audio/mpeg"), ("ice-name", "no name"), ("ice-public", "0"), ("ice-audio-info", "ice-bitrate=320;ice-channels=2;ice-samplerate=48000"), ("Expect", "100-continue")]
2023/11/11 18:03:57 [lang:3] API djon - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/djon' with body: { "user": "iwan" }
2023/11/11 18:03:57 [lang:3] API djon - Response (200): true
2023/11/11 18:03:57 [input_streamer:3] Decoding...
2023/11/11 18:04:16 [input_streamer:2] Feeding stopped: Avutil.Error(Invalid data found when processing input).
2023/11/11 18:04:16 [lang:3] API djoff - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/djoff' with body: { "user": "iwan" }
2023/11/11 18:04:16 [lang:3] API djoff - Response (200): true
2023/11/11 18:04:35 [lang:3] API auth - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/auth' with body: { "user": "", "password": "" }
2023/11/11 18:04:35 [lang:3] API auth - Response (200): true
2023/11/11 18:04:35 [lang:3] DJ Source connected! Last authenticated DJ: iwan - [("Host", "streaming.youradio.nl:8005"), ("icy-name", "No Name"), ("icy-pub", "0"), ("icy-br", "320"), ("content-type", "audio/mpeg")]
2023/11/11 18:04:35 [lang:3] API djon - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/djon' with body: { "user": "iwan" }
2023/11/11 18:04:35 [lang:3] API djon - Response (200): true
2023/11/11 18:04:35 [input_streamer:3] Decoding...
2023/11/11 18:05:05 [input_streamer:2] Error while reading from client: Timed out after waiting for 30.00 sec.
2023/11/11 18:05:05 [lang:3] API djoff - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/djoff' with body: { "user": "iwan" }
2023/11/11 18:05:05 [lang:3] API djoff - Response (200): true
Liquidsoap config:
# WARNING! This file is automatically generated by AzuraCast.
# Do not update it directly!
init.daemon.set(false)
init.daemon.pidfile.path.set("/var/azuracast/stations/radio_netblade/config/liquidsoap.pid")
log.stdout.set(true)
log.file.set(false)
settings.server.log.level.set(4)
settings.server.socket.set(true)
settings.server.socket.permissions.set(0o660)
settings.server.socket.path.set("/var/azuracast/stations/radio_netblade/config/liquidsoap.sock")
settings.harbor.bind_addrs.set(["0.0.0.0"])
settings.encoder.metadata.export.set(["artist","title","album","song"])
environment.set("TZ", "Europe/Amsterdam")
autodj_is_loading = ref(true)
ignore(autodj_is_loading)
autodj_ping_attempts = ref(0)
ignore(autodj_ping_attempts)
# Track live-enabled status.
live_enabled = ref(false)
ignore(live_enabled)
# Track live transition for crossfades.
to_live = ref(false)
ignore(to_live)
# Reimplement LS's now-deprecated drop_metadata function.
def drop_metadata(~id=null(), s)
let {metadata=_, ...tracks} = source.tracks(s)
source(id=id, tracks)
end
# Transport for HTTPS outputs.
https_transport = http.transport.ssl()
ignore(https_transport)
azuracast_api_url = "http://127.0.0.1:6010/api/internal/4/liquidsoap"
azuracast_api_key = "(PASSWORD)"
def azuracast_api_call(~timeout=2.0, url, payload) =
full_url = "#{azuracast_api_url}/#{url}"
log("API #{url} - Sending POST request to '#{full_url}' with body: #{payload}")
try
response = http.post(full_url,
headers=[
("Content-Type", "application/json"),
("User-Agent", "Liquidsoap AzuraCast"),
("X-Liquidsoap-Api-Key", "#{azuracast_api_key}")
],
timeout=timeout,
data=payload
)
log("API #{url} - Response (#{response.status_code}): #{response}")
"#{response}"
catch err do
log("API #{url} - Error: #{error.kind(err)} - #{error.message(err)}")
"false"
end
end
station_media_dir = "/var/azuracast/stations/radio_netblade/media"
def azuracast_media_protocol(~rlog=_,~maxtime=_,arg) =
["#{station_media_dir}/#{arg}"]
end
protocol.add(
"media",
azuracast_media_protocol,
doc="Pull files from AzuraCast media directory.",
syntax="media:uri"
)
playlist_default = playlist(id="playlist_default",mime_type="audio/x-mpegurl",mode="randomize",reload_mode="watch","/var/azuracast/stations/radio_netblade/playlists/playlist_default.m3u")
playlist_default = cue_cut(id="cue_playlist_default", playlist_default)
playlist_jingles = playlist(id="playlist_jingles",mime_type="audio/x-mpegurl",mode="randomize",reload_mode="watch","/var/azuracast/stations/radio_netblade/playlists/playlist_jingles.m3u")
playlist_jingles = cue_cut(id="cue_playlist_jingles", playlist_jingles)
playlist_jingles = drop_metadata(playlist_jingles)
# Standard Playlists
radio = random(id="standard_playlists", weights=[3], [playlist_default])
# Once per x Songs Playlists
radio = rotate(weights=[1,3], [playlist_jingles, radio])
# AutoDJ Next Song Script
def autodj_next_song() =
response = azuracast_api_call(
"nextsong",
""
)
if (response == "") or (response == "false") then
null()
else
r = request.create(response)
if request.resolve(r) then
r
else
null()
end
end
end
# Delayed ping for AutoDJ Next Song
def wait_for_next_song(autodj)
autodj_ping_attempts.set(autodj_ping_attempts() + 1)
if source.is_ready(autodj) then
log("AutoDJ is ready!")
autodj_is_loading.set(false)
-1.0
elsif autodj_ping_attempts() > 200 then
log("AutoDJ could not be initialized within the specified timeout.")
autodj_is_loading.set(false)
-1.0
else
0.5
end
end
dynamic = request.dynamic(id="next_song", timeout=20.0, retry_delay=10., autodj_next_song)
dynamic = cue_cut(id="cue_next_song", dynamic)
dynamic_startup = fallback(
id = "dynamic_startup",
track_sensitive = false,
[
dynamic,
source.available(
blank(id = "autodj_startup_blank", duration = 120.),
predicate.activates({autodj_is_loading()})
)
]
)
radio = fallback(id="autodj_fallback", track_sensitive = true, [dynamic_startup, radio])
ref_dynamic = ref(dynamic);
thread.run.recurrent(delay=0.25, { wait_for_next_song(ref_dynamic()) })
requests = request.queue(id="requests")
requests = cue_cut(id="cue_requests", requests)
radio = fallback(id="requests_fallback", track_sensitive = true, [requests, radio])
interrupting_queue = request.queue(id="interrupting_requests")
interrupting_queue = cue_cut(id="cue_interrupting_requests", interrupting_queue)
radio = fallback(id="interrupting_fallback", track_sensitive = false, [interrupting_queue, radio])
# Skip command (used by web UI)
def add_skip_command(s) =
def skip(_) =
source.skip(s)
"Done!"
end
server.register(namespace="radio", usage="skip", description="Skip the current song.", "skip",skip)
end
add_skip_command(radio)
# Apply amplification metadata (if supplied)
radio = amplify(override="liq_amplify", 1., radio)
# Replaygain Metadata
enable_replaygain_metadata()
radio = replaygain(radio)
def live_aware_crossfade(old, new) =
if to_live() then
# If going to the live show, play a simple sequence
sequence([fade.out(old.source),fade.in(new.source)])
else
# Otherwise, use the smart transition
cross.smart(old, new, fade_in=3.00, fade_out=3.00)
end
end
radio = cross(minimum=0., duration=4.50, live_aware_crossfade, radio)
# DJ Authentication
last_authenticated_dj = ref("")
live_dj = ref("")
def dj_auth(login) =
auth_info =
if (login.user == "source" or login.user == "") and (string.match(pattern="(:|,)+", login.password)) then
auth_string = string.split(separator="(:|,)", login.password)
{user = list.nth(default="", auth_string, 0),
password = list.nth(default="", auth_string, 2)}
else
{user = login.user, password = login.password}
end
response = azuracast_api_call(
timeout=5.0,
"auth",
json.stringify(auth_info)
)
if (response == "true") then
last_authenticated_dj.set(auth_info.user)
true
else
false
end
end
def live_connected(header) =
dj = last_authenticated_dj()
log("DJ Source connected! Last authenticated DJ: #{dj} - #{header}")
live_enabled.set(true)
live_dj.set(dj)
_ = azuracast_api_call(
timeout=5.0,
"djon",
json.stringify({user = dj})
)
end
def live_disconnected() =
_ = azuracast_api_call(
timeout=5.0,
"djoff",
json.stringify({user = live_dj()})
)
live_enabled.set(false)
live_dj.set("")
end
# A Pre-DJ source of radio that can be broadcast if needed
radio_without_live = radio
ignore(radio_without_live)
# Live Broadcasting
live = input.harbor("/", id = "input_streamer", port = 9025, auth = dj_auth, icy = true, icy_metadata_charset = "UTF-8", metadata_charset = "UTF-8", on_connect = live_connected, on_disconnect = live_disconnected, buffer = 5.00, max = 10.00)
def insert_missing(m) =
if m == [] then
[("title", "Live Broadcast"), ("is_live", "true")]
else
[("is_live", "true")]
end
end
live = metadata.map(insert_missing, live)
radio = fallback(id="live_fallback", track_sensitive=false, replay_metadata=true, [live, radio])
# Skip non-live track when live DJ goes live.
def check_live() =
if live.is_ready() then
if not to_live() then
to_live.set(true)
radio_without_live.skip()
end
else
to_live.set(false)
end
end
# Continuously check on live.
radio = source.on_frame(radio, check_live)
# Record Live Broadcasts
recording_base_path = "/var/azuracast/stations/radio_netblade/temp"
recording_extension = "mp3"
output.file(
%mp3(samplerate=44100, stereo=true, bitrate=128),
fun () -> begin
if (live_enabled()) then
time.string("#{recording_base_path}/#{live_dj()}/stream_%Y%m%d-%H%M%S.#{recording_extension}.tmp")
else
""
end
end,
live,
fallible=true,
on_close=fun (tempPath) -> begin
path = string.replace(pattern=".tmp$", (fun(_) -> ""), tempPath)
log("Recording stopped: Switching from #{tempPath} to #{path}")
process.run("mv #{tempPath} #{path}")
()
end
)
# Allow for Telnet-driven insertion of custom metadata.
radio = server.insert_metadata(id="custom_metadata", radio)
radio = ladspa.master_me(
bypass = false,
target = -14,
brickwall_bypass = false,
brickwall_ceiling = -1.00,
brickwall_release = 75.00,
eq_bypass = false,
eq_highpass_freq = 5.00,
eq_side_bandwidth = 1.00,
eq_side_freq = 600.00,
eq_side_gain = 1.00,
eq_tilt_gain = 0.00,
gate_attack = 0.00,
gate_bypass = true,
gate_hold = 50.00,
gate_release = 430.50,
gate_threshold = -90.00,
kneecomp_attack = 20.00,
kneecomp_bypass = false,
kneecomp_dry_wet = 50,
kneecomp_ff_fb = 50,
kneecomp_knee = 6.00,
kneecomp_link = 60,
kneecomp_makeup = 0.00,
kneecomp_release = 340.00,
kneecomp_strength = 20,
kneecomp_tar_thresh = -4.00,
leveler_brake_threshold = -10.00,
leveler_bypass = false,
leveler_max = 10.00,
leveler_max__ = 10.00,
leveler_speed = 20,
limiter_attack = 3.00,
limiter_bypass = false,
limiter_ff_fb = 50,
limiter_knee = 3.00,
limiter_makeup = 0.00,
limiter_release = 40.00,
limiter_strength = 80,
limiter_tar_thresh = 6.00,
mscomp_bypass = false,
high_attack = 8.00,
high_crossover = 8000.00,
high_knee = 12.00,
high_link = 30,
high_release = 30.00,
high_strength = 30,
high_tar_thresh = -12.00,
low_attack = 15.00,
low_crossover = 60.00,
low_knee = 12.00,
low_link = 70,
low_release = 150.00,
low_strength = 10,
low_tar_thresh = -3.00,
makeup = 1.00,
dc_blocker = false,
input_gain = 0.00,
mono = false,
phase_l = false,
phase_r = false,
stereo_correct = false,
radio
)
error_file = single(id="error_jingle", "/usr/local/share/icecast/web/error.mp3")
def tag_error_file(m) =
ignore(m)
[("is_error_file", "true")]
end
error_file = metadata.map(tag_error_file, error_file)
radio = fallback(id="safe_fallback", track_sensitive = false, [radio, error_file])
# Send metadata changes back to AzuraCast
last_title = ref("")
last_artist = ref("")
def metadata_updated(m) =
def f() =
if (m["is_error_file"] != "true") then
if (m["title"] != last_title() or m["artist"] != last_artist()) then
last_title.set(m["title"])
last_artist.set(m["artist"])
j = json()
if (m["song_id"] != "") then
j.add("song_id", m["song_id"])
j.add("media_id", m["media_id"])
j.add("playlist_id", m["playlist_id"])
else
j.add("artist", m["artist"])
j.add("title", m["title"])
end
_ = azuracast_api_call(
"feedback",
json.stringify(j)
)
end
end
end
thread.run(f)
end
radio.on_metadata(metadata_updated)
# Handle "Jingle Mode" tracks by replaying the previous metadata.
last_metadata = ref([])
def handle_jingle_mode(m) =
if (m["jingle_mode"] == "true") then
last_metadata()
else
last_metadata.set(m)
m
end
end
radio = metadata.map(update=false, strip=true, handle_jingle_mode, radio)
# Local Broadcasts
output.icecast(%fdkaac(channels=2, samplerate=44100, bitrate=128, afterburner=false, aot="mpeg4_aac_lc", sbr_mode=true), id="local_1", host = "127.0.0.1", port = 9021, password = "(PASSWORD)", mount = "/radio.aac", name = "Radio NetBlade", description = "netblade radio 10's", genre = "alternative", public = false, encoding = "UTF-8", radio)
output.icecast(%mp3(samplerate=44100, stereo=true, bitrate=320), id="local_2", host = "127.0.0.1", port = 9021, password = "(PASSWORD)", mount = "/radio.mp3", name = "Radio NetBlade", description = "netblade radio 10's", genre = "alternative", public = true, encoding = "UTF-8", radio)
# Remote Relays
Hi @netbladenl, I think the problem is in liquidsoap. I can't fix it, but I can reproduce it. It's not necessary for you to repeat the reproduction steps.
Start liquidsoap with the following script
s = sine(200.)
l = input.harbor("/test", port=8000)
l = metadata.map(fun(_) -> [], l)
r = fallback([l, s], track_sensitive=false)
output.file(%mp3, {"/tmp/f.mp3"}, l, fallible=true)
output.dummy(r)
[switch:3] Switch to sine.
ffmpeg -hide_banner -re -f lavfi -i "sine=frequency=1000:duration=20" -b:a 128k -c:a mp3 -f mp3 icecast://source:hackme@127.0.0.1:8010/test
.[output_file:2] Warning: there may be an infinite sequence of empty tracks!
Thanks so much for the detailed steps @vitoyucepi. I haven't been able to reproduce, however, either with my local dev env or using savonet/liquidsoap:v2.2.2
on docker. Could you provide more details on the reproduction steps on your end?
Thanks!
OK, here's a docker-compose setup.
compose.yaml
version: "3.8"
services:
liquidsoap:
image: savonet/liquidsoap:v2.2.2
command:
- "/tmp/test/main.liq"
ports:
- "127.0.0.1:8010:8000"
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "3"
volumes:
- ./:/tmp/test
mail.liq
s = sine(200.)
l = input.harbor("/test", port=8000)
l = metadata.map(fun(_) -> [], l)
r = fallback([l, s], track_sensitive=false)
output.file(%mp3, {"/tmp/f.mp3"}, l, fallible=true)
output.dummy(r)
docker compose up
command in the first terminal.docker stats
and look for cpu usage.
top
will also work.ffmpeg -hide_banner -re -f lavfi -i "sine=frequency=1000:duration=30" -b:a 128k -c:a mp3 -f mp3 icecast://source:hackme@127.0.0.1:8010/test
.
I'm not sure if the duration of the stream makes any difference.In the stats monitor, you'll see the following sequence of events.
While loading liquidsoap, the container will use 100% of the CPU. That's okay.
Start the stream.
I can reproduce now but weirdly enough, on amd64
only. The issue does not seem to pop out on arm64
, that's puzzling.
I'm having this same issue in 2.2.2. I just came on here to see if there was a solution, I may just roll back to 2.2.1 for now.
I'll get to this as soon as possible. Thanks for y'all patience.
I believe that the changes in this CI build might fix the problem: https://github.com/savonet/liquidsoap/actions/runs/6991394869 Any chance one of y'all could test them?
Version 2.2.3+git@b2df6caed has the same problem.
I was able to test and confirm that the latest commit on the fix-has-ticked-2.2.x branch does fix the issue.
Reproduction was tricky because it relied on two elements:
Partial frame fill can depend on timing such as when the harbor gets available. By changing the duration to 20.03
(frame sizes are 0.04
by default), I was able to make the test case reproducible.
Thanks for your work, unfortunately the problem still exists with the new version of liquidsoap
2023/11/28 13:09:30 [lang:3] API djon - Response (200): true
2023/11/28 13:09:30 [input_streamer:3] Decoding...
2023/11/28 13:09:39 [live_fallback:3] Switch to metadata_map.4 with transition.
2023/11/28 13:09:39 [metadata_map.7:3] Inserting missing metadata.
2023/11/28 13:10:09 [input_streamer:2] Error while reading from client: Harbor.Make(T).Websocket_closed
2023/11/28 13:10:09 [lang:3] API djoff - Sending POST request to 'http://127.0.0.1:6010/api/internal/4/liquidsoap/djoff' with body: { "user": "iwan" }
[matroska,webm @ 0x7f84e2e17000] File ended prematurely at pos. 198330 (0x306ba)
[matroska,webm @ 0x7f84e2e17000] Seek to desired resync point failed. Seeking to earliest point available instead.
2023/11/28 13:10:09 [lang:3] API djoff - Response (200): true
2023/11/28 13:10:09 [input_streamer:2] Feeding stopped: Ffmpeg_decoder.End_of_file.
2023/11/28 13:10:09 [lang:3] API djoff - Sending POST request to 'http://127.0.0.1:6010/api/internal/4/liquidsoap/djoff' with body: { "user": "" }
2023/11/28 13:10:09 [lang:3] API djoff - Response (200): true
[2023-11-28 13:10:20] WARN source/_source_read Nothing received on /radio.aac for 3 seconds
[2023-11-28 13:10:20] WARN source/_source_read Nothing received on /radio.mp3 for 3 seconds
[2023-11-28 13:10:20] WARN source/_source_read Nothing received on /radio.aac for 3 seconds
[2023-11-28 13:10:20] WARN source/_source_read Nothing received on /radio.mp3 for 3 seconds
[2023-11-28 13:10:20] WARN source/_source_read Nothing received on /radio.aac for 3 seconds
[2023-11-28 13:10:28] WARN source/_source_read Disconnecting /radio.aac due to socket timeout
[2023-11-28 13:10:28] WARN source/_source_read Disconnecting /radio.mp3 due to socket timeout
[2023-11-28 13:11:01] WARN fserve/fserve_client_create req for file "/usr/local/share/icecast/web/radio.mp3" No such file or directory
[2023-11-28 13:12:21] WARN fserve/fserve_client_create req for file "/usr/local/share/icecast/web/radio.mp3" No such file or directory
[2023-11-28 13:12:22] WARN fserve/fserve_client_create req for file "/usr/local/share/icecast/web/radio.mp3" No such file or directory
The autodj dont come back, after the streamer disconnects
Sorry to hear. How did you test it? The rolling release guide hasn't completed yet..
commit on the fix-has-ticked-2.2.x branch does fix the issue.
Oh I thought that the patch was already released, my bad. I've asked Buster to use the newest rolling release of liquidsoap with the Azuracast rolling release. Maybe I'm a bit to fast.
2.2.3+git@4181a8c9d works fine.
The build of rolling-release-v2.2.x
including the fix has just finished!
The problem has been resolved, thanks for all of your help!
Describe the bug I'm using the latest rolling release of Azuracast, and when the function to record live broadcasts is on liquidsoap stops working correctly. ID3 information is not being showed correctly. And when the streamer is disconnecting most of the time the autodj of liquidsoap dont play. and if it works you can hear the last seconds of the song being played before the streamer connects instead of directly a new song.
To Reproduce In azuracast you have to go to edit station profile -> streamers/dj's Turn on the option "Record Live Broadcasts" connect with butt or anything else that is able to set up a shoutcast/icecast stream and disconnect after a minute. Most likely the autodj doesnt play as it should. Turning this off solves the problem but live streamers wont be recorded.
Expected behavior When streamer disconnects the autodj should be playing, and when the streamer is connected the ID3 information should be showed instead of the song before playing when the streamer connects
Version details
Install method It's included within the docker distribution of Azuracast
Ticket at github of Azuracast ; https://github.com/AzuraCast/AzuraCast/issues/6727