Monday, September 8, 2008

Proper usage of the MPVolumeView class

IMPORTANT EDIT! 11/14/09
This post is no longer relevant regarding the details on keeping the volume slider in sync with the system volume. The undocumented call is no longer needed. In fact, if you use the call to "_updateVolumeFromAVSystemController", Apple will now reject your app during the review process.

I leave this post here for historical reference, but it is no longer relevant and should NOT be used!!

When dealing with audio in an app on the iPhone and iPod Touch, it is sometimes useful to have a volume control slider displayed onscreen. Apple supplies a class called MPVolumeView which is supposed to perform this function, however the documentation on it is quite poor.

Specifically, the MPVolumeView class provides a slider, and as you drag it, it adjusts the system volume. However, what the class doesn't provide is a method to automatically have the slider move in response to someone adjusting the volume via the volume buttons on the side of the iPhone. So, you can set the volume with the slider, but then if you adjust the volume with the buttons on the side of the phone, the slider is not reflecting the proper volume. Many iPhone apps exhibit this problem and personally I find it annoying.

It turns out, the reason so many apps behave this way is because there's not an easy documented method to query the system volume (at least not one I could find!). The main method for querying the system volume is in the Celestial private framework and you aren't supposed to use private frameworks in released apps.

Fortunately, someone discovered there is a system notifications that is generated whenever the volume changes. Also, the MPVolumeView slider can respond to a message called "_updateVolumeFromAVSystemController" which will cause it to adjust it's position based on the current system volume level.

So, given the two pieces of information, one can register a notification process to automatically adjust the MPVolumeView slider whenever the volume is changed using the buttons on the side of the phone.

Here's how it can be done - check out the code at pastebin. I didn't write this code, I just happened to run into it when searching for better information about MPVolumeView. There isn't really much in the way of comments in the code, but hopefully it's easy enough to follow.

UPDATE: the pastebin code expired, so here's my own version of the code. It was based on the pastebin code. I don't include the declarations from the .h file, but it should be pretty easy to figure out that part. You'll want to put this in the viewDidLoad method for whatever view will be holding the MPVolumeView.


- (void)viewDidLoad {

// create a frame to hold the MPVolumeView
CGRect frame = volumeViewHolder.bounds; // CGRectMake(0, 5, 180, 0);
volumeView = [[[MPVolumeView alloc] initWithFrame:frame] autorelease];
[volumeView sizeToFit];
[volumeViewHolder addSubview:volumeView];

// Find the volume view slider - we'll need to reference it in volumeChanged:
for (UIView *view in [volumeView subviews]){
if ([[[view class] description] isEqualToString:@"MPVolumeSlider"]) {
volumeViewSlider = view;
}
}

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(volumeChanged:)
name:@"AVSystemController_SystemVolumeDidChangeNotification"
object:nil];
}


- (void) volumeChanged:(NSNotification *)notify
{
//NSLog(@"volume changed");

// NOTE::: Do NOT use this call. It is no longer necessary and Apple will reject
// your app if you use it!

[volumeViewSlider _updateVolumeFromAVSystemController];
}


NOTE: when you compile this, you will get a warning that says "'UIView' may not respond to '- updateVolumeFromAVSSystemController'" but it can be safely ignored. The compiler is complaining that it can't verify volumeViewSlider is an object that can receive that message. This is some of the magic that makes Objective-C so powerful. You can send messages to objects even though the compiler doesn't know if the object supports the message. It's also an easy way to shoot yourself in the foot if you aren't certain of what you are doing.

In this instance, we know the volumeViewSlider object does support that message since we hand picked it by recursively searching the MPVolumeView until we got to the MPVolumeSlider piece of the object.

Thanks go out to the anonymous person who posted that original snippet of pastebin code. It was exactly what I was looking for.

[Edit: I must emphasize, the above code should NOT be used!! Apple will now reject an app which uses it. There's a demo project showing how to use some other features of the MPVolumeView class here.

17 comments:

Anonymous said...

pastebin entry is gone -- have the code lying around still?

Mostly Torn said...

I've updated this blog entry to now include my version of the code since the pastebin code expired.

kool_aqua said...

You could query the volume of the system by querying for "kAudioSessionProperty_CurrentHardwareOutputVolume" property (AudioToolbox framework). Did you find any luck using the Private Frameworks. I dont know how to use that. After I add a private framework to the project, I dont see the header files in the drop-down. Is there any additional setting that needs to go in?
Thanks!

Bill said...

I tried your code, and I get "build successful" but I don't see a volume slider. is there something more I have to do to make a slider? Interface builder?

Anonymous said...

Can anyone provide the .h file as well ?

Thanks in Advance

Anonymous said...

I have the same problem as Bill

Mostly Torn said...

Hi,

Since several people have asked, I'll post a complete sample project demonstrating this one feaure - hopefully tomorrow (Friday). If you don't see an updated blog entry about it, feel free to bug me about it. :-)

Cheers,
Brian

Mostly Torn said...

I've added a full XCode project sample demonstrating usage of this class. See my post here.

Anonymous said...

Do you think using this code will make Apple refuse the application in the App Store? I've heard that use of undocumented API can be a cause of not being approved.

Does this piece of code fall into this category (or how do you know when it does or not) ??

Mostly Torn said...

Use of this code should not be a problem getting the app approved. I already have 6 in the store that use it.

Using PRIVATE frameworks is a definite no-no, but using undocumented features of stuff in the public frameworks is usually considered OK.

That said, I know there are apps in the store (not mine!) that do in fact use private frameworks, so maybe Apple isn't so particular about that either. Those apps somehow got approved.

sbwoodside said...

So, theoretically it should be possible to send an event programmatically to the volumeViewSlider like this:

[((UISlider*)volumeViewSlider) setValue:0.5 animated:NO]; // doesn't actually change the volume

But, this adjusts the slider but doesn't change the volume. I guess that the system needs more, maybe a touch event or something before it will acknowledge that the slider has been adjusted. Does anyone know of a way to fake this out?

sbwoodside said...

Another thing is that you don't have to put up with a warning. Just declare:

UISlider * volumeViewSlider;

and then when you set it:

volumeViewSlider = (UISlider*)view;

MPVolumeSlider is a subclass of UISlider.

Another interesting thing is that if you're in GDB you can access the volume slider using volumeView._volumeSlider ... but when you try to compile that it fails. Is there any way to turn that into a dynamic access?

Anonymous said...

Thanks for the posting, very useful.

In order to get around the warning I just switched to the following call:

[volumeViewSlider performSelector:@selector(_updateVolumeFromAVSystemController)];

There may or may not be some extra overhead associated with calling the method this way, but given the use case that doesn't really matter

Anonymous said...

The VolumeView Slider *does* move when you adjust the volume buttons, but *only if there is sound playing when you do it*.

I have an app, for example, which plays sounds 'occasionally'. When its running the volume slider has no effect on my sounds and the buttons do not move it.

However, if I put play an audio file on itunes in the background while I use the app, the slider does adjust the volume - of both my apps' sounds and the iTunes music - and the buttons move the slider.

Grrr.

Martijn Thé said...

Thanks for the post.
The NSNotification object actually gets the volume setting inside it's userinfo field. So you don't have to call the unknown _updateVolumeFromAVSystemController, but instead:

- (void) volumeChanged:(NSNotification *)notify
{
[volumeViewSlider setValue:[[[notify userInfo] objectForKey:@"AVSystemController_AudioVolumeNotificationParameter"] floatValue]];
}

thierry said...

Great post !

But are you able to put a thumb image with a height bigger than 25 pixels ?

Joey said...

Hello,

when I set an image for a custom slider like this :

***
UIImage *lefTrack = [[[UIImage alloc] initWithContentsOfFile:[NSString stringWithFormat:@"%@/sliderRedBG.png", [[NSBundle mainBundle] resourcePath]]] stretchableImageWithLeftCapWidth:220.0 topCapHeight:0];
UIImage *rightTrack = [[[UIImage alloc] initWithContentsOfFile:[NSString stringWithFormat:@"%@/sliderRightBG.png", [[NSBundle mainBundle] resourcePath]]] stretchableImageWithLeftCapWidth:220.0 topCapHeight:0];

[(UISlider *)volumeViewSlider setMinimumTrackImage:lefTrack forState:UIControlStateNormal];
[(UISlider *)volumeViewSlider setMaximumTrackImage:rightTrack forState:UIControlStateNormal];
[(UISlider *)volumeViewSlider setThumbImage:[[UIImage alloc] initWithContentsOfFile:[NSString stringWithFormat:@"%@/sliderHandle.png", [[NSBundle mainBundle] resourcePath]]] forState:UIControlStateNormal];

***

My slider is 5 pixels too high. Did you experiment this ?

Thanks

Joey


All content copyright © 2009  Brian Stormont, unless otherwise noted.   All rights reserved.