Android 和 iOS 如何关闭 WebRTC PeerConnections

WebRTC 是一项令人着迷的技术,为网络带来了实时通信功能。虽然 WebRTC 相对易于使用,但它有许多复杂之处,如果不正确理解,可能会导致问题。其中一个问题是关闭 PeerConnections,尤其是在网格调用等复杂场景中。在这里,我们将深入探讨问题的症结,并学习如何克服这一挑战。

面临的挑战

我曾经花了一个多星期的时间尝试调试一个看似简单的问题。在涉及来自不同平台(Android 和 iOS)的 8 个参与者的网状调用场景中,我的应用程序变得无响应并最终崩溃。当我试图关闭一个PeerConnection时,其他参与者仍处于 “连接 “状态,这种情况特别发生在iOS端。看似资源消耗崩溃,实际上是WebRTC后台任务导致主UI线程阻塞的问题。

崩溃目录

在过去六个月紧张而充满激情的开发过程中,我们从未经历过类似的崩溃。这种异常情况让我们感到困惑。更令人费解的是,崩溃报告显示存在资源消耗问题——在关闭对等连接这一看似无害的任务中,我们从未想到过这种可能性。

旨在防止任何应用程序占用系统资源的 iOS watchdog 最终终止了我们的应用程序。这向我们发出信号,表明幕后确实存在问题。

事故报告:

Date/Time:           2023-06-21 15:53:37.6520 +0500
Launch Time:         2023-06-21 15:43:18.2579 +0500
OS Version:          iPhone OS 16.5 (20F66)
Release Type:        User
Baseband Version:    4.02.01
Report Version:      104

Exception Type:  EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Termination Reason: FRONTBOARD 2343432205 
<RBSTerminateContext| domain:10 code:0x8BADF00D explanation:scene-update watchdog transgression: application<com.younite.development>:2048 exhausted real (wall clock) 
time allowance of 10.00 seconds
ProcessVisibility: Foreground
ProcessState: Running
WatchdogEvent: scene-update
WatchdogVisibility: Foreground
WatchdogCPUStatistics: (
"Elapsed total CPU time (seconds): 3.380 (user 3.380, system 0.000), 5% CPU",
"Elapsed application CPU time (seconds): 0.049, 0% CPU"
) reportType:CrashLog maxTerminationResistance:Interactive>

Triggered by Thread:  0

Thread 0 name:   Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libsystem_kernel.dylib                0x20bb88558 __psynch_cvwait + 8
1   libsystem_pthread.dylib               0x22c9d0078 _pthread_cond_wait + 1232
2   WebRTC                                0x109f92a20 0x109e4c000 + 1337888
3   WebRTC                                0x109f92900 0x109e4c000 + 1337600
4   WebRTC                                0x109ebe20c 0x109e4c000 + 467468
5   WebRTC                                0x109ebe10c 0x109e4c000 + 467212
6   WebRTC                                0x109ebda38 0x109e4c000 + 465464
7   WebRTC                                0x109ebda14 0x109e4c000 + 465428
8   WebRTC                                0x109f6b52c 0x109e4c000 + 1176876
9   libobjc.A.dylib                       0x1c5cb60a4 object_cxxDestructFromClass(objc_object*, objc_class*) + 116
10  libobjc.A.dylib                       0x1c5cbae00 objc_destructInstance + 80
11  libobjc.A.dylib                       0x1c5cc44fc _objc_rootDealloc + 80
12  libobjc.A.dylib                       0x1c5cb60a4 object_cxxDestructFromClass(objc_object*, objc_class*) + 116
13  libobjc.A.dylib                       0x1c5cbae00 objc_destructInstance + 80
14  libobjc.A.dylib                       0x1c5cc44fc _objc_rootDealloc + 80
15  WebRTC                                0x109f7302c 0x109e4c000 + 1208364
19  libswiftCore.dylib                    0x1c6d11628 _swift_release_dealloc + 56
20  libswiftCore.dylib                    0x1c6d1244c bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1>>::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 132
21  libswiftCore.dylib                    0x1c6d012c8 swift_arrayDestroy + 124
22  libswiftCore.dylib                    0x1c6a1c2b0 _DictionaryStorage.deinit + 468
23  libswiftCore.dylib                    0x1c6a1c31c _DictionaryStorage.__deallocating_deinit + 16
24  libswiftCore.dylib                    0x1c6d11628 _swift_release_dealloc + 56
25  libswiftCore.dylib                    0x1c6d1244c bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1>>::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 132
27  libswiftCore.dylib                    0x1c6d11628 _swift_release_dealloc + 56
28  libswiftCore.dylib                    0x1c6d1244c bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1>>::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 132
30  libsystem_blocks.dylib                0x22c9c5134 _call_dispose_helpers_excp + 48
31  libsystem_blocks.dylib                0x22c9c4d64 _Block_release + 252
32  libdispatch.dylib                     0x1d40eaeac _dispatch_client_callout + 20
33  libdispatch.dylib                     0x1d40f96a4 _dispatch_main_queue_drain + 928
34  libdispatch.dylib                     0x1d40f92f4 _dispatch_main_queue_callback_4CF + 44
35  CoreFoundation                        0x1cccb3c28 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16
36  CoreFoundation                        0x1ccc95560 __CFRunLoopRun + 1992
37  CoreFoundation                        0x1ccc9a3ec CFRunLoopRunSpecific + 612
38  GraphicsServices                      0x20815f35c GSEventRunModal + 164
39  UIKitCore                             0x1cf0276e8 -[UIApplication _run] + 888
40  UIKitCore                             0x1cf02734c UIApplicationMain + 340
41  Younite                               0x101509d00 main + 64
42  dyld                                  0x1ec19adec start + 2220

调试时:

Android 和 iOS 如何关闭 WebRTC PeerConnections

这些崩溃报告为我们所面临的挑战提供了一个缩影。通过坚持不懈的努力和深入分析,我们发现了问题的核心——我们在理解和使用 WebRTC 协议时出现了问题。

连接状态的重要性

在讨论关闭对等连接的策略之前,了解连接的不同状态非常重要。在WebRTC中,RTCPeerConnection.connectionState属性可以告诉您当前的连接状态。可能的状态有:

  • new:连接刚刚创建,尚未完成协商。
  • connecting:连接正在协商中。
  • connected:连接已成功协商并且活动数据通道已打开。
  • disconnected:一个或多个传输已断开。
  • failed:一项或多项传输已终止或失败。
  • closed: 连接已关闭。

在调用RTCPeerConnection.close()之前,确保连接是 connectedfailed 是至关重要的。在过渡状态(connectingdisconnected)下关闭连接可能会导致问题,并可能导致应用程序变得无响应。

解析 PeerConnection::Close()

PeerConnection::Close()函数是管理 WebRTC 中对等连接生命周期的核心。该本地函数负责有序关闭连接。然而,它的行为可能很复杂,包含多个条件检查和子过程,以满足连接生命周期不同阶段的需要。

本质上,该函数首先检查连接是否已经关闭。如果不是,它会继续更新连接状态,向任何观察者发出关闭信号,并停止所有收发器。它还确保在销毁传输控制器之前完成所有异步统计请求。然后释放相关资源,如语音/视频/数据通道、事件日志等。

这是 WebRTC 原生函数,源于 Chromium 的 WebRTC m115 分支头的活动分支:branch-heads/5790。

void PeerConnection::Close() {
  RTC_DCHECK_RUN_ON(signaling_thread());
  TRACE_EVENT0("webrtc", "PeerConnection::Close");

  RTC_LOG_THREAD_BLOCK_COUNT();

  if (IsClosed()) {
    return;
  }
  // Update stats here so that we have the most recent stats for tracks and
  // streams before the channels are closed.
  legacy_stats_->UpdateStats(kStatsOutputLevelStandard);

  ice_connection_state_ = PeerConnectionInterface::kIceConnectionClosed;
  Observer()->OnIceConnectionChange(ice_connection_state_);
  standardized_ice_connection_state_ =
      PeerConnectionInterface::IceConnectionState::kIceConnectionClosed;
  connection_state_ = PeerConnectionInterface::PeerConnectionState::kClosed;
  Observer()->OnConnectionChange(connection_state_);

  sdp_handler_->Close();

  NoteUsageEvent(UsageEvent::CLOSE_CALLED);

  if (ConfiguredForMedia()) {
    for (const auto& transceiver : rtp_manager()->transceivers()->List()) {
      transceiver->internal()->SetPeerConnectionClosed();
      if (!transceiver->stopped())
        transceiver->StopInternal();
    }
  }
  // Ensure that all asynchronous stats requests are completed before destroying
  // the transport controller below.
  if (stats_collector_) {
    stats_collector_->WaitForPendingRequest();
  }

  // Don't destroy BaseChannels until after stats has been cleaned up so that
  // the last stats request can still read from the channels.
  sdp_handler_->DestroyAllChannels();

  // The event log is used in the transport controller, which must be outlived
  // by the former. CreateOffer by the peer connection is implemented
  // asynchronously and if the peer connection is closed without resetting the
  // WebRTC session description factory, the session description factory would
  // call the transport controller.
  sdp_handler_->ResetSessionDescFactory();
  if (ConfiguredForMedia()) {
    rtp_manager_->Close();
  }

  network_thread()->BlockingCall([this] {
    // Data channels will already have been unset via the DestroyAllChannels()
    // call above, which triggers a call to TeardownDataChannelTransport_n().
    // TODO(tommi): ^^ That's not exactly optimal since this is yet another
    // blocking hop to the network thread during Close(). Further still, the
    // voice/video/data channels will be cleared on the worker thread.
    RTC_DCHECK_RUN_ON(network_thread());
    transport_controller_.reset();
    port_allocator_->DiscardCandidatePool();
    if (network_thread_safety_) {
      network_thread_safety_->SetNotAlive();
    }
  });

  worker_thread()->BlockingCall([this] {
    RTC_DCHECK_RUN_ON(worker_thread());
    worker_thread_safety_->SetNotAlive();
    call_.reset();
    // The event log must outlive call (and any other object that uses it).
    event_log_.reset();
  });
  ReportUsagePattern();
  // The .h file says that observer can be discarded after close() returns.
  // Make sure this is true.
  observer_ = nullptr;

  // Signal shutdown to the sdp handler. This invalidates weak pointers for
  // internal pending callbacks.
  sdp_handler_->PrepareForShutdown();
}

PeerConnection::Close()函数负责正确关闭WebRTCPeerConnection并清理所有相关资源。让我们来分析一下这个方法中发生了什么:

  1. RTC_DCHECK_RUN_ON(signaling_thread());:此行检查以确保Close()方法是从信令线程调用的。信令线程用于更改 PeerConnection 状态的操作,例如处理 SDP 提供和应答、ICE 候选以及关闭连接。
  2. if (IsClosed()) { return; }:这一行行检查 PeerConnection 是否已关闭。如果是,该方法会立即返回,因为没有任何工作要做。
  3. legacy_stats_->UpdateStats(kStatsOutputLevelStandard);:这一行在 PeerConnection 关闭之前更新其统计信息。
  4. 接下来的几行将 ICE 连接状态和标准连接状态设置为“关闭”,并调用观察者(可能是使用 WebRTC 库的应用程序)上的OnIceConnectionChangeOnConnectionChange方法。这将通知观察者连接已关闭。
  5. sdp_handler_->Close();:这一行关闭 SDP(会话描述协议)处理程序,该处理程序负责处理 WebRTC 握手过程中的 SDP 提议和应答。
  6. 如果为连接配置了媒体,则下一个代码块将停止所有活动的收发器。收发器是媒体(音频或视频)数据的发送器和接收器的组合。
  7. 接下来的几行清理与 PeerConnection 关联的各种资源,例如传输控制器(处理用于连接的实际网络传输)、端口分配器(用于查找连接的本地和远程端口),以及任何正在进行的统计数据收集。
  8. 调用observer_ = nullptr;删除对观察者的引用,因为连接关闭后不再需要它。
  9. 最后,该方法调用sdp_handler_->PrepareForShutdown();,通过使内部挂起回调的弱指针无效,为关闭SDP处理程序做好准备。

关闭 PeerConnections:Android 和 iOS 的最佳实践

以下是关闭对等连接时需要遵循的一些一般准则,以及针对 Android 和 iOS 平台量身定制的示例。

Android

单个 PeerConnection 关闭

// Assuming peerConnection is a PeerConnection object
fun disconnectPeer(peerConnection: PeerConnection) {
    // Check the connection state
    if (peerConnection.connectionState() == PeerConnection.PeerConnectionState.CONNECTED ||
        peerConnection.connectionState() == PeerConnection.PeerConnectionState.FAILED) {
        // Close each track
        peerConnection.localStreams.forEach { mediaStream ->
            mediaStream.videoTracks.forEach { it.setEnabled(false) }
            mediaStream.audioTracks.forEach { it.setEnabled(false) }
        }

        // Close the connection
        peerConnection.close()
    }

    // Nullify the reference
    peerConnection = null
}

网格调用 PeerConnect 关闭

// Assuming peerConnections is a list of PeerConnection objects
fun disconnectPeers(peerConnections: MutableList<PeerConnection>) {
    peerConnections.forEach { peerConnection ->
        // Check the connection state
        if (peerConnection.connectionState() == PeerConnection.PeerConnectionState.CONNECTED ||
            peerConnection.connectionState() == PeerConnection.PeerConnectionState.FAILED) {
            // Close each track
            peerConnection.localStreams.forEach { mediaStream ->
                mediaStream.videoTracks.forEach { it.setEnabled(false) }
                mediaStream.audioTracks.forEach { it.setEnabled(false) }
            }

            // Close the connection
            peerConnection.close()
        }
    }

    // Clear the list
    peerConnections.clear()
}

iOS系统

单个 PeerConnection 关闭

// Assuming peerConnection is a RTCPeerConnection object
func disconnectPeer(peerConnection: RTCPeerConnection) {
    // Check the connection state
    if (peerConnection.connectionState == .connected || 
        peerConnection.connectionState == .failed) {
        // Close each track
        peerConnection.senders.forEach { sender in
            sender.track?.isEnabled = false
        }

        // Close the connection
        peerConnection.close()
    }

    // Nullify the reference
    peerConnection = nil
}

网格调用 PeerConnect 关闭

// Assuming peerConnections is an array of RTCPeerConnection objects
func disconnectPeers(peerConnections: inout [RTCPeerConnection]) {
    peerConnections.forEach { peerConnection in
        // Check the connection state
        if (peerConnection.connectionState == .connected || 
            peerConnection.connectionState == .failed) {
            // Close each track
            peerConnection.senders.forEach { sender in
                sender.track?.isEnabled = false
            }

            // Close the connection
            peerConnection.close()
        }
    }

    // Empty the array
    peerConnections.removeAll()
}

通用解决方案

以下是无缝关闭 PeerConnections 的改进方法:

// Assuming peers is an array of RTCPeerConnection objects
function disconnectPeers(peers) {
    peers.forEach(peer => {
        // Close each track
        peer.getTracks().forEach(track => {
            track.stop();
        });
        
        // Remove all event listeners
        peer.ontrack = null;
        peer.onremovetrack = null;
        peer.onicecandidate = null;
        peer.oniceconnectionstatechange = null;
        peer.onsignalingstatechange = null;
        
        // Close the connection
        peer.close();
    });
    
    // Empty the array
    peers = [];
}

这种方法可确保与 PeerConnection 相关的所有资源和事件监听器被正确关闭和移除。与 PeerConnection 相关的每个轨迹都会被单独停止,从而确保完全、安全地断开连接。移除事件侦听器有助于防止连接关闭后出现意外触发。

作者:Muhammad Usman Bashir

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/29028.html

(1)

相关推荐

发表回复

登录后才能评论