Swift中AVPlayer播放WebSocket直播视频:首次可播后续提示Asset不可播放
问题:首次直播视频可播放,二次播放触发「Asset不可播放」错误
已实现通过WebSocket接收直播摄像头二进制数据、并使用AVPlayer播放的功能,但首次接收数据时视频正常播放,第二次接收数据时就出现「Asset不可播放」错误,视频仅能播放2-3秒就触发该错误,无法定位原因。
提供的代码
WebSocket消息接收代码
private func receiveMessage() { webSocketTask?.receive { [weak self] result in guard let self = self else { return } switch result { case .success(let message): switch message { case .string(let text): self.handleIncomingMessage(text) case .data(let data): self.appendDataToBuffer(data) @unknown default: print("Unknown message type received") } // Continue receiving messages self.receiveMessage() case .failure(let error): print("Failed to receive message: \(error)") // Check WebSocket state if let state = self.webSocketTask?.state { print("WebSocket state: \(state)") // If the connection is still active, try receiving again if state == .running { self.receiveMessage() } else { print("WebSocket is not running. State: \(state)") } } } } }
数据追加与视频处理代码
func appendDataToBuffer(_ data: Data) { self.dataBuffer.append(data) bufferQueue.async { [weak self] in guard let self = self else { return } if !self.isProcessingBuffer { self.startProcessingLoop() } } } private func startProcessingLoop() { processingQueue.async { [weak self] in guard let self = self else { return } self.isProcessingBuffer = true while self.isStreamingActive { var chunk: Data? self.bufferQueue.sync { let dynamicChunkSize = self.determineChunkSize() if self.dataBuffer.count >= dynamicChunkSize { print("Is Databuffer Count is greater than dynamic chunk size \(self.dataBuffer.count >= dynamicChunkSize)") chunk = self.dataBuffer.prefix(dynamicChunkSize) self.dataBuffer.removeFirst(dynamicChunkSize) } } guard let chunkToProcess = chunk else { usleep(100_000) continue } self.processVideoChunk(chunkToProcess) } self.isProcessingBuffer = false } } private func processVideoChunk(_ chunk: Data) { // Store the chunk in memory videoChunks.append(chunk) // Combine all chunks into a single Data object let combinedData = Data(videoChunks.joined()) // Create an AVAsset from the combined data guard let tempURL = createTemporaryURL(with: combinedData) else { logger.error("Failed to create temporary URL for video data") return } DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) { [weak self] in guard let self = self else { return } // Load the AVAsset asynchronously let asset = AVAsset(url: tempURL) asset.loadValuesAsynchronously(forKeys: ["playable"]) { var error: NSError? = nil let status = asset.statusOfValue(forKey: "playable", error: &error) DispatchQueue.main.async { if status == .loaded { self.logger.log("Asset loaded successfully") if asset.isPlayable { self.logger.log("Asset is playable") let playerItem = AVPlayerItem(asset: asset) self.delegate?.pausePlayer() // Use delegate to start the video playback self.delegate?.startVideo(playerItem: playerItem) self.isProcessingBuffer = false // Clean up temporary URL try? FileManager.default.removeItem(at: tempURL) self.clearChunks() } else { self.logger.error("Asset is not playable") try? FileManager.default.removeItem(at: tempURL) self.clearChunks() self.isProcessingBuffer = false } } else { self.logger.error("Error loading asset: \(error?.localizedDescription ?? "Unknown error")") } } } } }
问题分析与修复方案
1. 临时文件冲突与清理时机错误
- 问题:临时URL可能重复,导致第二次写入时覆盖未完全释放的文件;或是在AVAsset未完成加载就删除临时文件,造成读取失败。
- 修复:
- 创建临时URL时用UUID保证唯一性,避免文件名冲突:
func createTemporaryURL(with data: Data) -> URL? { let tempDir = NSTemporaryDirectory() let fileName = UUID().uuidString + ".mp4" // 根据实际视频格式调整后缀 let tempURL = URL(fileURLWithPath: tempDir).appendingPathComponent(fileName) do { try data.write(to: tempURL) return tempURL } catch { logger.error("Failed to write data to temp URL: \(error)") return nil } } - 延迟清理临时文件,通过监听
AVPlayerItem.didPlayToEndTimeNotification,确认视频播放完成后再删除文件。
- 创建临时URL时用UUID保证唯一性,避免文件名冲突:
2. videoChunks数组未正确清空
- 问题:
clearChunks()若未彻底清空数组,第二次处理时会混入第一次的旧数据,导致合并后的视频数据损坏。 - 修复:确保清空逻辑正确:
同时在处理新视频段前主动调用清空方法,避免残留旧数据。private func clearChunks() { videoChunks.removeAll() }
3. 处理流程竞态条件
- 问题:
startProcessingLoop循环取chunk并调用processVideoChunk,但后者提前设置isProcessingBuffer = false,导致循环被重复触发,多个流程同时运行造成数据混乱。 - 修复:将
isProcessingBuffer = false移到整个播放流程完成后,比如在监听视频播放完成的回调中设置,确保一次只处理一个完整视频段。
4. 不必要的异步延迟
- 问题:
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2)引入的延迟可能导致数据被覆盖或清理,且无法保证视频数据完整性。 - 修复:直接移除该延迟,同步处理AVAsset加载逻辑。
5. 直播流处理方式错误
- 问题:直播流是连续二进制数据,直接合并所有chunks创建AVAsset的方式不适合直播场景——AVAsset需要完整的容器结构(如MP4的moov原子),而直播流可能是分段裸流(如H.264)。
- 修复:使用
AVAssetWriter实时将接收的流数据写入临时文件,构建合法视频容器后再用AVPlayer播放,这种方式更适配直播持续写入的场景。
关键修改后的processVideoChunk示例
private func processVideoChunk(_ chunk: Data) { videoChunks.append(chunk) guard let tempURL = createTemporaryURL(with: Data(videoChunks.joined())) else { logger.error("Failed to create temporary URL for video data") return } let asset = AVAsset(url: tempURL) asset.loadValuesAsynchronously(forKeys: ["playable"]) { [weak self, tempURL] in guard let self = self else { return } var error: NSError? = nil let status = asset.statusOfValue(forKey: "playable", error: &error) DispatchQueue.main.async { if status == .loaded { if asset.isPlayable { let playerItem = AVPlayerItem(asset: asset) self.delegate?.pausePlayer() self.delegate?.startVideo(playerItem: playerItem) // 监听播放完成后清理资源 NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: .main) { [weak self] _ in try? FileManager.default.removeItem(at: tempURL) self?.clearChunks() self?.isProcessingBuffer = false NotificationCenter.default.removeObserver(self!, name: .AVPlayerItemDidPlayToEndTime, object: playerItem) } } else { self.logger.error("Asset is not playable") try? FileManager.default.removeItem(at: tempURL) self.clearChunks() self.isProcessingBuffer = false } } else { self.logger.error("Error loading asset: \(error?.localizedDescription ?? "Unknown error")") try? FileManager.default.removeItem(at: tempURL) self.clearChunks() self.isProcessingBuffer = false } } } }
内容的提问来源于stack exchange,提问作者user28467484
相关产品推荐
相关产品推荐

