Skip to content

Android Auto / CarPlay cold start (app process killed): playback events (MediaItemTransition, MetadataReceived, PlaybackProgressUpdated) are never delivered to JS — headless task is never started #2662

Description

@neoassyrian

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:

  1. 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.

  1. 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

  1. Register a background handler in index.js:
    TrackPlayer.registerBackgroundEventHandler(() => handleBackgroundEvent);
  2. Configure the browse tree with setBrowseTree(...) including at least one playable item.
  3. Build/run on a device connected to Android Auto (or the Desktop Head Unit).
  4. Force-stop the app (adb shell am force-stop <pkg>), or swipe it away so the process is killed.
  5. From Android Auto, tap a playable item to start playback.
  6. 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)
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions