diff --git a/CHANGELOG.md b/CHANGELOG.md index 94971cce5..c37594a4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/src/Entity/Interfaces/StationMountInterface.php b/src/Entity/Interfaces/StationMountInterface.php index 6cbe322db..97eeea2b4 100644 --- a/src/Entity/Interfaces/StationMountInterface.php +++ b/src/Entity/Interfaces/StationMountInterface.php @@ -31,4 +31,6 @@ interface StationMountInterface public function getAutodjAdapterType(): AdapterTypeInterface; public function getIsPublic(): bool; + + public function getIsShoutcast(): bool; } diff --git a/src/Entity/StationMount.php b/src/Entity/StationMount.php index a9abf0f1a..5a56d0051 100644 --- a/src/Entity/StationMount.php +++ b/src/Entity/StationMount.php @@ -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. * diff --git a/src/Entity/StationRemote.php b/src/Entity/StationRemote.php index e5ba6f14e..78037e8a4 100644 --- a/src/Entity/StationRemote.php +++ b/src/Entity/StationRemote.php @@ -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. * diff --git a/src/Radio/AutoDJ/Annotations.php b/src/Radio/AutoDJ/Annotations.php index 9957cb12a..674274a97 100644 --- a/src/Radio/AutoDJ/Annotations.php +++ b/src/Radio/AutoDJ/Annotations.php @@ -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. diff --git a/src/Radio/Backend/Liquidsoap/ConfigWriter.php b/src/Radio/Backend/Liquidsoap/ConfigWriter.php index 7e0893983..cabf4425b 100644 --- a/src/Radio/Backend/Liquidsoap/ConfigWriter.php +++ b/src/Radio/Backend/Liquidsoap/ConfigWriter.php @@ -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 <<appendBlock( << 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( << 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 <<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 diff --git a/util/docker/stations/liquidsoap/build_chroot.liq b/util/docker/stations/liquidsoap/build_chroot.liq deleted file mode 100644 index 298137afc..000000000 --- a/util/docker/stations/liquidsoap/build_chroot.liq +++ /dev/null @@ -1,2 +0,0 @@ -liquidsoap.chroot.make('/tmp/liquidsoap') -shutdown() diff --git a/util/docker/stations/setup/liquidsoap.sh b/util/docker/stations/setup/liquidsoap.sh index 4130635cf..a55c3532a 100644 --- a/util/docker/stations/setup/liquidsoap.sh +++ b/util/docker/stations/setup/liquidsoap.sh @@ -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 diff --git a/util/docker/stations/liquidsoap/generate_fallback_files.liq b/util/generate_fallback_files.liq similarity index 94% rename from util/docker/stations/liquidsoap/generate_fallback_files.liq rename to util/generate_fallback_files.liq index 00d54d5e6..f415406a8 100644 --- a/util/docker/stations/liquidsoap/generate_fallback_files.liq +++ b/util/generate_fallback_files.liq @@ -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)