全部產品
Search
文件中心

ApsaraVideo Live:自訂視頻渲染

更新時間:Nov 20, 2025

本文將為您介紹如何?外部視頻渲染。

功能介紹

ARTC 內建了經過市場廣泛驗證的視頻渲染模組,推薦客戶優先使用,以確保獲得穩定、高效的視頻播放體驗。

對於已具備成熟自研渲染能力的客戶,或在色彩精度、幀率控制等方面有特殊需求的情境,ARTC SDK 也提供靈活的介面支援,允許接入自訂視頻渲染模組,滿足多樣化的業務需求。

前提條件

在設定視頻配置之前,請確保達成以下條件:

技術原理

遠端視頻渲染

本地視頻渲染-Buffer

本地視頻渲染-textureID

功能實現

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層對外調用傳遞,提升處理效率,避免記憶體流失。

// TODO:作資源釋放相關工作
mAliRtcEngine.unRegisterVideoSampleObserver()

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;
}
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];
    
    // 調試介面-展示Face Service特徵點
    //[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;
    }
}