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 {
|
||||
$ps = [];
|
||||
try {
|
||||
$stale_time = now_ms() - 15000;
|
||||
$stale_time = now_ms() - 30000;
|
||||
// Clean up stale sessions first
|
||||
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)
|
||||
if (rand(1, 10) === 1) {
|
||||
try {
|
||||
$stale_db_time = now_ms() - 15000;
|
||||
$stale_db_time = now_ms() - 30000;
|
||||
$stmt = db()->prepare("DELETE FROM voice_sessions WHERE last_seen < ?");
|
||||
$stmt->execute([$stale_db_time]);
|
||||
} catch (Exception $e) {}
|
||||
@ -181,7 +181,7 @@ if ($action === "list_all") {
|
||||
JOIN users u ON vs.user_id = u.id
|
||||
WHERE vs.last_seen > ?
|
||||
");
|
||||
$stale_db_time = now_ms() - 15000;
|
||||
$stale_db_time = now_ms() - 30000;
|
||||
$stmt->execute([$stale_db_time]);
|
||||
$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);
|
||||
|
||||
try {
|
||||
$stale_time = now_ms() - 15000;
|
||||
$stale_time = now_ms() - 30000;
|
||||
if ($target_type === 'user') {
|
||||
$stmt = db()->prepare("SELECT peer_id, name FROM voice_sessions WHERE user_id = ? AND last_seen > ?");
|
||||
$stmt->execute([(int)$target_id, $stale_time]);
|
||||
|
||||
@ -27,6 +27,8 @@ class VoiceChannel {
|
||||
this.isSelfMuted = false;
|
||||
this.isDeafened = false;
|
||||
|
||||
this.peerStates = {}; // userId -> { makingOffer, ignoreOffer }
|
||||
|
||||
this.whisperSettings = []; // from DB
|
||||
this.whisperPeers = new Set(); // active whisper target peer_ids
|
||||
this.isWhispering = false;
|
||||
@ -240,7 +242,7 @@ class VoiceChannel {
|
||||
|
||||
startPolling() {
|
||||
if (this.pollInterval) clearInterval(this.pollInterval);
|
||||
this.pollInterval = setInterval(() => this.poll(), 1000);
|
||||
this.pollInterval = setInterval(() => this.poll(), 500);
|
||||
this.poll(); // Initial poll
|
||||
}
|
||||
|
||||
@ -303,6 +305,11 @@ class VoiceChannel {
|
||||
if (this.peers[userId]) return this.peers[userId];
|
||||
|
||||
console.log('Creating PeerConnection for:', userId, 'as offeror:', isOfferor);
|
||||
|
||||
if (!this.peerStates[userId]) {
|
||||
this.peerStates[userId] = { makingOffer: false, ignoreOffer: false };
|
||||
}
|
||||
|
||||
const pc = new RTCPeerConnection({
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
@ -314,6 +321,22 @@ class VoiceChannel {
|
||||
|
||||
pc.oniceconnectionstatechange = () => {
|
||||
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) {
|
||||
@ -333,6 +356,11 @@ class VoiceChannel {
|
||||
console.log('Received remote track from:', userId, 'Stream count:', event.streams.length);
|
||||
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]) {
|
||||
console.log('Replacing existing audio element for:', userId);
|
||||
this.remoteAudios[userId].pause();
|
||||
@ -357,17 +385,13 @@ class VoiceChannel {
|
||||
console.log('Remote audio playing successfully for:', userId);
|
||||
}).catch(e => {
|
||||
console.warn('Autoplay prevented or play failed for:', userId, e);
|
||||
// In case of autoplay prevention, we might need a user gesture,
|
||||
// but they just clicked a channel so it should be fine.
|
||||
// In case of autoplay prevention, we might need a user gesture
|
||||
});
|
||||
};
|
||||
|
||||
if (isOfferor) {
|
||||
pc.createOffer().then(offer => {
|
||||
return pc.setLocalDescription(offer);
|
||||
}).then(() => {
|
||||
this.sendSignal(userId, { type: 'offer', offer: pc.localDescription });
|
||||
});
|
||||
// Manual offer if explicitly requested (though onnegotiationneeded should handle it)
|
||||
if (isOfferor && pc.signalingState === 'stable') {
|
||||
pc.onnegotiationneeded();
|
||||
}
|
||||
|
||||
return pc;
|
||||
@ -379,38 +403,68 @@ class VoiceChannel {
|
||||
|
||||
console.log('Handling signaling from:', from, 'type:', data.type);
|
||||
|
||||
switch (data.type) {
|
||||
case 'offer':
|
||||
await this.handleOffer(from, data.offer);
|
||||
break;
|
||||
case 'answer':
|
||||
await this.handleAnswer(from, data.answer);
|
||||
break;
|
||||
case 'ice_candidate':
|
||||
await this.handleCandidate(from, data.candidate);
|
||||
break;
|
||||
case 'voice_speaking':
|
||||
this.updateSpeakingUI(data.user_id, data.speaking, data.is_whisper);
|
||||
break;
|
||||
try {
|
||||
switch (data.type) {
|
||||
case 'offer':
|
||||
await this.handleOffer(from, data.offer);
|
||||
break;
|
||||
case 'answer':
|
||||
await this.handleAnswer(from, data.answer);
|
||||
break;
|
||||
case 'ice_candidate':
|
||||
await this.handleCandidate(from, data.candidate);
|
||||
break;
|
||||
case 'voice_speaking':
|
||||
this.updateSpeakingUI(data.user_id, data.speaking, data.is_whisper);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Signaling error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async handleOffer(from, offer) {
|
||||
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));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
this.sendSignal(from, { type: 'answer', answer: pc.localDescription });
|
||||
if (offer.type === "offer") {
|
||||
await pc.setLocalDescription();
|
||||
this.sendSignal(from, { type: 'answer', answer: pc.localDescription });
|
||||
}
|
||||
}
|
||||
|
||||
async handleAnswer(from, answer) {
|
||||
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) {
|
||||
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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user