Skip to content

Recipe: Register a Custom Cue Parser

Cue Parsers demonstrated the ICueParser shape against a bare, empty CueParserRegistry, to show the extension point in isolation. This recipe is the same format wired into an actual player, alongside the built-ins a real player pre-seeds, not instead of them.

The task

A media server sends chapter markers in a small proprietary text format instead of a WebVTT chapter track. setup({ cueParsers: [...] }) registers the format for the whole player lifetime, and player.resolveCueParser(url) proves it resolves without disturbing .vtt resolution.

TypeScript
/**
* Recipe: register a custom cue parser on a real player.
*
* [Cue Parsers](/nomercy-player-core/tour/cue-parsers) demonstrated the
* `ICueParser` shape against a bare, empty registry. This recipe wires the
* same idea into an actual player: pass the parser via `setup({ cueParsers })`
* so it's registered alongside the built-in VTT/sprite-VTT/LRC parsers a real
* player pre-seeds, instead of replacing them.
*/

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

interface ChapterMarkerPayload {
label: string;
}

// A small proprietary chapter-marker format: one "seconds\tlabel" pair per line.
const chapterMarkerParser: ICueParser<ChapterMarkerPayload> = {
id: 'demo:chapter-markers',
canParse: (url) => url.endsWith('.chapters.txt'),
parse: (raw) => {
const marks = raw
.trim()
.split('\n')
.map((line) => {
const [at, label] = line.split('\t');
return { start: Number(at), end: Number(at), label: label ?? '' };
});
return createCueList(
marks.map((mark) => ({ start: mark.start, end: mark.end, payload: { label: mark.label } })),
);
},
};

const player = tourPlayer('custom-cue-parser-demo');

player.setup({
logLevel: 'info',
cueParsers: [chapterMarkerParser], // registered alongside the built-ins, not instead of them
});
await player.ready();

// The custom format resolves for its own extension...
console.log(player.resolveCueParser('movie.chapters.txt')?.id); // 'demo:chapter-markers'
// ...while .vtt still resolves to the kit's built-in parser, unaffected.
console.log(player.resolveCueParser('captions.vtt')?.id); // 'vtt'

// registerCueParser/unregisterCueParser are the runtime-facing equivalent of
// the setup()-time cueParsers list, for formats that aren't known upfront.
player.unregisterCueParser('demo:chapter-markers');
console.log(player.resolveCueParser('movie.chapters.txt')); // undefined — unregistered

await player.dispose();

Registering later instead of at setup

Not every format is known upfront. player.registerCueParser(parser, prepend?) does the same registration at runtime, for a parser loaded from a plugin bundle or negotiated after the server responds. The registry resolves most-recently-registered first, so a plain call outranks every parser already registered, including the built-ins. Pass prepend: true to register it at the low-priority end instead, behind the current set, so it only wins when nothing else claims the URL. unregisterCueParser(id) reverses either path.

Next steps

  • Recipe: Use authFetch: the kit’s auth-aware fetch pipeline, standalone, for data-layer code that isn’t a plugin.