数日間、私は今、webRTCクライアントを動作させようとしていますが、何が間違っているのかわかりません。マルチピアwebrtcクライアントを作成しようとしていますが、Chromeで両側をテストしています。呼び出し先が呼び出しを受信して応答を作成すると、次のエラーが表示されます。
Failed to set local answer sdp: Called in wrong state: kStable
受信側は両方のビデオ接続を正しく確立し、ローカルおよびリモートストリームを表示しています。しかし、発信者は着信者の回答を受け取らないようです。誰かが私がここで間違っていることを教えてもらえますか?
私が使用しているコードは次のとおりです(関連する部分を表示し、読みやすくするためのストリップバージョンです)
class WebRTC_Client
{
private peerConns = {};
private sendAudioByDefault = true;
private sendVideoByDefault = true;
private offerOptions = {
offerToReceiveAudio: true,
offerToReceiveVideo: true
};
private constraints = {
"audio": true,
"video": {
frameRate: 5,
width: 256,
height: 194
}
};
private serversCfg = {
iceServers: [{
urls: ["stun:stun.l.google.com:19302"]
}]
};
private SignalingChannel;
public constructor(SignalingChannel){
this.SignalingChannel = SignalingChannel;
this.bindSignalingHandlers();
}
/*...*/
private gotStream(stream) {
(<any>window).localStream = stream;
this.videoAssets[0].srcObject = stream;
}
private stopLocalTracks(){}
private start() {
var self = this;
if( !this.isReady() ){
console.error('Could not start WebRTC because no WebSocket user connectionId had been assigned yet');
}
this.buttonStart.disabled = true;
this.stopLocalTracks();
navigator.mediaDevices.getUserMedia(this.getConstrains())
.then((stream) => {
self.gotStream(stream);
self.SignalingChannel.send(JSON.stringify({type: 'onReadyForTeamspeak'}));
})
.catch(function(error) { trace('getUserMedia error: ', error); });
}
public addPeerId(peerId){
this.availablePeerIds[peerId] = peerId;
this.preparePeerConnection(peerId);
}
private preparePeerConnection(peerId){
var self = this;
if( this.peerConns[peerId] ){
return;
}
this.peerConns[peerId] = new RTCPeerConnection(this.serversCfg);
this.peerConns[peerId].ontrack = function (evt) { self.gotRemoteStream(evt, peerId); };
this.peerConns[peerId].onicecandidate = function (evt) { self.iceCallback(evt, peerId); };
this.peerConns[peerId].onnegotiationneeded = function (evt) { if( self.isCallingTo(peerId) ) { self.createOffer(peerId); } };
this.addLocalTracks(peerId);
}
private addLocalTracks(peerId){
var self = this;
var localTracksCount = 0;
(<any>window).localStream.getTracks().forEach(
function (track) {
self.peerConns[peerId].addTrack(
track,
(<any>window).localStream
);
localTracksCount++;
}
);
trace('Added ' + localTracksCount + ' local tracks to remote peer #' + peerId);
}
private call() {
var self = this;
trace('Start calling all available new peers if any available');
// only call if there is anyone to call
if( !Object.keys(this.availablePeerIds).length ){
trace('There are no callable peers available that I know of');
return;
}
for( let peerId in this.availablePeerIds ){
if( !this.availablePeerIds.hasOwnProperty(peerId) ){
continue;
}
this.preparePeerConnection(peerId);
}
}
private createOffer(peerId){
var self = this;
this.peerConns[peerId].createOffer( this.offerOptions )
.then( function (offer) { return self.peerConns[peerId].setLocalDescription(offer); } )
.then( function () {
trace('Send offer to peer #' + peerId);
self.SignalingChannel.send(JSON.stringify({ "sdp": self.peerConns[peerId].localDescription, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));
})
.catch(function(error) { self.onCreateSessionDescriptionError(error); });
}
private answerCall(peerId){
var self = this;
trace('Answering call from peer #' + peerId);
this.peerConns[peerId].createAnswer()
.then( function (answer) { return self.peerConns[peerId].setLocalDescription(answer); } )
.then( function () {
trace('Send answer to peer #' + peerId);
self.SignalingChannel.send(JSON.stringify({ "sdp": self.peerConns[peerId].localDescription, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));
})
.catch(function(error) { self.onCreateSessionDescriptionError(error); });
}
private onCreateSessionDescriptionError(error) {
console.warn('Failed to create session description: ' + error.toString());
}
private gotRemoteStream(e, peerId) {
if (this.audioAssets[peerId].srcObject !== e.streams[0]) {
this.videoAssets[peerId].srcObject = e.streams[0];
trace('Added stream source of remote peer #' + peerId + ' to DOM');
}
}
private iceCallback(event, peerId) {
this.SignalingChannel.send(JSON.stringify({ "candidate": event.candidate, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));
}
private handleCandidate(candidate, peerId) {
this.peerConns[peerId].addIceCandidate(candidate)
.then(
this.onAddIceCandidateSuccess,
this.onAddIceCandidateError
);
trace('Peer #' + peerId + ': New ICE candidate: ' + (candidate ? candidate.candidate : '(null)'));
}
private onAddIceCandidateSuccess() {
trace('AddIceCandidate success.');
}
private onAddIceCandidateError(error) {
console.warn('Failed to add ICE candidate: ' + error.toString());
}
private hangup() {}
private bindSignalingHandlers(){
this.SignalingChannel.registerHandler('onWebRTCPeerConn', (signal) => this.handleSignals(signal));
}
private handleSignals(signal){
var self = this,
peerId = signal.connectionId;
if( signal.sdp ) {
trace('Received sdp from peer #' + peerId);
this.peerConns[peerId].setRemoteDescription(new RTCSessionDescription(signal.sdp))
.then( function () {
if( self.peerConns[peerId].remoteDescription.type === 'answer' ){
trace('Received sdp answer from peer #' + peerId);
} else if( self.peerConns[peerId].remoteDescription.type === 'offer' ){
trace('Received sdp offer from peer #' + peerId);
self.answerCall(peerId);
} else {
trace('Received sdp ' + self.peerConns[peerId].remoteDescription.type + ' from peer #' + peerId);
}
})
.catch(function(error) { trace('Unable to set remote description for peer #' + peerId + ': ' + error); });
} else if( signal.candidate ){
this.handleCandidate(new RTCIceCandidate(signal.candidate), peerId);
} else if( signal.closeConn ){
trace('Closing signal received from peer #' + peerId);
this.endCall(peerId,true);
}
}
}
メソッド RTCPeerConnection.addTrack を2回呼び出して(オーディオトラック用とビデオトラック用)、同様の構成体を使用して、送信者と受信者のピア間のWebRTC接続を構築しています。
Stage 2The Web Evolution 1.0の進化 :に示す例と同じ構造を使用しました。
let pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection(), stream, videoTrack, videoSender;
(async () => {
try {
stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
videoTrack = stream.getVideoTracks()[0];
pc1.addTrack(stream.getAudioTracks()[0], stream);
} catch (e) {
console.log(e);
}
})();
checkbox.onclick = () => {
if (checkbox.checked) {
videoSender = pc1.addTrack(videoTrack, stream);
} else {
pc1.removeTrack(videoSender);
}
}
pc2.ontrack = e => {
video.srcObject = e.streams[0];
e.track.onended = e => video.srcObject = video.srcObject; // Chrome/Firefox bug
}
pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
pc1.onnegotiationneeded = async e => {
try {
await pc1.setLocalDescription(await pc1.createOffer());
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription(await pc2.createAnswer());
await pc1.setRemoteDescription(pc2.localDescription);
} catch (e) {
console.log(e);
}
}
ここでテストしてください: https://jsfiddle.net/q8Lw39fd/
お気づきのとおり、この例ではcreateOffer
メソッドが直接呼び出されることはありません。代わりに、 RTCPeerConnection.onnegotiationneeded イベントをトリガーするaddTrack
を介して間接的に呼び出されます。
ただし、あなたの場合と同様に、Chromeはこのイベントをトリガーしますtwice、各トラックに1回、これによりあなたが言及したエラーメッセージ:
DOMException:ローカル応答の設定に失敗しましたsdp:間違った状態で呼び出されました:kStable
ちなみに、これはFirefoxでは発生しません。イベントをトリガーするのは一度だけです。
この問題の解決策は、Chromeの動作に対する回避策を記述することです:(再)ネゴシエーションメカニズムへのネストされた呼び出しを防ぐガード。
修正された例の関連部分は次のようになります。
pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
var isNegotiating = false; // Workaround for Chrome: skip nested negotiations
pc1.onnegotiationneeded = async e => {
if (isNegotiating) {
console.log("SKIP nested negotiations");
return;
}
isNegotiating = true;
try {
await pc1.setLocalDescription(await pc1.createOffer());
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription(await pc2.createAnswer());
await pc1.setRemoteDescription(pc2.localDescription);
} catch (e) {
console.log(e);
}
}
pc1.onsignalingstatechange = (e) => { // Workaround for Chrome: skip nested negotiations
isNegotiating = (pc1.signalingState != "stable");
}
ここでテストしてください: https://jsfiddle.net/q8Lw39fd/8/
このガードメカニズムを独自のコードに簡単に実装できるはずです。
ここに答えを送信しています:
.then( function (answer) { return self.peerConns[peerId].setLocalDescription(answer); } )
私のものを見てください:
var callback = function (answer) {
createdDescription(answer, fromId);
};
peerConnection[fromId].createAnswer().then(callback).catch(errorHandler);
function createdDescription(description, fromId) {
console.log('Got description');
peerConnection[fromId].setLocalDescription(description).then(function() {
console.log("Sending SDP:", fromId, description);
serverConnection.emit('signal', fromId, {'sdp': description});
}).catch(errorHandler);
}