iOS 17使用AVMutableMovie/AVPlayer直播流出现黑屏问题求助
iOS 17 下 AVMutableMovie 分片直播黑屏问题解决方案
问题概述
在iOS 17系统中,通过WSS抓取5秒分片视频数据,使用AVMutableMovie初始化AVPlayerItem,并通过replaceCurrentItem(with:)切换分片实现直播播放时,出现持续黑屏现象,该功能在iOS 16上运行正常。尝试调整播放逻辑后虽能播放,但切换分片时会出现画面闪烁。
可能原因
iOS 17对AVMutableMovie的内存数据初始化逻辑做了调整,直接通过Data初始化的AVMutableMovie可能未正确解析或加载媒体轨道,导致AVPlayerItem无法渲染画面。此外,频繁调用replaceCurrentItem(with:)会触发播放器的资源重置,引发闪烁。
解决方案
1. 验证AVMutableMovie轨道有效性
在创建AVPlayerItem前,先检查AVMutableMovie是否包含有效轨道,避免加载无效资源:
private func playNext() { let nextSet = self.dataCollector.getNextItem(length: self.configuration.frameDelay) guard nextSet.count == self.configuration.frameDelay else { return } var playerTime = self.player.currentItem != nil ? Int(CMTimeGetSeconds(player.currentTime())) : 0 var allData = Data() allData.appendAll(dataSet: dataCollector.getFileType()) nextSet.forEach { data in playerTime += 1 allData.append(data.getFragmentData()) self.currentFragmentTimes.updateValue(data.getFragmentTime(), forKey: playerTime) } guard allData.count > 0 else { return } // 初始化AVMutableMovie并检查轨道 guard let movie = AVMutableMovie(data: allData, options: nil), movie.tracks.count > 0 else { // 处理无效媒体数据情况 return } let playerItem = AVPlayerItem(asset: movie) // 监听item状态,确保准备就绪后播放 let statusObserver = playerItem.observe(\.status, options: [.new]) { [weak self] item, change in guard let self = self else { return } if item.status == .readyToPlay { self.player.replaceCurrentItem(with: item) self.player.play() } else if item.status == .failed { // 处理加载失败 print("Player item failed: \(item.error?.localizedDescription ?? "Unknown error")") } } // 保存观察者引用避免被释放 self.itemObservers.append(statusObserver) self.playerInitializedTime = nil }
2. 改用AVURLAsset加载临时文件
iOS 17对内存媒体数据的兼容性下降,建议将分片数据写入临时文件,通过AVURLAsset加载,提升兼容性:
private func playNext() { let nextSet = self.dataCollector.getNextItem(length: self.configuration.frameDelay) guard nextSet.count == self.configuration.frameDelay else { return } var playerTime = self.player.currentItem != nil ? Int(CMTimeGetSeconds(player.currentTime())) : 0 var allData = Data() allData.appendAll(dataSet: dataCollector.getFileType()) nextSet.forEach { data in playerTime += 1 allData.append(data.getFragmentData()) self.currentFragmentTimes.updateValue(data.getFragmentTime(), forKey: playerTime) } guard allData.count > 0 else { return } // 创建临时文件URL let tempDir = NSTemporaryDirectory() let tempFileName = UUID().uuidString + ".mp4" // 根据实际媒体格式调整后缀 let tempFileURL = URL(fileURLWithPath: tempDir).appendingPathComponent(tempFileName) do { try allData.write(to: tempFileURL) let asset = AVURLAsset(url: tempFileURL) let playerItem = AVPlayerItem(asset: asset) // 监听状态并播放 let statusObserver = playerItem.observe(\.status, options: [.new]) { [weak self] item, change in guard let self = self else { return } if item.status == .readyToPlay { self.player.replaceCurrentItem(with: item) self.player.play() // 播放完成后删除临时文件 DispatchQueue.global().async { try? FileManager.default.removeItem(at: tempFileURL) } } } self.itemObservers.append(statusObserver) self.playerInitializedTime = nil } catch { print("Failed to write temp file: \(error)") } }
3. 使用AVQueuePlayer实现无缝切换
频繁调用replaceCurrentItem(with:)是导致闪烁的主要原因,改用AVQueuePlayer预加载下一个分片,实现无缝播放:
// 初始化时替换为AVQueuePlayer private let player = AVQueuePlayer() private func playNext() { let nextSet = self.dataCollector.getNextItem(length: self.configuration.frameDelay) guard nextSet.count == self.configuration.frameDelay else { return } var playerTime = self.player.currentItem != nil ? Int(CMTimeGetSeconds(player.currentTime())) : 0 var allData = Data() allData.appendAll(dataSet: dataCollector.getFileType()) nextSet.forEach { data in playerTime += 1 allData.append(data.getFragmentData()) self.currentFragmentTimes.updateValue(data.getFragmentTime(), forKey: playerTime) } guard allData.count > 0 else { return } // 写入临时文件并创建AVPlayerItem let tempDir = NSTemporaryDirectory() let tempFileName = UUID().uuidString + ".mp4" let tempFileURL = URL(fileURLWithPath: tempDir).appendingPathComponent(tempFileName) do { try allData.write(to: tempFileURL) let asset = AVURLAsset(url: tempFileURL) let playerItem = AVPlayerItem(asset: asset) // 监听当前播放项结束事件,自动添加下一个项 let endObserver = playerItem.observe(\.status, options: [.new]) { [weak self] item, change in guard let self = self else { return } if item.status == .readyToPlay { // 如果队列中没有正在播放的项,直接插入并播放 if self.player.items().isEmpty { self.player.insert(item, after: nil) self.player.play() } else { // 否则添加到队列末尾 self.player.insert(item, after: self.player.currentItem) } // 播放完成后删除临时文件 NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: item, queue: .main) { _ in DispatchQueue.global().async { try? FileManager.default.removeItem(at: tempFileURL) } } } } self.itemObservers.append(endObserver) self.playerInitializedTime = nil } catch { print("Failed to write temp file: \(error)") } }
注意事项
- 确保分片媒体数据格式一致,避免因格式切换导致播放异常。
- 及时清理临时文件和观察者引用,避免内存泄漏。
- 针对iOS 17,优先使用
AVURLAsset而非内存数据初始化的AVMutableMovie,提升兼容性。
内容的提问来源于stack exchange,提问作者Rajeev Ghantasala
相关产品推荐
相关产品推荐

