使用 RTC SDK 实现视频内部采集或外部自定义采集后,在渲染和编码传输前,你可以对视频帧进行自定义处理,并传回 RTC SDK 进行本地预览和编码发送。例如以下场景:

前提条件

你已经集成 RTC SDK,实现了基本的音视频通话。
支持视频自定义处理功能的 SDK 详见API 参考。

功能说明
适用范围
此功能在视频处理链路的位置

alt

功能实现

本文以接入第三方美颜 SDK 为例,介绍 RTC SDK 的自定义视频处理的实现方法。参考步骤描述和示例代码,将涉及到美颜处理的部分替换为实际的实现代码。

API 时序图

alt

1. 实现视频处理器接口

首先,你需要自行实现 IVideoProcessor 接口。

public class VideoProcessor extends IVideoProcessor {
    @Override
    public VideoFrame processVideoFrame(VideoFrame frame) {}
    }
java
@protocol ByteRTCVideoProcessorDelegate <NSObject>

- (ByteRTCVideoFrame* _Nullable)processVideoFrame:(ByteRTCVideoFrame* _Nonnull)srcFrame;

@end
swift
// 继承IVideoProcessor 类,实现 processVideoFrame 纯虚接口,在 processVideoFrame 内完成自定义处理视频过程
class SimpleVideoProcessor : public bytertc::IVideoProcessor {
public:
    virtual bytertc::IVideoFrame* processVideoFrame(bytertc::IVideoFrame* src_frame) override;
    // 自定义处理 yuv 视频帧
};
cpp
2. 注册视频处理器

实现 IVideoProcessor 接口后,你需要将其注册进 RTC SDK 中,只有完成注册后,自定义视频处理器才会获取到采集的视频帧。在注册时,你可以指定 RTC SDK 返回给 IVideoProcessor.processVideoFrame 的视频帧格式。

注意:重复调用 registerLocalVideoProcessor 接口时,仅最后一次调用生效。

// 返回的视频帧格式仅支持 I420 和 TEXTURE_20
private void setVideoProcessor(){
    VideoPreprocessorConfig config = new VideoPreprocessorConfig();
    config.requiredPixelFormat = VideoPixelFormat.TEXTURE_2D;
    rtcVideo.registerLocalVideoProcessor(customVideoProcessor, config);
}
java
返回的视频帧格式仅支持 I420 和 Unknow
let config = ByteRTCVideoPreprocessorConfig()
config.required_pixel_format = .I420
if customVideoProcessor == nil {
    customVideoProcessor = SimpleVideoProcessor()
}
self.rtcVideoKit.registerLocalVideoProcessor(customVideoProcessor!, with: config)
swift
// For macOS
// 返回的视频帧格式仅支持 kVideoPixelFormatI420
bytertc::VideoPreprocessorConfig config;
config.required_pixel_format = bytertc::kVideoPixelFormatI420; // 推荐视频格式
if(!mCustomVideoProcessor) {
    mCustomVideoProcessor.reset(new SimpleVideoProcessor());
}
rtc_engine_->registerLocalVideoProcessor(processor, config); //内存类型: kVideoFrameTypeCVPixelBuffer
// For Windows
// 返回的视频帧格式仅支持 kVideoPixelFormatI420
bytertc::VideoPreprocessorConfig config;
config.required_pixel_format = bytertc::kVideoPixelFormatI420; // 推荐视频格式
if(!mCustomVideoProcessor) {
    mCustomVideoProcessor.reset(new SimpleVideoProcessor());
}
video->registerLocalVideoProcessor(processor, config); //内存类型: kVideoFrameTypeRawMemory
cpp
3. 完成自定义视频处理

将自定义视频处理器注册到 RTC SDK 后,App 会通过 IVideoProcessor.processVideoFrame 回调收到采集的视频帧,你需要在这里完成自定义的视频处理操作。以下示例代码以第三方美颜为例,说明如何处理视频帧。

public VideoFrame processI420Frame(VideoFrame frame) {

        if (!renderSwitch) {
            return frame;
        }

        int width = frame.getWidth();
        int height = frame.getHeight();
        
        // 美颜处理      

        CpuBufferVideoFrameBuilder builder = new CpuBufferVideoFrameBuilder(VideoPixelFormat.I420);
        builder.setWidth(width)
                .setHeight(height)
                .setRotation(frame.getRotation()) // set rotation back if rotation has not been changed.
                .setTimeStampUs(frame.getTimeStampUs())
                .setColorSpace(frame.getColorSpace())
                .setPlaneData(0, frame.getPlaneData(0))
                .setPlaneData(1, frame.getPlaneData(1))
                .setPlaneData(2, frame.getPlaneData(2))
                .setPlaneStride(0, frame.getPlaneStride(0))
                .setPlaneStride(1, frame.getPlaneStride(1))
                .setPlaneStride(2, frame.getPlaneStride(2))
                .setReleaseCallback(() -> {
                });

        return builder.build();
    }
// 注意:
// - 返回纹理视频帧给 RTC SDK时,需要通过 GLTextureVideoFrameBuilder 辅助构建。
// - 在构建返回的视频帧时,如果你没有处理旋转变换,请将原始的旋转角度设置过来。
java
func processVideoFrame(_ srcFrame: ByteRTCVideoFrame) -> ByteRTCVideoFrame? {
    
    guard let srcPixelBuffer = srcFrame.textureBuf else { return srcFrame }
    CVPixelBufferLockBaseAddress(srcPixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
    
    let width = CVPixelBufferGetWidth(srcPixelBuffer)
    let height = CVPixelBufferGetHeight(srcPixelBuffer)
    
    guard let buffer0 = CVPixelBufferGetBaseAddressOfPlane(srcPixelBuffer, 0),
          let buffer1 = CVPixelBufferGetBaseAddressOfPlane(srcPixelBuffer, 1),
          let buffer2 = CVPixelBufferGetBaseAddressOfPlane(srcPixelBuffer, 2) else { return srcFrame }
    
    // 省略自定义处理buffer部分...
    
    CVPixelBufferUnlockBaseAddress(srcPixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
    
    // 返回处理后的frame
    return srcFrame
}
// 注意:
// 在构建返回的视频帧时,如果你没有处理旋转变换,请将原始的旋转角度设置过来。
swift
// processVideoFrame 中输出的 YUV 数据可能存在对齐问题。例如,YUV 数据中可能存在 y_stride > width, u_stride > width/2, v_stride > width/2 的情况。而示例中使用的第三方美颜 SDK 要求输入不包含 stride 的 YUV 数据,因此,你需要按行将数据拷贝到空 buffer 中,传给第三方美颜 SDK 处理,再将处理后的 YUV 视频帧传回 RTC SDK。
bytertc::IVideoFrame *processVideoFrame(bytertc::IVideoFrame * videoFrame)
    //你可以进行视频画面的自定义处理,yuv数据可能存在对齐问题,
    //即可能出现: y_stride > width, u_stride > width/2, v_stride > width/2
    //如果数据转换中出现花瓶,可参考 
    
    //width: videoFrame->width();
    //height: videoFrame->height();
    
    //y数据指针: videoFrame->getPlaneData(0);
    //u数据指针: videoFrame->getPlaneData(1);
    //v数据指针: videoFrame->getPlaneData(2);
    
    //y的步长:videoFrame->getPlaneStride(0);
    //u的步长:videoFrame->getPlaneStride(1);
    //v的步长:videoFrame->getPlaneStride(2);

// 对 YUV 的步长进行判断,按行或按块拷贝 frameBuffer 中
// YUV 数据按行拷贝 frameBuffer
if (videoFrame->getPlaneStride(0) > width
        || videoFrame->getPlaneStride(1) > (width+1)/2
        || videoFrame->getPlaneStride(2) > (width+1)/2) {

        int dst_offset = 0;
        int src_offset = 0;
        for(int i=0; i<height; i++) {
            memcpy(frameBuffer + dst_offset, videoFrame->getPlaneData(0) + src_offset, width);
            src_offset += videoFrame->getPlaneStride(0);
            dst_offset += width;
        }
        src_offset = 0;
        for (int i=0; i<height/2; i++) {
            memcpy(frameBuffer + dst_offset, videoFrame->getPlaneData(1) + src_offset, width/2);
            src_offset += videoFrame->getPlaneStride(1);
            dst_offset += width/2;
        }
        src_offset = 0;
        for (int i=0; i<height/2; i++) {
            memcpy(frameBuffer + dst_offset, videoFrame->getPlaneData(2) + src_offset, width/2);
            src_offset += videoFrame->getPlaneStride(2);
            dst_offset += width/2;
        }
        // ...
        //省略,自定义处理部分,美颜处理的 YUV buffer 是 frameBuffer
        //将处理完的数据按行传回 SDK
        
        src_offset = 0;
        dst_offset = 0;

        for(int i=0; i<height; i++) {
            memcpy(videoFrame->getPlaneData(0) + dst_offset, frameBuffer + src_offset, width);
            src_offset += width;
            dst_offset += videoFrame->getPlaneStride(0);
        }

        dst_offset = 0;
        for(int i=0; i<height/2; i++) {
            memcpy(videoFrame->getPlaneData(1) + dst_offset, frameBuffer + src_offset, width/2);
            src_offset += width/2;
            dst_offset += videoFrame->getPlaneStride(1);
        }

        dst_offset = 0;
        for(int i=0; i<height/2; i++) {
            memcpy(videoFrame->getPlaneData(2) + dst_offset, frameBuffer + src_offset, width/2);
            src_offset += width/2;
            dst_offset += videoFrame->getPlaneStride(2);
        }

    } else {// YUV 数据按块拷贝 frameBuffer
        memcpy(frameBuffer, videoFrame->getPlaneData(0), width*height);
        memcpy(frameBuffer + width*height, videoFrame->getPlaneData(1), width*height/4);
        memcpy(frameBuffer + width * height *5/4, videoFrame->getPlaneData(2), width*height/4);
        // ...
        //省略,自定义处理部分
        //将处理完的数据按块传回 SDK
        memcpy(videoFrame->getPlaneData(0), frameBuffer, width*height);
        memcpy(videoFrame->getPlaneData(1), frameBuffer + width*height, width*height/4);
        memcpy(videoFrame->getPlaneData(2), frameBuffer + width*height*5/4, width*height/4);
    }
cpp
4. 返回自定义处理视频

经过自定义视频处理后,在 IVideoProcessor.processVideoFrame 中构建的视频帧返回值将被返回给 RTC SDK 进行后续的处理。
各端对返回 RTC SDK 的视频帧格式支持如下:

平台内存类型支持的视频帧像素格式

iOS/Mac

kVideoFrameTypeRawMemory

支持NV12格式的数据

kVideoFrameTypeCVPixelBuffer支持I420、NV12、BGRA、ARGB格式的数据
AndroidkVideoFrameTypeRawMemory支持I420、NV12、NV21、RGBA格式的数据
kVideoFrameTypeGLTexture支持Texture2D、TextureOES格式的数据
Windows/LinuxkVideoFrameTypeRawMemory支持I420、NV12、RGBA、BGRA、ARGB格式的数据
5. 取消注册视频处理器

通过调用 registerLocalVideoProcessor 并将其中的 IVideoProcessor 设置为 null 来实现取消注册自定义视频处理器。完成取消注册之后,你将不会再通过 IVideoProcessor.processVideoFrame 收到采集的视频帧。

示例项目

API 参考

你可以根据上文的描述和示例,使用以下客户端 SDK,在不同的端上实现自定义视频处理。

表格中的 macOS API 接口为 Objective-C,而示例项目中的 macOS 项目使用的是 Windows SDK 中的 API 接口。

平台AndroidiOSmacOSWindowsLinux
视频处理器接口IVideoProcessorByteRTCVideoProcessorDelegateByteRTCVideoProcessorDelegateIVideoProcessorIVideoProcessor
注册视频处理器registerLocalVideoProcessorregisterLocalVideoProcessor:withConfig:registerLocalVideoProcessor:withConfig:RegisterLocalVideoProcessorRegisterLocalVideoProcessor