diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3c801a2 --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/dist/public/index.html b/dist/public/index.html new file mode 100644 index 0000000..140e0d4 --- /dev/null +++ b/dist/public/index.html @@ -0,0 +1,42 @@ + + + + + + + WebRTC Phone + + + + +
+
+
+

+ WebRTCPhone +

+
+
+
+
+

Users Online:

+
+
+

+ Select active user on the left menu. +

+
+ + +
+
+
+
+ + + + + diff --git a/dist/public/scripts/index.js b/dist/public/scripts/index.js new file mode 100644 index 0000000..011359a --- /dev/null +++ b/dist/public/scripts/index.js @@ -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); +}); diff --git a/dist/public/styles.css b/dist/public/styles.css new file mode 100644 index 0000000..78c3aaf --- /dev/null +++ b/dist/public/styles.css @@ -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; +} diff --git a/dist/src/index.js b/dist/src/index.js new file mode 100644 index 0000000..6a0a718 --- /dev/null +++ b/dist/src/index.js @@ -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}]`); +}); diff --git a/dist/src/server.js b/dist/src/server.js new file mode 100644 index 0000000..915a59a --- /dev/null +++ b/dist/src/server.js @@ -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(`

Hello World

`); + }); + } + 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; diff --git a/package.json b/package.json index 583dd72..7789a79 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "module": "./src/index.ts", "scripts": { "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": [], "author": "", diff --git a/public/index.html b/public/index.html index 8d69a9e..140e0d4 100644 --- a/public/index.html +++ b/public/index.html @@ -4,27 +4,25 @@ - Dogeller + WebRTC Phone -
- doge logo

- Dogeller + WebRTCPhone

-

Active Users:

+

Users Online:

@@ -39,9 +37,6 @@

- - - diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..61e83ad --- /dev/null +++ b/scripts/build.sh @@ -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" diff --git a/tsconfig.json b/tsconfig.json index d5e4f48..15cae34 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "allowJs": true, "strict": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "outDir": "dist" } } \ No newline at end of file