1512 lines
63 KiB
HTML
1512 lines
63 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" xmlns="http://www.w3.org/1999/html">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
transparent: 'transparent',
|
|
current: 'currentColor',
|
|
'main': '#262c3c',
|
|
'primary': '#202433',
|
|
'secondary': '#205295',
|
|
'accent': '#4797de',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<title>BoostedAudio Client</title>
|
|
</head>
|
|
<body class="flex h-screen w-screen bg-[url(https://i.imgur.com/6MelMqC.jpg)] bg-no-repeat bg-cover">
|
|
|
|
<div class="z-10 relative drop-shadow-lg hidden">
|
|
<div id="toogleDiv" class="hover:scale-150 duration-100 absolute top-1/2 -left-6">
|
|
<div id="toggleButton" class="h-10 w-14
|
|
rounded-3xl bg-main text-white cursor-pointer transition-transform duration-300 ease-in-out">
|
|
<svg class="h-full w-full relative left-3" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<!-- Sidebar and Toggle Button -->
|
|
<div id="sidebar"
|
|
class="w-64 bg-main text-white p-4 transform -translate-x-full transition-transform duration-300 ease-in-out fixed h-screen">
|
|
<!-- Sidebar content here -->
|
|
Sidebar Content
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Side bar
|
|
const sidebar = document.getElementById('sidebar');
|
|
const toggleButton = document.getElementById('toggleButton');
|
|
const toogleDiv = document.getElementById('toogleDiv');
|
|
let barOpen = false;
|
|
|
|
document.addEventListener('click', (event) => {
|
|
if (sidebar.contains(event.target) || toggleButton.contains(event.target) || sidebar.classList.contains("-translate-x-full")) return;
|
|
sidebar.classList.add('-translate-x-full');
|
|
toggleButton.classList.toggle('translate-x-64');
|
|
barOpen = false;
|
|
});
|
|
|
|
toggleButton.addEventListener('click', () => {
|
|
sidebar.classList.toggle('-translate-x-full');
|
|
toggleButton.classList.toggle('translate-x-64');
|
|
barOpen = !barOpen;
|
|
});
|
|
|
|
toogleDiv.addEventListener("click", e => {
|
|
if (toogleDiv.classList.contains("hover:scale-150")) {
|
|
toogleDiv.classList.remove("hover:scale-150")
|
|
} else {
|
|
setTimeout(() => {
|
|
if (!barOpen) {
|
|
toogleDiv.classList.add("hover:scale-150")
|
|
}
|
|
}, 300)
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<div class="w-full h-full
|
|
bg-gradient-to-t from-primary/80 via-secondary/80 to-primary/80">
|
|
<!--Connection-->
|
|
<div id="connection"
|
|
class="z-50 fixed bg-gray-800/60 left-0 w-full h-full flex justify-center items-center text-slate-200">
|
|
<div class="flex justify-center">
|
|
<p class="gray-animation relative bottom-20 text-5xl">Click on the page to connect</p>
|
|
</div>
|
|
</div>
|
|
<style>
|
|
@keyframes grayChange {
|
|
0% {
|
|
color: #f3f4f6;
|
|
}
|
|
50% {
|
|
color: #9CA3AF;
|
|
}
|
|
100% {
|
|
color: #f3f4f6;
|
|
}
|
|
}
|
|
|
|
.gray-animation {
|
|
animation: grayChange 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes redChange {
|
|
0% {
|
|
color: #f3f4f6;
|
|
}
|
|
50% {
|
|
color: #ef8080;
|
|
}
|
|
100% {
|
|
color: #f3f4f6;
|
|
}
|
|
}
|
|
|
|
.red-animation:hover {
|
|
animation: redChange 1.5s ease-in-out infinite;
|
|
}
|
|
</style>
|
|
<!--Settings-->
|
|
<div id="settings"
|
|
class="z-50 fixed top-0 left-0 w-full h-full bg-gray-800/50 flex justify-center items-center hidden text-slate-200">
|
|
<!--Params-->
|
|
<div id="params"
|
|
class="bg-main p-4 rounded-lg w-2/3 h-fit grid gap-5 drop-shadow-lg ring ring-primary ring-2 hover:ring-accent duration-300">
|
|
<!--Title-->
|
|
<div class="text-center">
|
|
<h1 class="text-xl font-bold">Settings</h1>
|
|
<label class="text-sm text-slate-400">Parameters of the audio client</label>
|
|
</div>
|
|
<!--Params-->
|
|
<div id="microphoneSelectDiv" class="w-full h-10">
|
|
<label class="block text-m font-bold mb-2">Microphone Device</label>
|
|
<select id="microphoneSelect"
|
|
class="w-full h-full bg-primary rounded focus:outline-none focus:ring focus:ring-accent hover:ring hover:ring-accent duration-300">
|
|
</select>
|
|
</div>
|
|
<style>
|
|
.sliderMicThreshold {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 100%;
|
|
height: 15px;
|
|
border-radius: 7.5px;
|
|
background: #ddd;
|
|
outline: none;
|
|
opacity: 0.7;
|
|
-webkit-transition: .2s;
|
|
transition: opacity .2s;
|
|
}
|
|
|
|
.sliderMicThreshold::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 25px;
|
|
height: 25px;
|
|
border-radius: 50%;
|
|
background: #7d7d7d;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.sliderMicThreshold::-moz-range-thumb {
|
|
width: 25px;
|
|
height: 25px;
|
|
border-radius: 50%;
|
|
background: #7d7d7d;
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
<label class="block font-bold mt-6">Microphone sensitivity</label>
|
|
<p>Sensivity of the microphone in dB, the noise bellow this dB amount will be cancel, the bar moving represent your current mic volume</p>
|
|
<input type="range" min="-100" max="0" step="0.01" value="0.5" class="sliderMicThreshold ring ring-primary"
|
|
id="sliderMicThreshold">
|
|
|
|
<label class="block font-bold mt-6">Noise Suppression</label>
|
|
<div class="flex gap-x-1 items-center">
|
|
<input type="checkbox" id="noiseSuppression"
|
|
class="accent-accent h-4 w-4 text-gray-800 rounded border-gray-300">
|
|
<label>Removes noise from your microphone audio</label>
|
|
</div>
|
|
|
|
<label class="block font-bold mt-6">Echo Cancellation</label>
|
|
<div class="flex gap-x-1 items-center">
|
|
<input type="checkbox" id="echoCancellation"
|
|
class="accent-accent h-4 w-4 text-gray-800 rounded border-gray-300">
|
|
<label>Removes echo from your microphone audio</label>
|
|
</div>
|
|
|
|
<!--Close Button-->
|
|
<div class="mt-5 flex justify-center">
|
|
<button id="closeSettings"
|
|
class="bg-secondary px-3 py-2 hover:bg-accent text-xl text-white font-bold rounded-xl duration-300">
|
|
Close Settings
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="h-full w-full p-8 text-slate-200">
|
|
<div class="bg-main rounded-lg shadow-2xl max-h-screen">
|
|
<!--Content-->
|
|
<div class="p-6">
|
|
<h1 class="text-2xl font-bold mb-4">BoostedAudio</h1>
|
|
<div class="flex mb-6 items-center gap-5">
|
|
<!--Ambient Volume-->
|
|
<div id="ambient" class="w-1/3 pr-4 grid grid-cols-1">
|
|
<label class="block text-sm font-medium mb-2">Ambient Volume</label>
|
|
<input id="ambientVolume" type="range" class="accent-accent w-full ring-slate-800" min="0"
|
|
max="100">
|
|
</div>
|
|
<!--Voice Volume-->
|
|
<div id="voice" class="w-1/3 pr-4 grid grid-cols-1">
|
|
<label class="block text-sm font-medium mb-2">Voice Volume</label>
|
|
<input id="voiceVolume" type="range" class="accent-accent w-full ring-slate-800" min="0"
|
|
max="150">
|
|
</div>
|
|
|
|
<div class="flex justify-center items-center relative m-5">
|
|
<div id="speakingHalo"
|
|
class="h-14 w-14 bg-emerald-500 absolute blur-lg opacity-80 rounded-full duration-300 hidden">
|
|
</div>
|
|
<svg id="muteButton" class="absolute z-10 h-12 w-12 text-white cursor-pointer
|
|
bg-secondary hover:bg-accent duration-300 font-bold rounded red-animation"
|
|
stroke="currentColor" fill="none" xmlns="http://www.w3.org/2000/svg" width="512.000000pt"
|
|
height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
|
preserveAspectRatio="xMidYMid meet">
|
|
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)" fill="currentColor"
|
|
stroke="currentColor">
|
|
<path d="M2393 4786 c-401 -77 -709 -387 -778 -785 -12 -69 -15 -226 -15 -881 0 -848 2 -885 49 -1023 65 -188 194 -361 358 -480 70 -51 224 -123 319 -149 114 -31 354 -31 467 0 217 59 408 188 535 360 66 88 136 235 164 342 l23 85 0 865 0 865 -23 85 c-92 348 -367 620 -713 705 -90 22 -296 28 -386 11z m357 -335 c93 -29 185 -86 260 -161 75 -76 120 -148 158 -252 l27 -73 0 -845 0 -845 -27 -73 c-38 -104 -83 -176 -158 -252 -76 -76 -167 -132 -265 -163 -57 -18 -93 -22 -190 -22 -104 1 -130 5 -193 27 -104 37 -176 83 -252 158 -75 76 -120 147 -158 252 l-27 73 -3 812 c-3 888 -3 886 56 1014 83 181 253 321 444 364 86 20 243 13 328 -14z"/>
|
|
<path d="M1066 2520 c-40 -13 -83 -56 -97 -99 -15 -45 -6 -206 20 -338 60 -311 205 -578 440 -814 259 -258 568 -413 924 -464 l47 -6 0 -185 c0 -164 2 -189 20 -223 54 -108 209 -114 273 -11 20 32 22 52 27 219 l5 184 106 18 c730 123 1314 785 1322 1501 2 138 -4 155 -67 202 -38 29 -134 29 -172 0 -58 -43 -67 -65 -76 -191 -18 -259 -86 -463 -221 -663 -66 -98 -202 -238 -302 -312 -116 -86 -295 -172 -440 -210 -115 -30 -128 -32 -315 -32 -186 0 -200 2 -313 32 -227 60 -417 170 -588 341 -237 237 -353 496 -377 844 -9 125 -18 149 -73 189 -29 21 -103 31 -143 18z"/>
|
|
</g>
|
|
</svg>
|
|
|
|
<svg id="mutedButton" class="absolute z-10 h-12 w-12 text-red-500 cursor-pointer
|
|
bg-secondary hover:bg-accent duration-300 font-bold rounded"
|
|
stroke="currentColor" fill="currentColor"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="128.000000pt" height="128.000000pt" viewBox="0 0 128.000000 128.000000"
|
|
preserveAspectRatio="xMidYMid meet">
|
|
<g transform="translate(0.000000,128.000000) scale(0.100000,-0.100000)"
|
|
fill="currentColor" stroke="currentColor">
|
|
<path d="M52 1228 c-7 -7 -12 -19 -12 -28 0 -20 1140 -1160 1161 -1160 19 0 39 20 39 39 0 9 -59 75 -131 147 l-131 131 31 48 c61 96 87 244 48 269 -30 20 -51 -4 -59 -70 -7 -55 -61 -184 -77 -184 -3 0 -28 21 -54 48 l-46 47 29 59 30 59 0 200 c0 239 -5 263 -75 332 -101 102 -229 102 -330 0 -56 -55 -75 -102 -75 -181 l0 -49 -153 153 c-84 83 -159 152 -168 152 -8 0 -20 -5 -27 -12z m676 -95 c65 -48 67 -56 70 -276 3 -173 1 -206 -14 -239 -9 -21 -20 -38 -23 -38 -3 0 -68 62 -143 137 l-138 138 0 87 c0 121 24 171 95 204 40 19 119 12 153 -13z"/>
|
|
<path d="M400 681 c0 -63 22 -114 75 -167 52 -52 116 -80 166 -72 23 3 21 6 -26 51 -77 72 -111 107 -165 167 l-50 55 0 -34z"/>
|
|
<path d="M212 668 c-26 -26 -8 -138 34 -223 59 -115 162 -200 283 -231 l70 -18 3 -70 c3 -76 21 -102 56 -80 13 9 18 28 20 82 3 69 4 72 30 78 44 9 116 34 132 44 13 8 11 13 -11 36 l-27 26 -68 -17 c-215 -54 -426 90 -451 307 -3 29 -10 59 -16 66 -12 15 -39 16 -55 0z"/>
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
<!--MuteButton-->
|
|
|
|
<!--ParamButton-->
|
|
<div id="paramButton" class="h-12 w-12 p-1
|
|
bg-secondary hover:bg-accent duration-300 text-white font-bold rounded cursor-pointer">
|
|
<svg class="hover:animate-spin"
|
|
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round"
|
|
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 id="userList" class="text-lg font-bold mb-4">User List</h2>
|
|
<!--Search-->
|
|
<div id="searchDiv" class="p-1 w-1/5 mb-4 relative">
|
|
<input id="search" type="text"
|
|
class="w-full px-4 py-2 ring ring-primary focus:ring-accent hover:ring-accent duration-300 bg-secondary text-gray-300 border-none rounded-full focus:outline-none"
|
|
placeholder="Search..." maxlength="20">
|
|
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
|
<svg id="search-logo" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-300"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor">
|
|
<path fill-rule="evenodd"
|
|
d="M14.833 13.392a8 8 0 1 0-1.44 1.44l5.334 5.333a1 1 0 0 0 1.5-1.333l-5.333-5.333zM2 8a6 6 0 1 1 12 0A6 6 0 0 1 2 8z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
const searchInput = document.getElementById('search');
|
|
const svgIcon = document.getElementById('search-logo');
|
|
|
|
searchInput.addEventListener('focus', () => {
|
|
svgIcon.classList.add('animate-bounce');
|
|
});
|
|
|
|
searchInput.addEventListener('blur', () => {
|
|
svgIcon.classList.remove('animate-bounce');
|
|
});
|
|
</script>
|
|
|
|
<!--UserCount-->
|
|
<div id="userCountDiv" class="text-sm pb-3 flex gap-1">
|
|
<p class="text-slate-400">Players you can hear: </p>
|
|
<p id="userCount" class="text-slate-200">0</p>
|
|
</div>
|
|
|
|
<!--Mute Info-->
|
|
<p id="muteUser" class="text-red-500 text-center p-10 text-5xl italic hidden">You have been muted by the server</p>
|
|
|
|
<!--Users-->
|
|
<div id="users" class="overflow-auto
|
|
sm:max-h-[5rem] md:max-h-[20rem] 2xl:sm:max-h-[30rem]
|
|
min-h-0 bg-primary p-2 rounded-lg bg-scroll grid gap-4 sm:grid-cols-1 md:grid-cols-3 lg:grid-cols-4 drop-shadow-lg">
|
|
<div id="user"
|
|
class="user flex items-center bg-main p-4 rounded-lg focus:outline-none focus:ring hover:ring duration-300">
|
|
<img src="" alt="User 1" class="logo w-12 h-12 rounded-lg mr-4">
|
|
<div class="flex-grow">
|
|
<p class="font-medium username"></p>
|
|
<input type="range" class="user-volume accent-accent w-full mt-1" min="0" max="150">
|
|
</div>
|
|
<!--User Mute Button-->
|
|
<svg class="muteButton h-12 w-12 p-1 text-secondary hover:text-accent cursor-pointer
|
|
duration-300 font-bold rounded"
|
|
stroke="currentColor" fill="none" xmlns="http://www.w3.org/2000/svg"
|
|
width="512.000000pt"
|
|
height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
|
preserveAspectRatio="xMidYMid meet">
|
|
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
|
fill="currentColor"
|
|
stroke="currentColor">
|
|
<path d="M2393 4786 c-401 -77 -709 -387 -778 -785 -12 -69 -15 -226 -15 -881 0 -848 2 -885 49 -1023 65 -188 194 -361 358 -480 70 -51 224 -123 319 -149 114 -31 354 -31 467 0 217 59 408 188 535 360 66 88 136 235 164 342 l23 85 0 865 0 865 -23 85 c-92 348 -367 620 -713 705 -90 22 -296 28 -386 11z m357 -335 c93 -29 185 -86 260 -161 75 -76 120 -148 158 -252 l27 -73 0 -845 0 -845 -27 -73 c-38 -104 -83 -176 -158 -252 -76 -76 -167 -132 -265 -163 -57 -18 -93 -22 -190 -22 -104 1 -130 5 -193 27 -104 37 -176 83 -252 158 -75 76 -120 147 -158 252 l-27 73 -3 812 c-3 888 -3 886 56 1014 83 181 253 321 444 364 86 20 243 13 328 -14z"/>
|
|
<path d="M1066 2520 c-40 -13 -83 -56 -97 -99 -15 -45 -6 -206 20 -338 60 -311 205 -578 440 -814 259 -258 568 -413 924 -464 l47 -6 0 -185 c0 -164 2 -189 20 -223 54 -108 209 -114 273 -11 20 32 22 52 27 219 l5 184 106 18 c730 123 1314 785 1322 1501 2 138 -4 155 -67 202 -38 29 -134 29 -172 0 -58 -43 -67 -65 -76 -191 -18 -259 -86 -463 -221 -663 -66 -98 -202 -238 -302 -312 -116 -86 -295 -172 -440 -210 -115 -30 -128 -32 -315 -32 -186 0 -200 2 -313 32 -227 60 -417 170 -588 341 -237 237 -353 496 -377 844 -9 125 -18 149 -73 189 -29 21 -103 31 -143 18z"/>
|
|
</g>
|
|
</svg>
|
|
<svg class="mutedButton h-12 w-12 p-1 text-red-500 hover:text-accent cursor-pointer
|
|
duration-300 font-bold rounded"
|
|
stroke="currentColor" fill="currentColor"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="128.000000pt" height="128.000000pt" viewBox="0 0 128.000000 128.000000"
|
|
preserveAspectRatio="xMidYMid meet">
|
|
<g transform="translate(0.000000,128.000000) scale(0.100000,-0.100000)"
|
|
fill="currentColor" stroke="currentColor">
|
|
<path d="M52 1228 c-7 -7 -12 -19 -12 -28 0 -20 1140 -1160 1161 -1160 19 0 39 20 39 39 0 9 -59 75 -131 147 l-131 131 31 48 c61 96 87 244 48 269 -30 20 -51 -4 -59 -70 -7 -55 -61 -184 -77 -184 -3 0 -28 21 -54 48 l-46 47 29 59 30 59 0 200 c0 239 -5 263 -75 332 -101 102 -229 102 -330 0 -56 -55 -75 -102 -75 -181 l0 -49 -153 153 c-84 83 -159 152 -168 152 -8 0 -20 -5 -27 -12z m676 -95 c65 -48 67 -56 70 -276 3 -173 1 -206 -14 -239 -9 -21 -20 -38 -23 -38 -3 0 -68 62 -143 137 l-138 138 0 87 c0 121 24 171 95 204 40 19 119 12 153 -13z"/>
|
|
<path d="M400 681 c0 -63 22 -114 75 -167 52 -52 116 -80 166 -72 23 3 21 6 -26 51 -77 72 -111 107 -165 167 l-50 55 0 -34z"/>
|
|
<path d="M212 668 c-26 -26 -8 -138 34 -223 59 -115 162 -200 283 -231 l70 -18 3 -70 c3 -76 21 -102 56 -80 13 9 18 28 20 82 3 69 4 72 30 78 44 9 116 34 132 44 13 8 11 13 -11 36 l-27 26 -68 -17 c-215 -54 -426 90 -451 307 -3 29 -10 59 -16 66 -12 15 -39 16 -55 0z"/>
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
<!--Footer of the div-->
|
|
<div class="relative p-2">
|
|
<!--DON'T REMOVE-->
|
|
<p class="z-10 absolute bottom-2 right-2 underline"><a
|
|
href="https://www.spigotmc.org/resources/boostedaudio-%E2%9C%A8proximity-voice-chat-and-music.112942/">Powered
|
|
by BoostedAudio</a></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|
|
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/hark@1.2.3/hark.bundle.min.js"></script>
|
|
<script>
|
|
const servers = {
|
|
iceServers: [],
|
|
iceCandidatePoolSize: 10,
|
|
}
|
|
|
|
const RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
|
|
|
// Map<String(layerId), Map<UUID, PeerConnectionContainer>>
|
|
const peerConnections = new Map();
|
|
const layersSpatialized = new Map();
|
|
|
|
// Map<String, Function>
|
|
const registeredPackets = new Map();
|
|
registerPackets();
|
|
|
|
// Map<UUID, AudioObject>
|
|
const audioMap = new Map();
|
|
|
|
let audioContext;
|
|
let speechEvents;
|
|
|
|
let micStream;
|
|
let micGainNode;
|
|
let micDestination;
|
|
|
|
let playerId;
|
|
const params = new URLSearchParams(window.location.search);
|
|
const wsIP = "wss://localhost:8081";
|
|
let proximityChat = true
|
|
let serverInfoSet = false;
|
|
let ws;
|
|
let voiceVolume = 1;
|
|
let ambientVolume = 1;
|
|
|
|
let clientLoc;
|
|
let consent = false;
|
|
|
|
let currentDeviceId;
|
|
let noiseSuppression = true;
|
|
let echoCancellation = true;
|
|
let maxDistance;
|
|
let rolloffFactor;
|
|
let refDistance;
|
|
let distanceModel;
|
|
let muted = false;
|
|
let serverMuted = false;
|
|
|
|
// HTML elements
|
|
const users = document.getElementById('users');
|
|
const muteUser = document.getElementById('muteUser');
|
|
|
|
const voice = document.getElementById('voiceVolume');
|
|
const ambient = document.getElementById('ambientVolume');
|
|
const micThreshold = document.getElementById('sliderMicThreshold');
|
|
const microphoneSelect = document.getElementById('microphoneSelect');
|
|
const paramButton = document.getElementById('paramButton');
|
|
const closeButton = document.getElementById('closeSettings');
|
|
const settingsMenu = document.getElementById('settings');
|
|
const paramsWindow = document.getElementById('params');
|
|
const connectionDiv = document.getElementById("connection");
|
|
|
|
const muteButton = document.getElementById('muteButton');
|
|
const mutedButton = document.getElementById('mutedButton');
|
|
|
|
let userTemplate;
|
|
|
|
let isOpen = false;
|
|
|
|
console.log(adapter.browserDetails.browser)
|
|
console.log(adapter.browserDetails.version)
|
|
|
|
// Setup html user element
|
|
const user = document.getElementById("user")
|
|
userTemplate = user.outerHTML
|
|
user.remove()
|
|
|
|
// CONNECT BUTTON
|
|
connectionDiv.addEventListener("click", () => {
|
|
connectionDiv.classList.add("hidden");
|
|
setupWithConsent();
|
|
});
|
|
|
|
paramButton.addEventListener('click', () => {
|
|
settingsMenu.classList.remove('hidden');
|
|
});
|
|
|
|
closeButton.addEventListener('click', () => {
|
|
settingsMenu.classList.add('hidden');
|
|
});
|
|
|
|
settingsMenu.addEventListener('click', e => {
|
|
if (paramsWindow.contains(e.target)) return
|
|
settingsMenu.classList.add('hidden');
|
|
});
|
|
|
|
function getLayerMap(layerId) {
|
|
if (!peerConnections.has(layerId)) peerConnections.set(layerId, new Map());
|
|
return peerConnections.get(layerId)
|
|
}
|
|
|
|
|
|
// Search bar
|
|
const search = document.getElementById("search");
|
|
const userCount = document.getElementById("userCount");
|
|
if (!proximityChat) {
|
|
document.getElementById("userList").classList.add("hidden")
|
|
users.classList.add("hidden")
|
|
document.getElementById("searchDiv").classList.add("hidden")
|
|
document.getElementById("userCountDiv").classList.add("hidden")
|
|
document.getElementById("microphoneSelectDiv").classList.add("hidden")
|
|
muteButton.classList.add("hidden")
|
|
mutedButton.classList.add("hidden")
|
|
document.getElementById("voice").classList.add("hidden")
|
|
document.getElementById("paramButton").classList.add("hidden")
|
|
} else {
|
|
setInterval(() => {
|
|
userCount.textContent = getLayerMap("proximitychat").size
|
|
const userElement = users.getElementsByClassName("user")
|
|
if (search.value.length === 0) return
|
|
for (let i = 0; i < userElement.length; i++) {
|
|
const usr = userElement[i];
|
|
const nameElement = usr.querySelector(".username");
|
|
if (nameElement) {
|
|
if (nameElement.toLowerCase().textContent.includes(search.value.toLowerCase())) {
|
|
usr.classList.remove('hidden');
|
|
} else {
|
|
usr.classList.add('hidden');
|
|
}
|
|
}
|
|
}
|
|
}, 100);
|
|
askMic()
|
|
}
|
|
|
|
async function askMic() {
|
|
micStream = await getUserMic();
|
|
|
|
connectionDiv.classList.add("hidden");
|
|
populateMicrophoneOptions();
|
|
setupWithConsent();
|
|
}
|
|
|
|
|
|
function getUserMic(deviceId) {
|
|
try {
|
|
let constraints = {
|
|
audio: {
|
|
noiseSuppression: noiseSuppression,
|
|
echoCancellation: echoCancellation,
|
|
channelCount: 1,
|
|
autoGainControl: true
|
|
}
|
|
};
|
|
if (deviceId) constraints.audio.deviceId = deviceId;
|
|
return navigator.mediaDevices.getUserMedia(constraints);
|
|
} catch (err) {
|
|
console.log("Error with getUserMic ", err)
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function initWithServerInfo() {
|
|
setupAudioSystem()
|
|
}
|
|
|
|
setInterval(async () => {
|
|
audioMap.forEach((audioObj, uuid) => {
|
|
const loc = audioObj.location;
|
|
//console.log("Spatialize audio", loc)
|
|
if (loc) {
|
|
spatializeAudio(audioObj, loc)
|
|
if (distance(loc, clientLoc) >= audioObj.maxVoiceDistance) {
|
|
audioObj.gainNode.gain.value = 0;
|
|
} else {
|
|
audioObj.gainNode.gain.value = 1;
|
|
}
|
|
}
|
|
})
|
|
}, 50);
|
|
|
|
function registerPackets() {
|
|
registeredPackets.set("AddPeerPacket", async packet => {
|
|
if (serverMuted) return
|
|
const from = packet.from;
|
|
const to = packet.to;
|
|
const layerId = packet.layerId;
|
|
const spatialized = packet.spatialized;
|
|
|
|
console.log("Spatialized", layerId, spatialized)
|
|
layersSpatialized.set(layerId, spatialized)
|
|
let username = packet.username;
|
|
let pc;
|
|
switch (packet.rtcDesc.type) {
|
|
// Packet action from server
|
|
case "createoffer":
|
|
pc = await createPeerConnection(layerId, from, username)
|
|
const offer = await pc.createOffer()
|
|
await pc.setLocalDescription(offer)
|
|
|
|
const offerPacket = [
|
|
{
|
|
"type": "AddPeerPacket",
|
|
"value": {
|
|
"layerId": layerId,
|
|
"from": playerId,
|
|
"to": from,
|
|
"username": "",
|
|
"spatialized": spatialized,
|
|
"rtcDesc": {
|
|
"type": offer.type,
|
|
"sdp": offer.sdp,
|
|
}
|
|
}
|
|
}
|
|
]
|
|
/*console.log("Offer: " + JSON.stringify(offerPacket))*/
|
|
|
|
console.log("Creating offer")
|
|
ws.send(JSON.stringify(offerPacket))
|
|
break
|
|
// Packet action from other user, check by server
|
|
// Receive offer
|
|
case "offer":
|
|
pc = await createPeerConnection(layerId, from, username)
|
|
const desc = new RTCSessionDescription(packet.rtcDesc);
|
|
await pc.setRemoteDescription(desc)
|
|
|
|
const answer = await pc.createAnswer()
|
|
await pc.setLocalDescription(answer)
|
|
|
|
const answerPacket = [
|
|
{
|
|
"type": "AddPeerPacket",
|
|
"value": {
|
|
"layerId": layerId,
|
|
"from": to,
|
|
"to": from,
|
|
"username": "",
|
|
"spatialized": spatialized,
|
|
"rtcDesc": {
|
|
"type": answer.type,
|
|
"sdp": answer.sdp,
|
|
}
|
|
}
|
|
}
|
|
]
|
|
|
|
ws.send(JSON.stringify(answerPacket))
|
|
console.log("Receive OFFER, Sending answer")
|
|
break
|
|
// Packet action from other user, check by server
|
|
// Receive answer
|
|
case "answer":
|
|
pc = getLayerMap(layerId).get(from).pc
|
|
await pc.setRemoteDescription(new RTCSessionDescription(packet.rtcDesc))
|
|
console.log("Receive answer")
|
|
break
|
|
}
|
|
});
|
|
registeredPackets.set("RTCIcePacket", async packet => {
|
|
console.log("RECEIVING ICE CANDIDATE")
|
|
const from = packet.from;
|
|
const to = packet.to;
|
|
const layerId = packet.layerId;
|
|
console.log(from, to, layerId, packet)
|
|
const pc = getLayerMap(layerId).get(from).pc
|
|
const iceCandidate = new RTCIceCandidate({
|
|
"candidate": packet.candidate.candidate,
|
|
"sdpMid": packet.candidate.sdpMid,
|
|
"sdpMLineIndex": packet.candidate.sdpMLineIndex
|
|
});
|
|
|
|
const candidate = new RTCIceCandidate(iceCandidate);
|
|
|
|
await pc.addIceCandidate(candidate)
|
|
.catch((error) => {
|
|
console.error("Erreur with ICE candidate set : ", error);
|
|
});
|
|
|
|
console.log("ICE Candidate set")
|
|
});
|
|
registeredPackets.set("RemovePeerPacket", async packet => {
|
|
const layerId = packet.layerId;
|
|
const playerToRemoveId = packet.playerToRemove;
|
|
closePeerConnection(getLayerMap(layerId).get(playerToRemoveId))
|
|
});
|
|
registeredPackets.set("UpdatePeersLocationsPacket", async packet => {
|
|
// Parse clientloc
|
|
clientLoc = packet.clientLoc;
|
|
//console.log("UpdateClientLoc")
|
|
// Parse peers info
|
|
const players = packet.playersAround;
|
|
const map = new Map();
|
|
for (const key in players) {
|
|
if (players.hasOwnProperty(key)) map.set(key, players[key]);
|
|
}
|
|
|
|
if (proximityChat)
|
|
peerConnections.forEach((layerMap, layerId) => {
|
|
if (layersSpatialized.get(layerId) === true) {
|
|
layerMap.forEach((peerObj, uuid) => {
|
|
peerObj.loc = map.get(uuid)
|
|
spatializeAudio(peerObj, peerObj.loc)
|
|
})
|
|
}
|
|
})
|
|
});
|
|
registeredPackets.set("UpdateAudioLocationPacket", async packet => {
|
|
const audioId = packet.audioId;
|
|
//console.log("UpdateAudioLoc,", packet.newLocation)
|
|
audioMap.get(audioId).location = packet.newLocation;
|
|
});
|
|
registeredPackets.set("ChangeAudioTimePacket", async packet => {
|
|
audioMap.get(packet.audioId).audio.currentTime = packet.timeToPlay;
|
|
});
|
|
registeredPackets.set("AddAudioPacket", async packet => {
|
|
const id = packet.uuid;
|
|
let panner;
|
|
let audioSource;
|
|
const link = encodeURI(packet.link)
|
|
const spatialInfo = packet.spatialInfo;
|
|
const synchronous = packet.synchronous;
|
|
console.log(synchronous)
|
|
let maxVoiceDistance = 0;
|
|
let gainNode;
|
|
let audio;
|
|
if (spatialInfo) {
|
|
panner = new PannerNode(audioContext, {
|
|
panningModel: "HRTF",
|
|
distanceModel: spatialInfo.distanceModel,
|
|
refDistance: Math.max(spatialInfo.refDistance, 0.1),
|
|
rolloffFactor: spatialInfo.rolloffFactor,
|
|
coneInnerAngle: 360,
|
|
coneOuterAngle: 360,
|
|
coneOuterGain: 1,
|
|
});
|
|
|
|
const url = link.startsWith("https") ? link : link.startsWith("http") ? 'https://corsproxy.io/?' + link : link;
|
|
const audioHtml = new Audio(url)
|
|
audioHtml.crossOrigin = "anonymous"
|
|
audioSource = audioContext.createMediaElementSource(audioHtml);
|
|
audio = audioSource.mediaElement;
|
|
maxVoiceDistance = spatialInfo.maxVoiceDistance;
|
|
gainNode = audioContext.createGain();
|
|
|
|
audioSource.connect(panner);
|
|
panner.connect(gainNode);
|
|
gainNode.connect(audioContext.destination);
|
|
} else
|
|
audio = new Audio(link)
|
|
|
|
console.log("Playing " + id, " url", link)
|
|
const oldAudio = audioMap.get(id);
|
|
if (oldAudio) {
|
|
oldAudio.audio.pause();
|
|
}
|
|
|
|
if (synchronous) {
|
|
audio.addEventListener('loadedmetadata', () => {
|
|
let durMs = audio.duration * 1000;
|
|
let ts = Date.now();
|
|
audio.currentTime = ts % durMs / 1000;
|
|
audio.play();
|
|
});
|
|
} else audio.play();
|
|
|
|
audio.addEventListener('ended', function () {
|
|
console.log("Audio finished playing ", audioMap.get(id));
|
|
audioMap.delete(id)
|
|
if (gainNode) gainNode.disconnect();
|
|
const endAudioPacket =
|
|
[
|
|
{
|
|
"type": "RemoveAudioPacket",
|
|
"value": {
|
|
"uuid": id,
|
|
"fade": 0,
|
|
"audioLink": link,
|
|
}
|
|
}
|
|
]
|
|
ws.send(JSON.stringify(endAudioPacket))
|
|
});
|
|
|
|
const audioObj = {
|
|
audio: audio,
|
|
audioSource: audioSource,
|
|
panner: panner,
|
|
location: spatialInfo ? spatialInfo.location : null,
|
|
gainNode: gainNode,
|
|
maxVoiceDistance: maxVoiceDistance,
|
|
fadeInterval: null,
|
|
}
|
|
fadeIn(audio, packet.fadeIn, audioObj)
|
|
audioMap.set(id, audioObj)
|
|
});
|
|
registeredPackets.set("PausePlayAudioPacket", async packet => {
|
|
const audioObj = audioMap.get(packet.uuid);
|
|
const audio = audioObj.audio;
|
|
if (audio.paused)
|
|
fadeIn(audio, packet.fade, audioObj)
|
|
else
|
|
fadeOut(audio, packet.fade, audioObj)
|
|
});
|
|
registeredPackets.set("RemoveAudioPacket", async packet => {
|
|
const id = packet.uuid;
|
|
const audioObj = audioMap.get(id);
|
|
if (audioObj) {
|
|
console.log("Stop playing audio: " + id)
|
|
audioMap.delete(id)
|
|
fadeOut(audioObj.audio, packet.fade, () => {
|
|
if (audioObj.panner) audioObj.panner.disconnect();
|
|
clearInterval(audioObj.lerpInterval);
|
|
}, audioObj)
|
|
}
|
|
});
|
|
registeredPackets.set("TrustPacket", async packet => {
|
|
const serverInfo = packet.serverInfo;
|
|
maxDistance = serverInfo.maxDistance;
|
|
rolloffFactor = serverInfo.rolloffFactor;
|
|
refDistance = serverInfo.refDistance;
|
|
distanceModel = serverInfo.distanceModel;
|
|
playerId = serverInfo.playerId;
|
|
console.log("ServerInfo set")
|
|
serverInfoSet = true;
|
|
initWithServerInfo()
|
|
});
|
|
registeredPackets.set("IceServersPacket", async packet => {
|
|
servers.iceServers = packet.iceServers;
|
|
console.log("IceServers refreshed")
|
|
})
|
|
registeredPackets.set("ServerChangePacket", async packet => {
|
|
const serverName = packet.serverName;
|
|
console.log("Server change to " + serverName)
|
|
audioMap.forEach((audioObj, uuid) => {
|
|
const audio = audioObj.audio;
|
|
audio.pause();
|
|
audio.currentTime = 0;
|
|
})
|
|
audioMap.clear()
|
|
});
|
|
registeredPackets.set("ServerMutePacket", async packet => {
|
|
const mute = packet.mute;
|
|
serverMuted = mute;
|
|
if (mute) {
|
|
muteUser.classList.remove("hidden");
|
|
for (let value of peerConnections.values()) {
|
|
for (let pc of value.values()) {
|
|
closePeerConnection(pc);
|
|
}
|
|
}
|
|
} else {
|
|
muteUser.classList.add("hidden")
|
|
}
|
|
});
|
|
registeredPackets.set("ClientMutePacket", async packet => {
|
|
muteToggle();
|
|
});
|
|
}
|
|
|
|
async function handlePacket(type, packet) {
|
|
if (type !== "TrustPacket") {
|
|
if (!consent || !serverInfoSet) {
|
|
console.log("Delaying the Packet waiting for server response or user click (consent)...")
|
|
setTimeout(function () {
|
|
handlePacket(type, packet)
|
|
}, 100)
|
|
return
|
|
}
|
|
}
|
|
|
|
try {
|
|
registeredPackets.get(type)(packet);
|
|
} catch (e) {
|
|
console.log("Error packet processing", type, packet, e);
|
|
}
|
|
}
|
|
|
|
function distance(location1, location2) {
|
|
const x1 = location1.x;
|
|
const y1 = location1.y;
|
|
const z1 = location1.z;
|
|
|
|
const x2 = location2.x;
|
|
const y2 = location2.y;
|
|
const z2 = location2.z;
|
|
|
|
const dx = x2 - x1;
|
|
const dy = y2 - y1;
|
|
const dz = z2 - z1;
|
|
|
|
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
}
|
|
|
|
async function fadeIn(audio, duration, audioObj) {
|
|
function fade() {
|
|
/* console.log(
|
|
"Starting FadeIn " + duration,
|
|
"Audio Volume " + audio.volume,
|
|
"Ambient Volume " + ambientVolume, audioObj
|
|
)*/
|
|
|
|
const interval = 10;
|
|
const stepNb = duration / interval;
|
|
const volumeStep = ambientVolume / stepNb;
|
|
//const step = interval / duration;
|
|
audio.volume = 0;
|
|
|
|
const fadeInterval = setInterval(() => {
|
|
if (audio.volume < ambientVolume) {
|
|
audio.volume = Math.min(ambientVolume, audio.volume + volumeStep);
|
|
} else {
|
|
clearInterval(fadeInterval);
|
|
/*console.log("End FadeIn ", audioObj)*/
|
|
audioObj.fadeInterval = null;
|
|
}
|
|
}, interval);
|
|
audioObj.fadeInterval = fadeInterval;
|
|
}
|
|
|
|
if (audioObj.fadeInterval) {
|
|
clearInterval(audioObj.fadeInterval)
|
|
/* console.log("Cancel Fade ", audioObj.fadeInterval)*/
|
|
}
|
|
fade()
|
|
}
|
|
|
|
async function fadeOut(audio, duration, afterFade, audioObj) {
|
|
function fade() {
|
|
const interval = 10;
|
|
const stepNb = duration / interval;
|
|
const volumeStep = ambientVolume / stepNb;
|
|
|
|
/* console.log(
|
|
"Starting FadeOut " + duration,
|
|
"Audio Volume " + audio.volume,
|
|
"Ambient Volume " + ambientVolume, audioObj
|
|
)*/
|
|
|
|
const fadeInterval = setInterval(() => {
|
|
if (audio.volume > 0) {
|
|
audio.volume = Math.max(0, audio.volume - volumeStep);
|
|
} else {
|
|
audio.pause();
|
|
clearInterval(fadeInterval);
|
|
/* console.log("End FadeOut ", audioObj)*/
|
|
audioObj.fadeInterval = null;
|
|
if (afterFade) afterFade()
|
|
}
|
|
}, interval);
|
|
audioObj.fadeInterval = fadeInterval;
|
|
}
|
|
|
|
if (audioObj.fadeInterval) {
|
|
clearInterval(audioObj.fadeInterval)
|
|
/* console.log("Cancel Fade ", audioObj.fadeInterval)*/
|
|
}
|
|
fade()
|
|
}
|
|
|
|
function spatializeAudio(pannerObject, loc) {
|
|
if (!loc) return;
|
|
|
|
pannerObject.t = 0;
|
|
if (!pannerObject.lerpInterval) {
|
|
pannerObject.lastLoc = loc;
|
|
pannerObject.currentLoc = loc;
|
|
|
|
pannerObject.lerpInterval = setInterval(() => {
|
|
const lerpLoc = {
|
|
x: lerp(pannerObject.lastLoc.x, pannerObject.currentLoc.x, pannerObject.t),
|
|
y: lerp(pannerObject.lastLoc.y, pannerObject.currentLoc.y, pannerObject.t),
|
|
z: lerp(pannerObject.lastLoc.z, pannerObject.currentLoc.z, pannerObject.t)
|
|
};
|
|
spatializeAudio0(pannerObject, lerpLoc)
|
|
//console.log("T: " + pannerObject.t + " ", lerpLoc)
|
|
pannerObject.t += 0.1;
|
|
}, 5);
|
|
} else {
|
|
pannerObject.lastLoc = pannerObject.currentLoc;
|
|
pannerObject.currentLoc = loc;
|
|
}
|
|
}
|
|
|
|
function spatializeAudio0(pannerObject, location) {
|
|
// console.log("Spatialize audio", objToSpatialize, pannerObject, clientLoc.yaw)
|
|
//console.log(pannerObject, location)
|
|
// Calc peer loc relative to the client
|
|
const x = location.x - clientLoc.x;
|
|
const y = location.y - clientLoc.y;
|
|
const z = location.z - clientLoc.z;
|
|
|
|
// Calc yaw
|
|
const yawRadian = degrees_to_radians(-clientLoc.yaw)
|
|
|
|
const cosYaw = Math.cos(yawRadian)
|
|
const sinYaw = Math.sin(yawRadian)
|
|
|
|
// Rotate
|
|
const newX = x * cosYaw - z * sinYaw;
|
|
const newZ = x * sinYaw + z * cosYaw;
|
|
|
|
//console.log(clientLoc.yaw + " | " + newX + " " + y + " " + newZ)
|
|
|
|
// Set the values
|
|
pannerObject.panner.positionX.value = newX
|
|
pannerObject.panner.positionY.value = y
|
|
pannerObject.panner.positionZ.value = newZ
|
|
}
|
|
|
|
function lerp(start, end, t) {
|
|
return start * (1 - t) + end * t;
|
|
}
|
|
|
|
function degrees_to_radians(degrees) {
|
|
const pi = Math.PI;
|
|
return degrees * (pi / 180);
|
|
}
|
|
|
|
async function createPeerConnection(layerId, peerId, username) {
|
|
console.log("New RTCPeerConnection")
|
|
let pc = new RTCPeerConnection(servers)
|
|
if (getLayerMap(layerId).has(peerId)) closePeerConnection(getLayerMap(layerId).get(peerId))
|
|
|
|
// Init var
|
|
const panner = new PannerNode(audioContext, {
|
|
panningModel: "HRTF",
|
|
distanceModel: distanceModel,
|
|
refDistance: Math.max(refDistance, 0.1),
|
|
rolloffFactor: rolloffFactor,
|
|
coneInnerAngle: 360,
|
|
coneOuterAngle: 360,
|
|
coneOuterGain: 1,
|
|
});
|
|
|
|
const gain = audioContext.createGain();
|
|
|
|
const mediaStream = new MediaStream();
|
|
|
|
const peerConnectionContainer = {
|
|
pc: pc,
|
|
panner: panner,
|
|
gain: gain,
|
|
gainValue: 1,
|
|
playerId: peerId,
|
|
username: username,
|
|
layerId: layerId,
|
|
mute: false,
|
|
loc: null,
|
|
element: null
|
|
}
|
|
|
|
// RTCICEPACKET
|
|
pc.onicecandidate = (event) => {
|
|
if (event.candidate) {
|
|
const packet = [
|
|
{
|
|
"type": "RTCIcePacket",
|
|
"value": {
|
|
"layerId": layerId,
|
|
"from": playerId,
|
|
"to": peerId,
|
|
type: 'candidate',
|
|
candidate: event.candidate
|
|
}
|
|
}
|
|
]
|
|
|
|
ws.send(JSON.stringify(packet));
|
|
console.log("Sending ICE candidate ", playerId)
|
|
}
|
|
}
|
|
|
|
getLayerMap(layerId).set(peerId, peerConnectionContainer)
|
|
|
|
pc.onicecandidateerror = event => {
|
|
console.error("Error ICE Candidate :", event);
|
|
};
|
|
|
|
pc.onerror = event => {
|
|
console.error("Error RTCPeerConnection :", event);
|
|
};
|
|
|
|
pc.ontrack = event => {
|
|
if (event.track.kind === 'audio') {
|
|
const track = event.track;
|
|
|
|
mediaStream.addTrack(track)
|
|
// Connecting things
|
|
const source = audioContext.createMediaStreamSource(mediaStream);
|
|
//source.connect(audioContext.destination)
|
|
source.connect(panner)
|
|
panner.connect(gain)
|
|
gain.connect(audioContext.destination)
|
|
|
|
// We use a dummy audio muted, because of a bug in chrome https://bugs.chromium.org/p/chromium/issues/detail?id=933677
|
|
const dummyAudio = new Audio();
|
|
dummyAudio.muted = true;
|
|
dummyAudio.srcObject = mediaStream;
|
|
console.log("Track set")
|
|
updateEveryVolume()
|
|
}
|
|
}
|
|
|
|
if (micStream) {
|
|
const finalMicStream = micDestination.stream;
|
|
pc.addTrack(finalMicStream.getAudioTracks()[0], finalMicStream);
|
|
}
|
|
|
|
async function userThing() {
|
|
// Show user
|
|
await showUser(peerConnectionContainer)
|
|
|
|
// Setup user bar
|
|
const userVolume = peerConnectionContainer.element.getElementsByClassName("user-volume").item(0);
|
|
|
|
const savedVolume = getCookie(peerConnectionContainer.playerId + '_volume');
|
|
const userVolumeValue = savedVolume ? parseFloat(savedVolume) : 100;
|
|
|
|
gain.gain.value = userVolumeValue / 100;
|
|
userVolume.value = userVolumeValue;
|
|
peerConnectionContainer.gainValue = userVolumeValue / 100;
|
|
|
|
|
|
userVolume.addEventListener('input', (e) => {
|
|
peerConnectionContainer.gainValue = parseFloat(e.target.value) / 100;
|
|
if (peerConnectionContainer.mute) return;
|
|
updateEveryVolume();
|
|
|
|
setCookie(peerConnectionContainer.playerId + '_volume', e.target.value);
|
|
});
|
|
}
|
|
|
|
userThing();
|
|
|
|
updateEveryVolume();
|
|
return pc;
|
|
}
|
|
|
|
function showUser(peerObj) {
|
|
return new Promise(async (resolve, reject) => {
|
|
let username = peerObj.username;
|
|
|
|
const element = htmlStringToElement(userTemplate)
|
|
const nameElement = element.getElementsByClassName("username").item(0)
|
|
const logoElement = element.getElementsByClassName("logo").item(0)
|
|
|
|
const muteButton = element.getElementsByClassName("muteButton").item(0)
|
|
const mutedButton = element.getElementsByClassName("mutedButton").item(0)
|
|
|
|
// Charger l'état mute et le volume depuis les cookies, ou utiliser des valeurs par défaut
|
|
const muteStatus = getCookie(peerObj.playerId + '_mute');
|
|
const volumeLevel = getCookie(peerObj.playerId + '_volume');
|
|
|
|
peerObj.mute = muteStatus === 'true';
|
|
peerObj.gain.gain.value = muteStatus === 'true' ? 0 : (volumeLevel ? parseFloat(volumeLevel) : peerObj.gainValue * voiceVolume);
|
|
|
|
// Mettre à jour l'interface en fonction de l'état mute
|
|
if (peerObj.mute) {
|
|
muteButton.classList.add('hidden');
|
|
mutedButton.classList.remove('hidden');
|
|
} else {
|
|
muteButton.classList.remove('hidden');
|
|
mutedButton.classList.add('hidden');
|
|
}
|
|
|
|
// Gestion des boutons Mute
|
|
muteButton.addEventListener('click', e => {
|
|
muteButton.classList.add('hidden');
|
|
mutedButton.classList.remove('hidden');
|
|
peerObj.mute = true;
|
|
peerObj.gain.gain.value = 0;
|
|
setCookie(peerObj.playerId + '_mute', 'true');
|
|
setCookie(peerObj.playerId + '_volume', '0');
|
|
});
|
|
|
|
mutedButton.addEventListener('click', e => {
|
|
mutedButton.classList.add('hidden');
|
|
muteButton.classList.remove('hidden');
|
|
peerObj.mute = false;
|
|
peerObj.gain.gain.value = peerObj.gainValue * voiceVolume;
|
|
setCookie(peerObj.playerId + '_mute', 'false');
|
|
setCookie(peerObj.playerId + '_volume', peerObj.gain.gain.value.toString());
|
|
});
|
|
|
|
element.id = peerObj.playerId
|
|
|
|
nameElement.textContent = username;
|
|
const uuid = peerObj.playerId;
|
|
logoElement.src = "https://cravatar.eu/avatar/" + username;
|
|
|
|
peerObj.element = element;
|
|
|
|
users.insertAdjacentElement("beforeend", element);
|
|
resolve();
|
|
})
|
|
}
|
|
|
|
function closePeerConnection(peerConnectionContainer) {
|
|
if (peerConnectionContainer == null) return
|
|
peerConnectionContainer.pc.close()
|
|
if (peerConnectionContainer.element != null) peerConnectionContainer.element.remove()
|
|
getLayerMap(peerConnectionContainer.layerId).delete(peerConnectionContainer.playerId)
|
|
}
|
|
|
|
function htmlStringToElement(htmlString) {
|
|
const tempContainer = document.createElement("div");
|
|
tempContainer.innerHTML = htmlString;
|
|
return tempContainer.firstElementChild;
|
|
}
|
|
|
|
function setCookie(name, value) {
|
|
document.cookie = name + "=" + value + ";path=/";
|
|
}
|
|
|
|
function getCookie(name) {
|
|
let nameEQ = name + "=";
|
|
let ca = document.cookie.split(';');
|
|
for (let i = 0; i < ca.length; i++) {
|
|
let c = ca[i];
|
|
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
|
|
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function sendTrustPacket() {
|
|
const trustPacket = [{
|
|
"type": "TrustPacket",
|
|
"value": {
|
|
"token": params.get("t")
|
|
}
|
|
}]
|
|
|
|
ws.send(JSON.stringify(trustPacket))
|
|
}
|
|
|
|
async function populateMicrophoneOptions() {
|
|
try {
|
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
const audioInputDevices = devices.filter(device => device.kind === "audioinput");
|
|
|
|
microphoneSelect.innerHTML = ""; // Clear previous options
|
|
|
|
audioInputDevices.forEach(device => {
|
|
const option = document.createElement("option");
|
|
option.value = device.deviceId;
|
|
option.textContent = device.label || `Microphone ${microphoneSelect.childElementCount + 1}`;
|
|
microphoneSelect.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error("Error populating microphone options:", error);
|
|
}
|
|
}
|
|
|
|
function updateEveryVolume() {
|
|
audioMap.forEach((audioObj, id) => {
|
|
audioObj.audio.volume = ambientVolume;
|
|
})
|
|
|
|
peerConnections.forEach((layerMap, layerId) => {
|
|
layerMap.forEach((peerObj, id) => {
|
|
peerObj.gain.gain.value = peerObj.gainValue * voiceVolume;
|
|
});
|
|
})
|
|
}
|
|
|
|
async function setupAudioSystem() {
|
|
const savedVoiceVolume = getCookie('voice_volume');
|
|
const savedAmbientVolume = getCookie('ambient_volume');
|
|
const savedMicThreshold = getCookie('mic_threshold');
|
|
const savedNoiseSuppression = getCookie('noiseSuppression');
|
|
const savedEchoCancellation = getCookie('echoCancellation');
|
|
|
|
voiceVolume = savedVoiceVolume ? parseFloat(savedVoiceVolume) / 100 : 1; // Valeur par défaut à 100 si pas de cookie
|
|
ambientVolume = savedAmbientVolume ? parseFloat(savedAmbientVolume) / 100 : 0.666; // Valeur par défaut à 66.6 si pas de cookie
|
|
|
|
voice.value = voiceVolume * 100;
|
|
ambient.value = ambientVolume * 100;
|
|
|
|
let micThre = savedMicThreshold ? savedMicThreshold : -80;
|
|
micThreshold.value = micThre;
|
|
if (speechEvents) speechEvents.setThreshold(micThre);
|
|
|
|
|
|
// Noise params
|
|
const noiseSupp = savedNoiseSuppression ? (savedNoiseSuppression === "true") : true;
|
|
const echoCancell = savedEchoCancellation ? (savedEchoCancellation === "true") : true;
|
|
|
|
if (noiseSupp !== noiseSuppression || echoCancell !== echoCancellation) updateMic()
|
|
|
|
noiseSuppression = noiseSupp;
|
|
echoCancellation = echoCancell;
|
|
|
|
const noiseSuppressionEl = document.getElementById('noiseSuppression');
|
|
const echoCancellationEl = document.getElementById('echoCancellation');
|
|
|
|
noiseSuppressionEl.checked = noiseSuppression;
|
|
echoCancellationEl.checked = echoCancellation;
|
|
|
|
if (proximityChat) {
|
|
voice.addEventListener('input', function () {
|
|
voiceVolume = parseFloat(this.value) / 100;
|
|
updateEveryVolume();
|
|
// Sauvegarder le nouveau volume dans les cookies
|
|
setCookie('voice_volume', this.value);
|
|
});
|
|
noiseSuppressionEl.addEventListener('change', () => {
|
|
const bool = noiseSuppressionEl.checked;
|
|
if (noiseSuppression === bool) return;
|
|
noiseSuppression = bool;
|
|
setCookie('noiseSuppression', bool);
|
|
updateMic(currentDeviceId);
|
|
});
|
|
echoCancellationEl.addEventListener('change', () => {
|
|
const bool = echoCancellationEl.checked;
|
|
if (echoCancellation === bool) return;
|
|
echoCancellation = bool;
|
|
setCookie('echoCancellation', bool);
|
|
updateMic(currentDeviceId);
|
|
});
|
|
}
|
|
|
|
ambient.addEventListener('input', function () {
|
|
ambientVolume = parseFloat(this.value) / 100;
|
|
updateEveryVolume();
|
|
// Sauvegarder le nouveau volume dans les cookies
|
|
setCookie('ambient_volume', this.value);
|
|
});
|
|
|
|
micThreshold.addEventListener('input', function () {
|
|
let newThreshold = parseFloat(this.value);
|
|
speechEvents.setThreshold(newThreshold);
|
|
console.log("Threshold set to: " + newThreshold)
|
|
// Sauvegarder le nouveau volume dans les cookies
|
|
setCookie('mic_threshold', this.value);
|
|
});
|
|
|
|
if (proximityChat)
|
|
microphoneSelect.addEventListener("change", async () => {
|
|
const selectedDeviceId = microphoneSelect.value;
|
|
updateMic(selectedDeviceId);
|
|
});
|
|
}
|
|
|
|
async function updateMic(deviceId) {
|
|
try {
|
|
if (deviceId) {
|
|
console.log("New device selected: " + deviceId)
|
|
currentDeviceId = deviceId;
|
|
}
|
|
const oldGain = micGainNode.gain.value;
|
|
console.log("OldGain: ", oldGain)
|
|
|
|
const isMuted = micStream.getTracks().at(0).enabled;
|
|
micStream = await getUserMic(deviceId);
|
|
|
|
micDestination = audioContext.createMediaStreamDestination();
|
|
micGainNode = audioContext.createGain();
|
|
audioContext.createMediaStreamSource(micStream).connect(micGainNode);
|
|
micGainNode.connect(micDestination);
|
|
|
|
audioContext.createMediaStreamSource(micStream).connect(micGainNode);
|
|
const newAudioTrack = micStream.getAudioTracks()[0];
|
|
newAudioTrack.enabled = isMuted;
|
|
|
|
// Update all
|
|
peerConnections.forEach((layerMap, id) => {
|
|
layerMap.forEach((peerObj, id) => {
|
|
const pc = peerObj.pc;
|
|
const senders = pc.getSenders();
|
|
const audioSender = senders.find(sender => sender.track.kind === 'audio');
|
|
if (audioSender) {
|
|
const currentAudioTrack = audioSender.track;
|
|
currentAudioTrack.stop();
|
|
const newTrack = micDestination.stream.getAudioTracks()[0];
|
|
audioSender.replaceTrack(newTrack);
|
|
console.log("replaceTrack", newTrack)
|
|
}
|
|
})
|
|
});
|
|
|
|
if (oldGain !== undefined) micGainNode.gain.value = oldGain;
|
|
console.log("New gain: ", micGainNode.gain.value)
|
|
} catch (error) {
|
|
console.error("Error getting selected microphone stream:", error);
|
|
}
|
|
}
|
|
|
|
function muteToggle() {
|
|
mute(!muted);
|
|
}
|
|
|
|
/**
|
|
* Mute the client
|
|
* @param bool
|
|
*/
|
|
function mute(bool) {
|
|
if (bool) {
|
|
muteButton.classList.add('hidden');
|
|
mutedButton.classList.remove('hidden');
|
|
if (micStream)
|
|
micStream.getTracks().forEach(t => {
|
|
t.enabled = false
|
|
})
|
|
else console.log("MicStream is null, this mean you browser certainly don't have access to your mic")
|
|
muted = true;
|
|
} else {
|
|
mutedButton.classList.add('hidden');
|
|
muteButton.classList.remove('hidden');
|
|
micStream.getTracks().forEach(t => {
|
|
t.enabled = true
|
|
})
|
|
muted = false;
|
|
}
|
|
const clientMutePacket =
|
|
[
|
|
{
|
|
"type": "ClientMutePacket",
|
|
"value": {
|
|
"muted": muted
|
|
}
|
|
}
|
|
]
|
|
ws.send(JSON.stringify(clientMutePacket))
|
|
}
|
|
|
|
|
|
function setupWithConsent() {
|
|
if (consent) return
|
|
consent = true;
|
|
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
|
|
if (proximityChat && micStream) {
|
|
micDestination = audioContext.createMediaStreamDestination();
|
|
micGainNode = audioContext.createGain();
|
|
audioContext.createMediaStreamSource(micStream).connect(micGainNode);
|
|
micGainNode.connect(micDestination);
|
|
|
|
// Mute Button
|
|
mutedButton.classList.add('hidden');
|
|
muteButton.addEventListener('click', e => {
|
|
muteToggle();
|
|
});
|
|
|
|
mutedButton.addEventListener('click', e => {
|
|
muteToggle();
|
|
});
|
|
|
|
// Hark
|
|
speechEvents = hark(micStream, {});
|
|
speechEvents.setInterval(10);
|
|
micGainNode.gain.value = 0;
|
|
|
|
const speakingHalo = document.getElementById("speakingHalo");
|
|
speechEvents.on('speaking', () => {
|
|
//console.log("speaking")
|
|
micGainNode.gain.value = 1;
|
|
speakingHalo.classList.remove("hidden");
|
|
});
|
|
|
|
speechEvents.on('stopped_speaking', () => {
|
|
//console.log("stopped_speaking")
|
|
micGainNode.gain.value = 0;
|
|
speakingHalo.classList.add("hidden");
|
|
});
|
|
|
|
speechEvents.on('volume_change', (currentdB, currentThreshold) => {
|
|
const percentage = 100 + currentdB;
|
|
micThreshold.style.background = `linear-gradient(to right, #4CAF50 0%, #e6a87c ${percentage}%, #ddd ${percentage}%, #ddd 100%)`;
|
|
});
|
|
} else {
|
|
muteButton.classList.add('hidden');
|
|
}
|
|
|
|
function connectWebSocket() {
|
|
if (!params.has("t")) {
|
|
alert("Without token the client can't connect")
|
|
console.log("Without token this will not connected to the websocket")
|
|
return
|
|
}
|
|
|
|
console.log("Starting WebSocket...")
|
|
ws = new WebSocket(wsIP);
|
|
|
|
ws.onopen = () => {
|
|
isOpen = true;
|
|
console.log("WebSocket Open")
|
|
|
|
sendTrustPacket()
|
|
|
|
setInterval(() => {
|
|
console.log("States:")
|
|
peerConnections.forEach((layerMap, k) => {
|
|
layerMap.forEach((pc, k) => {
|
|
console.log("- State: " + pc.pc.connectionState)
|
|
console.log(pc.gain.gain.value)
|
|
console.log(pc)
|
|
})
|
|
})
|
|
}, 3000)
|
|
}
|
|
|
|
ws.onmessage = (message) => {
|
|
handlePackets(message.data)
|
|
}
|
|
|
|
ws.onclose = (event) => {
|
|
console.log('Connexion WebSocket fermée. Raison :', event);
|
|
}
|
|
|
|
// Gérer les erreurs de connexion
|
|
ws.onerror = (error) => {
|
|
console.error('Erreur de connexion WebSocket :', error);
|
|
}
|
|
}
|
|
|
|
connectWebSocket()
|
|
|
|
let retryCount = 0;
|
|
let iframe = false;
|
|
const interval = setInterval(() => {
|
|
if (isOpen) {
|
|
clearInterval(interval);
|
|
return;
|
|
}
|
|
if (retryCount >= 3) {
|
|
if (!iframe)
|
|
alert("You are unable to connect to the websocket, try to refresh the page or restart your browser")
|
|
}
|
|
|
|
if (navigator.userAgent.indexOf("Firefox") !== -1 && !iframe) {
|
|
// If it's Firefox, create an iframe with a certain IP address
|
|
iframe = true;
|
|
window.open(wsIP.replace("wss", "https"), '_blank', "width=800,height=800");
|
|
|
|
alert("Try to refresh the page. Else You are on firefox and the server use a self signed certificate, so you need to accept it manually on the popup, click on advanced and click on accept, after accepting close this popup and alert. If you already accept the certificate, try refresh the page")
|
|
}
|
|
console.log("Retry to connect nb: ", retryCount)
|
|
connectWebSocket();
|
|
retryCount++;
|
|
}, 3000)
|
|
}
|
|
|
|
async function handlePackets(packetList) {
|
|
const jsonArray = JSON.parse(packetList);
|
|
for (const packetObject of jsonArray) {
|
|
const type = packetObject.type;
|
|
const value = packetObject.value;
|
|
await handlePacket(type, value)
|
|
}
|
|
|
|
}
|
|
</script>
|