[mirotalk] - add select devices before join room
This commit is contained in:
+28
-26
@@ -90,7 +90,8 @@ body {
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
background: url('../images/bg.svg');
|
||||
/* background: url('../images/bg.svg'); */
|
||||
background: var(--body-bg);
|
||||
}
|
||||
|
||||
/*--------------------------------------------------------------
|
||||
@@ -106,22 +107,35 @@ body {
|
||||
}
|
||||
|
||||
/*--------------------------------------------------------------
|
||||
# Loading...
|
||||
# Init User
|
||||
--------------------------------------------------------------*/
|
||||
|
||||
#loadingDiv {
|
||||
color: #fff;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
#loadingDiv h1 {
|
||||
font-size: 60px;
|
||||
font-family: 'Comfortaa';
|
||||
}
|
||||
#loadingDiv pre {
|
||||
.init-user {
|
||||
display: block;
|
||||
padding: 5px;
|
||||
font-family: 'Comfortaa';
|
||||
}
|
||||
|
||||
.init-user select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background: #2c2c2c;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.init-user button {
|
||||
margin-top: 15px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
width: 40px;
|
||||
background: white;
|
||||
color: black;
|
||||
font-size: 1.5rem;
|
||||
padding: 4px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/*--------------------------------------------------------------
|
||||
@@ -174,18 +188,6 @@ body {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#initAudioBtn,
|
||||
#initVideoBtn {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
width: 40px;
|
||||
background: white;
|
||||
color: black;
|
||||
font-size: 1.5rem;
|
||||
padding: 4px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
#buttonsBar #leaveRoomBtn {
|
||||
color: #ff2d00;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.8 MiB After Width: | Height: | Size: 52 KiB |
+169
-36
@@ -19,7 +19,9 @@
|
||||
*
|
||||
*/
|
||||
|
||||
'use strict'; // https://www.w3schools.com/js/js_strict.asp
|
||||
'use strict';
|
||||
|
||||
// https://www.w3schools.com/js/js_strict.asp
|
||||
|
||||
const signalingServer = getSignalingServer();
|
||||
const roomId = getRoomId();
|
||||
@@ -245,6 +247,7 @@ let isRecScreenStream = false;
|
||||
let isChatPasteTxt = false;
|
||||
let needToCreateOffer = false; // after session description answer
|
||||
let signalingSocket; // socket.io connection to our webserver
|
||||
let initStream; // initial webcam stream
|
||||
let localMediaStream; // my microphone / webcam
|
||||
let remoteMediaStream; // peers microphone / webcam
|
||||
let recScreenStream; // recorded screen stream
|
||||
@@ -262,6 +265,11 @@ let countTime; // conference count time
|
||||
// init audio-video
|
||||
let initAudioBtn;
|
||||
let initVideoBtn;
|
||||
// init Devices select
|
||||
let initVideo;
|
||||
let initVideoSelect;
|
||||
let initMicrophoneSelect;
|
||||
let initSpeakerSelect;
|
||||
// buttons bar
|
||||
let buttonsBar;
|
||||
let shareRoomBtn;
|
||||
@@ -420,11 +428,19 @@ let speechRecognitionIcon;
|
||||
let speechRecognitionStart;
|
||||
let speechRecognitionStop;
|
||||
|
||||
// Local Storage class
|
||||
let lS = new LocalStorage();
|
||||
|
||||
/**
|
||||
* Load all Html elements by Id
|
||||
*/
|
||||
function getHtmlElementsById() {
|
||||
countTime = getId('countTime');
|
||||
// Init devices select
|
||||
initVideo = getId('initVideo');
|
||||
initVideoSelect = getId('initVideoSelect');
|
||||
initMicrophoneSelect = getId('initMicrophoneSelect');
|
||||
initSpeakerSelect = getId('initSpeakerSelect');
|
||||
// my video
|
||||
myVideo = getId('myVideo');
|
||||
myVideoWrap = getId('myVideoWrap');
|
||||
@@ -1011,6 +1027,7 @@ function handleButtonsRule() {
|
||||
*/
|
||||
async function whoAreYou() {
|
||||
console.log('11. Who are you?');
|
||||
|
||||
if (myPeerName) {
|
||||
checkPeerAudioVideo();
|
||||
whoAreYouJoin();
|
||||
@@ -1020,21 +1037,28 @@ async function whoAreYou() {
|
||||
|
||||
playSound('newMessage');
|
||||
|
||||
loadLocalStorage();
|
||||
|
||||
// start cam init stream
|
||||
|
||||
if (useVideo && initVideoSelect) {
|
||||
changeCamera(initVideoSelect.value);
|
||||
myVideoChange = true;
|
||||
refreshLocalMedia();
|
||||
}
|
||||
|
||||
const initUser = getId('initUser');
|
||||
initUser.classList.toggle('hidden');
|
||||
|
||||
Swal.fire({
|
||||
allowOutsideClick: false,
|
||||
allowEscapeKey: false,
|
||||
background: swalBackground,
|
||||
title: 'MiroTalk P2P',
|
||||
position: 'center',
|
||||
imageAlt: 'mirotalk-name',
|
||||
imageUrl: welcomeImg,
|
||||
title: 'Enter your name',
|
||||
input: 'text',
|
||||
inputValue: window.localStorage.peer_name ? window.localStorage.peer_name : '',
|
||||
html: `<br>
|
||||
<div style="padding: 10px;">
|
||||
<button id="initAudioBtn" class="${className.audioOn}" onclick="handleAudio(event, true)"></button>
|
||||
<button id="initVideoBtn" class="${className.videoOn}" onclick="handleVideo(event, true)"></button>
|
||||
</div>`,
|
||||
html: initUser, // inject html
|
||||
confirmButtonText: `Join meeting`,
|
||||
showClass: {
|
||||
popup: 'animate__animated animate__fadeInDown',
|
||||
@@ -1052,8 +1076,31 @@ async function whoAreYou() {
|
||||
playSound('addPeer');
|
||||
});
|
||||
|
||||
// select video - audio
|
||||
|
||||
initVideoSelect.onchange = () => {
|
||||
videoSelect.selectedIndex = initVideoSelect.selectedIndex;
|
||||
lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value);
|
||||
changeCamera(initVideoSelect.value);
|
||||
myVideoChange = true;
|
||||
refreshLocalMedia();
|
||||
};
|
||||
initMicrophoneSelect.onchange = () => {
|
||||
audioInputSelect.selectedIndex = initMicrophoneSelect.selectedIndex;
|
||||
lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, audioInputSelect.selectedIndex, audioInputSelect.value);
|
||||
myVideoChange = false;
|
||||
refreshLocalMedia();
|
||||
};
|
||||
initSpeakerSelect.onchange = () => {
|
||||
audioOutputSelect.selectedIndex = initSpeakerSelect.selectedIndex;
|
||||
lS.setLocalStorageDevices(lS.MEDIA_TYPE.speaker, audioOutputSelect.selectedIndex, audioOutputSelect.value);
|
||||
changeAudioDestination();
|
||||
};
|
||||
|
||||
if (isMobileDevice) return;
|
||||
|
||||
// init video -audio buttons
|
||||
|
||||
initAudioBtn = getId('initAudioBtn');
|
||||
initVideoBtn = getId('initVideoBtn');
|
||||
|
||||
@@ -1069,6 +1116,76 @@ async function whoAreYou() {
|
||||
setTippy(initVideoBtn, 'Stop the video', 'top');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings from Local Storage
|
||||
*/
|
||||
function loadLocalStorage() {
|
||||
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;
|
||||
//
|
||||
audioInputSelect.selectedIndex = initMicrophoneSelect.selectedIndex;
|
||||
audioOutputSelect.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;
|
||||
audioInputSelect.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;
|
||||
audioOutputSelect.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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change init camera by device id
|
||||
* @param {string} deviceId
|
||||
*/
|
||||
function changeCamera(deviceId) {
|
||||
if (initStream) {
|
||||
stopTracks(initStream);
|
||||
initVideo.style.display = 'block';
|
||||
}
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ video: { deviceId: deviceId } })
|
||||
.then((camStream) => {
|
||||
initVideo.srcObject = camStream;
|
||||
initStream = camStream;
|
||||
console.log('Success attached init video stream');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[Error] changeCamera', err);
|
||||
userLog('error', 'Error while swapping camera' + err, 'top-end');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check peer audio and video &audio=1&video=1
|
||||
* 1/true = enabled / 0/false = disabled
|
||||
@@ -1092,7 +1209,7 @@ function checkPeerAudioVideo() {
|
||||
/**
|
||||
* Room and Peer name are ok Join Channel
|
||||
*/
|
||||
function whoAreYouJoin() {
|
||||
async function whoAreYouJoin() {
|
||||
myVideoWrap.style.display = 'inline';
|
||||
myVideoParagraph.innerHTML = myPeerName + ' (me)';
|
||||
setPeerAvatarImgName('myVideoAvatarImage', myPeerName, useAvatarApi);
|
||||
@@ -1122,6 +1239,7 @@ async function joinToChannel() {
|
||||
peer_rec_status: isRecScreenStream,
|
||||
peer_privacy_status: isVideoPrivacyActive,
|
||||
});
|
||||
handleBodyOnMouseMove(); // show/hide buttonsBar...
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1559,8 +1677,8 @@ function setButtonsBarPosition(position) {
|
||||
*/
|
||||
async function initEnumerateDevices() {
|
||||
console.log('05. init Enumerate Devices');
|
||||
await initEnumerateAudioDevices();
|
||||
await initEnumerateVideoDevices();
|
||||
await initEnumerateAudioDevices();
|
||||
if (!useAudio && !useVideo) {
|
||||
initEnumerateDevicesFailed = true;
|
||||
playSound('alert');
|
||||
@@ -1634,20 +1752,25 @@ function enumerateAudioDevices(stream) {
|
||||
.enumerateDevices()
|
||||
.then((devices) =>
|
||||
devices.forEach((device) => {
|
||||
let el = null;
|
||||
let el,
|
||||
eli = null;
|
||||
if ('audioinput' === device.kind) {
|
||||
el = getId('audioSource');
|
||||
eli = getId('initMicrophoneSelect');
|
||||
} else if ('audiooutput' === device.kind) {
|
||||
el = getId('audioOutput');
|
||||
eli = getId('initSpeakerSelect');
|
||||
}
|
||||
if (!el) return;
|
||||
addChild(device, el, device.kind);
|
||||
addChild(device, [el, eli]);
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
stopTracks(stream);
|
||||
isEnumerateAudioDevices = true;
|
||||
getId('audioOutput').disabled = !('sinkId' in HTMLMediaElement.prototype);
|
||||
const sinkId = 'sinkId' in HTMLMediaElement.prototype;
|
||||
getId('audioOutput').disabled = !sinkId;
|
||||
if (!sinkId) getId('initSpeakerSelect').display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1661,12 +1784,14 @@ function enumerateVideoDevices(stream) {
|
||||
.enumerateDevices()
|
||||
.then((devices) =>
|
||||
devices.forEach((device) => {
|
||||
let el = null;
|
||||
let el,
|
||||
eli = null;
|
||||
if ('videoinput' === device.kind) {
|
||||
el = getId('videoSource');
|
||||
eli = getId('initVideoSelect');
|
||||
}
|
||||
if (!el) return;
|
||||
addChild(device, el, device.kind);
|
||||
addChild(device, [el, eli]);
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
@@ -1688,24 +1813,28 @@ function stopTracks(stream) {
|
||||
/**
|
||||
* Add child to element
|
||||
* @param {object} device
|
||||
* @param {object} el
|
||||
* @param {string} kind audio/video
|
||||
* @param {object} els
|
||||
*/
|
||||
function addChild(device, el, kind) {
|
||||
const option = document.createElement('option');
|
||||
option.value = device.deviceId;
|
||||
switch (kind) {
|
||||
case 'videoinput':
|
||||
option.text = `📹 ` + device.label || `📹 camera ${el.length + 1}`;
|
||||
break;
|
||||
case 'audioinput':
|
||||
option.text = `🎤 ` + device.label || `🎤 microphone ${el.length + 1}`;
|
||||
break;
|
||||
case 'audiooutput':
|
||||
option.text = `🔈 ` + device.label || `🔈 speaker ${el.length + 1}`;
|
||||
break;
|
||||
}
|
||||
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);
|
||||
});
|
||||
selectors = [getId('audioSource'), getId('audioOutput'), getId('videoSource')];
|
||||
}
|
||||
|
||||
@@ -1764,8 +1893,6 @@ async function setupLocalMedia() {
|
||||
*/
|
||||
async function loadLocalMedia(stream) {
|
||||
console.log('10. Access granted to audio - video device');
|
||||
// hide loading div
|
||||
getId('loadingDiv').style.display = 'none';
|
||||
|
||||
localMediaStream = stream;
|
||||
|
||||
@@ -1909,7 +2036,6 @@ async function loadLocalMedia(stream) {
|
||||
setupMySettings();
|
||||
setupVideoUrlPlayer();
|
||||
startCountTime();
|
||||
handleBodyOnMouseMove();
|
||||
|
||||
if (isVideoFullScreenSupported) {
|
||||
handleVideoPlayerFs(myLocalMedia.id, myVideoFullScreenBtn.id);
|
||||
@@ -3281,15 +3407,18 @@ function setupMySettings() {
|
||||
audioInputSelect.addEventListener('change', (e) => {
|
||||
myVideoChange = false;
|
||||
refreshLocalMedia();
|
||||
lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, audioInputSelect.selectedIndex, audioInputSelect.value);
|
||||
});
|
||||
// select audio output
|
||||
audioOutputSelect.addEventListener('change', (e) => {
|
||||
changeAudioDestination();
|
||||
lS.setLocalStorageDevices(lS.MEDIA_TYPE.speaker, audioOutputSelect.selectedIndex, audioOutputSelect.value);
|
||||
});
|
||||
// select video input
|
||||
videoSelect.addEventListener('change', (e) => {
|
||||
myVideoChange = true;
|
||||
refreshLocalMedia();
|
||||
lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value);
|
||||
});
|
||||
// select video quality
|
||||
videoQualitySelect.addEventListener('change', (e) => {
|
||||
@@ -3846,6 +3975,8 @@ function handleAudio(e, init, force = null) {
|
||||
if (init) {
|
||||
audioBtn.className = myAudioStatus ? className.audioOn : className.audioOff;
|
||||
setTippy(initAudioBtn, myAudioStatus ? 'Stop the audio' : 'Start the audio', 'top');
|
||||
getId('initMicrophoneSelect').disabled = !myAudioStatus;
|
||||
getId('initSpeakerSelect').disabled = !myAudioStatus;
|
||||
}
|
||||
setMyAudioStatus(myAudioStatus);
|
||||
}
|
||||
@@ -3871,6 +4002,8 @@ function handleVideo(e, init, force = null) {
|
||||
if (init) {
|
||||
videoBtn.className = myVideoStatus ? className.videoOn : className.videoOff;
|
||||
setTippy(initVideoBtn, myVideoStatus ? 'Stop the video' : 'Start the video', 'top');
|
||||
initVideo.style.display = myVideoStatus ? 'block' : 'none';
|
||||
initVideoSelect.disabled = !myVideoStatus;
|
||||
}
|
||||
setMyVideoStatus(myVideoStatus);
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
+18
-10
@@ -66,19 +66,26 @@
|
||||
<h1>WebRTC</h1>
|
||||
</div>
|
||||
|
||||
<!-- show this before to join -->
|
||||
<!-- init user devices -->
|
||||
|
||||
<div id="loadingDiv" class="center pulsate">
|
||||
<h1>Loading...</h1>
|
||||
<pre>
|
||||
Please allow the camera or microphone
|
||||
access to use this app.
|
||||
</pre>
|
||||
<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=""
|
||||
class="mirror"
|
||||
poster="../images/loader.gif"
|
||||
style="object-fit: var(--videoObjFit)"
|
||||
></video>
|
||||
<button id="initAudioBtn" class="fas fa-microphone" onclick="handleAudio(event, true)"></button>
|
||||
<button id="initVideoBtn" class="fas fa-video" onclick="handleVideo(event, true)"></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>
|
||||
|
||||
<!-- Start buttons bar
|
||||
https://fontawesome.com/icons?d=gallery
|
||||
-->
|
||||
<!-- Start buttons bar https://fontawesome.com/icons?d=gallery -->
|
||||
|
||||
<div id="buttonsBar" class="fadein">
|
||||
<button id="shareRoomBtn" class="fas fa-users"></button>
|
||||
@@ -476,6 +483,7 @@ access to use this app.
|
||||
<script defer src="https://unpkg.com/@popperjs/core@2"></script>
|
||||
<script defer src="https://unpkg.com/tippy.js@6"></script>
|
||||
<script defer src="/socket.io/socket.io.js"></script>
|
||||
<script defer src="../js/localStorage.js"></script>
|
||||
<script defer src="../js/client.js"></script>
|
||||
<script defer src="../js/detectSpeaking.js"></script>
|
||||
<script defer src="../js/speechRecognition.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user