utd_video_effects_kit 0.2.0
utd_video_effects_kit: ^0.2.0 copied to clipboard
Real-time video filters and beauty effects (LUT color grading, skin smoothing, whitening, background blur, plus MediaPipe face effects — eye color, makeup, accessories) for LiveKit Flutter apps, as a [...]
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:utd_video_effects_kit/utd_video_effects_kit.dart';
/// Demo: open the camera with a [VideoEffectsProcessor] attached and drive every
/// effect — color/beauty sliders, a LUT picker, makeup color pickers, accessory
/// toggles, and background blur.
///
/// Run on a REAL DEVICE — simulators/emulators hit the I420/CPU fallback and the
/// MediaPipe face pass won't be representative. Platform runners (android/ ios/)
/// are generated with `flutter create .` in this directory.
void main() => runApp(const _EffectsDemoApp());
class _EffectsDemoApp extends StatelessWidget {
const _EffectsDemoApp();
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: _EffectsDemoPage(),
);
}
}
class _EffectsDemoPage extends StatefulWidget {
const _EffectsDemoPage();
@override
State<_EffectsDemoPage> createState() => _EffectsDemoPageState();
}
class _EffectsDemoPageState extends State<_EffectsDemoPage> {
final VideoEffectsProcessor _fx = VideoEffectsProcessor.create();
LocalVideoTrack? _track;
// Small palettes for the makeup / eye-color pickers.
static const _eyePalette = ['#3366CC', '#2E8B57', '#7B3FA0', '#8C6239'];
static const _lipPalette = ['#CC2244', '#B5179E', '#E0584F', '#7A1F3D'];
static const _shadowPalette = ['#8844AA', '#C06C84', '#355C7D', '#6C5B7B'];
static const _blushPalette = ['#E87070', '#F4A6A6', '#D98880', '#E59866'];
@override
void initState() {
super.initState();
_start();
}
Future<void> _start() async {
final track = await LocalVideoTrack.createCameraTrack(
CameraCaptureOptions(processor: _fx),
);
await _fx.setEnabled(true);
if (mounted) setState(() => _track = track);
}
@override
void dispose() {
_track?.stop();
_fx.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final track = _track;
return Scaffold(
appBar: AppBar(title: const Text('Video Effects Demo')),
body: Column(
children: [
Expanded(
flex: 3,
child: track == null
? const Center(child: CircularProgressIndicator())
: VideoTrackRenderer(track,
mirrorMode: VideoViewMirrorMode.mirror),
),
if (!_fx.isSupported)
const Padding(
padding: EdgeInsets.all(8),
child: Text(
'Native effects pipeline not available — running passthrough.',
style: TextStyle(color: Colors.orange),
textAlign: TextAlign.center,
),
),
Expanded(
flex: 4,
child: ValueListenableBuilder<EffectsState>(
valueListenable: _fx.state,
builder: (_, s, __) => ListView(
padding: const EdgeInsets.all(12),
children: [
_section('Beauty'),
_slider('Smoothing', s.smoothing, _fx.setSmoothing),
_slider('Whitening', s.whitening, _fx.setWhitening),
_slider('Background blur', s.backgroundBlur,
_fx.setBackgroundBlur),
const Divider(),
_section('LUT filter'),
_filterPicker(s.filterKey),
const Divider(),
_section('Eye color'),
_colorPicker(_eyePalette, s.eyeColor,
(c) => _fx.setEyeColor(c, opacity: s.eyeColorOpacity)),
_section('Lipstick'),
_colorPicker(_lipPalette, s.lipstick,
(c) => _fx.setLipstick(c, opacity: s.lipstickOpacity)),
_section('Eyeshadow'),
_colorPicker(_shadowPalette, s.eyeshadow,
(c) => _fx.setEyeshadow(c, opacity: s.eyeshadowOpacity)),
_section('Blush'),
_colorPicker(_blushPalette, s.blush,
(c) => _fx.setBlush(c, opacity: s.blushOpacity)),
const Divider(),
_section('Accessories'),
Wrap(
spacing: 8,
children: [
FilterChip(
label: const Text('Glasses'),
selected: s.glasses,
onSelected: _fx.setGlasses,
),
FilterChip(
label: const Text('Crown'),
selected: s.crown,
onSelected: _fx.setCrown,
),
FilterChip(
label: const Text('Cat ears'),
selected: s.catEars,
onSelected: _fx.setCatEars,
),
],
),
],
),
),
),
],
),
);
}
Widget _section(String label) => Padding(
padding: const EdgeInsets.only(top: 4, bottom: 4),
child: Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
);
Widget _slider(String label, double value, ValueChanged<double> onChanged) {
return Row(
children: [
SizedBox(width: 130, child: Text(label)),
Expanded(child: Slider(value: value, onChanged: onChanged)),
],
);
}
/// A row of color swatches. Tapping a swatch sets that hex; tapping the active
/// one again clears the effect (passes null).
Widget _colorPicker(
List<String> palette, String? active, ValueChanged<String?> onChanged) {
return Row(
children: [
for (final hex in palette)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
child: GestureDetector(
onTap: () => onChanged(active == hex ? null : hex),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: _hexToColor(hex),
shape: BoxShape.circle,
border: Border.all(
color: active == hex ? Colors.white : Colors.black26,
width: active == hex ? 3 : 1,
),
),
),
),
),
TextButton(onPressed: () => onChanged(null), child: const Text('Off')),
],
);
}
Widget _filterPicker(String? active) {
return SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
for (final f in VideoEffectsFilters.all)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ChoiceChip(
label: Text(f.label),
selected: active == f.key,
onSelected: (sel) => _fx.setFilter(sel ? f.key : null),
),
),
],
),
);
}
Color _hexToColor(String hex) {
final clean = hex.replaceAll('#', '');
final v = int.parse(clean.length == 6 ? 'FF$clean' : clean, radix: 16);
return Color(v);
}
}