First, a little background info to explain my motivation: I've got a Qt/C++/Objective-C++ application that uses CoreAudio/AVFoundation to receive incoming audio from specified audio inputs on the Mac, modify the audio, and then play the modified audio back out through some specified audio outputs. This all worked fine until Mojave and Catalina, at which point Apple's new microphone-privacy-restrictions caused it to no longer be able to receive the incoming audio (it only received zeroes/silence instead, due to lack of explicit user-permission to use the microphone).
To fix that, I added code to jump through the new get-the-user's-permission hoops (i.e. added a NSMicrophoneUsageDescription
tag to the Info.plist, added calls to authorizationStatusForMediaType
and requestAccessForMediaType
as suggested, etc), and now my app again works as expected when launched from its icon (i.e. it puts up the "MyAudioProcessingApp would like to use the microphone" requester, and once the user responds, a checkbox for my app appears in the "Security and Privacy / Privacy / Microphone" control panel and governs whether or not my app can listen to the incoming audio). That's all working fine, as far as it goes.
My problem is this -- my application also has a "background mode" feature, where the user can ask the app to install itself as a non-GUI system-service (which runs at boot via launchd/launchctl), so that it will do its audio-processing thing in the background as soon as the Mac is booted (i.e. without requiring anyone to log in or manually launch the application). This is quite useful for people who want to run this application on a "headless/embedded" mac as part of a fixed audio installation, where all anyone needs to do is power on the Mac to have it start processing audio.
However, what I've found is that when my app is running as a background process this way, [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]
always returns AVAuthorizationStatusDenied
, even when the user has previously given permission for my app to have access to the Microphone. This occurs even though the process's effective-user-ID is the same as that of the user that gave microphone-permission, and the executable running is the same file as the one that previously generated the permissions-prompt that the user agreed to.
My question is, is there some special trick I need in order to get microphone access while running in the background? Or has Apple decided that launchctl-launched-daemons simply can't get access to the microphone under any circumstance and I am therefore out of luck?
ps my application's MyAudioProcessingApp.app/Contents/Info.plist
file and /Library/LaunchDaemons/com.mycompany.myprogram.plist
file (both lightly anonymized) are below, in case they are relevant:
----- begin MyProcessingApp.app/Contents/Info.plist ------- snip ------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>MyAudioProcessingApp</string>
<key>CFBundleGetInfoString</key>
<string>Created by Qt/QMake</string>
<key>CFBundleIconFile</key>
<string>vcore.icns</string>
<key>CFBundleIdentifier</key>
<string>com.mycompany.MyAudioProcessingApp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>LSMinimumSystemVersion</key>
<string>10.10</string>
<key>NOTE</key>
<string>This file was generated by Qt/QMake.</string>
<key>NSMicrophoneUsageDescription</key>
<string>To allow MyAudioProcessingApp to process incoming audio data.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
</dict>
</plist>
----- end MyProcessingApp.app/Contents/Info.plist ------- snip ------
---- begin /Library/LaunchDaemons/com.mycompany.myprogram.plist ------ snip ------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "
http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:</string>
</dict>
<key>Label</key>
<string>com.mycompany.MyAudioProcessingApp</string>
<key>Program</key>
<string>/Library/MyCompany/MyAudioProcessingApp/run_my_program_in_background.sh</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/myprogram.stdout</string>
<key>StandardErrorPath</key>
<string>/tmp/myprogram.stderr</string>
<key>UserName</key>
<string>jaf</string> // NOTE: this is set dynamically to the correct user as part of the install-as-service step
<key>ProcessType</key>
<string>Interactive</string>
<key>GroupName</key>
<string>admin</string>
<key>InitGroups</key>
<true/>
</dict>
</plist>
---- end /Library/LaunchDaemons/com.mycompany.myprogram.plist ------ snip ------
---- begin /Library/MyCompany/MyAudioProcessingApp/run_my_program_in_background.sh ------ snip ------
#!/bin/bash
PATH_TO_MYPROGRAM_EXE="/Library/MyCompany/MyAudioProcessingApp/MyAudioProcessingApp.app/Contents/MacOS/MyAudioProcessingApp"
"$PATH_TO_MYPROGRAM_EXE" run_without_gui
exit 0
---- end /Library/MyCompany/MyAudioProcessingApp/run_my_program_in_background.sh ------ snip ------