fix: replace ScriptProcessor with MediaRecorder for voice chat
This commit is contained in:
parent
efcb5a0dd6
commit
d3e2ebca78
79
game.js
79
game.js
|
|
@ -1245,7 +1245,7 @@
|
||||||
let voiceSocket = null;
|
let voiceSocket = null;
|
||||||
let voiceStream = null;
|
let voiceStream = null;
|
||||||
let audioCtx = null;
|
let audioCtx = null;
|
||||||
let voiceProcessor = null;
|
let voiceRecorder = null;
|
||||||
let voiceActive = false;
|
let voiceActive = false;
|
||||||
const VOICE_SERVER = 'https://voicegrech.mkn8n.ru';
|
const VOICE_SERVER = 'https://voicegrech.mkn8n.ru';
|
||||||
|
|
||||||
|
|
@ -1269,11 +1269,11 @@
|
||||||
voiceActive = false;
|
voiceActive = false;
|
||||||
voiceBtn.innerHTML = '🎤<span style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:36px;color:#e74c3c;font-weight:bold;">/</span>';
|
voiceBtn.innerHTML = '🎤<span style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:36px;color:#e74c3c;font-weight:bold;">/</span>';
|
||||||
voiceBtn.style.background = '#555';
|
voiceBtn.style.background = '#555';
|
||||||
|
if (voiceRecorder) { voiceRecorder.stop(); voiceRecorder = null; }
|
||||||
if (voiceStream) {
|
if (voiceStream) {
|
||||||
voiceStream.getTracks().forEach(t => t.stop());
|
voiceStream.getTracks().forEach(t => t.stop());
|
||||||
voiceStream = null;
|
voiceStream = null;
|
||||||
}
|
}
|
||||||
if (voiceProcessor) { voiceProcessor.disconnect(); voiceProcessor = null; }
|
|
||||||
if (audioCtx) { audioCtx.close(); audioCtx = null; }
|
if (audioCtx) { audioCtx.close(); audioCtx = null; }
|
||||||
if (voiceSocket) { voiceSocket.disconnect(); voiceSocket = null; }
|
if (voiceSocket) { voiceSocket.disconnect(); voiceSocket = null; }
|
||||||
return;
|
return;
|
||||||
|
|
@ -1281,30 +1281,8 @@
|
||||||
|
|
||||||
// Включить
|
// Включить
|
||||||
try {
|
try {
|
||||||
voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 24000 } });
|
voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } });
|
||||||
audioCtx = new AudioContext({ sampleRate: 24000 });
|
audioCtx = new AudioContext({ sampleRate: 24000 });
|
||||||
const source = audioCtx.createMediaStreamSource(voiceStream);
|
|
||||||
voiceProcessor = audioCtx.createScriptProcessor(4096, 1, 1);
|
|
||||||
|
|
||||||
voiceProcessor.onaudioprocess = (e) => {
|
|
||||||
if (!voiceActive || !voiceSocket || !voiceSocket.connected) return;
|
|
||||||
const pcm = e.inputBuffer.getChannelData(0);
|
|
||||||
// Конвертируем float32 → int16 для экономии трафика
|
|
||||||
const int16 = new Int16Array(pcm.length);
|
|
||||||
for (let i = 0; i < pcm.length; i++) {
|
|
||||||
const s = Math.max(-1, Math.min(1, pcm[i]));
|
|
||||||
int16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
|
||||||
}
|
|
||||||
voiceSocket.emit('voice_data', int16.buffer);
|
|
||||||
};
|
|
||||||
|
|
||||||
source.connect(voiceProcessor);
|
|
||||||
// ScriptProcessor MUST connect to destination to fire onaudioprocess events
|
|
||||||
// Use a zero-gain node to silence own playback while keeping the processor active
|
|
||||||
const silentGain = audioCtx.createGain();
|
|
||||||
silentGain.gain.value = 0; // mute — don't hear ourselves
|
|
||||||
voiceProcessor.connect(silentGain);
|
|
||||||
silentGain.connect(audioCtx.destination);
|
|
||||||
|
|
||||||
// Подключаемся к голосовому серверу
|
// Подключаемся к голосовому серверу
|
||||||
voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] });
|
voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] });
|
||||||
|
|
@ -1312,35 +1290,68 @@
|
||||||
voiceSocket.emit('voice_join', { world_id: worldId, x: player.x, y: player.y, name: playerName || 'Игрок' });
|
voiceSocket.emit('voice_join', { world_id: worldId, x: player.x, y: player.y, name: playerName || 'Игрок' });
|
||||||
});
|
});
|
||||||
|
|
||||||
voiceSocket.on('voice_in', (payload) => {
|
voiceSocket.on('voice_in', async (payload) => {
|
||||||
// Воспроизводим входящий голос
|
// Воспроизводим входящий голос (WebM/Opus or raw PCM)
|
||||||
const { data, meta, volume } = payload;
|
const { data, meta, volume } = payload;
|
||||||
if (!audioCtx || audioCtx.state === 'closed') return;
|
if (!audioCtx || audioCtx.state === 'closed') {
|
||||||
|
audioCtx = new AudioContext({ sampleRate: 24000 });
|
||||||
|
}
|
||||||
|
if (audioCtx.state === 'suspended') await audioCtx.resume();
|
||||||
|
|
||||||
// Int16 → Float32
|
try {
|
||||||
|
// Try WebM/Opus decode first (if server relayed MediaRecorder output)
|
||||||
|
const blob = new Blob([data], { type: 'audio/webm; codecs=opus' });
|
||||||
|
const arrayBuf = await blob.arrayBuffer();
|
||||||
|
const decoded = await audioCtx.decodeAudioData(arrayBuf);
|
||||||
|
const src = audioCtx.createBufferSource();
|
||||||
|
src.buffer = decoded;
|
||||||
|
const gain = audioCtx.createGain();
|
||||||
|
gain.gain.value = volume || 1;
|
||||||
|
src.connect(gain).connect(audioCtx.destination);
|
||||||
|
src.start();
|
||||||
|
} catch(_) {
|
||||||
|
// Fallback: raw PCM int16 → float32
|
||||||
|
try {
|
||||||
const int16 = new Int16Array(data);
|
const int16 = new Int16Array(data);
|
||||||
const float32 = new Float32Array(int16.length);
|
const float32 = new Float32Array(int16.length);
|
||||||
for (let i = 0; i < int16.length; i++) {
|
for (let i = 0; i < int16.length; i++) {
|
||||||
float32[i] = int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF) * volume;
|
float32[i] = int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buf = audioCtx.createBuffer(1, float32.length, 24000);
|
const buf = audioCtx.createBuffer(1, float32.length, 24000);
|
||||||
buf.getChannelData(0).set(float32);
|
buf.getChannelData(0).set(float32);
|
||||||
const src = audioCtx.createBufferSource();
|
const src = audioCtx.createBufferSource();
|
||||||
src.buffer = buf;
|
src.buffer = buf;
|
||||||
|
|
||||||
const gain = audioCtx.createGain();
|
const gain = audioCtx.createGain();
|
||||||
gain.gain.value = volume;
|
gain.gain.value = volume || 1;
|
||||||
src.connect(gain).connect(audioCtx.destination);
|
src.connect(gain).connect(audioCtx.destination);
|
||||||
src.start();
|
src.start();
|
||||||
|
} catch(e2) { /* ignore decode errors */ }
|
||||||
|
}
|
||||||
|
|
||||||
// Индикатор
|
// Индикатор
|
||||||
speakingIndicator.style.display = 'block';
|
speakingIndicator.style.display = 'block';
|
||||||
speakingIndicator.textContent = `🔊 ${meta.name}`;
|
speakingIndicator.textContent = '🔊 ' + (meta.name || '???');
|
||||||
clearTimeout(speakingTimeout);
|
clearTimeout(speakingTimeout);
|
||||||
speakingTimeout = setTimeout(() => { speakingIndicator.style.display = 'none'; }, 500);
|
speakingTimeout = setTimeout(() => { speakingIndicator.style.display = 'none'; }, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// MediaRecorder — moderne API, works in all browsers without ScriptProcessor
|
||||||
|
const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
|
||||||
|
? 'audio/webm;codecs=opus' : 'audio/webm';
|
||||||
|
voiceRecorder = new MediaRecorder(voiceStream, {
|
||||||
|
mimeType,
|
||||||
|
audioBitsPerSecond: 16000
|
||||||
|
});
|
||||||
|
voiceRecorder.ondataavailable = (e) => {
|
||||||
|
if (!voiceActive || !voiceSocket || !voiceSocket.connected) return;
|
||||||
|
if (e.data.size > 0) {
|
||||||
|
e.data.arrayBuffer().then(buf => {
|
||||||
|
voiceSocket.emit('voice_data', buf);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
voiceRecorder.start(100); // 100ms chunks for low latency
|
||||||
|
|
||||||
voiceActive = true;
|
voiceActive = true;
|
||||||
voiceBtn.textContent = '🎤';
|
voiceBtn.textContent = '🎤';
|
||||||
voiceBtn.style.background = '#2ecc71';
|
voiceBtn.style.background = '#2ecc71';
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="game.js?v=8"></script>
|
<script src="game.js?v=9"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue