Android Native

Lumaris ships as a normal Maven artifact. Add a repository, add a dependency, call one initialiser with your licence key — and from there build either of the two things the SDK is for. Which artifact you add is the tab; what you build with it is the section.

Minimum SDK
28 (Android 9)
Artifact
com.apima.lumaris:lumaris
Version
0.4.0
ABIs
arm64-v8a · armeabi-v7a · x86_64
Call provider
Optional — Agora today

Choose your integration

The core artifact, with no call provider linked. It gives you two things: a finished makeup screen you open in one call, and the provider-agnostic session that hands you the filtered frame and the delay-matched microphone so you can publish them yourself — into WebRTC, LiveKit, Daily, Twilio, or a recorder. Set up once, then build either or both.

Setup

The same four steps whichever of the two you build.

  1. Add the repository

    Lumaris is served from its own Maven repository. It is public and needs no credentials — no token, no username, nothing to keep out of source control.

    settings.gradle.kts
    dependencyResolutionManagement {    repositories {        google()        mavenCentral()        maven { url = uri("https://maven.apimastudio.com") }    }}
  2. Add the dependency

    The makeup engine on its own: camera, face tracking, the native landmark pipeline and the shader layers. It links no call provider at all.

    app/build.gradle.kts
    dependencies {    implementation("com.apima.lumaris:lumaris:0.4.0")}

    About 6 MB across three ABIs. If you are integrating with Agora, use the artifact on the Agora tab instead of this one — not both.

  3. Check your minimum SDK

    Face tracking needs Android 9 (API 28) or later, and the renderer needs OpenGL ES 3.0.

    app/build.gradle.kts
    android {    defaultConfig {        minSdk = 28    }}

    If your app supports older Android versions, you can keep your own minimum. On devices below Android 9 the makeup does not start, and the rest of your app runs as normal — check LumarisCallSession.isMakeupActive to hide makeup controls there.

  4. Initialise with your licence key

    Call this once, as early as you like. It returns immediately — activation is a network call on a background thread, so there is nothing to await, nothing to catch and no callback to wire up. Your licence key comes from the project page in the Lumaris dashboard. It is safe in source control: it only works together with the package name and signing certificate registered against it.

    MyApplication.kt
    import android.app.Applicationimport com.apima.lumaris.Lumarisclass MyApplication : Application() {    override fun onCreate() {        super.onCreate()        Lumaris.init(this, "lum_your_license_key")    }}

    Makeup stays off until activation succeeds, then turns on by itself. If something is wrong, the SDK says so in logcat under the tag Lumaris — with the reason code and what to do about it.

The makeup screen

The SDK's own full-screen editor: live camera, six makeup categories, AR masks, three saved looks. You open it and read back what the user built. No rendering code of your own.

  1. Ask for the camera permission

    The SDK declares CAMERA, INTERNET and ACCESS_NETWORK_STATE in its own manifest, so your manifest needs no edit — they merge in. The runtime request is still yours to make, because when to ask a user is your product's decision, not ours.

    ProfileActivity.kt
    private val requestCamera = registerForActivityResult(    ActivityResultContracts.RequestPermission(),) { granted ->    if (granted) Lumaris.openSettings(this)}private fun openMakeup() {    val granted = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ==        PackageManager.PERMISSION_GRANTED    if (granted) Lumaris.openSettings(this)    else requestCamera.launch(Manifest.permission.CAMERA)}
  2. Open the screen

    One call, from wherever your own UI puts it — a row on a profile page, a button in a call lobby. The screen is self-contained: the user sees themselves, tries looks and everything is saved as they go. There is no result to collect.

    ProfileActivity.kt
    binding.makeupRow.setOnClickListener {    Lumaris.openSettings(this)}
  3. Read the look back

    Three saved slots, one of them marked as the default. defaultLook is never null, so there is no empty state to handle — a user who has never opened the screen still gets something sensible.

    ProfileActivity.kt
    val look = Lumaris.defaultLook(this)look.namelook.maskEnabledlook.categoriesval all = Lumaris.looks(this)
  4. Optional: give first-time users something to see

    On a fresh install the default slot is a blank slate nobody finds on their own. This fills it once with a randomly chosen, fully-on look. Every call after the first is a no-op — including for a user who later turns everything back off deliberately, which must stick.

    MyApplication.kt
    Lumaris.init(this, "lum_your_license_key")Lumaris.randomizeDefaultLookIfEmpty(this)
  5. Optional: watch the licence state

    Purely informational — the SDK gates itself and needs nothing from you here. Worth wiring into a debug screen while you integrate, so a rejected key is visible without reading logcat.

    DebugActivity.kt
    lifecycleScope.launch {    Lumaris.licenseState.collect { state ->        when (state) {            is LicenseState.Active -> showBadge(state.grade)            is LicenseState.Rejected -> showError(state.reasonCode, state.message)            else -> Unit        }    }}

Into your own video pipeline

LumarisCallSession is the provider-agnostic core: camera, face tracking, makeup render, local preview and a microphone already delayed to match the video. It hands you a filtered frame and an audio chunk and has no opinion about where they go. The Agora adapter on the next tab is a thin layer over exactly this and reaches nothing you cannot — use it as the worked example.

  1. Add the call permissions

    Camera and network already merge in from the SDK's own manifest. The audio ones are yours, because a call is the only thing that needs them. RECORD_AUDIO and CAMERA both need a runtime request before you start the session.

    AndroidManifest.xml
    <uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /><uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
  2. Start the session with two callbacks

    Everything the SDK does is behind this one call. localPreview is the view group the local user sees themselves in; the two callbacks are where your provider takes over.

    CallActivity.kt
    LumarisCallSession.start(    context = this,    lifecycleOwner = this,    localPreview = binding.localVideoContainer,    onComposedFrameTexture = { textureId, width, height, eglContext, timestampNs ->        // The SDK's GL render thread, with eglContext already current.        // textureId is overwritten by the next frame: copy, then push.        val slot = myTexturePool.next(width, height)        myTexturePool.blit(from = textureId, into = slot)        myProvider.pushVideo(slot, width, height, eglContext, timestampNs)    },    onAudioChunk = { chunk, sampleRate, channels ->        // 16 kHz mono PCM16, 10 ms, already delayed to match the video.        myProvider.pushAudio(chunk, sampleRate, channels)    },)

    The video callback runs on the SDK's GL render thread with its EGL context current, and the texture it gives you is overwritten by the next frame. Copy it into your own texture before returning — the Agora adapter keeps a small round-robin pool and blits into it, and skipping that step is torn frames at the far end, not a crash.

  3. Un-mirror what leaves the device

    The local preview is mirrored, because a self-view that is not reads as wrong. What you publish must not be: the person on the other end should see you the way anyone across a table would. Check isMirrored rather than assuming — it changes with the camera.

    CallActivity.kt
    val outgoing = Matrix()if (LumarisCallSession.isMirrored) {    outgoing.postScale(-1f, 1f, 0.5f, 0.5f)}
  4. Wire the lifecycle

    pause suspends the GL render thread and the microphone; the camera pauses on its own through the lifecycleOwner you passed to start. Backgrounding is a pause rather than something the SDK fights to survive — keeping capture alive in the background would mean a permanent camera-in-use notification you never asked for.

    CallActivity.kt
    override fun onResume() {    super.onResume()    LumarisCallSession.resume()}override fun onPause() {    super.onPause()    LumarisCallSession.pause()}override fun onDestroy() {    super.onDestroy()    LumarisCallSession.stop()}
  5. Control it while it runs

    Apply a saved look, turn makeup off and on, or flip between the front and rear camera — all live, with no restart. Apply a look explicitly after start: without it the session runs with nothing applied and only whatever the catalogue enables by default shows, which is how a user's saved AR mask appears to go missing.

    CallActivity.kt
    LumarisCallSession.applyLook(Lumaris.defaultLook(this))LumarisCallSession.setMakeupEnabled(false)LumarisCallSession.switchCamera()

LumarisCallSession is public API and stays that way — it is the same surface the Agora adapter on the next tab is built on, so that adapter doubles as a complete worked example of everything in this last section. Targeting a provider we have not covered yet? Get in touch and we will point you at the right pieces.