16

What I am trying currently is to play a message when app receives remote notification while in the background (or likely woken up from a suspended state).

The sound is not playing at all after the app is woken from a suspended mode.

When application is in the foreground, a sound is played immediately after didReceiveRemoteNotification: method is called.

What would be an appropriate way to have sounds played immediately when didReceiveRemoteNotification: method is called while app is woken up from a suspended mode?

Here is the some code (speech manager class):

-(void)textToSpeechWithMessage:(NSString*)message andLanguageCode:(NSString*)languageCode{

AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *error = nil;
DLog(@"Activating audio session");
if (![audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionMixWithOthers error:&error]) {
    DLog(@"Unable to set audio session category: %@", error);
}
BOOL result = [audioSession setActive:YES error:&error];
if (!result) {
    DLog(@"Error activating audio session: %@", error);

}else{
    AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:message];

    [utterance setRate:0.5f];

    [utterance setVolume:0.8f];

    utterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:languageCode];

    [self.synthesizer speakUtterance:utterance];
}

}

-(void)textToSpeechWithMessage:(NSString*)message{

[self textToSpeechWithMessage:message andLanguageCode:[[NSLocale preferredLanguages] objectAtIndex:0]];

}

And later on in AppDelegate:

[[MCSpeechManager sharedInstance] textToSpeechWithMessage:messageText];

I have enabled Audio,AirPlay and Picture in Picture option in Capabilities->Background Modes section.

EDIT:

Maybe I should start a background task and run expiration handler if needed? I guess that might work, but also I would like to hear the common way of solving this kind of situations.

Also with this code I get next error when I receive a notification in the background:

Error activating audio session: Error Domain=NSOSStatusErrorDomain Code=561015905 "(null)"

Code 561015905 applies to:

AVAudioSessionErrorCodeCannotStartPlaying = '!pla', /* 0x21706C61, 561015905

And it is described as:

This error type can occur if the app’s Information property list does not permit audio use, or if the app is in the background and using a category which does not allow background audio.

but I am getting the same error with other categories (AVAudioSessionCategoryAmbient and AVAudioSessionCategorySoloAmbient)

Cœur
  • 37,241
  • 25
  • 195
  • 267
Whirlwind
  • 14,286
  • 11
  • 68
  • 157
  • Will add this info as soon as I get my hands on computer. didReceiveRemoteNotification is invoked while app is suspended. Means app is not running and phone can be locked or some other app can be running etc. – Whirlwind Jan 04 '17 at 06:25
  • @SwiftArchitect The method which is invoked when remote notification is received is `application:(UIApplication*)application didReceiveRemoteNotification:(NSDictionary*)userInfo` (notice that it doesn't have a completion handler). Also, here : https://developer.apple.com/reference/uikit/uiapplicationdelegate/1623013-application we can find out that this method is called only if app is running in a foreground...But from what I can see,it is called while my app is in suspended mode (it stops on a break point I put in the method above). – Whirlwind Jan 04 '17 at 15:39
  • introduced: 3.0, deprecated: 10.0. Even if you find a solution using that route, it uses a deprecated API which is bound to be eventually rejected. – SwiftArchitect Jan 05 '17 at 04:21
  • Although it might seem non-related, playing audio from background needs another special permission in Info.plist. I guess it might be worth a try to set 'UIBackgroundModes' = 'audio' in your project's Info.plist. Please keep in mind that this permission is used for apps which need to play music in background. All radio apps use this. App Store might reject your app if this permission is set without a specific explanation of why your app needs this permission. Good Luck! – Tarun Tyagi Jan 16 '17 at 07:47
  • 1
    @TarunTyagi Thanks Tarun. Actually I did enabled background mode (see my question). I am about to write support ticket for this, so will eventually come back with a solution. – Whirlwind Jan 16 '17 at 13:15

2 Answers2

5

As I cannot reproduce the error you are describing, let me offer a few pointers, and some code.

  • Are you building/testing/running against the latest SDK? There have been significant changes around the notification mechanism in iOS X
  • I must assume that the invocation to didReceiveRemoteNotification must occur in response to a user action from said notification, as tapping on the notification message for example.
  • There is no need to set any of the background modes save App downloads content in response to push notifications.

If all of the above statements are true, the present answer will focus on what happens when a notification arrives.

  1. Device receives notification
    Remote
  2. User taps on message
  3. App launches
  4. didReceiveRemoteNotification is invoked

At step 4, textToSpeechWithMessage works as expected:

func application(_ application: UIApplication,
                 didReceiveRemoteNotification
                 userInfo: [AnyHashable : Any],
                 fetchCompletionHandler completionHandler:
                 @escaping (UIBackgroundFetchResult) -> Void) {
    textToSpeechWithMessage(message: "Speak up", "en-US")
}

For simplicity, I am using OneSignal to hook up notifications:

import OneSignal
...
_ = OneSignal.init(launchOptions: launchOptions,
                   appId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
// or
_ = OneSignal.init(launchOptions: launchOptions,
                   appId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
                   {
                       (s:String?, t:[AnyHashable : Any]?, u:Bool) in
                       self.textToSpeechWithMessage(message: "OneDignal", "en-US")
                   }

textToSpeechWithMessage is mostly untouched, here it is in Swift 3 for completeness:

import AVFoundation
...
let synthesizer = AVSpeechSynthesizer()
func textToSpeechWithMessage(message:String, _ languageCode:String)
{
    let audioSession = AVAudioSession.sharedInstance()

    print("Activating audio session")
    do {
        try audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord,
                                     with: [AVAudioSessionCategoryOptions.defaultToSpeaker,
                                            AVAudioSessionCategoryOptions.mixWithOthers]
        )
        try audioSession.setActive(true)

        let utterance = AVSpeechUtterance(string:message)
        utterance.rate = 0.5
        utterance.volume = 0.8
        utterance.voice = AVSpeechSynthesisVoice(language: languageCode)
        self.synthesizer.speak(utterance)

    } catch {
        print("Unable to set audio session category: %@", error);
    }
}
SwiftArchitect
  • 47,376
  • 28
  • 140
  • 179
  • Hi, thanks for the response. In my situation when user taps on notification everything will work. But I have a need to play a sound even if user wasnt tapped on it. And yeah, I am building against latest SDK and testing on a real device which runs latest iOS... – Whirlwind Jan 04 '17 at 06:20
  • From what I have experienced, `didReceiveRemoteNotification` is only invoked after the user took some kind of action. Are you able to get `didReceiveRemoteNotification` fire prior a user response? – SwiftArchitect Jan 04 '17 at 06:27
  • didReceiveRemoteNotification will be executed even if user dont tap the notification(so yeah, it will fire prior to user response, that is not the issue). I can produce this and is tested by simply putting a break point into this method. Still, the phone is not acting the same when it is connected / not connected to power supply , so I have to test this by logging things while not having connected the phone to the computer... will get back to you with results. – Whirlwind Jan 04 '17 at 06:38
  • I see. All I can offer then is the obvious: http://stackoverflow.com/questions/39382852/didreceiveremotenotification-not-called-ios-10 and http://stackoverflow.com/questions/5056689/didreceiveremotenotification-when-in-background – SwiftArchitect Jan 05 '17 at 04:26
  • Thanks, I am not using a variant of a `applicationDidReceiveRemoteNotification:` method that includes `completionHandler`, so that might be a problem. I haven't had a chance to test this extensively, but what I've noticed about this method is: 1) If app is terminated by the user, and notification arrives, this method wont be executed. 2) if app is attached to a debugger, but in background, this method fires. The third possibility would be to have a phone de-attached from power suply, in the background. This also probably wont work because docs says this method is working only in a foreground. – Whirlwind Jan 05 '17 at 15:14
  • Of course, in first case, i have logged everything into filesystem. But actually nothing was logged, because this method didn't fire at the time notification has arrived. – Whirlwind Jan 05 '17 at 15:18
  • I understand. You may not be able to achieve what you want given the direction the iOS is going, and would you have a workaround, run the risk of rejection at the approval stage. Text-to-speech is clearly an attempt to circumvent the unwritten rule of only being allowed to play sounds submitted as resources alongside the binary. I will draw my hat in respectfully retire this answer if proven wrong ; I actually like your idea. You can alway repost this bounty for a legendary 500 points to attract more attention. – SwiftArchitect Jan 07 '17 at 21:35
  • I think something like 1) receiving a remote notification , 2) starting a background task and asking a system for an additional time to finish some things (play a sound), and immediatelly return will work. This is because if you start a sound in a foreground and the app goes to background, sound will finish. This is perfectly legal stuff in many scenarios, I checked with Apple. Also this will work because of backgorund audio modes enabled in capabilites. I havent really tested all this, but I can bet it might work. Still, if there is another way, I would like to hear it... – Whirlwind Jan 07 '17 at 21:41
  • Also I didnt know about the rule you mentioned... is there some documentation about that? – Whirlwind Jan 07 '17 at 21:43
  • Clarification: the app can't take action before you have some kind of interaction, unless you are in the foreground. If you are in the background, then for your app to receive said notification, the user must take some kind of action. Once that action is taken, the code I posted actually works. – SwiftArchitect Jan 07 '17 at 21:48
  • Yeah. That is correct. Also, for future readers, when I said, I checked with Apple, I meant, I asked their engeeners, is it okay to ask additional foreground time while app is in background, but there was no mentions about audio and it was about something else completely unrelated to audio... Also, system uses different heuristics to determine if we will get and how much of that asked time... But pattern was the same. Maybe I will use support ticket to hear how this should be done and, if it is allowed. – Whirlwind Jan 09 '17 at 11:41
  • I awarded a bounty to you because of time and effort you put into this. Still, I can't accept an answer because it doesn't solve my situation, and will raise another bounty. – Whirlwind Jan 10 '17 at 07:56
0

Please implement

-(void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler; 

method. You'll get callback in background to play audio.

Whirlwind
  • 14,286
  • 11
  • 68
  • 157
Laxman Sahni
  • 572
  • 1
  • 6
  • 8