[call-me] - #3 Show a drop-down list of registered users to call

This commit is contained in:
Miroslav Pejic
2025-01-22 17:32:56 +01:00
parent e99ab18cef
commit 067b49012e
5 changed files with 212 additions and 61 deletions
+31 -9
View File
@@ -269,6 +269,9 @@ const isAuthorized = (req) => {
function handleConnection(socket) {
console.log('User connected:', socket.id);
// Refresh connected users
broadcastConnectedUsers();
// Send a ping message to the newly connected client
sendPing(socket);
@@ -287,6 +290,7 @@ function handleConnection(socket) {
handleSignIn(data);
break;
case 'offerAccept':
case 'offerBusy':
case 'offerDecline':
case 'offerCreate':
handleOffer(data);
@@ -336,7 +340,7 @@ function handleConnection(socket) {
socket.username = name;
console.log('User signed in:', name);
sendMsgTo(socket, { type: 'signIn', success: true });
console.log('Connected Users', getConnectedUsers());
broadcastConnectedUsers();
} else {
sendMsgTo(socket, { type: 'signIn', success: false, message: 'Username already in use' });
}
@@ -366,6 +370,10 @@ function handleConnection(socket) {
console.warn(`User ${name} declined your call`);
sendError(recipientSocket || socket, `User ${name} declined your call`);
break;
case 'offerBusy':
console.warn(`User ${name} busy in another call`);
sendError(recipientSocket || socket, `User ${name} busy in another call.`);
break;
default:
console.warn(`Unknown offer type: ${type}`);
break;
@@ -381,7 +389,7 @@ function handleConnection(socket) {
case 'leave':
if (recipientSocket !== undefined) {
console.log('Leave room', socket.username);
sendMsgTo(recipientSocket, { type: 'leave' });
sendMsgTo(recipientSocket, { type: 'leave', name: socket.username });
}
break;
default:
@@ -394,10 +402,11 @@ function handleConnection(socket) {
// Function to handle the closing of a connection
function handleClose() {
if (socket.username) {
console.log('User disconnected:', socket.username);
users.delete(socket.username);
console.log('Connected Users', getConnectedUsers());
const name = socket.username;
if (name) {
console.log('User disconnected:', name);
users.delete(name);
broadcastConnectedUsers();
}
}
}
@@ -413,9 +422,22 @@ function getConnectedUsers() {
return Array.from(users.keys());
}
// Function to broadcast a message to all connection
function broadcastMsg(socket, message) {
console.log('Broadcast message:', message.type);
// Function to broadcast all connected users
function broadcastConnectedUsers() {
const connectedUsers = getConnectedUsers();
console.log('Connected Users', connectedUsers);
broadcastMsg({ type: 'users', users: connectedUsers });
}
// Function to broadcast a message to all connected clients
function broadcastMsg(message) {
console.log('Broadcast message:', message);
io.emit('message', message);
}
// Function to broadcast a message to all connected clients except the sender
function broadcastMsgExpectSender(socket, message) {
console.log('Broadcast message:', message);
socket.broadcast.emit('message', message);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "call-me",
"version": "1.0.51",
"version": "1.0.60",
"description": "Your Go-To for Instant Video Calls",
"author": "Miroslav Pejic - miroslav.pejic.85@gmail.com",
"license": "AGPLv3",
+151 -42
View File
@@ -3,9 +3,6 @@
// This user agent
const userAgent = navigator.userAgent;
// Check device
const isMobileDevice = isMobile(userAgent);
// WebSocket connection to the signaling server
const socket = io();
@@ -23,7 +20,7 @@ const signInPage = document.getElementById('signInPage');
const usernameIn = document.getElementById('usernameIn');
const signInBtn = document.getElementById('signInBtn');
const roomPage = document.getElementById('roomPage');
const callUsernameIn = document.getElementById('callUsernameIn');
const callUsernameSelect = document.getElementById('callUsernameSelect');
const hideBtn = document.getElementById('hideBtn');
const callBtn = document.getElementById('callBtn');
const swapCameraBtn = document.getElementById('swapCameraBtn');
@@ -36,14 +33,19 @@ const localUsername = document.getElementById('localUsername');
const remoteVideo = document.getElementById('remoteVideo');
// User and connection information
let userInfo;
let userName;
let connectedUser;
let thisConnection;
let camera = 'user';
let stream;
// Variable to store the interval ID
let sessionTimerId = null;
// On html page loaded...
document.addEventListener('DOMContentLoaded', async function () {
userInfo = getUserInfo(userAgent);
handleToolTip();
handleLocalStorage();
handleDirectJoin();
@@ -51,6 +53,28 @@ document.addEventListener('DOMContentLoaded', async function () {
fetchRandomImage();
});
// Get user information from User-Agent string
function getUserInfo(userAgent) {
const parser = new UAParser(userAgent);
const { device, os, browser } = parser.getResult();
// Determine device type and specific characteristics
const deviceType = device.type || 'desktop';
const isIPad = device.model?.toLowerCase() === 'ipad';
return {
device: {
isMobile: deviceType === 'mobile',
isTablet: deviceType === 'tablet',
isDesktop: deviceType === 'desktop',
isIPad,
},
os: { name: os.name || 'Unknown OS', version: os.version || 'Unknown Version' },
browser: { name: browser.name || 'Unknown Browser', version: browser.version || 'Unknown Version' },
userAgent,
};
}
// Handle config
appTitle.innerText = app.title;
appName.innerText = app.name;
@@ -106,6 +130,7 @@ async function checkHostPassword(maxRetries = 3, attempts = 0) {
if (validationResult.success) {
await Swal.fire({
position: 'top',
icon: 'success',
title: 'Access Granted',
text: 'Password validated successfully!',
@@ -117,6 +142,7 @@ async function checkHostPassword(maxRetries = 3, attempts = 0) {
attempts++;
if (attempts < maxRetries) {
await Swal.fire({
position: 'top',
icon: 'error',
title: 'Invalid Password',
text: `Please try again. (${attempts}/${maxRetries} attempts)`,
@@ -125,6 +151,7 @@ async function checkHostPassword(maxRetries = 3, attempts = 0) {
checkHostPassword(maxRetries, attempts);
} else {
await Swal.fire({
position: 'top',
icon: 'warning',
title: 'Too Many Attempts',
text: 'You have exceeded the maximum number of attempts. Please try again later.',
@@ -138,6 +165,7 @@ async function checkHostPassword(maxRetries = 3, attempts = 0) {
} catch (error) {
console.error('Error:', error);
Swal.fire({
position: 'top',
icon: 'error',
title: 'Error',
text: 'An error occurred while joining the host.',
@@ -177,7 +205,7 @@ async function fetchRandomImage() {
// Initialize tooltips and handle hiding them when clicked
function handleToolTip() {
if (isMobileDevice) return;
if (userInfo.device.isMobile) return;
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-toggle="tooltip"]'));
const tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
@@ -196,7 +224,6 @@ function handleToolTip() {
// Handle localStorage data
function handleLocalStorage() {
usernameIn.value = localStorage.callMeUsername ? localStorage.callMeUsername : '';
callUsernameIn.value = localStorage.callMeUsernameToCall ? localStorage.callMeUsernameToCall : '';
}
// Handle Room direct join
@@ -215,25 +242,63 @@ function handleDirectJoin() {
if (call) {
// Call user if call is provided
callUsernameIn.value = call;
handleCallClick();
setTimeout(() => {
selectIndexByValue(call);
handleCallClick();
}, 3000);
}
}
if (!password) checkHostPassword();
}
// Session Time
// Select index by passed value
function selectIndexByValue(value) {
for (let i = 0; i < callUsernameSelect.options.length; i++) {
if (callUsernameSelect.options[i].value === value) {
callUsernameSelect.selectedIndex = i; // Select the option
break;
}
}
}
// Remove option by value
function removeOptionByValue(value) {
for (let i = 0; i < callUsernameSelect.options.length; i++) {
if (callUsernameSelect.options[i].value === value) {
alert(value);
callUsernameSelect.remove(i); // Remove the matching option
break;
}
}
}
// Start Session Time
function startSessionTime() {
console.log('Start session time');
elemDisplay(sessionTime, true, 'inline-flex');
let sessionElapsedTime = 0;
setInterval(function printTime() {
if (sessionTimerId !== null) {
clearInterval(sessionTimerId);
}
sessionTimerId = setInterval(function printTime() {
sessionElapsedTime++;
sessionTime.innerText = secondsToHms(sessionElapsedTime);
}, 1000);
}
// Stop Session Time
function stopSessionTime() {
console.log('Stop session time');
if (sessionTimerId !== null) {
clearInterval(sessionTimerId);
sessionTimerId = null;
}
elemDisplay(sessionTime, false);
}
// Session Time in h/m/s
function secondsToHms(d) {
d = Number(d);
@@ -292,8 +357,11 @@ function handleMessage(data) {
case 'candidate':
handleCandidate(data);
break;
case 'users':
handleUsers(data);
break;
case 'leave':
handleLeave();
handleLeave(false);
break;
case 'error':
handleError(data.message, data.message);
@@ -303,18 +371,13 @@ function handleMessage(data) {
}
}
// helpers
function isMobile(userAgent) {
return !!/Android|webOS|iPhone|iPad|iPod|BB10|BlackBerry|IEMobile|Opera Mini|Mobile|mobile/i.test(userAgent || '');
}
// Enumerate Devices for camera swap functionality
function handleEnumerateDevices() {
navigator.mediaDevices
.enumerateDevices()
.then((devices) => {
const videoInputs = devices.filter((device) => device.kind === 'videoinput');
if (videoInputs.length > 1 && isMobileDevice) {
if (videoInputs.length > 1 && userInfo.device.isMobile) {
swapCameraBtn.addEventListener('click', swapCamera);
elemDisplay(swapCameraBtn, true, 'inline');
}
@@ -336,7 +399,7 @@ function handleListeners() {
localVideoContainer.addEventListener('click', toggleFullScreen);
remoteVideo.addEventListener('click', toggleFullScreen);
// Add keyUp listeners
callUsernameIn.addEventListener('keyup', (e) => handleKeyUp(e, handleCallClick));
callUsernameSelect.addEventListener('keyup', (e) => handleKeyUp(e, handleCallClick));
usernameIn.addEventListener('keyup', (e) => handleKeyUp(e, handleSignInClick));
}
@@ -372,10 +435,9 @@ function toggleLocalVideo() {
// Handle call button click
function handleCallClick() {
const callToUsername = callUsernameIn.value.trim();
const callToUsername = callUsernameSelect.value.trim();
if (callToUsername.length > 0) {
if (callToUsername === userName) {
callUsernameIn.value = '';
handleError('You cannot call yourself.');
return;
}
@@ -385,7 +447,6 @@ function handleCallClick() {
from: userName,
to: callToUsername,
});
localStorage.callMeUsernameToCall = callToUsername;
popupMsg(`You are calling ${callToUsername}.<br/>Please wait for them to answer.`);
} else {
handleError('Please enter a username to call.');
@@ -423,6 +484,7 @@ function swapCamera() {
// Refresh video streams
refreshLocalVideoStream(newStream);
refreshPeerVideoStreams(newStream);
// Check video/audio status
checkVideoAudioStatus();
})
@@ -503,7 +565,7 @@ function handlePing(data) {
type: 'pong',
message: {
client: 'Hello Server!',
agent: userAgent,
userInfo,
},
});
}
@@ -511,8 +573,8 @@ function handlePing(data) {
// Handle user not found from the server
function handleNotFound(data) {
const { username } = data;
callUsernameIn.value = '';
handleError(`Username ${username} not found!`);
removeOptionByValue(username);
}
// Handle sign-in response from the server
@@ -603,7 +665,7 @@ function offerCreate() {
type: 'offer',
offer,
});
elemDisplay(callUsernameIn, false);
elemDisplay(callUsernameSelect, false);
})
.catch((error) => {
handleError('Error when creating an offer.', error);
@@ -612,9 +674,17 @@ function offerCreate() {
// Accept incoming offer
function offerAccept(data) {
// I'm already in call decline the new one!
if (remoteVideo.srcObject) {
data.type = 'offerBusy';
sendMsg({ ...data });
return;
}
sound('ring');
Swal.fire({
position: 'center',
position: 'top',
imageUrl: 'assets/ring.png',
imageWidth: 284,
imageHeight: 120,
@@ -628,7 +698,7 @@ function offerAccept(data) {
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
elemDisplay(callUsernameIn, false);
elemDisplay(callUsernameSelect, false);
elemDisplay(callBtn, false);
data.type = 'offerCreate';
socket.recipient = data.from;
@@ -677,6 +747,22 @@ function handleCandidate(data) {
});
}
// Handle connected users
function handleUsers(data) {
console.log('Connected users ------>', data.users);
callUsernameSelect.innerHTML = '';
data.users.forEach((user) => {
if (user === userName) return;
const option = document.createElement('option');
option.value = user;
option.textContent = user;
callUsernameSelect.appendChild(option);
});
if (callUsernameSelect.options.length === 0) {
callUsernameSelect.innerHTML = '<option value="" disabled selected>Select a user to call</option>';
}
}
// Play audio sound
async function sound(name) {
const sound = './assets/' + name + '.wav';
@@ -689,30 +775,53 @@ async function sound(name) {
}
}
// Handle leaving the room
function handleLeave() {
// Stop local video tracks
if (localVideo.srcObject != null) {
localVideo.srcObject.getTracks().forEach((track) => track.stop());
localVideo.srcObject = null;
// Helper function to stop all tracks and clear media stream
function stopMediaStream(videoElement) {
if (videoElement.srcObject) {
videoElement.srcObject.getTracks().forEach((track) => track.stop());
videoElement.srcObject = null;
}
// Stop remote video tracks
if (remoteVideo.srcObject != null) {
remoteVideo.srcObject.getTracks().forEach((track) => track.stop());
remoteVideo.srcObject = null;
}
// Disconnect from server
}
// Helper function to disconnect and clean up the connection
function disconnectConnection() {
if (thisConnection) {
thisConnection.close();
thisConnection = null;
}
// GoTo homepage
connectedUser = null;
window.location.href = '/';
}
// Handle leaving the room
function handleLeave(disconnect = true) {
if (disconnect) {
// Stop local and remote video tracks
stopMediaStream(localVideo);
stopMediaStream(remoteVideo);
// Disconnect from server and reset state
disconnectConnection();
connectedUser = null;
// Redirect to homepage
window.location.href = '/';
} else {
// Show UI elements
elemDisplay(callUsernameSelect, true);
elemDisplay(callBtn, true, 'inline');
// Stop remote video tracks only
stopMediaStream(remoteVideo);
// Stop session time
stopSessionTime();
// Reset state
connectedUser = null;
}
}
// Handle and display errors
function handleError(message, error = false, position = 'center', timer = 6000) {
function handleError(message, error = false, position = 'top', timer = 6000) {
if (error) console.error(error);
sound('notify');
Swal.fire({
+7 -4
View File
@@ -93,10 +93,10 @@
<span id="sessionTime">0s</span>
<div class="row text-center">
<div class="col-md-12">
<div class="mb-3">
<!-- Input field for entering the username to call -->
<input id="callUsernameIn" type="text" class="form-control" placeholder="Username to call" />
</div>
<!-- Input field for selecting the username to call -->
<select id="callUsernameSelect">
<option value="" disabled selected>Select a user to call</option>
</select>
<!-- Button to hide/show the local video -->
<button
id="hideBtn"
@@ -166,6 +166,9 @@
<script src="config.js"></script>
<script src="client.js"></script>
<!-- Include UaParser -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/UAParser.js/1.0.37/ua-parser.min.js"></script>
<!-- Include Axios file -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+22 -5
View File
@@ -217,16 +217,33 @@ input {
border-color: #007bff;
}
/* Text Input Styles */
#callUsernameIn {
/* Select Input Styles */
select {
width: 100%;
padding: 10px;
margin-bottom: 10px;
background: rgba(0, 0, 0, 0.5);
color: var(--text-color);
border: none;
border-radius: 4px;
font-size: 16px;
text-align: center;
text-align-last: center;
appearance: none;
outline: none;
cursor: pointer;
}
#callUsernameIn::placeholder {
color: #525252;
opacity: 1;
#callUsernameSelect option[data-icon]::before {
font-family: 'Font Awesome 6';
font-weight: 900;
margin-right: 8px;
font-size: 1rem;
content: attr(data-icon);
}
select::-ms-expand {
display: none;
}
/* Center Alignment */