WebRTC三种通信方式造成此错误:未能设置远程应答sdp:在错误状态下调用:稳定



我一直在尝试创建三种类似的通信方式:

用户1向用户2和用户3发送邀请

用户2接收来自用户1的邀请并回答。同时,User2创建另一个邀请,并将其发送给user3

user3同时回答User1和User2的报价。

为了实现这一点,我复制了两次RTCPeerConnection。然而,用户1和2之间的连接已正确建立,但用户3无法加入呼叫。我一直在犯这个错误。

Error InvalidStateError: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: stable

这是我的代码:

"use strict";
// Get our hostname
var myHostname = window.location.hostname;
console.log("Hostname: " + myHostname);
// WebSocket chat/signaling channel variables.
var connection = null;
var clientID = 0;

var mediaConstraints = {
audio: false,            // We want an audio track
video: true             // ...and we want a video track
};
var myUsername = null;
var targetUsername = null;      // To store username of other peer
var targetUsername2 = 'User3'; 
var myPeerConnection = null;    // RTCPeerConnection
var myPeerConnection2 = null;    // RTCPeerConnection

// To work both with and without addTrack() we need to note
// if it's available
var hasAddTrack = false;
var hasAddTrack2 = false;

function log_error(text) {
var time = new Date();
console.error("[" + time.toLocaleTimeString() + "] " + text);
}
// Send a JavaScript object by converting it to JSON and sending
// it as a message on the WebSocket connection.
function sendToServer(msg) {
var msgJSON = JSON.stringify(msg);
console.log("Sending '" + msg.type + "' message: " + msgJSON);
connection.send(msgJSON);
}
function setUsername() {
myUsername = document.getElementById("name").value;
sendToServer({
name: myUsername,
date: Date.now(),
id: clientID,
type: "username"
});
}
// Open and configure the connection to the WebSocket server.
function connect() {
var serverUrl;
var scheme = "ws";

if (document.location.protocol === "https:") {
scheme += "s";
}
serverUrl = scheme + "://" + myHostname + ":443";
connection = new WebSocket(serverUrl, "json");
connection.onopen = function(evt) {
};
connection.onerror = function(evt) {
console.dir(evt);
}
connection.onmessage = function(evt) {
var text = "";
var msg = JSON.parse(evt.data);
console.log("Message received: ");
console.dir(msg);
var time = new Date(msg.date);
var timeStr = time.toLocaleTimeString();
switch(msg.type) {
case "id":
clientID = msg.id;
setUsername();
break;
case "rejectusername":
myUsername = msg.name;
break;
case "userlist":      // Received an updated user list
handleUserlistMsg(msg);
break;
// Signaling messages: these messages are used to trade WebRTC
// signaling information during negotiations leading up to a video
// call.
case "video-offer":  // Invitation and offer to chat
handleVideoOfferMsg(msg);
handleVideoOfferMsg2(msg);
break;
case "video-answer":  // Callee has answered our offer
handleVideoAnswerMsg(msg);
handleVideoAnswerMsg2(msg);
break;
case "new-ice-candidate": // A new ICE candidate has been received
handleNewICECandidateMsg(msg);
handleNewICECandidateMsg2(msg);
break;
case "hang-up": // The other peer has hung up the call
handleHangUpMsg(msg);
break;
// Unknown message; output to console for debugging.
default:
log_error("Unknown message received:");
log_error(msg);
}
};
}

function createPeerConnection() {
console.log("Setting up a connection (myPeerConnection)");
// Create an RTCPeerConnection which knows to use our chosen
// STUN server.
myPeerConnection = new RTCPeerConnection({
iceServers: [     // Information about ICE servers - Use your own!
{
url: 'stun:stun.l.google.com:19302'
},
{
url: 'turn:numb.viagenie.ca',
credential: 'muazkh',
username: 'webrtc@live.com'
}
]
});

// Do we have addTrack()? If not, we will use streams instead.
hasAddTrack = (myPeerConnection.addTrack !== undefined);
// Set up event handlers for the ICE negotiation process.
myPeerConnection.onicecandidate = handleICECandidateEvent;
myPeerConnection.onremovestream = handleRemoveStreamEvent;
myPeerConnection.oniceconnectionstatechange = handleICEConnectionStateChangeEvent;
myPeerConnection.onicegatheringstatechange = handleICEGatheringStateChangeEvent;
myPeerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
myPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent;
// Because the deprecation of addStream() and the addstream event is recent,
// we need to use those if addTrack() and track aren't available.
if (hasAddTrack) {
myPeerConnection.ontrack = handleTrackEvent;
} else {
myPeerConnection.onaddstream = handleAddStreamEvent;
}
}



//Clone
function createPeerConnection2() {
console.log("Setting up a connection... (myPeerConnection2)");
// Create an RTCPeerConnection which knows to use our chosen
// STUN server.
myPeerConnection2 = new RTCPeerConnection({
iceServers: [     // Information about ICE servers - Use your own!
{
url: 'stun:stun.l.google.com:19302'
},
{
url: 'turn:numb.viagenie.ca',
credential: 'muazkh',
username: 'webrtc@live.com'
}
]
});
// Do we have addTrack()? If not, we will use streams instead.
hasAddTrack2 = (myPeerConnection2.addTrack !== undefined);
// Set up event handlers for the ICE negotiation process.
myPeerConnection2.onicecandidate = handleICECandidateEvent2;
myPeerConnection2.onremovestream = handleRemoveStreamEvent2;
myPeerConnection2.oniceconnectionstatechange = handleICEConnectionStateChangeEvent2;
myPeerConnection2.onicegatheringstatechange = handleICEGatheringStateChangeEvent2;
myPeerConnection2.onsignalingstatechange = handleSignalingStateChangeEvent2;
myPeerConnection2.onnegotiationneeded = handleNegotiationNeededEvent2;
// Because the deprecation of addStream() and the addstream event is recent,
// we need to use those if addTrack() and track aren't available.
if (hasAddTrack2) {
myPeerConnection2.ontrack = handleTrackEvent2;
} else {
myPeerConnection2.onaddstream = handleAddStreamEvent2;
}
}

function handleNegotiationNeededEvent() {
console.log("*** Negotiation needed");
console.log("---> Creating offer For myPeerConnection1");
myPeerConnection.createOffer().then(function(offer) {
console.log("---> Creating new description object to send to remote peer (myPeerConnection)");
return myPeerConnection.setLocalDescription(offer);
})
.then(function() {
console.log("---> Sending offer to remote peer (myPeerConnection1)");
sendToServer({
name: myUsername,
target: targetUsername,
type: "video-offer",
sdp: myPeerConnection.localDescription
});
})
.catch(reportError);
}

//Clone
function handleNegotiationNeededEvent2() {
console.log("*** Negotiation needed");
console.log("---> Creating offer For myPeerConnection2");
myPeerConnection2.createOffer().then(function(offer) {
console.log("---> Creating new description object to send to remote peer (myPeerConnection2)");
return myPeerConnection2.setLocalDescription(offer);
})
.then(function() {
console.log("---> Sending offer to remote peer (myPeerConnection2)");
sendToServer({
name: myUsername,
target: targetUsername2,
type: "video-offer",
sdp: myPeerConnection2.localDescription
});
})
.catch(reportError);
}

function handleTrackEvent(event) {
console.log("*** Track event");
document.getElementById("received_video").srcObject = event.streams[0];
document.getElementById("hangup-button").disabled = false;
}
function handleTrackEvent2(event) {
console.log("*** Track event");
document.getElementById("received_video2").srcObject = event.streams[0];
document.getElementById("hangup-button").disabled = false;
}
// Called by the WebRTC layer when a stream starts arriving from the
// remote peer. We use this to update our user interface, in this
// example.
function handleAddStreamEvent(event) {
console.log("*** Stream added");
document.getElementById("received_video").srcObject = event.stream;
document.getElementById("hangup-button").disabled = false;
}
function handleAddStreamEvent2(event) {
console.log("*** Stream added");
document.getElementById("received_video2").srcObject = event.stream;
document.getElementById("hangup-button").disabled = false;
}

function handleRemoveStreamEvent(event) {
console.log("*** Stream removed");
closeVideoCall();
}
//Clone
function handleRemoveStreamEvent2(event) {
console.log("*** Stream removed");
closeVideoCall();
}

function handleICECandidateEvent(event) {
if (event.candidate) {
console.log("Outgoing ICE candidate: " + event.candidate.candidate);
sendToServer({
type: "new-ice-candidate",
target: targetUsername,
candidate: event.candidate
});
}
}

//Clone
function handleICECandidateEvent2(event) {
if (event.candidate) {
console.log("handleICECandidateEvent2")
console.log("Outgoing ICE candidate: " + event.candidate.candidate);
sendToServer({
type: "new-ice-candidate",
target: targetUsername2,
candidate: event.candidate
});
}
}

function handleICEConnectionStateChangeEvent(event) {
console.log("*** ICE connection state changed to " + myPeerConnection.iceConnectionState);
switch(myPeerConnection.iceConnectionState) {
case "closed":
case "failed":
case "disconnected":
closeVideoCall();
break;
}
}

//Clone
function handleICEConnectionStateChangeEvent2(event) {
console.log("*** ICE connection state changed to " + myPeerConnection2.iceConnectionState);
switch(myPeerConnection2.iceConnectionState) {
case "closed":
case "failed":
case "disconnected":
closeVideoCall();
break;
}
}

function handleSignalingStateChangeEvent(event) {
console.log("*** WebRTC signaling state changed to: " + myPeerConnection.signalingState);
switch(myPeerConnection.signalingState) {
case "closed":
closeVideoCall();
break;
}
}

//Clone
function handleSignalingStateChangeEvent2(event) {
console.log("*** WebRTC signaling state changed to: " + myPeerConnection2.signalingState);
switch(myPeerConnection2.signalingState) {
case "closed":
closeVideoCall();
break;
}
}

function handleICEGatheringStateChangeEvent(event) {
console.log("*** ICE gathering state changed to: " + myPeerConnection.iceGatheringState);
}

function handleICEGatheringStateChangeEvent2(event) {
console.log("*** ICE gathering state changed to: " + myPeerConnection2.iceGatheringState);
}
// Given a message containing a list of usernames, this function
// populates the user list box with those names, making each item
// clickable to allow starting a video call.
function handleUserlistMsg(msg) {
var i;
var listElem = document.getElementById("userlistbox");

while (listElem.firstChild) {
listElem.removeChild(listElem.firstChild);
}
// Add member names from the received list
for (i=0; i < msg.users.length; i++) {
var item = document.createElement("li");
item.appendChild(document.createTextNode(msg.users[i]));
item.addEventListener("click", invite, false);
listElem.appendChild(item);
}
}

function closeVideoCall() {
var remoteVideo = document.getElementById("received_video");
var remoteVideo2 = document.getElementById("received_video2");
var localVideo = document.getElementById("local_video");
console.log("Closing the call");
// Close the RTCPeerConnection
if (myPeerConnection) {
console.log("--> Closing the peer connection");
// Disconnect all our event listeners; we don't want stray events
// to interfere with the hangup while it's ongoing.
myPeerConnection.onaddstream = null;  // For older implementations
myPeerConnection.ontrack = null;      // For newer ones
myPeerConnection.onremovestream = null;
myPeerConnection.onnicecandidate = null;
myPeerConnection.oniceconnectionstatechange = null;
myPeerConnection.onsignalingstatechange = null;
myPeerConnection.onicegatheringstatechange = null;
myPeerConnection.onnotificationneeded = null;
// Stop the videos
if (remoteVideo.srcObject) {
remoteVideo.srcObject.getTracks().forEach(track => track.stop());
}
if (localVideo.srcObject) {
localVideo.srcObject.getTracks().forEach(track => track.stop());
}
remoteVideo.src = null;
localVideo.src = null;
// Close the peer connection
myPeerConnection.close();
myPeerConnection = null;
}

//Clone
if (myPeerConnection2) {
console.log("--> Closing the peer connection (myPeerConnection2)");
// Disconnect all our event listeners; we don't want stray events
// to interfere with the hangup while it's ongoing.
myPeerConnection2.onaddstream = null;  // For older implementations
myPeerConnection2.ontrack = null;      // For newer ones
myPeerConnection2.onremovestream = null;
myPeerConnection2.onnicecandidate = null;
myPeerConnection2.oniceconnectionstatechange = null;
myPeerConnection2.onsignalingstatechange = null;
myPeerConnection2.onicegatheringstatechange = null;
myPeerConnection2.onnotificationneeded = null;
// Stop the videos
if (remoteVideo2.srcObject) {
remoteVideo2.srcObject.getTracks().forEach(track => track.stop());
}
if (localVideo.srcObject) {
localVideo.srcObject.getTracks().forEach(track => track.stop());
}
remoteVideo2.src = null;
localVideo.src = null;
// Close the peer connection
myPeerConnection2.close();
myPeerConnection2 = null;
}
// Disable the hangup button
document.getElementById("hangup-button").disabled = true;
targetUsername = null;
}
// Handle the "hang-up" message, which is sent if the other peer
// has hung up the call or otherwise disconnected.
function handleHangUpMsg(msg) {
console.log("*** Received hang up notification from other peer");
closeVideoCall();
}
// Hang up the call by closing our end of the connection, then
// sending a "hang-up" message to the other peer (keep in mind that
// the signaling is done on a different connection). This notifies
// the other peer that the connection should be terminated and the UI
// returned to the "no call in progress" state.
function hangUpCall() {
closeVideoCall();
sendToServer({
name: myUsername,
target: targetUsername,
type: "hang-up"
});
//Clone
closeVideoCall();
sendToServer({
name: myUsername,
target: targetUsername2,
type: "hang-up"
});
}
// Handle a click on an item in the user list by inviting the clicked
// user to video chat. Note that we don't actually send a message to
// the callee here -- calling RTCPeerConnection.addStream() issues
// a |notificationneeded| event, so we'll let our handler for that
// make the offer.
function invite(evt) {
console.log("Starting to prepare an invitation");
if (myPeerConnection) {
alert("You can't start a call because you already have one open!");
} else {
var clickedUsername = evt.target.textContent;
// Don't allow users to call themselves, because weird.
if (clickedUsername === myUsername) {
alert("I'm afraid I can't let you talk to yourself. That would be weird.");
return;
}
// Record the username being called for future reference
targetUsername = clickedUsername;
console.log("Inviting user " + targetUsername);
// Call createPeerConnection() to create the RTCPeerConnection.
console.log("Setting up connection to invite user: " + targetUsername );
createPeerConnection();
console.log("Setting up connection to invite user: " + targetUsername2);
createPeerConnection2();
// Now configure and create the local stream, attach it to the
// "preview" box (id "local_video"), and add it to the
// RTCPeerConnection.
console.log("Requesting webcam access...");
navigator.mediaDevices.getUserMedia(mediaConstraints)
.then(function(localStream) {
console.log("-- Local video stream obtained");
document.getElementById("local_video").srcObject = localStream;
if (hasAddTrack) {
console.log("-- Adding tracks to the RTCPeerConnection");
localStream.getTracks().forEach(track => myPeerConnection.addTrack(track, localStream));
} else {
console.log("-- Adding stream to the RTCPeerConnection");
myPeerConnection.addStream(localStream);
}
if (hasAddTrack2) {
console.log("-- Adding tracks to the RTCPeerConnection2");
localStream.getTracks().forEach(track => myPeerConnection2.addTrack(track, localStream));
} else {
console.log("-- Adding stream to the RTCPeerConnection2");
myPeerConnection2.addStream(localStream);
}
})
.catch(handleGetUserMediaError);
}
}
// Accept an offer to video chat. We configure our local settings,
// create our RTCPeerConnection, get and attach our local camera
// stream, then create and send an answer to the caller.
function handleVideoOfferMsg(msg) {
var localStream = null;
targetUsername = msg.name;
// Call createPeerConnection() to create the RTCPeerConnection.
console.log("Starting to accept invitation from " + targetUsername);
createPeerConnection();
// We need to set the remote description to the received SDP offer
// so that our local WebRTC layer knows how to talk to the caller.
var desc = new RTCSessionDescription(msg.sdp);
myPeerConnection.setRemoteDescription(desc).then(function () {
console.log("Setting up the local media stream (myPeerConnection1)");
return navigator.mediaDevices.getUserMedia(mediaConstraints);
})
.then(function(stream) {
console.log("-- Local video stream obtained");
localStream = stream;
document.getElementById("local_video").srcObject = localStream;
if (hasAddTrack) {
console.log("-- Adding tracks to the RTCPeerConnection");
localStream.getTracks().forEach(track =>
myPeerConnection.addTrack(track, localStream)
);
} else {
console.log("-- Adding stream to the RTCPeerConnection");
myPeerConnection.addStream(localStream);
}
})
.then(function() {
console.log("------> Creating answer");
// Now that we've successfully set the remote description, we need to
// start our stream up locally then create an SDP answer. This SDP
// data describes the local end of our call, including the codec
// information, options agreed upon, and so forth.
return myPeerConnection.createAnswer();
})
.then(function(answer) {
console.log("------> Setting local description after creating answer");
// We now have our answer, so establish that as the local description.
// This actually configures our end of the call to match the settings
// specified in the SDP.
return myPeerConnection.setLocalDescription(answer);
})
.then(function() {
var msg = {
name: myUsername,
target: targetUsername,
type: "video-answer",
sdp: myPeerConnection.localDescription
};
// We've configured our end of the call now. Time to send our
// answer back to the caller so they know that we want to talk
// and how to talk to us.
console.log("Sending answer packet back to other peer");
sendToServer(msg);
})
.catch(handleGetUserMediaError);
}



//Clone
function handleVideoOfferMsg2(msg) {
var localStream = null;
// Call createPeerConnection() to create the RTCPeerConnection.
console.log("Starting to accept invitation from " + targetUsername2);
createPeerConnection2();
// We need to set the remote description to the received SDP offer
// so that our local WebRTC layer knows how to talk to the caller.
var desc2 = new RTCSessionDescription(msg.sdp);
myPeerConnection2.setRemoteDescription(desc2).then(function () {
console.log("Setting up the local media stream... (myPeerConnection2)");
return navigator.mediaDevices.getUserMedia(mediaConstraints);
})
.then(function(stream) {
console.log("-- Local video stream obtained");
localStream = stream;
document.getElementById("local_video").srcObject = localStream;
if (hasAddTrack2) {
console.log("-- Adding tracks to the RTCPeerConnection (myPeerConnection2)");
localStream.getTracks().forEach(track =>
myPeerConnection2.addTrack(track, localStream)
);
} else {
console.log("-- Adding stream to the RTCPeerConnection (myPeerConnection2)");
myPeerConnection2.addStream(localStream);
}
})
.then(function() {
console.log("------> Creating answer (myPeerConnection2)");
// Now that we've successfully set the remote description, we need to
// start our stream up locally then create an SDP answer. This SDP
// data describes the local end of our call, including the codec
// information, options agreed upon, and so forth.
return myPeerConnection2.createAnswer();
})
.then(function(answer) {
console.log("------> Setting local description after creating answer (myPeerConnection2)");
// We now have our answer, so establish that as the local description.
// This actually configures our end of the call to match the settings
// specified in the SDP.
return myPeerConnection2.setLocalDescription(answer);
})
.then(function() {
var msg = {
name: myUsername,
target: targetUsername2,
type: "video-answer",
sdp: myPeerConnection2.localDescription
};
// We've configured our end of the call now. Time to send our
// answer back to the caller so they know that we want to talk
// and how to talk to us.
console.log("Sending answer packet back to other peer (myPeerConnection2)");
sendToServer(msg);
})
.catch(handleGetUserMediaError);
}
// Responds to the "video-answer" message sent to the caller
// once the callee has decided to accept our request to talk.
function handleVideoAnswerMsg(msg) {
console.log("Call recipient has accepted our call");
// Configure the remote description, which is the SDP payload
// in our "video-answer" message.
var desc = new RTCSessionDescription(msg.sdp);
myPeerConnection.setRemoteDescription(desc)
.catch(reportError);
}
function handleVideoAnswerMsg2(msg) {
console.log("Call recipient has accepted our call");
// Configure the remote description, which is the SDP payload
// in our "video-answer" message.
var desc2 = new RTCSessionDescription(msg.sdp);
myPeerConnection2.setRemoteDescription(desc2)
.catch(reportError);
}
// A new ICE candidate has been received from the other peer. Call
// RTCPeerConnection.addIceCandidate() to send it along to the
// local ICE framework.
function handleNewICECandidateMsg(msg) {
var candidate = new RTCIceCandidate(msg.candidate);
console.log("Adding received ICE candidate: " + JSON.stringify(candidate));
myPeerConnection.addIceCandidate(candidate)
}

function handleNewICECandidateMsg2(msg) {
var candidate = new RTCIceCandidate(msg.candidate);
console.log("Adding received ICE candidate: " + JSON.stringify(candidate));
myPeerConnection2.addIceCandidate(candidate).catch(reportError);
}
// Handle errors which occur when trying to access the local media
// hardware; that is, exceptions thrown by getUserMedia(). The two most
// likely scenarios are that the user has no camera and/or microphone
// or that they declined to share their equipment when prompted. If
// they simply opted not to share their media, that's not really an
// error, so we won't present a message in that situation.
function handleGetUserMediaError(e) {
console.log(e);
switch(e.name) {
case "NotFoundError":
alert("Unable to open your call because no camera and/or microphone" +
"were found.");
break;
case "SecurityError":
case "PermissionDeniedError":
// Do nothing; this is the same as the user canceling the call.
break;
default:
alert("Error opening your camera and/or microphone: " + e.message);
break;
}
// Make sure we shut down our end of the RTCPeerConnection so we're
// ready to try again.
closeVideoCall();
}
// Handles reporting errors. Currently, we just dump stuff to console but
// in a real-world application, an appropriate (and user-friendly)
// error message should be displayed.
function reportError(errMessage) {
log_error("Error " + errMessage.name + ": " + errMessage.message);
}

你可以在这里找到日志文件

您需要更好地组织代码,一切都会更好地工作。使用、Object、constructor或对象创建者,发送peer的id以找到正确的peer。当您调用handleVideoAnswerMsg时,您的错误消息似乎来自带有msg-switch语句的代码,您在2对等操作上设置了sdp,所以第二次第一次投掷时,第二次永远不会被调用。你可以添加一个id来选择正确的id例如:

const id = msg.id;
case "video-offer":  // Invitation and offer to chat
handleVideoOffer(id, msg.sdp);
break;
case "video-answer":  // Callee has answered our offer
handleVideoAnswer(id, msg.sdp);;
break;
case "new-ice-candidate": // A new ICE candidate has been received
handleNewICECandidate(id, msg.ice);

您还可以在每种情况下调用createPeerConnection(1/2(

最新更新