mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
audio debug
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DkrikzyM.js"></script>
|
<script type="module" crossorigin src="/assets/index-Ck-dlrgH.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Bz1ixYh7.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Bz1ixYh7.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ export default function VideoTile({
|
|||||||
.map(([key]) => key);
|
.map(([key]) => key);
|
||||||
const limiterCaps = overcurrentLimiter?.caps || null;
|
const limiterCaps = overcurrentLimiter?.caps || null;
|
||||||
const limiterGroups = overcurrentLimiter?.overcurrent?.groups || null;
|
const limiterGroups = overcurrentLimiter?.overcurrent?.groups || null;
|
||||||
|
const debugAudio =
|
||||||
|
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugAudio');
|
||||||
const debugHud =
|
const debugHud =
|
||||||
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugHud');
|
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugHud');
|
||||||
const limiterFill = useMemo(() => {
|
const limiterFill = useMemo(() => {
|
||||||
@@ -126,6 +128,50 @@ export default function VideoTile({
|
|||||||
);
|
);
|
||||||
const duckGain = autoLevelEnabled && autoLevelMode === 'duck' && brushOrVacuumActive ? AUDIO_DUCK_FACTOR : 1;
|
const duckGain = autoLevelEnabled && autoLevelMode === 'duck' && brushOrVacuumActive ? AUDIO_DUCK_FACTOR : 1;
|
||||||
const effectiveRoverGain = Math.max(0, Math.min(1, baseRoverGain * duckGain));
|
const effectiveRoverGain = Math.max(0, Math.min(1, baseRoverGain * duckGain));
|
||||||
|
const logAudio = useCallback(
|
||||||
|
(event, meta = {}) => {
|
||||||
|
if (!debugAudio) return;
|
||||||
|
const audioEl = audioRef.current;
|
||||||
|
const ctx = audioContextRef.current;
|
||||||
|
console.log('[AudioDebug]', {
|
||||||
|
event,
|
||||||
|
roverLabel: label || null,
|
||||||
|
hasDedicatedAudio,
|
||||||
|
audioUrl: audioSessionInfo?.url || null,
|
||||||
|
autoLevelEnabled,
|
||||||
|
autoLevelMode,
|
||||||
|
baseRoverGain,
|
||||||
|
effectiveRoverGain,
|
||||||
|
element: audioEl
|
||||||
|
? {
|
||||||
|
muted: audioEl.muted,
|
||||||
|
volume: audioEl.volume,
|
||||||
|
paused: audioEl.paused,
|
||||||
|
ended: audioEl.ended,
|
||||||
|
readyState: audioEl.readyState,
|
||||||
|
networkState: audioEl.networkState,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
context: ctx
|
||||||
|
? {
|
||||||
|
state: ctx.state,
|
||||||
|
sampleRate: ctx.sampleRate,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
...meta,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
|
debugAudio,
|
||||||
|
label,
|
||||||
|
hasDedicatedAudio,
|
||||||
|
audioSessionInfo?.url,
|
||||||
|
autoLevelEnabled,
|
||||||
|
autoLevelMode,
|
||||||
|
baseRoverGain,
|
||||||
|
effectiveRoverGain,
|
||||||
|
],
|
||||||
|
);
|
||||||
const discordUrl =
|
const discordUrl =
|
||||||
session?.socials?.find((entry) => {
|
session?.socials?.find((entry) => {
|
||||||
const key = String(entry?.id || entry?.label || '').toLowerCase();
|
const key = String(entry?.id || entry?.label || '').toLowerCase();
|
||||||
@@ -147,6 +193,10 @@ export default function VideoTile({
|
|||||||
});
|
});
|
||||||
}, [debugHud, overlayFill, overlayMotors, overlayVisible, limiterActive, limiterCaps, limiterGroups, wheelOvercurrents]);
|
}, [debugHud, overlayFill, overlayMotors, overlayVisible, limiterActive, limiterCaps, limiterGroups, wheelOvercurrents]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logAudio('settings/update');
|
||||||
|
}, [logAudio]);
|
||||||
|
|
||||||
const scheduleRestart = useCallback(() => {
|
const scheduleRestart = useCallback(() => {
|
||||||
clearTimeout(restartTimer.current);
|
clearTimeout(restartTimer.current);
|
||||||
restartTimer.current = setTimeout(() => setRestartToken(Date.now()), RESTART_DELAY_MS);
|
restartTimer.current = setTimeout(() => setRestartToken(Date.now()), RESTART_DELAY_MS);
|
||||||
@@ -181,6 +231,13 @@ export default function VideoTile({
|
|||||||
compressor.attack.value = COMPRESSOR_SETTINGS.attack;
|
compressor.attack.value = COMPRESSOR_SETTINGS.attack;
|
||||||
compressor.release.value = COMPRESSOR_SETTINGS.release;
|
compressor.release.value = COMPRESSOR_SETTINGS.release;
|
||||||
audioCompressorRef.current = compressor;
|
audioCompressorRef.current = compressor;
|
||||||
|
logAudio('graph/compressor-created', {
|
||||||
|
threshold: compressor.threshold.value,
|
||||||
|
knee: compressor.knee.value,
|
||||||
|
ratio: compressor.ratio.value,
|
||||||
|
attack: compressor.attack.value,
|
||||||
|
release: compressor.release.value,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!audioGainRef.current) {
|
if (!audioGainRef.current) {
|
||||||
audioGainRef.current = ctx.createGain();
|
audioGainRef.current = ctx.createGain();
|
||||||
@@ -191,29 +248,34 @@ export default function VideoTile({
|
|||||||
audioSourceNodeRef.current.connect(audioCompressorRef.current);
|
audioSourceNodeRef.current.connect(audioCompressorRef.current);
|
||||||
audioCompressorRef.current.connect(audioGainRef.current);
|
audioCompressorRef.current.connect(audioGainRef.current);
|
||||||
audioGainRef.current.connect(ctx.destination);
|
audioGainRef.current.connect(ctx.destination);
|
||||||
|
logAudio('graph/connected');
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
graphFailedRef.current = true;
|
graphFailedRef.current = true;
|
||||||
|
logAudio('graph/failed');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, []);
|
}, [logAudio]);
|
||||||
|
|
||||||
const resumeAudioContext = useCallback(async () => {
|
const resumeAudioContext = useCallback(async () => {
|
||||||
const ctx = audioContextRef.current;
|
const ctx = audioContextRef.current;
|
||||||
if (!ctx || ctx.state !== 'suspended') return;
|
if (!ctx || ctx.state !== 'suspended') return;
|
||||||
|
logAudio('context/resume-attempt');
|
||||||
await ctx.resume().catch(() => {});
|
await ctx.resume().catch(() => {});
|
||||||
}, []);
|
logAudio('context/resume-result');
|
||||||
|
}, [logAudio]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!audioSessionInfo?.url || !autoLevelEnabled || autoLevelMode !== 'compressor') {
|
if (!audioSessionInfo?.url || !autoLevelEnabled || autoLevelMode !== 'compressor') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const unlock = () => {
|
const unlock = () => {
|
||||||
|
logAudio('unlock/gesture');
|
||||||
resumeAudioContext();
|
resumeAudioContext();
|
||||||
const target = audioRef.current;
|
const target = audioRef.current;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
target.muted = false;
|
target.muted = false;
|
||||||
target.play().catch(() => {});
|
target.play().then(() => logAudio('unlock/play-ok')).catch((err) => logAudio('unlock/play-fail', { error: err?.message }));
|
||||||
};
|
};
|
||||||
window.addEventListener('pointerdown', unlock, { passive: true });
|
window.addEventListener('pointerdown', unlock, { passive: true });
|
||||||
window.addEventListener('keydown', unlock);
|
window.addEventListener('keydown', unlock);
|
||||||
@@ -221,7 +283,7 @@ export default function VideoTile({
|
|||||||
window.removeEventListener('pointerdown', unlock);
|
window.removeEventListener('pointerdown', unlock);
|
||||||
window.removeEventListener('keydown', unlock);
|
window.removeEventListener('keydown', unlock);
|
||||||
};
|
};
|
||||||
}, [audioSessionInfo?.url, autoLevelEnabled, autoLevelMode, resumeAudioContext]);
|
}, [audioSessionInfo?.url, autoLevelEnabled, autoLevelMode, resumeAudioContext, logAudio]);
|
||||||
|
|
||||||
const ensurePlayback = useCallback(async () => {
|
const ensurePlayback = useCallback(async () => {
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
@@ -305,9 +367,10 @@ export default function VideoTile({
|
|||||||
let active = true;
|
let active = true;
|
||||||
let player;
|
let player;
|
||||||
const resetMuteId = setTimeout(() => setMuted(true), 0);
|
const resetMuteId = setTimeout(() => setMuted(true), 0);
|
||||||
const handleStatus = (nextStatus, info) => {
|
const handleStatus = (nextStatus, info) => {
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
setStatus(nextStatus);
|
logAudio('video/status', { nextStatus, info: info || null });
|
||||||
|
setStatus(nextStatus);
|
||||||
setDetail(info || null);
|
setDetail(info || null);
|
||||||
if (nextStatus === 'playing') {
|
if (nextStatus === 'playing') {
|
||||||
ensurePlayback();
|
ensurePlayback();
|
||||||
@@ -337,7 +400,7 @@ export default function VideoTile({
|
|||||||
clearTimeout(resetMuteId);
|
clearTimeout(resetMuteId);
|
||||||
player?.stop();
|
player?.stop();
|
||||||
};
|
};
|
||||||
}, [usingSnapshot, sessionInfo?.url, sessionInfo?.token, restartToken, scheduleRestart, ensurePlayback, hasDedicatedAudio]);
|
}, [usingSnapshot, sessionInfo?.url, sessionInfo?.token, restartToken, scheduleRestart, ensurePlayback, hasDedicatedAudio, logAudio]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'stopped' && sessionInfo?.url) {
|
if (status === 'stopped' && sessionInfo?.url) {
|
||||||
@@ -351,6 +414,7 @@ export default function VideoTile({
|
|||||||
reductionPollRef.current = null;
|
reductionPollRef.current = null;
|
||||||
if (!audioEl || !audioSessionInfo?.url) {
|
if (!audioEl || !audioSessionInfo?.url) {
|
||||||
setLevelIndicator(null);
|
setLevelIndicator(null);
|
||||||
|
logAudio('route/no-audio-url');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,6 +428,7 @@ export default function VideoTile({
|
|||||||
const contextRunning = audioContextRef.current?.state === 'running';
|
const contextRunning = audioContextRef.current?.state === 'running';
|
||||||
// If Chrome has not unlocked WebAudio yet, keep direct element volume so audio still works.
|
// If Chrome has not unlocked WebAudio yet, keep direct element volume so audio still works.
|
||||||
audioEl.volume = contextRunning ? 0 : effectiveRoverGain;
|
audioEl.volume = contextRunning ? 0 : effectiveRoverGain;
|
||||||
|
logAudio('route/graph', { contextRunning, elementVolume: audioEl.volume });
|
||||||
if (gainNode) {
|
if (gainNode) {
|
||||||
gainNode.gain.value = effectiveRoverGain;
|
gainNode.gain.value = effectiveRoverGain;
|
||||||
}
|
}
|
||||||
@@ -385,6 +450,7 @@ export default function VideoTile({
|
|||||||
const amount = Math.round(Math.abs(reduction) * 10) / 10;
|
const amount = Math.round(Math.abs(reduction) * 10) / 10;
|
||||||
const formatted = Number.isInteger(amount) ? amount.toFixed(0) : amount.toFixed(1);
|
const formatted = Number.isInteger(amount) ? amount.toFixed(0) : amount.toFixed(1);
|
||||||
setLevelIndicator(`Level: ${formatted}dB`);
|
setLevelIndicator(`Level: ${formatted}dB`);
|
||||||
|
logAudio('compressor/reduction', { reduction, formatted });
|
||||||
} else {
|
} else {
|
||||||
setLevelIndicator(null);
|
setLevelIndicator(null);
|
||||||
}
|
}
|
||||||
@@ -394,6 +460,7 @@ export default function VideoTile({
|
|||||||
|
|
||||||
const hasGraph = Boolean(audioSourceNodeRef.current && audioGainRef.current);
|
const hasGraph = Boolean(audioSourceNodeRef.current && audioGainRef.current);
|
||||||
audioEl.volume = hasGraph ? 0 : effectiveRoverGain;
|
audioEl.volume = hasGraph ? 0 : effectiveRoverGain;
|
||||||
|
logAudio('route/direct', { hasGraph, elementVolume: audioEl.volume, duckMode, brushOrVacuumActive });
|
||||||
if (audioCompressorRef.current) {
|
if (audioCompressorRef.current) {
|
||||||
audioCompressorRef.current.threshold.value = 0;
|
audioCompressorRef.current.threshold.value = 0;
|
||||||
audioCompressorRef.current.knee.value = 0;
|
audioCompressorRef.current.knee.value = 0;
|
||||||
@@ -417,6 +484,7 @@ export default function VideoTile({
|
|||||||
effectiveRoverGain,
|
effectiveRoverGain,
|
||||||
ensureAudioGraph,
|
ensureAudioGraph,
|
||||||
resumeAudioContext,
|
resumeAudioContext,
|
||||||
|
logAudio,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Audio-only WHEP (no pausing/muting; keeps trying to play)
|
// Audio-only WHEP (no pausing/muting; keeps trying to play)
|
||||||
@@ -428,6 +496,7 @@ export default function VideoTile({
|
|||||||
let player;
|
let player;
|
||||||
const handleStatus = (nextStatus, info) => {
|
const handleStatus = (nextStatus, info) => {
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
|
logAudio('audio/status', { nextStatus, info: info || null });
|
||||||
setAudioDetail(info || (nextStatus === 'connected' ? 'connected' : null));
|
setAudioDetail(info || (nextStatus === 'connected' ? 'connected' : null));
|
||||||
setAudioStatus((prev) => {
|
setAudioStatus((prev) => {
|
||||||
if (nextStatus === 'connected' && (prev === 'playing' || prev === 'connecting')) {
|
if (nextStatus === 'connected' && (prev === 'playing' || prev === 'connecting')) {
|
||||||
@@ -443,7 +512,7 @@ export default function VideoTile({
|
|||||||
const target = audioRef.current;
|
const target = audioRef.current;
|
||||||
if (target) {
|
if (target) {
|
||||||
target.muted = false;
|
target.muted = false;
|
||||||
target.play().catch(() => {});
|
target.play().then(() => logAudio('audio/play-ok')).catch((err) => logAudio('audio/play-fail', { error: err?.message }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (['error', 'failed', 'disconnected', 'closed'].includes(nextStatus)) {
|
if (['error', 'failed', 'disconnected', 'closed'].includes(nextStatus)) {
|
||||||
@@ -470,7 +539,7 @@ export default function VideoTile({
|
|||||||
active = false;
|
active = false;
|
||||||
player?.stop();
|
player?.stop();
|
||||||
};
|
};
|
||||||
}, [audioSessionInfo?.url, audioSessionInfo?.token, audioRestartToken, scheduleAudioRestart, resumeAudioContext]);
|
}, [audioSessionInfo?.url, audioSessionInfo?.token, audioRestartToken, scheduleAudioRestart, resumeAudioContext, logAudio]);
|
||||||
|
|
||||||
// Keep nudging the audio element to play in case autoplay was blocked.
|
// Keep nudging the audio element to play in case autoplay was blocked.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -490,14 +559,17 @@ export default function VideoTile({
|
|||||||
if (!target) return;
|
if (!target) return;
|
||||||
target.muted = false;
|
target.muted = false;
|
||||||
if (!target.paused && !target.ended) return;
|
if (!target.paused && !target.ended) return;
|
||||||
|
logAudio('retry/play-attempt');
|
||||||
await resumeAudioContext();
|
await resumeAudioContext();
|
||||||
target
|
target
|
||||||
.play()
|
.play()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
logAudio('retry/play-ok');
|
||||||
setAudioStatus((prev) => (prev === 'connected' ? 'playing' : prev));
|
setAudioStatus((prev) => (prev === 'connected' ? 'playing' : prev));
|
||||||
setAudioDetail((prev) => (prev === 'paused' ? null : prev));
|
setAudioDetail((prev) => (prev === 'paused' ? null : prev));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
logAudio('retry/play-fail', { error: err?.message });
|
||||||
setAudioDetail((prev) => prev || err?.message || 'autoplay blocked');
|
setAudioDetail((prev) => prev || err?.message || 'autoplay blocked');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -506,7 +578,7 @@ export default function VideoTile({
|
|||||||
audioPlayInterval.current = setInterval(attemptPlay, AUDIO_RETRY_MS);
|
audioPlayInterval.current = setInterval(attemptPlay, AUDIO_RETRY_MS);
|
||||||
|
|
||||||
return () => clearInterval(audioPlayInterval.current);
|
return () => clearInterval(audioPlayInterval.current);
|
||||||
}, [audioSessionInfo?.url, audioStatus, resumeAudioContext]);
|
}, [audioSessionInfo?.url, audioStatus, resumeAudioContext, logAudio]);
|
||||||
|
|
||||||
// Reflect audio element events back into status/detail so the HUD stays accurate.
|
// Reflect audio element events back into status/detail so the HUD stays accurate.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -514,10 +586,12 @@ export default function VideoTile({
|
|||||||
if (!audioEl) return undefined;
|
if (!audioEl) return undefined;
|
||||||
|
|
||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
|
logAudio('element/play');
|
||||||
setAudioStatus((prev) => (prev === 'error' ? prev : 'playing'));
|
setAudioStatus((prev) => (prev === 'error' ? prev : 'playing'));
|
||||||
setAudioDetail(null);
|
setAudioDetail(null);
|
||||||
};
|
};
|
||||||
const handlePause = () => {
|
const handlePause = () => {
|
||||||
|
logAudio('element/pause');
|
||||||
setAudioStatus((prev) => {
|
setAudioStatus((prev) => {
|
||||||
if (['error', 'failed', 'disconnected', 'closed', 'stopped'].includes(prev)) return prev;
|
if (['error', 'failed', 'disconnected', 'closed', 'stopped'].includes(prev)) return prev;
|
||||||
return 'paused';
|
return 'paused';
|
||||||
@@ -525,28 +599,39 @@ export default function VideoTile({
|
|||||||
setAudioDetail((prev) => prev || 'paused');
|
setAudioDetail((prev) => prev || 'paused');
|
||||||
};
|
};
|
||||||
const handleEnded = () => {
|
const handleEnded = () => {
|
||||||
|
logAudio('element/ended');
|
||||||
setAudioStatus((prev) => (prev === 'error' ? prev : 'stopped'));
|
setAudioStatus((prev) => (prev === 'error' ? prev : 'stopped'));
|
||||||
setAudioDetail((prev) => prev || 'ended');
|
setAudioDetail((prev) => prev || 'ended');
|
||||||
};
|
};
|
||||||
const handleError = () => {
|
const handleError = () => {
|
||||||
const { error } = audioEl;
|
const { error } = audioEl;
|
||||||
const message = error?.message || 'audio error';
|
const message = error?.message || 'audio error';
|
||||||
|
logAudio('element/error', { error: message });
|
||||||
setAudioStatus('error');
|
setAudioStatus('error');
|
||||||
setAudioDetail(message);
|
setAudioDetail(message);
|
||||||
};
|
};
|
||||||
|
const handleWaiting = () => logAudio('element/waiting');
|
||||||
|
const handleCanPlay = () => logAudio('element/canplay');
|
||||||
|
const handleStalled = () => logAudio('element/stalled');
|
||||||
|
|
||||||
audioEl.addEventListener('play', handlePlay);
|
audioEl.addEventListener('play', handlePlay);
|
||||||
audioEl.addEventListener('pause', handlePause);
|
audioEl.addEventListener('pause', handlePause);
|
||||||
audioEl.addEventListener('ended', handleEnded);
|
audioEl.addEventListener('ended', handleEnded);
|
||||||
audioEl.addEventListener('error', handleError);
|
audioEl.addEventListener('error', handleError);
|
||||||
|
audioEl.addEventListener('waiting', handleWaiting);
|
||||||
|
audioEl.addEventListener('canplay', handleCanPlay);
|
||||||
|
audioEl.addEventListener('stalled', handleStalled);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
audioEl.removeEventListener('play', handlePlay);
|
audioEl.removeEventListener('play', handlePlay);
|
||||||
audioEl.removeEventListener('pause', handlePause);
|
audioEl.removeEventListener('pause', handlePause);
|
||||||
audioEl.removeEventListener('ended', handleEnded);
|
audioEl.removeEventListener('ended', handleEnded);
|
||||||
audioEl.removeEventListener('error', handleError);
|
audioEl.removeEventListener('error', handleError);
|
||||||
|
audioEl.removeEventListener('waiting', handleWaiting);
|
||||||
|
audioEl.removeEventListener('canplay', handleCanPlay);
|
||||||
|
audioEl.removeEventListener('stalled', handleStalled);
|
||||||
};
|
};
|
||||||
}, [audioSessionInfo?.url]);
|
}, [audioSessionInfo?.url, logAudio]);
|
||||||
|
|
||||||
const snapshotStatus = snapshotFeed?.error
|
const snapshotStatus = snapshotFeed?.error
|
||||||
? `Error: ${snapshotFeed.error}`
|
? `Error: ${snapshotFeed.error}`
|
||||||
|
|||||||
Reference in New Issue
Block a user