Persistence
Saving and restoring playback state across page loads: resume position, last-played track, volume level, subtitle preferences. The player core’s storage adapter system handles all of this.
Prerequisites: Core: Adapters covers the three built-in storage backends.
Storage backends
Three implementations ship with the core:
import {
LocalStorageBackend,
MemoryStorageBackend,
IndexedDBBackend,
} from '@nomercy-entertainment/nomercy-player-core';
// Default: persists across sessions, synchronous, shared across tabs:
player.setup({ storage: new LocalStorageBackend() });
// Ephemeral: ideal for tests or incognito; no persistence:
player.setup({ storage: new MemoryStorageBackend() });
// High-capacity: for large datasets (EQ preset libraries, subtitle cache):
player.setup({ storage: new IndexedDBBackend({ dbName: 'my-player-db' }) });
Built-in plugins write to storage under a per-plugin namespace (nmplayer-<plugin-id>-*). The player core itself reads and writes nothing — config.storage is consumed only by plugins, via Plugin.storage.
What persists
The core persists nothing on its own. Volume, mute, and language live in memory for the session unless you persist them — listen for the relevant events and write to storage yourself, or let a plugin do it:
EqualizerPluginpersists EQ state when you set itspersistKeyoption.SubtitleOverlayPluginpersists subtitle style preferences.
Playback position is likewise not saved automatically, because the core does not know where your server stores watch history. That requires a consumer plugin.
Saving and restoring playback position
Write a consumer plugin that saves position to your backend.
ResumePlugin is typed against NMVideoPlayer directly because it calls this.player.item() — NMVideoPlayer satisfies WithCurrentItem<VideoPlaylistItem> structurally.
For a portable alternative typed against the interface, use IVideoPlayer & WithCurrentItem<VideoPlaylistItem>.
import { Plugin } from '@nomercy-entertainment/nomercy-player-core';
import type { NMVideoPlayer, VideoPlaylistItem } from '@nomercy-entertainment/nomercy-video-player';
interface ResumeOptions {
saveIntervalMs?: number; // how often to save position. Default: 5000 (5s)
minProgressSeconds?: number; // ignore saves before this point. Default: 5
}
export class ResumePlugin extends Plugin<NMVideoPlayer, ResumeOptions, {}> {
static readonly id = 'myapp:resume';
static readonly version = '1.0.0';
static readonly description = 'Save and restore video playback position';
private saveTimer: ReturnType<typeof setInterval> | null = null;
use(): void {
const intervalMs = this.opts.saveIntervalMs ?? 5000;
const minSeconds = this.opts.minProgressSeconds ?? 5;
// Save position periodically while playing:
this.on('play', () => {
this.saveTimer = this.interval(() => {
const time = this.player.time();
const item = this.player.item();
if (item && time > minSeconds) {
this.savePosition(String(item.id), time);
}
}, intervalMs);
});
// Stop saving on pause/end:
this.on('pause', () => this.clearSaveTimer());
this.on('ended', () => this.clearSaveTimer());
// Restore position when a new item becomes current:
this.on('current', async ({ item }) => {
if (!item) return;
const saved = await this.loadPosition(String(item.id));
if (saved && saved > 5) {
// Seek to saved position after the item is loaded into the backend
this.player.once('mediaReady', () => this.player.time(saved));
}
});
}
dispose(): void {
this.clearSaveTimer();
}
private clearSaveTimer(): void {
if (this.saveTimer !== null) {
clearInterval(this.saveTimer);
this.saveTimer = null;
}
}
private async savePosition(itemId: string, time: number): Promise<void> {
// Use this.fetch to get auth headers automatically:
await this.fetch(`/api/progress/${itemId}`, {
method: 'PUT',
responseType: 'json',
body: JSON.stringify({ position: time }),
headers: { 'Content-Type': 'application/json' },
});
}
private async loadPosition(itemId: string): Promise<number | null> {
try {
const data = await this.fetch<{ position: number }>(`/api/progress/${itemId}`, {
responseType: 'json',
});
return data.position ?? null;
} catch {
return null;
}
}
}
Register it:
player.addPlugin(ResumePlugin, {
saveIntervalMs: 5000,
minProgressSeconds: 10, // don't save if the user watched less than 10 seconds
});
Local-only position saving (no server)
For a purely client-side implementation, use this.storage directly:
// Inside a plugin:
private savePosition(itemId: string, time: number): void {
this.storage.set(`resume-${itemId}`, String(time));
}
private async loadPosition(itemId: string): Promise<number | null> {
const saved = await this.storage.get(`resume-${itemId}`);
return saved !== null ? parseFloat(saved) : null;
}
this.storage reads from the same IStorage backend the player is configured with.
Keys are automatically namespaced.
Choosing the right storage backend for your context
| Scenario | Backend |
|---|---|
| Standard web app | LocalStorageBackend (default) |
| SSR (Next.js, Nuxt) | MemoryStorageBackend on server, LocalStorageBackend on client. See Advanced: SSR |
| Test suite | MemoryStorageBackend, no cross-test contamination |
| Large EQ preset library | IndexedDBBackend |
| Sync across devices | Custom backend calling your API |
IndexedDB backend
For large data (EQ snapshots, subtitle caches):
import { IndexedDBBackend } from '@nomercy-entertainment/nomercy-player-core';
player.setup({
storage: new IndexedDBBackend({ dbName: 'my-player-db' }), // database name
});
IndexedDBBackend is async internally. The IStorage interface allows async returns (Promise<string | null> etc.) so the built-in IndexedDBBackend satisfies it natively.
Writes complete asynchronously; reads return the last committed value.
For a fully async custom backend, see Advanced: Custom Adapter.
What to read next
- Core: Adapters: full
IStorageinterface and the backend catalog - Advanced: Custom Adapter: writing an async IndexedDB-Worker backend
- Recipes: Lyrics and Equalizer: EQ state is persisted through this same storage system