placeCall method
Place a call to to — a Nexus identity (app), a phone number (pstn) or a
SIP URI. This device joins as a WebRTC leg; the control plane dials to.
Implementation
Future<NexusCall?> placeCall({
required String to,
VoiceEndpointType type = VoiceEndpointType.app,
String? callerId,
String? displayName,
String? callerDisplayName,
String? callerAvatarUrl,
bool record = false,
Map<String, dynamic> metadata = const {},
}) async {
try {
if (!await _ensureMicPermission()) return _fail(null, 'microphone_denied');
// The name + avatar the CALLEE sees for us (rings + call screen). Ride in
// session metadata → the backend puts them in the ring payload as
// `callerName` / `callerAvatar`.
final md = <String, dynamic>{
...metadata,
if (callerDisplayName != null && callerDisplayName.isNotEmpty) 'callerName': callerDisplayName,
if (callerAvatarUrl != null && callerAvatarUrl.isNotEmpty) 'callerAvatar': callerAvatarUrl,
};
final session = await _post('/partner/voice/calls', {
'direction': 'outbound',
'recordingMode': record ? 'mixed' : 'disabled',
if (md.isNotEmpty) 'metadata': md,
});
final sessionId = _id(session?['session']);
if (sessionId == null) return _fail(null, 'session_create_failed');
_set(NexusCall(
sessionId: sessionId,
direction: VoiceCallDirection.outbound,
state: VoiceCallState.connecting,
remoteAddress: to,
remoteName: displayName,
startedAt: DateTime.now(),
metadata: metadata,
));
// On ANDROID we deliberately do NOT start a native outgoing call: a
// self-managed outgoing ConnectionService can auto-end and echo back an
// "ended" event that would tear down a healthy call, and a foreground
// outbound call is driven by the app's own UI + the media engine.
// On iOS the opposite is true and mandatory: CallKit only activates the
// audio session for a call it knows about, so an unreported outbound call
// would connect with no audio at all.
if (!kIsWeb && Platform.isIOS) {
unawaited(_guard(() => _callKit.reportOutgoing(
callId: sessionId,
handle: to,
displayName: displayName ?? callerDisplayName,
)));
}
// 1) This device's WebRTC leg → join the media room. Carry our identity so
// the control plane can tell the callee WHO is calling (the ring's `from`).
final myLeg = await _post('/partner/voice/legs', {
'sessionId': sessionId,
'role': 'caller',
'endpointType': 'webrtc',
'direction': 'outbound',
'identityId': _identityId,
});
final legId = _id(myLeg?['leg']);
final token = NexusJoinToken.tryParse(myLeg?['join'] as Map<String, dynamic>?);
if (myLeg?['error'] != null) {
NexusLog.warn('voice: backend could not provision media (${myLeg!['error']}) — '
'is LiveKit configured + reachable from the backend?');
return _fail(sessionId, myLeg['error'].toString());
}
if (legId == null || token == null) return _fail(sessionId, 'join_failed');
NexusLog.info('voice: joining media room "${token.room}" at ${token.url}');
_set(current.value!.copyWith(legId: legId, state: VoiceCallState.ringing));
try {
await _engine.connect(token);
} catch (e) {
NexusLog.error('voice: media connect failed to ${token.url} — $e. '
'The DEVICE must be able to reach the media server; a private LAN IP is '
'unreachable over a tunnel / cellular / different Wi-Fi.');
return _fail(sessionId, 'media_connect_failed');
}
// 2) Dial the remote party (app/pstn/sip).
final callee = await _post('/partner/voice/legs', {
'sessionId': sessionId,
'role': 'callee',
'endpointType': type.name,
'direction': 'outbound',
'address': to,
'callerId': ?callerId,
if (type == VoiceEndpointType.app) 'identityId': to,
});
if (callee?['error'] != null) return _fail(sessionId, callee!['error'].toString());
_startPresence();
_armRingTimeout(sessionId); // auto-end if the callee never answers
return current.value;
} catch (e) {
NexusLog.error('voice.placeCall failed: $e');
return _fail(current.value?.sessionId, 'error');
}
}