From: Marcel van der Veldt Date: Sat, 24 Aug 2024 21:50:39 +0000 (+0200) Subject: Add config option to specify the http content length header (#1607) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=9313a0d276e1dbf0937aa0d457a9a91968b52966;p=music-assistant-server.git Add config option to specify the http content length header (#1607) --- diff --git a/music_assistant/common/models/config_entries.py b/music_assistant/common/models/config_entries.py index a9428a91..46f0a4a5 100644 --- a/music_assistant/common/models/config_entries.py +++ b/music_assistant/common/models/config_entries.py @@ -27,6 +27,7 @@ from music_assistant.constants import ( CONF_EQ_TREBLE, CONF_FLOW_MODE, CONF_HIDE_PLAYER, + CONF_HTTP_PROFILE, CONF_ICON, CONF_LOG_LEVEL, CONF_OUTPUT_CHANNELS, @@ -592,6 +593,24 @@ CONF_ENTRY_SAMPLE_RATES = ConfigEntry( ) +CONF_ENTRY_HTTP_PROFILE = ConfigEntry( + key=CONF_HTTP_PROFILE, + type=ConfigEntryType.STRING, + options=( + ConfigValueOption("Profile 1 - chunked", "chunked"), + ConfigValueOption("Profile 2 - no content length", "no_content_length"), + ConfigValueOption("Profile 3 - forced content length", "forced_content_length"), + ), + default_value="chunked", + label="HTTP Profile used for sending audio", + category="advanced", + description="This is considered to be a very advanced setting, only adjust this if needed, " + "for example if your player stops playing halfway streams or if you experience " + "other playback related issues. In most cases the default setting, " + "'chunked transfer encoding', works just fine. \n\n", +) + + def create_sample_rates_config_entry( max_sample_rate: int, max_bit_depth: int, diff --git a/music_assistant/constants.py b/music_assistant/constants.py index 3d1ced95..ab5474ef 100644 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -65,6 +65,7 @@ CONF_ANNOUNCE_VOLUME_MAX: Final[str] = "announce_volume_max" CONF_ICON: Final[str] = "icon" CONF_LANGUAGE: Final[str] = "language" CONF_SAMPLE_RATES: Final[str] = "sample_rates" +CONF_HTTP_PROFILE: Final[str] = "http_profile" # config default values DEFAULT_HOST: Final[str] = "0.0.0.0" diff --git a/music_assistant/server/controllers/streams.py b/music_assistant/server/controllers/streams.py index 914443c4..47285269 100644 --- a/music_assistant/server/controllers/streams.py +++ b/music_assistant/server/controllers/streams.py @@ -35,6 +35,7 @@ from music_assistant.constants import ( CONF_BIND_PORT, CONF_CROSSFADE, CONF_CROSSFADE_DURATION, + CONF_HTTP_PROFILE, CONF_OUTPUT_CHANNELS, CONF_PUBLISH_IP, CONF_SAMPLE_RATES, @@ -45,6 +46,7 @@ from music_assistant.server.helpers.audio import ( FFMpeg, check_audio_support, crossfade_pcm_parts, + get_chunksize, get_ffmpeg_stream, get_hls_substream, get_icy_stream, @@ -264,16 +266,29 @@ class StreamsController(CoreController): default_sample_rate=queue_item.streamdetails.audio_format.sample_rate, default_bit_depth=queue_item.streamdetails.audio_format.bit_depth, ) + http_profile: str = self.mass.config.get_raw_player_config_value( + queue_id, CONF_HTTP_PROFILE, "chunked" + ) # prepare request, add some DLNA/UPNP compatible headers headers = { **DEFAULT_STREAM_HEADERS, "Content-Type": f"audio/{output_format.output_format_str}", + "Accept-Ranges": "none", + "Cache-Control": "no-cache", + "Connection": "close", } resp = web.StreamResponse( status=200, reason="OK", headers=headers, ) + if http_profile == "forced_content_length": + resp.content_length = get_chunksize( + output_format, queue_item.streamdetails.duration or 120 + ) + elif http_profile == "chunked": + resp.enable_chunked_encoding() + await resp.prepare(request) # return early if this is not a GET request @@ -340,10 +355,17 @@ class StreamsController(CoreController): ) == "1" and output_format.content_type in (ContentType.MP3, ContentType.AAC) icy_meta_interval = 16384 + # prepare request, add some DLNA/UPNP compatible headers + http_profile: str = self.mass.config.get_raw_player_config_value( + queue_id, CONF_HTTP_PROFILE, "chunked" + ) # prepare request, add some DLNA/UPNP compatible headers headers = { **DEFAULT_STREAM_HEADERS, "Content-Type": f"audio/{output_format.output_format_str}", + "Accept-Ranges": "none", + "Cache-Control": "no-cache", + "Connection": "close", } if enable_icy: headers["icy-metaint"] = str(icy_meta_interval) @@ -353,6 +375,10 @@ class StreamsController(CoreController): reason="OK", headers=headers, ) + if http_profile == "forced_content_length": + resp.content_length = get_chunksize(output_format, 24 * 2600) + elif http_profile == "chunked": + resp.enable_chunked_encoding() await resp.prepare(request) # return early if this is not a GET request diff --git a/music_assistant/server/providers/dlna/__init__.py b/music_assistant/server/providers/dlna/__init__.py index c5de303f..9446245e 100644 --- a/music_assistant/server/providers/dlna/__init__.py +++ b/music_assistant/server/providers/dlna/__init__.py @@ -27,6 +27,7 @@ from music_assistant.common.models.config_entries import ( CONF_ENTRY_CROSSFADE_DURATION, CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, CONF_ENTRY_ENFORCE_MP3, + CONF_ENTRY_HTTP_PROFILE, ConfigEntry, ConfigValueType, create_sample_rates_config_entry, @@ -79,6 +80,7 @@ PLAYER_CONFIG_ENTRIES = ( CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, CONF_ENTRY_CROSSFADE_DURATION, CONF_ENTRY_ENFORCE_MP3, + CONF_ENTRY_HTTP_PROFILE, create_sample_rates_config_entry(192000, 24, 96000, 24), ) diff --git a/music_assistant/server/providers/hass_players/__init__.py b/music_assistant/server/providers/hass_players/__init__.py index 59028c6e..713f31cc 100644 --- a/music_assistant/server/providers/hass_players/__init__.py +++ b/music_assistant/server/providers/hass_players/__init__.py @@ -17,6 +17,7 @@ from music_assistant.common.models.config_entries import ( CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED, CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED, + CONF_ENTRY_HTTP_PROFILE, ConfigEntry, ConfigValueOption, ConfigValueType, @@ -94,6 +95,7 @@ PLAYER_CONFIG_ENTRIES = ( CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, CONF_ENTRY_CROSSFADE_DURATION, CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED, + CONF_ENTRY_HTTP_PROFILE, ) diff --git a/music_assistant/server/providers/ugp/__init__.py b/music_assistant/server/providers/ugp/__init__.py index 85983c65..e4ca5feb 100644 --- a/music_assistant/server/providers/ugp/__init__.py +++ b/music_assistant/server/providers/ugp/__init__.py @@ -33,9 +33,13 @@ from music_assistant.common.models.enums import ( ) from music_assistant.common.models.media_items import AudioFormat from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.constants import CONF_GROUP_MEMBERS, SYNCGROUP_PREFIX +from music_assistant.constants import CONF_GROUP_MEMBERS, CONF_HTTP_PROFILE, SYNCGROUP_PREFIX from music_assistant.server.controllers.streams import DEFAULT_STREAM_HEADERS -from music_assistant.server.helpers.audio import get_ffmpeg_stream, get_player_filter_params +from music_assistant.server.helpers.audio import ( + get_chunksize, + get_ffmpeg_stream, + get_player_filter_params, +) from music_assistant.server.helpers.multi_client_stream import MultiClientStream from music_assistant.server.helpers.util import TaskManager from music_assistant.server.models.player_provider import PlayerProvider @@ -361,14 +365,28 @@ class UniversalGroupProvider(PlayerProvider): if not (stream := self.streams.get(ugp_player_id, None)) or stream.done: raise web.HTTPNotFound(body=f"There is no active UGP stream for {ugp_player_id}!") - resp = web.StreamResponse( - status=200, - reason="OK", - headers={ - **DEFAULT_STREAM_HEADERS, - "Content-Type": f"audio/{fmt}", - }, + output_format = AudioFormat( + content_type=ContentType.try_parse(fmt), + sample_rate=stream.audio_format.sample_rate, + bit_depth=stream.audio_format.bit_depth, + ) + + http_profile: str = self.mass.config.get_raw_player_config_value( + child_player_id, CONF_HTTP_PROFILE, "chunked" ) + headers = { + **DEFAULT_STREAM_HEADERS, + "Content-Type": "faudio/{fmt}", + "Accept-Ranges": "none", + "Cache-Control": "no-cache", + "Connection": "close", + } + + resp = web.StreamResponse(status=200, reason="OK", headers=headers) + if http_profile == "forced_content_length": + resp.content_length = get_chunksize(output_format, 24 * 3600) + elif http_profile == "chunked": + resp.enable_chunked_encoding() await resp.prepare(request) # return early if this is not a GET request @@ -383,7 +401,7 @@ class UniversalGroupProvider(PlayerProvider): ) async for chunk in stream.get_stream( - output_format=AudioFormat(content_type=ContentType.try_parse(fmt)), + output_format=output_format, filter_params=get_player_filter_params(self.mass, child_player_id) if child_player_id else None,