DOMException:无法设置远程应答 sdp:在错误状态下调用:稳定

问题描述 投票:0回答:1

当第三个用户进入视频会议时,我收到以下错误:“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; }
我尝试为每个进入房间的新同伴添加一个新连接!它没有改变任何东西我仍然遇到同样的错误..

javascript reactjs socket.io webrtc p2p
1个回答
0
投票
你是对的 - RTCPeerConnection 旨在处理一个点对点连接,不可能向其中添加第三个用户。

有 2 种一般方法可以解决您的问题:

    为房间中的每个用户对创建一个单独的 RTCPeerConnection,但请注意,每个连接分别对媒体进行编码,因此对于房间内的 2-3-4 个用户来说它可以很好地工作(取决于设备性能),但对于用户的设备处理更多传出连接。
  • 考虑在 SFU 模式下使用媒体服务器 - 在这种情况下,每个用户只有一个传出连接和多个传入连接。它将解决性能问题并允许您在房间内拥有多个用户。一般来说,大多数群组视频通话都是使用SFU进行的。我更喜欢 Janus Media Server,它的 VideoRoom 应该适合您的目标:
  • https://janus.conf.meetecho.com/demos/mvideoroom.html。一开始,他们的客户端库可能会有所帮助:https://janus.conf.meetecho.com/docs/resources.html
© www.soinside.com 2019 - 2024. All rights reserved.