How to trigger AVAudioPlayer playback using a remote push notification? - objective-c

I'm creating an app that has a remotely-triggered alarm. Essentially, I'm trying to trigger a looping MP3 file to play (while the app is backgrounded) when a remote push notification arrives with a particular payload.
I've tried using didReceiveRemoteNotification: fetchCompletionHandler:, so that code can be run as a result of receiving a remote notification with a particular userInfo payload.
Here is my attempted didReceiveRemoteNotification: fetchCompletionHandler: from my AppDelegate.m:
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSString *command = [userInfo valueForKeyPath:#"custom.a.command"];
if (command) {
UIApplicationState applicationState = [[UIApplication sharedApplication] applicationState];
if ([command isEqualToString:#"alarm"] && applicationState != UIApplicationStateActive) {
// Play alarm sound on loop until app is opened by user
NSLog(#"playing alarm.mp3");
NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:#"alarm" ofType:#"mp3"];
NSURL *soundFileURL = [NSURL fileURLWithPath:soundFilePath];
NSError *error;
self.player = nil;
self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFileURL error:&error];
self.player.numberOfLoops = -1; // Infinitely loop while self.player is playing
self.player.delegate = self;
[self.player play];
}
}
completionHandler(UIBackgroundFetchResultNewData);
}
I expected the looping audio file to start playing as soon as the push notification arrived (with the app inactive or backgrounded), but it didn't. Instead, the audio playback surprisingly began when I then bring the app to the foreground.
What is missing from this approach, and/or can a different way work better?

You cannot start an audio session with the app in background. Audio sessions have to be initialized/started while the app is in foreground. An audio session properly initialized and running can continue if the app is pushed to background provided another app in foreground does not interrupt it.
Based on this information, I would say that your application likely has to start an audio session while you are in control and in foreground, keep the audio session alive while in background. Upon receiving the push notification, use the existing opened audio session to deliver audio out.
This has serious limitations since any other app, like Netflix, that uses a dedicated audio session may interrupt your app's audio session and prevent it from being able to play the MP3 when it arrives.
You may want to consider pre-packaging and/or downloading the MP3 ahead of time, and refer to them direcly in the Sound parameters of your push notification.
You may follow this tutorial to see how you can play custom sounds using push notifications: https://medium.com/#dmennis/the-3-ps-to-custom-alert-sounds-in-ios-push-notifications-9ea2a2956c11
func pushNotificationHandler(userInfo: Dictionary<AnyHashable,Any>) {
// Parse the aps payload
let apsPayload = userInfo["aps"] as! [String: AnyObject]
// Play custom push notification sound (if exists) by parsing out the "sound" key and playing the audio file specified
// For example, if the incoming payload is: { "sound":"tarzanwut.aiff" } the app will look for the tarzanwut.aiff file in the app bundle and play it
if let mySoundFile : String = apsPayload["sound"] as? String {
playSound(fileName: mySoundFile)
}
}
// Play the specified audio file with extension
func playSound(fileName: String) {
var sound: SystemSoundID = 0
if let soundURL = Bundle.main.url(forAuxiliaryExecutable: fileName) {
AudioServicesCreateSystemSoundID(soundURL as CFURL, &sound)
AudioServicesPlaySystemSound(sound)
}
}

Related

How do I detect when a Quick Look panel is dismissed?

I've written a Quick Look plugin that attempts to play music like this:
OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options)
{
NSURL *fileURL = (__bridge NSURL*)url;
AudioPlayer *player = // load player with fileURL
// Create a semaphore
sema = dispatch_semaphore_create(0);
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
// Start playback and signal the semaphore once finished
[player play:^{
dispatch_semaphore_signal(sema);
}];
// Wait here until the player completion block signals the semaphore to stop waiting
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(#"%#", #"done!");
return kQLReturnNoError;
}
For various reasons, it's not practical for me to transcode these audio files into a format that macOS knows, or else I could just hand the OS an MP3 file and get the system's plugin to play it for me. So instead I'm using a dirty hack with semaphores to halt execution to keep my player object around, or else it'd abruptly stop immediately after starting playback.
The problem with that is that the file will just continue playing after the Quick Look panel stops previewing it due to the quicklookd process still running.
Is there a way to stop playback the way the system plugins do when they're dismissed?
Have you tried to use following delegate methods:
According to Apple Documentation:
func previewControllerWillDismiss(QLPreviewController)
Called before the preview controller is closed.
func previewControllerDidDismiss(QLPreviewController)
Called after the preview controller is closed.

iOS MPMoviePlayerController does not play audio after the app was relaunched on iPad

I'm trying to use MPMoviePlayerController to create an audio player without having to implement my own scrubbing and play/pause button.
I have code that records audio into NSData and saves it to disk. The method below tests audio by playing it with the MPMoviePlayerController.
The code below works (plays audio, it is heard) if I execute the method immediately after recording is done. It also works if I press home button, then return to the app.
However, when I kill the app and restart, or hit "run" from xCode, I do not hear any audio. Here are the symptoms:
The code below lists that the NSData exists on disk and has length
The path to NSData is the same both times
NSData is kAudioFormatMPEG4AAC format
The media player displays correct duration
Media player's scrubber moves from start to finish
Speaker volume is set to maximum in both cases.
No audio is heard after the app was killed and restarted.
What could be causing my MPMoviePlayerController to not provide any audio upon app relaunch? I'm writing the audio length into the file's extended attributes, could this be messing with the "Playability" of the file?
-(void)testPlayback:(AudioNote*)note
{
NSString* path = [note filepath];
if(note == nil || path.length == 0)
{
return;
}
NSURL* url = [NSURL fileURLWithPath:path];
//file exists, and data exists on disk in both cases
NSString* exists = ([note fileExists]? #"YES":#"NO");
NSUInteger length = note.fileData.length;
DLog(#"Playing note (exists: %#, data length:%i), duration: %.2f",exists,length,note.durationSeconds);
self.moviePlayer=[[MPMoviePlayerController alloc] initWithContentURL:url];
self.moviePlayer.controlStyle=MPMovieControlStyleDefault;
[self.view addSubview:self.moviePlayer.view];
[self.moviePlayer prepareToPlay];
[self.moviePlayer play];
}

Can not restart an interrupted audio input queue in background mode on iOS

I'm writing an iOS App using an AudioQueue for recording. I create an input queue configured to get linear PCM, stated this queue and everything works as expected.
To manage interruptions, I implemented the delegate methods of AVAudioSession to catch the begin and the end of an interruption. The method endInterruption looks like the following:
- (void)endInterruptionWithFlags:(NSUInteger)flags;
{
if (flags == AVAudioSessionInterruptionFlags_ShouldResume && audioQueue != 0) {
NSLog(#"Current audio session - category: '%#' mode: '%#'",
[[AVAudioSession sharedInstance] category],
[[AVAudioSession sharedInstance] mode]);
NSError *error = nil;
OSStatus errorStatus;
if ((errorStatus = AudioSessionSetActive(true)) != noErr) {
error = [self errorForAudioSessionServiceWithOSStatus:errorStatus];
NSLog(#"Could not reactivate the audio session: %#",
[error localizedDescription]);
} else {
if ((errorStatus = AudioQueueStart(audioQueue, NULL)) != noErr) {
error = [self errorForAudioQueueServiceWithOSStatus:errorStatus];
NSLog(#"Could not restart the audio queue: %#",
[error localizedDescription]);
}
}
}
// ...
}
If the app gets interrupted while it is in foreground, everything works correct. The problem appears, if the interruption happens in the background. Activating the audio session result in the error !cat:
The specified audio session category cannot be used for the attempted audio operation. For example, you attempted to play or record audio with the audio session category set to kAudioSessionCategory_AudioProcessing.
Starting the queue without activating the session results in the error code: -12985
At that point the category is set to AVAudioSessionCategoryPlayAndRecord and the mode is AVAudioSessionModeDefault.
I couldn't find any documentation for this error message, nor if it is possible to restart an input audio queue in the background.
Yes it is possible, but to reactivate the session in the background, the audio session has to either set AudioSessionProperty kAudioSessionProperty_OverrideCategoryMixWithOthers
OSStatus propertySetError = 0;
UInt32 allowMixing = true;
propertySetError = AudioSessionSetProperty (
kAudioSessionProperty_OverrideCategoryMixWithOthers,
sizeof (allowMixing),
&allowMixing
);
or the app has to receive remote control command events:
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[self becomeFirstResponder];
At the present there is no way to reactivate if you are in the background.
Have you made your app support backgrounding in the info.plist? I'm not sure if recording is possible in the background, but you probably need to add "Required Background Modes" and then a value in that array of "App plays audio"
Update I just checked and recording in the background is possible.

AVPlayer class events

Are there any delegate methods in AVPlayer class? I need to handle interruptions such as phone call etc. AVAudioPlayer supports. If AVPlayer doesn't support it, how to stream audio with AVAudioPlayer?
AVPlayer doesn't have the methods you want but you can use AVAudioSession object instead
1) Select AVAudioSession object (for example [AVAudioSession sharedInstance])
2) Set it active by calling setActive:error: method
3) Set its delegate (class implementing AVAudioSessionDelegate protocol)
4) Implement delegate's methods such as
-(void)beginInterruption;
-(void)endInterruptionWithFlags:(NSUInteger)flags;
-(void)endInterruption;
EDIT
I don't see any delegates available in AVPlayer class
So how to stream audio with AVAudioPlayer? Because we don't know how you need to stream it, and most important from where, providind some inspiration
see related questions:
stopping an AVAudioPlayer
Reusing an AVAudioPlayer for a different sound
avaudioplayer playingsong
Streaming with an AVAudioplayer
http://blog.guvenergokce.com/avaudioplayer-on-iphone-simulator/57/
http://www.iphonedevsdk.com/forum/iphone-sdk-development/15991-sample-code-avaudioplayer.html
and tutorial
http://mobileorchard.com/easy-audio-playback-with-avaudioplayer/
AVAudioPlayerDelegate Protocol Reference http://developer.apple.com/library/ios/#documentation/AVFoundation/Reference/AVAudioPlayerDelegateProtocolReference/Reference/Reference.html#//apple_ref/doc/uid/TP40008068
Responding to Sound Playback Completion
– audioPlayerDidFinishPlaying:successfully:
Responding to an Audio Decoding Error
– audioPlayerDecodeErrorDidOccur:error:
Handling Audio Interruptions
– audioPlayerBeginInterruption:
– audioPlayerEndInterruption:
– audioPlayerEndInterruption:withFlags:
I don't think AVPlayer will get you there. Take a look at AVAudioPlayerDelegate, The audioPlayerBeginInterruption would be the delegate method you are looking for.
Here's a sample of code I use for AVAudioPlayer (I'm assuming you already know how to build your url):
// Instantiates the AVAudioPlayer object, initializing it with the sound
NSError * errAV = nil;
AVAudioPlayer *newPlayer = [[AVAudioPlayer alloc] initWithContentsOfUrl: mUrl error: &errAV];
if (newPlayer == nil) {
NSString * msg = [[NSString alloc] initWithFormat:#"An internal error has occured: %#", [errAV localizedDescription]];
UIAlertView *uiav = [[UIAlertView alloc] initWithTitle:#"Play Sound"
message:msg delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[uiav show];
[uiav release];
[msg release];
} else {
self.appSoundPlayer = newPlayer;
[newPlayer release];
// "Preparing to play" attaches to the audio hardware and ensures that playback
// starts quickly when the user taps Play
[appSoundPlayer prepareToPlay];
[appSoundPlayer setVolume: 1.0];
[appSoundPlayer setDelegate: self];
[appSoundPlayer play];
}
Even when using AVAudioPlayer, you can initialize an Audio Session, where in you can specify the kind of playback (or recording, for that matter) you will be doing, and a callback for handling interruptions like phone calls.
Have a look at AudioSessionInitialize() and it's third parameter, a callback function for handling interruptions. In your callback, you can handle both the start and end of an interruption.
The salient different here, between using an AudioSession and relying on the AVAudioPlayer callbacks, is that the former occurs at a lower level, perhaps before the latter's delegate methods are called. So with the AudioSession callback, you have finer control, I think, but then you have to do more, perhaps, depending on the complexity of your app's audio setup.
It has been a long while since the question was posted. However, for the sake of completion, I would like to add: AVPlayer can be used to handle interruptions by adding a TimeObserver as follows:
When initialising the AVPlayer:
AVPlayer *_aplayer;
id _aplayerObserver;
_aplayer = [[AVPlayer alloc] initWithURL:mediaURL];
_aplayerObserver = [_aplayer addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:NULL usingBlock:^(CMTime time)
{
if (((time.value/time.timescale) >= (_aplayer.currentItem.asset.duration.value/_aplayer.currentItem.asset.duration.timescale))
{
// media file played to its end
// you can add here code that should run after the media file is completed,
// thus mimicing AVAudioPlayer's audioPlayerDidFinishPlaying event
}
else
{
if (_aplayer.rate == 0)
// audio player was interrupted
}
}
If you choose this solution, please take note of what addPeriodicTimeObserverForInterval's documentation says:
You must retain the returned value [i.e. _aplayerObserver] as long as you want the time observer to be invoked by the player. Each invocation of this method should be paired with a corresponding call to removeTimeObserver:.

sound and Nstimer stopped when iphone is in deepsleepmode?

I am creating an application in which I'm using nstimer and avaudioplayer to play sound,but both sound and timer stops when phone is in deep sleep mode.how to solve this issue?
here is the code to play audio
-(void)PlayTickTickSound:(NSString*)SoundFileName
{
//Get the filename of the sound file:
NSString *path = [NSString stringWithFormat:#"%#%#",[[NSBundle mainBundle] resourcePath],[NSString stringWithFormat:#"/%#",SoundFileName]];// #"/Tick.mp3"];
//Get a URL for the sound file
NSURL *filePath = [NSURL fileURLWithPath:path isDirectory:NO];
NSError *error;
if(self.TickPlayer==nil)
{
self.TickPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:filePath error:&error];
// handle errors here.
self.TickPlayer.delegate=self;
[self.TickPlayer setNumberOfLoops:-1]; // repeat forever
[self.TickPlayer play];
}
else
{
[self.TickPlayer play];
}
}
In order to prevent an app from going to sleep when the screen is locked, you must set your audio session to be of type kAudioSessionCategory_MediaPlayback.
Here's an example:
UInt32 category = kAudioSessionCategory_MediaPlayback;
OSStatus result = AudioSessionSetProperty(kAudioSessionProperty_AudioCategory,
sizeof(category), &category);
if (result){
DebugLog(#"ERROR SETTING AUDIO CATEGORY!\n");
}
result = AudioSessionSetActive(true);
if (result) {
DebugLog(#"ERROR SETTING AUDIO SESSION ACTIVE!\n");
}
If you don't set the audio session category, then your app will sleep.
This will only continue to prevent the app from being put to sleep as long as you continue to play audio. If you stop playing audio and the screen is still locked, the app will go to sleep and your timers will be paused.
If you want the app to remain awake indefinitely, you'll need to play a "silent" audio file to keep it awake.
I have a code example of this here: Preventing iPhone Sleep