当包含 Ice 服务器时,WebRTC 陷入连接状态(远程候选者甚至通过 LAN 也会导致问题)

2024-01-09

我暂时创建了一个RTCPeerConnection没有任何iceServers在尝试解决一个问题时上一期 https://stackoverflow.com/questions/62020695/webrtc-stuck-in-connecting-state/62371958?noredirect=1#comment110974045_62371958.

let peer = new RTCPeerConnection();

这在我的本地网络上运行得很好。

但是,不在同一网络上的设备(例如,4G 上的移动设备)将无法连接。我记得我必须加回一些iceServers to the RTCPeerConnection构造函数。

let peer = RTCPeerConnection(
  {
    iceServers: [
      {
        urls: [
          "stun:stun1.l.google.com:19302",
          "stun:stun2.l.google.com:19302",
        ],
      },
      {
        urls: [
          "stun:global.stun.twilio.com:3478?transport=udp",
        ],
      },
    ],
    iceCandidatePoolSize: 10,
  }
);

这样做之后,我的 WebRTC 连接就一直停留在连接状态。即使在我的本地网络上,也没有一个连接成功。 (不再是这种情况,请参阅edit 2 below)

这是连接的状态:

  • 冰候选人聚集在一起。
  • 报价/答案已创建。
  • 报价/答复和冰候选已通过我的信令服务成功发送。
  • 我成功设置了远程和本地描述,并在两端添加了冰候选项。
  • 连接保持连接状态。
  • 大约 30 秒后,连接超时并失败。

EDIT: 看来我离开的时候iceServers空白,连接仍然收集一个ice候选者,所以我假设我的浏览器(chrome)提供了一个默认的ice服务器。在这种情况下,只有我的自定义 Ice 服务器(如上所示)导致了问题,而不是浏览器默认值。


编辑 2:新观察

我已经添加了大量的日志记录,每当我确实有的时候我就会注意到一些东西iceServers包括:

每当对等点 A 在一段时间内第一次发起与对等点 B 的连接时,对等点 B 就会收集两个ice 候选者:1 个本地主机候选者和 1 个远程候选者。正如我上面已经说过的,连接失败。

但是当我快速尝试再次连接时...对等点 B 只收集一个ice候选者:本地主机候选者。远程候选人未聚集。我的第一个假设是我正在使用的 STUN 服务器(在本例中可能是 google 的)对其服务有某种形式的速率限制。这个场景真正有趣的是连接成功了!

远程候选人搞乱了连接,这有点神秘……我希望这些新细节能有所帮助。我已经被这个问题困扰了好几个月了!而且这两个设备都在我的 LAN 上,因此我预计远程候选设备绝对不会产生任何影响。


对等 A 代码(发起者):

export class WebRTCConnection {
  private _RTCPeerConnection: any;
  private _fetch: any;
  private _crypto: any;

  private _entity: any;
  private _hostAddress: any;
  private _eventHandlers: ConnectionEventHandlers;
  private _peer: any;
  private _peerChannel: any;

  constructor({
    entity,
    hostAddress,
    eventHandlers,
    RTCPeerConnection,
    fetch,
    crypto,
  }: {
    entity: any,
    hostAddress: any,
    eventHandlers: ConnectionEventHandlers,
    RTCPeerConnection: any,
    fetch: any,
    crypto: any,
  }) {
    this._RTCPeerConnection = RTCPeerConnection;
    this._fetch = fetch;
    this._crypto = crypto;

    this._entity = entity;
    this._hostAddress = hostAddress;
    this._eventHandlers = eventHandlers;

    this._initPeer();
  }

  async _initPeer() {
    this._peer = new this._RTCPeerConnection(/* as shown in question */);

    let resolveOfferPromise: (value: any) => void;
    let resolveIceCandidatesPromise: (value: any[]) => void;
    
    let iceCandidatesPromise: Promise<any[]> = new Promise((resolve, _reject) => {
      resolveIceCandidatesPromise = resolve;
    });

    let offerPromise: Promise<any> = new Promise((resolve, _reject) => {
      resolveOfferPromise = resolve;
    });

    this._peer.onnegotiationneeded = async () => {
      let offer = await this._peer.createOffer();
      await this._peer.setLocalDescription(offer);
      resolveOfferPromise(this._peer.localDescription);
    };

    this._peer.onicecandidateerror = () => {
      // log error
    };

    let iceCandidates: any[] = [];

    this._peer.onicecandidate = async (evt: any) => {
      if (evt.candidate) {
        // Save ice candidate
        iceCandidates.push(evt.candidate);
      } else {
        resolveIceCandidatesPromise(iceCandidates);
      }
    };

    (async () => {
      // No more ice candidates, send on over signaling service
      let offer: any = await offerPromise;
      let iceCandidates: any[] = await iceCandidatesPromise;

      let sigData = // reponse after sending offer and iceCandidates over signaling service

      let answer = sigData.answer;
      await this._peer.setRemoteDescription(answer);

      for (let candidate of sigData.iceCandidates) {
        await this._peer.addIceCandidate(candidate);
      }
    })();

    this._peer.onicegatheringstatechange = (evt: any) => {
      // log state
    };

    this._peer.onconnectionstatechange = async () => {
      // log state
    };

    this._peerChannel = this._peer.createDataChannel("...", {
      id: ...,
      ordered: true,
    });

    this._peerChannel.onopen = () => {
      // log this
    };

    this._peerChannel.onmessage = (event: any) => {
      // do something
    };
  }

  send(msg: any) {
    this._peerChannel.send(
      new TextEncoder().encode(JSON.stringify(msg)).buffer,
    );
  }

  close() {
    if (this._peer) {
      this._peer.destroy();
    }
  }
}

对等 B 代码:

export class WebRTCConnection {
  constructor({ signalData, eventHandlers, RTCPeerConnection }) {
    this._eventHandlers = eventHandlers;

    this._peer = new RTCPeerConnection(/* as seen above */);

    this._isChannelOpen = false;

    this._peer.ondatachannel = (event) => {
      event.channel.onopen = () => {
        this._mainDataChannel = event.channel;
        event.channel.onmessage = async (event) => {
          // do something
        };
        this._isChannelOpen = true;
      };
    };

    this._peer.onicecandidateerror = () => {
      // log error
    };

    this._iceCandidates = [];
    this._isIceCandidatesFinished = false;
    this._iceCandidatesPromise = new Promise((resolve, _reject) => {
      this._resolveIceCandidatesPromise = resolve;
    });
    this._isAnswerFinished = false;
    this._isSignalDataSent = false;

    this._peer.onicecandidate = async (evt) => {
      if (evt.candidate) {
        // Save ice candidate
        this._iceCandidates.push(evt.candidate);
      } else {
        // No more ice candidates, send on over signaling service when ready
        this._isIceCandidatesFinished = true;
        this._resolveIceCandidatesPromise();
        this._sendSignalData();
      }
    };

    (async () => {
      let sigData = JSON.parse(signalData);

      let offer = sigData.offer;
      await this._peer.setRemoteDescription(offer);

      this._answer = await this._peer.createAnswer();
      await this._peer.setLocalDescription(this._answer);

      for (let candidate of sigData.iceCandidates) {
        await this._peer.addIceCandidate(candidate);
      }

      this._isAnswerFinished = true;
      this._sendSignalData();
    })();

    this._peer.onconnectionstatechange = async () => {
      // log state
    };
  }

  _sendSignalData() {
    if (false
      || !this._isIceCandidatesFinished
      || !this._isAnswerFinished
      || this._isSignalDataSent
    ) {
      return;
    }

    this._isSignalDataSent = true;

    this._eventHandlers.onSignal(JSON.stringify({
      answer: {
        type: this._answer.type,
        sdp: this._answer.sdp,
      },
      iceCandidates: this._iceCandidates,
    }));
  }

  send(msg) {
    this._mainDataChannel.send(new TextEncoder().encode(JSON.stringify(msg)));
  }

  close() {
    this._peer.destroy();
  }
}

您的代码可以在 LAN 上运行,无需iceServers因为 STUN 服务器不用于收集候选主机(您的计算机已经知道其本地 IP 地址),并且候选主机足以在 LAN 上建立 WebRTC 连接。

连接可能会失败,因为其中一个对等点位于对称NAT https://en.wikipedia.org/wiki/Network_address_translation#Methods_of_translation, 在之上STUN https://en.wikipedia.org/wiki/STUN无法工作。您可以使用本页中的代码检查网络是否位于对称 NAT 之后:我是否处于对称 NAT 后面? https://webrtchacks.com/symmetric-nat/(该页面还提供了JSFiddle https://jsfiddle.net/5ftsd5c2/17/,您可以在其中检查控制台消息是否打印“正常 nat”或“对称 nat”。如果它没有打印任何内容,而小提琴工作正常,则意味着您没有获得服务器反射候选者。)

我认为你应该首先在 WAN 上与同行一起测试你的代码,检查他们是否位于正常的 nat 后面。您是否曾经在 WAN 上尝试过通过以太网或 WiFi 连接对等点的代码? 3G/4G 网络似乎经常处于对称 NAT 下。

UPDATE(谢谢@肖恩·杜波依斯 https://stackoverflow.com/users/5472819/sean-dubois为了comment https://stackoverflow.com/questions/62772851/webrtc-stuck-in-connecting-state-when-ice-servers-are-included-remote-candidate/62817141#comment111089772_62817141): A "对称NAT”,我在上面使用过的表达方式,并在RFC 3489 https://www.rfc-editor.org/rfc/rfc3489#section-5(2003 年 3 月),可以用 中引入的更新术语来更好地术语RFC 4787 https://www.rfc-editor.org/rfc/rfc4787#section-4.1(2007 年 1 月)。STUN https://en.wikipedia.org/wiki/STUN仅适用于具有“端点独立映射“行为。A“对称NAT”(旧术语)有一个“地址相关映射“行为或”地址和端口相关映射“ 行为,not an "端点独立映射“ 行为。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

当包含 Ice 服务器时,WebRTC 陷入连接状态(远程候选者甚至通过 LAN 也会导致问题) 的相关文章

随机推荐