Events
addEventListener
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
| Event | Value | Description |
|---|---|---|
| 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.
| Field | Type | Default | Description |
|---|---|---|---|
| state * | PlaybackState | — | — |
TrackPlayer.addEventListener(Event.PlaybackStateChanged, ({ state }) => {
console.log('New state:', state);
}); IsPlayingChanged
Fires when playing status toggles.
| Field | Type | Default | Description |
|---|---|---|---|
| playing * | boolean | — | — |
TrackPlayer.addEventListener(Event.IsPlayingChanged, ({ playing }) => {
console.log('Playing:', playing);
}); MediaItemTransition
Fires when the active queue item changes.
| Field | Type | Default | Description |
|---|---|---|---|
| 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+
| Field | Type | Default | Description |
|---|---|---|---|
| title | string | — | — |
| artist | string | — | — |
| albumTitle | string | — | — |
| artworkUrl | string | — | — |
| genre | string | — | — |
TrackPlayer.addEventListener(Event.MediaMetadataChanged, ({ title, artist }) => {
console.log('Now playing:', title, artist);
}); 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`).
| Field | Type | Default | Description |
|---|---|---|---|
| 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.
| Field | Type | Default | Description |
|---|---|---|---|
| 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.
| Field | Type | Default | Description |
|---|---|---|---|
| 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.
| Field | Type | Default | Description |
|---|---|---|---|
| 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.
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
| Field | Type | Default | Description |
|---|---|---|---|
| position * | number | — | — |
RemoteSkipForward
| Field | Type | Default | Description |
|---|---|---|---|
| interval * | number | — | — |
RemoteSkipBackward
| Field | Type | Default | Description |
|---|---|---|---|
| interval * | number | — | — |
Background event handler
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.
BackgroundEvent (event.type + fields).
See changelog if you previously used raw { event, payload }.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;
}
});