{"record":{"id":"2cf374c22551ce4d","repo":"moeru-ai/airi","slug":"no-microphone-stream-available-for-voice-input","errorCode":null,"errorMessage":"No microphone stream available for voice input","messagePattern":"No microphone stream available for voice input","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/composables/audio/voice-input-session.ts","lineNumber":565,"sourceCode":"          }\n        }\n\n        volumeFallbackAnimationFrame = requestAnimationFrame(analyze)\n      }\n\n      volumeFallbackAnimationFrame = requestAnimationFrame(analyze)\n    }\n    catch (error) {\n      stopVolumeFallback()\n      lastError.value = error\n      log('error', 'volume-fallback-start-failed', 'Failed to start volume-based recorder fallback.', { error })\n    }\n  }\n\n  async function startAutoSegmentation() {\n    const stream = mediaRef.value\n    if (!stream)\n      throw new Error('No microphone stream available for voice input')\n\n    await startVoiceInputVadDetectionSafely({\n      init: initVAD,\n      loaded: () => vadLoaded.value,\n      start: startVAD,\n      stream,\n      getError: () => vadError.value,\n      log,\n    })\n    await startVolumeFallback(stream)\n  }\n\n  async function stop(options: { flushActiveRecording?: boolean } = {}) {\n    stopVolumeFallback()\n    disposeVAD()\n    transcriptionChain.reset()\n    stoppedRecordingSegments.length = 0\n    vadRecordings.clear()","sourceCodeStart":547,"sourceCodeEnd":583,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/composables/audio/voice-input-session.ts#L547-L583","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the media stream is acquired and assigned before starting segmentation: await the session start (getUserMedia) first, then begin auto-segmentation","Check navigator.mediaDevices and permissions beforehand: const perm = await navigator.mediaDevices.query({ name: 'microphone' as PermissionName }) and surface a permission prompt if state === 'prompt'","Run only in secure contexts (https:// or localhost); in Electron, grant permission via session.setPermissionRequestHandler","Guard the call: if (!mediaRef.value) await startMedia() / or report 'microphone unavailable' to the user instead of throwing"],"exampleFix":"// before\nstartAutoSegmentation() // mediaRef.value is null -> throws\n\n// after\nasync function ensureVoiceInput() {\n  if (!mediaRef.value) {\n    if (!navigator.mediaDevices?.getUserMedia)\n      throw new Error('Microphone API unavailable (requires https or localhost)')\n    mediaRef.value = await navigator.mediaDevices.getUserMedia({ audio: true })\n  }\n  await startAutoSegmentation()\n}","handlingStrategy":"validation","validationCode":"async function ensureMicStream(): Promise<MediaStream | null> {\n  if (!navigator.mediaDevices?.getUserMedia) return null // insecure context\n  try {\n    return await navigator.mediaDevices.getUserMedia({ audio: true })\n  } catch { return null }\n}\nconst stream = await ensureMicStream()\nif (!stream) return disableVoiceInput('Microphone unavailable or permission denied')","typeGuard":"const hasMicStream = (mediaRef: Ref<MediaStream | null | undefined>): mediaRef is Ref<MediaStream> => !!mediaRef.value","tryCatchPattern":"catch (e) { if (e.message === 'No microphone stream available for voice input') { setVoiceInputUnavailable(); return } throw e }","preventionTips":["Await getUserMedia before starting VAD/segmentation","Run only on https:// or localhost; configure Electron permission handlers","Watch track 'ended' events and stop segmentation when the stream dies","Prompt for mic permission in UI before enabling voice features"],"tags":["microphone","getusermedia","vad","voice-input","permissions"],"backgroundTag":"microphone-access-unavailable","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}