Skip to content

miniapp SDK: speaker.isPlaying stays false during createStream() playback, silently breaking barge-in and echo guards #3892

Description

@skthegawd

Summary

session.speaker.isPlaying returns false for the entire duration of playback started with
speaker.createStream().

_state is only ever assigned from an inbound SPEAKER_STATE envelope (modules/speaker.ts,
_applyState). createStream() sets nothing locally, so unless the host emits a state event for
streamed PCM, isPlaying reads idle while audio is actively playing.

Impact

isPlaying is the natural guard for three things in a voice app, and all three broke at once for us
without any error:

  1. Barge-in. if (session.speaker.isPlaying) { session.speaker.stop() } never fired, so a button
    press fell through to opening the microphone instead of stopping the answer.
  2. Echo suppression. "Do not capture while speaking" let a follow-up capture open on top of our
    own playback.
  3. Ambient level measurement, which trained on our own voice.

The tell was that a code path calling speaker.stop() unconditionally worked instantly, while the
one guarded by isPlaying never triggered.

Reproduction

const stream = await session.speaker.createStream({ sampleRate: 24000 })
const write = stream.write(pcm)
console.log(session.speaker.isPlaying)   // false, while audio is audibly playing
await write
await stream.close()

close() is documented as "Resolves when playback has ended", and it does — so the SDK clearly knows
when playback is running. It just is not reflected in state/isPlaying.

Suggested fix

Set _state = "playing" locally when createStream() succeeds and clear it on close()/abort(),
or emit SPEAKER_STATE host-side for streamed playback the way play() and speak() presumably do.

Either way it would help to document which of play(), speak() and createStream() drive
isPlaying, since today they appear to differ and the difference is invisible.

Related

speaker.stop() does stop a createStream() playback — only the state reporting is missing. Note
that stop() is a one-shot with no result, so an app that tracks playback itself has no completion
signal after stopping; we clear our own flag at the call site.

Environment

  • @mentra/miniapp 3.1.0-dev.27
  • Device: Mentra Live, MentraOS app staging.20260808.*
  • Playback: 24 kHz PCM via createStream()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions