6

I used following code to detect system volume changed by users.

NotificationCenter.default.addObserver(self, selector: #selector(volumeChanged), name: NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification"), object: nil)

When I updated to iOS 15, I found that this code is not working, but for any previous versions of iOS it works.

I also used an addObserver function, but that is ok.

Is this a iOS 15 bug and if so what can I do to fix it.

thanks :)

Len_X
  • 825
  • 11
  • 29
Gyeom
  • 63
  • 1
  • 4

6 Answers6

16

I hooked MPVolumeControllerSystemDataSource's method _systemVolumeDidChange and at iOS 15.0(at least beta2) the notification name has changed to SystemVolumeDidChange and here is the new notification structure:

{
    AudioCategory = "Audio/Video";
    Reason = ExplicitVolumeChange;
    SequenceNumber = 1069;
    Volume = 0;
}

There're two points to be noticed:

  1. This notification at iOS 15(at least in beta2) will be called twice even if you press volume button once, but their SequenceNumber is equal;
  2. This notification callback is not on main thread.
AdamWang
  • 191
  • 4
8

What you're doing is unsupported, so it's not really surprising if it doesn't work on all systems. The correct documented approach is to use KVO on the audio session outputVolume property: https://developer.apple.com/documentation/avfaudio/avaudiosession/1616533-outputvolume

matt
  • 515,959
  • 87
  • 875
  • 1,141
4

I was also struggling to solve the task of handling volume buttons press event, aiming to

  • get the event
  • recognise if it is volume up or down
  • be able to react even at system max/min level
  • handle the event (in my case, executing the js callback in WKWebView)
  • have it working on both iOS 15 / iOS below 15.

Final solution which works for me (as per sep-2022) is below:

In my view controller

var lastVolumeNotificationSequenceNumber: Int? //see below explanations - avoiding duplicate events
var currentVolume: Float? //needed to remember your current volume, to properly react on up/down events

In my loadView func:

        if #available(iOS 15, *) {
            NotificationCenter.default.addObserver(self, selector: #selector(volumeChanged(_:)), name: NSNotification.Name(rawValue: "SystemVolumeDidChange"), object: nil)
        }
        else {
            NotificationCenter.default.addObserver(self, selector: #selector(volumeChanged(_:)), name: NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification"), object: nil)
        }

#available tag allows you to choose the notification set up according to iOS version.

...And my view controller has this:

@objc func volumeChanged(_ notification: NSNotification) {
        DispatchQueue.main.async { [self] in
            if #available(iOS 15, *) {
                volumeControlIOS15(notification)
            }
            else {
                volumeControlIOS14(notification)
            }
        }
    }

This one is to handle the event itself and to distinguish the code for 14/15 versions (slightly different)

Please note: DispatchQueue.main.async used here, as soon as completion handler is (as stated above) NOT on the main thread, and in my case it HAS to be. I had some crashes and thread warnings before I figured it out.

    func manageVolume(volume: Float, minVolume: Float) {
        switch volume {
        case minVolume: do {
            currentVolume = minVolume + 0.0625
        }
        case 1: do {
            currentVolume = 0.9375
        }
        default: break
        }
        
        if let cV = currentVolume {
            if volume > cV {
                //do your stuff here
            }
            if volume < cV {
                //do your stuff here
            }
            currentVolume = volume
        }
        else {
            currentVolume = volume
        }
    }
   

This function is to handle the volume button press event, and also helps you to a) understand if the event is "up" or "down", and b) manage the case of reaching max/min value, should you need to proceed with event handling even when you're touching the max/min (this is done by simply reducing/increasing current volume variable - remember, you can't change the system volume itself, however, the variable is all yours ;) )

    func volumeControlIOS15(_ notification: NSNotification) {
        let minVolume: Float = 0.0625
        
        if let volume = notification.userInfo!["Volume"] as? Float {
            //avoiding duplicate events if same ID notification was generated
            if let seqN = self.lastVolumeNotificationSequenceNumber {
                if seqN == notification.userInfo!["SequenceNumber"] as! Int {
                    NSLog("Duplicate nofification received")
                }
                else {
                    self.lastVolumeNotificationSequenceNumber = (notification.userInfo!["SequenceNumber"] as! Int)
                    manageVolume(volume: volume, minVolume: minVolume)
                }
            }
            else {
                self.lastVolumeNotificationSequenceNumber = (notification.userInfo!["SequenceNumber"] as! Int)
                manageVolume(volume: volume, minVolume: minVolume)
            }
        }
    }

It is the main iOS 15 implementation func. As you can see, minVolume is not a number, it's a let constant - and it's different from iOS 14 (I found on iOS 14 it is 0, while iOS 15 is not going below 0.0625 on my physical device - please don't ask me why, it's a mystery ;))

It is also handling the last notification unique ID and omitting duplicated notification events, which are (somehow) quite common with iOS15.

    func volumeControlIOS14(_ notification: NSNotification) {
        //old implementation for iOS < 15
        let minVolume: Float = 0
        
        if let volume = notification.userInfo!["AVSystemController_AudioVolumeNotificationParameter"] as? Float {
            manageVolume(volume: volume, minVolume: minVolume)
        }
        
    }

Same here for iOS 14, with 3 main differences: a) notification UserInfo key, as stated above, is different b) no duplicate notifications control - as soon as there are no duplicates I ever observed on iOS 14, and c) minVolume is 0, which is correct for iOS 14

Hope it is helpful :)

  • iOS 16 has different min and max volume levels. – Deepak Sharma Sep 24 '22 at 15:29
  • Thanks for the heads up, Deepak! Good one. I had no chance to test it on 16 yet, will probably add one more case to handle. – Andrey Lesnykh Sep 28 '22 at 13:24
  • Tested on iOS 16, you need create an instance of MPVolumeView and add it to your view hierarchy. It is required to receive the nofication. let volumeView = MPVolumeView(frame: CGRect.zero) volumeView.clipsToBounds = true volumeView.showsRouteButton = false view.addSubview(volumeView) – nahung89 May 17 '23 at 09:51
3

Having tried AdamWang's answer, I found that you need create and retain an instance of MPVolumeView (but don't need to add to your view hierarchy) or the notification will not be emitted.

ouflak
  • 2,458
  • 10
  • 44
  • 49
3

If someone suddenly did not understand how to apply AdamWang's solution, you just need to replace "AVSystemController_SystemVolumeDidChangeNotification" with "SystemVolumeDidChange".

ouflak
  • 2,458
  • 10
  • 44
  • 49
1

In iOS15 the @"AVSystemController_SystemVolumeDidChangeNotification" notification is no longer being called.

Use Key Value observing instead. (expanding on matt's answer above)

In your ViewController.m file

#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>

@interface ViewController : UIViewController
{
    AVAudioSession *audioSession;
}

@end

In your View Controller.m file

-(void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

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

}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated]; 

    [audioSession removeObserver:self forKeyPath:@"outputVolume"];
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

    CGFloat newVolume = audioSession.outputVolume;
    NSLog(@"newVolume: %f", newVolume);

      //if the volume gets to max or min observer won't trigger
    if (newVolume > 0.9 || newVolume < 0.1) {
        [self setSystemVolume:0.5];
        return;
    }
}

  //set the volume programatically
- (void)setSystemVolume:(CGFloat)volume {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wdeprecated-declarations"
    [[MPMusicPlayerController applicationMusicPlayer] setVolume:(float)volume];
    #pragma clang diagnostic pop
}

You can hide the volume slider using an MPVolumeView moved off screen.

Hide device Volume HUD view while adjusitng volume with MPVolumeView slider

birdman
  • 1,134
  • 13
  • 13