Taking control of the volume buttons on iOS, like Camera+

Camera+ just got VolumeSnap back in their app. If you don’t know the story, Camera+ used the volume button on the iPhone to snap a picture. Apple rejected their app and kept them out of the store for three months when they found out. Then when iOS 5 came out Apple implemented volume snap in their own camera app. So now that Camera+ has it again and Apple seems to have relaxed their policy, I tried to figure out how Camera+ did it.

I came up with this class RBVolumeButtons. You use it like this:

   RBVolumeButtons *buttonStealer = [[[RBVolumeButtons alloc] init] autorelease];
   buttonStealer.upBlock = ^{ 
      counter++;
      [counterLabel setText:[NSString stringWithFormat:@"%i",counter]];
   };
   buttonStealer.downBlock = ^{ 
      counter--;
      [counterLabel setText:[NSString stringWithFormat:@"%i",counter]];
   };

It’s really that simple. Here’s how I did it.

First thing I had to do was figure out how to listen to the button presses. As far as I know, there isn’t a way to do it directly, so I listen to a volume change notification.

To do this, you start by initializing an audio session and adding a volume property listener

      AudioSessionInitialize(NULL, NULL, NULL, NULL);
      AudioSessionSetActive(YES);
      AudioSessionAddPropertyListener(kAudioSessionProperty_CurrentHardwareOutputVolume, volumeListenerCallback, self);

The callback looks like this

void volumeListenerCallback (
                             void                      *inClientData,
                             AudioSessionPropertyID    inID,
                             UInt32                    inDataSize,
                             const void                *inData
                             ){
   const float *volumePointer = inData;
   float volume = *volumePointer;

   
   if( volume > [(RBVolumeButtons*)inClientData launchVolume] )
   {
      [(RBVolumeButtons*)inClientData volumeUp];
   }
   else if( volume < [(RBVolumeButtons*)inClientData launchVolume] )
   {
      [(RBVolumeButtons*)inClientData volumeDown];
   }

}

So now I can get the volume values. Now I need to prevent the volume change notifications from showing up on screen. To do this, you can use an MPVolumeView. This is a special slider that will let you change the system volume for use in music playing applications. When it is in your app, the normal system volume change notifications don’t show on screen.

      CGRect frame = CGRectMake(0, -100, 10, 0);
      MPVolumeView *volumeView = [[[MPVolumeView alloc] initWithFrame:frame] autorelease];
      [volumeView sizeToFit];
      [[[[UIApplication sharedApplication] windows] objectAtIndex:0] addSubview:volumeView];

I get the window from the UIApplication and add the MPVolumeView off the screen.

So now the volume notifications aren’t showing and we’re getting some callbacks about when the volume changes.

When the user presses the volume buttons in your app, you don’t want the system volume to change. It would suck to have them taking pictures and turn up their ringtone volume all the way or something.

So how do I pull that off? This is pretty hacky but it works. First I store the volume that the device was at when the app launched. Then, each time the button is pressed, I set it back to that. In order two prevent getting two callbacks, I stop listening for notifications, set the volume, and start listening again. Like I said, it’s a bit of a hack.

AudioSessionRemovePropertyListenerWithUserData(kAudioSessionProperty_CurrentHardwareOutputVolume, volumeListenerCallback, self);
   
   [[MPMusicPlayerController applicationMusicPlayer] setVolume:launchVolume];
   
   [self performSelector:@selector(initializeVolumeButtonStealer) withObject:self afterDelay:0.1];

Now you just need to do some tricks with observing the application’s state (backgrounding, becoming inactive, etc.) to make sure that you’re always observing at the right times.

It’s a bit rough around the edges, but it should work. I’m sure there some issues with AudioSessions and if your app records or plays audio, this might mess some stuff up. You’ll also need to link the MediaPlayer and AudioToolbox frameworks with your project. There is a bug where the volume change notification shows when you close the app if the volume was at 0% or 100% when you launched the app. I don’t know if these are the same approaches that Camera+ used to do the same thing, but their implementation doesn’t have the bugs that mine does. If anyone knows a better way, let me know.

I have no idea if Apple will allow this in the App Store. It only uses public APIs (as far as I can tell) so it’s possible they will.

You can check out the full project on GitHub.

This entry was posted in Uncategorized. Bookmark the permalink.

13 Responses to Taking control of the volume buttons on iOS, like Camera+

  1. Seth says:

    Thanks for your post. I’m trying to figure out how to do my own imlementation of the ‘volume snap’ feature.

    One thing I’ve noticed on Camera+ is they’re doing something different. You can have a song playing in the Music app and it keeps playing in Camera+ The buttons are just taken over. Vol+/- no longer adjust the music volume.

    This approach stops the music app from playing music… :(

  2. Randall says:

    I think that UIImagePickerController will give you this functionality. If it does, I imagine that’s actually how Camera+ did it. They did a really good job of hiding it if that’s what they did.

  3. Marian Ignev says:

    Hi Randall,
    thanks for this great post. I have the following question. Do you know whether we have access to this buttons on locked iDevice?

    Thanks in advance :)

  4. Randall says:

    I would guess that you don’t. I haven’t tried it though.

  5. Leon Wubbe says:

    This is great! But…

    I have created a kind of stopwatch, wich starts/stops now using the volume control buttons. That works fantastic!

    When I want to control the playback volume from within the application, using
    [[MPMusicPlayerController applicationMusicPlayer] setVolume:volume];
    That does not work. Is that possible? My App also plays sounds, and I want to control the volume of that sound.

  6. Oliver says:

    Hey Randall,

    I am quite a newbie in iPhone programming.
    I was checking out your buttonStealer programme and was wondering if you think, you could also copy the button, rather than stealing.
    What I mean is to keep it’s original function (turning the volume) and still be able to implement a counter of the hits of the + and – buttons.
    Do you think, that’s possible!?

    Cheers, Oli

  7. Martin says:

    Hi,

    just to clarify: This tutorial only works on a jailbroken iPhone. I’ve tried it without a jailbreak and it did not work. After i’ve jailbroken my iPhone it worked perfectly, so keep that in mind.

    Very good tutorial btw

    cheers Martin

  8. Leon Wubbe says:

    This is not true. I have incorporated this functionality in my RC-Timer Pro App and it works on non-jailbroken iPhones.

    Very good tutorial indeed

    Regards, Leon

  9. m0rt1m3r says:

    Hi Randall,

    Thanks for a great post!
    I’d like to translate it to Polish and post it on my blog. Are you ok with this?
    Of course you’d be named as the author and source would be linked.

    Please let me know.
    Best Regards!

  10. Riccardo says:

    Hi Randall,

    Just to make you know that I just published a GitHub sample project inspired by your brilliant one: https://github.com/raneri/VolumeListener
    Basically, it does the same thing that RBVolumeButtons does, but it uses a shared sound instance, so it doesn’t interrupt playing music or other sounds.

    Thank you very much for your work!

  11. madhu says:

    Hi,
    i am using the RBVolumeButtons in my application to disable the volume keys effect on audio sound when they pressed. This is working fine when the application is in foreground/Active.
    How can i apply the same functionality when my app is in background and plays the sound in background(using background running task).

  12. madhu says:

    Hi all ,
    I used the RBVolumeButtons in my application to disable the effect of volume keys on audio sound. this working fine when the app is in foreground/ Active.
    But the sound is getting effected when the app is playing sound from background.
    How can i disable the effect of volume keys on audio sound when app is in background and plays sound.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>