Add config option to specify the http content length header (#1607)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 24 Aug 2024 21:50:39 +0000 (23:50 +0200)
committerGitHub <noreply@github.com>
Sat, 24 Aug 2024 21:50:39 +0000 (23:50 +0200)
music_assistant/common/models/config_entries.py
music_assistant/constants.py
music_assistant/server/controllers/streams.py
music_assistant/server/providers/dlna/__init__.py
music_assistant/server/providers/hass_players/__init__.py
music_assistant/server/providers/ugp/__init__.py

index a9428a91bf7e3e257390c18581d734f21b8db379..46f0a4a5a8801f9c8a13a9762f1c55c25185e26d 100644 (file)
@@ -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,
index 3d1ced95e83a7421322402b346a890775fa05f01..ab5474eff02cc2c87c8d084799136d615c190c64 100644 (file)
@@ -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"
index 914443c49ff2b2f29217b679a062798c5b6db4ac..4728526919d7127cb46c2002832369af23bcaee4 100644 (file)
@@ -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
index c5de303fdb5b01579e11b62fe75c225c2a738d6e..9446245e04eed8e37731b9b1b8d532376b080871 100644 (file)
@@ -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),
 )
 
index 59028c6e108f09bf6bf1a497c3f776f496a2f724..713f31cc7e7b2d27e307d1de5c2a9a6aebdd0673 100644 (file)
@@ -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,
 )
 
 
index 85983c6530babe37136848edd9b2df5e88fdfae2..e4ca5febcce197616d1a9ef84f5b5af8ba3a3c35 100644 (file)
@@ -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,