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
| Parameter | Type | Description |
|---|---|---|
name | string | - |
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
| Parameter | Type |
|---|---|
parentId | string |
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
| Parameter | Type |
|---|---|
id | string |
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
| Parameter | Type |
|---|---|
direction | RemoteVolumeDirection |
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
| Parameter | Type |
|---|---|
error | SessionError |
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
| Parameter | Type |
|---|---|
muted | boolean |
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
| Parameter | Type |
|---|---|
volume | number |
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
| Parameter | Type |
|---|---|
mode | MediaRepeatMode |
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
| Parameter | Type |
|---|---|
enabled | boolean |
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
| Parameter | Type |
|---|---|
id | string |
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
| Parameter | Type |
|---|---|
query | string |
focus | SearchFocus |
Returns
void | Promise<void>
search()?
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
| Parameter | Type |
|---|---|
query | string |
Returns
Promise<BrowseItem[]>
seekTo()
seekTo(position: number): void | Promise<void>;
Defined in: packages/media-session/src/types.ts:268
Parameters
| Parameter | Type | Description |
|---|---|---|
position | number | milliseconds |
Returns
void | Promise<void>
setRate()
setRate(rate: number): void | Promise<void>;
Defined in: packages/media-session/src/types.ts:283
Parameters
| Parameter | Type |
|---|---|
rate | number |
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
| Parameter | Type | Description |
|---|---|---|
index | number | index 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>