Call Lifecycle
Use lifecycle callbacks to keep call UI synchronized with real-time call progress. The diagrams below show the two main perspectives: placing a call and receiving one.
Outbound Call (Caller Perspective)
When your app calls serverCall(), the callee leg progresses through status updates reported by onLegStatusUpdate.
Inbound Call (Callee Perspective)
When your app receives a call invite, the local call transitions through invite handling into an active call.
Callback Sources
onLegStatusUpdatereports callee leg progression:Ringing,Answered,Completed. Use this to update outbound call UI (e.g. show "Ringing..." then "Connected").onCallInvite/onCallInviteCancelhandle inbound invite lifecycle. See Receive an inbound call for cancel reason details.onCallHangupfires when the call ends for any reason, on both outbound and inbound calls. Always use this as the final cleanup trigger.onCallMediaDisconnect/onCallMediaReconnecting/onCallMediaReconnectionreport media-layer interruptions. See Reconnection for details.
Status Updates
onLegStatusUpdate provides callee leg progression (for example: ringing -> answered -> completed). This is most relevant for outbound calls where you want to reflect the callee's state in your UI.
client.on('legStatusUpdate', (callId, legId, legStatus) => {
if (legStatus === LegStatus.Answered) {
console.log(`Call ${callId} was answered by ${legId}`);
} else if (legStatus === LegStatus.Completed) {
console.log(`Call ${callId} was completed`);
} else if (legStatus === LegStatus.Ringing) {
console.log(`Call ${callId} is ringing`);
}
});
client.setOnLegStatusUpdate { call, legId, status ->
when(status) {
LegStatus.ringing -> println("Leg $legId for call $call is ringing")
LegStatus.answered -> println("Leg $legId for call $call was answered")
LegStatus.completed -> println("Leg $legId for call $call was completed")
}
}
client.setOnLegStatusUpdate((call, legId, status) -> {
switch (status) {
case ringing:
System.out.println("Leg " + legId + " for call " + call + " is ringing");
break;
case answered:
System.out.println("Leg " + legId + " for call " + call + " was answered");
break;
case completed:
System.out.println("Leg " + legId + " for call " + call + " was completed");
break;
}
return null;
});
class CallStatusExampleDelegate: NSObject, VGVoiceClientDelegate {
func voiceClient(_ client: VGVoiceClient, didReceiveLegStatusUpdateForCall callId: VGCallId, withLegId legId: VGCallId, andStatus status: VGLegStatus) {
switch(status){
case .ringing:
print("Leg \(legId) is ringing")
case .answered:
print("Leg \(legId) has answered")
case .completed:
print("Leg \(legId) has completed")
default:
print("Leg has entered the Unknown")
}
}
func voiceClient(_ client: VGVoiceClient, didReceiveInviteForCall callId: VGCallId, from caller: String, with type: VGVoiceChannelType) {}
func voiceClient(_ client: VGVoiceClient, didReceiveHangupForCall callId: VGCallId, withQuality callQuality: VGRTCQuality, reason: VGHangupReason) {}
func voiceClient(_ client: VGVoiceClient, didReceiveInviteCancelForCall callId: VGCallId, with reason: VGVoiceInviteCancelReason) {}
func client(_ client: VGBaseClient, didReceiveSessionErrorWith reason: VGSessionErrorReason) {}
}
- (void) voiceClient:(VGVoiceClient *)client didReceiveLegStatusUpdateForCall:(VGCallId)callId withLegId:(NSString *)legId andStatus:(VGLegStatus)status {
switch (status) {
case VGLegStatusRinging:
NSLog(@"Leg %@ is ringing", legId);
break;
case VGLegStatusAnswered:
NSLog(@"Leg %@ has answered", legId);
break;
case VGLegStatusCompleted:
NSLog(@"Leg %@ has completed", legId);
break;
case VGLegStatusUnknown:
NSLog(@"Leg has entered the Unknown");
break;
}
}
Initiating Hangup
Use hangup to terminate your call.
const callId = 'callId';
await client.hangup(callId);
// Callback
client.hangup("callId") {
it?.let { err ->
// Handle Error hanging up call
println("Error hanging up call: $err")
}
}
// Coroutine
try {
client.hangup("callId")
} catch (e: Error) {
// Handle Error hanging up call
}
client.hangup("callId", err -> {
if (err != null) {
// Handle Error hanging up call
System.out.println("Error hanging up call: " + err);
}
return null;
});
// Callback
client.hangup("someCallId") { error in
guard let error = error else {
print("Call hangup successful")
return
}
print("Failed to hangup \(error.localizedDescription)")
}
// Async Await
Task.detached {
do {
// Simple Case
try await client.hangup("someCallId")
// Fully Parameter list
try await client.hangup("someCallId", reason: "reason-text", code: "reason-code")
}
catch {
print("Failed to say hangup' \(error.localizedDescription)")
}
}
[client hangup:@"someCallid" callback:^(NSError * _Nullable error) {
if(error){
NSLog(@"Failed to hangup: %@", error);
}
}];
Handling Hangup Errors
hangup() can fail if the call has already ended, the session expired, or there is a network issue. Always handle the error to ensure your UI transitions to the ended state regardless:
try {
await client.hangup(callId);
} catch (error) {
console.error("Hangup error:", error.type, error.message);
} finally {
// Always clean up — if hangup failed locally, the server will
// eventually terminate the call via mediaTimeout
showCallEndedUI();
}
client.hangup(callId) { error ->
if (error != null) {
// Hangup request failed — but the call may still end via server-side timeout.
// Clean up local UI anyway to avoid a stuck call screen.
println("Hangup error: ${error.message}")
}
// Always transition to end-of-call UI
showCallEndedUI()
}
client.hangup(callId) { error in
if let error = error {
print("Hangup error: \(error.localizedDescription)")
}
self.showCallEndedUI()
}
Even if hangup() fails, always clean up your local call state and UI. The server will terminate the call independently via timeout if the client-side hangup did not reach it. Waiting for onCallHangup as a secondary confirmation is good practice.
Receiving Hangup Events
Use onCallHangup to handle final call teardown and UI cleanup.
client.on(
'callHangup',
(callId: string, callQuality: RTCQuality, reason: HangupReason) => {
if (reason == 'LOCAL_HANGUP') {
console.log(`Call ${callId} was hung up locally`);
} else if (reason == 'REMOTE_HANGUP') {
console.log(`Call ${callId} was hung up remotely`);
} else if (reason == 'REMOTE_REJECT') {
console.log(`Call ${callId} was rejected remotely`);
} else if (reason == 'REMOTE_NO_ANSWER_TIMEOUT') {
console.log(`Call ${callId} timed out`);
} else if (reason == 'MEDIA_TIMEOUT') {
console.log(`Call ${callId} timed out`);
} else {
exhaustiveCheck(reason);
}
}
);
client.setOnCallHangupListener { call, callQuality, reason ->
println("Received $reason callQuality: $callQuality for Call $call")
}
client.setOnCallHangupListener((call, legId, reason) -> {
System.out.println("Leg " + legId + " for call " + call + " was hung up for reason " + reason);
return null;
});
class HangupExampleDelegate: NSObject, VGVoiceClientDelegate {
func voiceClient(_ client: VGVoiceClient, didReceiveHangupForCall callId: VGCallId, withQuality callQuality: VGRTCQuality, reason: VGHangupReason) {
print("call: \(callId) has finished")
}
func voiceClient(_ client: VGVoiceClient, didReceiveInviteForCall callId: VGCallId, from caller: String, with type: VGVoiceChannelType) {}
func voiceClient(_ client: VGVoiceClient, didReceiveInviteCancelForCall callId: VGCallId, with reason: VGVoiceInviteCancelReason) {}
func client(_ client: VGBaseClient, didReceiveSessionErrorWith reason: VGSessionErrorReason) {}
}
- (void)voiceClient:(VGVoiceClient *)client didReceiveHangupForCall:(VGCallId)callId withQuality:(VGRTCQuality *)callQuality andReason:(VGHangupReason)reason {
NSLog(@"Call %@ has finished with reason: %ld and quality: %@", callId, reason, callQuality);
}
Debugging lifecycle events
If call events are not arriving as expected, enable verbose logging during development to trace lifecycle transitions. See Logging and diagnostics for full configuration options including custom loggers and topic filtering.
NCCO hangup behavior
connectNCCO behaves like standard two-party calling — when one party hangs up, the point-to-point connection ends and both legs are terminated.conversationNCCO behavior is conversation-scoped — hanging up only removes that participant's leg from the shared room. Other participants remain connected until they leave individually.