Add Dockerfile
This commit is contained in:
7
Dockerfile
Normal file
7
Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM node:20
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
COPY ./dist .
|
||||||
|
EXPOSE 5555
|
||||||
|
CMD ["node", "src/index.js"]
|
||||||
42
dist/public/index.html
vendored
Normal file
42
dist/public/index.html
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
|
<title>WebRTC Phone</title>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css?family=Montserrat:300,400,500,700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" href="./styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<div class="logo-container">
|
||||||
|
<h1 class="logo-text">
|
||||||
|
WebRTC<span class="logo-highlight">Phone</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="content-container">
|
||||||
|
<div class="active-users-panel" id="active-user-container">
|
||||||
|
<h3 class="panel-title">Users Online:</h3>
|
||||||
|
</div>
|
||||||
|
<div class="video-chat-container">
|
||||||
|
<h2 class="talk-info" id="talking-with-info">
|
||||||
|
Select active user on the left menu.
|
||||||
|
</h2>
|
||||||
|
<div class="video-container">
|
||||||
|
<video autoplay class="remote-video" id="remote-video"></video>
|
||||||
|
<video autoplay muted class="local-video" id="local-video"></video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js" integrity="sha384-mkQ3/7FUtcGyoppY6bz/PORYoGqOl7/aSUMn2ymDOJcapfS6PHqxhRTMh1RR0Q6+" crossorigin="anonymous"></script>
|
||||||
|
<script src="./scripts/index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
106
dist/public/scripts/index.js
vendored
Normal file
106
dist/public/scripts/index.js
vendored
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"use strict";
|
||||||
|
let isAlreadyCalling = false;
|
||||||
|
let getCalled = false;
|
||||||
|
const existingCalls = [];
|
||||||
|
const { RTCPeerConnection, RTCSessionDescription } = window;
|
||||||
|
const peerConnection = new RTCPeerConnection();
|
||||||
|
function unselectUsersFromList() {
|
||||||
|
const alreadySelectedUser = document.querySelectorAll(".active-user.active-user--selected");
|
||||||
|
alreadySelectedUser.forEach(el => {
|
||||||
|
el.setAttribute("class", "active-user");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function createUserItemContainer(socketId) {
|
||||||
|
const userContainerEl = document.createElement("div");
|
||||||
|
const usernameEl = document.createElement("p");
|
||||||
|
userContainerEl.setAttribute("class", "active-user");
|
||||||
|
userContainerEl.setAttribute("id", socketId);
|
||||||
|
usernameEl.setAttribute("class", "username");
|
||||||
|
usernameEl.innerHTML = `Socket: ${socketId}`;
|
||||||
|
userContainerEl.appendChild(usernameEl);
|
||||||
|
userContainerEl.addEventListener("click", () => {
|
||||||
|
unselectUsersFromList();
|
||||||
|
userContainerEl.setAttribute("class", "active-user active-user--selected");
|
||||||
|
const talkingWithInfo = document.getElementById("talking-with-info");
|
||||||
|
talkingWithInfo.innerHTML = `Talking with: "Socket: ${socketId}"`;
|
||||||
|
console.log('calling socket id [%o]', socketId);
|
||||||
|
callUser(socketId);
|
||||||
|
});
|
||||||
|
return userContainerEl;
|
||||||
|
}
|
||||||
|
async function callUser(socketId) {
|
||||||
|
const offer = await peerConnection.createOffer();
|
||||||
|
await peerConnection.setLocalDescription(new RTCSessionDescription(offer));
|
||||||
|
socket.emit("call-user", {
|
||||||
|
offer,
|
||||||
|
to: socketId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function updateUserList(socketIds) {
|
||||||
|
const activeUserContainer = document.getElementById("active-user-container");
|
||||||
|
socketIds.forEach(socketId => {
|
||||||
|
if (socketId === socket.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const alreadyExistingUser = document.getElementById(socketId);
|
||||||
|
if (!alreadyExistingUser) {
|
||||||
|
const userContainerEl = createUserItemContainer(socketId);
|
||||||
|
activeUserContainer.appendChild(userContainerEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const socket = io.connect("localhost:5555");
|
||||||
|
socket.on("update-user-list", ({ users }) => {
|
||||||
|
updateUserList(users);
|
||||||
|
});
|
||||||
|
socket.on("remove-user", ({ socketId }) => {
|
||||||
|
const elToRemove = document.getElementById(socketId);
|
||||||
|
if (elToRemove) {
|
||||||
|
elToRemove.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
socket.on("call-made", async (data) => {
|
||||||
|
if (getCalled) {
|
||||||
|
const confirmed = confirm(`User "Socket: ${data.socket}" wants to call you. Do accept this call?`);
|
||||||
|
if (!confirmed) {
|
||||||
|
socket.emit("reject-call", {
|
||||||
|
from: data.socket
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer));
|
||||||
|
const answer = await peerConnection.createAnswer();
|
||||||
|
await peerConnection.setLocalDescription(new RTCSessionDescription(answer));
|
||||||
|
socket.emit("make-answer", {
|
||||||
|
answer,
|
||||||
|
to: data.socket
|
||||||
|
});
|
||||||
|
getCalled = true;
|
||||||
|
});
|
||||||
|
socket.on("answer-made", async (data) => {
|
||||||
|
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.answer));
|
||||||
|
if (!isAlreadyCalling) {
|
||||||
|
callUser(data.socket);
|
||||||
|
isAlreadyCalling = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
socket.on("call-rejected", data => {
|
||||||
|
alert(`User: "Socket: ${data.socket}" rejected your call.`);
|
||||||
|
unselectUsersFromList();
|
||||||
|
});
|
||||||
|
peerConnection.ontrack = function ({ streams: [stream] }) {
|
||||||
|
const remoteVideo = document.getElementById("remote-video");
|
||||||
|
if (remoteVideo) {
|
||||||
|
remoteVideo.srcObject = stream;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
navigator.getUserMedia({ video: true, audio: true }, stream => {
|
||||||
|
const localVideo = document.getElementById("local-video");
|
||||||
|
if (localVideo) {
|
||||||
|
localVideo.srcObject = stream;
|
||||||
|
}
|
||||||
|
stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
|
||||||
|
}, error => {
|
||||||
|
console.warn(error.message);
|
||||||
|
});
|
||||||
105
dist/public/styles.css
vendored
Normal file
105
dist/public/styles.css
vendored
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: "Montserrat", sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: #f9fafc;
|
||||||
|
color: #595354;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 10px 40px;
|
||||||
|
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header > .logo-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header > .logo-container > .logo-img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header > .logo-container > .logo-text {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header > .logo-container > .logo-text > .logo-highlight {
|
||||||
|
color: #65a9e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 89px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-users-panel {
|
||||||
|
width: 300px;
|
||||||
|
height: 100%;
|
||||||
|
border-right: 1px solid #cddfe7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
padding-left: 30px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 18px;
|
||||||
|
border-bottom: 1px solid #cddfe7;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-user {
|
||||||
|
padding: 10px 30px;
|
||||||
|
border-bottom: 1px solid #cddfe7;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-user:hover {
|
||||||
|
background-color: #e8e9eb;
|
||||||
|
transition: background-color 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-user--selected {
|
||||||
|
background-color: #fff;
|
||||||
|
border-right: 5px solid #65a9e5;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-chat-container {
|
||||||
|
padding: 0 20px;
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.talk-info {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-video {
|
||||||
|
border: 1px solid #cddfe7;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.local-video {
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid #cddfe7;
|
||||||
|
bottom: 60px;
|
||||||
|
right: 40px;
|
||||||
|
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
19
dist/src/index.js
vendored
Normal file
19
dist/src/index.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const server_1 = require("./server");
|
||||||
|
const server = new server_1.Server();
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('Received [SIGINT]. Shutting down...');
|
||||||
|
server.dispose();
|
||||||
|
});
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('Received [SIGTERM]. Shutting down...');
|
||||||
|
server.dispose();
|
||||||
|
});
|
||||||
|
process.on('SIGBREAK', () => {
|
||||||
|
console.log('Received [SIGBREAK]. Shutting down...');
|
||||||
|
server.dispose();
|
||||||
|
});
|
||||||
|
server.listen(port => {
|
||||||
|
console.log(`Server is listening on [http://localhost:${port}]`);
|
||||||
|
});
|
||||||
73
dist/src/server.js
vendored
Normal file
73
dist/src/server.js
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.Server = void 0;
|
||||||
|
const path_1 = __importDefault(require("path"));
|
||||||
|
const express_1 = __importDefault(require("express"));
|
||||||
|
const socket_io_1 = require("socket.io");
|
||||||
|
const http_1 = require("http");
|
||||||
|
class Server {
|
||||||
|
constructor() {
|
||||||
|
this.activeSockets = new Set();
|
||||||
|
this.DEFAULT_PORT = 5555;
|
||||||
|
this.app = (0, express_1.default)();
|
||||||
|
this.httpServer = (0, http_1.createServer)(this.app);
|
||||||
|
this.io = new socket_io_1.Server(this.httpServer);
|
||||||
|
this.configureApp();
|
||||||
|
this.handleRoutes();
|
||||||
|
this.handleSocketConnection();
|
||||||
|
}
|
||||||
|
configureApp() {
|
||||||
|
this.app.use(express_1.default.static(path_1.default.join(__dirname, "../public")));
|
||||||
|
}
|
||||||
|
handleRoutes() {
|
||||||
|
this.app.get('/', (req, res) => {
|
||||||
|
res.send(`<h1>Hello World</h1>`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleSocketConnection() {
|
||||||
|
this.io.on('connection', socket => {
|
||||||
|
if (!this.activeSockets.has(socket.id)) {
|
||||||
|
this.activeSockets.add(socket.id);
|
||||||
|
console.log(this.activeSockets.entries());
|
||||||
|
socket.emit("update-user-list", {
|
||||||
|
users: Array.from(this.activeSockets)
|
||||||
|
});
|
||||||
|
socket.broadcast.emit("update-user-list", {
|
||||||
|
users: [socket.id]
|
||||||
|
});
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
this.activeSockets.delete(socket.id);
|
||||||
|
socket.broadcast.emit("remove-user", {
|
||||||
|
socketId: socket.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
socket.on("call-user", data => {
|
||||||
|
console.log('[Server] call received [%o]', data);
|
||||||
|
socket.to(data.to).emit("call-made", {
|
||||||
|
offer: data.offer,
|
||||||
|
socket: socket.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
socket.on("make-answer", data => {
|
||||||
|
socket.to(data.to).emit("answer-made", {
|
||||||
|
socket: socket.id,
|
||||||
|
answer: data.answer
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
listen(callback) {
|
||||||
|
this.httpServer.listen(this.DEFAULT_PORT, () => {
|
||||||
|
callback(this.DEFAULT_PORT);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dispose() {
|
||||||
|
this.httpServer.close();
|
||||||
|
this.io.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.Server = Server;
|
||||||
@@ -5,7 +5,9 @@
|
|||||||
"module": "./src/index.ts",
|
"module": "./src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "ts-node src/index.ts",
|
"start": "ts-node src/index.ts",
|
||||||
"dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts"
|
"dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts",
|
||||||
|
"build": "bash scripts/build.sh",
|
||||||
|
"build:tsc": "tsc"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@@ -4,27 +4,25 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
<title>Dogeller</title>
|
<title>WebRTC Phone</title>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css?family=Montserrat:300,400,500,700&display=swap"
|
href="https://fonts.googleapis.com/css?family=Montserrat:300,400,500,700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="./styles.css" />
|
<link rel="stylesheet" href="./styles.css" />
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
<img src="./img/doge.png" alt="doge logo" class="logo-img" />
|
|
||||||
<h1 class="logo-text">
|
<h1 class="logo-text">
|
||||||
Doge<span class="logo-highlight">ller</span>
|
WebRTC<span class="logo-highlight">Phone</span>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
<div class="active-users-panel" id="active-user-container">
|
<div class="active-users-panel" id="active-user-container">
|
||||||
<h3 class="panel-title">Active Users:</h3>
|
<h3 class="panel-title">Users Online:</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="video-chat-container">
|
<div class="video-chat-container">
|
||||||
<h2 class="talk-info" id="talking-with-info">
|
<h2 class="talk-info" id="talking-with-info">
|
||||||
@@ -39,9 +37,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js" integrity="sha384-mkQ3/7FUtcGyoppY6bz/PORYoGqOl7/aSUMn2ymDOJcapfS6PHqxhRTMh1RR0Q6+" crossorigin="anonymous"></script>
|
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js" integrity="sha384-mkQ3/7FUtcGyoppY6bz/PORYoGqOl7/aSUMn2ymDOJcapfS6PHqxhRTMh1RR0Q6+" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script src="./scripts/index.js"></script>
|
<script src="./scripts/index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
28
scripts/build.sh
Executable file
28
scripts/build.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
echo "Building"
|
||||||
|
|
||||||
|
bun run build:tsc
|
||||||
|
|
||||||
|
# Declare array with proper syntax
|
||||||
|
filesToCopy=("index.html" "styles.css")
|
||||||
|
destination="dist"
|
||||||
|
|
||||||
|
# Create destination directory if it doesn't exist
|
||||||
|
mkdir -p "${destination}"
|
||||||
|
|
||||||
|
# Copy files from public directory
|
||||||
|
for file in "${filesToCopy[@]}"; do
|
||||||
|
echo "[public/${file}] --> [${destination}/public]"
|
||||||
|
|
||||||
|
# Check if source file exists
|
||||||
|
if [ -f "public/${file}" ]; then
|
||||||
|
cp "public/${file}" "${destination}/public"
|
||||||
|
else
|
||||||
|
echo "Warning: public/${file} not found, skipping..."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Build complete"
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"outDir": "dist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user