@@ -134,7 +134,15 @@ interface IProps extends AbstractDialogTabProps, WithTranslation {
134134 */
135135 selectedAudioOutputId : string ;
136136}
137-
137+ /**
138+ * The type of the React {@code Component} state of {@link AudioDeviceSelection}.
139+ */
140+ interface IState {
141+ /**
142+ * Whether microphone permission is granted locally.
143+ */
144+ localHasAudioPermission : boolean | null ;
145+ }
138146
139147const styles = ( theme : Theme ) => {
140148 return {
@@ -182,7 +190,7 @@ const styles = (theme: Theme) => {
182190 *
183191 * @augments Component
184192 */
185- class AudioDevicesSelection extends AbstractDialogTab < IProps , { } > {
193+ class AudioDevicesSelection extends AbstractDialogTab < IProps , IState > {
186194
187195 /**
188196 * Whether current component is mounted or not.
@@ -195,6 +203,15 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
195203 */
196204 _unMounted : boolean ;
197205
206+ /**
207+ * Stores the current microphone permission status.
208+ *
209+ * This is used to track whether audio permissions are granted, denied,
210+ * or still unknown, so that the component can react correctly when the
211+ * permission state changes.
212+ */
213+ _permissionStatus : PermissionStatus | null = null ;
214+
198215 /**
199216 * Initializes a new DeviceSelection instance.
200217 *
@@ -204,7 +221,13 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
204221 constructor ( props : IProps ) {
205222 super ( props ) ;
206223
224+ this . state = {
225+ localHasAudioPermission : null
226+ }
227+
207228 this . _unMounted = true ;
229+ this . _onPermissionChange = this . _onPermissionChange . bind ( this ) ;
230+
208231 }
209232
210233 /**
@@ -214,6 +237,9 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
214237 */
215238 override componentDidMount ( ) {
216239 this . _unMounted = false ;
240+
241+ this . _setupPermissionListener ( ) ;
242+
217243 Promise . all ( [
218244 this . _createAudioInputTrack ( this . props . selectedAudioInputId )
219245 ] )
@@ -245,6 +271,48 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
245271 override componentWillUnmount ( ) {
246272 this . _unMounted = true ;
247273 disposeTrack ( this . props . previewAudioTrack ) ;
274+
275+ if ( this . _permissionStatus ) {
276+ this . _permissionStatus . removeEventListener ( 'change' , this . _onPermissionChange ) ;
277+ }
278+ }
279+
280+ /**
281+ * Sets up listener for microphone permission changes.
282+ */
283+ async _setupPermissionListener ( ) {
284+ try {
285+ this . _permissionStatus = await navigator . permissions . query ( {
286+ name : 'microphone' as PermissionName
287+ } ) ;
288+
289+ // This is the initial permission state
290+ this . setState ( {
291+ localHasAudioPermission : this . _permissionStatus . state === 'granted'
292+ } ) ;
293+
294+ this . _permissionStatus . addEventListener ( 'change' , this . _onPermissionChange ) ;
295+ } catch ( error ) {
296+ console . warn ( ' Permissions API not supported for AudioDeviceSelection ' , error ) ;
297+ }
298+ }
299+
300+
301+ /**
302+ * Called when microphone permission changes.
303+ */
304+ _onPermissionChange ( ) {
305+ if ( this . _unMounted || ! this . _permissionStatus ) {
306+ return ;
307+ }
308+
309+ const isGranted = this . _permissionStatus . state === 'granted' ;
310+ this . setState ( { localHasAudioPermission : isGranted } ) ;
311+
312+ if ( isGranted ) {
313+ this . _createAudioInputTrack ( this . props . selectedAudioInputId ) ;
314+ this . props . dispatch ( getAvailableDevices ( ) ) ;
315+ }
248316 }
249317
250318 /**
@@ -310,6 +378,11 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
310378 t
311379 } = this . props ;
312380 const { audioInput, audioOutput } = this . _getSelectors ( ) ;
381+ const { localHasAudioPermission } = this . state ;
382+
383+ const effectivePermission = localHasAudioPermission !== null
384+ ? localHasAudioPermission
385+ : hasAudioPermission ;
313386
314387 const classes = withStyles . getClasses ( this . props ) ;
315388
@@ -328,7 +401,7 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
328401 { this . _renderSelector ( audioInput ) }
329402 </ div > }
330403
331- { ! hideAudioInputPreview && hasAudioPermission && ! iAmVisitor
404+ { ! hideAudioInputPreview && effectivePermission && ! iAmVisitor
332405 && < AudioInputPreview
333406 track = { this . props . previewAudioTrack } /> }
334407
@@ -385,7 +458,7 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
385458 aria-live = 'polite'
386459 className = { classes . outputContainer } >
387460 { this . _renderSelector ( audioOutput ) }
388- { ! hideAudioOutputPreview && hasAudioPermission
461+ { ! hideAudioOutputPreview && effectivePermission
389462 && < AudioOutputPreview
390463 className = { classes . outputButton }
391464 deviceId = { selectedAudioOutputId } /> }
@@ -450,10 +523,15 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
450523 */
451524 _getSelectors ( ) {
452525 const { availableDevices, hasAudioPermission } = this . props ;
526+ const { localHasAudioPermission } = this . state ;
527+
528+ const effectivePermission = localHasAudioPermission !== null
529+ ? localHasAudioPermission
530+ : hasAudioPermission ;
453531
454532 const audioInput = {
455533 devices : availableDevices . audioInput ,
456- hasPermission : hasAudioPermission ,
534+ hasPermission : effectivePermission ,
457535 icon : 'icon-microphone' ,
458536 isDisabled : this . props . disableAudioInputChange || this . props . disableDeviceChange ,
459537 key : 'audioInput' ,
@@ -468,7 +546,7 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
468546 if ( ! this . props . hideAudioOutputSelect ) {
469547 audioOutput = {
470548 devices : availableDevices . audioOutput ,
471- hasPermission : hasAudioPermission ,
549+ hasPermission : effectivePermission ,
472550 icon : 'icon-speaker' ,
473551 isDisabled : this . props . disableDeviceChange ,
474552 key : 'audioOutput' ,
0 commit comments