Adapter: Chapter Source
IChapterSource is a small, standalone contract for chapter lookup: load once per item, then answer “which chapter is active at this time” synchronously, cheap enough to call on every scrubber hover. Unlike Adapter: Video Backend, this is not a setup() config field, the player’s own chapters() / chapter() / seekToChapter() surface (see Chapters) reads item.chapters directly and never consults this interface. IChapterSource exists for custom UI code that wants the same lookup shape without re-deriving it, and as an extension point for chapter data that doesn’t already live on the item.
The interface
interface IChapterSource {
/** Called after loadedmetadata. Fetch/cache here so current() is synchronous. */
load(item: BasePlaylistItem): Promise<Chapter[]>;
/** Chapter active at timeSeconds, or null outside every chapter's range. Synchronous. */
current(timeSeconds: number): Chapter | null;
/** The complete chapter list for the current item. Synchronous. */
all(): Chapter[];
/** Release cached data. Called on unload/dispose. */
unload(): void;
}
The shipped implementation
VttChapterSource reads chapters straight from item.chapters, the typed field a consumer populates directly (canonical ChapterRef[]) or normalizePlaylistItem derives from the server wire format’s millisecond-based chapters array at queue-ingest time (see The Queue & Playlist). It does not fetch or parse a file itself, the list is already resolved by the time load() runs, this class’s job is only the synchronous current(time) lookup on top of whatever list is already there:
import nmplayer from '@nomercy-entertainment/nomercy-video-player';
const player = nmplayer('player');
const source = new VttChapterSource();
await source.load(player.item()!); // caches item.chapters
const active = source.current(player.time()); // Chapter | null, no async lookup
Rules & restrictions
load()returns an empty array for an item with nochaptersfield, this is a legitimate “no chapters” state, not an error.current()andall()must stay synchronous in any implementation, they’re called from hot paths (scrubber hover,timeupdate-driven UI) where anawaitwould introduce visible lag.- Not wired into
DesktopUiPlugineither, its own chapter-marker rendering (chapterMethods.ts) readsplayer.chapters()directly, the same source of truth this adapter’s default implementation reads from, just without going through theIChapterSourceinterface.
How to extend it
Implement the four methods against a source normalizePlaylistItem doesn’t already cover, a chapters endpoint separate from the item payload, or any format your server doesn’t already ship as the item’s chapters field:
class ApiChapterSource implements IChapterSource {
private chapters: Chapter[] = [];
async load(item: BasePlaylistItem): Promise<Chapter[]> {
this.chapters = await fetchChaptersFromApi(item.id);
return this.chapters;
}
current(timeSeconds: number): Chapter | null {
return this.chapters.find((c) => timeSeconds >= c.start && timeSeconds < c.end) ?? null;
}
all(): Chapter[] {
return this.chapters;
}
unload(): void {
this.chapters = [];
}
}
Call load() yourself from a 'mediaReady' listener in your own overlay code, there is no automatic hook that invokes it.
Next steps
- Adapter: Thumbnail Source: the same standalone, sync-lookup shape applied to scrub-preview thumbnails.