Interface: RetryOptions
Defined in: packages/player/src/player.ts:375
Give a failed entry another go before the queue moves past it.
Remarks
mpv's own behaviour on a hard failure is to advance: the entry ends with
MPV_END_FILE_REASON_ERROR and the next one starts. That is right for a file
that will never play and wrong for a stream that was unlucky, and nothing in
mpv distinguishes the two. This option is where that distinction is made, and
it is made from PlayerError.retryable — the same flag a UI reads to
decide whether to draw a Retry button.
Exactly what happens
- An entry ends with an
errorwhose typed classification isretryable. - If that entry has attempts left, the player jumps back to it
(
playlist-play-index), preserving whether it was playing, and emits PlayerEventMap.retrying. Noerrorevent is emitted for that attempt — nothing has failed for good yet. - On success the counter resets. On another failure step 1 runs again.
- When the attempts run out, the advance mpv already performed is left
alone and the
errorevent fires with the attempt count in its second argument.
There is deliberately no delay between attempts
Not "we did not get to it" — a delay is the one thing this layer must not have. The only way to wait in JavaScript is a timer, and JS timers freeze with the screen off (ARCHITECTURE, "Platform truths"), so a backoff written here would silently become "retry when the user next unlocks the phone" — a bug that is invisible in every test that runs with the display on. Spaced, backed-off retrying is owned by the layer that can actually do it natively: PlayerOptions.networkReconnect. This layer only answers the question that layer cannot see, "should the queue move on?", and it answers it immediately.
The consequence to know: a re-attempt is issued a moment after mpv has already started the next entry, so a failure at a queue boundary can produce a brief blip of the following track before the failed one restarts.
When the counter resets
Attempts are tracked per entry generation, not per player. The count resets when:
- the entry plays successfully (any
playbackRestartreachingready), - the failure is on a different playlist index than the one being retried,
- the app moves the cursor itself — PlaylistApi.jumpTo, PlaylistApi.next, PlaylistApi.previous, Player.load, Player.loadPlaylist,
- the app edits the queue — PlaylistApi.add,
remove,move,clear,shuffle,unshuffle.
The cursor moves are also the cancellation rule: a user who skips during a retry has said what they want, and the player stops arguing.
Properties
maxAttempts?
readonly optional maxAttempts?: number;
Defined in: packages/player/src/player.ts:385
How many extra attempts one entry gets before the queue is allowed to move
past it. 0 disables retrying entirely (mpv's own behaviour).
Must be a non-negative integer; anything else throws an invalid-state
PlayerError.
Default Value
retryLiveEof?
readonly optional retryLiveEof?: boolean;
Defined in: packages/player/src/player.ts:436
Treat a clean end of a live entry as a retryable failure.
Default Value
false
Remarks
The failure this covers is the one neither other layer can see: a radio
server that closes the connection politely. FFmpeg's reconnection
({@link PlayerOptions.networkReconnect}) does not act on it, deliberately
— reconnect_at_eof is the option that would, and it is unsafe as a global
default because http.c:1871 does not guard it on is_streamed, so it
would turn every finite track's natural end into a reconnect storm (see
NetworkReconnectOptions). mpv then reports
MPV_END_FILE_REASON_EOF — a clean end — and the queue moves on. For a
file that is right. For a station it is the one thing the listener did not
ask for.
With this on, an entry that ends with eof while
PlayerState.isLive was true takes the same path a retryable
error takes: it re-attempts under the same per-entry budget
(maxAttempts), emits PlayerEventMap.retrying with a
synthesised network error, and emits no
PlayerEventMap.trackEnded for that attempt. A finite entry is never
affected, whatever this is set to — isLive is mpv's seekable = no, and
a seekable file ends for good.
There is no delay here either, and for the same reason as the rest of this option: a JS timer would freeze with the screen off. This layer re-attempts immediately; spaced retrying belongs to PlayerOptions.networkReconnect, which owns the transient drop. This one owns the clean close.
When the budget resets — sustained playback, not the restart
The ordinary retry budget resets on the first playbackRestart. That rule
cannot be used here: a station that reconnects, plays for a second and
drops again would clear its budget on every reconnect and re-attempt
forever. So a live-eof generation resets only after
LIVE_EOF_BUDGET_RESET_SECONDS of playback since the restart — long
enough that a station which drops once an hour keeps recovering all day,
short enough that a server serving two seconds and hanging up exhausts its
budget and stops.
The trade, stated plainly
A broadcast that has genuinely ended will be re-attempted maxAttempts times before the queue moves on. That is bounded by design and it is the honest cost: nothing in the protocol distinguishes "this station is off the air" from "this station's server just closed a socket". The price of recovering the second is a few extra connects on the first.