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 = `
  • `; } + const public_chat_active = rc.chatPeerName === 'all' ? ' active' : ''; + // ALL li += `
  • `; @@ -5019,7 +5104,7 @@ function getParticipantsList(peers) { id='${peer_id}' data-to-id="${peer_id}" data-to-name="${peer_name}" - class="clearfix" + class="clearfix${peer_chat_active}" onclick="rc.showPeerAboutAndMessages(this.id, '${peer_name}', event)" >`; @@ -5351,6 +5436,7 @@ function adaptAspectRatio(participantsCount) { // desktop aspect ratio switch (participantsCount) { case 1: + //case 2: case 3: case 4: case 7: diff --git a/public/js/RoomClient.js b/public/js/RoomClient.js index 4a02965d..4cf87326 100644 --- a/public/js/RoomClient.js +++ b/public/js/RoomClient.js @@ -9,7 +9,7 @@ * @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.10 * */ @@ -31,6 +31,8 @@ const html = { userHand: 'fas fa-hand-paper pulsate', pip: 'fas fa-images', fullScreen: 'fas fa-expand', + fullScreenOn: 'fas fa-compress-alt', + fullScreenOff: 'fas fa-expand-alt', snapshot: 'fas fa-camera-retro', sendFile: 'fas fa-upload', sendMsg: 'fas fa-paper-plane', @@ -43,8 +45,11 @@ const html = { bg: 'fas fa-circle-half-stroke', pin: 'fas fa-map-pin', videoPrivacy: 'far fa-circle', - expand: 'fas fa-ellipsis-vertical', + expand: 'fas fa-bars dropdown-button', hideALL: 'fas fa-eye', + mirror: 'fas fa-arrow-right-arrow-left', + close: 'fas fa-times', + stop: 'fas fa-circle-stop', }; const icons = { @@ -64,13 +69,15 @@ const icons = { fileSend: '', fileReceive: '', recording: '', - moderator: '', + moderator: '', broadcaster: '', codecs: '', theme: '', recSync: '', refresh: '', editor: '', + up: '', + down: '', }; const image = { @@ -169,9 +176,9 @@ const VideoAI = { enabled: true, active: false, info: {}, - avatar: null, + avatarId: null, avatarName: 'Monica', - avatarVoice: '', + avatarVoice: null, quality: 'medium', virtualBackground: true, background: '../images/virtual/1.jpg', @@ -323,6 +330,7 @@ class RoomClient { screen_cant_share: false, chat_cant_privately: false, chat_cant_chatgpt: false, + media_cant_sharing: false, }; // Chat messages @@ -333,6 +341,8 @@ class RoomClient { this.chatMessageNotifyDelay = 10000; // ms this.chatMessageSpamCount = 0; this.chatMessageSpamCountToBan = 10; + this.chatPeerId = 'all'; + this.chatPeerName = 'all'; // HeyGen Video AI this.videoAIContainer = null; @@ -350,12 +360,15 @@ class RoomClient { this.device = null; this.isMobileDevice = DetectRTC.isMobileDevice; + this.isMobileSafari = this.isMobileDevice && DetectRTC.browser.name === 'Safari'; + this.isScreenShareSupported = navigator.getDisplayMedia || navigator.mediaDevices.getDisplayMedia ? true : false; this.isMySettingsOpen = false; this._isConnected = false; + this.isDocumentOnFullScreen = false; this.isVideoOnFullScreen = false; this.isVideoFullScreenSupported = this.isFullScreenSupported(); this.isVideoPictureInPictureSupported = document.pictureInPictureEnabled; @@ -436,13 +449,14 @@ class RoomClient { this.myVideoEl = null; this.myAudioEl = null; - this.showPeerInfo = false; // on peerName mouse hover + this.showPeerInfo = false; // on peerName mouse hover show additional info this.videoProducerId = null; this.screenProducerId = null; this.audioProducerId = null; this.audioConsumers = new Map(); + this.peers = new Map(); this.consumers = new Map(); this.producers = new Map(); this.producerLabel = new Map(); @@ -541,11 +555,14 @@ class RoomClient { console.log('00-WARNING ----> You are Banned from the Room!'); return this.isBanned(); } - const peers = new Map(JSON.parse(room.peers)); + // ########################################## + this.peers = new Map(JSON.parse(room.peers)); + // ########################################## + if (!peer_info.peer_token) { // hack... - for (let peer of Array.from(peers.keys()).filter((id) => id !== this.peer_id)) { - let peer_info = peers.get(peer).peer_info; + for (let peer of Array.from(this.peers.keys()).filter((id) => id !== this.peer_id)) { + let peer_info = this.peers.get(peer).peer_info; if (peer_info.peer_name == this.peer_name) { console.log('00-WARNING ----> Username already in use'); return this.userNameAlreadyInRoom(); @@ -584,11 +601,10 @@ class RoomClient { survey = room.survey; console.log('07.0 ----> Room Leave Redirect', room.redirect); redirect = room.redirect; - let peers = new Map(JSON.parse(room.peers)); - participantsCount = peers.size; + participantsCount = this.peers.size; // ME - for (let peer of Array.from(peers.keys()).filter((id) => id == this.peer_id)) { - let my_peer_info = peers.get(peer).peer_info; + for (let peer of Array.from(this.peers.keys()).filter((id) => id == this.peer_id)) { + let my_peer_info = this.peers.get(peer).peer_info; console.log('07.1 ----> My Peer info', my_peer_info); isPresenter = window.localStorage.isReconnected === 'true' ? isPresenter : my_peer_info.peer_presenter; this.peer_info.peer_presenter = isPresenter; @@ -624,27 +640,10 @@ class RoomClient { // Handle Room moderator rules if (room.moderator && (!isRulesActive || !isPresenter)) { console.log('07.2 ----> ROOM MODERATOR', room.moderator); - const { - video_start_privacy, - audio_start_muted, - video_start_hidden, - audio_cant_unmute, - video_cant_unhide, - screen_cant_share, - chat_cant_privately, - chat_cant_chatgpt, - } = room.moderator; - - this._moderator.video_start_privacy = video_start_privacy; - this._moderator.audio_start_muted = audio_start_muted; - this._moderator.video_start_hidden = video_start_hidden; - this._moderator.audio_cant_unmute = audio_cant_unmute; - this._moderator.video_cant_unhide = video_cant_unhide; - this._moderator.screen_cant_share = screen_cant_share; - this._moderator.chat_cant_privately = chat_cant_privately; - this._moderator.chat_cant_chatgpt = chat_cant_chatgpt; - // + // Update `this._moderator` with properties from `room.moderator`, keeping existing ones. + this._moderator = { ...this._moderator, ...room.moderator }; + if (this._moderator.video_start_privacy || localStorageSettings.moderator_video_start_privacy) { this.peer_info.peer_video_privacy = true; this.emitCmd({ @@ -653,7 +652,7 @@ class RoomClient { active: true, broadcast: true, }); - this.userLog('warning', 'The Moderator starts video in privacy mode', 'top-end'); + this.userLog('warning', 'The Moderator starts your video in privacy mode', 'top-end'); } if (this._moderator.audio_start_muted && this._moderator.video_start_hidden) { this.userLog('warning', 'The Moderator disabled your audio and video', 'top-end'); @@ -690,23 +689,39 @@ class RoomClient { if (room.thereIsPolls) { this.socket.emit('updatePoll'); } + // Host protected enabled in the server side + if (room.hostProtected) { + RoomURL = window.location.origin + '/join/?room=' + room_id; + } + + // Share Media Data on Join + if ( + room.shareMediaData && + Object.keys(room.shareMediaData).length !== 0 && + room.shareMediaData.action === 'open' + ) { + this.shareVideoAction(room.shareMediaData); + } } // PARTICIPANTS - for (let peer of Array.from(peers.keys()).filter((id) => id !== this.peer_id)) { - let peer_info = peers.get(peer).peer_info; + for (let peer of Array.from(this.peers.keys()).filter((id) => id !== this.peer_id)) { + let peer_info = this.peers.get(peer).peer_info; // console.log('07.1 ----> Remote Peer info', peer_info); - const canSetVideoOff = !isBroadcastingEnabled || (isBroadcastingEnabled && peer_info.peer_presenter); - if (!peer_info.peer_video && canSetVideoOff) { - console.log('Detected peer video off ' + peer_info.peer_name); + const { peer_id, peer_name, peer_presenter, peer_video, peer_recording } = peer_info; + + const canSetVideoOff = !isBroadcastingEnabled || (isBroadcastingEnabled && peer_presenter); + + if (!peer_video && canSetVideoOff) { + console.log('Detected peer video off ' + peer_name); this.setVideoOff(peer_info, true); } - if (peer_info.peer_recording) { + if (peer_recording) { this.handleRecordingAction({ - peer_id: peer_info.id, - peer_name: peer_info.peer_name, + peer_id: peer_id, + peer_name: peer_name, action: enums.recording.started, }); } @@ -767,6 +782,8 @@ class RoomClient { return; } + producerTransportData['proprietaryConstraints'] = { optional: [{ googDscp: true }] }; + this.producerTransport = device.createSendTransport(producerTransportData); console.info('07.4 producerTransportData ---->', { @@ -813,19 +830,18 @@ class RoomClient { break; case 'disconnected': console.log('Producer Transport disconnected', { id: this.producerTransport.id }); - /* + this.restartIce(); popupHtmlMessage( null, image.network, 'Producer Transport disconnected', - 'Check Your Network Connectivity (Restarted ICE)', + 'Network connection may have dropped or changed (Restarted ICE)', 'center', false, - true + false, ); - */ break; case 'failed': console.warn('Producer Transport failed', { id: this.producerTransport.id }); @@ -900,19 +916,19 @@ class RoomClient { break; case 'disconnected': console.log('Consumer Transport disconnected', { id: this.consumerTransport.id }); - /* + this.restartIce(); popupHtmlMessage( null, image.network, 'Consumer Transport disconnected', - 'Check Your Network Connectivity (Restarted ICE)', + 'Network connection may have dropped or changed (Restarted ICE)', 'center', false, - true + false, ); - */ + break; case 'failed': console.warn('Consumer Transport failed', { id: this.consumerTransport.id }); @@ -1497,6 +1513,9 @@ class RoomClient { throw new Error('Producer not found!'); } + console.log('PRODUCER MEDIA TYPE ----> ' + type); + console.log('PRODUCER', producer); + this.producers.set(producer.id, producer); this.producerLabel.set(type, producer.id); @@ -1511,6 +1530,10 @@ class RoomClient { if (type == mediaType.video) this.videoProducerId = producer.id; if (type == mediaType.screen) this.screenProducerId = producer.id; elem = await this.handleProducer(producer.id, type, stream); + // No mirror effect for producer + if (!isInitVideoMirror && elem.classList.contains('mirror')) { + elem.classList.remove('mirror'); + } //if (!screen && !isEnumerateDevices) enumerateVideoDevices(stream); } else { this.localAudioStream = stream; @@ -1525,19 +1548,23 @@ class RoomClient { } producer.on('trackended', () => { - console.log('Producer track ended', { id: producer.id }); + console.log('Producer track ended', { id: producer.id, type }); this.closeProducer(type); }); producer.on('transportclose', () => { - console.log('Producer transport close', { id: producer.id }); + console.log('Producer transport close', { id: producer.id, type }); if (!audio) { const d = this.getId(producer.id + '__video'); + const vb = this.getId(producer.id + '__vb'); + elem.srcObject.getTracks().forEach(function (track) { track.stop(); }); elem.parentNode.removeChild(elem); + d.parentNode.removeChild(d); + vb.parentNode.removeChild(vb); handleAspectRatio(); console.log('[transportClose] Video-element-count', this.videoMediaContainer.childElementCount); @@ -1552,14 +1579,18 @@ class RoomClient { }); producer.on('close', () => { - console.log('Closing producer', { id: producer.id }); + console.log('Closing producer', { id: producer.id, type }); if (!audio) { const d = this.getId(producer.id + '__video'); + const vb = this.getId(producer.id + '__vb'); + elem.srcObject.getTracks().forEach(function (track) { track.stop(); }); elem.parentNode.removeChild(elem); + d.parentNode.removeChild(d); + vb.parentNode.removeChild(vb); handleAspectRatio(); console.log('[closingProducer] Video-element-count', this.videoMediaContainer.childElementCount); @@ -1610,7 +1641,7 @@ class RoomClient { } // #################################################### - // AUDIO/VIDEO CONSTRAINTS + // AUDIO/VIDEO/SCREEN CONSTRAINTS // #################################################### getAudioConstraints(deviceId) { @@ -1655,149 +1686,85 @@ class RoomClient { const defaultFrameRate = { ideal: 30 }; const selectedValue = this.getSelectedIndexValue(videoFps); const customFrameRate = parseInt(selectedValue, 10); - const frameRate = selectedValue == 'max' ? defaultFrameRate : customFrameRate; - let videoConstraints = { + const frameRate = selectedValue === 'max' ? defaultFrameRate : { ideal: customFrameRate }; + + // Base constraints structure with dynamic values for resolution and frame rate + const videoBaseConstraints = (width, height, exact = false) => ({ audio: false, video: { - width: { ideal: 3840 }, - height: { ideal: 2160 }, + width: exact ? { exact: width } : { ideal: width }, + height: exact ? { exact: height } : { ideal: height }, deviceId: deviceId, - aspectRatio: 1.777, // 16:9 + aspectRatio: 1.777, // 16:9 aspect ratio frameRate: frameRate, }, - }; // Init auto detect max cam resolution and fps + }); + + const videoResolutionMap = { + qvga: { width: 320, height: 240, exact: true }, + vga: { width: 640, height: 480, exact: true }, + hd: { width: 1280, height: 720, exact: true }, + fhd: { width: 1920, height: 1080, exact: true }, + '2k': { width: 2560, height: 1440, exact: true }, + '4k': { width: 3840, height: 2160, exact: true }, + '6k': { width: 6144, height: 3456, exact: true }, + '8k': { width: 7680, height: 4320, exact: true }, + }; - videoFps.disabled = false; + let videoConstraints; switch (videoQuality.value) { case 'default': - // This will make the browser use HD Video and 30fps as default. - videoConstraints = { - audio: false, - video: { - width: { ideal: 1280 }, - height: { ideal: 720 }, - deviceId: deviceId, - aspectRatio: 1.777, - }, - }; + // Default ideal HD resolution + videoConstraints = videoBaseConstraints(1280, 720); videoFps.selectedIndex = 0; videoFps.disabled = true; break; - case 'qvga': - videoConstraints = { - audio: false, - video: { - width: { exact: 320 }, - height: { exact: 240 }, - deviceId: deviceId, - aspectRatio: 1.777, - frameRate: frameRate, - }, - }; // video cam constraints low bandwidth - break; - case 'vga': - videoConstraints = { - audio: false, - video: { - width: { exact: 640 }, - height: { exact: 480 }, - deviceId: deviceId, - aspectRatio: 1.777, - frameRate: frameRate, - }, - }; // video cam constraints medium bandwidth - break; - case 'hd': - videoConstraints = { - audio: false, - video: { - width: { exact: 1280 }, - height: { exact: 720 }, - deviceId: deviceId, - aspectRatio: 1.777, - frameRate: frameRate, - }, - }; // video cam constraints high bandwidth - break; - case 'fhd': - videoConstraints = { - audio: false, - video: { - width: { exact: 1920 }, - height: { exact: 1080 }, - deviceId: deviceId, - aspectRatio: 1.777, - frameRate: frameRate, - }, - }; // video cam constraints very high bandwidth - break; - case '2k': - videoConstraints = { - audio: false, - video: { - width: { exact: 2560 }, - height: { exact: 1440 }, - deviceId: deviceId, - aspectRatio: 1.777, - frameRate: frameRate, - }, - }; // video cam constraints ultra high bandwidth - break; - case '4k': - videoConstraints = { - audio: false, - video: { - width: { exact: 3840 }, - height: { exact: 2160 }, - deviceId: deviceId, - aspectRatio: 1.777, - frameRate: frameRate, - }, - }; // video cam constraints ultra high bandwidth - break; - case '6k': - videoConstraints = { - audio: false, - video: { - width: { exact: 6144 }, - height: { exact: 3456 }, - deviceId: deviceId, - aspectRatio: 1.777, - frameRate: frameRate, - }, - }; // video cam constraints Very ultra high bandwidth - break; - case '8k': - videoConstraints = { - audio: false, - video: { - width: { exact: 7680 }, - height: { exact: 4320 }, - deviceId: deviceId, - aspectRatio: 1.777, - frameRate: frameRate, - }, - }; // video cam constraints Very ultra high bandwidth - break; default: + // Ideal Full HD if no match found in the video resolution map + const { width, height, exact } = videoResolutionMap[videoQuality.value] || { + width: 1920, + height: 1080, + }; + videoConstraints = videoBaseConstraints(width, height, exact); break; } + this.videoQualitySelectedIndex = videoQuality.selectedIndex; + return videoConstraints; } getScreenConstraints() { + const defaultFrameRate = { ideal: 30 }; const selectedValue = this.getSelectedIndexValue(screenFps); - const frameRate = selectedValue == 'max' ? 30 : parseInt(selectedValue, 10); - return { + const customFrameRate = parseInt(selectedValue, 10); + const frameRate = selectedValue === 'max' ? defaultFrameRate : { ideal: customFrameRate }; + + // Base constraints structure with dynamic values for resolution and frame rate + const screenBaseConstraints = (width, height) => ({ audio: true, video: { - width: { ideal: 1920 }, - height: { ideal: 1080 }, + width: { ideal: width }, + height: { ideal: height }, + aspectRatio: 1.777, // 16:9 aspect ratio frameRate: frameRate, }, + }); + + const screenResolutionMap = { + hd: { width: 1280, height: 720 }, + fhd: { width: 1920, height: 1080 }, + '2k': { width: 2560, height: 1440 }, + '4k': { width: 3840, height: 2160 }, + '6k': { width: 6144, height: 3456 }, + '8k': { width: 7680, height: 4320 }, }; + + // Default to Full HD if no match found in the screen resolution map + const { width, height } = screenResolutionMap[screenQuality.value] || { width: 1920, height: 1080 }; + + return screenBaseConstraints(width, height); } // #################################################### @@ -1946,6 +1913,34 @@ class RoomClient { return { encodings, codec }; } + // #################################################### + // HELPERS + // #################################################### + + createButton(id, className) { + const button = document.createElement('button'); + button.id = id; + button.className = className; + return button; + } + + getConsumerIdByProducerId(producerId) { + for (let [consumerId, consumer] of this.consumers.entries()) { + if (consumer._producerId === producerId) { + return consumerId; + } + } + return null; + } + + getProducerIdByConsumerId(consumerId) { + const consumer = this.consumers.get(consumerId); + if (consumer) { + return consumer._producerId; + } + return null; + } + // #################################################### // PRODUCER // #################################################### @@ -1987,17 +1982,20 @@ class RoomClient { } async handleProducer(id, type, stream) { - let elem, vb, vp, ts, d, p, i, au, pip, fs, pm, pb, pn; + let elem, vb, vp, ts, d, p, i, au, pip, fs, pm, pb, pn, pv, mv; switch (type) { case mediaType.video: case mediaType.screen: let isScreen = type === mediaType.screen; this.removeVideoOff(this.peer_id); + d = document.createElement('div'); d.className = 'Camera'; d.id = id + '__video'; + elem = document.createElement('video'); elem.setAttribute('id', id); + elem.setAttribute('volume', this.peer_id + '___pVolume'); !isScreen && elem.setAttribute('name', this.peer_id); elem.setAttribute('playsinline', true); elem.controls = isVideoControlsOn; @@ -2007,35 +2005,32 @@ class RoomClient { elem.poster = image.poster; elem.style.objectFit = isScreen || isBroadcastingEnabled ? 'contain' : 'var(--videoObjFit)'; elem.className = this.isMobileDevice || isScreen ? '' : 'mirror'; + vb = document.createElement('div'); - vb.setAttribute('id', this.peer_id + '__vb'); - vb.className = 'videoMenuBar fadein'; - pip = document.createElement('button'); - pip.id = id + '__pictureInPicture'; - pip.className = html.pip; - fs = document.createElement('button'); - fs.id = id + '__fullScreen'; - fs.className = html.fullScreen; - ts = document.createElement('button'); - ts.id = id + '__snapshot'; - ts.className = html.snapshot; - pn = document.createElement('button'); - pn.id = id + '__pin'; - pn.className = html.pin; - vp = document.createElement('button'); - vp.id = this.peer_id + '__vp'; - vp.className = html.videoPrivacy; - au = document.createElement('button'); - au.id = this.peer_id + '__audio'; - au.className = this.peer_info.peer_audio ? html.audioOn : html.audioOff; + vb.id = id + '__vb'; + vb.className = 'videoMenuBar hidden'; + + pip = this.createButton(id + '__pictureInPicture', html.pip); + fs = this.createButton(id + '__fullScreen', html.fullScreen); + ts = this.createButton(id + '__snapshot', html.snapshot); + mv = this.createButton(id + '__mirror', html.mirror); + pn = this.createButton(id + '__pin', html.pin); + vp = this.createButton(this.peer_id + '__vp', html.videoPrivacy); + au = this.createButton( + this.peer_id + '__audio', + this.peer_info.peer_audio ? html.audioOn : html.audioOff, + ); au.style.cursor = 'default'; + p = document.createElement('p'); p.id = this.peer_id + '__name'; p.className = html.userName; p.innerText = (isPresenter ? '⭐️ ' : '') + this.peer_name + ' (me)'; + i = document.createElement('i'); i.id = this.peer_id + '__hand'; i.className = html.userHand; + pm = document.createElement('div'); pb = document.createElement('div'); pm.setAttribute('id', this.peer_id + '_pitchMeter'); @@ -2044,14 +2039,27 @@ class RoomClient { pb.className = 'bar'; pb.style.height = '1%'; pm.appendChild(pb); + + pv = document.createElement('input'); + pv.id = this.peer_id + '___pVolume'; + pv.type = 'range'; + pv.min = 0; + pv.max = 100; + pv.value = 100; + + BUTTONS.producerVideo.audioVolumeInput && vb.appendChild(pv); BUTTONS.producerVideo.muteAudioButton && vb.appendChild(au); BUTTONS.producerVideo.videoPrivacyButton && !isScreen && vb.appendChild(vp); BUTTONS.producerVideo.snapShotButton && vb.appendChild(ts); BUTTONS.producerVideo.videoPictureInPicture && this.isVideoPictureInPictureSupported && vb.appendChild(pip); + BUTTONS.producerVideo.videoMirrorButton && vb.appendChild(mv); BUTTONS.producerVideo.fullScreenButton && this.isVideoFullScreenSupported && vb.appendChild(fs); + if (!this.isMobileDevice) vb.appendChild(pn); + + vb.appendChild(p); d.appendChild(elem); d.appendChild(pm); d.appendChild(i); @@ -2068,21 +2076,10 @@ class RoomClient { d.appendChild(zp); } d.appendChild(p); - this.videoMediaContainer.appendChild(vb); + /* this.videoMediaContainer.appendChild(vb); this.videoMediaContainer.appendChild(d); let pv; - - // Create and append peer name header - const peerNameHeader = document.createElement('div'); - peerNameHeader.className = 'peer-name-header'; - - const peerNameContainer = document.createElement('div'); - peerNameContainer.className = 'peer-name-container'; - - const peerNameSpan = document.createElement('span'); - peerNameSpan.className = 'peer-name'; - peerNameSpan.textContent = peer_name; // Create and append volume control to peerNameSpan pv = document.createElement('input'); @@ -2093,7 +2090,7 @@ class RoomClient { pv.value = 100; peerNameContainer.appendChild(pv); - peerNameContainer.appendChild(peerNameSpan); + peerNameContainer.appendChild(peerNameSpan); */ if (peer_info.peer_npub) { const nostrIcon = document.createElement('span'); @@ -2102,19 +2099,26 @@ class RoomClient { event.stopPropagation(); this.handleNostrClick(peer_info.peer_npub); }); - peerNameContainer.appendChild(nostrIcon); + vb.appendChild(nostrIcon); } + + // Create and append peer name header + const peerNameHeader = document.createElement('div'); + peerNameHeader.className = 'peer-name-header'; + + const peerNameContainer = document.createElement('div'); + peerNameContainer.className = 'peer-name-container'; + + const peerNameSpan = document.createElement('span'); + peerNameSpan.className = 'peer-name'; + peerNameSpan.textContent = peer_name; + peerNameHeader.appendChild(peerNameContainer); vb.appendChild(peerNameHeader); - if (this.isMobileDevice) { - peerNameHeader.style.backgroundImage = `url('${peer_info.peer_url || image.avatar}')`; - } - this.addCloseButton(peerNameHeader, vb); - - // Update the event listener + /* // Update the event listener d.addEventListener('click', (event) => { if (!event.target.closest('.' + html.zapIcon.split(' ')[0])) { const menuBarElement = vb; // Reference to the videoMenuBar element @@ -2123,38 +2127,61 @@ class RoomClient { }); - this.attachMediaStream(elem, stream, type, 'Producer'); + this.attachMediaStream(elem, stream, type, 'Producer'); */ + //d.appendChild(vb); + document.body.appendChild(vb); + this.videoMediaContainer.appendChild(d); + + await this.attachMediaStream(elem, stream, type, 'Producer'); + this.myVideoEl = elem; this.isVideoPictureInPictureSupported && this.handlePIP(elem.id, pip.id); this.isVideoFullScreenSupported && this.handleFS(elem.id, fs.id); + this.handleVB(d.id, vb.id); this.handleDD(elem.id, this.peer_id, true); this.handleTS(elem.id, ts.id); + this.handleMV(elem.id, mv.id); this.handlePN(elem.id, pn.id, d.id, isScreen); this.handleZV(elem.id, d.id, this.peer_id); + this.handlePV(id + '___' + pv.id); + + this.setAV( + this.audioConsumers.get(this.peer_id + '___pVolume'), + this.peer_id + '___pVolume', + this.peer_info.peer_audio_volume, + ); + if (!isScreen) this.handleVP(elem.id, vp.id); + this.popupPeerInfo(p.id, this.peer_info); this.checkPeerInfoStatus(this.peer_info); + if (isScreen) pn.click(); + handleAspectRatio(); - if (!this.isMobileDevice) { - this.setTippy(pn.id, 'Toggle Pin', 'bottom'); - this.setTippy(pip.id, 'Toggle picture in picture', 'bottom'); - this.setTippy(ts.id, 'Snapshot', 'bottom'); - this.setTippy(vp.id, 'Toggle video privacy', 'bottom'); - this.setTippy(au.id, 'Audio status', 'bottom'); - } + console.log('[addProducer] Video-element-count', this.videoMediaContainer.childElementCount); break; case mediaType.audio: elem = document.createElement('audio'); - elem.id = id + '__localAudio'; + elem.setAttribute('id', id); + elem.setAttribute('name', id + '__localAudio'); + elem.setAttribute('volume', this.peer_id + '___pVolume'); elem.controls = false; elem.autoplay = true; elem.muted = true; elem.volume = 0; this.myAudioEl = elem; this.localAudioEl.appendChild(elem); + await this.attachMediaStream(elem, stream, type, 'Producer'); + + const audioConsumerId = this.peer_id + '___pVolume'; + this.audioConsumers.set(audioConsumerId, elem.id); + + this.setAV(elem.id, audioConsumerId, this.peer_info.peer_audio_volume); + this.handlePV(elem.id + '___' + audioConsumerId); + console.log('[addProducer] audio-element-count', this.localAudioEl.childElementCount); break; default: @@ -2251,10 +2278,12 @@ class RoomClient { if (type !== mediaType.audio) { const elem = this.getId(producer_id); const d = this.getId(producer_id + '__video'); + const vb = this.getId(producer_id + '__vb'); elem.srcObject.getTracks().forEach(function (track) { track.stop(); }); d.parentNode.removeChild(d); + vb.parentNode.removeChild(vb); //alert(this.pinnedVideoPlayerId + '==' + producer_id); if (this.isVideoPinned && this.pinnedVideoPlayerId == producer_id) { @@ -2271,7 +2300,7 @@ class RoomClient { } if (type === mediaType.audio) { - const au = this.getId(producer_id + '__localAudio'); + const au = this.getName(producer_id + '__localAudio'); au.srcObject.getTracks().forEach(function (track) { track.stop(); }); @@ -2379,12 +2408,12 @@ class RoomClient { } consumer.on('trackended', () => { - console.log('Consumer track end', { id: consumer.id }); + console.log('Consumer track end', { id: consumer.id, type }); this.removeConsumer(consumer.id, consumer.kind); }); consumer.on('transportclose', () => { - console.log('Consumer transport close', { id: consumer.id }); + console.log('Consumer transport close', { id: consumer.id, type }); this.removeConsumer(consumer.id, consumer.kind); }); @@ -2431,7 +2460,7 @@ class RoomClient { }; } - handleConsumer(id, type, stream, peer_name, peer_info) { + /* handleConsumer(id, type, stream, peer_name, peer_info) { const { peer_id: remotePeerId, peer_audio: remotePeerAudio, @@ -2451,24 +2480,45 @@ class RoomClient { return button; }; - let elem, vb, d, p, i, cm, au, pip, fs, ts, sf, sm, sv, gl, ban, ko, pb, pm, pv, pn; + let elem, vb, d, p, i, cm, au, pip, fs, ts, sf, sm, sv, gl, ban, ko, pb, pm, pv, pn; */ + async handleConsumer(id, type, stream, peer_name, peer_info) { + let elem, vb, d, p, i, cm, au, pip, fs, ts, sf, sm, sv, gl, ban, ko, pb, pm, pv, pn, ha, mv; + + let eDiv, eBtn, eVc; // expand buttons + + console.log('PEER-INFO', peer_info); + + const remotePeerId = peer_info.peer_id; + const remoteIsScreen = type == mediaType.screen; + const remotePeerAudio = peer_info.peer_audio; + const remotePeerAudioVolume = peer_info.peer_audio_volume; + const remotePrivacyOn = peer_info.peer_video_privacy; + const remotePeerPresenter = peer_info.peer_presenter; + const remoteLNAddress = peer_info.peer_lnaddress; + const peer_npub = peer_info.peer_npub; + const peer_url = peer_info.peer_url; + + switch (type) { case mediaType.video: case mediaType.screen: this.removeVideoOff(remotePeerId); + d = document.createElement('div'); d.className = 'Camera'; d.id = id + '__video'; + elem = document.createElement('video'); elem.setAttribute('id', id); - !isScreen && elem.setAttribute('name', remotePeerId); + elem.setAttribute('volumeBar', remotePeerId + '___pVolume'); + !remoteIsScreen && elem.setAttribute('name', remotePeerId); elem.setAttribute('playsinline', true); elem.controls = isVideoControlsOn; elem.autoplay = true; elem.className = ''; elem.poster = image.poster; - elem.style.objectFit = isScreen || isBroadcastingEnabled ? 'contain' : 'var(--videoObjFit)'; + /* elem.style.objectFit = isScreen || isBroadcastingEnabled ? 'contain' : 'var(--videoObjFit)'; vb = document.createElement('div'); vb.setAttribute('id', remotePeerId + '__vb'); vb.className = 'videoMenuBar fadein'; @@ -2492,25 +2542,9 @@ class RoomClient { pv.max = 100; pv.value = 100; peerNameContainer.appendChild(peerNameSpan); - peerNameContainer.appendChild(pv); - - if (peer_npub) { - const nostrIcon = document.createElement('span'); - nostrIcon.className = html.nostrIcon + ' nostr-icon-inline'; - nostrIcon.addEventListener('click', (event) => { - event.stopPropagation(); - this.handleNostrClick(peer_npub); - }); - peerNameContainer.appendChild(nostrIcon); - } - - peerNameHeader.appendChild(peerNameContainer); - vb.appendChild(peerNameHeader); - - if (this.isMobileDevice) { - peerNameHeader.style.backgroundImage = `url('${peer_url || image.avatar}')`; - } - this.addCloseButton(peerNameHeader, vb); + peerNameContainer.appendChild(pv); */ + + /* this.addCloseButton(peerNameHeader, vb); pip = createButton(id + '__pictureInPicture', html.pip); fs = createButton(id + '__fullScreen', html.fullScreen); @@ -2523,15 +2557,56 @@ class RoomClient { au = createButton(remotePeerId + '__audio', remotePeerAudio ? html.audioOn : html.audioOff); gl = createButton(id + '___' + remotePeerId + '___geoLocation', html.geolocation); ban = createButton(id + '___' + remotePeerId + '___ban', html.ban); - ko = createButton(id + '___' + remotePeerId + '___kickOut', html.kickOut); + ko = createButton(id + '___' + remotePeerId + '___kickOut', html.kickOut); */ + elem.style.objectFit = remoteIsScreen || isBroadcastingEnabled ? 'contain' : 'var(--videoObjFit)'; + + vb = document.createElement('div'); + vb.id = id + '__vb'; + vb.className = 'videoMenuBar hidden'; + + if (p) { + const nostrIcon = document.createElement('span'); + nostrIcon.className = html.nostrIcon + ' nostr-icon-inline'; + nostrIcon.addEventListener('click', (event) => { + event.stopPropagation(); + this.handleNostrClick(peer_npub); + }); + vb.appendChild(nostrIcon); + } + + eDiv = document.createElement('div'); + eDiv.className = 'expand-video'; + + eBtn = this.createButton(remotePeerId + '_videoExpandBtn', html.expand); + + eVc = document.createElement('div'); + eVc.className = 'expand-video-content'; + + pip = this.createButton(id + '__pictureInPicture', html.pip); + mv = this.createButton(id + '__videoMirror', html.mirror); + fs = this.createButton(id + '__fullScreen', html.fullScreen); + ts = this.createButton(id + '__snapshot', html.snapshot); + pn = this.createButton(id + '__pin', html.pin); + ha = this.createButton(id + '__hideALL', html.hideALL + ' focusMode'); + sf = this.createButton(id + '___' + remotePeerId + '___sendFile', html.sendFile); + sm = this.createButton(id + '___' + remotePeerId + '___sendMsg', html.sendMsg); + sv = this.createButton(id + '___' + remotePeerId + '___sendVideo', html.sendVideo); + cm = this.createButton(id + '___' + remotePeerId + '___video', html.videoOn); + au = this.createButton(remotePeerId + '__audio', remotePeerAudio ? html.audioOn : html.audioOff); + gl = this.createButton(id + '___' + remotePeerId + '___geoLocation', html.geolocation); + ban = this.createButton(id + '___' + remotePeerId + '___ban', html.ban); + ko = this.createButton(id + '___' + remotePeerId + '___kickOut', html.kickOut); + i = document.createElement('i'); i.id = remotePeerId + '__hand'; i.className = html.userHand; + p = document.createElement('p'); p.id = remotePeerId + '__name'; p.className = html.userName; - p.innerText = (remotePeerPresenter ? '⭐️ ' : ' ') + peer_name; + p.innerText = (remotePeerPresenter ? '⭐️ ' : '') + peer_name; + pm = document.createElement('div'); pb = document.createElement('div'); pm.setAttribute('id', remotePeerId + '__pitchMeter'); @@ -2541,7 +2616,7 @@ class RoomClient { pb.style.height = '1%'; pm.appendChild(pb); - const appendButtons = (parent, buttons) => { + /* const appendButtons = (parent, buttons) => { buttons.forEach(button => parent.appendChild(button)); }; @@ -2576,8 +2651,62 @@ class RoomClient { } appendButtons(vb, [ts, pip, fs]); - if (!this.isMobileDevice) vb.appendChild(pn); + if (!this.isMobileDevice) vb.appendChild(pn); */ + + const peerNameHeader = document.createElement('div'); + peerNameHeader.className = 'peer-name-header'; + + const peerNameContainer = document.createElement('div'); + peerNameContainer.className = 'peer-name-container'; + + const peerNameSpan = document.createElement('span'); + peerNameSpan.className = 'peer-name'; + peerNameSpan.textContent = peer_name; + + this.addCloseVBButton(peerNameHeader); + + peerNameContainer.appendChild(peerNameSpan); + + pv = document.createElement('input'); + pv.id = remotePeerId + '___pVolume'; + pv.type = 'range'; + pv.min = 0; + pv.max = 100; + pv.value = 100; + + BUTTONS.consumerVideo.audioVolumeInput && peerNameContainer.appendChild(pv); + peerNameHeader.appendChild(peerNameContainer); + + vb.appendChild(peerNameHeader); + eVc.appendChild(peerNameHeader); + if (this.isMobileDevice) { + peerNameHeader.style.backgroundImage = `url('${peer_url || image.avatar}')`; + } + + BUTTONS.consumerVideo.sendMessageButton && eVc.appendChild(sm); + BUTTONS.consumerVideo.sendFileButton && eVc.appendChild(sf); + BUTTONS.consumerVideo.sendVideoButton && eVc.appendChild(sv); + BUTTONS.consumerVideo.geolocationButton && eVc.appendChild(gl); + BUTTONS.consumerVideo.banButton && eVc.appendChild(ban); + BUTTONS.consumerVideo.ejectButton && eVc.appendChild(ko); + + eDiv.appendChild(eBtn); + eDiv.appendChild(eVc); + vb.appendChild(eDiv); + + vb.appendChild(au); + vb.appendChild(cm); + BUTTONS.consumerVideo.snapShotButton && vb.appendChild(ts); + BUTTONS.consumerVideo.videoPictureInPicture && + this.isVideoPictureInPictureSupported && + vb.appendChild(pip); + BUTTONS.consumerVideo.videoMirrorButton && vb.appendChild(mv); + BUTTONS.consumerVideo.fullScreenButton && this.isVideoFullScreenSupported && vb.appendChild(fs); + BUTTONS.consumerVideo.focusVideoButton && vb.appendChild(ha); + + if (!this.isMobileDevice) vb.appendChild(pn); + d.appendChild(elem); d.appendChild(i); @@ -2598,34 +2727,51 @@ class RoomClient { } }); - this.videoMediaContainer.appendChild(vb); + document.body.appendChild(vb); this.videoMediaContainer.appendChild(d); - this.attachMediaStream(elem, stream, type, 'Consumer'); + await this.attachMediaStream(elem, stream, type, 'Consumer'); this.isVideoPictureInPictureSupported && this.handlePIP(elem.id, pip.id); this.isVideoFullScreenSupported && this.handleFS(elem.id, fs.id); + this.handleVB(d.id, vb.id); this.handleDD(elem.id, remotePeerId); this.handleTS(elem.id, ts.id); + this.handleMV(elem.id, mv.id); this.handleSF(sf.id); this.handleSM(sm.id, peer_name); this.handleSV(sv.id); BUTTONS.consumerVideo.muteVideoButton && this.handleCM(cm.id); BUTTONS.consumerVideo.muteAudioButton && this.handleAU(au.id); - this.handlePV(id + '___' + pv.id); + this.handleCV(id + '___' + pv.id); this.handleGL(gl.id); this.handleBAN(ban.id); this.handleKO(ko.id); - this.handlePN(elem.id, pn.id, d.id, isScreen); + this.handlePN(elem.id, pn.id, d.id, remoteIsScreen); this.handleZV(elem.id, d.id, remotePeerId); this.popupPeerInfo(p.id, peer_info); this.checkPeerInfoStatus(peer_info); - if (!isScreen && remotePrivacyOn) this.setVideoPrivacyStatus(remotePeerId, remotePrivacyOn); - if (isScreen) pn.click(); - this.sound('joined'); - handleAspectRatio(); + + if (!remoteIsScreen && remotePrivacyOn) this.setVideoPrivacyStatus(remotePeerId, remotePrivacyOn); + + if (remoteIsScreen && !isHideALLVideosActive) pn.click(); + + if (isHideALLVideosActive) { + isHideALLVideosActive = false; + const children = this.videoMediaContainer.children; + const btnsHA = document.querySelectorAll('.focusMode'); + for (let child of children) { + child.style.display = 'block'; + } + btnsHA.forEach((btn) => { + btn.style.color = 'white'; + }); + } + console.log('[addConsumer] Video-element-count', this.videoMediaContainer.childElementCount); + if (!this.isMobileDevice) { this.setTippy(pn.id, 'Toggle Pin', 'bottom'); this.setTippy(pip.id, 'Toggle picture in picture', 'bottom'); + this.setTippy(mv.id, 'Toggle mirror', 'bottom'); this.setTippy(ts.id, 'Snapshot', 'bottom'); this.setTippy(sf.id, 'Send file', 'bottom'); this.setTippy(sm.id, 'Send message', 'bottom'); @@ -2637,25 +2783,45 @@ class RoomClient { this.setTippy(ban.id, 'Ban', 'bottom'); this.setTippy(ko.id, 'Eject', 'bottom'); } + + // Use helper function to set audio volume + this.setAV( + this.audioConsumers.get(remotePeerId + '___pVolume'), + remotePeerId + '___pVolume', + remotePeerAudioVolume, + true, + ); + this.setPeerAudio(remotePeerId, remotePeerAudio); + + handleAspectRatio(); + this.sound('joined'); break; case mediaType.audio: elem = document.createElement('audio'); - elem.id = id; + elem.setAttribute('id', id); + elem.setAttribute('volumeBar', remotePeerId + '___pVolume'); elem.autoplay = true; elem.audio = 1.0; this.remoteAudioEl.appendChild(elem); - this.attachMediaStream(elem, stream, type, 'Consumer'); - let audioConsumerId = remotePeerId + '___pVolume'; + + await this.attachMediaStream(elem, stream, type, 'Consumer'); + + // Store audio consumer and set volume + const audioConsumerId = remotePeerId + '___pVolume'; this.audioConsumers.set(audioConsumerId, id); - let inputPv = this.getId(audioConsumerId); - if (inputPv) { - this.handlePV(id + '___' + audioConsumerId); - this.setPeerAudio(remotePeerId, remotePeerAudio); - } + + // Use helper function to set audio volume + this.setAV(id, audioConsumerId, remotePeerAudioVolume, true); + this.handleCV(id + '___' + audioConsumerId); + + this.setPeerAudio(remotePeerId, remotePeerAudio); + if (sinkId && speakerSelect.value) { this.changeAudioDestination(elem); } + + //elem.addEventListener('play', () => { elem.volume = 0.1 }); console.log('[Add audioConsumers]', this.audioConsumers); break; default: @@ -2677,6 +2843,8 @@ class RoomClient { if (consumer_kind === 'video') { const d = this.getId(consumer_id + '__video'); + const vb = this.getId(consumer_id + '__vb'); + if (d) { // Check if video is in focus-mode... if (d.hasAttribute('focus-mode')) { @@ -2686,6 +2854,8 @@ class RoomClient { } } d.parentNode.removeChild(d); + vb.parentNode.removeChild(vb); + //alert(this.pinnedVideoPlayerId + '==' + consumer_id); if (this.isVideoPinned && this.pinnedVideoPlayerId == consumer_id) { this.removeVideoPinMediaContainer(); @@ -2717,6 +2887,7 @@ class RoomClient { } } + this.consumers.get(consumer_id).close(); this.consumers.delete(consumer_id); this.sound('left'); } @@ -2729,7 +2900,7 @@ class RoomClient { console.log('setVideoOff', peer_info); let d, vb, i, h, au, sf, sm, sv, gl, ban, ko, p, pm, pb, pv; - const { peer_id, peer_name, peer_audio, peer_presenter, peer_npub, peer_lnaddress } = peer_info; + const { peer_id, peer_name, peer_audio, peer_presenter, peer_npub, peer_lnaddress, peer_url } = peer_info; // Error handling: Check if peer_id is valid if (!peer_id) { @@ -2739,7 +2910,7 @@ class RoomClient { this.removeVideoOff(peer_id); - // Create main container + /* // Create main container d = document.createElement('div'); d.className = 'Camera'; d.id = peer_id + '__videoOff'; @@ -2763,19 +2934,10 @@ class RoomClient { if (this.isMobileDevice) { peerNameHeader.style.backgroundImage = `url('${peer_info.peer_url || image.avatar}')`; - } + } */ - if (peer_npub) { - const nostrIcon = document.createElement('span'); - nostrIcon.className = html.nostrIcon + ' nostr-icon-inline'; - nostrIcon.addEventListener('click', (event) => { - event.stopPropagation(); - this.handleNostrClick(peer_npub); - }); - peerNameContainer.appendChild(nostrIcon); - } - peerNameHeader.appendChild(peerNameContainer); + /* peerNameHeader.appendChild(peerNameContainer); vb.appendChild(peerNameHeader); // Create audio button @@ -2818,24 +2980,82 @@ class RoomClient { ko.id = 'remotePeer___' + peer_id + '___kickOut'; ko.className = html.kickOut; - // Append buttons to video menu bar + // Append buttons to video menu bar */ + + d = document.createElement('div'); + d.className = 'Camera'; + d.id = peer_id + '__videoOff'; + + vb = document.createElement('div'); + vb.id = peer_id + '__vb'; + vb.className = 'videoMenuBar hidden'; + + au = this.createButton(peer_id + '__audio', peer_audio ? html.audioOn : html.audioOff); + + if (peer_npub) { + console.log('peer_npub', peer_npub); + const nostrIcon = document.createElement('span'); + nostrIcon.className = html.nostrIcon + ' nostr-icon-inline'; + nostrIcon.addEventListener('click', (event) => { + event.stopPropagation(); + this.handleNostrClick(peer_npub); + }); + vb.appendChild(nostrIcon); + } + + pv = document.createElement('input'); + pv.id = peer_id + '___pVolume'; + pv.type = 'range'; + pv.min = 0; + pv.max = 100; + pv.value = 100; + + if (remotePeer) { + sf = this.createButton('remotePeer___' + peer_id + '___sendFile', html.sendFile); + sm = this.createButton('remotePeer___' + peer_id + '___sendMsg', html.sendMsg); + sv = this.createButton('remotePeer___' + peer_id + '___sendVideo', html.sendVideo); + gl = this.createButton('remotePeer___' + peer_id + '___geoLocation', html.geolocation); + ban = this.createButton('remotePeer___' + peer_id + '___ban', html.ban); + ko = this.createButton('remotePeer___' + peer_id + '___kickOut', html.kickOut); + } + + // Create and append avatar image + i = document.createElement('img'); + i.className = 'videoAvatarImage center'; + i.id = peer_id + '__img'; + d.appendChild(i); + + + p = document.createElement('p'); + p.id = peer_id + '__name'; + p.className = html.userName; + p.innerText = (peer_presenter ? '⭐️ ' : '') + peer_name + (remotePeer ? '' : ' (me) '); + + h = document.createElement('i'); + h.id = peer_id + '__hand'; + h.className = html.userHand; + + pm = document.createElement('div'); + pb = document.createElement('div'); + pm.setAttribute('id', peer_id + '__pitchMeter'); + pb.setAttribute('id', peer_id + '__pitchBar'); + pm.className = 'speechbar'; + pb.className = 'bar'; + pb.style.height = '1%'; + pm.appendChild(pb); + + if (remotePeer) { BUTTONS.videoOff.ejectButton && vb.appendChild(ko); BUTTONS.videoOff.banButton && vb.appendChild(ban); BUTTONS.videoOff.geolocationButton && vb.appendChild(gl); BUTTONS.videoOff.sendVideoButton && vb.appendChild(sv); BUTTONS.videoOff.sendFileButton && vb.appendChild(sf); BUTTONS.videoOff.sendMessageButton && vb.appendChild(sm); - BUTTONS.videoOff.audioVolumeInput && !this.isMobileDevice && vb.appendChild(pv); } - + BUTTONS.videoOff.audioVolumeInput && vb.appendChild(pv); + vb.appendChild(au); - // Create and append avatar image - i = document.createElement('img'); - i.className = 'videoAvatarImage center'; - i.id = peer_id + '__img'; - d.appendChild(i); - // Add lightning address button if available if (peer_lnaddress) { const zp = document.createElement('button'); @@ -2870,30 +3090,37 @@ class RoomClient { d.appendChild(p); d.appendChild(h); d.appendChild(pm); - this.videoMediaContainer.appendChild(vb); - - // Append the main container to the video media container + //d.appendChild(vb); + + document.body.appendChild(vb); this.videoMediaContainer.appendChild(d); // Set up event handlers BUTTONS.videoOff.muteAudioButton && this.handleAU(au.id); - + if (remotePeer) { - this.handlePV('remotePeer___' + peer_id + '___pVolume'); + this.handleCV('remotePeer___' + pv.id); this.handleSM(sm.id); this.handleSF(sf.id); this.handleSV(sv.id); this.handleGL(gl.id); this.handleBAN(ban.id); this.handleKO(ko.id); + } else { + this.handlePV(this.audioConsumers.get(pv.id) + '___' + pv.id); } - + + this.handleVB(d.id, vb.id); this.handleDD(d.id, peer_id, !remotePeer); this.popupPeerInfo(p.id, peer_info); - this.setVideoAvatarImgName(i.id, peer_name, peer_info.peer_url); - - i.style.display = 'block'; - + this.checkPeerInfoStatus(peer_info); + this.setVideoAvatarImgName(i.id, peer_name, peer_url); + this.getId(i.id).style.display = 'block'; + + handleAspectRatio(); + + if (isParticipantsListOpen) getRoomParticipants(); + if (!this.isMobileDevice && remotePeer) { this.setTippy(sm.id, 'Send message', 'bottom'); this.setTippy(sf.id, 'Send file', 'bottom'); @@ -2904,7 +3131,6 @@ class RoomClient { this.setTippy(ban.id, 'Ban', 'bottom'); this.setTippy(ko.id, 'Eject', 'bottom'); } - remotePeer ? this.setPeerAudio(peer_id, peer_audio) : this.setIsAudio(peer_id, peer_audio); // Update the event listener @@ -2915,12 +3141,11 @@ class RoomClient { } }); - this.addCloseButton(peerNameHeader, vb); + // This was close button for the menu bar which may be needed after merge. + // this.addCloseButton(peerNameHeader, vb); console.log('[setVideoOff] Video-element-count', this.videoMediaContainer.childElementCount); - handleAspectRatio(); - if (isParticipantsListOpen) getRoomParticipants(); wbUpdate(); this.editorUpdate(); @@ -3105,7 +3330,11 @@ class RoomClient { } removeVideoOff(peer_id) { - let pvOff = this.getId(peer_id + '__videoOff'); + const pvOff = this.getId(peer_id + '__videoOff'); + const vb = this.getId(peer_id + '__vb'); + + if (vb) vb.parentNode.removeChild(vb); + if (pvOff) { pvOff.parentNode.removeChild(pvOff); handleAspectRatio(); @@ -3293,7 +3522,7 @@ class RoomClient { } } - setVideoAvatarImgName(elemId, peer_name, peer_url) { + setVideoAvatarImgName(elemId, peer_name, peer_url, remotePeer = false) { let elem = this.getId(elemId); if (cfg.useAvatarSvg) { console.log('setVideoAvatarImgName: ', peer_name, 'url: ', peer_url); @@ -3377,17 +3606,19 @@ class RoomClient { setPeerAudio(peer_id, status) { console.log('Set peer audio enabled: ' + status); const audioStatus = this.getPeerAudioBtn(peer_id); // producer, consumers - const audioVolume = this.getPeerAudioVolumeBtn(peer_id); // consumers + const audioVolume = this.getPeerAudioVolumeBar(peer_id); // consumers if (audioStatus) audioStatus.className = status ? html.audioOn : html.audioOff; if (audioVolume) status ? show(audioVolume) : hide(audioVolume); } setIsAudio(peer_id, status) { if (!isBroadcastingEnabled || (isBroadcastingEnabled && isPresenter)) { - console.log('Set audio enabled: ' + status); + console.log('Set local audio enabled: ' + status); this.peer_info.peer_audio = status; const audioStatus = this.getPeerAudioBtn(peer_id); // producer, consumers + const audioVolume = this.getPeerAudioVolumeBar(peer_id); // consumers if (audioStatus) audioStatus.className = status ? html.audioOn : html.audioOff; + if (audioVolume) status ? show(audioVolume) : hide(audioVolume); } } @@ -3395,7 +3626,7 @@ class RoomClient { if (!isBroadcastingEnabled || (isBroadcastingEnabled && isPresenter)) { this.peer_info.peer_video = status; if (!this.peer_info.peer_video) { - console.log('Set video enabled: ' + status); + console.log('Set local video enabled: ' + status); this.setVideoOff(this.peer_info, false); this.sendVideoOff(); } @@ -3406,7 +3637,7 @@ class RoomClient { if (!isBroadcastingEnabled || (isBroadcastingEnabled && isPresenter)) { this.peer_info.peer_screen = status; if (!this.peer_info.peer_screen && !this.peer_info.peer_video) { - console.log('Set screen enabled: ' + status); + console.log('Set local screen enabled: ' + status); this.setVideoOff(this.peer_info, false); this.sendVideoOff(); } @@ -3446,7 +3677,7 @@ class RoomClient { } getName(name) { - return document.getElementsByName(name); + return document.getElementsByName(name)[0]; } getEcN(cn) { @@ -3466,7 +3697,7 @@ class RoomClient { return this.getId(peer_id + '__audio'); } - getPeerAudioVolumeBtn(peer_id) { + getPeerAudioVolumeBar(peer_id) { return this.getId(peer_id + '___pVolume'); } @@ -3528,7 +3759,7 @@ class RoomClient { } } - msgPopup(type, message) { + msgPopup(type, message, timer = 3000) { switch (type) { case 'warning': case 'error': @@ -3573,7 +3804,7 @@ class RoomClient { showConfirmButton: false, timerProgressBar: true, toast: true, - timer: 3000, + timer: timer, }); Toast.fire({ icon: 'info', @@ -3751,16 +3982,45 @@ class RoomClient { // #################################################### isFullScreenSupported() { - return ( + const fsSupported = document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || - document.msFullscreenEnabled || - ('webkitEnterFullscreen' in document.createElement('video')) - ); + document.msFullscreenEnabled; + + fsSupported ? this.handleFullScreenEvents() : (this.getId('fullScreenButton').style.display = 'none'); + + return fsSupported; + } + + handleFullScreenEvents() { + document.addEventListener('fullscreenchange', (e) => { + const fullscreenElement = document.fullscreenElement; + if (!fullscreenElement) { + const fullScreenIcon = this.getId('fullScreenIcon'); + fullScreenIcon.className = html.fullScreenOff; + this.isDocumentOnFullScreen = false; + } + }); + } + + toggleRoomFullScreen() { + const fullScreenIcon = this.getId('fullScreenIcon'); + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen(); + fullScreenIcon.className = html.fullScreenOn; + this.isDocumentOnFullScreen = true; + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + fullScreenIcon.className = html.fullScreenOff; + this.isDocumentOnFullScreen = false; + } + } } toggleFullScreen(elem = null) { + if (this.isDocumentOnFullScreen) return; const element = elem ? elem : document.documentElement; const fullScreen = this.isFullScreen(); if (fullScreen) { @@ -3811,7 +4071,7 @@ class RoomClient { handleFS(elemId, fsId) { let videoPlayer = this.getId(elemId); let btnFs = this.getId(fsId); - if (btnFs) { + if (videoPlayer && btnFs) { this.setTippy(fsId, 'Full screen', 'bottom'); btnFs.addEventListener('click', (e) => { e.stopPropagation(); // Prevent click from reaching container @@ -3820,22 +4080,12 @@ class RoomClient { } this.toggleFullScreen(videoPlayer); }); - } - if (videoPlayer) { - videoPlayer.addEventListener('dblclick', (e) => { - if (videoPlayer.classList.contains('videoCircle')) { - return this.userLog('info', 'Full Screen not allowed if video on privacy mode', 'top-end'); - } - if (!videoPlayer.hasAttribute('controls')) { - if ((this.isMobileDevice && this.isVideoOnFullScreen) || !this.isMobileDevice) { - this.toggleFullScreen(videoPlayer); - } + videoPlayer.addEventListener('fullscreenchange', (e) => { + if (!document.fullscreenElement) { + videoPlayer.style.pointerEvents = 'auto'; + this.isVideoOnFullScreen = false; } }); - videoPlayer.addEventListener('fullscreenchange', () => { - this.isVideoOnFullScreen = !!document.fullscreenElement; - videoPlayer.style.pointerEvents = 'auto'; - }); videoPlayer.addEventListener('webkitfullscreenchange', () => { this.isVideoOnFullScreen = !!document.webkitFullscreenElement; videoPlayer.style.pointerEvents = 'auto'; @@ -3880,7 +4130,7 @@ class RoomClient { } } - handlePN(elemId, pnId, camId, isScreen = false, isAvatar = false) { + handlePN(elemId, pnId, camId, remoteIsScreen = false, isAvatar = false) { let videoPlayer = this.getId(elemId); let btnPn = this.getId(pnId); let cam = this.getId(camId); @@ -3907,13 +4157,14 @@ class RoomClient { if (this.isScreenAllowed) return; return this.msgPopup('toast', 'Another video seems pinned, unpin it before to pin this one'); } - if (!isScreen && !isBroadcastingEnabled) videoPlayer.style.objectFit = 'var(--videoObjFit)'; + if (!remoteIsScreen && !isBroadcastingEnabled) videoPlayer.style.objectFit = 'var(--videoObjFit)'; this.videoPinMediaContainer.removeChild(cam); cam.className = 'Camera'; this.videoMediaContainer.appendChild(cam); this.removeVideoPinMediaContainer(); setColor(btnPn, 'white'); } + this.resizeVideoMenuBar(); handleAspectRatio(); }); @@ -4016,6 +4267,67 @@ class RoomClient { } } + // #################################################### + // HANDLE VIDEO AND MENU BAR + // #################################################### + + handleVB(videoId, videoBarId) { + const videoPlayer = this.getId(videoId); + const videoBar = this.getId(videoBarId); + if (videoPlayer && videoBar) { + videoPlayer.addEventListener('click', () => { + const videoMenuBar = rc.getEcN('videoMenuBar'); + for (let i = 0; i < videoMenuBar.length; i++) { + const menuBar = videoMenuBar[i]; + if (menuBar.id != videoBarId) { + hide(menuBar); + } + } + + rc.resizeVideoMenuBar(); + setCamerasBorderNone(); + + if (videoBar.classList.contains('hidden')) { + rc.sound('open'); + show(videoBar); + animateCSS(videoBar, 'fadeInDown'); + if (participantsCount > 1) videoPlayer.style.border = 'var(--videoBar-active)'; + } else { + animateCSS(videoBar, 'fadeOutUp').then((msg) => { + hide(videoBar); + }); + videoPlayer.style.border = 'none'; + } + }); + } + } + + resizeVideoMenuBar() { + const somethingPinned = + this.isVideoPinned || + this.isChatPinned || + this.isEditorPinned || + this.isPollPinned || + transcription.isPin(); + const menuBarWidth = + this.isVideoPinned || this.isChatPinned || this.isPollPinned || transcription.isPin() ? '75%' : '70%'; + const videoMenuBar = rc.getEcN('videoMenuBar'); + for (let i = 0; i < videoMenuBar.length; i++) { + const menuBar = videoMenuBar[i]; + menuBar.style.width = somethingPinned ? menuBarWidth : '100%'; + } + } + + addCloseVBButton(containerElement) { + const closeBtn = document.createElement('div'); + closeBtn.className = `${html.close} videoMenuBarClose`; + closeBtn.addEventListener('click', (e) => { + e.stopPropagation(); + hideClassElements('videoMenuBar'); + }); + containerElement.appendChild(closeBtn); + } + // #################################################### // REMOVE VIDEO PIN MEDIA CONTAINER // #################################################### @@ -4043,6 +4355,7 @@ class RoomClient { this.videoMediaContainer.style.top = 0; this.videoMediaContainer.style.width = '75%'; this.videoMediaContainer.style.height = '100%'; + this.resizeVideoMenuBar(); } videoMediaContainerUnpin() { @@ -4050,6 +4363,7 @@ class RoomClient { this.videoMediaContainer.style.right = null; this.videoMediaContainer.style.width = '100%'; this.videoMediaContainer.style.height = '100%'; + this.resizeVideoMenuBar(); } adaptVideoObjectFit(index) { @@ -4086,6 +4400,21 @@ class RoomClient { } } + // #################################################### + // HANDLE VIDEO MIRROR + // #################################################### + + handleMV(elemId, tsId) { + let videoPlayer = this.getId(elemId); + let btnMv = this.getId(tsId); + if (btnMv && videoPlayer) { + btnMv.addEventListener('click', () => { + videoPlayer.classList.toggle('mirror'); + //rc.roomMessage('toggleVideoMirror', videoPlayer.classList.contains('mirror')); + }); + } + } + // #################################################### // VIDEO CIRCLE - PRIVACY MODE // #################################################### @@ -4119,7 +4448,7 @@ class RoomClient { } setVideoPrivacyStatus(elemName, privacy) { - let videoPlayer = this.getName(elemName)[0]; + let videoPlayer = this.getName(elemName); if (!videoPlayer) return; if (privacy) { videoPlayer.classList.remove('videoDefault'); @@ -4213,7 +4542,7 @@ class RoomClient { } this.chatCenter(); this.sound('open'); - this.showPeerAboutAndMessages('all', 'all'); + this.showPeerAboutAndMessages(this.chatPeerId, this.chatPeerName); } // console.log("toggleChat: isPinned", this.isChatPinned, " isChatOpen ", this.isChatOpen) isParticipantsListOpen = !isParticipantsListOpen; @@ -4300,6 +4629,7 @@ class RoomClient { this.chatPinned(); this.isChatPinned = true; setColor(chatTogglePin, 'lime'); + this.resizeVideoMenuBar(); resizeVideoMedia(); chatRoom.style.resize = 'none'; if (!this.isMobileDevice) this.makeUnDraggable(chatRoom, chatHeader); @@ -4319,6 +4649,7 @@ class RoomClient { this.chatCenter(); this.isChatPinned = false; setColor(chatTogglePin, 'white'); + this.resizeVideoMenuBar(); resizeVideoMedia(); if (!this.isMobileDevice) this.makeDraggable(chatRoom, chatHeader); if (!this.isPlistOpen()) this.toggleShowParticipants(); @@ -4855,6 +5186,7 @@ class RoomClient { 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 @@ -5045,6 +5377,7 @@ class RoomClient { this.pollPinned(); this.isPollPinned = true; setColor(pollTogglePin, 'lime'); + this.resizeVideoMenuBar(); resizeVideoMedia(); pollRoom.style.resize = 'none'; if (!this.isMobileDevice) this.makeUnDraggable(pollRoom, pollHeader); @@ -5059,6 +5392,7 @@ class RoomClient { this.pollCenter(); this.isPollPinned = false; setColor(pollTogglePin, 'white'); + this.resizeVideoMenuBar(); resizeVideoMedia(); if (!this.isMobileDevice) this.makeDraggable(pollRoom, pollHeader); } @@ -5390,6 +5724,7 @@ class RoomClient { this.editorPinned(); this.isEditorPinned = true; setColor(editorTogglePin, 'lime'); + this.resizeVideoMenuBar(); resizeVideoMedia(); document.documentElement.style.setProperty('--editor-height', '80vh'); //if (!this.isMobileDevice) this.makeUnDraggable(editorRoom, editorHeader); @@ -5404,6 +5739,7 @@ class RoomClient { this.pollCenter(); this.isEditorPinned = false; setColor(editorTogglePin, 'white'); + this.resizeVideoMenuBar(); resizeVideoMedia(); document.documentElement.style.setProperty('--editor-height', '85vh'); //if (!this.isMobileDevice) this.makeDraggable(editorRoom, editorHeader); @@ -6499,6 +6835,10 @@ class RoomClient { } shareVideo(peer_id = 'all') { + if (this._moderator.media_cant_sharing) { + return userLog('warning', 'The moderator does not allow you to share any media', 'top-end', 6000); + } + this.sound('open'); Swal.fire({ @@ -6515,9 +6855,9 @@ class RoomClient { }).then((result) => { if (result.value) { result.value = filterXSS(result.value); - if (!this.thereAreParticipants()) { - return userLog('info', 'No participants detected', 'top-end'); - } + // if (!this.thereAreParticipants()) { + // return userLog('info', 'No participants detected', 'top-end'); + // } if (!this.isVideoTypeSupported(result.value)) { return userLog('warning', 'Something wrong, try with another Video or audio URL'); } @@ -6542,6 +6882,24 @@ class RoomClient { } } }); + + // Take URL from clipboard ex: + // https://www.youtube.com/watch?v=1ZYbU82GVz4 + + navigator.clipboard + .readText() + .then((clipboardText) => { + if (!clipboardText) return false; + const sanitizedText = filterXSS(clipboardText); + const inputElement = Swal.getInput(); + if (this.isVideoTypeSupported(sanitizedText) && inputElement) { + inputElement.value = sanitizedText; + } + return false; + }) + .catch(() => { + return false; + }); } getVideoType(url) { @@ -6571,8 +6929,8 @@ class RoomClient { } shareVideoAction(data) { - let peer_name = data.peer_name; - let action = data.action; + const { peer_name, action } = data; + switch (action) { case 'open': this.userLog('info', `${peer_name} opened the video`, 'top-end'); @@ -6590,7 +6948,7 @@ class RoomClient { openVideo(data) { let d, vb, e, video, pn; let peer_name = data.peer_name; - let video_url = data.video_url; + let video_url = data.video_url + (this.isMobileSafari ? '&enablejsapi=1&mute=1' : ''); // Safari need user interaction let is_youtube = data.is_youtube; let video_type = this.getVideoType(video_url); this.closeVideo(); @@ -6600,13 +6958,9 @@ class RoomClient { d.id = '__shareVideo'; vb = document.createElement('div'); vb.setAttribute('id', '__videoBar'); - vb.className = 'videoMenuBar fadein'; - e = document.createElement('button'); - e.className = 'fas fa-times'; - e.id = '__videoExit'; - pn = document.createElement('button'); - pn.id = '__pinUnpin'; - pn.className = html.pin; + vb.className = 'videoMenuBarShare fadein'; + e = this.createButton('__videoExit', 'fas fa-times'); + pn = this.createButton('__pinUnpin', html.pin); if (is_youtube) { video = document.createElement('iframe'); video.setAttribute('title', peer_name); @@ -6616,6 +6970,34 @@ class RoomClient { ); video.setAttribute('frameborder', '0'); video.setAttribute('allowfullscreen', true); + + // Safari on Mobile needs user interaction to unmute video + if (this.isMobileSafari) { + Swal.fire({ + allowOutsideClick: false, + allowEscapeKey: false, + background: swalBackground, + position: 'top', + // icon: 'info', + imageUrl: image.videoShare, + title: 'Unmute Video', + text: 'Tap the button below to unmute and play the video with sound.', + confirmButtonText: 'Unmute', + didOpen: () => { + // Focus on the button when the popup opens + const unmuteButton = Swal.getConfirmButton(); + if (unmuteButton) unmuteButton.focus(); + }, + }).then((result) => { + if (result.isConfirmed) { + if (video && video.contentWindow) { + // Unmute the video and play + video.contentWindow.postMessage('{"event":"command","func":"unMute","args":""}', '*'); + video.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*'); + } + } + }); + } } else { video = document.createElement('video'); video.type = video_type; @@ -6635,11 +7017,16 @@ class RoomClient { d.appendChild(vb); this.videoMediaContainer.appendChild(d); handleAspectRatio(); - let exitVideoBtn = this.getId(e.id); + + const exitVideoBtn = this.getId(e.id); exitVideoBtn.addEventListener('click', (e) => { e.preventDefault(); + if (this._moderator.media_cant_sharing) { + return userLog('warning', 'The moderator does not allow you close this media', 'top-end', 6000); + } this.closeVideo(true); }); + this.handlePN(video.id, pn.id, d.id); if (!this.isMobileDevice) { this.setTippy(pn.id, 'Toggle Pin video player', 'bottom'); @@ -6899,6 +7286,9 @@ class RoomClient { 'top-end', ); break; + case 'media_cant_sharing': + this.userLog('info', `${icons.moderator} Moderator: everyone can't share media ${status}`, 'top-end'); + break; case 'disconnect_all_on_leave': this.userLog('info', `${icons.moderator} Moderator: disconnect all on leave room ${status}`, 'top-end'); break; @@ -7299,7 +7689,7 @@ class RoomClient { if ([80, 90, 100].includes(audioVolumeTmp)) audioColorTmp = 'red'; if (!isPitchBarEnabled) { - const peerVideo = this.getName(peer_id)[0]; + const peerVideo = this.getName(peer_id); const peerAvatarImg = this.getId(peer_id + '__img'); if (peerAvatarImg) { this.applyBoxShadowEffect(peerAvatarImg, audioColorTmp, 200); @@ -7338,50 +7728,154 @@ class RoomClient { } // #################################################### - // HANDLE PEER VOLUME - // ################################################### + // HANDLE PEERS AUDIO VOLUME + // #################################################### + + handleCV(uid) { + this.handleVolumeControl(uid, true); // Consumer + } handlePV(uid) { + this.handleVolumeControl(uid, false); // Producer + } + + setAV(audioElementId, volumeElementId, volumeValue, isConsumer = false) { + const volumeInput = this.getId(volumeElementId); + const audioPlayer = this.getId(audioElementId); + const volume = volumeValue / 100; + + if (volumeInput && audioPlayer) { + console.log('Setting audio volume:', volumeValue); + volumeInput.value = volumeValue; + if (isConsumer) { + this.toggleVolumeInput(volumeInput, volumeValue); + } + this.setAudioVolume(audioPlayer, volume); + } + } + + toggleVolumeInput(volumeInput, volumeValue) { + /* + If the producer has changed the volume from the default value of 100, + disable the volume input control on the consumer side to prevent further adjustments. + Otherwise, keep the input enabled if the volume is still at 100. + */ + volumeInput.disabled = volumeValue < 100; + } + + handleVolumeControl(uid, isConsumer = true) { const words = uid.split('___'); - let peer_id = words[1] + '___pVolume'; - let audioConsumerId = this.audioConsumers.get(peer_id); - let audioConsumerPlayer = this.getId(audioConsumerId); - let inputPv = this.getId(peer_id); - if (inputPv && audioConsumerPlayer) { - inputPv.style.display = 'inline'; - inputPv.value = 100; - + const volumeInputId = `${words[1]}___pVolume`; + const audioPlayer = this.getId(isConsumer ? this.audioConsumers.get(volumeInputId) : words[0]); + const inputElement = this.getId(volumeInputId); + + if (inputElement && audioPlayer) { + show(inputElement); + inputElement.value = 100; + + let volumeUpdateTimeout; + const updateVolume = () => { - const volume = inputPv.value / 100; - this.setAudioVolume(audioConsumerPlayer, volume); + const volume = inputElement.value / 100; + this.setAudioVolume(audioPlayer, volume); + + // Update producer audio volume + if (!isConsumer) this.peer_info.peer_audio_volume = inputElement.value; + + // Clear any existing timeout to prevent sending too frequently + if (volumeUpdateTimeout) { + clearTimeout(volumeUpdateTimeout); + } + + // Set a timeout to send the update after 0.5 second + volumeUpdateTimeout = setTimeout(() => { + // Prepare the command to update peer volume + const cmd = { + type: 'peerAudio', + peer_name: this.peer_name, + [isConsumer ? 'audioConsumerId' : 'audioProducerId']: isConsumer + ? this.audioConsumers.get(volumeInputId) + : this.audioProducerId, + volumeInputId: volumeInputId, + volume: volume, + broadcast: true, + }; + this.emitCmd(cmd); + }, 500); // 0.5 second delay }; - - inputPv.addEventListener('input', updateVolume); - inputPv.addEventListener('change', updateVolume); - - if (this.isMobileDevice) { - inputPv.addEventListener('touchstart', updateVolume); - inputPv.addEventListener('touchmove', updateVolume); - } + + this.addVolumeEventListeners(inputElement, updateVolume); } } - setAudioVolume(audioConsumerPlayer, volume) { - if (audioConsumerPlayer) { + setAudioVolume(audioPlayer, volume) { + if (audioPlayer) { if (this.isMobileDevice) { - // On mobile, we'll use a different approach - audioConsumerPlayer.muted = volume === 0; - if (!audioConsumerPlayer.muted) { - // We can only set volume to 1 on mobile, so we'll adjust playback rate instead - audioConsumerPlayer.playbackRate = Math.max(0.1, volume); + audioPlayer.muted = volume === 0; + if (!audioPlayer.muted) { + // Adjust playback rate as volume on mobile devices + audioPlayer.playbackRate = Math.max(0.1, volume); } } else { - // On desktop, we can directly set the volume - audioConsumerPlayer.volume = volume; + // Set volume directly on desktop devices + audioPlayer.volume = volume; } } } + handlePeerAudio(cmd) { + console.log('handlePeerAudio', { cmd }); + + const { volumeInputId, audioProducerId, audioConsumerId, volume } = cmd; + + const volumeInput = this.getId(volumeInputId); + + if (!volumeInput) return; + + volumeInput.value = volume * 100; + + if (audioProducerId) { + this.handleConsumerAudio(audioProducerId, volume); + this.toggleVolumeInput(volumeInput, volumeInput.value); + } + + if (audioConsumerId) this.handleProducerAudio(audioConsumerId, volume); + } + + handleConsumerAudio(audioProducerId, volume) { + const consumerAudioId = this.getConsumerIdByProducerId(audioProducerId); + if (!consumerAudioId) return; + + const consumerAudioPlayer = this.getId(consumerAudioId); + if (!consumerAudioPlayer) return; + + this.setAudioVolume(consumerAudioPlayer, volume); + + console.log('handleConsumerPeerAudio', { consumerAudioId, consumerAudioPlayer }); + } + + handleProducerAudio(audioConsumerId, volume) { + const producerAudioId = this.getProducerIdByConsumerId(audioConsumerId); + if (!producerAudioId) return; + + const producerAudioPlayer = this.getId(producerAudioId); + if (!producerAudioPlayer) return; + + this.setAudioVolume(producerAudioPlayer, volume); + + console.log('handleProducerPeerAudio', { producerAudioId, producerAudioPlayer }); + } + + addVolumeEventListeners(inputElement, updateVolumeCallback) { + inputElement.addEventListener('input', updateVolumeCallback); + inputElement.addEventListener('change', updateVolumeCallback); + + if (this.isMobileDevice) { + inputElement.addEventListener('touchstart', updateVolumeCallback); + inputElement.addEventListener('touchmove', updateVolumeCallback); + } + } + // #################################################### // HANDLE DOMINANT SPEAKER // ################################################### @@ -7564,6 +8058,9 @@ class RoomClient { case 'ejectAll': this.exit(); break; + case 'peerAudio': + this.handlePeerAudio(cmd); + break; default: break; //... @@ -7641,7 +8138,7 @@ class RoomClient { const peerAudioButton = this.getId(data.peer_id + '___pAudio'); if (peerAudioButton) { const peerAudioIcon = peerAudioButton.querySelector('i'); - if (peerAudioIcon && peerAudioIcon.style.color == 'red') { + if (peerAudioIcon && peerAudioIcon.classList.contains('red')) { if (isRulesActive && isPresenter) { data.action = 'unmute'; return this.confirmPeerAction(data.action, data); @@ -7667,7 +8164,7 @@ class RoomClient { const peerVideoButton = this.getId(data.peer_id + '___pVideo'); if (peerVideoButton) { const peerVideoIcon = peerVideoButton.querySelector('i'); - if (peerVideoIcon && peerVideoIcon.style.color == 'red') { + if (peerVideoIcon && peerVideoIcon.classList.contains('red')) { if (isRulesActive && isPresenter) { data.action = 'unhide'; return this.confirmPeerAction(data.action, data); @@ -7691,7 +8188,7 @@ class RoomClient { const peerScreenButton = this.getId(id); if (peerScreenButton) { const peerScreenStatus = peerScreenButton.querySelector('i'); - if (peerScreenStatus && peerScreenStatus.style.color == 'red') { + if (peerScreenStatus && peerScreenStatus.classList.contains('red')) { if (isRulesActive && isPresenter) { data.action = 'start'; return this.confirmPeerAction(data.action, data); @@ -8129,6 +8626,9 @@ class RoomClient { showPeerAboutAndMessages(peer_id, peer_name, event = null) { this.hidePeerMessages(); + this.chatPeerId = peer_id; + this.chatPeerName = peer_name; + const chatAbout = this.getId('chatAbout'); const participant = this.getId(peer_id); const participantsList = this.getId('participantsList'); @@ -8269,6 +8769,10 @@ class RoomClient { this._moderator.chat_cant_chatgpt = data.status; rc.roomMessage('chat_cant_chatgpt', data.status); break; + case 'media_cant_sharing': + this._moderator.media_cant_sharing = data.status; + rc.roomMessage('media_cant_sharing', data.status); + break; default: break; } @@ -8368,33 +8872,44 @@ class RoomClient { popupPeerInfo(id, peer_info) { if (this.showPeerInfo && !this.isMobileDevice) { + //console.log('POPUP_PEER_INFO', peer_info); + + // Destructuring peer_info + const { + join_data_time, + peer_name, + peer_presenter, + is_desktop_device, + is_mobile_device, + is_tablet_device, + is_ipad_pro_device, + os_name, + os_version, + browser_name, + browser_version, + } = peer_info; + + const emojiPeerInfo = [ + { label: 'Join Time', value: join_data_time, emoji: '⏰' }, + { label: 'Name', value: peer_name, emoji: '👤' }, + { label: 'Presenter', value: peer_presenter ? 'Yes' : 'No', emoji: peer_presenter ? '⭐' : '🎤' }, + { label: 'Desktop Device', value: is_desktop_device ? 'Yes' : 'No', emoji: '💻' }, + { label: 'Mobile Device', value: is_mobile_device ? 'Yes' : 'No', emoji: '📱' }, + { label: 'Tablet Device', value: is_tablet_device ? 'Yes' : 'No', emoji: '📲' }, + { label: 'iPad Pro', value: is_ipad_pro_device ? 'Yes' : 'No', emoji: '📱' }, + { label: 'OS', value: `${os_name} ${os_version}`, emoji: '🖥️' }, + { label: 'Browser', value: `${browser_name} ${browser_version}`, emoji: '🌐' }, + ]; + + // Format the peer info into a structured string + const peerInfoFormatted = emojiPeerInfo + .map((item) => `${item.emoji} ${item.label}: ${item.value}`) + .join('
    '); + + // Apply the improved Tippy.js tooltip this.setTippy( id, - '
    ' +
    -                    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 Salman MIT 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

    - -