私はWebRTCでかなり新しいです。オーディオもビデオも使用せずに、2 つのピア間に単純なデータ チャネルを確立しようとしています。ただのテキストデータ。最後はゲームマスターとなるピアに2~7人のピアが接続するゲームになります。
html5rocks、MDN、その他のスタックの投稿を何時間もグーグルで調べて読んだ後、私は多くのことを試しましたが、まだうまくいきません。
2 つの異なる Firefox タブでページを開くと、すべて正常に動作します。タブの 1 つが「Hello, world!」を送信していることがわかります。もう一方は「うまくいきました!」と送信します。DataChannel は十分に確立されており、両方のタブがそれぞれのピアのメッセージを取得します。
ただし、Chromeで実行すると機能しません。私のテストの 1 つでは、何かを送信する前に DataChannel が不思議なことに閉じられましたが、別のテストでは、RTCPeerConnection.ondatachannel イベントがまったく呼び出されていないようです (詳細は後述)。Firefox と Chrome を順番に通信させようとすると、setRemoteDescription の失敗に関するさまざまな謎のエラーが発生します。
もちろん、これらのケースのいずれにおいても、Web/JavaScript コンソールにエラー メッセージが表示されることはありません。簡単すぎたでしょう。
私の問題はシグナリング プロセスにあるわけではありません。少なくともそうは思いません。非常に単純な Node.js サーバーとの通信には、プレーンな WebSocket が使用されます。PeerJS などのライブラリの使用は避けたいと思います。まず第一に、手動で行うことで物事を学ぶほうがよいからです。第二に、シグナリング Node.js サーバーをシグナリング以外の目的で使用したいからです。それ自体は Node 側では問題ではありませんが、ブラウザ側の問題です (100 KB 以上の縮小/難読化されたソース コードの海で小さな雨滴を見つけるつもりはないため)。
基本的なシナリオは非常に単純です。現在接続しているユーザーのリストは、ページ上で 15 秒ごとに自動的に更新されます。ユーザー名をクリックすると、彼に接続され、「Hello, world!」が送信されます。彼が「それはうまくいく!」と答えている間。同時に; とりあえず食べる。シンプルなチャット テキスト ボックスは、基本的なコミュニケーションを設定できるようになったら、もちろん次の論理的なステップです。
より具体的には、ユーザー A がユーザー B をクリックすると、次のようになります。
- シグナリングWebSocketを介して、AはBに電話をかけたいことを示すメッセージを送信します
- B は WebRTC オファーで A に返信します
- A はオファーを取得し、WebRTC 回答で応答します。
- DataChannel が確立されました
- B 側の DataChannel が開くと、彼は「Hello, world!」を送信します。Aへ
A 側の DataChannel が開いたら、「It works!」と送信します。Bに; これは逆の順序で発生する可能性があります
- 使用するブラウザに関係なく機能させるには、何を変更すればよいですか? (もちろん、現在 Firefox と Chrome でしか動作しないことはわかっています)
- おまけのオプションの質問です。特に接続が正常に確立された後でも、複数の ICE 候補が表示されるのはなぜですか?
最新の Firefox と Chrome が必要だと思います。Windows 7 64 ビットでは、45 および 49。
以下は私の JavaScript コードです。次に、いくつかのシナリオに対応する出力、そして最後に、他の投稿やチュートリアルを読んでこれまでに得たいくつかの考えを示します。
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 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 の呼び出し:
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 を作成する必要があることを何度も読みました。したがって、次のようにコードを変更して、それが事実であることを確認しようとしました:
pc.createOffer(desc=>{ pc.setLocalDescription(desc, _=>say("setLocalDescription 成功"), fail=>say("setLocalDescription 失敗: " + 失敗)); say("オファーを送信中 " + o. from); ws.send({type: 'RTCSignal', to: o.from, answer: true, sdp: desc}); }, fail=>say("createOffer failed: "+fail), sdpOptions); pc.channel = createDataChannel(pc, 'チャット');
この変更は、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
いずれにしても、イベント RTCPeerConnection.ondatachannel は決して呼び出されないようです。ハンドラーがまったく呼び出されないのか、接続が確立されていないのか、よくわからないような気がします。
また、別の時点で DataChannel を作成しようとしましたが、成功しませんでした。たとえば、両側で setRemoteDescription が呼び出された後。その場合、Firefox はオファーの作成を拒否します。これは、オーディオ/ビデオも、トラック (それが何であるかわからない) も、DataChannel (まだ作成されていない) も要求していないためです。したがって、これまでのところ、オファーを送信する前にチャネルを作成するのが正しい方法であるというのが私の結論です。少なくとも Firefox で動作する唯一のものです。
また、オーディオ/ビデオを要求していないことを考えると、オファーと回答を送信する義務はないということも何度も読みました. しかし、コードからそれを絞り出しても、何も起こらないようです。ICEサーバーの交換などはありません...別の場所で、setLocalDescriptionが呼び出される前にICEサーバーが開始されないことを読みました。そのため、setLocalDescription を呼び出す必要があるため、オファーを作成する必要があります。そこから、シグナリング チャネルを介して他のピアに送信する義務があること、setRemoteDescription を呼び出す義務があること、および応答する必要があることは論理的に思えます。
オーディオ/ビデオ ストリームを送信する予定はありませんが、コードで sdpOptions = {mandatory: { OfferToReceiveAudio: true, OfferToReceiveVideo: false } };`` を使用しています。両方を false に設定すると、Chrome が ICE サーバーを開始しないため、P2P 接続ができなくなることに気付く前に、すでに多くのことをグーグルで検索しました。
そしてこれ:{DtlsSrtpKeyAgreement: true}, {RtpDataChannels: true }]
チュートリアルからコピーしたもので、実際に何をするのかよくわかりません。とにかく、それをすべて削除するか、どちらかを false に設定しても、結果は何も変わりません。
長い文章を読んでいただきありがとうございます。どうすれば問題を解決できるか考えていただければ幸いです。私が何をすべきか教えてください、または少なくともそれが何であるかについての手がかりを教えてください.
ご助力ありがとうございます。
編集:OMG!コードのすべての行が 1 つの大きな行にまとめられているようです。大変申し訳ありません、予想外でした。ちょっとしたコメントで、次回のためにこれを修正する方法を教えてください. ありがとうございました。