Custom video rendering

Updated at:
Copy as MD

Integrate a custom video rendering module with the ARTC SDK to meet specific requirements for color accuracy and frame rate control.

Feature description

Alibaba Real-Time Communication (ARTC) includes a built-in, market-proven video rendering module that provides a stable and efficient video playback experience.

For scenarios that require a self-developed rendering module or specific color accuracy and frame rate control, the ARTC SDK provides flexible interfaces to integrate a custom video rendering module.

Prerequisites

Before you configure video settings, make sure the following prerequisites are met:

Technical principles

Remote video rendering

image

Buffer-based local video rendering

image

Texture ID-based local video rendering

image

Implementation

Android implementation

1. Register a observer

First, call the registerVideoSampleObserver method of AliRtcEngine to register a custom processing observer. Then, implement the methods of the AliRtcEngine.AliRtcVideoObserver abstract class. This lets you apply retouching throughout the AliRtcEngine processing pipeline.

Example of registering the AliRtcVideoObserver:

mAliRtcEngine.registerVideoSampleObserver(mAliRtcVideoObserver);

Implement the abstract interface as needed. ARTC supports custom processing at three stages: after local video capture, before local encoding, and after remote video decoding.

public enum AliRtcVideoObserPosition{
        /*! Captured video data. Corresponds to the onLocalVideoSample callback. */
        AliRtcPositionPostCapture(1),
        /*! Rendered video data. Corresponds to the onRemoteVideoSample callback. */
        AliRtcPositionPreRender(2),
        /*! Video data before encoding. Corresponds to the onPreEncodeVideoSample callback. */
        AliRtcPositionPreEncoder(4);
    }
public static abstract class AliRtcVideoObserver {
    // Callback for subscribed local video capture data.
    public boolean onLocalVideoSample(AliRtcVideoSourceType sourceType, AliRtcVideoSample videoSample){
        // TODO: If retouching and effects are needed during local video capture, process them here.
    }

    // Callback for subscribed remote video data.
    public boolean onRemoteVideoSample(String callId,AliRtcVideoSourceType sourceType, AliRtcVideoSample videoSample){
        // TODO: If retouching and effects are needed before displaying the frame pulled from the remote end, process them here.
    }

    // Callback for local video data before encoding.
    public boolean onPreEncodeVideoSample(AliRtcVideoSourceType sourceType, AliRtcVideoSample videoRawData){
        // TODO: If retouching and effects are needed before encoding the local video frame, process them here.
    }

    ...

    public int onGetObservedFramePosition(){
        // TODO: Specify the timing for the callback based on your business needs. Use the AliRtcVideoObserPosition values defined above.
        // For example, to perform custom processing before capture and pre-rendering, define the following value.
        // return AliRtcVideoObserPosition.AliRtcPositionPostCapture.getValue() | AliRtcVideoObserPosition.AliRtcPositionPreRender.getValue();
    }
}
Note

To use the Queen SDK, you must process the video stream in the corresponding interface.

Unregister the listener

When custom processing is no longer needed, unregister the listener to reduce calls from the SDK layer, improve processing efficiency, and prevent memory leaks.

// TODO: Perform resource release tasks.
mAliRtcEngine.unRegisterVideoSampleObserver()

2. Implement custom processing

The preceding callback interfaces can be implemented by calling the handleBeautyProcess method.

private boolean handleBeautyProcess(AliRtcEngine.AliRtcVideoSample videoSample) {
    if (!isAdvanceBeautifyOn) {     // Specifies whether to enable retouching.
        return false;
    }
    if (mQueenBeautyImp == null) {
        mQueenBeautyImp = new QueenBeautyImp(getContext(), videoSample.glContex);
    }

    return mQueenBeautyImp.onBeautyProcess(videoSample);
}
Note

QueenBeautyImp is a simple wrapper class for QueenBeautyEffector.

Create a retouching processor

Create the processor as follows:

// Add a synchronization lock to prevent multiple creations.
private synchronized void ensureQueenEngine(Context context, long glShareContext) {
        if (mQueenBeautyEffector == null) {
            try {
                QueenConfig queenConfig = new QueenConfig();
                bool isNeedCreateNewThread = glShareContext != 0; // Specifies whether to create a separate thread. We recommend that you set this to true for texture mode and false for buffer mode.
                bool isNeedCreateNewGLContext = true; // Specifies whether to create a GL context. For texture mode, keep this value consistent with isNeedCreateNewThread. For buffer mode, we recommend that you set this to true.
                queenConfig.withNewGlThread = isNeedCreateNewThread; 
                queenConfig.withContext = isNeedCreateNewGLContext;
                queenConfig.shareGlContext = glShareContext;
//                queenConfig.enableDebugLog = true;   // Debugging feature: Enable logs.
                mQueenBeautyEffector = new QueenBeautyEffector(context, queenConfig);
                // Advanced retouching debugging features.
//                mQueenBeautyEffector.getEngine().enableFacePointDebug(true);    // Enable facial landmark debugging.
//                mQueenBeautyEffector.getEngine().enableFaceDetectGPUMode(false);  // Disable the GPU mode for facial recognition.
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
Set retouching parameters
private void updateQueenEngineParams() {
    mQueenBeautyEffector.onUpdateParams(() -> {
        QueenEngine queenEngine = mQueenBeautyEffector.getEngine();
        // Skin smoothing and sharpening share one switch.
        queenEngine.enableBeautyType(BeautyFilterType.kSkinBuffing, true); // Skin smoothing switch.
        queenEngine.setBeautyParam(com.aliyun.android.libqueen.models.BeautyParams.kBPSkinBuffing, 0.85f);  // Skin smoothing. Valid values: [0,1].
        queenEngine.setBeautyParam(com.aliyun.android.libqueen.models.BeautyParams.kBPSkinSharpen, 0.2f);  // Sharpening. Valid values: [0,1].
        // Skin whitening and rosy cheeks share one switch.
        queenEngine.enableBeautyType(BeautyFilterType.kSkinWhiting, true); // Skin whitening switch.
        queenEngine.setBeautyParam(BeautyParams.kBPSkinWhitening, 0.5f);  // Skin whitening. Valid values: [0,1].

        // Eye enlarging and face slimming.
        queenEngine.enableBeautyType(BeautyFilterType.kFaceShape, true);
        queenEngine.updateFaceShape(FaceShapeType.typeBigEye,1.0f);
        queenEngine.updateFaceShape(FaceShapeType.typeCutFace,1.0f);
    });
}
Process frames

Process the data as a texture or a buffer based on the callback data type.

// Add a synchronization lock to prevent mQueenBeautyEffector from being destroyed in a multi-threaded environment.
public synchronized boolean onBeautyProcess(AliRtcEngine.AliRtcVideoSample videoSample) {
    // Update retouching parameters.
    updateQueenEngineParams();

    boolean result = false;
    if (videoSample.glContex != 0 && videoSample.textureid > 0) {
        // Texture mode.
        result = onProcessBeautyTexture(videoSample);
    } else {
        // Buffer mode.
        result = onProcessBeautyBuffer(videoSample);
    }
    return result;
}

Process texture callbacks:

private boolean onProcessBeautyTexture(AliRtcEngine.AliRtcVideoSample videoSample) {
    boolean result = false;
    boolean isOesTexture = videoSample.format == AliRtcEngine.AliRtcVideoFormat.AliRtcVideoFormatTextureOES;
    // The texture captured by an Android camera is by default a landscape image rotated by 270 degrees. The Queen SDK automatically swaps the width and height.
    // However, the width and height in the videoSample callback are already corrected by the ARTC SDK. Therefore, you must manually swap the width and height here.
    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 indicates QueenResult.QUEEN_OK.
        // Modify the texture ID.
        videoSample.textureid = newTextId;
        videoSample.format = AliRtcEngine.AliRtcVideoFormat.AliRtcVideoFormatTexture2D;

        result = true;
    }
    return result;
}

Process buffer callbacks:

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. Exit and destroy resources

When you leave a meeting or exit the video call interface, destroy the custom processing engine promptly.

// Add a synchronization lock to prevent mQueenBeautyEffector from being destroyed in a multi-threaded environment.
public synchronized void release() { 
    if (mQueenBeautyEffector != null) {
        mQueenBeautyEffector.onReleaseEngine();
        mQueenBeautyEffector = null;
    }
}

iOS implementation

Buffer processing integration

1. Register a listener

First, you can implement the AliRtcEngineDelegate delegate interface and pass it to the AliRtcEngine instance during initialization.

Next, register a video processing observer by calling: [self.engine registerVideoSampleObserver];

In addition, you can implement the following key interfaces to precisely control listener events.

// The position where video data is output. By default, all positions are returned.
- (NSInteger)onGetVideoObservedFramePosition {
    // Return one or a combination of the following values.
    // AliRtcPositionPostCapture: Local captured video data. Corresponds to the onCaptureVideoSample callback.
    // AliRtcPositionPreRender: Remote rendered video data. Corresponds to the onRemoteVideoSample callback.
    // AliRtcPositionPreEncoder: Video data before encoding. Corresponds to the onPreEncodeVideoSample callback.
    return AliRtcPositionPreRender|AliRtcPositionPostCapture;
}

// Specify the video data output format. This is optional.
- (AliRtcVideoFormat)onGetVideoFormatPreference {
    return AliRtcVideoFormat_cvPixelBuffer;
}
2. Implement custom processing
// Callback for local captured video data.
- (BOOL)onCaptureVideoSample:(AliRtcVideoSource)videoSource videoSample:(AliRtcVideoDataSample *)videoSample {
       return [self processVideoBeauty:videoSample];
}

// Callback for subscribed remote video data.
- (BOOL)onRemoteVideoSample:(NSString *)uid videoSource:(AliRtcVideoSource)videoSource videoSample:(AliRtcVideoDataSample *)videoSample {
       return [self processVideoBeauty:videoSample];
}
Create a retouching processor

The preceding callback interfaces can be implemented by calling the handleBeautyProcess method.

- (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;
        // Perform image editing on the pixelBuffer and output the processed buffer.
        kQueenResultCode resultCode = [self.beautyEngine processPixelBuffer:bufferData]; // The thread that executes this method must always be the same.
        if (resultCode == kQueenResultCodeOK && bufferData.bufferOut)
        {
            return YES;
        }
    }
    return NO;
}

The initBeautyEngine method creates the retouching engine.

Set retouching parameters
#pragma mark Retouching-related processing
- (void)initBeautyEngine:(BOOL)processBuffer
{
    if (self.beautyEngine != nil) {
        return;
    }
    // Initialize the engine configuration information object.
    QueenEngineConfigInfo *configInfo = [QueenEngineConfigInfo new];

    // Specify whether to automatically set the image rotation angle. If the device screen is locked and the image is captured from the camera by default, you can set the image rotation angle automatically.
    configInfo.autoSettingImgAngle = YES;
    configInfo.runOnCustomThread = processBuffer ? NO : YES; // Set to YES for texture calls and NO for buffer calls. NO indicates that the call does not run on the current caller thread, and a new thread is created internally.
    configInfo.withContext = processBuffer ? YES : NO;        // Set to NO for texture calls and YES for buffer calls. YES indicates that a glContext needs to be created internally.

    // Debugging interface: Enable the logging feature of the Queen SDK.
    // configInfo.enableDebugLog = YES;
    
    // Initialize the engine.
    self.beautyEngine = [[QueenEngine alloc] initWithConfigInfo:configInfo];
    
    // Basic retouching.
    // Skin smoothing.
    [_beautyEngine setQueenBeautyType:kQueenBeautyTypeSkinBuffing enable:YES mode:kQueenBeautyFilterModeSkinBuffing_Natural];
    // Set the skin smoothing coefficient.
    [self.beautyEngine setQueenBeautyParams:kQueenBeautyParamsSkinBuffing value:0.5f];
    // Set the sharpening coefficient.
    [self.beautyEngine setQueenBeautyParams:kQueenBeautyParamsSharpen value:0.5f];
    
    // Skin whitening.
    [_beautyEngine setQueenBeautyType:kQueenBeautyTypeSkinWhiting enable:YES];
    // Set the skin whitening coefficient.
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsWhitening value:0.5f];
    // Advanced retouching.
    [_beautyEngine setQueenBeautyType:kQueenBeautyTypeFaceBuffing enable:YES];
    // Wrinkle removal.
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsWrinkles value:0.5f];
    // Set the eye bag removal coefficient.
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsPouch value:0.5f];
    // Set the nasolabial fold removal coefficient.
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsNasolabialFolds value:0.5f];
    // Set the teeth whitening coefficient.
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsWhiteTeeth value:0.5f];
    
    // Enable the face shaping feature.
    [_beautyEngine setQueenBeautyType:kQueenBeautyTypeFaceShape enable:YES mode:kQueenBeautyFilterModeFaceShape_Main];
    // Face slimming.
    [_beautyEngine setFaceShape:kQueenBeautyFaceShapeTypeCutFace value:0.5f];
    // Eye enlarging.
    [_beautyEngine setFaceShape:kQueenBeautyFaceShapeTypeBigEye value:0.9f];
    
    // Debugging interface: Display facial recognition feature points.
    //[self.beautyEngine showFaceDetectPoint:YES];
    
}
Process frames

Process buffer callbacks:

- (bool)handleBeautyProcessBuffer:(CVPixelBufferRef)pixelBufferRef {
    if (!self.beautyEngine) {
        [self initBeautyEngine];
    }
    if (self.beautyEngine && pixelBufferRef)
    {
        QEPixelBufferData *bufferData = [QEPixelBufferData new];
        bufferData.bufferIn = pixelBufferRef;
        bufferData.bufferOut = pixelBufferRef;
        // Perform image editing on the pixelBuffer and output the processed buffer.
        kQueenResultCode resultCode = [self.beautyEngine processPixelBuffer:bufferData]; // The thread that executes this method must always be the same.
        if (resultCode == kQueenResultCodeOK && bufferData.bufferOut)
        {
            return YES;
        }
    }
    return NO;
}

Process texture callbacks:

- (bool)handleBeautyProcessTexture:(int)textureID withWidth:(int)width withHeight:(int)height {
    // See the pure texture processing mode section below.
}
3. Exit and destroy resources

Unregister the event listener.

/* I420/cvPixelBuffer */
unregisterVideoSampleObserver

Destroy the beautyEngine.

    // When processing a buffer, an asynchronous thread exists internally. Therefore, you can destroy the engine when you destroy the RTCEngine.
    // Note the destruction order. Make sure that RTCEngine does not call beautyEngine again. You can release it last.
- (void)dealloc {   
    ...
    if (nil != self.beautyEngine) {
         [self.beautyEngine destroyEngine];
         self.beautyEngine = nil;
    }
}

Pure texture integration

1. Register a listener

First, you can implement the Delegate interface AliRtcEngineDelegate and pass it to the AliRtcEngine instance during initialization.

Next, register a video processing observer by calling: [self.engine registerLocalVideoTexture];

[_engine registerLocalVideoTexture];	// Enable the texture callback for the capture stage.
2. Implement custom processing

You can implement the following three methods of the Delegate interface AliRtcEngineDelegate.

- (void)onTextureCreate:(void *)context {
    // DO NOTHING. You can also initialize the retouching engine here. In this example, it is initialized within 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
{
    // For texture calls, you must destroy the engine here to ensure that it is called on the same thread where the engine was created.
    if (nil != self.beautyEngine) {
         [self.beautyEngine destroyEngine];
         self.beautyEngine = nil;
    }
}
3. Create a retouching processor
- (void)initBeautyEngine:(BOOL)processBuffer
{
    if (self.beautyEngine != nil) {
        return;
    }
    // Initialize the engine configuration object.
    QueenEngineConfigInfo *configInfo = [QueenEngineConfigInfo new];

    // Specify whether to automatically set the image rotation angle. Set this to YES if the device screen is locked and the default image is captured from the camera.
    configInfo.autoSettingImgAngle = YES;
    
    configInfo.runOnCustomThread = processBuffer ? NO : YES; // Set to YES for texture calls and NO for buffer calls. If set to NO, the process does not run on the current caller thread. A new thread is created internally.
    configInfo.withContext = processBuffer ? YES : NO;        // Set to NO for texture calls and YES for buffer calls. If set to YES, an internal glContext is created.
    
    // A debugging API to enable the log feature of the retouching SDK.
    // configInfo.enableDebugLog = YES;

    // Initialize the engine.
    self.beautyEngine = [[QueenEngine alloc] initWithConfigInfo:configInfo];
    
    ...
    // The rest of the code remains the same.
}
4. Process frames
- (int)handleBeautyProcessTexture:(int)textureID withWidth:(int)width withHeight:(int)height {
    if (!self.beautyEngine) {
        [self initBeautyEngine:NO];
    }

    // Advanced retouching features require a face detection algorithm, so you must pass bufData.
    // If you enable only basic retouching, do not call the updateInputDataAndRunAlg method.
    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;
}

When processing in texture mode, the application layer must extract the buffer and pass it to the retouching SDK to call the API. This method improves overall performance by avoiding the re-extraction of buffer data from the texture.

The following code shows how to extract the buffer:

-(uint8_t *)extractI420DataFromPixelBuffer:(CVPixelBufferRef) pixelBuffer {
    uint8_t *bufData = nil;
    // Get the format information.
    OSType pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
    size_t bufWidth = CVPixelBufferGetWidth(pixelBuffer);
    size_t bufHeight = CVPixelBufferGetHeight(pixelBuffer);
        
    // Lock the 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);
            // Allocate the standard NV12 size (no padding).
            size_t bufDataSize = width * height * 3 / 2;
            bufData = (uint8_t *)malloc(bufDataSize);
            // Copy the Y plane row by row (remove stride padding).
            for (size_t i = 0; i < height; i++) {
                memcpy(bufData + i * width, yPlane + i * yStride, width);
            }
            // Copy the UV plane row by row.
            for (size_t i = 0; i < height / 2; i++) {
                memcpy(bufData + width * height + i * width,
                       uvPlane + i * uvStride,
                       width);
            }
        }
        
    } else {
        // Packed format, such as BGRA.
        void *baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer);
        size_t dataSize = CVPixelBufferGetDataSize(pixelBuffer);
        // Process the baseAddress.
        bufData = (uint8_t *)malloc(dataSize);
        memcpy(bufData, baseAddress, dataSize);
    }
    CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);

    return bufData;
}
5. Exit and destroy

Unregister the event listener.

/* textureID */
unregisterLocalVideoTexture

Invoked textures must be released and destroyed in onTextureDestroy().

- (void)onTextureDestory
{
    // When using textures, destroy the engine here. This ensures the engine is destroyed on the same thread where it was created.
    if (nil != self.beautyEngine) {
         [self.beautyEngine destroyEngine];
         self.beautyEngine = nil;
    }
}