Skip to main content

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

  1. An entry ends with an error whose typed classification is retryable.
  2. If that entry has attempts left, the player jumps back to it (playlist-play-index), preserving whether it was playing, and emits PlayerEventMap.retrying. No error event is emitted for that attempt — nothing has failed for good yet.
  3. On success the counter resets. On another failure step 1 runs again.
  4. When the attempts run out, the advance mpv already performed is left alone and the error event 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 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

DEFAULT_RETRY_MAX_ATTEMPTS


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.