Skip to main content

Interface: MediaHandler

Defined in: packages/media-session/src/types.ts:262

The fan-in interface. Every remote surface — notification, lock screen, Bluetooth, headset, watch, Android Auto, Control Center, and the app's own UI — arrives here.

Every method may return a promise; the session dispatches and returns immediately, so a slow handler can never ANR the OS. A rejected promise is reported through MediaServiceConfig.onHandlerError, never swallowed.

Methods

customAction()

customAction(name: string, extras?: Record<string, unknown>): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:384

One of PlaybackState.customActions was pressed.

Android only. MPRemoteCommandCenter's command set is fixed and carries no app-defined identifier, so no iOS remote surface can invoke a custom action and this is never called there by the session — see MediaCustomAction. Call it from your own UI if you want one code path on both platforms.

Parameters

ParameterTypeDescription
namestring-
extras?Record<string, unknown>the controller's payload, or undefined when there is none. Only Android controllers can send one.

Returns

void | Promise<void>


getChildren()

getChildren(parentId: string): Promise<BrowseItem[]>;

Defined in: packages/media-session/src/types.ts:481

The children of a browsable node — one screen of the car's browse tree.

Called with BROWSE_ROOT for the root tabs, then with whatever id the user drilled into. The car hands ids back verbatim; nothing native parses them.

Unlike every other method here this one is awaited: a browser expects an answer (media3 returns a ListenableFuture, CarPlay fills a list after the push), and it is the browser being kept waiting, not a finger on a button. Answer fast anyway — Android Auto shows a spinner until you do.

Return [] for "this node has nothing under it" — Google's own guidance is to prefer an empty list over an error code. Throw (or reject with) a BrowseError for "I cannot answer this": a sign-in screen instead of an empty one.

The root is capped at four browsable tabs on both platforms (BROWSE_ROOT), and anything dropped is reported on the session-error channel as browseRootRejected rather than vanishing.

Parameters

ParameterType
parentIdstring

Returns

Promise<BrowseItem[]>


getMediaItem()

getMediaItem(id: string): Promise<BrowseItem | undefined>;

Defined in: packages/media-session/src/types.ts:489

One browse node by id — Android's onGetItem, CarPlay refreshing a row.

undefined means "no such item", which the car renders as a missing row rather than an error. Throw a BrowseError for "it exists and you may not have it".

Parameters

ParameterType
idstring

Returns

Promise<BrowseItem | undefined>


onAdjustDeviceVolume()?

optional onAdjustDeviceVolume(direction: RemoteVolumeDirection): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:354

A hardware volume key moved one notch on a backend that can only be nudged — volumeControl: 'relative'.

Most apps never implement this. With the default 'absolute' the library turns the notch into a level itself (one 1 / steps step from the last published volume, quantised and clamped) and delivers onSetDeviceVolume instead — so a Cast or UPnP backend needs one method, not two, and the step arithmetic is written and tested once rather than in every app.

Which of the two you get is decided by what you published, never by which methods you defined: 'relative' → this, 'absolute'onSetDeviceVolume, 'fixed' → neither.

Either way, the routing is the point. With the app backgrounded or the screen locked there is no Activity to receive a key event; the platform hands the press to the media session's volume provider, which exists only because MediaServiceApi.setRemotePlayback made the session advertise remote playback.

Android only — see onSetDeviceVolume.

Parameters

ParameterType
directionRemoteVolumeDirection

Returns

void | Promise<void>


onPlaybackResumption()?

optional onPlaybackResumption(): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:427

Android only: this JS runtime was started by the media service to complete a playback resumption after the process had been killed.

By the time this runs the notification is already on screen, the session already carries the persisted track and position, and the play the user pressed is about to be replayed on this handler. So there is nothing you have to do here — it exists so an app can log it, fire an analytics event, or refresh a token before the replayed play() needs one.

Optional for the same reason onSleepTimer is: an informational callback added after v1 must not break structural implementors of this interface.

Requires android.playbackResumption: true, withPersistence, and MediaService.init at module scope — see MediaServiceConfig.android.

Returns

void | Promise<void>


onSessionError()?

optional onSessionError(error: SessionError): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:456

The session failed at something, and there was no call to reject.

The one method here that is not a user gesture. Everything on this channel used to be a native log line — the OS refusing a foreground service, an artwork download that came back empty, a drawable name that does not resolve, a resumption that never completed — so an app could ship a broken background story and only find out from a bug report. See SessionError and, for the per-platform emission rules, the codes themselves.

Nothing here is required. Every code names a degradation the session has already handled as well as it can; implementing this changes no behaviour, it only makes the failure visible. It is void/Promise<void> and fire-and-forget like the rest: a throw or a rejection is routed to MediaServiceConfig.onHandlerError (as 'onSessionError') and can never take the session down, and it cannot re-enter this channel.

Never silently dropped. A handler that does not implement it — including a CompositeMediaHandler whose inner handler does not — gets a console.error floor instead, because a swallowed error channel would be a worse bug than the ones it reports.

Optional for the reason onSleepTimer is: this interface is the player-agnostic contract, and a method added after v1 must not break structural implementors. BaseMediaHandler supplies the console floor.

Parameters

ParameterType
errorSessionError

Returns

void | Promise<void>


onSetDeviceMuted()?

optional onSetDeviceMuted(muted: boolean): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:362

A remote surface asked to mute or unmute the remote device. Same request/acknowledge contract as onSetDeviceVolume, and Android only for the same reason.

Parameters

ParameterType
mutedboolean

Returns

void | Promise<void>


onSetDeviceVolume()?

optional onSetDeviceVolume(volume: number): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:330

A remote surface asked for an absolute volume on the remote device, 0..1.

Reachable only while MediaServiceApi.setRemotePlayback has published a device with volumeControl: 'absolute' — from the surfaces that express a level (Android's remote volume dialog, the output switcher, a MediaController) and from the hardware volume keys, whose notch the library converts for you (see onAdjustDeviceVolume).

A request, not a fact — the same contract as every other handler method. Move the backend, then republish through setRemotePlayback; that republish is what moves the slider on every surface.

Android only. iOS gives an app no way to take over the hardware volume buttons and MPRemoteCommandCenter has no volume command at all, so setRemotePlayback is a documented no-op there and this — like onAdjustDeviceVolume and onSetDeviceMuted — is never invoked by the session on iOS. Drive a remote device's volume from your own in-app slider there, which is what Google's own iOS cast apps do.

Optional for the reason onSleepTimer is: a method added after v1 must not break structural implementors. BaseMediaHandler supplies a no-op.

Parameters

ParameterType
volumenumber

Returns

void | Promise<void>


onSetRepeatMode()?

optional onSetRepeatMode(mode: MediaRepeatMode): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:298

A remote surface asked for a different repeat mode.

A request, not a fact. Nothing has changed until the app changes it and broadcasts a setPlaybackState carrying the new repeatMode — the same acknowledgement contract play/pause follow, and on Android it is literally what completes media3's pending-operation future.

Only reachable when the app advertises the setRepeatMode capability.

Optional for the reason onSleepTimer is: this interface is the player-agnostic contract, and a method added after v1 must not break structural implementors. BaseMediaHandler supplies a no-op.

Parameters

ParameterType
modeMediaRepeatMode

Returns

void | Promise<void>


onSetShuffle()?

optional onSetShuffle(enabled: boolean): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:304

A remote surface asked to turn shuffle on or off. See onSetRepeatMode — same request/acknowledge contract, gated on the setShuffle capability, optional for the same reason.

Parameters

ParameterType
enabledboolean

Returns

void | Promise<void>


onSleepTimer()?

optional onSleepTimer(): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:407

The native sleep timer elapsed.

Playback is already paused when this runs. The timer fires on a platform timer (Handler.postDelayed / DispatchQueue.main.asyncAfter), pauses natively on the same path a notification pause takes, and only then calls this — the ordering is deliberate and is the reason the feature is native at all: your setTimeout would not have fired (see the README's background-playback limits).

So this is where a timer badge is cleared, an analytics event is logged, or stopService() is called if you would rather end background execution than sit paused. Pausing again here is harmless but redundant.

Optional: the pause has already happened natively by the time this fires, so a handler with nothing to add can simply omit it — structural implementors of this interface (the player-agnostic contract) must not break when the library grows an informational callback.

Returns

void | Promise<void>


onTaskRemoved()

onTaskRemoved(): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:371

Android only: the app's task was swiped out of Recents.

The native default policy (keep playing if playing, otherwise stop the service) has already been decided by the time this runs — overriding this method is for side effects (persistence, analytics), not for changing that decision. Call stopService() here to force a stop.

Returns

void | Promise<void>


pause()

pause(): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:264

Returns

void | Promise<void>


play()

play(): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:263

Returns

void | Promise<void>


playFromMediaId()

playFromMediaId(id: string): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:502

A car, a head unit or an assistant asked to play a browse item.

Build the queue around id, broadcast it, start playback — the same acknowledge-by-broadcast contract as play: nothing on any surface moves until the app's next setQueue/setPlaybackState says so.

Not optional, and deliberately: a browse tree whose leaves do nothing when tapped is the purest silent no-op in this package. BaseMediaHandler's default therefore reports playFromMediaIdUnhandled on the session-error channel instead of returning quietly.

Parameters

ParameterType
idstring

Returns

void | Promise<void>


playFromSearch()?

optional playFromSearch(query: string, focus: SearchFocus): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:515

"Play some jazz" — a voice query from Assistant or the head unit's mic.

query may be '', which is Assistant's bare "play music": resume, or pick something. focus carries what the assistant managed to classify (artist, album, genre…), and is { kind: 'any' } when it classified nothing.

Optional, and the absence is advertised: a handler without this method makes the session answer voice playback requests with ERROR_NOT_SUPPORTED rather than playing something arbitrary.

Parameters

ParameterType
querystring
focusSearchFocus

Returns

void | Promise<void>


optional search(query: string): Promise<BrowseItem[]>;

Defined in: packages/media-session/src/types.ts:528

Browsable results for the car's search tab.

Optional, and the absence is advertised: without it the session drops COMMAND_CODE_LIBRARY_SEARCH from what browsers may use, which is the only thing that makes media3's legacy stub publish android.media.browse.SEARCH_SUPPORTED = false — and that key alone is what hides Android Auto's search tab.

Never called on iOS: CarPlay audio apps have no search template, so there is no surface to type into. A missing surface, not a missing feature.

Parameters

ParameterType
querystring

Returns

Promise<BrowseItem[]>


seekTo()

seekTo(position: number): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:268

Parameters

ParameterTypeDescription
positionnumbermilliseconds

Returns

void | Promise<void>


setRate()

setRate(rate: number): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:283

Parameters

ParameterType
ratenumber

Returns

void | Promise<void>


skipToNext()

skipToNext(): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:269

Returns

void | Promise<void>


skipToPrevious()

skipToPrevious(): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:270

Returns

void | Promise<void>


skipToQueueItem()

skipToQueueItem(index: number): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:282

Play an arbitrary entry of the broadcast queue.

Reached from a remote surface on Android only — Android Auto, Wear, a car head unit, any controller that renders the queue (Player.COMMAND_SEEK_TO_MEDIA_ITEM). MPRemoteCommandCenter has no queue-jump command, so no iOS remote surface can invoke this; it is still part of the interface because your own UI calls it on both platforms.

Parameters

ParameterTypeDescription
indexnumberindex into the last broadcast queue

Returns

void | Promise<void>


stop()

stop(): void | Promise<void>;

Defined in: packages/media-session/src/types.ts:266

Release resources. Does NOT end background execution — call stopService() for that.

Returns

void | Promise<void>