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.
What I have tried:
my server.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");
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")));
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",
},
});
var kurentoClient = null;
var userRegistry = new UserRegistry();
var pipelines = {};
var candidatesQueue = {};
var idCounter = 0;
function nextUniqueId() {
idCounter++;
return idCounter.toString();
}
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));
};
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];
};
function CallMediaPipeline() {
this.pipeline = null;
this.webRtcEndpoint = {};
}
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;
};
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);
}
}
script.js
const wsUrl = "ws://localhost:3000/one2one";
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");
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;
let webRtcPeer = null;
window.onload = function () {
registerButton.addEventListener("click", register);
callButton.addEventListener("click", call);
terminateButton.addEventListener("click", stop);
setRegisterState(RegisterState.NOT_REGISTERED);
document.getElementById("name").focus();
};
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);
}
}
const ws = new WebSocket(wsUrl);
ws.onmessage = handleWebSocketMessage;
window.onbeforeunload = function () {
ws.close();
};
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();
}
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,
};
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);
});
}
);
}
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 = "";
}
}
my ejs file
<!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>
pkg 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"
}
}