[mirotalk] - add room duration

This commit is contained in:
Miroslav Pejic
2026-02-03 17:05:41 +01:00
parent c21575ecea
commit 92418deea1
20 changed files with 177 additions and 51 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# ====================================================
# MiroTalk P2P v.1.7.19 - Environment Configuration
# MiroTalk P2P v.1.7.20 - Environment Configuration
# ====================================================
# App environment
+19 -17
View File
@@ -80,6 +80,7 @@ This project is proudly sponsored by
- Push-to-talk functionality, similar to a walkie-talkie.
- Advanced collaborative whiteboard for teachers.
- Real-time sharing of YouTube embed videos, video files (MP4, WebM, OGG), and audio files (MP3).
- Meeting Duration (HH:MM:SS): Set the meeting time in hours, minutes, and seconds for precise duration control.
- Full-screen mode with one-click video element zooming and pin/unpin.
- Customizable UI themes.
- Right-click options on video elements for additional controls.
@@ -120,21 +121,22 @@ This project is proudly sponsored by
<br/>
- You can `directly join a room` by using links like:
- https://p2p.mirotalk.com/join?room=test&name=random&avatar=0&audio=0&video=0&screen=0&chat=0&hide=0&notify=0
- https://mirotalk.up.railway.app/join?room=test&name=random&avatar=0&audio=0&video=0&screen=0&chat=0&hide=0&notify=0
- https://p2p.mirotalk.com/join?room=test&name=random&avatar=0&audio=0&video=0&screen=0&chat=0&hide=0&notify=0&duration=unlimited
- https://mirotalk.up.railway.app/join?room=test&name=random&avatar=0&audio=0&video=0&screen=0&chat=0&hide=0&notify=0&duration=unlimited
| Params | Type | Description |
| ------ | ------- | --------------- |
| room | string | Room Id |
| name | string | User name |
| avatar | Mixed | User avatar |
| audio | boolean | Audio stream |
| video | boolean | Video stream |
| screen | boolean | Screen stream |
| chat. | boolean | Chat |
| hide | boolean | Hide myself |
| notify | boolean | Welcome message |
| token | string | jwt token |
| Params | Type | Description |
| -------- | ------- | ------------------------- |
| room | string | Room Id |
| name | string | User name |
| avatar | Mixed | User avatar |
| audio | boolean | Audio stream |
| video | boolean | Video stream |
| screen | boolean | Screen stream |
| chat. | boolean | Chat |
| hide | boolean | Hide myself |
| notify | boolean | Welcome message |
| duration | string | Meeting duration HH:MM:SS |
| token | string | jwt token |
> **Note**
>
@@ -353,9 +355,9 @@ curl -X POST "https://mirotalk.up.railway.app/api/v1/meeting" -H "authorization:
### 4. Join Meeting (Basic)
```bash
curl -X POST "http://localhost:3000/api/v1/join" -H "authorization: mirotalkp2p_default_secret" -H "Content-Type: application/json" --data '{"room":"test","name":"random","avatar":false,"audio":true,"video":true,"screen":false,"chat":false,"hide":false,"notify":true}'
curl -X POST "https://p2p.mirotalk.com/api/v1/join" -H "authorization: mirotalkp2p_default_secret" -H "Content-Type: application/json" --data '{"room":"test","name":"random","avatar":false,"audio":true,"video":true,"screen":false,"chat":false,"hide":false,"notify":true}'
curl -X POST "https://mirotalk.up.railway.app/api/v1/join" -H "authorization: mirotalkp2p_default_secret" -H "Content-Type: application/json" --data '{"room":"test","name":"random","avatar":false,"audio":true,"video":true,"screen":false,"chat":false,"hide":false,"notify":true}'
curl -X POST "http://localhost:3000/api/v1/join" -H "authorization: mirotalkp2p_default_secret" -H "Content-Type: application/json" --data '{"room":"test","name":"random","avatar":false,"audio":true,"video":true,"screen":false,"chat":false,"hide":false,"notify":true,"duration":"unlimited"}'
curl -X POST "https://p2p.mirotalk.com/api/v1/join" -H "authorization: mirotalkp2p_default_secret" -H "Content-Type: application/json" --data '{"room":"test","name":"random","avatar":false,"audio":true,"video":true,"screen":false,"chat":false,"hide":false,"notify":true,"duration":"unlimited"}'
curl -X POST "https://mirotalk.up.railway.app/api/v1/join" -H "authorization: mirotalkp2p_default_secret" -H "Content-Type: application/json" --data '{"room":"test","name":"random","avatar":false,"audio":true,"video":true,"screen":false,"chat":false,"hide":false,"notify":true,"duration":"unlimited"}'
```
### 5. Join Meeting with Token
+1
View File
@@ -28,6 +28,7 @@ async function getJoin() {
chat: false,
hide: false,
notify: true,
duration: 'unlimited',
token: {
username: 'username',
password: 'password',
+1
View File
@@ -27,6 +27,7 @@ $data = array(
"chat" => false,
"hide" => false,
"notify" => true,
"duration" => "unlimited",
"token" => array(
"username" => "username",
"password" => "password",
+1
View File
@@ -22,6 +22,7 @@ data = {
"chat": "false",
"hide": "false",
"notify": "true",
"duration": "unlimited",
"token": {
"username": "username",
"password": "password",
+1
View File
@@ -18,6 +18,7 @@ REQUEST_DATA='{
"chat": false,
"hide": false,
"notify": true,
"duration": "unlimited",
"token": {
"username": "username",
"password": "password",
+3
View File
@@ -105,6 +105,9 @@ paths:
notify:
type: boolean
default: false
duration:
type: string
default: 'unlimited'
token:
type: object
description: |
+3 -1
View File
@@ -58,7 +58,7 @@ module.exports = class ServerApi {
getJoinURL(data) {
// Get data
const { room, name, avatar, audio, video, screen, chat, notify, hide, token } = data;
const { room, name, avatar, audio, video, screen, chat, notify, hide, duration, token } = data;
const roomValue = room || uuidV4();
const nameValue = name || 'User-' + this.getRandomNumber();
@@ -69,6 +69,7 @@ module.exports = class ServerApi {
const chatValue = chat || false;
const hideValue = hide || false;
const notifyValue = notify || false;
const durationValue = duration || 'unlimited';
const jwtToken = token ? '&token=' + this.getToken(token) : '';
const joinURL =
@@ -84,6 +85,7 @@ module.exports = class ServerApi {
`&chat=${chatValue}` +
`&hide=${hideValue}` +
`&notify=${notifyValue}` +
`&duration=${durationValue}` +
jwtToken;
return joinURL;
+1 -1
View File
@@ -2,7 +2,7 @@
/**
* ==============================================
* MiroTalk P2P v.1.7.19 - Configuration File
* MiroTalk P2P v.1.7.20 - Configuration File
* ==============================================
*
* Branding and customizations require a license:
+3 -3
View File
@@ -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.7.19
* @version 1.7.20
*
*/
@@ -640,10 +640,10 @@ app.get('/join/', async (req, res) => {
log.debug('Request Query', req.query);
/*
http://localhost:3000/join?room=test&name=mirotalk&audio=1&video=1&screen=0&chat=1&notify=0&hide=0
https://p2p.mirotalk.com/join?room=test&name=mirotalk&audio=1&video=1&screen=0&chat=1&notify=0&hide=0
https://p2p.mirotalk.com/join?room=test&name=mirotalk&audio=1&video=1&screen=0&chat=1&notify=0&hide=0&duration=00:00:30
https://mirotalk.up.railway.app/join?room=test&name=mirotalk&audio=1&video=1&screen=0&chat=1&notify=0&hide=0
*/
const { room, name, audio, video, screen, chat, notify, hide, token } = checkXSS(req.query);
const { room, name, audio, video, screen, chat, notify, hide, duration, token } = checkXSS(req.query);
if (!room) {
log.warn('/join/params room empty', room);
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mirotalk",
"version": "1.7.19",
"version": "1.7.20",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mirotalk",
"version": "1.7.19",
"version": "1.7.20",
"license": "AGPL-3.0",
"dependencies": {
"@mattermost/client": "11.3.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mirotalk",
"version": "1.7.19",
"version": "1.7.20",
"description": "A free WebRTC browser-based video call",
"main": "server.js",
"scripts": {
+1 -1
View File
@@ -79,7 +79,7 @@ let brand = {
},
about: {
imageUrl: '../images/mirotalk-logo.gif',
title: 'WebRTC P2P v1.7.19',
title: 'WebRTC P2P v1.7.20',
html: `
<button
id="support-button"
+99 -21
View File
@@ -15,7 +15,7 @@
* @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.7.19
* @version 1.7.20
*
*/
@@ -28,6 +28,7 @@ const signalingServer = getSignalingServer();
// This room
const myRoomId = getId('myRoomId');
const roomSessionDuration = getRoomDuration();
const roomId = getRoomId();
const myRoomUrl = window.location.origin + '/join/' + roomId; // share room url
@@ -992,8 +993,7 @@ function getSignalingServer() {
*/
function getRoomId() {
// check if passed as params /join?room=id
let qs = new URLSearchParams(window.location.search);
let queryRoomId = filterXSS(qs.get('room'));
let queryRoomId = getQueryParam('room');
// skip /join/
let roomId = queryRoomId ? queryRoomId : window.location.pathname.split('/join/')[1];
@@ -1014,6 +1014,74 @@ function getRoomId() {
return roomId;
}
/**
* Room Session Duration
*/
function getRoomDuration() {
const roomDuration = getQueryParam('duration');
if (isValidDuration(roomDuration)) {
if (roomDuration === 'unlimited') {
console.log('The room has no time limit');
return roomDuration;
}
const timeLimit = timeToMilliseconds(roomDuration);
setTimeout(() => {
playSound('eject');
Swal.fire({
background: swBg,
position: 'center',
title: 'Time Limit Reached',
text: 'The room has reached its time limit and will close shortly',
icon: 'warning',
timer: 6000, // 6 seconds
timerProgressBar: true,
showConfirmButton: false,
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
},
willClose: () => {
exitRoom();
},
});
}, timeLimit);
console.log('Direct join', { duration: roomDuration, timeLimit: timeLimit });
return roomDuration;
}
return 'unlimited';
}
/**
* Convert HH:MM:SS to milliseconds
* @param {string} timeString Time string in HH:MM:SS format
* @returns {integer} milliseconds
*/
function timeToMilliseconds(timeString) {
const [hours, minutes, seconds] = timeString.split(':').map(Number);
return (hours * 3600 + minutes * 60 + seconds) * 1000;
}
/**
* Validate duration format
* @param {string} duration Duration string
* @returns {boolean} true/false
*/
function isValidDuration(duration) {
if (duration === 'unlimited') return true;
// Check if the format is HH:MM:SS
const regex = /^(\d{2}):(\d{2}):(\d{2})$/;
const match = duration.match(regex);
if (!match) return false;
const [hours, minutes, seconds] = match.slice(1).map(Number);
// Validate ranges: hours, minutes, and seconds
if (hours < 0 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59) {
return false;
}
return true;
}
/**
* Generate random Id
* @param {integer} length
@@ -1049,8 +1117,7 @@ function getUUID() {
* @returns {boolean} true/false (default true)
*/
function getNotify() {
let qs = new URLSearchParams(window.location.search);
let notify = filterXSS(qs.get('notify'));
let notify = getQueryParam('notify');
if (notify) {
let queryNotify = notify === '1' || notify === 'true';
if (queryNotify != null) {
@@ -1068,8 +1135,7 @@ function getNotify() {
* @returns {boolean} true/false
*/
function getChat() {
let qs = new URLSearchParams(window.location.search);
let chat = filterXSS(qs.get('chat'));
let chat = getQueryParam('chat');
if (chat) {
let queryChat = chat === '1' || chat === 'true';
if (queryChat != null) {
@@ -1088,8 +1154,7 @@ function getChat() {
*/
function getPeerToken() {
if (window.sessionStorage.peer_token) return window.sessionStorage.peer_token;
let qs = new URLSearchParams(window.location.search);
let token = filterXSS(qs.get('token'));
let token = getQueryParam('token');
let queryToken = false;
if (token) {
queryToken = token;
@@ -1103,8 +1168,7 @@ function getPeerToken() {
* @returns {string} Peer Name
*/
function getPeerName() {
const qs = new URLSearchParams(window.location.search);
const name = filterXSS(qs.get('name'));
const name = getQueryParam('name');
if (isHtml(name)) {
console.log('Direct join', { name: 'Invalid name' });
return 'Invalid name';
@@ -1138,8 +1202,7 @@ function generateRandomName() {
* @returns {string} Peer Avatar
*/
function getPeerAvatar() {
const qs = new URLSearchParams(window.location.search);
const avatar = filterXSS(qs.get('avatar'));
const avatar = getQueryParam('avatar');
const avatarDisabled = avatar === '0' || avatar === 'false';
console.log('Direct join', { avatar: avatar });
@@ -1155,8 +1218,7 @@ function getPeerAvatar() {
* @returns {boolean} true/false
*/
function getScreenEnabled() {
let qs = new URLSearchParams(window.location.search);
let screen = filterXSS(qs.get('screen'));
let screen = getQueryParam('screen');
if (screen) {
screen = screen.toLowerCase();
let queryPeerScreen = screen === '1' || screen === 'true';
@@ -1172,8 +1234,7 @@ function getScreenEnabled() {
* @returns {boolean} true/false
*/
function getHideMeActive() {
let qs = new URLSearchParams(window.location.search);
let hide = filterXSS(qs.get('hide'));
let hide = getQueryParam('hide');
let queryHideMe = false;
if (hide) {
hide = hide.toLowerCase();
@@ -1183,6 +1244,16 @@ function getHideMeActive() {
return queryHideMe;
}
/**
* Get query parameter from URL
* @param {string} param parameter name
* @returns {string} parameter value
*/
function getQueryParam(param) {
const urlParams = new URLSearchParams(window.location.search);
return filterXSS(urlParams.get(param));
}
/**
* Check if there is peer connections
* @returns {boolean} true/false
@@ -2161,9 +2232,8 @@ async function changeLocalMicrophone(deviceId) {
* 1/true = enabled / 0/false = disabled
*/
function checkPeerAudioVideo() {
let qs = new URLSearchParams(window.location.search);
let audio = filterXSS(qs.get('audio'));
let video = filterXSS(qs.get('video'));
let audio = getQueryParam('audio');
let video = getQueryParam('video');
if (audio) {
audio = audio.toLowerCase();
let queryPeerAudio = useAudio ? audio === '1' || audio === 'true' : false;
@@ -13570,7 +13640,7 @@ function showAbout() {
Swal.fire({
background: swBg,
position: 'center',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.7.19',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.7.20',
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
customClass: { image: 'img-about' },
html: `
@@ -13631,6 +13701,14 @@ function leaveRoom() {
surveyActive ? leaveFeedback() : redirectOnLeave();
}
/**
* Exit the Room
*/
function exitRoom() {
checkRecording();
redirectOnLeave();
}
/**
* Ask for feedback when room exit
*/
+18 -1
View File
@@ -4,7 +4,7 @@
* Custom Room page: build /join URL from form settings.
*
* Query params used by client.js:
* - room, name, avatar, audio, video, screen, chat, hide, notify
* - room, name, avatar, audio, video, screen, chat, hide, notify, duration, token
*/
document.addEventListener('DOMContentLoaded', () => {
@@ -19,6 +19,8 @@ document.addEventListener('DOMContentLoaded', () => {
const avatarEl = document.getElementById('avatar');
const tokenEl = document.getElementById('token');
const durationEl = document.getElementById('duration');
const audioEl = document.getElementById('audio');
const videoEl = document.getElementById('video');
const screenEl = document.getElementById('screen');
@@ -53,12 +55,25 @@ document.addEventListener('DOMContentLoaded', () => {
const boolToFlag = (checked) => (checked ? '1' : '0');
const normalizeDuration = (raw) => {
const value = safe(raw);
if (!value) return 'unlimited';
if (value.toLowerCase() === 'unlimited') return 'unlimited';
// Accept HH:MM:SS format only
const re = /^(\d{2}):(\d{2}):(\d{2})$/;
if (!re.test(value)) {
throw new Error('Duration must be HH:MM:SS (e.g. 12:30:00) or left empty for unlimited');
}
return value;
};
const buildJoinUrl = () => {
const room = safe(roomEl?.value) || 'random';
const name = safe(nameEl?.value) || 'random';
const avatarRaw = safe(avatarEl?.value);
const avatar = avatarRaw ? avatarRaw : '0';
const token = safe(tokenEl?.value);
const duration = normalizeDuration(durationEl?.value);
const url = new URL('/join', window.location.origin);
url.searchParams.set('room', room);
@@ -72,6 +87,8 @@ document.addEventListener('DOMContentLoaded', () => {
url.searchParams.set('hide', boolToFlag(!!hideEl?.checked));
url.searchParams.set('notify', boolToFlag(!!notifyEl?.checked));
url.searchParams.set('duration', duration);
if (token) url.searchParams.set('token', token);
return url;
+2
View File
@@ -9,6 +9,7 @@ class IframeApi {
chat: false,
hide: false,
notify: false,
duration: 'unlimited',
width: '100vw',
height: '100vh',
token: null,
@@ -53,6 +54,7 @@ class IframeApi {
chat: this.options.chat ? 1 : 0,
hide: this.options.hide ? 1 : 0,
notify: this.options.notify ? 1 : 0,
duration: this.options.duration || 'unlimited',
});
if (this.options.token) {
+15
View File
@@ -136,6 +136,21 @@
</label>
</div>
<div class="cr-field">
<label class="cr-label" for="duration">Duration (optional)</label>
<input
id="duration"
name="duration"
class="cr-input"
type="text"
inputmode="numeric"
placeholder="12:30:00"
pattern="^\d{2}:\d{2}:\d{2}$"
maxlength="8"
/>
<p class="cr-hint">Format HH:MM:SS. Leave empty for unlimited (uses duration=unlimited).</p>
</div>
<div class="cr-field">
<label class="cr-label" for="crPreviewUrl">Join link</label>
<div class="cr-preview-row">
+1
View File
@@ -20,6 +20,7 @@
screen: 0,
hide: 0,
notify: 0,
duration: 'unlimited', // HH:MM:SS
token: null,
width: '100vw',
height: '100vh',
+3 -2
View File
@@ -143,6 +143,7 @@ describe('test-api', () => {
chat: false,
hide: false,
notify: false,
duration: '00:30:00',
token: { username: 'user', password: 'pass', presenter: true, expire: '1h' },
};
@@ -150,7 +151,7 @@ describe('test-api', () => {
const result = serverApi.getJoinURL(data);
result.should.equal(
'https://example.com/join?room=room1&name=John%20Doe&avatar=avatar.jpg&audio=true&video=false&screen=false&chat=false&hide=false&notify=false&token=testToken'
'https://example.com/join?room=room1&name=John%20Doe&avatar=avatar.jpg&audio=true&video=false&screen=false&chat=false&hide=false&notify=false&duration=00:30:00&token=testToken'
);
tokenStub.restore();
@@ -168,7 +169,7 @@ describe('test-api', () => {
const result = serverApi.getJoinURL({});
result.should.equal(
'https://example.com/join?room=room1&name=User-123456&avatar=false&audio=false&video=false&screen=false&chat=false&hide=false&notify=false'
'https://example.com/join?room=room1&name=User-123456&avatar=false&audio=false&video=false&screen=false&chat=false&hide=false&notify=false&duration=unlimited'
);
});
});
+1
View File
@@ -20,6 +20,7 @@
chat: 0,
hide: 0,
notify: 0,
duration: 'unlimited', // HH:MM:SS
token: null,
width: '100vw',
height: '100vh',