Overview
The Android SDK provides public APIs for:
- SDK initialization and license handling
- real-time processed preview
- front and back camera input
- built-in beauty, makeup, and face shaping
- local and cloud
.nosmaifilters - active-effect state
- background effects
- processed video recording
- processed output for live streaming
The official Android repository contains releases and a complete sample application:
github.com/nosmai/camera-sdk-android
Requirements
| Requirement | Value |
|---|---|
| Minimum Android version | API 21 |
| Supported device architecture | arm64-v8a |
| Native page size | 16 KB compatible |
| Java compatibility | Java 11 |
| Camera API | Camera2 |
| Test environment | Physical Android device |
The SDK does not run on an x86 or x86_64 emulator. Use a physical arm64 Android device for camera, face tracking, recording, and performance testing.
Install
1. Download the AAR
Download the current AAR from the Android SDK releases.
For SDK 3.0.1, download both nosmai-sdk-3.0.1.aar and SHA256SUMS, then verify the artifact:
shasum -a 256 -c SHA256SUMS
Place it in the application module:
app/
libs/
nosmai-sdk-3.0.1.aar
2. Add the dependency
android {
compileSdk = 35
defaultConfig {
minSdk = 21
ndk {
abiFilters += "arm64-v8a"
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
implementation(files("libs/nosmai-sdk-3.0.1.aar"))
}
If the downloaded release uses a different filename, use that exact filename in the Gradle dependency.
3. Prevent duplicate native libraries
If another dependency includes the same shared C++ runtime, add:
android {
packaging {
jniLibs {
pickFirsts += "lib/arm64-v8a/libc++_shared.so"
}
}
}
Only add a pickFirst rule when Gradle reports a duplicate native-library error. Do not add broad packaging exclusions.
Permissions
Add the required permissions:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:name=".NosmaiApplication"
...>
</application>
</manifest>
| Permission | Required when |
|---|---|
CAMERA | Showing a camera preview |
INTERNET | License verification and cloud filters |
RECORD_AUDIO | Recording or streaming with microphone audio |
Request camera and microphone permission at runtime. Do not request microphone permission if the application never records or streams audio.
Initialize
Initialize Nosmai once from the application class.
package com.example.cameraapp;
import android.app.Application;
import com.nosmai.effect.api.NosmaiSDK;
public final class NosmaiApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
NosmaiSDK.initialize(
this,
BuildConfig.NOSMAI_LICENSE_KEY
);
}
}
Register the class:
<application
android:name=".NosmaiApplication"
...>
</application>
Check the state when needed:
if (!NosmaiSDK.isInitialized()) {
// Do not open the camera experience yet.
}
Do not initialize from every activity, fragment, or composable. Keep initialization under one application-level owner.
Add the preview
Create a container:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/preview_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Create NosmaiPreviewView after camera permission is granted:
private NosmaiPreviewView previewView;
private void createNosmaiPreview() {
FrameLayout container = findViewById(R.id.preview_container);
previewView = new NosmaiPreviewView(this);
container.addView(
previewView,
new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
);
NosmaiSDK.startProcessing(previewView);
}
startProcessing prepares the view for camera frames and effects. It does not grant camera permission.
Connect Camera2
The native Android SDK accepts Camera2 input from the application. The official sample includes a Camera2Helper that handles camera selection, frame size, sensor orientation, camera close, and background-thread ownership.
Use the helper from the official sample or provide equivalent Camera2 code in the host application.
The required application responsibilities are:
- create one Camera2 source
- connect it to
NosmaiPreviewView - tell the SDK whether the front or back camera is active
- provide the camera orientation
- start the camera after the Nosmai preview is ready
- stop the camera before removing the screen
Recommended direct camera connection
The sample application uses the optimized direct camera connection when the device supports it. The public Nosmai setup is:
previewView.enableOesInput(true);
previewView.addOnOesReadyListener(surfaceTexture -> {
cameraHelper.setInputMode(Camera2Helper.InputMode.OES);
cameraHelper.setOesPreviewSurfaceTexture(surfaceTexture);
cameraHelper.setPreviewSizeCallback((width, height) -> {
int rotation = calculateFrameRotation();
previewView.setOesInputFrameInfo(width, height, rotation);
});
previewView.setCameraOrientation(
cameraHelper.isFrontCamera(),
cameraHelper.getSensorOrientation()
);
NosmaiSDK.setCameraFacing(cameraHelper.isFrontCamera());
cameraHelper.startCamera();
});
Camera2Helper and calculateFrameRotation in this example come from the official sample application, not from the public SDK AAR.
Keep the sample's device fallback behavior. If direct input cannot start on a device, reconnect using the sample's normal Camera2 frame input instead of leaving the preview black.
Front-camera mirroring
Tell the SDK which camera is active:
NosmaiSDK.setCameraFacing(isFrontCamera);
Set the preview mirror preference:
NosmaiSDK.setMirrorX(isFrontCamera);
Back-camera preview should normally not be mirrored.
Do not mirror the Camera2 frame and the Nosmai preview at the same time. Applying the same horizontal flip twice can make the preview look unchanged while face effects use the wrong direction.
Switch camera
Stop or reconfigure the current Camera2 session before opening the other camera.
private boolean isSwitchingCamera;
private void switchCamera() {
if (isSwitchingCamera) {
return;
}
isSwitchingCamera = true;
cameraHelper.stopCamera();
isFrontCamera = !isFrontCamera;
setupCameraHelper(isFrontCamera);
NosmaiSDK.setCameraFacing(isFrontCamera);
NosmaiSDK.setMirrorX(isFrontCamera);
cameraHelper.startCamera();
isSwitchingCamera = false;
}
Prevent rapid repeated taps while a switch is already running.
Apply built-in beauty
Use NosmaiBeauty for built-in controls.
import com.nosmai.effect.api.NosmaiBeauty; NosmaiBeauty.applySkinSmoothing(0.4f); NosmaiBeauty.applySkinWhitening(0.2f); NosmaiBeauty.applySharpen(0.15f); NosmaiBeauty.applyTeethWhitening(0.3f);
Skin smoothing, whitening, sharpening, and teeth whitening use values from 0.0 to 1.0.
Remove built-in beauty and makeup:
NosmaiBeauty.clearAllBeautyFilters();
Apply makeup
NosmaiBeauty.applyLipstickStyle(
NosmaiBeauty.LIPSTICK_MATTE,
0.62f,
0.12f,
0.18f
);
NosmaiBeauty.setMakeupIntensity(
NosmaiBeauty.MAKEUP_LIPSTICK,
0.65f
);
Remove one makeup category:
NosmaiBeauty.removeMakeup(NosmaiBeauty.MAKEUP_LIPSTICK);
Remove all built-in makeup:
NosmaiBeauty.clearBuiltInMakeup();
Apply a local .nosmai package
Use applyEffect for filter, effect, beauty_effect, and background packages:
NosmaiEffects.applyEffect(
filterPath,
new NosmaiEffects.EffectCallback() {
@Override
public void onSuccess() {
// Update selected state after success.
}
@Override
public void onError(String errorMessage) {
// Present or record the error.
}
}
);
Applying a package is asynchronous. Do not mark a filter as selected before onSuccess.
Remove an external package:
NosmaiEffects.removeEffect(filterInfo);
Use the typed NosmaiFilterInfo when available so the SDK can remove the correct category.
Adjust effect parameters
Some .nosmai packages expose adjustable numeric or text values.
Set a numeric value:
boolean updated = NosmaiEffects.setEffectParameter(
"intensity",
0.7f
);
Read a numeric value:
float intensity =
NosmaiEffects.getEffectParameterValue("intensity");
Set a text value:
boolean updated = NosmaiEffects.setEffectParameter(
"headerText",
"Hello"
);
Use the exact parameter names and types authored in the selected package. Unknown names and incompatible value types return false.
List local filters
Production filters use a .nosmai file, external manifest, and preview image.
List<NosmaiFilterInfo> all = NosmaiEffects.getFilters();
List<NosmaiFilterInfo> effects = NosmaiEffects.getFilters(
NosmaiFilterInfo.Type.EFFECT
);
Development-only loose packages can be scanned asynchronously:
NosmaiEffects.getDebugFilters(
NosmaiFilterInfo.Type.FILTER,
(filters, error) -> {
if (error != null) {
return;
}
// Show development filters.
}
);
Do not use debug discovery as the production catalog.
Observe active state
Listen for state changes instead of guessing which item remained active:
NosmaiEffects.PipelineStateListener listener = state -> {
NosmaiFilterInfo activeFilter = NosmaiEffects.getActiveFilterInfo();
NosmaiFilterInfo activeEffect = NosmaiEffects.getActiveEffectInfo();
// Update the application's selected buttons.
};
NosmaiEffects.addPipelineStateListener(listener);
Remove the listener when its screen or controller is destroyed:
NosmaiEffects.removePipelineStateListener(listener);
Background effects
Apply a manual background configuration through NosmaiEffects:
NosmaiBackgroundSegmentationConfig config =
new NosmaiBackgroundSegmentationConfig();
config.mode = NosmaiBackgroundSegmentationConfig.Mode.BLUR;
config.blurStrength = 55.0f;
NosmaiEffects.setBackgroundSegmentation(config);
Clear it:
NosmaiEffects.clearBackgroundSegmentation();
Use the exact constructors and mode names from the installed SDK version. Background configuration classes can gain new modes between releases.
Cloud filters
Use NosmaiCloud.cachedList() to paint an existing catalog immediately. Run NosmaiCloud.fetch(...) away from the main thread so opening a cloud-filter sheet never blocks the camera interface.
List<NosmaiCloud.Item> cached = NosmaiCloud.cachedList();
NosmaiCloud.FilterQuery query = new NosmaiCloud.FilterQuery();
query.filterType = "effects";
query.page = 1;
query.limit = 20;
query.fetchAllPages = false;
query.cleanupRemoved = false;
cloudExecutor.execute(() -> {
boolean success = NosmaiCloud.fetch(query);
List<NosmaiCloud.Item> items = success
? NosmaiCloud.list()
: Collections.emptyList();
mainHandler.post(() -> {
// Confirm this screen or request still owns the result.
// Then render items or show Retry.
});
});
Download the selected identifier and apply only the returned local path:
NosmaiCloud.download(
item.id,
(filterId, success, localPath, error) -> {
if (success && localPath != null && !localPath.isEmpty()) {
NosmaiEffects.applyEffect(
localPath,
new NosmaiEffects.EffectCallback() {
@Override
public void onSuccess() {
// Confirm selection from the pipeline listener.
}
@Override
public void onError(String message) {
// Keep the previous selection and offer Retry.
}
}
);
}
}
);
Deduplicate repeated taps by cloud identifier and ignore UI updates after the owning sheet closes. See Cloud filters for category values, pagination, progress callbacks, cache behavior, and complete edge-case handling.
Record processed video
Create an output path in application storage, then start recording:
File output = new File(
getExternalFilesDir(null),
"nosmai_" + System.currentTimeMillis() + ".mp4"
);
NosmaiSDK.startRecording(
previewView,
output.getAbsolutePath(),
new NosmaiSDK.RecordingCallback() {
@Override
public void onStarted(boolean success, String error) {
if (!success) {
// Recording did not start.
}
}
}
);
Stop and wait for finalization:
NosmaiSDK.stopRecording(new NosmaiSDK.RecordingCallback() {
@Override
public void onCompleted(
String outputPath,
boolean success,
String error
) {
if (success) {
// The MP4 is finalized at outputPath.
}
}
});
Do not leave the camera screen while recording is still being finalized.
Native Android photo capture is owned by the application's camera implementation in the current SDK. Flutter exposes a higher-level capturePhoto method through its platform view.
Lifecycle
Use the following ownership:
| Event | Action |
|---|---|
| Camera screen opens | Create preview, start Nosmai processing, then start Camera2 |
| App goes to background | Stop Camera2 and pause the preview view |
| App returns | Resume the preview view and reconnect Camera2 |
| Camera screen closes | Stop Camera2, stop processing, remove the preview |
| Application truly finishes SDK use | Call full cleanup |
Example:
@Override
protected void onResume() {
super.onResume();
if (previewView != null) {
previewView.onResume();
}
if (hasCameraPermission()) {
cameraHelper.startCamera();
}
}
@Override
protected void onPause() {
if (cameraHelper != null) {
cameraHelper.stopCamera();
}
if (previewView != null) {
previewView.onPause();
}
super.onPause();
}
@Override
protected void onDestroy() {
if (cameraHelper != null) {
cameraHelper.stopCamera();
}
NosmaiSDK.stopProcessing();
if (previewView != null && previewView.getParent() != null) {
((ViewGroup) previewView.getParent()).removeView(previewView);
}
previewView = null;
super.onDestroy();
}
Do not call NosmaiSDK.cleanup() every time a camera activity briefly pauses. Full cleanup requires a later initialization before the SDK can be used again.
Release configuration
The AAR includes consumer rules for its public SDK classes. If release-only failures occur, confirm:
- the current AAR is used
- consumer ProGuard rules are merged
- JNI methods have not been renamed or removed
- the final APK or AAB contains
lib/arm64-v8a/libnosmai.so - all protected SDK assets are present
Distribute an Android App Bundle when publishing to Google Play. Devices without the supported architecture will not receive an incompatible split.
Common problems
| Problem | What to check |
|---|---|
UnsatisfiedLinkError | AAR version, Java API version, native library packaging, and ABI |
| Black preview | Runtime permission, camera source, preview readiness, and activity lifecycle |
| Preview stays black after camera switch | Stop the old Camera2 session completely before opening the new one |
| Camera indicator stays on | Stop Camera2 when leaving the screen |
| Effect appears reversed | Camera-facing value and single mirror owner |
| Filter apply crashes in release only | Consumer ProGuard rules and matching AAR classes/native library |
| Recording does not start | Preview readiness, output path, storage, and current recording state |
| Effect selection UI is wrong | Update it from apply callbacks and active-state listener |
| Emulator cannot install | Use an arm64 physical device |
Production checklist
- initialize once at application level
- request camera permission before starting Camera2
- use one camera source per preview
- stop the old camera before switching
- stop Camera2 before removing the screen
- apply external packages asynchronously
- remove active-state listeners
- finalize recording before navigation
- test front and back camera orientation
- test background and foreground transitions
- test repeated camera-screen navigation
- test rapid filter switching
- test a low-end and high-end arm64 device
- test a long recording or streaming session