Events

Two APIs, one rule: a listener lives as long as the place you register it.

registerPlaybackSession

TrackPlayer.registerPlaybackSession(session:() => void | Promise<void>):voidv5.9.2+

Registers the app's playback session logic: a function that runs immediately at registration and is the process-scoped home for playback-critical code. Call once from `index.js`, before `AppRegistry.registerComponent`. Listeners registered inside it live as long as the JS runtime — they survive UI unmounts (app swiped away while music continues), unlike listeners registered in components. - Events reach listeners registered here in the foreground and in the background. On Android, background delivery arrives through the library's built-in forwarding; on iOS, ordinary emitter delivery continues during audio background playback. - If the app also registers registerBackgroundEventHandler, background events go exclusively to that handler. Session listeners receive events again when the app returns to the foreground. Use one registration style or the other. - On Android, async listeners registered inside the session are awaited during background delivery with a guaranteed execution window. - Apps typically call this once from `index.js`, but multiple calls are allowed and each invokes its session function.

The home for your playback logic. Register it from index.js; the function runs at startup, and listeners you add inside it stay alive as long as JS is running. That includes while the app is backgrounded, and after the user swipes it away while music keeps playing. On Android, a listener that returns a promise is awaited in the background, so async work like saving progress gets a guaranteed window to finish. It works the same on iOS, Android, and web.

import TrackPlayer, { Event } from '@rntp/player';

// index.js — before AppRegistry.registerComponent
TrackPlayer.registerPlaybackSession(() => {
  TrackPlayer.addEventListener(Event.PlaybackProgressUpdated, ({ mediaId, position }) => {
    saveProgress(mediaId, position);
  });
  TrackPlayer.addEventListener(Event.PlaybackError, ({ code }) => {
    if (code === 'network') TrackPlayer.retry();
  });
});

addEventListener

TrackPlayer.addEventListener(event:T,listener:EventPayloadByEvent[T] extends never ? () => void : (event: EventPayloadByEvent[T]) => void):EmitterSubscription

Subscribe to player events (payload only; the event name is the first argument). - iOS: foreground and audio background (UIBackgroundModes `audio`). - Android: events go to registerBackgroundEventHandler when the app is not foregrounded; if no background handler is registered, they arrive here instead. - On Android, while the app is backgrounded, a listener that returns a promise is awaited with a guaranteed execution window. The library holds a wakelock until it settles, the same as for the background handler. - Listeners registered inside components are removed when the UI unmounts (for example, when the app is swiped away while music continues); registerPlaybackSession is the recommended place for playback-critical listeners. - Remote control events are emitted only when setCommands uses `handling: 'js'` or `'hybrid'` for that command. - Android Auto and CarPlay do not invoke JS listeners; use native setCommands for in-car behavior.

Subscribe to a single event. In components, use it for display state; the listener is removed when the component unmounts.

import TrackPlayer, { Event } from '@rntp/player';
import { useEffect } from 'react';

useEffect(() => {
  const sub = TrackPlayer.addEventListener(Event.PlaybackError, (event) => {
    console.error('Playback error', event.code, event.message);
  });
  return () => sub.remove();
}, []);

Event catalogue

EventValueDescription
Event.PlaybackStateChanged 'event.playback-state-changed'
Event.IsPlayingChanged 'event.is-playing-changed'
Event.MediaItemTransition 'event.media-item-transition'
Event.MediaMetadataChanged 'event.media-metadata-changed'
Event.MetadataReceived 'event.metadata-received'
Event.PlaybackError 'event.playback-error'
Event.PlaybackProgressUpdated 'event.playback-progress-updated'
Event.QueueChanged 'event.queue-changed'
Event.RemotePlay 'event.remote-play'
Event.RemotePause 'event.remote-pause'
Event.RemoteNext 'event.remote-next'
Event.RemotePrevious 'event.remote-previous'
Event.RemoteStop 'event.remote-stop'
Event.RemoteSeek 'event.remote-seek'
Event.RemoteSkipForward 'event.remote-skip-forward'
Event.RemoteSkipBackward 'event.remote-skip-backward'
Event.SleepTimerTriggered 'event.sleep-timer-triggered'

Event payloads

PlaybackStateChanged

Fires when the playback state transitions.

FieldTypeDefaultDescription
state * PlaybackState
TrackPlayer.addEventListener(Event.PlaybackStateChanged, ({ state }) => {
  console.log('New state:', state);
});

IsPlayingChanged

Fires when playing status toggles.

FieldTypeDefaultDescription
playing * boolean
TrackPlayer.addEventListener(Event.IsPlayingChanged, ({ playing }) => {
  console.log('Playing:', playing);
});

MediaItemTransition

Fires when the active queue item changes.

FieldTypeDefaultDescription
item * MediaItem | null
index * number
TrackPlayer.addEventListener(Event.MediaItemTransition, ({ item, index }) => {
  console.log('Now playing:', item?.title, 'at index', index);
});

MediaMetadataChanged

Fires when the effective metadata of the currently active media item changes — the view that backs getActiveMediaItem() and the system Now Playing info (lock screen, Bluetooth, CarPlay / Android Auto). This is the event most apps want: it accounts for track transitions, explicit updateMetadata calls, and the auto-update path that merges incoming stream metadata into the queued item.

Effective metadata for the active item (getActiveMediaItem, Now Playing). Use MetadataReceivedEvent for raw per-frame stream metadata instead. v5.1.0+

FieldTypeDefaultDescription
title string
artist string
albumTitle string
artworkUrl string
genre string
TrackPlayer.addEventListener(Event.MediaMetadataChanged, ({ title, artist }) => {
  console.log('Now playing:', title, artist);
});
The `useActiveMediaItem` hook subscribes to this event internally, so UI that uses the hook stays in sync with the lock screen automatically.

MetadataReceived

Fires for every metadata frame received off the wire (ICY blocks on Shoutcast/Icecast streams, ID3 frames in MP3/AAC streams, Vorbis comments, …). The payload reflects stream-derived fields only and is not merged with the queued media item — for the merged effective view, subscribe to MediaMetadataChanged.

Useful for analytics / scrobbling and for sanitization pipelines that want to filter stream metadata before writing it back (combine with autoUpdateMetadataFromStream: false and call updateMetadata yourself).

Fired for every metadata frame the audio stream pushes — ICY blocks (Shoutcast/Icecast radio), ID3 tags, Vorbis comments, QuickTime metadata. The payload reflects *stream-derived* fields only; user-supplied fields on the queued MediaItem (e.g. a static `genre` or `albumTitle`) are not merged in here. For the effective merged view that `getActiveMediaItem` returns (and that the lock screen / system Now Playing info reflects), subscribe to MediaMetadataChangedEvent instead. Typical consumers: analytics / scrobbling, sanitization pipelines that filter the raw stream before calling updateMetadata themselves (use with `PlayerConfig.autoUpdateMetadataFromStream: false`).

FieldTypeDefaultDescription
title string
artist string
albumTitle string
artworkUrl string
genre string
TrackPlayer.addEventListener(Event.MetadataReceived, ({ title, artist }) => {
  console.log('Stream pushed:', title, artist);
});

PlaybackError

Fires when a playback error occurs.

FieldTypeDefaultDescription
code * PlaybackErrorCode
message * string
TrackPlayer.addEventListener(Event.PlaybackError, ({ code, message }) => {
  console.error(`[${code}] ${message}`);
  if (code === 'network') TrackPlayer.retry();
});

PlaybackProgressUpdated

Fires periodically during playback when progressSync is configured. Contains the same payload sent to the HTTP endpoint and saved natively.

FieldTypeDefaultDescription
mediaId * string
position * number
duration * number
timestamp * number
TrackPlayer.addEventListener(Event.PlaybackProgressUpdated, ({ mediaId, position, duration, timestamp }) => {
  console.log(`${mediaId}: ${position}/${duration}s at ${timestamp}`);
});

SleepTimerTriggered

Fires when a sleep timer pauses playback.

FieldTypeDefaultDescription
type * 'time' | 'mediaItem'Which sleep timer mode triggered: countdown or media-item boundary.
TrackPlayer.addEventListener(Event.SleepTimerTriggered, ({ type }) => {
  console.log(`Sleep timer fired (${type})`);
});

QueueChanged

Fires when the queue is modified. No payload.

TrackPlayer.addEventListener(Event.QueueChanged, () => {
  const queue = TrackPlayer.getQueue();
});

Remote control events

These fire in response to hardware buttons, lock screen controls, and notification actions on the phone (and similar surfaces that route through the same native session).

With default handling: 'native', the native player handles the press immediately — no JavaScript runs. Remote events are not emitted in that mode.

To intercept presses yourself, set handling to 'js' or 'hybrid' in setCommands, then listen for the Remote* events. Put these listeners in your playback session; remote presses usually happen while the app is in the background.

Android Auto & CarPlay: In-car browse and transport controls run in native code (Media3 session / CarPlay templates). They do not start the React Native JS runtime. addEventListener and registerBackgroundEventHandler will not run your custom remote logic there. Use handling: 'native' with the capabilities and skip intervals you need, or implement custom behavior in native code. See Android Auto & CarPlay → Remote controls in-car.

RemotePlay / RemotePause / RemoteNext / RemotePrevious / RemoteStop

All four fire with no payload.

RemoteSeek

FieldTypeDefaultDescription
position * number

RemoteSkipForward

FieldTypeDefaultDescription
interval * number

RemoteSkipBackward

FieldTypeDefaultDescription
interval * number

Background event handler (advanced)

TrackPlayer.registerBackgroundEventHandler(factory:() => BackgroundEventHandler):void

Android only. Headless JS handler while the app UI is backgrounded. Normalizes Headless payloads to BackgroundEvent before invoking the handler. - Use registerPlaybackSession instead; session listeners returning a promise get the same guaranteed background execution window. This handler is kept for backwards compatibility. - iOS: no-op — use addEventListener (including audio background with UIBackgroundModes `audio`). - Register from `index.js` before `AppRegistry.registerComponent`. Handler receives BackgroundEvent; keep work under ~5s. - When no handler is registered, background events are delivered to addEventListener listeners instead. - UI foreground on Android: events are delivered to addEventListener. - Does **not** run for Android Auto or CarPlay — in-car controls use native setCommands handling only. - Only receives remote events when setCommands uses `handling: 'js'` or `'hybrid'` for that command; default `native` performs actions without invoking this handler.

Use registerPlaybackSession instead. Session listeners that return a promise are awaited in the background with a guaranteed execution window (the library holds a wakelock until they settle), so this handler offers nothing extra. It is kept for backwards compatibility.

If you register it, it takes over: while the app is backgrounded on Android, events go to this handler only, and your session listeners see them again when the app returns to the foreground. It is Android-only (no-op on iOS) and does not cover Android Auto or CarPlay — see the callout under Remote control events above.

v5.3.0+: Android handlers receive BackgroundEvent (event.type + fields). See changelog if you previously used raw { event, payload }.
If registered, call it in app entry (e.g. index.js) before setupPlayer() and AppRegistry.registerComponent — not inside a component.
import TrackPlayer, { Event, type BackgroundEvent } from '@rntp/player';

// index.js — before AppRegistry.registerComponent
TrackPlayer.registerBackgroundEventHandler(() => async (event: BackgroundEvent) => {
  switch (event.type) {
    case Event.RemotePause:
      TrackPlayer.pause();
      break;
    case Event.PlaybackProgressUpdated:
      await TrackPlayer.updateMetadata(0, { title: `At ${event.position}s` });
      break;
  }
});
ende