diff --git a/README.md b/README.md index 991f52bf..ca57c261 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ Free WebRTC - SFU - Simple, Secure, Scalable Real-Time Video Conferences with su - Right-click options on video elements for additional controls. - Supports [REST API](app/api/README.md) (Application Programming Interface). - Integration with [Slack](https://api.slack.com/apps/) for enhanced communication. +- Integration with [Discord](https://discord.com) for enhanced communication. +- Integration with [Mattermost](https://mattermost.com/) for enhanced communication. - Utilizes [Sentry](https://sentry.io/) for error reporting. - And much more... @@ -205,6 +207,10 @@ $ PORT=3011 npm start - Install [docker engine](https://docs.docker.com/engine/install/) and [docker compose](https://docs.docker.com/compose/install/) ```bash +# Clone this repo +$ git clone https://github.com/miroslavpejic85/mirotalksfu.git +# Go to to dir mirotalksfu +$ cd mirotalksfu # Copy app/src/config.template.js in app/src/config.js IMPORTANT (edit it according to your needs) $ cp app/src/config.template.js app/src/config.js # Copy docker-compose.template.yml in docker-compose.yml and edit it if needed diff --git a/SECURITY.md b/SECURITY.md index 570fea68..ae64fd10 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -28,7 +28,7 @@ We would like to extend our gratitude to the following individuals for their res | Name | Contact | | ----------------- | ------------------------------- | | `Hendrik Siewert` | hendrik.siewert@upb.de | -| `Caio Fook` | caio.fook@gmail.com | +| `Caio Fook` | https://github.com/caiofook | | `Nishant Jain` | https://twitter.com/realArcherL | Their dedication to security has contributed to the continuous improvement of our systems, ensuring the safety and privacy of our users and data. diff --git a/app/src/Discord.js b/app/src/Discord.js new file mode 100644 index 00000000..2b8f5ffd --- /dev/null +++ b/app/src/Discord.js @@ -0,0 +1,75 @@ +'use strict'; + +const { Client, GatewayIntentBits } = require('discord.js'); + +const { v4: uuidV4 } = require('uuid'); + +const Logger = require('./Logger'); + +const log = new Logger('Discord'); + +// Discord Bot Class Implementation +class Discord { + constructor(token, commands) { + this.token = token; + this.commands = commands; + this.discordClient = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, // Make sure this is enabled in your Discord Developer Portal + ], + }); + + this.setupEventHandlers(); + + this.discordClient.login(this.token).catch((error) => { + log.error('Failed to login to Discord:', error); + }); + } + + setupEventHandlers() { + this.discordClient.once('ready', () => { + log.info(`Discord Bot Logged in as ${this.discordClient.user.tag}!`, '😎'); + }); + + this.discordClient.on('error', (error) => { + log.error(`Discord Client Error: ${error.message}`, { stack: error.stack }); + }); + + this.discordClient.on('messageCreate', async (message) => { + if (message.author.bot) return; + + for (const command of this.commands) { + if (message.content.startsWith(command.name)) { + switch (command.name) { + case '/sfu': + const roomId = this.generateMeetingRoom(command.baseUrl); + await this.sendMessage(message.channel, `${command.message} ${roomId}`); + break; + //.... + default: + await this.sendMessage(message.channel, command.message); + break; + } + break; // Exit the loop after finding the command + } + } + }); + } + + generateMeetingRoom(baseUrl) { + const roomId = uuidV4(); + return `${baseUrl}${roomId}`; + } + + async sendMessage(channel, content) { + try { + await channel.send(content); + } catch (error) { + log.error(`Failed to send message: ${error.message}`); + } + } +} + +module.exports = Discord; diff --git a/app/src/Host.js b/app/src/Host.js index f660028e..f1f4c771 100644 --- a/app/src/Host.js +++ b/app/src/Host.js @@ -3,7 +3,6 @@ module.exports = class Host { constructor() { this.authorizedIPs = new Map(); - this.roomActive = false; } /** @@ -30,7 +29,6 @@ module.exports = class Host { */ setAuthorizedIP(ip, authorized) { this.authorizedIPs.set(ip, authorized); - this.setRoomActive(); } /** @@ -42,37 +40,12 @@ module.exports = class Host { return this.authorizedIPs.has(ip); } - /** - * Host room status - * @returns boolean - */ - isRoomActive() { - return this.roomActive; - } - - /** - * Set host room activate - */ - setRoomActive() { - this.roomActive = true; - } - - /** - * Set host room deactivate - */ - setRoomDeactivate() { - this.roomActive = false; - } - /** * Delete ip from authorized IPs * @param {string} ip * @returns boolean */ deleteIP(ip) { - if (this.isAuthorizedIP(ip)) { - this.setRoomDeactivate(); - } return this.authorizedIPs.delete(ip); } }; diff --git a/app/src/Mattermost.js b/app/src/Mattermost.js new file mode 100644 index 00000000..d463b27c --- /dev/null +++ b/app/src/Mattermost.js @@ -0,0 +1,100 @@ +'use strict'; + +const { Client4 } = require('@mattermost/client'); + +const { v4: uuidV4 } = require('uuid'); + +const config = require('./config'); + +const Logger = require('./Logger'); + +const log = new Logger('Mattermost'); + +class Mattermost { + constructor(app) { + const { + enabled, + token, + serverUrl, + username, + password, + commands = '/sfu', + texts = '/sfu', + } = config.mattermost || {}; + + if (!enabled) return; // Check if Mattermost integration is enabled + + this.app = app; + this.allowed = config.api.allowed && config.api.allowed.mattermost; + this.token = token; + this.serverUrl = serverUrl; + this.username = username; + this.password = password; + this.commands = commands; + this.texts = texts; + + this.client = new Client4(); + this.client.setUrl(this.serverUrl); + this.authenticate(); + this.setupEventHandlers(); + } + + async authenticate() { + try { + const user = await this.client.login(this.username, this.password); + log.debug('--------> Logged into Mattermost as', user.username); + } catch (error) { + log.error('Failed to log into Mattermost:', error); + } + } + + setupEventHandlers() { + this.app.post('/mattermost', (req, res) => { + // + if (!this.allowed) { + return res + .status(403) + .send('This endpoint has been disabled. Please contact the administrator for further information.'); + } + + log.debug('Mattermost request received:', { header: req.header, body: req.body }); + + const { token, text, command, channel_id } = req.body; + if (token !== this.token) { + log.error('Invalid token attempt', { token }); + return res.status(403).send('Invalid token'); + } + + const payload = { text: '', channel_id }; + if (this.processInput(command, payload, req) || this.processInput(text, payload, req)) { + return res.json(payload); + } + + return res.status(200).send('Command not recognized'); + }); + } + + processInput(input, payload, req) { + for (const cmd of [...this.commands, ...this.texts]) { + if (input.trim() === cmd.name) { + switch (cmd.name) { + case '/sfu': + payload.text = `${cmd.message} ${this.getMeetingURL(req)}`; + break; + default: + break; + } + return true; + } + } + return false; + } + + getMeetingURL(req) { + const host = req.headers.host; + const protocol = host.includes('localhost') ? 'http' : 'https'; + return `${protocol}://${host}/join/${uuidV4()}`; + } +} + +module.exports = Mattermost; diff --git a/app/src/Peer.js b/app/src/Peer.js index 599e66f5..30964b08 100644 --- a/app/src/Peer.js +++ b/app/src/Peer.js @@ -12,6 +12,7 @@ module.exports = class Peer { peer_name, peer_presenter, peer_audio, + peer_audio_volume, peer_video, peer_video_privacy, peer_recording, @@ -29,6 +30,7 @@ module.exports = class Peer { this.peer_presenter = peer_presenter; this.peer_audio = peer_audio; this.peer_video = peer_video; + this.peer_audio_volume = peer_audio_volume; this.peer_video_privacy = peer_video_privacy; this.peer_recording = peer_recording; this.peer_hand = peer_hand; @@ -84,6 +86,10 @@ module.exports = class Peer { this.peer_info.peer_recording = data.status; this.peer_recording = data.status; break; + case 'peerAudio': + this.peer_info.peer_audio_volume = data.volume; + this.peer_audio_volume = data.volume; + break; default: break; } diff --git a/app/src/Room.js b/app/src/Room.js index ee098fa3..bb7ed977 100644 --- a/app/src/Room.js +++ b/app/src/Room.js @@ -47,6 +47,7 @@ module.exports = class Room { screen_cant_share: false, chat_cant_privately: false, chat_cant_chatgpt: false, + media_cant_sharing: false, }; this.survey = config.survey; this.redirect = config.redirect; @@ -65,6 +66,11 @@ module.exports = class Room { // Polls this.polls = []; + + this.isHostProtected = config.host.protected; + + // Share Media + this.shareMediaData = {}; } // #################################################### @@ -87,15 +93,25 @@ module.exports = class Room { fromUrl: this.rtmp && this.rtmp.fromUrl, fromStream: this.rtmp && this.rtmp.fromStream, }, + hostProtected: this.isHostProtected, moderator: this._moderator, survey: this.survey, redirect: this.redirect, videoAIEnabled: this.videoAIEnabled, thereIsPolls: this.thereIsPolls(), + shareMediaData: this.shareMediaData, peers: JSON.stringify([...this.peers]), }; } + // ############################################## + // SHARE MEDIA + // ############################################## + + updateShareMedia(data) { + this.shareMediaData = data; + } + // ############################################## // POLLS // ############################################## @@ -449,6 +465,9 @@ module.exports = class Room { case 'chat_cant_chatgpt': this._moderator.chat_cant_chatgpt = data.status; break; + case 'media_cant_sharing': + this._moderator.media_cant_sharing = data.status; + break; default: break; } diff --git a/app/src/Server.js b/app/src/Server.js index 4f19ba51..9c9887df 100644 --- a/app/src/Server.js +++ b/app/src/Server.js @@ -8,14 +8,14 @@ ███████ ███████ ██ ██ ████ ███████ ██ ██ prod dependencies: { - @ffmpeg-installer/ffmpeg: https://www.npmjs.com/package/@ffmpeg-installer/ffmpeg + @mattermost/client : https://www.npmjs.com/package/@mattermost/client @sentry/node : https://www.npmjs.com/package/@sentry/node axios : https://www.npmjs.com/package/axios - body-parser : https://www.npmjs.com/package/body-parser compression : https://www.npmjs.com/package/compression colors : https://www.npmjs.com/package/colors cors : https://www.npmjs.com/package/cors crypto-js : https://www.npmjs.com/package/crypto-js + discord.js : https://www.npmjs.com/package/discord.js dompurify : https://www.npmjs.com/package/dompurify express : https://www.npmjs.com/package/express express-openid-connect : https://www.npmjs.com/package/express-openid-connect @@ -55,7 +55,7 @@ dev dependencies: { * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.5.67 + * @version 1.6.31 * */ @@ -87,6 +87,8 @@ const yaml = require('js-yaml'); const swaggerUi = require('swagger-ui-express'); const swaggerDocument = yaml.load(fs.readFileSync(path.join(__dirname, '/../api/swagger.yaml'), 'utf8')); const Sentry = require('@sentry/node'); +// const Discord = require('./Discord.js'); +// const Mattermost = require('./Mattermost.js'); const restrictAccessByIP = require('./middleware/IpWhitelist.js'); const packageJson = require('../../package.json'); @@ -109,7 +111,6 @@ const CryptoJS = require('crypto-js'); const qS = require('qs'); const slackEnabled = config.slack.enabled; const slackSigningSecret = config.slack.signingSecret; -const bodyParser = require('body-parser'); const app = express(); @@ -153,9 +154,13 @@ const jwtCfg = { const hostCfg = { protected: config.host.protected, user_auth: config.host.user_auth, + users: config.host.users, users_from_db: config.host.users_from_db, + users_api_room_allowed: config.host.users_api_room_allowed, + users_api_rooms_allowed: config.host.users_api_rooms_allowed, users_api_endpoint: config.host.users_api_endpoint, users_api_secret_key: config.host.users_api_secret_key, + api_room_exists: config.host.api_room_exists, users: config.host.users, authenticated: !config.host.protected, }; @@ -190,6 +195,14 @@ if (sentryEnabled) { */ } +// Discord Bot +const { enabled, commands, token } = config.discord || {}; + +if (enabled && commands.length > 0 && token) { + const discordBot = new Discord(token, commands); + log.info('Discord bot is enabled and starting'); +} + // Stats const defaultStats = { enabled: true, @@ -244,6 +257,7 @@ const views = { donate: path.join(__dirname, '../../', 'public/views/support.html'), // legacy so keep this line faq: path.join(__dirname, '../../', 'public/views/faq.html'), presskit: path.join(__dirname, '../../', 'public/views/presskit.html'), + whoAreYou: path.join(__dirname, '../../', 'public/views/whoAreYou.html'), }; const authHost = new Host(); // Authenticated IP by Login @@ -316,7 +330,6 @@ function OIDCAuth(req, res, next) { log.debug('[OIDC] ------> Host protected', { authenticated: hostCfg.authenticated, authorizedIPs: authHost.getAuthorizedIPs(), - activeRoom: authHost.isRoomActive(), }); } next(); @@ -351,12 +364,12 @@ function getMeetCount(roomList) { function startServer() { // Start the app + app.use(express.static(dir.public)); app.use(cors(corsOptions)); app.use(compression()); - app.use(express.json({ limit: '50mb' })); // Ensure the body parser can handle large files - app.use(express.static(dir.public)); - app.use(bodyParser.urlencoded({ extended: true })); - app.use(bodyParser.raw({ type: 'video/webm', limit: '50mb' })); // handle raw binary data + app.use(express.json({ limit: '50mb' })); // Handles JSON payloads + app.use(express.urlencoded({ extended: true, limit: '50mb' })); // Handles URL-encoded payloads + app.use(express.raw({ type: 'video/webm', limit: '50mb' })); // Handles raw binary data app.use(restApi.basePath + '/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); // api docs // IP Whitelist check ... @@ -404,6 +417,9 @@ function startServer() { }); */ + // Mattermost + // const mattermost = new Mattermost(app); + // POST start from here... app.post('*', function (next) { next(); @@ -487,9 +503,22 @@ function startServer() { // Route to display user information app.get('/profile', OIDCAuth, (req, res) => { if (OIDC.enabled) { - return res.json(req.oidc.user); // Send user information as JSON + const user = { ...req.oidc.user }; + user.peer_name = { + force: OIDC.peer_name?.force || false, + email: OIDC.peer_name?.email || false, + name: OIDC.peer_name?.name || false, + }; + log.debug('OIDC get Profile', user); + return res.json(user); } - res.sendFile(views.notFound); + // OIDC disabled + res.status(201).json({ + email: false, + name: false, + peer_name: false, + message: 'Profile not found because OIDC is disabled', + }); }); // Authentication Callback Route @@ -511,7 +540,6 @@ function startServer() { log.debug('[OIDC] ------> Logout', { authenticated: hostCfg.authenticated, authorizedIPs: authHost.getAuthorizedIPs(), - activeRoom: authHost.isRoomActive(), }); } req.logout(); // Logout user @@ -532,7 +560,8 @@ function startServer() { // main page app.get(['/'], OIDCAuth, (req, res) => { //log.debug('/ - hostCfg ----->', hostCfg); - if ((!OIDC.enabled && hostCfg.protected && !hostCfg.authenticated) || authHost.isRoomActive()) { + + if (!OIDC.enabled && hostCfg.protected) { const ip = getIP(req); if (allowedIP(ip)) { //res.sendFile(views.landing); @@ -540,7 +569,7 @@ function startServer() { res.redirect('/active'); } else { hostCfg.authenticated = false; - res.sendFile(views.login); + res.redirect('/login'); } } else { res.redirect('/active'); @@ -560,20 +589,33 @@ function startServer() { app.get(['/newroom'], OIDCAuth, (req, res) => { //log.info('/newroom - hostCfg ----->', hostCfg); - if ((!OIDC.enabled && hostCfg.protected && !hostCfg.authenticated) || authHost.isRoomActive()) { + if (!OIDC.enabled && hostCfg.protected) { const ip = getIP(req); if (allowedIP(ip)) { - res.sendFile(views.newRoom); + res.redirect('/'); hostCfg.authenticated = true; } else { hostCfg.authenticated = false; - res.sendFile(views.login); + res.redirect('/login'); } } else { res.sendFile(views.newRoom); } }); + // Check if room active (exists) + app.post(['/isRoomActive'], (req, res) => { + const { roomId } = checkXSS(req.body); + + if (roomId && (hostCfg.protected || hostCfg.user_auth)) { + const roomActive = roomList.has(roomId); + if (roomActive) log.debug('isRoomActive', { roomId, roomActive }); + res.status(200).json({ message: roomActive }); + } else { + res.status(400).json({ message: 'Unauthorized' }); + } + }); + // Handle Direct join room with params app.get('/join/', async (req, res) => { if (Object.keys(req.query).length > 0) { @@ -615,9 +657,14 @@ function startServer() { isPeerPresenter = presenter === '1' || presenter === 'true'; if (isPeerPresenter && !hostCfg.users_from_db) { - const roomAllowedForUser = isRoomAllowedForUser('Direct Join with token', username, room); + const roomAllowedForUser = await isRoomAllowedForUser('Direct Join with token', username, room); if (!roomAllowedForUser) { - return res.status(401).json({ message: 'Direct Room Join for this User is Unauthorized' }); + log.warn('Direct Room Join for this User is Unauthorized', { + username: username, + room: room, + }); + return res.redirect('/whoAreYou/' + room); + //return res.status(401).json({ message: 'Direct Room Join for this User is Unauthorized' }); } } } catch (err) { @@ -627,10 +674,12 @@ function startServer() { : res.sendFile(views.newRoom); } } else { - const allowRoomAccess = isAllowedRoomAccess('/join/params', req, hostCfg, authHost, roomList, room); - const roomAllowedForUser = isRoomAllowedForUser('Direct Join with token', name, room); + const allowRoomAccess = isAllowedRoomAccess('/join/params', req, hostCfg, roomList, room); + const roomAllowedForUser = await isRoomAllowedForUser('Direct Join without token', name, room); if (!allowRoomAccess && !roomAllowedForUser) { - return res.status(401).json({ message: 'Direct Room Join Unauthorized' }); + log.warn('Direct Room Join Unauthorized', room); + return res.redirect('/whoAreYou/' + room); + //return res.status(401).json({ message: 'Direct Room Join Unauthorized' }); } } @@ -659,26 +708,41 @@ function startServer() { }); // join room by id - app.get('/join/:roomId', (req, res) => { + app.get('/join/:roomId', async (req, res) => { // - const roomId = req.params.roomId; + const { roomId } = req.params; + + if (!roomId) { + log.warn('/join/:roomId empty', roomId); + return res.redirect('/'); + } if (!Validator.isValidRoomName(roomId)) { log.warn('/join/:roomId invalid', roomId); return res.redirect('/'); } - const allowRoomAccess = isAllowedRoomAccess('/join/:roomId', req, hostCfg, authHost, roomList, roomId); + const allowRoomAccess = isAllowedRoomAccess('/join/:roomId', req, hostCfg, roomList, roomId); if (allowRoomAccess) { - if (hostCfg.protected) authHost.setRoomActive(); - + // 1. Protect room access with database check + if (!OIDC.enabled && hostCfg.protected && hostCfg.users_from_db) { + const roomExists = await roomExistsForUser(roomId); + log.debug('/join/:roomId exists from API endpoint', roomExists); + return roomExists ? res.sendFile(views.room) : res.redirect('/login'); + } + // 2. Protect room access with configuration check + if (!OIDC.enabled && hostCfg.protected && !hostCfg.users_from_db) { + const roomExists = hostCfg.users.some( + (user) => user.allowed_rooms && user.allowed_rooms.includes(roomId), + ); + log.debug('/join/:roomId exists from config allowed rooms', roomExists); + return roomExists ? res.sendFile(views.room) : res.redirect('/whoAreYou/' + roomId); + } res.sendFile(views.room); } else { - if (!OIDC.enabled && hostCfg.protected) { - return res.sendFile(views.login); - } - res.redirect('/'); + // Who are you? + !OIDC.enabled && hostCfg.protected ? res.redirect('/whoAreYou/' + roomId) : res.redirect('/'); } }); @@ -729,6 +793,11 @@ function startServer() { res.send(stats); }); + // handle who are you: Presenter or Guest + app.get(['/whoAreYou/:roomId'], (req, res) => { + res.sendFile(views.whoAreYou); + }); + // handle login if user_auth enabled app.get(['/login'], (req, res) => { res.sendFile(views.login); @@ -738,11 +807,11 @@ function startServer() { app.get(['/logged'], (req, res) => { const ip = getIP(req); if (allowedIP(ip)) { - res.sendFile(views.newRoom); + res.redirect('/'); hostCfg.authenticated = true; } else { hostCfg.authenticated = false; - res.sendFile(views.login); + res.redirect('/login'); } }); @@ -777,7 +846,9 @@ function startServer() { config.presenters.list.includes(username).toString(); const token = encodeToken({ username: username, password: password, presenter: isPresenter }); - return res.status(200).json({ message: token }); + const allowedRooms = await getUserAllowedRooms(username, password); + + return res.status(200).json({ message: token, allowedRooms: allowedRooms }); } if (isPeerValid) { @@ -785,7 +856,8 @@ function startServer() { const isPresenter = config.presenters && config.presenters.list && config.presenters.list.includes(username).toString(); const token = encodeToken({ username: username, password: password, presenter: isPresenter }); - return res.status(200).json({ message: token }); + const allowedRooms = await getUserAllowedRooms(username, password); + return res.status(200).json({ message: token, allowedRooms: allowedRooms }); } else { return res.status(401).json({ message: 'unauthorized' }); } @@ -1204,29 +1276,53 @@ function startServer() { function getServerConfig(tunnel = false) { return { - app_version: packageJson.version, - node_version: process.versions.node, - cors_options: corsOptions, - middleware: config.middleware, + // General Server Information server_listen: host, server_tunnel: tunnel, - hostConfig: hostCfg, + + // Core Configurations + cors_options: corsOptions, jwtCfg: jwtCfg, - presenters: config.presenters, rest_api: restApi, + + // Middleware and UI + middleware: config.middleware, + configUI: config.ui, + + // Security, Authorization, and User Management + oidc: OIDC.enabled ? OIDC : false, + hostProtected: hostCfg.protected || hostCfg.user_auth ? hostCfg : false, + ip_lookup_enabled: config.IPLookup?.enabled ? config.IPLookup : false, + presenters: config.presenters, + + // Communication Integrations + discord_enabled: config.discord?.enabled ? config.discord : false, + mattermost_enabled: config.mattermost?.enabled ? config.mattermost : false, + slack_enabled: slackEnabled ? config.slack : false, + chatGPT_enabled: config.chatGPT?.enabled ? config.chatGPT : false, + + // Media and Video Configurations + mediasoup_listenInfos: config.mediasoup.webRtcTransport.listenInfos, mediasoup_worker_bin: mediasoup.workerBin, + rtmp_enabled: rtmpCfg.enabled ? rtmpCfg : false, + videAI_enabled: config.videoAI.enabled ? config.videoAI : false, + serverRec: config?.server?.recording, + + // Centralized Logging + sentry_enabled: sentryEnabled ? config.sentry : false, + + // Additional Configurations and Features + survey_enabled: config.survey?.enabled ? config.survey : false, + redirect_enabled: config.redirect?.enabled ? config.redirect : false, + stats_enabled: config.stats?.enabled ? config.stats : false, + ngrok_enabled: config.ngrok?.enabled ? config.ngrok : false, + email_alerts: config.email?.alert ? config.email : false, + + // Version Information + app_version: packageJson.version, + node_version: process.versions.node, mediasoup_server_version: mediasoup.version, mediasoup_client_version: mediasoupClient.version, - mediasoup_listenInfos: config.mediasoup.webRtcTransport.listenInfos, - ip_lookup_enabled: config.IPLookup.enabled, - sentry_enabled: sentryEnabled, - redirect_enabled: config.redirect.enabled, - slack_enabled: slackEnabled, - stats_enabled: config.stats.enabled, - chatGPT_enabled: config.chatGPT.enabled, - configUI: config.ui, - serverRec: config?.server?.recording, - oidc: OIDC.enabled ? OIDC : false, }; } @@ -1390,7 +1486,7 @@ function startServer() { }); socket.on('join', async (dataObject, cb) => { - if (!roomList.has(socket.room_id)) { + if (!roomExists(socket)) { return cb({ error: 'Room does not exist', }); @@ -1414,7 +1510,7 @@ function startServer() { return cb('invalid'); } - const room = roomList.get(socket.room_id); + const room = getRoom(socket); const { peer_name, peer_id, peer_uuid, peer_token, os_name, os_version, browser_name, browser_version } = data.peer_info; @@ -1429,6 +1525,7 @@ function startServer() { const validToken = await isValidToken(peer_token); if (!validToken) { + log.warn('[Join] - Invalid token', peer_token); return cb('unauthorized'); } @@ -1438,6 +1535,7 @@ function startServer() { if (!isPeerValid) { // redirect peer to login page + log.warn('[Join] - Invalid peer not authenticated', isPeerValid); return cb('unauthorized'); } @@ -1461,12 +1559,13 @@ function startServer() { return cb('unauthorized'); } } else { - return cb('unauthorized'); + if (!hostCfg.users_from_db) return cb('unauthorized'); } if (!hostCfg.users_from_db) { const roomAllowedForUser = isRoomAllowedForUser('[Join]', peer_name, room.id); if (!roomAllowedForUser) { + log.warn('[Join] - Room not allowed for this peer', { peer_name, room_id: room.id }); return cb('notAllowed'); } } @@ -1553,6 +1652,7 @@ function startServer() { if ((hostCfg.protected || hostCfg.user_auth) && isPresenter && !hostCfg.users_from_db) { const roomAllowedForUser = isRoomAllowedForUser('[Join]', peer_name, room.id); if (!roomAllowedForUser) { + log.warn('[Join] - Room not allowed for this peer', { peer_name, room_id: room.id }); return cb('notAllowed'); } } @@ -1572,13 +1672,16 @@ function startServer() { }); socket.on('getRouterRtpCapabilities', (_, callback) => { - if (!roomList.has(socket.room_id)) { + if (!roomExists(socket)) { return callback({ error: 'Room not found' }); } - const room = roomList.get(socket.room_id); + const { room, peer } = getRoomAndPeer(socket); + + const { peer_name } = peer || 'undefined'; + + log.debug('Get RouterRtpCapabilities', peer_name); - log.debug('Get RouterRtpCapabilities', getPeerName(room)); try { const getRouterRtpCapabilities = room.getRtpCapabilities(); @@ -1594,13 +1697,15 @@ function startServer() { }); socket.on('createWebRtcTransport', async (_, callback) => { - if (!roomList.has(socket.room_id)) { + if (!roomExists(socket)) { return callback({ error: 'Room not found' }); } - const room = roomList.get(socket.room_id); + const { room, peer } = getRoomAndPeer(socket); + + const { peer_name } = peer || 'undefined'; - log.debug('Create WebRtc transport', getPeerName(room)); + log.debug('Create WebRtc transport', peer_name); try { const createWebRtcTransport = await room.createWebRtcTransport(socket.id); @@ -1617,13 +1722,13 @@ function startServer() { }); socket.on('connectTransport', async ({ transport_id, dtlsParameters }, callback) => { - if (!roomList.has(socket.room_id)) { + if (!roomExists(socket)) { return callback({ error: 'Room not found' }); } - const room = roomList.get(socket.room_id); + const { room, peer } = getRoomAndPeer(socket); - const peer_name = getPeerName(room, false); + const { peer_name } = peer || 'undefined'; log.debug('Connect transport', { peer_name: peer_name, transport_id: transport_id }); @@ -1642,15 +1747,17 @@ function startServer() { }); socket.on('restartIce', async ({ transport_id }, callback) => { - if (!roomList.has(socket.room_id)) { + if (!roomExists(socket)) { return callback({ error: 'Room not found' }); } - const room = roomList.get(socket.room_id); + const peer = getPeer(socket); - const peer = room.getPeer(socket.id); + if (!peer) { + return callback({ error: 'Peer not found' }); + } - const peer_name = getPeerName(room, false); + const { peer_name } = peer || 'undefined'; log.debug('Restart ICE', { peer_name: peer_name, transport_id: transport_id }); @@ -1675,13 +1782,17 @@ function startServer() { }); socket.on('produce', async ({ producerTransportId, kind, appData, rtpParameters }, callback, errback) => { - if (!roomList.has(socket.room_id)) { + if (!roomExists(socket)) { return callback({ error: 'Room not found' }); } - const room = roomList.get(socket.room_id); + const { room, peer } = getRoomAndPeer(socket); + + if (!peer) { + return callback({ error: 'Peer not found' }); + } - const peer_name = getPeerName(room, false); + const { peer_name } = peer || 'undefined'; // peer_info.audio OR video ON const data = { @@ -1693,8 +1804,6 @@ function startServer() { status: true, }; - const peer = room.getPeer(socket.id); - peer.updatePeerInfo(data); try { @@ -1734,13 +1843,13 @@ function startServer() { }); socket.on('consume', async ({ consumerTransportId, producerId, rtpCapabilities }, callback) => { - if (!roomList.has(socket.room_id)) { + if (!roomExists(socket)) { return callback({ error: 'Room not found' }); } - const room = roomList.get(socket.room_id); + const { room, peer } = getRoomAndPeer(socket); - const peer_name = getPeerName(room, false); + const { peer_name } = peer || 'undefined'; try { const params = await room.consume(socket.id, consumerTransportId, producerId, rtpCapabilities); @@ -1763,11 +1872,11 @@ function startServer() { }); socket.on('producerClosed', (data) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; - const room = roomList.get(socket.room_id); + const { room, peer } = getRoomAndPeer(socket); - const peer = room.getPeer(socket.id); + if (!peer) return; peer.updatePeerInfo(data); // peer_info.audio OR video OFF @@ -1775,24 +1884,20 @@ function startServer() { }); socket.on('pauseProducer', async ({ producer_id }, callback) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; - const room = roomList.get(socket.room_id); - - const peer_name = getPeerName(room, false); - - const peer = room.getPeer(socket.id); + const peer = getPeer(socket); if (!peer) { return callback({ - error: `peer with ID: ${socket.id} for producer with id "${producer_id}" not found`, + error: `Peer with ID: ${socket.id} for producer with id "${producer_id}" not found`, }); } const producer = peer.getProducer(producer_id); if (!producer) { - return callback({ error: `producer with id "${producer_id}" not found` }); + return callback({ error: `Producer with id "${producer_id}" not found` }); } try { @@ -1801,19 +1906,17 @@ function startServer() { return callback({ error: error.message }); } + const { peer_name } = peer || 'undefined'; + log.debug('Producer paused', { peer_name: peer_name, producer_id: producer_id }); callback('successfully'); }); socket.on('resumeProducer', async ({ producer_id }, callback) => { - if (!roomList.has(socket.room_id)) return; - - const room = roomList.get(socket.room_id); + if (!roomExists(socket)) return; - const peer_name = getPeerName(room, false); - - const peer = room.getPeer(socket.id); + const peer = getPeer(socket); if (!peer) { return callback({ @@ -1833,19 +1936,17 @@ function startServer() { return callback({ error: error.message }); } + const { peer_name } = peer || 'undefined'; + log.debug('Producer resumed', { peer_name: peer_name, producer_id: producer_id }); callback('successfully'); }); socket.on('resumeConsumer', async ({ consumer_id }, callback) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; - const room = roomList.get(socket.room_id); - - const peer_name = getPeerName(room, false); - - const peer = room.getPeer(socket.id); + const peer = getPeer(socket); if (!peer) { return callback({ @@ -1865,17 +1966,21 @@ function startServer() { return callback({ error: error.message }); } + const { peer_name } = peer || 'undefined'; + log.debug('Consumer resumed', { peer_name: peer_name, consumer_id: consumer_id }); callback('successfully'); }); socket.on('getProducers', () => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; - const room = roomList.get(socket.room_id); + const { room, peer } = getRoomAndPeer(socket); - log.debug('Get producers', getPeerName(room)); + const { peer_name } = peer || 'undefined'; + + log.debug('Get producers', peer_name); // send all the current producer to newly joined member const producerList = room.getProducerListForPeer(); @@ -1884,9 +1989,9 @@ function startServer() { }); socket.on('getPeerCounts', async ({}, callback) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; - const room = roomList.get(socket.room_id); + const room = getRoom(socket); const peerCounts = room.getPeersCount(); @@ -1896,17 +2001,18 @@ function startServer() { }); socket.on('cmd', async (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); log.debug('cmd', data); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); + + const peer = getPeer(socket); switch (data.type) { case 'privacy': - const peer = room.getPeer(socket.id); peer.updatePeerInfo({ type: data.type, status: data.active }); break; case 'ejectAll': @@ -1914,6 +2020,12 @@ function startServer() { const isPresenter = await isPeerPresenter(socket.room_id, socket.id, peer_name, peer_uuid); if (!isPresenter) return; break; + case 'peerAudio': + // Keep producer volume to update consumer on join room... + if (data.audioProducerId) { + peer.updatePeerInfo({ type: data.type, volume: data.volume * 100 }); + } + break; default: break; //... @@ -1923,13 +2035,13 @@ function startServer() { }); socket.on('roomAction', async (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); const isPresenter = await isPeerPresenter(socket.room_id, socket.id, data.peer_name, data.peer_uuid); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); log.debug('Room action:', data); @@ -1998,11 +2110,11 @@ function startServer() { }); socket.on('roomLobby', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); data.room = room.toJson(); @@ -2024,7 +2136,7 @@ function startServer() { }); socket.on('peerAction', async (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); @@ -2052,7 +2164,7 @@ function startServer() { if (!isPresenter) return; } - const room = roomList.get(socket.room_id); + const room = getRoom(socket); if (data.action === 'ban') room.addBannedPeer(data.to_peer_uuid); @@ -2062,16 +2174,14 @@ function startServer() { }); socket.on('updatePeerInfo', (dataObject) => { - if (!roomList.has(socket.room_id)) return; - - const data = checkXSS(dataObject); + if (!roomExists(socket)) return; - const room = roomList.get(socket.room_id); - - const peer = room.getPeer(socket.id); + const { room, peer } = getRoomAndPeer(socket); if (!peer) return; + const data = checkXSS(dataObject); + peer.updatePeerInfo(data); if (data.broadcast) { @@ -2081,11 +2191,11 @@ function startServer() { }); socket.on('updateRoomModerator', async (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); const isPresenter = await isPeerPresenter(socket.room_id, socket.id, data.peer_name, data.peer_uuid); @@ -2101,6 +2211,7 @@ function startServer() { case 'screen_cant_share': case 'chat_cant_privately': case 'chat_cant_chatgpt': + case 'media_cant_sharing': room.broadCast(socket.id, 'updateRoomModerator', moderator); break; default: @@ -2109,11 +2220,11 @@ function startServer() { }); socket.on('updateRoomModeratorALL', async (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); const isPresenter = await isPeerPresenter(socket.room_id, socket.id, data.peer_name, data.peer_uuid); @@ -2127,17 +2238,19 @@ function startServer() { }); socket.on('getRoomInfo', async (_, cb) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; + + const { room, peer } = getRoomAndPeer(socket); - const room = roomList.get(socket.room_id); + const { peer_name } = peer || 'undefined'; - log.debug('Send Room Info to', getPeerName(room)); + log.debug('Send Room Info to', peer_name); cb(room.toJson()); }); socket.on('fileInfo', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); @@ -2148,37 +2261,41 @@ function startServer() { log.debug('Send File Info', data); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); data.broadcast ? room.broadCast(socket.id, 'fileInfo', data) : room.sendTo(data.peer_id, 'fileInfo', data); }); socket.on('file', (data) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; - const room = roomList.get(socket.room_id); + const room = getRoom(socket); data.broadcast ? room.broadCast(socket.id, 'file', data) : room.sendTo(data.peer_id, 'file', data); }); socket.on('fileAbort', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); - roomList.get(socket.room_id).broadCast(socket.id, 'fileAbort', data); + const room = getRoom(socket); + + room.broadCast(socket.id, 'fileAbort', data); }); socket.on('receiveFileAbort', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); - roomList.get(socket.room_id).broadCast(socket.id, 'receiveFileAbort', data); + const room = getRoom(socket); + + room.broadCast(socket.id, 'receiveFileAbort', data); }); socket.on('shareVideoAction', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); @@ -2189,7 +2306,9 @@ function startServer() { log.debug('Share video: ', data); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); + + room.updateShareMedia(data); data.peer_id == 'all' ? room.broadCast(socket.id, 'shareVideoAction', data) @@ -2197,11 +2316,11 @@ function startServer() { }); socket.on('wbCanvasToJson', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); // const objLength = bytesToSize(Object.keys(data).length); @@ -2211,44 +2330,44 @@ function startServer() { }); socket.on('whiteboardAction', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); log.debug('Whiteboard', data); room.broadCast(socket.id, 'whiteboardAction', data); }); socket.on('setVideoOff', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); log.debug('Video off data', data.peer_name); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); room.broadCast(socket.id, 'setVideoOff', data); }); socket.on('recordingAction', async (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); log.debug('Recording action', data); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); room.broadCast(socket.id, 'recordingAction', data); }); socket.on('refreshParticipantsCount', () => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; - const room = roomList.get(socket.room_id); + const room = getRoom(socket); const peerCounts = room.getPeers().size; @@ -2261,18 +2380,19 @@ function startServer() { }); socket.on('message', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); - const room = roomList.get(socket.room_id); + const { room, peer } = getRoomAndPeer(socket); + + const { peer_name } = peer || 'undefined'; - // check if the message coming from real peer - const realPeer = isRealPeer(data.peer_name, socket.id, socket.room_id); + const realPeer = data.peer_name === peer_name; if (!realPeer) { - const peer_name = getPeerName(room, false); - log.debug('Fake message detected', { + log.warn('Fake message detected', { + ip: getIpSocket(socket), realFrom: peer_name, fakeFrom: data.peer_name, msg: data.peer_msg, @@ -2288,8 +2408,10 @@ function startServer() { }); socket.on('getChatGPT', async ({ time, room, name, prompt, context }, cb) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; + if (!config.chatGPT.enabled) return cb({ message: 'ChatGPT seems disabled, try later!' }); + // https://platform.openai.com/docs/api-reference/completions/create try { // Add the prompt to the context @@ -2332,11 +2454,11 @@ function startServer() { } }); - // https://docs.heygen.com/reference/overview-copy - + // https://docs.heygen.com/reference/avatar-list socket.on('getAvatarList', async ({}, cb) => { if (!config.videoAI.enabled || !config.videoAI.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); + try { const response = await axios.get(`${config.videoAI.basePath}/v1/avatar.list`, { headers: { @@ -2356,9 +2478,11 @@ function startServer() { } }); + // https://docs.heygen.com/reference/get-voices socket.on('getVoiceList', async ({}, cb) => { if (!config.videoAI.enabled || !config.videoAI.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); + try { const response = await axios.get(`${config.videoAI.basePath}/v1/voice.list`, { headers: { @@ -2378,30 +2502,30 @@ function startServer() { } }); - socket.on('streamingNew', async ({ quality, avatar_name, voice_id }, cb) => { - if (!roomList.has(socket.room_id)) return; + // https://docs.heygen.com/reference/new-session + socket.on('streamingNew', async ({ quality, avatar_id, voice_id }, cb) => { + if (!roomExists(socket)) return; + if (!config.videoAI.enabled || !config.videoAI.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); try { + const voice = voice_id ? { voice_id: voice_id } : {}; const response = await axios.post( `${config.videoAI.basePath}/v1/streaming.new`, { quality, - avatar_name, - voice: { - voice_id: voice_id, - }, + avatar_id, + voice: voice, }, { headers: { - 'Content-Type': 'application/json', - 'X-Api-Key': config.videoAI.apiKey, + accept: 'application/json', + 'content-type': 'application/json', + 'x-api-key': config.videoAI.apiKey, }, }, ); - log.warn('STREAMING NEW', response); - const data = { response: response.data }; log.debug('streamingNew', data); @@ -2413,8 +2537,10 @@ function startServer() { } }); + // https://docs.heygen.com/reference/start-session socket.on('streamingStart', async ({ session_id, sdp }, cb) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; + if (!config.videoAI.enabled || !config.videoAI.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); @@ -2441,8 +2567,10 @@ function startServer() { } }); + // https://docs.heygen.com/reference/submit-ice-information socket.on('streamingICE', async ({ session_id, candidate }, cb) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; + if (!config.videoAI.enabled || !config.videoAI.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); @@ -2469,10 +2597,13 @@ function startServer() { } }); + // https://docs.heygen.com/reference/send-task socket.on('streamingTask', async ({ session_id, text }, cb) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; + if (!config.videoAI.enabled || !config.videoAI.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); + try { const response = await axios.post( `${config.videoAI.basePath}/v1/streaming.task`, @@ -2499,8 +2630,41 @@ function startServer() { } }); + // https://docs.heygen.com/reference/interrupt-task + socket.on('streamingInterrupt', async ({ session_id, text }, cb) => { + if (!roomExists(socket)) return; + + if (!config.videoAI.enabled || !config.videoAI.apiKey) + return cb({ error: 'Video AI seems disabled, try later!' }); + + try { + const response = await axios.post( + `${config.videoAI.basePath}/v1/streaming.interrupt`, + { + session_id, + }, + { + headers: { + 'Content-Type': 'application/json', + 'X-Api-Key': config.videoAI.apiKey, + }, + }, + ); + + const data = { response: response.data }; + + log.debug('streamingInterrupt', data); + + cb(data); + } catch (error) { + log.error('streamingInterrupt', error.response.data); + cb({ error: error.response?.status === 500 ? 'Internal server error' : error.response.data.message }); + } + }); + socket.on('talkToOpenAI', async ({ text, context }, cb) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; + if (!config.videoAI.enabled || !config.videoAI.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); try { @@ -2525,8 +2689,10 @@ function startServer() { } }); + // https://docs.heygen.com/reference/close-session socket.on('streamingStop', async ({ session_id }, cb) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; + if (!config.videoAI.enabled || !config.videoAI.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); try { @@ -2555,14 +2721,17 @@ function startServer() { }); socket.on('getRTMP', async ({}, cb) => { - if (!roomList.has(socket.room_id)) return; - const room = roomList.get(socket.room_id); + if (!roomExists(socket)) return; + + const room = getRoom(socket); + const rtmpFiles = await room.getRTMP(rtmpDir); + cb(rtmpFiles); }); socket.on('startRTMP', async (dataObject, cb) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; if (rtmpCfg && rtmpFileStreamsCount >= rtmpCfg.maxStreams) { log.warn('RTMP max file streams reached', rtmpFileStreamsCount); @@ -2574,35 +2743,39 @@ function startServer() { const isPresenter = await isPeerPresenter(socket.room_id, socket.id, peer_name, peer_uuid); if (!isPresenter) return cb(false); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); const host = config.ngrok.enabled ? 'localhost' : socket.handshake.headers.host.split(':')[0]; const rtmp = await room.startRTMP(socket.id, room, host, 1935, `../${rtmpDir}/${file}`); if (rtmp !== false) rtmpFileStreamsCount++; + log.debug('startRTMP - rtmpFileStreamsCount ---->', rtmpFileStreamsCount); cb(rtmp); }); socket.on('stopRTMP', async () => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; - const room = roomList.get(socket.room_id); + const room = getRoom(socket); rtmpFileStreamsCount--; + log.debug('stopRTMP - rtmpFileStreamsCount ---->', rtmpFileStreamsCount); await room.stopRTMP(); }); socket.on('endOrErrorRTMP', async () => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; + rtmpFileStreamsCount--; + log.debug('endRTMP - rtmpFileStreamsCount ---->', rtmpFileStreamsCount); }); socket.on('startRTMPfromURL', async (dataObject, cb) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; if (rtmpCfg && rtmpUrlStreamsCount >= rtmpCfg.maxStreams) { log.warn('RTMP max Url streams reached', rtmpUrlStreamsCount); @@ -2614,41 +2787,45 @@ function startServer() { const isPresenter = await isPeerPresenter(socket.room_id, socket.id, peer_name, peer_uuid); if (!isPresenter) return cb(false); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); const host = config.ngrok.enabled ? 'localhost' : socket.handshake.headers.host.split(':')[0]; const rtmp = await room.startRTMPfromURL(socket.id, room, host, 1935, inputVideoURL); if (rtmp !== false) rtmpUrlStreamsCount++; + log.debug('startRTMPfromURL - rtmpUrlStreamsCount ---->', rtmpUrlStreamsCount); cb(rtmp); }); socket.on('stopRTMPfromURL', async () => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; - const room = roomList.get(socket.room_id); + const room = getRoom(socket); rtmpUrlStreamsCount--; + log.debug('stopRTMPfromURL - rtmpUrlStreamsCount ---->', rtmpUrlStreamsCount); await room.stopRTMPfromURL(); }); socket.on('endOrErrorRTMPfromURL', async () => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; + rtmpUrlStreamsCount--; + log.debug('endRTMPfromURL - rtmpUrlStreamsCount ---->', rtmpUrlStreamsCount); }); socket.on('createPoll', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); const { question, options } = data; - const room = roomList.get(socket.room_id); + const room = getRoom(socket); const newPoll = { question: question, @@ -2664,17 +2841,18 @@ function startServer() { }); socket.on('vote', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); - const room = roomList.get(socket.room_id); + const { room, peer } = getRoomAndPeer(socket); + + const { peer_name } = peer || socket.id; const roomPolls = room.getPolls(); const poll = roomPolls[data.pollIndex]; if (poll) { - const peer_name = getPeerName(room, false) || socket.id; poll.voters.set(peer_name, data.option); room.sendToAll('updatePolls', room.convertPolls(roomPolls)); log.debug('[Poll] vote', roomPolls); @@ -2682,9 +2860,9 @@ function startServer() { }); socket.on('updatePoll', () => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; - const room = roomList.get(socket.room_id); + const room = getRoom(socket); const roomPolls = room.getPolls(); @@ -2695,13 +2873,13 @@ function startServer() { }); socket.on('editPoll', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); const { index, question, options } = data; - const room = roomList.get(socket.room_id); + const room = getRoom(socket); const roomPolls = room.getPolls(); @@ -2714,7 +2892,7 @@ function startServer() { }); socket.on('deletePoll', async (data) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const { index, peer_name, peer_uuid } = checkXSS(data); @@ -2722,7 +2900,7 @@ function startServer() { // const isPresenter = await isPeerPresenter(socket.room_id, socket.id, peer_name, peer_uuid); // if (!isPresenter) return; - const room = roomList.get(socket.room_id); + const room = getRoom(socket); const roomPolls = room.getPolls(); @@ -2736,22 +2914,22 @@ function startServer() { // Room collaborative editor socket.on('editorChange', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; //const data = checkXSS(dataObject); const data = dataObject; - const room = roomList.get(socket.room_id); + const room = getRoom(socket); room.broadCast(socket.id, 'editorChange', data); }); socket.on('editorActions', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; const data = checkXSS(dataObject); - const room = roomList.get(socket.room_id); + const room = getRoom(socket); log.debug('editorActions', data); @@ -2759,22 +2937,20 @@ function startServer() { }); socket.on('editorUpdate', (dataObject) => { - if (!roomList.has(socket.room_id)) return; + if (!roomExists(socket)) return; //const data = checkXSS(dataObject); const data = dataObject; - const room = roomList.get(socket.room_id); + const room = getRoom(socket); room.broadCast(socket.id, 'editorUpdate', data); }); socket.on('disconnect', async () => { - if (!roomList.has(socket.room_id)) return; - - const room = roomList.get(socket.room_id); + if (!roomExists(socket)) return; - const peer = room.getPeer(socket.id); + const { room, peer } = getRoomAndPeer(socket); const { peer_name, peer_uuid } = peer || {}; @@ -2811,15 +2987,13 @@ function startServer() { }); socket.on('exitRoom', async (_, callback) => { - if (!roomList.has(socket.room_id)) { + if (!roomExists(socket)) { return callback({ error: 'Not currently in a room', }); } - const room = roomList.get(socket.room_id); - - const peer = room.getPeer(socket.id); + const { room, peer } = getRoomAndPeer(socket); const { peer_name, peer_uuid } = peer || {}; @@ -2858,33 +3032,27 @@ function startServer() { }); // common - function getPeerName(room, json = true) { - try { - const DEFAULT_PEER_NAME = 'undefined'; - const peer = room.getPeer(socket.id); - const peerName = peer.peer_name || DEFAULT_PEER_NAME; - if (json) { - return { peer_name: peerName }; - } - return peerName; - } catch (err) { - log.error('getPeerName', err); - return json ? { peer_name: DEFAULT_PEER_NAME } : DEFAULT_PEER_NAME; - } - } - function isRealPeer(name, id, roomId) { - if (!roomList.has(socket.room_id)) return false; + function getRoomAndPeer(socket) { + const room = getRoom(socket); - const room = roomList.get(roomId); + const peer = getPeer(socket); - const peer = room.getPeer(id); + return { room, peer }; + } - if (!peer) return false; + function getRoom(socket) { + return roomList.get(socket.room_id) || {}; + } - const { peer_name } = peer; + function getPeer(socket) { + const room = getRoom(socket); // Reusing getRoom to retrieve the room - return peer_name == name; + return room.getPeer ? room.getPeer(socket.id) || {} : {}; + } + + function roomExists(socket) { + return roomList.has(socket.room_id); } function isValidFileName(fileName) { @@ -2896,6 +3064,7 @@ function startServer() { const pattern = new RegExp( '^(https?:\\/\\/)?' + // protocol '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + 'localhost|' + // allow localhost '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string @@ -3021,11 +3190,19 @@ function startServer() { async function isAuthPeer(username, password) { if (hostCfg.users_from_db && hostCfg.users_api_endpoint) { try { - const response = await axios.post(hostCfg.users_api_endpoint, { - email: username, - password: password, - api_secret_key: hostCfg.users_api_secret_key, - }); + // Using either email or username, as the username can also be an email here. + const response = await axios.post( + hostCfg.users_api_endpoint, + { + email: username, + username: username, + password: password, + api_secret_key: hostCfg.users_api_secret_key, + }, + { + timeout: 5000, // Timeout set to 5 seconds (5000 milliseconds) + }, + ); return response.data && response.data.message === true; } catch (error) { log.error('AXIOS isAuthPeer error', error.message); @@ -3109,37 +3286,104 @@ function startServer() { return roomPeersArray; } - function isAllowedRoomAccess(logMessage, req, hostCfg, authHost, roomList, roomId) { + function isAllowedRoomAccess(logMessage, req, hostCfg, roomList, roomId) { const OIDCUserAuthenticated = OIDC.enabled && req.oidc.isAuthenticated(); const hostUserAuthenticated = hostCfg.protected && hostCfg.authenticated; - const roomActive = authHost.isRoomActive(); const roomExist = roomList.has(roomId); const roomCount = roomList.size; const allowRoomAccess = (!hostCfg.protected && !OIDC.enabled) || // No host protection and OIDC mode enabled (default) - OIDCUserAuthenticated || // User authenticated via OIDC - hostUserAuthenticated || // User authenticated via Login + (OIDCUserAuthenticated && roomExist) || // User authenticated via OIDC and room Exist + (hostUserAuthenticated && roomExist) || // User authenticated via Login and room Exist ((OIDCUserAuthenticated || hostUserAuthenticated) && roomCount === 0) || // User authenticated joins the first room roomExist; // User Or Guest join an existing Room log.debug(logMessage, { - OIDCUserEnabled: OIDC.enabled, OIDCUserAuthenticated: OIDCUserAuthenticated, hostUserAuthenticated: hostUserAuthenticated, - hostProtected: hostCfg.protected, - hostAuthenticated: hostCfg.authenticated, - roomActive: roomActive, roomExist: roomExist, roomCount: roomCount, - roomId: roomId, + extraInfo: { + roomId: roomId, + OIDCUserEnabled: OIDC.enabled, + hostProtected: hostCfg.protected, + hostAuthenticated: hostCfg.authenticated, + }, allowRoomAccess: allowRoomAccess, }); return allowRoomAccess; } - function isRoomAllowedForUser(message, username, room) { + async function roomExistsForUser(room) { + if (hostCfg.protected || hostCfg.user_auth) { + // Check if passed room exists + if (hostCfg.users_from_db && hostCfg.api_room_exists) { + try { + const response = await axios.post( + hostCfg.api_room_exists, + { + room: room, + api_secret_key: hostCfg.users_api_secret_key, + }, + { + timeout: 5000, // Timeout set to 5 seconds (5000 milliseconds) + }, + ); + log.debug('AXIOS roomExistsForUser', { room: room, exists: true }); + return response.data && response.data.message === true; + } catch (error) { + log.error('AXIOS roomExistsForUser error', error.message); + return false; + } + } + } + } + + async function getUserAllowedRooms(username, password) { + // Gel user allowed rooms from db... + if (hostCfg.protected && hostCfg.users_from_db && hostCfg.users_api_rooms_allowed) { + try { + // Using either email or username, as the username can also be an email here. + const response = await axios.post( + hostCfg.users_api_rooms_allowed, + { + email: username, + username: username, + password: password, + api_secret_key: hostCfg.users_api_secret_key, + }, + { + timeout: 5000, // Timeout set to 5 seconds (5000 milliseconds) + }, + ); + const allowedRooms = response.data ? response.data.message : {}; + log.debug('AXIOS getUserAllowedRooms', allowedRooms); + return allowedRooms; + } catch (error) { + log.error('AXIOS getUserAllowedRooms error', error.message); + return {}; + } + } + + // Get allowed rooms for user from config.js file + if (hostCfg.protected && !hostCfg.users_from_db) { + const isOIDCEnabled = config.oidc && config.oidc.enabled; + + const user = hostCfg.users.find((user) => user.displayname === username || user.username === username); + + if (!isOIDCEnabled && !user) { + log.debug('getUserAllowedRooms - user not found', username); + return false; + } + return user.allowed_rooms; + } + + return ['*']; + } + + async function isRoomAllowedForUser(message, username, room) { const logData = { message, username, room }; log.debug('isRoomAllowedForUser ------>', logData); @@ -3147,6 +3391,30 @@ function startServer() { const isOIDCEnabled = config.oidc && config.oidc.enabled; if (hostCfg.protected || hostCfg.user_auth) { + // Check if allowed room for user from DB... + if (hostCfg.users_from_db && hostCfg.users_api_room_allowed) { + try { + // Using either email or username, as the username can also be an email here. + const response = await axios.post( + hostCfg.users_api_room_allowed, + { + email: username, + username: username, + room: room, + api_secret_key: hostCfg.users_api_secret_key, + }, + { + timeout: 5000, // Timeout set to 5 seconds (5000 milliseconds) + }, + ); + log.debug('AXIOS isRoomAllowedForUser', { room: room, allowed: true }); + return response.data && response.data.message === true; + } catch (error) { + log.error('AXIOS isRoomAllowedForUser error', error.message); + return false; + } + } + const isInPresenterLists = config.presenters.list.includes(username); if (isInPresenterLists) { @@ -3202,12 +3470,10 @@ function startServer() { function allowedIP(ip) { const authorizedIPs = authHost.getAuthorizedIPs(); const authorizedIP = authHost.isAuthorizedIP(ip); - const isRoomActive = authHost.isRoomActive(); log.info('Allowed IPs', { ip: ip, authorizedIP: authorizedIP, authorizedIPs: authorizedIPs, - isRoomActive: isRoomActive, }); return authHost != null && authorizedIP; } @@ -3221,7 +3487,6 @@ function startServer() { log.info('Remove IP from auth', { ip: ip, authorizedIps: authHost.getAuthorizedIPs(), - roomActive: authHost.isRoomActive(), }); } } diff --git a/app/src/XSS.js b/app/src/XSS.js index c734d32b..d2001042 100644 --- a/app/src/XSS.js +++ b/app/src/XSS.js @@ -14,7 +14,7 @@ const log = new Logger('Xss'); // Configure DOMPurify purify.setConfig({ ALLOWED_TAGS: ['a', 'img', 'div', 'span', 'svg', 'g', 'p'], // Allow specific tags - ALLOWED_ATTR: ['href', 'src', 'title', 'id', 'class'], // Allow specific attributes + ALLOWED_ATTR: ['href', 'src', 'title', 'id', 'class', 'target'], // Allow specific attributes ALLOWED_URI_REGEXP: /^(?!data:|javascript:|vbscript:|file:|view-source:).*/, // Disallow dangerous URIs }); diff --git a/app/src/config.template.js b/app/src/config.template.js index 93227f21..4918b798 100644 --- a/app/src/config.template.js +++ b/app/src/config.template.js @@ -78,7 +78,9 @@ module.exports = { - dir: Directory where your video files are stored to be streamed via RTMP. - ffmpeg: Path of the ffmpeg installation on the system (which ffmpeg) - Important: Ensure your RTMP server is operational before proceeding. You can start the server by running the following command: + Important: Before proceeding, make sure your RTMP server is up and running. + For more information, refer to the documentation here: https://docs.mirotalk.com/mirotalk-sfu/rtmp/. + You can start the server by running the following command: - Start: npm run nms-start - Start the RTMP server. - Stop: npm run npm-stop - Stop the RTMP server. - Logs: npm run npm-logs - View the logs of the RTMP server. @@ -119,6 +121,7 @@ module.exports = { join: true, token: false, slack: true, + mattermost: true, //... }, }, @@ -141,6 +144,11 @@ module.exports = { For those seeking an open-source solution, check out: https://github.com/panva/node-oidc-provider */ enabled: false, + peer_name: { + force: true, // Enforce using profile data for peer_name + email: true, // Use email as peer_name + name: false, // Don't use full name (family_name + given_name) + }, config: { issuerBaseURL: 'https://server.example.com', baseURL: `http://localhost:${process.env.PORT ? process.env.PORT : 3010}`, // https://sfu.mirotalk.com @@ -170,8 +178,14 @@ module.exports = { protected: false, user_auth: false, users_from_db: false, // if true ensure that api.token is also set to true. - //users_api_endpoint: 'http://localhost:9000/api/v1/user/isAuth', - users_api_endpoint: 'https://webrtc.mirotalk.com/api/v1/user/isAuth', + users_api_endpoint: 'http://localhost:9000/api/v1/user/isAuth', + users_api_room_allowed: 'http://localhost:9000/api/v1/user/isRoomAllowed', + users_api_rooms_allowed: 'http://localhost:9000/api/v1/user/roomsAllowed', + api_room_exists: 'http://localhost:9000/api/v1/room/exists', + //users_api_endpoint: 'https://webrtc.mirotalk.com/api/v1/user/isAuth', + //users_api_room_allowed: 'https://webrtc.mirotalk.com/api/v1/user/isRoomAllowed', + //users_api_rooms_allowed: 'https://webrtc.mirotalk.com/api/v1/user/roomsAllowed', + //api_room_exists: 'https://webrtc.mirotalk.com//api/v1/room/exists', users_api_secret_key: 'mirotalkweb_default_secret', users: [ { @@ -230,7 +244,7 @@ module.exports = { basePath: 'https://api.heygen.com', apiKey: '', systemLimit: - 'You are a streaming avatar from MiroTalk SFU, an industry-leading product that specialize in videos communications. Audience will try to have a conversation with you, please try answer the questions or respond their comments naturally, and concisely. - please try your best to response with short answers, and only answer the last question.', + 'You are a streaming avatar from MiroTalk SFU, an industry-leading product that specialize in videos communications.', }, email: { /* @@ -265,6 +279,39 @@ module.exports = { DSN: '', tracesSampleRate: 0.5, }, + mattermost: { + /* + Mattermost: https://mattermost.com + 1. Navigate to Main Menu > Integrations > Slash Commands in Mattermost. + 2. Click on Add Slash Command and configure the following settings: + - Title: Enter a descriptive title (e.g., `SFU Command`). + - Command Trigger Word: Set the trigger word to `sfu`. + - Callback URLs: Enter the URL for your Express server (e.g., `https://yourserver.com/mattermost`). + - Request Method: Select POST. + - Enable Autocomplete: Check the box for Autocomplete. + - Autocomplete Description: Provide a brief description (e.g., `Get MiroTalk SFU meeting room`). + 3. Save the slash command and copy the generated token (YourMattermostToken). + */ + enabled: false, + serverUrl: 'YourMattermostServerUrl', + username: 'YourMattermostUsername', + password: 'YourMattermostPassword', + token: 'YourMattermostToken', + commands: [ + { + name: '/sfu', + message: 'Here is your meeting room:', + }, + //.... + ], + texts: [ + { + name: '/sfu', + message: 'Here is your meeting room:', + }, + //.... + ], + }, slack: { /* Slack @@ -276,6 +323,28 @@ module.exports = { enabled: false, signingSecret: '', }, + discord: { + /* + Discord + 1. Go to the Discord Developer Portal: https://discord.com/developers/. + 2. Create a new application and name it whatever you like. + 3. Under the Bot section, click Add Bot and confirm. + 4. Copy your bot token (this will be used later). + 5. Under OAuth2 -> URL Generator, select bot scope, and under Bot Permissions, select the permissions you need (e.g., Send Messages and Read Messages). + 6. Copy the generated invite URL, open it in a browser, and invite the bot to your Discord server. + 7. Add the Bot in the Server channel permissions + 8. Type /sfu (commands.name) in the channel, the response will return a URL for the meeting + */ + enabled: false, + token: '', + commands: [ + { + name: '/sfu', + message: 'Here is your SFU meeting room:', + baseUrl: 'https://sfu.mirotalk.com/join/', + }, + ], + }, IPLookup: { /* GeoJS @@ -385,13 +454,16 @@ module.exports = { }, producerVideo: { videoPictureInPicture: true, + videoMirrorButton: true, fullScreenButton: true, snapShotButton: true, muteAudioButton: true, videoPrivacyButton: true, + audioVolumeInput: true, }, consumerVideo: { videoPictureInPicture: true, + videoMirrorButton: true, fullScreenButton: true, snapShotButton: true, focusVideoButton: true, @@ -400,7 +472,7 @@ module.exports = { sendVideoButton: true, muteVideoButton: true, muteAudioButton: true, - audioVolumeInput: true, // Disabled for mobile + audioVolumeInput: true, geolocationButton: true, // Presenter banButton: true, // presenter ejectButton: true, // presenter @@ -410,7 +482,7 @@ module.exports = { sendFileButton: true, sendVideoButton: true, muteAudioButton: true, - audioVolumeInput: true, // Disabled for mobile + audioVolumeInput: true, geolocationButton: true, // Presenter banButton: true, // presenter ejectButton: true, // presenter diff --git a/package.json b/package.json index 4951fcd8..df8c8f67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hivetalksfu", - "version": "1.5.67", + "version": "1.6.31", "description": "WebRTC SFU browser-based video calls", "main": "Server.js", "scripts": { @@ -57,42 +57,42 @@ "node": ">=18" }, "dependencies": { - "@sentry/node": "^8.28.0", + "@mattermost/client": "^10.0.0", + "@sentry/node": "^8.37.1", "axios": "^1.7.7", - "body-parser": "1.20.2", "colors": "1.4.0", - "compression": "1.7.4", + "compression": "1.7.5", "cors": "2.8.5", "crypto-js": "4.2.0", - "dompurify": "^3.1.6", + "dompurify": "^3.1.7", "ejs": "^3.1.10", - "express": "4.20.0", + "express": "4.21.1", "express-handlebars": "^7.1.3", "express-openid-connect": "^2.17.1", "fluent-ffmpeg": "^2.1.3", "he": "^1.2.0", "httpolyglot": "0.1.2", "js-yaml": "^4.1.0", - "jsdom": "^25.0.0", + "jsdom": "^25.0.1", "jsonwebtoken": "^9.0.2", - "mediasoup": "3.14.14", - "mediasoup-client": "3.7.16", + "mediasoup": "3.14.16", + "mediasoup-client": "3.7.17", "ngrok": "^5.0.0-beta.2", - "nodemailer": "^6.9.15", + "nodemailer": "^6.9.16", "nostr-tools": "^2.5.2", - "openai": "^4.58.1", + "openai": "^4.71.1", "qs": "6.13.0", - "socket.io": "4.7.5", + "socket.io": "4.8.1", "swagger-ui-express": "5.0.1", - "uuid": "10.0.0" + "uuid": "11.0.2" }, "devDependencies": { - "mocha": "^10.7.3", + "mocha": "^10.8.2", "node-fetch": "^3.3.2", - "nodemon": "^3.1.4", + "nodemon": "^3.1.7", "prettier": "3.3.3", "proxyquire": "^2.1.3", "should": "^13.2.3", - "sinon": "^18.0.0" + "sinon": "^19.0.2" } } diff --git a/public/css/GroupChat.css b/public/css/GroupChat.css index f075a5d0..984aecd1 100644 --- a/public/css/GroupChat.css +++ b/public/css/GroupChat.css @@ -19,8 +19,9 @@ border: var(--border); border-radius: 10px; box-shadow: var(--box-shadow); - transition: background 1s; - transition: width 0.5s ease-in-out; + transition: + background 1s, + width 0.5s ease-in-out; /* border: 1px solid lime; */ } @@ -373,3 +374,10 @@ input[type='text'] { background: var(--select-bg); transform: translateY(-3px); } + +/* handle screen sizes */ +@media screen and (max-width: 600px) { + .people-list { + width: 100% !important; + } +} diff --git a/public/css/Room.css b/public/css/Room.css index 2fbb73e1..04c34649 100644 --- a/public/css/Room.css +++ b/public/css/Room.css @@ -215,6 +215,10 @@ body { color: red; } +#toggleExtraButton { + color: #66beff; +} + /*-------------------------------------------------------------- # Bottom buttons --------------------------------------------------------------*/ @@ -253,6 +257,20 @@ body { background: var(--body-bg) !important; } +@media screen and (max-width: 500px) { + #bottomButtons button { + width: 46px; + font-size: 1.2rem; + } +} + +@media screen and (max-width: 350px) { + #bottomButtons button { + width: 36px; + font-size: 1rem; + } +} + /*-------------------------------------------------------------- # Room QR --------------------------------------------------------------*/ @@ -370,6 +388,13 @@ body { color: white; } +/* Responsive adjustments */ +@media screen and (max-width: 500px) { + .title p { + font-size: 0.8em; + } +} + .inline-check-box { margin-bottom: 20px; display: inline-flex; @@ -422,17 +447,19 @@ body { color: #fff; table-layout: fixed; border-collapse: collapse; + border-radius: 10px; border-style: hidden; + background: var(--body-bg); } .settingsTable td, th { text-align: left; - padding: 5px; + padding: 10px; /* border: 1px solid grey; */ } -.settingsTable tr:nth-child(even) { - /* background-color: #121212; */ -} +/* .settingsTable tr:nth-child(even) { + background: var(--select-bg); +} */ .settingsTable i { border: none; border-radius: 5px; @@ -451,10 +478,6 @@ th { width: 180px; } -#VideoMirrorDiv { - margin-top: 10px; -} - /*-------------------------------------------------------------- # RTMP settings --------------------------------------------------------------*/ @@ -706,9 +729,10 @@ th { display: block; } +/* moderator title */ .mod-title { - margin-top: 5px; - color: grey; + font-size: 1.1rem; + color: #c2c2c2; } /*-------------------------------------------------------------- @@ -939,6 +963,17 @@ th { # Room/user emoji picker --------------------------------------------------------------*/ +#usernameEmoji { + position: absolute; + z-index: 9999; + border-radius: 10px; + border: var(--border); + background: var(--body-bg); + box-shadow: var(--box-shadow); + --rgb-background: var(--body-bg); + --color-border-over: var(--body-bg); +} + .roomEmoji { z-index: 9; position: absolute; @@ -1070,7 +1105,8 @@ th { h1, h2, -h3 { +h3, +h4 { color: #c2c2c2; } @@ -1130,6 +1166,10 @@ button:hover { color: red !important; } +.yellow { + color: yellow !important; +} + .green { color: rgb(0, 255, 71) !important; } @@ -1437,8 +1477,13 @@ hr { } .wa { - /* width: auto; */ - width: 320px; + width: 260px; +} + +@media screen and (max-width: 500px) { + .wa { + width: 220px; + } } .ml-5 { diff --git a/public/css/Root.css b/public/css/Root.css index 3c235cbe..5dbddc1e 100644 --- a/public/css/Root.css +++ b/public/css/Root.css @@ -57,6 +57,8 @@ --videoObjFit: cover; --dd-color: #ffffff; + + --videoBar-active: 0.1px solid rgba(102, 190, 255, 0.32); } * { diff --git a/public/css/VideoGrid.css b/public/css/VideoGrid.css index 0c30e316..9ddfaa86 100644 --- a/public/css/VideoGrid.css +++ b/public/css/VideoGrid.css @@ -47,6 +47,11 @@ border: 3px solid rgb(113, 157, 239); } */ +.Camera .fa-hand-paper { + margin: 10px !important; + font-size: 1.5rem !important; +} + #videoMediaContainer i { position: absolute; display: none; @@ -105,11 +110,11 @@ z-index: 1; position: absolute; left: 0; - top: 15px; + top: 0; display: flex; align-items: center; padding: 5px; - margin: 5px; + margin: 1px; width: 50px; /* Set your image width */ height: 50px; /* Set your image height */ border-radius: 5px; /* Remove button border */ @@ -146,18 +151,12 @@ font-size: 10px; display: flex; align-items: center; - padding: 5px; - margin: 5px; + padding: 10px; + margin: 10px; width: auto; height: 25px; border-radius: 5px; - background: rgba(0, 0, 0, 0.9); -} - -.peer-name { - background-color: rgba(0, 0, 0, 0.9); - color: white; - font-size: 18px; + background: var(--body-bg); } .fscreen { @@ -173,11 +172,47 @@ } .videoMenuBar { - z-index: 10; + z-index: 2; position: fixed; display: inline; top: 0; left: 0; + padding: 15px; + width: 100%; + font-size: small; + font-weight: bold; + text-align: center; + background: var(--body-bg); + cursor: default; + /* overflow: hidden; */ +} + +.videoMenuBarClose { + position: absolute; + display: flex; + top: 30px; + right: 30px; + padding: 10px; + border-radius: 50%; + color: white; + align-items: center; + justify-content: center; + cursor: pointer; + user-select: none; + background: var(--body-bg); +} + +.videoMenuBarClose:hover { + background: var(--btns-bg-color); +} + +.videoAvatarMenuBar, +.videoMenuBarShare { + z-index: 2; + position: absolute; + display: inline; + top: 0; + left: 0; padding: 20px; background: rgba(0, 0, 0, 0.9); font-size: small; @@ -215,8 +250,10 @@ } .videoMenuBar input, -.videoMenuBar button { - display: inline !important; +.videoMenuBar button, +.videoAvatarMenuBar button, +.videoMenuBarShare button { + font-size: 1.2rem; float: right; color: #fff; background: transparent; @@ -224,251 +261,109 @@ border: none; } -.videoMenuBar button:hover { +.videoMenuBar button:hover, +.videoAvatarMenuBar button:hover, +.videoMenuBarShare button:hover { color: grey; transition: all 0.3s ease-in-out; } -.videoMenuBarClose { - display: none; +.expand-video .fa-bars { + color: #66beff !important; } - -@media (max-width: 768px) { - .videoMenuBar { - position: fixed; - padding: 15px; - padding-left: 65px; - bottom: 0; - background: rgba(0, 0, 0, 1); - transform: translateY(100%); - transition: transform 0.3s ease-out, opacity 0.3s ease-out; - display: grid !important; - grid-gap: 5px 5px; - grid-template-columns: 50%; - grid-template-areas: - "header header" - "controls controls"; - align-content: start; - justify-items: start; - overflow-y: auto; - z-index: 1000; - } - - .videoMenuBarClose { - display: flex; - position: absolute; - top: 20px; - right: 20px; - width: 30px; - height: 30px; - background: rgba(0, 0, 0, 0.5); - border-radius: 50%; - color: white; - font-size: 24px; - align-items: center; - justify-content: center; - cursor: pointer; - z-index: 10; - user-select: none; - transition: background-color 0.3s ease; - } - - .videoMenuBar .nostr-image-button { - top: 10px; - left: 65px !important; - width: 50px; - height: 50px; - } - - .peer-name-container { - display: row; - width: 100%; - height: 100%; - padding: 10px; - margin: 10px; - } - - .videoMenuBar .peer-name-header { - grid-area: header; - width: 100%; - padding: 40px; - height: 120px; - background-color: rgba(255, 255, 255, 0.2); - background-size: cover; - background-position: center; - background-repeat: no-repeat; - border-radius: 10px; - margin-bottom: 10px; - display: flex; - align-items: center; - justify-content: center; - } - - .videoMenuBar .peer-name { - font-size: 18px; - font-weight: bold; - color: #fff; - text-shadow: 0 0 10px rgba(0, 0, 0, 0.5); - background-color: rgba(36, 36, 36, 0.8); - border-radius: 10px; - padding: 6px; - width: 100%; - } - - .videoMenuBar input, - .videoMenuBar button { - display: inline !important; - font-size: 16px; - color: #fff; - background: rgba(255, 255, 255, 0.2); - width: 100%; - height: 45px; - display: flex; - justify-content: center; - align-items: center; - border-radius: 10px; - } - - .videoMenuBar .fas, - .videoMenuBar .fab { - font-size: 20px; - margin-right: 8px; - transition: color 0.3s; - } - - .videoMenuBar input[type="range"] { - display: inline !important; - width: 100%; - margin: 16px 0; - background: rgba(255, 255, 255, 0.1); - height: 6px; - } - - .videoMenuBar button:active { - background: rgba(255, 255, 255, 0.3); - } +.expand-video .dropdown-button { + cursor: pointer; + position: relative; } -@media (max-width: 1024px) and (orientation: landscape) { - .videoMenuBar { - position: fixed; - padding: 15px; - padding-left: 65px; - bottom: 0; - background: rgba(0, 0, 0, 1); - transform: translateY(100%); - transition: transform 0.3s ease-out, opacity 0.3s ease-out; - display: grid !important; - grid-gap: 10px 5px; - grid-template-columns: 50%; - grid-template-areas: - "header header" - "controls controls"; - align-content: start; - justify-items: start; - overflow-y: auto; - z-index: 1000; - } - - .videoMenuBarClose { - display: flex; - position: absolute; - top: 20px; - right: 20px; - width: 30px; - height: 30px; - background: rgba(0, 0, 0, 0.5); - border-radius: 50%; - color: white; - font-size: 24px; - align-items: center; - justify-content: center; - cursor: pointer; - z-index: 10; - user-select: none; - transition: background-color 0.3s ease; - } - - .videoMenuBar .nostr-image-button { - top: 10px; - left: 65px !important; - width: 50px; - height: 50px; - } - - .videoMenuBar .peer-name-header { - grid-area: header; - width: 100%; - padding: 40px; - background-color: rgba(255, 255, 255, 0.2); - background-size: cover; - background-position: center; - background-repeat: no-repeat; - border-radius: 10px; - margin-bottom: 10px; - display: flex; - align-items: center; - justify-content: center; - } - - .videoMenuBar .peer-name { - font-size: 18px; - font-weight: bold; - color: #fff; - text-shadow: 0 0 10px rgba(0, 0, 0, 0.5); - background-color: rgba(36, 36, 36, 0.8); - border-radius: 10px; - padding: 6px; - width: 100%; - } - - .videoMenuBar input, - .videoMenuBar button { - display: inline !important; - font-size: 16px; - color: #fff; - background: rgba(255, 255, 255, 0.2); - width: 100%; - height: 45px; - display: flex; - justify-content: center; - align-items: center; - border-radius: 10px; - } +.expand-video-content { + z-index: 200 ; + display: none; + position: fixed; + right: 10px; + width: calc(100% - 20px); + max-height: 100%; + max-width: 500px; + padding: 20px; + border-radius: 5px; + background: var(--body-bg); + box-shadow: var(--box-shadow); + max-height: calc(100vh - 80px); /* viewport height minus menubar and some padding */ + top: 60px; /* position below the menubar */ - .videoMenuBar .fas, - .videoMenuBar .fab { - font-size: 20px; - margin-right: 8px; - transition: color 0.3s; - } +} - .videoMenuBar input[type="range"] { - display: inline !important; - width: 100%; - margin: 20px 0; - background: rgba(98, 0, 255, 0.1); - border-radius: 5px; - height: 7px; - } +.expand-video:hover .expand-video-content { + z-index: 1000; + display: grid !important; + grid-gap: 5px 5px; + grid-template-columns: 50%; + grid-template-areas: + 'header header' + 'controls controls'; + align-content: start; + justify-items: start; + overflow-y: auto; +} + +.peer-name-container { + display: row; + width: 100%; + height: 100%; + padding: 10px; + margin: 10px; +} - .videoMenuBar button:active { - background: rgba(255, 255, 255, 0.3); - } +.expand-video-content .peer-name-header { + grid-area: header; + width: 100%; + padding: 40px; + height: 120px; + background: var(--btns-bg-color); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + border-radius: 10px; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; +} +.expand-video-content .peer-name { + font-size: 18px; + font-weight: bold; + color: #fff; + background: var(--body-bg); + border-radius: 10px; + padding: 6px; + width: 100%; } +.expand-video-content input[type='range'] { + display: inline !important; + width: 100%; + margin: 16px 0; + background: rgba(255, 255, 255, 0.1); + height: 6px; +} -.expand-video-content { - position: relative; - display: none; - float: right; - width: auto; +.expand-video-content button { + display: flex !important; + font-size: 16px; + color: #fff; + background: var(--btns-bg-color); + width: 100%; + height: 45px; + justify-content: center; + align-items: center; + border-radius: 10px; } -.expand-video:hover .expand-video-content { - display: inline-flex; +.expand-video-content button:hover { + color: white; + background: var(--body-bg); } #videoMediaContainer video { @@ -505,6 +400,7 @@ video { object-fit: var(--videoObjFit); border-radius: 10px; cursor: pointer; + transition: transform 0.3s ease-in-out; } #canvasAIElement { @@ -555,13 +451,22 @@ input[type='range'] { } } -@media screen and (max-width: 600px) { - .username { - font-size: 12px; - } -} @media screen and (max-width: 500px) { .username { font-size: 10px; } -} \ No newline at end of file + .videoMenuBar input, + .videoMenuBar button, + .videoMenuBarShare button { + font-size: 1rem; + } + .expand-video-content { + z-index: 1000; + min-width: 100%; + max-height: auto; + left: 0px; + } + .expand-video-content .peer-name { + font-size: 12px; + } +} diff --git a/public/css/landing.css b/public/css/landing.css index b1a75416..63671785 100644 --- a/public/css/landing.css +++ b/public/css/landing.css @@ -2171,3 +2171,24 @@ main { color: #fff; /* Ensure text color inherits from parent */ border-color: white; } + +/* #roomName { + text-align: left; +} */ + +/*-------------------------------------------------------------- +# Login and Join ROOM +--------------------------------------------------------------*/ + +#joinRoomForm { + display: none; +} + +/*-------------------------------------------------------------- +# Who are you +--------------------------------------------------------------*/ + +.disabled { + pointer-events: none; + opacity: 0.5; +} diff --git a/public/js/Common.js b/public/js/Common.js index c9e6515f..e8c21c1a 100644 --- a/public/js/Common.js +++ b/public/js/Common.js @@ -170,7 +170,7 @@ const lastRoom = document.getElementById('lastRoom'); const lastRoomName = window.localStorage.lastRoom ? window.localStorage.lastRoom : ''; if (lastRoomContainer && lastRoom && lastRoomName) { lastRoomContainer.style.display = 'inline-flex'; - lastRoom.setAttribute('href', '/join/' + lastRoomName); + lastRoom.setAttribute('href', '/join/?room=' + lastRoomName); lastRoom.innerText = lastRoomName; } @@ -244,7 +244,8 @@ function joinRoom() { return; } - window.location.href = '/join/' + roomName; + //window.location.href = '/join/' + roomName; + window.location.href = '/join/?room=' + roomName; window.localStorage.lastRoom = roomName; } diff --git a/public/js/LocalStorage.js b/public/js/LocalStorage.js index 11eb2093..815076cb 100644 --- a/public/js/LocalStorage.js +++ b/public/js/LocalStorage.js @@ -28,6 +28,7 @@ class LocalStorage { moderator_screen_cant_share: false, // Everyone can't share screen moderator_chat_cant_privately: false, // Everyone can't chat privately, only Public chat allowed moderator_chat_cant_chatgpt: false, // Everyone can't chat with ChatGPT + moderator_media_cant_sharing: false, // Everyone can't share media moderator_disconnect_all_on_leave: false, // Disconnect all participants on leave room mic_auto_gain_control: false, mic_echo_cancellations: true, diff --git a/public/js/Login.js b/public/js/Login.js index e6e0d1f6..1e273f65 100644 --- a/public/js/Login.js +++ b/public/js/Login.js @@ -4,8 +4,13 @@ console.log(window.location); const usernameInput = document.getElementById('username'); const passwordInput = document.getElementById('password'); +const loginForm = document.getElementById('loginForm'); const loginBtn = document.getElementById('loginButton'); +const joinRoomForm = document.getElementById('joinRoomForm'); +const selectRoom = document.getElementById('selectRoom'); +const joinSelectRoomBtn = document.getElementById('joinSelectRoomButton'); + usernameInput.onkeyup = (e) => { if (e.keyCode === 13) { e.preventDefault(); @@ -23,20 +28,23 @@ loginBtn.onclick = (e) => { login(); }; +joinSelectRoomBtn.onclick = (e) => { + join(); +}; + function login() { const username = filterXSS(document.getElementById('username').value); const password = filterXSS(document.getElementById('password').value); // http://localhost:3010/join/?room=test - // http://localhost:3010/join/?room=test&roomPassword=0&name=mirotalksfu&audio=0&video=0&screen=0¬ify=0 - // http://localhost:3010/join/?room=test&roomPassword=0&name=mirotalksfu&audio=0&video=0&screen=0¬ify=0&username=username&password=password + // http://localhost:3010/join/?room=test&roomPassword=0&name=admin&audio=0&video=0&screen=0¬ify=0 const qs = new URLSearchParams(window.location.search); const room = filterXSS(qs.get('room')); // http://localhost:3010/join/test const pathParts = window.location.pathname.split('/'); - const roomPath = pathParts[pathParts.length - 1]; + const roomPath = filterXSS(pathParts[pathParts.length - 1]); if (username && password) { axios @@ -51,11 +59,26 @@ function login() { const token = response.data.message; window.sessionStorage.peer_token = token; + // Allowed rooms + const allowedRooms = response.data.allowedRooms; + if (allowedRooms && !allowedRooms.includes('*')) { + console.log('User detected with limited join room access!', allowedRooms); + loginForm.style.display = 'none'; + joinRoomForm.style.display = 'block'; + allowedRooms.forEach((room) => { + const option = document.createElement('option'); + option.value = room; + option.text = room; + selectRoom.appendChild(option); + }); + return; + } + if (room) { return (window.location.href = '/join/' + window.location.search); // return (window.location.href = '/join/?room=' + room + '&token=' + token); } - if (roomPath) { + if (roomPath && roomPath !== 'login') { return (window.location.href = '/join/' + roomPath); // return (window.location.href ='/join/?room=' + roomPath + '&token=' + token); } @@ -81,3 +104,10 @@ function login() { return; } } + +function join() { + //window.location.href = '/join/' + selectRoom.value; + const username = filterXSS(document.getElementById('username').value); + const roomId = filterXSS(document.getElementById('selectRoom').value); + window.location.href = '/join/?room=' + roomId + '&name=' + username + '&token=' + window.sessionStorage.peer_token; +} diff --git a/public/js/Room.js b/public/js/Room.js index 06f664a3..b9824713 100644 --- a/public/js/Room.js +++ b/public/js/Room.js @@ -11,7 +11,7 @@ if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.h * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.5.67 + * @version 1.6.31 * */ @@ -50,6 +50,7 @@ let redirect = { let recCodecs = null; let recPrioritizeH264 = false; +let isToggleExtraBtnClicked = false; const _PEER = { presenter: '', @@ -236,6 +237,7 @@ let isEnumerateVideoDevices = false; let isAudioAllowed = false; let isVideoAllowed = false; let isVideoPrivacyActive = false; +let isInitVideoMirror = true; let isRecording = false; let isAudioVideoAllowed = false; let isParticipantsListOpen = false; @@ -269,7 +271,10 @@ let initStream = null; let scriptProcessor = null; -const RoomURL = window.location.origin + '/join/' + room_id; // window.location.origin + '/join/?room=' + roomId + '&token=' + myToken +// window.location.origin + '/join/' + roomId +// window.location.origin + '/join/?room=' + roomId + '&token=' + myToken + +let RoomURL = window.location.origin + '/join/' + room_id; let transcription; @@ -974,29 +979,32 @@ function refreshMainButtonsToolTipPlacement() { // Control buttons setTippy('shareButton', 'Share room', placement); + setTippy('hideMeButton', 'Toggle hide self view', placement); setTippy('startRecButton', 'Start recording', placement); setTippy('stopRecButton', 'Stop recording', placement); + setTippy('fullScreenButton', 'Toggle full screen', placement); setTippy('emojiRoomButton', 'Toggle emoji reaction', placement); - setTippy('chatButton', 'Toggle the chat', placement); setTippy('pollButton', 'Toggle the poll', placement); setTippy('editorButton', 'Toggle the editor', placement); setTippy('transcriptionButton', 'Toggle transcription', placement); setTippy('whiteboardButton', 'Toggle the whiteboard', placement); setTippy('snapshotRoomButton', 'Snapshot screen, window, or tab', placement); setTippy('settingsButton', 'Toggle the settings', placement); + setTippy('restartICEButton', 'Restart ICE', placement); setTippy('aboutButton', 'About this project', placement); // Bottom buttons + setTippy('toggleExtraButton', 'Toggle extra buttons', bPlacement); setTippy('startAudioButton', 'Start the audio', bPlacement); setTippy('stopAudioButton', 'Stop the audio', bPlacement); setTippy('startVideoButton', 'Start the video', bPlacement); setTippy('stopVideoButton', 'Stop the video', bPlacement); setTippy('swapCameraButton', 'Swap the camera', bPlacement); - setTippy('hideMeButton', 'Toggle hide self view', bPlacement); setTippy('startScreenButton', 'Start screen share', bPlacement); setTippy('stopScreenButton', 'Stop screen share', bPlacement); setTippy('raiseHandButton', 'Raise your hand', bPlacement); setTippy('lowerHandButton', 'Lower your hand', bPlacement); + setTippy('chatButton', 'Toggle the chat', bPlacement); setTippy('exitButton', 'Leave room', bPlacement); } } @@ -1061,6 +1069,7 @@ async function initRoom() { } else { setButtonsInit(); handleSelectsInit(); + handleUsernameEmojiPicker(); await whoAreYou(); await setSelectsInit(); } @@ -1249,6 +1258,14 @@ function setupInitButtons() { initStopScreenButton.onclick = async () => { await toggleScreenSharing(); }; + initVideoMirrorButton.onclick = () => { + initVideo.classList.toggle('mirror'); + isInitVideoMirror = initVideo.classList.contains('mirror'); + }; + initUsernameEmojiButton.onclick = () => { + getId('usernameInput').value = ''; + toggleUsernameEmoji(); + }; } // #################################################### @@ -1449,6 +1466,7 @@ function getPeerInfo() { peer_token: peer_token, peer_presenter: isPresenter, peer_audio: isAudioAllowed, + peer_audio_volume: 100, peer_video: isVideoAllowed, peer_screen: isScreenAllowed, peer_recording: isRecording, @@ -1511,7 +1529,6 @@ function getInfo() { async function whoAreYou() { console.log('04 ----> Who are you?'); - hide(loadingDiv); document.body.style.background = 'var(--body-bg)'; try { @@ -1520,7 +1537,11 @@ async function whoAreYou() { }); const serverButtons = response.data.message; if (serverButtons) { - BUTTONS = serverButtons; + // Merge serverButtons into BUTTONS, keeping the existing keys in BUTTONS if they are not present in serverButtons + BUTTONS = { + ...BUTTONS, // Spread current BUTTONS first to keep existing keys + ...serverButtons, // Overwrite or add new keys from serverButtons + }; console.log('04 ----> AXIOS ROOM BUTTONS SETTINGS', { serverButtons: serverButtons, clientButtons: BUTTONS, @@ -1535,6 +1556,7 @@ async function whoAreYou() { } if (peer_name) { + hide(loadingDiv); checkMedia(); getPeerInfo(); joinRoom(peer_name, room_id); @@ -1569,6 +1591,36 @@ async function whoAreYou() { hide(initStartScreenButton); } + // Fetch the OIDC profile and manage peer_name + let force_peer_name = false; + + try { + const { data: profile } = await axios.get('/profile', { timeout: 5000 }); + + if (profile) { + console.log('AXIOS GET OIDC Profile retrieved successfully', profile); + + // Define peer_name based on the profile properties and preferences + const peerNamePreference = profile.peer_name || {}; + default_name = + (peerNamePreference.email && profile.email) || + (peerNamePreference.name && profile.name) || + default_name; + + // Set localStorage and force_peer_name if applicable + if (default_name && peerNamePreference.force) { + window.localStorage.peer_name = default_name; + force_peer_name = true; + } else { + console.warn('AXIOS GET Profile retrieved but missing required peer name fields'); + } + } else { + console.warn('AXIOS GET Profile data is empty or undefined'); + } + } catch (error) { + console.error('AXIOS OIDC Error fetching profile', error.message || error); + } + initUser.classList.toggle('hidden'); Swal.fire({ @@ -1578,13 +1630,16 @@ async function whoAreYou() { title: BRAND.app.name, input: 'text', inputPlaceholder: 'Enter your email or name', - inputAttributes: { maxlength: 32 }, + inputAttributes: { maxlength: 32, id: 'usernameInput' }, inputValue: default_name, html: initUser, // Inject HTML confirmButtonText: `Lets Go!`, customClass: { popup: 'init-modal-size' }, showClass: { popup: 'animate__animated animate__fadeInDown' }, hideClass: { popup: 'animate__animated animate__fadeOutUp' }, + willOpen: () => { + hide(loadingDiv); + }, inputValidator: (name) => { if (!name) return 'Please enter your email or name'; if (name.length > 30) return 'Name must be max 30 char'; @@ -1598,6 +1653,9 @@ async function whoAreYou() { peer_name = name; }, }).then(async () => { + if (!usernameEmoji.classList.contains('hidden')) { + usernameEmoji.classList.add('hidden'); + } if (initStream && !joinRoomWithScreen) { await stopTracks(initStream); elemDisplay('initVideo', false); @@ -1607,6 +1665,11 @@ async function whoAreYou() { joinRoom(peer_name, room_id); }); + if (force_peer_name) { + getId('usernameInput').disabled = true; + hide(initUsernameEmojiButton); + } + if (!isVideoAllowed) { elemDisplay('initVideo', false); initVideoContainerShow(false); @@ -2087,7 +2150,15 @@ function roomIsReady() { myProfileAvatar.setAttribute('src', rc.genAvatarSvg(peer_name, 64)); } }); + show(toggleExtraButton); //* + /* if (rc.isValidEmail(peer_name)) { + myProfileAvatar.style.borderRadius = `50px`; + myProfileAvatar.setAttribute('src', rc.genGravatar(peer_name)); + } else { + myProfileAvatar.setAttribute('src', rc.genAvatarSvg(peer_name, 64)); + } + show(toggleExtraButton); //* */ BUTTONS.main.exitButton && show(exitButton); BUTTONS.main.shareButton && show(shareButton); BUTTONS.main.hideMeButton && show(hideMeButton); @@ -2154,6 +2225,7 @@ function roomIsReady() { if (navigator.getDisplayMedia || navigator.mediaDevices.getDisplayMedia) { if (BUTTONS.main.startScreenButton) { show(startScreenButton); + show(ScreenQualityDiv); show(ScreenFpsDiv); } BUTTONS.main.snapshotRoomButton && show(snapshotRoomButton); @@ -2190,7 +2262,6 @@ function roomIsReady() { if (!DetectRTC.isMobileDevice) show(pinUnpinGridDiv); if (!isSpeechSynthesisSupported) hide(speechMsgDiv); // If we want the sidebar collapsed and it's not already collapsed - toggleSidebar(); handleButtons(); handleSelects(); handleInputs(); @@ -2206,6 +2277,7 @@ function roomIsReady() { if (room_password) { lockRoomButton.click(); } + //show(restartICEButton); // TEST } function elemDisplay(element, display, mode = 'block') { @@ -2325,6 +2397,7 @@ function handleButtons() { } isHideMeActive = !isHideMeActive; rc.handleHideMe(); + hideClassElements('videoMenuBar'); }; settingsButton.onclick = () => { rc.toggleMySettings(); @@ -2560,7 +2633,7 @@ function handleButtons() { transcription.stop(); }; fullScreenButton.onclick = () => { - rc.toggleFullScreen(); + rc.toggleRoomFullScreen(); }; recordingImage.onclick = () => { isRecording ? stopRecButton.click() : startRecButton.click(); @@ -2583,10 +2656,26 @@ function handleButtons() { }; raiseHandButton.onclick = () => { rc.updatePeerInfo(peer_name, socket.id, 'hand', true); + hideClassElements('videoMenuBar'); }; lowerHandButton.onclick = () => { rc.updatePeerInfo(peer_name, socket.id, 'hand', false); }; + toggleExtraButton.onclick = () => { + toggleExtraButtons(); + if (!DetectRTC.isMobileDevice) { + isToggleExtraBtnClicked = true; + setTimeout(() => { + isToggleExtraBtnClicked = false; + }, 2000); + } + }; + toggleExtraButton.onmouseover = () => { + if (isToggleExtraBtnClicked || DetectRTC.isMobileDevice) return; + if (control.style.display === 'none') { + toggleExtraButtons(); + } + }; startAudioButton.onclick = async () => { const moderator = rc.getModerator(); if (moderator.audio_cant_unmute) { @@ -2672,6 +2761,9 @@ function handleButtons() { rc.shareVideo('all'); }; videoCloseBtn.onclick = () => { + if (rc._moderator.media_cant_sharing) { + return userLog('warning', 'The moderator does not allow you close this media', 'top-end', 6000); + } rc.closeVideo(true); }; sendAbortBtn.onclick = () => { @@ -2764,9 +2856,9 @@ function handleButtons() { aboutButton.onclick = () => { showAbout(); }; - // restartICE.onclick = async () => { - // await rc.restartIce(); - // }; + restartICEButton.onclick = async () => { + await rc.restartIce(); + }; } // #################################################### @@ -2780,6 +2872,8 @@ function setButtonsInit() { setTippy('initAudioVideoButton', 'Toggle the audio & video', 'top'); setTippy('initStartScreenButton', 'Toggle screen sharing', 'top'); setTippy('initStopScreenButton', 'Toggle screen sharing', 'top'); + setTippy('initVideoMirrorButton', 'Toggle video mirror', 'top'); + setTippy('initUsernameEmojiButton', 'Toggle username emoji', 'top'); } if (!isAudioAllowed) hide(initAudioButton); if (!isVideoAllowed) hide(initVideoButton); @@ -3046,11 +3140,13 @@ function handleCameraMirror(video) { // Desktop devices... if (!video.classList.contains('mirror')) { video.classList.toggle('mirror'); + isInitVideoMirror = true; } } else { // Mobile, Tablet, IPad devices... if (video.classList.contains('mirror')) { video.classList.remove('mirror'); + isInitVideoMirror = false; } } } @@ -3065,6 +3161,9 @@ function handleSelects() { videoQuality.onchange = () => { rc.closeThenProduce(RoomClient.mediaType.video, videoSelect.value); }; + screenQuality.onchange = () => { + rc.closeThenProduce(RoomClient.mediaType.screen); + }; videoFps.onchange = () => { rc.closeThenProduce(RoomClient.mediaType.video, videoSelect.value); localStorageSettings.video_fps = videoFps.selectedIndex; @@ -3147,11 +3246,6 @@ function handleSelects() { lS.setSettings(localStorageSettings); e.target.blur(); }; - switchVideoMirror.onchange = (e) => { - rc.toggleVideoMirror(); - rc.roomMessage('toggleVideoMirror', e.currentTarget.checked); - e.target.blur(); - }; switchSounds.onchange = (e) => { isSoundEnabled = e.currentTarget.checked; rc.roomMessage('sounds', isSoundEnabled); @@ -3170,9 +3264,11 @@ function handleSelects() { isButtonsBarOver = e.currentTarget.checked; localStorageSettings.keep_buttons_visible = isButtonsBarOver; lS.setSettings(localStorageSettings); + checkButtonsBar(); + const status = isButtonsBarOver ? 'enabled' : 'disabled'; + userLog('info', `Buttons always visible ${status}`, 'top-end'); e.target.blur(); }; - // audio options switchAutoGainControl.onchange = (e) => { localStorageSettings.mic_auto_gain_control = e.currentTarget.checked; @@ -3383,6 +3479,14 @@ function handleSelects() { lS.setSettings(localStorageSettings); e.target.blur(); }; + switchEveryoneCantMediaSharing.onchange = (e) => { + const mediaCantSharing = e.currentTarget.checked; + rc.updateRoomModerator({ type: 'media_cant_sharing', status: mediaCantSharing }); + rc.roomMessage('media_cant_sharing', mediaCantSharing); + localStorageSettings.moderator_media_cant_sharing = mediaCantSharing; + lS.setSettings(localStorageSettings); + e.target.blur(); + }; switchDisconnectAllOnLeave.onchange = (e) => { const disconnectAll = e.currentTarget.checked; rc.roomMessage('disconnect_all_on_leave', disconnectAll); @@ -3475,6 +3579,24 @@ function handleInputs() { // EMOJI PIKER // #################################################### +function toggleUsernameEmoji() { + getId('usernameEmoji').classList.toggle('hidden'); +} + +function handleUsernameEmojiPicker() { + const pickerOptions = { + theme: 'dark', + onEmojiSelect: addEmojiToUsername, + }; + const emojiUsernamePicker = new EmojiMart.Picker(pickerOptions); + getId('usernameEmoji').appendChild(emojiUsernamePicker); + + function addEmojiToUsername(data) { + getId('usernameInput').value += data.native; + toggleUsernameEmoji(); + } +} + function handleChatEmojiPicker() { const pickerOptions = { theme: 'dark', @@ -3727,7 +3849,7 @@ function handleRoomClientEvents() { show(stopVideoButton); setColor(startVideoButton, 'red'); setVideoButtonsDisabled(false); - switchVideoMirror.disabled = false; + hideClassElements('videoMenuBar'); // if (isParticipantsListOpen) getRoomParticipants(); }); rc.on(RoomClient.EVENTS.pauseVideo, () => { @@ -3736,6 +3858,7 @@ function handleRoomClientEvents() { show(startVideoButton); setColor(startVideoButton, 'red'); setVideoButtonsDisabled(false); + hideClassElements('videoMenuBar'); }); rc.on(RoomClient.EVENTS.resumeVideo, () => { console.log('Room event: Client resume video'); @@ -3743,6 +3866,7 @@ function handleRoomClientEvents() { show(stopVideoButton); setVideoButtonsDisabled(false); isVideoPrivacyActive = false; + hideClassElements('videoMenuBar'); }); rc.on(RoomClient.EVENTS.stopVideo, () => { console.log('Room event: Client stop video'); @@ -3750,29 +3874,33 @@ function handleRoomClientEvents() { show(startVideoButton); setVideoButtonsDisabled(false); isVideoPrivacyActive = false; - switchVideoMirror.disabled = true; + hideClassElements('videoMenuBar'); // if (isParticipantsListOpen) getRoomParticipants(); }); rc.on(RoomClient.EVENTS.startScreen, () => { console.log('Room event: Client start screen'); hide(startScreenButton); show(stopScreenButton); + hideClassElements('videoMenuBar'); // if (isParticipantsListOpen) getRoomParticipants(); }); rc.on(RoomClient.EVENTS.pauseScreen, () => { console.log('Room event: Client pause screen'); hide(startScreenButton); show(stopScreenButton); + hideClassElements('videoMenuBar'); }); rc.on(RoomClient.EVENTS.resumeScreen, () => { console.log('Room event: Client resume screen'); hide(stopScreenButton); show(startScreenButton); + hideClassElements('videoMenuBar'); }); rc.on(RoomClient.EVENTS.stopScreen, () => { console.log('Room event: Client stop screen'); hide(stopScreenButton); show(startScreenButton); + hideClassElements('videoMenuBar'); // if (isParticipantsListOpen) getRoomParticipants(); }); rc.on(RoomClient.EVENTS.roomLock, () => { @@ -3976,123 +4104,74 @@ function showButtons() { isButtonsVisible || (rc.isMobileDevice && rc.isChatOpen) || (rc.isMobileDevice && rc.isMySettingsOpen) - ) - toggleClassElements('videoMenuBar', 'inline'); - control.style.display = 'flex'; + ) + return; + toggleExtraButton.innerHTML = icons.down; bottomButtons.style.display = 'flex'; isButtonsVisible = true; } function checkButtonsBar() { if (localStorageSettings.keep_buttons_visible) { - toggleButtonsBar('show'); + // Always show buttons if keep_buttons_visible is true + control.style.display = 'flex'; + toggleExtraButton.innerHTML = icons.up; + bottomButtons.style.display = 'flex'; isButtonsVisible = true; } else { - if (!isButtonsBarOver && isButtonsVisible) { // Only hide if visible and not being hovered - toggleButtonsBar('hide'); + // Only hide if not hovering and keep_buttons_visible is false + if (!isButtonsBarOver) { + control.style.display = 'none'; + toggleExtraButton.innerHTML = icons.up; bottomButtons.style.display = 'none'; isButtonsVisible = false; } } - + + // Maintain your existing timeout check setTimeout(() => { checkButtonsBar(); }, 10000); } -function toggleButtonsBar(action = 'toggle') { - if (action === 'show') { - control.style.display = 'flex'; - isButtonsBarOver = false; - } else if (action === 'hide' && !localStorageSettings.keep_buttons_visible) { - // Only hide if keep_buttons_visible is false - control.style.display = 'none'; - isButtonsBarOver = false; - } else if (action === 'toggle' && !localStorageSettings.keep_buttons_visible) { - // Only toggle if keep_buttons_visible is false - control.style.display = control.style.display === 'flex' ? 'none' : 'flex'; - } -} - -const collapseButton = document.getElementById('collapseButtonBar'); -const sidebar = document.getElementById('control'); -const buttons = sidebar.querySelectorAll('button:not(#collapseButtonBar)'); +function toggleExtraButtons() { + const isControlHidden = control.style.display === 'none' || control.style.display === ''; + const displayValue = isControlHidden ? 'flex' : 'none'; + const iconHtml = isControlHidden ? icons.up : icons.down; -collapseButton.style.color = '#66beff'; -function toggleSidebar() { - isSidebarCollapsed = !isSidebarCollapsed; - - if (isSidebarCollapsed) { - collapseButton.innerHTML = ''; - collapseSidebar(); - } else { - collapseButton.innerHTML = ''; - expandSidebar(); - } + elemDisplay('control', isControlHidden, displayValue); + toggleExtraButton.innerHTML = iconHtml; + hideClassElements('videoMenuBar'); } -function collapseSidebar() { - const sidebarRect = sidebar.getBoundingClientRect(); - - // Calculate the bottom position for the collapse - const bottomY = sidebarRect.height; - - buttons.forEach((button) => { - button.style.transition = 'all 0.5s ease-in-out'; - button.style.transform = `translateY(${bottomY}px) scale(0.1)`; - button.style.opacity = '0'; - }); - - setTimeout(() => { - buttons.forEach(button => button.style.display = 'none'); - sidebar.style.width = '50px'; - sidebar.style.height = '50px'; - sidebar.style.position = 'absolute'; - sidebar.style.bottom = '20px'; // Add some padding from bottom - }, 500); +function hideClassElements(className) { + const elements = rc.getEcN(className); + for (let i = 0; i < elements.length; i++) { + hide(elements[i]); + } + setCamerasBorderNone(); } -function expandSidebar() { - - setTimeout(() => { - // Reset sidebar position and size - sidebar.style.width = ''; - sidebar.style.height = ''; - sidebar.style.position = ''; - sidebar.style.bottom = ''; - - buttons.forEach((button) => { - button.style.display = ''; - button.style.opacity = '0'; - button.style.transform = 'translateY(100%)'; - - // Trigger reflow - button.offsetHeight; - - button.style.transition = 'all 0.5s ease-in-out'; - button.style.transform = 'translateY(0) scale(1)'; - button.style.opacity = '1'; - }); - - // Show collapse button after animation - setTimeout(() => { - collapseButton.style.opacity = '1'; - }, 500); - }, 200); +function setCamerasBorderNone() { + const cameras = rc.getEcN('Camera'); + for (let i = 0; i < cameras.length; i++) { + cameras[i].style.border = 'none'; + } } -collapseButton.onclick = toggleSidebar; +// https://animate.style -// Minimal initial setup -sidebar.style.overflow = 'hidden'; -sidebar.style.transition = 'all 0.5s ease-in-out'; -collapseButton.style.zIndex = '1'; - -function toggleClassElements(className, displayState) { - let elements = rc.getEcN(className); - for (let i = 0; i < elements.length; i++) { - elements[i].style.display = displayState; - } +function animateCSS(element, animation, prefix = 'animate__') { + return new Promise((resolve, reject) => { + const animationName = `${prefix}${animation}`; + element.classList.add(`${prefix}animated`, animationName); + function handleAnimationEnd(event) { + event.stopPropagation(); + element.classList.remove(`${prefix}animated`, animationName); + resolve('Animation ended'); + } + element.addEventListener('animationend', handleAnimationEnd, { once: true }); + }); } function setAudioButtonsDisabled(disabled) { @@ -4814,12 +4893,14 @@ function getParticipantsList(peers) { // CHAT-GPT if (chatGPT) { + const chatgpt_active = rc.chatPeerName === 'ChatGPT' ? ' active' : ''; + li = `
' + - JSON.stringify( - peer_info, - [ - 'join_data_time', - 'peer_id', - 'peer_name', - 'peer_audio', - 'peer_video', - 'peer_video_privacy', - 'peer_screen', - 'peer_hand', - 'is_desktop_device', - 'is_mobile_device', - 'is_tablet_device', - 'is_ipad_pro_device', - 'os_name', - 'os_version', - 'browser_name', - 'browser_version', - //'user_agent', - ], - 2, - ) + - '', + `${peerInfoFormatted}`, 'top-start', true, ); @@ -8540,6 +9055,7 @@ class RoomClient { // ############################################## getAvatarList() { + this.msgPopup('toast', 'Please hold on, we are processing the avatar lists...', 10000); this.socket .request('getAvatarList') .then(function (completion) { @@ -8562,6 +9078,7 @@ class RoomClient { 'Angela in Black Dress', 'Kayla in Casual Suit', 'Anna in Brown T-shirt', + 'Anna in White T-shirt', 'Briana in Brown suit', 'Justin in White Shirt', 'Leah in Black Suit', @@ -8569,9 +9086,12 @@ class RoomClient { 'Tyler in Casual Suit', 'Tyler in Shirt', 'Tyler in Suit', - 'default', + 'Edward in Blue Shirt', + 'Susan in Black Shirt', + 'Monica in Sleeveless', ]; + //console.log('AVATARS LISTS', completion.response.avatars); completion.response.avatars.forEach((avatar) => { avatar.avatar_states.forEach((avatarUi) => { if ( @@ -8614,9 +9134,9 @@ class RoomClient { img.style.border = 'var(--border)'; const avatarData = img.getAttribute('avatarData'); const avatarDataArr = avatarData.split('|'); - VideoAI.avatar = avatarDataArr[0]; + VideoAI.avatarId = avatarDataArr[0]; VideoAI.avatarName = avatarDataArr[1]; - VideoAI.avatarVoice = avatarDataArr[2] ? avatarDataArr[2] : ''; + //VideoAI.avatarVoice = avatarDataArr[2] ? avatarDataArr[2] : ''; use the default one avatarVideoAIPreview.setAttribute('src', avatarUi.video_url.grey); avatarVideoAIPreview.play(); @@ -8680,7 +9200,7 @@ class RoomClient { const selectedPreviewURL = completion.response.list.find( (flag) => flag.voice_id === selectedVoiceID, )?.preview?.movio; - VideoAI.avatarVoice = selectedVoiceID; + VideoAI.avatarVoice = selectedVoiceID ? selectedVoiceID : null; if (selectedPreviewURL) { const avatarPreviewAudio = document.getElementById('avatarPreviewAudio'); avatarPreviewAudio.src = selectedPreviewURL; @@ -8696,19 +9216,12 @@ class RoomClient { async handleVideoAI() { const vb = document.createElement('div'); vb.setAttribute('id', 'avatar__vb'); - vb.className = 'videoMenuBar fadein'; + vb.className = 'videoAvatarMenuBar fadein'; - const fs = document.createElement('button'); - fs.id = 'avatar__fs'; - fs.className = html.fullScreen; - - const pin = document.createElement('button'); - pin.id = 'avatar__pin'; - pin.className = html.pin; - - const ss = document.createElement('button'); - ss.id = 'avatar__stopSession'; - ss.className = html.kickOut; + const interrupt = this.createButton('avatar__interrupt', html.stop); + const fs = this.createButton('avatar__fs', html.fullScreen); + const pin = this.createButton('avatar__pin', html.pin); + const ss = this.createButton('avatar__stopSession', html.kickOut); const avatarName = document.createElement('div'); const an = document.createElement('span'); @@ -8739,6 +9252,7 @@ class RoomClient { // Append elements to video container vb.appendChild(ss); this.isVideoFullScreenSupported && vb.appendChild(fs); + vb.appendChild(interrupt); !this.isMobileDevice && vb.appendChild(pin); avatarName.appendChild(an); @@ -8760,12 +9274,17 @@ class RoomClient { this.handlePN(this.videoAIElement.id, pin.id, this.videoAIContainer.id, true, true); } + interrupt.onclick = () => { + this.streamingInterrupt(); + }; + ss.onclick = () => { this.stopSession(); }; if (!this.isMobileDevice) { this.setTippy(pin.id, 'Toggle Pin', 'bottom'); + this.setTippy(interrupt.id, 'Interrupt avatar speaking', 'bottom'); this.setTippy(fs.id, 'Toggle full screen', 'bottom'); this.setTippy(ss.id, 'Stop VideoAI session', 'bottom'); } @@ -8777,11 +9296,11 @@ class RoomClient { async streamingNew() { try { - const { quality, avatar, avatarVoice } = VideoAI; + const { quality, avatarId, avatarVoice } = VideoAI; const response = await this.socket.request('streamingNew', { quality: quality, - avatar_name: avatar, + avatar_id: avatarId, voice_id: avatarVoice, }); @@ -8954,6 +9473,13 @@ class RoomClient { } } + streamingInterrupt() { + if (VideoAI.enabled && VideoAI.active && VideoAI.info.session_id) { + const response = this.socket.request('streamingInterrupt', { session_id: VideoAI.info.session_id }); + console.log('Video AI streamingInterrupt', response); + } + } + startRendering() { if (!VideoAI.virtualBackground) return; @@ -9018,6 +9544,10 @@ class RoomClient { stopRendering() { this.renderAIToken = null; + if (isHideMeActive) { + isHideMeActive = !isHideMeActive; + this.handleHideMe(); + } } stopSession() { @@ -9344,7 +9874,7 @@ class RoomClient { } toggleVideoMirror() { - const peerVideo = this.getName(this.peer_id)[0]; + const peerVideo = this.getName(this.peer_id); if (peerVideo) peerVideo.classList.toggle('mirror'); } diff --git a/public/js/Rules.js b/public/js/Rules.js index c33ea23e..af29d7bb 100644 --- a/public/js/Rules.js +++ b/public/js/Rules.js @@ -48,13 +48,16 @@ let BUTTONS = { }, producerVideo: { videoPictureInPicture: true, + videoMirrorButton: true, fullScreenButton: true, snapShotButton: true, muteAudioButton: true, videoPrivacyButton: true, + audioVolumeInput: true, }, consumerVideo: { videoPictureInPicture: true, + videoMirrorButton: true, fullScreenButton: true, snapShotButton: true, focusVideoButton: true, @@ -63,7 +66,7 @@ let BUTTONS = { sendVideoButton: true, muteVideoButton: true, muteAudioButton: true, - audioVolumeInput: true, // Disabled for mobile + audioVolumeInput: true, geolocationButton: true, // Presenter banButton: true, // presenter ejectButton: true, // presenter @@ -73,7 +76,7 @@ let BUTTONS = { sendFileButton: true, sendVideoButton: true, muteAudioButton: true, - audioVolumeInput: true, // Disabled for mobile + audioVolumeInput: true, geolocationButton: true, // Presenter banButton: true, // presenter ejectButton: true, // presenter @@ -175,6 +178,7 @@ function handleRules(isPresenter) { switchEveryoneCantShareScreen.checked = localStorageSettings.moderator_screen_cant_share; switchEveryoneCantChatPrivately.checked = localStorageSettings.moderator_chat_cant_privately; switchEveryoneCantChatChatGPT.checked = localStorageSettings.moderator_chat_cant_chatgpt; + switchEveryoneCantMediaSharing.checked = localStorageSettings.moderator_media_cant_sharing; switchDisconnectAllOnLeave.checked = localStorageSettings.moderator_disconnect_all_on_leave; // Update moderator settings... @@ -187,6 +191,7 @@ function handleRules(isPresenter) { screen_cant_share: switchEveryoneCantShareScreen.checked, chat_cant_privately: switchEveryoneCantChatPrivately.checked, chat_cant_chatgpt: switchEveryoneCantChatChatGPT.checked, + media_cant_sharing: switchEveryoneCantMediaSharing.checked, }; console.log('Rules moderator data ---->', moderatorData); rc.updateRoomModeratorALL(moderatorData); diff --git a/public/js/Transcription.js b/public/js/Transcription.js index a37bfce9..7cf1cd2b 100644 --- a/public/js/Transcription.js +++ b/public/js/Transcription.js @@ -318,6 +318,7 @@ class Transcription { transcriptionRoom.style.transform = null; document.documentElement.style.setProperty('--transcription-width', '25%'); document.documentElement.style.setProperty('--transcription-height', '100%'); + rc.resizeVideoMenuBar(); } unpinned() { @@ -334,6 +335,7 @@ class Transcription { this.center(); this.isPinned = false; setColor(transcriptionTogglePinBtn, 'white'); + rc.resizeVideoMenuBar(); resizeVideoMedia(); resizeTranscriptionRoom(); transcriptionRoom.style.resize = 'both'; diff --git a/public/js/WhoAreYou.js b/public/js/WhoAreYou.js new file mode 100644 index 00000000..29ffdfa2 --- /dev/null +++ b/public/js/WhoAreYou.js @@ -0,0 +1,122 @@ +'use strict'; + +console.log(window.location); + +const mediaQuery = window.matchMedia('(max-width: 640px)'); + +const settings = JSON.parse(localStorage.getItem('SFU_SETTINGS')) || {}; +console.log('Settings:', settings); + +const autoJoinRoom = false; // Automatically join the guest to the meeting +const intervalTime = 5000; // Interval to check room status + +const presenterLoginBtn = document.getElementById('presenterLoginButton'); +const guestJoinRoomBtn = document.getElementById('guestJoinRoomButton'); + +// Disable the guest join button initially +guestJoinRoomBtn.classList.add('disabled'); + +// Extract room ID from URL path using XSS filtering +const pathParts = window.location.pathname.split('/'); +const roomId = filterXSS(pathParts[pathParts.length - 1]); + +let intervalId = null; +let roomActive = false; + +// Button event handlers +presenterLoginBtn.addEventListener('click', () => { + window.location.href = '/login'; +}); + +guestJoinRoomBtn.addEventListener('click', () => { + window.location.href = `/join/${roomId}`; +}); + +// Function to play sound +function playSound(name) { + if (!settings.sounds) return; + + const soundSrc = `../sounds/${name}.wav`; + const audio = new Audio(soundSrc); + audio.volume = 0.5; + + audio.play().catch((err) => { + console.error(`Error playing sound: ${err}`); + }); +} + +// Handle screen resize to adjust presenter login button visibility +function handleScreenResize(e) { + if (!roomActive) { + presenterLoginBtn.style.display = e.matches ? 'flex' : 'inline-flex'; + } +} + +// Function to check room status from the server +async function checkRoomStatus(roomId) { + if (!roomId) { + console.warn('Room ID is empty!'); + return; + } + + try { + const response = await axios.post('/isRoomActive', { roomId }); + const isActive = response.data.message; + console.log('Room active status:', isActive); + + roomActive = isActive; + if (roomActive) { + playSound('roomActive'); + guestJoinRoomBtn.classList.remove('disabled'); + presenterLoginBtn.style.display = 'none'; + + if (autoJoinRoom) { + guestJoinRoomBtn.click(); + } + } else { + guestJoinRoomBtn.classList.add('disabled'); + handleScreenResize(mediaQuery); + } + } catch (error) { + console.error('Error checking room status:', error); + } +} + +// Start interval to check room status every 5 seconds +function startRoomStatusCheck() { + intervalId = setInterval(() => { + if (document.visibilityState === 'visible') { + checkRoomStatus(roomId); + } + }, intervalTime); +} + +// Fallback to setTimeout for room status checks +function fallbackRoomStatusCheck() { + if (document.visibilityState === 'visible') { + checkRoomStatus(roomId); + } + setTimeout(fallbackRoomStatusCheck, intervalTime); +} + +// Page visibility change handler to pause or resume status checks +function handleVisibilityChange() { + if (document.visibilityState === 'visible') { + console.log('Page is visible. Resuming room status checks.'); + checkRoomStatus(roomId); + if (!intervalId) startRoomStatusCheck(); + } else { + console.log('Page is hidden. Pausing room status checks.'); + clearInterval(intervalId); + intervalId = null; + } +} + +// Initialize event listeners +mediaQuery.addEventListener('change', handleScreenResize); +document.addEventListener('visibilitychange', handleVisibilityChange); + +// Start checking room status on page load +handleScreenResize(mediaQuery); +checkRoomStatus(roomId); +startRoomStatusCheck(); diff --git a/public/sfu/MediasoupClient.js b/public/sfu/MediasoupClient.js index 0a33119e..73ea0c9b 100644 --- a/public/sfu/MediasoupClient.js +++ b/public/sfu/MediasoupClient.js @@ -1968,11 +1968,11 @@ const ReactNativeUnifiedPlan_1 = require('./handlers/ReactNativeUnifiedPlan'); const ReactNative_1 = require('./handlers/ReactNative'); const logger = new Logger_1.Logger('Device'); - function detectDevice() { + function detectDevice(userAgent) { // React-Native. // NOTE: react-native-webrtc >= 1.75.0 is required. // NOTE: react-native-webrtc with Unified Plan requires version >= 106.0.0. - if (typeof navigator === 'object' && navigator.product === 'ReactNative') { + if (!userAgent && typeof navigator === 'object' && navigator.product === 'ReactNative') { logger.debug('detectDevice() | React-Native detected'); if (typeof RTCPeerConnection === 'undefined') { logger.warn( @@ -1989,10 +1989,14 @@ } } // Browser. - else if (typeof navigator === 'object' && typeof navigator.userAgent === 'string') { - const ua = navigator.userAgent; - const uaParser = new ua_parser_js_1.UAParser(ua); - logger.debug('detectDevice() | browser detected [ua:%s, parsed:%o]', ua, uaParser.getResult()); + else if (userAgent || (typeof navigator === 'object' && typeof navigator.userAgent === 'string')) { + userAgent ?? (userAgent = navigator.userAgent); + const uaParser = new ua_parser_js_1.UAParser(userAgent); + logger.debug( + 'detectDevice() | browser detected [userAgent:%s, parsed:%o]', + userAgent, + uaParser.getResult(), + ); const browser = uaParser.getBrowser(); const browserName = browser.name?.toLowerCase(); const browserVersion = parseInt(browser.major ?? '0'); @@ -2067,7 +2071,7 @@ // Best effort for Chromium based browsers. else if (engineName === 'blink') { // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec - const match = ua.match(/(?:(?:Chrome|Chromium))[ /](\w+)/i); + const match = userAgent.match(/(?:(?:Chrome|Chromium))[ /](\w+)/i); if (match) { const version = Number(match[1]); if (version >= 111) { @@ -2107,25 +2111,12 @@ * * @throws {UnsupportedError} if device is not supported. */ - constructor({ handlerName, handlerFactory, Handler } = {}) { + constructor({ handlerName, handlerFactory } = {}) { // Loaded flag. this._loaded = false; // Observer instance. this._observer = new enhancedEvents_1.EnhancedEventEmitter(); logger.debug('constructor()'); - // Handle deprecated option. - if (Handler) { - logger.warn( - 'constructor() | Handler option is DEPRECATED, use handlerName or handlerFactory instead', - ); - if (typeof Handler === 'string') { - handlerName = Handler; - } else { - throw new TypeError( - 'non string Handler option no longer supported, use handlerFactory instead', - ); - } - } if (handlerName && handlerFactory) { throw new TypeError('just one of handlerName or handlerInterface can be given'); } @@ -13912,7 +13903,7 @@ /** * Expose mediasoup-client version. */ - exports.version = '3.7.16'; + exports.version = '3.7.17'; /** * Expose parseScalabilityMode() function. */ @@ -16445,7 +16436,7 @@ 50: [ function (require, module, exports) { ///////////////////////////////////////////////////////////////////////////////// - /* UAParser.js v1.0.38 + /* UAParser.js v1.0.39 Copyright © 2012-2021 Faisal SalmanMIT License */ /* Detect Browser, Engine, OS, CPU, and Device type/model from User-Agent data. @@ -16461,7 +16452,7 @@ // Constants ///////////// - var LIBVERSION = '1.0.38', + var LIBVERSION = '1.0.39', EMPTY = '', UNKNOWN = '?', FUNC_TYPE = 'function', @@ -16504,7 +16495,8 @@ ZEBRA = 'Zebra', FACEBOOK = 'Facebook', CHROMIUM_OS = 'Chromium OS', - MAC_OS = 'Mac OS'; + MAC_OS = 'Mac OS', + SUFFIX_BROWSER = ' Browser'; /////////// // Helper @@ -16622,7 +16614,7 @@ return i === UNKNOWN ? undefined : i; } } - return str; + return map.hasOwnProperty('*') ? map['*'] : str; }; /////////////// @@ -16694,18 +16686,23 @@ [VERSION, [NAME, 'Baidu']], [ /(kindle)\/([\w\.]+)/i, // Kindle - /(lunascape|maxthon|netfront|jasmine|blazer)[\/ ]?([\w\.]*)/i, // Lunascape/Maxthon/Netfront/Jasmine/Blazer + /(lunascape|maxthon|netfront|jasmine|blazer|sleipnir)[\/ ]?([\w\.]*)/i, + // Lunascape/Maxthon/Netfront/Jasmine/Blazer/Sleipnir // Trident based /(avant|iemobile|slim)\s?(?:browser)?[\/ ]?([\w\.]*)/i, // Avant/IEMobile/SlimBrowser /(?:ms|\()(ie) ([\w\.]+)/i, // Internet Explorer // Webkit/KHTML based // Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon - /(flock|rockmelt|midori|epiphany|silk|skyfire|bolt|iron|vivaldi|iridium|phantomjs|bowser|quark|qupzilla|falkon|rekonq|puffin|brave|whale(?!.+naver)|qqbrowserlite|qq|duckduckgo)\/([-\w\.]+)/i, - // Rekonq/Puffin/Brave/Whale/QQBrowserLite/QQ, aka ShouQ - /(heytap|ovi)browser\/([\d\.]+)/i, // Heytap/Ovi + /(flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|qupzilla|falkon|rekonq|puffin|brave|whale(?!.+naver)|qqbrowserlite|duckduckgo|klar|helio)\/([-\w\.]+)/i, + // Rekonq/Puffin/Brave/Whale/QQBrowserLite/QQ//Vivaldi/DuckDuckGo/Klar/Helio + /(heytap|ovi)browser\/([\d\.]+)/i, // HeyTap/Ovi /(weibo)__([\d\.]+)/i, // Weibo ], [NAME, VERSION], + [ + /quark(?:pc)?\/([-\w\.]+)/i, // Quark + ], + [VERSION, [NAME, 'Quark']], [ /\bddg\/([\w\.]+)/i, // DuckDuckGo ], @@ -16771,11 +16768,15 @@ [ /\bqihu|(qi?ho?o?|360)browser/i, // 360 ], - [[NAME, '360 ' + BROWSER]], - [/(oculus|sailfish|huawei|vivo)browser\/([\w\.]+)/i], - [[NAME, /(.+)/, '$1 ' + BROWSER], VERSION], + [[NAME, '360' + SUFFIX_BROWSER]], [ - // Oculus/Sailfish/HuaweiBrowser/VivoBrowser + /\b(qq)\/([\w\.]+)/i, // QQ + ], + [[NAME, /(.+)/, '$1Browser'], VERSION], + [/(oculus|sailfish|huawei|vivo|pico)browser\/([\w\.]+)/i], + [[NAME, /(.+)/, '$1' + SUFFIX_BROWSER], VERSION], + [ + // Oculus/Sailfish/HuaweiBrowser/VivoBrowser/PicoBrowser /samsungbrowser\/([\w\.]+)/i, // Samsung Internet ], [VERSION, [NAME, SAMSUNG + ' Internet']], @@ -16798,7 +16799,7 @@ ], [NAME, VERSION], [ - /(lbbrowser)/i, // LieBao Browser + /(lbbrowser|rekonq)/i, // LieBao Browser/Rekonq /\[(linkedin)app\]/i, // LinkedIn App for iOS & Android ], [NAME], @@ -16861,6 +16862,10 @@ /(navigator|netscape\d?)\/([-\w\.]+)/i, // Netscape ], [[NAME, 'Netscape'], VERSION], + [ + /(wolvic)\/([\w\.]+)/i, // Wolvic + ], + [NAME, VERSION], [ /mobile vr; rv:([\w\.]+)\).+firefox/i, // Firefox Reality ], @@ -16868,20 +16873,19 @@ [ /ekiohf.+(flow)\/([\w\.]+)/i, // Flow /(swiftfox)/i, // Swiftfox - /(icedragon|iceweasel|camino|chimera|fennec|maemo browser|minimo|conkeror|klar)[\/ ]?([\w\.\+]+)/i, - // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror/Klar + /(icedragon|iceweasel|camino|chimera|fennec|maemo browser|minimo|conkeror)[\/ ]?([\w\.\+]+)/i, + // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror /(seamonkey|k-meleon|icecat|iceape|firebird|phoenix|palemoon|basilisk|waterfox)\/([-\w\.]+)$/i, // Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix /(firefox)\/([\w\.]+)/i, // Other Firefox-based /(mozilla)\/([\w\.]+) .+rv\:.+gecko\/\d+/i, // Mozilla // Other - /(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|sleipnir|obigo|mosaic|(?:go|ice|up)[\. ]?browser)[-\/ ]?v?([\w\.]+)/i, - // Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf/Sleipnir/Obigo/Mosaic/Go/ICE/UP.Browser + /(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|obigo|mosaic|(?:go|ice|up)[\. ]?browser)[-\/ ]?v?([\w\.]+)/i, + // Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf/Obigo/Mosaic/Go/ICE/UP.Browser /(links) \(([\w\.]+)/i, // Links - /panasonic;(viera)/i, // Panasonic Viera ], - [NAME, VERSION], + [NAME, [VERSION, /_/g, '.']], [ /(cobalt)\/([\w\.]+)/i, // Cobalt ], @@ -16940,8 +16944,8 @@ ], [MODEL, [VENDOR, SAMSUNG], [TYPE, TABLET]], [ - /\b((?:s[cgp]h|gt|sm)-\w+|sc[g-]?[\d]+a?|galaxy nexus)/i, - /samsung[- ]([-\w]+)/i, + /\b((?:s[cgp]h|gt|sm)-(?![lr])\w+|sc[g-]?[\d]+a?|galaxy nexus)/i, + /samsung[- ]((?!sm-[lr])[-\w]+)/i, /sec-(sgh\w+)/i, ], [MODEL, [VENDOR, SAMSUNG], [TYPE, MOBILE]], @@ -16980,7 +16984,7 @@ /\b(hm[-_ ]?note?[_ ]?(?:\d\w)?) bui/i, // Xiaomi Hongmi /\b(redmi[\-_ ]?(?:note|k)?[\w_ ]+)(?: bui|\))/i, // Xiaomi Redmi /oid[^\)]+; (m?[12][0-389][01]\w{3,6}[c-y])( bui|; wv|\))/i, // Xiaomi Redmi 'numeric' models - /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note lte|max|cc)?[_ ]?(?:\d?\w?)[_ ]?(?:plus|se|lite)?)(?: bui|\))/i, // Xiaomi Mi + /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note lte|max|cc)?[_ ]?(?:\d?\w?)[_ ]?(?:plus|se|lite|pro)?)(?: bui|\))/i, // Xiaomi Mi ], [ [MODEL, /_/g, ' '], @@ -17080,7 +17084,7 @@ [ // Amazon /(alexa)webm/i, - /(kf[a-z]{2}wi|aeo[c-r]{2})( bui|\))/i, // Kindle Fire without Silk / Echo Show + /(kf[a-z]{2}wi|aeo(?!bc)\w\w)( bui|\))/i, // Kindle Fire without Silk / Echo Show /(kf[a-z]+)( bui|\)).+silk\//i, // Kindle Fire HD ], [MODEL, [VENDOR, AMAZON], [TYPE, TABLET]], @@ -17122,6 +17126,20 @@ /(alcatel|geeksphone|nexian|panasonic(?!(?:;|\.))|sony(?!-bra))[-_ ]?([-\w]*)/i, // Alcatel/GeeksPhone/Nexian/Panasonic/Sony ], [VENDOR, [MODEL, /_/g, ' '], [TYPE, MOBILE]], + [ + // TCL + /droid [\w\.]+; ((?:8[14]9[16]|9(?:0(?:48|60|8[01])|1(?:3[27]|66)|2(?:6[69]|9[56])|466))[gqswx])\w*(\)| bui)/i, + ], + [MODEL, [VENDOR, 'TCL'], [TYPE, TABLET]], + [ + // itel + /(itel) ((\w+))/i, + ], + [ + [VENDOR, lowerize], + MODEL, + [TYPE, strMapper, { tablet: ['p10001l', 'w7001'], '*': 'mobile' }], + ], [ // Acer /droid.+; ([ab][1-7]-?[0178a]\d\d?)/i, @@ -17138,6 +17156,11 @@ /; ((?:power )?armor(?:[\w ]{0,8}))(?: bui|\))/i, ], [MODEL, [VENDOR, 'Ulefone'], [TYPE, MOBILE]], + [ + // Nothing + /droid.+; (a(?:015|06[35]|142p?))/i, + ], + [MODEL, [VENDOR, 'Nothing'], [TYPE, MOBILE]], [ // MIXED /(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus|dell|meizu|motorola|polytron|infinix|tecno)[-_ ]?([-\w]*)/i, @@ -17369,6 +17392,10 @@ // WEARABLES /////////////////// + /\b(sm-[lr]\d\d[05][fnuw]?s?)\b/i, // Samsung Galaxy Watch + ], + [MODEL, [VENDOR, SAMSUNG], [TYPE, WEARABLE]], + [ /((pebble))app/i, // Pebble ], [VENDOR, MODEL, [TYPE, WEARABLE]], diff --git a/public/sounds/roomActive.wav b/public/sounds/roomActive.wav new file mode 100644 index 00000000..a2dc0eda Binary files /dev/null and b/public/sounds/roomActive.wav differ diff --git a/public/sounds/roomDisactive.wav b/public/sounds/roomDisactive.wav new file mode 100644 index 00000000..9d1f0e03 Binary files /dev/null and b/public/sounds/roomDisactive.wav differ diff --git a/public/views/Room.html b/public/views/Room.html index 545f4a3c..a5129f22 100644 --- a/public/views/Room.html +++ b/public/views/Room.html @@ -107,8 +107,10 @@ - - + Loading + + + + + - + -- + - +@@ -327,7 +333,7 @@Loading
-