Tous les produits
Search
Centre de documentation

ApsaraVideo Live:Custom video rendering

Dernière mise à jour :Aug 19, 2026

Intégrez un module de rendu vidéo personnalisé au SDK ARTC pour répondre à des exigences spécifiques en matière de précision des couleurs et de contrôle de la fréquence d'images.

Description de la fonctionnalité

Alibaba Real-Time Communication (ARTC) inclut un module de rendu vidéo intégré, éprouvé sur le marché, qui offre une expérience de lecture vidéo stable et efficace.

Pour les scénarios nécessitant un module de rendu développé en interne ou un contrôle précis de la précision des couleurs et de la fréquence d'images, le SDK ARTC fournit des interfaces flexibles permettant d'intégrer un module de rendu vidéo personnalisé.

Prérequis

Avant de configurer les paramètres vidéo, assurez-vous que les conditions suivantes sont remplies :

Principes techniques

Rendu vidéo distant

image

Rendu vidéo local basé sur un tampon

image

Rendu vidéo local basé sur un ID de texture

image

Implémentation

Implémentation Android

1. Enregistrer un observateur

Commencez par appeler la méthode registerVideoSampleObserver de AliRtcEngine pour enregistrer un observateur de traitement personnalisé. Ensuite, implémentez les méthodes de la classe abstraite AliRtcEngine.AliRtcVideoObserver. Cela vous permet d'appliquer des effets de retouche tout au long du pipeline de traitement AliRtcEngine.

Exemple d'enregistrement de l'AliRtcVideoObserver :

mAliRtcEngine.registerVideoSampleObserver(mAliRtcVideoObserver);

Implémentez l'interface abstraite selon vos besoins. ARTC prend en charge le traitement personnalisé à trois étapes : après la capture vidéo locale, avant l'encodage local et après le décodage vidéo distant.

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();
    }
}
Remarque

Pour utiliser le Queen SDK, vous devez traiter le flux vidéo dans l'interface correspondante.

Annuler l'enregistrement de l'écouteur

Lorsque le traitement personnalisé n'est plus nécessaire, annulez l'enregistrement de l'écouteur afin de réduire les appels depuis la couche SDK, d'améliorer l'efficacité du traitement et d'éviter les fuites de mémoire.

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

2. Implémenter le traitement personnalisé

Les interfaces de rappel précédentes peuvent être implémentées en appelant la méthode handleBeautyProcess.

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);
}
Remarque

QueenBeautyImp est une classe wrapper simple pour QueenBeautyEffector.

Créer un processeur de retouche

Créez le processeur comme suit :

// 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();
            }
        }
    }

Définir les paramètres de retouche

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);
    });
}

Traiter les images

Traitez les données sous forme de texture ou de tampon en fonction du type de données du rappel.

// 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;
}

Traiter les rappels de texture :

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;
}

Traiter les rappels de tampon :

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. Quitter et détruire les ressources

Lorsque vous quittez une réunion ou l'interface d'appel vidéo, détruisez rapidement le moteur de traitement personnalisé.

// 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;
    }
}

Implémentation iOS

Intégration du traitement par tampon

1. Enregistrer un écouteur

Commencez par implémenter l'interface déléguée AliRtcEngineDelegate et transmettez-la à l'instance AliRtcEngine lors de l'initialisation.

Ensuite, enregistrez un observateur de traitement vidéo en appelant : [self.engine registerVideoSampleObserver];

De plus, vous pouvez implémenter les interfaces clés suivantes pour contrôler précisément les événements de l'écouteur.

// 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. Implémenter le traitement personnalisé

// 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];
}
Créer un processeur de retouche

Les interfaces de rappel précédentes peuvent être implémentées en appelant la méthode 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;
        // 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;
}

La méthode initBeautyEngine crée le engine de retouche.

Définir les paramètres de retouche
#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];
    
}
Traiter les images

Traiter les rappels de tampon :

- (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;
}

Traiter les rappels de texture :

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

3. Quitter et détruire les ressources

Annulez l'enregistrement de l'écouteur d'événements.

/* I420/cvPixelBuffer */
unregisterVideoSampleObserver

Détruisez le 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;
    }
}

Intégration en texture pure

1. Enregistrer un écouteur

Commencez par implémenter l'interface Delegate AliRtcEngineDelegate et transmettez-la à l'instance AliRtcEngine lors de l'initialisation.

Ensuite, enregistrez un observateur de traitement vidéo en appelant : [self.engine registerLocalVideoTexture];

[_engine registerLocalVideoTexture];	// Enable the texture callback for the capture stage.

2. Implémenter le traitement personnalisé

Vous pouvez implémenter les trois méthodes suivantes de l'interface Delegate 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. Créer un processeur de retouche

- (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. Traiter les images

- (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;
}

Lors du traitement en mode texture, la couche application doit extraire le tampon et le transmettre au SDK de retouche pour appeler l'API. Cette méthode améliore les performances globales en évitant la ré-extraction des données du tampon à partir de la texture.

Le code suivant montre comment extraire le tampon :

-(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. Quitter et détruire

Annulez l'enregistrement de l'écouteur d'événements.

/* textureID */
unregisterLocalVideoTexture

Les textures invoquées doivent être libérées et détruites dans 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;
    }
}