系统音量变化观察器在 iOS 15 上不起作用

问题描述 投票:0回答:7

我使用以下代码来检测用户更改的系统音量。

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

当我更新到 iOS 15 时,我发现这段代码不起作用,但对于任何以前版本的 iOS 来说它都有效。

我还使用了

addObserver
功能,但没关系。

这是 iOS 15 的错误吗?如果是,我能做些什么来修复它。

谢谢:)

swift volume ios15
7个回答
17
投票

我挂钩了MPVolumeControllerSystemDataSource的方法_systemVolumeDidChange,并且在iOS 15.0(至少beta2),通知名称已更改为SystemVolumeDidChange,这是新的通知结构:

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

有两点需要注意:

  1. 在 iOS 15(至少在 beta2 中),即使按一次音量按钮,此通知也会被调用两次,但它们的 SequenceNumber 是相等的;
  2. 此通知回调不在 主线程上。

8
投票

您正在做的事情不受支持,因此如果它不能在所有系统上运行也不足为奇。正确记录的方法是在音频会话

outputVolume
属性上使用 KVO:https://developer.apple.com/documentation/avfaudio/avaudiosession/1616533-outputvolume


4
投票

我也在努力解决处理音量按钮按下事件的任务,旨在

  • 获取活动
  • 识别音量是调高还是调低
  • 即使在系统最大/最小级别也能够做出反应
  • 处理事件(在我的例子中,执行WKWebView中的js回调)
  • 使其能够在 iOS 15 / iOS 15 以下版本上运行。

对我有用的最终解决方案(根据 2022 年 9 月)如下:

在我的视图控制器中

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

在我的 loadView 函数中:

        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 标签允许您选择根据 iOS 版本设置的通知。

...我的视图控制器有这个:

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

这个是处理事件本身以及区分14/15版本的代码(略有不同)

请注意:这里使用 DispatchQueue.main.async,一旦完成处理程序(如上所述)不在主线程上,而在我的情况下它必须在主线程上。在我弄清楚之前,我遇到了一些崩溃和线程警告。

    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
        }
    }
   

此函数用于处理音量按钮按下事件,还可以帮助您 a) 了解事件是“向上”还是“向下”,以及 b) 管理达到最大/最小值的情况(如果您需要继续)即使您触及最大/最小,也可以进行事件处理(这是通过简单地减少/增加当前音量变量来完成的 - 请记住,您无法更改系统音量本身,但是,该变量都是您的;))

    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)
            }
        }
    }

它是iOS 15的主要实现函数。正如你所看到的,minVolume 不是一个数字,它是一个 let 常量 - 它与 iOS 14 不同(我发现在 iOS 14 上它是 0,而 iOS 15 在我的物理设备上不会低于 0.0625 - 请不要问我为什么,这是一个谜;))

它还处理最后一个通知唯一 ID 并省略重复的通知事件,这在 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)
        }
        
    }

iOS 14 也是如此,有 3 个主要区别:a) 通知 UserInfo 键,如上所述,不同 b) 没有重复的通知控件 - 只要我在 iOS 14 上观察到没有重复,c) minVolume 是0,这对于 iOS 14 是正确的

希望对您有帮助:)


3
投票

尝试了 AdamWang 的答案后,我发现您需要创建并保留 MPVolumeView 的实例(但不需要添加到视图层次结构中),否则将不会发出通知。


3
投票

如果有人突然不明白如何应用AdamWang的解决方案,你只需要将“

AVSystemController_SystemVolumeDidChangeNotification
”替换为“
SystemVolumeDidChange
”即可。


1
投票

在 iOS15 中,不再调用@“AVSystemController_SystemVolumeDidChangeNotification”通知。

使用键值观察来代替。 (扩展上面马特的答案)

在你的 ViewController.m 文件中

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

@interface ViewController : UIViewController
{
    AVAudioSession *audioSession;
}

@end

在您的 View Controller.m 文件中

-(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
}

您可以使用移出屏幕的 MPVolumeView 来隐藏音量滑块。

使用 MPVolumeView 滑块调整音量时隐藏设备音量 HUD 视图


0
投票

我尝试了上面安德烈的解决方案。它有效,但我不断收到重复的事件,有时甚至在不改变音量的情况下收到额外的事件。 这是我的代码,考虑应用程序阶段。

class VolumeObserver {
    var volume: Float = 0
    private var volumeObserver: NSKeyValueObservation?
    let volumeView = MPVolumeView(frame: .zero)
    var callback:()->Void = {}
    init(_ back:@escaping ()->Void){
        callback = back
        observeVolumeChanges()
        NotificationCenter.default.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.willResignActiveNotification, object: nil)
    }
    
    func observeVolumeChanges() {
        try! AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default, options: [])//Prevent the app stop the current playing media
        let audioSession = AVAudioSession.sharedInstance()
        try! audioSession.setActive(true)
        volume = audioSession.outputVolume
        
        volumeObserver = audioSession.observe(\.outputVolume) { [weak self] (audioSession, _) in
            guard let self = self else { return }
            let newVolume = audioSession.outputVolume
            if abs(newVolume - self.volume ) > 0.01 {
                self.volume = newVolume
                callback()
            }
        }
    }
    @objc func appMovedToBackground() {
        volumeObserver?.invalidate()
    }
    
    @objc func appMovedToForeground() {
        observeVolumeChanges()
    }//it's important to remove and re-register after the app's phase changes.
}
© www.soinside.com 2019 - 2024. All rights reserved.