diff --git a/music_assistant/controllers/streams.py b/music_assistant/controllers/streams.py index ebca68276..ed22d5051 100644 --- a/music_assistant/controllers/streams.py +++ b/music_assistant/controllers/streams.py @@ -50,7 +50,6 @@ ) from music_assistant.helpers.audio import LOGGER as AUDIO_LOGGER from music_assistant.helpers.audio import ( - check_audio_support, crossfade_pcm_parts, get_chunksize, get_hls_substream, @@ -61,7 +60,7 @@ get_stream_details, ) from music_assistant.helpers.ffmpeg import LOGGER as FFMPEG_LOGGER -from music_assistant.helpers.ffmpeg import get_ffmpeg_stream +from music_assistant.helpers.ffmpeg import check_ffmpeg_version, get_ffmpeg_stream from music_assistant.helpers.util import get_ip, get_ips, select_free_port, try_parse_bool from music_assistant.helpers.webserver import Webserver from music_assistant.models.core_controller import CoreController @@ -216,25 +215,11 @@ async def get_config_entries( async def setup(self, config: CoreConfig) -> None: """Async initialize of module.""" - ffmpeg_present, libsoxr_support, version = await check_audio_support() - major_version = int("".join(char for char in version.split(".")[0] if not char.isalpha())) - if not ffmpeg_present: - self.logger.error("FFmpeg binary not found on your system, playback will NOT work!.") - elif major_version < 6: - self.logger.error("FFMpeg version is too old, you may run into playback issues.") - elif not libsoxr_support: - self.logger.warning( - "FFmpeg version found without libsoxr support, " - "highest quality audio not available. " - ) - self.logger.info( - "Detected ffmpeg version %s %s", - version, - "with libsoxr support" if libsoxr_support else "", - ) # copy log level to audio/ffmpeg loggers AUDIO_LOGGER.setLevel(self.logger.level) FFMPEG_LOGGER.setLevel(self.logger.level) + # perform check for ffmpeg version + await check_ffmpeg_version() # start the webserver self.publish_port = config.get_value(CONF_BIND_PORT) self.publish_ip = config.get_value(CONF_PUBLISH_IP) diff --git a/music_assistant/helpers/audio.py b/music_assistant/helpers/audio.py index f6170da01..5ce3b6b4f 100644 --- a/music_assistant/helpers/audio.py +++ b/music_assistant/helpers/audio.py @@ -29,7 +29,6 @@ MusicAssistantError, ProviderUnavailableError, ) -from music_assistant_models.helpers import set_global_cache_values from music_assistant_models.streamdetails import AudioFormat from music_assistant.constants import ( @@ -47,7 +46,7 @@ from .dsp import filter_to_ffmpeg_params from .ffmpeg import FFMpeg, get_ffmpeg_stream from .playlists import IsHLSPlaylist, PlaylistItem, fetch_playlist, parse_m3u -from .process import AsyncProcess, check_output, communicate +from .process import AsyncProcess, communicate from .util import TimedAsyncGenerator, create_tempfile, detect_charset if TYPE_CHECKING: @@ -835,21 +834,6 @@ async def get_file_stream( yield data -async def check_audio_support() -> tuple[bool, bool, str]: - """Check if ffmpeg is present (with/without libsoxr support).""" - # check for FFmpeg presence - returncode, output = await check_output("ffmpeg", "-version") - ffmpeg_present = returncode == 0 and "FFmpeg" in output.decode() - - # use globals as in-memory cache - version = output.decode().split("ffmpeg version ")[1].split(" ")[0].split("-")[0] - libsoxr_support = "enable-libsoxr" in output.decode() - result = (ffmpeg_present, libsoxr_support, version) - # store in global cache for easy access by 'get_ffmpeg_args' - await set_global_cache_values({"ffmpeg_support": result}) - return result - - async def get_preview_stream( mass: MusicAssistant, provider_instance_id_or_domain: str, diff --git a/music_assistant/helpers/ffmpeg.py b/music_assistant/helpers/ffmpeg.py index 8ff24e84e..82ac1b511 100644 --- a/music_assistant/helpers/ffmpeg.py +++ b/music_assistant/helpers/ffmpeg.py @@ -7,15 +7,15 @@ import time from collections import deque from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Final from music_assistant_models.enums import ContentType from music_assistant_models.errors import AudioError -from music_assistant_models.helpers import get_global_cache_value +from music_assistant_models.helpers import get_global_cache_value, set_global_cache_values from music_assistant.constants import VERBOSE_LOG_LEVEL -from .process import AsyncProcess +from .process import AsyncProcess, check_output from .util import TimedAsyncGenerator, close_async_generator if TYPE_CHECKING: @@ -23,6 +23,7 @@ LOGGER = logging.getLogger("ffmpeg") MINIMAL_FFMPEG_VERSION = 6 +CACHE_ATTR_LIBSOXR_PRESENT: Final[str] = "libsoxr_present" class FFMpeg(AsyncProcess): @@ -186,7 +187,7 @@ async def get_ffmpeg_stream( yield chunk -def get_ffmpeg_args( # noqa: PLR0915 +def get_ffmpeg_args( input_format: AudioFormat, output_format: AudioFormat, filter_params: list[str], @@ -199,24 +200,6 @@ def get_ffmpeg_args( # noqa: PLR0915 """Collect all args to send to the ffmpeg process.""" if extra_args is None: extra_args = [] - ffmpeg_present, libsoxr_support, version = get_global_cache_value("ffmpeg_support") - if not ffmpeg_present: - msg = ( - "FFmpeg binary is missing from system." - "Please install ffmpeg on your OS to enable playback." - ) - raise AudioError( - msg, - ) - - major_version = int("".join(char for char in version.split(".")[0] if not char.isalpha())) - if major_version < MINIMAL_FFMPEG_VERSION: - msg = ( - f"FFmpeg version {version} is not supported. " - f"Minimal version required is {MINIMAL_FFMPEG_VERSION}." - ) - raise AudioError(msg) - # generic args generic_args = [ "ffmpeg", @@ -311,8 +294,9 @@ def get_ffmpeg_args( # noqa: PLR0915 input_format.sample_rate != output_format.sample_rate or input_format.bit_depth > output_format.bit_depth ): - # prefer resampling with libsoxr due to its high quality (if its available) - # but skip libsoxr if loudnorm filter is present, due to this bug: + libsoxr_support = get_global_cache_value(CACHE_ATTR_LIBSOXR_PRESENT) + # prefer resampling with libsoxr due to its high quality + # but skip if loudnorm filter is present, due to this bug: # https://trac.ffmpeg.org/ticket/11323 loudnorm_present = any("loudnorm" in f for f in filter_params) if libsoxr_support and not loudnorm_present: @@ -334,3 +318,38 @@ def get_ffmpeg_args( # noqa: PLR0915 extra_args += ["-af", ",".join(filter_params)] return generic_args + input_args + extra_args + output_args + + +async def check_ffmpeg_version() -> None: + """Check if ffmpeg is present (with libsoxr support).""" + # check for FFmpeg presence + returncode, output = await check_output("ffmpeg", "-version") + ffmpeg_present = returncode == 0 and "FFmpeg" in output.decode() + + # use globals as in-memory cache + version = output.decode().split("ffmpeg version ")[1].split(" ")[0].split("-")[0] + libsoxr_support = "enable-libsoxr" in output.decode() + await set_global_cache_values({CACHE_ATTR_LIBSOXR_PRESENT: libsoxr_support}) + + if not ffmpeg_present: + msg = ( + "FFmpeg binary is missing from system." + "Please install ffmpeg on your OS to enable playback." + ) + raise AudioError( + msg, + ) + + major_version = int("".join(char for char in version.split(".")[0] if not char.isalpha())) + if major_version < MINIMAL_FFMPEG_VERSION: + msg = ( + f"FFmpeg version {version} is not supported. " + f"Minimal version required is {MINIMAL_FFMPEG_VERSION}." + ) + raise AudioError(msg) + + LOGGER.info( + "Detected ffmpeg version %s %s", + version, + "with libsoxr support" if libsoxr_support else "", + )