You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,确认视频播放完成后再删除文件。

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 02:50:03