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

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.

Related

How to trigger AVAudioPlayer playback using a remote push notification?

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)
}
}

AVAudioPlayer on background thread

I know very little about using background threads, but this seems to play my sound in the way that I need it to as follows:
1) I need this very short sound effect to play repeatedly even if the sound overlaps.
2) I need the sound to be played perfectly on time.
3) I need the loading of the sound to not affect the on-screen graphics by stuttering.
I am currently just trying out this method with one sound, but if successful, I will roll it out to other sound effects that need the same treatment. My question is this: Am I using the background thread properly? Will there be any sort of memory leaks?
Here's the code:
-(void) playAudio {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSString *path = [NSString stringWithFormat:#"%#/metronome.mp3", [[NSBundle mainBundle] resourcePath]];
NSURL *metronomeSound = [NSURL fileURLWithPath:path];
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:metronomeSound error:nil];
[_audioPlayer prepareToPlay];
[_audioPlayer play];
});
}
//handles collision detection
-(void) didBeginContact:(SKPhysicsContact *)contact {
uint32_t categoryA = contact.bodyA.categoryBitMask;
uint32_t categoryB = contact.bodyB.categoryBitMask;
if (categoryA == kLineCategory || categoryB == kLineCategory) {
NSLog(#"line contact");
[self playAudio];
}
}
I use the AVAudioPlayer and use it asynchronously and in background threads without any problems and no leaks as far as I can tell. However, I have implemented a singleton class that handles all the allocations and keeps an array of AVAudioPlayer instances that also play asynchronously as needed. If you need to play a sound repeatedly, you should allocate an AVAudioPlayer instance for every time you want to play it. In that case, latency will be negligible and you can even play the same sound simultaneously.
Concerning your strategy I think it needs some refinements, in particular if you want to prevent any delays. The main problem is always reading from disk, which is the slowest operation of all and your limiting step.
Thus, I would also implement an array of AVAudioPlayers each already initialized to play a specific sound, in particular if this sound is played often and repeatedly. You could remove those instances of players that are played less often from the array if memory starts to grow and reload them a few seconds before if you can tell which ones will be needed.
And one more thing... Don't forget to lock and unlock the array, if you are going to access it from multiple threads or better yet, create a GCD queue to handle all accesses to the array.

Why does iOS not inform my application about audio session interruptions?

I am using AVPlayer to play sound from different sources (including iPod music library). Due to the fact that AVPlayer is more low level AVAudioPlayer I have to handle interruptions myself. Using AVAudioPlayer is not an option!
In the Apple developer documents they mention to either listen AVAudioSessionInterruptionNotificationor setup a listener with AudioSessionInitialize. But when doing so I only receive notifications when the interruption ended, but due to their documents my app should be able to handle both.
I am using the following code to initialize my audio session: (simplified version, removed unimportant lines)
AudioSessionInitialize(NULL, NULL, ARInterruptionListenerCallback, nil);
UInt32 sessionCategory = kAudioSessionCategory_MediaPlayback;
AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, sizeof(sessionCategory), &sessionCategory);
AudioSessionSetActive(true);
And here is how the listener looks like:
void ARInterruptionListenerCallback(void *inUserData, UInt32 interruptionState) {
if (interruptionState == kAudioSessionBeginInterruption) {
// Here is the code which is never called...
}
else if (interruptionState == kAudioSessionEndInterruption) {
// But this case will be executed!
}
}
Btw. I am expecting this code executed when there is an interruption like a phone call or similar. Am I misunderstand what Apple declares as interruption?
There is a bug in iOS 6. The interrupt code never seems to be called for begin interrupt - That includes both the old deprecated method (using AVAudioSession delegate) and the new notification method.
Myself and a few others have raised a high severity bug request with Apple so I suggest you do the same (bugreport.apple.com) to get their attention.
I'm not sure what you're using the interrupt for, but I was using it to allow audio to resume after the interrupt if necessary, and also update the play/pause button in the interface. I had to change my method to watch the AVPlayer rate property to toggle the play/pause button, and not allow audio to resume after an interruption.
Edit: see this thread

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