/
/
/
1"""Tests for player-specific live source output codec selection."""
2
3from __future__ import annotations
4
5from types import SimpleNamespace
6from unittest.mock import MagicMock
7
8import pytest
9from music_assistant_models.enums import MediaType
10from music_assistant_models.player import PlayerMedia
11
12from music_assistant.constants import (
13 CONF_OUTPUT_CODEC,
14 CONF_PREFER_WAV_FOR_LIVE_SOURCES,
15)
16from music_assistant.controllers.streams import StreamsController
17from music_assistant.providers.chromecast.constants import CAST_PLAYER_CONFIG_ENTRIES
18from music_assistant.providers.sonos.player import SonosPlayer
19from music_assistant.providers.sonos_s1.player import SonosPlayer as SonosS1Player
20
21
22def _controller(output_codec: str, prefer_wav_for_live_sources: bool) -> StreamsController:
23 """Return a streams controller with one configured HTTP player."""
24 controller = StreamsController.__new__(StreamsController)
25 controller._base_url = "http://mass:8097"
26 controller.mass = mass = MagicMock()
27 player = MagicMock()
28 values = {
29 CONF_OUTPUT_CODEC: output_codec,
30 CONF_PREFER_WAV_FOR_LIVE_SOURCES: prefer_wav_for_live_sources,
31 }
32 player.config.get_value.side_effect = lambda key, default=None: values.get(key, default)
33 player.flow_mode = False
34 player.supports_gapless = True
35 mass.players.get_player.return_value = player
36 mass.player_queues.get.return_value = SimpleNamespace(
37 crossfade_enabled=False,
38 overlay_enabled=False,
39 overlay_source=None,
40 )
41 return controller
42
43
44def _media(media_type: MediaType) -> PlayerMedia:
45 """Return queue media with the data needed to resolve its stream URL."""
46 return PlayerMedia(
47 uri="fake_plugin://audio_source/main",
48 media_type=media_type,
49 source_id="queue-1",
50 queue_item_id="item-1",
51 queue_session_id="session-1",
52 )
53
54
55@pytest.mark.parametrize(
56 ("prefer_wav_for_live_sources", "expected_extension"),
57 [(False, "mp3"), (True, "wav")],
58)
59async def test_audio_source_url_respects_wav_preference(
60 prefer_wav_for_live_sources: bool,
61 expected_extension: str,
62) -> None:
63 """Live sources use WAV only when the player opts into the low-latency path."""
64 controller = _controller("mp3", prefer_wav_for_live_sources)
65
66 url = await controller.resolve_stream_url("player-1", _media(MediaType.AUDIO_SOURCE))
67
68 assert url == (
69 f"http://mass:8097/single/session-1/queue-1/item-1/player-1.{expected_extension}"
70 )
71
72
73async def test_wav_preference_does_not_change_regular_tracks() -> None:
74 """The live source preference leaves regular output codec selection unchanged."""
75 controller = _controller("aac", True)
76
77 url = await controller.resolve_stream_url("player-1", _media(MediaType.TRACK))
78
79 assert url == "http://mass:8097/single/session-1/queue-1/item-1/player-1.aac"
80
81
82def test_chromecast_prefers_wav_for_live_sources_by_default() -> None:
83 """Cast players default to the known-compatible low-latency WAV path."""
84 entry = next(
85 entry
86 for entry in CAST_PLAYER_CONFIG_ENTRIES
87 if entry.key == CONF_PREFER_WAV_FOR_LIVE_SOURCES
88 )
89
90 assert entry.default_value is True
91
92
93@pytest.mark.parametrize("player_class", [SonosPlayer, SonosS1Player])
94async def test_sonos_prefers_wav_for_live_sources_by_default(
95 player_class: type[SonosPlayer | SonosS1Player],
96) -> None:
97 """Sonos players default to the known-compatible low-latency WAV path."""
98 player = player_class.__new__(player_class)
99
100 entries = await player.get_config_entries()
101 entry = next(entry for entry in entries if entry.key == CONF_PREFER_WAV_FOR_LIVE_SOURCES)
102
103 assert entry.default_value is True
104