Skip to main content

agenticerp_voice 0.1.2 copy "agenticerp_voice: ^0.1.2" to clipboard
agenticerp_voice: ^0.1.2 copied to clipboard

Voice SDK for the AgenticERP call centre: place phone calls to customers from your Flutter app through your company hotline, with the call audio in the app.

example/lib/main.dart

import 'dart:async';

import 'package:agenticerp_voice/agenticerp_voice.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

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

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

  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Voice SDK demo',
        theme: ThemeData(colorSchemeSeed: const Color(0xFF6E62FF), useMaterial3: true),
        home: const CallScreen(),
      );
}

class CallScreen extends StatefulWidget {
  const CallScreen({super.key});

  @override
  State<CallScreen> createState() => _CallScreenState();
}

class _CallScreenState extends State<CallScreen> {
  final _baseUrl = TextEditingController(text: 'https://erp.example.com');
  // In a real app the token comes from YOUR server (see README → "Token server").
  final _token = TextEditingController();
  final _phone = TextEditingController();
  final _log = <String>[];

  AerpClient? _client;
  StreamSubscription<AerpClientEvent>? _clientSub;
  AerpCall? _call;
  AerpFromNumber? _from;
  bool _muted = false;
  bool _speaker = false;
  bool _onHold = false;
  Timer? _ticker;

  bool get _inCall => _call != null && !_call!.isEnded;

  void _addLog(String line) => setState(() => _log.insert(0, '${TimeOfDay.now().format(context)}  $line'));

  Future<void> _connect() async {
    await _clientSub?.cancel();
    await _client?.dispose();
    final client = AerpClient(baseUrl: _baseUrl.text.trim());
    _clientSub = client.events.listen((e) {
      switch (e) {
        case AerpClientConnected(:final user):
          _addLog('Đã kết nối: ${user.displayName} (máy lẻ ${user.extension ?? '-'})');
        case AerpClientDisconnected(:final reason):
          _addLog('Mất kết nối: $reason');
        case AerpClientError(:final code, :final message):
          _addLog('Lỗi kết nối [$code] $message');
        case AerpTokenWillExpire(:final expiresIn):
          // Fetch a fresh token from your server, then: client.updateToken(newToken)
          _addLog('Token sắp hết hạn (${expiresIn.inSeconds}s) — lấy token mới rồi gọi updateToken()');
        case AerpRequestAccessToken(:final reason):
          _addLog('Cần token mới ($reason) — gọi updateToken()');
      }
      if (mounted) setState(() {});
    });
    _client = client;
    final result = await client.connect(_token.text);
    if (!mounted) return;
    setState(() {
      _from = client.fromNumbers.where((n) => n.isDefault).firstOrNull ?? client.fromNumbers.firstOrNull;
    });
    if (!result.status) _addLog('connect thất bại: ${result.code} — ${result.message}');
  }

  Future<void> _makeCall() async {
    final client = _client;
    if (client == null || !client.isConnected) return;
    // The SDK does not ask for permissions itself — the app owns that UX.
    if (!await Permission.microphone.request().isGranted) {
      _addLog('Chưa cấp quyền micro');
      return;
    }
    final call = AerpCall(client);
    call.events.listen((e) {
      switch (e) {
        case AerpSignalingStateChanged(:final state, :final reason):
          _addLog('Cuộc gọi: ${_stateLabel(state)}${reason == null ? '' : ' ($reason)'}');
          if (state == AerpSignalingState.answered) {
            _ticker ??= Timer.periodic(const Duration(seconds: 1), (_) => setState(() {}));
          }
          if (state.isTerminal) {
            _ticker?.cancel();
            _ticker = null;
          }
        case AerpMediaStateChanged(:final state):
          _addLog('Âm thanh: ${state.name}');
        case AerpCallInfoUpdated(:final info):
          _onHold = info.onHold;
        case AerpCallError(:final code, :final message):
          _addLog('Lỗi cuộc gọi [$code] $message');
      }
      if (mounted) setState(() {});
    });
    setState(() {
      _call = call;
      _muted = false;
      _onHold = false;
    });
    final result = await call.makeCall(AerpCallParams(
      to: _phone.text,
      from: _from?.number,
      customData: {'booking_id': 'demo-123'},
    ));
    if (!result.status) _addLog('makeCall thất bại: ${result.code} — ${result.message}');
  }

  String _stateLabel(AerpSignalingState s) => switch (s) {
        AerpSignalingState.calling => 'Đang gọi…',
        AerpSignalingState.ringing => 'Đang đổ chuông…',
        AerpSignalingState.answered => 'Đang nói chuyện',
        AerpSignalingState.busy => 'Máy bận',
        AerpSignalingState.ended => 'Đã kết thúc',
      };

  String _talkTime() {
    final d = _call?.talkDuration;
    if (d == null) return '';
    return ' ${d.inMinutes.toString().padLeft(2, '0')}:${(d.inSeconds % 60).toString().padLeft(2, '0')}';
  }

  @override
  void dispose() {
    _ticker?.cancel();
    _call?.destroy();
    _clientSub?.cancel();
    _client?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final client = _client;
    final call = _call;
    return Scaffold(
      appBar: AppBar(title: const Text('Gọi qua tổng đài')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          TextField(controller: _baseUrl, decoration: const InputDecoration(labelText: 'Địa chỉ tổng đài')),
          TextField(
            controller: _token,
            decoration: const InputDecoration(labelText: 'Token (do máy chủ của bạn ký)'),
            maxLines: 2,
          ),
          const SizedBox(height: 8),
          FilledButton.tonal(
            onPressed: _inCall ? null : _connect,
            child: Text(client?.isConnected == true ? 'Kết nối lại' : 'Kết nối'),
          ),
          if (client?.isConnected == true) ...[
            const Divider(height: 32),
            InputDecorator(
              decoration: const InputDecoration(labelText: 'Đầu số gọi ra'),
              child: DropdownButton<AerpFromNumber>(
                value: _from,
                isExpanded: true,
                underline: const SizedBox.shrink(),
                items: [
                  for (final n in client!.fromNumbers)
                    DropdownMenuItem(value: n, child: Text('${n.number}${n.name == null ? '' : ' · ${n.name}'}')),
                ],
                onChanged: _inCall ? null : (n) => setState(() => _from = n),
              ),
            ),
            TextField(
              controller: _phone,
              keyboardType: TextInputType.phone,
              decoration: const InputDecoration(labelText: 'Số khách hàng'),
            ),
            const SizedBox(height: 8),
            if (!_inCall)
              FilledButton.icon(onPressed: _makeCall, icon: const Icon(Icons.call), label: const Text('Gọi'))
            else ...[
              Text(
                '${_stateLabel(call!.signalingState)}${_talkTime()}',
                style: Theme.of(context).textTheme.titleLarge,
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 8),
              Wrap(
                alignment: WrapAlignment.center,
                spacing: 8,
                children: [
                  FilterChip(
                    label: const Text('Tắt tiếng'),
                    selected: _muted,
                    onSelected: (v) async {
                      await call.mute(v);
                      setState(() => _muted = v);
                    },
                  ),
                  FilterChip(
                    label: const Text('Loa ngoài'),
                    selected: _speaker,
                    onSelected: (v) async {
                      await call.setSpeakerphoneOn(v);
                      setState(() => _speaker = v);
                    },
                  ),
                  FilterChip(
                    label: const Text('Giữ máy'),
                    selected: _onHold,
                    onSelected: call.signalingState == AerpSignalingState.answered
                        ? (v) async {
                            final r = await call.hold(v);
                            if (r.status) setState(() => _onHold = v);
                          }
                        : null,
                  ),
                ],
              ),
              const SizedBox(height: 8),
              FilledButton.icon(
                style: FilledButton.styleFrom(backgroundColor: Colors.red),
                onPressed: call.hangup,
                icon: const Icon(Icons.call_end),
                label: const Text('Gác máy'),
              ),
            ],
          ],
          const Divider(height: 32),
          for (final line in _log) Text(line, style: Theme.of(context).textTheme.bodySmall),
        ],
      ),
    );
  }
}
1
likes
140
points
130
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Voice SDK for the AgenticERP call centre: place phone calls to customers from your Flutter app through your company hotline, with the call audio in the app.

Homepage

License

unknown (license)

Dependencies

audioplayers, clock, flutter, http, livekit_client, meta

More

Packages that depend on agenticerp_voice