【解決方法】Node.js、websocket、kurento メディア サーバーを使用して webrtc ビデオ通話アプリケーションでルームに参加すると、2 人目のユーザーのリモート ストリームが表示されない

プログラミングQA


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

コメント

タイトルとURLをコピーしました