I have a master-detail app. Tapping on each cell of the tableView in the MasterViewController navigates to a DetailViewController where you can download and play an audio file. I had problems keeping the AVAudioPlayer instance, single, so that different files from multiple cells wouldn't play at the same time. Before fixing that, my remoteControlReceivedWithEvent was working fine and I could manage events on all view controllers.
Fixing that issue now, my player plays only one file at the same time, so if I happen to navigate back to the MasterViewController and then select another row and enter a different cell and start playing that, it will stop the first file and start the second. BUT the problem at the moment is that trying to use the control center while the user is on else where out of the DetailViewController, remoteControlReceivedWithEvent is called, but the value is nil, so nothing happens. This is while I can detect which row is now playing in my cellForRowAtIndexPath: and highlight it on the MasterViewController as follows:
if (entry.audioManager && [entry.audioManager soundPlayer].isPlaying)
{
equalizer.hidden = NO;
[self animateTheEqualizer];
}
However I can't spot that in remoteControlReceivedWithEvent since I don't have any access to the indices.
I have already included canBecomeFirstResponder and so on, and I'm sure that it's only the confusion of the table view understanding which file is currently being played. Everything works fine if I try this in the DetailViewController but not outside it in another VC or even in the background.
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
//if it is a remote control event handle it correctly
if (event.type == UIEventTypeRemoteControl)
{
if (event.subtype == UIEventSubtypeRemoteControlPlay)
{
[entry.audioManager playAudio];
// [[NIKDetailViewController sharedController].feedEntry.audioManager playAudio];
}
else if (event.subtype == UIEventSubtypeRemoteControlPause)
{
[entry.audioManager pauseAudio];
}
else if (event.subtype == UIEventSubtypeRemoteControlTogglePlayPause)
{
[entry.audioManager togglePlayPause];
}
else if (event.subtype == UIEventSubtypeRemoteControlBeginSeekingBackward)
{
[entry.audioManager rewindTheAudio];
}
else if (event.subtype == UIEventSubtypeRemoteControlBeginSeekingForward)
{
[entry.audioManager fastForwardTheAudio];
}
}
}
and in my AudioManager class:
- (void)playAudio
{
//Play the audio and set the button to represent the audio is playing
if ([soundPlayer isPlaying])
{
[soundPlayer pause];
[self deleteTimer];
}
else
{
[self createTimer];
[soundPlayer play];
}
[self refreshButton];
// [[NIKMasterViewController sharedController] markEntryAsPlaying:detailViewController.feedEntry];
}
- (void)pauseAudio
{
//Pause the audio and set the button to represent the audio is paused
[soundPlayer pause];
[self deleteTimer];
[self refreshButton];
}
Should I markAsNowPlaying manually or is there a delegate method I could use for this to spot the right index and avoid getting nil?
Ok... Problem finally solved! It was something totally different that I needed to do. I'm just using the wrong object entry.audioManager. I was supposed to use currentAudioManager which is an extern declared in AudioManager class. That would keep the currently-played-file and won't be nil.
Related
In an app I have a player object that has an AVQueuePlayer property to play audio files. In my player object I have all the code necessary to handle AVAudioSessionInterruptionNotification as below:
- (void)p_audioSessionInterruption:(NSNotification *)notification
{
NSUInteger interruptionType = [[[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
if (interruptionType == AVAudioSessionInterruptionTypeBegan) {
if (self.isPlaying) {
self.interruptedWhilePlaying = YES;
[self p_pauseAfterInterruption];
}
return;
}
if (interruptionType == AVAudioSessionInterruptionTypeEnded) {
if (self.interruptedWhilePlaying) {
self.interruptedWhilePlaying = NO;
[self p_playAfterInterruption];
}
return;
}
}
The self.isPlaying is a read-only property that is:
- (BOOL)isPlaying
{
return self.queueplayer.rate != 0;
}
In iOS 8 this all seems to be working just fine. In iOS 9, though, it seems "something" get called prior to handling the notification and alters the AVQueuePlayer rate property to 0. So when checking if playing in the AVAudioSessionInterruptionTypeBegan case the player does not gets paused after interruption and when interruption ends the interruptedWhilePlaying property is NO so does not gets resumed.
I have added KVO observation to rate property of AVQueuePlayer trying to find out what changes the rate but could not find anything enlightening. I provide the observeValueForKeyPath stacktrace in the screenshot.
Any help would be much appreciated. Thank you in advance.
I try to control that the user may advance seconds in song playback. I managed only for users can see the playhead but not interact with it.
I am using AVAudioSession in mode with AVAudioSessionCategoryPlayback. AVPlayer and AVPlayerItem
_playerItem = [AVPlayerItem playerItemWithURL:url];
_player = [AVPlayer playerWithPlayerItem:self.playerItem];
[EDIT] This way, I control the remote events:
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
//if it is a remote control event handle it correctly
if (event.type == UIEventTypeRemoteControl)
{
if (event.subtype == UIEventSubtypeRemoteControlPlay) {
NSLog(#"Responds nice to the event UIEventSubtypeRemoteControlPlay");
} else if (event.subtype == UIEventSubtypeRemoteControlPause) {
NSLog(#"Responds nice to the event UIEventSubtypeRemoteControlPause");
} else if (event.subtype == UIEventSubtypeRemoteControlTogglePlayPause) {
NSLog(#"Responds nice to the event UIEventSubtypeRemoteControlTogglePlayPause");
} else if (event.subtype == UIEventSubtypeRemoteControlBeginSeekingBackward) {
NSLog(#"NEVER Responds!!!");
} else if (event.subtype == UIEventSubtypeRemoteControlBeginSeekingForward) {
NSLog(#"NEVER Responds!!!");
} else if (event.subtype == UIEventSubtypeRemoteControlNextTrack){
NSLog(#"Responds nice to the event UIEventSubtypeRemoteControlNextTrack");
} else if (event.subtype == UIEventSubtypeRemoteControlPreviousTrack){
NSLog(#"Responds nice to the event UIEventSubtypeRemoteControlPreviousTrack");
} else if (event.subtype == UIEventSubtypeRemoteControlStop){
NSLog(#"Responds nice to the event UIEventSubtypeRemoteControlStop");
}
}
}
How do I active the bar in remote controls (see screenshot) running in backgroud audio for doing it (iPhone blocked). Not even know if it is possible.
¿Is it possible to do it? In 'Music native App iOS' works fine!
The AVPlayer class has a seekToTime: method that will do what you want.
In your progress bar callback multiply the percentage represented by the progress bar position by the total media time and pass that value to seekToTime:.
I've implemented an audio player using AVAudioPlayer (not AVPlayer). I'm able to handle the remote control events with the following method. It works quite alright so far, however I see two more subtypes for these events: UIEventSubtypeRemoteControlEndSeekingForward and UIEventSubtypeRemoteControlEndSeekingBackward.
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
//if it is a remote control event handle it correctly
if (event.type == UIEventTypeRemoteControl)
{
if (event.subtype == UIEventSubtypeRemoteControlPlay)
{
[self playAudio];
}
else if (event.subtype == UIEventSubtypeRemoteControlPause)
{
[self pauseAudio];
}
else if (event.subtype == UIEventSubtypeRemoteControlTogglePlayPause)
{
[self togglePlayPause];
}
else if (event.subtype == UIEventSubtypeRemoteControlBeginSeekingBackward)
{
[self rewindTheAudio]; //this method rewinds the audio by 15 seconds.
}
else if (event.subtype == UIEventSubtypeRemoteControlBeginSeekingForward)
{
[self fastForwardTheAudio]; //this method fast-forwards the audio by 15 seconds.
}
}
So the questions:
In order to have things work right, am I supposed to implement those two subtypes, too?
This method only enables the rewind, play/pause, and fast forward buttons on lock screen, but it doesn't display the file title, artwork, and duration. How can I display that info using AVAudioPlayer or AVAudioSession (I don't really want one more library/API to implement this)?
2-a. I discovered MPNowPlayingInfoCenter while searching and I don't know much about it. Do I have to use it to implement those stuff above? :-[
You are correct, MPNowPlayingInfoCenter is the only way to do this. So go ahead and link with MediaPlayer.framework. In the class that handles playing tracks, import <MediaPlayer/MediaPlayer.h>. Whenever your track changes, do this:
NSDictionary *info = #{ MPMediaItemPropertyArtist: artistName,
MPMediaItemPropertyAlbumTitle: albumName,
MPMediaItemPropertyTitle: songName };
[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = info;
I have realized representation of my AVPlayer on iPhone lock screen via MPNowPlayingInfoCenter. But I can't found how to add ±15 seconds rewind buttons like in standard Music app.
So the question is How to add this buttons on lock screen?
I'm using AVAudioPlayer at the moment, but the remote controlling method which is - (void)remoteControlReceivedWithEvent:(UIEvent *)event must not be involved with the type of the player you're using.
Follow this:
In your view controller's viewDidLoad method add the following code:
//Make sure the system follows our playback status - to support the playback when the app enters the background mode.
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
[[AVAudioSession sharedInstance] setActive: YES error: nil];
Then add these methods:
viewDidAppear:: (if not implemented already)
- (void)viewDidAppear:(BOOL)animated {
[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];
}
viewWillDisappear: (if not implemented already)
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
//End recieving events
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
[self resignFirstResponder];
}
And:
//Make sure we can recieve remote control events
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
//if it is a remote control event handle it correctly
if (event.type == UIEventTypeRemoteControl)
{
if (event.subtype == UIEventSubtypeRemoteControlPlay)
{
[self playAudio];
}
else if (event.subtype == UIEventSubtypeRemoteControlPause)
{
[self pauseAudio];
}
else if (event.subtype == UIEventSubtypeRemoteControlTogglePlayPause)
{
[self togglePlayPause];
}
else if (event.subtype == UIEventSubtypeRemoteControlBeginSeekingBackward)
{
[self rewindTheAudio]; //You must implement 15" rewinding in this method.
}
else if (event.subtype == UIEventSubtypeRemoteControlBeginSeekingForward)
{
[self fastForwardTheAudio]; //You must implement 15" fast-forwarding in this method.
}
}
}
This is working fine in my app, however if you want to be able to receive remote control events in all view controllers, then you should set it in the AppDelegate.
I want to implement the following things,
App is running a music or video (using MPMoviePlayerController) in background.
User double clicks the home button and go to the first screen showing playback controls (fast rewind, play or pause, fast forward buttons)
User click fast rewind or fast forward button.
Then app play previous or next music or video.
For the 3rd step, I should know which button is clicked.
(As I naturally know, the currently playing item is paused, stopped.. using MPMoviePlayerPlaybackStateDidChangeNotification notification).
Which notification should I register? Or are there any other approaches?
I got the answer by myself.
That is using UIApplication's beginReceivingRemoteControlEvents.
In an appropriate place (like viewWillAppear:) put the following code
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[self becomeFirstResponder];
And the view controller should implement the following method returning YES
- (BOOL)canBecomeFirstResponder {
return YES;
}
And then you can receive remote controller event in the following method.
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
if( event.type == UIEventTypeRemoteControl ) {
NSLog(#"sub type: %d", event.subtype);
}
}
And event.subtype is as below,
typedef enum {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0,
// for UIEventTypeMotion, available in iPhone OS 3.0
UIEventSubtypeMotionShake = 1,
// for UIEventTypeRemoteControl, available in iPhone OS 4.0
UIEventSubtypeRemoteControlPlay = 100,
UIEventSubtypeRemoteControlPause = 101,
UIEventSubtypeRemoteControlStop = 102,
UIEventSubtypeRemoteControlTogglePlayPause = 103,
UIEventSubtypeRemoteControlNextTrack = 104,
UIEventSubtypeRemoteControlPreviousTrack = 105,
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
UIEventSubtypeRemoteControlEndSeekingBackward = 107,
UIEventSubtypeRemoteControlBeginSeekingForward = 108,
UIEventSubtypeRemoteControlEndSeekingForward = 109,
} UIEventSubtype;
This might be a very late answer, but as I notice, there aren't many Q/As about audio playing and remote controls, so I hope my answer helps the others who have the same problem:
I'm using AVAudioPlayer at the moment, but the remote controlling method which is - (void)remoteControlReceivedWithEvent:(UIEvent *)event must not be involved with the type of the player you're using.
To get the forward and rewind buttons on lock screen work, follow this:
In your view controller's viewDidLoad method add the following code:
//Make sure the system follows our playback status - to support the playback when the app enters the background mode.
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
[[AVAudioSession sharedInstance] setActive: YES error: nil];
Then add these methods:
viewDidAppear:: (if not implemented already)
- (void)viewDidAppear:(BOOL)animated {
[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];
}
viewWillDisappear: (if not implemented already)
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
//End recieving events
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
[self resignFirstResponder];
}
And:
//Make sure we can recieve remote control events
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
//if it is a remote control event handle it correctly
if (event.type == UIEventTypeRemoteControl)
{
if (event.subtype == UIEventSubtypeRemoteControlPlay)
{
[self playAudio];
}
else if (event.subtype == UIEventSubtypeRemoteControlPause)
{
[self pauseAudio];
}
else if (event.subtype == UIEventSubtypeRemoteControlTogglePlayPause)
{
[self togglePlayPause];
}
else if (event.subtype == UIEventSubtypeRemoteControlBeginSeekingBackward)
{
[self rewindTheAudio]; //You must implement 15" rewinding in this method.
}
else if (event.subtype == UIEventSubtypeRemoteControlBeginSeekingForward)
{
[self fastForwardTheAudio]; //You must implement 15" fastforwarding in this method.
}
}
}
This is working fine in my app, however if you want to be able to receive remote control events in all view controllers, then you should set it in the AppDelegate.
NOTE! This code is working fine at the moment, but I see two more subtypes called UIEventSubtypeRemoteControlEndSeekingBackward and UIEventSubtypeRemoteControlEndSeekingBackward. I'm not sure if they have to be implemented or not, if someone knows about it, let us know.