Click here to Skip to main content
15,942,964 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
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");

// 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);
	}
}



script.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 = "";
	}
}





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"
	}
}
Posted

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900