mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(voice): keep mic publish state in sync with voice state (#3088)
This commit is contained in:
1 parent
e3fcedbec5
commit
376afd2ad6
7 files changed
+264
-7
No files matched your search
@@ -38,6 +38,16 @@ interface DisconnectParticipantParams {
|
|||||||
serverId: string;
|
serverId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MuteMicrophoneTrackParams {
|
||||||
|
userId: UserID;
|
||||||
|
guildId?: GuildID;
|
||||||
|
channelId: ChannelID;
|
||||||
|
connectionId: string;
|
||||||
|
regionId: string;
|
||||||
|
serverId: string;
|
||||||
|
trackSid: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface UpdateParticipantPermissionsParams {
|
interface UpdateParticipantPermissionsParams {
|
||||||
userId: UserID;
|
userId: UserID;
|
||||||
guildId?: GuildID;
|
guildId?: GuildID;
|
||||||
@@ -63,6 +73,8 @@ export class DisabledLiveKitService implements ILiveKitService {
|
|||||||
|
|
||||||
async updateParticipantPermissions(_params: UpdateParticipantPermissionsParams): Promise<void> {}
|
async updateParticipantPermissions(_params: UpdateParticipantPermissionsParams): Promise<void> {}
|
||||||
|
|
||||||
|
async muteMicrophoneTrack(_params: MuteMicrophoneTrackParams): Promise<void> {}
|
||||||
|
|
||||||
async disconnectParticipant(_params: DisconnectParticipantParams): Promise<void> {}
|
async disconnectParticipant(_params: DisconnectParticipantParams): Promise<void> {}
|
||||||
|
|
||||||
async listParticipants(_params: {
|
async listParticipants(_params: {
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ interface UpdateParticipantParams {
|
|||||||
canVideo?: boolean;
|
canVideo?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MuteMicrophoneTrackParams {
|
||||||
|
userId: UserID;
|
||||||
|
guildId?: GuildID;
|
||||||
|
channelId: ChannelID;
|
||||||
|
connectionId: string;
|
||||||
|
regionId: string;
|
||||||
|
serverId: string;
|
||||||
|
trackSid: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface UpdateParticipantPermissionsParams {
|
interface UpdateParticipantPermissionsParams {
|
||||||
userId: UserID;
|
userId: UserID;
|
||||||
guildId?: GuildID;
|
guildId?: GuildID;
|
||||||
@@ -87,6 +97,8 @@ export abstract class ILiveKitService {
|
|||||||
|
|
||||||
abstract updateParticipantPermissions(params: UpdateParticipantPermissionsParams): Promise<void>;
|
abstract updateParticipantPermissions(params: UpdateParticipantPermissionsParams): Promise<void>;
|
||||||
|
|
||||||
|
abstract muteMicrophoneTrack(params: MuteMicrophoneTrackParams): Promise<void>;
|
||||||
|
|
||||||
abstract disconnectParticipant(params: DisconnectParticipantParams): Promise<void>;
|
abstract disconnectParticipant(params: DisconnectParticipantParams): Promise<void>;
|
||||||
|
|
||||||
abstract listParticipants(params: ListParticipantsParams): Promise<ListParticipantsResult>;
|
abstract listParticipants(params: ListParticipantsParams): Promise<ListParticipantsResult>;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {ILiveKitService} from '@app/api/infrastructure/ILiveKitService';
|
|||||||
import {Logger} from '@app/api/Logger';
|
import {Logger} from '@app/api/Logger';
|
||||||
import type {VoiceRegionMetadata, VoiceServerRecord} from '@app/api/voice/VoiceModel';
|
import type {VoiceRegionMetadata, VoiceServerRecord} from '@app/api/voice/VoiceModel';
|
||||||
import type {VoiceTopology} from '@app/api/voice/VoiceTopology';
|
import type {VoiceTopology} from '@app/api/voice/VoiceTopology';
|
||||||
import {AccessToken, RoomServiceClient, TrackSource} from 'livekit-server-sdk';
|
import {AccessToken, RoomServiceClient, type TrackInfo, TrackSource} from 'livekit-server-sdk';
|
||||||
|
|
||||||
interface CreateTokenParams {
|
interface CreateTokenParams {
|
||||||
userId: UserID;
|
userId: UserID;
|
||||||
@@ -47,6 +47,16 @@ interface DisconnectParticipantParams {
|
|||||||
serverId: string;
|
serverId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MuteMicrophoneTrackParams {
|
||||||
|
userId: UserID;
|
||||||
|
guildId?: GuildID;
|
||||||
|
channelId: ChannelID;
|
||||||
|
connectionId: string;
|
||||||
|
regionId: string;
|
||||||
|
serverId: string;
|
||||||
|
trackSid: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface UpdateParticipantPermissionsParams {
|
interface UpdateParticipantPermissionsParams {
|
||||||
userId: UserID;
|
userId: UserID;
|
||||||
guildId?: GuildID;
|
guildId?: GuildID;
|
||||||
@@ -76,6 +86,8 @@ interface LiveKitPublishPermissions {
|
|||||||
|
|
||||||
export const VOICE_TOKEN_TTL_SECONDS = 60 * 10;
|
export const VOICE_TOKEN_TTL_SECONDS = 60 * 10;
|
||||||
|
|
||||||
|
export const SERVER_MUTE_ATTRIBUTE = 'server_mute';
|
||||||
|
|
||||||
export function computeLiveKitPublishSources(permissions: LiveKitPublishPermissions): Array<TrackSource> {
|
export function computeLiveKitPublishSources(permissions: LiveKitPublishPermissions): Array<TrackSource> {
|
||||||
const sources: Array<TrackSource> = [];
|
const sources: Array<TrackSource> = [];
|
||||||
if (permissions.canSpeak) {
|
if (permissions.canSpeak) {
|
||||||
@@ -202,8 +214,10 @@ export class LiveKitService extends ILiveKitService {
|
|||||||
if (!participant) {
|
if (!participant) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mute !== undefined && participant.tracks) {
|
if (mute !== undefined) {
|
||||||
for (const track of participant.tracks) {
|
const tracks =
|
||||||
|
(await this.setServerMuteAttribute(server, roomName, participantIdentity, mute)) ?? participant.tracks;
|
||||||
|
for (const track of tracks) {
|
||||||
if (track.source === TrackSource.MICROPHONE && track.sid) {
|
if (track.source === TrackSource.MICROPHONE && track.sid) {
|
||||||
await server.roomServiceClient.mutePublishedTrack(roomName, participantIdentity, track.sid, mute);
|
await server.roomServiceClient.mutePublishedTrack(roomName, participantIdentity, track.sid, mute);
|
||||||
}
|
}
|
||||||
@@ -277,6 +291,39 @@ export class LiveKitService extends ILiveKitService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async setServerMuteAttribute(
|
||||||
|
server: ServerClientConfig,
|
||||||
|
roomName: string,
|
||||||
|
participantIdentity: string,
|
||||||
|
mute: boolean,
|
||||||
|
): Promise<Array<TrackInfo> | null> {
|
||||||
|
try {
|
||||||
|
const updated = await server.roomServiceClient.updateParticipant(roomName, participantIdentity, {
|
||||||
|
attributes: {[SERVER_MUTE_ATTRIBUTE]: mute ? 'true' : ''},
|
||||||
|
});
|
||||||
|
return updated.tracks;
|
||||||
|
} catch (error) {
|
||||||
|
Logger.warn({error, participantIdentity, roomName}, 'Failed to record server mute on LiveKit participant');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async muteMicrophoneTrack(params: MuteMicrophoneTrackParams): Promise<void> {
|
||||||
|
const {userId, guildId, channelId, connectionId, regionId, serverId, trackSid} = params;
|
||||||
|
const roomName = this.getRoomName(guildId, channelId);
|
||||||
|
const participantIdentity = this.getParticipantIdentity(userId, connectionId);
|
||||||
|
const server = this.tryResolveServerClient(regionId, serverId);
|
||||||
|
if (server === null) {
|
||||||
|
Logger.debug({regionId, serverId, participantIdentity, roomName}, 'LiveKit track mute skipped: unknown server');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await server.roomServiceClient.mutePublishedTrack(roomName, participantIdentity, trackSid, true);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error({error, participantIdentity, roomName, trackSid}, 'Error muting LiveKit microphone track');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async disconnectParticipant(params: DisconnectParticipantParams): Promise<void> {
|
async disconnectParticipant(params: DisconnectParticipantParams): Promise<void> {
|
||||||
const {userId, guildId, channelId, connectionId, regionId, serverId} = params;
|
const {userId, guildId, channelId, connectionId, regionId, serverId} = params;
|
||||||
const roomName = this.getRoomName(guildId, channelId);
|
const roomName = this.getRoomName(guildId, channelId);
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import type {ChannelID, GuildID} from '@app/api/BrandedTypes';
|
|||||||
import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
|
import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
|
||||||
import type {ILiveKitService} from '@app/api/infrastructure/ILiveKitService';
|
import type {ILiveKitService} from '@app/api/infrastructure/ILiveKitService';
|
||||||
import type {IVoiceRoomStore} from '@app/api/infrastructure/IVoiceRoomStore';
|
import type {IVoiceRoomStore} from '@app/api/infrastructure/IVoiceRoomStore';
|
||||||
|
import {SERVER_MUTE_ATTRIBUTE} from '@app/api/infrastructure/LiveKitService';
|
||||||
import {isDMRoom, parseParticipantMetadataWithRaw, parseRoomName} from '@app/api/infrastructure/VoiceRoomContext';
|
import {isDMRoom, parseParticipantMetadataWithRaw, parseRoomName} from '@app/api/infrastructure/VoiceRoomContext';
|
||||||
import {Logger} from '@app/api/Logger';
|
import {Logger} from '@app/api/Logger';
|
||||||
import type {VoiceTopology} from '@app/api/voice/VoiceTopology';
|
import type {VoiceTopology} from '@app/api/voice/VoiceTopology';
|
||||||
import type {WebhookEvent} from 'livekit-server-sdk';
|
import type {WebhookEvent} from 'livekit-server-sdk';
|
||||||
import {WebhookReceiver} from 'livekit-server-sdk';
|
import {TrackSource, WebhookReceiver} from 'livekit-server-sdk';
|
||||||
|
|
||||||
interface VoiceWebhookParticipantContext {
|
interface VoiceWebhookParticipantContext {
|
||||||
readonly type: 'dm' | 'guild';
|
readonly type: 'dm' | 'guild';
|
||||||
@@ -440,6 +441,45 @@ export class LiveKitWebhookService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleTrackPublished(event: WebhookEvent): Promise<void> {
|
||||||
|
if (event.event !== 'track_published') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {participant, track} = event;
|
||||||
|
if (!participant?.metadata || track?.source !== TrackSource.MICROPHONE || !track.sid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (participant.attributes[SERVER_MUTE_ATTRIBUTE] !== 'true') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parsed = parseParticipantMetadataWithRaw(participant.metadata);
|
||||||
|
if (!parsed) {
|
||||||
|
Logger.warn({metadata: participant.metadata}, 'Failed to parse participant metadata');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {context, raw} = parsed;
|
||||||
|
if (!raw.region_id || !raw.server_id) {
|
||||||
|
Logger.warn(
|
||||||
|
{participantIdentity: participant.identity, trackSid: track.sid},
|
||||||
|
'Cannot mute published microphone without a region and server',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Logger.debug(
|
||||||
|
{participantIdentity: participant.identity, trackSid: track.sid},
|
||||||
|
'Muting microphone published while server muted',
|
||||||
|
);
|
||||||
|
await this.liveKitService.muteMicrophoneTrack({
|
||||||
|
guildId: context.type === 'guild' ? context.guildId : undefined,
|
||||||
|
channelId: context.channelId,
|
||||||
|
userId: context.userId,
|
||||||
|
connectionId: context.connectionId,
|
||||||
|
regionId: raw.region_id,
|
||||||
|
serverId: raw.server_id,
|
||||||
|
trackSid: track.sid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async processEvent(data: {event: WebhookEvent; apiKey: string}): Promise<void> {
|
async processEvent(data: {event: WebhookEvent; apiKey: string}): Promise<void> {
|
||||||
const {event, apiKey} = data;
|
const {event, apiKey} = data;
|
||||||
Logger.debug({event: event.event, apiKey}, 'Dispatching LiveKit webhook event');
|
Logger.debug({event: event.event, apiKey}, 'Dispatching LiveKit webhook event');
|
||||||
@@ -454,6 +494,9 @@ export class LiveKitWebhookService {
|
|||||||
case 'room_finished':
|
case 'room_finished':
|
||||||
await this.handleRoomFinished(event, apiKey);
|
await this.handleRoomFinished(event, apiKey);
|
||||||
break;
|
break;
|
||||||
|
case 'track_published':
|
||||||
|
await this.handleTrackPublished(event);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Logger.debug({event: event.event}, 'Ignoring LiveKit webhook event');
|
Logger.debug({event: event.event}, 'Ignoring LiveKit webhook event');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import {createChannelID, createGuildID} from '@app/api/BrandedTypes';
|
import {createChannelID, createGuildID, createUserID} from '@app/api/BrandedTypes';
|
||||||
import {
|
import {
|
||||||
computeLiveKitPublishSources,
|
computeLiveKitPublishSources,
|
||||||
LiveKitService,
|
LiveKitService,
|
||||||
@@ -137,3 +137,62 @@ describe('LiveKitService listParticipants', () => {
|
|||||||
expect(result).toEqual({status: 'ok', participants: []});
|
expect(result).toEqual({status: 'ok', participants: []});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('LiveKitService updateParticipant mute', () => {
|
||||||
|
const params = {
|
||||||
|
userId: createUserID(3n),
|
||||||
|
guildId: createGuildID(1n),
|
||||||
|
channelId: createChannelID(2n),
|
||||||
|
connectionId: 'conn-1',
|
||||||
|
regionId: 'region-1',
|
||||||
|
serverId: 'region-1-server-1',
|
||||||
|
};
|
||||||
|
|
||||||
|
function roomServiceClient(
|
||||||
|
listedTracks: Array<{sid: string; source: TrackSource}>,
|
||||||
|
updatedTracks: typeof listedTracks,
|
||||||
|
) {
|
||||||
|
const calls: Array<Array<unknown>> = [];
|
||||||
|
const client = {
|
||||||
|
listParticipants: async () => [{identity: 'user_3_conn-1', tracks: listedTracks}],
|
||||||
|
updateParticipant: async (...args: Array<unknown>) => {
|
||||||
|
calls.push(['updateParticipant', ...args]);
|
||||||
|
return {identity: 'user_3_conn-1', tracks: updatedTracks};
|
||||||
|
},
|
||||||
|
mutePublishedTrack: async (...args: Array<unknown>) => {
|
||||||
|
calls.push(['mutePublishedTrack', ...args]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return {client, calls};
|
||||||
|
}
|
||||||
|
|
||||||
|
it('records the mute on the participant and mutes every current microphone track', async () => {
|
||||||
|
const {client, calls} = roomServiceClient(
|
||||||
|
[{sid: 'TR_old', source: TrackSource.MICROPHONE}],
|
||||||
|
[
|
||||||
|
{sid: 'TR_old', source: TrackSource.MICROPHONE},
|
||||||
|
{sid: 'TR_new', source: TrackSource.MICROPHONE},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
const service = createServiceWithRoomServiceClient(client);
|
||||||
|
|
||||||
|
await service.updateParticipant({...params, mute: true});
|
||||||
|
|
||||||
|
expect(calls).toEqual([
|
||||||
|
['updateParticipant', 'guild_1_channel_2', 'user_3_conn-1', {attributes: {server_mute: 'true'}}],
|
||||||
|
['mutePublishedTrack', 'guild_1_channel_2', 'user_3_conn-1', 'TR_old', true],
|
||||||
|
['mutePublishedTrack', 'guild_1_channel_2', 'user_3_conn-1', 'TR_new', true],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears the recorded mute on unmute', async () => {
|
||||||
|
const {client, calls} = roomServiceClient([], []);
|
||||||
|
const service = createServiceWithRoomServiceClient(client);
|
||||||
|
|
||||||
|
await service.updateParticipant({...params, mute: false});
|
||||||
|
|
||||||
|
expect(calls).toEqual([
|
||||||
|
['updateParticipant', 'guild_1_channel_2', 'user_3_conn-1', {attributes: {server_mute: ''}}],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,10 +4,11 @@ import {createChannelID, createGuildID} from '@app/api/BrandedTypes';
|
|||||||
import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
|
import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
|
||||||
import type {ILiveKitService} from '@app/api/infrastructure/ILiveKitService';
|
import type {ILiveKitService} from '@app/api/infrastructure/ILiveKitService';
|
||||||
import type {IVoiceRoomStore} from '@app/api/infrastructure/IVoiceRoomStore';
|
import type {IVoiceRoomStore} from '@app/api/infrastructure/IVoiceRoomStore';
|
||||||
|
import {SERVER_MUTE_ATTRIBUTE} from '@app/api/infrastructure/LiveKitService';
|
||||||
import {LiveKitWebhookService} from '@app/api/infrastructure/LiveKitWebhookService';
|
import {LiveKitWebhookService} from '@app/api/infrastructure/LiveKitWebhookService';
|
||||||
import type {IVoiceRepository} from '@app/api/voice/IVoiceRepository';
|
import type {IVoiceRepository} from '@app/api/voice/IVoiceRepository';
|
||||||
import {VoiceTopology} from '@app/api/voice/VoiceTopology';
|
import {VoiceTopology} from '@app/api/voice/VoiceTopology';
|
||||||
import type {WebhookEvent} from 'livekit-server-sdk';
|
import {TrackSource, type WebhookEvent} from 'livekit-server-sdk';
|
||||||
import {describe, expect, it, vi} from 'vitest';
|
import {describe, expect, it, vi} from 'vitest';
|
||||||
|
|
||||||
const GUILD_ID = createGuildID(1n);
|
const GUILD_ID = createGuildID(1n);
|
||||||
@@ -66,3 +67,76 @@ describe('LiveKitWebhookService room_finished', () => {
|
|||||||
expect(disconnectAllVoiceUsersInChannel).not.toHaveBeenCalled();
|
expect(disconnectAllVoiceUsersInChannel).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function trackPublished(source: TrackSource, attributes: Record<string, string>): WebhookEvent {
|
||||||
|
return {
|
||||||
|
event: 'track_published',
|
||||||
|
room: {name: `guild_${GUILD_ID}_channel_${CHANNEL_ID}`},
|
||||||
|
participant: {
|
||||||
|
identity: 'user_3_conn-1',
|
||||||
|
attributes,
|
||||||
|
metadata: JSON.stringify({
|
||||||
|
user_id: '3',
|
||||||
|
channel_id: CHANNEL_ID.toString(),
|
||||||
|
guild_id: GUILD_ID.toString(),
|
||||||
|
connection_id: 'conn-1',
|
||||||
|
region_id: 'eu',
|
||||||
|
server_id: 'eu-1',
|
||||||
|
token_nonce: 'nonce',
|
||||||
|
issued_at: '0',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
track: {sid: 'TR_new', source},
|
||||||
|
} as unknown as WebhookEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackHarness() {
|
||||||
|
const muteMicrophoneTrack = vi.fn(async () => {});
|
||||||
|
const service = new LiveKitWebhookService(
|
||||||
|
{} as unknown as IVoiceRoomStore,
|
||||||
|
{} as unknown as IGatewayService,
|
||||||
|
{muteMicrophoneTrack} as unknown as ILiveKitService,
|
||||||
|
new VoiceTopology({} as unknown as IVoiceRepository, null),
|
||||||
|
);
|
||||||
|
return {service, muteMicrophoneTrack};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('LiveKitWebhookService track_published', () => {
|
||||||
|
it('mutes a microphone published by a participant the guild has muted', async () => {
|
||||||
|
const {service, muteMicrophoneTrack} = trackHarness();
|
||||||
|
|
||||||
|
await service.processEvent({
|
||||||
|
event: trackPublished(TrackSource.MICROPHONE, {[SERVER_MUTE_ATTRIBUTE]: 'true'}),
|
||||||
|
apiKey: 'key',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(muteMicrophoneTrack).toHaveBeenCalledWith({
|
||||||
|
guildId: GUILD_ID,
|
||||||
|
channelId: CHANNEL_ID,
|
||||||
|
userId: 3n,
|
||||||
|
connectionId: 'conn-1',
|
||||||
|
regionId: 'eu',
|
||||||
|
serverId: 'eu-1',
|
||||||
|
trackSid: 'TR_new',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves a microphone alone when the participant is not muted by the guild', async () => {
|
||||||
|
const {service, muteMicrophoneTrack} = trackHarness();
|
||||||
|
|
||||||
|
await service.processEvent({event: trackPublished(TrackSource.MICROPHONE, {}), apiKey: 'key'});
|
||||||
|
|
||||||
|
expect(muteMicrophoneTrack).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves other track sources alone', async () => {
|
||||||
|
const {service, muteMicrophoneTrack} = trackHarness();
|
||||||
|
|
||||||
|
await service.processEvent({
|
||||||
|
event: trackPublished(TrackSource.CAMERA, {[SERVER_MUTE_ATTRIBUTE]: 'true'}),
|
||||||
|
apiKey: 'key',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(muteMicrophoneTrack).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -334,6 +334,12 @@ export class VoiceEngineV2AppMediaExecutionAdapter extends Store {
|
|||||||
return isVoiceSpeakPermissionDenied(connection?.guildId ?? null, channelId);
|
return isVoiceSpeakPermissionDenied(connection?.guildId ?? null, channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isMicrophoneHeldByVoiceState(channelId: string | null): boolean {
|
||||||
|
if (this.isSpeakPermissionDenied(channelId)) return false;
|
||||||
|
const audioState = this.getEffectiveAudioState();
|
||||||
|
return audioState.serverMute || audioState.serverDeaf;
|
||||||
|
}
|
||||||
|
|
||||||
private transitionMediaState(event: VoiceMediaEvent): void {
|
private transitionMediaState(event: VoiceMediaEvent): void {
|
||||||
this.update(() => {
|
this.update(() => {
|
||||||
const nextSnapshot = transitionVoiceMediaSnapshot(this.mediaStateSnapshot, event);
|
const nextSnapshot = transitionVoiceMediaSnapshot(this.mediaStateSnapshot, event);
|
||||||
@@ -563,7 +569,7 @@ export class VoiceEngineV2AppMediaExecutionAdapter extends Store {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setAudioPublicationsMuted(room, false, 'voice state update');
|
this.setAudioPublicationsMuted(room, this.shouldMuteMicrophonePublication(), 'voice state update');
|
||||||
this.syncLocalSpeakingOverride(room);
|
this.syncLocalSpeakingOverride(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -898,6 +904,10 @@ export class VoiceEngineV2AppMediaExecutionAdapter extends Store {
|
|||||||
channelId: string | null,
|
channelId: string | null,
|
||||||
options: VoiceEngineV2MicrophoneOptions,
|
options: VoiceEngineV2MicrophoneOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (this.isMicrophoneHeldByVoiceState(channelId)) {
|
||||||
|
logger.debug('Skipping microphone enable: muted or deafened by the server');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.transitionMediaState({
|
this.transitionMediaState({
|
||||||
type: 'microphone.enable.request',
|
type: 'microphone.enable.request',
|
||||||
hasPublication: this.hasMicrophonePublication(room),
|
hasPublication: this.hasMicrophonePublication(room),
|
||||||
|
|||||||
Reference in new issue
Block a user