Skip to content

Request microphone permission before audio capture starts - #1183

Open
hiroshihorie wants to merge 4 commits into
mainfrom
hiroshi/mic-permission-before-capture
Open

Request microphone permission before audio capture starts#1183
hiroshihorie wants to merge 4 commits into
mainfrom
hiroshi/mic-permission-before-capture

Conversation

@hiroshihorie

@hiroshihorie hiroshihorie commented Aug 27, 2026

Copy link
Copy Markdown
Member

Flutter counterpart of client-sdk-swift #1085, matching its final merged behavior. Builds on #1182.

Why

webrtc-sdk/webrtc#265 (first shipped in m144.7559.12) removed the blocking mic permission request from the AudioEngine device. The pre-enable check is now passive: it returns kAudioEngineErrorInsufficientDevicePermission (-9000) instead of prompting, so requesting permission is the SDK's job.

flutter-webrtc still pins 144.7559.10, so this is not load-bearing yet. It is harmless there, since getUserMedia in flutter-webrtc already prompts and the status is resolved before the device's blocking path runs. Once flutter-webrtc bumps past .12 and the pin here follows, this is what keeps the current behavior.

What Flutter already had

flutter-webrtc's getUserMedia calls AVCaptureDevice requestAccessForMediaType: and waits for the answer, so every livekit_client mic path (publish, restartTrack on unmute, pre-connect audio) already prompted before the audio device saw the track. That part of #1085 needs no port. #1182 already maps -9000 to TrackCreateException for the direct ADM entry points (setEngineAvailability, startLocalRecording).

What this adds

The one behavior from #1085 that was missing: only prompt while the app can show the alert.

  • Native ensureMicrophoneAccess in LiveKitPlugin.swift: authorized passes, denied/restricted fail, notDetermined requests access. On iOS the request is only made while UIApplication.shared.applicationState == .active. An inactive or backgrounded app (locked screen, CallKit wake, app switcher) has the alert deferred by the system, and awaiting it would suspend getUserMedia and the _publishRunner behind it, blocking camera and screen share publishes for as long as the app stays there. Failing fast lets the next foreground attempt prompt normally. macOS can present the prompt regardless, so it always requests. No app extension concern here, the plugin is app-only.
  • The gate is skipped while engine input availability is disabled (setEngineAvailability, the CallKit flow), mirroring the same late fix in Add RPC tester interface to example app #1085: the audio device module defers opening input entirely and runs no permission check there, so gating would turn a working background connect into a deviceAccessDenied failure. The check reads the plugin's tracked availability value, so it also covers gating done natively before the Flutter engine exists.
  • LocalTrack.createStream calls it for AudioCaptureOptions on Apple platforms before getUserMedia. That is the Flutter choke point: LocalAudioTrack.create(), restartTrack() and PreConnectAudioBuffer.startRecording() all reach it. Since the prompt in Flutter happens at getUserMedia rather than at capture start, the gate sits in front of that instead of in startCapture as in Swift.
  • Failures surface as TrackCreateException through the deviceAccessDenied code introduced in Resolve a default audio session from engine state when no policy was pushed #1182.
  • Docs for withPreConnectAudio and PreConnectAudioBuffer.startRecording now say permission is requested at recording start but only while the app is active, so callers running at app launch should request it up front (matching the final Add RPC tester interface to example app #1085 wording). Native.setEngineAvailability documents that permission is not requested there and must be granted before input availability is restored.

Testing

  • flutter analyze, flutter test, dart format --set-exit-if-changed, import_sorter --exit-if-changed clean.
  • Unit tests cover Native.ensureMicrophoneAccess (no-op when unimplemented, propagates deviceAccessDenied). The createStream gate is behind lkPlatformIsApple() and not reachable from unit tests.
  • The example app builds for iOS (device SDK) and macOS with the change, re-verified after the rebase onto main. On-device run against a fresh install (first-launch prompt) and a CallKit background wake still to do.

Refs CLT-3243, client-sdk-swift#1085

Base automatically changed from hiroshi/native-audio-session-preset to main September 1, 2026 06:16
webrtc-sdk/webrtc#265 (m144.7559.12 and later) removed the blocking mic
permission request from the AudioEngine device. It now only checks the
status and fails with kAudioEngineErrorInsufficientDevicePermission, so
requesting permission is the SDK's job.

The native plugin gains ensureMicrophoneAccess: authorized passes,
denied or restricted fails, and notDetermined requests access, on iOS
only while the app is active. An inactive or backgrounded app has the
alert deferred by the system, and waiting on it would suspend
getUserMedia and the publish queue behind it, so it fails fast instead
and the next foreground attempt prompts normally.

LocalTrack.createStream calls it for audio on Apple platforms before
getUserMedia, which covers publishing, restartTrack on unmute, and
pre-connect audio. Failures surface as TrackCreateException through the
existing audio engine error mapping.
With input availability disabled (the CallKit flow from setEngineAvailability)
the audio device module defers opening input entirely and runs no permission
check, so gating track creation there turned a working background connect into
a deviceAccessDenied failure. The plugin already tracks the last-set
availability for both the channel and native static paths, so the handler
reads that instead of a module getter.

Also restores the setEngineAvailability doc comment that the previous commit
accidentally split from its declaration, adds the note that permission must be
granted before input availability is restored, and aligns the
withPreConnectAudio guidance with client-sdk-swift: request permission up
front when calling it before the app becomes active.
@hiroshihorie
hiroshihorie force-pushed the hiroshi/mic-permission-before-capture branch from c6e95ca to 0a0e30c Compare September 1, 2026 06:21
@hiroshihorie
hiroshihorie marked this pull request as ready for review September 1, 2026 06:25
devin-ai-integration[bot]

This comment was marked as resolved.

startRecording set isRecording and armed the agent-ready timeout before
creating the track, so a creation failure (now a designed outcome when the
mic permission gate rejects) left the buffer latched: retries were silently
ignored while capturing nothing, and the stale timeout later completed the
unobserved agentReadyFuture as an unhandled error. Route the failure through
the same stopRecording cleanup the later start steps already use.

ReusableCompleter.completeError also delivered errors nobody can observe,
since future() hands out a fresh completer once completed. Complete silently
in that case, matching what reset() and dispose() already do.
devin-ai-integration[bot]

This comment was marked as resolved.

…t skip cleanup

All failure paths invoked the app-provided onError callback before their own
cleanup, so a callback that throws left the buffer latched as recording and
replaced the original failure with the callback's own error. Route every call
site through a helper that logs and swallows callback errors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant