Chapters
Chapters come from the item’s typed chapters field: canonical ChapterRef[] ({ index, start, end, title }, seconds) set directly, or the server’s wire format’s millisecond-based chapters array, which normalizePlaylistItem converts to the canonical shape at queue-ingest time. chapters() is a plain synchronous getter over whatever list is already on the active item, there’s no async fetch to wait on.
Listing and navigating
chapters() returns the resolved list, each entry an index, start, and end in seconds, plus a display title. seekToChapter(idx) jumps directly; nextChapter() / previousChapter() step relative to the current position, not the last chapter you explicitly selected, so they stay correct even after a manual scrub:
import nmplayer from '@nomercy-entertainment/nomercy-video-player';
const player = nmplayer('player');
const list = player.chapters(); // Chapter[]: { index, start, end, title }
player.seekToChapter(2);
player.nextChapter();
player.previousChapter();
chapter() reads the chapter containing the current playback position, or null when no chapter covers it (before the list resolves, or in a gap the safety net has not backfilled yet). Only seekToChapter() emits 'chapter' with { index, title }, and nextChapter() / previousChapter() inherit it because they route through seekToChapter(). Playback crossing a boundary on its own does not fire it, so to follow the active chapter during ordinary playback, read chapter() on each 'time' tick. The 'chapters' event fires once the list resolves.
chapters() returns [] for an item with no chapters field, a chapter-marker UI can render unconditionally and simply show nothing when the list is empty.
Malformed lists get a safety net. Once the media duration is known, any span the list fails to cover (a lead-in before the first chapter, a hole between two chapters, or a tail after the last one) is backfilled with a synthetic chapter carrying a translated fallback title (English “Chapter”) and synthetic: true, so markers and nextChapter() always operate on a list that spans the whole timeline. A list that already covers the full duration passes through untouched, and an empty list stays empty; the net only catches lists that claim partial coverage.
Arbitrary time windows
Chapters are one instance of a more general primitive: playSegment({ startSec, endSec, onEnd }) seeks to startSec and watches every 'time' event for endSec. When the boundary is reached, 'segmentBoundary' always fires first, then onEnd decides what happens:
onEnd | Behavior |
|---|---|
'loop' | Seek back to startSec and keep playing. |
'hold' | Pause at endSec, last frame stays visible. |
'advance' | Emit the boundary event and let playback continue naturally. |
player.playSegment({ startSec: 30, endSec: 45, onEnd: 'loop' }); // a 15s preview loop
player.clearSegment(); // stop watching, no seek or pause
This isn’t chapter-specific, it’s the same primitive an intro-skip loop, a disc-menu state window, or a highlight-reel scrubber would use. Any previously active segment window is replaced immediately when you call playSegment() again; clearSegment() stops watching without moving playback.
Next steps
- Playback State & Events: playState(), bufferState(), and the full event surface a UI subscribes to.