microsoft/semantic-kernel · error · MediaStreamError

Failed to receive audio frame

Error message

Failed to receive audio frame

What it means

MediaStreamError raised by the realtime sample's audio track recv(): the await on the internal asyncio queue (self._queue.get()) is wrapped in try/except, and any Exception during dequeue is logged then re-raised as MediaStreamError('Failed to receive audio frame'). It is a sample-level wrapper around aiortc's MediaStreamTrack meant to surface audio-capture failures to the WebRTC peer consistently.

Source

Thrown at python/samples/concepts/realtime/utils.py:104

            "channels": channels,
            "frame_duration": frame_duration,
            "dtype": dtype,
            "frame_size": int(sample_rate * frame_duration / 1000),
        })
        MediaStreamTrack.__init__(self)

    async def recv(self) -> Frame:
        """Receive the next frame of audio data."""
        if not self._recording_task:
            self._recording_task = asyncio.create_task(self.start_recording())

        try:
            frame = await self._queue.get()
            self._queue.task_done()
            return frame
        except Exception as e:
            logger.error(f"Error receiving audio frame: {e!s}")
            raise MediaStreamError("Failed to receive audio frame")

    def _sounddevice_callback(self, indata: np.ndarray, frames: int, time: Any, status: Any) -> None:
        if status:
            logger.warning(f"Audio input status: {status}")
        if self._loop and self._loop.is_running():
            asyncio.run_coroutine_threadsafe(self._queue.put(self._create_frame(indata)), self._loop)

    def _create_frame(self, indata: np.ndarray) -> Frame:
        audio_data = indata.copy()
        if audio_data.dtype != self.dtype:
            audio_data = (
                (audio_data * 32767).astype(self.dtype) if self.dtype == np.int16 else audio_data.astype(self.dtype)
            )
        frame = AudioFrame(
            format="s16",
            layout="mono",
            samples=len(audio_data),
        )

View on GitHub (pinned to c028a0c7dc)

Solutions

  1. Confirm the microphone device id passed to the recorder exists and is permitted (check OS audio permissions).
  2. Ensure the asyncio event loop is running for the lifetime of the track so _sounddevice_callback can enqueue frames.
  3. Catch MediaStreamError at the consumer and end the WebRTC track cleanly rather than crashing.
  4. Verify sounddevice can open the stream at the configured sample_rate/channels/dtype before starting.

Example fix

# before
try:
    frame = await self._queue.get()
    self._queue.task_done()
    return frame
except Exception as e:
    logger.error(f"Error receiving audio frame: {e!s}")
    raise MediaStreamError("Failed to receive audio frame")
# after - distinguish cancellation from real errors, preserve cause
try:
    frame = await self._queue.get()
    self._queue.task_done()
    return frame
except asyncio.CancelledError:
    raise
except Exception as e:
    logger.error(f"Error receiving audio frame: {e!s}")
    raise MediaStreamError("Failed to receive audio frame") from e
Defensive patterns

Strategy: try-catch

Validate before calling

# Before starting the track, confirm the device can be opened.
import sounddevice as sd
def device_available(device, sample_rate, channels, dtype):
    try:
        sd.check_input_settings(device=device, samplerate=sample_rate,
                                channels=channels, dtype=dtype)
        return True
    except Exception:
        return False

Type guard

def track_ready(track) -> bool:
    import asyncio
    return (
        getattr(track, "_loop", None) is not None
        and track._loop.is_running()
        and getattr(track, "_recording_task", None) is not None
        and not track._recording_task.done()
    )

Try / catch

try:
    frame = await track.recv()
except MediaStreamError:
    # end the WebRTC track cleanly; do not crash the peer
    await track.stop()

Prevention

When it happens

Trigger: The queue.get() / task_done() call raises - e.g. the recording task was cancelled, the event loop is closed, the queue is in a bad state, or the underlying sounddevice callback stopped feeding frames (device disconnected, permission revoked, loop not running).

Common situations: Audio input device removed/disconnected mid-session; microphone permission denied by the OS; the asyncio loop (self._loop) stopped so run_coroutine_threadsafe callbacks no longer enqueue frames; or the track's recording task was cancelled during teardown.

Related errors


AI-assisted analysis of microsoft/semantic-kernel@c028a0c7dc (2026-08-13). Data as JSON: /api/errors/147e349063d983c1. Report an issue: GitHub.