Function: useVisualizer()
function useVisualizer(
player: Player | null | undefined,
options?: VisualizerOptions,
enabled?: boolean,
pauseWhenInactive?: boolean
): UseVisualizerResult;
Defined in: packages/player/src/hooks/useVisualizer.ts:146
Subscribe a component to the player's visualizer for as long as it is mounted.
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
player | Player | null | undefined | undefined | The player to tap, or undefined/null while one is still being created (the hook then does nothing, which keeps it usable directly with usePlayer()'s result). |
options? | VisualizerOptions | undefined | Per-subscriber tuning; see VisualizerOptions. |
enabled? | boolean | true | Set false to drop the subscription without unmounting — e.g. while the visualizer is off screen. Defaults to true. |
pauseWhenInactive? | boolean | true | Drop the subscription whenever the app leaves the foreground or the device's display goes off, and take it back when both are true again. Defaults to true, and should stay that way; see below. |
Returns
The newest VisualizerFrame, plus any typed error.
Remarks
This re-renders at the frame rate (up to options.fps, itself capped by
the device's ~20 Hz). That is the point of the hook, but it means the
component using it should be a small leaf that paints bars and nothing else.
For anything heavier, subscribe imperatively with
player.visualizer.subscribe() and drive an animated value instead of React
state.
Unsubscribing is what releases the platform effect, so unmounting (or
flipping enabled to false) genuinely returns the audio framework to its
idle state — there is no hidden capture left running.
Why pauseWhenInactive defaults to on. The frames are native callbacks,
so unlike a JS timer they do not freeze when the app goes to the
background — the sampler thread keeps delivering and this hook keeps calling
setState, at up to 60 Hz, against a display that cannot present anything.
Left alone through a long screen-off that is tens of thousands of pointless
renders and a matching amount of CPU and battery spent drawing to nobody, and
the app is busy working through them at the moment the user unlocks. Pausing
costs nothing while visible and removes the whole class of problem: because
the native tap is derived from the listener set, dropping the subscription
disarms mpv's ring and stops the sampler thread outright.
Why the gate is two signals, not one. AppState alone is not enough on
Android, and the failure is not theoretical: a screen-off soak on a Poco F4
(MIUI, charging) recorded AppState reporting the app foreground again
while the display stayed off — subscribed 11:25, paused 11:36, re-subscribed
11:43 with the screen still off, paused 11:53 — and the visualizer burned
65-80 % of a core in the window it was wrongly awake. So the hook ANDs
AppState with the platform's own display state
(ScreenStateSource — PowerManager.isInteractive() +
ACTION_SCREEN_ON/OFF on Android): either signal saying "inactive"
pauses, and both must say "active" to resume. On iOS the second signal is
a constant true and that is correct — locking an iPhone resigns the app's
active state and backgrounds it, and there is no iOS state where the app is
foreground-active with the display off, so AppState is already the display
truth there.
Audio is untouched by this. Only the visual feed pauses; playback, the media session and everything else keep running in the background exactly as before.
On resume the same options are used, so nothing has to be re-plumbed — but
the subscription is a new one, which means smoothing, peak caps and
auto-gain start from rest and UseVisualizerResult.frame is
undefined for a frame or two. Bars come back up from zero rather than
continuing mid-bounce.
Set it to false only for a surface that is genuinely painting while
inactive (a Live Activity, a widget, an external display). For a non-UI
consumer, the escape hatch is player.visualizer.subscribe() directly: the
imperative API is never AppState-gated, and never has been.
Example
function Bars({ player }: { player: Player }) {
const { frame, error } = useVisualizer(player, { bands: 24 })
if (error) return <Text>{error.message}</Text>
return (
<View style={{ flexDirection: 'row' }}>
{Array.from(frame?.bands ?? []).map((value, i) => (
<View key={i} style={{ height: 4 + value * 60, width: 6 }} />
))}
</View>
)
}