Finding the parent AVPlayer of an AVPlayerItem - ios

I have an AVPlayerItem that I have been able to grab from my UIWebView (visiting a website with an AVPlayer/video-file in the HTML) using some AVPlayer callbacks in my ViewController.
I need to access the AVPlayer that has that item set as its AVPlayerItem.
As of now I am doing this by calling [TheAVPlayerItem valueForKey:#"player"]; but this is undocumented and not guaranteed to work in the future.
I'm looking for a more authoritative way of achieving this. I'd even be happy with literally starting at the UIWebView and looping down through all subviews (or sub-objects?) and grabbing any AVPlayer then checking if it has its AVPlayerItem set to my AVPlayerItem.... alas I don't know how to do that. But I have included my attempt at doing so:
//[TheAVPlayerItem valueForKey:#"player"];//undocumented and prone to breaking.
for (NSObject *anObject in [myWebView subviews]) {
if ([anObject isKindOfClass:[AVPlayer class]]) {
NSLog(#"Found an AVPlayer, now check if AVPlayerItem is a match");//never called
} else {
}
}
Of course the NSLog was never called.

Related

ios SystemSound will not respond to volume buttons

Ive used SystemSound in my app in order to play simple sound effects. In addition to this I play a musicvideo through the MPMoviePlayerController - now when I turn the volume up/down the music from the video responds as intended (lowering the volume up/down).
But the system sounds that are played does not respond to the volume. Im playing the system sounds when the user taps certain areas in the app. Heres a snippet of my code for that:
- (void)handleTap:(UITapGestureRecognizer *)recognizer {
SystemSoundID completeSound = nil;
//yellow folder in xcode doesnt need subdirectory param
//blue folder (true folder) will need to use subdirectory:#"dirname"
NSURL *sound_path = [[NSBundle mainBundle] URLForResource: target_sound_filename withExtension: #"wav"];
AudioServicesCreateSystemSoundID((__bridge CFURLRef)sound_path, &completeSound);
AudioServicesPlaySystemSound(completeSound);
}
PS. I doublechecked that my "Settings->Sounds->Ringer and Alerts->Change With Buttons" is set to ON (as I read on some other SO answers that leaving this option OFF will cause systemsound to not respond to the volume buttons)
Further the reason for using systemsound is that it gave the most accurate and responsive results when playing multiple sounds (like in a game).
Id prefer to not use OpenAL if possible (even through 3rd party sound libraries like Finch or CocosDenshion)
Any ideas?
Use the AVAudioPlayer class for playing sounds that are controlled by the user's volume settings (non-system sounds).
You can retain instances of AVAudioPlayer for each sound file that you use regularly and simply call the play method. Use prepareToPlay to preload the buffers.
Cheers to Marcus for suggesting that i could retain instances of AVAudioPlayer for each sound file and use prepareToPlay to preload the sounds. It might be to help for others looking for the same solution so here is how I did it (feel free to comment if anyone have suggestions for improvements)
//top of viewcontroller.m
#property (nonatomic, strong) NSMutableDictionary *audioPlayers;
#synthesize audioPlayers = _audioPlayers;
//on viewDidLoad
self.audioPlayers = [NSMutableDictionary new];
//creating the instances and adding them to the nsmutabledictonary in order to retain them
//soundFile is just a NSString containing the name of the wav file
NSString *soundFile = [[NSBundle mainBundle] pathForResource:s ofType:#"wav"];
AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:soundFile] error:nil];
//audioPlayer.numberOfLoops = -1;
[audioPlayer prepareToPlay];
//add to dictonary with filename (omit extension) as key
[self.audioPlayers setObject:audioPlayer forKey:s];
//then i use the following to play the sound later on (i have it on a tap event)
//get pointer reference to the correct AVAudioPlayer instance for this sound, and play it
AVAudioPlayer *foo = [self.audioPlayers objectForKey:target_sound_filename];
[foo play];
//also im not sure how ARC will treat the strong property, im setting it to nil in dealloc atm.
-(void)dealloc {
self.audioPlayers = nil;
}

AVMusic plays double when switching views

When i switch views the music keep playing the the background what is fine with my app. But when the user comes back to the initial view the music starts again over the original one so the user hears the music double. I have already got some code to check whether the sound is already playing but it doesnt work. :(
Any thoughts?
if (audioPlayer.playing == 0 ) {
NSURL *url = [NSURL fileURLWithPath:[NSString stringWithFormat:#"%#/CheeZeeLab.mp3", [[NSBundle mainBundle] resourcePath]]];
NSError *error;
audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
audioPlayer.numberOfLoops = 0;
audioPlayer.volume = 1;
[audioPlayer prepareToPlay];
[audioPlayer stop];
if (audioPlayer == nil)
NSLog(#"werkt niet");
else
[audioPlayer play]; }
else{
}
I bet your problem is in how you transition back to your initial view.
If you're pushing (or doing a segue from the child back to the parent... i.e. a circular segue), you're creating a new instance of your parent view controller.
And creating a new instance starts a fresh version of the audio player playing that sound.
You need to properly pop the view to go back to the previous view controller.
Depending on how you have set up the AVAudioPlayer, with manual setter and getter, with property or even in the constructor. You are confronted with some issues.
When you initialize the ViewController you are allocating an AVAudioPlayer, and if it keeps playing it's not being deallocated, and still retained. And the ViewController won't be deallocated either until every property's retain count is 0.
After going back to the main view, and trying to set up the same ViewController again, my guess is that you are not trying to push the same instance to the stack. But rather making a new instance and pushing that one onto the stack? And that also makes a AVAudioPlayer.
This is ofc a bit of guesswork as a can't see you entire code. But if this is the case, I think you could set things up a bit better. Make sure that the ViewController gets properly deallocated and releasing all it's properties before making a new instance to push to the stack.

AVAudioPlayer in class category with ARC

I'm trying to add a custom click sound on all my buttons in my application. I've created a class category for UIButton with the following code in it:
NSURL *soundURL = [NSURL fileURLWithPath:[[NSBundle bundleWithIdentifier:#"com.apple.UIKit"] pathForResource:#"Tock" ofType:#"aiff"]];
NSError *error;
AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundURL error:&error];
if(audioPlayer == nil){
NSLog(#"%#", [error description]);
} else{
[audioPlayer play];
}
All this is executed in: - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
But from what I understand there is a common problem with ARC (wich I am using) here. The audioPlayer objects seems to be released before the sound is played. I've also had a look at AudioServicesPlaySystemSound();. But that's no alternative for me at the moment because I need a volume level controller in my application and I've found no way to change the system volume without leaving the app.
My question is: Is there any way to make a strong reference to my audioPlayer object in the UIButton class category? Or is there any other, maybe better way, to get this working?
When adding any object relationship in a category, it's always worth looking at associated objects. Use the objc_setAssociatedObject() and objc_getAssociatedObject() functions. Make sure to use a unique pointer value for the key, and in your case you'll definitely want to use one of the retain association policies to ensure your object's lifespan is appropriate.

AVAUdioPlayer - changing current URL

In my audio player app, if I hit the fast forward button - the player must find the next song in the playlist and play it. What I do is I get the URL from the next song, and try to put it in the background (background is an instance of AVAudioPlayer*) url field, but that property is read only. So what I'm actually doing - I'm calling the initWithContentsOfURL method (again) to set the URL like this :
[self.background initWithContentsOfURL:
[[_playlist.collection objectAtIndex:currentIndex] songURL] error:nil];
Is this legit? I mean, the compiler tells me that the expression result is unused, but it actually works.
Is there another way of doing it? Thanks ;-)
For playing more than one URL, or effectively changing the URL, use the subclass AVQueuePlayer.
AVQueuePlayer is a subclass of AVPlayer you use to play a number of items in sequence.
Example:
NSString *fooVideoPath = [[NSBundle mainBundle] pathForResource:#"tommy" ofType:#"mov"];
NSString *barVideoPath = [[NSBundle mainBundle] pathForResource:#"pamela" ofType:#"mp4"];
AVPlayerItem *fooVideoItem = [AVPlayerItem playerItemWithURL:[NSURL fileURLWithPath:fooVideoPath]];
AVPlayerItem *barVideoItem = [AVPlayerItem playerItemWithURL:[NSURL fileURLWithPath:barVideoPath]];
self.queuePlayer = [AVQueuePlayer queuePlayerWithItems:[NSArray arrayWithObjects:fooVideoItem, barVideoItem,nil]];
[self.queuePlayer play];
// things happening...
[self.queuePlayer advanceToNextItem];
Have checked 4 Apple samples, they are using AVAudioPlayer to play one song only. However, your result looks very interesting and impressive! Please let us know, are you stopping the playback before reinitializing object with the same address, are you starting the new audio session ?
As for me, I'd not put the playback and app stability in a risk doing something not mentioned in the documentation but , but to be on the bright side would use the AVAudioPlayer class as it seems the most right, which gives us:
use the error variable to track the possible errors
stop the playing AVAudioPlayer instance, initialize a new instance of AVAudioPlayer and set it to the property letting an old-one to be deallocated automatically.
And you probably know yourself, that
self.background = [self.background initWithContentsOfURL::
will remove the warning for you.

AVPlayer continues to play after ViewController is removed from NavigationController

So I'm using ARC in my project and when I add an AVPlayerLayer it works just fine and dandy, but when I pop the UIViewController from my UINavigationItem the video continues to play in the background. Does anyone know how you would handle this? It seems like something easy I'm just overlooking. Here's the code I have for the initially instantiations.
self.currentItem = [[AVPlayerItem alloc] initWithURL:url];
self.player = [[AVPlayer alloc]initWithPlayerItem:self.currentItem];
self.avPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
self.avPlayerLayer.bounds = self.view.bounds;
self.avPlayerLayer.frame = CGRectMake(0,55, 1024, 670);
self.view.backgroundColor = [UIColor clearColor];
[self.view.layer addSublayer:avPlayerLayer];
Also this is how I have the properties definied.
#property (strong) AVPlayer *player;
#property (strong) AVPlayerLayer *avPlayerLayer;
#property (strong) AVPlayerItem *currentItem;
Maybe that's entirely wrong as well. I'm not exactly sure when to use (strong) vs (weak). Any case thank you ahead of time for any help.
If avPlayerLayer is the only class interacting with the avPlayer, you don't need to maintain a reference to it with a property in the class you're using to present it (unless this class uses it outside the code you've shared). In fact, this is probably why it isn't working the way you expect.
The reason the clip continues to play (I think) is because the player isn't being deallocated. You create it, own it with the strong property in your class, than it's owned again by the AVPlayerLayer class you hand it to. So when the AVPlayerLayer is deallocated, the AVPlayer looses one owner. But it still has an owner (your class), so it isn't deallocated, and it keeps playing. The solution here is to get rid of your owning property altogether for *avPlayer. You don't need it. Create the AVPlayer and pass it to AVPlayerLayer. That should be all that's needed.
Something else you could do which might fix the behavior but not the problem, call:
[avPlayer pause]
In your AVPlayerLayer's dealloc method.
Re: Strong vs. Weak references: A strong reference implies ownership. As ARC is managing memory, it'll be doing all the [object retain]ing and [object release]ing that you previously would have done in code OR that you would have done with properties, ie:
#property (retain) NSObject *iAmRetainedWhenProperyIsAssigned;
So now with ARC, we simple users don't use words like retain or release in code or when defining properties. But we still design software. ARC is smart, but not smart enough to infer the architectural implications of relationships that we're defining. It still needs to be told that a class "owns" a reference to the object that the property refers to. Which reduces us, in the most basic terms:
(strong) means to ARC what (retain) meant to properties pre-ARC (owned/retained)
(weak) means to ARC what (assign) meant to properites pre-ARC (not owned/not retained)
Maybe you did not pause. Do it and then remove layer and nullify player.
[player Pause];
[player removefromsuperlayer];
player = nil;
When you want to stop an AVPlayerItem from loading, use AVQueuePlayer's removeAllItems and then re-initialize it.
[self.avPlayer removeAllItems];
self.avPlayer = [AVQueuePlayer playerWithURL:[NSURL URLWithString:#""]];
self.avPlayer = nil;
This will stop the current item from loading -- it is the only way I found to accomplish this.
2023 answer ...
As there are only very old answers. These days in fact:
( 1 ) All you need to do is nil it in the playerLayer:
///Unplay and return to blankness.
public func unplay() {
playerLayer.player = nil
}
However, as #isaac astutely pointed out, you almost always also have just a variable with the player in it (to make it easier for you to control it). Don't forget that you of course have to nil that as well.
Thus when you lunch the player, your code will look something like
var handyPlayer: AVPlayer?
...
handyPlayer = AVPlayer(url: u)
playerLayer.player = handyPlayer
handyPlayer?.play()
handyPlayer? ... seek, etc etc
( 2 ) Thus, almost always, the code is:
///Unplay and return to blankness.
public func unplay() {
playerLayer.player = nil
handyPlayer = nil
}
There is no need to pause, release anything, or anything else. That's all there is to it.

Resources