first commit

This commit is contained in:
2023-08-04 21:47:41 +03:00
commit 8afb59c0ac
9 changed files with 1913 additions and 0 deletions

2
.env Normal file
View File

@@ -0,0 +1,2 @@
SERVER_PORT=3000
SERVER_ADDRESS=93.116.13.156

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
movies/

15
README.md Normal file
View File

@@ -0,0 +1,15 @@
# SyncApp
![image](https://github.com/lumijiez/SyncApp/assets/59575049/d153828e-6fc2-46f9-a057-e47b171188da)
Tiny app based on WebSockets, and implemented using Express.js, Node.js in pair with ws.
## To-Do List
The following tasks are on our to-do list:
1. Automatize syncing between clients
2. Free the clients map on client exit (causes memory leaks)
3. Implement hosts and automatize hosting
4. Workaround for autoplay
5. Implement room authorization for added security

237
index.js Normal file
View File

@@ -0,0 +1,237 @@
const express = require('express');
const expressWs = require('express-ws');
const bodyParser = require('body-parser');
const dotenv = require('dotenv').config();
const { v4: uuidv4 } = require('uuid');
// Express Initialization + WS piggyback
const app = express();
expressWs(app);
// Middlewares
app.use(express.static('public'));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.set('view engine', 'ejs');
app.set('views', __dirname + '/views');
// Server startup
const server = app.listen(process.env.SERVER_PORT, () => {
console.log(`Server on port ${process.env.SERVER_PORT}`);
});
// Client mapping
const clients = new Map();
const rooms = new Map();
const room_hosts = new Map();
const room_links = new Map();
// Websocket handlers
app.ws('/ws', (ws, req) => {
// Generates an unique ID
const id = uuidv4();
const clientData = { id: id, name: 'Guest', client: ws };
clients.set(id, clientData);
let clientRoom;
// Sets the ID for a new connection
const response = { command: 'set_id', id: id };
const responseJson = JSON.stringify(response);
ws.send(responseJson);
ws.on('message', (message) => {
const msg = JSON.parse(message);
// Null command handler
if (msg.command == null) {
const response = { error: 'Bad command.' };
const responseJson = JSON.stringify(response);
ws.send(responseJson);
}
if (msg.command == 'sync') {
syncClients(msg.room_id, msg.time);
}
// Sets the client into a room
if (msg.command == 'set_room') {
clientRoom = msg.room_id;
if (rooms.get(msg.room_id) != null) {
rooms.get(msg.room_id).push(clients.get(msg.id));
} else {
rooms.set(msg.room_id, [clients.get(msg.id)]);
}
if (room_hosts.get(msg.room_id) == null) {
room_hosts.set(msg.room_id, id);
}
refreshAllClients(rooms, clientRoom);
}
if (msg.command == 'set_link') {
room_links[clientRoom] = msg.link;
console.log(room_links);
console.log(room_links[clientRoom]);
refreshAllClients(rooms, clientRoom);
}
if (msg.command == 'pause') {
console.log('PAUSED');
broadcastCommand(msg.room_id, 'pause');
}
if (msg.command == 'play') {
console.log('PLAYED');
broadcastCommand(msg.room_id, 'play');
}
// Websocket command to switch hosts
if (msg.command == 'make_host') {
if (room_hosts.get(msg.room_id) != msg.id) {
room_hosts.set(msg.room_id, msg.id);
refreshAllClients(rooms, clientRoom);
}
}
if (msg.command == 'sync_with_host') {
if (clients.get(room_hosts.get(msg.room_id)) != null) {
const response = { command: 'sync_with_me' };
const responseJson = JSON.stringify(response);
clients.get(room_hosts.get(msg.room_id)).client.send(responseJson);
}
}
// Websocket command to change a name
if (msg.command == 'change_name') {
clients.get(id).name = msg.name;
refreshAllClients(rooms, clientRoom);
}
// Websocket command to send a global message
if (msg.command == 'global') {
broadcastRoom(msg.room_id, msg.message, msg.name);
}
// Websocket command to send a text to a specific client
if (msg.command == 'text_id') {
const id = msg.id;
const response = {
command: 'message',
message: msg.name + ': ' + msg.message,
};
const responseJson = JSON.stringify(response);
clients.get(id).client.send(responseJson);
}
});
// On close, deletes the client data and randomizes a new host, if it exists
ws.on('close', (data) => {
console.log('CLIENT LEFT');
clients.delete(id);
if (rooms.has(clientRoom)) {
const clients = rooms.get(clientRoom);
const updatedClients = clients.filter((client) => client.id !== id);
if (updatedClients.length == 0) {
rooms.delete(clientRoom);
randomizeHost(clientRoom);
return;
}
rooms.set(clientRoom, updatedClients);
}
randomizeHost(clientRoom);
refreshAllClients(rooms, clientRoom);
});
});
// Chooses a random host on call
function randomizeHost(room_id) {
if (rooms.has(room_id)) {
const newHost =
rooms.get(room_id)[Math.floor(Math.random() * rooms.get(room_id).length)];
room_hosts.set(room_id, newHost.id);
} else {
room_hosts.delete(room_id);
}
}
function refreshAllClients(rooms, room_id) {
const response = { command: 'refresh' };
const responseJson = JSON.stringify(response);
for (const client of rooms.get(room_id)) {
client.client.send(responseJson);
}
}
function syncClients(room_id, time) {
const response = { command: 'sync', time: time };
const responseJson = JSON.stringify(response);
for (const client of rooms.get(room_id)) {
client.client.send(responseJson);
}
}
function broadcastCommand(room_id, command) {
const response = { command: command };
const responseJson = JSON.stringify(response);
for (const client of rooms.get(room_id)) {
client.client.send(responseJson);
}
}
function broadcastLink(room_id, link) {
const response = { command: 'set_link', link: link };
const responseJson = JSON.stringify(response);
for (const client of rooms.get(room_id)) {
client.client.send(responseJson);
}
}
function broadcastRoom(room_id, message, name) {
const response = { command: 'global', message: name + ': ' + message };
const responseJson = JSON.stringify(response);
for (const client of rooms.get(room_id)) {
client.client.send(responseJson);
}
}
// Collects information about all clients
function getClientsJson(room_id) {
const clts = [];
for (const client of rooms.get(room_id)) {
const js = { id: client.id, name: client.name };
clts.push(js);
}
return JSON.stringify(clts);
}
// Collects and sends back updated data
app.post('/api/getRefreshData', (req, res) => {
const data = req.body;
const client = clients.get(data.id);
const isHost = client.id == room_hosts.get(data.room_id) ? true : false;
const toSend = {
host: isHost,
name: client.name,
client_data: getClientsJson(data.room_id),
link: room_links[data.room_id],
};
console.log(toSend);
res.json(toSend).status(200);
});
app.get('/', (req, res) => {
res.sendFile(__dirname + '/public/join.html');
});
app.get('/img', (req, res) => {
res.sendFile(__dirname + '/public/img.png');
});
app.get('/api/file/:filename', (req, res) => {
res.sendFile(__dirname + '/movies/' + req.params.filename);
});
app.get('/a', (req, res) => {
res.sendFile(__dirname + '/public/clt.html');
});

1268
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "nodejs-login-system",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.20.2",
"dotenv": "^16.3.1",
"ejs": "^3.1.9",
"express": "^4.18.2",
"express-ws": "^5.0.2",
"jsonwebtoken": "^9.0.0",
"nodemon": "^2.0.22",
"uuid": "^9.0.0",
"ws": "^8.13.0"
}
}

166
public/client.html Normal file
View File

@@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Room ID Form</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65"
crossorigin="anonymous"
/>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
#form-container {
text-align: center;
border: 1px solid #ccc;
padding: 20px;
border-radius: 5px;
background-color: #f7f7f7;
}
</style>
</head>
<body>
<div id="form-container">
<form id="roomForm">
<h2>Room ID Form</h2>
<label for="roomId">Room ID:</label>
<input type="text" id="roomId" required />
<br /><br />
<button type="submit" id="submitBtn">Submit</button>
</form>
<div id="mainPage" style="display: none">
<div class="container text-center">
<div class="row">
<div class="col">
<img height="80" src="http://localhost:3000/img" />
</div>
</div>
<div class="row">
<div class="col">
<video
id="video_field"
height="300"
src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
controls
></video>
</div>
<div class="col">
<h4>Host? <span id="host_field"></span></h4>
<h4 id="room_field">Room ID:</h4>
<h4 id="name_field">Your name:</h4>
<h4>Other connected clients:</h4>
<p id="clients_field"></p>
</div>
<div class="col-5">
<p
id="global_field"
style="height: 200px; overflow-x: hidden; overflow-y: scroll"
></p>
<div style="display: flex; justify-content: center">
<input
style="width: 200px; margin-right: 10px"
type="text"
id="global_input"
class="form-control"
/>
<button
onclick="ClientInstance.sendGlobal()"
class="btn btn-primary small-button"
>
Send Global
</button>
</div>
</div>
<div class="col">
<button
onclick="ClientInstance.makeHost()"
class="btn btn-primary small-button"
>
Make Host
</button>
<div
style="display: flex; justify-content: center; padding: 10px"
>
<input
placeholder="Change name:"
style="width: 200px; margin-right: 10px"
type="text"
id="name_input"
class="form-control"
/>
<button
onclick="ClientInstance.changeName()"
class="btn btn-primary small-button"
>
Change Name
</button>
</div>
<div style="display: flex; justify-content: center">
<input
placeholder="Put link:"
style="width: 200px; margin-right: 10px"
type="text"
id="link_input"
class="form-control"
/>
<button
style="margin-right: 10px"
onclick="ClientInstance.setLink()"
class="btn btn-primary small-button"
>
Set Link
</button>
<button
onclick="ClientInstance.syncVideo()"
class="btn btn-primary small-button"
>
Sync
</button>
</div>
</div>
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js"
integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
crossorigin="anonymous"
></script>
<script src="https://unpkg.com/axios@1.1.2/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</div>
</div>
<script src="/client.js"></script>
<script>
let ClientInstance;
document.addEventListener('DOMContentLoaded', function () {
const roomForm = document.getElementById('roomForm');
const mainPage = document.getElementById('mainPage');
let hasSubmitted = false;
roomForm.addEventListener('submit', function (event) {
event.preventDefault();
const ID = document.getElementById('roomId').value;
mainPage.style.display = 'block';
roomForm.style.display = 'none';
hasSubmitted = true;
ClientInstance = new Client(ID);
ClientInstance.connect();
});
});
</script>
</body>
</html>

200
public/client.js Normal file
View File

@@ -0,0 +1,200 @@
class Client {
#ClientSocket;
#ClientData;
constructor(room_id) {
this.#ClientData = { name: 'Guest', id: 0, room: room_id };
this.#ClientSocket = new WebSocket('ws://93.116.13.156:3000/ws');
this.host_field = document.getElementById('host_field');
this.name_field = document.getElementById('name_field');
this.clients_field = document.getElementById('clients_field');
this.name_input = document.getElementById('name_input');
this.global_field = document.getElementById('global_field');
this.global_input = document.getElementById('global_input');
this.room_field = document.getElementById('room_field');
this.room_field.textContent += room_id;
this.video_field = document.getElementById('video_field');
this.link_input = document.getElementById('link_input');
}
connect() {
let isManuallySeeked = true;
let isManuallyPlayed = true;
let isManuallyPaused = true;
this.video_field.addEventListener('play', () => {
if (isManuallyPlayed) {
const js = {
command: 'play',
room_id: this.#ClientData.room,
};
this.sendCommand(js);
} else isManuallyPlayed = true;
});
video_field.addEventListener('pause', () => {
if (isManuallyPaused) {
const js = {
command: 'pause',
room_id: this.#ClientData.room,
};
this.sendCommand(js);
} else isManuallyPaused = true;
});
video_field.addEventListener('seeked', () => {
if (isManuallySeeked) this.syncForVid();
else isManuallySeeked = true;
});
this.#ClientSocket.addEventListener('open', () => {
console.log('Connected');
});
this.#ClientSocket.addEventListener('message', (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.command == 'set_id') {
this.#ClientData.id = msg.id;
this.refreshData();
const json = {
command: 'set_room',
room_id: this.#ClientData.room,
id: this.#ClientData.id,
};
this.sendCommand(json);
}
if (msg.command == 'set_link') {
this.video_field.src = msg.link;
this.video_field.play();
}
if (msg.command == 'sync') {
this.isManuallySeeked = false;
this.video_field.currentTime = msg.time;
// refresh_data(CLIENT.id, CLIENT.room);
}
if (msg.command == 'refresh') {
this.refreshData();
}
if (msg.command == 'play') {
this.isManuallyPlayed = false;
this.video_field.play();
}
if (msg.command == 'pause') {
this.isManuallyPaused = false;
this.video_field.pause();
}
if (msg.command == 'message') {
this.message_field.innerHTML += msg.message + '<br>';
}
if (msg.command == 'global') {
this.global_field.innerHTML += msg.message + '<br>';
}
if (msg.command == 'sync_with_me') {
this.syncVideo();
}
} catch (error) {
console.error('Error parsing JSON:', error);
}
});
}
getSocket() {
return this.#ClientSocket;
}
getClientData() {
return this.#ClientData;
}
sendCommand(json) {
this.#ClientSocket.send(JSON.stringify(json));
}
syncForVid() {
const js = {
command: 'sync_with_host',
room_id: this.#ClientData.room,
id: this.#ClientData.id,
};
this.sendCommand(js);
}
changeName() {
const js = {
command: 'change_name',
id: this.#ClientData.id,
room_id: this.#ClientData.room,
name: name_input.value,
};
this.sendCommand(js);
}
sendGlobal() {
const js = {
command: 'global',
message: global_input.value,
room_id: this.#ClientData.room,
name: this.#ClientData.name,
};
this.sendCommand(js);
}
makeHost() {
const js = {
command: 'make_host',
id: this.#ClientData.id,
room_id: this.#ClientData.room,
};
this.sendCommand(js);
}
setLink() {
const js = {
command: 'set_link',
link: link_input.value,
room_id: this.#ClientData.room,
};
this.sendCommand(js);
}
syncVideo() {
const js = {
command: 'sync',
time: video_field.currentTime,
room_id: this.#ClientData.room,
};
this.sendCommand(js);
}
refreshData() {
axios
.post('/api/getRefreshData', {
id: this.#ClientData.id,
room_id: this.#ClientData.room,
})
.then((response) => {
const data = response.data;
this.host_field.textContent = data.host;
this.#ClientData.name = data.name;
this.name_field.textContent = `Your name: ${data.name}`;
this.clients_field.innerHTML = '';
let clients = JSON.parse(data.client_data);
for (const client of clients)
this.clients_field.innerHTML += `<h5>${client.name}</h5>`;
if (data.link != undefined) this.video_field.src = data.link;
this.video_field.play();
this.syncForVid();
});
}
}

BIN
public/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB