How to record audio from a background (launchctl-based) process under Mojave/Catalina?
Asked Answered
T

3

8

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 ------
Tonisha answered 25/9, 2019 at 18:21 Comment(6)
I'm sorry to hear that my suggestion did not work out. I will check tomorrow, if I can dig something out.Condorcet
Thanks @Condorcet -- if it helps, I made up a little self-contained-toy-program/MCVE that can be used to reproduce the problem with a minimum of code -- it can be downloaded at this URL: public.msli.com/lcs/jaf/request_microphone_access.zipTonisha
Nice mvce. I have not found any way to do it. This appears to be blocked and Apple does not mention it in any documentation I could find. Now the question is if this is a bug or feature.Condorcet
Thanks for trying @Condorcet -- that is more or less the conclusion I had come to as well. I've filed a Technical Support Incident at developer.apple.com, and if/when they respond I'll describe their response here.Tonisha
I think you've taken the right approach in using a TS incident to chase it down. I'd also recommend filing a Feedback and calling it "incorrect behavior". Both Feedback and TSI people will probably want you to put together a smallest-possible test project to demo the issue. FWIW I looked for this info previously and couldn't find any documentation on the topic.Eyelash
@Eyelash thanks for the idea; I've filed a case at feedbackassistant.apple.comTonisha
T
3

I heard back from Apple's Tech Support people this morning; they say it's not possible to access the microphone from a non-GUI system service.

They recommend filing a feature request with their Feedback Assistant (which I have already done), or as a work-around, setting up my program as a Login Item instead, and setting up the Mac to auto-login that user.

Since I don't have a whole lot of confidence that Apple is going to change their behavior based on my requests, I suppose the latter is what I will look into next.

Tonisha answered 7/10, 2019 at 18:22 Comment(4)
That Apple tech is incorrect. See this answer on Ask DifferentQuesta
@Questa that answer seems to be about recording from the Terminal window, whereas this question is about recording from a launchctl-based background process -- not that same thing AFAICT.Tonisha
No, same thing. If I ssh into a Mac, I can record from the mic, no GUI required.Questa
Cool -- but what I need is to power on the Mac and have it automatically start processing the incoming audio, even before anyone has logged in (as part of an interactive audio installation where the Mac is running as a headless server box), which is why I want the program to run as a daemon process via launchd.Tonisha
M
2

I ran into the same problem when upgrading from High Sierra to Catalina and solved it by auto-logging in a newly created user upon boot. I also need to run recording scripts at certain hours so I created a plist file in ~/Library/LaunchAgents which calls an Automator bash script saved as an app, the plist of which had the NSMicrophoneUsageDescription added as described above. The first time I run the script/app, I have to accept mic permissions in a GUI window and, annoyingly, also whenever I make changes to the bash script/Automator app. I don't know whether this is true also for AppleScripts saved as apps. I prefer cron over LaunchAgents but scheduling the same app in cron via the bash command automator doesn't prompt the GUI permission request so I guess I'm stuck with LaunchAgents and different apps for different set of parameters (I haven't found any way to pass arguments from the LaunchAgents plist file to the app/bash script).

Moulder answered 5/4, 2022 at 3:58 Comment(0)
H
1

You could try moving your Job definition .plist to /System/Library/LaunchDaemons/ and also try it from there having removed the UserName key.

It might allow system daemons less restrictive access, after all, how do people who control mac using their voice get to log on. Might ask Apple tech support about that, or maybe the VP of inclusion.

You may need to temporarily disable SIP to get it in there. Also remember to use launchctl to remove the existing job first.

Less likely, but still worth a try if you're currently out of options is also trying it from /System/Library/LaunchAgents/ again, both with and without the UserName key

Hedrick answered 4/10, 2019 at 3:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.