[mirotalk] - #195 improve host protection, add auth user & new URL params

This commit is contained in:
Miroslav Pejic
2023-11-22 13:12:35 +01:00
parent a904cfc88b
commit ec14f5eb77
8 changed files with 292 additions and 84 deletions
+11 -2
View File
@@ -5,9 +5,18 @@ HTTPS=false # true or false
# Domain
HOST=localhost
# Host protection
# HOST_PROTECTED: When set to true, it requires a valid username and password from the HOST_USERS list to initialize or join a room.
# HOST_USER_AUTH: When set to true, it also requires a valid username and password, but these need to be provided in the URL parameters.
# HOST_USERS: This is the list of valid host users along with their credentials.
HOST_PROTECTED=false # true or false
HOST_USERNAME=username
HOST_PASSWORD=password
HOST_USER_AUTH=false # true or false
HOST_USERS='[{"username": "username", "password": "password"},{"username": "username2", "password": "password2"}]'
# Signaling Server listen port
+65 -16
View File
@@ -34,6 +34,7 @@
- Unlimited conference rooms with no time limitations.
- Translated into 133 languages.
- Host protection to prevent unauthorized access.
- User auth to prevent unauthorized access.
- Room password protection.
- Compatible with desktop and mobile devices.
- Optimized mobile room URL sharing.
@@ -91,26 +92,74 @@
<br/>
- You can `directly join a room` by using links like:
- https://p2p.mirotalk.com/join?room=test&name=mirotalk&audio=0&video=0&screen=0&notify=0
- https://mirotalk.up.railway.app/join?room=test&name=mirotalk&audio=0&video=0&screen=0&notify=0
- https://p2p.mirotalk.com/join?room=test&name=mirotalk&audio=0&video=0&screen=0&hide=0&notify=0
- https://mirotalk.up.railway.app/join?room=test&name=mirotalk&audio=0&video=0&screen=0&hide=0&notify=0
| Params | Type | Description |
| ------ | ------- | --------------- |
| room | string | Room Id |
| name | string | User name |
| audio | boolean | Audio stream |
| video | boolean | Video stream |
| screen | boolean | Screen stream |
| notify | boolean | Welcome message |
| Params | Type | Description |
| -------- | ------- | --------------- |
| room | string | Room Id |
| name | string | User name |
| audio | boolean | Audio stream |
| video | boolean | Video stream |
| screen | boolean | Screen stream |
| hide | boolean | Hide myself |
| notify | boolean | Welcome message |
| username | string | auth username |
| password | string | auth password |
> **Note**
>
> When [host protection is enabled](https://github.com/miroslavpejic85/mirotalk/commit/285c92605585bf204996dc0bade9b3e7c62d75df#commitcomment-103108955) the host needs to provide a valid username and password as specified in the `.env`.
>
> After host authentication, participants can join the room using any of the following URL formats:
>
> - https://p2p.mirotalk.com/join/test (URL path)
> - https://p2p.mirotalk.com/join/?room=test&name=mirotalk&audio=0&video=0&screen=0&notify=0 (URL with query parameters for direct join)
> The `username` and `password` parameters are required when either `HOST_PROTECTED` or `HOST_USER_AUTH` is set to `true` in the `.env` file. The valid list of users is defined in the `HOST_USERS` configuration.
</details>
<details>
<summary>Host Protection Configuration</summary>
<br/>
When [host protection](https://github.com/miroslavpejic85/mirotalk/commit/285c92605585bf204996dc0bade9b3e7c62d75df#commitcomment-103108955) or host user auth is enabled, the host/users must provide a valid username and password as specified in the `.env` file.
### `HOST_PROTECTED`
- **Description:** Requires the host to provide a valid username and password during room initialization.
- **Values:** `true` if protection is enabled, `false` if not (default false).
### `HOST_USER_AUTH`
- **Description:** Determines whether host authentication is required.
- **Values:** `true` if user authentication is required, `false` if not (default false).
### `HOST_USERS`
- **Description:** List of valid host users with their credentials.
- **Format:** JSON array with user objects: `{"username": "username", "password": "password"}`
Example:
```bash
HOST_USERS='[{"username": "username", "password": "password"},{"username": "username2", "password": "password2"}]'
```
### Room Initialization
To bypass the login page, join the room with URL parameters:
- [https://p2p.mirotalk.com/join/?room=test&username=username&password=password](https://p2p.mirotalk.com/join/?room=test&username=username&password=password)
### Participant Room Entry
If `HOST_PROTECTED` is enabled, participants can join using:
- [https://p2p.mirotalk.com/join/test](https://p2p.mirotalk.com/join/test) (URL path)
- [https://p2p.mirotalk.com/join/?room=test&name=mirotalk&audio=0&video=0&screen=0&hide=0&notify=0](https://p2p.mirotalk.com/join/?room=test&name=mirotalk&audio=0&video=0&screen=0&hide=0&notify=0) (URL with query parameters)
If `HOST_USER_AUTH` is enabled, participants can join with mandatory credentials:
- [https://p2p.mirotalk.com/join/?room=test&username=username&password=password](https://p2p.mirotalk.com/join/?room=test&username=username&password=password) (URL path)
- [https://p2p.mirotalk.com/join/?room=test&name=mirotalk&audio=0&video=0&screen=0&hide=0&notify=0&username=username&password=password](https://p2p.mirotalk.com/join/?room=test&name=mirotalk&audio=0&video=0&screen=0&hide=0&notify=0&username=username&password=password) (URL with query parameters)
</details>
</details>
+93 -38
View File
@@ -38,7 +38,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.2.3
* @version 1.2.4
*
*/
@@ -90,10 +90,13 @@ io = new Server({
// Host protection (disabled by default)
const hostProtected = getEnvBoolean(process.env.HOST_PROTECTED);
const userAuth = getEnvBoolean(process.env.HOST_USER_AUTH);
const hostUsersString = process.env.HOST_USERS || '[{"username": "MiroTalk", "password": "P2P"}]';
const hostUsers = JSON.parse(hostUsersString);
const hostCfg = {
protected: hostProtected,
username: process.env.HOST_USERNAME,
password: process.env.HOST_PASSWORD,
user_auth: userAuth,
users: hostUsers,
authenticated: !hostProtected,
};
@@ -311,19 +314,36 @@ app.get(['/test'], (req, res) => {
// no room name specified to join
app.get('/join/', (req, res) => {
if (hostCfg.authenticated && Object.keys(req.query).length > 0) {
if (Object.keys(req.query).length > 0) {
log.debug('Request Query', req.query);
/*
http://localhost:3000/join?room=test&name=mirotalk&audio=1&video=1&screen=1&notify=1
https://p2p.mirotalk.com/join?room=test&name=mirotalk&audio=1&video=1&screen=1&notify=1
https://mirotalk.up.railway.app/join?room=test&name=mirotalk&audio=1&video=1&screen=1&notify=1
http://localhost:3000/join?room=test&name=mirotalk&audio=1&video=1&screen=0&notify=0&hide=1&username=username&password=password
https://p2p.mirotalk.com/join?room=test&name=mirotalk&audio=1&video=1&screen=0&notify=0&hide=0
https://mirotalk.up.railway.app/join?room=test&name=mirotalk&audio=1&video=1&screen=0&notify=0&hide=0
*/
const { room, name, audio, video, screen, notify } = checkXSS(req.query);
// all the params are mandatory for the direct room join
// if (room && name && audio && video && screen && notify) {
if (room) {
const { room, name, audio, video, screen, notify, hide, username, password } = checkXSS(req.query);
// check if valid peer
const isPeerValid = isAuthPeer(username, password);
// Peer valid going to auth as host
if (hostCfg.protected && isPeerValid && !hostCfg.authenticated) {
const ip = getIP(req);
hostCfg.authenticated = true;
authHost = new Host(ip, true);
log.debug('Direct Join user auth as host done', {
ip: ip,
username: username,
password: password,
});
}
// Check if peer authenticated or valid
if (room && (hostCfg.authenticated || isPeerValid)) {
// only room mandatory
return res.sendFile(views.client);
} else {
return res.sendFile(views.login);
}
}
if (hostCfg.protected) {
@@ -350,7 +370,12 @@ app.get('/join/*', function (req, res) {
res.redirect('/');
});
// logged
// Login
app.get(['/login'], (req, res) => {
res.sendFile(views.login);
});
// Logged
app.get(['/logged'], (req, res) => {
const ip = getIP(req);
if (allowedIP(ip)) {
@@ -365,22 +390,29 @@ app.get(['/logged'], (req, res) => {
// handle login on host protected
app.post(['/login'], (req, res) => {
if (hostCfg.protected) {
//
const ip = getIP(req);
log.debug(`Request login to host from: ${ip}`, req.body);
const { username, password } = checkXSS(req.body);
const isPeerValid = isAuthPeer(username, password);
// Peer valid going to auth as host
if (hostCfg.protected && isPeerValid && !hostCfg.authenticated) {
const ip = getIP(req);
log.debug(`Request login to host from: ${ip}`, req.body);
const { username, password } = checkXSS(req.body);
if (username == hostCfg.username && password == hostCfg.password) {
hostCfg.authenticated = true;
authHost = new Host(ip, true);
log.debug('LOGIN OK', { ip: ip, authorized: authHost.isAuthorized(ip) });
res.status(200).json({ message: 'authorized' });
} else {
log.debug('LOGIN KO', { ip: ip, authorized: false });
hostCfg.authenticated = false;
res.status(401).json({ message: 'unauthorized' });
}
hostCfg.authenticated = true;
authHost = new Host(ip, true);
log.debug('HOST LOGIN OK', { ip: ip, authorized: authHost.isAuthorized(ip) });
return res.status(200).json({ message: 'authorized' });
}
// Peer auth valid
if (isPeerValid) {
log.debug('PEER LOGIN OK', { ip: ip, authorized: true });
return res.status(200).json({ message: 'authorized' });
} else {
res.redirect('/');
return res.status(401).json({ message: 'unauthorized' });
}
});
@@ -482,10 +514,8 @@ async function ngrokStart() {
const tunnelHttps = pu0.startsWith('https') ? pu0 : pu1;
// server settings
log.debug('settings', {
host_protected: hostCfg.protected,
host_username: hostCfg.username,
host_password: hostCfg.password,
iceServers: iceServers,
host: hostCfg,
ngrok: {
ngrok_enabled: ngrokEnabled,
ngrok_token: ngrokAuthToken,
@@ -537,10 +567,8 @@ server.listen(port, null, () => {
} else {
// server settings
log.debug('settings', {
host_protected: hostCfg.protected,
host_username: hostCfg.username,
host_password: hostCfg.password,
iceServers: iceServers,
host: hostCfg,
server: host,
test_ice_servers: testStunTurn,
api_docs: api_docs,
@@ -691,6 +719,8 @@ io.sockets.on('connect', async (socket) => {
channel_password,
peer_uuid,
peer_name,
peer_username,
peer_password,
peer_video,
peer_audio,
peer_video_status,
@@ -713,6 +743,23 @@ io.sockets.on('connect', async (socket) => {
// no presenter aka host in presenters init
if (!(channel in presenters)) presenters[channel] = {};
// User Auth required, we check if peer valid
if (hostCfg.user_auth) {
const isPeerValid = isAuthPeer(peer_username, peer_password);
log.debug('[' + socket.id + '] JOIN ROOM - HOST PROTECTED - USER AUTH check peer', {
ip: peer_ip,
peer_username: peer_username,
peer_password: peer_password,
peer_valid: isPeerValid,
});
if (!isPeerValid) {
// redirect peer to login page
return socket.emit('unauthorized');
}
}
// room locked by the participants can't join
if (peers[channel]['lock'] === true && peers[channel]['password'] != channel_password) {
log.debug('[' + socket.id + '] [Warning] Room Is Locked', channel);
@@ -759,6 +806,8 @@ io.sockets.on('connect', async (socket) => {
// Send some server info to joined peer
await sendToPeer(socket.id, sockets, 'serverInfo', {
peers_count: peerCounts,
host_protected: hostCfg.protected,
user_auth: hostCfg.user_auth,
is_presenter: isPresenter,
survey: {
active: surveyEnabled,
@@ -1279,15 +1328,21 @@ async function isPeerPresenter(room_id, peer_id, peer_name, peer_uuid) {
log.error('isPeerPresenter', err);
return false;
}
log.debug('[' + peer_id + '] isPeerPresenter', {
peer_name: peer_name,
peer_uuid: peer_uuid,
isPresenter: isPresenter,
presenter: presenters[room_id],
});
log.debug('[' + peer_id + '] isPeerPresenter', presenters[room_id]);
return isPresenter;
}
/**
* Check if peer is present in the host users
* @param {string} username
* @param {string} password
* @returns Boolean true/false
*/
function isAuthPeer(username, password) {
return hostCfg.users && hostCfg.users.some((user) => user.username === username && user.password === password);
}
/**
* Get ip
* @param {object} req
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "mirotalk",
"version": "1.2.3",
"version": "1.2.4",
"description": "A free WebRTC browser-based video call",
"main": "server.js",
"scripts": {
@@ -34,9 +34,9 @@
"license": "AGPL-3.0",
"homepage": "https://github.com/miroslavpejic85/mirotalk",
"dependencies": {
"@sentry/integrations": "^7.80.0",
"@sentry/node": "^7.80.0",
"axios": "^1.6.1",
"@sentry/integrations": "^7.81.1",
"@sentry/node": "^7.81.1",
"axios": "^1.6.2",
"body-parser": "^1.20.2",
"colors": "^1.4.0",
"compression": "^1.7.4",
@@ -45,7 +45,7 @@
"dotenv": "^16.3.1",
"express": "^4.18.2",
"ngrok": "^4.3.3",
"openai": "^4.17.4",
"openai": "^4.19.1",
"qs": "^6.11.2",
"socket.io": "^4.7.2",
"swagger-ui-express": "^5.0.0",
@@ -56,6 +56,6 @@
"devDependencies": {
"node-fetch": "^3.3.2",
"nodemon": "^3.0.1",
"prettier": "3.0.3"
"prettier": "3.1.0"
}
}
+104 -13
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.2.3
* @version 1.2.4
*
*/
@@ -501,6 +501,8 @@ let isDocumentOnFullScreen = false;
let myPeerId; // This socket.id
let myPeerUUID = getUUID(); // Unique peer id
let myPeerName = getPeerName();
let myUsername = window.sessionStorage.peer_username ? window.sessionStorage.peer_username : getPeerUsername(); // default false if not passed by query params
let myPassword = window.sessionStorage.peer_password ? window.sessionStorage.peer_password : getPeerPassword(); // default false if not passed by query params
let isPresenter = false; // True Who init the room (aka first peer joined)
let myHandStatus = false;
let myVideoStatus = false;
@@ -561,7 +563,7 @@ let isVideoFullScreenSupported = true;
let isVideoOnFullScreen = false;
let isScreenSharingSupported = false;
let isScreenStreaming = false;
let isHideMeActive = false; // Hide myself from the meeting view
let isHideMeActive = getHideMeActive();
let remoteMediaControls = false; // enable - disable peers video player controls (default false)
let camera = 'user'; // user = front-facing camera on a smartphone. | environment = the back camera on a smartphone.
@@ -631,6 +633,10 @@ let receiveInProgress = false;
*/
const chunkSize = 1024; // 1024 * 16; // 16kb/s
// server
let isHostProtected = false; // Username and Password required to initialize room
let isPeerAuthEnabled = false; // Username and Password required in the URL params to join room
// survey
let surveyActive = true; // when leaving the room give a feedback, if false will be redirected to newcall page
let surveyURL = 'https://www.questionpro.com/t/AUs7VZq00L';
@@ -896,6 +902,36 @@ function getNotify() {
return notify;
}
/**
* Get Peer username
* @returns {mixed} boolean false or username string
*/
function getPeerUsername() {
let qs = new URLSearchParams(window.location.search);
let username = filterXSS(qs.get('username'));
let queryUsername = false;
if (username) {
queryUsername = username;
}
console.log('Direct join', { username: queryUsername });
return queryUsername;
}
/**
* Get Peer password
* @returns {mixed} boolean false or password string
*/
function getPeerPassword() {
let qs = new URLSearchParams(window.location.search);
let password = filterXSS(qs.get('password'));
let queryPassword = false;
if (password) {
queryPassword = password;
}
console.log('Direct join', { password: queryPassword });
return queryPassword;
}
/**
* Check if peer name is set
* @returns {string} Peer Name
@@ -928,6 +964,22 @@ function getScreenEnabled() {
return false;
}
/**
* Hide myself from the meeting view
* @returns {boolean} true/false
*/
function getHideMeActive() {
let qs = new URLSearchParams(window.location.search);
let hide = filterXSS(qs.get('hide'));
let queryHideMe = false;
if (hide) {
hide = hide.toLowerCase();
queryHideMe = hide === '1' || hide === 'true';
}
console.log('Direct join', { hide: queryHideMe });
return queryHideMe;
}
/**
* Check if there is peer connections
* @returns {boolean} true/false
@@ -991,6 +1043,7 @@ function initClientPeer() {
// on receiving data from signaling server...
signalingSocket.on('connect', handleConnect);
signalingSocket.on('unauthorized', handleUnauthorized);
signalingSocket.on('roomIsLocked', handleUnlockTheRoom);
signalingSocket.on('roomAction', handleRoomAction);
signalingSocket.on('addPeer', handleAddPeer);
@@ -1071,7 +1124,10 @@ async function handleConnect() {
function handleServerInfo(config) {
console.log('13. Server info', config);
const { peers_count, is_presenter, survey, redirect } = config;
const { peers_count, host_protected, user_auth, is_presenter, survey, redirect } = config;
isHostProtected = host_protected;
isPeerAuthEnabled = user_auth;
// Get survey settings from server
surveyActive = survey.active;
@@ -1100,6 +1156,27 @@ function handleServerInfo(config) {
}
}
/**
* HOST_USER_AUTH enabled and peer not match valid username and password
*/
function handleUnauthorized() {
playSound('alert');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swBg,
imageUrl: images.forbidden,
title: 'Oops, Unauthorized',
text: 'The host has user authentication enabled',
confirmButtonText: `Login`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then(() => {
// Login required to join room
openURL(`/login/?room=${roomId}`);
});
}
/**
* Room is busy, disconnect me and alert the user that
* will be redirected to home page
@@ -1554,12 +1631,12 @@ function checkPeerAudioVideo() {
* Room and Peer name are ok Join Channel
*/
async function whoAreYouJoin() {
elemDisplay(myVideoWrap, true);
myVideoParagraph.innerText = myPeerName + ' (me)';
setPeerAvatarImgName('myVideoAvatarImage', myPeerName);
setPeerAvatarImgName('myProfileAvatar', myPeerName);
setPeerChatAvatarImgName('right', myPeerName);
joinToChannel();
handleHideMe(isHideMeActive);
}
/**
@@ -1574,6 +1651,8 @@ async function joinToChannel() {
peer_info: peerInfo,
peer_uuid: myPeerUUID,
peer_name: myPeerName,
peer_username: myUsername,
peer_password: myPassword,
peer_video: useVideo,
peer_audio: useAudio,
peer_video_status: myVideoStatus,
@@ -2404,10 +2483,11 @@ async function loadLocalMedia(stream, kind) {
// session time
mySessionTime.setAttribute('id', 'mySessionTime');
mySessionTime.className = 'notranslate';
// my peer name
myPeerName.setAttribute('id', 'myVideoParagraph');
myPeerName.className = 'videoPeerName';
myPeerName.className = 'videoPeerName notranslate';
// my hand status element
myHandStatusIcon.setAttribute('id', 'myHandStatusIcon');
@@ -2792,7 +2872,7 @@ async function loadRemoteMediaStream(stream, peers, peer_id, kind) {
remoteMedia.setAttribute('id', peer_id + '___video');
remoteMedia.setAttribute('playsinline', true);
remoteMedia.autoplay = true;
isMobileDevice ? (remoteMediaControls = false) : (remoteMediaControls = remoteMediaControls);
remoteMediaControls = isMobileDevice ? false : remoteMediaControls;
remoteMedia.style.objectFit = peer_screen_status ? 'contain' : 'var(--video-object-fit)';
remoteMedia.style.name = peer_id + (peer_screen_status ? '_typeScreen' : '_typeCam');
remoteMedia.controls = remoteMediaControls;
@@ -4965,7 +5045,8 @@ async function shareRoomUrl() {
if (navigator.share) {
try {
// not add title and description to load metadata from url
await navigator.share({ url: myRoomUrl });
const roomURL = getRoomURL();
await navigator.share({ url: roomURL });
userLog('toast', 'Room Shared successfully!');
} catch (err) {
/*
@@ -4988,6 +5069,7 @@ async function shareRoomUrl() {
*/
function shareRoomMeetingURL(checkScreen = false) {
playSound('newMessage');
const roomURL = getRoomURL();
Swal.fire({
background: swBg,
position: 'center',
@@ -4999,7 +5081,7 @@ function shareRoomMeetingURL(checkScreen = false) {
<br/>
<p style="color:rgb(8, 189, 89);">Join from your mobile device</p>
<p style="background:transparent; color:white; font-family: Arial, Helvetica, sans-serif;">No need for apps, simply capture the QR code with your mobile camera Or Invite someone else to join by sending them the following URL</p>
<p style="color:rgb(8, 189, 89);">${myRoomUrl}</p>`,
<p style="color:rgb(8, 189, 89);">${roomURL}</p>`,
showDenyButton: true,
showCancelButton: true,
cancelButtonColor: 'red',
@@ -5039,7 +5121,7 @@ function makeRoomQR() {
* Copy Room URL to clipboard
*/
function copyRoomURL() {
const roomURL = window.location.href;
const roomURL = getRoomURL();
const tmpInput = document.createElement('input');
document.body.appendChild(tmpInput);
tmpInput.value = roomURL;
@@ -5069,11 +5151,12 @@ function shareRoomByEmail() {
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
preConfirm: () => {
const roomURL = getRoomURL();
const selectedDateTime = document.getElementById('datetimePicker').value;
const newLine = '%0D%0A%0D%0A';
const email = '';
const emailSubject = `Please join our MiroTalk P2P Video Chat Meeting`;
const emailBody = `The meeting is scheduled at: ${newLine} DateTime: ${selectedDateTime} ${newLine} Click to join: ${myRoomUrl} ${newLine}`;
const emailBody = `The meeting is scheduled at: ${newLine} DateTime: ${selectedDateTime} ${newLine} Click to join: ${roomURL} ${newLine}`;
document.location = 'mailto:' + email + '?subject=' + emailSubject + '&body=' + emailBody;
},
});
@@ -5084,6 +5167,16 @@ function shareRoomByEmail() {
});
}
/**
* Get Room URL
* @returns {url} roomURL
*/
function getRoomURL() {
return isHostProtected && isPeerAuthEnabled
? window.location.origin + '/join/?room=' + roomId + '&username=' + myUsername + '&password=' + myPassword
: myRoomUrl;
}
/**
* Handle Audio ON - OFF
* @param {object} e event
@@ -7071,9 +7164,7 @@ function handleHideMe(isHideMeActive) {
setColor(hideMeBtn, 'black');
playSound('on');
}
if (Object.keys(peerConnections).length === 1) {
resizeVideoMedia();
}
resizeVideoMedia();
}
/**
+8 -8
View File
@@ -71,14 +71,14 @@
i
? (i.classList.add('is-active'), l.classList.add('is-active'))
: 'next' === t
? (e.firstElementChild.classList.add('is-active'),
e.parentNode
.getElementsByClassName('carousel-bullets')[0]
.firstElementChild.classList.add('is-active'))
: (e.lastElementChild.classList.add('is-active'),
e.parentNode
.getElementsByClassName('carousel-bullets')[0]
.lastElementChild.classList.add('is-active'));
? (e.firstElementChild.classList.add('is-active'),
e.parentNode
.getElementsByClassName('carousel-bullets')[0]
.firstElementChild.classList.add('is-active'))
: (e.lastElementChild.classList.add('is-active'),
e.parentNode
.getElementsByClassName('carousel-bullets')[0]
.lastElementChild.classList.add('is-active'));
}
function i(e, t) {
+1 -1
View File
@@ -611,7 +611,7 @@ access to use this app.
<div id="tabRecording" class="tabcontent">
<img id="recImage" src="../images/recording.png" />
<div style="display: table-row">
<span class="clw" id="recordingTime"></span>
<span class="clw notranslate" id="recordingTime"></span>
<br />
<button id="pauseRecBtn" class="buttons" style="display: none">
<i class="far fa-pause-circle"></i>
+4
View File
@@ -153,6 +153,10 @@
.then(function (response) {
console.log(response);
// Store in session
window.sessionStorage.peer_username = username;
window.sessionStorage.peer_password = password;
if (room) {
return (window.location.href =
'/join/' + window.location.search);