当第三个用户进入视频会议时,我收到以下错误:“DOMException:无法设置远程应答 sdp:在错误状态下调用:稳定”
我在这里看到这可能是因为对等点只能连接到一个对等点,因此如果我希望会议中有超过 2 个用户,我需要在每次新用户进入时创建一个新的 webRTC 连接,但这就是我的目的做过 ! 看看这里:
socket.on("user-joined", (id, clients, username, email) => {
console.log(`Utilisateur rejoint: ${username}, ID: ${id}`);
if (id !== socketId) {
this.getSources(); // ce salaud j'ai dû le metre ici aussi car en mode prod il faut le relancer quand un autre peer se connecte
message.success({
content: `${username} a rejoint la conférence.`,
className: "custom-message",
duration: 3,
})
// si l'id qui vient d'arriver ne correspond pas à mon socketId (moi) alors je play le sound de cette maniere,
//seul les utilisateurs déjà présents dans la room entendront le son si un new user arrive dans la room
}
clients.forEach((socketListId) => {
connections[socketListId] = this.createPeerConnection(socketListId); // HERE I CREATE A NEW USER FOR EACH NEW CONNECTION !
// c'est ici que j'initialise la connection P2P avec webRTC
console.log("Current state of connections:", connections);
// je collecte mes iceCandidates
connections[socketListId].onicecandidate = function (event) {
if (event.candidate != null) {
socket.emit(
"signal", // je spread mes icecandidate via "signal"
socketListId,
JSON.stringify({ ice: event.candidate })
)
}
}
// je check si un nouveau user (nouveau videoElement du coup) arrive dans la room
connections[socketListId].onaddstream = (event) => {
// c un event de webRTC go voir : https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addstream_event
let searchVideo = document.querySelector(
`[data-socket="${socketListId}"]`
)
if (!searchVideo) {
console.log(`Creating new video element for socketListId: ${socketListId}`);
videoElements = clients.length // videoElements = nbr de client connectés à la room..
console.log("videoElements: ", videoElements) // test adaptCSS
let main = document.getElementById("main")
let cssMesure = this.adaptCSS(main)
let video = document.createElement("video")
let css = {
minWidth: cssMesure.minWidth,
minHeight: cssMesure.minHeight,
maxHeight: "100%",
margin: "10px",
borderStyle: "solid",
borderColor: "#bdbdbd",
objectFit: "fill",
backgroundImage: `url(${backgroundBlck})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat'
};
for (let i in css) video.style[i] = css[i]
video.style.setProperty("width", cssMesure.width)
video.style.setProperty("height", cssMesure.height)
video.setAttribute("data-socket", socketListId)
video.style.borderRadius = "25px"
video.srcObject = event.stream
video.autoplay = true
video.playsinline = true
video.onclick = this.handleVideoClick
const videoId = "video_" + socketListId;
video.setAttribute("id", videoId);
// video.classList.add("video-with-username");
// video.setAttribute("data-username", username);
video.srcObject = event.stream;
main.appendChild(video)
this.adaptCSS(main);
}else{
console.log(`Updating existing video element for socketListId: ${socketListId}`);
searchVideo.srcObject = event.stream;
}
}
if (window.localStream instanceof MediaStream) {
// Ajout du stream à RTCPeerConnection..
console.log(`Adding stream to connection for user ${socketListId}`);
console.log("Current state of connections:", connections);
connections[socketListId].addStream(window.localStream);
} else {
message.error('Votre caméra n\'est pas disponible !!');
}
})
if (id === socketId) {
console.log("Local user has joined. Initiating offer creation logic.");
}
// Ici, je vais gérer le scénario au cas où un user se co à une salle avec des utilisateurs déjà présents.
//Cet utilisateur va envoyer son offre à tous les utilisateurs déjà présents dans la salle.
if (id === socketId) { // dans cette condition je veux être sûr que celui qui va envoyer son offer à tout lmonde est l'user qui vient de se connecter (genre moi localement quoi)
for (let otherUserId in connections) { // je loop à travers les autres utilisateurs dans la room
if (otherUserId === socketId) continue
let connection = connections[otherUserId];
console.log('ajout du stream à la connection pour user : ' + otherUserId);
try {
connection.addStream(window.localStream);
} catch (e) {
console.error('Erreur ajout stream à la co :', e);
continue; // Skip l'itérration pour passer à la suivante si erreur
}
console.log('Stream bien ajouté. Creation de l offre pour user : ' + otherUserId);
connection
.createOffer()
.then((description) => {
console.log('Offre creee avec succes!. Je set la local description pour user : ' + otherUserId);
connection.setLocalDescription(description);
})
.then(() => {
console.log('Local description -> OK . envoi du signal pour user ' + otherUserId);
socket.emit(
"signal",
otherUserId,
JSON.stringify({ sdp: connections[id].localDescription })
);
})
.catch((e) => console.error('Erreur durant création offer ou localdescription ): ', e));
}
}
这里有我的组件的大部分:
const server_url =
process.env.NODE_ENV === "production"
? "https://zakaribel.com:4001"
: "http://localhost:4001"
let connections = {}
let socket = null
let socketId = null
let videoElements = 0
const peerConnectionConfig = {
iceServers: [
{ urls: "stun:stun.services.mozilla.com" },
{ urls: "stun:stun.l.google.com:19302" },
// Serveur TURN (si un user est derrière un nat restrictif ou un pare feu chiant, un serveur TURN prendra le relais)
{
urls: "turn:turn.anyfirewall.com:443?transport=tcp",
credential: "webrtc",
username: "webrtc",
},
],
}
class Main extends Component {
constructor(props) {
super(props)
this.myVideo = React.createRef()
this.iceCandidatesQueue = {}
this.videoAvailable = false
this.audioAvailable = false
this.state = {
video: false,
audio: false,
screen: false,
showModal: false,
screenAvailable: false,
socketId: '',
messages: [],
message: "",
newmessages: 0,
askForUsername: true,
username: "",
usernames: {},
isSidebarOpen: false,
requestingSpeech: false,
speechRequestMessage: "",
password: "",
authorizedUsers: [],
connectedEmails: [],
currentUserEmail: "",
isAdmin: false,
loadingCamera: true,
}
axios
.get(`${server_url}/users`, { withCredentials: true } )
.then((response) => {
this.setState({ authorizedUsers: response.data })
})
.catch((error) => {
console.error(
"Erreur lors de la récupération des utilisateurs :",
error
)
})
this.sourcesPermissions()
}
sourcesPermissions = async () => {
try {
const videoStream = await navigator.mediaDevices.getUserMedia({
video: true,
})
const audioStream = await navigator.mediaDevices.getUserMedia({
audio: true,
})
const screenAvailable = !!navigator.mediaDevices.getDisplayMedia
this.setState({ screenAvailable })
if (videoStream || audioStream) {
window.localStream = videoStream || audioStream // je recupere le flux autorisé par l'user dans window.localStream
this.myVideo.current.srcObject = window.localStream // j'affiche ce flux dans mon element video
this.setState({ loadingCamera: false })
}
this.videoAvailable = !!videoStream // videoAvailable à true si videoStream est true et vis versa
this.audioAvailable = !!audioStream // meme délire
this.screenAvailable = screenAvailable
} catch (error) {
console.error(error)
this.setState({ loadingCamera: false })
}
}
getMediasAndInitConnection = () => {
this.setState(
{
video: this.videoAvailable, // videoAvailable = acces video autorisé par user donc this.state.video sera true
audio: this.audioAvailable, // ...
},
() => {
this.getSources() // llorsque l'utilisateur arrivera dans la room sa camera/son audio sera activée ou désactivée en fonction des permissions
this.serverConnection() // ensuite jenclenche la logique de connexion server notamment en recuperant l'username, l'email, signal pour SDP/iceCandidates etc....
}
)
}
getSources = () => {
if ((this.state.video && this.videoAvailable) || (this.state.audio && this.audioAvailable)) {
navigator.mediaDevices.getUserMedia({ video: this.state.video, audio: this.state.audio })
.then((stream) => {
this.getSources_Success(stream);
})
.catch((e) => console.log(e));
} else {
try {
if (this.myVideo.current && this.myVideo.current.srcObject) {
let tracks = this.myVideo.current.srcObject.getTracks();
tracks.forEach((track) => track.stop());
}
} catch (e) {
console.log(e);
}
}
};
getSources_Success = (stream) => {
window.localStream = stream
this.myVideo.current.srcObject = stream
for (let id in connections) {
if (id === socketId) continue // la jdis que si dans la liste des sockets ya une id qui correspond à MA socketId(moi)
connections[id].addStream(window.localStream)
// et ce sera envoyé aux autres users
connections[id]
.createOffer()
.then((description) => connections[id].setLocalDescription(description))
.then(() => {
// du coup j'envoie tout ça via une websocket..
// mon emission "signal" contiendra mon offer pour que tous les autres users puisse la receptionner
// createOffer qui va creer une SDP est un processus OBLIGATOIRE pour établir une connexion WebRTC
// c'est comme si t'allais à la banque pour ouvrir un compte et tu signes aucun papiers..
socket.emit(
"signal",
id,
JSON.stringify({ sdp: connections[id].localDescription })
)
})
.catch((e) => console.log(e))
}
}
// ici je vais réceptionner tout ce qui est SDP/iceCandidates
signalFromServer = (fromId, body) => {
let signal = JSON.parse(body);
if (fromId !== socketId) {
if (signal.sdp) {
connections[fromId]
.setRemoteDescription(new RTCSessionDescription(signal.sdp))
.then(() => {
console.log(`Remote description set successfully for ${fromId}`);
if (signal.sdp.type === "offer") {
connections[fromId]
.createAnswer()
.then((description) => {
console.log(`Answer created successfully for ${fromId}`);
connections[fromId]
.setLocalDescription(description)
.then(() => {
console.log(`Local description set successfully for ${fromId}`);
socket.emit(
"signal",
fromId,
JSON.stringify({
sdp: connections[fromId].localDescription,
})
);
})
.catch((e) => console.error(`Error setting local description for ${fromId}:`, e));
})
.catch((e) => console.error(`Error creating answer for ${fromId}:`, e));
}
if (this.iceCandidatesQueue[fromId]) {
this.iceCandidatesQueue[fromId].forEach((candidate) => {
connections[fromId]
.addIceCandidate(new RTCIceCandidate(candidate))
.catch((e) => console.error(`Error adding ice candidate for ${fromId}:`, e));
});
delete this.iceCandidatesQueue[fromId];
}
})
.catch((e) => console.error(`Error setting remote description for ${fromId}:`, e));
}
if (signal.ice) {
let iceCandidate = new RTCIceCandidate(signal.ice);
if (connections[fromId].remoteDescription) {
connections[fromId]
.addIceCandidate(iceCandidate)
.catch((e) => console.error(`Error adding ice candidate for ${fromId}:`, e));
} else {
if (!this.iceCandidatesQueue[fromId]) {
this.iceCandidatesQueue[fromId] = [];
}
this.iceCandidatesQueue[fromId].push(iceCandidate);
}
}
}
};
serverConnection = () => {
socket = io.connect(server_url, { secure: true })
// demande de parole
socket.on("speech-requested", ({ username }) => {
message.warning({
content: `${username} souhaite prendre la parole.`,
className: "custom-message",
duration: 3,
})
})
socket.on("signal", this.signalFromServer)
socket.on("connect", () => {
socket.on("redirectToMainPage", () => {
this.stopTracks()
});
socket.emit(
"joinCall",
window.location.href,
this.state.username,
this.state.currentUserEmail
)
socketId = socket.id
this.setState({ socketId });
socket.on("update-user-list", (users) => {
if (users) {
// si j'fais pas ça il va mdire undefined blablabla
let updatedUsernames = {}
users.forEach((user) => {
updatedUsernames[user.id] = user.username
})
this.setState({ usernames: updatedUsernames })
} else {
console.log(
"Pas encore de user ici.."
)
}
})
socket.on("chat-message", this.addMessage) // je recupere les messages emit coté serveur pour les display
socket.on("userLeft", (id) => {
let video = document.querySelector(`[data-socket="${id}"]`)
let username = this.state.usernames[id]
// J'update l'array usernames quand un user quitte la room "...this.state.usernames"
//car je creer une sorte de copie pour effectuer mon delete ensuite jenvoie cette copie à ma vraie state
const updatedUsernames = { ...this.state.usernames };
delete updatedUsernames[id]; // du coup je supprime l'utilisateur en supprimant son index id
this.setState({ usernames: updatedUsernames });
if (id !== socketId ) {
this.playUserDisconnectedSound()
message.info({
content: `${username} a quitté la conférence.`,
className: "custom-message",
duration: 3,
})
}
if (video !== null) {
videoElements--
video.parentNode.removeChild(video)
let main = document.getElementById("main")
this.adaptCSS(main)
}
})
socket.on("user-joined", (id, clients, username, email) => {
console.log(`Utilisateur rejoint: ${username}, ID: ${id}`);
// seul l'user qui s'est fait kick va listen " user-kicked" car c'est emit depuis server spécifiquement à la personne kicked et le reste jte fais pas un dessin
socket.on("user-kicked", () => {
window.location.href = "/";
socket.disconnect();
});
if (id !== socketId) {
this.getSources(); // ce salaud j'ai dû le metre ici aussi car en mode prod il faut le relancer quand un autre peer se connecte
message.success({
content: `${username} a rejoint la conférence.`,
className: "custom-message",
duration: 3,
})
// si l'id qui vient d'arriver ne correspond pas à mon socketId (moi) alors je play le sound de cette maniere,
//seul les utilisateurs déjà présents dans la room entendront le son si un new user arrive dans la room
}
this.setState((prevState) => ({
usernames: {
...prevState.usernames,
[id]: username,
},
}))
this.setState((prevState) => ({
connectedEmails: [...prevState.connectedEmails, email],
}))
console.log("Current state of connections:", connections);
clients.forEach((socketListId) => {
connections[socketListId] = this.createPeerConnection(socketListId);
// c'est ici que j'initialise la connection P2P avec webRTC
console.log("Current state of connections:", connections);
// je collecte mes iceCandidates
connections[socketListId].onicecandidate = function (event) {
if (event.candidate != null) {
socket.emit(
"signal", // je spread mes icecandidate via "signal"
socketListId,
JSON.stringify({ ice: event.candidate })
)
}
}
// je check si un nouveau user (nouveau videoElement du coup) arrive dans la room
connections[socketListId].onaddstream = (event) => {
// c un event de webRTC go voir : https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addstream_event
let searchVideo = document.querySelector(
`[data-socket="${socketListId}"]`
)
if (!searchVideo) {
console.log(`Creating new video element for socketListId: ${socketListId}`);
videoElements = clients.length // videoElements = nbr de client connectés à la room..
console.log("videoElements: ", videoElements) // test adaptCSS
let main = document.getElementById("main")
let cssMesure = this.adaptCSS(main)
let video = document.createElement("video")
let css = {
minWidth: cssMesure.minWidth,
minHeight: cssMesure.minHeight,
maxHeight: "100%",
margin: "10px",
borderStyle: "solid",
borderColor: "#bdbdbd",
objectFit: "fill",
backgroundImage: `url(${backgroundBlck})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat'
};
for (let i in css) video.style[i] = css[i]
video.style.setProperty("width", cssMesure.width)
video.style.setProperty("height", cssMesure.height)
video.setAttribute("data-socket", socketListId)
video.style.borderRadius = "25px"
video.srcObject = event.stream
video.autoplay = true
video.playsinline = true
video.onclick = this.handleVideoClick
const videoId = "video_" + socketListId;
video.setAttribute("id", videoId);
// video.classList.add("video-with-username");
// video.setAttribute("data-username", username);
video.srcObject = event.stream;
main.appendChild(video)
this.adaptCSS(main);
}else{
console.log(`Updating existing video element for socketListId: ${socketListId}`);
searchVideo.srcObject = event.stream;
}
}
if (window.localStream instanceof MediaStream) {
// Ajout du stream à RTCPeerConnection..
console.log(`Adding stream to connection for user ${socketListId}`);
console.log("Current state of connections:", connections);
connections[socketListId].addStream(window.localStream);
} else {
message.error('Votre caméra n\'est pas disponible !!');
}
})
if (id === socketId) {
console.log("Local user has joined. Initiating offer creation logic.");
}
// Ici, je vais gérer le scénario au cas où un user se co à une salle avec des utilisateurs déjà présents.
//Cet utilisateur va envoyer son offre à tous les utilisateurs déjà présents dans la salle.
if (id === socketId) { // dans cette condition je veux être sûr que celui qui va envoyer son offer à tout lmonde est l'user qui vient de se connecter (genre moi localement quoi)
for (let otherUserId in connections) { // je loop à travers les autres utilisateurs dans la room
if (otherUserId === socketId) continue
let connection = connections[otherUserId];
console.log('ajout du stream à la connection pour user : ' + otherUserId);
try {
connection.addStream(window.localStream);
} catch (e) {
console.error('Erreur ajout stream à la co :', e);
continue; // Skip l'itérration pour passer à la suivante si erreur
}
console.log('Stream bien ajouté. Creation de l offre pour user : ' + otherUserId);
connection
.createOffer()
.then((description) => {
console.log('Offre creee avec succes!. Je set la local description pour user : ' + otherUserId);
connection.setLocalDescription(description);
})
.then(() => {
console.log('Local description -> OK . envoi du signal pour user ' + otherUserId);
socket.emit(
"signal",
otherUserId,
JSON.stringify({ sdp: connections[id].localDescription })
);
})
.catch((e) => console.error('Erreur durant création offer ou localdescription ): ', e));
}
}
})
})
}
createPeerConnection = () => {
let peerConnection = new RTCPeerConnection(peerConnectionConfig);
return peerConnection;
}
我尝试为每个进入房间的新同伴添加一个新连接!它没有改变任何东西我仍然遇到同样的错误..
有 2 种一般方法可以解决您的问题: