Skip to content

LyricsPlugin

You need synced lyrics. A track object already has a lyricsUrl field, but something still needs to fetch the file, pick the right parser, and translate raw cue timestamps into events your UI can respond to at exactly the right moment. That is what this plugin does. On every current event it resolves the lyric URL, fetches the file through the core’s auth-aware fetch, runs the content through the cue parser registry, and hands the result to a CueTracker that fires line, lineEnter, and lineExit events in sync with playback.

TypeScript
import nmMPlayer from '@nomercy-entertainment/nomercy-music-player';
import { LyricsPlugin } from '@nomercy-entertainment/nomercy-music-player/plugins';
import type { LyricsEvents, LyricsOptions } from '@nomercy-entertainment/nomercy-music-player/plugins';
import type { MusicPlaylistItem } from '@nomercy-entertainment/nomercy-music-player';

Plugin id: 'lyrics'

What it does

The plugin hooks into current and runs automatically when autoFetch is enabled (the default). A CueTracker wakes at the right timestamp and emits lineEnter and lineExit on the plugin channel, which the player re-emits as plugin:lyrics:lineEnter and plugin:lyrics:lineExit. The line event fires alongside lineEnter as a convenience alias for single-line karaoke-style displays.

The URL file extension decides which parser runs. .lrc and .vtt parsers are pre-registered by the core. For other formats call player.registerCueParser(myParser) before setup().

Options

OptionTypeDefaultDescription
getLyricsUrl(track: MusicPlaylistItem) => string | undefinedundefinedCustom URL resolver. When provided it takes precedence over track.lyricsUrl. Use it when lyrics are stored under a URL pattern derived from the track id or when you need to sign the URL.
autoFetchbooleantrueAutomatically fetch and attach lyrics on every current event. Set to false when you want manual control via fetchLyrics(url).

Events

Listen from outside the plugin using the namespaced string form on player.on().

EventPayloadDescription
plugin:lyrics:loaded{ count: number }Fires at the end of attach() once the cue list is fetched, parsed, and the tracker is attached. count is the number of loaded cues.
plugin:lyrics:line{ text: string; [key: string]: unknown }Fires on every line enter. Convenience alias for lineEnter. Use this for single-line karaoke displays.
plugin:lyrics:lineEnter{ text: string; [key: string]: unknown }Fires when a cue becomes active. Mirrors the CueTracker enter event.
plugin:lyrics:lineExit{ text: string; [key: string]: unknown }Fires when a cue goes inactive. Use to dim or remove the highlight from the previous line.
TypeScript
player.on('plugin:lyrics:lineEnter', ({ text }) => {
highlightLine(text);
});

player.on('plugin:lyrics:lineExit', ({ text }) => {
dimLine(text);
});

Inside a plugin body you can use the class form: this.on(LyricsPlugin, 'lineEnter', ...). That form is protected and is not available on an external player.on() call.

Methods

current()

TypeScript
current(): { text: string; [key: string]: unknown } | undefined

Returns the currently active line payload, or undefined when no line is active (between cues or before lyrics are loaded).

TypeScript
const lyricsPlugin = player.getPlugin(LyricsPlugin)!;
const activeLine = lyricsPlugin.current();

if (activeLine) {
console.log(activeLine.text);
}

all()

TypeScript
all(): ReadonlyArray<Cue<{ text: string; [key: string]: unknown }>>

All cues for the current track. Returns an empty array if no lyrics are loaded. Use this to render a full lyric sheet before playback reaches each line.

TypeScript
import type { Cue } from '@nomercy-entertainment/nomercy-player-core';

const lyricsPlugin = player.getPlugin(LyricsPlugin)!;
const cues = lyricsPlugin.all();

cues.forEach((cue) => {
console.log(`[${cue.start}s] ${cue.payload.text}`);
});

clear()

TypeScript
clear(): void

Tears down the active cue tracker and clears all lyric state without disposing the plugin. The plugin keeps listening for the next current event, so the next track auto-fetches as usual when autoFetch is true.

fetchLyrics(url)

TypeScript
fetchLyrics(url: string): Promise<CueList<{ text: string; [key: string]: unknown }> | undefined>

Explicitly fetch and attach lyrics from a URL. Calling this takes precedence over the auto-fetch from track.lyricsUrl. Returns the parsed cue list, or undefined if the fetch or parse failed. On failure the plugin surfaces the error via player.on('warning', ...) with code 'plugin:lyrics/fetch-failed' (network error) or 'plugin:lyrics/no-parser' (no parser registered for that file extension).

TypeScript
import type { CueList } from '@nomercy-entertainment/nomercy-player-core';

const lyricsPlugin = player.getPlugin(LyricsPlugin)!;

const cueList = await lyricsPlugin.fetchLyrics(
'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Music/D/Derek%20Clegg/%5B2010%5D%20KJC/01%20Thaw%20You%20Out.lrc',
);

if (cueList) {
console.log(`Loaded ${cueList.cues.length} cues`);
}

Registration

Minimal registration, relying on track.lyricsUrl for the URL:

TypeScript
import nmMPlayer from '@nomercy-entertainment/nomercy-music-player';
import { LyricsPlugin } from '@nomercy-entertainment/nomercy-music-player/plugins';

const player = nmMPlayer('main')
.addPlugin(LyricsPlugin)
.setup({
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Music',
playlist: [
{
id: 'kjc-01',
name: 'Thaw You Out',
url: '/D/Derek%20Clegg/%5B2010%5D%20KJC/01%20Thaw%20You%20Out.mp3',
lyricsUrl: '/D/Derek%20Clegg/%5B2010%5D%20KJC/01%20Thaw%20You%20Out.lrc',
artist: 'Derek Clegg',
},
],
});

With a custom URL resolver when your lyric files live at a pattern the track object does not capture on its own:

TypeScript
import nmMPlayer from '@nomercy-entertainment/nomercy-music-player';
import { LyricsPlugin } from '@nomercy-entertainment/nomercy-music-player/plugins';
import type { MusicPlaylistItem } from '@nomercy-entertainment/nomercy-music-player';

const player = nmMPlayer('main')
.addPlugin(LyricsPlugin, {
getLyricsUrl: (track: MusicPlaylistItem) =>
`https://api.example.com/lyrics/${track.id}.lrc`,
})
.setup({ playlist: myTracks });

Full example with a lyric sheet and active-line highlight:

TypeScript
import nmMPlayer from '@nomercy-entertainment/nomercy-music-player';
import { AutoAdvancePlugin, LyricsPlugin } from '@nomercy-entertainment/nomercy-music-player/plugins';

const player = nmMPlayer('main')
.addPlugin(AutoAdvancePlugin)
.addPlugin(LyricsPlugin, { autoFetch: true })
.setup({
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Music',
playlist: [
{
id: 'kjc-01',
name: 'Thaw You Out',
url: '/D/Derek%20Clegg/%5B2010%5D%20KJC/01%20Thaw%20You%20Out.mp3',
lyricsUrl: '/D/Derek%20Clegg/%5B2010%5D%20KJC/01%20Thaw%20You%20Out.lrc',
artist: 'Derek Clegg',
},
],
});

const lyricsEl = document.getElementById('lyrics')!;

// Build the full lyric sheet as soon as the first line fires:
player.on('plugin:lyrics:lineEnter', () => {
if (lyricsEl.children.length === 0) {
const lyricsPlugin = player.getPlugin(LyricsPlugin)!;
const cues = lyricsPlugin.all();

lyricsEl.innerHTML = cues
.map((cue, index) => `<p data-index="${index}">${cue.payload.text}</p>`)
.join('');
}
});

// Scroll and highlight the active line:
player.on('plugin:lyrics:lineEnter', ({ text }) => {
lyricsEl.querySelector('.active')?.classList.remove('active');

const lineEls = lyricsEl.querySelectorAll<HTMLElement>('p');

for (const lineEl of lineEls) {
if (lineEl.textContent === text) {
lineEl.classList.add('active');
lineEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
break;
}
}
});

player.on('ready', () => {
player.item(0, { autoplay: true });
});

See also