utd_stream_sdk 0.2.1 copy "utd_stream_sdk: ^0.2.1" to clipboard
utd_stream_sdk: ^0.2.1 copied to clipboard

Headless LiveKit-based audio/video SDK for Flutter (audio call, video call, live stream, audio room) as logic-only sessions with no UI — build your own interface.

example/lib/main.dart

// Runnable example for the headless utd_stream_sdk.
//
// It demonstrates the three product surfaces — audio room, live stream, and a
// 1:1 call — building UI entirely from the sessions' `ValueListenable`s. The
// SDK ships no widgets; everything you see here is host-app UI reacting to SDK
// state, which is exactly how you would integrate it.
//
// Fill in your `appId` / `appKey` on the home screen before joining.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:livekit_client/livekit_client.dart'
    show VideoTrack, VideoTrackRenderer;
import 'package:utd_stream_sdk/utd_stream_sdk.dart';

void main() => runApp(const ExampleApp());

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'UTD Stream SDK',
    theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
    home: const HomePage(),
  );
}

/// Collects credentials + identity and routes into each demo flow.
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final _appId = TextEditingController();
  final _appKey = TextEditingController();
  final _identity = TextEditingController(text: 'user-1');
  final _room = TextEditingController(text: 'demo-room');
  final _callee = TextEditingController(text: 'user-2');

  // STEP 1: build one client per signed-in user. The same credentials work for
  // every product type the project enabled — the type is chosen per session.
  UTDStreamClient _client() =>
      UTDStreamClient(appId: _appId.text.trim(), appKey: _appKey.text.trim());

  void _open(Widget page) =>
      Navigator.of(context).push(MaterialPageRoute(builder: (_) => page));

  Future<void> _guard(Future<void> Function() action) async {
    if (_appId.text.trim().isEmpty || _appKey.text.trim().isEmpty) {
      _snack('Enter appId and appKey first');
      return;
    }
    try {
      await action();
    } on UTDStreamException catch (e) {
      _snack('${e.code ?? e.statusCode}: ${e.message}');
    }
  }

  void _snack(String m) {
    if (mounted)
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(m)));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('UTD Stream SDK — example')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _field(_appId, 'appId'),
          _field(_appKey, 'appKey'),
          _field(_identity, 'identity (this user)'),
          _field(_room, 'room name'),
          const SizedBox(height: 16),
          FilledButton.icon(
            icon: const Icon(Icons.event_seat),
            label: const Text('Join audio room as host'),
            // STEP 2a: join an audio room. The host can seed the seat grid.
            onPressed: () => _guard(() async {
              final client = _client();
              final session = await client.joinAudioRoom(
                identity: _identity.text.trim(),
                roomName: _room.text.trim(),
                asHost: true,
                seatCount: 8,
              );
              _open(AudioRoomPage(client: client, session: session));
            }),
          ),
          const SizedBox(height: 8),
          OutlinedButton.icon(
            icon: const Icon(Icons.live_tv),
            label: const Text('Go live (host)'),
            // STEP 2b: join a live stream as host, then goLive() to publish.
            onPressed: () => _guard(() async {
              final client = _client();
              final session = await client.joinLiveStream(
                identity: _identity.text.trim(),
                roomName: _room.text.trim(),
                asHost: true,
              );
              _open(LiveStreamPage(client: client, session: session));
            }),
          ),
          const Divider(height: 32),
          _field(_callee, 'callee identity'),
          const SizedBox(height: 8),
          OutlinedButton.icon(
            icon: const Icon(Icons.videocam),
            label: const Text('Start 1:1 video call'),
            // STEP 2c: calls have no room to mint first, so authenticate() mints
            // a bearer, then the call session start()s the call.
            onPressed: () => _guard(() async {
              final client = _client();
              await client.authenticate(identity: _identity.text.trim());
              final session = client.callSession(UTDStreamType.videoCall);
              _open(
                CallPage(
                  client: client,
                  session: session,
                  callee: _callee.text.trim(),
                ),
              );
            }),
          ),
        ],
      ),
    );
  }

  Widget _field(TextEditingController c, String label) => Padding(
    padding: const EdgeInsets.only(bottom: 8),
    child: TextField(
      controller: c,
      decoration: InputDecoration(
        labelText: label,
        border: const OutlineInputBorder(),
      ),
    ),
  );

  @override
  void dispose() {
    for (final c in [_appId, _appKey, _identity, _room, _callee]) {
      c.dispose();
    }
    super.dispose();
  }
}

/// Audio room: take/leave a seat (the engine grants publish; the SDK waits for
/// the grant before turning the mic on) and watch connection + roster state.
class AudioRoomPage extends StatefulWidget {
  const AudioRoomPage({super.key, required this.client, required this.session});

  final UTDStreamClient client;
  final UTDAudioRoomSession session;

  @override
  State<AudioRoomPage> createState() => _AudioRoomPageState();
}

class _AudioRoomPageState extends State<AudioRoomPage> {
  UTDAudioRoomSession get s => widget.session;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Audio room')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // STEP 3: render headless state straight from the listenables.
            _Listen(s.connected, (c) => _row('connected', c)),
            _Listen(
              s.canPublish,
              (c) => _row('canPublish (server-granted)', c),
            ),
            _Listen(s.micEnabled, (m) => _row('mic on', m)),
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              children: [
                FilledButton(
                  onPressed: () => _try(() => s.takeSeat(1)),
                  child: const Text('Take seat 1'),
                ),
                OutlinedButton(
                  onPressed: () => _try(() => s.leaveSeat()),
                  child: const Text('Leave seat'),
                ),
                _Listen(
                  s.micEnabled,
                  (on) => TextButton(
                    onPressed: () => _try(() => s.setMicrophone(!on)),
                    child: Text(on ? 'Mute' : 'Unmute'),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            const Text('Remote participants:'),
            Expanded(
              child: ValueListenableBuilder<List<RemoteParticipant>>(
                valueListenable: s.remoteParticipants,
                builder: (_, list, _) => ListView(
                  children: [
                    for (final p in list)
                      ListTile(dense: true, title: Text(p.identity)),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _try(Future<void> Function() f) async {
    try {
      await f();
    } on UTDStreamException catch (e) {
      if (mounted)
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text(e.message)));
    }
  }

  Widget _row(String label, bool on) => Row(
    children: [
      Icon(
        on ? Icons.check_circle : Icons.circle_outlined,
        size: 18,
        color: on ? Colors.green : Colors.grey,
      ),
      const SizedBox(width: 8),
      Text(label),
    ],
  );

  @override
  void dispose() {
    // STEP 4: dispose the session (and the client when done with it).
    s.dispose();
    widget.client.dispose();
    super.dispose();
  }
}

/// Live stream: the host publishes camera+mic via goLive(); viewers subscribe.
class LiveStreamPage extends StatefulWidget {
  const LiveStreamPage({
    super.key,
    required this.client,
    required this.session,
  });

  final UTDStreamClient client;
  final UTDLiveStreamSession session;

  @override
  State<LiveStreamPage> createState() => _LiveStreamPageState();
}

class _LiveStreamPageState extends State<LiveStreamPage> {
  UTDLiveStreamSession get s => widget.session;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Live stream')),
      body: Column(
        children: [
          Expanded(child: _LocalVideo(session: s)),
          Padding(
            padding: const EdgeInsets.all(12),
            child: Wrap(
              spacing: 8,
              children: [
                FilledButton(
                  onPressed: () => s.goLive(),
                  child: const Text('Go live'),
                ),
                OutlinedButton(
                  onPressed: () => s.leave(),
                  child: const Text('Leave'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    s.dispose();
    widget.client.dispose();
    super.dispose();
  }
}

/// 1:1 call: start the call, show its status, and hang up.
class CallPage extends StatefulWidget {
  const CallPage({
    super.key,
    required this.client,
    required this.session,
    required this.callee,
  });

  final UTDStreamClient client;
  final UTDCallSession session;
  final String callee;

  @override
  State<CallPage> createState() => _CallPageState();
}

class _CallPageState extends State<CallPage> {
  UTDCallSession get s => widget.session;

  @override
  void initState() {
    super.initState();
    // STEP 5: caller starts the call; mic (and camera for video) auto-publish.
    s.start(calleeIdentity: widget.callee);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('1:1 call')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ValueListenableBuilder<UTDCallStatus?>(
              valueListenable: s.status,
              builder: (_, st, _) => Text(
                'Status: ${st?.name ?? '—'}',
                style: Theme.of(context).textTheme.titleLarge,
              ),
            ),
            const SizedBox(height: 24),
            FilledButton.tonalIcon(
              icon: const Icon(Icons.call_end),
              label: const Text('Hang up'),
              onPressed: () async {
                await s.end();
                if (context.mounted) Navigator.of(context).pop();
              },
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    s.dispose();
    widget.client.dispose();
    super.dispose();
  }
}

/// Renders the local camera track once it is published, else a placeholder.
class _LocalVideo extends StatelessWidget {
  const _LocalVideo({required this.session});

  final UTDLiveStreamSession session;

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<bool>(
      valueListenable: session.cameraEnabled,
      builder: (_, on, _) {
        final lp = session.localParticipant;
        VideoTrack? track;
        if (on && lp != null) {
          for (final pub in lp.videoTrackPublications) {
            final t = pub.track;
            if (t is VideoTrack) {
              track = t;
              break;
            }
          }
        }
        if (track == null) {
          return const ColoredBox(
            color: Colors.black12,
            child: Center(child: Text('Camera off — tap "Go live"')),
          );
        }
        return VideoTrackRenderer(track);
      },
    );
  }
}

/// Small helper that renders a bool [ValueListenable].
class _Listen extends StatelessWidget {
  const _Listen(this.listenable, this.builder);

  final ValueListenable<bool> listenable;
  final Widget Function(bool) builder;

  @override
  Widget build(BuildContext context) => ValueListenableBuilder<bool>(
    valueListenable: listenable,
    builder: (_, v, _) => builder(v),
  );
}
0
likes
130
points
0
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Headless LiveKit-based audio/video SDK for Flutter (audio call, video call, live stream, audio room) as logic-only sessions with no UI — build your own interface.

Topics

#livekit #webrtc #voice-chat #video-call #live-streaming

License

MIT (license)

Dependencies

device_info_plus, dio, flutter, flutter_webrtc, livekit_client, package_info_plus, shared_preferences, web_socket_channel

More

Packages that depend on utd_stream_sdk