Skip to content

Event System

The player’s event bus is fully typed. Every event name maps to a concrete payload type through BaseEventMap (and its library-level extensions VideoEventMap, MusicEventMap).

Subscribing

TypeScript
// Read: payload type is inferred from the event name
player.on('time', ({ time }) => updateSeekBar(time));

// One-shot
player.once('ready', () => console.log('player ready'));

// Unsubscribe
const handler = ({ time }: { time: number }) => updateSeekBar(time);
player.on('time', handler);
player.off('time', handler);

All three methods are identical in shape:

TypeScript
interface Player {
on<K extends keyof EventMap>(event: K, handler: (payload: EventMap[K]) => void): void;
off<K extends keyof EventMap>(event: K, handler: (payload: EventMap[K]) => void): void;
once<K extends keyof EventMap>(event: K, handler: (payload: EventMap[K]) => void): void;
}

Cancellable before* events

Before-action events fire before the action executes. A listener can prevent the action, mutate the data, or async-gate execution.

TypeScript
player.on('beforePlay', (event) => {
if (!userIsLoggedIn()) {
event.preventDefault(); // cancel the play
}
});

player.on('beforeLoad', async (event) => {
// Redirect to a different item
event.data.item = await fetchLocalized(event.data.item.id);

// Gate on an async operation, player waits for this promise
event.delay(
new Promise((resolve) => {
confirmDialog('Play this?', resolve);
}),
);
});

BeforeEvent<TData> methods:

MethodEffect
preventDefault()Skip the default action and its post-action event
isDefaultPrevented()Check if any listener prevented the action
stopImmediatePropagation()Skip remaining listeners on this event (does not prevent default)
isPropagationStopped()Check if propagation was stopped
delay(promise)Gate the action on a promise; one rejection = preventDefault
isDelayed()Check if a delay was registered

When an action is prevented, a paired *Prevented event fires:

TypeScript
player.on('playPrevented', ({ reason }) => {
// reason: 'listener-prevented' | 'delay-rejected' | 'delay-timeout'
});

Cross-plugin events

Plugin events are namespaced as plugin:<id>:<event>. External consumer code listens via the string form on player.on():

TypeScript
// External consumer, string form (the only form available on player.on):
player.on('plugin:equalizer:band:changed', ({ band }) => {
// band payload shape matches EqualizerEvents['band:changed']
console.log(`${band.frequency} Hz → ${band.gain} dB`);
});

Inside a plugin body, the class-form provides full type inference via the plugin’s E generic. This form is protected, so it is only available inside Plugin subclasses, not on an external player.on() call:

TypeScript
// Inside a plugin body only (this.on is protected, not player.on):
import { EqualizerPlugin } from '@nomercy-entertainment/nomercy-player-core';

this.on(EqualizerPlugin, 'band:changed', ({ band }) => {
// Typed from EqualizerPlugin's E generic, band is EqBand
});

Plugin events auto-namespaced to plugin:<id>:<event>. The base class auto-fires these for every plugin:

EventPayloadWhen
plugin:<id>:installed{ id: string; version: string }After use() resolves
plugin:<id>:enabled{ id: string }After enable()
plugin:<id>:disabled{ id: string; reason: string }After disable()
plugin:<id>:opts:changed{ id: string; opts: object }After options(patch)
plugin:<id>:disposed{ id: string }After dispose()
plugin:<id>:failed{ id: string; error: unknown }When use() rejects

Inside plugins: this.on

Inside a plugin, always use this.on(...) instead of player.on(...). The this.on form auto-disposes all listeners when the plugin disposes:

TypeScript
class MyPlugin extends Plugin<...> {
use(): void {
this.on('time', ({ time }) => this.update(time)); // auto-disposed
this.on(EqualizerPlugin, 'band:changed', ({ band }) => { // auto-disposed
this.syncEq(band);
});
}
}

this.off, this.once, and this.hasListeners have the same overloaded signatures as the player methods.

Emitting from plugins

TypeScript
// Standard emit, auto-namespaced to plugin:<id>:<name>
this.emit('dataFetched', { count: 5 });

// Cancellable dispatch with mutation
const result = await this.dispatchBefore('beforeFetch', { url: targetUrl });
if (result.prevented) return;
await this.doFetch(result.data.url);
this.emit('fetched', { url: result.data.url });

dispatchBefore returns Promise<BeforeDispatchResult<TData>>:

FieldTypeMeaning
dataTDataPossibly mutated by listeners
preventedbooleantrue if any listener prevented or a delay rejected
reasonstring'listener-prevented' | 'delay-rejected' | 'delay-timeout'
causeunknownUnderlying error when a delay rejected

Event naming conventions

ContextPatternExample
Player core eventsBare camelCaseplay, pause, ready, time
Before-action eventsbefore<Action>beforePlay, beforeLoad, beforeSeek
Post-prevent events<action>PreventedplayPrevented, loadPrevented
Plugin eventsplugin:<id>:<event> (auto-applied by this.emit)plugin:equalizer:band:changed
Subsystem events<subsystem>:<event>backend:changed, auth:refreshed
Setup stage eventsBare camelCasesetupStart, pluginsRegistered, playlistReady

Events fire after the state change they describe. Pre-action events use before<X> naming.

BaseEventMap reference

Key events from BaseEventMap (shared by both video and music players):

EventPayloadCategory
readyvoidLifecycle
setupStart{ container: HTMLElement }Lifecycle
disposevoidLifecycle
phase{ from: PlayerPhase; to: PlayerPhase }Lifecycle
playActionOptionsTransport
pauseActionOptionsTransport
stopActionOptionsTransport
endedvoidTransport
time{ time: number }Time
seeked{ time: number }Time
duration{ duration: number }Time
progress{ time: number; duration: number; percentage: number }Time
current{ item: T | undefined; index: number }Queue
queueReadonlyArray<T>Queue
queue:exhaustedvoidQueue
volume{ level: number }Playback state
mute{ muted: boolean }Playback state
errorPlayerErrorEventErrors
fatalPlayerErrorEventErrors
warningPlayerErrorEventErrors
playlistResolving{ url: string }Setup
playlistReady{ length: number }Setup
playlistError{ url: string; error: Error; code: string }Setup
auth:refreshed{ tokenAcquiredAt: number }Auth
auth:failed{ error: PlayerErrorEvent['error'] }Auth
playback:metricsPlaybackMetricsMetrics