samviva

Pub Style PRs Welcome Sponsor

A Flutter liveness detection library powered by Google ML Kit Face Detection. Verify that a user is physically present — not a photo, video, or mask — through configurable active face challenges.


How It Works

Samviva uses Google ML Kit Face Detection to analyze every camera frame in real-time. ML Kit returns probability values (0.0–1.0) for facial expressions and head pose. Samviva compares those values against your configured thresholds to determine whether a challenge is completed.

Camera frame → ML Kit Face Detection → Probability values → Threshold comparison → Challenge result

All processing happens on-device — no server, no network call.


Supported Challenges

Challenge ML Kit Signal Default Threshold
blink Average of leftEyeOpenProbability + rightEyeOpenProbability < 0.3 (eyes must be 70%+ closed)
smile smilingProbability > 0.75 (75%+ confident smile)
turnLeft headEulerAngleY (positive = left) > 20.0°
turnRight headEulerAngleY (negative = right) < -20.0°

Probability Thresholds Explained

This is the core concept behind samviva's accuracy. Each ML Kit signal is a float between 0.0 and 1.0.

ML Kit reports eyeOpenProbability: 1.0 means fully open, 0.0 means fully closed.

Samviva detects a blink when the average eye open probability drops below blinkEyeClosedThreshold.

// Eye open probability: 0.12 → average < 0.3 → blink detected ✓
blinkEyeClosedThreshold: 0.3  // default — eyes must close ~70%
blinkEyeClosedThreshold: 0.1  // strict   — eyes must close ~90% (harder to spoof)
blinkEyeClosedThreshold: 0.5  // relaxed  — any half-blink counts

Lower = stricter. A lower threshold requires a more definitive eye closure.

Smile — smileThreshold

ML Kit reports smilingProbability: 1.0 means definitely smiling, 0.0 means not smiling.

Samviva detects a smile when this value rises above smileThreshold.

smileThreshold: 0.75  // default — must be clearly smiling
smileThreshold: 0.90  // strict  — must be broadly smiling
smileThreshold: 0.50  // relaxed — any hint of a smile counts

Higher = stricter.

Head Turn — headTurnAngle

ML Kit reports headEulerAngleY: the rotation of the head around the vertical axis in degrees.

  • Positive values → head turned to the left
  • Negative values → head turned to the right

Samviva detects a turn when the absolute angle exceeds headTurnAngle.

headTurnAngle: 20.0  // default — 20° rotation required
headTurnAngle: 30.0  // strict  — must turn head further
headTurnAngle: 10.0  // relaxed — slight turn counts

Higher = stricter.


Installation

1. Add dependency

# pubspec.yaml
dependencies:
  samviva: ^1.0.0

Then run:

flutter pub get

2. Android setup

In android/app/build.gradle, set minimum SDK to 21:

android {
    defaultConfig {
        minSdkVersion 21
    }
}

In android/app/src/main/AndroidManifest.xml, add camera permission:

<manifest>
    <uses-permission android:name="android.permission.CAMERA" />
    ...
</manifest>

3. iOS setup

In ios/Runner/Info.plist, add camera usage description:

<key>NSCameraUsageDescription</key>
<string>Camera is required for liveness verification.</string>

In ios/Podfile, ensure the minimum iOS version is 13.0:

platform :ios, '13.0'

Quick Start

import 'package:samviva/samviva.dart';

Scaffold(
  body: SamvivaLivenessView(
    challenges: const [
      SamvivaLivenessChallenge.blink,
      SamvivaLivenessChallenge.smile,
    ],
    onSuccess: (SamvivaLivenessResult result) {
      print('Liveness passed!');
      print('Captured image bytes: ${result.capturedImage?.length}');
    },
    onFailure: (SamvivaLivenessFailure failure) {
      print('Failed: ${failure.message}');
    },
  ),
);

Full Configuration

SamvivaLivenessView(
  // Challenge sequence — completed in order
  challenges: const [
    SamvivaLivenessChallenge.blink,
    SamvivaLivenessChallenge.smile,
    SamvivaLivenessChallenge.turnLeft,
    SamvivaLivenessChallenge.turnRight,
  ],

  // Probability thresholds and timeouts
  config: const SamvivaLivenessConfig(
    blinkEyeClosedThreshold: 0.3,           // lower = stricter blink
    smileThreshold: 0.75,                   // higher = stricter smile
    headTurnAngle: 20.0,                    // higher = must turn further
    challengeTimeout: Duration(seconds: 5), // per-challenge timeout
    totalTimeout: Duration(seconds: 30),    // whole session timeout
  ),

  // Called once when all challenges pass
  onSuccess: (SamvivaLivenessResult result) {
    result.completedChallenges; // List<SamvivaLivenessChallenge>
    result.capturedImage;       // Uint8List? (JPEG of face at completion)
    result.timestamp;           // DateTime (UTC)
  },

  // Called if the session fails for any reason
  onFailure: (SamvivaLivenessFailure failure) {
    failure.reason;   // SamvivaLivenessFailureReason enum
    failure.message;  // Human-readable description
  },

  // Optional: receive raw ML Kit probability every frame
  onChallengeUpdate: (SamvivaLivenessChallengeUpdate update) {
    update.challenge;    // current SamvivaLivenessChallenge
    update.probability;  // raw value from ML Kit
    update.threshold;    // configured threshold
    update.isPassed;     // whether threshold was crossed
  },

  // Optional: countdown (seconds) shown after all challenges pass
  successCountdown: 3,

  // Optional: title shown at the top of the screen
  title: 'Face Verification',

  // Optional: override displayed text (great for localisation)
  labels: const SamvivaLivenessLabels(
    blinkLabel: 'Blink',
    smileLabel: 'Smile',
    turnLeftLabel: 'Left',
    turnRightLabel: 'Right',
    blinkInstruction: 'Blink your eyes',
    smileInstruction: 'Give us a smile',
    turnLeftInstruction: 'Turn your head to the left',
    turnRightInstruction: 'Turn your head to the right',
    positionFaceText: 'Position your face in the oval',
    noFaceText: 'No face detected — move closer',
    holdStillText: 'Hold still…',
    successText: 'Verification complete!',
    failureText: 'Verification failed',
  ),

  // Optional: override text styles
  textStyles: const SamvivaLivenessTextStyles(
    titleStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
    instructionStyle: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
    stepLabelStyle: TextStyle(fontSize: 10, letterSpacing: 0.4),
    countdownStyle: TextStyle(fontSize: 96, fontWeight: FontWeight.w900),
  ),

  // Optional: override colors
  theme: const SamvivaLivenessTheme(
    activeColor: Color(0xFF4ADE80),      // arc, done steps, connectors, countdown
    errorColor: Color(0xFFF87171),        // no-face and failure state
    overlayColor: Color(0xBB000000),      // dark overlay outside the oval
    stepActiveColor: Colors.white,         // active step circle fill + glow
    stepPendingColor: Colors.white,        // pending step circle base color
    connectorIdleColor: Colors.white24,    // unfilled connector between steps
  ),
)

Localisation

Pass a SamvivaLivenessLabels to override every user-visible string. This lets you use your existing intl / flutter_localizations setup without any changes to the library.

// Example: Indonesian
SamvivaLivenessView(
  labels: SamvivaLivenessLabels(
    blinkLabel: 'Kedip',
    smileLabel: 'Senyum',
    turnLeftLabel: 'Kiri',
    turnRightLabel: 'Kanan',
    blinkInstruction: 'Kedipkan mata Anda',
    smileInstruction: 'Berikan senyuman',
    turnLeftInstruction: 'Palingkan kepala ke kiri',
    turnRightInstruction: 'Palingkan kepala ke kanan',
    positionFaceText: 'Posisikan wajah Anda di dalam oval',
    noFaceText: 'Wajah tidak terdeteksi — mendekat ke kamera',
    holdStillText: 'Tahan…',
    successText: 'Verifikasi berhasil!',
    failureText: 'Verifikasi gagal',
  ),
  ...
)

API Reference

SamvivaLivenessView

The main widget. Place it anywhere — typically as the full Scaffold body.

Parameter Type Required Default Description
challenges List<SamvivaLivenessChallenge> Ordered list of challenges to complete
onSuccess void Function(SamvivaLivenessResult) Fires when all challenges pass
onFailure void Function(SamvivaLivenessFailure) Fires on timeout or camera error
config SamvivaLivenessConfig defaults Thresholds and timeouts
onChallengeUpdate void Function(SamvivaLivenessChallengeUpdate)? null Per-frame probability data
title String 'Face Verification' Title shown at the top
successCountdown int? null Seconds to count down after success
labels SamvivaLivenessLabels English defaults All user-visible strings
textStyles SamvivaLivenessTextStyles Built-in defaults Font styles for each text element
theme SamvivaLivenessTheme Green defaults Colors for overlay, arc, steps

SamvivaLivenessConfig

Parameter Type Default Description
blinkEyeClosedThreshold double 0.3 Eye open prob must be below this for blink
smileThreshold double 0.75 Smile prob must be above this for smile
headTurnAngle double 20.0 Euler Y degrees required for head turn
challengeTimeout Duration 5s Max time per challenge before failure
totalTimeout Duration 30s Max time for the entire session

SamvivaLivenessTheme

Parameter Type Default Description
activeColor Color Color(0xFF4ADE80) Arc, done step circles, filled connectors, countdown number
errorColor Color Color(0xFFF87171) No-face detected and failure state
overlayColor Color Color(0xBB000000) Semi-transparent overlay outside the face oval
stepActiveColor Color Colors.white Active step circle fill, border, glow, and label
stepPendingColor Color Colors.white Pending step circle base color (applied at reduced opacity)
connectorIdleColor Color Colors.white24 Connector line between steps not yet completed

SamvivaLivenessLabels

Parameter Type Default Description
blinkLabel String 'Blink' Step circle label for blink
smileLabel String 'Smile' Step circle label for smile
turnLeftLabel String 'Left' Step circle label for turn left
turnRightLabel String 'Right' Step circle label for turn right
blinkInstruction String 'Blink your eyes' Bottom card instruction for blink
smileInstruction String 'Smile' Bottom card instruction for smile
turnLeftInstruction String 'Turn your head left' Bottom card instruction for turn left
turnRightInstruction String 'Turn your head right' Bottom card instruction for turn right
positionFaceText String 'Position your face in the oval' Shown when camera is ready and waiting
noFaceText String 'No face detected — center your face' Shown when no face is in frame
holdStillText String 'Hold still…' Shown during countdown
successText String 'Verification complete!' Shown on success
failureText String 'Verification failed' Shown on failure

SamvivaLivenessTextStyles

All fields are optional — unset fields fall back to built-in defaults.

Parameter Type Description
titleStyle TextStyle? Style for the title at the top of the screen
instructionStyle TextStyle? Style for the instruction text in the bottom card
stepLabelStyle TextStyle? Style for the label below each step circle. Color is managed by SamvivaLivenessTheme
countdownStyle TextStyle? Style for the large countdown number. Color is managed by SamvivaLivenessTheme

SamvivaLivenessResult

Field Type Description
completedChallenges List<SamvivaLivenessChallenge> All challenges completed in order
capturedImage Uint8List? JPEG face image at the moment of completion
timestamp DateTime UTC time of completion

SamvivaLivenessFailure

Field Type Description
reason SamvivaLivenessFailureReason Enum: timeout, challengeTimeout, cameraPermissionDenied, cameraError, unknown
message String Human-readable description

SamvivaChallengeUpdate

Field Type Description
challenge SamvivaLivenessChallenge Challenge being evaluated
probability double Raw ML Kit value (0.0–1.0, or degrees for head turn)
threshold double Configured threshold
isPassed bool Whether threshold was crossed

Tuning Thresholds

Use onChallengeUpdate to log raw probabilities during development:

onChallengeUpdate: (update) {
  debugPrint(
    '${update.challenge.name}: '
    'prob=${update.probability.toStringAsFixed(2)} '
    'threshold=${update.threshold}',
  );
},

Sample output:

blink: prob=0.08 threshold=0.3     ← passed (0.08 < 0.3)
smile: prob=0.82 threshold=0.75    ← passed (0.82 > 0.75)
turnLeft: prob=23.4 threshold=20.0 ← passed (23.4° > 20°)

Adjust thresholds based on your real-world data:

  • High-security KYC: blinkEyeClosedThreshold: 0.1, smileThreshold: 0.9, headTurnAngle: 30
  • Standard app login: blinkEyeClosedThreshold: 0.3, smileThreshold: 0.75, headTurnAngle: 20
  • Accessibility-friendly: blinkEyeClosedThreshold: 0.5, smileThreshold: 0.6, headTurnAngle: 15

Powered By


🤝 Contribute with Us

Samviva is open and growing — and I'd love your help to make it the best liveness detection library in the Flutter ecosystem. Whether you're squashing bugs, adding new challenge types, polishing the UI, or improving docs — your contribution is welcome.

Ways to contribute

  • 🐛 Bug reports & feature requests — open an issue on GitHub
  • 🚀 Code contributions — fork, branch, send a PR. Make sure these still pass:
    flutter analyze
    flutter test
    
  • 🎨 Design feedback — share screenshots, mockups, or UX ideas for the overlay and challenge UI
  • 🌐 New challenge types — head nod, mouth open, eye direction, and more are on the roadmap
  • 📣 Spread the word — star the repo, share with your team, write a blog post

First-time contributors are very welcome. Open an issue first to discuss your approach, and let's build it together.


☕ Support this Project

Samviva is built and maintained on personal time. If it saves you hours of integration work, treat me to a coffee — every bit of support helps me keep building, polishing, and shipping new features.

Sponsor on GitHub

Sponsors get a special thank-you in the next release notes. 🙏


License

MIT


Made with 💙 by Samuel Jiwandono — and hopefully you next.

Libraries

samviva
Samviva — a Flutter liveness detection library powered by Google ML Kit.