Skip to main content

gravix_effects_webrtc 1.1.2 copy "gravix_effects_webrtc: ^1.1.2" to clipboard
gravix_effects_webrtc: ^1.1.2 copied to clipboard

PlatformAndroid

Gravix Effects output for flutter_webrtc / LiveKit — publishes the running GravixCamera as a WebRTC video track.

example/lib/main.dart

// Gravix Effects + LiveKit — the camera with effects, looks, the ready-made beauty panel, and Go live / End live to a
// LiveKit room with the Gravix picture + the microphone.
//
//   flutter run --dart-define=LK_URL=wss://<your livekit host> --dart-define=LK_TOKEN=<a join token for a room>
//
// Any flutter_webrtc stack works the same way: GravixWebRtc.attach(camera) gives a normal MediaStream + video track.
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:gravix_effects_webrtc/beauty_ui.dart';
import 'package:gravix_effects_webrtc/gravix_effects_webrtc.dart';
import 'package:livekit_client/livekit_client.dart' as lk;
import 'package:permission_handler/permission_handler.dart';

const _liveKitUrl = String.fromEnvironment('LK_URL');
const _liveKitToken = String.fromEnvironment('LK_TOKEN');

void main() => runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: LiveScreen()));

class LiveScreen extends StatefulWidget {
  const LiveScreen({super.key});
  @override
  State<LiveScreen> createState() => _LiveScreenState();
}

class _LiveScreenState extends State<LiveScreen> {
  final _beauty = GravixBeautyController.shared; // remembers the chosen look + sliders across camera restarts
  GravixCamera? _cam;
  GravixWebRtcOutput? _out; // the camera's picture as a flutter_webrtc track
  lk.Room? _room;
  List<GravixLook> _looks = const [];
  String? _error;
  bool _busy = false;

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

  Future<void> _start() async {
    if (!GravixCamera.isSupported) return setState(() => _error = 'Gravix Effects runs on Android only.');
    try {
      unawaited(GravixCamera.preload()); // loads the face model while the permission dialog is open
      if (!await GravixCamera.requestPermission()) return setState(() => _error = 'Camera permission is needed.');
      // streaming: false — no frames go to WebRTC until Go live (saves the per-frame stream copy on the preview screen).
      final cam = await GravixCamera.start(lens: GravixLens.front, fps: 30, streaming: false);
      await _beauty.attach(cam);
      final out = await GravixWebRtc.attach(cam);
      final looks = [for (final name in await cam.builtinLooks()) await GravixLooks.builtin(name)];
      if (!mounted) {
        await cam.stop();
        return;
      }
      setState(() {
        _cam = cam;
        _out = out;
        _looks = looks;
      });
    } on GravixException catch (e) {
      setState(() => _error = 'Camera error ${e.code}: ${e.message ?? ''}');
    }
  }

  Future<void> _goLive() async {
    final cam = _cam, out = _out;
    if (cam == null || out == null) return;
    if (_liveKitUrl.isEmpty || _liveKitToken.isEmpty) {
      return _toast('Run with --dart-define=LK_URL=… --dart-define=LK_TOKEN=…');
    }
    if (!(await Permission.microphone.request()).isGranted) return _toast('Microphone permission is needed.');
    setState(() => _busy = true);
    try {
      final room = lk.Room(roomOptions: const lk.RoomOptions(adaptiveStream: false, dynacast: true));
      await room.connect(_liveKitUrl, _liveKitToken);
      await cam.setStreaming(true);
      // The Gravix track, published like a camera track. The stream is sent un-mirrored (as the camera sees it);
      // viewers mirror a front-camera host themselves, or start the camera with mirrorStream: true.
      // ignore: invalid_use_of_internal_member
      final track = lk.LocalVideoTrack(lk.TrackSource.camera, out.stream, out.videoTrack, const lk.CameraCaptureOptions());
      await room.localParticipant!.publishVideoTrack(track,
          publishOptions: const lk.VideoPublishOptions(
            videoCodec: 'vp8',
            simulcast: true,
            videoEncoding: lk.VideoEncoding(maxBitrate: 1200000, maxFramerate: 30),
          ));
      await room.localParticipant!.setMicrophoneEnabled(true);
      setState(() => _room = room);
    } catch (e) {
      await cam.setStreaming(false);
      _toast('Could not go live: $e');
    } finally {
      if (mounted) setState(() => _busy = false);
    }
  }

  Future<void> _endLive() async {
    final cam = _cam;
    setState(() => _busy = true);
    await _room?.disconnect();
    // LiveKit disposes a published track on unpublish: give the next Go live a fresh one.
    await _out?.detach();
    if (cam != null) {
      await cam.setStreaming(false);
      _out = await GravixWebRtc.attach(cam);
    }
    if (mounted) {
      setState(() {
        _room = null;
        _busy = false;
      });
    }
  }

  void _toast(String text) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text)));

  @override
  void dispose() {
    _room?.disconnect();
    final cam = _cam;
    if (cam != null) {
      _beauty.detach(cam);
      cam.stop(); // also disposes the output's track + stream
    }
    super.dispose();
  }

  void _openBeautyPanel() => showModalBottomSheet(
        context: context,
        backgroundColor: Colors.transparent,
        barrierColor: Colors.transparent,
        builder: (_) => GravixBeautyPanel(controller: _beauty),
      );

  @override
  Widget build(BuildContext context) {
    final cam = _cam;
    final live = _room != null;
    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(fit: StackFit.expand, children: [
        if (cam != null)
          GravixPreview(cam) // mirrored for the front camera, fills the screen
        else
          Center(
            child: _error == null
                ? const CircularProgressIndicator()
                : Text(_error!, style: const TextStyle(color: Colors.white), textAlign: TextAlign.center),
          ),
        if (live)
          const SafeArea(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Chip(label: Text('LIVE'), backgroundColor: Colors.red, labelStyle: TextStyle(color: Colors.white)),
            ),
          ),
        if (cam != null)
          Align(
            alignment: Alignment.bottomCenter,
            child: SafeArea(
              child: Column(mainAxisSize: MainAxisSize.min, children: [
                LooksRow(looks: _looks, controller: _beauty),
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
                    FilledButton.tonalIcon(
                        onPressed: _openBeautyPanel, icon: const Icon(Icons.face_retouching_natural), label: const Text('Beauty')),
                    FilledButton.tonalIcon(onPressed: cam.switchCamera, icon: const Icon(Icons.cameraswitch), label: const Text('Flip')),
                    FilledButton.icon(
                      onPressed: _busy ? null : (live ? _endLive : _goLive),
                      style: FilledButton.styleFrom(backgroundColor: live ? Colors.grey : Colors.red),
                      icon: Icon(live ? Icons.stop : Icons.videocam),
                      label: Text(live ? 'End live' : 'Go live'),
                    ),
                  ]),
                ),
              ]),
            ),
          ),
      ]),
    );
  }
}

/// The built-in looks as tappable thumbnails; the chosen one is outlined.
class LooksRow extends StatelessWidget {
  const LooksRow({super.key, required this.looks, required this.controller});
  final List<GravixLook> looks;
  final GravixBeautyController controller;

  @override
  Widget build(BuildContext context) => ListenableBuilder(
        listenable: controller,
        builder: (context, _) => SizedBox(
          height: 92,
          child: ListView(scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 12), children: [
            for (final look in looks)
              GestureDetector(
                onTap: () => controller.selectLookFile(look),
                child: Container(
                  width: 68,
                  margin: const EdgeInsets.symmetric(horizontal: 4),
                  child: Column(children: [
                    Container(
                      width: 60,
                      height: 60,
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(12),
                        border: Border.all(color: controller.isLook(look) ? Colors.white : Colors.transparent, width: 2),
                        image: look.thumbnail == null ? null : DecorationImage(image: MemoryImage(look.thumbnail!), fit: BoxFit.cover),
                        color: Colors.white24,
                      ),
                    ),
                    const SizedBox(height: 4),
                    Text(look.name, style: const TextStyle(color: Colors.white, fontSize: 11), overflow: TextOverflow.ellipsis),
                  ]),
                ),
              ),
          ]),
        ),
      );
}
1
likes
140
points
0
downloads

Documentation

API reference

Publisher

verified publishergravixcloud.com

Weekly Downloads

Gravix Effects output for flutter_webrtc / LiveKit — publishes the running GravixCamera as a WebRTC video track.

Homepage

License

unknown (license)

Dependencies

flutter, flutter_webrtc, gravix_effects

More

Packages that depend on gravix_effects_webrtc

Packages that implement gravix_effects_webrtc