如何防止 WebRTC peers 陷入死锁?探讨 WebRTC 的完美协商模式

在本文中,我们将探讨 WebRTC 的完美协商模式,以防止 WebRTC peers 陷入死锁。这一改变还能让我们在两个客户端之间使用对称代码。这样,我们就可以让对等方以任何顺序开始协商。

搭建WebRTC视频会议应用系列中,我们介绍了 webrtc 的基础知识以及如何在 Web 和 Android 中实现基本的网络会议应用程序。在以前的实现过程中,我们遇到的一个限制是 receiver(接收方)总是要比initiator(发起方)先开始。在现实世界中,这就要求参与者协调 webrtc 会话的启动。本文讨论的模式将解决这一限制。实施本文中的模式后,参与者只需使用会议 ID 并按任意顺序加入呼叫即可。

本文使用的源代码位于:https://github.com/bharath-kotha/WebRTC-Articles/tree/master/webrtc-android-2

让我们直接看看之前的实现中存在的问题,然后解决这些问题。

问题及解决方案

1. MQTT作为信令通道

如何防止 WebRTC peers 陷入死锁?探讨 WebRTC 的完美协商模式
使用 MQTT 作为信令服务器的问题

第一个问题不是来自 WebRTC 本身,而是来自我们选择的信号协议 MQTT。MQTT 采用发布-订阅模式。每当发布者在某个主题上发布消息时,该主题的所有订阅者都会收到该消息。如果该主题没有订阅者,则消息将被忽略。此外,它也没有检查主题现有订阅者数量的机制。

现在考虑序列图中描述的情况。假设 webrtc 客户端一连接到信令信道,就会创建并发送 SDP 提议。它不知道是否有订户已经连接到信令信道来收听它的信息。当然,如果第二个用户没有连接,SDP 提议就会丢失。而当第二个客户端连接并发送自己的 SDP 提议时,第一个客户端就不知道该如何处理该提议了。

我们将通过发送对等消息和确认来解决这个问题。我们将添加一种名为对等消息的新消息类型,并在实际 SDP 进程开始前交换对等消息。每个对等消息都有自己的 ID(一个随机生成的整数),并将此 ID 发送给另一个客户端。另一个客户端收到 ID 后,就会发送自己的 ID,同时将 hasRemoteId 参数设置为 true,表示收到了远程 ID。这里使用的 ID 也有助于解决接下来讨论的眩光问题。

发送对等信息的代码如下所示:

private var  selfId: Int = Random.nextInt(0, 10000)
private var remoteId: Int? = null

private fun sendPeerInfo() {
    val payload = JSONObject()
    payload.put("type", "peer")
    payload.put("selfId", this.selfId)
    payload.put("hasRemoteId", remoteId != null)
    sendMqttMessage(payload)
}

一旦收到对等信息,就会调用 handlePeerInfo 方法,该方法会存储远程 ID,并决定是否再次发送对等信息或开始使用 SDP 协议。相关代码如下:

private fun handlePeerInfo(message: JSONObject) {
  val hasRemoteId = message.get("hasRemoteId") as Boolean
  this.remoteId = message.get("selfId") as Int

  // If remote peer doesn't have our info then send it
  // Otherwise start the negotiation
  if(!hasRemoteId){
      sendPeerInfo()
  }
  else {
      createAndSendLocalDescription()
  }
}

下面的序列图描述了在我们做出这些更改后,对等信息交换是如何进行的:

如何防止 WebRTC peers 陷入死锁?探讨 WebRTC 的完美协商模式

2. Offer Glare(眩光)

如何防止 WebRTC peers 陷入死锁?探讨 WebRTC 的完美协商模式
Offer Glare

我们要解决的下一个问题是,当两个对等设备同时尝试发送报价时会出现竞赛条件。这种情况可能发生在会话首次初始化或随后更改(媒体添加或删除)时。请看上图。两个对等端都创建了自己的要约并设置了本地描述。之后,他们收到了来自远程对等设备的要约。由于对等方已经设置了自己的要约,因此不知道如何回应该要约。为了解决这个问题,我们将采用几种技术,一种是检测眩光(Glare),另一种是响应眩光。

为了识别眩光,我们会在开始创建要约之前将名为 makingOffer 的变量设置为 true,并在要约传送给对方后将其设置为 false。这种技术比依赖 signallingState 来识别眩光更可靠,因为创建要约是异步的,需要一些时间,而且如果我们在创建自己的要约时收到要约,我们可能无法检测到碰撞。

第二种技术是处理报价眩光。为了解决这个问题,我们将礼貌角色和无礼角色分别分配给其中一个同伴和另一个同伴。如果出现眩光,有礼貌的同伴会撤回报价,而无礼的同伴会继续报价。我们不应该同时回滚两个对等设备的提议,因为这可能会在下一次发送提议时再次造成提议眩光,最终导致连接无法建立。但要注意的是,如果没有发生要约碰撞,不礼貌的对等端将接受礼貌的对等端发出的要约。

那么,我们如何确定哪个对等体是有礼貌的,哪个是无礼的呢?这就是我们在交换对等信息时发送的 ID 的作用。我们将 ID 生成一个介于 1 和 10,000 之间的随机数,并发送给另一个对等点。ID 值较低的对等点是有礼貌的对等点,ID 值较高的对等点是无礼的对等点。因为在两个对等点都知道对方的 ID 之前,我们不会继续协商,所以我们总能知道哪个是有礼貌的对等点,哪个是无礼的对等点。

让我们看看生成和发送要约的代码:

private fun createAndSendLocalDescription() {
  val sdpObserver = object : SdpObserver {
      var sdpType : SessionDescription.Type = SessionDescription.Type.OFFER

      override fun onCreateSuccess(sdp: SessionDescription?) {
          Log.i(TAG, "onCreateSuccess: ${sdp?.description}")
          peerConnection?.setLocalDescription(dummySdpObserver, sdp)
          val payload = JSONObject()
          payload.put("type", "sdp")
          payload.put("sdp", sdp?.description)
          payload.put("sdpType", sdpType.ordinal)
          sendMqttMessage(payload)
          makingOffer = false
      }
  }
// If remote signallingState is stable then we didn't receive offer yet
  if(peerConnection?.localDescription == null && peerConnection?.signalingState() == PeerConnection.SignalingState.STABLE) {
      makingOffer = true
      peerConnection?.createOffer(sdpObserver, createSDPConstraints())
  }
// If we received the remote offer then generate answer
  else if (peerConnection?.remoteDescription != null && peerConnection?.signalingState() == PeerConnection.SignalingState.HAVE_REMOTE_OFFER) {
      sdpObserver.sdpType = SessionDescription.Type.ANSWER
      peerConnection?.createAnswer(sdpObserver, createSDPConstraints())
  }
}

上述方法会根据对等连接(peerConnection)的状态生成要约或应答,并将其发送给对方。如果我们尚未收到要约,也没有本地要约,那么我们将创建要约并发送。如果我们已经收到了远程要约,那么我们就会生成要约的应答并将其发送给远程对等网络。

现在让我们看看收到会话描述时如何处理:

private fun handleSdpMessage(message: JSONObject) {
  // Determine whether this peer is polite or impolite based on IDs
  val polite = selfId < remoteId!!
  val sdpTypeInt = message.get("sdpType") as Int
  val sdpType = SessionDescription.Type.values()[sdpTypeInt]

  // Detect collision
  val collision = sdpType == SessionDescription.Type.OFFER && (makingOffer || peerConnection!!.signalingState() != PeerConnection.SignalingState.STABLE)

  // If we're impolite peer and there's offer collision then ignore offer
  // Otherwise, proceed by adding remote description and generating answer
  ignoreOffer = !polite && collision
  if(ignoreOffer) {
      return
  }
  val sessionDescription = SessionDescription(sdpType, message.get("sdp") as String)
  // Setting remote offer while there is an existing local offer will rollback the local offer and use the remote offer
  peerConnection?.setRemoteDescription(dummySdpObserver, sessionDescription)
  if (sdpType == SessionDescription.Type.OFFER) {
      createAndSendLocalDescription()
  }
}

结论

通过使用本文中的技术,我们就能在对等方之间建立对称代码,并以对等方不会卡住的方式处理会话协商。请观看这个实时演示,了解眩光发生时的完美协商过程。

作者:Bharath Kotha
原文:https://bharathkotha.medium.com/perfect-negotiation-webrtc-series-part4-7ea5b009a9e2

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

(0)

相关推荐

发表回复

登录后才能评论