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
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
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'sinsert-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, where0is the head andplaylist.countis the end (mpv'sinsert-at, which takes the index asloadfile'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.