Update audio_streamer.py

This commit is contained in:
2025-12-17 17:25:16 +00:00
parent 3f5f5f51f9
commit a9445873fc

View File

@@ -1,11 +1,4 @@
#!/usr/bin/env python3
"""
Audio Streamer Module.
This module provides an HTTP streaming server that distributes audio to
connected clients (like Shoutcast/Icecast). It pipes audio from an FFMPEG
process into an asyncio buffer and serves it via aiohttp.
"""
import os
import asyncio
@@ -21,16 +14,13 @@ import logging
logger = logging.getLogger(__name__)
class StreamSource(Enum):
FILE = "file"
URL = "url"
SILENCE = "silence"
@dataclass(slots=True)
class StreamState:
"""Holds metadata about the current stream."""
source_type: StreamSource = StreamSource.SILENCE
source_path: str = ""
title: str = "Silence"
@@ -39,16 +29,7 @@ class StreamState:
duration: float = 0.0
listeners: int = 0
class AudioStreamer:
"""
Manages the FFMPEG subprocess and HTTP server for audio streaming.
Attributes:
_audio_buffer: Queue holding audio chunks from FFMPEG.
_clients: Set of active HTTP response objects to write to.
"""
CHUNK_SIZE = 2048
MAX_BUFFER_SIZE = 30
MAX_CLIENTS = 50
@@ -97,7 +78,6 @@ class AudioStreamer:
return self.state.source_path
async def start(self):
"""Initializes the web app and starts the broadcast loop."""
self._shutdown = False
self._app = web.Application()
self._app.router.add_get(self.mount_point, self._handle_stream)
@@ -112,10 +92,9 @@ class AudioStreamer:
await self._site.start()
self._broadcast_task = asyncio.create_task(self._broadcast_loop())
logger.info(f"HTTP streaming server started at {self.stream_url}")
logger.info(f"🔊 HTTP streaming server started at {self.stream_url}")
async def stop(self):
"""Stops FFMPEG, disconnects clients, and shuts down the server."""
self._shutdown = True
await self._stop_ffmpeg()
@@ -127,7 +106,6 @@ class AudioStreamer:
pass
self._broadcast_task = None
# Cleanup pending callback tasks
for task in self._pending_tasks:
task.cancel()
try:
@@ -151,10 +129,9 @@ class AudioStreamer:
self._clear_buffer()
gc.collect()
logger.info("HTTP streaming server stopped")
logger.info("🔇 HTTP streaming server stopped")
def _clear_buffer(self):
"""Drains the audio buffer to prevent memory bloat."""
count = 0
while not self._audio_buffer.empty():
try:
@@ -167,7 +144,6 @@ class AudioStreamer:
logger.debug(f"Cleared {count} chunks from buffer")
async def play_file(self, filepath: str, title: str = "", artist: str = "") -> bool:
"""Starts streaming a local file."""
if not os.path.exists(filepath):
logger.error(f"File not found: {filepath}")
return False
@@ -183,12 +159,11 @@ class AudioStreamer:
success = await self._start_ffmpeg(filepath)
if success:
self.state.is_playing = True
logger.info(f"Playing file: {filepath}")
logger.info(f"▶️ Playing file: {filepath}")
return success
async def play_url(self, url: str, title: str = "") -> bool:
"""Starts streaming from a remote URL."""
await self._stop_ffmpeg()
self.state.source_type = StreamSource.URL
@@ -200,7 +175,7 @@ class AudioStreamer:
success = await self._start_ffmpeg(url)
if success:
self.state.is_playing = True
logger.info(f"Playing URL: {url}")
logger.info(f"📻 Playing URL: {url}")
return success
@@ -213,7 +188,6 @@ class AudioStreamer:
self.state.artist = ""
async def _start_ffmpeg(self, source: str) -> bool:
"""Constructs and executes the FFMPEG command to transcode audio."""
codec_args = {
'mp3': ['-c:a', 'libmp3lame', '-b:a', f'{self.bitrate}k'],
'ogg': ['-c:a', 'libvorbis', '-b:a', f'{self.bitrate}k'],
@@ -223,7 +197,6 @@ class AudioStreamer:
format_args = codec_args.get(self.stream_format, codec_args['mp3'])
# Optimize inputs for network streams
if source.startswith(('http://', 'https://', 'rtmp://')):
cmd = [
'ffmpeg', '-reconnect', '1', '-reconnect_streamed', '1',
@@ -255,7 +228,6 @@ class AudioStreamer:
return False
async def _stop_ffmpeg(self):
"""Terminates the FFMPEG process."""
if self._stream_task:
self._stream_task.cancel()
try:
@@ -286,7 +258,6 @@ class AudioStreamer:
gc.collect()
async def _read_stream(self):
"""Reads stdout from FFMPEG and pushes to the broadcast buffer."""
try:
while not self._shutdown:
if not self._ffmpeg_process or self._ffmpeg_process.returncode is not None:
@@ -304,7 +275,6 @@ class AudioStreamer:
logger.debug('Stream EOF')
break
# Drop frames if buffer is full to keep stream "live"
dropped = 0
while self._audio_buffer.full():
try:
@@ -324,7 +294,6 @@ class AudioStreamer:
self.state.is_playing = False
# Fire track end callback safely
if self.on_track_end and not self._shutdown:
task = asyncio.create_task(self._safe_callback(self.on_track_end))
self._pending_tasks.add(task)
@@ -348,7 +317,6 @@ class AudioStreamer:
logger.error(f"Callback error: {e}")
async def _broadcast_loop(self):
"""Reads from the buffer and writes to all connected HTTP clients."""
try:
while not self._shutdown:
try:
@@ -378,7 +346,6 @@ class AudioStreamer:
logger.error(f"Broadcast loop error: {e}")
async def _get_duration(self, filepath: str) -> float:
"""Uses ffprobe to determine file duration."""
try:
process = await asyncio.create_subprocess_exec(
'ffprobe', '-v', 'quiet', '-show_entries', 'format=duration',
@@ -392,7 +359,6 @@ class AudioStreamer:
return 0.0
async def _handle_stream(self, request: web.Request) -> web.StreamResponse:
"""Handles HTTP GET requests for the audio stream."""
if len(self._clients) >= self.MAX_CLIENTS:
return web.Response(status=503, text="Server at capacity")
@@ -442,5 +408,12 @@ class AudioStreamer:
return web.Response(text=json.dumps(status), content_type='application/json')
async def _handle_info(self, request: web.Request) -> web.Response:
html = f"<!DOCTYPE html><html><head><title>XMPP Radio</title></head><body><h1>XMPP Radio Bot</h1><p><b>Now Playing:</b> {self.state.title}</p><p><b>Listeners:</b> {self.state.listeners}</p><audio controls autoplay><source src=\"{self.mount_point}\" type=\"audio/mpeg\"></audio></body></html>"
html = f"""<!DOCTYPE html>
<html><head><title>XMPP Radio</title></head>
<body>
<h1>🎵 XMPP Radio Bot</h1>
<p><b>Now Playing:</b> {self.state.title}</p>
<p><b>Listeners:</b> {self.state.listeners}</p>
<audio controls autoplay><source src="{self.mount_point}" type="audio/mpeg"></audio>
</body></html>"""
return web.Response(text=html, content_type='text/html')