[ad_1]
0 I am a beginner in WebRTC and currently working on developing a video call application with features like recording, screen sharing, and one-to-many communication. To implement this, I am using Node.js, WebSocket, and Kurento Media Server. I have encountered an issue where I am unable to see the remote stream of the second user when they join the call/room. There are no errors logged in either the browser console or the Node.js console. My Kurento Media Server is running on a remote machine using the Docker image "kurento/kurento-media-server:7.0.0". I have gone through multiple tutorials and blogs, but I haven't been able to find a solution. I'm providing the relevant code below. I would greatly appreciate any guidance on what I might have missed. Thank you in advance for your assistance.
私が試したこと:
私のサーバー.js
const express = require("express"); const app = express(); const path = require("path"); const ws = require("ws"); const minimist = require("minimist"); const url = require("url"); const kurento = require("kurento-client"); const fs = require("fs"); const http = require("http"); // Set the view engine to EJS app.set("view engine", "ejs"); app.use(express.static("Public")); app.use( express.static(path.join(__dirname, "public"), { "Content-Type": "application/javascript", }) ); app.use(express.static(path.join(__dirname, "Public/js"))); // Set the views directory app.set("views", path.join(__dirname, "views")); app.get("/", (req, res) => { const message = "Hello, EJS!"; res.render("index", { message }); }); var argv = minimist(process.argv.slice(2), { default: { as_uri: "http://localhost:3000/", ws_uri: "ws://10.68.338.282:8888/kurento", }, }); /* * Definition of global variables. */ var kurentoClient = null; var userRegistry = new UserRegistry(); var pipelines = {}; var candidatesQueue = {}; var idCounter = 0; function nextUniqueId() { idCounter++; return idCounter.toString(); } /* * Definition of helper classes */ // Represents caller and callee sessions function UserSession(id, name, ws) { this.id = id; this.name = name; this.ws = ws; this.peer = null; this.sdpOffer = null; } UserSession.prototype.sendMessage = function (message) { this.ws.send(JSON.stringify(message)); }; // Represents registrar of users function UserRegistry() { this.usersById = {}; this.usersByName = {}; } UserRegistry.prototype.register = function (user) { this.usersById[user.id] = user; this.usersByName[user.name] = user; }; UserRegistry.prototype.unregister = function (id) { var user = this.getById(id); if (user) delete this.usersById[id]; if (user && this.getByName(user.name)) delete this.usersByName[user.name]; }; UserRegistry.prototype.getById = function (id) { return this.usersById[id]; }; UserRegistry.prototype.getByName = function (name) { return this.usersByName[name]; }; UserRegistry.prototype.removeById = function (id) { var userSession = this.usersById[id]; if (!userSession) return; delete this.usersById[id]; delete this.usersByName[userSession.name]; }; // Represents a B2B active call function CallMediaPipeline() { this.pipeline = null; this.webRtcEndpoint = {}; } // Recover kurentoClient for the first time. function getKurentoClient(callback) { if (kurentoClient !== null) { console.log("Kurento connected successfully."); return callback(null, kurentoClient); } kurento(argv.ws_uri, function (error, _kurentoClient) { if (error) { var message = "Could not find media server at address " + argv.ws_uri; console.error(message + ". Exiting with error " + error); return callback(message + ". Exiting with error " + error); } kurentoClient = _kurentoClient; console.log("Kurento connected successfully."); callback(null, kurentoClient); }); } CallMediaPipeline.prototype.createPipeline = function ( callerId, calleeId, ws, callback ) { var self = this; getKurentoClient(function (error, kurentoClient) { if (error) { return callback(error); } kurentoClient.create("MediaPipeline", function (error, pipeline) { if (error) { console.error(error); return callback(error); } pipeline.create("WebRtcEndpoint", function (error, callerWebRtcEndpoint) { if (error) { pipeline.release(); console.error(error); return callback(error); } if (candidatesQueue[callerId]) { while (candidatesQueue[callerId].length) { var candidate = candidatesQueue[callerId].shift(); callerWebRtcEndpoint.addIceCandidate(candidate); } } callerWebRtcEndpoint.on("IceCandidateFound", function (event) { var candidate = kurento.getComplexType("IceCandidate")( event.candidate ); userRegistry.getById(callerId).ws.send( JSON.stringify({ id: "iceCandidate", candidate: candidate, }) ); }); pipeline.create( "WebRtcEndpoint", function (error, calleeWebRtcEndpoint) { if (error) { pipeline.release(); console.error(error); return callback(error); } if (candidatesQueue[calleeId]) { while (candidatesQueue[calleeId].length) { var candidate = candidatesQueue[calleeId].shift(); calleeWebRtcEndpoint.addIceCandidate(candidate); } } calleeWebRtcEndpoint.on("IceCandidateFound", function (event) { var candidate = kurento.getComplexType("IceCandidate")( event.candidate ); userRegistry.getById(calleeId).ws.send( JSON.stringify({ id: "iceCandidate", candidate: candidate, }) ); }); callerWebRtcEndpoint.connect( calleeWebRtcEndpoint, function (error) { if (error) { pipeline.release(); console.error(error); return callback(error); } calleeWebRtcEndpoint.connect( callerWebRtcEndpoint, function (error) { if (error) { pipeline.release(); console.error(error); return callback(error); } } ); self.pipeline = pipeline; self.webRtcEndpoint[callerId] = callerWebRtcEndpoint; self.webRtcEndpoint[calleeId] = calleeWebRtcEndpoint; console.log("Pipeline created successfully."); callback(null); } ); } ); }); }); }); }; CallMediaPipeline.prototype.generateSdpAnswer = function ( id, sdpOffer, callback ) { const webRtcEndpoint = this.webRtcEndpoint[id]; if (!webRtcEndpoint) { const errorMessage = `WebRtcEndpoint not found for id: ${id}`; console.error(errorMessage); return callback(errorMessage); } webRtcEndpoint.processOffer(sdpOffer, function (error, sdpAnswer) { if (error) { console.error("Error generating SDP answer:", error); return callback(error); } webRtcEndpoint.gatherCandidates(function (error) { if (error) { console.error("Error gathering candidates:", error); return callback(error); } console.log("SDP answer generated successfully."); callback(null, sdpAnswer); }); }); }; CallMediaPipeline.prototype.release = function () { if (this.pipeline) this.pipeline.release(); this.pipeline = null; }; /* * Server startup */ var asUrl = url.parse(argv.as_uri); var port = asUrl.port; var server = http.createServer(app).listen(port, function () { console.log("Kurento Tutorial started"); console.log("Open " + url.format(asUrl) + " with a WebRTC capable browser"); }); var wss = new ws.Server({ server: server, path: "/one2one", }); wss.on("connection", function (ws) { var sessionId = nextUniqueId(); console.log("Connection received with sessionId " + sessionId); ws.on("error", function (error) { console.log("Connection " + sessionId + " error"); stop(sessionId); }); ws.on("close", function () { console.log("Connection " + sessionId + " closed"); stop(sessionId); userRegistry.unregister(sessionId); }); ws.on("message", function (_message) { var message = JSON.parse(_message); console.log("Connection " + sessionId + " received message ", message); switch (message.id) { case "register": register(sessionId, message.name, ws); break; case "call": call(sessionId, message.to, message.from, message.sdpOffer); break; case "incomingCallResponse": incomingCallResponse( sessionId, message.from, message.callResponse, message.sdpOffer, ws ); break; case "stop": stop(sessionId); break; case "onIceCandidate": onIceCandidate(sessionId, message.candidate); break; default: ws.send( JSON.stringify({ id: "error", message: "Invalid message " + message, }) ); break; } }); }); function stop(sessionId) { if (!pipelines[sessionId]) { return; } var pipeline = pipelines[sessionId]; delete pipelines[sessionId]; pipeline.release(); var stopperUser = userRegistry.getById(sessionId); var stoppedUser = userRegistry.getByName(stopperUser.peer); stopperUser.peer = null; if (stoppedUser) { stoppedUser.peer = null; delete pipelines[stoppedUser.id]; var message = { id: "stopCommunication", message: "remote user hanged out", }; stoppedUser.sendMessage(message); } clearCandidatesQueue(sessionId); } function incomingCallResponse(calleeId, from, callResponse, calleeSdp, ws) { clearCandidatesQueue(calleeId); function onError(callerReason, calleeReason) { if (pipeline) pipeline.release(); if (caller) { var callerMessage = { id: "callResponse", response: "rejected", }; if (callerReason) callerMessage.message = callerReason; caller.sendMessage(callerMessage); } var calleeMessage = { id: "stopCommunication", }; if (calleeReason) calleeMessage.message = calleeReason; callee.sendMessage(calleeMessage); } var callee = userRegistry.getById(calleeId); if (!from || !userRegistry.getByName(from)) { return onError(null, "unknown from = " + from); } var caller = userRegistry.getByName(from); if (callResponse === "accept") { var pipeline = new CallMediaPipeline(); pipelines[caller.id] = pipeline; pipelines[callee.id] = pipeline; pipeline.createPipeline(caller.id, callee.id, ws, function (error) { if (error) { return onError(error, error); } pipeline.generateSdpAnswer( caller.id, caller.sdpOffer, function (error, callerSdpAnswer) { if (error) { return onError(error, error); } pipeline.generateSdpAnswer( callee.id, calleeSdp, function (error, calleeSdpAnswer) { if (error) { return onError(error, error); } var message = { id: "startCommunication", sdpAnswer: calleeSdpAnswer, }; callee.sendMessage(message); message = { id: "callResponse", response: "accepted", sdpAnswer: callerSdpAnswer, }; caller.sendMessage(message); } ); } ); }); } else { var decline = { id: "callResponse", response: "rejected", message: "user declined", }; caller.sendMessage(decline); } } function call(callerId, to, from, sdpOffer) { clearCandidatesQueue(callerId); var caller = userRegistry.getById(callerId); var rejectCause = "User " + to + " is not registered"; if (userRegistry.getByName(to)) { var callee = userRegistry.getByName(to); caller.sdpOffer = sdpOffer; callee.peer = from; caller.peer = to; var message = { id: "incomingCall", from: from, }; try { return callee.sendMessage(message); } catch (exception) { rejectCause = "Error " + exception; } } var message = { id: "callResponse", response: "rejected: ", message: rejectCause, }; caller.sendMessage(message); } function register(id, name, ws, callback) { function onError(error) { ws.send( JSON.stringify({ id: "registerResponse", response: "rejected ", message: error, }) ); } if (!name) { return onError("empty user name"); } if (userRegistry.getByName(name)) { return onError("User " + name + " is already registered"); } userRegistry.register(new UserSession(id, name, ws)); try { ws.send(JSON.stringify({ id: "registerResponse", response: "accepted" })); } catch (exception) { onError(exception); } } function clearCandidatesQueue(sessionId) { if (candidatesQueue[sessionId]) { delete candidatesQueue[sessionId]; } } function onIceCandidate(sessionId, _candidate) { var candidate = kurento.getComplexType("IceCandidate")(_candidate); var user = userRegistry.getById(sessionId); if ( pipelines[user.id] && pipelines[user.id].webRtcEndpoint && pipelines[user.id].webRtcEndpoint[user.id] ) { var webRtcEndpoint = pipelines[user.id].webRtcEndpoint[user.id]; webRtcEndpoint.addIceCandidate(candidate); } else { if (!candidatesQueue[user.id]) { candidatesQueue[user.id] = []; } candidatesQueue[sessionId].push(candidate); } }
スクリプト.js
// Define WebSocket URL const wsUrl = "ws://localhost:3000/one2one"; // DOM elements const videoInput = document.getElementById("videoInput"); const videoOutput = document.getElementById("videoOutput"); const registerButton = document.getElementById("register"); const callButton = document.getElementById("call"); const terminateButton = document.getElementById("terminate"); // States const RegisterState = { NOT_REGISTERED: 0, REGISTERING: 1, REGISTERED: 2, }; let registerState = RegisterState.NOT_REGISTERED; const CallState = { NO_CALL: 0, PROCESSING_CALL: 1, IN_CALL: 2, }; let callState = CallState.NO_CALL; // WebRTC variables let webRtcPeer = null; // Register event listener on window load window.onload = function () { registerButton.addEventListener("click", register); callButton.addEventListener("click", call); terminateButton.addEventListener("click", stop); setRegisterState(RegisterState.NOT_REGISTERED); document.getElementById("name").focus(); }; // Register WebSocket message handler function handleWebSocketMessage(message) { const parsedMessage = JSON.parse(message.data); console.log("Received message: " + message.data); console.log("events message: " + parsedMessage.id); switch (parsedMessage.id) { case "registerResponse": handleRegisterResponse(parsedMessage); break; case "callResponse": handleCallResponse(parsedMessage); break; case "incomingCall": handleIncomingCall(parsedMessage); break; case "startCommunication": handleStartCommunication(parsedMessage); break; case "stopCommunication": console.log("Communication ended by remote peer"); stop(true); break; case "iceCandidate": webRtcPeer.addIceCandidate(parsedMessage.candidate); break; default: console.error("Unrecognized message", parsedMessage); } } // WebSocket initialization const ws = new WebSocket(wsUrl); ws.onmessage = handleWebSocketMessage; window.onbeforeunload = function () { ws.close(); }; // Register functions function setRegisterState(nextState) { registerState = nextState; switch (nextState) { case RegisterState.NOT_REGISTERED: registerButton.disabled = false; callButton.disabled = true; terminateButton.disabled = true; break; case RegisterState.REGISTERING: registerButton.disabled = true; break; case RegisterState.REGISTERED: registerButton.disabled = true; setCallState(CallState.NO_CALL); break; default: return; } } function handleRegisterResponse(message) { if (message.response === "accepted") { setRegisterState(RegisterState.REGISTERED); } else { setRegisterState(RegisterState.NOT_REGISTERED); const errorMessage = message.message ? message.message : "Unknown reason for register rejection."; console.log(errorMessage); alert("Error registering user. See console for further information."); } } function register() { const name = document.getElementById("name").value; if (name === "") { window.alert("You must insert your user name"); return; } setRegisterState(RegisterState.REGISTERING); const message = { id: "register", name: name, }; sendMessage(message); document.getElementById("peer").focus(); } // Call functions function setCallState(nextState) { callState = nextState; switch (nextState) { case CallState.NO_CALL: callButton.disabled = false; terminateButton.disabled = true; break; case CallState.PROCESSING_CALL: callButton.disabled = true; terminateButton.disabled = true; break; case CallState.IN_CALL: callButton.disabled = true; terminateButton.disabled = false; break; default: return; } } function handleCallResponse(message) { if (message.response !== "accepted") { console.log("Call not accepted by peer. Closing call"); const errorMessage = message.message ? message.message : "Unknown reason for call rejection."; console.log(errorMessage); stop(true); } else { setCallState(CallState.IN_CALL); webRtcPeer.processAnswer(message.sdpAnswer); } } function call() { const peerName = document.getElementById("peer").value; if (peerName === "") { window.alert("You must specify the peer name"); return; } setCallState(CallState.PROCESSING_CALL); showSpinner(videoInput, videoOutput); // const options = { // localVideo: videoInput, // remoteVideo: videoOutput, // onicecandidate: onIceCandidate, // }; const options = { localVideo: videoInput, remoteVideo: videoOutput, onicecandidate: onIceCandidate, }; webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv( options, function (error) { if (error) { console.error(error); setCallState(CallState.NO_CALL); } this.generateOffer(function (error, offerSdp) { if (error) { console.error(error); setCallState(CallState.NO_CALL); } const message = { id: "call", from: document.getElementById("name").value, to: peerName, sdpOffer: offerSdp, }; sendMessage(message); }); } ); } // Other utility functions function sendMessage(message) { const jsonMessage = JSON.stringify(message); console.log("Sending message: " + jsonMessage); ws.send(jsonMessage); } function stop(message) { setCallState(CallState.NO_CALL); if (webRtcPeer) { webRtcPeer.dispose(); webRtcPeer = null; if (!message) { sendMessage({ id: "stop" }); } } hideSpinner(videoInput, videoOutput); } function onIceCandidate(candidate) { console.log("Local candidate" + JSON.stringify(candidate)); const message = { id: "onIceCandidate", candidate: candidate, }; sendMessage(message); } function handleIncomingCall(message) { if (callState !== CallState.NO_CALL) { const response = { id: "incomingCallResponse", from: message.from, callResponse: "reject", message: "busy", }; return sendMessage(response); } setCallState(CallState.PROCESSING_CALL); if ( confirm("User " + message.from + " is calling you. Do you accept the call?") ) { showSpinner(videoInput, videoOutput); const options = { localVideo: videoInput, remoteVideo: videoOutput, onicecandidate: onIceCandidate, }; console.log("OPTIONS------------", options); webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv( options, function (error) { if (error) { console.error(error); setCallState(CallState.NO_CALL); } this.generateOffer(function (error, offerSdp) { if (error) { console.error(error); setCallState(CallState.NO_CALL); } const response = { id: "incomingCallResponse", from: message.from, callResponse: "accept", sdpOffer: offerSdp, }; sendMessage(response); }); } ); } else { const response = { id: "incomingCallResponse", from: message.from, callResponse: "reject", message: "user declined", }; sendMessage(response); stop(true); } } function handleStartCommunication(message) { setCallState(CallState.IN_CALL); webRtcPeer.processAnswer(message.sdpAnswer); } function showSpinner() { for (var i = 0; i < arguments.length; i++) { arguments[i].poster = "./img/transparent-1px.png"; arguments[i].style.background = 'center transparent url("./img/spinner.gif") no-repeat'; } } function hideSpinner() { for (var i = 0; i < arguments.length; i++) { arguments[i].src = ""; arguments[i].poster = "./img/webrtc.png"; arguments[i].style.background = ""; } }
私のejsファイル
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>One2One-Kurento</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" /> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap-theme.min.css" /> <link rel="stylesheet" href="kurento.css" /> <script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/draggabilly/dist/draggabilly.pkgd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/ekko-lightbox/dist/ekko-lightbox.min.js"></script> <script src="kurento-utils.js"></script> <script src="jquery.min.js"></script> <script src="script.js" defer></script> </head> <body> <header> <div class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse" ></button> <a class="navbar-brand" href=".">Kurento Tutorial</a> </div> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1" > <ul class="nav navbar-nav navbar-right"> <li> <a href="https://github.com/Kurento/kurento/tutorials/javascript-node/tree/main/one2one-call" > Source Code </a> </li> </ul> </div> </div> </div> </header> <div class="container"> <div class="page-header"> <h1>Tutorial 4: Video Call 1 to 1 with WebRTC</h1> <p> This web application consists of a one-to-one video call using <a href="http://www.webrtc.org/">WebRTC</a>. In other words, this application is similar to a phone but with video. The <a href="img/pipeline.png" data-toggle="lightbox" data-title="Video Call 1 to 1 Media Pipeline" data-footer="Two interconnected WebRtcEnpoints Media Elements" >Media Pipeline</a > is composed of two interconnected WebRtcEndpoints. To run this demo, follow these steps: </p> <ol> <li> Open this page with a WebRTC-compliant browser (Chrome, Firefox). </li> <li> Type a nickname in the Name field and click Register. </li> <li> In a different machine (or a different tab in the same browser), follow the same procedure to register another user. </li> <li> Type the name of the user to be called in the Peer field and click Call. </li> <li> Grant access to the camera and microphone for both users. After the SDP negotiation, the communication should start. </li> <li> The called user should accept the incoming call (through a confirmation dialog). </li> <li>Click Stop to finish the communication.</li> </ol> </div> <div class="row"> <div class="col-md-5"> <label class="control-label" for="name">Name</label> <div class="row"> <div class="col-md-6"> <input id="name" name="name" class="form-control" type="text" /> </div> <div class="col-md-6 text-right"> <a id="register" href="#" class="btn btn-primary"> Register </a> </div> </div> <br /> <br /> <label class="control-label" for="peer">Peer</label> <div class="row"> <div class="col-md-6"> <input id="peer" name="peer" class="form-control" type="text" /> </div> <div class="col-md-6 text-right"> <a id="call" href="#" class="btn btn-success"> Call </a> <a id="terminate" href="#" class="btn btn-danger"> Stop </a> </div> </div> <br /> </div> <div class="col-md-7"> <div id="videoBig"> <video id="videoOutput" autoplay width="640px" height="480px" poster="img/webrtc.png" ></video> </div> <div id="videoSmall"> <video id="videoInput" autoplay width="240px" height="180px" poster="img/webrtc.png" ></video> </div> </div> </div> </div> <footer> <div class="foot-fixed-bottom"> <div class="container text-center"> <hr /> <div class="row">© 2014-2015 Kurento</div> <div class="row"> <div class="col-md-4"> <a href="http://www.urjc.es"> <img src="img/urjc.gif" alt="Universidad Rey Juan Carlos" height="50px" /> </a> </div> <div class="col-md-4"> <a href="https://kurento.openvidu.io/"> <img src="img/kurento.png" alt="Kurento" height="50px" /> </a> </div> <div class="col-md-4"> <a href="http://www.naevatec.com"> <img src="img/naevatec.png" alt="Naevatec" height="50px" /> </a> </div> </div> </div> </div> </footer> </body> </html>
パッケージjson
{ "name": "kurentoejs", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "nodemon server.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "bootstrap": "^5.2.3", "demo-console": "^1.5.0", "draggabilly": "^3.0.0", "ejs": "^3.1.9", "ekko-lightbox": "^5.3.0", "express": "^4.18.2", "jquery": "^3.7.0", "kurento-client": "^7.0.0", "minimist": "^1.2.8", "nodemon": "^2.0.22", "socket.io": "^4.6.1", "webrtc-adapter": "^8.2.2", "ws": "^8.13.0" } }
[ad_2]
コメント