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

Abstract

On iOS, background audio autoplay is disallowed, users are not even allowed to add exceptions. Thus, many existing Web apps and games - WebAssembly apps in particular, are incompatible with iOS without modifications. Fortunately, iOS 15 (late 2021) started to allow browser extensions, including Userscripts. To overcome this problem, a Userscript has been developed to intercept Web Audio contexts and inject a “Start Autoplay?” pop-up window into the page, enabling otherwise incompatible games be playable on iOS with sound.

For installation instructions, skip to section Installation.

Demo

The following three emulators had no audio due to aforementioned compatibility problem, which is successfully fixed by my Userscript.

DOSBox-X / PC-98

PC-98 emulation by a WebAssembly port of DOSBox-X via Emscripten, by yksoft1, running the game Touhou 4: 東方幻想郷 ~ Lotus Land Story

Pinky / NES

NES emulation by Pinky, an NES emulator written in Rust, running the game Super Mario Bros..

PCjs / IBM PC

IBM PC emulation by PCjs, a JavaScript emulator of IBM PC and compatibles, running the game Nine Princes in Amber

Development

Motivation

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 to run almost arbitrary apps, including emulators that Apple will never 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 demostratation 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, since iOS 15 (late 2021), Apple 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.

Implementation

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 dialog with a “Play” button into the DOM.

let dialog = document.createElement('dialog');
dialog.innerHTML = ` 
  <div>
    <p>
      This website has just created a Web Audio context.<br />
      Allow audio playback in the background?
    </p>
    <div>
      <button id="UnblockiOSWebAudioNo">No</button>
      <button id="UnblockiOSWebAudioYes">Yes</button>
    </div>
  </div>`;

document.body.appendChild(dialog);
dialog.showModal();

The “Yes” button has 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
// of 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.

Limitations

This Userscript doesn’t always work due to several limitations.

First, even with @run-at document-start, technical limitations on iOS make it impossible to run Userscript exactly before everything else. If the Web pages creates an AudioContext at an very-early moment (e.g. in a JavaScript file that runs as soon as the page is loaded), this AudioContext cannot be intercepted.

Next, background audio can be implemented via methods other than Web Audio’s AudioContext. This script makes no attempt to patch them.

Installation

  1. Install “Userscripts” app written by Justin Wasack (quoid) from App Store. Note that this is free software licensed under GPLv3, you can obtain its source code from GitHub.
Install Userscripts from App Store
  1. Open “Userscripts” app from iOS home screen, click “Set Userscripts Directory”, create and select an empty directory via iOS’s file manager for storing Userscripts. Do not skip this step, otherwise the App won’t be functional.
Open App from home screen Set a directory via iOS's file manager
  1. Use Safari to open my script Unblock iOS Background WebAudio hosted on OpenUserJS, click “Install”. Now the source code of the script should appear on the screen. You may use this chance review the code to see how it works.

  2. Click the “puzzle” icon at the right of the address bar to open the list of browser extensions, click “Userscripts”, and grant permissions.

Click Userscripts in the list of browser extensions Grant permissions
  1. Now the Userscripts menu should open, with a “Userscript Detected: Tap to Install” notice. Tap the notice to install.
Tap to install
  1. The script metadata should appear in the menu, scroll the menu to the end, and click “Install”.
Script information appears Scroll to the end and click Install
  1. For every new website that you need to run Userscripts, repeat step 4 to grant permissions, then refresh the page. Furthermore, you can check whether a Userscript is active by noticing whether its name is highlighted in the menu.

Note: By default, this script matches ALL websites, since the iOS “Userscripts” extension has no way to apply scripts to user- defined websites. Thus, make sure to enable this script only when it’s needed, and disable it otherwise.