init method
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());
}