Leon Overweel
Leon Overweel

Reputation: 1567

Why can't JavaScript .play() audio files on iPhone safari?

I've got a JavaScript web app working that plays some audio periodically like this:

var SOUND_SUCCESS = new Audio('success.mp3');
SOUND_SUCCESS.play();

This works great on desktop browsers (tested in Edge and Chrome), but it doesn't play on Safari on iPhone.

I've looked around Stack Overflow, and I found some answers from a couple of years ago that it's not possible to play audio from Safari unless you're in that full screen player. Is this still the case?

Upvotes: 75

Views: 117723

Answers (11)

Mohit Bhutaiya
Mohit Bhutaiya

Reputation: 1

Ensure the .play() method is triggered within a user interaction event, such as a button click. Example:

javascript

// Preload the audio
var SOUND_SUCCESS = new Audio('success.mp3');

// Play audio on user interaction
document.getElementById('playButton').addEventListener('click', function () {
    SOUND_SUCCESS.play().catch(error => {
        console.error('Audio playback failed:', error);
    });
});

Additional Tips Check Audio Format: Use formats compatible with Safari (e.g., MP3 or AAC). Mute Option for Autoplay: If autoplay is needed, ensure the audio starts muted:

javascript

var audio = new Audio('success.mp3');
audio.muted = true;
audio.play(); // Autoplay works only if muted

Handle Errors Gracefully: Use .catch() on the .play() promise to debug issues. Why the Restriction? Apple enforces these rules to avoid intrusive behavior, conserve battery life, and manage data usage. Always design web apps with user control in mind.

Upvotes: 0

Himanshu Dhiraj Mishra
Himanshu Dhiraj Mishra

Reputation: 1089

In Safari on iOS (for all devices, including iPad), preload and autoplay are disabled, unless the user is on a cellular network and be charged per data unit. No data is loaded until the user initiates it. I have simplified a working code for my project where I need to play button click sound, hope this will help.

 <button class="js-button-clicked">Button 1</button>
 <button class="js-button-clicked" >Button 2</button>

JS:

(function () {
  // Check if the browser supports web audio. Safari wants a prefix.
  if ("AudioContext" in window || "webkitAudioContext" in window) {
    //////////////////////////////////////////////////
    // Here's the part for just playing an audio file.
    //////////////////////////////////////////////////
    var ButtonPlay = function ButtonPlay(audioBuffer) {
      var source = context.createBufferSource();
      source.buffer = audioBuffer;
      source.connect(context.destination);
      source.start();
    };

    var soundUrl = "https://images.skidos.com/video-js/button_pressed.mp3";
    var AudioContext = window.AudioContext || window.webkitAudioContext;
    var context = new AudioContext(); // Make it crossbrowser
    var gainNode = context.createGain();
    gainNode.gain.value = 1; // set volume to 100%
    var eventButtons = document.querySelectorAll(".js-button-clicked");
    var yodelBuffer = void 0;

    // The Promise-based syntax for BaseAudioContext.decodeAudioData() is not supported in Safari(Webkit).
    window
      .fetch(soundUrl)
      .then((response) => response.arrayBuffer())
      .then((arrayBuffer) =>
        context.decodeAudioData(
          arrayBuffer,
          (audioBuffer) => {
            yodelBuffer = audioBuffer;
          },
          (error) => console.error(error)
        )
      );
    eventButtons.forEach((el) =>
      el.addEventListener("click", (event) => {
        return ButtonPlay(yodelBuffer);
      })
    );
    //////////////////////////////////////////////////
    // Here's the part for unlocking the audio context, probably for iOS only
    //////////////////////////////////////////////////

    function unlock() {
      console.log("unlocking");
      // create empty buffer and play it
      var buffer = context.createBuffer(1, 1, 22050);
      var source = context.createBufferSource();
      source.buffer = buffer;
      source.connect(context.destination);

      // play the file. noteOn is the older version of start()
      source.start ? source.start(0) : source.noteOn(0);

      // by checking the play state after some time, we know if we're really unlocked
    }
    // Try to unlock, so the unmute is hidden when not necessary (in most browsers).
    unlock();
  }
})();

Example: https://codepen.io/himstar/pen/MWBeLvG

  • tested on iPhoneXR, iPadAir and MacOS 12.5

Upvotes: 0

Ed Ballot
Ed Ballot

Reputation: 3485

iOS disables autoplay, instead requiring that play be initiated as part of a user interaction (e.g., you can start playback within a touchstart listener). There's a bit of documentation about this on Apple's developer documentation. There's also this article Overcoming iOS HTML5 audio limitations on IBM's developer site that has examples and more detail.

Upvotes: 58

Ron Ross
Ron Ross

Reputation: 285

I know it's been answered, but for some this may help. If you have the .play trigger in a setTimeout function you must keep the time below 951.

setTimeout(function(){$('audio').play}, 999) prevents auto play

setTimeout(function(){$('audio').play}, 950) auto play works

Upvotes: 3

joel Moses
joel Moses

Reputation: 445

Safari gives priority to the soundtrack of videos over audio files! If a video has a soundtrack, even if it is silent and set to mute it will block the audio. The solution is to strip the soundtrack from the video, not record over it. I hope this helps some one! I lost days of work with this problem.

Upvotes: 2

Wilrick B
Wilrick B

Reputation: 135

Very old question but something that might help some people out, in my case (ofcourse this might not be viable in all cases), it helped to play all audio files for 1millisecond, then pause them. After that, if you resume playing the audio it works without issue.

   playButton.addEventListener('click', function(){
      introVid.play();
      audioPlayers[0].play(); //every array element is constructed using new Audio("yourlink");
      audioPlayers[1].play();
      audioPlayers[2].play();
      audioPlayers[3].play();
      window.setTimeout(function(){
        audioPlayers[0].pause();
        audioPlayers[1].pause();
        audioPlayers[2].pause();
        audioPlayers[3].pause();
      },1);
    })

Upvotes: 1

user2415116
user2415116

Reputation: 777

To add to xingliang cai's response, here's a code sample I got to work for me (edited below to work on iOS14, thanks @AndrewL!):

const soundEffect = new Audio();
soundEffect.autoplay = true;

// onClick of first interaction on page before I need the sounds
// (This is a tiny MP3 file that is silent and extremely short - retrieved from https://bigsoundbank.com and then modified)
soundEffect.src = "data:audio/mpeg;base64,SUQzBAAAAAABEVRYWFgAAAAtAAADY29tbWVudABCaWdTb3VuZEJhbmsuY29tIC8gTGFTb25vdGhlcXVlLm9yZwBURU5DAAAAHQAAA1N3aXRjaCBQbHVzIMKpIE5DSCBTb2Z0d2FyZQBUSVQyAAAABgAAAzIyMzUAVFNTRQAAAA8AAANMYXZmNTcuODMuMTAwAAAAAAAAAAAAAAD/80DEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQsRbAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQMSkAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV";

// later on when you actually want to play a sound at any point without user interaction
soundEffect.src = 'path/to/file.mp3';

Upvotes: 63

AndrewL
AndrewL

Reputation: 3326

To get @user2415116's solution to work in iOS 14, I did this:

const soundEffect = new Audio();
soundEffect.autoplay = true;

// onClick of first interaction on page before I need the sounds
// (This is a tiny MP3 file that is silent and extremely short - retrieved from https://bigsoundbank.com and then modified)
soundEffect.src = "data:audio/mpeg;base64,SUQzBAAAAAABEVRYWFgAAAAtAAADY29tbWVudABCaWdTb3VuZEJhbmsuY29tIC8gTGFTb25vdGhlcXVlLm9yZwBURU5DAAAAHQAAA1N3aXRjaCBQbHVzIMKpIE5DSCBTb2Z0d2FyZQBUSVQyAAAABgAAAzIyMzUAVFNTRQAAAA8AAANMYXZmNTcuODMuMTAwAAAAAAAAAAAAAAD/80DEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQsRbAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQMSkAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV";

// later on when you actually want to play a sound at any point without user interaction
soundEffect.src = 'path/to/file.mp3';

Upvotes: 15

luky
luky

Reputation: 2370

Haha, I outsmarted it like this.

Put in the page the audio tag with autoplay (true)

<audio id="beep" src={Assets.SOUND_BEEP} autoPlay />

It will play the sound once the element is mounted. (Even on safari iOS)

Then it seems you can play it whenever you want again by calling

document.getElementById('beep').play();

But now you may say, but I don't want to play the sound as autoplay.

Yes, I outsmarted it to put the "muted" property on that, and then set it to false when playing.

<audio id="beep" src={Assets.SOUND_BEEP} autoPlay muted />

And play:

document.getElementById('beep').muted = false;
document.getElementById('beep').play();

Upvotes: -4

Tithos
Tithos

Reputation: 1437

I used this:

useEffect(() => {
  window.addEventListener('touchstart', () => {
    document.getElementById('audio').muted = false
    document.getElementById('audio').play()
  })
})

As soon as the user scrolls, the sound plays

Upvotes: 2

xingliang cai
xingliang cai

Reputation: 391

IOS on mobile disable automatic sound playing by default. So to get around this problem. You could put enable/disable switch button somewhere on the page and play some sound using an audio element("audioElement") if the user click the button switch.

After that, the same "audioElement" can be used to play future sounds by changing its "src" attribute and call its "play()" method, without any further user interaction.

Upvotes: 28

Related Questions