Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

js / html5 audio: Why is canplaythrough not fired on iOS safari?

i use the code below to preload an array of audio files (after user interacts with a button starting the process). After all audio files fired "canplaythrough" the code proceeds:

var loaded = 0;
function loadedAudio() {
    // this will be called every time an audio file is loaded
    // we keep track of the loaded files vs the requested files
    loaded++;
    console.log(loaded + " audio files loaded!");
    if (loaded == audioFiles.length){
      // all have loaded
      main();
    }
}

function preloadsounds()
{
  $("#loader").show();
  console.log(level.config);
  audioFiles = level.config.soundfiles;

  // we start preloading all the audio files with html audio
  for (var i in audioFiles) {
      preloadAudio(audioFiles[i]);
  }

}

function preloadAudio(url)
{
  console.log("trying to preload "+ url);
  var audio = new Audio();
  // once this file loads, it will call loadedAudio()
  // the file will be kept by the browser as cache
  audio.addEventListener('canplaythrough', loadedAudio, false);

  audio.addEventListener('error', function failed(e)
  {
    console.log("COULD NOT LOAD AUDIO");
    $("#NETWORKERROR").show();
  });
  audio.src = url;
}

works great on Android (Chrome and Firefox) but not a single canplaythrough event is fired on iOs Safari (tested live on 5s and emulated X both 11.x). All files are served from the same Origin. I also don't get any Error messages in my log.

What am I missing?

( the basis for the code above i go from: https://stackoverflow.com/a/31351186/2602592 )

like image 305
gauguerilla Avatar asked Apr 12 '18 09:04

gauguerilla


4 Answers

Try calling the load() method after setting the src.

function preloadAudio(url)
{
  console.log("trying to preload "+ url);
  var audio = new Audio();
  // once this file loads, it will call loadedAudio()
  // the file will be kept by the browser as cache
  audio.addEventListener('canplaythrough', loadedAudio, false);

  audio.addEventListener('error', function failed(e)
  {
    console.log("COULD NOT LOAD AUDIO");
    $("#NETWORKERROR").show();
  });
  audio.src = url;

  audio.load();  // add this line
}
like image 156
Gabriele Petrioli Avatar answered Nov 17 '22 04:11

Gabriele Petrioli


I see a lot of claims to make an audio object work, especially on Safari!

To go quickly I can tell you that you do not need much, just know to run everything on Safari 5 and more, and all browsers.

  1. Force the call to trough by reloading the first file in your list or file if you only use one. The Trough event is the one that will allow the user to read the file since it allows to know if the file is loaded and ready to be read. If you build a player, you must plan and let the user click on Play only if the through has occurred.

               ObjectAudio.src = file;

  2. Use trough to update your player

    ObjectAudio.addEventListener ('canplaythrough', Function, false);

  3. Use Progress to update the percentage buffering bar.

    ObjectAudio.addEventListener ('progress', Function, false);

  4. Use the timeupdate event to update the read bar.

    ObjectAudio.addEventListener ('timeupdate', Function, false);

You do not need complex things as I see what you are doing.


** Just one worry about Safari 5 Widows. For the object to work, QuickTime must be installed on the user, otherwise Safari 5 will not recognize the HTML 5 Audio object.

like image 39
Cherif Avatar answered Sep 17 '22 14:09

Cherif


Have you checked Network/Server and confirmed Safari is downloading the audio files?

If Safari's not downloading the audio (or only loading metadata instead of the full file), you could try setting audio.preload = 'auto' before setting audio.src

like image 2
searlea Avatar answered Nov 17 '22 05:11

searlea


In addition to the above, on a project where I'm reusing the same audio element multiple times by reassigning src at runtime, there were several more steps required. Yes, I would not get any canplaythrough event whatsoever if I did not at least

  1. set preload="auto" on the element before setting src
  2. set src
  3. call load() after setting src

but after most of a day of print-statement debugging and setting watchdog timeouts (since iOS inspection through Mac Safari is highly prone to both hardlocks and losing track of where it is....), I inadvertently stumbled across one more factor:

  1. set audio.currentTime=0 before reassigning src

A ==0 check happened to be the gating condition within my watchdog timeout to see if the audio had in fact cascaded through my canplaythrough handler and begun to play, but it turns out that resetting it ahead of time so it would absolutely be 0 afterwards if the load/play failed... made the load not fail. Go figure. I was, for the record, previously also seeing the error 206 in the asset/network inspector on failed files, as reported by Stephen in earlier answer commentary, so I guess maybe iOS always loads a bit of the file, but gives up trying to load any more if the play head is already further than the load progress?

Anyway, this miraculously let the audio load in some circumstances, but if audio load was triggered by e.g. a message arriving from another frame, I still saw no canplaythrough, and possibly no events whatsoever (didn't check for lesser events since recovering from a playback halt due to canplay-but-not-canplaythrough was going to be worse for me than not playing at all). So that watchdog timer from my debugging became structural:

  1. kick off a setTimeout(()=>{if(audio.readyState==4) audio.play(); else presumeError();},1000);

It turns out that most of the time, the audio is in fact loading, Safari just doesn't let you know.

HOWEVER, it also turns out that in some circumstances where you don't get load events, various other tools are equally broken. Like setTimeout(). It flat out doesn't run the callback. There's at least one StackOverflow on this sort of issue from the mid 2010s circa iOS 6 that has been copypasta'd onto a million other sketchier support sites with the same dubious answer involving not using .bind() or else rewriting .bind() but I doubt most folks are using .bind() to begin with. What some other answers point to is https://gist.github.com/ronkorving/3755461 which may be overkill, and which I didn't use in full, but I did steal the rough concept of:

  1. if setTimeout() isn't working (or you just want finer granularity on your load watcher), write your own based on a (requestAnimationFrame || webkitRequestAnimationFrame)(keepTrackOfRequestedAudio); loop.

So now, if preload isn't handled, you get the notice after you manually load(); if manual load() isn't handled, you get the notice when you check in after a delay, and if the delay isn't handled, you at least get the notice (or can proactively give up) by constantly burning cycles to constantly watch state. Of course, none of this guarantees your audio has permission to play to begin with, but that's an entirely different iOS problem (hint: .play() returns a promise, and you can .catch() that promise for permission errors on any platform I've tried so far).

like image 2
NNSkelly Avatar answered Nov 17 '22 04:11

NNSkelly