自定义视频渲染

本文将为您介绍如何实现外部视频渲染。

功能介绍

ARTC 内置了经过市场广泛验证的视频渲染模块,推荐客户优先使用,以确保获得稳定、高效的视频播放体验。

对于已具备成熟自研渲染能力的客户,或在色彩精度、帧率控制等方面有特殊需求的场景,ARTC SDK 也提供灵活的接口支持,允许接入自定义视频渲染模块,满足多样化的业务需求。

前提条件

在设置视频配置之前,请确保达成以下条件:

技术原理

远端视频渲染

image

本地视频渲染-Buffer

image

本地视频渲染-textureID

image

功能实现

Android端实现

1 注册监听

首先通过AliRtcEngine所提供的registerVideoSampleObserver接口来注册自定义处理观察类,然后实现这个抽象类AliRtcEngine.AliRtcVideoObserver的各方法。即可在AliRtcEngine全流程链路中,进行美颜处理。

注册监听接口AliRtcVideoObserver代码示例:

mAliRtcEngine.registerVideoSampleObserver(mAliRtcVideoObserver);

根据实际业务需要,实现抽象接口类。ARTC支持本地视频采集后、本地编码前、远程视频解码后三个阶段的自定义处理。

public enum AliRtcVideoObserPosition{
        /*! 采集视频数据,对应输出回调 onLocalVideoSample */
        AliRtcPositionPostCapture(1),
        /*! 渲染视频数据,对应输出回调 onRemoteVideoSample */
        AliRtcPositionPreRender(2),
        /*! 编码前视频数据,对应输出回调 onPreEncodeVideoSample */
        AliRtcPositionPreEncoder(4);
    }
public static abstract class AliRtcVideoObserver {
    // 订阅的本地采集视频数据回调
    public boolean onLocalVideoSample(AliRtcVideoSourceType sourceType, AliRtcVideoSample videoSample){
        // TODO: 如果需要本地视频采集环节,需要美颜特效处理,在此处处理。
    }

    // 订阅的远端视频数据回调
    public boolean onRemoteVideoSample(String callId,AliRtcVideoSourceType sourceType, AliRtcVideoSample videoSample){
        // TODO: 如果需要远端拉取后的画面在显示之前,需要美颜特效处理,在此处处理。
    }

    // 订阅的本地编码前视频数据回调
    public boolean onPreEncodeVideoSample(AliRtcVideoSourceType sourceType, AliRtcVideoSample videoRawData){
        // TODO: 如果需要在本地视频画面进行编码之前,需要美颜特效处理,在此处处理。
    }

    ...

    public int onGetObservedFramePosition(){
        // TODO: 此处根据业务需要,参照上面定义的AliRtcVideoObserPosition值,指定需要回调的处理时机。
        // 例如,需要在采集和预渲染前进行自定义处理,定义以下值。
        // return AliRtcVideoObserPosition.AliRtcPositionPostCapture.getValue() | AliRtcVideoObserPosition.AliRtcPositionPreRender.getValue();
    }
}
说明

美颜SDK的引入使用,根据业务需要,分别在对应的接口处理视频流即可。

关闭监听

在不需要自定义处理的情况下,可通过关闭自定义处理监听,来减少SDK层对外调用传递,提升处理效率,避免内存泄漏。

mAliRtcEngine.unRegisterVideoSampleObserver(
  // TODO:作资源释放相关工作
)

2 自定义处理

上述回调接口都可调用handleBeautyProcess方法实现。

private boolean handleBeautyProcess(AliRtcEngine.AliRtcVideoSample videoSample) {
    if (!isAdvanceBeautifyOn) {     // 是否开启美颜
        return false;
    }
    if (mQueenBeautyImp == null) {
        mQueenBeautyImp = new QueenBeautyImp(getContext(), videoSample.glContex);
    }

    return mQueenBeautyImp.onBeautyProcess(videoSample);
}
说明

QueenBeautyImp是对QueenBeautyEffector的简单包装类

2.1 创建美颜处理器

创建方法如下:

// 增加同步锁,防止创建多个
private synchronized void ensureQueenEngine(Context context, long glShareContext) {
        if (mQueenBeautyEffector == null) {
            try {
                QueenConfig queenConfig = new QueenConfig();
                bool isNeedCreateNewThread = glShareContext != 0;// 是否需要创建独立线程,纹理模式推荐为true,buffer模式推荐为false
                bool isNeedCreateNewGLContext = true; // 是否需要创建GL上下文,纹理模式保持与isNeedCreateNewThread的值一致,buffer模式推荐为true。
                queenConfig.withNewGlThread = isNeedCreateNewThread; 
                queenConfig.withContext = isNeedCreateNewGLContext;
                queenConfig.shareGlContext = glShareContext;
//                queenConfig.enableDebugLog = true;   // 调试功能-打开日志
                mQueenBeautyEffector = new QueenBeautyEffector(context, queenConfig);
                // 高级美颜调试功能
//                mQueenBeautyEffector.getEngine().enableFacePointDebug(true);    // 开启人脸关键点调试
//                mQueenBeautyEffector.getEngine().enableFaceDetectGPUMode(false);  // 关闭人脸检测GPU模式
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
2.2 美颜参数设置
private void updateQueenEngineParams() {
    mQueenBeautyEffector.onUpdateParams(() -> {
        QueenEngine queenEngine = mQueenBeautyEffector.getEngine();
        // 磨皮&锐化,共用一个功能开关
        queenEngine.enableBeautyType(BeautyFilterType.kSkinBuffing, true);//磨皮开关
        queenEngine.setBeautyParam(com.aliyun.android.libqueen.models.BeautyParams.kBPSkinBuffing, 0.85f);  //磨皮 [0,1]
        queenEngine.setBeautyParam(com.aliyun.android.libqueen.models.BeautyParams.kBPSkinSharpen, 0.2f);  //锐化 [0,1]
        // 美白&红润,共用一个功能开关
        queenEngine.enableBeautyType(BeautyFilterType.kSkinWhiting, true);//美白开关
        queenEngine.setBeautyParam(BeautyParams.kBPSkinWhitening, 0.5f);  //美白范围 [0,1]

        // 大眼,瘦脸
        queenEngine.enableBeautyType(BeautyFilterType.kFaceShape, true);
        queenEngine.updateFaceShape(FaceShapeType.typeBigEye,1.0f);
        queenEngine.updateFaceShape(FaceShapeType.typeCutFace,1.0f);
    });
}
2.3 处理帧

根据回调数据类型,区分处理是纹理,还是buffer。

// 增加同步锁,防止多线程下mQueenBeautyEffector已被销毁
public synchronized boolean onBeautyProcess(AliRtcEngine.AliRtcVideoSample videoSample) {
    // 更新美颜参数
    updateQueenEngineParams();

    boolean result = false;
    if (videoSample.glContex != 0 && videoSample.textureid > 0) {
        // 纹理模式
        result = onProcessBeautyTexture(videoSample);
    } else {
        // buffer模式
        result = onProcessBeautyBuffer(videoSample);
    }
    return result;
}

纹理回调的处理:

private boolean onProcessBeautyTexture(AliRtcEngine.AliRtcVideoSample videoSample) {
    boolean result = false;
    boolean isOesTexture = videoSample.format == AliRtcEngine.AliRtcVideoFormat.AliRtcVideoFormatTextureOES;
    // 因Android相机采集纹理默认是旋转270度后的横屏画面,Queen-sdk内部会自动进行宽高互换。
    // 但此处videoSample回调的宽高,rtc-sdk内部也已进行修正,因此此处需要手动进行宽高互换
    int w = isOesTexture ? videoSample.height : videoSample.width;
    int h = isOesTexture ? videoSample.width : videoSample.height;
    int newTextId = mQueenBeautyEffector.onProcessTexture((int)videoSample.textureid, isOesTexture, videoSample.matrix, w, h, 270, 0, 0);

    if (newTextId != videoSample.textureid) {     // 0-QueenResult.QUEEN_OK
        // 修改纹理id
        videoSample.textureid = newTextId;
        videoSample.format = AliRtcEngine.AliRtcVideoFormat.AliRtcVideoFormatTexture2D;

        result = true;
    }
    return result;
}

buffer回调的处理:

private boolean onProcessBeautyBuffer(AliRtcEngine.AliRtcVideoSample videoSample) {
    boolean result = false;
    int queenResult = mQueenBeautyEffector.onProcessDataBuf(videoSample.data, videoSample.data, ImageFormat.I420, videoSample.width, videoSample.height, 0, 0, 0, 0);
    if (queenResult == 0) {
        result = true;
    }
    return result;
}
3 退出销毁

在离会,或者离开视频通话界面时,及时销毁自定义处理引擎。

// 增加同步锁,防止多线程下mQueenBeautyEffector已被销毁
public synchronized void release() { 
    if (mQueenBeautyEffector != null) {
        mQueenBeautyEffector.onReleaseEngine();
        mQueenBeautyEffector = null;
    }
}

iOS端实现

Buffer处理接入

1 注册监听

首先,自定义接入实现Delegate接口AliRtcEngineDelegate,并在创建AliRtcEngine引擎时传入。

其次,声明需要注册监听视频处理观察器[self.engine registerVideoSampleObserver];

此外,重点覆盖实现几个接口,可以对兴趣监听事件精准控制。

//视频数据输出位置。默认全部返回
- (NSInteger)onGetVideoObservedFramePosition {
    // 返回一个或多个组合
    // AliRtcPositionPostCapture :本地采集视频数据,对应输出回调onCaptureVideoSample
    // AliRtcPositionPreRender :远端渲染视频数据,对应输出回调onRemoteVideoSample
    // AliRtcPositionPreEncoder:编码前视频数据,对应输出回调onPreEncodeVideoSample
    return AliRtcPositionPreRender|AliRtcPositionPostCapture;
}

// 指定视频数据输出格式,可不指定。
- (AliRtcVideoFormat)onGetVideoFormatPreference {
    return AliRtcVideoFormat_cvPixelBuffer;
//    return AliRtcVideoFormat_Texture2D;
}
2 自定义处理:
//本地采集视频数据回调
- (BOOL)onCaptureVideoSample:(AliRtcVideoSource)videoSource videoSample:(AliRtcVideoDataSample *)videoSample {
       return [self processVideoBeauty:videoSample];
}

//订阅的远端视频数据回调
- (BOOL)onRemoteVideoSample:(NSString *)uid videoSource:(AliRtcVideoSource)videoSource videoSample:(AliRtcVideoDataSample *)videoSample {
       return [self processVideoBeauty:videoSample];
}
2.1 创建美颜处理器

上述回调接口都可调用handleBeautyProcess方法实现。

- (bool)processVideoBeauty:(AliRtcVideoDataSample *)videoSample {
    
    if(videoSample) {
        if(videoSample.type == AliRtcBufferType_CVPixelBuffer) {
            if(videoSample.pixelBuffer) {
                return [self handleBeautyProcessBuffer:videoSample.pixelBuffer];
            }
            return false ;
        }
    }
    
    return false ;
}
- (bool)handleBeautyProcessBuffer:(CVPixelBufferRef)pixelBufferRef {
    if (!self.beautyEngine) {
        [self initBeautyEngine:YES];
    }
    if (self.beautyEngine && pixelBufferRef)
    {
        QEPixelBufferData *bufferData = [QEPixelBufferData new];
        bufferData.bufferIn = pixelBufferRef;
        bufferData.bufferOut = pixelBufferRef;
        // 对pixelBuffer进行图像处理,输出处理后的buffer
        kQueenResultCode resultCode = [self.beautyEngine processPixelBuffer:bufferData];//执行此方法的线程需要始终是同一条线程
        if (resultCode == kQueenResultCodeOK && bufferData.bufferOut)
        {
            return YES;
        }
    }
    return NO;
}

initBeautyEngine创建美颜engine

2.2 美颜参数设置
#pragma mark 美颜相关处理
- (void)initBeautyEngine:(BOOL)processBuffer
{
    if (self.beautyEngine != nil) {
        return;
    }
    // 初始化引擎配置信息对象
    QueenEngineConfigInfo *configInfo = [QueenEngineConfigInfo new];

    // 设置是否自动设置图片旋转角度,如设备锁屏,并且默认图像采集来自摄像头的话可以设置自动设置图片旋转角度
    configInfo.autoSettingImgAngle = YES;
    configInfo.runOnCustomThread = processBuffer ? NO : YES; // 纹理调用时,为YES,buffer调用时,为NO,NO表示不运行在当前调用者线程,内部会创建一个新线程
    configInfo.withContext = processBuffer ? YES : NO;        // 纹理调用时,为NO,buffer调用时,为YES,YES表示需要内部创建glContext

    // 调试接口,开启美颜sdk日志功能
    // configInfo.enableDebugLog = YES;
    
    // 引擎初始化
    self.beautyEngine = [[QueenEngine alloc] initWithConfigInfo:configInfo];
    
    // 基础美颜
    //磨皮
    [_beautyEngine setQueenBeautyType:kQueenBeautyTypeSkinBuffing enable:YES mode:kQueenBeautyFilterModeSkinBuffing_Natural];
    // 设置磨皮系数
    [self.beautyEngine setQueenBeautyParams:kQueenBeautyParamsSkinBuffing value:0.5f];
    // 设置锐化系数
    [self.beautyEngine setQueenBeautyParams:kQueenBeautyParamsSharpen value:0.5f];
    
    //美白
    [_beautyEngine setQueenBeautyType:kQueenBeautyTypeSkinWhiting enable:YES];
    // 设置美白系数
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsWhitening value:0.5f];
    // 高级美颜
    [_beautyEngine setQueenBeautyType:kQueenBeautyTypeFaceBuffing enable:YES];
    //祛皱纹
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsWrinkles value:0.5f];
    // 设置去眼袋系数
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsPouch value:0.5f];
    // 设置去法令纹系数
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsNasolabialFolds value:0.5f];
    // 设置白牙系数
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsWhiteTeeth value:0.5f];
    
    // 打开美型功能开关
    [_beautyEngine setQueenBeautyType:kQueenBeautyTypeFaceShape enable:YES mode:kQueenBeautyFilterModeFaceShape_Main];
    //瘦脸
    [_beautyEngine setFaceShape:kQueenBeautyFaceShapeTypeCutFace value:0.5f];
    //大眼
    [_beautyEngine setFaceShape:kQueenBeautyFaceShapeTypeBigEye value:0.9f];
    
    // 调试接口-展示人脸识别特征点
    //[self.beautyEngine showFaceDetectPoint:YES];
    
}
2.3 处理帧

buffer回调的处理:

- (bool)handleBeautyProcessBuffer:(CVPixelBufferRef)pixelBufferRef {
    if (!self.beautyEngine) {
        [self initBeautyEngine];
    }
    if (self.beautyEngine && pixelBufferRef)
    {
        QEPixelBufferData *bufferData = [QEPixelBufferData new];
        bufferData.bufferIn = pixelBufferRef;
        bufferData.bufferOut = pixelBufferRef;
        // 对pixelBuffer进行图像处理,输出处理后的buffer
        kQueenResultCode resultCode = [self.beautyEngine processPixelBuffer:bufferData];//执行此方法的线程需要始终是同一条线程
        if (resultCode == kQueenResultCodeOK && bufferData.bufferOut)
        {
            return YES;
        }
    }
    return NO;
}

纹理回调的处理:

- (bool)handleBeautyProcessTexture:(int)textureID withWidth:(int)width withHeight:(int)height {
    // 见下文纯纹理处理模式
}
3 退出销毁

反注册事件监听。

/* I420/cvPixelBuffer */
unregisterVideoSampleObserver

销毁美颜beautyEngine。

    // buffer处理时,因内部有异步线程。因此,可在销毁RTCEngine时一并调用销毁引擎。
    // 注意销毁顺序,需要确保RTCEngine中间不会再次调用beautyEngine,可将其放置最后释放
- (void)dealloc {   
    ...
    if (nil != self.beautyEngine) {
         [self.beautyEngine destroyEngine];
         self.beautyEngine = nil;
    }
}

纯纹理接入

1 注册监听

首先,自定义接入实现Delegate接口AliRtcEngineDelegate,并在创建AliRtcEngine引擎时传入。

其次,声明需要注册监听视频处理观察器[self.engine registerLocalVideoTexture];

[_engine registerLocalVideoTexture];	// 开启监听采集阶段的纹理回调
2 自定义处理

实现Delegate接口AliRtcEngineDelegate有以下3个方法。

- (void)onTextureCreate:(void *)context {
    // DO NOTHING,也可美颜引擎在此处初始化,本示例统一放到handleBeautyProcessTexture内部
}

- (int)onTextureUpdate:(int)textureId width:(int)width height:(int)height videoSample:(AliRtcVideoDataSample *_Nonnull)videoSample{
    
    if (self.settingModel.openRaceBeauty) {
        return [self handleBeautyProcessTexture:textureId withWidth:videoSample.width withHeight:videoSample.height];
    }
    return textureId;
}

- (void)onTextureDestory
{
    // 纹理调用时,必须在此处进行销毁引擎,以便保证与创建engine在同一个线程上调用
    if (nil != self.beautyEngine) {
         [self.beautyEngine destroyEngine];
         self.beautyEngine = nil;
    }
}
3 创建美颜处理器
- (void)initBeautyEngine:(BOOL)processBuffer
{
    if (self.beautyEngine != nil) {
        return;
    }
    // 初始化引擎配置信息对象
    QueenEngineConfigInfo *configInfo = [QueenEngineConfigInfo new];

    // 设置是否自动设置图片旋转角度,如设备锁屏,并且默认图像采集来自摄像头的话可以设置自动设置图片旋转角度
    configInfo.autoSettingImgAngle = YES;
    
    configInfo.runOnCustomThread = processBuffer ? NO : YES; // 纹理调用时,为YES,buffer调用时,为NO,NO表示不运行在当前调用者线程,内部会创建一个新线程
    configInfo.withContext = processBuffer ? YES : NO;        // 纹理调用时,为NO,buffer调用时,为YES,YES表示需要内部创建glContext
    
    // 调试接口,开启美颜sdk日志功能
    // configInfo.enableDebugLog = YES;

    // 引擎初始化
    self.beautyEngine = [[QueenEngine alloc] initWithConfigInfo:configInfo];
    
    ...
    // 其他保持相同
}
4 处理帧
- (int)handleBeautyProcessTexture:(int)textureID withWidth:(int)width withHeight:(int)height {
    if (!self.beautyEngine) {
        [self initBeautyEngine:NO];
    }

    // 高级美颜功能需要人脸检测算法,因此需要额外传入bufData。
    // 如果仅开基础美颜,则不用调用updateInputDataAndRunAlg方法。
    uint8_t* bufData = [self extractI420DataFromPixelBuffer:videoSample.pixelBuffer];
    [self.beautyEngine updateInputDataAndRunAlg:bufData
                                            withImgFormat:kQueenImageFormatNV12
                                            withWidth:width
                                            withHeight:height
                                             withStride:0
                                         withInputAngle:0
                                        withOutputAngle:0
                                           withFlipAxis:0];
    free(bufData);
    
    QETextureData* textureData = [[QETextureData alloc] init];
    textureData.inputTextureID = textureID;
    textureData.width = width;
    textureData.height = height;
    kQueenResultCode result = [self.beautyEngine processTexture:textureData];
    if (result != kQueenResultCodeOK)
    {
        return textureID;
    }
    return textureData.outputTextureID;
}

纹理模式处理时,为提升整体性能,避免从纹理中再次提取buffer数据,因此需要应用层,将buffer提取出来传递给美颜SDK,调用接口。

以下为buffer提取示例代码:

-(uint8_t *)extractI420DataFromPixelBuffer:(CVPixelBufferRef) pixelBuffer {
    uint8_t *bufData = nil;
    // 获取格式信息
    OSType pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
    size_t bufWidth = CVPixelBufferGetWidth(pixelBuffer);
    size_t bufHeight = CVPixelBufferGetHeight(pixelBuffer);
        
    // 锁定buffer
    CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
    
    if (CVPixelBufferIsPlanar(pixelBuffer)) {
        if (pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
            uint8_t *yPlane = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
            uint8_t *uvPlane = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);

            size_t yStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
            size_t uvStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);

            size_t width = CVPixelBufferGetWidth(pixelBuffer);
            size_t height = CVPixelBufferGetHeight(pixelBuffer);
            // 分配标准NV12大小(无padding)
            size_t bufDataSize = width * height * 3 / 2;
            bufData = (uint8_t *)malloc(bufDataSize);
            // 逐行拷贝Y平面(去除stride padding)
            for (size_t i = 0; i < height; i++) {
                memcpy(bufData + i * width, yPlane + i * yStride, width);
            }
            // 逐行拷贝UV平面
            for (size_t i = 0; i < height / 2; i++) {
                memcpy(bufData + width * height + i * width,
                       uvPlane + i * uvStride,
                       width);
            }
        }
        
    } else {
        // 打包格式(如BGRA)
        void *baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer);
        size_t dataSize = CVPixelBufferGetDataSize(pixelBuffer);
        // 处理baseAddress
        bufData = (uint8_t *)malloc(dataSize);
        memcpy(bufData, baseAddress, dataSize);
    }
    CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);

    return bufData;
}
5 退出销毁

反注册事件监听。

/* textureID */
unregisterLocalVideoTexture

纹理调用,必须在onTextureDestory()中释放销毁。

- (void)onTextureDestory
{
    // 纹理调用时,必须在此处进行销毁引擎,以便保证与创建engine在同一个线程上调用
    if (nil != self.beautyEngine) {
         [self.beautyEngine destroyEngine];
         self.beautyEngine = nil;
    }
}