start playing audio from a background task via AVAudioPlayer in Xcode - ios

I am trying to start playing a sound from a background task via an AVAudioPlayer that is instantiated then, so here's what I've got.
For readability I cut out all user selected values.
- (void)restartHandler {
bg = 0;
bg = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:bg];
}];
tim = [NSTimer timerWithTimeInterval:.1 target:self selector:#selector(upd) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:tim forMode:NSRunLoopCommonModes];
}
- (void)upd {
if ([[NSDate date] timeIntervalSinceDate:startDate] >= difference) {
[self playSoundFile];
[tim invalidate];
[[UIApplication sharedApplication] endBackgroundTask:bg];
}
}
- (void)playSoundFile {
NSError *sessionError = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&sessionError];
// new player object
_player = [[AVQueuePlayer alloc] init];
[_player insertItem:[AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:#"Manamana" withExtension:#"m4a"]] afterItem:nil];
[_player setVolume:.7];
[_player play];
NSLog(#"Testing");
}
Explanation: bg, tim, startDate, difference and _player are globally declared variables. I call - (void)restartHandler from a user-fired method and inside it start a 10Hz repeating timer for - (void)upd. When a pre-set difference is reached, - (void)playSoundFile gets called and initiates and starts the player. The testing NSLog at the end of the method gets called.
Now the strange thing is if I call - (void)playSoundFile when the app is active, everything works out just fine, but if I start it from the background task it just won't play any sound.
Edit
So I tried using different threads at runtime and as it seems, if the Player is not instantiated on the Main Thread this same problem will appear.
I also tried using AVPlayer and AVAudioPlayer where the AVAudioPlayer's .playing property did return YES and still no sound was playing.

From what I've learned after writing an player App, it seems that you can not start playing audio when your App is already in background for longer than X seconds, even if you have configured everything right.
To fix it, you have to use background task wisely.
The most important thing is that you must NOT call endBackgroundTask immediately after playSoundFile. Delay it for about 5-10 seconds.
Here is how I managed doing it for iOS6 and iOS7.
1, Add audio UIBackgroundModes in plist.
2, Set audio session category:
3, Create an AVQueuePlayer in the main thread and enqueue an audio asset before the App enter background.
*For continues playing in background (like playing a playlist)*
4, Make sure the audio queue in AVQueuePlayer never become empty, otherwise your App will be suspended when it finishes playing the last asset.
5, If 5 seconds is not enough for you to get the next asset you can employ background task to delay the suspend. When the asset is ready you should call endBackgroundTask after 10 seconds.

include your playSoundFile call in the below, this should always run it in main thread
dispatch_async(dispatch_get_main_queue(), ^{
});
add this if to your - (void) playSoundFile to check if player is created, if not, then create it
if(!_player) {
_player = [[AVQueuePlayer alloc] init];
}

Seems to me as if you do not have the appropriate background mode set?
Also, looks like another user had the same issue and fixed it with an Apple Docs post. Link to Post
You may need to just do the step to set the audio session as active:
[audioSession setActive:YES error:&activationError];
Hope it helps!

If you need to start playing a sound in the background (without a trick such as keeping on playing at volume zero until you need the sound):
Set Background mode Audio in your p.list;
Set AudioSession category to AVAudioSessionCategoryPlayback
Set AudioSession to active
When your backgrounded app becomes running, start a background task before playing the sound (apparently when the remaining background time is less than 10 seconds, the sound is not played)
This now works for me.

What fixed it for me was to start playing some audio just as application quits, so I added 1sec blank audio in applicationWillResignActive in the AppDelegate
func applicationWillResignActive(_ application: UIApplication) {
self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {
self.endBackgroundUpdateTask(true)
})
let secSilence = URL(fileURLWithPath: Bundle.main.path(forResource: "1secSilence", ofType: "mp3")!)
do {
audioPlayer = try AVAudioPlayer(contentsOf: secSilence)
}
catch {
print("Error in loading sound file")
}
audioPlayer.prepareToPlay()
audioPlayer.play()
}

Related

How to resume Audio using Mpmusicplayer after pause audio in swift 4.1(IOS 11)

I am working on Audio playing app using "MpmusicPlayer" and want to resume it after paused.
I am using "self.appMusicPlayer.currentPlaybackTime" but it is not working in swift 4.1 (IOS 11).
Is there have any other way to resume audio after pause using MpMusicPlayer?
It seems that this is a problem with iOS 11 that some people have run into when setting the currentPlaybackTime after calling play(). There was a work around posted on this apple dev forum here, but please find the code below (thanks to RunLoop).
I had just run into this exact problem. I managed to get around it
with the following code.
It creates a background thread that continuously checks the
currentPlaybackTime of the player. As soon as currentPlaybackTime
is not the time I set it to be I set currentPlaybackTime back to
what I wanted.
It feels like a terrible hack but it's working for me so far.
MPMusicPlayerController *player = [MPMusicPlayerController systemMusicPlayer];
player.currentPlaybackTime = _startTime;
[player play];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(queue, ^{
while(true) {
if (player.currentPlaybackTime != _startTime) {
player.currentPlaybackTime = _startTime;
break;
}
}
});

iOS pause between songs to exec code then resume, when app is in background

I am building an app which needs to play a track list, but between each song the music should pause to execute some code, then once complete the music should resume. This needs to work when the app is in the background as well as in the foreground.
I have tried a couple of methods but none seem to be able to do everything I want.
AVQueuePlayer - I can't seem to identify when any one song has stopped, only when the whole queue has stopped.
AVPlayer - I can identify when the track has ended with a notification, then I can run my extra code then load the next track. This works fine as long as the app is not in the background, when the app is in the background the code executes fine except the [avPlayer play] command does not work. It does not throw an error, it simply does not play. I know it has moved to the next song and loaded it into AVPlayer as I output the meta data and it has moved on.
Just to be clear the initial track does run in the background, it is only starting the next track which does not run in the background.
Code below...
Any idea what I am doing wrong?
Thanks!
+(void) playItem {
//get the play item from the song array based on intSongIndex
MPMediaItem *currentSong = [songsNowPlaying objectAtIndex:intSongIndex];
AVPlayerItem * currentItem = [AVPlayerItem playerItemWithURL:[currentSong valueForProperty:MPMediaItemPropertyAssetURL]];
[avPlayer replaceCurrentItemWithPlayerItem:currentItem];
//add notification to the currentItem
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:currentItem];
//play
[avPlayer play];
NSArray *metadataList = [[avPlayer currentItem].asset commonMetadata];
for (AVMetadataItem *metaItem in metadataList) {
NSLog(#"%#: %#",[metaItem commonKey], [metaItem value]);
}
//increment song index so next time the next song is selected
intSongIndex ++;
if (intSongIndex >= songsNowPlaying.count) {
intSongIndex = 0;
}
}
+ (void)playerItemDidReachEnd:(NSNotification *)notification {
//add code to be executed before the next song plays
//call playItem to play the next song
[self playItem];
}
Solved, this needed adding to the initial viewDidLoad
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];

NSTimer stops firing in Background after some time

Hey I am developing an app in which i have to make API call every 30 sec, so i created NSTimer for it.
But when my app goes into background timer stops firing after 3-4 minutes. So it works only 3-4 minutes in background,but not after that. How can i modify my code so that timer would not stop.
Here is some of my code.
- (IBAction)didTapStart:(id)sender {
NSLog(#"hey i m in the timer ..%#",[NSDate date]);
[objTimer invalidate];
objTimer=nil;
UIBackgroundTaskIdentifier bgTask = UIBackgroundTaskInvalid;
UIApplication *app = [UIApplication sharedApplication];
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
[app endBackgroundTask:bgTask];
}];
objTimer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:self
selector:#selector(methodFromTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:objTimer forMode:UITrackingRunLoopMode];
}
-(void)methodFromTimer{
[LOG debug:#"ViewController.m ::methodFromTimer " Message:[NSString stringWithFormat:#"hey i m from timer ....%#",[NSDate date] ]];
NSLog(#"hey i m from timer ....%#",[NSDate date]);
}
I even changed the code with the following:
[[NSRunLoop mainRunLoop] addTimer:objTimer forMode:NSRunLoopCommonModes];
This didn't work either.
Don't create UIBackgroundTaskIdentifier task as local and make it global as below:
Step -1
Step -2
Step -3
Step -4
As local one loose scope and global one won't ,and I created a demo and ran it for sometime with 1 sec repeating timer ,and worked smooth.
Still if u face issue pls let me know.
I ran again demo and here are logs of it running.
So its working fine and more than 3 minutes. Also that 3 minute logic is right but as uibackgroundtask is initiated so it shouldn't let it kill this task of timer.
Edited Part:-
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
[app endBackgroundTask:bgTask]; //Remove this line and it will run as long as timer is running and when app is killed then automatically all vairbles and scopes of it are dumped.
}];
Check it and let me know if it works out or not.
Hey I run ur code and I reached the expirationHandler but after released debug point ,the timer was running smooth.
No, don't do background tasks with NSTimer. It will not work as you might expect. You should be making use of background fetch APIs provided by Apple only. You can set the duration at which you want it to be called in that API. Though usually it is not recommended setting duration of the call you would like to make. Take a look at this apple background programming documentation
Also, to get you started quickly, you can follow this Appcoda tutorial
This worked for me, so I'm adding it to StackOverflow for any future answer seekers.
Add the following utility method to be called before you start your timer. When we call AppDelegate.RestartBackgroundTimer() it will ensure that your app will remain active - even if it's in the background or if the screen is locked. However, this will only ensure that for 3 minutes (as you mentioned):
class AppDelegate: UIResponder, UIApplicationDelegate {
static var backgroundTaskIdentifier: UIBackgroundTaskIdentifier? = nil;
static func RestartBackgroundTimer() {
if (AppDelegate.backgroundTaskIdentifier != nil) {
print("RestartBackgroundTimer: Ended existing background task");
UIApplication.sharedApplication().endBackgroundTask(AppDelegate.backgroundTaskIdentifier!);
AppDelegate.backgroundTaskIdentifier = nil;
}
print("RestartBackgroundTimer: Started new background task");
AppDelegate.backgroundTaskIdentifier = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({
UIApplication.sharedApplication().endBackgroundTask(AppDelegate.backgroundTaskIdentifier!);
AppDelegate.backgroundTaskIdentifier = nil;
})
}
}
Also, when starting your app, ensure the following runs. It will ensure that audio is played even if the app is in the background (and while you're at it, also ensure that your Info.plist contains "Required background modes" and that "App plays audio or streams audio/video using AirPlay" a.k.a. "audio" is in its collection):
import AVFoundation;
// setup audio to not stop in background or when silent
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback);
try AVAudioSession.sharedInstance().setActive(true);
} catch { }
Now, in the class that needs the timer to run more than 3 minutes (if in the background), you need to play a sound when only 30 seconds remains of background time. This will reset the background time remaining to 3 minutes (just create a "Silent.mp3" with e.g. AudaCity and drag & drop it to your XCode project).
To wrap it all up, do something like this:
import AVFoundation
class MyViewController : UIViewController {
var timer : NSTimer!;
override func viewDidLoad() {
// ensure we get background time & start timer
AppDelegate.RestartBackgroundTimer();
self.timer = NSTimer.scheduledTimerWithTimeInterval(0.25, target: self, selector: #selector(MyViewController.timerInterval), userInfo: nil, repeats: true);
}
func timerInterval() {
var bgTimeRemaining = UIApplication.sharedApplication().backgroundTimeRemaining;
print("Timer... " + NSDateComponentsFormatter().stringFromTimeInterval(bgTimeRemaining)!);
if NSInteger(bgTimeRemaining) < 30 {
// 30 seconds of background time remaining, play silent sound!
do {
var audioPlayer = try AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("Silent", ofType: "mp3")!));
audioPlayer.prepareToPlay();
audioPlayer.play();
} catch { }
}
}
}
It is normal behavior.
After iOS7, you got exactly 3 minutes of background time. Before that there was 10 minutes if i remember correctly. To extend that, your app needs to use some special services like location, audio or bluetooth which will keep it "alive" in the background.
Also, even if you use one of these services the "Background app refresh" setting must be enabled on your device for the app.
See this answer for details or the background execution part of the documentatio.

iOS AVAudioPlayer does not play while finger panning MKMapView

I have a basic beep sound that I loaded with AVAudioPlayer in my app.
It can play the beep fine if my finger isn't panning my MKMapView.
My beep is set to play every 2 seconds.
When I start panning the map view, and don't take my finger off the screen, the beep stops playing.
I recall NSUrlConnection also doesn't fire while scrolling a table view, I thought this might be the same problem but I couldn't figure out how I can add my audio player to the proper run loop.
I setup my player like this:
-(void)setupBeep
{
NSURL *url = [NSURL fileURLWithPath:[NSString stringWithFormat:#"%#/beep.mp3", [[NSBundle mainBundle] resourcePath]]];
NSError *error;
audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
audioPlayer.numberOfLoops = 0;
if(error)
{
NSLog(#"Error opening sound file: %#", [error localizedDescription]);
}
}
and I am playing my sound like this:
// plays a beeping sound
-(void)beep
{
[audioPlayer play];
}
Anyone came across this problem before?
How have you scheduled your -beep method to be called?
I suspect you’ve added an NSTimer to the main run loop in the default input mode. The reason you’re not hearing anything is that while MKMapView is tracking your finger, the run loop is in not in NSDefaultRunLoopMode—it’s in UITrackingRunLoopMode.
Try scheduling in NSRunLoopCommonModes, which includes both default and tracking modes:
NSTimer *timer = [NSTimer timerWithTimeInterval:interval target:self selector:#selector(beep) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Also, be aware that NSTimer retains its target. A repeating timer will reschedule itself automatically, so you’ll need to call its -invalidate method to remove it from the loop and allow the target to be released.
EDIT: Check out Apple’s Threading Programming Guide: Run Loops for a much more detailed explanation.

AVAudioPlayer only plays a custom audio file for one second

In my application I am using an AVAudioRecorder object to allow the user to record a sound file, and when the user is done recording they can play the sound via the AVAudioPlayer. The issue I'm having is when the user tries to play the audio file it only plays for one second. What is even more odd is that this code worked perfect on iOS 5, but since upgrading to iOS 6 this issue has started to happen.
I have checked the file that is being created via iTunes file sharing, and I'm able to play the entire sound file on my desktop.
Below I have pasted the code from my manipulation file that loads the audio player. From what I can tell the
- (void)playRecordingBtnClk:(id)sender
{
NSLog(#"Play btn clk");
if (!isRecording) {
// disable all buttons
[_recordButton disableButton];
[_stopButton disableButton];
[_playButton disableButton];
[_saveButton disableButton];
// create the player and play the file from the beginning
if (audioPlayer) {
[audioPlayer release];
audioPlayer = nil;
}
audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:audioRecorder.url error:&error];
audioPlayer.delegate = self;
audioPlayer.currentTime = 0.0;
[audioPlayer prepareToPlay];
if (error) {
NSLog(#"Error Playing Audio File: %#", [error debugDescription]);
return;
}
[audioPlayer play];
[self startTicker];
}
}
It appears that the
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
method is being called after one second which is why the audio player stops. Anyone have any ideas what is going on here?
For iOS 6.0, add two more lines:
soundPlayer.currentTime = soundPlayer.duration + soundPlayer.duration;
soundPlayer.numberOfLoops = 1;
Not a perfect solution. We may need to wait for iOS 6.0.1/6.1 update.
For more, please visit this, someone commented there, saying it might be a problem of CDAudioManager.

Resources