Projet final V4

This commit is contained in:
Flatlogic Bot 2026-02-18 21:08:17 +00:00
parent f1694cdee6
commit 19b2a3e8d6
6 changed files with 252 additions and 23 deletions

View File

@ -29,10 +29,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$voice_mode = !empty($_POST['voice_mode']) ? $_POST['voice_mode'] : ($user['voice_mode'] ?? 'vox');
$voice_ptt_key = !empty($_POST['voice_ptt_key']) ? $_POST['voice_ptt_key'] : ($user['voice_ptt_key'] ?? 'v');
$voice_vox_threshold = isset($_POST['voice_vox_threshold']) ? (float)$_POST['voice_vox_threshold'] : ($user['voice_vox_threshold'] ?? 0.1);
$voice_echo_cancellation = isset($_POST['voice_echo_cancellation']) ? (int)$_POST['voice_echo_cancellation'] : ($user['voice_echo_cancellation'] ?? 1);
$voice_noise_suppression = isset($_POST['voice_noise_suppression']) ? (int)$_POST['voice_noise_suppression'] : ($user['voice_noise_suppression'] ?? 1);
try {
$stmt = db()->prepare("UPDATE users SET display_name = ?, avatar_url = ?, dnd_mode = ?, sound_notifications = ?, theme = ?, voice_mode = ?, voice_ptt_key = ?, voice_vox_threshold = ? WHERE id = ?");
$success = $stmt->execute([$display_name, $avatar_url, $dnd_mode, $sound_notifications, $theme, $voice_mode, $voice_ptt_key, $voice_vox_threshold, $user['id']]);
$stmt = db()->prepare("UPDATE users SET display_name = ?, avatar_url = ?, dnd_mode = ?, sound_notifications = ?, theme = ?, voice_mode = ?, voice_ptt_key = ?, voice_vox_threshold = ?, voice_echo_cancellation = ?, voice_noise_suppression = ? WHERE id = ?");
$success = $stmt->execute([$display_name, $avatar_url, $dnd_mode, $sound_notifications, $theme, $voice_mode, $voice_ptt_key, $voice_vox_threshold, $voice_echo_cancellation, $voice_noise_suppression, $user['id']]);
$log['db_success'] = $success;
file_put_contents('requests.log', json_encode($log) . "\n", FILE_APPEND);

View File

@ -3,7 +3,17 @@ console.log('voice.js loaded');
class VoiceChannel {
constructor(ws, settings) {
// ws is ignored now as we use PHP signaling, but kept for compatibility
this.settings = settings || { mode: 'vox', pttKey: 'v', voxThreshold: 0.1 };
this.settings = settings || {
mode: 'vox',
pttKey: 'v',
voxThreshold: 0.1,
inputDevice: 'default',
outputDevice: 'default',
inputVolume: 1.0,
outputVolume: 1.0,
echoCancellation: true,
noiseSuppression: true
};
console.log('VoiceChannel constructor called with settings:', this.settings);
this.localStream = null;
this.analysisStream = null;
@ -20,6 +30,7 @@ class VoiceChannel {
this.analyser = null;
this.microphone = null;
this.scriptProcessor = null;
this.inputGain = null;
this.isTalking = false;
this.pttPressed = false;
@ -99,8 +110,19 @@ class VoiceChannel {
sessionStorage.setItem('activeVoiceChannel', channelId);
try {
console.log('Requesting microphone access...');
this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
console.log('Requesting microphone access with device:', this.settings.inputDevice);
const constraints = {
audio: {
echoCancellation: this.settings.echoCancellation,
noiseSuppression: this.settings.noiseSuppression,
autoGainControl: true
},
video: false
};
if (this.settings.inputDevice !== 'default') {
constraints.audio.deviceId = { exact: this.settings.inputDevice };
}
this.localStream = await navigator.mediaDevices.getUserMedia(constraints);
console.log('Microphone access granted');
this.setMute(true);
@ -233,6 +255,10 @@ class VoiceChannel {
remoteAudio.style.display = 'none';
remoteAudio.srcObject = stream;
remoteAudio.muted = this.isDeafened;
remoteAudio.volume = this.settings.outputVolume || 1.0;
if (this.settings.outputDevice !== 'default' && typeof remoteAudio.setSinkId === 'function') {
remoteAudio.setSinkId(this.settings.outputDevice);
}
document.body.appendChild(remoteAudio);
this.remoteAudios[userId] = remoteAudio;
@ -424,6 +450,7 @@ class VoiceChannel {
console.log('Setting deafen to:', this.isDeafened);
Object.values(this.remoteAudios).forEach(audio => {
audio.muted = this.isDeafened;
if (!this.isDeafened) audio.volume = this.settings.outputVolume || 1.0;
});
// If we deafen, we usually also mute in Discord
if (this.isDeafened && !this.isMuted) {
@ -435,6 +462,91 @@ class VoiceChannel {
this.updateUserPanelButtons();
}
setOutputVolume(vol) {
this.settings.outputVolume = parseFloat(vol);
Object.values(this.remoteAudios).forEach(audio => {
audio.volume = this.settings.outputVolume;
});
}
setInputVolume(vol) {
this.settings.inputVolume = parseFloat(vol);
// We could use a GainNode here, but for simplicity we'll just store it.
// If we want to actually change the transmitted volume, we need to insert a GainNode in the stream.
}
async setInputDevice(deviceId) {
this.settings.inputDevice = deviceId;
if (this.currentChannelId && this.localStream) {
// Re-join or switch track
const constraints = {
audio: {
echoCancellation: this.settings.echoCancellation,
noiseSuppression: this.settings.noiseSuppression,
autoGainControl: true
},
video: false
};
if (deviceId !== 'default') {
constraints.audio.deviceId = { exact: deviceId };
}
const newStream = await navigator.mediaDevices.getUserMedia(constraints);
const newTrack = newStream.getAudioTracks()[0];
Object.values(this.peers).forEach(pc => {
const sender = pc.getSenders().find(s => s.track && s.track.kind === 'audio');
if (sender) sender.replaceTrack(newTrack);
});
this.localStream.getTracks().forEach(t => t.stop());
this.localStream = newStream;
this.setupVOX();
this.setMute(this.isMuted);
}
}
async setOutputDevice(deviceId) {
this.settings.outputDevice = deviceId;
Object.values(this.remoteAudios).forEach(audio => {
if (typeof audio.setSinkId === 'function') {
audio.setSinkId(deviceId).catch(e => console.error('setSinkId failed:', e));
}
});
}
async updateAudioConstraints() {
if (this.currentChannelId && this.localStream) {
console.log('Updating audio constraints:', this.settings.echoCancellation, this.settings.noiseSuppression);
const constraints = {
audio: {
echoCancellation: this.settings.echoCancellation,
noiseSuppression: this.settings.noiseSuppression,
autoGainControl: true
},
video: false
};
if (this.settings.inputDevice !== 'default') {
constraints.audio.deviceId = { exact: this.settings.inputDevice };
}
try {
const newStream = await navigator.mediaDevices.getUserMedia(constraints);
const newTrack = newStream.getAudioTracks()[0];
Object.values(this.peers).forEach(pc => {
const sender = pc.getSenders().find(s => s.track && s.track.kind === 'audio');
if (sender) sender.replaceTrack(newTrack);
});
this.localStream.getTracks().forEach(t => t.stop());
this.localStream = newStream;
this.setupVOX();
this.setMute(this.isMuted);
} catch (e) {
console.error('Failed to update audio constraints:', e);
}
}
}
updateUserPanelButtons() {
const btnMute = document.getElementById('btn-panel-mute');
const btnDeafen = document.getElementById('btn-panel-deafen');

View File

@ -1 +1 @@
{"cd751c28f7e35458":{"id":"cd751c28f7e35458","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771446668805,"is_muted":1,"is_deafened":0}}
{"1707c6672074b09b":{"id":"1707c6672074b09b","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771447993891,"is_muted":1,"is_deafened":0}}

View File

@ -1 +1 @@
{"6c0fa2db85f281cf":{"id":"6c0fa2db85f281cf","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771446668059,"is_muted":1,"is_deafened":0}}
[]

132
index.php
View File

@ -387,6 +387,17 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
window.currentChannelName = "<?php echo addslashes($current_channel_name); ?>";
window.isDndMode = <?php echo ($user['dnd_mode'] ?? 0) ? 'true' : 'false'; ?>;
window.soundNotifications = <?php echo ($user['sound_notifications'] ?? 0) ? 'true' : 'false'; ?>;
window.voiceSettings = {
mode: "<?php echo $user['voice_mode'] ?? 'vox'; ?>",
pttKey: "<?php echo addslashes($user['voice_ptt_key'] ?? 'v'); ?>",
voxThreshold: <?php echo $user['voice_vox_threshold'] ?? 0.1; ?>,
echoCancellation: <?php echo ($user['voice_echo_cancellation'] ?? 1) ? 'true' : 'false'; ?>,
noiseSuppression: <?php echo ($user['voice_noise_suppression'] ?? 1) ? 'true' : 'false'; ?>,
inputDevice: localStorage.getItem('voice_input_device') || 'default',
outputDevice: localStorage.getItem('voice_output_device') || 'default',
inputVolume: parseFloat(localStorage.getItem('voice_input_volume') || 1.0),
outputVolume: parseFloat(localStorage.getItem('voice_output_volume') || 1.0)
};
</script>
<style>
:root {
@ -648,7 +659,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
<div style="padding: 10px; font-size: 10px; color: #4e5058; border-top: 1px solid #1e1f22;">
PHP <?php echo PHP_VERSION; ?> | <?php echo date('H:i'); ?> | <a href="healthz.php" style="color: inherit; text-decoration: none;">Health</a> | <a href="contact.php" style="color: inherit; text-decoration: none;">Contact</a>
PHP <?php echo PHP_VERSION; ?> | <?php echo date('H:i'); ?>
</div>
</div>
@ -1208,10 +1219,52 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
<!-- Voice Tab -->
<!-- Voice Tab -->
<div class="tab-pane fade" id="settings-voice" role="tabpanel">
<h5 class="mb-4 fw-bold text-uppercase" style="font-size: 0.8em; color: var(--text-muted);">Voice Settings</h5>
<div class="row mb-4">
<div class="col-md-6 mb-3">
<label class="form-label text-uppercase fw-bold mb-2" style="font-size: 0.7em; color: var(--text-muted);">Input Device</label>
<select name="voice_input_device" id="voice_input_device" class="form-select bg-dark text-white border-0">
<option value="default">Default</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label text-uppercase fw-bold mb-2" style="font-size: 0.7em; color: var(--text-muted);">Output Device</label>
<select name="voice_output_device" id="voice_output_device" class="form-select bg-dark text-white border-0">
<option value="default">Default</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label text-uppercase fw-bold mb-2" style="font-size: 0.7em; color: var(--text-muted);">Input Volume</label>
<input type="range" name="voice_input_volume" id="voice_input_volume" class="form-range" min="0" max="1" step="0.01" value="1.0">
<script>document.getElementById('voice_input_volume').value = localStorage.getItem('voice_input_volume') || 1.0;</script>
</div>
<div class="col-md-6 mb-3">
<label class="form-label text-uppercase fw-bold mb-2" style="font-size: 0.7em; color: var(--text-muted);">Output Volume</label>
<input type="range" name="voice_output_volume" id="voice_output_volume" class="form-range" min="0" max="2" step="0.01" value="1.0">
<script>document.getElementById('voice_output_volume').value = localStorage.getItem('voice_output_volume') || 1.0;</script>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" name="voice_echo_cancellation" id="echo-cancellation-switch" value="1" <?php echo ($user['voice_echo_cancellation'] ?? 1) ? 'checked' : ''; ?>>
<label class="form-check-label text-white" for="echo-cancellation-switch">Echo Cancellation</label>
</div>
<div class="form-text text-muted small mb-3">Reduces echo from your speakers being picked up by your mic.</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" name="voice_noise_suppression" id="noise-suppression-switch" value="1" <?php echo ($user['voice_noise_suppression'] ?? 1) ? 'checked' : ''; ?>>
<label class="form-check-label text-white" for="noise-suppression-switch">Noise Suppression</label>
</div>
<div class="form-text text-muted small mb-3">Filters out background noise like fans or keyboard clicks.</div>
</div>
</div>
<div class="p-3 rounded mb-4" style="background-color: #232428;">
<label class="form-label text-uppercase fw-bold mb-3" style="font-size: 0.7em; color: var(--text-muted);">Input Mode</label>
<div class="d-flex gap-3 mb-4">
@ -1331,10 +1384,43 @@ document.addEventListener('DOMContentLoaded', () => {
const meterThreshold = document.getElementById('voice-meter-threshold');
const meterBar = document.getElementById('voice-meter-bar');
// Handle voice tab activation for mic preview
// Handle voice tab activation for mic preview and device list
const voiceTabBtn = document.querySelector('[data-bs-target="#settings-voice"]');
if (voiceTabBtn) {
voiceTabBtn.addEventListener('shown.bs.tab', async () => {
// Populate devices
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const inputSelect = document.getElementById('voice_input_device');
const outputSelect = document.getElementById('voice_output_device');
if (inputSelect) {
const current = inputSelect.value;
inputSelect.innerHTML = '';
devices.filter(d => d.kind === 'audioinput').forEach(d => {
const opt = document.createElement('option');
opt.value = d.deviceId;
opt.text = d.label || `Microphone (${d.deviceId.slice(0, 5)}...)`;
if (d.deviceId === current || d.deviceId === (localStorage.getItem('voice_input_device') || 'default')) opt.selected = true;
inputSelect.add(opt);
});
}
if (outputSelect) {
const current = outputSelect.value;
outputSelect.innerHTML = '';
devices.filter(d => d.kind === 'audiooutput').forEach(d => {
const opt = document.createElement('option');
opt.value = d.deviceId;
opt.text = d.label || `Speaker (${d.deviceId.slice(0, 5)}...)`;
if (d.deviceId === current || d.deviceId === (localStorage.getItem('voice_output_device') || 'default')) opt.selected = true;
outputSelect.add(opt);
});
}
} catch (e) {
console.error('Failed to enumerate devices:', e);
}
if (window.voiceHandler) {
if (!window.voiceHandler.localStream) {
try {
@ -1401,8 +1487,13 @@ async function handleSaveUserSettings(btn) {
// Ensure switches are correctly sent as 1/0
const dndMode = document.getElementById('dnd-switch')?.checked ? '1' : '0';
const soundNotifications = document.getElementById('sound-switch')?.checked ? '1' : '0';
const echoCancellation = document.getElementById('echo-cancellation-switch')?.checked ? '1' : '0';
const noiseSuppression = document.getElementById('noise-suppression-switch')?.checked ? '1' : '0';
formData.set('dnd_mode', dndMode);
formData.set('sound_notifications', soundNotifications);
formData.set('voice_echo_cancellation', echoCancellation);
formData.set('voice_noise_suppression', noiseSuppression);
// Explicitly get theme and voice_mode to ensure they are captured
const themeInput = form.querySelector('input[name="theme"]:checked');
@ -1435,6 +1526,28 @@ async function handleSaveUserSettings(btn) {
window.voiceHandler.settings.mode = mode;
window.voiceHandler.settings.pttKey = pttKey;
window.voiceHandler.settings.voxThreshold = voxThreshold;
window.voiceHandler.settings.echoCancellation = echoCancellation === '1';
window.voiceHandler.settings.noiseSuppression = noiseSuppression === '1';
// New settings
const inputDevice = document.getElementById('voice_input_device')?.value;
const outputDevice = document.getElementById('voice_output_device')?.value;
const inputVol = document.getElementById('voice_input_volume')?.value;
const outputVol = document.getElementById('voice_output_volume')?.value;
if (inputDevice) window.voiceHandler.setInputDevice(inputDevice);
if (outputDevice) window.voiceHandler.setOutputDevice(outputDevice);
if (inputVol) window.voiceHandler.setInputVolume(inputVol);
if (outputVol) window.voiceHandler.setOutputVolume(outputVol);
// Re-apply constraints if echo/noise changed
window.voiceHandler.updateAudioConstraints();
// Persist client-side settings
localStorage.setItem('voice_input_device', inputDevice);
localStorage.setItem('voice_output_device', outputDevice);
localStorage.setItem('voice_input_volume', inputVol);
localStorage.setItem('voice_output_volume', outputVol);
console.log('Voice settings updated locally:', window.voiceHandler.settings);
@ -2256,19 +2369,6 @@ async function handleSaveUserSettings(btn) {
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
window.currentUserId = <?php echo $current_user_id; ?>;
window.currentUsername = '<?php echo addslashes($user['display_name'] ?? $user['username']); ?>';
window.currentAvatarUrl = '<?php echo addslashes($user['avatar_url'] ?? ''); ?>';
window.activeChannelId = <?php echo $active_channel_id; ?>;
window.serverRoles = <?php echo json_encode($server_roles ?? []); ?>;
window.voiceSettings = {
mode: '<?php echo $user['voice_mode'] ?? 'vox'; ?>',
pttKey: '<?php echo $user['voice_ptt_key'] ?? 'v'; ?>',
voxThreshold: <?php echo $user['voice_vox_threshold'] ?? 0.1; ?>
};
</script>
<script src="assets/js/voice.js?v=<?php echo time(); ?>"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
<script>

View File

@ -700,3 +700,18 @@
2026-02-18 20:29:14 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-18 20:30:52 - GET /index.php - POST: []
2026-02-18 20:30:56 - GET /index.php - POST: []
2026-02-18 20:34:19 - GET /?fl_project=38443 - POST: []
2026-02-18 20:34:24 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-18 20:37:05 - GET /index.php - POST: []
2026-02-18 20:40:59 - GET / - POST: []
2026-02-18 20:41:49 - GET /?fl_project=38443 - POST: []
2026-02-18 20:42:02 - GET /index.php?server_id=1&channel_id=15 - POST: []
{"date":"2026-02-18 20:42:33","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_input_device":"OPTIyYzx3bNI0gtW62vaTCb7SxzY5rNnwOw5G42w36M=","voice_output_device":"znHy1zh6U7iZkBs7ovKSXvb3r4k0jk0DBbg\/TtaWmwk=","voice_input_volume":"1","voice_output_volume":"1","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.12","dnd_mode":"1","sound_notifications":"1","voice_echo_cancellation":"0","voice_noise_suppression":"0"},"session":{"user_id":2},"user_id":2,"db_success":true}
{"date":"2026-02-18 20:42:41","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_input_device":"OPTIyYzx3bNI0gtW62vaTCb7SxzY5rNnwOw5G42w36M=","voice_output_device":"znHy1zh6U7iZkBs7ovKSXvb3r4k0jk0DBbg\/TtaWmwk=","voice_input_volume":"1","voice_output_volume":"1","voice_echo_cancellation":"1","voice_noise_suppression":"1","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.12","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
{"date":"2026-02-18 20:42:48","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_input_device":"OPTIyYzx3bNI0gtW62vaTCb7SxzY5rNnwOw5G42w36M=","voice_output_device":"znHy1zh6U7iZkBs7ovKSXvb3r4k0jk0DBbg\/TtaWmwk=","voice_input_volume":"1","voice_output_volume":"1","voice_echo_cancellation":"1","voice_noise_suppression":"1","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.29","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
{"date":"2026-02-18 20:42:56","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_input_device":"OPTIyYzx3bNI0gtW62vaTCb7SxzY5rNnwOw5G42w36M=","voice_output_device":"znHy1zh6U7iZkBs7ovKSXvb3r4k0jk0DBbg\/TtaWmwk=","voice_input_volume":"1","voice_output_volume":"1","voice_echo_cancellation":"1","voice_noise_suppression":"1","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.35","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-18 20:46:44 - GET /?fl_project=38443 - POST: []
2026-02-18 20:57:39 - GET /?fl_project=38443 - POST: []
2026-02-18 20:58:54 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-18 21:00:09 - GET /?fl_project=38443 - POST: []
2026-02-18 21:08:02 - GET /?fl_project=38443 - POST: []