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. - Maximum nesting depth: 4 levels (matching CarPlay's navigation stack limit). - Call again to update the tree (e.g. after loading new content)

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 * stringUnique identifier for this category.
title * stringDisplay 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 * stringUnique identifier for this item.
title * stringDisplay title.
artist stringArtist or subtitle.
artworkUrl MediaUrlArtwork image URL.
url MediaUrlAudio source URL. If present, this item is playable.
duration numberDuration hint in seconds.
isLive booleanWhether this is a live stream.
mimeType stringMIME 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

    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.

Remote controls in-car

Android Auto and CarPlay integrate with RNTP’s native player, not with your React JS bundle.

SurfaceWhat runs
Browse → playNative queue load and playback (no addEventListener, no background handler)
In-car transport (play, skip, next, …)Native handling from setCommands (handling, capabilities, skip intervals)
Phone lock screen / notification (app backgrounded)Same native session on Android; optional registerBackgroundEventHandler only if you use handling: 'js' / 'hybrid' and events are emitted

Implications:

  1. setCommands is not “JS-only.” Call it from JS during setup (or when your queue changes), but with handling: 'native' the stored native config drives behavior in the car without ongoing JavaScript.
  2. handling: 'js' (or hybrid overrides to 'js') is for the phone when you need custom logic and the RN runtime is up. Do not rely on it for Android Auto or CarPlay — native will skip the built-in action and your handler may never run.
  3. Browse selection on CarPlay goes through RNTPCarPlaySceneDelegate straight into AudioPlayer; Android Auto uses TrackPlayerPlaybackService. Neither path invokes registerBackgroundEventHandler.

For podcast-style fixed skip intervals without custom JS, prefer native mode:

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

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

Some head units still expose next/previous track actions; remapping those to time skips is not configurable from JS today and requires native changes if you need it in-car.

See also Events → Remote control events.

ende