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
// 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:
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.
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:
| Method | Effect |
|---|---|
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:
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():
// 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:
// 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:
| Event | Payload | When |
|---|---|---|
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:
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
// 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>>:
| Field | Type | Meaning |
|---|---|---|
data | TData | Possibly mutated by listeners |
prevented | boolean | true if any listener prevented or a delay rejected |
reason | string | 'listener-prevented' | 'delay-rejected' | 'delay-timeout' |
cause | unknown | Underlying error when a delay rejected |
Event naming conventions
| Context | Pattern | Example |
|---|---|---|
| Player core events | Bare camelCase | play, pause, ready, time |
| Before-action events | before<Action> | beforePlay, beforeLoad, beforeSeek |
| Post-prevent events | <action>Prevented | playPrevented, loadPrevented |
| Plugin events | plugin:<id>:<event> (auto-applied by this.emit) | plugin:equalizer:band:changed |
| Subsystem events | <subsystem>:<event> | backend:changed, auth:refreshed |
| Setup stage events | Bare camelCase | setupStart, 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):
| Event | Payload | Category |
|---|---|---|
ready | void | Lifecycle |
setupStart | { container: HTMLElement } | Lifecycle |
dispose | void | Lifecycle |
phase | { from: PlayerPhase; to: PlayerPhase } | Lifecycle |
play | ActionOptions | Transport |
pause | ActionOptions | Transport |
stop | ActionOptions | Transport |
ended | void | Transport |
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 |
queue | ReadonlyArray<T> | Queue |
queue:exhausted | void | Queue |
volume | { level: number } | Playback state |
mute | { muted: boolean } | Playback state |
error | PlayerErrorEvent | Errors |
fatal | PlayerErrorEvent | Errors |
warning | PlayerErrorEvent | Errors |
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:metrics | PlaybackMetrics | Metrics |