mml_face_sdk 0.1.1-dev.1
mml_face_sdk: ^0.1.1-dev.1 copied to clipboard
Offline, on-device 1:1 face verification and passive liveness SDK.
example/lib/main.dart
import 'dart:convert';
import 'package:mml_face_sdk/mml_face_sdk.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:image_picker/image_picker.dart';
void main() => runApp(const MaterialApp(home: Demo()));
class Demo extends StatefulWidget {
const Demo({super.key});
@override
State<Demo> createState() => _DemoState();
}
class _DemoState extends State<Demo> {
static const storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
static const publicKeyB64 = String.fromEnvironment('MML_LICENSE_PUBLIC_KEY');
static const activationUrl = String.fromEnvironment('MML_ACTIVATION_URL');
final code = TextEditingController();
final picker = ImagePicker();
MmlFaceSdk? sdk;
FaceTemplate? template;
String status = 'Not initialized';
@override
void initState() {
super.initState();
_start();
}
Future<void> _start() async {
if (publicKeyB64.isEmpty) {
setState(() => status = 'Set MML_LICENSE_PUBLIC_KEY.');
return;
}
sdk = MmlFaceSdk(
publicLicenseKey: SimplePublicKey(
base64Url.decode(base64Url.normalize(publicKeyB64)),
type: KeyPairType.ed25519,
),
);
final token = await storage.read(key: 'license');
final saved = await storage.read(key: 'template');
try {
if (token != null) await sdk!.initialize(license: token);
if (saved != null) template = FaceTemplate.fromJson(jsonDecode(saved));
setState(() => status = token == null ? 'Activation required' : 'Ready');
} catch (_) {
setState(() => status = 'Activation required');
}
}
Future<void> _activate() async {
try {
final token = await sdk!.activate(
endpoint: Uri.parse(activationUrl),
activationCode: code.text.trim(),
);
await storage.write(key: 'license', value: token);
setState(() => status = 'Lifetime licence activated');
} catch (_) {
setState(() => status = 'Activation failed');
}
}
Future<XFile?> _capture() => picker.pickImage(
source: ImageSource.camera,
preferredCameraDevice: CameraDevice.front,
imageQuality: 95,
);
Future<void> _enroll() async {
final file = await _capture();
if (file == null) return;
try {
template = await sdk!.createTemplate(await file.readAsBytes());
await storage.write(key: 'template', value: jsonEncode(template));
setState(() => status = 'Template enrolled locally');
} on FaceSdkException catch (e) {
setState(() => status = e.code.name);
}
}
Future<void> _compare(bool live) async {
if (template == null) {
setState(() => status = 'Enroll first');
return;
}
final file = await _capture();
if (file == null) return;
try {
final r = live
? await sdk!.verify(
encodedImage: await file.readAsBytes(),
template: template!,
)
: await sdk!.recognize(
encodedImage: await file.readAsBytes(),
template: template!,
);
setState(
() => status = live && r.livenessPassed != true
? 'Liveness needs 4 good frames/captures (${r.livenessScore?.toStringAsFixed(3) ?? '-'})'
: '${r.matched ? 'MATCH' : 'NO MATCH'} similarity=${r.similarity?.toStringAsFixed(3)}',
);
} on FaceSdkException catch (e) {
setState(() => status = e.code.name);
}
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('MML Face SDK')),
body: Padding(
padding: const EdgeInsets.all(20),
child: ListView(
children: [
Text(status),
const SizedBox(height: 20),
TextField(
controller: code,
decoration: const InputDecoration(
labelText: 'Customer app licence key',
),
),
FilledButton(onPressed: _activate, child: const Text('Activate')),
const Divider(),
FilledButton(
onPressed: _enroll,
child: const Text('Enroll / replace template'),
),
OutlinedButton(
onPressed: () => _compare(false),
child: const Text('Recognition only'),
),
OutlinedButton(
onPressed: () => _compare(true),
child: const Text('Liveness + verification'),
),
const Text(
'For liveness, capture four consecutive samples. Production apps should use a guided native frame stream.',
),
],
),
),
);
@override
void dispose() {
code.dispose();
sdk?.dispose();
super.dispose();
}
}