voxa_beauty 0.2.2
voxa_beauty: ^0.2.2 copied to clipboard
Real-time beauty filters for VoxaRTC — skin smoothing, tone and warmth on the camera capture path, entirely on the GPU. No licence, no subscription.
example/lib/main.dart
// Beauty filtering on a live VoxaRTC camera.
//
// Everything here runs on the capture path, before the encoder, so the room
// sees it — this is not a local preview overlay.
//
// Nothing needs a licence key or an account. Smoothing is a shader, and the
// face model for lipstick ships inside the package.
import 'package:flutter/material.dart';
import 'package:voxa_beauty/voxa_beauty.dart';
import 'package:voxa_rtc_engine/voxa_rtc_engine.dart';
/// Your VoxaRTC app id.
const appId = 'YOUR_APP_ID';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MaterialApp(home: BeautyDemo()));
}
class BeautyDemo extends StatefulWidget {
const BeautyDemo({super.key});
@override
State<BeautyDemo> createState() => _BeautyDemoState();
}
class _BeautyDemoState extends State<BeautyDemo> {
// Lazy initialisers rather than assignments after an await. build() runs the
// moment this mounts, long before the engine finishes starting, and the UI
// below reads _beauty — assigning it later would render an error for the
// first seconds. Both constructors are synchronous and cheap.
late final RtcEngine _engine = createAgoraRtcEngine();
late final VoxaBeauty _beauty = VoxaBeauty(_engine);
VideoViewController? _preview;
bool _on = false;
@override
void initState() {
super.initState();
_start();
}
Future<void> _start() async {
await _engine.initialize(const RtcEngineContext(appId: appId));
await _engine.enableVideo();
await _engine.startPreview();
setState(() {
_preview = VideoViewController(
rtcEngine: _engine, canvas: const VideoCanvas(uid: 0));
});
}
Future<void> _toggle() async {
if (_on) {
await _beauty.disable();
} else {
await _beauty.enable();
}
setState(() => _on = _beauty.isEnabled);
}
@override
void dispose() {
// The native filter outlives this engine, so leaving it enabled would keep
// it attached the next time any camera starts.
_beauty.disable();
_engine.release();
super.dispose();
}
@override
Widget build(BuildContext context) {
final preview = _preview;
// Gate on isSupported rather than assuming: a control that always fails is
// worse than no control.
final ready = preview != null && _beauty.isSupported;
return Scaffold(
backgroundColor: Colors.black,
body: Stack(children: [
if (preview != null)
Positioned.fill(child: AgoraVideoView(controller: preview)),
Positioned(
left: 16,
right: 16,
bottom: 40,
child: Column(mainAxisSize: MainAxisSize.min, children: [
if (_on) ...[
_Slider(
label: 'Smoothing',
value: _beauty.smoothing,
onChanged: (v) async {
await _beauty.setSmoothing(v);
setState(() {});
},
),
// Above zero this starts face tracking — the one thing here that
// is not free, which is why it stays off until asked for.
_Slider(
label: 'Lipstick',
value: _beauty.lipstick,
onChanged: (v) async {
await _beauty.setLipstick(v);
setState(() {});
},
),
],
const SizedBox(height: 8),
ElevatedButton(
onPressed: ready ? _toggle : null,
child: Text(_on ? 'Turn beauty off' : 'Turn beauty on'),
),
]),
),
]),
);
}
}
class _Slider extends StatelessWidget {
const _Slider({
required this.label,
required this.value,
required this.onChanged,
});
final String label;
final double value;
final ValueChanged<double> onChanged;
@override
Widget build(BuildContext context) {
return Row(children: [
SizedBox(
width: 90,
child: Text(label, style: const TextStyle(color: Colors.white)),
),
Expanded(child: Slider(value: value, onChanged: onChanged)),
SizedBox(
width: 40,
child: Text(value.toStringAsFixed(2),
style: const TextStyle(color: Colors.white70, fontSize: 12)),
),
]);
}
}