Arrange Act Assert

Jag Reehals thinking on things, mostly product development

How to fix: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission

05 Jan 2021

It's a good thing browsers don't allow sites to autoplay audio or videos without user interaction.

However, on iOS in particular, you might encounter the error

The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.

when there is an async operation between the user interaction and playing of audio.

The good news is there's a workaround for it.

Playing audio in web apps

Before a site can play audio, a user needs to have interacted with it for example, clicking on a play button.

The code for this is simple

const App = () => {
  const [audioError, setAudioError] = useState<Error>();
  const handleClick = () => {
    setAudioError(undefined);
    new Audio(
      'https://www.videomaker.com/sites/videomaker.com/files/downloads/free-sound-effects/Free_ExplosionSharp_6048_97_1.wav'
    )
      .play()
      .catch((e) => {
        setAudioError(e);
      });
  };

  return (
    <div>
      <p className="mb-4">Clicking play works everywhere!</p>
      <button
        className="border-2 border-green-700 bg-green-500 text-white rounded-md px-4 py-2 text-center"
        onClick={handleClick}
      >
        Play ▶
      </button>
      {audioError && <div className="mt-4 text-red-600">AUDIO ERROR: {audioError.message}</div>}
    </div>
  );
};

However, if in between clicking the button and playing audio, an async operation occurs then the behaviour of browsers differ.

const handleClick = async () => {
  setAudioError(undefined);
  await new Promise((r) => setTimeout(r, 2000));
  new Audio(
    'https://www.videomaker.com/sites/videomaker.com/files/downloads/free-sound-effects/Free_ExplosionSharp_6048_97_1.wav',
  )
    .play()
    .catch((e) => {
      setAudioError(e);
    });
};

For example, clicking on the 'play' button after a promise is resolved in Chrome plays the audio, whereas, in Safari, you'll see the error below:

The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.

Fixing the 'The request is not allowed' bug

The workaround is to load the audio in one interaction, and play the audio in another interaction. Now you can run as many promises as you want, the browser won't error.

The downside of this approach is you will have to change the UX of your web app.

import { useState } from 'react';

const App = () => {
  const [audioError, setAudioError] = useState<Error>();
  const [audio, setAudio] = useState<HTMLAudioElement>();

  // 1. load the audio in a user interaction
  const handleLoadClick = () => {
    const _audio = new Audio(
      'https://www.videomaker.com/sites/videomaker.com/files/downloads/free-sound-effects/Free_ExplosionSharp_6048_97_1.wav'
    );
    _audio.load();
    _audio.addEventListener('canplaythrough', () => {
      console.log('loaded audio');
      setAudio(_audio);
    });
  };

  // 2. now you can play the audio on all subsequent events
  const handleClick = async () => {
    setAudioError(undefined);
    await new Promise((r) => setTimeout(r, 2000));
    audio &&
      audio.play().catch((e) => {
        setAudioError(e);
      });
  };

  return (
    <div className="flex flex-col space-y-4">
      <p className="mb-4">
        The work around is to load the audio after a user interaction. Now you can play audio after
        async operations on iOS.
      </p>
      <button
        className="border-2 border-blue-700 bg-blue-500 text-white rounded-md px-4 py-2 text-center"
        onClick={handleLoadClick}
        disabled={audio !== undefined}
      >
        {audio ? '▼ Click the button below to play the audio' : 'Load Audio!'}
      </button>

      <button
        className={`border-2 border-green-700 bg-green-500 ${
          audio ? '' : 'cursor-not-allowed'
        } text-white rounded-md px-4 py-2 mt-4 text-center`}
        onClick={handleClick}
        disabled={audio === undefined}
      >
        {audio ? 'Play after resolving a promise ▶' : '▲ Click the button above to load audio'}
      </button>
      {audioError && <div className="mt-4 text-red-600">AUDIO ERROR: {audioError.message}</div>}
    </div>
  );
};

export default App;

You can try it for yourself here by loading the audio before clicking the play button

The full code for the examples above can be found here

ios mobile audio