DEV Community

Team Banuba
Team Banuba

Posted on

Face AR SDK Integration on Android and iOS: 6 Steps From Token to First Effect

Adding face AR to an existing mobile app is a six-step integration that touches two code files per platform, plus your build files. Banuba Face AR SDK is a real-time, on-device face tracking and AR effects SDK. Per Banuba's own technical specification it tracks a single face at 30 FPS on the Android High, iOS Mid and iOS High device tiers and at 25 FPS on Android Low, within a head-angle range of 80 degrees. The full table is in step 6.

The six steps are: get a client token, add the SDK packages, initialize once at app start, connect the camera to a player and an output view, load an effect, and handle the lifecycle. This guide is for a mobile developer who already ships an Android or iOS app and wants a face filter running in the camera view. The code below is taken from the official Banuba sample repositories for Android and iOS, so every call is one you can open and run. We build the SDK, and every figure named below comes from our own documentation; this guide contains no benchmark of our own.

What do you need before you add a face AR SDK to an app?

Four things, and all of them are checkable before you write a line.

  • A device and OS floor. Banuba Face AR SDK supports Android from 8.0 (API level 26) and iOS from 13.0, and needs OpenGL ES 3.0 or higher on any mobile platform, per the system requirements.
  • A camera. The iOS sample opens the front camera at the 1280x720 preset.
  • A language. The SDK exposes Kotlin and Java on Android, Swift and Objective-C on iOS.
  • A client token, which is step 1. One decision belongs here rather than later. If your app also targets Flutter or React Native, the same SDK ships as Banuba-maintained plugins, and our earlier walkthrough on adding AR features to a React Native app covers that path. This guide stays native.

Step 1: Get a client token and decide where it lives

Banuba Face AR SDK does nothing without a token. There are two kinds, per the token management page: a demo token for the trial, valid for 14 days and activating all SDK features, and a commercial token issued after payment, valid for the prepaid period and activating the features in your license. You request the demo token through the form on the Face AR SDK page.

The decision at this step is storage. Banuba recommends storing the token on your server, because it makes renewal faster. If the token is compiled into the app, every renewal means a new build submitted to the App Store or Google Play.

Where this step fails: an expired token. Banuba documents three states. While the token is valid, the SDK works as expected. For one month after expiry, the SDK works but shows a Banuba watermark. After that month, the functionality stops while the rest of your app keeps running. A demo token in a live app hits the first of those states 14 days after you got it, which is why the docs say not to ship one.

Step 2: Add the SDK packages, and only the ones you need

Banuba Face AR SDK is split into packages, and your app pulls the ones its features need. Three rules from the installation page decide the step.

First, the API package is always required. On Android that is com.banuba.sdk:sdk_api, on iOS it is BNBSdkApi. Second, face tracking lives in its own package, face_tracker on Android and BNBFaceTracker on iOS, and the docs say to include it whenever you deal with tracking. Third, every package must be the same version. Banuba warns that packages of different versions, even minor ones, may conflict or work incorrectly together.

On Android, add the Banuba Maven repository in settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven {
            name = "BanubaMaven"
            url = uri("https://nexus.banuba.net/repository/maven-releases")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Then declare the packages. Note the underscores in the artifact names and the exact version string:

val bnbVersion = "1.18.5"

dependencies {
    // Always required
    api("com.banuba.sdk:sdk_api:$bnbVersion")
    // Face tracking is used in most effects
    api("com.banuba.sdk:face_tracker:$bnbVersion")

    // Add only what your effects actually use:
    // api("com.banuba.sdk:background:$bnbVersion")   // background replacement
    // api("com.banuba.sdk:makeup:$bnbVersion")       // makeup
    // api("com.banuba.sdk:eyes:$bnbVersion")         // eye effects
    // api("com.banuba.sdk:lips:$bnbVersion")         // lipstick
    // api("com.banuba.sdk:hair:$bnbVersion")         // hair color
    // api("com.banuba.sdk:skin:$bnbVersion")         // skin retouch
}
Enter fullscreen mode Exit fullscreen mode

Pin the version rather than writing 1.18.+. A dynamic version resolves at build time, so two machines, or the same machine a week apart, can end up on different patch releases. That is the exact situation the docs warn about when they say every package must be on the same version.

For a face filter alone, sdk_api plus face_tracker is the whole list. The banuba_sdk package depends on everything, and the docs describe it as the choice when you do not care about size.

On iOS, add the Swift Package Manager packages through File > Add Package Dependencies, starting with https://github.com/sdk-banuba/BNBSdkApi, and add the feature packages from the sdk-banuba repositories at the same version. CocoaPods is also supported through a custom repository listed in the podspecs repo.

Where this step fails: a feature that renders wrong or not at all. The installation page says to read the application logs in that case to find which package is missing. The second failure is a version mismatch between two packages, which the same page says can break features without an obvious error.

Step 3: Initialize the SDK once at app start

Initialization is a single call with the token, placed in the application class so it runs once before any camera screen opens.

Android, from the sample's Application.kt:

import com.banuba.sdk.manager.BanubaSdkManager

const val BANUBA_CLIENT_TOKEN = <#Place your token here#>

class Application : android.app.Application() {
    override fun onCreate() {
        super.onCreate()
        BanubaSdkManager.initialize(this, BANUBA_CLIENT_TOKEN)
    }
}
Enter fullscreen mode Exit fullscreen mode

iOS, from the sample's AppDelegate.swift. The resourcePath array tells the SDK where to look for effects, and the sample points it at an effects folder inside the app bundle and at the bundle root:

import UIKit
import BNBSdkApi

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var banubaClientToken: String = <#Place your token here#>
    var window: UIWindow?

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        BanubaSdkManager.initialize(
            resourcePath: [
                Bundle.main.bundlePath + "/effects",
                Bundle.main.bundlePath
            ],
            clientTokenString: banubaClientToken
        )
        return true
    }
}
Enter fullscreen mode Exit fullscreen mode

The <#Place your token here#> placeholder is literally what the samples ship, and neither file compiles until you replace it. Put in the value from step 1, or the call that fetches it from your server.

Step 4: Connect the camera, the player and the output view

This is the step that turns the SDK on. Three objects do the work on both platforms: a camera device that produces frames, a Player that runs the pipeline, and an output that displays the result. The pattern is player.use(input, output), then play().

Android, from the sample's MainActivity.kt, trimmed to the pipeline:

import android.os.Bundle
import android.view.SurfaceView
import com.banuba.sdk.input.CameraDevice
import com.banuba.sdk.input.CameraInput
import com.banuba.sdk.output.SurfaceOutput
import com.banuba.sdk.player.Player

class MainActivity : BaseActivity(R.layout.main) {

    private val surfaceView by lazy(LazyThreadSafetyMode.NONE) {
        findViewById<SurfaceView>(R.id.surfaceView)
    }

    // The player executes the main pipeline
    private val player by lazy(LazyThreadSafetyMode.NONE) { Player() }

    // This camera device will pass frames to the CameraInput
    private val cameraDevice by lazy(LazyThreadSafetyMode.NONE) {
        CameraDevice(requireNotNull(this.applicationContext), this@MainActivity)
    }

    // The result will be displayed on the surface
    private val surfaceOutput by lazy(LazyThreadSafetyMode.NONE) {
        SurfaceOutput(surfaceView.holder)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Set where the player takes input frames from and where it displays the result
        player.use(CameraInput(cameraDevice), surfaceOutput)
    }
}
Enter fullscreen mode Exit fullscreen mode

iOS, from the sample's ViewController.swift. The output here is an EffectPlayerView placed in the storyboard:

import UIKit
import BNBSdkApi
import BNBSdkCore

class ViewController: UIViewController {
    // Output surface for the Player
    @IBOutlet weak var effectView: EffectPlayerView!

    // Input stream for the Player
    private let cameraDevice = CameraDevice(
        cameraMode: .FrontCameraSession,
        captureSessionPreset: .hd1280x720
    )

    // Player processes frames from the input and renders them into the outputs
    private var player: Player?

    // Current effect
    private var effect: BNBEffect?

    override func viewDidLoad() {
        super.viewDidLoad()
        player = Player()
        player?.use(input: Camera(cameraDevice: cameraDevice))
        player?.use(outputs: [effectView])
        player?.play()
        effect = player?.load(effect: "TrollGrandma")
        cameraDevice.start()
    }
}
Enter fullscreen mode Exit fullscreen mode

Where this step fails: a black view. On Android, nothing is drawn until cameraDevice.start() and player.play() have both run, and in the sample those calls sit in the lifecycle methods shown in step 6, not in onCreate. On iOS the sample starts the camera as the last line of viewDidLoad, after play() and after the effect load.

Step 5: Load an effect

An effect is a folder. Per the effect structure docs, only config.json is mandatory inside it, and the other files are images, shaders, videos and a config.js script that the effect refers to. The samples ship an effect named TrollGrandma in an effects folder, and both platforms load it by name.

Android loads asynchronously, and passing an empty string unloads:

companion object {
    private const val MASK_NAME = "effects/TrollGrandma"
}

// Inside onCreate, on a button:
var shouldApply = false
showMaskButton.setOnClickListener {
    shouldApply = !shouldApply
    showMaskButton.text = if (shouldApply) "Hide mask" else "Show mask"
    // The mask is loaded asynchronously and applied
    player.loadAsync(if (shouldApply) MASK_NAME else "")
}
Enter fullscreen mode Exit fullscreen mode

iOS loads synchronously with player?.load(effect: "TrollGrandma"), as shown in step 4.

Note the asymmetry in those two strings, because it is a common first bug. Android passes the path effects/TrollGrandma relative to assets. iOS passes the bare name TrollGrandma, because the effects folder is already in the resourcePath array you set in step 3. Copying the Android constant into the iOS project gives you a silent no-op.

The decision at this step is where effects come from. You can write the folder yourself starting from a blank config.json, build one in Banuba Studio, or buy one from the Banuba Asset Store. Which packages the effect needs comes back to step 2: a 3D mask needs face tracking only, while lipstick needs the lips package.

Where this step fails: the effect loads and nothing changes on screen, or part of it is missing. That is the missing-package symptom from step 2, and the logs name the package.

Step 6: Handle permissions, the lifecycle and the device you did not test on

The sample activity extends a BaseActivity that requests CAMERA and RECORD_AUDIO at onStart and finishes the activity if either is denied. Your app already has a permission flow, so the point is only that the camera device must not start before the grant.

The lifecycle calls in the Android sample are the part most ports drop. Each object is started, paused and closed in order:

override fun onStart() {
    super.onStart()
    // We start the camera and then the player starts taking frames
    cameraDevice.start()
}

override fun onResume() {
    super.onResume()
    player.play()
}

override fun onPause() {
    super.onPause()
    // Pause the player when the activity is inactive
    player.pause()
}

override fun onStop() {
    // After this, the camera stops capturing frames and transmitting them to the player
    cameraDevice.stop()
    super.onStop()
}

override fun onDestroy() {
    // After you are done using the player, free all resources by calling close()
    cameraDevice.close()
    surfaceOutput.close()
    player.close()
    super.onDestroy()
}
Enter fullscreen mode Exit fullscreen mode

The three close() calls in onDestroy are what the sample's own comment calls mandatory. A user who backgrounds the app without pressing anything goes through onPause and onStop, so the camera is released by that path too.

The last decision is the device you test on. Banuba's technical specification publishes face tracking figures per device tier, measured under fixed lab conditions, and names no handsets:

Tier, per Banuba's specification Single face, FPS Max angle, degrees Max distance, cm 5 faces, FPS
Android Low 25 80 170 22
Android High 30 80 180 27
iOS Mid 30 80 230 30
iOS High 30 80 230 30

Banuba produced these figures in its own lab, and no third party has repeated them. In text: single-face tracking holds 25 FPS on the Android Low tier and 30 FPS on Android High and both iOS tiers. The maximum tracking angle is 80 degrees on every tier. The maximum tracking distance is 170 cm on Android Low, 180 cm on Android High and 230 cm on both iOS tiers. With five faces in frame, Android Low drops to 22 FPS, Android High to 27, and both iOS tiers stay at 30. The specification lists five as the maximum number of faces tracked with acceptable quality on most devices.

So the low-tier Android handset is the one that shows the gap between your first effect and the spec sheet. Run the effect you intend to ship on one before you decide the effect is done, and if you want a device list to start from, our tested guide to AR SDKs on Android and iOS names the handsets we used.

Six steps, two code files per platform, one device to test on

The integration touches two code files per platform, plus your build files: an application class that initializes the SDK with the token, and a camera screen that connects a camera device to a Player and an output view, then loads an effect by folder name.

The costs sit at the edges. Token storage decides whether a renewal is a server change or a store submission. Package selection decides app size and whether a feature silently lacks its neural network. The lifecycle block decides whether the camera is released when the user leaves. And the Android Low tier, at 25 FPS single-face and 22 FPS with five faces in Banuba's own figures, is the device that tells you whether the effect is finished.

FAQ

Where is the Banuba Face AR SDK documentation for Android and iOS integration?

Banuba publishes the integration docs per platform under one Getting Started page, with Android, iOS, Web, Flutter, React Native and Desktop tabs, and the installation page lists every Banuba Face AR SDK package with what it provides. The same docs ship an LLM-friendly llms-full.txt file for AI-assisted setup.

Where are the Banuba Face AR SDK code samples?

Banuba keeps platform-specific sample projects on GitHub. The Android and iOS repositories linked in this guide hold the camera sample used above, and Banuba also publishes quickstart repositories for Flutter, React Native, Web, Unity and desktop.

What happens when the Banuba Face AR SDK token expires?

For one month after expiry, Banuba Face AR SDK keeps working but displays a Banuba watermark. After that month the SDK functionality stops while the rest of the app works normally. The token cannot be extended; you request a new one and update it in the app, which is why Banuba recommends storing it on a server.

Banuba SDK watermark appeared after the trial, what do I do?

A watermark after the trial means the 14-day demo token of Banuba Face AR SDK has expired. Request a commercial token, or a renewed demo token if you have not finished evaluating, and replace the value you pass to BanubaSdkManager.initialize.

Does Banuba Face AR SDK have Flutter and React Native plugins?

Yes. Banuba maintains the Banuba Face AR SDK Flutter plugin on pub.dev and the React Native plugin on npm, and both sit on the same SDK as the native Android and iOS integration shown here.

Top comments (0)