Skip to content

Commit

Permalink
Chore: Simplify ffmpeg version check
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelveldt committed Jan 16, 2025
1 parent 3e5c6be commit c6c2cbc
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 59 deletions.
21 changes: 3 additions & 18 deletions music_assistant/controllers/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 1 addition & 17 deletions music_assistant/helpers/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
67 changes: 43 additions & 24 deletions music_assistant/helpers/ffmpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,23 @@
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:
from music_assistant_models.media_items import AudioFormat

LOGGER = logging.getLogger("ffmpeg")
MINIMAL_FFMPEG_VERSION = 6
CACHE_ATTR_LIBSOXR_PRESENT: Final[str] = "libsoxr_present"


class FFMpeg(AsyncProcess):
Expand Down Expand Up @@ -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],
Expand All @@ -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",
Expand Down Expand Up @@ -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:
Expand All @@ -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 "",
)

0 comments on commit c6c2cbc

Please sign in to comment.