Core Adapter Ports
We provide the adapter pattern so you can change how the player behaves whenever you have a need that’s a bit unique, something that shouldn’t be the default for everyone. Every piece here, like where state gets saved, how the player talks to a server, or which clock it reads, already has a sensible default wired in. You only reach for an adapter when that default doesn’t fit you.
Say you want the player to remember resume positions and volume on your own server instead of in the browser, so they follow your users from their phone to their TV. You don’t fork anything or go digging through player internals for that. You write a small storage adapter that reads and writes against your API, hand it to setup(), and everything else carries on exactly as before. The player never knows where the bytes actually go, it just asks the port and gets an answer.
That’s really all a port is: a clearly defined slot with a default sitting in it. Swapping what’s in the slot is how you make the player yours without making it everyone’s.
This is also the line between an adapter and a plugin. If you’re changing how something the player already does works on the inside, that’s a port swap. If you’re adding something new for the player to do, like an equalizer or a skip button, that’s a plugin. Plugins and adapters covers when to reach for each.
The core ships the ports below. Libraries built on the core add ports of their own, documented with whichever library owns them.
How to wire a port
All ports are wired through setup():
import {
LocalStorageBackend,
MemoryStorageBackend,
} from '@nomercy-entertainment/nomercy-player-core';
import { browserPlatform } from '@nomercy-entertainment/nomercy-player-core';
player.setup({
storage: new MemoryStorageBackend(), // swap LocalStorage for in-memory
platform: {
...browserPlatform,
wakeLock: myNativeWakeLock, // override one sub-port, keep the rest
},
logger: myPinoLogger, // wire a Pino adapter
});
Core adapter catalog
Core infrastructure
| Port | Config key | Interface | Default | Replace when |
|---|---|---|---|---|
| Storage | storage | IStorage | LocalStorageBackend | Need IndexedDB, remote persistence, or ephemeral test storage |
| Logger | logger | ILogger | Console logger | Routing logs to Pino, Winston, or a remote sink |
| Id generator | (internal) | IIdGenerator | crypto.randomUUID() | Deterministic ids in tests, or a different id scheme |
| Event bus | (internal) | IEventBus | Default emitter | Custom event routing or debugging instrumentation |
| Lifecycle registry | (internal) | ILifecycleRegistry | Default registry | Custom plugin ordering or lifecycle observability |
Auth and fetch
| Port | Config key | Interface | Default | Replace when |
|---|---|---|---|---|
| Auth config | auth | AuthConfig | None (unauthenticated) | Your media sources require authorization |
| Fetch | (internal) | IFetch | globalThis.fetch | Injecting a mock fetch in tests or a custom fetch polyfill |
| Retry policy | (internal) | IRetryPolicy | Per-code policy map; default is no retry ({ attempts: 0 }) | Retry is applied per request inside authFetch |
Platform abstraction
The platform config key accepts an IPlatform bundle. Override individual sub-ports using spread:
platform: { ...browserPlatform, wakeLock: myNativeWakeLock }
| Sub-port | Interface | Default | Replace when |
|---|---|---|---|
| Wake lock | IPlatform.wakeLock | Browser Wake Lock API | Capacitor, native shell |
| Network monitor | IPlatform.network | Navigator connection API | Capacitor Network plugin |
| Visibility monitor | IPlatform.visibility | Page Visibility API | Non-standard environments |
| Capabilities probe | IPlatform.capabilities | MediaCapabilities API | Custom format support detection |
| Fullscreen controller | IPlatform.fullscreen | Document Fullscreen API | Native fullscreen (video only) |
| PiP controller | IPlatform.pip | HTMLVideoElement PiP | Native PiP (video only) |
Streaming
| Port | Config key | Interface | Default | Replace when |
|---|---|---|---|---|
| Stream registry | (internal) | IStreamRegistry | HLS + native stream sources | Adding a DASH or custom stream adapter |
i18n
| Port | Config key | Interface | Default | Replace when |
|---|---|---|---|---|
| Translator | translations / loadTranslations | ITranslator | Built-in BCP-47 string resolver | Custom translation backend (i18next, Fluent) |
| Language matcher | (internal) | ILanguageMatcher | BCP-47 matching | Stricter locale matching |
| Translation loader | loadTranslations | TranslationLoader | JSON file loader | Lazy load from CDN, merge from multiple sources |
Subtitles and cues
| Port | Config key | Interface | Default | Replace when |
|---|---|---|---|---|
| Cue parsers | cueParsers | ICueParser | VTT + LRC parsers | Adding SRT, TTML, or custom cue formats |
Realtime
| Port | Config key | Interface | Default | Replace when |
|---|---|---|---|---|
| Realtime channel factory | websocketFactory | IRealtimeChannel factory | Native WebSocket | SignalR, Socket.IO, or a custom realtime adapter |
Miscellaneous
| Port | Config key | Interface | Default | Replace when |
|---|---|---|---|---|
| Clock | clockSource | IClock | systemClock (Date.now()) | Distributed time sync (NTP-adjusted clock for group listening) |
| Media list | (internal) | IMediaList | Array-backed list | External data store |
| Preload strategy | preloadStrategy | IPreloadStrategy | Default lead-seconds preload | Custom buffer management |
| URL resolver | urlResolver | IUrlResolver | Base URL + category routing | Custom CDN URL signing or rewriting |
Storage backends
Three implementations ship with the core:
import {
LocalStorageBackend,
MemoryStorageBackend,
IndexedDBBackend,
} from '@nomercy-entertainment/nomercy-player-core';
// Default: persists across sessions, shared across tabs:
storage: new LocalStorageBackend();
// Ephemeral: ideal for tests; no persistence, no cross-tab state:
storage: new MemoryStorageBackend();
// High-capacity: for large datasets (EQ preset libraries, subtitle cache):
storage: new IndexedDBBackend({ dbName: 'my-player-db' });
All three implement IStorage:
interface IStorage {
get(key: string): string | null | Promise<string | null>;
set(key: string, value: string): void | Promise<void>;
remove(key: string): void | Promise<void>;
getJSON<T>(key: string): T | null | Promise<T | null>;
setJSON<T>(key: string, value: T): void | Promise<void>;
}
Methods may return values or Promises so synchronous backends (LocalStorageBackend) and asynchronous ones (IndexedDBBackend, remote API) share one interface. Plugin code always uses await regardless.
Writing a custom adapter
Implement the interface and pass the instance at setup:
import type { IStorage } from '@nomercy-entertainment/nomercy-player-core';
class RemoteStorageBackend implements IStorage {
get(key: string): string | null {
return sessionStorage.getItem(key);
}
set(key: string, value: string): void {
sessionStorage.setItem(key, value);
}
remove(key: string): void {
sessionStorage.removeItem(key);
}
getJSON<T>(key: string): T | null {
const raw = this.get(key);
if (raw === null) return null;
try { return JSON.parse(raw) as T; } catch { return null; }
}
setJSON<T>(key: string, value: T): void {
this.set(key, JSON.stringify(value));
}
}
player.setup({ storage: new RemoteStorageBackend() });
Library ports
Libraries built on the core add ports of their own. Those are documented with the library that owns them: see video configuration and music configuration.