diff --git a/audio_streamer.py b/audio_streamer.py index 8a1aa0c..ec0e8d9 100644 --- a/audio_streamer.py +++ b/audio_streamer.py @@ -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"
Now Playing: {self.state.title}
Listeners: {self.state.listeners}
" + html = f""" +Now Playing: {self.state.title}
+Listeners: {self.state.listeners}
+ +""" return web.Response(text=html, content_type='text/html') \ No newline at end of file