Search code examples
javascripthtml5-audiomobile-website

Trying to play an .mp3 via JS on iOS browsers?


I tried to make a metronome using HTML and JS. I know the audio can't autoplay due to restrictions (I don't want it to anyway), so I placed an <audio ...> element with a <source ...> inside, with all (I thought?) appropriate attributes; I controlled playback using JS triggered by a button click. This works on my laptop and even in the XBox Edge browser, but on iOS browsers (both Safari and Firefox) the sound does not play. The HTML looks like this:

<button id="metronome-button">Start</button>
<audio id="metronome-audio" autoplay="0" autostart="0" loop="0">
  <source src="tick.mp3" type="audio/mpeg">
</audio>

And the JS looks like this:

const metronomeAudio = document.getElementById('metronome-audio');
const metronomeButton = document.getElementById('metronome-button');
let metronomeInterval = null;

metronomeButton.addEventListener('click', () => {    
    metronomeInterval = setInterval(() => {
      metronomeAudio.loop = false;
      metronomeAudio.pause();
      metronomeAudio.play();
    }, 500);
});

Since this didn't work, I did more looking and found this solution in another StackOverflow thread, which uses JS and no HTML at all (other than being triggered by a button's click event):

function startMetronome () {
  setInterval(() => {
    let audio = new Audio('tick.mp3');
    audio.loop = false;
    audio.play();
    audio.onended = () => {
      audio.remove();
    }
  }, 500);  
}  

Again, this works on PC in various browsers, why does this fail specifically on iOS? (Have not tested on Android, don't have device.)


Solution

  • I am not sure if this is documented anywhere (I didn't stumble across it), but after some additional testing I discovered that iOS prohibits use of the .play() function on an existing HTML <audio> element or JS Audio() element, unless called directly and synchronously as the result of a user UI event. In other words, this will work:

    const button = document.getElementById('my-button')
    const audio = document.getElementById('my-audio')
    button.addEventListener('click', () => {
        audio.play()
    })
    

    But this will not work:

    const button = document.getElementById('my-button')
    const audio = document.getElementById('my-audio')
    button.addEventListener('click', () => {
        setTimeout(() => { audio.play() }, 1000)
    })
    

    Thus, having the .play() call in an async callback breaks playback on iOS.

    However, as mentioned in a comment on this answer here: https://stackoverflow.com/a/54432573/983173, if you instantiate your audio element within a synchronous user interaction event handler, you can re-use and re-play (e.g. .play()) an Audio() element as much as you like. For example:

    const button = document.getElementById('my-button')
    let audio = null
    button.addEventListener('click', () => {
        audio = new Audio('myAudio.mp3')
        // Works because `audio` itself was created synchronously during user event handler
        setTimeout(() => { audio.play() }, 1000)
    })