WebRTC DataChannel:在 Firefox 中工作但不工作 Chrome

WebRTC DataChannel: working in Firefox but not Chrome

我是 WebRTC 的新手。我正在尝试在两个对等点之间建立一个没有音频或视频的简单数据通道;只是文本数据。 最后,这将是一个游戏,其中 2-7 个同伴将连接到一个将成为游戏大师的同伴。

经过数小时的谷歌搜索和阅读 html5rocks、MDN 和其他堆栈 posts,我尝试了很多东西,但我仍然无法让它工作。

当我在两个不同的 Firefox 选项卡上打开页面时,一切正常。我可以看到其中一个选项卡发送 "Hello, world!",另一个发送 "It works!"。 DataChannel 建立良好,两个选项卡都收到各自对等方的消息。

但是,当 运行 它在 Chrome 上时,它不起作用。在我的一个测试中,DataChannel 在我能够发送任何内容之前神秘地关闭了,而在另一个测试中,RTCPeerConnection.ondatachannel 事件似乎根本没有被调用(更多细节在下面)。 如果我尝试让 Firefox 与 Chrome 通信,无论顺序如何,我都会收到有关 setRemoteDescription 失败的不同神秘错误。

当然,在 none 这些情况下,我会在 web/JavaScript 控制台中收到任何错误消息;这太容易了。

我的问题不在信令过程中,至少我不这么认为。一个普通的 WebSocket 用于与一个非常简单的 Node.js 服务器通信。 我宁愿避免使用诸如 PeerJS 之类的库。首先,因为我们最好通过手动操作来学习这件事,其次,因为我想将信令 Node.js 服务器用于其他事情,而不仅仅是发信。这在 Node 端本身不是问题,但它在浏览器端(因为我不会在 100+ KB 的海洋中找到一点雨滴 minified/obfuscated 源代码)

基本场景非常简单:当前连接的用户列表在页面上每 15 秒自动刷新一次。通过点击一个用户名,你可以连接到他,你发送 "Hello, world!" 而他同时回答 "It works!";那是暂时吃的。 简单的聊天文本框当然是下一个合乎逻辑的步骤,一旦我能够设置基本的通信。

更具体地说,如果我是用户 A 并单击用户 B,则应该会发生以下情况:

  1. A通过信令WebSocket向B发送消息,表示要呼叫他
  2. B 用 WebRTC 提议回复 A
  3. A 获得报价并回复 WebRTC 答案。
  4. DataChannel 已建立
  5. 当B端的DataChannel打开时,他发送"Hello, world!"给A
  6. 当A端的DataChannel打开时,他发送"It works!"给B;这可能以相反的顺序发生

    • 无论使用何种浏览器,我应该修改什么才能使其正常工作? (当然我知道它只在 Firefox 和 Chrome 当前有效)
    • 额外的可选问题,为什么我得到不止一个 ICE 候选人,尤其是在成功建立连接之后?

我想我应该安装最新的 Firefox 和 Crhome:resp。 45 和 49,在 Windows 7 个 64 位上。

下面是我的 JavaScript 代码;然后是对应于几个场景的输出,最后是我通过阅读其他 posts 和教程得到的一些想法。

function log (s) {
$('#log')[0].insertAdjacentHTML('beforeEnd', s+'<br />');
}

function callUser (e) {
var uname = this.href.substring(1+this.href.indexOf('#'));
ws.send({ type: 'RTCCall', to: uname });
log('Calling ' + uname + '...');
e.preventDefault();
return false;
}

function updateUserList (o) {
var div = $('#userlist')[0];
div.innerHTML='';
div.append('p', o.userlist.length + ' connected users');
for (var i=0, n=o.userlist.length; i<n; i++) {
var uname = o.userlist[i];
var a = div.append('a', {href: '#'+uname }, uname);
div.append('br');
a.onclick = callUser;
}}

function createRTCPeerConnection (to) {
log("Creating RTCPeerConnection...");
var RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection;
var pc = new RTCPeerConnection(pcConfig, pcOptions);
pc.onicecandidate = e=>{ if (e&&e.candidate) { ws.send({ type: 'RTCSignal', to: to, candidate: e.candidate }); log('ICE candidate received'); }};
pc.onconnectionstatechange = e=>log("Connection state change: " +pc.connectionState);
pc.onnegotiationneeded = e=>{ console.log("Negotiation needed: ", e); log("Negotiation needed: " +e); };
pc.onicecandidateerror = e=>log("ICE candidate error: " +e);
pc.oniceconnectionstatechange = e=>log("ICE connection state change: " +pc.iceConnectionState);
pc.onicegatheringstatechange = e=>log("ICE gathering state change: " +pc.iceGatheringState);
pc.onsignalingstatechange = e=>log("Signaling state change: " +pc.signalingState);
pc.onaddstream = e=>{ console.log(e); log('Add stream'); };
pc.ondatachannel = e=>{ 
log("Received data channel " + e.channel.label);
pc.channel=e.channel;
pc.channel.onopen = e=>{ log("Data channel opened"); pc.channel.send("It works!"); };
pc.channel.onmessage = e=>log("Message from " + to + ": " + e.data);
pc.channel.onerror = e=>log("Data channel error: " +e);
pc.channel.onclose = e=>log("Data channel closed: " +e);
};
log("RTCPeerConnection created");
return pc;
}

function createDataChannel (pc, name) {
log("Creating DataChannel " + name + "...");
pc.channel=pc.createDataChannel(name, { ordered: false });
pc.channel.onopen = _=>{ pc.channel.send("Hello, world!"); log("Data channel opened"); };
pc.channel.onmessage = e=>log("Message from " + pc.from + ": " + e.data);
pc.channel.onerror = e=>log("Data channel error: " +e);
pc.channel.onclose = e=>log("Data channel closed: " +e);
log("DataChannel " + name + " created");
return pc.channel;
}

var ws = new WSClient('ws://localhost:3003/');
var pc, 
pcConfig = {iceServers:[{url:'stun:stun.l.google.com:19302'}]},
pcOptions = { optional:  [
{DtlsSrtpKeyAgreement: true}, {RtpDataChannels: true }] 
},
sdpOptions = {mandatory:  { OfferToReceiveAudio: true,  OfferToReceiveVideo: false } };

log('Initializing...');
ws.on('connect', _=>log('Connected to web socket'));
ws.on('disconnect', _=>log('Disconnected from web socket'));
ws.on('userlist', o=>updateUserList(o));
ws.connect() .then(_=>{ ws.send({type:'userlist'}); setInterval(_=>ws.send({ type: 'userlist' }), 15000); });

ws.on('RTCCall', o=>{
log(o.from + " is calling !");
if (!pc) pc = createRTCPeerConnection(o.from);
pc.from = o.from;
pc.channel = createDataChannel(pc, 'chat');
pc.createOffer(desc=>{ 
pc.setLocalDescription(desc, _=>log("setLocalDescription succeeded"), fail=>log("setLocalDescription failed: " + fail));
log("Sending offer to " + o.from); 
ws.send({type: 'RTCSignal', to: o.from, answer: true, sdp: desc}); }, 
fail=>log("createOffer failed: "+fail), sdpOptions);
});//RTCCall

ws.on('RTCSignal', o=>{
log("Received signal from " + o.from + ": " + (o.sdp?"sdp":"") + (o.candidate?"ICE":""));
if (!pc) pc = createRTCPeerConnection(o.from);
pc.from = o.from;
if (o.sdp)  pc.setRemoteDescription(new RTCSessionDescription(o.sdp), _=>log("setRemoteDescription succeeded"), fail=>log("setRemoteDescription failed: " +fail));
else  if (o.candidate) pc.addIceCandidate(new RTCIceCandidate(o.candidate));
if (o.answer) pc.createAnswer(desc=>{ 
pc.setLocalDescription(desc, _=>log("setLocalDescription succeeded"), fail=>log("setLocalDescription failed: " + fail));
log("Sending answer to " + o.from); 
ws.send({type: 'RTCSignal', to: o.from, sdp: desc}); 
}, 
fail=>log("createAnswer failed: "+fail), sdpOptions);
});

这是 firefox 连接到 firefox 时的输出,完美运行:

来电者:

Initializing...
Connected to web socket
Calling user132...
Received signal from user132: sdp
Creating RTCPeerConnection...
RTCPeerConnection created
Signaling state change: have-remote-offer
setRemoteDescription succeeded
Received signal from user132: ICE
Received signal from user132: ICE
Received signal from user132: ICE
Sending answer to user132
Signaling state change: stable
setLocalDescription succeeded
Received signal from user132: ICE
ICE connection state change: checking
ICE connection state change: connected
ICE candidate received
ICE candidate received
ICE candidate received
ICE candidate received
Received data channel chat
Received signal from user132: ICE
ICE candidate received
Data channel opened
Message from user132: Hello, world!

呼叫:

Initializing...
Connected to web socket
user133 is calling !
Creating RTCPeerConnection...
RTCPeerConnection created
Creating DataChannel chat...
Negotiation needed: [object Event]
DataChannel chat created
Sending offer to user133
Signaling state change: have-local-offer
setLocalDescription succeeded
ICE candidate received
ICE candidate received
ICE candidate received
ICE candidate received
Received signal from user133: sdp
Signaling state change: stable
setRemoteDescription succeeded
ICE connection state change: checking
ICE connection state change: connected
ICE candidate received
Data channel opened
Received signal from user133: ICE
Received signal from user133: ICE
Received signal from user133: ICE
Received signal from user133: ICE
Received signal from user133: ICE
Message from user133: It works!

这是 Chrome 连接到 Chrome 时的输出,失败了:

来电者:

Initializing...
Connected to web socket
Calling user134...
Received signal from user134: sdp
Creating RTCPeerConnection...
RTCPeerConnection created
setRemoteDescription succeeded
Signaling state change: have-remote-offer
Sending answer to user134
setLocalDescription succeeded
Signaling state change: stable
Received signal from user134: ICE
ICE connection state change: checking
ICE candidate received
Received signal from user134: ICE
ICE candidate received
ICE connection state change: connected

呼叫:

Initializing...
Connected to web socket
user135 is calling !
Creating RTCPeerConnection...
RTCPeerConnection created
Creating DataChannel chat...
DataChannel chat created
Negotiation needed: [object Event]
Sending offer to user135
Signaling state change: have-local-offer
setLocalDescription succeeded
Received signal from user135: sdp
Data channel closed: [object Event]
setRemoteDescription succeeded
Signaling state change: stable
ICE connection state change: checking
ICE candidate received
Received signal from user135: ICE
ICE candidate received
Received signal from user135: ICE
ICE connection state change: connected
ICE connection state change: completed

这是 Firefox 连接到 Chrome 时的输出,失败的是:

Fiefox 来电者:

Initializing...
Connected to web socket
Calling user136...
Received signal from user136: sdp
Creating RTCPeerConnection...
RTCPeerConnection created
Signaling state change: have-remote-offer
setRemoteDescription succeeded
Received signal from user136: ICE
Received signal from user136: ICE
Received signal from user136: ICE
Received signal from user136: ICE
Sending answer to user136
Signaling state change: stable
setLocalDescription succeeded
ICE connection state change: failed
Received signal from user136: ICE
Received signal from user136: ICE
Received signal from user136: ICE
Received signal from user136: ICE

Chrome 来电:

Initializing...
Connected to web socket
user137 is calling !
Creating RTCPeerConnection...
RTCPeerConnection created
Creating DataChannel chat...
DataChannel chat created
Negotiation needed: [object Event]
Sending offer to user137
setLocalDescription succeeded
Signaling state change: have-local-offer
ICE candidate received
ICE candidate received
ICE candidate received
ICE candidate received
Received signal from user137: sdp
setRemoteDescription failed: OperationError: Failed to parse SessionDescription. 
ICE candidate received
ICE candidate received
ICE candidate received
ICE candidate received

这是 Firefox 以相反的方式连接到 Chrome 时的输出,但同样失败了: Chrome 来电者:

Initializing...
Connected to web socket
Calling user138...
Received signal from user138: sdp
Creating RTCPeerConnection...
RTCPeerConnection created
setRemoteDescription failed: OperationError: Failed to set remote offer sdp: Session error code: ERROR_CONTENT. Session error description: Failed to set
remote data description send parameters..
Signaling state change: have-remote-offer
Sending answer to user138
setLocalDescription failed: OperationError: Failed to set local sdp: Session error code: ERROR_CONTENT. Session error description: Failed to set remote
data description send parameters..
Received signal from user138: ICE
Received signal from user138: ICE
Received signal from user138: ICE
Received signal from user138: ICE

Firefox 调用:

Initializing...
Connected to web socket
user139 is calling !
Creating RTCPeerConnection...
RTCPeerConnection created
Creating DataChannel chat...
Negotiation needed: [object Event]
DataChannel chat created
Sending offer to user139
Signaling state change: have-local-offer
setLocalDescription succeeded
Received signal from user139: sdp
Signaling state change: stable
setRemoteDescription succeeded
ICE candidate received
ICE candidate received
ICE candidate received
ICE candidate received
ICE connection state change: failed

现在,一些想法:

  1. 我已经读过多次,应该在发送报价之前创建 DataChannel。因此,我尝试按如下方式修改我的代码以确保是这样:

    pc.createOffer(desc=>{ pc.setLocalDescription(desc, _=>say("setLocalDescription succeeded"), fail=>say("setLocalDescription failed: " + fail)); 说("Sending offer to " + o.from); ws.send({type: 'RTCSignal', to: o.from, answer: true, sdp: desc}); }, 失败=>说("createOffer failed: "+失败),sdpOptions); pc.channel = createDataChannel(pc, 'chat');

此修改对 Firefox 没有任何影响。它继续像以前一样工作。 对于Chrome,还是不行;但输出不同。以前,在调用 setRemoteDescription 之前,它感觉 DataChannel 在我能够发送任何内容之前神秘地关闭了。 然而,在这种情况下,我没有得到任何消息,DataChannel 保持连接状态。这是输出:

来电者:

Initializing...
Connected to web socket
Calling user142...
Received signal from user142: sdp
Creating RTCPeerConnection...
RTCPeerConnection created
setRemoteDescription succeeded
Signaling state change: have-remote-offer
Sending answer to user142
Signaling state change: stable
setLocalDescription succeeded
ICE candidate received
Received signal from user142: ICE
ICE connection state change: checking
ICE candidate received
Received signal from user142: ICE
ICE connection state change: connected

呼叫:

Initializing...
Connected to web socket
user143 is calling !
Creating RTCPeerConnection...
RTCPeerConnection created
Creating DataChannel chat...
DataChannel chat created
Negotiation needed: [object Event]
Sending offer to user143
setLocalDescription succeeded
Signaling state change: have-local-offer
Received signal from user143: sdp
Signaling state change: stable
setRemoteDescription succeeded
ICE connection state change: checking
Received signal from user143: ICE
ICE candidate received
ICE candidate received
Received signal from user143: ICE
ICE connection state change: connected
ICE connection state change: completed

无论如何,似乎在这两种情况中的 none 中,事件 RTCPeerConnection.ondatachannel 从未被调用过。我有一种感觉,如果我的处理程序从未被调用过,或者连接是否没有很好地建立,我真的不太清楚。

我也试过在另一个时刻创建 DataChannel 但没有成功。例如,在双方都调用了 setRemoteDescription 之后。 在这种情况下,Firefox 拒绝创建报价,因为我既没有请求 audio/video,也没有请求轨道(我不知道它是什么)和 DataChannel(尚未创建)。 所以到目前为止我的结论是在发送报价之前创建通道是正确的方法;至少是唯一可以与 Firefox 一起使用的。

我也读过很多次,鉴于我没有请求 audio/video,我没有义务发送报价和答复。但是,如果我从我的代码中挤出它,似乎什么也不会发生。没有 ICE 服务器交换等等...... 在其他地方,我读到在调用 setLocalDescription 之前没有 ICE 服务器启动。所以我必须调用 setLocalDescription,因此我必须创建报价。从那里看来,我有义务通过信令通道将其发送给另一个对等点似乎是合乎逻辑的,我有义务调用 setRemoteDescription 然后需要回答。

我在我的代码中使用 sdpOptions = {mandatory: { OfferToReceiveAudio: true, OfferToReceiveVideo: false } };``,尽管我不打算发送 audio/video 流。 在注意到如果我将它们都设置为 false 之前,我已经搜索了很多,然后 Chrome 永远不会启动它的 ICE 服务器,因此不可能有任何 P2P 连接。

还有这个:{DtlsSrtpKeyAgreement: true}, {RtpDataChannels: true }] 我从教程中复制了它,但并不真正了解它的作用。无论如何,将它全部删除,或者将其中一个或另一个设置为 false 不会改变我的结果。

感谢您阅读这么长的文章post。我希望你知道我如何解决这个问题。 请告诉我我应该做什么,或者至少给我一些线索。

非常感谢您的帮助。

编辑:天哪!似乎我所有的代码行都被折叠成一个大行。非常抱歉,这不是预期的。请在评论中告诉我下次如何解决此问题。谢谢。

删除这个:

pcOptions = { optional:  [
{DtlsSrtpKeyAgreement: true}, {RtpDataChannels: true }] 
},

这是旧的非标准 chrome 东西,在 Firefox 中什么都不做,但在 Chrome 中会导致蝙蝠飞出。在规范中,数据通道不 运行 通过 rtp,也不依赖 srtp。

当你在做的时候,把这个也去掉:

sdpOptions = {mandatory: {OfferToReceiveAudio: true,  OfferToReceiveVideo: false}};

格式已更改为(注意小写的“o”):

sdpOptions = { offerToReceiveAudio: true,  offerToReceiveVideo: false};

但仅数据通道是不必要的。如果还是不行,请告诉我。

我也强烈推荐adapter.js, the official WebRTC polyfill, which lets you use the latest spec with promises etc. like this。它是一个垫片而不是一个库,它的目标是最终消失。