init method

Future<void> init()

Turnkey setup — called automatically when voiceEnabled. Wires the SDK's BUILT-IN WebRTC engine + native call UI (CallKit/ConnectionService) and registers this device for incoming-call push so calls ring even when the app is killed. Developers do NOT call this.

Implementation

Future<void> init() async {
  if (_initialised) return;
  _initialised = true;
  if (_engine is NoopVoiceEngine) useEngine(LiveKitVoiceEngine());
  if (_callKit is NoopCallKit) useCallKit(NexusNativeCallKit());
  // Register our OWN native call account so incoming calls ring in the system
  // UI and are answered natively: a self-managed Telecom PhoneAccount on
  // Android, a CXProvider + PushKit VoIP registry on iOS. One contract, one
  // Dart code path.
  if (_hasNativeCallUi) {
    unawaited(NexusPlatform.instance.voiceRegisterAccount());
    // Answer/decline taken in the NATIVE call UI (Android's notification /
    // lock-screen ring, iOS's CallKit screen). On answer, connect media in
    // THIS (main) engine — the proven path on both platforms.
    NexusPlatform.instance.onNativeCallEvent((action, callId) {
      NexusLog.info('voice: native call action=$action call=$callId');
      if (action == 'answer') {
        unawaited(answer(callId));
      } else if (action == 'reject' || action == 'disconnect') {
        unawaited(hangup());
      }
    });
  }
  if (!kIsWeb && Platform.isIOS) {
    // CallKit owns the AVAudioSession: LiveKit must never activate it, and its
    // audio engine may only run inside CallKit's activate/deactivate window.
    unawaited(NexusCallAudio.prepare());
    NexusPlatform.instance.onNativeCallAudioSession(
      (active) => unawaited(NexusCallAudio.setActive(active)),
    );
    // The ring itself is raised natively from the APNs VoIP payload, before
    // Dart wakes up. This is that same payload — so the call state, the caller
    // name and a decline-before-answer all behave exactly as on Android.
    NexusPlatform.instance.onNativeVoicePush((data) {
      if (data['type'] == 'cancel_call') {
        // Native already turned the ring into a missed call — reconcile state.
        _onRemoteEnd(data, VoiceCallState.cancelled, 'cancelled');
      } else {
        unawaited(handleIncomingPush(data, alreadyRinging: true));
      }
    });
    // PushKit hands us the VoIP token asynchronously (and rotates it) —
    // register it the moment it lands so a killed app can be rung.
    NexusPlatform.instance.onNativeVoipToken((t) => unawaited(registerPushToken(voipToken: t)));
  }
  _listenForIncoming();
  // Mic permission MUST already be granted BEFORE an incoming call arrives: a
  // call is answered from a killed/background/cold-launch context where the OS
  // will NOT show a runtime permission dialog, so requesting it at answer time
  // silently fails and the callee never joins audio (the caller sees "no
  // answer"). Pre-warm it now — we're in the foreground at startup, the one
  // moment the dialog can actually appear. WhatsApp does the same up front.
  unawaited(_prewarmMic());
  // …and again whenever the app comes to the foreground, which is the first
  // moment a call answered from a background launch can still get the mic.
  try {
    _lifecycle = AppLifecycleListener(onResume: () => unawaited(_prewarmMic()));
  } catch (e) {
    NexusLog.debug('voice: lifecycle listener unavailable: $e');
  }
  // A cold launch driven by ACCEPTING a call in the native UI needs no polling:
  // the platform channel buffers a native "answer" raised before the listener
  // above attached and replays it the instant it does — on both platforms.
  unawaited(_registerDevice());
}