Skip to content

iOS Background Audio

iOS Background Audio solves a common pain point for web audio developers: on iOS, flipping the hardware ringer/silent switch mutes all audio played through the browser. This project demonstrates how to use the navigator.audioSession API to override that behaviour, ensuring audio continues to play regardless of the switch position — critical for music apps, games, and any web experience where audio is part of the core product.

The technique boils down to a single API call, but getting it right across iOS versions and browsers requires careful testing. This repo packages the solution as a clean, deployable demo with a live GitHub Pages site for on-device verification.

By default, Safari on iOS treats web audio as “ambient” — meaning the ringer switch silences it, just like a notification sound. For media-focused web apps (music players, DAWs, interactive audio experiences), that’s a deal-breaker. Users expect audio to behave like Spotify or YouTube, not like a ringtone.

The key is the Audio Session API, supported from Safari 16.4+. Setting the session type to "playback" tells the OS to treat the page’s audio as intentional media output:

function backgroundAudioFix(): void {
interface AudioSession {
type: "ambient" | "playback" | "auto";
}
type Nav = Navigator & { audioSession: AudioSession };
function hasAudioSession(nav: Navigator): nav is Nav {
return (nav as Nav).audioSession !== undefined;
}
if (typeof window !== "undefined" && hasAudioSession(window.navigator)) {
window.navigator.audioSession.type = "playback";
}
}

The function includes a runtime feature check, so it’s safe to call on any platform — it simply no-ops where the API isn’t available.

The project includes detailed cross-browser, cross-version test results gathered on BrowserStack:

PlatformBrowserSupported
iOS 18.1Chrome
iOS 18.1Safari
iOS 17.6.1Firefox 133.3
iOS 16Safari 16
iOS 16Chrome 92
iOS 15Chrome 92
iOS 15Safari 15
  • Silent switch bypass — Audio plays through the hardware ringer switch via the Audio Session API
  • Build-time audio encoding — WAV sources are transcoded to Opus at 48 kHz using FFmpeg during build, with content-addressed filenames derived from Git LFS OIDs for cache busting
  • Bun macros for asset manifestsmacros.ts reads the encoded asset manifest at compile time, embedding the sound file mapping directly into the bundle with zero runtime I/O
  • SoundManager with lazy caching — A lightweight SoundManager class fetches, decodes, and caches AudioBuffer instances on demand, avoiding redundant network requests
  • can-start-audio-context integration — Uses the can-start-audio-context library to handle the iOS autoplay policy, ensuring the AudioContext is resumed after a user gesture
  • GitHub Pages CI/CD — Automated deployment via GitHub Actions with Git LFS checkout and FFmpeg setup

The build pipeline in build.ts is worth noting. It pulls WAV files tracked by Git LFS, transcodes them to Opus with FFmpeg, and writes a JSON manifest to .cache/encoded.json. The macros.ts file then reads this manifest at compile time using Bun’s macro system — meaning the final bundle knows exactly which hashed filenames to fetch, with no runtime file-system access needed.

At runtime, SoundManager.fromContext() creates an audio manager preloaded with the macro-embedded manifest. When sm.fetch("rosa10") is called, it looks up the hashed Opus filename, fetches and decodes it, and caches the resulting AudioBuffer for reuse.

LanguageTypeScript, HTML
RuntimeBun
BuildBun bundler with HTML entrypoints, FFmpeg for audio encoding
AudioWeb Audio API, Audio Session API
Asset PipelineGit LFS + Bun macros for compile-time asset manifests
DeploymentGitHub Actions → GitHub Pages
PlatformWeb (Safari / iOS primarily)

Try it on an iOS device: jadujoel.github.io/ios-background-audio

Flip your ringer switch to silent and tap to start — the audio should play through.

The source code is available on the project’s GitHub repository.