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 =
|
export type VoiceRemoteSpeakingCommand =
|
||||||
| {type: 'setAudioLevelSpeaking'; identity: string; speaking: boolean}
|
| {type: 'setAudioLevelSpeaking'; identity: string; speaking: boolean}
|
||||||
| {type: 'setPlaybackBoost'; identity: string; boost: number}
|
| {type: 'setPlaybackBoost'; identity: string; boost: number}
|
||||||
| {type: 'clearPlaybackBoost'; identity: string}
|
| {type: 'clearPlaybackBoost'; identity: string};
|
||||||
| {type: 'rehydrateRemoteAnalysers'};
|
|
||||||
|
|
||||||
interface VoiceRemoteSpeakingMachineContext {
|
interface VoiceRemoteSpeakingMachineContext {
|
||||||
analysers: ReadonlyMap<string, RemoteSpeakingAnalyserState>;
|
analysers: ReadonlyMap<string, RemoteSpeakingAnalyserState>;
|
||||||
analyserSuspendedByVisibility: boolean;
|
|
||||||
commands: ReadonlyArray<VoiceRemoteSpeakingCommand>;
|
commands: ReadonlyArray<VoiceRemoteSpeakingCommand>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,8 +49,6 @@ export type VoiceRemoteSpeakingEvent =
|
|||||||
| {type: 'remote.attach'; identity: string; track: unknown}
|
| {type: 'remote.attach'; identity: string; track: unknown}
|
||||||
| {type: 'remote.detach'; identity: string}
|
| {type: 'remote.detach'; identity: string}
|
||||||
| {type: 'remote.tick'; identity: string; rms: number; threshold: number; nowMs: number; trackEnded?: boolean}
|
| {type: 'remote.tick'; identity: string; rms: number; threshold: number; nowMs: number; trackEnded?: boolean}
|
||||||
| {type: 'remote.visibilityHidden'}
|
|
||||||
| {type: 'remote.visibilityVisible'}
|
|
||||||
| {type: 'remote.clear'}
|
| {type: 'remote.clear'}
|
||||||
| {type: 'remote.clearCommands'};
|
| {type: 'remote.clearCommands'};
|
||||||
const EMPTY_REMOTE_COMMANDS: ReadonlyArray<VoiceRemoteSpeakingCommand> = [];
|
const EMPTY_REMOTE_COMMANDS: ReadonlyArray<VoiceRemoteSpeakingCommand> = [];
|
||||||
@@ -68,10 +64,9 @@ const REMOTE_PLAYBACK_UNITY_EPSILON = 0.01;
|
|||||||
|
|
||||||
function remoteContext(
|
function remoteContext(
|
||||||
analysers: ReadonlyMap<string, RemoteSpeakingAnalyserState> = new Map(),
|
analysers: ReadonlyMap<string, RemoteSpeakingAnalyserState> = new Map(),
|
||||||
analyserSuspendedByVisibility = false,
|
|
||||||
commands: ReadonlyArray<VoiceRemoteSpeakingCommand> = EMPTY_REMOTE_COMMANDS,
|
commands: ReadonlyArray<VoiceRemoteSpeakingCommand> = EMPTY_REMOTE_COMMANDS,
|
||||||
): VoiceRemoteSpeakingMachineContext {
|
): VoiceRemoteSpeakingMachineContext {
|
||||||
return {analysers, analyserSuspendedByVisibility, commands};
|
return {analysers, commands};
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendRemoteCommands(
|
function appendRemoteCommands(
|
||||||
@@ -210,26 +205,14 @@ function tickRemoteAnalyser(
|
|||||||
return appendRemoteCommands({...context, analysers}, commands);
|
return appendRemoteCommands({...context, analysers}, commands);
|
||||||
}
|
}
|
||||||
|
|
||||||
function detachAllRemoteAnalysers(
|
function detachAllRemoteAnalysers(context: VoiceRemoteSpeakingMachineContext): VoiceRemoteSpeakingMachineContext {
|
||||||
context: VoiceRemoteSpeakingMachineContext,
|
if (context.analysers.size === 0) return context;
|
||||||
analyserSuspendedByVisibility: boolean,
|
|
||||||
): VoiceRemoteSpeakingMachineContext {
|
|
||||||
if (context.analysers.size === 0) {
|
|
||||||
return context.analyserSuspendedByVisibility === analyserSuspendedByVisibility
|
|
||||||
? context
|
|
||||||
: {...context, analyserSuspendedByVisibility};
|
|
||||||
}
|
|
||||||
const commands: Array<VoiceRemoteSpeakingCommand> = [];
|
const commands: Array<VoiceRemoteSpeakingCommand> = [];
|
||||||
for (const identity of context.analysers.keys()) {
|
for (const identity of context.analysers.keys()) {
|
||||||
commands.push({type: 'setAudioLevelSpeaking', identity, speaking: false});
|
commands.push({type: 'setAudioLevelSpeaking', identity, speaking: false});
|
||||||
commands.push({type: 'clearPlaybackBoost', identity});
|
commands.push({type: 'clearPlaybackBoost', identity});
|
||||||
}
|
}
|
||||||
return appendRemoteCommands(remoteContext(new Map(), analyserSuspendedByVisibility, context.commands), commands);
|
return appendRemoteCommands(remoteContext(new Map(), context.commands), commands);
|
||||||
}
|
|
||||||
|
|
||||||
function showRemoteAnalysers(context: VoiceRemoteSpeakingMachineContext): VoiceRemoteSpeakingMachineContext {
|
|
||||||
if (!context.analyserSuspendedByVisibility) return context;
|
|
||||||
return appendRemoteCommands({...context, analyserSuspendedByVisibility: false}, [{type: 'rehydrateRemoteAnalysers'}]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const voiceRemoteSpeakingStateMachine = setup({
|
const voiceRemoteSpeakingStateMachine = setup({
|
||||||
@@ -245,9 +228,7 @@ const voiceRemoteSpeakingStateMachine = setup({
|
|||||||
event.type === 'remote.detach' ? detachRemoteAnalyser(context, event.identity) : context,
|
event.type === 'remote.detach' ? detachRemoteAnalyser(context, event.identity) : context,
|
||||||
),
|
),
|
||||||
tick: assign(({context, event}) => (event.type === 'remote.tick' ? tickRemoteAnalyser(context, event) : context)),
|
tick: assign(({context, event}) => (event.type === 'remote.tick' ? tickRemoteAnalyser(context, event) : context)),
|
||||||
hide: assign(({context}) => detachAllRemoteAnalysers(context, true)),
|
clear: assign(({context}) => detachAllRemoteAnalysers(context)),
|
||||||
show: assign(({context}) => showRemoteAnalysers(context)),
|
|
||||||
clear: assign(({context}) => detachAllRemoteAnalysers(context, false)),
|
|
||||||
clearCommands: assign(({context}) => ({...context, commands: EMPTY_REMOTE_COMMANDS})),
|
clearCommands: assign(({context}) => ({...context, commands: EMPTY_REMOTE_COMMANDS})),
|
||||||
},
|
},
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
@@ -257,8 +238,6 @@ const voiceRemoteSpeakingStateMachine = setup({
|
|||||||
'remote.attach': {actions: 'attach'},
|
'remote.attach': {actions: 'attach'},
|
||||||
'remote.detach': {actions: 'detach'},
|
'remote.detach': {actions: 'detach'},
|
||||||
'remote.tick': {actions: 'tick'},
|
'remote.tick': {actions: 'tick'},
|
||||||
'remote.visibilityHidden': {actions: 'hide'},
|
|
||||||
'remote.visibilityVisible': {actions: 'show'},
|
|
||||||
'remote.clear': {actions: 'clear'},
|
'remote.clear': {actions: 'clear'},
|
||||||
'remote.clearCommands': {actions: 'clearCommands'},
|
'remote.clearCommands': {actions: 'clearCommands'},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,8 +64,7 @@ class VoiceEngineV2AppRemoteSpeakingAdapter {
|
|||||||
private audioContext: AudioContext | null = null;
|
private audioContext: AudioContext | null = null;
|
||||||
private audioContextIsShared = false;
|
private audioContextIsShared = false;
|
||||||
private analysers = new Map<string, AnalyserHandle>();
|
private analysers = new Map<string, AnalyserHandle>();
|
||||||
private installedVisibilityListener = false;
|
private hydratedRoom: Room | null = null;
|
||||||
private roomForRehydrate: Room | null = null;
|
|
||||||
private machineSnapshot: VoiceRemoteSpeakingSnapshot = createVoiceRemoteSpeakingSnapshot();
|
private machineSnapshot: VoiceRemoteSpeakingSnapshot = createVoiceRemoteSpeakingSnapshot();
|
||||||
private autoScheduleTimerId: number | null = null;
|
private autoScheduleTimerId: number | null = null;
|
||||||
private readonly autoSchedule: boolean;
|
private readonly autoSchedule: boolean;
|
||||||
@@ -96,9 +95,6 @@ class VoiceEngineV2AppRemoteSpeakingAdapter {
|
|||||||
clearRemoteVoicePlaybackBoost(command.identity);
|
clearRemoteVoicePlaybackBoost(command.identity);
|
||||||
this.reapplyParticipantVolume(command.identity);
|
this.reapplyParticipantVolume(command.identity);
|
||||||
break;
|
break;
|
||||||
case 'rehydrateRemoteAnalysers':
|
|
||||||
if (this.roomForRehydrate) this.hydrateFromRoom(this.roomForRehydrate);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,22 +108,6 @@ class VoiceEngineV2AppRemoteSpeakingAdapter {
|
|||||||
this.stopAutoSchedule();
|
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 {
|
private getAudioContext(): AudioContext | null {
|
||||||
if (typeof window === 'undefined') return null;
|
if (typeof window === 'undefined') return null;
|
||||||
if (this.audioContext && this.audioContext.state !== 'closed') {
|
if (this.audioContext && this.audioContext.state !== 'closed') {
|
||||||
@@ -166,8 +146,6 @@ class VoiceEngineV2AppRemoteSpeakingAdapter {
|
|||||||
this.analysers.size <= REMOTE_SPEAKING_ANALYSER_HANDLES_CAP,
|
this.analysers.size <= REMOTE_SPEAKING_ANALYSER_HANDLES_CAP,
|
||||||
'remote speaking analyser registry exceeded cap',
|
'remote speaking analyser registry exceeded cap',
|
||||||
);
|
);
|
||||||
this.ensureVisibilityListener();
|
|
||||||
if (this.machineSnapshot.context.analyserSuspendedByVisibility) return;
|
|
||||||
if (participant.isLocal) return;
|
if (participant.isLocal) return;
|
||||||
if (publication.source !== VoiceTrackSource.Microphone) return;
|
if (publication.source !== VoiceTrackSource.Microphone) return;
|
||||||
if (track.kind !== VoiceTrackKind.Audio) return;
|
if (track.kind !== VoiceTrackKind.Audio) return;
|
||||||
@@ -249,12 +227,12 @@ class VoiceEngineV2AppRemoteSpeakingAdapter {
|
|||||||
);
|
);
|
||||||
this.detachAllAdapterAnalysers();
|
this.detachAllAdapterAnalysers();
|
||||||
this.applyCommands(this.transition({type: 'remote.clear'}));
|
this.applyCommands(this.transition({type: 'remote.clear'}));
|
||||||
this.roomForRehydrate = null;
|
this.hydratedRoom = null;
|
||||||
assert.equal(this.analysers.size, 0, 'clear post-condition: no analysers');
|
assert.equal(this.analysers.size, 0, 'clear post-condition: no analysers');
|
||||||
}
|
}
|
||||||
|
|
||||||
private reapplyParticipantVolume(identity: string): void {
|
private reapplyParticipantVolume(identity: string): void {
|
||||||
const room = getRoomFromMediaEngine() ?? this.roomForRehydrate;
|
const room = getRoomFromMediaEngine() ?? this.hydratedRoom;
|
||||||
const participant = room?.remoteParticipants.get(identity);
|
const participant = room?.remoteParticipants.get(identity);
|
||||||
if (!participant) return;
|
if (!participant) return;
|
||||||
try {
|
try {
|
||||||
@@ -267,9 +245,7 @@ class VoiceEngineV2AppRemoteSpeakingAdapter {
|
|||||||
hydrateFromRoom(room: Room): void {
|
hydrateFromRoom(room: Room): void {
|
||||||
assertObjectLike<Room>(room, 'hydrateFromRoom.room');
|
assertObjectLike<Room>(room, 'hydrateFromRoom.room');
|
||||||
assert.ok(room.remoteParticipants instanceof Map, 'hydrateFromRoom.room.remoteParticipants must be a Map');
|
assert.ok(room.remoteParticipants instanceof Map, 'hydrateFromRoom.room.remoteParticipants must be a Map');
|
||||||
this.ensureVisibilityListener();
|
this.hydratedRoom = room;
|
||||||
this.roomForRehydrate = room;
|
|
||||||
if (this.machineSnapshot.context.analyserSuspendedByVisibility) return;
|
|
||||||
room.remoteParticipants.forEach((participant) => {
|
room.remoteParticipants.forEach((participant) => {
|
||||||
participant.audioTrackPublications.forEach((publication) => {
|
participant.audioTrackPublications.forEach((publication) => {
|
||||||
const remotePub = publication as RemoteTrackPublication;
|
const remotePub = publication as RemoteTrackPublication;
|
||||||
@@ -287,7 +263,6 @@ class VoiceEngineV2AppRemoteSpeakingAdapter {
|
|||||||
this.analysers.size <= REMOTE_SPEAKING_ANALYSER_HANDLES_CAP,
|
this.analysers.size <= REMOTE_SPEAKING_ANALYSER_HANDLES_CAP,
|
||||||
'tickHost pre-condition: analyser cap respected',
|
'tickHost pre-condition: analyser cap respected',
|
||||||
);
|
);
|
||||||
if (this.machineSnapshot.context.analyserSuspendedByVisibility) return;
|
|
||||||
const sizeBefore = this.analysers.size;
|
const sizeBefore = this.analysers.size;
|
||||||
const expired: Array<AnalyserHandle> = [];
|
const expired: Array<AnalyserHandle> = [];
|
||||||
for (const handle of this.analysers.values()) {
|
for (const handle of this.analysers.values()) {
|
||||||
|
|||||||
Reference in new issue
Block a user