iOS Native

Lumaris ships as a binary XCFramework through CocoaPods. Point a Podfile at one podspec, call one initialiser with your licence key, and from there build either of the two things the SDK is for. No source is downloaded and no account is needed to fetch it.

Minimum iOS
15.0
Pod
Lumaris
Version
0.4.0
Module
import LumarisSDK
Slices
arm64 device · arm64 + x86_64 simulator
Call provider
Optional — Agora today

Choose your integration

The SDK owns the camera and draws the makeup into a view you place. Use this when the filtered picture is for your own app — a preview, a profile photo, a screen your users style themselves on — and nothing is being published to anyone.

  1. Add the pod

    One line, pointing at the podspec on our own host. There is no private spec repository to add, no CocoaPods account to create and no credentials to store in your CI.

    Podfile
    platform :ios, '15.0'target 'YourApp' do  use_frameworks!  pod 'Lumaris',      :podspec => 'https://maven.apimastudio.com/ios/Lumaris-0.4.0.podspec'end
  2. Install

    CocoaPods downloads the framework and resolves MediaPipe, which the SDK depends on for face tracking. Open the workspace it generates, not the project.

    Terminal
    pod installopen YourApp.xcworkspace
  3. Ask for the camera

    iOS refuses the camera without a usage string, and the refusal is silent in the sense that matters: your app launches and the preview is black. The microphone line is only needed if you are publishing audio.

    Info.plist
    <key>NSCameraUsageDescription</key><string>Used to show your camera with makeup applied.</string><!-- Only if you are publishing audio into a call. --><key>NSMicrophoneUsageDescription</key><string>Used for your voice during calls.</string>
  4. Start the licence

    One call, as early as you can make it. Nothing is awaited and nothing throws: the SDK runs unlicensed and simply draws no makeup, so a licence problem never takes your app down with it.

    AppDelegate.swift
    import LumarisSDKfunc application(    _ application: UIApplication,    didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {    Lumaris.start(licenseKey: "lum_your_key")    // Optional. Nothing is awaited: makeup stays off until activation    // succeeds and turns on by itself.    Lumaris.onLicenseStateChanged = { state in        print("Lumaris licence:", state)    }    return true}

    The key is bound to your bundle identifier. It is safe to keep in source control, because it is worthless against any other app.

  5. Show the camera

    LumarisView is an ordinary UIView — add it, size it, put it wherever you like. LumarisSession owns the camera, the face tracking and the render loop behind it.

    YourViewController.swift
    import LumarisSDKfinal class YourViewController: UIViewController {    private let lumarisView = LumarisView()    private let session = LumarisSession()    override func viewDidLoad() {        super.viewDidLoad()        lumarisView.frame = view.bounds        lumarisView.autoresizingMask = [.flexibleWidth, .flexibleHeight]        view.addSubview(lumarisView)    }    override func viewDidAppear(_ animated: Bool) {        super.viewDidAppear(animated)        // Camera, face tracking and rendering. Throws only if the camera        // is unavailable -- an unlicensed SDK starts and shows the camera        // with no makeup on it.        try? session.start(in: lumarisView)    }}
  6. Let the user choose a look

    The SDK carries its own makeup screen: categories, colours, finishes, the reshape controls and a live preview. Present it and what the user saves becomes their default look.

    YourViewController.swift
    // The SDK's own makeup screen: categories, colours, finishes, the// reshape controls and a live preview. What the user saves here becomes// the default look.Lumaris.openSettings(from: self)
  7. Read the look back

    If you would rather build your own UI, the catalogue is readable and what the user picked is one call away.

    YourViewController.swift
    // What the user chose, to apply it yourself or to show it in your// own UI.let look = Lumaris.defaultLook()let all = Lumaris.looks()// A first-run convenience: if the user has never opened the settings// screen, give them a look rather than a bare camera.Lumaris.randomizeDefaultLookIfEmpty()

Into your own video pipeline

LumarisSession is the provider-agnostic core: camera, face tracking, makeup render, local preview, and the numbers you need to keep audio in step with it. It hands you a finished frame; where that frame goes is yours — WebRTC, LiveKit, Daily, Twilio, or a recorder.

  1. Take the composed frames

    One callback, one CVPixelBuffer per frame, straight from the GPU. No readback and no copy, which is what keeps this affordable on a phone that is also tracking a face thirty times a second.

    CallViewController.swift
    let lumarisView = LumarisView()lumarisView.frame = view.boundsview.addSubview(lumarisView)let session = LumarisSession()try session.start(in: lumarisView)// Every composed frame, as a CVPixelBuffer, straight from the GPU -- no// readback and no copy. Hand it to your encoder, your WebRTC source or// your recorder.//// Already un-mirrored: the local preview is mirrored because a self-view// that is not reads as wrong, and what leaves the device is not, because// the person on the other end should see the user the way anyone across a// table would.lumarisView.renderer.onComposedFrame = { pixelBuffer in    yourVideoSource.push(pixelBuffer)}

    The frame arrives un-mirrored. The local preview is mirrored, because a self-view that is not reads as wrong; what leaves the device is not, because the person on the other end should see the user the way anyone across a table would. You do not have to undo anything.

  2. Hold the microphone back to match

    The picture waits for its own face tracking, so it leaves a few tens of milliseconds behind live. Audio has to wait with it. The SDK gives you the microphone, the delay line and the measured video delay; wiring the three together is the whole job, and it is what the Agora adapter does internally.

    CallViewController.swift
    // The picture waits for its own face tracking, so it leaves a few tens// of milliseconds behind live. Audio has to wait with it or the call is// out of sync -- and the amount is a property of this device, measured,// not a constant to copy from another phone.let microphone = MicrophoneCapture()let delayLine = AudioDelayLine(sampleRate: MicrophoneCapture.sampleRate,                               channels: MicrophoneCapture.channels)var videoDelay = VideoDelayEstimate()// Sampled per composed frame; the median is what the audio is held to.session.pipeline?.onFrameReady = {    videoDelay.record(session.displayDelayMilliseconds)    if let median = videoDelay.medianMilliseconds {        delayLine.targetDelayMilliseconds = median    }}var out = [Int16](repeating: 0, count: 0)microphone.onSamples = { samples, count in    delayLine.write(samples, count: count)    if out.count != count { out = [Int16](repeating: 0, count: count) }    out.withUnsafeMutableBufferPointer { buffer in        guard let base = buffer.baseAddress else { return }        delayLine.read(into: base, count: count)        yourAudioSource.push(base, count: count)    }}try microphone.start()

    Hold it to the measured median, not to a constant. The right number is a property of the device and the room — it was 41ms on an A14 and three times that on an older Android phone in the same test.

  3. Apply a look and control the session

    The session applies makeup category by category, which is what a saved look is made of. Camera switching, pausing for the background and stopping at the end of a call are all here too.

    CallViewController.swift
    // A saved look, applied category by category. The Agora adapter does// exactly this behind its own apply(_:).func apply(_ look: MakeupLook, to session: LumarisSession) {    session.setMaskEnabled(look.maskEnabled)    for category in MakeupCategory.allCases {        let settings = look.settings(for: category)        if settings.isEnabled {            session.apply(category, color: settings.color,                          finish: settings.finish, intensity: settings.intensity)        } else {            session.setEnabled(category, false)        }    }}session.switchCamera()session.clearMakeup()      // camera keeps runningsession.pause()            // app backgroundedsession.resume()session.stop()

That is the whole integration. Nothing here mentions a call provider, and nothing in the pod you installed links one.