Skip to main content

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 integer0xFF1DB954.

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:

  1. withPersistence(service, storage) — it writes the native mirror the service reads with no JS alive.

  2. MediaService.init(...) reachable at JS module scope, in a module your entry file imports for its side effects (import './src/playback' in index.js). A revived runtime loads your bundle but mounts no component, so an init inside a useEffect never runs — and Metro's release-mode inline requires goes further: an import { 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.

  3. 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.

  4. media3's MediaButtonReceiver in your AndroidManifest.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

ParameterType
methodkeyof MediaHandler
errorunknown

Returns

void