fix playback control and resume from player itself
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 28 Jul 2022 19:24:11 +0000 (21:24 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 28 Jul 2022 19:24:11 +0000 (21:24 +0200)
music_assistant/constants.py
music_assistant/controllers/streams.py
music_assistant/helpers/resources/announce.flac [deleted file]
music_assistant/helpers/resources/announce.mp3 [new file with mode: 0644]
music_assistant/helpers/resources/silence.mp3 [new file with mode: 0644]
music_assistant/models/player_queue.py

index aa55eef267a9e1720c7be7050fbe567ec91547dc..13eed2d00a9c1d9af18bff4c40bc530c0ea9f004 100755 (executable)
@@ -1,7 +1,18 @@
 """All constants for Music Assistant."""
 
+import pathlib
+
 ROOT_LOGGER_NAME = "music_assistant"
 
 UNKNOWN_ARTIST = "Unknown Artist"
 VARIOUS_ARTISTS = "Various Artists"
 VARIOUS_ARTISTS_ID = "89ad4ac3-39f7-470e-963a-56509c546377"
+
+
+RESOURCES_DIR = pathlib.Path(__file__).parent.resolve().joinpath("helpers/resources")
+
+ANNOUNCE_ALERT_FILE = str(RESOURCES_DIR.joinpath("announce.mp3"))
+SILENCE_FILE = str(RESOURCES_DIR.joinpath("silence.mp3"))
+
+# if duration is None (e.g. radio stream) = 48 hours
+FALLBACK_DURATION = 172800
index 49fdd229229dde7f63f1a0a50a6ecfb76d15a3be..cfaf72000206a6885484570ee51cdd880505c47d 100644 (file)
@@ -11,6 +11,7 @@ from uuid import uuid4
 
 from aiohttp import web
 
+from music_assistant.constants import FALLBACK_DURATION, SILENCE_FILE
 from music_assistant.helpers.audio import (
     check_audio_support,
     crossfade_pcm_parts,
@@ -164,12 +165,44 @@ class StreamsController:
             request.remote,
             request.headers,
         )
+        client_id = request.match_info.get("player_id", request.remote)
         stream_id = request.match_info["stream_id"]
         queue_stream = self.queue_streams.get(stream_id)
 
+        # try to recover from the situation where the player itself requests
+        # a stream that is already done
         if queue_stream is None:
-            self.logger.warning("Got stream request for unknown id: %s", stream_id)
+            self.logger.warning(
+                "Got stream request for unknown or finished id: %s, trying resume",
+                stream_id,
+            )
+            if player := self.mass.players.get_player(client_id):
+                self.mass.create_task(player.active_queue.resume())
+                return web.FileResponse(SILENCE_FILE)
             return web.Response(status=404)
+        if queue_stream.done.is_set():
+            self.logger.warning(
+                "Got stream request for finished stream: %s, assuming resume", stream_id
+            )
+            self.mass.create_task(queue_stream.queue.resume())
+            return web.FileResponse(SILENCE_FILE)
+
+        # handle a second connection for the same player
+        # this means either that the player itself want to skip to the next track
+        # or a misbehaving client which reconnects multiple times (e.g. Kodi)
+        if queue_stream.all_clients_connected.is_set():
+            self.logger.warning(
+                "Got stream request for running stream: %s, assuming next", stream_id
+            )
+            self.mass.create_task(queue_stream.queue.next())
+            return web.FileResponse(SILENCE_FILE)
+
+        if client_id in queue_stream.connected_clients:
+            self.logger.warning(
+                "Simultanuous connections detected from %s, playback may be disturbed",
+                client_id,
+            )
+            client_id += uuid4().hex
 
         # prepare request, add some DLNA/UPNP compatible headers
         headers = {
@@ -196,7 +229,6 @@ class StreamsController:
             # do not start stream on HEAD request
             return resp
 
-        client_id = request.remote
         enable_icy = request.headers.get("Icy-MetaData", "") == "1"
 
         # regular streaming - each chunk is sent to the callback here
@@ -609,7 +641,9 @@ class QueueStream:
             crossfade_size = int(self.sample_size_per_second * crossfade_duration)
             queue_track.streamdetails.seconds_skipped = seek_position
             # predict total size to expect for this track from duration
-            stream_duration = (queue_track.duration or 48 * 3600) - seek_position
+            stream_duration = (
+                queue_track.duration or FALLBACK_DURATION
+            ) - seek_position
             # buffer_duration has some overhead to account for padded silence
             buffer_duration = (crossfade_duration + 4) if use_crossfade else 4
             # send signal that we've loaded a new track into the buffer
diff --git a/music_assistant/helpers/resources/announce.flac b/music_assistant/helpers/resources/announce.flac
deleted file mode 100644 (file)
index 95c7cae..0000000
Binary files a/music_assistant/helpers/resources/announce.flac and /dev/null differ
diff --git a/music_assistant/helpers/resources/announce.mp3 b/music_assistant/helpers/resources/announce.mp3
new file mode 100644 (file)
index 0000000..6e2fa0a
Binary files /dev/null and b/music_assistant/helpers/resources/announce.mp3 differ
diff --git a/music_assistant/helpers/resources/silence.mp3 b/music_assistant/helpers/resources/silence.mp3
new file mode 100644 (file)
index 0000000..24f4d72
Binary files /dev/null and b/music_assistant/helpers/resources/silence.mp3 differ
index 802eb93f5929f07a01cec83ddb06cd43db75915d..0bb0639620d57fd9a433cf789e41fdbc04bb014e 100644 (file)
@@ -2,12 +2,12 @@
 from __future__ import annotations
 
 import asyncio
-import pathlib
 import random
 from asyncio import TimerHandle
 from dataclasses import dataclass
 from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
 
+from music_assistant.constants import ANNOUNCE_ALERT_FILE, FALLBACK_DURATION
 from music_assistant.helpers.tags import parse_tags
 from music_assistant.helpers.util import try_parse_int
 from music_assistant.models.enums import EventType, MediaType, QueueOption, RepeatMode
@@ -23,17 +23,6 @@ if TYPE_CHECKING:
     from music_assistant.controllers.streams import QueueStream
     from music_assistant.mass import MusicAssistant
 
-RESOURCES_DIR = (
-    pathlib.Path(__file__)
-    .parent.resolve()
-    .parent.resolve()
-    .joinpath("helpers/resources")
-)
-
-ANNOUNCE_ALERT_FILE = str(RESOURCES_DIR.joinpath("announce.flac"))
-
-FALLBACK_DURATION = 172800  # if duration is None (e.g. radio stream) = 48 hours
-
 
 @dataclass
 class QueueSnapShot:
@@ -385,7 +374,9 @@ class PlayerQueue:
     async def seek(self, position: int) -> None:
         """Seek to a specific position in the track (given in seconds)."""
         assert self.current_item, "No item loaded"
-        assert position < self.current_item.duration, "Position exceeds track duration"
+        assert self.current_item.media_item.media_type == MediaType.TRACK
+        assert self.current_item.duration
+        assert position < self.current_item.duration
         await self.play_index(self._current_index, position)
 
     async def resume(self) -> None:
@@ -809,7 +800,7 @@ class PlayerQueue:
                 duration = (
                     queue_track.streamdetails.seconds_streamed
                     or queue_track.duration
-                    or 48 * 3600
+                    or FALLBACK_DURATION
                 )
                 if duration is not None and elapsed_time_queue > (
                     duration + total_time
@@ -847,28 +838,3 @@ class PlayerQueue:
                     self._current_item_elapsed_time = try_parse_int(db_value)
 
         await self.settings.restore()
-
-    async def _wait_for_state(
-        self,
-        state: Union[None, PlayerState, Tuple[PlayerState]],
-        queue_item_id: Optional[str] = None,
-        timeout: int = 120,
-    ) -> None:
-        """Wait for player(queue) to reach a specific state."""
-        if state is not None and not isinstance(state, tuple):
-            state = (state,)
-
-        count = 0
-        while count < timeout * 10:
-
-            if (state is None or self.player.state in state) and (
-                queue_item_id is None
-                or self.current_item
-                and self.current_item.item_id == queue_item_id
-            ):
-                return
-
-            count += 1
-            await asyncio.sleep(0.1)
-
-        raise TimeoutError(f"Timeout while waiting on state(s) {state}")