An audio mixing console with equalizer sliders and a gray-blue British shorthair cat watching nearby.

Nitro Player: A Batteries-Included Music Engine for React Native

The Gray Cat
The Gray Cat
0 views

If you have ever tried to build a real music app in React Native, you already know the pain. The obvious starting point handles "play a URL," but the moment you need a proper playback queue, lock-screen controls, a working equalizer, offline downloads, and Android Auto or CarPlay integration, you find yourself bolting four or five libraries together and praying they cooperate. Nitro Player takes a different stance: it bundles the hard parts of a music app into a single engine and builds the whole thing on top of Nitro Modules, the statically-compiled JSI binding framework from the Margelo team.

That foundation is the headline. Instead of routing native calls through the legacy React Native bridge, Nitro Modules generate a compiled binding layer straight to C++, Swift, and Kotlin. The result is low-overhead, synchronous-capable native calls and New Architecture-native performance. For audio, where latency and steady state updates matter, that is a meaningful difference. The trade-off, which we will be candid about, is that this is a young library leaning on a pre-1.0 dependency. But the feature set is genuinely ambitious, so let's dig in.

Why This One Is Different

Most React Native audio modules treat playback as a single sound at a time. Nitro Player is architected around playlists and a real playback queue from the ground up, which is exactly the mental model a music app needs. Out of the box you get:

  • Full playlist and queue management with a dual "play next" / "up next" model that mirrors Spotify and Apple Music.
  • A built-in 5-band equalizer with a ±12 dB gain range.
  • Offline downloads with pause, resume, cancel, and progress tracking.
  • Lock screen, Control Center, and notification controls.
  • First-class Android Auto and CarPlay integration.
  • Audio device and route selection (AirPlay, Bluetooth, wired, speaker).
  • A complete set of React hooks for state, plus imperative listeners if you prefer.

The internals are organized into three modules you will interact with constantly: TrackPlayer for playback operations, PlayerQueue for playlists and track organization, and DownloadManager for offline content. Keep those three names in mind and the rest of the API falls into place quickly.

Getting It Into Your Project

Nitro Player requires the New Architecture, since Nitro Modules is New-Arch only. You install the player alongside its Nitro Modules peer dependency:

npm install react-native-nitro-player react-native-nitro-modules
yarn add react-native-nitro-player react-native-nitro-modules

On iOS, the pods are usually autolinked, so the practical step is just running the install from your ios directory:

cd ios && pod install

If you need to wire them in by hand, the Podfile entries are straightforward:

pod 'react-native-nitro-modules'
pod 'react-native-nitro-player'

On Android these are typically autolinked as well; if not, add them to your build.gradle dependencies and sync Gradle:

dependencies {
  implementation 'com.riteshshukla:nitro-player:latest'
  implementation 'com.riteshshukla:nitro-modules:latest'
}

After that, a clean rebuild of the app gets you running.

Setting the Stage

Before playing anything, you configure the player once, near app startup. This is where you opt into the in-car experiences and the persistent notification controls:

import { TrackPlayer } from 'react-native-nitro-player'

await TrackPlayer.configure({
  androidAutoEnabled: true,
  carPlayEnabled: false,
  showInNotification: true,
})

Every track you hand to the player follows a single TrackItem shape. It carries the usual metadata plus an extraPayload escape hatch for anything app-specific, such as an artist ID, a genre tag, or a favorite flag:

interface TrackItem {
  id: string
  title: string
  artist: string
  album: string
  duration: number
  url: string
  artwork?: string | null
  extraPayload?: {
    [key: string]: string | number | boolean | Record<string, unknown>
  }
}

That extraPayload field is more useful than it looks. Because it travels with the track through the queue and the now-playing state, you can read your own metadata back out in event handlers without maintaining a separate lookup table.

From Tracks to Sound

The core flow is build a playlist, fill it with tracks, load it, and play. Here is the whole thing end to end. First, assemble your tracks and create a playlist:

import { PlayerQueue } from 'react-native-nitro-player'
import type { TrackItem } from 'react-native-nitro-player'

const tracks: TrackItem[] = [
  {
    id: '1',
    title: 'Song Title',
    artist: 'Artist Name',
    album: 'Album Name',
    duration: 180.0,
    url: 'https://example.com/song.mp3',
    artwork: 'https://example.com/artwork.jpg',
    extraPayload: { artistId: '123', genre: 'Rock', isFavorite: true },
  },
]

const playlistId = await PlayerQueue.createPlaylist(
  'My Playlist',
  'Playlist description',
  'https://example.com/playlist-artwork.jpg'
)

await PlayerQueue.addTracksToPlaylist(playlistId, tracks)

Then load that playlist into the player and start a specific song:

import { TrackPlayer, PlayerQueue } from 'react-native-nitro-player'

await PlayerQueue.loadPlaylist(playlistId)
await TrackPlayer.playSong('1', playlistId)
await TrackPlayer.play()

From there, the playback controls are exactly what you would expect: play(), pause(), seek(position), skipToNext(), skipToPrevious(), and skipToIndex(index). There are also the niceties a polished player needs, like setVolume(0-100), setPlaybackSpeed(speed), and setRepeatMode('off' | 'track' | 'Playlist').

Reading the Player's Mind

Manually tracking playback state across components is tedious, so Nitro Player ships a suite of React hooks that subscribe to the engine for you. The cornerstone is useNowPlaying(), which hands back a complete snapshot of the current track, playback state, total duration, the active playlist, and the current index. Pair it with useOnPlaybackStateChange() to react to transitions like buffering or pausing:

import { useNowPlaying, useOnPlaybackStateChange } from 'react-native-nitro-player'
import { View, Text } from 'react-native'

function PlayerComponent() {
  const { currentTrack, currentState, totalDuration } = useNowPlaying()
  const { state, reason } = useOnPlaybackStateChange()

  return (
    <View>
      {currentTrack && (
        <Text>Now: {currentTrack.title} by {currentTrack.artist}</Text>
      )}
      <Text>State: {currentState}</Text>
      <Text>Duration: {totalDuration}s</Text>
    </View>
  )
}

The rest of the hook family covers the surfaces a music UI needs: useOnChangeTrack() for track transitions and their reason, useOnPlaybackProgressChange() for real-time scrubber updates, useOnSeek() for seek events, useActualQueue() for the live queue, usePlaylist() for playlist collections, and useDownloadProgress() for download bars. If you would rather wire things up imperatively, the same events are available as plain listeners: onChangeTrack(), onPlaybackStateChange(), onSeek(), and onPlaybackProgressChange().

The Queue Model That Earns Its Keep

This is the part that separates a toy from a real music engine. Nitro Player gives you two distinct ways to insert tracks ahead of the normal playlist order, and they behave differently on purpose:

import { TrackPlayer } from 'react-native-nitro-player'

// Plays immediately after the current track (LIFO stack)
await TrackPlayer.playNext('track-42')

// Appends to a temporary FIFO queue that drains after the current track
await TrackPlayer.addToUpNext('track-99')

playNext() behaves like a stack, so the most recent track you add jumps to the front of the line, which is what users expect from a "Play Next" button. addToUpNext() behaves like a FIFO queue, the equivalent of "Add to Queue," where tracks play in the order you added them. These temporary tracks live alongside your playlist and automatically clear when a new playlist is loaded.

To make sense of what is currently playing, the player exposes a currentPlayingType state that tells you the source of the active track: 'playlist', 'play-next', 'up-next', or 'not-playing'. Combined with skipToIndex(), which navigates the actual queue including those temporary insertions, you have everything you need to build a queue screen that matches the big streaming apps.

Tuning the Sound

The equalizer is one of Nitro Player's standout features, because it is simply absent from most React Native audio libraries. It offers five bands centered at 60 Hz, 230 Hz, 910 Hz, 3.6 kHz, and 14 kHz, each adjustable across a ±12 dB range. The useEqualizer() hook gives you the band data and the setters to drive a UI:

import { useEqualizer } from 'react-native-nitro-player'
import { View, Text, Switch, Slider, Button } from 'react-native'

function EqualizerControl() {
  const { isEnabled, setEnabled, bands, setBandGain, reset } = useEqualizer()

  return (
    <View>
      <Switch value={isEnabled} onValueChange={setEnabled} />
      {bands.map((band) => (
        <View key={band.index}>
          <Text>{band.frequencyLabel}</Text>
          <Slider
            minimumValue={-12}
            maximumValue={12}
            value={band.gainDb}
            onSlidingComplete={(value) => setBandGain(band.index, value)}
          />
        </View>
      ))}
      <Button title="Reset" onPress={reset} />
    </View>
  )
}

Under the hood the equalizer exposes setEnabled(), setBandGain(index, gain), setAllBandGains(), and reset(), so you can offer both a manual slider experience and one-tap presets.

Taking the Music Offline

A serious music app needs to work on the subway, and Nitro Player's DownloadManager handles that without a separate library. You configure it once, then download individual tracks or whole playlists, with full lifecycle control:

import { DownloadManager } from 'react-native-nitro-player'

await DownloadManager.configure()
await DownloadManager.downloadTrack(track)
await DownloadManager.downloadPlaylist(playlistId)

await DownloadManager.pauseDownload('1')
await DownloadManager.resumeDownload('1')
await DownloadManager.cancelDownload('1')

Progress is observable through the useDownloadProgress() hook, so wiring up a download indicator is trivial. The clever piece is the source preference, set to 'auto', 'download', or 'network'. With 'auto', the player transparently uses a downloaded file when it exists and falls back to streaming when it does not, which is exactly the behavior you want for a cached library.

Reaching the Dashboard

The in-car and route-selection features are where Nitro Player flexes its "batteries-included" claim. On Android, you can enumerate and switch audio outputs through AudioDevices.getAudioDevices() and setAudioDevice(), routing playback to Bluetooth, wired headphones, or the speaker. You can also customize the Android Auto browse UI with AndroidAutoMediaLibraryHelper.set() and clear(), defining a media library of folders and playlists rendered in either a grid or list layout. The useAndroidAutoConnection() hook tells you when a head unit connects.

On iOS, CarPlay is enabled through the carPlayEnabled configuration flag, and you can present the native AirPlay and route picker sheet with AudioRoutePicker.showRoutePicker(). These integrations are the kind of work that typically eats days of native code, so having them as first-class API calls is a real time saver.

A Candid Word on Maturity

Now for the honest part. Nitro Player is young. The repository was created in late 2025, it sits around 125 stars, and weekly downloads hover near 571. It is effectively driven by one primary maintainer plus one notable contributor, there is no LICENSE file declared in the repo metadata at the time of writing, and it depends on react-native-nitro-modules, which is itself still pre-1.0. It also hard-requires the New Architecture.

The flip side is encouraging. The release cadence is brisk, moving from 0.5.x to a stable 1.0 and on to 1.4.3 in roughly four months, with an automated semantic-release pipeline and conventional-commit changelogs. Since 1.0 the public API has been additive and stable, with recent releases focused on runtime crash fixes, a Buffering playback state, lazier loading on iOS, and search. Frequent alpha pre-releases tell you the surface is still settling, so pin your version and read the changelog before upgrading.

For context, the incumbent in this space pulls around 44,000 weekly downloads and is battle-tested, but it rides the legacy bridge and ships neither a built-in equalizer nor a download manager. Nitro Player is the modern, performance-focused challenger that bundles those features and runs on JSI, at the cost of being far smaller and younger. There is also a sibling project under the same "Nitro" branding aimed at simpler playback plus recording and web support, so do not confuse the two: that one is for audiobook and podcast use cases, while Nitro Player is the full music-library engine.

Should You Reach for It

Nitro Player is a compelling answer to a real and persistent problem: assembling a complete music-app audio stack in React Native usually means duct-taping libraries together. By packaging the queue model, equalizer, offline downloads, and Android Auto plus CarPlay support into one Nitro-Modules-powered engine, it offers a genuinely modern, New-Architecture-native path. If you are starting a fresh music app, value the performance story, and can tolerate a young library with a pre-1.0 dependency and an undeclared license, it is well worth a prototype. If you need rock-solid stability and a large community today, the established incumbent remains the safer default. Either way, the gray-blue cat in the corner approves of any tool that makes good sound easier to ship.