Skip to main content
createPCMStreamPlayer does one thing: play streamed PCM16 gap-tolerantly, and expose exactly what is playing as a MediaStream you can feed to lip sync. It is the bridge for realtime AI providers that hand you raw audio chunks and do not play them (Gemini Live, OpenAI Realtime over WebSocket) and for server TTS that returns audio only. It knows nothing about any provider — provider transport parsing is your glue and stays in your app.

API

options: PCMStreamPlayerOptions Returned PCMStreamPlayer:

Pattern

Create the player inside the user gesture, before any await — an AudioContext created in a post-fetch microtask starts suspended and cannot resume without another gesture.

Knowing when playback finished

There is no audio element to listen to, so the player surfaces a natural end through the onIdle option (with an isPlaying getter for polling). onIdle fires the moment the queue empties and every scheduled buffer has finished — i.e. all pushed audio has actually been heard.
  • It does not fire on stop() or close() — those are explicit interruptions, not a natural end.
  • It can fire more than once per player: a push after a drain restarts playback and a later drain fires it again.
This is the signal to drive a queue / sequential consumer (streamed-TTS playlists, multi-utterance agents): advance to the next item, reset the avatar, or release resources — without estimating audio duration from byte counts.

When NOT to use it

Never route a self-playing provider through createPCMStreamPlayer — you would hear the voice twice (double audio). ElevenLabs Conversational AI and OpenAI Realtime over WebRTC play the audio themselves. For those, do not use the player: tap their existing playback with the SDK’s cross-browser createElementTap() and feed that to useLipsyncStream({ source: { kind: "mediaStream", stream } }).
Decision rule: does the provider play the audio for you? Yes → tap its playback, no player. No (it hands you raw PCM) → createPCMStreamPlayer.

Server TTS

The same primitive powers “server returns audio only, the SDK does the lip sync”: your route synthesizes speech and returns base64 PCM16; the client plays it through the player and the tap drives the mouth. No server-side visemes, no SSE protocol. See Realtime providers.

Next

Realtime providers

The per-provider recipe.

Streaming & mic

Feeding the tap to lip sync.

Core client

The vanilla engine.