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

Swift中基于Agora SDK实现视频帧拦截存储与覆写问题

问题背景

我正在使用Flutter的Agora SDK开发视频流应用,同时依赖原生层的Raw Video Data插件,可在视频编码前通过Swift接口获取YUV420原始帧缓冲区。

需求是:拦截一帧原始视频数据并存储,用该存储数据覆写下一帧;接着拦截第三帧,用其覆写第四帧,以此循环。

插件提供的Demo示例(仅将捕获视频染成绿色):

public func onCapture(_ videoFrame: AgoraVideoFrame) -> Bool {
    memset(videoFrame.uBuffer, 0, Int(videoFrame.uStride * videoFrame.height) / 2)
    memset(videoFrame.vBuffer, 0, Int(videoFrame.vStride * videoFrame.height) / 2)
    return true
}

其中videoFrame.uBuffer是UnsafeMutableRawPointer类型。

我的两次尝试均未成功:
第一次尝试(仅将帧的一半涂成左上角像素颜色):

// first frame callback
bufferedFrameY = videoFrame.yBuffer.load(as: Int32.self)
bufferedFrameU = videoFrame.uBuffer.load(as: Int32.self)
bufferedFrameV = videoFrame.vBuffer.load(as: Int32.self)

//.. 2nd frame callback
memset(videoFrame.yBuffer, bufferedFrameY, Int(videoFrame.yStride * videoFrame.height))
memset(videoFrame.uBuffer, bufferedFrameU, Int(videoFrame.uStride * videoFrame.height / 2))
memset(videoFrame.vBuffer, bufferedFrameV, Int(videoFrame.vStride * videoFrame.height / 2))

第二次尝试(无任何效果):

// first frame callback
bufferedFrameY = videoFrame.yBuffer.load(as: Int32.self)
bufferedFrameU = videoFrame.uBuffer.load(as: Int32.self)
bufferedFrameV = videoFrame.vBuffer.load(as: Int32.self)

//.. 2nd frame callback
videoFrame.yBuffer.storeBytes(of: bufferedFrameY, as: Int32.self)
videoFrame.uBuffer.storeBytes(of: bufferedFrameU, as: Int32.self)
videoFrame.vBuffer.storeBytes(of: bufferedFrameV, as: Int32.self)

问题原因分析

你的两次尝试失败的核心原因是仅存储了单像素数据而非完整帧缓冲区,且对内存操作函数的用法有误:

  1. 第一次尝试中,load(as: Int32.self)仅读取了缓冲区的前4字节(单个Int32值),而非整个YUV帧数据;memset的第二个参数要求是字节值(Int8),你传入Int32会被截断为低8位,导致整个帧被填充为单一颜色值,而非原始帧内容。
  2. 第二次尝试仅将存储的单像素值写回缓冲区开头,仅修改了极少量数据,肉眼无法察觉。

正确实现方案

要实现"隔帧复用"的逻辑,需要完整复制并存储整帧的YUV数据,再在需要时将存储的数据写回当前帧缓冲区。具体步骤如下:

  1. 在插件类中添加用于存储帧数据和状态的成员变量:
// 跟踪帧计数,用于判断当前是需要缓冲还是覆写
private var frameCount: Int = 0
// 存储缓冲的Y/U/V通道数据
private var bufferedY: Data?
private var bufferedU: Data?
private var bufferedV: Data?
// 存储缓冲帧的参数,用于校验后续帧的维度是否一致
private var cachedYStride: Int = 0
private var cachedHeight: Int = 0
  1. 修改onCapture方法,实现帧缓冲与覆写逻辑:
public func onCapture(_ videoFrame: AgoraVideoFrame) -> Bool {
    frameCount += 1
    
    let currentYStride = Int(videoFrame.yStride)
    let currentHeight = Int(videoFrame.height)
    let ySize = currentYStride * currentHeight
    let uvSize = Int(videoFrame.uStride) * currentHeight / 2
    
    if frameCount % 2 == 1 {
        // 奇数帧:缓冲当前完整帧数据
        bufferedY = Data(bytes: videoFrame.yBuffer, count: ySize)
        bufferedU = Data(bytes: videoFrame.uBuffer, count: uvSize)
        bufferedV = Data(bytes: videoFrame.vBuffer, count: uvSize)
        // 记录当前帧的维度参数
        cachedYStride = currentYStride
        cachedHeight = currentHeight
    } else {
        // 偶数帧:用缓冲的帧数据覆写当前帧
        guard let yData = bufferedY, let uData = bufferedU, let vData = bufferedV else {
            return true
        }
        // 校验当前帧与缓冲帧的维度是否一致(避免分辨率变化导致错误)
        if currentYStride == cachedYStride && currentHeight == cachedHeight {
            // 将缓冲数据复制回当前帧的Y通道
            yData.withUnsafeBytes { srcPtr in
                memcpy(videoFrame.yBuffer, srcPtr.baseAddress, ySize)
            }
            // 复制U通道
            uData.withUnsafeBytes { srcPtr in
                memcpy(videoFrame.uBuffer, srcPtr.baseAddress, uvSize)
            }
            // 复制V通道
            vData.withUnsafeBytes { srcPtr in
                memcpy(videoFrame.vBuffer, srcPtr.baseAddress, uvSize)
            }
        }
    }
    
    return true
}

关键说明

  • 使用Data(bytes:count:)完整复制原始帧的缓冲区数据,避免原缓冲区被复用后丢失数据。
  • 通过memcpy实现内存块的完整拷贝,确保整帧数据被正确覆写。
  • 添加维度校验,防止视频分辨率动态变化时出现内存操作错误。

内容的提问来源于stack exchange,提问作者Froschgesicht

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 14:24:19