mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-26 11:58:53 +00:00
🚸(frontend) mute participants by default when joining a large meeting
When joining a meeting that already has many participants, new participants are now muted by default. This is an empirical change, not directly requested by users but informed by user experience: a lot of people joining large meetings arrive with their microphone and/or camera open, which can be painful for the host, who otherwise has to mute everyone or ask everyone to mute. Many of these participants are inattentive but still noisy. If you are joining a room that is already at a certain size, you are probably not the one expected to speak; presenters typically join among the very first participants.
This commit is contained in:
committed by
aleb_the_flash
parent
00e197b216
commit
040df0e15a
@@ -17,6 +17,7 @@ and this project adheres to
|
||||
|
||||
- ✨(frontend) enhance noise reduction with BBBA audio processing pipeline
|
||||
- 🚸(frontend) mute join notification sound in larger rooms
|
||||
- 🚸(frontend) mute participants by default when joining a large meeting
|
||||
|
||||
## [1.20.0] - 2026-06-12
|
||||
|
||||
|
||||
@@ -413,7 +413,10 @@ class Base(Configuration):
|
||||
),
|
||||
"max_participants_for_sound": values.PositiveIntegerValue(
|
||||
5, environ_name="FRONTEND_MAX_PARTICIPANTS_FOR_SOUND", environ_prefix=None
|
||||
)
|
||||
),
|
||||
"auto_mute_on_join_threshold": values.PositiveIntegerValue(
|
||||
50, environ_name="FRONTEND_AUTO_MUTE_ON_JOIN_THRESHOLD", environ_prefix=None
|
||||
),
|
||||
}
|
||||
|
||||
# Mail
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface ApiConfig {
|
||||
}
|
||||
transcription_destination?: string
|
||||
max_participants_for_sound: number
|
||||
auto_mute_on_join_threshold: number
|
||||
}
|
||||
|
||||
const fetchConfig = (): Promise<ApiConfig> => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum NotificationType {
|
||||
AutoMuteLargeRoom = 'autoMuteLargeRoom',
|
||||
ParticipantJoined = 'participantJoined',
|
||||
HandRaised = 'handRaised',
|
||||
ParticipantMuted = 'participantMuted',
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useToast } from '@react-aria/toast'
|
||||
import { useRef } from 'react'
|
||||
|
||||
import { type ToastProps } from './Toast'
|
||||
import { VStack } from '@/styled-system/jsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/primitives'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { StyledToastContainer } from './StyledToastContainer'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
|
||||
export function ToastAutoMuteLargeRoom({
|
||||
state,
|
||||
...props
|
||||
}: Readonly<ToastProps>) {
|
||||
const room = useRoomContext()
|
||||
const { t } = useTranslation('notifications', {
|
||||
keyPrefix: 'autoMuteLargeRoom',
|
||||
})
|
||||
const ref = useRef(null)
|
||||
const { toastProps, contentProps } = useToast(props, state, ref)
|
||||
const toast = props.toast
|
||||
|
||||
const handleDismiss = async () => {
|
||||
room.localParticipant
|
||||
.setMicrophoneEnabled(true)
|
||||
.finally(() => state.close(toast.key))
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledToastContainer {...toastProps} ref={ref}>
|
||||
<VStack
|
||||
justify="start"
|
||||
alignItems="self-start"
|
||||
{...contentProps}
|
||||
maxWidth="370px"
|
||||
gap="0.75rem"
|
||||
padding={14}
|
||||
>
|
||||
<p>{t('auto')}</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="text"
|
||||
className={css({
|
||||
color: 'primary.300',
|
||||
})}
|
||||
onPress={() => handleDismiss()}
|
||||
>
|
||||
{t('dismiss')}
|
||||
</Button>
|
||||
</VStack>
|
||||
</StyledToastContainer>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { ToastAnyRecording } from './ToastAnyRecording'
|
||||
import { ToastRecordingSaving } from './ToastRecordingSaving'
|
||||
import { ToastPermissionsRemoved } from './ToastPermissionsRemoved'
|
||||
import { ToastRecordingRequest } from './ToastRecordingRequest'
|
||||
import { ToastAutoMuteLargeRoom } from './ToastAutoMuteLargeRoom'
|
||||
|
||||
interface ToastRegionProps extends AriaToastRegionProps {
|
||||
state: ToastState<ToastData>
|
||||
@@ -45,6 +46,11 @@ const renderToast = (
|
||||
case NotificationType.LowerHand:
|
||||
return <ToastLowerHand key={toast.key} toast={toast} state={state} />
|
||||
|
||||
case NotificationType.AutoMuteLargeRoom:
|
||||
return (
|
||||
<ToastAutoMuteLargeRoom key={toast.key} toast={toast} state={state} />
|
||||
)
|
||||
|
||||
case NotificationType.TranscriptionStarted:
|
||||
case NotificationType.TranscriptionStopped:
|
||||
case NotificationType.TranscriptionLimitReached:
|
||||
|
||||
@@ -5,6 +5,15 @@ import type { Participant } from 'livekit-client'
|
||||
import type { NotificationPayload } from './NotificationPayload'
|
||||
import type { RecordingMode } from '@/features/recording'
|
||||
|
||||
export const notifyAutoMutedOnJoin = () => {
|
||||
toastQueue.add(
|
||||
{
|
||||
type: NotificationType.AutoMuteLargeRoom,
|
||||
},
|
||||
{ timeout: NotificationDuration.ALERT }
|
||||
)
|
||||
}
|
||||
|
||||
export const showLowerHandToast = (
|
||||
participant: Participant,
|
||||
onClose: () => void
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@@ -33,6 +33,9 @@ import { useIsMobile } from '@/utils/useIsMobile'
|
||||
import { navigateTo } from '@/navigation/navigateTo'
|
||||
import { connectionObserverStore } from '@/stores/connectionObserver'
|
||||
import { PictureInPictureConference } from '@/features/pip/components/PictureInPictureConference'
|
||||
import { notifyAutoMutedOnJoin } from '@/features/notifications/utils'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { userPreferencesStore } from '@/stores/userPreferences'
|
||||
|
||||
export const Conference = ({
|
||||
roomId,
|
||||
@@ -57,6 +60,8 @@ export const Conference = ({
|
||||
|
||||
const [isConnectionWarmedUp, setIsConnectionWarmedUp] = useState(false)
|
||||
|
||||
const userPreferencesSnap = useSnapshot(userPreferencesStore)
|
||||
|
||||
const {
|
||||
mutateAsync: createRoom,
|
||||
status: createStatus,
|
||||
@@ -173,6 +178,8 @@ export const Conference = ({
|
||||
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const hasAutoMutedRef = useRef(false)
|
||||
|
||||
/*
|
||||
* Ensure stable WebSocket connection URL. This is critical for legacy browser compatibility
|
||||
* (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs in WebSocket() constructor
|
||||
@@ -228,6 +235,19 @@ export const Conference = ({
|
||||
onError={(e) => {
|
||||
posthog.captureException(e)
|
||||
}}
|
||||
onConnected={async () => {
|
||||
if (!apiConfig) return
|
||||
if (
|
||||
userPreferencesSnap.is_auto_mute_large_room_enabled &&
|
||||
!hasAutoMutedRef.current &&
|
||||
userConfig.audioEnabled &&
|
||||
room.numParticipants > apiConfig.auto_mute_on_join_threshold
|
||||
) {
|
||||
hasAutoMutedRef.current = true
|
||||
await room.localParticipant.setMicrophoneEnabled(false)
|
||||
notifyAutoMutedOnJoin()
|
||||
}
|
||||
}}
|
||||
onDisconnected={(e) => {
|
||||
const metadata = {
|
||||
room_id: roomId,
|
||||
|
||||
@@ -39,6 +39,19 @@ export const GeneralTab = ({ id }: GeneralTabProps) => {
|
||||
fullWidth: true,
|
||||
}}
|
||||
/>
|
||||
<Field
|
||||
type="switch"
|
||||
label={t('preferences.autoMuteLargeRoom.label')}
|
||||
description={t('preferences.autoMuteLargeRoom.description')}
|
||||
isSelected={userPreferencesSnap.is_auto_mute_large_room_enabled}
|
||||
onChange={(value) =>
|
||||
(userPreferencesStore.is_auto_mute_large_room_enabled = value)
|
||||
}
|
||||
wrapperProps={{
|
||||
noMargin: true,
|
||||
fullWidth: true,
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"auto": "Du hast begonnen zu sprechen – deine Hand wird daher automatisch gesenkt.",
|
||||
"dismiss": "Hand oben lassen"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"auto": "Sie haben an einer großen Besprechung teilgenommen. Ihr Mikrofon wurde automatisch stummgeschaltet.",
|
||||
"dismiss": "Stummschaltung aufheben"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} hat mit {{emoji}} reagiert"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"idleDisconnectModal": {
|
||||
"label": "Meetings ohne Teilnehmende verlassen",
|
||||
"description": "Meetings nach einigen Minuten automatisch verlassen, wenn keine andere Person beitritt"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"label": "Mikrofon automatisch stummschalten",
|
||||
"description": "Dein Mikrofon wird automatisch stummgeschaltet, wenn du an einer großen Besprechung teilnimmst."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"auto": "It seems you have started speaking, so your hand will be lowered.",
|
||||
"dismiss": "Keep hand raised"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"auto": "You have joined a large meeting. Your microphone has been automatically muted.",
|
||||
"dismiss": "Unmute"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} reacted with {{emoji}}"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"idleDisconnectModal": {
|
||||
"label": "Leave calls with no participants",
|
||||
"description": "Automatically leaves a call after a few minutes if no other participant joins"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"label": "Automatically mute my microphone",
|
||||
"description": "Automatically mute your microphone when joining a large meeting."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"auto": "Il semblerait que vous ayez pris la parole, donc la main va être baissée.",
|
||||
"dismiss": "Laisser la main levée"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"auto": "Vous venez de rejoindre une grande réunion. Votre micro a été automatiquement coupé.",
|
||||
"dismiss": "Rétablir le micro"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} a reagi avec {{emoji}}"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"idleDisconnectModal": {
|
||||
"label": "Quitter les appels sans autre participant",
|
||||
"description": "Vous fait quitter un appel au bout de quelques minutes si aucun autre participant ne vous rejoint"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"label": "Couper automatiquement mon micro",
|
||||
"description": "Coupe automatiquement votre micro lorsque vous rejoignez une réunion de grande taille."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"auto": "Het lijkt erop dat u bent begonnen te spreken, dus we laten uw hand zakken.",
|
||||
"dismiss": "Houdt uw hand opgestoken"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"auto": "U bent zojuist lid geworden van een grote vergadering. Uw microfoon is automatisch gedempt.",
|
||||
"dismiss": "Microfoon inschakelen"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} reageerde met {{emoji}}"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"idleDisconnectModal": {
|
||||
"label": "Verlaat oproepen zonder deelnemers",
|
||||
"description": "Verlaat automatisch een oproep na een paar minuten als geen andere deelnemer meedoet"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"label": "Mijn microfoon automatisch dempen",
|
||||
"description": "Uw microfoon wordt automatisch gedempt wanneer u deelneemt aan een grote vergadering."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
|
||||
@@ -3,10 +3,12 @@ import { STORAGE_KEYS } from '@/utils/storageKeys'
|
||||
|
||||
type State = {
|
||||
is_idle_disconnect_modal_enabled: boolean
|
||||
is_auto_mute_large_room_enabled: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
is_idle_disconnect_modal_enabled: true,
|
||||
is_auto_mute_large_room_enabled: true,
|
||||
}
|
||||
|
||||
function getUserPreferencesState(): State {
|
||||
|
||||
Reference in New Issue
Block a user