Refus de caméra Flutter macOS TCC Processus responsable
Problème
Une application Flutter macOS qui devrait avoir accès à la caméra/microphone affiche un aperçu noir (ou retourne zéro images) même si :
NSCameraUsageDescriptionetNSMicrophoneUsageDescriptionsont présents dansmacos/Runner/Info.plist.com.apple.security.device.cameraetcom.apple.security.device.audio-inputsont àtruedansDebugProfile.entitlementsetRelease.entitlements.- Le plugin caméra natif signale
isInitialized: trueet retourne un ID de texture. - Aucune boîte de dialogue de permission n'apparaît jamais.
- Aucune erreur n'est enregistrée par le plugin caméra ou AVFoundation.
Le symptôme est généralement un « fallback timeout » qui se déclenche dans le plugin (par exemple
DivineCamera macOS: Initialization completed via timeout, ou similaire avec des gardes init de 2 secondes)
car le plugin attend un premier buffer d'échantillon qui n'arrive jamais. Les métadonnées caméra
du plugin montrent iso: 0.0, exposureDuration: 0.0, aperture: 0.0 — l'appareil a été énuméré
mais n'a jamais commencé à fournir d'échantillons.
Contexte / Conditions déclenchantes
Tout ce qui suit est vrai :
- L'application a été lancée via
flutter run -d macos(ou viaflutter rundepuis un IDE qui enveloppe flutter dans un terminal — VS Code, Cursor, Android Studio, cmux). - L'émulateur de terminal est une application hardened-runtime sans entitlements caméra/micro dans sa propre signature de code.
- La version macOS est 10.15+ (TCC enforcement resserri ; affecte Big Sur et versions ultérieures, s'intensifie sur Sequoia/Tahoe).
- Aucun appel
AVCaptureDevice.requestAccess(for: .video)explicite dans le plugin, OU l'appel est attribué au mauvais processus.
Spécifiquement, ce N'est PAS :
flutter-macos-permission-handler-camera-failure(c'est à propos du plugin permission_handler qui échoue silencieusement à faire le pont vers TCC).flutter-macos-duplicate-camera-plugin(c'est une erreur de build).- Une entrée Info.plist manquante (la description d'utilisation EST présente).
- Un entitlement manquant (l'entitlement EST présent).
Cause racine
macOS TCC attribue les demandes de permission au processus responsable, pas au processus d'accès.
Quand flutter run lance le .app comme enfant du terminal, TCC parcourt l'arborescence des processus
et choisit le terminal comme la « partie responsable » pour tout accès à une ressource protégée.
C'est le même mécanisme qui provoque les invites « Terminal wants access to ... » au lieu d'invites
pour les scripts individuels.
Pour la caméra/microphone spécifiquement, TCC impose que le processus responsable ait l'entitlement
correspondant. Si cmux / iTerm / VS Code / etc. est le processus responsable et qu'il est hardened-runtime
sans com.apple.security.device.camera, TCC enregistre :
tccd: Prompting policy for hardened runtime;
service: kTCCServiceCamera requires entitlement com.apple.security.device.camera
but it is missing for responsible={identifier=<terminal>, ...}
accessing={identifier=<YourApp>, binary_path=.../YourApp.app/Contents/MacOS/YourApp}
tccd: Policy disallows prompt for Sub:{<terminal>}; access to kTCCServiceCamera denied
Critique : TCC n'affichera même pas une invité à l'utilisateur. Il refuse silencieusement.
Du point de vue de l'application, AVCaptureSession.startRunning() retourne sans erreur mais
zéro sample buffer ne sont jamais produits, donc tout timeout de premier cadre dans le plugin
se déclenche finalement.
Diagnostic
Exécutez ceci pendant (ou juste après) la reproduction de l'échec :
/usr/bin/log show --last 10m --predicate 'subsystem == "com.apple.TCC"' --info 2>&1 \
| grep -iE "camera|microphone|<YourAppName>|Runner" \
| tail -40
Cherchez la phrase :
Policy disallows prompt for Sub:{<terminal.bundle.id>}...;
access to kTCCServiceCamera denied
et la AttributionChain montrant responsible={identifier=<terminal>} et
accessing={identifier=<YourApp>}. La présence de ces lignes confirme le diagnostic.
Lire directement la base de données TCC (~/Library/Application Support/com.apple.TCC/TCC.db)
échouera avec « authorization denied » sauf si Terminal a Accès disque complet — l'approche
log show est le moyen fiable.
Solution
Correctif immédiat (test)
Lancez le bundle .app via Launch Services au lieu de flutter run. Launch Services
rend l'app son propre processus responsable, de sorte que TCC demandera à l'utilisateur
et attribuera la demande à l'app elle-même (qui a l'entitlement).
# Build une fois via flutter
cd <project>/mobile
flutter build macos --debug
# Puis lancez via `open` — PAS via `flutter run`
open build/macos/Build/Products/Debug/<YourApp>.app
open route via launchd/LaunchServices, donc le processus responsable devient
<YourApp> plutôt que le terminal. Au premier lancement d'un état où TCC n'a pas
d'entrée pour l'app, macOS affichera l'invite de permission standard.
Si TCC a déjà mis en cache l'état refusé
Si vous aviez précédemment lancé via flutter run, TCC peut avoir mis en cache une entrée
refusée attribuée au terminal. Réinitialisez-la :
# Réduisez la portée : réinitialisez seulement les permissions de cette app
tccutil reset Camera <your.app.bundle.id>
tccutil reset Microphone <your.app.bundle.id>
# Ou réinitialisation générale (re-demande à CHAQUE app qui utilise caméra/micro) :
tccutil reset Camera
tccutil reset Microphone
Puis relancez via open.
Correctif long terme (applications distribuées)
Ce problème n'affecte que flutter run depuis un terminal. Les bundles .app de production
distribués aux utilisateurs finaux (via DMG, Mac App Store, notarized download) lancent via
LaunchServices et ne rencontrent pas cela. Aucun correctif de code n'est nécessaire pour les builds de release.
Pour une meilleure expérience développeur, les plugins devraient appeler
AVCaptureDevice.requestAccess(for: .video) (et .audio) explicitement avant
de démarrer la session de capture. Cela ne change pas le résultat TCC de flutter run
(TCC attribue toujours au terminal), mais fournit un chemin d'erreur clair granted=false
au lieu d'un timeout de cadre silencieux.
AVCaptureDevice.requestAccess(for: .video) { videoGranted in
guard videoGranted else {
completion(nil, "Camera permission denied")
return
}
AVCaptureDevice.requestAccess(for: .audio) { _ in
// audio denial can be non-fatal; continue without audio input
self.sessionQueue.async { self.setupCamera(completion: completion) }
}
}
Vérification
Après lancement via open :
- macOS devrait afficher une invite système standard : « YourApp would like to access the camera. » Accordez-la.
- L'aperçu caméra devrait apparaître dans l'app dans ~1 seconde (pas de fallback timeout).
log show --last 2m --predicate 'subsystem == "com.apple.TCC"'devrait montrerauthorizedau lieu dedenied, et le processus responsable devrait être l'app elle-même.- Les métadonnées du plugin devraient signaler des valeurs
isoetexposureDurationnon nulles — confirmant que l'AVCaptureDevice fonctionne réellement.
Si l'aperçu est toujours noir après tout cela, enquêtez séparément (c'est un vrai bug dans le code du plugin, pas un problème TCC).
Exemple de session
Symptôme observé : Application Flutter macOS en révision (PR ajoutant le support caméra macOS natif)
lancée via flutter run depuis cmux. L'UI caméra a rendu, le plugin a signalé isInitialized: true,
mais l'aperçu était noir et le log montrait DivineCamera macOS: Initialization completed via timeout.
Hypothèse initiale (erronée) : le code Swift du plugin manquait un appel AVCaptureDevice.requestAccess.
Exécution diagnostique :
/usr/bin/log show --last 15m --predicate 'subsystem == "com.apple.TCC"' --info \
| grep -iE "camera|Divine"
Preuve trouvée :
tccd: Prompting policy for hardened runtime; service: kTCCServiceCamera
requires entitlement com.apple.security.device.camera but it is missing for
responsible={identifier=com.cmuxterm.app, ...}
accessing={identifier=Divine, binary_path=.../Divine.app/.../Divine}
tccd: Policy disallows prompt for Sub:{com.cmuxterm.app}...;
access to kTCCServiceCamera denied
Conclusion : cmux était le processus responsable, pas Divine. Le plugin était innocent — le même échec aurait pu se produire avec n'importe quel plugin caméra.
Correctif appliqué :
cd mobile && flutter build macos --debug
open build/macos/Build/Products/Debug/Divine.app
L'app a demandé la permission caméra au premier lancement, l'utilisateur a accordé, l'aperçu a fonctionné.
Notes
- Quels émulateurs de terminal sont affectés : Tout terminal hardened-runtime qui ne demande pas l'entitlement caméra. cmux, iTerm2, Terminal.app, Warp, VS Code integrated terminal, Cursor integrated terminal, Android Studio integrated terminal. Essentiellement tous.
- Donner l'accès caméra au terminal aide-t-il ? En principe oui (System Settings > Privacy & Security > Camera > enable the terminal),
mais la plupart des terminaux sont signés hardened-runtime sans l'entitlement caméra donc
la case n'est pas offerte. Le workaround
openest plus fiable. - Pourquoi le message d'erreur est trompeur : Le plugin enregistre « initialization completed via timeout » — ce log est techniquement vrai (la garde init de 2 secondes s'est déclenchée) mais ne dit rien sur POURQUOI aucun cadre n'est arrivé. La vraie réponse n'est que dans le log du sous-système TCC, que la plupart des développeurs ne vérifient jamais.
- Affecte aussi : Microphone (
kTCCServiceMicrophone), screen recording (kTCCServiceScreenCapture), contacts, calendar — toute ressource protégée par TCC lancée via un terminal sans l'entitlement correspondant. Le même workflowlog show+opens'applique. - Y a-t-il un correctif pour
flutter run? Non. L'outil Flutter invoque l'app comme un sous-processus ; il n'y a aucun moyen de faire en sorte que Flutter-l'outil se retire de la chaîne de responsabilité. Apple ne fournit aucune API pour qu'une app revendique sa propre responsabilité TCC. Vous utilisez soitopen, soit acceptez les frictions au premier lancement en développement. - Connexe mais différent :
flutter-macos-permission-handler-camera-failurecouvre l'échec du plugin permission_handler Dart à faire le pont vers TCC. C'est un mode d'échec différent où le plugin lui-même est silencieux ; cette skill couvre TCC étant silencieux avant même que le plugin n'ait une chance de demander.
Références
- Apple Developer Forums: « App launched from Terminal inherits Terminal's TCC responsibility » (cherchez les posts DTS / Quinn "The Eskimo!" sur TCC).
- Apple Technical Note TN3127: « Inside Code Signing: Requirements » — couvre l'attribution du processus responsable.
man tccutil— syntaxereset [service] [bundleID].log show --predicate 'subsystem == "com.apple.TCC"'— la source de vérité authoritative pour ce que TCC a réellement fait.