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" /> 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:
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).
| Field | Type | Default | Description |
|---|---|---|---|
| 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).
| Field | Type | Default | Description |
|---|---|---|---|
| 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',
},
},
],
},
],
},
]); 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.
| Surface | What runs |
|---|---|
| Browse → play | Native 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:
setCommandsis not “JS-only.” Call it from JS during setup (or when your queue changes), but withhandling: 'native'the stored native config drives behavior in the car without ongoing JavaScript.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.- Browse selection on CarPlay goes through
RNTPCarPlaySceneDelegatestraight intoAudioPlayer; Android Auto usesTrackPlayerPlaybackService. Neither path invokesregisterBackgroundEventHandler.
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.