final static final static - 4 days ago 5
iOS Question

How to set app volume separately from system volume ( iOS device volume physical keys )?

Our app is able to play music on a wifi speaker. One of the features of the app is changing the volume of speaker through pressing volume + / volume - hard keys on the iPhone.

The logic behind this is getting the volume value of the system and send it to the speaker.

However, the problem is that this function affects the system volume. Is there anyway to avoid adjusting the system volume when pressing the volume keys while inside the app?

This is the code that I used to get the system volume on every press:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqual:@"outputVolume"])
{
CGFloat phoneVolume = [[AVAudioSession sharedInstance] outputVolume];
NSInteger volume = 100 * phoneVolume;

[self onHardKeyVolumeChange:volume];
}
}


Thanks.

Answer

Here's what I did:

  1. Get the current system volume.
  2. Hide volume adjust popup view.
  3. Add observer for changes in system volume.
  4. Set the system volume back to the volume you got(at step 1) in every callbacks you received from the observer.

I'll explain every step in detail.

Step 1 - Get the current system volume

Code for initializing the volumes:

- (void)initializeSystemVolume
{
    _originalSystemVolume = [[AVAudioSession sharedInstance] outputVolume];
    _currentSystemVolume = _originalSystemVolume;

    if(_currentSystemVolume == 0.0)
    {
        _currentSystemVolume = 0.0625;
    }

    else if(_currentSystemVolume == 1.0)
    {
        _currentSystemVolume = 0.9375;
    }

    [self setSystemVolume:_currentSystemVolume];
}

_originalSystemVolume - This is the volume of the system when entering the app.

_currentSystemVolume - This could also be the same as the original volume BUT this could be changed, while originalSystemVolume should stay the same.

As you can see from the if else statement, I will check first if the current system volume is at the maximum value(1.0) or the minimum value(0.0). Why would I do this?

Because from my experiments, I noticed that the callback of the volume key press would only be made only if the system volume has changed. So if the current system volume is at its minimum value(0.0), and you still pressed the volume - button. No callbacks would be made. Then you would never determine the volume - key press state in this case.

So that is why I need to change the current system volume to a higher volume(0.0625) if it is at its minimum or change it to a lower volume(0.9375) if it's at its maximum so that we will still be able to get callbacks from the system. Now, why 0.0625 and 0.9375?

Well, actually I just want to set it to the closest possible value. If you would notice, the volume of iOS breaks down into 16 levels, and each level increments by 0.0625. 0.0 is silent mode and 1.0 is the peak volume.

Step 2 - Hide volume adjust popup view

Code for hiding the volume popup:

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

Since we will not be affecting the system volume, then we shouldn't display the popup as well.

Credit of this code goes to another person. I am sorry I forgot where I got this, but I didn't write it.

Step 3 - Add observer for changes in system volume.

Now, we should listen for changes in the system volume with every key press, and then we can use the value returned by the callback to determine which volume key is pressed.

Code for setting the observer:

- (void)setVolumeChangeObserver
{
    [self removeVolumeChangeObserver];

    [[AVAudioSession sharedInstance] setActive:YES error:nil];
    [[AVAudioSession sharedInstance] addObserver:self forKeyPath:@"outputVolume" options:0 context:nil];
}

Code for removing the observer:

- (void)removeVolumeChangeObserver
{
    @try
    {
        [[AVAudioSession sharedInstance] removeObserver:self forKeyPath:@"outputVolume"];
    }

    @catch(id anException)
    {

    }
}

Code for the callback:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqual:@"outputVolume"])
    {
        if([[AVAudioSession sharedInstance] outputVolume] < _currentSystemVolume)
        {
            NSLog(@"Volume key down");

            //your code when volume key down is pressed.
        }

        else if([[AVAudioSession sharedInstance] outputVolume] > _currentSystemVolume)
        {
            NSLog(@"Volume key up");

            //your code when volume key up is pressed.
        }

        [self removeVolumeChangeObserver];
        [self setSystemVolume:_currentSystemVolume];
        [self setVolumeChangeObserver];
    }
}

As can be seen from the code, we user the outputVolume upon key press and compare it with our _currentSystemVolume that we set awhile ago, we could determine whether volume + is pressed or volume - is pressed.

After analyzing which key is pressed, we should immediately set the system volume back to what it was so that volume key press made within our app will not affect the system volume.

Important: You have to remove the observer first before you set the system volume. Why is that so? Because if you don't do that, once you set the system volume, this callback will be made again, and when that happens the setSystemVolume will be called once more, then callback will be made again, then your setSystemVolume will be called again, then over and over again... You will then create a deadlock on this. By removing the observer, no callbacks will be made.

Step 4 - Setting the system volume

Now, how do we set the system volume then?

Code for setting system volume:

- (void)setSystemVolume:(CGFloat)volume
{
    if(_volumeView == nil)
    {
        _volumeView = [[SystemVolumeView alloc] init];
    }

    _volumeView.getVolumeSlider.value = volume;
}

_volumeView is an instance of the class, SystemVolumeView, that I made, which extends MPVolumeView to retrieve the UISlider of MPVolumeView. MPVolumeView is the view that pops up when you adjust the volume of the system(media volume).

Code for SystemVolumeView:

SystemVolumeView.h

#import <MediaPlayer/MediaPlayer.h>

@interface SystemVolumeView : MPVolumeView

- (UISlider *)getVolumeSlider;

@end

SystemVolumeView.m

#import <AVFoundation/AVFoundation.h>

#import "SystemVolumeView.h"

@interface SystemVolumeView ()

@property UISlider *systemVolumeSlider;

@end

@implementation SystemVolumeView

- (UISlider *)getVolumeSlider
{
    if(_systemVolumeSlider != nil)
    {
        return _systemVolumeSlider;
    }

    self.showsRouteButton = false;
    self.showsVolumeSlider = false;
    self.hidden = true;

    for(UIView *subview in self.subviews)
    {
        if([subview isKindOfClass:[UISlider class]])
        {
            _systemVolumeSlider = (UISlider *)subview;
            _systemVolumeSlider.continuous = true;

            return _systemVolumeSlider;
        }
    }

    return nil;
}

@end

Credit of this code goes to the accepted answer in this link. I just translated it to Objective-C.

As can be seen from the above code, you can set the system volume by calling getVolumeSlider.value = yourDesiredVolume. yourDesiredVolume should only be of range 0 - 1.

Alright, after all of this, you should have an idea on how these works.

Now, you might noticed we didn't use _originalSystemVolume.

Here's what that was for. Imagine if the volume of the system is set to silent initially, then we would set it to a higher value to make everything work right? Now, once the app enters background, we should set the system volume back to what it was. In this case we would do this when app is resigning active.

- (void)applicationWillResignActive:(UIApplication *)application
{
    [self restoreSystemVolume];
}

Code for Restoring system volume:

- (void)restoreSystemVolume
{
    [self setSystemVolume:_originalSystemVolume];
}

That's all folks. I hope this answer will be of great help to you someday. :)

Thanks to @Joris van Liempd iDeveloper. This link from him helped me a lot on achieving this.

Comments