> ## Documentation Index
> Fetch the complete documentation index at: https://docs.facebetter.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Third-party Integration

> Wire Facebetter beauty into TRTC, Agora, LiveKit, and other Android video pipelines

SDK **2.0.0**. Hook Facebetter into a third-party SDK’s **custom video preprocess** callback: on that SDK’s **OpenGL ES thread**, run `processImage` on an external texture, then return the output texture.

Full texture rules: [Implement Beauty · External texture](/android/implement-beauty#external-texture-opengl-es). Room join, publish, and permissions belong in the vendor’s docs — this page only covers the Facebetter hookup.

```
Third-party SDK (GL thread)
  input textureId
       ↓
BeautyEffectEngine (externalContext = true)
  ImageFrame.createWithTexture → processImage
       ↓
  output textureId → write back to vendor frame / callback
```

## Shared contract

| Item       | Requirement                                                                                   |
| ---------- | --------------------------------------------------------------------------------------------- |
| Engine     | `externalContext = true`, lazy-create on the **vendor GL callback thread**                    |
| Input      | `GL_TEXTURE_2D` (RGBA); `stride` is usually `width * 4`                                       |
| Frame type | `ImageFrame.FrameType.VIDEO`                                                                  |
| Output     | SDK owns the output texture — do not `glDeleteTextures`. Always `ImageFrame.release()`        |
| Lifecycle  | When the vendor destroys the GL context, `engine.release()`, then recreate on the new context |

Facebetter **does not** accept `GL_TEXTURE_EXTERNAL_OES` for `createWithTexture`. If the vendor defaults to OES, switch to **Texture2D / RGB** per their docs, or blit to `GL_TEXTURE_2D` first.

### Shared helper

Reuse this on every vendor GL callback (must run on the GL thread):

```java theme={null}
import net.pixpark.facebetter.BeautyEffectEngine;
import net.pixpark.facebetter.ImageFrame;

/** @return beauty output GL_TEXTURE_2D; on failure returns srcTextureId (or 0, your choice) */
static int processTexture(BeautyEffectEngine engine, int srcTextureId, int width, int height) {
    if (engine == null || srcTextureId == 0 || width <= 0 || height <= 0) {
        return srcTextureId;
    }
    int stride = width * 4;
    ImageFrame input = ImageFrame.createWithTexture(srcTextureId, width, height, stride);
    if (input == null) {
        return srcTextureId;
    }
    input.type = ImageFrame.FrameType.VIDEO;
    ImageFrame output = engine.processImage(input);
    if (output == null) {
        input.release();
        return srcTextureId;
    }
    int dst = output.getTexture();
    output.release();
    input.release();
    return dst != 0 ? dst : srcTextureId;
}
```

Create the engine (also on the GL thread):

```java theme={null}
BeautyEffectEngine.EngineConfig config = new BeautyEffectEngine.EngineConfig();
config.appId = "your appId";
config.appKey = "your appKey";
// or config.licenseToken = "...";
config.externalContext = true;
BeautyEffectEngine engine = new BeautyEffectEngine(context, config);
engine.setSmoothing(0.5f);
```

Credentials: [Subscription](/intro/enable-service), [Auth & License](/intro/license).

***

## TRTC (Tencent)

Hook: `TRTCCloud.setLocalVideoProcessListener`. Use pixel format **Texture\_2D** and buffer type **TEXTURE**.

Create Facebetter in `onGLContextCreated`, process in `onProcessVideoFrame` and set `dstFrame.texture.textureId`, `release()` in `onGLContextDestory`.

```java theme={null}
import com.tencent.trtc.TRTCCloud;
import com.tencent.trtc.TRTCCloudDef;
import com.tencent.trtc.TRTCCloudListener;

trtcCloud.setLocalVideoProcessListener(
    TRTCCloudDef.TRTC_VIDEO_PIXEL_FORMAT_Texture_2D,
    TRTCCloudDef.TRTC_VIDEO_BUFFER_TYPE_TEXTURE,
    new TRTCCloudListener.TRTCVideoFrameListener() {
        @Override
        public void onGLContextCreated() {
            // TRTC has bound the GL context — lazy-create BeautyEffectEngine here
            ensureEngine();
        }

        @Override
        public int onProcessVideoFrame(
                TRTCCloudDef.TRTCVideoFrame srcFrame,
                TRTCCloudDef.TRTCVideoFrame dstFrame) {
            int out = processTexture(
                    beautyEngine,
                    srcFrame.texture.textureId,
                    srcFrame.width,
                    srcFrame.height);
            dstFrame.texture.textureId = out;
            return 0;
        }

        @Override
        public void onGLContextDestory() {
            if (beautyEngine != null) {
                beautyEngine.release();
                beautyEngine = null;
            }
        }
    });
```

Release the engine after removing the listener or leaving the room. See Tencent’s third-party beauty guide for the latest TRTC details.

For Flutter, use [Flutter · Third-party Integration](/flutter/third-party-integration) (`FacebetterPlugin.processTexture`, not this Java API).

***

## Agora

Hook: `RtcEngine.registerVideoFrameObserver` (Video SDK **4.x**). You need to:

1. Return `PROCESS_MODE_READ_WRITE` from `getVideoFrameProcessMode()`
2. Prefer a **TextureBuffer** with type **RGB (`GL_TEXTURE_2D`)**
3. Run Facebetter in `onCaptureVideoFrame` (or your chosen observe position), then **write** the processed texture back into the `VideoFrame`

```java theme={null}
import io.agora.rtc2.RtcEngine;
import io.agora.rtc2.video.IVideoFrameObserver;
import io.agora.base.VideoFrame;

engine.registerVideoFrameObserver(new IVideoFrameObserver() {
    @Override
    public boolean onCaptureVideoFrame(int sourceType, VideoFrame videoFrame) {
        VideoFrame.Buffer buffer = videoFrame.getBuffer();
        if (!(buffer instanceof VideoFrame.TextureBuffer)) {
            return true; // non-texture: pass through or convert, per your app
        }
        VideoFrame.TextureBuffer tex = (VideoFrame.TextureBuffer) buffer;
        if (tex.getType() != VideoFrame.TextureBuffer.Type.RGB) {
            return true; // OES: convert to GL_TEXTURE_2D, or adjust format preference
        }

        ensureEngine(); // must use the current GL / EGL context
        int outId = processTexture(
                beautyEngine,
                tex.getTextureId(),
                tex.getWidth(),
                tex.getHeight());

        // Wrap outId as a TextureBuffer and write it back into videoFrame.
        // Exact APIs (TextureBufferHelper / replaceBuffer, etc.) vary by Agora version —
        // follow Agora’s raw video-frame docs, then return true.
        return true;
    }

    @Override
    public int getVideoFrameProcessMode() {
        return IVideoFrameObserver.PROCESS_MODE_READ_WRITE;
    }

    @Override
    public int getVideoFormatPreference() {
        // Prefer a processable texture; use the constant from your Agora version
        return IVideoFrameObserver.VIDEO_PIXEL_DEFAULT;
    }

    // Implement other callbacks as needed; POSITION_POST_CAPTURER is common
});
```

<Warning>
  How you inject a new `textureId` back into Agora’s `VideoFrame` depends on their TextureBuffer helpers and differs across major versions. Facebetter only produces a new `GL_TEXTURE_2D`; the write-back step must follow [Agora’s video frame observer](https://docs.agora.io/en/video-calling/develop/product-workflow) / API Reference.
</Warning>

Unregister the observer and `beautyEngine.release()` before destroying `RtcEngine`.

***

## LiveKit

Hook: pass an `org.webrtc.VideoProcessor` (or equivalent track processor) when creating the local video track. Process textures in `onFrameCaptured`, then deliver via `sink.onFrame(...)`.

```kotlin theme={null}
import io.livekit.android.room.track.LocalVideoTrackOptions
import org.webrtc.VideoFrame
import org.webrtc.VideoProcessor
import org.webrtc.VideoSink

class FacebetterVideoProcessor(
    private val appContext: android.content.Context,
) : VideoProcessor {
    private var sink: VideoSink? = null
    private var beautyEngine: BeautyEffectEngine? = null

    override fun setSink(videoSink: VideoSink?) {
        sink = videoSink
    }

    override fun onCapturerStarted(success: Boolean) {}

    override fun onCapturerStopped() {
        beautyEngine?.release()
        beautyEngine = null
    }

    override fun onFrameCaptured(frame: VideoFrame) {
        val buffer = frame.buffer
        if (buffer !is VideoFrame.TextureBuffer ||
            buffer.type != VideoFrame.TextureBuffer.Type.RGB
        ) {
            sink?.onFrame(frame)
            return
        }

        ensureEngine() // LiveKit / WebRTC current EGL context
        val outId = processTexture(
            beautyEngine,
            buffer.textureId,
            buffer.width,
            buffer.height,
        )

        // Build a new TextureBuffer / VideoFrame from outId, then:
        // sink?.onFrame(processedFrame)
        // processedFrame.release()
        //
        // TextureBuffer construction needs WebRTC Handler / YuvConverter, etc. —
        // follow LiveKit Android SDK and WebRTC samples for the wrap.
        sink?.onFrame(frame)
    }
}

// Attach the processor when creating the track (API per your livekit-android version)
val videoTrack = room.localParticipant.createVideoTrack(
    options = LocalVideoTrackOptions(),
    videoProcessor = FacebetterVideoProcessor(context),
)
videoTrack.startCapture()
room.localParticipant.publishVideoTrack(videoTrack)
```

You can also implement a custom `VideoCapturer`, run `processTexture` after capture, then call `capturerObserver.onFrameCaptured`. Official processors (for example virtual background) live in LiveKit’s `livekit-android-track-processors` module.

***

## Troubleshooting

| Symptom            | Check                                                                                               |
| ------------------ | --------------------------------------------------------------------------------------------------- |
| Black / no video   | Engine created and `processImage` on the vendor **GL thread**; failed frames not dropped by mistake |
| No beauty          | `externalContext == true`; input is `GL_TEXTURE_2D`                                                 |
| Intermittent crash | `release()` on GL destroy / capturer stop; never `glDeleteTextures` on the output                   |
| Distortion         | Width / height / rotation / mirror match the vendor frame; OES not converted to 2D                  |

## Related

* [Implement Beauty](/android/implement-beauty)
* [Best Practices](/android/best-practices)
* [Error Handling](/android/error-handling)
* [API Reference](/android/api-reference)
