Skip to content

What is a chapter source?

IChapterSource is the contract for loading and querying chapter data for a playlist item. VttChapterSource is a ready-made implementation of it, exported for consumers building a custom chapter source. It is not wired into the player by default: player.chapters() is resolved by the player core’s media-tracks layer, which reads the inline chapters field or fetches a sidecar kind: 'chapters' VTT. A custom implementation could fetch chapters from an API endpoint or parse a different file format.

IChapterSource interface

TypeScript
import type { IChapterSource } from '@nomercy-entertainment/nomercy-video-player';
MethodSignatureDescription
load(item)(item: BasePlaylistItem) => Promise<Chapter[]>Fetch and cache chapters for the item. Called after loadedmetadata
current(timeSeconds)(timeSeconds: number) => Chapter | nullReturn the chapter active at timeSeconds. Must be synchronous
all()() => Chapter[]Return all chapters for the current item. Synchronous
unload()() => voidRelease cached chapter data. Called on unload and dispose

How chapters are resolved

The player core’s media-tracks layer resolves chapters for player.chapters(). It reads the chapters typed array on VideoPlaylistItem when present:

TypeScript
import type { VideoPlaylistItem } from '@nomercy-entertainment/nomercy-video-player';

// baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Films'
const item: VideoPlaylistItem = {
url: '/Tears.of.Steel.(2012)/Tears.of.Steel.(2012).NoMercy.m3u8',
chapters: [
{ index: 0, title: 'Opening', start: 0, end: 90 },
{ index: 1, title: 'Act 1', start: 90, end: 720 },
{ index: 2, title: 'Credits', start: 720, end: 730 },
],
};

When chapters is present and non-empty, the list is used directly without any network request.

You do not construct a chapter source yourself; the media-tracks layer handles resolution. The simplest way to show chapters is to pre-populate the item’s chapters field:

TypeScript
import type { VideoPlaylistItem } from '@nomercy-entertainment/nomercy-video-player';

// baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Films'
const item: VideoPlaylistItem = {
url: '/Sintel.(2010)/Sintel.(2010).NoMercy.m3u8',
chapters: [
{ index: 0, start: 0, end: 270, title: 'Opening' },
{ index: 1, start: 270, end: 540, title: 'The Hunt' },
{ index: 2, start: 540, end: 888, title: 'Finale' },
],
};

player.setup({ playlist: [item] });

To fetch chapters from a VTT file or an API instead of pre-populating them, implement a custom IChapterSource, shown next.

Custom chapter source

Implement IChapterSource to load chapters from a different format or source. When calling your API from inside a plugin, use this.fetch() instead of the global fetch. The plugin’s this.fetch() applies the player’s bearer token, handles 401 refresh-and-retry, and respects the retry policy, so raw fetch() skips all of that.

TypeScript
import { Plugin } from '@nomercy-entertainment/nomercy-player-core';
import type { Chapter } from '@nomercy-entertainment/nomercy-player-core';
import type { NMVideoPlayer, VideoPlaylistItem } from '@nomercy-entertainment/nomercy-video-player';

// IChapterSource is an interface, not a config field.
// The pattern is to build a Plugin that loads chapters on 'current'
// and emits 'chapters' so the player and UI receive the list.
class ApiChapterSourcePlugin extends Plugin<NMVideoPlayer<VideoPlaylistItem>> {
static readonly id = 'api-chapter-source';
static readonly version = '1.0.0';

override use(): void {
this.on('current', async ({ item }) => {
if (!item) return;
const data = await this.fetch<Array<{ name: string; start_ms: number; end_ms: number }>>(
`/api/chapters/${item.id}`,
{ responseType: 'json' },
);
const chapters: Chapter[] = data.map((ch, index) => ({
index,
title: ch.name,
start: ch.start_ms / 1000,
end: ch.end_ms / 1000,
}));
this.player.emit('chapters', { chapters });
});
}
}

player.addPlugin(ApiChapterSourcePlugin);

Loading chapters inside a plugin and emitting the chapters event is the supported pattern for custom sources; for inline and sidecar-VTT chapters the player core does the work itself.

Chapter type

Chapter is defined in @nomercy-entertainment/nomercy-player-core:

TypeScript
interface Chapter {
index: number;
start: number; // seconds
end: number; // seconds
title: string;
}

Querying chapters

The player exposes chapter queries through its top-level API:

TypeScript
// All chapters for the current item
const chapters = player.chapters();

// Chapter active at the current playback position
const currentChapter = player.chapter();

// Navigate chapters
player.nextChapter();
player.previousChapter();

The 'chapter' event is emitted by seekToChapter() / nextChapter() / previousChapter() (not on natural playback boundary crossing). The time-aware active chapter is available from the chapter() getter:

TypeScript
player.on('chapter', ({ index, title }) => {
console.log(`Now in chapter ${index}: ${title}`);
});

See also