Title: Android Auto / CarPlay cold start (app process killed): playback events (MediaItemTransition, MetadataReceived, PlaybackProgressUpdated) are never delivered to JS — headless task is never started
Environment
@rntp/player: 5.6.0
react-native: 0.85.3
- Platform: Android (Android Auto), New Architecture
- Background handler registered via
registerBackgroundEventHandler in index.js
Summary
When the app process is fully killed and the user starts playback from Android Auto (tapping a playable item in the browse tree), native playback works correctly (the browse tree is restored from SharedPreferences), but no playback events are ever delivered to JS. The headless JS task (TrackPlayerServiceBridge) is never started, so registerBackgroundEventHandler never fires. As a result, app-side logic that depends on these events does not run:
- Progress/stats reporting on
MediaItemTransition
- Dynamic queue extension (loading more tracks as the queue drains)
- ICY/stream metadata handling on
MetadataReceived
If the app has been foregrounded at least once (a React context exists), everything works. The failure is specific to a cold start where playback is initiated from the car while the RN runtime is not alive.
Expected behavior
When playback is started from Android Auto with the app process killed, playback events should start the headless JS task (TrackPlayerServiceBridge) so registerBackgroundEventHandler receives them — the same way events are delivered when the app is merely backgrounded.
Actual behavior
Events emitted by the playback service are silently dropped because there is no live React context yet, and the code path that would start the headless task is only reachable after a React context already exists. This is a chicken-and-egg: the mechanism that would boot JS requires JS to already be booted.
Root cause analysis
In android/.../extensions/ReactApplicationContextExt.kt:
- The playback service emits events via the
Context overload:
fun android.content.Context.emitEvent(event: EmitEvent) {
val reactContext = (applicationContext as? com.facebook.react.ReactApplication)
?.reactHost
?.currentReactContext as? ReactApplicationContext
reactContext?.emitEvent(event) // ← no-op when currentReactContext == null
}
On a cold start from Android Auto, currentReactContext is null, so reactContext?.emitEvent(event) is a no-op and the event is dropped.
- The logic that actually starts the headless task lives inside the
ReactApplicationContext overload, which can only be reached when a context already exists:
fun ReactApplicationContext.emitEvent(event: EmitEvent) {
if (!isAppOnForeground()) {
val service = Intent(applicationContext, TrackPlayerTaskService::class.java)
// ...putExtras(event)...
startService(service) // ← headless boot lives here
} else {
getJSModule(RCTDeviceEventEmitter::class.java).emit(...)
}
}
Because the Context.emitEvent fallback bails out when currentReactContext == null, the startService(TrackPlayerTaskService) path is never invoked on a cold start, and the headless task (TASK_NAME = "TrackPlayerServiceBridge", TrackPlayerTaskService) never runs.
Relevant call site: TrackPlayerPlaybackService.kt emits events (e.g. stream metadata in onMetadata, and MediaItemTransition) via this Context.emitEvent, all of which are dropped in the cold-start case.
Steps to reproduce
- Register a background handler in index.js:
TrackPlayer.registerBackgroundEventHandler(() => handleBackgroundEvent);
- Configure the browse tree with
setBrowseTree(...) including at least one playable item.
- Build/run on a device connected to Android Auto (or the Desktop Head Unit).
- Force-stop the app (
adb shell am force-stop <pkg>), or swipe it away so the process is killed.
- From Android Auto, tap a playable item to start playback.
- Observe: audio plays (native), but the JS background handler is never invoked — no
MediaItemTransition / MetadataReceived / PlaybackProgressUpdated are received. Add a log at the top of the registerBackgroundEventHandler callback to confirm it never fires.
Expected: the handler fires for each event once the headless task boots.
Suggested fix
Make the Context.emitEvent overload start the headless task when there is no live React context, instead of dropping the event. For example, when currentReactContext == null (and the app is not foreground), start TrackPlayerTaskService directly with the serialized event payload — mirroring what ReactApplicationContext.emitEvent does in the backgrounded case. That boots the RN runtime, runs index.js, registers the TrackPlayerServiceBridge headless task, and delivers the queued event.
Rough shape:
fun android.content.Context.emitEvent(event: EmitEvent) {
val reactContext = (applicationContext as? ReactApplication)
?.reactHost?.currentReactContext as? ReactApplicationContext
if (reactContext != null) {
reactContext.emitEvent(event)
} else {
// Cold start (e.g. Android Auto with app process killed): no React
// context yet, so start the headless task to boot JS and deliver the event.
val service = Intent(applicationContext, TrackPlayerTaskService::class.java).apply {
putExtra("event", event.type.value)
putExtra("payload", pairsToBundle(event.pairs()))
}
startService(service)
}
}
Title: Android Auto / CarPlay cold start (app process killed): playback events (
MediaItemTransition,MetadataReceived,PlaybackProgressUpdated) are never delivered to JS — headless task is never startedEnvironment
@rntp/player:5.6.0react-native:0.85.3registerBackgroundEventHandlerin index.jsSummary
When the app process is fully killed and the user starts playback from Android Auto (tapping a playable item in the browse tree), native playback works correctly (the browse tree is restored from
SharedPreferences), but no playback events are ever delivered to JS. The headless JS task (TrackPlayerServiceBridge) is never started, soregisterBackgroundEventHandlernever fires. As a result, app-side logic that depends on these events does not run:MediaItemTransitionMetadataReceivedIf the app has been foregrounded at least once (a React context exists), everything works. The failure is specific to a cold start where playback is initiated from the car while the RN runtime is not alive.
Expected behavior
When playback is started from Android Auto with the app process killed, playback events should start the headless JS task (
TrackPlayerServiceBridge) soregisterBackgroundEventHandlerreceives them — the same way events are delivered when the app is merely backgrounded.Actual behavior
Events emitted by the playback service are silently dropped because there is no live React context yet, and the code path that would start the headless task is only reachable after a React context already exists. This is a chicken-and-egg: the mechanism that would boot JS requires JS to already be booted.
Root cause analysis
In
android/.../extensions/ReactApplicationContextExt.kt:Contextoverload:On a cold start from Android Auto,
currentReactContextisnull, soreactContext?.emitEvent(event)is a no-op and the event is dropped.ReactApplicationContextoverload, which can only be reached when a context already exists:Because the
Context.emitEventfallback bails out whencurrentReactContext == null, thestartService(TrackPlayerTaskService)path is never invoked on a cold start, and the headless task (TASK_NAME = "TrackPlayerServiceBridge",TrackPlayerTaskService) never runs.Relevant call site: TrackPlayerPlaybackService.kt emits events (e.g. stream metadata in
onMetadata, andMediaItemTransition) via thisContext.emitEvent, all of which are dropped in the cold-start case.Steps to reproduce
setBrowseTree(...)including at least one playable item.adb shell am force-stop <pkg>), or swipe it away so the process is killed.MediaItemTransition/MetadataReceived/PlaybackProgressUpdatedare received. Add a log at the top of theregisterBackgroundEventHandlercallback to confirm it never fires.Expected: the handler fires for each event once the headless task boots.
Suggested fix
Make the
Context.emitEventoverload start the headless task when there is no live React context, instead of dropping the event. For example, whencurrentReactContext == null(and the app is not foreground), startTrackPlayerTaskServicedirectly with the serialized event payload — mirroring whatReactApplicationContext.emitEventdoes in the backgrounded case. That boots the RN runtime, runs index.js, registers theTrackPlayerServiceBridgeheadless task, and delivers the queued event.Rough shape: