You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

AVAudioSession重新激活后outputVolume无法获取正确设备音量

iOS 18 下 AVAudioSession 重新激活后 outputVolume 不更新的问题

问题描述

使用AVAudioSession.sharedInstance()管理音频会话时,首次激活(调用.setActive(true))后能正确获取设备真实音量,但停用会话(.setActive(false))并调整系统音量后,再次激活会话,outputVolume返回的仍是停用前的旧值,只有通过物理按键或控制中心调整音量后,观察者才能拿到正确值。该问题仅存在于iOS 18.0.1 和 iOS 18.1,iOS 17.6.1 无此问题,且启停音频会话是应用核心逻辑。

复现步骤

  • 启用音频会话,初始音量值正确
  • 按音量键,文本更新正常
  • 停用音频会话,通过物理按键/控制中心调整系统音量
  • 重新启用音频会话,outputVolume未更新,仍为停用前数值

复现代码

import AVFoundation
import SwiftUI

@main
struct AudioSessionTestApp: App {
    @State private var active = false
    @State private var volume = 0.0
    @State private var volumeObservation: NSKeyValueObservation?

    var body: some Scene {
        WindowGroup {
            ZStack {
                Color.red.interpolateTo(
                    color: Color.green,
                    fraction: volume
                ).ignoresSafeArea()

                VStack(spacing: 24) {
                    Text("Audio session active: \(active)")
                    Button("Toggle") {
                        active = !active
                        if active {
                            enableSession()
                        } else {
                            disableSession()
                        }
                    }.buttonStyle(.borderedProminent)
                    Text("Session volume: \(volume)")
                }
                .padding()
            }
        }
    }

    func enableSession() {
        let session = AVAudioSession.sharedInstance()

        try? session.setActive(true)
        try? session.setCategory(.playback)

        volume = Double(session.outputVolume)
        volumeObservation = session.observe(\.outputVolume) { session, _ in
            NSLog("Observed volume change to \(session.outputVolume)")
            volume = Double(session.outputVolume)
        }
    }

    func disableSession() {
        volumeObservation?.invalidate()
        volumeObservation = nil

        let session = AVAudioSession.sharedInstance()
        try? session.setActive(false)
    }
}

extension Color {
    var components: (r: Double, g: Double, b: Double, o: Double)? {
        let uiColor: UIColor

        var r: CGFloat = 0
        var g: CGFloat = 0
        var b: CGFloat = 0
        var o: CGFloat = 0

        if description.contains("NamedColor") {
            let lowerBound = description.range(of: "name: \"")!.upperBound
            let upperBound = description.range(of: "\", bundle")!.lowerBound
            let assetsName = String(description[lowerBound ..< upperBound])

            uiColor = UIColor(named: assetsName)!
        } else {
            uiColor = UIColor(self)
        }

        guard uiColor.getRed(&r, green: &g, blue: &b, alpha: &o) else { return nil }

        return (Double(r), Double(g), Double(b), Double(o))
    }

    func interpolateTo(color: Color, fraction: Double) -> Color {
        let s = components!
        let t = color.components!

        let r: Double = s.r + (t.r - s.r) * fraction
        let g: Double = s.g + (t.g - s.g) * fraction
        let b: Double = s.b + (t.b - s.b) * fraction
        let o: Double = s.o + (t.o - s.o) * fraction

        return Color(red: r, green: g, blue: b, opacity: o)
    }
}

解决方案

针对iOS 18的这个bug,以下是几种可行的规避方法:

方法1:激活会话后延迟读取outputVolume

iOS 18中,会话激活后需要一小段时间才能同步真实音量,延迟读取可以拿到正确值:

func enableSession() {
    let session = AVAudioSession.sharedInstance()
    
    do {
        try session.setCategory(.playback)
        try session.setActive(true)
    } catch {
        print("Failed to activate session: \(error)")
    }
    
    // 延迟0.1秒读取音量,确保同步完成
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        volume = Double(session.outputVolume)
    }
    
    volumeObservation = session.observe(\.outputVolume) { session, _ in
        NSLog("Observed volume change to \(session.outputVolume)")
        volume = Double(session.outputVolume)
    }
}

注:延迟时间可根据实际场景调整,0.1秒在多数情况下足够稳定。

方法2:播放静音音频触发会话同步

激活会话后播放一段极短的静音音频,强制会话同步系统音量:

func enableSession() {
    let session = AVAudioSession.sharedInstance()
    
    do {
        try session.setCategory(.playback)
        try session.setActive(true)
    } catch {
        print("Failed to activate session: \(error)")
    }
    
    // 播放静音音频触发同步(需提前将静音wav文件加入项目)
    guard let silenceURL = Bundle.main.url(forResource: "silence", withExtension: "wav") else {
        volume = Double(session.outputVolume)
        return
    }
    
    do {
        let player = try AVAudioPlayer(contentsOf: silenceURL)
        player.volume = 0
        player.play()
    } catch {
        print("Failed to play silence audio: \(error)")
    }
    
    // 立即读取真实音量
    volume = Double(session.outputVolume)
    
    volumeObservation = session.observe(\.outputVolume) { session, _ in
        NSLog("Observed volume change to \(session.outputVolume)")
        volume = Double(session.outputVolume)
    }
}

注:需提前准备一个无声音频文件(如1秒静音wav),该方法能确保会话立即同步真实音量,性能开销可忽略。

方法3:监听系统全局音量变化通知

注册系统音量变化的私有通知,捕获会话激活前的音量调整:

@State private var volumeNotificationObserver: NSObjectProtocol?

func enableSession() {
    let session = AVAudioSession.sharedInstance()
    
    do {
        try session.setCategory(.playback)
        try session.setActive(true)
    } catch {
        print("Failed to activate session: \(error)")
    }
    
    // 初始读取音量
    volume = Double(session.outputVolume)
    
    // KVO监听会话音量
    volumeObservation = session.observe(\.outputVolume) { session, _ in
        NSLog("Observed volume change to \(session.outputVolume)")
        volume = Double(session.outputVolume)
    }
    
    // 监听系统全局音量变化
    volumeNotificationObserver = NotificationCenter.default.addObserver(
        forName: NSNotification.Name("AVSystemController_SystemVolumeDidChangeNotification"),
        object: nil,
        queue: .main
    ) { notification in
        guard let volumeValue = notification.userInfo?["AVSystemController_AudioVolumeNotificationParameter"] as? Float else { return }
        volume = Double(volumeValue)
    }
}

func disableSession() {
    volumeObservation?.invalidate()
    volumeObservation = nil
    
    if let observer = volumeNotificationObserver {
        NotificationCenter.default.removeObserver(observer)
        volumeNotificationObserver = nil
    }
    
    let session = AVAudioSession.sharedInstance()
    do {
        try session.setActive(false)
    } catch {
        print("Failed to deactivate session: \(error)")
    }
}

注:该通知为私有API,虽在iOS各版本普遍可用,但需注意未来系统版本的兼容性风险。

总结

优先推荐方法1(延迟读取),实现简单无额外依赖;若延迟读取仍不稳定,可尝试方法2;方法3作为备选方案。

内容的提问来源于stack exchange,提问作者Albert221

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 05:44:58