AVPlayer播放本地下载音频失败但流媒体正常问题求助
先直接说核心问题:你的音频播放失败,90%的概率是因为下载完成的回调时机完全错了,导致AVPlayer拿到的是还没下载好的空文件或者无效路径。下面一步步拆解问题和修复方案:
核心问题:异步下载的回调逻辑错误
你的downloadSongAsynch方法里,URLSession.shared.downloadTask是异步执行的——也就是说,调用resume()后,代码会立刻往下走,而此时文件还在下载中,你却直接调用了completion(destinationUrl)。这时候destinationUrl指向的要么是个空文件,要么根本还没被创建,AVPlayer自然无法播放无效的资源。
分步修复方案
1. 修正下载方法的回调时机
把completion回调移到文件成功移动到目标路径之后,确保只有当文件完全就绪时,才通知上层初始化AVPlayer。同时补充错误处理和重复下载判断:
func downloadSongAsynch(audioUrl: URL, completion: @escaping (URL?, Error?) -> ()) { let fileManager = FileManager.default let documentsDirectoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("podcasts/") // 创建目录失败直接回调错误 do { try fileManager.createDirectory(atPath: documentsDirectoryURL.path, withIntermediateDirectories: true, attributes: nil) } catch { completion(nil, error) return } let destinationUrl = documentsDirectoryURL.appendingPathComponent(audioUrl.lastPathComponent) // 如果文件已存在,直接返回路径,避免重复下载 if fileManager.fileExists(atPath: destinationUrl.path) { completion(destinationUrl, nil) return } URLSession.shared.downloadTask(with: audioUrl) { location, response, error in guard let location = location, error == nil else { completion(nil, error) return } do { // 如果目标路径已有旧文件,先删除再移动 if fileManager.fileExists(atPath: destinationUrl.path) { try fileManager.removeItem(at: destinationUrl) } try fileManager.moveItem(at: location, to: destinationUrl) // 只有文件移动成功后,才回调通知上层 completion(destinationUrl, nil) } catch { completion(nil, error) } }.resume() }
2. 修正按钮点击的调用逻辑
注意你代码里SetUpBotton中调用的downloadPodcastAsynch是笔误,应该是downloadSongAsynch,同时补充弱引用避免循环引用,以及错误处理:
@IBAction func SetUpBotton(_ sender: Any) { getLatestPodcastURL { [weak self] podcastUrl in guard let self = self else { return } self.downloadSongAsynch(audioUrl: podcastUrl) { localUrl, error in guard let localUrl = localUrl, error == nil else { print("下载失败:\(error?.localizedDescription ?? "未知错误")") return } self.player = AVPlayer(url: localUrl) print("AVPlayer初始化完成,currentItem: \(self.player.currentItem)") } } }
3. 确保音频会话被激活
你只设置了AVAudioSession的category,但没有激活会话,这可能导致播放权限问题,在viewDidLoad里补充激活代码:
override func viewDidLoad() { super.viewDidLoad() do { try AVAudioSession.sharedInstance().setCategory(.playback) try AVAudioSession.sharedInstance().activate() // 新增激活会话 } catch { print("音频会话设置失败:\(error.localizedDescription)") } }
4. 给播放按钮加安全校验
播放前先检查player和currentItem是否有效,避免空指针或无效资源:
@IBAction func PlayButton(_ sender: Any) { guard let player = player, player.currentItem != nil else { print("Player未初始化或资源无效") return } player.play() print("开始播放,currentItem: \(player.currentItem)") }
关于BoringSSL错误
你提到的SSL_ERROR_ZERO_RETURN确实是iOS新版本的一个日志bug,它只是记录了连接正常关闭的状态,完全不会影响下载或播放功能,直接忽略这个日志就行。
额外排查建议
如果修复后还是有问题,可以加个AVPlayerItem状态监听,排查资源是否能正常加载:
// 在初始化player的时候替换成这个 let playerItem = AVPlayerItem(url: localUrl) playerItem.addObserver(self, forKeyPath: "status", options: [.old, .new], context: nil) self.player = AVPlayer(playerItem: playerItem) // 然后实现监听方法 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "status", let playerItem = object as? AVPlayerItem { switch playerItem.status { case .readyToPlay: print("资源已准备好播放") case .failed: print("资源加载失败:\(playerItem.error?.localizedDescription ?? "未知错误")") case .unknown: print("资源加载中") @unknown default: break } } }
内容的提问来源于stack exchange,提问作者RMoore

