iOS通知扩展播放远程音频文件失败问题求助
解决iOS通知扩展自动播放远程音频(兼容iOS10)的问题
嘿,我来帮你搞定这个问题!你现在遇到的情况,核心原因有几个:通知扩展的生命周期极短,默认没激活音频会话,再加上之前代码里的几个关键错误,导致音频根本没法正常播放。下面是一步步的修复方案,完全兼容iOS10:
1. 先把播放器“留住”——别用局部变量
通知扩展的didReceive方法执行完之后,系统会很快终止扩展进程,如果播放器是方法里的局部变量,直接就被销毁了,根本播不了。所以得在扩展类里声明全局的播放器变量:
class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? // 全局持有音频播放器,避免被提前释放 private var audioPlayer: AVAudioPlayer?
2. 必须激活音频会话
iOS里播放音频需要先配置并激活音频会话,尤其是通知这种后台场景,得设置成能绕过静音键的playback类别:
private func setupAudioSession() { let session = AVAudioSession.sharedInstance() do { // iOS10及以上用这个枚举,要是兼容iOS9的话可以用字符串常量AVAudioSessionCategoryPlayback try session.setCategory(.playback, mode: .default) try session.setActive(true, options: .notifyOthersOnDeactivation) } catch { print("音频会话配置翻车了: \(error.localizedDescription)") } }
3. 修复远程音频的下载&播放逻辑
你之前的代码犯了个错:AVAudioPlayer只能播放本地文件,不能直接用远程URL!所以得先把音频下载到本地临时目录,再播放:
private func downloadAndPlayAudio(from remoteURL: URL) { URLSession.shared.downloadTask(with: remoteURL) { [weak self] tempURL, _, error in guard let self = self else { return } if let error = error { print("音频下载失败: \(error.localizedDescription)") // 记得调用contentHandler结束扩展 self.contentHandler?(self.bestAttemptContent ?? UNNotificationContent()) return } // 下载后的tempURL是临时路径,得复制到缓存目录才能用 guard let tempURL = tempURL, let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { self.contentHandler?(self.bestAttemptContent ?? UNNotificationContent()) return } let localAudioURL = cacheDir.appendingPathComponent(remoteURL.lastPathComponent) do { // 如果缓存里已有这个文件,先删掉再复制 if FileManager.default.fileExists(atPath: localAudioURL.path) { try FileManager.default.removeItem(at: localAudioURL) } try FileManager.default.copyItem(at: tempURL, to: localAudioURL) // 先配置音频会话,再初始化播放器 self.setupAudioSession() self.audioPlayer = try AVAudioPlayer(contentsOf: localAudioURL) self.audioPlayer?.delegate = self self.audioPlayer?.volume = 1.0 self.audioPlayer?.play() } catch { print("播放准备失败: \(error.localizedDescription)") self.contentHandler?(self.bestAttemptContent ?? UNNotificationContent()) } }.resume() }
4. 等播放完再结束扩展
通知扩展必须等到所有任务完成后再调用contentHandler,否则系统会提前杀进程。所以要实现AVAudioPlayerDelegate,在播放完成后收尾:
extension NotificationService: AVAudioPlayerDelegate { func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { // 播放完成,告诉系统可以结束扩展了 self.contentHandler?(self.bestAttemptContent ?? UNNotificationContent()) // 顺便关闭音频会话,释放资源 do { try AVAudioSession.sharedInstance().setActive(false) } catch { print("关闭音频会话失败: \(error.localizedDescription)") } } }
5. 在didReceive里触发整个流程
最后把这些逻辑串起来,在didReceive里调用下载播放的方法:
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) // 替换成你的远程音频URL guard let audioURL = URL(string: "https://firebasestorage.googleapis.com/v0/b/XXX.appspot.com/o/XXXXXXX?alt=media&token=XXXXXXX") else { contentHandler(bestAttemptContent ?? UNNotificationContent()) return } downloadAndPlayAudio(from: audioURL) }
额外要注意的点
- 后台模式:你已经开了
Audio, AirPlay, and PiP,这个是必须的,确保主应用的后台模式配置正确,通知扩展会继承主应用的相关权限。 - iOS10兼容性:上面的代码里
AVAudioSession.Category.playback在iOS10是可用的,如果要兼容iOS9,把它换成字符串常量AVAudioSessionCategoryPlayback就行。 - 音频大小:通知扩展的内存限制很小(大概10MB),所以音频文件不能太大,否则会被系统直接终止进程。
- 静音键:
playback类别会绕过静音键,确保通知音频能正常播放,符合你“无需用户交互”的需求。
内容的提问来源于stack exchange,提问作者Dmytro Rostopira




