Skip to content

Core: The Queue

NMMusicPlayer and NMVideoPlayer don’t maintain their own array of playlist items with a separate cursor variable. They delegate to one MediaList<T> instance each, and every queue method on the player is a thin wrapper over it.

MediaList

MediaList<T extends BasePlaylistItem> is a cursor-aware ordered list: get() / set(items) for the whole array, current() / currentIndex() / setCurrent(target) for the cursor, peekNext() / peekPrevious() to read without moving it, and the mutation set (append, prepend, insert, remove, removeAt, move, clear, shuffle, sort). setCurrent accepts an item reference, its id, a numeric index, or a predicate, so callers never have to resolve an index by hand first.

Every mutation that shifts item positions shifts the cursor with it, so it keeps pointing at the same logical item: removing the item before the current one decrements the cursor, a shuffle re-finds the playing item’s new position and follows it. Every mutation emits a typed event (change, append, current, and so on) that the player mixin re-emits on the public event bus as queue, queue:append, item, and the rest, so a UI never has to poll.

Two lists exist per player: _queueList for what’s coming, and _backlogList for what already played, so previous() can look back past the queue head without the primary list growing unbounded in the wrong direction.

The player-facing surface

queue() / queueAppend() / queuePrepend() / queueInsert() / queueRemove() / queueRemoveAt() / queueMove() / queueClear() / queueShuffle() / queueSort() all delegate straight to _queueList, wiring its events to the public event bus on first use. item(target) moves the cursor and, once the player is past setup, kicks off a load() for the resolved item; peekNext() / peekPrevious() / queueLength() / queueIndexOf(id) / index() read without side effects.

playItem(target) and playNow(items, start?) exist because queue(items); item(start); play() races: item() fires a fire-and-forget load(), and a play() called right after it can reach the backend before the source is even set. Both helpers route through item(target, { autoplay: true }) internally, which only calls play() inside the resolved load()’s own continuation, never before it.

Every entry ingested by queue() / queueAppend() / queuePrepend() / queueInsert() passes through a normalization step first: your player class’s optional normalizePlaylistItem (for accepting a legacy wire format), then the consumer’s transformPlaylistItem config callback, then title-token interpolation for any tokens your player registered via registerTitleTokens().

TypeScript
/**
* The queue. `queue()` / `queueAppend()` / `item()` and friends all delegate
* to a single `MediaList<T>` per player — the cursor-aware ordered list both
* real players share instead of maintaining parallel queue state. A real
* backend additionally loads media on every cursor move; the queue mechanics
* below (cursor tracking, events, peeking) are identical with or without one.
*/

import type { BasePlaylistItem } from '@nomercy-entertainment/nomercy-player-core';
import { tourPlayer } from './tour-player';

interface Episode extends BasePlaylistItem {
id: string;
title: string;
durationSeconds: number;
}

const episodes: Episode[] = [
{ id: 'ep-1', title: 'Episode 1: Pilot', durationSeconds: 1800 },
{ id: 'ep-2', title: 'Episode 2: Follow-up', durationSeconds: 2100 },
{ id: 'ep-3', title: 'Episode 3: Finale', durationSeconds: 1950 },
];

const player = tourPlayer('queue-demo');
player.setup({ logLevel: 'info' });
await player.ready();

player.on('queue', (items) => console.log('queue replaced:', items.length, 'items'));
player.on('queue:append', ({ items }) => console.log('appended:', items.length, 'item(s)'));

player.queue(episodes);

// Setting the queue seeds the cursor at index 0 — no separate item() call needed.
console.log(player.item()?.title); // 'Episode 1: Pilot'
console.log(player.peekNext()?.title); // 'Episode 2: Follow-up' — without moving the cursor
console.log(player.queueLength()); // 3

const bonus: Episode = { id: 'ep-4', title: 'Episode 4: Bonus', durationSeconds: 900 };
player.queueAppend(bonus);
console.log(player.queueIndexOf('ep-4')); // 3

player.queueMove(3, 1); // move the bonus episode right after the pilot
console.log(player.queue().map((item) => item.id)); // ['ep-1', 'ep-4', 'ep-2', 'ep-3']

await player.dispose();

Next steps

  • The Plugin Base: the class that lets behavior extend the player without forking it.