The Android SDK is a single AAR containing the Java API
(com.affectiva.vision) and the native arm64 libraries. You feed it camera frames as byte
arrays; it returns Face objects with the same expressions, emotions, and measurements as
the other platforms.
1 Β· Requirements
- Device: 64-bit ARM (
arm64-v8a) running Android 8.0 (API level 26) or newer. 32-bit devices and x86 emulator images are not supported; use a physical device or an arm64 emulator image. - Native libraries are 16Β KB page-size aligned, so the AAR is compatible with Android 15+ devices that enforce 16Β KB pages.
- License key β passed to
Detector.start(license)(see Licensing).
2 Β· Project setup
The delivered AAR names the platform, NDK, and version, so the filename changes with every
release β for example
affectiva-emotion-sdk-sdk-android-arm64-ndk24-clang-5.3.1.aar. Drop it into
app/libs/ and match it by extension rather than by name, so it stays correct across
upgrades:
app/build.gradle
android {
defaultConfig {
minSdkVersion 26
ndk { abiFilters 'arm64-v8a' } // ship only the supported ABI
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
implementation 'androidx.annotation:annotation:1.7.0'
}
To pin one exact file instead, name it in full:
implementation files('libs/affectiva-emotion-sdk-sdk-android-arm64-ndk24-clang-5.3.1.aar')
The AAR declares package="com.affectiva.vision" and
minSdkVersion 26, and carries native libraries for arm64-v8a only
(libaffectiva-vision.so, its JNI wrapper, and the ONNX Runtime libraries β all bundled,
nothing extra to ship).
β¦or, if your organization mirrors the Affectiva Maven repository, depend on the published coordinates:
dependencies {
implementation 'com.affectiva.android:emotion-sdk:5.4.0'
}
Request the camera permission in your manifest (and at runtime, as usual for dangerous permissions):
AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="true" />
3 Β· Using the detector
The Java API mirrors the C++ flow: construct β enable β listen β start β process. On Android,
per-frame results are delivered through ImageListener.onImageResults;
FaceListener carries the face found/lost events.
FaceAnalyzer.java
import android.content.Context;
import com.affectiva.vision.Face;
import com.affectiva.vision.FaceListener;
import com.affectiva.vision.Feature;
import com.affectiva.vision.Frame;
import com.affectiva.vision.FrameDetector;
import com.affectiva.vision.ImageListener;
import com.affectiva.vision.ProcessStatusListener;
import com.affectiva.vision.AffectivaException;
import java.util.Arrays;
import java.util.Map;
public class FaceAnalyzer {
private FrameDetector detector;
public void create(Context context, String licenseKey) {
// 1. Construct: 30 fps target, track 1 face, auto thread pool.
detector = new FrameDetector(context);
// 2. Enable the metric families you need.
detector.enable(Arrays.asList(Feature.EXPRESSIONS, Feature.EMOTIONS));
// 3. Register listeners.
detector.setImageListener(new ImageListener() {
@Override
public void onImageResults(Map<Integer, Face> faces, Frame frame) {
for (Face face : faces.values()) {
Float smile = face.getExpressions().get(Face.Expression.SMILE);
Float joy = face.getEmotions().get(Face.Emotion.JOY);
android.util.Log.d("Affectiva", "t=" + frame.getTimeStamp()
+ " face=" + face.getId()
+ " smile=" + smile + " joy=" + joy);
}
}
@Override
public void onImageCapture(Frame frame) {
// called for every input frame, including dropped ones
}
});
detector.setFaceListener(new FaceListener() {
@Override
public void onFaceFound(long timestamp, int faceId) { }
@Override
public void onFaceLost(long timestamp, int faceId) { }
});
detector.setProcessStatusListener(new ProcessStatusListener() {
@Override
public void onProcessingException(AffectivaException exception) {
android.util.Log.e("Affectiva", "processing error", exception);
}
});
// 4. Start with your license key.
detector.start(licenseKey);
}
// 5. Call for every camera frame (NV21 bytes + timestamp in ms).
public void onCameraFrame(byte[] nv21, int width, int height, long timestampMs) {
Frame frame = new Frame(width, height, nv21,
Frame.ColorFormat.YUV_NV21, Frame.Rotation.CW_90, timestampMs);
detector.process(frame);
}
public void destroy() {
if (detector != null) {
detector.stop();
detector.dispose(); // releases the native detector β required
detector = null;
}
}
}
The example, explained
Constructing the detector
detector = new FrameDetector(context);
Input: any Android Context β the SDK uses it to initialize its
runtime storage on first run. We use the asynchronous FrameDetector because
camera callbacks must never block: it queues each frame for a background thread and returns
immediately. The single-argument constructor applies the defaults
processingFrameRateΒ =Β 30 (frames arriving faster than this are dropped, which
is what keeps the pipeline real-time), maxNumFacesΒ =Β 1, and
maxThreadPoolSizeΒ =Β 0 (auto: CPU cores β 1).
Enabling features
detector.enable(Arrays.asList(Feature.EXPRESSIONS, Feature.EMOTIONS));
Input: a list of the metric families to compute. Only enabled families cost CPU
and appear in the Face maps; tracking metrics (bounding box, measurements, confidence)
are always delivered. On Android, Feature has exactly these two values β face tracking
itself is always on. Call before start().
ImageListener β where results arrive
detector.setImageListener(new ImageListener() {
@Override
public void onImageResults(Map<Integer, Face> faces, Frame frame) {
On Android, ImageListener.onImageResults is the per-frame results callback.
Input: a map of tracking id β Face for every currently tracked face
(empty if none) and the Frame the results belong to β its
getTimeStamp() tells you which moment they describe. Output you read:
face.getExpressions() / getEmotions() return
Map<Enum, Float> with one entry per enabled metric, values 0β100 (see the
Metrics guide). Because this is the asynchronous detector, the callback
runs on a background thread β post to the main thread before touching views.
onImageCapture β every input frame
@Override
public void onImageCapture(Frame frame) {
// called for every input frame, including dropped ones
}
Fires for each frame you submit, including frames the detector drops to honor its
processing rate β unlike onImageResults, which fires only for processed frames. Useful
for keeping a preview or recording in sync with what you fed in; leave it empty otherwise.
Face events and background errors
detector.setFaceListener(new FaceListener() { ... });
detector.setProcessStatusListener(new ProcessStatusListener() {
@Override
public void onProcessingException(AffectivaException exception) {
FaceListener delivers the found/lost moments (input: frame
timestamp in ms + tracking id) β on Android it carries only these two events, since results flow
through ImageListener. ProcessStatusListener is how errors from the
background processing thread reach you; without it they would be invisible. Register all listeners
before start().
Starting
detector.start(licenseKey);
Input: your license key string. Validates the license and initializes the
embedded models; throws AffectivaException if the key is missing or invalid. There is
no environment-variable fallback on Android β always pass the key (load it from your app’s secure
configuration, not a hardcoded string, in production).
Feeding camera frames
public void onCameraFrame(byte[] nv21, int width, int height, long timestampMs) {
Frame frame = new Frame(width, height, nv21,
Frame.ColorFormat.YUV_NV21, Frame.Rotation.CW_90, timestampMs);
detector.process(frame);
}
Input: the camera’s pixel buffer plus geometry and timing. For
YUV_NV21, the byte array holds width Γ height Γ 3/2 bytes (the standard
Android camera preview layout). Rotation.CW_90 declares that the sensor image is
rotated 90Β° clockwise relative to an upright face β typical for portrait phone orientation; derive
the real value from your camera’s rotation info rather than hardcoding it. The timestamp (ms) must
strictly increase; SystemClock.elapsedRealtime() is a good source.
process() returns immediately β output arrives later on the background
thread via onImageResults, and frames may be silently skipped if they exceed the
processing rate.
Tearing down
detector.stop();
detector.dispose(); // releases the native detector β required
detector = null;
stop() halts processing and frees processing resources; dispose()
releases the underlying native detector object. Java garbage collection does not reclaim
native memory, so skipping dispose() leaks it. After dispose(), further
calls on the object are ignored (and logged) β hence dropping the reference.
4 Β· Camera integration
The SDK does not open the camera; it consumes pixel buffers your app captures. Typical setup with CameraX:
- Use an
ImageAnalysisuse case. CameraX deliversYUV_420_888images; convert each to an NV21 byte array (Y plane followed by interleaved VU) before wrapping it in aFramewithColorFormat.YUV_NV21. - Pass the frame’s rotation instead of rotating pixels: map
imageProxy.getImageInfo().getRotationDegrees()(90/180/270) toFrame.Rotation.CW_90 / CW_180 / CW_270. - Use
SystemClock.elapsedRealtime()(or the image timestamp converted to milliseconds) for the frame timestamp β timestamps must strictly increase.
Other capture stacks (Camera2, legacy Camera with NV21 preview callbacks, video
decoders) work the same way: any source that yields one of the supported
color formats plus a timestamp can feed the
detector.
5 Β· Lifecycle & threading
- Always call
dispose()when you are done with a detector (for example inonDestroy()). It releases the underlying native detector; relying on garbage collection alone leaks native memory. FrameDetectoris asynchronous:process()returns immediately and callbacks arrive on a background thread. Post results to the main thread before touching views.SyncFrameDetectorprocesses every frame on the calling thread β do not use it on the UI thread.- Between camera sessions, call
reset()instead of recreating the detector; timestamps may then restart at 0. - Constructor knobs:
new FrameDetector(context, processingFrameRate, maxNumFaces, maxThreadPoolSize)β see the detector reference for all overloads and defaults.
Next steps
- The Android API reference documents every Java class
and enum β including the Android-specific behaviors (
FeatureexposesEXPRESSIONS/EMOTIONS; results arrive viaImageListener; errors useAffectivaException).