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.42k stars 130 forks source link

Initial Streamer Metadata Not Sent to Output (Icecast/SHOUTcast) #2109

Closed BusterNeece closed 2 years ago

BusterNeece commented 2 years ago

Hello all!

This has been a long-reported issue, basically for as long as I've been using Liquidsoap, but I looked and couldn't immediately find an existing issue documenting what happens, so I wanted to "resurrect" this issue, so to speak, and see if maybe there's a novel way we could solve it in Liquidsoap 2.0.

Basically, the problem is this: when a live DJ connects to an input.harbor, whatever metadata their stream is using right when the stream connects doesn't get transmitted "downstream" as a metadata update on any Icecast/SHOUTcast outputs.

It's only when they first submit new metadata, thus triggering a change in metadata from their initial state, that it propagates this properly as a metadata change.

I recall previous discussions suggesting that perhaps this was a known issue in how Liquidsoap implements the protocols that allow DJs to connect directly to Liquidsoap as opposed to Icecast/SHOUTcast, but if there was an available solution for this, it would significantly benefit our support burden and render a much smoother experience for listeners.

Burchelwnd commented 2 years ago

I have the same problem. No streamer metadata is being sent. just the fixed name of the radio streamer misc. this is how it looks on the player

toots commented 2 years ago

I have just fixed the issue reported here: https://github.com/AzuraCast/AzuraCast/issues/4825. This might also have fixed this one. Any chance someone might be able to test the issue with the latest v2.0.2-preview? Thanks!

BusterNeece commented 2 years ago

@toots Playing with this locally, and we've made some progress on this, but we have another issue at play: Now I'm seeing "new metadata chunk" with the initial metadata for the DJ stream (very good!), but it's not actually sending that metadata update through to Icecast/SHOUTcast, so they're still showing outdated metadata.

toots commented 2 years ago

Okay. Do you have a script example that I can look at?

BusterNeece commented 2 years ago

@toots At some point you should just install AzuraCast ;) That's always the script I'm using!

BusterNeece commented 2 years ago

Here's the current Rolling Release auto-generated version of the AzuraCast liquidsoap script. Currently unmodified from what's deployed live:

# 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/azuratest_radio/config/liquidsoap.pid")
log.stdout.set(true)
log.file.set(false)
settings.server.log.level.set(4)
settings.server.telnet.set(true)
settings.server.telnet.bind_addr.set("0.0.0.0")
settings.server.telnet.port.set(8004)
settings.harbor.bind_addrs.set(["0.0.0.0"])

settings.tag.encodings.set(["UTF-8","ISO-8859-1"])
settings.encoder.metadata.export.set(["artist","title","album","song"])

settings.decoder.priorities.ogg.set(15)
settings.decoder.priorities.mad.set(15)
settings.decoder.priorities.flac.set(15)
settings.decoder.priorities.aac.set(15)
settings.decoder.priorities.ffmpeg.set(10)

setenv("TZ", "UTC")

azuracast_api_auth = ref("(PASSWORD)")
ignore(azuracast_api_auth)

autodj_is_loading = ref(true)
ignore(autodj_is_loading)

autodj_ping_attempts = ref(0)
ignore(autodj_ping_attempts)

playlist_default = playlist(id="playlist_default",mime_type="audio/x-mpegurl",mode="randomize",reload_mode="watch","/var/azuracast/stations/azuratest_radio/playlists/playlist_default.m3u")
playlist_default = cue_cut(id="cue_playlist_default", playlist_default)

playlist_test_schedule_thing = playlist(id="playlist_test_schedule_thing",mime_type="audio/x-mpegurl",mode="randomize",reload_mode="watch","/var/azuracast/stations/azuratest_radio/playlists/playlist_test_schedule_thing.m3u")
playlist_test_schedule_thing = cue_cut(id="cue_playlist_test_schedule_thing", playlist_test_schedule_thing)

# Standard Playlists
radio = random(id="standard_playlists", weights=[3], [playlist_default])

# Standard Schedule Switches

radio = switch(id="schedule_switch", track_sensitive=true, [ ({ 10h0m-22h0m }, playlist_test_schedule_thing), ({true}, radio) ])

# AutoDJ Next Song Script
def autodj_next_song() =
    log("autodj_next_song: Sending AzuraCast API Call...")
    uri = list.hd(process.read.lines(env=[("API_AUTH", !azuracast_api_auth)], 'curl -s --request POST --url http://web/api/internal/1522/nextsong --form api_auth="$API_AUTH"', timeout=10.), default="")
    log("autodj_next_song: AzuraCast API Response: #{uri}")

    if uri == "" or string.match(pattern="Error", uri) then
        null()
    else
        r = request.create(uri)
        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 := !autodj_ping_attempts + 1
    delay = ref(0.5)

    if source.is_ready(autodj) then
        log("AutoDJ is ready!")

        autodj_is_loading := false
        delay := -1.0
    elsif !autodj_ping_attempts > 200 then
        log("AutoDJ could not be initialized within the specified timeout.")

        autodj_is_loading := false
        delay := -1.0
    end

    !delay
end

dynamic = request.dynamic(id="next_song", timeout=20., retry_delay=2., 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])
add_skip_command(radio)

radio = crossfade(smart=false, duration=3.00, fade_out=2.00, fade_in=2.00, radio)

# DJ Authentication
live_enabled = ref(false)
last_authenticated_dj = ref("")
live_dj = ref("")

live_record_path = ref("")

def dj_auth(login) =
    user = ref("")
    password = ref("")

    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

    log("dj_auth: Sending AzuraCast API DJ Auth command for user: #{!user}")
    ret = list.hd(process.read.lines(env=[("DJ_USER", !user), ("DJ_PASSWORD", !password), ("API_AUTH", !azuracast_api_auth)], 'curl -s --request POST --url http://web/api/internal/1522/auth --form dj-user="$DJ_USER" --form dj-password="$DJ_PASSWORD" --form api_auth="$API_AUTH"', timeout=10.), default="")
    log("dj_auth: AzuraCast API Response: #{ret}")

    authed = bool_of_string(ret)
    if (authed) then
        last_authenticated_dj := !user
    end

    authed
end

def live_connected(header) =
    dj = !last_authenticated_dj
    log("DJ Source connected! Last authenticated DJ: #{dj} - #{header}")

    live_enabled := true
    live_dj := dj

    log("live_connected: Sending AzuraCast API DJ onConnect command...")
    ret = list.hd(process.read.lines(env=[("DJ_USER", dj), ("API_AUTH", !azuracast_api_auth)], 'curl -s --request POST --url http://web/api/internal/1522/djon --form dj-user="$DJ_USER" --form api_auth="$API_AUTH"', timeout=10.), default="")
    log("live_connected: AzuraCast API Response: #{ret}")

    if (string.contains(prefix="/", ret)) then
        live_record_path := ret
    end
end

def live_disconnected() =
    dj = !live_dj

    log("DJ Source disconnected! Current live DJ: #{dj}")

    log("live_disconnected: Sending AzuraCast API DJ onDisconnect command...")
    ret = list.hd(process.read.lines(env=[("DJ_USER", dj), ("API_AUTH", !azuracast_api_auth)], 'curl -s --request POST --url http://web/api/internal/1522/djoff --form dj-user="$DJ_USER" --form api_auth="$API_AUTH"', timeout=10.), default="")
    log("live_disconnected: AzuraCast API Response: #{ret}")

    live_enabled := false
    last_authenticated_dj := ""
    live_dj := ""

    live_record_path := ""
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 = "UTF-8", metadata_charset = "UTF-8", on_connect = live_connected, on_disconnect = live_disconnected, buffer = 5.00, max = 10.00)
ignore(output.dummy(live, fallible=true))

radio = fallback(id="live_fallback", replay_metadata=false, track_sensitive=false, [live, radio])

# Allow for Telnet-driven insertion of custom metadata.
radio = server.insert_metadata(id="custom_metadata", radio)

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

radio = fallback(id="safe_fallback", track_sensitive = false, [radio, single(id="error_jingle", "/usr/local/share/icecast/web/error.mp3")])

# Send metadata changes back to AzuraCast
def metadata_updated(m) =
    def f() =
        if (m["song_id"] != "") then
            ret = list.hd(process.read.lines(env=[("SONG", m["song_id"]), ("MEDIA", m["media_id"]), ("PLAYLIST", m["playlist_id"]), ("API_AUTH", !azuracast_api_auth)], 'curl -s --request POST --url http://web/api/internal/1522/feedback --form song="$SONG" --form media="$MEDIA" --form playlist="$PLAYLIST" --form api_auth="$API_AUTH"', timeout=10.), default="")
            log("AzuraCast Feedback Response: #{ret}")
        end
        (-1.)
    end

    thread.run.recurrent(fast=false, delay=0., f)
end

radio.on_metadata(metadata_updated)

# Local Broadcasts
output.icecast(%mp3(samplerate=44100, stereo=true, bitrate=128, id3v2=true), id="local_1", host = "127.0.0.1", port = 8000, password = "(PASSWORD)", mount = "/radio.mp3", name = "AzuraTest Radio", description = "A test radio station.", genre = "", public = false, encoding = "UTF-8", radio)
output.icecast(%mp3(samplerate=44100, stereo=true, bitrate=64, id3v2=true), id="local_2", host = "127.0.0.1", port = 8000, password = "(PASSWORD)", mount = "/mobile.mp3", name = "AzuraTest Radio", description = "A test radio station.", genre = "", public = false, encoding = "UTF-8", radio)

# Remote Relays
toots commented 2 years ago

Ok thanks. Any chance you could do like we did in https://github.com/AzuraCast/AzuraCast/issues/4825 and add a printout after each operator track where the metadata is being lost?

BusterNeece commented 2 years ago

@toots Is it possible that replay_metadata=false on the live stream fallback might be contributing to the problem?

BusterNeece commented 2 years ago

@toots Tests are showing it's not impacting it...I'm seeing this log entry:

[input_streamer:3] New metadata chunk ? -- &I ft. Chichi - Into the Green.

upon the initial DJ connection, which indicates the first chunk is successfully indicating a metadata update, but there is never a call to the on_metadata listener further down the configuration, so I'm not sure it's even registering as a metadata change somehow.

This is true regardless of the replay_metadata setting.

toots commented 2 years ago

Yeah I see it. However, it'd be nice to also have a printout right after the operator is defined and at every step, this'll really help debugging.

BusterNeece commented 2 years ago

@toots I'm not sure what you mean in this case. How am I supposed to log that an event isn't propagating up to another stream? Do I need to be logging a specific metadata value and seeing where it disappears? I don't know the syntax enough to know how to do that.

toots commented 2 years ago
def on_meta(n,s) =
  def fn(m) =
    print("Got metadata for #{n}: #{m}")
  end
  s.on_metadata(fn)
end

# Live Broadcasting
live = input.harbor("/", id = "input_streamer", port = 8005, 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)
on_meta("live", live)

# This is not needed anymore:
ignore(output.dummy(live, fallible=true))

radio = fallback(id="live_fallback", replay_metadata=false, track_sensitive=false, [live, radio])
on_meta("fallback", radio)

# Allow for Telnet-driven insertion of custom metadata.
radio = server.insert_metadata(id="custom_metadata", radio)
on_meta("server.insert_metadata", radio)

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

radio = fallback(id="safe_fallback", track_sensitive = false, [radio, single(id="error_jingle", "/usr/local/share/icecast/web/error.mp3")])
on_meta("fallback 2", radio)

# Send metadata changes back to AzuraCast
def metadata_updated(m) =
    def f() =
        if (m["song_id"] != "") then
            ret = list.hd(process.read.lines(env=[("SONG", m["song_id"]), ("MEDIA", m["media_id"]), ("PLAYLIST", m["playlist_id"]), ("API_AUTH", !azuracast_api_auth)], 'curl -s --request POST --url http://web/api/internal/1522/feedback --form song="$SONG" --form media="$MEDIA" --form playlist="$PLAYLIST" --form api_auth="$API_AUTH"', timeout=10.), default="")
            log("AzuraCast Feedback Response: #{ret}")
        end
        (-1.)
    end

    thread.run.recurrent(fast=false, delay=0., f)
end

radio.on_metadata(metadata_updated)

# Local Broadcasts
output.icecast(%mp3(samplerate=44100, stereo=true, bitrate=128, id3v2=true), id="local_1", host = "127.0.0.1", port = 8000, password = "(PASSWORD)", mount = "/radio.mp3", name = "AzuraTest Radio", description = "A test radio station.", genre = "", public = false, encoding = "UTF-8", radio)
output.icecast(%mp3(samplerate=44100, stereo=true, bitrate=64, id3v2=true), id="local_2", host = "127.0.0.1", port = 8000, password = "(PASSWORD)", mount = "/mobile.mp3", name = "AzuraTest Radio", description = "A test radio station.", genre = "", public = false, encoding = "UTF-8", radio)
BusterNeece commented 2 years ago

@toots Added all that; it doesn't even make it to the first one. It logs "new metadata chunk", and propagation stops there; it doesn't even hit the very next down the list.

toots commented 2 years ago

Can you remove the output.dummy using the live source?

toots commented 2 years ago

And also post the log? Thanks!

BusterNeece commented 2 years ago

@toots It has no impact.

2021/12/25 21:58:15 [lang:3] dj_auth: Sending AzuraCast API DJ Auth command for user: test
2021/12/25 21:58:15 [lang:3] dj_auth: AzuraCast API Response: true
2021/12/25 21:58:15 [lang:3] DJ Source connected! Last authenticated DJ: test - [("Authorization", "Basic dGVzdDp0ZXN0"), ("Host", "localhost:8005"), ("User-Agent", "libshout/2.4.1"), ("Content-Type", "audio/mpeg"), ("ice-public", "0"), ("ice-name", "Mixxx"), ("ice-description", "This stream is online for testing purposes!"), ("ice-genre", "Live Mix"), ("ice-url", "https://www.mixxx.org"), ("ice-irc", " "), ("ice-aim", " "), ("ice-icq", " "), ("ice-audio-info", "bitrate=128")]
2021/12/25 21:58:15 [lang:3] live_connected: Sending AzuraCast API DJ onConnect command...
2021/12/25 21:58:15 [lang:3] live_connected: AzuraCast API Response: true
2021/12/25 21:58:15 [input_streamer:3] Decoding...
2021/12/25 21:58:15 [lang:3] dj_auth: Sending AzuraCast API DJ Auth command for user: test
2021/12/25 21:58:16 [lang:3] dj_auth: AzuraCast API Response: true
2021/12/25 21:58:16 [input_streamer:3] New metadata chunk ? -- Pascal Michael Stiefel - Alpine Skyline Bonus Track.
2021/12/25 21:58:20 [live_fallback:3] Switch to input_streamer with transition.
2021/12/25 21:58:20 [server:3] Server command input_streamer.stop already registered! Previous definition replaced.
2021/12/25 21:58:20 [server:3] Server command input_streamer.status already registered! Previous definition replaced.
2021/12/25 21:58:20 [server:3] Server command input_streamer.buffer_length already registered! Previous definition replaced.
2021/12/25 21:59:33 [input_streamer:2] Feeding stopped: Mad.End_of_stream.
2021/12/25 21:59:33 [lang:3] DJ Source disconnected! Current live DJ: test
2021/12/25 21:59:33 [lang:3] live_disconnected: Sending AzuraCast API DJ onDisconnect command...
2021/12/25 21:59:33 [lang:3] live_disconnected: AzuraCast API Response: true
2021/12/25 21:59:38 [live_fallback:3] Switch to crossfade_0 with transition.

I can also verify that it is correctly logging metadata coming from the fallback when it's playing non-live broadcasts...I just can't paste that here because it tries to actually log the entire contents of the APIC ID3 data tag and that breaks everything.

toots commented 2 years ago

Could you try with a minimal script? This works locally:

s = input.harbor("test")
s.on_metadata(fun (m) -> print("Got meta: #{m}"))

output.dummy(fallible=true,s)
BusterNeece commented 2 years ago

@toots Unfortunately, it's not practical with our setup to test arbitrary scripts. We're running commit b6f2af0a.

toots commented 2 years ago

That fix was bad. I just pushed one that should be the definite one. Mind testing with the latest v2.0.2-preview? Thks

toots commented 2 years ago

Commit: e36d7fc53eff2e8116e44257a53db6821989e1c8

BusterNeece commented 2 years ago

@toots Just rebuilt with the latest commit and it yields the same results. Metadata updates, including the first one when a DJ connects, are being picked up as "new metadata chunks" in the logs, but it's not pushing that metadata "up" any further past that first "chunk detected" log entry:

2021/12/25 23:14:29 [lang:3] dj_auth: AzuraCast API Response: true
2021/12/25 23:14:29 [lang:3] DJ Source connected! Last authenticated DJ: test - [("Authorization", "Basic dGVzdDp0ZXN0"), ("Host", "localhost:8005"), ("User-Agent", "libshout/2.4.1"), ("Content-Type", "audio/mpeg"), ("ice-public", "0"), ("ice-name", "Mixxx"), ("ice-description", "This stream is online for testing purposes!"), ("ice-genre", "Live Mix"), ("ice-url", "https://www.mixxx.org"), ("ice-irc", " "), ("ice-aim", " "), ("ice-icq", " "), ("ice-audio-info", "bitrate=128")]
2021/12/25 23:14:29 [lang:3] live_connected: Sending AzuraCast API DJ onConnect command...
2021/12/25 23:14:29 [lang:3] live_connected: AzuraCast API Response: true
2021/12/25 23:14:29 [input_streamer:3] Decoding...
2021/12/25 23:14:30 [lang:3] dj_auth: Sending AzuraCast API DJ Auth command for user: test
2021/12/25 23:14:30 [lang:3] dj_auth: AzuraCast API Response: true
2021/12/25 23:14:30 [input_streamer:3] New metadata chunk ? -- Pascal Michael Stiefel - Sand 'n Sails Village.
2021/12/25 23:14:35 [live_fallback:3] Switch to input_streamer with transition.
2021/12/25 23:14:35 [server:3] Server command input_streamer.stop already registered! Previous definition replaced.
2021/12/25 23:14:35 [server:3] Server command input_streamer.status already registered! Previous definition replaced.
2021/12/25 23:14:35 [server:3] Server command input_streamer.buffer_length already registered! Previous definition replaced.
2021/12/25 23:15:05 [lang:3] dj_auth: Sending AzuraCast API DJ Auth command for user: test
2021/12/25 23:15:05 [lang:3] dj_auth: AzuraCast API Response: true
2021/12/25 23:15:05 [input_streamer:3] New metadata chunk ? -- Pascal Michael Stiefel - The Birdhouse Peak Indoor Variation.
2021/12/25 23:15:25 [lang:3] dj_auth: Sending AzuraCast API DJ Auth command for user: test
2021/12/25 23:15:25 [lang:3] dj_auth: AzuraCast API Response: true
2021/12/25 23:15:25 [input_streamer:3] New metadata chunk ? -- Patawawa - Red and White.
2021/12/25 23:15:42 [input_streamer:2] Feeding stopped: Mad.End_of_stream.
2021/12/25 23:15:42 [lang:3] DJ Source disconnected! Current live DJ: test
2021/12/25 23:15:42 [lang:3] live_disconnected: Sending AzuraCast API DJ onDisconnect command...
2021/12/25 23:15:42 [lang:3] live_disconnected: AzuraCast API Response: true

Tested with both Mixxx configured to connect to Icecast 2 and the WebDJ, same exact result both times.

Note that the WebDJ seems to report slightly different metadata format than Mixxx (Mixxx is above, WebDJ is below):

2021/12/25 23:17:42 [input_streamer:3] New metadata chunk 4EverfreeBrony -- Here On The Moon (ft. Faux Synder & Emily Jones).
2021/12/25 23:17:46 [live_fallback:3] Switch to input_streamer with transition.
2021/12/25 23:17:46 [server:3] Server command input_streamer.stop already registered! Previous definition replaced.
2021/12/25 23:17:46 [server:3] Server command input_streamer.status already registered! Previous definition replaced.
2021/12/25 23:17:46 [server:3] Server command input_streamer.buffer_length already registered! Previous definition replaced.
2021/12/25 23:18:08 [input_streamer:3] New metadata chunk Aftermath -- Pull Me Through (ft. LilyCloud).

It's missing the ? -- at the front. Not sure if that matters, but both are failing past that anyway.

toots commented 2 years ago

Has the issue changed? The initial report said that subsequent metadata updates worked but I don't see them printed as well here.

BusterNeece commented 2 years ago

@toots Yes, at some point we have a regression here, because now even subsequent metadata isn't being sent over. This appears to have been introduced in the last few versions of the software, and we're getting sporadic other reports of it as well.

toots commented 2 years ago

I can confirm the original issue on v2.0.2-preview but the regression seems absent there. It is present in main but main is in a state of flux so, for now, it shouldn't be used for production purposes.

I'm thinking about a fix. It is a design limitation. With the current design, things are working as intended: the input.harbor is an active source so its data is continuously consumed. This is important to make sure we don't run into memory leaks, and the reason why you had a output.dummy under it. I believe in previous versions, the operator might not have been active.

However, this means that all metadata passed before the operator is actually used are also immediately consumed and lost. This is not a desirable effect in most cases so I'm gonna see if I can come up with a reasonable fix. This is a similar situation than what the replay_metadata option is doing with source switches.

BusterNeece commented 2 years ago

@toots Sorry, it appears we were testing with a commit based on the master branch, not on the 2.0.2-preview branch. That was causing the full regression in my tests.

Testing off the latest commit in the 2.0.2 preview branch, I have this result:

So yes, it appears the regression is a non-issue and we are indeed progressing toward resolving the issue.

I'm not sure what the difference between the master branch is and the current 2.0.2 preview branch that makes it so that the former picked up the initial metadata of the WebDJ while the latter doesn't, but if we can reconcile that difference, everything else is working as expected.

toots commented 2 years ago

This should be fixed in the latest v2.0.2-preview. Make sure to pass replay_metadata=true to your switches!

BusterNeece commented 2 years ago

@toots Not only did the latest commit not fix this issue, it actually rolled back any of the progress we had previously made; now neither the WebDJ or Mixxx are reporting their first metadata chunks (and the WebDJ isn't even logging that it received the chunk). It's only reporting subsequent metadata.

To confirm, I am on the 2.0.2-preview branch and I did enable the replay_metadata setting on the live fallback.

I can go back to the drawing board with the logs, but it'll take me a bit of time to put them back, as I had just removed them.

I understand this is as frustrating for you as it is for me, but we can't leave it at this state.

Update: Logging shows it's now making it through to the fallback, but no further, as in, the first on_meta call above ("live") now shows that first metadata push, but not the second one after the fallback, with or without replay_metadata.

I am also noting that replay_metadata=true triggers a double-push of the metadata when the stream initially goes live, which is likely not to cause any problems, but may have unexpected side effects.

toots commented 2 years ago

Well, that's why having a minimal reproduction script helps. I'm working with this one, which mimicks your script and definitely works here:

def on_meta(n,s) =
  def fn(m) =
    print("Got metadata for #{n}: #{m}")
  end
  s.on_metadata(fn)
end

files = playlist("/tmp/pl")
on_meta("playlist", files)

live = input.harbor("test")
on_meta("live", live)

ignore(output.dummy(live, fallible=true))

radio = fallback(id="live_fallback", replay_metadata=true, track_sensitive=false, [live, files])
on_meta("fallback", radio)

# Allow for Telnet-driven insertion of custom metadata.
radio = server.insert_metadata(id="custom_metadata", radio)
on_meta("server.insert_metadata", radio)

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

radio = fallback(id="safe_fallback", track_sensitive = false, [radio, single(id="error_jingle", "/usr/local/share/icecast/web/error.mp3")])
on_meta("fallback 2", radio)
toots commented 2 years ago

Liquidsoap 2.0.2+git@7bb1b395

toots commented 2 years ago

Fix confirmed for Mixxx. What's your link for WebDJ?

BusterNeece commented 2 years ago

@toots I'm playing with this locally, and if we add back the output.dummy line, it works as expected on Mixxx. Without it, the metadata is delayed in pushing, as if it's "one song behind" the actual metadata. We removed it as you had mentioned it wasn't needed anymore.

The WebDJ is our term for the Webcaster module integrated into AzuraCast.

toots commented 2 years ago

Without the output.dummy also works here:

2021/12/27 09:32:58 [input.harbor_0:3] New metadata chunk ? -- My Song.
Got metadata for live: []
Got metadata for live: [("song", "My Song"), ("title", "My Song")]
2021/12/27 09:33:00 [live_fallback:3] Switch to input.harbor_0 with transition.
2021/12/27 09:33:00 [source:4] Source replay_metadata_0 gets up with content kind: {audio=pcm,video=any,midi=any}.
2021/12/27 09:33:00 [replay_metadata_0:4] Content type is {audio=pcm(stereo),video=none,midi=none}.
Got metadata for fallback: [("song", "My Song"), ("title", "My Song")]
Got metadata for server.insert_metadata: [("song", "My Song"), ("title", "My Song")]
Got metadata for amplify: [("song", "My Song"), ("title", "My Song")]
Got metadata for fallback 2: [("song", "My Song"), ("title", "My Song")]
toots commented 2 years ago

I can see the double meta, too.

BusterNeece commented 2 years ago

Also, I'm not sure the WebDJ/Webcaster is ever sending that first meta chunk. If you change songs or something after going live, it sends that as a metadata change, but I don't think it sends the original. There may be a way for us to kinda hack it into the JavaScript as an after-connect thing, though.

BusterNeece commented 2 years ago

In playing with this, I also discovered the reason why replay_metadata was turned off all this time: if we have a DJ connecting where we don't get that initial metadata push, for whatever reason, Liquidsoap just replays the metadata of the previous live stream, which may have been hours or days prior. For us to have that setting enabled reliably, we'd have to be able to guarantee we'd always have "fresh" metadata on any new source connecting, and I'm still not seeing us having that reliably.

toots commented 2 years ago

Webcaster is a different mechanism, it uses websockets and transmits metadata in-band.

toots commented 2 years ago

In playing with this, I also discovered the reason why replay_metadata was turned off all this time: if we have a DJ connecting where we don't get that initial metadata push, for whatever reason, Liquidsoap just replays the metadata of the previous live stream, which may have been hours or days prior. For us to have that setting enabled reliably, we'd have to be able to guarantee we'd always have "fresh" metadata on any new source connecting, and I'm still not seeing us having that reliably.

You should use map_metadata for that:

live = input.harbor("test")

def insert_ missing(m) =
  if m == [] then
    [("title", "AzuraCast live DJ")]
  else
    m
  end
end

live = map_metadata(insert_missing, live)

This will reliably inserts an initial metadata whenever a DJ connects.

BusterNeece commented 2 years ago

Well, this morning it seems things are fixed in my local tests; now, with or without the output.dummy line, Mixxx's metadata is being pushed correctly, including the initial metadata push.

If we can figure out why the Webcaster isn't sending that first metadata push on-connect, we should be good.

Sorry about the back and forth here, been ripping my hair out on this one :P

toots commented 2 years ago

No pb. I'm not sure how you're testing the metadata update but on the demo version at: https://webcast.github.io/webcaster/ this is a manual method and I don't think it was ever written to send an intial metadata:

Screen Shot 2021-12-27 at 9 46 25 AM
2021/12/27 09:46:22 [input.harbor_0:3] New metadata chunk Test -- Bla.
Got metadata for live: [("artist", "Test"), ("title", "Bla")]
Got metadata for fallback: [("artist", "Test"), ("title", "Bla")]
Got metadata for server.insert_metadata: [("artist", "Test"), ("title", "Bla")]
Got metadata for amplify: [("artist", "Test"), ("title", "Bla")]
Got metadata for fallback 2: [("artist", "Test"), ("title", "Bla")]
BusterNeece commented 2 years ago

@toots You are correct, there was nothing in there to send the initial metadata on-connect if it was already set when you go live. I've added that, as well as the "missing metadata detector" to handle noncompliant broadcast tools.

We should be good now.

Thank you again for all your help. :)

toots commented 2 years ago

Awesome. I'll be pushing the new release very soon!