moeru-ai/airi · error · Error

No microphone stream available for voice input

Error message

No microphone stream available for voice input

What it means

Thrown by startAutoSegmentation in the voice-input session composable when mediaRef.value is null/undefined at the moment segmentation starts. VAD (and the volume fallback) need the live MediaStream from getUserMedia; if microphone acquisition never completed, was stopped, or permission was denied, there is no stream to attach analyzers to.

Solutions

  1. Ensure the media stream is acquired and assigned before starting segmentation: await the session start (getUserMedia) first, then begin auto-segmentation
  2. Check navigator.mediaDevices and permissions beforehand: const perm = await navigator.mediaDevices.query({ name: 'microphone' as PermissionName }) and surface a permission prompt if state === 'prompt'
  3. Run only in secure contexts (https:// or localhost); in Electron, grant permission via session.setPermissionRequestHandler
  4. Guard the call: if (!mediaRef.value) await startMedia() / or report 'microphone unavailable' to the user instead of throwing

Example fix

// before
startAutoSegmentation() // mediaRef.value is null -> throws

// after
async function ensureVoiceInput() {
  if (!mediaRef.value) {
    if (!navigator.mediaDevices?.getUserMedia)
      throw new Error('Microphone API unavailable (requires https or localhost)')
    mediaRef.value = await navigator.mediaDevices.getUserMedia({ audio: true })
  }
  await startAutoSegmentation()
}
Defensive patterns

Strategy: validation

Validate before calling

async function ensureMicStream(): Promise<MediaStream | null> {
  if (!navigator.mediaDevices?.getUserMedia) return null // insecure context
  try {
    return await navigator.mediaDevices.getUserMedia({ audio: true })
  } catch { return null }
}
const stream = await ensureMicStream()
if (!stream) return disableVoiceInput('Microphone unavailable or permission denied')

Type guard

const hasMicStream = (mediaRef: Ref<MediaStream | null | undefined>): mediaRef is Ref<MediaStream> => !!mediaRef.value

Try / catch

catch (e) { if (e.message === 'No microphone stream available for voice input') { setVoiceInputUnavailable(); return } throw e }

Prevention

When it happens

Trigger: Calling the segmentation/start path before the session's start() resolved getUserMedia; microphone permission denied in the browser so mediaRef never populated; the stream was stopped/cleared (device unplugged, track ended) before auto-segmentation began; calling on a non-secure context where getUserMedia is unavailable.

Common situations: Race in UI wiring: a 'start recording' handler fires VAD setup in parallel with stream acquisition; Electron/Chromium permission handler not configured so permission queries auto-deny; page loaded over http:// (not localhost) so navigator.mediaDevices is undefined; OS-level mic privacy toggle blocking the device; calling start twice where the second call observes the torn-down state.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/2cf374c22551ce4d. Report an issue: GitHub.

Appendix: source

Thrown at packages/stage-ui/src/composables/audio/voice-input-session.ts:565

          }
        }

        volumeFallbackAnimationFrame = requestAnimationFrame(analyze)
      }

      volumeFallbackAnimationFrame = requestAnimationFrame(analyze)
    }
    catch (error) {
      stopVolumeFallback()
      lastError.value = error
      log('error', 'volume-fallback-start-failed', 'Failed to start volume-based recorder fallback.', { error })
    }
  }

  async function startAutoSegmentation() {
    const stream = mediaRef.value
    if (!stream)
      throw new Error('No microphone stream available for voice input')

    await startVoiceInputVadDetectionSafely({
      init: initVAD,
      loaded: () => vadLoaded.value,
      start: startVAD,
      stream,
      getError: () => vadError.value,
      log,
    })
    await startVolumeFallback(stream)
  }

  async function stop(options: { flushActiveRecording?: boolean } = {}) {
    stopVolumeFallback()
    disposeVAD()
    transcriptionChain.reset()
    stoppedRecordingSegments.length = 0
    vadRecordings.clear()

View on GitHub (pinned to 677329427f)