samviva 1.0.0
samviva: ^1.0.0 copied to clipboard
A Flutter liveness detection library powered by Google ML Kit. Detect live users with configurable probability thresholds via active face challenges.
samviva #
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.
Blink — blinkEyeClosedThreshold #
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 #
- Google ML Kit Face Detection — on-device face classification and head pose estimation
- camera — Flutter camera access and image streaming
🤝 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.
Sponsors get a special thank-you in the next release notes. 🙏
License #
MIT
Made with 💙 by Samuel Jiwandono — and hopefully you next.