问题描述
我是 WebRTC 的新手.我试图在两个对等点之间建立一个简单的数据通道,没有音频也没有视频;只是文本数据.最后,这将是一个游戏,其中 2-7 个节点将连接到一个将成为游戏大师的节点.
I'm fairly new in WebRTC. I'm trying to establish a simple data channel between two peers, without audio nor video; just text data.At the end it's going to be a game where 2-7 peers will connect to a peer who will be game master.
在谷歌搜索和阅读 html5rocks、MDN 和其他堆栈帖子数小时后,我尝试了很多东西,但我仍然没有设法让它发挥作用.
After hours of googling and reading html5rocks, MDN and other stack posts, I have tried many things, but I still doesn't manage to make it work.
当我在两个不同的 Firefox 选项卡上打开页面时,一切正常.我可以看到其中一个选项卡发送你好,世界!"另一个发送它有效!".DataChannel 已建立良好,两个选项卡都收到各自对等方的消息.
Everything works fine when I open the page on two different Firefox tabs. I can see that one of the tabs sends "Hello, world!" and that the other sends "It works!". The DataChannel is well established and both tabs get their respective peer's message.
但是,在 Chrome 上运行它时,它不起作用.在我的一个测试中,DataChannel 在我能够发送任何东西之前就神秘地关闭了,而在另一个测试中,RTCPeerConnection.ondatachannel 事件似乎根本没有被调用(更多细节在后面).如果我尝试让 Firefox 与 Chrome 通信,无论顺序如何,我都会收到有关 setRemoteDescription 失败的各种神秘错误.
However, when running it on Chrome, it doesn't work. In one of my tests, the DataChannel is mysteriously closed before I'm able to send anything, while in another, RTCPeerConnection.ondatachannel event don't seem to be called at all (more details further down).If I try to make Firefox communicate with Chrome, regardless on the order, I obtain different mysterious errors about a failure of setRemoteDescription.
当然,在这些情况下,我都不会在 web/JavaScript 控制台中收到任何错误消息;这太容易了.
OF course in none of these cases I get any error message in the web/JavaScript console; it would have been too easy.
我的问题不在于信令过程,至少我不这么认为.一个普通的 WebSocket 用于与一个非常简单的 Node.js 服务器进行通信.我宁愿避免使用像 PeerJS 这样的库.首先是因为我们可以更好地通过手动学习来学习,其次是因为我想将信令 Node.js 服务器用于其他事情,而不仅仅是信令.这在 Node 端本身不是问题,但在浏览器端是问题(因为我不会在 100 多 KB 压缩/混淆源代码的海洋中找到一点点雨滴)
My problem isn't in the signaling process, at least I don't think so. A plain WebSocket is used to communicate with a very simple Node.js server.I would prefer avoid using a library such as PeerJS. First of all because we better learn the thing by doing it manually, and secondly because I would like to use the signaling Node.js server for other things than just signaling. That's not a problem per se on Node side, but it is on browser side (because I'm not going to find a little raindrop in an ocean of 100+ KB minified/obfuscated source code)
基本场景非常简单:页面上每 15 秒自动刷新当前连接的用户列表.通过单击用户名,您可以与他建立联系,并发送Hello, world!".当他回答它有效!"同时;暂时先吃.简单的聊天文本框当然是下一个合乎逻辑的步骤,一旦我能够设置基本的通信.
The basic scenario is very simple: a list of currently connected users is automatically refreshed every 15 seconds on the page. By clicking on a user name, you get connected to him, you send "Hello, world!" while he answers "It works!" concurrently; that's eat for the moment.The simple chat text box is of course the next logical step, once I'm able to set up a basic communication.
更具体地说,如果我是用户 A 并点击用户 B,应该会发生以下情况:
More specifically, if I'm user A and click on user B, the following is supposed to happen :
- 通过信令WebSocket,A向B发送消息,表示他想给他打电话
- B 以 WebRTC 报价回复 A
- A 获得报价并回复 WebRTC 答案.
- DataChannel 已建立
- 当B端的DataChannel打开时,他发送Hello, world!"给A
当 A 端的 DataChannel 打开时,他发送It works!"对乙;这可能以相反的顺序发生
- Through the signaling WebSocket, A sends a message to B indicating that he wants to call him
- B replies to A with a WebRTC offer
- A obtains the offer and replies with a WebRTC answer.
- The DataChannel is established
- When the DataChannel on B side is open, he sends "Hello, world!" to A
When the DataChannel on A side is open, he sends "It works!" to B; this can happen in the opposite order
- 无论使用何种浏览器,我应该修改哪些内容才能使其正常工作?(当然我知道它目前只适用于 Firefox 和 Chrome)
- 额外的可选问题,为什么我会收到多个 ICE 候选人,尤其是在成功建立连接之后?
我想我应该拥有最新的 Firefox 和 Crhome:resp.45 和 49,在 Windows 7 64 位上.
I think I should have the latest Firefox and Crhome: resp. 45 and 49, on Windows 7 64 bits.
下面是我的 JavaScript 代码;然后是几个场景对应的输出,最后是我通过阅读其他帖子和教程获得的一些想法.
Below is my JavaScript code; then the outputs corresponding to a few scenarios, and finally some thoughs I got so far by reading other posts and tutorials.
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 时的输出,效果很好:
Here is the output when firefox connects to firefox, what works perfectly :
来电者:
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 时的输出,失败的内容:
Here is the output when Chrome connects to Chrome, what fails :
来电者:
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 时的输出,失败的内容:
Here is the output when Firefox connects to Chrome, what fails:
Fiefox 来电者:
Fiefox caller:
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 来电显示:
Here is the output when Firefox connects to Chrome the other way round, what also fails:Chrome caller:
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 调用:
Firefox called:
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
现在,一些想法:
我多次读到应该在发送要约之前创建 DataChannel.因此,我尝试按如下方式修改我的代码以确保是这种情况:
I have read multiple times that the DataChannel should be created before the offer is sent. Thus I tried to modify my code as follows to make sure it's the case :
pc.createOffer(desc=>{pc.setLocalDescription(desc, _=>say("setLocalDescription 成功"), fail=>say("setLocalDescription failed:" + fail));say("发送报价至" + o.from);ws.send({type: 'RTCSignal', to: o.from, answer: true, sdp: desc});},fail=>say("createOffer 失败:"+fail), sdpOptions);pc.channel = createDataChannel(pc, 'chat');
pc.createOffer(desc=>{pc.setLocalDescription(desc, _=>say("setLocalDescription succeeded"), fail=>say("setLocalDescription failed: " + fail));say("Sending offer to " + o.from);ws.send({type: 'RTCSignal', to: o.from, answer: true, sdp: desc}); },fail=>say("createOffer failed: "+fail), sdpOptions);pc.channel = createDataChannel(pc, 'chat');
此修改不会改变 Firefox 的任何内容.它继续像以前一样工作.对于 Chrome,它仍然不起作用;但输出是不同的.以前,在我能够发送任何东西之前,就在调用 setRemoteDescription 之前,它似乎神秘地关闭了 DataChannel.然而,在这种情况下,我没有得到任何消息,DataChannel 保持连接状态.这是输出:
This modification doesn't change anything for firefox. It continues working as well as before.For Chrome, it still doesn't work; but the output is different. Previously, it semmed that the DataChannel is mysteriously closed before I'm able to send anything, just before calling setRemoteDescription.IN this case however, I don't get any news, the DataChannel stays in connecting state. Here is the output:
来电者:
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
无论如何似乎在这两种情况下都没有调用事件 RTCPeerConnection.ondatachannel .我有一种感觉,我不太清楚我的处理程序是否从未被调用过,或者连接是否没有很好地建立.
Anyway it seems that in none of the two cases, the event RTCPeerConnection.ondatachannel is never called. I have the feeling that I can't really know well if my handler is just never called, or if the connection hasn't well been established.
我也尝试过在另一个时刻创建 DataChannel 没有成功.例如,在双方都调用了 setRemoteDescription 之后.在这种情况下,Firefox 拒绝创建报价,因为我既没有请求音频/视频,也没有请求轨道(我不知道它是什么)和数据通道(它还没有创建).所以到目前为止我的结论是在发送报价之前创建渠道是正确的方法;至少是唯一一个可以与 Firefox 配合使用的.
I have also tried to create the DataChannel at another moment without success. For example, after setRemoteDescription has been called on both side.IN that case, Firefox refuses to create an offer, because I'm neither requesting audio/video, nor a track (I don't know what it is) and nor a DataChannel (it hasn't been created yet).So my conclusion so far is that creating the channel before sending the offer is the correct way; at least the only one that will work with Firefox.
我也读过很多次,因为我没有要求音频/视频,所以我没有义务发送报价和答复.但是如果我从我的代码中挤出它,似乎什么也没有发生.没有ICE服务器交换等等...在其他地方,我读到在调用 setLocalDescription 之前没有任何 ICE 服务器内容启动.所以我必须调用 setLocalDescription,因此我必须创建一个报价.从那里开始,我必须通过信令通道将其发送给另一个对等方,我必须调用 setRemoteDescription 然后被要求回答,这似乎是合乎逻辑的.
I have also read many times that, given that I'm not requesting audio/video, I'm not obliged to send an offer and an answer. But if I squeeze that out from my code, nothing seems to happen. No ICE server exchange and so on...Somewhere else, I read that no ICE server stuff starts before setLocalDescription is called. So I must call setLocalDescription, and therefore I must create an offer. From there it seems logical that I'm obliged to send it to the other peer via the signaling channel, that I'm obliged to call setRemoteDescription and then required to answer.
我在我的代码中使用 sdpOptions = {mandatory: { OfferToReceiveAudio: true, OfferToReceiveVideo: false } };`` 虽然我不打算发送音频/视频流.在注意到如果我将它们都设置为 false 之前,我已经在 google 上搜索了很多,然后 Chrome 永远不会启动它的 ICE 服务器,因此不可能有任何 P2P 连接.
I'm using sdpOptions = {mandatory: { OfferToReceiveAudio: true, OfferToReceiveVideo: false } };`` in my code, although I don't plan to send audio/video streams.I have already googled a lot before noticing that if I set them both to false, then Chrome never starts its ICE server thing, and thus there couldn't be any P2P connection.
还有这个:{DtlsSrtpKeyAgreement: true}, {RtpDataChannels: true }]
我从教程中复制了它,但并不真正知道它的作用.无论如何,将其全部删除,或将其中一个设置为 false 不会对我的结果产生任何影响.
And this one: {DtlsSrtpKeyAgreement: true}, {RtpDataChannels: true }]
I copied it from a tutorial without really knowing what it does. Anyway, removing it alltogether, or setting one or the other to false doesn't change anything to my results.
感谢您阅读这么长的帖子.我希望你知道我如何解决这个问题.请告诉我我应该做什么,或者至少给我一些可能的线索.
Thank you for reading a so long post. I hope you have an idea how I could solve the problem.Please telle me what I should do or at least give me clues on what it could be.
非常感谢您的帮助.
天哪!似乎我所有的代码行都被折叠在一个大行中.非常抱歉,出乎意料.请在评论中告诉我下次如何解决此问题.谢谢.
EDit: OMG! It seems that all my lines of code have been collapsed together in a single big line. I'm very sorry, it wasn't expected. Telle me how to fix this for the next time in a little comment. Thank you.
推荐答案
删除这个:
pcOptions = { optional: [
{DtlsSrtpKeyAgreement: true}, {RtpDataChannels: true }]
},
这是旧的非标准 chrome 东西,在 Firefox 中什么也不做,但会导致蝙蝠在 Chrome 中飞出.在规范中,数据通道不运行在 rtp 上,也不依赖于 srtp.
It's old non-standard chrome stuff that does nothing in Firefox, but causes bats to fly out in Chrome. Data channels don't run over rtp, nor rely on srtp, in the spec.
当你在做的时候,也删除它:
While you're at it, remove this as well:
sdpOptions = {mandatory: {OfferToReceiveAudio: true, OfferToReceiveVideo: false}};
格式已更改为(注意小写的o"):
The format has changed to (note the lower-case 'o's):
sdpOptions = { offerToReceiveAudio: true, offerToReceiveVideo: false};
但这对于数据通道来说是不必要的.如果还是不行,请告诉我.
But it's unnecessary for just data channels. If it still doesn't work, let me know.
我还强烈推荐 adapter.js,官方的 WebRTC polyfill,它可以让您使用最新的规范与承诺等像这样.一个 shim 不仅仅是一个图书馆,它的目标是最终消失.
I also highly recommend adapter.js, the official WebRTC polyfill, which lets you use the latest spec with promises etc. like this. A shim more than a library, it aims to eventually disappear.
这篇关于WebRTC DataChannel:适用于 Firefox 而不是 Chrome的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!