DEV Community

Cover image for I Built a Free MIT Audio Engine for Expo Because Background Audio Should Not Require a Commercial License
Mark Melton
Mark Melton

Posted on Originally published at Medium

I Built a Free MIT Audio Engine for Expo Because Background Audio Should Not Require a Commercial License

Daily Bible - Offline & Audio was never supposed to be a music app.

It is a Bible. Lightweight. Offline-first. People open it on a commute, lock the phone, and keep listening. Verses queue from on-device TTS. Some readers want a quiet ambient bed under scripture. Some chapters arrive as HLS. All of it has to survive the lock screen, Bluetooth earbuds, a wired headset in a car, and the moment Android decides your process is optional.

That is not a demo. That is a product people pray with.

I needed a player that could do all of that inside an Expo app - New Architecture, config plugin, no hand-maintained native snowflake - without turning Daily Bible into a media suite.

The obvious existing solution went commercial. The license closed the door.

I had one honest option: build the player Daily Bible actually needed. I open-sourced it.

daily-react-native-player — MIT, New Architecture only (Expo SDK 57+ / RN 0.86+).


The constraint nobody puts on a landing page

A music app can be heavy. A Bible app cannot.

Daily Bible has to feel instant. Download a translation. Play a chapter. Put the phone in a pocket. Skip the next verse from a Bluetooth bud without unlocking. If someone wants ambient rain under the narration, the lock screen still has to show scripture — not the bed track.

That last sentence is the whole product.

On-device TTS sharpened the constraint further. The player does not run the model. It consumes URLs. The pipeline generates speech at the edge, hands the engine a queue that grows while the current verse is still speaking. Append mid-play. Insert a silence gap that is a real track, not a setTimeout that drifts when the OS is busy. Seek an HLS chapter before the playlist is ready. Keep the session alive when the user kills the app — if that is the policy.

Expo was non-negotiable. Continuous native generation. A config plugin that injects iOS audio background mode and Android's mediaPlayback foreground service at prebuild — not a wiki page of native edits you forget on the next npx expo prebuild.

New Architecture only. No legacy bridge tax.


Install

npx expo install daily-react-native-player
Enter fullscreen mode Exit fullscreen mode

Add the config plugin:

{
  "expo": {
    "plugins": ["daily-react-native-player"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Peers: Expo SDK 57+, React Native 0.86+, New Architecture only. iOS + Android. No web player.


Wiring remotes before the root component

// index.ts — must be before registerRootComponent
import {
  registerPlaybackService,
  setupPlayer,
  add,
  play,
  pause,
  skipToNext,
  skipToPrevious,
  Event,
  addEventListener,
} from 'daily-react-native-player';
import { registerRootComponent } from 'expo';
import App from './App';

registerPlaybackService(() => async () => {
  addEventListener(Event.RemotePlay, () => void play());
  addEventListener(Event.RemotePause, () => void pause());
  addEventListener(Event.RemoteNext, () => void skipToNext());
  addEventListener(Event.RemotePrevious, () => void skipToPrevious());
});

registerRootComponent(App);
Enter fullscreen mode Exit fullscreen mode

Remotes emit to JavaScript. Next means what your app defines — the native layer does not guess.


Loading a queue and playing

await setupPlayer({ progressUpdateEventInterval: 1 });

await add([
  { url: 'https://cdn.example.com/verse1.mp3', title: 'Genesis 1:1' },
  { url: 'https://cdn.example.com/verse2.mp3', title: 'Genesis 1:2' },
  createSilenceTrack({ durationMs: 800 }), // real queue item, not a timer
  { url: 'https://cdn.example.com/verse3.mp3', title: 'Genesis 1:3' },
]);

addEventListener(Event.PlaybackActiveTrackChanged, ({ track }) => {
  // sync your "now reading" UI
  console.log('now playing', track?.title);
});

await play();
Enter fullscreen mode Exit fullscreen mode

What it does — the non-negotiables

Lock screen, notification, Control Center, Bluetooth. Play, pause, stop, next, previous from every surface users actually touch. MediaSession on Android. MPRemoteCommandCenter on iOS. Hardware buttons emit to JavaScript — no native guessing about what Next means.

Audio that keeps going with the screen off. Android mediaPlayback foreground service. iOS audio background mode. Both injected at Expo prebuild by the config plugin. Continue-after-kill when you want the session to survive.

A real playlist. Not one URL at a time. add, remove, skip while audio keeps playing. Chapter-scale queues. Stable track IDs. The same Playback* events your Now Playing UI needs anyway.

Native silence tracks. SilenceMediaSource on Android, cached PCM WAV on iOS. First-class queue items with progress and remote events. Not a setTimeout that drifts.

Optional ambient dual-audio. Second player is lazy — created only when the first ambient API is called. Never requests audio focus. Never owns Now Playing. Speech-only apps never pay for it. Bible-style listening needs it. Music apps can ignore it.

HLS VOD + seek-after-ready. Plus WAV, mp3, m4a. Seek before the playlist is ready — stashed, applied on READY.

Pitch-preserving setRate. Silence stays at 1× so pauses are not stretched.

Production hardening. Setup coalesce + 10s timeout. FGS sync promotion. Reset play-intent-first. These are not nice-to-haves; they are what makes the player behave on real OEM Android devices.

One native audio owner. Media3 on Android. AVFoundation on iOS. No second focus-owning library bolted on for ambient.

MIT. Commercial use. No license gate. Fork it. Ship it.


Who this is for

Edge-native TTS pipelines. You generate URLs. We play them in the background. Progressive append, silence gaps, pitch-preserving rate — all there.

Music and playlist apps. A real queue that mutates while playing. Lock-screen artwork. Headset remotes. No commercial license required to get the fundamentals right.

Narration, meditation, language learning, audiobooks, podcasts. Anything that talks while the phone is in a pocket.


If you want to hear it in the wild, open Daily Bible - Offline & Audio.

That is not a sample app. That is why the engine exists.

GitHub: github.com/coommark/daily-react-native-player
npm: npmjs.com/package/daily-react-native-player

Star the repo. If you have been looking for background audio on Expo that treats TTS, playlists, and speech as first-class — this is it.

Top comments (0)