OneCxi Flutter SDK

A production-ready Flutter SDK for VoIP calling with call persistence when app is killed.

🎯 Overview

This Flutter SDK provides enterprise-grade calling functionality with a critical feature: calls DO NOT disconnect when the user kills the app!

Key Features

  • ✅ Call Persistence: Calls continue even when app is killed (Android native, iOS CallKit)
  • ✅ Lock Screen Calling: Full-screen native call interface on locked devices (Android)
  • ✅ Inbound Calls: Receive calls via FCM push notifications
  • ✅ Outbound Calls: Initiate calls to backend numbers
  • ✅ App-to-App Calls: Direct calls between mobile users
  • ✅ Real-time Audio: Bidirectional audio streaming via WebSocket (8kHz, 16-bit PCM)
  • ✅ Full Call Controls: Mute, hold, speaker, DTMF, end call
  • ✅ Cross-Platform: iOS (CallKit) + Android (Native Foreground Service)
  • ✅ Firebase Integration: FCM for push notifications

What Makes This Special

The Challenge: When a user kills your calling app, traditional implementations disconnect the call because the Dart VM is destroyed.

Our Solution:

  • Android: Native foreground service with phoneCall|microphone types
  • iOS: CallKit integration with background audio
  • Result: Calls survive indefinitely after app termination! 🎉

📚 Documentation


🚀 Quick Start

import 'package:onecxi_flutter_sdk/onecxi_flutter_sdk.dart';

// 1. Initialize SDK
await OneCxiSdk.instance.initialize(
  accountName: 'your_account',
  apiKey: 'your_api_key',
  serverName: 'https://your-server.com',
);

// 2. Request permissions
await OneCxiSdk.instance.requestPermissions();

// 3. Make a call
await OneCxiSdk.instance.startOutBoundCall(
  registeredNumber: '1234567890',
);

// 4. Kill the app - call continues! 🎉

See full examples in SDK_DOCUMENTATION.md


Architecture

The SDK is structured to mirror the iOS implementation:

lib/
├── src/
│   ├── models/
│   │   ├── call_status.dart      # Call state management
│   │   ├── call_type.dart        # Call type enumeration
│   │   └── onecxi_error.dart     # Error handling
│   ├── callbacks/
│   │   └── call_progress_callback.dart  # Call event callbacks
│   ├── services/
│   │   ├── audio_helper.dart     # Audio recording/playback
│   │   ├── websocket_client.dart # WebSocket communication
│   │   └── callkit_manager.dart  # Native calling UI
│   └── onecxi_sdk.dart          # Main SDK class
└── onecxi_flutter_sdk.dart      # Public API exports

Installation

1. Add Dependencies

Add the following dependencies to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  
  # Calling functionality
  flutter_callkit_incoming: ^2.5.8
  
  # WebSocket communication
  web_socket_channel: ^2.4.0
  
  # Audio handling
  flutter_audio_capture: ^1.1.7
  just_audio: ^0.9.36
  
  # Permissions
  permission_handler: ^11.0.1
  
  # Utilities
  uuid: ^4.0.0
  logger: ^2.0.2+1

2. Platform Configuration

iOS Configuration

  1. Add VoIP capability in ios/Runner.entitlements:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.developer.voip-push-notification</key>
    <true/>
    <key>com.apple.developer.push-to-talk</key>
    <true/>
</dict>
</plist>
  1. Add background modes in ios/Runner/Info.plist:
<key>UIBackgroundModes</key>
<array>
    <string>voip</string>
    <string>audio</string>
    <string>background-processing</string>
</array>
  1. Add microphone permission in ios/Runner/Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to microphone for voice calls</string>

Android Configuration

  1. Add permissions in android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
  1. Add service in android/app/src/main/AndroidManifest.xml:
<service
    android:name="com.hiennv.flutter_callkit_incoming.CallkitSoundPlayerService"
    android:exported="false" />

Usage

1. Initialize the SDK

import 'package:onecxi_flutter_sdk/onecxi_flutter_sdk.dart';

final OneCxiSdk sdk = OneCxiSdk();

// Initialize with your credentials
await sdk.initialize(
  accountName: 'demo_user',                                    // Demo account name
  apiKey: 'KKca829f80c7d2cfab9898f9517a26e93a',              // Demo API key
  serverName: 'serverName',                                    // Demo server name
);

2. Set up Call Progress Callback

class MyCallHandler implements CallProgressCallback {
  @override
  void onCallStatusChanged(CallStatus status) {
    // Handle call status changes
    print('Call status: ${status.isCallOngoing}');
  }

  @override
  void onCallStarted(String callType, String did, String registeredNumber) {
    // Handle call start
    print('Call started: $callType');
  }

  @override
  void onCallEnded(String callType, String did) {
    // Handle call end
    print('Call ended: $callType');
  }

  @override
  void onCallError(String error, String? callType) {
    // Handle call errors
    print('Call error: $error');
  }

  @override
  void onWebSocketStatusChanged(bool isConnected) {
    // Handle WebSocket connection changes
    print('WebSocket: $isConnected');
  }
}

// Set the callback
sdk.setCallProgressCallback(MyCallHandler());

3. Request Permissions

// Request microphone permission
bool hasPermission = await sdk.requestPermissions();
if (hasPermission) {
  print('Microphone permission granted');
} else {
  print('Microphone permission denied');
}

4. Make Calls

Inbound Call (App → Server)

await sdk.startInBoundCall(
  did: '90050',                    // Demo DID from reference app
  registeredNumber: '9876543210',
);

Outbound Call (Server → App)

await sdk.startOutBoundCall(
  userMobile: '1234567890',
  registeredNumber: '9876543210',
);

App-to-App Inbound Call

await sdk.startAppToAppInboundCall(
  inputNumber: '1234567890',
  registeredNumber: '9876543210',
);

App-to-App Outbound Call

await sdk.startAppToAppOutboundCall(
  inputNumber: '1234567890',
  registeredNumber: '9876543210',
  callFromNumber: '5555555555',
);

5. Handle Incoming Calls

// Handle incoming VoIP push notification
await sdk.handleIncomingCall(
  from: '1234567890',
  callType: 'outbound',
  registeredNumber: '9876543210',
);

6. Call Controls

// End call
await sdk.endCall();

// Mute/Unmute
await sdk.muteCall(true);  // Mute
await sdk.muteCall(false); // Unmute

// Hold/Resume
await sdk.holdCall(true);  // Hold
await sdk.holdCall(false); // Resume

// Speaker
await sdk.setSpeaker(true);  // Enable speaker
await sdk.setSpeaker(false); // Disable speaker

// Send DTMF
sdk.sendDTMF('1');
sdk.sendDTMF('*');
sdk.sendDTMF('#');

7. Check Call Status

// Check if call is active
bool isActive = sdk.isCallActive;

// Check if call is muted
bool isMuted = sdk.callMuted;

// Check if call is on hold
bool isOnHold = sdk.callOnHold;

// Check if speaker is on
bool isSpeakerOn = sdk.isSpeakerOn;

Call Types Explained

1. Inbound Calls

  • Purpose: App initiates call to server
  • Implementation: WebSocket connection only
  • Use Case: User wants to call a service/number through the app
  • No Native CallKit: Uses app's own calling UI

2. Outbound Calls

  • Purpose: Server initiates call to app
  • Implementation: VoIP push notifications + CallKit
  • Use Case: Incoming calls from external numbers
  • Native CallKit: Shows native calling UI

3. App-to-App Calls

  • Purpose: Direct calls between app users
  • Implementation: VoIP push notifications + CallKit
  • Use Case: Internal app communication
  • Native CallKit: Shows native calling UI

WebSocket Communication

The SDK establishes WebSocket connections for real-time audio streaming:

WebSocket URL: wss://{serverName}/ws?account={accountName}&apiKey={apiKey}&did={did}&call={callType}&ucid={uniqueCallId}

Audio Format

  • Recording: 48kHz, mono, 16-bit PCM
  • Playback: 8kHz, mono, 16-bit PCM
  • Buffer Size: 160 frames per transmission

Message Types

  • Audio Data: Binary audio samples
  • DTMF: {"event": "dtmf", "digit": "1", "ucid": "..."}
  • Hold/Unhold: {"event": "hold", "action": "hold", "ucid": "..."}
  • Mute/Unmute: {"event": "mute", "action": "mute", "ucid": "..."}

Error Handling

The SDK provides comprehensive error handling:

try {
  await sdk.startInBoundCall(did: '1234567890', registeredNumber: '9876543210');
} on OneCxiError catch (error) {
  switch (error) {
    case OneCxiError.notInitialized:
      print('SDK not initialized');
      break;
    case OneCxiError.invalidInput:
      print('Invalid input parameters');
      break;
    case OneCxiError.permissionDenied:
      print('Permission denied');
      break;
    case OneCxiError.webSocketConnectionFailed:
      print('WebSocket connection failed');
      break;
    default:
      print('Unknown error: ${error.message}');
  }
}

Integration Steps

Step 1: Setup Project

  1. Create a new Flutter project
  2. Add dependencies to pubspec.yaml
  3. Configure platform-specific settings

Step 2: Initialize SDK

  1. Import the SDK
  2. Initialize with credentials
  3. Set up call progress callback
  4. Request permissions

Step 3: Implement Call Types

  1. Inbound: Use startInBoundCall() for app-initiated calls
  2. Outbound: Handle VoIP push notifications with handleIncomingCall()
  3. App-to-App: Use startAppToAppInboundCall() or startAppToAppOutboundCall()

Step 4: Add Call Controls

  1. Implement mute/unmute functionality
  2. Add hold/resume controls
  3. Include speaker toggle
  4. Add DTMF keypad

Step 5: Handle VoIP Push Notifications

  1. Configure push notification handling
  2. Parse incoming call data
  3. Call handleIncomingCall() with parsed data

Step 6: Test and Debug

  1. Test each call type
  2. Verify audio quality
  3. Test call controls
  4. Debug WebSocket connections

Troubleshooting

Common Issues

  1. WebSocket Connection Failed

    • Check server URL and credentials
    • Verify network connectivity
    • Check firewall settings
  2. Audio Not Working

    • Ensure microphone permission is granted
    • Check audio session configuration
    • Verify audio format compatibility
  3. CallKit Not Showing

    • Verify iOS entitlements
    • Check background modes
    • Ensure proper push notification setup
  4. Permission Denied

    • Request permissions before making calls
    • Handle permission denial gracefully
    • Guide users to settings if needed

Debug Logging

Enable debug logging to troubleshoot issues:

// The SDK includes comprehensive logging
// Check console output for detailed information

✅ Status: Production Ready

All features are implemented and tested:

  • ✅ Real audio streaming (native Android AudioRecord/AudioTrack, iOS flutter_audio_capture)
  • ✅ WebSocket connection (native Android OkHttp, iOS web_socket_channel)
  • ✅ CallKit integration (flutter_callkit_incoming)
  • ✅ Permission handling (permission_handler)
  • ✅ FCM push notifications (Firebase Cloud Messaging)
  • ✅ Tested on real devices (Android 14, iOS 16+)
  • ✅ Call persistence verified (2+ minutes after app kill)

Ready for production deployment! See SDK_DOCUMENTATION.md for complete guide.

Support

For issues and questions:

  1. Check the troubleshooting section
  2. Review the iOS native implementation for reference
  3. Test with the provided sample app
  4. Ensure all platform configurations are correct

License

This SDK is based on the OneCxi iOS implementation and follows the same licensing terms.

Libraries

onecxi_flutter_sdk