使用 RTC SDK 实现视频内部采集或外部自定义采集后,在渲染和编码传输前,你可以对视频帧进行自定义处理,并传回 RTC SDK 进行本地预览和编码发送。例如以下场景:
前提条件
你已经集成 RTC SDK,实现了基本的音视频通话。
支持视频自定义处理功能的 SDK 详见API 参考。
功能说明
适用范围
- 适用于:内部摄像头采集视频流、外部自定义摄像头采集视频流
- 不适用于:内部屏幕采集视频流、外部自定义屏幕采集视频流、静态图
此功能在视频处理链路的位置
功能实现
本文以接入第三方美颜 SDK 为例,介绍 RTC SDK 的自定义视频处理的实现方法。参考步骤描述和示例代码,将涉及到美颜处理的部分替换为实际的实现代码。
1. 实现视频处理器接口
首先,你需要自行实现 IVideoProcessor
接口。
public class VideoProcessor extends IVideoProcessor {
@Override
public VideoFrame processVideoFrame(VideoFrame frame) {}
}
@protocol ByteRTCVideoProcessorDelegate <NSObject>
- (ByteRTCVideoFrame* _Nullable)processVideoFrame:(ByteRTCVideoFrame* _Nonnull)srcFrame;
@end
// 继承IVideoProcessor 类,实现 processVideoFrame 纯虚接口,在 processVideoFrame 内完成自定义处理视频过程
class SimpleVideoProcessor : public bytertc::IVideoProcessor {
public:
virtual bytertc::IVideoFrame* processVideoFrame(bytertc::IVideoFrame* src_frame) override;
// 自定义处理 yuv 视频帧
};
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);
}
返回的视频帧格式仅支持 I420 和 Unknow
let config = ByteRTCVideoPreprocessorConfig()
config.requiredPixelFormat = .I420
if customVideoProcessor == nil {
customVideoProcessor = SimpleVideoProcessor()
}
self.rtcVideoKit.registerLocalVideoProcessor(customVideoProcessor!, withConfig: config)
// 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
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 辅助构建。
// - 在构建返回的视频帧时,如果你没有处理旋转变换,请将原始的旋转角度设置过来。
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
}
// 注意:
// 在构建返回的视频帧时,如果你没有处理旋转变换,请将原始的旋转角度设置过来。
// 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);
}
4. 返回自定义处理视频
经过自定义视频处理后,在 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格式的数据 |
5. 取消注册视频处理器
通过调用 registerLocalVideoProcessor
并将其中的 IVideoProcessor
设置为 null
来实现取消注册自定义视频处理器。完成取消注册之后,你将不会再通过 IVideoProcessor.processVideoFrame
收到采集的视频帧。
示例项目
API 参考
你可以根据上文的描述和示例,使用以下客户端 SDK,在不同的端上实现自定义视频处理。
表格中的 macOS API 接口为 Objective-C,而示例项目中的 macOS 项目使用的是 Windows SDK 中的 API 接口。