Projet final V8 + Vocal amélioré

This commit is contained in:
Flatlogic Bot 2026-02-19 15:47:00 +00:00
parent 29d6cdef20
commit e988030fc8
2 changed files with 85 additions and 31 deletions

View File

@ -34,7 +34,7 @@ function json_out($data, int $code = 200): void {
function get_room_participants(string $room): array { function get_room_participants(string $room): array {
$ps = []; $ps = [];
try { try {
$stale_time = now_ms() - 15000; $stale_time = now_ms() - 30000;
// Clean up stale sessions first // Clean up stale sessions first
db()->prepare("DELETE FROM voice_sessions WHERE last_seen < ?")->execute([$stale_time]); db()->prepare("DELETE FROM voice_sessions WHERE last_seen < ?")->execute([$stale_time]);
@ -168,7 +168,7 @@ if ($action === "list_all") {
// Periodic cleanup of the DB table (stale sessions > 15s) // Periodic cleanup of the DB table (stale sessions > 15s)
if (rand(1, 10) === 1) { if (rand(1, 10) === 1) {
try { try {
$stale_db_time = now_ms() - 15000; $stale_db_time = now_ms() - 30000;
$stmt = db()->prepare("DELETE FROM voice_sessions WHERE last_seen < ?"); $stmt = db()->prepare("DELETE FROM voice_sessions WHERE last_seen < ?");
$stmt->execute([$stale_db_time]); $stmt->execute([$stale_db_time]);
} catch (Exception $e) {} } catch (Exception $e) {}
@ -181,7 +181,7 @@ if ($action === "list_all") {
JOIN users u ON vs.user_id = u.id JOIN users u ON vs.user_id = u.id
WHERE vs.last_seen > ? WHERE vs.last_seen > ?
"); ");
$stale_db_time = now_ms() - 15000; $stale_db_time = now_ms() - 30000;
$stmt->execute([$stale_db_time]); $stmt->execute([$stale_db_time]);
$sessions = $stmt->fetchAll(PDO::FETCH_ASSOC); $sessions = $stmt->fetchAll(PDO::FETCH_ASSOC);
@ -253,7 +253,7 @@ if ($action === "find_whisper_targets") {
if (!$target_type || !$target_id) json_out(["error" => "Missing parameters"], 400); if (!$target_type || !$target_id) json_out(["error" => "Missing parameters"], 400);
try { try {
$stale_time = now_ms() - 15000; $stale_time = now_ms() - 30000;
if ($target_type === 'user') { if ($target_type === 'user') {
$stmt = db()->prepare("SELECT peer_id, name FROM voice_sessions WHERE user_id = ? AND last_seen > ?"); $stmt = db()->prepare("SELECT peer_id, name FROM voice_sessions WHERE user_id = ? AND last_seen > ?");
$stmt->execute([(int)$target_id, $stale_time]); $stmt->execute([(int)$target_id, $stale_time]);

View File

@ -27,6 +27,8 @@ class VoiceChannel {
this.isSelfMuted = false; this.isSelfMuted = false;
this.isDeafened = false; this.isDeafened = false;
this.peerStates = {}; // userId -> { makingOffer, ignoreOffer }
this.whisperSettings = []; // from DB this.whisperSettings = []; // from DB
this.whisperPeers = new Set(); // active whisper target peer_ids this.whisperPeers = new Set(); // active whisper target peer_ids
this.isWhispering = false; this.isWhispering = false;
@ -240,7 +242,7 @@ class VoiceChannel {
startPolling() { startPolling() {
if (this.pollInterval) clearInterval(this.pollInterval); if (this.pollInterval) clearInterval(this.pollInterval);
this.pollInterval = setInterval(() => this.poll(), 1000); this.pollInterval = setInterval(() => this.poll(), 500);
this.poll(); // Initial poll this.poll(); // Initial poll
} }
@ -303,6 +305,11 @@ class VoiceChannel {
if (this.peers[userId]) return this.peers[userId]; if (this.peers[userId]) return this.peers[userId];
console.log('Creating PeerConnection for:', userId, 'as offeror:', isOfferor); console.log('Creating PeerConnection for:', userId, 'as offeror:', isOfferor);
if (!this.peerStates[userId]) {
this.peerStates[userId] = { makingOffer: false, ignoreOffer: false };
}
const pc = new RTCPeerConnection({ const pc = new RTCPeerConnection({
iceServers: [ iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.l.google.com:19302' },
@ -314,6 +321,22 @@ class VoiceChannel {
pc.oniceconnectionstatechange = () => { pc.oniceconnectionstatechange = () => {
console.log(`ICE Connection State with ${userId}: ${pc.iceConnectionState}`); console.log(`ICE Connection State with ${userId}: ${pc.iceConnectionState}`);
if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'disconnected') {
console.log(`ICE failure with ${userId}, attempting to restart...`);
// If it failed, we could try to renegotiate, but for now let's just wait for poll to maybe clean it up
}
};
pc.onnegotiationneeded = async () => {
try {
this.peerStates[userId].makingOffer = true;
await pc.setLocalDescription();
this.sendSignal(userId, { type: 'offer', offer: pc.localDescription });
} catch (err) {
console.error('onnegotiationneeded error:', err);
} finally {
this.peerStates[userId].makingOffer = false;
}
}; };
if (this.localStream) { if (this.localStream) {
@ -333,6 +356,11 @@ class VoiceChannel {
console.log('Received remote track from:', userId, 'Stream count:', event.streams.length); console.log('Received remote track from:', userId, 'Stream count:', event.streams.length);
const stream = event.streams[0] || new MediaStream([event.track]); const stream = event.streams[0] || new MediaStream([event.track]);
// Ensure AudioContext is running
if (this.audioContext && this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
if (this.remoteAudios[userId]) { if (this.remoteAudios[userId]) {
console.log('Replacing existing audio element for:', userId); console.log('Replacing existing audio element for:', userId);
this.remoteAudios[userId].pause(); this.remoteAudios[userId].pause();
@ -357,17 +385,13 @@ class VoiceChannel {
console.log('Remote audio playing successfully for:', userId); console.log('Remote audio playing successfully for:', userId);
}).catch(e => { }).catch(e => {
console.warn('Autoplay prevented or play failed for:', userId, e); console.warn('Autoplay prevented or play failed for:', userId, e);
// In case of autoplay prevention, we might need a user gesture, // In case of autoplay prevention, we might need a user gesture
// but they just clicked a channel so it should be fine.
}); });
}; };
if (isOfferor) { // Manual offer if explicitly requested (though onnegotiationneeded should handle it)
pc.createOffer().then(offer => { if (isOfferor && pc.signalingState === 'stable') {
return pc.setLocalDescription(offer); pc.onnegotiationneeded();
}).then(() => {
this.sendSignal(userId, { type: 'offer', offer: pc.localDescription });
});
} }
return pc; return pc;
@ -379,38 +403,68 @@ class VoiceChannel {
console.log('Handling signaling from:', from, 'type:', data.type); console.log('Handling signaling from:', from, 'type:', data.type);
switch (data.type) { try {
case 'offer': switch (data.type) {
await this.handleOffer(from, data.offer); case 'offer':
break; await this.handleOffer(from, data.offer);
case 'answer': break;
await this.handleAnswer(from, data.answer); case 'answer':
break; await this.handleAnswer(from, data.answer);
case 'ice_candidate': break;
await this.handleCandidate(from, data.candidate); case 'ice_candidate':
break; await this.handleCandidate(from, data.candidate);
case 'voice_speaking': break;
this.updateSpeakingUI(data.user_id, data.speaking, data.is_whisper); case 'voice_speaking':
break; this.updateSpeakingUI(data.user_id, data.speaking, data.is_whisper);
break;
}
} catch (err) {
console.error('Signaling error:', err);
} }
} }
async handleOffer(from, offer) { async handleOffer(from, offer) {
const pc = this.createPeerConnection(from, false); const pc = this.createPeerConnection(from, false);
const state = this.peerStates[from];
const offerCollision = (offer.type === "offer") &&
(state.makingOffer || pc.signalingState !== "stable");
// Politeness: higher peer_id is polite
const isPolite = this.myPeerId > from;
state.ignoreOffer = !isPolite && offerCollision;
if (state.ignoreOffer) {
console.log('Polite peer: ignoring offer from impolite peer to avoid collision', from);
return;
}
await pc.setRemoteDescription(new RTCSessionDescription(offer)); await pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await pc.createAnswer(); if (offer.type === "offer") {
await pc.setLocalDescription(answer); await pc.setLocalDescription();
this.sendSignal(from, { type: 'answer', answer: pc.localDescription }); this.sendSignal(from, { type: 'answer', answer: pc.localDescription });
}
} }
async handleAnswer(from, answer) { async handleAnswer(from, answer) {
const pc = this.peers[from]; const pc = this.peers[from];
if (pc) await pc.setRemoteDescription(new RTCSessionDescription(answer)); if (pc) {
await pc.setRemoteDescription(new RTCSessionDescription(answer));
}
} }
async handleCandidate(from, candidate) { async handleCandidate(from, candidate) {
const pc = this.peers[from]; const pc = this.peers[from];
if (pc) await pc.addIceCandidate(new RTCIceCandidate(candidate)); const state = this.peerStates[from];
try {
if (pc) {
await pc.addIceCandidate(new RTCIceCandidate(candidate));
}
} catch (err) {
if (!state || !state.ignoreOffer) {
console.warn('Failed to add ICE candidate', err);
}
}
} }
setupVOX() { setupVOX() {