From 8a4e89aee115714f77ac1600f646094bb6c8b4ed Mon Sep 17 00:00:00 2001 From: Miroslav Pejic Date: Fri, 14 Nov 2025 23:06:59 +0100 Subject: [PATCH] [mirotalk] - add activeRooms, update dep --- .env.template | 3 + app/src/api.js | 8 ++ app/src/config.template.js | 1 + app/src/server.js | 35 +++++- package-lock.json | 12 +-- package.json | 4 +- public/css/activeRooms.css | 195 ++++++++++++++++++++++++++++++++++ public/css/client.css | 2 + public/js/activeRooms.js | 89 ++++++++++++++++ public/js/brand.js | 2 +- public/js/buttons.js | 1 + public/js/client.js | 17 ++- public/views/activeRooms.html | 37 +++++++ public/views/client.html | 4 + 14 files changed, 397 insertions(+), 13 deletions(-) create mode 100644 public/css/activeRooms.css create mode 100644 public/js/activeRooms.js create mode 100644 public/views/activeRooms.html diff --git a/.env.template b/.env.template index e60175b8..e4a7409e 100644 --- a/.env.template +++ b/.env.template @@ -52,6 +52,9 @@ OIDC_AUTH_REQUIRED=false # set to true if authentication is required for all rou OIDC_AUTH_LOGOUT=true # controls automatic logout from both your app and Auth0 when set to true SESSION_SECRET='mirotalk-p2p-oidc-secret' +# Show active rooms +SHOW_ACTIVE_ROOMS=false # true or false + # Maximum participants per room ROOM_MAX_PARTICIPANTS=1000 diff --git a/app/src/api.js b/app/src/api.js index 5dc809f4..47b73ea4 100644 --- a/app/src/api.js +++ b/app/src/api.js @@ -35,6 +35,14 @@ module.exports = class ServerApi { }; } + getActiveRooms(roomList) { + return Object.entries(roomList).map(([roomId, room]) => ({ + id: roomId, + peers: room && typeof room === 'object' ? Object.keys(room).length : 0, + join: this.getProtocol() + this._host + '/' + roomId, + })); + } + getMeetings(peers) { const meetings = {}; for (const room_id in peers) { diff --git a/app/src/config.template.js b/app/src/config.template.js index 919bee1f..65fef1b4 100644 --- a/app/src/config.template.js +++ b/app/src/config.template.js @@ -156,6 +156,7 @@ module.exports = { showMaxBtn: true, }, settings: { + showActiveRoomsBtn: true, showMicOptionsBtn: true, showTabRoomPeerName: true, showTabRoomParticipants: true, diff --git a/app/src/server.js b/app/src/server.js index f0ee8524..d2da0831 100755 --- a/app/src/server.js +++ b/app/src/server.js @@ -45,7 +45,7 @@ dependencies: { * @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.6.36 + * @version 1.6.37 * */ @@ -153,6 +153,7 @@ const hostCfg = { users: hostUsers, authenticated: !hostProtected, maxRoomParticipants: parseInt(process.env.ROOM_MAX_PARTICIPANTS) || 1000, + showActiveRooms: getEnvBoolean(process.env.SHOW_ACTIVE_ROOMS) || false, }; // JWT config @@ -396,6 +397,7 @@ const views = { newCall: path.join(__dirname, '../../', 'public/views/newcall.html'), notFound: path.join(__dirname, '../../', 'public/views/404.html'), privacy: path.join(__dirname, '../../', 'public/views/privacy.html'), + activeRooms: path.join(__dirname, '../../', 'public/views/activeRooms.html'), stunTurn: path.join(__dirname, '../../', 'public/views/testStunTurn.html'), }; @@ -403,7 +405,7 @@ const views = { const brandHtmlInjection = config?.brand?.htmlInjection ?? true; // File to cache and inject custom HTML data like OG tags and any other elements. -const filesPath = [views.landing, views.newCall, views.client, views.login]; +const filesPath = [views.landing, views.newCall, views.client, views.login, views.activeRooms]; const htmlInjector = new HtmlInjector(filesPath, config?.brand || null); const channels = {}; // collect channels @@ -564,6 +566,11 @@ app.get('/newcall', OIDCAuth, (req, res) => { } }); +// Get Active rooms +app.get('/activeRooms', OIDCAuth, (req, res) => { + htmlInjector.injectHtml(views.activeRooms, res); +}); + // Get stats endpoint app.get('/stats', (req, res) => { //log.debug('Send stats', statsData); @@ -1011,6 +1018,30 @@ function getMeetingURL(host) { return 'http' + (host.includes('localhost') ? '' : 's') + '://' + host + '/join/' + uuidV4(); } +// request active rooms endpoint +app.get(`${apiBasePath}/activeRooms`, (req, res) => { + // Check if endpoint allowed + if (!hostCfg.showActiveRooms) { + return res.status(403).json({ + error: 'This endpoint has been disabled. Please contact the administrator for further information.', + }); + } + // check if user was authorized for the api call + const { host, authorization = api_key_secret } = req.headers; + const api = new ServerApi(host, authorization, api_key_secret); + + // Get active rooms + const activeRooms = api.getActiveRooms(peers); + res.json({ activeRooms: activeRooms }); + + // log.debug the output if all done + log.debug('MiroTalk get active rooms - Authorized', { + header: req.headers, + body: req.body, + activeRooms: activeRooms, + }); +}); + // end of MiroTalk API v1 // not match any of page before, so 404 not found diff --git a/package-lock.json b/package-lock.json index 50e8c965..29a669f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mirotalk", - "version": "1.6.36", + "version": "1.6.37", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mirotalk", - "version": "1.6.36", + "version": "1.6.37", "license": "AGPL-3.0", "dependencies": { "@mattermost/client": "11.1.0", @@ -29,7 +29,7 @@ "jsdom": "^27.2.0", "jsonwebtoken": "^9.0.2", "nodemailer": "^7.0.10", - "openai": "^6.8.1", + "openai": "^6.9.0", "qs": "^6.14.0", "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", @@ -4224,9 +4224,9 @@ } }, "node_modules/openai": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.8.1.tgz", - "integrity": "sha512-ACifslrVgf+maMz9vqwMP4+v9qvx5Yzssydizks8n+YUJ6YwUoxj51sKRQ8HYMfR6wgKLSIlaI108ZwCk+8yig==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.9.0.tgz", + "integrity": "sha512-n2sJRYmM+xfJ0l3OfH8eNnIyv3nQY7L08gZQu3dw6wSdfPtKAk92L83M2NIP5SS8Cl/bsBBG3yKzEOjkx0O+7A==", "license": "Apache-2.0", "bin": { "openai": "bin/cli" diff --git a/package.json b/package.json index 4a55e0d2..c407fd8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mirotalk", - "version": "1.6.36", + "version": "1.6.37", "description": "A free WebRTC browser-based video call", "main": "server.js", "scripts": { @@ -61,7 +61,7 @@ "jsonwebtoken": "^9.0.2", "js-yaml": "^4.1.1", "nodemailer": "^7.0.10", - "openai": "^6.8.1", + "openai": "^6.9.0", "qs": "^6.14.0", "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", diff --git a/public/css/activeRooms.css b/public/css/activeRooms.css new file mode 100644 index 00000000..1229f5cc --- /dev/null +++ b/public/css/activeRooms.css @@ -0,0 +1,195 @@ +@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@500&display=swap'); + +:root { + --primary-bg: #1e2229; + --card-bg: #15181d; + --accent: #1870d7; + --accent2: #2186ff; + --text: #f6f8fa; + --card-shadow: 0 2px 8px #15181d; +} + +body { + font-family: 'Montserrat'; + background: var(--primary-bg); + margin: 0; + padding: 0; + color: var(--text); +} + +.container { + max-width: 1200px; + margin: 40px auto; + background: var(--card-bg); + border-radius: 12px; + box-shadow: var(--card-shadow); + padding: 32px; +} + +h1 { + text-align: center; + color: var(--accent2); + margin-bottom: 32px; + letter-spacing: 1px; +} + +.search-bar { + display: flex; + justify-content: center; + margin-bottom: 28px; +} + +.search-input { + padding: 10px 16px; + border-radius: 6px 0 0 6px; + border: none; + font-size: 1.1em; + background: #23262e; + color: var(--text); + outline: none; + width: 220px; +} + +.search-btn { + padding: 10px 20px; + border-radius: 0 6px 6px 0; + border: none; + background: var(--accent); + color: #fff; + font-size: 1.1em; + cursor: pointer; + font-weight: bold; + transition: background 0.2s; +} + +.search-btn:hover { + background: var(--accent2); +} + +.rooms { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 28px; + justify-content: center; +} + +.room-card { + background: var(--accent); + border-radius: 12px; + box-shadow: 0 1px 6px #15181d; + padding: 32px 24px; + min-height: 220px; + text-align: center; + transition: + box-shadow 0.2s, + transform 0.2s; + color: var(--text); + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-start; +} + +.room-card:hover { + cursor: pointer; + transform: translateY(-4px) scale(1.03); +} + +.room-title { + font-size: 1.15em; + color: #f6f8fa; + margin-bottom: 14px; + font-weight: bold; + letter-spacing: 0.5px; + word-break: break-all; + overflow-wrap: anywhere; + max-width: 100%; +} + +.peer-count { + font-size: 2.3em; + color: #ffd700; + font-weight: bold; + margin-bottom: 8px; + margin-top: auto; + align-self: center; +} + +.peer-label { + font-size: 0.9em; + color: #f6f8fa; + opacity: 0.7; + margin-bottom: 60px; + align-self: center; +} + +.empty { + text-align: center; + color: #b3c6e0; + margin-top: 40px; + font-size: 1.2em; +} + +.refresh-btn { + display: block; + margin: 0 auto 32px auto; + padding: 10px 28px; + background: var(--accent2); + color: #fff; + border: none; + border-radius: 7px; + cursor: pointer; + font-size: 1.1em; + font-weight: bold; + letter-spacing: 0.5px; + box-shadow: 0 1px 4px #15181d; + transition: background 0.2s; +} + +.refresh-btn:hover { + background: var(--accent); +} + +.join-btn { + display: block; + margin: 0 auto 0 auto; + position: absolute; + left: 50%; + bottom: 18px; + transform: translateX(-50%); + padding: 10px 24px; + background: var(--accent2); + color: #fff; + border-radius: 6px; + font-size: 1em; + font-weight: bold; + text-decoration: none; + transition: background 0.2s; + box-shadow: 0 1px 4px #15181d; +} + +.join-btn:hover { + background: var(--accent); +} + +@media (max-width: 900px) { + .container { + max-width: 98vw; + padding: 16px; + } + .rooms { + gap: 16px; + } +} + +@media (max-width: 600px) { + .rooms { + grid-template-columns: 1fr; + } + .room-card { + padding: 20px 10px; + } + .peer-label { + margin-bottom: 70px; + } +} diff --git a/public/css/client.css b/public/css/client.css index 4d2d5a25..740391fb 100755 --- a/public/css/client.css +++ b/public/css/client.css @@ -1342,6 +1342,7 @@ button { border-radius: 50px; } +#activeRoomsBtn, #myPeerNameSetBtn, #captionEveryoneBtn, #muteEveryoneBtn, @@ -1390,6 +1391,7 @@ button { transition: all 0.3s ease-in-out; } +#activeRoomsBtn:hover, #captionEveryoneBtn:hover { color: green; transform: translateY(-3px); diff --git a/public/js/activeRooms.js b/public/js/activeRooms.js new file mode 100644 index 00000000..777651a0 --- /dev/null +++ b/public/js/activeRooms.js @@ -0,0 +1,89 @@ +'use strict'; + +console.log(window.location); + +const isTest = false; // Set to true for testing with mock data + +const roomsDiv = document.getElementById('rooms'); +const searchInput = document.getElementById('searchInput'); +const searchBtn = document.getElementById('search-btn'); +const refreshBtn = document.getElementById('refresh-btn'); + +let allRooms = []; + +searchBtn.addEventListener('click', handleSearch); +refreshBtn.addEventListener('click', fetchRooms); + +function setRoomsContent(html) { + roomsDiv.innerHTML = html; +} + +function getRoomsData(res) { + return !isTest ? res.data.activeRooms || [] : mockRooms(); +} + +function mockRooms(roomCount = 1000) { + return Array.from({ length: roomCount }, () => { + const id = getUUID(); + return { + id, + peers: Math.floor(Math.random() * 10) + 1, + join: `${window.location.origin}/${id}`, + }; + }); +} + +function getUUID() { + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => + (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16) + ); +} + +async function fetchRooms() { + setRoomsContent('
Loading...
'); + try { + const res = await axios.get('/api/v1/activeRooms'); + if (res.status !== 200) throw new Error('Failed to fetch active rooms'); + allRooms = getRoomsData(res); + renderRooms(allRooms); + } catch (err) { + const errorMsg = err.response?.data?.error || err.message; + setRoomsContent(`
${errorMsg}
`); + } +} + +function renderRooms(rooms) { + if (!rooms.length) { + setRoomsContent('
No active rooms found.
'); + return; + } + setRoomsContent( + rooms + .map( + (room) => ` +
+
+ + ${room.id} +
+
+ + ${room.peers} +
+
${room.peers === 1 ? 'peer' : 'peers'}
+ + Join + +
+ ` + ) + .join('') + ); +} + +function handleSearch() { + const value = searchInput.value.trim().toLowerCase(); + renderRooms(!value ? allRooms : allRooms.filter((room) => room.id.toLowerCase().includes(value))); +} + +fetchRooms(); diff --git a/public/js/brand.js b/public/js/brand.js index f3e4b068..0255d41e 100644 --- a/public/js/brand.js +++ b/public/js/brand.js @@ -77,7 +77,7 @@ let brand = { }, about: { imageUrl: '../images/mirotalk-logo.gif', - title: 'WebRTC P2P v1.6.36', + title: 'WebRTC P2P v1.6.37', html: ` + + +
+ + + + diff --git a/public/views/client.html b/public/views/client.html index 1e849062..58b4f2c0 100755 --- a/public/views/client.html +++ b/public/views/client.html @@ -376,6 +376,10 @@ access to use this app. +