Skip to content

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:

5
Consumer pluginYour app code

Everything: NoMercy server endpoints, SignalR hubs, auth tokens, watch-party logic, business rules.

4
Built-in pluginShipped inside the core / library packages

Generic behaviour: EQ, Lyrics, AutoAdvance, UI. No knowledge of any server API.

3
Per-librarynomercy-video-player / nomercy-music-player

Domain types (VideoPlaylistItem, MusicPlaylistItem) and domain methods. Builds on the core engine.

2
Corenomercy-player-core

Generic contracts: lifecycle, queue, auth pipeline, plugin system, event bus, i18n, storage, and a set of adapter ports.

1
BackendHtml5VideoBackend / WebAudioBackend

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:

TypeScript
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:

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

// Inside MyNoMercyPlugin.use():
this.on(EqualizerPlugin, 'band:changed', ({ band }) => {
this.syncEqToServer(band);
});

Three-package layout

npm packageLayerConsumers
@nomercy-entertainment/nomercy-player-core@beta2 (core) + 4 (built-in plugins)Library authors, advanced consumers writing plugins directly against the core
@nomercy-entertainment/nomercy-video-player@beta3 + 4Anyone building a video player
@nomercy-entertainment/nomercy-music-player@beta3 + 4Anyone 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:

TypeScript
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.