You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动