All Products
Search
Document Center

ApsaraVideo Live:Rendering video kustom

Last Updated:Apr 11, 2026

Topik ini menjelaskan cara mengimplementasikan rendering video kustom.

Deskripsi fitur

Alibaba Real-Time Communication (ARTC) mencakup modul rendering video yang telah terbukti di pasar. Modul bawaan ini menyediakan pengalaman pemutaran video yang stabil dan efisien.

Jika Anda memiliki modul rendering buatan sendiri yang sudah matang atau kebutuhan khusus terkait akurasi warna dan kontrol laju frame, kit pengembangan perangkat lunak (SDK) ARTC menyediakan antarmuka yang fleksibel. Antarmuka ini memungkinkan Anda mengintegrasikan modul rendering video kustom untuk memenuhi kebutuhan bisnis Anda.

Prasyarat

Sebelum mengonfigurasi pengaturan video, pastikan prasyarat berikut terpenuhi:

Prinsip teknis

Rendering video remote

Rendering video lokal berbasis buffer

Rendering video lokal berbasis ID texture

Implementasi

Implementasi Android

1. Daftarkan observer

Pertama, panggil metode registerVideoSampleObserver dari AliRtcEngine untuk mendaftarkan observer pemrosesan kustom. Lalu, implementasikan metode dari kelas abstrak AliRtcEngine.AliRtcVideoObserver. Hal ini memungkinkan Anda menerapkan retouching di seluruh pipeline pemrosesan AliRtcEngine.

Contoh pendaftaran AliRtcVideoObserver:

mAliRtcEngine.registerVideoSampleObserver(mAliRtcVideoObserver);

Implementasikan antarmuka abstrak sesuai kebutuhan. ARTC mendukung pemrosesan kustom pada tiga tahap: setelah penangkapan video lokal, sebelum encoding lokal, dan setelah decoding video remote.

public enum AliRtcVideoObserPosition{
        /*! Data video yang ditangkap. Sesuai dengan callback onLocalVideoSample. */
        AliRtcPositionPostCapture(1),
        /*! Data video yang dirender. Sesuai dengan callback onRemoteVideoSample. */
        AliRtcPositionPreRender(2),
        /*! Data video sebelum encoding. Sesuai dengan callback onPreEncodeVideoSample. */
        AliRtcPositionPreEncoder(4);
    }
public static abstract class AliRtcVideoObserver {
    // Callback untuk data tangkapan video lokal yang berlangganan.
    public boolean onLocalVideoSample(AliRtcVideoSourceType sourceType, AliRtcVideoSample videoSample){
        // TODO: Jika diperlukan retouching dan efek selama penangkapan video lokal, proses di sini.
    }

    // Callback untuk data video remote yang berlangganan.
    public boolean onRemoteVideoSample(String callId,AliRtcVideoSourceType sourceType, AliRtcVideoSample videoSample){
        // TODO: Jika diperlukan retouching dan efek sebelum menampilkan frame yang ditarik dari ujung remote, proses di sini.
    }

    // Callback untuk data video lokal sebelum encoding.
    public boolean onPreEncodeVideoSample(AliRtcVideoSourceType sourceType, AliRtcVideoSample videoRawData){
        // TODO: Jika diperlukan retouching dan efek sebelum meng-encode frame video lokal, proses di sini.
    }

    ...

    public int onGetObservedFramePosition(){
        // TODO: Tentukan waktu callback berdasarkan kebutuhan bisnis Anda. Gunakan nilai AliRtcVideoObserPosition yang didefinisikan di atas.
        // Misalnya, untuk melakukan pemrosesan kustom sebelum capture dan pre-rendering, definisikan nilai berikut.
        // return AliRtcVideoObserPosition.AliRtcPositionPostCapture.getValue() | AliRtcVideoObserPosition.AliRtcPositionPreRender.getValue();
    }
}
Catatan

Untuk menggunakan Queen SDK, Anda harus memproses aliran video di antarmuka yang sesuai.

Unregister listener

Saat pemrosesan kustom tidak lagi diperlukan, unregister listener untuk mengurangi panggilan dari lapisan SDK, meningkatkan efisiensi pemrosesan, dan mencegah kebocoran memori.

// TODO: Lakukan tugas pelepasan resource.
mAliRtcEngine.unRegisterVideoSampleObserver()

2. Implementasikan pemrosesan kustom

Antarmuka callback di atas dapat diimplementasikan dengan memanggil metode handleBeautyProcess.

private boolean handleBeautyProcess(AliRtcEngine.AliRtcVideoSample videoSample) {
    if (!isAdvanceBeautifyOn) {     // Menentukan apakah retouching diaktifkan.
        return false;
    }
    if (mQueenBeautyImp == null) {
        mQueenBeautyImp = new QueenBeautyImp(getContext(), videoSample.glContex);
    }

    return mQueenBeautyImp.onBeautyProcess(videoSample);
}
Catatan

QueenBeautyImp adalah kelas wrapper sederhana untuk QueenBeautyEffector.

Buat prosesor retouching

Buat prosesor sebagai berikut:

// Tambahkan lock sinkronisasi untuk mencegah pembuatan ganda.
private synchronized void ensureQueenEngine(Context context, long glShareContext) {
        if (mQueenBeautyEffector == null) {
            try {
                QueenConfig queenConfig = new QueenConfig();
                bool isNeedCreateNewThread = glShareContext != 0; // Menentukan apakah perlu membuat thread terpisah. Kami merekomendasikan mengatur nilai ini ke true untuk mode texture dan false untuk mode buffer.
                bool isNeedCreateNewGLContext = true; // Menentukan apakah perlu membuat konteks GL. Untuk mode texture, pertahankan nilai ini konsisten dengan isNeedCreateNewThread. Untuk mode buffer, kami merekomendasikan mengatur nilai ini ke true.
                queenConfig.withNewGlThread = isNeedCreateNewThread; 
                queenConfig.withContext = isNeedCreateNewGLContext;
                queenConfig.shareGlContext = glShareContext;
//                queenConfig.enableDebugLog = true;   // Fitur debugging: Aktifkan log.
                mQueenBeautyEffector = new QueenBeautyEffector(context, queenConfig);
                // Fitur debugging retouching tingkat lanjut.
//                mQueenBeautyEffector.getEngine().enableFacePointDebug(true);    // Aktifkan debugging landmark wajah.
//                mQueenBeautyEffector.getEngine().enableFaceDetectGPUMode(false);  // Nonaktifkan mode GPU untuk pengenalan wajah.
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
Atur parameter retouching
private void updateQueenEngineParams() {
    mQueenBeautyEffector.onUpdateParams(() -> {
        QueenEngine queenEngine = mQueenBeautyEffector.getEngine();
        // Penghalusan dan ketajaman kulit berbagi satu sakelar.
        queenEngine.enableBeautyType(BeautyFilterType.kSkinBuffing, true); // Sakelar penghalusan kulit.
        queenEngine.setBeautyParam(com.aliyun.android.libqueen.models.BeautyParams.kBPSkinBuffing, 0.85f);  // Penghalusan kulit. Nilai valid: [0,1].
        queenEngine.setBeautyParam(com.aliyun.android.libqueen.models.BeautyParams.kBPSkinSharpen, 0.2f);  // Ketajaman. Nilai valid: [0,1].
        // Pemutihan kulit dan pipi kemerahan berbagi satu sakelar.
        queenEngine.enableBeautyType(BeautyFilterType.kSkinWhiting, true); // Sakelar pemutihan kulit.
        queenEngine.setBeautyParam(BeautyParams.kBPSkinWhitening, 0.5f);  // Pemutihan kulit. Nilai valid: [0,1].

        // Pembesaran mata dan pelangsingan wajah.
        queenEngine.enableBeautyType(BeautyFilterType.kFaceShape, true);
        queenEngine.updateFaceShape(FaceShapeType.typeBigEye,1.0f);
        queenEngine.updateFaceShape(FaceShapeType.typeCutFace,1.0f);
    });
}
Proses frame

Proses data sebagai texture atau buffer berdasarkan jenis data callback.

// Tambahkan lock sinkronisasi untuk mencegah mQueenBeautyEffector dihancurkan di lingkungan multi-threaded.
public synchronized boolean onBeautyProcess(AliRtcEngine.AliRtcVideoSample videoSample) {
    // Perbarui parameter retouching.
    updateQueenEngineParams();

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

Memproses callback tekstur:

private boolean onProcessBeautyTexture(AliRtcEngine.AliRtcVideoSample videoSample) {
    boolean result = false;
    boolean isOesTexture = videoSample.format == AliRtcEngine.AliRtcVideoFormat.AliRtcVideoFormatTextureOES;
    // Texture yang ditangkap oleh kamera Android secara default merupakan gambar landscape yang diputar 270 derajat. Queen SDK secara otomatis menukar lebar dan tinggi.
    // Namun, lebar dan tinggi dalam callback videoSample telah dikoreksi oleh SDK ARTC. Oleh karena itu, Anda harus secara manual menukar lebar dan tinggi di sini.
    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 menunjukkan QueenResult.QUEEN_OK.
        // Ubah ID texture.
        videoSample.textureid = newTextId;
        videoSample.format = AliRtcEngine.AliRtcVideoFormat.AliRtcVideoFormatTexture2D;

        result = true;
    }
    return result;
}

Proses callback 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. Keluar dan hancurkan resource

Saat Anda meninggalkan rapat atau keluar dari antarmuka panggilan video, segera hancurkan engine pemrosesan kustom.

// Tambahkan lock sinkronisasi untuk mencegah mQueenBeautyEffector dihancurkan di lingkungan multi-threaded.
public synchronized void release() { 
    if (mQueenBeautyEffector != null) {
        mQueenBeautyEffector.onReleaseEngine();
        mQueenBeautyEffector = null;
    }
}

Implementasi iOS

Integrasi pemrosesan buffer

1. Daftarkan listener

Pertama, Anda dapat mengimplementasikan antarmuka delegate AliRtcEngineDelegate dan meneruskannya ke instans AliRtcEngine selama inisialisasi.

Selanjutnya, daftarkan observer pemrosesan video dengan memanggil: [self.engine registerVideoSampleObserver];

Di samping itu, Anda dapat mengimplementasikan antarmuka kunci berikut untuk mengontrol event listener secara tepat.

// Posisi tempat data video dikeluarkan. Secara default, semua posisi dikembalikan.
- (NSInteger)onGetVideoObservedFramePosition {
    // Kembalikan satu atau kombinasi nilai berikut.
    // AliRtcPositionPostCapture: Data video lokal yang ditangkap. Sesuai dengan callback onCaptureVideoSample.
    // AliRtcPositionPreRender: Data video remote yang dirender. Sesuai dengan callback onRemoteVideoSample.
    // AliRtcPositionPreEncoder: Data video sebelum encoding. Sesuai dengan callback onPreEncodeVideoSample.
    return AliRtcPositionPreRender|AliRtcPositionPostCapture;
}

// Tentukan format output data video. Ini opsional.
- (AliRtcVideoFormat)onGetVideoFormatPreference {
    return AliRtcVideoFormat_cvPixelBuffer;
}
2. Implementasikan pemrosesan kustom
// Callback untuk data video lokal yang ditangkap.
- (BOOL)onCaptureVideoSample:(AliRtcVideoSource)videoSource videoSample:(AliRtcVideoDataSample *)videoSample {
       return [self processVideoBeauty:videoSample];
}

// Callback untuk data video remote yang berlangganan.
- (BOOL)onRemoteVideoSample:(NSString *)uid videoSource:(AliRtcVideoSource)videoSource videoSample:(AliRtcVideoDataSample *)videoSample {
       return [self processVideoBeauty:videoSample];
}
Buat prosesor retouching

Antarmuka callback di atas dapat diimplementasikan dengan memanggil metode 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;
        // Lakukan pengeditan gambar pada pixelBuffer dan keluarkan buffer yang telah diproses.
        kQueenResultCode resultCode = [self.beautyEngine processPixelBuffer:bufferData]; // Thread yang mengeksekusi metode ini harus selalu sama.
        if (resultCode == kQueenResultCodeOK && bufferData.bufferOut)
        {
            return YES;
        }
    }
    return NO;
}

Metode initBeautyEngine membuat engine retouching.

Atur parameter retouching
#pragma mark Pemrosesan terkait retouching
- (void)initBeautyEngine:(BOOL)processBuffer
{
    if (self.beautyEngine != nil) {
        return;
    }
    // Inisialisasi objek konfigurasi engine.
    QueenEngineConfigInfo *configInfo = [QueenEngineConfigInfo new];

    // Tentukan apakah akan mengatur sudut rotasi gambar secara otomatis. Jika layar perangkat terkunci dan gambar ditangkap dari kamera secara default, Anda dapat mengatur sudut rotasi gambar secara otomatis.
    configInfo.autoSettingImgAngle = YES;
    configInfo.runOnCustomThread = processBuffer ? NO : YES; // Atur ke YES untuk panggilan texture dan NO untuk panggilan buffer. NO menunjukkan bahwa panggilan tidak dijalankan pada thread pemanggil saat ini, dan thread baru dibuat secara internal.
    configInfo.withContext = processBuffer ? YES : NO;        // Atur ke NO untuk panggilan texture dan YES untuk panggilan buffer. YES menunjukkan bahwa glContext perlu dibuat secara internal.

    // Antarmuka debugging: Aktifkan fitur logging Queen SDK.
    // configInfo.enableDebugLog = YES;
    
    // Inisialisasi engine.
    self.beautyEngine = [[QueenEngine alloc] initWithConfigInfo:configInfo];
    
    // Retouching dasar.
    // Penghalusan kulit.
    [_beautyEngine setQueenBeautyType:kQueenBeautyTypeSkinBuffing enable:YES mode:kQueenBeautyFilterModeSkinBuffing_Natural];
    // Atur koefisien penghalusan kulit.
    [self.beautyEngine setQueenBeautyParams:kQueenBeautyParamsSkinBuffing value:0.5f];
    // Atur koefisien ketajaman.
    [self.beautyEngine setQueenBeautyParams:kQueenBeautyParamsSharpen value:0.5f];
    
    // Pemutihan kulit.
    [_beautyEngine setQueenBeautyType:kQueenBeautyTypeSkinWhiting enable:YES];
    // Atur koefisien pemutihan kulit.
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsWhitening value:0.5f];
    // Retouching tingkat lanjut.
    [_beautyEngine setQueenBeautyType:kQueenBeautyTypeFaceBuffing enable:YES];
    // Penghilangan kerutan.
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsWrinkles value:0.5f];
    // Atur koefisien penghilangan kantung mata.
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsPouch value:0.5f];
    // Atur koefisien penghilangan lipatan nasolabial.
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsNasolabialFolds value:0.5f];
    // Atur koefisien pemutihan gigi.
    [_beautyEngine setQueenBeautyParams:kQueenBeautyParamsWhiteTeeth value:0.5f];
    
    // Aktifkan fitur pembentukan wajah.
    [_beautyEngine setQueenBeautyType:kQueenBeautyTypeFaceShape enable:YES mode:kQueenBeautyFilterModeFaceShape_Main];
    // Pelangsingan wajah.
    [_beautyEngine setFaceShape:kQueenBeautyFaceShapeTypeCutFace value:0.5f];
    // Pembesaran mata.
    [_beautyEngine setFaceShape:kQueenBeautyFaceShapeTypeBigEye value:0.9f];
    
    // Antarmuka debugging: Tampilkan titik fitur pengenalan wajah.
    //[self.beautyEngine showFaceDetectPoint:YES];
    
}
Proses frame

Proses callback buffer:

- (bool)handleBeautyProcessBuffer:(CVPixelBufferRef)pixelBufferRef {
    if (!self.beautyEngine) {
        [self initBeautyEngine];
    }
    if (self.beautyEngine && pixelBufferRef)
    {
        QEPixelBufferData *bufferData = [QEPixelBufferData new];
        bufferData.bufferIn = pixelBufferRef;
        bufferData.bufferOut = pixelBufferRef;
        // Lakukan pengeditan gambar pada pixelBuffer dan keluarkan buffer yang telah diproses.
        kQueenResultCode resultCode = [self.beautyEngine processPixelBuffer:bufferData]; // Thread yang mengeksekusi metode ini harus selalu sama.
        if (resultCode == kQueenResultCodeOK && bufferData.bufferOut)
        {
            return YES;
        }
    }
    return NO;
}

Proses callback tekstur:

- (bool)handleBeautyProcessTexture:(int)textureID withWidth:(int)width withHeight:(int)height {
    // Lihat bagian mode pemrosesan texture murni di bawah.
}
3. Keluar dan hancurkan resource

Unregister listener event.

/* I420/cvPixelBuffer */
unregisterVideoSampleObserver

Hancurkan beautyEngine.

    // Saat memproses buffer, terdapat thread asinkron secara internal. Oleh karena itu, Anda dapat menghancurkan engine saat menghancurkan RTCEngine.
    // Perhatikan urutan penghancuran. Pastikan RTCEngine tidak memanggil beautyEngine lagi. Anda dapat melepaskannya terakhir.
- (void)dealloc {   
    ...
    if (nil != self.beautyEngine) {
         [self.beautyEngine destroyEngine];
         self.beautyEngine = nil;
    }
}

Integrasi texture murni

1. Daftarkan listener

Pertama, Anda dapat mengimplementasikan antarmuka Delegate AliRtcEngineDelegate dan meneruskannya ke instans AliRtcEngine selama inisialisasi.

Selanjutnya, daftarkan observer pemrosesan video dengan memanggil: [self.engine registerLocalVideoTexture];

[_engine registerLocalVideoTexture];	// Aktifkan callback texture untuk tahap capture.
2. Implementasikan pemrosesan kustom

Anda dapat mengimplementasikan tiga metode berikut dari antarmuka Delegate AliRtcEngineDelegate.

- (void)onTextureCreate:(void *)context {
    // JANGAN LAKUKAN APA-APA. Anda juga dapat menginisialisasi engine retouching di sini. Dalam contoh ini, inisialisasi dilakukan dalam 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
{
    // Untuk panggilan texture, Anda harus menghancurkan engine di sini untuk memastikan dipanggil pada thread yang sama tempat engine dibuat.
    if (nil != self.beautyEngine) {
         [self.beautyEngine destroyEngine];
         self.beautyEngine = nil;
    }
}
3. Buat prosesor retouching
- (void)initBeautyEngine:(BOOL)processBuffer
{
    if (self.beautyEngine != nil) {
        return;
    }
    // Inisialisasi objek konfigurasi engine.
    QueenEngineConfigInfo *configInfo = [QueenEngineConfigInfo new];

    // Tentukan apakah akan mengatur sudut rotasi gambar secara otomatis. Atur ke YES jika layar perangkat terkunci dan gambar default ditangkap dari kamera.
    configInfo.autoSettingImgAngle = YES;
    
    configInfo.runOnCustomThread = processBuffer ? NO : YES; // Atur ke YES untuk panggilan texture dan NO untuk panggilan buffer. Jika diatur ke NO, proses tidak dijalankan pada thread pemanggil saat ini. Thread baru dibuat secara internal.
    configInfo.withContext = processBuffer ? YES : NO;        // Atur ke NO untuk panggilan texture dan YES untuk panggilan buffer. Jika diatur ke YES, glContext internal dibuat.
    
    // API debugging untuk mengaktifkan fitur log SDK retouching.
    // configInfo.enableDebugLog = YES;

    // Inisialisasi engine.
    self.beautyEngine = [[QueenEngine alloc] initWithConfigInfo:configInfo];
    
    ...
    // Sisa kode tetap sama.
}
4. Proses frame
- (int)handleBeautyProcessTexture:(int)textureID withWidth:(int)width withHeight:(int)height {
    if (!self.beautyEngine) {
        [self initBeautyEngine:NO];
    }

    // Fitur retouching tingkat lanjut memerlukan algoritma deteksi wajah, sehingga Anda harus meneruskan bufData.
    // Jika Anda hanya mengaktifkan retouching dasar, jangan panggil metode 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;
}

Saat memproses dalam mode texture, lapisan aplikasi harus mengekstraksi buffer dan meneruskannya ke SDK retouching untuk memanggil API. Metode ini meningkatkan kinerja keseluruhan dengan menghindari ekstraksi ulang data buffer dari texture.

Kode berikut menunjukkan cara mengekstraksi buffer:

-(uint8_t *)extractI420DataFromPixelBuffer:(CVPixelBufferRef) pixelBuffer {
    uint8_t *bufData = nil;
    // Dapatkan informasi format.
    OSType pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
    size_t bufWidth = CVPixelBufferGetWidth(pixelBuffer);
    size_t bufHeight = CVPixelBufferGetHeight(pixelBuffer);
        
    // Kunci 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);
            // Alokasikan ukuran NV12 standar (tanpa padding).
            size_t bufDataSize = width * height * 3 / 2;
            bufData = (uint8_t *)malloc(bufDataSize);
            // Salin bidang Y baris demi baris (hapus padding stride).
            for (size_t i = 0; i < height; i++) {
                memcpy(bufData + i * width, yPlane + i * yStride, width);
            }
            // Salin bidang UV baris demi baris.
            for (size_t i = 0; i < height / 2; i++) {
                memcpy(bufData + width * height + i * width,
                       uvPlane + i * uvStride,
                       width);
            }
        }
        
    } else {
        // Format packed, seperti BGRA.
        void *baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer);
        size_t dataSize = CVPixelBufferGetDataSize(pixelBuffer);
        // Proses baseAddress.
        bufData = (uint8_t *)malloc(dataSize);
        memcpy(bufData, baseAddress, dataSize);
    }
    CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);

    return bufData;
}
5. Keluar dan hancurkan

Unregister listener event.

/* textureID */
unregisterLocalVideoTexture

Texture yang dipanggil harus dilepaskan dan dihancurkan dalam onTextureDestroy().

- (void)onTextureDestory
{
    // Saat menggunakan texture, hancurkan engine di sini. Ini memastikan engine dihancurkan pada thread yang sama tempat dibuat.
    if (nil != self.beautyEngine) {
         [self.beautyEngine destroyEngine];
         self.beautyEngine = nil;
    }
}