Skip to main content

Interface: PlaylistAddOptions

Defined in: packages/player/src/player.ts:1139

Options for PlaylistApi.add.

Remarks

Before you use any of this: an insert renumbers the queue. That sentence is obvious and its consequence is not, so it is written down here rather than left to be discovered.

Apps almost always keep a side table mapping playlist index → their own track metadata (artwork, ids, analytics keys), because PlayerState carries a playlist cursor and not its contents. Every such map is invalidated the moment an entry is inserted anywhere but the end: position: 'next' and position: <n> push every later entry down by one, and after that index k describes the track that used to be at k - 1. Nothing throws, nothing warns — the queue is correct, the app's labels are one row off, and the symptom shows up later as the wrong artwork on the wrong song.

PlaylistApi.remove and PlaylistApi.move have the identical property, and PlaylistApi.shuffle has it maximally.

The fix is to key on identity, not position. Read PlaylistApi.entries and use entryId — mpv's own entry id, "unique for the entire life time of the current mpv core instance" (mpv 0.41.0 input.rst), which survives inserts, removes, moves and shuffles — or the entry's uri if your sources are unique. Re-read after every PlayerEventMap.queueChanged; that is what the event is for.

// Fragile: `myTracks[index]` after any insert.
// Stable:
const byId = new Map(myTracks.map((t) => [t.uri, t]))
const rows = player.playlist.entries().map((e) => ({
...e,
track: byId.get(e.uri),
}))

Extends

Properties

headers?

readonly optional headers?: Readonly<Record<string, string>>;

Defined in: packages/player/src/player.ts:697

HTTP request headers for this source only — the typed form of mpv's http-header-fields.

Example

await player.load(`${server}/Audio/${id}/stream`, {
headers: { Authorization: `MediaBrowser Token="${token}"` },
})

Throws

PlayerErrorException with code invalid-state when a header name is empty, padded with whitespace, or contains :, CR, LF or NUL, or when a value contains CR, LF or NUL. mpv writes these lines into the request verbatim (stream/stream_lavf.c:218 joins each with \r\n), so those characters are request splitting, not a formatting preference.

Remarks

Why this exists rather than "just use mpvOptions". The raw route was unsafe in exactly the case people reach for it: http-header-fields is itself a ,-separated list, and the file-option string it travels in is also ,-separated, so any header containing a comma (Accept: text/html, application/xml, a multi-valued Cache-Control, a cookie pair) used to corrupt the whole option list. This path escapes both layers — the list separator with mpv's backslash form, the option value with mpv's fixed-length %n% form — so a header value can contain anything except the characters above.

Interaction with PlayerOptions.userAgent. They are different mpv options (user-agent vs http-header-fields) and both are sent, so a per-source User-Agent header does not silently disappear — but it does take precedence, because FFmpeg only appends its own user_agent line if (!has_header(s->headers, "\r\nUser-Agent: ")) (libavformat/http.c, FFmpeg 8.1.2, the tree these binaries are built from). Set one or the other, not both.

Interaction with SourceResolver. Headers belong to the entry, not to the URL, and survive a rewrite: mpv applies per-file options in load_per_file_options() (player/loadfile.c:1707) and only then runs the on_load hook that rewrites stream-open-filename (loadfile.c:1725). So a resolver that swaps a logical URI for a signed CDN URL still sends the headers the queue entry carried. If your signed URL makes the header redundant, drop the header — nothing removes it for you.

What it does not do. These are HTTP(S) options. file://, and any protocol not served by libavformat's HTTP client, ignore them (mpv: "Unknown or misspelled options are silently ignored").

Inherited from

SourceOptions.headers


mpvOptions?

readonly optional mpvOptions?: Readonly<Record<string, string>>;

Defined in: packages/player/src/player.ts:708

Extra per-file mpv options, e.g. { 'audio-channels': 'stereo' }.

Values are escaped with mpv's own fixed-length quoting before they are joined into loadfile's option list, so a value may contain commas, colons, quotes and spaces. A key given here wins over the typed options above it: pass 'http-header-fields' yourself and headers is not emitted at all (and you own both layers of escaping); pass 'demuxer' and the .m3u8 guard steps aside.

Inherited from

SourceOptions.mpvOptions


play?

readonly optional play?: boolean;

Defined in: packages/player/src/player.ts:1191

Start playback if nothing is currently playing — mpv's *-play action variants.

Remarks

This is mpv's wording, not a softening of ours: "Append the file, and if nothing is currently playing, start playback. (Always starts with the added file, even if the playlist was not empty before running this command.)" (mpv 0.41.0 input.rst). On a player that is already playing it does nothing at all — it is not "play this now". For that, add the entry and then PlaylistApi.jumpTo it.


position?

readonly optional position?: number | "next";

Defined in: packages/player/src/player.ts:1178

Where the entry goes. Omitted, it is appended to the end.

  • 'next' — directly after the entry that is currently playing (mpv's insert-next). This is the "play this after the current track" button, and it is index-free on purpose: it stays correct even if the queue moves between your reading it and mpv acting on it.
  • a number — an exact 0-based index, where 0 is the head and playlist.count is the end (mpv's insert-at, which takes the index as loadfile's third argument).

Remarks

A number is validated, not clamped. mpv itself would silently append — "the new item will be inserted at the index position in the playlist, or appended to the end if index is less than 0 or greater than the size of the playlist" (mpv 0.41.0 input.rst) — which turns a caller's off-by-one into a track at the wrong end of the queue with nothing to notice it by. So a non-integer, a negative, or an index past the current playlist.count throws an invalid-state PlayerError, exactly like every other out-of-domain option in this API. The count is read from mpv at call time (playlist-count), not from the last broadcast snapshot.

Honest caveat: an inserted entry is not itself prefetched if a prefetch is already running. With prefetchPlaylist on, mpv opens the next entry as soon as the current one is fully read — and prefetch_next() begins if (!mpctx->opts->prefetch_open || mpctx->open_active) return; (mpv 0.41.0 player/loadfile.c:1278). So once that opener is running, an entry inserted in front of it gets no prefetch of its own, and at the boundary open_demux_reentrant() compares the running opener's URL against the one it now needs (strcmp(mpctx->open_url, url), loadfile.c:1223), logs Dropping finished prefetch of wrong URL. (or Aborting ongoing prefetch…), calls cancel_open() — which joins that thread on the core thread — and opens cold. The insert is still correct; it just costs the prefetch that was in flight, which is the same trade mpv's own manual warns about for any queue edit near a boundary (PlayerOptions.prefetchPlaylist). Inserting well before the current track ends is free.


startPosition?

readonly optional startPosition?: number;

Defined in: packages/player/src/player.ts:649

Start position in seconds (mpv's per-file start option).

On Player.loadPlaylist this applies to one entry — the one at startIndex — and not to the rest of the queue. See LoadPlaylistOptions.startPosition.

Inherited from

SourceOptions.startPosition