着信を受ける

Vonage API が応答 Webhook フローを実行した後、着信はアクティブなセッションを持つクライアントに配信されます。

インバウンドパス

警告:まずリスナーを登録してください コールの招待を登録するリスナー 以前 セッションを作成または再接続することで、受信した招待を見逃さないようにします。

着信コールの招待への対応

client.on('callInvite', async (callId, from, channelType) => {
  console.log(`Received call invite from ${from} on ${channelType}`);

  // To accept the call:
  await client.answer(callId);

  // Or to reject the call:
  // await client.reject(callId);
});

client.on('callInviteCancel', (callId, reason) => {
  if (reason === CancelReason.AnsweredElsewhere) {
    console.log(`Call ${callId} was answered elsewhere`);
  } else if (reason === CancelReason.RejectedElsewhere) {
    console.log(`Call ${callId} was rejected elsewhere`);
  } else if (reason === CancelReason.RemoteCancel) {
    console.log(`Call ${callId} was cancelled by the caller`);
  } else if (reason === CancelReason.RemoteTimeout) {
    console.log(`Call ${callId} timed out`);
  }
});

 // Answer Call
 client.setCallInviteListener {callId, from, channelType ->
     println("Incoming Call from $from by channel $channelType")
     client.answer(callId) {
         it?.let {err ->
             // Handle Error in answering call
             println("Error in answering call: $err")
         }
     }
 }

 // Or reject the call:
 // client.setCallInviteListener { callId, from, channelType ->
 //     client.reject(callId) {
 //         it?.let {err ->
 //             println("Error in rejecting call: $err")
 //         }
 //     }
 // }

 // Invite Cancelled
 client.setCallInviteCancelListener { callId, reason ->
         when (reason) {
             VoiceInviteCancelReason.RemoteCancel -> println("Call Invite for call $callId was cancelled by the caller")
             VoiceInviteCancelReason.AnsweredElsewhere -> println("Call Invite for call $callId was answered on another device")
             VoiceInviteCancelReason.RejectedElsewhere -> println("Call Invite for call $callId was rejected on another device")
             VoiceInviteCancelReason.RemoteTimeout -> println("Call Invite for call $callId timed out")
         }
     }

 // Answer Call
 client.setCallInviteListener((callId, from, channelType) -> {
     System.out.println("Incoming Call from " + from + " by channel " + channelType);
     client.answer(callId, it -> {
         if (it != null) {
             // Handle Error in answering call
             System.out.println("Error in answering call: " + it);
         }
         return null;
     });
     return null;
 });

 // Or reject the call:
 // client.setCallInviteListener((callId, from, channelType) -> {
 //     client.reject(callId, it -> {
 //         if (it != null) {
 //             System.out.println("Error in rejecting call: " + it);
 //         }
 //         return null;
 //     });
 //     return null;
 // });

 // Invite Cancelled
 client.setCallInviteCancelListener((callId, reason) -> {
     switch (reason) {
         case RemoteCancel:
             System.out.println("Call Invite for call " + callId + " was cancelled by the caller");
             break;
         case AnsweredElsewhere:
             System.out.println("Call Invite for call " + callId + " was answered on another device");
             break;
         case RejectedElsewhere:
             System.out.println("Call Invite for call " + callId + " was rejected on another device");
             break;
         case RemoteTimeout:
             System.out.println("Call Invite for call " + callId + " timed out");
             break;
     }
     return null;
 });
 
 class inboundCallExampleDelegate: NSObject, VGVoiceClientDelegate {
     
     
     func voiceClient(_ client: VGVoiceClient, didReceiveInviteForCall callId: VGCallId, from caller: String, with type: VGVoiceChannelType) {
         print(caller)
         // Answer Invite
         client.answer(callId) { error in
             guard error != nil else {
                 print("Failed to answer \(error?.localizedDescription ?? "")")
                 return
             }
         }
         // Or reject the invite:
         // client.reject(callId) { error in
         //     guard error != nil else {
         //         print("Failed to reject \(error?.localizedDescription ?? "")")
         //         return
         //     }
         // }
     }
     func voiceClient(_ client: VGVoiceClient, didReceiveInviteCancelForCall callId: VGCallId, with reason: VGVoiceInviteCancelReason) {
         guard callId == "currentCallId" else {
             return
         }
         
         switch reason {
         case .answeredElsewhere:
             print("Call answered elsewhere")
         case .remoteCancel:
             print("Call cancelled by remote")
         case .rejectedElsewhere:
             print("Call rejected elsewhere")
         case .remoteTimeout:
             print("Call timed out")
         default:
             print("reason unknown")
         }
     }
     
     func client(_ client: VGBaseClient, didReceiveSessionErrorWith reason: VGSessionErrorReason) {}
     func voiceClient(_ client: VGVoiceClient, didReceiveHangupForCall callId: VGCallId, withQuality callQuality: VGRTCQuality, reason: VGHangupReason) {}
 }
 

 - (void) voiceClient:(VGVoiceClient *)client didReceiveInviteForCall:(VGCallId)callId from:(NSString *)caller withChannelType:(VGVoiceChannelType)type {
     NSLog(@"Caller: %@", caller);
     // Answer Invite
     [client answer:callId callback:^(NSError * _Nullable error) {
         if(error){
             NSLog(@"Failed to answer: %@", error);
         }
     }];
     // Or reject the invite:
     // [client reject:callId callback:^(NSError * _Nullable error) {
     //     if(error){
     //         NSLog(@"Failed to reject: %@", error);
     //     }
     // }];
 }

 - (void) voiceClient:(VGVoiceClient *)client didReceiveInviteCancelForCall:(VGCallId)callId withReason:(VGVoiceInviteCancelReason)reason {
     if(![callId isEqualToString:@"currentCallId"]){
         return;
     }
     switch(reason) {
         case VGVoiceInviteCancelReasonAnsweredElsewhere:
             NSLog(@"Call answered elsewhere");
             break;
         case VGVoiceInviteCancelReasonRemoteCancel:
             NSLog(@"Call canceled by remote");
             break;
         case VGVoiceInviteCancelReasonRejectedElsewhere:
             NSLog(@"Call rejected elsewhere");
             break;
         case VGVoiceInviteCancelReasonRemoteTimeout:
             NSLog(@"Call timed out");
             break;
         case VGVoiceInviteCancelReasonUnknown:
             NSLog(@"Reason Unknown");
             break;
     }
 }

招待のキャンセル時の動作

招待は、ユーザーが操作を行う前にキャンセルされたり、タイムアウトしたりすることがあります。アプリには、以下のいずれかの理由が指定されたキャンセルコールバックが送信されます:

理由 説明
RemoteCancel 招待に応じる前に、相手は電話を切ってしまった。
RemoteTimeout ユーザーが何も操作を行わなかったため、Vonage API での招待の有効期限が切れました。
AnsweredElsewhere そのユーザーが、別のデバイスまたはクライアントインスタンスで通話に応答しました。
RejectedElsewhere そのユーザーが、別のデバイスまたはクライアントインスタンスでその着信を拒否しました。

UXガイドライン

  • キャンセル理由が発生した場合は、着信UIを直ちに終了する。
  • 電話をかけようとしないでください answer() または reject() キャンセル通知を受け取った後、その招待は有効ではなくなります。
  • 複数のデバイスが関与するシナリオでは(AnsweredElsewhere / RejectedElsewhere)、着信音が止まった理由をユーザーが理解できるよう、短いトーストメッセージや視覚的なインジケーターを表示することを検討してください。

エラー処理

answer() そして reject() セッションの有効期限が切れている場合、招待が失効している(すでにキャンセルされている)場合、またはネットワーク上の問題がある場合、処理が失敗することがあります。必ずエラーコールバックを処理し、それに応じてUIを更新してください:

try {
  await client.answer(callId);
  showActiveCallUI();
} catch (error) {
  console.error("Answer failed:", error.type, error.message);
  dismissIncomingCallUI();
}

try {
  await client.reject(callId);
} catch (error) {
  console.error("Reject failed:", error.type, error.message);
} finally {
  dismissIncomingCallUI();
}
client.answer(callId) { error ->
    if (error != null) {
        // Answer failed — dismiss call UI and inform the user
        println("Answer failed: ${error.message}")
        // Common causes:
        // - Session expired (re-create session and retry)
        // - Invite already cancelled by caller
        dismissIncomingCallUI()
        return@answer
    }
    // Success — transition to active call UI
    showActiveCallUI()
}

client.reject(callId) { error ->
    if (error != null) {
        // Reject failed — the invite may have already been cancelled
        println("Reject failed: ${error.message}")
    }
    // In both success and failure, dismiss the incoming call UI
    dismissIncomingCallUI()
}
client.answer(callId) { error in
    if let error = error {
        print("Answer failed: \(error.localizedDescription)")
        self.dismissIncomingCallUI()
        return
    }
    self.showActiveCallUI()
}

client.reject(callId) { error in
    if let error = error {
        print("Reject failed: \(error.localizedDescription)")
    }
    self.dismissIncomingCallUI()
}

もし answer() セッションの有効期限が切れたために失敗した場合は、新しいJWTを使用してセッションを再作成し、回答を再試行してください。招待がすでにキャンセルされている場合(発信者が通話を切った場合)、復旧は不可能です。その場合はUIを閉じ、次の招待を待ってください。

さらに読む

  • NCCOリファレンス — 回答ウェブフックが返すJSON構造に関する完全なドキュメント