From 72e14c17baa0de7bde815782cf3a04152ac34944 Mon Sep 17 00:00:00 2001 From: Hampus Date: Tue, 13 Oct 2026 04:36:23 +0200 Subject: [PATCH] fix(voice): keep the playback boost while the window is hidden (#3444) --- .../engine/VoiceParticipantStateMachine.ts | 33 ++++--------------- .../VoiceEngineV2AppRemoteSpeakingAdapter.ts | 33 +++---------------- 2 files changed, 10 insertions(+), 56 deletions(-) diff --git a/fluxer_app/src/features/voice/engine/VoiceParticipantStateMachine.ts b/fluxer_app/src/features/voice/engine/VoiceParticipantStateMachine.ts index 4eb72137c..ec07db6f4 100644 --- a/fluxer_app/src/features/voice/engine/VoiceParticipantStateMachine.ts +++ b/fluxer_app/src/features/voice/engine/VoiceParticipantStateMachine.ts @@ -38,12 +38,10 @@ interface RemoteSpeakingAnalyserState { export type VoiceRemoteSpeakingCommand = | {type: 'setAudioLevelSpeaking'; identity: string; speaking: boolean} | {type: 'setPlaybackBoost'; identity: string; boost: number} - | {type: 'clearPlaybackBoost'; identity: string} - | {type: 'rehydrateRemoteAnalysers'}; + | {type: 'clearPlaybackBoost'; identity: string}; interface VoiceRemoteSpeakingMachineContext { analysers: ReadonlyMap; - analyserSuspendedByVisibility: boolean; commands: ReadonlyArray; } @@ -51,8 +49,6 @@ export type VoiceRemoteSpeakingEvent = | {type: 'remote.attach'; identity: string; track: unknown} | {type: 'remote.detach'; identity: string} | {type: 'remote.tick'; identity: string; rms: number; threshold: number; nowMs: number; trackEnded?: boolean} - | {type: 'remote.visibilityHidden'} - | {type: 'remote.visibilityVisible'} | {type: 'remote.clear'} | {type: 'remote.clearCommands'}; const EMPTY_REMOTE_COMMANDS: ReadonlyArray = []; @@ -68,10 +64,9 @@ const REMOTE_PLAYBACK_UNITY_EPSILON = 0.01; function remoteContext( analysers: ReadonlyMap = new Map(), - analyserSuspendedByVisibility = false, commands: ReadonlyArray = EMPTY_REMOTE_COMMANDS, ): VoiceRemoteSpeakingMachineContext { - return {analysers, analyserSuspendedByVisibility, commands}; + return {analysers, commands}; } function appendRemoteCommands( @@ -210,26 +205,14 @@ function tickRemoteAnalyser( return appendRemoteCommands({...context, analysers}, commands); } -function detachAllRemoteAnalysers( - context: VoiceRemoteSpeakingMachineContext, - analyserSuspendedByVisibility: boolean, -): VoiceRemoteSpeakingMachineContext { - if (context.analysers.size === 0) { - return context.analyserSuspendedByVisibility === analyserSuspendedByVisibility - ? context - : {...context, analyserSuspendedByVisibility}; - } +function detachAllRemoteAnalysers(context: VoiceRemoteSpeakingMachineContext): VoiceRemoteSpeakingMachineContext { + if (context.analysers.size === 0) return context; const commands: Array = []; for (const identity of context.analysers.keys()) { commands.push({type: 'setAudioLevelSpeaking', identity, speaking: false}); commands.push({type: 'clearPlaybackBoost', identity}); } - return appendRemoteCommands(remoteContext(new Map(), analyserSuspendedByVisibility, context.commands), commands); -} - -function showRemoteAnalysers(context: VoiceRemoteSpeakingMachineContext): VoiceRemoteSpeakingMachineContext { - if (!context.analyserSuspendedByVisibility) return context; - return appendRemoteCommands({...context, analyserSuspendedByVisibility: false}, [{type: 'rehydrateRemoteAnalysers'}]); + return appendRemoteCommands(remoteContext(new Map(), context.commands), commands); } const voiceRemoteSpeakingStateMachine = setup({ @@ -245,9 +228,7 @@ const voiceRemoteSpeakingStateMachine = setup({ event.type === 'remote.detach' ? detachRemoteAnalyser(context, event.identity) : context, ), tick: assign(({context, event}) => (event.type === 'remote.tick' ? tickRemoteAnalyser(context, event) : context)), - hide: assign(({context}) => detachAllRemoteAnalysers(context, true)), - show: assign(({context}) => showRemoteAnalysers(context)), - clear: assign(({context}) => detachAllRemoteAnalysers(context, false)), + clear: assign(({context}) => detachAllRemoteAnalysers(context)), clearCommands: assign(({context}) => ({...context, commands: EMPTY_REMOTE_COMMANDS})), }, }).createMachine({ @@ -257,8 +238,6 @@ const voiceRemoteSpeakingStateMachine = setup({ 'remote.attach': {actions: 'attach'}, 'remote.detach': {actions: 'detach'}, 'remote.tick': {actions: 'tick'}, - 'remote.visibilityHidden': {actions: 'hide'}, - 'remote.visibilityVisible': {actions: 'show'}, 'remote.clear': {actions: 'clear'}, 'remote.clearCommands': {actions: 'clearCommands'}, }, diff --git a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppRemoteSpeakingAdapter.ts b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppRemoteSpeakingAdapter.ts index eccf5d910..5a1ebda92 100644 --- a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppRemoteSpeakingAdapter.ts +++ b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppRemoteSpeakingAdapter.ts @@ -64,8 +64,7 @@ class VoiceEngineV2AppRemoteSpeakingAdapter { private audioContext: AudioContext | null = null; private audioContextIsShared = false; private analysers = new Map(); - private installedVisibilityListener = false; - private roomForRehydrate: Room | null = null; + private hydratedRoom: Room | null = null; private machineSnapshot: VoiceRemoteSpeakingSnapshot = createVoiceRemoteSpeakingSnapshot(); private autoScheduleTimerId: number | null = null; private readonly autoSchedule: boolean; @@ -96,9 +95,6 @@ class VoiceEngineV2AppRemoteSpeakingAdapter { clearRemoteVoicePlaybackBoost(command.identity); this.reapplyParticipantVolume(command.identity); break; - case 'rehydrateRemoteAnalysers': - if (this.roomForRehydrate) this.hydrateFromRoom(this.roomForRehydrate); - break; } } } @@ -112,22 +108,6 @@ class VoiceEngineV2AppRemoteSpeakingAdapter { this.stopAutoSchedule(); } - private ensureVisibilityListener(): void { - if (this.installedVisibilityListener) return; - if (typeof document === 'undefined') return; - document.addEventListener('visibilitychange', () => { - if (document.hidden) { - if (this.machineSnapshot.context.analyserSuspendedByVisibility) return; - this.detachAllAdapterAnalysers(); - this.applyCommands(this.transition({type: 'remote.visibilityHidden'})); - } else { - if (!this.machineSnapshot.context.analyserSuspendedByVisibility) return; - this.applyCommands(this.transition({type: 'remote.visibilityVisible'})); - } - }); - this.installedVisibilityListener = true; - } - private getAudioContext(): AudioContext | null { if (typeof window === 'undefined') return null; if (this.audioContext && this.audioContext.state !== 'closed') { @@ -166,8 +146,6 @@ class VoiceEngineV2AppRemoteSpeakingAdapter { this.analysers.size <= REMOTE_SPEAKING_ANALYSER_HANDLES_CAP, 'remote speaking analyser registry exceeded cap', ); - this.ensureVisibilityListener(); - if (this.machineSnapshot.context.analyserSuspendedByVisibility) return; if (participant.isLocal) return; if (publication.source !== VoiceTrackSource.Microphone) return; if (track.kind !== VoiceTrackKind.Audio) return; @@ -249,12 +227,12 @@ class VoiceEngineV2AppRemoteSpeakingAdapter { ); this.detachAllAdapterAnalysers(); this.applyCommands(this.transition({type: 'remote.clear'})); - this.roomForRehydrate = null; + this.hydratedRoom = null; assert.equal(this.analysers.size, 0, 'clear post-condition: no analysers'); } private reapplyParticipantVolume(identity: string): void { - const room = getRoomFromMediaEngine() ?? this.roomForRehydrate; + const room = getRoomFromMediaEngine() ?? this.hydratedRoom; const participant = room?.remoteParticipants.get(identity); if (!participant) return; try { @@ -267,9 +245,7 @@ class VoiceEngineV2AppRemoteSpeakingAdapter { hydrateFromRoom(room: Room): void { assertObjectLike(room, 'hydrateFromRoom.room'); assert.ok(room.remoteParticipants instanceof Map, 'hydrateFromRoom.room.remoteParticipants must be a Map'); - this.ensureVisibilityListener(); - this.roomForRehydrate = room; - if (this.machineSnapshot.context.analyserSuspendedByVisibility) return; + this.hydratedRoom = room; room.remoteParticipants.forEach((participant) => { participant.audioTrackPublications.forEach((publication) => { const remotePub = publication as RemoteTrackPublication; @@ -287,7 +263,6 @@ class VoiceEngineV2AppRemoteSpeakingAdapter { this.analysers.size <= REMOTE_SPEAKING_ANALYSER_HANDLES_CAP, 'tickHost pre-condition: analyser cap respected', ); - if (this.machineSnapshot.context.analyserSuspendedByVisibility) return; const sizeBefore = this.analysers.size; const expired: Array = []; for (const handle of this.analysers.values()) {