flutter_biometric_liveness 1.0.1
flutter_biometric_liveness: ^1.0.1 copied to clipboard
Passive face liveness detection, face template extraction, and cosine-similarity face matching for Flutter (iOS & Android).
video_liveness #
A Flutter plugin for passive face liveness detection, face extraction, and face matching.
- Verifies a real live human face — rejects printed photos, screen replays, and 3-D masks
- Extracts a compact 512-byte face template from any face image
- Compares two templates with cosine similarity — fast enough for 2,000+ user databases
- Works on iOS and Android
- License-controlled — each client app must be granted a license token
- All AI models are encrypted and decrypted in-memory at runtime — never left on disk
Screenshots #
[assets/1.jpeg] [assets/2.jpeg] [assets/3.jpg] [assets/4.jpg]
Want to try it out yourself? Download the Example APK from Releases
Requirements #
| Platform | Minimum version |
|---|---|
| Android | API 24 (Android 7.0) |
| iOS | 12.0 |
| Flutter | 3.x |
Installation #
dependencies:
video_liveness:
path: ../video_liveness # adjust to your local path
Android setup #
Add the camera permission to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
iOS setup #
Add the camera usage description to ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>Required for identity liveness verification</string>
Obtaining a License #
The SDK is license-gated. You must have a valid token before it will run.
| sushanta.97.behera@gmail.com |
Provide your iOS Bundle Identifier and/or Android package name.
We issue an opaque token tied to your app identifier and an expiry date.
No code changes are needed — the SDK validates the token automatically on every launch.
Internet required on first launch so the SDK can fetch and verify your token.
After the first success, the token is cached locally and subsequent launches work offline.
Quick Start #
import 'package:video_liveness/video_liveness_sdk.dart';
// 1. Initialise once at startup (loads the face-recognition model, ~300 ms)
await VideoLiveness.initFaceRecognition();
// 2. Launch liveness + face extraction
await VideoLiveness.launch(
context: context,
defaultCameraLens: CameraLensDirection.front, // Use .back to default to rear camera
onResult: (LivenessResult result) {
if (result.passed) {
// result.faceTemplate — Uint8List, 512 bytes — save this to your backend
// result.capturedFrame — JPEG of the cropped face (max 200 px) — display only
print('Liveness passed: ${result.livenessPct.toStringAsFixed(1)}%');
}
},
onError: (e) => print('Error: $e'),
);
Feature 1 — Liveness Detection #
The SDK opens a full-screen camera with an oval face guide and voice prompts. It runs MiniFASNet passive anti-spoof inference and returns a result.
Thresholds #
| Value | Meaning |
|---|---|
livenessPct > 65 |
Real live face — pass |
livenessPct ≤ 65 |
Possible spoof — reject |
LivenessResult fields #
| Field | Type | Description |
|---|---|---|
passed |
bool |
true when livenessPct > 65 |
livenessPct |
double |
Confidence 0–100% |
avgSpoofScore |
double |
Raw model output (lower = more live) |
capturedFrame |
Uint8List? |
JPEG cropped face (max 200 px, quality 65) |
faceTemplate |
Uint8List? |
512-byte face embedding (only when initFaceRecognition() has been called) |
failureReason |
String? |
Non-null only when passed == false |
Feature 2 — Face Template Extraction #
Extract a 512-byte face template from any image — useful for gallery-based enrollment.
// From any image bytes (JPEG or PNG)
final result = await VideoLiveness.extractFace(imageBytes);
if (result != null) {
final template = result.faceTemplate; // Uint8List, 512 bytes — store in backend
final faceImage = result.croppedFace; // JPEG bytes — display only
}
What extractFace() does internally:
- ML Kit detects the largest face bounding box
- Bounding box is expanded by 50% margin
- Crop is resized to max 200 px on the longest side
- Saved as JPEG quality 65
- FaceNet TFLite inference → 128-dim float32 embedding
- Packed as 512-byte
Uint8List(128 × float32, little-endian)
Template format: 512 bytes = 128 × float32 (4 bytes each, little-endian).
Store exactly these bytes in your backend. Do not convert or re-encode them.
Feature 3 — Face Matching #
Compare two templates with a synchronous cosine similarity score.
// Both templates must be generated by this SDK (from same model version)
final score = VideoLiveness.compareFaces(enrolledTemplate, liveTemplate);
// score ∈ [0.0, 1.0]
Recommended thresholds #
| Score | Meaning | Action |
|---|---|---|
| ≥ 0.85 | Strong match (0.85 to 0.90 is recommended) | ✅ Approve — same person |
| 0.70 – 0.84 | Borderline | ⚠️ Optional manual review |
| < 0.70 | No match | ❌ Reject — different person |
Bulk matching (2,000+ users) #
compareFaces() is pure Dart and synchronous. To match one live face against
a database of N enrolled templates:
double bestScore = 0.0;
int? bestMatchIndex;
for (int i = 0; i < enrolledTemplates.length; i++) {
final score = VideoLiveness.compareFaces(enrolledTemplates[i], liveTemplate);
if (score > bestScore) {
bestScore = score;
bestMatchIndex = i;
}
}
if (bestScore >= 0.65) {
print('Matched user index $bestMatchIndex with score ${(bestScore * 100).toStringAsFixed(1)}%');
}
2,000 comparisons complete in < 1 ms on any mid-range device.
Full Enrollment + Verification Flow #
// ── 1. App startup ─────────────────────────────────────────────────────────
await VideoLiveness.initFaceRecognition();
// ── 2. Enrollment (camera — includes liveness check) ───────────────────────
await VideoLiveness.launch(
context: context,
onResult: (result) async {
if (result.passed && result.faceTemplate != null) {
await myBackend.saveTemplate(userId, result.faceTemplate!);
}
},
);
// ── 3. Enrollment (gallery — no liveness, use for backend import only) ─────
final pick = await ImagePicker().pickImage(source: ImageSource.gallery);
if (pick != null) {
final extraction = await VideoLiveness.extractFace(await pick.readAsBytes());
if (extraction != null) {
await myBackend.saveTemplate(userId, extraction.faceTemplate);
}
}
// ── 4. Verification ─────────────────────────────────────────────────────────
final enrolledTemplate = await myBackend.getTemplate(userId); // Uint8List 512 bytes
await VideoLiveness.launch(
context: context,
onResult: (result) {
if (!result.passed) return; // Liveness failed
if (result.faceTemplate == null) return;
final score = VideoLiveness.compareFaces(enrolledTemplate, result.faceTemplate!);
final matched = score >= 0.65;
print('Score: ${(score * 100).toStringAsFixed(1)}% Match: $matched');
},
);
Camera Permission #
The SDK requests camera permission at runtime. On Android you can request it
explicitly before calling launch():
import 'package:permission_handler/permission_handler.dart';
final status = await Permission.camera.request();
if (status.isGranted) {
await VideoLiveness.launch(...);
}
How It Works #
- License gate — the SDK fetches a per-app token securely and validates it inside a compiled native C++ layer (not readable Dart or Kotlin/Swift). Checks app identifier, HMAC signature, and expiry date.
- Face detection — Google ML Kit detects and tracks the face on every camera frame.
- Liveness inference — MiniFASNet TFLite model (encrypted, decrypted in RAM at runtime) scores each face region. Up to 3 passes; best score wins.
- Face extraction — After liveness passes, a still photo is captured, cropped to the face region, and run through FaceNet TFLite to produce a 512-byte template.
- Result — returned to your app via
onResultcallback. The SDK pops itself off the navigator.
Notes #
- Both AI models (MiniFASNet and FaceNet) are XOR-encrypted in the asset bundle and decrypted in RAM at runtime. They are never written to the filesystem in plaintext.
- Liveness typically completes in 2–5 seconds on a mid-range device.
initFaceRecognition()takes ~300 ms. Call it at app startup, not insideonResult.- Template comparison is entirely offline — no network call needed.
- Templates from different SDK releases or models are not compatible. Re-enroll users after a model update.
- A liveness score of
999.0internally means a license or model error (fail-closed behaviour).