Search code examples
permissionsspeech-recognitionmicrophoneuser-permissionsgetusermedia

PermissionStatus API: Safari appears to support the change event but nothing fires when user allows microphone


When the native dialog box shows, we want to react to user's choice about allowing or not allowing the app to access the device microphone.

That is,

  • do something if he|she taps [ALLOW]
  • do anohter thing if he|she taps [DO NOT ALLOW]

According to caniuse Safari appears as if it has been supporting this since v16.4.

Yet as of v17.6 nothing happens when user clicks/touches any of the buttons even though everything seems to be correctly set up with the callback function successfully attached either with .addEventListener or .onchange


Note that the same code runs nicely without any issues on Windows and Android.


Solution

  • There are two ways to trigger the native dialog box to ask for permissions about reading microphone input.

    1. Start a speech recognition session
    2. Start a getUserMedia session so that the app is ready to record audio

    Either way you can read and save the initial permission state and then start a setInterval(checkFunction,500); to read the latter state and compare them using a logic like if(previous==current) or perhaps if(previous!=current) then use clearInterval() as soon as change is detected and finally write the code to gracefully react to user's choice to offer the best UX.


    Note that it is also possible to achieve the desired result without a setInterval check. But you need to employ one of the so called browser-sniffing methods to detect if the user is using an Apple device. Then handle the situation like so,

    if (navigator.mediaDevices){
      if (navigator.mediaDevices.getUserMedia){
        console.log('Attempting to turn on the mic via getUserMedia,,, so that the permission dialog is triggered');
        navigator.mediaDevices.getUserMedia({ audio: true })  // Make the prompt show
        .then(function (stream) {
          console.log('Dialog triggering seems to be successful'); // On Windows, this line fires after change event
          // Here we assume that for Windows and Android the change event is already set up and good to go.
          if (isApple) { // You must create your own global isApple boolean const using something like ua-parser-js
            if ("permissions" in navigator) {
              const micPermissionPromiseApple = navigator.permissions.query({name:'microphone'});
              micPermissionPromiseApple.then(function(result) {
                console.log('In this Apple device, mic permission state is now set to '+result.state);
                // HANDLE if it is granted or denied
              });
            }
          }
        }) // End of then() block
        .catch(function (error) {
          console.error('Error accessing the microphone:', error);
        });
      }
    }
    

    Finally here is a thenable function to TURN OFF the mic, in case you need. Remember to paste it inside the scope of stream, not outside!

    function turnOFFgetUserMediaMic() {
      return new Promise((resolve, reject) => {
        try {
          console.log('Attempting to turn off the mic');
          const tracks = stream.getTracks();
          tracks.forEach(track => track.stop());
          stream = null; // Release the stream
          console.log('Mic should be turned off now');
          resolve(); // Resolve the promise when the mic is turned off
        } catch (error) {
          reject(error); // Reject the promise if an error occurs
        }
      });
    }