24

I have done a lot of research, both on Google and StackOverflow. All the answers I found do not work in iOS 7. I started writing fresh app in iOS 7 SDK with Xcode 5.

All I'm trying to do is play audio in the app from a file stored in the app bundle (not from the Music library). I want to have audio played in background and controlled when screen is locked (in addition to Control Center).

I set the APPNAME-Info.plist key, UIBackgroundModes, to audio. It is not handling things in the app delegate; everything is done inside the ViewController

@interface ViewController : UIViewController <AVAudioPlayerDelegate>

Within the implementation's viewDidAppear: method I call super and then the following code:

// Once the view has loaded then we can register to begin receiving controls and we can become the first responder
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[self becomeFirstResponder];

In my implementation's viewWillDisappear: method, I have the following code:

// End receiving events
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
[self resignFirstResponder];

I have also implemented the canBecomeFirstResponder method, which returns YES. Next, I implemented the remoteControlReceivedWithEvent: method:

- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
    // If it is a remote control event handle it correctly
    if (event.type == UIEventTypeRemoteControl) {
        if (event.subtype == UIEventSubtypeRemoteControlPlay) {
            [self playPauseAudio:self];
        } else if (event.subtype == UIEventSubtypeRemoteControlPause) {
            [self playPauseAudio:self];
        } else if (event.subtype == UIEventSubtypeRemoteControlTogglePlayPause) {
            [self playPauseAudio:self];
        }
    }
}

What is confusing me is that this exact same setup was working fine on iOS 6. On iOS 7, it doesn't work. It used to be so easy in iOS 6. Something fundamentally changed in iOS 7 SDK. What am I missing?

Sam Spencer
  • 8,492
  • 12
  • 76
  • 133
codejunkie
  • 1,122
  • 3
  • 13
  • 29

5 Answers5

16

I managed to solve this, and to save hair pulling by another poor soul here goes:

Firstly make sure your Info.plist correctly lists audio as a background mode.

(If you dont know what i'm talking about select YOURAPPNAME-Info.plist select that. Click oin the plus sign and add a new key called UIBackgroundModes and expand it. Add a value called audio.)

You'll need a reference to whatever playback object is creating the audio. Since I'm only playing audio and AVplayer was not abiding by the background audio, use this in your view controller's header:

@property (nonatomic, retain) MPMoviePlayerController *audioPlayer;

In the implementation, do the following:

 [super viewDidAppear:animated];

    //Once the view has loaded then we can register to begin recieving controls and we can become the first responder
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    [self becomeFirstResponder];

and

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];

    //End recieving events
    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
    [self resignFirstResponder];

add two methods

//Make sure we can recieve remote control events
- (BOOL)canBecomeFirstResponder {
    return YES;
}

- (void) registerForAudioObjectNotifications {

    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];

    [notificationCenter addObserver: self
                           selector: @selector (handlePlaybackStateChanged:)
                               name: MixerHostAudioObjectPlaybackStateDidChangeNotification
                             object: audioObject];
}

now ALL important code - this enables your app to control audio from "control center" and from lock screen:

- (void) remoteControlReceivedWithEvent: (UIEvent *) receivedEvent {

    if (receivedEvent.type == UIEventTypeRemoteControl) {

        switch (receivedEvent.subtype) {

            case UIEventSubtypeRemoteControlTogglePlayPause:
                [self playOrStop: nil];
                break;

            default:
                break;
        }
    }
}

you can add many many types of Event types here and call any method.

Typical events are:

 UIEventSubtypeRemoteControlPlay                 = 100,  //Parent EVENT

// All below are sub events and you can catch them using switch or If /else.
    UIEventSubtypeRemoteControlPause                = 101,
    UIEventSubtypeRemoteControlStop                 = 102,
    UIEventSubtypeRemoteControlTogglePlayPause      = 103,
    UIEventSubtypeRemoteControlNextTrack            = 104,
    UIEventSubtypeRemoteControlPreviousTrack        = 105,
    UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
    UIEventSubtypeRemoteControlEndSeekingBackward   = 107,
    UIEventSubtypeRemoteControlBeginSeekingForward  = 108,
    UIEventSubtypeRemoteControlEndSeekingForward    = 109,

To Debug help you can use:

 MPMoviePlayerController *mp1= (MPMoviePlayerController *)[notification object];
    NSLog(@"Movie State is: %d",[mp1 playbackState]);

    switch ([mp1 playbackState]) {
        case 0:
            NSLog(@"******* video has stopped");
            break;
        case 1:
            NSLog(@"******* video is playing after being paused or moved.");
                       break;
        case 2:
            NSLog(@"******* video is paused");
                break;
        case 3:
            NSLog(@"******* video was interrupted");
            break;
        case 4:
            NSLog(@"******* video is seeking forward");
                     break;
        case 5:
            NSLog(@"******* video is seeking Backwards");
            break;
        default:
            break;

and thats it - hope it helps some one out there! - this is working perfect on iOS 7 and iOS 6 with Storyboard app as well as control using Headphone and all new control centre too.

Moshe
  • 57,511
  • 78
  • 272
  • 425
codejunkie
  • 1,122
  • 3
  • 13
  • 29
  • 1
    Unfortunately this still does not work for me from the control center. This is the exact same code I was fruitfully using on iOS6, but with iOS7 something broke. Actually I've also seen that other apps such as StereoMood and 8Tracks have the same problem. – super Sep 28 '13 at 16:50
  • @codejunkie: I had the same experience as super did. On iOS 7.0.3 it is still the same. I did most of the steps as you listed except that my responder code is in AppDelegate and I don't have an audio object. Control centre simply does not call remoteControlReceivedWithEvent:. Note that my code works for all pre-iOS7 occasions and still works for the earbud remote-control. See my question here please: http://stackoverflow.com/questions/19686704/how-to-block-ios-7-control-centre-from-controlling-music-app – kakyo Oct 30 '13 at 15:28
  • Actually it started working again after a reset of my iPhone. Now on iOS 7.0.3 keeps working with no problems. That's really something weird with iOS7. – super Nov 05 '13 at 14:39
  • 1
    What does MixerHostAudioObjectPlaybackStateDidChangeNotification has to do with receiving remove events? – ambientlight Aug 01 '14 at 06:32
7

Apparently the problem was on Apple's side as iOS update 7.0.3 fixes this issue. Besides what Alex noted about UIEventSubtype changes the code that worked on iOS6 now works on iOS7.

For sake of completeness, here is my relevant code that is working in both iOS6 and iOS7 - after the udpate to 7.0.3. Also included AVFoundation.framework and MediaPlayer.framework in project Build Phases -> Link binary with libraries. No code for this in app delegate.

In viewcontroller .h file:

#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>

@interface NewsDetailViewController : UIViewController <UIWebViewDelegate, AVAudioSessionDelegate>
@property (nonatomic) MPMoviePlayerController *audioPlayer;

In viewcontroller .m file:

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.audioPlayer = [[MPMoviePlayerController alloc] initWithContentURL:audioUrl];
    [self.audioPlayer prepareToPlay];
    [self.audioPlayer.view setFrame:CGRectMake(0, 0, self.audioView.frame.size.width,     42)];
    self.audioPlayer.view.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    [self.audioView addSubview:self.audioPlayer.view];
    [self.audioPlayer play];

    NSError *setCategoryError = nil;
    NSError *activationError = nil;
    [[AVAudioSession sharedInstance] setActive:YES error:&activationError];
    [[AVAudioSession sharedInstance] setDelegate:self];
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&setCategoryError];
}

- (BOOL)canBecomeFirstResponder {
    return YES;
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    [self becomeFirstResponder];
}

- (void)viewWillDisappear:(BOOL)animated {
    [self.audioPlayer stop];
    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
    [self resignFirstResponder];

    [super viewWillDisappear:animated];
}

- (void)remoteControlReceivedWithEvent:(UIEvent *)receivedEvent {

    if (receivedEvent.type == UIEventTypeRemoteControl) {
        switch (receivedEvent.subtype) {
            case UIEventSubtypeRemoteControlPlay:
                [self.audioPlayer play];
                break;
            case UIEventSubtypeRemoteControlPause:
                [self.audioPlayer pause];
                break;
            case UIEventSubtypeRemoteControlTogglePlayPause:
                if (self.audioPlayer.playbackState == MPMoviePlaybackStatePlaying) {
                    [self.audioPlayer pause];
                }
                else {
                    [self.audioPlayer play];
                }
                break;
            default:
                break;
        }
    }
}
6

If you want to play audio in background in iphone and simulator also then you need to write this code in plist and Firstly make sure your Info.plist correctly lists audio as a background mode.

(If you dont know what i'm talking about select YOURAPPNAME-Info.plist select that. Click on the plus sign and type a key UIBackgroundModes and enter. Add a value called "App plays audio"(for simulator) or "App plays audio or streams audio/video using AirPlay"(For iphone).)

enter image description here

in AppDelegate.m

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    __block UIBackgroundTaskIdentifier task = 0;
    task=[application beginBackgroundTaskWithExpirationHandler:^{
    NSLog(@"Expiration handler called %f",[application backgroundTimeRemaining]);
    [application endBackgroundTask:task];
    task=UIBackgroundTaskInvalid;
    }];
}

Add these two framework in your project and some line of code in ViewController.h

#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>

@interface ViewController : UIViewController <UIWebViewDelegate, AVAudioSessionDelegate>
@property (nonatomic) MPMoviePlayerController *audioPlayer;

Remind that these frameworks refrences should be added in your project.

Then in Viewcontrller.m

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    [self becomeFirstResponder];
}

- (void)viewWillDisappear:(BOOL)animated {
    [self.audioPlayer stop];
    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
    [self resignFirstResponder];

    [super viewWillDisappear:animated];
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    NSURL *audioUrl = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"songName" ofType:@"mp3"]];
    self.audioPlayer = [[MPMoviePlayerController alloc] initWithContentURL:audioUrl];
    [self.audioPlayer prepareToPlay];
    [self.audioPlayer.view setFrame:CGRectMake(0, 0, self.view.frame.size.width-100,     42)];
    self.audioPlayer.view.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    [self.view addSubview:self.audioPlayer.view];
    [self.audioPlayer play];


    // for run application in background
    NSError *setCategoryError = nil;
    NSError *activationError = nil;
    [[AVAudioSession sharedInstance] setActive:YES error:&activationError];
    [[AVAudioSession sharedInstance] setDelegate:self];
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&setCategoryError];
}

I hope it will help you to play audio in background in iphone and simulator as well.

chandan
  • 2,453
  • 23
  • 31
  • modifying appdelegate is not good code practice and we should try to stay away from appdelegate as this relies on OS when app is first loaded and takes away the control from app controllers more read here: http://www.hollance.com/2012/02/dont-abuse-the-app-delegate/ – codejunkie Feb 12 '14 at 08:34
  • working perfectly in background,,, what about when view is changed? – Noor Apr 08 '15 at 10:07
  • i have pop back to another view but want to play same audio from previous view – Noor Apr 08 '15 at 10:13
4

One thing to note that is different from iOS6 to iOS7 with remote control events is that in iOS6 the play/pause events came as one UIEventSubtype:
UIEventSubtypeRemoteControlTogglePlayPause
And in iOS7 they come as two separate subtypes:
UIEventSubtypeRemoteControlPause
UIEventSubtypeRemoteControlPlay

0

Since I am also interested in finding this solution I will add some more information from my side.

I am experiencing the same problem, but still Apple documentation hasn't changed about remote control event management.

I tried moving some stuff and something interesting happened.

I originally had the remote control event management in my TabBar controller. Now that i moved everything in the player view controller, I can see that I can again control the music playback with my headset but not from the buttons in the new control panel of iOS7 (the one that come from the bottom on the screen).

This is quite weird.

In order to solve the problem, let's try to enrich the thread with our test.

super
  • 411
  • 3
  • 16
  • I'm currently testing and finding work around using MPMPlayerViewController and registering with its notification object and obtaining that state will update once i have all working. – codejunkie Sep 26 '13 at 18:48
  • I am not using a MPPlayer, so I cannot register for those notification. It's crazy because I solved the problem just moving everything in the AppDelegate, but now it's not working anymore. – super Oct 08 '13 at 16:12