utd_video_effects_kit 0.9.0 copy "utd_video_effects_kit: ^0.9.0" to clipboard
utd_video_effects_kit: ^0.9.0 copied to clipboard

Real-time video filters and beauty effects for LiveKit Flutter apps — LUT color grading, skin retouch, makeup, accessories, and background blur as a livekit_client TrackProcessor.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:utd_video_effects_kit/utd_video_effects_kit.dart';

import 'diagnostics_page.dart';

/// Demo: open the camera with a [VideoEffectsProcessor] attached and drive the
/// whole effect surface through the kit's drop-in [VideoEffectsSheet] (beauty
/// sliders, LUT filters, makeup looks, accessories, 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;
  // Camera acquisition failure (permission denied / camera busy). Rendered with
  // a retry button instead of stranding the page on the spinner forever.
  String? _startError;
  // Live tracking-telemetry chip (long-press the preview to toggle): surfaces
  // the numbers a lag report needs — which tracking path the device is on
  // (sync/async, delegate, detect input px), the landmark age at draw, and the
  // render p95 — without digging through the Diagnostics screens.
  bool _telemetryOn = false;
  Timer? _telemetryTimer;
  String _telemetry = '';

  @override
  void initState() {
    super.initState();
    _start();
  }

  Future<void> _start() async {
    // This page is the reference integration: without the try/catch a camera
    // permission denial would surface as an unhandled zone error and leave the
    // spinner up forever.
    try {
      final track = await LocalVideoTrack.createCameraTrack(
        CameraCaptureOptions(processor: _fx),
      );
      await _fx.setEnabled(true);
      // Device-validation default: light the DEBUG_LATENCY logcat probes from
      // startup so every plain run captures the landmarker/pipeline latency
      // story — without this, the lines only appear after someone remembers
      // to open telemetry/Diagnostics (session must be attached; it is, since
      // createCameraTrack attaches before setEnabled resolves).
      await _fx.setDebugFlags({'debugLatency': true});
      if (mounted) setState(() => _track = track);
    } catch (e) {
      if (mounted) setState(() => _startError = '$e');
    }
  }

  Future<void> _toggleTelemetry() async {
    if (_telemetryOn) {
      _telemetryTimer?.cancel();
      setState(() {
        _telemetryOn = false;
        _telemetry = '';
      });
      // Deliberately leave debugLatency ON: the logcat probes are the device
      // A/B record and must survive toggling the on-screen overlay off.
      return;
    }
    setState(() => _telemetryOn = true);
    await _fx.setDebugFlags({'debugLatency': true});
    await _fx.perfStats(reset: true);
    _telemetryTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
      final info = await _fx.debugInfo();
      final perf = await _fx.perfStats();
      if (!mounted || !_telemetryOn) return;
      String n(Object? v, [int digits = 1]) =>
          v is num ? v.toStringAsFixed(digits) : '—';
      setState(() {
        _telemetry = 'track ${info?['syncActive'] == true ? 'SYNC' : 'async'}'
            '${info?['syncFellBack'] == true ? ' (fell back)' : ''}'
            '  ${info?['delegate'] ?? '—'}'
            '  in ${info?['syncDetectLong'] ?? '—'}px\n'
            'inferMs ${n(info?['syncInferMs'])}'
            '  ageMs ${n(info?['landmarkAgeMs'])}'
            '  asyncLatMs ${n(info?['avgLatencyMs'])}\n'
            'p95 ${n(perf?.p95Ms)}ms  level ${_topLevel(perf)}';
      });
    });
  }

  static String _topLevel(EffectsPerfStats? p) {
    if (p == null) return '—';
    var top = 0;
    var topFrames = -1;
    for (var i = 0; i < p.levelFrames.length; i++) {
      if (p.levelFrames[i] > topFrames) {
        topFrames = p.levelFrames[i];
        top = i;
      }
    }
    return 'L$top';
  }

  @override
  void dispose() {
    _telemetryTimer?.cancel();
    // Let LiveKit's async stop (which calls processor.destroy() mid-way) finish
    // BEFORE releasing the processor's notifiers — the reverse order would race
    // destroy() against dispose() on every teardown.
    final stopped = _track?.stop();
    if (stopped != null) {
      stopped.whenComplete(_fx.dispose);
    } else {
      _fx.dispose();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final track = _track;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Video Effects Demo'),
        actions: [
          IconButton(
            tooltip: 'Tracking telemetry',
            icon: Icon(_telemetryOn ? Icons.speed : Icons.speed_outlined),
            onPressed: _toggleTelemetry,
          ),
          IconButton(
            tooltip: 'Diagnostics',
            icon: const Icon(Icons.troubleshoot),
            onPressed: () => Navigator.of(context).push(
              MaterialPageRoute<void>(
                builder: (_) => DiagnosticsPage(processor: _fx, track: _track),
              ),
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton.extended(
        icon: const Icon(Icons.auto_fix_high),
        label: const Text('Effects'),
        onPressed: () => VideoEffectsSheet.show(context, processor: _fx),
      ),
      body: Column(
        children: [
          Expanded(
            child: Stack(
              children: [
                Positioned.fill(
                  child: track == null
                      ? Center(
                          child: _startError == null
                              ? const CircularProgressIndicator()
                              : Column(
                                  mainAxisSize: MainAxisSize.min,
                                  children: [
                                    Padding(
                                      padding: const EdgeInsets.all(12),
                                      child: Text(
                                        'Camera unavailable: $_startError',
                                        style:
                                            const TextStyle(color: Colors.orange),
                                        textAlign: TextAlign.center,
                                      ),
                                    ),
                                    TextButton(
                                      onPressed: () {
                                        setState(() => _startError = null);
                                        _start();
                                      },
                                      child: const Text('Retry'),
                                    ),
                                  ],
                                ),
                        )
                      : VideoTrackRenderer(track,
                          mirrorMode: VideoViewMirrorMode.mirror),
                ),
                if (_telemetryOn && _telemetry.isNotEmpty)
                  Positioned(
                    left: 8,
                    top: 8,
                    child: Container(
                      padding: const EdgeInsets.symmetric(
                          horizontal: 8, vertical: 6),
                      decoration: BoxDecoration(
                        color: Colors.black.withValues(alpha: 0.55),
                        borderRadius: BorderRadius.circular(6),
                      ),
                      child: Text(
                        _telemetry,
                        style: const TextStyle(
                          color: Colors.greenAccent,
                          fontSize: 11,
                          fontFamily: 'monospace',
                        ),
                      ),
                    ),
                  ),
              ],
            ),
          ),
          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,
              ),
            ),
        ],
      ),
    );
  }
}
0
likes
130
points
193
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Real-time video filters and beauty effects for LiveKit Flutter apps — LUT color grading, skin retouch, makeup, accessories, and background blur as a livekit_client TrackProcessor.

Topics

#livekit #video #webrtc #filters #beauty

License

MIT (license)

Dependencies

flutter, flutter_webrtc, livekit_client

More

Packages that depend on utd_video_effects_kit

Packages that implement utd_video_effects_kit