当你使用 RTC SDK 实现视频内部采集或外部自定义采集后,在渲染和编码传输前,你可以自定义逻辑,对视频帧进行处理。
注意:
RTC SDK 和 CV SDK 进行了深度整合,联合提供强大且较下述方案更易集成的 RTC-CV 联合集成方案,强烈建议你使用联合方案,详情请咨询技术支持同学。
Android、iOS、Mac、Windows、Linux。


首先,你需要自行实现 IVideoProcessor 接口。下表提供了各端实现 IVideoProcessor 的示例代码,以在画面右上方添加一段颜色条为例,展示如何完成一个简单的视频处理。各端稍有差异,请以实际示例代码为准。
| 平台 | 示例代码 | 处理效果 |
|---|---|---|
iOS/macOS | 【附件下载】: SimpleVideoProcessor.h,大小为 187.00Bytes【附件下载】: SimpleVideoProcessor.mm,大小为 4.21KB |
|
Android | 【附件下载】: SimpleVideoProcessor.java,大小为 10.91KB | |
【附件下载】: 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 |
实现 IVideoProcessor 接口后,你需要将其注册进 RTC SDK 中,只有完成注册后,自定义视频处理器才会获取到采集的视频帧。在注册时,你可以指定 RTC SDK 返回给 IVideoProcessor.processVideoFrame 的视频帧格式,各端支持的视频帧格式如下:
| 平台 | 内存类型 | 支持的视频帧像素格式 | 示例代码 |
|---|---|---|---|
iOS/macOS | kVideoFrameTypeRawMemory | / | / |
kVideoFrameTypeCVPixelBuffer | ByteRTCVideoPixelFormatUnknown(不推荐)、 | | |
Android | kVideoFrameTypeRawMemory | kVideoPixelFormatUnknown(不推荐)、 | |
kVideoFrameTypeGLTexture | kVideoPixelFormatUnknown(不推荐)、 | | |
Windows/Linux | kVideoFrameTypeRawMemory | kVideoPixelFormatUnknown(不推荐)、 | |
注意:
重复调用
registerLocalVideoProcessor接口时,仅最后一次调用生效。
kVideoFrameTypeRawMemory:与平台无关的原始 CPU 内存 Buffer,用来存放视频帧数据。
kVideoFrameTypeCVPixelBuffer:CVPixelBufferRef 类型,在 iOS/Mac 上使用,是视频帧数据的平台层包装。
kVideoFrameTypeGLTexture:纹理类型,视频帧数据存储在方便 GPU 进行访问的地方并通过纹理 ID 来标识。
kVideoPixelFormatUnknown:RTC SDK 回调给IVideoProcessor.processVideoFrame的视频帧格式,即 RTC SDK 内部视频处理时使用的格式。为避免调用registerLocalVideoProcessor时设置了与 RTC SDK 内部处理不同的格式而有可能导致的格式转换,不推荐使用。
将自定义视频处理器注册到 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 中处理了纹理矩阵转换,如果你的实现没有处理纹理矩阵转换,在构建返回的视频帧时,请将原始的纹理矩阵设置过来。
在构建返回的视频帧时,如果你没有处理旋转变换,请将原始的旋转角度设置过来。
经过自定义视频处理后,在 IVideoProcessor.processVideoFrame 中构建的视频帧返回值将被返回给 RTC SDK 进行后续的处理。
各端对返回 RTC SDK 的视频帧格式支持如下:
| 平台 | 内存类型 | 支持的视频帧像素格式 |
|---|---|---|
iOS/Mac | kVideoFrameTypeRawMemory | 支持NV12格式的数据 |
| kVideoFrameTypeCVPixelBuffer | 支持I420、NV12、BGRA、ARGB格式的数据 | |
| Android | kVideoFrameTypeRawMemory | 支持I420、NV12、NV21、RGBA格式的数据 |
| kVideoFrameTypeGLTexture | 支持Texture2D、TextureOES格式的数据 | |
| Windows/Linux | kVideoFrameTypeRawMemory | 支持I420、NV12、RGBA、BGRA、ARGB格式的数据 |
通过调用 registerLocalVideoProcessor 并将其中的 IVideoProcessor 设置为 null 来实现取消注册自定义视频处理器。完成取消注册之后,你将不会再通过 IVideoProcessor.processVideoFrame 收到采集的视频帧。
以下是各个平台的相关 API 参考: