diff --git a/src/main_stream_service/main_stream_manager.py b/src/main_stream_service/main_stream_manager.py index 9410641..9587dfc 100644 --- a/src/main_stream_service/main_stream_manager.py +++ b/src/main_stream_service/main_stream_manager.py @@ -304,25 +304,35 @@ async def _stop_or_recover_paused_track( async def _resume_radio_if_silent( self, track: Track, ctx: "_CycleContext" ) -> None: - """Возобновляет радио, если станция играет, а Ruark молчит.""" - if not ( - ctx.last_track_progress != track.progress - and track.type == "FmRadio" - and not await self._ruark_controls.is_playing() - ): + """Возобновляет радио, если станция играет, а Ruark молчит. + + Пересылка не чаще SILENCE_RESEND_GRACE после последней отправки: + свежепривязанному потоку нужно время на буферизацию HLS, иначе + ежесекундные перепривязки сами не дают радио стартовать. + """ + if track.type != "FmRadio": + return + if ctx.last_track_progress == track.progress: + return + # До первой привязки потока возобновлять нечего + if not ctx.last_stream_sent_at: + return + if time.monotonic() - ctx.last_stream_sent_at < SILENCE_RESEND_GRACE: + return + if await self._ruark_controls.is_playing(): return logger.info("🔁 Возобновляем воспроизведение радио") radio_url = await self._station_controls.get_radio_url() - if radio_url: - await self._send_track_to_stream_server( - track_url=radio_url, - radio=True, - title=track.title, - ) - await asyncio.sleep(1) - else: + if not radio_url: logger.warning("⚠️ Не удалось получить URL радиостанции") + return + ctx.last_stream_sent_at = time.monotonic() + await self._send_track_to_stream_server( + track_url=radio_url, + radio=True, + title=track.title, + ) async def _refresh_track_if_unchanged( self, track: Track, ctx: "_CycleContext" @@ -577,7 +587,10 @@ async def _restore_ruark_after_speech( logger.info("🔁 Возвращаем громкость Ruark") await self._ruark_controls.set_volume(self._ruark_volume) - for _ in range(30): + # Радио буферизует HLS дольше, чем стартует трек, — + # ждём дольше, чтобы не перепривязывать поток зря + attempts = 80 if track.type == "FmRadio" else 30 + for _ in range(attempts): if await self._ruark_controls.is_playing(): logger.info("▶️ Ruark начал играть") await self._station_controls.fade_out_alice_volume() @@ -592,6 +605,7 @@ async def _restore_ruark_after_speech( if ctx.track_url: # Для трека продолжаем с текущей позиции станции, # а не с начала — иначе рассинхрон + ctx.last_stream_sent_at = time.monotonic() await self._send_track_to_stream_server( ctx.track_url, radio=track.type == "FmRadio", diff --git a/tests/test_streaming_loop.py b/tests/test_streaming_loop.py index 949ee68..b963573 100644 --- a/tests/test_streaming_loop.py +++ b/tests/test_streaming_loop.py @@ -179,6 +179,47 @@ async def test_radio_track_sent_with_radio_flag(fast_sleep): assert call.kwargs["radio"] is True +async def test_silent_radio_not_respammed_within_grace(fast_sleep): + """Молчащий Ruark на радио не бомбардируется перепривязками.""" + # Прогресс радио тикает каждую итерацию — как на живой станции + tracks = [ + make_track( + id="fm_jazz", type="FmRadio", duration=0.0, progress=float(p) + ) + for p in range(1, 60) + ] + station = make_station_controls(tracks=tracks) + manager = make_manager(station, make_ruark(is_playing=False)) + send = manager._send_track_to_stream_server + + await drive_streaming(manager, until=lambda: False, timeout=0.3) + + # Только первоначальный свитч: грейс не даёт слать каждую итерацию + assert send.call_count == 1 + + +async def test_silent_radio_resent_after_grace(fast_sleep, monkeypatch): + """После грейса радио пересылается, если Ruark так и молчит.""" + monkeypatch.setattr( + "main_stream_service.main_stream_manager.SILENCE_RESEND_GRACE", 0.0 + ) + tracks = [ + make_track( + id="fm_jazz", type="FmRadio", duration=0.0, progress=float(p) + ) + for p in range(1, 60) + ] + station = make_station_controls(tracks=tracks) + manager = make_manager(station, make_ruark(is_playing=False)) + send = manager._send_track_to_stream_server + + await drive_streaming(manager, until=lambda: send.call_count >= 2) + + assert send.call_count >= 2 + resume_call = send.call_args_list[1] + assert resume_call.kwargs["radio"] is True + + async def test_alice_speech_ducks_ruark_volume(fast_sleep): track = make_track() # Первый вызов — инициализация last_alice_state, затем смена состояния