当你使用 RTC SDK 实现视频内部采集或外部自定义采集后,在渲染和编码传输前,你可以自定义逻辑,对视频帧进行处理。

功能简介
适用场景

注意:
RTC SDK 和 CV SDK 进行了深度整合,联合提供强大且较下述方案更易集成的 RTC-CV 联合集成方案,强烈建议你使用联合方案,详情请咨询技术支持同学。

适用平台

Android、iOS、Mac、Windows、Linux。

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

alt

集成步骤
交互时序

alt

实现步骤
1. 实现视频处理器接口

首先,你需要自行实现 IVideoProcessor 接口。下表提供了各端实现 IVideoProcessor 的示例代码,以在画面右上方添加一段颜色条为例,展示如何完成一个简单的视频处理。各端稍有差异,请以实际示例代码为准。

平台示例代码处理效果

iOS/macOS

【附件下载】: SimpleVideoProcessor.h,大小为 187.00Bytes【附件下载】: SimpleVideoProcessor.mm,大小为 4.21KB

Android

【附件下载】: SimpleVideoProcessor.java,大小为 10.91KB

下列 Android 代码文件来自 webrtc 开源代码,是封装了 OpenGL API 的工具类。在 SimpleVideoProcessor.java 示例代码中处理纹理类型的视频帧时使用到了这些工具类。

【附件下载】: GlGenericDrawer.java,大小为 27.39KB【附件下载】: GlUtil.java,大小为 2.12KB【附件下载】: GlShader.java,大小为 4.84KB【附件下载】: RendererCommon.java,大小为 10.63KB【附件下载】: FilterType.java,大小为 608.00Bytes

Windows/Linux

【附件下载】: simple_video_processor.h,大小为 414.00Bytes【附件下载】: simple_video_processor.cc,大小为 935.00Bytes
2. 注册视频处理器

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

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

iOS/macOS

kVideoFrameTypeRawMemory

/

/

kVideoFrameTypeCVPixelBuffer

ByteRTCVideoPixelFormatUnknown(不推荐)、
ByteRTCVideoPixelFormatI420

ByteRTCVideoPreprocessorConfig* config = [[ByteRTCVideoPreprocessorConfig alloc] init];
config.required_pixel_format = ByteRTCVideoPixelFormatI420;
if (customVideoProcessor_ == nil) {
    customVideoProcessor_ = [[SimpleVideoProcessor alloc] init];
}
[self.rtcVideoKit registerLocalVideoProcessor:customVideoProcessor_ withConfig:config];
objectivec

Android

kVideoFrameTypeRawMemory

kVideoPixelFormatUnknown(不推荐)、
kVideoPixelFormatI420

VideoPreprocessorConfig config = new VideoPreprocessorConfig();
config.required_pixel_format = VideoPixelFormat.kVideoPixelFormatI420
if(mCustomVideoProcessor == null) {
    mCustomVideoProcessor = new SimpleVideoProcessor();
}
mRtcVideo.registerLocalVideoProcessor(mCustomVideoProcessor, config);
java

kVideoFrameTypeGLTexture

kVideoPixelFormatUnknown(不推荐)、
kVideoPixelFormatTexture2D

VideoPreprocessorConfig config = new VideoPreprocessorConfig();
config.required_pixel_format = VideoPixelFormat.kVideoPixelFormatTexture2D
if(mCustomVideoProcessor == null) {
    mCustomVideoProcessor = new SimpleVideoProcessor();
}
mRtcVideo.registerLocalVideoProcessor(mCustomVideoProcessor, config);
java

Windows/Linux

kVideoFrameTypeRawMemory

kVideoPixelFormatUnknown(不推荐)、
kVideoPixelFormatI420

bytertc::VideoPreprocessorConfig config;
config.required_pixel_format = bytertc::kVideoPixelFormatI420;
if(!mCustomVideoProcessor) {
    mCustomVideoProcessor.reset(new SimpleVideoProcessor());
}
rtc_engine_->registerLocalVideoProcessor(processor, config);
cpp

注意:

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

  • kVideoFrameTypeRawMemory:与平台无关的原始 CPU 内存 Buffer,用来存放视频帧数据。

  • kVideoFrameTypeCVPixelBuffer:CVPixelBufferRef 类型,在 iOS/Mac 上使用,是视频帧数据的平台层包装。

  • kVideoFrameTypeGLTexture:纹理类型,视频帧数据存储在方便 GPU 进行访问的地方并通过纹理 ID 来标识。

  • kVideoPixelFormatUnknown:RTC SDK 回调给IVideoProcessor.processVideoFrame 的视频帧格式,即 RTC SDK 内部视频处理时使用的格式。为避免调用 registerLocalVideoProcessor时设置了与 RTC SDK 内部处理不同的格式而有可能导致的格式转换,不推荐使用。

3. 完成自定义视频处理

将自定义视频处理器注册到 RTC SDK 后,App 会通过 IVideoProcessor.processVideoFrame 回调收到采集的视频帧,你需要在这里完成自定义的视频处理操作。示例代码参考步骤1:实现视频处理器接口。

关于示例代码的补充说明
// out-of-place
- (ByteRTCVideoFrame*) processI420FrameOutOfPlace:(ByteRTCVideoFrame* _Nonnull)srcFrame {
    CVPixelBufferRef srcPixelBuffer = srcFrame.textureBuf;
    CVPixelBufferLockBaseAddress(srcPixelBuffer, 0); 

    CVPixelBufferRef dstPixelBuffer = NULL;
    // [OpenGLES Compatibility] To create a CVOpenGLESTexture object successfully, the pixel buffer passed to
    // CVOpenGLESTextureCacheCreateTextureFromImage() must be backed by an IOSurface.
    NSDictionary *pixelBufferAttributes = @{(id)kCVPixelBufferIOSurfacePropertiesKey : @{}};       
    int width = CVPixelBufferGetWidth(srcPixelBuffer);
    int height = CVPixelBufferGetHeight(srcPixelBuffer);
    OSType format = CVPixelBufferGetPixelFormatType(srcPixelBuffer);
    CVReturn ret = CVPixelBufferCreate(kCFAllocatorDefault,
                                       width,
                                       height,
                                       format,
                                       (__bridge CFDictionaryRef)(pixelBufferAttributes),
                                       &dstPixelBuffer);
    if (ret != kCVReturnSuccess || dstPixelBuffer == nil) { 
        // failed to create a pixel buffer.
        return nil;
    }

    // 省略
    ...

    ByteRTCVideoFrame *retFrame = [[ByteRTCVideoFrame alloc] init];
    retFrame.format = ByteRTCVideoPixelFormatCVPixelBuffer; // 返回格式为CVPixelBuffer。
    retFrame.height = height;
    retFrame.width = width;
    retFrame.textureBuf = dstPixelBuffer;  // 将返回的CVPixelBuffer设置给textureBuf。
    retFrame.rotation = srcFrame.rotation; // 如果你没有处理旋转变换,请将原始的旋转角度设置过来。
    return retFrame;
}
objectivec

注意:

  • 通常推荐返回给 RTC SDK 的视频帧格式为 ByteRTCVideoPixelFormatCVPixelBuffer,并将返回的 CVPixelBuffer 设置给 ByteRTCVideoFrame的textureBuf 字段。

  • 在创建返回给 RTC SDK 的 CVPixelBuffer 时,请使用kCVPixelBufferIOSurfacePropertiesKey 以兼容 OpenGLES。

  • 在构建返回的视频帧时,如果你没有处理旋转变换,请将原始的旋转角度设置过来。

public VideoFrame processI420Frame(VideoFrame frame) {
        if (mYUVBuffersAvailable.get() == false) {
            // buffer is not available (not released), drop this frame.
            return null;
        }
        // 省略代码
        ...
        // 通过复用一个YUV buffer,可以优化内存的分配和释放开销,记得要在setReleaseCallback回调中更新buffer的可用状态。
        mYUVBuffersAvailable.set(false);
        // 当处理I420格式RawMemory时,通常返回给RTC SDK的视频帧也是I420格式的RawMemory,需要通过CpuBufferVideoFrameBuilder
        // 辅助构建。
        CpuBufferVideoFrameBuilder builder = new CpuBufferVideoFrameBuilder(VideoPixelFormat.kVideoPixelFormatI420);
        builder.setWidth(width)
                .setHeight(height)
                .setRotation(frame.getRotation()) // 如果你没有处理旋转变换,请将原始的旋转角度设置过来。
                .setTimeStampUs(frame.getTimeStampUs())
                .setColorSpace(frame.getColorSpace())
                .setPlaneData(0, mYUVBuffers[0].slice())
                .setPlaneData(1, mYUVBuffers[1].slice())
                .setPlaneData(2, mYUVBuffers[2].slice())
                .setPlaneStride(0, strip_y)
                .setPlaneStride(1, strip_u)
                .setPlaneStride(2, strip_v)
                .setReleaseCallback(() -> {
                    // 更新buffer可用
                    mYUVBuffersAvailable.set(true);
                });

        return builder.build();
}
java

注意:

  • 示例代码在处理 I420 格式视频帧时使用了一个 mYUVBuffers 来优化内存的分配和释放,需在 setReleaseCallback 回调中更新 mYUVBuffers 的可用状态。

  • 当处理 I420 格式 RawMemory 时,通常返回给 RTC SDK 的视频帧也是 I420 格式的 RawMemory,需要通过 CpuBufferVideoFrameBuilder 辅助构建。

  • 在构建返回的视频帧时,如果你没有处理旋转变换,请将原始的旋转角度设置过来。

@Override
public void onGLEnvInitiated() {
    // you can init Gl resource here or later in processTextureFrame.
}

@Override
public void onGLEnvRelease() {
    // you need to release Gl resource here.
    releaseGl();
}

void releaseGl() {
    if (mTextureDrawer != null) {
        mTextureDrawer.release();
        mTextureDrawer = null;
    }
    if (mFrameBuffer != null) {
        mFrameBuffer.dispose();
        mFrameBuffer = null;
    }
}

public synchronized VideoFrame processTextureFrame(VideoFrame frame) {
    // we can init gl resource in here if we don't init it in onGLEnvInitiated callback.
    if (mFrameBuffer == null || mFrameBuffer.mTexWidth != frame.getWidth()
            || mFrameBuffer.mTexHeight != frame.getHeight()) { // resize
        mFrameBuffer =  new FrameBuffer(frame.getWidth(), frame.getHeight());
    }
    if (mTextureDrawer == null) { // create drawer
        mTextureDrawer =  new GlColorEffect(shaderCallback);
    }

    float c = mFrameCount % 30;
    shaderCallback.greenV = (c+1) / 30; // update value of green channel.

    // draw color effect.
   
    mFrameBuffer.attachCurrent();
    // do matix transform. note: we should set maxtrix back if transform has not been changed.
    float[] tex_mat = frame.getTextureMatrix();
    if (tex_mat == null) {
        tex_mat = TEX_MATRIX;
    }
    int target = frame.getPixelFormat() == VideoPixelFormat.kVideoPixelFormatTextureOES ?
            FORMAT_TEXTURE_OES : FORMAT_TEXTURE_2D;        
    drawTexture(target, frame.getTextureID(), frame.getWidth(), frame.getHeight(), tex_mat);
    mFrameBuffer.detachCurrent();

    // use GLTextureVideoFrameBuilder to return a texture frame.
    GLTextureVideoFrameBuilder builder = new GLTextureVideoFrameBuilder(VideoPixelFormat.kVideoPixelFormatTexture2D);
    builder.setEGLContext(EGL14.eglGetCurrentContext()) // set current gl context
            .setTextureID(mFrameBuffer.mTextureId)
            .setWidth(frame.getWidth())
            .setHeight(frame.getHeight())
            .setRotation(frame.getRotation()) // set rotation back if rotation has not been changed.
            .setTimeStampUs(frame.getTimeStampUs());

    return builder.build();
}
java

注意:

  • RTC SDK 会在某些情况下重置 GL 环境(例如切换前后置摄像头),此时 RTC SDK 会给自定义视频处理器回调 onGLEnvRelease 和 onGLEnvInitiated。你需要在 onGLEnvRelease 释放旧的 GL 资源,并在 onGLEnvInitiated 或者在 processTextureFrame 申请新的 GL 资源。

  • 返回纹理视频帧给 RTC SDK时,需要通过 GLTextureVideoFrameBuilder 辅助构建。

  • 示例代码在 drawTexture 中处理了纹理矩阵转换,如果你的实现没有处理纹理矩阵转换,在构建返回的视频帧时,请将原始的纹理矩阵设置过来。

  • 在构建返回的视频帧时,如果你没有处理旋转变换,请将原始的旋转角度设置过来。

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 参考

以下是各个平台的相关 API 参考: