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).