Implement Liquidsoap 2.2.x Rolling Release (#6249)

* Update paths for fallbacks in utility scripts.

* Move LS util files up.

* Update installed LS version.

* Initial LS config changes for 2.2.x.

* Fix Shoutcast on 2.2.x.

* Update changelog.

* Replace deprecated LS operators & fix a warning (#6246)

* Update Liquidsoap 2.2.x rolling release.

* Don't send empty annotations.

* Update for Enums.
This commit is contained in:
Buster Neece 2023-06-05 07:18:50 -05:00 committed by GitHub
parent 437ca77ead
commit 35a6c8c014
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 134 additions and 86 deletions

View File

@ -3,6 +3,20 @@
These changes have not yet been incorporated into a stable release, but if you are on the latest version of the rolling
release channel, you can take advantage of these new features and fixes.
## New Features/Changes
- **Update to Liquidsoap 2.2.x**: We're updating to the latest version of Liquidsoap, which includes many bug fixes,
performance improvements and other changes. We have adopted our syntax to match Liquidsoap's new supported syntax, but
if you use custom Liquidsoap code, you will need to update your code accordingly. You can see the most important
changes in this [migration guide](https://www.liquidsoap.info/doc-dev/migrating.html#from-2.1.x-to-2.2.x). The most
common changes you will need to make are to mutable (`ref()`) variables:
- `!var` becomes `var()`
- `var := value` optionally becomes `var.set(value)`
## Code Quality/Technical Changes
## Bug Fixes
---
# AzuraCast 0.18.3 (Jun 5, 2023)

View File

@ -31,4 +31,6 @@ interface StationMountInterface
public function getAutodjAdapterType(): AdapterTypeInterface;
public function getIsPublic(): bool;
public function getIsShoutcast(): bool;
}

View File

@ -413,6 +413,14 @@ class StationMount implements
return $this->getStation()->getFrontendType();
}
public function getIsShoutcast(): bool
{
return match ($this->getAutodjAdapterType()) {
FrontendAdapters::Shoutcast => true,
default => false
};
}
/**
* Retrieve the API version of the object/array.
*

View File

@ -11,7 +11,6 @@ use App\Radio\Enums\StreamProtocols;
use App\Radio\Remote\AbstractRemote;
use App\Utilities;
use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException;
use Psr\Http\Message\UriInterface;
use Stringable;
@ -376,6 +375,14 @@ class StationRemote implements
return (RemoteAdapters::AzuraRelay !== $this->getType());
}
public function getIsShoutcast(): bool
{
return match ($this->getAutodjAdapterType()) {
RemoteAdapters::Shoutcast1, RemoteAdapters::Shoutcast2 => true,
default => false,
};
}
/**
* Retrieve the API version of the object/array.
*

View File

@ -83,44 +83,56 @@ final class Annotations implements EventSubscriberInterface
return;
}
$backendConfig = $station->getBackendConfig();
$annotations = [];
$annotationsRaw = [
$annotationsRaw = array_filter([
'title' => $media->getTitle(),
'artist' => $media->getArtist(),
'duration' => $media->getLength(),
'song_id' => $media->getSongId(),
'media_id' => $media->getId(),
'liq_amplify' => $media->getAmplify() ?? 0.0,
'liq_cross_duration' => $media->getFadeOverlap() ?? $backendConfig->getCrossfadeDuration(),
'liq_fade_in' => $media->getFadeIn() ?? $backendConfig->getCrossfade(),
'liq_fade_out' => $media->getFadeOut() ?? $backendConfig->getCrossfade(),
'liq_amplify' => $media->getAmplify(),
'liq_cross_duration' => $media->getFadeOverlap(),
'liq_fade_in' => $media->getFadeIn(),
'liq_fade_out' => $media->getFadeOut(),
'liq_cue_in' => $media->getCueIn(),
'liq_cue_out' => $media->getCueOut(),
];
]);
// Safety checks for cue lengths.
if ($annotationsRaw['liq_cue_out'] < 0) {
if (
isset($annotationsRaw['liq_cue_out'])
&& $annotationsRaw['liq_cue_out'] < 0
) {
$cue_out = abs($annotationsRaw['liq_cue_out']);
if (0.0 === $cue_out || $cue_out > $annotationsRaw['duration']) {
$annotationsRaw['liq_cue_out'] = null;
} else {
$annotationsRaw['liq_cue_out'] = max(0, $annotationsRaw['duration'] - $cue_out);
if (0.0 === $cue_out) {
unset($annotationsRaw['liq_cue_out']);
}
if (isset($annotationsRaw['duration'])) {
if ($cue_out > $annotationsRaw['duration']) {
unset($annotationsRaw['liq_cue_out']);
} else {
$annotationsRaw['liq_cue_out'] = max(0, $annotationsRaw['duration'] - $cue_out);
}
}
}
if ($annotationsRaw['liq_cue_out'] > $annotationsRaw['duration']) {
$annotationsRaw['liq_cue_out'] = null;
if (
isset($annotationsRaw['liq_cue_out'], $annotationsRaw['duration'])
&& $annotationsRaw['liq_cue_out'] > $annotationsRaw['duration']
) {
unset($annotationsRaw['liq_cue_out']);
}
if ($annotationsRaw['liq_cue_in'] > $annotationsRaw['duration']) {
$annotationsRaw['liq_cue_in'] = null;
if (
isset($annotationsRaw['liq_cue_in'], $annotationsRaw['duration'])
&& $annotationsRaw['liq_cue_in'] > $annotationsRaw['duration']
) {
unset($annotationsRaw['liq_cue_in']);
}
foreach ($annotationsRaw as $name => $prop) {
if (null === $prop) {
continue;
}
$prop = ConfigWriter::annotateString((string)$prop);
// Convert Liquidsoap-specific annotations to floats.

View File

@ -201,11 +201,9 @@ final class ConfigWriter implements EventSubscriberInterface
settings.server.socket.path.set("{$socketFile}")
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"])
setenv("TZ", "{$stationTz}")
environment.set("TZ", "{$stationTz}")
autodj_is_loading = ref(true)
ignore(autodj_is_loading)
@ -233,8 +231,8 @@ final class ConfigWriter implements EventSubscriberInterface
<<<LIQ
azuracast_api_url = "{$stationApiUrl}"
azuracast_api_key = "{$stationApiAuth}"
def azuracast_api_call(~timeout_ms=2000, url, payload) =
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}")
@ -245,7 +243,7 @@ final class ConfigWriter implements EventSubscriberInterface
("User-Agent", "Liquidsoap AzuraCast"),
("X-Liquidsoap-Api-Key", "#{azuracast_api_key}")
],
timeout_ms=timeout_ms,
timeout=timeout,
data=payload
)
@ -271,7 +269,7 @@ final class ConfigWriter implements EventSubscriberInterface
["#{station_media_dir}/#{arg}"]
end
add_protocol(
protocol.add(
"media",
azuracast_media_protocol,
doc="Pull files from AzuraCast media directory.",
@ -283,15 +281,15 @@ final class ConfigWriter implements EventSubscriberInterface
$event->appendBlock(
<<<LIQ
def azuracast_media_protocol(~rlog=_,~maxtime,arg) =
timeout_ms = 1000 * (int_of_float(maxtime) - int_of_float(time()))
timeout = 1000.0 * (maxtime - time())
j = json()
j.add("uri", arg)
[azuracast_api_call(timeout_ms=timeout_ms, "cp", json.stringify(j))]
[azuracast_api_call(timeout=timeout, "cp", json.stringify(j))]
end
add_protocol(
protocol.add(
"media",
azuracast_media_protocol,
temporary=true,
@ -605,22 +603,22 @@ final class ConfigWriter implements EventSubscriberInterface
# Delayed ping for AutoDJ Next Song
def wait_for_next_song(autodj)
autodj_ping_attempts := !autodj_ping_attempts + 1
autodj_ping_attempts.set(autodj_ping_attempts() + 1)
if source.is_ready(autodj) then
log("AutoDJ is ready!")
autodj_is_loading := false
autodj_is_loading.set(false)
-1.0
elsif !autodj_ping_attempts > 200 then
elsif autodj_ping_attempts() > 200 then
log("AutoDJ could not be initialized within the specified timeout.")
autodj_is_loading := false
autodj_is_loading.set(false)
-1.0
else
0.5
end
end
dynamic = request.dynamic(id="next_song", timeout=20., retry_delay=10., autodj_next_song)
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(
@ -630,14 +628,14 @@ final class ConfigWriter implements EventSubscriberInterface
dynamic,
source.available(
blank(id = "autodj_startup_blank", duration = 120.),
predicate.activates({!autodj_is_loading})
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) })
thread.run.recurrent(delay=0.25, { wait_for_next_song(ref_dynamic()) })
LIQ
);
}
@ -833,7 +831,7 @@ final class ConfigWriter implements EventSubscriberInterface
$event->appendBlock(
<<<LS
def live_aware_crossfade(old, new) =
if !to_live then
if to_live() then
# If going to the live show, play a simple sequence
sequence([fade.out(old.source),fade.in(new.source)])
else
@ -884,13 +882,13 @@ final class ConfigWriter implements EventSubscriberInterface
end
response = azuracast_api_call(
timeout_ms=5000,
timeout=5.0,
"auth",
json.stringify(auth_info)
)
if (response == "true") then
last_authenticated_dj := auth_info.user
last_authenticated_dj.set(auth_info.user)
true
else
false
@ -898,14 +896,14 @@ final class ConfigWriter implements EventSubscriberInterface
end
def live_connected(header) =
dj = !last_authenticated_dj
dj = last_authenticated_dj()
log("DJ Source connected! Last authenticated DJ: #{dj} - #{header}")
live_enabled := true
live_dj := dj
live_enabled.set(true)
live_dj.set(dj)
_ = azuracast_api_call(
timeout_ms=5000,
timeout=5.0,
"djon",
json.stringify({user = dj})
)
@ -913,13 +911,13 @@ final class ConfigWriter implements EventSubscriberInterface
def live_disconnected() =
_ = azuracast_api_call(
timeout_ms=5000,
timeout=5.0,
"djoff",
json.stringify({user = !live_dj})
json.stringify({user = live_dj()})
)
live_enabled := false
live_dj := ""
live_enabled.set(false)
live_dj.set("")
end
LIQ
);
@ -967,12 +965,12 @@ final class ConfigWriter implements EventSubscriberInterface
# Skip non-live track when live DJ goes live.
def check_live() =
if live.is_ready() then
if not !to_live then
to_live := true
if not to_live() then
to_live.set(true)
radio_without_live.skip()
end
else
to_live := false
to_live.set(false)
end
end
@ -997,10 +995,11 @@ final class ConfigWriter implements EventSubscriberInterface
recording_extension = "{$recordExtension}"
output.file(
{$formatString},
{$formatString},
fun () -> begin
if (!live_enabled) then
"#{recording_base_path}/#{!live_dj}/{$recordPathPrefix}_%Y%m%d-%H%M%S.#{recording_extension}.tmp"
if (live_enabled()) then
"#{recording_base_path}/#{live_dj()}/{$recordPathPrefix}_%Y%m%d-%H%M%S.#{recording_extension}.tmp"
else
""
end
@ -1058,7 +1057,10 @@ final class ConfigWriter implements EventSubscriberInterface
<<<LIQ
error_file = single(id="error_jingle", "{$errorFile}")
error_file = single(id="error_jingle", "{$errorFile}")
def tag_error_file(m) =
ignore(m)
ignore(m)
[("is_error_file", "true")]
end
@ -1077,10 +1079,10 @@ final class ConfigWriter implements EventSubscriberInterface
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 := m["title"]
last_artist := m["artist"]
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
@ -1109,9 +1111,9 @@ final class ConfigWriter implements EventSubscriberInterface
last_metadata = ref([])
def handle_jingle_mode(m) =
if (m["jingle_mode"] == "true") then
!last_metadata
last_metadata()
else
last_metadata := m
last_metadata.set(m)
m
end
end
@ -1284,7 +1286,10 @@ final class ConfigWriter implements EventSubscriberInterface
}
$output_params[] = 'name = "' . self::cleanUpString($station->getName()) . '"';
$output_params[] = 'description = "' . self::cleanUpString($station->getDescription()) . '"';
if (!$mount->getIsShoutcast()) {
$output_params[] = 'description = "' . self::cleanUpString($station->getDescription()) . '"';
}
$output_params[] = 'genre = "' . self::cleanUpString($station->getGenre()) . '"';
if (!empty($station->getUrl())) {
@ -1294,17 +1299,21 @@ final class ConfigWriter implements EventSubscriberInterface
$output_params[] = 'public = ' . ($mount->getIsPublic() ? 'true' : 'false');
$output_params[] = 'encoding = "' . $charset . '"';
if (null !== $protocol) {
if (!$mount->getIsShoutcast() && null !== $protocol) {
$output_params[] = 'protocol="' . $protocol->value . '"';
}
if ($format->sendIcyMetadata()) {
$output_params[] = 'icy_metadata="true"';
$output_params[] = 'send_icy_metadata="true"';
}
$output_params[] = 'radio';
return 'output.icecast(' . implode(', ', $output_params) . ')';
$outputCommand = ($mount->getIsShoutcast())
? 'output.shoutcast'
: 'output.icecast';
return $outputCommand . '(' . implode(', ', $output_params) . ')';
}
private function getOutputFormatString(StreamFormats $format, int $bitrate = 128): string

View File

@ -1,2 +0,0 @@
liquidsoap.chroot.make('/tmp/liquidsoap')
shutdown()

View File

@ -15,14 +15,12 @@ apt-get install -y --no-install-recommends ladspa-sdk
# Per-architecture LS installs
ARCHITECTURE=amd64
ARM_FULL_BUILD="${ARM_FULL_BUILD:-false}"
if [[ "$(uname -m)" = "aarch64" && ${ARM_FULL_BUILD} == "false" ]]; then
if [[ "$(uname -m)" = "aarch64" ]]; then
ARCHITECTURE=arm64
fi
# wget -O /tmp/liquidsoap.deb "https://github.com/savonet/liquidsoap/releases/download/v2.1.4/liquidsoap_2.1.4-ubuntu-jammy-1_${ARCHITECTURE}.deb"
wget -O /tmp/liquidsoap.deb "https://github.com/savonet/liquidsoap-release-assets/releases/download/rolling-release-v2.1.x/liquidsoap-d6313d1_2.1.5-ubuntu-jammy-1_${ARCHITECTURE}.deb"
wget -O /tmp/liquidsoap.deb "https://github.com/savonet/liquidsoap-release-assets/releases/download/rolling-release-v2.2.x/liquidsoap-8101608_2.2.0-ubuntu-jammy-1_${ARCHITECTURE}.deb"
dpkg -i /tmp/liquidsoap.deb
apt-get install -y -f --no-install-recommends

View File

@ -17,14 +17,14 @@ output.file(%vorbis.cbr(samplerate=44100, channels=2, bitrate=128), "fallback-[1
output.file(%vorbis.cbr(samplerate=44100, channels=2, bitrate=192), "fallback-[192].ogg", input, fallible=true)
output.file(%vorbis.cbr(samplerate=44100, channels=2, bitrate=256), "fallback-[256].ogg", input, fallible=true)
output.file(%vorbis.cbr(samplerate=44100, channels=2, bitrate=320), "fallback-[320].ogg", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=32, afterburner=false, aot="mpeg4_he_aac_v2", sbr_mode=true), "fallback-[32].mp4", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=48, afterburner=false, aot="mpeg4_he_aac_v2", sbr_mode=true), "fallback-[48].mp4", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=64, afterburner=false, aot="mpeg4_he_aac_v2", sbr_mode=true), "fallback-[64].mp4", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=96, afterburner=false, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[96].mp4", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=128, afterburner=false, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[128].mp4", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=192, afterburner=true, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[192].mp4", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=256, afterburner=true, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[256].mp4", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=320, afterburner=true, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[320].mp4", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=32, afterburner=false, aot="mpeg4_he_aac_v2", sbr_mode=true), "fallback-[32].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=48, afterburner=false, aot="mpeg4_he_aac_v2", sbr_mode=true), "fallback-[48].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=64, afterburner=false, aot="mpeg4_he_aac_v2", sbr_mode=true), "fallback-[64].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=96, afterburner=false, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[96].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=128, afterburner=false, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[128].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=192, afterburner=true, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[192].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=256, afterburner=true, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[256].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=320, afterburner=true, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[320].aac", input, fallible=true)
output.file(%opus(samplerate=48000, bitrate=32, vbr="constrained", application="audio", channels=2, signal="music", complexity=10, max_bandwidth="full_band"), "fallback-[32].opus", input, fallible=true)
output.file(%opus(samplerate=48000, bitrate=48, vbr="constrained", application="audio", channels=2, signal="music", complexity=10, max_bandwidth="full_band"), "fallback-[48].opus", input, fallible=true)
output.file(%opus(samplerate=48000, bitrate=64, vbr="constrained", application="audio", channels=2, signal="music", complexity=10, max_bandwidth="full_band"), "fallback-[64].opus", input, fallible=true)