This commit is contained in:
legop3
2026-02-21 13:30:46 -05:00
parent 4ccc7dfae6
commit eefe6f01bb
3 changed files with 93 additions and 28 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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-Ck-dlrgH.js"></script> <script type="module" crossorigin src="/assets/index-BkBPhgTa.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bz1ixYh7.css"> <link rel="stylesheet" crossorigin href="/assets/index-Bz1ixYh7.css">
</head> </head>
<body> <body>
+82 -17
View File
@@ -133,8 +133,9 @@ export default function VideoTile({
if (!debugAudio) return; if (!debugAudio) return;
const audioEl = audioRef.current; const audioEl = audioRef.current;
const ctx = audioContextRef.current; const ctx = audioContextRef.current;
console.log('[AudioDebug]', { const payload = {
event, event,
ts: Date.now(),
roverLabel: label || null, roverLabel: label || null,
hasDedicatedAudio, hasDedicatedAudio,
audioUrl: audioSessionInfo?.url || null, audioUrl: audioSessionInfo?.url || null,
@@ -159,7 +160,12 @@ export default function VideoTile({
} }
: null, : null,
...meta, ...meta,
}); };
try {
console.log(`[AudioDebug] ${event} ${JSON.stringify(payload)}`);
} catch {
console.log('[AudioDebug]', event, payload);
}
}, },
[ [
debugAudio, debugAudio,
@@ -265,25 +271,75 @@ export default function VideoTile({
logAudio('context/resume-result'); logAudio('context/resume-result');
}, [logAudio]); }, [logAudio]);
const forceUnlockCompressorAudio = useCallback(async () => {
if (!audioSessionInfo?.url || !autoLevelEnabled || autoLevelMode !== 'compressor') return;
const graphReady = ensureAudioGraph();
if (!graphReady) {
logAudio('unlock/no-graph');
return;
}
await resumeAudioContext();
const target = audioRef.current;
const gainNode = audioGainRef.current;
if (gainNode) {
gainNode.gain.value = effectiveRoverGain;
}
if (!target) {
logAudio('unlock/no-audio-element');
return;
}
// Compressor mode uses graph-only output.
target.volume = 0;
target.muted = false;
target
.play()
.then(() => logAudio('unlock/force-play-ok'))
.catch((err) => logAudio('unlock/force-play-fail', { error: err?.message }));
}, [
audioSessionInfo?.url,
autoLevelEnabled,
autoLevelMode,
ensureAudioGraph,
resumeAudioContext,
effectiveRoverGain,
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'); logAudio('unlock/gesture');
resumeAudioContext(); forceUnlockCompressorAudio();
const target = audioRef.current;
if (!target) return;
target.muted = false;
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, capture: true });
window.addEventListener('touchstart', unlock, { passive: true, capture: true });
window.addEventListener('click', unlock, { passive: true, capture: true });
window.addEventListener('keydown', unlock); window.addEventListener('keydown', unlock);
return () => { return () => {
window.removeEventListener('pointerdown', unlock); window.removeEventListener('pointerdown', unlock);
window.removeEventListener('touchstart', unlock);
window.removeEventListener('click', unlock);
window.removeEventListener('keydown', unlock); window.removeEventListener('keydown', unlock);
}; };
}, [audioSessionInfo?.url, autoLevelEnabled, autoLevelMode, resumeAudioContext, logAudio]); }, [audioSessionInfo?.url, autoLevelEnabled, autoLevelMode, forceUnlockCompressorAudio, logAudio]);
useEffect(() => {
if (!audioSessionInfo?.url || !autoLevelEnabled || autoLevelMode !== 'compressor') {
return undefined;
}
const interval = setInterval(() => {
const ctx = audioContextRef.current;
const statusActive = ['connecting', 'connected', 'playing', 'paused'].includes(audioStatus);
if (!statusActive) return;
if (!ctx || ctx.state !== 'running') {
logAudio('unlock/interval');
forceUnlockCompressorAudio();
}
}, 1000);
return () => clearInterval(interval);
}, [audioSessionInfo?.url, autoLevelEnabled, autoLevelMode, audioStatus, forceUnlockCompressorAudio, logAudio]);
const ensurePlayback = useCallback(async () => { const ensurePlayback = useCallback(async () => {
const video = videoRef.current; const video = videoRef.current;
@@ -426,11 +482,11 @@ export default function VideoTile({
const compressor = audioCompressorRef.current; const compressor = audioCompressorRef.current;
const gainNode = audioGainRef.current; const gainNode = audioGainRef.current;
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. // Compressor mode is graph-only: do not route direct element output.
audioEl.volume = contextRunning ? 0 : effectiveRoverGain; audioEl.volume = 0;
logAudio('route/graph', { contextRunning, elementVolume: audioEl.volume }); logAudio('route/graph', { contextRunning, elementVolume: audioEl.volume });
if (gainNode) { if (gainNode) {
gainNode.gain.value = effectiveRoverGain; gainNode.gain.value = contextRunning ? effectiveRoverGain : 0;
} }
if (compressor) { if (compressor) {
compressor.threshold.value = COMPRESSOR_SETTINGS.threshold; compressor.threshold.value = COMPRESSOR_SETTINGS.threshold;
@@ -458,9 +514,8 @@ export default function VideoTile({
return; return;
} }
const hasGraph = Boolean(audioSourceNodeRef.current && audioGainRef.current); audioEl.volume = effectiveRoverGain;
audioEl.volume = hasGraph ? 0 : effectiveRoverGain; logAudio('route/direct', { elementVolume: audioEl.volume, duckMode, brushOrVacuumActive });
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;
@@ -469,7 +524,8 @@ export default function VideoTile({
audioCompressorRef.current.release.value = 0.25; audioCompressorRef.current.release.value = 0.25;
} }
if (audioGainRef.current) { if (audioGainRef.current) {
audioGainRef.current.gain.value = effectiveRoverGain; // In direct route, silence graph output to avoid duplicate path.
audioGainRef.current.gain.value = 0;
} }
if (duckMode && brushOrVacuumActive) { if (duckMode && brushOrVacuumActive) {
setLevelIndicator('Level: ducking'); setLevelIndicator('Level: ducking');
@@ -509,6 +565,7 @@ export default function VideoTile({
}); });
if (nextStatus === 'playing') { if (nextStatus === 'playing') {
resumeAudioContext(); resumeAudioContext();
forceUnlockCompressorAudio();
const target = audioRef.current; const target = audioRef.current;
if (target) { if (target) {
target.muted = false; target.muted = false;
@@ -539,7 +596,15 @@ export default function VideoTile({
active = false; active = false;
player?.stop(); player?.stop();
}; };
}, [audioSessionInfo?.url, audioSessionInfo?.token, audioRestartToken, scheduleAudioRestart, resumeAudioContext, logAudio]); }, [
audioSessionInfo?.url,
audioSessionInfo?.token,
audioRestartToken,
scheduleAudioRestart,
resumeAudioContext,
forceUnlockCompressorAudio,
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(() => {