Skip to content

Core: Cue Parsers

Subtitles, lyrics, and sprite-preview thumbnails are all the same shape underneath: timed text or data, looked up by playback position. The kit parses all three into one structure.

CueList

createCueList(cues) builds an immutable, time-indexed CueList<T> from an array of { start, end, payload } entries. Input order doesn’t matter, it sorts by start on construction, and every lookup is O(log n) via binary search: active(time) returns every cue whose interval contains time (support for overlapping cues is intentional, parallel subtitle tracks and adjacent sprite thumbnails both need it), next(time) and prev(time) locate the nearest cue on either side. createMutableCueList() exposes the same query surface over a live buffer that grows and shrinks at runtime, for streamed-in captions or chapter lists, notifying subscribe()d listeners on every change.

Built-in parsers

Three ICueParser implementations ship with the kit and auto-register during setup(): lrcParser (.lrc, plain or word-level enhanced LRC), vttSubtitleParser (.vtt, WebVTT with inline <b> / <i> / <u> tags preserved and cue-setting alignment), and spriteVttParser (sprite-sheet crops via #xywh= fragments, only for URLs that hint at sprites). Each has an id, a canParse(url, contentType?) predicate, and a parse(raw, opts?) function.

The registry

CueParserRegistry resolves most-recently-registered first: register(parser) adds one (or replaces an existing entry with the same id), resolve(url, contentType?) walks the list newest-first and returns the first match. A real player pre-seeds its registry with the three built-ins during setup(), so a custom parser registered afterward, or supplied via setup({ cueParsers: [...] }), wins resolution over the built-in for the same URL pattern without disabling it. player.registerCueParser(parser, prepend?) / unregisterCueParser(id) / resolveCueParser(url) are the runtime-facing wrappers around the same registry.

The example below parses real VTT and LRC text directly (parseVtt, parseVttSubtitles, parseLrc, and their sprite counterpart are exported standalone, no player needed), then registers a small custom format against a bare registry to show the extension point in isolation.

TypeScript
/**
* Cue parsers turn raw text into a `CueList`: a time-indexed, binary-searched
* structure (`active(t)` / `next(t)` / `prev(t)`), not a plain array. The kit
* ships VTT, sprite-VTT, and LRC parsers behind a `CueParserRegistry` that a
* real player pre-seeds with all three; a bare `new CueParserRegistry()` (as
* built here) starts empty, so custom formats can be demonstrated in isolation.
*/

import type { ICueParser } from '@nomercy-entertainment/nomercy-player-core';
import {
CueParserRegistry,
createCueList,
parseLrc,
parseVttSubtitles,
} from '@nomercy-entertainment/nomercy-player-core';

const vtt = `WEBVTT

00:00:01.000 --> 00:00:04.000
Hello there.

00:00:04.500 --> 00:00:07.000
<b>Bold</b> caption line.
`;

const subtitleCues = parseVttSubtitles(vtt);
console.log(subtitleCues.cues.length); // 2
console.log(subtitleCues.active(2)[0]?.payload.text); // 'Hello there.' — the cue covering t=2s
console.log(subtitleCues.next(4.2)?.payload.text); // 'Bold caption line.' — the next cue after t=4.2s

const lrc = `[00:12.50]First line
[00:16.00]Second line
`;

const lyricCues = parseLrc(lrc);
console.log(lyricCues.cues.length); // 2
console.log(lyricCues.active(13)[0]?.payload.text); // 'First line' — active until the next cue starts

// Custom formats register alongside the built-ins a real player seeds — most
// recently registered wins resolution for a given URL.
interface ChapterMarkerPayload {
label: string;
}

const chapterMarkerParser: ICueParser<ChapterMarkerPayload> = {
id: 'demo:chapter-markers',
canParse: (url) => url.endsWith('.chapters.json'),
parse: (raw) => {
const marks = JSON.parse(raw) as { at: number; label: string }[];
return createCueList(
marks.map((mark) => ({ start: mark.at, end: mark.at, payload: { label: mark.label } })),
);
},
};

const registry = new CueParserRegistry();
registry.register(chapterMarkerParser);

console.log(registry.resolve('movie.chapters.json')?.id); // 'demo:chapter-markers'
console.log(registry.resolve('captions.vtt')); // undefined — this standalone registry has no VTT parser

Next steps

  • Typed Errors: the PlayerError hierarchy every failure path in this tour, from a rejected ready() to a parse failure, surfaces through.