Skip to main content

Class: Player

Defined in: packages/player/src/player.ts:2255

The typed audio player.

One Player owns one mpv core. There is no singleton: create as many as you need, and destroy() each when you are done.

Example

const player = await Player.create({ volume: 0.8 })
const stop = player.onStateChange((state) => console.log(state.status))
await player.load('https://example.com/track.flac')
player.play()
// …
stop()
player.destroy()

Properties

playlist

readonly playlist: PlaylistApi;

Defined in: packages/player/src/player.ts:3134

Queue manipulation. See PlaylistApi.

Accessors

destroyed

Get Signature

get destroyed(): boolean;

Defined in: packages/player/src/player.ts:2530

Whether destroy has been called.

Returns

boolean


state

Get Signature

get state(): PlayerState;

Defined in: packages/player/src/player.ts:2506

The current immutable snapshot.

Returns

PlayerState


visualizer

Get Signature

get visualizer(): VisualizerController;

Defined in: packages/player/src/player.ts:4204

Real-time spectrum and waveform of this player's output.

Entirely lazy: reading this property, and reading player.visualizer.capabilities, allocate nothing. mpv's tap stays disarmed and no sampler thread exists until the first subscribe(); the last unsubscribe releases all of it.

Example
if (player.visualizer.capabilities.fft) {
const stop = player.visualizer.subscribe((frame) => {
paintBars(frame.bands) // 32 values in [0, 1], already smoothed
})
// …later
stop()
}
Remarks

Identical on Android and iOS, and it needs no permission. The samples come from mpv itself, through two properties added by this project's libmpv patch (pcm-tap, pcm-tap-frame) — the same source patch in both binary forks, tapped at the point where mpv hands audio to the device, so what you see is what is audible. capabilities.fft is false only when the linked libmpv predates the patch, and subscribe() then throws a typed unsupported error rather than silently doing nothing. See ARCHITECTURE §21.

Returns

VisualizerController

Methods

clearAudioFilters()

clearAudioFilters(): void;

Defined in: packages/player/src/player.ts:3512

Remove every filter set through setAudioFilters.

Equivalent to setAudioFilters([]); spelled out because "set the empty array" is not an obvious way to say it. The managed loudness-normalization entry is not a filter you set, so it survives — turn it off with setLoudnessNormalization(false).

Returns

void


clearError()

clearError(): boolean;

Defined in: packages/player/src/player.ts:2591

Dismiss a settled error from state, without pretending it did not happen.

Returns

boolean

true if there was an error to clear.

Remarks

You usually do not need this. state.error clears itself three ways — a new entry starting, playback restarting, or a deliberate stop — all of which are documented on PlayerState.error. It survives in exactly one situation: the last entry failed and nothing has happened since. This is the button for that, i.e. for a user dismissing a banner.

It clears state, never events. The error event has already fired and is already in your logs; nothing here suppresses a future one, replays a past one, or makes the failure un-happen. If you want fewer error events, the knob is PlayerOptions.retry, which changes what is a final failure — not this, which only changes what the UI is still showing.

status moves to 'idle', because error and status: 'error' are one fact and a snapshot carrying one without the other would be a lie. Listeners are notified exactly as for any other snapshot change.


command()

command(args: readonly string[]): Promise<void>;

Defined in: packages/player/src/player.ts:4091

Run an arbitrary mpv command.

Parameters

ParameterTypeDescription
argsreadonly string[]Command name followed by its arguments, all as strings.

Returns

Promise<void>

Throws

PlayerErrorException with a typed PlayerError.


destroy()

destroy(): void;

Defined in: packages/player/src/player.ts:4223

Destroy the player and its mpv core. Idempotent.

After this, every method throws a disposed PlayerError, all listeners are dropped, and the native batch listener detaches itself by returning false on its next invocation (the back-pressure contract in docs/specs/player-core.md §2.5).

Returns

void


getAudioFilters()

getAudioFilters(): string;

Defined in: packages/player/src/player.ts:3862

The chain mpv currently has, as mpv prints it.

This is a read-back of the raw af property, not a reconstruction: mpv serialises with the same rules compileAudioFilters uses, so for a chain set through Player.setAudioFilters this returns exactly the string that was written. Useful as an assertion in tests and on-device checks, and as the way to see a chain that was set through the raw escape hatch.

With setLoudnessNormalization on, the string ends with the managed @rnmedia_loudnorm:loudnorm=… entry — it is a real chain member, and this read-back is honest about the whole property.

Returns

string

The af value; '' when no filters are active.


getChapters()

getChapters(): readonly ChapterEntry[];

Defined in: packages/player/src/player.ts:3910

The current entry's chapters — title and start time, in file order.

Returns

readonly ChapterEntry[]

[] when the entry has no chapters (an ordinary music track), or when nothing is loaded. The two are the same answer to a caller.

Remarks

One bounded native read, on demand — one mpv_get_property("chapter-list", MPV_FORMAT_NODE), whatever the chapter count, and a pull rather than a subscription for exactly the reasons PlaylistApi.entries is (see there): it is a variable-size array that mpv already owns, it changes only when the entry changes, and a snapshot of it in PlayerState would be a second copy on the bridge.

The cursor — which chapter is playing now — is PlayerState.chapter, updated from mpv's observed chapter property, and PlayerEventMap.chapterChanged is when to re-read this (you rarely need to: chapters do not change within an entry).

Chapters come from the container: m4b audiobooks, Matroska/.mka, Ogg chapter tags, and podcast MP3s with an ID3 CHAP frame. mpv can also load them from a side file (--chapters-file), reachable through mpvOptions: { 'chapters-file': … } on the load — which is how an app feeds chapters from a podcast feed's own JSON.

Throws

PlayerErrorException if the player has been destroyed.

Example

const chapters = player.getChapters()
const current = player.state.chapter
const label =
current !== undefined && current >= 0
? (chapters[current]?.title ?? `Chapter ${current + 1}`)
: undefined

getCommonMetadata()

getCommonMetadata(): CommonMetadata;

Defined in: packages/player/src/player.ts:4041

The current entry's tags, normalised to the fields a now-playing screen actually renders.

Returns

CommonMetadata

The normalised view; {} when nothing is loaded or the entry is untagged. Fields with no usable tag are absent rather than empty.

Remarks

Same single node read as getMetadata, with the most-copied snippet in the ecosystem applied to it: title ?? TITLE ?? icy-title, album_artist ?? albumartist ?? TPE2, "4/12" split into number and total, 2006-05-01 reduced to a year. FLAC/Vorbis, ID3, MP4 and ICY each spell these differently and mpv passes the demuxer's spelling through unchanged, so every app has been writing this function.

The full mapping table is on toCommonMetadata, which is exported so it can be applied to a tag map from anywhere (a metadataChanged payload, a persisted snapshot) without a player.

This is a convenience over the raw map, never a replacement for it: everything else the file carries — MusicBrainz ids, ReplayGain tags, icy-url, custom fields — is still in getMetadata.


getEqualizerFilters()

getEqualizerFilters(): readonly AudioFilter[] | undefined;

Defined in: packages/player/src/player.ts:3499

The equaliser half as setEqualizerFilters last set it, or undefined when nobody owns it.

Bookkeeping, not an mpv read — a raw setPropertyString('af', …) is invisible to it, exactly as documented on setLoudnessNormalization.

Returns

readonly AudioFilter[] | undefined


getLoudnessNormalization()

getLoudnessNormalization():
| Readonly<LoudnessNormalizationOptions>
| undefined;

Defined in: packages/player/src/player.ts:3782

The loudness normalization currently applied by setLoudnessNormalization — the resolved options (defaults filled in), or undefined when it is off.

This is the toggle's own bookkeeping, not an mpv read — a raw af write through the escape hatch is invisible to it, exactly as documented there.

Returns

| Readonly<LoudnessNormalizationOptions> | undefined


getMetadata()

getMetadata(): Metadata;

Defined in: packages/player/src/player.ts:4006

The current entry's tag map (mpv's metadata).

Returns

Metadata

Every tag mpv currently reports, or {} when nothing is loaded (mpv answers metadata with "property unavailable" while there is no demuxer).

Remarks

One synchronous read: metadata is fetched as an MPV_FORMAT_NODE map and converted natively (MpvClient.getPropertyMap). No string parsing is involved anywhere — the manual's "Trying to retrieve this property as a raw string doesn't work" is about the string format, and a node read is the documented way to get the map, which is also why it is atomic: mpv builds the whole node under its own lock, so the result cannot mix two tag generations.

It used to walk metadata/list/count + metadata/list/N/key + metadata/list/N/value, at a cost of 2N + 1 blocking round-trips into mpv's core — 41 of them for a 20-tag FLAC, issued from inside the event batch at a track boundary. That is why this is still a pull rather than a field of PlayerState, but the pull is now cheap and constant.


getMetadataValue()

getMetadataValue(key: string): string | undefined;

Defined in: packages/player/src/player.ts:4076

One tag of the current entry, by name (mpv's metadata/by-key/<key>).

Parameters

ParameterTypeDescription
keystringTag name. mpv matches these case-insensitively (mp_tags_get_bstr), so 'Title' and 'title' find the same tag.

Returns

string | undefined

The tag's value, or undefined when the entry has no such tag (or nothing is loaded).

Remarks

The pull half of the tag-store route — pair it with PlayerEventMap.metadataChanged, which tells you when to pull. Reading one key is one property read; reading several is cheaper through Player.getMetadata, which is one node read for the whole map.

For the now-playing line, prefer PlayerState.title. It is the same underlying update (mpv folds icy-title into media-title and invalidates both together), but it arrives in the snapshot, so it reaches the media session and every broadcast channel for free. This function is for the keys media-title does not carry — the station name, the bitrate, the album. See state.title for the full comparison.

Example

// The song is `player.state.title`. This is everything around it:
player.getMetadataValue('icy-name') // station
player.getMetadataValue('icy-genre')
player.getMetadataValue('icy-title') // the song again, pulled rather than
// pushed — useful inside a handler

getPosition()

getPosition(): number;

Defined in: packages/player/src/player.ts:2607

The playback position in seconds, projected locally from the last anchor.

time-pos is never observed and never polled natively: mpv publishes the truth on discontinuities and this extrapolates between them. See docs/specs/player-core.md §3.

Returns

number


getPropertyBool()

getPropertyBool(name: string): boolean | undefined;

Defined in: packages/player/src/player.ts:4113

Read any mpv property as a boolean. undefined when unavailable.

Parameters

ParameterType
namestring

Returns

boolean | undefined


getPropertyNumber()

getPropertyNumber(name: string): number | undefined;

Defined in: packages/player/src/player.ts:4107

Read any mpv property as a number. undefined when unavailable.

Parameters

ParameterType
namestring

Returns

number | undefined


getPropertyString()

getPropertyString(name: string): string | undefined;

Defined in: packages/player/src/player.ts:4101

Read any mpv property as a string. undefined when unavailable.

Parameters

ParameterType
namestring

Returns

string | undefined


getRawHandle()

getRawHandle(): bigint;

Defined in: packages/player/src/player.ts:4166

The underlying mpv_handle* as an integer, for the future video plugin.

Returns

bigint

The handle as a bigint (Nitro's UInt64).


getReplayGainMode()

getReplayGainMode(): ReplayGainMode;

Defined in: packages/player/src/player.ts:3802

The ReplayGain mode mpv currently has — including one this player switched off on your behalf.

This is what makes the mutual exclusion with setLoudnessNormalization observable rather than merely true: after setLoudnessNormalization(true) this reads 'no', and after setReplayGain({ mode: 'album' }) getLoudnessNormalization reads undefined. Two mutually exclusive switches whose UI cannot see which one won is how "everything is 3 dB too loud" survives a code review.

Bookkeeping, not an mpv read — a raw setPropertyString('replaygain', …) through the escape hatch is invisible to it, exactly like the af halves.

Returns

ReplayGainMode


getVolume()

getVolume(): number;

Defined in: packages/player/src/player.ts:3081

Read the output volume, in the same 0..1 scale setVolume takes.

Reads mpv directly rather than returning state.volume: the observed volume property arrives asynchronously in the next event batch, so a read immediately after a write would still see the old value. Anything doing read-modify-restore (wireAudioSession's ducking, for one) needs the truth, not the last broadcast. Falls back to the snapshot if mpv reports the property unavailable.

Returns

number

Volume in 0..1.


isPlaying()

isPlaying(): boolean;

Defined in: packages/player/src/player.ts:2525

Whether playback is currently un-paused — PlayerState.playing as a method.

A convenience, but not only that: it is what makes a Player satisfy @afkcodes/timbre-audio-session's AudioSessionPlayerLike.isPlaying, which wireAudioSession consults to tell its own interruption pause apart from one the user asked for — a user pause must never be auto-resumed when the interruption ends.

Reflects the observed mpv pause property: for a few milliseconds after play/pause it still reports the previous value, until the property change round-trips through the native event loop. Subscribe with onStateChange when the transition itself matters.

Returns

boolean


load()

load(source: string, options?: LoadOptions): Promise<void>;

Defined in: packages/player/src/player.ts:2649

Replace whatever is playing with a single source.

Parameters

ParameterTypeDescription
sourcestringURI (https://…) or absolute file path.
optionsLoadOptionsSee LoadOptions.

Returns

Promise<void>

Remarks

A source whose path ends in .m3u8/.m3u is loaded with demuxer=lavf forced, unless options.mpvOptions already names a demuxer — see formatFileOptions for why.


loadPlaylist()

loadPlaylist(sources: readonly string[], options?: LoadPlaylistOptions): Promise<void>;

Defined in: packages/player/src/player.ts:2691

Replace the playlist with sources and start at startIndex.

Uses mpv's own playlist, so transitions between entries are gapless.

Parameters

ParameterTypeDescription
sourcesreadonly string[]URIs or file paths, in order.
optionsLoadPlaylistOptionsSee LoadPlaylistOptions.

Returns

Promise<void>

Throws

PlayerErrorException with code invalid-state when both shuffle: true and an explicit startIndex are given — see LoadPlaylistOptions.shuffle.

Remarks

The .m3u8/.m3u demuxer=lavf guard described on load is applied to each entry independently.

With shuffle: true the URI this player remembers for error classification is sources[0], which after the shuffle is probably not what starts playing. That is a hint, not a contract — the same staleness already applies after any playlist.next() — and it only affects whether a failure is reported as network or load-failed.


nextChapter()

nextChapter(): Promise<void>;

Defined in: packages/player/src/player.ts:3954

Skip to the next chapter (mpv's add chapter 1).

At the last chapter mpv advances to the next playlist entry, which is the behaviour a chapter-skip button is expected to have in an audiobook app.

Returns

Promise<void>


observeProperty()

observeProperty(name: string, format: MpvFormat): void;

Defined in: packages/player/src/player.ts:4146

Observe an extra mpv property.

Its changes arrive as kind: 'property' events. The built-in reducer ignores names it does not know, so use onStateChange plus your own bookkeeping, or read the property when you need it.

Parameters

ParameterTypeDescription
namestringmpv property name.
formatMpvFormatThe format to receive it in.

Returns

void


on()

on<K extends keyof PlayerEventMap>(event: K, listener: PlayerEventMap[K]): Unsubscribe;

Defined in: packages/player/src/player.ts:2557

Subscribe to a discrete event.

Type Parameters

Type Parameter
K extends keyof PlayerEventMap

Parameters

ParameterTypeDescription
eventKOne of PlayerEventName.
listenerPlayerEventMap[K]Called with that event's payload.

Returns

Unsubscribe

A function that removes the listener.


onStateChange()

onStateChange(listener: (state: PlayerState) => void): Unsubscribe;

Defined in: packages/player/src/player.ts:2543

Subscribe to whole-state changes.

Fires at most once per native event batch, and only when the reducer actually produced a new snapshot.

Parameters

ParameterTypeDescription
listener(state: PlayerState) => voidCalled with the new snapshot.

Returns

Unsubscribe

A function that removes the listener.


pause()

pause(): void;

Defined in: packages/player/src/player.ts:2851

Pause playback (pause = yes).

Returns

void


play()

play(): void;

Defined in: packages/player/src/player.ts:2845

Resume playback (pause = no).

Returns

void


previousChapter()

previousChapter(): Promise<void>;

Defined in: packages/player/src/player.ts:3975

Skip to the previous chapter (mpv's add chapter -1).

Returns

Promise<void>

Remarks

This is restart-or-previous, and mpv already implements it — the same convention PlaylistApi.previous applies to the queue. mpv 0.41.0 options.rst, --chapter-seek-threshold (default 5.0): "Distance in seconds from the beginning of a chapter within which a backward chapter seek will go to the previous chapter. Past this threshold, a backward chapter seek will go to the beginning of the current chapter instead."

So add chapter -1 is not chapter = chapter - 1, and this method deliberately does not smooth that over. Change the threshold with setPropertyNumber('chapter-seek-threshold', n); a negative value means always go back a chapter.


resyncPosition()

resyncPosition(): number;

Defined in: packages/player/src/player.ts:2620

Re-anchor the projected position on an exact time-pos read.

One synchronous property read, no polling. useProgress calls it once when it subscribes so that a component mounting mid-playback starts from the truth rather than from an anchor that may be seconds old.

Returns

number

The position in seconds after the resync.


seekBy()

seekBy(deltaSeconds: number): Promise<void>;

Defined in: packages/player/src/player.ts:2899

Seek by a delta relative to the current position.

Parameters

ParameterTypeDescription
deltaSecondsnumberSeconds to move; negative seeks backwards.

Returns

Promise<void>

Remarks

Not seekTo(getPosition() + delta). That is what an app has to write without this method, and it races the projection: the position it reads is extrapolated from an anchor that may be a few hundred milliseconds old, so a rapid tap on a ±15 s button accumulates the projection error into the target. mpv's relative seek is applied to mpv's clock, at the instant the command runs, and cannot drift.

Uses relative+exact — precise rather than nearest-keyframe — matching seekTo. mpv's own default for a relative seek is keyframes (fast), but for audio the difference is a decode of at most a frame or two, and a jump-back button that lands somewhere other than where the label said is a worse trade than that.

mpv clamps the target to the file: seeking past the end ends the entry, and seeking before 0 lands at 0. On a live stream (state.isLive) a backward seek fails inside mpv and playback continues unchanged.


seekTo()

seekTo(seconds: number): Promise<void>;

Defined in: packages/player/src/player.ts:2870

Seek to an absolute position.

Uses mpv's seek <seconds> absolute+exact, i.e. a precise seek rather than the nearest keyframe.

Parameters

ParameterTypeDescription
secondsnumberTarget position; negative values are clamped to 0.

Returns

Promise<void>


setAudioChannels()

setAudioChannels(mode: AudioChannelMode): void;

Defined in: packages/player/src/player.ts:3059

Force a channel layout on the output — including the mono downmix.

Parameters

ParameterTypeDescription
modeAudioChannelModeSee AudioChannelMode.

Returns

void

Throws

PlayerErrorException with code invalid-state for a value outside that union.

Remarks

'mono' is the accessibility case this exists for: single-sided hearing loss, or one earbud in. Both mobile platforms offer it as a system accessibility toggle, and an app that wants its own switch has had no way to ask for one.

mpv 0.41.0 options.rst on --audio-channels: "--audio-channels=<stereo| mono> — Force a downmix to stereo or mono." No filter and no engine flag is involved — this is mpv's own channel-layout negotiation, not the pan filter (which is not compiled into these binaries).

Applies to the entry that is already playing: the option carries UPDATE_AUDIO (options/options.c), so mpv rebuilds the audio chain in place. The rebuild reopens the audio device, so expect a very short gap — this is a settings-screen control, not something to toggle per frame.

One documented consequence, mpv's own: a single-layout list "triggers decoder-downmix, which might be different from the normal mpv downmix", because the decision is made before the device is opened.


setAudioFilterParam()

setAudioFilterParam(
filter: AudioFilter,
param: string,
value: string | number
): Promise<void>;

Defined in: packages/player/src/player.ts:3596

Change one sub-option of a running filter, without rebuilding the chain (mpv's af-command).

This is the call a slider makes. setAudioFilters replaces the af property, and mpv answers that by destroying and recreating every entry whose arguments changed — correct for a settings change, ruinous sixty times a second. af-command instead reaches into the live filter and updates it: for an EQ band that means new biquad coefficients with the filter's own state left intact, so a gain sweeps rather than clicks.

Parameters

ParameterTypeDescription
filterAudioFilterThe entry to command. Both halves of its address are needed, which is why this takes the filter rather than a label: the AudioFilter.label finds the mpv entry, and AudioFilter.name finds the libavfilter filter inside it (see the remarks — passing mpv's all default instead reports failure on a command that worked). Build a chain that carries labels with equalizerPresetChain(preset, { editable: true }), or set AudioFilter.label yourself.
paramstringThe sub-option to change, spelled as the filter's AVOption table spells it (g or gain on equalizer).
valuestring | numberThe new value. A number is stringified; strings are passed through, which is how volume takes '-6dB'.

Returns

Promise<void>

Throws

PlayerErrorException with code invalid-state for an entry with no label, a label or parameter mpv could not parse, or the reserved LOUDNESS_NORMALIZATION_LABEL; with code mpv when the command itself fails.

Remarks

Why the filter name is sent as mpv's <target>. af-command takes <label> <command> <argument> [<target>], and <target> — which selects filters inside the entry's libavfilter graph — defaults to all (mpv 0.41.0 player/command.c:7523-7531). all is the wrong default here and, worse, a silently wrong one: mpv wraps every af entry in its own graph with an abuffer source and an abuffersink sink around the real filter, and avfilter_graph_send_command overwrites its result on every matching filter and returns the last one (FFmpeg n8.1.2 libavfilter/avfiltergraph.c:1470-1481). The sink answers ENOSYS (libavfilter/avfilter.c:610-629), so the gain lands on the running filter and mpv still reports the command as failed. Naming the filter narrows the loop to the one that implements the command, so success means success.

When it really fails. mpv's af-command returns failure — a rejected Promise with code: 'mpv' — when there is no audio chain to command (nothing loaded, or playback stopped: player/command.c:6716-6730), when no entry carries that label (filters/f_output_chain.c:445-465), or when libavfilter refuses the parameter. That last one is the honest signal that a filter does not support runtime changes: ff_filter_process_command looks the option up with AV_OPT_FLAG_RUNTIME_PARAM and returns ENOSYS without it (FFmpeg n8.1.2 libavfilter/avfilter.c:905-916). See AUDIO_FILTER_RUNTIME_PARAMS for the ones this library ships bindings for and the citations behind them.

Treat a failure as "fall back to a chain write", not as fatal — that is what useEqualizer does. Every failure mode above is one a full setAudioFilters handles correctly, only less smoothly.

The af property is deliberately not rewritten. That is the whole point — rewriting it is the rebuild being avoided — but it has two consequences worth knowing. getAudioFilters, which reads mpv's property, keeps showing the value the entry was created with. And if anything later rebuilds the chain from that property (a new file, an audio device change, the next setAudioFilters), the running value is lost. This library's own bookkeeping does not have that problem: a successful call updates the user half it remembers, so a later setLoudnessNormalization or setAudioFilters composes from the new value rather than reverting it. For everything else, write the chain once the gesture is over — the pattern useEqualizer implements.

The command is asynchronous (mpv_command_async), so unlike setAudioFilters it never blocks the JS thread.

Example

// While the finger is down: 6 dB on the 1 kHz band of an editable chain.
const chain = equalizerPresetChain(curve, { editable: true })
await player.setAudioFilterParam(chain[6], 'g', 6)

setAudioFilters()

setAudioFilters(filters: readonly AudioFilter[]): void;

Defined in: packages/player/src/player.ts:3421

Replace the audio filter chain (mpv's af).

Build entries with the AudioFilters factories; they compile to mpv's own af grammar and are validated against each filter's documented ffmpeg ranges before anything is written. The whole chain is replaced atomically: mpv parses the string first and leaves the previous chain in place if any entry is bad, so a rejected call never leaves playback half-filtered.

Parameters

ParameterTypeDescription
filtersreadonly AudioFilter[]The new chain, in signal-flow order (first entry runs first). Pass [] — or call Player.clearAudioFilters — to remove every filter.

Returns

void

Throws

PlayerErrorException with code invalid-state when the chain is malformed or a value is out of the filter's range (checked here, before mpv sees it), and with code mpv (errno: -11, MPV_ERROR_PROPERTY_ERROR) when mpv itself rejects it.

Remarks

Availability is a property of the binary, not of this API. mpv resolves any name it does not implement itself through avfilter_get_by_name(), so a filter exists only if it was compiled into that platform's libmpv. Both platforms ship the same EQ/DSP set from the pinned binaries — Android v1.1.9-rnmedia.2 and later, iOS v0.7.2-rnmedia.2 and later — so no per-platform branching is needed.

On binaries older than those pins (an app that overrode the pin, or a stock media-kit build, whose audio flavour compiles in only overlay and equalizer) a call here fails with code: 'mpv', errno: -11 and mpv logs Option af: <name> doesn't exist. at error level (visible through PlayerOptions.onLog). That is the honest signal, and it is also the supported way to probe support: try the chain, catch, fall back.

This is a chain rebuild, not a parameter poke — do not call it from a slider. Applying filters mid-playback does not reload the file, reset the position or drop the audio device, but mpv does rebuild the graph: entries whose arguments are byte-identical are kept and every entry that differs is destroyed and recreated, losing whatever it had buffered (mpv 0.41.0 filters/f_output_chain.c:535-593), and if that shortens the chain's measured delay by 0.2 s or more mpv issues an exact refresh seek to resynchronise (player/audio.c:107-125). One write is a settings change and inaudible; sixty writes a second is a stutter. To move a number on a running filter — an EQ band under a finger — use setAudioFilterParam. The write is also synchronous: it blocks the calling thread until mpv's core has applied it.

The chain survives track changes — af is a global option, not a per-entry one.

Coexists with setLoudnessNormalization, by construction. This method owns the user half of the chain; the loudness-normalization toggle owns exactly one managed, labelled entry appended after it. Setting filters here never turns normalization off, and toggling normalization never touches the chain set here. (The raw setPropertyString('af', …) escape hatch bypasses both halves' bookkeeping — after using it, the next call to either method rewrites the property from that bookkeeping, exactly as documented on setLoudnessNormalization.)

Example

import { AudioFilters } from '@afkcodes/timbre-player'

player.setAudioFilters([
AudioFilters.volume({ gainDb: -6 }), // headroom for the boost below
AudioFilters.bass({ frequency: 110, gain: 12 }),
AudioFilters.limiter(), // and nothing clips
])

setChapter()

setChapter(index: number): void;

Defined in: packages/player/src/player.ts:3938

Jump to the start of a chapter.

Parameters

ParameterTypeDescription
indexnumber0-based chapter index.

Returns

void

Throws

PlayerErrorException with code invalid-state when index is not a non-negative integer.

Remarks

mpv 0.41.0 input.rst on the chapter property: "Setting this property results in an absolute seek to the start of the chapter." An index past the last chapter is clamped by mpv itself (it seeks to the end of the file); this method rejects only what cannot mean anything at all.

Writing the property is a property write, not a command, so — like setRate — it returns immediately and the resulting seek arrives through PlayerEventMap.seekStarted / seekCompleted like any other. nextChapter and previousChapter are commands and are therefore awaitable; the asymmetry is mpv's, and hiding it would mean inventing a promise that resolves on nothing.


setEqualizerFilters()

setEqualizerFilters(filters: readonly AudioFilter[] | null): void;

Defined in: packages/player/src/player.ts:3469

Replace the equaliser half of the chain — the managed, labelled entries an EQ screen owns — leaving every other filter exactly where it is.

This is what useEqualizer calls, and it is the reason that hook no longer clobbers a chain set through setAudioFilters. Reach for it directly only if you are driving an equaliser without the hook.

Parameters

ParameterTypeDescription
filtersreadonly AudioFilter[] | nullThe equaliser entries, in signal-flow order — normally the output of equalizerPresetChain(curve, { editable: true }), i.e. pre-amp, ten bands, limiter, every entry labelled @rnmedia_eq_…. Pass [] for a flat/disabled equaliser that still belongs to you, and null to hand ownership back entirely (they compile to the same af, but only null lets the next setAudioFilters stop composing around you).

Returns

void

Throws

PlayerErrorException with code invalid-state when an entry carries LOUDNESS_NORMALIZATION_LABEL (that label has another owner), and with code mpv (errno: -11) when mpv rejects the chain — the same taxonomy as setAudioFilters.

Remarks

Chain order is equaliser → user → loudness normalization, and each of the three is deliberate. The equaliser leads because its first entry is the headroom pre-amp, which has to attenuate before the boosts it is sized for, and its last is the limiter that catches what the bands' summed response still lets through (ARCHITECTURE §18). The user half follows, unchanged from where extraFilters used to put it, so this fix is not audible on an app that was already composing correctly. The loudness entry stays at the tail because it must hear everything above it.

Same rebuild cost as setAudioFilters — it is one write of the whole af property, so a slider still belongs on setAudioFilterParam, and useEqualizer still only calls this when the graph changes.


setLoop()

setLoop(mode: LoopMode): void;

Defined in: packages/player/src/player.ts:3118

Set repeat behaviour.

Parameters

ParameterTypeDescription
modeLoopMode'track' repeats the current entry forever, 'playlist' repeats the whole queue forever, 'off' disables both.

Returns

void


setLoudnessNormalization()

setLoudnessNormalization(enabled: boolean, options?: LoudnessNormalizationOptions): void;

Defined in: packages/player/src/player.ts:3750

Turn EBU R128 loudness normalization on or off — ffmpeg's loudnorm, managed as one labelled entry of the af chain.

This is the "make everything the same loudness" switch for content that carries no ReplayGain tags: podcasts mixed at wildly different levels, a queue mixing loud modern masters with quiet archival ones. One call, no chain bookkeeping — it composes with whatever setAudioFilters has set (the managed entry sits at the tail, so it normalizes the signal your EQ actually produced, and its built-in true-peak limiter guards the whole chain's output).

Parameters

ParameterTypeDescription
enabledbooleantrue inserts or replaces the managed entry; false removes it and only it.
optionsLoudnessNormalizationOptionsSee LoudnessNormalizationOptions. Meaningful only with enabled: true; ignored (deliberately, not silently — this sentence is the notice) when disabling.

Returns

void

Throws

PlayerErrorException with code invalid-state when an option is outside ffmpeg's documented range, and with code mpv (errno: -11) when the linked libmpv lacks the filter — the same availability probe as setAudioFilters, and the same parity note: both platforms' pinned binaries compile loudnorm in.

Remarks

What one-pass loudnorm honestly is. loudnorm has a linear mode (one fixed gain for the whole file) and a dynamic mode (a gain that rides the signal). The linear mode is unreachable live: ffmpeg enters it only when all four measured_* values from a prior analysis pass are supplied (FFmpeg 8.1.2 af_loudnorm.c:820-825 — the gate requires measured_tp, measured_thresh, measured_lra and measured_i all non-default), and a live player cannot measure a file it has not finished playing. So this API is always ffmpeg's dynamic mode, and that has real costs:

  • It is a dynamics processor. The gain is recomputed per 100 ms block from short-term loudness and smoothed with a 21-tap Gaussian (≈2 s window, af_loudnorm.c:139-159,505), with a built-in true-peak limiter (10 ms attack / 100 ms release, af_loudnorm.c:799-800) catching what the ride pushes at the ceiling. Macro-dynamics — the difference between a verse and a chorus, a whisper and a shout — are genuinely compressed. On well-mastered music that is a loss; this switch is for material whose levels are wrong, not a mastering upgrade.
  • It resamples the whole chain to 192 kHz. In dynamic mode the filter advertises exactly one input rate (af_loudnorm.c:740,752; doc/filters.texi: "the audio stream will be upsampled to 192 kHz"), so libavfilter converts up and back down around it. Measurable CPU and battery cost — the most expensive single entry this library ships.
  • It buffers 3 s of audio. The dynamic mode's lookahead window (af_loudnorm.c:697,775: the first frame it consumes is 3000 ms). Position and A/V pts stay correct, but enabling it mid-track rebuilds the chain and refills that window, so expect a short hiccup on toggle — this is a settings switch, not a per-track one.

If your files do carry ReplayGain tags, prefer setReplayGain: it levels loudness in mpv's volume domain from the tags — zero DSP, zero latency, zero resampling — and preserves dynamics completely. You cannot run both, and this method is what stops you: enabled: true switches ReplayGain to 'no', because the two solve the same problem and their gain changes stack — a track leveled by ReplayGain would get re-leveled, and re-compressed, by loudnorm. The rule is symmetric: setReplayGain({ mode: 'track' | 'album' }) removes this managed entry. getReplayGainMode reports which one is live. Tagged library → ReplayGain; untagged / mixed-provenance streams → this. (The same rule is written on ReplayGainOptions, from the other side.)

The switch-off costs nothing when there was nothing to switch off: both states are tracked in TypeScript, so replaygain is written only when it was actually on.

For loudness smoothing at native sample rate, AudioFilters.dynamicNormalizer (ffmpeg dynaudnorm) is the cheaper, less faithful cousin — it chases a peak/RMS window rather than an EBU R128 target. Reach for it through setAudioFilters when the 192 kHz cost is unacceptable; it is a different trade, not a hidden mode of this API.

The managed entry is visible in getAudioFilters read-backs as @rnmedia_loudnorm:loudnorm=… (LOUDNESS_NORMALIZATION_LABEL), and af is a global option, so it survives track changes exactly like the user chain does.

Example

player.setLoudnessNormalization(true) // −16 LUFS
player.setLoudnessNormalization(true, { targetLufs: -18 }) // spoken word
player.setLoudnessNormalization(false) // off

setMuted()

setMuted(muted: boolean): void;

Defined in: packages/player/src/player.ts:3107

Mute or unmute output.

Parameters

ParameterType
mutedboolean

Returns

void


setPitch()

setPitch(ratio: number): void;

Defined in: packages/player/src/player.ts:3026

Shift the pitch without changing the speed.

Parameters

ParameterTypeDescription
rationumberFrequency multiplier. 1 is the file's own pitch, 2 is an octave up, 0.5 an octave down.

Returns

void

Throws

PlayerErrorException with code invalid-state when ratio is outside mpv's documented 0.01 … 100.

Remarks

A ratio, not semitones, and deliberately so. mpv's --pitch is a frequency factor (mpv 0.41.0 options.rst: "Raise or lower the audio's pitch by the factor given as parameter. Does not affect playback speed"), and exposing semitones would mean this library picking a tuning convention and hiding the actual knob. The conversion is one line, and it is the manual's own: "octaves are separated by a factor of 2 whereas semitones are represented by a factor of 2^(1/12)".

const semitones = (n: number) => 2 ** (n / 12)
player.setPitch(semitones(-2)) // down a whole tone
player.setPitch(semitones(7)) // up a perfect fifth ≈ 1.4983
player.setPitch(1) // back to the recording's own pitch

Independent of setRate. Both drive mpv's own scaletempo2 — the same filter already in the chain for speed (ARCHITECTURE §18) — so this needs no ffmpeg filter, no engine flag and no GPL-licensed library (rubberband is GPL and is not, and will not be, compiled in). Speed and pitch compose: 1.5× speed with setPitch(1) is a faster audiobook at the right pitch; 1× speed with setPitch(1.06) is a semitone-up transposition for a singer.

The honest range. mpv accepts 0.01 … 100 and this method enforces exactly that, but the useful range is narrower and mpv says why: "the range of pitch change is effectively limited by the min-speed and max-speed parameters of scaletempo2: for example, a min-speed of 0.25 limits the highest pitch factor to 4 (1/0.25)". With the defaults (0.25 / 8.0) that is roughly 0.125 … 4 before the output goes silent rather than extreme. Values are validated, never clamped, because a pitch of 0 is a bug in the caller and silently turning it into 0.01 hides it.

The current value is in PlayerState.pitch, and it is global: mpv's --reset-on-next-file resets nothing by default, so it survives track changes like speed and volume do.


setPrefetchPlaylist()

setPrefetchPlaylist(enabled: boolean): void;

Defined in: packages/player/src/player.ts:3271

Turn mpv's prefetch-playlist on or off on a running player.

Parameters

ParameterTypeDescription
enabledbooleanWhether mpv may open the next queue entry while the current one is finishing.

Returns

void

Throws

PlayerErrorException with code invalid-state when enabled is not a boolean, or if the player has been destroyed.

Remarks

The runtime twin of PlayerOptions.prefetchPlaylist — read that first, because every caveat it documents applies here and one more does on top. mpv's manual disclaims correctness when the playlist is edited or stepped backwards while an entry is ending, and turning the option on mid-session does not change that; it only changes when you start paying for it. Specifically: an entry inserted after an opener has already started gets no prefetch of its own, and at the boundary mpv logs Dropping finished prefetch of wrong URL. and opens cold (player/loadfile.c:1223, mpv 0.41.0). See PlaylistAddOptions.position.

Flipping it takes effect from the next prefetch decision. Turning it off does not abort an opener already running — mpv checks the option when it decides to prefetch (prefetch_next()), not while it is doing so — so one more boundary may still be gapless after you disable it. That is not a race this library could close without cancelling an in-flight open, which would be a worse trade than one extra early connection.

There is nothing to undo and no state kept here: this writes mpv's property and mpv is the record. Read it back with getPropertyBool(MpvProperty.prefetchPlaylist) if you need to.


setPropertyBool()

setPropertyBool(name: string, value: boolean): void;

Defined in: packages/player/src/player.ts:4131

Write any mpv property from a boolean.

Parameters

ParameterType
namestring
valueboolean

Returns

void


setPropertyNumber()

setPropertyNumber(name: string, value: number): void;

Defined in: packages/player/src/player.ts:4125

Write any mpv property from a number.

Parameters

ParameterType
namestring
valuenumber

Returns

void


setPropertyString()

setPropertyString(name: string, value: string): void;

Defined in: packages/player/src/player.ts:4119

Write any mpv property from a string.

Parameters

ParameterType
namestring
valuestring

Returns

void


setRate()

setRate(rate: number): void;

Defined in: packages/player/src/player.ts:2976

Set the playback rate.

Parameters

ParameterTypeDescription
ratenumberMultiplier. Clamped to mpv's documented 0.01 … 100.

Returns

void

Remarks

Pitch is preserved: mpv inserts scaletempo2 automatically whenever the speed is not 1 (--audio-pitch-correction, on by default). Use setPitch for the other axis — it moves pitch without moving speed.

One practical bound worth knowing: scaletempo2 mutes its output outside min-speed/max-speed (0.25/8.0), so a rate below 0.25× or above 8× is silent rather than fast.


setReplayGain()

setReplayGain(options: ReplayGainOptions): void;

Defined in: packages/player/src/player.ts:3317

Change ReplayGain normalisation on the fly.

All four mpv options behind this carry UPDATE_VOL, so the new gain is applied to the track that is already playing — no reload, no gap.

Only the fields you pass are written: setReplayGain({ mode: 'album' }) switches tag set and leaves preamp, clipping and fallback exactly as they were. Pass mode: 'no' to stop honouring tags — but note that a non-zero fallback written earlier is mode-independent and keeps applying (see ReplayGainOptions.fallback); to return to unity gain, pass { mode: 'no', fallback: 0 }.

Parameters

ParameterTypeDescription
optionsReplayGainOptionsSee ReplayGainOptions.

Returns

void

Throws

PlayerErrorException with code invalid-state if mode is not a ReplayGainMode or a gain is outside mpv's range.

Remarks

This and setLoudnessNormalization are mutually exclusive, and the API enforces it. Any mode other than 'no' turns loudness normalization off; setLoudnessNormalization(true) turns ReplayGain off. They level the same thing by different means and their gains stack, so running both is never what anyone wants — it was the top loudness bug in this library's own README ("everything is 3 dB too loud"), and leaving it to a documentation line was what made it a bug rather than a footnote. getReplayGainMode and getLoudnessNormalization both report the outcome, so a settings screen can show which switch won.

Turning the loser off is free when it was already off: the state of both is tracked in TypeScript, so the exclusion adds no bridge traffic in the ordinary case and exactly one property write in the case that needed it.


setSourceResolver()

setSourceResolver(resolver: SourceResolver | null): void;

Defined in: packages/player/src/player.ts:2830

Install (or remove) the function that turns a queue's logical URIs into the URLs mpv actually opens.

Parameters

ParameterTypeDescription
resolverSourceResolver | nullSee SourceResolver — read its remarks before writing one, in particular the determinism requirement. Pass null to remove the current resolver; every URI then passes through untouched again.

Returns

void

Throws

PlayerErrorException if the player has been destroyed, or if mpv rejects the hook registration.

Example

player.setSourceResolver(async ({ uri }) =>
uri.startsWith('library://') ? await sign(uri) : uri
)
await player.loadPlaylist(['library://a', 'library://b'])

Remarks

What this buys you over resolving in your own code before loadPlaylist. A URL signed at queue-build time has to stay valid for the whole queue; a URL resolved through this hook is minted per entry, moments before mpv opens it, so signature lifetimes can be short. It also survives everything that changes the queue behind your back — playlist.next(), repeat, shuffle, a resumed session — because mpv asks for whatever entry it is actually about to play rather than whatever you predicted.

Cost when installed but idle. The current and next entries are resolved as the queue moves — two calls per track change, de-duplicated and cached — and each answer is pushed into a native cache that the hook reads synchronously. Nothing polls, nothing is streamed, and a URI that is already resolved costs a map lookup.

Cost when not installed. One immediate mpv_hook_continue per load boundary, and nothing else — no property read, no rewrite, no JavaScript. The two load hooks are registered when the core starts rather than when a resolver arrives, because the fork's "behaves exactly like stock mpv" guarantee holds only while the hook name has no client at all: registering late would not preserve stock behaviour, it would only move the moment behaviour changes into the middle of a session, where nothing can measure it. That also makes PlayerEventMap.prefetchStarted available to players that never resolve anything.

One caveat worth knowing. mpv normalises the URI it hands back inside the hook: URLs pass through verbatim, but a relative local path is made absolute against the process's working directory (mp_normalize_path, mpv 0.41.0 player/command.c:564). Key your queue off URLs or absolute paths and this never comes up.


setVolume()

setVolume(volume: number): void;

Defined in: packages/player/src/player.ts:3101

Set the output volume.

Parameters

ParameterTypeDescription
volumenumber0..1, where 1 is mpv's volume=100 (no attenuation and no amplification). Values outside the range are clamped; use setPropertyNumber('volume', …) if you really want mpv's amplification range above 100.

Returns

void

Remarks

mpv's volume curve is gain = (volume / 100) ** 3, so 0.5 is much quieter than half as loud.


stop()

stop(options?: {
clearPlaylist?: boolean;
}): Promise<void>;

Defined in: packages/player/src/player.ts:2950

Stop playback and unload the current entry, keeping the player — and, by default, the queue — alive.

Parameters

ParameterTypeDescription
options{ clearPlaylist?: boolean; }clearPlaylist: true clears the queue too.
options.clearPlaylist?boolean-

Returns

Promise<void>

Remarks

This is the transport-button meaning of stop: "stop playing, keep my queue". It sends mpv's stop keep-playlist ("Do not clear the playlist"). mpv's own default is the opposite — mpv 0.41.0 input.rst: "Stop playback and clear playlist. With default settings, this is essentially like quit. Useful for the client API: playback can be stopped without terminating the player." That default is deliberately inverted here, because this API is judged against the React Native ecosystem, not against mpv's CLI: react-native-track-player's stop() keeps the queue (its queue-clearing call is the separate reset() — "Resets the player stopping the current track and clearing the queue"), so a migrator's stop() silently destroying a queue would be data loss, while the reverse surprise — the queue still being there — is benign. Destructive behaviour is opt-in, consistent with the rest of the API: pass { clearPlaylist: true } for mpv's full clear.

Afterwards the state settles to status: 'idle' (mpv ends the entry with reason stop, which also clears any error). The queue is intact but nothing is loaded: mpv leaves no playlist entry "current", and playlist-pos reads -1 whenever "no entry is 'current'" (input.rst, property docs) — so state.playlist becomes { index: -1, count: n } and PlayerState.hasNext / PlayerState.hasPrevious are both false. That means play alone does not resume — it only flips pause with nothing playing. The way back into the kept queue is PlaylistApi.jumpTo (or a fresh load); after { clearPlaylist: true } a new load is the only way, since the queue is gone.

Example

await player.stop() // queue intact, nothing playing
await player.playlist.jumpTo(0) // …and this is the way back in
await player.stop({ clearPlaylist: true }) // mpv's full clear; queue gone

toggle()

toggle(): void;

Defined in: packages/player/src/player.ts:2857

Flip between playing and paused, based on the current snapshot.

Returns

void


unobserveProperty()

unobserveProperty(name: string): void;

Defined in: packages/player/src/player.ts:4154

Stop observing a property added with observeProperty.

Parameters

ParameterType
namestring

Returns

void


create()

static create(options?: PlayerOptions): Promise<Player>;

Defined in: packages/player/src/player.ts:2397

Create and start a player.

Applies the pre-init mpv options, starts the core, registers the batched event listener, installs every property observation, and applies the initial volume/rate/mute/loop settings.

Parameters

ParameterTypeDescription
optionsPlayerOptionsSee PlayerOptions.

Returns

Promise<Player>

Throws

PlayerErrorException with code invalid-state if a typed option is out of mpv's documented range (checked before any core is created), or with a mapped mpv error if mpv rejects an option or fails to initialise — in which case the half-built core is torn down first.