Highlight
iOS 17 和 macOS 14 让 AirPods 自动切换、按压静音和空间音频的集成对开发者几乎零成本:媒体 App 注册 Now Playing 即可参与自动切换,CallKit App 自动获得按压静音,空间音频对 AVPlayer 和 AudioQueue App 开箱即用。
核心内容
AirPods 是全球最受欢迎的无线耳机,每天有数亿人用它听音乐、看视频、开视频会议。iOS 17 和 macOS 14 带来了三项关键改进,让 AirPods 体验更无缝。
自动切换
macOS 14 开始支持 AirPods 自动切换。用户在 iPhone 上听音乐,收到 Mac 上的会议通知,解锁 Mac 后 AirPods 自动连接并显示横幅确认,iPhone 上的音乐自动暂停。(01:33)
好消息是:所有 App Store App 和沙盒外的 App 都不需要做额外适配。自动切换算法根据 Now Playing 注册和输入音频活动做路由决策。
但有几个最佳实践可以优化体验:(05:17)
- 媒体/流媒体 App 建议注册 Now Playing,帮助系统正确优先路由
- 会议/游戏 App 不建议注册 Now Playing
- 用 Audio Services API 播放通知音效,区分通知和媒体内容
- 会议 App 只在通话期间打开麦克风,结束后立即关闭
- 媒体 App 使用用户选择的默认路由播放音频
- 避免在用户暂停后播放静音,如果必须则控制在 2 秒内
按压静音
iOS 17 和 macOS 14 支持按压 AirPods 杆部来静音/取消静音。用户按压后,系统显示麦克风状态横幅并播放提示音。(02:26)
iOS 上,所有 CallKit App 自动获得这个功能,无需额外代码。非 CallKit 的通信 App 需要用新的 AVAudioApplication API 接入。(07:03)
macOS 上的行为略有不同:App 收到静音手势通知后,需要自己负责静音上行音频。(09:47)
空间音频
超过 80% 的 Apple Music 订阅者使用空间音频收听。iOS 16 引入的个性化空间音频在 iOS 17 中继续扩展支持。(12:05)
macOS 14 支持 AVPlayer 和 AVSampleBufferAudioRenderer 的空间音频。iOS 和 tvOS 额外支持 AURemoteIO 和 AudioQueue。后两者没有 API 接口,自动为注册 Now Playing 的 App 启用,用户可以在控制中心配置。
详细内容
iOS 按压静音接入
import AVFAudio
import Combine
class AudioManager: ObservableObject {
@Published var isMuted = false
private var cancellables = Set<AnyCancellable>()
init() {
let center = NotificationCenter.default
center.publisher(for: AVAudioApplication.inputMuteStateChangeNotification)
.sink { [weak self] notification in
guard let self = self,
let userInfo = notification.userInfo,
let muteState = userInfo[AVAudioApplication.muteStateKey] as? Bool else {
return
}
self.isMuted = muteState
}
.store(in: &cancellables)
}
func toggleMute() {
let instance = AVAudioApplication.shared
instance.setInputMuted(!instance.isInputMuted)
}
}
关键点:
AVAudioApplication是 AVAudioSession 家族的新成员,管理应用级音频行为inputMuteStateChangeNotification通知在静音状态变化时触发muteStateKey从通知的userInfo中提取新的静音状态setInputMuted()在 App 内部触发静音时调用,保持系统状态同步isInputMuted读取当前静音状态- 订阅通知即表示 App 选择加入按压静音功能
macOS 按压静音接入
import AVFAudio
import CoreAudio
class MacAudioManager {
let instance = AVAudioApplication.shared
func setupMuteHandler() {
instance.setInputMuteStateChangeHandler { isMuted in
// 静音上行音频
self.muteUplinkAudio(isMuted)
// 更新 UI 状态
DispatchQueue.main.async {
self.updateMuteUI(isMuted)
}
// 返回是否成功处理
return true
}
}
func muteUplinkAudio(_ isMuted: Bool) {
var propertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyProcessInputMute,
mScope: kAudioObjectPropertyScopeInput,
mElement: kAudioObjectPropertyElementMain
)
var muteValue: UInt32 = isMuted ? 1 : 0
let size = UInt32(MemoryLayout.size(ofValue: muteValue))
AudioObjectSetPropertyData(
kAudioObjectSystemObject,
&propertyAddress,
0,
nil,
size,
&muteValue
)
}
func updateMuteUI(_ isMuted: Bool) {
// 更新麦克风状态指示器
}
}
关键点:
- macOS 上
setInputMuteStateChangeHandler是必须的,系统只负责通知,静音逻辑由 App 实现 - Handler 返回
Bool表示是否成功处理,可以拒绝不合适的静音操作 kAudioHardwarePropertyProcessInputMute是 CoreAudio 新属性,启用后会静音进程的所有输入音频但保持 IO 运行- Handler 不应该用于 UI 更新,UI 更新应该通过
inputMuteStateChangeNotification通知处理 AudioObjectSetPropertyData操作的是系统级音频对象,需要正确处理错误码
Now Playing 注册
媒体 App 注册 Now Playing 是参与自动切换和空间音频的关键:
import MediaPlayer
func setupNowPlaying() {
var nowPlayingInfo = [String: Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = "Song Title"
nowPlayingInfo[MPMediaItemPropertyArtist] = "Artist Name"
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget { _ in
// 处理播放
return .success
}
commandCenter.pauseCommand.addTarget { _ in
// 处理暂停
return .success
}
}
关键点:
MPNowPlayingInfoCenter向系统注册当前播放内容- 注册后系统可以正确路由音频到用户选择的设备
- 空间音频对 AudioQueue 和 AURemoteIO 自动为注册 Now Playing 的 App 启用
- 会议/游戏 App 不要注册 Now Playing,避免干扰自动切换算法
音频路由变化监听
import AVFAudio
func setupRouteChangeMonitoring() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleRouteChange),
name: AVAudioSession.routeChangeNotification,
object: nil
)
}
@objc func handleRouteChange(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return
}
let session = AVAudioSession.sharedInstance()
let currentRoute = session.currentRoute
switch reason {
case .newDeviceAvailable:
// AirPods 已连接
let hasHeadphones = currentRoute.outputs.contains { port in
port.portType == .headphones || port.portType == .bluetoothA2DP
}
if hasHeadphones {
print("AirPods connected")
}
case .oldDeviceUnavailable:
// AirPods 已断开
print("AirPods disconnected")
case .categoryChange:
// 音频类别变化
break
default:
break
}
}
关键点:
routeChangeNotification在音频路由变化时触发AVAudioSessionRouteChangeReason枚举说明变化原因currentRoute.outputs包含当前所有输出端口.bluetoothA2DP和.headphones可以检测 AirPods 连接状态- 正确处理
.oldDeviceUnavailable可以在 AirPods 断开时暂停播放
核心启发
1. 智能会议助手
做什么:在视频会议 App 中集成 AirPods 按压静音,并在 UI 中实时显示麦克风状态。
为什么值得做:AVAudioApplication 让按压静音的接入变成几行代码,但能显著提升用户在会议中的操作效率。
怎么开始:订阅 inputMuteStateChangeNotification,用 @Published 属性驱动 SwiftUI 的麦克风状态指示器,在工具栏显示静音图标。
2. 跨设备音频状态同步
做什么:做一个播客或音乐 App,在 AirPods 从 iPhone 切换到 Mac 时自动暂停,切换回来后恢复播放进度。
为什么值得做:自动切换是系统级功能,但 App 层面的播放状态同步需要开发者自己实现。
怎么开始:监听 routeChangeNotification,在 .oldDeviceUnavailable 时保存当前播放进度,在 .newDeviceAvailable 时检查是否是同一副 AirPods 并恢复播放。
3. 空间音频游戏 做什么:为游戏添加基于玩家头部方向的空间音频,让敌人位置可以通过声音判断。 为什么值得做:空间音频对 AURemoteIO 和 AudioQueue App 自动启用,只需要提供多声道内容。 怎么开始:用支持空间音频的音频格式(如 Dolby Atmos)编码游戏音效,通过 AudioQueue 播放,系统会自动处理头部追踪。
4. AirPods 连接感知 UI
做什么:在音频 App 中检测 AirPods 连接状态,连接时显示空间音频开关,断开时提示用户连接耳机获得最佳体验。
为什么值得做:不同音频设备的体验差异很大,根据设备类型调整 UI 可以提升用户满意度。
怎么开始:定期检查 AVAudioSession.sharedInstance().currentRoute.outputs,根据端口类型动态调整界面元素。
关联 Session
- What’s new in AVFoundation — AVFoundation 框架的新特性,包括音频处理改进
- Support HDR images in your app — 多媒体内容处理的最佳实践
- Explore AirPods features for your app — 更多 AirPods 功能集成指南
评论
GitHub Issues · utterances