# decode_stream.py import time import threading import av from av.audio.resampler import AudioResampler import asyncio class HLSReader: def __init__( self, url: str, *, sample_rate: int = 48_000, channels: int = 1, chunk_ms: int = 20, ): self.url = url self.sample_rate = sample_rate self.channels = channels self.chunk_ms = chunk_ms self._stop_evt = threading.Event() self._thread: threading.Thread | None = None self._loop: asyncio.AbstractEventLoop | None = None self._queue: "asyncio.Queue[bytes] | None" = None # derived self._bytes_per_sample = 2 # s16 self._samples_per_chunk = int(self.sample_rate * (self.chunk_ms / 1000.0)) self._chunk_bytes = self._samples_per_chunk * self.channels * self._bytes_per_sample def start(self, loop: asyncio.AbstractEventLoop, out_queue: "asyncio.Queue[bytes]") -> None: """Begin reading in a daemon thread, pushing chunks onto out_queue (on 'loop').""" self._loop = loop self._queue = out_queue self._thread = threading.Thread(target=self._run, name=f"hls-{id(self):x}", daemon=True) self._thread.start() def stop(self) -> None: self._stop_evt.set() # ---------- internal ---------- def _run(self) -> None: assert self._loop and self._queue while not self._stop_evt.is_set(): try: container = av.open(self.url, mode="r") audio_stream = next(s for s in container.streams if s.type == "audio") resampler = AudioResampler( format="s16", layout="mono" if self.channels == 1 else "stereo", rate=self.sample_rate, ) buf = bytearray() for packet in container.demux(audio_stream): if self._stop_evt.is_set(): break for frame in packet.decode(): for out in resampler.resample(frame): buf.extend(out.planes[0].to_bytes()) while len(buf) >= self._chunk_bytes: chunk = bytes(buf[: self._chunk_bytes]) del buf[: self._chunk_bytes] # push into asyncio queue on the event loop thread self._loop.call_soon_threadsafe(self._queue.put_nowait, chunk) # Some HLS variants EOF → reopen try: container.close() except Exception: pass if not self._stop_evt.is_set(): time.sleep(0.5) except Exception: # transient network/playlist issues → backoff & retry time.sleep(1.0)