Force Enable Web Audio Autoplay on iOS for Incompatible Apps via Userscript

Introduction

In recent years, the programmability of the Web has improved drastically. With the power of WebAssembly, Web browsers are becoming the universal application runtime. An interesting side-effect is that - if one accepts a moderate performance penalty, it’s now possible for iOS users run almost arbitrary apps, including emulators that Apple is never going to approve for its App Store.

Unfortunately, I’ve tried a couple of different WebAssembly apps, and I found most have no functional Web Audio due to iOS’s restriction on autoplay, making it impossible to play a multimedia game. Without explicit user interactions, no audio autoplay is permitted. By itself, this policy is totally reasonable - nobody is a fan of intrusive ads. But as yet another demostrate of Apple heavy-handed control of its proprietary platform, users have no way to add exceptions. Thus, most existing Web apps are incompatible with iOS, unless they’re modified by developers with a “Tap to Play Audio” button.

However, recent iOS versions started to allow browser extensions, including a Userscript manager written by Justin Wasack (quoid). Via Userscript, we can force-enable background Web Audio by monkey-patching the web page, allowing you to enjoy your games.

How it works

The essential part of the Userscript includes just 20 lines of code.

First, as early as possible, we replace the built-in AudioContext() constructor from the Web Audio API with our own wrapper by monkey-patching. The underlying AudioContext() is still called, but in our wrapper, we record the created objects via the array createdAudioContextList, allowing us to track all the AudioContext objects in the Web page.

let realAudioContext = window.AudioContext;
let createdAudioContextList = [];

window.AudioContext = function(options) {
  alert("Web Audio has been intercepted.");
  
  let context = new realAudioContext(options);
  createdAudioContextList.push(context);
  injectDialog();

  return context;
}

Next, we inject a “Play” button into the DOM (omitted), with the following onclick callback to unblock the AudioContext.

let buttonYes = document.getElementById("iOSWebAudioUnblockYes");
buttonYes.onclick = () => {
    for (const audioContext of createdAudioContextList) {
      unblockWebAudioContext(audioContext);
  }
}

// Allow a Web Audio context to play background audio.
//
// This function must be called in a callback as a result
// or user interaction, such as onclick.
function unblockWebAudioContext(audioContext) {
  if (audioContext.state === "suspended") {
    audioContext.resume();
  }
}

Furthermore, if the user minimizes the browser or turns off the screen, an AudioContext may become interrupted. An additional event handler is necessary to process this transition.

for (const audioContext of createdAudioContextList) {
  audioContext.onstatechange = () => {
    if (audioContext.state === "interrupted") {
      // running -> interrupted
      // Previously allowed, we can resume it without user interaction.
      audioContext.resume();
    }   
  }   
}   

That’s all it takes to force-enable Web Audio for incompatible apps on iOS, at this point, one can just tap the injected “Play” button and enjoy the game with audio.

The actual Userscript is necessarily more complex to handle a few more details.