| name | flutter-macos-permission-handler-camera-failure |
| description | Fix silent camera/microphone failure on Flutter macOS when using permission_handler plugin.
Use when: (1) Camera screen shows placeholder but no error on macOS, (2) CameraPermissionError
state is emitted but cause is unclear, (3) permission_handler checkCameraStatus() or
checkMicrophoneStatus() throws exceptions on macOS desktop, (4) Camera works on iOS/Android
but fails silently on macOS. The permission_handler plugin doesn't work reliably on macOS -
bypass it and let macOS handle permissions at the system level.
|
| author | Claude Code |
| version | 1.0.0 |
| date | "2026-01-28T00:00:00.000Z" |
Flutter macOS permission_handler Camera Failure
Problem
Camera/microphone permission checks using the permission_handler Flutter plugin throw
exceptions on macOS desktop, causing permission gates to emit error states. The camera
screen never renders, but the user sees a placeholder UI with no clear error message.
Context / Trigger Conditions
Use this skill when:
- Flutter app works on iOS/Android but camera fails on macOS
- Permission gate shows loading or error state on macOS
- Logs show
CameraPermissionError state being emitted
Permission.camera.status or Permission.microphone.status throws exceptions
- Camera placeholder UI appears but
VideoRecorderScreen (or equivalent) never initializes
- No camera-related logs appear after navigation to camera screen
Key diagnostic pattern:
🔐 CameraPermissionGate initState
🔐 Building with state: CameraPermissionInitial
🔐 Triggering permission refresh
🔐 Permission state changed: CameraPermissionError <-- This is the tell
Root Cause
The permission_handler plugin uses platform channels that don't work reliably on macOS
desktop. When checkCameraStatus() or checkMicrophoneStatus() is called:
- On iOS/Android: Returns proper permission status
- On macOS: Throws exception → BLoC catches → Emits error state → Screen blocked
macOS handles camera/microphone permissions at the system level - the first time an app
tries to access the camera, macOS shows its own permission dialog.
Solution
Bypass permission_handler on macOS and assume permissions are authorized:
Future<void> _onRefresh(
CameraPermissionRefresh event,
Emitter<CameraPermissionState> emit,
) async {
// On macOS desktop, permission_handler doesn't work reliably.
// macOS handles camera permissions at the system level when the app
// actually tries to access the camera, showing its own permission dialog.
if (!kIsWeb && Platform.isMacOS) {
emit(const CameraPermissionLoaded(CameraPermissionStatus.authorized));
return;
}
// Normal permission check for iOS/Android
try {
final status = await checkPermissions();
emit(CameraPermissionLoaded(status));
} catch (e) {
emit(const CameraPermissionError());
}
}
Required imports:
import 'dart:io';
import 'package:flutter/foundation.dart';
Verification
After implementing the fix:
- Logs should show:
🔐 macOS detected - bypassing permission_handler, assuming authorized
- Permission state changes to
CameraPermissionLoaded with status authorized
- Camera screen actually renders and camera initialization logs appear
- macOS shows its native permission dialog on first camera access (if not already granted)
Debugging Layers
When debugging silent camera failures, check these layers in order:
- Permission layer: Is the camera screen even being rendered? Check permission gate state.
- Initialization layer: Is
initialize() being called? Check for initialization logs.
- Device detection layer: Are camera devices found? Check
listDevices() results.
- Controller layer: Does camera controller initialize? Check for controller errors.
Add logging at each layer to trace where the flow stops.
Related Fixes
When implementing this fix, also add:
-
Error tracking in camera service: Add initializationError getter to report why
camera failed (no devices, permission denied by macOS, etc.)
-
UI error display: Pass error messages to placeholder widgets so users see what's wrong
-
Try-catch around native calls: CameraMacOS.instance.listDevices() can also throw -
wrap in try-catch and set error state
Example: Complete Fix
// In CameraPermissionBloc
Future<void> _onRefresh(...) async {
if (!kIsWeb && Platform.isMacOS) {
log('macOS detected - bypassing permission_handler');
emit(const CameraPermissionLoaded(CameraPermissionStatus.authorized));
return;
}
// ... normal flow for mobile
}
// In CameraMacOSService
@override
Future<void> initialize() async {
try {
_videoDevices = await CameraMacOS.instance.listDevices(...);
} catch (e) {
_initializationError = 'Failed to detect cameras: $e';
return;
}
if (_videoDevices?.isEmpty ?? true) {
_initializationError = 'No camera found. Please connect a camera.';
return;
}
// ... continue initialization
}
Notes
- This is a platform-specific workaround, not a bug in your code
- macOS Ventura+ has stricter permission handling; the native dialog will still appear
- Test on actual macOS hardware, not just simulators
- The
camera_macos or camera_macos_plus packages handle the native permission dialog
- Consider adding a "Camera permission required" UI that appears if macOS denies access
References