Skip to content

Overview

The player core ships a set of opt-in plugins that cover the capabilities every player needs, without forcing you to load anything you don’t use. None are registered automatically, so your bundle only pays for what you add. Most are re-exported from the main @nomercy-entertainment/nomercy-player-core barrel; a few use dedicated subpath imports to keep tree-shaking effective.

Each plugin entry below follows the same shape: what it does, its plugin id, import path, options table (every field from the source Options interface), events table, a registration example, and public methods where the plugin exposes them.

Main barrel exports

Import from @nomercy-entertainment/nomercy-player-core.

AudioGraphPlugin

Plugin id: 'audio-graph' | Import: import { AudioGraphPlugin, audioGraphPlugin } from '@nomercy-entertainment/nomercy-player-core'

Foundation plugin that owns the Web Audio signal chain. Every other audio plugin in the core (EqualizerPlugin, SpectrumPlugin, VisualizationPlugin, MixerPlugin) requires this one to be registered first.

On use() it creates (or reuses) an AudioContext, wraps the backend’s media element in a MediaElementAudioSourceNode, and wires the baseline chain: source → destination. Subsequent plugins extend the chain via insertEffect(). The chain topology is:

StageNode / ComponentNotes
Inputmedia element / backend outputThe raw audio source from the active backend
SourceMediaElementAudioSourceNodeWraps the media element; entry point into the Web Audio graph
Parallel tapshared AnalyserNodeRead-only branch consumed by SpectrumPlugin; does not affect the audio output
Pre-effects chainpreEffects[0..n]Inserted before post-effects, e.g. BiquadFilterNodes from EqualizerPlugin
Post-effects chainpostEffects[0..n]Inserted after pre-effects, e.g. gain/pan nodes from MixerPlugin
OutputAudioContext.destinationFinal output node; routes audio to the speaker

Options (AudioGraphOptions):

OptionTypeDefaultDescription
latencyHintAudioContextLatencyCategory'playback'Hint passed to new AudioContext({ latencyHint }). Use 'interactive' when round-trip latency matters (e.g. live instruments).
fftSize256 | 512 | 1024 | 2048 | 4096 | 8192 | 163842048FFT size for the shared AnalyserNode tapped by SpectrumPlugin. Higher = finer frequency resolution, lower time resolution.
smoothingnumber0.8Smoothing time constant (0–1) for the shared analyser. Higher values make spectrum output lag behind transients.

Events (AudioGraphEvents):

EventPayloadWhen
context:ready{ sampleRate: number }AudioContext created and baseline chain wired
context:closedvoidContext closed on dispose or browser suspend
chain:rebuiltvoidChain reconnected after an effect insert or remove
unsupported{ reason: string }AudioContext not available; plugin will not activate

Registration example:

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

player.addPlugin(AudioGraphPlugin, { latencyHint: 'playback', fftSize: 2048 });

Public methods:

MethodSignatureDescription
context()() => AudioContextReturns the player’s AudioContext. Creates one if called before use().
analyserSource()() => AnalyserNodeReturns the shared AnalyserNode tapped parallel to the source. Created on first call and reused.
outputNode()() => AudioNodeReturns the last node in the effect chain, wired directly to destination.
insertEffect()(node: AudioNode, position?: 'pre' | 'post') => AudioNodeAppends an effect node and triggers a chain rebuild. 'pre' inserts before post-effects (e.g. EQ); 'post' is default.
removeEffect()(node: AudioNode) => voidRemoves an effect node and rebuilds the chain.
pre()(node: AudioNode) => AudioNodeShorthand for insertEffect(node, 'pre').
post()(node: AudioNode) => AudioNodeShorthand for insertEffect(node, 'post').
route()(from: AudioNode, to: AudioNode) => voidManually connects two nodes outside the managed chain. Connections are cleaned up on dispose.
unroute()(from: AudioNode, to: AudioNode) => voidDisconnects a previously routed node pair.

CanvasPlugin

Plugin id: 'canvas' | Import: import { CanvasPlugin, canvasPlugin } from '@nomercy-entertainment/nomercy-player-core'

Mounts a single <canvas> into the player container and runs the shared RAF render loop. Visualization plugins register per-frame callbacks via addRenderer() rather than managing their own canvases. Without this plugin no canvas is created and no animation frames are requested, so there is zero cost when not used.

Options (CanvasOptions):

OptionTypeDefaultDescription
mountstring | HTMLElementplayer containerWhere to mount the canvas. Accepts a CSS selector or an element reference.
widthnumbercontainer clientWidthFixed logical width in CSS pixels. Setting both width and height disables ResizeObserver tracking.
heightnumbercontainer clientHeightFixed logical height in CSS pixels. See width.
fpsnumber60Declared but not currently read — the render loop runs at the display refresh rate and does not cap to this value.
pixelRationumberdevicePixelRatioBitmap resolution multiplier. Pass 2 for Retina output.
compositeMode'clear' | 'composite''clear''clear' clears the canvas at the start of each frame. 'composite' lets renderers accumulate on top of each other.

Events (CanvasEvents):

EventPayloadWhen
mounted{ width: number; height: number }After use() mounts the canvas and applies the initial size
resized{ width: number; height: number }Whenever the canvas is resized (ResizeObserver or size(w, h))
frame{ deltaMs: number; time: number }After all registered renderers have run on each RAF tick

Registration example:

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

player.addPlugin(CanvasPlugin, { fps: 60, compositeMode: 'clear' });

const canvas = player.getPlugin(CanvasPlugin);
const stop = canvas.addRenderer((ctx, deltaMs) => {
ctx.fillStyle = '#ff0000';
ctx.fillRect(0, 0, 10, 10);
});
// Remove when no longer needed:
stop();

Public methods:

MethodSignatureDescription
canvas()() => HTMLCanvasElementReturns the mounted <canvas> element. Throws before use().
context()() => CanvasRenderingContext2DReturns the cached 2D rendering context.
addRenderer()(fn: CanvasRenderFn) => () => voidRegister a per-frame render callback. Returns an unregister function.
removeRenderer()(fn: CanvasRenderFn) => voidRemove a previously registered callback.
size()() => { width: number; height: number }Read the current logical canvas size.
size(w, h)(width: number, height: number) => voidWrite a manual size override. Emits resized.
resize()() => voidForce a resize recalculation from the mount element’s clientWidth/clientHeight.

CastSenderPlugin

Plugin id: 'cast-sender' | Import: import { CastSenderPlugin, castSenderPlugin } from '@nomercy-entertainment/nomercy-player-core'

Chromecast Web Sender SDK bridge. Forwards current, play, pause, stop, seek, volume, and mute player events to the active Cast session via RemotePlayerController. Mirrors receiver state back to the player. In non-Chromium browsers (or when the Cast SDK is absent) the bridge stays passive, so connect() throws BrowserPolicyError and all forwarders no-op.

This is a base class. The video and music player packages each ship a thin subclass that overrides defaultContentType() and buildMetadata() for their media type.

Options (CastSenderOptions):

OptionTypeDefaultDescription
chromecastAppIdstringnoneChromecast app id passed to the Cast SDK.
enableAirPlaybooleannoneWhether AirPlay is allowed (Safari only).
customReceiverNamespacestringnoneCustom receiver namespace for messages.
resumeLocalOnDisconnectbooleantrueResume local playback after the receiver disconnects, restoring the receiver’s last time and play/pause state.
defaultContentTypestringsubclass defaultContent type when the playlist item lacks mime/contentType.
livebooleanfalseTreat the source as a live (unbounded) stream.

Events (CastSenderEvents):

EventPayloadWhen
cast:connected{ deviceName: string }Session established
cast:disconnectedvoidSession ended (by user or receiver)
cast:error{ error: Error }SDK error during connect or media load
cast:remote-state{ time: number; state: 'playing' | 'paused' | 'buffering' }Receiver play/pause state changed
cast:media-changed{ contentId: string }Receiver loaded a different content id than local
unsupported{ reason: string }Cast SDK absent (non-Chromium browser)

Registration example:

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

player.addPlugin(CastSenderPlugin, {
chromecastAppId: 'YOUR_APP_ID',
resumeLocalOnDisconnect: true,
});

const cast = player.getPlugin(CastSenderPlugin);
await cast.connect(); // opens device picker

Public methods:

MethodSignatureDescription
isConnected()() => booleanReturns true while a Cast session is established.
connect()() => Promise<void>Opens the Cast device picker. Emits cast:connected on success. Throws BrowserPolicyError when the SDK is absent.
disconnect()() => voidEnds the current session. Emits cast:disconnected.

EqualizerPlugin

Plugin id: 'equalizer' | Import: import { EqualizerPlugin, equalizerPlugin } from '@nomercy-entertainment/nomercy-player-core'

10-band parametric EQ with pre-gain, built-in named presets, custom presets, and persistence. Inserts a GainNode (pre-gain) followed by 10 peaking BiquadFilterNodes into the audio graph as 'post' effects. Requires AudioGraphPlugin.

Options (EqualizerOptions):

OptionTypeDefaultDescription
bandsReadonlyArray<EqBand>10-band layout + pre-gainBand layout. Index 0 must be { frequency: 'Pre', gain }.
presetstringnoneNamed preset to apply immediately on use().
presetsReadonlyArray<EqPreset>noneAdditional or replacement presets merged into the catalogue.
sliderValuesEqSliderValuesbuilt-in ±12 dB rangesOverride min/max/step ranges for slider helper methods.
persistKeystringnoneStorage key for automatic persistence of bands, active preset, and custom presets.
autoLoadbooleantrueRead persisted state on use(). Set to false to always start from bands/preset opts.
autoSavebooleantrue when persistKey setPersist state after every band() / preGain() / preset() call.
smoothingTimeConstantSecondsnumber0.05Time constant (seconds) for AudioParam.setTargetAtTime gain ramps.

Events (EqualizerEvents):

EventPayloadWhen
readyvoidFilter chain wired and initial state applied
band:changed{ band: EqBand }Single band gain updated
preset:changed{ name: string | undefined }Preset applied or cleared (undefined after reset())
change{ bands: EqBand[]; selectedPreset: string | undefined }Full snapshot after any mutation
savedvoidState written to storage

Registration example:

TypeScript
import { AudioGraphPlugin, EqualizerPlugin } from '@nomercy-entertainment/nomercy-player-core';

player
.addPlugin(AudioGraphPlugin, { latencyHint: 'playback' })
.addPlugin(EqualizerPlugin, { persistKey: 'player-eq' });

const eq = player.getPlugin(EqualizerPlugin);
eq?.band('Pre', 3.0); // set pre-gain to +3 dB
eq?.band('Pre'); // read current pre-gain
eq?.band(32, 4.0); // set 32 Hz band to +4 dB
eq?.preset('Rock');

Public methods:

MethodSignatureDescription
bands()() => EqBand[]Snapshot of all bands. Index 0 is always the 'Pre' pseudo-band.
preGain()() => numberRead current pre-gain value.
preGain(gain)(gain: number | string) => voidSet pre-gain. Values within ±0.05 snap to 0. Clears active preset.
band(freq)(freq: EqBandFrequency) => numberRead gain for a band by frequency.
band(target)(target: EqBand) => voidSet gain using an EqBand object.
band(freq, gain)(freq: EqBandFrequency, gain: number | string) => voidSet gain for a band. Clears active preset.
q(freq)(freq: number) => numberRead the Q factor of a band.
q(freq, value)(freq: number, value: number) => voidSet the Q factor. Values below 0.0001 are clamped.
preset()() => string | undefinedRead the active preset name.
preset(target)(target: EqPreset | string) => voidApply a preset by name or object.
presets()() => EqPreset[]All available presets (built-ins + custom + opts.presets).
addCustomPreset(preset)(preset: EqPreset) => voidAdd or replace a runtime custom preset.
removePreset(name)(name: string) => voidRemove a custom preset by name.
reset()() => voidReset all bands to initial values and clear the active preset.
save()() => voidExplicitly write state to storage. No-op without persistKey.
restore()() => voidRead and apply persisted state immediately. No-op without persistKey.
sliderValues()() => EqSliderValuesActive slider range config for pre-gain and frequency bands.
bandSliderMin(freq)(freq: EqBandFrequency) => numberMinimum raw gain for a range input.
bandSliderMax(freq)(freq: EqBandFrequency) => numberMaximum raw gain for a range input.
bandSliderStep(freq)(freq: EqBandFrequency) => numberStep increment for a range input.
bandSliderValue(freq)(freq: EqBandFrequency) => numberCurrent band gain mapped to a 0–100 percentage for a range input.

MixerPlugin

Plugin id: 'mixer' | Import: import { MixerPlugin, mixerPlugin } from '@nomercy-entertainment/nomercy-player-core'

Master gain (GainNode) and stereo pan (StereoPannerNode) stage in the audio graph. Sits at the end of the chain just before AudioContext.destination. Requires AudioGraphPlugin.

Options (MixerOptions):

OptionTypeDefaultDescription
gainnumber0Initial gain in dB. 0 = unity gain. Positive boosts, negative attenuates.
pannumber0Initial stereo pan. -1 = full left, 0 = centre, 1 = full right. Values outside ±1 are clamped.
persistKeystringnoneStorage key for automatic persistence of gain, pan, and mute state.
maxGainDbnumber24Symmetric dB ceiling. Calls outside ±maxGainDb are silently clamped.
smoothingTimeConstantSecondsnumber0.02Time constant (seconds) for AudioParam.setTargetAtTime gain ramps.

Events (MixerEvents):

EventPayloadWhen
gain:changed{ gain: number }Gain set (clamped dB value)
pan:changed{ pan: number }Pan set (clamped ±1 value)
mute:changed{ muted: boolean }Mute state toggled
savedvoidState written to storage

Registration example:

TypeScript
import { AudioGraphPlugin, MixerPlugin } from '@nomercy-entertainment/nomercy-player-core';

player
.addPlugin(AudioGraphPlugin)
.addPlugin(MixerPlugin, { gain: 0, persistKey: 'player-mixer' });

const mixer = player.getPlugin(MixerPlugin);
mixer?.gain(6); // +6 dB boost
mixer?.gain(); // read current gain in dB
mixer?.pan(-0.5); // pan slightly left
mixer?.muted(true); // mute

Public methods:

MethodSignatureDescription
gain()() => numberRead current gain in dB.
gain(dB)(dB: number) => voidSet master gain. Values outside ±maxGainDb are clamped. Emits gain:changed.
pan()() => numberRead current pan (-1..1).
pan(value)(value: number) => voidSet stereo pan. Values outside ±1 are clamped. Emits pan:changed.
muted()() => booleanReturns true when the mixer is muted.
muted(value)(value: boolean) => voidSet mute state. Ramps gain to 0 or back. Emits mute:changed.
save()() => voidExplicitly write state to storage. No-op without persistKey. Emits saved.

SpectrumPlugin

Plugin id: 'spectrum' | Import: import { SpectrumPlugin, spectrumPlugin } from '@nomercy-entertainment/nomercy-player-core'

FFT analyser that produces per-frame frequency, waveform, and band-energy data. Acquires the shared AnalyserNode from AudioGraphPlugin and emits a frame event on every RAF tick. VisualizationPlugin subclasses consume these frames. Requires AudioGraphPlugin.

Options (SpectrumOptions):

OptionTypeDefaultDescription
fftSize512 | 1024 | 2048 | 4096inherits from AudioGraphPlugin (usually 2048)FFT size for the AnalyserNode. Higher = finer frequency resolution, lower time resolution.
smoothingTimeConstantnumberinherits from shared analyser (usually 0.8)Smoothing 0–1. Higher values make spectrum output lag behind transients.
frameRatenumberRAF rateReserved for future frame-rate pacing control. Currently unused.

Events (SpectrumEvents):

EventPayloadWhen
frame{ frame: VisualizationFrame; energy: { bass: number; mid: number; treble: number } }Every RAF tick
opts:changedSpectrumOptionsAfter options(partial) is called on this plugin

Registration example:

TypeScript
import { AudioGraphPlugin, SpectrumPlugin } from '@nomercy-entertainment/nomercy-player-core';

player
.addPlugin(AudioGraphPlugin)
.addPlugin(SpectrumPlugin, { fftSize: 2048 });

player.on('plugin:spectrum:frame', ({ frame, energy }) => {
drawBars(frame.frequency);
updateMeter(energy.bass);
});

Public methods:

MethodSignatureDescription
analyser()() => AnalyserNodeReturns the live AnalyserNode. Throws before use().
currentFrame()() => VisualizationFrameReturns the most recent frame. Triggers an eager tick on first call.
bandEnergy(loHz, hiHz)(loHz: number, hiHz: number) => numberAverage FFT magnitude in the given frequency range, normalised to 0–1. Returns 0 when the analyser is unavailable.
registerBeatProvider(fn)(fn: () => { beat?: boolean; bpm?: number }) => voidRegister a beat/BPM provider polled every frame tick.
fftSize()() => 256 | 512 | 1024 | 2048 | 4096 | undefinedRead current FFT size.
fftSize(size)(size: 256 | 512 | 1024 | 2048 | 4096) => voidChange FFT size at runtime; reallocates buffers.
smoothingTimeConstant()() => number | undefinedRead current smoothing time constant.
smoothingTimeConstant(v)(value: number) => voidChange smoothing at runtime.

VisualizationPlugin

Plugin id: 'visualization' (base; subclasses must override) | Import: import { VisualizationPlugin } from '@nomercy-entertainment/nomercy-player-core'

Abstract base class for canvas-based audio visualizations. Subclass it, override render(ctx, frame), and register with the player. The base class handles wiring CanvasPlugin and SpectrumPlugin together so every render() call receives a fresh VisualizationFrame. Requires CanvasPlugin and SpectrumPlugin.

Options (VisualizationOptions):

OptionTypeDefaultDescription
clearBeforeRenderbooleanfalseClear the canvas before calling render(). Normally the canvas plugin handles clearing via compositeMode: 'clear'. Set this only when this specific visualizer needs an independent clear pass.
tick'frame' | 'time''frame'Render-loop pacing. Only 'frame' (driven by the canvas RAF loop) is active; 'time' is reserved for future use.

Events (VisualizationEvents):

EventPayloadWhen
unsupported{ reason: string }Emitted during use() when CanvasPlugin or SpectrumPlugin is unavailable
rendered{ frame: VisualizationFrame }After each render() call completes

Registration example:

TypeScript
import {
AudioGraphPlugin,
CanvasPlugin,
SpectrumPlugin,
VisualizationPlugin,
} from '@nomercy-entertainment/nomercy-player-core';

class MyBars extends VisualizationPlugin {
static override readonly id = 'myapp:bars';

protected override render(ctx: CanvasRenderingContext2D, frame) {
for (let i = 0; i < frame.frequency.length; i++) {
const height = (frame.frequency[i] / 255) * ctx.canvas.height;
ctx.fillStyle = `hsl(${i * 2}, 100%, 50%)`;
ctx.fillRect(i * 3, ctx.canvas.height - height, 2, height);
}
}
}

player
.addPlugin(AudioGraphPlugin)
.addPlugin(SpectrumPlugin)
.addPlugin(CanvasPlugin)
.addPlugin(MyBars);

Public methods:

MethodSignatureDescription
currentFrame()() => VisualizationFrame | undefinedReturns the most recent frame passed to render(), or undefined before the first tick.

Override hooks (protected):

HookWhen calledPurpose
render(ctx, frame)Every RAF tickRequired. Draw your visualization here.
setup(ctx)Once before first render()Optional. One-time setup (e.g. create ImageData, textures).
onResize(w, h)Canvas resizedOptional. Invalidate size-dependent caches.
onBeat(data)Beat provider firesOptional. React to beat events without re-slicing FFT data.

A working reference implementation, WaveformVisualization (id 'fillz:waveform'), ships in the same module and strokes the time-domain waveform as an oscilloscope line.

EmbedPlugin

Plugin id: 'embed' | Import: import { EmbedPlugin, embedPlugin } from '@nomercy-entertainment/nomercy-player-core'

Cross-origin postMessage bridge for embedding the player inside an <iframe>. Listens on window for inbound nm:command messages from the host page, validates origins, and dispatches to the player. Subscribes to player events and forwards them outward as nm:event messages.

Required <iframe> attributes for full functionality:

HTML
<iframe
src="..."
allow="autoplay; fullscreen; picture-in-picture; encrypted-media"
allowfullscreen
></iframe>

autoplay is load-bearing: without it MediaSessionPlugin never activates, so OS-level controls never appear.

Options (EmbedOptions):

OptionTypeDefaultDescription
allowedOriginsstring | string[]noneAllowed origin(s) for inbound nm:command messages. '*' accepts all (development only). Omitting the option rejects all inbound commands.
forwardEventsReadonlyArray<'ready' | 'play' | 'pause' | 'ended' | 'time' | 'volume' | 'mute'>['ready', 'play', 'pause', 'ended', 'time', 'volume', 'mute']Player event names to forward to the host page as nm:event messages.
applyIframeTweaksbooleantrue when inside an iframeApply iframe-appropriate UI adjustments (smaller controls, no popout button).

Inbound commands (EmbedCommand): play, pause, stop, seek (with time), volume (with level), mute, unmute, next, previous.

Outbound events (EmbedEventMessage): ready, play, pause, ended, time (with time), volume (with level), mute (with muted), error (with code+severity).

Registration example:

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

// Inside the iframe:
player.addPlugin(EmbedPlugin, {
allowedOrigins: 'https://example.com',
});

// On the host page:
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage({ type: 'nm:command', action: 'play' }, 'https://player.example.com');
window.addEventListener('message', (event) => {
if (event.data.type === 'nm:event') console.log(event.data.name, event.data);
});

Public methods:

MethodSignatureDescription
sendToHost(message)(message: EmbedEventMessage) => voidSend a structured event to the host page via window.parent.postMessage.
allowedOrigins()() => readonly string[]Read the current allowed-origins list.
allowedOrigins(origins)(origins: string | string[]) => voidReplace the allowed-origins list at runtime.

Subpath imports

These plugins are not in the main barrel. Use their dedicated subpath imports.

KeyHandlerPlugin

Plugin id: 'key-handler' | Import: import { KeyHandlerPlugin, keyHandlerPlugin } from '@nomercy-entertainment/nomercy-player-core/plugins/key-handler'

Keyboard binding router. Attaches a single keydown listener to the configured scope and dispatches events to registered combo callbacks. Keys silently no-op when the event target is an <input>, <textarea>, <select>, or contenteditable element.

Default bindings:

ComboAction
Spaceplayer.togglePlayback()
ArrowLeftplayer.rewind(5)
ArrowRightplayer.forward(5)
ArrowUpplayer.volumeUp()
ArrowDownplayer.volumeDown()
mplayer.toggleMute()
MediaPlayplayer.play()
MediaPauseplayer.pause()
MediaPlayPauseplayer.togglePlayback()
MediaStopplayer.stop() (fallback to pause())
MediaRewindplayer.rewind(5)
MediaFastForwardplayer.forward(5)
MediaTrackNextplayer.next?.()
MediaTrackPreviousplayer.previous?.()

Options (KeyHandlerOptions):

OptionTypeDefaultDescription
scope'document' | 'container' | HTMLElement'document'Where the keydown listener is attached. 'container' only fires when the player container or a child has focus.
bindingsRecord<string, (player: P) => void>noneExtra bindings merged on top of defaults. Same-combo entries here win.
extendbooleantruefalse clears defaults before applying opts.bindings. Use to build a fully custom binding set from scratch.
when(event: KeyboardEvent) => booleannoneGate predicate. Return false to suppress all key handling for that event (e.g. during modals).
cooldownMsnumber300Minimum milliseconds between consecutive fires of the same key. Set to 0 to disable.
disableMediaControlsbooleanfalseWhen true, hardware media keys (MediaPlay, etc.) are silently ignored.

Registration example:

TypeScript
import { KeyHandlerPlugin } from '@nomercy-entertainment/nomercy-player-core/plugins/key-handler';

player.addPlugin(KeyHandlerPlugin, {
scope: 'container',
bindings: {
'shift+ArrowLeft': (player) => player.rewind(30),
},
});

Public methods:

MethodSignatureDescription
bind(combo, fn)(combo: string, fn: (p: P) => void) => voidRegister a handler. Same combo replaces the previous.
unbind(combo)(combo: string) => voidRemove a handler. No-op when not present.
replace(combo, fn)(combo: string, fn: (p: P) => void) => voidSemantic alias for bind().
bindings()() => ReadonlyMap<string, (p: P) => void>Snapshot of the active binding map.
scope()() => EventTargetReturns the EventTarget the listener is attached to.

MediaSessionPlugin

Plugin id: 'media-session' | Import: import { MediaSessionPlugin, mediaSessionPlugin } from '@nomercy-entertainment/nomercy-player-core/plugins/media-session'

Bridges the player’s transport state to the operating system via navigator.mediaSession. Provides lock-screen artwork, hardware media key handling, Bluetooth remote controls, and OS Now Playing widgets. Silently no-ops in environments without navigator.mediaSession (Node, JSDOM, older WebViews).

Automatically wires on use(): current → pushes metadata; play/pause/endedplaybackState; time/seeksetPositionState. OS action handlers registered: play, pause, stop, previoustrack, nexttrack, seekbackward, seekforward, seekto.

Options (MediaSessionOptions): none. The interface is empty, reserved for future options.

Registration example:

TypeScript
import { MediaSessionPlugin } from '@nomercy-entertainment/nomercy-player-core/plugins/media-session';

player.addPlugin(MediaSessionPlugin);

Public methods:

MethodSignatureDescription
metadata()() => MediaSessionMetadata | undefinedRead the last metadata pushed to the OS.
metadata(meta)(meta: MediaSessionMetadata) => voidWrite metadata to navigator.mediaSession. Constructs a MediaMetadata object.
clearMetadata()() => voidClear navigator.mediaSession.metadata. Called automatically when the current item clears.

Override hooks (protected):

HookPurpose
getMetadata(item)Extract { title?, artist?, album? } from a playlist item. Override to map custom item shapes. Artwork is resolved separately.
addPlaybackActions()Register play, pause, stop OS action handlers.
addNavigationActions()Register previoustrack, nexttrack OS action handlers.
addSeekActions()Register seekbackward, seekforward, seekto OS action handlers.
setPlaybackState(state)Push a state to navigator.mediaSession.playbackState. Override to route to a native bridge.
updatePositionState(position)Push a position update via setPositionState. Skips when duration is not finite positive.

MessagePlugin

Plugin id: 'message' | Import: import { MessagePlugin, messagePlugin } from '@nomercy-entertainment/nomercy-player-core/plugins/message'

Toast and overlay-message surface for the player. UI plugins and consumers call show('text') to display a transient notification. The plugin manages timing and teardown automatically. The toast surface carries role="status" and aria-live="polite" for screen reader compatibility.

Two modes: transient (show, queue), which auto-hides after a duration; and persistent (displayPersistent, removePersistent), which stays until explicitly removed.

Options (MessageOptions):

OptionTypeDefaultDescription
durationMsnumber3000Default display duration in milliseconds for transient toasts. Per-message durationMs overrides this.
mountSelectorstringnoneCSS selector for an existing element in which to mount the toast surface. When absent, a <div> is created inside the player container.

Registration example:

TypeScript
import { MessagePlugin } from '@nomercy-entertainment/nomercy-player-core/plugins/message';

player.addPlugin(MessagePlugin, { durationMs: 3000 });

const msg = player.getPlugin(MessagePlugin);
msg?.show('Subtitle track loaded');
msg?.show('Buffering…', 0); // 0 ms = stays until dismissed or replaced
msg?.hide();
msg?.queue(['Loading…', { text: 'Almost there!', durationMs: 1500 }, 'Done.']);

Public methods:

MethodSignatureDescription
show(text, ms?)(text: string, ms?: number) => voidDisplay a transient toast. Replaces any current toast. ms defaults to 3000 (the durationMs option is honored by queue() / displayMessage(), not by show()).
hide()() => voidHide the current toast and cancel its timer.
displayMessage(data, ms?)(data: string | { text: string; durationMs?: number }, ms?: number) => voidCompatibility method. Prefer show() for new code.
queue(messages)(messages: ReadonlyArray<string | { text: string; durationMs?: number }>) => voidPlay a sequence of messages back-to-back. Calling again cancels the previous run.
clear()() => voidCancel any in-flight queue, hide the current toast, and reset all timers. Does not affect persistent messages.
displayPersistent(text, id)(text: string, id: string) => voidDisplay a persistent overlay. Updates text if id already exists.
removePersistent(id)(id: string) => voidRemove a persistent message by id.

TabLeaderPlugin

Plugin id: 'tab-leader' | Import: import { TabLeaderPlugin, tabLeaderPlugin } from '@nomercy-entertainment/nomercy-player-core/plugins/tab-leader'

Web Locks-based cross-tab leader election. Only one tab on the same origin holds the named lock at a time. When a second tab calls requestLock(), it queues behind the current holder; the browser hands the lock over when the first tab releases it or is closed.

Browser support: Web Locks (Chrome 69+, Firefox 96+, Safari 15.4+). In environments without navigator.locks the plugin emits unsupported and becomes a no-op, so playback is unaffected.

Options (TabLeaderOptions):

OptionTypeDefaultDescription
onLost'pause' | 'mute''pause'What to do when this tab loses leadership. 'mute' silences audio but continues playback.
handoffOnVisiblebooleantrueWhen true, attempts to reclaim leadership whenever the tab becomes visible.
getLockKey() => string() => 'nomercy-player-leader'Override the Web Locks key. Provide a function returning a per-player or per-session key to scope leadership to a specific player.

Events (TabLeaderEvents):

EventPayloadWhen
leader-acquiredvoidThis tab acquired the leader lock
leader-releasedvoidThis tab released the lock (voluntarily, or when another tab takes over)
unsupportedvoidWeb Locks API not available

Registration example:

TypeScript
import { TabLeaderPlugin } from '@nomercy-entertainment/nomercy-player-core/plugins/tab-leader';

player.addPlugin(TabLeaderPlugin, {
getLockKey: () => `nomercy-leader-${serverId}`,
onLost: 'pause',
});

const leader = player.getPlugin(TabLeaderPlugin);
leader?.isLeader(); // boolean

player.on('plugin:tab-leader:leader-released', () => player.pause());

Public methods:

MethodSignatureDescription
isLeader()() => booleanReturns true when this tab holds the leader lock.
requestLock()() => Promise<void>Request the leader lock. Resolves once granted. Returns the existing pending promise when an election is already in progress.
releaseLock()() => voidVoluntarily release the lock so another tab can take over.
requestLeadership()() => Promise<boolean>Alias for requestLock() returning true when leadership was acquired.
releaseLeadership()() => voidAlias for releaseLock().

Audio chain registration order

Audio plugins must be registered in dependency order: AudioGraphPlugin first, then the plugins that depend on it.

TypeScript
import {
AudioGraphPlugin,
CanvasPlugin,
EqualizerPlugin,
MixerPlugin,
SpectrumPlugin,
} from '@nomercy-entertainment/nomercy-player-core';

player
.addPlugin(AudioGraphPlugin, { latencyHint: 'playback' })
.addPlugin(EqualizerPlugin, { persistKey: 'player-eq' })
.addPlugin(MixerPlugin, { gain: 0 })
.addPlugin(SpectrumPlugin, { fftSize: 2048 })
.addPlugin(CanvasPlugin);

See also