How to integrate Banuba SDK with Android Video SDK?
The Banuba SDK is a software development kit that enables developers to integrate advanced augmented reality (AR) features, such as real-time face filters and effects, into their apps.

To integrate the Banuba SDK with Android Video SDK, follow these steps:
Step 1: Get Banuba Token
- To begin working with the Banuba SDK in your project, you'll need Banuba's client token.
- You can obtain this token by following the instructions in Banuba's documentation at banuba.com
Step 2: Add Banuba SDK dependencies
- Add custom maven repo to your
settings.gradle.kts:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/sdk-banuba/banuba-sdk-android")
credentials {
username = "sdk-banuba"
password = "<BANUBA_GITHUB_TOKEN>"
}
}
}
}
-
Replace
<BANUBA_GITHUB_TOKEN>with the GitHub token for Banuba's Maven repository. Get it from Banuba, along with your client token. -
Add Banuba SDK and VideoSDK dependencies in
app/build.gradle.ktsfile.
dependencies {
// Banuba SDK dependencies
implementation("com.banuba.sdk:banuba_sdk:1+")
// Video SDK dependencies
implementation("live.videosdk:rtc-android-sdk:3.0.0")
// other app dependencies
}
- For the rest of the VideoSDK setup, such as the permissions your app must declare, see Integrate Android SDK.
Step 3: Initialization of Banuba SDK
-
Create a
MainApplicationclass that extendsandroid.app.Application. -
Within the
MainApplicationclass, create a KEY field to store the token you generated from Banuba. This token will be used to initialize theBanubaSdkManager.
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
//...
val key = "Banuba Client Token"
BanubaSdkManager.initialize(applicationContext, key)
}
override fun onTerminate() {
BanubaSdkManager.deinitialize()
super.onTerminate()
}
}
Step 4: Add effects into your project
- Create an effects folder under the assets directory in your project and add the filters you want to use. You can download test filters from here Effects

Step 5: Integration of BanubaProcessor
- The
BanubaProcessorclass will implement theVideoFrameProcessorinterface from VideoSDK. - VideoSDK calls
onFrameCaptured()for every camera frame. Change theVideoFramethere to apply the effect. - Import
VideoFrame,JavaI420Buffer,EglBaseandSurfaceTextureHelperfrom thevideosdk.org.webrtcpackage. The frame you receive has an I420 buffer. - VideoSDK releases both the frame it passes in and the frame you return. So
BanubaProcessorreturns a newVideoFrameeach time, with its own reference to Banuba's latest output, and sends the original frame until Banuba has produced one.
import android.util.Size
import com.banuba.sdk.effect_player.CameraOrientation
import com.banuba.sdk.frame.FramePixelBufferFormat
import com.banuba.sdk.input.StreamInput
import com.banuba.sdk.output.FrameOutput
import com.banuba.sdk.player.Player
import com.banuba.sdk.types.FrameData
import com.banuba.sdk.types.FullImageData
import live.videosdk.rtc.android.listeners.VideoFrameProcessor
import videosdk.org.webrtc.EglBase
import videosdk.org.webrtc.JavaI420Buffer
import videosdk.org.webrtc.SurfaceTextureHelper
import videosdk.org.webrtc.VideoFrame
class BanubaProcessor : VideoFrameProcessor {
private var mPlayer: Player = Player()
private var mFrameOutput: FrameOutput? = null
private val eglContext: EglBase.Context = EglBase.create().eglBaseContext
private var mSurfaceTextureHelper: SurfaceTextureHelper? = null
// latest frame from Banuba: written on the handler thread, read on the camera thread
private val frameLock = Any()
private var processedFrame: VideoFrame? = null
private val streamInput: StreamInput = StreamInput()
init {
mSurfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglContext, false)
}
// set a visual effect in the Banuba Player
fun setEffect(effect: String) {
mPlayer.loadAsync("effects/$effect")
mPlayer.play()
}
// remove any applied effects
fun removeEffect() {
mPlayer.loadAsync("")
}
fun removePlayer() {
// close the player when it's no longer needed
mPlayer.close()
}
// method to initialize the frame output for video processing
private fun initOutput(): VideoFrame? {
val handler = mSurfaceTextureHelper!!.handler
mFrameOutput = FrameOutput { iOutput, framePixelBuffer ->
handler.post {
val i420Buffer = JavaI420Buffer.wrap(
framePixelBuffer.width,
framePixelBuffer.height,
framePixelBuffer.getPlane(0),
framePixelBuffer.getBytesPerRowOfPlane(0),
framePixelBuffer.getPlane(1),
framePixelBuffer.getBytesPerRowOfPlane(1),
framePixelBuffer.getPlane(2),
framePixelBuffer.getBytesPerRowOfPlane(2),
null
)
synchronized(frameLock) {
processedFrame?.release()
processedFrame = VideoFrame(i420Buffer, 0, System.nanoTime())
}
}
}
mFrameOutput!!.setFormat(FramePixelBufferFormat.I420_BT601_FULL)
handler.postDelayed({
mFrameOutput?.close()
}, 2000)
// attach the FrameOutput to the player for rendering
mPlayer.use(mFrameOutput)
// VideoSDK releases the frame you return, so give it a new frame with its own reference
synchronized(frameLock) {
val frame = processedFrame ?: return null
frame.buffer.retain()
return VideoFrame(frame.buffer, frame.rotation, frame.timestampNs)
}
}
// called whenever a new video frame is captured
override fun onFrameCaptured(videoFrame: VideoFrame): VideoFrame {
// Determine the camera orientation based on the rotation of the video frame
val cameraOrientation = CameraOrientation.values()[(videoFrame.rotation / 90) % 4]
val orientation = FullImageData.Orientation(cameraOrientation, true, 0)
val frameData = FrameData.create() ?: return videoFrame
val i420Buffer = videoFrame.buffer.toI420() ?: return videoFrame
// create FullImageData using the frame data and orientation
val fullImageData = FullImageData(
Size(i420Buffer.width, i420Buffer.height),
i420Buffer.dataY,
i420Buffer.dataU,
i420Buffer.dataV,
i420Buffer.strideY,
i420Buffer.strideU,
i420Buffer.strideV,
1, 1, 1,
orientation
)
frameData.addFullImg(fullImageData)
// releases only the reference toI420() added; VideoSDK releases videoFrame
i420Buffer.release()
streamInput.push(frameData, videoFrame.timestampNs)
// attach the StreamInput to the player for further processing
mPlayer.use(streamInput)
// return the processed frame, or the original one until Banuba has produced one
return initOutput() ?: videoFrame
}
}
Step 6: Use filters in the Room
- First, create an instance of
BanubaProcessorclass and pass it to thesetVideoProcessor()method of theRoomclass. - Next, use the
setEffectmethod on the instance ofBanubaProcessorclass to apply the desired effect. - To apply video filters as soon as the room is joined, you can call
setEffectmethod inonRoomJoinedevent.onRoomJoinedcan fire again, for example after a reconnect, so create the processor once and reuse it. - To stop sending the filtered video, call
room.removeVideoProcessor(). The processor stays applied until you remove it or leave the room.
// create the processor once and reuse it
private val banubaProcessor by lazy { BanubaProcessor() }
private val roomEventListener = object : RoomEventListener() {
override fun onRoomJoined(switchRoomId: String?) {
room.setVideoProcessor(banubaProcessor)
//here you can pass any effect that you want to apply
banubaProcessor.setEffect("PineappleGlasses")
}
}
To learn how video processors work in VideoSDK, see Video Processor.
Got a Question? Ask us on discord

