Projet final V8 + Vocal amélioré
This commit is contained in:
parent
29d6cdef20
commit
e988030fc8
@ -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]);
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user