Module livekit.plugins.lemonslice.meeting.audio
Meeting audio ingestion and relay streaming for external video meetings.
Functions
async def stream_meeting_relay(websocket_url: str,
audio_sink: Callable[[bytes], None],
*,
stop: asyncio.Event,
chat_sink: Callable[[str], None] | None = None,
reconnect_delay_s: float = 1.0,
max_reconnect_delay_s: float = 30.0) ‑> None-
Expand source code
async def stream_meeting_relay( websocket_url: str, audio_sink: Callable[[bytes], None], *, stop: asyncio.Event, chat_sink: Callable[[str], None] | None = None, reconnect_delay_s: float = _INITIAL_RECONNECT_DELAY_S, max_reconnect_delay_s: float = _MAX_RECONNECT_DELAY_S, ) -> None: """Stream meeting audio and chat from the LemonSlice meeting relay. Reconnects with exponential backoff until ``stop`` is set. Args: websocket_url: WebSocket URL returned by join_meeting. audio_sink: Callback invoked with each binary PCM payload. stop: Event that signals the relay loop to exit. chat_sink: Optional callback invoked with each text chat JSON payload. reconnect_delay_s: Initial reconnect delay in seconds. max_reconnect_delay_s: Maximum reconnect delay in seconds. """ backoff_time = reconnect_delay_s while not stop.is_set(): try: async with ( aiohttp.ClientSession() as http, http.ws_connect(websocket_url, heartbeat=20.0) as ws, ): backoff_time = reconnect_delay_s audio_frames = 0 chat_messages = 0 logger.debug("connected to meeting relay") async for msg in ws: if stop.is_set(): break if msg.type == aiohttp.WSMsgType.BINARY: audio_sink(msg.data) audio_frames += 1 if audio_frames == 1: logger.debug("received first pcm audio frame in meeting relay") elif msg.type == aiohttp.WSMsgType.TEXT and chat_sink is not None: chat_sink(msg.data) chat_messages += 1 if chat_messages == 1: logger.debug("received first chat message in meeting relay") elif msg.type in ( aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR, ): break except asyncio.CancelledError: raise except Exception: logger.warning( "meeting relay disconnected; retrying in %ss", backoff_time, exc_info=True, ) if not stop.is_set(): await asyncio.sleep(backoff_time) backoff_time = min(backoff_time * 2, max_reconnect_delay_s)Stream meeting audio and chat from the LemonSlice meeting relay.
Reconnects with exponential backoff until
stopis set.Args
websocket_url- WebSocket URL returned by join_meeting.
audio_sink- Callback invoked with each binary PCM payload.
stop- Event that signals the relay loop to exit.
chat_sink- Optional callback invoked with each text chat JSON payload.
reconnect_delay_s- Initial reconnect delay in seconds.
max_reconnect_delay_s- Maximum reconnect delay in seconds.
Classes
class MeetingAudioInput (*, rate_out: int = 16000, queue_size: int = 100)-
Expand source code
class MeetingAudioInput(AudioInput): """AudioInput that feeds mixed external meeting audio into AgentSession STT.""" def __init__(self, *, rate_out: int = _DEFAULT_STT_RATE, queue_size: int = 100) -> None: """Initialize the meeting audio input. Args: rate_out: Output sample rate in Hz for frames consumed by STT. queue_size: Maximum number of audio frames to buffer. """ super().__init__(label="lemonslice-meeting-audio") self._loop = asyncio.get_running_loop() self._rate_out = rate_out self._queue: asyncio.Queue[rtc.AudioFrame] = asyncio.Queue(maxsize=queue_size) self._resampler: rtc.AudioResampler | None = None self._resampler_in_rate: int | None = None def submit(self, payload: bytes) -> None: """Enqueue a serialized PCM frame from the meeting relay WebSocket.""" try: self._loop.call_soon_threadsafe(self._push, payload) except RuntimeError: pass def _enqueue(self, frame: rtc.AudioFrame) -> None: if self._queue.full(): try: self._queue.get_nowait() except asyncio.QueueEmpty: pass try: self._queue.put_nowait(frame) except asyncio.QueueFull: logger.warning("meeting audio queue full; dropping frame") def _push(self, payload: bytes) -> None: frame = _deserialize_frame(payload) if frame is None: return frame = _downmix_to_mono(frame) for out in self._resample(frame): self._enqueue(out) def _resample(self, frame: rtc.AudioFrame) -> list[rtc.AudioFrame]: if frame.sample_rate == self._rate_out: return [frame] if self._resampler is None or self._resampler_in_rate != frame.sample_rate: self._resampler = rtc.AudioResampler( frame.sample_rate, self._rate_out, num_channels=frame.num_channels ) self._resampler_in_rate = frame.sample_rate return self._resampler.push(frame) async def __anext__(self) -> rtc.AudioFrame: """Return the next resampled audio frame for STT consumption.""" return await self._queue.get()AudioInput that feeds mixed external meeting audio into AgentSession STT.
Initialize the meeting audio input.
Args
rate_out- Output sample rate in Hz for frames consumed by STT.
queue_size- Maximum number of audio frames to buffer.
Ancestors
Methods
def submit(self, payload: bytes) ‑> None-
Expand source code
def submit(self, payload: bytes) -> None: """Enqueue a serialized PCM frame from the meeting relay WebSocket.""" try: self._loop.call_soon_threadsafe(self._push, payload) except RuntimeError: passEnqueue a serialized PCM frame from the meeting relay WebSocket.