liveness_flutter 0.5.0
liveness_flutter: ^0.5.0 copied to clipboard
Beta: Plug-and-play liveness detection for Flutter. Configure action sequences like blink and smile, capture images or video, and send verification results to your backend.
0.5.0 #
A reliability and honesty release: sessions can no longer hang, onResult
really is called exactly once, the on-screen oval now decides where the
face must be, evidence photos show the action, and the anti-spoof pieces do
what the README says. New hooks bind sessions to your server (challenges,
image hashes, attestation) and let you plug in your own anti-spoof model.
The package is in beta: pre-1.0, so there are breaking changes; each is
listed below. Issues from anyone are welcome at
https://github.com/maykhid/liveness_flutter/issues.
Breaking #
- New
LivenessFailureReason.sessionTimeout. Exhaustiveswitches overLivenessFailureReasonneed a new case. - Sessions now end on their own: by default after 2 minutes overall
(
sessionTimeout) and after 10 s of not returning to a neutral face (neutralTimeout). PasssessionTimeout: nullfor the old unbounded behaviour. LivenessSession(config)throws anArgumentErrorfor an invalid config.multipleFacesfailures now need a second face for more thanmultipleFacesGrace(500 ms). Set it toDuration.zerofor the old instant behaviour.- Action photos are now taken at the action's peak by default, so their
timestampMsis earlier than before. SetcaptureAtPeak: falsefor the old completion-frame photos. CustomLivenessEventswitches need anActionPeakEventcase. - New
FaceGuidance.tooBrightvalue. Exhaustiveswitches overFaceGuidanceneed a new case. onResultcan now run right afterdispose(), when the widget's context is unmounted. Code that navigates inonResultmust checkcontext.mountedfirst (the README example now does).HttpLivenessUploader'smetadataJSON is nowresult.toJson(): the diagnostics (blink_ms,confidence_*,flashChallenge, …) moved from the top level into a nestedmetadataobject, and image file names changed from<action>_<index>.jpgto<action>_<kind>_<t>ms.jpg.HttpLivenessUploader.upload()now throws on non-2xx responses and on timeouts (60 s per attempt by default). Wrap it intry/catch.- The face-in-position test now follows the drawn target. Faces that
passed the old loose centre check may now get
notCentered,tooFarortooClose; tuneovalSizeFactorortargetFillMin/targetFillMax. - The default
LivenessStrings.centeringFaceis now "Fit your face in the oval" (it sits above a specific hint now), andFaceGuidance.multipleFaceswas removed from the defaultguidanceMessagesmap in favour ofLivenessStrings.multipleFaces. LivenessStrings.actionInstructions/guidanceMessagesare now getters returning your entries merged over the defaults (the constructor parameters are unchanged). The failed screen shows the reason-specific text fromfailureMessagesinstead offailed.- New
LivenessFailureReason.challengeExpired.LivenessConfig.actionsmay be empty when achallengeis set. FlashChallenge.phaseis now read-only; usebeginPhase(phase, timestampMs). The flash challenge is stricter, so borderline real faces that passed before may now befailed(it still only lowers the confidence score).- New
LivenessFailureReason.faceChanged. ML Kit face tracking is now enabled when no action needs contours. - New
LivenessFailureReason.permissionDenied. A denied camera used to reportsystemError.
Added #
LivenessConfig.sessionTimeout(default 2 min, nullable) andLivenessConfig.neutralTimeout(default 10 s).LivenessSession.tick(timestampMs): advances timers without a frame. The widget calls it every 250 ms.LivenessConfig.validate()throws anArgumentErrorfor emptyactions,jpegQualityoutside 1–100,maxImageDimension< 64,brightnessMin≥brightnessMax, and non-positive timeouts.LivenessSessioncalls it in its constructor; the constructor also asserts the numeric ranges.LivenessConfig.multipleFacesGrace(default 500 ms),DetectorTuning.secondaryFaceMinAreaRatio(default 0.35),LivenessSession.relevantFaces()andFaceSnapshot.area.DetectorUpdate.isPeak,ActionPeakEvent,CapturedImage.kind(reference|peak|completion|sequence) andLivenessConfig.captureAtPeak(defaulttrue).LivenessSessionState.copyWithgainsclearCurrentAction,clearFailureReasonandclearRemaining.LivenessConfig.invertPitch(defaultfalse): flips up/down head tilt for devices wherelookUp,lookDownornodbehave inverted, likemirrorYawdoes for left/right.FaceGuidance.tooBrightwith a default message, andFrameQuality.issueFor(config).metadata['cancelledBy']on cancelled results:'user','lifecycle','dispose'or'restart'.HttpLivenessUploader.client: inject anhttp.Client(your own, or aMockClientin tests).HttpLivenessUploader.timeout(default 60 s per attempt),maxRetries(default 0) andretryDelay(default 1 s, doubling): network errors, timeouts and 5xx responses are retried; 4xx never are.LivenessUploadException.LivenessController(LivenessDetector.controller, optional): readstate,actionPlanandsessionId, and callcancel()orrestart()from outside the widget.restart()starts a fresh session (new session ID, new shuffle, new camera) and still delivers exactly one result per session (cancelledBy: 'restart'for an interrupted one).LivenessSessionState.actionPlan(the executed order, from the first state),actionTimeoutandsessionRemaining.tick()keepsremainingandsessionRemainingcounting down when frames stall.LivenessTheme.ovalCenter(defaultOffset(0.5, 0.44)),ovalAspectRatio(default 1.35) andovalShape(TargetShape.oval|roundedRect|circle);LivenessDetector.targetRegionso a custom overlay's window drives detection;DetectorTuning.targetFillMin(0.15) andtargetFillMax(1.0). The debug overlay draws the detected face box in screen space.LivenessDetector.instructionAlignmentandcloseButtonBuilder;LivenessTheme.closeIconColor,closeButtonAlignment,flashTintOpacityandresultHoldDuration;LivenessStrings.close(close-button tooltip and screen-reader label).LivenessStrings.stepCounter(String Function(int current, int total)),failureMessages(perLivenessFailureReason, shown on the failed screen),failureFor(), thedefault*maps, andcopyWithonLivenessThemeandLivenessStrings.LivenessDetector.onFeedbackwithLivenessFeedbackevents (actionStarted,actionProgressHalf,actionCompleted,sessionSucceeded,sessionFailed) for sounds, haptics or TTS;LivenessConfig.hapticFeedback(opt-in); the instruction text is now a screen-reader live region, so each new instruction is announced.ResolutionPreset(forLivenessDetector.cameraResolution) is re-exported, so you no longer need to depend onpackage:camerato set it.- Server-bound sessions:
LivenessConfig.challenge(LivenessChallengewith nonce, ordered actions and expiry; runs that order with no shuffle and refuses an expired one),LivenessResult.nonce, per-image and per-frame SHA-256 intoJson()(images/frameslists,CapturedImage.sha256Hex), and an optionalLivenessAttestor(LivenessConfig.attestor) that signsLivenessResult.attestationPayloadintoLivenessResult.attestation. New guide:doc/server_verification.md. New dependency:crypto. LivenessConfig.randomActionCount: pick N actions at random fromactions(a pool) and shuffle them, per session. Docs now recommend at least one motion action, since pose-only actions can be satisfied by a photo.LivenessConfig.flashAllowedMisses(default 0);FlashChallengegainsbeginPhase(),allowedMisses,k,settle,minDeltaandsampleRgb(region:).- Identity continuity (
IdentityGuard): ML Kit tracking is now on whenever no action needs contours; a tracking-ID change while a face stays in view lowers the confidence score and is reported inmetadata['identity_*'], as are jumps in a rough face-geometry signature (FaceSnapshot.identitySignature).LivenessConfig.failOnFaceChange(opt-in) fails such sessions with the newLivenessFailureReason.faceChanged. - Presentation-attack detection hook:
LivenessConfig.frameAnalyzers(LivenessFrameAnalyzerreturning a 0–1 spoof probability perLivenessFrame: upright JPEG plus face box) andanalyzerWeight. Runs on the reference and evidence frames even when photos aren't captured; results go tometadata['analyzers']and into the confidence score. No model is bundled. - Near-duplicate detection in the static-feed guard: frames that differ by
less than sensor noise (
FrameQuality.lumaSamples) while the face box is frozen lower the confidence score (metadata['confidence_nearDuplicate*']). Soft signal only. LivenessConfig.mlInterval(default 100 ms) andmlIntervalBlink(default 50 ms);DetectorTuning.blinkPartialCloseThreshold.LivenessFailureReason.permissionDenied(fromCameraAccessDenied,CameraAccessDeniedWithoutPrompt,CameraAccessRestrictedand legacycameraPermissionerrors) with its own failure text,LivenessSession.permissionDenied(), andLivenessDetector.permissionDeniedBuilder(with aretrycallback).- GitHub Actions CI:
flutter analyzeandflutter testfor the package and the example, on pushes tomainand on pull requests. Widget tests driveLivenessDetectorthrough a fake camera, and the camera lifecycle is tested against a fake camera plugin (241 package tests, 38 example tests). - Reworked examples: a minimal
lib/main.dart(about 90 lines), focused recipes (custom UI, controller, server-bound session with upload), a fully branded KYC flow for a fictional fintech, and a separate test bench exposing every option, with guided device checks (pass / fail and a copyable report), with a result page that runs the server checks fromdoc/server_verification.mdagainst clearly labelled in-app fakes. DetectorTuning.maxFrameGap(default 250 ms): the most one gap between analysed frames can count toward a hold.
Fixed #
- Sessions could hang forever while searching for or centering the face,
while waiting for a neutral face, or while paused for bad lighting. The
neutral case fails with
actionTimeoutandmetadata['timeoutPhase'] = 'awaitingNeutral'. The action timer now keeps running during a quality pause. LivenessConfig(actions: [])threw aRangeErroron every frame and never ended. An invalid config now produces one immediatesystemErrorresult (withmetadata['configError']) and anonErrorcall, without opening the camera.- A single frame with a second face (a poster, a TV, someone walking past)
failed the session instantly. Secondary faces under 35 % of the primary
face's area are now ignored, and a comparable second face must stay for
more than 500 ms before the session fails. Until then the session pauses
with
FaceGuidance.multipleFaces. The primary face is now the largest one, not the first one ML Kit returns. - Evidence photos missed the action: they were taken after it completed (the blink photo showed open eyes, the nod photo a level head) and from the newest camera frame rather than the analysed one. Each action's photo now comes from its peak frame (eyes shut, deepest nod, start of a held pose).
- A single missed face detection (or one blurry or dark frame) reset the
current action, restarting a 400 ms pose hold. The detector now keeps
its progress through short gaps, but holds only count time it actually
saw: each gap between analysed frames adds at most
DetectorTuning.maxFrameGap(250 ms), so a lost face or a stalled camera can't complete a pose on two frames. A quality pause longer thanfaceLostGracerestarts the action. - Dark or blurry frames reset the face-lost and multiple-faces timers, so
alternating bad frames could stop
faceLostormultipleFacesfrom ever firing. Quality-held frames now leave those timers alone. state.remainingkept a stale countdown after an action completed. It is now only set while an action is being performed.- Overexposed frames told the user to "Find better lighting"
(
FaceGuidance.lowLight). They now reportFaceGuidance.tooBright. shuffleActionsused a non-cryptographicRandom(); it now usesRandom.secure(), since the shuffle is an anti-replay measure.google_mlkit_face_detectionnow allows>=0.14.0 <0.16.0. 0.15.x needs Flutter 3.44 / Dart 3.12; older SDKs keep resolving to 0.14.- Raising the screen brightness no longer delays camera start: the platform call now runs in the background.
onResultwas never called when the detector was removed before the session ended (route popped, system back), despite the "called exactly once" promise. It is now built indispose()and delivered on the next microtask (soonResultmay callsetState) as acancelledresult, or with the real outcome if the session had already ended. A throwingonResultthere goes toonError.HttpLivenessUploaderleft outsessionIdandconfidenceScore, the two audit fields the README promotes. Themetadatafield is nowresult.toJson(), and the request carries anX-Liveness-Sessionheader. Image file names now include the capture kind and timestamp (blink_peak_812ms.jpg,reference_120ms.jpg) instead of a list index.HttpLivenessUploader.upload()completed normally on any HTTP status, including 500. It now throwsLivenessUploadException(statusCode, body)for non-2xx responses (afteronResponsehas seen them).- The on-screen oval had no effect on detection: the face-position check
used the image centre ±25 % and a 4–75 % face area, whatever
ovalSizeFactorsaid. The drawn target is now mapped into camera space (cover-fit, sensor rotation, front-camera mirror) and the face must have its centre inside it and fill it withintargetFillMin–targetFillMax. LivenessTheme.hintStyleandLivenessStrings.multipleFaceswere declared but never used. Guidance hints ("Move closer", "Find better lighting") are now shown inhintStyleunder the current instruction instead of replacing it, andmultipleFacesis the multiple-faces hint unlessguidanceMessageshas its own entry. The close icon was always white (invisible on light scrims).- "Step N of M" was hard-coded English; the failed screen always said
"Verification failed" whatever the reason; and a partial
actionInstructionsorguidanceMessagesmap made every missing entry fall back to raw enum names likelookLeft. User maps are now merged over the defaults. - The failure confidence score divided progress by
config.actions.lengthrather than the number of actions actually planned for the session. - The colour-flash challenge was easy to pass by chance: it sampled the centre of the whole frame (including background), counted the moments right after each colour switch, allowed one wrong colour, and used a fixed 0.004 threshold. It now samples inside the face box, skips the first 150 ms of each colour, requires every colour by default, and requires each rise to beat 3 standard errors of the measured baseline noise against the baseline and the other colours. Auto-exposure is locked during the flash where supported. In simulation, noise-only passes dropped from ~5 % to 0 in 5,000 runs.
- Frame-sequence capture paused during the flash challenge, contradicting the README's "the flash moment is captured" claim. It now continues.
- Docs overstated the "replay guard": it only catches pixel-identical frames. It's now described as a static-feed guard, and the README says plainly that it can't detect a photo or screen held up to a real camera.
- Fast blinks were often missed: ML ran at ~10 fps, so a sub-100 ms eye
closure could fall between analysed frames. During
blinkandeyesClosedit now runs at ~20 fps (mlIntervalBlink), and a half-shut reading between open frames counts as the closed part of a blink (DetectorTuning.blinkPartialCloseThreshold, default 0.4). In simulation a 60 ms closure is now caught in ≥ 95 % of runs. - The example README said the platform folders weren't checked in (they
are; only the pub.dev package omits them), listed
minSdkVersion 21(the package needs 24), and described outdated upload fields. - The declared SDK constraints (Dart 3.4 / Flutter 3.22) were lower than
the dependencies allow:
camera0.12 needs Dart 3.10 / Flutter 3.38, and the code already used Flutter 3.27 APIs.pubspec.yamlnow says so. - The camera preview was scaled to the whole screen rather than the space the detector was given, and wasn't clipped. With the detector under an app bar, the drawn target and the detection zone pointed at different parts of the face. The preview now cover-fits its own space and is clipped, matching the detection maths.
- A restart (or quickly opening a new liveness screen) could open the new camera while the old one was still closing ("camera in use" on Android). Each camera now waits for the previous one's release (at most 2 s, so a hanging release can't block later sessions).
- Closing the screen while the camera was still starting could leave the camera (and torch) on. Startup now stops as soon as the screen is gone, and the camera is always released.
- Leaving during the colour-flash challenge delivered
success: truewithout the anti-replay check ever being evaluated (and touched disposed state). It is now acancelledresult withmetadata['flashChallenge'] = 'interrupted'. - A cancel while the camera was still starting was undone: the session came
back to
searchingFaceand the camera kept streaming. The session now stays ended and the late-opened camera is stopped. failOnMultipleFaces: falsestill blocked the user while a second face was in view (the face never counted as in position). The largest face is now used and the others are ignored, as documented.- With an attestor, a photo that finished encoding during attestation ended up in the delivered result but not in the signed payload, so server-side verification failed for a genuine user. The delivered result is now exactly the one that was attested.
0.4.4 #
- Updated README.md
0.4.3 #
- Updated dependencies and minor improvements.
0.4.2 #
- Documentation: restyled README for pub.dev (no code changes).
0.4.1 #
- Fixed captured photos coming out landscape on pipelines that deliver already-upright buffers (notably iOS): quarter-turn rotations are now only applied to landscape buffers.
- Much faster frame encoding: decoding and downscaling are fused, so full-resolution intermediates are never materialized (~4x less work at 1080p→720). Frame-sequence capture no longer starves on slower devices or debug builds.
HttpLivenessUploader.onProgress(sentBytes, totalBytes): drive an upload progress bar. Liveness payloads can be several MB — the example app now demonstrates a progress dialog, and the README documents the recommended loading-UX patterns.
0.4.0 #
Initial public release.
Liveness detection #
- 13 challenge actions, executed in the order you list them:
blink,smile,fullTeethSmile,nod,lookLeft,lookRight,lookUp,lookDown,tiltLeft,tiltRight,eyesClosed,openMouth, and experimentaldrawCircleWithNose. shuffleActionsrandomizes the order per session (anti-replay); the executed order is reported inLivenessResult.completedActions.- Every detection threshold is tunable via
DetectorTuning; detectors are pure-Dart state machines with unit tests. - Session flow: face search/centering, per-action timeouts, neutral-face reset between actions, single-face enforcement, face-lost grace period.
Anti-spoof & quality (no ML models, no downloads) #
- Replay guard: long runs of pixel-identical frames fail the session with
spoofSuspected(enableReplayGuard). - Micro-motion check: unnaturally still sessions lower the confidence score (soft signal only).
- Frame quality gates: too-dark / overexposed / blurry frames pause the
session with user guidance instead of failing (
enableQualityChecks,brightnessMin/Max,sharpnessMin). - Opt-in color-flash challenge (
enableFlashChallenge): the screen flashes randomly ordered colors and verifies the face reflects them — counters video replays on a second screen. Soft signal; result inmetadata['flashChallenge']. confidenceScore(0–1) on every result, with raw penalty counters inmetadataunderconfidence_*; securely randomsessionIdfor audit trails.
Capture & delivery (backend-agnostic) #
- Capture any mix of: per-action JPEG images, native session video, or a
steady frame sequence (
CaptureType.frameSequence) that works on every device — with fps, dimension, and JPEG-quality controls. - All JPEG encoding runs in background isolates; per-action captures have a synchronous fallback so no verification image is ever lost.
onResultdelivers everything; optionalLivenessUploader.custom(fn)wraps any transport, andHttpLivenessUploadercovers multipart POST.LivenessResult.toString()for readable logs andtoJson()for JSON-safe summaries.LivenessCapabilities.supportsVideoCapture()probes whether a device can record while detecting; sessions self-heal to a plain stream when it can't (metadata['videoUnavailable']).- Optional
autoDeleteVideocleanup for the recorded file.
Modes & UX #
- Assisted mode (
LivenessCameraMode.assisted): operator points the back camera at the subject and relays instructions; torch lights the face (assistedTorchEnabled), left/right signs flip automatically, flash challenge is skipped, andmetadata['cameraMode']records the mode. - Screen auto-brightens during the session and restores afterward
(
boostScreenBrightness). FaceGuidancestate (tooFar,tooClose,notCentered,lowLight,blurry,multipleFaces,noFace) with translatable messages.- Full theming via
LivenessTheme+LivenessStrings(all text localizable), or replace UI layers entirely withoverlayBuilder/instructionBuilder. showDebugOverlayshows live angles, probabilities, brightness, and replay-guard counters for threshold tuning.- Per-action callbacks (
onActionStarted/onActionCompleted), sync or async.