Roughly 80% of the code in a module I just wrote will never come into play. This is because I was debugging a problem in the manner of Ron Weasley blaming Snape for some evil at the beginning of a Harry Potter book.
In my case, Snape is the Safari web browser.
I’m predisposed to believing Safari is bizarre and the cause of many problems. When I hit an issue with it not allowing my web app to play audio though the other browsers did allow it, I started writing code before making sure I knew the cause of a problem.
Yesterday, I said the purpose of the play-audio-url was this:
Plays audio when it’s possible to play it via an HTMLAudioElement (as of 3/1/2019, this will work in Firefox and Chrome) and falls back to downloading the audio file and playing via an AudioContext if it can’t (as of 3/1/2019, Safari doesn’t allow HTMLAudioElement to play programmatically, even if the user has already clicked on the web page).
The above is a lie, it turns out.
I got there because I was trying to make Flame Buddy Sword Assistant work on iOS.
(Flame Buddy Sword Assistant is a web version of a magic sword from a D&D game I’m running. In short, unlike most magic swords, Flame Buddy Sword Assistant isn’t there to serve the user; it’s there to deliver the user to another entity. It’s not unlike quite a few software and hardware products today. And so, while it does work as a sword (sometimes), it speaks a lot on the behalf of its providing entity.)
From a technical perspective, it’s like an old-school Flash soundboard, but it operates under with modern web audio requirements and restrictions.
One of the recent restrictions is that a web page cannot play audio unless the user has first interacted with the page. Ostensibly, this is for the benefit of the user, however, this restriction does not apply to certain “important” whitelisted sites. Google chooses these sites; there’s no open process for deciding who the autoplaying elite are.
That one is not a problem for the app; I designed it so people would have to click to hear something from Flame Buddy Sword Assistant (the much more difficult alternative being having it recognize speech). And it worked fine on Firefox and Chrome. As you may recall, it did not work on Safari and Mobile Safari.
I implemented audio playing by setting the src
of an HTMLAudioElement, then calling the play
method on the element. On Safari, when I call that method, the promise returned by that method fails with an error with the message "Operation not supported"
and nothing more.
I searched the web for that and got into all of these articles and discussions about autoplay policy in Safari. It turns out that ‘autoplay’ in these contexts includes all sorts of programmatic media playing, not just stuff that plays when the page loads. I thought that, perhaps, Safari had misclassified my program as acting without interaction.
This is where I should have backed up and made sure this was indeed Safari’s issue but did not.
I saw that some people said that you could work around the perceived restriction by playing buffers in AudioContext
. I was all, aw yea I’m gonna solve this problem that a lot of people have in a neat NPM package!
I started play-audio-url
. It would try the HTMLAudioElement
to play a url (via the Audio
API). If that failed, it would fall back to use AudioContext
to do everything that HTMLAudioElement
would do:
- Download the file at the given url
- Decode the file contents to an uncompressed audio buffer
- Play the audio buffer via an AudioContext and SourceNode.
- Provide means to stop playback
It worked on Firefox, and then I tried it in Safari.
Nothing played.
Same error. Operation not supported
!
I found web pages talking about what formats Safari can understand. Here’s a table from Mozilla that says that Safari does not support ogg files. My audio file was an ogg file. DANT DANT DAH
I transcoded it to mp3. When I tried to play it, Safari still played static.
(It was late in the evening now, though I hadn’t realized it at the time. I’m no longer accustomed to staying up late, as I have a miserable time doing kid stuff on low sleep.)
After a bit of poking around, including some amusing console mucking like this —
> (new Audio).canPlayType('audio/ogg')
"maybe"
— and trying out sample rate adjustments, I tried playing the file in a player outside of the browser. The file actually contained all static. I had messed up the transcoding. After encoding it as an mp3 correctly, Safari played it fine via the HTMLAudioElement without needing all of that fallback code at all.
I don’t think I literally slapped my forehead, but I should have.
If I had looked into file formats way back here, I would not have lost sight of the truth and simply converted my audio files to mp3 and solved the problem with no code.
That sucked. Still, play-audio-url
is a convenient, if inessential, shorthand for the stuff you have to do with HTMLAudioElement
/Audio
if you want to download and play an audio file. Also, I was able to brush up on WebAudio. Did you know that stop
is no longer a method on AudioContext?
(I’m also writing this post so that I feel like I’m wrenching more value out of the mistake.)