Liveness Edge Flutter

Private, offline face-liveness verification for Flutter. Camera frames, face landmarks, active challenges, and passive anti-spoofing are processed on the device; the package does not make network requests.
Features
- Ready-to-use camera screen with face-position guidance.
- Three or four active challenges randomly selected from blink, one randomly directed head turn, smile, and open mouth.
- Passive presentation-attack score using MiniFASNetV2.
- Same-face continuity checks using normalized facial landmarks.
- Native MediaPipe Face Landmarker and ONNX Runtime processing.
- Final verified capture returned as JPEG bytes.
- Localizable messages and customizable guidance, supporting text, and retry UI.
- Android and iOS support with bundled models; no backend required.
Platform support
| Platform | Minimum version | Status |
|---|---|---|
| Android | API 24 | Supported |
| iOS | 15.1 | Supported |
The host device needs a front-facing camera. Desktop and web are not supported.
Installation
Add the package to your application:
flutter pub add liveness_edge_flutter
Alternatively, add it directly to pubspec.yaml:
dependencies:
liveness_edge_flutter: ^0.3.2
Android setup
Add camera permission to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
Ensure the application has minSdk 24 or newer.
iOS setup
Add a camera usage description to ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>The camera is used to verify that you are present.</string>
Set the application deployment target to iOS 15.1 or newer.
Usage
Import the package and push LivenessEdgeScreen as a full-screen route:
import 'package:flutter/material.dart';
import 'package:liveness_edge_flutter/liveness_edge_flutter.dart';
Future<LivenessResult?> captureLiveness(BuildContext context) async {
LivenessResult? verifiedResult;
await Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (routeContext) => LivenessEdgeScreen(
configuration: const LivenessConfiguration(
validations: {
LivenessValidation.blink,
LivenessValidation.headTurn,
LivenessValidation.smile,
LivenessValidation.openMouth,
LivenessValidation.passiveAntiSpoof,
},
timeout: Duration(minutes: 2),
maxFrames: 3000,
passiveAntiSpoofSensitivity: PassiveAntiSpoofSensitivity.high,
faceIdentitySensitivity: FaceIdentitySensitivity.strict,
),
onSuccess: (result) {
verifiedResult = result;
Navigator.of(routeContext).pop();
},
onFailed: (result) {
debugPrint('Liveness failed: ${result.instruction}');
},
onCancel: () => Navigator.of(routeContext).pop(),
),
),
);
return verifiedResult;
}
On success, result.imageBytes contains the verified JPEG and
result.passiveScore contains the median passive anti-spoof score. The package
does not upload or persist either value.
All user-facing text defaults to English. Override any message through the configuration to localize the flow or use your own product copy:
const LivenessConfiguration(
messages: LivenessMessages(
moveRight: 'Geser wajah ke kanan',
moveLeft: 'Geser wajah ke kiri',
blink: 'Kedipkan kedua mata sekali.',
turnLeft: 'Hadapkan kepala sedikit ke kiri.',
turnRight: 'Hadapkan kepala sedikit ke kanan.',
smile: 'Tersenyumlah.',
openMouth: 'Buka mulut.',
returnToNeutral: 'Kembali ke ekspresi netral.',
tryAgain: 'Coba lagi',
),
);
Colors, the camera-guide shape, an optional header, text, and the retry button can use the host application's design system. Text styles are merged with the themed defaults, while builders replace their corresponding built-in content:
LivenessEdgeScreen(
theme: const LivenessEdgeTheme(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
primaryColor: Colors.orange,
successColor: Colors.green,
inactiveRingColor: Colors.white24,
cameraShape: LivenessCameraShape.roundedRectangle,
cameraBorderRadius: 32,
guidelineTextStyle: TextStyle(
fontSize: 23,
fontWeight: FontWeight.w700,
),
supportingTextStyle: TextStyle(fontSize: 14),
),
headerBuilder: (context, state) => const CompanyLogo(),
retryButtonBuilder: (context, label, onPressed) => OutlinedButton.icon(
onPressed: onPressed,
icon: const Icon(Icons.replay_rounded),
label: Text(label),
),
);
cameraShape supports oval (the default), circle, and
roundedRectangle. cameraBorderRadius applies to rounded rectangles. The
header receives a read-only LivenessEdgeViewState containing the current
status, instruction, supporting text, progress, camera readiness, and error
state. The legacy guidelineTextStyle and supportingTextStyle screen
properties remain available for backward compatibility but are deprecated.
Configuration
| Property | Default | Description |
|---|---|---|
validations |
All validations | Enabled checks: blink, headTurn, smile, openMouth, and/or passiveAntiSpoof. headTurn randomly chooses either a left or right turn for the session. |
minimumActiveChallenges |
3 |
Minimum active actions selected per session, capped by the available pool. |
maximumActiveChallenges |
4 |
Maximum active actions selected per session, capped by the available pool. |
timeout |
2 minutes | Maximum session duration. |
maxFrames |
3000 | Safety cap for analyzed frames. Wall-clock duration is controlled by timeout. |
passiveAntiSpoofSensitivity |
balanced |
Anti-spoof preset: low (0.40), balanced (0.50), high (0.60), strict (0.70), or withValue(...) for a custom value from 0 to 1. |
faceIdentitySensitivity |
balanced |
Face replacement preset: low (0.16), balanced (0.10), high (0.06), strict (0.035), or withValue(...) for a custom value greater than 0. |
messages |
English LivenessMessages |
User-facing challenge instructions and screen labels. Override individual values to localize the flow. |
Treat the default threshold as a starting point. Calibrate it using genuine
users, target devices, lighting conditions, and representative attacks.
At least one validation must be enabled. A session securely shuffles its active
action pool and selects three or four actions without duplicates by default.
Because headTurn contributes one randomly directed action, the default pool
contains four active actions. If fewer active validations are enabled, the
configured minimum and maximum are capped to the number available. A
passive-only session is also supported by enabling only passiveAntiSpoof.
Only the current instruction is displayed. Each gesture must be observed across
multiple frames and return to its neutral pose before the next action starts.
Disabled active challenges are skipped, so their instructions are not shown.
Passive sensitivity is ignored when passiveAntiSpoof is disabled.
After the configured checks pass, the package waits for three stable, frontal frames with both eyes fully open before producing the verified JPEG.
The package keeps a frozen, normalized, on-device landmark descriptor for the first aligned face. A different frontal face must remain consistent for at least 750 ms and three analyzed frames before the active challenge returns to alignment. Two stable matching frames are required to recover from a pending mismatch, and a session cannot pass while a mismatch is pending. If no valid single face or required facial landmarks are available during an active challenge for 500 ms, the flow also returns to alignment. Before a challenge starts, the single-face grace period remains two seconds.
Landmark availability uses MediaPipe visibility and presence confidence when the model provides them, and also rejects non-finite or out-of-frame eye and mouth landmarks. It is an input-quality check, not general-purpose occlusion segmentation; applications that must detect every type of mask or obstruction should add a separately validated occlusion model.
Callback behavior
onSuccessruns once after both checks pass. It does not close the route; the host application controls navigation.onFailedruns once when an attempt fails because it times out, exceeds the frame limit, or does not pass the passive anti-spoof check. Retrying starts a new attempt and may invoke it again. It does not close the route.onCancelruns when the close button is pressed. When omitted, the screen attempts to pop its route automatically.- A failed or timed-out session shows a retry action. Camera or model errors do
not invoke
onFailed, because they do not produce a final liveness result.
Result fields
| Property | Availability | Description |
|---|---|---|
status |
Always | Current or terminal LivenessStatus. A terminal result is either passed or failed. |
passed |
Always | Convenience getter that is true only when status is passed. |
instruction |
Always | Localized guidance or the terminal result message. |
framesProcessed |
Always | Number of analyzed frames in the current attempt. |
imageBytes |
Success only | Mirrored, portrait-normalized JPEG cropped to the face-guide aspect ratio. |
passiveScore |
Terminal result when passive anti-spoofing is enabled | Median of the recent on-device live-score samples. Higher values are considered more likely to be live. |
How it works
Package integration flow
The android/ and ios/ directories at the package root contain the native
plugin implementations that are included in every host application. The
directories with the same names under example/ are only the Android and iOS
hosts used to run the sample application.
Host application (or example/lib/main.dart)
-> package public API (lib/liveness_edge_flutter.dart)
-> LivenessEdgeScreen and Dart challenge state machine
-> Flutter MethodChannel
-> Android: android/.../LivenessEdgeFlutterPlugin.kt
-> iOS: ios/Classes/LivenessEdgeFlutterPlugin.swift
-> MediaPipe Face Landmarker + ONNX Runtime
-> LivenessResult returned to the host application
The relevant package layout is:
liveness_edge_flutter/
├── lib/ public API and Dart implementation
├── android/ native Android plugin implementation
├── ios/ native iOS plugin implementation
└── example/
├── lib/ sample Flutter application
├── android/ Android host for the sample
└── ios/ iOS host for the sample
Frame processing flow
Flutter camera frame
-> upright, downsampled BGR frame
-> MediaPipe Face Landmarker (bounds, blink, expression, and head yaw)
-> MiniFASNetV2 through ONNX Runtime (passive live score)
-> challenge state machine (align, 3-4 randomized actions, neutral return,
same-face continuity, stable final capture)
-> LivenessResult
Models are bundled with the native Android and iOS implementations. Processing stays in application memory unless the host application stores or transmits the returned image.
Security and privacy
Client-side liveness is not tamper-proof. Do not use result.passed as the sole
authorization for banking, e-KYC, account recovery, or another high-risk server
transaction. For those flows, combine local inference with a server-issued
nonce, platform attestation, replay protection, and a trusted server-side
decision.
The passive model primarily targets printed-photo and screen-replay attacks. It has not been validated by this project against masks, 3D attacks, camera injection, rooted devices, or modified applications. The host application is responsible for consent, retention, logging, screenshots, backups, and privacy disclosures.
Review the licenses and suitability of MediaPipe, ONNX Runtime, and the bundled MiniFASNetV2 model before commercial distribution.
Troubleshooting
- Camera permission denied: request permission through the normal platform flow and ensure the manifest or plist entry above is present.
- Front camera unavailable: the capture screen reports an error and allows retrying; use a physical device with a front-facing camera.
- Frequent passive failures: test representative lighting and devices before
lowering
passiveAntiSpoofSensitivity; lower values increase spoof acceptance risk. - Simulator or emulator: camera and native inference behavior can differ from physical hardware, so release validation should use real devices.
Example and development
See example/lib/main.dart for a runnable integration.
flutter analyze
flutter test
cd example
flutter run
Bug reports and feature requests are welcome in the project issue tracker.
Support
If this package is useful to you, you can support its development through Buy Me a Coffee.
Libraries
- liveness_edge_flutter
- Offline face-liveness verification for Flutter.