fixing audio issues
This commit is contained in:
@@ -192,6 +192,15 @@ async function connect() {
|
||||
state.username = username;
|
||||
$('login-error').classList.add('hidden');
|
||||
|
||||
// Create the shared AudioContext and start the playback primer SYNCHRONOUSLY,
|
||||
// before any await. Mobile browsers (Android Chrome, iOS Safari, Firefox Android)
|
||||
// gate audio playback behind transient user activation that disappears after the
|
||||
// first await. Doing this here means audio playback for peers works on first join
|
||||
// without requiring some other media event (like a remote camera turning on) to
|
||||
// later activate the page's audio session.
|
||||
getAudioCtx();
|
||||
startAudioPlaybackPrimer();
|
||||
|
||||
try {
|
||||
// Check for saved device preferences
|
||||
const savedDevices = loadSavedDevices();
|
||||
@@ -201,12 +210,6 @@ async function connect() {
|
||||
|
||||
state.localStream = await navigator.mediaDevices.getUserMedia({ audio: audioConstraints, video: false });
|
||||
|
||||
// Create + resume the shared AudioContext while we're still inside the Connect
|
||||
// click's user-gesture window. Mobile browsers (especially iOS) only credit
|
||||
// AudioContexts created during a gesture; doing this here means every later peer
|
||||
// inherits a 'running' context and audio plays on the first try.
|
||||
getAudioCtx();
|
||||
|
||||
// Setup local audio analyser for speaking detection
|
||||
setupLocalAudioAnalyser();
|
||||
|
||||
@@ -633,6 +636,15 @@ function handlePageLeave() {
|
||||
teardownPeerAudio(id);
|
||||
try { state.peers[id].pc.close(); } catch (e) {}
|
||||
});
|
||||
if (state.audioPrimer) {
|
||||
state.audioPrimer.srcObject = null;
|
||||
state.audioPrimer.remove();
|
||||
state.audioPrimer = null;
|
||||
}
|
||||
if (state.audioPrimerSource) {
|
||||
try { state.audioPrimerSource.stop(); } catch (e) {}
|
||||
state.audioPrimerSource = null;
|
||||
}
|
||||
if (state.audioCtx) {
|
||||
try { state.audioCtx.close(); } catch (e) {}
|
||||
state.audioCtx = null;
|
||||
|
||||
@@ -12,6 +12,12 @@ const state = {
|
||||
// NOT used for output - peer audio plays via per-peer hidden <audio> elements (see
|
||||
// setupPeerAudio in webrtc.js) for reliable mobile autoplay.
|
||||
audioCtx: null,
|
||||
// Hidden <audio> element kept playing a silent stream for the entire session. Started
|
||||
// synchronously during the Connect tap so the page's audio session is active when
|
||||
// peer audio elements get created later. Without this, mobile browsers leave new
|
||||
// audio elements silent until some other media event activates the session.
|
||||
audioPrimer: null,
|
||||
audioPrimerSource: null,
|
||||
localAnalyser: null,
|
||||
localAudioSource: null,
|
||||
screenStream: null,
|
||||
|
||||
@@ -21,6 +21,75 @@ function getAudioCtx() {
|
||||
return state.audioCtx;
|
||||
}
|
||||
|
||||
// Start a hidden <audio> element playing a silent stream from the audio context. This
|
||||
// activates the page's audio session DURING the Connect tap (transient user activation
|
||||
// is still valid synchronously), so by the time peer audio elements are created later
|
||||
// from ontrack events - well outside any user gesture - audio playback Just Works. Without
|
||||
// this primer, mobile browsers leave new audio elements silent until some other media
|
||||
// event activates the session (e.g. a remote peer's camera coming on, which creates a
|
||||
// muted <video> that activates audio playback).
|
||||
//
|
||||
// Idempotent. Must be called synchronously inside the user-gesture handler.
|
||||
function startAudioPlaybackPrimer() {
|
||||
if (state.audioPrimer) return;
|
||||
const ctx = getAudioCtx();
|
||||
if (!ctx) return;
|
||||
|
||||
try {
|
||||
// Silent buffer source piping into a MediaStreamDestination. We use a non-zero
|
||||
// gain (well below audible) so browsers don't optimize the path away as silent.
|
||||
const buffer = ctx.createBuffer(1, 1, ctx.sampleRate);
|
||||
const source = ctx.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
source.loop = true;
|
||||
const gain = ctx.createGain();
|
||||
gain.gain.value = 0.0001;
|
||||
const dest = ctx.createMediaStreamDestination();
|
||||
source.connect(gain);
|
||||
gain.connect(dest);
|
||||
source.start();
|
||||
|
||||
const primer = document.createElement('audio');
|
||||
primer.autoplay = true;
|
||||
primer.playsInline = true;
|
||||
primer.id = 'audio-playback-primer';
|
||||
primer.srcObject = dest.stream;
|
||||
const container = document.getElementById('peer-audio-container') || document.body;
|
||||
container.appendChild(primer);
|
||||
primer.play().catch(e => console.warn('[Audio] primer play() rejected:', e?.message || e));
|
||||
|
||||
state.audioPrimer = primer;
|
||||
state.audioPrimerSource = source;
|
||||
console.log('[Audio] Playback primer started (ctx state:', ctx.state, ')');
|
||||
} catch (e) {
|
||||
console.warn('[Audio] startAudioPlaybackPrimer failed:', e?.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
// Defensive recovery for the case where a peer's <audio>.play() rejected (e.g. the
|
||||
// primer didn't take, or this device's mobile browser is stricter than expected). On
|
||||
// the next user tap anywhere in the page, retry play() on every peer audio element.
|
||||
let pendingAudioPlayRetry = false;
|
||||
function schedulePeerAudioPlayRetry() {
|
||||
if (pendingAudioPlayRetry) return;
|
||||
pendingAudioPlayRetry = true;
|
||||
const retry = () => {
|
||||
pendingAudioPlayRetry = false;
|
||||
document.removeEventListener('click', retry);
|
||||
document.removeEventListener('touchend', retry);
|
||||
Object.values(state.peers).forEach(peer => {
|
||||
if (peer.audioElement && peer.audioElement.paused) {
|
||||
peer.audioElement.play().catch(() => {});
|
||||
}
|
||||
});
|
||||
if (state.audioPrimer && state.audioPrimer.paused) {
|
||||
state.audioPrimer.play().catch(() => {});
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', retry, { once: true });
|
||||
document.addEventListener('touchend', retry, { once: true });
|
||||
}
|
||||
|
||||
function setupLocalAudioAnalyser() {
|
||||
if (!state.localStream) return;
|
||||
const ctx = getAudioCtx();
|
||||
@@ -585,10 +654,12 @@ function setupPeerAudio(peerId, stream) {
|
||||
const container = document.getElementById('peer-audio-container') || document.body;
|
||||
container.appendChild(audioEl);
|
||||
audioEl.srcObject = peer.audioDestination.stream;
|
||||
// Some browsers need an explicit play() even with autoplay=true. Catch failure
|
||||
// silently - if it really can't autoplay, the next user click will cover it via
|
||||
// resumeAllAudioContexts in ui.js.
|
||||
audioEl.play().catch(e => console.warn(`[Audio] play() rejected for ${peerId}:`, e?.message || e));
|
||||
audioEl.play().catch(e => {
|
||||
console.warn(`[Audio] play() rejected for ${peerId}:`, e?.message || e);
|
||||
// Defensive: queue a retry on next user tap. Should rarely fire because
|
||||
// startAudioPlaybackPrimer activates the audio session at Connect time.
|
||||
schedulePeerAudioPlayRetry();
|
||||
});
|
||||
peer.audioElement = audioEl;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user