Skip to main content

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:

  1. 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.
  2. 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.
  3. endFile with reason stop/quit — deliberate teardown; the state goes to idle with 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's mp_next_file consults --loop-playlist and wraps, so this is true for any non-empty queue, including a queue of one (which re-plays that entry).
  • loop: 'track' — loop-file does not affect playlist-next, so this follows the plain rule below. Repeat-one does not remove the next button.
  • otherwise — true while 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 false before mpv has said anything — the honest default, since a just-issued loadfile is far more often a finite track, and seekable is unavailable (not false) while the core is idle.
  • It flips to true the moment seekable = false arrives while an entry is loaded (idle-active = no).
  • It resets to false on every startFile and on every playlist-pos change, 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.titlemetadataChanged + getMetadataValue
shapeone coalesced stringthe whole tag map, or one key
deliverypart of every snapshot, so it rides the state fan-out and every broadcast channel built on itan event, and only while something is listening
costnone beyond the snapshotone node read per batch that touched the tags
on a radio streamfollows StreamTitle automaticallyicy-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).