sip_voip_plugin 1.0.4 copy "sip_voip_plugin: ^1.0.4" to clipboard
sip_voip_plugin: ^1.0.4 copied to clipboard

A simple Flutter plugin for SIP VoIP calling with CallKit support. Supports single and multi-account SIP registrations.

example/lib/main.dart

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:sip_ua/sip_ua.dart';
import 'package:sip_voip_plugin/voip_service.dart';
import 'package:sip_voip_plugin/sip_service.dart';
import 'package:sip_voip_plugin/voip_plugin.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Set up global error handler
  FlutterError.onError = (FlutterErrorDetails details) {
    FlutterError.presentError(details);
    debugPrint('═══════════════════════════════════════');
    debugPrint('❌ FLUTTER ERROR');
    debugPrint('═══════════════════════════════════════');
    debugPrint('Exception: ${details.exception}');
    debugPrint('Library: ${details.library}');
    debugPrint('Context: ${details.context}');
    debugPrint('═══════════════════════════════════════');
    debugPrint('Stack Trace:');
    debugPrint(details.stack.toString());
    debugPrint('═══════════════════════════════════════');
  };
  
  // Handle async errors
  PlatformDispatcher.instance.onError = (error, stack) {
    debugPrint('═══════════════════════════════════════');
    debugPrint('❌ PLATFORM DISPATCHER ERROR');
    debugPrint('═══════════════════════════════════════');
    debugPrint('Error: $error');
    debugPrint('═══════════════════════════════════════');
    debugPrint('Stack Trace:');
    debugPrint(stack.toString());
    debugPrint('═══════════════════════════════════════');
    return true;
  };
  
  try {
    // Initialize VoIP service
    await VoipService.initialize();
    
    // Request necessary permissions
    await _requestPermissions();
    
    runApp(const MyApp());
  } catch (e, stackTrace) {
    debugPrint('═══════════════════════════════════════');
    debugPrint('❌ EXCEPTION: Main Function');
    debugPrint('═══════════════════════════════════════');
    debugPrint('Error Type: ${e.runtimeType}');
    debugPrint('Error Message: $e');
    debugPrint('═══════════════════════════════════════');
    debugPrint('Stack Trace:');
    debugPrint(stackTrace.toString());
    debugPrint('═══════════════════════════════════════');
    rethrow;
  }
}

Future<void> _requestPermissions() async {
  try {
    // Request microphone permission for VoIP calls
    await Permission.microphone.request();
    // Request phone permission for call management (Android)
    await Permission.phone.request();
    // Request notification permission for Android 13+
    if (await Permission.notification.isDenied) {
      await Permission.notification.request();
    }
  } catch (e, stackTrace) {
    debugPrint('═══════════════════════════════════════');
    debugPrint('❌ EXCEPTION: Request Permissions');
    debugPrint('═══════════════════════════════════════');
    debugPrint('Error Type: ${e.runtimeType}');
    debugPrint('Error Message: $e');
    debugPrint('═══════════════════════════════════════');
    debugPrint('Stack Trace:');
    debugPrint(stackTrace.toString());
    debugPrint('═══════════════════════════════════════');
  }
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SIP VoIP Call',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const SipCallPage(),
    );
  }
}

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

  @override
  State<SipCallPage> createState() => _SipCallPageState();
}

class _SipCallPageState extends State<SipCallPage> {
  // SIP controllers - Pre-filled with production server credentials
  final TextEditingController _sipServerController = TextEditingController(text: '686e4c6534db7.voip.nextelco.io');
  final TextEditingController _sipUsernameController = TextEditingController(text: '2502');
  final TextEditingController _sipPasswordController = TextEditingController(text: '12345678');
  final TextEditingController _sipRealmController = TextEditingController(text: 'wss://ws.nxtlinks.io');
  final TextEditingController _sipDisplayNameController = TextEditingController(text: '(201)-908-9308');
  final TextEditingController _sipTargetController = TextEditingController();
  
  String _callStatus = 'Ready';
  bool _isCallActive = false;
  
  // SIP state
  bool _sipRegistered = false;
  String _sipRegistrationStatus = 'Not registered';
  String _transportStatus = 'Unknown';
  bool _isTransportConnected = false;

  @override
  void initState() {
    super.initState();
    try {
      // Set up callback to receive call status updates
      VoipService.onCallStatusChanged = (status, data) {
        try {
          if (mounted) {
            setState(() {
              switch (status) {
                case 'accepted':
                  _callStatus = 'Call accepted';
                  _isCallActive = true;
                  break;
                case 'declined':
                  _callStatus = 'Call declined';
                  _isCallActive = false;
                  break;
                case 'ended':
                  _callStatus = 'Call ended';
                  _isCallActive = false;
                  break;
                case 'timeout':
                  _callStatus = 'Call missed (timeout)';
                  _isCallActive = false;
                  break;
              }
            });
            
            // Show snackbar with caller info
            if (data != null) {
              final callerName = data['callerName'] ?? 'Unknown';
              _showSnackBar('Call $status: $callerName');
            }
          }
        } catch (e, stackTrace) {
          _printException('Call Status Callback', e, stackTrace);
        }
      };
      
      // Initialize SIP service
      _initializeSip();
      
      // Set up SIP callbacks
      SipService.onCallStateChanged = (state) {
        if (mounted) {
          final stateStr = state.state.toString();
          setState(() {
            if (stateStr.contains('CONNECTED') || stateStr.contains('connected')) {
              _isCallActive = true;
              _callStatus = 'Call active';
            } else if (stateStr.contains('ENDED') || stateStr.contains('ended') ||
                       stateStr.contains('FAILED') || stateStr.contains('failed')) {
              _isCallActive = false;
              _callStatus = 'Call ended';
            }
          });
        }
      };
      
      SipService.onRegistrationStateChanged = (state) {
        if (mounted) {
          setState(() {
            _sipRegistered = state.state == RegistrationStateEnum.REGISTERED;
            _sipRegistrationStatus = _getRegistrationStatusText(state.state);
          });
        }
      };
      
      SipService.onTransportStateChanged = (state) {
        if (mounted) {
          final stateStr = state.state.toString();
          setState(() {
            _isTransportConnected = stateStr.contains('CONNECTED') || 
                                   stateStr.contains('connected');
            _transportStatus = _getTransportStatusText(state.state);
            
            // Show notification for disconnection
            if (stateStr.contains('DISCONNECTED') || stateStr.contains('disconnected')) {
              _showSnackBar('⚠️ SIP transport disconnected. Reconnecting...');
            } else if (stateStr.contains('CONNECTED') || stateStr.contains('connected')) {
              // Only show connected message if we were previously disconnected
              if (!_isTransportConnected && _sipRegistered) {
                _showSnackBar('✅ SIP transport connected');
              }
            }
          });
        }
      };
    } catch (e, stackTrace) {
      _printException('InitState', e, stackTrace);
    }
  }
  
  Future<void> _initializeSip() async {
    try {
      await SipService.initialize();
    } catch (e, stackTrace) {
      _printException('Initialize SIP', e, stackTrace);
    }
  }
  
  String _getRegistrationStatusText(RegistrationStateEnum? state) {
    if (state == null) return 'Unknown';
    switch (state) {
      case RegistrationStateEnum.REGISTERED:
        return '✅ Registered';
      case RegistrationStateEnum.UNREGISTERED:
        return '❌ Unregistered';
      case RegistrationStateEnum.REGISTRATION_FAILED:
        return '❌ Registration failed';
      default:
        return 'Unknown';
    }
  }
  
  String _getTransportStatusText(dynamic state) {
    if (state == null) return 'Unknown';
    final stateStr = state.toString();
    if (stateStr.contains('CONNECTED') || stateStr.contains('connected')) {
      return '✅ Connected';
    } else if (stateStr.contains('CONNECTING') || stateStr.contains('connecting')) {
      return '⏳ Connecting...';
    } else if (stateStr.contains('DISCONNECTED') || stateStr.contains('disconnected')) {
      return '❌ Disconnected';
    } else {
      return stateStr;
    }
  }
  
  /// Print exception with full details
  void _printException(String context, dynamic error, StackTrace stackTrace) {
    debugPrint('═══════════════════════════════════════');
    debugPrint('❌ EXCEPTION: Main - $context');
    debugPrint('═══════════════════════════════════════');
    debugPrint('Error Type: ${error.runtimeType}');
    debugPrint('Error Message: $error');
    debugPrint('═══════════════════════════════════════');
    debugPrint('Stack Trace:');
    debugPrint(stackTrace.toString());
    debugPrint('═══════════════════════════════════════');
  }

  @override
  void dispose() {
    VoipService.onCallStatusChanged = null;
    SipService.onCallStateChanged = null;
    SipService.onRegistrationStateChanged = null;
    SipService.onTransportStateChanged = null;
    SipService.dispose();
    _sipServerController.dispose();
    _sipUsernameController.dispose();
    _sipPasswordController.dispose();
    _sipRealmController.dispose();
    _sipDisplayNameController.dispose();
    _sipTargetController.dispose();
    super.dispose();
  }
  
  Future<void> _registerSip() async {
    if (_sipServerController.text.isEmpty || 
        _sipUsernameController.text.isEmpty || 
        _sipPasswordController.text.isEmpty) {
      _showSnackBar('Please enter SIP server, username, and password');
      return;
    }
    
    setState(() {
      _sipRegistrationStatus = 'Registering...';
    });
    
    try {
      // Check if realm is provided (WebSocket connection)
      final realm = _sipRealmController.text.trim();
      final useWebSocket = realm.isNotEmpty && (realm.startsWith('ws://') || realm.startsWith('wss://'));
      
      final success = await SipService.register(
        username: _sipUsernameController.text.trim(),
        password: _sipPasswordController.text.trim(),
        domain: _sipServerController.text.trim(),
        realm: realm.isNotEmpty ? realm : null,
        displayName: _sipDisplayNameController.text.isEmpty 
            ? _sipUsernameController.text.trim()
            : _sipDisplayNameController.text.trim(),
        callerId: _sipDisplayNameController.text.isNotEmpty 
            ? _sipDisplayNameController.text.trim()
            : null,
        sendAs: _sipDisplayNameController.text.isNotEmpty 
            ? _sipDisplayNameController.text.trim()
            : null,
        useWebSocket: useWebSocket,
        wsUrl: useWebSocket ? realm : null,
      );
      
      if (success) {
        _showSnackBar('SIP registration initiated');
      } else {
        setState(() {
          _sipRegistrationStatus = 'Registration failed';
        });
        _showSnackBar('Failed to register SIP');
      }
    } catch (e, stackTrace) {
      _printException('Register SIP', e, stackTrace);
      setState(() {
        _sipRegistrationStatus = 'Error: $e';
      });
      _showSnackBar('Error: $e');
    }
  }
  
  Future<void> _unregisterSip() async {
    try {
      await SipService.unregister();
      setState(() {
        _sipRegistered = false;
        _sipRegistrationStatus = 'Unregistered';
      });
      _showSnackBar('SIP unregistered');
    } catch (e, stackTrace) {
      _printException('Unregister SIP', e, stackTrace);
      _showSnackBar('Error: $e');
    }
  }
  
  Future<void> _makeSipCall() async {
    if (!_sipRegistered) {
      _showSnackBar('Please register SIP first');
      return;
    }
    
    if (_sipTargetController.text.isEmpty) {
      _showSnackBar('Please enter target SIP URI or extension');
      return;
    }
    
    setState(() {
      _callStatus = 'Calling...';
      _isCallActive = true;
    });
    
    try {
      await SipService.makeCall(_sipTargetController.text.trim());
      setState(() {
        _callStatus = 'Call initiated';
      });
    } catch (e, stackTrace) {
      _printException('Make SIP Call', e, stackTrace);
      setState(() {
        _callStatus = 'Error: $e';
        _isCallActive = false;
      });
      _showSnackBar('Error: $e');
    }
  }

  Future<void> _endCall() async {
    try {
      // End both CallKit and SIP calls
      await VoipService.endAllCalls();
      if (SipService.isInCall) {
        await SipService.hangup();
      }
      setState(() {
        _callStatus = 'Call ended';
        _isCallActive = false;
      });
    } catch (e, stackTrace) {
      _printException('End Call', e, stackTrace);
      _showSnackBar('Error ending call: $e');
    }
  }

  void _showSnackBar(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message)),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('SIP VoIP Call'),
        centerTitle: true,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(24.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Status Card
            Card(
              elevation: 4,
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  children: [
                    Icon(
                      _sipRegistered ? Icons.phone_enabled : Icons.phone_disabled,
                      size: 48,
                      color: _sipRegistered ? Colors.green : Colors.grey,
                    ),
                    const SizedBox(height: 8),
                    Text(
                      _callStatus,
                      style: Theme.of(context).textTheme.headlineSmall?.copyWith(
                            fontWeight: FontWeight.bold,
                          ),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      _sipRegistrationStatus,
                      style: TextStyle(
                        color: _sipRegistered ? Colors.green : Colors.grey,
                        fontSize: 14,
                      ),
                    ),
                    if (_transportStatus != 'Unknown') ...[
                      const SizedBox(height: 4),
                      Text(
                        _transportStatus,
                        style: TextStyle(
                          color: _isTransportConnected ? Colors.green : Colors.orange,
                          fontSize: 12,
                          fontWeight: FontWeight.w500,
                        ),
                      ),
                    ],
                  ],
                ),
              ),
            ),
            const SizedBox(height: 32),
            
            // SIP Registration Section
            Card(
              color: Colors.teal.shade50,
              child: ExpansionTile(
                initiallyExpanded: true,
                leading: Icon(
                  _sipRegistered ? Icons.check_circle : Icons.radio_button_unchecked,
                  color: _sipRegistered ? Colors.green : Colors.grey,
                ),
                title: Text(
                  'SIP Registration',
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Colors.teal.shade700,
                  ),
                ),
                subtitle: Text(_sipRegistrationStatus),
                children: [
                  Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      children: [
                        TextField(
                          controller: _sipServerController,
                          decoration: const InputDecoration(
                            labelText: 'Domain / SIP Server',
                            hintText: 'e.g., domain.voip.example.com',
                            prefixIcon: Icon(Icons.dns),
                            border: OutlineInputBorder(),
                          ),
                        ),
                        const SizedBox(height: 12),
                        TextField(
                          controller: _sipRealmController,
                          decoration: const InputDecoration(
                            labelText: 'Realm / WebSocket URL',
                            hintText: 'e.g., wss://ws.example.com',
                            prefixIcon: Icon(Icons.cloud),
                            border: OutlineInputBorder(),
                            helperText: 'Required for WebSocket (wss:// or ws://)',
                          ),
                        ),
                        const SizedBox(height: 12),
                        TextField(
                          controller: _sipUsernameController,
                          decoration: const InputDecoration(
                            labelText: 'Username',
                            hintText: 'e.g., 2502',
                            prefixIcon: Icon(Icons.person),
                            border: OutlineInputBorder(),
                          ),
                        ),
                        const SizedBox(height: 12),
                        TextField(
                          controller: _sipPasswordController,
                          decoration: const InputDecoration(
                            labelText: 'Password',
                            hintText: 'Enter SIP password',
                            prefixIcon: Icon(Icons.lock),
                            border: OutlineInputBorder(),
                          ),
                          obscureText: false,
                        ),
                        const SizedBox(height: 12),
                        TextField(
                          controller: _sipDisplayNameController,
                          decoration: const InputDecoration(
                            labelText: 'Caller ID / Display Name',
                            hintText: 'e.g., (201)-908-9308',
                            prefixIcon: Icon(Icons.badge),
                            border: OutlineInputBorder(),
                          ),
                        ),
                        const SizedBox(height: 16),
                        Row(
                          children: [
                            Expanded(
                              child: ElevatedButton.icon(
                                onPressed: _sipRegistered ? null : _registerSip,
                                icon: const Icon(Icons.login),
                                label: const Text('Register'),
                                style: ElevatedButton.styleFrom(
                                  padding: const EdgeInsets.symmetric(vertical: 12),
                                  backgroundColor: Colors.teal,
                                  foregroundColor: Colors.white,
                                ),
                              ),
                            ),
                            const SizedBox(width: 8),
                            Expanded(
                              child: OutlinedButton.icon(
                                onPressed: _sipRegistered ? _unregisterSip : null,
                                icon: const Icon(Icons.logout),
                                label: const Text('Unregister'),
                                style: OutlinedButton.styleFrom(
                                  padding: const EdgeInsets.symmetric(vertical: 12),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 16),
            
            // SIP Call Section
            Card(
              color: Colors.blue.shade50,
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    Row(
                      children: [
                        Icon(Icons.call, color: Colors.blue.shade700, size: 20),
                        const SizedBox(width: 8),
                        Text(
                          'Make SIP Call',
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            color: Colors.blue.shade700,
                            fontSize: 16,
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 16),
                    TextField(
                      controller: _sipTargetController,
                      decoration: const InputDecoration(
                        labelText: 'SIP Target (Extension or URI)',
                        hintText: 'e.g., 1002 or sip:1002@sip.example.com',
                        prefixIcon: Icon(Icons.phone),
                        border: OutlineInputBorder(),
                        helperText: 'Enter extension number or full SIP URI',
                      ),
                    ),
                    const SizedBox(height: 16),
                    ElevatedButton.icon(
                      onPressed: _sipRegistered && !_isCallActive ? _makeSipCall : null,
                      icon: const Icon(Icons.call),
                      label: const Text('Make SIP Call'),
                      style: ElevatedButton.styleFrom(
                        padding: const EdgeInsets.symmetric(vertical: 16),
                        backgroundColor: Colors.blue,
                        foregroundColor: Colors.white,
                      ),
                    ),
                    const SizedBox(height: 12),
                    ElevatedButton.icon(
                      onPressed: _isCallActive ? _endCall : null,
                      icon: const Icon(Icons.call_end),
                      label: const Text('End Call'),
                      style: ElevatedButton.styleFrom(
                        padding: const EdgeInsets.symmetric(vertical: 16),
                        backgroundColor: Colors.red,
                        foregroundColor: Colors.white,
                      ),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),
            
            // Info Card
            Card(
              color: Colors.blue.shade50,
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Icon(Icons.info_outline, color: Colors.blue.shade700),
                        const SizedBox(width: 8),
                        Text(
                          'How to use:',
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            color: Colors.blue.shade700,
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 8),
                    const Text(
                      '1. Enter your SIP server details\n'
                      '2. Enter your extension/username and password\n'
                      '3. Tap "Register" to connect to SIP server\n'
                      '4. Wait for "✅ Registered" status\n'
                      '5. Enter target extension or SIP URI\n'
                      '6. Tap "Make SIP Call" to initiate call\n'
                      '7. Use "End Call" to hang up\n\n'
                      'Note: Requires a SIP server (Asterisk, FreeSWITCH, etc.)',
                      style: TextStyle(fontSize: 12),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
90
points
21
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A simple Flutter plugin for SIP VoIP calling with CallKit support. Supports single and multi-account SIP registrations.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, flutter_callkit_incoming, flutter_webrtc, permission_handler, sip_ua, uuid

More

Packages that depend on sip_voip_plugin