Architecture
The Player SDK is organized into five layers so that generic infrastructure never gets tangled up with your application-specific code. Each layer only knows about the layer directly below it, and it communicates upward through defined ports. That discipline is what lets you swap a backend, replace a UI plugin, or write a consumer plugin that talks to SignalR without any of those changes leaking into the shared core.
The five layers
No layer may reach “down” past the layer below it or “up” into a layer above it except through a defined port. Listed highest to lowest:
Everything: NoMercy server endpoints, SignalR hubs, auth tokens, watch-party logic, business rules.
Generic behaviour: EQ, Lyrics, AutoAdvance, UI. No knowledge of any server API.
Domain types (VideoPlaylistItem, MusicPlaylistItem) and domain methods. Builds on the core engine.
Generic contracts: lifecycle, queue, auth pipeline, plugin system, event bus, i18n, storage, and a set of adapter ports.
Browser media APIs only: the video/audio elements, MediaSource, AudioContext.
Why this matters
The boundary between layer 4 and layer 5 is the enforcement point. Anything NoMercy-specific lives at layer 5. Built-in plugins (layer 4) must work for any consumer, with any backend, without any knowledge of the NoMercy infrastructure.
The practical consequence: when you add a built-in plugin from the package, it does not import from your app. When you write a consumer plugin, it can import from anywhere, but it should not modify core internals.
Adapter ports
Layer 2 (core) exposes a set of named adapter ports.
Each port has a typed interface and a default implementation.
You replace a port only when you need different behavior, for example swapping LocalStorageBackend for IndexedDBBackend, or replacing the browser network monitor with a Capacitor native adapter.
The video and music library layers add ports of their own on top of these, documented with each library.
See Core Adapters for the full port catalog.
Plugin layering in practice
A typical app setup:
import { nmplayer } from '@nomercy-entertainment/nomercy-video-player';
import { DesktopUiPlugin, OctopusPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';
// DesktopUiPlugin → layer 4 (built-in, generic UI)
// OctopusPlugin → layer 4 (built-in, generic subtitle renderer)
import { MyNoMercyPlugin } from '@/player/plugins/nomercy-connect';
// MyNoMercyPlugin → layer 5 (consumer, knows about SignalR hub URLs)
const player = nmplayer('main').setup({ ... });
player
.addPlugin(DesktopUiPlugin) // layer 4
.addPlugin(OctopusPlugin, { ... }) // layer 4
.addPlugin(MyNoMercyPlugin, { ... }); // layer 5
Layer-5 plugins can listen to events from layer-4 plugins using the typed class-form:
import { EqualizerPlugin } from '@nomercy-entertainment/nomercy-player-core';
// Inside MyNoMercyPlugin.use():
this.on(EqualizerPlugin, 'band:changed', ({ band }) => {
this.syncEqToServer(band);
});
Three-package layout
| npm package | Layer | Consumers |
|---|---|---|
@nomercy-entertainment/nomercy-player-core@beta | 2 (core) + 4 (built-in plugins) | Library authors, advanced consumers writing plugins directly against the core |
@nomercy-entertainment/nomercy-video-player@beta | 3 + 4 | Anyone building a video player |
@nomercy-entertainment/nomercy-music-player@beta | 3 + 4 | Anyone building a music player |
Both library packages re-export everything from the core, so consumers of the video or music player do not need to install the core separately unless they are writing core-level plugins.
Container class system boundary
State classes on .nomercyplayer (the player container element) are applied by the core’s container-class-emit mixin. They are bare class names with no prefix: playing, paused, stopped, ended, buffering, loading, muted, fullscreen, pip, theater, and active / inactive for pointer activity.
These are core-level, present regardless of which UI plugin is active.
The UI overlay (controls bar, seek bar, menus, spinner) is DesktopUiPlugin’s domain.
It reads the core’s state classes and adds its own DOM into the container.
DesktopUiPlugin never modifies the core state classes; it only reads them.
This boundary is intentional: a consumer can build a completely custom UI plugin that responds to the same state classes without touching DesktopUiPlugin.
The state source (core) and the UI renderer (plugin) are decoupled.
Mixin composition
The player prototype is assembled at class definition time from a set of mixins via composeMixins.
Each mixin owns one surface:
composeMixins(PlayerClass.prototype,
lifecycleMethods, // setup, dispose, ready, phase
transportMethods, // play, pause, stop, next, previous, rewind, forward, restart
timeMethods, // time, duration, buffered, timeData, playbackRate
volumeMethods, // volume, mute, unmute, toggleMute, volumeUp, volumeDown
queueMethods, // queue, queueAppend, current, backlog, and the rest
loadingMethods, // load, loadQueue
mediaTracksMethods, // subtitles, audioTracks, qualityLevels, chapters
// and more
);
This is why a fix in volumeMethods lands once and applies identically to both the music and video player.