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

Flutter iOS端CallKit唤醒异常与WebRTC音频静默问题排查

背景与架构

开发一款Flutter iOS应用,结合PushKit/VoIP通知与flutter_callkit_incoming管理原生通话界面。通话连接成功后,跳转至自定义CallScreen页面,该页面嵌入托管在flutter_inappwebview容器中的ZegoCloud WebRTC音视频界面。应用后端基于Node.js/MongoDB,通过ngrok隧道路由载荷。

核心问题

应用在后台/被杀状态下处理来电时,出现两类故障:

场景1:应用唤醒但音视频流完全静默

后台/被杀状态收到VoIP通知,点击Accept可成功唤醒应用并跳转至CallScreen,CallKit原生UI与应用界面均显示在系统卡片滑块中(原生电话应用可准确记录通话时长),但无任何音视频流传输,WebView层无法捕获麦克风/音频输入。

场景2:应用无法从后台/被杀状态唤醒

接受来电会触发actionCallAccept监听器,但应用无法进入前台或加载WebView。

注:应用处于前台时,接收通知后功能完全正常:WebView加载通话房间,双向媒体流运行流畅。此场景下系统通话记录显示时长为0 seconds,因为应用会立即调用endCall信号清除覆盖窗口。

场景1日志

[YOUR_APP] push payload: [AnyHashable("roomName"): ROOM_UUID_PLACEHOLDER, AnyHashable("type"): incoming_call, AnyHashable("callerName"): USER_NAME_PLACEHOLDER, AnyHashable("id"): CALL_UUID_PLACEHOLDER]
flutter: [YOUR_APP] event: Event.actionCallIncoming
flutter: [YOUR_APP] event body: {extra: {roomName: ROOM_UUID_PLACEHOLDER, callerName: USER_NAME_PLACEHOLDER}, isAccepted: false, missedCallNotification: {subtitle: Missed Call, callbackText: Call back, isShowCallback: true, showNotification: true}, handle: , uuid: CALL_UUID_PLACEHOLDER, duration: 30000, normalHandle: 0, id: CALL_UUID_PLACEHOLDER, type: 0, avatar: , ios: {audioSessionActive: true, supportsHolding: true, maximumCallGroups: 2, maximumCallsPerCallGroup: 1, handleType: generic, audioSessionPreferredIOBufferDuration: 0.005, supportsGrouping: true, configureAudioSession: true, audioSessionMode: , supportsUngrouping: true, ringtonePath: , audioSessionPreferredSampleRate: 44100.0, supportsDTMF: true, iconName: AppLogo, supportsVideo: false, includesCallsInRecents: true}, nameCaller: USER_NAME_PLACEHOLDER, appName: Callkit}
flutter ns Flutter exiting
flutter ns Flutter exiting
Error Domain=NSOSStatusErrorDomain Code=561017449 "Session activation failed" UserInfo={NSLocalizedDescription=Session activation failed}
flutter: [YOUR_APP] event: Event.actionCallAccept
flutter: [YOUR_APP] event body: {avatar: , isAccepted: true, extra: {roomName: ROOM_UUID_PLACEHOLDER, callerName: USER_NAME_PLACEHOLDER}, nameCaller: USER_NAME_PLACEHOLDER, normalHandle: 0, missedCallNotification: {subtitle: Missed Call, showNotification: true, callbackText: Call back, isShowCallback: true}, id: CALL_UUID_PLACEHOLDER, uuid: CALL_UUID_PLACEHOLDER, appName: Callkit, duration: 30000, ios: {supportsVideo: false, maximumCallsPerCallGroup: 1, supportsHolding: true, maximumCallGroups: 2, ringtonePath: , iconName: AppLogo, supportsGrouping: true, audioSessionMode: , audioSessionPreferredSampleRate: 44100.0, supportsUngrouping: true, audioSessionActive: true, audioSessionPreferredIOBufferDuration: 0.005, handleType: generic, configureAudioSession: true, supportsDTMF: true, includesCallsInRecents: true}, type: 0, handle: }
flutter: [YOUR_APP] event: Event.actionCallToggleAudioSession
flutter: [YOUR_APP] event body: {isActivate: true}
WebContent[2864] xpc_user_sessions_get_foreground_uid() failed with error 1 - Operation not permitted
WebContent[2864] AudioSession::endInterruption but session is already uninterrupted!
WebContent[2864] AppleAVD: ERROR: GetSupportBitsFromIORegistry(): IOServiceGetMatchingServices failed! (error = 53)
WebContent[2864] AppleAVD: ERROR: AppleAVDCheckPlatform(): Failed to successfully retrieve support bits! (error = 53)
WebContent[2864] AppleAVD: ERROR: AVDRegister(): AppleAVDCheckPlatform() returned error 53
WebContent[2864] ERROR AppleProResHW: 48: AppleProResHW_CheckPlatform(): IOServiceGetMatchingService failed
WebContent[2864] xpc_user_sessions_get_foreground_uid() failed with error 1 - Operation not permitted
Failed to terminate process: Error Domain=com.apple.extensionKit.errorDomain Code=18 "(null)" UserInfo={NSUnderlyingError=0x1243a0840 {Error Domain=RBSRequestErrorDomain Code=3 "No such process found" UserInfo={NSLocalizedFailureReason=No such process found}}}
flutter: [YOUR_APP] event: Event.actionCallEnded
flutter: [YOUR_APP] event body: {avatar: , isAccepted: true, extra: {roomName: ROOM_UUID_PLACEHOLDER, callerName: USER_NAME_PLACEHOLDER}, nameCaller: USER_NAME_PLACEHOLDER, normalHandle: 0, missedCallNotification: {callbackText: Call back, subtitle: Missed Call, isShowCallback: true, showNotification: true}, id: CALL_UUID_PLACEHOLDER, uuid: CALL_UUID_PLACEHOLDER, appName: Callkit, duration: 30000, ios: {audioSessionActive: true, supportsUngrouping: true, supportsGrouping: true, audioSessionPreferredSampleRate: 44100.0, audioSessionPreferredIOBufferDuration: 0.005, includesCallsInRecents: true, iconName: AppLogo, maximumCallsPerCallGroup: 1, maximumCallGroups: 2, configureAudioSession: true, supportsHolding: true, audioSessionMode: , ringtonePath: , handleType: generic, supportsVideo: false, supportsDTMF: true}, type: 0, handle: }
"FlutterWebViewController - dealloc"
Requested transaction successfully: endCall
"WebViewChannelDelegate - dealloc"
"FindInteractionControl - dealloc"
"InAppWebView - dealloc"
"PullToRefreshControl - dealloc"
Requested transaction successfully: endCall
flutter: [YOUR_APP] event: Event.actionCallEnded
flutter: [YOUR_APP] event body: {extra: {roomName: ROOM_UUID_PLACEHOLDER, callerName: USER_NAME_PLACEHOLDER}, isAccepted: true, missedCallNotification: {subtitle: Missed Call, showNotification: true, isShowCallback: true, callbackText: Call back}, handle: , uuid: CALL_UUID_PLACEHOLDER, duration: 30000, normalHandle: 0, id: CALL_UUID_PLACEHOLDER, type: 0, avatar: , ios: {audioSessionActive: true, supportsHolding: true, maximumCallGroups: 2, maximumCallsPerCallGroup: 1, handleType: generic, audioSessionPreferredIOBufferDuration: 0.005, supportsGrouping: true, configureAudioSession: true, audioSessionMode: , supportsUngrouping: true, ringtonePath: , audioSessionPreferredSampleRate: 44100.0, supportsDTMF: true, iconName: AppLogo, supportsVideo: false, includesCallsInRecents: true}, nameCaller: USER_NAME_PLACEHOLDER, appName: Callkit}
flutter: [YOUR_APP] event: Event.actionCallTimeout
flutter: [YOUR_APP] event body: {avatar: , isAccepted: false, extra: {}, nameCaller: , normalHandle: 0, missedCallNotification: {subtitle: Missed Call, showNotification: true, isShowCallback: true, callbackText: Call back}, id: CALL_UUID_PLACEHOLDER, uuid: CALL_UUID_PLACEHOLDER, appName: Callkit, duration: 30000, ios: {configureAudioSession: true, supportsHolding: true, supportsVideo: true, iconName: AppLogo, maximumCallsPerCallGroup: 1, audioSessionPreferredSampleRate: 44100.0, includesCallsInRecents: true, audioSessionMode: , handleType: , supportsUngrouping: true, audioSessionPreferredIOBufferDuration: 0.005, supportsGrouping: true, ringtonePath: , audioSessionActive: true, maximumCallGroups: 2, supportsDTMF: true}, type: 0, handle: }
Error requesting transaction: Error Domain=com.apple.CallKit.error.requesttransaction Code=4 "(null)"
Error requesting transaction: Error Domain=com.apple.CallKit.error.requesttransaction Code=4 "(null)"
flutter: [YOUR_APP] event: Event.actionCallToggleAudioSession
flutter: [YOUR_APP] event body: {isActivate: false}
Failed to terminate process: Error Domain=com.apple.extensionKit.errorDomain Code=18 "(null)" UserInfo={NSUnderlyingError=0x1241b3570 {Error Domain=RBSRequestErrorDomain Code=3 "No such process found" UserInfo={NSLocalizedFailureReason=No such process found}}}

问题排查与解决方案

针对场景1:音视频流静默

原因分析

  1. 日志中Error Domain=NSOSStatusErrorDomain Code=561017449 "Session activation failed"表明AudioSession激活失败,WebView进程无权限访问音频硬件。
  2. xpc_user_sessions_get_foreground_uid() failed说明WebContent进程未获得前台权限,导致无法获取系统音频资源。
  3. Flutter引擎在唤醒时出现flutter ns Flutter exiting,可能导致音频会话状态混乱。

解决方案

  • 手动管理AudioSession:禁用flutter_callkit_incoming自动配置AudioSession(设置ios.configureAudioSession: false),在actionCallAccept事件中手动激活AudioSession并设置正确模式:
    import 'package:audio_session/audio_session.dart';
    
    void handleCallAccept() async {
      final session = await AudioSession.instance;
      await session.configure(AudioSessionConfiguration(
        avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
        avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.allowBluetooth | AVAudioSessionCategoryOptions.defaultToSpeaker,
        avAudioSessionMode: AVAudioSessionMode.voiceChat,
      ));
      await session.setActive(true);
      // 再加载WebView和ZegoCloud通话界面
    }
    
  • 延迟加载WebView:在AudioSession激活成功后,延迟1-2秒再初始化flutter_inappwebview,确保音频权限完全生效。
  • 权限检查:确保应用已申请NSMicrophoneUsageDescription和NSCameraUsageDescription权限,且在后台唤醒时重新请求权限(iOS 14+需注意权限状态)。

针对场景2:应用无法唤醒

原因分析

  1. PushKit VoIP通知处理超时:iOS要求VoIP通知的didReceiveIncomingPushWithPayload回调必须在30秒内完成CallKit的reportNewIncomingCall操作,否则系统会终止应用。
  2. Flutter引擎初始化耗时过长:应用被杀状态下唤醒,Flutter启动时间超过系统允许的阈值,导致CallKit回调未及时响应。
  3. 后台任务未正确配置:未在VoIP通知处理时启动后台任务,导致系统提前终止应用进程。

解决方案

  • 优化VoIP通知处理流程:在原生iOS代码的didReceiveIncomingPushWithPayload中,先调用reportNewIncomingCall,再启动Flutter引擎或处理业务逻辑,避免阻塞主线程。
  • 添加后台任务:在VoIP通知处理时启动后台任务,延长应用唤醒后的存活时间:
    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
        let taskId = UIApplication.shared.beginBackgroundTask(expirationHandler: {
            UIApplication.shared.endBackgroundTask(taskId)
        })
        
        // 先报告CallKit来电
        let uuid = UUID()
        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .generic, value: payload.dictionaryPayload["callerName"] as? String ?? "")
        update.hasVideo = false
        
        callController.reportNewIncomingCall(with: uuid, update: update) { error in
            if let error = error {
                print("Failed to report incoming call: \(error)")
            } else {
                // 再处理Flutter唤醒和业务逻辑
                DispatchQueue.main.async {
                    // 唤醒Flutter或跳转页面
                }
            }
            completion()
            UIApplication.shared.endBackgroundTask(taskId)
        }
    }
    
  • 检查PushKit配置:确保VoIP证书有效,PKPushRegistry已正确注册,且应用开启了Voice over IP后台模式。

内容的提问来源于stack exchange,提问作者Hannan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 09:05:55