---
title: Aplikasi Softphone flutter WebRTC meggunakan package sip_UA.
description: "================================================ Aplikasi                             : Softphone Mobile Client Modul                                 : Integrasi [SIP_UA](https://pub.dev/packages/sip_ua) Flutter Dokumen                         : Panduan Pengembangan Aplikasi (Developer Guide) ------------------------------------------------------------------- Versi                                    : v1.0.0 Penulis/Pembuat         : Solusipbx (Developer Team) Tanggal Dibuat              : 04 Juli 2026 Terakhir…"
url: "https://solusipbx.id/pusat-bantuan/aplikasi-softphone-flutter-webrtc-sip-ua/"
updated: "2026-07-04"
category: Developer
---

# Aplikasi Softphone flutter WebRTC meggunakan package sip_UA.

================================================

Aplikasi                             : Softphone Mobile Client

Modul                                 : Integrasi [SIP_UA](https://pub.dev/packages/sip_ua) Flutter

Dokumen                         : Panduan Pengembangan Aplikasi (Developer Guide)

-------------------------------------------------------------------

Versi                                    : v1.0.0

Penulis/Pembuat         : Solusipbx (Developer Team)

Tanggal Dibuat              : 04 Juli 2026

Terakhir Diubah            : 04 Juli 2026

===============================================

Berikut adalah **Developer Guide** untuk membangun aplikasi Softphone WebRTC menggunakan package sip_ua di Flutter dan 3CX sebagai [Core PBX](https://solusipbx.com). Panduan ini dirancang agar developer bisa langsung menyalin, mengikuti, dan mengintegrasikannya dengan infrastruktur server yang sudah berjalan di port 443 (Nginx & CoTURN). P

**🛠️****Langkah 1: Persiapan Dependensi & Izin Perangkat**

Buka file pubspec.yaml proyek Flutter Anda, lalu tambahkan package sip_ua dan library pendukung audio.

YAML

```
dependencies:
  flutter:
    sdk: flutter
  sip_ua: ^0.7.10 # Gunakan versi stabil terbaru
permission_handler: ^11.3.0 # Untuk mengurus izin Mikrofon
```

**Konfigurasi Izin OS (Izin Mikrofon & Internet)**

**Android (android/app/src/main/AndroidManifest.xml)**

Tambahkan baris berikut di dalam tag <manifest>:

XML

```
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
iOS (ios/Runner/Info.plist)
```

Tambahkan key berikut untuk meminta izin mikrofon secara resmi pada ekosistem Apple:

XML

```
<key>NSMicrophoneUsageDescription</key>
<string>Aplikasi ini memerlukan akses mikrofon untuk melakukan panggilan telepon VoIP.</string>
```

**Langkah 2: Arsitektur Kelas Controller (sip_helper.dart)**

Buat sebuah file baru bernama sip_helper.dart. Kelas ini bertugas membungkus (*wrapper*) semua logika koneksi, registrasi, hingga penanganan pemicu panggilan (*call states*).

Dart

```
import 'package:sip_ua/sip_ua.dart';
import 'package:permission_handler/permission_handler.dart';

class MySipController implements SipUaHelperListener {
  final SIPUAHelper _helper = SIPUAHelper();

  // Callback untuk dioper ke UI
  Function(RegistrationState state)? onRegistrationStateChanged;
  Function(Call call, CallState state)? onCallStateChanged;

  MySipController() {
    // Daftarkan listener utama
    _helper.addSipUaHelperListener(this);
  }

  /// REVISI: Menambahkan 'required String authId' ke dalam parameter fungsi
  Future<void> connectAndRegister({
    required String extension,
    required String authId,
    required String password,
    required String domain,
    required String wssUrl,
  }) async {
    // 1. Wajib pastikan izin mikrofon disetujui
    var status = await Permission.microphone.request();
    if (!status.isGranted) {
      print("Izin mikrofon ditolak oleh user.");
      return;
    }

    // 2. Setup Konfigurasi Kredensial SIP
    UASettings settings = UASettings();
    settings.wsUri = wssUrl;
    settings.uri = 'sip:$extension@$domain';
    settings.authId = authId; // REVISI: Sekarang variabel ini sudah valid
    settings.password = password;
    settings.displayName = extension;
    settings.transportType = TransportType.WS;

    // 3. Masukkan Konfigurasi CoTURN Port 443 Anda
    settings.pcConfig = {
      'iceServers': [
        {'url': 'stun:$domain:443'},
        {
          'url': 'turn:$domain:443',
          'username': 'admin_coturn',
          'credential': 'passwordcoturn' // password di turnserver.conf
        }
      ],
      'iceTransportPolicy': 'all'
    };

    // 4. Mulai proses registrasi di latar belakang
    _helper.start(settings);
  }

  /// Fungsi Melakukan Panggilan Keluar (Outbound Call)
  void makeVoiceCall(String targetExtension) {
    _helper.call(targetExtension, voiceonly: true);
  }

  /// Fungsi Menutup atau Menolak Panggilan
  void hangupOrReject(Call call) {
    call.hangup();
  }

  /// Fungsi Menerima Panggilan Masuk (Inbound Call)
  void answerCall(Call call) {
    call.answer(_helper.buildConstraints());
  }

  // ===========================================================================
  // IMPLEMENTASI SIPUAHELPERLISTENER (EVENT HANDLERS)
  // ===========================================================================

  @override
  void registrationStateChanged(RegistrationState state) {
    print('Status Registrasi SIP: ${state.state}');
    if (onRegistrationStateChanged != null) {
      onRegistrationStateChanged!(state);
    }
  }

  @override
  void callStateChanged(Call call, CallState state) {
    print('Status Panggilan: ${state.state}');
    if (onCallStateChanged != null) {
      onCallStateChanged!(call, state);
    }
  }

  @override
  void transportStateChanged(TransportState state) {
    print('Status Koneksi WebSocket: ${state.state}');
  }

  @override
  void onNewMessage(SIPMessageRequest request) {
    print('Menerima pesan teks SIP baru');
  }

  @override
  void onNewNotify(Notify notify) {
    print('Menerima notifikasi baru dari PBX');
  }
}
```

**Langkah 3: Integrasi ke Tampilan Aplikasi (main.dart)**

Berikut adalah contoh implementasi UI sederhana untuk melakukan registrasi ekstensi, melakukan panggilan, dan menerima panggilan masuk secara *realtime*.

```
import 'package:flutter/material.dart';
import 'package:sip_ua/sip_ua.dart';
import 'sip_helper.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const SoftphoneScreen(),
    );
  }
}

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

class _SoftphoneScreenState extends State<SoftphoneScreen> {
  final MySipController _sipController = MySipController();
  final TextEditingController _targetController = TextEditingController();

  String _regStatus = "DISCONNECTED";
  Call? _activeCall;
  String _callStatus = "IDLE";

  @override
  void initState() {
    // REVISI: Mengubah super.override manually menjadi standar Flutter
    super.initState();

    // Sambungkan event listener controller ke state UI
    _sipController.onRegistrationStateChanged = (RegistrationState state) {
      setState(() {
        _regStatus = state.state == RegistrationStateEnum.REGISTERED
            ? "CONNECTED  ✔️ "
            : state.state.toString().split('.').last;
      });
    };

    _sipController.onCallStateChanged = (Call call, CallState state) {
      setState(() {
        _activeCall = call;
        _callStatus = state.state.toString().split('.').last;
      });
    };

    // Auto-Connect saat aplikasi terbuka
    _sipController.connectAndRegister(
      extension: "101",
      authId: "passwordAuthid",
      password: "passwordExtension",
      domain: "pbx.domainanda.com",
      wssUrl: "wss://rtc.yourdomain.com/yourappname",
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Alana Hotel Softphone")),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("Status Ekstensi: $_regStatus",
                style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            const SizedBox(height: 20),
            Text("Status Call: $_callStatus",
                style: TextStyle(fontSize: 16, color: Colors.blue.shade700)),
            const Divider(height: 40),

            if (_activeCall == null || _callStatus == "CALL_INITIATION" || _callStatus == "IDLE") ...[
              TextField(
                controller: _targetController,
                decoration: const InputDecoration(
                  labelText: "Masukkan Nomor Ekstensi Tujuan",
                  border: OutlineInputBorder(),
                ),
                keyboardType: TextInputType.number,
              ),
              const SizedBox(height: 10),
              ElevatedButton.icon(
                onPressed: () => _sipController.makeVoiceCall(_targetController.text),
                icon: const Icon(Icons.call),
                label: const Text("Panggil"),
                style: ElevatedButton.styleFrom(backgroundColor: Colors.green, foregroundColor: Colors.white),
              ),
            ],

            if (_activeCall != null && _callStatus == "INCOMING") ...[
              Text("Panggilan Masuk dari: ${_activeCall!.remote_identity}",
                  style: const TextStyle(fontSize: 16, color: Colors.orange)),
              const SizedBox(height: 15),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  ElevatedButton(
                    onPressed: () => _sipController.answerCall(_activeCall!),
                    style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
                    child: const Text("Terima"),
                  ),
                  ElevatedButton(
                    onPressed: () => _sipController.hangupOrReject(_activeCall!),
                    style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                    child: const Text("Tolak"),
                  ),
                ],
              )
            ],

            if (_activeCall != null && _callStatus == "CONFIRMED") ...[
              const Icon(Icons.phone_in_talk, size: 60, color: Colors.green),
              const SizedBox(height: 15),
              ElevatedButton.icon(
                onPressed: () => _sipController.hangupOrReject(_activeCall!),
                icon: const Icon(Icons.call_end),
                label: const Text("Putuskan Panggilan"),
                style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white),
              )
            ],
          ],
        ),
      ),
    );
  }
}
```

**Dokumen Tambahan Penting bagi Developer**

1. **Format Ekstensi URL:** Penulisan string settings.uri harus menggunakan format lengkap URI SIP: 'sip:USERNAME@DOMAIN'. Kesalahan ketik atau hilangnya prefiks sip: akan menggagalkan handshake.
2. **Penanganan Audio:** Package sip_ua secara internal langsung berkoordinasi dengan *WebRTC native framework* smartphone. Selama panggilan berada pada state CONFIRMED, aliran suara dari mikrofon ke speaker sudah dikelola otomatis secara *realtime*.
3. **Tahap Lanjutan (Push Notification):** Jika aplikasi ini ingin diuji saat ponsel mati (*standby*), developer tinggal mengintegrasikan kelas MySipController di atas dengan package firebase_messaging (FCM) dan flutter_callkit_incoming agar event panggilan masuk bisa langsung membangunkan sistem operasi Android/iOS secara instan.
4. Saat sistem operasi smartphone masuk ke mode hemat daya (*sleep/standby*), koneksi WebSocket (wss://) yang diatur oleh sip_ua ke webrtc gateway akan otomatis diputus oleh sistem operasi. Dokumen add-on ini memberikan panduan kepada developer untuk mengintegrasikan **Firebase Cloud Messaging (FCM)** dan **Flutter CallKit Incoming** guna membangunkan aplikasi dari jarak jauh ketika ada panggilan masuk di PBX 3CX.

apa yang harus di persiapkan? tambahkan paket berikut di bawah dependensi sip_ua:

```
dependencies:
  # ... dependensi sip_ua dan permission_handler yang sudah ada ...

  # DEPENDENSI ADD-ON UNTUK GOOGLE PUSH
  firebase_core: ^3.0.0
  firebase_messaging: ^15.0.0
  flutter_callkit_incoming: ^1.0.0 # Mengaktifkan UI Panggilan penuh di layar saat sleep
```

**Konfigurasi Hak Akses Tambahan OS**

**Android (android/app/src/main/AndroidManifest.xml)**

Tambahkan izin ini di dalam tag <manifest> untuk mendukung push dan pembongkaran status *lockscreen*:

XML

```
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
```

**iOS (ios/Runner/Info.plist)**

Tambahkan kapabilitas latar belakang (*Background Modes*) berikut:

XML

```
<key>UIBackgroundModes</key>
<array>
  <string>voip</string>
  <string>remote-notification</string>
</array>
```

**Langkah 2: Logika Background Handler (main.dart)**

Developer harus mendaftarkan fungsi penanganan pesan latar belakang (*Background Handler*) di file main.dart sebelum fungsi runApp dijalankan. Fungsi ini akan dipanggil oleh sistem operasi Google Play Services secara instan meskipun aplikasi Anda sedang ditutup total.

```
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';

/// Fungsi penanganan paket push yang masuk saat aplikasi MATI/SLEEP total
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();

  // Konfigurasi Parameter UI Layar Panggil Bawaan Sistem HP (CallKit)
  var params = <String, dynamic>{
    'id': message.data['call_id'] ?? '123',
    'nameCaller': message.data['caller_name'] ?? 'Panggilan Hotel',
    'handle': message.data['caller_extension'] ?? '000',
    'type': 0, // 0 = Panggilan Suara (Audio VoIP)
    'duration': 30000, // Waktu tunggu dering (30 detik)
    'android': {
      'isCustomNotification': true,
      'isShowLogo': false,
      'ringtone': 'ringtone_default',
    },
  };

  // Tampilkan antarmuka panggilan masuk di layar penuh
  await FlutterCallkitIncoming.showCallkitIncoming(params);
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Inisialisasi Firebase Core
  await Firebase.initializeApp();

  // Daftarkan background handler resmi Google FCM
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  runApp(const MyApp());
```

**Langkah 3: Mengambil Token FCM Perangkat**

Di dalam komponen UI utama atau saat aplikasi pertama kali mendeteksi user berhasil login/registrasi, token perangkat wajib diambil dan dikirimkan ke server webhook backend (Creomate) Anda.

```
import 'package:firebase_messaging/firebase_messaging.dart';

void dapatkanDanSimpanTokenFCM(String nomorEkstensi) async {
  FirebaseMessaging messaging = FirebaseMessaging.instance;

  // 1. Minta izin Push Notification secara interaktif ke User
  NotificationSettings settings = await messaging.requestPermission(
    alert: true, badge: true, sound: true,
  );

  if (settings.authorizationStatus == AuthorizationStatus.authorized) {
    // 2. Ambil token unik dari server Google
    String? token = await messaging.getToken();
    print("FCM Token untuk Ekstensi $nomorEkstensi: $token");

    // 3. TODO: Developer melakukan HTTP POST ke API Creomate / Database internal
    // untuk memetakan bahwa: Ekstensi 101 = Token XYZ
    // _apiService.simpanTokenKeServer(nomorEkstensi, token);
  }
}
```

**Langkah 4: Sinkronisasi Aksi "Terima Panggilan" dengan sip_ua**

Ketika layar CallKit muncul dan user menekan tombol hijau (**"Accept / Terima"**), aplikasi harus otomatis melalukan inisialisasi ulang WebSocket sip_ua dan mengangkat telepon. Tambahkan listener ini di dalam initState halaman utama Anda:

```
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';

void ikutiAksiCallKit() {
  FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {
    if (event == null) return;

    switch (event.event) {
      case Event.actionCallAccept:
        print("User menekan tombol Terima Telepon!");

        // 1. Jalankan ulang pendaftaran WebSocket ke Janus/3CX secara cepat
        _sipController.connectAndRegister(
          extension: "101",
          authId: "passwordAuthid",
          password: "passwordExtension",
          domain: "pbx.domainanda.com",
          wssUrl: "wss://rtc.yourdomain.com/yourappname",
        ).then((_) {
          // 2. Tambahkan delay kecil/tunggu event INCOMING terpicu di sip_ua,
          // kemudian panggil fungsi answerCall() otomatis.
        });
        break;

      case Event.actionCallDecline:
        print("User menolak panggilan dari layar kunci.");
        // Kirim sinyal reject ke controller jika diperlukan
        break;

      default:
        break;
    }
  });
}
```

[3CX PBX System]

      │

      ▼ (Event: Ada Panggilan Masuk ke Ekstensi Mati)

[Webhook Manager / Creomate]

      │

      ▼ (HTTP POST mencari Token FCM Ekstensi terkait)

[Google FCM Cloud Services]

      │

      ▼ (Sinyal Jalur Prioritas Tinggi / High Priority Push)

[Ponsel Android / iOS (Kondisi Sleep)]

      │

      ├─► Membangunkan '_firebaseMessagingBackgroundHandler'

      ├─► Memicu 'FlutterCallkitIncoming.showCallkitIncoming'

      └─► User Klik Terima ──► WebSocket Terhubung Kembali ──► Audio WebRTC Berjalan
