[mirotalksfu] - add select devices before join

هذا الالتزام موجود في:
Miroslav Pejic
2023-01-20 17:31:11 +01:00
الأصل 7a1db0edc5
التزام 8f3ead242b
7 ملفات معدلة مع 326 إضافات و184 حذوفات

عرض الملف

@@ -34,9 +34,10 @@ dependencies: {
* @link GitHub: https://github.com/miroslavpejic85/mirotalksfu
* @link Live demo: https://sfu.mirotalk.com
* @license For open source use: AGPLv3
* @license For commercial or closed source, contact us at info.mirotalk@gmail.com
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or buy 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.0.0
* @version 1.0.1
*
*/

عرض الملف

@@ -1,6 +1,6 @@
{
"name": "mirotalksfu",
"version": "1.0.0",
"version": "1.0.1",
"description": "WebRTC SFU browser-based video calls",
"main": "Server.js",
"scripts": {

عرض الملف

@@ -81,7 +81,8 @@ body {
width: 100%;
height: 100%;
overflow: hidden;
background: url('../images/background.jpg');
/* background: url('../images/background.jpg'); */
background: var(--body-bg);
}
/*--------------------------------------------------------------
@@ -97,27 +98,22 @@ body {
}
/*--------------------------------------------------------------
# Loading...
# Init User
--------------------------------------------------------------*/
#loadingDiv {
color: #fff;
padding: 30px;
border-radius: 10px;
background: transparent;
.init-user {
display: block;
padding: 5px;
}
#loadingDiv h1 {
.init-user select {
margin-top: 15px;
padding: 10px;
font-size: 60px;
font-family: 'Comfortaa';
background: rgba(0, 0, 0, 0.7);
border-radius: 10px;
cursor: pointer;
}
#loadingDiv p {
padding: 10px;
font-family: 'Comfortaa';
background: rgba(0, 0, 0, 0.7);
border-radius: 10px;
.init-user button {
margin-top: 15px;
}
/*--------------------------------------------------------------

62
public/js/LocalStorage.js Normal file
عرض الملف

@@ -0,0 +1,62 @@
'use-strict';
class LocalStorage {
constructor() {
this.MEDIA_TYPE = {
audio: 'audio',
video: 'video',
speaker: 'speaker',
};
this.DEVICES_COUNT = {
audio: 0,
speaker: 0,
video: 0,
};
this.LOCAL_STORAGE_DEVICES = {
audio: {
count: 0,
index: 0,
select: null,
},
speaker: {
count: 0,
index: 0,
select: null,
},
video: {
count: 0,
index: 0,
select: null,
},
};
}
setLocalStorageDevices(type, index, select) {
switch (type) {
case this.MEDIA_TYPE.audio:
this.LOCAL_STORAGE_DEVICES.audio.count = this.DEVICES_COUNT.audio;
this.LOCAL_STORAGE_DEVICES.audio.index = index;
this.LOCAL_STORAGE_DEVICES.audio.select = select;
break;
case this.MEDIA_TYPE.video:
this.LOCAL_STORAGE_DEVICES.video.count = this.DEVICES_COUNT.video;
this.LOCAL_STORAGE_DEVICES.video.index = index;
this.LOCAL_STORAGE_DEVICES.video.select = select;
break;
case this.MEDIA_TYPE.speaker:
this.LOCAL_STORAGE_DEVICES.speaker.count = this.DEVICES_COUNT.speaker;
this.LOCAL_STORAGE_DEVICES.speaker.index = index;
this.LOCAL_STORAGE_DEVICES.speaker.select = select;
break;
default:
break;
}
localStorage.setItem('LOCAL_STORAGE_DEVICES', JSON.stringify(this.LOCAL_STORAGE_DEVICES));
}
getLocalStorageDevices() {
return JSON.parse(localStorage.getItem('LOCAL_STORAGE_DEVICES'));
}
}

عرض الملف

@@ -8,9 +8,10 @@ if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.h
* @link GitHub: https://github.com/miroslavpejic85/mirotalksfu
* @link Live demo: https://sfu.mirotalk.com
* @license For open source use: AGPLv3
* @license For commercial or closed source, contact us at info.mirotalk@gmail.com
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or buy 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.0.0
* @version 1.0.1
*
*/
@@ -53,6 +54,8 @@ const wbHeight = 600;
const swalImageUrl = '../images/pricing-illustration.svg';
const lS = new LocalStorage();
// ####################################################
// DYNAMIC SETTINGS
// ####################################################
@@ -109,6 +112,8 @@ let isButtonsBarOver = false;
let isRoomLocked = false;
let initStream = null;
// ####################################################
// INIT ROOM
// ####################################################
@@ -223,31 +228,27 @@ function makeId(length) {
async function initEnumerateDevices() {
console.log('01 ----> init Enumerate Devices');
await initEnumerateAudioDevices();
whoAreYou();
await initEnumerateVideoDevices();
await initEnumerateAudioDevices();
if (!isVideoAllowed) {
hide(initVideo);
hide(initVideoSelect);
}
if (!isAudioAllowed) {
hide(initMicrophoneSelect);
hide(initSpeakerSelect);
}
if (!isAudioAllowed && !isVideoAllowed && !joinRoomWithoutAudioVideo) {
openURL(`/permission?room_id=${room_id}&message=Not allowed both Audio and Video`);
} else {
hide(loadingDiv);
setButtonsInit();
setSelectsInit();
handleSelectsInit();
getPeerGeoLocation();
whoAreYou();
}
}
async function initEnumerateAudioDevices() {
if (isEnumerateAudioDevices) return;
// allow the audio
await navigator.mediaDevices
.getUserMedia({ audio: true })
.then((stream) => {
enumerateAudioDevices(stream);
isAudioAllowed = true;
})
.catch(() => {
isAudioAllowed = false;
});
}
async function initEnumerateVideoDevices() {
if (isEnumerateVideoDevices) return;
// allow the video
@@ -262,31 +263,6 @@ async function initEnumerateVideoDevices() {
});
}
function enumerateAudioDevices(stream) {
console.log('02 ----> Get Audio Devices');
navigator.mediaDevices
.enumerateDevices()
.then((devices) =>
devices.forEach((device) => {
let el = null;
if ('audioinput' === device.kind) {
el = microphoneSelect;
RoomClient.DEVICES_COUNT.audio++;
} else if ('audiooutput' === device.kind) {
el = speakerSelect;
RoomClient.DEVICES_COUNT.speaker++;
}
if (!el) return;
addChild(device, el);
}),
)
.then(() => {
stopTracks(stream);
isEnumerateAudioDevices = true;
speakerSelect.disabled = !('sinkId' in HTMLMediaElement.prototype);
});
}
function enumerateVideoDevices(stream) {
console.log('03 ----> Get Video Devices');
navigator.mediaDevices
@@ -294,12 +270,14 @@ function enumerateVideoDevices(stream) {
.then((devices) =>
devices.forEach((device) => {
let el = null;
let eli = null;
if ('videoinput' === device.kind) {
el = videoSelect;
RoomClient.DEVICES_COUNT.video++;
eli = initVideoSelect;
lS.DEVICES_COUNT.video++;
}
if (!el) return;
addChild(device, el);
addChild(device, [el, eli]);
}),
)
.then(() => {
@@ -308,17 +286,76 @@ function enumerateVideoDevices(stream) {
});
}
async function initEnumerateAudioDevices() {
if (isEnumerateAudioDevices) return;
// allow the audio
await navigator.mediaDevices
.getUserMedia({ audio: true })
.then((stream) => {
enumerateAudioDevices(stream);
isAudioAllowed = true;
})
.catch(() => {
isAudioAllowed = false;
});
}
function enumerateAudioDevices(stream) {
console.log('02 ----> Get Audio Devices');
navigator.mediaDevices
.enumerateDevices()
.then((devices) =>
devices.forEach((device) => {
let el = null;
let eli = null;
if ('audioinput' === device.kind) {
el = microphoneSelect;
eli = initMicrophoneSelect;
lS.DEVICES_COUNT.audio++;
} else if ('audiooutput' === device.kind) {
el = speakerSelect;
eli = initSpeakerSelect;
lS.DEVICES_COUNT.speaker++;
}
if (!el) return;
addChild(device, [el, eli]);
}),
)
.then(() => {
stopTracks(stream);
isEnumerateAudioDevices = true;
const sinkId = 'sinkId' in HTMLMediaElement.prototype;
speakerSelect.disabled = !sinkId;
if (!sinkId) hide(initSpeakerSelect);
});
}
function stopTracks(stream) {
stream.getTracks().forEach((track) => {
track.stop();
});
}
function addChild(device, el) {
let option = document.createElement('option');
option.value = device.deviceId;
option.innerText = device.label;
el.appendChild(option);
function addChild(device, els) {
let kind = device.kind;
els.forEach((el) => {
let option = document.createElement('option');
option.value = device.deviceId;
switch (kind) {
case 'videoinput':
option.innerHTML = `📹 ` + device.label || `📹 camera ${el.length + 1}`;
break;
case 'audioinput':
option.innerHTML = `🎤 ` + device.label || `🎤 microphone ${el.length + 1}`;
break;
case 'audiooutput':
option.innerHTML = `🔈 ` + device.label || `🔈 speaker ${el.length + 1}`;
break;
default:
break;
}
el.appendChild(option);
});
}
// ####################################################
@@ -407,6 +444,7 @@ function getPeerGeoLocation() {
function whoAreYou() {
console.log('04 ----> Who are you');
sound('open');
if (peer_name) {
checkMedia();
@@ -420,21 +458,18 @@ function whoAreYou() {
default_name = getCookie(room_id + '_name');
}
const initUser = document.getElementById('initUser');
initUser.classList.toggle('hidden');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swalBackground,
imageAlt: 'mirotalksfu-username',
imageUrl: image.username,
title: 'MiroTalk SFU',
input: 'text',
inputPlaceholder: 'Enter your name',
inputValue: default_name,
html: `<br />
<div style="padding: 10px;">
<button id="initAudioButton" class="fas fa-microphone" onclick="handleAudio(event)"></button>
<button id="initVideoButton" class="fas fa-video" onclick="handleVideo(event)"></button>
<button id="initAudioVideoButton" class="fas fa-eye" onclick="handleAudioVideo(event)"></button>
</div>`,
html: initUser, // Inject HTML
confirmButtonText: `Join meeting`,
showClass: {
popup: 'animate__animated animate__fadeInDown',
@@ -451,23 +486,13 @@ function whoAreYou() {
peer_name = name;
},
}).then(() => {
if (initStream) {
stopTracks(initStream);
hide(initVideo);
}
getPeerInfo();
joinRoom(peer_name, room_id);
});
if (!DetectRTC.isMobileDevice) {
setTippy('initAudioButton', 'Toggle the audio', 'left');
setTippy('initVideoButton', 'Toggle the video', 'right');
setTippy('initAudioVideoButton', 'Toggle the audio & video', 'right');
}
initAudioButton = document.getElementById('initAudioButton');
initVideoButton = document.getElementById('initVideoButton');
initAudioVideoButton = document.getElementById('initAudioVideoButton');
if (!isAudioAllowed) hide(initAudioButton);
if (!isVideoAllowed) hide(initVideoButton);
if (!isAudioAllowed || !isVideoAllowed) hide(initAudioVideoButton);
isAudioVideoAllowed = isAudioAllowed && isVideoAllowed;
}
function handleAudio(e) {
@@ -475,6 +500,7 @@ function handleAudio(e) {
e.target.className = 'fas fa-microphone' + (isAudioAllowed ? '' : '-slash');
setColor(e.target, isAudioAllowed ? 'white' : 'red');
setColor(startAudioButton, isAudioAllowed ? 'white' : 'red');
checkInitAudio(isAudioAllowed);
}
function handleVideo(e) {
@@ -482,6 +508,7 @@ function handleVideo(e) {
e.target.className = 'fas fa-video' + (isVideoAllowed ? '' : '-slash');
setColor(e.target, isVideoAllowed ? 'white' : 'red');
setColor(startVideoButton, isVideoAllowed ? 'white' : 'red');
checkInitVideo(isVideoAllowed);
}
function handleAudioVideo(e) {
@@ -500,6 +527,28 @@ function handleAudioVideo(e) {
setColor(initVideoButton, isVideoAllowed ? 'white' : 'red');
setColor(startAudioButton, isAudioAllowed ? 'white' : 'red');
setColor(startVideoButton, isVideoAllowed ? 'white' : 'red');
checkInitVideo(isVideoAllowed);
checkInitAudio(isAudioAllowed);
}
function checkInitVideo(isVideoAllowed) {
if (isVideoAllowed) {
if (initVideoSelect.value) changeCamera(initVideoSelect.value);
sound('joined');
} else {
if (initStream) {
stopTracks(initStream);
hide(initVideo);
sound('left');
}
}
initVideoSelect.disabled = !isVideoAllowed;
}
function checkInitAudio(isAudioAllowed) {
initMicrophoneSelect.disabled = !isAudioAllowed;
initSpeakerSelect.disabled = !isAudioAllowed;
isAudioAllowed ? sound('joined') : sound('left');
}
function checkMedia() {
@@ -557,7 +606,7 @@ async function shareRoom(useNavigator = false) {
denyButtonText: `Email invite`,
cancelButtonText: `Close`,
showClass: {
popup: 'animate__animated animate__fadeInUp',
popup: 'animate__animated animate__fadeInDown',
},
hideClass: {
popup: 'animate__animated animate__fadeOutUp',
@@ -1003,25 +1052,119 @@ function handleButtons() {
}
// ####################################################
// HTML SELECTS
// HANDLE INIT USER
// ####################################################
function setButtonsInit() {
if (!DetectRTC.isMobileDevice) {
setTippy('initAudioButton', 'Toggle the audio', 'left');
setTippy('initVideoButton', 'Toggle the video', 'right');
setTippy('initAudioVideoButton', 'Toggle the audio & video', 'right');
}
initAudioButton = document.getElementById('initAudioButton');
initVideoButton = document.getElementById('initVideoButton');
initAudioVideoButton = document.getElementById('initAudioVideoButton');
if (!isAudioAllowed) hide(initAudioButton);
if (!isVideoAllowed) hide(initVideoButton);
if (!isAudioAllowed || !isVideoAllowed) hide(initAudioVideoButton);
isAudioVideoAllowed = isAudioAllowed && isVideoAllowed;
}
function handleSelectsInit() {
// devices init options
initVideoSelect.onchange = () => {
changeCamera(initVideoSelect.value);
videoSelect.selectedIndex = initVideoSelect.selectedIndex;
lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value);
};
initMicrophoneSelect.onchange = () => {
microphoneSelect.selectedIndex = initMicrophoneSelect.selectedIndex;
lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, microphoneSelect.selectedIndex, microphoneSelect.value);
};
initSpeakerSelect.onchange = () => {
speakerSelect.selectedIndex = initSpeakerSelect.selectedIndex;
lS.setLocalStorageDevices(lS.MEDIA_TYPE.speaker, initSpeakerSelect.selectedIndex, initSpeakerSelect.value);
};
}
function setSelectsInit() {
const localStorageDevices = lS.getLocalStorageDevices();
console.log('04 ----> Get Local Storage Devices before', localStorageDevices);
if (localStorageDevices) {
initMicrophoneSelect.selectedIndex = localStorageDevices.audio.index;
initSpeakerSelect.selectedIndex = localStorageDevices.speaker.index;
initVideoSelect.selectedIndex = localStorageDevices.video.index;
//
microphoneSelect.selectedIndex = initMicrophoneSelect.selectedIndex;
speakerSelect.selectedIndex = initSpeakerSelect.selectedIndex;
videoSelect.selectedIndex = initVideoSelect.selectedIndex;
//
if (lS.DEVICES_COUNT.audio != localStorageDevices.audio.count) {
console.log('04.1 ----> Audio devices seems changed, use default index 0');
initMicrophoneSelect.selectedIndex = 0;
microphoneSelect.selectedIndex = 0;
lS.setLocalStorageDevices(
lS.MEDIA_TYPE.audio,
initMicrophoneSelect.selectedIndex,
initMicrophoneSelect.value,
);
}
if (lS.DEVICES_COUNT.speaker != localStorageDevices.speaker.count) {
console.log('04.2 ----> Speaker devices seems changed, use default index 0');
initSpeakerSelect.selectedIndex = 0;
speakerSelect.selectedIndex = 0;
lS.setLocalStorageDevices(
lS.MEDIA_TYPE.speaker,
initSpeakerSelect.selectedIndexIndex,
initSpeakerSelect.value,
);
}
if (lS.DEVICES_COUNT.video != localStorageDevices.video.count) {
console.log('04.3 ----> Video devices seems changed, use default index 0');
initVideoSelect.selectedIndex = 0;
videoSelect.selectedIndex = 0;
lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, initVideoSelect.selectedIndex, initVideoSelect.value);
}
//
console.log('04.4 ----> Get Local Storage Devices after', lS.getLocalStorageDevices());
}
if (initVideoSelect.value) changeCamera(initVideoSelect.value);
}
function changeCamera(deviceId) {
if (initStream) {
stopTracks(initStream);
show(initVideo);
}
navigator.mediaDevices
.getUserMedia({ video: { deviceId: deviceId } })
.then((camStream) => {
initVideo.srcObject = camStream;
initStream = camStream;
console.log('04.5 ----> Success attached init video stream');
})
.catch((err) => {
console.error('[Error] changeCamera', err);
userLog('error', 'Error while swapping camera' + err.tostring(), 'top-end');
});
}
function handleSelects() {
// devices options
videoSelect.onchange = () => {
rc.closeThenProduce(RoomClient.mediaType.video, videoSelect.value);
rc.setLocalStorageDevices(RoomClient.mediaType.video, videoSelect.selectedIndex, videoSelect.value);
lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value);
};
videoQuality.onchange = () => {
rc.closeThenProduce(RoomClient.mediaType.video, videoSelect.value);
};
microphoneSelect.onchange = () => {
rc.closeThenProduce(RoomClient.mediaType.audio, microphoneSelect.value);
rc.setLocalStorageDevices(RoomClient.mediaType.audio, microphoneSelect.selectedIndex, microphoneSelect.value);
lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, microphoneSelect.selectedIndex, microphoneSelect.value);
};
speakerSelect.onchange = () => {
rc.attachSinkId(rc.myAudioEl, speakerSelect.value);
rc.setLocalStorageDevices(RoomClient.mediaType.speaker, speakerSelect.selectedIndex, speakerSelect.value);
initSpeakerSelect.onchange = () => {
rc.attachSinkId(rc.myAudioEl, initSpeakerSelect.value);
lS.setLocalStorageDevices(lS.MEDIA_TYPE.speaker, initSpeakerSelect.selectedIndex, initSpeakerSelect.value);
};
// room
switchSounds.onchange = (e) => {
@@ -2093,7 +2236,7 @@ function showAbout() {
</div>
`,
showClass: {
popup: 'animate__animated animate__fadeInUp',
popup: 'animate__animated animate__fadeInDown',
},
hideClass: {
popup: 'animate__animated animate__fadeOutUp',

عرض الملف

@@ -9,7 +9,7 @@
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or buy 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.0.0
* @version 1.0.1
*
*/
@@ -66,30 +66,6 @@ const mediaType = {
speaker: 'speakerType',
};
const LOCAL_STORAGE_DEVICES = {
audio: {
count: 0,
index: 0,
select: null,
},
speaker: {
count: 0,
index: 0,
select: null,
},
video: {
count: 0,
index: 0,
select: null,
},
};
const DEVICES_COUNT = {
audio: 0,
speaker: 0,
video: 0,
};
const _EVENTS = {
openRoom: 'openRoom',
exitRoom: 'exitRoom',
@@ -653,30 +629,7 @@ class RoomClient {
// ####################################################
startLocalMedia() {
let localStorageDevices = this.getLocalStorageDevices();
console.log('08 ----> Get Local Storage Devices before', localStorageDevices);
if (localStorageDevices) {
microphoneSelect.selectedIndex = localStorageDevices.audio.index;
speakerSelect.selectedIndex = localStorageDevices.speaker.index;
videoSelect.selectedIndex = localStorageDevices.video.index;
//
if (DEVICES_COUNT.audio != localStorageDevices.audio.count) {
console.log('08.1 ----> Audio devices seems changed, use default index 0');
microphoneSelect.selectedIndex = 0;
this.setLocalStorageDevices(mediaType.audio, microphoneSelect.selectedIndex, microphoneSelect.value);
}
if (DEVICES_COUNT.speaker != localStorageDevices.speaker.count) {
console.log('08.2 ----> Speaker devices seems changed, use default index 0');
speakerSelect.selectedIndex = 0;
this.setLocalStorageDevices(mediaType.speaker, speakerSelect.selectedIndex, speakerSelect.value);
}
if (DEVICES_COUNT.video != localStorageDevices.video.count) {
console.log('08.3 ----> Video devices seems changed, use default index 0');
videoSelect.selectedIndex = 0;
this.setLocalStorageDevices(mediaType.video, videoSelect.selectedIndex, videoSelect.value);
}
console.log('08.4 ----> Get Local Storage Devices after', this.getLocalStorageDevices());
}
console.log('08 ----> Start local media');
if (this.isAudioAllowed) {
console.log('09 ----> Start audio media');
this.produce(mediaType.audio, microphoneSelect.value);
@@ -1804,7 +1757,7 @@ class RoomClient {
console.error('Attach SinkId error: ', errorMessage);
this.userLog('error', errorMessage, 'top-end');
speakerSelect.selectedIndex = 0;
this.setLocalStorageDevices(mediaType.speaker, 0, speakerSelect.value);
lS.setLocalStorageDevices(lS.MEDIA_TYPE.speaker, 0, speakerSelect.value);
});
} else {
let error = `Browser seems doesn't support output device selection.`;
@@ -4191,33 +4144,4 @@ class RoomClient {
);
}
}
// ####################################################
// LOCAL STORAGE DEVICES
// ####################################################
setLocalStorageDevices(type, index, select) {
switch (type) {
case RoomClient.mediaType.audio:
LOCAL_STORAGE_DEVICES.audio.count = DEVICES_COUNT.audio;
LOCAL_STORAGE_DEVICES.audio.index = index;
LOCAL_STORAGE_DEVICES.audio.select = select;
break;
case RoomClient.mediaType.video:
LOCAL_STORAGE_DEVICES.video.count = DEVICES_COUNT.video;
LOCAL_STORAGE_DEVICES.video.index = index;
LOCAL_STORAGE_DEVICES.video.select = select;
break;
case RoomClient.mediaType.speaker:
LOCAL_STORAGE_DEVICES.speaker.count = DEVICES_COUNT.speaker;
LOCAL_STORAGE_DEVICES.speaker.index = index;
LOCAL_STORAGE_DEVICES.speaker.select = select;
break;
}
localStorage.setItem('LOCAL_STORAGE_DEVICES', JSON.stringify(LOCAL_STORAGE_DEVICES));
}
getLocalStorageDevices() {
return JSON.parse(localStorage.getItem('LOCAL_STORAGE_DEVICES'));
}
}

عرض الملف

@@ -73,6 +73,7 @@
<script defer src="/socket.io/socket.io.js"></script>
<script defer src="../sfu/MediasoupClient.js"></script>
<script defer src="../js/LocalStorage.js"></script>
<script defer src="../js/Rules.js"></script>
<script defer src="../js/Room.js"></script>
<script defer src="../js/RoomClient.js"></script>
@@ -90,10 +91,25 @@
<body onload="initClient()">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="loadingDiv" class="center pulsate">
<h1>Loading...</h1>
<p>Please allow the camera or microphone access to use this app.</p>
</div>
<section>
<div id="initUser" class="init-user hidden">
<p>Please allow the camera & microphone access to use this app.</p>
<video
id="initVideo"
playsinline="true"
autoplay=""
poster="../images/loader.gif"
class="mirror"
style="object-fit: var(--videoObjFit)"
></video>
<button id="initAudioButton" class="fas fa-microphone" onclick="handleAudio(event)"></button>
<button id="initVideoButton" class="fas fa-video" onclick="handleVideo(event)"></button>
<button id="initAudioVideoButton" class="fas fa-eye" onclick="handleAudioVideo(event)"></button>
<select id="initVideoSelect" class="form-select text-light bg-dark"></select>
<select id="initMicrophoneSelect" class="form-select text-light bg-dark"></select>
<select id="initSpeakerSelect" class="form-select text-light bg-dark"></select>
</div>
</section>
<div id="control" class="fadein">
<button id="shareButton" class="hidden"><i class="fas fa-share-alt"></i></button>