Interface: MediaServiceConfig
Defined in: packages/media-session/src/types.ts:649
Configuration accepted by MediaService.init.
Properties
android?
optional android?: {
notificationChannelId: string;
notificationChannelName: string;
notificationColor?: number;
notificationIcon?: string;
onRevivalRequested?: () => void;
playbackResumption?: boolean;
stopForegroundOnPause?: boolean;
stopForegroundTimeoutMs?: number;
};
Defined in: packages/media-session/src/types.ts:650
notificationChannelId
notificationChannelId: string;
notificationChannelName
notificationChannelName: string;
notificationColor?
optional notificationColor?: number;
Notification accent colour as an ARGB integer — 0xFF1DB954.
Include the alpha byte: 0x1DB954 is transparent black. Applied to
Notification.color after media3 has built the notification. Android 12+
media shades often derive their own palette from the artwork and may
ignore it, so it is a hint rather than a guarantee. Ignored on iOS, which
has no colour surface — the lock screen's palette comes from the artwork.
notificationIcon?
optional notificationIcon?: string;
Drawable resource name in the consumer app, e.g. 'ic_notification'.
onRevivalRequested?
optional onRevivalRequested?: () => void;
Called when the media service needs this app to run its init path
again, now — the missing half of playbackResumption for a JS
runtime that is still alive.
The scenario, concretely
The user presses your stop button; stopService() ends the session and
clears the handlers, but the System UI keeps offering its media
resumption card (stop ends background execution, not the user's place
in the album — the persisted record deliberately survives). The user
taps play on that card. The OS starts the media service into your
still-running process; the service rebuilds the session from the
persisted mirror and waits for MediaService.init. In a killed
process that init arrives by itself — booting the runtime re-runs your
module scope. In an alive process the module scope already ran and
will never run again, so without this callback the revival times out
after 10 s and the card's play button silently does nothing.
What to do in it
Run exactly what your module scope runs — your idempotent "bring the
session up" path, ending in MediaService.init(...) (plus your
withPersistence wrapping and catch-up broadcasts). It is invoked only
while the service is actually waiting (never while an init is already
in flight or the session is up), so calling init from it is safe:
android: {
playbackResumption: true,
onRevivalRequested: () => void playback.start(),
}
Lifetime
Registered at init and — unlike the handlers — retained across
stopService(), because the window it exists for is precisely
"after stop, before the next init". Replaced by the next init;
dropped with the runtime on a dev reload. Never invoked on iOS, which
has no service to revive (see playbackResumption).
Returns
void
playbackResumption?
optional playbackResumption?: boolean;
Let the media service come back after the process was killed, from the System UI resumption card, a Bluetooth reconnect or a headset play button — booting the JS runtime behind it.
Default
false — opt-in until it is proven on more hardware than ours.
Requires all four, and says so in the log when one is missing:
-
withPersistence(service, storage)— it writes the native mirror the service reads with no JS alive. -
MediaService.init(...)reachable at JS module scope, in a module your entry file imports for its side effects (import './src/playback'inindex.js). A revived runtime loads your bundle but mounts no component, so aninitinside auseEffectnever runs — and Metro's release-mode inline requires goes further: animport { x } from './m'whose bindings are only used inside a component defers./m's module scope to the first render, which a headless runtime never performs. Only a bare side-effect import in the entry file's own graph is guaranteed to execute at bundle load. -
onRevivalRequested — the same recovery for a runtime that is still alive: after
stopService()the resumption card can start the service into a process whose module scope already ran and cannot run again, so the service asks the app to re-initialize instead. -
media3's
MediaButtonReceiverin yourAndroidManifest.xml:<receiver android:name="androidx.media3.session.MediaButtonReceiver"android:exported="true"><intent-filter><action android:name="android.intent.action.MEDIA_BUTTON" /></intent-filter></receiver>That declaration is what makes media3 advertise resumption to the System UI at all; it is deliberately not merged in from this library, because it changes how media buttons are routed for every app that installs the package.
The platform story, so the asymmetry is not mistaken for a gap
- Both platforms:
withPersistence/restorePersisted— the same record, the same behaviour. That is the cross-platform feature. - Android: this flag adds an automatic consumer of that record — the resumption card, Bluetooth, a media button revive the process for you.
- iOS: the consumer of the same record is the next manual launch — the user opens the app and it is paused where they left it. An automatic iOS twin cannot exist: a terminated iOS app stays terminated, because force-quit is read as user intent and nothing may resurrect a process for playback. Apple's policy, not a missing feature here.
If that ever changes the flag has a natural home at
config.ios.playbackResumption; it is namespaced under android on
purpose, not by accident.
stopForegroundOnPause?
optional stopForegroundOnPause?: boolean;
stopForeground(STOP_FOREGROUND_DETACH) on pause: notification survives,
service is demoted and therefore killable.
Default
true
stopForegroundTimeoutMs?
optional stopForegroundTimeoutMs?: number;
How long a paused service stays foreground before media3 demotes it, in
milliseconds. Omit for media3's default of 10 minutes; 0 demotes
immediately.
Maps to MediaSessionService.setForegroundServiceTimeoutMs. Must be
>= 0; media3 clamps anything above 600 000 back down to 600 000, so
that is the real ceiling. Shorter = the process (and your JS handler)
becomes reclaimable sooner after a pause; longer = a resume from the
notification is more likely to find everything still alive. Pair a short
timeout with withPersistence.
ios?
optional ios?: {
artworkCacheSize?: number;
supportedPlaybackRates?: number[];
};
Defined in: packages/media-session/src/types.ts:782
artworkCacheSize?
optional artworkCacheSize?: number;
Decoded-artwork cache capacity.
Default
8
supportedPlaybackRates?
optional supportedPlaybackRates?: number[];
Playback rates the lock-screen rate control offers, ascending.
MPChangePlaybackRateCommand.supportedPlaybackRates is a fixed list and
iOS snaps the user's choice to a member of it, so an audiobook app that
offers 1.0/1.25/1.5/1.75/2.0/3.0 cannot say so without this.
Default
[0.5, 0.75, 1, 1.25, 1.5, 2]
Namespaced under ios because there is genuinely no Android twin: media3
takes an arbitrary float through COMMAND_SET_SPEED_AND_PITCH and its
notification draws no rate control at all, so there is no list to hand it.
Setting it on Android is harmless and does nothing.
jumpBackwardSeconds?
optional jumpBackwardSeconds?: number;
Defined in: packages/media-session/src/types.ts:828
How far the rewind control jumps, in seconds.
Default
15
15 — not media3's 5 — is the deliberate shared default: it matches RNTP V4
(backwardJumpInterval) and V5 (backwardInterval), it is what this
package already did on iOS, and a symmetric pair cannot surprise someone who
sets one and forgets the other. Podcast and audiobook apps set 30 here
explicitly, which is the whole reason the knob exists.
jumpForwardSeconds?
optional jumpForwardSeconds?: number;
Defined in: packages/media-session/src/types.ts:818
How far the fastForward control jumps, in seconds.
Default
15
Cross-platform, and top-level rather than per-platform because that is exactly the point of it: before this option existed the two platforms disagreed from the same JS call — iOS pinned 15 s in both directions while Android set no increment and inherited media3's 5 s back / 15 s forward.
Android: SimpleBasePlayer.State.Builder.setSeekForwardIncrementMs.
iOS: MPSkipIntervalCommand.preferredIntervals.
onHandlerError?
optional onHandlerError?: (method: keyof MediaHandler, error: unknown) => void;
Defined in: packages/media-session/src/types.ts:834
Called when a handler method throws or rejects. Defaults to console.error.
There is no other place for the error to go — handler invocations are
fire-and-forget by design.
Parameters
| Parameter | Type |
|---|---|
method | keyof MediaHandler |
error | unknown |
Returns
void