Android Auto & CarPlay

RNTP V5 includes full Android Auto and CarPlay support. Users can browse and play audio from their car dashboard without touching their phone.

Android setup

1. Automotive app descriptor

Create res/xml/automotive_app_desc.xml:

<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
  <uses name="media" />
</automotiveApp>

Reference it in your app’s AndroidManifest.xml:

<meta-data
  android:name="com.google.android.gms.car.application"
  android:resource="@xml/automotive_app_desc" />
You do not need to declare the media service yourself. RNTP's library manifest already declares TrackPlayerPlaybackService with the correct intent filters; Android manifest merging pulls it into your app automatically.

Setting the browse tree

Android Auto and CarPlay display a browse tree when the user opens your app on the dashboard. Configure it with setBrowseTree:

TrackPlayer.setBrowseTree(categories:BrowseCategory[]):void

Set the media browse tree for Android Auto and CarPlay. Each category becomes a top-level browsable folder (Android Auto) or tab (CarPlay); its items can be playable tracks or browsable containers with children. When a user selects a playable item, the native player loads its siblings as the queue and starts playback without JS intervention. - On CarPlay, a maximum of 4 tabs are shown. If more than 4 categories are provided, the first 3 become tabs and the rest are grouped under a "More" tab. - A cold CarPlay connection immediately shows the last published browse tree, then refreshes when JavaScript publishes another tree. - Maximum nesting depth: 4 levels (matching CarPlay's navigation stack limit). - Call again to update the tree (e.g. after loading new content) - Listen for Event.BrowseRefreshRequested to refresh when a car starts browsing.

import TrackPlayer from '@rntp/player';

TrackPlayer.setBrowseTree([
  {
    mediaId: 'recent',
    title: 'Recently Played',
    items: [
      {
        mediaId: 'track-1',
        url: 'https://example.com/track1.mp3',
        title: 'Episode 42',
        artist: 'My Podcast',
        artworkUrl: 'https://example.com/artwork.jpg',
      },
    ],
  },
  {
    mediaId: 'favorites',
    title: 'Favorites',
    items: [/* ... */],
  },
]);

BrowseCategory

A top-level category. On Android Auto each category is a browsable folder; on CarPlay each is a tab (max 4; overflow creates a “More” tab).

A top-level category in the browse tree. On Android Auto, each category is a browsable folder. On CarPlay, each category is a tab (max 4; overflow creates a "More" tab).

FieldTypeDefaultDescription
mediaId * string—Unique identifier for this category.
title * string—Display title for the category (e.g. "Albums", "Podcasts").
items * BrowseItem[]—Items within this category — can be playable tracks or browsable containers.

BrowseItem

Each item is either playable (has url) or browsable (has children). url takes precedence if both are present.

An item in the browse tree — either a playable track or a browsable container. - `url` present → playable track (leaf node) - `children` present (no `url`) → browsable container (tapping drills down) - `url` takes precedence if both are present Maximum nesting depth: 4 levels (matching CarPlay's navigation stack limit).

FieldTypeDefaultDescription
mediaId * string—Unique identifier for this item.
title * string—Display title.
artist string—Artist or subtitle.
artworkUrl MediaUrl—Artwork image URL.
url MediaUrl—Audio source URL. If present, this item is playable.
duration number—Duration hint in seconds.
isLive boolean—Whether this is a live stream.
mimeType string—MIME type hint for format detection.
extras v5.1.3+Record<string, unknown>—App-defined payload attached to this item. Opaque to the player — when a playable item is selected from CarPlay or Android Auto, extras are loaded into the queue and returned unchanged by getActiveMediaItem, getQueue, and the Event.MediaItemTransition event. Must be JSON-serializable. Keep payloads small.
children BrowseItem[]—Child items. If present (and no `url`), this item is browsable.

App-defined payload

Browse items support the same extras field as queue media items. When the user selects a playable item from the dashboard, native code loads its siblings as the queue and preserves extras on each track — readable via getActiveMediaItem, getQueue, and MediaItemTransition without any JS handler.

TrackPlayer.setBrowseTree([
  {
    mediaId: 'music',
    title: 'Music',
    items: [
      {
        mediaId: 'album-1',
        title: 'Chill Vibes',
        children: [
          {
            mediaId: 'track-1',
            url: 'https://example.com/track1.mp3',
            title: 'Sunset',
            extras: {
              source: 'recommendations',
              isrc: 'USRC17607839',
            },
          },
        ],
      },
    ],
  },
]);
Must be JSON-serializable. Keep payloads small — for large blobs, keep them in your own JS state keyed by mediaId.

Nested browsing

Items can contain children to create drill-down navigation (e.g. albums containing tracks):

TrackPlayer.setBrowseTree([
  {
    mediaId: 'music',
    title: 'Music',
    items: [
      {
        mediaId: 'album-1',
        title: 'Chill Vibes',
        artist: 'Various Artists',
        artworkUrl: 'https://example.com/album-art.jpg',
        children: [
          {
            mediaId: 'track-1',
            url: 'https://example.com/track1.mp3',
            title: 'Sunset',
            artist: 'Artist A',
          },
          {
            mediaId: 'track-2',
            url: 'https://example.com/track2.mp3',
            title: 'Moonrise',
            artist: 'Artist B',
          },
        ],
      },
    ],
  },
]);

When a user selects a playable item, its siblings become the queue. Maximum nesting depth is 4 levels (matching CarPlay’s navigation stack limit).

iOS — CarPlay

CarPlay lets users browse and play audio from your app on the car’s built-in display. CarPlay requires adopting the iOS scene lifecycle — your app needs a PhoneSceneDelegate for the phone UI and RNTP provides RNTPCarPlaySceneDelegate for the car display.

1. Add the CarPlay entitlement

In Xcode, open your target’s Signing & Capabilities tab and add the CarPlay Audio entitlement. This inserts the following into your .entitlements file:

<key>com.apple.developer.carplay-audio</key>
<true/>

2. Adopt the scene lifecycle

CarPlay uses iOS scenes. This means your app’s window creation must move from AppDelegate to a PhoneSceneDelegate. This is a one-time migration.

Update AppDelegate.swift — remove the window creation, keep only the factory setup:

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
  var reactNativeDelegate: ReactNativeDelegate?
  var reactNativeFactory: RCTReactNativeFactory?

  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    let delegate = ReactNativeDelegate()
    let factory = RCTReactNativeFactory(delegate: delegate)
    delegate.dependencyProvider = RCTAppDependencyProvider()

    reactNativeDelegate = delegate
    reactNativeFactory = factory

    // Start React Native here, not in a scene, so a CarPlay-only launch still runs your JS.
    // default() is nil in Release builds, but this parameter is non-null.
    factory.rootViewFactory.initializeReactHost(
      launchOptions: launchOptions,
      devMenuConfiguration: RCTDevMenuConfiguration.default() ?? RCTDevMenuConfiguration()
    )

    return true
  }
}

Create PhoneSceneDelegate.swift in your app target — this takes over window creation:

import UIKit

class PhoneSceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?

  func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    guard let windowScene = scene as? UIWindowScene else { return }
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
          let factory = appDelegate.reactNativeFactory else { return }

    let window = UIWindow(windowScene: windowScene)
    factory.startReactNative(
      withModuleName: "YourAppName",
      in: window,
      launchOptions: nil
    )
    self.window = window
    window.makeKeyAndVisible()
  }
}

3. Add the scene manifest to Info.plist

Register both the phone and CarPlay scenes:

<key>UIApplicationSceneManifest</key>
<dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <false/>
  <key>UISceneConfigurations</key>
  <dict>
    <key>UIWindowSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneConfigurationName</key>
        <string>Phone</string>
        <key>UISceneClassName</key>
        <string>UIWindowScene</string>
        <key>UISceneDelegateClassName</key>
        <string>$(PRODUCT_MODULE_NAME).PhoneSceneDelegate</string>
      </dict>
    </array>
    <key>CPTemplateApplicationSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneConfigurationName</key>
        <string>CarPlay</string>
        <key>UISceneClassName</key>
        <string>CPTemplateApplicationScene</string>
        <key>UISceneDelegateClassName</key>
        <string>RNTPCarPlaySceneDelegate</string>
      </dict>
    </array>
  </dict>
</dict>

4. Set the browse tree

CarPlay uses the same setBrowseTree() call as Android Auto — no extra code required.

CarPlay behavior

  • Tabs — Each top-level category appears as a tab. CarPlay supports a maximum of 4 tabs; if you provide more, a “More” tab is automatically created for the overflow.
  • Playback — Selecting an item starts playback natively without any extra handling in your app.
  • Now Playing — The Now Playing screen is provided automatically by CarPlay using the metadata you set via MPNowPlayingInfoCenter (RNTP keeps this up to date).
  • Item limits — The number of items displayed per list varies by car manufacturer and CarPlay version. Keep lists concise where possible.
  • Updates — Publishing a new tree updates the lists in place. The driver stays where they are unless the list they’re on was removed or renamed.

Cold start and fresh content

A car can connect while your app isn’t running. RNTP persists the last tree you published with setBrowseTree, so Android Auto and CarPlay show it straight away. To replace it with fresh content, listen for Event.BrowseRefreshRequested inside registerPlaybackSession:

TrackPlayer.registerPlaybackSession(() => {
  TrackPlayer.addEventListener(Event.BrowseRefreshRequested, async () => {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 10_000);
    try {
      TrackPlayer.setBrowseTree(await fetchBrowseTree(controller.signal));
    } catch {
      // Keep the tree the car is already showing.
    } finally {
      clearTimeout(timeout);
    }
  });

  if (TrackPlayer.getBrowseTreeInfo().isEmpty) {
    TrackPlayer.setBrowseTree(bundledBrowseTree);
  }
});

The event fires when Android Auto first asks for your library on a connection, and when a CarPlay screen connects. Register the listener synchronously in the session so it exists before the event arrives. On Android the listener runs inside a background task with a 30 second ceiling, so timers and fetch work; bring your own shorter timeout like the one above. Android sends at most one refresh every 15 seconds.

Do the fetching inside the listener. On a cold start React Native keeps timers paused until a background task runs, so an async refresh started directly in the session can hang until something else wakes it.

Only publish a bundled baseline when getBrowseTreeInfo().isEmpty. Publishing it every time the session starts would replace a newer tree that an earlier refresh already stored.

TrackPlayer.getBrowseTreeInfo():BrowseTreeInfov5.10.0+

Reports whether native holds a persisted browse tree, and its revision, which increments on every accepted setBrowseTree call. Use this in registerPlaybackSession to avoid replacing a non-empty native snapshot with a bundled baseline. Web returns `{ isEmpty: true, revision: 0 }`.

Starting JavaScript from the car (Android)

Android Auto can browse and play without your JavaScript, using the persisted tree and native handling. Set android.autoStartJs: true in setupPlayer and RNTP starts your JavaScript in the background when a car browses your app or playback events need delivering, so session listeners and the refresh above run even after the app process was killed. Your entry file then runs without any UI, so keep registerPlaybackSession free of anything that needs a screen. The setting is persisted, which means it applies from the next cold start after your app has run once with it.

Starting JavaScript from CarPlay (iOS)

CarPlay launches your app process when it connects. Start React Native in application(_:didFinishLaunchingWithOptions:), as in the AppDelegate from step 2 above, rather than in the phone scene, so a launch that only has a CarPlay screen still runs your session.

Remote controls in-car

Android Auto and CarPlay drive RNTP’s native player. Browsing, playback and transport controls work without your JavaScript running.

SurfaceWhat runs
Browse → playThe queue loads natively. On Android the Play that follows goes to your play handler if you registered one; on CarPlay the selection plays directly. The resulting playback events reach your session listeners, and on Android a killed app starts JS for them when autoStartJs is on
In-car transport (play, skip, next, …)Your remote handlers when JavaScript is running and the player has connected, otherwise native handling. Buttons and skip intervals come from setCommands
Phone lock screen / notification (app backgrounded)The same session and the same handlers as in the car

Implications:

  1. setCommands is not “JS-only.” Call it from JS during setup or when your queue changes. RNTP saves the capabilities and skip intervals, so the car shows the right buttons and skips the right distance even when no JavaScript is running.
  2. Handlers need a running, set-up runtime. A press that arrives before your JavaScript has started and connected to the player runs natively. With android.autoStartJs, a cold start from the car boots JavaScript in the background, and handlers registered in your playback session take over once setupPlayer() has run in that same runtime, so call it from the playback session too, not only from a screen.
  3. Browse selection on CarPlay goes through RNTPCarPlaySceneDelegate straight into AudioPlayer and never calls a handler. Android Auto installs the queue in TrackPlayerPlaybackService, then sends Play through the same path as the Play button, so a play handler sees it.

Fixed podcast-style skip intervals need no handler at all:

import { PlayerCommand } from '@rntp/player';

TrackPlayer.setCommands({
  capabilities: [
    PlayerCommand.PlayPause,
    PlayerCommand.SkipForward,
    PlayerCommand.SkipBackward,
  ],
  forwardInterval: 30,
  backwardInterval: 10,
});

Some head units only show next/previous track buttons. To make them skip time instead, register next and previous handlers that call TrackPlayer.seekBy(30) and TrackPlayer.seekBy(-10). Like any handler, this only applies while your JavaScript is running and set up.

See also Events → Remote control handlers.

ende