savonet / liquidsoap

Liquidsoap is a statically typed scripting general-purpose language with dedicated operators and backend for all thing media, streaming, file generation, automation, HTTP backend and more.
http://liquidsoap.info
GNU General Public License v2.0
1.45k stars 131 forks source link

The case of the disappearing buffer #4139

Open SpeedyCharly opened 2 months ago

SpeedyCharly commented 2 months ago

Description

Posted this as a bug, then as discussion on the Azuracast Forum [https://github.com/AzuraCast/AzuraCast/issues/7433] but it didn't attract the attention I had hoped... [Buster later stated it was likely an LS issue]

Wondering if anyone else has experienced this;

For a long while now, we've been having live streamers getting disconnected for a few seconds without warning following a tiny bit of dead air (usually slight delay in switching to mike after a song). This typically occurs once or twice over the course of their broadcast. Such delays are not usually long enough for the streaming app to go off air. On such occasions, Autodj automatically kicks in with a song that will stop playing as soon as the stream comes back after the little bit of dead air ends.

Looking to the logs was not giving us any obvious clues as to what was happening, so I decided to do some experimentation to hopefully discover what was happening. I would intentionally create little bits of dead air, and, after about 10 or 12 such bits the Autodj would kick in until I resumed streaming (all without having to reconnect as the encoder would stay alive during such a short delay).

After a few times doing this, I started noticing that the buffer delay was getting shorter after each such little bit of dead air. Starting at 30 seconds, it would erode all the way to zero at which time the Autodj would kick in on the next bit of dead air.

I don't remember exactly on which release it started doing this, but it seems to me it wasn't always so; the delay would remain at 30 secs or whatever time you would set the buffer at.

I've included a bit of LS log showing one such occasion where the streamer was dropped - I personally don't see a clue to a solution, but you guys might...

As always, thank you for your hard work and for such a great app Speedy Charly

Steps to reproduce

Stated above...

Expected behavior

There is no reason that little bits of dead air should erode the buffer down to zero, it should remain constant. In fact this is a fairly recent issue that didn't exist in previous versions...

Liquidsoap version

LiquidSoap 2.2.5

Liquidsoap build config

# WARNING! This file is automatically generated by AzuraCast.
# Do not update it directly!

# Custom Configuration (Specified in Station Profile)
log.level.set(4)

init.daemon.set(false)
init.daemon.pidfile.path.set("/var/azuracast/stations/radio_des_festivals/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_des_festivals/config/liquidsoap.sock")

settings.harbor.bind_addrs.set(["0.0.0.0"])
settings.encoder.metadata.export.set(["artist","title","album","song"])

environment.set("TZ", "America/Detroit")

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/1/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_des_festivals/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"
)

# Optimize Performance
runtime.gc.set(runtime.gc.get().{
  space_overhead = 80,
  allocation_policy = 2
})

playlist_country_franco = playlist(id="playlist_country_franco",mime_type="audio/x-mpegurl",mode="randomize",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_country_franco.m3u")

playlist_ouesteurne = playlist(id="playlist_ouesteurne",mime_type="audio/x-mpegurl",mode="normal",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_ouesteurne.m3u")
playlist_ouesteurne = merge_tracks(id="merge_playlist_ouesteurne", playlist_ouesteurne)

playlist_vero = playlist(id="playlist_vero",mime_type="audio/x-mpegurl",mode="normal",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_vero.m3u")
playlist_vero = merge_tracks(id="merge_playlist_vero", playlist_vero)

playlist_ouesteurne2 = playlist(id="playlist_ouesteurne2",mime_type="audio/x-mpegurl",mode="normal",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_ouesteurne2.m3u")

playlist_campagnard = playlist(id="playlist_campagnard",mime_type="audio/x-mpegurl",mode="normal",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_campagnard.m3u")
playlist_campagnard = merge_tracks(id="merge_playlist_campagnard", playlist_campagnard)

playlist_campagnard2 = playlist(id="playlist_campagnard2",mime_type="audio/x-mpegurl",mode="normal",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_campagnard2.m3u")
playlist_campagnard2 = merge_tracks(id="merge_playlist_campagnard2", playlist_campagnard2)

playlist_country_anglo = playlist(id="playlist_country_anglo",mime_type="audio/x-mpegurl",mode="random",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_country_anglo.m3u")

playlist_retro_franco = playlist(id="playlist_retro_franco",mime_type="audio/x-mpegurl",mode="random",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_retro_franco.m3u")

playlist_jingles_animateurs = playlist(id="playlist_jingles_animateurs",mime_type="audio/x-mpegurl",mode="random",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_jingles_animateurs.m3u")
playlist_jingles_animateurs = drop_metadata(playlist_jingles_animateurs)

playlist_jingles_radio_id = playlist(id="playlist_jingles_radio_id",mime_type="audio/x-mpegurl",mode="random",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_jingles_radio_id.m3u")
playlist_jingles_radio_id = drop_metadata(playlist_jingles_radio_id)

playlist_jingles_speciales = playlist(id="playlist_jingles_speciales",mime_type="audio/x-mpegurl",mode="random",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_jingles_speciales.m3u")
playlist_jingles_speciales = drop_metadata(playlist_jingles_speciales)

playlist_retro_anglo = playlist(id="playlist_retro_anglo",mime_type="audio/x-mpegurl",mode="random",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_retro_anglo.m3u")

playlist_retro_qc = playlist(id="playlist_retro_qc",mime_type="audio/x-mpegurl",mode="random",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_retro_qc.m3u")

playlist_vedette = playlist(id="playlist_vedette",mime_type="audio/x-mpegurl",mode="randomize",reload_mode="watch","/var/azuracast/stations/radio_des_festivals/playlists/playlist_vedette.m3u")

# Standard Playlists
radio = random(id="standard_playlists", weights=[15], [playlist_country_franco])

# Once per x Minutes Playlists
radio = fallback(track_sensitive=true, [delay(900., playlist_country_anglo), radio])
radio = fallback(track_sensitive=true, [delay(1800., playlist_retro_franco), radio])
radio = fallback(track_sensitive=true, [delay(1200., playlist_jingles_animateurs), radio])
radio = fallback(track_sensitive=true, [delay(900., playlist_jingles_radio_id), radio])
radio = fallback(track_sensitive=true, [delay(2400., playlist_jingles_speciales), radio])
radio = fallback(track_sensitive=true, [delay(1500., playlist_retro_anglo), radio])
radio = fallback(track_sensitive=true, [delay(1080., playlist_retro_qc), radio])
radio = fallback(track_sensitive=true, [delay(3600., playlist_vedette), radio])

# Interrupting Schedule Switches

radio = switch(id="schedule_switch", track_sensitive=false, [ ({ (1w) and 18h0m-19h0m }, playlist_ouesteurne), ({ (2w) and 13h0m-16h0m }, playlist_vero), ({ (6w) and 12h0m-15h0m }, playlist_ouesteurne2), ({ (0w) and 18h0m-22h0m }, playlist_campagnard), ({ (1w) and 12h0m-15h0m }, playlist_campagnard2), ({true}, radio) ])

# AutoDJ Next Song Script
def autodj_next_song() =
    response = azuracast_api_call(
        "nextsong",
        ""
    )
    if (response == "") or (response == "false") then
        null()
    else
        request.create(response)
    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_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", timeout=20.0)
radio = fallback(id="requests_fallback", track_sensitive = true, [requests, radio])

interrupting_queue = request.queue(id="interrupting_requests", timeout=20.0)
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)

# Custom Configuration (Specified in Station Profile)
# radio = amplify(1.,override="replaygain_track_gain",radio)

# If a song has <song> <silence> <bonus track>,
# blank.skip() will skip to next song and NOT play <bonus track>.
# Defaults: max_blank=20.0 (seconds), threshold=-40.0 (dB)
radio = blank.skip(radio, max_blank=5.0, threshold=-60.0)

# Apply amplification metadata (if supplied)
radio = amplify(override="liq_amplify", 1., radio)

radio = ladspa.master_me(
    bypass = false,
    target = -18,
    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
)

# 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 = 8005, auth = dj_auth, icy = true, icy_metadata_charset = "ISO-8859-1", metadata_charset = "ISO-8859-1", on_connect = live_connected, on_disconnect = live_disconnected, buffer = 30.00, max = 35.00)

last_live_meta = ref([])

def insert_missing(m) =
    def updates =
        if m == [] then
            [("title", "Emission en direct"), ("is_live", "true")]
        else
            [("is_live", "true")]
        end
    end
    last_live_meta := [...m, ...list.assoc.remove("title", updates)]
    updates
end
live = metadata.map(insert_missing, live)

live = insert_metadata(live)
def insert_latest_live_metadata() =
  log("Inserting last live meta: #{last_live_meta()}")
  live.insert_metadata(last_live_meta())
end

def transition_to_live(_, s) =
  log("executing transition to live")
  insert_latest_live_metadata()
  s
end

def transition_to_radio(_, s) = s end

radio = fallback(
  id="live_fallback",
  track_sensitive=false,
  replay_metadata=true,
  transitions=[transition_to_live, transition_to_radio],
  [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)

# Allow for Telnet-driven insertion of custom metadata.
radio = server.insert_metadata(id="custom_metadata", 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"])

                # Only send some metadata to AzuraCast
                def fl(k, _) =
                    tags = ["song_id", "media_id", "playlist_id", "artist", "title"]
                    string.contains(prefix="liq_", k) or string.contains(prefix="replaygain_", k) or list.mem(k, tags)
                end

                feedback_meta = list.assoc.filter((fl), metadata.cover.remove(m))

                j = json()
                for item = list.iterator(feedback_meta) do
                    let (tag, value) = item
                    j.add(tag, value)
                end

                _ = azuracast_api_call(
                    "feedback",
                    json.stringify(compact=true, 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.shoutcast(%mp3(samplerate=44100, stereo=true, bitrate=128), id="local_1", host = "127.0.0.1", port = 8000, password = "(PASSWORD):#1", icy_id = 1, name = "Radio des Festivals", genre = "Country - Retro", url = "https://www.radiodesfestivals.ca", public = false, encoding = "ISO-8859-1", radio)
output.shoutcast(%mp3(samplerate=44100, stereo=true, bitrate=64), id="local_2", host = "127.0.0.1", port = 8000, password = "(PASSWORD):#2", icy_id = 2, name = "Radio des Festivals", genre = "Country - Retro", url = "https://www.radiodesfestivals.ca", public = true, encoding = "ISO-8859-1", radio)

# Remote Relays

Installation method

From official container image

Additional Info

LS Log;

024/09/09 17:44:56 [harbor:4] Request to update metadata for mount / on port 8005 2024/09/09 17:44:56 [lang:3] API auth - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/auth' with body: { "user": "XXX", "password": "YYYY" } 2024/09/09 17:44:57 [lang:3] API auth - Response (200): true 2024/09/09 17:44:57 [harbor:4] Client logged in. 2024/09/09 17:44:57 [input_streamer:3] New metadata chunk ? -- Honoré Godbout - Y'a trop d'amour autour de moi - Speedy en ondes. 2024/09/09 17:45:01 [lang:3] API feedback - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/feedback' with body: {"title":"Honoré Godbout - Y'a trop d'amour autour de moi - Speedy en ondes"} 2024/09/09 17:45:01 [server:4] New client unix socket "". 2024/09/09 17:45:01 [server:4] Client unix socket "" disconnected. 2024/09/09 17:45:01 [lang:3] API feedback - Response (200): true 2024/09/09 17:46:01 [server:4] New client unix socket "". 2024/09/09 17:46:01 [server:4] Client unix socket "" disconnected. 2024/09/09 17:47:01 [server:4] New client unix socket "". 2024/09/09 17:47:01 [server:4] Client unix socket "" disconnected. 2024/09/09 17:48:01 [server:4] New client unix socket "". 2024/09/09 17:48:01 [server:4] Client unix socket "" disconnected. 2024/09/09 17:48:38 [input_streamer:4] End of track

!! Delay in opening mike at end of song >> switch to Autodj immediately - no buffer left! !! 2024/09/09 17:48:38 [input_streamer:4] Buffer emptied, buffering needed.

Of course, it got eroded to zero 2024/09/09 17:48:38 [live_fallback:3] Switch to ladspa with forgetful transition. 2024/09/09 17:48:38 [source:4] Source replay_metadata.159 gets down. 2024/09/09 17:48:38 [source:4] Source replay_metadata.160 gets up with content type: {audio=pcm(stereo)}. 2024/09/09 17:48:38 [replay_metadata.160:3] Content type is {audio=pcm(stereo)}. 2024/09/09 17:48:38 [next_song:4] Remaining 0 requests 2024/09/09 17:48:38 [next_song:3] Prepared "/var/azuracast/stations/radio_des_festivals/media/Country Anglo/Freddy Fender - Sweet Summer Day.mp3" (RID 2687). 2024/09/09 17:48:38 [request.2687:4] Currently on air. 2024/09/09 17:48:38 [lang:3] API nextsong - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/nextsong' with body: 2024/09/09 17:48:38 [lang:3] API feedback - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/feedback' with body: {"song_id":"d867c90e776a3afebe1b8e122fb38f2a","artist":"Dani","title":"Un p'tit boy c'est gentil (1970)","playlist_id":"17","mediaid":"19267"} 2024/09/09 17:48:38 [lang:3] API nextsong - Response (200): annotate:title="A.L L.A",artist="Éric Morel",duration="173.61",song_id="4b2f87ba02a7360a719db709764945cd",media_id="19024",playlist_id="21":media:Vedette/éricmorel-a.ll.a.mp3 2024/09/09 17:48:38 [request.2688:4] Pushed ["annotate:title="A.L L.A",artist="Éric Morel",duration="173.61",song_id="4b2f87ba02a7360a719db709764945cd",media_id="19024",playlist_id="21":media:Vedette/éric_morel-a.l_l.a.mp3";...]. 2024/09/09 17:48:38 [request.2688:4] Resolving "annotate:title="A.L _ L.A",artist="Éric Morel",duration="173.61",song_id="4b2f87ba02a7360a719db709764945cd",media_id="19024",playlist_id="21":media:Vedette/éricmorel-a.ll.a.mp3" (timeout 20s)... 2024/09/09 17:48:38 [request.2688:4] Pushed ["media:Vedette/éric_morel-a.l_l.a.mp3";...]. 2024/09/09 17:48:38 [request.2688:4] Resolving "media:Vedette/éricmorel-a.ll.a.mp3" (timeout 20s)... 2024/09/09 17:48:38 [request.2688:4] Pushed ["/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.l_l.a.mp3";...]. 2024/09/09 17:48:38 [decoder.video.metadata:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éricmorel-a.ll.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder.ogg.metadata:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.l_l.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder.image.metadata:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éricmorel-a.ll.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder.id3:4] Unsupported file extension for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.l_l.a.mp3"! 2024/09/09 17:48:38 [decoder.id3:4] Unsupported file extension for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éricmorel-a.ll.a.mp3"! 2024/09/09 17:48:38 [decoder.id3:4] Unsupported file extension for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.l_l.a.mp3"! 2024/09/09 17:48:38 [decoder.flac.metadata:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éricmorel-a.ll.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [metadata.mp4:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.l_l.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder.ffmpeg:4] Unsupported file extension for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éricmorel-a.ll.a.mp3"! 2024/09/09 17:48:38 [metadata.flac:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.l_l.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder.ogg:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éricmorel-a.ll.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder.taglib:4] Unsupported file extension for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.l_l.a.mp3"! 2024/09/09 17:48:38 [decoder.ogg:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éricmorel-a.ll.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder.ffmpeg:4] Unsupported file extension for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.l_l.a.mp3"! 2024/09/09 17:48:38 [decoder.mad:4] Unsupported file extension for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éricmorel-a.ll.a.mp3"! 2024/09/09 17:48:38 [decoder.flac:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.l_l.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder.mp4:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éricmorel-a.ll.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder.aac:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.l_l.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder.midi:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éricmorel-a.ll.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder.srt:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.l_l.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder.aiff:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éricmorel-a.ll.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder.wav:4] Unsupported MIME type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.l_l.a.mp3": audio/mpeg! 2024/09/09 17:48:38 [decoder:4] Available decoders: ffmpeg (priority: 10), mad (priority: 1), image (priority: 1) 2024/09/09 17:48:38 [decoder:4] Trying decoder "ffmpeg" 2024/09/09 17:48:38 [decoder.ffmpeg:3] Requested content-type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éricmorel-a.ll.a.mp3": {audio=pcm(stereo)} 2024/09/09 17:48:38 [decoder.ffmpeg:3] FFmpeg recognizes "/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.l_l.a.mp3" as audio: {codec: mp3, 44100Hz, 2 channel(s)} 2024/09/09 17:48:38 [decoder.ffmpeg:3] Decoded content-type for "/var/azuracast/stations/radio_des_festivals/media/Vedette/éricmorel-a.ll.a.mp3": {audio=pcm(stereo)} 2024/09/09 17:48:38 [decoder:4] Selected decoder ffmpeg for file "/var/azuracast/stations/radio_des_festivals/media/Vedette/éric_morel-a.ll.a.mp3" with expected kind {audio=pcm(stereo)} and detected content {audio=pcm(stereo)} 2024/09/09 17:48:38 [next_song:4] Queued 1 requests 2024/09/09 17:48:38 [lang:3] API feedback - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/feedback' with body: {"song_id":"facd4f25e9873385dc3cc1564a8713ff","artist":"Freddy Fender","title":"Sweet Summer Day","playlist_id":"14","media_id":"20351"} 2024/09/09 17:48:38 [lang:3] API feedback - Response (200): true 2024/09/09 17:48:39 [lang:3] API feedback - Response (200): true 2024/09/09 17:49:01 [server:4] New client unix socket "". 2024/09/09 17:49:01 [server:4] Client unix socket "" disconnected.

!! Return to live feed after 3 seconds of streamer silence - not even long enough for encoder to disconnect! Returns with metadata from previous song ? Shouldn't it display XXX is live? Returns to normal operations at end of voice segment with buffer restored to 30 seconds !! 2024/09/09 17:49:11 [live_fallback:3] Switch to insert_metadata with transition. 2024/09/09 17:49:11 [source:4] Source replay_metadata.160 gets down. 2024/09/09 17:49:11 [lang:3] executing transition to live 2024/09/09 17:49:11 [lang:3] Inserting last live meta: [("pass", "XXXX:YYYY"), ("metadata_url", "https://radiodesfestivals.ca"), ("title", "Honoré Godbout - Y'a trop d'amour autour de moi - Speedy en ondes"), ("is_live", "true")] 2024/09/09 17:49:11 [source:4] Source replay_metadata.161 gets up with content type: {audio=pcm(stereo)}. 2024/09/09 17:49:11 [replay_metadata.161:3] Content type is {audio=pcm(stereo)}. 2024/09/09 17:49:11 [next_song:4] Finished with "/var/azuracast/stations/radio_des_festivals/media/Country Anglo/Freddy Fender - Sweet Summer Day.mp3". 2024/09/09 17:49:11 [request.2687:4] Request finished. 2024/09/09 17:49:11 [lang:3] API feedback - Sending POST request to 'http://127.0.0.1:6010/api/internal/1/liquidsoap/feedback' with body: {"title":"Honoré Godbout - Y'a trop d'amour autour de moi - Speedy en ondes"} 2024/09/09 17:49:11 [lang:3] API feedback - Response (200): true 2024/09/09 17:50:01 [server:4] New client unix socket "". 2024/09/09 17:50:01 [server:4] Client unix socket "" disconnected.

vitoyucepi commented 2 months ago

Hi @SpeedyCharly,

Let me retell your story.

  1. The DJ connects to a liquidsoap mount point using the client, like butt or Mixx.
  2. Liquidsoap buffers 30 seconds of the incoming stream before switching from autodj to live.
  3. The client disconnects from the server for a short time, which is less than the buffer size.
  4. After reconnecting to the mount point, the client will continue to send data, but the buffer size decreases.
  5. After several disconnections, the buffer becomes completely depleted.
  6. When the buffer becomes empty, Liquidsoap switches from live to autodj.
  7. On the next connect, Liquidsoap buffers 30 seconds of the incoming stream before switching from autodj to live.

Am I correct?

SpeedyCharly commented 2 months ago

@vitoyucepi

Sounds about right save for points 3 & 4 ; client doesn't actually disconnect from the server - it's a simple case of a bit of dead air when DJ is slow to open mike at end of song or some such... We're running Sam Broadcaster and it doesn't actually disconnect event when AutoDj kicks in when the buffer is depleted. Normal operations, with buffer reset, resumes as soon as operator resumes streaming... We don't need to restart the encoder unless we exceed the app's timeout.

vitoyucepi commented 2 months ago

Hi @SpeedyCharly, I still don't understand the concept of dead air, especially I'd like to understand how it works from a data transfer perspective. Maybe you can record a demo track and annotate it? By the way, can you run a standalone liquidsoap server with a custom script?

SpeedyCharly commented 2 months ago

Dead air is typically silence in the delay time between the end of a song and the dj turning on his mike, or any other delay between operations. The client is still connected but nothing is being transmitted to the server. Which is the main reason we have a buffer to begin with, or else we'd be disconnected all the time.

And no, I don't have the space on my server to run anything else but my Azuracast installation... radio on a shoestring here...

SpeedyCharly commented 1 month ago

Here's how I figure this is happening; • Buffer is initially set at 30 secs. [on my station] • Dead air occurs [Client connected but no data stream] > LS starts to count down from 30 while buffer empties. • Let's say client resumes sending after 10 seconds > LS will buffer for the remaining 20 secs. then resume streaming [no gap at listener end] New buffer length is now 20 secs. • More dead air will result in further erosion of the buffer time until it reaches 0 where AutoDj will kick in with a song from playlist. • If stream resumes quickly, the client won't have time to disconnect and LS will buffer again for the full 30 seconds before outputting the stream. • Longer gaps will disconnect the client which will have to be reset to return to air...

Am I wrong anywhere on this?

vitoyucepi commented 1 month ago

Hi @SpeedyCharly, I feel like this is an expected way of functioning. As for your problem, I'm theorizing about the behavior similar to the Twitch disconnect protection screen that displays a placeholder image when the streamer is not connected before going offline.