flutter_biometric_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

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.

Email 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:

  1. ML Kit detects the largest face bounding box
  2. Bounding box is expanded by 50% margin
  3. Crop is resized to max 200 px on the longest side
  4. Saved as JPEG quality 65
  5. FaceNet TFLite inference → 128-dim float32 embedding
  6. 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]
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

  1. 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.
  2. Face detection — Google ML Kit detects and tracks the face on every camera frame.
  3. Liveness inference — MiniFASNet TFLite model (encrypted, decrypted in RAM at runtime) scores each face region. Up to 3 passes; best score wins.
  4. 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.
  5. Result — returned to your app via onResult callback. 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 inside onResult.
  • 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.0 internally means a license or model error (fail-closed behaviour).