Events

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: UI foreground; registerBackgroundEventHandler when backgrounded. - 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.

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 use addEventListener on iOS (and on Android while the UI is foreground). On Android with the UI backgrounded, handle them in registerBackgroundEventHandler instead.

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

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. - 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. - UI foreground on Android: use addEventListener instead. - 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.

By default (handling: 'native'), play, pause, skip, and seek work without JavaScript — you do not need a background handler for basic playback.

Register one when you want to run JS yourself — for example to update metadata, implement custom skip logic on the phone, handle progressSync ticks, or use handling: 'js' / 'hybrid' for commands that emit to JS. On Android, this runs while the app UI is backgrounded (not when the whole process is force-stopped); on iOS the function is a no-op — use addEventListener for all events (including while audio plays in the background with UIBackgroundModes audio).

This handler 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