You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native iOS集成Twilio VoIP推送无法触发CallKit UI,提示缺失twi_message_type字段

React Native iOS集成Twilio VoIP推送无法触发CallKit UI,提示缺失twi_message_type字段

我之前在把Twilio Voice集成到React Native iOS项目时,刚好踩过一模一样的坑!你遇到的问题核心有两个:后端触发呼叫的方式错误,导致Twilio没有生成符合要求的VoIP推送Payload;iOS端的PushKit回调处理逻辑没配合Twilio SDK和CallKit的规则,最终导致SDK报错、CallKit UI不触发还被系统强制杀死App。

问题根源拆解

  1. 用错了Twilio API:你后端现在用Twilio Notify API手动发VoIP推送,但Twilio Voice的呼入推送必须由Twilio的Voice Calls API(而非Notify API)触发。Notify API不会自动添加Twilio Voice SDK要求的保留字段(比如twi_message_type),而这个字段是SDK识别「呼入呼叫」类型的关键——没有它,SDK直接报错,而且CallKit要求收到VoIP推送后必须立刻上报呼叫,否则系统会判定App违规并杀死它。
  2. Payload不符合SDK要求:你当前收到的Payload里缺少twi_message_type: "twilio.voice.incoming",这是Twilio自动生成的字段,手动用Notify API根本加不了(Twilio会拦截带twi_前缀的保留字段)。

分步解决方案

1. 先改后端:用Twilio Calls API触发呼入

立刻停止用Twilio Notify API发VoIP推送,改用Twilio的Create Call API(POST请求到/2010-04-01/Accounts/{你的AccountSid}/Calls.json)来触发呼入。核心参数示例:

From: "client:demoUser"  // 呼叫方的客户端标识
To: "client:你的App用户唯一ID"  // 接收方在Twilio注册的客户端ID
Url: "https://你的服务器域名/your-twiml-endpoint"  // 返回Twiml的接口,用来控制呼叫流程

当你用这个API发起呼叫时,Twilio会自动给目标设备发送包含所有必要字段的VoIP推送,Payload会自带twi_message_type、twi_call_sid等SDK必需的字段,完全不需要你手动添加任何twi_前缀的内容。

2. 调整iOS端的PushKit与Twilio SDK交互逻辑

你现有代码只是把Payload传给了Twilio SDK,但没配合SDK的回调触发CallKit,也没正确处理PushKit的completion时机。

第一步:完善TwilioVoiceModule的Delegate实现

Twilio Voice SDK会通过TVONotificationDelegate回调给你呼入邀请,你需要在这个回调里触发CallKit全屏UI,同时处理前台的横幅通知。修改TwilioVoiceModule.swift:

import TwilioVoice

class TwilioVoiceModule: NSObject, RCTBridgeModule, TVONotificationDelegate {
    private var incomingPushCompletion: (() -> Void)?
    
    // ... 你的现有代码 ...
    
    @objc func handleIncomingPush(_ payload: [String: Any], completion: @escaping () -> Void) {
        print("✅ [TwilioVoiceModule] handleIncomingPush: \(payload)")
        self.incomingPushCompletion = completion
        TwilioVoiceSDK.handleNotification(payload, delegate: self, delegateQueue: nil)
    }
    
    // 实现TVONotificationDelegate的回调
    func notificationReceived(_ callInvite: TVOCallInvite?, error: Error?) {
        // 必须先调用PushKit的completion,避免系统超时判定违规
        defer {
            self.incomingPushCompletion?()
            self.incomingPushCompletion = nil
        }
        
        guard let callInvite = callInvite else {
            print("❌ 呼入邀请解析失败: \(error?.localizedDescription ?? "未知错误")")
            return
        }
        
        // 1. 触发CallKit全屏呼叫UI
        CallKitManager.shared.reportIncomingCall(
            uuid: callInvite.uuid,
            handle: callInvite.from,
            hasVideo: false
        ) { error in
            if let error = error {
                print("❌ 上报CallKit失败: \(error.localizedDescription)")
            }
        }
        
        // 2. 前台时展示横幅通知(可选需求)
        if UIApplication.shared.applicationState == .active {
            let content = UNMutableNotificationContent()
            content.title = "Incoming Call"
            content.body = "你收到来自 \(callInvite.from) 的呼叫"
            content.sound = .default
            let request = UNNotificationRequest(
                identifier: UUID().uuidString,
                content: content,
                trigger: nil
            )
            UNUserNotificationCenter.current().add(request)
        }
        
        // 3. 把呼入事件桥接到React Native端
        self.sendEvent(withName: "callIncoming", body: [
            "callSid": callInvite.callSid,
            "from": callInvite.from,
            "uuid": callInvite.uuid.uuidString
        ])
    }
}
第二步:调整AppDelegate的PushKit回调

把PushKit的completion回调传递给TwilioVoiceModule,确保在处理完呼叫逻辑后再执行:

func pushRegistry( _ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void ) {
    let payloadDict = payload.dictionaryPayload
    let stringKeyedPayload = payloadDict.reduce(into: [String: Any]()) { (result, entry) in
        result[entry.key] = entry.value
    }
    print("📲 [AppDelegate] VoIP push payload: \(stringKeyedPayload)")
    
    if let bridge = self.bridge, let module = bridge.module(forName: "TwilioVoiceModule") as? TwilioVoiceModule {
        // 把completion传给模块处理
        module.handleIncomingPush(stringKeyedPayload, completion: completion)
    } else {
        // 降级处理,避免阻塞PushKit流程
        completion()
    }
}
第三步:添加CallKit管理类

你需要一个单例类来统一处理CallKit的呼叫上报、接听、挂断等操作,示例:

import CallKit

class CallKitManager: NSObject {
    static let shared = CallKitManager()
    private let callController = CXCallController()
    private let provider: CXProvider
    
    override init() {
        let config = CXProviderConfiguration(localizedName: "你的App名称")
        config.supportsVideo = false
        config.maximumCallsPerCallGroup = 1
        config.supportedHandleTypes = [.generic]
        // 可选:设置CallKit的图标
        if let icon = UIImage(named: "call-icon")?.pngData() {
            config.iconTemplateImageData = icon
        }
        provider = CXProvider(configuration: config)
        super.init()
        provider.setDelegate(self, queue: nil)
    }
    
    // 上报呼入给CallKit
    func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool, completion: @escaping (Error?) -> Void) {
        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .generic, value: handle)
        update.hasVideo = hasVideo
        update.supportsDTMF = true
        update.supportsHolding = true
        
        provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
    }
}

// 实现CXProviderDelegate处理用户在CallKit的操作
extension CallKitManager: CXProviderDelegate {
    func providerDidReset(_ provider: CXProvider) {
        // 重置逻辑,比如清理当前呼叫状态
    }
    
    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        // 用户点击接听,通知TwilioVoiceModule处理接听逻辑
        action.fulfill()
    }
    
    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        // 用户点击挂断,通知TwilioVoiceModule处理挂断逻辑
        action.fulfill()
    }
}

3. 验证正确的Payload格式

当后端用Calls API触发呼叫后,你收到的Payload应该是Twilio自动生成的如下格式:

{
  "aps": {
    "alert": {
      "title": "Incoming Call",
      "body": "You have a call from client:demoUser"
    },
    "sound": "default"
  },
  "twi_message_type": "twilio.voice.incoming",
  "twi_message_id": "RU*********************************f0445",
  "twi_call_sid": "CA*********************************12345",
  "from": "demoUser",
  "to": "你的App用户唯一ID"
}

最后检查要点

  • 确保iOS端的Twilio Voice SDK版本和React Native版本兼容(你用的6.12.1和0.78.2是兼容的)
  • 确认PushKit的voip类型推送证书已经正确上传到Twilio后台
  • 验证React Native端的callIncoming事件监听器能正常收到原生桥接的事件

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 13:08:10