Skip to content

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():

TypeScript
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

PortConfig keyInterfaceDefaultReplace when
StoragestorageIStorageLocalStorageBackendNeed IndexedDB, remote persistence, or ephemeral test storage
LoggerloggerILoggerConsole loggerRouting logs to Pino, Winston, or a remote sink
Id generator(internal)IIdGeneratorcrypto.randomUUID()Deterministic ids in tests, or a different id scheme
Event bus(internal)IEventBusDefault emitterCustom event routing or debugging instrumentation
Lifecycle registry(internal)ILifecycleRegistryDefault registryCustom plugin ordering or lifecycle observability

Auth and fetch

PortConfig keyInterfaceDefaultReplace when
Auth configauthAuthConfigNone (unauthenticated)Your media sources require authorization
Fetch(internal)IFetchglobalThis.fetchInjecting a mock fetch in tests or a custom fetch polyfill
Retry policy(internal)IRetryPolicyPer-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:

TypeScript
platform: { ...browserPlatform, wakeLock: myNativeWakeLock }
Sub-portInterfaceDefaultReplace when
Wake lockIPlatform.wakeLockBrowser Wake Lock APICapacitor, native shell
Network monitorIPlatform.networkNavigator connection APICapacitor Network plugin
Visibility monitorIPlatform.visibilityPage Visibility APINon-standard environments
Capabilities probeIPlatform.capabilitiesMediaCapabilities APICustom format support detection
Fullscreen controllerIPlatform.fullscreenDocument Fullscreen APINative fullscreen (video only)
PiP controllerIPlatform.pipHTMLVideoElement PiPNative PiP (video only)

Streaming

PortConfig keyInterfaceDefaultReplace when
Stream registry(internal)IStreamRegistryHLS + native stream sourcesAdding a DASH or custom stream adapter

i18n

PortConfig keyInterfaceDefaultReplace when
Translatortranslations / loadTranslationsITranslatorBuilt-in BCP-47 string resolverCustom translation backend (i18next, Fluent)
Language matcher(internal)ILanguageMatcherBCP-47 matchingStricter locale matching
Translation loaderloadTranslationsTranslationLoaderJSON file loaderLazy load from CDN, merge from multiple sources

Subtitles and cues

PortConfig keyInterfaceDefaultReplace when
Cue parserscueParsersICueParserVTT + LRC parsersAdding SRT, TTML, or custom cue formats

Realtime

PortConfig keyInterfaceDefaultReplace when
Realtime channel factorywebsocketFactoryIRealtimeChannel factoryNative WebSocketSignalR, Socket.IO, or a custom realtime adapter

Miscellaneous

PortConfig keyInterfaceDefaultReplace when
ClockclockSourceIClocksystemClock (Date.now())Distributed time sync (NTP-adjusted clock for group listening)
Media list(internal)IMediaListArray-backed listExternal data store
Preload strategypreloadStrategyIPreloadStrategyDefault lead-seconds preloadCustom buffer management
URL resolverurlResolverIUrlResolverBase URL + category routingCustom CDN URL signing or rewriting

Storage backends

Three implementations ship with the core:

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

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

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