asr_client/asr/decode_stream.py
2025-10-15 10:51:30 +03:30

81 lines
2.9 KiB
Python

# 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)