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
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
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
| Parameter | Type | Description |
|---|---|---|
args | readonly 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
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
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
| Parameter | Type | Description |
|---|---|---|
key | string | Tag 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
| Parameter | Type |
|---|---|
name | string |
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
| Parameter | Type |
|---|---|
name | string |
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
| Parameter | Type |
|---|---|
name | string |
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
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
| Parameter | Type | Description |
|---|---|---|
source | string | URI (https://…) or absolute file path. |
options | LoadOptions | See 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
| Parameter | Type | Description |
|---|---|---|
sources | readonly string[] | URIs or file paths, in order. |
options | LoadPlaylistOptions | See 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
| Parameter | Type | Description |
|---|---|---|
name | string | mpv property name. |
format | MpvFormat | The 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
| Parameter | Type | Description |
|---|---|---|
event | K | One of PlayerEventName. |
listener | PlayerEventMap[K] | Called with that event's payload. |
Returns
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
| Parameter | Type | Description |
|---|---|---|
listener | (state: PlayerState) => void | Called with the new snapshot. |
Returns
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
| Parameter | Type | Description |
|---|---|---|
deltaSeconds | number | Seconds 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
| Parameter | Type | Description |
|---|---|---|
seconds | number | Target 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
| Parameter | Type | Description |
|---|---|---|
mode | AudioChannelMode | See 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
| Parameter | Type | Description |
|---|---|---|
filter | AudioFilter | The 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. |
param | string | The sub-option to change, spelled as the filter's AVOption table spells it (g or gain on equalizer). |
value | string | number | The 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
| Parameter | Type | Description |
|---|---|---|
filters | readonly 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
| Parameter | Type | Description |
|---|---|---|
index | number | 0-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
| Parameter | Type | Description |
|---|---|---|
filters | readonly AudioFilter[] | null | The 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
| Parameter | Type | Description |
|---|---|---|
mode | LoopMode | '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
| Parameter | Type | Description |
|---|---|---|
enabled | boolean | true inserts or replaces the managed entry; false removes it and only it. |
options | LoudnessNormalizationOptions | See 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
| Parameter | Type |
|---|---|
muted | boolean |
Returns
void
setPitch()
setPitch(ratio: number): void;
Defined in: packages/player/src/player.ts:3026
Shift the pitch without changing the speed.
Parameters
| Parameter | Type | Description |
|---|---|---|
ratio | number | Frequency 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
| Parameter | Type | Description |
|---|---|---|
enabled | boolean | Whether 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
| Parameter | Type |
|---|---|
name | string |
value | boolean |
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
| Parameter | Type |
|---|---|
name | string |
value | number |
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
| Parameter | Type |
|---|---|
name | string |
value | string |
Returns
void
setRate()
setRate(rate: number): void;
Defined in: packages/player/src/player.ts:2976
Set the playback rate.
Parameters
| Parameter | Type | Description |
|---|---|---|
rate | number | Multiplier. 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
| Parameter | Type | Description |
|---|---|---|
options | ReplayGainOptions | See 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
| Parameter | Type | Description |
|---|---|---|
resolver | SourceResolver | null | See 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
| Parameter | Type | Description |
|---|---|---|
volume | number | 0..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
| Parameter | Type | Description |
|---|---|---|
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
| Parameter | Type |
|---|---|
name | string |
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
| Parameter | Type | Description |
|---|---|---|
options | PlayerOptions | See 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.