Interface: PlayerState
Defined in: packages/player/src/state.ts:132
One immutable snapshot of everything the player knows.
Snapshots are produced only by reducePlayerState; nothing mutates a
PlayerState in place, and an event that changes nothing returns the same
object identity — so a snapshot is safe to hand to useSyncExternalStore.
Properties
bufferedPosition?
readonly optional bufferedPosition?: number;
Defined in: packages/player/src/state.ts:177
Absolute timestamp (seconds, directly comparable to position) up to which
the demuxer has buffered — mpv's demuxer-cache-time.
Remarks
Second-granular, deliberately. mpv republishes this several times a
second for as long as the cache is filling (including while paused), and
every republication would otherwise mint a new PlayerState and wake every
state listener. The reducer therefore only adopts a value that moved at
least BUFFERED_POSITION_STEP second from the last published one —
plus one guaranteed update at the instant the buffer reaches
duration, so "fully buffered" is always observable exactly.
The value itself is mpv's, unrounded: what is quantised is how often it changes, not what it says. Do not use it as a clock — it is a fill level, and projectPosition is what moves smoothly.
bufferingPercent?
readonly optional bufferingPercent?: number;
Defined in: packages/player/src/state.ts:197
How full the network cache is on its way back to playing, in percent —
mpv's cache-buffering-state, and present only while
status === 'buffering'.
Remarks
What it measures, exactly. mpv 0.41.0 input.rst: "The percentage
(0-100) of the cache fill status until the player will unpause". It is a
progress bar for the current stall, not a gauge of how much audio is
buffered ahead — that is bufferedPosition. Once playback resumes
the number stops meaning anything, which is why this field is dropped the
moment the status leaves 'buffering' rather than left at a stale 100.
Quantised, deliberately, exactly like bufferedPosition: mpv republishes the value continuously while a stall fills, and each accepted change is a fresh snapshot and a full listener fan-out. Only movements of at least BUFFERING_PERCENT_STEP are adopted, so a spinner label updates a handful of times per stall instead of dozens of times per second.
chapter?
readonly optional chapter?: number;
Defined in: packages/player/src/state.ts:261
0-based index of the chapter the position is inside — mpv's chapter.
Remarks
undefined when the current entry has no chapters at all (mpv reports the
property unavailable), which is the common case and is how a UI should
decide whether to draw chapter controls. mpv also uses -1 for "the
position is before the start of the first chapter", and that value is
passed through as-is rather than smoothed to 0, because they are
genuinely different positions.
Read the chapters themselves with Player.getChapters; this is the cursor, exactly as playlist is the cursor for the queue.
coreIdle
readonly coreIdle: boolean;
Defined in: packages/player/src/state.ts:361
mpv's core-idle: true whenever no audio is being produced — which
includes paused, buffering, restarting and idle. Combined with
playing this is what separates buffering from ready.
duration?
readonly optional duration?: number;
Defined in: packages/player/src/state.ts:149
Duration of the current entry in seconds; undefined while unknown and
always undefined while isLive.
Remarks
The live suppression is not cosmetic. On an unseekable stream mpv's
duration is the length of what it has cached, not the length of the
broadcast: on-device it read 1.93 and climbed to 2.14 and beyond,
several times a second, forever. Publishing that would make every seek bar
lie and turn any duration-keyed broadcast into a ticker, so a live entry
reports no duration at all rather than a dishonest one.
eofReached
readonly eofReached: boolean;
Defined in: packages/player/src/state.ts:374
mpv's eof-reached.
Remarks
Never used to decide that a track ended — with keep-open=no (mpv's
default, which we keep) the property "will logically be cleared
immediately after it's set" (mpv input.rst, eof-reached), so the edge
is not reliably observable. Its only job here is to stop us reporting
buffering for a core that is idle because it hit EOF.
error?
readonly optional error?: PlayerError;
Defined in: packages/player/src/state.ts:286
The failure that produced status: 'error'. Present iff status is
'error'.
Remarks
How long it sticks, exactly — the three ways it clears, all of them already in the reducer and none of them a timer:
startFile— a new entry is loading, so every file-scoped field including this one is dropped. On a queue this is the common case: mpv advances past the failed entry within milliseconds, and the error is visible only in the window before the next entry starts.playbackRestart— audio is flowing again, which is the strongest possible evidence that whatever failed no longer applies. This is the "auto-clear on the next successful playback restart" rule.endFilewith reasonstop/quit— deliberate teardown; the state goes toidlewith nothing left over.
So it survives indefinitely in exactly one situation: the last entry
failed and nothing started after it. That is the case
Player.clearError exists for — a user dismissing a banner. Clearing
is a change to this snapshot only: the error event has already been
delivered and nothing suppresses, replays or un-logs it.
hasNext
readonly hasNext: boolean;
Defined in: packages/player/src/state.ts:237
Whether PlaylistApi.next would move to another entry.
Remarks
Loop-aware, and computed here so it cannot be got wrong per screen. The rules, each read off mpv's own behaviour rather than guessed:
loop: 'playlist'— mpv'smp_next_fileconsults--loop-playlistand wraps, so this istruefor any non-empty queue, including a queue of one (which re-plays that entry).loop: 'track'—loop-filedoes not affectplaylist-next, so this follows the plain rule below. Repeat-one does not remove the next button.- otherwise —
truewhile the cursor is before the last entry.
It travels in the snapshot with hasPrevious so the pair is always one coherent reading; deriving them separately in a component is how a UI ends up briefly offering a skip that does nothing.
hasPrevious
readonly hasPrevious: boolean;
Defined in: packages/player/src/state.ts:246
Whether PlaylistApi.previous would move to another entry.
Same loop rules as hasNext, mirrored. Note this describes the
queue, not the restart-or-previous behaviour: with the default
restartThreshold, previous() is still useful at the head of a queue
because it restarts the current entry. See PlaylistApi.previous.
idleActive
readonly idleActive: boolean;
Defined in: packages/player/src/state.ts:363
mpv's idle-active: nothing is loaded and the core is parked.
isLive
readonly isLive: boolean;
Defined in: packages/player/src/state.ts:355
Whether the current entry is an endless live stream with no meaningful total length.
Remarks
Exactly seekable === false on a loaded entry. mpv has no "is live"
property, but on every on-device sample seekable discriminated cleanly:
unseekable network streams (Icecast, HLS live) report false, finite
tracks report true.
Semantics, precisely:
- It is
falsebefore mpv has said anything — the honest default, since a just-issuedloadfileis far more often a finite track, andseekableis unavailable (notfalse) while the core is idle. - It flips to
truethe momentseekable = falsearrives while an entry is loaded (idle-active = no). - It resets to
falseon everystartFileand on everyplaylist-poschange, so a live entry cannot leak its liveness onto the finite track that follows it.
While it is true, duration is suppressed to undefined.
loop
readonly loop: LoopMode;
Defined in: packages/player/src/state.ts:213
Repeat behaviour.
loopRaw
readonly loopRaw: LoopRaw;
Defined in: packages/player/src/state.ts:215
The raw mpv strings loop was derived from.
muted
readonly muted: boolean;
Defined in: packages/player/src/state.ts:211
Whether output is muted (mpv's mute).
pitch
readonly pitch: number;
Defined in: packages/player/src/state.ts:207
Pitch multiplier (mpv's pitch); 1 is the file's own pitch.
Independent of rate — see Player.setPitch for the ratio ↔
semitone conversion and for why mpv's own scaletempo2 bounds the useful
range.
playing
readonly playing: boolean;
Defined in: packages/player/src/state.ts:136
Playback intent — the inverse of mpv's pause.
playlist
readonly playlist: PlaylistPosition;
Defined in: packages/player/src/state.ts:217
Playlist cursor.
positionAnchor
readonly positionAnchor: PositionAnchor;
Defined in: packages/player/src/state.ts:151
The anchor projectPosition extrapolates from.
positionAnchorMs
readonly positionAnchorMs: PositionAnchorMs;
Defined in: packages/player/src/state.ts:159
The same anchor in milliseconds, in the shape a media session broadcasts.
Derived from positionAnchor in the reducer and recomputed only when it (or whether the position is advancing) actually changes — see PositionAnchorMs.
rate
readonly rate: number;
Defined in: packages/player/src/state.ts:199
Playback rate multiplier (mpv's speed; mpv accepts 0.01–100).
seekable?
readonly optional seekable?: boolean;
Defined in: packages/player/src/state.ts:332
mpv's seekable: "whether it's generally possible to seek in the current
file". undefined until mpv publishes it for the current entry (and while
nothing is loaded), which is why it is tri-state rather than a boolean.
Useful directly — a UI should not offer a scrubber when this is false —
and it is the sole input to isLive.
seeking
readonly seeking: boolean;
Defined in: packages/player/src/state.ts:323
true while mpv is repositioning; position projection freezes.
status
readonly status: PlayerStatus;
Defined in: packages/player/src/state.ts:134
Coarse lifecycle stage. See PlayerStatus.
title?
readonly optional title?: string;
Defined in: packages/player/src/state.ts:321
Title of the current entry (mpv's media-title), when known.
Remarks
This is the now-playing surface, and it is one of two deliberately
different routes to a track's metadata. Read this one when you want the
line a lock screen shows; reach for Player.getMetadata() /
Player.getMetadataValue() when you want a specific tag.
state.title | metadataChanged + getMetadataValue | |
|---|---|---|
| shape | one coalesced string | the whole tag map, or one key |
| delivery | part of every snapshot, so it rides the state fan-out and every broadcast channel built on it | an event, and only while something is listening |
| cost | none beyond the snapshot | one node read per batch that touched the tags |
| on a radio stream | follows StreamTitle automatically | icy-title, icy-name, icy-genre, icy-br … individually |
mpv derives media-title from the demuxer's title tag when there is one
and from ICY's icy-title on a live stream, and invalidates both it and
metadata on the same MP_EVENT_METADATA_UPDATE (player/command.c). So
on an Icecast/Shoutcast station this field is the currently-playing song,
changing every few minutes without a track change — which is exactly what a
media session wants to publish, and exactly why it lives in state rather
than behind a pull.
The reason both exist: a media session re-broadcasts state, not events,
so the now-playing line has to be a state field or it cannot reach the
notification at all. And the full tag map cannot be a state field, because
building it costs a synchronous read and most apps never look at it — see
PlayerEventMap.metadataChanged, which is why that one is opt-in.
One consequence worth knowing on a live stream: this field carries the
song, and the station is only in the tag map (icy-name). An app that
wants both needs both routes.
volume
readonly volume: number;
Defined in: packages/player/src/state.ts:209
Volume normalised to 0..1; 1 is mpv's volume=100 (unattenuated).