mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(voice): keep the playback boost while the window is hidden (#3444)
This commit is contained in:
1 parent
aa0ebdb97e
commit
72e14c17ba
2 files changed
+10
-56
No files matched your search
@@ -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<string, RemoteSpeakingAnalyserState>;
|
||||
analyserSuspendedByVisibility: boolean;
|
||||
commands: ReadonlyArray<VoiceRemoteSpeakingCommand>;
|
||||
}
|
||||
|
||||
@@ -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<VoiceRemoteSpeakingCommand> = [];
|
||||
@@ -68,10 +64,9 @@ const REMOTE_PLAYBACK_UNITY_EPSILON = 0.01;
|
||||
|
||||
function remoteContext(
|
||||
analysers: ReadonlyMap<string, RemoteSpeakingAnalyserState> = new Map(),
|
||||
analyserSuspendedByVisibility = false,
|
||||
commands: ReadonlyArray<VoiceRemoteSpeakingCommand> = 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<VoiceRemoteSpeakingCommand> = [];
|
||||
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'},
|
||||
},
|
||||
|
||||
@@ -64,8 +64,7 @@ class VoiceEngineV2AppRemoteSpeakingAdapter {
|
||||
private audioContext: AudioContext | null = null;
|
||||
private audioContextIsShared = false;
|
||||
private analysers = new Map<string, AnalyserHandle>();
|
||||
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>(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<AnalyserHandle> = [];
|
||||
for (const handle of this.analysers.values()) {
|
||||
|
||||
Reference in new issue
Block a user