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.
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'endInstall
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.xcworkspaceAsk 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>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.
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) }}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)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.
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.
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.
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.
The makeup-filtered frame is published into a live call, so the person on the other end sees the look rather than the raw camera. Lumaris publishes a custom video track and holds the microphone back to match it, which is what keeps lip sync intact while the picture waits for its own face tracking.
Add the pod, with the Agora subspec
The subspec is the only thing that declares Agora's SDK. A customer who writes pod 'Lumaris' never resolves it, never links it, and never ships it.
Podfile platform :ios, '15.0'target 'YourApp' do use_frameworks! # The Agora subspec. It is the only thing that declares Agora's own SDK, # so 'Lumaris' on its own never resolves or links it. pod 'Lumaris/Agora', :podspec => 'https://maven.apimastudio.com/ios/Lumaris-0.4.0.podspec'endInstall
CocoaPods resolves Agora and MediaPipe alongside the framework.
Terminal pod installopen YourApp.xcworkspaceAsk for the camera and the microphone
Both, this time: a call publishes audio as well as video.
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>Start the licence
Exactly as in the standalone flow. An unlicensed call still connects, with the camera unfiltered.
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}Join with custom tracks
Two of them, and the pair matters. The video track carries the composed picture; the audio track carries a microphone Lumaris delays to match it. Turn Agora's own camera and microphone publishing off, or the far end receives both copies.
CallViewController.swift import AgoraRtcKitimport LumarisSDKimport LumarisAgoralet engine = AgoraRtcEngineKit.sharedEngine(withAppId: appId, delegate: self)engine.enableVideo()// Two custom tracks: the composed video, and the microphone Lumaris holds// back to match it.let videoTrack = LumarisAgoraVideo.shared.createVideoTrack(engine)let audioTrack = LumarisAgoraVideo.shared.createAudioTrack(engine)let options = AgoraRtcChannelMediaOptions()options.clientRoleType = .broadcasteroptions.publishCameraTrack = false // Lumaris publishes insteadoptions.publishCustomVideoTrack = trueoptions.customVideoTrackId = videoTrackoptions.publishMicrophoneTrack = falseoptions.publishCustomAudioTrack = trueoptions.publishCustomAudioTrackId = audioTrackengine.joinChannel(byToken: token, channelId: channel, uid: 0, mediaOptions: options)Start the session
One call brings up the camera, the tracking, the render and the publishing, and draws the local preview into the view you pass.
CallViewController.swift let lumarisView = LumarisView()lumarisView.frame = view.boundsview.addSubview(lumarisView)// Camera, tracking, render and publishing, in one call.try LumarisAgoraVideo.shared.start(engine: engine, in: lumarisView)// The look the user saved, or any other.LumarisAgoraVideo.shared.apply(Lumaris.defaultLook())Expect a brief silence as the call connects. The SDK measures this device's own video delay in the first moments and holds the microphone back by exactly that much; a number guessed in advance would be wrong on most phones.
Control it during the call
Makeup on and off, front and back camera, and the lifecycle calls your view controller already has to make.
CallViewController.swift LumarisAgoraVideo.shared.setMakeupEnabled(false) // off, camera keeps runningLumarisAgoraVideo.shared.switchCamera()LumarisAgoraVideo.shared.pause() // app backgroundedLumarisAgoraVideo.shared.resume()LumarisAgoraVideo.shared.stop() // call ended
A provider that is not Agora gets its own adapter built the same way — the session underneath knows nothing about Agora, and neither does the core framework.