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

WebRTC iOS:如何将RTCVideoFrame转为CVPixelBuffer并修改摄像头流?

解决WebRTC RTCVideoFrame转CVPixelBuffer及修改摄像头流的问题

我之前也碰到过一模一样的情况,WebRTC移除了直接获取CVPixelBuffer的API后,确实得换个思路处理。下面分享两种可行的方案,一种是实现RTCVideoFrame到CVPixelBuffer的转换,另一种是更高效的视频流修改方式:

一、将RTCVideoFrame转为CVPixelBuffer的可行方法

WebRTC现在的帧缓冲基本都是基于YUV格式(主要是I420),我们可以先把RTCVideoFrameBuffer转换成RTCI420Buffer,再手动将YUV数据拷贝到新创建的CVPixelBuffer中。具体代码如下:

func convertRTCVideoFrameToCVPixelBuffer(_ frame: RTCVideoFrame) -> CVPixelBuffer? {
    // 先将帧缓冲转为I420格式
    guard let i420Buffer = frame.buffer.toI420() as? RTCI420Buffer else {
        print("无法转换为I420缓冲")
        return nil
    }
    
    let width = i420Buffer.width
    let height = i420Buffer.height
    
    // 创建CVPixelBuffer,指定YUV420BiPlanar格式
    var pixelBuffer: CVPixelBuffer?
    let creationStatus = CVPixelBufferCreate(
        kCFAllocatorDefault,
        width,
        height,
        kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
        [
            kCVPixelBufferMetalCompatibilityKey: true,
            kCVPixelBufferCGImageCompatibilityKey: true
        ] as CFDictionary,
        &pixelBuffer
    )
    
    guard creationStatus == kCVReturnSuccess, let pb = pixelBuffer else {
        print("创建CVPixelBuffer失败")
        return nil
    }
    
    CVPixelBufferLockBaseAddress(pb, .readWrite)
    
    // 拷贝Y平面数据
    if let yDestBase = CVPixelBufferGetBaseAddressOfPlane(pb, 0) {
        memcpy(yDestBase, i420Buffer.dataY, i420Buffer.strideY * height)
    }
    
    // 拷贝UV平面数据(I420的U/V是单独平面,需要转为交错格式)
    if let uvDestBase = CVPixelBufferGetBaseAddressOfPlane(pb, 1) {
        let uvStride = CVPixelBufferGetBytesPerRowOfPlane(pb, 1)
        var uvByteIndex = 0
        
        for row in 0..<height/2 {
            let uRowData = i420Buffer.dataU + row * i420Buffer.strideU
            let vRowData = i420Buffer.dataV + row * i420Buffer.strideV
            
            // 交错写入U和V数据
            for col in 0..<width/2 {
                uvDestBase.advanced(by: uvByteIndex).pointee = uRowData.advanced(by: col).pointee
                uvByteIndex += 1
                uvDestBase.advanced(by: uvByteIndex).pointee = vRowData.advanced(by: col).pointee
                uvByteIndex += 1
            }
            
            // 跳过每行的填充字节(如果stride和实际宽度不一致)
            uvByteIndex += uvStride - width
        }
    }
    
    CVPixelBufferUnlockBaseAddress(pb, .readWrite)
    return pb
}

二、修改RTCCameraVideoCapturer视频流的更优方案

直接修改SDK肯定不是最优解,我们可以通过包装原生的RTCVideoCapturerDelegate来实现视频流的拦截和修改,处理完后再转发给WebRTC的内部逻辑。这里分两种场景:

1. 基于YUV数据直接处理(性能最优)

如果你的滤镜可以直接操作YUV格式的数据,建议直接修改RTCI420Buffer,避免格式转换的开销:

class FilteredVideoCapturerDelegate: NSObject, RTCVideoCapturerDelegate {
    private let originalDelegate: RTCVideoCapturerDelegate
    
    init(originalDelegate: RTCVideoCapturerDelegate) {
        self.originalDelegate = originalDelegate
        super.init()
    }
    
    func capturer(_ capturer: RTCVideoCapturer, didCapture frame: RTCVideoFrame) {
        guard let processedFrame = applyYUVFilter(to: frame) else {
            originalDelegate.capturer(capturer, didCapture: frame)
            return
        }
        originalDelegate.capturer(capturer, didCapture: processedFrame)
    }
    
    private func applyYUVFilter(to frame: RTCVideoFrame) -> RTCVideoFrame? {
        guard let i420Buffer = frame.buffer.toI420() as? RTCI420Buffer else {
            return nil
        }
        
        // 创建可写的I420缓冲(原缓冲通常是只读的)
        let mutableBuffer = RTCI420MutableBuffer(width: i420Buffer.width, height: i420Buffer.height)
        mutableBuffer.dataY.copyMemory(from: i420Buffer.dataY, count: i420Buffer.strideY * i420Buffer.height)
        mutableBuffer.dataU.copyMemory(from: i420Buffer.dataU, count: i420Buffer.strideU * i420Buffer.height / 2)
        mutableBuffer.dataV.copyMemory(from: i420Buffer.dataV, count: i420Buffer.strideV * i420Buffer.height / 2)
        
        // 示例:调整Y通道亮度
        let yData = mutableBuffer.dataY
        for i in 0..<(mutableBuffer.strideY * mutableBuffer.height) {
            yData[i] = min(255, max(0, yData[i] + 30))
        }
        
        // 创建处理后的视频帧
        let processedBuffer = mutableBuffer as RTCVideoFrameBuffer
        return RTCVideoFrame(buffer: processedBuffer, rotation: frame.rotation, timeStampNs: frame.timeStampNs)
    }
}

使用时,替换原capturer的delegate即可:

// 假设原capturer是rtccameraCapturer,原delegate是self
let filteredDelegate = FilteredVideoCapturerDelegate(originalDelegate: self)
rtccameraCapturer.delegate = filteredDelegate

2. 基于CoreImage等CVPixelBuffer的滤镜处理

如果需要用CoreImage这类依赖CVPixelBuffer的框架,就可以先把RTCVideoFrame转成CVPixelBuffer,处理后再转回RTCVideoFrame:

private func applyCoreImageFilter(to frame: RTCVideoFrame) -> RTCVideoFrame? {
    guard let inputPixelBuffer = convertRTCVideoFrameToCVPixelBuffer(frame) else {
        return nil
    }
    
    // 应用CoreImage滤镜(示例:棕褐色滤镜)
    let ciImage = CIImage(cvImageBuffer: inputPixelBuffer)
    guard let filter = CIFilter(name: "CISepiaTone") else {
        return nil
    }
    filter.setValue(ciImage, forKey: kCIInputImageKey)
    filter.setValue(0.8, forKey: kCIInputIntensityKey)
    
    guard let filteredImage = filter.outputImage else {
        return nil
    }
    
    // 将处理后的CIImage转回CVPixelBuffer
    let context = CIContext(options: [.useSoftwareRenderer: false])
    var outputPixelBuffer: CVPixelBuffer?
    let bufferAttributes = [
        kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
        kCVPixelBufferWidthKey: filteredImage.extent.width,
        kCVPixelBufferHeightKey: filteredImage.extent.height
    ] as CFDictionary
    
    CVPixelBufferCreate(kCFAllocatorDefault,
                        Int(filteredImage.extent.width),
                        Int(filteredImage.extent.height),
                        kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
                        bufferAttributes,
                        &outputPixelBuffer)
    
    guard let pb = outputPixelBuffer else {
        return nil
    }
    
    context.render(filteredImage, to: pb)
    
    // 将CVPixelBuffer转回RTCVideoFrame
    let rtcBuffer = RTCCVPixelBuffer(pixelBuffer: pb)
    return RTCVideoFrame(buffer: rtcBuffer, rotation: frame.rotation, timeStampNs: frame.timeStampNs)
}

然后把这个方法替换到上面的FilteredVideoCapturerDelegate的applyYUVFilter位置即可。

总结

  • 如果只是需要转CVPixelBuffer,通过I420缓冲手动拷贝YUV数据是目前可行的方案;
  • 修改视频流优先选择直接操作YUV缓冲的方式,性能更好;如果必须用CoreImage等框架,再进行格式转换;
  • 通过包装delegate的方式,不需要修改WebRTC SDK,完全可以在Xcode工程内实现。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 09:30:12