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 anyawait — 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 theonIdle 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()orclose()— 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.
When NOT to use it
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.