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
调试时:
这些崩溃报告为我们所面临的挑战提供了一个缩影。通过坚持不懈的努力和深入分析,我们发现了问题的核心——我们在理解和使用 WebRTC 协议时出现了问题。
连接状态的重要性
在讨论关闭对等连接的策略之前,了解连接的不同状态非常重要。在WebRTC中,RTCPeerConnection.connectionState
属性可以告诉您当前的连接状态。可能的状态有:
new
:连接刚刚创建,尚未完成协商。connecting
:连接正在协商中。connected
:连接已成功协商并且活动数据通道已打开。disconnected
:一个或多个传输已断开。failed
:一项或多项传输已终止或失败。closed
: 连接已关闭。
在调用RTCPeerConnection.close()
之前,确保连接是 connected
或 failed
是至关重要的。在过渡状态(connecting
或 disconnected
)下关闭连接可能会导致问题,并可能导致应用程序变得无响应。
解析 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
并清理所有相关资源。让我们来分析一下这个方法中发生了什么:
RTC_DCHECK_RUN_ON(signaling_thread());
:此行检查以确保Close()
方法是从信令线程调用的。信令线程用于更改 PeerConnection 状态的操作,例如处理 SDP 提供和应答、ICE 候选以及关闭连接。if (IsClosed()) { return; }
:这一行行检查 PeerConnection 是否已关闭。如果是,该方法会立即返回,因为没有任何工作要做。legacy_stats_->UpdateStats(kStatsOutputLevelStandard);
:这一行在 PeerConnection 关闭之前更新其统计信息。- 接下来的几行将 ICE 连接状态和标准连接状态设置为“关闭”,并调用观察者(可能是使用 WebRTC 库的应用程序)上的
OnIceConnectionChange
和OnConnectionChange
方法。这将通知观察者连接已关闭。 sdp_handler_->Close();
:这一行关闭 SDP(会话描述协议)处理程序,该处理程序负责处理 WebRTC 握手过程中的 SDP 提议和应答。- 如果为连接配置了媒体,则下一个代码块将停止所有活动的收发器。收发器是媒体(音频或视频)数据的发送器和接收器的组合。
- 接下来的几行清理与 PeerConnection 关联的各种资源,例如传输控制器(处理用于连接的实际网络传输)、端口分配器(用于查找连接的本地和远程端口),以及任何正在进行的统计数据收集。
- 调用
observer_ = nullptr;
删除对观察者的引用,因为连接关闭后不再需要它。 - 最后,该方法调用
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